@sage-protocol/pi-adapter 0.2.2 → 0.3.1
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 +33 -290
- package/SKILL.md +20 -2
- package/dist/sage-pi-adapter.d.ts +24 -11
- package/dist/sage-pi-adapter.d.ts.map +1 -1
- package/dist/sage-pi-adapter.js +242 -128
- package/dist/sage-pi-adapter.js.map +1 -1
- package/extensions/index.ts +303 -44
- package/package.json +2 -2
- package/skills/sage-bridge/SKILL.md +43 -5
package/dist/sage-pi-adapter.js
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
+
const SAGE_ENV_PASSTHROUGH = [
|
|
5
|
+
"SAGE_FILTER_EXECUTION",
|
|
6
|
+
"SAGE_SKILL_USED",
|
|
7
|
+
"SAGE_SKILL_OUTCOME",
|
|
8
|
+
"SAGE_SOURCE",
|
|
9
|
+
"SAGE_SESSION_ID",
|
|
10
|
+
];
|
|
11
|
+
function sagePassthroughEnv() {
|
|
12
|
+
const env = {};
|
|
13
|
+
for (const key of SAGE_ENV_PASSTHROUGH) {
|
|
14
|
+
const value = process.env[key];
|
|
15
|
+
if (value !== undefined)
|
|
16
|
+
env[key] = value;
|
|
17
|
+
}
|
|
18
|
+
return env;
|
|
19
|
+
}
|
|
4
20
|
function formatSageCommand(command, args) {
|
|
5
21
|
const quoted = [command, ...args].map((part) => {
|
|
6
22
|
if (!part)
|
|
@@ -582,6 +598,10 @@ export class SageMcpBridge {
|
|
|
582
598
|
stopped = false;
|
|
583
599
|
startPromise = null;
|
|
584
600
|
stderrBuffer = "";
|
|
601
|
+
degraded = false;
|
|
602
|
+
lastHealthError = null;
|
|
603
|
+
lastHealthCheckedAt = null;
|
|
604
|
+
cachedToolNames = [];
|
|
585
605
|
constructor(command, args, env = {}, requestTimeoutMs = 20_000) {
|
|
586
606
|
this.command = command;
|
|
587
607
|
this.args = args;
|
|
@@ -591,6 +611,19 @@ export class SageMcpBridge {
|
|
|
591
611
|
isReady() {
|
|
592
612
|
return this.ready && this.proc !== null && !this.stopped;
|
|
593
613
|
}
|
|
614
|
+
isDegraded() {
|
|
615
|
+
return this.degraded;
|
|
616
|
+
}
|
|
617
|
+
getHealthSnapshot() {
|
|
618
|
+
return {
|
|
619
|
+
bridgeConnected: this.isReady(),
|
|
620
|
+
degraded: this.degraded,
|
|
621
|
+
lastHealthError: this.lastHealthError,
|
|
622
|
+
lastHealthCheckedAt: this.lastHealthCheckedAt,
|
|
623
|
+
availableToolCount: this.cachedToolNames.length,
|
|
624
|
+
availableToolNames: [...this.cachedToolNames],
|
|
625
|
+
};
|
|
626
|
+
}
|
|
594
627
|
async start() {
|
|
595
628
|
if (this.isReady())
|
|
596
629
|
return;
|
|
@@ -600,6 +633,10 @@ export class SageMcpBridge {
|
|
|
600
633
|
this.stopped = false;
|
|
601
634
|
await this.spawn();
|
|
602
635
|
await this.initialize();
|
|
636
|
+
// Fail-open health probe: mark degraded if tools/list fails post-init,
|
|
637
|
+
// but never throw from start(). Tool-use call sites surface the
|
|
638
|
+
// structured degraded error when the bridge is unreachable.
|
|
639
|
+
await this.probeHealth(Math.min(this.requestTimeoutMs, 2_000));
|
|
603
640
|
})();
|
|
604
641
|
try {
|
|
605
642
|
await this.startPromise;
|
|
@@ -608,6 +645,22 @@ export class SageMcpBridge {
|
|
|
608
645
|
this.startPromise = null;
|
|
609
646
|
}
|
|
610
647
|
}
|
|
648
|
+
async probeHealth(timeoutMs = Math.min(this.requestTimeoutMs, 2_000)) {
|
|
649
|
+
this.lastHealthCheckedAt = Date.now();
|
|
650
|
+
try {
|
|
651
|
+
const tools = await this.listTools(timeoutMs);
|
|
652
|
+
this.cachedToolNames = tools.map((t) => t.name).filter(Boolean);
|
|
653
|
+
this.degraded = false;
|
|
654
|
+
this.lastHealthError = null;
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
catch (error) {
|
|
658
|
+
this.degraded = true;
|
|
659
|
+
this.lastHealthError =
|
|
660
|
+
error instanceof Error ? error.message : String(error);
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
611
664
|
async stop() {
|
|
612
665
|
this.stopped = true;
|
|
613
666
|
this.ready = false;
|
|
@@ -617,8 +670,8 @@ export class SageMcpBridge {
|
|
|
617
670
|
this.proc = null;
|
|
618
671
|
}
|
|
619
672
|
}
|
|
620
|
-
async listTools() {
|
|
621
|
-
const result = (await this.request("tools/list", {}));
|
|
673
|
+
async listTools(timeoutMs) {
|
|
674
|
+
const result = (await this.request("tools/list", {}, timeoutMs));
|
|
622
675
|
return result.tools ?? [];
|
|
623
676
|
}
|
|
624
677
|
async callTool(name, args) {
|
|
@@ -636,10 +689,45 @@ export class SageMcpBridge {
|
|
|
636
689
|
return result;
|
|
637
690
|
}
|
|
638
691
|
async callCodeModeSearch(args) {
|
|
639
|
-
|
|
692
|
+
this.assertReadyForToolCall();
|
|
693
|
+
try {
|
|
694
|
+
return await this.callWithFallback(CODE_MODE_SEARCH_TOOLS, args);
|
|
695
|
+
}
|
|
696
|
+
catch (error) {
|
|
697
|
+
this.markDegradedFromError(error);
|
|
698
|
+
throw error;
|
|
699
|
+
}
|
|
640
700
|
}
|
|
641
701
|
async callCodeModeExecute(args) {
|
|
642
|
-
|
|
702
|
+
this.assertReadyForToolCall();
|
|
703
|
+
try {
|
|
704
|
+
return await this.callWithFallback(CODE_MODE_EXECUTE_TOOLS, args);
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
this.markDegradedFromError(error);
|
|
708
|
+
throw error;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
assertReadyForToolCall() {
|
|
712
|
+
if (!this.isReady() || this.degraded) {
|
|
713
|
+
const detail = this.lastHealthError
|
|
714
|
+
? ` Last health error: ${this.lastHealthError}.`
|
|
715
|
+
: "";
|
|
716
|
+
throw new Error(`Sage MCP bridge unavailable; restart Pi or verify 'sage mcp start' works.${detail}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
markDegradedFromError(error) {
|
|
720
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
721
|
+
// Only flip degraded on transport-level failures, not on known MCP-level
|
|
722
|
+
// errors (for example `tools/list` returning an unknown tool). Transport
|
|
723
|
+
// failures surface as "not running", "stopped", or plain JSON-RPC errors
|
|
724
|
+
// from dead processes.
|
|
725
|
+
const looksTransportFailure = /bridge stopped|not running|failed to spawn|request timed out|EPIPE|ECONNRESET|closed/i.test(message);
|
|
726
|
+
if (!looksTransportFailure)
|
|
727
|
+
return;
|
|
728
|
+
this.degraded = true;
|
|
729
|
+
this.lastHealthError = message;
|
|
730
|
+
this.lastHealthCheckedAt = Date.now();
|
|
643
731
|
}
|
|
644
732
|
async callWithFallback(candidates, args) {
|
|
645
733
|
let lastUnknownError = null;
|
|
@@ -714,7 +802,7 @@ export class SageMcpBridge {
|
|
|
714
802
|
resolve();
|
|
715
803
|
});
|
|
716
804
|
}
|
|
717
|
-
request(method, params) {
|
|
805
|
+
request(method, params, timeoutMs = this.requestTimeoutMs) {
|
|
718
806
|
return new Promise((resolve, reject) => {
|
|
719
807
|
if (!this.proc?.stdin?.writable) {
|
|
720
808
|
reject(new Error("MCP process not running"));
|
|
@@ -724,7 +812,7 @@ export class SageMcpBridge {
|
|
|
724
812
|
const timeout = setTimeout(() => {
|
|
725
813
|
this.pending.delete(id);
|
|
726
814
|
reject(new Error(this.withRecentStderr(`MCP request timed out: ${method}`)));
|
|
727
|
-
},
|
|
815
|
+
}, timeoutMs);
|
|
728
816
|
this.pending.set(id, { resolve, reject, timeout });
|
|
729
817
|
const req = { jsonrpc: "2.0", id, method, params };
|
|
730
818
|
this.proc.stdin.write(JSON.stringify(req) + "\n", (err) => {
|
|
@@ -793,105 +881,89 @@ export class SageHookRuntime {
|
|
|
793
881
|
this.commandRunner = config.commandRunner ?? defaultSageCommandRunner;
|
|
794
882
|
}
|
|
795
883
|
async getSessionContext(sessionId) {
|
|
796
|
-
const result = await this.run(["skill", "context", "--format", "
|
|
884
|
+
const result = await this.run(["skill", "context", "--format", "pi"], { SAGE_SESSION_ID: sessionId ?? "" }, undefined, true);
|
|
797
885
|
return result?.stdout.trim() ?? "";
|
|
798
886
|
}
|
|
799
887
|
/**
|
|
800
|
-
* Gather
|
|
888
|
+
* Gather skill visibility summary for user-facing display.
|
|
801
889
|
*
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
-
* string
|
|
805
|
-
*
|
|
806
|
-
* The result is cached for `identityCacheTtlMs` (default 60 s) to avoid
|
|
807
|
-
* redundant CLI calls on every turn.
|
|
890
|
+
* Runs `sage library list` and `sage library active` in parallel to
|
|
891
|
+
* produce a summary of what libraries, skills, and prompts are loaded.
|
|
892
|
+
* Returns empty string when the CLI is unavailable.
|
|
808
893
|
*/
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
async getIdentityContext() {
|
|
812
|
-
const now = Date.now();
|
|
813
|
-
if (this.identityCache && now < this.identityCache.expiresAt) {
|
|
814
|
-
return this.identityCache.value;
|
|
815
|
-
}
|
|
816
|
-
const [walletResult, libraryResult, activeResult] = await Promise.allSettled([
|
|
817
|
-
this.run(["wallet", "current"], {}, undefined, true),
|
|
894
|
+
async getSkillVisibilitySummary() {
|
|
895
|
+
const [libraryResult, activeResult] = await Promise.allSettled([
|
|
818
896
|
this.run(["library", "list"], {}, undefined, true),
|
|
819
897
|
this.run(["library", "active"], {}, undefined, true),
|
|
820
898
|
]);
|
|
821
|
-
const
|
|
822
|
-
// --- Wallet ---
|
|
823
|
-
const walletOut = walletResult.status === "fulfilled" ? walletResult.value?.stdout.trim() ?? "" : "";
|
|
824
|
-
if (walletOut) {
|
|
825
|
-
const addrMatch = walletOut.match(/Address:\s*(0x[a-fA-F0-9]+)/i);
|
|
826
|
-
const typeMatch = walletOut.match(/Type:\s*(\S+)/i);
|
|
827
|
-
const delegationMatch = walletOut.match(/Active on-chain delegation:\s*(.+)/i);
|
|
828
|
-
const delegatorMatch = walletOut.match(/Delegator:\s*(0x[a-fA-F0-9]+)/i);
|
|
829
|
-
const delegateSignerMatch = walletOut.match(/Delegate signer:\s*(0x[a-fA-F0-9]+)/i);
|
|
830
|
-
const profileMatch = walletOut.match(/Profile:\s*(\S+)/i);
|
|
831
|
-
const networkMatch = walletOut.match(/Network:\s*(.+)/i);
|
|
832
|
-
const chainMatch = walletOut.match(/Chain(?:\s*ID)?:\s*(\S+)/i);
|
|
833
|
-
if (addrMatch) {
|
|
834
|
-
const addr = addrMatch[1];
|
|
835
|
-
const walletType = typeMatch?.[1] ?? "unknown";
|
|
836
|
-
const network = networkMatch?.[1]?.trim() ?? (chainMatch?.[1] === "8453" ? "Base Mainnet" : chainMatch?.[1] === "84532" ? "Base Sepolia" : chainMatch?.[1] ?? "");
|
|
837
|
-
lines.push(`- Wallet: ${addr.slice(0, 10)}...${addr.slice(-4)} (${walletType}${network ? `, ${network}` : ""})`);
|
|
838
|
-
}
|
|
839
|
-
if (delegationMatch && delegatorMatch && delegateSignerMatch) {
|
|
840
|
-
const delegator = delegatorMatch[1];
|
|
841
|
-
const delegate = delegateSignerMatch[1];
|
|
842
|
-
lines.push(`- On-chain delegation: ${delegationMatch[1].trim()} via ${delegate.slice(0, 10)}...${delegate.slice(-4)} for ${delegator.slice(0, 10)}...${delegator.slice(-4)}`);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
// --- Counts only — agent can query details via tools ---
|
|
846
|
-
const activeOut = activeResult.status === "fulfilled" ? activeResult.value?.stdout.trim() ?? "" : "";
|
|
847
|
-
if (activeOut) {
|
|
848
|
-
let activeCount = 0;
|
|
849
|
-
for (const line of activeOut.split("\n")) {
|
|
850
|
-
if (/^\s*\d+\.\s+/.test(line))
|
|
851
|
-
activeCount++;
|
|
852
|
-
}
|
|
853
|
-
if (activeCount > 0) {
|
|
854
|
-
lines.push(`- ${activeCount} active libraries`);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
899
|
+
const libraries = [];
|
|
857
900
|
const libraryOut = libraryResult.status === "fulfilled" ? libraryResult.value?.stdout.trim() ?? "" : "";
|
|
858
901
|
if (libraryOut) {
|
|
859
|
-
let totalSkills = 0;
|
|
860
|
-
let totalPrompts = 0;
|
|
861
|
-
let libraryCount = 0;
|
|
862
902
|
for (const line of libraryOut.split("\n")) {
|
|
863
|
-
const m = line.match(
|
|
903
|
+
const m = line.match(/^\s*(.+?)\s+\((\d+)\s+prompts?,\s*(\d+)\s+skills?\)/);
|
|
864
904
|
if (m) {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
905
|
+
libraries.push({
|
|
906
|
+
name: m[1].trim(),
|
|
907
|
+
prompts: Number.parseInt(m[2], 10),
|
|
908
|
+
skills: Number.parseInt(m[3], 10),
|
|
909
|
+
});
|
|
868
910
|
}
|
|
869
911
|
}
|
|
870
|
-
|
|
871
|
-
|
|
912
|
+
}
|
|
913
|
+
let totalSkills = libraries.reduce((s, l) => s + l.skills, 0);
|
|
914
|
+
let totalPrompts = libraries.reduce((s, l) => s + l.prompts, 0);
|
|
915
|
+
// --- Active libraries + exposed skills ---
|
|
916
|
+
const activeOut = activeResult.status === "fulfilled" ? activeResult.value?.stdout.trim() ?? "" : "";
|
|
917
|
+
const activeNames = [];
|
|
918
|
+
const exposedSkills = [];
|
|
919
|
+
let inSkillSection = false;
|
|
920
|
+
if (activeOut) {
|
|
921
|
+
for (const line of activeOut.split("\n")) {
|
|
922
|
+
if (line.includes("Skill-activated libraries")) {
|
|
923
|
+
inSkillSection = true;
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
if (inSkillSection) {
|
|
927
|
+
const skillMatch = line.match(/^\s+-\s+(\S+)/);
|
|
928
|
+
if (skillMatch) {
|
|
929
|
+
exposedSkills.push(skillMatch[1]);
|
|
930
|
+
}
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
// Prompt-active section: numbered list " 1. name (id)"
|
|
934
|
+
const numMatch = line.match(/^\s*\d+\.\s+(\S+)/);
|
|
935
|
+
if (numMatch) {
|
|
936
|
+
activeNames.push(numMatch[1]);
|
|
937
|
+
}
|
|
872
938
|
}
|
|
873
939
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
: "";
|
|
893
|
-
|
|
894
|
-
|
|
940
|
+
// --- Build display ---
|
|
941
|
+
const headerParts = [];
|
|
942
|
+
if (libraries.length > 0) {
|
|
943
|
+
headerParts.push(`${libraries.length} libraries`);
|
|
944
|
+
}
|
|
945
|
+
if (totalSkills > 0) {
|
|
946
|
+
headerParts.push(`${totalSkills} skills`);
|
|
947
|
+
}
|
|
948
|
+
if (totalPrompts > 0) {
|
|
949
|
+
headerParts.push(`${totalPrompts} prompts`);
|
|
950
|
+
}
|
|
951
|
+
const header = headerParts.length > 0 ? `[Sage] ${headerParts.join(", ")} loaded` : "";
|
|
952
|
+
const detailParts = [];
|
|
953
|
+
if (activeNames.length > 0) {
|
|
954
|
+
const activeDetails = activeNames.map((name) => {
|
|
955
|
+
const lib = libraries.find((l) => l.name === name);
|
|
956
|
+
return lib ? `${name} (${lib.skills}s, ${lib.prompts}p)` : name;
|
|
957
|
+
});
|
|
958
|
+
detailParts.push(`active: ${activeDetails.join(", ")}`);
|
|
959
|
+
}
|
|
960
|
+
if (exposedSkills.length > 0) {
|
|
961
|
+
detailParts.push(`exposed: ${exposedSkills.slice(0, 6).join(", ")}${exposedSkills.length > 6 ? ` +${exposedSkills.length - 6} more` : ""}`);
|
|
962
|
+
}
|
|
963
|
+
if (header && detailParts.length > 0) {
|
|
964
|
+
return `${header}\n${detailParts.join(" | ")}`;
|
|
965
|
+
}
|
|
966
|
+
return header;
|
|
895
967
|
}
|
|
896
968
|
async suggestSkill(prompt, sessionId) {
|
|
897
969
|
const trimmed = prompt.trim();
|
|
@@ -936,6 +1008,32 @@ export class SageHookRuntime {
|
|
|
936
1008
|
TOKENS_OUTPUT: usage.output ?? "",
|
|
937
1009
|
}, JSON.stringify({ response: trimmed }), true);
|
|
938
1010
|
}
|
|
1011
|
+
async recordSuggestionUse(skillKeys, sessionId) {
|
|
1012
|
+
if (this.config.enableRlmFeedback === false)
|
|
1013
|
+
return;
|
|
1014
|
+
const used = [...new Set(skillKeys.map((key) => key.trim()).filter(Boolean))];
|
|
1015
|
+
if (used.length === 0)
|
|
1016
|
+
return;
|
|
1017
|
+
await this.run(["suggest", "feedback", "use", ...used.flatMap((key) => ["--used", key])], {
|
|
1018
|
+
SAGE_SOURCE: this.config.source ?? "pi",
|
|
1019
|
+
SAGE_SESSION_ID: sessionId ?? "",
|
|
1020
|
+
}, undefined, true);
|
|
1021
|
+
}
|
|
1022
|
+
async recordExecutionOutcome(skillKey, status, queryPreview, sessionId) {
|
|
1023
|
+
if (this.config.enableRlmFeedback === false)
|
|
1024
|
+
return;
|
|
1025
|
+
const key = skillKey.trim();
|
|
1026
|
+
if (!key)
|
|
1027
|
+
return;
|
|
1028
|
+
const args = ["suggest", "feedback", "outcome", key, "--status", status];
|
|
1029
|
+
const preview = queryPreview?.trim();
|
|
1030
|
+
if (preview)
|
|
1031
|
+
args.push("--query-preview", preview.slice(0, 240));
|
|
1032
|
+
await this.run(args, {
|
|
1033
|
+
SAGE_SOURCE: this.config.source ?? "pi",
|
|
1034
|
+
SAGE_SESSION_ID: sessionId ?? "",
|
|
1035
|
+
}, undefined, true);
|
|
1036
|
+
}
|
|
939
1037
|
async scanPreTool(toolName, toolInput, sessionId) {
|
|
940
1038
|
if (this.config.enableSecurityHooks === false)
|
|
941
1039
|
return null;
|
|
@@ -992,6 +1090,7 @@ export class SageHookRuntime {
|
|
|
992
1090
|
const commandEnv = {
|
|
993
1091
|
HOME: process.env.HOME ?? "",
|
|
994
1092
|
PATH: process.env.PATH ?? "",
|
|
1093
|
+
...sagePassthroughEnv(),
|
|
995
1094
|
...(this.config.env ?? {}),
|
|
996
1095
|
...env,
|
|
997
1096
|
};
|
|
@@ -1188,39 +1287,21 @@ export function createSageExecuteTool(mcpBridge) {
|
|
|
1188
1287
|
},
|
|
1189
1288
|
};
|
|
1190
1289
|
}
|
|
1191
|
-
// Tier 3: Complete CLI reference — available on-demand via sage_status().
|
|
1192
|
-
// Not injected into system prompt; model requests it when needed.
|
|
1193
1290
|
const SAGE_CLI_REFERENCE = `
|
|
1194
|
-
## Sage CLI Reference (
|
|
1195
|
-
|
|
1196
|
-
### Skill Trigger Workflow
|
|
1197
|
-
1) Discover: sage_search({ domain: "skills", action: "search", params: { query: "..." } })
|
|
1198
|
-
2) Inspect: sage_search({ domain: "skills", action: "get", params: { key: "..." } })
|
|
1199
|
-
3) Activate: sage_execute({ domain: "skills", action: "use", params: { key: "..." } })
|
|
1200
|
-
4) Hook mode: sage suggest hook skill --limit 3 --provision (reads PROMPT env/stdin)
|
|
1291
|
+
## Sage CLI Reference (on-demand)
|
|
1201
1292
|
|
|
1202
|
-
###
|
|
1203
|
-
-
|
|
1204
|
-
-
|
|
1205
|
-
- Inspect: sage skill get <key>
|
|
1206
|
-
- Libraries: sage library list | sage library discover | sage library sync
|
|
1207
|
-
- Gov: sage governance dao discover | sage governance proposals list --dao <addr>
|
|
1208
|
-
- MCP: sage mcp start | sage mcp hub list | sage mcp hub status
|
|
1293
|
+
### Skill Discovery
|
|
1294
|
+
- sage_search({ domain: "skills", action: "search", params: { query: "..." } })
|
|
1295
|
+
- sage_search({ domain: "skills", action: "get", params: { key: "..." } })
|
|
1209
1296
|
|
|
1210
|
-
###
|
|
1211
|
-
-
|
|
1212
|
-
-
|
|
1213
|
-
-
|
|
1297
|
+
### External Tools
|
|
1298
|
+
- Web search: sage_search({ domain: "external", action: "search", params: { tool_name: "web_search", tool_params: { query: "..." } } })
|
|
1299
|
+
- URL fetch: sage_execute({ domain: "external", action: "call", params: { tool_name: "fetch", tool_params: { url: "https://..." } } })
|
|
1300
|
+
- List tools: sage_search({ domain: "external", action: "list_servers" })
|
|
1214
1301
|
|
|
1215
|
-
### Hub
|
|
1216
|
-
- Start: sage_execute({ domain: "hub", action: "start", params: { server_id: "brave-search" } })
|
|
1217
|
-
- Stop: sage_execute({ domain: "hub", action: "stop", params: { server_id: "brave-search" } })
|
|
1218
|
-
- List: sage_search({ domain: "external", action: "list_servers" })
|
|
1219
|
-
|
|
1220
|
-
### Guardrails
|
|
1221
|
-
- Verify uncertain syntax with sage --help / sage <group> --help.
|
|
1222
|
-
- Prefer sage search --search-type skills when sage skill search is unavailable.
|
|
1223
|
-
- Do not use legacy removed top-level commands: sage dao ..., sage proposals ..., sage ipfs ...
|
|
1302
|
+
### Hub
|
|
1303
|
+
- Start server: sage_execute({ domain: "hub", action: "start", params: { server_id: "brave-search" } })
|
|
1304
|
+
- Stop server: sage_execute({ domain: "hub", action: "stop", params: { server_id: "brave-search" } })
|
|
1224
1305
|
`.trim();
|
|
1225
1306
|
export function createSageStatusTool(mcpBridge) {
|
|
1226
1307
|
return {
|
|
@@ -1231,7 +1312,8 @@ export function createSageStatusTool(mcpBridge) {
|
|
|
1231
1312
|
"Call this to see what external tools (web search, GitHub, fetch, browser automation, etc.) are available.",
|
|
1232
1313
|
parameters: Type.Object({}),
|
|
1233
1314
|
execute: async () => {
|
|
1234
|
-
const
|
|
1315
|
+
const health = mcpBridge.getHealthSnapshot();
|
|
1316
|
+
const bridgeConnected = health.bridgeConnected && !health.degraded;
|
|
1235
1317
|
if (bridgeConnected) {
|
|
1236
1318
|
try {
|
|
1237
1319
|
const context = await mcpBridge.callCodeModeSearch({
|
|
@@ -1245,7 +1327,10 @@ export function createSageStatusTool(mcpBridge) {
|
|
|
1245
1327
|
type: "text",
|
|
1246
1328
|
text: JSON.stringify({
|
|
1247
1329
|
bridgeConnected,
|
|
1330
|
+
degraded: false,
|
|
1248
1331
|
sageAdapterVersion: "0.2.0",
|
|
1332
|
+
availableToolCount: health.availableToolCount,
|
|
1333
|
+
availableToolNames: health.availableToolNames,
|
|
1249
1334
|
context: extractJsonFromMcpResult(context) ?? context,
|
|
1250
1335
|
}, null, 2),
|
|
1251
1336
|
},
|
|
@@ -1256,8 +1341,27 @@ export function createSageStatusTool(mcpBridge) {
|
|
|
1256
1341
|
],
|
|
1257
1342
|
};
|
|
1258
1343
|
}
|
|
1259
|
-
catch {
|
|
1260
|
-
//
|
|
1344
|
+
catch (error) {
|
|
1345
|
+
// Tool call failed even though the bridge claims ready; surface as
|
|
1346
|
+
// degraded rather than falling back silently.
|
|
1347
|
+
return {
|
|
1348
|
+
content: [
|
|
1349
|
+
{
|
|
1350
|
+
type: "text",
|
|
1351
|
+
text: JSON.stringify({
|
|
1352
|
+
bridgeConnected,
|
|
1353
|
+
degraded: true,
|
|
1354
|
+
sageAdapterVersion: "0.2.0",
|
|
1355
|
+
lastHealthError: error instanceof Error ? error.message : String(error),
|
|
1356
|
+
}, null, 2),
|
|
1357
|
+
},
|
|
1358
|
+
{
|
|
1359
|
+
type: "text",
|
|
1360
|
+
text: SAGE_CLI_REFERENCE,
|
|
1361
|
+
},
|
|
1362
|
+
],
|
|
1363
|
+
details: { bridgeConnected, degraded: true },
|
|
1364
|
+
};
|
|
1261
1365
|
}
|
|
1262
1366
|
}
|
|
1263
1367
|
return {
|
|
@@ -1266,7 +1370,12 @@ export function createSageStatusTool(mcpBridge) {
|
|
|
1266
1370
|
type: "text",
|
|
1267
1371
|
text: JSON.stringify({
|
|
1268
1372
|
bridgeConnected,
|
|
1373
|
+
degraded: health.degraded,
|
|
1269
1374
|
sageAdapterVersion: "0.2.0",
|
|
1375
|
+
lastHealthError: health.lastHealthError,
|
|
1376
|
+
lastHealthCheckedAt: health.lastHealthCheckedAt,
|
|
1377
|
+
availableToolCount: health.availableToolCount,
|
|
1378
|
+
availableToolNames: health.availableToolNames,
|
|
1270
1379
|
}, null, 2),
|
|
1271
1380
|
},
|
|
1272
1381
|
{
|
|
@@ -1274,7 +1383,11 @@ export function createSageStatusTool(mcpBridge) {
|
|
|
1274
1383
|
text: SAGE_CLI_REFERENCE,
|
|
1275
1384
|
},
|
|
1276
1385
|
],
|
|
1277
|
-
details: {
|
|
1386
|
+
details: {
|
|
1387
|
+
bridgeConnected,
|
|
1388
|
+
degraded: health.degraded,
|
|
1389
|
+
lastHealthError: health.lastHealthError,
|
|
1390
|
+
},
|
|
1278
1391
|
};
|
|
1279
1392
|
},
|
|
1280
1393
|
};
|
|
@@ -1419,6 +1532,7 @@ export async function createSageSessionConfig(config = {}) {
|
|
|
1419
1532
|
wrapToolsWithSecurity: wrapTools,
|
|
1420
1533
|
setupHooks: (session) => {
|
|
1421
1534
|
activeSessionId = session.sessionId;
|
|
1535
|
+
// Inject Sage session context (skill context from CLI).
|
|
1422
1536
|
hookRuntime
|
|
1423
1537
|
.getSessionContext(activeSessionId)
|
|
1424
1538
|
.then(async (context) => {
|
|
@@ -1429,16 +1543,16 @@ export async function createSageSessionConfig(config = {}) {
|
|
|
1429
1543
|
.catch((error) => {
|
|
1430
1544
|
console.error("[sage-pi-adapter] failed to inject session context:", error);
|
|
1431
1545
|
});
|
|
1432
|
-
//
|
|
1546
|
+
// Show loaded skills/libraries summary to the user.
|
|
1433
1547
|
hookRuntime
|
|
1434
|
-
.
|
|
1435
|
-
.then(async (
|
|
1436
|
-
if (!
|
|
1548
|
+
.getSkillVisibilitySummary()
|
|
1549
|
+
.then(async (summary) => {
|
|
1550
|
+
if (!summary)
|
|
1437
1551
|
return;
|
|
1438
|
-
await injectHookMessage(session, "sage-
|
|
1552
|
+
await injectHookMessage(session, "sage-loaded", summary, true);
|
|
1439
1553
|
})
|
|
1440
1554
|
.catch((error) => {
|
|
1441
|
-
console.error("[sage-pi-adapter] failed to
|
|
1555
|
+
console.error("[sage-pi-adapter] failed to get skill summary:", error);
|
|
1442
1556
|
});
|
|
1443
1557
|
const unsubscribe = session.subscribe((event) => {
|
|
1444
1558
|
const parsed = parseMessageEndEvent(event);
|