@agentwonderland/mcp 0.1.40 → 0.1.42

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/dist/setup.js ADDED
@@ -0,0 +1,346 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { homedir, platform } from "node:os";
4
+ import path from "node:path";
5
+ import { createInterface } from "node:readline/promises";
6
+ import { stdin as input, stdout as output } from "node:process";
7
+ import { execFileSync } from "node:child_process";
8
+ export const SERVER_NAME = "agentwonderland";
9
+ export const SERVER_CONFIG = {
10
+ command: "npx",
11
+ args: ["-y", "@agentwonderland/mcp"],
12
+ };
13
+ const CLIENT_ORDER = [
14
+ "codex",
15
+ "claude-code",
16
+ "claude-desktop",
17
+ "cursor",
18
+ "antigravity",
19
+ ];
20
+ const CLIENT_LABELS = {
21
+ codex: "Codex",
22
+ "claude-code": "Claude Code",
23
+ "claude-desktop": "Claude Desktop",
24
+ cursor: "Cursor",
25
+ antigravity: "Google Antigravity",
26
+ };
27
+ export async function runSetupCli(args = process.argv.slice(2)) {
28
+ const options = parseSetupArgs(args);
29
+ if (options.help) {
30
+ printSetupHelp();
31
+ return;
32
+ }
33
+ const clients = getSetupClients();
34
+ const selected = await chooseClients(clients, options);
35
+ if (selected.length === 0) {
36
+ console.log("No MCP clients selected. Nothing changed.");
37
+ printManualConfig();
38
+ return;
39
+ }
40
+ console.log("");
41
+ console.log("Agent Wonderland will add this MCP server entry:");
42
+ console.log(JSON.stringify({ mcpServers: { [SERVER_NAME]: SERVER_CONFIG } }, null, 2));
43
+ console.log("");
44
+ const results = [];
45
+ for (const client of selected) {
46
+ results.push(await installClient(client, options));
47
+ }
48
+ printResults(results, options);
49
+ }
50
+ export function parseSetupArgs(args) {
51
+ const options = {};
52
+ for (let index = 0; index < args.length; index++) {
53
+ const arg = args[index];
54
+ if (arg === "--help" || arg === "-h") {
55
+ options.help = true;
56
+ }
57
+ else if (arg === "--all") {
58
+ options.all = true;
59
+ }
60
+ else if (arg === "--yes" || arg === "-y") {
61
+ options.yes = true;
62
+ }
63
+ else if (arg === "--dry-run") {
64
+ options.dryRun = true;
65
+ }
66
+ else if (arg === "--clients") {
67
+ const value = args[index + 1];
68
+ if (!value)
69
+ throw new Error("--clients requires a comma-separated value");
70
+ options.clients = parseClientList(value);
71
+ index++;
72
+ }
73
+ else if (arg.startsWith("--clients=")) {
74
+ options.clients = parseClientList(arg.slice("--clients=".length));
75
+ }
76
+ else {
77
+ throw new Error(`Unknown setup option: ${arg}`);
78
+ }
79
+ }
80
+ return options;
81
+ }
82
+ function parseClientList(value) {
83
+ const selected = value
84
+ .split(",")
85
+ .map((item) => item.trim().toLowerCase())
86
+ .filter(Boolean);
87
+ const invalid = selected.filter((item) => !CLIENT_ORDER.includes(item));
88
+ if (invalid.length > 0) {
89
+ throw new Error(`Unknown client(s): ${invalid.join(", ")}`);
90
+ }
91
+ return selected;
92
+ }
93
+ export function getSetupClients(home = homedir(), env = process.env) {
94
+ const appData = env.APPDATA ?? path.join(home, "AppData", "Roaming");
95
+ const isWindows = platform() === "win32";
96
+ const isMac = platform() === "darwin";
97
+ const claudeDesktopPath = isMac
98
+ ? path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")
99
+ : isWindows
100
+ ? path.join(appData, "Claude", "claude_desktop_config.json")
101
+ : path.join(home, ".config", "Claude", "claude_desktop_config.json");
102
+ const clients = [
103
+ {
104
+ id: "codex",
105
+ name: CLIENT_LABELS.codex,
106
+ path: path.join(home, ".codex", "config.toml"),
107
+ format: "toml",
108
+ detected: existsSync(path.join(home, ".codex")) || commandExists("codex"),
109
+ note: "Global Codex CLI/Desktop config",
110
+ },
111
+ {
112
+ id: "claude-code",
113
+ name: CLIENT_LABELS["claude-code"],
114
+ path: "claude mcp add-json --scope user",
115
+ format: "claude-code-cli",
116
+ detected: commandExists("claude"),
117
+ note: "Claude Code user-scope MCP config via the Claude CLI",
118
+ },
119
+ {
120
+ id: "claude-desktop",
121
+ name: CLIENT_LABELS["claude-desktop"],
122
+ path: claudeDesktopPath,
123
+ format: "json",
124
+ detected: existsSync(claudeDesktopPath) || existsSync(path.dirname(claudeDesktopPath)),
125
+ note: "Claude Desktop developer config",
126
+ },
127
+ {
128
+ id: "cursor",
129
+ name: CLIENT_LABELS.cursor,
130
+ path: path.join(home, ".cursor", "mcp.json"),
131
+ format: "json",
132
+ detected: existsSync(path.join(home, ".cursor")),
133
+ note: "Global Cursor MCP config",
134
+ },
135
+ {
136
+ id: "antigravity",
137
+ name: CLIENT_LABELS.antigravity,
138
+ path: path.join(home, ".gemini", "antigravity", "mcp_config.json"),
139
+ format: "json",
140
+ detected: existsSync(path.join(home, ".gemini", "antigravity")),
141
+ note: "Antigravity raw MCP config",
142
+ },
143
+ ];
144
+ return clients;
145
+ }
146
+ async function chooseClients(clients, options) {
147
+ if (options.clients?.length) {
148
+ return clients.filter((client) => options.clients?.includes(client.id));
149
+ }
150
+ if (options.all) {
151
+ return clients;
152
+ }
153
+ const detected = clients.filter((client) => client.detected);
154
+ if (options.yes) {
155
+ return detected.length > 0 ? detected : clients.filter((client) => client.id === "codex");
156
+ }
157
+ console.log("Agent Wonderland MCP setup");
158
+ console.log("");
159
+ console.log("Detected clients:");
160
+ for (const client of clients) {
161
+ const marker = client.detected ? "found" : "not found";
162
+ console.log(` ${client.id.padEnd(15)} ${marker.padEnd(9)} ${client.path}`);
163
+ }
164
+ console.log("");
165
+ const rl = createInterface({ input, output });
166
+ try {
167
+ const selected = [];
168
+ for (const client of clients) {
169
+ const defaultAnswer = client.detected ? "Y/n" : "y/N";
170
+ const answer = (await rl.question(`Install for ${client.name}? (${defaultAnswer}) `)).trim().toLowerCase();
171
+ const yes = answer === ""
172
+ ? client.detected
173
+ : answer === "y" || answer === "yes";
174
+ if (yes)
175
+ selected.push(client);
176
+ }
177
+ return selected;
178
+ }
179
+ finally {
180
+ rl.close();
181
+ }
182
+ }
183
+ export async function installClient(client, options = {}) {
184
+ try {
185
+ if (client.format === "claude-code-cli") {
186
+ return installClaudeCode(client, options);
187
+ }
188
+ const existing = await readExisting(client.path);
189
+ const next = client.format === "json"
190
+ ? mergeJsonMcpConfig(existing)
191
+ : mergeCodexTomlConfig(existing);
192
+ if (next === existing) {
193
+ return { client, status: "unchanged", message: "already configured" };
194
+ }
195
+ const backupPath = existing ? `${client.path}.bak-${timestamp()}` : undefined;
196
+ if (!options.dryRun) {
197
+ await mkdir(path.dirname(client.path), { recursive: true });
198
+ if (backupPath) {
199
+ await writeFile(backupPath, existing, "utf8");
200
+ }
201
+ await writeFile(client.path, next, "utf8");
202
+ }
203
+ return {
204
+ client,
205
+ status: existing ? "updated" : "created",
206
+ backupPath,
207
+ };
208
+ }
209
+ catch (error) {
210
+ return {
211
+ client,
212
+ status: "failed",
213
+ message: error instanceof Error ? error.message : String(error),
214
+ };
215
+ }
216
+ }
217
+ function installClaudeCode(client, options) {
218
+ if (!commandExists("claude")) {
219
+ return {
220
+ client,
221
+ status: "failed",
222
+ message: [
223
+ "Claude Code CLI was not found on PATH.",
224
+ "Run manually after installing Claude Code:",
225
+ `claude mcp add-json ${SERVER_NAME} '${JSON.stringify({ type: "stdio", ...SERVER_CONFIG })}' --scope user`,
226
+ ].join(" "),
227
+ };
228
+ }
229
+ if (!options.dryRun) {
230
+ execFileSync("claude", [
231
+ "mcp",
232
+ "add-json",
233
+ SERVER_NAME,
234
+ JSON.stringify({ type: "stdio", ...SERVER_CONFIG }),
235
+ "--scope",
236
+ "user",
237
+ ], { stdio: "pipe" });
238
+ }
239
+ return {
240
+ client,
241
+ status: options.dryRun ? "skipped" : "updated",
242
+ message: options.dryRun ? "would run Claude Code CLI installer" : "installed with Claude Code CLI",
243
+ };
244
+ }
245
+ async function readExisting(filePath) {
246
+ try {
247
+ return await readFile(filePath, "utf8");
248
+ }
249
+ catch (error) {
250
+ if (error.code === "ENOENT")
251
+ return "";
252
+ throw error;
253
+ }
254
+ }
255
+ export function mergeJsonMcpConfig(existing) {
256
+ const parsed = existing.trim()
257
+ ? JSON.parse(existing)
258
+ : {};
259
+ const mcpServers = typeof parsed.mcpServers === "object" && parsed.mcpServers !== null && !Array.isArray(parsed.mcpServers)
260
+ ? parsed.mcpServers
261
+ : {};
262
+ const next = {
263
+ ...parsed,
264
+ mcpServers: {
265
+ ...mcpServers,
266
+ [SERVER_NAME]: SERVER_CONFIG,
267
+ },
268
+ };
269
+ return `${JSON.stringify(next, null, 2)}\n`;
270
+ }
271
+ export function mergeCodexTomlConfig(existing) {
272
+ const block = [
273
+ `[mcp_servers.${SERVER_NAME}]`,
274
+ `command = "npx"`,
275
+ `args = ["-y", "@agentwonderland/mcp"]`,
276
+ "",
277
+ ].join("\n");
278
+ const pattern = new RegExp(`\\n?\\[mcp_servers\\.${SERVER_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\][\\s\\S]*?(?=\\n\\[[^\\]]+\\]|$)`);
279
+ const trimmed = existing.trimEnd();
280
+ if (!trimmed)
281
+ return block;
282
+ if (pattern.test(trimmed)) {
283
+ const next = trimmed.replace(pattern, `\n${block.trimEnd()}`);
284
+ return `${next.trimEnd()}\n`;
285
+ }
286
+ return `${trimmed}\n\n${block}`;
287
+ }
288
+ function printResults(results, options) {
289
+ console.log(options.dryRun ? "Dry run complete:" : "Setup complete:");
290
+ for (const result of results) {
291
+ const suffix = result.backupPath ? ` (backup: ${result.backupPath})` : "";
292
+ const detail = result.message ? ` - ${result.message}` : "";
293
+ console.log(` ${result.status.padEnd(9)} ${result.client.name}: ${result.client.path}${suffix}${detail}`);
294
+ }
295
+ console.log("");
296
+ console.log("Restart any clients you updated, then ask your coding agent:");
297
+ console.log(' "Check my Agent Wonderland wallet status."');
298
+ console.log("");
299
+ console.log("If no wallet is configured yet, ask:");
300
+ console.log(' "Set up payment for Agent Wonderland."');
301
+ }
302
+ function printSetupHelp() {
303
+ console.log([
304
+ "Agent Wonderland MCP setup",
305
+ "",
306
+ "Usage:",
307
+ " npx @agentwonderland/mcp setup",
308
+ " npx @agentwonderland/mcp setup --clients codex,claude-code",
309
+ " npx @agentwonderland/mcp setup --all --yes",
310
+ "",
311
+ "Supported clients:",
312
+ ` ${CLIENT_ORDER.join(", ")}`,
313
+ "",
314
+ "Options:",
315
+ " --clients <list> Comma-separated client ids",
316
+ " --all Offer/install every supported config target",
317
+ " -y, --yes Accept defaults without prompts",
318
+ " --dry-run Show what would be changed without writing files",
319
+ " -h, --help Show this help",
320
+ ].join("\n"));
321
+ }
322
+ function printManualConfig() {
323
+ console.log("");
324
+ console.log("Manual MCP config:");
325
+ console.log(JSON.stringify({ mcpServers: { [SERVER_NAME]: SERVER_CONFIG } }, null, 2));
326
+ }
327
+ function timestamp() {
328
+ return new Date().toISOString().replace(/[:.]/g, "-");
329
+ }
330
+ function commandExists(command) {
331
+ try {
332
+ if (platform() === "win32") {
333
+ execFileSync("where", [command], { stdio: "ignore" });
334
+ }
335
+ else {
336
+ execFileSync("sh", ["-lc", `command -v ${shellQuote(command)}`], { stdio: "ignore" });
337
+ }
338
+ return true;
339
+ }
340
+ catch {
341
+ return false;
342
+ }
343
+ }
344
+ function shellQuote(value) {
345
+ return `'${value.replace(/'/g, "'\\''")}'`;
346
+ }
@@ -213,6 +213,8 @@ export function registerWalletTools(server) {
213
213
  "",
214
214
  chainStatus,
215
215
  "",
216
+ "No MCP restart is required for wallet changes.",
217
+ "",
216
218
  `Fund this address with USDC on ${defaultCh === "solana" ? "Solana" : "Tempo"} to start using agents.`,
217
219
  ].join("\n"));
218
220
  }
@@ -244,6 +246,8 @@ export function registerWalletTools(server) {
244
246
  ` Chains: solana`,
245
247
  ` Consumer principal: ${principal}`,
246
248
  "",
249
+ "No MCP restart is required for wallet changes.",
250
+ "",
247
251
  "Fund this address with USDC on Solana to start using agents.",
248
252
  ].join("\n") + owsNudge);
249
253
  }
@@ -269,6 +273,8 @@ export function registerWalletTools(server) {
269
273
  ` Chains: tempo, base`,
270
274
  ` Consumer principal: ${principal}`,
271
275
  "",
276
+ "No MCP restart is required for wallet changes.",
277
+ "",
272
278
  `Fund this address with USDC on Tempo or Base to start using agents.`,
273
279
  ].join("\n") + owsNudge);
274
280
  }
@@ -293,6 +299,8 @@ export function registerWalletTools(server) {
293
299
  ` Storage: ~/.ows/ (AES-256-GCM encrypted)`,
294
300
  ` Consumer principal: ${principal}`,
295
301
  "",
302
+ "No MCP restart is required for wallet changes.",
303
+ "",
296
304
  `Fund this address with USDC on ${defaultCh === "solana" ? "Solana" : "Tempo"} to start using agents.`,
297
305
  "For testnet: npx mppx account fund",
298
306
  ].join("\n"));
@@ -317,6 +325,8 @@ export function registerWalletTools(server) {
317
325
  ` Name: ${walletName}`,
318
326
  ` Chains: ${selectedChains.join(", ")}`,
319
327
  ` Consumer principal: ${principal}`,
328
+ "",
329
+ "No MCP restart is required for wallet changes.",
320
330
  ].join("\n"));
321
331
  }
322
332
  if (defaultCh === "solana") {
@@ -345,6 +355,8 @@ export function registerWalletTools(server) {
345
355
  ` Name: ${walletName}`,
346
356
  ` Chains: solana`,
347
357
  ` Consumer principal: ${principal}`,
358
+ "",
359
+ "No MCP restart is required for wallet changes.",
348
360
  ].join("\n") + owsNudge);
349
361
  }
350
362
  catch (err) {
@@ -376,6 +388,8 @@ export function registerWalletTools(server) {
376
388
  ` Name: ${walletName}`,
377
389
  ` Chains: ${selectedChains.join(", ")}`,
378
390
  ` Consumer principal: ${principal}`,
391
+ "",
392
+ "No MCP restart is required for wallet changes.",
379
393
  ].join("\n") + owsNudge);
380
394
  }
381
395
  catch {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@agentwonderland/mcp",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
4
4
  "type": "module",
5
5
  "description": "MCP server for the Agent Wonderland AI agent marketplace",
6
6
  "bin": {
7
- "agentwonderland-mcp": "./dist/index.js"
7
+ "agentwonderland-mcp": "dist/index.js"
8
8
  },
9
9
  "exports": {
10
10
  ".": {
@@ -23,6 +23,7 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@modelcontextprotocol/sdk": "^1.12.1",
26
+ "@scure/bip39": "^1.6.0",
26
27
  "@solana/spl-token": "^0.4.14",
27
28
  "@solana/web3.js": "1.95.8",
28
29
  "qrcode": "^1.5.4",
@@ -72,7 +72,7 @@ describe("api-client headers", () => {
72
72
  "X-AW-Rebate-Principal":
73
73
  "did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
74
74
  "X-AW-Surface": "mcp",
75
- "X-AW-MCP-Version": "0.1.37",
75
+ "X-AW-MCP-Version": "0.1.42",
76
76
  "X-AW-MCP-Tool": "run_agent",
77
77
  "X-AW-MCP-Action": "execute",
78
78
  }),
@@ -0,0 +1,88 @@
1
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import {
6
+ installClient,
7
+ mergeCodexTomlConfig,
8
+ mergeJsonMcpConfig,
9
+ parseSetupArgs,
10
+ type SetupClient,
11
+ } from "../../setup.js";
12
+
13
+ let tempDirs: string[] = [];
14
+
15
+ afterEach(async () => {
16
+ await Promise.all(tempDirs.map((dir) => rm(dir, { recursive: true, force: true })));
17
+ tempDirs = [];
18
+ });
19
+
20
+ describe("MCP setup", () => {
21
+ it("merges the server into existing JSON MCP config", () => {
22
+ const next = JSON.parse(mergeJsonMcpConfig(JSON.stringify({
23
+ mcpServers: {
24
+ existing: { command: "node", args: ["server.js"] },
25
+ },
26
+ otherSetting: true,
27
+ })));
28
+
29
+ expect(next.otherSetting).toBe(true);
30
+ expect(next.mcpServers.existing.command).toBe("node");
31
+ expect(next.mcpServers.agentwonderland).toEqual({
32
+ command: "npx",
33
+ args: ["-y", "@agentwonderland/mcp"],
34
+ });
35
+ });
36
+
37
+ it("replaces an existing Codex TOML block without disturbing other servers", () => {
38
+ const next = mergeCodexTomlConfig([
39
+ "[mcp_servers.other]",
40
+ 'command = "node"',
41
+ 'args = ["other.js"]',
42
+ "",
43
+ "[mcp_servers.agentwonderland]",
44
+ 'command = "old"',
45
+ 'args = ["old"]',
46
+ "",
47
+ "[model_provider.openai]",
48
+ 'name = "OpenAI"',
49
+ "",
50
+ ].join("\n"));
51
+
52
+ expect(next).toContain("[mcp_servers.other]");
53
+ expect(next).toContain("[mcp_servers.agentwonderland]");
54
+ expect(next).toContain('command = "npx"');
55
+ expect(next).toContain('args = ["-y", "@agentwonderland/mcp"]');
56
+ expect(next).not.toContain('command = "old"');
57
+ expect(next).toContain("[model_provider.openai]");
58
+ });
59
+
60
+ it("writes config and creates a backup when updating", async () => {
61
+ const dir = await mkdtemp(path.join(tmpdir(), "aw-mcp-setup-"));
62
+ tempDirs.push(dir);
63
+ const configPath = path.join(dir, "mcp.json");
64
+ await writeFile(configPath, JSON.stringify({ mcpServers: { old: { command: "node" } } }), "utf8");
65
+ const client: SetupClient = {
66
+ id: "cursor",
67
+ name: "Cursor",
68
+ path: configPath,
69
+ format: "json",
70
+ detected: true,
71
+ note: "test",
72
+ };
73
+
74
+ const result = await installClient(client);
75
+ const written = JSON.parse(await readFile(configPath, "utf8"));
76
+
77
+ expect(result.status).toBe("updated");
78
+ expect(result.backupPath).toBeTruthy();
79
+ expect(written.mcpServers.agentwonderland.args).toEqual(["-y", "@agentwonderland/mcp"]);
80
+ });
81
+
82
+ it("parses client selection flags", () => {
83
+ expect(parseSetupArgs(["--clients", "codex,cursor", "--yes"])).toEqual({
84
+ clients: ["codex", "cursor"],
85
+ yes: true,
86
+ });
87
+ });
88
+ });
@@ -29,7 +29,7 @@ interface RequestOptions {
29
29
  extraHeaders?: Record<string, string>;
30
30
  }
31
31
 
32
- const MCP_VERSION = "0.1.37";
32
+ const MCP_VERSION = "0.1.42";
33
33
 
34
34
  function inferToolHeaders(path: string, method: string): Record<string, string> {
35
35
  if (path === "/solve") {
@@ -11,6 +11,9 @@
11
11
  import type { LocalAccount } from "viem/accounts";
12
12
  import type { Hex } from "viem";
13
13
  import type { Keypair } from "@solana/web3.js";
14
+ import { createHmac } from "node:crypto";
15
+ import { mnemonicToSeedSync, validateMnemonic } from "@scure/bip39";
16
+ import { wordlist } from "@scure/bip39/wordlists/english";
14
17
 
15
18
  // Note: OWS provides encrypted key storage at rest (~/.ows/, AES-256-GCM).
16
19
  // For EVM signing, we export the secp256k1 key and use viem's native
@@ -155,6 +158,56 @@ function keypairFromEd25519Hex(privateKeyHex: string, KeypairCtor: typeof import
155
158
  throw new Error(`Unsupported ed25519 key length: ${bytes.length} bytes.`);
156
159
  }
157
160
 
161
+ function parseExportedSecret(exported: string): { secp256k1?: string; ed25519?: string; mnemonic?: string } {
162
+ const secret = exported.trim();
163
+ try {
164
+ const parsed = JSON.parse(secret) as { secp256k1?: string; ed25519?: string; mnemonic?: string };
165
+ if (parsed.secp256k1 || parsed.ed25519 || parsed.mnemonic) return parsed;
166
+ } catch {
167
+ // OWS-created wallets export a BIP-39 mnemonic, not a JSON key object.
168
+ }
169
+
170
+ const hex = secret.startsWith("0x") ? secret.slice(2) : secret;
171
+ if (/^[0-9a-fA-F]+$/.test(hex)) {
172
+ if (hex.length === 64) return { secp256k1: hex, ed25519: hex };
173
+ if (hex.length === 128) return { ed25519: hex };
174
+ }
175
+
176
+ if (validateMnemonic(secret, wordlist)) {
177
+ return { mnemonic: secret };
178
+ }
179
+
180
+ return {};
181
+ }
182
+
183
+ function deriveSolanaSeedFromMnemonic(mnemonic: string, path = "m/44'/501'/0'/0'"): Uint8Array {
184
+ const seed = mnemonicToSeedSync(mnemonic);
185
+ let digest = createHmac("sha512", "ed25519 seed").update(seed).digest();
186
+ let key = digest.subarray(0, 32);
187
+ let chainCode = digest.subarray(32);
188
+
189
+ for (const part of path.split("/").slice(1)) {
190
+ if (!part.endsWith("'")) {
191
+ throw new Error(`Unsupported non-hardened ed25519 path segment: ${part}`);
192
+ }
193
+ const index = Number(part.slice(0, -1));
194
+ if (!Number.isInteger(index) || index < 0) {
195
+ throw new Error(`Unsupported ed25519 path segment: ${part}`);
196
+ }
197
+
198
+ const data = Buffer.alloc(37);
199
+ data[0] = 0;
200
+ Buffer.from(key).copy(data, 1);
201
+ data.writeUInt32BE(index + 0x80000000, 33);
202
+
203
+ digest = createHmac("sha512", chainCode).update(data).digest();
204
+ key = digest.subarray(0, 32);
205
+ chainCode = digest.subarray(32);
206
+ }
207
+
208
+ return Uint8Array.from(key);
209
+ }
210
+
158
211
  // ── Public API ───────────────────────────────────────────────────
159
212
 
160
213
  /**
@@ -278,7 +331,13 @@ export async function owsAccountFromWalletId(
278
331
  // (EIP-5806 delegate calls) which OWS's native signing can't serialize.
279
332
  // OWS provides encrypted storage at rest; viem handles signing in memory.
280
333
  const exported = ows.exportWallet(walletId);
281
- const keys = JSON.parse(exported) as { secp256k1?: string; ed25519?: string };
334
+ const keys = parseExportedSecret(exported);
335
+
336
+ if (keys.mnemonic) {
337
+ const { mnemonicToAccount } = await import("viem/accounts");
338
+ return mnemonicToAccount(keys.mnemonic);
339
+ }
340
+
282
341
  if (!keys.secp256k1) {
283
342
  throw new Error(`Wallet "${wallet.name}" has no secp256k1 key for EVM signing.`);
284
343
  }
@@ -295,12 +354,17 @@ export async function owsSolanaKeypairFromWalletId(
295
354
  findSolanaAccount(wallet);
296
355
 
297
356
  const exported = ows.exportWallet(walletId);
298
- const keys = JSON.parse(exported) as { secp256k1?: string; ed25519?: string };
357
+ const keys = parseExportedSecret(exported);
358
+ const { Keypair } = await import("@solana/web3.js");
359
+
360
+ if (keys.mnemonic) {
361
+ return Keypair.fromSeed(deriveSolanaSeedFromMnemonic(keys.mnemonic));
362
+ }
363
+
299
364
  if (!keys.ed25519) {
300
365
  throw new Error(`Wallet "${wallet.name}" has no ed25519 key for Solana signing.`);
301
366
  }
302
367
 
303
- const { Keypair } = await import("@solana/web3.js");
304
368
  return keypairFromEd25519Hex(keys.ed25519, Keypair);
305
369
  }
306
370