@sage-protocol/pi-adapter 0.3.7 → 0.3.9

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.
@@ -44,6 +44,18 @@ function sagePassthroughEnv() {
44
44
  }
45
45
  return env;
46
46
  }
47
+ const SAGE_COMMAND_TIMEOUT_KILL_GRACE_MS = 500;
48
+ const SAGE_SOFT_COMMAND_LOG_COOLDOWN_MS = 60_000;
49
+ const SAGE_HOOK_TIMEOUTS = {
50
+ sessionContext: 4_000,
51
+ skillVisibility: 2_500,
52
+ suggest: 4_000,
53
+ capture: 1_500,
54
+ feedback: 1_500,
55
+ securityScan: 4_000,
56
+ resolveSkill: 4_000,
57
+ quality: 3_000,
58
+ };
47
59
  function formatSageCommand(command, args) {
48
60
  const quoted = [command, ...args].map((part) => {
49
61
  if (!part)
@@ -160,6 +172,7 @@ const DOMAINLESS_ACTION_DOMAIN_HINTS = {
160
172
  };
161
173
  const DEFAULT_SEARCH_ACTION_BY_DOMAIN = {
162
174
  prompts: "list",
175
+ behaviors: "recommend",
163
176
  skills: "list",
164
177
  builder: "recommend",
165
178
  governance: "list_subdaos",
@@ -170,6 +183,8 @@ const DEFAULT_SEARCH_ACTION_BY_DOMAIN = {
170
183
  security: "scan",
171
184
  meta: "get_project_context",
172
185
  hub: "status",
186
+ activation: "active",
187
+ wallet: "balance",
173
188
  library: "get_active",
174
189
  help: "list",
175
190
  external: "list_servers",
@@ -425,15 +440,23 @@ export async function defaultSageCommandRunner(request) {
425
440
  let stderr = "";
426
441
  let timedOut = false;
427
442
  let settled = false;
443
+ let killEscalation = null;
428
444
  const timeout = setTimeout(() => {
429
445
  timedOut = true;
430
446
  proc.kill("SIGTERM");
447
+ killEscalation = setTimeout(() => {
448
+ if (settled)
449
+ return;
450
+ proc.kill("SIGKILL");
451
+ }, SAGE_COMMAND_TIMEOUT_KILL_GRACE_MS);
431
452
  }, request.timeoutMs);
432
453
  const finish = (result) => {
433
454
  if (settled)
434
455
  return;
435
456
  settled = true;
436
457
  clearTimeout(timeout);
458
+ if (killEscalation)
459
+ clearTimeout(killEscalation);
437
460
  resolve(result);
438
461
  };
439
462
  const fail = (err) => {
@@ -441,6 +464,8 @@ export async function defaultSageCommandRunner(request) {
441
464
  return;
442
465
  settled = true;
443
466
  clearTimeout(timeout);
467
+ if (killEscalation)
468
+ clearTimeout(killEscalation);
444
469
  reject(err);
445
470
  };
446
471
  proc.on("error", (err) => {
@@ -638,6 +663,53 @@ function formatOperationSubject(request) {
638
663
  }
639
664
  return `${request.domain}/${request.action}`;
640
665
  }
666
+ function summarizeInventoryItem(value) {
667
+ if (typeof value === "string" && value.trim()) {
668
+ return truncateText(value.trim(), 96);
669
+ }
670
+ if (!value || typeof value !== "object")
671
+ return null;
672
+ const item = value;
673
+ const toolName = stringValue(item.tool_name);
674
+ const sessionId = stringValue(item.session_id);
675
+ const instanceId = stringValue(item.instance_id);
676
+ const serverId = stringValue(item.server_id) ?? stringValue(item.id) ?? instanceId;
677
+ if (toolName && serverId) {
678
+ return truncateText(`${toolName} @ ${serverId}`, 96);
679
+ }
680
+ if (instanceId) {
681
+ return truncateText(sessionId ? `${instanceId} (session ${sessionId})` : instanceId, 96);
682
+ }
683
+ const label = stringValue(item.title) ??
684
+ stringValue(item.name) ??
685
+ stringValue(item.key) ??
686
+ stringValue(item.id) ??
687
+ stringValue(item.url) ??
688
+ serverId;
689
+ if (!label)
690
+ return null;
691
+ const suffix = [];
692
+ if (typeof item.running === "boolean") {
693
+ suffix.push(item.running ? "running" : "stopped");
694
+ }
695
+ if (typeof item.enabled === "boolean") {
696
+ suffix.push(item.enabled ? "enabled" : "disabled");
697
+ }
698
+ if (typeof item.available === "boolean" && item.available === false) {
699
+ suffix.push("unavailable");
700
+ }
701
+ if (Array.isArray(item.tools)) {
702
+ suffix.push(`${item.tools.length} ${item.tools.length === 1 ? "tool" : "tools"}`);
703
+ }
704
+ else if (typeof item.tool_count === "number") {
705
+ suffix.push(`${item.tool_count} ${item.tool_count === 1 ? "tool" : "tools"}`);
706
+ }
707
+ const status = stringValue(item.status);
708
+ if (status && !suffix.includes(status))
709
+ suffix.push(status);
710
+ const decorated = suffix.length > 0 ? `${label} (${suffix.slice(0, 3).join(", ")})` : label;
711
+ return truncateText(decorated, 96);
712
+ }
641
713
  function formatOperationDetails(request) {
642
714
  const details = [];
643
715
  if (request.domain === "external") {
@@ -659,6 +731,7 @@ function formatOperationDetails(request) {
659
731
  const query = stringValue(request.params.query);
660
732
  const key = stringValue(request.params.key) ?? stringValue(request.params.name);
661
733
  const serverId = stringValue(request.params.server_id);
734
+ const sessionId = stringValue(request.params.session_id);
662
735
  const library = stringValue(request.params.library);
663
736
  if (query)
664
737
  details.push(`query: ${quotedText(query)}`);
@@ -666,6 +739,8 @@ function formatOperationDetails(request) {
666
739
  details.push(`key: ${truncateText(key, 84)}`);
667
740
  if (serverId)
668
741
  details.push(`server: ${serverId}`);
742
+ if (sessionId)
743
+ details.push(`session: ${truncateText(sessionId, 84)}`);
669
744
  if (library)
670
745
  details.push(`library: ${truncateText(library, 84)}`);
671
746
  return details;
@@ -675,6 +750,8 @@ function summarizePayloadItems(payload) {
675
750
  ["results", "results"],
676
751
  ["tools", "tools"],
677
752
  ["servers", "servers"],
753
+ ["running_servers", "servers"],
754
+ ["sessions", "sessions"],
678
755
  ["skills", "skills"],
679
756
  ["prompts", "prompts"],
680
757
  ["libraries", "libraries"],
@@ -682,26 +759,12 @@ function summarizePayloadItems(payload) {
682
759
  ["rooms", "rooms"],
683
760
  ["messages", "messages"],
684
761
  ];
685
- const getItemLabel = (value) => {
686
- if (typeof value === "string" && value.trim())
687
- return truncateText(value.trim(), 96);
688
- if (!value || typeof value !== "object")
689
- return null;
690
- const item = value;
691
- const label = stringValue(item.title) ??
692
- stringValue(item.name) ??
693
- stringValue(item.key) ??
694
- stringValue(item.id) ??
695
- stringValue(item.url) ??
696
- stringValue(item.server_id);
697
- return label ? truncateText(label, 96) : null;
698
- };
699
762
  if (Array.isArray(payload)) {
700
763
  return {
701
764
  count: payload.length,
702
765
  label: payload.length === 1 ? "item" : "items",
703
766
  items: payload
704
- .map(getItemLabel)
767
+ .map(summarizeInventoryItem)
705
768
  .filter((item) => Boolean(item))
706
769
  .slice(0, 4),
707
770
  };
@@ -717,7 +780,7 @@ function summarizePayloadItems(payload) {
717
780
  count: items.length,
718
781
  label: items.length === 1 ? label.replace(/s$/, "") : label,
719
782
  items: items
720
- .map(getItemLabel)
783
+ .map(summarizeInventoryItem)
721
784
  .filter((item) => Boolean(item))
722
785
  .slice(0, 4),
723
786
  };
@@ -755,24 +818,55 @@ function summarizeRenderedResult(request, details, isError) {
755
818
  return { status: "failed", preview };
756
819
  }
757
820
  const payload = extractJsonFromMcpResult(details) ?? details;
758
- const listSummary = summarizePayloadItems(payload);
759
- if (listSummary) {
760
- return {
761
- status: `${listSummary.count} ${listSummary.label}`,
762
- preview: listSummary.items,
763
- };
764
- }
765
821
  if (payload && typeof payload === "object") {
766
822
  const obj = payload;
823
+ const explicitStatus = stringValue(obj.status);
824
+ if (request?.domain === "hub" && request.action === "start") {
825
+ const tools = Array.isArray(obj.tools)
826
+ ? obj.tools
827
+ .map((item) => summarizeInventoryItem(item))
828
+ .filter((item) => Boolean(item))
829
+ .slice(0, 3)
830
+ : [];
831
+ const preview = [
832
+ ...tools,
833
+ ...[
834
+ stringValue(obj.hint),
835
+ stringValue(obj.message),
836
+ stringValue(obj.summary),
837
+ ]
838
+ .filter((item) => Boolean(item))
839
+ .map((item) => truncateText(item, 120)),
840
+ ].slice(0, 3);
841
+ return {
842
+ status: explicitStatus ?? defaultOperationStatus(request),
843
+ preview,
844
+ };
845
+ }
846
+ const listSummary = summarizePayloadItems(payload);
847
+ if (listSummary) {
848
+ const preview = listSummary.items.length
849
+ ? listSummary.items
850
+ : stringValue(obj.hint)
851
+ ? [truncateText(stringValue(obj.hint) ?? "", 120)]
852
+ : [];
853
+ return {
854
+ status: `${listSummary.count} ${listSummary.label}`,
855
+ preview,
856
+ };
857
+ }
767
858
  const message = stringValue(obj.message) ??
768
859
  stringValue(obj.summary) ??
769
- stringValue(obj.status);
860
+ stringValue(obj.hint);
770
861
  if (message) {
771
862
  return {
772
- status: defaultOperationStatus(request),
863
+ status: explicitStatus ?? defaultOperationStatus(request),
773
864
  preview: [truncateText(message, 120)],
774
865
  };
775
866
  }
867
+ if (explicitStatus) {
868
+ return { status: explicitStatus, preview: [] };
869
+ }
776
870
  if (obj.ok === true) {
777
871
  return { status: defaultOperationStatus(request), preview: [] };
778
872
  }
@@ -1155,25 +1249,65 @@ export class SageMcpBridge {
1155
1249
  export class SageHookRuntime {
1156
1250
  config;
1157
1251
  commandRunner;
1252
+ sessionContextCache = new Map();
1253
+ skillVisibilityCache = null;
1254
+ softFailureState = new Map();
1158
1255
  constructor(config) {
1159
1256
  this.config = config;
1160
1257
  this.commandRunner = config.commandRunner ?? defaultSageCommandRunner;
1161
1258
  }
1259
+ getSoftTimeout(maxTimeoutMs) {
1260
+ return Math.max(250, Math.min(this.config.timeoutMs, maxTimeoutMs));
1261
+ }
1262
+ clearSoftFailure(commandText) {
1263
+ this.softFailureState.delete(commandText);
1264
+ }
1265
+ logSoftFailure(commandText, message) {
1266
+ const now = Date.now();
1267
+ const previous = this.softFailureState.get(commandText);
1268
+ if (previous &&
1269
+ now - previous.lastLoggedAt < SAGE_SOFT_COMMAND_LOG_COOLDOWN_MS) {
1270
+ previous.suppressedCount += 1;
1271
+ this.softFailureState.set(commandText, previous);
1272
+ return;
1273
+ }
1274
+ const suffix = previous && previous.suppressedCount > 0
1275
+ ? `\n[sage-pi-adapter] Suppressed ${previous.suppressedCount} similar timeout/error log(s) for ${commandText}`
1276
+ : "";
1277
+ this.softFailureState.set(commandText, {
1278
+ lastLoggedAt: now,
1279
+ suppressedCount: 0,
1280
+ });
1281
+ console.warn(`${message}${suffix}`);
1282
+ }
1162
1283
  async getSessionContext(sessionId) {
1163
- const result = await this.run(["skill", "context", "--format", "pi"], { SAGE_SESSION_ID: sessionId ?? "" }, undefined, true);
1164
- return result?.stdout.trim() ?? "";
1284
+ const cacheKey = sessionId?.trim() || "__default__";
1285
+ const result = await this.run(["skill", "context", "--format", "pi"], { SAGE_SESSION_ID: sessionId ?? "" }, undefined, {
1286
+ failurePolicy: "warn",
1287
+ timeoutMs: this.getSoftTimeout(SAGE_HOOK_TIMEOUTS.sessionContext),
1288
+ });
1289
+ const context = result?.stdout.trim() ?? "";
1290
+ if (context) {
1291
+ this.sessionContextCache.set(cacheKey, context);
1292
+ return context;
1293
+ }
1294
+ return this.sessionContextCache.get(cacheKey) ?? "";
1165
1295
  }
1166
1296
  /**
1167
1297
  * Gather skill visibility summary for user-facing display.
1168
1298
  *
1169
1299
  * Runs `sage library list` and `sage library active` in parallel to
1170
1300
  * produce a compact summary of active libraries with skill counts.
1171
- * Returns empty summary when the CLI is unavailable.
1301
+ * Returns cached or empty summary when the CLI is unavailable.
1172
1302
  */
1173
1303
  async getSkillVisibilitySummary() {
1304
+ const softRunOptions = {
1305
+ failurePolicy: "warn",
1306
+ timeoutMs: this.getSoftTimeout(SAGE_HOOK_TIMEOUTS.skillVisibility),
1307
+ };
1174
1308
  const [libraryResult, activeResult] = await Promise.allSettled([
1175
- this.run(["library", "list"], {}, undefined, true),
1176
- this.run(["library", "active"], {}, undefined, true),
1309
+ this.run(["library", "list"], {}, undefined, softRunOptions),
1310
+ this.run(["library", "active"], {}, undefined, softRunOptions),
1177
1311
  ]);
1178
1312
  const libraries = [];
1179
1313
  const libraryOut = libraryResult.status === "fulfilled"
@@ -1192,7 +1326,6 @@ export class SageHookRuntime {
1192
1326
  }
1193
1327
  }
1194
1328
  const totalSkills = libraries.reduce((s, l) => s + l.skills, 0);
1195
- // --- Active libraries ---
1196
1329
  const activeOut = activeResult.status === "fulfilled"
1197
1330
  ? (activeResult.value?.stdout.trim() ?? "")
1198
1331
  : "";
@@ -1200,17 +1333,12 @@ export class SageHookRuntime {
1200
1333
  if (activeOut) {
1201
1334
  for (const line of activeOut.split("\n")) {
1202
1335
  if (line.includes("Skill-activated libraries"))
1203
- break; // stop at skill section
1336
+ break;
1204
1337
  const numMatch = line.match(/^\s*\d+\.\s+(\S+)/);
1205
1338
  if (numMatch)
1206
1339
  activeNames.push(numMatch[1]);
1207
1340
  }
1208
1341
  }
1209
- // --- Build compact display ---
1210
- // Format: "zenith (48s) · sunago (4p)" using middle dot as separator.
1211
- // Unicode glyphs get ASCII fallback when SAGE_NO_UNICODE=1.
1212
- // Compact to 3 libs max, with "+N more" if exceeded.
1213
- // Falls back to totals when active data is unavailable.
1214
1342
  let summary = "";
1215
1343
  if (activeNames.length > 0) {
1216
1344
  const maxShow = 3;
@@ -1222,7 +1350,7 @@ export class SageHookRuntime {
1222
1350
  return `${name} (${lib.prompts}p)`;
1223
1351
  return name;
1224
1352
  });
1225
- const SEP = unicodeSafe("\u00B7"); // middle dot or |
1353
+ const SEP = unicodeSafe("\u00B7");
1226
1354
  const suffix = activeNames.length > maxShow
1227
1355
  ? ` ${SEP} +${activeNames.length - maxShow} more`
1228
1356
  : "";
@@ -1232,7 +1360,16 @@ export class SageHookRuntime {
1232
1360
  summary = `${libraries.length} libs, ${totalSkills} skills`;
1233
1361
  }
1234
1362
  summary = summary ? `[Sage] ${summary}` : "";
1235
- return { summary, libraryCount: libraries.length, skillCount: totalSkills };
1363
+ const current = {
1364
+ summary,
1365
+ libraryCount: libraries.length,
1366
+ skillCount: totalSkills,
1367
+ };
1368
+ if (current.summary) {
1369
+ this.skillVisibilityCache = current;
1370
+ return current;
1371
+ }
1372
+ return this.skillVisibilityCache ?? current;
1236
1373
  }
1237
1374
  async suggestSkill(prompt, sessionId) {
1238
1375
  const trimmed = prompt.trim();
@@ -1252,7 +1389,10 @@ export class SageHookRuntime {
1252
1389
  SAGE_PROMPT: trimmed,
1253
1390
  SAGE_SOURCE: this.config.source ?? "pi",
1254
1391
  SAGE_SESSION_ID: sessionId ?? "",
1255
- }, JSON.stringify({ prompt: trimmed }), true);
1392
+ }, JSON.stringify({ prompt: trimmed }), {
1393
+ failurePolicy: "warn",
1394
+ timeoutMs: this.getSoftTimeout(SAGE_HOOK_TIMEOUTS.suggest),
1395
+ });
1256
1396
  return result?.stdout.trim() ?? "";
1257
1397
  }
1258
1398
  async capturePrompt(prompt, sessionId) {
@@ -1266,7 +1406,10 @@ export class SageHookRuntime {
1266
1406
  SAGE_PROMPT: trimmed,
1267
1407
  SAGE_SOURCE: this.config.source ?? "pi",
1268
1408
  SAGE_SESSION_ID: sessionId ?? "",
1269
- }, JSON.stringify({ prompt: trimmed }), true);
1409
+ }, JSON.stringify({ prompt: trimmed }), {
1410
+ failurePolicy: "silent",
1411
+ timeoutMs: this.getSoftTimeout(SAGE_HOOK_TIMEOUTS.capture),
1412
+ });
1270
1413
  }
1271
1414
  async captureResponse(response, usage, sessionId) {
1272
1415
  if (this.config.enableRlmFeedback === false)
@@ -1281,7 +1424,10 @@ export class SageHookRuntime {
1281
1424
  SAGE_SESSION_ID: sessionId ?? "",
1282
1425
  TOKENS_INPUT: usage.input ?? "",
1283
1426
  TOKENS_OUTPUT: usage.output ?? "",
1284
- }, JSON.stringify({ response: trimmed }), true);
1427
+ }, JSON.stringify({ response: trimmed }), {
1428
+ failurePolicy: "silent",
1429
+ timeoutMs: this.getSoftTimeout(SAGE_HOOK_TIMEOUTS.capture),
1430
+ });
1285
1431
  }
1286
1432
  async recordSuggestionUse(skillKeys, sessionId) {
1287
1433
  if (this.config.enableRlmFeedback === false)
@@ -1294,7 +1440,10 @@ export class SageHookRuntime {
1294
1440
  await this.run(["suggest", "feedback", "use", ...used.flatMap((key) => ["--used", key])], {
1295
1441
  SAGE_SOURCE: this.config.source ?? "pi",
1296
1442
  SAGE_SESSION_ID: sessionId ?? "",
1297
- }, undefined, true);
1443
+ }, undefined, {
1444
+ failurePolicy: "silent",
1445
+ timeoutMs: this.getSoftTimeout(SAGE_HOOK_TIMEOUTS.feedback),
1446
+ });
1298
1447
  }
1299
1448
  async recordSuggestionSkip(sessionId) {
1300
1449
  if (this.config.enableRlmFeedback === false)
@@ -1302,7 +1451,10 @@ export class SageHookRuntime {
1302
1451
  await this.run(["suggest", "feedback", "skip"], {
1303
1452
  SAGE_SOURCE: this.config.source ?? "pi",
1304
1453
  SAGE_SESSION_ID: sessionId ?? "",
1305
- }, undefined, true);
1454
+ }, undefined, {
1455
+ failurePolicy: "silent",
1456
+ timeoutMs: this.getSoftTimeout(SAGE_HOOK_TIMEOUTS.feedback),
1457
+ });
1306
1458
  }
1307
1459
  async recordExecutionOutcome(skillKey, status, queryPreview, sessionId, _model) {
1308
1460
  if (this.config.enableRlmFeedback === false)
@@ -1317,7 +1469,10 @@ export class SageHookRuntime {
1317
1469
  await this.run(args, {
1318
1470
  SAGE_SOURCE: this.config.source ?? "pi",
1319
1471
  SAGE_SESSION_ID: sessionId ?? "",
1320
- }, undefined, true);
1472
+ }, undefined, {
1473
+ failurePolicy: "silent",
1474
+ timeoutMs: this.getSoftTimeout(SAGE_HOOK_TIMEOUTS.feedback),
1475
+ });
1321
1476
  }
1322
1477
  async scanPreTool(toolName, toolInput, sessionId) {
1323
1478
  if (this.config.enableSecurityHooks === false)
@@ -1330,7 +1485,10 @@ export class SageHookRuntime {
1330
1485
  const result = await this.run(["security", "scan-hook"], {
1331
1486
  SAGE_SOURCE: this.config.source ?? "pi",
1332
1487
  SAGE_SESSION_ID: sessionId ?? "",
1333
- }, JSON.stringify(payload), true);
1488
+ }, JSON.stringify(payload), {
1489
+ failurePolicy: "warn",
1490
+ timeoutMs: this.getSoftTimeout(SAGE_HOOK_TIMEOUTS.securityScan),
1491
+ });
1334
1492
  return result ? parseSecurityDecision(result.stdout) : null;
1335
1493
  }
1336
1494
  async scanPostTool(toolName, toolInput, toolResult, sessionId) {
@@ -1345,7 +1503,10 @@ export class SageHookRuntime {
1345
1503
  const result = await this.run(["security", "scan-hook"], {
1346
1504
  SAGE_SOURCE: this.config.source ?? "pi",
1347
1505
  SAGE_SESSION_ID: sessionId ?? "",
1348
- }, JSON.stringify(payload), true);
1506
+ }, JSON.stringify(payload), {
1507
+ failurePolicy: "warn",
1508
+ timeoutMs: this.getSoftTimeout(SAGE_HOOK_TIMEOUTS.securityScan),
1509
+ });
1349
1510
  return result ? parseSecurityDecision(result.stdout) : null;
1350
1511
  }
1351
1512
  /**
@@ -1358,7 +1519,10 @@ export class SageHookRuntime {
1358
1519
  if (!names.length)
1359
1520
  return results;
1360
1521
  const settled = await Promise.allSettled(names.map(async (name) => {
1361
- const result = await this.run(["skill", "get", name, "--format", "raw"], { SAGE_SOURCE: this.config.source ?? "pi" }, undefined, true);
1522
+ const result = await this.run(["skill", "get", name, "--format", "raw"], { SAGE_SOURCE: this.config.source ?? "pi" }, undefined, {
1523
+ failurePolicy: "warn",
1524
+ timeoutMs: this.getSoftTimeout(SAGE_HOOK_TIMEOUTS.resolveSkill),
1525
+ });
1362
1526
  const content = result?.stdout.trim() ?? "";
1363
1527
  return { name, content };
1364
1528
  }));
@@ -1383,7 +1547,10 @@ export class SageHookRuntime {
1383
1547
  if (cached !== undefined)
1384
1548
  return cached;
1385
1549
  try {
1386
- const result = await this.run(["suggest", "feedback", "breakdown", skillKey, "--format", "json"], {}, undefined, true);
1550
+ const result = await this.run(["suggest", "feedback", "breakdown", skillKey, "--format", "json"], {}, undefined, {
1551
+ failurePolicy: "silent",
1552
+ timeoutMs: this.getSoftTimeout(SAGE_HOOK_TIMEOUTS.quality),
1553
+ });
1387
1554
  if (!result?.stdout) {
1388
1555
  this.qualityCache.set(skillKey, null);
1389
1556
  return null;
@@ -1416,8 +1583,9 @@ export class SageHookRuntime {
1416
1583
  async prefetchSkillQualities(keys) {
1417
1584
  await Promise.allSettled(keys.map((k) => this.querySkillQuality(k)));
1418
1585
  }
1419
- async run(args, env, stdin, ignoreFailures = false) {
1586
+ async run(args, env, stdin, options = {}) {
1420
1587
  const commandText = formatSageCommand(this.config.sageBin, args);
1588
+ const failurePolicy = options.failurePolicy ?? "throw";
1421
1589
  try {
1422
1590
  const commandEnv = {
1423
1591
  HOME: process.env.HOME ?? "",
@@ -1434,33 +1602,39 @@ export class SageHookRuntime {
1434
1602
  args,
1435
1603
  env: commandEnv,
1436
1604
  stdin,
1437
- timeoutMs: this.config.timeoutMs,
1605
+ timeoutMs: options.timeoutMs ?? this.config.timeoutMs,
1438
1606
  });
1439
1607
  if (result.timedOut || result.exitCode !== 0) {
1440
1608
  const detail = (result.stderr || result.stdout || "").trim();
1441
1609
  const message = result.timedOut
1442
1610
  ? `[sage-pi-adapter] Sage command timed out: ${commandText}`
1443
1611
  : `[sage-pi-adapter] Sage command failed (${result.exitCode}): ${commandText}${detail ? `\n${detail}` : ""}`;
1444
- if (ignoreFailures) {
1445
- console.error(message);
1446
- return result;
1612
+ if (failurePolicy === "throw") {
1613
+ throw new Error(message);
1447
1614
  }
1448
- throw new Error(message);
1615
+ if (failurePolicy === "warn") {
1616
+ this.logSoftFailure(commandText, message);
1617
+ }
1618
+ return null;
1449
1619
  }
1620
+ this.clearSoftFailure(commandText);
1450
1621
  return result;
1451
1622
  }
1452
1623
  catch (error) {
1453
1624
  const detail = error instanceof Error ? error.message : String(error);
1454
1625
  const message = `[sage-pi-adapter] Sage command error: ${commandText}${detail ? `\n${detail}` : ""}`;
1455
- if (!ignoreFailures)
1626
+ if (failurePolicy === "throw")
1456
1627
  throw new Error(message);
1457
- console.error(message);
1628
+ if (failurePolicy === "warn") {
1629
+ this.logSoftFailure(commandText, message);
1630
+ }
1458
1631
  return null;
1459
1632
  }
1460
1633
  }
1461
1634
  }
1462
1635
  const SageDomainType = Type.Union([
1463
1636
  Type.Literal("prompts"),
1637
+ Type.Literal("behaviors"),
1464
1638
  Type.Literal("skills"),
1465
1639
  Type.Literal("builder"),
1466
1640
  Type.Literal("governance"),
@@ -1471,6 +1645,8 @@ const SageDomainType = Type.Union([
1471
1645
  Type.Literal("security"),
1472
1646
  Type.Literal("meta"),
1473
1647
  Type.Literal("hub"),
1648
+ Type.Literal("activation"),
1649
+ Type.Literal("wallet"),
1474
1650
  Type.Literal("library"),
1475
1651
  Type.Literal("help"),
1476
1652
  Type.Literal("external"),
@@ -1479,20 +1655,18 @@ export function createSageSearchTool(mcpBridge) {
1479
1655
  return {
1480
1656
  name: "sage_search",
1481
1657
  label: "Sage: Search",
1482
- promptSnippet: "Search the web, documentation, prompts, skills, and external services via Sage MCP bridge",
1658
+ promptSnippet: "Route read-only Sage code-mode requests: discover domains/actions, inspect context, query hub/external tools, and search capabilities",
1483
1659
  promptGuidelines: [
1484
- "Use sage_search (NOT bash sage commands) for web searches, documentation lookups, and prompt/skill discovery",
1485
- "NEVER run 'sage' CLI via bash -- always use the sage_search or sage_execute tools instead",
1486
- "For web search: sage_search({ domain: 'external', action: 'search', params: { tool_name: 'web_search', tool_params: { query: '...' } } })",
1487
- "To list available tools: sage_search({ domain: 'external', action: 'list_servers' })",
1660
+ "Use sage_search (NOT bash sage commands) for Sage read/discovery flows such as help.list, prompts.search, behaviors.show, meta.get_project_context, meta.suggest, hub.list_servers/status, wallet.balance/address, and external.search",
1661
+ "Use sage_search({ domain: 'help', action: 'list', params: {} }) before guessing unknown Sage domains or actions",
1662
+ "Use sage_search({ domain: 'hub', action: 'list_servers', params: {} }) for MCP server inventory; use domain='external', action='list_servers' only to inspect running external servers and tool exposure",
1663
+ "If Sage says an action is a mutation or non-search inspect action, retry with sage_execute instead of falling back to bash",
1488
1664
  ],
1489
- description: "Search the web, find documentation, search prompts/skills, and query external services. " +
1490
- "For web search: domain='external', action='search', params={tool_name:'web_search', tool_params:{query:'your query'}}. " +
1491
- "For prompt search: domain='prompts', action='search', params={query:'search terms'}. " +
1492
- "For skill search: domain='skills', action='search', params={query:'search terms'}. " +
1493
- "For GitHub code search: domain='external', action='search', params={tool_name:'github_search_code', tool_params:{query:'...'}}. " +
1494
- "For library docs: domain='external', action='search', params={tool_name:'get_library_docs', tool_params:{context7CompatibleLibraryID:'/lib/id'}}. " +
1495
- "To list available external servers/tools: domain='external', action='list_servers'.",
1665
+ description: "Route read-only Sage code-mode requests across prompts, behaviors, skills, builder, governance, chat, social, RLM, security, meta, hub, activation, wallet, library, help, and external services. " +
1666
+ "Start with domain='help', action='list' when you do not know the route. " +
1667
+ "Use domain='meta', action='get_project_context' for local Sage state and domain='meta', action='suggest' with params={goal:'...'} when you need route suggestions. " +
1668
+ "Use domain='hub' for MCP hub inventory/status and domain='external' for external tool discovery/search. " +
1669
+ "Some heavy inspect actions such as prompts.get and skills.get are classified by Sage as non-search and should use sage_execute.",
1496
1670
  parameters: Type.Object({
1497
1671
  domain: Type.Optional(SageDomainType),
1498
1672
  action: Type.Optional(Type.String()),
@@ -1543,16 +1717,15 @@ export function createSageExecuteTool(mcpBridge) {
1543
1717
  return {
1544
1718
  name: "sage_execute",
1545
1719
  label: "Sage: Execute",
1546
- promptSnippet: "Execute mutations: call external tools, use pi's hub-compat layer, create prompts, fetch URLs",
1720
+ promptSnippet: "Execute Sage mutations and non-search inspect actions: activate skills, fetch full prompt/skill bodies, manage hub servers, and call external tools",
1547
1721
  promptGuidelines: [
1548
- "Use sage_execute (NOT bash sage commands) for calling external tools, fetching URLs, and managing MCP servers",
1722
+ "Use sage_execute (NOT bash sage commands) for Sage mutations and for Sage actions that the code-mode router classifies as non-search, including prompts.get, skills.get/use, behaviors.run, activation.activate/deactivate, library writes, hub.start/stop, and external.call/execute",
1723
+ "After starting an MCP server with sage_execute({ domain: 'hub', action: 'start', params: { server_id: '...' } }), call its tools through domain='external' with params { tool_name, tool_params } instead of invoking sage via bash",
1549
1724
  "To fetch a URL: sage_execute({ domain: 'external', action: 'call', params: { tool_name: 'fetch', tool_params: { url: 'https://...' } } })",
1550
1725
  ],
1551
- description: "Execute mutations: create prompts, use skills, send chat messages, call external tools, and manage external MCP servers through pi's hub-compat wrapper. " +
1552
- "Preferred raw MCP clients use direct hub_* tools; in pi use domain='hub', action='start', params={server_id:'duckduckgo'} when needed. " +
1553
- "To use a skill: domain='skills', action='use', params={name:'skill-name'}. " +
1554
- "To call an external tool: domain='external', action='call', params={tool_name:'tool_name', tool_params:{...}}. " +
1555
- "To fetch a URL: domain='external', action='call', params={tool_name:'fetch', tool_params:{url:'https://example.com'}}.",
1726
+ description: "Execute Sage mutations and non-search inspect actions across prompts, behaviors, skills, builder, chat, library, activation, hub, and external tool surfaces. " +
1727
+ "Use this for prompts.get, skills.get/use, behaviors.run, activation.activate, library.prompt_add/skill_add, hub.start/stop, and external.call/execute. " +
1728
+ "Use domain='external', action='call' with params={tool_name:'fetch', tool_params:{url:'https://example.com'}} for URL fetches, and use domain='hub' to start or stop MCP servers before calling their tools.",
1556
1729
  parameters: Type.Object({
1557
1730
  domain: Type.Optional(SageDomainType),
1558
1731
  action: Type.Optional(Type.String()),
@@ -1585,34 +1758,7 @@ export function createSageExecuteTool(mcpBridge) {
1585
1758
  return toToolResultText(result);
1586
1759
  }
1587
1760
  catch (error) {
1588
- let errorMsg = error instanceof Error ? error.message : String(error);
1589
- // Detect hub start failures due to missing credentials and add helpful context
1590
- try {
1591
- const normalized = normalizeCodeModeRequest("execute", params);
1592
- if (normalized.domain === "hub" &&
1593
- normalized.action === "start" &&
1594
- /EOF|empty|parse|unexpected end/i.test(errorMsg)) {
1595
- const serverId = String(normalized.params.server_id ?? "unknown");
1596
- const knownKeyRequirements = {
1597
- "brave-search": "BRAVE_API_KEY",
1598
- github: "GITHUB_TOKEN",
1599
- };
1600
- const requiredKey = knownKeyRequirements[serverId];
1601
- if (requiredKey) {
1602
- errorMsg +=
1603
- `. Server '${serverId}' likely failed because ${requiredKey} is not configured. ` +
1604
- `Set it with: sage mcp hub env set ${serverId} ${requiredKey}=<your-key>`;
1605
- }
1606
- else {
1607
- errorMsg +=
1608
- `. Server '${serverId}' may require environment variables (API keys). ` +
1609
- `Check with: sage mcp hub env list ${serverId}`;
1610
- }
1611
- }
1612
- }
1613
- catch {
1614
- // normalizeCodeModeRequest can throw -- ignore, keep original errorMsg
1615
- }
1761
+ const errorMsg = augmentHubStartErrorMessage(params, error instanceof Error ? error.message : String(error));
1616
1762
  return {
1617
1763
  content: [
1618
1764
  {
@@ -1627,21 +1773,269 @@ export function createSageExecuteTool(mcpBridge) {
1627
1773
  };
1628
1774
  }
1629
1775
  const SAGE_CLI_REFERENCE = `
1630
- ## Sage CLI Reference (on-demand)
1776
+ ## Sage Code-Mode Reference
1631
1777
 
1632
- ### Skill Discovery
1633
- - sage_search({ domain: "skills", action: "search", params: { query: "..." } })
1634
- - sage_search({ domain: "skills", action: "get", params: { key: "..." } })
1778
+ ### Route Discovery
1779
+ - List domains/actions: sage_search({ domain: "help", action: "list", params: {} })
1780
+ - Suggest the right route: sage_search({ domain: "meta", action: "suggest", params: { goal: "..." } })
1781
+ - Inspect local Sage state: sage_search({ domain: "meta", action: "get_project_context", params: {} })
1635
1782
 
1636
- ### External Tools
1637
- - Web search: sage_search({ domain: "external", action: "search", params: { tool_name: "web_search", tool_params: { query: "..." } } })
1638
- - URL fetch: sage_execute({ domain: "external", action: "call", params: { tool_name: "fetch", tool_params: { url: "https://..." } } })
1639
- - List tools: sage_search({ domain: "external", action: "list_servers" })
1783
+ ### Skills / Prompts / Behaviors
1784
+ - Search skills: sage_search({ domain: "skills", action: "search", params: { query: "..." } })
1785
+ - Inspect skill body: sage_execute({ domain: "skills", action: "get", params: { key: "..." } })
1786
+ - Activate skill: sage_execute({ domain: "skills", action: "use", params: { key: "..." } })
1787
+ - Search prompts: sage_search({ domain: "prompts", action: "search", params: { query: "..." } })
1788
+ - Inspect prompt body: sage_execute({ domain: "prompts", action: "get", params: { key: "..." } })
1789
+ - Inspect behavior: sage_search({ domain: "behaviors", action: "show", params: { key: "..." } })
1640
1790
 
1641
- ### Hub
1642
- - Start server: sage_execute({ domain: "hub", action: "start", params: { server_id: "brave-search" } })
1643
- - Stop server: sage_execute({ domain: "hub", action: "stop", params: { server_id: "brave-search" } })
1791
+ ### Hub / External Tools
1792
+ - List MCP servers: sage_search({ domain: "hub", action: "list_servers", params: {} })
1793
+ - Hub status: sage_search({ domain: "hub", action: "status", params: {} })
1794
+ - Start server: sage_execute({ domain: "hub", action: "start", params: { server_id: "github" } })
1795
+ - Running external servers/tools: sage_search({ domain: "external", action: "list_servers", params: {} })
1796
+ - Web search: sage_search({ domain: "external", action: "search", params: { tool_name: "web_search", tool_params: { query: "..." } } })
1797
+ - URL fetch: sage_execute({ domain: "external", action: "call", params: { tool_name: "fetch", tool_params: { url: "https://..." } } })
1644
1798
  `.trim();
1799
+ function buildHubRenderContext(action, params, context) {
1800
+ return {
1801
+ ...(context && typeof context === "object" ? context : {}),
1802
+ args: {
1803
+ domain: "hub",
1804
+ action,
1805
+ params,
1806
+ },
1807
+ };
1808
+ }
1809
+ function augmentHubStartErrorMessage(params, errorMsg) {
1810
+ try {
1811
+ const normalized = normalizeCodeModeRequest("execute", params);
1812
+ if (normalized.domain === "hub" &&
1813
+ normalized.action === "start" &&
1814
+ /EOF|empty|parse|unexpected end/i.test(errorMsg)) {
1815
+ const serverId = String(normalized.params.server_id ?? "unknown");
1816
+ const knownKeyRequirements = {
1817
+ "brave-search": "BRAVE_API_KEY",
1818
+ github: "GITHUB_TOKEN",
1819
+ };
1820
+ const requiredKey = knownKeyRequirements[serverId];
1821
+ if (requiredKey) {
1822
+ return (errorMsg +
1823
+ `. Server '${serverId}' likely failed because ${requiredKey} is not configured. ` +
1824
+ `Set it with: sage mcp hub env set ${serverId} ${requiredKey}=<your-key>`);
1825
+ }
1826
+ return (errorMsg +
1827
+ `. Server '${serverId}' may require environment variables (API keys). ` +
1828
+ `Check with: sage mcp hub env list ${serverId}`);
1829
+ }
1830
+ }
1831
+ catch {
1832
+ // Keep original error text if normalization fails.
1833
+ }
1834
+ return errorMsg;
1835
+ }
1836
+ export function createHubListServersTool(mcpBridge) {
1837
+ return {
1838
+ name: "hub_list_servers",
1839
+ label: "Hub: List Servers",
1840
+ promptSnippet: "List Sage MCP servers directly when you need inventory/status without composing a generic sage_search call",
1841
+ promptGuidelines: [
1842
+ "Prefer hub_list_servers() for simple MCP server inventory in Pi; it is the direct convenience wrapper over domain='hub', action='list_servers'",
1843
+ "Use sage_search({ domain: 'external', action: 'list_servers', params: {} }) only when you specifically need running external servers/tools rather than the full Sage hub inventory",
1844
+ ],
1845
+ description: "List all Sage MCP servers with running/available state. This is a Pi-native convenience wrapper for sage_search({ domain:'hub', action:'list_servers', params:{} }).",
1846
+ parameters: Type.Object({}),
1847
+ renderCall: (_args, theme) => renderSageToolCall("search", { domain: "hub", action: "list_servers", params: {} }, theme),
1848
+ renderResult: (result, options, theme, context) => renderSageToolResult("search", result, options, theme, buildHubRenderContext("list_servers", {}, context)),
1849
+ execute: async () => {
1850
+ try {
1851
+ const result = await callCodeModeWithWebResilience(mcpBridge, "search", {
1852
+ domain: "hub",
1853
+ action: "list_servers",
1854
+ params: {},
1855
+ });
1856
+ return toToolResultText(result);
1857
+ }
1858
+ catch (error) {
1859
+ return {
1860
+ content: [
1861
+ {
1862
+ type: "text",
1863
+ text: `hub_list_servers failed: ${error instanceof Error ? error.message : String(error)}`,
1864
+ },
1865
+ ],
1866
+ isError: true,
1867
+ };
1868
+ }
1869
+ },
1870
+ };
1871
+ }
1872
+ export function createHubStatusTool(mcpBridge) {
1873
+ return {
1874
+ name: "hub_status",
1875
+ label: "Hub: Status",
1876
+ promptSnippet: "Inspect Sage MCP hub runtime status directly when you want running server state without composing a generic sage_search call",
1877
+ promptGuidelines: [
1878
+ "Prefer hub_status() for simple MCP runtime status in Pi; it is the direct convenience wrapper over domain='hub', action='status'",
1879
+ "Use sage_status() when you need full adapter health plus Sage context, not just hub runtime state",
1880
+ ],
1881
+ description: "Inspect Sage MCP hub runtime status. This is a Pi-native convenience wrapper for sage_search({ domain:'hub', action:'status', params:{} }).",
1882
+ parameters: Type.Object({}),
1883
+ renderCall: (_args, theme) => renderSageToolCall("search", { domain: "hub", action: "status", params: {} }, theme),
1884
+ renderResult: (result, options, theme, context) => renderSageToolResult("search", result, options, theme, buildHubRenderContext("status", {}, context)),
1885
+ execute: async () => {
1886
+ try {
1887
+ const result = await callCodeModeWithWebResilience(mcpBridge, "search", {
1888
+ domain: "hub",
1889
+ action: "status",
1890
+ params: {},
1891
+ });
1892
+ return toToolResultText(result);
1893
+ }
1894
+ catch (error) {
1895
+ return {
1896
+ content: [
1897
+ {
1898
+ type: "text",
1899
+ text: `hub_status failed: ${error instanceof Error ? error.message : String(error)}`,
1900
+ },
1901
+ ],
1902
+ isError: true,
1903
+ };
1904
+ }
1905
+ },
1906
+ };
1907
+ }
1908
+ export function createHubSessionListTool(mcpBridge) {
1909
+ return {
1910
+ name: "hub_session_list",
1911
+ label: "Hub: Session List",
1912
+ promptSnippet: "List active Sage MCP hub sessions directly when you need isolated instance visibility without composing a generic sage_search call",
1913
+ promptGuidelines: [
1914
+ "Prefer hub_session_list() for simple hub session inspection in Pi; it wraps domain='hub', action='session_list'",
1915
+ "Pass server_id when you want to filter sessions for one server, and use hub_stop_server with server_id plus session_id to stop a specific isolated session",
1916
+ ],
1917
+ description: "List active Sage MCP hub sessions, optionally filtered by server_id. This is a Pi-native convenience wrapper for sage_search({ domain:'hub', action:'session_list', params:{ server_id? } }).",
1918
+ parameters: Type.Object({
1919
+ server_id: Type.Optional(Type.String({
1920
+ description: "Optional hub server identifier to filter sessions",
1921
+ })),
1922
+ }),
1923
+ renderCall: (args, theme) => renderSageToolCall("search", {
1924
+ domain: "hub",
1925
+ action: "session_list",
1926
+ params: normalizeParams(args),
1927
+ }, theme),
1928
+ renderResult: (result, options, theme, context) => renderSageToolResult("search", result, options, theme, buildHubRenderContext("session_list", normalizeParams(context?.args), context)),
1929
+ execute: async (_toolCallId, params) => {
1930
+ try {
1931
+ const payload = normalizeParams(params);
1932
+ const result = await callCodeModeWithWebResilience(mcpBridge, "search", {
1933
+ domain: "hub",
1934
+ action: "session_list",
1935
+ params: payload,
1936
+ });
1937
+ return toToolResultText(result);
1938
+ }
1939
+ catch (error) {
1940
+ return {
1941
+ content: [
1942
+ {
1943
+ type: "text",
1944
+ text: `hub_session_list failed: ${error instanceof Error ? error.message : String(error)}`,
1945
+ },
1946
+ ],
1947
+ isError: true,
1948
+ };
1949
+ }
1950
+ },
1951
+ };
1952
+ }
1953
+ export function createHubStopServerTool(mcpBridge) {
1954
+ return {
1955
+ name: "hub_stop_server",
1956
+ label: "Hub: Stop Server",
1957
+ promptSnippet: "Stop a Sage MCP server directly when you know the server_id and want to end its runtime cleanly",
1958
+ promptGuidelines: [
1959
+ "Prefer hub_stop_server({ server_id: '...' }) for simple MCP server shutdown in Pi; it wraps domain='hub', action='stop'",
1960
+ "Use hub_status() or hub_list_servers() after stopping a server if you need to confirm the resulting hub state",
1961
+ ],
1962
+ description: "Stop a Sage MCP server by server_id. This is a Pi-native convenience wrapper for sage_execute({ domain:'hub', action:'stop', params:{ server_id:'...' } }).",
1963
+ parameters: Type.Object({
1964
+ server_id: Type.String({
1965
+ description: "Hub server identifier to stop, such as github or brave-search",
1966
+ }),
1967
+ session_id: Type.Optional(Type.String({
1968
+ description: "Optional hub session identifier for isolated instances",
1969
+ })),
1970
+ }),
1971
+ renderCall: (args, theme) => renderSageToolCall("execute", { domain: "hub", action: "stop", params: normalizeParams(args) }, theme),
1972
+ renderResult: (result, options, theme, context) => renderSageToolResult("execute", result, options, theme, buildHubRenderContext("stop", normalizeParams(context?.args), context)),
1973
+ execute: async (_toolCallId, params) => {
1974
+ try {
1975
+ const payload = normalizeParams(params);
1976
+ const result = await callCodeModeWithWebResilience(mcpBridge, "execute", {
1977
+ domain: "hub",
1978
+ action: "stop",
1979
+ params: payload,
1980
+ });
1981
+ return toToolResultText(result);
1982
+ }
1983
+ catch (error) {
1984
+ return {
1985
+ content: [
1986
+ {
1987
+ type: "text",
1988
+ text: `hub_stop_server failed: ${error instanceof Error ? error.message : String(error)}`,
1989
+ },
1990
+ ],
1991
+ isError: true,
1992
+ };
1993
+ }
1994
+ },
1995
+ };
1996
+ }
1997
+ export function createHubStartServerTool(mcpBridge) {
1998
+ return {
1999
+ name: "hub_start_server",
2000
+ label: "Hub: Start Server",
2001
+ promptSnippet: "Start a Sage MCP server directly when you already know the server_id and want Pi to expose its tools",
2002
+ promptGuidelines: [
2003
+ "Prefer hub_start_server({ server_id: '...' }) for simple MCP server startup in Pi; it wraps domain='hub', action='start'",
2004
+ "After starting an external MCP server, call its tools through sage_execute/sage_search with domain='external' and params { tool_name, tool_params }",
2005
+ ],
2006
+ description: "Start a Sage MCP server by server_id. This is a Pi-native convenience wrapper for sage_execute({ domain:'hub', action:'start', params:{ server_id:'...' } }).",
2007
+ parameters: Type.Object({
2008
+ server_id: Type.String({
2009
+ description: "Hub server identifier to start, such as github or brave-search",
2010
+ }),
2011
+ session_id: Type.Optional(Type.String({
2012
+ description: "Optional hub session identifier for isolated instances",
2013
+ })),
2014
+ }),
2015
+ renderCall: (args, theme) => renderSageToolCall("execute", { domain: "hub", action: "start", params: normalizeParams(args) }, theme),
2016
+ renderResult: (result, options, theme, context) => renderSageToolResult("execute", result, options, theme, buildHubRenderContext("start", normalizeParams(context?.args), context)),
2017
+ execute: async (_toolCallId, params) => {
2018
+ try {
2019
+ const payload = normalizeParams(params);
2020
+ const result = await callCodeModeWithWebResilience(mcpBridge, "execute", {
2021
+ domain: "hub",
2022
+ action: "start",
2023
+ params: payload,
2024
+ });
2025
+ return toToolResultText(result);
2026
+ }
2027
+ catch (error) {
2028
+ const errorMsg = augmentHubStartErrorMessage({ domain: "hub", action: "start", params }, error instanceof Error ? error.message : String(error));
2029
+ return {
2030
+ content: [
2031
+ { type: "text", text: `hub_start_server failed: ${errorMsg}` },
2032
+ ],
2033
+ isError: true,
2034
+ };
2035
+ }
2036
+ },
2037
+ };
2038
+ }
1645
2039
  export function createSageStatusTool(mcpBridge) {
1646
2040
  return {
1647
2041
  name: "sage_status",
@@ -1791,6 +2185,11 @@ export function createSageExtensions(options) {
1791
2185
  console.error("[sage-pi-adapter] failed to start MCP bridge:", error);
1792
2186
  });
1793
2187
  const sageTools = [
2188
+ createHubListServersTool(mcpBridge),
2189
+ createHubStatusTool(mcpBridge),
2190
+ createHubSessionListTool(mcpBridge),
2191
+ createHubStartServerTool(mcpBridge),
2192
+ createHubStopServerTool(mcpBridge),
1794
2193
  createSageSearchTool(mcpBridge),
1795
2194
  createSageExecuteTool(mcpBridge),
1796
2195
  createSageStatusTool(mcpBridge),