@sage-protocol/pi-adapter 0.3.6 → 0.3.8
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 -2
- package/dist/behavior/pi-executor.d.ts +0 -1
- package/dist/behavior/pi-executor.d.ts.map +1 -1
- package/dist/behavior/pi-executor.js +8 -3
- package/dist/behavior/pi-executor.js.map +1 -1
- package/dist/behavior/resolver.d.ts.map +1 -1
- package/dist/behavior/resolver.js +13 -5
- package/dist/behavior/resolver.js.map +1 -1
- package/dist/sage-pi-adapter.d.ts +12 -2
- package/dist/sage-pi-adapter.d.ts.map +1 -1
- package/dist/sage-pi-adapter.js +652 -79
- 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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
4
6
|
// ── Unicode glyph fallback ──
|
|
5
7
|
// Some terminals lack glyph support for arrows, checkmarks, and dotted separators.
|
|
6
8
|
// Set SAGE_NO_UNICODE=1 to force ASCII-safe output in widget, summary, and quality icons.
|
|
@@ -46,9 +48,7 @@ function formatSageCommand(command, args) {
|
|
|
46
48
|
const quoted = [command, ...args].map((part) => {
|
|
47
49
|
if (!part)
|
|
48
50
|
return "''";
|
|
49
|
-
return /[\s"'`$\\]/.test(part)
|
|
50
|
-
? JSON.stringify(part)
|
|
51
|
-
: part;
|
|
51
|
+
return /[\s"'`$\\]/.test(part) ? JSON.stringify(part) : part;
|
|
52
52
|
});
|
|
53
53
|
return quoted.join(" ");
|
|
54
54
|
}
|
|
@@ -101,7 +101,10 @@ const EXTERNAL_EXECUTE_ACTIONS = new Set(["execute", "call"]);
|
|
|
101
101
|
const EXTERNAL_TOOL_ALIASES = {
|
|
102
102
|
web_search: "duckduckgo_web_search",
|
|
103
103
|
};
|
|
104
|
-
const WEB_SEARCH_PROVIDER_TOOLS = new Set([
|
|
104
|
+
const WEB_SEARCH_PROVIDER_TOOLS = new Set([
|
|
105
|
+
"duckduckgo_web_search",
|
|
106
|
+
"brave_web_search",
|
|
107
|
+
]);
|
|
105
108
|
const WEB_SEARCH_RETRY_PATTERNS = [
|
|
106
109
|
/ddg detected an anomaly/i,
|
|
107
110
|
/too quickly/i,
|
|
@@ -119,7 +122,8 @@ const WEB_SEARCH_MIN_INTERVAL_MS = 250;
|
|
|
119
122
|
const WEB_SEARCH_RETRY_BASE_MS = 250;
|
|
120
123
|
const LAST_WEB_SEARCH_CALL_BY_PROVIDER = new Map();
|
|
121
124
|
const DAEMON_IPC_MAX_ATTEMPTS = Math.max(1, Number.parseInt(process.env.SAGE_DAEMON_IPC_MAX_ATTEMPTS || "3", 10) || 3);
|
|
122
|
-
const DAEMON_IPC_RETRY_BASE_MS = Math.max(50, Number.parseInt(process.env.SAGE_DAEMON_IPC_RETRY_BASE_MS || "350", 10) ||
|
|
125
|
+
const DAEMON_IPC_RETRY_BASE_MS = Math.max(50, Number.parseInt(process.env.SAGE_DAEMON_IPC_RETRY_BASE_MS || "350", 10) ||
|
|
126
|
+
350);
|
|
123
127
|
const DOMAINLESS_ACTION_DOMAIN_HINTS = {
|
|
124
128
|
search: "prompts",
|
|
125
129
|
list: "prompts",
|
|
@@ -156,6 +160,7 @@ const DOMAINLESS_ACTION_DOMAIN_HINTS = {
|
|
|
156
160
|
};
|
|
157
161
|
const DEFAULT_SEARCH_ACTION_BY_DOMAIN = {
|
|
158
162
|
prompts: "list",
|
|
163
|
+
behaviors: "recommend",
|
|
159
164
|
skills: "list",
|
|
160
165
|
builder: "recommend",
|
|
161
166
|
governance: "list_subdaos",
|
|
@@ -166,6 +171,8 @@ const DEFAULT_SEARCH_ACTION_BY_DOMAIN = {
|
|
|
166
171
|
security: "scan",
|
|
167
172
|
meta: "get_project_context",
|
|
168
173
|
hub: "status",
|
|
174
|
+
activation: "active",
|
|
175
|
+
wallet: "balance",
|
|
169
176
|
library: "get_active",
|
|
170
177
|
help: "list",
|
|
171
178
|
external: "list_servers",
|
|
@@ -280,7 +287,9 @@ async function callWithDaemonRetry(call, args) {
|
|
|
280
287
|
await wait(daemonRetryBackoffMs(attempt));
|
|
281
288
|
}
|
|
282
289
|
}
|
|
283
|
-
throw lastError instanceof Error
|
|
290
|
+
throw lastError instanceof Error
|
|
291
|
+
? lastError
|
|
292
|
+
: new Error(String(lastError ?? "unknown daemon IPC failure"));
|
|
284
293
|
}
|
|
285
294
|
function withExternalToolName(request, toolName) {
|
|
286
295
|
return {
|
|
@@ -310,7 +319,8 @@ async function callCodeModeWithWebResilience(mcpBridge, mode, request) {
|
|
|
310
319
|
}
|
|
311
320
|
catch (error) {
|
|
312
321
|
failures.push(`${provider} (attempt ${attempt}): ${toErrorMessage(error)}`);
|
|
313
|
-
const shouldRetry = isTransientWebSearchError(error) &&
|
|
322
|
+
const shouldRetry = isTransientWebSearchError(error) &&
|
|
323
|
+
attempt < WEB_SEARCH_MAX_ATTEMPTS_PER_PROVIDER;
|
|
314
324
|
if (shouldRetry) {
|
|
315
325
|
await wait(retryBackoffMs(attempt));
|
|
316
326
|
continue;
|
|
@@ -346,7 +356,8 @@ function normalizeCodeModeRequest(mode, args) {
|
|
|
346
356
|
if (!rawAction) {
|
|
347
357
|
if (mode === "search") {
|
|
348
358
|
const fallbackDomain = rawDomain || "help";
|
|
349
|
-
const fallbackAction = DEFAULT_SEARCH_ACTION_BY_DOMAIN[fallbackDomain] ||
|
|
359
|
+
const fallbackAction = DEFAULT_SEARCH_ACTION_BY_DOMAIN[fallbackDomain] ||
|
|
360
|
+
DEFAULT_SEARCH_ACTION_BY_DOMAIN.help;
|
|
350
361
|
return {
|
|
351
362
|
domain: fallbackDomain,
|
|
352
363
|
action: fallbackAction,
|
|
@@ -585,8 +596,314 @@ function appendText(result, text) {
|
|
|
585
596
|
existing.push({ type: "text", text });
|
|
586
597
|
return { ...result, content: existing };
|
|
587
598
|
}
|
|
599
|
+
function createTextComponent(text) {
|
|
600
|
+
for (const moduleName of ["@earendil-works/pi-tui", "@mariozechner/pi-tui"]) {
|
|
601
|
+
try {
|
|
602
|
+
const mod = require(moduleName);
|
|
603
|
+
if (mod.Text)
|
|
604
|
+
return new mod.Text(text, 0, 0);
|
|
605
|
+
}
|
|
606
|
+
catch (_error) {
|
|
607
|
+
void _error;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return {
|
|
611
|
+
text,
|
|
612
|
+
setText(next) {
|
|
613
|
+
this.text = next;
|
|
614
|
+
},
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function truncateText(text, max = 80) {
|
|
618
|
+
return text.length > max ? `${text.slice(0, Math.max(0, max - 3))}...` : text;
|
|
619
|
+
}
|
|
620
|
+
function quotedText(text, max = 72) {
|
|
621
|
+
return `"${truncateText(text, max)}"`;
|
|
622
|
+
}
|
|
623
|
+
function stringValue(value) {
|
|
624
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
625
|
+
}
|
|
626
|
+
function toolParamValue(request) {
|
|
627
|
+
return normalizeParams(request.params.tool_params);
|
|
628
|
+
}
|
|
629
|
+
function normalizeRenderRequest(mode, args) {
|
|
630
|
+
try {
|
|
631
|
+
return normalizeCodeModeRequest(mode, normalizeParams(args));
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function formatOperationSubject(request) {
|
|
638
|
+
if (request.domain === "external") {
|
|
639
|
+
const toolName = stringValue(request.params.tool_name) ?? "tool";
|
|
640
|
+
return `${request.action} ${toolName}`;
|
|
641
|
+
}
|
|
642
|
+
return `${request.domain}/${request.action}`;
|
|
643
|
+
}
|
|
644
|
+
function summarizeInventoryItem(value) {
|
|
645
|
+
if (typeof value === "string" && value.trim()) {
|
|
646
|
+
return truncateText(value.trim(), 96);
|
|
647
|
+
}
|
|
648
|
+
if (!value || typeof value !== "object")
|
|
649
|
+
return null;
|
|
650
|
+
const item = value;
|
|
651
|
+
const toolName = stringValue(item.tool_name);
|
|
652
|
+
const sessionId = stringValue(item.session_id);
|
|
653
|
+
const instanceId = stringValue(item.instance_id);
|
|
654
|
+
const serverId = stringValue(item.server_id) ?? stringValue(item.id) ?? instanceId;
|
|
655
|
+
if (toolName && serverId) {
|
|
656
|
+
return truncateText(`${toolName} @ ${serverId}`, 96);
|
|
657
|
+
}
|
|
658
|
+
if (instanceId) {
|
|
659
|
+
return truncateText(sessionId ? `${instanceId} (session ${sessionId})` : instanceId, 96);
|
|
660
|
+
}
|
|
661
|
+
const label = stringValue(item.title) ??
|
|
662
|
+
stringValue(item.name) ??
|
|
663
|
+
stringValue(item.key) ??
|
|
664
|
+
stringValue(item.id) ??
|
|
665
|
+
stringValue(item.url) ??
|
|
666
|
+
serverId;
|
|
667
|
+
if (!label)
|
|
668
|
+
return null;
|
|
669
|
+
const suffix = [];
|
|
670
|
+
if (typeof item.running === "boolean") {
|
|
671
|
+
suffix.push(item.running ? "running" : "stopped");
|
|
672
|
+
}
|
|
673
|
+
if (typeof item.enabled === "boolean") {
|
|
674
|
+
suffix.push(item.enabled ? "enabled" : "disabled");
|
|
675
|
+
}
|
|
676
|
+
if (typeof item.available === "boolean" && item.available === false) {
|
|
677
|
+
suffix.push("unavailable");
|
|
678
|
+
}
|
|
679
|
+
if (Array.isArray(item.tools)) {
|
|
680
|
+
suffix.push(`${item.tools.length} ${item.tools.length === 1 ? "tool" : "tools"}`);
|
|
681
|
+
}
|
|
682
|
+
else if (typeof item.tool_count === "number") {
|
|
683
|
+
suffix.push(`${item.tool_count} ${item.tool_count === 1 ? "tool" : "tools"}`);
|
|
684
|
+
}
|
|
685
|
+
const status = stringValue(item.status);
|
|
686
|
+
if (status && !suffix.includes(status))
|
|
687
|
+
suffix.push(status);
|
|
688
|
+
const decorated = suffix.length > 0 ? `${label} (${suffix.slice(0, 3).join(", ")})` : label;
|
|
689
|
+
return truncateText(decorated, 96);
|
|
690
|
+
}
|
|
691
|
+
function formatOperationDetails(request) {
|
|
692
|
+
const details = [];
|
|
693
|
+
if (request.domain === "external") {
|
|
694
|
+
const toolName = stringValue(request.params.tool_name);
|
|
695
|
+
const toolParams = toolParamValue(request);
|
|
696
|
+
const query = stringValue(toolParams.query);
|
|
697
|
+
const url = stringValue(toolParams.url);
|
|
698
|
+
const libraryId = stringValue(toolParams.context7CompatibleLibraryID);
|
|
699
|
+
if (toolName)
|
|
700
|
+
details.push(`tool: ${toolName}`);
|
|
701
|
+
if (query)
|
|
702
|
+
details.push(`query: ${quotedText(query)}`);
|
|
703
|
+
if (url)
|
|
704
|
+
details.push(`url: ${truncateText(url, 84)}`);
|
|
705
|
+
if (libraryId)
|
|
706
|
+
details.push(`library: ${truncateText(libraryId, 84)}`);
|
|
707
|
+
return details;
|
|
708
|
+
}
|
|
709
|
+
const query = stringValue(request.params.query);
|
|
710
|
+
const key = stringValue(request.params.key) ?? stringValue(request.params.name);
|
|
711
|
+
const serverId = stringValue(request.params.server_id);
|
|
712
|
+
const sessionId = stringValue(request.params.session_id);
|
|
713
|
+
const library = stringValue(request.params.library);
|
|
714
|
+
if (query)
|
|
715
|
+
details.push(`query: ${quotedText(query)}`);
|
|
716
|
+
if (key)
|
|
717
|
+
details.push(`key: ${truncateText(key, 84)}`);
|
|
718
|
+
if (serverId)
|
|
719
|
+
details.push(`server: ${serverId}`);
|
|
720
|
+
if (sessionId)
|
|
721
|
+
details.push(`session: ${truncateText(sessionId, 84)}`);
|
|
722
|
+
if (library)
|
|
723
|
+
details.push(`library: ${truncateText(library, 84)}`);
|
|
724
|
+
return details;
|
|
725
|
+
}
|
|
726
|
+
function summarizePayloadItems(payload) {
|
|
727
|
+
const namedArrays = [
|
|
728
|
+
["results", "results"],
|
|
729
|
+
["tools", "tools"],
|
|
730
|
+
["servers", "servers"],
|
|
731
|
+
["running_servers", "servers"],
|
|
732
|
+
["sessions", "sessions"],
|
|
733
|
+
["skills", "skills"],
|
|
734
|
+
["prompts", "prompts"],
|
|
735
|
+
["libraries", "libraries"],
|
|
736
|
+
["items", "items"],
|
|
737
|
+
["rooms", "rooms"],
|
|
738
|
+
["messages", "messages"],
|
|
739
|
+
];
|
|
740
|
+
if (Array.isArray(payload)) {
|
|
741
|
+
return {
|
|
742
|
+
count: payload.length,
|
|
743
|
+
label: payload.length === 1 ? "item" : "items",
|
|
744
|
+
items: payload
|
|
745
|
+
.map(summarizeInventoryItem)
|
|
746
|
+
.filter((item) => Boolean(item))
|
|
747
|
+
.slice(0, 4),
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
if (!payload || typeof payload !== "object")
|
|
751
|
+
return null;
|
|
752
|
+
const obj = payload;
|
|
753
|
+
for (const [key, label] of namedArrays) {
|
|
754
|
+
const items = obj[key];
|
|
755
|
+
if (!Array.isArray(items))
|
|
756
|
+
continue;
|
|
757
|
+
return {
|
|
758
|
+
count: items.length,
|
|
759
|
+
label: items.length === 1 ? label.replace(/s$/, "") : label,
|
|
760
|
+
items: items
|
|
761
|
+
.map(summarizeInventoryItem)
|
|
762
|
+
.filter((item) => Boolean(item))
|
|
763
|
+
.slice(0, 4),
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
function defaultOperationStatus(request) {
|
|
769
|
+
switch (request?.action) {
|
|
770
|
+
case "search":
|
|
771
|
+
return "searched";
|
|
772
|
+
case "list":
|
|
773
|
+
return "loaded";
|
|
774
|
+
case "get":
|
|
775
|
+
return "opened";
|
|
776
|
+
case "use":
|
|
777
|
+
return "activated";
|
|
778
|
+
case "sync":
|
|
779
|
+
return "synced";
|
|
780
|
+
case "start":
|
|
781
|
+
return "started";
|
|
782
|
+
case "stop":
|
|
783
|
+
return "stopped";
|
|
784
|
+
case "status":
|
|
785
|
+
return "status loaded";
|
|
786
|
+
default:
|
|
787
|
+
return "completed";
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function summarizeRenderedResult(request, details, isError) {
|
|
791
|
+
if (isError) {
|
|
792
|
+
const errorText = extractText(details);
|
|
793
|
+
const preview = errorText
|
|
794
|
+
? [truncateText(errorText.split(/\r?\n/)[0] ?? errorText, 120)]
|
|
795
|
+
: [];
|
|
796
|
+
return { status: "failed", preview };
|
|
797
|
+
}
|
|
798
|
+
const payload = extractJsonFromMcpResult(details) ?? details;
|
|
799
|
+
if (payload && typeof payload === "object") {
|
|
800
|
+
const obj = payload;
|
|
801
|
+
const explicitStatus = stringValue(obj.status);
|
|
802
|
+
if (request?.domain === "hub" && request.action === "start") {
|
|
803
|
+
const tools = Array.isArray(obj.tools)
|
|
804
|
+
? obj.tools
|
|
805
|
+
.map((item) => summarizeInventoryItem(item))
|
|
806
|
+
.filter((item) => Boolean(item))
|
|
807
|
+
.slice(0, 3)
|
|
808
|
+
: [];
|
|
809
|
+
const preview = [
|
|
810
|
+
...tools,
|
|
811
|
+
...[
|
|
812
|
+
stringValue(obj.hint),
|
|
813
|
+
stringValue(obj.message),
|
|
814
|
+
stringValue(obj.summary),
|
|
815
|
+
]
|
|
816
|
+
.filter((item) => Boolean(item))
|
|
817
|
+
.map((item) => truncateText(item, 120)),
|
|
818
|
+
].slice(0, 3);
|
|
819
|
+
return {
|
|
820
|
+
status: explicitStatus ?? defaultOperationStatus(request),
|
|
821
|
+
preview,
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
const listSummary = summarizePayloadItems(payload);
|
|
825
|
+
if (listSummary) {
|
|
826
|
+
const preview = listSummary.items.length
|
|
827
|
+
? listSummary.items
|
|
828
|
+
: stringValue(obj.hint)
|
|
829
|
+
? [truncateText(stringValue(obj.hint) ?? "", 120)]
|
|
830
|
+
: [];
|
|
831
|
+
return {
|
|
832
|
+
status: `${listSummary.count} ${listSummary.label}`,
|
|
833
|
+
preview,
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
const message = stringValue(obj.message) ??
|
|
837
|
+
stringValue(obj.summary) ??
|
|
838
|
+
stringValue(obj.hint);
|
|
839
|
+
if (message) {
|
|
840
|
+
return {
|
|
841
|
+
status: explicitStatus ?? defaultOperationStatus(request),
|
|
842
|
+
preview: [truncateText(message, 120)],
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
if (explicitStatus) {
|
|
846
|
+
return { status: explicitStatus, preview: [] };
|
|
847
|
+
}
|
|
848
|
+
if (obj.ok === true) {
|
|
849
|
+
return { status: defaultOperationStatus(request), preview: [] };
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return { status: defaultOperationStatus(request), preview: [] };
|
|
853
|
+
}
|
|
854
|
+
function renderSageToolCall(mode, args, theme) {
|
|
855
|
+
const request = normalizeRenderRequest(mode, args);
|
|
856
|
+
const toolName = mode === "search" ? "sage_search " : "sage_execute ";
|
|
857
|
+
if (!request) {
|
|
858
|
+
return createTextComponent(theme.fg("toolTitle", theme.bold(toolName)) +
|
|
859
|
+
theme.fg("warning", "(invalid request)"));
|
|
860
|
+
}
|
|
861
|
+
const lines = [
|
|
862
|
+
theme.fg("toolTitle", theme.bold(toolName)) +
|
|
863
|
+
theme.fg("accent", formatOperationSubject(request)),
|
|
864
|
+
];
|
|
865
|
+
for (const detail of formatOperationDetails(request).slice(0, 3)) {
|
|
866
|
+
lines.push(theme.fg("dim", ` ${detail}`));
|
|
867
|
+
}
|
|
868
|
+
return createTextComponent(lines.join("\n"));
|
|
869
|
+
}
|
|
870
|
+
function renderSageToolResult(mode, result, options, theme, context) {
|
|
871
|
+
if (options.isPartial) {
|
|
872
|
+
return createTextComponent(theme.fg("warning", mode === "search" ? "Searching..." : "Executing..."));
|
|
873
|
+
}
|
|
874
|
+
const request = normalizeRenderRequest(mode, context?.args);
|
|
875
|
+
const summary = summarizeRenderedResult(request, result.details, Boolean(context?.isError ?? result.isError));
|
|
876
|
+
const statusColor = summary.status === "failed" ? "error" : "success";
|
|
877
|
+
const lines = [
|
|
878
|
+
theme.fg(statusColor, summary.status) +
|
|
879
|
+
theme.fg("muted", " · ") +
|
|
880
|
+
theme.fg("accent", request
|
|
881
|
+
? formatOperationSubject(request)
|
|
882
|
+
: mode === "search"
|
|
883
|
+
? "search"
|
|
884
|
+
: "execute"),
|
|
885
|
+
];
|
|
886
|
+
for (const detail of request
|
|
887
|
+
? formatOperationDetails(request).slice(0, 2)
|
|
888
|
+
: []) {
|
|
889
|
+
lines.push(theme.fg("dim", ` ${detail}`));
|
|
890
|
+
}
|
|
891
|
+
if (options.expanded) {
|
|
892
|
+
if (summary.preview.length > 0) {
|
|
893
|
+
for (const line of summary.preview) {
|
|
894
|
+
lines.push(theme.fg("muted", ` ${line}`));
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
else if (!(context?.isError ?? result.isError)) {
|
|
898
|
+
lines.push(theme.fg("dim", " raw payload hidden"));
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return createTextComponent(lines.join("\n"));
|
|
902
|
+
}
|
|
588
903
|
function looksUnknownToolError(error) {
|
|
589
|
-
const msg = error instanceof Error
|
|
904
|
+
const msg = error instanceof Error
|
|
905
|
+
? error.message.toLowerCase()
|
|
906
|
+
: String(error).toLowerCase();
|
|
590
907
|
return (msg.includes("unknown tool") ||
|
|
591
908
|
msg.includes("not found") ||
|
|
592
909
|
msg.includes("no such tool") ||
|
|
@@ -614,8 +931,8 @@ function parseSecurityDecision(stdout) {
|
|
|
614
931
|
if (rawDecision === "allow")
|
|
615
932
|
return { decision: "allow", reason };
|
|
616
933
|
}
|
|
617
|
-
catch {
|
|
618
|
-
|
|
934
|
+
catch (_error) {
|
|
935
|
+
void _error;
|
|
619
936
|
}
|
|
620
937
|
}
|
|
621
938
|
return null;
|
|
@@ -771,7 +1088,8 @@ export class SageMcpBridge {
|
|
|
771
1088
|
catch (error) {
|
|
772
1089
|
if (!looksUnknownToolError(error))
|
|
773
1090
|
throw error;
|
|
774
|
-
lastUnknownError =
|
|
1091
|
+
lastUnknownError =
|
|
1092
|
+
error instanceof Error ? error : new Error(String(error));
|
|
775
1093
|
}
|
|
776
1094
|
}
|
|
777
1095
|
if (lastUnknownError)
|
|
@@ -807,7 +1125,7 @@ export class SageMcpBridge {
|
|
|
807
1125
|
proc.on("exit", (code) => {
|
|
808
1126
|
if (this.stopped)
|
|
809
1127
|
return;
|
|
810
|
-
const message = this.withRecentStderr(`MCP process exited${this.ready ?
|
|
1128
|
+
const message = this.withRecentStderr(`MCP process exited${this.ready ? "" : " before initialization"} (code ${code ?? -1})`);
|
|
811
1129
|
this.ready = false;
|
|
812
1130
|
this.rejectAllPending(message);
|
|
813
1131
|
rejectOnce(new Error(message));
|
|
@@ -930,7 +1248,9 @@ export class SageHookRuntime {
|
|
|
930
1248
|
this.run(["library", "active"], {}, undefined, true),
|
|
931
1249
|
]);
|
|
932
1250
|
const libraries = [];
|
|
933
|
-
const libraryOut = libraryResult.status === "fulfilled"
|
|
1251
|
+
const libraryOut = libraryResult.status === "fulfilled"
|
|
1252
|
+
? (libraryResult.value?.stdout.trim() ?? "")
|
|
1253
|
+
: "";
|
|
934
1254
|
if (libraryOut) {
|
|
935
1255
|
for (const line of libraryOut.split("\n")) {
|
|
936
1256
|
const m = line.match(/^\s*(.+?)\s+\((\d+)\s+prompts?,\s*(\d+)\s+skills?\)/);
|
|
@@ -945,7 +1265,9 @@ export class SageHookRuntime {
|
|
|
945
1265
|
}
|
|
946
1266
|
const totalSkills = libraries.reduce((s, l) => s + l.skills, 0);
|
|
947
1267
|
// --- Active libraries ---
|
|
948
|
-
const activeOut = activeResult.status === "fulfilled"
|
|
1268
|
+
const activeOut = activeResult.status === "fulfilled"
|
|
1269
|
+
? (activeResult.value?.stdout.trim() ?? "")
|
|
1270
|
+
: "";
|
|
949
1271
|
const activeNames = [];
|
|
950
1272
|
if (activeOut) {
|
|
951
1273
|
for (const line of activeOut.split("\n")) {
|
|
@@ -973,7 +1295,9 @@ export class SageHookRuntime {
|
|
|
973
1295
|
return name;
|
|
974
1296
|
});
|
|
975
1297
|
const SEP = unicodeSafe("\u00B7"); // middle dot or |
|
|
976
|
-
const suffix = activeNames.length > maxShow
|
|
1298
|
+
const suffix = activeNames.length > maxShow
|
|
1299
|
+
? ` ${SEP} +${activeNames.length - maxShow} more`
|
|
1300
|
+
: "";
|
|
977
1301
|
summary = shown.join(` ${SEP} `) + suffix;
|
|
978
1302
|
}
|
|
979
1303
|
else if (libraries.length > 0) {
|
|
@@ -986,7 +1310,13 @@ export class SageHookRuntime {
|
|
|
986
1310
|
const trimmed = prompt.trim();
|
|
987
1311
|
if (!trimmed)
|
|
988
1312
|
return "";
|
|
989
|
-
const args = [
|
|
1313
|
+
const args = [
|
|
1314
|
+
"suggest",
|
|
1315
|
+
"hook",
|
|
1316
|
+
"skill",
|
|
1317
|
+
"--limit",
|
|
1318
|
+
String(this.config.suggestLimit ?? 3),
|
|
1319
|
+
];
|
|
990
1320
|
if (this.config.enableProvision !== false)
|
|
991
1321
|
args.push("--provision");
|
|
992
1322
|
const result = await this.run(args, {
|
|
@@ -1028,7 +1358,9 @@ export class SageHookRuntime {
|
|
|
1028
1358
|
async recordSuggestionUse(skillKeys, sessionId) {
|
|
1029
1359
|
if (this.config.enableRlmFeedback === false)
|
|
1030
1360
|
return;
|
|
1031
|
-
const used = [
|
|
1361
|
+
const used = [
|
|
1362
|
+
...new Set(skillKeys.map((key) => key.trim()).filter(Boolean)),
|
|
1363
|
+
];
|
|
1032
1364
|
if (used.length === 0)
|
|
1033
1365
|
return;
|
|
1034
1366
|
await this.run(["suggest", "feedback", "use", ...used.flatMap((key) => ["--used", key])], {
|
|
@@ -1044,7 +1376,7 @@ export class SageHookRuntime {
|
|
|
1044
1376
|
SAGE_SESSION_ID: sessionId ?? "",
|
|
1045
1377
|
}, undefined, true);
|
|
1046
1378
|
}
|
|
1047
|
-
async recordExecutionOutcome(skillKey, status, queryPreview, sessionId,
|
|
1379
|
+
async recordExecutionOutcome(skillKey, status, queryPreview, sessionId, _model) {
|
|
1048
1380
|
if (this.config.enableRlmFeedback === false)
|
|
1049
1381
|
return;
|
|
1050
1382
|
const key = skillKey.trim();
|
|
@@ -1201,6 +1533,7 @@ export class SageHookRuntime {
|
|
|
1201
1533
|
}
|
|
1202
1534
|
const SageDomainType = Type.Union([
|
|
1203
1535
|
Type.Literal("prompts"),
|
|
1536
|
+
Type.Literal("behaviors"),
|
|
1204
1537
|
Type.Literal("skills"),
|
|
1205
1538
|
Type.Literal("builder"),
|
|
1206
1539
|
Type.Literal("governance"),
|
|
@@ -1211,6 +1544,8 @@ const SageDomainType = Type.Union([
|
|
|
1211
1544
|
Type.Literal("security"),
|
|
1212
1545
|
Type.Literal("meta"),
|
|
1213
1546
|
Type.Literal("hub"),
|
|
1547
|
+
Type.Literal("activation"),
|
|
1548
|
+
Type.Literal("wallet"),
|
|
1214
1549
|
Type.Literal("library"),
|
|
1215
1550
|
Type.Literal("help"),
|
|
1216
1551
|
Type.Literal("external"),
|
|
@@ -1219,32 +1554,33 @@ export function createSageSearchTool(mcpBridge) {
|
|
|
1219
1554
|
return {
|
|
1220
1555
|
name: "sage_search",
|
|
1221
1556
|
label: "Sage: Search",
|
|
1222
|
-
promptSnippet: "
|
|
1557
|
+
promptSnippet: "Route read-only Sage code-mode requests: discover domains/actions, inspect context, query hub/external tools, and search capabilities",
|
|
1223
1558
|
promptGuidelines: [
|
|
1224
|
-
"Use sage_search (NOT bash sage commands) for
|
|
1225
|
-
"
|
|
1226
|
-
"
|
|
1227
|
-
"
|
|
1559
|
+
"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",
|
|
1560
|
+
"Use sage_search({ domain: 'help', action: 'list', params: {} }) before guessing unknown Sage domains or actions",
|
|
1561
|
+
"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",
|
|
1562
|
+
"If Sage says an action is a mutation or non-search inspect action, retry with sage_execute instead of falling back to bash",
|
|
1228
1563
|
],
|
|
1229
|
-
description: "
|
|
1230
|
-
"
|
|
1231
|
-
"
|
|
1232
|
-
"
|
|
1233
|
-
"
|
|
1234
|
-
"For library docs: domain='external', action='search', params={tool_name:'get_library_docs', tool_params:{context7CompatibleLibraryID:'/lib/id'}}. " +
|
|
1235
|
-
"To list available external servers/tools: domain='external', action='list_servers'.",
|
|
1564
|
+
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. " +
|
|
1565
|
+
"Start with domain='help', action='list' when you do not know the route. " +
|
|
1566
|
+
"Use domain='meta', action='get_project_context' for local Sage state and domain='meta', action='suggest' with params={goal:'...'} when you need route suggestions. " +
|
|
1567
|
+
"Use domain='hub' for MCP hub inventory/status and domain='external' for external tool discovery/search. " +
|
|
1568
|
+
"Some heavy inspect actions such as prompts.get and skills.get are classified by Sage as non-search and should use sage_execute.",
|
|
1236
1569
|
parameters: Type.Object({
|
|
1237
1570
|
domain: Type.Optional(SageDomainType),
|
|
1238
1571
|
action: Type.Optional(Type.String()),
|
|
1239
1572
|
params: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
1240
1573
|
}),
|
|
1574
|
+
renderCall: (args, theme) => renderSageToolCall("search", args, theme),
|
|
1575
|
+
renderResult: (result, options, theme, context) => renderSageToolResult("search", result, options, theme, context),
|
|
1241
1576
|
execute: async (_toolCallId, params) => {
|
|
1242
1577
|
try {
|
|
1243
1578
|
const normalized = normalizeCodeModeRequest("search", params);
|
|
1244
1579
|
const domain = normalized.domain;
|
|
1245
1580
|
const action = normalized.action;
|
|
1246
1581
|
const payload = normalized.params;
|
|
1247
|
-
if (domain === "external" &&
|
|
1582
|
+
if (domain === "external" &&
|
|
1583
|
+
!["list_servers", "search"].includes(action)) {
|
|
1248
1584
|
return {
|
|
1249
1585
|
content: [
|
|
1250
1586
|
{
|
|
@@ -1280,21 +1616,22 @@ export function createSageExecuteTool(mcpBridge) {
|
|
|
1280
1616
|
return {
|
|
1281
1617
|
name: "sage_execute",
|
|
1282
1618
|
label: "Sage: Execute",
|
|
1283
|
-
promptSnippet: "Execute mutations:
|
|
1619
|
+
promptSnippet: "Execute Sage mutations and non-search inspect actions: activate skills, fetch full prompt/skill bodies, manage hub servers, and call external tools",
|
|
1284
1620
|
promptGuidelines: [
|
|
1285
|
-
"Use sage_execute (NOT bash sage commands) for
|
|
1621
|
+
"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",
|
|
1622
|
+
"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",
|
|
1286
1623
|
"To fetch a URL: sage_execute({ domain: 'external', action: 'call', params: { tool_name: 'fetch', tool_params: { url: 'https://...' } } })",
|
|
1287
1624
|
],
|
|
1288
|
-
description: "Execute mutations
|
|
1289
|
-
"
|
|
1290
|
-
"
|
|
1291
|
-
"To call an external tool: domain='external', action='call', params={tool_name:'tool_name', tool_params:{...}}. " +
|
|
1292
|
-
"To fetch a URL: domain='external', action='call', params={tool_name:'fetch', tool_params:{url:'https://example.com'}}.",
|
|
1625
|
+
description: "Execute Sage mutations and non-search inspect actions across prompts, behaviors, skills, builder, chat, library, activation, hub, and external tool surfaces. " +
|
|
1626
|
+
"Use this for prompts.get, skills.get/use, behaviors.run, activation.activate, library.prompt_add/skill_add, hub.start/stop, and external.call/execute. " +
|
|
1627
|
+
"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.",
|
|
1293
1628
|
parameters: Type.Object({
|
|
1294
1629
|
domain: Type.Optional(SageDomainType),
|
|
1295
1630
|
action: Type.Optional(Type.String()),
|
|
1296
1631
|
params: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
1297
1632
|
}),
|
|
1633
|
+
renderCall: (args, theme) => renderSageToolCall("execute", args, theme),
|
|
1634
|
+
renderResult: (result, options, theme, context) => renderSageToolResult("execute", result, options, theme, context),
|
|
1298
1635
|
execute: async (_toolCallId, params) => {
|
|
1299
1636
|
try {
|
|
1300
1637
|
const normalized = normalizeCodeModeRequest("execute", params);
|
|
@@ -1320,32 +1657,7 @@ export function createSageExecuteTool(mcpBridge) {
|
|
|
1320
1657
|
return toToolResultText(result);
|
|
1321
1658
|
}
|
|
1322
1659
|
catch (error) {
|
|
1323
|
-
|
|
1324
|
-
// Detect hub start failures due to missing credentials and add helpful context
|
|
1325
|
-
try {
|
|
1326
|
-
const normalized = normalizeCodeModeRequest("execute", params);
|
|
1327
|
-
if (normalized.domain === "hub" &&
|
|
1328
|
-
normalized.action === "start" &&
|
|
1329
|
-
/EOF|empty|parse|unexpected end/i.test(errorMsg)) {
|
|
1330
|
-
const serverId = String(normalized.params.server_id ?? "unknown");
|
|
1331
|
-
const knownKeyRequirements = {
|
|
1332
|
-
"brave-search": "BRAVE_API_KEY",
|
|
1333
|
-
github: "GITHUB_TOKEN",
|
|
1334
|
-
};
|
|
1335
|
-
const requiredKey = knownKeyRequirements[serverId];
|
|
1336
|
-
if (requiredKey) {
|
|
1337
|
-
errorMsg += `. Server '${serverId}' likely failed because ${requiredKey} is not configured. ` +
|
|
1338
|
-
`Set it with: sage mcp hub env set ${serverId} ${requiredKey}=<your-key>`;
|
|
1339
|
-
}
|
|
1340
|
-
else {
|
|
1341
|
-
errorMsg += `. Server '${serverId}' may require environment variables (API keys). ` +
|
|
1342
|
-
`Check with: sage mcp hub env list ${serverId}`;
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
catch {
|
|
1347
|
-
// normalizeCodeModeRequest can throw -- ignore, keep original errorMsg
|
|
1348
|
-
}
|
|
1660
|
+
const errorMsg = augmentHubStartErrorMessage(params, error instanceof Error ? error.message : String(error));
|
|
1349
1661
|
return {
|
|
1350
1662
|
content: [
|
|
1351
1663
|
{
|
|
@@ -1360,21 +1672,269 @@ export function createSageExecuteTool(mcpBridge) {
|
|
|
1360
1672
|
};
|
|
1361
1673
|
}
|
|
1362
1674
|
const SAGE_CLI_REFERENCE = `
|
|
1363
|
-
## Sage
|
|
1675
|
+
## Sage Code-Mode Reference
|
|
1364
1676
|
|
|
1365
|
-
###
|
|
1366
|
-
- sage_search({ domain: "
|
|
1367
|
-
- sage_search({ domain: "
|
|
1677
|
+
### Route Discovery
|
|
1678
|
+
- List domains/actions: sage_search({ domain: "help", action: "list", params: {} })
|
|
1679
|
+
- Suggest the right route: sage_search({ domain: "meta", action: "suggest", params: { goal: "..." } })
|
|
1680
|
+
- Inspect local Sage state: sage_search({ domain: "meta", action: "get_project_context", params: {} })
|
|
1368
1681
|
|
|
1369
|
-
###
|
|
1370
|
-
-
|
|
1371
|
-
-
|
|
1372
|
-
-
|
|
1682
|
+
### Skills / Prompts / Behaviors
|
|
1683
|
+
- Search skills: sage_search({ domain: "skills", action: "search", params: { query: "..." } })
|
|
1684
|
+
- Inspect skill body: sage_execute({ domain: "skills", action: "get", params: { key: "..." } })
|
|
1685
|
+
- Activate skill: sage_execute({ domain: "skills", action: "use", params: { key: "..." } })
|
|
1686
|
+
- Search prompts: sage_search({ domain: "prompts", action: "search", params: { query: "..." } })
|
|
1687
|
+
- Inspect prompt body: sage_execute({ domain: "prompts", action: "get", params: { key: "..." } })
|
|
1688
|
+
- Inspect behavior: sage_search({ domain: "behaviors", action: "show", params: { key: "..." } })
|
|
1373
1689
|
|
|
1374
|
-
### Hub
|
|
1375
|
-
-
|
|
1376
|
-
-
|
|
1690
|
+
### Hub / External Tools
|
|
1691
|
+
- List MCP servers: sage_search({ domain: "hub", action: "list_servers", params: {} })
|
|
1692
|
+
- Hub status: sage_search({ domain: "hub", action: "status", params: {} })
|
|
1693
|
+
- Start server: sage_execute({ domain: "hub", action: "start", params: { server_id: "github" } })
|
|
1694
|
+
- Running external servers/tools: sage_search({ domain: "external", action: "list_servers", params: {} })
|
|
1695
|
+
- Web search: sage_search({ domain: "external", action: "search", params: { tool_name: "web_search", tool_params: { query: "..." } } })
|
|
1696
|
+
- URL fetch: sage_execute({ domain: "external", action: "call", params: { tool_name: "fetch", tool_params: { url: "https://..." } } })
|
|
1377
1697
|
`.trim();
|
|
1698
|
+
function buildHubRenderContext(action, params, context) {
|
|
1699
|
+
return {
|
|
1700
|
+
...(context && typeof context === "object" ? context : {}),
|
|
1701
|
+
args: {
|
|
1702
|
+
domain: "hub",
|
|
1703
|
+
action,
|
|
1704
|
+
params,
|
|
1705
|
+
},
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
function augmentHubStartErrorMessage(params, errorMsg) {
|
|
1709
|
+
try {
|
|
1710
|
+
const normalized = normalizeCodeModeRequest("execute", params);
|
|
1711
|
+
if (normalized.domain === "hub" &&
|
|
1712
|
+
normalized.action === "start" &&
|
|
1713
|
+
/EOF|empty|parse|unexpected end/i.test(errorMsg)) {
|
|
1714
|
+
const serverId = String(normalized.params.server_id ?? "unknown");
|
|
1715
|
+
const knownKeyRequirements = {
|
|
1716
|
+
"brave-search": "BRAVE_API_KEY",
|
|
1717
|
+
github: "GITHUB_TOKEN",
|
|
1718
|
+
};
|
|
1719
|
+
const requiredKey = knownKeyRequirements[serverId];
|
|
1720
|
+
if (requiredKey) {
|
|
1721
|
+
return (errorMsg +
|
|
1722
|
+
`. Server '${serverId}' likely failed because ${requiredKey} is not configured. ` +
|
|
1723
|
+
`Set it with: sage mcp hub env set ${serverId} ${requiredKey}=<your-key>`);
|
|
1724
|
+
}
|
|
1725
|
+
return (errorMsg +
|
|
1726
|
+
`. Server '${serverId}' may require environment variables (API keys). ` +
|
|
1727
|
+
`Check with: sage mcp hub env list ${serverId}`);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
catch {
|
|
1731
|
+
// Keep original error text if normalization fails.
|
|
1732
|
+
}
|
|
1733
|
+
return errorMsg;
|
|
1734
|
+
}
|
|
1735
|
+
export function createHubListServersTool(mcpBridge) {
|
|
1736
|
+
return {
|
|
1737
|
+
name: "hub_list_servers",
|
|
1738
|
+
label: "Hub: List Servers",
|
|
1739
|
+
promptSnippet: "List Sage MCP servers directly when you need inventory/status without composing a generic sage_search call",
|
|
1740
|
+
promptGuidelines: [
|
|
1741
|
+
"Prefer hub_list_servers() for simple MCP server inventory in Pi; it is the direct convenience wrapper over domain='hub', action='list_servers'",
|
|
1742
|
+
"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",
|
|
1743
|
+
],
|
|
1744
|
+
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:{} }).",
|
|
1745
|
+
parameters: Type.Object({}),
|
|
1746
|
+
renderCall: (_args, theme) => renderSageToolCall("search", { domain: "hub", action: "list_servers", params: {} }, theme),
|
|
1747
|
+
renderResult: (result, options, theme, context) => renderSageToolResult("search", result, options, theme, buildHubRenderContext("list_servers", {}, context)),
|
|
1748
|
+
execute: async () => {
|
|
1749
|
+
try {
|
|
1750
|
+
const result = await callCodeModeWithWebResilience(mcpBridge, "search", {
|
|
1751
|
+
domain: "hub",
|
|
1752
|
+
action: "list_servers",
|
|
1753
|
+
params: {},
|
|
1754
|
+
});
|
|
1755
|
+
return toToolResultText(result);
|
|
1756
|
+
}
|
|
1757
|
+
catch (error) {
|
|
1758
|
+
return {
|
|
1759
|
+
content: [
|
|
1760
|
+
{
|
|
1761
|
+
type: "text",
|
|
1762
|
+
text: `hub_list_servers failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1763
|
+
},
|
|
1764
|
+
],
|
|
1765
|
+
isError: true,
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
},
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
export function createHubStatusTool(mcpBridge) {
|
|
1772
|
+
return {
|
|
1773
|
+
name: "hub_status",
|
|
1774
|
+
label: "Hub: Status",
|
|
1775
|
+
promptSnippet: "Inspect Sage MCP hub runtime status directly when you want running server state without composing a generic sage_search call",
|
|
1776
|
+
promptGuidelines: [
|
|
1777
|
+
"Prefer hub_status() for simple MCP runtime status in Pi; it is the direct convenience wrapper over domain='hub', action='status'",
|
|
1778
|
+
"Use sage_status() when you need full adapter health plus Sage context, not just hub runtime state",
|
|
1779
|
+
],
|
|
1780
|
+
description: "Inspect Sage MCP hub runtime status. This is a Pi-native convenience wrapper for sage_search({ domain:'hub', action:'status', params:{} }).",
|
|
1781
|
+
parameters: Type.Object({}),
|
|
1782
|
+
renderCall: (_args, theme) => renderSageToolCall("search", { domain: "hub", action: "status", params: {} }, theme),
|
|
1783
|
+
renderResult: (result, options, theme, context) => renderSageToolResult("search", result, options, theme, buildHubRenderContext("status", {}, context)),
|
|
1784
|
+
execute: async () => {
|
|
1785
|
+
try {
|
|
1786
|
+
const result = await callCodeModeWithWebResilience(mcpBridge, "search", {
|
|
1787
|
+
domain: "hub",
|
|
1788
|
+
action: "status",
|
|
1789
|
+
params: {},
|
|
1790
|
+
});
|
|
1791
|
+
return toToolResultText(result);
|
|
1792
|
+
}
|
|
1793
|
+
catch (error) {
|
|
1794
|
+
return {
|
|
1795
|
+
content: [
|
|
1796
|
+
{
|
|
1797
|
+
type: "text",
|
|
1798
|
+
text: `hub_status failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1799
|
+
},
|
|
1800
|
+
],
|
|
1801
|
+
isError: true,
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
},
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
export function createHubSessionListTool(mcpBridge) {
|
|
1808
|
+
return {
|
|
1809
|
+
name: "hub_session_list",
|
|
1810
|
+
label: "Hub: Session List",
|
|
1811
|
+
promptSnippet: "List active Sage MCP hub sessions directly when you need isolated instance visibility without composing a generic sage_search call",
|
|
1812
|
+
promptGuidelines: [
|
|
1813
|
+
"Prefer hub_session_list() for simple hub session inspection in Pi; it wraps domain='hub', action='session_list'",
|
|
1814
|
+
"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",
|
|
1815
|
+
],
|
|
1816
|
+
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? } }).",
|
|
1817
|
+
parameters: Type.Object({
|
|
1818
|
+
server_id: Type.Optional(Type.String({
|
|
1819
|
+
description: "Optional hub server identifier to filter sessions",
|
|
1820
|
+
})),
|
|
1821
|
+
}),
|
|
1822
|
+
renderCall: (args, theme) => renderSageToolCall("search", {
|
|
1823
|
+
domain: "hub",
|
|
1824
|
+
action: "session_list",
|
|
1825
|
+
params: normalizeParams(args),
|
|
1826
|
+
}, theme),
|
|
1827
|
+
renderResult: (result, options, theme, context) => renderSageToolResult("search", result, options, theme, buildHubRenderContext("session_list", normalizeParams(context?.args), context)),
|
|
1828
|
+
execute: async (_toolCallId, params) => {
|
|
1829
|
+
try {
|
|
1830
|
+
const payload = normalizeParams(params);
|
|
1831
|
+
const result = await callCodeModeWithWebResilience(mcpBridge, "search", {
|
|
1832
|
+
domain: "hub",
|
|
1833
|
+
action: "session_list",
|
|
1834
|
+
params: payload,
|
|
1835
|
+
});
|
|
1836
|
+
return toToolResultText(result);
|
|
1837
|
+
}
|
|
1838
|
+
catch (error) {
|
|
1839
|
+
return {
|
|
1840
|
+
content: [
|
|
1841
|
+
{
|
|
1842
|
+
type: "text",
|
|
1843
|
+
text: `hub_session_list failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1844
|
+
},
|
|
1845
|
+
],
|
|
1846
|
+
isError: true,
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
},
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
export function createHubStopServerTool(mcpBridge) {
|
|
1853
|
+
return {
|
|
1854
|
+
name: "hub_stop_server",
|
|
1855
|
+
label: "Hub: Stop Server",
|
|
1856
|
+
promptSnippet: "Stop a Sage MCP server directly when you know the server_id and want to end its runtime cleanly",
|
|
1857
|
+
promptGuidelines: [
|
|
1858
|
+
"Prefer hub_stop_server({ server_id: '...' }) for simple MCP server shutdown in Pi; it wraps domain='hub', action='stop'",
|
|
1859
|
+
"Use hub_status() or hub_list_servers() after stopping a server if you need to confirm the resulting hub state",
|
|
1860
|
+
],
|
|
1861
|
+
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:'...' } }).",
|
|
1862
|
+
parameters: Type.Object({
|
|
1863
|
+
server_id: Type.String({
|
|
1864
|
+
description: "Hub server identifier to stop, such as github or brave-search",
|
|
1865
|
+
}),
|
|
1866
|
+
session_id: Type.Optional(Type.String({
|
|
1867
|
+
description: "Optional hub session identifier for isolated instances",
|
|
1868
|
+
})),
|
|
1869
|
+
}),
|
|
1870
|
+
renderCall: (args, theme) => renderSageToolCall("execute", { domain: "hub", action: "stop", params: normalizeParams(args) }, theme),
|
|
1871
|
+
renderResult: (result, options, theme, context) => renderSageToolResult("execute", result, options, theme, buildHubRenderContext("stop", normalizeParams(context?.args), context)),
|
|
1872
|
+
execute: async (_toolCallId, params) => {
|
|
1873
|
+
try {
|
|
1874
|
+
const payload = normalizeParams(params);
|
|
1875
|
+
const result = await callCodeModeWithWebResilience(mcpBridge, "execute", {
|
|
1876
|
+
domain: "hub",
|
|
1877
|
+
action: "stop",
|
|
1878
|
+
params: payload,
|
|
1879
|
+
});
|
|
1880
|
+
return toToolResultText(result);
|
|
1881
|
+
}
|
|
1882
|
+
catch (error) {
|
|
1883
|
+
return {
|
|
1884
|
+
content: [
|
|
1885
|
+
{
|
|
1886
|
+
type: "text",
|
|
1887
|
+
text: `hub_stop_server failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1888
|
+
},
|
|
1889
|
+
],
|
|
1890
|
+
isError: true,
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
},
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
export function createHubStartServerTool(mcpBridge) {
|
|
1897
|
+
return {
|
|
1898
|
+
name: "hub_start_server",
|
|
1899
|
+
label: "Hub: Start Server",
|
|
1900
|
+
promptSnippet: "Start a Sage MCP server directly when you already know the server_id and want Pi to expose its tools",
|
|
1901
|
+
promptGuidelines: [
|
|
1902
|
+
"Prefer hub_start_server({ server_id: '...' }) for simple MCP server startup in Pi; it wraps domain='hub', action='start'",
|
|
1903
|
+
"After starting an external MCP server, call its tools through sage_execute/sage_search with domain='external' and params { tool_name, tool_params }",
|
|
1904
|
+
],
|
|
1905
|
+
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:'...' } }).",
|
|
1906
|
+
parameters: Type.Object({
|
|
1907
|
+
server_id: Type.String({
|
|
1908
|
+
description: "Hub server identifier to start, such as github or brave-search",
|
|
1909
|
+
}),
|
|
1910
|
+
session_id: Type.Optional(Type.String({
|
|
1911
|
+
description: "Optional hub session identifier for isolated instances",
|
|
1912
|
+
})),
|
|
1913
|
+
}),
|
|
1914
|
+
renderCall: (args, theme) => renderSageToolCall("execute", { domain: "hub", action: "start", params: normalizeParams(args) }, theme),
|
|
1915
|
+
renderResult: (result, options, theme, context) => renderSageToolResult("execute", result, options, theme, buildHubRenderContext("start", normalizeParams(context?.args), context)),
|
|
1916
|
+
execute: async (_toolCallId, params) => {
|
|
1917
|
+
try {
|
|
1918
|
+
const payload = normalizeParams(params);
|
|
1919
|
+
const result = await callCodeModeWithWebResilience(mcpBridge, "execute", {
|
|
1920
|
+
domain: "hub",
|
|
1921
|
+
action: "start",
|
|
1922
|
+
params: payload,
|
|
1923
|
+
});
|
|
1924
|
+
return toToolResultText(result);
|
|
1925
|
+
}
|
|
1926
|
+
catch (error) {
|
|
1927
|
+
const errorMsg = augmentHubStartErrorMessage({ domain: "hub", action: "start", params }, error instanceof Error ? error.message : String(error));
|
|
1928
|
+
return {
|
|
1929
|
+
content: [
|
|
1930
|
+
{ type: "text", text: `hub_start_server failed: ${errorMsg}` },
|
|
1931
|
+
],
|
|
1932
|
+
isError: true,
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
},
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1378
1938
|
export function createSageStatusTool(mcpBridge) {
|
|
1379
1939
|
return {
|
|
1380
1940
|
name: "sage_status",
|
|
@@ -1480,7 +2040,11 @@ export function wrapToolsWithSageSecurity(tools, options) {
|
|
|
1480
2040
|
text: `Sage security blocked ${tool.name}: ${pre.reason ?? "high-risk payload detected"}`,
|
|
1481
2041
|
},
|
|
1482
2042
|
],
|
|
1483
|
-
details: {
|
|
2043
|
+
details: {
|
|
2044
|
+
blockedBySageSecurity: true,
|
|
2045
|
+
reason: pre.reason,
|
|
2046
|
+
tool: tool.name,
|
|
2047
|
+
},
|
|
1484
2048
|
isError: true,
|
|
1485
2049
|
};
|
|
1486
2050
|
}
|
|
@@ -1520,6 +2084,11 @@ export function createSageExtensions(options) {
|
|
|
1520
2084
|
console.error("[sage-pi-adapter] failed to start MCP bridge:", error);
|
|
1521
2085
|
});
|
|
1522
2086
|
const sageTools = [
|
|
2087
|
+
createHubListServersTool(mcpBridge),
|
|
2088
|
+
createHubStatusTool(mcpBridge),
|
|
2089
|
+
createHubSessionListTool(mcpBridge),
|
|
2090
|
+
createHubStartServerTool(mcpBridge),
|
|
2091
|
+
createHubStopServerTool(mcpBridge),
|
|
1523
2092
|
createSageSearchTool(mcpBridge),
|
|
1524
2093
|
createSageExecuteTool(mcpBridge),
|
|
1525
2094
|
createSageStatusTool(mcpBridge),
|
|
@@ -1635,7 +2204,9 @@ export async function createSageSessionConfig(config = {}) {
|
|
|
1635
2204
|
const content = parsed.content.trim();
|
|
1636
2205
|
if (!content)
|
|
1637
2206
|
return;
|
|
1638
|
-
void hookRuntime
|
|
2207
|
+
void hookRuntime
|
|
2208
|
+
.capturePrompt(content, activeSessionId)
|
|
2209
|
+
.catch((error) => {
|
|
1639
2210
|
console.error("[sage-pi-adapter] prompt capture failed:", error);
|
|
1640
2211
|
});
|
|
1641
2212
|
void extension.sageInjectSuggestion(content, session, activeSessionId);
|
|
@@ -1646,7 +2217,9 @@ export async function createSageSessionConfig(config = {}) {
|
|
|
1646
2217
|
if (!response)
|
|
1647
2218
|
return;
|
|
1648
2219
|
const usage = extractUsage(parsed.usage);
|
|
1649
|
-
void hookRuntime
|
|
2220
|
+
void hookRuntime
|
|
2221
|
+
.captureResponse(response, usage, activeSessionId)
|
|
2222
|
+
.catch((error) => {
|
|
1650
2223
|
console.error("[sage-pi-adapter] response capture failed:", error);
|
|
1651
2224
|
});
|
|
1652
2225
|
}
|