agent-kb 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/LICENSE +19 -0
- package/README.md +139 -0
- package/dist/api/index.d.ts +756 -0
- package/dist/api/index.js +8 -0
- package/dist/chunk-I77A4URT.js +5963 -0
- package/dist/cli/index.d.ts +84 -0
- package/dist/cli/index.js +1022 -0
- package/package.json +68 -0
|
@@ -0,0 +1,1022 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
KbError,
|
|
4
|
+
featureDisabled,
|
|
5
|
+
hasBlockingFindings,
|
|
6
|
+
load,
|
|
7
|
+
openKb
|
|
8
|
+
} from "../chunk-I77A4URT.js";
|
|
9
|
+
|
|
10
|
+
// src/cli/index.ts
|
|
11
|
+
import * as fs2 from "fs";
|
|
12
|
+
import * as os2 from "os";
|
|
13
|
+
import { pathToFileURL } from "url";
|
|
14
|
+
import { Command, CommanderError, InvalidArgumentError, Option } from "commander";
|
|
15
|
+
|
|
16
|
+
// src/content/agent-kb-skill.ts
|
|
17
|
+
import * as fs from "fs";
|
|
18
|
+
import * as os from "os";
|
|
19
|
+
import * as path from "path";
|
|
20
|
+
var AGENT_KB_SKILL = `---
|
|
21
|
+
name: using-agent-kb
|
|
22
|
+
description: Use when interacting with a project's agent-kb knowledge base \u2014 reading entities, writing changes, searching, or maintaining graph integrity.
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# Using agent-kb
|
|
26
|
+
|
|
27
|
+
Agent KB is a local, versioned knowledge graph of typed entities and relations, edited via change sets. All writes go through a propose \u2192 accept flow; reads and searches are lock-free.
|
|
28
|
+
|
|
29
|
+
## Step 1 \u2014 Discover the schema first
|
|
30
|
+
|
|
31
|
+
Always call this before reading or writing. It returns every entity type (with field names, types, required/optional, enum values, and ref targets), all relations, and available context recipes.
|
|
32
|
+
|
|
33
|
+
- CLI: \`agent-kb schema describe\`
|
|
34
|
+
- MCP: \`kb_describe_schema\`
|
|
35
|
+
|
|
36
|
+
## Step 2 \u2014 Read and search
|
|
37
|
+
|
|
38
|
+
### Retrieve and query
|
|
39
|
+
|
|
40
|
+
| Goal | CLI | MCP |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| Get one entity by ID | \`agent-kb get <id> [--links]\` | \`kb_get_entity\` |
|
|
43
|
+
| Query entities by type | \`agent-kb find <type> [--where JSON]\` | \`kb_find_entities\` |
|
|
44
|
+
| Traverse the graph | \`agent-kb related <id>\` | \`kb_related\` |
|
|
45
|
+
| Trace dependency tree | \`agent-kb trace <id>\` | \`kb_trace\` |
|
|
46
|
+
| Assemble context pack | \`agent-kb context <id> [--recipe <name>]\` | \`kb_get_context_pack\` |
|
|
47
|
+
|
|
48
|
+
### Search
|
|
49
|
+
|
|
50
|
+
\`agent-kb search <text> [--mode fts|semantic|hybrid]\` / MCP: \`kb_search\`
|
|
51
|
+
|
|
52
|
+
- **fts** (default) \u2014 full-text keyword search; always available.
|
|
53
|
+
- **semantic** \u2014 vector similarity; requires vectors enabled.
|
|
54
|
+
- **hybrid** \u2014 RRF fusion of fts + semantic; requires vectors enabled.
|
|
55
|
+
|
|
56
|
+
Before using semantic or hybrid: call \`kb_status\` and check \`vector_enabled\`. If \`false\`, those modes return \`FEATURE_DISABLED\`.
|
|
57
|
+
|
|
58
|
+
## Step 3 \u2014 Write workflow (change sets)
|
|
59
|
+
|
|
60
|
+
All writes are atomic and go through a three-step change set flow:
|
|
61
|
+
|
|
62
|
+
1. **Propose** \u2014 \`agent-kb propose <file.json>\` / MCP: \`kb_propose_change_set\`
|
|
63
|
+
2. **Validate** (optional) \u2014 \`agent-kb validate <id>\` / MCP: \`kb_validate_change_set\`
|
|
64
|
+
3. **Accept** \u2014 \`agent-kb accept <id>\` / MCP: \`kb_accept_change_set\`
|
|
65
|
+
|
|
66
|
+
To reject: \`agent-kb reject <id> --reason <text>\` / MCP: \`kb_reject_change_set\`
|
|
67
|
+
|
|
68
|
+
To discover existing change-set IDs:
|
|
69
|
+
- CLI: \`agent-kb changesets\`
|
|
70
|
+
- MCP: \`kb_list_change_sets\`
|
|
71
|
+
|
|
72
|
+
## Integrity and recovery
|
|
73
|
+
|
|
74
|
+
- **Verify** \u2014 \`agent-kb verify\` / MCP: \`kb_verify\` \u2014 check referential consistency and dangling links.
|
|
75
|
+
- **Reindex** \u2014 \`agent-kb reindex\` / MCP: \`kb_reindex\` \u2014 rebuild the derived index (FTS + vectors when enabled). Use after \`VECTOR_INDEX_FAILED\` / \`VECTOR_DIM_MISMATCH\` or to backfill vectors after enabling them.
|
|
76
|
+
`.trim();
|
|
77
|
+
function parseSkillScope(raw) {
|
|
78
|
+
const resolved = raw === true ? "project" : String(raw);
|
|
79
|
+
if (resolved === "project" || resolved === "global") {
|
|
80
|
+
return { ok: true, scope: resolved };
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
message: `invalid scope '${String(raw)}': must be 'global' or 'project'`
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function installSkill(scope, root, home = os.homedir()) {
|
|
88
|
+
const base = scope === "global" ? home : root;
|
|
89
|
+
const dir = path.join(base, ".claude", "skills", "agent-kb");
|
|
90
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
91
|
+
const skillPath = path.join(dir, "SKILL.md");
|
|
92
|
+
fs.writeFileSync(skillPath, AGENT_KB_SKILL, "utf8");
|
|
93
|
+
return { path: skillPath };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/cli/exit-codes.ts
|
|
97
|
+
function exitCodeFor(env) {
|
|
98
|
+
if (env.ok) {
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
switch (env.error.code) {
|
|
102
|
+
case "LOCK_TIMEOUT":
|
|
103
|
+
return 3;
|
|
104
|
+
case "IO_ERROR":
|
|
105
|
+
case "PARSE_ERROR":
|
|
106
|
+
case "INTERNAL":
|
|
107
|
+
return 4;
|
|
108
|
+
default:
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/cli/mcp-command.ts
|
|
114
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
115
|
+
|
|
116
|
+
// src/mcp/tools.ts
|
|
117
|
+
import { z } from "zod";
|
|
118
|
+
function txt(text) {
|
|
119
|
+
return { type: "text", text };
|
|
120
|
+
}
|
|
121
|
+
function parseErr(err) {
|
|
122
|
+
return { content: [txt(`VALIDATION_ERROR: ${err.message}`)], isError: true };
|
|
123
|
+
}
|
|
124
|
+
function envelopeToResult(env) {
|
|
125
|
+
if (env.ok) {
|
|
126
|
+
return {
|
|
127
|
+
content: [txt(JSON.stringify(env.data))],
|
|
128
|
+
structuredContent: env.data
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
content: [txt(`${env.error.code}: ${env.error.message}`)],
|
|
133
|
+
isError: true,
|
|
134
|
+
structuredContent: { error: { code: env.error.code, message: env.error.message } }
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
var DESCRIBE_HINT = "call kb_describe_schema for the entity's fields, enum values, and ref targets";
|
|
138
|
+
var fieldsSchema = z.record(z.string(), z.unknown()).describe(`Field values keyed by field name; types are entity-specific \u2014 ${DESCRIBE_HINT}.`);
|
|
139
|
+
var setSchema = z.record(z.string(), z.unknown()).describe(
|
|
140
|
+
`Fields to set, keyed by field name; a value is a scalar or a {add,remove} list mutation \u2014 ${DESCRIBE_HINT}.`
|
|
141
|
+
);
|
|
142
|
+
var createOpSchema = z.object({
|
|
143
|
+
op: z.literal("create"),
|
|
144
|
+
entity: z.string().describe("Entity type name to create (see kb_describe_schema.entities)."),
|
|
145
|
+
handle: z.string().describe("Temporary @handle naming the new entity so later ops can reference it."),
|
|
146
|
+
fields: fieldsSchema
|
|
147
|
+
}).strict();
|
|
148
|
+
var updateOpSchema = z.object({
|
|
149
|
+
op: z.literal("update"),
|
|
150
|
+
id: z.string().describe("EntityId of the entity to update."),
|
|
151
|
+
base_rev: z.number().describe("Revision the update is based on (optimistic-concurrency guard)."),
|
|
152
|
+
set: setSchema
|
|
153
|
+
}).strict();
|
|
154
|
+
var deleteOpSchema = z.object({
|
|
155
|
+
op: z.literal("delete"),
|
|
156
|
+
id: z.string().describe("EntityId of the entity to delete."),
|
|
157
|
+
base_rev: z.number().describe("Revision the delete is based on (optimistic-concurrency guard).")
|
|
158
|
+
}).strict();
|
|
159
|
+
var linkOpSchema = z.object({
|
|
160
|
+
op: z.literal("link"),
|
|
161
|
+
from: z.string().describe("Source: an EntityId or an @handle defined in this change set."),
|
|
162
|
+
relation: z.string().describe("Relation name (see kb_describe_schema.relations)."),
|
|
163
|
+
to: z.string().describe("Target: an EntityId or an @handle defined in this change set."),
|
|
164
|
+
base_rev: z.number().optional().describe("Optional base revision for concurrency.")
|
|
165
|
+
}).strict();
|
|
166
|
+
var unlinkOpSchema = z.object({
|
|
167
|
+
op: z.literal("unlink"),
|
|
168
|
+
from: z.string().describe("Source EntityId."),
|
|
169
|
+
relation: z.string().describe("Relation name."),
|
|
170
|
+
to: z.string().describe("Target EntityId.")
|
|
171
|
+
}).strict();
|
|
172
|
+
var mcpOpSchema = z.discriminatedUnion("op", [
|
|
173
|
+
createOpSchema,
|
|
174
|
+
updateOpSchema,
|
|
175
|
+
deleteOpSchema,
|
|
176
|
+
linkOpSchema,
|
|
177
|
+
unlinkOpSchema
|
|
178
|
+
]).describe(
|
|
179
|
+
"A change operation; `op` selects the variant \u2014 create (entity+handle+fields), update (id+base_rev+set), delete (id+base_rev), link (from+relation+to), unlink (from+relation+to)."
|
|
180
|
+
);
|
|
181
|
+
var whereValueSchema = z.union([
|
|
182
|
+
z.string(),
|
|
183
|
+
z.number(),
|
|
184
|
+
z.boolean(),
|
|
185
|
+
z.null(),
|
|
186
|
+
z.array(z.unknown()),
|
|
187
|
+
z.looseObject({
|
|
188
|
+
eq: z.unknown().optional(),
|
|
189
|
+
ne: z.unknown().optional(),
|
|
190
|
+
gt: z.unknown().optional(),
|
|
191
|
+
gte: z.unknown().optional(),
|
|
192
|
+
lt: z.unknown().optional(),
|
|
193
|
+
lte: z.unknown().optional(),
|
|
194
|
+
in: z.unknown().optional(),
|
|
195
|
+
contains: z.unknown().optional()
|
|
196
|
+
})
|
|
197
|
+
]);
|
|
198
|
+
var whereSchema = z.record(z.string(), whereValueSchema).describe(
|
|
199
|
+
"Filter map keyed by field name. Each value is a scalar (equality), an array (IN \u2014 matches any), or an operator object with keys eq/ne/gt/gte/lt/lte/in/contains. Fields must exist on the entity (see kb_describe_schema)."
|
|
200
|
+
);
|
|
201
|
+
function buildTools(engine) {
|
|
202
|
+
const getEntitySchema = z.object({
|
|
203
|
+
id: z.string().describe("EntityId to retrieve."),
|
|
204
|
+
include: z.array(z.literal("links")).optional().describe("Set to ['links'] to also return the entity's inbound + outbound links.")
|
|
205
|
+
});
|
|
206
|
+
const getEntityTool = {
|
|
207
|
+
name: "kb_get_entity",
|
|
208
|
+
description: "Retrieve a single entity by ID, optionally including its outbound and inbound links.",
|
|
209
|
+
inputShape: getEntitySchema.shape,
|
|
210
|
+
handler: (input) => {
|
|
211
|
+
const r = getEntitySchema.safeParse(input);
|
|
212
|
+
if (!r.success) return parseErr(r.error);
|
|
213
|
+
const { id, include } = r.data;
|
|
214
|
+
return envelopeToResult(engine.get({ id, ...include !== void 0 ? { include } : {} }));
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
const findEntitiesSchema = z.object({
|
|
218
|
+
entity: z.string().describe("Entity type name to query (see kb_describe_schema.entities)."),
|
|
219
|
+
where: whereSchema.optional(),
|
|
220
|
+
order_by: z.string().optional().describe("Field name to sort by."),
|
|
221
|
+
order: z.enum(["asc", "desc"]).optional().describe("Sort direction (default asc)."),
|
|
222
|
+
limit: z.number().optional().describe("Maximum rows to return."),
|
|
223
|
+
offset: z.number().optional().describe("Rows to skip, for pagination.")
|
|
224
|
+
});
|
|
225
|
+
const findEntitiesTool = {
|
|
226
|
+
name: "kb_find_entities",
|
|
227
|
+
description: "Query entities of a given type with optional filtering, sorting, and pagination.",
|
|
228
|
+
inputShape: findEntitiesSchema.shape,
|
|
229
|
+
handler: (input) => {
|
|
230
|
+
const r = findEntitiesSchema.safeParse(input);
|
|
231
|
+
if (!r.success) return parseErr(r.error);
|
|
232
|
+
const { entity, where, order_by, order, limit, offset } = r.data;
|
|
233
|
+
return envelopeToResult(
|
|
234
|
+
engine.find({
|
|
235
|
+
entity,
|
|
236
|
+
...where !== void 0 ? { where } : {},
|
|
237
|
+
...order_by !== void 0 ? { order_by } : {},
|
|
238
|
+
...order !== void 0 ? { order } : {},
|
|
239
|
+
...limit !== void 0 ? { limit } : {},
|
|
240
|
+
...offset !== void 0 ? { offset } : {}
|
|
241
|
+
})
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
const relatedSchema = z.object({
|
|
246
|
+
id: z.string().describe("EntityId to traverse from."),
|
|
247
|
+
depth: z.number().optional().describe("Maximum traversal hops (default 1)."),
|
|
248
|
+
relations: z.array(z.string()).optional().describe("Restrict traversal to these relation names (default: all)."),
|
|
249
|
+
direction: z.enum(["out", "in", "both"]).optional().describe("Edge direction to follow: out, in, or both (default both).")
|
|
250
|
+
});
|
|
251
|
+
const relatedTool = {
|
|
252
|
+
name: "kb_related",
|
|
253
|
+
description: "Traverse the knowledge graph from an entity and return related nodes and edges.",
|
|
254
|
+
inputShape: relatedSchema.shape,
|
|
255
|
+
handler: (input) => {
|
|
256
|
+
const r = relatedSchema.safeParse(input);
|
|
257
|
+
if (!r.success) return parseErr(r.error);
|
|
258
|
+
const { id, depth, relations, direction } = r.data;
|
|
259
|
+
return envelopeToResult(
|
|
260
|
+
engine.related({
|
|
261
|
+
id,
|
|
262
|
+
...depth !== void 0 ? { depth } : {},
|
|
263
|
+
...relations !== void 0 ? { relations } : {},
|
|
264
|
+
...direction !== void 0 ? { direction } : {}
|
|
265
|
+
})
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
const traceSchema = z.object({
|
|
270
|
+
id: z.string().describe("Root EntityId to trace from.")
|
|
271
|
+
});
|
|
272
|
+
const traceTool = {
|
|
273
|
+
name: "kb_trace",
|
|
274
|
+
description: "Produce a nested trace tree from a root entity following configured trace paths.",
|
|
275
|
+
inputShape: traceSchema.shape,
|
|
276
|
+
handler: (input) => {
|
|
277
|
+
const r = traceSchema.safeParse(input);
|
|
278
|
+
if (!r.success) return parseErr(r.error);
|
|
279
|
+
return envelopeToResult(engine.trace({ id: r.data.id }));
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
const searchSchema = z.object({
|
|
283
|
+
text: z.string().describe("Query text to search for."),
|
|
284
|
+
types: z.array(z.string()).optional().describe("Restrict results to these entity type names (default: all types)."),
|
|
285
|
+
mode: z.enum(["fts", "semantic", "hybrid"]).optional().describe(
|
|
286
|
+
"Search mode: fts (keyword, default), semantic or hybrid (both require vector_enabled)."
|
|
287
|
+
),
|
|
288
|
+
limit: z.number().optional().describe("Maximum hits to return.")
|
|
289
|
+
});
|
|
290
|
+
const searchTool = {
|
|
291
|
+
name: "kb_search",
|
|
292
|
+
description: "Search entities using one of three modes: fts (keyword full-text search), semantic (meaning/vector similarity \u2014 requires vectors enabled), or hybrid (RRF fusion of fts and semantic \u2014 also requires vectors enabled). The semantic and hybrid modes return a FEATURE_DISABLED error when vectors are not enabled. Check kb_status.vector_enabled before using semantic or hybrid. Results are ranked by relevance with optional type filter.",
|
|
293
|
+
inputShape: searchSchema.shape,
|
|
294
|
+
handler: (input) => {
|
|
295
|
+
const r = searchSchema.safeParse(input);
|
|
296
|
+
if (!r.success) return parseErr(r.error);
|
|
297
|
+
const { text, types, mode, limit } = r.data;
|
|
298
|
+
return envelopeToResult(
|
|
299
|
+
engine.search({
|
|
300
|
+
text,
|
|
301
|
+
...types !== void 0 ? { types } : {},
|
|
302
|
+
...mode !== void 0 ? { mode } : {},
|
|
303
|
+
...limit !== void 0 ? { limit } : {}
|
|
304
|
+
})
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
const contextPackSchema = z.object({
|
|
309
|
+
id: z.string().describe("EntityId to build the context pack for."),
|
|
310
|
+
recipe: z.string().optional().describe("Named context recipe (see kb_describe_schema.recipes)."),
|
|
311
|
+
max_depth: z.number().optional().describe("Maximum traversal depth for the recipe.")
|
|
312
|
+
});
|
|
313
|
+
const contextPackTool = {
|
|
314
|
+
name: "kb_get_context_pack",
|
|
315
|
+
description: "Assemble a context pack for an entity using a named recipe and optional max traversal depth.",
|
|
316
|
+
inputShape: contextPackSchema.shape,
|
|
317
|
+
handler: (input) => {
|
|
318
|
+
const r = contextPackSchema.safeParse(input);
|
|
319
|
+
if (!r.success) return parseErr(r.error);
|
|
320
|
+
const { id, recipe, max_depth } = r.data;
|
|
321
|
+
return envelopeToResult(
|
|
322
|
+
engine.context({
|
|
323
|
+
id,
|
|
324
|
+
...recipe !== void 0 ? { recipe } : {},
|
|
325
|
+
...max_depth !== void 0 ? { maxDepth: max_depth } : {}
|
|
326
|
+
})
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
const proposeSchema = z.object({
|
|
331
|
+
by: z.string().describe("Actor proposing the change set (agent or user id)."),
|
|
332
|
+
reason: z.string().describe("Human-readable reason for the change set."),
|
|
333
|
+
ops: z.array(mcpOpSchema).describe("Ordered operations applied atomically on acceptance.")
|
|
334
|
+
});
|
|
335
|
+
const proposeTool = {
|
|
336
|
+
name: "kb_propose_change_set",
|
|
337
|
+
description: "Submit a new change set (list of ops) for validation and optional acceptance.",
|
|
338
|
+
inputShape: proposeSchema.shape,
|
|
339
|
+
handler: (input) => {
|
|
340
|
+
const r = proposeSchema.safeParse(input);
|
|
341
|
+
if (!r.success) return parseErr(r.error);
|
|
342
|
+
const { by, reason, ops } = r.data;
|
|
343
|
+
return envelopeToResult(engine.propose({ changeSet: { by, reason, ops } }));
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
const validateSchema = z.object({
|
|
347
|
+
changeset_id: z.string().optional().describe("Id of a stored change set to validate."),
|
|
348
|
+
changeset: z.looseObject({
|
|
349
|
+
by: z.string().optional().describe("Actor who authored the change set."),
|
|
350
|
+
reason: z.string().optional().describe("Reason for the change set."),
|
|
351
|
+
ops: z.array(z.record(z.string(), z.unknown())).optional().describe("Operations to validate; see kb_propose_change_set for the op structure.")
|
|
352
|
+
}).optional().describe(
|
|
353
|
+
`A change set to validate inline (typically the full stored object, incl. id/status); its ops mirror kb_propose_change_set \u2014 ${DESCRIBE_HINT}. Prefer changeset_id for a stored set. Kept permissive (unknown keys preserved) so a possibly-invalid change set is REPORTED as violations rather than rejected here.`
|
|
354
|
+
)
|
|
355
|
+
});
|
|
356
|
+
const validateTool = {
|
|
357
|
+
name: "kb_validate_change_set",
|
|
358
|
+
description: "Validate a change set by ID (from storage) or by providing the change-set object inline.",
|
|
359
|
+
inputShape: validateSchema.shape,
|
|
360
|
+
handler: (input) => {
|
|
361
|
+
const r = validateSchema.safeParse(input);
|
|
362
|
+
if (!r.success) return parseErr(r.error);
|
|
363
|
+
const { changeset_id, changeset } = r.data;
|
|
364
|
+
return envelopeToResult(
|
|
365
|
+
engine.validate({
|
|
366
|
+
...changeset_id !== void 0 ? { id: changeset_id } : {},
|
|
367
|
+
...changeset !== void 0 ? { changeSet: changeset } : {}
|
|
368
|
+
})
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
const acceptSchema = z.object({
|
|
373
|
+
changeset_id: z.string().describe("Id of the proposed change set to accept."),
|
|
374
|
+
by: z.string().optional().describe("Actor accepting the change set.")
|
|
375
|
+
});
|
|
376
|
+
const acceptTool = {
|
|
377
|
+
name: "kb_accept_change_set",
|
|
378
|
+
description: "Accept a proposed change set, applying its ops to the knowledge base.",
|
|
379
|
+
inputShape: acceptSchema.shape,
|
|
380
|
+
handler: (input) => {
|
|
381
|
+
const r = acceptSchema.safeParse(input);
|
|
382
|
+
if (!r.success) return parseErr(r.error);
|
|
383
|
+
const { changeset_id, by } = r.data;
|
|
384
|
+
return envelopeToResult(
|
|
385
|
+
engine.accept({ id: changeset_id, ...by !== void 0 ? { by } : {} })
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
const rejectSchema = z.object({
|
|
390
|
+
changeset_id: z.string().describe("Id of the proposed change set to reject."),
|
|
391
|
+
reason: z.string().describe("Mandatory reason for rejecting the change set."),
|
|
392
|
+
by: z.string().optional().describe("Actor rejecting the change set.")
|
|
393
|
+
});
|
|
394
|
+
const rejectTool = {
|
|
395
|
+
name: "kb_reject_change_set",
|
|
396
|
+
description: "Reject a proposed change set with a mandatory reason.",
|
|
397
|
+
inputShape: rejectSchema.shape,
|
|
398
|
+
handler: (input) => {
|
|
399
|
+
const r = rejectSchema.safeParse(input);
|
|
400
|
+
if (!r.success) return parseErr(r.error);
|
|
401
|
+
const { changeset_id, reason, by } = r.data;
|
|
402
|
+
return envelopeToResult(
|
|
403
|
+
engine.reject({ id: changeset_id, reason, ...by !== void 0 ? { by } : {} })
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
const recordAgentRunSchema = z.object({
|
|
408
|
+
by: z.string().describe("Actor recording the run (agent or user id)."),
|
|
409
|
+
reason: z.string().optional().describe('Reason for the change set (defaults to "record agent run").'),
|
|
410
|
+
fields: z.record(z.string(), z.unknown()).describe(`Fields for the AgentRun entity, keyed by field name \u2014 ${DESCRIBE_HINT}.`)
|
|
411
|
+
});
|
|
412
|
+
const recordAgentRunTool = {
|
|
413
|
+
name: "kb_record_agent_run",
|
|
414
|
+
description: "Sugar: propose a change set that creates a single AgentRun entity with the given fields. The engine re-validates the AgentRun type against the current schema.",
|
|
415
|
+
inputShape: recordAgentRunSchema.shape,
|
|
416
|
+
handler: (input) => {
|
|
417
|
+
const r = recordAgentRunSchema.safeParse(input);
|
|
418
|
+
if (!r.success) return parseErr(r.error);
|
|
419
|
+
const { by, reason, fields } = r.data;
|
|
420
|
+
const changeSet = {
|
|
421
|
+
by,
|
|
422
|
+
reason: reason ?? "record agent run",
|
|
423
|
+
ops: [
|
|
424
|
+
{
|
|
425
|
+
op: "create",
|
|
426
|
+
entity: "AgentRun",
|
|
427
|
+
handle: "@agent-run",
|
|
428
|
+
fields
|
|
429
|
+
}
|
|
430
|
+
]
|
|
431
|
+
};
|
|
432
|
+
return envelopeToResult(engine.propose({ changeSet }));
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
const statusTool = {
|
|
436
|
+
name: "kb_status",
|
|
437
|
+
description: "Report KB health: entity counts by type, number of pending change sets, index_healthy (FTS/derived index fingerprint match), and vector_enabled (whether semantic search is available). Check vector_enabled before using semantic or hybrid search modes in kb_search.",
|
|
438
|
+
inputShape: {},
|
|
439
|
+
handler: (_input) => {
|
|
440
|
+
return envelopeToResult(engine.status());
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
const reindexTool = {
|
|
444
|
+
name: "kb_reindex",
|
|
445
|
+
description: "Rebuild the derived index (FTS + vectors when enabled). Use this to recover after VECTOR_INDEX_FAILED / VECTOR_DIM_MISMATCH or to backfill vectors after enabling them. Returns the new canonical fingerprint on success.",
|
|
446
|
+
inputShape: {},
|
|
447
|
+
handler: (_input) => {
|
|
448
|
+
return envelopeToResult(engine.reindex());
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
const listChangeSetsSchema = z.object({
|
|
452
|
+
status: z.enum(["proposed", "accepted", "rejected", "superseded"]).optional().describe("Filter by change-set status (default: all).")
|
|
453
|
+
});
|
|
454
|
+
const listChangeSetsTool = {
|
|
455
|
+
name: "kb_list_change_sets",
|
|
456
|
+
description: "List change sets, optionally filtered by status (proposed/accepted/rejected/superseded). Use this to discover change-set ids for accept/reject and to see what is pending.",
|
|
457
|
+
inputShape: listChangeSetsSchema.shape,
|
|
458
|
+
handler: (input) => {
|
|
459
|
+
const r = listChangeSetsSchema.safeParse(input);
|
|
460
|
+
if (!r.success) return parseErr(r.error);
|
|
461
|
+
const { status } = r.data;
|
|
462
|
+
return envelopeToResult(engine.listChangeSets(status !== void 0 ? { status } : {}));
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
const verifySchema = z.object({
|
|
466
|
+
events: z.boolean().optional().describe("Set true to also verify the event log.")
|
|
467
|
+
});
|
|
468
|
+
const verifyTool = {
|
|
469
|
+
name: "kb_verify",
|
|
470
|
+
description: "Check knowledge-base integrity (referential consistency, dangling links, etc.); set events: true to also verify the event log. Returns a report of findings.",
|
|
471
|
+
inputShape: verifySchema.shape,
|
|
472
|
+
handler: (input) => {
|
|
473
|
+
const r = verifySchema.safeParse(input);
|
|
474
|
+
if (!r.success) return parseErr(r.error);
|
|
475
|
+
const { events } = r.data;
|
|
476
|
+
return envelopeToResult(engine.verify(events ? { events: true } : {}));
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
const describeSchemaSchema = z.object({});
|
|
480
|
+
const describeSchemaTool = {
|
|
481
|
+
name: "kb_describe_schema",
|
|
482
|
+
description: "Return the knowledge-base schema \u2014 entity types with their fields (name/type/required), relations, and context recipes. Call this FIRST to learn what entity types and fields you can create and query.",
|
|
483
|
+
inputShape: describeSchemaSchema.shape,
|
|
484
|
+
handler: (_input) => {
|
|
485
|
+
return envelopeToResult(engine.describeSchema());
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
return [
|
|
489
|
+
getEntityTool,
|
|
490
|
+
findEntitiesTool,
|
|
491
|
+
relatedTool,
|
|
492
|
+
traceTool,
|
|
493
|
+
searchTool,
|
|
494
|
+
contextPackTool,
|
|
495
|
+
proposeTool,
|
|
496
|
+
validateTool,
|
|
497
|
+
acceptTool,
|
|
498
|
+
rejectTool,
|
|
499
|
+
recordAgentRunTool,
|
|
500
|
+
statusTool,
|
|
501
|
+
reindexTool,
|
|
502
|
+
listChangeSetsTool,
|
|
503
|
+
verifyTool,
|
|
504
|
+
describeSchemaTool
|
|
505
|
+
];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// src/mcp/server.ts
|
|
509
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
510
|
+
var MCP_INSTRUCTIONS = `
|
|
511
|
+
Agent KB is a local knowledge graph of typed entities and relations, edited via change sets.
|
|
512
|
+
|
|
513
|
+
Getting started:
|
|
514
|
+
1. Call kb_describe_schema FIRST to learn the available entity types, their fields, and relations.
|
|
515
|
+
|
|
516
|
+
Write workflow:
|
|
517
|
+
- kb_propose_change_set \u2192 (optional) kb_validate_change_set \u2192 kb_accept_change_set
|
|
518
|
+
- Discover existing change set IDs with kb_list_change_sets.
|
|
519
|
+
|
|
520
|
+
Search:
|
|
521
|
+
- kb_search supports modes: fts (full-text), semantic, and hybrid.
|
|
522
|
+
- Check kb_status.vector_enabled before using semantic or hybrid modes \u2014 if false, those modes return FEATURE_DISABLED.
|
|
523
|
+
|
|
524
|
+
Recovery:
|
|
525
|
+
- After VECTOR_INDEX_FAILED or VECTOR_DIM_MISMATCH errors, call kb_reindex to rebuild the index.
|
|
526
|
+
- For data integrity issues, call kb_verify.
|
|
527
|
+
`.trim();
|
|
528
|
+
function createMcpServer(opts) {
|
|
529
|
+
if (!opts.mcpEnabled) {
|
|
530
|
+
throw featureDisabled("MCP server is disabled (set mcp.enabled = true in config)");
|
|
531
|
+
}
|
|
532
|
+
const server = new McpServer(
|
|
533
|
+
{ name: "agent-kb", version: "0.1.0" },
|
|
534
|
+
{ instructions: MCP_INSTRUCTIONS }
|
|
535
|
+
);
|
|
536
|
+
const tools = buildTools(opts.engine);
|
|
537
|
+
for (const tool of tools) {
|
|
538
|
+
server.registerTool(
|
|
539
|
+
tool.name,
|
|
540
|
+
{ description: tool.description, inputSchema: tool.inputShape },
|
|
541
|
+
// The SDK validates args against inputSchema before calling this handler;
|
|
542
|
+
// we also parse defensively inside tool.handler. Wrap in async to satisfy
|
|
543
|
+
// the SDK's Promise-returning callback expectation.
|
|
544
|
+
async (args) => tool.handler(args)
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
return server;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/cli/mcp-command.ts
|
|
551
|
+
function isMcpInvocation(argv) {
|
|
552
|
+
const args = argv.slice(2);
|
|
553
|
+
let i = 0;
|
|
554
|
+
while (i < args.length) {
|
|
555
|
+
const tok = args[i];
|
|
556
|
+
if (tok === void 0) break;
|
|
557
|
+
if (tok === "--json" || tok === "--verbose") {
|
|
558
|
+
i += 1;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (tok === "--root") {
|
|
562
|
+
i += 2;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (tok.startsWith("--root=")) {
|
|
566
|
+
i += 1;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
return tok === "mcp";
|
|
570
|
+
}
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
function parseMcpArgs(argv, cwd) {
|
|
574
|
+
const args = argv.slice(2);
|
|
575
|
+
let root;
|
|
576
|
+
let json = false;
|
|
577
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
578
|
+
const tok = args[i];
|
|
579
|
+
if (tok === void 0) continue;
|
|
580
|
+
if (tok === "--json") {
|
|
581
|
+
json = true;
|
|
582
|
+
} else if (tok === "--root") {
|
|
583
|
+
const val = args[i + 1];
|
|
584
|
+
if (val !== void 0) {
|
|
585
|
+
root = val;
|
|
586
|
+
i += 1;
|
|
587
|
+
}
|
|
588
|
+
} else if (tok.startsWith("--root=")) {
|
|
589
|
+
root = tok.slice("--root=".length);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return { root: root ?? cwd, json };
|
|
593
|
+
}
|
|
594
|
+
async function connectStdio(server, deps) {
|
|
595
|
+
const createTransport = deps?.createTransport ?? (() => new StdioServerTransport());
|
|
596
|
+
const stdin = deps?.stdin ?? process.stdin;
|
|
597
|
+
const signals = deps?.signals ?? process;
|
|
598
|
+
const transport = createTransport();
|
|
599
|
+
let closing = false;
|
|
600
|
+
const closeTransport = () => {
|
|
601
|
+
if (closing) return;
|
|
602
|
+
closing = true;
|
|
603
|
+
void transport.close();
|
|
604
|
+
};
|
|
605
|
+
const closed = new Promise((resolve) => {
|
|
606
|
+
transport.onclose = resolve;
|
|
607
|
+
});
|
|
608
|
+
const onStdinDone = () => closeTransport();
|
|
609
|
+
const onSignal = () => closeTransport();
|
|
610
|
+
stdin.once("end", onStdinDone);
|
|
611
|
+
stdin.once("error", onStdinDone);
|
|
612
|
+
stdin.once("close", onStdinDone);
|
|
613
|
+
signals.once("SIGINT", onSignal);
|
|
614
|
+
signals.once("SIGTERM", onSignal);
|
|
615
|
+
try {
|
|
616
|
+
await server.connect(transport);
|
|
617
|
+
await closed;
|
|
618
|
+
} finally {
|
|
619
|
+
stdin.removeListener("end", onStdinDone);
|
|
620
|
+
stdin.removeListener("error", onStdinDone);
|
|
621
|
+
stdin.removeListener("close", onStdinDone);
|
|
622
|
+
signals.removeListener("SIGINT", onSignal);
|
|
623
|
+
signals.removeListener("SIGTERM", onSignal);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
var defaultConnect = (server) => connectStdio(server);
|
|
627
|
+
async function runMcp(argv, deps) {
|
|
628
|
+
const out = deps?.out ?? process.stdout;
|
|
629
|
+
const err = deps?.err ?? process.stderr;
|
|
630
|
+
const openKbFn = deps?.openKb ?? openKb;
|
|
631
|
+
const loadConfigFn = deps?.loadConfig ?? load;
|
|
632
|
+
const createServerFn = deps?.createServer ?? createMcpServer;
|
|
633
|
+
const connectFn = deps?.connect ?? defaultConnect;
|
|
634
|
+
const cwd = deps?.cwd ?? process.cwd();
|
|
635
|
+
const { root, json } = parseMcpArgs(argv, cwd);
|
|
636
|
+
let engine;
|
|
637
|
+
try {
|
|
638
|
+
engine = openKbFn({ root });
|
|
639
|
+
const config = loadConfigFn(root);
|
|
640
|
+
const server = createServerFn({ engine, mcpEnabled: config.mcp.enabled });
|
|
641
|
+
await connectFn(server);
|
|
642
|
+
return 0;
|
|
643
|
+
} catch (e) {
|
|
644
|
+
const code = e instanceof KbError ? e.code : "INTERNAL";
|
|
645
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
646
|
+
const env = { ok: false, error: { code, message } };
|
|
647
|
+
if (json) {
|
|
648
|
+
out.write(JSON.stringify(env) + "\n");
|
|
649
|
+
} else {
|
|
650
|
+
err.write(`[${code}] ${message}
|
|
651
|
+
`);
|
|
652
|
+
}
|
|
653
|
+
return exitCodeFor(env);
|
|
654
|
+
} finally {
|
|
655
|
+
try {
|
|
656
|
+
engine?.close();
|
|
657
|
+
} catch {
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/cli/index.ts
|
|
663
|
+
function intArg(v) {
|
|
664
|
+
const n = Number(v);
|
|
665
|
+
if (!Number.isInteger(n) || n < 0)
|
|
666
|
+
throw new InvalidArgumentError("expected a non-negative integer");
|
|
667
|
+
return n;
|
|
668
|
+
}
|
|
669
|
+
function runCli(argv, deps) {
|
|
670
|
+
const out = deps?.out ?? process.stdout;
|
|
671
|
+
const err = deps?.err ?? process.stderr;
|
|
672
|
+
const openKbFn = deps?.openKb ?? openKb;
|
|
673
|
+
const cwd = deps?.cwd ?? process.cwd();
|
|
674
|
+
const homedirFn = deps?.homedir ?? os2.homedir;
|
|
675
|
+
let exitCode = 0;
|
|
676
|
+
const program = new Command();
|
|
677
|
+
program.exitOverride();
|
|
678
|
+
program.configureOutput({
|
|
679
|
+
writeOut: (s) => out.write(s),
|
|
680
|
+
writeErr: (s) => err.write(s)
|
|
681
|
+
});
|
|
682
|
+
program.name("agent-kb").description("Agent KB knowledge base CLI").version("0.1.0").option("--json", "output as JSON (machine-readable)").option("--verbose", "enable verbose logging").option("--root <dir>", "project root directory");
|
|
683
|
+
function globalOpts() {
|
|
684
|
+
return program.opts();
|
|
685
|
+
}
|
|
686
|
+
function dispatch(call, exitFor = exitCodeFor) {
|
|
687
|
+
const opts = globalOpts();
|
|
688
|
+
const root = opts.root ?? cwd;
|
|
689
|
+
let env;
|
|
690
|
+
let engine;
|
|
691
|
+
try {
|
|
692
|
+
engine = openKbFn({ root });
|
|
693
|
+
env = call(engine);
|
|
694
|
+
} catch (e) {
|
|
695
|
+
const code = e instanceof KbError ? e.code : "INTERNAL";
|
|
696
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
697
|
+
env = { ok: false, error: { code, message } };
|
|
698
|
+
} finally {
|
|
699
|
+
try {
|
|
700
|
+
engine?.close?.();
|
|
701
|
+
} catch {
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
render(env, opts.json ?? false, out, err);
|
|
705
|
+
return exitFor(env);
|
|
706
|
+
}
|
|
707
|
+
function render(env, json, outStream, errStream) {
|
|
708
|
+
if (json) {
|
|
709
|
+
outStream.write(JSON.stringify(env) + "\n");
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (env.ok) {
|
|
713
|
+
outStream.write(JSON.stringify(env.data, null, 2) + "\n");
|
|
714
|
+
} else {
|
|
715
|
+
errStream.write(`[${env.error.code}] ${env.error.message}
|
|
716
|
+
`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
program.command("init").description("Initialise a new Agent KB in this project").option("--force", "overwrite existing .agent-kb/").option(
|
|
720
|
+
"--skill [scope]",
|
|
721
|
+
"also install the agent-kb Claude skill ('global' or 'project', default 'project')"
|
|
722
|
+
).action((opts) => {
|
|
723
|
+
const root = globalOpts().root ?? cwd;
|
|
724
|
+
if (opts.skill !== void 0) {
|
|
725
|
+
const parsed = parseSkillScope(opts.skill);
|
|
726
|
+
if (!parsed.ok) {
|
|
727
|
+
const errEnv = {
|
|
728
|
+
ok: false,
|
|
729
|
+
error: { code: "VALIDATION_ERROR", message: parsed.message }
|
|
730
|
+
};
|
|
731
|
+
render(errEnv, globalOpts().json ?? false, out, err);
|
|
732
|
+
exitCode = exitCodeFor(errEnv);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
exitCode = dispatch((engine) => {
|
|
736
|
+
const initEnv = engine.init(opts.force ? { force: true } : {});
|
|
737
|
+
if (!initEnv.ok) return initEnv;
|
|
738
|
+
const { path: skillPath } = installSkill(parsed.scope, root, homedirFn());
|
|
739
|
+
const initData = initEnv.data;
|
|
740
|
+
return { ok: true, data: { ...initData, skill: skillPath } };
|
|
741
|
+
});
|
|
742
|
+
} else {
|
|
743
|
+
exitCode = dispatch((engine) => engine.init(opts.force ? { force: true } : {}));
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
program.command("status").description("Show KB status").action(() => {
|
|
747
|
+
exitCode = dispatch((engine) => engine.status());
|
|
748
|
+
});
|
|
749
|
+
program.command("reindex").description("Rebuild the index from canonical files").action(() => {
|
|
750
|
+
exitCode = dispatch((engine) => engine.reindex());
|
|
751
|
+
});
|
|
752
|
+
const schemaCmd = program.command("schema").description("Schema management");
|
|
753
|
+
schemaCmd.command("apply <file>").description("Validate + install a schema file and reindex; --dry-run validates only").option("--dry-run", "validate the schema file without writing or reindexing").action((file, opts) => {
|
|
754
|
+
exitCode = dispatch(
|
|
755
|
+
(engine) => engine.applySchema({ file, ...opts.dryRun === true ? { dryRun: true } : {} }),
|
|
756
|
+
(env) => {
|
|
757
|
+
if (env.ok) {
|
|
758
|
+
const r = env.data;
|
|
759
|
+
if (r.applied === false && Array.isArray(r.violations) && r.violations.length > 0) {
|
|
760
|
+
return 1;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return exitCodeFor(env);
|
|
764
|
+
}
|
|
765
|
+
);
|
|
766
|
+
});
|
|
767
|
+
schemaCmd.command("describe").description("Show the knowledge-base schema: entity types, their fields, and relations").action(() => {
|
|
768
|
+
exitCode = dispatch((engine) => engine.describeSchema());
|
|
769
|
+
});
|
|
770
|
+
program.command("migrate").description("Run pending migrations").option("--allow-data-loss", "allow destructive migration steps").action((opts) => {
|
|
771
|
+
exitCode = dispatch(
|
|
772
|
+
(engine) => engine.migrate(opts.allowDataLoss ? { allowDataLoss: true } : {})
|
|
773
|
+
);
|
|
774
|
+
});
|
|
775
|
+
program.command("get <id>").description("Get an entity by ID").option("--links", "include linked entity IDs").action((id, opts) => {
|
|
776
|
+
exitCode = dispatch((engine) => engine.get(opts.links ? { id, include: ["links"] } : { id }));
|
|
777
|
+
});
|
|
778
|
+
program.command("find <type>").description("Find entities by type with optional filters").option("--where <json>", "filter clause as JSON").addOption(new Option("--limit <n>", "max results").argParser(intArg)).addOption(new Option("--offset <n>", "skip N results").argParser(intArg)).option("--order-by <field>", "field to sort by").option("--desc", "sort descending (default: asc)").action(
|
|
779
|
+
(type, opts) => {
|
|
780
|
+
exitCode = dispatch((engine) => {
|
|
781
|
+
let parsedWhere;
|
|
782
|
+
if (opts.where !== void 0) {
|
|
783
|
+
try {
|
|
784
|
+
parsedWhere = JSON.parse(opts.where);
|
|
785
|
+
} catch {
|
|
786
|
+
const errEnv = {
|
|
787
|
+
ok: false,
|
|
788
|
+
error: { code: "VALIDATION_ERROR", message: `invalid --where JSON: ${opts.where}` }
|
|
789
|
+
};
|
|
790
|
+
return errEnv;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return engine.find({
|
|
794
|
+
entity: type,
|
|
795
|
+
...parsedWhere !== void 0 ? { where: parsedWhere } : {},
|
|
796
|
+
// Only send order/order_by when --order-by is explicitly provided
|
|
797
|
+
...opts.orderBy !== void 0 ? { order_by: opts.orderBy, order: opts.desc ? "desc" : "asc" } : {},
|
|
798
|
+
...opts.limit !== void 0 ? { limit: opts.limit } : {},
|
|
799
|
+
...opts.offset !== void 0 ? { offset: opts.offset } : {}
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
);
|
|
804
|
+
program.command("related <id>").description("Get related entities").addOption(new Option("--depth <n>", "traversal depth").argParser(intArg)).option("--relations <list>", "comma-separated relation names").addOption(new Option("--dir <dir>", "direction: out|in|both").choices(["out", "in", "both"])).action(
|
|
805
|
+
(id, opts) => {
|
|
806
|
+
exitCode = dispatch(
|
|
807
|
+
(engine) => engine.related({
|
|
808
|
+
id,
|
|
809
|
+
...opts.depth !== void 0 ? { depth: opts.depth } : {},
|
|
810
|
+
...opts.relations !== void 0 ? { relations: opts.relations.split(",") } : {},
|
|
811
|
+
...opts.dir !== void 0 ? { direction: opts.dir } : {}
|
|
812
|
+
})
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
);
|
|
816
|
+
program.command("trace <id>").description("Trace the dependency tree from an entity").action((id) => {
|
|
817
|
+
exitCode = dispatch((engine) => engine.trace({ id }));
|
|
818
|
+
});
|
|
819
|
+
program.command("search <text>").description("Full-text or semantic search").option("--types <list>", "comma-separated entity types to restrict search").addOption(
|
|
820
|
+
new Option("--mode <mode>", "search mode: fts|semantic|hybrid").choices([
|
|
821
|
+
"fts",
|
|
822
|
+
"semantic",
|
|
823
|
+
"hybrid"
|
|
824
|
+
])
|
|
825
|
+
).addOption(new Option("--limit <n>", "max results").argParser(intArg)).action(
|
|
826
|
+
(text, opts) => {
|
|
827
|
+
exitCode = dispatch(
|
|
828
|
+
(engine) => engine.search({
|
|
829
|
+
text,
|
|
830
|
+
...opts.types !== void 0 ? { types: opts.types.split(",") } : {},
|
|
831
|
+
...opts.mode !== void 0 ? { mode: opts.mode } : {},
|
|
832
|
+
...opts.limit !== void 0 ? { limit: opts.limit } : {}
|
|
833
|
+
})
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
);
|
|
837
|
+
program.command("context <id>").description("Assemble a context pack for an entity").option("--recipe <name>", "context recipe name").addOption(new Option("--max-depth <n>", "max traversal depth").argParser(intArg)).action((id, opts) => {
|
|
838
|
+
exitCode = dispatch(
|
|
839
|
+
(engine) => engine.context({
|
|
840
|
+
id,
|
|
841
|
+
...opts.recipe !== void 0 ? { recipe: opts.recipe } : {},
|
|
842
|
+
...opts.maxDepth !== void 0 ? { maxDepth: opts.maxDepth } : {}
|
|
843
|
+
})
|
|
844
|
+
);
|
|
845
|
+
});
|
|
846
|
+
program.command("why <id>").description("Explain why an entity is relevant to a root context").requiredOption("--in-context <root>", "root entity ID for context").option("--recipe <name>", "context recipe name").action((id, opts) => {
|
|
847
|
+
exitCode = dispatch(
|
|
848
|
+
(engine) => engine.why({
|
|
849
|
+
id,
|
|
850
|
+
root: opts.inContext,
|
|
851
|
+
...opts.recipe !== void 0 ? { recipe: opts.recipe } : {}
|
|
852
|
+
})
|
|
853
|
+
);
|
|
854
|
+
});
|
|
855
|
+
program.command("propose <file>").description("Propose a change set from a JSON file").action((file) => {
|
|
856
|
+
exitCode = dispatch((engine) => {
|
|
857
|
+
let changeSet;
|
|
858
|
+
try {
|
|
859
|
+
const raw = fs2.readFileSync(file, "utf8");
|
|
860
|
+
changeSet = JSON.parse(raw);
|
|
861
|
+
} catch (e) {
|
|
862
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
863
|
+
const errEnv = {
|
|
864
|
+
ok: false,
|
|
865
|
+
error: {
|
|
866
|
+
code: "VALIDATION_ERROR",
|
|
867
|
+
message: `cannot read/parse changeset file: ${message}`
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
return errEnv;
|
|
871
|
+
}
|
|
872
|
+
return engine.propose({ changeSet });
|
|
873
|
+
});
|
|
874
|
+
});
|
|
875
|
+
program.command("validate <idOrFile>").description("Validate a change set (by ID or JSON file path)").action((idOrFile) => {
|
|
876
|
+
exitCode = dispatch((engine) => {
|
|
877
|
+
if (fs2.existsSync(idOrFile)) {
|
|
878
|
+
let changeSet;
|
|
879
|
+
try {
|
|
880
|
+
const raw = fs2.readFileSync(idOrFile, "utf8");
|
|
881
|
+
changeSet = JSON.parse(raw);
|
|
882
|
+
} catch (e) {
|
|
883
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
884
|
+
const errEnv = {
|
|
885
|
+
ok: false,
|
|
886
|
+
error: {
|
|
887
|
+
code: "VALIDATION_ERROR",
|
|
888
|
+
message: `cannot read/parse changeset file: ${message}`
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
return errEnv;
|
|
892
|
+
}
|
|
893
|
+
return engine.validate({ changeSet });
|
|
894
|
+
}
|
|
895
|
+
return engine.validate({ id: idOrFile });
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
program.command("accept <id>").description("Accept a proposed change set").option("--by <actor>", "actor accepting the change set").action((id, opts) => {
|
|
899
|
+
exitCode = dispatch(
|
|
900
|
+
(engine) => engine.accept({
|
|
901
|
+
id,
|
|
902
|
+
...opts.by !== void 0 ? { by: opts.by } : {}
|
|
903
|
+
})
|
|
904
|
+
);
|
|
905
|
+
});
|
|
906
|
+
program.command("reject <id>").description("Reject a proposed change set").requiredOption("--reason <text>", "rejection reason").option("--by <actor>", "actor rejecting the change set").action((id, opts) => {
|
|
907
|
+
exitCode = dispatch(
|
|
908
|
+
(engine) => engine.reject({
|
|
909
|
+
id,
|
|
910
|
+
reason: opts.reason,
|
|
911
|
+
...opts.by !== void 0 ? { by: opts.by } : {}
|
|
912
|
+
})
|
|
913
|
+
);
|
|
914
|
+
});
|
|
915
|
+
program.command("changesets").description("List change sets").addOption(
|
|
916
|
+
new Option("--status <s>", "filter by status").choices([
|
|
917
|
+
"proposed",
|
|
918
|
+
"accepted",
|
|
919
|
+
"rejected",
|
|
920
|
+
"superseded"
|
|
921
|
+
])
|
|
922
|
+
).action((opts) => {
|
|
923
|
+
exitCode = dispatch(
|
|
924
|
+
(engine) => engine.listChangeSets(opts.status !== void 0 ? { status: opts.status } : {})
|
|
925
|
+
);
|
|
926
|
+
});
|
|
927
|
+
const docsCmd = program.command("docs").description("Documentation generation");
|
|
928
|
+
docsCmd.command("generate").description("Generate docs from the KB").action(() => {
|
|
929
|
+
exitCode = dispatch((engine) => engine.generateDocs());
|
|
930
|
+
});
|
|
931
|
+
program.command("scan").description("Scan source code for KB entities").option("--propose", "create a proposed change set from scan findings").action((opts) => {
|
|
932
|
+
exitCode = dispatch((engine) => engine.scan(opts.propose ? { propose: true } : {}));
|
|
933
|
+
});
|
|
934
|
+
program.command("verify").description("Verify KB integrity").option("--events", "include event log verification").action((opts) => {
|
|
935
|
+
exitCode = dispatch(
|
|
936
|
+
(engine) => engine.verify(opts.events ? { events: true } : {}),
|
|
937
|
+
(env) => env.ok && hasBlockingFindings(env.data) ? 1 : exitCodeFor(env)
|
|
938
|
+
);
|
|
939
|
+
});
|
|
940
|
+
const snapshotCmd = program.command("snapshot").description("Snapshot management");
|
|
941
|
+
snapshotCmd.command("create").description("Create a KB snapshot").option("--name <n>", "snapshot name").action((opts) => {
|
|
942
|
+
exitCode = dispatch(
|
|
943
|
+
(engine) => engine.snapshot(opts.name !== void 0 ? { name: opts.name } : {})
|
|
944
|
+
);
|
|
945
|
+
});
|
|
946
|
+
program.command("export").description("Export the KB").addOption(
|
|
947
|
+
new Option("--format <fmt>", "export format: json or graph").choices(["json", "graph"]).makeOptionMandatory()
|
|
948
|
+
).action((opts) => {
|
|
949
|
+
exitCode = dispatch((engine) => engine.export({ format: opts.format }));
|
|
950
|
+
});
|
|
951
|
+
const skillCmd = program.command("skill").description("Skill management");
|
|
952
|
+
skillCmd.command("install [scope]").description(
|
|
953
|
+
"Install the agent-kb Claude skill (scope: 'global' or 'project', default 'project')"
|
|
954
|
+
).action((scope) => {
|
|
955
|
+
const root = globalOpts().root ?? cwd;
|
|
956
|
+
const parsed = parseSkillScope(scope ?? "project");
|
|
957
|
+
if (!parsed.ok) {
|
|
958
|
+
const env = {
|
|
959
|
+
ok: false,
|
|
960
|
+
error: { code: "VALIDATION_ERROR", message: parsed.message }
|
|
961
|
+
};
|
|
962
|
+
render(env, globalOpts().json ?? false, out, err);
|
|
963
|
+
exitCode = exitCodeFor(env);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
try {
|
|
967
|
+
const { path: skillPath } = installSkill(parsed.scope, root, homedirFn());
|
|
968
|
+
const env = { ok: true, data: { skill: skillPath } };
|
|
969
|
+
render(env, globalOpts().json ?? false, out, err);
|
|
970
|
+
exitCode = exitCodeFor(env);
|
|
971
|
+
} catch (e) {
|
|
972
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
973
|
+
const env = {
|
|
974
|
+
ok: false,
|
|
975
|
+
error: { code: "IO_ERROR", message }
|
|
976
|
+
};
|
|
977
|
+
render(env, globalOpts().json ?? false, out, err);
|
|
978
|
+
exitCode = exitCodeFor(env);
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
program.command("mcp").description("Run the MCP server over stdio (requires mcp.enabled in config)").action(() => {
|
|
982
|
+
err.write(
|
|
983
|
+
"[INTERNAL] `agent-kb mcp` must be launched via the entry point (npx agent-kb mcp)\n"
|
|
984
|
+
);
|
|
985
|
+
exitCode = 4;
|
|
986
|
+
});
|
|
987
|
+
try {
|
|
988
|
+
program.parse(argv, { from: "node" });
|
|
989
|
+
} catch (e) {
|
|
990
|
+
if (e instanceof CommanderError) {
|
|
991
|
+
return e.exitCode === 0 ? 0 : 2;
|
|
992
|
+
}
|
|
993
|
+
return 4;
|
|
994
|
+
}
|
|
995
|
+
return exitCode;
|
|
996
|
+
}
|
|
997
|
+
async function main(argv, deps) {
|
|
998
|
+
if (isMcpInvocation(argv)) {
|
|
999
|
+
return runMcp(argv, deps);
|
|
1000
|
+
}
|
|
1001
|
+
return runCli(argv, deps);
|
|
1002
|
+
}
|
|
1003
|
+
function isEntryPoint() {
|
|
1004
|
+
const argv1 = process.argv[1];
|
|
1005
|
+
if (argv1 === void 0) return false;
|
|
1006
|
+
let resolved = argv1;
|
|
1007
|
+
try {
|
|
1008
|
+
resolved = fs2.realpathSync(argv1);
|
|
1009
|
+
} catch {
|
|
1010
|
+
}
|
|
1011
|
+
return import.meta.url === pathToFileURL(resolved).href;
|
|
1012
|
+
}
|
|
1013
|
+
if (isEntryPoint()) {
|
|
1014
|
+
void main(process.argv).then(
|
|
1015
|
+
(code) => process.exit(code),
|
|
1016
|
+
() => process.exit(4)
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
export {
|
|
1020
|
+
main,
|
|
1021
|
+
runCli
|
|
1022
|
+
};
|