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.
Files changed (65) hide show
  1. package/bin/cli.d.ts +4 -0
  2. package/bin/cli.js +70 -12
  3. package/bin/test-provider.d.ts +2 -0
  4. package/bin/test-provider.js +5 -1
  5. package/index.d.ts +20 -0
  6. package/package.json +44 -10
  7. package/src/bareguard-adapter.d.ts +118 -0
  8. package/src/bareguard-adapter.js +75 -3
  9. package/src/checkpoint.d.ts +61 -0
  10. package/src/checkpoint.js +17 -8
  11. package/src/circuit-breaker.d.ts +70 -0
  12. package/src/circuit-breaker.js +20 -4
  13. package/src/errors.d.ts +106 -0
  14. package/src/errors.js +50 -1
  15. package/src/loop.d.ts +135 -0
  16. package/src/loop.js +80 -18
  17. package/src/mcp-bridge.d.ts +133 -0
  18. package/src/mcp-bridge.js +199 -26
  19. package/src/mcp.d.ts +4 -0
  20. package/src/memory.d.ts +50 -0
  21. package/src/memory.js +22 -2
  22. package/src/planner.d.ts +62 -0
  23. package/src/planner.js +26 -7
  24. package/src/provider-anthropic.d.ts +55 -0
  25. package/src/provider-anthropic.js +34 -10
  26. package/src/provider-clipipe.d.ts +86 -0
  27. package/src/provider-clipipe.js +28 -18
  28. package/src/provider-fallback.d.ts +44 -0
  29. package/src/provider-fallback.js +18 -8
  30. package/src/provider-ollama.d.ts +41 -0
  31. package/src/provider-ollama.js +29 -7
  32. package/src/provider-openai.d.ts +57 -0
  33. package/src/provider-openai.js +34 -7
  34. package/src/providers.d.ts +6 -0
  35. package/src/retry.d.ts +44 -0
  36. package/src/retry.js +15 -1
  37. package/src/run-plan.d.ts +126 -0
  38. package/src/run-plan.js +46 -13
  39. package/src/scheduler.d.ts +102 -0
  40. package/src/scheduler.js +32 -4
  41. package/src/state.d.ts +45 -0
  42. package/src/state.js +18 -2
  43. package/src/store-jsonfile.d.ts +85 -0
  44. package/src/store-jsonfile.js +50 -8
  45. package/src/store-sqlite.d.ts +90 -0
  46. package/src/store-sqlite.js +31 -7
  47. package/src/stores.d.ts +3 -0
  48. package/src/stream.d.ts +79 -0
  49. package/src/stream.js +32 -0
  50. package/src/tools.d.ts +8 -0
  51. package/src/transport-jsonl.d.ts +30 -0
  52. package/src/transport-jsonl.js +13 -0
  53. package/src/transports.d.ts +2 -0
  54. package/tools/browse.d.ts +10 -0
  55. package/tools/browse.js +2 -0
  56. package/tools/defer.d.ts +33 -0
  57. package/tools/defer.js +12 -3
  58. package/tools/mobile.d.ts +34 -0
  59. package/tools/mobile.js +28 -15
  60. package/tools/shell.d.ts +31 -0
  61. package/tools/shell.js +83 -6
  62. package/tools/spawn.d.ts +107 -0
  63. package/tools/spawn.js +24 -5
  64. package/types/index.d.ts +66 -0
  65. package/types/shims.d.ts +16 -0
@@ -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: object, readQueue: Function}}
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: Array, close: Function}|null>}
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
  },
@@ -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: Array}}
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 };
@@ -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
+ };