@sage-protocol/pi-adapter 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +206 -124
- package/dist/sage-pi-adapter.js.map +1 -1
- package/extensions/index.ts +161 -44
- package/package.json +1 -1
- 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,72 +881,26 @@ 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
|
|
801
|
-
*
|
|
802
|
-
* Fetches wallet, library, and skill information in parallel and returns a
|
|
803
|
-
* compact block suitable for appending to the system prompt. Returns an empty
|
|
804
|
-
* string if the sage CLI is unavailable or all queries fail.
|
|
888
|
+
* Gather skill visibility summary for user-facing display.
|
|
805
889
|
*
|
|
806
|
-
*
|
|
807
|
-
*
|
|
890
|
+
* Runs `sage library list` and `sage library active` in parallel to
|
|
891
|
+
* produce a concise one-line summary of what libraries and skills are
|
|
892
|
+
* available. 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
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
+
let totalSkills = 0;
|
|
900
|
+
let totalPrompts = 0;
|
|
901
|
+
let libraryCount = 0;
|
|
857
902
|
const libraryOut = libraryResult.status === "fulfilled" ? libraryResult.value?.stdout.trim() ?? "" : "";
|
|
858
903
|
if (libraryOut) {
|
|
859
|
-
let totalSkills = 0;
|
|
860
|
-
let totalPrompts = 0;
|
|
861
|
-
let libraryCount = 0;
|
|
862
904
|
for (const line of libraryOut.split("\n")) {
|
|
863
905
|
const m = line.match(/\((\d+)\s+prompts?,\s*(\d+)\s+skills?\)/);
|
|
864
906
|
if (m) {
|
|
@@ -867,31 +909,29 @@ export class SageHookRuntime {
|
|
|
867
909
|
totalSkills += Number.parseInt(m[2], 10);
|
|
868
910
|
}
|
|
869
911
|
}
|
|
870
|
-
|
|
871
|
-
|
|
912
|
+
}
|
|
913
|
+
const activeOut = activeResult.status === "fulfilled" ? activeResult.value?.stdout.trim() ?? "" : "";
|
|
914
|
+
let activeCount = 0;
|
|
915
|
+
if (activeOut) {
|
|
916
|
+
for (const line of activeOut.split("\n")) {
|
|
917
|
+
if (/^\s*\d+\.\s+/.test(line))
|
|
918
|
+
activeCount++;
|
|
872
919
|
}
|
|
873
920
|
}
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
"- Publish: `sage library push <name>`\n" +
|
|
889
|
-
"- Follow: `sage social follow <address>`";
|
|
890
|
-
const block = lines.length > 0
|
|
891
|
-
? `## Sage Protocol Context\n${PROTOCOL_DESC}\n\n${lines.join("\n")}\n\n${KEY_COMMANDS}`
|
|
892
|
-
: "";
|
|
893
|
-
this.identityCache = { value: block, expiresAt: now + this.identityCacheTtlMs };
|
|
894
|
-
return block;
|
|
921
|
+
const parts = [];
|
|
922
|
+
if (libraryCount > 0) {
|
|
923
|
+
parts.push(`${libraryCount} libraries`);
|
|
924
|
+
}
|
|
925
|
+
if (totalSkills > 0) {
|
|
926
|
+
parts.push(`${totalSkills} skills`);
|
|
927
|
+
}
|
|
928
|
+
if (totalPrompts > 0) {
|
|
929
|
+
parts.push(`${totalPrompts} prompts`);
|
|
930
|
+
}
|
|
931
|
+
if (activeCount > 0) {
|
|
932
|
+
parts.push(`${activeCount} active`);
|
|
933
|
+
}
|
|
934
|
+
return parts.length > 0 ? `[Sage] ${parts.join(", ")} loaded` : "";
|
|
895
935
|
}
|
|
896
936
|
async suggestSkill(prompt, sessionId) {
|
|
897
937
|
const trimmed = prompt.trim();
|
|
@@ -936,6 +976,32 @@ export class SageHookRuntime {
|
|
|
936
976
|
TOKENS_OUTPUT: usage.output ?? "",
|
|
937
977
|
}, JSON.stringify({ response: trimmed }), true);
|
|
938
978
|
}
|
|
979
|
+
async recordSuggestionUse(skillKeys, sessionId) {
|
|
980
|
+
if (this.config.enableRlmFeedback === false)
|
|
981
|
+
return;
|
|
982
|
+
const used = [...new Set(skillKeys.map((key) => key.trim()).filter(Boolean))];
|
|
983
|
+
if (used.length === 0)
|
|
984
|
+
return;
|
|
985
|
+
await this.run(["suggest", "feedback", "use", ...used.flatMap((key) => ["--used", key])], {
|
|
986
|
+
SAGE_SOURCE: this.config.source ?? "pi",
|
|
987
|
+
SAGE_SESSION_ID: sessionId ?? "",
|
|
988
|
+
}, undefined, true);
|
|
989
|
+
}
|
|
990
|
+
async recordExecutionOutcome(skillKey, status, queryPreview, sessionId) {
|
|
991
|
+
if (this.config.enableRlmFeedback === false)
|
|
992
|
+
return;
|
|
993
|
+
const key = skillKey.trim();
|
|
994
|
+
if (!key)
|
|
995
|
+
return;
|
|
996
|
+
const args = ["suggest", "feedback", "outcome", key, "--status", status];
|
|
997
|
+
const preview = queryPreview?.trim();
|
|
998
|
+
if (preview)
|
|
999
|
+
args.push("--query-preview", preview.slice(0, 240));
|
|
1000
|
+
await this.run(args, {
|
|
1001
|
+
SAGE_SOURCE: this.config.source ?? "pi",
|
|
1002
|
+
SAGE_SESSION_ID: sessionId ?? "",
|
|
1003
|
+
}, undefined, true);
|
|
1004
|
+
}
|
|
939
1005
|
async scanPreTool(toolName, toolInput, sessionId) {
|
|
940
1006
|
if (this.config.enableSecurityHooks === false)
|
|
941
1007
|
return null;
|
|
@@ -992,6 +1058,7 @@ export class SageHookRuntime {
|
|
|
992
1058
|
const commandEnv = {
|
|
993
1059
|
HOME: process.env.HOME ?? "",
|
|
994
1060
|
PATH: process.env.PATH ?? "",
|
|
1061
|
+
...sagePassthroughEnv(),
|
|
995
1062
|
...(this.config.env ?? {}),
|
|
996
1063
|
...env,
|
|
997
1064
|
};
|
|
@@ -1188,39 +1255,21 @@ export function createSageExecuteTool(mcpBridge) {
|
|
|
1188
1255
|
},
|
|
1189
1256
|
};
|
|
1190
1257
|
}
|
|
1191
|
-
// Tier 3: Complete CLI reference — available on-demand via sage_status().
|
|
1192
|
-
// Not injected into system prompt; model requests it when needed.
|
|
1193
1258
|
const SAGE_CLI_REFERENCE = `
|
|
1194
|
-
## Sage CLI Reference (
|
|
1259
|
+
## Sage CLI Reference (on-demand)
|
|
1195
1260
|
|
|
1196
|
-
### Skill
|
|
1197
|
-
|
|
1198
|
-
|
|
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)
|
|
1261
|
+
### Skill Discovery
|
|
1262
|
+
- sage_search({ domain: "skills", action: "search", params: { query: "..." } })
|
|
1263
|
+
- sage_search({ domain: "skills", action: "get", params: { key: "..." } })
|
|
1201
1264
|
|
|
1202
|
-
###
|
|
1203
|
-
-
|
|
1204
|
-
-
|
|
1205
|
-
-
|
|
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
|
|
1265
|
+
### External Tools
|
|
1266
|
+
- Web search: sage_search({ domain: "external", action: "search", params: { tool_name: "web_search", tool_params: { query: "..." } } })
|
|
1267
|
+
- URL fetch: sage_execute({ domain: "external", action: "call", params: { tool_name: "fetch", tool_params: { url: "https://..." } } })
|
|
1268
|
+
- List tools: sage_search({ domain: "external", action: "list_servers" })
|
|
1209
1269
|
|
|
1210
|
-
###
|
|
1211
|
-
-
|
|
1212
|
-
-
|
|
1213
|
-
- Bounty: sage bounties create --title "Task" --description "..." --reward 100 --deadline 7d --subdao 0x...
|
|
1214
|
-
|
|
1215
|
-
### Hub Management
|
|
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 ...
|
|
1270
|
+
### Hub
|
|
1271
|
+
- Start server: sage_execute({ domain: "hub", action: "start", params: { server_id: "brave-search" } })
|
|
1272
|
+
- Stop server: sage_execute({ domain: "hub", action: "stop", params: { server_id: "brave-search" } })
|
|
1224
1273
|
`.trim();
|
|
1225
1274
|
export function createSageStatusTool(mcpBridge) {
|
|
1226
1275
|
return {
|
|
@@ -1231,7 +1280,8 @@ export function createSageStatusTool(mcpBridge) {
|
|
|
1231
1280
|
"Call this to see what external tools (web search, GitHub, fetch, browser automation, etc.) are available.",
|
|
1232
1281
|
parameters: Type.Object({}),
|
|
1233
1282
|
execute: async () => {
|
|
1234
|
-
const
|
|
1283
|
+
const health = mcpBridge.getHealthSnapshot();
|
|
1284
|
+
const bridgeConnected = health.bridgeConnected && !health.degraded;
|
|
1235
1285
|
if (bridgeConnected) {
|
|
1236
1286
|
try {
|
|
1237
1287
|
const context = await mcpBridge.callCodeModeSearch({
|
|
@@ -1245,7 +1295,10 @@ export function createSageStatusTool(mcpBridge) {
|
|
|
1245
1295
|
type: "text",
|
|
1246
1296
|
text: JSON.stringify({
|
|
1247
1297
|
bridgeConnected,
|
|
1298
|
+
degraded: false,
|
|
1248
1299
|
sageAdapterVersion: "0.2.0",
|
|
1300
|
+
availableToolCount: health.availableToolCount,
|
|
1301
|
+
availableToolNames: health.availableToolNames,
|
|
1249
1302
|
context: extractJsonFromMcpResult(context) ?? context,
|
|
1250
1303
|
}, null, 2),
|
|
1251
1304
|
},
|
|
@@ -1256,8 +1309,27 @@ export function createSageStatusTool(mcpBridge) {
|
|
|
1256
1309
|
],
|
|
1257
1310
|
};
|
|
1258
1311
|
}
|
|
1259
|
-
catch {
|
|
1260
|
-
//
|
|
1312
|
+
catch (error) {
|
|
1313
|
+
// Tool call failed even though the bridge claims ready; surface as
|
|
1314
|
+
// degraded rather than falling back silently.
|
|
1315
|
+
return {
|
|
1316
|
+
content: [
|
|
1317
|
+
{
|
|
1318
|
+
type: "text",
|
|
1319
|
+
text: JSON.stringify({
|
|
1320
|
+
bridgeConnected,
|
|
1321
|
+
degraded: true,
|
|
1322
|
+
sageAdapterVersion: "0.2.0",
|
|
1323
|
+
lastHealthError: error instanceof Error ? error.message : String(error),
|
|
1324
|
+
}, null, 2),
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
type: "text",
|
|
1328
|
+
text: SAGE_CLI_REFERENCE,
|
|
1329
|
+
},
|
|
1330
|
+
],
|
|
1331
|
+
details: { bridgeConnected, degraded: true },
|
|
1332
|
+
};
|
|
1261
1333
|
}
|
|
1262
1334
|
}
|
|
1263
1335
|
return {
|
|
@@ -1266,7 +1338,12 @@ export function createSageStatusTool(mcpBridge) {
|
|
|
1266
1338
|
type: "text",
|
|
1267
1339
|
text: JSON.stringify({
|
|
1268
1340
|
bridgeConnected,
|
|
1341
|
+
degraded: health.degraded,
|
|
1269
1342
|
sageAdapterVersion: "0.2.0",
|
|
1343
|
+
lastHealthError: health.lastHealthError,
|
|
1344
|
+
lastHealthCheckedAt: health.lastHealthCheckedAt,
|
|
1345
|
+
availableToolCount: health.availableToolCount,
|
|
1346
|
+
availableToolNames: health.availableToolNames,
|
|
1270
1347
|
}, null, 2),
|
|
1271
1348
|
},
|
|
1272
1349
|
{
|
|
@@ -1274,7 +1351,11 @@ export function createSageStatusTool(mcpBridge) {
|
|
|
1274
1351
|
text: SAGE_CLI_REFERENCE,
|
|
1275
1352
|
},
|
|
1276
1353
|
],
|
|
1277
|
-
details: {
|
|
1354
|
+
details: {
|
|
1355
|
+
bridgeConnected,
|
|
1356
|
+
degraded: health.degraded,
|
|
1357
|
+
lastHealthError: health.lastHealthError,
|
|
1358
|
+
},
|
|
1278
1359
|
};
|
|
1279
1360
|
},
|
|
1280
1361
|
};
|
|
@@ -1419,6 +1500,7 @@ export async function createSageSessionConfig(config = {}) {
|
|
|
1419
1500
|
wrapToolsWithSecurity: wrapTools,
|
|
1420
1501
|
setupHooks: (session) => {
|
|
1421
1502
|
activeSessionId = session.sessionId;
|
|
1503
|
+
// Inject Sage session context (skill context from CLI).
|
|
1422
1504
|
hookRuntime
|
|
1423
1505
|
.getSessionContext(activeSessionId)
|
|
1424
1506
|
.then(async (context) => {
|
|
@@ -1429,16 +1511,16 @@ export async function createSageSessionConfig(config = {}) {
|
|
|
1429
1511
|
.catch((error) => {
|
|
1430
1512
|
console.error("[sage-pi-adapter] failed to inject session context:", error);
|
|
1431
1513
|
});
|
|
1432
|
-
//
|
|
1514
|
+
// Show loaded skills/libraries summary to the user.
|
|
1433
1515
|
hookRuntime
|
|
1434
|
-
.
|
|
1435
|
-
.then(async (
|
|
1436
|
-
if (!
|
|
1516
|
+
.getSkillVisibilitySummary()
|
|
1517
|
+
.then(async (summary) => {
|
|
1518
|
+
if (!summary)
|
|
1437
1519
|
return;
|
|
1438
|
-
await injectHookMessage(session, "sage-
|
|
1520
|
+
await injectHookMessage(session, "sage-loaded", summary, true);
|
|
1439
1521
|
})
|
|
1440
1522
|
.catch((error) => {
|
|
1441
|
-
console.error("[sage-pi-adapter] failed to
|
|
1523
|
+
console.error("[sage-pi-adapter] failed to get skill summary:", error);
|
|
1442
1524
|
});
|
|
1443
1525
|
const unsubscribe = session.subscribe((event) => {
|
|
1444
1526
|
const parsed = parseMessageEndEvent(event);
|