@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.
- package/CHANGELOG.md +68 -0
- package/README.md +40 -21
- package/cjs/index.cjs +3295 -360
- package/cjs/index.cjs.map +1 -1
- package/esm/backends/local.d.mts +1 -1
- package/esm/backends/local.d.mts.map +1 -1
- package/esm/backends/local.mjs +199 -31
- package/esm/backends/local.mjs.map +1 -1
- package/esm/backends/mock.d.mts +12 -6
- package/esm/backends/mock.d.mts.map +1 -1
- package/esm/backends/mock.mjs +45 -3
- package/esm/backends/mock.mjs.map +1 -1
- package/esm/contracts/index.d.mts +3 -3
- package/esm/contracts/tool-io.type.d.mts +248 -11
- package/esm/contracts/tool-io.type.d.mts.map +1 -1
- package/esm/contracts/workspace-backend.contract.d.mts +78 -2
- package/esm/contracts/workspace-backend.contract.d.mts.map +1 -1
- package/esm/contracts/workspace-ops.contract.d.mts +41 -16
- package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
- package/esm/contracts/workspace-policy.type.d.mts +142 -14
- package/esm/contracts/workspace-policy.type.d.mts.map +1 -1
- package/esm/contracts/workspace.contract.d.mts +75 -22
- package/esm/contracts/workspace.contract.d.mts.map +1 -1
- package/esm/errors.d.mts +80 -6
- package/esm/errors.d.mts.map +1 -1
- package/esm/errors.mjs +23 -2
- package/esm/errors.mjs.map +1 -1
- package/esm/index.d.mts +13 -5
- package/esm/index.mjs +10 -2
- package/esm/ops.d.mts +22 -3
- package/esm/ops.d.mts.map +1 -1
- package/esm/ops.mjs +341 -82
- package/esm/ops.mjs.map +1 -1
- package/esm/policy/jail.mjs +73 -0
- package/esm/policy/jail.mjs.map +1 -0
- package/esm/policy/policy.d.mts +26 -16
- package/esm/policy/policy.d.mts.map +1 -1
- package/esm/policy/policy.mjs +62 -86
- package/esm/policy/policy.mjs.map +1 -1
- package/esm/policy/shell-check.d.mts +64 -0
- package/esm/policy/shell-check.d.mts.map +1 -0
- package/esm/policy/shell-check.mjs +627 -0
- package/esm/policy/shell-check.mjs.map +1 -0
- package/esm/policy/shell-parser.mjs +802 -0
- package/esm/policy/shell-parser.mjs.map +1 -0
- package/esm/policy/tokenize-command.d.mts +63 -0
- package/esm/policy/tokenize-command.d.mts.map +1 -0
- package/esm/policy/tokenize-command.mjs +76 -24
- package/esm/policy/tokenize-command.mjs.map +1 -1
- package/esm/shell-jobs.d.mts +45 -0
- package/esm/shell-jobs.d.mts.map +1 -0
- package/esm/shell-jobs.mjs +247 -0
- package/esm/shell-jobs.mjs.map +1 -0
- package/esm/todos.d.mts +22 -0
- package/esm/todos.d.mts.map +1 -0
- package/esm/todos.mjs +73 -0
- package/esm/todos.mjs.map +1 -0
- package/esm/tools/edit-file.d.mts +1 -1
- package/esm/tools/edit-file.d.mts.map +1 -1
- package/esm/tools/edit-file.mjs +3 -2
- package/esm/tools/edit-file.mjs.map +1 -1
- package/esm/tools/glob.d.mts +1 -1
- package/esm/tools/glob.d.mts.map +1 -1
- package/esm/tools/glob.mjs +3 -2
- package/esm/tools/glob.mjs.map +1 -1
- package/esm/tools/grep.d.mts +1 -1
- package/esm/tools/grep.d.mts.map +1 -1
- package/esm/tools/grep.mjs +3 -2
- package/esm/tools/grep.mjs.map +1 -1
- package/esm/tools/json-schemas.mjs +175 -0
- package/esm/tools/json-schemas.mjs.map +1 -0
- package/esm/tools/list-dir.d.mts +32 -0
- package/esm/tools/list-dir.d.mts.map +1 -0
- package/esm/tools/list-dir.mjs +50 -0
- package/esm/tools/list-dir.mjs.map +1 -0
- package/esm/tools/multi-edit.d.mts +38 -0
- package/esm/tools/multi-edit.d.mts.map +1 -0
- package/esm/tools/multi-edit.mjs +121 -0
- package/esm/tools/multi-edit.mjs.map +1 -0
- package/esm/tools/read-file.d.mts +5 -5
- package/esm/tools/read-file.d.mts.map +1 -1
- package/esm/tools/read-file.mjs +10 -20
- package/esm/tools/read-file.mjs.map +1 -1
- package/esm/tools/run-shell.d.mts +39 -13
- package/esm/tools/run-shell.d.mts.map +1 -1
- package/esm/tools/run-shell.mjs +60 -47
- package/esm/tools/run-shell.mjs.map +1 -1
- package/esm/tools/run-tests.d.mts +11 -2
- package/esm/tools/run-tests.d.mts.map +1 -1
- package/esm/tools/run-tests.mjs +15 -7
- package/esm/tools/run-tests.mjs.map +1 -1
- package/esm/tools/schema.mjs +24 -19
- package/esm/tools/schema.mjs.map +1 -1
- package/esm/tools/shell-kill.d.mts +25 -0
- package/esm/tools/shell-kill.d.mts.map +1 -0
- package/esm/tools/shell-kill.mjs +36 -0
- package/esm/tools/shell-kill.mjs.map +1 -0
- package/esm/tools/shell-output.d.mts +30 -0
- package/esm/tools/shell-output.d.mts.map +1 -0
- package/esm/tools/shell-output.mjs +49 -0
- package/esm/tools/shell-output.mjs.map +1 -0
- package/esm/tools/todo.d.mts +43 -0
- package/esm/tools/todo.d.mts.map +1 -0
- package/esm/tools/todo.mjs +126 -0
- package/esm/tools/todo.mjs.map +1 -0
- package/esm/tools/write-file.d.mts +1 -1
- package/esm/tools/write-file.d.mts.map +1 -1
- package/esm/tools/write-file.mjs +3 -2
- package/esm/tools/write-file.mjs.map +1 -1
- package/esm/workspace.d.mts +12 -9
- package/esm/workspace.d.mts.map +1 -1
- package/esm/workspace.mjs +109 -28
- package/esm/workspace.mjs.map +1 -1
- package/llms-full.txt +192 -35
- package/llms.txt +1 -1
- package/package.json +5 -4
- package/skills/README.md +2 -2
- package/skills/build-loop-agent/SKILL.md +7 -7
- package/skills/index.md +6 -1
- package/skills/use-a-workspace/SKILL.md +185 -28
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { WorkspaceJobError, WorkspacePolicyError } from "./errors.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../../packages/ai-workspace/src/shell-jobs.ts
|
|
4
|
+
/** Finished commands kept for a last `shell_output` read before the oldest are pruned. */
|
|
5
|
+
const MAX_FINISHED_JOBS = 16;
|
|
6
|
+
/** Longest `waitMs` a `shellOutput` call may ask for. */
|
|
7
|
+
const MAX_OUTPUT_WAIT_MS = 3e4;
|
|
8
|
+
/** How long `kill()` / `dispose()` wait for a killed tree to report its exit. */
|
|
9
|
+
const KILL_SETTLE_MS = 5e3;
|
|
10
|
+
/** Resolve after `ms`, without keeping the event loop alive. */
|
|
11
|
+
function delay(ms) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
setTimeout(resolve, ms).unref?.();
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Keep the last `maxBytes` UTF-8 bytes of `text`, cutting forward to a
|
|
18
|
+
* character boundary so the kept tail never starts mid-character.
|
|
19
|
+
*/
|
|
20
|
+
function tailUtf8(text, maxBytes) {
|
|
21
|
+
const buffer = Buffer.from(text, "utf8");
|
|
22
|
+
if (buffer.byteLength <= maxBytes) return {
|
|
23
|
+
text,
|
|
24
|
+
bytes: buffer.byteLength
|
|
25
|
+
};
|
|
26
|
+
let start = buffer.byteLength - maxBytes;
|
|
27
|
+
while (start < buffer.byteLength && ((buffer[start] ?? 0) & 192) === 128) start++;
|
|
28
|
+
const kept = buffer.subarray(start);
|
|
29
|
+
return {
|
|
30
|
+
text: kept.toString("utf8"),
|
|
31
|
+
bytes: kept.byteLength
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Bytes of `text` from byte `skip` on, moved forward to a character boundary. */
|
|
35
|
+
function skipUtf8(text, skip) {
|
|
36
|
+
const buffer = Buffer.from(text, "utf8");
|
|
37
|
+
let start = Math.max(0, skip);
|
|
38
|
+
while (start < buffer.byteLength && ((buffer[start] ?? 0) & 192) === 128) start++;
|
|
39
|
+
return buffer.subarray(start).toString("utf8");
|
|
40
|
+
}
|
|
41
|
+
/** One background command and its retained output. */
|
|
42
|
+
var ShellJob = class {
|
|
43
|
+
constructor(id, command, retainBytes) {
|
|
44
|
+
this.id = id;
|
|
45
|
+
this.command = command;
|
|
46
|
+
this.retainBytes = retainBytes;
|
|
47
|
+
this.status = "running";
|
|
48
|
+
this.exitCode = null;
|
|
49
|
+
this.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
50
|
+
this.produced = 0;
|
|
51
|
+
this.readCursor = 0;
|
|
52
|
+
this.chunks = [];
|
|
53
|
+
this.retained = 0;
|
|
54
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
55
|
+
this.killRequested = false;
|
|
56
|
+
this.timedOut = false;
|
|
57
|
+
}
|
|
58
|
+
append(stream, chunk) {
|
|
59
|
+
if (chunk === "") return;
|
|
60
|
+
const bytes = Buffer.byteLength(chunk, "utf8");
|
|
61
|
+
this.chunks.push({
|
|
62
|
+
stream,
|
|
63
|
+
text: chunk,
|
|
64
|
+
offset: this.produced,
|
|
65
|
+
bytes
|
|
66
|
+
});
|
|
67
|
+
this.produced += bytes;
|
|
68
|
+
this.retained += bytes;
|
|
69
|
+
this.trim();
|
|
70
|
+
this.notify();
|
|
71
|
+
}
|
|
72
|
+
/** Drop the oldest output until at most `retainBytes` are kept. */
|
|
73
|
+
trim() {
|
|
74
|
+
while (this.retained > this.retainBytes && this.chunks.length > 0) {
|
|
75
|
+
const oldest = this.chunks[0];
|
|
76
|
+
if (oldest === void 0) return;
|
|
77
|
+
const excess = this.retained - this.retainBytes;
|
|
78
|
+
if (oldest.bytes <= excess) {
|
|
79
|
+
this.chunks.shift();
|
|
80
|
+
this.retained -= oldest.bytes;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const kept = tailUtf8(oldest.text, oldest.bytes - excess);
|
|
84
|
+
this.retained -= oldest.bytes - kept.bytes;
|
|
85
|
+
oldest.offset += oldest.bytes - kept.bytes;
|
|
86
|
+
oldest.text = kept.text;
|
|
87
|
+
oldest.bytes = kept.bytes;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
finish(exitCode) {
|
|
91
|
+
if (this.status !== "running") return;
|
|
92
|
+
this.exitCode = exitCode;
|
|
93
|
+
this.status = this.timedOut ? "timed-out" : this.killRequested ? "killed" : "exited";
|
|
94
|
+
this.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
95
|
+
if (this.timer !== void 0) clearTimeout(this.timer);
|
|
96
|
+
this.notify();
|
|
97
|
+
}
|
|
98
|
+
notify() {
|
|
99
|
+
for (const listener of [...this.listeners]) listener();
|
|
100
|
+
}
|
|
101
|
+
/** Resolve on the next output or state change (or after `ms`, or on abort). */
|
|
102
|
+
waitForChange(ms, signal) {
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
if (signal?.aborted) {
|
|
105
|
+
resolve();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
let timer;
|
|
109
|
+
const done = () => {
|
|
110
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
111
|
+
this.listeners.delete(done);
|
|
112
|
+
signal?.removeEventListener("abort", done);
|
|
113
|
+
resolve();
|
|
114
|
+
};
|
|
115
|
+
timer = setTimeout(done, ms);
|
|
116
|
+
timer.unref?.();
|
|
117
|
+
this.listeners.add(done);
|
|
118
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
info() {
|
|
122
|
+
return {
|
|
123
|
+
id: this.id,
|
|
124
|
+
command: this.command,
|
|
125
|
+
status: this.status,
|
|
126
|
+
exitCode: this.exitCode,
|
|
127
|
+
...this.process?.pid !== void 0 ? { pid: this.process.pid } : {},
|
|
128
|
+
startedAt: this.startedAt,
|
|
129
|
+
...this.endedAt !== void 0 ? { endedAt: this.endedAt } : {}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** Output produced after byte `since`, plus how much of it is gone. */
|
|
133
|
+
read(since) {
|
|
134
|
+
const from = Math.min(Math.max(0, Math.floor(since)), this.produced);
|
|
135
|
+
const firstRetained = this.chunks[0]?.offset ?? this.produced;
|
|
136
|
+
const droppedBytes = Math.max(0, firstRetained - from);
|
|
137
|
+
let stdout = "";
|
|
138
|
+
let stderr = "";
|
|
139
|
+
for (const chunk of this.chunks) {
|
|
140
|
+
if (chunk.offset + chunk.bytes <= from) continue;
|
|
141
|
+
const text = chunk.offset >= from ? chunk.text : skipUtf8(chunk.text, from - chunk.offset);
|
|
142
|
+
if (chunk.stream === "stdout") stdout += text;
|
|
143
|
+
else stderr += text;
|
|
144
|
+
}
|
|
145
|
+
this.readCursor = this.produced;
|
|
146
|
+
return {
|
|
147
|
+
...this.info(),
|
|
148
|
+
stdout,
|
|
149
|
+
stderr,
|
|
150
|
+
cursor: this.produced,
|
|
151
|
+
droppedBytes,
|
|
152
|
+
truncated: droppedBytes > 0
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* The background commands one workspace owns. Shared by every projection
|
|
158
|
+
* (`readonly()`, `scope()`) of the workspace, so `dispose()` on any of them
|
|
159
|
+
* stops them all.
|
|
160
|
+
*/
|
|
161
|
+
var ShellJobs = class {
|
|
162
|
+
constructor() {
|
|
163
|
+
this.jobs = /* @__PURE__ */ new Map();
|
|
164
|
+
this.nextId = 1;
|
|
165
|
+
this.disposed = false;
|
|
166
|
+
}
|
|
167
|
+
/** Whether {@link dispose} has run. */
|
|
168
|
+
get isDisposed() {
|
|
169
|
+
return this.disposed;
|
|
170
|
+
}
|
|
171
|
+
/** Start a command as a background job. */
|
|
172
|
+
start(spec) {
|
|
173
|
+
if (this.disposed) throw new WorkspacePolicyError("The workspace has been disposed; it cannot start new commands.", {
|
|
174
|
+
type: "denied-command",
|
|
175
|
+
reason: "not-allowed",
|
|
176
|
+
command: spec.command
|
|
177
|
+
});
|
|
178
|
+
const running = [...this.jobs.values()].filter((job) => job.status === "running").length;
|
|
179
|
+
if (running >= spec.maxJobs) throw new WorkspacePolicyError(`${running} background commands are already running (the limit is ${spec.maxJobs}). Stop one with shell_kill first.`, {
|
|
180
|
+
type: "denied-command",
|
|
181
|
+
reason: "not-allowed",
|
|
182
|
+
command: spec.command
|
|
183
|
+
});
|
|
184
|
+
const job = new ShellJob(`job-${this.nextId++}`, spec.command, spec.retainBytes);
|
|
185
|
+
this.jobs.set(job.id, job);
|
|
186
|
+
const handle = spec.launch((stream, chunk) => job.append(stream, chunk));
|
|
187
|
+
job.process = handle;
|
|
188
|
+
if (spec.timeoutMs !== void 0 && spec.timeoutMs > 0) {
|
|
189
|
+
job.timer = setTimeout(() => {
|
|
190
|
+
if (job.status === "running") {
|
|
191
|
+
job.timedOut = true;
|
|
192
|
+
handle.kill();
|
|
193
|
+
}
|
|
194
|
+
}, spec.timeoutMs);
|
|
195
|
+
job.timer.unref?.();
|
|
196
|
+
}
|
|
197
|
+
handle.done.then(({ exitCode }) => job.finish(exitCode));
|
|
198
|
+
this.prune();
|
|
199
|
+
return job.info();
|
|
200
|
+
}
|
|
201
|
+
/** Forget the oldest finished jobs beyond {@link MAX_FINISHED_JOBS}. */
|
|
202
|
+
prune() {
|
|
203
|
+
const finished = [...this.jobs.values()].filter((job) => job.status !== "running");
|
|
204
|
+
for (const job of finished.slice(0, Math.max(0, finished.length - MAX_FINISHED_JOBS))) this.jobs.delete(job.id);
|
|
205
|
+
}
|
|
206
|
+
get(id) {
|
|
207
|
+
const job = this.jobs.get(id);
|
|
208
|
+
if (job === void 0) throw new WorkspaceJobError(`No background command with id "${id}" exists in this workspace.`, {
|
|
209
|
+
type: "not-found",
|
|
210
|
+
jobId: id
|
|
211
|
+
});
|
|
212
|
+
return job;
|
|
213
|
+
}
|
|
214
|
+
/** New output of a job (see {@link ShellOutputOptions}). */
|
|
215
|
+
async output(id, opts) {
|
|
216
|
+
const job = this.get(id);
|
|
217
|
+
const since = opts?.since ?? job.readCursor;
|
|
218
|
+
const waitMs = Math.min(Math.max(0, opts?.waitMs ?? 0), MAX_OUTPUT_WAIT_MS);
|
|
219
|
+
if (waitMs > 0 && job.status === "running" && job.produced <= since) await job.waitForChange(waitMs, opts?.signal);
|
|
220
|
+
return job.read(since);
|
|
221
|
+
}
|
|
222
|
+
/** Kill a job's process tree and wait (bounded) for it to stop. */
|
|
223
|
+
async kill(id) {
|
|
224
|
+
const job = this.get(id);
|
|
225
|
+
await this.stop(job);
|
|
226
|
+
return job.info();
|
|
227
|
+
}
|
|
228
|
+
async stop(job) {
|
|
229
|
+
if (job.status !== "running" || job.process === void 0) return;
|
|
230
|
+
job.killRequested = true;
|
|
231
|
+
job.process.kill();
|
|
232
|
+
await Promise.race([job.process.done, delay(KILL_SETTLE_MS)]);
|
|
233
|
+
}
|
|
234
|
+
/** Every job, oldest first. */
|
|
235
|
+
list() {
|
|
236
|
+
return [...this.jobs.values()].map((job) => job.info());
|
|
237
|
+
}
|
|
238
|
+
/** Kill every running job and refuse new ones. Idempotent. */
|
|
239
|
+
async dispose() {
|
|
240
|
+
this.disposed = true;
|
|
241
|
+
await Promise.all([...this.jobs.values()].map((job) => this.stop(job)));
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
//#endregion
|
|
246
|
+
export { ShellJobs };
|
|
247
|
+
//# sourceMappingURL=shell-jobs.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell-jobs.mjs","names":[],"sources":["../../../../../../../packages/ai-workspace/src/shell-jobs.ts"],"sourcesContent":["import { WorkspaceJobError, WorkspacePolicyError } from \"./errors\";\nimport type {\n ShellJobInfo,\n ShellJobStatus,\n ShellOutputOptions,\n ShellOutputResult,\n WorkspaceBackendProcess,\n WorkspaceOutputStream,\n} from \"./contracts\";\n\n/** Default most background commands running at once. */\nexport const DEFAULT_MAX_BACKGROUND_JOBS = 4;\n\n/** Finished commands kept for a last `shell_output` read before the oldest are pruned. */\nconst MAX_FINISHED_JOBS = 16;\n\n/** Longest `waitMs` a `shellOutput` call may ask for. */\nexport const MAX_OUTPUT_WAIT_MS = 30_000;\n\n/** How long `kill()` / `dispose()` wait for a killed tree to report its exit. */\nconst KILL_SETTLE_MS = 5_000;\n\n/** One retained piece of output. */\ninterface OutputChunk {\n stream: WorkspaceOutputStream;\n text: string;\n /** Cumulative byte offset of the chunk's first byte. */\n offset: number;\n bytes: number;\n}\n\n/** What {@link ShellJobs.start} needs to launch one command. */\nexport interface ShellJobSpec {\n /** The command line (already policy-checked). */\n command: string;\n /** Most bytes of output to keep (newest wins). */\n retainBytes: number;\n /** Most commands running at once. */\n maxJobs: number;\n /** Wall-clock cap, if any. */\n timeoutMs?: number;\n /** Start the process; output must be delivered through `onOutput`. */\n launch(onOutput: (stream: WorkspaceOutputStream, chunk: string) => void): WorkspaceBackendProcess;\n}\n\n/** Resolve after `ms`, without keeping the event loop alive. */\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n\n timer.unref?.();\n });\n}\n\n/**\n * Keep the last `maxBytes` UTF-8 bytes of `text`, cutting forward to a\n * character boundary so the kept tail never starts mid-character.\n */\nfunction tailUtf8(text: string, maxBytes: number): { text: string; bytes: number } {\n const buffer = Buffer.from(text, \"utf8\");\n\n if (buffer.byteLength <= maxBytes) {\n return { text, bytes: buffer.byteLength };\n }\n\n let start = buffer.byteLength - maxBytes;\n\n while (start < buffer.byteLength && ((buffer[start] ?? 0) & 0xc0) === 0x80) {\n start++;\n }\n\n const kept = buffer.subarray(start);\n\n return { text: kept.toString(\"utf8\"), bytes: kept.byteLength };\n}\n\n/** Bytes of `text` from byte `skip` on, moved forward to a character boundary. */\nfunction skipUtf8(text: string, skip: number): string {\n const buffer = Buffer.from(text, \"utf8\");\n let start = Math.max(0, skip);\n\n while (start < buffer.byteLength && ((buffer[start] ?? 0) & 0xc0) === 0x80) {\n start++;\n }\n\n return buffer.subarray(start).toString(\"utf8\");\n}\n\n/** One background command and its retained output. */\nclass ShellJob {\n public status: ShellJobStatus = \"running\";\n\n public exitCode: number | null = null;\n\n public endedAt?: string;\n\n public readonly startedAt = new Date().toISOString();\n\n /** Total bytes produced so far (both streams). */\n public produced = 0;\n\n /** Where the next default read starts. */\n public readCursor = 0;\n\n /** Retained output, oldest first. */\n private readonly chunks: OutputChunk[] = [];\n\n private retained = 0;\n\n private readonly listeners = new Set<() => void>();\n\n public process?: WorkspaceBackendProcess;\n\n public killRequested = false;\n\n public timedOut = false;\n\n public timer?: ReturnType<typeof setTimeout>;\n\n public constructor(\n public readonly id: string,\n public readonly command: string,\n private readonly retainBytes: number,\n ) {}\n\n public append(stream: WorkspaceOutputStream, chunk: string): void {\n if (chunk === \"\") {\n return;\n }\n\n const bytes = Buffer.byteLength(chunk, \"utf8\");\n\n this.chunks.push({ stream, text: chunk, offset: this.produced, bytes });\n this.produced += bytes;\n this.retained += bytes;\n this.trim();\n this.notify();\n }\n\n /** Drop the oldest output until at most `retainBytes` are kept. */\n private trim(): void {\n while (this.retained > this.retainBytes && this.chunks.length > 0) {\n const oldest = this.chunks[0];\n\n if (oldest === undefined) {\n return;\n }\n\n const excess = this.retained - this.retainBytes;\n\n if (oldest.bytes <= excess) {\n this.chunks.shift();\n this.retained -= oldest.bytes;\n\n continue;\n }\n\n const kept = tailUtf8(oldest.text, oldest.bytes - excess);\n\n this.retained -= oldest.bytes - kept.bytes;\n oldest.offset += oldest.bytes - kept.bytes;\n oldest.text = kept.text;\n oldest.bytes = kept.bytes;\n }\n }\n\n public finish(exitCode: number): void {\n if (this.status !== \"running\") {\n return;\n }\n\n this.exitCode = exitCode;\n this.status = this.timedOut ? \"timed-out\" : this.killRequested ? \"killed\" : \"exited\";\n this.endedAt = new Date().toISOString();\n\n if (this.timer !== undefined) {\n clearTimeout(this.timer);\n }\n\n this.notify();\n }\n\n private notify(): void {\n for (const listener of [...this.listeners]) {\n listener();\n }\n }\n\n /** Resolve on the next output or state change (or after `ms`, or on abort). */\n public waitForChange(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal?.aborted) {\n resolve();\n\n return;\n }\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const done = (): void => {\n if (timer !== undefined) {\n clearTimeout(timer);\n }\n\n this.listeners.delete(done);\n signal?.removeEventListener(\"abort\", done);\n resolve();\n };\n\n timer = setTimeout(done, ms);\n timer.unref?.();\n this.listeners.add(done);\n signal?.addEventListener(\"abort\", done, { once: true });\n });\n }\n\n public info(): ShellJobInfo {\n return {\n id: this.id,\n command: this.command,\n status: this.status,\n exitCode: this.exitCode,\n ...(this.process?.pid !== undefined ? { pid: this.process.pid } : {}),\n startedAt: this.startedAt,\n ...(this.endedAt !== undefined ? { endedAt: this.endedAt } : {}),\n };\n }\n\n /** Output produced after byte `since`, plus how much of it is gone. */\n public read(since: number): ShellOutputResult {\n const from = Math.min(Math.max(0, Math.floor(since)), this.produced);\n const firstRetained = this.chunks[0]?.offset ?? this.produced;\n const droppedBytes = Math.max(0, firstRetained - from);\n let stdout = \"\";\n let stderr = \"\";\n\n for (const chunk of this.chunks) {\n const end = chunk.offset + chunk.bytes;\n\n if (end <= from) {\n continue;\n }\n\n const text = chunk.offset >= from ? chunk.text : skipUtf8(chunk.text, from - chunk.offset);\n\n if (chunk.stream === \"stdout\") {\n stdout += text;\n } else {\n stderr += text;\n }\n }\n\n this.readCursor = this.produced;\n\n return {\n ...this.info(),\n stdout,\n stderr,\n cursor: this.produced,\n droppedBytes,\n truncated: droppedBytes > 0,\n };\n }\n}\n\n/**\n * The background commands one workspace owns. Shared by every projection\n * (`readonly()`, `scope()`) of the workspace, so `dispose()` on any of them\n * stops them all.\n */\nexport class ShellJobs {\n private readonly jobs = new Map<string, ShellJob>();\n\n private nextId = 1;\n\n private disposed = false;\n\n /** Whether {@link dispose} has run. */\n public get isDisposed(): boolean {\n return this.disposed;\n }\n\n /** Start a command as a background job. */\n public start(spec: ShellJobSpec): ShellJobInfo {\n if (this.disposed) {\n throw new WorkspacePolicyError(\n \"The workspace has been disposed; it cannot start new commands.\",\n { type: \"denied-command\", reason: \"not-allowed\", command: spec.command },\n );\n }\n\n const running = [...this.jobs.values()].filter((job) => job.status === \"running\").length;\n\n if (running >= spec.maxJobs) {\n throw new WorkspacePolicyError(\n `${running} background commands are already running (the limit is ${spec.maxJobs}). Stop one with shell_kill first.`,\n { type: \"denied-command\", reason: \"not-allowed\", command: spec.command },\n );\n }\n\n const job = new ShellJob(`job-${this.nextId++}`, spec.command, spec.retainBytes);\n\n this.jobs.set(job.id, job);\n\n const handle = spec.launch((stream, chunk) => job.append(stream, chunk));\n\n job.process = handle;\n\n if (spec.timeoutMs !== undefined && spec.timeoutMs > 0) {\n job.timer = setTimeout(() => {\n if (job.status === \"running\") {\n job.timedOut = true;\n handle.kill();\n }\n }, spec.timeoutMs);\n job.timer.unref?.();\n }\n\n void handle.done.then(({ exitCode }) => job.finish(exitCode));\n this.prune();\n\n return job.info();\n }\n\n /** Forget the oldest finished jobs beyond {@link MAX_FINISHED_JOBS}. */\n private prune(): void {\n const finished = [...this.jobs.values()].filter((job) => job.status !== \"running\");\n\n for (const job of finished.slice(0, Math.max(0, finished.length - MAX_FINISHED_JOBS))) {\n this.jobs.delete(job.id);\n }\n }\n\n private get(id: string): ShellJob {\n const job = this.jobs.get(id);\n\n if (job === undefined) {\n throw new WorkspaceJobError(\n `No background command with id \"${id}\" exists in this workspace.`,\n {\n type: \"not-found\",\n jobId: id,\n },\n );\n }\n\n return job;\n }\n\n /** New output of a job (see {@link ShellOutputOptions}). */\n public async output(id: string, opts?: ShellOutputOptions): Promise<ShellOutputResult> {\n const job = this.get(id);\n const since = opts?.since ?? job.readCursor;\n const waitMs = Math.min(Math.max(0, opts?.waitMs ?? 0), MAX_OUTPUT_WAIT_MS);\n\n if (waitMs > 0 && job.status === \"running\" && job.produced <= since) {\n await job.waitForChange(waitMs, opts?.signal);\n }\n\n return job.read(since);\n }\n\n /** Kill a job's process tree and wait (bounded) for it to stop. */\n public async kill(id: string): Promise<ShellJobInfo> {\n const job = this.get(id);\n\n await this.stop(job);\n\n return job.info();\n }\n\n private async stop(job: ShellJob): Promise<void> {\n if (job.status !== \"running\" || job.process === undefined) {\n return;\n }\n\n job.killRequested = true;\n job.process.kill();\n await Promise.race([job.process.done, delay(KILL_SETTLE_MS)]);\n }\n\n /** Every job, oldest first. */\n public list(): ShellJobInfo[] {\n return [...this.jobs.values()].map((job) => job.info());\n }\n\n /** Kill every running job and refuse new ones. Idempotent. */\n public async dispose(): Promise<void> {\n this.disposed = true;\n await Promise.all([...this.jobs.values()].map((job) => this.stop(job)));\n }\n}\n"],"mappings":"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"}
|
package/esm/todos.d.mts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { TodoListResult, WorkspaceTodoInput } from "./contracts/tool-io.type.mjs";
|
|
2
|
+
//#region ../../packages/ai-workspace/src/todos.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The in-memory todo list of one workspace (shared by its projections).
|
|
5
|
+
* `write` replaces the whole list, the way an agent's plan tool usually
|
|
6
|
+
* works: the model sends the full updated list every time.
|
|
7
|
+
*/
|
|
8
|
+
declare class TodoStore {
|
|
9
|
+
private items;
|
|
10
|
+
/** The current list and a count per status. */
|
|
11
|
+
read(): TodoListResult;
|
|
12
|
+
/**
|
|
13
|
+
* Replace the list. Items without an `id` get their 1-based position as
|
|
14
|
+
* one. Throws a `RangeError` naming every problem when the list is invalid
|
|
15
|
+
* (too long, empty content, unknown status, duplicate id); the old list is
|
|
16
|
+
* kept in that case.
|
|
17
|
+
*/
|
|
18
|
+
write(todos: readonly WorkspaceTodoInput[]): TodoListResult;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { TodoStore };
|
|
22
|
+
//# sourceMappingURL=todos.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"todos.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/todos.ts"],"mappings":";;;;;;;cA+Da,SAAA;EAAA,QACH,KAAA;EAuB0D;EApB3D,IAAA,IAAQ,cAAA;;;;;;;EAoBR,KAAA,CAAM,KAAA,WAAgB,kBAAA,KAAuB,cAAA;AAAA"}
|
package/esm/todos.mjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//#region ../../packages/ai-workspace/src/todos.ts
|
|
2
|
+
/** Most items a todo list may hold. */
|
|
3
|
+
const MAX_TODOS = 100;
|
|
4
|
+
/** Longest `content` one todo item may have. */
|
|
5
|
+
const MAX_TODO_CONTENT_LENGTH = 1e3;
|
|
6
|
+
/** The statuses a todo item may have, in display order. */
|
|
7
|
+
const TODO_STATUSES = [
|
|
8
|
+
"pending",
|
|
9
|
+
"in_progress",
|
|
10
|
+
"completed"
|
|
11
|
+
];
|
|
12
|
+
/**
|
|
13
|
+
* Validate a whole replacement list. Returns the problems found (empty when
|
|
14
|
+
* the list is valid) so the tool schema and the direct method share one
|
|
15
|
+
* set of rules.
|
|
16
|
+
*/
|
|
17
|
+
function todoListProblems(todos) {
|
|
18
|
+
const problems = [];
|
|
19
|
+
const ids = /* @__PURE__ */ new Set();
|
|
20
|
+
if (todos.length > 100) problems.push(`a todo list holds at most ${100} items (got ${todos.length})`);
|
|
21
|
+
todos.forEach((todo, index) => {
|
|
22
|
+
if (todo.content.trim() === "") problems.push(`todos[${index}].content must not be empty`);
|
|
23
|
+
if (todo.content.length > 1e3) problems.push(`todos[${index}].content is longer than ${MAX_TODO_CONTENT_LENGTH} characters`);
|
|
24
|
+
if (!TODO_STATUSES.includes(todo.status)) problems.push(`todos[${index}].status must be one of ${TODO_STATUSES.join(", ")}`);
|
|
25
|
+
const id = todo.id ?? String(index + 1);
|
|
26
|
+
if (ids.has(id)) problems.push(`todos[${index}].id "${id}" is used more than once`);
|
|
27
|
+
ids.add(id);
|
|
28
|
+
});
|
|
29
|
+
return problems;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The in-memory todo list of one workspace (shared by its projections).
|
|
33
|
+
* `write` replaces the whole list, the way an agent's plan tool usually
|
|
34
|
+
* works: the model sends the full updated list every time.
|
|
35
|
+
*/
|
|
36
|
+
var TodoStore = class {
|
|
37
|
+
constructor() {
|
|
38
|
+
this.items = [];
|
|
39
|
+
}
|
|
40
|
+
/** The current list and a count per status. */
|
|
41
|
+
read() {
|
|
42
|
+
const counts = {
|
|
43
|
+
pending: 0,
|
|
44
|
+
in_progress: 0,
|
|
45
|
+
completed: 0
|
|
46
|
+
};
|
|
47
|
+
for (const item of this.items) counts[item.status]++;
|
|
48
|
+
return {
|
|
49
|
+
todos: this.items.map((item) => ({ ...item })),
|
|
50
|
+
counts
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Replace the list. Items without an `id` get their 1-based position as
|
|
55
|
+
* one. Throws a `RangeError` naming every problem when the list is invalid
|
|
56
|
+
* (too long, empty content, unknown status, duplicate id); the old list is
|
|
57
|
+
* kept in that case.
|
|
58
|
+
*/
|
|
59
|
+
write(todos) {
|
|
60
|
+
const problems = todoListProblems(todos);
|
|
61
|
+
if (problems.length > 0) throw new RangeError(`Invalid todo list: ${problems.join("; ")}.`);
|
|
62
|
+
this.items = todos.map((todo, index) => ({
|
|
63
|
+
id: todo.id ?? String(index + 1),
|
|
64
|
+
content: todo.content.trim(),
|
|
65
|
+
status: todo.status
|
|
66
|
+
}));
|
|
67
|
+
return this.read();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
export { TODO_STATUSES, TodoStore, todoListProblems };
|
|
73
|
+
//# sourceMappingURL=todos.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"todos.mjs","names":[],"sources":["../../../../../../../packages/ai-workspace/src/todos.ts"],"sourcesContent":["import type {\n TodoListResult,\n WorkspaceTodo,\n WorkspaceTodoInput,\n WorkspaceTodoStatus,\n} from \"./contracts\";\n\n/** Most items a todo list may hold. */\nexport const MAX_TODOS = 100;\n\n/** Longest `content` one todo item may have. */\nexport const MAX_TODO_CONTENT_LENGTH = 1_000;\n\n/** The statuses a todo item may have, in display order. */\nexport const TODO_STATUSES: readonly WorkspaceTodoStatus[] = [\n \"pending\",\n \"in_progress\",\n \"completed\",\n];\n\n/**\n * Validate a whole replacement list. Returns the problems found (empty when\n * the list is valid) so the tool schema and the direct method share one\n * set of rules.\n */\nexport function todoListProblems(todos: readonly WorkspaceTodoInput[]): string[] {\n const problems: string[] = [];\n const ids = new Set<string>();\n\n if (todos.length > MAX_TODOS) {\n problems.push(`a todo list holds at most ${MAX_TODOS} items (got ${todos.length})`);\n }\n\n todos.forEach((todo, index) => {\n if (todo.content.trim() === \"\") {\n problems.push(`todos[${index}].content must not be empty`);\n }\n\n if (todo.content.length > MAX_TODO_CONTENT_LENGTH) {\n problems.push(`todos[${index}].content is longer than ${MAX_TODO_CONTENT_LENGTH} characters`);\n }\n\n if (!TODO_STATUSES.includes(todo.status)) {\n problems.push(`todos[${index}].status must be one of ${TODO_STATUSES.join(\", \")}`);\n }\n\n const id = todo.id ?? String(index + 1);\n\n if (ids.has(id)) {\n problems.push(`todos[${index}].id \"${id}\" is used more than once`);\n }\n\n ids.add(id);\n });\n\n return problems;\n}\n\n/**\n * The in-memory todo list of one workspace (shared by its projections).\n * `write` replaces the whole list, the way an agent's plan tool usually\n * works: the model sends the full updated list every time.\n */\nexport class TodoStore {\n private items: WorkspaceTodo[] = [];\n\n /** The current list and a count per status. */\n public read(): TodoListResult {\n const counts: Record<WorkspaceTodoStatus, number> = {\n pending: 0,\n in_progress: 0,\n completed: 0,\n };\n\n for (const item of this.items) {\n counts[item.status]++;\n }\n\n return { todos: this.items.map((item) => ({ ...item })), counts };\n }\n\n /**\n * Replace the list. Items without an `id` get their 1-based position as\n * one. Throws a `RangeError` naming every problem when the list is invalid\n * (too long, empty content, unknown status, duplicate id); the old list is\n * kept in that case.\n */\n public write(todos: readonly WorkspaceTodoInput[]): TodoListResult {\n const problems = todoListProblems(todos);\n\n if (problems.length > 0) {\n throw new RangeError(`Invalid todo list: ${problems.join(\"; \")}.`);\n }\n\n this.items = todos.map((todo, index) => ({\n id: todo.id ?? String(index + 1),\n content: todo.content.trim(),\n status: todo.status,\n }));\n\n return this.read();\n }\n}\n"],"mappings":";;AAQA,MAAa,YAAY;;AAGzB,MAAa,0BAA0B;;AAGvC,MAAa,gBAAgD;CAC3D;CACA;CACA;AACF;;;;;;AAOA,SAAgB,iBAAiB,OAAgD;CAC/E,MAAM,WAAqB,CAAC;CAC5B,MAAM,sBAAM,IAAI,IAAY;CAE5B,IAAI,MAAM,cACR,SAAS,KAAK,iCAAuC,cAAc,MAAM,OAAO,EAAE;CAGpF,MAAM,SAAS,MAAM,UAAU;EAC7B,IAAI,KAAK,QAAQ,KAAK,MAAM,IAC1B,SAAS,KAAK,SAAS,MAAM,4BAA4B;EAG3D,IAAI,KAAK,QAAQ,cACf,SAAS,KAAK,SAAS,MAAM,2BAA2B,wBAAwB,YAAY;EAG9F,IAAI,CAAC,cAAc,SAAS,KAAK,MAAM,GACrC,SAAS,KAAK,SAAS,MAAM,0BAA0B,cAAc,KAAK,IAAI,GAAG;EAGnF,MAAM,KAAK,KAAK,MAAM,OAAO,QAAQ,CAAC;EAEtC,IAAI,IAAI,IAAI,EAAE,GACZ,SAAS,KAAK,SAAS,MAAM,QAAQ,GAAG,yBAAyB;EAGnE,IAAI,IAAI,EAAE;CACZ,CAAC;CAED,OAAO;AACT;;;;;;AAOA,IAAa,YAAb,MAAuB;;eACY,CAAC;;;CAGlC,AAAO,OAAuB;EAC5B,MAAM,SAA8C;GAClD,SAAS;GACT,aAAa;GACb,WAAW;EACb;EAEA,KAAK,MAAM,QAAQ,KAAK,OACtB,OAAO,KAAK,OAAO;EAGrB,OAAO;GAAE,OAAO,KAAK,MAAM,KAAK,UAAU,EAAE,GAAG,KAAK,EAAE;GAAG;EAAO;CAClE;;;;;;;CAQA,AAAO,MAAM,OAAsD;EACjE,MAAM,WAAW,iBAAiB,KAAK;EAEvC,IAAI,SAAS,SAAS,GACpB,MAAM,IAAI,WAAW,sBAAsB,SAAS,KAAK,IAAI,EAAE,EAAE;EAGnE,KAAK,QAAQ,MAAM,KAAK,MAAM,WAAW;GACvC,IAAI,KAAK,MAAM,OAAO,QAAQ,CAAC;GAC/B,SAAS,KAAK,QAAQ,KAAK;GAC3B,QAAQ,KAAK;EACf,EAAE;EAEF,OAAO,KAAK,KAAK;CACnB;AACF"}
|
|
@@ -2,7 +2,7 @@ import { EditFileInput, EditFileResult } 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
|
|
5
|
+
//#region ../../packages/ai-workspace/src/tools/edit-file.d.ts
|
|
6
6
|
/**
|
|
7
7
|
* Build the agent-facing `edit_file` tool over a workspace's policy-
|
|
8
8
|
* enforced {@link WorkspaceOps}.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit-file.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"edit-file.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/edit-file.ts"],"mappings":";;;;;;;;;AAiDA;;;;;;;;;;;;;;;;;AAG6C;;;;;;;;iBAH7B,gBAAA,CACd,GAAA,EAAK,YAAA,EACL,OAAA;EAAY,IAAA;AAAA,IACX,YAAA,CAAa,aAAA,EAAe,cAAA"}
|
package/esm/tools/edit-file.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { objectSchema, optionalBooleanField, optionalStringField, stringField } from "./schema.mjs";
|
|
2
|
+
import { EDIT_FILE_JSON_SCHEMA } from "./json-schemas.mjs";
|
|
2
3
|
import { tool } from "@warlock.js/ai";
|
|
3
4
|
|
|
4
|
-
//#region
|
|
5
|
+
//#region ../../packages/ai-workspace/src/tools/edit-file.ts
|
|
5
6
|
/** Default tool name exposed to the LLM. */
|
|
6
7
|
const DEFAULT_NAME = "edit_file";
|
|
7
8
|
/** Input schema for the `edit_file` tool. */
|
|
@@ -11,7 +12,7 @@ const inputSchema = objectSchema({
|
|
|
11
12
|
newString: stringField(),
|
|
12
13
|
replaceAll: optionalBooleanField(),
|
|
13
14
|
expectHash: optionalStringField()
|
|
14
|
-
});
|
|
15
|
+
}, EDIT_FILE_JSON_SCHEMA);
|
|
15
16
|
/**
|
|
16
17
|
* Build the agent-facing `edit_file` tool over a workspace's policy-
|
|
17
18
|
* enforced {@link WorkspaceOps}.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit-file.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"edit-file.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/edit-file.ts"],"sourcesContent":["import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { objectSchema, optionalBooleanField, optionalStringField, stringField } from \"./schema\";\nimport { EDIT_FILE_JSON_SCHEMA } from \"./json-schemas\";\nimport type { EditFileInput, EditFileResult, WorkspaceOps } from \"../contracts\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"edit_file\";\n\n/** Input schema for the `edit_file` tool. */\nconst inputSchema = objectSchema<EditFileInput>(\n {\n path: stringField(),\n oldString: stringField(),\n newString: stringField(),\n replaceAll: optionalBooleanField(),\n expectHash: optionalStringField(),\n },\n EDIT_FILE_JSON_SCHEMA,\n);\n\n/**\n * Build the agent-facing `edit_file` tool over a workspace's policy-\n * enforced {@link WorkspaceOps}.\n *\n * The tool validates `{ path, oldString, newString, replaceAll?,\n * expectHash? }` against a Standard Schema, then delegates to\n * `ops.editFile`, which applies the exact-string replacement under the\n * read-before-edit guard and returns the replacement count plus the\n * post-edit `hash`.\n *\n * **Errors flow as data.** A non-unique `oldString` (without\n * `replaceAll`), a missing `oldString`, or a stale `expectHash` cause\n * `ops` to throw a `WorkspaceEditError`; the `tool()` wrapper catches it\n * and surfaces it in the returned `{ error }` field — `invoke()` never\n * throws — so the agent can re-read and retry.\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 editTool = makeEditFileTool(ops);\n * const { data, error } = await editTool.invoke({\n * path: \"src/index.ts\",\n * oldString: \"const a = 1;\",\n * newString: \"const a = 2;\",\n * expectHash,\n * });\n * if (error) console.warn(error.message); // e.g. stale-hash → re-read\n */\nexport function makeEditFileTool(\n ops: WorkspaceOps,\n options?: { name?: string },\n): ToolContract<EditFileInput, EditFileResult> {\n return tool<EditFileInput, EditFileResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Replace an exact substring in a workspace file. oldString must match \" +\n \"uniquely unless replaceAll is set. Pass expectHash (from read_file) to \" +\n \"reject the edit if the file changed since you read it. Returns the \" +\n \"number of replacements and the new content hash.\",\n input: inputSchema,\n async execute(input) {\n return ops.editFile(input);\n },\n });\n}\n"],"mappings":";;;;;;AAMA,MAAM,eAAe;;AAGrB,MAAM,cAAc,aAClB;CACE,MAAM,YAAY;CAClB,WAAW,YAAY;CACvB,WAAW,YAAY;CACvB,YAAY,qBAAqB;CACjC,YAAY,oBAAoB;AAClC,GACA,qBACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,iBACd,KACA,SAC6C;CAC7C,OAAO,KAAoC;EACzC,MAAM,SAAS,QAAQ;EACvB,aACE;EAIF,OAAO;EACP,MAAM,QAAQ,OAAO;GACnB,OAAO,IAAI,SAAS,KAAK;EAC3B;CACF,CAAC;AACH"}
|
package/esm/tools/glob.d.mts
CHANGED
|
@@ -2,7 +2,7 @@ import { GlobInput, GlobResult } 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
|
|
5
|
+
//#region ../../packages/ai-workspace/src/tools/glob.d.ts
|
|
6
6
|
/** Options accepted by {@link makeGlobTool} to customize the vended tool. */
|
|
7
7
|
interface MakeGlobToolOptions {
|
|
8
8
|
/**
|
package/esm/tools/glob.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"glob.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"glob.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/glob.ts"],"mappings":";;;;;;UAMiB,mBAAA;;;AAAjB;;;EAME,IAAI;AAAA;AAmCN;;;;;;;;;;;;;;;;;;AAGqC;AAHrC,iBAAgB,YAAA,CACd,GAAA,EAAK,YAAA,EACL,OAAA,GAAU,mBAAA,GACT,YAAA,CAAa,SAAA,EAAW,UAAA"}
|
package/esm/tools/glob.mjs
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { objectSchema, stringField } from "./schema.mjs";
|
|
2
|
+
import { GLOB_JSON_SCHEMA } from "./json-schemas.mjs";
|
|
2
3
|
import { tool } from "@warlock.js/ai";
|
|
3
4
|
|
|
4
|
-
//#region
|
|
5
|
+
//#region ../../packages/ai-workspace/src/tools/glob.ts
|
|
5
6
|
/**
|
|
6
7
|
* Standard Schema for {@link GlobInput} — a single required `pattern`
|
|
7
8
|
* string. Built on the package's shared, dependency-free schema builders
|
|
8
9
|
* (no schema library, matching the validator idiom the `@warlock.js/ai`
|
|
9
10
|
* tool runtime expects).
|
|
10
11
|
*/
|
|
11
|
-
const globInputSchema = objectSchema({ pattern: stringField() });
|
|
12
|
+
const globInputSchema = objectSchema({ pattern: stringField() }, GLOB_JSON_SCHEMA);
|
|
12
13
|
/**
|
|
13
14
|
* Build the agent-facing `glob` tool — resolve a glob pattern to the
|
|
14
15
|
* matching workspace-relative paths within the jail. The returned
|
package/esm/tools/glob.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"glob.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"glob.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/glob.ts"],"sourcesContent":["import { type ToolContract, tool } from \"@warlock.js/ai\";\nimport { objectSchema, stringField } from \"./schema\";\nimport { GLOB_JSON_SCHEMA } from \"./json-schemas\";\nimport type { GlobInput, GlobResult, WorkspaceOps } from \"../contracts\";\n\n/** Options accepted by {@link makeGlobTool} to customize the vended tool. */\nexport interface MakeGlobToolOptions {\n /**\n * Override the tool name the LLM sees. Defaults to `\"glob\"`. Use a\n * custom name when wiring several workspaces into one agent so each\n * path-match surface is addressable.\n */\n name?: string;\n}\n\n/**\n * Standard Schema for {@link GlobInput} — a single required `pattern`\n * string. Built on the package's shared, dependency-free schema builders\n * (no schema library, matching the validator idiom the `@warlock.js/ai`\n * tool runtime expects).\n */\nconst globInputSchema = objectSchema<GlobInput>(\n {\n pattern: stringField(),\n },\n GLOB_JSON_SCHEMA,\n);\n\n/**\n * Build the agent-facing `glob` tool — resolve a glob pattern to the\n * matching workspace-relative paths within the jail. The returned\n * {@link ToolContract} validates the LLM's arguments, delegates to\n * {@link WorkspaceOps.glob} (which returns a bare sorted `string[]`), and\n * wraps the result in a {@link GlobResult} so the agent always reads a\n * stable `{ paths }` envelope. The jail and `denyPaths` filtering are\n * enforced in the shared ops layer; a policy violation surfaces as typed\n * tool-error *data* via the runtime's `invoke()` wrapper.\n *\n * @param ops - The policy-enforced operation layer to delegate to.\n * @param options - Optional `{ name }` override for the vended tool name.\n * @returns A {@link ToolContract} the agent can call as `glob`.\n *\n * @example\n * const glob = makeGlobTool(ops);\n * const { data } = await glob.invoke({ pattern: \"src/models/**\\/*.ts\" });\n * console.log(data?.paths);\n */\nexport function makeGlobTool(\n ops: WorkspaceOps,\n options?: MakeGlobToolOptions,\n): ToolContract<GlobInput, GlobResult> {\n return tool<GlobInput, GlobResult>({\n name: options?.name ?? \"glob\",\n description:\n \"Find files in the workspace whose path matches a glob pattern \" +\n \"(supports `*`, `**`, and `?`). Returns the matching \" +\n \"workspace-relative paths, sorted.\",\n action: (input) => `Finding files matching ${input.pattern}`,\n input: globInputSchema,\n async execute(input) {\n const paths = await ops.glob(input.pattern);\n\n return { paths };\n },\n });\n}\n"],"mappings":";;;;;;;;;;;AAqBA,MAAM,kBAAkB,aACtB,EACE,SAAS,YAAY,EACvB,GACA,gBACF;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,aACd,KACA,SACqC;CACrC,OAAO,KAA4B;EACjC,MAAM,SAAS,QAAQ;EACvB,aACE;EAGF,SAAS,UAAU,0BAA0B,MAAM;EACnD,OAAO;EACP,MAAM,QAAQ,OAAO;GAGnB,OAAO,EAAE,aAFW,IAAI,KAAK,MAAM,OAAO,EAE3B;EACjB;CACF,CAAC;AACH"}
|
package/esm/tools/grep.d.mts
CHANGED
|
@@ -2,7 +2,7 @@ import { GrepInput, GrepResult } 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
|
|
5
|
+
//#region ../../packages/ai-workspace/src/tools/grep.d.ts
|
|
6
6
|
/** Options accepted by {@link makeGrepTool} to customize the vended tool. */
|
|
7
7
|
interface MakeGrepToolOptions {
|
|
8
8
|
/**
|
package/esm/tools/grep.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grep.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"grep.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/grep.ts"],"mappings":";;;;;;UAMiB,mBAAA;;;AAAjB;;;EAME,IAAI;AAAA;AAoCN;;;;;;;;;;;;;;;;;;AAAA,iBAAgB,YAAA,CACd,GAAA,EAAK,YAAA,EACL,OAAA,GAAU,mBAAA,GACT,YAAA,CAAa,SAAA,EAAW,UAAA"}
|
package/esm/tools/grep.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { objectSchema, optionalBooleanField, optionalStringField, stringField } from "./schema.mjs";
|
|
2
|
+
import { GREP_JSON_SCHEMA } from "./json-schemas.mjs";
|
|
2
3
|
import { tool } from "@warlock.js/ai";
|
|
3
4
|
|
|
4
|
-
//#region
|
|
5
|
+
//#region ../../packages/ai-workspace/src/tools/grep.ts
|
|
5
6
|
/**
|
|
6
7
|
* Standard Schema for {@link GrepInput} — `pattern` is a required string;
|
|
7
8
|
* `glob` and `ignoreCase` are optional. Built on the package's shared,
|
|
@@ -12,7 +13,7 @@ const grepInputSchema = objectSchema({
|
|
|
12
13
|
pattern: stringField(),
|
|
13
14
|
glob: optionalStringField(),
|
|
14
15
|
ignoreCase: optionalBooleanField()
|
|
15
|
-
});
|
|
16
|
+
}, GREP_JSON_SCHEMA);
|
|
16
17
|
/**
|
|
17
18
|
* Build the agent-facing `grep` tool — a regex content search across the
|
|
18
19
|
* jailed file set. The returned {@link ToolContract} validates the LLM's
|
package/esm/tools/grep.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grep.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"grep.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/grep.ts"],"sourcesContent":["import { type ToolContract, tool } from \"@warlock.js/ai\";\nimport { objectSchema, optionalBooleanField, optionalStringField, stringField } from \"./schema\";\nimport { GREP_JSON_SCHEMA } from \"./json-schemas\";\nimport type { GrepInput, GrepResult, WorkspaceOps } from \"../contracts\";\n\n/** Options accepted by {@link makeGrepTool} to customize the vended tool. */\nexport interface MakeGrepToolOptions {\n /**\n * Override the tool name the LLM sees. Defaults to `\"grep\"`. Use a\n * custom name when wiring several workspaces into one agent so each\n * search surface is addressable.\n */\n name?: string;\n}\n\n/**\n * Standard Schema for {@link GrepInput} — `pattern` is a required string;\n * `glob` and `ignoreCase` are optional. Built on the package's shared,\n * dependency-free schema builders (no schema library, matching the\n * validator idiom the `@warlock.js/ai` tool runtime expects).\n */\nconst grepInputSchema = objectSchema<GrepInput>(\n {\n pattern: stringField(),\n glob: optionalStringField(),\n ignoreCase: optionalBooleanField(),\n },\n GREP_JSON_SCHEMA,\n);\n\n/**\n * Build the agent-facing `grep` tool — a regex content search across the\n * jailed file set. The returned {@link ToolContract} validates the LLM's\n * arguments, then delegates verbatim to {@link WorkspaceOps.grep}, so the\n * policy jail, `denyPaths` filtering, and match cap are enforced in the\n * single shared ops layer rather than duplicated here. A policy violation\n * (e.g. a jail-resolution failure) surfaces as typed tool-error *data*\n * via the runtime's `invoke()` wrapper, never as a thrown run-killer.\n *\n * @param ops - The policy-enforced operation layer to delegate to.\n * @param options - Optional `{ name }` override for the vended tool name.\n * @returns A {@link ToolContract} the agent can call as `grep`.\n *\n * @example\n * const grep = makeGrepTool(ops);\n * const { data } = await grep.invoke({ pattern: \"TODO\", glob: \"src/*.ts\" });\n * console.log(data?.total, data?.matches);\n */\nexport function makeGrepTool(\n ops: WorkspaceOps,\n options?: MakeGrepToolOptions,\n): ToolContract<GrepInput, GrepResult> {\n return tool<GrepInput, GrepResult>({\n name: options?.name ?? \"grep\",\n description:\n \"Search file contents across the workspace for a regular-expression \" +\n \"pattern. Optionally narrow the scanned files with a glob and match \" +\n \"case-insensitively. Returns every matching line with its file path \" +\n \"and 1-based line number.\",\n action: (input) => `Searching for /${input.pattern}/`,\n input: grepInputSchema,\n async execute(input) {\n return ops.grep(input.pattern, {\n glob: input.glob,\n ignoreCase: input.ignoreCase,\n });\n },\n });\n}\n"],"mappings":";;;;;;;;;;;AAqBA,MAAM,kBAAkB,aACtB;CACE,SAAS,YAAY;CACrB,MAAM,oBAAoB;CAC1B,YAAY,qBAAqB;AACnC,GACA,gBACF;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,aACd,KACA,SACqC;CACrC,OAAO,KAA4B;EACjC,MAAM,SAAS,QAAQ;EACvB,aACE;EAIF,SAAS,UAAU,kBAAkB,MAAM,QAAQ;EACnD,OAAO;EACP,MAAM,QAAQ,OAAO;GACnB,OAAO,IAAI,KAAK,MAAM,SAAS;IAC7B,MAAM,MAAM;IACZ,YAAY,MAAM;GACpB,CAAC;EACH;CACF,CAAC;AACH"}
|