@warlock.js/ai-workspace 5.30.0 → 5.32.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 +52 -2
  2. package/README.md +34 -21
  3. package/cjs/index.cjs +3134 -315
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/backends/local.d.mts +1 -3
  6. package/esm/backends/local.d.mts.map +1 -1
  7. package/esm/backends/local.mjs +166 -28
  8. package/esm/backends/local.mjs.map +1 -1
  9. package/esm/backends/mock.d.mts +14 -9
  10. package/esm/backends/mock.d.mts.map +1 -1
  11. package/esm/backends/mock.mjs +34 -0
  12. package/esm/backends/mock.mjs.map +1 -1
  13. package/esm/contracts/index.d.mts +5 -4
  14. package/esm/contracts/tool-io.type.d.mts +216 -19
  15. package/esm/contracts/tool-io.type.d.mts.map +1 -1
  16. package/esm/contracts/workspace-backend.contract.d.mts +62 -4
  17. package/esm/contracts/workspace-backend.contract.d.mts.map +1 -1
  18. package/esm/contracts/workspace-ops.contract.d.mts +33 -6
  19. package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
  20. package/esm/contracts/workspace-policy.type.d.mts +136 -16
  21. package/esm/contracts/workspace-policy.type.d.mts.map +1 -1
  22. package/esm/contracts/workspace.contract.d.mts +65 -13
  23. package/esm/contracts/workspace.contract.d.mts.map +1 -1
  24. package/esm/errors.d.mts +103 -17
  25. package/esm/errors.d.mts.map +1 -1
  26. package/esm/errors.mjs +22 -1
  27. package/esm/errors.mjs.map +1 -1
  28. package/esm/index.d.mts +14 -5
  29. package/esm/index.mjs +10 -2
  30. package/esm/ops.d.mts +21 -3
  31. package/esm/ops.d.mts.map +1 -1
  32. package/esm/ops.mjs +230 -64
  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 +30 -20
  37. package/esm/policy/policy.d.mts.map +1 -1
  38. package/esm/policy/policy.mjs +64 -87
  39. package/esm/policy/policy.mjs.map +1 -1
  40. package/esm/policy/shell-check.d.mts +63 -0
  41. package/esm/policy/shell-check.d.mts.map +1 -0
  42. package/esm/policy/shell-check.mjs +638 -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 +62 -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 +2 -3
  59. package/esm/tools/edit-file.d.mts.map +1 -1
  60. package/esm/tools/edit-file.mjs +2 -1
  61. package/esm/tools/edit-file.mjs.map +1 -1
  62. package/esm/tools/glob.d.mts +3 -4
  63. package/esm/tools/glob.d.mts.map +1 -1
  64. package/esm/tools/glob.mjs +2 -1
  65. package/esm/tools/glob.mjs.map +1 -1
  66. package/esm/tools/grep.d.mts +3 -4
  67. package/esm/tools/grep.d.mts.map +1 -1
  68. package/esm/tools/grep.mjs +2 -1
  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 +31 -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 +37 -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 +2 -3
  81. package/esm/tools/read-file.d.mts.map +1 -1
  82. package/esm/tools/read-file.mjs +2 -1
  83. package/esm/tools/read-file.mjs.map +1 -1
  84. package/esm/tools/run-shell.d.mts +36 -15
  85. package/esm/tools/run-shell.d.mts.map +1 -1
  86. package/esm/tools/run-shell.mjs +59 -53
  87. package/esm/tools/run-shell.mjs.map +1 -1
  88. package/esm/tools/run-tests.d.mts +11 -4
  89. package/esm/tools/run-tests.d.mts.map +1 -1
  90. package/esm/tools/run-tests.mjs +10 -3
  91. package/esm/tools/run-tests.mjs.map +1 -1
  92. package/esm/tools/schema.mjs +23 -18
  93. package/esm/tools/schema.mjs.map +1 -1
  94. package/esm/tools/shell-kill.d.mts +24 -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 +29 -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 +42 -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 +2 -3
  107. package/esm/tools/write-file.d.mts.map +1 -1
  108. package/esm/tools/write-file.mjs +2 -1
  109. package/esm/tools/write-file.mjs.map +1 -1
  110. package/esm/workspace.d.mts +14 -11
  111. package/esm/workspace.d.mts.map +1 -1
  112. package/esm/workspace.mjs +108 -27
  113. package/esm/workspace.mjs.map +1 -1
  114. package/llms-full.txt +155 -34
  115. package/llms.txt +1 -1
  116. package/package.json +3 -3
  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 +148 -27
package/esm/index.d.mts CHANGED
@@ -1,20 +1,29 @@
1
- import { EditFileInput, EditFileResult, ExecOptions, GlobInput, GlobResult, GrepInput, GrepMatch, GrepResult, ReadFileInput, ReadFileResult, RunShellInput, RunShellResult, RunTestsInput, WorkspaceToolName, WriteFileInput, WriteFileResult } from "./contracts/tool-io.type.mjs";
2
- import { WorkspaceBackend, WorkspaceBackendExecOptions, WorkspaceBackendExecResult } from "./contracts/workspace-backend.contract.mjs";
1
+ import { EditFileInput, EditFileResult, ExecOptions, GlobInput, GlobResult, GrepInput, GrepMatch, GrepResult, ListDirEntry, ListDirEntryType, ListDirInput, ListDirOptions, ListDirResult, MultiEditInput, MultiEditOperation, MultiEditResult, ReadFileInput, ReadFileResult, RunShellBackgroundResult, RunShellInput, RunShellResult, RunTestsInput, ShellJobInfo, ShellJobStatus, ShellKillInput, ShellOutputInput, ShellOutputOptions, ShellOutputResult, StartShellOptions, TodoListResult, TodoWriteInput, WorkspaceTodo, WorkspaceTodoInput, WorkspaceTodoStatus, WorkspaceToolName, WriteFileInput, WriteFileResult } from "./contracts/tool-io.type.mjs";
2
+ import { WorkspaceAllowCommand, WorkspaceBackendType, WorkspaceBackgroundPolicy, WorkspaceCommandCheckContext, WorkspaceCommandPattern, WorkspaceListPolicy, WorkspacePolicy, WorkspaceReadPolicy, WorkspaceShellInterpreter, WorkspaceShellPolicy } from "./contracts/workspace-policy.type.mjs";
3
+ import { WorkspaceBackend, WorkspaceBackendExecOptions, WorkspaceBackendExecResult, WorkspaceBackendProcess, WorkspaceBackendStartOptions, WorkspaceBackendStat, WorkspaceOutputStream } from "./contracts/workspace-backend.contract.mjs";
3
4
  import { WorkspaceOps } from "./contracts/workspace-ops.contract.mjs";
4
- import { WorkspaceBackendType, WorkspacePolicy, WorkspaceReadPolicy, WorkspaceShellPolicy } from "./contracts/workspace-policy.type.mjs";
5
5
  import { Workspace, WorkspaceTools } from "./contracts/workspace.contract.mjs";
6
- import { WorkspaceEditError, WorkspaceEditErrorOptions, WorkspaceEditFailure, WorkspacePolicyError, WorkspacePolicyErrorOptions, WorkspacePolicyViolation } from "./errors.mjs";
6
+ import "./contracts/index.mjs";
7
+ import { WorkspaceEditError, WorkspaceEditErrorOptions, WorkspaceEditFailure, WorkspaceJobError, WorkspaceJobErrorOptions, WorkspaceJobFailure, WorkspacePolicyError, WorkspacePolicyErrorOptions, WorkspacePolicyViolation, WorkspaceShellRefusalReason } from "./errors.mjs";
7
8
  import { ResolvedPath, buildEnv, isCommandAllowed, resolveInJail } from "./policy/policy.mjs";
9
+ import { WorkspaceShellCheck, WorkspaceShellPath, WorkspaceShellPathAccess, checkShellCommand } from "./policy/shell-check.mjs";
10
+ import { ArgvAnalysis, analyzeArgvCommand, tokenizeCommand } from "./policy/tokenize-command.mjs";
8
11
  import { createOps } from "./ops.mjs";
9
12
  import { createLocalBackend } from "./backends/local.mjs";
10
13
  import { MockBackendSeed, MockExecResult, createMockBackend } from "./backends/mock.mjs";
11
14
  import { makeEditFileTool } from "./tools/edit-file.mjs";
12
15
  import { MakeGlobToolOptions, makeGlobTool } from "./tools/glob.mjs";
13
16
  import { MakeGrepToolOptions, makeGrepTool } from "./tools/grep.mjs";
17
+ import { makeListDirTool } from "./tools/list-dir.mjs";
18
+ import { makeMultiEditTool } from "./tools/multi-edit.mjs";
14
19
  import { makeReadFileTool } from "./tools/read-file.mjs";
15
20
  import { MakeRunShellToolOptions, makeRunShellTool } from "./tools/run-shell.mjs";
16
21
  import { MakeRunTestsToolOptions, makeRunTestsTool } from "./tools/run-tests.mjs";
22
+ import { makeShellKillTool } from "./tools/shell-kill.mjs";
23
+ import { makeShellOutputTool } from "./tools/shell-output.mjs";
24
+ import { TodoStore } from "./todos.mjs";
25
+ import { makeTodoReadTool, makeTodoWriteTool } from "./tools/todo.mjs";
17
26
  import { makeWriteFileTool } from "./tools/write-file.mjs";
18
27
  import { workspace } from "./workspace.mjs";
19
- export { type EditFileInput, type EditFileResult, type ExecOptions, type GlobInput, type GlobResult, type GrepInput, type GrepMatch, type GrepResult, type MakeGlobToolOptions, type MakeGrepToolOptions, type MakeRunShellToolOptions, type MakeRunTestsToolOptions, type MockBackendSeed, type MockExecResult, type ReadFileInput, type ReadFileResult, type ResolvedPath, type RunShellInput, type RunShellResult, type RunTestsInput, type Workspace, type WorkspaceBackend, type WorkspaceBackendExecOptions, type WorkspaceBackendExecResult, type WorkspaceBackendType, WorkspaceEditError, type WorkspaceEditErrorOptions, type WorkspaceEditFailure, type WorkspaceOps, type WorkspacePolicy, WorkspacePolicyError, type WorkspacePolicyErrorOptions, type WorkspacePolicyViolation, type WorkspaceReadPolicy, type WorkspaceShellPolicy, type WorkspaceToolName, type WorkspaceTools, type WriteFileInput, type WriteFileResult, buildEnv, createLocalBackend, createMockBackend, createOps, isCommandAllowed, makeEditFileTool, makeGlobTool, makeGrepTool, makeReadFileTool, makeRunShellTool, makeRunTestsTool, makeWriteFileTool, resolveInJail, workspace };
28
+ export { type ArgvAnalysis, type EditFileInput, type EditFileResult, type ExecOptions, type GlobInput, type GlobResult, type GrepInput, type GrepMatch, type GrepResult, type ListDirEntry, type ListDirEntryType, type ListDirInput, type ListDirOptions, type ListDirResult, type MakeGlobToolOptions, type MakeGrepToolOptions, type MakeRunShellToolOptions, type MakeRunTestsToolOptions, type MockBackendSeed, type MockExecResult, type MultiEditInput, type MultiEditOperation, type MultiEditResult, type ReadFileInput, type ReadFileResult, type ResolvedPath, type RunShellBackgroundResult, type RunShellInput, type RunShellResult, type RunTestsInput, type ShellJobInfo, type ShellJobStatus, type ShellKillInput, type ShellOutputInput, type ShellOutputOptions, type ShellOutputResult, type StartShellOptions, type TodoListResult, TodoStore, type TodoWriteInput, type Workspace, type WorkspaceAllowCommand, type WorkspaceBackend, type WorkspaceBackendExecOptions, type WorkspaceBackendExecResult, type WorkspaceBackendProcess, type WorkspaceBackendStartOptions, type WorkspaceBackendStat, type WorkspaceBackendType, type WorkspaceBackgroundPolicy, type WorkspaceCommandCheckContext, type WorkspaceCommandPattern, WorkspaceEditError, type WorkspaceEditErrorOptions, type WorkspaceEditFailure, WorkspaceJobError, type WorkspaceJobErrorOptions, type WorkspaceJobFailure, type WorkspaceListPolicy, type WorkspaceOps, type WorkspaceOutputStream, type WorkspacePolicy, WorkspacePolicyError, type WorkspacePolicyErrorOptions, type WorkspacePolicyViolation, type WorkspaceReadPolicy, type WorkspaceShellCheck, type WorkspaceShellInterpreter, type WorkspaceShellPath, type WorkspaceShellPathAccess, type WorkspaceShellPolicy, type WorkspaceShellRefusalReason, type WorkspaceTodo, type WorkspaceTodoInput, type WorkspaceTodoStatus, type WorkspaceToolName, type WorkspaceTools, type WriteFileInput, type WriteFileResult, analyzeArgvCommand, buildEnv, checkShellCommand, createLocalBackend, createMockBackend, createOps, isCommandAllowed, makeEditFileTool, makeGlobTool, makeGrepTool, makeListDirTool, makeMultiEditTool, makeReadFileTool, makeRunShellTool, makeRunTestsTool, makeShellKillTool, makeShellOutputTool, makeTodoReadTool, makeTodoWriteTool, makeWriteFileTool, resolveInJail, tokenizeCommand, workspace };
20
29
  import "./workspace.mjs";
package/esm/index.mjs CHANGED
@@ -1,4 +1,6 @@
1
- import { WorkspaceEditError, WorkspacePolicyError } from "./errors.mjs";
1
+ import { WorkspaceEditError, WorkspaceJobError, WorkspacePolicyError } from "./errors.mjs";
2
+ import { analyzeArgvCommand, tokenizeCommand } from "./policy/tokenize-command.mjs";
3
+ import { checkShellCommand } from "./policy/shell-check.mjs";
2
4
  import { buildEnv, isCommandAllowed, resolveInJail } from "./policy/policy.mjs";
3
5
  import { createOps } from "./ops.mjs";
4
6
  import { createLocalBackend } from "./backends/local.mjs";
@@ -6,10 +8,16 @@ import { createMockBackend } from "./backends/mock.mjs";
6
8
  import { makeEditFileTool } from "./tools/edit-file.mjs";
7
9
  import { makeGlobTool } from "./tools/glob.mjs";
8
10
  import { makeGrepTool } from "./tools/grep.mjs";
11
+ import { makeListDirTool } from "./tools/list-dir.mjs";
12
+ import { makeMultiEditTool } from "./tools/multi-edit.mjs";
9
13
  import { makeReadFileTool } from "./tools/read-file.mjs";
10
14
  import { makeRunShellTool } from "./tools/run-shell.mjs";
11
15
  import { makeRunTestsTool } from "./tools/run-tests.mjs";
16
+ import { makeShellKillTool } from "./tools/shell-kill.mjs";
17
+ import { makeShellOutputTool } from "./tools/shell-output.mjs";
18
+ import { TodoStore } from "./todos.mjs";
19
+ import { makeTodoReadTool, makeTodoWriteTool } from "./tools/todo.mjs";
12
20
  import { makeWriteFileTool } from "./tools/write-file.mjs";
13
21
  import { workspace } from "./workspace.mjs";
14
22
 
15
- export { WorkspaceEditError, WorkspacePolicyError, buildEnv, createLocalBackend, createMockBackend, createOps, isCommandAllowed, makeEditFileTool, makeGlobTool, makeGrepTool, makeReadFileTool, makeRunShellTool, makeRunTestsTool, makeWriteFileTool, resolveInJail, workspace };
23
+ export { TodoStore, WorkspaceEditError, WorkspaceJobError, WorkspacePolicyError, analyzeArgvCommand, buildEnv, checkShellCommand, createLocalBackend, createMockBackend, createOps, isCommandAllowed, makeEditFileTool, makeGlobTool, makeGrepTool, makeListDirTool, makeMultiEditTool, makeReadFileTool, makeRunShellTool, makeRunTestsTool, makeShellKillTool, makeShellOutputTool, makeTodoReadTool, makeTodoWriteTool, makeWriteFileTool, resolveInJail, tokenizeCommand, workspace };
package/esm/ops.d.mts CHANGED
@@ -1,7 +1,24 @@
1
+ import { WorkspacePolicy } from "./contracts/workspace-policy.type.mjs";
1
2
  import { WorkspaceBackend } from "./contracts/workspace-backend.contract.mjs";
2
3
  import { WorkspaceOps } from "./contracts/workspace-ops.contract.mjs";
3
- import { WorkspacePolicy } from "./contracts/workspace-policy.type.mjs";
4
+ import "./contracts/index.mjs";
5
+ import { ShellJobs } from "./shell-jobs.mjs";
4
6
  //#region ../../packages/ai-workspace/src/ops.d.ts
7
+ /**
8
+ * The internal, single-instance implementation of {@link WorkspaceOps}.
9
+ * Holds the backend + policy and is the one place the jail, command
10
+ * gating, read caps, and the read-before-edit guard are enforced — both
11
+ * the agent-facing tools and the human-facing direct methods funnel
12
+ * through this object, so there is exactly one set of rules.
13
+ */
14
+ /**
15
+ * State an ops layer shares with the other projections of the same
16
+ * workspace (`readonly()`, `scope()`): the background commands it owns.
17
+ */
18
+ export interface OpsRuntime {
19
+ /** The workspace's background commands. */
20
+ jobs: ShellJobs;
21
+ }
5
22
  /**
6
23
  * Create the policy-enforced operation layer over a backend.
7
24
  *
@@ -14,12 +31,13 @@ import { WorkspacePolicy } from "./contracts/workspace-policy.type.mjs";
14
31
  *
15
32
  * @param backend - The dumb IO executor (local disk or in-memory mock).
16
33
  * @param policy - The policy that bounds every operation.
34
+ * @param runtime - State shared with other views of the same workspace
35
+ * (its background commands); a fresh one when omitted.
17
36
  *
18
37
  * @example
19
38
  * const ops = createOps(localBackend, { cwd: "/srv/api", shell: { allow: ["npm"] } });
20
39
  * const { content, hash } = await ops.readFile("src/index.ts");
21
40
  */
22
- declare function createOps(backend: WorkspaceBackend, policy: WorkspacePolicy): WorkspaceOps;
41
+ export declare function createOps(backend: WorkspaceBackend, policy: WorkspacePolicy, runtime?: OpsRuntime): WorkspaceOps;
23
42
  //#endregion
24
- export { createOps };
25
43
  //# sourceMappingURL=ops.d.mts.map
package/esm/ops.d.mts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ops.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/ops.ts"],"mappings":";;;;;;;;;AAunBA;;;;;;;;;;;;iBAAgB,SAAA,CACd,OAAA,EAAS,gBAAA,EACT,MAAA,EAAQ,eAAA,GACP,YAAA"}
1
+ {"version":3,"file":"ops.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/ops.ts"],"mappings":";;;;;;;;;;;;;;;;;iBA8LiB;;EAEf,MAAM;;;;;;;;;;;;;;;;;;;;;wBAitBQ,UACd,SAAS,kBACT,QAAQ,iBACR,UAAU,aACT"}
package/esm/ops.mjs CHANGED
@@ -1,5 +1,8 @@
1
1
  import { WorkspaceEditError, WorkspacePolicyError } from "./errors.mjs";
2
- import { buildEnv, isCommandAllowed, resolveInJail } from "./policy/policy.mjs";
2
+ import { globToRegExp, isInside, matchesAnyGlob } from "./policy/jail.mjs";
3
+ import { checkShellCommand } from "./policy/shell-check.mjs";
4
+ import { buildCommandEnv, resolveInJail } from "./policy/policy.mjs";
5
+ import { ShellJobs } from "./shell-jobs.mjs";
3
6
  import path from "node:path";
4
7
  import { fs } from "@warlock.js/fs";
5
8
 
@@ -10,6 +13,14 @@ const DEFAULT_READ_LINES = 2e3;
10
13
  const DEFAULT_MAX_GREP_MATCHES = 1e3;
11
14
  /** Default per-command output byte cap when the policy sets none. */
12
15
  const DEFAULT_MAX_OUTPUT_BYTES = 1e6;
16
+ /** Default `list.ignore` when the policy sets none. */
17
+ const DEFAULT_LIST_IGNORE = ["node_modules", ".git"];
18
+ /** Default deepest `depth` a listing may ask for. */
19
+ const DEFAULT_LIST_MAX_DEPTH = 5;
20
+ /** Default most entries one listing returns. */
21
+ const DEFAULT_LIST_MAX_ENTRIES = 500;
22
+ /** Most edits one `multiEdit` call may carry. */
23
+ const MAX_MULTI_EDITS = 100;
13
24
  /**
14
25
  * Exit code reported for a command refused because its signal was already
15
26
  * aborted (the shell convention for an interrupted command). Callers should
@@ -100,32 +111,6 @@ function capOutput(value, maxBytes) {
100
111
  return clipUtf8(value, maxBytes);
101
112
  }
102
113
  /**
103
- * Minimal glob match over a `/`-separated relative path. Supports `**`
104
- * (spans separators), `*` (within a segment), and `?` (one non-separator
105
- * char) — enough for the workspace's `glob`/grep narrowing without
106
- * pulling in a runtime dependency.
107
- */
108
- function globToRegExp(glob) {
109
- let source = "";
110
- for (let index = 0; index < glob.length; index++) {
111
- const char = glob[index];
112
- if (char === "*") {
113
- if (glob[index + 1] === "*") {
114
- source += ".*";
115
- index++;
116
- if (glob[index + 1] === "/") index++;
117
- } else source += "[^/]*";
118
- continue;
119
- }
120
- if (char === "?") {
121
- source += "[^/]";
122
- continue;
123
- }
124
- source += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
125
- }
126
- return new RegExp(`^${source}$`);
127
- }
128
- /**
129
114
  * A quantifier: `+`, `*`, `?`, or a `{m,n}` bound.
130
115
  */
131
116
  const QUANTIFIER_SOURCE = String.raw`[+*?]|\{\d*,?\d*\}`;
@@ -147,17 +132,11 @@ function isSafeGrepPattern(pattern) {
147
132
  if (pattern.length > MAX_GREP_PATTERN_LENGTH) return false;
148
133
  return !NESTED_QUANTIFIER_PATTERN.test(pattern);
149
134
  }
150
- /**
151
- * The internal, single-instance implementation of {@link WorkspaceOps}.
152
- * Holds the backend + policy and is the one place the jail, command
153
- * gating, read caps, and the read-before-edit guard are enforced — both
154
- * the agent-facing tools and the human-facing direct methods funnel
155
- * through this object, so there is exactly one set of rules.
156
- */
157
135
  var Ops = class {
158
- constructor(backend, policy) {
136
+ constructor(backend, policy, runtime) {
159
137
  this.backend = backend;
160
138
  this.policy = policy;
139
+ this.runtime = runtime;
161
140
  this.pathLocks = /* @__PURE__ */ new Map();
162
141
  }
163
142
  /** Run `task` after every earlier mutation of `absolutePath` has settled. */
@@ -251,30 +230,76 @@ var Ops = class {
251
230
  expectedHash: input.expectHash,
252
231
  actualHash: currentHash
253
232
  });
254
- const occurrences = countOccurrences(current, input.oldString);
255
- if (occurrences === 0) throw new WorkspaceEditError(`The text to replace was not found in "${input.path}".`, {
256
- type: "not-found",
257
- path: relativePath || input.path,
258
- matches: 0
259
- });
260
- if (occurrences > 1 && !input.replaceAll) throw new WorkspaceEditError(`The text to replace is not unique in "${input.path}" (${occurrences} matches); pass replaceAll or include more surrounding context.`, {
261
- type: "not-unique",
262
- path: relativePath || input.path,
263
- matches: occurrences
264
- });
265
- const updated = input.replaceAll ? current.split(input.oldString).join(input.newString) : replaceFirst(current, input.oldString, input.newString);
233
+ const { content: updated, replacements } = applyReplacement(current, input, input.path, relativePath || input.path);
266
234
  await this.backend.writeFile(absolutePath, updated);
267
235
  return {
268
236
  path: relativePath || input.path,
269
- replacements: input.replaceAll ? occurrences : 1,
237
+ replacements,
270
238
  hash: fs.hash.string(updated)
271
239
  };
272
240
  }
273
- async exec(command, opts) {
274
- if (!isCommandAllowed(this.policy, command)) throw new WorkspacePolicyError(`Command is not permitted by the workspace shell policy: ${command}`, {
241
+ async multiEdit(input) {
242
+ const { absolutePath, relativePath } = await resolveInJail(this.policy, input.path);
243
+ const shownPath = relativePath || input.path;
244
+ if (input.edits.length === 0 || input.edits.length > 100) throw new RangeError(`A multi-edit needs between 1 and ${100} edits (got ${input.edits.length}).`);
245
+ return this.withPathLock(absolutePath, async () => {
246
+ const current = await this.backend.readFile(absolutePath);
247
+ const currentHash = fs.hash.string(current);
248
+ if (input.expectHash !== void 0 && input.expectHash !== currentHash) throw new WorkspaceEditError(`File "${input.path}" changed since it was read; none of the edits were applied.`, {
249
+ type: "stale-hash",
250
+ path: shownPath,
251
+ expectedHash: input.expectHash,
252
+ actualHash: currentHash
253
+ });
254
+ let content = current;
255
+ let replacements = 0;
256
+ input.edits.forEach((edit, editIndex) => {
257
+ const applied = applyReplacement(content, edit, input.path, shownPath, editIndex);
258
+ content = applied.content;
259
+ replacements += applied.replacements;
260
+ });
261
+ await this.backend.writeFile(absolutePath, content);
262
+ return {
263
+ path: shownPath,
264
+ edits: input.edits.length,
265
+ replacements,
266
+ hash: fs.hash.string(content)
267
+ };
268
+ });
269
+ }
270
+ /**
271
+ * Check a command line against the shell policy: every command in it
272
+ * (argv or shell mode), then — shell mode — every redirection target and
273
+ * `cd` directory against the jail with symlinks resolved.
274
+ */
275
+ async assertCommandAllowed(command) {
276
+ const verdict = checkShellCommand(this.policy, command);
277
+ if (!verdict.allowed) throw new WorkspacePolicyError(verdict.message ?? `Command is not permitted by the workspace shell policy: ${command}`, {
278
+ type: "denied-command",
279
+ command,
280
+ ...verdict.reason !== void 0 ? { reason: verdict.reason } : {},
281
+ ...verdict.executable !== void 0 ? { executable: verdict.executable } : {},
282
+ ...verdict.path !== void 0 ? { path: verdict.path } : {}
283
+ });
284
+ for (const target of verdict.paths) try {
285
+ await resolveInJail(this.policy, target.path);
286
+ } catch (error) {
287
+ if (!(error instanceof WorkspacePolicyError)) throw error;
288
+ throw new WorkspacePolicyError(target.access === "cwd" ? `\`cd ${target.target}\` resolves (through a symlink) outside the workspace jail or onto a denied path.` : `The redirection target \`${target.target}\` resolves (through a symlink) outside the workspace jail or onto a denied path.`, {
289
+ type: "denied-command",
290
+ reason: "path-escape",
291
+ command,
292
+ path: target.target
293
+ });
294
+ }
295
+ if (this.runtime.jobs.isDisposed) throw new WorkspacePolicyError("The workspace has been disposed; it cannot run new commands.", {
275
296
  type: "denied-command",
297
+ reason: "not-allowed",
276
298
  command
277
299
  });
300
+ }
301
+ async exec(command, opts) {
302
+ await this.assertCommandAllowed(command);
278
303
  const signal = opts?.signal;
279
304
  if (signal?.aborted) return {
280
305
  exitCode: CANCELLED_BEFORE_START_EXIT_CODE,
@@ -287,11 +312,13 @@ var Ops = class {
287
312
  const shell = this.policy.shell;
288
313
  const timeoutMs = opts?.timeoutMs ?? shell?.timeoutMs;
289
314
  const maxOutputBytes = shell?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
315
+ const interpreter = shell?.interpreter ?? "none";
290
316
  const result = await this.backend.exec(command, {
291
317
  cwd: this.policy.cwd,
292
318
  timeoutMs,
293
- env: buildEnv(this.policy),
294
- ...signal ? { signal } : {}
319
+ env: buildCommandEnv(this.policy),
320
+ ...signal ? { signal } : {},
321
+ ...interpreter !== "none" ? { interpreter } : {}
295
322
  });
296
323
  const stdout = capOutput(result.stdout, maxOutputBytes);
297
324
  const stderr = capOutput(result.stderr, maxOutputBytes);
@@ -305,6 +332,120 @@ var Ops = class {
305
332
  cancelled
306
333
  };
307
334
  }
335
+ async startShell(command, opts) {
336
+ const background = this.policy.shell?.background;
337
+ if (background === void 0) throw new WorkspacePolicyError("Background commands are not enabled for this workspace. Run the command without background, or ask for shell.background to be enabled.", {
338
+ type: "denied-command",
339
+ reason: "not-allowed",
340
+ command
341
+ });
342
+ await this.assertCommandAllowed(command);
343
+ const start = this.backend.start?.bind(this.backend);
344
+ if (start === void 0) throw new WorkspacePolicyError("This workspace backend cannot run background commands.", {
345
+ type: "denied-command",
346
+ reason: "not-allowed",
347
+ command
348
+ });
349
+ const shell = this.policy.shell;
350
+ const interpreter = shell?.interpreter ?? "none";
351
+ const timeoutMs = lowerOf(background.timeoutMs, opts?.timeoutMs);
352
+ const env = buildCommandEnv(this.policy);
353
+ return this.runtime.jobs.start({
354
+ command,
355
+ retainBytes: shell?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES,
356
+ maxJobs: background.maxJobs ?? 4,
357
+ ...timeoutMs !== void 0 ? { timeoutMs } : {},
358
+ launch: (onOutput) => start(command, {
359
+ cwd: this.policy.cwd,
360
+ env,
361
+ ...interpreter !== "none" ? { interpreter } : {},
362
+ onOutput
363
+ })
364
+ });
365
+ }
366
+ shellOutput(id, opts) {
367
+ return this.runtime.jobs.output(id, opts);
368
+ }
369
+ shellKill(id) {
370
+ return this.runtime.jobs.kill(id);
371
+ }
372
+ listShells() {
373
+ return this.runtime.jobs.list();
374
+ }
375
+ dispose() {
376
+ return this.runtime.jobs.dispose();
377
+ }
378
+ async listDir(inputPath = ".", opts) {
379
+ const { absolutePath: root, relativePath: rootRelative } = await resolveInJail(this.policy, inputPath);
380
+ const { absolutePath: jailRoot } = await resolveInJail(this.policy, ".");
381
+ const listPolicy = this.policy.list;
382
+ const maxDepth = Math.max(1, Math.floor(listPolicy?.maxDepth ?? DEFAULT_LIST_MAX_DEPTH));
383
+ const depth = Math.min(Math.max(1, Math.floor(opts?.depth ?? 1)), maxDepth);
384
+ const maxEntries = Math.max(1, Math.floor(listPolicy?.maxEntries ?? DEFAULT_LIST_MAX_ENTRIES));
385
+ const ignore = [...listPolicy?.ignore ?? DEFAULT_LIST_IGNORE, ...opts?.ignore ?? []];
386
+ const insideCwd = isInside(root, jailRoot);
387
+ if (!await this.isDirectory(root)) throw new Error(`"${inputPath}" is not a directory in the workspace.`);
388
+ const entries = [];
389
+ let level = [root];
390
+ let truncated = false;
391
+ for (let currentDepth = 1; currentDepth <= depth && level.length > 0; currentDepth++) {
392
+ const nextLevel = [];
393
+ for (const directory of level) {
394
+ let children;
395
+ try {
396
+ children = await this.backend.list(directory);
397
+ } catch {
398
+ continue;
399
+ }
400
+ children.sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
401
+ for (const child of children) {
402
+ const relativeToJail = insideCwd ? path.relative(jailRoot, child).split(path.sep).join("/") : child;
403
+ if (insideCwd && (this.isDenied(relativeToJail) || matchesAnyGlob(relativeToJail, ignore))) continue;
404
+ const relativeToRoot = path.relative(root, child).split(path.sep).join("/");
405
+ if (!insideCwd && matchesAnyGlob(relativeToRoot, ignore)) continue;
406
+ if (entries.length >= maxEntries) {
407
+ truncated = true;
408
+ break;
409
+ }
410
+ const described = await this.describe(child);
411
+ entries.push({
412
+ path: relativeToJail,
413
+ type: described.type,
414
+ ...described.size !== void 0 ? { size: described.size } : {},
415
+ depth: currentDepth
416
+ });
417
+ if (described.type === "directory") nextLevel.push(child);
418
+ }
419
+ if (truncated) break;
420
+ }
421
+ if (truncated) break;
422
+ level = nextLevel;
423
+ }
424
+ return {
425
+ path: insideCwd ? rootRelative || "." : root,
426
+ entries,
427
+ truncated
428
+ };
429
+ }
430
+ /** Kind and size of a listed entry; symlinks are reported, never followed. */
431
+ async describe(absPath) {
432
+ if (this.backend.stat) try {
433
+ const stats = await this.backend.stat(absPath);
434
+ return stats.type === "file" ? {
435
+ type: "file",
436
+ size: stats.size
437
+ } : { type: stats.type };
438
+ } catch {
439
+ return { type: "other" };
440
+ }
441
+ if (await this.isDirectory(absPath)) try {
442
+ await resolveInJail(this.policy, absPath);
443
+ return { type: "directory" };
444
+ } catch {
445
+ return { type: "other" };
446
+ }
447
+ return { type: "file" };
448
+ }
308
449
  async grep(pattern, opts) {
309
450
  if (!isSafeGrepPattern(pattern)) throw new WorkspacePolicyError(`Grep pattern is too long or too likely to cause catastrophic regex backtracking: ${pattern}`, {
310
451
  type: "unsafe-pattern",
@@ -328,7 +469,7 @@ var Ops = class {
328
469
  }
329
470
  const lines = content.split("\n");
330
471
  for (let index = 0; index < lines.length; index++) {
331
- const line = lines[index];
472
+ const line = lines[index] ?? "";
332
473
  if (line.length > MAX_GREP_LINE_SCAN_LENGTH) continue;
333
474
  if (regex.test(line)) {
334
475
  matches.push({
@@ -375,17 +516,40 @@ var Ops = class {
375
516
  /** Whether a `/`-separated relative path hits the policy deny list. */
376
517
  isDenied(relativePath) {
377
518
  const denyPaths = this.policy.denyPaths;
378
- if (!denyPaths || denyPaths.length === 0) return false;
379
- return denyPaths.some((glob) => {
380
- if (globToRegExp(glob).test(relativePath)) return true;
381
- if (!glob.includes("*") && !glob.includes("?")) {
382
- const prefix = glob.endsWith("/") ? glob : `${glob}/`;
383
- return relativePath.startsWith(prefix);
384
- }
385
- return false;
386
- });
519
+ return denyPaths !== void 0 && denyPaths.length > 0 && matchesAnyGlob(relativePath, denyPaths);
387
520
  }
388
521
  };
522
+ /** The lower of two optional limits (either may be absent). */
523
+ function lowerOf(policyLimit, requested) {
524
+ if (requested === void 0 || requested <= 0) return policyLimit;
525
+ return policyLimit === void 0 ? requested : Math.min(policyLimit, requested);
526
+ }
527
+ /**
528
+ * Apply one exact-string replacement to `content` under the edit rules:
529
+ * `oldString` must occur, and exactly once unless `replaceAll`.
530
+ */
531
+ function applyReplacement(content, edit, inputPath, shownPath, editIndex) {
532
+ const occurrences = countOccurrences(content, edit.oldString);
533
+ const which = editIndex === void 0 ? "" : `Edit ${editIndex + 1}: `;
534
+ const nothingApplied = editIndex === void 0 ? "" : " None of the edits were applied.";
535
+ const indexOption = editIndex === void 0 ? {} : { editIndex };
536
+ if (occurrences === 0) throw new WorkspaceEditError(`${which}The text to replace was not found in "${inputPath}".${nothingApplied}`, {
537
+ type: "not-found",
538
+ path: shownPath,
539
+ matches: 0,
540
+ ...indexOption
541
+ });
542
+ if (occurrences > 1 && !edit.replaceAll) throw new WorkspaceEditError(`${which}The text to replace is not unique in "${inputPath}" (${occurrences} matches); pass replaceAll or include more surrounding context.${nothingApplied}`, {
543
+ type: "not-unique",
544
+ path: shownPath,
545
+ matches: occurrences,
546
+ ...indexOption
547
+ });
548
+ return {
549
+ content: edit.replaceAll ? content.split(edit.oldString).join(edit.newString) : replaceFirst(content, edit.oldString, edit.newString),
550
+ replacements: edit.replaceAll ? occurrences : 1
551
+ };
552
+ }
389
553
  /** Count non-overlapping occurrences of `needle` in `haystack`. */
390
554
  function countOccurrences(haystack, needle) {
391
555
  if (needle === "") return 0;
@@ -417,13 +581,15 @@ function replaceFirst(haystack, needle, replacement) {
417
581
  *
418
582
  * @param backend - The dumb IO executor (local disk or in-memory mock).
419
583
  * @param policy - The policy that bounds every operation.
584
+ * @param runtime - State shared with other views of the same workspace
585
+ * (its background commands); a fresh one when omitted.
420
586
  *
421
587
  * @example
422
588
  * const ops = createOps(localBackend, { cwd: "/srv/api", shell: { allow: ["npm"] } });
423
589
  * const { content, hash } = await ops.readFile("src/index.ts");
424
590
  */
425
- function createOps(backend, policy) {
426
- return new Ops(backend, policy);
591
+ function createOps(backend, policy, runtime) {
592
+ return new Ops(backend, policy, runtime ?? { jobs: new ShellJobs() });
427
593
  }
428
594
 
429
595
  //#endregion