@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.
@@ -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(request);
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(providerRequest);
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
- proc.on("error", (err) => {
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 && proc.stdin.writable) {
366
- proc.stdin.write(request.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
- if (!this.ready) {
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(`MCP process exited (code ${code ?? -1})`);
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 (!ignoreFailures && (result.timedOut || result.exitCode !== 0)) {
813
- const reason = result.timedOut
814
- ? `timed out: ${this.config.sageBin} ${args.join(" ")}`
815
- : `failed (${result.exitCode}): ${result.stderr || result.stdout}`;
816
- throw new Error(reason);
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 error;
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 pi-compatible MCP server management",
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)