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
package/bin/cli.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ export type Provider = import("../types").Provider;
3
+ export type ToolDef = import("../types").ToolDef;
4
+ export type Ctx = import("../types").Ctx;
package/bin/cli.js CHANGED
@@ -36,7 +36,12 @@ const { Loop } = require('../src/loop');
36
36
  const { Stream } = require('../src/stream');
37
37
  const { JsonlTransport } = require('../src/transport-jsonl');
38
38
 
39
+ /** @typedef {import('../types').Provider} Provider */
40
+ /** @typedef {import('../types').ToolDef} ToolDef */
41
+ /** @typedef {import('../types').Ctx} Ctx */
42
+
39
43
  const args = process.argv.slice(2);
44
+ /** @param {string} name */
40
45
  const flag = (name) => {
41
46
  const i = args.indexOf(`--${name}`);
42
47
  return i >= 0 ? args[i + 1] : undefined;
@@ -45,7 +50,7 @@ const flag = (name) => {
45
50
  const configPath = flag('config');
46
51
 
47
52
  if (configPath) {
48
- runConfigMode(configPath).catch((err) => {
53
+ runConfigMode(configPath).catch((/** @type {any} */ err) => {
49
54
  process.stdout.write(JSON.stringify({ type: 'loop:error', data: { source: 'cli', error: err.message } }) + '\n');
50
55
  process.exit(1);
51
56
  });
@@ -55,6 +60,7 @@ if (configPath) {
55
60
 
56
61
  // ─── Mode 2: config-driven ────────────────────────────────────────────────
57
62
 
63
+ /** @param {string} cfgPath */
58
64
  async function runConfigMode(cfgPath) {
59
65
  const cfg = readConfig(cfgPath);
60
66
  const stream = new Stream({ transport: new JsonlTransport() });
@@ -68,9 +74,12 @@ async function runConfigMode(cfgPath) {
68
74
  // Bareguard Gate (optional but strongly recommended for spawn children).
69
75
  // Fail-closed: if the config asks for a gate but wiring fails, exit non-zero
70
76
  // rather than run an ungoverned child agent.
71
- let policy = null;
72
- let onLlmResult = null;
73
- let onToolResult = null;
77
+ /** @type {Function | undefined} */
78
+ let policy;
79
+ /** @type {Function | undefined} */
80
+ let onLlmResult;
81
+ /** @type {Function | undefined} */
82
+ let onToolResult;
74
83
  let gatedTools = tools;
75
84
  if (cfg.gate) {
76
85
  try {
@@ -97,7 +106,7 @@ async function runConfigMode(cfgPath) {
97
106
  }
98
107
  if (typeof humanChannel !== 'function') {
99
108
  let warned = false;
100
- humanChannel = async (event) => {
109
+ humanChannel = async (/** @type {any} */ event) => {
101
110
  if (!warned) {
102
111
  process.stderr.write(`[cli] no humanChannel configured — ${event.kind} on ${event.rule} auto-denying.\n`);
103
112
  warned = true;
@@ -148,7 +157,7 @@ async function runConfigMode(cfgPath) {
148
157
  policy,
149
158
  onLlmResult,
150
159
  onToolResult,
151
- onError: (err, meta) => {
160
+ onError: (/** @type {any} */ err, /** @type {any} */ meta) => {
152
161
  process.stderr.write(`[loop:error ${meta.source}] ${err.message}\n`);
153
162
  },
154
163
  });
@@ -158,6 +167,7 @@ async function runConfigMode(cfgPath) {
158
167
  process.exit(0);
159
168
  }
160
169
 
170
+ /** @param {string} cfgPath */
161
171
  function readConfig(cfgPath) {
162
172
  const abs = path.resolve(cfgPath);
163
173
  let raw;
@@ -179,6 +189,10 @@ function readStdin() {
179
189
  });
180
190
  }
181
191
 
192
+ /**
193
+ * @param {any} cfg
194
+ * @param {string} stdin
195
+ */
182
196
  function buildInitialMessage(cfg, stdin) {
183
197
  if (!stdin) {
184
198
  return { role: 'user', content: cfg.defaultPrompt || 'Begin.' };
@@ -195,7 +209,13 @@ function buildInitialMessage(cfg, stdin) {
195
209
  return { role: 'user', content: stdin };
196
210
  }
197
211
 
212
+ /**
213
+ * @param {string[]} names
214
+ * @param {{ stream: InstanceType<typeof Stream> }} ctx
215
+ * @returns {Promise<ToolDef[]>}
216
+ */
198
217
  async function resolveTools(names, ctx) {
218
+ /** @type {ToolDef[]} */
199
219
  const tools = [];
200
220
  for (const name of names) {
201
221
  const resolved = await resolveOneTool(name, ctx);
@@ -204,6 +224,11 @@ async function resolveTools(names, ctx) {
204
224
  return tools;
205
225
  }
206
226
 
227
+ /**
228
+ * @param {string} name
229
+ * @param {{ stream: InstanceType<typeof Stream> }} ctx
230
+ * @returns {Promise<ToolDef | ToolDef[] | null>}
231
+ */
207
232
  async function resolveOneTool(name, ctx) {
208
233
  switch (name) {
209
234
  case 'shell_read':
@@ -215,16 +240,16 @@ async function resolveOneTool(name, ctx) {
215
240
  return tools.find(t => t.name === name) || null;
216
241
  }
217
242
  case 'shell_*': {
218
- const { createShellTools } = require('../tools/shell');
219
- return createShellTools().tools;
243
+ const { createShellTools: createShellToolsAll } = require('../tools/shell');
244
+ return createShellToolsAll().tools;
220
245
  }
221
246
  case 'spawn': {
222
247
  const { createSpawnTool } = require('../tools/spawn');
223
- return createSpawnTool({ stream: ctx.stream }).tool;
248
+ return /** @type {ToolDef} */ (createSpawnTool({ stream: ctx.stream }).tool);
224
249
  }
225
250
  case 'defer': {
226
251
  const { createDeferTool } = require('../tools/defer');
227
- return createDeferTool().tool;
252
+ return /** @type {ToolDef} */ (createDeferTool().tool);
228
253
  }
229
254
  default:
230
255
  process.stderr.write(`[cli] unknown tool name in config: ${name}\n`);
@@ -269,6 +294,11 @@ function runStdioMode() {
269
294
 
270
295
  // ─── Shared: provider construction ────────────────────────────────────────
271
296
 
297
+ /**
298
+ * @param {string} name
299
+ * @param {string} [model]
300
+ * @returns {Provider}
301
+ */
272
302
  function createProvider(name, model) {
273
303
  if (name === 'openai') {
274
304
  const { OpenAIProvider } = require('../src/provider-openai');
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export type Provider = import("../types").Provider;
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ /** @typedef {import('../types').Provider} Provider */
5
+
4
6
  const args = process.argv.slice(2);
7
+ /** @param {string} name */
5
8
  const flag = (name) => {
6
9
  const i = args.indexOf(`--${name}`);
7
10
  return i >= 0 ? args[i + 1] : undefined;
@@ -10,6 +13,7 @@ const flag = (name) => {
10
13
  const providerName = flag('provider') || 'openai';
11
14
  const model = flag('model');
12
15
 
16
+ /** @returns {Provider} */
13
17
  function createProvider() {
14
18
  if (providerName === 'openai') {
15
19
  const { OpenAIProvider } = require('../src/provider-openai');
@@ -21,7 +25,7 @@ function createProvider() {
21
25
  if (providerName === 'anthropic') {
22
26
  const { AnthropicProvider } = require('../src/provider-anthropic');
23
27
  return new AnthropicProvider({
24
- apiKey: process.env.ANTHROPIC_API_KEY,
28
+ apiKey: process.env.ANTHROPIC_API_KEY || '',
25
29
  ...(model && { model }),
26
30
  });
27
31
  }
package/index.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { Loop } from "./src/loop";
2
+ import { Planner } from "./src/planner";
3
+ import { StateMachine } from "./src/state";
4
+ import { Scheduler } from "./src/scheduler";
5
+ import { Checkpoint } from "./src/checkpoint";
6
+ import { Memory } from "./src/memory";
7
+ import { Stream } from "./src/stream";
8
+ import { Retry } from "./src/retry";
9
+ import { runPlan } from "./src/run-plan";
10
+ import { CircuitBreaker } from "./src/circuit-breaker";
11
+ import { wireGate } from "./src/bareguard-adapter";
12
+ import { defaultActionTranslator } from "./src/bareguard-adapter";
13
+ import { BareAgentError } from "./src/errors";
14
+ import { ProviderError } from "./src/errors";
15
+ import { ToolError } from "./src/errors";
16
+ import { TimeoutError } from "./src/errors";
17
+ import { ValidationError } from "./src/errors";
18
+ import { CircuitOpenError } from "./src/errors";
19
+ import { HaltError } from "./src/errors";
20
+ export { Loop, Planner, StateMachine, Scheduler, Checkpoint, Memory, Stream, Retry, runPlan, CircuitBreaker, wireGate, defaultActionTranslator, BareAgentError, ProviderError, ToolError, TimeoutError, ValidationError, CircuitOpenError, HaltError };
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "bare-agent",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "files": [
5
5
  "index.js",
6
+ "index.d.ts",
7
+ "bareagent.context.md",
6
8
  "src/",
7
9
  "bin/",
8
10
  "tools/",
11
+ "types/",
9
12
  "LICENSE",
10
13
  "NOTICE"
11
14
  ],
@@ -17,18 +20,43 @@
17
20
  "url": "git+https://github.com/hamr0/bareagent.git"
18
21
  },
19
22
  "main": "index.js",
23
+ "types": "./index.d.ts",
20
24
  "bin": {
21
25
  "bare-agent": "bin/cli.js"
22
26
  },
23
27
  "exports": {
24
- ".": "./index.js",
25
- "./errors": "./src/errors.js",
26
- "./providers": "./src/providers.js",
27
- "./stores": "./src/stores.js",
28
- "./transports": "./src/transports.js",
29
- "./tools": "./src/tools.js",
30
- "./mcp": "./src/mcp.js",
31
- "./bareguard": "./src/bareguard-adapter.js",
28
+ ".": {
29
+ "types": "./index.d.ts",
30
+ "default": "./index.js"
31
+ },
32
+ "./errors": {
33
+ "types": "./src/errors.d.ts",
34
+ "default": "./src/errors.js"
35
+ },
36
+ "./providers": {
37
+ "types": "./src/providers.d.ts",
38
+ "default": "./src/providers.js"
39
+ },
40
+ "./stores": {
41
+ "types": "./src/stores.d.ts",
42
+ "default": "./src/stores.js"
43
+ },
44
+ "./transports": {
45
+ "types": "./src/transports.d.ts",
46
+ "default": "./src/transports.js"
47
+ },
48
+ "./tools": {
49
+ "types": "./src/tools.d.ts",
50
+ "default": "./src/tools.js"
51
+ },
52
+ "./mcp": {
53
+ "types": "./src/mcp.d.ts",
54
+ "default": "./src/mcp.js"
55
+ },
56
+ "./bareguard": {
57
+ "types": "./src/bareguard-adapter.d.ts",
58
+ "default": "./src/bareguard-adapter.js"
59
+ },
32
60
  "./package.json": "./package.json"
33
61
  },
34
62
  "engines": {
@@ -62,6 +90,14 @@
62
90
  }
63
91
  },
64
92
  "scripts": {
65
- "test": "node --test --test-force-exit test/**/*.test.js"
93
+ "test": "node --test --test-force-exit test/**/*.test.js",
94
+ "typecheck": "tsc --noEmit",
95
+ "prebuild:types": "node scripts/clean-types.js",
96
+ "build:types": "tsc",
97
+ "prepublishOnly": "npm run build:types"
98
+ },
99
+ "devDependencies": {
100
+ "@types/node": "^22.19.19",
101
+ "typescript": "^5.7.0"
66
102
  }
67
103
  }
@@ -0,0 +1,118 @@
1
+ export type Ctx = import("../types").Ctx;
2
+ export type ToolDef = import("../types").ToolDef;
3
+ export type Usage = import("../types").Usage;
4
+ /**
5
+ * A bareguard Gate instance. Comes from the ambient `bareguard` module, so its
6
+ * methods are accessed structurally here.
7
+ */
8
+ export type Gate = {
9
+ check: (action: any) => (GateDecision | Promise<GateDecision>);
10
+ record: (action: any, outcome?: any) => any;
11
+ allows?: ((toolName: string) => (boolean | Promise<boolean>)) | undefined;
12
+ };
13
+ /**
14
+ * A decision returned by `gate.check`.
15
+ */
16
+ export type GateDecision = {
17
+ /**
18
+ * - 'allow' when permitted.
19
+ */
20
+ outcome?: string | undefined;
21
+ /**
22
+ * - 'halt' for halt-severity denials.
23
+ */
24
+ severity?: string | undefined;
25
+ /**
26
+ * - The matched rule name.
27
+ */
28
+ rule?: string | undefined;
29
+ /**
30
+ * - Human-readable reason.
31
+ */
32
+ reason?: string | undefined;
33
+ /**
34
+ * - Arbitrary structured context.
35
+ */
36
+ context?: Record<string, any> | undefined;
37
+ };
38
+ /**
39
+ * Wire a bareguard Gate into bareagent's Loop.
40
+ *
41
+ * Returns:
42
+ * - `policy` — async (toolName, args, ctx) closure for `new Loop({ policy })`.
43
+ * Allow → true; deny → tagged reason string; halt → throws HaltError.
44
+ * - `onLlmResult` — callback for `new Loop({ onLlmResult })`. Forwards every
45
+ * provider.generate result to gate.record as a `{type:'llm'}` action
46
+ * so `budget.maxCostUsd` covers token-only workloads.
47
+ * - `onToolResult` — callback for `new Loop({ onToolResult })`. Forwards every
48
+ * tool.execute result to gate.record with ctx in scope.
49
+ * - `filterTools` — async (tools) => filtered. Drops tools denied by gate.allows
50
+ * so the LLM never sees them. No audit, no record. Bulk-only:
51
+ * when MCP tools are exposed via `mcp_discover`+`mcp_invoke`
52
+ * meta-tools, filterTools cannot drop the inner names (they
53
+ * are not in the tool list). Gate those via bareguard's
54
+ * `tools.denyArgPatterns: { mcp_invoke: [/"name":"…"/] }`
55
+ * — see src/mcp-bridge.js (Gov shape).
56
+ * - `wrapTool` / `wrapTools` — DEPRECATED. Pre-BA1 shim that wraps execute() to
57
+ * call gate.record post-hoc. Loses _ctx and never sees LLM cost.
58
+ * Prefer `onToolResult` (and `onLlmResult` for budget correctness).
59
+ *
60
+ * Halt-severity decisions (budget exhausted, limits.maxTurns hit, gate terminated)
61
+ * throw HaltError from the policy closure; Loop catches it and exits cleanly with
62
+ * loop:error{source:'halt'} + loop:done — the deny is NOT fed back to the LLM.
63
+ *
64
+ * @param {Gate} gate - A bareguard Gate instance (must have .check, .record, .allows).
65
+ * @param {object} [options]
66
+ * @param {Function} [options.formatDeny] - (decision, toolName) => string. Transforms
67
+ * the deny string fed to the LLM. The second arg is the bareagent tool name (handy
68
+ * for tool-specific deny copy). Default: "[deny: <rule>] <reason>" or
69
+ * "[deny: <rule>] <toolName> denied" when bareguard omits a reason. Halt bypasses
70
+ * this (HaltError doesn't reach the LLM).
71
+ * @param {Function} [options.actionTranslator] - (toolName, args, ctx) => action.
72
+ * Builds the action object passed to `gate.check` and `gate.record`. Default:
73
+ * `{ type: toolName, args, _ctx: ctx }`. Override when bareguard's primitives
74
+ * need a specific shape — e.g. `bashCheck` requires `{type:'bash', cmd:...}`,
75
+ * `fsCheck` requires `{type:'read'|'write'|'edit', path:...}`. The default shape
76
+ * matches `tools.denylist` / `tools.allowlist` (which read `action.type`) but
77
+ * does NOT activate `bash`/`fs`/`net` primitives — those need their own
78
+ * `action.type` value. Adopters using those primitives must translate.
79
+ * @returns {{policy: Function, onLlmResult: Function, onToolResult: Function, filterTools: Function, wrapTool: Function, wrapTools: Function}}
80
+ *
81
+ * @example
82
+ * const { Gate } = require('bareguard');
83
+ * const { Loop } = require('bare-agent');
84
+ * const { wireGate } = require('bare-agent/bareguard');
85
+ *
86
+ * const gate = new Gate({
87
+ * budget: { maxCostUsd: 0.50 },
88
+ * limits: { maxTurns: 20 },
89
+ * audit: { path: './audit.jsonl' },
90
+ * });
91
+ * await gate.init();
92
+ *
93
+ * const { policy, onLlmResult, onToolResult, filterTools } = wireGate(gate);
94
+ * const loop = new Loop({ provider, policy, onLlmResult, onToolResult });
95
+ * const tools = await filterTools(myTools);
96
+ * await loop.run(messages, tools);
97
+ */
98
+ export function wireGate(gate: Gate, options?: {
99
+ formatDeny?: Function | undefined;
100
+ actionTranslator?: Function | undefined;
101
+ }): {
102
+ policy: Function;
103
+ onLlmResult: Function;
104
+ onToolResult: Function;
105
+ filterTools: Function;
106
+ wrapTool: Function;
107
+ wrapTools: Function;
108
+ };
109
+ /**
110
+ * @param {string} toolName
111
+ * @param {any} args
112
+ * @param {Ctx} ctx
113
+ */
114
+ export function defaultActionTranslator(toolName: string, args: any, ctx: Ctx): {
115
+ type: string;
116
+ args: any;
117
+ _ctx: any;
118
+ };
@@ -2,10 +2,34 @@
2
2
 
3
3
  const { HaltError } = require('./errors');
4
4
 
5
+ /** @typedef {import('../types').Ctx} Ctx */
6
+ /** @typedef {import('../types').ToolDef} ToolDef */
7
+ /** @typedef {import('../types').Usage} Usage */
8
+
9
+ /**
10
+ * A bareguard Gate instance. Comes from the ambient `bareguard` module, so its
11
+ * methods are accessed structurally here.
12
+ * @typedef {object} Gate
13
+ * @property {(action: any) => (GateDecision | Promise<GateDecision>)} check
14
+ * @property {(action: any, outcome?: any) => any} record
15
+ * @property {(toolName: string) => (boolean | Promise<boolean>)} [allows]
16
+ */
17
+
18
+ /**
19
+ * A decision returned by `gate.check`.
20
+ * @typedef {object} GateDecision
21
+ * @property {string} [outcome] - 'allow' when permitted.
22
+ * @property {string} [severity] - 'halt' for halt-severity denials.
23
+ * @property {string} [rule] - The matched rule name.
24
+ * @property {string} [reason] - Human-readable reason.
25
+ * @property {Record<string, any>} [context] - Arbitrary structured context.
26
+ */
27
+
5
28
  // Safe-stringify for tool results: tools can return circular structures or
6
29
  // values that include functions / undefined / bigints. Falling back to String()
7
30
  // keeps gate.record from throwing inside onToolResult (which would surface as a
8
31
  // loop:error{source:'onToolResult'} for what is really a serialization quirk).
32
+ /** @param {any} value */
9
33
  function safeStringify(value) {
10
34
  if (typeof value === 'string') return value;
11
35
  try {
@@ -46,7 +70,7 @@ let warnedWrap = false;
46
70
  * throw HaltError from the policy closure; Loop catches it and exits cleanly with
47
71
  * loop:error{source:'halt'} + loop:done — the deny is NOT fed back to the LLM.
48
72
  *
49
- * @param {object} gate - A bareguard Gate instance (must have .check, .record, .allows).
73
+ * @param {Gate} gate - A bareguard Gate instance (must have .check, .record, .allows).
50
74
  * @param {object} [options]
51
75
  * @param {Function} [options.formatDeny] - (decision, toolName) => string. Transforms
52
76
  * the deny string fed to the LLM. The second arg is the bareagent tool name (handy
@@ -93,6 +117,11 @@ function wireGate(gate, options = {}) {
93
117
  const formatDeny = options.formatDeny || defaultFormatDeny;
94
118
  const translate = options.actionTranslator || defaultActionTranslator;
95
119
 
120
+ /**
121
+ * @param {string} toolName
122
+ * @param {any} args
123
+ * @param {Ctx} ctx
124
+ */
96
125
  const policy = async (toolName, args, ctx) => {
97
126
  const decision = await gate.check(translate(toolName, args, ctx));
98
127
  if (decision.outcome === 'allow') return true;
@@ -105,6 +134,15 @@ function wireGate(gate, options = {}) {
105
134
  return formatDeny(decision, toolName);
106
135
  };
107
136
 
137
+ /**
138
+ * @param {object} arg
139
+ * @param {string|null} [arg.model]
140
+ * @param {string|null} [arg.provider]
141
+ * @param {Usage} [arg.usage]
142
+ * @param {number} [arg.costUsd]
143
+ * @param {number|null} [arg.durationMs]
144
+ * @param {Ctx} [arg.ctx]
145
+ */
108
146
  const onLlmResult = async ({ model, provider, usage, costUsd, durationMs, ctx }) => {
109
147
  // LLM rounds bypass actionTranslator — they always use the canonical
110
148
  // {type:'llm'} action so budget rules can match without translator collusion.
@@ -118,6 +156,15 @@ function wireGate(gate, options = {}) {
118
156
  );
119
157
  };
120
158
 
159
+ /**
160
+ * @param {object} arg
161
+ * @param {string} arg.name
162
+ * @param {any} [arg.args]
163
+ * @param {any} [arg.result]
164
+ * @param {Error|null} [arg.error]
165
+ * @param {number|null} [arg.durationMs]
166
+ * @param {Ctx} [arg.ctx]
167
+ */
121
168
  const onToolResult = async ({ name, args, result, error, durationMs, ctx }) => {
122
169
  const action = translate(name, args, ctx);
123
170
  if (error) {
@@ -133,6 +180,10 @@ function wireGate(gate, options = {}) {
133
180
  }
134
181
  };
135
182
 
183
+ /**
184
+ * @param {ToolDef[]} tools
185
+ * @returns {Promise<ToolDef[]>}
186
+ */
136
187
  const filterTools = async (tools) => {
137
188
  if (!Array.isArray(tools)) {
138
189
  throw new Error('[wireGate.filterTools] expects an array of tools');
@@ -140,13 +191,20 @@ function wireGate(gate, options = {}) {
140
191
  if (typeof gate.allows !== 'function') {
141
192
  throw new Error('[wireGate.filterTools] gate must have .allows (bareguard >= 0.2)');
142
193
  }
194
+ // Bind to the Gate so `this` stays correct (bareguard's allows reads
195
+ // this._initialized) — extracting the method unbound would crash.
196
+ const allows = gate.allows.bind(gate);
143
197
  // Parallel: gate.allows is config-driven and pure, so concurrent calls are
144
198
  // safe. Matters for large MCP catalogs (50+ tools) where sequential awaits
145
199
  // were noticeable overhead on every startup.
146
- const verdicts = await Promise.all(tools.map(t => gate.allows(t.name)));
200
+ const verdicts = await Promise.all(tools.map(t => allows(t.name)));
147
201
  return tools.filter((_, i) => verdicts[i]);
148
202
  };
149
203
 
204
+ /**
205
+ * @param {ToolDef} tool
206
+ * @returns {ToolDef}
207
+ */
150
208
  function wrapTool(tool) {
151
209
  if (!warnedWrap) {
152
210
  warnedWrap = true;
@@ -161,7 +219,7 @@ function wireGate(gate, options = {}) {
161
219
  const original = tool.execute;
162
220
  return {
163
221
  ...tool,
164
- execute: async (args) => {
222
+ execute: async (/** @type {any} */ args) => {
165
223
  const action = { type: tool.name, args };
166
224
  const startedAt = Date.now();
167
225
  try {
@@ -182,6 +240,10 @@ function wireGate(gate, options = {}) {
182
240
  };
183
241
  }
184
242
 
243
+ /**
244
+ * @param {ToolDef[]} tools
245
+ * @returns {ToolDef[]}
246
+ */
185
247
  function wrapTools(tools) {
186
248
  if (!Array.isArray(tools)) {
187
249
  throw new Error('[wireGate.wrapTools] expects an array of tools');
@@ -192,6 +254,11 @@ function wireGate(gate, options = {}) {
192
254
  return { policy, onLlmResult, onToolResult, filterTools, wrapTool, wrapTools };
193
255
  }
194
256
 
257
+ /**
258
+ * @param {GateDecision} decision
259
+ * @param {string} toolName
260
+ * @returns {string}
261
+ */
195
262
  function defaultFormatDeny(decision, toolName) {
196
263
  const tag = `[deny: ${decision.rule}]`;
197
264
  return decision.reason ? `${tag} ${decision.reason}` : `${tag} ${toolName} denied`;
@@ -202,6 +269,11 @@ function defaultFormatDeny(decision, toolName) {
202
269
  // does NOT activate `bash`/`fs`/`net` primitives — those require `action.type`
203
270
  // to be `bash`/`read`/`write`/etc. and read fields like `action.cmd` /
204
271
  // `action.path` at the top level. Override via `wireGate(gate, { actionTranslator })`.
272
+ /**
273
+ * @param {string} toolName
274
+ * @param {any} args
275
+ * @param {Ctx} ctx
276
+ */
205
277
  function defaultActionTranslator(toolName, args, ctx) {
206
278
  return { type: toolName, args, _ctx: ctx ?? null };
207
279
  }
@@ -0,0 +1,61 @@
1
+ export type CheckpointOptions = {
2
+ /**
3
+ * - Tool names that require approval (exact match).
4
+ */
5
+ tools?: string[] | undefined;
6
+ /**
7
+ * - Custom predicate — overrides tools list if set.
8
+ */
9
+ shouldAsk?: ((toolName: string, args: any) => boolean) | undefined;
10
+ /**
11
+ * - Async `(question, context) => void` to deliver the question.
12
+ */
13
+ send?: ((question: string, context: any) => any) | undefined;
14
+ /**
15
+ * - Async `(context) => string` that resolves with the user's reply.
16
+ */
17
+ waitForReply?: ((context: any) => any) | undefined;
18
+ /**
19
+ * - Ms to wait before auto-denying. 0 disables.
20
+ */
21
+ timeout?: number | undefined;
22
+ };
23
+ /**
24
+ * @typedef {object} CheckpointOptions
25
+ * @property {Array<string>} [tools] - Tool names that require approval (exact match).
26
+ * @property {(toolName: string, args: any) => boolean} [shouldAsk] - Custom predicate — overrides tools list if set.
27
+ * @property {(question: string, context: any) => any} [send] - Async `(question, context) => void` to deliver the question.
28
+ * @property {(context: any) => any} [waitForReply] - Async `(context) => string` that resolves with the user's reply.
29
+ * @property {number} [timeout=300000] - Ms to wait before auto-denying. 0 disables.
30
+ */
31
+ export class Checkpoint {
32
+ /**
33
+ * @param {CheckpointOptions} [options={}]
34
+ */
35
+ constructor(options?: CheckpointOptions);
36
+ tools: Set<string>;
37
+ send: ((question: string, context: any) => any) | null;
38
+ waitForReply: ((context: any) => any) | null;
39
+ shouldAskFn: ((toolName: string, args: any) => boolean) | null;
40
+ timeout: number;
41
+ /**
42
+ * @param {string} toolName - Name of the tool being invoked.
43
+ * @param {any} args - Arguments passed to the tool.
44
+ * @returns {boolean} Whether approval should be requested.
45
+ */
46
+ shouldAsk(toolName: string, args: any): boolean;
47
+ /**
48
+ * Send a question and wait for a reply. Rejects with TimeoutError if `timeout` ms elapse
49
+ * without a reply — the Loop catches this, auto-denies the tool call, and routes the
50
+ * error through loop:error + onError. No silent hangs.
51
+ * @param {string} question - The approval question to send.
52
+ * @param {{tool?: string, [key: string]: any}} [context={}] - Context passed to send and waitForReply.
53
+ * @returns {Promise<string|null>} The user's reply, or null.
54
+ * @throws {Error} `[Checkpoint] send and waitForReply callbacks required` — when callbacks are missing.
55
+ * @throws {TimeoutError} When no reply arrives within `timeout` ms.
56
+ */
57
+ ask(question: string, context?: {
58
+ tool?: string;
59
+ [key: string]: any;
60
+ }): Promise<string | null>;
61
+ }
package/src/checkpoint.js CHANGED
@@ -4,16 +4,20 @@ const { TimeoutError } = require('./errors');
4
4
 
5
5
  const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
6
6
 
7
+ /**
8
+ * @typedef {object} CheckpointOptions
9
+ * @property {Array<string>} [tools] - Tool names that require approval (exact match).
10
+ * @property {(toolName: string, args: any) => boolean} [shouldAsk] - Custom predicate — overrides tools list if set.
11
+ * @property {(question: string, context: any) => any} [send] - Async `(question, context) => void` to deliver the question.
12
+ * @property {(context: any) => any} [waitForReply] - Async `(context) => string` that resolves with the user's reply.
13
+ * @property {number} [timeout=300000] - Ms to wait before auto-denying. 0 disables.
14
+ */
15
+
7
16
  class Checkpoint {
8
17
  /**
9
- * @param {object} options
10
- * @param {Array<string>} [options.tools] - Tool names that require approval (exact match).
11
- * @param {Function} [options.shouldAsk] - Custom predicate `(toolName, args) => bool` — overrides tools list if set.
12
- * @param {Function} options.send - Async `(question, context) => void` to deliver the question.
13
- * @param {Function} options.waitForReply - Async `(context) => string` that resolves with the user's reply.
14
- * @param {number} [options.timeout=300000] - Ms to wait before auto-denying. 0 disables.
18
+ * @param {CheckpointOptions} [options={}]
15
19
  */
16
- constructor(options = {}) {
20
+ constructor(options = /** @type {CheckpointOptions} */ ({})) {
17
21
  this.tools = new Set(options.tools || []);
18
22
  this.send = options.send || null;
19
23
  this.waitForReply = options.waitForReply || null;
@@ -21,6 +25,11 @@ class Checkpoint {
21
25
  this.timeout = options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS;
22
26
  }
23
27
 
28
+ /**
29
+ * @param {string} toolName - Name of the tool being invoked.
30
+ * @param {any} args - Arguments passed to the tool.
31
+ * @returns {boolean} Whether approval should be requested.
32
+ */
24
33
  shouldAsk(toolName, args) {
25
34
  if (this.shouldAskFn) return this.shouldAskFn(toolName, args);
26
35
  return this.tools.has(toolName);
@@ -31,7 +40,7 @@ class Checkpoint {
31
40
  * without a reply — the Loop catches this, auto-denies the tool call, and routes the
32
41
  * error through loop:error + onError. No silent hangs.
33
42
  * @param {string} question - The approval question to send.
34
- * @param {object} [context={}] - Context passed to send and waitForReply.
43
+ * @param {{tool?: string, [key: string]: any}} [context={}] - Context passed to send and waitForReply.
35
44
  * @returns {Promise<string|null>} The user's reply, or null.
36
45
  * @throws {Error} `[Checkpoint] send and waitForReply callbacks required` — when callbacks are missing.
37
46
  * @throws {TimeoutError} When no reply arrives within `timeout` ms.