keystone-cli 1.0.3 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +276 -32
  2. package/package.json +8 -4
  3. package/src/cli.ts +350 -416
  4. package/src/commands/doc.ts +31 -0
  5. package/src/commands/event.ts +29 -0
  6. package/src/commands/graph.ts +37 -0
  7. package/src/commands/index.ts +14 -0
  8. package/src/commands/init.ts +185 -0
  9. package/src/commands/run.ts +124 -0
  10. package/src/commands/schema.ts +40 -0
  11. package/src/commands/utils.ts +78 -0
  12. package/src/commands/validate.ts +111 -0
  13. package/src/db/workflow-db.test.ts +314 -0
  14. package/src/db/workflow-db.ts +810 -210
  15. package/src/expression/evaluator-audit.test.ts +4 -2
  16. package/src/expression/evaluator.test.ts +14 -1
  17. package/src/expression/evaluator.ts +166 -19
  18. package/src/parser/config-schema.ts +18 -0
  19. package/src/parser/schema.ts +153 -22
  20. package/src/parser/test-schema.ts +6 -6
  21. package/src/parser/workflow-parser.test.ts +24 -0
  22. package/src/parser/workflow-parser.ts +65 -3
  23. package/src/runner/auto-heal.test.ts +5 -6
  24. package/src/runner/blueprint-executor.test.ts +2 -2
  25. package/src/runner/debug-repl.test.ts +5 -8
  26. package/src/runner/debug-repl.ts +59 -16
  27. package/src/runner/durable-timers.test.ts +11 -2
  28. package/src/runner/engine-executor.test.ts +1 -1
  29. package/src/runner/events.ts +57 -0
  30. package/src/runner/executors/artifact-executor.ts +166 -0
  31. package/src/runner/{blueprint-executor.ts → executors/blueprint-executor.ts} +15 -7
  32. package/src/runner/{engine-executor.ts → executors/engine-executor.ts} +55 -7
  33. package/src/runner/executors/file-executor.test.ts +48 -0
  34. package/src/runner/executors/file-executor.ts +324 -0
  35. package/src/runner/{foreach-executor.ts → executors/foreach-executor.ts} +168 -80
  36. package/src/runner/executors/human-executor.ts +144 -0
  37. package/src/runner/executors/join-executor.ts +75 -0
  38. package/src/runner/executors/llm-executor.ts +1266 -0
  39. package/src/runner/executors/memory-executor.ts +71 -0
  40. package/src/runner/executors/plan-executor.ts +104 -0
  41. package/src/runner/executors/request-executor.ts +265 -0
  42. package/src/runner/executors/script-executor.ts +43 -0
  43. package/src/runner/executors/shell-executor.ts +403 -0
  44. package/src/runner/executors/subworkflow-executor.ts +114 -0
  45. package/src/runner/executors/types.ts +69 -0
  46. package/src/runner/executors/wait-executor.ts +59 -0
  47. package/src/runner/join-scheduling.test.ts +197 -0
  48. package/src/runner/llm-adapter-runtime.test.ts +209 -0
  49. package/src/runner/llm-adapter.test.ts +419 -24
  50. package/src/runner/llm-adapter.ts +130 -26
  51. package/src/runner/llm-clarification.test.ts +2 -1
  52. package/src/runner/llm-executor.test.ts +532 -17
  53. package/src/runner/mcp-client-audit.test.ts +1 -2
  54. package/src/runner/mcp-client.ts +136 -46
  55. package/src/runner/mcp-manager.test.ts +4 -0
  56. package/src/runner/mcp-server.test.ts +58 -0
  57. package/src/runner/mcp-server.ts +26 -0
  58. package/src/runner/memoization.test.ts +190 -0
  59. package/src/runner/optimization-runner.ts +4 -9
  60. package/src/runner/quality-gate.test.ts +69 -0
  61. package/src/runner/reflexion.test.ts +6 -17
  62. package/src/runner/resource-pool.ts +102 -14
  63. package/src/runner/services/context-builder.ts +144 -0
  64. package/src/runner/services/secret-manager.ts +105 -0
  65. package/src/runner/services/workflow-validator.ts +131 -0
  66. package/src/runner/shell-executor.test.ts +28 -4
  67. package/src/runner/standard-tools-ast.test.ts +196 -0
  68. package/src/runner/standard-tools-execution.test.ts +27 -0
  69. package/src/runner/standard-tools-integration.test.ts +6 -10
  70. package/src/runner/standard-tools.ts +339 -102
  71. package/src/runner/step-executor.test.ts +216 -4
  72. package/src/runner/step-executor.ts +69 -941
  73. package/src/runner/stream-utils.ts +7 -3
  74. package/src/runner/test-harness.ts +20 -1
  75. package/src/runner/timeout.test.ts +10 -0
  76. package/src/runner/timeout.ts +11 -2
  77. package/src/runner/tool-integration.test.ts +1 -1
  78. package/src/runner/wait-step.test.ts +102 -0
  79. package/src/runner/workflow-runner.test.ts +208 -15
  80. package/src/runner/workflow-runner.ts +890 -818
  81. package/src/runner/workflow-scheduler.ts +75 -0
  82. package/src/runner/workflow-state.ts +269 -0
  83. package/src/runner/workflow-subflows.test.ts +13 -12
  84. package/src/scripts/generate-schemas.ts +16 -0
  85. package/src/templates/agents/explore.md +1 -0
  86. package/src/templates/agents/general.md +1 -0
  87. package/src/templates/agents/handoff-router.md +14 -0
  88. package/src/templates/agents/handoff-specialist.md +15 -0
  89. package/src/templates/agents/keystone-architect.md +13 -44
  90. package/src/templates/agents/my-agent.md +1 -0
  91. package/src/templates/agents/software-engineer.md +1 -0
  92. package/src/templates/agents/summarizer.md +1 -0
  93. package/src/templates/agents/test-agent.md +1 -0
  94. package/src/templates/agents/tester.md +1 -0
  95. package/src/templates/{basic-inputs.yaml → basics/basic-inputs.yaml} +2 -0
  96. package/src/templates/{basic-shell.yaml → basics/basic-shell.yaml} +4 -1
  97. package/src/templates/{full-feature-demo.yaml → basics/full-feature-demo.yaml} +2 -0
  98. package/src/templates/{stop-watch.yaml → basics/stop-watch.yaml} +1 -0
  99. package/src/templates/{child-rollback.yaml → control-flow/child-rollback.yaml} +1 -0
  100. package/src/templates/{cleanup-finally.yaml → control-flow/cleanup-finally.yaml} +1 -0
  101. package/src/templates/{fan-out-fan-in.yaml → control-flow/fan-out-fan-in.yaml} +3 -0
  102. package/src/templates/control-flow/idempotency-example.yaml +30 -0
  103. package/src/templates/{loop-parallel.yaml → control-flow/loop-parallel.yaml} +3 -0
  104. package/src/templates/{parent-rollback.yaml → control-flow/parent-rollback.yaml} +1 -0
  105. package/src/templates/{retry-policy.yaml → control-flow/retry-policy.yaml} +3 -0
  106. package/src/templates/features/artifact-example.yaml +40 -0
  107. package/src/templates/{engine-example.yaml → features/engine-example.yaml} +1 -0
  108. package/src/templates/{human-interaction.yaml → features/human-interaction.yaml} +1 -0
  109. package/src/templates/{llm-agent.yaml → features/llm-agent.yaml} +1 -0
  110. package/src/templates/{memory-service.yaml → features/memory-service.yaml} +2 -0
  111. package/src/templates/{robust-automation.yaml → features/robust-automation.yaml} +3 -0
  112. package/src/templates/features/script-example.yaml +28 -0
  113. package/src/templates/patterns/agent-handoff.yaml +53 -0
  114. package/src/templates/{approval-process.yaml → patterns/approval-process.yaml} +1 -0
  115. package/src/templates/{batch-processor.yaml → patterns/batch-processor.yaml} +2 -0
  116. package/src/templates/{composition-child.yaml → patterns/composition-child.yaml} +2 -1
  117. package/src/templates/patterns/composition-parent.yaml +18 -0
  118. package/src/templates/{data-pipeline.yaml → patterns/data-pipeline.yaml} +2 -0
  119. package/src/templates/{decompose-implement.yaml → scaffolding/decompose-implement.yaml} +1 -0
  120. package/src/templates/{decompose-problem.yaml → scaffolding/decompose-problem.yaml} +1 -0
  121. package/src/templates/{decompose-research.yaml → scaffolding/decompose-research.yaml} +1 -0
  122. package/src/templates/{decompose-review.yaml → scaffolding/decompose-review.yaml} +1 -0
  123. package/src/templates/{dev.yaml → scaffolding/dev.yaml} +1 -0
  124. package/src/templates/scaffolding/review-loop.yaml +97 -0
  125. package/src/templates/{scaffold-feature.yaml → scaffolding/scaffold-feature.yaml} +2 -0
  126. package/src/templates/{scaffold-generate.yaml → scaffolding/scaffold-generate.yaml} +1 -0
  127. package/src/templates/{scaffold-plan.yaml → scaffolding/scaffold-plan.yaml} +1 -0
  128. package/src/templates/testing/invalid.yaml +6 -0
  129. package/src/ui/dashboard.tsx +191 -33
  130. package/src/utils/auth-manager.test.ts +337 -0
  131. package/src/utils/auth-manager.ts +157 -61
  132. package/src/utils/blueprint-utils.ts +4 -6
  133. package/src/utils/config-loader.test.ts +2 -0
  134. package/src/utils/config-loader.ts +12 -3
  135. package/src/utils/constants.ts +76 -0
  136. package/src/utils/container.ts +63 -0
  137. package/src/utils/context-injector.test.ts +200 -0
  138. package/src/utils/context-injector.ts +244 -0
  139. package/src/utils/doc-generator.ts +85 -0
  140. package/src/utils/env-filter.ts +45 -0
  141. package/src/utils/json-parser.test.ts +12 -0
  142. package/src/utils/json-parser.ts +30 -5
  143. package/src/utils/logger.ts +12 -1
  144. package/src/utils/mermaid.ts +4 -0
  145. package/src/utils/paths.ts +52 -1
  146. package/src/utils/process-sandbox-worker.test.ts +46 -0
  147. package/src/utils/process-sandbox.ts +227 -14
  148. package/src/utils/redactor.test.ts +11 -6
  149. package/src/utils/redactor.ts +25 -9
  150. package/src/utils/sandbox.ts +3 -0
  151. package/src/runner/llm-executor.ts +0 -638
  152. package/src/runner/shell-executor.ts +0 -366
  153. package/src/templates/composition-parent.yaml +0 -14
  154. package/src/templates/invalid.yaml +0 -5
@@ -1,8 +1,9 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
+ import { Lang, parse } from '@ast-grep/napi';
3
4
  import type { AgentTool } from '../parser/schema';
4
5
  import { LIMITS, TIMEOUTS } from '../utils/constants';
5
- import { detectShellInjectionRisk } from './shell-executor';
6
+ import { detectShellInjectionRisk } from './executors/shell-executor';
6
7
 
7
8
  export const STANDARD_TOOLS: AgentTool[] = [
8
9
  {
@@ -76,6 +77,25 @@ export const STANDARD_TOOLS: AgentTool[] = [
76
77
  content: '${{ args.content }}',
77
78
  },
78
79
  },
80
+ {
81
+ name: 'append_file',
82
+ description: 'Append content to the end of a file (creates if not exists)',
83
+ parameters: {
84
+ type: 'object',
85
+ properties: {
86
+ path: { type: 'string', description: 'Path to the file to append to' },
87
+ content: { type: 'string', description: 'Content to append to the file' },
88
+ },
89
+ required: ['path', 'content'],
90
+ },
91
+ execution: {
92
+ id: 'std_append_file',
93
+ type: 'file',
94
+ op: 'append',
95
+ path: '${{ args.path }}',
96
+ content: '${{ args.content }}',
97
+ },
98
+ },
79
99
  {
80
100
  name: 'list_files',
81
101
  description: 'List files in a directory',
@@ -123,22 +143,6 @@ export const STANDARD_TOOLS: AgentTool[] = [
123
143
  required: ['pattern'],
124
144
  },
125
145
  execution: {
126
- id: 'std_search_files',
127
- type: 'script',
128
- run: `
129
- (function() {
130
- const fs = require('node:fs');
131
- const path = require('node:path');
132
- const { globSync } = require('glob');
133
- const dir = args.dir || '.';
134
- const pattern = args.pattern;
135
- try {
136
- return globSync(pattern, { cwd: dir, nodir: true });
137
- } catch (e) {
138
- throw new Error('Search failed: ' + e.message);
139
- }
140
- })();
141
- `,
142
146
  allowInsecure: true,
143
147
  },
144
148
  },
@@ -162,106 +166,147 @@ export const STANDARD_TOOLS: AgentTool[] = [
162
166
  id: 'std_search_content',
163
167
  type: 'script',
164
168
  run: `
165
- (function() {
169
+ (async function() {
166
170
  const fs = require('node:fs');
167
171
  const path = require('node:path');
168
172
  const { globSync } = require('glob');
173
+ // Try to request worker_threads. If not available, we might fall back or fail safely.
174
+ // Standard Node environment carries it.
175
+ const { Worker } = require('node:worker_threads');
176
+
169
177
  const dir = args.dir || '.';
170
178
  const pattern = args.pattern || '**/*';
171
179
  const query = args.query;
172
- const Re2 = (() => {
173
- try {
174
- return require('re2');
175
- } catch {
176
- return null;
177
- }
178
- })();
179
- const maxQueryLength = ${LIMITS.MAX_SEARCH_QUERY_LENGTH};
180
- const maxPatternLength = ${LIMITS.MAX_REGEX_PATTERN_LENGTH};
181
- const maxResults = ${LIMITS.MAX_SEARCH_RESULTS};
182
- const maxFileBytes = ${LIMITS.MAX_SEARCH_FILE_BYTES};
183
- const maxLineLength = ${LIMITS.MAX_SEARCH_LINE_LENGTH};
184
- const regexTimeoutMs = ${TIMEOUTS.REGEX_TIMEOUT_MS};
180
+
181
+ const LIMITS = ${JSON.stringify(LIMITS)};
182
+ const TIMEOUTS = ${JSON.stringify(TIMEOUTS)};
185
183
 
186
- if (query.length > maxQueryLength) {
187
- throw new Error('Search query exceeds maximum length of ' + maxQueryLength + ' characters');
184
+ if (query.length > LIMITS.MAX_SEARCH_QUERY_LENGTH) {
185
+ throw new Error('Search query exceeds maximum length of ' + LIMITS.MAX_SEARCH_QUERY_LENGTH + ' characters');
188
186
  }
189
187
 
190
188
  const isRegex = query.startsWith('/') && query.endsWith('/') && query.length > 1;
191
- let regex;
192
- let normalizedQuery = '';
193
- let regexDeadline;
194
-
195
- // ReDoS protection: detect dangerous regex patterns
196
- if (isRegex) {
197
- const pattern = query.slice(1, -1);
198
- if (pattern.length > maxPatternLength) {
199
- throw new Error('Regex pattern exceeds maximum length of ' + maxPatternLength + ' characters');
200
- }
201
- // Detect common ReDoS patterns: nested quantifiers, overlapping alternations
202
- const dangerousPatterns = [
203
- /\\([^)]*(?:[+*]|\\{\\d+(?:,\\d*)?\\})[^)]*\\)(?:[+*]|\\{\\d+(?:,\\d*)?\\})/, // (x+)+, (x{1,3})*
204
- /\\([^)]*\\|[^)]*\\)(?:[+*]|\\{\\d+(?:,\\d*)?\\})/, // (a|aa)+
205
- /(?:[+*]|\\{\\d+(?:,\\d*)?\\}){2,}/, // consecutive quantifiers
206
- ];
207
- if (dangerousPatterns.some(p => p.test(pattern))) {
208
- throw new Error('Regex pattern contains potentially dangerous constructs (possible ReDoS). Simplify the pattern.');
209
- }
210
189
 
211
- try {
212
- regex = Re2 ? new Re2(pattern) : new RegExp(pattern);
213
- } catch (e) {
214
- throw new Error('Invalid regular expression: ' + e.message);
215
- }
216
-
217
- } else {
218
- normalizedQuery = query.toLowerCase();
190
+ // If NOT regex, use simple main-thread search (safe)
191
+ if (!isRegex) {
192
+ const normalizedQuery = query.toLowerCase();
193
+ const files = globSync(pattern, { cwd: dir, nodir: true });
194
+ const results = [];
195
+ for (const file of files) {
196
+ const fullPath = path.join(dir, file);
197
+ try {
198
+ const stat = fs.statSync(fullPath);
199
+ if (stat.size > LIMITS.MAX_SEARCH_FILE_BYTES) continue;
200
+ const content = fs.readFileSync(fullPath, 'utf8');
201
+ const lines = content.split('\\n');
202
+ for (let i = 0; i < lines.length; i++) {
203
+ const line = lines[i];
204
+ if (line.length > LIMITS.MAX_SEARCH_LINE_LENGTH) continue;
205
+ if (line.toLowerCase().includes(normalizedQuery)) {
206
+ results.push({
207
+ file,
208
+ line: i + 1,
209
+ content: line.trim()
210
+ });
211
+ }
212
+ if (results.length >= LIMITS.MAX_SEARCH_RESULTS) break;
213
+ }
214
+ } catch {}
215
+ if (results.length >= LIMITS.MAX_SEARCH_RESULTS) break;
216
+ }
217
+ return results;
219
218
  }
220
-
219
+
220
+ // For REGEX, use a Worker to handle ReDoS/Timeouts
221
+ // We pass the files list to the worker so it does the heavy lifting
221
222
  const files = globSync(pattern, { cwd: dir, nodir: true });
222
- const results = [];
223
- for (const file of files) {
224
- if (isRegex) {
225
- regexDeadline = Date.now() + regexTimeoutMs;
226
- }
227
- const fullPath = path.join(dir, file);
228
- let stat;
229
- try {
230
- stat = fs.statSync(fullPath);
231
- } catch {
232
- continue;
233
- }
234
- if (stat.size > maxFileBytes) {
235
- continue;
236
- }
237
- let content;
223
+
224
+ const workerScript = \`
225
+ const { parentPort, workerData } = require('node:worker_threads');
226
+ const fs = require('node:fs');
227
+ const path = require('node:path');
228
+
238
229
  try {
239
- content = fs.readFileSync(fullPath, 'utf8');
240
- } catch {
241
- continue;
242
- }
243
- const lines = content.split('\\n');
244
- for (let i = 0; i < lines.length; i++) {
245
- if (regexDeadline && Date.now() > regexDeadline) {
246
- throw new Error('Regex search exceeded time limit');
247
- }
248
- const line = lines[i];
249
- if (line.length > maxLineLength) {
250
- continue;
230
+ const { files, dir, query, LIMITS } = workerData;
231
+ let Re2;
232
+ try { Re2 = require('re2'); } catch {}
233
+
234
+ const patternStr = query.slice(1, -1);
235
+ let regex;
236
+ try {
237
+ regex = Re2 ? new Re2(patternStr) : new RegExp(patternStr);
238
+ } catch (e) {
239
+ parentPort.postMessage({ error: 'Invalid regex: ' + e.message });
240
+ process.exit(0);
251
241
  }
252
- const matched = isRegex ? regex.test(line) : line.toLowerCase().includes(normalizedQuery);
253
- if (matched) {
254
- results.push({
255
- file,
256
- line: i + 1,
257
- content: line.trim()
258
- });
242
+
243
+ const results = [];
244
+
245
+ for (const file of files) {
246
+ const fullPath = path.join(dir, file);
247
+ try {
248
+ const stat = fs.statSync(fullPath);
249
+ if (stat.size > LIMITS.MAX_SEARCH_FILE_BYTES) continue;
250
+ const content = fs.readFileSync(fullPath, 'utf8');
251
+ const lines = content.split('\\n');
252
+ for (let i = 0; i < lines.length; i++) {
253
+ const line = lines[i];
254
+ if (line.length > LIMITS.MAX_SEARCH_LINE_LENGTH) continue;
255
+
256
+ // This matching is interruptible by thread termination
257
+ if (regex.test(line)) {
258
+ results.push({
259
+ file,
260
+ line: i + 1,
261
+ content: line.trim()
262
+ });
263
+ }
264
+ if (results.length >= LIMITS.MAX_SEARCH_RESULTS) break;
265
+ }
266
+ } catch {}
267
+ if (results.length >= LIMITS.MAX_SEARCH_RESULTS) break;
259
268
  }
260
- if (results.length >= maxResults) break;
269
+
270
+ parentPort.postMessage({ results });
271
+ } catch (err) {
272
+ parentPort.postMessage({ error: err.message });
261
273
  }
262
- if (results.length >= maxResults) break;
263
- }
264
- return results;
274
+ \`;
275
+
276
+ return new Promise((resolve, reject) => {
277
+ const worker = new Worker(workerScript, {
278
+ eval: true,
279
+ workerData: { files, dir, query, LIMITS }
280
+ });
281
+
282
+ // Hard timeout
283
+ const timeout = setTimeout(() => {
284
+ worker.terminate();
285
+ reject(new Error('Regex search timed out (possible ReDoS or large working set).'));
286
+ }, TIMEOUTS.REGEX_TIMEOUT_MS);
287
+
288
+ worker.on('message', (msg) => {
289
+ clearTimeout(timeout);
290
+ if (msg.error) {
291
+ reject(new Error(msg.error));
292
+ } else {
293
+ resolve(msg.results);
294
+ }
295
+ worker.terminate();
296
+ });
297
+
298
+ worker.on('error', (err) => {
299
+ clearTimeout(timeout);
300
+ reject(err);
301
+ });
302
+
303
+ worker.on('exit', (code) => {
304
+ if (code !== 0) {
305
+ clearTimeout(timeout);
306
+ reject(new Error(\`Worker stopped with exit code \${code}\`));
307
+ }
308
+ });
309
+ });
265
310
  })();
266
311
  `,
267
312
  allowInsecure: true,
@@ -285,6 +330,189 @@ export const STANDARD_TOOLS: AgentTool[] = [
285
330
  dir: '${{ args.dir }}',
286
331
  },
287
332
  },
333
+ {
334
+ name: 'ast_grep_search',
335
+ description:
336
+ 'Search for structural code patterns using AST pattern matching. More precise than regex for code refactoring.',
337
+ parameters: {
338
+ type: 'object',
339
+ properties: {
340
+ pattern: {
341
+ type: 'string',
342
+ description: 'AST-grep pattern to search for, e.g. "console.log($A)"',
343
+ },
344
+ language: {
345
+ type: 'string',
346
+ description: 'Programming language (javascript, typescript, python, rust, go, etc.)',
347
+ default: 'typescript',
348
+ },
349
+ paths: {
350
+ type: 'array',
351
+ items: { type: 'string' },
352
+ description: 'File paths to search in',
353
+ },
354
+ },
355
+ required: ['pattern', 'paths'],
356
+ },
357
+ execution: {
358
+ id: 'std_ast_grep_search',
359
+ type: 'script',
360
+ run: `
361
+ (function() {
362
+ const fs = require('node:fs');
363
+ const path = require('node:path');
364
+ const { Lang, parse } = require('@ast-grep/napi');
365
+
366
+ const pattern = args.pattern;
367
+ const language = args.language || 'typescript';
368
+ const paths = args.paths || [];
369
+
370
+ const langMap = {
371
+ javascript: Lang.JavaScript,
372
+ typescript: Lang.TypeScript,
373
+ tsx: Lang.Tsx,
374
+ python: Lang.Python,
375
+ rust: Lang.Rust,
376
+ go: Lang.Go,
377
+ c: Lang.C,
378
+ cpp: Lang.Cpp,
379
+ java: Lang.Java,
380
+ kotlin: Lang.Kotlin,
381
+ swift: Lang.Swift,
382
+ html: Lang.Html,
383
+ css: Lang.Css,
384
+ json: Lang.Json,
385
+ };
386
+
387
+ const lang = langMap[language.toLowerCase()];
388
+ if (!lang) {
389
+ throw new Error('Unsupported language: ' + language);
390
+ }
391
+
392
+ const results = [];
393
+ for (const filePath of paths) {
394
+ if (!fs.existsSync(filePath)) continue;
395
+ const content = fs.readFileSync(filePath, 'utf8');
396
+ const tree = parse(lang, content);
397
+ const root = tree.root();
398
+ const matches = root.findAll(pattern);
399
+
400
+ for (const match of matches) {
401
+ const range = match.range();
402
+ results.push({
403
+ file: filePath,
404
+ line: range.start.line + 1,
405
+ column: range.start.column + 1,
406
+ content: match.text(),
407
+ });
408
+ }
409
+ }
410
+ return results;
411
+ })();
412
+ `,
413
+ allowInsecure: true,
414
+ },
415
+ },
416
+ {
417
+ name: 'ast_grep_replace',
418
+ description:
419
+ 'Replace structural code patterns using AST-aware rewriting. Safer than regex for code refactoring.',
420
+ parameters: {
421
+ type: 'object',
422
+ properties: {
423
+ pattern: {
424
+ type: 'string',
425
+ description: 'AST-grep pattern to match, e.g. "console.log($A)"',
426
+ },
427
+ rewrite: { type: 'string', description: 'Replacement pattern, e.g. "logger.info($A)"' },
428
+ language: {
429
+ type: 'string',
430
+ description: 'Programming language (javascript, typescript, python, rust, go, etc.)',
431
+ default: 'typescript',
432
+ },
433
+ paths: {
434
+ type: 'array',
435
+ items: { type: 'string' },
436
+ description: 'File paths to apply replacements to',
437
+ },
438
+ },
439
+ required: ['pattern', 'rewrite', 'paths'],
440
+ },
441
+ execution: {
442
+ id: 'std_ast_grep_replace',
443
+ type: 'script',
444
+ run: `
445
+ (function() {
446
+ const fs = require('node:fs');
447
+ const path = require('node:path');
448
+ const { Lang, parse } = require('@ast-grep/napi');
449
+
450
+ const pattern = args.pattern;
451
+ const rewrite = args.rewrite;
452
+ const language = args.language || 'typescript';
453
+ const paths = args.paths || [];
454
+
455
+ const langMap = {
456
+ javascript: Lang.JavaScript,
457
+ typescript: Lang.TypeScript,
458
+ tsx: Lang.Tsx,
459
+ python: Lang.Python,
460
+ rust: Lang.Rust,
461
+ go: Lang.Go,
462
+ c: Lang.C,
463
+ cpp: Lang.Cpp,
464
+ java: Lang.Java,
465
+ kotlin: Lang.Kotlin,
466
+ swift: Lang.Swift,
467
+ html: Lang.Html,
468
+ css: Lang.Css,
469
+ json: Lang.Json,
470
+ };
471
+
472
+ const lang = langMap[language.toLowerCase()];
473
+ if (!lang) {
474
+ throw new Error('Unsupported language: ' + language);
475
+ }
476
+
477
+ const results = [];
478
+ for (const filePath of paths) {
479
+ if (!fs.existsSync(filePath)) continue;
480
+ const content = fs.readFileSync(filePath, 'utf8');
481
+ const tree = parse(lang, content);
482
+ const root = tree.root();
483
+ const edit = root.replace(pattern, rewrite);
484
+
485
+ if (edit !== content) {
486
+ fs.writeFileSync(filePath, edit);
487
+ results.push({
488
+ file: filePath,
489
+ modified: true,
490
+ });
491
+ }
492
+ }
493
+ return results;
494
+ })();
495
+ `,
496
+ allowInsecure: true,
497
+ },
498
+ },
499
+ {
500
+ name: 'fetch',
501
+ description: 'Fetch content from a URL (GET request)',
502
+ parameters: {
503
+ type: 'object',
504
+ properties: {
505
+ url: { type: 'string', description: 'URL to fetch' },
506
+ },
507
+ required: ['url'],
508
+ },
509
+ execution: {
510
+ id: 'std_fetch',
511
+ type: 'request',
512
+ url: '${{ args.url }}',
513
+ method: 'GET',
514
+ },
515
+ },
288
516
  ];
289
517
 
290
518
  /**
@@ -292,8 +520,7 @@ export const STANDARD_TOOLS: AgentTool[] = [
292
520
  */
293
521
  export function validateStandardToolSecurity(
294
522
  toolName: string,
295
- // biome-ignore lint/suspicious/noExplicitAny: arguments can be any shape
296
- args: any,
523
+ args: unknown,
297
524
  options: { allowOutsideCwd?: boolean; allowInsecure?: boolean }
298
525
  ): void {
299
526
  const cwd = process.cwd();
@@ -330,13 +557,23 @@ export function validateStandardToolSecurity(
330
557
  'read_file',
331
558
  'read_file_lines',
332
559
  'write_file',
560
+ 'append_file',
333
561
  'list_files',
334
562
  'search_files',
335
563
  'search_content',
564
+ 'ast_grep_search',
565
+ 'ast_grep_replace',
336
566
  ].includes(toolName)
337
567
  ) {
338
568
  const rawPath = args.path || args.dir || '.';
339
569
  assertWithinCwd(rawPath);
570
+
571
+ // For AST tools, validate all paths in the array
572
+ if (['ast_grep_search', 'ast_grep_replace'].includes(toolName) && Array.isArray(args.paths)) {
573
+ for (const p of args.paths) {
574
+ assertWithinCwd(p);
575
+ }
576
+ }
340
577
  }
341
578
 
342
579
  // 2. Check shell risk for run_command and guard working directory