nolo-cli 0.1.19 → 0.1.21

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 (111) hide show
  1. package/README.md +9 -1
  2. package/agent-runtime/agentConfigOptions.ts +12 -0
  3. package/agent-runtime/agentRecordConfig.ts +99 -0
  4. package/agent-runtime/agentRecordKeys.ts +14 -0
  5. package/agent-runtime/dialogMessageRecord.ts +16 -0
  6. package/agent-runtime/dialogWritePlan.ts +130 -0
  7. package/agent-runtime/hostAdapter.ts +13 -0
  8. package/agent-runtime/hybridRecordStore.ts +147 -0
  9. package/agent-runtime/index.ts +69 -0
  10. package/agent-runtime/localLoop.ts +69 -5
  11. package/agent-runtime/localToolPolicy.ts +130 -0
  12. package/agent-runtime/localWorkspaceTools.ts +1532 -0
  13. package/agent-runtime/openAiCompatibleProvider.ts +70 -0
  14. package/agent-runtime/openAiCompatibleProviderConfig.ts +38 -0
  15. package/agent-runtime/platformChatProvider.ts +241 -0
  16. package/agent-runtime/taskWorkspace.ts +193 -0
  17. package/agent-runtime/types.ts +1 -0
  18. package/agent-runtime/workspaceSession.ts +76 -0
  19. package/agentAliases.ts +37 -0
  20. package/agentPullCommand.ts +1 -1
  21. package/agentRunCommand.ts +278 -52
  22. package/agentRuntimeCommands.ts +354 -164
  23. package/agentRuntimeLocal.ts +38 -0
  24. package/ai/agent/agentSlice.ts +10 -0
  25. package/ai/agent/buildEditingContext.ts +5 -0
  26. package/ai/agent/buildSystemPrompt.ts +41 -18
  27. package/ai/agent/canvasEditingContext.ts +49 -0
  28. package/ai/agent/cliExecutor.ts +15 -4
  29. package/ai/agent/createAgentSchema.ts +2 -0
  30. package/ai/agent/executeToolCall.ts +3 -2
  31. package/ai/agent/hooks/usePublicAgents.ts +6 -0
  32. package/ai/agent/pageBuilderHandoffRules.ts +75 -0
  33. package/ai/agent/runAgentClientLoop.ts +4 -1
  34. package/ai/agent/runtimeGuidance.ts +19 -0
  35. package/ai/agent/server/fetchPublicAgents.ts +51 -1
  36. package/ai/agent/streamAgentChatTurn.ts +20 -2
  37. package/ai/agent/streamAgentChatTurnUtils.ts +60 -16
  38. package/ai/chat/accumulateToolCallChunks.ts +40 -9
  39. package/ai/chat/parseApiError.ts +3 -0
  40. package/ai/chat/sendOpenAICompletionsRequest.native.ts +23 -10
  41. package/ai/chat/sendOpenAICompletionsRequest.ts +13 -1
  42. package/ai/chat/updateTotalUsage.ts +26 -9
  43. package/ai/llm/deepinfra.ts +51 -0
  44. package/ai/llm/getPricing.ts +6 -0
  45. package/ai/llm/kimi.ts +2 -0
  46. package/ai/llm/openrouterModels.ts +0 -135
  47. package/ai/llm/providers.ts +1 -0
  48. package/ai/llm/types.ts +8 -0
  49. package/ai/taskRun/taskRunProtocol.ts +882 -0
  50. package/ai/token/calculatePrice.ts +30 -0
  51. package/ai/token/externalToolCost.ts +49 -29
  52. package/ai/token/prepareTokenUsageData.ts +6 -1
  53. package/ai/token/serverTokenWriter.ts +4 -2
  54. package/ai/tools/agent/agentTools.ts +21 -0
  55. package/ai/tools/agent/presets/appBuilderPreset.ts +7 -0
  56. package/ai/tools/agent/streamParallelAgentsTool.ts +2 -1
  57. package/ai/tools/agent/taskRunTool.ts +112 -0
  58. package/ai/tools/applyEditTool.ts +6 -3
  59. package/ai/tools/applyLineEditsTool.ts +6 -3
  60. package/ai/tools/checkEnvTool.ts +14 -9
  61. package/ai/tools/codeSearchTool.ts +17 -5
  62. package/ai/tools/execBashTool.ts +33 -29
  63. package/ai/tools/fetchWebpageSupport.ts +24 -0
  64. package/ai/tools/fetchWebpageTool.ts +18 -5
  65. package/ai/tools/index.ts +158 -0
  66. package/ai/tools/jdProductScraperTool.ts +821 -0
  67. package/ai/tools/listFilesTool.ts +6 -3
  68. package/ai/tools/localFilesTool.ts +200 -0
  69. package/ai/tools/readFileTool.ts +6 -3
  70. package/ai/tools/searchRepoTool.ts +6 -3
  71. package/ai/tools/table/rowTools.ts +6 -1
  72. package/ai/tools/taobaoTmallProductScraperTool.ts +49 -0
  73. package/ai/tools/toolApiClient.ts +20 -6
  74. package/ai/tools/wereadGatewayTool.ts +152 -0
  75. package/ai/tools/writeFileTool.ts +6 -3
  76. package/client/agentConfigResolver.test.ts +70 -0
  77. package/client/agentConfigResolver.ts +1 -0
  78. package/client/agentRun.test.ts +430 -7
  79. package/client/agentRun.ts +504 -64
  80. package/client/hybridRecordStore.test.ts +115 -0
  81. package/client/hybridRecordStore.ts +41 -0
  82. package/client/localAgentRecords.test.ts +27 -0
  83. package/client/localAgentRecords.ts +7 -0
  84. package/client/localDialogRecords.test.ts +124 -0
  85. package/client/localDialogRecords.ts +30 -0
  86. package/client/localProviderResolver.test.ts +78 -0
  87. package/client/localProviderResolver.ts +1 -0
  88. package/client/localRuntimeAdapter.test.ts +621 -9
  89. package/client/localRuntimeAdapter.ts +275 -250
  90. package/client/localRuntimeDryRun.test.ts +116 -0
  91. package/client/localToolPolicy.ts +8 -81
  92. package/client/taskRunPrompt.ts +26 -0
  93. package/client/taskWorktree.ts +8 -0
  94. package/client/workspaceSession.test.ts +57 -0
  95. package/client/workspaceSession.ts +11 -0
  96. package/commandRegistry.ts +23 -6
  97. package/connectorRunArtifact.ts +121 -0
  98. package/database/actions/write.ts +16 -2
  99. package/database/hooks/useUserData.ts +9 -3
  100. package/database/server/dataHandlers.ts +18 -20
  101. package/database/server/emailRepository.ts +3 -3
  102. package/database/server/patch.ts +18 -10
  103. package/database/server/query.ts +43 -4
  104. package/database/server/read.ts +24 -38
  105. package/database/server/recordIdentity.ts +100 -0
  106. package/database/server/write.ts +21 -25
  107. package/index.ts +70 -33
  108. package/machineCommands.ts +318 -144
  109. package/package.json +4 -1
  110. package/tableCommands.ts +181 -0
  111. package/taskRunCommand.ts +265 -0
@@ -0,0 +1,1532 @@
1
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { dirname, relative, resolve, sep } from "node:path";
3
+
4
+ import type {
5
+ AgentRuntimeToolCallInput,
6
+ AgentRuntimeToolResult,
7
+ } from "./types";
8
+
9
+ type LocalWorkspaceToolArgs = {
10
+ workspaceRoot: string;
11
+ commandTimeoutMs?: number;
12
+ };
13
+
14
+ type WorkspaceFileArgs = {
15
+ path?: unknown;
16
+ content?: unknown;
17
+ oldText?: unknown;
18
+ newText?: unknown;
19
+ expectedReplacements?: unknown;
20
+ query?: unknown;
21
+ command?: unknown;
22
+ cmd?: unknown;
23
+ patch?: unknown;
24
+ script?: unknown;
25
+ args?: unknown;
26
+ branch?: unknown;
27
+ paths?: unknown;
28
+ message?: unknown;
29
+ staged?: unknown;
30
+ baseUrl?: unknown;
31
+ base?: unknown;
32
+ waitSelector?: unknown;
33
+ scrollSelector?: unknown;
34
+ focusSelector?: unknown;
35
+ expectText?: unknown;
36
+ screenshotPath?: unknown;
37
+ metricsPath?: unknown;
38
+ };
39
+
40
+ type OpenAiCompatibleTool = Record<string, unknown> & {
41
+ function?: Record<string, unknown> & { name?: string };
42
+ };
43
+
44
+ const WORKSPACE_TOOL_NAMES = [
45
+ "listWorkspaceFiles",
46
+ "readWorkspaceFile",
47
+ "writeWorkspaceFile",
48
+ "replaceWorkspaceText",
49
+ "listFiles",
50
+ "readFile",
51
+ "writeFile",
52
+ "searchWorkspace",
53
+ "applyEdit",
54
+ "applyLineEdits",
55
+ "applyPatch",
56
+ "gitStatus",
57
+ "gitDiff",
58
+ "gitCreateBranch",
59
+ "gitAdd",
60
+ "gitCommit",
61
+ "commitWorkspace",
62
+ "runPackageScript",
63
+ "startPreview",
64
+ "getPreviewStatus",
65
+ "stopPreview",
66
+ "releasePreview",
67
+ "captureVisualState",
68
+ "execShell",
69
+ "execBash",
70
+ ] as const;
71
+
72
+ const DEFAULT_LOCAL_CODING_TOOL_NAMES = [
73
+ "readWorkspaceFile",
74
+ "writeWorkspaceFile",
75
+ "replaceWorkspaceText",
76
+ "searchWorkspace",
77
+ "applyPatch",
78
+ "gitStatus",
79
+ "gitDiff",
80
+ "gitCreateBranch",
81
+ "commitWorkspace",
82
+ "runPackageScript",
83
+ "startPreview",
84
+ "getPreviewStatus",
85
+ "stopPreview",
86
+ "releasePreview",
87
+ "captureVisualState",
88
+ ] as const;
89
+
90
+ const SHELL_TOOL_NAMES = ["execShell", "execBash"] as const;
91
+ const DEFAULT_WORKSPACE_COMMAND_TIMEOUT_MS = 120_000;
92
+
93
+ function buildWorkspacePathProperty() {
94
+ return {
95
+ type: "string",
96
+ description: "Path relative to the workspace root.",
97
+ };
98
+ }
99
+
100
+ function buildListWorkspaceFilesTool(): OpenAiCompatibleTool {
101
+ return {
102
+ type: "function",
103
+ function: {
104
+ name: "listWorkspaceFiles",
105
+ description: "List files and directories inside a workspace directory.",
106
+ parameters: {
107
+ type: "object",
108
+ properties: {
109
+ path: buildWorkspacePathProperty(),
110
+ },
111
+ },
112
+ },
113
+ };
114
+ }
115
+
116
+ function buildReadWorkspaceFileTool(): OpenAiCompatibleTool {
117
+ return {
118
+ type: "function",
119
+ function: {
120
+ name: "readWorkspaceFile",
121
+ description: "Read a UTF-8 text file inside the workspace.",
122
+ parameters: {
123
+ type: "object",
124
+ properties: {
125
+ path: buildWorkspacePathProperty(),
126
+ },
127
+ required: ["path"],
128
+ },
129
+ },
130
+ };
131
+ }
132
+
133
+ function buildWriteWorkspaceFileTool(): OpenAiCompatibleTool {
134
+ return {
135
+ type: "function",
136
+ function: {
137
+ name: "writeWorkspaceFile",
138
+ description: "Write UTF-8 text content to a file inside the workspace.",
139
+ parameters: {
140
+ type: "object",
141
+ properties: {
142
+ path: buildWorkspacePathProperty(),
143
+ content: {
144
+ type: "string",
145
+ description: "Full UTF-8 file content to write.",
146
+ },
147
+ },
148
+ required: ["path", "content"],
149
+ },
150
+ },
151
+ };
152
+ }
153
+
154
+ function buildReplaceWorkspaceTextTool(): OpenAiCompatibleTool {
155
+ return {
156
+ type: "function",
157
+ function: {
158
+ name: "replaceWorkspaceText",
159
+ description: "Replace exact UTF-8 text in one workspace file without constructing a patch.",
160
+ parameters: {
161
+ type: "object",
162
+ properties: {
163
+ path: buildWorkspacePathProperty(),
164
+ oldText: {
165
+ type: "string",
166
+ description: "Exact text currently present in the file.",
167
+ },
168
+ newText: {
169
+ type: "string",
170
+ description: "Replacement text to write in place of oldText.",
171
+ },
172
+ expectedReplacements: {
173
+ type: "integer",
174
+ description: "Expected replacement count. Defaults to 1.",
175
+ },
176
+ },
177
+ required: ["path", "oldText", "newText"],
178
+ },
179
+ },
180
+ };
181
+ }
182
+
183
+ function buildSearchWorkspaceTool(): OpenAiCompatibleTool {
184
+ return {
185
+ type: "function",
186
+ function: {
187
+ name: "searchWorkspace",
188
+ description: "Search text in the current workspace using ripgrep when available.",
189
+ parameters: {
190
+ type: "object",
191
+ properties: {
192
+ query: {
193
+ type: "string",
194
+ description: "Search query or regular expression.",
195
+ },
196
+ path: buildWorkspacePathProperty(),
197
+ },
198
+ required: ["query"],
199
+ },
200
+ },
201
+ };
202
+ }
203
+
204
+ function buildApplyPatchTool(): OpenAiCompatibleTool {
205
+ return {
206
+ type: "function",
207
+ function: {
208
+ name: "applyPatch",
209
+ description: "Apply a unified diff patch to files inside the workspace.",
210
+ parameters: {
211
+ type: "object",
212
+ properties: {
213
+ patch: {
214
+ type: "string",
215
+ description: "Unified diff text accepted by git apply.",
216
+ },
217
+ },
218
+ required: ["patch"],
219
+ },
220
+ },
221
+ };
222
+ }
223
+
224
+ function buildGitStatusTool(): OpenAiCompatibleTool {
225
+ return {
226
+ type: "function",
227
+ function: {
228
+ name: "gitStatus",
229
+ description: "Show concise git status for the workspace.",
230
+ parameters: { type: "object", properties: {} },
231
+ },
232
+ };
233
+ }
234
+
235
+ function buildGitDiffTool(): OpenAiCompatibleTool {
236
+ return {
237
+ type: "function",
238
+ function: {
239
+ name: "gitDiff",
240
+ description: "Show git diff for the workspace.",
241
+ parameters: {
242
+ type: "object",
243
+ properties: {
244
+ path: buildWorkspacePathProperty(),
245
+ staged: {
246
+ type: "boolean",
247
+ description: "When true, show staged changes.",
248
+ },
249
+ },
250
+ },
251
+ },
252
+ };
253
+ }
254
+
255
+ function buildGitCreateBranchTool(): OpenAiCompatibleTool {
256
+ return {
257
+ type: "function",
258
+ function: {
259
+ name: "gitCreateBranch",
260
+ description: "Create and switch to a local git branch in the workspace.",
261
+ parameters: {
262
+ type: "object",
263
+ properties: {
264
+ branch: {
265
+ type: "string",
266
+ description: "Branch name to create and check out.",
267
+ },
268
+ },
269
+ required: ["branch"],
270
+ },
271
+ },
272
+ };
273
+ }
274
+
275
+ function buildGitAddTool(): OpenAiCompatibleTool {
276
+ return {
277
+ type: "function",
278
+ function: {
279
+ name: "gitAdd",
280
+ description: "Stage workspace files for commit.",
281
+ parameters: {
282
+ type: "object",
283
+ properties: {
284
+ paths: {
285
+ type: "array",
286
+ items: { type: "string" },
287
+ description: "Workspace-relative paths to stage.",
288
+ },
289
+ },
290
+ required: ["paths"],
291
+ },
292
+ },
293
+ };
294
+ }
295
+
296
+ function buildGitCommitTool(): OpenAiCompatibleTool {
297
+ return {
298
+ type: "function",
299
+ function: {
300
+ name: "gitCommit",
301
+ description: "Commit staged workspace changes.",
302
+ parameters: {
303
+ type: "object",
304
+ properties: {
305
+ message: {
306
+ type: "string",
307
+ description: "Commit subject.",
308
+ },
309
+ },
310
+ required: ["message"],
311
+ },
312
+ },
313
+ };
314
+ }
315
+
316
+ function buildCommitWorkspaceTool(): OpenAiCompatibleTool {
317
+ return {
318
+ type: "function",
319
+ function: {
320
+ name: "commitWorkspace",
321
+ description: "Stage selected workspace paths, commit them, and return branch, commit hash, and clean status.",
322
+ parameters: {
323
+ type: "object",
324
+ properties: {
325
+ paths: {
326
+ type: "array",
327
+ items: { type: "string" },
328
+ description: "Workspace-relative paths to stage before committing.",
329
+ },
330
+ message: {
331
+ type: "string",
332
+ description: "Commit subject.",
333
+ },
334
+ },
335
+ required: ["paths", "message"],
336
+ },
337
+ },
338
+ };
339
+ }
340
+
341
+ function buildRunPackageScriptTool(): OpenAiCompatibleTool {
342
+ return {
343
+ type: "function",
344
+ function: {
345
+ name: "runPackageScript",
346
+ description: "Run a package script in the workspace, for example a test or lint script.",
347
+ parameters: {
348
+ type: "object",
349
+ properties: {
350
+ script: {
351
+ type: "string",
352
+ description: "Package script name to run.",
353
+ },
354
+ args: {
355
+ type: "array",
356
+ items: { type: "string" },
357
+ description: "Optional extra arguments passed after --.",
358
+ },
359
+ },
360
+ required: ["script"],
361
+ },
362
+ },
363
+ };
364
+ }
365
+
366
+ function buildPreviewLifecycleTool(toolName: string): OpenAiCompatibleTool {
367
+ const descriptions: Record<string, string> = {
368
+ startPreview: "Start the local preview stack for the current workspace.",
369
+ getPreviewStatus: "Read local preview status, including localApiOrigin and process state.",
370
+ stopPreview: "Stop the local preview stack for the current workspace.",
371
+ releasePreview: "Release the local preview slot for the current workspace after stopping preview.",
372
+ };
373
+ return {
374
+ type: "function",
375
+ function: {
376
+ name: toolName,
377
+ description: descriptions[toolName] ?? "Run a local preview lifecycle action.",
378
+ parameters: { type: "object", properties: {} },
379
+ },
380
+ };
381
+ }
382
+
383
+ function buildCaptureVisualStateTool(): OpenAiCompatibleTool {
384
+ return {
385
+ type: "function",
386
+ function: {
387
+ name: "captureVisualState",
388
+ description: "Capture a real local app screenshot and DOM/computed-style metrics for a selected UI state.",
389
+ parameters: {
390
+ type: "object",
391
+ properties: {
392
+ baseUrl: {
393
+ type: "string",
394
+ description: "Optional local preview base URL. When omitted, the tool reads preview:status.",
395
+ },
396
+ path: {
397
+ type: "string",
398
+ description: "App route to open, for example / or /dialog-123. Defaults to /.",
399
+ },
400
+ waitSelector: {
401
+ type: "string",
402
+ description: "CSS selector that must become visible before capture.",
403
+ },
404
+ scrollSelector: {
405
+ type: "string",
406
+ description: "Optional CSS selector to scroll into view before capture.",
407
+ },
408
+ focusSelector: {
409
+ type: "string",
410
+ description: "Optional CSS selector for the target element whose rect/style should be reported.",
411
+ },
412
+ expectText: {
413
+ type: "string",
414
+ description: "Optional visible text expected on the page before capture.",
415
+ },
416
+ screenshotPath: {
417
+ type: "string",
418
+ description: "Workspace-relative screenshot path. Defaults under test-results/frontend-agent/.",
419
+ },
420
+ metricsPath: {
421
+ type: "string",
422
+ description: "Workspace-relative metrics JSON path. Defaults under test-results/frontend-agent/.",
423
+ },
424
+ },
425
+ required: ["waitSelector"],
426
+ },
427
+ },
428
+ };
429
+ }
430
+
431
+ function buildExecShellTool(toolName: string): OpenAiCompatibleTool {
432
+ return {
433
+ type: "function",
434
+ function: {
435
+ name: toolName,
436
+ description: "Run a shell command in the local workspace.",
437
+ parameters: {
438
+ type: "object",
439
+ properties: {
440
+ cmd: {
441
+ type: "string",
442
+ description: "Shell command to run.",
443
+ },
444
+ command: {
445
+ type: "string",
446
+ description: "Shell command to run.",
447
+ },
448
+ },
449
+ },
450
+ },
451
+ };
452
+ }
453
+
454
+ function renameWorkspaceToolDefinition(
455
+ tool: OpenAiCompatibleTool,
456
+ name: string
457
+ ): OpenAiCompatibleTool {
458
+ return {
459
+ ...tool,
460
+ function: {
461
+ ...(tool.function ?? {}),
462
+ name,
463
+ },
464
+ };
465
+ }
466
+
467
+ function buildWorkspaceToolDefinition(toolName: string) {
468
+ if (toolName === "listWorkspaceFiles" || toolName === "listFiles") {
469
+ return renameWorkspaceToolDefinition(buildListWorkspaceFilesTool(), toolName);
470
+ }
471
+ if (toolName === "readWorkspaceFile" || toolName === "readFile") {
472
+ return renameWorkspaceToolDefinition(buildReadWorkspaceFileTool(), toolName);
473
+ }
474
+ if (toolName === "writeWorkspaceFile" || toolName === "writeFile") {
475
+ return renameWorkspaceToolDefinition(buildWriteWorkspaceFileTool(), toolName);
476
+ }
477
+ if (toolName === "replaceWorkspaceText" || toolName === "applyEdit" || toolName === "applyLineEdits") {
478
+ return renameWorkspaceToolDefinition(buildReplaceWorkspaceTextTool(), toolName);
479
+ }
480
+ if (toolName === "searchWorkspace") return buildSearchWorkspaceTool();
481
+ if (toolName === "applyPatch") return buildApplyPatchTool();
482
+ if (toolName === "gitStatus") return buildGitStatusTool();
483
+ if (toolName === "gitDiff") return buildGitDiffTool();
484
+ if (toolName === "gitCreateBranch") return buildGitCreateBranchTool();
485
+ if (toolName === "gitAdd") return buildGitAddTool();
486
+ if (toolName === "gitCommit") return buildGitCommitTool();
487
+ if (toolName === "commitWorkspace") return buildCommitWorkspaceTool();
488
+ if (toolName === "runPackageScript") return buildRunPackageScriptTool();
489
+ if (toolName === "startPreview" || toolName === "getPreviewStatus" || toolName === "stopPreview" || toolName === "releasePreview") {
490
+ return buildPreviewLifecycleTool(toolName);
491
+ }
492
+ if (toolName === "captureVisualState") return buildCaptureVisualStateTool();
493
+ if (toolName === "execShell" || toolName === "execBash") return buildExecShellTool(toolName);
494
+ return null;
495
+ }
496
+
497
+ function removeRedundantWorkspaceToolAliases(toolNames: Set<string>) {
498
+ const next = new Set(toolNames);
499
+ if (next.has("listWorkspaceFiles")) next.delete("listFiles");
500
+ if (next.has("readWorkspaceFile")) next.delete("readFile");
501
+ if (next.has("writeWorkspaceFile")) next.delete("writeFile");
502
+ if (next.has("replaceWorkspaceText")) {
503
+ next.delete("applyEdit");
504
+ next.delete("applyLineEdits");
505
+ }
506
+ return next;
507
+ }
508
+
509
+ export function buildLocalWorkspaceToolset(args: {
510
+ declaredToolNames?: string[];
511
+ exposeShellTools?: boolean;
512
+ }) {
513
+ const exposeShellTools = args.exposeShellTools === true;
514
+ return {
515
+ toolNames: [
516
+ ...DEFAULT_LOCAL_CODING_TOOL_NAMES,
517
+ ...(exposeShellTools ? SHELL_TOOL_NAMES : []),
518
+ ],
519
+ exposeShellTools,
520
+ };
521
+ }
522
+
523
+ export function buildLocalWorkspacePolicyToolNames(args: {
524
+ declaredToolNames?: string[];
525
+ exposeShellTools?: boolean;
526
+ }) {
527
+ return [...new Set([
528
+ ...buildLocalWorkspaceToolset(args).toolNames,
529
+ ...(args.declaredToolNames ?? []),
530
+ ])];
531
+ }
532
+
533
+ export function buildLocalWorkspaceOpenAiTools(args: {
534
+ toolNames?: string[];
535
+ exposeShellTools?: boolean;
536
+ }) {
537
+ const declaredTools = removeRedundantWorkspaceToolAliases(new Set(args.toolNames ?? []));
538
+ return WORKSPACE_TOOL_NAMES
539
+ .filter((toolName) => {
540
+ if (!declaredTools.has(toolName)) return false;
541
+ if (!args.exposeShellTools && (toolName === "execShell" || toolName === "execBash")) {
542
+ return false;
543
+ }
544
+ return true;
545
+ })
546
+ .map((toolName) => buildWorkspaceToolDefinition(toolName))
547
+ .filter((tool): tool is OpenAiCompatibleTool => Boolean(tool));
548
+ }
549
+
550
+ function parseWorkspaceToolArguments(raw: string): WorkspaceFileArgs {
551
+ try {
552
+ const parsed = JSON.parse(raw || "{}");
553
+ return parsed && typeof parsed === "object" ? parsed as WorkspaceFileArgs : {};
554
+ } catch {
555
+ return {};
556
+ }
557
+ }
558
+
559
+ function normalizeWorkspaceRelativePath(args: {
560
+ workspaceRoot: string;
561
+ targetPath: string;
562
+ }) {
563
+ const relativePath = relative(args.workspaceRoot, args.targetPath);
564
+ return relativePath || ".";
565
+ }
566
+
567
+ function isPathInsideWorkspace(args: {
568
+ workspaceRoot: string;
569
+ targetPath: string;
570
+ }) {
571
+ const relativePath = relative(args.workspaceRoot, args.targetPath);
572
+ return relativePath === "" || (!relativePath.startsWith("..") && !relativePath.includes(`..${sep}`));
573
+ }
574
+
575
+ function requireWorkspaceToolPath(args: WorkspaceFileArgs) {
576
+ const requestedPath = typeof args.path === "string" ? args.path.trim() : "";
577
+ if (!requestedPath) throw new Error("Workspace tool requires a non-empty path.");
578
+ return requestedPath;
579
+ }
580
+
581
+ function requireWorkspaceFileContent(args: WorkspaceFileArgs) {
582
+ if (typeof args.content !== "string") {
583
+ throw new Error("writeWorkspaceFile requires string content.");
584
+ }
585
+ return args.content;
586
+ }
587
+
588
+ function requireWorkspaceOldText(args: WorkspaceFileArgs) {
589
+ if (typeof args.oldText !== "string" || !args.oldText) {
590
+ throw new Error("replaceWorkspaceText requires non-empty oldText.");
591
+ }
592
+ return args.oldText;
593
+ }
594
+
595
+ function requireWorkspaceNewText(args: WorkspaceFileArgs) {
596
+ if (typeof args.newText !== "string") {
597
+ throw new Error("replaceWorkspaceText requires string newText.");
598
+ }
599
+ return args.newText;
600
+ }
601
+
602
+ function readExpectedReplacementCount(args: WorkspaceFileArgs) {
603
+ if (args.expectedReplacements === undefined) return 1;
604
+ const value = Number(args.expectedReplacements);
605
+ if (!Number.isInteger(value) || value < 1) {
606
+ throw new Error("replaceWorkspaceText expectedReplacements must be a positive integer.");
607
+ }
608
+ return value;
609
+ }
610
+
611
+ function countExactTextOccurrences(args: {
612
+ content: string;
613
+ oldText: string;
614
+ }) {
615
+ return args.content.split(args.oldText).length - 1;
616
+ }
617
+
618
+ function pluralizeReplacement(count: number) {
619
+ return count === 1 ? "replacement" : "replacements";
620
+ }
621
+
622
+ function requireWorkspaceSearchQuery(args: WorkspaceFileArgs) {
623
+ const query = typeof args.query === "string" ? args.query.trim() : "";
624
+ if (!query) throw new Error("searchWorkspace requires a non-empty query.");
625
+ return query;
626
+ }
627
+
628
+ function requireWorkspacePatch(args: WorkspaceFileArgs) {
629
+ const patch = typeof args.patch === "string" ? args.patch : "";
630
+ if (!patch.trim()) throw new Error("applyPatch requires a non-empty patch.");
631
+ return patch;
632
+ }
633
+
634
+ function requireShellCommand(args: WorkspaceFileArgs, toolName: string) {
635
+ const command = typeof args.cmd === "string"
636
+ ? args.cmd.trim()
637
+ : typeof args.command === "string"
638
+ ? args.command.trim()
639
+ : "";
640
+ if (!command) throw new Error(`${toolName} requires a non-empty command.`);
641
+ return command;
642
+ }
643
+
644
+ function requirePackageScript(args: WorkspaceFileArgs) {
645
+ const script = typeof args.script === "string" ? args.script.trim() : "";
646
+ if (!script) throw new Error("runPackageScript requires a non-empty script.");
647
+ return script;
648
+ }
649
+
650
+ function requireVisualWaitSelector(args: WorkspaceFileArgs) {
651
+ const selector = typeof args.waitSelector === "string" ? args.waitSelector.trim() : "";
652
+ if (!selector) throw new Error("captureVisualState requires a non-empty waitSelector.");
653
+ return selector;
654
+ }
655
+
656
+ function requireGitBranchName(args: WorkspaceFileArgs) {
657
+ const branch = typeof args.branch === "string" ? args.branch.trim() : "";
658
+ if (!branch) throw new Error("gitCreateBranch requires a non-empty branch.");
659
+ return branch;
660
+ }
661
+
662
+ function requireGitCommitMessage(args: WorkspaceFileArgs) {
663
+ const message = typeof args.message === "string" ? args.message.trim() : "";
664
+ if (!message) throw new Error("gitCommit requires a non-empty message.");
665
+ return message;
666
+ }
667
+
668
+ function readGitAddPaths(args: WorkspaceFileArgs) {
669
+ const paths = Array.isArray(args.paths)
670
+ ? args.paths.map((value) => String(value).trim()).filter(Boolean)
671
+ : [];
672
+ if (paths.length === 0) throw new Error("gitAdd requires at least one path.");
673
+ return paths;
674
+ }
675
+
676
+ function readPackageScriptArgs(args: WorkspaceFileArgs) {
677
+ return Array.isArray(args.args)
678
+ ? args.args.map((value) => String(value)).filter(Boolean)
679
+ : [];
680
+ }
681
+
682
+ function readOptionalStringArg(value: unknown) {
683
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
684
+ }
685
+
686
+ function readVisualStateCaptureArgs(args: WorkspaceFileArgs) {
687
+ const baseUrl = readOptionalStringArg(args.baseUrl) ?? readOptionalStringArg(args.base);
688
+ const path = readOptionalStringArg(args.path) ?? "/";
689
+ const waitSelector = requireVisualWaitSelector(args);
690
+ const scrollSelector = readOptionalStringArg(args.scrollSelector);
691
+ const focusSelector = readOptionalStringArg(args.focusSelector);
692
+ const expectText = readOptionalStringArg(args.expectText);
693
+ const screenshotPath =
694
+ readOptionalStringArg(args.screenshotPath) ?? "test-results/frontend-agent/visual-state.png";
695
+ const metricsPath =
696
+ readOptionalStringArg(args.metricsPath) ?? "test-results/frontend-agent/visual-state-metrics.json";
697
+ return {
698
+ baseUrl,
699
+ path,
700
+ waitSelector,
701
+ scrollSelector,
702
+ focusSelector,
703
+ expectText,
704
+ screenshotPath,
705
+ metricsPath,
706
+ };
707
+ }
708
+
709
+ async function readWorkspacePackageScripts(workspaceRoot: string) {
710
+ try {
711
+ const raw = await readFile(resolve(workspaceRoot, "package.json"), "utf8");
712
+ const parsed = JSON.parse(raw);
713
+ const scripts = parsed && typeof parsed === "object" && parsed.scripts && typeof parsed.scripts === "object"
714
+ ? Object.keys(parsed.scripts).sort()
715
+ : [];
716
+ return { scripts };
717
+ } catch (error) {
718
+ return {
719
+ scripts: [],
720
+ error: error instanceof Error ? error.message : String(error),
721
+ };
722
+ }
723
+ }
724
+
725
+ function formatAvailableScripts(scripts: string[]) {
726
+ return scripts.length ? scripts.join(", ") : "(none)";
727
+ }
728
+
729
+ function truncateToolOutput(value: string, limit = 20_000) {
730
+ if (value.length <= limit) return value;
731
+ return `${value.slice(0, limit)}\n\n[truncated ${value.length - limit} chars]`;
732
+ }
733
+
734
+ function parseLastJsonObject(text: string): Record<string, unknown> | null {
735
+ const trimmed = text.trim();
736
+ for (let index = trimmed.lastIndexOf("{"); index >= 0; index = trimmed.lastIndexOf("{", index - 1)) {
737
+ try {
738
+ const parsed = JSON.parse(trimmed.slice(index));
739
+ return parsed && typeof parsed === "object" ? parsed as Record<string, unknown> : null;
740
+ } catch {
741
+ // Try the previous opening brace.
742
+ }
743
+ }
744
+ return null;
745
+ }
746
+
747
+ async function writeProcessInput(proc: any, input: string) {
748
+ const stdin = proc.stdin;
749
+ if (!stdin) return;
750
+ const encoded = new TextEncoder().encode(input);
751
+ if (typeof stdin.getWriter === "function") {
752
+ const writer = stdin.getWriter();
753
+ try {
754
+ await writer.write(encoded);
755
+ await writer.close();
756
+ } finally {
757
+ writer.releaseLock();
758
+ }
759
+ return;
760
+ }
761
+ if (typeof stdin.write === "function") {
762
+ await stdin.write(encoded);
763
+ await stdin.end?.();
764
+ }
765
+ }
766
+
767
+ async function runWorkspaceCommand(args: {
768
+ workspaceRoot: string;
769
+ command: string[];
770
+ stdin?: string;
771
+ timeoutMs?: number;
772
+ }) {
773
+ const timeoutMs = args.timeoutMs ?? DEFAULT_WORKSPACE_COMMAND_TIMEOUT_MS;
774
+ const detached = process.platform !== "win32";
775
+ const proc = Bun.spawn(args.command, {
776
+ cwd: resolve(args.workspaceRoot),
777
+ stdout: "pipe",
778
+ stderr: "pipe",
779
+ stdin: args.stdin === undefined ? "ignore" : "pipe",
780
+ detached,
781
+ });
782
+ if (args.stdin !== undefined) await writeProcessInput(proc, args.stdin);
783
+ const stdoutPromise = new Response(proc.stdout).text();
784
+ const stderrPromise = new Response(proc.stderr).text();
785
+ let timeout: ReturnType<typeof setTimeout> | undefined;
786
+ const timeoutResult = Symbol("timeout");
787
+ const exitOrTimeout = await Promise.race([
788
+ proc.exited,
789
+ new Promise<typeof timeoutResult>((resolveTimeout) => {
790
+ timeout = setTimeout(() => resolveTimeout(timeoutResult), timeoutMs);
791
+ }),
792
+ ]);
793
+ if (timeout) clearTimeout(timeout);
794
+ const timedOut = exitOrTimeout === timeoutResult;
795
+ if (timedOut) {
796
+ const kill = (signal: NodeJS.Signals) => {
797
+ if (detached && typeof proc.pid === "number") {
798
+ try {
799
+ process.kill(-proc.pid, signal);
800
+ return;
801
+ } catch {
802
+ // Fall through to killing the immediate child.
803
+ }
804
+ }
805
+ try {
806
+ proc.kill(signal);
807
+ } catch {
808
+ // The command may have exited after the timeout won the race.
809
+ }
810
+ };
811
+ kill("SIGTERM");
812
+ await Promise.race([
813
+ proc.exited.catch(() => 124),
814
+ new Promise((resolveKill) => setTimeout(resolveKill, 500)),
815
+ ]);
816
+ kill("SIGKILL");
817
+ }
818
+ const [stdout, rawStderr] = await Promise.all([
819
+ stdoutPromise,
820
+ stderrPromise,
821
+ ]);
822
+ const exitCode = timedOut ? 124 : Number(exitOrTimeout);
823
+ const stderr = timedOut
824
+ ? `${rawStderr.trim() ? `${rawStderr.trim()}\n` : ""}command timed out after ${timeoutMs}ms\n`
825
+ : rawStderr;
826
+ return {
827
+ stdout,
828
+ stderr,
829
+ exitCode,
830
+ timedOut,
831
+ content: truncateToolOutput([
832
+ stdout.trim() ? `stdout:\n${stdout.trim()}` : "",
833
+ stderr.trim() ? `stderr:\n${stderr.trim()}` : "",
834
+ `exitCode: ${exitCode}`,
835
+ ].filter(Boolean).join("\n\n")),
836
+ };
837
+ }
838
+
839
+ export function resolveLocalWorkspaceToolPath(args: {
840
+ workspaceRoot: string;
841
+ requestedPath: string;
842
+ }) {
843
+ const workspaceRoot = resolve(args.workspaceRoot);
844
+ const targetPath = resolve(workspaceRoot, args.requestedPath);
845
+ if (!isPathInsideWorkspace({ workspaceRoot, targetPath })) {
846
+ throw new Error(`Workspace tool path escapes workspace root: ${args.requestedPath}`);
847
+ }
848
+ return targetPath;
849
+ }
850
+
851
+ async function readWorkspaceFileTool(args: {
852
+ call: AgentRuntimeToolCallInput;
853
+ workspaceRoot: string;
854
+ }): Promise<AgentRuntimeToolResult> {
855
+ const parsed = parseWorkspaceToolArguments(args.call.arguments);
856
+ const requestedPath = requireWorkspaceToolPath(parsed);
857
+ const absolutePath = resolveLocalWorkspaceToolPath({
858
+ workspaceRoot: args.workspaceRoot,
859
+ requestedPath,
860
+ });
861
+ const content = await readFile(absolutePath, "utf8");
862
+ return {
863
+ content,
864
+ metadata: {
865
+ path: normalizeWorkspaceRelativePath({
866
+ workspaceRoot: resolve(args.workspaceRoot),
867
+ targetPath: absolutePath,
868
+ }),
869
+ bytes: Buffer.byteLength(content),
870
+ },
871
+ };
872
+ }
873
+
874
+ async function writeWorkspaceFileTool(args: {
875
+ call: AgentRuntimeToolCallInput;
876
+ workspaceRoot: string;
877
+ }): Promise<AgentRuntimeToolResult> {
878
+ const parsed = parseWorkspaceToolArguments(args.call.arguments);
879
+ const requestedPath = requireWorkspaceToolPath(parsed);
880
+ const content = requireWorkspaceFileContent(parsed);
881
+ const absolutePath = resolveLocalWorkspaceToolPath({
882
+ workspaceRoot: args.workspaceRoot,
883
+ requestedPath,
884
+ });
885
+ await mkdir(dirname(absolutePath), { recursive: true });
886
+ await writeFile(absolutePath, content, "utf8");
887
+ const relativePath = normalizeWorkspaceRelativePath({
888
+ workspaceRoot: resolve(args.workspaceRoot),
889
+ targetPath: absolutePath,
890
+ });
891
+ return {
892
+ content: `wrote ${relativePath}`,
893
+ metadata: {
894
+ path: relativePath,
895
+ bytes: Buffer.byteLength(content),
896
+ },
897
+ };
898
+ }
899
+
900
+ async function replaceWorkspaceTextTool(args: {
901
+ call: AgentRuntimeToolCallInput;
902
+ workspaceRoot: string;
903
+ }): Promise<AgentRuntimeToolResult> {
904
+ const parsed = parseWorkspaceToolArguments(args.call.arguments);
905
+ const requestedPath = requireWorkspaceToolPath(parsed);
906
+ const oldText = requireWorkspaceOldText(parsed);
907
+ const newText = requireWorkspaceNewText(parsed);
908
+ const expectedReplacements = readExpectedReplacementCount(parsed);
909
+ const absolutePath = resolveLocalWorkspaceToolPath({
910
+ workspaceRoot: args.workspaceRoot,
911
+ requestedPath,
912
+ });
913
+ const content = await readFile(absolutePath, "utf8");
914
+ const replacementCount = countExactTextOccurrences({ content, oldText });
915
+ if (replacementCount !== expectedReplacements) {
916
+ throw new Error(
917
+ `replaceWorkspaceText expected ${expectedReplacements} ${pluralizeReplacement(expectedReplacements)} ` +
918
+ `but found ${replacementCount} in ${requestedPath}.`
919
+ );
920
+ }
921
+ const nextContent = content.split(oldText).join(newText);
922
+ await writeFile(absolutePath, nextContent, "utf8");
923
+ const relativePath = normalizeWorkspaceRelativePath({
924
+ workspaceRoot: resolve(args.workspaceRoot),
925
+ targetPath: absolutePath,
926
+ });
927
+ return {
928
+ content: `replaced ${replacementCount} occurrence${replacementCount === 1 ? "" : "s"} in ${relativePath}`,
929
+ metadata: {
930
+ path: relativePath,
931
+ replacements: replacementCount,
932
+ bytes: Buffer.byteLength(nextContent),
933
+ },
934
+ };
935
+ }
936
+
937
+ async function formatWorkspaceDirEntry(args: {
938
+ workspaceRoot: string;
939
+ dirPath: string;
940
+ name: string;
941
+ }) {
942
+ const absolutePath = resolve(args.dirPath, args.name);
943
+ const info = await stat(absolutePath);
944
+ const relativePath = normalizeWorkspaceRelativePath({
945
+ workspaceRoot: resolve(args.workspaceRoot),
946
+ targetPath: absolutePath,
947
+ });
948
+ return info.isDirectory() ? `${relativePath}/` : relativePath;
949
+ }
950
+
951
+ async function listWorkspaceFilesTool(args: {
952
+ call: AgentRuntimeToolCallInput;
953
+ workspaceRoot: string;
954
+ }): Promise<AgentRuntimeToolResult> {
955
+ const parsed = parseWorkspaceToolArguments(args.call.arguments);
956
+ const requestedPath = typeof parsed.path === "string" && parsed.path.trim()
957
+ ? parsed.path.trim()
958
+ : ".";
959
+ const dirPath = resolveLocalWorkspaceToolPath({
960
+ workspaceRoot: args.workspaceRoot,
961
+ requestedPath,
962
+ });
963
+ const names = (await readdir(dirPath)).sort((left, right) => left.localeCompare(right));
964
+ const entries = await Promise.all(names.map((name) => formatWorkspaceDirEntry({
965
+ workspaceRoot: args.workspaceRoot,
966
+ dirPath,
967
+ name,
968
+ })));
969
+ return {
970
+ content: entries.join("\n"),
971
+ metadata: {
972
+ path: requestedPath,
973
+ count: entries.length,
974
+ },
975
+ };
976
+ }
977
+
978
+ async function searchWorkspaceTool(args: {
979
+ call: AgentRuntimeToolCallInput;
980
+ workspaceRoot: string;
981
+ }): Promise<AgentRuntimeToolResult> {
982
+ const parsed = parseWorkspaceToolArguments(args.call.arguments);
983
+ const query = requireWorkspaceSearchQuery(parsed);
984
+ const requestedPath = typeof parsed.path === "string" && parsed.path.trim()
985
+ ? parsed.path.trim()
986
+ : ".";
987
+ const searchPath = resolveLocalWorkspaceToolPath({
988
+ workspaceRoot: args.workspaceRoot,
989
+ requestedPath,
990
+ });
991
+ const relativeSearchPath = normalizeWorkspaceRelativePath({
992
+ workspaceRoot: resolve(args.workspaceRoot),
993
+ targetPath: searchPath,
994
+ });
995
+ const result = await (async () => {
996
+ try {
997
+ return await runWorkspaceCommand({
998
+ workspaceRoot: args.workspaceRoot,
999
+ command: [
1000
+ "rg",
1001
+ "--line-number",
1002
+ "--no-heading",
1003
+ "--hidden",
1004
+ "--glob",
1005
+ "!node_modules",
1006
+ "--glob",
1007
+ "!.git",
1008
+ query,
1009
+ relativeSearchPath,
1010
+ ],
1011
+ });
1012
+ } catch {
1013
+ return await runWorkspaceCommand({
1014
+ workspaceRoot: args.workspaceRoot,
1015
+ command: [
1016
+ "grep",
1017
+ "-R",
1018
+ "-n",
1019
+ "-I",
1020
+ "--exclude-dir=node_modules",
1021
+ "--exclude-dir=.git",
1022
+ query,
1023
+ relativeSearchPath,
1024
+ ],
1025
+ });
1026
+ }
1027
+ })();
1028
+ return {
1029
+ content: result.content,
1030
+ metadata: {
1031
+ query,
1032
+ path: requestedPath,
1033
+ exitCode: result.exitCode,
1034
+ },
1035
+ };
1036
+ }
1037
+
1038
+ async function applyPatchTool(args: {
1039
+ call: AgentRuntimeToolCallInput;
1040
+ workspaceRoot: string;
1041
+ }): Promise<AgentRuntimeToolResult> {
1042
+ const parsed = parseWorkspaceToolArguments(args.call.arguments);
1043
+ const patch = requireWorkspacePatch(parsed);
1044
+ const checkResult = await runWorkspaceCommand({
1045
+ workspaceRoot: args.workspaceRoot,
1046
+ command: ["git", "apply", "--check", "--whitespace=nowarn", "-"],
1047
+ stdin: patch,
1048
+ });
1049
+ if (checkResult.exitCode !== 0) {
1050
+ throw new Error(
1051
+ [
1052
+ "applyPatch failed during git apply --check.",
1053
+ "Use replaceWorkspaceText/applyEdit for exact single-file edits when possible.",
1054
+ checkResult.content,
1055
+ ].join("\n\n")
1056
+ );
1057
+ }
1058
+ const result = await runWorkspaceCommand({
1059
+ workspaceRoot: args.workspaceRoot,
1060
+ command: ["git", "apply", "--whitespace=nowarn", "-"],
1061
+ stdin: patch,
1062
+ });
1063
+ if (result.exitCode !== 0) throw new Error(result.content);
1064
+ return {
1065
+ content: "patch applied",
1066
+ metadata: { bytes: Buffer.byteLength(patch) },
1067
+ };
1068
+ }
1069
+
1070
+ async function gitStatusTool(args: {
1071
+ workspaceRoot: string;
1072
+ }): Promise<AgentRuntimeToolResult> {
1073
+ const result = await runWorkspaceCommand({
1074
+ workspaceRoot: args.workspaceRoot,
1075
+ command: ["git", "status", "--short"],
1076
+ });
1077
+ return {
1078
+ content: result.content,
1079
+ metadata: { exitCode: result.exitCode },
1080
+ };
1081
+ }
1082
+
1083
+ async function gitDiffTool(args: {
1084
+ call: AgentRuntimeToolCallInput;
1085
+ workspaceRoot: string;
1086
+ }): Promise<AgentRuntimeToolResult> {
1087
+ const parsed = parseWorkspaceToolArguments(args.call.arguments);
1088
+ const command = ["git", "diff"];
1089
+ const staged = parsed.staged === true;
1090
+ if (staged) command.push("--staged");
1091
+ if (typeof parsed.path === "string" && parsed.path.trim()) {
1092
+ const absolutePath = resolveLocalWorkspaceToolPath({
1093
+ workspaceRoot: args.workspaceRoot,
1094
+ requestedPath: parsed.path.trim(),
1095
+ });
1096
+ command.push("--", normalizeWorkspaceRelativePath({
1097
+ workspaceRoot: resolve(args.workspaceRoot),
1098
+ targetPath: absolutePath,
1099
+ }));
1100
+ }
1101
+ const result = await runWorkspaceCommand({
1102
+ workspaceRoot: args.workspaceRoot,
1103
+ command,
1104
+ });
1105
+ return {
1106
+ content: result.content,
1107
+ metadata: { staged, exitCode: result.exitCode },
1108
+ };
1109
+ }
1110
+
1111
+ async function gitCreateBranchTool(args: {
1112
+ call: AgentRuntimeToolCallInput;
1113
+ workspaceRoot: string;
1114
+ }): Promise<AgentRuntimeToolResult> {
1115
+ const parsed = parseWorkspaceToolArguments(args.call.arguments);
1116
+ const branch = requireGitBranchName(parsed);
1117
+ const result = await runWorkspaceCommand({
1118
+ workspaceRoot: args.workspaceRoot,
1119
+ command: ["git", "switch", "-c", branch],
1120
+ });
1121
+ if (result.exitCode !== 0) throw new Error(result.content);
1122
+ return {
1123
+ content: result.content,
1124
+ metadata: { branch, exitCode: result.exitCode },
1125
+ };
1126
+ }
1127
+
1128
+ async function gitAddTool(args: {
1129
+ call: AgentRuntimeToolCallInput;
1130
+ workspaceRoot: string;
1131
+ }): Promise<AgentRuntimeToolResult> {
1132
+ const parsed = parseWorkspaceToolArguments(args.call.arguments);
1133
+ const paths = readGitAddPaths(parsed).map((requestedPath) => {
1134
+ const absolutePath = resolveLocalWorkspaceToolPath({
1135
+ workspaceRoot: args.workspaceRoot,
1136
+ requestedPath,
1137
+ });
1138
+ return normalizeWorkspaceRelativePath({
1139
+ workspaceRoot: resolve(args.workspaceRoot),
1140
+ targetPath: absolutePath,
1141
+ });
1142
+ });
1143
+ const result = await runWorkspaceCommand({
1144
+ workspaceRoot: args.workspaceRoot,
1145
+ command: ["git", "add", "--", ...paths],
1146
+ });
1147
+ if (result.exitCode !== 0) throw new Error(result.content);
1148
+ return {
1149
+ content: result.content,
1150
+ metadata: { paths, exitCode: result.exitCode },
1151
+ };
1152
+ }
1153
+
1154
+ async function gitCommitTool(args: {
1155
+ call: AgentRuntimeToolCallInput;
1156
+ workspaceRoot: string;
1157
+ }): Promise<AgentRuntimeToolResult> {
1158
+ const parsed = parseWorkspaceToolArguments(args.call.arguments);
1159
+ const message = requireGitCommitMessage(parsed);
1160
+ const result = await runWorkspaceCommand({
1161
+ workspaceRoot: args.workspaceRoot,
1162
+ command: ["git", "commit", "-m", message],
1163
+ });
1164
+ if (result.exitCode !== 0) throw new Error(result.content);
1165
+ return {
1166
+ content: result.content,
1167
+ metadata: { exitCode: result.exitCode },
1168
+ };
1169
+ }
1170
+
1171
+ async function commitWorkspaceTool(args: {
1172
+ call: AgentRuntimeToolCallInput;
1173
+ workspaceRoot: string;
1174
+ }): Promise<AgentRuntimeToolResult> {
1175
+ const parsed = parseWorkspaceToolArguments(args.call.arguments);
1176
+ const paths = readGitAddPaths(parsed).map((requestedPath) => {
1177
+ const absolutePath = resolveLocalWorkspaceToolPath({
1178
+ workspaceRoot: args.workspaceRoot,
1179
+ requestedPath,
1180
+ });
1181
+ return normalizeWorkspaceRelativePath({
1182
+ workspaceRoot: resolve(args.workspaceRoot),
1183
+ targetPath: absolutePath,
1184
+ });
1185
+ });
1186
+ const message = requireGitCommitMessage(parsed);
1187
+ const addResult = await runWorkspaceCommand({
1188
+ workspaceRoot: args.workspaceRoot,
1189
+ command: ["git", "add", "--", ...paths],
1190
+ });
1191
+ if (addResult.exitCode !== 0) throw new Error(addResult.content);
1192
+
1193
+ const commitResult = await runWorkspaceCommand({
1194
+ workspaceRoot: args.workspaceRoot,
1195
+ command: ["git", "commit", "-m", message],
1196
+ });
1197
+ if (commitResult.exitCode !== 0) throw new Error(commitResult.content);
1198
+
1199
+ const [branchResult, hashResult, statusResult] = await Promise.all([
1200
+ runWorkspaceCommand({
1201
+ workspaceRoot: args.workspaceRoot,
1202
+ command: ["git", "branch", "--show-current"],
1203
+ }),
1204
+ runWorkspaceCommand({
1205
+ workspaceRoot: args.workspaceRoot,
1206
+ command: ["git", "rev-parse", "HEAD"],
1207
+ }),
1208
+ runWorkspaceCommand({
1209
+ workspaceRoot: args.workspaceRoot,
1210
+ command: ["git", "status", "--short"],
1211
+ }),
1212
+ ]);
1213
+ const branch = branchResult.stdout.trim();
1214
+ const commitHash = hashResult.stdout.trim();
1215
+ const status = statusResult.stdout.trim();
1216
+ return {
1217
+ content: [
1218
+ `branch: ${branch}`,
1219
+ `commit: ${commitHash}`,
1220
+ `status: ${status || "clean"}`,
1221
+ "",
1222
+ commitResult.content,
1223
+ ].join("\n"),
1224
+ metadata: {
1225
+ paths,
1226
+ message,
1227
+ branch,
1228
+ commitHash,
1229
+ status,
1230
+ clean: status === "",
1231
+ exitCode: commitResult.exitCode,
1232
+ },
1233
+ };
1234
+ }
1235
+
1236
+ async function runPackageScriptTool(args: {
1237
+ call: AgentRuntimeToolCallInput;
1238
+ workspaceRoot: string;
1239
+ commandTimeoutMs?: number;
1240
+ }): Promise<AgentRuntimeToolResult> {
1241
+ const parsed = parseWorkspaceToolArguments(args.call.arguments);
1242
+ const script = requirePackageScript(parsed);
1243
+ const extraArgs = readPackageScriptArgs(parsed);
1244
+ const packageScripts = await readWorkspacePackageScripts(args.workspaceRoot);
1245
+ if (!packageScripts.scripts.includes(script)) {
1246
+ return {
1247
+ content: [
1248
+ `script not found: ${script}`,
1249
+ `available scripts: ${formatAvailableScripts(packageScripts.scripts)}`,
1250
+ ...(packageScripts.error ? [`package.json read error: ${packageScripts.error}`] : []),
1251
+ ].join("\n"),
1252
+ metadata: {
1253
+ script,
1254
+ exitCode: 1,
1255
+ reason: "script-not-found",
1256
+ availableScripts: packageScripts.scripts,
1257
+ ...(packageScripts.error ? { packageJsonError: packageScripts.error } : {}),
1258
+ },
1259
+ };
1260
+ }
1261
+ const result = await runWorkspaceCommand({
1262
+ workspaceRoot: args.workspaceRoot,
1263
+ command: ["bun", "run", script, ...(extraArgs.length ? ["--", ...extraArgs] : [])],
1264
+ timeoutMs: args.commandTimeoutMs,
1265
+ });
1266
+ return {
1267
+ content: result.content,
1268
+ metadata: {
1269
+ script,
1270
+ args: extraArgs,
1271
+ exitCode: result.exitCode,
1272
+ reason: result.exitCode === 0 ? "ok" : "script-failed",
1273
+ timedOut: result.timedOut,
1274
+ stdoutTail: result.stdout.trim().slice(-4000),
1275
+ stderrTail: result.stderr.trim().slice(-4000),
1276
+ },
1277
+ };
1278
+ }
1279
+
1280
+ async function resolveVisualStateBaseUrl(args: {
1281
+ workspaceRoot: string;
1282
+ explicitBaseUrl?: string;
1283
+ commandTimeoutMs?: number;
1284
+ }) {
1285
+ if (args.explicitBaseUrl) return args.explicitBaseUrl;
1286
+ const statusResult = await runWorkspaceCommand({
1287
+ workspaceRoot: args.workspaceRoot,
1288
+ command: ["bun", "run", "preview:status"],
1289
+ timeoutMs: args.commandTimeoutMs,
1290
+ });
1291
+ if (statusResult.exitCode !== 0) throw new Error(statusResult.content);
1292
+ const status = parseLastJsonObject(statusResult.stdout);
1293
+ const localApiOrigin = typeof status?.localApiOrigin === "string" ? status.localApiOrigin : "";
1294
+ if (!localApiOrigin) {
1295
+ throw new Error("captureVisualState could not read localApiOrigin from preview:status.");
1296
+ }
1297
+ return localApiOrigin;
1298
+ }
1299
+
1300
+ async function captureVisualStateTool(args: {
1301
+ call: AgentRuntimeToolCallInput;
1302
+ workspaceRoot: string;
1303
+ commandTimeoutMs?: number;
1304
+ }): Promise<AgentRuntimeToolResult> {
1305
+ const parsed = parseWorkspaceToolArguments(args.call.arguments);
1306
+ const captureArgs = readVisualStateCaptureArgs(parsed);
1307
+ const baseUrl = await resolveVisualStateBaseUrl({
1308
+ workspaceRoot: args.workspaceRoot,
1309
+ explicitBaseUrl: captureArgs.baseUrl,
1310
+ commandTimeoutMs: args.commandTimeoutMs,
1311
+ });
1312
+ const extraArgs = [
1313
+ "--base",
1314
+ baseUrl,
1315
+ "--path",
1316
+ captureArgs.path,
1317
+ "--wait-selector",
1318
+ captureArgs.waitSelector,
1319
+ ...(captureArgs.scrollSelector ? ["--scroll-selector", captureArgs.scrollSelector] : []),
1320
+ ...(captureArgs.focusSelector ? ["--focus-selector", captureArgs.focusSelector] : []),
1321
+ ...(captureArgs.expectText ? ["--expect-text", captureArgs.expectText] : []),
1322
+ "--screenshot",
1323
+ captureArgs.screenshotPath,
1324
+ "--metrics",
1325
+ captureArgs.metricsPath,
1326
+ ];
1327
+ const result = await runPackageScriptTool({
1328
+ call: {
1329
+ id: args.call.id,
1330
+ name: "runPackageScript",
1331
+ arguments: JSON.stringify({
1332
+ script: "probe:visual-review",
1333
+ args: extraArgs,
1334
+ }),
1335
+ },
1336
+ workspaceRoot: args.workspaceRoot,
1337
+ commandTimeoutMs: args.commandTimeoutMs,
1338
+ });
1339
+ const metrics = parseLastJsonObject(String(result.metadata?.stdoutTail ?? result.content));
1340
+ return {
1341
+ content: [
1342
+ `pageUrl: ${typeof metrics?.pageUrl === "string" ? metrics.pageUrl : ""}`,
1343
+ `screenshotPath: ${captureArgs.screenshotPath}`,
1344
+ `metricsPath: ${captureArgs.metricsPath}`,
1345
+ `waitSelector: ${captureArgs.waitSelector}`,
1346
+ ...(captureArgs.focusSelector ? [`focusSelector: ${captureArgs.focusSelector}`] : []),
1347
+ ...(captureArgs.expectText ? [`expectText: ${captureArgs.expectText}`] : []),
1348
+ "",
1349
+ result.content,
1350
+ ].join("\n").trim(),
1351
+ metadata: {
1352
+ ...result.metadata,
1353
+ script: "probe:visual-review",
1354
+ args: extraArgs,
1355
+ baseUrl,
1356
+ path: captureArgs.path,
1357
+ waitSelector: captureArgs.waitSelector,
1358
+ scrollSelector: captureArgs.scrollSelector,
1359
+ focusSelector: captureArgs.focusSelector,
1360
+ expectText: captureArgs.expectText,
1361
+ screenshotPath: captureArgs.screenshotPath,
1362
+ metricsPath: captureArgs.metricsPath,
1363
+ ...(metrics ?? {}),
1364
+ },
1365
+ };
1366
+ }
1367
+
1368
+ async function previewLifecycleTool(args: {
1369
+ call: AgentRuntimeToolCallInput;
1370
+ workspaceRoot: string;
1371
+ commandTimeoutMs?: number;
1372
+ script: "preview:start" | "preview:status" | "preview:stop" | "preview:release";
1373
+ }): Promise<AgentRuntimeToolResult> {
1374
+ const result = await runPackageScriptTool({
1375
+ call: {
1376
+ id: args.call.id,
1377
+ name: "runPackageScript",
1378
+ arguments: JSON.stringify({ script: args.script }),
1379
+ },
1380
+ workspaceRoot: args.workspaceRoot,
1381
+ commandTimeoutMs: args.commandTimeoutMs,
1382
+ });
1383
+ const summary = parseLastJsonObject(String(result.metadata?.stdoutTail ?? result.content)) ?? {};
1384
+ return {
1385
+ content: [
1386
+ typeof summary.previewUrl === "string" ? `previewUrl: ${summary.previewUrl}` : "",
1387
+ typeof summary.localApiOrigin === "string" ? `localApiOrigin: ${summary.localApiOrigin}` : "",
1388
+ typeof summary.serverDbPath === "string" ? `serverDbPath: ${summary.serverDbPath}` : "",
1389
+ "",
1390
+ result.content,
1391
+ ].filter((line, index, lines) => line || (index > 0 && lines[index - 1])).join("\n").trim(),
1392
+ metadata: {
1393
+ ...result.metadata,
1394
+ script: args.script,
1395
+ ...(summary ?? {}),
1396
+ },
1397
+ };
1398
+ }
1399
+
1400
+ async function execShellTool(args: {
1401
+ call: AgentRuntimeToolCallInput;
1402
+ workspaceRoot: string;
1403
+ commandTimeoutMs?: number;
1404
+ }): Promise<AgentRuntimeToolResult> {
1405
+ const parsed = parseWorkspaceToolArguments(args.call.arguments);
1406
+ const command = requireShellCommand(parsed, args.call.name);
1407
+ const result = await runWorkspaceCommand({
1408
+ workspaceRoot: args.workspaceRoot,
1409
+ command: ["/bin/sh", "-lc", command],
1410
+ timeoutMs: args.commandTimeoutMs,
1411
+ });
1412
+ return {
1413
+ content: result.content,
1414
+ metadata: { exitCode: result.exitCode, timedOut: result.timedOut },
1415
+ };
1416
+ }
1417
+
1418
+ export function createLocalWorkspaceToolExecutors(args: LocalWorkspaceToolArgs) {
1419
+ return {
1420
+ listWorkspaceFiles: (call: AgentRuntimeToolCallInput) => listWorkspaceFilesTool({
1421
+ call,
1422
+ workspaceRoot: args.workspaceRoot,
1423
+ }),
1424
+ readWorkspaceFile: (call: AgentRuntimeToolCallInput) => readWorkspaceFileTool({
1425
+ call,
1426
+ workspaceRoot: args.workspaceRoot,
1427
+ }),
1428
+ writeWorkspaceFile: (call: AgentRuntimeToolCallInput) => writeWorkspaceFileTool({
1429
+ call,
1430
+ workspaceRoot: args.workspaceRoot,
1431
+ }),
1432
+ replaceWorkspaceText: (call: AgentRuntimeToolCallInput) => replaceWorkspaceTextTool({
1433
+ call,
1434
+ workspaceRoot: args.workspaceRoot,
1435
+ }),
1436
+ applyEdit: (call: AgentRuntimeToolCallInput) => replaceWorkspaceTextTool({
1437
+ call,
1438
+ workspaceRoot: args.workspaceRoot,
1439
+ }),
1440
+ applyLineEdits: (call: AgentRuntimeToolCallInput) => replaceWorkspaceTextTool({
1441
+ call,
1442
+ workspaceRoot: args.workspaceRoot,
1443
+ }),
1444
+ listFiles: (call: AgentRuntimeToolCallInput) => listWorkspaceFilesTool({
1445
+ call,
1446
+ workspaceRoot: args.workspaceRoot,
1447
+ }),
1448
+ readFile: (call: AgentRuntimeToolCallInput) => readWorkspaceFileTool({
1449
+ call,
1450
+ workspaceRoot: args.workspaceRoot,
1451
+ }),
1452
+ writeFile: (call: AgentRuntimeToolCallInput) => writeWorkspaceFileTool({
1453
+ call,
1454
+ workspaceRoot: args.workspaceRoot,
1455
+ }),
1456
+ searchWorkspace: (call: AgentRuntimeToolCallInput) => searchWorkspaceTool({
1457
+ call,
1458
+ workspaceRoot: args.workspaceRoot,
1459
+ }),
1460
+ applyPatch: (call: AgentRuntimeToolCallInput) => applyPatchTool({
1461
+ call,
1462
+ workspaceRoot: args.workspaceRoot,
1463
+ }),
1464
+ gitStatus: () => gitStatusTool({
1465
+ workspaceRoot: args.workspaceRoot,
1466
+ }),
1467
+ gitDiff: (call: AgentRuntimeToolCallInput) => gitDiffTool({
1468
+ call,
1469
+ workspaceRoot: args.workspaceRoot,
1470
+ }),
1471
+ gitCreateBranch: (call: AgentRuntimeToolCallInput) => gitCreateBranchTool({
1472
+ call,
1473
+ workspaceRoot: args.workspaceRoot,
1474
+ }),
1475
+ gitAdd: (call: AgentRuntimeToolCallInput) => gitAddTool({
1476
+ call,
1477
+ workspaceRoot: args.workspaceRoot,
1478
+ }),
1479
+ gitCommit: (call: AgentRuntimeToolCallInput) => gitCommitTool({
1480
+ call,
1481
+ workspaceRoot: args.workspaceRoot,
1482
+ }),
1483
+ commitWorkspace: (call: AgentRuntimeToolCallInput) => commitWorkspaceTool({
1484
+ call,
1485
+ workspaceRoot: args.workspaceRoot,
1486
+ }),
1487
+ runPackageScript: (call: AgentRuntimeToolCallInput) => runPackageScriptTool({
1488
+ call,
1489
+ workspaceRoot: args.workspaceRoot,
1490
+ commandTimeoutMs: args.commandTimeoutMs,
1491
+ }),
1492
+ startPreview: (call: AgentRuntimeToolCallInput) => previewLifecycleTool({
1493
+ call,
1494
+ workspaceRoot: args.workspaceRoot,
1495
+ commandTimeoutMs: args.commandTimeoutMs,
1496
+ script: "preview:start",
1497
+ }),
1498
+ getPreviewStatus: (call: AgentRuntimeToolCallInput) => previewLifecycleTool({
1499
+ call,
1500
+ workspaceRoot: args.workspaceRoot,
1501
+ commandTimeoutMs: args.commandTimeoutMs,
1502
+ script: "preview:status",
1503
+ }),
1504
+ stopPreview: (call: AgentRuntimeToolCallInput) => previewLifecycleTool({
1505
+ call,
1506
+ workspaceRoot: args.workspaceRoot,
1507
+ commandTimeoutMs: args.commandTimeoutMs,
1508
+ script: "preview:stop",
1509
+ }),
1510
+ releasePreview: (call: AgentRuntimeToolCallInput) => previewLifecycleTool({
1511
+ call,
1512
+ workspaceRoot: args.workspaceRoot,
1513
+ commandTimeoutMs: args.commandTimeoutMs,
1514
+ script: "preview:release",
1515
+ }),
1516
+ captureVisualState: (call: AgentRuntimeToolCallInput) => captureVisualStateTool({
1517
+ call,
1518
+ workspaceRoot: args.workspaceRoot,
1519
+ commandTimeoutMs: args.commandTimeoutMs,
1520
+ }),
1521
+ execShell: (call: AgentRuntimeToolCallInput) => execShellTool({
1522
+ call,
1523
+ workspaceRoot: args.workspaceRoot,
1524
+ commandTimeoutMs: args.commandTimeoutMs,
1525
+ }),
1526
+ execBash: (call: AgentRuntimeToolCallInput) => execShellTool({
1527
+ call,
1528
+ workspaceRoot: args.workspaceRoot,
1529
+ commandTimeoutMs: args.commandTimeoutMs,
1530
+ }),
1531
+ };
1532
+ }