@warlock.js/ai-workspace 5.29.0 → 5.31.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 (120) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +40 -21
  3. package/cjs/index.cjs +3295 -360
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/backends/local.d.mts +1 -1
  6. package/esm/backends/local.d.mts.map +1 -1
  7. package/esm/backends/local.mjs +199 -31
  8. package/esm/backends/local.mjs.map +1 -1
  9. package/esm/backends/mock.d.mts +12 -6
  10. package/esm/backends/mock.d.mts.map +1 -1
  11. package/esm/backends/mock.mjs +45 -3
  12. package/esm/backends/mock.mjs.map +1 -1
  13. package/esm/contracts/index.d.mts +3 -3
  14. package/esm/contracts/tool-io.type.d.mts +248 -11
  15. package/esm/contracts/tool-io.type.d.mts.map +1 -1
  16. package/esm/contracts/workspace-backend.contract.d.mts +78 -2
  17. package/esm/contracts/workspace-backend.contract.d.mts.map +1 -1
  18. package/esm/contracts/workspace-ops.contract.d.mts +41 -16
  19. package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
  20. package/esm/contracts/workspace-policy.type.d.mts +142 -14
  21. package/esm/contracts/workspace-policy.type.d.mts.map +1 -1
  22. package/esm/contracts/workspace.contract.d.mts +75 -22
  23. package/esm/contracts/workspace.contract.d.mts.map +1 -1
  24. package/esm/errors.d.mts +80 -6
  25. package/esm/errors.d.mts.map +1 -1
  26. package/esm/errors.mjs +23 -2
  27. package/esm/errors.mjs.map +1 -1
  28. package/esm/index.d.mts +13 -5
  29. package/esm/index.mjs +10 -2
  30. package/esm/ops.d.mts +22 -3
  31. package/esm/ops.d.mts.map +1 -1
  32. package/esm/ops.mjs +341 -82
  33. package/esm/ops.mjs.map +1 -1
  34. package/esm/policy/jail.mjs +73 -0
  35. package/esm/policy/jail.mjs.map +1 -0
  36. package/esm/policy/policy.d.mts +26 -16
  37. package/esm/policy/policy.d.mts.map +1 -1
  38. package/esm/policy/policy.mjs +62 -86
  39. package/esm/policy/policy.mjs.map +1 -1
  40. package/esm/policy/shell-check.d.mts +64 -0
  41. package/esm/policy/shell-check.d.mts.map +1 -0
  42. package/esm/policy/shell-check.mjs +627 -0
  43. package/esm/policy/shell-check.mjs.map +1 -0
  44. package/esm/policy/shell-parser.mjs +802 -0
  45. package/esm/policy/shell-parser.mjs.map +1 -0
  46. package/esm/policy/tokenize-command.d.mts +63 -0
  47. package/esm/policy/tokenize-command.d.mts.map +1 -0
  48. package/esm/policy/tokenize-command.mjs +76 -24
  49. package/esm/policy/tokenize-command.mjs.map +1 -1
  50. package/esm/shell-jobs.d.mts +45 -0
  51. package/esm/shell-jobs.d.mts.map +1 -0
  52. package/esm/shell-jobs.mjs +247 -0
  53. package/esm/shell-jobs.mjs.map +1 -0
  54. package/esm/todos.d.mts +22 -0
  55. package/esm/todos.d.mts.map +1 -0
  56. package/esm/todos.mjs +73 -0
  57. package/esm/todos.mjs.map +1 -0
  58. package/esm/tools/edit-file.d.mts +1 -1
  59. package/esm/tools/edit-file.d.mts.map +1 -1
  60. package/esm/tools/edit-file.mjs +3 -2
  61. package/esm/tools/edit-file.mjs.map +1 -1
  62. package/esm/tools/glob.d.mts +1 -1
  63. package/esm/tools/glob.d.mts.map +1 -1
  64. package/esm/tools/glob.mjs +3 -2
  65. package/esm/tools/glob.mjs.map +1 -1
  66. package/esm/tools/grep.d.mts +1 -1
  67. package/esm/tools/grep.d.mts.map +1 -1
  68. package/esm/tools/grep.mjs +3 -2
  69. package/esm/tools/grep.mjs.map +1 -1
  70. package/esm/tools/json-schemas.mjs +175 -0
  71. package/esm/tools/json-schemas.mjs.map +1 -0
  72. package/esm/tools/list-dir.d.mts +32 -0
  73. package/esm/tools/list-dir.d.mts.map +1 -0
  74. package/esm/tools/list-dir.mjs +50 -0
  75. package/esm/tools/list-dir.mjs.map +1 -0
  76. package/esm/tools/multi-edit.d.mts +38 -0
  77. package/esm/tools/multi-edit.d.mts.map +1 -0
  78. package/esm/tools/multi-edit.mjs +121 -0
  79. package/esm/tools/multi-edit.mjs.map +1 -0
  80. package/esm/tools/read-file.d.mts +5 -5
  81. package/esm/tools/read-file.d.mts.map +1 -1
  82. package/esm/tools/read-file.mjs +10 -20
  83. package/esm/tools/read-file.mjs.map +1 -1
  84. package/esm/tools/run-shell.d.mts +39 -13
  85. package/esm/tools/run-shell.d.mts.map +1 -1
  86. package/esm/tools/run-shell.mjs +60 -47
  87. package/esm/tools/run-shell.mjs.map +1 -1
  88. package/esm/tools/run-tests.d.mts +11 -2
  89. package/esm/tools/run-tests.d.mts.map +1 -1
  90. package/esm/tools/run-tests.mjs +15 -7
  91. package/esm/tools/run-tests.mjs.map +1 -1
  92. package/esm/tools/schema.mjs +24 -19
  93. package/esm/tools/schema.mjs.map +1 -1
  94. package/esm/tools/shell-kill.d.mts +25 -0
  95. package/esm/tools/shell-kill.d.mts.map +1 -0
  96. package/esm/tools/shell-kill.mjs +36 -0
  97. package/esm/tools/shell-kill.mjs.map +1 -0
  98. package/esm/tools/shell-output.d.mts +30 -0
  99. package/esm/tools/shell-output.d.mts.map +1 -0
  100. package/esm/tools/shell-output.mjs +49 -0
  101. package/esm/tools/shell-output.mjs.map +1 -0
  102. package/esm/tools/todo.d.mts +43 -0
  103. package/esm/tools/todo.d.mts.map +1 -0
  104. package/esm/tools/todo.mjs +126 -0
  105. package/esm/tools/todo.mjs.map +1 -0
  106. package/esm/tools/write-file.d.mts +1 -1
  107. package/esm/tools/write-file.d.mts.map +1 -1
  108. package/esm/tools/write-file.mjs +3 -2
  109. package/esm/tools/write-file.mjs.map +1 -1
  110. package/esm/workspace.d.mts +12 -9
  111. package/esm/workspace.d.mts.map +1 -1
  112. package/esm/workspace.mjs +109 -28
  113. package/esm/workspace.mjs.map +1 -1
  114. package/llms-full.txt +192 -35
  115. package/llms.txt +1 -1
  116. package/package.json +5 -4
  117. package/skills/README.md +2 -2
  118. package/skills/build-loop-agent/SKILL.md +7 -7
  119. package/skills/index.md +6 -1
  120. package/skills/use-a-workspace/SKILL.md +185 -28
@@ -1,27 +1,47 @@
1
- import { RunShellInput, RunShellResult } from "../contracts/tool-io.type.mjs";
1
+ import { RunShellBackgroundResult, RunShellInput, RunShellResult } from "../contracts/tool-io.type.mjs";
2
2
  import { WorkspaceOps } from "../contracts/workspace-ops.contract.mjs";
3
3
  import { ToolContract } from "@warlock.js/ai";
4
4
 
5
- //#region ../ai-workspace/src/tools/run-shell.d.ts
5
+ //#region ../../packages/ai-workspace/src/tools/run-shell.d.ts
6
6
  /** Options for {@link makeRunShellTool}. */
7
7
  interface MakeRunShellToolOptions {
8
8
  /** Override the tool name exposed to the LLM (default `"run_shell"`). */
9
9
  name?: string;
10
+ /**
11
+ * Upper bound for a model-supplied `timeoutMs` on a foreground command —
12
+ * the model can lower the timeout, never raise it past this. The
13
+ * workspace passes its `shell.timeoutMs`.
14
+ */
15
+ maxTimeoutMs?: number;
16
+ /**
17
+ * Accept `background: true` and start the command as a background job
18
+ * (`ops.startShell`), returning a {@link RunShellBackgroundResult}. The
19
+ * workspace sets it when the policy has `shell.background`. Default
20
+ * `false`: a background request is refused.
21
+ */
22
+ background?: boolean;
10
23
  }
11
24
  /**
12
- * Build the `run_shell` tool — a {@link ToolContract} that runs a single
13
- * shell command through the policy-enforced {@link WorkspaceOps} layer.
25
+ * Build the `run_shell` tool — a {@link ToolContract} that runs a command
26
+ * line through the policy-enforced {@link WorkspaceOps} layer.
14
27
  *
15
- * The command's leading executable basename is gated against the shell
16
- * allow/deny policy by `ops.exec`; a blocked command throws a
17
- * `WorkspacePolicyError` which the `tool()` runtime catches and surfaces
18
- * in the result's `error` field (never a thrown run-killer), so the agent
19
- * reads the refusal as tool data and self-corrects. A command that runs
20
- * but exits non-zero is *not* an error — its `exitCode`/`stderr` come back
21
- * in `data` for the agent to inspect.
28
+ * Every command in the line is checked against the shell policy by
29
+ * `ops.exec`; a refused line throws a `WorkspacePolicyError` (with a
30
+ * `reason` and a message that says how to fix the command) which the
31
+ * `tool()` runtime catches and surfaces in the result's `error` field
32
+ * (never a thrown run-killer), so the agent reads the refusal as tool data
33
+ * and self-corrects. A command that runs but exits non-zero is *not* an
34
+ * error — its `exitCode`/`stderr` come back in `data` for the agent to
35
+ * inspect.
36
+ *
37
+ * The agent run's cancellation `signal` (from the dispatch `ctx`) is passed
38
+ * to `ops.exec` per invocation: a cancelled run never starts the command,
39
+ * and one cancelled mid-command kills its process tree (`cancelled: true`).
40
+ * A background command (`background: true`, when enabled) belongs to the
41
+ * workspace instead and keeps running after the call returns.
22
42
  *
23
43
  * @param ops - The policy-enforced operation layer to delegate `exec` to.
24
- * @param options - Optional tool-name override.
44
+ * @param options - Tool-name override, timeout cap and background support.
25
45
  *
26
46
  * @example
27
47
  * const runShell = makeRunShellTool(ops);
@@ -29,7 +49,13 @@ interface MakeRunShellToolOptions {
29
49
  * if (error) handleDenied(error);
30
50
  * else console.log(data.exitCode, data.stdout);
31
51
  */
32
- declare function makeRunShellTool(ops: WorkspaceOps, options?: MakeRunShellToolOptions): ToolContract<RunShellInput, RunShellResult>;
52
+ declare function makeRunShellTool(ops: WorkspaceOps, options?: MakeRunShellToolOptions & {
53
+ background?: false;
54
+ }): ToolContract<RunShellInput, RunShellResult>;
55
+ declare function makeRunShellTool(ops: WorkspaceOps, options: MakeRunShellToolOptions & {
56
+ background: true;
57
+ }): ToolContract<RunShellInput, RunShellResult | RunShellBackgroundResult>;
58
+ declare function makeRunShellTool(ops: WorkspaceOps, options?: MakeRunShellToolOptions): ToolContract<RunShellInput, RunShellResult | RunShellBackgroundResult>;
33
59
  //#endregion
34
60
  export { MakeRunShellToolOptions, makeRunShellTool };
35
61
  //# sourceMappingURL=run-shell.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"run-shell.d.mts","names":[],"sources":["../../../../../../../ai-workspace/src/tools/run-shell.ts"],"mappings":";;;;;;UAsDiB,uBAAA;;EAEf,IAAI;AAAA;;;;AAAA;AAwBN;;;;;;;;;;;;;;;;;iBAAgB,gBAAA,CACd,GAAA,EAAK,YAAA,EACL,OAAA,GAAU,uBAAA,GACT,YAAA,CAAa,aAAA,EAAe,cAAA"}
1
+ {"version":3,"file":"run-shell.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/run-shell.ts"],"mappings":";;;;;;UA2EiB,uBAAA;;EAEf,IAAA;EAFe;;;;;EAQf,YAAA;EAOA;;AAAU;AAwCZ;;;EAxCE,UAAA;AAAA;;;;;;;;;;;;;;;AA2C2C;AAC7C;;;;;;;;;;;;;iBAJgB,gBAAA,CACd,GAAA,EAAK,YAAA,EACL,OAAA,GAAU,uBAAA;EAA4B,UAAA;AAAA,IACrC,YAAA,CAAa,aAAA,EAAe,cAAA;AAAA,iBACf,gBAAA,CACd,GAAA,EAAK,YAAA,EACL,OAAA,EAAS,uBAAA;EAA4B,UAAA;AAAA,IACpC,YAAA,CAAa,aAAA,EAAe,cAAA,GAAiB,wBAAA;AAAA,iBAChC,gBAAA,CACd,GAAA,EAAK,YAAA,EACL,OAAA,GAAU,uBAAA,GACT,YAAA,CAAa,aAAA,EAAe,cAAA,GAAiB,wBAAA"}
@@ -1,62 +1,75 @@
1
+ import { WorkspacePolicyError } from "../errors.mjs";
2
+ import { withJsonSchema } from "./schema.mjs";
3
+ import { runShellJsonSchema } from "./json-schemas.mjs";
1
4
  import { tool } from "@warlock.js/ai";
2
5
 
3
- //#region ../ai-workspace/src/tools/run-shell.ts
6
+ //#region ../../packages/ai-workspace/src/tools/run-shell.ts
4
7
  /** The default tool name `run_shell` is exposed to the LLM under. */
5
8
  const DEFAULT_RUN_SHELL_TOOL_NAME = "run_shell";
6
9
  /**
7
10
  * Hand-rolled Standard Schema for {@link RunShellInput}. We validate the
8
11
  * model's arguments without a runtime schema dependency: `command` must be
9
- * a non-empty string, and `timeoutMs` (when present) a positive number.
10
- * Invalid args surface as a `SchemaValidationError` in the tool result's
11
- * `error` field rather than reaching `ops.exec`.
12
- */
13
- const runShellInputSchema = { "~standard": {
14
- version: 1,
15
- vendor: "@warlock.js/ai-workspace",
16
- validate: (value) => {
17
- if (typeof value !== "object" || value === null) return { issues: [{ message: "expected an object" }] };
18
- const candidate = value;
19
- if (typeof candidate.command !== "string" || candidate.command.length === 0) return { issues: [{
20
- message: "command must be a non-empty string",
21
- path: ["command"]
22
- }] };
23
- if (candidate.timeoutMs !== void 0 && (typeof candidate.timeoutMs !== "number" || candidate.timeoutMs <= 0)) return { issues: [{
24
- message: "timeoutMs must be a positive number",
25
- path: ["timeoutMs"]
26
- }] };
27
- const result = { command: candidate.command };
28
- if (candidate.timeoutMs !== void 0) result.timeoutMs = candidate.timeoutMs;
29
- return { value: result };
30
- }
31
- } };
32
- /**
33
- * Build the `run_shell` tool — a {@link ToolContract} that runs a single
34
- * shell command through the policy-enforced {@link WorkspaceOps} layer.
35
- *
36
- * The command's leading executable basename is gated against the shell
37
- * allow/deny policy by `ops.exec`; a blocked command throws a
38
- * `WorkspacePolicyError` which the `tool()` runtime catches and surfaces
39
- * in the result's `error` field (never a thrown run-killer), so the agent
40
- * reads the refusal as tool data and self-corrects. A command that runs
41
- * but exits non-zero is *not* an error — its `exitCode`/`stderr` come back
42
- * in `data` for the agent to inspect.
43
- *
44
- * @param ops - The policy-enforced operation layer to delegate `exec` to.
45
- * @param options - Optional tool-name override.
46
- *
47
- * @example
48
- * const runShell = makeRunShellTool(ops);
49
- * const { data, error } = await runShell.invoke({ command: "npm run build" });
50
- * if (error) handleDenied(error);
51
- * else console.log(data.exitCode, data.stdout);
12
+ * a non-empty string, `timeoutMs` (when present) a positive number and
13
+ * `background` (when present) a boolean. Invalid args surface as a
14
+ * `SchemaValidationError` in the tool result's `error` field rather than
15
+ * reaching `ops.exec`.
52
16
  */
17
+ function runShellInputSchema(background) {
18
+ return withJsonSchema({ "~standard": {
19
+ version: 1,
20
+ vendor: "@warlock.js/ai-workspace",
21
+ validate: (value) => {
22
+ if (typeof value !== "object" || value === null) return { issues: [{ message: "expected an object" }] };
23
+ const candidate = value;
24
+ if (typeof candidate.command !== "string" || candidate.command.length === 0) return { issues: [{
25
+ message: "command must be a non-empty string",
26
+ path: ["command"]
27
+ }] };
28
+ if (candidate.timeoutMs !== void 0 && (typeof candidate.timeoutMs !== "number" || candidate.timeoutMs <= 0)) return { issues: [{
29
+ message: "timeoutMs must be a positive number",
30
+ path: ["timeoutMs"]
31
+ }] };
32
+ if (candidate.background !== void 0 && typeof candidate.background !== "boolean") return { issues: [{
33
+ message: "background must be a boolean",
34
+ path: ["background"]
35
+ }] };
36
+ const result = { command: candidate.command };
37
+ if (typeof candidate.timeoutMs === "number") result.timeoutMs = candidate.timeoutMs;
38
+ if (candidate.background === true) result.background = true;
39
+ return { value: result };
40
+ }
41
+ } }, runShellJsonSchema(background));
42
+ }
43
+ /** The lower of the model's timeout and the cap (either may be absent). */
44
+ function cappedTimeout(requested, cap) {
45
+ if (requested === void 0) return cap;
46
+ return cap === void 0 ? requested : Math.min(requested, cap);
47
+ }
53
48
  function makeRunShellTool(ops, options) {
49
+ const background = options?.background === true;
54
50
  return tool({
55
51
  name: options?.name ?? DEFAULT_RUN_SHELL_TOOL_NAME,
56
- description: "Run a single shell command inside the workspace. The command's executable must be permitted by the shell policy; output is byte-capped and the run is time-limited. A non-zero exit code is returned as data, not an error.",
52
+ description: "Run a shell command inside the workspace. Every command in it must be permitted by the shell policy; a refusal explains how to fix the command. Output is byte-capped and the run is time-limited. A non-zero exit code is returned as data, not an error." + (background ? " Pass background: true for a long-running command (dev server, watcher): it returns an id at once; read its output with shell_output and stop it with shell_kill." : ""),
57
53
  action: (input) => `Running \`${input.command}\``,
58
- input: runShellInputSchema,
59
- execute: (input) => ops.exec(input.command, { timeoutMs: input.timeoutMs })
54
+ input: runShellInputSchema(background),
55
+ execute: async (input, ctx) => {
56
+ if (input.background === true) {
57
+ if (!background) throw new WorkspacePolicyError("Background commands are not enabled for this workspace; run the command without background.", {
58
+ type: "denied-command",
59
+ reason: "not-allowed",
60
+ command: input.command
61
+ });
62
+ return {
63
+ ...await ops.startShell(input.command, input.timeoutMs !== void 0 ? { timeoutMs: input.timeoutMs } : void 0),
64
+ background: true
65
+ };
66
+ }
67
+ const timeoutMs = cappedTimeout(input.timeoutMs, options?.maxTimeoutMs);
68
+ return ops.exec(input.command, {
69
+ ...timeoutMs !== void 0 ? { timeoutMs } : {},
70
+ ...ctx?.signal ? { signal: ctx.signal } : {}
71
+ });
72
+ }
60
73
  });
61
74
  }
62
75
 
@@ -1 +1 @@
1
- {"version":3,"file":"run-shell.mjs","names":[],"sources":["../../../../../../../ai-workspace/src/tools/run-shell.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { tool, type ToolContract } from \"@warlock.js/ai\";\nimport type {\n RunShellInput,\n RunShellResult,\n WorkspaceOps,\n} from \"../contracts\";\n\n/** The default tool name `run_shell` is exposed to the LLM under. */\nconst DEFAULT_RUN_SHELL_TOOL_NAME = \"run_shell\";\n\n/**\n * Hand-rolled Standard Schema for {@link RunShellInput}. We validate the\n * model's arguments without a runtime schema dependency: `command` must be\n * a non-empty string, and `timeoutMs` (when present) a positive number.\n * Invalid args surface as a `SchemaValidationError` in the tool result's\n * `error` field rather than reaching `ops.exec`.\n */\nconst runShellInputSchema: StandardSchemaV1<RunShellInput> = {\n \"~standard\": {\n version: 1,\n vendor: \"@warlock.js/ai-workspace\",\n validate: (value) => {\n if (typeof value !== \"object\" || value === null) {\n return { issues: [{ message: \"expected an object\" }] };\n }\n\n const candidate = value as Record<string, unknown>;\n\n if (typeof candidate.command !== \"string\" || candidate.command.length === 0) {\n return { issues: [{ message: \"command must be a non-empty string\", path: [\"command\"] }] };\n }\n\n if (\n candidate.timeoutMs !== undefined &&\n (typeof candidate.timeoutMs !== \"number\" || candidate.timeoutMs <= 0)\n ) {\n return {\n issues: [{ message: \"timeoutMs must be a positive number\", path: [\"timeoutMs\"] }],\n };\n }\n\n const result: RunShellInput = { command: candidate.command };\n\n if (candidate.timeoutMs !== undefined) {\n result.timeoutMs = candidate.timeoutMs as number;\n }\n\n return { value: result };\n },\n },\n};\n\n/** Options for {@link makeRunShellTool}. */\nexport interface MakeRunShellToolOptions {\n /** Override the tool name exposed to the LLM (default `\"run_shell\"`). */\n name?: string;\n}\n\n/**\n * Build the `run_shell` tool — a {@link ToolContract} that runs a single\n * shell command through the policy-enforced {@link WorkspaceOps} layer.\n *\n * The command's leading executable basename is gated against the shell\n * allow/deny policy by `ops.exec`; a blocked command throws a\n * `WorkspacePolicyError` which the `tool()` runtime catches and surfaces\n * in the result's `error` field (never a thrown run-killer), so the agent\n * reads the refusal as tool data and self-corrects. A command that runs\n * but exits non-zero is *not* an error — its `exitCode`/`stderr` come back\n * in `data` for the agent to inspect.\n *\n * @param ops - The policy-enforced operation layer to delegate `exec` to.\n * @param options - Optional tool-name override.\n *\n * @example\n * const runShell = makeRunShellTool(ops);\n * const { data, error } = await runShell.invoke({ command: \"npm run build\" });\n * if (error) handleDenied(error);\n * else console.log(data.exitCode, data.stdout);\n */\nexport function makeRunShellTool(\n ops: WorkspaceOps,\n options?: MakeRunShellToolOptions,\n): ToolContract<RunShellInput, RunShellResult> {\n return tool<RunShellInput, RunShellResult>({\n name: options?.name ?? DEFAULT_RUN_SHELL_TOOL_NAME,\n description:\n \"Run a single shell command inside the workspace. The command's \" +\n \"executable must be permitted by the shell policy; output is \" +\n \"byte-capped and the run is time-limited. A non-zero exit code is \" +\n \"returned as data, not an error.\",\n action: (input) => `Running \\`${input.command}\\``,\n input: runShellInputSchema,\n execute: (input) => ops.exec(input.command, { timeoutMs: input.timeoutMs }),\n });\n}\n"],"mappings":";;;;AASA,MAAM,8BAA8B;;;;;;;;AASpC,MAAM,sBAAuD,EAC3D,aAAa;CACX,SAAS;CACT,QAAQ;CACR,WAAW,UAAU;EACnB,IAAI,OAAO,UAAU,YAAY,UAAU,MACzC,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,qBAAqB,CAAC,EAAE;EAGvD,MAAM,YAAY;EAElB,IAAI,OAAO,UAAU,YAAY,YAAY,UAAU,QAAQ,WAAW,GACxE,OAAO,EAAE,QAAQ,CAAC;GAAE,SAAS;GAAsC,MAAM,CAAC,SAAS;EAAE,CAAC,EAAE;EAG1F,IACE,UAAU,cAAc,WACvB,OAAO,UAAU,cAAc,YAAY,UAAU,aAAa,IAEnE,OAAO,EACL,QAAQ,CAAC;GAAE,SAAS;GAAuC,MAAM,CAAC,WAAW;EAAE,CAAC,EAClF;EAGF,MAAM,SAAwB,EAAE,SAAS,UAAU,QAAQ;EAE3D,IAAI,UAAU,cAAc,QAC1B,OAAO,YAAY,UAAU;EAG/B,OAAO,EAAE,OAAO,OAAO;CACzB;AACF,EACF;;;;;;;;;;;;;;;;;;;;;;AA6BA,SAAgB,iBACd,KACA,SAC6C;CAC7C,OAAO,KAAoC;EACzC,MAAM,SAAS,QAAQ;EACvB,aACE;EAIF,SAAS,UAAU,aAAa,MAAM,QAAQ;EAC9C,OAAO;EACP,UAAU,UAAU,IAAI,KAAK,MAAM,SAAS,EAAE,WAAW,MAAM,UAAU,CAAC;CAC5E,CAAC;AACH"}
1
+ {"version":3,"file":"run-shell.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/run-shell.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { WorkspacePolicyError } from \"../errors\";\nimport type {\n RunShellBackgroundResult,\n RunShellInput,\n RunShellResult,\n WorkspaceOps,\n} from \"../contracts\";\nimport { runShellJsonSchema } from \"./json-schemas\";\nimport { withJsonSchema } from \"./schema\";\n\n/** The default tool name `run_shell` is exposed to the LLM under. */\nconst DEFAULT_RUN_SHELL_TOOL_NAME = \"run_shell\";\n\n/**\n * Hand-rolled Standard Schema for {@link RunShellInput}. We validate the\n * model's arguments without a runtime schema dependency: `command` must be\n * a non-empty string, `timeoutMs` (when present) a positive number and\n * `background` (when present) a boolean. Invalid args surface as a\n * `SchemaValidationError` in the tool result's `error` field rather than\n * reaching `ops.exec`.\n */\nfunction runShellInputSchema(background: boolean): StandardSchemaV1<RunShellInput> {\n const schema: StandardSchemaV1<RunShellInput> = {\n \"~standard\": {\n version: 1,\n vendor: \"@warlock.js/ai-workspace\",\n validate: (value) => {\n if (typeof value !== \"object\" || value === null) {\n return { issues: [{ message: \"expected an object\" }] };\n }\n\n const candidate = value as Record<string, unknown>;\n\n if (typeof candidate.command !== \"string\" || candidate.command.length === 0) {\n return {\n issues: [{ message: \"command must be a non-empty string\", path: [\"command\"] }],\n };\n }\n\n if (\n candidate.timeoutMs !== undefined &&\n (typeof candidate.timeoutMs !== \"number\" || candidate.timeoutMs <= 0)\n ) {\n return {\n issues: [{ message: \"timeoutMs must be a positive number\", path: [\"timeoutMs\"] }],\n };\n }\n\n if (candidate.background !== undefined && typeof candidate.background !== \"boolean\") {\n return {\n issues: [{ message: \"background must be a boolean\", path: [\"background\"] }],\n };\n }\n\n const result: RunShellInput = { command: candidate.command };\n\n if (typeof candidate.timeoutMs === \"number\") {\n result.timeoutMs = candidate.timeoutMs;\n }\n\n if (candidate.background === true) {\n result.background = true;\n }\n\n return { value: result };\n },\n },\n };\n\n return withJsonSchema(schema, runShellJsonSchema(background));\n}\n\n/** Options for {@link makeRunShellTool}. */\nexport interface MakeRunShellToolOptions {\n /** Override the tool name exposed to the LLM (default `\"run_shell\"`). */\n name?: string;\n /**\n * Upper bound for a model-supplied `timeoutMs` on a foreground command —\n * the model can lower the timeout, never raise it past this. The\n * workspace passes its `shell.timeoutMs`.\n */\n maxTimeoutMs?: number;\n /**\n * Accept `background: true` and start the command as a background job\n * (`ops.startShell`), returning a {@link RunShellBackgroundResult}. The\n * workspace sets it when the policy has `shell.background`. Default\n * `false`: a background request is refused.\n */\n background?: boolean;\n}\n\n/** The lower of the model's timeout and the cap (either may be absent). */\nfunction cappedTimeout(requested: number | undefined, cap: number | undefined): number | undefined {\n if (requested === undefined) {\n return cap;\n }\n\n return cap === undefined ? requested : Math.min(requested, cap);\n}\n\n/**\n * Build the `run_shell` tool — a {@link ToolContract} that runs a command\n * line through the policy-enforced {@link WorkspaceOps} layer.\n *\n * Every command in the line is checked against the shell policy by\n * `ops.exec`; a refused line throws a `WorkspacePolicyError` (with a\n * `reason` and a message that says how to fix the command) which the\n * `tool()` runtime catches and surfaces in the result's `error` field\n * (never a thrown run-killer), so the agent reads the refusal as tool data\n * and self-corrects. A command that runs but exits non-zero is *not* an\n * error — its `exitCode`/`stderr` come back in `data` for the agent to\n * inspect.\n *\n * The agent run's cancellation `signal` (from the dispatch `ctx`) is passed\n * to `ops.exec` per invocation: a cancelled run never starts the command,\n * and one cancelled mid-command kills its process tree (`cancelled: true`).\n * A background command (`background: true`, when enabled) belongs to the\n * workspace instead and keeps running after the call returns.\n *\n * @param ops - The policy-enforced operation layer to delegate `exec` to.\n * @param options - Tool-name override, timeout cap and background support.\n *\n * @example\n * const runShell = makeRunShellTool(ops);\n * const { data, error } = await runShell.invoke({ command: \"npm run build\" });\n * if (error) handleDenied(error);\n * else console.log(data.exitCode, data.stdout);\n */\nexport function makeRunShellTool(\n ops: WorkspaceOps,\n options?: MakeRunShellToolOptions & { background?: false },\n): ToolContract<RunShellInput, RunShellResult>;\nexport function makeRunShellTool(\n ops: WorkspaceOps,\n options: MakeRunShellToolOptions & { background: true },\n): ToolContract<RunShellInput, RunShellResult | RunShellBackgroundResult>;\nexport function makeRunShellTool(\n ops: WorkspaceOps,\n options?: MakeRunShellToolOptions,\n): ToolContract<RunShellInput, RunShellResult | RunShellBackgroundResult>;\nexport function makeRunShellTool(\n ops: WorkspaceOps,\n options?: MakeRunShellToolOptions,\n): ToolContract<RunShellInput, RunShellResult | RunShellBackgroundResult> {\n const background = options?.background === true;\n\n return tool<RunShellInput, RunShellResult | RunShellBackgroundResult>({\n name: options?.name ?? DEFAULT_RUN_SHELL_TOOL_NAME,\n description:\n \"Run a shell command inside the workspace. Every command in it must be \" +\n \"permitted by the shell policy; a refusal explains how to fix the \" +\n \"command. Output is byte-capped and the run is time-limited. A \" +\n \"non-zero exit code is returned as data, not an error.\" +\n (background\n ? \" Pass background: true for a long-running command (dev server, \" +\n \"watcher): it returns an id at once; read its output with \" +\n \"shell_output and stop it with shell_kill.\"\n : \"\"),\n action: (input) => `Running \\`${input.command}\\``,\n input: runShellInputSchema(background),\n // The run's signal belongs to this one invocation: it is read from the\n // dispatch ctx on every call and never stored on the tool or workspace.\n execute: async (input, ctx) => {\n if (input.background === true) {\n if (!background) {\n throw new WorkspacePolicyError(\n \"Background commands are not enabled for this workspace; run the command without background.\",\n { type: \"denied-command\", reason: \"not-allowed\", command: input.command },\n );\n }\n\n const job = await ops.startShell(\n input.command,\n input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : undefined,\n );\n\n return { ...job, background: true };\n }\n\n const timeoutMs = cappedTimeout(input.timeoutMs, options?.maxTimeoutMs);\n\n return ops.exec(input.command, {\n ...(timeoutMs !== undefined ? { timeoutMs } : {}),\n ...(ctx?.signal ? { signal: ctx.signal } : {}),\n });\n },\n });\n}\n"],"mappings":";;;;;;;AAaA,MAAM,8BAA8B;;;;;;;;;AAUpC,SAAS,oBAAoB,YAAsD;CAgDjF,OAAO,eAAe,EA9CpB,aAAa;EACX,SAAS;EACT,QAAQ;EACR,WAAW,UAAU;GACnB,IAAI,OAAO,UAAU,YAAY,UAAU,MACzC,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,qBAAqB,CAAC,EAAE;GAGvD,MAAM,YAAY;GAElB,IAAI,OAAO,UAAU,YAAY,YAAY,UAAU,QAAQ,WAAW,GACxE,OAAO,EACL,QAAQ,CAAC;IAAE,SAAS;IAAsC,MAAM,CAAC,SAAS;GAAE,CAAC,EAC/E;GAGF,IACE,UAAU,cAAc,WACvB,OAAO,UAAU,cAAc,YAAY,UAAU,aAAa,IAEnE,OAAO,EACL,QAAQ,CAAC;IAAE,SAAS;IAAuC,MAAM,CAAC,WAAW;GAAE,CAAC,EAClF;GAGF,IAAI,UAAU,eAAe,UAAa,OAAO,UAAU,eAAe,WACxE,OAAO,EACL,QAAQ,CAAC;IAAE,SAAS;IAAgC,MAAM,CAAC,YAAY;GAAE,CAAC,EAC5E;GAGF,MAAM,SAAwB,EAAE,SAAS,UAAU,QAAQ;GAE3D,IAAI,OAAO,UAAU,cAAc,UACjC,OAAO,YAAY,UAAU;GAG/B,IAAI,UAAU,eAAe,MAC3B,OAAO,aAAa;GAGtB,OAAO,EAAE,OAAO,OAAO;EACzB;CACF,EAGyB,GAAG,mBAAmB,UAAU,CAAC;AAC9D;;AAsBA,SAAS,cAAc,WAA+B,KAA6C;CACjG,IAAI,cAAc,QAChB,OAAO;CAGT,OAAO,QAAQ,SAAY,YAAY,KAAK,IAAI,WAAW,GAAG;AAChE;AA0CA,SAAgB,iBACd,KACA,SACwE;CACxE,MAAM,aAAa,SAAS,eAAe;CAE3C,OAAO,KAA+D;EACpE,MAAM,SAAS,QAAQ;EACvB,aACE,gQAIC,aACG,sKAGA;EACN,SAAS,UAAU,aAAa,MAAM,QAAQ;EAC9C,OAAO,oBAAoB,UAAU;EAGrC,SAAS,OAAO,OAAO,QAAQ;GAC7B,IAAI,MAAM,eAAe,MAAM;IAC7B,IAAI,CAAC,YACH,MAAM,IAAI,qBACR,+FACA;KAAE,MAAM;KAAkB,QAAQ;KAAe,SAAS,MAAM;IAAQ,CAC1E;IAQF,OAAO;KAAE,GAAG,MALM,IAAI,WACpB,MAAM,SACN,MAAM,cAAc,SAAY,EAAE,WAAW,MAAM,UAAU,IAAI,MACnE;KAEiB,YAAY;IAAK;GACpC;GAEA,MAAM,YAAY,cAAc,MAAM,WAAW,SAAS,YAAY;GAEtE,OAAO,IAAI,KAAK,MAAM,SAAS;IAC7B,GAAI,cAAc,SAAY,EAAE,UAAU,IAAI,CAAC;IAC/C,GAAI,KAAK,SAAS,EAAE,QAAQ,IAAI,OAAO,IAAI,CAAC;GAC9C,CAAC;EACH;CACF,CAAC;AACH"}
@@ -1,8 +1,9 @@
1
1
  import { RunShellResult, RunTestsInput } from "../contracts/tool-io.type.mjs";
2
+ import { WorkspaceShellInterpreter } from "../contracts/workspace-policy.type.mjs";
2
3
  import { WorkspaceOps } from "../contracts/workspace-ops.contract.mjs";
3
4
  import { ToolContract } from "@warlock.js/ai";
4
5
 
5
- //#region ../ai-workspace/src/tools/run-tests.d.ts
6
+ //#region ../../packages/ai-workspace/src/tools/run-tests.d.ts
6
7
  /** Options for {@link makeRunTestsTool}. */
7
8
  interface MakeRunTestsToolOptions {
8
9
  /** Override the tool name exposed to the LLM (default `"run_tests"`). */
@@ -13,6 +14,13 @@ interface MakeRunTestsToolOptions {
13
14
  * quoted argument.
14
15
  */
15
16
  command?: string;
17
+ /**
18
+ * The workspace's `shell.interpreter`, so the pattern is quoted for the
19
+ * way the line runs: double quotes in argv mode (the default), single
20
+ * quotes for `/bin/sh` (so `$` and backticks in a pattern stay literal),
21
+ * double quotes for cmd.exe.
22
+ */
23
+ interpreter?: WorkspaceShellInterpreter;
16
24
  }
17
25
  /**
18
26
  * Build the `run_tests` tool — a {@link ToolContract} convenience over
@@ -27,7 +35,8 @@ interface MakeRunTestsToolOptions {
27
35
  * data, never a second command. Like `run_shell`, the resolved command's
28
36
  * executable is gated by the shell policy — a denial surfaces in the
29
37
  * result's `error` field — and a non-zero exit (failing tests) comes back
30
- * as `data` for the agent to read and fix.
38
+ * as `data` for the agent to read and fix. The agent run's cancellation
39
+ * `signal` is threaded per invocation, exactly as for `run_shell`.
31
40
  *
32
41
  * @param ops - The policy-enforced operation layer to delegate `exec` to.
33
42
  * @param options - Optional tool-name and base-command overrides.
@@ -1 +1 @@
1
- {"version":3,"file":"run-tests.d.mts","names":[],"sources":["../../../../../../../ai-workspace/src/tools/run-tests.ts"],"mappings":";;;;;;UAmEiB,uBAAA;;EAEf,IAAA;EAFe;;;;AAQR;EAAP,OAAO;AAAA;;;;;;;;;;;;;;;;;AA6BoC;;;;;;;iBAH7B,gBAAA,CACd,GAAA,EAAK,YAAA,EACL,OAAA,GAAU,uBAAA,GACT,YAAA,CAAa,aAAA,EAAe,cAAA"}
1
+ {"version":3,"file":"run-tests.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/run-tests.ts"],"mappings":";;;;;;;UAyEiB,uBAAA;;EAEf,IAAA;;AAFF;;;;EAQE,OAAA;EAAA;;;;AAOuC;AAoCzC;EApCE,WAAA,GAAc,yBAAyB;AAAA;;;;;;;;;;;;;;;;AAuCI;;;;;;;;;iBAH7B,gBAAA,CACd,GAAA,EAAK,YAAA,EACL,OAAA,GAAU,uBAAA,GACT,YAAA,CAAa,aAAA,EAAe,cAAA"}
@@ -1,6 +1,8 @@
1
+ import { withJsonSchema } from "./schema.mjs";
2
+ import { RUN_TESTS_JSON_SCHEMA } from "./json-schemas.mjs";
1
3
  import { tool } from "@warlock.js/ai";
2
4
 
3
- //#region ../ai-workspace/src/tools/run-tests.ts
5
+ //#region ../../packages/ai-workspace/src/tools/run-tests.ts
4
6
  /** The default tool name `run_tests` is exposed to the LLM under. */
5
7
  const DEFAULT_RUN_TESTS_TOOL_NAME = "run_tests";
6
8
  /** The default command run when no `command` override is configured. */
@@ -14,7 +16,7 @@ const DEFAULT_TEST_COMMAND = "npm test";
14
16
  * happens without a runtime schema dependency, mirroring the wider tool
15
17
  * layer.
16
18
  */
17
- const runTestsInputSchema = { "~standard": {
19
+ const runTestsInputSchema = withJsonSchema({ "~standard": {
18
20
  version: 1,
19
21
  vendor: "@warlock.js/ai-workspace",
20
22
  validate: (value) => {
@@ -33,7 +35,12 @@ const runTestsInputSchema = { "~standard": {
33
35
  if (candidate.pattern !== void 0) result.pattern = candidate.pattern;
34
36
  return { value: result };
35
37
  }
36
- } };
38
+ } }, RUN_TESTS_JSON_SCHEMA);
39
+ /** Quote the test pattern as one literal argument for the line's interpreter. */
40
+ function quotePattern(pattern, interpreter) {
41
+ if (interpreter === "sh" && process.platform !== "win32") return `'${pattern.replace(/'/g, "'\\''")}'`;
42
+ return `"${pattern}"`;
43
+ }
37
44
  /**
38
45
  * Build the `run_tests` tool — a {@link ToolContract} convenience over
39
46
  * `run_shell` that runs the workspace's configured test command through
@@ -47,7 +54,8 @@ const runTestsInputSchema = { "~standard": {
47
54
  * data, never a second command. Like `run_shell`, the resolved command's
48
55
  * executable is gated by the shell policy — a denial surfaces in the
49
56
  * result's `error` field — and a non-zero exit (failing tests) comes back
50
- * as `data` for the agent to read and fix.
57
+ * as `data` for the agent to read and fix. The agent run's cancellation
58
+ * `signal` is threaded per invocation, exactly as for `run_shell`.
51
59
  *
52
60
  * @param ops - The policy-enforced operation layer to delegate `exec` to.
53
61
  * @param options - Optional tool-name and base-command overrides.
@@ -64,9 +72,9 @@ function makeRunTestsTool(ops, options) {
64
72
  description: "Run the workspace's test suite, optionally narrowed to a path or name pattern forwarded to the test runner. Failing tests return a non-zero exit code as data, not an error.",
65
73
  action: (input) => input.pattern ? `Running tests matching "${input.pattern}"` : "Running tests",
66
74
  input: runTestsInputSchema,
67
- execute: (input) => {
68
- const command = input.pattern ? `${baseCommand} "${input.pattern}"` : baseCommand;
69
- return ops.exec(command);
75
+ execute: (input, ctx) => {
76
+ const command = input.pattern ? `${baseCommand} ${quotePattern(input.pattern, options?.interpreter ?? "none")}` : baseCommand;
77
+ return ops.exec(command, ctx?.signal ? { signal: ctx.signal } : void 0);
70
78
  }
71
79
  });
72
80
  }
@@ -1 +1 @@
1
- {"version":3,"file":"run-tests.mjs","names":[],"sources":["../../../../../../../ai-workspace/src/tools/run-tests.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { tool, type ToolContract } from \"@warlock.js/ai\";\nimport type {\n RunShellResult,\n RunTestsInput,\n WorkspaceOps,\n} from \"../contracts\";\n\n/** The default tool name `run_tests` is exposed to the LLM under. */\nconst DEFAULT_RUN_TESTS_TOOL_NAME = \"run_tests\";\n\n/** The default command run when no `command` override is configured. */\nconst DEFAULT_TEST_COMMAND = \"npm test\";\n\n/**\n * Hand-rolled Standard Schema for {@link RunTestsInput}. `pattern` is the\n * only field and is optional; when present it must be a string without\n * double quotes or newlines — the pattern is forwarded to the runner as a\n * single double-quoted argument, and those characters would break out of\n * the quoting (i.e. inject extra arguments or commands). Validation\n * happens without a runtime schema dependency, mirroring the wider tool\n * layer.\n */\nconst runTestsInputSchema: StandardSchemaV1<RunTestsInput> = {\n \"~standard\": {\n version: 1,\n vendor: \"@warlock.js/ai-workspace\",\n validate: (value) => {\n // A no-argument call (the common case) is valid and runs the bare\n // test command.\n if (value === undefined || value === null) {\n return { value: {} };\n }\n\n if (typeof value !== \"object\") {\n return { issues: [{ message: \"expected an object\" }] };\n }\n\n const candidate = value as Record<string, unknown>;\n\n if (candidate.pattern !== undefined && typeof candidate.pattern !== \"string\") {\n return { issues: [{ message: \"pattern must be a string\", path: [\"pattern\"] }] };\n }\n\n if (typeof candidate.pattern === \"string\" && /[\"\\r\\n]/.test(candidate.pattern)) {\n return {\n issues: [\n {\n message: \"pattern must not contain double quotes or newlines\",\n path: [\"pattern\"],\n },\n ],\n };\n }\n\n const result: RunTestsInput = {};\n\n if (candidate.pattern !== undefined) {\n result.pattern = candidate.pattern as string;\n }\n\n return { value: result };\n },\n },\n};\n\n/** Options for {@link makeRunTestsTool}. */\nexport interface MakeRunTestsToolOptions {\n /** Override the tool name exposed to the LLM (default `\"run_tests\"`). */\n name?: string;\n /**\n * The base test command to run (default `\"npm test\"`). When the model\n * supplies a `pattern`, it is appended to this command as a single\n * quoted argument.\n */\n command?: string;\n}\n\n/**\n * Build the `run_tests` tool — a {@link ToolContract} convenience over\n * `run_shell` that runs the workspace's configured test command through\n * the policy-enforced {@link WorkspaceOps} layer.\n *\n * The base command defaults to `\"npm test\"` and can be overridden via\n * `options.command`. When the model passes a `pattern`, it is appended to\n * the command as a **single quoted argument** — a path/suite filter the\n * tokenizer hands to the runner as one argv element (e.g.\n * `npm test \"src/cart\"`), so shell metacharacters inside it are literal\n * data, never a second command. Like `run_shell`, the resolved command's\n * executable is gated by the shell policy — a denial surfaces in the\n * result's `error` field — and a non-zero exit (failing tests) comes back\n * as `data` for the agent to read and fix.\n *\n * @param ops - The policy-enforced operation layer to delegate `exec` to.\n * @param options - Optional tool-name and base-command overrides.\n *\n * @example\n * const runTests = makeRunTestsTool(ops, { command: \"pnpm test\" });\n * const { data } = await runTests.invoke({ pattern: \"cart-total\" });\n * if (data.exitCode !== 0) inspect(data.stderr);\n */\nexport function makeRunTestsTool(\n ops: WorkspaceOps,\n options?: MakeRunTestsToolOptions,\n): ToolContract<RunTestsInput, RunShellResult> {\n const baseCommand = options?.command ?? DEFAULT_TEST_COMMAND;\n\n return tool<RunTestsInput, RunShellResult>({\n name: options?.name ?? DEFAULT_RUN_TESTS_TOOL_NAME,\n description:\n \"Run the workspace's test suite, optionally narrowed to a path or \" +\n \"name pattern forwarded to the test runner. Failing tests return a \" +\n \"non-zero exit code as data, not an error.\",\n action: (input) =>\n input.pattern ? `Running tests matching \"${input.pattern}\"` : \"Running tests\",\n input: runTestsInputSchema,\n execute: (input) => {\n // The pattern rides as ONE double-quoted token (quotes/newlines are\n // rejected by the schema), so it reaches the runner as a single argv\n // element and can never smuggle in additional commands or arguments.\n const command = input.pattern ? `${baseCommand} \"${input.pattern}\"` : baseCommand;\n\n return ops.exec(command);\n },\n });\n}\n"],"mappings":";;;;AASA,MAAM,8BAA8B;;AAGpC,MAAM,uBAAuB;;;;;;;;;;AAW7B,MAAM,sBAAuD,EAC3D,aAAa;CACX,SAAS;CACT,QAAQ;CACR,WAAW,UAAU;EAGnB,IAAI,UAAU,UAAa,UAAU,MACnC,OAAO,EAAE,OAAO,CAAC,EAAE;EAGrB,IAAI,OAAO,UAAU,UACnB,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,qBAAqB,CAAC,EAAE;EAGvD,MAAM,YAAY;EAElB,IAAI,UAAU,YAAY,UAAa,OAAO,UAAU,YAAY,UAClE,OAAO,EAAE,QAAQ,CAAC;GAAE,SAAS;GAA4B,MAAM,CAAC,SAAS;EAAE,CAAC,EAAE;EAGhF,IAAI,OAAO,UAAU,YAAY,YAAY,UAAU,KAAK,UAAU,OAAO,GAC3E,OAAO,EACL,QAAQ,CACN;GACE,SAAS;GACT,MAAM,CAAC,SAAS;EAClB,CACF,EACF;EAGF,MAAM,SAAwB,CAAC;EAE/B,IAAI,UAAU,YAAY,QACxB,OAAO,UAAU,UAAU;EAG7B,OAAO,EAAE,OAAO,OAAO;CACzB;AACF,EACF;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,SAAgB,iBACd,KACA,SAC6C;CAC7C,MAAM,cAAc,SAAS,WAAW;CAExC,OAAO,KAAoC;EACzC,MAAM,SAAS,QAAQ;EACvB,aACE;EAGF,SAAS,UACP,MAAM,UAAU,2BAA2B,MAAM,QAAQ,KAAK;EAChE,OAAO;EACP,UAAU,UAAU;GAIlB,MAAM,UAAU,MAAM,UAAU,GAAG,YAAY,IAAI,MAAM,QAAQ,KAAK;GAEtE,OAAO,IAAI,KAAK,OAAO;EACzB;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"run-tests.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/run-tests.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { tool, type ToolContract } from \"@warlock.js/ai\";\nimport type {\n RunShellResult,\n RunTestsInput,\n WorkspaceOps,\n WorkspaceShellInterpreter,\n} from \"../contracts\";\nimport { RUN_TESTS_JSON_SCHEMA } from \"./json-schemas\";\nimport { withJsonSchema } from \"./schema\";\n\n/** The default tool name `run_tests` is exposed to the LLM under. */\nconst DEFAULT_RUN_TESTS_TOOL_NAME = \"run_tests\";\n\n/** The default command run when no `command` override is configured. */\nconst DEFAULT_TEST_COMMAND = \"npm test\";\n\n/**\n * Hand-rolled Standard Schema for {@link RunTestsInput}. `pattern` is the\n * only field and is optional; when present it must be a string without\n * double quotes or newlines — the pattern is forwarded to the runner as a\n * single double-quoted argument, and those characters would break out of\n * the quoting (i.e. inject extra arguments or commands). Validation\n * happens without a runtime schema dependency, mirroring the wider tool\n * layer.\n */\nconst runTestsInputSchema: StandardSchemaV1<RunTestsInput> = withJsonSchema<RunTestsInput>(\n {\n \"~standard\": {\n version: 1,\n vendor: \"@warlock.js/ai-workspace\",\n validate: (value) => {\n // A no-argument call (the common case) is valid and runs the bare\n // test command.\n if (value === undefined || value === null) {\n return { value: {} };\n }\n\n if (typeof value !== \"object\") {\n return { issues: [{ message: \"expected an object\" }] };\n }\n\n const candidate = value as Record<string, unknown>;\n\n if (candidate.pattern !== undefined && typeof candidate.pattern !== \"string\") {\n return { issues: [{ message: \"pattern must be a string\", path: [\"pattern\"] }] };\n }\n\n if (typeof candidate.pattern === \"string\" && /[\"\\r\\n]/.test(candidate.pattern)) {\n return {\n issues: [\n {\n message: \"pattern must not contain double quotes or newlines\",\n path: [\"pattern\"],\n },\n ],\n };\n }\n\n const result: RunTestsInput = {};\n\n if (candidate.pattern !== undefined) {\n result.pattern = candidate.pattern as string;\n }\n\n return { value: result };\n },\n },\n },\n RUN_TESTS_JSON_SCHEMA,\n);\n\n/** Options for {@link makeRunTestsTool}. */\nexport interface MakeRunTestsToolOptions {\n /** Override the tool name exposed to the LLM (default `\"run_tests\"`). */\n name?: string;\n /**\n * The base test command to run (default `\"npm test\"`). When the model\n * supplies a `pattern`, it is appended to this command as a single\n * quoted argument.\n */\n command?: string;\n /**\n * The workspace's `shell.interpreter`, so the pattern is quoted for the\n * way the line runs: double quotes in argv mode (the default), single\n * quotes for `/bin/sh` (so `$` and backticks in a pattern stay literal),\n * double quotes for cmd.exe.\n */\n interpreter?: WorkspaceShellInterpreter;\n}\n\n/** Quote the test pattern as one literal argument for the line's interpreter. */\nfunction quotePattern(pattern: string, interpreter: WorkspaceShellInterpreter): string {\n if (interpreter === \"sh\" && process.platform !== \"win32\") {\n return `'${pattern.replace(/'/g, \"'\\\\''\")}'`;\n }\n\n return `\"${pattern}\"`;\n}\n\n/**\n * Build the `run_tests` tool — a {@link ToolContract} convenience over\n * `run_shell` that runs the workspace's configured test command through\n * the policy-enforced {@link WorkspaceOps} layer.\n *\n * The base command defaults to `\"npm test\"` and can be overridden via\n * `options.command`. When the model passes a `pattern`, it is appended to\n * the command as a **single quoted argument** — a path/suite filter the\n * tokenizer hands to the runner as one argv element (e.g.\n * `npm test \"src/cart\"`), so shell metacharacters inside it are literal\n * data, never a second command. Like `run_shell`, the resolved command's\n * executable is gated by the shell policy — a denial surfaces in the\n * result's `error` field — and a non-zero exit (failing tests) comes back\n * as `data` for the agent to read and fix. The agent run's cancellation\n * `signal` is threaded per invocation, exactly as for `run_shell`.\n *\n * @param ops - The policy-enforced operation layer to delegate `exec` to.\n * @param options - Optional tool-name and base-command overrides.\n *\n * @example\n * const runTests = makeRunTestsTool(ops, { command: \"pnpm test\" });\n * const { data } = await runTests.invoke({ pattern: \"cart-total\" });\n * if (data.exitCode !== 0) inspect(data.stderr);\n */\nexport function makeRunTestsTool(\n ops: WorkspaceOps,\n options?: MakeRunTestsToolOptions,\n): ToolContract<RunTestsInput, RunShellResult> {\n const baseCommand = options?.command ?? DEFAULT_TEST_COMMAND;\n\n return tool<RunTestsInput, RunShellResult>({\n name: options?.name ?? DEFAULT_RUN_TESTS_TOOL_NAME,\n description:\n \"Run the workspace's test suite, optionally narrowed to a path or \" +\n \"name pattern forwarded to the test runner. Failing tests return a \" +\n \"non-zero exit code as data, not an error.\",\n action: (input) =>\n input.pattern ? `Running tests matching \"${input.pattern}\"` : \"Running tests\",\n input: runTestsInputSchema,\n execute: (input, ctx) => {\n // The pattern rides as ONE quoted token (double quotes and newlines are\n // rejected by the schema; under /bin/sh it is single-quoted, so `$` and\n // backticks stay literal), so it reaches the runner as a single argv\n // element and can never smuggle in additional commands or arguments.\n const command = input.pattern\n ? `${baseCommand} ${quotePattern(input.pattern, options?.interpreter ?? \"none\")}`\n : baseCommand;\n\n return ops.exec(command, ctx?.signal ? { signal: ctx.signal } : undefined);\n },\n });\n}\n"],"mappings":";;;;;;AAYA,MAAM,8BAA8B;;AAGpC,MAAM,uBAAuB;;;;;;;;;;AAW7B,MAAM,sBAAuD,eAC3D,EACE,aAAa;CACX,SAAS;CACT,QAAQ;CACR,WAAW,UAAU;EAGnB,IAAI,UAAU,UAAa,UAAU,MACnC,OAAO,EAAE,OAAO,CAAC,EAAE;EAGrB,IAAI,OAAO,UAAU,UACnB,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,qBAAqB,CAAC,EAAE;EAGvD,MAAM,YAAY;EAElB,IAAI,UAAU,YAAY,UAAa,OAAO,UAAU,YAAY,UAClE,OAAO,EAAE,QAAQ,CAAC;GAAE,SAAS;GAA4B,MAAM,CAAC,SAAS;EAAE,CAAC,EAAE;EAGhF,IAAI,OAAO,UAAU,YAAY,YAAY,UAAU,KAAK,UAAU,OAAO,GAC3E,OAAO,EACL,QAAQ,CACN;GACE,SAAS;GACT,MAAM,CAAC,SAAS;EAClB,CACF,EACF;EAGF,MAAM,SAAwB,CAAC;EAE/B,IAAI,UAAU,YAAY,QACxB,OAAO,UAAU,UAAU;EAG7B,OAAO,EAAE,OAAO,OAAO;CACzB;AACF,EACF,GACA,qBACF;;AAsBA,SAAS,aAAa,SAAiB,aAAgD;CACrF,IAAI,gBAAgB,QAAQ,QAAQ,aAAa,SAC/C,OAAO,IAAI,QAAQ,QAAQ,MAAM,OAAO,EAAE;CAG5C,OAAO,IAAI,QAAQ;AACrB;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,iBACd,KACA,SAC6C;CAC7C,MAAM,cAAc,SAAS,WAAW;CAExC,OAAO,KAAoC;EACzC,MAAM,SAAS,QAAQ;EACvB,aACE;EAGF,SAAS,UACP,MAAM,UAAU,2BAA2B,MAAM,QAAQ,KAAK;EAChE,OAAO;EACP,UAAU,OAAO,QAAQ;GAKvB,MAAM,UAAU,MAAM,UAClB,GAAG,YAAY,GAAG,aAAa,MAAM,SAAS,SAAS,eAAe,MAAM,MAC5E;GAEJ,OAAO,IAAI,KAAK,SAAS,KAAK,SAAS,EAAE,QAAQ,IAAI,OAAO,IAAI,MAAS;EAC3E;CACF,CAAC;AACH"}
@@ -1,4 +1,4 @@
1
- //#region ../ai-workspace/src/tools/schema.ts
1
+ //#region ../../packages/ai-workspace/src/tools/schema.ts
2
2
  /**
3
3
  * Tiny, dependency-free [Standard Schema](https://standardschema.dev)
4
4
  * builders for the workspace tools' input validation. The package pins
@@ -15,6 +15,10 @@
15
15
  */
16
16
  /** The vendor tag stamped on every issue these builders produce. */
17
17
  const VENDOR = "ai-workspace";
18
+ /** Attach a JSON Schema description to a hand-rolled Standard Schema. */
19
+ function withJsonSchema(schema, jsonSchema) {
20
+ return Object.assign(schema, { jsonSchema });
21
+ }
18
22
  /** Required string field — rejects anything that is not a string. */
19
23
  function stringField() {
20
24
  return (value, key) => {
@@ -65,24 +69,25 @@ function optionalBooleanField() {
65
69
  };
66
70
  }
67
71
  /**
68
- * Build a {@link StandardSchemaV1} for a flat object whose every property
69
- * is validated by a {@link FieldValidator}. The input must be a non-null
70
- * object; each declared field is validated and the (possibly coerced)
71
- * values are collected into the typed result. All field issues are merged
72
- * so the caller sees every problem at once.
73
- *
74
- * `T` is constrained to `object` rather than `Record<string, unknown>` so
75
- * the tool IO `interface`s (which carry no implicit string index
76
- * signature) satisfy it directly — only the declared keys in `shape` are
77
- * ever read, so a string index signature is never required.
78
- *
79
- * @example
80
- * const schema = objectSchema<{ path: string; limit?: number }>({
81
- * path: stringField(),
82
- * limit: optionalNumberField(),
83
- * });
72
+ * Optional array-of-strings field — accepts `undefined` or an array whose
73
+ * every element is a string (at most `maxItems` of them).
84
74
  */
85
- function objectSchema(shape) {
75
+ function optionalStringArrayField(maxItems = 50) {
76
+ return (value, key) => {
77
+ if (value === void 0) return { value: void 0 };
78
+ if (Array.isArray(value) && value.length <= maxItems && value.every((item) => typeof item === "string")) return { value };
79
+ return { issues: [{
80
+ message: `"${key}" must be an array of at most ${maxItems} strings when provided`,
81
+ path: [key]
82
+ }] };
83
+ };
84
+ }
85
+ function objectSchema(shape, jsonSchema) {
86
+ const schema = buildObjectSchema(shape);
87
+ return jsonSchema === void 0 ? schema : withJsonSchema(schema, jsonSchema);
88
+ }
89
+ /** The validator behind {@link objectSchema}. */
90
+ function buildObjectSchema(shape) {
86
91
  return { "~standard": {
87
92
  version: 1,
88
93
  vendor: VENDOR,
@@ -107,5 +112,5 @@ function objectSchema(shape) {
107
112
  }
108
113
 
109
114
  //#endregion
110
- export { objectSchema, optionalBooleanField, optionalNumberField, optionalStringField, stringField };
115
+ export { objectSchema, optionalBooleanField, optionalNumberField, optionalStringArrayField, optionalStringField, stringField, withJsonSchema };
111
116
  //# sourceMappingURL=schema.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.mjs","names":[],"sources":["../../../../../../../ai-workspace/src/tools/schema.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\n\n/**\n * Tiny, dependency-free [Standard Schema](https://standardschema.dev)\n * builders for the workspace tools' input validation. The package pins\n * only `@warlock.js/ai` and `@warlock.js/fs` as runtime dependencies, so\n * rather than pull in a schema library we hand-roll the few shapes the\n * file tools need — exactly the pattern `@warlock.js/ai`'s own `tool()`\n * tests use. Each builder returns a `StandardSchemaV1`, which is what\n * `tool({ input })` validates against before calling `execute`.\n *\n * These intentionally cover only the primitive cases the FILE tools\n * require (`string`, `optional string`, `optional number`, `optional\n * boolean`, and an `object` of fields). They are not a general-purpose\n * validator.\n */\n\n/** The vendor tag stamped on every issue these builders produce. */\nconst VENDOR = \"ai-workspace\";\n\n/**\n * A single field validator inside {@link objectSchema}: given a value,\n * return either the coerced value or a list of issues. Field validators\n * receive the raw property and the property name (for issue messages).\n */\ntype FieldValidator<T> = (\n value: unknown,\n key: string,\n) => { value: T } | { issues: StandardSchemaV1.Issue[] };\n\n/** Required string field — rejects anything that is not a string. */\nexport function stringField(): FieldValidator<string> {\n return (value, key) => {\n if (typeof value === \"string\") {\n return { value };\n }\n\n return { issues: [{ message: `\"${key}\" must be a string`, path: [key] }] };\n };\n}\n\n/**\n * Optional string field — accepts `undefined` (the property absent or\n * explicitly undefined) or a string, and rejects every other type.\n */\nexport function optionalStringField(): FieldValidator<string | undefined> {\n return (value, key) => {\n if (value === undefined) {\n return { value: undefined };\n }\n\n if (typeof value === \"string\") {\n return { value };\n }\n\n return {\n issues: [{ message: `\"${key}\" must be a string when provided`, path: [key] }],\n };\n };\n}\n\n/**\n * Optional finite-number field — accepts `undefined` or a finite number,\n * rejecting `NaN`/`Infinity` and non-number types.\n */\nexport function optionalNumberField(): FieldValidator<number | undefined> {\n return (value, key) => {\n if (value === undefined) {\n return { value: undefined };\n }\n\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return { value };\n }\n\n return {\n issues: [\n { message: `\"${key}\" must be a finite number when provided`, path: [key] },\n ],\n };\n };\n}\n\n/** Optional boolean field — accepts `undefined` or a boolean. */\nexport function optionalBooleanField(): FieldValidator<boolean | undefined> {\n return (value, key) => {\n if (value === undefined) {\n return { value: undefined };\n }\n\n if (typeof value === \"boolean\") {\n return { value };\n }\n\n return {\n issues: [{ message: `\"${key}\" must be a boolean when provided`, path: [key] }],\n };\n };\n}\n\n/** The per-key field validator map describing an object schema's shape. */\ntype ObjectShape<T> = {\n [K in keyof T]-?: FieldValidator<T[K]>;\n};\n\n/**\n * Build a {@link StandardSchemaV1} for a flat object whose every property\n * is validated by a {@link FieldValidator}. The input must be a non-null\n * object; each declared field is validated and the (possibly coerced)\n * values are collected into the typed result. All field issues are merged\n * so the caller sees every problem at once.\n *\n * `T` is constrained to `object` rather than `Record<string, unknown>` so\n * the tool IO `interface`s (which carry no implicit string index\n * signature) satisfy it directly — only the declared keys in `shape` are\n * ever read, so a string index signature is never required.\n *\n * @example\n * const schema = objectSchema<{ path: string; limit?: number }>({\n * path: stringField(),\n * limit: optionalNumberField(),\n * });\n */\nexport function objectSchema<T extends object>(\n shape: ObjectShape<T>,\n): StandardSchemaV1<T> {\n return {\n \"~standard\": {\n version: 1,\n vendor: VENDOR,\n validate(input) {\n if (typeof input !== \"object\" || input === null || Array.isArray(input)) {\n return { issues: [{ message: \"input must be an object\" }] };\n }\n\n const source = input as Record<string, unknown>;\n const issues: StandardSchemaV1.Issue[] = [];\n const result: Record<string, unknown> = {};\n\n for (const key of Object.keys(shape) as (keyof T)[]) {\n const field = shape[key];\n const outcome = field(source[key as string], key as string);\n\n if (\"issues\" in outcome) {\n issues.push(...outcome.issues);\n\n continue;\n }\n\n // Only carry through keys that resolved to a defined value, so\n // optional-absent fields stay absent rather than becoming\n // explicit `undefined` properties.\n if (outcome.value !== undefined) {\n result[key as string] = outcome.value;\n }\n }\n\n if (issues.length > 0) {\n return { issues };\n }\n\n return { value: result as T };\n },\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,MAAM,SAAS;;AAaf,SAAgB,cAAsC;CACpD,QAAQ,OAAO,QAAQ;EACrB,IAAI,OAAO,UAAU,UACnB,OAAO,EAAE,MAAM;EAGjB,OAAO,EAAE,QAAQ,CAAC;GAAE,SAAS,IAAI,IAAI;GAAqB,MAAM,CAAC,GAAG;EAAE,CAAC,EAAE;CAC3E;AACF;;;;;AAMA,SAAgB,sBAA0D;CACxE,QAAQ,OAAO,QAAQ;EACrB,IAAI,UAAU,QACZ,OAAO,EAAE,OAAO,OAAU;EAG5B,IAAI,OAAO,UAAU,UACnB,OAAO,EAAE,MAAM;EAGjB,OAAO,EACL,QAAQ,CAAC;GAAE,SAAS,IAAI,IAAI;GAAmC,MAAM,CAAC,GAAG;EAAE,CAAC,EAC9E;CACF;AACF;;;;;AAMA,SAAgB,sBAA0D;CACxE,QAAQ,OAAO,QAAQ;EACrB,IAAI,UAAU,QACZ,OAAO,EAAE,OAAO,OAAU;EAG5B,IAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,GACpD,OAAO,EAAE,MAAM;EAGjB,OAAO,EACL,QAAQ,CACN;GAAE,SAAS,IAAI,IAAI;GAA0C,MAAM,CAAC,GAAG;EAAE,CAC3E,EACF;CACF;AACF;;AAGA,SAAgB,uBAA4D;CAC1E,QAAQ,OAAO,QAAQ;EACrB,IAAI,UAAU,QACZ,OAAO,EAAE,OAAO,OAAU;EAG5B,IAAI,OAAO,UAAU,WACnB,OAAO,EAAE,MAAM;EAGjB,OAAO,EACL,QAAQ,CAAC;GAAE,SAAS,IAAI,IAAI;GAAoC,MAAM,CAAC,GAAG;EAAE,CAAC,EAC/E;CACF;AACF;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,aACd,OACqB;CACrB,OAAO,EACL,aAAa;EACX,SAAS;EACT,QAAQ;EACR,SAAS,OAAO;GACd,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GACpE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,0BAA0B,CAAC,EAAE;GAG5D,MAAM,SAAS;GACf,MAAM,SAAmC,CAAC;GAC1C,MAAM,SAAkC,CAAC;GAEzC,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GAAkB;IACnD,MAAM,QAAQ,MAAM;IACpB,MAAM,UAAU,MAAM,OAAO,MAAgB,GAAa;IAE1D,IAAI,YAAY,SAAS;KACvB,OAAO,KAAK,GAAG,QAAQ,MAAM;KAE7B;IACF;IAKA,IAAI,QAAQ,UAAU,QACpB,OAAO,OAAiB,QAAQ;GAEpC;GAEA,IAAI,OAAO,SAAS,GAClB,OAAO,EAAE,OAAO;GAGlB,OAAO,EAAE,OAAO,OAAY;EAC9B;CACF,EACF;AACF"}
1
+ {"version":3,"file":"schema.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/schema.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\n\n/**\n * Tiny, dependency-free [Standard Schema](https://standardschema.dev)\n * builders for the workspace tools' input validation. The package pins\n * only `@warlock.js/ai` and `@warlock.js/fs` as runtime dependencies, so\n * rather than pull in a schema library we hand-roll the few shapes the\n * file tools need — exactly the pattern `@warlock.js/ai`'s own `tool()`\n * tests use. Each builder returns a `StandardSchemaV1`, which is what\n * `tool({ input })` validates against before calling `execute`.\n *\n * These intentionally cover only the primitive cases the FILE tools\n * require (`string`, `optional string`, `optional number`, `optional\n * boolean`, and an `object` of fields). They are not a general-purpose\n * validator.\n */\n\n/** The vendor tag stamped on every issue these builders produce. */\nconst VENDOR = \"ai-workspace\";\n\n/** A JSON Schema object, as providers send it with a tool definition. */\nexport type JsonSchemaObject = Record<string, unknown>;\n\n/**\n * A Standard Schema that also carries the JSON Schema of its input as a\n * top-level `jsonSchema` property — the property `@warlock.js/ai`'s\n * `extractJsonSchema` reads — so providers send the model the real\n * parameter shape instead of a bare `{ type: \"object\" }`.\n */\nexport type JsonStandardSchema<T> = StandardSchemaV1<T> & { jsonSchema: JsonSchemaObject };\n\n/** Attach a JSON Schema description to a hand-rolled Standard Schema. */\nexport function withJsonSchema<T>(\n schema: StandardSchemaV1<T>,\n jsonSchema: JsonSchemaObject,\n): JsonStandardSchema<T> {\n return Object.assign(schema, { jsonSchema });\n}\n\n/**\n * A single field validator inside {@link objectSchema}: given a value,\n * return either the coerced value or a list of issues. Field validators\n * receive the raw property and the property name (for issue messages).\n */\nexport type FieldValidator<T> = (\n value: unknown,\n key: string,\n) => { value: T } | { issues: StandardSchemaV1.Issue[] };\n\n/** Required string field — rejects anything that is not a string. */\nexport function stringField(): FieldValidator<string> {\n return (value, key) => {\n if (typeof value === \"string\") {\n return { value };\n }\n\n return { issues: [{ message: `\"${key}\" must be a string`, path: [key] }] };\n };\n}\n\n/**\n * Optional string field — accepts `undefined` (the property absent or\n * explicitly undefined) or a string, and rejects every other type.\n */\nexport function optionalStringField(): FieldValidator<string | undefined> {\n return (value, key) => {\n if (value === undefined) {\n return { value: undefined };\n }\n\n if (typeof value === \"string\") {\n return { value };\n }\n\n return {\n issues: [{ message: `\"${key}\" must be a string when provided`, path: [key] }],\n };\n };\n}\n\n/**\n * Optional finite-number field — accepts `undefined` or a finite number,\n * rejecting `NaN`/`Infinity` and non-number types.\n */\nexport function optionalNumberField(): FieldValidator<number | undefined> {\n return (value, key) => {\n if (value === undefined) {\n return { value: undefined };\n }\n\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return { value };\n }\n\n return {\n issues: [{ message: `\"${key}\" must be a finite number when provided`, path: [key] }],\n };\n };\n}\n\n/** Optional boolean field — accepts `undefined` or a boolean. */\nexport function optionalBooleanField(): FieldValidator<boolean | undefined> {\n return (value, key) => {\n if (value === undefined) {\n return { value: undefined };\n }\n\n if (typeof value === \"boolean\") {\n return { value };\n }\n\n return {\n issues: [{ message: `\"${key}\" must be a boolean when provided`, path: [key] }],\n };\n };\n}\n\n/**\n * Optional array-of-strings field — accepts `undefined` or an array whose\n * every element is a string (at most `maxItems` of them).\n */\nexport function optionalStringArrayField(maxItems = 50): FieldValidator<string[] | undefined> {\n return (value, key) => {\n if (value === undefined) {\n return { value: undefined };\n }\n\n if (\n Array.isArray(value) &&\n value.length <= maxItems &&\n value.every((item) => typeof item === \"string\")\n ) {\n return { value: value as string[] };\n }\n\n return {\n issues: [\n {\n message: `\"${key}\" must be an array of at most ${maxItems} strings when provided`,\n path: [key],\n },\n ],\n };\n };\n}\n\n/** The per-key field validator map describing an object schema's shape. */\ntype ObjectShape<T> = {\n [K in keyof T]-?: FieldValidator<T[K]>;\n};\n\n/**\n * Build a {@link StandardSchemaV1} for a flat object whose every property\n * is validated by a {@link FieldValidator}. The input must be a non-null\n * object; each declared field is validated and the (possibly coerced)\n * values are collected into the typed result. All field issues are merged\n * so the caller sees every problem at once.\n *\n * `T` is constrained to `object` rather than `Record<string, unknown>` so\n * the tool IO `interface`s (which carry no implicit string index\n * signature) satisfy it directly — only the declared keys in `shape` are\n * ever read, so a string index signature is never required.\n *\n * @example\n * const schema = objectSchema<{ path: string; limit?: number }>({\n * path: stringField(),\n * limit: optionalNumberField(),\n * });\n */\nexport function objectSchema<T extends object>(shape: ObjectShape<T>): StandardSchemaV1<T>;\nexport function objectSchema<T extends object>(\n shape: ObjectShape<T>,\n jsonSchema: JsonSchemaObject,\n): JsonStandardSchema<T>;\nexport function objectSchema<T extends object>(\n shape: ObjectShape<T>,\n jsonSchema?: JsonSchemaObject,\n): StandardSchemaV1<T> {\n const schema = buildObjectSchema(shape);\n\n return jsonSchema === undefined ? schema : withJsonSchema(schema, jsonSchema);\n}\n\n/** The validator behind {@link objectSchema}. */\nfunction buildObjectSchema<T extends object>(shape: ObjectShape<T>): StandardSchemaV1<T> {\n return {\n \"~standard\": {\n version: 1,\n vendor: VENDOR,\n validate(input) {\n if (typeof input !== \"object\" || input === null || Array.isArray(input)) {\n return { issues: [{ message: \"input must be an object\" }] };\n }\n\n const source = input as Record<string, unknown>;\n const issues: StandardSchemaV1.Issue[] = [];\n const result: Record<string, unknown> = {};\n\n for (const key of Object.keys(shape) as (keyof T)[]) {\n const field = shape[key];\n const outcome = field(source[key as string], key as string);\n\n if (\"issues\" in outcome) {\n issues.push(...outcome.issues);\n\n continue;\n }\n\n // Only carry through keys that resolved to a defined value, so\n // optional-absent fields stay absent rather than becoming\n // explicit `undefined` properties.\n if (outcome.value !== undefined) {\n result[key as string] = outcome.value;\n }\n }\n\n if (issues.length > 0) {\n return { issues };\n }\n\n return { value: result as T };\n },\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,MAAM,SAAS;;AAcf,SAAgB,eACd,QACA,YACuB;CACvB,OAAO,OAAO,OAAO,QAAQ,EAAE,WAAW,CAAC;AAC7C;;AAaA,SAAgB,cAAsC;CACpD,QAAQ,OAAO,QAAQ;EACrB,IAAI,OAAO,UAAU,UACnB,OAAO,EAAE,MAAM;EAGjB,OAAO,EAAE,QAAQ,CAAC;GAAE,SAAS,IAAI,IAAI;GAAqB,MAAM,CAAC,GAAG;EAAE,CAAC,EAAE;CAC3E;AACF;;;;;AAMA,SAAgB,sBAA0D;CACxE,QAAQ,OAAO,QAAQ;EACrB,IAAI,UAAU,QACZ,OAAO,EAAE,OAAO,OAAU;EAG5B,IAAI,OAAO,UAAU,UACnB,OAAO,EAAE,MAAM;EAGjB,OAAO,EACL,QAAQ,CAAC;GAAE,SAAS,IAAI,IAAI;GAAmC,MAAM,CAAC,GAAG;EAAE,CAAC,EAC9E;CACF;AACF;;;;;AAMA,SAAgB,sBAA0D;CACxE,QAAQ,OAAO,QAAQ;EACrB,IAAI,UAAU,QACZ,OAAO,EAAE,OAAO,OAAU;EAG5B,IAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,GACpD,OAAO,EAAE,MAAM;EAGjB,OAAO,EACL,QAAQ,CAAC;GAAE,SAAS,IAAI,IAAI;GAA0C,MAAM,CAAC,GAAG;EAAE,CAAC,EACrF;CACF;AACF;;AAGA,SAAgB,uBAA4D;CAC1E,QAAQ,OAAO,QAAQ;EACrB,IAAI,UAAU,QACZ,OAAO,EAAE,OAAO,OAAU;EAG5B,IAAI,OAAO,UAAU,WACnB,OAAO,EAAE,MAAM;EAGjB,OAAO,EACL,QAAQ,CAAC;GAAE,SAAS,IAAI,IAAI;GAAoC,MAAM,CAAC,GAAG;EAAE,CAAC,EAC/E;CACF;AACF;;;;;AAMA,SAAgB,yBAAyB,WAAW,IAA0C;CAC5F,QAAQ,OAAO,QAAQ;EACrB,IAAI,UAAU,QACZ,OAAO,EAAE,OAAO,OAAU;EAG5B,IACE,MAAM,QAAQ,KAAK,KACnB,MAAM,UAAU,YAChB,MAAM,OAAO,SAAS,OAAO,SAAS,QAAQ,GAE9C,OAAO,EAAS,MAAkB;EAGpC,OAAO,EACL,QAAQ,CACN;GACE,SAAS,IAAI,IAAI,gCAAgC,SAAS;GAC1D,MAAM,CAAC,GAAG;EACZ,CACF,EACF;CACF;AACF;AA8BA,SAAgB,aACd,OACA,YACqB;CACrB,MAAM,SAAS,kBAAkB,KAAK;CAEtC,OAAO,eAAe,SAAY,SAAS,eAAe,QAAQ,UAAU;AAC9E;;AAGA,SAAS,kBAAoC,OAA4C;CACvF,OAAO,EACL,aAAa;EACX,SAAS;EACT,QAAQ;EACR,SAAS,OAAO;GACd,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GACpE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,0BAA0B,CAAC,EAAE;GAG5D,MAAM,SAAS;GACf,MAAM,SAAmC,CAAC;GAC1C,MAAM,SAAkC,CAAC;GAEzC,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GAAkB;IACnD,MAAM,QAAQ,MAAM;IACpB,MAAM,UAAU,MAAM,OAAO,MAAgB,GAAa;IAE1D,IAAI,YAAY,SAAS;KACvB,OAAO,KAAK,GAAG,QAAQ,MAAM;KAE7B;IACF;IAKA,IAAI,QAAQ,UAAU,QACpB,OAAO,OAAiB,QAAQ;GAEpC;GAEA,IAAI,OAAO,SAAS,GAClB,OAAO,EAAE,OAAO;GAGlB,OAAO,EAAE,OAAO,OAAY;EAC9B;CACF,EACF;AACF"}
@@ -0,0 +1,25 @@
1
+ import { ShellJobInfo, ShellKillInput } from "../contracts/tool-io.type.mjs";
2
+ import { WorkspaceOps } from "../contracts/workspace-ops.contract.mjs";
3
+ import { ToolContract } from "@warlock.js/ai";
4
+
5
+ //#region ../../packages/ai-workspace/src/tools/shell-kill.d.ts
6
+ /**
7
+ * Build the agent-facing `shell_kill` tool: stop a background command
8
+ * started with `run_shell({ background: true })`, killing its whole process
9
+ * tree, and return its final state. Killing a command that already stopped
10
+ * just reports its state. An unknown id surfaces as a `WorkspaceJobError`
11
+ * in the result's `error` field.
12
+ *
13
+ * @param ops - The shared, policy-enforced operation layer.
14
+ * @param options - Optional overrides; `name` renames the LLM-visible tool.
15
+ *
16
+ * @example
17
+ * const { data } = await makeShellKillTool(ops).invoke({ id: "job-1" });
18
+ * console.log(data?.status); // "killed"
19
+ */
20
+ declare function makeShellKillTool(ops: WorkspaceOps, options?: {
21
+ name?: string;
22
+ }): ToolContract<ShellKillInput, ShellJobInfo>;
23
+ //#endregion
24
+ export { makeShellKillTool };
25
+ //# sourceMappingURL=shell-kill.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-kill.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/shell-kill.ts"],"mappings":";;;;;;;;;AAyBA;;;;;;;;;;iBAAgB,iBAAA,CACd,GAAA,EAAK,YAAA,EACL,OAAA;EAAY,IAAA;AAAA,IACX,YAAA,CAAa,cAAA,EAAgB,YAAA"}
@@ -0,0 +1,36 @@
1
+ import { objectSchema, stringField } from "./schema.mjs";
2
+ import { SHELL_KILL_JSON_SCHEMA } from "./json-schemas.mjs";
3
+ import { tool } from "@warlock.js/ai";
4
+
5
+ //#region ../../packages/ai-workspace/src/tools/shell-kill.ts
6
+ /** Default tool name exposed to the LLM. */
7
+ const DEFAULT_NAME = "shell_kill";
8
+ /** Input schema for the `shell_kill` tool. */
9
+ const inputSchema = objectSchema({ id: stringField() }, SHELL_KILL_JSON_SCHEMA);
10
+ /**
11
+ * Build the agent-facing `shell_kill` tool: stop a background command
12
+ * started with `run_shell({ background: true })`, killing its whole process
13
+ * tree, and return its final state. Killing a command that already stopped
14
+ * just reports its state. An unknown id surfaces as a `WorkspaceJobError`
15
+ * in the result's `error` field.
16
+ *
17
+ * @param ops - The shared, policy-enforced operation layer.
18
+ * @param options - Optional overrides; `name` renames the LLM-visible tool.
19
+ *
20
+ * @example
21
+ * const { data } = await makeShellKillTool(ops).invoke({ id: "job-1" });
22
+ * console.log(data?.status); // "killed"
23
+ */
24
+ function makeShellKillTool(ops, options) {
25
+ return tool({
26
+ name: options?.name ?? DEFAULT_NAME,
27
+ description: "Stop a background command started with run_shell (background: true), killing it and everything it started. Returns its final state.",
28
+ action: (input) => `Stopping ${typeof input.id === "string" ? input.id : "a background command"}`,
29
+ input: inputSchema,
30
+ execute: (input) => ops.shellKill(input.id)
31
+ });
32
+ }
33
+
34
+ //#endregion
35
+ export { makeShellKillTool };
36
+ //# sourceMappingURL=shell-kill.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-kill.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/shell-kill.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport type { ShellJobInfo, ShellKillInput, WorkspaceOps } from \"../contracts\";\nimport { SHELL_KILL_JSON_SCHEMA } from \"./json-schemas\";\nimport { objectSchema, stringField } from \"./schema\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"shell_kill\";\n\n/** Input schema for the `shell_kill` tool. */\nconst inputSchema = objectSchema<ShellKillInput>({ id: stringField() }, SHELL_KILL_JSON_SCHEMA);\n\n/**\n * Build the agent-facing `shell_kill` tool: stop a background command\n * started with `run_shell({ background: true })`, killing its whole process\n * tree, and return its final state. Killing a command that already stopped\n * just reports its state. An unknown id surfaces as a `WorkspaceJobError`\n * in the result's `error` field.\n *\n * @param ops - The shared, policy-enforced operation layer.\n * @param options - Optional overrides; `name` renames the LLM-visible tool.\n *\n * @example\n * const { data } = await makeShellKillTool(ops).invoke({ id: \"job-1\" });\n * console.log(data?.status); // \"killed\"\n */\nexport function makeShellKillTool(\n ops: WorkspaceOps,\n options?: { name?: string },\n): ToolContract<ShellKillInput, ShellJobInfo> {\n return tool<ShellKillInput, ShellJobInfo>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Stop a background command started with run_shell (background: true), \" +\n \"killing it and everything it started. Returns its final state.\",\n action: (input) =>\n `Stopping ${typeof input.id === \"string\" ? input.id : \"a background command\"}`,\n input: inputSchema,\n execute: (input) => ops.shellKill(input.id),\n });\n}\n"],"mappings":";;;;;;AAMA,MAAM,eAAe;;AAGrB,MAAM,cAAc,aAA6B,EAAE,IAAI,YAAY,EAAE,GAAG,sBAAsB;;;;;;;;;;;;;;;AAgB9F,SAAgB,kBACd,KACA,SAC4C;CAC5C,OAAO,KAAmC;EACxC,MAAM,SAAS,QAAQ;EACvB,aACE;EAEF,SAAS,UACP,YAAY,OAAO,MAAM,OAAO,WAAW,MAAM,KAAK;EACxD,OAAO;EACP,UAAU,UAAU,IAAI,UAAU,MAAM,EAAE;CAC5C,CAAC;AACH"}
@@ -0,0 +1,30 @@
1
+ import { ShellOutputInput, ShellOutputResult } from "../contracts/tool-io.type.mjs";
2
+ import { WorkspaceOps } from "../contracts/workspace-ops.contract.mjs";
3
+ import { ToolContract } from "@warlock.js/ai";
4
+
5
+ //#region ../../packages/ai-workspace/src/tools/shell-output.d.ts
6
+ /**
7
+ * Build the agent-facing `shell_output` tool: the new output of a
8
+ * background command started with `run_shell({ background: true })`, plus
9
+ * its state (`running` / `exited` / `killed` / `timed-out`, `exitCode`).
10
+ *
11
+ * Without `since` it returns everything produced since the previous read;
12
+ * `cursor` in the result is the position to pass as `since` next time.
13
+ * `waitMs` (at most 30 s) waits for new output or for the command to stop
14
+ * when there is nothing new yet — the agent run's signal ends the wait
15
+ * early. Only the newest `shell.maxOutputBytes` of a command's output are
16
+ * kept; `droppedBytes` says how much the reader missed.
17
+ *
18
+ * @param ops - The shared, policy-enforced operation layer.
19
+ * @param options - Optional overrides; `name` renames the LLM-visible tool.
20
+ *
21
+ * @example
22
+ * const { data } = await makeShellOutputTool(ops).invoke({ id: "job-1", waitMs: 2_000 });
23
+ * if (data?.stdout.includes("ready")) console.log("dev server is up");
24
+ */
25
+ declare function makeShellOutputTool(ops: WorkspaceOps, options?: {
26
+ name?: string;
27
+ }): ToolContract<ShellOutputInput, ShellOutputResult>;
28
+ //#endregion
29
+ export { makeShellOutputTool };
30
+ //# sourceMappingURL=shell-output.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-output.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/shell-output.ts"],"mappings":";;;;;;;;;AAqCA;;;;;;;;;;;;;;;iBAAgB,mBAAA,CACd,GAAA,EAAK,YAAA,EACL,OAAA;EAAY,IAAA;AAAA,IACX,YAAA,CAAa,gBAAA,EAAkB,iBAAA"}