@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.
- package/README.md +85 -0
- package/bin/emdee.js +196 -0
- package/package.json +78 -0
- package/src/cli/read-commands.ts +64 -0
- package/src/core/indexer.ts +346 -0
- package/src/core/parseEdges.ts +106 -0
- package/src/core/resolveLink.ts +136 -0
- package/src/core/siblings.ts +65 -0
- package/src/core/syncDocEdges.ts +427 -0
- package/src/lib/cache/bust.ts +44 -0
- package/src/lib/cache/invalidation.ts +28 -0
- package/src/lib/mcp/activity.ts +254 -0
- package/src/lib/mcp/tools/add_association.ts +311 -0
- package/src/lib/mcp/tools/append_doc.ts +70 -0
- package/src/lib/mcp/tools/append_section.ts +121 -0
- package/src/lib/mcp/tools/create_child.ts +319 -0
- package/src/lib/mcp/tools/delete_doc.ts +68 -0
- package/src/lib/mcp/tools/distill_doc.ts +262 -0
- package/src/lib/mcp/tools/filename.ts +63 -0
- package/src/lib/mcp/tools/get_context.ts +227 -0
- package/src/lib/mcp/tools/get_doc.ts +91 -0
- package/src/lib/mcp/tools/get_image.ts +62 -0
- package/src/lib/mcp/tools/get_neighbors.ts +96 -0
- package/src/lib/mcp/tools/get_summary.ts +18 -0
- package/src/lib/mcp/tools/index.ts +26 -0
- package/src/lib/mcp/tools/lint.ts +552 -0
- package/src/lib/mcp/tools/lint_doc.ts +76 -0
- package/src/lib/mcp/tools/lint_gate.ts +49 -0
- package/src/lib/mcp/tools/list_docs.ts +18 -0
- package/src/lib/mcp/tools/list_summary_drift.ts +95 -0
- package/src/lib/mcp/tools/materialize_subgroup.ts +274 -0
- package/src/lib/mcp/tools/move_doc.ts +439 -0
- package/src/lib/mcp/tools/patch_preamble.ts +145 -0
- package/src/lib/mcp/tools/patch_section.ts +113 -0
- package/src/lib/mcp/tools/read_doc_section.ts +70 -0
- package/src/lib/mcp/tools/rename_doc.ts +167 -0
- package/src/lib/mcp/tools/restore_doc.ts +41 -0
- package/src/lib/mcp/tools/search.ts +36 -0
- package/src/lib/mcp/tools/sections.ts +130 -0
- package/src/lib/mcp/tools/split_doc.ts +129 -0
- package/src/lib/mcp/tools/trash_doc.ts +116 -0
- package/src/lib/mcp/tools/types.ts +7 -0
- package/src/lib/mcp/tools/upload_image.ts +77 -0
- package/src/lib/mcp/tools/validate_args.ts +36 -0
- package/src/lib/mcp/tools/vault.ts +430 -0
- package/src/lib/mcp/tools/write_doc.ts +81 -0
- package/src/lib/mcp/tools/write_doc_preview.ts +59 -0
- package/src/lib/owner/identity.ts +65 -0
- package/src/lib/storage/FilesystemStorage.ts +88 -0
- package/src/lib/storage/SupabaseStorage.ts +358 -0
- package/src/lib/storage/VaultStorage.ts +35 -0
- package/src/lib/storage/index.ts +41 -0
- package/src/lib/supabase/admin.ts +16 -0
- package/src/lib/supabase/client.ts +8 -0
- package/src/lib/supabase/oauth.ts +296 -0
- package/src/lib/supabase/server.ts +22 -0
- package/src/lib/system-nodes.ts +68 -0
- package/src/lib/trash/state.ts +88 -0
- package/src/mcp/server.ts +380 -0
- package/templates/types/CONCEPT.md +21 -0
- package/templates/types/HACKATHON.md +28 -0
- package/templates/types/NOVEL/CHARACTERS.md +29 -0
- package/templates/types/NOVEL/DRAFT.md +15 -0
- package/templates/types/NOVEL/EDITS.md +19 -0
- package/templates/types/NOVEL/INBOX.md +15 -0
- package/templates/types/NOVEL/INSTRUCTIONS.md +32 -0
- package/templates/types/NOVEL/LEARNINGS.md +9 -0
- package/templates/types/NOVEL/OUTBOX.md +15 -0
- package/templates/types/NOVEL/PLOT.md +27 -0
- package/templates/types/NOVEL/WORLDBUILDING.md +23 -0
- package/templates/types/NOVEL.md +32 -0
- package/templates/types/PERSON.md +27 -0
- package/templates/types/PROJECT/BRAND.md +23 -0
- package/templates/types/PROJECT/BUILD.md +9 -0
- package/templates/types/PROJECT/IDEAS.md +11 -0
- package/templates/types/PROJECT/INBOX.md +15 -0
- package/templates/types/PROJECT/INSTRUCTIONS.md +23 -0
- package/templates/types/PROJECT/LEARNINGS.md +9 -0
- package/templates/types/PROJECT/LOGS.md +9 -0
- package/templates/types/PROJECT/OUTBOX.md +15 -0
- package/templates/types/PROJECT/SPRINT.md +56 -0
- package/templates/types/PROJECT.md +26 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import {
|
|
4
|
+
CallToolRequestSchema,
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
type CallToolResult,
|
|
7
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import {
|
|
10
|
+
listDocs,
|
|
11
|
+
listSummaryDrift,
|
|
12
|
+
getSummary,
|
|
13
|
+
getNeighbors,
|
|
14
|
+
getContext,
|
|
15
|
+
getDoc,
|
|
16
|
+
readDocSection,
|
|
17
|
+
search,
|
|
18
|
+
appendSection,
|
|
19
|
+
patchSection,
|
|
20
|
+
writeDocPreview,
|
|
21
|
+
writeDoc,
|
|
22
|
+
createChild,
|
|
23
|
+
addAssociation,
|
|
24
|
+
getImage,
|
|
25
|
+
moveDoc,
|
|
26
|
+
trashDoc,
|
|
27
|
+
restoreDoc,
|
|
28
|
+
} from "../lib/mcp/tools/index.js";
|
|
29
|
+
import type { ToolContext } from "../lib/mcp/tools/types.js";
|
|
30
|
+
// SPRINT-021: this stdio entrypoint is hardcoded to local mode (no
|
|
31
|
+
// clerk_id, no namespace), so mcp_activity logging is intentionally
|
|
32
|
+
// skipped here. Cloud-mode logging lives in app/api/mcp/route.ts.
|
|
33
|
+
|
|
34
|
+
const docsDir = path.resolve(process.env.EMDEE_DOCS ?? path.join(process.cwd(), "docs"));
|
|
35
|
+
const ctx: ToolContext = { mode: "local", docsDir };
|
|
36
|
+
|
|
37
|
+
const server = new Server(
|
|
38
|
+
{
|
|
39
|
+
name: "emdee",
|
|
40
|
+
version: "0.0.1",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
capabilities: { tools: {} },
|
|
44
|
+
instructions: `You are working inside a Emdee vault — a plain-markdown knowledge graph.
|
|
45
|
+
|
|
46
|
+
BEFORE writing or editing any doc:
|
|
47
|
+
1. Call get_doc("INFO.md") to load vault conventions (doc structure, naming rules, relationship sections).
|
|
48
|
+
2. If working on a specific project, call get_doc on that project's INSTRUCTIONS.md first (e.g. get_doc("projects/ATLAS/INSTRUCTIONS.md")).
|
|
49
|
+
3. Use patch_section for incremental edits — never write_doc for single-section changes.
|
|
50
|
+
|
|
51
|
+
Key conventions:
|
|
52
|
+
- Every doc starts with one H1 + one > blockquote summary immediately below it. No summary = invisible to get_summary.
|
|
53
|
+
- Sprints: Child of [[PROJECT — BUILD]] if active/spec, Child of [[PROJECT — LOGS]] if shipped. Change this when a sprint closes.
|
|
54
|
+
- Learnings: individual files under learnings/, Child of [[PROJECT — LEARNINGS]]. LEARNINGS.md is a thin index only.
|
|
55
|
+
- Relationships: Parent of / Child of = taxonomy. Associated with = everything else.`,
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
60
|
+
tools: [
|
|
61
|
+
{
|
|
62
|
+
name: "list_docs",
|
|
63
|
+
description:
|
|
64
|
+
"Enumerate every doc in the vault as {path, title, summary}. Cheap entry point — call this first when starting cold to see what exists. Pass `format: \"text\"` to receive one path per line instead of the JSON envelope (~5× cheaper in tokens).",
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: "object",
|
|
67
|
+
properties: {
|
|
68
|
+
format: { type: "string", enum: ["json", "text"], description: "Response shape. Default `json`. `text` = newline-delimited paths, no envelope." },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "list_summary_drift",
|
|
74
|
+
description:
|
|
75
|
+
"SPRINT-081: return paths whose body has drifted since the summary was last authored. Cloud mode reads persisted hashes from vault_files; local mode returns every doc as candidate. Response is minimal — path + current summary + reason. Pass `format: \"text\"` for newline-delimited paths only. Use this as the entry point to the summariser workflow — feed the returned paths into get_doc + propose new summaries.",
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: "object",
|
|
78
|
+
properties: {
|
|
79
|
+
prefix: { type: "string", description: "Optional path prefix filter." },
|
|
80
|
+
limit: { type: "number", description: "Max candidates to return. Default 20." },
|
|
81
|
+
offset: { type: "number", description: "Skip the first N candidates. Default 0." },
|
|
82
|
+
format: { type: "string", enum: ["json", "text"], description: "Response shape. Default `json`." },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "get_summary",
|
|
88
|
+
description:
|
|
89
|
+
"Return {path, title, summary} for one doc. Use this when you know which doc to look at but don't want to spend tokens on the full body yet. Pass `format: \"text\"` to receive just the blockquote summary text (no path/title wrapper).",
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
path: { type: "string", description: "Relative path of the doc, e.g. people/KIRAN.md" },
|
|
94
|
+
format: { type: "string", enum: ["json", "text"], description: "Response shape. Default `json`. `text` = bare summary line only." },
|
|
95
|
+
},
|
|
96
|
+
required: ["path"],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "get_neighbors",
|
|
101
|
+
description:
|
|
102
|
+
"Return the doc plus its 1-hop neighborhood, categorized by relationship type. Each neighbor is {path, title, summary, note}. `note` is the prose written next to the wiki-link on the declaring side — read it for relationship context. Also returns `mentioned_in`: docs that reference this one in prose without declaring a relationship.",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: { path: { type: "string" } },
|
|
106
|
+
required: ["path"],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "get_context",
|
|
111
|
+
description:
|
|
112
|
+
"Return the focal doc plus its multi-hop neighbourhood within a token budget. Focal + 1-hop neighbours get full bodies (when include_full=true); deeper hops get summary only. Response includes `doc_content_hash` (hash of the FOCAL doc raw content). Pass `expected_content_hash` from a prior call to short-circuit when the focal hasn't changed (returns `{ unchanged: true, path, doc_content_hash }` — note that neighbourhood changes don't bust this; refetch unconditionally if you're chasing a structural change). Nodes that don't fit in budget_tokens land in budget.dropped_paths. Use this instead of chaining get_doc + get_neighbors when you need a coherent local view of one doc and what surrounds it.",
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: {
|
|
116
|
+
path: { type: "string" },
|
|
117
|
+
hops: { type: "number", description: "Max BFS depth, 1–3. Default 2." },
|
|
118
|
+
budget_tokens: { type: "number", description: "Rough token cap (chars÷4). Default 8000." },
|
|
119
|
+
include_full: { type: "boolean", description: "Inline focal + hop-1 bodies. Default true." },
|
|
120
|
+
include_associates: { type: "boolean", description: "Include assoc edges in the walk. Default true." },
|
|
121
|
+
expected_content_hash: { type: "string", description: "Hash from a prior get_context. If matches focal doc, returns { unchanged: true }." },
|
|
122
|
+
},
|
|
123
|
+
required: ["path"],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "get_doc",
|
|
128
|
+
description:
|
|
129
|
+
"Returns title + summary + preamble + section headings + `doc_content_hash` (sha256 of raw content, first 16 hex). Each section in `sections` carries `{ id, heading, content_hash }` — `id` is a stable short string for patch_section / append_section lookup (preferred over `heading` when the heading text is fuzzy or may collide), `content_hash` is the version guard for patch_section. Pass `full=true` for the body. Pass `expected_content_hash` from a prior get_doc response to short-circuit: when matching, returns `{ unchanged: true, path, doc_content_hash }` and skips section parsing entirely. Use `get_context` instead when you need the focal + its neighbourhood. Pass `format: \"text\"` for bare markdown (raw file when full=true, else H1 + summary + `##` headings), no JSON envelope — ~3× cheaper for body-heavy reads.",
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: "object",
|
|
132
|
+
properties: {
|
|
133
|
+
path: { type: "string" },
|
|
134
|
+
full: { type: "boolean", description: "Include the full markdown content. Default false — light envelope only." },
|
|
135
|
+
expected_content_hash: { type: "string", description: "Hash from a prior get_doc response. If matches current doc, returns { unchanged: true }." },
|
|
136
|
+
format: { type: "string", enum: ["json", "text"], description: "Response shape. Default `json`. `text` = bare markdown, no envelope; skips hashes + section IDs so version-guarded writes still require the JSON path." },
|
|
137
|
+
},
|
|
138
|
+
required: ["path"],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "read_doc_section",
|
|
143
|
+
description:
|
|
144
|
+
"Read one H2 section's body without paying for the whole doc. Returns `{ path, section_id, heading, body, content_hash }`. Either `heading` or `section_id` (from get_doc.sections[].id) must be provided — `section_id` is preferred when available because it's an exact match instead of a fuzzy heading-name lookup. Mismatch returns `section_id_heading_mismatch`. Pass `expected_content_hash` from a prior read to short-circuit: when matching, returns `{ unchanged: true, section_id, content_hash }`. Use this instead of get_doc(full=true) when you only need one section.",
|
|
145
|
+
inputSchema: {
|
|
146
|
+
type: "object",
|
|
147
|
+
properties: {
|
|
148
|
+
path: { type: "string" },
|
|
149
|
+
heading: { type: "string" },
|
|
150
|
+
section_id: { type: "string", description: "Preferred lookup key from get_doc.sections[].id." },
|
|
151
|
+
expected_content_hash: { type: "string", description: "Hash from a prior read. If matches, returns { unchanged: true }." },
|
|
152
|
+
},
|
|
153
|
+
required: ["path"],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: "search",
|
|
158
|
+
description:
|
|
159
|
+
"Case-insensitive substring search over titles, summaries, and full content. Returns top matches as {path, title, summary, snippet}. Use this for cold starts when there is no known path.",
|
|
160
|
+
inputSchema: {
|
|
161
|
+
type: "object",
|
|
162
|
+
properties: {
|
|
163
|
+
query: { type: "string" },
|
|
164
|
+
limit: { type: "number", description: "Max results (default 10)" },
|
|
165
|
+
},
|
|
166
|
+
required: ["query"],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: "append_section",
|
|
171
|
+
description:
|
|
172
|
+
"Append markdown content to the end of an existing H2 section. Section-scoped — safer than write_doc for incremental edits. Either `heading` or `section_id` (from get_doc.sections[].id) must be provided — `section_id` is the preferred exact-match lookup. Pass create_if_missing=true (with `heading`) to add a new H2 at the end of the file if not found (default false, returns section_not_found). Returns the new section_id + content_hash for follow-up patches. Pass `gate_on_warnings: [\"code\", ...]` to hard-block the write when any of those lint codes would fire on the proposed content. Edge convention: `## Associated with` is for cross-tree links only (e.g. project↔person, sprint↔learning). Do NOT add an associate that's already a parent/child OR a sibling (shares a parent) of this doc — the hierarchy already conveys that relationship and duplicate edges get suppressed in the graph.",
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: "object",
|
|
175
|
+
additionalProperties: false, properties: {
|
|
176
|
+
path: { type: "string" },
|
|
177
|
+
heading: { type: "string", description: "H2 heading text without the `## ` prefix" },
|
|
178
|
+
section_id: { type: "string", description: "Preferred lookup key from get_doc.sections[].id." },
|
|
179
|
+
body: { type: "string", description: "Markdown body to append to the section" },
|
|
180
|
+
create_if_missing: {
|
|
181
|
+
type: "boolean",
|
|
182
|
+
description: "If true, create the section at end of file when heading is not found. Default false.",
|
|
183
|
+
},
|
|
184
|
+
gate_on_warnings: {
|
|
185
|
+
type: "array", items: { type: "string" },
|
|
186
|
+
description: "Lint codes that hard-block the write when they would fire on the proposed content. Default [].",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
required: ["path", "body"],
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: "patch_section",
|
|
194
|
+
description:
|
|
195
|
+
"Replace the body of an existing H2 section. Version-guarded: pass expected_content_hash from a prior get_doc, append_section, or patch_section response. Either `heading` or `section_id` (from get_doc.sections[].id) must be provided — `section_id` is the preferred exact-match lookup. If both are provided and resolve to different sections, returns `section_id_heading_mismatch`. Mismatch on the hash returns a structured version_conflict error with the actual hash so you can re-read and reconcile. Pass `gate_on_warnings: [\"code\", ...]` to hard-block the write when any of those lint codes would fire on the proposed content. This is the ONLY safe path for destructive section edits — never use write_doc for incremental edits, it replaces the entire file and silently loses content. Edge convention: `## Associated with` is for cross-tree links only. Do NOT add an associate that's already a parent/child OR a sibling (shares a parent) — the hierarchy already conveys that relationship and duplicate edges get suppressed in the graph.",
|
|
196
|
+
inputSchema: {
|
|
197
|
+
type: "object",
|
|
198
|
+
additionalProperties: false, properties: {
|
|
199
|
+
path: { type: "string" },
|
|
200
|
+
heading: { type: "string", description: "H2 heading text without the `## ` prefix" },
|
|
201
|
+
section_id: { type: "string", description: "Preferred lookup key from get_doc.sections[].id." },
|
|
202
|
+
body: { type: "string", description: "New body content for the section" },
|
|
203
|
+
expected_content_hash: {
|
|
204
|
+
type: "string",
|
|
205
|
+
description: "Short hash of the section's current body (from get_doc.sections or a previous mutation's response).",
|
|
206
|
+
},
|
|
207
|
+
gate_on_warnings: {
|
|
208
|
+
type: "array", items: { type: "string" },
|
|
209
|
+
description: "Lint codes that hard-block the write when they would fire on the proposed content. Default [].",
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
required: ["path", "body", "expected_content_hash"],
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: "write_doc_preview",
|
|
217
|
+
description:
|
|
218
|
+
"Preview the diff that write_doc would produce. ALWAYS call this before write_doc — write_doc replaces the entire file and silently destroys sections not present in the new payload. If the change is section-scoped, prefer append_section or patch_section instead.",
|
|
219
|
+
inputSchema: {
|
|
220
|
+
type: "object",
|
|
221
|
+
additionalProperties: false, properties: {
|
|
222
|
+
path: { type: "string" },
|
|
223
|
+
content: { type: "string", description: "Proposed new content for the entire file" },
|
|
224
|
+
},
|
|
225
|
+
required: ["path", "content"],
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: "write_doc",
|
|
230
|
+
description:
|
|
231
|
+
"Create or overwrite a markdown doc at the given relative path. DESTRUCTIVE — full-file replacement, silently deletes any content not in the new payload. Use append_section or patch_section for incremental edits. Always run write_doc_preview first to see what would be lost. Pass `gate_on_warnings: [\"code\", ...]` to hard-block the write when any of those lint codes would fire on the proposed content. Edge convention: `## Associated with` is for cross-tree links only. Do NOT add an associate that's already a parent/child OR a sibling (shares a parent) — the hierarchy already conveys that relationship and duplicate edges get suppressed in the graph.",
|
|
232
|
+
inputSchema: {
|
|
233
|
+
type: "object",
|
|
234
|
+
additionalProperties: false, properties: {
|
|
235
|
+
path: { type: "string" },
|
|
236
|
+
content: { type: "string" },
|
|
237
|
+
gate_on_warnings: {
|
|
238
|
+
type: "array", items: { type: "string" },
|
|
239
|
+
description: "Lint codes that hard-block the write when they would fire on the proposed content. Default [].",
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
required: ["path", "content"],
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: "create_child",
|
|
247
|
+
description:
|
|
248
|
+
"Atomic create-and-link: writes a new doc with the canonical scaffold (H1 + summary placeholder + Child of / Parent of / Associated with / Notes) AND patches the parent's `## Parent of` to add the new bullet. Collapses the 5-round-trip add-child flow into one call. Use this instead of write_doc + patch_section for adding child nodes. `child_path` defaults to `<parent dir>/<sanitized title>.md`. Pass `gate_on_warnings` to hard-block on lint codes; multiple_child_of is always hard-gated internally.",
|
|
249
|
+
inputSchema: {
|
|
250
|
+
type: "object",
|
|
251
|
+
additionalProperties: false, properties: {
|
|
252
|
+
parent_path: { type: "string" },
|
|
253
|
+
title: { type: "string" },
|
|
254
|
+
body: { type: "string", description: "Optional body content appended after ## Notes." },
|
|
255
|
+
summary: { type: "string", description: "Optional blockquote summary; placeholder if omitted." },
|
|
256
|
+
child_path: { type: "string", description: "Optional path override. Default: <parent dir>/<sanitized title>.md." },
|
|
257
|
+
gate_on_warnings: {
|
|
258
|
+
type: "array", items: { type: "string" },
|
|
259
|
+
description: "Lint codes to hard-block on. Default [].",
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
required: ["parent_path", "title"],
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: "add_association",
|
|
267
|
+
description:
|
|
268
|
+
"Atomic two-sided assoc: patches both docs' `## Associated with` to include the other (with optional shared label, identical on both bullets). Hard-refuses if the pair is already linked hierarchically OR if they share a parent (siblings) — returns `would_duplicate_hierarchy` with the existing edge info. Idempotent: if both sides already declare the assoc, returns ok with `a_updated: false, b_updated: false`. Use this instead of two patch_section calls for cross-tree links.",
|
|
269
|
+
inputSchema: {
|
|
270
|
+
type: "object",
|
|
271
|
+
additionalProperties: false, properties: {
|
|
272
|
+
a_path: { type: "string" },
|
|
273
|
+
b_path: { type: "string" },
|
|
274
|
+
label: { type: "string", description: "Optional shared label appended as ` — <label>` to both bullets." },
|
|
275
|
+
gate_on_warnings: {
|
|
276
|
+
type: "array", items: { type: "string" },
|
|
277
|
+
description: "Lint codes to hard-block on (in addition to associate_duplicates_hierarchy and sibling_assoc_redundant which are always hard-gated). Default [].",
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
required: ["a_path", "b_path"],
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: "get_image",
|
|
285
|
+
description:
|
|
286
|
+
"Fetch an image from the vault and return it as a visual content block so you can see and analyze it. Use this to label images (patch_preamble) and associate them with relevant docs (add_association). Returns { doc_path, image_url } plus the image bytes.",
|
|
287
|
+
inputSchema: {
|
|
288
|
+
type: "object",
|
|
289
|
+
properties: {
|
|
290
|
+
doc_path: { type: "string", description: "Path to the image doc, e.g. images/photo-2026-06-09.md." },
|
|
291
|
+
},
|
|
292
|
+
required: ["doc_path"],
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
name: "move_doc",
|
|
297
|
+
description:
|
|
298
|
+
"Atomic reparenting. Removes the child's bullet from old parent's `## Parent of`, adds it to new parent's, and rewrites the child's `## Child of` to declare the new parent — all in one call. Replaces the 3-write hash-guarded patch_section dance that drops asymmetric edges if any step fails. If the child has more than one Child of bullet, `old_parent_path` MUST disambiguate; otherwise refuses with `ambiguous_parent`. Idempotent.",
|
|
299
|
+
inputSchema: {
|
|
300
|
+
type: "object",
|
|
301
|
+
additionalProperties: false, properties: {
|
|
302
|
+
path: { type: "string", description: "Child doc path to reparent." },
|
|
303
|
+
new_parent_path: { type: "string", description: "Path of the new parent doc." },
|
|
304
|
+
old_parent_path: {
|
|
305
|
+
type: "string",
|
|
306
|
+
description: "Disambiguating old parent when the child has multiple Child of bullets. Required only in that case.",
|
|
307
|
+
},
|
|
308
|
+
position: {
|
|
309
|
+
type: "number",
|
|
310
|
+
description: "Optional 0-indexed bullet position in the new parent's Parent of. Default: append at end.",
|
|
311
|
+
},
|
|
312
|
+
gate_on_warnings: {
|
|
313
|
+
type: "array", items: { type: "string" },
|
|
314
|
+
description: "Lint codes to hard-block on. Default [].",
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
required: ["path", "new_parent_path"],
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
name: "trash_doc",
|
|
322
|
+
description:
|
|
323
|
+
"Flag a doc as trashed without reparenting or rewriting its markdown. The doc's `## Child of` and `## Parent of` stay intact so restore is lossless. State persists in `.emdee/trashed.json` keyed by doc path. Idempotent.",
|
|
324
|
+
inputSchema: {
|
|
325
|
+
type: "object",
|
|
326
|
+
additionalProperties: false, properties: {
|
|
327
|
+
path: { type: "string", description: "Path of the doc to trash." },
|
|
328
|
+
original_parent_path: {
|
|
329
|
+
type: "string",
|
|
330
|
+
description: "Override the auto-derived restore target. Use when the Child of bullet is ambiguous or unresolvable.",
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
required: ["path"],
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: "restore_doc",
|
|
338
|
+
description:
|
|
339
|
+
"Reverse a previous `trash_doc`. Clears the doc's entry in `.emdee/trashed.json` so the renderer surfaces it again under its original parent.",
|
|
340
|
+
inputSchema: {
|
|
341
|
+
type: "object",
|
|
342
|
+
additionalProperties: false, properties: {
|
|
343
|
+
path: { type: "string", description: "Path of the trashed doc to restore." },
|
|
344
|
+
},
|
|
345
|
+
required: ["path"],
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
}));
|
|
350
|
+
|
|
351
|
+
server.setRequestHandler(CallToolRequestSchema, async (req): Promise<CallToolResult> => {
|
|
352
|
+
const { name, arguments: args } = req.params;
|
|
353
|
+
const a = args ?? {};
|
|
354
|
+
|
|
355
|
+
switch (name) {
|
|
356
|
+
case "list_docs": return await listDocs(ctx, a) as CallToolResult;
|
|
357
|
+
case "list_summary_drift": return await listSummaryDrift(ctx, a) as CallToolResult;
|
|
358
|
+
case "get_summary": return await getSummary(ctx, a) as CallToolResult;
|
|
359
|
+
case "get_neighbors": return await getNeighbors(ctx, a) as CallToolResult;
|
|
360
|
+
case "get_context": return await getContext(ctx, a) as CallToolResult;
|
|
361
|
+
case "get_doc": return await getDoc(ctx, a) as CallToolResult;
|
|
362
|
+
case "read_doc_section": return await readDocSection(ctx, a) as CallToolResult;
|
|
363
|
+
case "search": return await search(ctx, a) as CallToolResult;
|
|
364
|
+
case "append_section": return await appendSection(ctx, a) as CallToolResult;
|
|
365
|
+
case "patch_section": return await patchSection(ctx, a) as CallToolResult;
|
|
366
|
+
case "write_doc_preview": return await writeDocPreview(ctx, a) as CallToolResult;
|
|
367
|
+
case "write_doc": return await writeDoc(ctx, a) as CallToolResult;
|
|
368
|
+
case "create_child": return await createChild(ctx, a) as CallToolResult;
|
|
369
|
+
case "add_association": return await addAssociation(ctx, a) as CallToolResult;
|
|
370
|
+
case "get_image": return await getImage(ctx, a) as CallToolResult;
|
|
371
|
+
case "move_doc": return await moveDoc(ctx, a) as CallToolResult;
|
|
372
|
+
case "trash_doc": return await trashDoc(ctx, a) as CallToolResult;
|
|
373
|
+
case "restore_doc": return await restoreDoc(ctx, a) as CallToolResult;
|
|
374
|
+
default:
|
|
375
|
+
throw new Error(`unknown tool: ${name}`);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const transport = new StdioServerTransport();
|
|
380
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# CONCEPT-NAME
|
|
2
|
+
|
|
3
|
+
> One-line summary of the concept and why it matters. Replace.
|
|
4
|
+
|
|
5
|
+
## Child of
|
|
6
|
+
|
|
7
|
+
* [[Parent doc]]
|
|
8
|
+
|
|
9
|
+
## Associated with
|
|
10
|
+
|
|
11
|
+
* [[Related doc]] — relationship prose
|
|
12
|
+
|
|
13
|
+
## Context
|
|
14
|
+
|
|
15
|
+
What the concept is, why it exists, what problem it addresses. A few paragraphs of stable reference content.
|
|
16
|
+
|
|
17
|
+
## Notes
|
|
18
|
+
|
|
19
|
+
* Open questions or observations.
|
|
20
|
+
|
|
21
|
+
— <author>, <YYYY-MM-DD>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# HACKATHON-NAME
|
|
2
|
+
|
|
3
|
+
> One-line summary of the hackathon and what you built there. Replace.
|
|
4
|
+
|
|
5
|
+
## Child of
|
|
6
|
+
|
|
7
|
+
* [[HACKATHONS]]
|
|
8
|
+
|
|
9
|
+
## Associated with
|
|
10
|
+
|
|
11
|
+
* [[Project built here]] — prose
|
|
12
|
+
* [[Teammate]] — prose
|
|
13
|
+
|
|
14
|
+
## Details
|
|
15
|
+
|
|
16
|
+
**When:** YYYY-MM-DD
|
|
17
|
+
**Where:** Venue / online
|
|
18
|
+
**Status:** Upcoming / In progress / Done
|
|
19
|
+
|
|
20
|
+
## Outcome
|
|
21
|
+
|
|
22
|
+
What was built, what was submitted, what placed. One or two short paragraphs.
|
|
23
|
+
|
|
24
|
+
## Reflections
|
|
25
|
+
|
|
26
|
+
What went well, what didn't, what to do differently next time. Promote any reusable lessons up to the project's LEARNINGS or BRAIN.
|
|
27
|
+
|
|
28
|
+
— <author>, <YYYY-MM-DD>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# <NOVEL> — CHARACTERS
|
|
2
|
+
|
|
3
|
+
> Cast of [[<NOVEL>]] — who's in it, what they want, how they sound. The voice bible.
|
|
4
|
+
|
|
5
|
+
## Child of
|
|
6
|
+
|
|
7
|
+
* [[<NOVEL>]]
|
|
8
|
+
|
|
9
|
+
## Protagonist
|
|
10
|
+
|
|
11
|
+
**Name:**
|
|
12
|
+
**Want:** (external goal driving the plot)
|
|
13
|
+
**Need:** (internal arc — what they must learn)
|
|
14
|
+
**Voice:** sample line that captures their speech pattern
|
|
15
|
+
**Arc:** beginning → end transformation
|
|
16
|
+
|
|
17
|
+
## Antagonist
|
|
18
|
+
|
|
19
|
+
**Name:**
|
|
20
|
+
**Want:**
|
|
21
|
+
**Voice:**
|
|
22
|
+
**Why they oppose the protagonist:**
|
|
23
|
+
|
|
24
|
+
## Supporting cast
|
|
25
|
+
|
|
26
|
+
Brief entries — name, role, one defining trait. Promote to full entry if they become important enough to need their own arc.
|
|
27
|
+
|
|
28
|
+
* **Name** — role, defining trait
|
|
29
|
+
* ...
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# <NOVEL> — DRAFT
|
|
2
|
+
|
|
3
|
+
> Current draft state for [[<NOVEL>]] — the prose being actively written. Write forward; capture revision ideas in [[EDITS]] rather than re-editing here.
|
|
4
|
+
|
|
5
|
+
## Child of
|
|
6
|
+
|
|
7
|
+
* [[<NOVEL>]]
|
|
8
|
+
|
|
9
|
+
## Current scene
|
|
10
|
+
|
|
11
|
+
What you're writing right now. Replace as you advance through the manuscript.
|
|
12
|
+
|
|
13
|
+
## Draft body
|
|
14
|
+
|
|
15
|
+
The actual prose. Either grows here as one long file, or each chapter is its own sub-file under `<novel>/drafts/`. Choose based on length.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# <NOVEL> — EDITS
|
|
2
|
+
|
|
3
|
+
> Revision notes for [[<NOVEL>]] — changes to make, inconsistencies caught, beta feedback to address. Append while drafting; resolve in revision passes.
|
|
4
|
+
|
|
5
|
+
## Child of
|
|
6
|
+
|
|
7
|
+
* [[<NOVEL>]]
|
|
8
|
+
|
|
9
|
+
## Pending revisions
|
|
10
|
+
|
|
11
|
+
* `[scene/chapter ref]` — what to change, why.
|
|
12
|
+
|
|
13
|
+
## Resolved
|
|
14
|
+
|
|
15
|
+
* `[date]` `[ref]` — what was changed.
|
|
16
|
+
|
|
17
|
+
## Open craft questions
|
|
18
|
+
|
|
19
|
+
Things that aren't simple fixes — structural problems, voice questions, character arc concerns. Promote to LEARNINGS once resolved if they generalized.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# <NOVEL> — INBOX
|
|
2
|
+
|
|
3
|
+
> Incoming proposals for [[<NOVEL>]] — beta feedback, agent queries, ideas from other novels or projects. Triage: actionable items move to EDITS or PLOT; craft-level lessons that generalize move to LEARNINGS.
|
|
4
|
+
|
|
5
|
+
## Child of
|
|
6
|
+
|
|
7
|
+
* [[<NOVEL>]]
|
|
8
|
+
|
|
9
|
+
## Open
|
|
10
|
+
|
|
11
|
+
* (none yet)
|
|
12
|
+
|
|
13
|
+
## Triaged
|
|
14
|
+
|
|
15
|
+
* (none yet)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# <NOVEL> — INSTRUCTIONS
|
|
2
|
+
|
|
3
|
+
> Operating protocol for writing [[<NOVEL>]]. Read this first when starting any writing session — daily cadence, draft workflow, where to put what.
|
|
4
|
+
|
|
5
|
+
## Child of
|
|
6
|
+
|
|
7
|
+
* [[<NOVEL>]]
|
|
8
|
+
|
|
9
|
+
## Session start protocol
|
|
10
|
+
|
|
11
|
+
1. Read CONTEXT (front-door) to remember the premise.
|
|
12
|
+
2. Check PLOT for where you are in the arc.
|
|
13
|
+
3. Check DRAFT for the current scene being written.
|
|
14
|
+
4. Skim EDITS for any pending revision notes that affect today's writing.
|
|
15
|
+
5. Then write into DRAFT.
|
|
16
|
+
|
|
17
|
+
## Writing discipline
|
|
18
|
+
|
|
19
|
+
- Wordcount target per session: <fill in>
|
|
20
|
+
- Draft cadence: <every weekday / weekends only / etc.>
|
|
21
|
+
- New scenes go into DRAFT. Don't edit while drafting — write forward, capture revision ideas in EDITS for later.
|
|
22
|
+
- Character voice questions: check CHARACTERS first; if unclear, write a placeholder in DRAFT and a question in EDITS.
|
|
23
|
+
|
|
24
|
+
## Revision protocol
|
|
25
|
+
|
|
26
|
+
- Beta feedback lands in INBOX. Triage weekly: actionable items move to EDITS, others archived.
|
|
27
|
+
- A revision pass means reading DRAFT linearly while consulting EDITS. Apply changes, then move addressed EDITS entries to LEARNINGS if they generalized into a craft lesson.
|
|
28
|
+
|
|
29
|
+
## Communication conventions
|
|
30
|
+
|
|
31
|
+
- Inbox/Outbox: cross-project coordination if you have multiple novels in flight or are running a Patreon/serial.
|
|
32
|
+
- Queries to agents go in OUTBOX with status (`pending`, `rejected`, `requested-material`, etc.).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# <NOVEL> — LEARNINGS
|
|
2
|
+
|
|
3
|
+
> Craft lessons distilled from writing [[<NOVEL>]] — what worked, what didn't, anti-patterns in your own writing. Dated entries, supersede explicitly. See [[INFO]] → Writing conventions → LEARNINGS authoring format.
|
|
4
|
+
|
|
5
|
+
## Child of
|
|
6
|
+
|
|
7
|
+
* [[<NOVEL>]]
|
|
8
|
+
|
|
9
|
+
* (none yet)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# <NOVEL> — OUTBOX
|
|
2
|
+
|
|
3
|
+
> Outgoing for [[<NOVEL>]] — agent queries, submission tracking, beta-reader requests, anything sent out. Each entry carries status (`pending`, `rejected`, `requested-material`, `accepted`).
|
|
4
|
+
|
|
5
|
+
## Child of
|
|
6
|
+
|
|
7
|
+
* [[<NOVEL>]]
|
|
8
|
+
|
|
9
|
+
## Open
|
|
10
|
+
|
|
11
|
+
* (none yet)
|
|
12
|
+
|
|
13
|
+
## Shipped
|
|
14
|
+
|
|
15
|
+
* (none yet)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# <NOVEL> — PLOT
|
|
2
|
+
|
|
3
|
+
> Story arc and structural beats for [[<NOVEL>]]. The roadmap — what happens, in what order, to what end. Distinct from DRAFT (the actual prose being written) and EDITS (changes to make).
|
|
4
|
+
|
|
5
|
+
## Child of
|
|
6
|
+
|
|
7
|
+
* [[<NOVEL>]]
|
|
8
|
+
|
|
9
|
+
## Premise
|
|
10
|
+
|
|
11
|
+
One paragraph: the central conflict and stakes.
|
|
12
|
+
|
|
13
|
+
## Structure
|
|
14
|
+
|
|
15
|
+
- **Act 1** — setup, inciting incident
|
|
16
|
+
- **Act 2** — escalation, midpoint reversal
|
|
17
|
+
- **Act 3** — climax, resolution
|
|
18
|
+
|
|
19
|
+
## Beats
|
|
20
|
+
|
|
21
|
+
* Beat 1 — what happens, why it matters
|
|
22
|
+
* Beat 2 — what happens, why it matters
|
|
23
|
+
* ...
|
|
24
|
+
|
|
25
|
+
## Open questions
|
|
26
|
+
|
|
27
|
+
* Plot threads that aren't resolved yet — promote to a beat when resolved, or kill if abandoned.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# <NOVEL> — WORLDBUILDING
|
|
2
|
+
|
|
3
|
+
> Setting, lore, and rules of the world in [[<NOVEL>]]. The ground truth that DRAFT must stay consistent with.
|
|
4
|
+
|
|
5
|
+
## Child of
|
|
6
|
+
|
|
7
|
+
* [[<NOVEL>]]
|
|
8
|
+
|
|
9
|
+
## Setting
|
|
10
|
+
|
|
11
|
+
When and where. One paragraph each on the major locations.
|
|
12
|
+
|
|
13
|
+
## Rules
|
|
14
|
+
|
|
15
|
+
The constraints of the world — magic system, technology level, social structures, anything that defines what's possible vs impossible. If DRAFT contradicts a rule here, either the rule changes (update here, sign + date) or the draft is wrong.
|
|
16
|
+
|
|
17
|
+
## Lore
|
|
18
|
+
|
|
19
|
+
Backstory, history, mythology — only what's actually referenced or felt in the story. Resist the urge to over-build.
|
|
20
|
+
|
|
21
|
+
## Open questions
|
|
22
|
+
|
|
23
|
+
Things you haven't decided yet but might need to. Resolve before they show up in DRAFT.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# NOVEL-TITLE
|
|
2
|
+
|
|
3
|
+
> One-line summary of the novel — premise, genre, current stage. Replace.
|
|
4
|
+
|
|
5
|
+
## Child of
|
|
6
|
+
|
|
7
|
+
* [[NOVELS]]
|
|
8
|
+
|
|
9
|
+
## Associated with
|
|
10
|
+
|
|
11
|
+
* [[Related person]] — beta reader, editor, co-author, inspiration
|
|
12
|
+
|
|
13
|
+
## CONTEXT
|
|
14
|
+
|
|
15
|
+
Premise, genre, length target, intended audience, current stage. The stable "what this novel is" — different from PLOT which contains the actual story arc.
|
|
16
|
+
|
|
17
|
+
A novel front-door file holds identity + relationships + premise context. The story itself lives in the tier files: PLOT (arc), CHARACTERS (cast), WORLDBUILDING (setting), DRAFT (current writing), EDITS (revision tracking), LEARNINGS (craft lessons), INBOX/OUTBOX (cross-novel coordination if you have multiple in flight), INSTRUCTIONS (your operating protocol for this novel — daily wordcount, draft cadence, beta-reader workflow).
|
|
18
|
+
|
|
19
|
+
<!--
|
|
20
|
+
Tier files for this novel live in `docs/novels/NOVEL-TITLE/`:
|
|
21
|
+
- INSTRUCTIONS.md — operating protocol for this novel (cadence, wordcount target, draft workflow)
|
|
22
|
+
- PLOT.md — story arc, beats, structure
|
|
23
|
+
- CHARACTERS.md — cast, voice, motivation
|
|
24
|
+
- WORLDBUILDING.md — setting, lore, rules of the world
|
|
25
|
+
- DRAFT.md — current draft state (current scene / chapter being written)
|
|
26
|
+
- EDITS.md — revision notes, beta feedback to address
|
|
27
|
+
- LEARNINGS.md — craft lessons distilled across drafts
|
|
28
|
+
- INBOX.md — incoming proposals (from other novels, agents, beta readers)
|
|
29
|
+
- OUTBOX.md — outgoing proposals (queries to agents, submissions)
|
|
30
|
+
|
|
31
|
+
Tier files declare `Child of [[NOVEL-TITLE]]` so they nest under this novel in the sidebar.
|
|
32
|
+
-->
|