reasonix 0.21.0 → 0.22.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.
package/dist/cli/index.js CHANGED
@@ -1322,6 +1322,26 @@ function computeP95(samples) {
1322
1322
  // src/mcp/registry.ts
1323
1323
  var DEFAULT_MAX_RESULT_CHARS = 32e3;
1324
1324
  var DEFAULT_MAX_RESULT_TOKENS = 8e3;
1325
+ function registerSingleMcpTool(mcpTool, env) {
1326
+ if (!mcpTool.name) return "";
1327
+ const registeredName = `${env.prefix}${mcpTool.name}`;
1328
+ env.registry.register({
1329
+ name: registeredName,
1330
+ description: mcpTool.description ?? "",
1331
+ parameters: mcpTool.inputSchema,
1332
+ fn: async (args, ctx) => {
1333
+ const t0 = env.tracker ? Date.now() : 0;
1334
+ const live = env.host.client;
1335
+ const toolResult = await live.callTool(mcpTool.name, args, {
1336
+ onProgress: env.onProgress ? (info) => env.onProgress({ toolName: registeredName, ...info }) : void 0,
1337
+ signal: ctx?.signal
1338
+ });
1339
+ if (env.tracker) env.tracker.record(Date.now() - t0);
1340
+ return flattenMcpResult(toolResult, { maxChars: env.maxResultChars });
1341
+ }
1342
+ });
1343
+ return registeredName;
1344
+ }
1325
1345
  async function bridgeMcpTools(client, opts = {}) {
1326
1346
  const registry = opts.registry ?? new ToolRegistry({ autoFlatten: opts.autoFlatten });
1327
1347
  const prefix = opts.namePrefix ?? "";
@@ -1329,39 +1349,25 @@ async function bridgeMcpTools(client, opts = {}) {
1329
1349
  const result = { registry, registeredNames: [], skipped: [] };
1330
1350
  const serverName = opts.serverName ?? prefix.replace(/_$/, "") ?? "anon";
1331
1351
  const tracker = opts.onSlow ? new LatencyTracker(serverName, { thresholdMs: opts.slowThresholdMs, onSlow: opts.onSlow }) : null;
1352
+ const host = opts.host ?? { client };
1353
+ const env = {
1354
+ registry,
1355
+ host,
1356
+ prefix,
1357
+ maxResultChars,
1358
+ tracker,
1359
+ onProgress: opts.onProgress
1360
+ };
1332
1361
  const listed = await client.listTools();
1333
1362
  for (const mcpTool of listed.tools) {
1334
1363
  if (!mcpTool.name) {
1335
1364
  result.skipped.push({ name: "?", reason: "empty tool name" });
1336
1365
  continue;
1337
1366
  }
1338
- const registeredName = `${prefix}${mcpTool.name}`;
1339
- registry.register({
1340
- name: registeredName,
1341
- description: mcpTool.description ?? "",
1342
- parameters: mcpTool.inputSchema,
1343
- fn: async (args, ctx) => {
1344
- const t0 = tracker ? Date.now() : 0;
1345
- const toolResult = await client.callTool(mcpTool.name, args, {
1346
- // Forward server-side progress frames to the bridge caller,
1347
- // tagged with the registered name so multi-server UIs can
1348
- // disambiguate. No-op when `onProgress` isn't configured —
1349
- // the client then also omits the _meta.progressToken and
1350
- // the server won't emit progress.
1351
- onProgress: opts.onProgress ? (info) => opts.onProgress({ toolName: registeredName, ...info }) : void 0,
1352
- // Thread the tool-dispatch AbortSignal all the way down to
1353
- // the MCP request so Esc truly cancels in flight — the
1354
- // client will emit notifications/cancelled AND reject the
1355
- // pending promise immediately, no "wait for subprocess".
1356
- signal: ctx?.signal
1357
- });
1358
- if (tracker) tracker.record(Date.now() - t0);
1359
- return flattenMcpResult(toolResult, { maxChars: maxResultChars });
1360
- }
1361
- });
1362
- result.registeredNames.push(registeredName);
1367
+ const registeredName = registerSingleMcpTool(mcpTool, env);
1368
+ if (registeredName) result.registeredNames.push(registeredName);
1363
1369
  }
1364
- return result;
1370
+ return { ...result, env };
1365
1371
  }
1366
1372
  function flattenMcpResult(result, opts = {}) {
1367
1373
  const parts = result.content.map(blockToString);
@@ -12978,7 +12984,244 @@ function EditConfirm({ block: block2, onChoose }) {
12978
12984
  // src/cli/ui/McpBrowser.tsx
12979
12985
  import { Box as Box7, Text as Text7 } from "ink";
12980
12986
  import React9, { useState as useState4 } from "react";
12981
- function McpBrowser({ servers, configPath, onClose }) {
12987
+
12988
+ // src/cli/ui/mcp-disable.ts
12989
+ function toggleMcpDisabled(action, name) {
12990
+ const trimmed = name.trim();
12991
+ if (!trimmed) {
12992
+ return `usage: /mcp ${action} <name> \xB7 pick a name shown in /mcp (anonymous servers can't be named-toggled).`;
12993
+ }
12994
+ const cfg = readConfig();
12995
+ const current = new Set(cfg.mcpDisabled ?? []);
12996
+ if (action === "disable") {
12997
+ if (current.has(trimmed)) {
12998
+ return `\u25B8 ${trimmed} is already disabled \u2014 restart to apply, or /mcp enable ${trimmed}.`;
12999
+ }
13000
+ current.add(trimmed);
13001
+ writeConfig({ ...cfg, mcpDisabled: [...current].sort() });
13002
+ return `\u25B8 ${trimmed} disabled \u2014 takes effect on next launch. /mcp enable ${trimmed} to revert.`;
13003
+ }
13004
+ if (!current.has(trimmed)) {
13005
+ return `\u25B8 ${trimmed} is not disabled.`;
13006
+ }
13007
+ current.delete(trimmed);
13008
+ writeConfig({ ...cfg, mcpDisabled: current.size > 0 ? [...current].sort() : void 0 });
13009
+ return `\u25B8 ${trimmed} re-enabled \u2014 takes effect on next launch.`;
13010
+ }
13011
+
13012
+ // src/mcp/drift.ts
13013
+ function classifyToolListDrift(before, after) {
13014
+ const beforeNames = before.map(nameOf);
13015
+ const afterNames = after.map(nameOf);
13016
+ const beforeSet = new Set(beforeNames);
13017
+ const afterSet = new Set(afterNames);
13018
+ const added = afterNames.filter((n) => !beforeSet.has(n));
13019
+ const removed = beforeNames.filter((n) => !afterSet.has(n));
13020
+ const edited = [];
13021
+ const sharedLen = Math.min(before.length, after.length);
13022
+ for (let i = 0; i < sharedLen; i++) {
13023
+ if (beforeNames[i] === afterNames[i] && hash(before[i]) !== hash(after[i])) {
13024
+ edited.push(beforeNames[i]);
13025
+ }
13026
+ }
13027
+ if (before.length === after.length && edited.length === 0 && beforeNames.every((n, i) => n === afterNames[i])) {
13028
+ return { kind: "identity", added: [], removed: [], edited: [] };
13029
+ }
13030
+ if (removed.length > 0) {
13031
+ return { kind: "remove", added, removed, edited };
13032
+ }
13033
+ if (after.length > before.length && beforeNames.every((n, i) => n === afterNames[i] && hash(before[i]) === hash(after[i]))) {
13034
+ return { kind: "append", added, removed: [], edited: [] };
13035
+ }
13036
+ const sameNameSet = beforeSet.size === afterSet.size && [...beforeSet].every((n) => afterSet.has(n));
13037
+ if (sameNameSet) {
13038
+ const positionsMatch = beforeNames.every((n, i) => n === afterNames[i]);
13039
+ if (positionsMatch) {
13040
+ return { kind: "edit", added: [], removed: [], edited };
13041
+ }
13042
+ return { kind: "reorder", added: [], removed: [], edited };
13043
+ }
13044
+ return { kind: "reorder", added, removed: [], edited };
13045
+ }
13046
+ function nameOf(spec) {
13047
+ return spec.function?.name ?? "";
13048
+ }
13049
+ function hash(spec) {
13050
+ return JSON.stringify(spec);
13051
+ }
13052
+
13053
+ // src/mcp/reconnect.ts
13054
+ async function reconnectMcpServer(args) {
13055
+ const t0 = Date.now();
13056
+ const accept = args.accept ?? ["identity"];
13057
+ let parsed;
13058
+ try {
13059
+ parsed = parseMcpSpec(args.spec);
13060
+ } catch (err) {
13061
+ return {
13062
+ ok: false,
13063
+ reason: "spec_parse",
13064
+ message: err.message,
13065
+ ms: Date.now() - t0
13066
+ };
13067
+ }
13068
+ const transport = parsed.transport === "sse" ? new SseTransport({ url: parsed.url }) : parsed.transport === "streamable-http" ? new StreamableHttpTransport({ url: parsed.url }) : new StdioTransport({ command: parsed.command, args: parsed.args });
13069
+ const next = new McpClient({ transport });
13070
+ try {
13071
+ await next.initialize();
13072
+ const listed = await next.listTools();
13073
+ const drift = classifyToolListDrift(toolsToSpecs(args.beforeTools), toolsToSpecs(listed.tools));
13074
+ const acceptedKind = drift.kind === "identity" ? "identity" : drift.kind === "append" && accept.includes("append") ? "append" : null;
13075
+ if (acceptedKind === null) {
13076
+ await next.close().catch(() => {
13077
+ });
13078
+ const refused = drift.kind;
13079
+ return {
13080
+ ok: false,
13081
+ reason: driftReason(refused),
13082
+ message: driftMessage(drift),
13083
+ ms: Date.now() - t0
13084
+ };
13085
+ }
13086
+ const addedTools = acceptedKind === "append" ? listed.tools.filter((t2) => drift.added.includes(t2.name)) : [];
13087
+ const old = args.host.client;
13088
+ args.host.client = next;
13089
+ await old.close().catch(() => {
13090
+ });
13091
+ return {
13092
+ ok: true,
13093
+ kind: acceptedKind,
13094
+ afterTools: listed.tools,
13095
+ addedTools,
13096
+ ms: Date.now() - t0
13097
+ };
13098
+ } catch (err) {
13099
+ await next.close().catch(() => {
13100
+ });
13101
+ return {
13102
+ ok: false,
13103
+ reason: "handshake",
13104
+ message: err.message,
13105
+ ms: Date.now() - t0
13106
+ };
13107
+ }
13108
+ }
13109
+ function driftReason(kind) {
13110
+ if (kind === "append") return "drift_added";
13111
+ if (kind === "edit") return "drift_edited";
13112
+ if (kind === "reorder") return "drift_reordered";
13113
+ return "drift_removed";
13114
+ }
13115
+ function driftMessage(drift) {
13116
+ if (drift.kind === "append") {
13117
+ return `tool list grew (${drift.added.length} added: ${drift.added.join(", ")}). Restart Reasonix to bridge the new tool(s).`;
13118
+ }
13119
+ if (drift.kind === "edit") {
13120
+ return `tool description/schema changed for ${drift.edited.join(", ")}. Restart Reasonix to apply.`;
13121
+ }
13122
+ if (drift.kind === "remove") {
13123
+ return `tool(s) removed: ${drift.removed.join(", ")}. Restart Reasonix to drop them from the registry.`;
13124
+ }
13125
+ return "tool list reordered or restructured \u2014 cache prefix would be invalidated. Restart Reasonix.";
13126
+ }
13127
+ function toolsToSpecs(tools) {
13128
+ return tools.map((t2) => ({
13129
+ type: "function",
13130
+ function: {
13131
+ name: t2.name,
13132
+ description: t2.description ?? "",
13133
+ parameters: t2.inputSchema
13134
+ }
13135
+ }));
13136
+ }
13137
+
13138
+ // src/cli/ui/mcp-lifecycle.ts
13139
+ var STATE = {
13140
+ handshake: { glyph: "\u21BB", label: "handshake\u2026" },
13141
+ connected: { glyph: "\u2713", label: "connected" },
13142
+ failed: { glyph: "\u2716", label: "failed" },
13143
+ disabled: { glyph: "\u25CB", label: "disabled" },
13144
+ reconnect: { glyph: "\u21BB", label: "reconnect\u2026" }
13145
+ };
13146
+ var NAME_COL = 22;
13147
+ var STATE_COL = 15;
13148
+ function formatMcpLifecycleEvent(ev) {
13149
+ const { glyph, label } = STATE[ev.state];
13150
+ const namePart = `MCP \xB7 ${ev.name}`;
13151
+ const namePad = " ".repeat(Math.max(1, NAME_COL - namePart.length));
13152
+ const stateField = `${glyph} ${label}`.padEnd(STATE_COL);
13153
+ return `\u2318 ${namePart}${namePad}${stateField}${describeDetail(ev)}`;
13154
+ }
13155
+ function describeDetail(ev) {
13156
+ if (ev.state === "handshake") return "initialise \u2192 tools/list \u2192 resources/list";
13157
+ if (ev.state === "failed") return ev.reason;
13158
+ if (ev.state === "disabled") return `via /mcp disable ${ev.name}`;
13159
+ if (ev.state === "reconnect") return "tearing down \xB7 re-handshake \xB7 listing tools";
13160
+ const parts = [`${ev.tools} tools`];
13161
+ if (ev.resources && ev.resources > 0) parts.push(`${ev.resources} resources`);
13162
+ if (ev.prompts && ev.prompts > 0) parts.push(`${ev.prompts} prompts`);
13163
+ parts.push(`${ev.ms}ms`);
13164
+ return parts.join(" \xB7 ");
13165
+ }
13166
+
13167
+ // src/cli/ui/mcp-reconnect-kickoff.ts
13168
+ function kickOffMcpReconnect(target, postInfo, applyAppend) {
13169
+ const beforeTools = target.report.tools.supported ? target.report.tools.items : [];
13170
+ const accept = applyAppend ? ["identity", "append"] : ["identity"];
13171
+ void (async () => {
13172
+ try {
13173
+ const result = await reconnectMcpServer({
13174
+ host: target.host,
13175
+ spec: target.spec,
13176
+ beforeTools,
13177
+ accept
13178
+ });
13179
+ if (result.ok) {
13180
+ if (result.kind === "append" && applyAppend) {
13181
+ applyAppend(target, result.addedTools);
13182
+ }
13183
+ postInfo(
13184
+ formatMcpLifecycleEvent({
13185
+ state: "connected",
13186
+ name: target.label,
13187
+ tools: result.afterTools.length,
13188
+ ms: result.ms
13189
+ })
13190
+ );
13191
+ if (result.kind === "append") {
13192
+ const names = result.addedTools.map((t2) => t2.name).join(", ");
13193
+ postInfo(`\u25B8 ${target.label}: added ${result.addedTools.length} tool(s) \u2014 ${names}`);
13194
+ }
13195
+ } else {
13196
+ postInfo(
13197
+ formatMcpLifecycleEvent({
13198
+ state: "failed",
13199
+ name: target.label,
13200
+ reason: `${result.reason} \xB7 ${result.message}`
13201
+ })
13202
+ );
13203
+ }
13204
+ } catch (err) {
13205
+ postInfo(
13206
+ formatMcpLifecycleEvent({
13207
+ state: "failed",
13208
+ name: target.label,
13209
+ reason: err.message
13210
+ })
13211
+ );
13212
+ }
13213
+ })();
13214
+ return formatMcpLifecycleEvent({ state: "reconnect", name: target.label });
13215
+ }
13216
+
13217
+ // src/cli/ui/McpBrowser.tsx
13218
+ function McpBrowser({
13219
+ servers,
13220
+ configPath,
13221
+ onClose,
13222
+ postInfo,
13223
+ applyAppend
13224
+ }) {
12982
13225
  const [index, setIndex] = useState4(0);
12983
13226
  const max = Math.max(0, servers.length - 1);
12984
13227
  useKeystroke((ev) => {
@@ -12986,6 +13229,17 @@ function McpBrowser({ servers, configPath, onClose }) {
12986
13229
  if (ev.upArrow) setIndex((i) => Math.max(0, i - 1));
12987
13230
  else if (ev.downArrow) setIndex((i) => Math.min(max, i + 1));
12988
13231
  else if (ev.escape) onClose();
13232
+ else if (ev.input === "r") {
13233
+ const target = servers[index];
13234
+ if (!target) return;
13235
+ postInfo(kickOffMcpReconnect(target, postInfo, applyAppend));
13236
+ onClose();
13237
+ } else if (ev.input === "d") {
13238
+ const target = servers[index];
13239
+ if (!target) return;
13240
+ postInfo(toggleMcpDisabled("disable", target.label));
13241
+ onClose();
13242
+ }
12989
13243
  });
12990
13244
  return /* @__PURE__ */ React9.createElement(Box7, { flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React9.createElement(Box7, null, /* @__PURE__ */ React9.createElement(Text7, { bold: true, color: COLOR.brand }, "\u25C8 MCP browser"), /* @__PURE__ */ React9.createElement(
12991
13245
  Text7,
@@ -12993,7 +13247,7 @@ function McpBrowser({ servers, configPath, onClose }) {
12993
13247
  dimColor: true
12994
13248
  },
12995
13249
  ` \xB7 ${configPath} \xB7 ${servers.length} server${servers.length === 1 ? "" : "s"}`
12996
- )), /* @__PURE__ */ React9.createElement(Box7, { marginTop: 1, flexDirection: "column" }, servers.length === 0 ? /* @__PURE__ */ React9.createElement(Text7, { dimColor: true }, "No MCP servers attached. Run `reasonix setup` to pick some, or launch with --mcp.") : servers.map((s, i) => /* @__PURE__ */ React9.createElement(ServerRow, { key: s.label + s.spec, server: s, active: i === index }))), /* @__PURE__ */ React9.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text7, { dimColor: true }, "\u2191\u2193 pick \xB7 [r] reconnect (Stage C) \xB7 [d] disable (Stage C) \xB7 esc quit")));
13250
+ )), /* @__PURE__ */ React9.createElement(Box7, { marginTop: 1, flexDirection: "column" }, servers.length === 0 ? /* @__PURE__ */ React9.createElement(Text7, { dimColor: true }, "No MCP servers attached. Run `reasonix setup` to pick some, or launch with --mcp.") : servers.map((s, i) => /* @__PURE__ */ React9.createElement(ServerRow, { key: s.label + s.spec, server: s, active: i === index }))), /* @__PURE__ */ React9.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text7, { dimColor: true }, "\u2191\u2193 pick \xB7 [r] reconnect \xB7 [d] disable \xB7 esc quit")));
12997
13251
  }
12998
13252
  function ServerRow({ server, active }) {
12999
13253
  const { label, toolCount, report } = server;
@@ -18979,6 +19233,32 @@ function formatDuration(ms) {
18979
19233
  return mm === 0 ? `${h}h` : `${h}h${mm}m`;
18980
19234
  }
18981
19235
 
19236
+ // src/cli/ui/mcp-append.ts
19237
+ function applyMcpAppend(loop2, target, addedTools) {
19238
+ const accepted = [];
19239
+ for (const mcpTool of addedTools) {
19240
+ if (!mcpTool.name) continue;
19241
+ const registeredName = registerSingleMcpTool(mcpTool, target.bridgeEnv);
19242
+ if (!registeredName) continue;
19243
+ const spec = {
19244
+ type: "function",
19245
+ function: {
19246
+ name: registeredName,
19247
+ description: mcpTool.description ?? "",
19248
+ parameters: mcpTool.inputSchema
19249
+ }
19250
+ };
19251
+ loop2.prefix.addTool(spec);
19252
+ accepted.push(mcpTool);
19253
+ }
19254
+ if (accepted.length === 0) return;
19255
+ if (target.report.tools.supported) {
19256
+ const merged = [...target.report.tools.items, ...accepted];
19257
+ target.report.tools.items = merged;
19258
+ target.toolCount = merged.length;
19259
+ }
19260
+ }
19261
+
18982
19262
  // src/cli/ui/mcp-browse.ts
18983
19263
  function formatResourceList(servers) {
18984
19264
  const lines = [];
@@ -19107,14 +19387,7 @@ async function handleMcpBrowseSlash(kind, arg, servers, log) {
19107
19387
  );
19108
19388
  return;
19109
19389
  }
19110
- const client2 = server2.client;
19111
- if (!client2) {
19112
- log.pushWarning(
19113
- `server [${server2.label}] is not connected (display-only)`,
19114
- "Resource read requires a live MCP client."
19115
- );
19116
- return;
19117
- }
19390
+ const client2 = server2.host.client;
19118
19391
  try {
19119
19392
  const result = await client2.readResource(arg);
19120
19393
  log.pushInfo(formatResourceContents(arg, result));
@@ -19131,14 +19404,7 @@ async function handleMcpBrowseSlash(kind, arg, servers, log) {
19131
19404
  );
19132
19405
  return;
19133
19406
  }
19134
- const client = server.client;
19135
- if (!client) {
19136
- log.pushWarning(
19137
- `server [${server.label}] is not connected (display-only)`,
19138
- "Prompt fetch requires a live MCP client."
19139
- );
19140
- return;
19141
- }
19407
+ const client = server.host.client;
19142
19408
  try {
19143
19409
  const result = await client.getPrompt(arg);
19144
19410
  log.pushInfo(formatPromptMessages(arg, result));
@@ -20606,6 +20872,9 @@ var mcp = (args, loop2, ctx) => {
20606
20872
  if (sub === "disable" || sub === "enable") {
20607
20873
  return toggleDisabled(sub, args[1], { servers, specs });
20608
20874
  }
20875
+ if (sub === "reconnect") {
20876
+ return triggerReconnect(args[1], servers, ctx.postInfo, loop2);
20877
+ }
20609
20878
  const wantsTextDump = sub === "text";
20610
20879
  if (servers.length === 0 && specs.length === 0 && toolSpecs.length === 0) {
20611
20880
  return {
@@ -20681,29 +20950,35 @@ function toggleDisabled(action, rawName, ctx) {
20681
20950
  const list2 = [...known].sort().join(", ") || "(none)";
20682
20951
  return { info: `unknown MCP server "${name}". Known: ${list2}.` };
20683
20952
  }
20684
- const cfg = readConfig();
20685
- const current = new Set(cfg.mcpDisabled ?? []);
20686
- if (action === "disable") {
20687
- if (current.has(name)) {
20688
- return { info: `\u25B8 ${name} is already disabled \u2014 restart to apply, or /mcp enable ${name}.` };
20689
- }
20690
- current.add(name);
20691
- writeConfig({ ...cfg, mcpDisabled: [...current].sort() });
20692
- return {
20693
- info: `\u25B8 ${name} disabled \u2014 takes effect on next launch. /mcp enable ${name} to revert.`
20694
- };
20695
- }
20696
- if (!current.has(name)) {
20697
- return { info: `\u25B8 ${name} is not disabled.` };
20698
- }
20699
- current.delete(name);
20700
- writeConfig({ ...cfg, mcpDisabled: current.size > 0 ? [...current].sort() : void 0 });
20701
- return { info: `\u25B8 ${name} re-enabled \u2014 takes effect on next launch.` };
20953
+ return { info: toggleMcpDisabled(action, name) };
20702
20954
  }
20703
20955
  function parseLabelFromSpec(spec) {
20704
20956
  const match = spec.match(/^([a-zA-Z_][a-zA-Z0-9_-]*)=/);
20705
20957
  return match ? match[1] ?? null : null;
20706
20958
  }
20959
+ function triggerReconnect(rawName, servers, postInfo, loop2) {
20960
+ const name = rawName?.trim();
20961
+ if (!name) {
20962
+ return {
20963
+ info: "usage: /mcp reconnect <name> \xB7 pick a name shown in /mcp."
20964
+ };
20965
+ }
20966
+ const target = servers.find((s) => s.label === name);
20967
+ if (!target) {
20968
+ const list2 = servers.map((s) => s.label).sort().join(", ");
20969
+ return { info: `unknown MCP server "${name}". Known: ${list2 || "(none)"}.` };
20970
+ }
20971
+ if (!postInfo) {
20972
+ return { info: "/mcp reconnect requires the interactive TUI (postInfo not wired)." };
20973
+ }
20974
+ return {
20975
+ info: kickOffMcpReconnect(
20976
+ target,
20977
+ postInfo,
20978
+ (t2, addedTools) => applyMcpAppend(loop2, t2, addedTools)
20979
+ )
20980
+ };
20981
+ }
20707
20982
  var handlers7 = { mcp };
20708
20983
 
20709
20984
  // src/cli/ui/slash/handlers/memory.ts
@@ -24660,7 +24935,9 @@ Continue executing from the next pending step. Call mark_step_complete after eac
24660
24935
  {
24661
24936
  servers: mcpServers ?? [],
24662
24937
  configPath: defaultConfigPath(),
24663
- onClose: () => setPendingMcpBrowser(false)
24938
+ onClose: () => setPendingMcpBrowser(false),
24939
+ postInfo: (text) => log.pushInfo(text),
24940
+ applyAppend: (target, addedTools) => applyMcpAppend(loop2, target, addedTools)
24664
24941
  }
24665
24942
  ) : pendingPlan ? /* @__PURE__ */ React54.createElement(
24666
24943
  PlanConfirm,
@@ -24796,33 +25073,6 @@ function Setup({ onReady }) {
24796
25073
  )), error ? /* @__PURE__ */ React55.createElement(Box47, { marginTop: 1 }, /* @__PURE__ */ React55.createElement(Text49, { color: COLOR.err, bold: true }, GLYPH.err), /* @__PURE__ */ React55.createElement(Text49, { color: COLOR.err }, ` ${error}`)) : value ? /* @__PURE__ */ React55.createElement(Box47, { marginTop: 1 }, /* @__PURE__ */ React55.createElement(Text49, { dimColor: true }, ` preview \xB7 ${redactKey(value)}`)) : null, /* @__PURE__ */ React55.createElement(Box47, { marginTop: 1 }, /* @__PURE__ */ React55.createElement(Text49, { dimColor: true }, " /exit to abort")));
24797
25074
  }
24798
25075
 
24799
- // src/cli/ui/mcp-lifecycle.ts
24800
- var STATE = {
24801
- handshake: { glyph: "\u21BB", label: "handshake\u2026" },
24802
- connected: { glyph: "\u2713", label: "connected" },
24803
- failed: { glyph: "\u2716", label: "failed" },
24804
- disabled: { glyph: "\u25CB", label: "disabled" }
24805
- };
24806
- var NAME_COL = 22;
24807
- var STATE_COL = 15;
24808
- function formatMcpLifecycleEvent(ev) {
24809
- const { glyph, label } = STATE[ev.state];
24810
- const namePart = `MCP \xB7 ${ev.name}`;
24811
- const namePad = " ".repeat(Math.max(1, NAME_COL - namePart.length));
24812
- const stateField = `${glyph} ${label}`.padEnd(STATE_COL);
24813
- return `\u2318 ${namePart}${namePad}${stateField}${describeDetail(ev)}`;
24814
- }
24815
- function describeDetail(ev) {
24816
- if (ev.state === "handshake") return "initialise \u2192 tools/list \u2192 resources/list";
24817
- if (ev.state === "failed") return ev.reason;
24818
- if (ev.state === "disabled") return `via /mcp disable ${ev.name}`;
24819
- const parts = [`${ev.tools} tools`];
24820
- if (ev.resources && ev.resources > 0) parts.push(`${ev.resources} resources`);
24821
- if (ev.prompts && ev.prompts > 0) parts.push(`${ev.prompts} prompts`);
24822
- parts.push(`${ev.ms}ms`);
24823
- return parts.join(" \xB7 ");
24824
- }
24825
-
24826
25076
  // src/cli/ui/mcp-toast.ts
24827
25077
  function formatMcpSlowToast(t2) {
24828
25078
  const seconds = (t2.p95Ms / 1e3).toFixed(1);
@@ -24941,10 +25191,12 @@ async function chatCommand(opts) {
24941
25191
  const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : spec.transport === "streamable-http" ? new StreamableHttpTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
24942
25192
  const mcp3 = new McpClient({ transport });
24943
25193
  await mcp3.initialize();
25194
+ const host = { client: mcp3 };
24944
25195
  const bridge = await bridgeMcpTools(mcp3, {
24945
25196
  registry: tools,
24946
25197
  namePrefix: prefix,
24947
25198
  serverName: label,
25199
+ host,
24948
25200
  onProgress: (info) => progressSink.current?.(info),
24949
25201
  onSlow: (info) => process.stderr.write(
24950
25202
  `${formatMcpSlowToast({ name: info.serverName, p95Ms: info.p95Ms, sampleSize: info.sampleSize })}
@@ -24986,7 +25238,8 @@ async function chatCommand(opts) {
24986
25238
  spec: raw,
24987
25239
  toolCount: bridge.registeredNames.length,
24988
25240
  report,
24989
- client: mcp3
25241
+ host,
25242
+ bridgeEnv: bridge.env
24990
25243
  });
24991
25244
  } catch (err) {
24992
25245
  const reason = err.message;