@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.
- package/.claude/commands/describe-image.md +17 -0
- package/.claude/commands/summarise.md +24 -0
- package/.claude/commands/vault-status.md +21 -0
- package/.claude-plugin/plugin.json +21 -0
- package/README.md +19 -0
- package/bin/emdee.js +74 -0
- package/package.json +3 -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
|
@@ -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.
|
|
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": {
|
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;
|