@aisystemresources/emdee 0.2.0 → 0.3.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.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: describe-image
3
+ description: Describe an EMDEE vault image doc and rename it from timestamp to semantic title.
4
+ argument-hint: <vault-path>
5
+ ---
6
+
7
+ Take the vault doc path from `$ARGUMENTS`. Run the emdee-describe-image skill workflow:
8
+
9
+ 1. `emdee get-image --doc-path $ARGUMENTS --remote --out /tmp/emdee-describe.jpg` — save the image locally
10
+ 2. Read the image visually and compose:
11
+ - A semantic title, 3-6 words, UPPERCASE-with-HYPHENS (e.g. `HANDSTAND-BALANCE-DRILL`)
12
+ - A one-line summary, 15-30 words
13
+ 3. `emdee rename-doc --old-path $ARGUMENTS --new-title <TITLE> --remote`
14
+ 4. Get the fresh preamble hash: `emdee get-doc --path <new-path> --remote --json`
15
+ 5. `emdee patch-preamble --path <new-path> --body "> <summary>" --expected-hash <hash> --remote`
16
+
17
+ Report the before/after path + title + summary as a punch list at the end.
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: summarise
3
+ description: Refresh drifting doc summaries. Optional path prefix argument narrows the batch.
4
+ argument-hint: [path-prefix]
5
+ ---
6
+
7
+ Run the emdee-summariser skill's batch flow:
8
+
9
+ 1. `emdee list-summary-drift --remote --limit 20 --format text` (append `--prefix $ARGUMENTS` if the user provided one) → get up to 20 candidate paths
10
+ 2. For each path, `emdee get-doc --path <path> --remote --full --format text` → read the body
11
+ 3. Compose a fresh summary (15-40 words) that answers "what is this and why does it exist"
12
+ 4. Present all proposals as a punch list — do NOT patch yet:
13
+
14
+ ```
15
+ <path>
16
+ Current: <old blockquote>
17
+ Proposed: <new blockquote>
18
+ ```
19
+
20
+ 5. Wait for the user's approval (per-doc or bulk). On approval:
21
+ - `emdee get-doc --path <path> --remote --json` → fetch preamble.content_hash
22
+ - `emdee patch-preamble --path <path> --body "> <new>" --expected-hash <hash> --remote`
23
+
24
+ Never batch-patch without human review — image content is high-signal but the model can misread ambiguous shots.
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: vault-status
3
+ description: Show a mini dashboard of the user's EMDEE vault — total docs + top 3 drifting summaries.
4
+ ---
5
+
6
+ Run these commands via the Bash tool and present the results as a compact dashboard:
7
+
8
+ 1. `emdee list --remote | wc -l` → total doc count
9
+ 2. `emdee list-summary-drift --remote --limit 3 --format text` → top 3 drifting paths
10
+
11
+ Format the output as:
12
+
13
+ ```
14
+ Vault: <N> docs total, <M> with drifting summaries
15
+ Top drift candidates:
16
+ - <path 1>
17
+ - <path 2>
18
+ - <path 3>
19
+ ```
20
+
21
+ If `emdee list --remote` errors with "run login", tell the user to run `emdee login` first and stop.
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "emdee",
3
+ "displayName": "Emdee — Local-first knowledge graph",
4
+ "description": "Local-first knowledge graph backed by plain markdown. Bundles the emdee.tech MCP server, the emdee CLI, and 4 Claude Code skills that teach vault conventions + workflows.",
5
+ "version": "0.2.2",
6
+ "author": {
7
+ "name": "AI System Resources",
8
+ "url": "https://emdee.tech"
9
+ },
10
+ "homepage": "https://emdee.tech",
11
+ "repository": "https://github.com/AISystemResources/emdee",
12
+ "license": "MIT",
13
+ "keywords": ["knowledge-graph", "markdown", "vault", "mcp", "claude"],
14
+ "mcpServers": {
15
+ "emdee": {
16
+ "type": "http",
17
+ "url": "https://emdee.tech/api/mcp"
18
+ }
19
+ },
20
+ "skills": "./skills"
21
+ }
package/README.md CHANGED
@@ -8,6 +8,23 @@ LLM agents need a stable, human-readable substrate to read and write their own c
8
8
 
9
9
  ## Install
10
10
 
11
+ **Claude Code plugin (recommended — MCP + CLI + skills in one shot):**
12
+
13
+ ```bash
14
+ claude plugin install @aisystemresources/emdee
15
+ ```
16
+
17
+ This installs the CLI, wires up the emdee.tech MCP server, and drops in 4 auto-loading skills. Then:
18
+
19
+ ```bash
20
+ emdee login # PKCE flow, opens browser
21
+ emdee whoami # confirms your namespace
22
+ ```
23
+
24
+ Slash commands available after install: `/vault-status`, `/describe-image <path>`, `/summarise [prefix]`.
25
+
26
+ **CLI-only (no plugin, for scripting or non-Claude-Code use):**
27
+
11
28
  ```bash
12
29
  npm install -g @aisystemresources/emdee
13
30
  cd ~/my-vault
@@ -16,6 +33,8 @@ emdee list # your owner + 5 virtual system nodes
16
33
  emdee mcp # stdio MCP server — point Claude Code at it
17
34
  ```
18
35
 
36
+ **claude.ai users:** the plugin doesn't apply (different install model). Use the connect panel on emdee.tech to add the HTTP MCP server directly.
37
+
19
38
  ## The 5-node OS layer
20
39
 
21
40
  Every Emdee vault has exactly 5 canonical top-level nodes, plus your owner node:
package/bin/emdee.js CHANGED
@@ -664,4 +664,78 @@ program
664
664
  shellRead("list-summary-drift", opts, extra);
665
665
  });
666
666
 
667
+ // -----------------------------------------------------------------------
668
+ // Deferred parity gap closers (post-SPRINT-091): 4 remaining tool verbs.
669
+ // -----------------------------------------------------------------------
670
+
671
+ program
672
+ .command("get-image")
673
+ .description("Fetch an image doc's binary. Default output prints metadata JSON; --out writes decoded bytes to file.")
674
+ .requiredOption("--doc-path <path>", "Path to the image doc (e.g. images/PHOTO-...md)")
675
+ .option("--out <path>", "Write the decoded image bytes to this file")
676
+ .option("--remote", "Route through emdee.tech")
677
+ .option("--json", "Pretty-print metadata")
678
+ .action((opts) => {
679
+ const outResolved = opts.out ? path.resolve(process.cwd(), opts.out) : "";
680
+ const extra = ["--doc-path", opts.docPath];
681
+ if (outResolved) extra.push("--out", outResolved);
682
+ if (opts.remote) extra.push("--remote");
683
+ if (opts.json) extra.push("--json");
684
+ shellRead("get-image", opts, extra);
685
+ });
686
+
687
+ program
688
+ .command("distill-doc")
689
+ .description("Read-only intake for split planning: returns section boundaries + rubric-quoted vault context.")
690
+ .requiredOption("--path <path>", "Doc to distill")
691
+ .option("-d, --docs <dir>", "docs directory (local mode)")
692
+ .option("--remote", "Route through emdee.tech")
693
+ .option("--json", "Machine-parseable output")
694
+ .action((opts) => {
695
+ const extra = argsFromOpts(opts, {
696
+ path: "--path", remote: "--remote", json: "--json",
697
+ });
698
+ shellWrite("distill-doc", opts, extra);
699
+ });
700
+
701
+ program
702
+ .command("materialize-subgroup")
703
+ .description("Promote an H3 subgroup inside a doc's Parent of into a real intermediate parent doc.")
704
+ .requiredOption("--source-path <path>", "Source doc holding the subgroup")
705
+ .requiredOption("--subgroup-heading <heading>", "H3 heading text (without ###)")
706
+ .option("--new-doc-title <title>", "Override the derived new doc title")
707
+ .option("--new-doc-path <path>", "Override the derived new doc path")
708
+ .option("--summary <text>", "Blockquote summary for the new intermediate")
709
+ .option("-d, --docs <dir>", "docs directory (local mode)")
710
+ .option("--remote", "Route through emdee.tech")
711
+ .option("--json", "Machine-parseable output")
712
+ .action((opts) => {
713
+ const extra = argsFromOpts(opts, {
714
+ sourcePath: "--source-path", subgroupHeading: "--subgroup-heading",
715
+ newDocTitle: "--new-doc-title", newDocPath: "--new-doc-path",
716
+ summary: "--summary", remote: "--remote", json: "--json",
717
+ });
718
+ shellWrite("materialize-subgroup", opts, extra);
719
+ });
720
+
721
+ program
722
+ .command("split-doc")
723
+ .description("Atomically refactor a doc into concept nodes. Extracts array read from a JSON file.")
724
+ .requiredOption("--source-path <path>", "Source doc being split")
725
+ .requiredOption("--rewrite-source-content <text>", "New markdown for the source (with wiki-links to extracts)")
726
+ .requiredOption("--extracts-file <path>", 'JSON file: [{"path":"<new>.md","content":"<md>"}, ...]')
727
+ .option("-d, --docs <dir>", "docs directory (local mode)")
728
+ .option("--remote", "Route through emdee.tech")
729
+ .option("--json", "Machine-parseable output")
730
+ .action((opts) => {
731
+ const extractsResolved = path.resolve(process.cwd(), opts.extractsFile);
732
+ const extra = argsFromOpts(opts, {
733
+ sourcePath: "--source-path",
734
+ rewriteSourceContent: "--rewrite-source-content",
735
+ remote: "--remote", json: "--json",
736
+ });
737
+ extra.push("--extracts-file", extractsResolved);
738
+ shellWrite("split-doc", opts, extra);
739
+ });
740
+
667
741
  program.parseAsync();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aisystemresources/emdee",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Local-first document management with markdown rendering, knowledge graph, and MCP server.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,6 +36,8 @@
36
36
  "src/mcp/",
37
37
  "skills/",
38
38
  "templates/",
39
+ ".claude-plugin/",
40
+ ".claude/commands/",
39
41
  "README.md"
40
42
  ],
41
43
  "dependencies": {
@@ -25,6 +25,8 @@ import { search } from "../lib/mcp/tools/search";
25
25
  import { readDocSection } from "../lib/mcp/tools/read_doc_section";
26
26
  import { listDocs } from "../lib/mcp/tools/list_docs";
27
27
  import { listSummaryDrift } from "../lib/mcp/tools/list_summary_drift";
28
+ import { getImage } from "../lib/mcp/tools/get_image";
29
+ import { writeFileSync } from "node:fs";
28
30
 
29
31
  const docsDir = path.resolve(process.env.EMDEE_DOCS ?? path.join(process.cwd(), "docs"));
30
32
 
@@ -273,14 +275,59 @@ async function cmdDriftBatch(argv: string[]): Promise<void> {
273
275
  }
274
276
  }
275
277
 
278
+ // get-image needs a bespoke handler: the tool returns a two-part content
279
+ // block (text metadata + image data). Default output is the metadata JSON;
280
+ // with --out, decode the base64 and write the binary to a file.
281
+ async function cmdGetImage(argv: string[]): Promise<void> {
282
+ const { values } = parseArgs({
283
+ args: argv,
284
+ options: {
285
+ "doc-path": { type: "string" },
286
+ out: { type: "string" },
287
+ remote: { type: "boolean" },
288
+ json: { type: "boolean" },
289
+ },
290
+ strict: true,
291
+ });
292
+ const docPath = asString(values["doc-path"]);
293
+ if (!docPath) {
294
+ process.stderr.write("get-image: --doc-path required\n");
295
+ process.exit(1);
296
+ }
297
+ const args = { doc_path: docPath };
298
+ const result = values.remote
299
+ ? await callTool("get_image", args)
300
+ : await (getImage as unknown as ToolFn)({ mode: "local", docsDir }, args);
301
+
302
+ const content = (result as { content?: Array<{ type: string; text?: string; data?: string; mimeType?: string }> }).content ?? [];
303
+ const meta = content.find((c) => c.type === "text");
304
+ const image = content.find((c) => c.type === "image");
305
+ const metaParsed = meta?.text ? JSON.parse(meta.text) : {};
306
+
307
+ if (image?.data && values.out) {
308
+ const outPath = path.resolve(values.out);
309
+ writeFileSync(outPath, Buffer.from(image.data, "base64"));
310
+ process.stdout.write(`Saved ${image.data.length} base64 bytes (${image.mimeType}) to ${outPath}\n`);
311
+ return;
312
+ }
313
+
314
+ const payload = {
315
+ ...metaParsed,
316
+ mime_type: image?.mimeType,
317
+ size_bytes: image?.data ? Buffer.from(image.data, "base64").byteLength : 0,
318
+ };
319
+ process.stdout.write(JSON.stringify(payload, null, values.json ? 2 : 0) + "\n");
320
+ }
321
+
276
322
  const [, , sub, ...rest] = process.argv;
277
323
 
278
324
  async function main(): Promise<void> {
279
325
  if (sub === "list") return cmdList(rest);
280
326
  if (sub === "drift-batch") return cmdDriftBatch(rest);
327
+ if (sub === "get-image") return cmdGetImage(rest);
281
328
  if (sub && READ_VERBS[sub]) return runStructuredRead(sub, rest);
282
329
  process.stderr.write(`unknown read subcommand: ${sub ?? "(none)"}\n`);
283
- process.stderr.write(`verbs: list, drift-batch, ${Object.keys(READ_VERBS).join(", ")}\n`);
330
+ process.stderr.write(`verbs: list, drift-batch, get-image, ${Object.keys(READ_VERBS).join(", ")}\n`);
284
331
  process.exit(1);
285
332
  }
286
333
 
@@ -25,6 +25,10 @@ import { writeDocPreview } from "../lib/mcp/tools/write_doc_preview";
25
25
  import { trashDoc } from "../lib/mcp/tools/trash_doc";
26
26
  import { restoreDoc } from "../lib/mcp/tools/restore_doc";
27
27
  import { deleteDoc } from "../lib/mcp/tools/delete_doc";
28
+ import { distillDoc } from "../lib/mcp/tools/distill_doc";
29
+ import { materializeSubgroup } from "../lib/mcp/tools/materialize_subgroup";
30
+ import { splitDoc } from "../lib/mcp/tools/split_doc";
31
+ import { readFileSync } from "node:fs";
28
32
  import { callTool, unwrapText } from "./remote-client";
29
33
  import { NeedsLoginError } from "./auth";
30
34
 
@@ -286,6 +290,63 @@ const VERBS: Record<string, VerbSpec> = {
286
290
  parse: { ...COMMON, path: { type: "string" } },
287
291
  buildArgs: (v) => ({ path: asString(v.path) }),
288
292
  },
293
+ "distill-doc": {
294
+ // READ-ONLY intake for split planning — lives under write commands
295
+ // alongside its executor split_doc so users find them together.
296
+ toolName: "distill_doc",
297
+ toolFn: distillDoc as unknown as ToolFn,
298
+ parse: { ...COMMON, path: { type: "string" } },
299
+ buildArgs: (v) => ({ path: asString(v.path) }),
300
+ },
301
+ "materialize-subgroup": {
302
+ toolName: "materialize_subgroup",
303
+ toolFn: materializeSubgroup as unknown as ToolFn,
304
+ parse: {
305
+ ...COMMON,
306
+ "source-path": { type: "string" },
307
+ "subgroup-heading": { type: "string" },
308
+ "new-doc-title": { type: "string" },
309
+ "new-doc-path": { type: "string" },
310
+ summary: { type: "string" },
311
+ },
312
+ buildArgs: (v) => {
313
+ const args: Record<string, unknown> = {
314
+ source_path: asString(v["source-path"]),
315
+ subgroup_heading: asString(v["subgroup-heading"]),
316
+ };
317
+ const t = optionalString(v["new-doc-title"]);
318
+ if (t) args.new_doc_title = t;
319
+ const p = optionalString(v["new-doc-path"]);
320
+ if (p) args.new_doc_path = p;
321
+ const s = optionalString(v.summary);
322
+ if (s) args.summary = s;
323
+ return args;
324
+ },
325
+ },
326
+ "split-doc": {
327
+ toolName: "split_doc",
328
+ toolFn: splitDoc as unknown as ToolFn,
329
+ parse: {
330
+ ...COMMON,
331
+ "source-path": { type: "string" },
332
+ "rewrite-source-content": { type: "string" },
333
+ // Extracts is a complex array-of-objects; take it via --extracts-file
334
+ // pointing at a JSON document rather than shoehorn it into a flag.
335
+ "extracts-file": { type: "string" },
336
+ },
337
+ buildArgs: (v) => {
338
+ const args: Record<string, unknown> = {
339
+ source_path: asString(v["source-path"]),
340
+ rewrite_source_content: asString(v["rewrite-source-content"]),
341
+ };
342
+ const extractsFile = optionalString(v["extracts-file"]);
343
+ if (extractsFile) {
344
+ const parsed = JSON.parse(readFileSync(extractsFile, "utf8"));
345
+ args.extracts = parsed;
346
+ }
347
+ return args;
348
+ },
349
+ },
289
350
  };
290
351
 
291
352
  function formatOutput(result: unknown, wantJson: boolean): string {
@@ -14,10 +14,10 @@ export async function registerClient(clientName: string | null, redirectUris: st
14
14
  return data.client_id;
15
15
  }
16
16
 
17
- export async function getClient(clientId: string): Promise<{ client_id: string; redirect_uris: string[] } | null> {
17
+ export async function getClient(clientId: string): Promise<{ client_id: string; client_name: string | null; redirect_uris: string[] } | null> {
18
18
  const { data } = await adminClient()
19
19
  .from("oauth_clients")
20
- .select("client_id, redirect_uris")
20
+ .select("client_id, client_name, redirect_uris")
21
21
  .eq("client_id", clientId)
22
22
  .maybeSingle();
23
23
  return data ?? null;