indusagi-coding-agent 0.1.62 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/entry.js +5395 -1509
- package/dist/guardrails.js +2031 -0
- package/dist/index.js +18371 -0
- package/dist/types/boot/contract.d.ts +2 -0
- package/dist/types/boot/index.d.ts +1 -1
- package/dist/types/boot/runners/addon-wiring.d.ts +103 -0
- package/dist/types/boot/runners/addon-wiring.test.d.ts +19 -0
- package/dist/types/boot/runners/checkpoint.d.ts +133 -0
- package/dist/types/boot/runners/checkpoint.test.d.ts +12 -0
- package/dist/types/boot/runners/delegate-runner.d.ts +83 -0
- package/dist/types/boot/runners/delegate-runner.test.d.ts +13 -0
- package/dist/types/boot/runners/index.d.ts +2 -0
- package/dist/types/boot/runners/memdir.d.ts +103 -0
- package/dist/types/boot/runners/memdir.test.d.ts +12 -0
- package/dist/types/boot/runners/read-state.d.ts +82 -0
- package/dist/types/boot/runners/read-state.test.d.ts +10 -0
- package/dist/types/boot/runners/session.d.ts +37 -2
- package/dist/types/boot/runners/session.test.d.ts +10 -0
- package/dist/types/briefing/context-docs.d.ts +38 -0
- package/dist/types/briefing/context-docs.test.d.ts +18 -0
- package/dist/types/briefing/index.d.ts +2 -0
- package/dist/types/capability-deck/cards/index.d.ts +6 -0
- package/dist/types/capability-deck/cards/memory-card.d.ts +9 -10
- package/dist/types/capability-deck/cards/plan-file.d.ts +56 -0
- package/dist/types/capability-deck/cards/plan-tools.d.ts +97 -0
- package/dist/types/capability-deck/cards/plan-tools.test.d.ts +9 -0
- package/dist/types/capability-deck/checkpoint.int.test.d.ts +25 -0
- package/dist/types/capability-deck/index.d.ts +1 -1
- package/dist/types/capability-deck/read-edit-gate.int.test.d.ts +21 -0
- package/dist/types/conductor/bash-guard.d.ts +106 -0
- package/dist/types/conductor/bash-guard.test.d.ts +17 -0
- package/dist/types/conductor/conductor.d.ts +37 -6
- package/dist/types/conductor/contract.d.ts +214 -2
- package/dist/types/conductor/diagnostics.d.ts +183 -0
- package/dist/types/conductor/diagnostics.test.d.ts +10 -0
- package/dist/types/conductor/index.d.ts +4 -1
- package/dist/types/conductor/permission-gate.integration.test.d.ts +22 -0
- package/dist/types/conductor/permission-wiring.test.d.ts +14 -0
- package/dist/types/conductor/permissions.d.ts +217 -0
- package/dist/types/conductor/permissions.test.d.ts +12 -0
- package/dist/types/conductor/plan-mode.integration.test.d.ts +23 -0
- package/dist/types/conductor/post-edit-diagnostics.test.d.ts +13 -0
- package/dist/types/conductor/transcript-store/serialize.test.d.ts +10 -0
- package/dist/types/conductor/transcript-store/store.d.ts +18 -0
- package/dist/types/console/components/StatusBar.d.ts +14 -3
- package/dist/types/console/components/WorkingIndicator.d.ts +44 -0
- package/dist/types/console/components/WorkingIndicator.test.d.ts +9 -0
- package/dist/types/console/contract.d.ts +2 -1
- package/dist/types/console/input/keymap.d.ts +10 -1
- package/dist/types/console/overlays/approval-queue.d.ts +71 -0
- package/dist/types/console/overlays/approval.d.ts +104 -0
- package/dist/types/console/overlays/approval.test.d.ts +17 -0
- package/dist/types/console/overlays/host.d.ts +4 -3
- package/dist/types/console/overlays/index.d.ts +2 -0
- package/dist/types/guardrails.d.ts +33 -0
- package/dist/types/launch/contract.d.ts +2 -0
- package/dist/types/launch/index.d.ts +1 -1
- package/dist/types/launch/oauth.d.ts +13 -0
- package/dist/types/settings/contract.d.ts +47 -0
- package/dist/types/settings/index.d.ts +2 -2
- package/dist/types/window-budget/condenser.d.ts +15 -1
- package/dist/types/window-budget/index.d.ts +3 -1
- package/dist/types/window-budget/microcompact.d.ts +68 -0
- package/dist/types/window-budget/microcompact.test.d.ts +16 -0
- package/dist/types/window-budget/rehydrate.d.ts +56 -0
- package/dist/types/workspace/brand.d.ts +1 -1
- package/package.json +14 -3
|
@@ -0,0 +1,2031 @@
|
|
|
1
|
+
// src/conductor/bash-guard.ts
|
|
2
|
+
var OPERATORS = ["&&", "||", ";", "|", "&", "\n"];
|
|
3
|
+
function splitTopLevel(command) {
|
|
4
|
+
const slices = [];
|
|
5
|
+
let current = "";
|
|
6
|
+
let quote;
|
|
7
|
+
let i = 0;
|
|
8
|
+
const flush = () => {
|
|
9
|
+
const trimmed = current.trim();
|
|
10
|
+
if (trimmed.length > 0) slices.push(trimmed);
|
|
11
|
+
current = "";
|
|
12
|
+
};
|
|
13
|
+
while (i < command.length) {
|
|
14
|
+
const ch = command[i];
|
|
15
|
+
if (ch === "\\" && quote !== "'") {
|
|
16
|
+
current += ch;
|
|
17
|
+
if (i + 1 < command.length) {
|
|
18
|
+
current += command[i + 1];
|
|
19
|
+
i += 2;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
i += 1;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (quote !== void 0) {
|
|
26
|
+
current += ch;
|
|
27
|
+
if (ch === quote) quote = void 0;
|
|
28
|
+
i += 1;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (ch === '"' || ch === "'") {
|
|
32
|
+
quote = ch;
|
|
33
|
+
current += ch;
|
|
34
|
+
i += 1;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
let matched;
|
|
38
|
+
for (const op of OPERATORS) {
|
|
39
|
+
if (command.startsWith(op, i)) {
|
|
40
|
+
matched = op;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (matched !== void 0) {
|
|
45
|
+
flush();
|
|
46
|
+
i += matched.length;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
current += ch;
|
|
50
|
+
i += 1;
|
|
51
|
+
}
|
|
52
|
+
flush();
|
|
53
|
+
return slices;
|
|
54
|
+
}
|
|
55
|
+
function extractSubshells(slice) {
|
|
56
|
+
const inner = [];
|
|
57
|
+
let outer = "";
|
|
58
|
+
let quote;
|
|
59
|
+
let i = 0;
|
|
60
|
+
while (i < slice.length) {
|
|
61
|
+
const ch = slice[i];
|
|
62
|
+
if (ch === "\\" && quote !== "'") {
|
|
63
|
+
outer += ch;
|
|
64
|
+
if (i + 1 < slice.length) {
|
|
65
|
+
outer += slice[i + 1];
|
|
66
|
+
i += 2;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
i += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (quote !== void 0) {
|
|
73
|
+
if (quote === '"' && ch === "`") {
|
|
74
|
+
const close = findBacktickClose(slice, i + 1);
|
|
75
|
+
inner.push(slice.slice(i + 1, close));
|
|
76
|
+
i = close + 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
outer += ch;
|
|
80
|
+
if (ch === quote) quote = void 0;
|
|
81
|
+
i += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (ch === '"' || ch === "'") {
|
|
85
|
+
quote = ch;
|
|
86
|
+
outer += ch;
|
|
87
|
+
i += 1;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (ch === "(" || ch === "$" && slice[i + 1] === "(") {
|
|
91
|
+
const start = ch === "$" ? i + 2 : i + 1;
|
|
92
|
+
const close = findParenClose(slice, start);
|
|
93
|
+
inner.push(slice.slice(start, close));
|
|
94
|
+
i = close + 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (ch === "`") {
|
|
98
|
+
const close = findBacktickClose(slice, i + 1);
|
|
99
|
+
inner.push(slice.slice(i + 1, close));
|
|
100
|
+
i = close + 1;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
outer += ch;
|
|
104
|
+
i += 1;
|
|
105
|
+
}
|
|
106
|
+
return { outer: outer.trim(), inner };
|
|
107
|
+
}
|
|
108
|
+
function findParenClose(s, start) {
|
|
109
|
+
let depth = 1;
|
|
110
|
+
let quote;
|
|
111
|
+
let i = start;
|
|
112
|
+
while (i < s.length) {
|
|
113
|
+
const ch = s[i];
|
|
114
|
+
if (ch === "\\" && quote !== "'") {
|
|
115
|
+
i += 2;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (quote !== void 0) {
|
|
119
|
+
if (ch === quote) quote = void 0;
|
|
120
|
+
i += 1;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (ch === '"' || ch === "'") {
|
|
124
|
+
quote = ch;
|
|
125
|
+
i += 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (ch === "(" || ch === "$" && s[i + 1] === "(") depth += 1;
|
|
129
|
+
else if (ch === ")") {
|
|
130
|
+
depth -= 1;
|
|
131
|
+
if (depth === 0) return i;
|
|
132
|
+
}
|
|
133
|
+
i += 1;
|
|
134
|
+
}
|
|
135
|
+
return s.length;
|
|
136
|
+
}
|
|
137
|
+
function findBacktickClose(s, start) {
|
|
138
|
+
let i = start;
|
|
139
|
+
while (i < s.length) {
|
|
140
|
+
if (s[i] === "\\") {
|
|
141
|
+
i += 2;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (s[i] === "`") return i;
|
|
145
|
+
i += 1;
|
|
146
|
+
}
|
|
147
|
+
return s.length;
|
|
148
|
+
}
|
|
149
|
+
function tokenizeArgv(slice) {
|
|
150
|
+
const tokens = [];
|
|
151
|
+
let current = "";
|
|
152
|
+
let started = false;
|
|
153
|
+
let quote;
|
|
154
|
+
let i = 0;
|
|
155
|
+
const flush = () => {
|
|
156
|
+
if (started) tokens.push(current);
|
|
157
|
+
current = "";
|
|
158
|
+
started = false;
|
|
159
|
+
};
|
|
160
|
+
while (i < slice.length) {
|
|
161
|
+
const ch = slice[i];
|
|
162
|
+
if (ch === "\\" && quote !== "'") {
|
|
163
|
+
if (i + 1 < slice.length) {
|
|
164
|
+
current += slice[i + 1];
|
|
165
|
+
started = true;
|
|
166
|
+
i += 2;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
i += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (quote !== void 0) {
|
|
173
|
+
if (ch === quote) {
|
|
174
|
+
quote = void 0;
|
|
175
|
+
} else {
|
|
176
|
+
current += ch;
|
|
177
|
+
}
|
|
178
|
+
started = true;
|
|
179
|
+
i += 1;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (ch === '"' || ch === "'") {
|
|
183
|
+
quote = ch;
|
|
184
|
+
started = true;
|
|
185
|
+
i += 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (ch === " " || ch === " ") {
|
|
189
|
+
flush();
|
|
190
|
+
i += 1;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
current += ch;
|
|
194
|
+
started = true;
|
|
195
|
+
i += 1;
|
|
196
|
+
}
|
|
197
|
+
flush();
|
|
198
|
+
return tokens;
|
|
199
|
+
}
|
|
200
|
+
var WRAPPERS = /* @__PURE__ */ new Set([
|
|
201
|
+
"env",
|
|
202
|
+
"nice",
|
|
203
|
+
"nohup",
|
|
204
|
+
"timeout",
|
|
205
|
+
"xargs",
|
|
206
|
+
"command",
|
|
207
|
+
"builtin",
|
|
208
|
+
"time",
|
|
209
|
+
"stdbuf",
|
|
210
|
+
"setsid",
|
|
211
|
+
"ionice"
|
|
212
|
+
]);
|
|
213
|
+
var ELEVATION = /* @__PURE__ */ new Set(["sudo", "doas"]);
|
|
214
|
+
function stripWrappers(argv) {
|
|
215
|
+
const rest = [...argv];
|
|
216
|
+
let elevated = false;
|
|
217
|
+
for (; ; ) {
|
|
218
|
+
const head = rest[0];
|
|
219
|
+
if (head === void 0) break;
|
|
220
|
+
const token = head.toLowerCase();
|
|
221
|
+
if (ELEVATION.has(token)) {
|
|
222
|
+
rest.shift();
|
|
223
|
+
elevated = true;
|
|
224
|
+
consumeWrapperOptions(rest, token);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (WRAPPERS.has(token)) {
|
|
228
|
+
rest.shift();
|
|
229
|
+
consumeWrapperOptions(rest, token);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(head)) {
|
|
233
|
+
rest.shift();
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
return { rest, elevated };
|
|
239
|
+
}
|
|
240
|
+
function consumeWrapperOptions(rest, wrapper) {
|
|
241
|
+
for (; ; ) {
|
|
242
|
+
const head = rest[0];
|
|
243
|
+
if (head === void 0) return;
|
|
244
|
+
if (head === "--") {
|
|
245
|
+
rest.shift();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (head.startsWith("-")) {
|
|
249
|
+
rest.shift();
|
|
250
|
+
if (!head.includes("=") && FLAG_TAKES_VALUE.has(head)) {
|
|
251
|
+
const value = rest[0];
|
|
252
|
+
if (value !== void 0 && !value.startsWith("-")) rest.shift();
|
|
253
|
+
}
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if ((wrapper === "timeout" || wrapper === "nice") && /^\d+[a-z]?$/i.test(head)) {
|
|
257
|
+
rest.shift();
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
var FLAG_TAKES_VALUE = /* @__PURE__ */ new Set(["-n", "-u", "-g", "-s", "--kill-after", "--signal", "--user"]);
|
|
264
|
+
function parseBashCommand(command) {
|
|
265
|
+
if (typeof command !== "string" || command.trim().length === 0) return [];
|
|
266
|
+
const out = [];
|
|
267
|
+
parseInto(command, out);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
function parseInto(command, out) {
|
|
271
|
+
for (const slice of splitTopLevel(command)) {
|
|
272
|
+
const { outer, inner } = extractSubshells(slice);
|
|
273
|
+
if (outer.length > 0) {
|
|
274
|
+
const argv = tokenizeArgv(outer);
|
|
275
|
+
if (argv.length > 0) {
|
|
276
|
+
const { rest, elevated } = stripWrappers(argv);
|
|
277
|
+
const name = rest[0];
|
|
278
|
+
if (name !== void 0 && name.length > 0) {
|
|
279
|
+
out.push({
|
|
280
|
+
name: name.toLowerCase(),
|
|
281
|
+
args: rest.slice(1),
|
|
282
|
+
elevated,
|
|
283
|
+
raw: slice.trim()
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
for (const body of inner) {
|
|
289
|
+
if (body.trim().length > 0) parseInto(body, out);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function catastrophicMessage(reason) {
|
|
294
|
+
return `This command is blocked: ${reason}. It is on the built-in catastrophic-command blocklist and cannot be run regardless of permission rules.`;
|
|
295
|
+
}
|
|
296
|
+
function normPath(token) {
|
|
297
|
+
const collapsed = token.replace(/\/+/g, "/");
|
|
298
|
+
if (collapsed.length > 1 && collapsed.endsWith("/")) return collapsed.slice(0, -1);
|
|
299
|
+
return collapsed;
|
|
300
|
+
}
|
|
301
|
+
var ROOT_TARGETS = /* @__PURE__ */ new Set([
|
|
302
|
+
"/",
|
|
303
|
+
"/*",
|
|
304
|
+
"~",
|
|
305
|
+
"~/",
|
|
306
|
+
"$home",
|
|
307
|
+
"$home/",
|
|
308
|
+
"/bin",
|
|
309
|
+
"/boot",
|
|
310
|
+
"/dev",
|
|
311
|
+
"/etc",
|
|
312
|
+
"/home",
|
|
313
|
+
"/lib",
|
|
314
|
+
"/opt",
|
|
315
|
+
"/proc",
|
|
316
|
+
"/root",
|
|
317
|
+
"/sbin",
|
|
318
|
+
"/srv",
|
|
319
|
+
"/sys",
|
|
320
|
+
"/usr",
|
|
321
|
+
"/var"
|
|
322
|
+
]);
|
|
323
|
+
function isRootRm(cmd) {
|
|
324
|
+
if (cmd.name !== "rm") return false;
|
|
325
|
+
let recursive = false;
|
|
326
|
+
const targets = [];
|
|
327
|
+
for (const arg of cmd.args) {
|
|
328
|
+
if (arg.startsWith("--")) {
|
|
329
|
+
if (arg === "--recursive") recursive = true;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (arg.startsWith("-") && arg.length > 1) {
|
|
333
|
+
if (arg.includes("r") || arg.includes("R")) recursive = true;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
targets.push(normPath(arg.toLowerCase()));
|
|
337
|
+
}
|
|
338
|
+
if (!recursive) return false;
|
|
339
|
+
return targets.some((t) => ROOT_TARGETS.has(t) || t === "/.." || /^\/[a-z]+\/\*$/.test(t));
|
|
340
|
+
}
|
|
341
|
+
var FORK_BOMB = /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/;
|
|
342
|
+
var CATASTROPHIC_RULES = [
|
|
343
|
+
{
|
|
344
|
+
reason: "recursive force-delete of a root or system directory (rm -rf /)",
|
|
345
|
+
test: isRootRm
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
reason: "piping a downloaded remote script straight into a shell (curl | sh)",
|
|
349
|
+
test: isRemoteScriptPipedToShell
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
reason: "writing raw bytes to a block device (dd of=/dev/\u2026)",
|
|
353
|
+
test: (cmd) => cmd.name === "dd" && cmd.args.some((a) => /^of=\/dev\/(sd|nvme|disk|hd|mmcblk|vd)/i.test(a))
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
reason: "formatting a filesystem (mkfs)",
|
|
357
|
+
test: (cmd) => /^mkfs(\.|$)/.test(cmd.name)
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
reason: "redirecting output over a raw disk device (> /dev/sd\u2026)",
|
|
361
|
+
test: (cmd) => /[>]\s*\/dev\/(sd|nvme|disk|hd|mmcblk|vd)/i.test(cmd.raw)
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
reason: "world-writable recursive chmod of a root or system directory (chmod -R 777 /)",
|
|
365
|
+
test: isRootChmod
|
|
366
|
+
}
|
|
367
|
+
];
|
|
368
|
+
var SHELL_SINKS = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "dash", "ksh", "fish", "csh", "tcsh"]);
|
|
369
|
+
var FETCHERS = /* @__PURE__ */ new Set(["curl", "wget", "fetch"]);
|
|
370
|
+
function isRemoteScriptPipedToShell(cmd) {
|
|
371
|
+
if (!SHELL_SINKS.has(cmd.name)) return false;
|
|
372
|
+
return /\b(curl|wget|fetch)\b/i.test(cmd.raw) && /https?:\/\//i.test(cmd.raw);
|
|
373
|
+
}
|
|
374
|
+
function isRootChmod(cmd) {
|
|
375
|
+
if (cmd.name !== "chmod") return false;
|
|
376
|
+
let recursive = false;
|
|
377
|
+
let world = false;
|
|
378
|
+
const targets = [];
|
|
379
|
+
for (const arg of cmd.args) {
|
|
380
|
+
if (arg === "-R" || arg === "--recursive" || /^-[a-z]*R/i.test(arg)) recursive = true;
|
|
381
|
+
else if (/^[0-7]?777$/.test(arg) || arg.includes("o+w") || arg.includes("a+w")) world = true;
|
|
382
|
+
else if (!arg.startsWith("-")) targets.push(normPath(arg.toLowerCase()));
|
|
383
|
+
}
|
|
384
|
+
return recursive && world && targets.some((t) => ROOT_TARGETS.has(t));
|
|
385
|
+
}
|
|
386
|
+
function scanForRemotePipeToShell(cmds) {
|
|
387
|
+
const hasRemoteFetch = cmds.some(
|
|
388
|
+
(c) => FETCHERS.has(c.name) && c.args.some((a) => /^https?:\/\//i.test(a))
|
|
389
|
+
);
|
|
390
|
+
if (!hasRemoteFetch) return void 0;
|
|
391
|
+
const hasShellSink = cmds.some(
|
|
392
|
+
(c) => SHELL_SINKS.has(c.name) && !c.args.some((a) => !a.startsWith("-") && /\.(sh|bash)$/i.test(a))
|
|
393
|
+
);
|
|
394
|
+
if (hasShellSink) {
|
|
395
|
+
return "piping a downloaded remote script straight into a shell (curl | sh)";
|
|
396
|
+
}
|
|
397
|
+
return void 0;
|
|
398
|
+
}
|
|
399
|
+
function catastrophicReason(cmd) {
|
|
400
|
+
for (const rule of CATASTROPHIC_RULES) {
|
|
401
|
+
if (rule.test(cmd)) return rule.reason;
|
|
402
|
+
}
|
|
403
|
+
return void 0;
|
|
404
|
+
}
|
|
405
|
+
function evaluateCatastrophic(command) {
|
|
406
|
+
if (FORK_BOMB.test(command)) {
|
|
407
|
+
return catastrophicMessage("fork bomb");
|
|
408
|
+
}
|
|
409
|
+
const cmds = parseBashCommand(command);
|
|
410
|
+
if (cmds.length === 0) return void 0;
|
|
411
|
+
for (const cmd of cmds) {
|
|
412
|
+
const reason = catastrophicReason(cmd);
|
|
413
|
+
if (reason !== void 0) return catastrophicMessage(reason);
|
|
414
|
+
}
|
|
415
|
+
const piped = scanForRemotePipeToShell(cmds);
|
|
416
|
+
if (piped !== void 0) return catastrophicMessage(piped);
|
|
417
|
+
return void 0;
|
|
418
|
+
}
|
|
419
|
+
function bashSubcommandSubjects(command) {
|
|
420
|
+
const cmds = parseBashCommand(command);
|
|
421
|
+
if (cmds.length === 0) return [command];
|
|
422
|
+
return cmds.map((c) => [c.name, ...c.args].join(" ").trim());
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// src/conductor/permissions.ts
|
|
426
|
+
var READ_ONLY_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
427
|
+
"read",
|
|
428
|
+
"ls",
|
|
429
|
+
"grep",
|
|
430
|
+
"find",
|
|
431
|
+
"glob",
|
|
432
|
+
"websearch",
|
|
433
|
+
"webfetch",
|
|
434
|
+
"todoread"
|
|
435
|
+
]);
|
|
436
|
+
var EDIT_TOOL_NAMES = /* @__PURE__ */ new Set(["edit", "write", "multiedit"]);
|
|
437
|
+
function canon(toolName) {
|
|
438
|
+
return toolName.toLowerCase();
|
|
439
|
+
}
|
|
440
|
+
function isReadOnlyToolName(toolName, extra) {
|
|
441
|
+
const name = canon(toolName);
|
|
442
|
+
if (READ_ONLY_TOOL_NAMES.has(name)) return true;
|
|
443
|
+
if (extra) {
|
|
444
|
+
for (const candidate of extra) {
|
|
445
|
+
if (canon(candidate) === name) return true;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
function isEditToolName(toolName) {
|
|
451
|
+
return EDIT_TOOL_NAMES.has(canon(toolName));
|
|
452
|
+
}
|
|
453
|
+
function normalizeMode(mode) {
|
|
454
|
+
return mode === "bypassPermissions" ? "bypass" : mode;
|
|
455
|
+
}
|
|
456
|
+
function parseRule(raw) {
|
|
457
|
+
const trimmed = raw.trim();
|
|
458
|
+
const open = trimmed.indexOf("(");
|
|
459
|
+
if (open === -1 || !trimmed.endsWith(")")) {
|
|
460
|
+
return { toolName: trimmed };
|
|
461
|
+
}
|
|
462
|
+
const toolName = trimmed.slice(0, open).trim();
|
|
463
|
+
const ruleContent = trimmed.slice(open + 1, trimmed.length - 1).trim();
|
|
464
|
+
return ruleContent.length > 0 ? { toolName, ruleContent } : { toolName };
|
|
465
|
+
}
|
|
466
|
+
function makeRule(ruleBehavior, raw) {
|
|
467
|
+
return { ruleBehavior, ruleValue: parseRule(raw) };
|
|
468
|
+
}
|
|
469
|
+
function ruleSubject(toolName, input) {
|
|
470
|
+
if (input === null || typeof input !== "object") {
|
|
471
|
+
return typeof input === "string" ? input : "";
|
|
472
|
+
}
|
|
473
|
+
const bag = input;
|
|
474
|
+
const name = canon(toolName);
|
|
475
|
+
if ((name === "bash" || name === "shell") && typeof bag.command === "string") {
|
|
476
|
+
return bag.command;
|
|
477
|
+
}
|
|
478
|
+
if (typeof bag.command === "string") return bag.command;
|
|
479
|
+
try {
|
|
480
|
+
return JSON.stringify(bag);
|
|
481
|
+
} catch {
|
|
482
|
+
return "";
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function specifierMatches(specifier, subject) {
|
|
486
|
+
if (specifier.endsWith(":*")) {
|
|
487
|
+
return subject.startsWith(specifier.slice(0, -2));
|
|
488
|
+
}
|
|
489
|
+
if (specifier.endsWith("*") && !specifier.slice(0, -1).includes("*")) {
|
|
490
|
+
return subject.startsWith(specifier.slice(0, -1));
|
|
491
|
+
}
|
|
492
|
+
if (specifier.includes("*")) {
|
|
493
|
+
const escaped = specifier.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
494
|
+
return new RegExp(`^${escaped}$`).test(subject);
|
|
495
|
+
}
|
|
496
|
+
return specifier === subject;
|
|
497
|
+
}
|
|
498
|
+
function isBashTool(toolName) {
|
|
499
|
+
const name = canon(toolName);
|
|
500
|
+
return name === "bash" || name === "shell";
|
|
501
|
+
}
|
|
502
|
+
function bashSubjects(toolName, input) {
|
|
503
|
+
const command = ruleSubject(toolName, input);
|
|
504
|
+
if (command.length === 0) return [];
|
|
505
|
+
return bashSubcommandSubjects(command);
|
|
506
|
+
}
|
|
507
|
+
function toolMatchesRule(toolName, input, rule) {
|
|
508
|
+
const ruleName = canon(rule.ruleValue.toolName);
|
|
509
|
+
const callName = canon(toolName);
|
|
510
|
+
const nameMatches = ruleName === callName || ruleName.startsWith("mcp__") && callName.startsWith(`${ruleName}__`);
|
|
511
|
+
if (!nameMatches) return false;
|
|
512
|
+
const specifier = rule.ruleValue.ruleContent;
|
|
513
|
+
if (specifier === void 0) return true;
|
|
514
|
+
if (isBashTool(toolName)) {
|
|
515
|
+
const subjects = bashSubjects(toolName, input);
|
|
516
|
+
if (subjects.length === 0) return specifierMatches(specifier, ruleSubject(toolName, input));
|
|
517
|
+
return subjects.some((subject) => specifierMatches(specifier, subject));
|
|
518
|
+
}
|
|
519
|
+
return specifierMatches(specifier, ruleSubject(toolName, input));
|
|
520
|
+
}
|
|
521
|
+
function bashAllowSetCoversAll(toolName, input, allowRules) {
|
|
522
|
+
const callName = canon(toolName);
|
|
523
|
+
const subjects = bashSubjects(toolName, input);
|
|
524
|
+
if (subjects.length === 0) return false;
|
|
525
|
+
return subjects.every(
|
|
526
|
+
(subject) => allowRules.some((rule) => {
|
|
527
|
+
if (canon(rule.ruleValue.toolName) !== callName) return false;
|
|
528
|
+
const specifier = rule.ruleValue.ruleContent;
|
|
529
|
+
if (specifier === void 0) return true;
|
|
530
|
+
return specifierMatches(specifier, subject);
|
|
531
|
+
})
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
function resolveRuleDecision(toolName, input, rules, mode, readOnlyExtra) {
|
|
535
|
+
const m = normalizeMode(mode);
|
|
536
|
+
if (isBashTool(toolName)) {
|
|
537
|
+
const command = ruleSubject(toolName, input);
|
|
538
|
+
if (command.length > 0 && evaluateCatastrophic(command) !== void 0) {
|
|
539
|
+
return "deny";
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
for (const rule of rules) {
|
|
543
|
+
if (rule.ruleBehavior === "deny" && toolMatchesRule(toolName, input, rule)) {
|
|
544
|
+
return "deny";
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const readOnly = isReadOnlyToolName(toolName, readOnlyExtra);
|
|
548
|
+
if (m === "plan" && !readOnly) {
|
|
549
|
+
return "deny";
|
|
550
|
+
}
|
|
551
|
+
for (const rule of rules) {
|
|
552
|
+
if (rule.ruleBehavior === "ask" && toolMatchesRule(toolName, input, rule)) {
|
|
553
|
+
return "ask";
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (m === "bypass") {
|
|
557
|
+
return "allow";
|
|
558
|
+
}
|
|
559
|
+
if (readOnly) {
|
|
560
|
+
return "allow";
|
|
561
|
+
}
|
|
562
|
+
if (m === "acceptEdits" && isEditToolName(toolName)) {
|
|
563
|
+
return "allow";
|
|
564
|
+
}
|
|
565
|
+
if (isBashTool(toolName)) {
|
|
566
|
+
const allowRules = rules.filter((r) => r.ruleBehavior === "allow");
|
|
567
|
+
if (allowRules.length > 0 && bashAllowSetCoversAll(toolName, input, allowRules)) {
|
|
568
|
+
return "allow";
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
for (const rule of rules) {
|
|
572
|
+
if (rule.ruleBehavior === "allow" && toolMatchesRule(toolName, input, rule)) {
|
|
573
|
+
return "allow";
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return "ask";
|
|
578
|
+
}
|
|
579
|
+
function noResolverMessage(toolName) {
|
|
580
|
+
return `Tool "${toolName}" requires approval, but this session is non-interactive (no approval prompt is available). Denying. Add an allow rule under permissions.allow in settings, or run with --permission-mode acceptEdits/bypass, to permit it.`;
|
|
581
|
+
}
|
|
582
|
+
function declinedMessage(toolName) {
|
|
583
|
+
return `Tool "${toolName}" was denied by the user.`;
|
|
584
|
+
}
|
|
585
|
+
function blockedMessage(toolName) {
|
|
586
|
+
return `Tool "${toolName}" is blocked by the active permission policy.`;
|
|
587
|
+
}
|
|
588
|
+
function createPermissionGate(config) {
|
|
589
|
+
return async (toolName, input, opts) => {
|
|
590
|
+
if (isBashTool(toolName)) {
|
|
591
|
+
const command = ruleSubject(toolName, input);
|
|
592
|
+
if (command.length > 0) {
|
|
593
|
+
const reason = evaluateCatastrophic(command);
|
|
594
|
+
if (reason !== void 0) {
|
|
595
|
+
return { behavior: "deny", message: reason };
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
const behavior = resolveRuleDecision(
|
|
600
|
+
toolName,
|
|
601
|
+
input,
|
|
602
|
+
config.rules,
|
|
603
|
+
config.mode(),
|
|
604
|
+
config.readOnlyToolNames
|
|
605
|
+
);
|
|
606
|
+
if (behavior === "allow") {
|
|
607
|
+
return { behavior: "allow" };
|
|
608
|
+
}
|
|
609
|
+
if (behavior === "deny") {
|
|
610
|
+
return { behavior: "deny", message: blockedMessage(toolName) };
|
|
611
|
+
}
|
|
612
|
+
const resolver = config.requestApproval;
|
|
613
|
+
if (resolver === void 0) {
|
|
614
|
+
return { behavior: "deny", message: noResolverMessage(toolName) };
|
|
615
|
+
}
|
|
616
|
+
let choice;
|
|
617
|
+
try {
|
|
618
|
+
choice = await resolver(toolName, input, opts);
|
|
619
|
+
} catch {
|
|
620
|
+
choice = "deny";
|
|
621
|
+
}
|
|
622
|
+
if (choice === "deny") {
|
|
623
|
+
return { behavior: "deny", message: declinedMessage(toolName) };
|
|
624
|
+
}
|
|
625
|
+
if (choice === "allow-always" && config.appendAllowRule) {
|
|
626
|
+
config.appendAllowRule(makeRule("allow", toolName));
|
|
627
|
+
}
|
|
628
|
+
return { behavior: "allow" };
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// src/briefing/compose.ts
|
|
633
|
+
function joinBlocks(parts) {
|
|
634
|
+
return parts.filter((p) => p.trim().length > 0).join("\n\n");
|
|
635
|
+
}
|
|
636
|
+
function bullets(lines) {
|
|
637
|
+
return lines.map((l) => `- ${l}`).join("\n");
|
|
638
|
+
}
|
|
639
|
+
function escapeXml(value) {
|
|
640
|
+
let out = "";
|
|
641
|
+
for (const ch of value) {
|
|
642
|
+
switch (ch) {
|
|
643
|
+
case "&":
|
|
644
|
+
out += "&";
|
|
645
|
+
break;
|
|
646
|
+
case "<":
|
|
647
|
+
out += "<";
|
|
648
|
+
break;
|
|
649
|
+
case ">":
|
|
650
|
+
out += ">";
|
|
651
|
+
break;
|
|
652
|
+
case '"':
|
|
653
|
+
out += """;
|
|
654
|
+
break;
|
|
655
|
+
case "'":
|
|
656
|
+
out += "'";
|
|
657
|
+
break;
|
|
658
|
+
default:
|
|
659
|
+
out += ch;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return out;
|
|
663
|
+
}
|
|
664
|
+
function hasTool(ctx, id) {
|
|
665
|
+
return (ctx.tools ?? []).some((t) => t.name === id);
|
|
666
|
+
}
|
|
667
|
+
function hasToolPrefix(ctx, prefix) {
|
|
668
|
+
return (ctx.tools ?? []).some((t) => t.name.startsWith(prefix));
|
|
669
|
+
}
|
|
670
|
+
var TOOL_SUMMARIES = {
|
|
671
|
+
read: "Open a file's contents for inspection.",
|
|
672
|
+
write: "Create a new file or overwrite an existing one wholesale.",
|
|
673
|
+
edit: "Apply a precise in-place change by matching exact existing text.",
|
|
674
|
+
bash: "Run a shell command in the workspace.",
|
|
675
|
+
grep: "Search file contents by pattern across the tree.",
|
|
676
|
+
find: "Locate files and directories by name or glob.",
|
|
677
|
+
ls: "List the entries of a directory.",
|
|
678
|
+
task: "Hand a self-contained sub-task to a delegate agent.",
|
|
679
|
+
todoread: "Read back the current task checklist.",
|
|
680
|
+
todowrite: "Record or revise the task checklist.",
|
|
681
|
+
webfetch: "Retrieve and read the contents of a URL.",
|
|
682
|
+
websearch: "Query the web for current information."
|
|
683
|
+
};
|
|
684
|
+
function describeTool(tool) {
|
|
685
|
+
const summary = TOOL_SUMMARIES[tool.name] ?? tool.description?.trim() ?? "";
|
|
686
|
+
return summary.length > 0 ? `\`${tool.name}\` \u2014 ${summary}` : `\`${tool.name}\``;
|
|
687
|
+
}
|
|
688
|
+
var ROLE_SECTION = {
|
|
689
|
+
id: "role",
|
|
690
|
+
render() {
|
|
691
|
+
return joinBlocks([
|
|
692
|
+
"You are a terminal-based software engineering assistant. You operate inside a real workspace and make progress by reading code, running commands, and editing files directly \u2014 not by describing what someone else should do.",
|
|
693
|
+
"Work toward the user's actual goal. Take the initiative to gather the context you need, but stay within the scope of what was asked: do not redesign, rename, or refactor beyond the request unless the user invites it."
|
|
694
|
+
]);
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
var TOOLS_SECTION = {
|
|
698
|
+
id: "tools",
|
|
699
|
+
title: "Tools",
|
|
700
|
+
applies(ctx) {
|
|
701
|
+
return (ctx.tools ?? []).length > 0;
|
|
702
|
+
},
|
|
703
|
+
render(ctx) {
|
|
704
|
+
const lines = (ctx.tools ?? []).map(describeTool);
|
|
705
|
+
return joinBlocks([
|
|
706
|
+
"# Tools",
|
|
707
|
+
"These capabilities are available to you this turn. Reach for the most specific one for the job.",
|
|
708
|
+
bullets(lines)
|
|
709
|
+
]);
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
var GUIDELINES_SECTION = {
|
|
713
|
+
id: "guidelines",
|
|
714
|
+
title: "Working guidance",
|
|
715
|
+
render(ctx) {
|
|
716
|
+
const items = [];
|
|
717
|
+
if (hasTool(ctx, "read")) {
|
|
718
|
+
items.push(
|
|
719
|
+
"Always inspect a file with the reader before you change it. Do not shell out to `cat`, `head`, or `sed` to view a file when the reader is available."
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
if (hasTool(ctx, "edit")) {
|
|
723
|
+
items.push(
|
|
724
|
+
"For a targeted change, prefer the editor: it replaces an exact span of existing text, so the old text you supply must match the file byte-for-byte."
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
if (hasTool(ctx, "write")) {
|
|
728
|
+
items.push(
|
|
729
|
+
"Reserve the writer for creating a new file or replacing one in full; for anything smaller, edit in place rather than rewriting the whole file."
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
if (hasTool(ctx, "grep") || hasTool(ctx, "find") || hasTool(ctx, "ls")) {
|
|
733
|
+
items.push(
|
|
734
|
+
"Explore the tree with the dedicated search and listing tools rather than shell equivalents \u2014 they are quicker and already skip ignored paths."
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
if (hasTool(ctx, "bash")) {
|
|
738
|
+
items.push(
|
|
739
|
+
"Use the shell for builds, tests, and other commands that no purpose-built tool covers. Keep commands scoped and avoid destructive operations unless the user asked for them."
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
items.push(
|
|
743
|
+
"Keep your replies short and to the point. Lead with the result; add only the explanation the user needs to act on it."
|
|
744
|
+
);
|
|
745
|
+
items.push(
|
|
746
|
+
"When a change is non-trivial, verify it \u2014 run the build or the relevant tests \u2014 before you report it as done."
|
|
747
|
+
);
|
|
748
|
+
return joinBlocks(["# Working guidance", bullets(items)]);
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
var TASK_SECTION = {
|
|
752
|
+
id: "tasks",
|
|
753
|
+
title: "Task tracking",
|
|
754
|
+
applies(ctx) {
|
|
755
|
+
return hasTool(ctx, "todowrite") || hasTool(ctx, "todoread");
|
|
756
|
+
},
|
|
757
|
+
render() {
|
|
758
|
+
return joinBlocks([
|
|
759
|
+
"# Task tracking",
|
|
760
|
+
"For work that spans several steps, maintain a checklist with the task tools. Break the job into concrete items, mark exactly one in progress at a time, and tick items off as you finish them so the user can follow along."
|
|
761
|
+
]);
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
var SUBAGENTS_SECTION = {
|
|
765
|
+
id: "subagents",
|
|
766
|
+
title: "Delegates",
|
|
767
|
+
applies(ctx) {
|
|
768
|
+
return (ctx.subagents ?? []).length > 0;
|
|
769
|
+
},
|
|
770
|
+
render(ctx) {
|
|
771
|
+
const lines = (ctx.subagents ?? []).map((s) => {
|
|
772
|
+
const when = s.when ? ` Use it when ${s.when}` : "";
|
|
773
|
+
return `**${s.name}** \u2014 ${s.purpose}.${when}`;
|
|
774
|
+
});
|
|
775
|
+
return joinBlocks([
|
|
776
|
+
"# Delegates",
|
|
777
|
+
"You can offload a focused, self-contained piece of work to one of these delegates with the task tool. Hand off when a sub-problem is well-scoped enough to be solved without your full conversation context.",
|
|
778
|
+
bullets(lines)
|
|
779
|
+
]);
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
var PLAN_SECTION = {
|
|
783
|
+
id: "plan-mode",
|
|
784
|
+
title: "Plan mode",
|
|
785
|
+
applies(ctx) {
|
|
786
|
+
return hasTool(ctx, "enter_plan_mode") || hasTool(ctx, "exit_plan_mode");
|
|
787
|
+
},
|
|
788
|
+
render() {
|
|
789
|
+
return joinBlocks([
|
|
790
|
+
"# Plan mode",
|
|
791
|
+
bullets([
|
|
792
|
+
"For a task that needs investigation before any change, call `enter_plan_mode` first. That switches you into a read-only research phase: you may read, search, and inspect, but every file-mutating and shell tool is blocked until you leave.",
|
|
793
|
+
"While in plan mode, gather all the context you need and draft a concrete plan. Do not try to edit files or run commands \u2014 those calls are rejected with a steering message.",
|
|
794
|
+
"When the plan is ready, call `exit_plan_mode` with the full plan text. The user reviews it and either approves (you leave plan mode and carry the plan out) or rejects (you stay in plan mode to revise and re-propose). Use it to ask permission to start, not to do the work."
|
|
795
|
+
])
|
|
796
|
+
]);
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
var CONNECTORS_SECTION = {
|
|
800
|
+
id: "connectors",
|
|
801
|
+
title: "Connectors",
|
|
802
|
+
applies(ctx) {
|
|
803
|
+
return hasToolPrefix(ctx, "connector_") || hasToolPrefix(ctx, "saas_");
|
|
804
|
+
},
|
|
805
|
+
render() {
|
|
806
|
+
return joinBlocks([
|
|
807
|
+
"# Connectors",
|
|
808
|
+
bullets([
|
|
809
|
+
"When a task needs an external service (issue trackers, calendars, docs, and the like), prefer the connector tools over scraping or guessing.",
|
|
810
|
+
"Confirm the connection is authorized before you rely on it, and surface a clear next step if it is not.",
|
|
811
|
+
"Page through large result sets deliberately rather than assuming the first response is complete."
|
|
812
|
+
])
|
|
813
|
+
]);
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
var PROJECT_CONTEXT_SECTION = {
|
|
817
|
+
id: "project-context",
|
|
818
|
+
title: "Project context",
|
|
819
|
+
applies(ctx) {
|
|
820
|
+
return (ctx.contextDocs ?? []).length > 0;
|
|
821
|
+
},
|
|
822
|
+
render(ctx) {
|
|
823
|
+
const docs = (ctx.contextDocs ?? []).map(
|
|
824
|
+
(d) => `## ${d.path}
|
|
825
|
+
|
|
826
|
+
${d.body.trim()}`
|
|
827
|
+
);
|
|
828
|
+
return joinBlocks([
|
|
829
|
+
"# Project context",
|
|
830
|
+
"The following project documents describe conventions for this repository. Treat them as standing instructions.",
|
|
831
|
+
...docs
|
|
832
|
+
]);
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
var SKILLS_SECTION = {
|
|
836
|
+
id: "skills",
|
|
837
|
+
title: "Skills",
|
|
838
|
+
applies(ctx) {
|
|
839
|
+
return (ctx.skills ?? []).length > 0;
|
|
840
|
+
},
|
|
841
|
+
render(ctx) {
|
|
842
|
+
const skills = ctx.skills ?? [];
|
|
843
|
+
const entries = skills.map((card) => {
|
|
844
|
+
return [
|
|
845
|
+
" <skill>",
|
|
846
|
+
` <name>${escapeXml(card.name)}</name>`,
|
|
847
|
+
` <description>${escapeXml(card.description)}</description>`,
|
|
848
|
+
` <location>${escapeXml(card.location)}</location>`,
|
|
849
|
+
" </skill>"
|
|
850
|
+
].join("\n");
|
|
851
|
+
});
|
|
852
|
+
return joinBlocks([
|
|
853
|
+
"# Skills",
|
|
854
|
+
"Each skill below is a set of task-specific instructions stored on disk. When a task lines up with a skill's description, load its file with the reader and follow it. Paths a skill mentions are relative to that skill's own directory.",
|
|
855
|
+
`<available_skills>
|
|
856
|
+
${entries.join("\n")}
|
|
857
|
+
</available_skills>`
|
|
858
|
+
]);
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
var FOOTER_SECTION = {
|
|
862
|
+
id: "footer",
|
|
863
|
+
render(ctx) {
|
|
864
|
+
const where = ctx.cwd ?? ctx.workspace;
|
|
865
|
+
const when = (ctx.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
866
|
+
const lines = [];
|
|
867
|
+
if (where) lines.push(`Working directory: ${where}`);
|
|
868
|
+
lines.push(`Current time: ${when}`);
|
|
869
|
+
return lines.join("\n");
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
var BRIEFING_SECTIONS = [
|
|
873
|
+
ROLE_SECTION,
|
|
874
|
+
TOOLS_SECTION,
|
|
875
|
+
GUIDELINES_SECTION,
|
|
876
|
+
TASK_SECTION,
|
|
877
|
+
SUBAGENTS_SECTION,
|
|
878
|
+
PLAN_SECTION,
|
|
879
|
+
CONNECTORS_SECTION,
|
|
880
|
+
PROJECT_CONTEXT_SECTION,
|
|
881
|
+
SKILLS_SECTION,
|
|
882
|
+
FOOTER_SECTION
|
|
883
|
+
];
|
|
884
|
+
function composeBriefing(input) {
|
|
885
|
+
const inputs = isInputs(input) ? input : { sections: BRIEFING_SECTIONS, context: input };
|
|
886
|
+
const { sections, context, prelude, append } = inputs;
|
|
887
|
+
const rendered = [];
|
|
888
|
+
if (prelude && prelude.trim().length > 0) rendered.push(prelude.trim());
|
|
889
|
+
for (const section of sections) {
|
|
890
|
+
if (section.applies && !section.applies(context)) continue;
|
|
891
|
+
const fragment = section.render(context);
|
|
892
|
+
if (fragment.trim().length > 0) rendered.push(fragment.trim());
|
|
893
|
+
}
|
|
894
|
+
if (append && append.trim().length > 0) rendered.push(append.trim());
|
|
895
|
+
return joinBlocks(rendered);
|
|
896
|
+
}
|
|
897
|
+
function isInputs(value) {
|
|
898
|
+
return "sections" in value && "context" in value;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// src/briefing/context-docs.ts
|
|
902
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
903
|
+
import { dirname, extname, isAbsolute, join, parse, resolve } from "node:path";
|
|
904
|
+
var DEFAULT_MAX_IMPORT_DEPTH = 5;
|
|
905
|
+
var DEFAULT_MAX_BYTES_PER_DOC = 4e4;
|
|
906
|
+
var CANDIDATE_FILES = [
|
|
907
|
+
"AGENTS.md",
|
|
908
|
+
"CLAUDE.md",
|
|
909
|
+
"INDUSAGI.md",
|
|
910
|
+
"CLAUDE.local.md",
|
|
911
|
+
"AGENTS.local.md",
|
|
912
|
+
join(".indusagi", "CLAUDE.md"),
|
|
913
|
+
join(".claude", "CLAUDE.md")
|
|
914
|
+
];
|
|
915
|
+
var TEXT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
916
|
+
".md",
|
|
917
|
+
".markdown",
|
|
918
|
+
".txt",
|
|
919
|
+
".text",
|
|
920
|
+
".json",
|
|
921
|
+
".jsonc",
|
|
922
|
+
".yaml",
|
|
923
|
+
".yml",
|
|
924
|
+
".toml",
|
|
925
|
+
".xml",
|
|
926
|
+
".csv",
|
|
927
|
+
".html",
|
|
928
|
+
".htm",
|
|
929
|
+
".css",
|
|
930
|
+
".js",
|
|
931
|
+
".ts",
|
|
932
|
+
".tsx",
|
|
933
|
+
".jsx",
|
|
934
|
+
".mjs",
|
|
935
|
+
".cjs",
|
|
936
|
+
".mts",
|
|
937
|
+
".cts",
|
|
938
|
+
".py",
|
|
939
|
+
".rb",
|
|
940
|
+
".go",
|
|
941
|
+
".rs",
|
|
942
|
+
".sh",
|
|
943
|
+
".env"
|
|
944
|
+
]);
|
|
945
|
+
var IMPORT_REGEX = /(?:^|\s)@((?:[^\s\\]|\\ )+)/g;
|
|
946
|
+
function shortenPath(path, cwd, home) {
|
|
947
|
+
if (cwd.length > 0 && path.startsWith(cwd)) {
|
|
948
|
+
const rest = path.slice(cwd.length).replace(/^[/\\]+/, "");
|
|
949
|
+
return rest.length > 0 ? `./${rest}` : ".";
|
|
950
|
+
}
|
|
951
|
+
if (home.length > 0 && path.startsWith(home)) {
|
|
952
|
+
return `~${path.slice(home.length)}`;
|
|
953
|
+
}
|
|
954
|
+
return path;
|
|
955
|
+
}
|
|
956
|
+
function dirChain(cwd) {
|
|
957
|
+
const chain = [];
|
|
958
|
+
let current = cwd;
|
|
959
|
+
for (let i = 0; i < 256; i += 1) {
|
|
960
|
+
chain.push(current);
|
|
961
|
+
const parent = dirname(current);
|
|
962
|
+
if (parent === current || current === parse(current).root) break;
|
|
963
|
+
current = parent;
|
|
964
|
+
}
|
|
965
|
+
return chain.reverse();
|
|
966
|
+
}
|
|
967
|
+
function safeRealpath(path) {
|
|
968
|
+
try {
|
|
969
|
+
return realpathSync(path);
|
|
970
|
+
} catch {
|
|
971
|
+
return path;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
function isTextFile(path) {
|
|
975
|
+
const ext = extname(path).toLowerCase();
|
|
976
|
+
return ext.length === 0 || TEXT_FILE_EXTENSIONS.has(ext);
|
|
977
|
+
}
|
|
978
|
+
function readFileBody(absPath) {
|
|
979
|
+
if (!existsSync(absPath)) return null;
|
|
980
|
+
let raw;
|
|
981
|
+
try {
|
|
982
|
+
raw = readFileSync(absPath, "utf8");
|
|
983
|
+
} catch {
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
const trimmed = raw.trim();
|
|
987
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
988
|
+
}
|
|
989
|
+
function extractImports(body, baseDir, home) {
|
|
990
|
+
const out = [];
|
|
991
|
+
IMPORT_REGEX.lastIndex = 0;
|
|
992
|
+
let match;
|
|
993
|
+
while ((match = IMPORT_REGEX.exec(body)) !== null) {
|
|
994
|
+
let spec = match[1];
|
|
995
|
+
if (spec === void 0 || spec.length === 0) continue;
|
|
996
|
+
const hash = spec.indexOf("#");
|
|
997
|
+
if (hash >= 0) spec = spec.slice(0, hash);
|
|
998
|
+
spec = spec.replace(/\\ /g, " ").trim();
|
|
999
|
+
if (spec.length === 0) continue;
|
|
1000
|
+
let abs;
|
|
1001
|
+
if (spec.startsWith("~/")) {
|
|
1002
|
+
abs = join(home, spec.slice(2));
|
|
1003
|
+
} else if (isAbsolute(spec)) {
|
|
1004
|
+
abs = spec;
|
|
1005
|
+
} else {
|
|
1006
|
+
abs = resolve(baseDir, spec);
|
|
1007
|
+
}
|
|
1008
|
+
if (!isTextFile(abs)) continue;
|
|
1009
|
+
out.push(abs);
|
|
1010
|
+
}
|
|
1011
|
+
return out;
|
|
1012
|
+
}
|
|
1013
|
+
function loadDocTree(absPath, cwd, home, seen, depth, maxDepth, maxBytes, out) {
|
|
1014
|
+
const real = safeRealpath(absPath);
|
|
1015
|
+
if (seen.has(real)) return;
|
|
1016
|
+
seen.add(real);
|
|
1017
|
+
const body = readFileBody(absPath);
|
|
1018
|
+
if (body === null) return;
|
|
1019
|
+
const clipped = body.length > maxBytes ? body.slice(0, maxBytes) : body;
|
|
1020
|
+
out.push({ path: shortenPath(absPath, cwd, home), body: clipped });
|
|
1021
|
+
if (depth >= maxDepth) return;
|
|
1022
|
+
const baseDir = dirname(absPath);
|
|
1023
|
+
for (const child of extractImports(body, baseDir, home)) {
|
|
1024
|
+
loadDocTree(child, cwd, home, seen, depth + 1, maxDepth, maxBytes, out);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
function gatherContextDocs(cwd, home, opts) {
|
|
1028
|
+
const out = [];
|
|
1029
|
+
try {
|
|
1030
|
+
const maxDepth = opts?.maxImportDepth ?? DEFAULT_MAX_IMPORT_DEPTH;
|
|
1031
|
+
const maxBytes = opts?.maxBytesPerDoc ?? DEFAULT_MAX_BYTES_PER_DOC;
|
|
1032
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1033
|
+
const roots = [...dirChain(cwd)];
|
|
1034
|
+
if (home.length > 0) roots.push(home);
|
|
1035
|
+
for (const dir of roots) {
|
|
1036
|
+
if (dir.length === 0) continue;
|
|
1037
|
+
for (const name of CANDIDATE_FILES) {
|
|
1038
|
+
const candidate = join(dir, name);
|
|
1039
|
+
loadDocTree(candidate, cwd, home, seen, 0, maxDepth, maxBytes, out);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
} catch {
|
|
1043
|
+
return out;
|
|
1044
|
+
}
|
|
1045
|
+
return out;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// src/capability-deck/contract.ts
|
|
1049
|
+
function capabilityId(raw) {
|
|
1050
|
+
return raw;
|
|
1051
|
+
}
|
|
1052
|
+
function deckFault(kind, message, cause) {
|
|
1053
|
+
return cause === void 0 ? { kind, message } : { kind, message, cause };
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/capability-deck/builtin-bridge.ts
|
|
1057
|
+
import {
|
|
1058
|
+
createReadTool,
|
|
1059
|
+
createWriteTool,
|
|
1060
|
+
createEditTool,
|
|
1061
|
+
createLsTool,
|
|
1062
|
+
createGrepTool,
|
|
1063
|
+
createFindTool,
|
|
1064
|
+
createBashTool,
|
|
1065
|
+
createProcessTool,
|
|
1066
|
+
createWebSearchTool,
|
|
1067
|
+
createWebFetchTool,
|
|
1068
|
+
todoReadTool,
|
|
1069
|
+
todoWriteTool
|
|
1070
|
+
} from "indusagi/agent";
|
|
1071
|
+
var READ_ONLY = ["authoring", "survey"];
|
|
1072
|
+
var MUTATING = ["authoring"];
|
|
1073
|
+
var BUILTIN_DESCRIPTORS = [
|
|
1074
|
+
{
|
|
1075
|
+
id: capabilityId("read"),
|
|
1076
|
+
title: "Read file",
|
|
1077
|
+
summary: "Return the contents of a file at a path, optionally from an offset and capped to a line limit; renders images inline where supported.",
|
|
1078
|
+
profiles: READ_ONLY,
|
|
1079
|
+
build: (ctx) => createReadTool(ctx.cwd, {
|
|
1080
|
+
readState: ctx.framework?.["readState"]
|
|
1081
|
+
})
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
id: capabilityId("ls"),
|
|
1085
|
+
title: "List directory",
|
|
1086
|
+
summary: "Enumerate the entries of a directory, with an optional cap on how many are returned.",
|
|
1087
|
+
profiles: READ_ONLY,
|
|
1088
|
+
build: (ctx) => createLsTool(ctx.cwd)
|
|
1089
|
+
},
|
|
1090
|
+
{
|
|
1091
|
+
id: capabilityId("grep"),
|
|
1092
|
+
title: "Search file contents",
|
|
1093
|
+
summary: "Scan files for lines matching a pattern, with optional case-insensitivity, literal matching, surrounding context, and a result cap.",
|
|
1094
|
+
profiles: READ_ONLY,
|
|
1095
|
+
build: (ctx) => createGrepTool(ctx.cwd)
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
id: capabilityId("find"),
|
|
1099
|
+
title: "Find files by name",
|
|
1100
|
+
summary: "Locate files and directories whose names match a glob-style pattern beneath a root, capped to a result limit.",
|
|
1101
|
+
profiles: READ_ONLY,
|
|
1102
|
+
build: (ctx) => createFindTool(ctx.cwd)
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
id: capabilityId("websearch"),
|
|
1106
|
+
title: "Web search",
|
|
1107
|
+
summary: "Query the live web for a string and return a ranked set of result snippets, capped to a requested count.",
|
|
1108
|
+
profiles: READ_ONLY,
|
|
1109
|
+
build: () => createWebSearchTool()
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
id: capabilityId("webfetch"),
|
|
1113
|
+
title: "Fetch URL",
|
|
1114
|
+
summary: "Retrieve a single URL and return its body as text, Markdown, or HTML, with an optional request timeout.",
|
|
1115
|
+
profiles: READ_ONLY,
|
|
1116
|
+
build: () => createWebFetchTool()
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
id: capabilityId("todoread"),
|
|
1120
|
+
title: "Read checklist",
|
|
1121
|
+
summary: "Read back the session's running task checklist so the agent can review what is pending, active, or done.",
|
|
1122
|
+
profiles: READ_ONLY,
|
|
1123
|
+
// The framework's default in-memory checklist singleton; a session-aware
|
|
1124
|
+
// persistent store is wired by the separate checklist provisioner, not here.
|
|
1125
|
+
build: () => todoReadTool
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
id: capabilityId("write"),
|
|
1129
|
+
title: "Write file",
|
|
1130
|
+
summary: "Create or overwrite a file at a path with the given contents, making parent directories as needed.",
|
|
1131
|
+
profiles: MUTATING,
|
|
1132
|
+
build: (ctx) => createWriteTool(ctx.cwd, {
|
|
1133
|
+
readState: ctx.framework?.["readState"],
|
|
1134
|
+
checkpoint: ctx.framework?.["checkpoint"]
|
|
1135
|
+
})
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
id: capabilityId("edit"),
|
|
1139
|
+
title: "Edit file",
|
|
1140
|
+
summary: "Apply a find-and-replace edit to a file, swapping an exact span of old text for new text and reporting the resulting diff.",
|
|
1141
|
+
profiles: MUTATING,
|
|
1142
|
+
build: (ctx) => createEditTool(ctx.cwd, {
|
|
1143
|
+
readState: ctx.framework?.["readState"],
|
|
1144
|
+
checkpoint: ctx.framework?.["checkpoint"]
|
|
1145
|
+
})
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
id: capabilityId("bash"),
|
|
1149
|
+
title: "Run shell command",
|
|
1150
|
+
summary: "Execute a shell command in the working directory, streaming its output and honoring an optional timeout.",
|
|
1151
|
+
profiles: MUTATING,
|
|
1152
|
+
build: (ctx) => createBashTool(ctx.cwd)
|
|
1153
|
+
},
|
|
1154
|
+
{
|
|
1155
|
+
id: capabilityId("process"),
|
|
1156
|
+
title: "Manage background processes",
|
|
1157
|
+
summary: "Start, list, inspect, feed input to, and stop long-running background commands without blocking the agent on them.",
|
|
1158
|
+
profiles: MUTATING,
|
|
1159
|
+
build: (ctx) => createProcessTool({ cwd: ctx.cwd })
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
id: capabilityId("todowrite"),
|
|
1163
|
+
title: "Update checklist",
|
|
1164
|
+
summary: "Replace or amend the session's task checklist, setting each item's text, status, and priority.",
|
|
1165
|
+
profiles: MUTATING,
|
|
1166
|
+
// Paired with the read singleton above; both share the framework default
|
|
1167
|
+
// in-memory store. The checklist provisioner swaps in a persistent store.
|
|
1168
|
+
build: () => todoWriteTool
|
|
1169
|
+
}
|
|
1170
|
+
];
|
|
1171
|
+
var BUILTIN_BRIDGE = Object.freeze(
|
|
1172
|
+
Object.fromEntries(BUILTIN_DESCRIPTORS.map((d) => [d.id, d]))
|
|
1173
|
+
);
|
|
1174
|
+
var BUILTIN_IDS = BUILTIN_DESCRIPTORS.map(
|
|
1175
|
+
(d) => d.id
|
|
1176
|
+
);
|
|
1177
|
+
var BUILTIN_PROFILES = Object.freeze(
|
|
1178
|
+
Object.fromEntries(BUILTIN_DESCRIPTORS.map((d) => [d.id, d.profiles]))
|
|
1179
|
+
);
|
|
1180
|
+
function builtinDescriptors() {
|
|
1181
|
+
return BUILTIN_DESCRIPTORS;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// src/capability-deck/manifest.ts
|
|
1185
|
+
function cardOf(descriptor) {
|
|
1186
|
+
return {
|
|
1187
|
+
id: descriptor.id,
|
|
1188
|
+
title: descriptor.title,
|
|
1189
|
+
summary: descriptor.summary,
|
|
1190
|
+
build: descriptor.build
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
var CAPABILITY_CARDS = builtinDescriptors().map(cardOf);
|
|
1194
|
+
var CAPABILITY_INDEX = new Map(CAPABILITY_CARDS.map((card) => [card.id, card]));
|
|
1195
|
+
var CARD_PROFILES = new Map(
|
|
1196
|
+
builtinDescriptors().map((descriptor) => [descriptor.id, descriptor.profiles])
|
|
1197
|
+
);
|
|
1198
|
+
function cardsForProfile(profile) {
|
|
1199
|
+
if (profile === "all") return [...CAPABILITY_CARDS];
|
|
1200
|
+
return CAPABILITY_CARDS.filter((card) => {
|
|
1201
|
+
const profiles = CARD_PROFILES.get(card.id);
|
|
1202
|
+
return profiles !== void 0 && profiles.includes(profile);
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// src/capability-deck/cards/todo-card.ts
|
|
1207
|
+
import { Type } from "@sinclair/typebox";
|
|
1208
|
+
var TodoLedger = class {
|
|
1209
|
+
items = [];
|
|
1210
|
+
/** Replace the entire checklist with `next`; returns the stored snapshot. */
|
|
1211
|
+
set(next) {
|
|
1212
|
+
this.items = Object.freeze([...next]);
|
|
1213
|
+
return this.items;
|
|
1214
|
+
}
|
|
1215
|
+
/** Return the current checklist as a frozen, read-only snapshot. */
|
|
1216
|
+
read() {
|
|
1217
|
+
return this.items;
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
var TodoItemSchema = Type.Object(
|
|
1221
|
+
{
|
|
1222
|
+
task: Type.String({ description: "Short imperative description of the work item." }),
|
|
1223
|
+
state: Type.Union(
|
|
1224
|
+
[
|
|
1225
|
+
Type.Literal("pending"),
|
|
1226
|
+
Type.Literal("active"),
|
|
1227
|
+
Type.Literal("done"),
|
|
1228
|
+
Type.Literal("dropped")
|
|
1229
|
+
],
|
|
1230
|
+
{ description: "Lifecycle state of the item." }
|
|
1231
|
+
),
|
|
1232
|
+
weight: Type.Union([Type.Literal("low"), Type.Literal("normal"), Type.Literal("high")], {
|
|
1233
|
+
description: "Relative importance of the item."
|
|
1234
|
+
})
|
|
1235
|
+
},
|
|
1236
|
+
{ additionalProperties: false }
|
|
1237
|
+
);
|
|
1238
|
+
var TodoParams = Type.Object(
|
|
1239
|
+
{
|
|
1240
|
+
action: Type.Union([Type.Literal("read"), Type.Literal("set")], {
|
|
1241
|
+
description: "`read` returns the current checklist; `set` replaces it with the items you provide."
|
|
1242
|
+
}),
|
|
1243
|
+
items: Type.Optional(
|
|
1244
|
+
Type.Array(TodoItemSchema, {
|
|
1245
|
+
description: "The complete desired checklist. Required for `set` (send the whole list, not a delta); ignored for `read`."
|
|
1246
|
+
})
|
|
1247
|
+
)
|
|
1248
|
+
},
|
|
1249
|
+
{ additionalProperties: false }
|
|
1250
|
+
);
|
|
1251
|
+
var TODO_DESCRIPTION = 'Maintain a working checklist for the current task. Call with `action:"set"` and a complete `items` list to record or revise your plan \u2014 always send the full intended list, since the previous one is discarded. Call with `action:"read"` to recall the current plan. Mark items `active` while in progress and `done` when finished so the list reflects real status; use `dropped` for work you decided to skip.';
|
|
1252
|
+
var STATE_GLYPH = {
|
|
1253
|
+
pending: "[ ]",
|
|
1254
|
+
active: "[~]",
|
|
1255
|
+
done: "[x]",
|
|
1256
|
+
dropped: "[-]"
|
|
1257
|
+
};
|
|
1258
|
+
function renderChecklist(items) {
|
|
1259
|
+
if (items.length === 0) return "The checklist is empty.";
|
|
1260
|
+
const lines = items.map((it) => {
|
|
1261
|
+
const weight = it.weight === "normal" ? "" : ` (${it.weight})`;
|
|
1262
|
+
return `${STATE_GLYPH[it.state]} ${it.task}${weight}`;
|
|
1263
|
+
});
|
|
1264
|
+
return lines.join("\n");
|
|
1265
|
+
}
|
|
1266
|
+
function buildTodoCapability(_ctx) {
|
|
1267
|
+
const ledger = new TodoLedger();
|
|
1268
|
+
return {
|
|
1269
|
+
name: "todo",
|
|
1270
|
+
label: "Checklist",
|
|
1271
|
+
description: TODO_DESCRIPTION,
|
|
1272
|
+
parameters: TodoParams,
|
|
1273
|
+
async execute(_toolCallId, params) {
|
|
1274
|
+
const items = params.action === "set" ? ledger.set(params.items ?? []) : ledger.read();
|
|
1275
|
+
const heading = params.action === "set" ? "Checklist updated:" : "Current checklist:";
|
|
1276
|
+
const text = `${heading}
|
|
1277
|
+
${renderChecklist(items)}`;
|
|
1278
|
+
return {
|
|
1279
|
+
content: [{ type: "text", text }],
|
|
1280
|
+
details: { action: params.action, items }
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
var todoCard = {
|
|
1286
|
+
id: capabilityId("todo"),
|
|
1287
|
+
title: "Checklist",
|
|
1288
|
+
summary: "Keep an in-memory checklist of the current task's steps; set or read it.",
|
|
1289
|
+
// The card surface is the erased `Capability`; the builder keeps its precise
|
|
1290
|
+
// params/details type for direct callers and erases here. The erasure goes
|
|
1291
|
+
// through `unknown` because TypeBox tools are invariant in their schema (the
|
|
1292
|
+
// framework's own tool arrays are `AgentTool<any>[]` for the same reason).
|
|
1293
|
+
build: (ctx) => buildTodoCapability(ctx)
|
|
1294
|
+
};
|
|
1295
|
+
|
|
1296
|
+
// src/capability-deck/cards/bg-process-card.ts
|
|
1297
|
+
import { spawn } from "node:child_process";
|
|
1298
|
+
import { Type as Type2 } from "@sinclair/typebox";
|
|
1299
|
+
var MAX_BUFFERED_LINES = 2e3;
|
|
1300
|
+
function pushLines(buf, chunk) {
|
|
1301
|
+
for (const line of chunk.split(/\r?\n/)) {
|
|
1302
|
+
if (line.length === 0) continue;
|
|
1303
|
+
buf.lines.push(line);
|
|
1304
|
+
}
|
|
1305
|
+
if (buf.lines.length > MAX_BUFFERED_LINES) {
|
|
1306
|
+
const drop = buf.lines.length - MAX_BUFFERED_LINES;
|
|
1307
|
+
buf.lines.splice(0, drop);
|
|
1308
|
+
buf.cursor = Math.max(0, buf.cursor - drop);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
var DaemonTable = class {
|
|
1312
|
+
handles = /* @__PURE__ */ new Map();
|
|
1313
|
+
nextSeq = 1;
|
|
1314
|
+
cwd;
|
|
1315
|
+
constructor(cwd) {
|
|
1316
|
+
this.cwd = cwd;
|
|
1317
|
+
}
|
|
1318
|
+
start(command) {
|
|
1319
|
+
const id = `bg-${this.nextSeq++}`;
|
|
1320
|
+
const child = spawn(command, {
|
|
1321
|
+
cwd: this.cwd,
|
|
1322
|
+
shell: true,
|
|
1323
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1324
|
+
detached: false
|
|
1325
|
+
});
|
|
1326
|
+
const handle = {
|
|
1327
|
+
id,
|
|
1328
|
+
command,
|
|
1329
|
+
child,
|
|
1330
|
+
state: "running",
|
|
1331
|
+
exitCode: null,
|
|
1332
|
+
signal: null,
|
|
1333
|
+
startedAt: Date.now(),
|
|
1334
|
+
stdout: { lines: [], cursor: 0 },
|
|
1335
|
+
stderr: { lines: [], cursor: 0 }
|
|
1336
|
+
};
|
|
1337
|
+
child.stdout?.setEncoding("utf8");
|
|
1338
|
+
child.stderr?.setEncoding("utf8");
|
|
1339
|
+
child.stdout?.on("data", (d) => pushLines(handle.stdout, d));
|
|
1340
|
+
child.stderr?.on("data", (d) => pushLines(handle.stderr, d));
|
|
1341
|
+
child.on("exit", (code, sig) => {
|
|
1342
|
+
handle.exitCode = code;
|
|
1343
|
+
handle.signal = sig;
|
|
1344
|
+
handle.state = sig ? "signalled" : "exited";
|
|
1345
|
+
});
|
|
1346
|
+
child.on("error", (err) => {
|
|
1347
|
+
pushLines(handle.stderr, `spawn error: ${String(err)}`);
|
|
1348
|
+
handle.state = "exited";
|
|
1349
|
+
});
|
|
1350
|
+
this.handles.set(id, handle);
|
|
1351
|
+
return handle;
|
|
1352
|
+
}
|
|
1353
|
+
get(id) {
|
|
1354
|
+
return this.handles.get(id);
|
|
1355
|
+
}
|
|
1356
|
+
list() {
|
|
1357
|
+
return [...this.handles.values()];
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Signal a handle's process to terminate. Sends SIGTERM immediately and a
|
|
1361
|
+
* SIGKILL after `graceMs` if the child is still alive. Resolves once the
|
|
1362
|
+
* child has exited or the kill has been sent.
|
|
1363
|
+
*/
|
|
1364
|
+
async stop(handle, graceMs = 3e3) {
|
|
1365
|
+
if (handle.state !== "running") return;
|
|
1366
|
+
const child = handle.child;
|
|
1367
|
+
child.kill("SIGTERM");
|
|
1368
|
+
await new Promise((resolve2) => {
|
|
1369
|
+
const timer = setTimeout(() => {
|
|
1370
|
+
if (handle.state === "running") child.kill("SIGKILL");
|
|
1371
|
+
resolve2();
|
|
1372
|
+
}, graceMs);
|
|
1373
|
+
child.once("exit", () => {
|
|
1374
|
+
clearTimeout(timer);
|
|
1375
|
+
resolve2();
|
|
1376
|
+
});
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
var DaemonParams = Type2.Object(
|
|
1381
|
+
{
|
|
1382
|
+
action: Type2.Union(
|
|
1383
|
+
[Type2.Literal("start"), Type2.Literal("poll"), Type2.Literal("stop"), Type2.Literal("list")],
|
|
1384
|
+
{
|
|
1385
|
+
description: "`start` launches a long-running command; `poll` reads its captured output; `stop` terminates it; `list` shows all tracked processes."
|
|
1386
|
+
}
|
|
1387
|
+
),
|
|
1388
|
+
command: Type2.Optional(
|
|
1389
|
+
Type2.String({
|
|
1390
|
+
description: "Shell command to launch. Required for `start`."
|
|
1391
|
+
})
|
|
1392
|
+
),
|
|
1393
|
+
id: Type2.Optional(
|
|
1394
|
+
Type2.String({
|
|
1395
|
+
description: "Handle returned by `start`. Required for `poll` and `stop`."
|
|
1396
|
+
})
|
|
1397
|
+
),
|
|
1398
|
+
sinceLast: Type2.Optional(
|
|
1399
|
+
Type2.Boolean({
|
|
1400
|
+
description: "On `poll`, return only output appended since the previous poll of this handle. Defaults to false (return the full retained buffer)."
|
|
1401
|
+
})
|
|
1402
|
+
)
|
|
1403
|
+
},
|
|
1404
|
+
{ additionalProperties: false }
|
|
1405
|
+
);
|
|
1406
|
+
var DAEMON_DESCRIPTION = 'Run and manage long-lived background processes that persist across tool calls \u2014 dev servers, file watchers, anything you start once and observe over time. Use `action:"start"` with a `command` to launch one (you get back a handle `id`), `action:"poll"` with that `id` to read its accumulated output, `action:"stop"` to terminate it, and `action:"list"` to see what is running. Do NOT use this for ordinary one-shot commands that finish on their own \u2014 run those with the shell tool.';
|
|
1407
|
+
function drainBuffer(buf, sinceLast) {
|
|
1408
|
+
if (sinceLast) {
|
|
1409
|
+
const fresh = buf.lines.slice(buf.cursor);
|
|
1410
|
+
buf.cursor = buf.lines.length;
|
|
1411
|
+
return fresh;
|
|
1412
|
+
}
|
|
1413
|
+
return [...buf.lines];
|
|
1414
|
+
}
|
|
1415
|
+
function statusLine(h) {
|
|
1416
|
+
if (h.state === "running") return "running";
|
|
1417
|
+
if (h.state === "signalled") return `signalled (${h.signal ?? "?"})`;
|
|
1418
|
+
return `exited (code ${h.exitCode ?? "?"})`;
|
|
1419
|
+
}
|
|
1420
|
+
function buildDaemonCapability(ctx) {
|
|
1421
|
+
const table = new DaemonTable(ctx.cwd);
|
|
1422
|
+
return {
|
|
1423
|
+
name: "bg-process",
|
|
1424
|
+
label: "Background process",
|
|
1425
|
+
description: DAEMON_DESCRIPTION,
|
|
1426
|
+
parameters: DaemonParams,
|
|
1427
|
+
async execute(_toolCallId, params) {
|
|
1428
|
+
switch (params.action) {
|
|
1429
|
+
case "start": {
|
|
1430
|
+
if (!params.command) {
|
|
1431
|
+
return errResult("start", "`command` is required to start a background process.");
|
|
1432
|
+
}
|
|
1433
|
+
const h = table.start(params.command);
|
|
1434
|
+
return {
|
|
1435
|
+
content: [
|
|
1436
|
+
{
|
|
1437
|
+
type: "text",
|
|
1438
|
+
text: `Started background process ${h.id}: ${h.command}
|
|
1439
|
+
Poll it with action:"poll", id:"${h.id}".`
|
|
1440
|
+
}
|
|
1441
|
+
],
|
|
1442
|
+
details: { action: "start", ok: true, id: h.id, state: h.state }
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
case "poll": {
|
|
1446
|
+
const h = params.id ? table.get(params.id) : void 0;
|
|
1447
|
+
if (!h) return errResult("poll", `No background process with id "${params.id ?? ""}".`);
|
|
1448
|
+
const out = drainBuffer(h.stdout, params.sinceLast ?? false);
|
|
1449
|
+
const err = drainBuffer(h.stderr, params.sinceLast ?? false);
|
|
1450
|
+
const parts = [`Process ${h.id} \u2014 ${statusLine(h)}`];
|
|
1451
|
+
if (out.length) parts.push(`--- stdout ---
|
|
1452
|
+
${out.join("\n")}`);
|
|
1453
|
+
if (err.length) parts.push(`--- stderr ---
|
|
1454
|
+
${err.join("\n")}`);
|
|
1455
|
+
if (out.length === 0 && err.length === 0) parts.push("(no new output)");
|
|
1456
|
+
return {
|
|
1457
|
+
content: [{ type: "text", text: parts.join("\n") }],
|
|
1458
|
+
details: { action: "poll", ok: true, id: h.id, state: h.state, exitCode: h.exitCode }
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
case "stop": {
|
|
1462
|
+
const h = params.id ? table.get(params.id) : void 0;
|
|
1463
|
+
if (!h) return errResult("stop", `No background process with id "${params.id ?? ""}".`);
|
|
1464
|
+
await table.stop(h);
|
|
1465
|
+
return {
|
|
1466
|
+
content: [{ type: "text", text: `Stopped background process ${h.id} (${statusLine(h)}).` }],
|
|
1467
|
+
details: { action: "stop", ok: true, id: h.id, state: h.state, exitCode: h.exitCode }
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
case "list": {
|
|
1471
|
+
const all = table.list();
|
|
1472
|
+
const text = all.length === 0 ? "No background processes are being tracked." : all.map((h) => `${h.id} [${statusLine(h)}] ${h.command}`).join("\n");
|
|
1473
|
+
return {
|
|
1474
|
+
content: [{ type: "text", text }],
|
|
1475
|
+
details: {
|
|
1476
|
+
action: "list",
|
|
1477
|
+
ok: true,
|
|
1478
|
+
processes: all.map((h) => ({ id: h.id, command: h.command, state: h.state }))
|
|
1479
|
+
}
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
function errResult(action, message) {
|
|
1487
|
+
return {
|
|
1488
|
+
content: [{ type: "text", text: message }],
|
|
1489
|
+
details: { action, ok: false },
|
|
1490
|
+
isError: true
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
var daemonCard = {
|
|
1494
|
+
id: capabilityId("bg-process"),
|
|
1495
|
+
title: "Background process",
|
|
1496
|
+
summary: "Start, poll, and stop long-lived child processes that span multiple turns.",
|
|
1497
|
+
// Erase the precisely-typed builder to the `Capability` card surface (through
|
|
1498
|
+
// `unknown`, as TypeBox tools are invariant in their parameter schema).
|
|
1499
|
+
build: (ctx) => buildDaemonCapability(ctx)
|
|
1500
|
+
};
|
|
1501
|
+
|
|
1502
|
+
// src/capability-deck/cards/task-card.ts
|
|
1503
|
+
import { Type as Type3 } from "@sinclair/typebox";
|
|
1504
|
+
var DELEGATE_HANDLE_KEY = "delegate";
|
|
1505
|
+
function readDelegateRunner(ctx) {
|
|
1506
|
+
const handle = ctx.framework?.[DELEGATE_HANDLE_KEY];
|
|
1507
|
+
if (handle && typeof handle.run === "function") {
|
|
1508
|
+
return handle;
|
|
1509
|
+
}
|
|
1510
|
+
return void 0;
|
|
1511
|
+
}
|
|
1512
|
+
var TaskParams = Type3.Object(
|
|
1513
|
+
{
|
|
1514
|
+
objective: Type3.String({
|
|
1515
|
+
description: "A complete, self-contained statement of what the sub-agent should accomplish. Include everything it needs; it does not share your conversation history."
|
|
1516
|
+
}),
|
|
1517
|
+
agent: Type3.Optional(
|
|
1518
|
+
Type3.String({
|
|
1519
|
+
description: "Optional named sub-agent profile to run as. Omit to use the default profile."
|
|
1520
|
+
})
|
|
1521
|
+
),
|
|
1522
|
+
context: Type3.Optional(
|
|
1523
|
+
Type3.String({
|
|
1524
|
+
description: "Optional extra background the sub-agent should start with."
|
|
1525
|
+
})
|
|
1526
|
+
)
|
|
1527
|
+
},
|
|
1528
|
+
{ additionalProperties: false }
|
|
1529
|
+
);
|
|
1530
|
+
function baseDescription(agents) {
|
|
1531
|
+
const roster = agents.length > 0 ? ` Available sub-agent profiles: ${agents.join(", ")}.` : "";
|
|
1532
|
+
return "Delegate a focused, self-contained piece of work to a sub-agent that runs its own tool loop and returns a single report. Use this to keep your own context clean when a task is well-scoped \u2014 searching a large codebase, drafting a file, or investigating a question end-to-end. Give a complete `objective`; the sub-agent does not see your conversation." + roster;
|
|
1533
|
+
}
|
|
1534
|
+
var STUB_NOTE = "Sub-agent delegation is not wired in this environment, so the objective was not run. Wire a DelegateRunner into the deck context to enable it, or perform the work inline with the other tools.";
|
|
1535
|
+
function buildTaskCapability(ctx) {
|
|
1536
|
+
const runner = readDelegateRunner(ctx);
|
|
1537
|
+
const agents = runner?.listAgents?.() ?? [];
|
|
1538
|
+
return {
|
|
1539
|
+
name: "task",
|
|
1540
|
+
label: "Delegate task",
|
|
1541
|
+
description: baseDescription(agents),
|
|
1542
|
+
parameters: TaskParams,
|
|
1543
|
+
async execute(_toolCallId, params, signal) {
|
|
1544
|
+
if (!runner) {
|
|
1545
|
+
return {
|
|
1546
|
+
content: [{ type: "text", text: STUB_NOTE }],
|
|
1547
|
+
details: { delegated: false, ok: false, agent: params.agent },
|
|
1548
|
+
isError: true
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
const result = await runner.run(
|
|
1552
|
+
{ objective: params.objective, agent: params.agent, context: params.context },
|
|
1553
|
+
signal
|
|
1554
|
+
);
|
|
1555
|
+
return {
|
|
1556
|
+
content: [{ type: "text", text: result.report }],
|
|
1557
|
+
details: { delegated: true, ok: result.ok, agent: params.agent },
|
|
1558
|
+
isError: result.ok ? void 0 : true
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
var taskCard = {
|
|
1564
|
+
id: capabilityId("task"),
|
|
1565
|
+
title: "Delegate task",
|
|
1566
|
+
summary: "Hand a self-contained objective to a sub-agent and receive one report back.",
|
|
1567
|
+
// Erase the precisely-typed builder to the `Capability` card surface (through
|
|
1568
|
+
// `unknown`, as TypeBox tools are invariant in their parameter schema).
|
|
1569
|
+
build: (ctx) => buildTaskCapability(ctx)
|
|
1570
|
+
};
|
|
1571
|
+
|
|
1572
|
+
// src/capability-deck/cards/saas-card.ts
|
|
1573
|
+
import { Type as Type4 } from "@sinclair/typebox";
|
|
1574
|
+
var SAAS_GATEWAY_KEY = "saasGateway";
|
|
1575
|
+
function readGateway(ctx) {
|
|
1576
|
+
const handle = ctx.framework?.[SAAS_GATEWAY_KEY];
|
|
1577
|
+
if (handle && typeof handle.discover === "function" && typeof handle.execute === "function") {
|
|
1578
|
+
return handle;
|
|
1579
|
+
}
|
|
1580
|
+
return void 0;
|
|
1581
|
+
}
|
|
1582
|
+
var SaasParams = Type4.Object(
|
|
1583
|
+
{
|
|
1584
|
+
action: Type4.Union([Type4.Literal("discover"), Type4.Literal("execute")], {
|
|
1585
|
+
description: "`discover` lists available remote tool slugs; `execute` runs one slug with arguments."
|
|
1586
|
+
}),
|
|
1587
|
+
slug: Type4.Optional(
|
|
1588
|
+
Type4.String({
|
|
1589
|
+
description: "Vendor tool slug to run (e.g. GITHUB_CREATE_ISSUE). Required for `execute`."
|
|
1590
|
+
})
|
|
1591
|
+
),
|
|
1592
|
+
arguments: Type4.Optional(
|
|
1593
|
+
Type4.Record(Type4.String(), Type4.Unknown(), {
|
|
1594
|
+
description: "Arguments object for the remote tool, as the connector expects."
|
|
1595
|
+
})
|
|
1596
|
+
),
|
|
1597
|
+
toolkits: Type4.Optional(
|
|
1598
|
+
Type4.Array(Type4.String(), {
|
|
1599
|
+
description: "On `discover`, restrict results to these toolkit names."
|
|
1600
|
+
})
|
|
1601
|
+
),
|
|
1602
|
+
query: Type4.Optional(
|
|
1603
|
+
Type4.String({
|
|
1604
|
+
description: "On `discover`, free-text filter over available tools."
|
|
1605
|
+
})
|
|
1606
|
+
)
|
|
1607
|
+
},
|
|
1608
|
+
{ additionalProperties: false }
|
|
1609
|
+
);
|
|
1610
|
+
var SAAS_DESCRIPTION = 'Discover and run third-party SaaS actions (GitHub, Slack, Gmail, etc.) through the connector gateway. Use `action:"discover"` to find the right tool slug, then `action:"execute"` with that `slug` and an `arguments` object to perform the action. Tool slugs are vendor-defined and case-sensitive; pass them exactly as listed.';
|
|
1611
|
+
var STUB_NOTE2 = "The SaaS connector gateway is not configured in this environment, so no remote action was performed. Configure a connector (and authenticate the relevant toolkit) to enable these actions.";
|
|
1612
|
+
function buildSaasCapability(ctx) {
|
|
1613
|
+
const gateway = readGateway(ctx);
|
|
1614
|
+
return {
|
|
1615
|
+
name: "saas-action",
|
|
1616
|
+
label: "SaaS action",
|
|
1617
|
+
description: SAAS_DESCRIPTION,
|
|
1618
|
+
parameters: SaasParams,
|
|
1619
|
+
async execute(_toolCallId, params) {
|
|
1620
|
+
if (!gateway) {
|
|
1621
|
+
return {
|
|
1622
|
+
content: [{ type: "text", text: STUB_NOTE2 }],
|
|
1623
|
+
details: { action: params.action, ok: false },
|
|
1624
|
+
isError: true
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
if (params.action === "discover") {
|
|
1628
|
+
const tools = await gateway.discover({ toolkits: params.toolkits, query: params.query });
|
|
1629
|
+
const text = tools.length === 0 ? "No remote tools matched." : tools.map((t) => `- ${t.slug}${t.description ? ` \u2014 ${t.description}` : ""}`).join("\n");
|
|
1630
|
+
return {
|
|
1631
|
+
content: [{ type: "text", text }],
|
|
1632
|
+
details: { action: "discover", ok: true, count: tools.length }
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
if (!params.slug) {
|
|
1636
|
+
return {
|
|
1637
|
+
content: [{ type: "text", text: "`slug` is required to execute a SaaS action." }],
|
|
1638
|
+
details: { action: "execute", ok: false },
|
|
1639
|
+
isError: true
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
const result = await gateway.execute(params.slug, params.arguments ?? {});
|
|
1643
|
+
return {
|
|
1644
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
1645
|
+
details: { action: "execute", ok: result.ok, slug: params.slug },
|
|
1646
|
+
isError: result.ok ? void 0 : true
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
var saasCard = {
|
|
1652
|
+
id: capabilityId("saas-action"),
|
|
1653
|
+
title: "SaaS action",
|
|
1654
|
+
summary: "Discover and run third-party SaaS actions through the connector gateway.",
|
|
1655
|
+
// Erase the precisely-typed builder to the `Capability` card surface (through
|
|
1656
|
+
// `unknown`, as TypeBox tools are invariant in their parameter schema).
|
|
1657
|
+
build: (ctx) => buildSaasCapability(ctx)
|
|
1658
|
+
};
|
|
1659
|
+
|
|
1660
|
+
// src/capability-deck/cards/memory-card.ts
|
|
1661
|
+
import { Type as Type5 } from "@sinclair/typebox";
|
|
1662
|
+
var MEMORY_HANDLE_KEY = "memoryStore";
|
|
1663
|
+
var InMemoryStore = class {
|
|
1664
|
+
buffer = "";
|
|
1665
|
+
read() {
|
|
1666
|
+
return this.buffer;
|
|
1667
|
+
}
|
|
1668
|
+
replace(content) {
|
|
1669
|
+
this.buffer = content;
|
|
1670
|
+
}
|
|
1671
|
+
append(line) {
|
|
1672
|
+
this.buffer = this.buffer.length === 0 ? line : `${this.buffer}
|
|
1673
|
+
${line}`;
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
function readStore(ctx) {
|
|
1677
|
+
const handle = ctx.framework?.[MEMORY_HANDLE_KEY];
|
|
1678
|
+
if (handle && typeof handle.read === "function" && typeof handle.replace === "function" && typeof handle.append === "function") {
|
|
1679
|
+
return handle;
|
|
1680
|
+
}
|
|
1681
|
+
return new InMemoryStore();
|
|
1682
|
+
}
|
|
1683
|
+
var MemoryParams = Type5.Object(
|
|
1684
|
+
{
|
|
1685
|
+
action: Type5.Union(
|
|
1686
|
+
[Type5.Literal("read"), Type5.Literal("replace"), Type5.Literal("append")],
|
|
1687
|
+
{
|
|
1688
|
+
description: "`read` returns the note; `replace` overwrites it; `append` adds a line to the end."
|
|
1689
|
+
}
|
|
1690
|
+
),
|
|
1691
|
+
content: Type5.Optional(
|
|
1692
|
+
Type5.String({
|
|
1693
|
+
description: "New note body (for `replace`) or the line to add (for `append`). Ignored for `read`."
|
|
1694
|
+
})
|
|
1695
|
+
)
|
|
1696
|
+
},
|
|
1697
|
+
{ additionalProperties: false }
|
|
1698
|
+
);
|
|
1699
|
+
var MEMORY_DESCRIPTION = 'Keep a small working-memory note that persists across turns \u2014 durable facts, decisions, or reminders you want to retain even after older messages scroll out of context. Use `action:"read"` to recall it, `action:"replace"` to rewrite it from scratch, and `action:"append"` to add a single line. Keep it concise; it is a scratchpad, not a log.';
|
|
1700
|
+
function buildMemoryCapability(ctx) {
|
|
1701
|
+
const store = readStore(ctx);
|
|
1702
|
+
return {
|
|
1703
|
+
name: "memory",
|
|
1704
|
+
label: "Working memory",
|
|
1705
|
+
description: MEMORY_DESCRIPTION,
|
|
1706
|
+
parameters: MemoryParams,
|
|
1707
|
+
async execute(_toolCallId, params) {
|
|
1708
|
+
if (params.action === "replace") store.replace(params.content ?? "");
|
|
1709
|
+
else if (params.action === "append" && params.content) store.append(params.content);
|
|
1710
|
+
const note = store.read();
|
|
1711
|
+
const heading = params.action === "read" ? "Working memory:" : params.action === "replace" ? "Working memory replaced:" : "Working memory appended:";
|
|
1712
|
+
const body = note.length === 0 ? "(empty)" : note;
|
|
1713
|
+
return {
|
|
1714
|
+
content: [{ type: "text", text: `${heading}
|
|
1715
|
+
${body}` }],
|
|
1716
|
+
details: { action: params.action, ok: true, length: note.length }
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
var memoryCard = {
|
|
1722
|
+
id: capabilityId("memory"),
|
|
1723
|
+
title: "Working memory",
|
|
1724
|
+
summary: "Read and update a persistent scratch note that survives across turns.",
|
|
1725
|
+
// Erase the precisely-typed builder to the `Capability` card surface (through
|
|
1726
|
+
// `unknown`, as TypeBox tools are invariant in their parameter schema).
|
|
1727
|
+
build: (ctx) => buildMemoryCapability(ctx)
|
|
1728
|
+
};
|
|
1729
|
+
|
|
1730
|
+
// src/capability-deck/cards/plan-tools.ts
|
|
1731
|
+
import { Type as Type6 } from "@sinclair/typebox";
|
|
1732
|
+
var PLAN_HANDLE_KEY = "planController";
|
|
1733
|
+
function readPlanController(ctx) {
|
|
1734
|
+
const handle = ctx.framework?.[PLAN_HANDLE_KEY];
|
|
1735
|
+
if (handle && typeof handle.enterPlanMode === "function") {
|
|
1736
|
+
return handle;
|
|
1737
|
+
}
|
|
1738
|
+
return void 0;
|
|
1739
|
+
}
|
|
1740
|
+
var EnterPlanParams = Type6.Object({}, { additionalProperties: false });
|
|
1741
|
+
var ENTER_PLAN_DESCRIPTION = "Enter plan mode: a read-only research phase in which you may only read, search, and inspect \u2014 every file-mutating and shell tool is blocked until you leave. Use this when a task needs investigation and a plan before any change is made. While in plan mode, gather all the context you need, then call `exit_plan_mode` with the plan you intend to carry out; the user reviews and approves it before you act.";
|
|
1742
|
+
var ENTER_PLAN_PROSE = "You are now in plan mode (read-only). Investigate freely with the read, search, and listing tools, but do not attempt to edit files or run shell commands \u2014 they are blocked here. When you have a concrete plan, call `exit_plan_mode` with the full plan text so the user can approve it before you make any changes.";
|
|
1743
|
+
function buildEnterPlanModeCapability(ctx) {
|
|
1744
|
+
const controller = readPlanController(ctx);
|
|
1745
|
+
return {
|
|
1746
|
+
name: "enter_plan_mode",
|
|
1747
|
+
label: "Enter plan mode",
|
|
1748
|
+
description: ENTER_PLAN_DESCRIPTION,
|
|
1749
|
+
readOnly: true,
|
|
1750
|
+
parameters: EnterPlanParams,
|
|
1751
|
+
async execute() {
|
|
1752
|
+
const applied = controller !== void 0;
|
|
1753
|
+
if (controller) controller.enterPlanMode();
|
|
1754
|
+
return {
|
|
1755
|
+
content: [{ type: "text", text: ENTER_PLAN_PROSE }],
|
|
1756
|
+
details: { enterPlanMode: true, applied }
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
var ExitPlanParams = Type6.Object(
|
|
1762
|
+
{
|
|
1763
|
+
plan: Type6.String({
|
|
1764
|
+
description: "The plan you intend to carry out, in markdown. The user reviews this before approving the exit from plan mode; on approval you leave plan mode and begin."
|
|
1765
|
+
})
|
|
1766
|
+
},
|
|
1767
|
+
{ additionalProperties: false }
|
|
1768
|
+
);
|
|
1769
|
+
var EXIT_PLAN_DESCRIPTION = "Leave plan mode and propose the plan you intend to carry out. Provide the full plan as `plan`; the user reviews it and either approves (you exit plan mode and begin the work) or rejects (you stay in plan mode to revise). Only call this once you have a concrete, reviewed plan \u2014 not to do work, but to ask permission to start it.";
|
|
1770
|
+
function buildExitPlanModeCapability(_ctx) {
|
|
1771
|
+
return {
|
|
1772
|
+
name: "exit_plan_mode",
|
|
1773
|
+
label: "Exit plan mode",
|
|
1774
|
+
description: EXIT_PLAN_DESCRIPTION,
|
|
1775
|
+
// Marked read-only so the plan-mode gate never blocks the very tool used to
|
|
1776
|
+
// request leaving plan mode (it mutates nothing — the conductor owns the flip).
|
|
1777
|
+
readOnly: true,
|
|
1778
|
+
parameters: ExitPlanParams,
|
|
1779
|
+
async execute(_toolCallId, params) {
|
|
1780
|
+
const plan = params.plan;
|
|
1781
|
+
return {
|
|
1782
|
+
content: [
|
|
1783
|
+
{
|
|
1784
|
+
type: "text",
|
|
1785
|
+
text: `Proposed plan (awaiting your approval to leave plan mode):
|
|
1786
|
+
|
|
1787
|
+
${plan}`
|
|
1788
|
+
}
|
|
1789
|
+
],
|
|
1790
|
+
details: { exitPlan: true, plan }
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
var enterPlanModeCard = {
|
|
1796
|
+
id: capabilityId("enter_plan_mode"),
|
|
1797
|
+
title: "Enter plan mode",
|
|
1798
|
+
summary: "Switch into a read-only research phase where mutating tools are blocked.",
|
|
1799
|
+
build: (ctx) => buildEnterPlanModeCapability(ctx)
|
|
1800
|
+
};
|
|
1801
|
+
var exitPlanModeCard = {
|
|
1802
|
+
id: capabilityId("exit_plan_mode"),
|
|
1803
|
+
title: "Exit plan mode",
|
|
1804
|
+
summary: "Propose a plan and request approval to leave plan mode and begin work.",
|
|
1805
|
+
build: (ctx) => buildExitPlanModeCapability(ctx)
|
|
1806
|
+
};
|
|
1807
|
+
|
|
1808
|
+
// src/capability-deck/cards/index.ts
|
|
1809
|
+
var APP_NOVEL_CARDS = [
|
|
1810
|
+
todoCard,
|
|
1811
|
+
daemonCard,
|
|
1812
|
+
taskCard,
|
|
1813
|
+
saasCard,
|
|
1814
|
+
memoryCard,
|
|
1815
|
+
enterPlanModeCard,
|
|
1816
|
+
exitPlanModeCard
|
|
1817
|
+
];
|
|
1818
|
+
|
|
1819
|
+
// src/capability-deck/provision.ts
|
|
1820
|
+
var PROFILE_TABLE = Object.freeze({
|
|
1821
|
+
authoring: { builtins: "survey", appCards: false },
|
|
1822
|
+
survey: { builtins: "all", appCards: false },
|
|
1823
|
+
all: { builtins: "all", appCards: true }
|
|
1824
|
+
});
|
|
1825
|
+
function cardsForDeckProfile(profile) {
|
|
1826
|
+
const policy = PROFILE_TABLE[profile];
|
|
1827
|
+
if (policy === void 0) {
|
|
1828
|
+
throw deckFault(
|
|
1829
|
+
"unknown_capability",
|
|
1830
|
+
`No deck profile is registered under "${String(profile)}".`
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
const builtins = cardsForProfile(policy.builtins);
|
|
1834
|
+
return policy.appCards ? [...builtins, ...APP_NOVEL_CARDS] : builtins;
|
|
1835
|
+
}
|
|
1836
|
+
function buildSelected(cards, ctx) {
|
|
1837
|
+
const built = [];
|
|
1838
|
+
for (const card of cards) {
|
|
1839
|
+
try {
|
|
1840
|
+
built.push(card.build(ctx));
|
|
1841
|
+
} catch (cause) {
|
|
1842
|
+
throw deckFault(
|
|
1843
|
+
"build_failed",
|
|
1844
|
+
`Capability "${String(card.id)}" failed to build.`,
|
|
1845
|
+
cause
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
return built;
|
|
1850
|
+
}
|
|
1851
|
+
function provisionDeck(profile, ctx) {
|
|
1852
|
+
const selected = cardsForDeckProfile(profile);
|
|
1853
|
+
const capabilities = buildSelected(selected, ctx);
|
|
1854
|
+
return {
|
|
1855
|
+
tools() {
|
|
1856
|
+
return [...capabilities];
|
|
1857
|
+
},
|
|
1858
|
+
box() {
|
|
1859
|
+
return [...capabilities];
|
|
1860
|
+
}
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// src/boot/runners/read-state.ts
|
|
1865
|
+
import { normalize } from "node:path";
|
|
1866
|
+
var READ_STATE_HANDLE_KEY = "readState";
|
|
1867
|
+
var ReadStateStore = class {
|
|
1868
|
+
entries = /* @__PURE__ */ new Map();
|
|
1869
|
+
/** Normalize an absolute path into the store's canonical key. */
|
|
1870
|
+
key(absPath) {
|
|
1871
|
+
return normalize(absPath);
|
|
1872
|
+
}
|
|
1873
|
+
/** The recorded read fingerprint for a path, or `undefined` if never read. */
|
|
1874
|
+
get(absPath) {
|
|
1875
|
+
return this.entries.get(this.key(absPath));
|
|
1876
|
+
}
|
|
1877
|
+
/** Record (or replace) the read fingerprint for a path; returns this store. */
|
|
1878
|
+
set(absPath, value) {
|
|
1879
|
+
this.entries.set(this.key(absPath), value);
|
|
1880
|
+
return this;
|
|
1881
|
+
}
|
|
1882
|
+
/** Whether a path has a recorded read fingerprint this session. */
|
|
1883
|
+
has(absPath) {
|
|
1884
|
+
return this.entries.has(this.key(absPath));
|
|
1885
|
+
}
|
|
1886
|
+
/** Drop a path's recorded read fingerprint; returns whether one existed. */
|
|
1887
|
+
delete(absPath) {
|
|
1888
|
+
return this.entries.delete(this.key(absPath));
|
|
1889
|
+
}
|
|
1890
|
+
/** Forget every recorded read for this session. */
|
|
1891
|
+
clear() {
|
|
1892
|
+
this.entries.clear();
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
function createReadStateStore() {
|
|
1896
|
+
return new ReadStateStore();
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// src/boot/runners/checkpoint.ts
|
|
1900
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
1901
|
+
import { dirname as dirname2, normalize as normalize2 } from "node:path";
|
|
1902
|
+
var CHECKPOINT_HANDLE_KEY = "checkpoint";
|
|
1903
|
+
var ROOT_NODE_ID = "__root__";
|
|
1904
|
+
var CheckpointStore = class {
|
|
1905
|
+
/** node id → (normalized abs path → first-seen pre-mutation content). */
|
|
1906
|
+
byNode = /* @__PURE__ */ new Map();
|
|
1907
|
+
/** The transcript node the next {@link record} files its snapshot under. */
|
|
1908
|
+
current = null;
|
|
1909
|
+
/** Normalize an absolute path into the store's canonical per-node key. */
|
|
1910
|
+
key(absPath) {
|
|
1911
|
+
return normalize2(absPath);
|
|
1912
|
+
}
|
|
1913
|
+
/** The node id new snapshots are currently filed under (`null` until pinned). */
|
|
1914
|
+
activeNodeId() {
|
|
1915
|
+
return this.current;
|
|
1916
|
+
}
|
|
1917
|
+
/**
|
|
1918
|
+
* Pin the transcript node subsequent {@link record} calls file snapshots under.
|
|
1919
|
+
*
|
|
1920
|
+
* The conductor calls this as its head advances (at turn start), so all of a
|
|
1921
|
+
* turn's edits key to the node that was active before the turn ran. Passing the
|
|
1922
|
+
* same id again is harmless.
|
|
1923
|
+
*
|
|
1924
|
+
* @param id the active transcript node id, or `null` to fall back to the root
|
|
1925
|
+
*/
|
|
1926
|
+
setActiveNodeId(id) {
|
|
1927
|
+
this.current = id;
|
|
1928
|
+
}
|
|
1929
|
+
/** The node id snapshots are filed under right now (active id or the root). */
|
|
1930
|
+
resolveNodeId() {
|
|
1931
|
+
return this.current ?? ROOT_NODE_ID;
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Capture the pre-mutation content of `absPath` under the active node.
|
|
1935
|
+
*
|
|
1936
|
+
* The framework's duck-typed `CheckpointHandle` entry point. Called once per
|
|
1937
|
+
* mutated path immediately before the write lands, with the file's OLD on-disk
|
|
1938
|
+
* content (or `null` when the file did not exist). First-seen wins: a later
|
|
1939
|
+
* `record` of the same path under the same node is ignored so the snapshot
|
|
1940
|
+
* reflects the file's state when the node became active, not a mid-turn rewrite.
|
|
1941
|
+
*
|
|
1942
|
+
* @param absPath absolute path of the file about to be written
|
|
1943
|
+
* @param previous the file's content before this mutation, or `null` if absent
|
|
1944
|
+
*/
|
|
1945
|
+
record(absPath, previous) {
|
|
1946
|
+
const nodeId = this.resolveNodeId();
|
|
1947
|
+
let paths = this.byNode.get(nodeId);
|
|
1948
|
+
if (paths === void 0) {
|
|
1949
|
+
paths = /* @__PURE__ */ new Map();
|
|
1950
|
+
this.byNode.set(nodeId, paths);
|
|
1951
|
+
}
|
|
1952
|
+
const k = this.key(absPath);
|
|
1953
|
+
if (paths.has(k)) return;
|
|
1954
|
+
paths.set(k, previous);
|
|
1955
|
+
}
|
|
1956
|
+
/**
|
|
1957
|
+
* The recorded snapshots for a node, or `[]` when nothing was tracked there.
|
|
1958
|
+
*
|
|
1959
|
+
* Each entry pairs the normalized absolute path with the content it held before
|
|
1960
|
+
* the node's first mutation of it (`null` = the file was absent).
|
|
1961
|
+
*
|
|
1962
|
+
* @param nodeId the transcript node to read snapshots for
|
|
1963
|
+
*/
|
|
1964
|
+
snapshotFor(nodeId) {
|
|
1965
|
+
const paths = this.byNode.get(nodeId);
|
|
1966
|
+
if (paths === void 0) return [];
|
|
1967
|
+
return [...paths].map(([path, previous]) => ({ path, previous }));
|
|
1968
|
+
}
|
|
1969
|
+
/** Whether a node has ANY recorded file snapshot (the picker's restore gate). */
|
|
1970
|
+
hasSnapshot(nodeId) {
|
|
1971
|
+
const paths = this.byNode.get(nodeId);
|
|
1972
|
+
return paths !== void 0 && paths.size > 0;
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* Roll the working tree back to a node's recorded state.
|
|
1976
|
+
*
|
|
1977
|
+
* For each path the node tracked: when its snapshot is a string, the file is
|
|
1978
|
+
* rewritten with that pre-mutation content (creating parent directories as
|
|
1979
|
+
* needed); when the snapshot is `null` the file is DELETED (it did not exist at
|
|
1980
|
+
* that point). Returns the absolute paths that were restored, in record order.
|
|
1981
|
+
*
|
|
1982
|
+
* A safe no-op when the node has no snapshot — navigating to a node that never
|
|
1983
|
+
* mutated files restores nothing. Per-path failures are swallowed so one
|
|
1984
|
+
* unwritable path never aborts the rest of the restore.
|
|
1985
|
+
*
|
|
1986
|
+
* @param nodeId the transcript node whose file state to restore to
|
|
1987
|
+
* @returns the absolute paths that were written or deleted
|
|
1988
|
+
*/
|
|
1989
|
+
restore(nodeId) {
|
|
1990
|
+
const snapshots = this.snapshotFor(nodeId);
|
|
1991
|
+
const restored = [];
|
|
1992
|
+
for (const { path, previous } of snapshots) {
|
|
1993
|
+
try {
|
|
1994
|
+
if (previous === null) {
|
|
1995
|
+
rmSync(path, { force: true });
|
|
1996
|
+
} else {
|
|
1997
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
1998
|
+
writeFileSync(path, previous, "utf8");
|
|
1999
|
+
}
|
|
2000
|
+
restored.push(path);
|
|
2001
|
+
} catch {
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
return restored;
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
function createCheckpointStore() {
|
|
2008
|
+
return new CheckpointStore();
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// src/guardrails.ts
|
|
2012
|
+
var READ_BEFORE_EDIT_MESSAGE = "File has not been read yet. Read it first before writing to it.";
|
|
2013
|
+
export {
|
|
2014
|
+
CHECKPOINT_HANDLE_KEY,
|
|
2015
|
+
CheckpointStore,
|
|
2016
|
+
EDIT_TOOL_NAMES,
|
|
2017
|
+
READ_BEFORE_EDIT_MESSAGE,
|
|
2018
|
+
READ_ONLY_TOOL_NAMES,
|
|
2019
|
+
READ_STATE_HANDLE_KEY,
|
|
2020
|
+
ReadStateStore,
|
|
2021
|
+
bashSubcommandSubjects,
|
|
2022
|
+
composeBriefing,
|
|
2023
|
+
createCheckpointStore,
|
|
2024
|
+
createPermissionGate,
|
|
2025
|
+
createReadStateStore,
|
|
2026
|
+
evaluateCatastrophic,
|
|
2027
|
+
gatherContextDocs,
|
|
2028
|
+
makeRule,
|
|
2029
|
+
provisionDeck,
|
|
2030
|
+
resolveRuleDecision
|
|
2031
|
+
};
|