mrplex 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,11 @@
2
2
 
3
3
  *Markdown Repos, plexed.*
4
4
 
5
- **Turn ordinary Markdown folders into queryable, versioned, graph-aware knowledge stores without giving up files.** Your notes stay plain `.md` on disk; agents and humans share the same repository, every write is versioned, and links survive renames.
5
+ mrplex is not a better notes app. It is a small kernel that leaves your files as files and makes the folder a multi-agent substrate.
6
+
7
+ You keep working with ordinary `.md` with YAML frontmatter. mrplex adds versions, optimistic concurrency, CEL filters, full-text search, optional embeddings, and a link graph bound to document identity so renames do not smash backlinks. CLI, MCP, and REST are the same model with different sockets.
8
+
9
+ If you wanted an Obsidian clone, this is the wrong repo. If you're an Obsidian user, now your agents can utilize and manage your vault through mrplex's MCP interface.
6
10
 
7
11
  ## Install
8
12
 
@@ -12,247 +16,165 @@ Node ≥ 20.11:
12
16
  npm install -g mrplex
13
17
  ```
14
18
 
15
- Prefer not to install globally? `npx mrplex …` works the same way.
19
+ `npx mrplex …` works. From a git checkout: `npm install && npm link`.
16
20
 
17
- Point every command at a database and default repo once (the tour below uses `starship`):
21
+ No config required. With no flags, the CLI uses `./mrplex.db`. Pass `-r <repo>` when a command needs a repo.
18
22
 
19
- ```bash
20
- export MRPLEX_DATABASE=./demo.db
21
- export MRPLEX_REPO=starship
22
- ```
23
+ ## Five minutes on the USS Meridian
23
24
 
24
- Or persist settings in `~/.config/mrplex/config.json`:
25
-
26
- ```bash
27
- mrplex config set-database ./demo.db
28
- mrplex config set-repo starship
29
- ```
30
-
31
- Each setting resolves **flag → env → config file → default**, so one-off overrides never require editing anything.
32
-
33
- > **From a git checkout?** Run `npm install && npm link` (or `npm run cli -- …`). Every `mrplex …` command below is equivalent to `npx mrplex …`.
34
-
35
- ## A five-minute tour
36
-
37
- This walkthrough uses the **USS Meridian** — a sample starship knowledge base in [`fixtures/starship/`](fixtures/starship/): crew files, mission records, officer logs, and equipment status, all ordinary Markdown with YAML frontmatter.
38
-
39
- ### 1. Sync the sample corpus
40
-
41
- Clone the repo (for the fixture files), install, and sync the folder into a fresh mrplex repo:
25
+ The fixture in `fixtures/starship/` is a starship knowledge base: crew, missions, logs, equipment. Ordinary Markdown. The product is the questions you can ask without opening every file.
42
26
 
43
27
  ```bash
44
28
  git clone https://github.com/usergenic/mrplex
45
29
  cd mrplex
46
- npm install && npm link # skip if you installed globally above
47
-
48
- export MRPLEX_DATABASE=./demo.db
49
- export MRPLEX_REPO=starship
30
+ npm install && npm link
50
31
 
51
32
  mrplex repos create starship
52
33
  mrplex sync fixtures/starship --once -r starship
53
34
  ```
54
35
 
55
- `sync` is the normal way to load a Markdown folder: it pushes local files into the repo, materializes version metadata, and keeps a cursor for two-way updates. Frontmatter values written as repo-root paths (`/crew/foo.md`) and embedded wikilinks/inline links are indexed automatically — no link-config setup step.
36
+ `sync` loads a folder, versions it, and keeps a cursor for two-way updates. Wikilinks, inline links, and frontmatter values written as repo-root paths (`/crew/foo.md`) are indexed automatically.
56
37
 
57
- No bootstrap, no token — local mode is full-trust (whoever can run the binary owns the database).
58
-
59
- ### 2. Query it like data
60
-
61
- Frontmatter is queryable with [CEL](https://github.com/google/cel-spec) filters:
38
+ ### Ask the folder like data
62
39
 
63
40
  ```bash
64
- # What's broken or offline right now?
65
- mrplex query --filter 'status == "damaged" || status == "offline"'
66
-
67
- # Who reports directly to the captain?
68
- mrplex query --filter '$has_static("crew/kestrel-vance.md", "reports_to")'
69
- ```
70
-
71
- ```text
72
- PATH STATUS
73
- equipment/plasma-manifold-3.md damaged
74
- equipment/shuttle-corvid.md offline
75
-
76
-
77
- PATH TITLE
78
- crew/aria-okonkwo.md Commander Aria Okonkwo
79
- crew/dax-thorne.md Lieutenant Commander Dax Thorne
80
- crew/quill-vasquez.md Doctor Quill Vasquez
81
- ```
41
+ # What's broken?
42
+ mrplex -r starship query --filter 'status == "damaged" || status == "offline"'
82
43
 
83
- Compose with full-text search filter, text, and semantic (when configured) all AND together:
44
+ # Who reports to the captain?
45
+ mrplex -r starship query --filter '$has("crew/kestrel-vance.md", "reports_to")'
84
46
 
85
- ```bash
86
- mrplex query --text 'manifold coolant'
87
- mrplex query --filter 'type == "mission"' --text 'Halloway'
47
+ # Filter AND full-text
48
+ mrplex -r starship query --filter 'type == "mission"' --text 'Halloway'
88
49
  ```
89
50
 
90
- ### 3. Follow the links as a graph
91
-
92
- Markdown links, wikilinks, and frontmatter repo-root paths (`/crew/foo.md`) build a derived index bound to document *identity*, so backlinks survive renames. Query membership in CEL:
51
+ `query` returns lean hits. Default projection is paths, not bodies. Hydrate what you need. That is how an agent stays cheap.
93
52
 
94
- ```bash
95
- # Every mission indexed by the mission log MOC
96
- mrplex query --filter '$in_static("moc/missions.md")'
53
+ Filter, text, and semantic compose with AND. Semantic rank is a shortlist, not an oracle. Inspect the top hits; do not treat cosine as authority.
97
54
 
98
- # What touches the damaged plasma manifold? (maintainer field, body links, logs…)
99
- mrplex query --filter '$has_static("equipment/plasma-manifold-3.md")'
100
- ```
55
+ ### Follow the graph
101
56
 
102
- Explore *how* documents connect neighborhood expansion from a root set:
57
+ Links live on document identity, not on the string you typed. Move a file and the graph still knows who pointed at it. The written text may go stale; the relation does not.
103
58
 
104
59
  ```bash
105
- mrplex graph --roots missions/the-hollow-signal.md --degrees 2 --direction both --render summary
106
- ```
107
-
108
- The missing officer, his mission, the encounter, and the logs that mention him appear as a connected neighborhood — the kind of thread a flat file listing can't give you.
60
+ # Everything a map-of-content claims
61
+ mrplex -r starship query --filter '$in("moc/missions.md")'
109
62
 
110
- Mermaid for slides or docs:
63
+ # What touches the damaged manifold?
64
+ mrplex -r starship query --filter '$has("equipment/plasma-manifold-3.md")'
111
65
 
112
- ```bash
113
- mrplex graph --roots crew/kestrel-vance.md --degrees 2 --direction out --render mermaid
66
+ # Neighborhood, not a file listing
67
+ mrplex -r starship graph --roots missions/the-hollow-signal.md --degrees 2 --direction both --render summary
68
+ mrplex -r starship graph --roots crew/kestrel-vance.md --degrees 2 --direction out --render mermaid
114
69
  ```
115
70
 
116
- ### 4. Change something safely
71
+ ### Write without clobbering
117
72
 
118
- Every write inserts a new version; nothing is overwritten. Rename a document and the link graph follows its identity:
73
+ Every write inserts a version. `prev_version_id` is optimistic concurrency. Stale prev loses; the current version comes back. Delete is a move to `:deleted/…`. Restore is a normal put.
119
74
 
120
75
  ```bash
121
- # See current version id, then move the manifold note
122
- V=$(mrplex --json docs get equipment/plasma-manifold-3.md | jq -r .version_id)
123
- mrplex docs mv equipment/manifold-3.md --prev "$V"
76
+ V=$(mrplex -r starship --json docs get equipment/plasma-manifold-3.md | jq -r .version_id)
77
+ mrplex -r starship docs mv equipment/manifold-3.md --prev "$V"
124
78
 
125
- # Backlinks still resolve the graph never broke; link text may need repair
126
- mrplex query --filter '$has_static("equipment/manifold-3.md")'
127
- mrplex links stale
128
- ```
129
-
130
- History and unified diff between any two versions:
79
+ mrplex -r starship query --filter '$has("equipment/manifold-3.md")'
80
+ mrplex -r starship links stale
131
81
 
132
- ```bash
133
- mrplex docs history equipment/manifold-3.md
134
- mrplex docs diff equipment/manifold-3.md --from v1 --to v2
82
+ mrplex -r starship docs history equipment/manifold-3.md
83
+ mrplex -r starship docs diff equipment/manifold-3.md --from v1 --to v2
135
84
  ```
136
85
 
137
- Deletion moves a document to a system-namespace path (`:deleted/…`); restore is a normal `docs put` back to user territory.
138
-
139
- ### 5. Connect an agent
86
+ ## Connect an agent
140
87
 
141
- CLI, MCP, and REST are surfaces over the same model. Point Cursor (or any MCP client) at your local database:
88
+ CLI, MCP, and REST are surfaces over one kernel. For a database only you can touch, local stdio with `--unsafe` is the fastest first attachment:
142
89
 
143
90
  ```json
144
91
  {
145
92
  "mcpServers": {
146
93
  "mrplex": {
147
94
  "command": "mrplex",
148
- "args": ["mcp-stdio", "--unsafe", "--database", "./demo.db"],
149
- "env": { "MRPLEX_REPO": "starship" }
95
+ "args": ["mcp-stdio", "--unsafe", "--database", "./mrplex.db"]
150
96
  }
151
97
  }
152
98
  }
153
99
  ```
154
100
 
155
- An agent can now ask relational questions *"What's broken on the ship, who maintains it, and which missions were affected?"* — and recover structured answers through `query` and `graph` instead of reading every file into context.
101
+ `--unsafe` means full-trust kernel: no auth in-process. That is correct for a private local file. It is not a networked default.
156
102
 
157
- Serve HTTP for remote clients or Streamable HTTP MCP:
103
+ The questions that justify the extra process are relational: *what is broken, who maintains it, which missions were hit?* Query and graph first. `docs get` / `docs_get_many` only for the hits you will actually use.
104
+
105
+ ## Policy shell (local or networked)
106
+
107
+ When you want principals — MCP as maintainer, a separate admin for repo create/delete — scaffold a policy and mint keys. No YAML to copy:
158
108
 
159
109
  ```bash
160
- mrplex serve --unsafe --port 8321 &
161
- mrplex --server http://127.0.0.1:8321 query --filter 'status == "missing"'
110
+ mrplex policy create --principal brendan --author "Brendan Baldwin <brendan@example.com>"
111
+ # policy.yaml with admin (operator) + brendan (maintainer)
112
+
113
+ KEY=$(mrplex key mint brendan --policy policy.yaml) # MCP day-to-day
114
+ # mrplex key mint admin --policy policy.yaml # when you need repos create/delete
115
+
116
+ mrplex serve --policy policy.yaml --audit audit.jsonl --port 8321 &
117
+ # or: mrplex mcp-stdio --policy policy.yaml --key "$KEY"
162
118
  ```
163
119
 
164
- For shared or networked deployments, run the authenticating shell instead see [Authentication](#authentication) below. Never expose the raw kernel (`serve --unsafe`) directly to an untrusted network.
120
+ `policy create` writes the minimal file; `policy check` validates it (and can dump a principal's entitlement). Edit the file only when you outgrow the defaults.
121
+
122
+ Three shapes: **embedded** (`serve --policy`), **launcher** (`mcp-stdio --policy`), **fronting proxy** (`proxy --policy --upstream`). Edit the policy and `kill -HUP` to reload. OIDC device flow via `mrplex login`. Details in [docs/archive/security.md](docs/archive/security.md).
123
+
124
+ Never expose `serve --unsafe` to an untrusted network. The flag is loud on purpose.
165
125
 
166
126
  ## How it works
167
127
 
168
- | Concept | What it means |
169
- |--------|----------------|
170
- | **Document** | One Markdown file with YAML frontmatter, addressed by repo-relative path |
171
- | **Version** | Every write appends; `prev_version_id` optimistic concurrency rejects stale writes |
172
- | **Query** | CEL filters over frontmatter + `$path` / `$body` / `$updated_at` intrinsics; composes with FTS and semantic search |
173
- | **Link graph** | Derived index over inline links, wikilinks, and frontmatter repo-root paths; `$in`, `$has`, `$backlinks`, `$links` in CEL |
174
- | **Graph** | BFS neighborhood expansion over the link index — *how* things connect, not just *which* match |
175
- | **Surfaces** | `mrplex` CLI (local or `--server`), MCP at `/mcp` or `mcp-stdio`, REST at `/repos/{repo}/…` |
176
-
177
- Two layers: a **full-trust kernel** (no in-engine auth) and an optional **access-and-identity shell** (API keys, OIDC, per-path write policy, audit log). See [docs/archive/security.md](docs/archive/security.md) for trust boundaries and deployment shapes.
178
-
179
- ## Features
180
-
181
- <details>
182
- <summary>Full feature list</summary>
183
-
184
- - **Versioned Markdown store** — every write inserts; `docs.put` handles in-place update and move; any past state is addressable
185
- - **Byte-exact frontmatter** — `frontmatter_raw` (verbatim YAML) or `frontmatter` (JSON); exactly one; round-trips are byte-exact via raw
186
- - **Optimistic concurrency** — stale `prev_version_id` → `stale_prev` with current version returned
187
- - **Deletion as move** — `:deleted/…` paths; idempotent delete; restore via `docs.put`
188
- - **Unified diff** — `docs.diff`, REST `/diff`, MCP `docs_diff`, CLI `mrplex docs diff`; `patch(1)`-applicable output
189
- - **CEL filter queries** — frontmatter fields + `$`-intrinsics; `list()` polymorphism for scalar-or-list fields
190
- - **Link graph** — inline, wikilink, and frontmatter path extraction (no per-field config); backlinks survive renames; set algebra (`$in("moc/**") && !$in("moc/draft.md")`); `$in_static` for link-only membership; `links stale` / `repair` / `backfill`
191
- - **Graph exploration** — BFS with direction lens, visibility filter, graph-only `$degrees` intrinsic; CLI `--render summary|yaml|mermaid|json`
192
- - **Full-text search** — SQLite FTS5 or Postgres `websearch_to_tsquery`; composes with filter via AND
193
- - **Semantic search** — pluggable `--embedder` hook; chunker + backlog worker; `$semantic_score` in `select`; no hook → `semantic_unavailable`
194
- - **HTTP surfaces** — MCP (Streamable HTTP + STDIO), REST with `If-Match` / content negotiation / `MOVE`
195
- - **CLI** — thin MCP client; `--database` local or `--server` remote; identical commands over both
196
- - **Storage** — SQLite (default) or Postgres+pgvector; same kernel test suite on both
197
- - **Path policy** — configurable sigils and disallowed chars; NFC + case-insensitive identity, case-preserved storage
198
- - **Canonical paths** — API responses use slashless repo-relative paths; leading `/` accepted as root alias on input only
199
-
200
- Prior design docs in [docs/archive/](docs/archive/) may be out of date where later work supersedes them.
201
-
202
- </details>
203
-
204
- ## Authentication
205
-
206
- For anything beyond single-user local use, run the **authenticating shell** — `serve --policy`. It reads declarative YAML (roles, principals, grants, key hashes, OIDC bindings), authenticates each request, and dispatches against a per-principal *guarded* kernel: read visibility narrowed, writes enforced per-path, author derived from the credential, every call audited.
207
-
208
- ```yaml
209
- # policy.yaml
210
- roles:
211
- editor:
212
- grants:
213
- - repo: notes
214
- read: "**"
215
- write: ["drafts/**", "inbox/**"]
216
- operator:
217
- grants:
218
- - { repo: "*", read: "**", write: "**" }
219
- destructive: true
220
-
221
- principals:
222
- brendan:
223
- author: Brendan Baldwin <brendan@example.com>
224
- roles: [operator]
225
- keys:
226
- - sha256:... # `mrplex key mint brendan --policy policy.yaml`
227
- ann:
228
- roles: [editor]
229
- oidc: { email: ann@example.com }
230
- ```
128
+ | Piece | Job |
129
+ | -------------- | ---------------------------------------------------------------------------------------- |
130
+ | **Document** | One Markdown file with YAML frontmatter, addressed by repo-relative path |
131
+ | **Version** | Every write appends. `prev_version_id` rejects stale writers |
132
+ | **Query** | CEL over frontmatter + `$path` / `$body` / `$updated_at`; AND with FTS and semantic |
133
+ | **Link graph** | Derived from inline links, wikilinks, and frontmatter repo-root paths. Bound to identity |
134
+ | **Graph** | BFS neighborhood. *How* things connect, not only *which* match |
135
+ | **Surfaces** | CLI (`--database` or `--server`), MCP (`/mcp` or `mcp-stdio`), REST |
231
136
 
232
- ```bash
233
- mrplex key mint brendan --policy policy.yaml
234
- mrplex serve --policy policy.yaml --audit audit.jsonl --port 8321 &
235
- curl -H "Authorization: Bearer $KEY" http://127.0.0.1:8321/repos
236
- ```
137
+ Two layers: a **full-trust kernel** and an optional **access-and-identity shell** (keys, OIDC, per-path grants, audit). The kernel does not pretend to be a user system. Local CLI against a database file is full-trust — possession of the file is root.
237
138
 
238
- Three deployment shapes — **embedded** (`serve --policy`), **launcher** (`mcp-stdio --policy`), and **fronting proxy** (`proxy --policy --upstream`). Edit the policy and `kill -HUP` to reload grants without restart. OIDC device-flow login via `mrplex login`. Full details in [docs/archive/security.md](docs/archive/security.md).
139
+ What ships today:
140
+
141
+ - Versioned Markdown store; byte-exact `frontmatter_raw` or structured `frontmatter`
142
+ - Optimistic concurrency; delete-as-move; unified diff
143
+ - CEL + `list()` for scalar-or-list fields; `$in` / `$has` / `$backlinks` / `$links` (membership is a filter, not a separate CLI flag)
144
+ - FTS5 or Postgres `websearch_to_tsquery`
145
+ - Pluggable embedder; no hook → `semantic_unavailable` instead of silent junk vectors
146
+ - SQLite default or Postgres+pgvector; same kernel suite on both
147
+ - NFC + case-insensitive identity, case-preserved storage
239
148
 
240
149
  ## Embeddings
241
150
 
242
- mrplex never calls an embedding provider itself — wire one with `--embedder` (subprocess command or HTTP URL). For local CPU embeddings:
151
+ mrplex never calls an embedding vendor. Wire a hook:
243
152
 
244
153
  ```bash
245
154
  npm install -g @mrplex/embedder
246
- export MRPLEX_EMBEDDER=mrplex-embedder # or: mrplex config set-embedder mrplex-embedder
247
155
 
248
156
  mrplex serve --unsafe --embedder mrplex-embedder
249
157
  mrplex embed backfill -r starship
250
- mrplex query -r starship --semantic 'distress beacon star map' # uses env/config
251
- # or per-invocation in local (--database) mode:
252
- mrplex query -r starship --embedder mrplex-embedder --semantic 'distress beacon star map'
158
+ mrplex -r starship query --semantic 'distress beacon star map'
159
+ ```
160
+
161
+ With `--server`, configure the embedder on the host. Protocol: [packages/embedder/README.md](packages/embedder/README.md).
162
+
163
+ Use semantic search to generate candidates. Then filter, hydrate, and read. Rank without inspection is how agents launder a guess into a citation.
164
+
165
+ ## Config & environment
166
+
167
+ Resolution is always **flag → env → config file → default**. Defaults: database `./mrplex.db`, no default repo (pass `-r`).
168
+
169
+ Persist defaults when you are tired of typing flags:
170
+
171
+ ```bash
172
+ mrplex config set-database ./mrplex.db
173
+ mrplex config set-repo starship
174
+ mrplex config set-author "Brendan Baldwin <brendan@example.com>"
253
175
  ```
254
176
 
255
- Resolution: **flag `MRPLEX_EMBEDDER` `mrplex config set-embedder` → unset**. In local mode, `query --embedder` overrides for that call; with `--server`, configure the embedder on the host. See [packages/embedder/README.md](packages/embedder/README.md) for protocol details.
177
+ Or with env vars: `MRPLEX_DATABASE`, `MRPLEX_REPO`, `MRPLEX_AUTHOR`, `MRPLEX_EMBEDDER`.
256
178
 
257
179
  ## Development
258
180
 
@@ -263,22 +185,15 @@ npm test
263
185
  npm run typecheck
264
186
  npm run lint
265
187
  npm run build
266
- ```
267
188
 
268
- Seed the full dev fixture set (notes + starship):
269
-
270
- ```bash
271
189
  npm run seed -- --database ./mrplex.db
272
190
  ```
273
191
 
274
- CI runs typecheck + lint + tests on Ubuntu & macOS × Node 20 & 22, plus Postgres+pgvector parity. If tests fail with a `NODE_MODULE_VERSION` error from `better-sqlite3`, run `npm rebuild better-sqlite3`.
275
-
276
- ### Postgres locally
192
+ CI: typecheck + lint + tests on Ubuntu and macOS × Node 20 and 22, plus Postgres+pgvector parity. If `better-sqlite3` throws `NODE_MODULE_VERSION`, run `npm rebuild better-sqlite3`.
277
193
 
278
194
  ```bash
279
195
  npm run pg:up
280
- export MRPLEX_DATABASE=postgres://mrplex:mrplex@localhost:5432/mrplex
281
- mrplex serve --unsafe
196
+ mrplex --database postgres://mrplex:mrplex@localhost:5432/mrplex serve --unsafe
282
197
  ```
283
198
 
284
199
  ## License
package/dist/cli/main.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * is forwarded verbatim as a bearer for a shell fronting the server; mrplex
14
14
  * itself ignores it. serve deliberately bypasses the seam — it IS the server.
15
15
  */
16
- import { readFileSync, renameSync, writeFileSync } from "node:fs";
16
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
17
17
  import { Command, InvalidArgumentError, Option } from "commander";
18
18
  import { parseDocument as parseYamlDocument } from "yaml";
19
19
  import { openLocalClient } from "../client/local.js";
@@ -35,7 +35,7 @@ import { fileAuditSink } from "../shell/audit.js";
35
35
  import { mintKey } from "../shell/keys.js";
36
36
  import { deviceFlowLogin, loadTokenSet, saveTokenSet, } from "../shell/login.js";
37
37
  import { createOidcVerifier } from "../shell/oidc.js";
38
- import { PolicyError, compile, loadPolicyFile } from "../shell/policy.js";
38
+ import { PolicyError, compile, loadPolicyFile, parsePolicy, scaffoldPolicyYaml, } from "../shell/policy.js";
39
39
  import { startProxyServer } from "../shell/proxy.js";
40
40
  import { startShellServer } from "../shell/serve.js";
41
41
  import { startShellStdio } from "../shell/stdio.js";
@@ -359,7 +359,7 @@ function renderEntitlement(principalId, e) {
359
359
  .join("");
360
360
  return (`principal: ${principalId}\n` +
361
361
  `author: ${e.author}\n` +
362
- `destructive: ${e.destructive} impersonate: ${e.impersonate}\n` +
362
+ `maintain: ${e.maintain} destructive: ${e.destructive} impersonate: ${e.impersonate}\n` +
363
363
  `read:\n${claims(e.read)}` +
364
364
  `write:\n${claims(e.write)}`);
365
365
  }
@@ -745,9 +745,9 @@ function buildProgram() {
745
745
  }
746
746
  });
747
747
  // -------- key (policy tooling) --------
748
- // `key mint` and `policy check` read/edit the policy file by definition, so
749
- // they take --policy directly and never touch the serve gate (auth-shell §1
750
- // "Policy tooling").
748
+ // `key mint`, `policy create`, and `policy check` read/edit the policy file
749
+ // by definition, so they take a file path directly and never touch the serve
750
+ // gate (auth-shell §1 "Policy tooling").
751
751
  const key = program.command("key").description("API-key tooling for the auth shell");
752
752
  key
753
753
  .command("mint <principal>")
@@ -774,6 +774,44 @@ function buildProgram() {
774
774
  });
775
775
  // -------- policy --------
776
776
  const policy = program.command("policy").description("policy-file tooling for the auth shell");
777
+ policy
778
+ .command("create [file]")
779
+ .description("write a starter policy.yaml (admin + maintainer)")
780
+ .option("--principal <id>", "maintainer principal id (MCP day-to-day)", "local")
781
+ .option("--force", "overwrite the file if it already exists", false)
782
+ .action(function (file) {
783
+ const localOpts = this.opts();
784
+ const gopts = this.optsWithGlobals();
785
+ try {
786
+ const path = file ?? "policy.yaml";
787
+ if (existsSync(path) && !localOpts.force) {
788
+ const err = new Error(`policy: ${path} already exists (pass --force to overwrite)`);
789
+ err.code = "cli_usage";
790
+ throw err;
791
+ }
792
+ const principal = localOpts.principal;
793
+ // Reuse the global --author / MRPLEX_AUTHOR (same meaning: write stamp).
794
+ const author = gopts.author ?? `${principal} <${principal}@localhost>`;
795
+ const text = scaffoldPolicyYaml({ principal, author });
796
+ // Refuse to write anything that wouldn't load — scaffold is pure, but
797
+ // this catches template/option bugs before the operator discovers them.
798
+ parsePolicy(text);
799
+ const tmp = `${path}.tmp.${process.pid}`;
800
+ writeFileSync(tmp, text);
801
+ renameSync(tmp, path);
802
+ process.stderr.write(`wrote ${path}\n` +
803
+ `next: mrplex key mint ${principal} --policy ${path} # MCP\n` +
804
+ ` mrplex key mint admin --policy ${path} # repo create/delete\n` +
805
+ ` mrplex serve --policy ${path} --database <url>\n`);
806
+ }
807
+ catch (err) {
808
+ if (err instanceof PolicyError) {
809
+ process.stderr.write(`policy: ${err.message}\n`);
810
+ process.exit(1);
811
+ }
812
+ reportError(err);
813
+ }
814
+ });
777
815
  policy
778
816
  .command("check [principal]")
779
817
  .description("validate a policy file; with a principal, print its effective entitlement")
@@ -1404,7 +1442,7 @@ function buildProgram() {
1404
1442
  .option("--once", "run startup reconciliation once, then exit (no watcher)", false)
1405
1443
  .option("--interval <ms>", "feed poll interval in ms (daemon; default 5000)", parsePositiveInt)
1406
1444
  .option("--debounce <ms>", "burst debounce in ms (daemon; default 5000)", parsePositiveInt)
1407
- .option("--settle <ms>", "skip files younger than this many ms (partial saves)", parsePositiveInt)
1445
+ .option("--settle <ms>", "minimum mtime age in ms before sync touches a path (default 0)", parsePositiveInt)
1408
1446
  .option("--include <glob>", "include glob (default **/*.md); repeat to add more", (value, prev) => [...(prev ?? []), value])
1409
1447
  .option("--exclude <glob>", "exclude glob (wins over include); repeat to add more", (value, prev) => [...(prev ?? []), value])
1410
1448
  .option("--dry-run", "report the actions a reconciliation would take, changing nothing", false)