@namzu/sdk 52.4.0 → 52.6.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.
Files changed (152) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/authorization/gate.d.ts +11 -0
  3. package/dist/authorization/gate.d.ts.map +1 -1
  4. package/dist/authorization/gate.js +35 -0
  5. package/dist/authorization/gate.js.map +1 -1
  6. package/dist/authorization/removal.js +11 -6
  7. package/dist/authorization/removal.js.map +1 -1
  8. package/dist/authorization/rules.d.ts.map +1 -1
  9. package/dist/authorization/rules.js +30 -2
  10. package/dist/authorization/rules.js.map +1 -1
  11. package/dist/authorization/shell-paths.d.ts +20 -0
  12. package/dist/authorization/shell-paths.d.ts.map +1 -1
  13. package/dist/authorization/shell-paths.js +254 -52
  14. package/dist/authorization/shell-paths.js.map +1 -1
  15. package/dist/bridge/acp/server.d.ts +8 -0
  16. package/dist/bridge/acp/server.d.ts.map +1 -1
  17. package/dist/bridge/acp/server.js +7 -0
  18. package/dist/bridge/acp/server.js.map +1 -1
  19. package/dist/manager/resident/outbox.d.ts +4 -4
  20. package/dist/public-runtime.d.ts +2 -1
  21. package/dist/public-runtime.d.ts.map +1 -1
  22. package/dist/public-runtime.js +4 -1
  23. package/dist/public-runtime.js.map +1 -1
  24. package/dist/public-types.d.ts +1 -0
  25. package/dist/public-types.d.ts.map +1 -1
  26. package/dist/runtime/query/auto-review/classifier.d.ts +130 -0
  27. package/dist/runtime/query/auto-review/classifier.d.ts.map +1 -0
  28. package/dist/runtime/query/auto-review/classifier.js +244 -0
  29. package/dist/runtime/query/auto-review/classifier.js.map +1 -0
  30. package/dist/runtime/query/auto-review/index.d.ts +7 -0
  31. package/dist/runtime/query/auto-review/index.d.ts.map +1 -0
  32. package/dist/runtime/query/auto-review/index.js +4 -0
  33. package/dist/runtime/query/auto-review/index.js.map +1 -0
  34. package/dist/runtime/query/auto-review/prompt.d.ts +20 -0
  35. package/dist/runtime/query/auto-review/prompt.d.ts.map +1 -0
  36. package/dist/runtime/query/auto-review/prompt.js +89 -0
  37. package/dist/runtime/query/auto-review/prompt.js.map +1 -0
  38. package/dist/runtime/query/auto-review/rules.d.ts +49 -0
  39. package/dist/runtime/query/auto-review/rules.d.ts.map +1 -0
  40. package/dist/runtime/query/auto-review/rules.js +81 -0
  41. package/dist/runtime/query/auto-review/rules.js.map +1 -0
  42. package/dist/runtime/query/auto-review/transcript.d.ts +59 -0
  43. package/dist/runtime/query/auto-review/transcript.d.ts.map +1 -0
  44. package/dist/runtime/query/auto-review/transcript.js +116 -0
  45. package/dist/runtime/query/auto-review/transcript.js.map +1 -0
  46. package/dist/runtime/query/auto-review/verdict.d.ts +21 -0
  47. package/dist/runtime/query/auto-review/verdict.d.ts.map +1 -0
  48. package/dist/runtime/query/auto-review/verdict.js +45 -0
  49. package/dist/runtime/query/auto-review/verdict.js.map +1 -0
  50. package/dist/runtime/query/executor.d.ts +7 -0
  51. package/dist/runtime/query/executor.d.ts.map +1 -1
  52. package/dist/runtime/query/executor.js +26 -2
  53. package/dist/runtime/query/executor.js.map +1 -1
  54. package/dist/runtime/query/index.js +2 -2
  55. package/dist/runtime/query/index.js.map +1 -1
  56. package/dist/runtime/query/iteration/phases/context.d.ts +0 -9
  57. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  58. package/dist/runtime/query/iteration/phases/context.js +11 -2
  59. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  60. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  61. package/dist/runtime/query/iteration/phases/tool-review.js +39 -0
  62. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  63. package/dist/runtime/query/path-escalation.d.ts.map +1 -1
  64. package/dist/runtime/query/path-escalation.js +43 -10
  65. package/dist/runtime/query/path-escalation.js.map +1 -1
  66. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  67. package/dist/runtime/query/resume-pending.js +14 -0
  68. package/dist/runtime/query/resume-pending.js.map +1 -1
  69. package/dist/runtime/query/review-policy.d.ts +88 -1
  70. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  71. package/dist/runtime/query/review-policy.js +504 -11
  72. package/dist/runtime/query/review-policy.js.map +1 -1
  73. package/dist/tools/builtins/bash.d.ts.map +1 -1
  74. package/dist/tools/builtins/bash.js +2 -0
  75. package/dist/tools/builtins/bash.js.map +1 -1
  76. package/dist/tools/builtins/classifier-input.d.ts +9 -0
  77. package/dist/tools/builtins/classifier-input.d.ts.map +1 -0
  78. package/dist/tools/builtins/classifier-input.js +20 -0
  79. package/dist/tools/builtins/classifier-input.js.map +1 -0
  80. package/dist/tools/builtins/code-analysis.d.ts +2 -0
  81. package/dist/tools/builtins/code-analysis.d.ts.map +1 -1
  82. package/dist/tools/builtins/code-analysis.js +196 -119
  83. package/dist/tools/builtins/code-analysis.js.map +1 -1
  84. package/dist/tools/builtins/computer-use.d.ts +10 -10
  85. package/dist/tools/builtins/edit-apply.d.ts.map +1 -1
  86. package/dist/tools/builtins/edit-apply.js +30 -12
  87. package/dist/tools/builtins/edit-apply.js.map +1 -1
  88. package/dist/tools/builtins/edit-match.d.ts +47 -0
  89. package/dist/tools/builtins/edit-match.d.ts.map +1 -0
  90. package/dist/tools/builtins/edit-match.js +262 -0
  91. package/dist/tools/builtins/edit-match.js.map +1 -0
  92. package/dist/tools/builtins/edit.d.ts.map +1 -1
  93. package/dist/tools/builtins/edit.js +12 -0
  94. package/dist/tools/builtins/edit.js.map +1 -1
  95. package/dist/tools/builtins/monitor.d.ts.map +1 -1
  96. package/dist/tools/builtins/monitor.js +3 -0
  97. package/dist/tools/builtins/monitor.js.map +1 -1
  98. package/dist/tools/builtins/web.d.ts.map +1 -1
  99. package/dist/tools/builtins/web.js +3 -0
  100. package/dist/tools/builtins/web.js.map +1 -1
  101. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  102. package/dist/tools/builtins/write-file.js +6 -0
  103. package/dist/tools/builtins/write-file.js.map +1 -1
  104. package/dist/tools/defineTool.d.ts +2 -0
  105. package/dist/tools/defineTool.d.ts.map +1 -1
  106. package/dist/tools/defineTool.js +1 -0
  107. package/dist/tools/defineTool.js.map +1 -1
  108. package/dist/types/acp/index.d.ts +7 -0
  109. package/dist/types/acp/index.d.ts.map +1 -1
  110. package/dist/types/code-analysis/index.d.ts +54 -3
  111. package/dist/types/code-analysis/index.d.ts.map +1 -1
  112. package/dist/types/hitl/index.d.ts +45 -1
  113. package/dist/types/hitl/index.d.ts.map +1 -1
  114. package/dist/types/hitl/index.js.map +1 -1
  115. package/dist/types/tool/index.d.ts +16 -0
  116. package/dist/types/tool/index.d.ts.map +1 -1
  117. package/dist/types/tool/index.js.map +1 -1
  118. package/package.json +1 -1
  119. package/src/authorization/gate.ts +35 -0
  120. package/src/authorization/removal.ts +12 -6
  121. package/src/authorization/rules.ts +27 -2
  122. package/src/authorization/shell-paths.ts +264 -51
  123. package/src/bridge/acp/server.ts +20 -0
  124. package/src/public-runtime.ts +18 -0
  125. package/src/public-types.ts +11 -0
  126. package/src/runtime/query/auto-review/classifier.ts +404 -0
  127. package/src/runtime/query/auto-review/index.ts +23 -0
  128. package/src/runtime/query/auto-review/prompt.ts +97 -0
  129. package/src/runtime/query/auto-review/rules.ts +106 -0
  130. package/src/runtime/query/auto-review/transcript.ts +159 -0
  131. package/src/runtime/query/auto-review/verdict.ts +50 -0
  132. package/src/runtime/query/executor.ts +29 -2
  133. package/src/runtime/query/index.ts +2 -2
  134. package/src/runtime/query/iteration/phases/context.ts +17 -3
  135. package/src/runtime/query/iteration/phases/tool-review.ts +41 -0
  136. package/src/runtime/query/path-escalation.ts +44 -9
  137. package/src/runtime/query/resume-pending.ts +14 -0
  138. package/src/runtime/query/review-policy.ts +591 -14
  139. package/src/tools/builtins/bash.ts +3 -0
  140. package/src/tools/builtins/classifier-input.ts +19 -0
  141. package/src/tools/builtins/code-analysis.ts +211 -119
  142. package/src/tools/builtins/edit-apply.ts +31 -12
  143. package/src/tools/builtins/edit-match.ts +274 -0
  144. package/src/tools/builtins/edit.ts +13 -0
  145. package/src/tools/builtins/monitor.ts +4 -0
  146. package/src/tools/builtins/web.ts +3 -0
  147. package/src/tools/builtins/write-file.ts +6 -0
  148. package/src/tools/defineTool.ts +3 -0
  149. package/src/types/acp/index.ts +7 -0
  150. package/src/types/code-analysis/index.ts +51 -3
  151. package/src/types/hitl/index.ts +49 -1
  152. package/src/types/tool/index.ts +17 -0
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared helpers for `ToolDefinition.classifierInput`: what the auto-mode
3
+ * classifier is shown of a call to a built-in tool.
4
+ */
5
+
6
+ /** Cuts `text` to `limit` characters, saying how much was left out. */
7
+ export function clip(text: string, limit: number): string {
8
+ if (text.length <= limit) return text
9
+ return `${text.slice(0, limit)}... [${text.length - limit} more characters]`
10
+ }
11
+
12
+ /** The characters a change adds and removes, in a form short enough to read at a glance. */
13
+ export function describeChange(removed: string | undefined, added: string | undefined): string {
14
+ const parts: string[] = []
15
+ if (removed !== undefined && removed !== '')
16
+ parts.push(`removes ${JSON.stringify(clip(removed, 200))}`)
17
+ if (added !== undefined && added !== '') parts.push(`adds ${JSON.stringify(clip(added, 300))}`)
18
+ return parts.length > 0 ? parts.join(', ') : 'no text change'
19
+ }
@@ -1,4 +1,5 @@
1
1
  import { realpath, stat } from 'node:fs/promises'
2
+ import { isAbsolute, relative, sep } from 'node:path'
2
3
  import { z } from 'zod'
3
4
 
4
5
  import type {
@@ -112,7 +113,14 @@ const functionSourceOp = z.object({
112
113
  ),
113
114
  function_id: z.number().int().min(0).optional(),
114
115
  path: z.string().optional(),
115
- name: z.string().min(1).optional(),
116
+ name: z.string().min(1).optional().describe('`name` or `Class.method`.'),
117
+ line: z
118
+ .number()
119
+ .int()
120
+ .min(1)
121
+ .optional()
122
+ .describe('With `column`: cut a long function around this position, not at its end.'),
123
+ column: z.number().int().min(1).optional(),
116
124
  max_chars: maxChars,
117
125
  ...paging,
118
126
  })
@@ -123,7 +131,10 @@ const callersOp = z.object({
123
131
  .describe(
124
132
  'Call sites of a callee TEXT (not scope-aware), each with its caller. Per file by default.',
125
133
  ),
126
- name: z.string().min(1).describe('Callee text: `foo`, `obj.foo`, `this.foo`.'),
134
+ name: z
135
+ .string()
136
+ .min(1)
137
+ .describe('Callee text: `foo`, `obj.foo`, `this.foo`; or a method as `Class.method`.'),
127
138
  scope: z
128
139
  .enum(['file', 'workspace'])
129
140
  .optional()
@@ -207,122 +218,176 @@ export const CodeAnalysisTool = defineTool({
207
218
  }
208
219
  const root = owner.root
209
220
 
210
- switch (input.operation) {
211
- case 'index': {
212
- const result = await provider.index({
213
- root,
214
- // A narrower path indexes just that part of the root, which is
215
- // how a root too big to index whole is covered in steps.
216
- ...(target !== root ? { scope: target } : {}),
217
- ...(input.force ? { force: true } : {}),
218
- ...signalOf(context),
219
- })
220
- return render(result, (value) => {
221
- const f = value.files
222
- const lines = [
223
- `Indexed ${value.root}: ${f.indexed} files (${f.parsed} parsed, ${f.unchanged} unchanged, ${f.removed} removed, ${f.skipped} skipped, ${f.withParseErrors} with parse errors) in ${value.durationMs} ms.`,
224
- `${value.functions} functions, ${value.strings} string literals, ${value.calls} calls. ${value.persisted ? 'Cached on disk.' : 'Held in memory only; it is gone when this process exits.'}`,
225
- ]
226
- if (value.truncated) lines.push(value.truncated)
227
- const reasons = Object.entries(value.skippedReasons)
228
- if (reasons.length > 0) {
229
- lines.push(`Skipped: ${reasons.map(([why, n]) => `${n} ${why}`).join(', ')}.`)
230
- }
231
- return lines
232
- })
233
- }
234
- case 'find_literal': {
235
- const result = await provider.findLiteral({
236
- root,
237
- text: input.text,
238
- path: target,
239
- ...(input.case_sensitive === false ? { caseSensitive: false } : {}),
240
- ...pageArgs(input),
241
- ...signalOf(context),
242
- })
243
- return renderPage(result, (hit) => {
244
- const where = `${hit.path}:${hit.line}:${hit.column}`
245
- const shown = JSON.stringify(hit.value) + (hit.valueTruncated ? ' (cut)' : '')
246
- const inside = hit.function ? ` in ${describeFunction(hit.function)}` : ' at top level'
247
- return `${where} ${hit.kind} ${shown}${inside}${staleMark(hit.stale)}`
248
- })
249
- }
250
- case 'enclosing_function': {
251
- const result = await provider.enclosingFunction({
252
- root,
253
- path: target,
254
- line: input.line,
255
- ...(input.column !== undefined ? { column: input.column } : {}),
256
- maxChars: input.max_chars ?? DEFAULT_MAX_CHARS,
257
- ...signalOf(context),
258
- })
259
- return render(result, (value) => {
260
- if (!value.function) {
261
- return [`${target}:${input.line}:${input.column ?? 1} is not inside any function.`]
221
+ const outcome = await (async (): Promise<ToolResult> => {
222
+ switch (input.operation) {
223
+ case 'index': {
224
+ const result = await provider.index({
225
+ root,
226
+ // A narrower path indexes just that part of the root, which is
227
+ // how a root too big to index whole is covered in steps.
228
+ ...(target !== root ? { scope: target } : {}),
229
+ ...(input.force ? { force: true } : {}),
230
+ ...signalOf(context),
231
+ })
232
+ return render(result, (value) => {
233
+ const f = value.files
234
+ const lines = [
235
+ `Indexed ${value.root}: ${f.indexed} files (${f.parsed} parsed, ${f.unchanged} unchanged, ${f.removed} removed, ${f.skipped} skipped, ${f.withParseErrors} with parse errors) in ${value.durationMs} ms.`,
236
+ `${value.functions} functions, ${value.strings} string literals, ${value.calls} calls. ${value.persisted ? 'Cached on disk.' : 'Held in memory only; it is gone when this process exits.'}`,
237
+ ]
238
+ if (value.truncated) lines.push(value.truncated)
239
+ const reasons = Object.entries(value.skippedReasons)
240
+ if (reasons.length > 0) {
241
+ lines.push(`Skipped: ${reasons.map(([why, n]) => `${n} ${why}`).join(', ')}.`)
242
+ }
243
+ return lines
244
+ })
245
+ }
246
+ case 'find_literal': {
247
+ const result = await provider.findLiteral({
248
+ root,
249
+ text: input.text,
250
+ path: target,
251
+ ...(input.case_sensitive === false ? { caseSensitive: false } : {}),
252
+ ...pageArgs(input),
253
+ ...signalOf(context),
254
+ })
255
+ return renderPage(
256
+ result,
257
+ (hit) => {
258
+ const shown = JSON.stringify(hit.value) + (hit.valueTruncated ? ' (cut)' : '')
259
+ const kind = hit.kind === 'string' ? '' : ` (${hit.kind})`
260
+ const inside = hit.function ? ` in ${shortFunction(hit.function)}` : ' at top level'
261
+ return `${hit.line}:${hit.column} ${shown}${kind}${inside}${staleMark(hit.stale)}`
262
+ },
263
+ (hit) => relativeTo(root, hit.path),
264
+ )
265
+ }
266
+ case 'enclosing_function': {
267
+ const result = await provider.enclosingFunction({
268
+ root,
269
+ path: target,
270
+ line: input.line,
271
+ ...(input.column !== undefined ? { column: input.column } : {}),
272
+ maxChars: input.max_chars ?? DEFAULT_MAX_CHARS,
273
+ ...signalOf(context),
274
+ })
275
+ return render(result, (value) => {
276
+ if (!value.function) {
277
+ const place = `${relativeTo(root, target)}:${input.line}:${input.column ?? 1}`
278
+ return [
279
+ value.inClass !== undefined
280
+ ? `${place} is in the body of class ${value.inClass}, outside every method; not inside any function.`
281
+ : `${place} is not inside any function.`,
282
+ ]
283
+ }
284
+ const lines = [describeFunction(value.function, root)]
285
+ if (value.parents.length > 0) {
286
+ lines.push(`parents: ${value.parents.map(shortFunction).join(' < ')}`)
287
+ }
288
+ if (value.source) lines.push(...sourceLines(value.source))
289
+ return lines
290
+ })
291
+ }
292
+ case 'function_source': {
293
+ if (input.function_id === undefined && !(input.path !== undefined && input.name)) {
294
+ return fail('function_source needs `function_id`, or `path` together with `name`.')
262
295
  }
263
- const lines = [`${describeFunction(value.function)}`]
264
- if (value.parents.length > 0) {
265
- lines.push(`parents: ${value.parents.map(describeFunction).join(' < ')}`)
296
+ const result = await provider.functionSource({
297
+ root,
298
+ ...(input.function_id !== undefined ? { id: input.function_id } : {}),
299
+ ...(input.name ? { name: input.name, path: target } : {}),
300
+ ...(input.line !== undefined
301
+ ? {
302
+ around: {
303
+ line: input.line,
304
+ ...(input.column !== undefined ? { column: input.column } : {}),
305
+ },
306
+ }
307
+ : {}),
308
+ maxChars: input.max_chars ?? DEFAULT_MAX_CHARS,
309
+ ...pageArgs(input),
310
+ ...signalOf(context),
311
+ })
312
+ return renderPage(result, (item) =>
313
+ [
314
+ describeFunction(item.function, root),
315
+ item.parents.length > 0
316
+ ? `parents: ${item.parents.map(shortFunction).join(' < ')}`
317
+ : undefined,
318
+ ...sourceLines(item.source),
319
+ ]
320
+ .filter((l): l is string => l !== undefined)
321
+ .join('\n'),
322
+ )
323
+ }
324
+ case 'callers_of': {
325
+ const scope = input.scope ?? 'file'
326
+ if (scope === 'file' && input.path === undefined) {
327
+ return fail(
328
+ 'callers_of looks in one file by default, so it needs `path`. Pass `scope: "workspace"` to search the whole index; it matches by name only and will mix unrelated functions that share one.',
329
+ )
266
330
  }
267
- if (value.source) lines.push(...sourceLines(value.source))
268
- return lines
269
- })
270
- }
271
- case 'function_source': {
272
- if (input.function_id === undefined && !(input.path !== undefined && input.name)) {
273
- return fail('function_source needs `function_id`, or `path` together with `name`.')
331
+ const result = await provider.callersOf({
332
+ root,
333
+ name: input.name,
334
+ scope,
335
+ ...(input.path !== undefined ? { path: target } : {}),
336
+ ...(input.match ? { match: input.match } : {}),
337
+ ...pageArgs(input),
338
+ ...signalOf(context),
339
+ })
340
+ return renderPage(
341
+ result,
342
+ (site) => {
343
+ const inside = site.caller ? ` in ${shortFunction(site.caller)}` : ' at top level'
344
+ const how = site.kind === 'new' ? 'new ' : ''
345
+ const tag =
346
+ site.kind === 'call-via'
347
+ ? ' [call/apply/bind]'
348
+ : site.kind === 'tagged'
349
+ ? ' [tagged template]'
350
+ : ''
351
+ const label = site.resolution === 'text-only' ? ' [text-only]' : ''
352
+ return `${site.line}:${site.column} ${how}${site.callee}(${site.argumentCount} arg${site.argumentCount === 1 ? '' : 's'})${tag}${label}${inside}${staleMark(site.stale)}`
353
+ },
354
+ (site) => relativeTo(root, site.path),
355
+ )
274
356
  }
275
- const result = await provider.functionSource({
276
- root,
277
- ...(input.function_id !== undefined ? { id: input.function_id } : {}),
278
- ...(input.name ? { name: input.name, path: target } : {}),
279
- maxChars: input.max_chars ?? DEFAULT_MAX_CHARS,
280
- ...pageArgs(input),
281
- ...signalOf(context),
282
- })
283
- return renderPage(result, (item) =>
284
- [
285
- describeFunction(item.function),
286
- item.parents.length > 0
287
- ? `parents: ${item.parents.map(describeFunction).join(' < ')}`
288
- : undefined,
289
- ...sourceLines(item.source),
290
- ]
291
- .filter((l): l is string => l !== undefined)
292
- .join('\n'),
293
- )
294
- }
295
- case 'callers_of': {
296
- const scope = input.scope ?? 'file'
297
- if (scope === 'file' && input.path === undefined) {
298
- return fail(
299
- 'callers_of looks in one file by default, so it needs `path`. Pass `scope: "workspace"` to search the whole index; it matches by name only and will mix unrelated functions that share one.',
357
+ case 'imports': {
358
+ const result = await provider.imports({
359
+ root,
360
+ path: target,
361
+ ...pageArgs(input),
362
+ ...signalOf(context),
363
+ })
364
+ return render(
365
+ result,
366
+ (page) =>
367
+ page.items.length === 0
368
+ ? [page.total === 0 ? 'No results.' : 'No results on this page.']
369
+ : [
370
+ relativeTo(root, target),
371
+ ...page.items.map((edge) => ` ${describeEdge(edge)}`),
372
+ page.nextCursor !== undefined
373
+ ? `-- ${page.items.length} of ${page.total} shown. next cursor: ${page.nextCursor}`
374
+ : `-- ${page.items.length} of ${page.total} shown. last page.`,
375
+ ],
376
+ (page) => ({
377
+ total: page.total,
378
+ count: page.items.length,
379
+ items: page.items,
380
+ ...(page.nextCursor !== undefined ? { nextCursor: page.nextCursor } : {}),
381
+ }),
300
382
  )
301
383
  }
302
- const result = await provider.callersOf({
303
- root,
304
- name: input.name,
305
- scope,
306
- ...(input.path !== undefined ? { path: target } : {}),
307
- ...(input.match ? { match: input.match } : {}),
308
- ...pageArgs(input),
309
- ...signalOf(context),
310
- })
311
- return renderPage(result, (site) => {
312
- const inside = site.caller ? ` in ${describeFunction(site.caller)}` : ' at top level'
313
- return `${site.path}:${site.line}:${site.column} ${site.kind === 'new' ? 'new ' : ''}${site.callee}(${site.argumentCount} arg${site.argumentCount === 1 ? '' : 's'})${inside}${staleMark(site.stale)}`
314
- })
315
- }
316
- case 'imports': {
317
- const result = await provider.imports({
318
- root,
319
- path: target,
320
- ...pageArgs(input),
321
- ...signalOf(context),
322
- })
323
- return renderPage(result, (edge) => describeEdge(edge, target))
324
384
  }
325
- }
385
+ })()
386
+ // With one root, a relative path is unambiguous. With several, the same
387
+ // `src/a.ts` could be in any of them, so the reader is told which this is.
388
+ return roots.length > 1 && outcome.success
389
+ ? { ...outcome, output: `paths are relative to ${root}\n${outcome.output}` }
390
+ : outcome
326
391
  },
327
392
  })
328
393
 
@@ -381,13 +446,27 @@ async function owningRoot(roots: readonly string[], target: string): Promise<Own
381
446
  return sawFile ? { kind: 'file' } : { kind: 'none' }
382
447
  }
383
448
 
384
- function describeFunction(fn: AnalysisFunction): string {
385
- const lines = fn.endLine === fn.line ? `line ${fn.line}` : `lines ${fn.line}-${fn.endLine}`
386
- return `${fn.name ?? '(anonymous)'} [${fn.kind}, id ${fn.id}] ${fn.path}:${fn.line}:${fn.column} (${lines})`
449
+ /** `path` relative to the root (POSIX separators), or `path` itself when it is not under it. */
450
+ function relativeTo(root: string, path: string): string {
451
+ const rel = relative(root, path)
452
+ // `..` as a whole segment is outside the root; `..x` is a directory named so.
453
+ if (rel === '' || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return path
454
+ return sep === '/' ? rel : rel.split(sep).join('/')
387
455
  }
388
456
 
389
- function describeEdge(edge: AnalysisModuleEdge, path: string): string {
390
- const where = `${path}:${edge.line}:${edge.column}`
457
+ /** `Svc.init#12 L10-40`: enough to cite and to ask for by id, no repeated path or kind. */
458
+ function shortFunction(fn: AnalysisFunction): string {
459
+ const lines = fn.endLine === fn.line ? `L${fn.line}` : `L${fn.line}-${fn.endLine}`
460
+ return `${fn.qualifiedName ?? fn.name ?? '(anonymous)'}#${fn.id} ${lines}`
461
+ }
462
+
463
+ /** The full line a single function gets: kind and place included. */
464
+ function describeFunction(fn: AnalysisFunction, root: string): string {
465
+ return `${shortFunction(fn)} [${fn.kind}] ${relativeTo(root, fn.path)}:${fn.line}:${fn.column}`
466
+ }
467
+
468
+ function describeEdge(edge: AnalysisModuleEdge): string {
469
+ const where = `${edge.line}:${edge.column}`
391
470
  if (edge.direction === 'import') {
392
471
  const names = edge.names.length > 0 ? ` { ${edge.names.join(', ')} }` : ''
393
472
  return `${where} import (${edge.kind})${names} from ${JSON.stringify(edge.source)}`
@@ -400,7 +479,9 @@ function sourceLines(slice: AnalysisSourceSlice): string[] {
400
479
  const lines = ['--- source ---', slice.text]
401
480
  if (slice.truncated) {
402
481
  lines.push(
403
- `--- cut: showing ${slice.text.length} of ${slice.chars} characters; raise max_chars to see more ---`,
482
+ slice.window
483
+ ? `--- cut: showing ${slice.text.length} of ${slice.chars} characters, the function's first ${slice.window.headerChars} and the part around the position; raise max_chars to see more ---`
484
+ : `--- cut: showing ${slice.text.length} of ${slice.chars} characters; raise max_chars to see more ---`,
404
485
  )
405
486
  }
406
487
  return lines
@@ -449,6 +530,8 @@ function render<T>(
449
530
  function renderPage<T>(
450
531
  result: CodeAnalysisResult<AnalysisPage<T>>,
451
532
  line: (item: T) => string,
533
+ /** Groups consecutive items of one file under a single heading. */
534
+ fileOf?: (item: T) => string,
452
535
  ): ToolResult {
453
536
  return render(
454
537
  result,
@@ -456,7 +539,16 @@ function renderPage<T>(
456
539
  if (page.items.length === 0) {
457
540
  return [page.total === 0 ? 'No results.' : 'No results on this page.']
458
541
  }
459
- const lines = page.items.map(line)
542
+ const lines: string[] = []
543
+ let current: string | undefined
544
+ for (const item of page.items) {
545
+ const file = fileOf?.(item)
546
+ if (file !== undefined && file !== current) {
547
+ lines.push(file)
548
+ current = file
549
+ }
550
+ lines.push(file === undefined ? line(item) : ` ${line(item)}`)
551
+ }
460
552
  lines.push(
461
553
  page.nextCursor !== undefined
462
554
  ? `-- ${page.items.length} of ${page.total} shown. next cursor: ${page.nextCursor}`
@@ -1,3 +1,4 @@
1
+ import { reconcileNewString, resolveOldString } from './edit-match.js'
1
2
  import type { EditInput } from './edit.js'
2
3
 
3
4
  /**
@@ -177,27 +178,47 @@ function applyOne(
177
178
 
178
179
  const replacement = normalizeLineEndings(content, input)
179
180
 
180
- if (!content.includes(replacement.oldString)) {
181
- return {
182
- success: false,
183
- error:
184
- 'old_string/oldStr not found in file. Make sure the string matches exactly, including whitespace and indentation.',
181
+ // Exact first — which, after the CRLF reconciliation above, is what nearly
182
+ // every call is. On a miss, recover the span across a difference the model
183
+ // did not mean: a curly quote for a straight one, a `\uXXXX` escape for the
184
+ // character or the reverse. `resolveOldString` returns the real span in the
185
+ // file, and `reconcileNewString` carries the same change onto the
186
+ // replacement so it fits where it lands. See `edit-match.ts`.
187
+ let oldString = replacement.oldString
188
+ let newString = replacement.newString
189
+ if (!content.includes(oldString)) {
190
+ const resolved = resolveOldString(content, oldString)
191
+ if (resolved === null) {
192
+ return {
193
+ success: false,
194
+ error:
195
+ 'old_string/oldStr not found in file. Make sure the string matches exactly, including whitespace and indentation.',
196
+ }
185
197
  }
198
+ newString = reconcileNewString(oldString, resolved, newString)
199
+ oldString = resolved
200
+ }
201
+
202
+ // A deletion takes the line's trailing newline with it, so removing a whole
203
+ // line does not leave a blank one behind. Only when the anchor is not
204
+ // already newline-terminated and the file has it followed by one.
205
+ if (newString === '' && !oldString.endsWith('\n') && content.includes(`${oldString}\n`)) {
206
+ oldString = `${oldString}\n`
186
207
  }
187
208
 
188
209
  if (replacement.replace_all) {
189
- const parts = content.split(replacement.oldString)
210
+ const parts = content.split(oldString)
190
211
  const replacements = parts.length - 1
191
212
  return {
192
213
  success: true,
193
- content: parts.join(replacement.newString),
214
+ content: parts.join(newString),
194
215
  replacements,
195
216
  }
196
217
  }
197
218
 
198
219
  // Uniqueness check: old_string/oldStr must appear exactly once
199
- const firstIndex = content.indexOf(replacement.oldString)
200
- const secondIndex = content.indexOf(replacement.oldString, firstIndex + 1)
220
+ const firstIndex = content.indexOf(oldString)
221
+ const secondIndex = content.indexOf(oldString, firstIndex + 1)
201
222
 
202
223
  if (secondIndex !== -1) {
203
224
  const lineNumber = content.slice(0, firstIndex).split('\n').length
@@ -211,9 +232,7 @@ function applyOne(
211
232
  return {
212
233
  success: true,
213
234
  content:
214
- content.slice(0, firstIndex) +
215
- replacement.newString +
216
- content.slice(firstIndex + replacement.oldString.length),
235
+ content.slice(0, firstIndex) + newString + content.slice(firstIndex + oldString.length),
217
236
  replacements: 1,
218
237
  }
219
238
  }