@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 +74 -0
- package/package.json +1 -1
- package/src/cli/read-commands.ts +48 -1
- package/src/cli/write-commands.ts +61 -0
- package/src/lib/supabase/oauth.ts +2 -2
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
package/src/cli/read-commands.ts
CHANGED
|
@@ -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;
|