@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.
- package/README.md +5 -3
- package/dist/sage-pi-adapter.d.ts +13 -2
- package/dist/sage-pi-adapter.d.ts.map +1 -1
- package/dist/sage-pi-adapter.js +514 -115
- package/dist/sage-pi-adapter.js.map +1 -1
- package/extensions/index.ts +1297 -1085
- package/package.json +1 -5
- package/skills/sage-bridge/SKILL.md +0 -327
package/dist/sage-pi-adapter.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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.
|
|
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
|
|
1164
|
-
|
|
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,
|
|
1176
|
-
this.run(["library", "active"], {}, undefined,
|
|
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;
|
|
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");
|
|
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
|
-
|
|
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 }),
|
|
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 }),
|
|
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 }),
|
|
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,
|
|
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,
|
|
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,
|
|
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),
|
|
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),
|
|
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,
|
|
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,
|
|
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,
|
|
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 (
|
|
1445
|
-
|
|
1446
|
-
return result;
|
|
1612
|
+
if (failurePolicy === "throw") {
|
|
1613
|
+
throw new Error(message);
|
|
1447
1614
|
}
|
|
1448
|
-
|
|
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 (
|
|
1626
|
+
if (failurePolicy === "throw")
|
|
1456
1627
|
throw new Error(message);
|
|
1457
|
-
|
|
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: "
|
|
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
|
|
1485
|
-
"
|
|
1486
|
-
"
|
|
1487
|
-
"
|
|
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: "
|
|
1490
|
-
"
|
|
1491
|
-
"
|
|
1492
|
-
"
|
|
1493
|
-
"
|
|
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:
|
|
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
|
|
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
|
|
1552
|
-
"
|
|
1553
|
-
"
|
|
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
|
-
|
|
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
|
|
1776
|
+
## Sage Code-Mode Reference
|
|
1631
1777
|
|
|
1632
|
-
###
|
|
1633
|
-
- sage_search({ domain: "
|
|
1634
|
-
- sage_search({ domain: "
|
|
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
|
-
###
|
|
1637
|
-
-
|
|
1638
|
-
-
|
|
1639
|
-
-
|
|
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
|
-
-
|
|
1643
|
-
-
|
|
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),
|