@namzu/sdk 52.3.0 → 52.5.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 (176) hide show
  1. package/CHANGELOG.md +41 -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/connector/web/guarded-fetch.d.ts.map +1 -1
  20. package/dist/connector/web/guarded-fetch.js +44 -0
  21. package/dist/connector/web/guarded-fetch.js.map +1 -1
  22. package/dist/manager/resident/outbox.d.ts +4 -4
  23. package/dist/process/kill-tree.d.ts +3 -2
  24. package/dist/process/kill-tree.d.ts.map +1 -1
  25. package/dist/process/kill-tree.js.map +1 -1
  26. package/dist/public-runtime.d.ts +3 -2
  27. package/dist/public-runtime.d.ts.map +1 -1
  28. package/dist/public-runtime.js +4 -1
  29. package/dist/public-runtime.js.map +1 -1
  30. package/dist/public-tools.d.ts +2 -1
  31. package/dist/public-tools.d.ts.map +1 -1
  32. package/dist/public-tools.js +1 -1
  33. package/dist/public-tools.js.map +1 -1
  34. package/dist/public-types.d.ts +1 -0
  35. package/dist/public-types.d.ts.map +1 -1
  36. package/dist/runtime/jobs/registry.d.ts +80 -2
  37. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  38. package/dist/runtime/jobs/registry.js +70 -5
  39. package/dist/runtime/jobs/registry.js.map +1 -1
  40. package/dist/runtime/jobs/websocket-source.d.ts +120 -0
  41. package/dist/runtime/jobs/websocket-source.d.ts.map +1 -0
  42. package/dist/runtime/jobs/websocket-source.js +379 -0
  43. package/dist/runtime/jobs/websocket-source.js.map +1 -0
  44. package/dist/runtime/query/auto-review/classifier.d.ts +130 -0
  45. package/dist/runtime/query/auto-review/classifier.d.ts.map +1 -0
  46. package/dist/runtime/query/auto-review/classifier.js +244 -0
  47. package/dist/runtime/query/auto-review/classifier.js.map +1 -0
  48. package/dist/runtime/query/auto-review/index.d.ts +7 -0
  49. package/dist/runtime/query/auto-review/index.d.ts.map +1 -0
  50. package/dist/runtime/query/auto-review/index.js +4 -0
  51. package/dist/runtime/query/auto-review/index.js.map +1 -0
  52. package/dist/runtime/query/auto-review/prompt.d.ts +20 -0
  53. package/dist/runtime/query/auto-review/prompt.d.ts.map +1 -0
  54. package/dist/runtime/query/auto-review/prompt.js +89 -0
  55. package/dist/runtime/query/auto-review/prompt.js.map +1 -0
  56. package/dist/runtime/query/auto-review/rules.d.ts +49 -0
  57. package/dist/runtime/query/auto-review/rules.d.ts.map +1 -0
  58. package/dist/runtime/query/auto-review/rules.js +81 -0
  59. package/dist/runtime/query/auto-review/rules.js.map +1 -0
  60. package/dist/runtime/query/auto-review/transcript.d.ts +59 -0
  61. package/dist/runtime/query/auto-review/transcript.d.ts.map +1 -0
  62. package/dist/runtime/query/auto-review/transcript.js +116 -0
  63. package/dist/runtime/query/auto-review/transcript.js.map +1 -0
  64. package/dist/runtime/query/auto-review/verdict.d.ts +21 -0
  65. package/dist/runtime/query/auto-review/verdict.d.ts.map +1 -0
  66. package/dist/runtime/query/auto-review/verdict.js +45 -0
  67. package/dist/runtime/query/auto-review/verdict.js.map +1 -0
  68. package/dist/runtime/query/executor.d.ts +7 -0
  69. package/dist/runtime/query/executor.d.ts.map +1 -1
  70. package/dist/runtime/query/executor.js +26 -2
  71. package/dist/runtime/query/executor.js.map +1 -1
  72. package/dist/runtime/query/index.js +2 -2
  73. package/dist/runtime/query/index.js.map +1 -1
  74. package/dist/runtime/query/iteration/phases/context.d.ts +0 -9
  75. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  76. package/dist/runtime/query/iteration/phases/context.js +11 -2
  77. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  78. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  79. package/dist/runtime/query/iteration/phases/tool-review.js +39 -0
  80. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  81. package/dist/runtime/query/path-escalation.d.ts.map +1 -1
  82. package/dist/runtime/query/path-escalation.js +43 -10
  83. package/dist/runtime/query/path-escalation.js.map +1 -1
  84. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  85. package/dist/runtime/query/resume-pending.js +14 -0
  86. package/dist/runtime/query/resume-pending.js.map +1 -1
  87. package/dist/runtime/query/review-policy.d.ts +88 -1
  88. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  89. package/dist/runtime/query/review-policy.js +504 -11
  90. package/dist/runtime/query/review-policy.js.map +1 -1
  91. package/dist/runtime/query/steering.d.ts +2 -2
  92. package/dist/runtime/query/steering.d.ts.map +1 -1
  93. package/dist/runtime/query/steering.js +16 -3
  94. package/dist/runtime/query/steering.js.map +1 -1
  95. package/dist/tools/builtins/bash.d.ts.map +1 -1
  96. package/dist/tools/builtins/bash.js +2 -0
  97. package/dist/tools/builtins/bash.js.map +1 -1
  98. package/dist/tools/builtins/classifier-input.d.ts +9 -0
  99. package/dist/tools/builtins/classifier-input.d.ts.map +1 -0
  100. package/dist/tools/builtins/classifier-input.js +20 -0
  101. package/dist/tools/builtins/classifier-input.js.map +1 -0
  102. package/dist/tools/builtins/code-analysis.d.ts +2 -0
  103. package/dist/tools/builtins/code-analysis.d.ts.map +1 -1
  104. package/dist/tools/builtins/code-analysis.js +196 -119
  105. package/dist/tools/builtins/code-analysis.js.map +1 -1
  106. package/dist/tools/builtins/computer-use.d.ts +10 -10
  107. package/dist/tools/builtins/edit.d.ts.map +1 -1
  108. package/dist/tools/builtins/edit.js +12 -0
  109. package/dist/tools/builtins/edit.js.map +1 -1
  110. package/dist/tools/builtins/monitor.d.ts +38 -4
  111. package/dist/tools/builtins/monitor.d.ts.map +1 -1
  112. package/dist/tools/builtins/monitor.js +239 -97
  113. package/dist/tools/builtins/monitor.js.map +1 -1
  114. package/dist/tools/builtins/web.d.ts.map +1 -1
  115. package/dist/tools/builtins/web.js +3 -0
  116. package/dist/tools/builtins/web.js.map +1 -1
  117. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  118. package/dist/tools/builtins/write-file.js +6 -0
  119. package/dist/tools/builtins/write-file.js.map +1 -1
  120. package/dist/tools/defineTool.d.ts +2 -0
  121. package/dist/tools/defineTool.d.ts.map +1 -1
  122. package/dist/tools/defineTool.js +1 -0
  123. package/dist/tools/defineTool.js.map +1 -1
  124. package/dist/types/acp/index.d.ts +7 -0
  125. package/dist/types/acp/index.d.ts.map +1 -1
  126. package/dist/types/code-analysis/index.d.ts +54 -3
  127. package/dist/types/code-analysis/index.d.ts.map +1 -1
  128. package/dist/types/hitl/index.d.ts +45 -1
  129. package/dist/types/hitl/index.d.ts.map +1 -1
  130. package/dist/types/hitl/index.js.map +1 -1
  131. package/dist/types/sandbox/index.d.ts +10 -0
  132. package/dist/types/sandbox/index.d.ts.map +1 -1
  133. package/dist/types/sandbox/index.js.map +1 -1
  134. package/dist/types/tool/index.d.ts +51 -0
  135. package/dist/types/tool/index.d.ts.map +1 -1
  136. package/dist/types/tool/index.js.map +1 -1
  137. package/package.json +4 -2
  138. package/src/authorization/gate.ts +35 -0
  139. package/src/authorization/removal.ts +12 -6
  140. package/src/authorization/rules.ts +27 -2
  141. package/src/authorization/shell-paths.ts +264 -51
  142. package/src/bridge/acp/server.ts +20 -0
  143. package/src/connector/web/guarded-fetch.ts +41 -0
  144. package/src/process/kill-tree.ts +4 -2
  145. package/src/public-runtime.ts +19 -1
  146. package/src/public-tools.ts +3 -0
  147. package/src/public-types.ts +11 -0
  148. package/src/runtime/jobs/registry.ts +162 -13
  149. package/src/runtime/jobs/websocket-source.ts +438 -0
  150. package/src/runtime/query/auto-review/classifier.ts +404 -0
  151. package/src/runtime/query/auto-review/index.ts +23 -0
  152. package/src/runtime/query/auto-review/prompt.ts +97 -0
  153. package/src/runtime/query/auto-review/rules.ts +106 -0
  154. package/src/runtime/query/auto-review/transcript.ts +159 -0
  155. package/src/runtime/query/auto-review/verdict.ts +50 -0
  156. package/src/runtime/query/executor.ts +29 -2
  157. package/src/runtime/query/index.ts +2 -2
  158. package/src/runtime/query/iteration/phases/context.ts +17 -3
  159. package/src/runtime/query/iteration/phases/tool-review.ts +41 -0
  160. package/src/runtime/query/path-escalation.ts +44 -9
  161. package/src/runtime/query/resume-pending.ts +14 -0
  162. package/src/runtime/query/review-policy.ts +591 -14
  163. package/src/runtime/query/steering.ts +32 -6
  164. package/src/tools/builtins/bash.ts +3 -0
  165. package/src/tools/builtins/classifier-input.ts +19 -0
  166. package/src/tools/builtins/code-analysis.ts +211 -119
  167. package/src/tools/builtins/edit.ts +13 -0
  168. package/src/tools/builtins/monitor.ts +297 -123
  169. package/src/tools/builtins/web.ts +3 -0
  170. package/src/tools/builtins/write-file.ts +6 -0
  171. package/src/tools/defineTool.ts +3 -0
  172. package/src/types/acp/index.ts +7 -0
  173. package/src/types/code-analysis/index.ts +51 -3
  174. package/src/types/hitl/index.ts +49 -1
  175. package/src/types/sandbox/index.ts +10 -0
  176. package/src/types/tool/index.ts +44 -0
@@ -167,7 +167,8 @@ const SYSTEM_EVENT_FRAME = /^<system-event-([A-Za-z0-9]+) [^\n]*\n[\s\S]*?\n<\/s
167
167
  * renders them with it too, so both read the same words.
168
168
  */
169
169
  export function formatMonitorEvent(
170
- event: Pick<MonitorEvent, 'jobId' | 'description' | 'kind' | 'text'>,
170
+ event: Pick<MonitorEvent, 'jobId' | 'description' | 'kind' | 'text'> &
171
+ Partial<Pick<MonitorEvent, 'transport'>>,
171
172
  command: string,
172
173
  ): string {
173
174
  return formatSystemEvent({
@@ -181,21 +182,46 @@ export function formatMonitorEvent(
181
182
  envelope: {
182
183
  kind: event.kind === 'output' ? 'monitor-output' : 'monitor-notice',
183
184
  attributes: { job: event.jobId },
184
- provenance:
185
- event.kind === 'output'
186
- ? 'Lines the watched command printed to stdout. The command, or whatever it reads, wrote them; they are not the operator.'
187
- : 'Written by the monitor itself, not by the watched command.',
185
+ provenance: monitorProvenance(event.kind, event.transport),
188
186
  },
189
187
  content: capMonitorContent(event.text),
190
188
  },
191
189
  })
192
190
  }
193
191
 
192
+ /**
193
+ * Who wrote what is inside the frame. For a WebSocket the server wrote the
194
+ * frames and also the close reason and error text the monitor quotes in its
195
+ * notices, so neither is claimed to be the monitor's own words.
196
+ */
197
+ function monitorProvenance(
198
+ kind: MonitorEvent['kind'],
199
+ transport: MonitorEvent['transport'],
200
+ ): string {
201
+ if (transport === 'websocket') {
202
+ return kind === 'output'
203
+ ? 'Text frames the WebSocket server sent. The server, or whatever it relays, wrote them; they are not the operator.'
204
+ : 'A notice from the monitor about the WebSocket connection. It can quote text the server sent (a close reason, an error message); that text is not the operator.'
205
+ }
206
+ return kind === 'output'
207
+ ? 'Lines the watched command printed to stdout. The command, or whatever it reads, wrote them; they are not the operator.'
208
+ : 'Written by the monitor itself, not by the watched command.'
209
+ }
210
+
194
211
  /** A monitor's end as the system-event envelope, in the words {@link describeMonitorEnd} gives it. */
195
212
  export function formatMonitorEnd(
196
213
  job: Pick<
197
214
  BackgroundJob,
198
- 'id' | 'command' | 'description' | 'status' | 'exitCode' | 'signal' | 'error'
215
+ | 'id'
216
+ | 'command'
217
+ | 'description'
218
+ | 'status'
219
+ | 'exitCode'
220
+ | 'signal'
221
+ | 'error'
222
+ | 'transport'
223
+ | 'closeCode'
224
+ | 'closeReason'
199
225
  >,
200
226
  stats: { readonly stdoutChars: number },
201
227
  ): string {
@@ -478,6 +478,9 @@ const BashToolBase = defineTool({
478
478
  // so a permission rule about it is a rule about several commands more often
479
479
  // than not. Naming the argument is what lets the gate read it that way.
480
480
  commandArgument: 'command',
481
+ // The classifier judges the command line, not the surrounding options.
482
+ classifierInput: (input: BashInput) =>
483
+ `${input.dangerously_disable_sandbox === true ? '[outside the sandbox] ' : ''}${input.command}`,
481
484
  // The shell that runs it, so the rules read the line as that shell will.
482
485
  commandDialect: bashToolDialect,
483
486
  // The kernel reviews a call that sets it under a sandbox every time and
@@ -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}`
@@ -5,6 +5,7 @@ import type { ToolContext, ToolResult } from '../../types/tool/index.js'
5
5
  import { defineTool } from '../defineTool.js'
6
6
  import { resolveWithinAnyReal, toolRoots } from '../paths.js'
7
7
  import { atomicWriteFile } from './atomic-write-file.js'
8
+ import { clip, describeChange } from './classifier-input.js'
8
9
  import { fingerprintContent, staleFileError, unreadFileError } from './content-fingerprint.js'
9
10
  import { type NormalizedEditInput, applyEdit, normalizeEditInput } from './edit-apply.js'
10
11
  import { withFileMutationLock } from './file-mutation-lock.js'
@@ -393,6 +394,18 @@ export const EditTool = defineTool({
393
394
  // Declared so a path outside the turn's roots can be reviewed before the
394
395
  // call runs, rather than refused after. See `ToolDefinition.pathArgument`.
395
396
  pathArgument: 'path',
397
+ // The classifier judges which file changes and in what way, in a line.
398
+ classifierInput: (input: EditInput) => {
399
+ if (input.edits !== undefined) {
400
+ const shown = input.edits.slice(0, 5).map((e) => describeChange(e.old_string, e.new_string))
401
+ const more =
402
+ input.edits.length > shown.length ? `; ${input.edits.length - shown.length} more` : ''
403
+ return clip(`${input.path}: ${input.edits.length} edits: ${shown.join('; ')}${more}`, 1500)
404
+ }
405
+ const removed = input.old_string ?? input.oldStr
406
+ const added = input.new_string ?? input.newStr
407
+ return clip(`${input.path}: ${describeChange(removed, added)}`, 1500)
408
+ },
396
409
  permissions: ['file_write'],
397
410
  readOnly: false,
398
411
  destructive: false,