@aisystemresources/emdee 0.1.2 → 0.2.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 +32 -0
- package/bin/emdee.js +468 -3
- package/package.json +2 -1
- package/skills/emdee-conventions.md +106 -0
- package/skills/emdee-describe-image.md +98 -0
- package/skills/emdee-onboarder.md +121 -0
- package/skills/emdee-summariser.md +98 -0
- package/src/cli/auth-commands.ts +72 -0
- package/src/cli/auth.ts +215 -0
- package/src/cli/read-commands.ts +245 -15
- package/src/cli/remote-client.ts +74 -0
- package/src/cli/skills-install.ts +54 -0
- package/src/cli/write-commands.ts +349 -0
- package/src/lib/mcp/tools/add_association.ts +2 -14
- package/src/lib/mcp/tools/create_child.ts +0 -2
package/README.md
CHANGED
|
@@ -44,6 +44,38 @@ npm run mcp # stdio MCP server
|
|
|
44
44
|
|
|
45
45
|
The web viewer (`emdee start`, `emdee serve-next`) is repo-only — it needs the Next.js `app/` tree that isn't in the published tarball. `init`, `list`, `drift-batch`, `mcp` all work from the global install.
|
|
46
46
|
|
|
47
|
+
## Claude Code skills
|
|
48
|
+
|
|
49
|
+
The package ships a set of `.md` skill files under `skills/` that teach Claude Code the vault conventions + workflows. Install them into your Claude Code skills directory:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
emdee skills-install
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This copies:
|
|
56
|
+
|
|
57
|
+
- **emdee-conventions** — always-loaded; teaches Claude the 5-node OS layer, doc shape, edge discipline, tool selection (CLI vs MCP), path conventions. Loading this once means Claude writes correctly to the vault the first time in every session.
|
|
58
|
+
- **emdee-describe-image** — auto-triggers on IMAGES/ docs with `_description pending_` summary. Runs the 4-step get-image → rename-doc → patch-preamble workflow.
|
|
59
|
+
- **emdee-summariser** — batch-refresh drifting doc summaries via `list-summary-drift`.
|
|
60
|
+
- **emdee-onboarder** — walks a new user from `emdee init` through their first project doc + connecting to Claude.
|
|
61
|
+
|
|
62
|
+
Re-run `emdee skills-install` after upgrading the package to pick up updated skill content.
|
|
63
|
+
|
|
64
|
+
## CLI
|
|
65
|
+
|
|
66
|
+
Every MCP tool has a matching `emdee <verb>` CLI command. Same guards, same errors, same semantics — 3–40× cheaper in tokens because the JSON-RPC envelope is skipped. Everything supports `--remote` (routes through emdee.tech via `POST /api/mcp`) or defaults local.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
emdee login # PKCE flow, saves creds to ~/.config/emdee/
|
|
70
|
+
emdee whoami
|
|
71
|
+
emdee list --remote # your live vault paths
|
|
72
|
+
emdee get-doc --path VAULT.md --full --remote # full markdown
|
|
73
|
+
emdee create-child --parent-path VAULT.md --title "MY-PROJECT" --remote
|
|
74
|
+
emdee patch-section --path X --heading Notes --body "..." --expected-hash <hash> --remote
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Full verb list via `emdee --help`. Auth commands: `login`, `logout`, `whoami`. Reads: `get-doc`, `get-summary`, `get-neighbors`, `get-context`, `search`, `read-doc-section`, `list-docs`, `list-summary-drift`, `list`, `drift-batch`. Writes: `patch-section`, `append-section`, `append-doc`, `patch-preamble`, `write-doc`, `write-doc-preview`, `create-child`, `add-association`, `move-doc`, `rename-doc`, `trash-doc`, `restore-doc`, `delete-doc`.
|
|
78
|
+
|
|
47
79
|
## MCP tools
|
|
48
80
|
|
|
49
81
|
The stdio server (`emdee mcp`) exposes 18 tools.
|
package/bin/emdee.js
CHANGED
|
@@ -159,13 +159,15 @@ program
|
|
|
159
159
|
|
|
160
160
|
program
|
|
161
161
|
.command("list")
|
|
162
|
-
.description("Print one doc path per line
|
|
163
|
-
.option("-d, --docs <dir>", "docs directory", "docs")
|
|
162
|
+
.description("Print one doc path per line. Local by default; --remote reads your live vault via emdee.tech.")
|
|
163
|
+
.option("-d, --docs <dir>", "docs directory (local mode)", "docs")
|
|
164
164
|
.option("--prefix <prefix>", "filter to paths starting with this prefix")
|
|
165
|
+
.option("--remote", "route through emdee.tech (requires `emdee login`)")
|
|
165
166
|
.action((opts) => {
|
|
166
167
|
const docs = path.resolve(process.cwd(), opts.docs);
|
|
167
168
|
const args = ["tsx", path.join(pkgRoot, "src/cli/read-commands.ts"), "list"];
|
|
168
169
|
if (opts.prefix) args.push("--prefix", opts.prefix);
|
|
170
|
+
if (opts.remote) args.push("--remote");
|
|
169
171
|
const child = spawn("npx", args, {
|
|
170
172
|
cwd: pkgRoot,
|
|
171
173
|
stdio: "inherit",
|
|
@@ -177,10 +179,11 @@ program
|
|
|
177
179
|
program
|
|
178
180
|
.command("drift-batch")
|
|
179
181
|
.description("Print a batch of docs (path + summary + body) for offline summariser workflows")
|
|
180
|
-
.option("-d, --docs <dir>", "docs directory", "docs")
|
|
182
|
+
.option("-d, --docs <dir>", "docs directory (local mode)", "docs")
|
|
181
183
|
.option("--limit <n>", "docs per batch", "10")
|
|
182
184
|
.option("--offset <k>", "skip the first K docs", "0")
|
|
183
185
|
.option("--prefix <prefix>", "filter to paths starting with this prefix")
|
|
186
|
+
.option("--remote", "route through emdee.tech (requires `emdee login`)")
|
|
184
187
|
.action((opts) => {
|
|
185
188
|
const docs = path.resolve(process.cwd(), opts.docs);
|
|
186
189
|
const args = [
|
|
@@ -191,6 +194,7 @@ program
|
|
|
191
194
|
"--offset", opts.offset,
|
|
192
195
|
];
|
|
193
196
|
if (opts.prefix) args.push("--prefix", opts.prefix);
|
|
197
|
+
if (opts.remote) args.push("--remote");
|
|
194
198
|
const child = spawn("npx", args, {
|
|
195
199
|
cwd: pkgRoot,
|
|
196
200
|
stdio: "inherit",
|
|
@@ -199,4 +203,465 @@ program
|
|
|
199
203
|
child.on("exit", (code) => process.exit(code ?? 0));
|
|
200
204
|
});
|
|
201
205
|
|
|
206
|
+
// SPRINT-091: PKCE login against emdee.tech. Credentials stashed in
|
|
207
|
+
// ~/.config/emdee/credentials.json for `--remote` calls to pick up.
|
|
208
|
+
function shellAuth(sub, extra = []) {
|
|
209
|
+
const child = spawn(
|
|
210
|
+
"npx",
|
|
211
|
+
["tsx", path.join(pkgRoot, "src/cli/auth-commands.ts"), sub, ...extra],
|
|
212
|
+
{ cwd: pkgRoot, stdio: "inherit", env: { ...process.env } },
|
|
213
|
+
);
|
|
214
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// SPRINT-091 chunk 2: write verbs shell through the dispatcher.
|
|
218
|
+
function shellWrite(verb, opts, extra = []) {
|
|
219
|
+
const docs = opts.docs ? path.resolve(process.cwd(), opts.docs) : path.join(process.cwd(), "docs");
|
|
220
|
+
const child = spawn(
|
|
221
|
+
"npx",
|
|
222
|
+
["tsx", path.join(pkgRoot, "src/cli/write-commands.ts"), verb, ...extra],
|
|
223
|
+
{ cwd: pkgRoot, stdio: "inherit", env: { ...process.env, EMDEE_DOCS: docs } },
|
|
224
|
+
);
|
|
225
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// SPRINT-091 chunk 3: structured read verbs share read-commands.ts dispatcher.
|
|
229
|
+
function shellRead(verb, opts, extra = []) {
|
|
230
|
+
const docs = opts.docs ? path.resolve(process.cwd(), opts.docs) : path.join(process.cwd(), "docs");
|
|
231
|
+
const child = spawn(
|
|
232
|
+
"npx",
|
|
233
|
+
["tsx", path.join(pkgRoot, "src/cli/read-commands.ts"), verb, ...extra],
|
|
234
|
+
{ cwd: pkgRoot, stdio: "inherit", env: { ...process.env, EMDEE_DOCS: docs } },
|
|
235
|
+
);
|
|
236
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Every write verb takes the same core flag surface; a small helper builds
|
|
240
|
+
// the extra-args array from commander's parsed opts + a spec of which flags
|
|
241
|
+
// map to which write-commands.ts flag names.
|
|
242
|
+
function argsFromOpts(opts, mapping) {
|
|
243
|
+
const extra = [];
|
|
244
|
+
for (const [optKey, cliFlag] of Object.entries(mapping)) {
|
|
245
|
+
const v = opts[optKey];
|
|
246
|
+
if (Array.isArray(v)) {
|
|
247
|
+
for (const item of v) extra.push(cliFlag, item);
|
|
248
|
+
} else if (typeof v === "string" && v.length > 0) {
|
|
249
|
+
extra.push(cliFlag, v);
|
|
250
|
+
} else if (v === true) {
|
|
251
|
+
extra.push(cliFlag);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return extra;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
program
|
|
258
|
+
.command("login")
|
|
259
|
+
.description("Sign in to emdee.tech via browser (PKCE). Stashes tokens in ~/.config/emdee/.")
|
|
260
|
+
.option("--host <url>", "override the cloud host (defaults to $EMDEE_CLOUD_URL or https://emdee.tech)")
|
|
261
|
+
.action((opts) => {
|
|
262
|
+
const extra = opts.host ? ["--host", opts.host] : [];
|
|
263
|
+
shellAuth("login", extra);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
program
|
|
267
|
+
.command("logout")
|
|
268
|
+
.description("Remove stored credentials.")
|
|
269
|
+
.action(() => shellAuth("logout"));
|
|
270
|
+
|
|
271
|
+
program
|
|
272
|
+
.command("whoami")
|
|
273
|
+
.description("Print the currently logged-in email + namespace.")
|
|
274
|
+
.action(() => shellAuth("whoami"));
|
|
275
|
+
|
|
276
|
+
// SPRINT-094: install the EMDEE Claude Code skills into ~/.claude/skills/
|
|
277
|
+
program
|
|
278
|
+
.command("skills-install")
|
|
279
|
+
.description("Copy packaged skills/*.md into a Claude Code skills directory (default ~/.claude/skills/).")
|
|
280
|
+
.option("--dir <path>", "Target directory")
|
|
281
|
+
.action((opts) => {
|
|
282
|
+
// Resolve --dir against the user's original cwd (not pkgRoot). We resolve
|
|
283
|
+
// here in the shell so the child process sees an absolute path regardless
|
|
284
|
+
// of where tsx runs from.
|
|
285
|
+
const resolvedDir = opts.dir ? path.resolve(process.cwd(), opts.dir) : "";
|
|
286
|
+
const extra = resolvedDir ? ["--dir", resolvedDir] : [];
|
|
287
|
+
const child = spawn(
|
|
288
|
+
"npx",
|
|
289
|
+
["tsx", path.join(pkgRoot, "src/cli/skills-install.ts"), ...extra],
|
|
290
|
+
{ cwd: pkgRoot, stdio: "inherit", env: { ...process.env } },
|
|
291
|
+
);
|
|
292
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// -----------------------------------------------------------------------
|
|
296
|
+
// SPRINT-091 chunk 2: write-side CLI verbs.
|
|
297
|
+
// Each mirrors the corresponding MCP tool. --remote routes through cloud;
|
|
298
|
+
// --json returns the raw MCP envelope for machine consumption.
|
|
299
|
+
// -----------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
program
|
|
302
|
+
.command("patch-section")
|
|
303
|
+
.description("Replace an H2 section's body — version-guarded. Same shape as the patch_section MCP tool.")
|
|
304
|
+
.requiredOption("--path <path>", "Vault doc path")
|
|
305
|
+
.requiredOption("--body <text>", "New section body")
|
|
306
|
+
.requiredOption("--expected-hash <hash>", "Prior content_hash from get_doc")
|
|
307
|
+
.option("--section-id <id>", "Section id (preferred over --heading)")
|
|
308
|
+
.option("--heading <heading>", "H2 heading text (without ##)")
|
|
309
|
+
.option("--gate-on <code...>", "Lint codes to hard-block on")
|
|
310
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
311
|
+
.option("--remote", "Route through emdee.tech")
|
|
312
|
+
.option("--json", "Machine-parseable output")
|
|
313
|
+
.action((opts) => {
|
|
314
|
+
const extra = argsFromOpts(opts, {
|
|
315
|
+
path: "--path", body: "--body", expectedHash: "--expected-hash",
|
|
316
|
+
sectionId: "--section-id", heading: "--heading", gateOn: "--gate-on",
|
|
317
|
+
remote: "--remote", json: "--json",
|
|
318
|
+
});
|
|
319
|
+
shellWrite("patch-section", opts, extra);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
program
|
|
323
|
+
.command("append-section")
|
|
324
|
+
.description("Append markdown to the end of an existing H2 section. --create-if-missing adds it at end of file.")
|
|
325
|
+
.requiredOption("--path <path>", "Vault doc path")
|
|
326
|
+
.requiredOption("--body <text>", "Content to append")
|
|
327
|
+
.option("--section-id <id>", "Section id (preferred over --heading)")
|
|
328
|
+
.option("--heading <heading>", "H2 heading text (without ##)")
|
|
329
|
+
.option("--create-if-missing", "Create the section at end of file if not found")
|
|
330
|
+
.option("--gate-on <code...>", "Lint codes to hard-block on")
|
|
331
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
332
|
+
.option("--remote", "Route through emdee.tech")
|
|
333
|
+
.option("--json", "Machine-parseable output")
|
|
334
|
+
.action((opts) => {
|
|
335
|
+
const extra = argsFromOpts(opts, {
|
|
336
|
+
path: "--path", body: "--body", sectionId: "--section-id",
|
|
337
|
+
heading: "--heading", createIfMissing: "--create-if-missing",
|
|
338
|
+
gateOn: "--gate-on", remote: "--remote", json: "--json",
|
|
339
|
+
});
|
|
340
|
+
shellWrite("append-section", opts, extra);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
program
|
|
344
|
+
.command("append-doc")
|
|
345
|
+
.description("Append to the end of a doc (after every section). Ideal for LOGS, daily notes.")
|
|
346
|
+
.requiredOption("--path <path>", "Vault doc path")
|
|
347
|
+
.requiredOption("--body <text>", "Content to append")
|
|
348
|
+
.option("--gate-on <code...>", "Lint codes to hard-block on")
|
|
349
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
350
|
+
.option("--remote", "Route through emdee.tech")
|
|
351
|
+
.option("--json", "Machine-parseable output")
|
|
352
|
+
.action((opts) => {
|
|
353
|
+
const extra = argsFromOpts(opts, {
|
|
354
|
+
path: "--path", body: "--body", gateOn: "--gate-on",
|
|
355
|
+
remote: "--remote", json: "--json",
|
|
356
|
+
});
|
|
357
|
+
shellWrite("append-doc", opts, extra);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
program
|
|
361
|
+
.command("patch-preamble")
|
|
362
|
+
.description("Replace the region between H1 and first H2 (blockquote summary + intro paragraphs).")
|
|
363
|
+
.requiredOption("--path <path>", "Vault doc path")
|
|
364
|
+
.requiredOption("--body <text>", "New preamble body")
|
|
365
|
+
.requiredOption("--expected-hash <hash>", "Prior preamble content_hash from get_doc")
|
|
366
|
+
.option("--gate-on <code...>", "Lint codes to hard-block on")
|
|
367
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
368
|
+
.option("--remote", "Route through emdee.tech")
|
|
369
|
+
.option("--json", "Machine-parseable output")
|
|
370
|
+
.action((opts) => {
|
|
371
|
+
const extra = argsFromOpts(opts, {
|
|
372
|
+
path: "--path", body: "--body", expectedHash: "--expected-hash",
|
|
373
|
+
gateOn: "--gate-on", remote: "--remote", json: "--json",
|
|
374
|
+
});
|
|
375
|
+
shellWrite("patch-preamble", opts, extra);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
program
|
|
379
|
+
.command("create-child")
|
|
380
|
+
.description("Atomic write + parent-of patch: create a new doc as child of an existing one.")
|
|
381
|
+
.requiredOption("--parent-path <path>", "Parent doc path")
|
|
382
|
+
.requiredOption("--title <title>", "New doc's H1 title")
|
|
383
|
+
.option("--body <text>", "Optional body appended after ## Notes")
|
|
384
|
+
.option("--summary <text>", "Optional blockquote summary (placeholder if omitted)")
|
|
385
|
+
.option("--child-path <path>", "Override the derived child path")
|
|
386
|
+
.option("--gate-on <code...>", "Lint codes to hard-block on")
|
|
387
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
388
|
+
.option("--remote", "Route through emdee.tech")
|
|
389
|
+
.option("--json", "Machine-parseable output")
|
|
390
|
+
.action((opts) => {
|
|
391
|
+
const extra = argsFromOpts(opts, {
|
|
392
|
+
parentPath: "--parent-path", title: "--title", body: "--body",
|
|
393
|
+
summary: "--summary", childPath: "--child-path", gateOn: "--gate-on",
|
|
394
|
+
remote: "--remote", json: "--json",
|
|
395
|
+
});
|
|
396
|
+
shellWrite("create-child", opts, extra);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
program
|
|
400
|
+
.command("add-association")
|
|
401
|
+
.description("Atomic two-sided assoc patch. Hard-refuses hierarchy or sibling duplicates.")
|
|
402
|
+
.requiredOption("--a-path <path>", "First doc path")
|
|
403
|
+
.requiredOption("--b-path <path>", "Second doc path")
|
|
404
|
+
.option("--label <text>", "Shared label on both bullets")
|
|
405
|
+
.option("--gate-on <code...>", "Lint codes to hard-block on")
|
|
406
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
407
|
+
.option("--remote", "Route through emdee.tech")
|
|
408
|
+
.option("--json", "Machine-parseable output")
|
|
409
|
+
.action((opts) => {
|
|
410
|
+
const extra = argsFromOpts(opts, {
|
|
411
|
+
aPath: "--a-path", bPath: "--b-path", label: "--label",
|
|
412
|
+
gateOn: "--gate-on", remote: "--remote", json: "--json",
|
|
413
|
+
});
|
|
414
|
+
shellWrite("add-association", opts, extra);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
program
|
|
418
|
+
.command("move-doc")
|
|
419
|
+
.description("Atomic reparent: three-side edge update (child's Child of + both parents' Parent of).")
|
|
420
|
+
.requiredOption("--path <path>", "Child doc to reparent")
|
|
421
|
+
.requiredOption("--new-parent-path <path>", "New parent doc")
|
|
422
|
+
.option("--old-parent-path <path>", "Old parent (required if child has multiple Child of bullets)")
|
|
423
|
+
.option("--position <n>", "0-indexed position in new parent's Parent of")
|
|
424
|
+
.option("--gate-on <code...>", "Lint codes to hard-block on")
|
|
425
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
426
|
+
.option("--remote", "Route through emdee.tech")
|
|
427
|
+
.option("--json", "Machine-parseable output")
|
|
428
|
+
.action((opts) => {
|
|
429
|
+
const extra = argsFromOpts(opts, {
|
|
430
|
+
path: "--path", newParentPath: "--new-parent-path",
|
|
431
|
+
oldParentPath: "--old-parent-path", position: "--position",
|
|
432
|
+
gateOn: "--gate-on", remote: "--remote", json: "--json",
|
|
433
|
+
});
|
|
434
|
+
shellWrite("move-doc", opts, extra);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
program
|
|
438
|
+
.command("rename-doc")
|
|
439
|
+
.description("Rewrite H1, move file, update every [[old_title]] wiki-link across the vault. DESTRUCTIVE.")
|
|
440
|
+
.requiredOption("--old-path <path>", "Existing doc path")
|
|
441
|
+
.requiredOption("--new-title <title>", "New H1 title")
|
|
442
|
+
.option("--new-path <path>", "Override the derived new path")
|
|
443
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
444
|
+
.option("--remote", "Route through emdee.tech")
|
|
445
|
+
.option("--json", "Machine-parseable output")
|
|
446
|
+
.action((opts) => {
|
|
447
|
+
const extra = argsFromOpts(opts, {
|
|
448
|
+
oldPath: "--old-path", newTitle: "--new-title", newPath: "--new-path",
|
|
449
|
+
remote: "--remote", json: "--json",
|
|
450
|
+
});
|
|
451
|
+
shellWrite("rename-doc", opts, extra);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// -----------------------------------------------------------------------
|
|
455
|
+
// SPRINT-091 chunk 3: full-file writes + lifecycle.
|
|
456
|
+
// -----------------------------------------------------------------------
|
|
457
|
+
|
|
458
|
+
program
|
|
459
|
+
.command("write-doc")
|
|
460
|
+
.description("Create or overwrite an entire doc. DESTRUCTIVE — always run write-doc-preview first.")
|
|
461
|
+
.requiredOption("--path <path>", "Vault doc path")
|
|
462
|
+
.requiredOption("--content <text>", "Full markdown content")
|
|
463
|
+
.option("--gate-on <code...>", "Lint codes to hard-block on")
|
|
464
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
465
|
+
.option("--remote", "Route through emdee.tech")
|
|
466
|
+
.option("--json", "Machine-parseable output")
|
|
467
|
+
.action((opts) => {
|
|
468
|
+
const extra = argsFromOpts(opts, {
|
|
469
|
+
path: "--path", content: "--content", gateOn: "--gate-on",
|
|
470
|
+
remote: "--remote", json: "--json",
|
|
471
|
+
});
|
|
472
|
+
shellWrite("write-doc", opts, extra);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
program
|
|
476
|
+
.command("write-doc-preview")
|
|
477
|
+
.description("Diff + list of sections that would be removed by write-doc. Always call before write-doc.")
|
|
478
|
+
.requiredOption("--path <path>", "Vault doc path")
|
|
479
|
+
.requiredOption("--content <text>", "Proposed full markdown content")
|
|
480
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
481
|
+
.option("--remote", "Route through emdee.tech")
|
|
482
|
+
.option("--json", "Machine-parseable output")
|
|
483
|
+
.action((opts) => {
|
|
484
|
+
const extra = argsFromOpts(opts, {
|
|
485
|
+
path: "--path", content: "--content",
|
|
486
|
+
remote: "--remote", json: "--json",
|
|
487
|
+
});
|
|
488
|
+
shellWrite("write-doc-preview", opts, extra);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
program
|
|
492
|
+
.command("trash-doc")
|
|
493
|
+
.description("Sidecar-based soft delete. Restore is lossless (edges preserved).")
|
|
494
|
+
.requiredOption("--path <path>", "Vault doc path")
|
|
495
|
+
.option("--original-parent-path <path>", "Override the auto-derived restore target")
|
|
496
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
497
|
+
.option("--remote", "Route through emdee.tech")
|
|
498
|
+
.option("--json", "Machine-parseable output")
|
|
499
|
+
.action((opts) => {
|
|
500
|
+
const extra = argsFromOpts(opts, {
|
|
501
|
+
path: "--path", originalParentPath: "--original-parent-path",
|
|
502
|
+
remote: "--remote", json: "--json",
|
|
503
|
+
});
|
|
504
|
+
shellWrite("trash-doc", opts, extra);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
program
|
|
508
|
+
.command("restore-doc")
|
|
509
|
+
.description("Reverse a previous trash-doc. Edges were never touched.")
|
|
510
|
+
.requiredOption("--path <path>", "Vault doc path")
|
|
511
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
512
|
+
.option("--remote", "Route through emdee.tech")
|
|
513
|
+
.option("--json", "Machine-parseable output")
|
|
514
|
+
.action((opts) => {
|
|
515
|
+
const extra = argsFromOpts(opts, { path: "--path", remote: "--remote", json: "--json" });
|
|
516
|
+
shellWrite("restore-doc", opts, extra);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
program
|
|
520
|
+
.command("delete-doc")
|
|
521
|
+
.description("Permanently remove a doc. NO UNDO. Returns inbound_edges + title_conflicts.")
|
|
522
|
+
.requiredOption("--path <path>", "Vault doc path")
|
|
523
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
524
|
+
.option("--remote", "Route through emdee.tech")
|
|
525
|
+
.option("--json", "Machine-parseable output")
|
|
526
|
+
.action((opts) => {
|
|
527
|
+
const extra = argsFromOpts(opts, { path: "--path", remote: "--remote", json: "--json" });
|
|
528
|
+
shellWrite("delete-doc", opts, extra);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// -----------------------------------------------------------------------
|
|
532
|
+
// SPRINT-091 chunk 3: structured reads (get-doc, get-summary, get-neighbors,
|
|
533
|
+
// get-context, search, read-doc-section, list-docs, list-summary-drift).
|
|
534
|
+
// -----------------------------------------------------------------------
|
|
535
|
+
|
|
536
|
+
program
|
|
537
|
+
.command("get-doc")
|
|
538
|
+
.description("Fetch a doc's envelope (title + summary + preamble + section headings). Pass --full for the body.")
|
|
539
|
+
.requiredOption("--path <path>", "Vault doc path")
|
|
540
|
+
.option("--full", "Include the full markdown body")
|
|
541
|
+
.option("--format <fmt>", "text | json (default text for --full)")
|
|
542
|
+
.option("--expected-hash <hash>", "Short-circuit if focal unchanged")
|
|
543
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
544
|
+
.option("--remote", "Route through emdee.tech")
|
|
545
|
+
.option("--json", "Machine-parseable output")
|
|
546
|
+
.action((opts) => {
|
|
547
|
+
const extra = argsFromOpts(opts, {
|
|
548
|
+
path: "--path", full: "--full", format: "--format",
|
|
549
|
+
expectedHash: "--expected-hash", remote: "--remote", json: "--json",
|
|
550
|
+
});
|
|
551
|
+
shellRead("get-doc", opts, extra);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
program
|
|
555
|
+
.command("get-summary")
|
|
556
|
+
.description("Return {path, title, summary} for one doc — cheapest way to preview.")
|
|
557
|
+
.requiredOption("--path <path>", "Vault doc path")
|
|
558
|
+
.option("--format <fmt>", "text | json")
|
|
559
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
560
|
+
.option("--remote", "Route through emdee.tech")
|
|
561
|
+
.option("--json", "Machine-parseable output")
|
|
562
|
+
.action((opts) => {
|
|
563
|
+
const extra = argsFromOpts(opts, {
|
|
564
|
+
path: "--path", format: "--format", remote: "--remote", json: "--json",
|
|
565
|
+
});
|
|
566
|
+
shellRead("get-summary", opts, extra);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
program
|
|
570
|
+
.command("get-neighbors")
|
|
571
|
+
.description("Return the doc + 1-hop neighbours categorised by relationship type.")
|
|
572
|
+
.requiredOption("--path <path>", "Vault doc path")
|
|
573
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
574
|
+
.option("--remote", "Route through emdee.tech")
|
|
575
|
+
.option("--json", "Machine-parseable output")
|
|
576
|
+
.action((opts) => {
|
|
577
|
+
const extra = argsFromOpts(opts, { path: "--path", remote: "--remote", json: "--json" });
|
|
578
|
+
shellRead("get-neighbors", opts, extra);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
program
|
|
582
|
+
.command("get-context")
|
|
583
|
+
.description("Return the focal doc + multi-hop neighbourhood within a token budget.")
|
|
584
|
+
.requiredOption("--path <path>", "Vault doc path")
|
|
585
|
+
.option("--hops <n>", "Max BFS depth (1-3, default 2)")
|
|
586
|
+
.option("--budget-tokens <n>", "Rough token cap (default 8000)")
|
|
587
|
+
.option("--include-full", "Inline focal + hop-1 bodies")
|
|
588
|
+
.option("--include-associates", "Include assoc edges in the walk")
|
|
589
|
+
.option("--expected-hash <hash>", "Short-circuit if focal unchanged")
|
|
590
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
591
|
+
.option("--remote", "Route through emdee.tech")
|
|
592
|
+
.option("--json", "Machine-parseable output")
|
|
593
|
+
.action((opts) => {
|
|
594
|
+
const extra = argsFromOpts(opts, {
|
|
595
|
+
path: "--path", hops: "--hops", budgetTokens: "--budget-tokens",
|
|
596
|
+
includeFull: "--include-full", includeAssociates: "--include-associates",
|
|
597
|
+
expectedHash: "--expected-hash", remote: "--remote", json: "--json",
|
|
598
|
+
});
|
|
599
|
+
shellRead("get-context", opts, extra);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
program
|
|
603
|
+
.command("search")
|
|
604
|
+
.description("Case-insensitive substring match over titles, summaries, content.")
|
|
605
|
+
.requiredOption("--query <text>", "Search query")
|
|
606
|
+
.option("--limit <n>", "Max results (default 10)")
|
|
607
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
608
|
+
.option("--remote", "Route through emdee.tech")
|
|
609
|
+
.option("--json", "Machine-parseable output")
|
|
610
|
+
.action((opts) => {
|
|
611
|
+
const extra = argsFromOpts(opts, {
|
|
612
|
+
query: "--query", limit: "--limit", remote: "--remote", json: "--json",
|
|
613
|
+
});
|
|
614
|
+
shellRead("search", opts, extra);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
program
|
|
618
|
+
.command("read-doc-section")
|
|
619
|
+
.description("Read one H2 section's body without paying for the whole doc.")
|
|
620
|
+
.requiredOption("--path <path>", "Vault doc path")
|
|
621
|
+
.option("--section-id <id>", "Section id (preferred over --heading)")
|
|
622
|
+
.option("--heading <heading>", "H2 heading text (without ##)")
|
|
623
|
+
.option("--expected-hash <hash>", "Short-circuit if unchanged")
|
|
624
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
625
|
+
.option("--remote", "Route through emdee.tech")
|
|
626
|
+
.option("--json", "Machine-parseable output")
|
|
627
|
+
.action((opts) => {
|
|
628
|
+
const extra = argsFromOpts(opts, {
|
|
629
|
+
path: "--path", sectionId: "--section-id", heading: "--heading",
|
|
630
|
+
expectedHash: "--expected-hash", remote: "--remote", json: "--json",
|
|
631
|
+
});
|
|
632
|
+
shellRead("read-doc-section", opts, extra);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
program
|
|
636
|
+
.command("list-docs")
|
|
637
|
+
.description("Enumerate every doc in the vault. Structured (vs `list` which is bytes-only).")
|
|
638
|
+
.option("--format <fmt>", "text | json (default text)")
|
|
639
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
640
|
+
.option("--remote", "Route through emdee.tech")
|
|
641
|
+
.option("--json", "Machine-parseable output")
|
|
642
|
+
.action((opts) => {
|
|
643
|
+
const extra = argsFromOpts(opts, {
|
|
644
|
+
format: "--format", remote: "--remote", json: "--json",
|
|
645
|
+
});
|
|
646
|
+
shellRead("list-docs", opts, extra);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
program
|
|
650
|
+
.command("list-summary-drift")
|
|
651
|
+
.description("Return paths whose body has drifted since their summary was last authored.")
|
|
652
|
+
.option("--prefix <p>", "Path prefix filter")
|
|
653
|
+
.option("--limit <n>", "Max candidates (default 20)")
|
|
654
|
+
.option("--offset <k>", "Skip first N candidates")
|
|
655
|
+
.option("--format <fmt>", "text | json (default text)")
|
|
656
|
+
.option("-d, --docs <dir>", "docs directory (local mode)")
|
|
657
|
+
.option("--remote", "Route through emdee.tech")
|
|
658
|
+
.option("--json", "Machine-parseable output")
|
|
659
|
+
.action((opts) => {
|
|
660
|
+
const extra = argsFromOpts(opts, {
|
|
661
|
+
prefix: "--prefix", limit: "--limit", offset: "--offset",
|
|
662
|
+
format: "--format", remote: "--remote", json: "--json",
|
|
663
|
+
});
|
|
664
|
+
shellRead("list-summary-drift", opts, extra);
|
|
665
|
+
});
|
|
666
|
+
|
|
202
667
|
program.parseAsync();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aisystemresources/emdee",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Local-first document management with markdown rendering, knowledge graph, and MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"src/lib/owner/",
|
|
35
35
|
"src/lib/system-nodes.ts",
|
|
36
36
|
"src/mcp/",
|
|
37
|
+
"skills/",
|
|
37
38
|
"templates/",
|
|
38
39
|
"README.md"
|
|
39
40
|
],
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: emdee-conventions
|
|
3
|
+
description: |
|
|
4
|
+
Use whenever reading or writing an EMDEE vault. Covers the 5-node OS layer,
|
|
5
|
+
doc shape (H1 + blockquote + relationship sections), edge discipline, tool
|
|
6
|
+
selection (CLI vs MCP, atomic multi-side writes), and path conventions
|
|
7
|
+
(UPPERCASE filenames, lowercase folders, hub-next-to-folder). Load this once
|
|
8
|
+
and every subsequent vault operation lands correctly the first time.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# EMDEE conventions — always-loaded
|
|
12
|
+
|
|
13
|
+
You are working inside an Emdee vault. Every doc is plain markdown. Humans browse it through a Next.js renderer at [emdee.tech](https://emdee.tech); agents (Claude Code, Claude.ai, Cursor) read and write the same files through an MCP server OR the `emdee` CLI. The vault is the source of truth — anything you say must trace back to a file the user wrote.
|
|
14
|
+
|
|
15
|
+
## The 5-node OS layer (virtual system nodes)
|
|
16
|
+
|
|
17
|
+
Every vault has exactly 5 canonical top-level docs plus one owner node. **These 5 are virtual** — they appear in every read (`emdee list`, `get_doc`, MCP responses) without being on disk. Edit them and your version wins.
|
|
18
|
+
|
|
19
|
+
| Node | Purpose |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `EMDEE` | Vault root — everything's anchor |
|
|
22
|
+
| `VAULT` | Private notes, projects, knowledge |
|
|
23
|
+
| `SHARED` | Content shared into this vault by others (cloud only) |
|
|
24
|
+
| `GRAVEYARD` | Archived / retired docs |
|
|
25
|
+
| `IMAGES` | Images and visual assets |
|
|
26
|
+
| `<YOUR-NAME>` | Personal subtree, one per user, uppercase |
|
|
27
|
+
|
|
28
|
+
## Doc shape (universal)
|
|
29
|
+
|
|
30
|
+
```md
|
|
31
|
+
# TITLE
|
|
32
|
+
|
|
33
|
+
> One-line blockquote summary — this is what routing sees.
|
|
34
|
+
|
|
35
|
+
## Child of
|
|
36
|
+
|
|
37
|
+
* [[PARENT]]
|
|
38
|
+
|
|
39
|
+
## Parent of
|
|
40
|
+
|
|
41
|
+
* [[CHILD1]]
|
|
42
|
+
* [[CHILD2]]
|
|
43
|
+
|
|
44
|
+
## Associated with
|
|
45
|
+
|
|
46
|
+
* [[CROSS-TREE-NODE]] — optional prose about the link
|
|
47
|
+
|
|
48
|
+
## Notes
|
|
49
|
+
|
|
50
|
+
Freeform content.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Rules the lint enforces (see `lint_doc(path)` or `emdee patch-section --gate-on <code>`):
|
|
54
|
+
|
|
55
|
+
- **One parent per doc.** `## Child of` should have exactly one bullet. Multiple parents → demote secondaries to `## Associated with`.
|
|
56
|
+
- **No sibling associations.** Docs that share a parent are already related through it. `## Associated with` is for cross-tree links (project↔person, sprint↔learning).
|
|
57
|
+
- **Reciprocal edges.** If A's `## Parent of` lists `[[B]]`, B's `## Child of` must list `[[A]]`. Asymmetric edges fire `asymmetric_parent_edge` / `asymmetric_child_edge` warnings.
|
|
58
|
+
- **First wiki-link = declared edge.** `* [[TARGET]] — prose about the link`. The bullet's leading link is the edge; other links in the bullet are inline mentions only.
|
|
59
|
+
- **Sibling order is derived, never declared.** `get_neighbors` returns `prev_sibling` / `next_sibling` computed from the parent's `## Parent of` order. Do NOT add `[[next-node]]` / `[[prev-node]]` bullets.
|
|
60
|
+
|
|
61
|
+
## Path conventions
|
|
62
|
+
|
|
63
|
+
- **Filenames UPPERCASE with HYPHENS:** `EDMUND.md`, `HANDSTAND-BALANCE-DRILL.md`. Never `edmund.md` or `handstandBalanceDrill.md`.
|
|
64
|
+
- **Folders lowercase:** `edmund/personal/philosophy/CLARITY-IS-POWER.md`. Not `edmund/Personal/PHILOSOPHY/`.
|
|
65
|
+
- **Hub sits next to folder.** `edmund/personal/PHILOSOPHY.md` (the hub) + `edmund/personal/philosophy/*` (the children). NOT `edmund/personal/philosophy/PHILOSOPHY.md`. The hub is a sibling of the folder it heads.
|
|
66
|
+
|
|
67
|
+
## Tool selection
|
|
68
|
+
|
|
69
|
+
Prefer CLI over MCP for token efficiency:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
emdee list --remote # 40× cheaper than list_docs MCP call
|
|
73
|
+
emdee get-doc --path X --remote --full # 3× cheaper than get_doc MCP call
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
All 16 tool verbs are available as `emdee <verb>`. Same guards, same errors, same semantics as the MCP tool of the same name (hyphenated). Everything supports `--remote` (cloud) or defaults local.
|
|
77
|
+
|
|
78
|
+
For writes, always prefer the atomic multi-side variants over raw section patches:
|
|
79
|
+
|
|
80
|
+
| Instead of… | Use… |
|
|
81
|
+
|---|---|
|
|
82
|
+
| Two `patch_section` calls (child's Child of + parent's Parent of) | `emdee create-child` |
|
|
83
|
+
| Two `patch_section` calls on both docs' Associated with | `emdee add-association` |
|
|
84
|
+
| Three `patch_section` calls to reparent | `emdee move-doc` |
|
|
85
|
+
| Search+replace across every doc that references a title | `emdee rename-doc` |
|
|
86
|
+
|
|
87
|
+
The atomic variants take care of edge discipline (hard-refuse on sibling-assoc-redundant, would-duplicate-hierarchy) and rollback semantics.
|
|
88
|
+
|
|
89
|
+
For destructive full-file writes, always run `emdee write-doc-preview` first — `write-doc` silently drops any section not in the new payload.
|
|
90
|
+
|
|
91
|
+
## Version-guarded writes
|
|
92
|
+
|
|
93
|
+
Every destructive write (`patch-section`, `patch-preamble`, `move-doc`) requires `--expected-hash <hash>` from a prior read. Get it via `emdee get-doc --path X` (returns per-section content_hash + doc_content_hash). A stale hash returns `version_conflict` — refetch, reconcile, retry.
|
|
94
|
+
|
|
95
|
+
## HARD RULES you'll trip if you're not careful
|
|
96
|
+
|
|
97
|
+
1. **Any Supabase `.select()` reading > 1000 rows must paginate.** Use `.range(offset, offset + PAGE - 1)` in a loop. `.range()` alone doesn't lift the server-side 1000-row cap.
|
|
98
|
+
2. **`patch_section` on a `## Child of` requires patching the new parent's `## Parent of` in the same turn.** Otherwise you leave an asymmetric edge. Or just use `move-doc`.
|
|
99
|
+
3. **New / modified MCP tools require an e2e spec in the same PR.** Test the tool's behaviour end-to-end via a real `ToolContext` + temp filesystem.
|
|
100
|
+
4. **Migrations touch `supabase/migrations/**` and are NEVER auto-merged.** Human review required.
|
|
101
|
+
|
|
102
|
+
## When in doubt
|
|
103
|
+
|
|
104
|
+
- `emdee lint-doc --path X` — surface every warning code the doc trips
|
|
105
|
+
- `emdee get-doc --path X` — see current state including per-section hashes
|
|
106
|
+
- Read the source of truth: `edmund/projects/emdee_os/LEARNINGS.md` in the vault
|