@shrkcrft/mcp-server 0.1.0-alpha.16 → 0.1.0-alpha.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/server/fit-array-to-budget.d.ts +6 -2
  2. package/dist/server/fit-array-to-budget.d.ts.map +1 -1
  3. package/dist/server/fit-array-to-budget.js +45 -13
  4. package/dist/tools/all-tools.d.ts.map +1 -1
  5. package/dist/tools/all-tools.js +6 -0
  6. package/dist/tools/code-find-usages.tool.d.ts.map +1 -1
  7. package/dist/tools/code-find-usages.tool.js +44 -6
  8. package/dist/tools/compress-context.tool.d.ts.map +1 -1
  9. package/dist/tools/compress-context.tool.js +3 -2
  10. package/dist/tools/delegate-task.tool.d.ts +3 -0
  11. package/dist/tools/delegate-task.tool.d.ts.map +1 -0
  12. package/dist/tools/delegate-task.tool.js +94 -0
  13. package/dist/tools/deps-audit.tool.js +8 -4
  14. package/dist/tools/get-graph-callers.tool.d.ts.map +1 -1
  15. package/dist/tools/get-graph-callers.tool.js +19 -6
  16. package/dist/tools/get-graph-context.tool.d.ts.map +1 -1
  17. package/dist/tools/get-graph-context.tool.js +49 -15
  18. package/dist/tools/get-graph-cycles.tool.js +2 -2
  19. package/dist/tools/get-graph-deps.tool.js +2 -2
  20. package/dist/tools/get-graph-hubs.tool.d.ts +3 -0
  21. package/dist/tools/get-graph-hubs.tool.d.ts.map +1 -0
  22. package/dist/tools/get-graph-hubs.tool.js +61 -0
  23. package/dist/tools/get-graph-impact.tool.d.ts.map +1 -1
  24. package/dist/tools/get-graph-impact.tool.js +36 -14
  25. package/dist/tools/get-graph-path.tool.d.ts +3 -0
  26. package/dist/tools/get-graph-path.tool.d.ts.map +1 -0
  27. package/dist/tools/get-graph-path.tool.js +144 -0
  28. package/dist/tools/get-graph-search.tool.d.ts.map +1 -1
  29. package/dist/tools/get-graph-search.tool.js +22 -3
  30. package/dist/tools/get-graph-status.tool.d.ts +5 -3
  31. package/dist/tools/get-graph-status.tool.d.ts.map +1 -1
  32. package/dist/tools/get-graph-status.tool.js +15 -6
  33. package/dist/tools/get-knowledge-graph.tool.js +1 -1
  34. package/dist/tools/graph-staleness.d.ts +34 -0
  35. package/dist/tools/graph-staleness.d.ts.map +1 -0
  36. package/dist/tools/graph-staleness.js +36 -0
  37. package/dist/tools/primary-tools.d.ts +1 -1
  38. package/dist/tools/primary-tools.d.ts.map +1 -1
  39. package/dist/tools/primary-tools.js +12 -1
  40. package/dist/tools/start-here.tool.js +2 -2
  41. package/package.json +27 -27
  42. package/dist/tools/r22-extras.tool.d.ts +0 -4
  43. package/dist/tools/r22-extras.tool.d.ts.map +0 -1
  44. package/dist/tools/r22-extras.tool.js +0 -42
  45. package/dist/tools/r26-ingest.tool.d.ts +0 -10
  46. package/dist/tools/r26-ingest.tool.d.ts.map +0 -1
  47. package/dist/tools/r26-ingest.tool.js +0 -174
  48. package/dist/tools/r34-search-unified.tool.d.ts +0 -3
  49. package/dist/tools/r34-search-unified.tool.d.ts.map +0 -1
  50. package/dist/tools/r34-search-unified.tool.js +0 -38
@@ -13,8 +13,12 @@ export interface IFittedArray {
13
13
  * rest dropped) with the FULL original cached in `store` — the agent can
14
14
  * `retrieve_original` with the returned `ccrKey`.
15
15
  *
16
- * This routes through `compressJson({ maxTokens })`, which owns the
17
- * lossless-vs-lossy decision, so the big-array tools all budget identically.
16
+ * `compressJson({ maxTokens })` owns the lossless-vs-lossy DECISION, but its
17
+ * sampler keep-count is not derived from the budget, so a single sample can
18
+ * still exceed `maxTokens`. We therefore binary-search the row cap so the
19
+ * emitted payload actually fits the budget (down to a 1-row floor — a single
20
+ * row's columnar envelope may still exceed a very small budget, which is the
21
+ * best achievable while keeping any data).
18
22
  */
19
23
  export declare function fitArrayToBudget(array: readonly unknown[], maxTokens: number | undefined, store?: ICcrStore): IFittedArray;
20
24
  //# sourceMappingURL=fit-array-to-budget.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fit-array-to-budget.d.ts","sourceRoot":"","sources":["../../src/server/fit-array-to-budget.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwC,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE1F,MAAM,WAAW,YAAY;IAC3B,yEAAyE;IACzE,KAAK,EAAE,OAAO,CAAC;IACf,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,SAAS,OAAO,EAAE,EACzB,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,KAAK,CAAC,EAAE,SAAS,GAChB,YAAY,CAed"}
1
+ {"version":3,"file":"fit-array-to-budget.d.ts","sourceRoot":"","sources":["../../src/server/fit-array-to-budget.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,YAAY;IAC3B,yEAAyE;IACzE,KAAK,EAAE,OAAO,CAAC;IACf,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,SAAS,OAAO,EAAE,EACzB,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,KAAK,CAAC,EAAE,SAAS,GAChB,YAAY,CA2Cd"}
@@ -1,4 +1,4 @@
1
- import { compactArrayToColumnar, compressJson } from '@shrkcrft/compress';
1
+ import { compactArrayToColumnar, compressJson, estimateTokens, EContentType, } from '@shrkcrft/compress';
2
2
  /**
3
3
  * Fit a homogeneous object array to an optional token budget (P5.2).
4
4
  *
@@ -7,22 +7,54 @@ import { compactArrayToColumnar, compressJson } from '@shrkcrft/compress';
7
7
  * rest dropped) with the FULL original cached in `store` — the agent can
8
8
  * `retrieve_original` with the returned `ccrKey`.
9
9
  *
10
- * This routes through `compressJson({ maxTokens })`, which owns the
11
- * lossless-vs-lossy decision, so the big-array tools all budget identically.
10
+ * `compressJson({ maxTokens })` owns the lossless-vs-lossy DECISION, but its
11
+ * sampler keep-count is not derived from the budget, so a single sample can
12
+ * still exceed `maxTokens`. We therefore binary-search the row cap so the
13
+ * emitted payload actually fits the budget (down to a 1-row floor — a single
14
+ * row's columnar envelope may still exceed a very small budget, which is the
15
+ * best achievable while keeping any data).
12
16
  */
13
17
  export function fitArrayToBudget(array, maxTokens, store) {
14
18
  const columnar = compactArrayToColumnar(array) ?? array;
15
19
  if (!maxTokens || maxTokens <= 0)
16
20
  return { value: columnar };
17
- const result = compressJson(JSON.stringify(array), {
18
- maxTokens,
19
- ...(store ? { store } : {}),
20
- });
21
- // A CCR key is set only on the lossy sample path; under budget the result is
22
- // the lossless columnar/minified form, so fall back to the columnar value.
23
- if (result.ccrKey) {
24
- const firstLine = result.compressed.split('\n')[0] ?? 'null';
25
- return { value: JSON.parse(firstLine), ccrKey: result.ccrKey };
21
+ const json = JSON.stringify(array);
22
+ const run = (maxItems) => {
23
+ const r = compressJson(json, {
24
+ maxTokens,
25
+ ...(maxItems !== undefined ? { maxItems } : {}),
26
+ ...(store ? { store } : {}),
27
+ });
28
+ // A CCR key is set only on the lossy sample path; under budget the result
29
+ // is the lossless form, so the caller falls back to the columnar value.
30
+ if (!r.ccrKey)
31
+ return null;
32
+ const firstLine = r.compressed.split('\n')[0] ?? 'null';
33
+ return { value: JSON.parse(firstLine), ccrKey: r.ccrKey };
34
+ };
35
+ const fits = (fitted) => estimateTokens(JSON.stringify(fitted.value), EContentType.JsonArray) <= maxTokens;
36
+ // Default sample (no cap). Null → under budget, emit the lossless form.
37
+ const initial = run();
38
+ if (!initial)
39
+ return { value: columnar };
40
+ if (fits(initial))
41
+ return initial;
42
+ // Largest row cap whose sampled payload still fits the budget.
43
+ let lo = 1;
44
+ let hi = array.length;
45
+ let best = null;
46
+ while (lo <= hi) {
47
+ const mid = Math.floor((lo + hi) / 2);
48
+ const candidate = run(mid);
49
+ if (candidate && fits(candidate)) {
50
+ best = candidate;
51
+ lo = mid + 1;
52
+ }
53
+ else {
54
+ hi = mid - 1;
55
+ }
26
56
  }
27
- return { value: columnar };
57
+ // Even one row over budget → keep the smallest sample (best effort, still
58
+ // recoverable via ccrKey) rather than the much larger default sample.
59
+ return best ?? run(1) ?? initial;
28
60
  }
@@ -1 +1 @@
1
- {"version":3,"file":"all-tools.d.ts","sourceRoot":"","sources":["../../src/tools/all-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AA+UpE,eAAO,MAAM,SAAS,EAAE,SAAS,eAAe,EAyR9C,CAAC"}
1
+ {"version":3,"file":"all-tools.d.ts","sourceRoot":"","sources":["../../src/tools/all-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAkVpE,eAAO,MAAM,SAAS,EAAE,SAAS,eAAe,EA4R9C,CAAC"}
@@ -46,7 +46,10 @@ import { getGraphStatusTool } from "./get-graph-status.tool.js";
46
46
  import { getGraphSearchTool } from "./get-graph-search.tool.js";
47
47
  import { getGraphContextTool } from "./get-graph-context.tool.js";
48
48
  import { getGraphImpactTool } from "./get-graph-impact.tool.js";
49
+ import { getGraphPathTool } from "./get-graph-path.tool.js";
50
+ import { getGraphHubsTool } from "./get-graph-hubs.tool.js";
49
51
  import { getGraphCallersTool } from "./get-graph-callers.tool.js";
52
+ import { delegateTaskTool } from "./delegate-task.tool.js";
50
53
  import { getGraphCyclesTool } from "./get-graph-cycles.tool.js";
51
54
  import { getGraphUnresolvedTool } from "./get-graph-unresolved.tool.js";
52
55
  import { getGraphDepsTool } from "./get-graph-deps.tool.js";
@@ -227,7 +230,10 @@ export const ALL_TOOLS = Object.freeze([
227
230
  getGraphSearchTool,
228
231
  getGraphContextTool,
229
232
  getGraphImpactTool,
233
+ getGraphPathTool,
234
+ getGraphHubsTool,
230
235
  getGraphCallersTool,
236
+ delegateTaskTool,
231
237
  getGraphCyclesTool,
232
238
  getGraphUnresolvedTool,
233
239
  getGraphDepsTool,
@@ -1 +1 @@
1
- {"version":3,"file":"code-find-usages.tool.d.ts","sourceRoot":"","sources":["../../src/tools/code-find-usages.tool.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAGpE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,EAAE,eAmGhC,CAAC"}
1
+ {"version":3,"file":"code-find-usages.tool.d.ts","sourceRoot":"","sources":["../../src/tools/code-find-usages.tool.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAIpE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,EAAE,eAuIhC,CAAC"}
@@ -1,7 +1,8 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import * as nodePath from 'node:path';
3
- import { EdgeKind, GraphQueryApi, GraphStore, NodeKind } from '@shrkcrft/graph';
3
+ import { EdgeKind, GraphQueryApi, GraphStore, NodeKind, loadGraphApiCached } from '@shrkcrft/graph';
4
4
  import { FORMAT_INPUT_PROPERTY, formatObjectArrays } from "../server/columnar-format.js";
5
+ import { callGraphLanguageNote } from "./graph-staleness.js";
5
6
  /**
6
7
  * `code_find_usages` — structured usage finder backed by the
7
8
  * SharkCraft graph (file + symbol nodes + import/declare edges).
@@ -17,7 +18,7 @@ import { FORMAT_INPUT_PROPERTY, formatObjectArrays } from "../server/columnar-fo
17
18
  */
18
19
  export const codeFindUsagesTool = {
19
20
  name: 'code_find_usages',
20
- description: 'Find structured usages of a symbol via the SharkCraft graph (file + symbol nodes). Read-only. Distinguishes definition, import-of-declaring-file, and neighbouring symbols. Pass `format:"table"` for a token-efficient columnar encoding of the definitions/importers/neighbours arrays.',
21
+ description: 'Find where a symbol is used (use this instead of grep). Returns the definition site and exact use sites as path:line via the SharkCraft graph, plus files that import the declaring file and neighbouring symbols. Read-only; needs `shrk graph index`. Pass `format:"table"` for a token-efficient columnar encoding.',
21
22
  inputSchema: {
22
23
  type: 'object',
23
24
  properties: {
@@ -40,12 +41,12 @@ export const codeFindUsagesTool = {
40
41
  return {
41
42
  data: {
42
43
  error: 'no-graph',
43
- message: 'The SharkCraft graph index has not been built yet. Build it with `shrk graph build`.',
44
- nextCommand: 'shrk graph build',
44
+ message: 'The SharkCraft graph index has not been built yet. Build it with `shrk graph index`.',
45
+ nextCommand: 'shrk graph index',
45
46
  },
46
47
  };
47
48
  }
48
- const api = GraphQueryApi.fromStore(ctx.cwd);
49
+ const api = loadGraphApiCached(ctx.cwd) ?? GraphQueryApi.fromStore(ctx.cwd);
49
50
  const matches = api.findSymbol(symbolName, { exact: true, limit: maxResults });
50
51
  if (matches.length === 0) {
51
52
  return {
@@ -62,13 +63,29 @@ export const codeFindUsagesTool = {
62
63
  const definitions = [];
63
64
  const importerSet = new Map();
64
65
  const neighbours = [];
66
+ const useSites = [];
65
67
  for (const sym of matches) {
66
68
  const declaringFile = declaringFileOf(api, sym.id);
67
69
  definitions.push({
68
70
  symbolId: sym.id,
69
71
  file: declaringFile?.path ?? null,
72
+ ...(sym.line ? { line: sym.line } : {}),
70
73
  kind: String(sym.label && sym.label.length > 0 ? sym.label : sym.kind),
71
74
  });
75
+ // Exact use sites (path:line) from the symbol's own call/reference
76
+ // edges, so the agent jumps straight to where it's used rather than
77
+ // grepping inside each importing file.
78
+ for (const site of api.referenceSitesOf(sym.id)) {
79
+ if (!site.node.path)
80
+ continue;
81
+ // Prune use sites whose file no longer exists — uniformly with
82
+ // importersOfDeclaringFile below, so the payload never lists a deleted
83
+ // file in one field while dropping it in another (a self-contradicting,
84
+ // authoritative-looking result is worse than a uniformly-stale one).
85
+ if (!pathExists(ctx.cwd, site.node.path))
86
+ continue;
87
+ useSites.push({ file: site.node.path, ...(site.line ? { line: site.line } : {}) });
88
+ }
72
89
  if (declaringFile) {
73
90
  for (const importer of api.importersOf(declaringFile.id)) {
74
91
  if (!importer.path)
@@ -98,13 +115,34 @@ export const codeFindUsagesTool = {
98
115
  }
99
116
  }
100
117
  }
118
+ // Result-file staleness: which surviving result files changed content
119
+ // since indexing (deleted ones are already pruned above). Flags a payload
120
+ // whose line numbers / membership may be out of date for files the agent
121
+ // just edited. Read-only.
122
+ const resultPaths = [
123
+ ...definitions.map((d) => d.file),
124
+ ...useSites.map((u) => u.file),
125
+ ...[...importerSet.values()].map((i) => i.file),
126
+ ].filter((p) => !!p);
127
+ const stale = api.staleFilesAmong(ctx.cwd, resultPaths);
128
+ // Non-TS languages have no call/reference extraction, so empty useSites must
129
+ // not be read as "no usages".
130
+ const langNote = matches[0] ? callGraphLanguageNote(api, matches[0]) : undefined;
101
131
  const data = {
102
132
  symbol: { name: symbolName, kind: matches[0]?.kind ?? 'unknown' },
103
133
  definitions,
134
+ useSites,
104
135
  importersOfDeclaringFile: [...importerSet.values()],
105
136
  neighbouringSymbols: neighbours.slice(0, 12),
106
137
  totalSymbolMatches: matches.length,
107
- note: 'importersOfDeclaringFile = files that import the declaring file. This is a structural signal — it may include type-only imports and unused references. Pair with `shrk impact` for a tighter blast radius.',
138
+ note: (langNote ? langNote + ' ' : '') +
139
+ 'useSites = exact file:line of each call/reference to the symbol (first use per file). importersOfDeclaringFile = files that import the declaring file (coarser; may include type-only/unused imports). Pair with `shrk impact` for a tighter blast radius.',
140
+ ...(stale.modified.length > 0
141
+ ? {
142
+ stale: { modified: stale.modified },
143
+ staleHint: 'Some result files changed since indexing — run `shrk graph index --changed` for fresh line numbers.',
144
+ }
145
+ : {}),
108
146
  };
109
147
  // `format:"table"` columnar-encodes the homogeneous object-array fields
110
148
  // (definitions, importersOfDeclaringFile, neighbouringSymbols); the scalar
@@ -1 +1 @@
1
- {"version":3,"file":"compress-context.tool.d.ts","sourceRoot":"","sources":["../../src/tools/compress-context.tool.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAIpE;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,eA4EjC,CAAC"}
1
+ {"version":3,"file":"compress-context.tool.d.ts","sourceRoot":"","sources":["../../src/tools/compress-context.tool.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAIpE;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,eA6EjC,CAAC"}
@@ -21,11 +21,12 @@ export const compressContextTool = {
21
21
  description: 'Optional task/query text that biases which lines or matches are kept.',
22
22
  },
23
23
  maxItems: {
24
- type: 'number',
24
+ type: 'integer',
25
+ minimum: 1,
25
26
  description: 'Soft cap on retained lines / matches / hunks (compressor-specific).',
26
27
  },
27
28
  maxTokens: {
28
- type: 'number',
29
+ type: 'integer',
29
30
  minimum: 1,
30
31
  description: 'Token budget for a JSON array. When set and the lossless columnar form still exceeds it, falls back to the lossy SmartCrusher row-sampler (kept rows + CCR original).',
31
32
  },
@@ -0,0 +1,3 @@
1
+ import type { IToolDefinition } from '../server/tool-definition.js';
2
+ export declare const delegateTaskTool: IToolDefinition;
3
+ //# sourceMappingURL=delegate-task.tool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delegate-task.tool.d.ts","sourceRoot":"","sources":["../../src/tools/delegate-task.tool.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAUpE,eAAO,MAAM,gBAAgB,EAAE,eA0E9B,CAAC"}
@@ -0,0 +1,94 @@
1
+ import { compressMarkdown } from '@shrkcrft/compress';
2
+ import { loadProjectConfig } from '@shrkcrft/config';
3
+ const READONLY_NOTE = 'Read-only. The CLI is the only write path. The worker may emit ONLY the allowed ops and touch ONLY the guardrail globs; the edit is verified deterministically and auto-reverted on failure — so it lands only if it passes the recipe verification.';
4
+ export const delegateTaskTool = {
5
+ name: 'delegate_task',
6
+ description: 'Get a compact brief for delegating a MECHANICAL, deterministically-verifiable edit to the local-LLM worker (read-only). Returns the recipe fence — allowed ops, guardrail globs, verification — and the exact `shrk delegate run` next command. Hand the grunt edit to the local worker instead of spending your own tokens reading the whole file and writing the edit. Never writes; needs a `delegation` block in sharkcraft.config.ts.',
7
+ cliCommand: 'delegate',
8
+ inputSchema: {
9
+ type: 'object',
10
+ properties: {
11
+ task: { type: 'string' },
12
+ recipe: { type: 'string' },
13
+ },
14
+ required: ['task', 'recipe'],
15
+ additionalProperties: false,
16
+ },
17
+ async handler(input, ctx) {
18
+ const args = input;
19
+ const task = (args.task ?? '').trim();
20
+ const recipeId = (args.recipe ?? '').trim();
21
+ if (!task || !recipeId) {
22
+ return { isError: true, error: { code: 'invalid-input', message: 'task and recipe are required' } };
23
+ }
24
+ const loaded = await loadProjectConfig(ctx.inspection.projectRoot);
25
+ if (!loaded.ok) {
26
+ return { isError: true, error: { code: 'config-error', message: loaded.error.message } };
27
+ }
28
+ const delegation = loaded.value.config.delegation;
29
+ if (!delegation || delegation.enabled === false) {
30
+ return {
31
+ isError: true,
32
+ error: {
33
+ code: 'not-enabled',
34
+ message: 'delegation is not enabled in sharkcraft.config.ts',
35
+ details: { nextCommand: 'add a delegation { recipes: [...] } block to sharkcraft.config.ts' },
36
+ },
37
+ };
38
+ }
39
+ const recipes = delegation.recipes ?? [];
40
+ const recipe = recipes.find((r) => r.id === recipeId);
41
+ if (!recipe) {
42
+ return {
43
+ isError: true,
44
+ error: {
45
+ code: 'not-found',
46
+ message: `unknown recipe "${recipeId}". Available: ${recipes.map((r) => r.id).join(', ') || '(none)'}`,
47
+ details: { available: recipes.map((r) => r.id) },
48
+ },
49
+ };
50
+ }
51
+ const provider = recipe.provider ?? delegation.provider ?? 'auto';
52
+ const briefMarkdown = buildBriefMarkdown(task, recipe, provider);
53
+ // Compress the brief body (CCR-reversible when the server store is present).
54
+ // Small briefs pass through unchanged via the net-loss guard.
55
+ const compressed = compressMarkdown(briefMarkdown, ctx.ccrStore ? { store: ctx.ccrStore, query: task } : { query: task });
56
+ return {
57
+ data: {
58
+ schema: 'sharkcraft.delegate-task/v1',
59
+ recipeId: recipe.id,
60
+ title: recipe.title ?? recipe.id,
61
+ task,
62
+ allowedOps: recipe.allowedOps,
63
+ guardrailGlobs: recipe.guardrailGlobs,
64
+ verificationIds: recipe.verificationIds,
65
+ provider,
66
+ riskCeiling: recipe.riskCeiling ?? null,
67
+ brief: compressed.compressed,
68
+ ...(compressed.ccrKey ? { ccrKey: compressed.ccrKey } : {}),
69
+ next: `shrk delegate run "${task}" --recipe ${recipe.id} --apply`,
70
+ note: READONLY_NOTE,
71
+ },
72
+ };
73
+ },
74
+ };
75
+ function buildBriefMarkdown(task, recipe, provider) {
76
+ return [
77
+ `# Delegate brief: ${recipe.title ?? recipe.id}`,
78
+ '',
79
+ `**Task:** ${task}`,
80
+ '',
81
+ `**Recipe:** \`${recipe.id}\``,
82
+ `**Allowed ops:** ${recipe.allowedOps.join(', ')}`,
83
+ `**Guardrail globs (the worker may ONLY touch files matching these):** ${recipe.guardrailGlobs.join(', ')}`,
84
+ `**Verification (must pass or the edit is reverted):** ${recipe.verificationIds.join(', ') || '(none)'}`,
85
+ `**Provider:** ${provider}${recipe.model ? ` (${recipe.model})` : ''}`,
86
+ '',
87
+ '## How to delegate',
88
+ '',
89
+ 'The CLI is the only write path. Run the `next` command: the local worker generates the edit,',
90
+ 'the deterministic engine verifies it against the recipe verification, and auto-reverts on failure —',
91
+ 'so the edit lands only if it is correct. You pay for this brief and the compact result, not for',
92
+ 'reading the whole file or writing the edit yourself.',
93
+ ].join('\n');
94
+ }
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
2
  import * as nodePath from 'node:path';
3
- import { GraphQueryApi, GraphStore, NodeKind } from '@shrkcrft/graph';
3
+ import { GraphQueryApi, GraphStore, NodeKind, loadGraphApiCached } from '@shrkcrft/graph';
4
4
  import { FORMAT_INPUT_PROPERTY, formatObjectArrays, COLUMNAR_LEGEND } from "../server/columnar-format.js";
5
5
  import { fitArrayToBudget } from "../server/fit-array-to-budget.js";
6
6
  /**
@@ -19,7 +19,7 @@ export const depsAuditTool = {
19
19
  package: { type: 'string' },
20
20
  ...FORMAT_INPUT_PROPERTY,
21
21
  maxTokens: {
22
- type: 'number',
22
+ type: 'integer',
23
23
  minimum: 1,
24
24
  description: 'Token budget for the per-package report list. When set and the columnar form still exceeds it, falls back to the lossy SmartCrusher row-sampler (full original cached — retrieve via the returned ccrKey).',
25
25
  },
@@ -34,11 +34,11 @@ export const depsAuditTool = {
34
34
  data: {
35
35
  error: 'no-graph',
36
36
  message: 'The SharkCraft graph index is required for deps-audit.',
37
- nextCommand: 'shrk graph build',
37
+ nextCommand: 'shrk graph index',
38
38
  },
39
39
  };
40
40
  }
41
- const api = GraphQueryApi.fromStore(ctx.cwd);
41
+ const api = loadGraphApiCached(ctx.cwd) ?? GraphQueryApi.fromStore(ctx.cwd);
42
42
  const packages = listWorkspacePackages(ctx.cwd, onlyPackage);
43
43
  const reports = packages.map((p) => buildPackageReport(api, ctx.cwd, p));
44
44
  const totals = reports.reduce((acc, r) => {
@@ -238,6 +238,10 @@ function rootOfSpecifier(spec) {
238
238
  function isBuiltinModule(spec) {
239
239
  if (spec.startsWith('node:'))
240
240
  return true;
241
+ // Bun runtime builtins (`bun:test`, `bun:sqlite`, …) are runtime-provided,
242
+ // never an npm dependency — so they are not "missing".
243
+ if (spec.startsWith('bun:'))
244
+ return true;
241
245
  return new Set([
242
246
  'fs', 'path', 'os', 'crypto', 'http', 'https', 'url', 'util', 'stream',
243
247
  'events', 'child_process', 'process', 'buffer', 'querystring', 'zlib',
@@ -1 +1 @@
1
- {"version":3,"file":"get-graph-callers.tool.d.ts","sourceRoot":"","sources":["../../src/tools/get-graph-callers.tool.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAUpE,eAAO,MAAM,mBAAmB,EAAE,eA0DjC,CAAC"}
1
+ {"version":3,"file":"get-graph-callers.tool.d.ts","sourceRoot":"","sources":["../../src/tools/get-graph-callers.tool.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAWpE,eAAO,MAAM,mBAAmB,EAAE,eAsEjC,CAAC"}
@@ -1,9 +1,10 @@
1
- import { GraphQueryApi, GraphStore } from '@shrkcrft/graph';
1
+ import { GraphQueryApi, GraphStore, loadGraphApiCached } from '@shrkcrft/graph';
2
2
  import { FORMAT_INPUT_PROPERTY, formatObjectArrays } from "../server/columnar-format.js";
3
+ import { callGraphLanguageNote, graphResultStaleness } from "./graph-staleness.js";
3
4
  const NEXT = 'shrk graph index';
4
5
  export const getGraphCallersTool = {
5
6
  name: 'get_graph_callers',
6
- description: 'Return files that call or reference the given symbol. Mode "call" → calls-symbol edges; mode "reference" → both references-symbol and calls-symbol. Read-only.',
7
+ description: 'Find who calls/references a symbol (use this instead of grep before changing a function/type). Returns each caller as path:line of the first call site. Mode "call" → calls-symbol edges; mode "reference" → both references-symbol and calls-symbol. Read-only; needs `shrk graph index`.',
7
8
  cliCommand: 'graph callers',
8
9
  inputSchema: {
9
10
  type: 'object',
@@ -36,7 +37,7 @@ export const getGraphCallersTool = {
36
37
  },
37
38
  };
38
39
  }
39
- const api = GraphQueryApi.fromStore(ctx.inspection.projectRoot);
40
+ const api = loadGraphApiCached(ctx.inspection.projectRoot) ?? GraphQueryApi.fromStore(ctx.inspection.projectRoot);
40
41
  const sym = resolveSymbol(api, target);
41
42
  if (!sym) {
42
43
  return {
@@ -48,13 +49,25 @@ export const getGraphCallersTool = {
48
49
  },
49
50
  };
50
51
  }
51
- const hits = mode === 'reference' ? api.referencesOf(sym.id) : api.callersOf(sym.id);
52
+ const cwd = ctx.inspection.projectRoot;
53
+ const sites = mode === 'reference' ? api.referenceSitesOf(sym.id) : api.callerSitesOf(sym.id);
54
+ // Targeted staleness over the result files: drop callers whose file was
55
+ // deleted on disk, flag those whose content changed since indexing — so a
56
+ // stale index never silently serves a wrong/dead caller. Read-only.
57
+ const fresh = graphResultStaleness(api, cwd, [sym.path, ...sites.map((s) => s.node.path)]);
58
+ const live = sites.filter((s) => !s.node.path || !fresh.deletedSet.has(s.node.path));
59
+ const note = callGraphLanguageNote(api, sym);
52
60
  const data = {
53
61
  schema: 'sharkcraft.graph-callers/v1',
54
62
  symbol: summarise(sym),
55
63
  mode,
56
- total: hits.length,
57
- callers: hits.slice(0, 200).map(summarise),
64
+ total: live.length,
65
+ callers: live.slice(0, 200).map((s) => ({
66
+ ...summarise(s.node),
67
+ ...(s.line ? { line: s.line } : {}),
68
+ })),
69
+ ...(note ? { note } : {}),
70
+ ...(fresh.field ?? {}),
58
71
  };
59
72
  return { data: formatObjectArrays(data, input) };
60
73
  },
@@ -1 +1 @@
1
- {"version":3,"file":"get-graph-context.tool.d.ts","sourceRoot":"","sources":["../../src/tools/get-graph-context.tool.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AASpE,eAAO,MAAM,mBAAmB,EAAE,eAgEjC,CAAC"}
1
+ {"version":3,"file":"get-graph-context.tool.d.ts","sourceRoot":"","sources":["../../src/tools/get-graph-context.tool.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAUpE,eAAO,MAAM,mBAAmB,EAAE,eAsGjC,CAAC"}
@@ -1,5 +1,6 @@
1
- import { GraphQueryApi, GraphStore, NodeKind, } from '@shrkcrft/graph';
1
+ import { GraphQueryApi, GraphStore, NodeKind, loadGraphApiCached, } from '@shrkcrft/graph';
2
2
  import { FORMAT_INPUT_PROPERTY, formatObjectArrays } from "../server/columnar-format.js";
3
+ import { dropDeleted, graphResultStaleness } from "./graph-staleness.js";
3
4
  const NEXT = 'shrk graph index';
4
5
  export const getGraphContextTool = {
5
6
  name: 'get_graph_context',
@@ -31,7 +32,7 @@ export const getGraphContextTool = {
31
32
  },
32
33
  };
33
34
  }
34
- const api = GraphQueryApi.fromStore(ctx.inspection.projectRoot);
35
+ const api = loadGraphApiCached(ctx.inspection.projectRoot) ?? GraphQueryApi.fromStore(ctx.inspection.projectRoot);
35
36
  const anchor = resolveAnchor(api, target);
36
37
  if (!anchor) {
37
38
  return {
@@ -43,24 +44,57 @@ export const getGraphContextTool = {
43
44
  },
44
45
  };
45
46
  }
46
- const neighbours = api.neighbours(anchor.id);
47
+ // A SYMBOL has no imports-file edges (those are file→file), so its import
48
+ // context is the DECLARING FILE's imports — compute neighbours on that file
49
+ // (mirrors the CLI; otherwise importsFrom/importedBy are wrongly empty).
50
+ const declaringFile = anchor.kind === NodeKind.Symbol
51
+ ? api.declaringFileOf(anchor.id) ?? (anchor.path ? api.findFile(anchor.path) : undefined)
52
+ : undefined;
53
+ const subjectId = anchor.kind === NodeKind.File ? anchor.id : declaringFile?.id ?? anchor.id;
54
+ const neighbours = api.neighbours(subjectId);
47
55
  const symbols = anchor.kind === NodeKind.File ? api.symbolsIn(anchor.id) : [];
56
+ // Who uses this symbol — references + calls (the CLI provides these; the MCP
57
+ // previously omitted them, returning a confidently-wrong "nothing uses this").
58
+ const referencedBy = anchor.kind === NodeKind.Symbol ? api.referencesOf(anchor.id) : [];
59
+ const calledBy = anchor.kind === NodeKind.Symbol ? api.callersOf(anchor.id) : [];
60
+ // Typed subtype/supertype edges (extends / implements) — the precise
61
+ // "who implements this interface" answer for a symbol anchor.
62
+ const subtypes = anchor.kind === NodeKind.Symbol ? api.subtypesOf(anchor.id) : [];
63
+ const supertypes = anchor.kind === NodeKind.Symbol ? api.supertypesOf(anchor.id) : [];
64
+ const importsFrom = neighbours.out
65
+ .filter((o) => o.edge.kind === 'imports-file')
66
+ .slice(0, 50)
67
+ .map((o) => 'resolved' in o.target
68
+ ? { id: o.target.id, resolved: false }
69
+ : { ...summarise(o.target), resolved: true });
70
+ const importedBy = neighbours.in
71
+ .filter((i) => i.edge.kind === 'imports-file')
72
+ .slice(0, 50)
73
+ .map((i) => 'resolved' in i.source
74
+ ? { id: i.source.id, resolved: false }
75
+ : { ...summarise(i.source), resolved: true });
76
+ const referencedByRows = referencedBy.slice(0, 50).map(summarise);
77
+ const calledByRows = calledBy.slice(0, 50).map(summarise);
78
+ // Drop imports/refs to/from files deleted on disk; flag the rest if changed.
79
+ const fresh = graphResultStaleness(api, ctx.inspection.projectRoot, [
80
+ anchor.path,
81
+ ...importsFrom.map((x) => ('path' in x ? x.path : undefined)),
82
+ ...importedBy.map((x) => ('path' in x ? x.path : undefined)),
83
+ ...referencedByRows.map((x) => x.path),
84
+ ...calledByRows.map((x) => x.path),
85
+ ]);
48
86
  const data = {
49
87
  schema: 'sharkcraft.graph-context/v1',
50
88
  anchor: summarise(anchor),
51
- importsFrom: neighbours.out
52
- .filter((o) => o.edge.kind === 'imports-file')
53
- .slice(0, 50)
54
- .map((o) => ('resolved' in o.target
55
- ? { id: o.target.id, resolved: false }
56
- : { ...summarise(o.target), resolved: true })),
57
- importedBy: neighbours.in
58
- .filter((i) => i.edge.kind === 'imports-file')
59
- .slice(0, 50)
60
- .map((i) => ('resolved' in i.source
61
- ? { id: i.source.id, resolved: false }
62
- : { ...summarise(i.source), resolved: true })),
89
+ ...(declaringFile ? { declaredIn: summarise(declaringFile) } : {}),
90
+ importsFrom: dropDeleted(importsFrom, fresh.deletedSet),
91
+ importedBy: dropDeleted(importedBy, fresh.deletedSet),
63
92
  symbols: symbols.slice(0, 50).map(summarise),
93
+ ...(referencedByRows.length > 0 ? { referencedBy: dropDeleted(referencedByRows, fresh.deletedSet) } : {}),
94
+ ...(calledByRows.length > 0 ? { calledBy: dropDeleted(calledByRows, fresh.deletedSet) } : {}),
95
+ ...(subtypes.length > 0 ? { subtypes: subtypes.slice(0, 50).map(summarise) } : {}),
96
+ ...(supertypes.length > 0 ? { supertypes: supertypes.slice(0, 50).map(summarise) } : {}),
97
+ ...(fresh.field ?? {}),
64
98
  };
65
99
  return { data: formatObjectArrays(data, input) };
66
100
  },
@@ -1,4 +1,4 @@
1
- import { GraphQueryApi, GraphStore } from '@shrkcrft/graph';
1
+ import { GraphQueryApi, GraphStore, loadGraphApiCached } from '@shrkcrft/graph';
2
2
  import { FORMAT_INPUT_PROPERTY, formatObjectArrays } from "../server/columnar-format.js";
3
3
  const NEXT = 'shrk graph index';
4
4
  /**
@@ -40,7 +40,7 @@ export const getGraphCyclesTool = {
40
40
  },
41
41
  };
42
42
  }
43
- const api = GraphQueryApi.fromStore(ctx.inspection.projectRoot);
43
+ const api = loadGraphApiCached(ctx.inspection.projectRoot) ?? GraphQueryApi.fromStore(ctx.inspection.projectRoot);
44
44
  const all = api.cycles();
45
45
  const filtered = all.filter((c) => c.size >= minSize);
46
46
  const limited = filtered.slice(0, rawLimit);
@@ -1,4 +1,4 @@
1
- import { EdgeKind, GraphQueryApi, GraphStore } from '@shrkcrft/graph';
1
+ import { EdgeKind, GraphQueryApi, GraphStore, loadGraphApiCached } from '@shrkcrft/graph';
2
2
  const NEXT = 'shrk graph index';
3
3
  /**
4
4
  * Read-only MCP mirror of `shrk graph deps`. Returns the workspace
@@ -41,7 +41,7 @@ export const getGraphDepsTool = {
41
41
  },
42
42
  };
43
43
  }
44
- const api = GraphQueryApi.fromStore(ctx.inspection.projectRoot);
44
+ const api = loadGraphApiCached(ctx.inspection.projectRoot) ?? GraphQueryApi.fromStore(ctx.inspection.projectRoot);
45
45
  const pkgId = `package:${target}`;
46
46
  const pkgNode = api.neighbours(pkgId)?.node;
47
47
  if (!pkgNode) {
@@ -0,0 +1,3 @@
1
+ import type { IToolDefinition } from '../server/tool-definition.js';
2
+ export declare const getGraphHubsTool: IToolDefinition;
3
+ //# sourceMappingURL=get-graph-hubs.tool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-graph-hubs.tool.d.ts","sourceRoot":"","sources":["../../src/tools/get-graph-hubs.tool.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAWpE,eAAO,MAAM,gBAAgB,EAAE,eA4C9B,CAAC"}