@aisystemresources/emdee 0.1.1 → 0.2.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 +32 -0
- package/bin/emdee.js +475 -4
- package/package.json +2 -1
- package/skills/emdee-conventions.md +106 -0
- package/skills/emdee-describe-image.md +98 -0
- package/skills/emdee-onboarder.md +121 -0
- package/skills/emdee-summariser.md +98 -0
- package/src/cli/auth-commands.ts +72 -0
- package/src/cli/auth.ts +215 -0
- package/src/cli/read-commands.ts +245 -15
- package/src/cli/remote-client.ts +74 -0
- package/src/cli/skills-install.ts +54 -0
- package/src/cli/write-commands.ts +349 -0
- package/src/lib/mcp/tools/add_association.ts +2 -14
- package/src/lib/mcp/tools/create_child.ts +0 -2
- package/src/mcp/server.ts +2 -1
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// SPRINT-091 chunk 2: write-side CLI verbs.
|
|
2
|
+
//
|
|
3
|
+
// Dispatcher that fronts the 8 most-common write tools as CLI commands.
|
|
4
|
+
// Every verb accepts --remote (routes through cloud) and --json (returns
|
|
5
|
+
// the raw MCP envelope for machine consumption). Local mode calls the tool
|
|
6
|
+
// function directly with the same ToolContext shape MCP uses — same code,
|
|
7
|
+
// same guards, same errors.
|
|
8
|
+
//
|
|
9
|
+
// One dispatcher rather than one file per verb keeps the wire-up compact.
|
|
10
|
+
// bin/emdee.js just shells `write-commands.ts <verb> <args>` for each.
|
|
11
|
+
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { parseArgs, type ParseArgsConfig } from "node:util";
|
|
14
|
+
import type { ToolContext } from "../lib/mcp/tools/types";
|
|
15
|
+
import { patchSection } from "../lib/mcp/tools/patch_section";
|
|
16
|
+
import { appendSection } from "../lib/mcp/tools/append_section";
|
|
17
|
+
import { appendDoc } from "../lib/mcp/tools/append_doc";
|
|
18
|
+
import { patchPreamble } from "../lib/mcp/tools/patch_preamble";
|
|
19
|
+
import { createChild } from "../lib/mcp/tools/create_child";
|
|
20
|
+
import { addAssociation } from "../lib/mcp/tools/add_association";
|
|
21
|
+
import { moveDoc } from "../lib/mcp/tools/move_doc";
|
|
22
|
+
import { renameDoc } from "../lib/mcp/tools/rename_doc";
|
|
23
|
+
import { writeDoc } from "../lib/mcp/tools/write_doc";
|
|
24
|
+
import { writeDocPreview } from "../lib/mcp/tools/write_doc_preview";
|
|
25
|
+
import { trashDoc } from "../lib/mcp/tools/trash_doc";
|
|
26
|
+
import { restoreDoc } from "../lib/mcp/tools/restore_doc";
|
|
27
|
+
import { deleteDoc } from "../lib/mcp/tools/delete_doc";
|
|
28
|
+
import { callTool, unwrapText } from "./remote-client";
|
|
29
|
+
import { NeedsLoginError } from "./auth";
|
|
30
|
+
|
|
31
|
+
const docsDir = path.resolve(process.env.EMDEE_DOCS ?? path.join(process.cwd(), "docs"));
|
|
32
|
+
|
|
33
|
+
type ToolFn = (ctx: ToolContext, args: Record<string, unknown>) => Promise<unknown>;
|
|
34
|
+
|
|
35
|
+
interface VerbSpec {
|
|
36
|
+
toolName: string;
|
|
37
|
+
toolFn: ToolFn;
|
|
38
|
+
parse: ParseArgsConfig["options"];
|
|
39
|
+
buildArgs: (values: Record<string, string | boolean | undefined>) => Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Shared flags. Every write verb accepts these.
|
|
43
|
+
const COMMON = {
|
|
44
|
+
remote: { type: "boolean" },
|
|
45
|
+
json: { type: "boolean" },
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
48
|
+
function asString(v: unknown): string {
|
|
49
|
+
return typeof v === "string" ? v : "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function optionalString(v: unknown): string | undefined {
|
|
53
|
+
return typeof v === "string" && v.trim() ? v : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const VERBS: Record<string, VerbSpec> = {
|
|
57
|
+
"patch-section": {
|
|
58
|
+
toolName: "patch_section",
|
|
59
|
+
toolFn: patchSection as unknown as ToolFn,
|
|
60
|
+
parse: {
|
|
61
|
+
...COMMON,
|
|
62
|
+
path: { type: "string" },
|
|
63
|
+
"section-id": { type: "string" },
|
|
64
|
+
heading: { type: "string" },
|
|
65
|
+
body: { type: "string" },
|
|
66
|
+
"expected-hash": { type: "string" },
|
|
67
|
+
"gate-on": { type: "string", multiple: true },
|
|
68
|
+
},
|
|
69
|
+
buildArgs: (v) => {
|
|
70
|
+
const args: Record<string, unknown> = {
|
|
71
|
+
path: asString(v.path),
|
|
72
|
+
body: asString(v.body),
|
|
73
|
+
expected_content_hash: asString(v["expected-hash"]),
|
|
74
|
+
};
|
|
75
|
+
if (v["section-id"]) args.section_id = v["section-id"];
|
|
76
|
+
if (v.heading) args.heading = v.heading;
|
|
77
|
+
if (Array.isArray(v["gate-on"])) args.gate_on_warnings = v["gate-on"];
|
|
78
|
+
return args;
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
"append-section": {
|
|
82
|
+
toolName: "append_section",
|
|
83
|
+
toolFn: appendSection as unknown as ToolFn,
|
|
84
|
+
parse: {
|
|
85
|
+
...COMMON,
|
|
86
|
+
path: { type: "string" },
|
|
87
|
+
"section-id": { type: "string" },
|
|
88
|
+
heading: { type: "string" },
|
|
89
|
+
body: { type: "string" },
|
|
90
|
+
"create-if-missing": { type: "boolean" },
|
|
91
|
+
"gate-on": { type: "string", multiple: true },
|
|
92
|
+
},
|
|
93
|
+
buildArgs: (v) => {
|
|
94
|
+
const args: Record<string, unknown> = {
|
|
95
|
+
path: asString(v.path),
|
|
96
|
+
body: asString(v.body),
|
|
97
|
+
};
|
|
98
|
+
if (v["section-id"]) args.section_id = v["section-id"];
|
|
99
|
+
if (v.heading) args.heading = v.heading;
|
|
100
|
+
if (v["create-if-missing"]) args.create_if_missing = true;
|
|
101
|
+
if (Array.isArray(v["gate-on"])) args.gate_on_warnings = v["gate-on"];
|
|
102
|
+
return args;
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
"append-doc": {
|
|
106
|
+
toolName: "append_doc",
|
|
107
|
+
toolFn: appendDoc as unknown as ToolFn,
|
|
108
|
+
parse: {
|
|
109
|
+
...COMMON,
|
|
110
|
+
path: { type: "string" },
|
|
111
|
+
body: { type: "string" },
|
|
112
|
+
"gate-on": { type: "string", multiple: true },
|
|
113
|
+
},
|
|
114
|
+
buildArgs: (v) => {
|
|
115
|
+
const args: Record<string, unknown> = { path: asString(v.path), body: asString(v.body) };
|
|
116
|
+
if (Array.isArray(v["gate-on"])) args.gate_on_warnings = v["gate-on"];
|
|
117
|
+
return args;
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
"patch-preamble": {
|
|
121
|
+
toolName: "patch_preamble",
|
|
122
|
+
toolFn: patchPreamble as unknown as ToolFn,
|
|
123
|
+
parse: {
|
|
124
|
+
...COMMON,
|
|
125
|
+
path: { type: "string" },
|
|
126
|
+
body: { type: "string" },
|
|
127
|
+
"expected-hash": { type: "string" },
|
|
128
|
+
"gate-on": { type: "string", multiple: true },
|
|
129
|
+
},
|
|
130
|
+
buildArgs: (v) => {
|
|
131
|
+
const args: Record<string, unknown> = {
|
|
132
|
+
path: asString(v.path),
|
|
133
|
+
body: asString(v.body),
|
|
134
|
+
expected_content_hash: asString(v["expected-hash"]),
|
|
135
|
+
};
|
|
136
|
+
if (Array.isArray(v["gate-on"])) args.gate_on_warnings = v["gate-on"];
|
|
137
|
+
return args;
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
"create-child": {
|
|
141
|
+
toolName: "create_child",
|
|
142
|
+
toolFn: createChild as unknown as ToolFn,
|
|
143
|
+
parse: {
|
|
144
|
+
...COMMON,
|
|
145
|
+
"parent-path": { type: "string" },
|
|
146
|
+
title: { type: "string" },
|
|
147
|
+
body: { type: "string" },
|
|
148
|
+
summary: { type: "string" },
|
|
149
|
+
"child-path": { type: "string" },
|
|
150
|
+
"gate-on": { type: "string", multiple: true },
|
|
151
|
+
},
|
|
152
|
+
buildArgs: (v) => {
|
|
153
|
+
const args: Record<string, unknown> = {
|
|
154
|
+
parent_path: asString(v["parent-path"]),
|
|
155
|
+
title: asString(v.title),
|
|
156
|
+
};
|
|
157
|
+
const body = optionalString(v.body);
|
|
158
|
+
if (body) args.body = body;
|
|
159
|
+
const summary = optionalString(v.summary);
|
|
160
|
+
if (summary) args.summary = summary;
|
|
161
|
+
const childPath = optionalString(v["child-path"]);
|
|
162
|
+
if (childPath) args.child_path = childPath;
|
|
163
|
+
if (Array.isArray(v["gate-on"])) args.gate_on_warnings = v["gate-on"];
|
|
164
|
+
return args;
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
"add-association": {
|
|
168
|
+
toolName: "add_association",
|
|
169
|
+
toolFn: addAssociation as unknown as ToolFn,
|
|
170
|
+
parse: {
|
|
171
|
+
...COMMON,
|
|
172
|
+
"a-path": { type: "string" },
|
|
173
|
+
"b-path": { type: "string" },
|
|
174
|
+
label: { type: "string" },
|
|
175
|
+
"gate-on": { type: "string", multiple: true },
|
|
176
|
+
},
|
|
177
|
+
buildArgs: (v) => {
|
|
178
|
+
const args: Record<string, unknown> = {
|
|
179
|
+
a_path: asString(v["a-path"]),
|
|
180
|
+
b_path: asString(v["b-path"]),
|
|
181
|
+
};
|
|
182
|
+
const label = optionalString(v.label);
|
|
183
|
+
if (label) args.label = label;
|
|
184
|
+
if (Array.isArray(v["gate-on"])) args.gate_on_warnings = v["gate-on"];
|
|
185
|
+
return args;
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
"move-doc": {
|
|
189
|
+
toolName: "move_doc",
|
|
190
|
+
toolFn: moveDoc as unknown as ToolFn,
|
|
191
|
+
parse: {
|
|
192
|
+
...COMMON,
|
|
193
|
+
path: { type: "string" },
|
|
194
|
+
"new-parent-path": { type: "string" },
|
|
195
|
+
"old-parent-path": { type: "string" },
|
|
196
|
+
position: { type: "string" },
|
|
197
|
+
"gate-on": { type: "string", multiple: true },
|
|
198
|
+
},
|
|
199
|
+
buildArgs: (v) => {
|
|
200
|
+
const args: Record<string, unknown> = {
|
|
201
|
+
path: asString(v.path),
|
|
202
|
+
new_parent_path: asString(v["new-parent-path"]),
|
|
203
|
+
};
|
|
204
|
+
const oldParent = optionalString(v["old-parent-path"]);
|
|
205
|
+
if (oldParent) args.old_parent_path = oldParent;
|
|
206
|
+
const pos = optionalString(v.position);
|
|
207
|
+
if (pos) args.position = Number(pos);
|
|
208
|
+
if (Array.isArray(v["gate-on"])) args.gate_on_warnings = v["gate-on"];
|
|
209
|
+
return args;
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
"rename-doc": {
|
|
213
|
+
toolName: "rename_doc",
|
|
214
|
+
toolFn: renameDoc as unknown as ToolFn,
|
|
215
|
+
parse: {
|
|
216
|
+
...COMMON,
|
|
217
|
+
"old-path": { type: "string" },
|
|
218
|
+
"new-title": { type: "string" },
|
|
219
|
+
"new-path": { type: "string" },
|
|
220
|
+
},
|
|
221
|
+
buildArgs: (v) => {
|
|
222
|
+
const args: Record<string, unknown> = {
|
|
223
|
+
old_path: asString(v["old-path"]),
|
|
224
|
+
new_title: asString(v["new-title"]),
|
|
225
|
+
};
|
|
226
|
+
const newPath = optionalString(v["new-path"]);
|
|
227
|
+
if (newPath) args.new_path = newPath;
|
|
228
|
+
return args;
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
"write-doc": {
|
|
232
|
+
toolName: "write_doc",
|
|
233
|
+
toolFn: writeDoc as unknown as ToolFn,
|
|
234
|
+
parse: {
|
|
235
|
+
...COMMON,
|
|
236
|
+
path: { type: "string" },
|
|
237
|
+
content: { type: "string" },
|
|
238
|
+
"gate-on": { type: "string", multiple: true },
|
|
239
|
+
},
|
|
240
|
+
buildArgs: (v) => {
|
|
241
|
+
const args: Record<string, unknown> = {
|
|
242
|
+
path: asString(v.path),
|
|
243
|
+
content: asString(v.content),
|
|
244
|
+
};
|
|
245
|
+
if (Array.isArray(v["gate-on"])) args.gate_on_warnings = v["gate-on"];
|
|
246
|
+
return args;
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
"write-doc-preview": {
|
|
250
|
+
toolName: "write_doc_preview",
|
|
251
|
+
toolFn: writeDocPreview as unknown as ToolFn,
|
|
252
|
+
parse: {
|
|
253
|
+
...COMMON,
|
|
254
|
+
path: { type: "string" },
|
|
255
|
+
content: { type: "string" },
|
|
256
|
+
},
|
|
257
|
+
buildArgs: (v) => ({
|
|
258
|
+
path: asString(v.path),
|
|
259
|
+
content: asString(v.content),
|
|
260
|
+
}),
|
|
261
|
+
},
|
|
262
|
+
"trash-doc": {
|
|
263
|
+
toolName: "trash_doc",
|
|
264
|
+
toolFn: trashDoc as unknown as ToolFn,
|
|
265
|
+
parse: {
|
|
266
|
+
...COMMON,
|
|
267
|
+
path: { type: "string" },
|
|
268
|
+
"original-parent-path": { type: "string" },
|
|
269
|
+
},
|
|
270
|
+
buildArgs: (v) => {
|
|
271
|
+
const args: Record<string, unknown> = { path: asString(v.path) };
|
|
272
|
+
const op = optionalString(v["original-parent-path"]);
|
|
273
|
+
if (op) args.original_parent_path = op;
|
|
274
|
+
return args;
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
"restore-doc": {
|
|
278
|
+
toolName: "restore_doc",
|
|
279
|
+
toolFn: restoreDoc as unknown as ToolFn,
|
|
280
|
+
parse: { ...COMMON, path: { type: "string" } },
|
|
281
|
+
buildArgs: (v) => ({ path: asString(v.path) }),
|
|
282
|
+
},
|
|
283
|
+
"delete-doc": {
|
|
284
|
+
toolName: "delete_doc",
|
|
285
|
+
toolFn: deleteDoc as unknown as ToolFn,
|
|
286
|
+
parse: { ...COMMON, path: { type: "string" } },
|
|
287
|
+
buildArgs: (v) => ({ path: asString(v.path) }),
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
function formatOutput(result: unknown, wantJson: boolean): string {
|
|
292
|
+
// MCP tools return { content: [{type: "text", text: "..."}] }. The text
|
|
293
|
+
// is JSON. For human-friendly output we pull the parsed JSON out and
|
|
294
|
+
// pretty-print. For --json we return the parsed JSON as-is.
|
|
295
|
+
let payload: unknown = result;
|
|
296
|
+
const withContent = result as { content?: Array<{ type: string; text?: string }> };
|
|
297
|
+
if (withContent.content?.[0]?.type === "text" && typeof withContent.content[0].text === "string") {
|
|
298
|
+
try {
|
|
299
|
+
payload = JSON.parse(withContent.content[0].text);
|
|
300
|
+
} catch {
|
|
301
|
+
payload = withContent.content[0].text;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (wantJson) return JSON.stringify(payload, null, 2);
|
|
305
|
+
if (typeof payload === "string") return payload;
|
|
306
|
+
return JSON.stringify(payload, null, 2);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function runVerb(verbName: string, argv: string[]): Promise<void> {
|
|
310
|
+
const spec = VERBS[verbName];
|
|
311
|
+
if (!spec) {
|
|
312
|
+
process.stderr.write(`unknown write verb: ${verbName}\navailable: ${Object.keys(VERBS).join(", ")}\n`);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
const { values: rawValues } = parseArgs({ args: argv, options: spec.parse, strict: true });
|
|
316
|
+
const values = rawValues as unknown as Record<string, string | boolean | undefined>;
|
|
317
|
+
const args = spec.buildArgs(values);
|
|
318
|
+
const wantJson = Boolean(values.json);
|
|
319
|
+
const remote = Boolean(values.remote);
|
|
320
|
+
|
|
321
|
+
const result = remote
|
|
322
|
+
? await callTool(spec.toolName, args).then((r) => r as unknown)
|
|
323
|
+
: await spec.toolFn({ mode: "local", docsDir }, args);
|
|
324
|
+
|
|
325
|
+
const output = remote
|
|
326
|
+
? formatOutput({ content: [{ type: "text", text: unwrapText(result as { content?: Array<{ type: string; text?: string }> }) }] }, wantJson)
|
|
327
|
+
: formatOutput(result, wantJson);
|
|
328
|
+
|
|
329
|
+
process.stdout.write(output + "\n");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const [, , verb, ...rest] = process.argv;
|
|
333
|
+
|
|
334
|
+
async function main(): Promise<void> {
|
|
335
|
+
if (!verb) {
|
|
336
|
+
process.stderr.write(`usage: emdee <verb> [options]\nverbs: ${Object.keys(VERBS).join(", ")}\n`);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
await runVerb(verb, rest);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
main().catch((err) => {
|
|
343
|
+
if (err instanceof NeedsLoginError) {
|
|
344
|
+
process.stderr.write(`${err.message}\n`);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
348
|
+
process.exit(1);
|
|
349
|
+
});
|
|
@@ -3,8 +3,8 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { validatePath, readVaultFile, writeVaultFile, loadVaultIndex } from "./vault";
|
|
4
4
|
import { lintDocContent } from "./lint";
|
|
5
5
|
import { buildLintVaultContext } from "./lint_doc";
|
|
6
|
-
import { evaluateLintGate
|
|
7
|
-
import type {
|
|
6
|
+
import { evaluateLintGate } from "./lint_gate";
|
|
7
|
+
import type { LintVaultContext } from "./lint";
|
|
8
8
|
import { resolveWikiLink } from "../../../core/resolveLink";
|
|
9
9
|
import type { ToolContext } from "./types";
|
|
10
10
|
import { validateArgs } from "./validate_args";
|
|
@@ -113,16 +113,6 @@ function patchAssociatedWith(content: string, otherTitle: string, label?: string
|
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
function fixesFromWarnings(warnings: LintWarning[], codes: string[]): LintFix[] {
|
|
117
|
-
const codeSet = new Set(codes);
|
|
118
|
-
return warnings.filter((w) => codeSet.has(w.code)).map((w) => ({
|
|
119
|
-
code: w.code,
|
|
120
|
-
line: w.line ?? null,
|
|
121
|
-
fix_suggestion: w.suggestion,
|
|
122
|
-
original_message: w.message,
|
|
123
|
-
}));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
116
|
function json(value: unknown) {
|
|
127
117
|
return { content: [{ type: "text" as const, text: JSON.stringify(value, null, 2) }] };
|
|
128
118
|
}
|
|
@@ -282,11 +272,9 @@ export async function addAssociation(ctx: ToolContext, args: Record<string, unkn
|
|
|
282
272
|
}
|
|
283
273
|
}
|
|
284
274
|
|
|
285
|
-
let bWritten = bPatch.alreadyPresent;
|
|
286
275
|
if (!bPatch.alreadyPresent) {
|
|
287
276
|
try {
|
|
288
277
|
await writeVaultFile(ctx, bPath, bPatch.newContent);
|
|
289
|
-
bWritten = true;
|
|
290
278
|
} catch (err) {
|
|
291
279
|
return json({
|
|
292
280
|
error: "partial_write",
|
|
@@ -291,11 +291,9 @@ export async function createChild(ctx: ToolContext, args: Record<string, unknown
|
|
|
291
291
|
}
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
let parentWritten = alreadyPresent;
|
|
295
294
|
if (!alreadyPresent) {
|
|
296
295
|
try {
|
|
297
296
|
await writeVaultFile(ctx, parentPath, newParentContent);
|
|
298
|
-
parentWritten = true;
|
|
299
297
|
} catch (err) {
|
|
300
298
|
return json({
|
|
301
299
|
error: "partial_write",
|
package/src/mcp/server.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
restoreDoc,
|
|
28
28
|
} from "../lib/mcp/tools/index.js";
|
|
29
29
|
import type { ToolContext } from "../lib/mcp/tools/types.js";
|
|
30
|
+
import pkg from "../../package.json";
|
|
30
31
|
// SPRINT-021: this stdio entrypoint is hardcoded to local mode (no
|
|
31
32
|
// clerk_id, no namespace), so mcp_activity logging is intentionally
|
|
32
33
|
// skipped here. Cloud-mode logging lives in app/api/mcp/route.ts.
|
|
@@ -37,7 +38,7 @@ const ctx: ToolContext = { mode: "local", docsDir };
|
|
|
37
38
|
const server = new Server(
|
|
38
39
|
{
|
|
39
40
|
name: "emdee",
|
|
40
|
-
version:
|
|
41
|
+
version: pkg.version,
|
|
41
42
|
},
|
|
42
43
|
{
|
|
43
44
|
capabilities: { tools: {} },
|