@aisystemresources/emdee 0.1.1 → 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
@@ -2,12 +2,18 @@
2
2
  import { Command } from "commander";
3
3
  import { spawn } from "node:child_process";
4
4
  import { mkdir, writeFile, access } from "node:fs/promises";
5
+ import { createRequire } from "node:module";
5
6
  import readline from "node:readline/promises";
6
7
  import path from "node:path";
7
8
  import { fileURLToPath } from "node:url";
8
9
 
9
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
11
  const pkgRoot = path.resolve(__dirname, "..");
12
+ // Version comes from package.json — `npm version <bump>` is the one
13
+ // place the string ever needs to change. createRequire works on any
14
+ // Node >= 14 without depending on the newer `import ... with { type: "json" }`
15
+ // attribute syntax (stable only in Node 20.10+).
16
+ const pkg = createRequire(import.meta.url)("../package.json");
11
17
 
12
18
  // SPRINT-090: `start` and `serve-next` shell out to Vite / Next.js against
13
19
  // the full repo. Those files (app/, next.config.*, etc.) aren't in the
@@ -60,7 +66,7 @@ function ownerNodeScaffold(title) {
60
66
  }
61
67
 
62
68
  const program = new Command();
63
- program.name("emdee").description("Emdee — local docs + knowledge graph + MCP").version("0.1.0");
69
+ program.name("emdee").description("Emdee — local docs + knowledge graph + MCP").version(pkg.version);
64
70
 
65
71
  program
66
72
  .command("init")
@@ -153,13 +159,15 @@ program
153
159
 
154
160
  program
155
161
  .command("list")
156
- .description("Print one doc path per line (token-cheap; local docs/ only)")
157
- .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")
158
164
  .option("--prefix <prefix>", "filter to paths starting with this prefix")
165
+ .option("--remote", "route through emdee.tech (requires `emdee login`)")
159
166
  .action((opts) => {
160
167
  const docs = path.resolve(process.cwd(), opts.docs);
161
168
  const args = ["tsx", path.join(pkgRoot, "src/cli/read-commands.ts"), "list"];
162
169
  if (opts.prefix) args.push("--prefix", opts.prefix);
170
+ if (opts.remote) args.push("--remote");
163
171
  const child = spawn("npx", args, {
164
172
  cwd: pkgRoot,
165
173
  stdio: "inherit",
@@ -171,10 +179,11 @@ program
171
179
  program
172
180
  .command("drift-batch")
173
181
  .description("Print a batch of docs (path + summary + body) for offline summariser workflows")
174
- .option("-d, --docs <dir>", "docs directory", "docs")
182
+ .option("-d, --docs <dir>", "docs directory (local mode)", "docs")
175
183
  .option("--limit <n>", "docs per batch", "10")
176
184
  .option("--offset <k>", "skip the first K docs", "0")
177
185
  .option("--prefix <prefix>", "filter to paths starting with this prefix")
186
+ .option("--remote", "route through emdee.tech (requires `emdee login`)")
178
187
  .action((opts) => {
179
188
  const docs = path.resolve(process.cwd(), opts.docs);
180
189
  const args = [
@@ -185,6 +194,7 @@ program
185
194
  "--offset", opts.offset,
186
195
  ];
187
196
  if (opts.prefix) args.push("--prefix", opts.prefix);
197
+ if (opts.remote) args.push("--remote");
188
198
  const child = spawn("npx", args, {
189
199
  cwd: pkgRoot,
190
200
  stdio: "inherit",
@@ -193,4 +203,465 @@ program
193
203
  child.on("exit", (code) => process.exit(code ?? 0));
194
204
  });
195
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
+
196
667
  program.parseAsync();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aisystemresources/emdee",
3
- "version": "0.1.1",
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
  ],