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
package/src/mcp-bridge.js CHANGED
@@ -6,6 +6,60 @@ const { join } = require('node:path');
6
6
  const { homedir } = require('node:os');
7
7
  const { ToolError } = require('./errors');
8
8
 
9
+ /** @typedef {import('../types').ToolDef} ToolDef */
10
+
11
+ /**
12
+ * A server definition as found in an IDE/MCP config file.
13
+ * @typedef {object} ServerDef
14
+ * @property {string} command
15
+ * @property {string[]} [args]
16
+ * @property {Record<string, string>} [env]
17
+ * @property {string} [cwd]
18
+ */
19
+
20
+ /**
21
+ * Raw tool descriptor as returned by an MCP server's tools/list.
22
+ * @typedef {object} McpTool
23
+ * @property {string} name
24
+ * @property {string} [description]
25
+ * @property {Record<string, any>} [inputSchema]
26
+ */
27
+
28
+ /**
29
+ * Per-server entry persisted in .mcp-bridge.json.
30
+ * @typedef {object} BridgeServerEntry
31
+ * @property {string} command
32
+ * @property {string[]} args
33
+ * @property {Record<string, string>} [env]
34
+ * @property {string} [cwd]
35
+ * @property {Record<string, string>} tools - tool name -> "allow" | "deny"
36
+ */
37
+
38
+ /**
39
+ * Persisted bridge config (.mcp-bridge.json).
40
+ * @typedef {object} BridgeConfig
41
+ * @property {string} discovered - ISO timestamp
42
+ * @property {string} ttl
43
+ * @property {Record<string, BridgeServerEntry>} servers
44
+ */
45
+
46
+ /**
47
+ * A denied-tool descriptor surfaced to the LLM.
48
+ * @typedef {object} DeniedTool
49
+ * @property {string} server
50
+ * @property {string} tool
51
+ * @property {string} description
52
+ */
53
+
54
+ /**
55
+ * JSON-RPC stdio client over a spawned MCP server.
56
+ * @typedef {object} RpcClient
57
+ * @property {(method: string, params?: object) => Promise<any>} rpc
58
+ * @property {(method: string, params?: object) => void} notify
59
+ * @property {import('node:child_process').ChildProcessWithoutNullStreams} child
60
+ * @property {string} stderr
61
+ */
62
+
9
63
  // --- Config discovery (from IDE configs) ---
10
64
 
11
65
  const DEFAULT_CONFIG_PATHS = [
@@ -16,8 +70,13 @@ const DEFAULT_CONFIG_PATHS = [
16
70
  () => join(homedir(), '.cursor', 'mcp.json'), // Cursor
17
71
  ];
18
72
 
73
+ /**
74
+ * @param {string[]} [configPaths]
75
+ * @returns {Map<string, ServerDef>}
76
+ */
19
77
  function discoverServers(configPaths) {
20
78
  const paths = configPaths || DEFAULT_CONFIG_PATHS.map(fn => fn());
79
+ /** @type {Map<string, ServerDef>} */
21
80
  const servers = new Map();
22
81
 
23
82
  for (const p of paths) {
@@ -40,14 +99,21 @@ function discoverServers(configPaths) {
40
99
  const DEFAULT_BRIDGE_PATH = () => join(process.cwd(), '.mcp-bridge.json');
41
100
  const DEFAULT_TTL = '24h';
42
101
 
102
+ /** @param {string} [ttl] */
43
103
  function parseTTL(ttl) {
44
104
  const match = (ttl || DEFAULT_TTL).match(/^(\d+)(s|m|h|d)$/);
45
105
  if (!match) return 24 * 60 * 60 * 1000;
46
106
  const n = parseInt(match[1]);
47
- const unit = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[match[2]];
107
+ /** @type {Record<string, number>} */
108
+ const units = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
109
+ const unit = units[match[2]];
48
110
  return n * unit;
49
111
  }
50
112
 
113
+ /**
114
+ * @param {string} filePath
115
+ * @returns {BridgeConfig|null}
116
+ */
51
117
  function readBridgeConfig(filePath) {
52
118
  try {
53
119
  return JSON.parse(readFileSync(filePath, 'utf8'));
@@ -56,10 +122,15 @@ function readBridgeConfig(filePath) {
56
122
  }
57
123
  }
58
124
 
125
+ /**
126
+ * @param {string} filePath
127
+ * @param {BridgeConfig} config
128
+ */
59
129
  function writeBridgeConfig(filePath, config) {
60
130
  writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n');
61
131
  }
62
132
 
133
+ /** @param {BridgeConfig|null} config */
63
134
  function isExpired(config) {
64
135
  if (!config || !config.discovered) return true;
65
136
  const ttlMs = parseTTL(config.ttl);
@@ -73,8 +144,13 @@ function isExpired(config) {
73
144
  * - New tools on existing server: added as "allow"
74
145
  * - Removed tools on existing server: removed from config
75
146
  * - Existing tools: user's allow/deny preserved
147
+ * @param {BridgeConfig|null} existing
148
+ * @param {Map<string, ServerDef>} discovered
149
+ * @param {Map<string, McpTool[]>} freshTools
150
+ * @returns {BridgeConfig}
76
151
  */
77
152
  function mergeBridgeConfig(existing, discovered, freshTools) {
153
+ /** @type {BridgeConfig} */
78
154
  const merged = {
79
155
  discovered: new Date().toISOString(),
80
156
  ttl: existing?.ttl || DEFAULT_TTL,
@@ -86,6 +162,7 @@ function mergeBridgeConfig(existing, discovered, freshTools) {
86
162
  const existingServer = existing?.servers?.[name];
87
163
  const existingTools = existingServer?.tools || {};
88
164
 
165
+ /** @type {Record<string, string>} */
89
166
  const tools = {};
90
167
  for (const t of serverTools) {
91
168
  tools[t.name] = existingTools[t.name] || 'allow';
@@ -105,8 +182,13 @@ function mergeBridgeConfig(existing, discovered, freshTools) {
105
182
 
106
183
  // --- Env resolution ---
107
184
 
185
+ /**
186
+ * @param {Record<string, string>} [env]
187
+ * @returns {Record<string, string>}
188
+ */
108
189
  function resolveEnv(env) {
109
190
  if (!env) return {};
191
+ /** @type {Record<string, string>} */
110
192
  const resolved = {};
111
193
  for (const [k, v] of Object.entries(env)) {
112
194
  resolved[k] = typeof v === 'string'
@@ -118,6 +200,11 @@ function resolveEnv(env) {
118
200
 
119
201
  // --- JSON-RPC stdio client ---
120
202
 
203
+ /**
204
+ * @param {string} name
205
+ * @param {ServerDef} def
206
+ * @returns {RpcClient}
207
+ */
121
208
  function createRpcClient(name, def) {
122
209
  const { command, args = [], env, cwd } = def;
123
210
  const mergedEnv = { ...process.env, ...resolveEnv(env) };
@@ -128,6 +215,7 @@ function createRpcClient(name, def) {
128
215
  ...(cwd && { cwd }),
129
216
  });
130
217
 
218
+ /** @type {Map<number, {resolve: (v: any) => void, reject: (e: any) => void}>} */
131
219
  const pending = new Map();
132
220
  let nextId = 1;
133
221
  let buffer = '';
@@ -167,6 +255,11 @@ function createRpcClient(name, def) {
167
255
  pending.clear();
168
256
  });
169
257
 
258
+ /**
259
+ * @param {string} method
260
+ * @param {object} [params]
261
+ * @returns {Promise<any>}
262
+ */
170
263
  function rpc(method, params = {}) {
171
264
  const id = nextId++;
172
265
  return new Promise((resolve, reject) => {
@@ -176,6 +269,10 @@ function createRpcClient(name, def) {
176
269
  });
177
270
  }
178
271
 
272
+ /**
273
+ * @param {string} method
274
+ * @param {object} [params]
275
+ */
179
276
  function notify(method, params = {}) {
180
277
  const msg = JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n';
181
278
  child.stdin.write(msg);
@@ -186,6 +283,10 @@ function createRpcClient(name, def) {
186
283
 
187
284
  // --- Content unwrapping ---
188
285
 
286
+ /**
287
+ * @param {Array<{type?: string, text?: string}>|any} content
288
+ * @returns {string|any}
289
+ */
189
290
  function unwrapContent(content) {
190
291
  if (!Array.isArray(content) || content.length === 0) return '';
191
292
  if (content.length === 1 && content[0].type === 'text') return content[0].text;
@@ -197,6 +298,12 @@ function unwrapContent(content) {
197
298
  // Runtime arg-dependent policy has moved to Loop-level (new Loop({ policy })).
198
299
  // mcp-bridge retains only the static .mcp-bridge.json allow/deny filter below —
199
300
  // that decides which tools are exposed to the Loop in the first place.
301
+ /**
302
+ * @param {string} serverName
303
+ * @param {McpTool[]} mcpTools
304
+ * @param {(method: string, params?: object) => Promise<any>} rpc
305
+ * @returns {ToolDef[]}
306
+ */
200
307
  function wrapTools(serverName, mcpTools, rpc) {
201
308
  return mcpTools.map(t => ({
202
309
  name: `${serverName}_${t.name}`,
@@ -216,6 +323,7 @@ function wrapTools(serverName, mcpTools, rpc) {
216
323
 
217
324
  // --- Server lifecycle ---
218
325
 
326
+ /** @param {import('node:child_process').ChildProcess} child */
219
327
  async function killServer(child) {
220
328
  if (child.exitCode !== null) return;
221
329
 
@@ -228,7 +336,9 @@ async function killServer(child) {
228
336
  // Short grace, then SIGTERM, then SIGKILL. Each wait clears its timer
229
337
  // promptly when the child closes so we don't block the event loop after
230
338
  // exit (which kept node:test's file-level wrapper hanging).
339
+ /** @param {number} ms @returns {Promise<void>} */
231
340
  const waitClose = (ms) => new Promise(resolve => {
341
+ /** @type {NodeJS.Timeout} */
232
342
  let timer;
233
343
  const onClose = () => { clearTimeout(timer); resolve(); };
234
344
  child.once('close', onClose);
@@ -254,34 +364,57 @@ async function killServer(child) {
254
364
 
255
365
  // --- Connect + list tools from a server ---
256
366
 
367
+ /**
368
+ * @param {string} name
369
+ * @param {ServerDef} def
370
+ * @param {number} [timeout]
371
+ * @returns {Promise<{mcpTools: McpTool[], client: RpcClient}>}
372
+ */
257
373
  async function connectAndListTools(name, def, timeout = 15000) {
258
374
  const client = createRpcClient(name, def);
259
375
 
260
- const init = client.rpc('initialize', {
261
- protocolVersion: '2024-11-05',
262
- capabilities: {},
263
- clientInfo: { name: 'bare-agent', version: '0.5.0' },
264
- });
376
+ try {
377
+ const init = client.rpc('initialize', {
378
+ protocolVersion: '2024-11-05',
379
+ capabilities: {},
380
+ clientInfo: { name: 'bare-agent', version: '0.5.0' },
381
+ });
265
382
 
266
- let timerId;
267
- const timer = new Promise((_, reject) => {
268
- timerId = setTimeout(() => reject(new ToolError(`MCP server "${name}" init timed out after ${timeout}ms`)), timeout);
269
- });
383
+ let timerId;
384
+ const timer = new Promise((_, reject) => {
385
+ timerId = setTimeout(() => reject(new ToolError(`MCP server "${name}" init timed out after ${timeout}ms`)), timeout);
386
+ });
270
387
 
271
- try { await Promise.race([init, timer]); } finally { clearTimeout(timerId); }
272
- client.notify('notifications/initialized');
388
+ try { await Promise.race([init, timer]); } finally { clearTimeout(timerId); }
389
+ client.notify('notifications/initialized');
273
390
 
274
- const { tools: mcpTools } = await client.rpc('tools/list');
391
+ const { tools: mcpTools } = await client.rpc('tools/list');
275
392
 
276
- return { mcpTools, client };
393
+ return { mcpTools, client };
394
+ } catch (err) {
395
+ // Connect failed (init timeout, tools/list error, etc.) — the child was
396
+ // spawned but never returned to the caller, so the caller can't track it
397
+ // for close(). Kill it here, or its open stdin pipe keeps the event loop
398
+ // alive and hangs the process on exit (notably node:test's file wrapper).
399
+ await killServer(client.child);
400
+ throw err;
401
+ }
277
402
  }
278
403
 
279
404
  // --- System context for LLM ---
280
405
 
406
+ /**
407
+ * @param {string[]} servers
408
+ * @param {ToolDef[]} tools
409
+ * @param {DeniedTool[]} denied
410
+ * @returns {string}
411
+ */
281
412
  function buildSystemContext(servers, tools, denied) {
413
+ /** @type {string[]} */
282
414
  const lines = [];
283
415
  lines.push(`MCP Bridge: ${tools.length} tools available from ${servers.length} server(s): ${servers.join(', ')}.`);
284
416
 
417
+ /** @type {Record<string, string[]>} */
285
418
  const byServer = {};
286
419
  for (const t of tools) {
287
420
  const sep = t.name.indexOf('_');
@@ -328,9 +461,9 @@ function buildSystemContext(servers, tools, denied) {
328
461
  * tool name doesn't travel as `action.type` — that's a deliberate v0.9
329
462
  * trade for one consistent gate-check call per LLM tool invocation.
330
463
  *
331
- * @param {Array} tools - The bulk-loaded, name-prefixed tools array.
332
- * @param {string} discoveredAt - ISO timestamp from .mcp-bridge.json.
333
- * @returns {Array} [mcp_discover, mcp_invoke]
464
+ * @param {ToolDef[]} tools - The bulk-loaded, name-prefixed tools array.
465
+ * @param {string} [discoveredAt] - ISO timestamp from .mcp-bridge.json.
466
+ * @returns {ToolDef[]} [mcp_discover, mcp_invoke]
334
467
  */
335
468
  function buildMetaTools(tools, discoveredAt) {
336
469
  // Catalog descriptors: same info the LLM would see for bulk-loaded tools,
@@ -364,7 +497,7 @@ function buildMetaTools(tools, discoveredAt) {
364
497
  },
365
498
  },
366
499
  },
367
- execute: async ({ server } = {}) => {
500
+ execute: async (/** @type {{ server?: string }} */ { server } = {}) => {
368
501
  const filtered = server
369
502
  ? catalog.filter(t => t.server === server)
370
503
  : catalog;
@@ -394,7 +527,7 @@ function buildMetaTools(tools, discoveredAt) {
394
527
  },
395
528
  required: ['name'],
396
529
  },
397
- execute: async ({ name, args }) => {
530
+ execute: async (/** @type {{ name: string, args?: object }} */ { name, args }) => {
398
531
  const tool = byName.get(name);
399
532
  if (!tool) {
400
533
  throw new ToolError(`mcp_invoke: unknown tool "${name}". Call mcp_discover for the current catalog.`, {
@@ -431,7 +564,14 @@ function buildMetaTools(tools, discoveredAt) {
431
564
  * @param {string[]} [opts.servers] - Limit to these server names.
432
565
  * @param {number} [opts.timeout=15000] - Per-server init timeout in ms.
433
566
  * @param {boolean} [opts.refresh=false] - Force re-discovery regardless of TTL.
434
- * @returns {Promise<{tools: Array, metaTools: Array, servers: string[], systemContext: string, denied: Array, close: Function}>}
567
+ * @param {(name: string, def: ServerDef) => boolean | Promise<boolean>} [opts.confirmServer]
568
+ * Vet each discovered server BEFORE its `command` is spawned. Connecting to an
569
+ * MCP server runs its command, and discovery reads configs from the cwd (a
570
+ * `.mcp.json` in an untrusted repo) as well as the user's home/IDE configs.
571
+ * Return false to skip a server (its command is never executed). A throw is
572
+ * treated as a deny (fail-closed). Default: every discovered server is trusted
573
+ * (unchanged behavior) — pass this to gate command execution.
574
+ * @returns {Promise<{tools: ToolDef[], metaTools?: ToolDef[], servers: string[], systemContext: string, denied: DeniedTool[], errors?: Array<{server: string, error: string}>, close: Function}>}
435
575
  */
436
576
  async function createMCPBridge(opts = {}) {
437
577
  if ('policy' in opts) {
@@ -444,6 +584,16 @@ async function createMCPBridge(opts = {}) {
444
584
  const bridgePath = opts.bridgePath || DEFAULT_BRIDGE_PATH();
445
585
  const timeout = opts.timeout || 15000;
446
586
 
587
+ // Vet a server before spawning its command. Fail-closed: an undefined hook
588
+ // trusts all (unchanged behavior); a throw denies.
589
+ const confirmServer = typeof opts.confirmServer === 'function' ? opts.confirmServer : null;
590
+ /** @param {string} name @param {ServerDef} def @returns {Promise<boolean>} */
591
+ const vetServer = async (name, def) => {
592
+ if (!confirmServer) return true;
593
+ try { return (await confirmServer(name, def)) === true; }
594
+ catch { return false; }
595
+ };
596
+
447
597
  let config = readBridgeConfig(bridgePath);
448
598
  const needsRefresh = opts.refresh || !config || isExpired(config);
449
599
 
@@ -459,16 +609,23 @@ async function createMCPBridge(opts = {}) {
459
609
  // If discovered.size === 0 but config exists, fall through and use the existing config
460
610
  // rather than wiping it on a transient discovery failure.
461
611
  if (discovered.size > 0) {
612
+ /** @type {Map<string, McpTool[]>} */
462
613
  const freshTools = new Map();
614
+ /** @type {Map<string, RpcClient>} */
463
615
  const connectResults = new Map();
616
+ /** @type {Array<{server: string, error: string}>} */
464
617
  const errors = [];
465
618
 
466
- const toDiscover = opts.servers
467
- ? [...discovered.entries()].filter(([n]) => opts.servers.includes(n))
619
+ const reqServers = opts.servers;
620
+ const toDiscover = reqServers
621
+ ? [...discovered.entries()].filter(([n]) => reqServers.includes(n))
468
622
  : [...discovered.entries()];
469
623
 
470
624
  await Promise.all(toDiscover.map(async ([name, def]) => {
471
625
  try {
626
+ // Denied by confirmServer: skip silently — this is the caller's own
627
+ // decision, not a connection failure, so it must not land in `errors`.
628
+ if (!(await vetServer(name, def))) return;
472
629
  const result = await connectAndListTools(name, def, timeout);
473
630
  freshTools.set(name, result.mcpTools);
474
631
  connectResults.set(name, result.client);
@@ -499,24 +656,38 @@ async function createMCPBridge(opts = {}) {
499
656
  }
500
657
  }
501
658
 
659
+ // At this point config is guaranteed non-null: it either existed, was just
660
+ // written by the refresh branch, or we returned early above.
661
+ if (!config) {
662
+ return { tools: [], servers: [], systemContext: '', denied: [], close: async () => {} };
663
+ }
664
+ /** @type {BridgeConfig} */
665
+ const cfg = config;
666
+
502
667
  // Filter to requested servers
503
- const serverNames = opts.servers
504
- ? opts.servers.filter(n => config.servers[n])
505
- : Object.keys(config.servers);
668
+ const reqServers2 = opts.servers;
669
+ const serverNames = reqServers2
670
+ ? reqServers2.filter(n => cfg.servers[n])
671
+ : Object.keys(cfg.servers);
506
672
 
507
673
  if (serverNames.length === 0) {
508
674
  return { tools: [], servers: [], systemContext: '', denied: [], close: async () => {} };
509
675
  }
510
676
 
511
677
  // Connect to servers and wrap only allowed tools
678
+ /** @type {ToolDef[]} */
512
679
  const tools = [];
680
+ /** @type {import('node:child_process').ChildProcess[]} */
513
681
  const children = [];
682
+ /** @type {string[]} */
514
683
  const connected = [];
684
+ /** @type {DeniedTool[]} */
515
685
  const denied = [];
686
+ /** @type {Array<{server: string, error: string}>} */
516
687
  const errors = [];
517
688
 
518
689
  await Promise.all(serverNames.map(async (name) => {
519
- const serverConf = config.servers[name];
690
+ const serverConf = cfg.servers[name];
520
691
  const allowedToolNames = Object.entries(serverConf.tools)
521
692
  .filter(([, perm]) => perm === 'allow')
522
693
  .map(([t]) => t);
@@ -525,6 +696,8 @@ async function createMCPBridge(opts = {}) {
525
696
  .map(([t]) => t);
526
697
 
527
698
  try {
699
+ // Denied by confirmServer: skip silently (the caller's decision, not a failure).
700
+ if (!(await vetServer(name, serverConf))) return;
528
701
  const { mcpTools, client } = await connectAndListTools(name, serverConf, timeout);
529
702
 
530
703
  // Only wrap tools that are allowed in config
package/src/mcp.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { createMCPBridge } from "./mcp-bridge";
2
+ import { discoverServers } from "./mcp-bridge";
3
+ import { buildMetaTools } from "./mcp-bridge";
4
+ export { createMCPBridge, discoverServers, buildMetaTools };
@@ -0,0 +1,50 @@
1
+ export type Store = import("../types").Store;
2
+ /**
3
+ * Persistence + search across turns and sessions.
4
+ * Thin wrapper that delegates to a swappable store.
5
+ *
6
+ * Interface:
7
+ * store(content, metadata) → id
8
+ * search(query, options) → [{ id, content, metadata, score }]
9
+ * get(id) → { content, metadata }
10
+ * delete(id) → void
11
+ *
12
+ * Stores (swappable):
13
+ * SQLite FTS5 — store-sqlite.js (peer dep: better-sqlite3)
14
+ * JSON file — store-jsonfile.js (zero deps)
15
+ * Bring your own: implement { store, search, get, delete }
16
+ */
17
+ /** @typedef {import('../types').Store} Store */
18
+ export class Memory {
19
+ /**
20
+ * @param {{ store?: Store }} [options] - Store backend (must implement store/search/get/delete).
21
+ * @throws {Error} `[Memory] requires options.store` — when options.store is missing.
22
+ */
23
+ constructor(options?: {
24
+ store?: Store;
25
+ });
26
+ /** @type {Store} */
27
+ _store: Store;
28
+ /**
29
+ * @param {any} content
30
+ * @param {Record<string, any>} [metadata]
31
+ * @returns {any} id
32
+ */
33
+ store(content: any, metadata?: Record<string, any>): any;
34
+ /**
35
+ * @param {string} query
36
+ * @param {Record<string, any>} [options]
37
+ * @returns {any}
38
+ */
39
+ search(query: string, options?: Record<string, any>): any;
40
+ /**
41
+ * @param {any} id
42
+ * @returns {any}
43
+ */
44
+ get(id: any): any;
45
+ /**
46
+ * @param {any} id
47
+ * @returns {any}
48
+ */
49
+ delete(id: any): any;
50
+ }
package/src/memory.js CHANGED
@@ -15,29 +15,49 @@
15
15
  * JSON file — store-jsonfile.js (zero deps)
16
16
  * Bring your own: implement { store, search, get, delete }
17
17
  */
18
+ /** @typedef {import('../types').Store} Store */
19
+
18
20
  class Memory {
19
21
  /**
20
- * @param {object} options
21
- * @param {object} options.store - Store backend (must implement store/search/get/delete).
22
+ * @param {{ store?: Store }} [options] - Store backend (must implement store/search/get/delete).
22
23
  * @throws {Error} `[Memory] requires options.store` — when options.store is missing.
23
24
  */
24
25
  constructor(options = {}) {
25
26
  if (!options.store) throw new Error('[Memory] requires options.store');
27
+ /** @type {Store} */
26
28
  this._store = options.store;
27
29
  }
28
30
 
31
+ /**
32
+ * @param {any} content
33
+ * @param {Record<string, any>} [metadata]
34
+ * @returns {any} id
35
+ */
29
36
  store(content, metadata = {}) {
30
37
  return this._store.store(content, metadata);
31
38
  }
32
39
 
40
+ /**
41
+ * @param {string} query
42
+ * @param {Record<string, any>} [options]
43
+ * @returns {any}
44
+ */
33
45
  search(query, options = {}) {
34
46
  return this._store.search(query, options);
35
47
  }
36
48
 
49
+ /**
50
+ * @param {any} id
51
+ * @returns {any}
52
+ */
37
53
  get(id) {
38
54
  return this._store.get(id);
39
55
  }
40
56
 
57
+ /**
58
+ * @param {any} id
59
+ * @returns {any}
60
+ */
41
61
  delete(id) {
42
62
  return this._store.delete(id);
43
63
  }
@@ -0,0 +1,62 @@
1
+ export type Provider = import("../types").Provider;
2
+ export type Step = {
3
+ /**
4
+ * - Unique step identifier.
5
+ */
6
+ id: string;
7
+ /**
8
+ * - Description of the step to execute.
9
+ */
10
+ action: string;
11
+ /**
12
+ * - Ids of steps that must complete first.
13
+ */
14
+ dependsOn: string[];
15
+ /**
16
+ * - Lifecycle status (e.g. 'pending').
17
+ */
18
+ status: string;
19
+ };
20
+ export type PlannerOptions = {
21
+ /**
22
+ * - LLM provider (must implement generate()).
23
+ */
24
+ provider: Provider;
25
+ /**
26
+ * - Custom planning prompt override.
27
+ */
28
+ prompt?: string | undefined;
29
+ /**
30
+ * - Cache time-to-live in ms. 0 disables caching.
31
+ */
32
+ cacheTTL?: number | undefined;
33
+ };
34
+ export class Planner {
35
+ /**
36
+ * @param {PlannerOptions} options
37
+ * @throws {Error} `[Planner] requires a provider` — when options.provider is missing.
38
+ */
39
+ constructor(options?: PlannerOptions);
40
+ provider: import("../types").Provider;
41
+ prompt: string;
42
+ _cacheTTL: number;
43
+ _cache: Map<any, any>;
44
+ /**
45
+ * Generate a step DAG from a goal.
46
+ * @param {string} goal - The user's goal to decompose.
47
+ * @param {{info?: string}} [context={}] - Optional context with info field.
48
+ * @returns {Promise<Step[]>}
49
+ * @throws {Error} `[Planner] could not parse plan` — when LLM output is not parseable JSON.
50
+ * @throws {Error} `[Planner] expected JSON array` — when parsed result is not an array.
51
+ * @throws {Error} `[Planner] step missing id or action` — when a step lacks required fields.
52
+ */
53
+ plan(goal: string, context?: {
54
+ info?: string;
55
+ }): Promise<Step[]>;
56
+ clearCache(): void;
57
+ /**
58
+ * @param {string} text - Raw LLM output to parse into steps.
59
+ * @returns {Step[]}
60
+ */
61
+ _parse(text: string): Step[];
62
+ }
package/src/planner.js CHANGED
@@ -1,5 +1,22 @@
1
1
  'use strict';
2
2
 
3
+ /** @typedef {import('../types').Provider} Provider */
4
+
5
+ /**
6
+ * @typedef {object} Step
7
+ * @property {string} id - Unique step identifier.
8
+ * @property {string} action - Description of the step to execute.
9
+ * @property {string[]} dependsOn - Ids of steps that must complete first.
10
+ * @property {string} status - Lifecycle status (e.g. 'pending').
11
+ */
12
+
13
+ /**
14
+ * @typedef {object} PlannerOptions
15
+ * @property {Provider} provider - LLM provider (must implement generate()).
16
+ * @property {string} [prompt] - Custom planning prompt override.
17
+ * @property {number} [cacheTTL] - Cache time-to-live in ms. 0 disables caching.
18
+ */
19
+
3
20
  const PLAN_PROMPT = `You are a planning agent. Break the user's goal into concrete steps.
4
21
 
5
22
  Rules:
@@ -16,12 +33,10 @@ Output format:
16
33
 
17
34
  class Planner {
18
35
  /**
19
- * @param {object} options
20
- * @param {object} options.provider - LLM provider (must implement generate()).
21
- * @param {string} [options.prompt] - Custom planning prompt override.
36
+ * @param {PlannerOptions} options
22
37
  * @throws {Error} `[Planner] requires a provider` — when options.provider is missing.
23
38
  */
24
- constructor(options = {}) {
39
+ constructor(options = /** @type {PlannerOptions} */ ({})) {
25
40
  if (!options.provider) throw new Error('[Planner] requires a provider');
26
41
  this.provider = options.provider;
27
42
  this.prompt = options.prompt || PLAN_PROMPT;
@@ -32,8 +47,8 @@ class Planner {
32
47
  /**
33
48
  * Generate a step DAG from a goal.
34
49
  * @param {string} goal - The user's goal to decompose.
35
- * @param {object} [context={}] - Optional context with info field.
36
- * @returns {Promise<Array<{id: string, action: string, dependsOn: string[], status: string}>>}
50
+ * @param {{info?: string}} [context={}] - Optional context with info field.
51
+ * @returns {Promise<Step[]>}
37
52
  * @throws {Error} `[Planner] could not parse plan` — when LLM output is not parseable JSON.
38
53
  * @throws {Error} `[Planner] expected JSON array` — when parsed result is not an array.
39
54
  * @throws {Error} `[Planner] step missing id or action` — when a step lacks required fields.
@@ -74,6 +89,10 @@ class Planner {
74
89
  this._cache.clear();
75
90
  }
76
91
 
92
+ /**
93
+ * @param {string} text - Raw LLM output to parse into steps.
94
+ * @returns {Step[]}
95
+ */
77
96
  _parse(text) {
78
97
  // Extract JSON array from response (handle markdown code blocks)
79
98
  const cleaned = text.replace(/^```(?:json)?\s*\n?/m, '').replace(/\n?```\s*$/m, '').trim();
@@ -93,7 +112,7 @@ class Planner {
93
112
  const ids = new Set(steps.map(s => s.id));
94
113
  return steps.map(s => {
95
114
  if (!s.id || !s.action) throw new Error(`[Planner] step missing id or action: ${JSON.stringify(s)}`);
96
- const deps = (s.dependsOn || []).filter(d => ids.has(d));
115
+ const deps = (s.dependsOn || []).filter(/** @param {string} d */ d => ids.has(d));
97
116
  return { id: s.id, action: s.action, dependsOn: deps, status: 'pending' };
98
117
  });
99
118
  }