@aisystemresources/emdee 0.2.0 → 0.2.2

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/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.2.2",
4
4
  "description": "Local-first document management with markdown rendering, knowledge graph, and MCP server.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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;