@aisystemresources/emdee 0.1.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.
Files changed (82) hide show
  1. package/README.md +85 -0
  2. package/bin/emdee.js +196 -0
  3. package/package.json +78 -0
  4. package/src/cli/read-commands.ts +64 -0
  5. package/src/core/indexer.ts +346 -0
  6. package/src/core/parseEdges.ts +106 -0
  7. package/src/core/resolveLink.ts +136 -0
  8. package/src/core/siblings.ts +65 -0
  9. package/src/core/syncDocEdges.ts +427 -0
  10. package/src/lib/cache/bust.ts +44 -0
  11. package/src/lib/cache/invalidation.ts +28 -0
  12. package/src/lib/mcp/activity.ts +254 -0
  13. package/src/lib/mcp/tools/add_association.ts +311 -0
  14. package/src/lib/mcp/tools/append_doc.ts +70 -0
  15. package/src/lib/mcp/tools/append_section.ts +121 -0
  16. package/src/lib/mcp/tools/create_child.ts +319 -0
  17. package/src/lib/mcp/tools/delete_doc.ts +68 -0
  18. package/src/lib/mcp/tools/distill_doc.ts +262 -0
  19. package/src/lib/mcp/tools/filename.ts +63 -0
  20. package/src/lib/mcp/tools/get_context.ts +227 -0
  21. package/src/lib/mcp/tools/get_doc.ts +91 -0
  22. package/src/lib/mcp/tools/get_image.ts +62 -0
  23. package/src/lib/mcp/tools/get_neighbors.ts +96 -0
  24. package/src/lib/mcp/tools/get_summary.ts +18 -0
  25. package/src/lib/mcp/tools/index.ts +26 -0
  26. package/src/lib/mcp/tools/lint.ts +552 -0
  27. package/src/lib/mcp/tools/lint_doc.ts +76 -0
  28. package/src/lib/mcp/tools/lint_gate.ts +49 -0
  29. package/src/lib/mcp/tools/list_docs.ts +18 -0
  30. package/src/lib/mcp/tools/list_summary_drift.ts +95 -0
  31. package/src/lib/mcp/tools/materialize_subgroup.ts +274 -0
  32. package/src/lib/mcp/tools/move_doc.ts +439 -0
  33. package/src/lib/mcp/tools/patch_preamble.ts +145 -0
  34. package/src/lib/mcp/tools/patch_section.ts +113 -0
  35. package/src/lib/mcp/tools/read_doc_section.ts +70 -0
  36. package/src/lib/mcp/tools/rename_doc.ts +167 -0
  37. package/src/lib/mcp/tools/restore_doc.ts +41 -0
  38. package/src/lib/mcp/tools/search.ts +36 -0
  39. package/src/lib/mcp/tools/sections.ts +130 -0
  40. package/src/lib/mcp/tools/split_doc.ts +129 -0
  41. package/src/lib/mcp/tools/trash_doc.ts +116 -0
  42. package/src/lib/mcp/tools/types.ts +7 -0
  43. package/src/lib/mcp/tools/upload_image.ts +77 -0
  44. package/src/lib/mcp/tools/validate_args.ts +36 -0
  45. package/src/lib/mcp/tools/vault.ts +430 -0
  46. package/src/lib/mcp/tools/write_doc.ts +81 -0
  47. package/src/lib/mcp/tools/write_doc_preview.ts +59 -0
  48. package/src/lib/owner/identity.ts +65 -0
  49. package/src/lib/storage/FilesystemStorage.ts +88 -0
  50. package/src/lib/storage/SupabaseStorage.ts +358 -0
  51. package/src/lib/storage/VaultStorage.ts +35 -0
  52. package/src/lib/storage/index.ts +41 -0
  53. package/src/lib/supabase/admin.ts +16 -0
  54. package/src/lib/supabase/client.ts +8 -0
  55. package/src/lib/supabase/oauth.ts +296 -0
  56. package/src/lib/supabase/server.ts +22 -0
  57. package/src/lib/system-nodes.ts +68 -0
  58. package/src/lib/trash/state.ts +88 -0
  59. package/src/mcp/server.ts +380 -0
  60. package/templates/types/CONCEPT.md +21 -0
  61. package/templates/types/HACKATHON.md +28 -0
  62. package/templates/types/NOVEL/CHARACTERS.md +29 -0
  63. package/templates/types/NOVEL/DRAFT.md +15 -0
  64. package/templates/types/NOVEL/EDITS.md +19 -0
  65. package/templates/types/NOVEL/INBOX.md +15 -0
  66. package/templates/types/NOVEL/INSTRUCTIONS.md +32 -0
  67. package/templates/types/NOVEL/LEARNINGS.md +9 -0
  68. package/templates/types/NOVEL/OUTBOX.md +15 -0
  69. package/templates/types/NOVEL/PLOT.md +27 -0
  70. package/templates/types/NOVEL/WORLDBUILDING.md +23 -0
  71. package/templates/types/NOVEL.md +32 -0
  72. package/templates/types/PERSON.md +27 -0
  73. package/templates/types/PROJECT/BRAND.md +23 -0
  74. package/templates/types/PROJECT/BUILD.md +9 -0
  75. package/templates/types/PROJECT/IDEAS.md +11 -0
  76. package/templates/types/PROJECT/INBOX.md +15 -0
  77. package/templates/types/PROJECT/INSTRUCTIONS.md +23 -0
  78. package/templates/types/PROJECT/LEARNINGS.md +9 -0
  79. package/templates/types/PROJECT/LOGS.md +9 -0
  80. package/templates/types/PROJECT/OUTBOX.md +15 -0
  81. package/templates/types/PROJECT/SPRINT.md +56 -0
  82. package/templates/types/PROJECT.md +26 -0
@@ -0,0 +1,62 @@
1
+ import { readVaultFile, validatePath } from "./vault";
2
+ import type { ToolContext } from "./types";
3
+
4
+ function err(text: string) {
5
+ return { content: [{ type: "text" as const, text: JSON.stringify({ error: text }) }] };
6
+ }
7
+
8
+ /**
9
+ * Fetch an image doc from the vault and return both its metadata and the raw
10
+ * image bytes as an MCP image content block so Claude can visually analyze it.
11
+ *
12
+ * Workflow for batch labelling:
13
+ * 1. list_docs → find images/*.md with "_description pending_" in summary
14
+ * 2. get_image(doc_path) → Claude sees the image
15
+ * 3. patch_preamble → write a real description as the blockquote summary
16
+ * 4. add_association → link to relevant project / person / event docs
17
+ */
18
+ export async function getImage(ctx: ToolContext, args: Record<string, unknown>): Promise<unknown> {
19
+ const docPath = String(args.doc_path ?? "").trim();
20
+ if (!docPath) return err("doc_path is required");
21
+
22
+ try { validatePath(docPath); } catch (e) { return err(String(e)); }
23
+
24
+ const raw = await readVaultFile(ctx, docPath);
25
+ if (!raw) return err(`doc not found: ${docPath}`);
26
+
27
+ // Extract the first image URL from the doc body.
28
+ const m = raw.match(/!\[.*?\]\((https?:\/\/[^)]+)\)/);
29
+ if (!m) return err(`no image URL found in ${docPath}`);
30
+ const imageUrl = m[1];
31
+
32
+ // Fetch the image binary.
33
+ let imageResp: Response;
34
+ try {
35
+ imageResp = await fetch(imageUrl);
36
+ } catch (e) {
37
+ return err(`failed to fetch image: ${String(e)}`);
38
+ }
39
+ if (!imageResp.ok) return err(`image fetch returned ${imageResp.status}`);
40
+
41
+ const contentType = imageResp.headers.get("content-type") ?? "image/jpeg";
42
+ const mimeType = contentType.split(";")[0].trim();
43
+
44
+ const buffer = Buffer.from(await imageResp.arrayBuffer());
45
+ if (buffer.length === 0) return err("image fetch returned empty body");
46
+
47
+ const base64 = buffer.toString("base64");
48
+
49
+ return {
50
+ content: [
51
+ {
52
+ type: "text" as const,
53
+ text: JSON.stringify({ doc_path: docPath, image_url: imageUrl }),
54
+ },
55
+ {
56
+ type: "image" as const,
57
+ data: base64,
58
+ mimeType,
59
+ },
60
+ ],
61
+ };
62
+ }
@@ -0,0 +1,96 @@
1
+ import { loadVaultIndex } from "./vault";
2
+ import type { DocIndex, DocNode, Link, ToolContext } from "./types";
3
+ import { getPrevNextSiblings } from "../../../core/siblings";
4
+ import { resolveWikiLink } from "../../../core/resolveLink";
5
+
6
+ function json(value: unknown) {
7
+ return { content: [{ type: "text" as const, text: JSON.stringify(value, null, 2) }] };
8
+ }
9
+
10
+ interface NeighborRef { path: string; title: string; summary: string; note: string; }
11
+
12
+ function buildNeighbors(idx: DocIndex, focal: DocNode) {
13
+ const byPath = new Map(idx.docs.map((d) => [d.path, d]));
14
+ // Locality-aware resolver: bullets like `[[DAY1]]` are disambiguated
15
+ // by the focal's path when two docs share the title or slug.
16
+ const resolve = (t: string) => byPath.get(t) ?? resolveWikiLink(idx, t, focal.path);
17
+ const refFor = (n: DocNode, note: string): NeighborRef => ({ path: n.path, title: n.title, summary: n.summary, note });
18
+
19
+ const declaredParents = new Map<string, NeighborRef>();
20
+ const declaredChildren = new Map<string, NeighborRef>();
21
+ const declaredAssoc = new Map<string, NeighborRef>();
22
+ for (const l of focal.parents) { const n = resolve(l.title); if (n) declaredParents.set(n.path, refFor(n, l.note)); }
23
+ for (const l of focal.children) { const n = resolve(l.title); if (n) declaredChildren.set(n.path, refFor(n, l.note)); }
24
+ // Compute focal's parent paths once for the sibling check below.
25
+ const focalParentPaths = new Set(
26
+ focal.parents
27
+ .map((l) => resolve(l.title)?.path)
28
+ .filter((p): p is string => !!p)
29
+ );
30
+ for (const l of focal.associates) {
31
+ const n = resolve(l.title);
32
+ if (!n) continue;
33
+ // Hierarchy beats associate — if the target is already a parent or
34
+ // child of this focal, drop the assoc entry.
35
+ if (declaredParents.has(n.path) || declaredChildren.has(n.path)) continue;
36
+ // Sibling beats associate — if the target shares one of this focal's
37
+ // parents, the relationship is already conveyed by the hierarchy.
38
+ if (focalParentPaths.size > 0) {
39
+ const candidateParentPaths = n.parents
40
+ .map((pl) => resolve(pl.title)?.path)
41
+ .filter((p): p is string => !!p);
42
+ if (candidateParentPaths.some((p) => focalParentPaths.has(p))) continue;
43
+ }
44
+ declaredAssoc.set(n.path, refFor(n, l.note));
45
+ }
46
+
47
+ const focalTitleLower = focal.title.toLowerCase();
48
+ const matchesFocal = (l: Link) => l.title.toLowerCase() === focalTitleLower;
49
+ for (const other of idx.docs) {
50
+ if (other.path === focal.path) continue;
51
+ const asChild = other.children.find(matchesFocal);
52
+ if (asChild && !declaredParents.has(other.path)) declaredParents.set(other.path, refFor(other, asChild.note));
53
+ const asParent = other.parents.find(matchesFocal);
54
+ if (asParent && !declaredChildren.has(other.path)) declaredChildren.set(other.path, refFor(other, asParent.note));
55
+ const asAssoc = other.associates.find(matchesFocal);
56
+ if (asAssoc && !declaredAssoc.has(other.path)) declaredAssoc.set(other.path, refFor(other, asAssoc.note));
57
+ }
58
+
59
+ const declared = new Set([...declaredParents.keys(), ...declaredChildren.keys(), ...declaredAssoc.keys()]);
60
+ const mentionedIn = idx.docs
61
+ .filter((d) => d.path !== focal.path && !declared.has(d.path) && d.mentions.some((m) => m.toLowerCase() === focalTitleLower))
62
+ .map((d) => ({ path: d.path, title: d.title, summary: d.summary }));
63
+
64
+ // Prev/next sibling — shared helper. Uses the parent's `## Parent of`
65
+ // bullet order, augmented with any other doc whose primary parent
66
+ // matches focal's primary parent (catches asymmetric edges where the
67
+ // child declared `Child of` but the parent didn't reciprocate).
68
+ let prev_sibling: { path: string; title: string; summary: string } | null = null;
69
+ let next_sibling: { path: string; title: string; summary: string } | null = null;
70
+ const { prevPath, nextPath } = getPrevNextSiblings(idx, focal.path);
71
+ if (prevPath) {
72
+ const p = idx.docs.find((d) => d.path === prevPath);
73
+ if (p) prev_sibling = { path: p.path, title: p.title, summary: p.summary };
74
+ }
75
+ if (nextPath) {
76
+ const n = idx.docs.find((d) => d.path === nextPath);
77
+ if (n) next_sibling = { path: n.path, title: n.title, summary: n.summary };
78
+ }
79
+
80
+ return {
81
+ path: focal.path, title: focal.title, summary: focal.summary,
82
+ parents: [...declaredParents.values()],
83
+ children: [...declaredChildren.values()],
84
+ associated: [...declaredAssoc.values()],
85
+ mentioned_in: mentionedIn,
86
+ prev_sibling,
87
+ next_sibling,
88
+ };
89
+ }
90
+
91
+ export async function getNeighbors(ctx: ToolContext, args: Record<string, unknown>): Promise<unknown> {
92
+ const idx = await loadVaultIndex(ctx);
93
+ const focal = idx.docs.find((d) => d.path === String(args.path));
94
+ if (!focal) throw new Error(`no such doc: ${args.path}`);
95
+ return json(buildNeighbors(idx, focal));
96
+ }
@@ -0,0 +1,18 @@
1
+ import { loadVaultIndex } from "./vault";
2
+ import type { ToolContext } from "./types";
3
+
4
+ function json(value: unknown) {
5
+ return { content: [{ type: "text" as const, text: JSON.stringify(value, null, 2) }] };
6
+ }
7
+
8
+ function text(value: string) {
9
+ return { content: [{ type: "text" as const, text: value }] };
10
+ }
11
+
12
+ export async function getSummary(ctx: ToolContext, args: Record<string, unknown>): Promise<unknown> {
13
+ const idx = await loadVaultIndex(ctx);
14
+ const doc = idx.docs.find((d) => d.path === String(args.path));
15
+ if (!doc) throw new Error(`no such doc: ${args.path}`);
16
+ if (args.format === "text") return text(doc.summary ?? "");
17
+ return json({ path: doc.path, title: doc.title, summary: doc.summary });
18
+ }
@@ -0,0 +1,26 @@
1
+ export { listDocs } from "./list_docs";
2
+ export { listSummaryDrift } from "./list_summary_drift";
3
+ export { getSummary } from "./get_summary";
4
+ export { getNeighbors } from "./get_neighbors";
5
+ export { getContext } from "./get_context";
6
+ export { getDoc } from "./get_doc";
7
+ export { readDocSection } from "./read_doc_section";
8
+ export { search } from "./search";
9
+ export { appendSection } from "./append_section";
10
+ export { patchSection } from "./patch_section";
11
+ export { writeDocPreview } from "./write_doc_preview";
12
+ export { writeDoc } from "./write_doc";
13
+ export { deleteDoc } from "./delete_doc";
14
+ export { splitDoc } from "./split_doc";
15
+ export { materializeSubgroup } from "./materialize_subgroup";
16
+ export { renameDoc } from "./rename_doc";
17
+ export { patchPreamble } from "./patch_preamble";
18
+ export { appendDoc } from "./append_doc";
19
+ export { lintDoc } from "./lint_doc";
20
+ export { distillDoc } from "./distill_doc";
21
+ export { createChild } from "./create_child";
22
+ export { addAssociation } from "./add_association";
23
+ export { moveDoc } from "./move_doc";
24
+ export { trashDoc } from "./trash_doc";
25
+ export { restoreDoc } from "./restore_doc";
26
+ export { getImage } from "./get_image";