@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 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 (token-cheap; local docs/ only)")
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.1.2",
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