Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Before starting new work, review [docs/concepts.md](docs/concepts.md) for canoni
- see `package.json`
- Releases go through the `/release` skill. Its tag push runs `.github/workflows/release.yml`, which publishes to npm.

Space names (e.g. `personal`, `politics`) are resolved via a config file - `$SCTX_CONFIG`, `$XDG_CONFIG_HOME/structured-context/config.json`, `--config <file>` param, or the nearest `.sctx.json` walking up from the working directory
Space names (e.g. `personal`, `politics`) are resolved via a config file - `$SCTX_CONFIG`, `$XDG_CONFIG_HOME/structured-context/config.json`, `--config <file>` param, or the nearest `.sctx.json` or `.sctx/config.json` walking up from the working directory. Relative paths resolve from the config root, and local plugins, schemas and map views sit in `.sctx/` (`src/config/layout.ts`)

The lefthook pre-commit hook syncs the plugin version (below), then runs biome (`--write`, re-staged) and the typecheck.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ See [docs/concepts.md](docs/concepts.md) (`sctx docs concepts`) for terminology:

## Configuration

The config file maps space names to paths and schemas, with optional plugin settings. It is found from `--config`, then `$SCTX_CONFIG`, then the nearest `.sctx.json` walking up from the working directory, then `$XDG_CONFIG_HOME/structured-context/config.json` (default `~/.config/...`). Paths resolve relative to the config file, and `includeSpacesFrom` pulls in spaces from other config files.
The config file maps space names to paths and schemas, with optional plugin settings. It is found from `--config`, then `$SCTX_CONFIG`, then the nearest `.sctx.json` or `.sctx/config.json` walking up from the working directory, then `$XDG_CONFIG_HOME/structured-context/config.json` (default `~/.config/...`). Paths resolve from the folder holding `.sctx.json` or `.sctx/`. Local plugins, schemas and map views go in `.sctx/`, and `includeSpacesFrom` pulls in spaces from other config files.

See `config.example.json` and [docs/config.md](docs/config.md) (`sctx docs config`) for the full reference, including `fieldMap`, `typeInference`, templates, filter views and plugin loading.

Expand Down
11 changes: 9 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,19 @@ const config: Config = {
const graph = await loadSpaceGraph('my-space', config, { configDir: process.cwd() });
```

Relative paths in a space's plugin config are resolved against the directory of the config file that
defined the space. A hand-assembled `Config` has no such file, so you must pass an explicit
Relative paths in a space's plugin config are resolved against the [config root](config.md#config-file-location)
of the file that defined the space. A hand-assembled `Config` has no such file, so you must pass an explicit
`configDir` to anchor them (`createSpaceContext`, `loadSpaceGraph`, and `validateFile` all accept it).
Omitting it throws rather than silently guessing the working directory — prefer absolute paths so the
anchor never matters.

## Resources

`resolveConfigPath()` returns the config's `root` along with its path. `resourceDir(root, name)` returns the
folder holding one kind of resource for that root (such as `plugins` or `map-views`), or `undefined` when
there is none. It follows the [resources folder](config.md#resources-folder) layout, so a tool that adds its
own kind of resource finds it the same way.

## Plugin render hooks

Plugins declare render formats via `render.formats` and implement `render(context, graph, options)`. The hook may return a single string or multi-file output:
Expand Down
10 changes: 7 additions & 3 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This document is the canonical reference for concepts and terminology used in th

## Space

A **space** is a named collection of nodes organised according to a schema. Spaces are the primary unit of organisation — a space has a backing format (a `space directory` or a `space on a page` file) and may be registered in a config file (`.sctx.json` or the global `config.json`) with a name for convenient access.
A **space** is a named collection of nodes organised according to a schema. Spaces are the primary unit of organisation — a space has a backing format (a `space directory` or a `space on a page` file) and may be registered in a config file with a name for convenient access.

```mermaid
flowchart TD
Expand Down Expand Up @@ -478,10 +478,14 @@ Plugins that support parsing produce raw `SpaceNode[]` results given a suitable

The `plugins` field in config is a **map** from plugin name to plugin config object. All plugin names must start with `sctx-`, but the prefix is optional in config and normalised on load. Built-in plugins take precedence and all are loaded by default. External plugin names specified in config are then resolved in order:

1. Config-adjacent: `{configDir}/plugins/{sctx-name}`
1. The config root's plugins folder: `.sctx/plugins/{sctx-name}`
2. npm: a package matching `sctx-*`

Each plugin declares a `configSchema` JSON Schema; the loader validates the config block against it before invoking the plugin. Config fields annotated with `format: 'path'` are resolved relative to `configDir` by the loader.
Each plugin declares a `configSchema` JSON Schema; the loader validates the config block against it before invoking the plugin. Config fields annotated with `format: 'path'` are resolved relative to the config root by the loader.

### Config root

The **config root** is the folder a config file's relative paths resolve from: the folder holding `.sctx.json` or `.sctx/`, or the global config's folder. Its **resources folder** (`.sctx/`, or the global config's folder) holds plugins, schemas and map view definitions. In code, `configDir` and `SpaceConfig.sourceDir` hold a config root. See [Resources folder](config.md#resources-folder).

### Dispatch contract

Expand Down
20 changes: 15 additions & 5 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,22 @@ structured-context uses the first config file it finds, in this order:

1. `--config <path>`
2. `$SCTX_CONFIG`
3. The nearest `.sctx.json`, looking in the working directory and then each parent folder
3. The nearest `.sctx.json` or `.sctx/config.json`, looking in the working directory and then each parent folder
4. `~/.config/structured-context/config.json` (or `$XDG_CONFIG_HOME/structured-context/config.json`)

It is recommended to keep a `.sctx.json` at the base of the notes it describes (for example the root of an Obsidian vault), or to use the global config. The CLI and agent sessions look in parent folders only, so run commands from inside the folder that holds the config. To use the spaces from another config file too, list it in `includeSpacesFrom`.
It is recommended to keep a local config at the base of the notes it describes (for example the root of an Obsidian vault), or to use the global config. A local config is either `.sctx.json` or `config.json` inside a `.sctx/` folder. A folder can't hold both. The CLI and agent sessions look in parent folders only, so run commands from inside the folder that holds the config. To use the spaces from another config file too, list it in `includeSpacesFrom`.

Paths in a config file are relative to the folder that holds that file. In a `.sctx.json` at a vault root, `"path": "."` is the whole vault and `"path": "notes"` is one folder.
The **config root** is the folder that holds `.sctx.json` or `.sctx/`, or the global config's folder. Relative paths in a config file resolve from its root. In a vault-root config, `"path": "."` is the whole vault and `"path": "notes"` is one folder, whichever of the two file names it uses.

### Resources folder

Files that structured-context reads besides the config go in the root's `.sctx/` folder (for the global config, its own folder), one subfolder each:

| Folder | Holds |
|---|---|
| `.sctx/plugins/` | Local [plugins](#plugins) |
| `.sctx/schemas/` | Custom schemas, found by file name (`"schema": "my_wiki.json"`) |
| `.sctx/map-views/` | Map view definitions, read by `structured-context-map-views` |

`sctx spaces` shows which config file is used and reports problems with it. `sctx spaces --json` returns the same information for tools and hooks.

Expand All @@ -27,7 +37,7 @@ sctx spaces add wiki notes --schema knowledge_wiki.json

This adds the space to the config file found from the working directory, or to the one named with `--config`. When no `.sctx.json` is found, or only the global config exists, pass `--config <folder>/.sctx.json` (creates the file) or `--global`. If the config file has comments or trailing commas, the command prints the entry to add by hand.

Custom schemas can sit anywhere. Reference them by relative path, for example `./.sctx/schemas/my_wiki.json`; `.sctx/schemas/` is the suggested folder.
Custom schemas can sit anywhere. A schema in `.sctx/schemas/` is found by its file name, before one of the same name in the root; reference others by relative path, for example `./schemas/my_wiki.json`.

See `config.example.json` for the full structure.

Expand Down Expand Up @@ -78,7 +88,7 @@ Use `plugins` to load parse plugins that read spaces from non-markdown sources.
}
```

All plugin names must start with `sctx-` (the prefix is optional in config and normalised on load). The special name `markdown` refers to the built-in markdown plugin. External plugins are resolved in order: config-adjacent (`{configDir}/plugins/{name}`), then npm. Each plugin must export a `configSchema` JSON Schema; config is validated against it on load. Fields annotated `format: 'path'` in a plugin's `configSchema` are resolved relative to the config file directory.
All plugin names must start with `sctx-` (the prefix is optional in config and normalised on load). The special name `markdown` refers to the built-in markdown plugin. External plugins are resolved in order: the root's plugins folder (`.sctx/plugins/{name}`, see [Resources folder](#resources-folder)), then npm. Each plugin must export a `configSchema` JSON Schema; config is validated against it on load. Fields annotated `format: 'path'` in a plugin's `configSchema` are resolved relative to the config root.

Plugins can also contribute schemas and partials via a `<pluginRoot>/schemas/` directory (or `"sctx": { "schemas": "..." }` in `package.json`). If a space specifies a plugin-prefixed schema (e.g. `"sctx-wardley-mapping/wardley_map.json"`), schemas are resolved statically without executing plugin code, and the plugin's code hooks are loaded when parsing or rendering.

Expand Down
2 changes: 1 addition & 1 deletion plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "structured-context",
"version": "0.23.12",
"version": "0.23.13",
"description": "Structured content authoring for Obsidian — automatic validation hooks, slash commands, and agent skills to keep markdown files compliant with your schema.",
"homepage": "https://github.com/mindsocket/structured-context",
"repository": "https://github.com/mindsocket/structured-context",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ empty or `info` only. Hooks pick up a fixed config on the next prompt. A missing
| `E_SCTX_OUTDATED` | The major.minor of `sctx --version` should match the plugin's version in `plugin/.claude-plugin/plugin.json` |
| `E_CONFIG_NOT_FOUND` | At `info` level, nothing is set up: use the setup-space skill. As an error, `--config` or `$SCTX_CONFIG` points at a missing file: correct the path or unset the variable |
| `E_TARGET_REQUIRED` | Ask where the config belongs. Usually `--config <folder holding the files>/.sctx.json`; `--global` only if the user asks |
| `E_CONFIG_AMBIGUOUS` | Ask the user which of the two config files to keep, then merge the other's spaces into it |
| `E_HOOK_FAILED` | A hook crashed and was skipped. Set `SCTX_DEBUG=1` for the session, or run the same `sctx` command by hand. Usually a problem with its own code above |

Other codes (`E_CONFIG_INVALID`, `E_INCLUDE_NOT_FOUND`, `E_SPACE_PATH_MISSING`, `E_SCHEMA_*`,
Expand Down
1 change: 1 addition & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type { AnySchemaObject, SchemaObject, ValidateFunction } from 'ajv';
export { type SpaceGraph, type SpaceNode, spaceGraphFrom } from './browser';
export type { Config, SpaceConfig } from './config';
export { loadConfig, resolveConfigPath, resolveSchema, resolveSchemaPath, setConfigPath } from './config';
export { resourceDir } from './config/layout';
export { addSpace } from './config/write';
export { LOCAL_CONFIG_FILE, PLUGIN_PREFIX } from './constants';
export { SctxError, type SctxErrorCode } from './errors';
Expand Down
12 changes: 10 additions & 2 deletions src/config/health.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { existsSync } from 'node:fs';
import { LOCAL_CONFIG_FILE } from '../constants';
import { SctxError, type SctxErrorCode } from '../errors';
import { type Config, getConfigSourceFiles, loadConfig, type ResolvedConfigPath, resolveConfigPath } from '.';
import {
type Config,
getConfigSourceFiles,
LOCAL_CONFIG_DESCRIPTION,
loadConfig,
type ResolvedConfigPath,
resolveConfigPath,
} from '.';

export type ConfigProblem = {
code: SctxErrorCode;
Expand All @@ -21,6 +28,7 @@ export type ConfigHealth = {
const FIXES: Partial<Record<SctxErrorCode, string>> = {
E_CONFIG_NOT_FOUND: `Create a config with \`sctx spaces add <name> <path> --schema <schema> --config <folder>/${LOCAL_CONFIG_FILE}\`, or use the setup-space skill.`,
E_CONFIG_INVALID: 'Fix the JSON in the config file named in the message; `sctx docs config` describes the shape.',
E_CONFIG_AMBIGUOUS: 'Merge the two config files into one and delete the other.',
E_INCLUDE_NOT_FOUND: 'Correct or remove the missing path in `includeSpacesFrom`.',
E_NO_SPACES: 'Add a space with `sctx spaces add`.',
E_SPACE_PATH_MISSING: 'The folder or file was moved or deleted. Correct `path` in the config, or remove the space.',
Expand Down Expand Up @@ -56,7 +64,7 @@ export function inspectConfig(options: { startDir?: string } = {}): { health: Co
if (!exists) {
const detail =
resolved.source === 'none'
? `No ${LOCAL_CONFIG_FILE} found from the working directory upward, and no global config at ${resolved.path}`
? `No ${LOCAL_CONFIG_DESCRIPTION} found from the working directory upward, and no global config at ${resolved.path}`
: `Config file not found: ${resolved.path}`;
health.problems.push(problem('E_CONFIG_NOT_FOUND', resolved.source === 'none' ? 'info' : 'error', detail));
return { health };
Expand Down
Loading
Loading