bare-agent 0.12.1 → 0.13.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.
@@ -0,0 +1,225 @@
1
+ 'use strict';
2
+
3
+ // RT-1 — the msgs⇄units adapter for the `assemble` context-assembly seam (src/loop.js).
4
+ //
5
+ // Division of labour (litectx CE-PRD §8.2, line 321 — the frozen contract):
6
+ // - bareagent owns GRAMMAR: msgs→units, units→msgs, atomic bundling of a tool-call + its result(s),
7
+ // `pinned` flags, the pairing seatbelt, and fail-open. Broken tool-pairing and a dropped system
8
+ // prompt are UNREPRESENTABLE here, by construction — not by trusting the consumer.
9
+ // - litectx owns CONTENT + RELEVANCE: a single verb `assemble(units, ctx) → { units, dropped, tokens }`
10
+ // (the AssembleResult envelope — `dropped[]` is load-bearing, ships in the same slice, never silent)
11
+ // that does SELECT (keep / drop / reorder / recall-inject) + COMPRESS (rewrite a unit's `content`) +
12
+ // fit-to-`ctx.budget`, best-effort. It never learns the message grammar. `unitAssembler` unwraps `.units`.
13
+ //
14
+ // The neutral unit (the SOCKET litectx codes against — exactly these 7 enumerable fields):
15
+ // { id, role, content, kind, pinned, atomic, tokensApprox }
16
+ // id — stable within one assemble() call; litectx references units by it.
17
+ // role — 'system' | 'user' | 'assistant' | 'tool' (the message grammar role).
18
+ // content — flat string view litectx reads to score and MAY rewrite to COMPRESS.
19
+ // kind — litectx's memory-node kind ('code'|'doc'|'fact'|'episode') on units litectx
20
+ // INJECTS from recall. Transcript-derived units carry `null` — and that is the
21
+ // CORRECT value, not a placeholder. `role` and `kind` are orthogonal: `role`
22
+ // (user/assistant/tool/system) carries conversational position = bareagent's
23
+ // grammar, present on every unit; `kind` carries litectx's graph-node type, present
24
+ // only on units litectx owns. Typing a live turn would force litectx to learn the
25
+ // provider's conversation structure — the exact coupling the keystone boundary
26
+ // forbids (PRD §1.2 / litectx CE-PRD §8.2). RATIFIED by litectx (2026-06-12): do
27
+ // NOT define transcript-kinds. injected = kinded, transcript = null.
28
+ // pinned — true ⇒ bareagent guarantees the unit is never dropped and never reordered
29
+ // (system prompt, first user/task turn). litectx still sees its tokensApprox so the
30
+ // pin counts against the budget: pin-don't-hide.
31
+ // atomic — group-id (string) | null. litectx keeps/drops units sharing one group-id WHOLE,
32
+ // never split. bareagent pre-bundles an assistant tool-call + ALL its result(s) into
33
+ // one unit, so each bundle is its OWN group (group-id = the unit's id); everything
34
+ // else is null. NOTE: string|null, not boolean — a boolean collapses every bundle
35
+ // under one key and litectx would fit them all-or-nothing (test/context-units.test.js (real-litectx sweep)).
36
+ // tokensApprox — chars/4 estimate over the unit's backing messages, for litectx's budget math.
37
+ //
38
+ // `_msgs` (the verbatim backing messages) is attached NON-ENUMERABLE: it does not appear in the 7-field
39
+ // view, JSON, or iteration. It is bareagent's reconstruction backing — litectx must not read it. A unit
40
+ // with no backing (one litectx minted via recall-inject) is synthesised into a single message on the
41
+ // way out.
42
+
43
+ /** chars/4 token estimate over a list of messages (matches poc2 / the Loop's own heuristic). */
44
+ function approxTokens(msgs) {
45
+ let c = 0;
46
+ for (const m of msgs) {
47
+ if (m.content != null) c += String(m.content).length;
48
+ if (m.tool_calls) c += JSON.stringify(m.tool_calls).length;
49
+ }
50
+ return Math.ceil(c / 4);
51
+ }
52
+
53
+ /** Flat string view of a unit's backing messages — what litectx scores on and may rewrite. */
54
+ function renderContent(msgs) {
55
+ const parts = [];
56
+ for (const m of msgs) {
57
+ if (m.content != null && String(m.content).length) parts.push(String(m.content));
58
+ }
59
+ return parts.join('\n');
60
+ }
61
+
62
+ let _seq = 0;
63
+ /** Attach the verbatim backing + original-content fingerprint as non-enumerable carry. */
64
+ function withBacking(unit, msgs) {
65
+ Object.defineProperty(unit, '_msgs', { value: msgs, enumerable: false, writable: true });
66
+ Object.defineProperty(unit, '_origContent', { value: unit.content, enumerable: false, writable: true });
67
+ return unit;
68
+ }
69
+
70
+ /**
71
+ * msgs → neutral units. Bundles each assistant-tool-call message with the contiguous tool result(s)
72
+ * that answer its ids into ONE atomic unit (so pairing can never be split). system + first user turn
73
+ * are pinned.
74
+ * @param {Array<Record<string, any>>} msgs
75
+ * @returns {Array<Record<string, any>>}
76
+ */
77
+ function toUnits(msgs) {
78
+ const units = [];
79
+ let seenUser = false;
80
+ for (let i = 0; i < msgs.length; i++) {
81
+ const m = msgs[i];
82
+
83
+ if (m.role === 'system') {
84
+ units.push(withBacking({ id: `u${_seq++}`, role: 'system', content: renderContent([m]), kind: null, pinned: true, atomic: null, tokensApprox: approxTokens([m]) }, [m]));
85
+ continue;
86
+ }
87
+ if (m.role === 'user') {
88
+ const pinned = !seenUser; // first user turn = the task; pin it
89
+ seenUser = true;
90
+ units.push(withBacking({ id: `u${_seq++}`, role: 'user', content: renderContent([m]), kind: null, pinned, atomic: null, tokensApprox: approxTokens([m]) }, [m]));
91
+ continue;
92
+ }
93
+ if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) {
94
+ // bundle this assistant message with the contiguous tool results answering its ids
95
+ const ids = new Set(m.tool_calls.map((t) => t.id));
96
+ const group = [m];
97
+ let j = i + 1;
98
+ while (j < msgs.length && msgs[j].role === 'tool' && ids.has(msgs[j].tool_call_id)) {
99
+ group.push(msgs[j]);
100
+ j++;
101
+ }
102
+ // `atomic` is litectx's group-id (string|null), NOT a boolean: units sharing one are kept/dropped
103
+ // whole. bareagent already pre-bundles a tool-call + its result(s) into ONE unit, so each bundle is
104
+ // its OWN group — group-id = the unit's id. (A boolean would collapse every bundle under one key,
105
+ // making litectx fit them all-or-nothing — see test/context-units.test.js (real-litectx sweep).)
106
+ const gid = `u${_seq++}`;
107
+ units.push(withBacking({ id: gid, role: 'assistant', content: renderContent(group.slice(1)), kind: null, pinned: false, atomic: gid, tokensApprox: approxTokens(group) }, group));
108
+ i = j - 1;
109
+ continue;
110
+ }
111
+ // plain assistant text, or a stray tool message (handled by the seatbelt on the way out)
112
+ units.push(withBacking({ id: `u${_seq++}`, role: m.role, content: renderContent([m]), kind: null, pinned: false, atomic: null, tokensApprox: approxTokens([m]) }, [m]));
113
+ }
114
+ return units;
115
+ }
116
+
117
+ /**
118
+ * Drop any tool-result whose tool_call_id has no open assistant tool-call before it, and any assistant
119
+ * tool-call message left with zero surviving results. The final grammar guard: even if litectx hands
120
+ * back something that would orphan a pair, the wire is always valid. Returns a fresh array.
121
+ */
122
+ function pairingSeatbelt(msgs) {
123
+ // pass 1: which tool_call ids actually have a result present?
124
+ const resultIds = new Set();
125
+ for (const m of msgs) if (m.role === 'tool' && m.tool_call_id != null) resultIds.add(m.tool_call_id);
126
+
127
+ const out = [];
128
+ const open = new Set();
129
+ for (const m of msgs) {
130
+ if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) {
131
+ const surviving = m.tool_calls.filter((tc) => resultIds.has(tc.id));
132
+ if (!surviving.length) continue; // assistant tool-call with no results at all → drop the message
133
+ for (const tc of surviving) open.add(tc.id);
134
+ out.push(surviving.length === m.tool_calls.length ? m : { ...m, tool_calls: surviving });
135
+ continue;
136
+ }
137
+ if (m.role === 'tool') {
138
+ if (!open.has(m.tool_call_id)) continue; // orphan result → drop
139
+ open.delete(m.tool_call_id);
140
+ out.push(m);
141
+ continue;
142
+ }
143
+ out.push(m);
144
+ }
145
+ return out;
146
+ }
147
+
148
+ /**
149
+ * units → msgs. Honors drop (absent units), reorder (order of the returned array), recall-inject
150
+ * (units with no backing → one synthesised message), and COMPRESS (a unit whose `content` was rewritten
151
+ * is reconstructed from the new content). Atomic units keep their assistant tool-call message verbatim
152
+ * so pairing holds; a content rewrite lands on the tool RESULT. A multi-result atomic bundle whose
153
+ * content was rewritten is kept VERBATIM — a flat string can't be faithfully split back into N
154
+ * results, and splitting is grammar (bareagent's), not litectx's to attempt. This isn't a special
155
+ * case: litectx's compress() is a pure text→text render that returns verbatim when handed no single
156
+ * parseable format (compress.js — "never returns less than the body losslessly"), so a flattened
157
+ * multi-result unit round-trips unchanged on both sides. RATIFIED by litectx (2026-06-12). The pairing
158
+ * seatbelt is the final guard.
159
+ * @param {Array<Record<string, any>>} units
160
+ * @returns {Array<Record<string, any>>}
161
+ */
162
+ function fromUnits(units) {
163
+ const msgs = [];
164
+ for (const u of units) {
165
+ if (!u || typeof u !== 'object') continue;
166
+ const backing = u._msgs;
167
+
168
+ if (!Array.isArray(backing)) {
169
+ // litectx-minted (recall-inject): synthesise one message from role + content
170
+ msgs.push({ role: u.role || 'user', content: u.content == null ? '' : String(u.content) });
171
+ continue;
172
+ }
173
+
174
+ const edited = u.content !== u._origContent;
175
+ if (!edited) {
176
+ for (const m of backing) msgs.push(m); // verbatim — preserves tool_call_id pairing exactly
177
+ continue;
178
+ }
179
+
180
+ if (!u.atomic) {
181
+ // single backing message, content rewritten
182
+ msgs.push({ ...backing[0], content: u.content == null ? '' : String(u.content) });
183
+ continue;
184
+ }
185
+
186
+ // atomic + content rewritten: assistant message stays verbatim (pairing); rewrite lands on the
187
+ // single result. Multi-result bundle → keep verbatim (unsplittable).
188
+ const results = backing.slice(1);
189
+ if (results.length === 1) {
190
+ msgs.push(backing[0]);
191
+ msgs.push({ ...results[0], content: u.content == null ? '' : String(u.content) });
192
+ } else {
193
+ for (const m of backing) msgs.push(m);
194
+ }
195
+ }
196
+ return pairingSeatbelt(msgs);
197
+ }
198
+
199
+ /**
200
+ * Wrap litectx's `assemble(units, ctx)` verb into the Loop's msgs-level `assemble(msgs, ctx)` seam.
201
+ * litectx ships the **`AssembleResult` envelope** `{ units, dropped, tokens }` (CE-PRD §8.2: `dropped[]`
202
+ * is load-bearing — it ships in the same slice, never silently truncated). This wrapper accepts that
203
+ * envelope (uses `.units`) OR a bare `units` array (a simpler consumer). `dropped`/`tokens` are litectx's
204
+ * accounting; the Loop's seam is msgs-in/msgs-out, so they're not threaded onward here (the canonical
205
+ * transcript already holds every dropped unit by id — restorable on demand).
206
+ * Fail-OPEN at this layer too: any other return shape → the original msgs are sent unchanged. A thrown
207
+ * error (incl. HaltError) is left to the Loop's own fail-open / HaltError handling — not swallowed here.
208
+ * @param {(units: Array<Record<string, any>>, ctx: any) => (any | Promise<any>)} assembleUnits
209
+ * @returns {(msgs: Array<Record<string, any>>, ctx: any) => Promise<Array<Record<string, any>>>}
210
+ */
211
+ function unitAssembler(assembleUnits) {
212
+ if (typeof assembleUnits !== 'function') {
213
+ throw new Error('[context-units] unitAssembler(fn): fn must be (units, ctx) => units');
214
+ }
215
+ return async (msgs, ctx) => {
216
+ const units = toUnits(msgs);
217
+ const out = await assembleUnits(units, ctx);
218
+ // litectx returns { units, dropped, tokens }; a bare array is also accepted. Anything else → fail-open.
219
+ const view = Array.isArray(out) ? out : (out && Array.isArray(out.units) ? out.units : null);
220
+ if (!view) return msgs; // fail-open: unrecognised return → full context
221
+ return fromUnits(view);
222
+ };
223
+ }
224
+
225
+ module.exports = { toUnits, fromUnits, unitAssembler, approxTokens, pairingSeatbelt };
package/src/loop.d.ts CHANGED
@@ -20,6 +20,16 @@ export type LoopOptions = {
20
20
  onError?: Function | undefined;
21
21
  throwOnError?: boolean | undefined;
22
22
  policy?: Function | undefined;
23
+ /**
24
+ * - async (msgs, ctx) => msgs. Context-assembly chokepoint: shape the
25
+ * window sent to the provider each round (e.g. a context-engineering library). Returns a VIEW — the
26
+ * canonical transcript is never mutated. Fail-open (a thrown error degrades to full context); a
27
+ * thrown HaltError propagates. `ctx` is the per-run opaque blob (`run(msgs, tools, { ctx })`), the
28
+ * same object forwarded to `policy`; litectx reads `ctx.task` (intent) and `ctx.budget`. The
29
+ * neutral-unit signature `assemble(units, ctx)` is provided by bareagent's msgs⇄units adapter
30
+ * (src/context-units.js), which composes over this msgs-level seam.
31
+ */
32
+ assemble?: Function | undefined;
23
33
  onLlmResult?: Function | undefined;
24
34
  onToolResult?: Function | undefined;
25
35
  /**
@@ -49,6 +59,7 @@ export class Loop {
49
59
  throwOnError: boolean;
50
60
  store: import("../types").Store | null;
51
61
  policy: Function | null;
62
+ assemble: Function | null;
52
63
  onLlmResult: Function | null;
53
64
  onToolResult: Function | null;
54
65
  _stopped: boolean;
package/src/loop.js CHANGED
@@ -26,6 +26,13 @@ const { ToolError, HaltError } = require('./errors');
26
26
  * @property {Function} [onError]
27
27
  * @property {boolean} [throwOnError]
28
28
  * @property {Function} [policy]
29
+ * @property {Function} [assemble] - async (msgs, ctx) => msgs. Context-assembly chokepoint: shape the
30
+ * window sent to the provider each round (e.g. a context-engineering library). Returns a VIEW — the
31
+ * canonical transcript is never mutated. Fail-open (a thrown error degrades to full context); a
32
+ * thrown HaltError propagates. `ctx` is the per-run opaque blob (`run(msgs, tools, { ctx })`), the
33
+ * same object forwarded to `policy`; litectx reads `ctx.task` (intent) and `ctx.budget`. The
34
+ * neutral-unit signature `assemble(units, ctx)` is provided by bareagent's msgs⇄units adapter
35
+ * (src/context-units.js), which composes over this msgs-level seam.
29
36
  * @property {Function} [onLlmResult]
30
37
  * @property {Function} [onToolResult]
31
38
  * @property {number} [maxRounds] - Removed in v0.8; presence throws a migration error.
@@ -132,6 +139,10 @@ class Loop {
132
139
  throw new Error('[Loop] options.policy must be a function (toolName, args, ctx) => true | string');
133
140
  }
134
141
  this.policy = options.policy || null;
142
+ if (options.assemble != null && typeof options.assemble !== 'function') {
143
+ throw new Error('[Loop] options.assemble must be a function (msgs, info) => msgs');
144
+ }
145
+ this.assemble = options.assemble || null;
135
146
  if (options.onLlmResult != null && typeof options.onLlmResult !== 'function') {
136
147
  throw new Error('[Loop] options.onLlmResult must be a function');
137
148
  }
@@ -243,10 +254,29 @@ class Loop {
243
254
  for (let round = 0; round < HARD_ROUND_LIMIT; round++) {
244
255
  if (this._stopped) break;
245
256
 
257
+ // RT-1: context-assembly chokepoint. Let a caller (e.g. a context-engineering library) shape
258
+ // the window sent to the provider this round. Returns a VIEW — the canonical `msgs` transcript
259
+ // is never mutated, so result.msgs stays complete and correct. Fail-OPEN: an assembly error
260
+ // degrades to sending full context (a context-optimizer bug must not halt the agent); a thrown
261
+ // HaltError is a governance exit and propagates (same contract as onLlmResult).
262
+ let toSend = msgs;
263
+ if (this.assemble) {
264
+ try {
265
+ const view = await this.assemble(msgs, ctx);
266
+ if (Array.isArray(view)) {
267
+ toSend = view;
268
+ this._safeEmit({ type: 'loop:assemble', data: { round, before: msgs.length, after: toSend.length } });
269
+ }
270
+ } catch (err) {
271
+ if (err instanceof HaltError) throw err;
272
+ this._reportError('assemble', err, { round });
273
+ }
274
+ }
275
+
246
276
  let result;
247
277
  const llmStartedAt = Date.now();
248
278
  try {
249
- const generate = () => this.provider.generate(msgs, tools, options);
279
+ const generate = () => this.provider.generate(toSend, tools, options);
250
280
  result = this.retry ? await this.retry.call(generate) : await generate();
251
281
  } catch (err) {
252
282
  this._reportError('provider', err, { round });
package/src/tools.d.ts CHANGED
@@ -5,4 +5,5 @@ import { createSpawnTool } from "../tools/spawn";
5
5
  import { spawnChild } from "../tools/spawn";
6
6
  import { createDeferTool } from "../tools/defer";
7
7
  import { readQueue as readDeferQueue } from "../tools/defer";
8
- export { createBrowsingTools, createMobileTools, createShellTools, createSpawnTool, spawnChild, createDeferTool, readDeferQueue };
8
+ import { liteCtxMcpBridgeConfig } from "../tools/litectx-mcp";
9
+ export { createBrowsingTools, createMobileTools, createShellTools, createSpawnTool, spawnChild, createDeferTool, readDeferQueue, liteCtxMcpBridgeConfig };
package/src/tools.js CHANGED
@@ -5,6 +5,7 @@ const { createMobileTools } = require('../tools/mobile');
5
5
  const { createShellTools } = require('../tools/shell');
6
6
  const { createSpawnTool, spawnChild } = require('../tools/spawn');
7
7
  const { createDeferTool, readQueue: readDeferQueue } = require('../tools/defer');
8
+ const { liteCtxMcpBridgeConfig } = require('../tools/litectx-mcp');
8
9
 
9
10
  module.exports = {
10
11
  createBrowsingTools,
@@ -14,4 +15,5 @@ module.exports = {
14
15
  spawnChild,
15
16
  createDeferTool,
16
17
  readDeferQueue,
18
+ liteCtxMcpBridgeConfig,
17
19
  };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Build a curated bridge config that mounts litectx-mcp read-only on its own db.
3
+ * @param {object} opts
4
+ * @param {string} opts.root - the child's OWN litectx root/db (passed as `--root`). Isolation.
5
+ * @param {string} [opts.command='litectx-mcp'] - the server command (override for a fake/abs path).
6
+ * @param {string[]} [opts.args=[]] - extra args appended after `--root <root>` (e.g. `--no-embeddings`).
7
+ * @param {boolean} [opts.writable=false] - opt-in: allow remember/forget (still child-db-local).
8
+ * @param {string} [opts.name='litectx'] - server name (tool prefix → `<name>_recall`, …).
9
+ * @param {string} [opts.ttl='24h'] - bridge config TTL.
10
+ * @param {string} [opts.now] - ISO timestamp for `discovered` (default: now). Pre-seed fresh so
11
+ * `createMCPBridge` skips IDE discovery and connects straight to this curated server.
12
+ * @returns {import('../src/mcp-bridge').BridgeConfig}
13
+ */
14
+ export function liteCtxMcpBridgeConfig(opts: {
15
+ root: string;
16
+ command?: string | undefined;
17
+ args?: string[] | undefined;
18
+ writable?: boolean | undefined;
19
+ name?: string | undefined;
20
+ ttl?: string | undefined;
21
+ now?: string | undefined;
22
+ }): import("../src/mcp-bridge").BridgeConfig;
23
+ /** Read/reason verbs a child is given by default. */
24
+ export const READ_VERBS: string[];
25
+ /** Write verbs — denied unless `writable`, then allowed (writes still land in the child's OWN db). */
26
+ export const WRITE_VERBS: string[];
27
+ /** Admin/review verbs — always denied to a child (human/hook + review flows). */
28
+ export const ADMIN_VERBS: string[];
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ // RT-4 — mount litectx-mcp into a child agent, read-only, on its own db. A thin recipe helper: it
4
+ // builds the curated `.mcp-bridge.json` config that `createMCPBridge` consumes, so a parent composing
5
+ // a child's toolbox can't fat-finger the allow-list (e.g. accidentally allow a write verb). It encodes
6
+ // ONE thing — the agreed read-only default (PRD §4.2) — and knows only litectx-mcp's public verb names
7
+ // (the same strings a hand-written `.mcp-bridge.json` would carry). It does NOT import litectx: the
8
+ // dependency direction stays one-way; this is config curation, bareagent's job.
9
+ //
10
+ // Default toolbox (PRD §4.2):
11
+ // recall · get · impact · recent → allow (read / reason — the point of giving a child memory)
12
+ // remember · forget → deny (agent writes are by:"agent", suspect-until-curated;
13
+ // flip with { writable: true } as the explicit opt-in)
14
+ // index · promotions → deny (index is human/hook-driven; promotions is a review flow)
15
+ //
16
+ // Isolation is OWN-DB, not a scope column (this is what decouples RT-4 from RT-5): the child gets its
17
+ // own `--root` → physical isolation, zero schema change. An opted-in child's writes land in ITS db;
18
+ // promotion to the parent is an explicit parent-side `recall`→`remember`, never automatic.
19
+
20
+ /** Read/reason verbs a child is given by default. */
21
+ const READ_VERBS = ['recall', 'get', 'impact', 'recent'];
22
+ /** Write verbs — denied unless `writable`, then allowed (writes still land in the child's OWN db). */
23
+ const WRITE_VERBS = ['remember', 'forget'];
24
+ /** Admin/review verbs — always denied to a child (human/hook + review flows). */
25
+ const ADMIN_VERBS = ['index', 'promotions'];
26
+
27
+ /**
28
+ * Build a curated bridge config that mounts litectx-mcp read-only on its own db.
29
+ * @param {object} opts
30
+ * @param {string} opts.root - the child's OWN litectx root/db (passed as `--root`). Isolation.
31
+ * @param {string} [opts.command='litectx-mcp'] - the server command (override for a fake/abs path).
32
+ * @param {string[]} [opts.args=[]] - extra args appended after `--root <root>` (e.g. `--no-embeddings`).
33
+ * @param {boolean} [opts.writable=false] - opt-in: allow remember/forget (still child-db-local).
34
+ * @param {string} [opts.name='litectx'] - server name (tool prefix → `<name>_recall`, …).
35
+ * @param {string} [opts.ttl='24h'] - bridge config TTL.
36
+ * @param {string} [opts.now] - ISO timestamp for `discovered` (default: now). Pre-seed fresh so
37
+ * `createMCPBridge` skips IDE discovery and connects straight to this curated server.
38
+ * @returns {import('../src/mcp-bridge').BridgeConfig}
39
+ */
40
+ function liteCtxMcpBridgeConfig(opts) {
41
+ if (!opts || typeof opts.root !== 'string' || !opts.root) {
42
+ throw new Error('[litectx-mcp] requires opts.root (the child\'s own db root — own-db isolation)');
43
+ }
44
+ const { root, command = 'litectx-mcp', args = [], writable = false, name = 'litectx', ttl = '24h', now } = opts;
45
+
46
+ /** @type {Record<string, string>} */
47
+ const tools = {};
48
+ for (const v of READ_VERBS) tools[v] = 'allow';
49
+ for (const v of WRITE_VERBS) tools[v] = writable ? 'allow' : 'deny';
50
+ for (const v of ADMIN_VERBS) tools[v] = 'deny';
51
+
52
+ return {
53
+ discovered: now || new Date().toISOString(),
54
+ ttl,
55
+ servers: {
56
+ [name]: {
57
+ command,
58
+ args: ['--root', root, ...args],
59
+ tools,
60
+ },
61
+ },
62
+ };
63
+ }
64
+
65
+ module.exports = { liteCtxMcpBridgeConfig, READ_VERBS, WRITE_VERBS, ADMIN_VERBS };