bare-agent 0.10.4 → 0.12.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/bin/cli.d.ts +4 -0
- package/bin/cli.js +70 -12
- package/bin/test-provider.d.ts +2 -0
- package/bin/test-provider.js +5 -1
- package/index.d.ts +20 -0
- package/package.json +44 -10
- package/src/bareguard-adapter.d.ts +118 -0
- package/src/bareguard-adapter.js +75 -3
- package/src/checkpoint.d.ts +61 -0
- package/src/checkpoint.js +17 -8
- package/src/circuit-breaker.d.ts +70 -0
- package/src/circuit-breaker.js +20 -4
- package/src/errors.d.ts +106 -0
- package/src/errors.js +50 -1
- package/src/loop.d.ts +135 -0
- package/src/loop.js +80 -18
- package/src/mcp-bridge.d.ts +133 -0
- package/src/mcp-bridge.js +199 -26
- package/src/mcp.d.ts +4 -0
- package/src/memory.d.ts +50 -0
- package/src/memory.js +22 -2
- package/src/planner.d.ts +62 -0
- package/src/planner.js +26 -7
- package/src/provider-anthropic.d.ts +55 -0
- package/src/provider-anthropic.js +34 -10
- package/src/provider-clipipe.d.ts +86 -0
- package/src/provider-clipipe.js +28 -18
- package/src/provider-fallback.d.ts +44 -0
- package/src/provider-fallback.js +18 -8
- package/src/provider-ollama.d.ts +41 -0
- package/src/provider-ollama.js +29 -7
- package/src/provider-openai.d.ts +57 -0
- package/src/provider-openai.js +34 -7
- package/src/providers.d.ts +6 -0
- package/src/retry.d.ts +44 -0
- package/src/retry.js +15 -1
- package/src/run-plan.d.ts +126 -0
- package/src/run-plan.js +46 -13
- package/src/scheduler.d.ts +102 -0
- package/src/scheduler.js +32 -4
- package/src/state.d.ts +45 -0
- package/src/state.js +18 -2
- package/src/store-jsonfile.d.ts +85 -0
- package/src/store-jsonfile.js +50 -8
- package/src/store-sqlite.d.ts +90 -0
- package/src/store-sqlite.js +31 -7
- package/src/stores.d.ts +3 -0
- package/src/stream.d.ts +79 -0
- package/src/stream.js +32 -0
- package/src/tools.d.ts +8 -0
- package/src/transport-jsonl.d.ts +30 -0
- package/src/transport-jsonl.js +13 -0
- package/src/transports.d.ts +2 -0
- package/tools/browse.d.ts +10 -0
- package/tools/browse.js +2 -0
- package/tools/defer.d.ts +33 -0
- package/tools/defer.js +12 -3
- package/tools/mobile.d.ts +34 -0
- package/tools/mobile.js +28 -15
- package/tools/shell.d.ts +31 -0
- package/tools/shell.js +83 -6
- package/tools/spawn.d.ts +107 -0
- package/tools/spawn.js +24 -5
- package/types/index.d.ts +66 -0
- package/types/shims.d.ts +16 -0
package/tools/defer.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {object} [options]
|
|
3
|
+
* @param {string} [options.queuePath] - Override queue file path.
|
|
4
|
+
* @returns {{tool: import('../types').ToolDef, readQueue: () => Promise<Record<string, any>[]>, queuePath: string}}
|
|
5
|
+
*/
|
|
6
|
+
export function createDeferTool(options?: {
|
|
7
|
+
queuePath?: string | undefined;
|
|
8
|
+
}): {
|
|
9
|
+
tool: import("../types").ToolDef;
|
|
10
|
+
readQueue: () => Promise<Record<string, any>[]>;
|
|
11
|
+
queuePath: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Read the queue and reconstruct the live status of each id by folding
|
|
15
|
+
* append-only status lines (latest wins). Exposed for tests + library
|
|
16
|
+
* users; the wake script does its own jq-based fold.
|
|
17
|
+
*/
|
|
18
|
+
/** @param {string} [queuePath] */
|
|
19
|
+
export function readQueue(queuePath?: string): Promise<Record<string, any>[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Generate a sortable, unique id. 9-char base36 timestamp + 20-char hex
|
|
22
|
+
* random. Lexicographically sortable by emit time; unique enough for any
|
|
23
|
+
* realistic defer rate. Same shape as the PRD's `def_01J...` sketch.
|
|
24
|
+
*/
|
|
25
|
+
export function generateId(): string;
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the active queue path. Precedence:
|
|
28
|
+
* 1. Caller-supplied option (createDeferTool({ queuePath: '...' }))
|
|
29
|
+
* 2. BAREAGENT_DEFER_QUEUE env var
|
|
30
|
+
* 3. ./bareagent-defers.jsonl
|
|
31
|
+
*/
|
|
32
|
+
/** @param {string} [option] */
|
|
33
|
+
export function resolveQueuePath(option?: string): string;
|
package/tools/defer.js
CHANGED
|
@@ -54,6 +54,7 @@ function generateId() {
|
|
|
54
54
|
* 2. BAREAGENT_DEFER_QUEUE env var
|
|
55
55
|
* 3. ./bareagent-defers.jsonl
|
|
56
56
|
*/
|
|
57
|
+
/** @param {string} [option] */
|
|
57
58
|
function resolveQueuePath(option) {
|
|
58
59
|
return option
|
|
59
60
|
|| process.env.BAREAGENT_DEFER_QUEUE
|
|
@@ -68,6 +69,7 @@ function resolveQueuePath(option) {
|
|
|
68
69
|
*
|
|
69
70
|
* Returns { ok: true, iso } on success, { ok: false, error } on failure.
|
|
70
71
|
*/
|
|
72
|
+
/** @param {*} when */
|
|
71
73
|
function validateWhen(when) {
|
|
72
74
|
if (typeof when !== 'string' || !when) {
|
|
73
75
|
return { ok: false, error: 'when must be an ISO 8601 timestamp string' };
|
|
@@ -88,6 +90,7 @@ function validateWhen(when) {
|
|
|
88
90
|
* Anything else is the LLM either confused or trying to defer something
|
|
89
91
|
* meaningless.
|
|
90
92
|
*/
|
|
93
|
+
/** @param {*} action */
|
|
91
94
|
function validateAction(action) {
|
|
92
95
|
if (!action || typeof action !== 'object' || Array.isArray(action)) {
|
|
93
96
|
return { ok: false, error: 'action must be an object' };
|
|
@@ -103,6 +106,10 @@ function validateAction(action) {
|
|
|
103
106
|
* atomic for writes < PIPE_BUF on POSIX (4KB on Linux); a JSON record
|
|
104
107
|
* with a small action is well under that.
|
|
105
108
|
*/
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} queuePath
|
|
111
|
+
* @param {Record<string, any>} record
|
|
112
|
+
*/
|
|
106
113
|
async function appendRecord(queuePath, record) {
|
|
107
114
|
const dir = path.dirname(path.resolve(queuePath));
|
|
108
115
|
// Best-effort dir creation; ignore "already exists".
|
|
@@ -121,10 +128,12 @@ async function appendRecord(queuePath, record) {
|
|
|
121
128
|
* append-only status lines (latest wins). Exposed for tests + library
|
|
122
129
|
* users; the wake script does its own jq-based fold.
|
|
123
130
|
*/
|
|
131
|
+
/** @param {string} [queuePath] */
|
|
124
132
|
async function readQueue(queuePath) {
|
|
125
133
|
const path = resolveQueuePath(queuePath);
|
|
126
134
|
try {
|
|
127
135
|
const text = await fsp.readFile(path, 'utf8');
|
|
136
|
+
/** @type {Record<string, Record<string, any>>} */
|
|
128
137
|
const records = {};
|
|
129
138
|
for (const line of text.split('\n')) {
|
|
130
139
|
if (!line.trim()) continue;
|
|
@@ -134,7 +143,7 @@ async function readQueue(queuePath) {
|
|
|
134
143
|
records[r.id] = { ...records[r.id], ...r };
|
|
135
144
|
}
|
|
136
145
|
return Object.values(records);
|
|
137
|
-
} catch (err) {
|
|
146
|
+
} catch (/** @type {any} */ err) {
|
|
138
147
|
if (err.code === 'ENOENT') return [];
|
|
139
148
|
throw err;
|
|
140
149
|
}
|
|
@@ -143,7 +152,7 @@ async function readQueue(queuePath) {
|
|
|
143
152
|
/**
|
|
144
153
|
* @param {object} [options]
|
|
145
154
|
* @param {string} [options.queuePath] - Override queue file path.
|
|
146
|
-
* @returns {{tool:
|
|
155
|
+
* @returns {{tool: import('../types').ToolDef, readQueue: () => Promise<Record<string, any>[]>, queuePath: string}}
|
|
147
156
|
*/
|
|
148
157
|
function createDeferTool(options = {}) {
|
|
149
158
|
const queuePath = resolveQueuePath(options.queuePath);
|
|
@@ -166,7 +175,7 @@ function createDeferTool(options = {}) {
|
|
|
166
175
|
},
|
|
167
176
|
required: ['action', 'when'],
|
|
168
177
|
},
|
|
169
|
-
execute: async ({ action, when }) => {
|
|
178
|
+
execute: async (/** @type {{action: *, when: *}} */ { action, when }) => {
|
|
170
179
|
const a = validateAction(action);
|
|
171
180
|
if (!a.ok) throw new Error(`[defer] ${a.error}`);
|
|
172
181
|
const w = validateWhen(when);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type ToolDef = import("../types").ToolDef;
|
|
2
|
+
/**
|
|
3
|
+
* A connected baremobile page/device handle. baremobile ships no types, so the
|
|
4
|
+
* surface used here is described structurally; all calls resolve to `any`.
|
|
5
|
+
*/
|
|
6
|
+
export type MobilePage = any;
|
|
7
|
+
/** @typedef {import('../types').ToolDef} ToolDef */
|
|
8
|
+
/**
|
|
9
|
+
* A connected baremobile page/device handle. baremobile ships no types, so the
|
|
10
|
+
* surface used here is described structurally; all calls resolve to `any`.
|
|
11
|
+
* @typedef {any} MobilePage
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Creates mobile control tools via baremobile (optional dep).
|
|
15
|
+
* Returns { tools, close } or null if baremobile is not installed.
|
|
16
|
+
*
|
|
17
|
+
* Tools follow the snapshot() → tap(ref) observe-act pattern.
|
|
18
|
+
* Action tools auto-return a fresh snapshot so the LLM sees the result.
|
|
19
|
+
* Supports dual platform: Android (default) and iOS via platform option.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} [opts] - Options passed to baremobile connect()
|
|
22
|
+
* @param {string} [opts.platform] - 'android' (default) or 'ios'
|
|
23
|
+
* @param {string} [opts.device] - Device serial or 'auto'
|
|
24
|
+
* @param {boolean} [opts.termux] - Use Termux ADB on-device mode
|
|
25
|
+
* @returns {Promise<{tools: ToolDef[], close: Function}|null>}
|
|
26
|
+
*/
|
|
27
|
+
export function createMobileTools(opts?: {
|
|
28
|
+
platform?: string | undefined;
|
|
29
|
+
device?: string | undefined;
|
|
30
|
+
termux?: boolean | undefined;
|
|
31
|
+
}): Promise<{
|
|
32
|
+
tools: ToolDef[];
|
|
33
|
+
close: Function;
|
|
34
|
+
} | null>;
|
package/tools/mobile.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
/** @typedef {import('../types').ToolDef} ToolDef */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A connected baremobile page/device handle. baremobile ships no types, so the
|
|
7
|
+
* surface used here is described structurally; all calls resolve to `any`.
|
|
8
|
+
* @typedef {any} MobilePage
|
|
9
|
+
*/
|
|
10
|
+
|
|
3
11
|
/**
|
|
4
12
|
* Creates mobile control tools via baremobile (optional dep).
|
|
5
13
|
* Returns { tools, close } or null if baremobile is not installed.
|
|
@@ -12,14 +20,16 @@
|
|
|
12
20
|
* @param {string} [opts.platform] - 'android' (default) or 'ios'
|
|
13
21
|
* @param {string} [opts.device] - Device serial or 'auto'
|
|
14
22
|
* @param {boolean} [opts.termux] - Use Termux ADB on-device mode
|
|
15
|
-
* @returns {Promise<{tools:
|
|
23
|
+
* @returns {Promise<{tools: ToolDef[], close: Function}|null>}
|
|
16
24
|
*/
|
|
17
25
|
async function createMobileTools(opts = {}) {
|
|
18
26
|
const platform = opts.platform || 'android';
|
|
19
27
|
|
|
28
|
+
/** @type {(opts: object) => Promise<MobilePage>} */
|
|
20
29
|
let connectFn;
|
|
21
30
|
try {
|
|
22
31
|
if (platform === 'ios') {
|
|
32
|
+
// baremobile/ios is declared as an ambient `any` module in types/shims.d.ts.
|
|
23
33
|
({ connect: connectFn } = await import('baremobile/ios'));
|
|
24
34
|
} else {
|
|
25
35
|
({ connect: connectFn } = await import('baremobile'));
|
|
@@ -31,6 +41,7 @@ async function createMobileTools(opts = {}) {
|
|
|
31
41
|
const SETTLE_MS = 1000;
|
|
32
42
|
const settle = () => new Promise((r) => setTimeout(r, SETTLE_MS));
|
|
33
43
|
|
|
44
|
+
/** @type {MobilePage|null} */
|
|
34
45
|
let _page = null;
|
|
35
46
|
|
|
36
47
|
async function getPage() {
|
|
@@ -38,6 +49,7 @@ async function createMobileTools(opts = {}) {
|
|
|
38
49
|
return _page;
|
|
39
50
|
}
|
|
40
51
|
|
|
52
|
+
/** @param {(page: MobilePage) => any} fn */
|
|
41
53
|
async function actionAndSnapshot(fn) {
|
|
42
54
|
const page = await getPage();
|
|
43
55
|
await fn(page);
|
|
@@ -45,6 +57,7 @@ async function createMobileTools(opts = {}) {
|
|
|
45
57
|
return await page.snapshot();
|
|
46
58
|
}
|
|
47
59
|
|
|
60
|
+
/** @type {ToolDef[]} */
|
|
48
61
|
const tools = [
|
|
49
62
|
{
|
|
50
63
|
name: 'mobile_snapshot',
|
|
@@ -65,7 +78,7 @@ async function createMobileTools(opts = {}) {
|
|
|
65
78
|
},
|
|
66
79
|
required: ['ref'],
|
|
67
80
|
},
|
|
68
|
-
execute: async ({ ref }) => actionAndSnapshot((page) => page.tap(ref)),
|
|
81
|
+
execute: async (/** @type {{ ref: number }} */ { ref }) => actionAndSnapshot((page) => page.tap(ref)),
|
|
69
82
|
},
|
|
70
83
|
{
|
|
71
84
|
name: 'mobile_type',
|
|
@@ -79,7 +92,7 @@ async function createMobileTools(opts = {}) {
|
|
|
79
92
|
},
|
|
80
93
|
required: ['ref', 'text'],
|
|
81
94
|
},
|
|
82
|
-
execute: async ({ ref, text, clear }) => actionAndSnapshot((page) => page.type(ref, text, { clear })),
|
|
95
|
+
execute: async (/** @type {{ ref: number, text: string, clear?: boolean }} */ { ref, text, clear }) => actionAndSnapshot((page) => page.type(ref, text, { clear })),
|
|
83
96
|
},
|
|
84
97
|
{
|
|
85
98
|
name: 'mobile_press',
|
|
@@ -91,7 +104,7 @@ async function createMobileTools(opts = {}) {
|
|
|
91
104
|
},
|
|
92
105
|
required: ['key'],
|
|
93
106
|
},
|
|
94
|
-
execute: async ({ key }) => actionAndSnapshot((page) => page.press(key)),
|
|
107
|
+
execute: async (/** @type {{ key: string }} */ { key }) => actionAndSnapshot((page) => page.press(key)),
|
|
95
108
|
},
|
|
96
109
|
{
|
|
97
110
|
name: 'mobile_scroll',
|
|
@@ -104,7 +117,7 @@ async function createMobileTools(opts = {}) {
|
|
|
104
117
|
},
|
|
105
118
|
required: ['ref', 'direction'],
|
|
106
119
|
},
|
|
107
|
-
execute: async ({ ref, direction }) => actionAndSnapshot((page) => page.scroll(ref, direction)),
|
|
120
|
+
execute: async (/** @type {{ ref: number, direction: string }} */ { ref, direction }) => actionAndSnapshot((page) => page.scroll(ref, direction)),
|
|
108
121
|
},
|
|
109
122
|
{
|
|
110
123
|
name: 'mobile_swipe',
|
|
@@ -120,7 +133,7 @@ async function createMobileTools(opts = {}) {
|
|
|
120
133
|
},
|
|
121
134
|
required: ['x1', 'y1', 'x2', 'y2'],
|
|
122
135
|
},
|
|
123
|
-
execute: async ({ x1, y1, x2, y2, duration }) => actionAndSnapshot((page) => page.swipe(x1, y1, x2, y2, duration)),
|
|
136
|
+
execute: async (/** @type {{ x1: number, y1: number, x2: number, y2: number, duration?: number }} */ { x1, y1, x2, y2, duration }) => actionAndSnapshot((page) => page.swipe(x1, y1, x2, y2, duration)),
|
|
124
137
|
},
|
|
125
138
|
{
|
|
126
139
|
name: 'mobile_long_press',
|
|
@@ -132,7 +145,7 @@ async function createMobileTools(opts = {}) {
|
|
|
132
145
|
},
|
|
133
146
|
required: ['ref'],
|
|
134
147
|
},
|
|
135
|
-
execute: async ({ ref }) => actionAndSnapshot((page) => page.longPress(ref)),
|
|
148
|
+
execute: async (/** @type {{ ref: number }} */ { ref }) => actionAndSnapshot((page) => page.longPress(ref)),
|
|
136
149
|
},
|
|
137
150
|
{
|
|
138
151
|
name: 'mobile_launch',
|
|
@@ -144,7 +157,7 @@ async function createMobileTools(opts = {}) {
|
|
|
144
157
|
},
|
|
145
158
|
required: ['pkg'],
|
|
146
159
|
},
|
|
147
|
-
execute: async ({ pkg }) => {
|
|
160
|
+
execute: async (/** @type {{ pkg: string }} */ { pkg }) => {
|
|
148
161
|
const page = await getPage();
|
|
149
162
|
await page.launch(pkg);
|
|
150
163
|
await new Promise((r) => setTimeout(r, 2000));
|
|
@@ -184,7 +197,7 @@ async function createMobileTools(opts = {}) {
|
|
|
184
197
|
},
|
|
185
198
|
required: ['x', 'y'],
|
|
186
199
|
},
|
|
187
|
-
execute: async ({ x, y }) => actionAndSnapshot((page) => page.tapXY(x, y)),
|
|
200
|
+
execute: async (/** @type {{ x: number, y: number }} */ { x, y }) => actionAndSnapshot((page) => page.tapXY(x, y)),
|
|
188
201
|
},
|
|
189
202
|
];
|
|
190
203
|
|
|
@@ -202,7 +215,7 @@ async function createMobileTools(opts = {}) {
|
|
|
202
215
|
},
|
|
203
216
|
required: ['action'],
|
|
204
217
|
},
|
|
205
|
-
execute: async ({ action, extras }) => actionAndSnapshot((page) => page.intent(action, extras || {})),
|
|
218
|
+
execute: async (/** @type {{ action: string, extras?: object }} */ { action, extras }) => actionAndSnapshot((page) => page.intent(action, extras || {})),
|
|
206
219
|
},
|
|
207
220
|
{
|
|
208
221
|
name: 'mobile_tap_grid',
|
|
@@ -214,7 +227,7 @@ async function createMobileTools(opts = {}) {
|
|
|
214
227
|
},
|
|
215
228
|
required: ['cell'],
|
|
216
229
|
},
|
|
217
|
-
execute: async ({ cell }) => actionAndSnapshot((page) => page.tapGrid(cell)),
|
|
230
|
+
execute: async (/** @type {{ cell: string }} */ { cell }) => actionAndSnapshot((page) => page.tapGrid(cell)),
|
|
218
231
|
},
|
|
219
232
|
{
|
|
220
233
|
name: 'mobile_grid',
|
|
@@ -241,7 +254,7 @@ async function createMobileTools(opts = {}) {
|
|
|
241
254
|
},
|
|
242
255
|
required: ['passcode'],
|
|
243
256
|
},
|
|
244
|
-
execute: async ({ passcode }) => actionAndSnapshot((page) => page.unlock(passcode)),
|
|
257
|
+
execute: async (/** @type {{ passcode: string }} */ { passcode }) => actionAndSnapshot((page) => page.unlock(passcode)),
|
|
245
258
|
});
|
|
246
259
|
}
|
|
247
260
|
|
|
@@ -256,7 +269,7 @@ async function createMobileTools(opts = {}) {
|
|
|
256
269
|
},
|
|
257
270
|
required: ['text'],
|
|
258
271
|
},
|
|
259
|
-
execute: async ({ text }) => {
|
|
272
|
+
execute: async (/** @type {{ text: string }} */ { text }) => {
|
|
260
273
|
const page = await getPage();
|
|
261
274
|
const ref = page.findByText(text);
|
|
262
275
|
return ref !== null && ref !== undefined ? ref : null;
|
|
@@ -276,7 +289,7 @@ async function createMobileTools(opts = {}) {
|
|
|
276
289
|
},
|
|
277
290
|
required: ['text'],
|
|
278
291
|
},
|
|
279
|
-
execute: async ({ text, timeout }) => {
|
|
292
|
+
execute: async (/** @type {{ text: string, timeout?: number }} */ { text, timeout }) => {
|
|
280
293
|
const page = await getPage();
|
|
281
294
|
return await page.waitForText(text, timeout || 10000);
|
|
282
295
|
},
|
|
@@ -293,7 +306,7 @@ async function createMobileTools(opts = {}) {
|
|
|
293
306
|
},
|
|
294
307
|
required: ['ref', 'state'],
|
|
295
308
|
},
|
|
296
|
-
execute: async ({ ref, state, timeout }) => {
|
|
309
|
+
execute: async (/** @type {{ ref: number, state: string, timeout?: number }} */ { ref, state, timeout }) => {
|
|
297
310
|
const page = await getPage();
|
|
298
311
|
return await page.waitForState(ref, state, timeout || 10000);
|
|
299
312
|
},
|
package/tools/shell.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type GrepArgs = {
|
|
2
|
+
pattern: string;
|
|
3
|
+
path: string;
|
|
4
|
+
recursive?: boolean | undefined;
|
|
5
|
+
maxMatches?: number | undefined;
|
|
6
|
+
flags?: string | undefined;
|
|
7
|
+
};
|
|
8
|
+
export type RunArgvArgs = {
|
|
9
|
+
argv: string[];
|
|
10
|
+
cwd?: string | undefined;
|
|
11
|
+
timeout?: number | undefined;
|
|
12
|
+
maxBuffer?: number | undefined;
|
|
13
|
+
env?: Record<string, string> | undefined;
|
|
14
|
+
};
|
|
15
|
+
export type ExecCommandArgs = {
|
|
16
|
+
command: string;
|
|
17
|
+
cwd?: string | undefined;
|
|
18
|
+
timeout?: number | undefined;
|
|
19
|
+
maxBuffer?: number | undefined;
|
|
20
|
+
env?: Record<string, string> | undefined;
|
|
21
|
+
};
|
|
22
|
+
export type ToolDef = import("../types").ToolDef;
|
|
23
|
+
/**
|
|
24
|
+
* Create the three shell tools. No options — configuration is per-call via tool args,
|
|
25
|
+
* gating is the caller's responsibility via `new Loop({ policy })`.
|
|
26
|
+
*
|
|
27
|
+
* @returns {{tools: ToolDef[]}}
|
|
28
|
+
*/
|
|
29
|
+
export function createShellTools(): {
|
|
30
|
+
tools: ToolDef[];
|
|
31
|
+
};
|
package/tools/shell.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* Library ships zero baked-in allowlist — gating is the agent author's responsibility.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
/** @typedef {import('../types').ToolDef} ToolDef */
|
|
16
|
+
|
|
15
17
|
const fs = require('node:fs/promises');
|
|
16
18
|
const path = require('node:path');
|
|
17
19
|
const { exec, execFile } = require('node:child_process');
|
|
@@ -21,6 +23,10 @@ const DEFAULT_GREP_MAX_MATCHES = 200;
|
|
|
21
23
|
const DEFAULT_EXEC_TIMEOUT_MS = 30_000;
|
|
22
24
|
const DEFAULT_EXEC_MAX_BUFFER = 1024 * 1024; // 1 MB
|
|
23
25
|
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} p
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
24
30
|
function expandHome(p) {
|
|
25
31
|
if (!p) return p;
|
|
26
32
|
if (p.startsWith('~/') || p === '~') {
|
|
@@ -30,6 +36,10 @@ function expandHome(p) {
|
|
|
30
36
|
return p;
|
|
31
37
|
}
|
|
32
38
|
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} rawPath
|
|
41
|
+
* @param {number} [maxBytes]
|
|
42
|
+
*/
|
|
33
43
|
async function readEntry(rawPath, maxBytes) {
|
|
34
44
|
const resolved = path.resolve(expandHome(rawPath));
|
|
35
45
|
const stat = await fs.stat(resolved);
|
|
@@ -56,6 +66,7 @@ async function readEntry(rawPath, maxBytes) {
|
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
// Probe the first 1KB for NUL bytes to skip binary files in grep walks.
|
|
69
|
+
/** @param {string} filePath */
|
|
59
70
|
async function isProbablyText(filePath) {
|
|
60
71
|
try {
|
|
61
72
|
const fh = await fs.open(filePath, 'r');
|
|
@@ -74,6 +85,11 @@ async function isProbablyText(filePath) {
|
|
|
74
85
|
}
|
|
75
86
|
}
|
|
76
87
|
|
|
88
|
+
/**
|
|
89
|
+
* @param {string} dir
|
|
90
|
+
* @param {boolean} recursive
|
|
91
|
+
* @returns {AsyncGenerator<string>}
|
|
92
|
+
*/
|
|
77
93
|
async function* walk(dir, recursive) {
|
|
78
94
|
let entries;
|
|
79
95
|
try {
|
|
@@ -91,16 +107,56 @@ async function* walk(dir, recursive) {
|
|
|
91
107
|
}
|
|
92
108
|
}
|
|
93
109
|
|
|
110
|
+
// Conservative ReDoS guard. Rejects the classic catastrophic-backtracking shape:
|
|
111
|
+
// a quantifier (* + {n,}) applied to a group whose body itself contains an
|
|
112
|
+
// unbounded quantifier — e.g. (a+)+, (a*)*, (.+)* . JS RegExp has no execution
|
|
113
|
+
// timeout, and grep runs the pattern against attacker-influenceable file content
|
|
114
|
+
// on the main thread, so one such pattern blocks the whole event loop. Errs toward
|
|
115
|
+
// rejection; the agent simply rephrases. (Single-level nesting only — does not
|
|
116
|
+
// detect deeply nested groups or overlapping alternation like (a|a)*.)
|
|
117
|
+
const UNBOUNDED_QUANT = /[*+]|\{\d+,\}/;
|
|
118
|
+
/** @param {string} pattern */
|
|
119
|
+
function looksCatastrophic(pattern) {
|
|
120
|
+
// A quantifier binds to the atom immediately before it — no whitespace between
|
|
121
|
+
// `)` and the quantifier in a real regex.
|
|
122
|
+
const groupQuant = /\(([^()]*)\)(?:[*+]|\{\d+,\})/g;
|
|
123
|
+
let m;
|
|
124
|
+
while ((m = groupQuant.exec(pattern)) !== null) {
|
|
125
|
+
// Drop escaped literals (\+ \* \{ …) so a group like (\+)+ — one-or-more
|
|
126
|
+
// literal plus signs, which is linear — isn't mistaken for a nested quantifier.
|
|
127
|
+
const body = m[1].replace(/\\./g, '');
|
|
128
|
+
if (UNBOUNDED_QUANT.test(body)) return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @typedef {object} GrepArgs
|
|
135
|
+
* @property {string} pattern
|
|
136
|
+
* @property {string} path
|
|
137
|
+
* @property {boolean} [recursive]
|
|
138
|
+
* @property {number} [maxMatches]
|
|
139
|
+
* @property {string} [flags]
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
/** @param {GrepArgs} args */
|
|
94
143
|
async function grepPath({ pattern, path: rawPath, recursive = true, maxMatches, flags = 'i' }) {
|
|
95
144
|
const resolved = path.resolve(expandHome(rawPath));
|
|
96
145
|
const cap = maxMatches || DEFAULT_GREP_MAX_MATCHES;
|
|
146
|
+
if (looksCatastrophic(pattern)) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`shell_grep: pattern rejected — nested unbounded quantifier (e.g. "(a+)+") risks catastrophic ` +
|
|
149
|
+
`backtracking that would block the process. Simplify the regex.`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
97
152
|
let re;
|
|
98
153
|
try {
|
|
99
154
|
re = new RegExp(pattern, flags);
|
|
100
|
-
} catch (err) {
|
|
155
|
+
} catch (/** @type {any} */ err) {
|
|
101
156
|
throw new Error(`shell_grep: invalid regex — ${err.message}`);
|
|
102
157
|
}
|
|
103
158
|
|
|
159
|
+
/** @type {{file: string, line: number, text: string}[]} */
|
|
104
160
|
const hits = [];
|
|
105
161
|
const stat = await fs.stat(resolved).catch(() => null);
|
|
106
162
|
if (!stat) throw new Error(`shell_grep: path not found — ${rawPath}`);
|
|
@@ -134,6 +190,16 @@ async function grepPath({ pattern, path: rawPath, recursive = true, maxMatches,
|
|
|
134
190
|
return { hits, truncated, fileCount: files.length };
|
|
135
191
|
}
|
|
136
192
|
|
|
193
|
+
/**
|
|
194
|
+
* @typedef {object} RunArgvArgs
|
|
195
|
+
* @property {string[]} argv
|
|
196
|
+
* @property {string} [cwd]
|
|
197
|
+
* @property {number} [timeout]
|
|
198
|
+
* @property {number} [maxBuffer]
|
|
199
|
+
* @property {Record<string, string>} [env]
|
|
200
|
+
*/
|
|
201
|
+
|
|
202
|
+
/** @param {RunArgvArgs} args */
|
|
137
203
|
function runArgv({ argv, cwd, timeout, maxBuffer, env }) {
|
|
138
204
|
if (!Array.isArray(argv) || argv.length === 0 || typeof argv[0] !== 'string') {
|
|
139
205
|
return Promise.reject(new Error('shell_run: argv must be a non-empty array of strings, starting with the command'));
|
|
@@ -175,6 +241,16 @@ function runArgv({ argv, cwd, timeout, maxBuffer, env }) {
|
|
|
175
241
|
});
|
|
176
242
|
}
|
|
177
243
|
|
|
244
|
+
/**
|
|
245
|
+
* @typedef {object} ExecCommandArgs
|
|
246
|
+
* @property {string} command
|
|
247
|
+
* @property {string} [cwd]
|
|
248
|
+
* @property {number} [timeout]
|
|
249
|
+
* @property {number} [maxBuffer]
|
|
250
|
+
* @property {Record<string, string>} [env]
|
|
251
|
+
*/
|
|
252
|
+
|
|
253
|
+
/** @param {ExecCommandArgs} args */
|
|
178
254
|
function execCommand({ command, cwd, timeout, maxBuffer, env }) {
|
|
179
255
|
return new Promise((resolve) => {
|
|
180
256
|
exec(
|
|
@@ -210,9 +286,10 @@ function execCommand({ command, cwd, timeout, maxBuffer, env }) {
|
|
|
210
286
|
* Create the three shell tools. No options — configuration is per-call via tool args,
|
|
211
287
|
* gating is the caller's responsibility via `new Loop({ policy })`.
|
|
212
288
|
*
|
|
213
|
-
* @returns {{tools:
|
|
289
|
+
* @returns {{tools: ToolDef[]}}
|
|
214
290
|
*/
|
|
215
291
|
function createShellTools() {
|
|
292
|
+
/** @type {ToolDef[]} */
|
|
216
293
|
const tools = [
|
|
217
294
|
{
|
|
218
295
|
name: 'shell_read',
|
|
@@ -225,7 +302,7 @@ function createShellTools() {
|
|
|
225
302
|
},
|
|
226
303
|
required: ['path'],
|
|
227
304
|
},
|
|
228
|
-
execute: async ({ path: p, maxBytes }) => readEntry(p, maxBytes),
|
|
305
|
+
execute: async (/** @type {{path: string, maxBytes?: number}} */ { path: p, maxBytes }) => readEntry(p, maxBytes),
|
|
229
306
|
},
|
|
230
307
|
{
|
|
231
308
|
name: 'shell_grep',
|
|
@@ -241,7 +318,7 @@ function createShellTools() {
|
|
|
241
318
|
},
|
|
242
319
|
required: ['pattern', 'path'],
|
|
243
320
|
},
|
|
244
|
-
execute: async (args) => grepPath(args),
|
|
321
|
+
execute: async (/** @type {GrepArgs} */ args) => grepPath(args),
|
|
245
322
|
},
|
|
246
323
|
{
|
|
247
324
|
name: 'shell_run',
|
|
@@ -261,7 +338,7 @@ function createShellTools() {
|
|
|
261
338
|
},
|
|
262
339
|
required: ['argv'],
|
|
263
340
|
},
|
|
264
|
-
execute: async (args) => runArgv(args),
|
|
341
|
+
execute: async (/** @type {RunArgvArgs} */ args) => runArgv(args),
|
|
265
342
|
},
|
|
266
343
|
{
|
|
267
344
|
name: 'shell_exec',
|
|
@@ -277,7 +354,7 @@ function createShellTools() {
|
|
|
277
354
|
},
|
|
278
355
|
required: ['command'],
|
|
279
356
|
},
|
|
280
|
-
execute: async (args) => execCommand(args),
|
|
357
|
+
execute: async (/** @type {ExecCommandArgs} */ args) => execCommand(args),
|
|
281
358
|
},
|
|
282
359
|
];
|
|
283
360
|
return { tools };
|
package/tools/spawn.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library-level: spawn a child and return a handle.
|
|
3
|
+
*
|
|
4
|
+
* Returns: {
|
|
5
|
+
* wait() — Promise<{ text, usage, cost, error, events }>
|
|
6
|
+
* onLine(fn) — subscribe to every JSONL event from child stdout
|
|
7
|
+
* kill(sig?) — terminate the child
|
|
8
|
+
* pid — child process id
|
|
9
|
+
* }
|
|
10
|
+
*
|
|
11
|
+
* Use this from library code; the LLM-callable tool below wraps it with blocking semantics.
|
|
12
|
+
*/
|
|
13
|
+
export type ChildEvent = Record<string, any> & {
|
|
14
|
+
type?: string;
|
|
15
|
+
text?: string;
|
|
16
|
+
ts?: string;
|
|
17
|
+
data?: any;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Library-level: spawn a child and return a handle.
|
|
21
|
+
*
|
|
22
|
+
* Returns: {
|
|
23
|
+
* wait() — Promise<{ text, usage, cost, error, events }>
|
|
24
|
+
* onLine(fn) — subscribe to every JSONL event from child stdout
|
|
25
|
+
* kill(sig?) — terminate the child
|
|
26
|
+
* pid — child process id
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* Use this from library code; the LLM-callable tool below wraps it with blocking semantics.
|
|
30
|
+
*/
|
|
31
|
+
export type SpawnChildOptions = {
|
|
32
|
+
/**
|
|
33
|
+
* - Path to a bareagent config JSON file.
|
|
34
|
+
*/
|
|
35
|
+
config?: string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* - Optional JSON input passed to the child on stdin.
|
|
38
|
+
*/
|
|
39
|
+
input?: any;
|
|
40
|
+
/**
|
|
41
|
+
* - Override the bareagent CLI path.
|
|
42
|
+
*/
|
|
43
|
+
cliPath?: string | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* - Force-kill child after this many ms.
|
|
46
|
+
*/
|
|
47
|
+
timeoutMs?: number | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* - bareagent Stream — child:stderr events get re-emitted here.
|
|
50
|
+
*/
|
|
51
|
+
stream?: import("../src/stream").Stream | undefined;
|
|
52
|
+
};
|
|
53
|
+
export type Stream = import("../src/stream").Stream;
|
|
54
|
+
/**
|
|
55
|
+
* LLM-callable spawn tool. Blocks; returns the child's final result.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} [options]
|
|
58
|
+
* @param {string} [options.cliPath] - Override the bareagent CLI path (default: ./bin/cli.js relative to this file).
|
|
59
|
+
* @param {number} [options.timeoutMs] - Force-kill child after this many ms (default 10 min).
|
|
60
|
+
* @param {Stream} [options.stream] - bareagent Stream instance — child:stderr events get re-emitted here.
|
|
61
|
+
* @returns {{tool: import('../types').ToolDef, spawnChild: typeof spawnChild}}
|
|
62
|
+
*/
|
|
63
|
+
export function createSpawnTool(options?: {
|
|
64
|
+
cliPath?: string | undefined;
|
|
65
|
+
timeoutMs?: number | undefined;
|
|
66
|
+
stream?: import("../src/stream").Stream | undefined;
|
|
67
|
+
}): {
|
|
68
|
+
tool: import("../types").ToolDef;
|
|
69
|
+
spawnChild: typeof spawnChild;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Library-level: spawn a child and return a handle.
|
|
73
|
+
*
|
|
74
|
+
* Returns: {
|
|
75
|
+
* wait() — Promise<{ text, usage, cost, error, events }>
|
|
76
|
+
* onLine(fn) — subscribe to every JSONL event from child stdout
|
|
77
|
+
* kill(sig?) — terminate the child
|
|
78
|
+
* pid — child process id
|
|
79
|
+
* }
|
|
80
|
+
*
|
|
81
|
+
* Use this from library code; the LLM-callable tool below wraps it with blocking semantics.
|
|
82
|
+
*
|
|
83
|
+
* @typedef {Record<string, any> & {type?: string, text?: string, ts?: string, data?: any}} ChildEvent
|
|
84
|
+
*
|
|
85
|
+
* @typedef {object} SpawnChildOptions
|
|
86
|
+
* @property {string} [config] - Path to a bareagent config JSON file.
|
|
87
|
+
* @property {*} [input] - Optional JSON input passed to the child on stdin.
|
|
88
|
+
* @property {string} [cliPath] - Override the bareagent CLI path.
|
|
89
|
+
* @property {number} [timeoutMs] - Force-kill child after this many ms.
|
|
90
|
+
* @property {Stream} [stream] - bareagent Stream — child:stderr events get re-emitted here.
|
|
91
|
+
*
|
|
92
|
+
* @param {SpawnChildOptions} [opts]
|
|
93
|
+
*/
|
|
94
|
+
export function spawnChild({ config, input, cliPath, timeoutMs, stream }?: SpawnChildOptions): {
|
|
95
|
+
wait: () => Promise<{
|
|
96
|
+
text: any;
|
|
97
|
+
usage: any;
|
|
98
|
+
cost: any;
|
|
99
|
+
error: any;
|
|
100
|
+
events: ChildEvent[];
|
|
101
|
+
exitCode: any;
|
|
102
|
+
signal: any;
|
|
103
|
+
}>;
|
|
104
|
+
onLine: (fn: (event: ChildEvent) => void) => () => void;
|
|
105
|
+
kill: (sig?: NodeJS.Signals) => void;
|
|
106
|
+
pid: number | undefined;
|
|
107
|
+
};
|