bare-agent 0.11.0 → 0.12.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.
Files changed (68) hide show
  1. package/README.md +1 -0
  2. package/bareagent.context.md +1149 -0
  3. package/bin/cli.d.ts +4 -0
  4. package/bin/cli.js +40 -10
  5. package/bin/test-provider.d.ts +2 -0
  6. package/bin/test-provider.js +5 -1
  7. package/index.d.ts +20 -0
  8. package/package.json +46 -10
  9. package/src/bareguard-adapter.d.ts +118 -0
  10. package/src/bareguard-adapter.js +75 -3
  11. package/src/checkpoint.d.ts +61 -0
  12. package/src/checkpoint.js +17 -8
  13. package/src/circuit-breaker.d.ts +70 -0
  14. package/src/circuit-breaker.js +20 -4
  15. package/src/errors.d.ts +106 -0
  16. package/src/errors.js +50 -1
  17. package/src/loop.d.ts +135 -0
  18. package/src/loop.js +73 -17
  19. package/src/mcp-bridge.d.ts +133 -0
  20. package/src/mcp-bridge.js +179 -27
  21. package/src/mcp.d.ts +4 -0
  22. package/src/memory.d.ts +50 -0
  23. package/src/memory.js +22 -2
  24. package/src/planner.d.ts +62 -0
  25. package/src/planner.js +26 -7
  26. package/src/provider-anthropic.d.ts +55 -0
  27. package/src/provider-anthropic.js +32 -11
  28. package/src/provider-clipipe.d.ts +86 -0
  29. package/src/provider-clipipe.js +28 -18
  30. package/src/provider-fallback.d.ts +44 -0
  31. package/src/provider-fallback.js +18 -8
  32. package/src/provider-ollama.d.ts +41 -0
  33. package/src/provider-ollama.js +27 -7
  34. package/src/provider-openai.d.ts +57 -0
  35. package/src/provider-openai.js +31 -16
  36. package/src/providers.d.ts +6 -0
  37. package/src/providers.js +8 -0
  38. package/src/retry.d.ts +44 -0
  39. package/src/retry.js +15 -1
  40. package/src/run-plan.d.ts +126 -0
  41. package/src/run-plan.js +46 -13
  42. package/src/scheduler.d.ts +102 -0
  43. package/src/scheduler.js +32 -4
  44. package/src/state.d.ts +45 -0
  45. package/src/state.js +18 -2
  46. package/src/store-jsonfile.d.ts +85 -0
  47. package/src/store-jsonfile.js +33 -8
  48. package/src/store-sqlite.d.ts +90 -0
  49. package/src/store-sqlite.js +31 -7
  50. package/src/stores.d.ts +3 -0
  51. package/src/stream.d.ts +79 -0
  52. package/src/stream.js +32 -0
  53. package/src/tools.d.ts +8 -0
  54. package/src/transport-jsonl.d.ts +30 -0
  55. package/src/transport-jsonl.js +13 -0
  56. package/src/transports.d.ts +2 -0
  57. package/tools/browse.d.ts +10 -0
  58. package/tools/browse.js +2 -0
  59. package/tools/defer.d.ts +33 -0
  60. package/tools/defer.js +12 -3
  61. package/tools/mobile.d.ts +34 -0
  62. package/tools/mobile.js +28 -15
  63. package/tools/shell.d.ts +31 -0
  64. package/tools/shell.js +55 -6
  65. package/tools/spawn.d.ts +107 -0
  66. package/tools/spawn.js +24 -5
  67. package/types/index.d.ts +66 -0
  68. package/types/shims.d.ts +16 -0
@@ -0,0 +1,30 @@
1
+ export type JsonlTransportOptions = {
2
+ /**
3
+ * - Writable stream to write JSONL lines to. Defaults to process.stdout.
4
+ */
5
+ output?: NodeJS.WritableStream | undefined;
6
+ };
7
+ /**
8
+ * JSONL transport: one JSON object per line to a writable stream.
9
+ * Default: process.stdout. Pipe-friendly, parseable by any language.
10
+ *
11
+ * Debug output goes to stderr (never pollutes stdout).
12
+ */
13
+ /**
14
+ * @typedef {object} JsonlTransportOptions
15
+ * @property {NodeJS.WritableStream} [output] - Writable stream to write JSONL lines to. Defaults to process.stdout.
16
+ */
17
+ export class JsonlTransport {
18
+ /**
19
+ * @param {JsonlTransportOptions} [options={}]
20
+ */
21
+ constructor(options?: JsonlTransportOptions);
22
+ _output: NodeJS.WritableStream | (NodeJS.WriteStream & {
23
+ fd: 1;
24
+ });
25
+ /**
26
+ * @param {*} event - Event object to serialize as one JSON line.
27
+ * @returns {void}
28
+ */
29
+ write(event: any): void;
30
+ }
@@ -6,11 +6,24 @@
6
6
  *
7
7
  * Debug output goes to stderr (never pollutes stdout).
8
8
  */
9
+
10
+ /**
11
+ * @typedef {object} JsonlTransportOptions
12
+ * @property {NodeJS.WritableStream} [output] - Writable stream to write JSONL lines to. Defaults to process.stdout.
13
+ */
14
+
9
15
  class JsonlTransport {
16
+ /**
17
+ * @param {JsonlTransportOptions} [options={}]
18
+ */
10
19
  constructor(options = {}) {
11
20
  this._output = options.output || process.stdout;
12
21
  }
13
22
 
23
+ /**
24
+ * @param {*} event - Event object to serialize as one JSON line.
25
+ * @returns {void}
26
+ */
14
27
  write(event) {
15
28
  this._output.write(JSON.stringify(event) + '\n');
16
29
  }
@@ -0,0 +1,2 @@
1
+ export { JsonlTransport };
2
+ import { JsonlTransport } from "./transport-jsonl";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Creates browsing tools via barebrowse (optional dep).
3
+ * Returns { tools, close } or null if barebrowse is not installed.
4
+ * @param {object} [opts] - Options passed to barebrowse createBrowseTools
5
+ * @returns {Promise<{tools: Array, close: Function}|null>}
6
+ */
7
+ export function createBrowsingTools(opts?: object): Promise<{
8
+ tools: any[];
9
+ close: Function;
10
+ } | null>;
package/tools/browse.js CHANGED
@@ -8,6 +8,8 @@
8
8
  */
9
9
  async function createBrowsingTools(opts = {}) {
10
10
  try {
11
+ // barebrowse is an optional dep; the subpath is declared as an ambient `any`
12
+ // module in types/shims.d.ts and resolves to `any` at runtime.
11
13
  const { createBrowseTools } = await import('barebrowse/bareagent');
12
14
  return createBrowseTools(opts);
13
15
  } catch {
@@ -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 {
@@ -99,6 +115,7 @@ async function* walk(dir, recursive) {
99
115
  // rejection; the agent simply rephrases. (Single-level nesting only — does not
100
116
  // detect deeply nested groups or overlapping alternation like (a|a)*.)
101
117
  const UNBOUNDED_QUANT = /[*+]|\{\d+,\}/;
118
+ /** @param {string} pattern */
102
119
  function looksCatastrophic(pattern) {
103
120
  // A quantifier binds to the atom immediately before it — no whitespace between
104
121
  // `)` and the quantifier in a real regex.
@@ -113,6 +130,16 @@ function looksCatastrophic(pattern) {
113
130
  return false;
114
131
  }
115
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 */
116
143
  async function grepPath({ pattern, path: rawPath, recursive = true, maxMatches, flags = 'i' }) {
117
144
  const resolved = path.resolve(expandHome(rawPath));
118
145
  const cap = maxMatches || DEFAULT_GREP_MAX_MATCHES;
@@ -125,10 +152,11 @@ async function grepPath({ pattern, path: rawPath, recursive = true, maxMatches,
125
152
  let re;
126
153
  try {
127
154
  re = new RegExp(pattern, flags);
128
- } catch (err) {
155
+ } catch (/** @type {any} */ err) {
129
156
  throw new Error(`shell_grep: invalid regex — ${err.message}`);
130
157
  }
131
158
 
159
+ /** @type {{file: string, line: number, text: string}[]} */
132
160
  const hits = [];
133
161
  const stat = await fs.stat(resolved).catch(() => null);
134
162
  if (!stat) throw new Error(`shell_grep: path not found — ${rawPath}`);
@@ -162,6 +190,16 @@ async function grepPath({ pattern, path: rawPath, recursive = true, maxMatches,
162
190
  return { hits, truncated, fileCount: files.length };
163
191
  }
164
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 */
165
203
  function runArgv({ argv, cwd, timeout, maxBuffer, env }) {
166
204
  if (!Array.isArray(argv) || argv.length === 0 || typeof argv[0] !== 'string') {
167
205
  return Promise.reject(new Error('shell_run: argv must be a non-empty array of strings, starting with the command'));
@@ -203,6 +241,16 @@ function runArgv({ argv, cwd, timeout, maxBuffer, env }) {
203
241
  });
204
242
  }
205
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 */
206
254
  function execCommand({ command, cwd, timeout, maxBuffer, env }) {
207
255
  return new Promise((resolve) => {
208
256
  exec(
@@ -238,9 +286,10 @@ function execCommand({ command, cwd, timeout, maxBuffer, env }) {
238
286
  * Create the three shell tools. No options — configuration is per-call via tool args,
239
287
  * gating is the caller's responsibility via `new Loop({ policy })`.
240
288
  *
241
- * @returns {{tools: Array}}
289
+ * @returns {{tools: ToolDef[]}}
242
290
  */
243
291
  function createShellTools() {
292
+ /** @type {ToolDef[]} */
244
293
  const tools = [
245
294
  {
246
295
  name: 'shell_read',
@@ -253,7 +302,7 @@ function createShellTools() {
253
302
  },
254
303
  required: ['path'],
255
304
  },
256
- execute: async ({ path: p, maxBytes }) => readEntry(p, maxBytes),
305
+ execute: async (/** @type {{path: string, maxBytes?: number}} */ { path: p, maxBytes }) => readEntry(p, maxBytes),
257
306
  },
258
307
  {
259
308
  name: 'shell_grep',
@@ -269,7 +318,7 @@ function createShellTools() {
269
318
  },
270
319
  required: ['pattern', 'path'],
271
320
  },
272
- execute: async (args) => grepPath(args),
321
+ execute: async (/** @type {GrepArgs} */ args) => grepPath(args),
273
322
  },
274
323
  {
275
324
  name: 'shell_run',
@@ -289,7 +338,7 @@ function createShellTools() {
289
338
  },
290
339
  required: ['argv'],
291
340
  },
292
- execute: async (args) => runArgv(args),
341
+ execute: async (/** @type {RunArgvArgs} */ args) => runArgv(args),
293
342
  },
294
343
  {
295
344
  name: 'shell_exec',
@@ -305,7 +354,7 @@ function createShellTools() {
305
354
  },
306
355
  required: ['command'],
307
356
  },
308
- execute: async (args) => execCommand(args),
357
+ execute: async (/** @type {ExecCommandArgs} */ args) => execCommand(args),
309
358
  },
310
359
  ];
311
360
  return { tools };