@sage-protocol/pi-adapter 0.1.9 → 0.2.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 +76 -0
- package/SKILL.md +57 -0
- package/dist/behavior/index.d.ts +10 -0
- package/dist/behavior/index.d.ts.map +1 -0
- package/dist/behavior/index.js +9 -0
- package/dist/behavior/index.js.map +1 -0
- package/dist/behavior/pi-executor.d.ts +83 -0
- package/dist/behavior/pi-executor.d.ts.map +1 -0
- package/dist/behavior/pi-executor.js +163 -0
- package/dist/behavior/pi-executor.js.map +1 -0
- package/dist/behavior/resolver.d.ts +20 -0
- package/dist/behavior/resolver.d.ts.map +1 -0
- package/dist/behavior/resolver.js +163 -0
- package/dist/behavior/resolver.js.map +1 -0
- package/dist/behavior/types.d.ts +149 -0
- package/dist/behavior/types.d.ts.map +1 -0
- package/dist/behavior/types.js +16 -0
- package/dist/behavior/types.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/sage-pi-adapter.d.ts +23 -0
- package/dist/sage-pi-adapter.d.ts.map +1 -1
- package/dist/sage-pi-adapter.js +278 -25
- package/dist/sage-pi-adapter.js.map +1 -1
- package/extensions/index.ts +522 -49
- package/package.json +2 -2
- package/skills/sage-bridge/SKILL.md +75 -3
package/dist/sage-pi-adapter.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
+
function formatSageCommand(command, args) {
|
|
5
|
+
const quoted = [command, ...args].map((part) => {
|
|
6
|
+
if (!part)
|
|
7
|
+
return "''";
|
|
8
|
+
return /[\s"'`$\\]/.test(part)
|
|
9
|
+
? JSON.stringify(part)
|
|
10
|
+
: part;
|
|
11
|
+
});
|
|
12
|
+
return quoted.join(" ");
|
|
13
|
+
}
|
|
4
14
|
const CODE_MODE_SEARCH_TOOLS = ["sage_search", "sage_code_sage_search"];
|
|
5
15
|
const CODE_MODE_EXECUTE_TOOLS = ["sage_execute", "sage_code_sage_execute"];
|
|
6
16
|
const SEARCH_ACTION_ALIASES = {
|
|
@@ -62,6 +72,8 @@ const WEB_SEARCH_MAX_ATTEMPTS_PER_PROVIDER = 2;
|
|
|
62
72
|
const WEB_SEARCH_MIN_INTERVAL_MS = 250;
|
|
63
73
|
const WEB_SEARCH_RETRY_BASE_MS = 250;
|
|
64
74
|
const LAST_WEB_SEARCH_CALL_BY_PROVIDER = new Map();
|
|
75
|
+
const DAEMON_IPC_MAX_ATTEMPTS = Math.max(1, Number.parseInt(process.env.SAGE_DAEMON_IPC_MAX_ATTEMPTS || "3", 10) || 3);
|
|
76
|
+
const DAEMON_IPC_RETRY_BASE_MS = Math.max(50, Number.parseInt(process.env.SAGE_DAEMON_IPC_RETRY_BASE_MS || "350", 10) || 350);
|
|
65
77
|
const DOMAINLESS_ACTION_DOMAIN_HINTS = {
|
|
66
78
|
search: "prompts",
|
|
67
79
|
list: "prompts",
|
|
@@ -142,6 +154,14 @@ function isTransientWebSearchError(error) {
|
|
|
142
154
|
const msg = toErrorMessage(error);
|
|
143
155
|
return WEB_SEARCH_RETRY_PATTERNS.some((pattern) => pattern.test(msg));
|
|
144
156
|
}
|
|
157
|
+
function isTransientDaemonIpcError(error) {
|
|
158
|
+
const msg = toErrorMessage(error).toLowerCase();
|
|
159
|
+
return (msg.includes("daemon ipc failed") ||
|
|
160
|
+
msg.includes("resource temporarily unavailable") ||
|
|
161
|
+
msg.includes("os error 35") ||
|
|
162
|
+
msg.includes("eagain") ||
|
|
163
|
+
msg.includes("temporarily unavailable"));
|
|
164
|
+
}
|
|
145
165
|
function getExternalWebSearchTool(request) {
|
|
146
166
|
if (request.domain !== "external")
|
|
147
167
|
return "";
|
|
@@ -193,6 +213,26 @@ function retryBackoffMs(attempt) {
|
|
|
193
213
|
const jitter = Math.floor(Math.random() * 200);
|
|
194
214
|
return WEB_SEARCH_RETRY_BASE_MS * 2 ** Math.max(attempt - 1, 0) + jitter;
|
|
195
215
|
}
|
|
216
|
+
function daemonRetryBackoffMs(attempt) {
|
|
217
|
+
const jitter = Math.floor(Math.random() * 150);
|
|
218
|
+
return DAEMON_IPC_RETRY_BASE_MS * 2 ** Math.max(attempt - 1, 0) + jitter;
|
|
219
|
+
}
|
|
220
|
+
async function callWithDaemonRetry(call, args) {
|
|
221
|
+
let lastError;
|
|
222
|
+
for (let attempt = 1; attempt <= DAEMON_IPC_MAX_ATTEMPTS; attempt += 1) {
|
|
223
|
+
try {
|
|
224
|
+
return await call(args);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
lastError = error;
|
|
228
|
+
const retryable = isTransientDaemonIpcError(error);
|
|
229
|
+
if (!retryable || attempt >= DAEMON_IPC_MAX_ATTEMPTS)
|
|
230
|
+
throw error;
|
|
231
|
+
await wait(daemonRetryBackoffMs(attempt));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "unknown daemon IPC failure"));
|
|
235
|
+
}
|
|
196
236
|
function withExternalToolName(request, toolName) {
|
|
197
237
|
return {
|
|
198
238
|
...request,
|
|
@@ -208,7 +248,7 @@ async function callCodeModeWithWebResilience(mcpBridge, mode, request) {
|
|
|
208
248
|
: (args) => mcpBridge.callCodeModeExecute(args);
|
|
209
249
|
const requestedTool = getExternalWebSearchTool(request);
|
|
210
250
|
if (!WEB_SEARCH_PROVIDER_TOOLS.has(requestedTool)) {
|
|
211
|
-
return call
|
|
251
|
+
return callWithDaemonRetry(call, request);
|
|
212
252
|
}
|
|
213
253
|
const providers = webSearchProviderOrder(requestedTool);
|
|
214
254
|
const failures = [];
|
|
@@ -217,7 +257,7 @@ async function callCodeModeWithWebResilience(mcpBridge, mode, request) {
|
|
|
217
257
|
for (let attempt = 1; attempt <= WEB_SEARCH_MAX_ATTEMPTS_PER_PROVIDER; attempt += 1) {
|
|
218
258
|
try {
|
|
219
259
|
await enforceWebSearchPacing(provider);
|
|
220
|
-
return await call
|
|
260
|
+
return await callWithDaemonRetry(call, providerRequest);
|
|
221
261
|
}
|
|
222
262
|
catch (error) {
|
|
223
263
|
failures.push(`${provider} (attempt ${attempt}): ${toErrorMessage(error)}`);
|
|
@@ -318,7 +358,7 @@ function normalizeAction(aliasesByDomain, domain, action) {
|
|
|
318
358
|
return normalizedAction;
|
|
319
359
|
return domainAliases[normalizedAction] ?? normalizedAction;
|
|
320
360
|
}
|
|
321
|
-
async function defaultSageCommandRunner(request) {
|
|
361
|
+
export async function defaultSageCommandRunner(request) {
|
|
322
362
|
return new Promise((resolve, reject) => {
|
|
323
363
|
const proc = spawn(request.command, request.args, {
|
|
324
364
|
env: { ...process.env, ...request.env },
|
|
@@ -339,12 +379,18 @@ async function defaultSageCommandRunner(request) {
|
|
|
339
379
|
clearTimeout(timeout);
|
|
340
380
|
resolve(result);
|
|
341
381
|
};
|
|
342
|
-
|
|
382
|
+
const fail = (err) => {
|
|
343
383
|
if (settled)
|
|
344
384
|
return;
|
|
345
385
|
settled = true;
|
|
346
386
|
clearTimeout(timeout);
|
|
347
387
|
reject(err);
|
|
388
|
+
};
|
|
389
|
+
proc.on("error", (err) => {
|
|
390
|
+
fail(err);
|
|
391
|
+
});
|
|
392
|
+
proc.stdin.on("error", (err) => {
|
|
393
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
348
394
|
});
|
|
349
395
|
proc.stdout.setEncoding("utf8");
|
|
350
396
|
proc.stderr.setEncoding("utf8");
|
|
@@ -362,10 +408,19 @@ async function defaultSageCommandRunner(request) {
|
|
|
362
408
|
timedOut,
|
|
363
409
|
});
|
|
364
410
|
});
|
|
365
|
-
if (request.stdin
|
|
366
|
-
proc.stdin.
|
|
411
|
+
if (!request.stdin) {
|
|
412
|
+
proc.stdin.end();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (!proc.stdin.writable || proc.stdin.destroyed) {
|
|
416
|
+
proc.stdin.end();
|
|
417
|
+
return;
|
|
367
418
|
}
|
|
368
|
-
proc.stdin.end()
|
|
419
|
+
proc.stdin.end(request.stdin, (err) => {
|
|
420
|
+
if (!err)
|
|
421
|
+
return;
|
|
422
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
423
|
+
});
|
|
369
424
|
});
|
|
370
425
|
}
|
|
371
426
|
function extractText(input) {
|
|
@@ -526,6 +581,7 @@ export class SageMcpBridge {
|
|
|
526
581
|
ready = false;
|
|
527
582
|
stopped = false;
|
|
528
583
|
startPromise = null;
|
|
584
|
+
stderrBuffer = "";
|
|
529
585
|
constructor(command, args, env = {}, requestTimeoutMs = 20_000) {
|
|
530
586
|
this.command = command;
|
|
531
587
|
this.args = args;
|
|
@@ -630,12 +686,10 @@ export class SageMcpBridge {
|
|
|
630
686
|
proc.on("exit", (code) => {
|
|
631
687
|
if (this.stopped)
|
|
632
688
|
return;
|
|
633
|
-
|
|
634
|
-
rejectOnce(new Error(`MCP process exited before initialization (code ${code ?? -1})`));
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
689
|
+
const message = this.withRecentStderr(`MCP process exited${this.ready ? '' : ' before initialization'} (code ${code ?? -1})`);
|
|
637
690
|
this.ready = false;
|
|
638
|
-
this.rejectAllPending(
|
|
691
|
+
this.rejectAllPending(message);
|
|
692
|
+
rejectOnce(new Error(message));
|
|
639
693
|
});
|
|
640
694
|
proc.stdin.on("error", () => {
|
|
641
695
|
// Swallow EPIPE — the exit handler above handles cleanup
|
|
@@ -651,6 +705,10 @@ export class SageMcpBridge {
|
|
|
651
705
|
this.handleLine(line);
|
|
652
706
|
}
|
|
653
707
|
});
|
|
708
|
+
proc.stderr.setEncoding("utf8");
|
|
709
|
+
proc.stderr.on("data", (chunk) => {
|
|
710
|
+
this.appendStderr(chunk);
|
|
711
|
+
});
|
|
654
712
|
this.proc = proc;
|
|
655
713
|
settled = true;
|
|
656
714
|
resolve();
|
|
@@ -665,11 +723,17 @@ export class SageMcpBridge {
|
|
|
665
723
|
const id = randomUUID();
|
|
666
724
|
const timeout = setTimeout(() => {
|
|
667
725
|
this.pending.delete(id);
|
|
668
|
-
reject(new Error(`MCP request timed out: ${method}`));
|
|
726
|
+
reject(new Error(this.withRecentStderr(`MCP request timed out: ${method}`)));
|
|
669
727
|
}, this.requestTimeoutMs);
|
|
670
728
|
this.pending.set(id, { resolve, reject, timeout });
|
|
671
729
|
const req = { jsonrpc: "2.0", id, method, params };
|
|
672
|
-
this.proc.stdin.write(JSON.stringify(req) + "\n")
|
|
730
|
+
this.proc.stdin.write(JSON.stringify(req) + "\n", (err) => {
|
|
731
|
+
if (!err)
|
|
732
|
+
return;
|
|
733
|
+
this.pending.delete(id);
|
|
734
|
+
clearTimeout(timeout);
|
|
735
|
+
reject(new Error(this.withRecentStderr(`Failed to write MCP request: ${err.message}`)));
|
|
736
|
+
});
|
|
673
737
|
});
|
|
674
738
|
}
|
|
675
739
|
notify(method, params) {
|
|
@@ -707,6 +771,19 @@ export class SageMcpBridge {
|
|
|
707
771
|
}
|
|
708
772
|
this.pending.clear();
|
|
709
773
|
}
|
|
774
|
+
appendStderr(chunk) {
|
|
775
|
+
this.stderrBuffer += chunk;
|
|
776
|
+
const maxChars = 4000;
|
|
777
|
+
if (this.stderrBuffer.length > maxChars) {
|
|
778
|
+
this.stderrBuffer = this.stderrBuffer.slice(-maxChars);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
withRecentStderr(message) {
|
|
782
|
+
const stderr = this.stderrBuffer.trim();
|
|
783
|
+
if (!stderr)
|
|
784
|
+
return message;
|
|
785
|
+
return `${message}\nRecent MCP stderr:\n${stderr}`;
|
|
786
|
+
}
|
|
710
787
|
}
|
|
711
788
|
export class SageHookRuntime {
|
|
712
789
|
config;
|
|
@@ -719,6 +796,103 @@ export class SageHookRuntime {
|
|
|
719
796
|
const result = await this.run(["skill", "context", "--format", "opencode"], { SAGE_SESSION_ID: sessionId ?? "" }, undefined, true);
|
|
720
797
|
return result?.stdout.trim() ?? "";
|
|
721
798
|
}
|
|
799
|
+
/**
|
|
800
|
+
* Gather dynamic Sage Protocol identity context for system prompt injection.
|
|
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.
|
|
805
|
+
*
|
|
806
|
+
* The result is cached for `identityCacheTtlMs` (default 60 s) to avoid
|
|
807
|
+
* redundant CLI calls on every turn.
|
|
808
|
+
*/
|
|
809
|
+
identityCache = null;
|
|
810
|
+
identityCacheTtlMs = 60_000;
|
|
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),
|
|
818
|
+
this.run(["library", "list"], {}, undefined, true),
|
|
819
|
+
this.run(["library", "active"], {}, undefined, true),
|
|
820
|
+
]);
|
|
821
|
+
const lines = [];
|
|
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
|
+
}
|
|
857
|
+
const libraryOut = libraryResult.status === "fulfilled" ? libraryResult.value?.stdout.trim() ?? "" : "";
|
|
858
|
+
if (libraryOut) {
|
|
859
|
+
let totalSkills = 0;
|
|
860
|
+
let totalPrompts = 0;
|
|
861
|
+
let libraryCount = 0;
|
|
862
|
+
for (const line of libraryOut.split("\n")) {
|
|
863
|
+
const m = line.match(/\((\d+)\s+prompts?,\s*(\d+)\s+skills?\)/);
|
|
864
|
+
if (m) {
|
|
865
|
+
libraryCount++;
|
|
866
|
+
totalPrompts += Number.parseInt(m[1], 10);
|
|
867
|
+
totalSkills += Number.parseInt(m[2], 10);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (libraryCount > 0) {
|
|
871
|
+
lines.push(`- ${libraryCount} libraries, ${totalSkills} skills, ${totalPrompts} prompts installed`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
const PROTOCOL_DESC = "Sage Protocol is a decentralized network for curating prompts, skills, and knowledge on Base (L2).\n" +
|
|
875
|
+
"Libraries are like gardens — cultivated by communities around shared interests, taste, and tooling preferences.\n" +
|
|
876
|
+
"Users follow creators, tip valuable work, and post bounties for skills they need.\n" +
|
|
877
|
+
"Skills stay valuable when kept current with tooling and documentation — different knowledge bases and context work together to provide compounding value.\n" +
|
|
878
|
+
"SXXX is the governance token: vote, create DAOs, stake on proposals, shape the protocol. Burns from activity create deflationary pressure.\n" +
|
|
879
|
+
"Credits are the utility currency for IPFS storage and API services.\n" +
|
|
880
|
+
"The more skills published and curated, the more valuable discovery becomes for every user and agent — a compounding network effect.\n" +
|
|
881
|
+
"Use sage_search, sage_execute, and sage_status to interact with the protocol.";
|
|
882
|
+
const KEY_COMMANDS = "### Key Commands\n" +
|
|
883
|
+
"- Search: `sage_search({ domain: \"skills\", action: \"search\", params: { query: \"...\" } })` or `sage search \"...\" --search-type skills`\n" +
|
|
884
|
+
"- Use skill: `sage_execute({ domain: \"skills\", action: \"use\", params: { key: \"...\" } })`\n" +
|
|
885
|
+
"- Tip: `sage tip <address> <amount>` or `sage social tip ...`\n" +
|
|
886
|
+
"- Bounty: `sage bounties create --title \"...\" --reward <amount>`\n" +
|
|
887
|
+
"- DAOs: `sage governance dao discover`\n" +
|
|
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;
|
|
895
|
+
}
|
|
722
896
|
async suggestSkill(prompt, sessionId) {
|
|
723
897
|
const trimmed = prompt.trim();
|
|
724
898
|
if (!trimmed)
|
|
@@ -791,7 +965,29 @@ export class SageHookRuntime {
|
|
|
791
965
|
}, JSON.stringify(payload), true);
|
|
792
966
|
return result ? parseSecurityDecision(result.stdout) : null;
|
|
793
967
|
}
|
|
968
|
+
/**
|
|
969
|
+
* Resolve skill content by name(s) via `sage skill get`.
|
|
970
|
+
* Returns a map of skill name -> SKILL.md content (or empty string on miss).
|
|
971
|
+
* Used to auto-inject skills into subagent contexts when Pi can't resolve them locally.
|
|
972
|
+
*/
|
|
973
|
+
async resolveSkills(names) {
|
|
974
|
+
const results = new Map();
|
|
975
|
+
if (!names.length)
|
|
976
|
+
return results;
|
|
977
|
+
const settled = await Promise.allSettled(names.map(async (name) => {
|
|
978
|
+
const result = await this.run(["skill", "get", name, "--format", "raw"], { SAGE_SOURCE: this.config.source ?? "pi" }, undefined, true);
|
|
979
|
+
const content = result?.stdout.trim() ?? "";
|
|
980
|
+
return { name, content };
|
|
981
|
+
}));
|
|
982
|
+
for (const outcome of settled) {
|
|
983
|
+
if (outcome.status === "fulfilled" && outcome.value.content) {
|
|
984
|
+
results.set(outcome.value.name, outcome.value.content);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return results;
|
|
988
|
+
}
|
|
794
989
|
async run(args, env, stdin, ignoreFailures = false) {
|
|
990
|
+
const commandText = formatSageCommand(this.config.sageBin, args);
|
|
795
991
|
try {
|
|
796
992
|
const commandEnv = {
|
|
797
993
|
HOME: process.env.HOME ?? "",
|
|
@@ -809,17 +1005,25 @@ export class SageHookRuntime {
|
|
|
809
1005
|
stdin,
|
|
810
1006
|
timeoutMs: this.config.timeoutMs,
|
|
811
1007
|
});
|
|
812
|
-
if (
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1008
|
+
if (result.timedOut || result.exitCode !== 0) {
|
|
1009
|
+
const detail = (result.stderr || result.stdout || "").trim();
|
|
1010
|
+
const message = result.timedOut
|
|
1011
|
+
? `[sage-pi-adapter] Sage command timed out: ${commandText}`
|
|
1012
|
+
: `[sage-pi-adapter] Sage command failed (${result.exitCode}): ${commandText}${detail ? `\n${detail}` : ""}`;
|
|
1013
|
+
if (ignoreFailures) {
|
|
1014
|
+
console.error(message);
|
|
1015
|
+
return result;
|
|
1016
|
+
}
|
|
1017
|
+
throw new Error(message);
|
|
817
1018
|
}
|
|
818
1019
|
return result;
|
|
819
1020
|
}
|
|
820
1021
|
catch (error) {
|
|
1022
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1023
|
+
const message = `[sage-pi-adapter] Sage command error: ${commandText}${detail ? `\n${detail}` : ""}`;
|
|
821
1024
|
if (!ignoreFailures)
|
|
822
|
-
throw
|
|
1025
|
+
throw new Error(message);
|
|
1026
|
+
console.error(message);
|
|
823
1027
|
return null;
|
|
824
1028
|
}
|
|
825
1029
|
}
|
|
@@ -849,8 +1053,6 @@ export function createSageSearchTool(mcpBridge) {
|
|
|
849
1053
|
"Use sage_search (NOT bash sage commands) for web searches, documentation lookups, and prompt/skill discovery",
|
|
850
1054
|
"NEVER run 'sage' CLI via bash -- always use the sage_search or sage_execute tools instead",
|
|
851
1055
|
"For web search: sage_search({ domain: 'external', action: 'search', params: { tool_name: 'web_search', tool_params: { query: '...' } } })",
|
|
852
|
-
"For GitHub code search: sage_search({ domain: 'external', action: 'search', params: { tool_name: 'github_search_code', tool_params: { query: '...' } } })",
|
|
853
|
-
"For library docs: sage_search({ domain: 'external', action: 'search', params: { tool_name: 'get_library_docs', tool_params: { context7CompatibleLibraryID: '/lib/id' } } })",
|
|
854
1056
|
"To list available tools: sage_search({ domain: 'external', action: 'list_servers' })",
|
|
855
1057
|
],
|
|
856
1058
|
description: "Search the web, find documentation, search prompts/skills, and query external services. " +
|
|
@@ -909,10 +1111,8 @@ export function createSageExecuteTool(mcpBridge) {
|
|
|
909
1111
|
label: "Sage: Execute",
|
|
910
1112
|
promptSnippet: "Execute mutations: call external tools, use pi's hub-compat layer, create prompts, fetch URLs",
|
|
911
1113
|
promptGuidelines: [
|
|
912
|
-
"Use sage_execute (NOT bash sage commands) for calling external tools, fetching URLs, and
|
|
1114
|
+
"Use sage_execute (NOT bash sage commands) for calling external tools, fetching URLs, and managing MCP servers",
|
|
913
1115
|
"To fetch a URL: sage_execute({ domain: 'external', action: 'call', params: { tool_name: 'fetch', tool_params: { url: 'https://...' } } })",
|
|
914
|
-
"In pi, hub lifecycle remains available through the compatibility wrapper: sage_execute({ domain: 'hub', action: 'start', params: { server_id: 'duckduckgo' } })",
|
|
915
|
-
"To call any external tool: sage_execute({ domain: 'external', action: 'call', params: { tool_name: 'tool_name', tool_params: { ... } } })",
|
|
916
1116
|
],
|
|
917
1117
|
description: "Execute mutations: create prompts, use skills, send chat messages, call external tools, and manage external MCP servers through pi's hub-compat wrapper. " +
|
|
918
1118
|
"Preferred raw MCP clients use direct hub_* tools; in pi use domain='hub', action='start', params={server_id:'duckduckgo'} when needed. " +
|
|
@@ -988,6 +1188,40 @@ export function createSageExecuteTool(mcpBridge) {
|
|
|
988
1188
|
},
|
|
989
1189
|
};
|
|
990
1190
|
}
|
|
1191
|
+
// Tier 3: Complete CLI reference — available on-demand via sage_status().
|
|
1192
|
+
// Not injected into system prompt; model requests it when needed.
|
|
1193
|
+
const SAGE_CLI_REFERENCE = `
|
|
1194
|
+
## Sage CLI Reference (Tier 3 — on-demand)
|
|
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)
|
|
1201
|
+
|
|
1202
|
+
### Rust CLI Fallback (when user asks for terminal commands)
|
|
1203
|
+
- Discovery: sage search "<query>" --search-type skills --scope both
|
|
1204
|
+
- Suggest: sage suggest skill "<intent>" --limit 3 --provision
|
|
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
|
|
1209
|
+
|
|
1210
|
+
### Library / DAO / Bounty Recipes
|
|
1211
|
+
- Library: sage library create <name>; sage library skill add <path> -l <library>; sage library prompt add <name> -l <library> --file <path>; sage library push <library>; sage library promote <library> --dao 0x... --collection default
|
|
1212
|
+
- DAO: sage governance dao create --name "My DAO" --description "..." --governance personal|team|community [--operator 0x...|--burn 1500]
|
|
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 ...
|
|
1224
|
+
`.trim();
|
|
991
1225
|
export function createSageStatusTool(mcpBridge) {
|
|
992
1226
|
return {
|
|
993
1227
|
name: "sage_status",
|
|
@@ -1015,6 +1249,10 @@ export function createSageStatusTool(mcpBridge) {
|
|
|
1015
1249
|
context: extractJsonFromMcpResult(context) ?? context,
|
|
1016
1250
|
}, null, 2),
|
|
1017
1251
|
},
|
|
1252
|
+
{
|
|
1253
|
+
type: "text",
|
|
1254
|
+
text: SAGE_CLI_REFERENCE,
|
|
1255
|
+
},
|
|
1018
1256
|
],
|
|
1019
1257
|
};
|
|
1020
1258
|
}
|
|
@@ -1031,6 +1269,10 @@ export function createSageStatusTool(mcpBridge) {
|
|
|
1031
1269
|
sageAdapterVersion: "0.2.0",
|
|
1032
1270
|
}, null, 2),
|
|
1033
1271
|
},
|
|
1272
|
+
{
|
|
1273
|
+
type: "text",
|
|
1274
|
+
text: SAGE_CLI_REFERENCE,
|
|
1275
|
+
},
|
|
1034
1276
|
],
|
|
1035
1277
|
details: { bridgeConnected },
|
|
1036
1278
|
};
|
|
@@ -1187,6 +1429,17 @@ export async function createSageSessionConfig(config = {}) {
|
|
|
1187
1429
|
.catch((error) => {
|
|
1188
1430
|
console.error("[sage-pi-adapter] failed to inject session context:", error);
|
|
1189
1431
|
});
|
|
1432
|
+
// Inject dynamic identity context (wallet, libraries, skills) at session start.
|
|
1433
|
+
hookRuntime
|
|
1434
|
+
.getIdentityContext()
|
|
1435
|
+
.then(async (identity) => {
|
|
1436
|
+
if (!identity)
|
|
1437
|
+
return;
|
|
1438
|
+
await injectHookMessage(session, "sage-identity-context", identity, false);
|
|
1439
|
+
})
|
|
1440
|
+
.catch((error) => {
|
|
1441
|
+
console.error("[sage-pi-adapter] failed to inject identity context:", error);
|
|
1442
|
+
});
|
|
1190
1443
|
const unsubscribe = session.subscribe((event) => {
|
|
1191
1444
|
const parsed = parseMessageEndEvent(event);
|
|
1192
1445
|
if (!parsed)
|