project-librarian 0.3.0 → 0.4.1

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.
@@ -7,13 +7,14 @@ const install_skill_1 = require("./install-skill");
7
7
  const modes_1 = require("./modes");
8
8
  const migration_1 = require("./migration");
9
9
  const templates_1 = require("./templates");
10
+ const wiki_visualizer_1 = require("./wiki-visualizer");
10
11
  const workspace_1 = require("./workspace");
11
12
  function codeIndex() {
12
13
  return require("./code-index");
13
14
  }
14
15
  function printUsage() {
15
16
  console.log(`Usage:
16
- project-librarian [init] [options]
17
+ project-librarian [init|update] [options]
17
18
  project-librarian install-skill [--scope user|project] [--agents codex|claude|cursor|gemini|all]
18
19
  project-librarian mcp
19
20
 
@@ -33,6 +34,8 @@ Options:
33
34
  --issue-title <title> Override the generated issue draft title.
34
35
  --query <terms> Search wiki paths, metadata, titles, and bodies (answer-shaped, capped output).
35
36
  --wiki-impact <page-or-term> Show wiki backlinks, decision_ref citations, and router depth for matching pages.
37
+ --wiki-visualize Write a static wiki graph visualizer to .project-wiki/wiki-graph.html.
38
+ --wiki-visualize-out <path> With --wiki-visualize, write under a custom .project-wiki/ path.
36
39
  --refresh-index Update the managed auto-discovered wiki index block.
37
40
  --capture-inbox Append a candidate note with --title, --content, and optional --category.
38
41
  --glossary-init Create and route the optional glossary page.
@@ -40,6 +43,7 @@ Options:
40
43
  --review-migration Sync unit coverage and compatible inbox statuses into migration review files.
41
44
  --no-git-config Install hook files without changing git core.hooksPath.
42
45
  --code-index Build the disposable .project-wiki code evidence index.
46
+ --code-index-health Inspect code evidence cache compatibility and print rebuild guidance without writing.
43
47
  --acknowledge-small-repo With --code-index, proceed below the small-repo scale gate after its cost warning.
44
48
  --incremental With --code-index, require an existing compatible index and update only changes.
45
49
  --code-index-full With --code-index, force a full rebuild even when incremental update is possible.
@@ -49,10 +53,12 @@ Options:
49
53
  --code-report Print architecture and ownership summaries from the code evidence index.
50
54
  --code-report-section <section> With --code-report, print one section: coverage, ownership, languages, parsers, workspaces, workspace-graph, routes, hotspots, configs, or edges.
51
55
  --code-impact <term> Show file, symbol, route, import, and edge impact evidence for a term.
56
+ --code-context-pack <term> Print a budgeted first-pass code context pack for a path, symbol, route, or module term.
52
57
  --code-search-symbol <term> Search indexed symbols.
53
58
 
54
59
  Commands:
55
- mcp Run the stdio MCP server exposing answer-shaped code-evidence tools (code_impact, code_ownership, code_workspace_graph, code_search, code_status) over the existing .project-wiki index.
60
+ update Run the idempotent wiki/setup update path; rejects migration flags.
61
+ mcp Run the stdio MCP server exposing answer-shaped code-evidence tools (code_context_pack, code_impact, code_ownership, code_workspace_graph, code_search, code_status) over the existing .project-wiki index.
56
62
 
57
63
  --help Show this help.`);
58
64
  }
@@ -63,6 +69,20 @@ Commands:
63
69
  function exitAfterStdoutDrain(code) {
64
70
  process.stdout.write("", () => process.exit(code));
65
71
  }
72
+ function activeCodeEvidenceCliModes() {
73
+ const modes = [
74
+ { active: args_1.codeQueryMode, drainStdout: true, run: (codeIndexModule) => codeIndexModule.runCodeQueryMode() },
75
+ { active: args_1.codeReportMode, drainStdout: true, run: (codeIndexModule) => codeIndexModule.runCodeReportMode() },
76
+ { active: args_1.codeStatusMode, drainStdout: true, run: (codeIndexModule) => codeIndexModule.runCodeStatusMode() },
77
+ { active: args_1.codeFilesMode, drainStdout: true, run: (codeIndexModule) => codeIndexModule.runCodeFilesMode() },
78
+ { active: args_1.codeImpactMode, drainStdout: true, run: (codeIndexModule) => codeIndexModule.runCodeImpactMode() },
79
+ { active: args_1.codeContextPackMode, drainStdout: true, run: (codeIndexModule) => codeIndexModule.runCodeContextPackMode() },
80
+ { active: args_1.codeSearchSymbolMode, drainStdout: true, run: (codeIndexModule) => codeIndexModule.runCodeSearchSymbolMode() },
81
+ { active: args_1.codeIndexHealthMode, drainStdout: true, run: (codeIndexModule) => codeIndexModule.runCodeIndexHealthMode() },
82
+ { active: args_1.codeIndexMode, drainStdout: false, run: (codeIndexModule) => codeIndexModule.runCodeIndexMode() },
83
+ ];
84
+ return modes.filter((mode) => mode.active);
85
+ }
66
86
  if (args_1.helpMode) {
67
87
  printUsage();
68
88
  process.exit(0);
@@ -87,6 +107,10 @@ if (args_1.missingValueOptions.length > 0) {
87
107
  printUsage();
88
108
  process.exit(1);
89
109
  }
110
+ if (args_1.command === "update" && args_1.migrateMode) {
111
+ console.error("update cannot be combined with --migrate or --adopt-existing; use project-librarian --migrate for migration.");
112
+ process.exit(1);
113
+ }
90
114
  if (args_1.fixMode && !args_1.doctorMode) {
91
115
  console.error("--fix is only supported with --doctor.");
92
116
  process.exit(1);
@@ -99,6 +123,10 @@ if (args_1.codeReportSection && !args_1.codeReportMode) {
99
123
  console.error("--code-report-section is only supported with --code-report.");
100
124
  process.exit(1);
101
125
  }
126
+ if (args_1.wikiVisualizeOutput && !args_1.wikiVisualizeMode) {
127
+ console.error("--wiki-visualize-out is only supported with --wiki-visualize.");
128
+ process.exit(1);
129
+ }
102
130
  if (args_1.codeIndexIncrementalMode && !args_1.codeIndexMode) {
103
131
  console.error("--incremental is only supported with --code-index.");
104
132
  process.exit(1);
@@ -134,50 +162,37 @@ else {
134
162
  runInitCommand();
135
163
  }
136
164
  function runInitCommand() {
137
- const activeCodeModes = [args_1.codeQueryMode, args_1.codeReportMode, args_1.codeStatusMode, args_1.codeFilesMode, args_1.codeImpactMode, args_1.codeSearchSymbolMode, args_1.codeIndexMode].filter(Boolean).length;
138
- if (activeCodeModes > 1) {
139
- console.error("Use one code evidence mode at a time: --code-index, --code-query, --code-report, --code-status, --code-files, --code-impact, or --code-search-symbol.");
165
+ const activeCodeModes = activeCodeEvidenceCliModes();
166
+ if (activeCodeModes.length > 1) {
167
+ console.error("Use one code evidence mode at a time: --code-index, --code-index-health, --code-query, --code-report, --code-status, --code-files, --code-impact, --code-context-pack, or --code-search-symbol.");
140
168
  process.exit(1);
141
169
  }
142
- if (args_1.codeQueryMode) {
143
- codeIndex().runCodeQueryMode();
144
- exitAfterStdoutDrain(0);
145
- return;
146
- }
147
- if (args_1.codeReportMode) {
148
- codeIndex().runCodeReportMode();
149
- exitAfterStdoutDrain(0);
170
+ const activeCodeMode = activeCodeModes[0];
171
+ if (activeCodeMode) {
172
+ activeCodeMode.run(codeIndex());
173
+ if (activeCodeMode.drainStdout)
174
+ exitAfterStdoutDrain(0);
175
+ else
176
+ process.exit(0);
150
177
  return;
151
178
  }
152
- if (args_1.codeStatusMode) {
153
- codeIndex().runCodeStatusMode();
154
- exitAfterStdoutDrain(0);
155
- return;
156
- }
157
- if (args_1.codeFilesMode) {
158
- codeIndex().runCodeFilesMode();
159
- exitAfterStdoutDrain(0);
160
- return;
161
- }
162
- if (args_1.codeImpactMode) {
163
- codeIndex().runCodeImpactMode();
164
- exitAfterStdoutDrain(0);
165
- return;
166
- }
167
- if (args_1.codeSearchSymbolMode) {
168
- codeIndex().runCodeSearchSymbolMode();
169
- exitAfterStdoutDrain(0);
170
- return;
171
- }
172
- if (args_1.codeIndexMode) {
173
- codeIndex().runCodeIndexMode();
174
- process.exit(0);
175
- }
176
179
  if (args_1.wikiImpactMode) {
177
180
  (0, modes_1.runWikiImpactMode)();
178
181
  exitAfterStdoutDrain(0);
179
182
  return;
180
183
  }
184
+ if (args_1.wikiVisualizeMode) {
185
+ try {
186
+ const output = (0, wiki_visualizer_1.writeWikiVisualizer)(args_1.wikiVisualizeOutput);
187
+ console.log(`Project wiki visualizer written: ${output}`);
188
+ exitAfterStdoutDrain(0);
189
+ }
190
+ catch (error) {
191
+ console.error(error instanceof Error ? error.message : String(error));
192
+ process.exit(1);
193
+ }
194
+ return;
195
+ }
181
196
  if (args_1.queryTerm) {
182
197
  (0, modes_1.runQueryMode)();
183
198
  exitAfterStdoutDrain(0);
@@ -228,6 +243,10 @@ function runInitCommand() {
228
243
  (0, modes_1.runLintMode)();
229
244
  process.exit(0);
230
245
  }
246
+ if (args_1.refreshIndexMode && !args_1.migrateMode && !args_1.glossaryMode && !args_1.captureInboxMode) {
247
+ runRefreshIndexOnlyMode();
248
+ process.exit(0);
249
+ }
231
250
  const migrationState = args_1.migrateMode ? (0, migration_1.prepareMigrationMode)() : null;
232
251
  const results = [];
233
252
  if (migrationState)
@@ -336,3 +355,12 @@ function runInitCommand() {
336
355
  console.log(`${String(status).padEnd(7)} ${relativePath}`);
337
356
  }
338
357
  }
358
+ function runRefreshIndexOnlyMode() {
359
+ const results = [];
360
+ results.push(["wiki/index.md", (0, workspace_1.writeStarter)("wiki/index.md", templates_1.index)]);
361
+ results.push(["wiki/index.md auto-discovered pages", (0, workspace_1.upsertMarkedSection)("wiki/index.md", "<!-- PROJECT-WIKI-AUTO-INDEX:START -->", "<!-- PROJECT-WIKI-AUTO-INDEX:END -->", (0, modes_1.buildRefreshIndexBlock)())]);
362
+ console.log("Project Librarian refresh-index complete.");
363
+ for (const [relativePath, status] of results) {
364
+ console.log(`${String(status).padEnd(7)} ${relativePath}`);
365
+ }
366
+ }
@@ -11,10 +11,11 @@
11
11
  // frame on \n and parse each non-empty line as one JSON-RPC message.
12
12
  //
13
13
  // Methods implemented exactly: initialize, notifications/initialized (no-op),
14
- // ping, tools/list, tools/call. Unknown method -> JSON-RPC -32601. Parse error ->
15
- // JSON-RPC -32700 with id null. These protocol error responses are MCP/JSON-RPC
16
- // spec compliance, NOT fallback coding: a malformed frame or unknown method has a
17
- // single spec-defined reply.
14
+ // ping, resources/list, resources/read, prompts/list, prompts/get, tools/list,
15
+ // tools/call. Unknown method -> JSON-RPC -32601. Parse error -> JSON-RPC -32700
16
+ // with id null. These protocol error responses are MCP/JSON-RPC spec compliance,
17
+ // NOT fallback coding: a malformed frame or unknown method has a single
18
+ // spec-defined reply.
18
19
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
19
20
  if (k2 === undefined) k2 = k;
20
21
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -49,7 +50,7 @@ var __importStar = (this && this.__importStar) || (function () {
49
50
  };
50
51
  })();
51
52
  Object.defineProperty(exports, "__esModule", { value: true });
52
- exports.TRUST_SENTENCE = exports.TRUNCATION_NOTICE = exports.MAX_RESPONSE_CHARS = exports.SUPPORTED_PROTOCOL_VERSION = void 0;
53
+ exports.PROMPT_TRUNCATION_NOTICE = exports.MAX_PROMPT_CHARS = exports.RESOURCE_TRUNCATION_NOTICE = exports.MAX_RESOURCE_CHARS = exports.TRUST_SENTENCE = exports.TRUNCATION_NOTICE = exports.MAX_RESPONSE_CHARS = exports.SUPPORTED_PROTOCOL_VERSION = void 0;
53
54
  exports.scaleGuidanceLines = scaleGuidanceLines;
54
55
  exports.runMcpServerMode = runMcpServerMode;
55
56
  exports.handleLine = handleLine;
@@ -57,6 +58,7 @@ const fs = __importStar(require("node:fs"));
57
58
  const path = __importStar(require("node:path"));
58
59
  const code_index_1 = require("./code-index");
59
60
  const code_index_file_policy_1 = require("./code-index-file-policy");
61
+ const schema_1 = require("./code-index/schema");
60
62
  const workspace_1 = require("./workspace");
61
63
  // Pinned MCP protocol version. This is the spec revision this server is written
62
64
  // against; one constant so the supported version is auditable in a single place.
@@ -66,6 +68,7 @@ exports.SUPPORTED_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSION;
66
68
  const JSONRPC_PARSE_ERROR = -32700;
67
69
  const JSONRPC_METHOD_NOT_FOUND = -32601;
68
70
  const JSONRPC_INVALID_PARAMS = -32602;
71
+ const JSONRPC_RESOURCE_NOT_FOUND = -32002;
69
72
  // Hard cap on a single tool-result text payload. The benchmark finding is that
70
73
  // answer-shaped, bounded responses are the hypothesis under test; an unbounded
71
74
  // dump would reintroduce the tool-output cost the boundary measured. When a body
@@ -74,6 +77,14 @@ const MAX_RESPONSE_CHARS = 4000;
74
77
  exports.MAX_RESPONSE_CHARS = MAX_RESPONSE_CHARS;
75
78
  const TRUNCATION_NOTICE = "[truncated — refine the query]";
76
79
  exports.TRUNCATION_NOTICE = TRUNCATION_NOTICE;
80
+ const MAX_RESOURCE_CHARS = 6000;
81
+ exports.MAX_RESOURCE_CHARS = MAX_RESOURCE_CHARS;
82
+ const RESOURCE_TRUNCATION_NOTICE = "[truncated - read the backing file directly or narrow the request]";
83
+ exports.RESOURCE_TRUNCATION_NOTICE = RESOURCE_TRUNCATION_NOTICE;
84
+ const MAX_PROMPT_CHARS = 6000;
85
+ exports.MAX_PROMPT_CHARS = MAX_PROMPT_CHARS;
86
+ const PROMPT_TRUNCATION_NOTICE = "[truncated - shorten the prompt arguments]";
87
+ exports.PROMPT_TRUNCATION_NOTICE = PROMPT_TRUNCATION_NOTICE;
77
88
  // The trust sentence appended to every tool description (B4 analogue). It tells
78
89
  // the agent the index is authoritative for structure questions so it does not
79
90
  // re-run repo-wide greps, gated on the staleness signal that code_status reports.
@@ -96,6 +107,205 @@ function serverInfo() {
96
107
  }
97
108
  }
98
109
  // ---------------------------------------------------------------------------
110
+ // Resources and prompts (fixed registry; no arbitrary file reads)
111
+ // ---------------------------------------------------------------------------
112
+ function finalizeResourceText(text) {
113
+ if (text.length <= MAX_RESOURCE_CHARS)
114
+ return text;
115
+ const budget = MAX_RESOURCE_CHARS - RESOURCE_TRUNCATION_NOTICE.length - 1;
116
+ return `${text.slice(0, budget > 0 ? budget : 0).trimEnd()}\n${RESOURCE_TRUNCATION_NOTICE}`;
117
+ }
118
+ function finalizePromptText(text) {
119
+ if (text.length <= MAX_PROMPT_CHARS)
120
+ return text;
121
+ const budget = MAX_PROMPT_CHARS - PROMPT_TRUNCATION_NOTICE.length - 1;
122
+ return `${text.slice(0, budget > 0 ? budget : 0).trimEnd()}\n${PROMPT_TRUNCATION_NOTICE}`;
123
+ }
124
+ function readProjectMarkdown(relativePath) {
125
+ const absolutePath = path.join(workspace_1.root, relativePath);
126
+ if (!fs.existsSync(absolutePath)) {
127
+ return `Resource unavailable: ${relativePath} is missing. Run \`project-librarian\` to bootstrap or refresh the project wiki.`;
128
+ }
129
+ return finalizeResourceText(fs.readFileSync(absolutePath, "utf8"));
130
+ }
131
+ function codeStatusResourceText() {
132
+ let opened;
133
+ try {
134
+ opened = (0, code_index_1.openCodeEvidenceDatabaseForServing)();
135
+ }
136
+ catch (error) {
137
+ if (error instanceof code_index_1.CodeEvidenceIndexUnavailableError) {
138
+ return `Code status unavailable: ${error.message}`;
139
+ }
140
+ throw error;
141
+ }
142
+ try {
143
+ const staleness = (0, code_index_1.codeIndexStaleness)(opened.database);
144
+ return finalizeResourceText(statusAnswer(opened.database, opened.relativePath, staleness));
145
+ }
146
+ finally {
147
+ opened.database.close();
148
+ }
149
+ }
150
+ const RESOURCES = [
151
+ {
152
+ uri: "project-librarian://wiki/startup",
153
+ name: "wiki-startup",
154
+ title: "Project Wiki Startup",
155
+ description: "Compact session-start project context from wiki/startup.md.",
156
+ mimeType: "text/markdown",
157
+ read: () => readProjectMarkdown("wiki/startup.md"),
158
+ },
159
+ {
160
+ uri: "project-librarian://wiki/index",
161
+ name: "wiki-index",
162
+ title: "Project Wiki Index",
163
+ description: "Project wiki router from wiki/index.md.",
164
+ mimeType: "text/markdown",
165
+ read: () => readProjectMarkdown("wiki/index.md"),
166
+ },
167
+ {
168
+ uri: "project-librarian://code/status",
169
+ name: "code-status",
170
+ title: "Code Evidence Status",
171
+ description: "Current code-evidence freshness, coverage, and scale guidance.",
172
+ mimeType: "text/plain",
173
+ read: codeStatusResourceText,
174
+ },
175
+ ];
176
+ const RESOURCES_BY_URI = new Map(RESOURCES.map((resource) => [resource.uri, resource]));
177
+ function resourceDescriptor(resource) {
178
+ return {
179
+ uri: resource.uri,
180
+ name: resource.name,
181
+ title: resource.title,
182
+ description: resource.description,
183
+ mimeType: resource.mimeType,
184
+ annotations: { audience: ["assistant"], priority: resource.uri === "project-librarian://code/status" ? 0.7 : 0.9 },
185
+ };
186
+ }
187
+ function readResource(uri) {
188
+ if (typeof uri !== "string" || uri.trim() === "") {
189
+ throw new ToolArgumentError("missing required string argument: uri");
190
+ }
191
+ const resource = RESOURCES_BY_URI.get(uri.trim());
192
+ if (!resource) {
193
+ throw new ResourceNotFoundError(`Resource not found: ${uri}`);
194
+ }
195
+ return {
196
+ contents: [{
197
+ uri: resource.uri,
198
+ mimeType: resource.mimeType,
199
+ text: resource.read(),
200
+ }],
201
+ };
202
+ }
203
+ function optionalPromptArg(args, key, placeholder) {
204
+ const value = args[key];
205
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : placeholder;
206
+ }
207
+ function requiredPromptArg(args, key) {
208
+ const value = args[key];
209
+ if (typeof value !== "string" || value.trim() === "") {
210
+ throw new ToolArgumentError(`missing required string argument: ${key}`);
211
+ }
212
+ return value.trim();
213
+ }
214
+ const PROMPTS = [
215
+ {
216
+ name: "wiki_taxonomy_update",
217
+ title: "Wiki Taxonomy Update",
218
+ description: "Classify and apply project-planning content through the wiki taxonomy before editing.",
219
+ arguments: [
220
+ { name: "content", description: "Project-planning content to classify and apply.", required: true },
221
+ { name: "target", description: "Optional intended wiki area or page." },
222
+ ],
223
+ get: (args) => {
224
+ const content = requiredPromptArg(args, "content");
225
+ const target = optionalPromptArg(args, "target", "choose the canonical, decision, source, or meta target from wiki/meta/document-taxonomy.md");
226
+ return [
227
+ "Use the Project Librarian wiki contract for this planning update.",
228
+ "1. Read project-librarian://wiki/startup and project-librarian://wiki/index first.",
229
+ "2. Classify the content with wiki/meta/document-taxonomy.md before editing.",
230
+ "3. Update the wiki in the same turn, then refresh the index and run wiki lint.",
231
+ `Target hint: ${target}`,
232
+ "Planning content:",
233
+ content,
234
+ ].join("\n");
235
+ },
236
+ },
237
+ {
238
+ name: "code_impact_trace",
239
+ title: "Code Impact Trace",
240
+ description: "Trace a path, symbol, route, or module through code evidence before planning a change.",
241
+ arguments: [
242
+ { name: "term", description: "Path, symbol, route, module, or concept to trace.", required: true },
243
+ { name: "question", description: "Optional user question or change intent." },
244
+ ],
245
+ get: (args) => {
246
+ const term = requiredPromptArg(args, "term");
247
+ const question = optionalPromptArg(args, "question", "identify impacted files, owners, imports, routes, and tests before editing");
248
+ return [
249
+ `Build a code impact trace for "${term}".`,
250
+ "1. Read project-librarian://code/status and stop to rebuild the index if it is stale.",
251
+ "2. Call code_context_pack first for bounded first-pass context.",
252
+ "3. Call code_impact only when deeper incoming/outgoing edge detail is needed.",
253
+ "4. Report paths, symbols, route handlers, import aliases, owners, and missing test coverage before editing.",
254
+ `Question: ${question}`,
255
+ ].join("\n");
256
+ },
257
+ },
258
+ {
259
+ name: "retrieval_quality_review",
260
+ title: "Retrieval Quality Review",
261
+ description: "Review whether wiki/code retrieval evidence is precise, complete, bounded, and stale-aware.",
262
+ arguments: [
263
+ { name: "query", description: "The retrieval query or task being evaluated.", required: true },
264
+ { name: "expected_evidence", description: "Optional expected files, blocks, symbols, or answer terms." },
265
+ ],
266
+ get: (args) => {
267
+ const query = requiredPromptArg(args, "query");
268
+ const expected = optionalPromptArg(args, "expected_evidence", "state the expected files, wiki blocks, symbols, answer terms, or say unknown");
269
+ return [
270
+ `Review retrieval quality for query "${query}".`,
271
+ "Use project-librarian://wiki/startup, project-librarian://wiki/index, and project-librarian://code/status as the first context sources.",
272
+ "Evaluate source hit rate, evidence precision, block integrity, graph hop usefulness, scan/output byte discipline, stale-state handling, and answer correctness.",
273
+ "Flag broad pages, hub expansion noise, source-snippet leakage, missing edge cases, and any unsupported quality claim.",
274
+ `Expected evidence: ${expected}`,
275
+ ].join("\n");
276
+ },
277
+ },
278
+ ];
279
+ const PROMPTS_BY_NAME = new Map(PROMPTS.map((prompt) => [prompt.name, prompt]));
280
+ function promptDescriptor(prompt) {
281
+ return {
282
+ name: prompt.name,
283
+ title: prompt.title,
284
+ description: prompt.description,
285
+ arguments: prompt.arguments,
286
+ };
287
+ }
288
+ function getPrompt(name, rawArgs) {
289
+ if (typeof name !== "string" || name.trim() === "") {
290
+ throw new ToolArgumentError("missing required string argument: name");
291
+ }
292
+ const prompt = PROMPTS_BY_NAME.get(name.trim());
293
+ if (!prompt) {
294
+ throw new ResourceNotFoundError(`Prompt not found: ${name}`);
295
+ }
296
+ const args = rawArgs && typeof rawArgs === "object" && !Array.isArray(rawArgs) ? rawArgs : {};
297
+ return {
298
+ description: prompt.description,
299
+ messages: [{
300
+ role: "user",
301
+ content: {
302
+ type: "text",
303
+ text: finalizePromptText(prompt.get(args)),
304
+ },
305
+ }],
306
+ };
307
+ }
308
+ // ---------------------------------------------------------------------------
99
309
  // Answer-shape helpers
100
310
  // ---------------------------------------------------------------------------
101
311
  function requireStringArg(args, key) {
@@ -108,6 +318,9 @@ function requireStringArg(args, key) {
108
318
  // Thrown for bad tool arguments; surfaces as a JSON-RPC -32602 invalid params.
109
319
  class ToolArgumentError extends Error {
110
320
  }
321
+ // Thrown for unknown fixed resources/prompts; surfaces as JSON-RPC -32002.
322
+ class ResourceNotFoundError extends Error {
323
+ }
111
324
  // Prepend a single staleness warning line when the index is stale, then enforce
112
325
  // the hard char cap with an explicit truncation notice. The warning is counted
113
326
  // against the cap so the cap is a true ceiling on the returned text.
@@ -154,8 +367,8 @@ function ownershipAnswer(filePath) {
154
367
  lines.push(`Workspace owner: ${info.owner} (source: ${info.owner_source})${info.codeowners ? `; codeowners ${info.codeowners}` : ""}.`);
155
368
  return lines.join("\n");
156
369
  }
157
- function impactAnswer(database, term) {
158
- const impact = (0, code_index_1.codeImpact)(database, term);
370
+ function impactAnswer(database, term, context) {
371
+ const impact = (0, code_index_1.codeImpact)(database, term, { staleness: context.staleness });
159
372
  const matches = (impact.matches ?? {});
160
373
  const edges = (impact.edges ?? {});
161
374
  const owners = asRows(impact.impacted_owners);
@@ -263,6 +476,16 @@ function statusAnswer(database, relativePath, staleness) {
263
476
  return lines.join("\n");
264
477
  }
265
478
  const TOOLS = [
479
+ {
480
+ name: "code_context_pack",
481
+ description: `Budgeted first-pass context for a path, symbol, route, or module term: matching files, symbols, routes, imports, edges, owners, freshness, and scale guidance without source snippets. ${TRUST_SENTENCE}`,
482
+ inputSchema: {
483
+ type: "object",
484
+ properties: { term: { type: "string", description: "Path, symbol, route, module, or concept to build a compact code context pack for." } },
485
+ required: ["term"],
486
+ },
487
+ run: (database, args, context) => (0, code_index_1.codeContextPack)(database, requireStringArg(args, "term"), { staleness: context.staleness }),
488
+ },
266
489
  {
267
490
  name: "code_impact",
268
491
  description: `Impact of a file, symbol, route, or module term across the indexed code: matching symbols/signatures, routes, imports, dependent edges, and impacted owners. ${TRUST_SENTENCE}`,
@@ -271,7 +494,7 @@ const TOOLS = [
271
494
  properties: { term: { type: "string", description: "File path, symbol name, route, or module to trace." } },
272
495
  required: ["term"],
273
496
  },
274
- run: (database, args) => impactAnswer(database, requireStringArg(args, "term")),
497
+ run: (database, args, context) => impactAnswer(database, requireStringArg(args, "term"), context),
275
498
  },
276
499
  {
277
500
  name: "code_ownership",
@@ -315,6 +538,59 @@ const TOOLS = [
315
538
  },
316
539
  ];
317
540
  const TOOLS_BY_NAME = new Map(TOOLS.map((tool) => [tool.name, tool]));
541
+ let cachedStaleness = null;
542
+ function statFingerprint(relativePath) {
543
+ const absolutePath = path.join(workspace_1.root, relativePath);
544
+ try {
545
+ const stat = fs.statSync(absolutePath);
546
+ if (!stat.isFile())
547
+ return `${relativePath}:not-file`;
548
+ return [
549
+ relativePath,
550
+ stat.size,
551
+ stat.mtimeMs,
552
+ stat.ctimeMs,
553
+ ].join(":");
554
+ }
555
+ catch {
556
+ return `${relativePath}:missing`;
557
+ }
558
+ }
559
+ function codeEvidenceFreshnessCacheKey(opened) {
560
+ try {
561
+ const database = opened.database;
562
+ const scopes = (0, schema_1.indexedScopes)(database);
563
+ const parserMode = (0, schema_1.indexedParserMode)(database);
564
+ const schemaVersion = (0, schema_1.readMetaValue)(database, "schema_version");
565
+ const updatedAt = (0, schema_1.readMetaValue)(database, "updated_at");
566
+ const currentFiles = (0, code_index_file_policy_1.discoverCodeFiles)(scopes.length > 0 ? scopes : ["."]);
567
+ const currentFingerprint = currentFiles.map(statFingerprint).join("\n");
568
+ const indexedFingerprint = database.prepare("SELECT path, hash, mtime_ms, size FROM files ORDER BY path")
569
+ .all()
570
+ .map((row) => `${String(row.path)}:${String(row.hash)}:${Number(row.mtime_ms)}:${Number(row.size)}`)
571
+ .join("\n");
572
+ return [
573
+ opened.relativePath,
574
+ schemaVersion,
575
+ updatedAt,
576
+ parserMode,
577
+ scopes.join("\0"),
578
+ currentFingerprint,
579
+ indexedFingerprint,
580
+ ].join("\n---\n");
581
+ }
582
+ catch {
583
+ return null;
584
+ }
585
+ }
586
+ function codeIndexStalenessForToolCall(opened) {
587
+ const cacheKey = codeEvidenceFreshnessCacheKey(opened);
588
+ if (cacheKey && cachedStaleness?.cacheKey === cacheKey)
589
+ return cachedStaleness.staleness;
590
+ const staleness = (0, code_index_1.codeIndexStaleness)(opened.database);
591
+ cachedStaleness = cacheKey ? { cacheKey, staleness } : null;
592
+ return staleness;
593
+ }
318
594
  // ---------------------------------------------------------------------------
319
595
  // tools/call dispatch
320
596
  // ---------------------------------------------------------------------------
@@ -337,10 +613,10 @@ function callTool(name, rawArgs) {
337
613
  throw error;
338
614
  }
339
615
  try {
340
- const staleness = (0, code_index_1.codeIndexStaleness)(opened.database);
616
+ const staleness = codeIndexStalenessForToolCall(opened);
341
617
  const body = name === "code_status"
342
618
  ? statusAnswer(opened.database, opened.relativePath, staleness)
343
- : TOOLS_BY_NAME.get(name).run(opened.database, args);
619
+ : TOOLS_BY_NAME.get(name).run(opened.database, args, { staleness });
344
620
  return toolResultContent(finalizeAnswer(body, staleness));
345
621
  }
346
622
  catch (error) {
@@ -380,11 +656,49 @@ function handleRequest(request) {
380
656
  case "initialize":
381
657
  return successResponse(id, {
382
658
  protocolVersion: negotiatedProtocolVersion(request.params),
383
- capabilities: { tools: {} },
659
+ capabilities: { tools: {}, resources: {}, prompts: {} },
384
660
  serverInfo: serverInfo(),
385
661
  });
386
662
  case "ping":
387
663
  return successResponse(id, {});
664
+ case "resources/list":
665
+ return successResponse(id, {
666
+ resources: RESOURCES.map(resourceDescriptor),
667
+ });
668
+ case "resources/read": {
669
+ const params = request.params && typeof request.params === "object" ? request.params : {};
670
+ try {
671
+ return successResponse(id, readResource(params.uri));
672
+ }
673
+ catch (error) {
674
+ if (error instanceof ToolArgumentError) {
675
+ return errorResponse(id, { code: JSONRPC_INVALID_PARAMS, message: error.message });
676
+ }
677
+ if (error instanceof ResourceNotFoundError) {
678
+ return errorResponse(id, { code: JSONRPC_RESOURCE_NOT_FOUND, message: error.message });
679
+ }
680
+ throw error;
681
+ }
682
+ }
683
+ case "prompts/list":
684
+ return successResponse(id, {
685
+ prompts: PROMPTS.map(promptDescriptor),
686
+ });
687
+ case "prompts/get": {
688
+ const params = request.params && typeof request.params === "object" ? request.params : {};
689
+ try {
690
+ return successResponse(id, getPrompt(params.name, params.arguments));
691
+ }
692
+ catch (error) {
693
+ if (error instanceof ToolArgumentError) {
694
+ return errorResponse(id, { code: JSONRPC_INVALID_PARAMS, message: error.message });
695
+ }
696
+ if (error instanceof ResourceNotFoundError) {
697
+ return errorResponse(id, { code: JSONRPC_RESOURCE_NOT_FOUND, message: error.message });
698
+ }
699
+ throw error;
700
+ }
701
+ }
388
702
  case "tools/list":
389
703
  return successResponse(id, {
390
704
  tools: TOOLS.map((tool) => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })),
package/dist/migration.js CHANGED
@@ -37,6 +37,7 @@ exports.generatedMigrationInboxFiles = void 0;
37
37
  exports.classifyMarkdown = classifyMarkdown;
38
38
  exports.formOnlyMigrationDocumentReason = formOnlyMigrationDocumentReason;
39
39
  exports.extractMigrationUnits = extractMigrationUnits;
40
+ exports.loadMigrationUnitContext = loadMigrationUnitContext;
40
41
  exports.collectMigrationCoverageDiagnostics = collectMigrationCoverageDiagnostics;
41
42
  exports.collectMigrationUnitMapDiagnostics = collectMigrationUnitMapDiagnostics;
42
43
  exports.collectMigrationSplitPlanDiagnostics = collectMigrationSplitPlanDiagnostics;
@@ -321,6 +322,9 @@ function expectedMigrationUnits() {
321
322
  .flatMap((legacyRoot) => (0, wiki_files_1.walkMarkdownFiles)((0, workspace_1.abs)(legacyRoot), [], (0, workspace_1.abs)(legacyRoot)))
322
323
  .flatMap((file) => extractMigrationUnits(file.basePath, (0, workspace_1.read)(file.path)));
323
324
  }
325
+ function loadMigrationUnitContext() {
326
+ return { units: expectedMigrationUnits() };
327
+ }
324
328
  function isMigrationConfidence(value) {
325
329
  return ["high", "medium", "low"].includes(value);
326
330
  }
@@ -353,8 +357,8 @@ function isReviewedCoverageRetarget(cells) {
353
357
  return note.includes("reviewed low-confidence content; retargeted")
354
358
  || reason.includes("reviewed source context; taxonomy target assigned");
355
359
  }
356
- function collectMigrationCoverageDiagnostics() {
357
- const units = expectedMigrationUnits();
360
+ function collectMigrationCoverageDiagnostics(context = loadMigrationUnitContext()) {
361
+ const units = context.units;
358
362
  if (units.length === 0)
359
363
  return [];
360
364
  if (!(0, workspace_1.exists)("wiki/migration/coverage.md")) {
@@ -417,8 +421,8 @@ function collectMigrationCoverageDiagnostics() {
417
421
  }
418
422
  return diagnostics.sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code) || a.message.localeCompare(b.message));
419
423
  }
420
- function collectMigrationUnitMapDiagnostics() {
421
- const units = expectedMigrationUnits();
424
+ function collectMigrationUnitMapDiagnostics(context = loadMigrationUnitContext()) {
425
+ const units = context.units;
422
426
  if (units.length === 0)
423
427
  return [];
424
428
  const file = "wiki/migration/unit-map.md";
@@ -485,8 +489,8 @@ function collectMigrationUnitMapDiagnostics() {
485
489
  }
486
490
  return diagnostics.sort((a, b) => a.file.localeCompare(b.file) || a.code.localeCompare(b.code) || a.message.localeCompare(b.message));
487
491
  }
488
- function collectMigrationSplitPlanDiagnostics() {
489
- const units = expectedMigrationUnits();
492
+ function collectMigrationSplitPlanDiagnostics(context = loadMigrationUnitContext()) {
493
+ const units = context.units;
490
494
  if (units.length === 0)
491
495
  return [];
492
496
  const file = "wiki/migration/split-plan.md";