@truealter/sdk 0.2.4 → 0.4.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/dist/index.js CHANGED
@@ -1,6 +1,156 @@
1
+ import { p256 } from '@noble/curves/p256';
2
+ import { sha256 } from '@noble/hashes/sha256';
3
+ import { randomBytes, bytesToHex as bytesToHex$1, hexToBytes } from '@noble/hashes/utils';
1
4
  import * as ed25519 from '@noble/ed25519';
2
5
  import { sha512 } from '@noble/hashes/sha512';
3
- import { randomBytes, bytesToHex, hexToBytes } from '@noble/hashes/utils';
6
+ import { spawnSync } from 'child_process';
7
+ import { homedir, platform } from 'os';
8
+ import { join, resolve, dirname } from 'path';
9
+ import { env } from 'process';
10
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, copyFileSync, renameSync, unlinkSync } from 'fs';
11
+ import { createHash } from 'crypto';
12
+
13
+ var __defProp = Object.defineProperty;
14
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
15
+ var __getOwnPropNames = Object.getOwnPropertyNames;
16
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
17
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
18
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
19
+ }) : x)(function(x) {
20
+ if (typeof require !== "undefined") return require.apply(this, arguments);
21
+ throw Error('Dynamic require of "' + x + '" is not supported');
22
+ });
23
+ var __esm = (fn, res) => function __init() {
24
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
25
+ };
26
+ var __export = (target, all) => {
27
+ for (var name in all)
28
+ __defProp(target, name, { get: all[name], enumerable: true });
29
+ };
30
+ var __copyProps = (to, from, except, desc) => {
31
+ if (from && typeof from === "object" || typeof from === "function") {
32
+ for (let key of __getOwnPropNames(from))
33
+ if (!__hasOwnProp.call(to, key) && key !== except)
34
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
35
+ }
36
+ return to;
37
+ };
38
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
39
+
40
+ // src/signing.ts
41
+ var signing_exports = {};
42
+ __export(signing_exports, {
43
+ canonicalArgsSha256: () => canonicalArgsSha256,
44
+ canonicalStringify: () => canonicalStringify,
45
+ loadPrivateKey: () => loadPrivateKey,
46
+ signInvocation: () => signInvocation
47
+ });
48
+ function canonicalStringify(value) {
49
+ return stringifyInner(value);
50
+ }
51
+ function stringifyInner(value) {
52
+ if (value === null) return "null";
53
+ if (value === void 0) {
54
+ throw new TypeError("canonicalStringify: undefined is not representable in JSON");
55
+ }
56
+ if (typeof value === "boolean") return value ? "true" : "false";
57
+ if (typeof value === "number") {
58
+ if (!Number.isFinite(value)) {
59
+ throw new TypeError("canonicalStringify: non-finite numbers are not representable");
60
+ }
61
+ return JSON.stringify(value);
62
+ }
63
+ if (typeof value === "string") return encodeString(value);
64
+ if (Array.isArray(value)) {
65
+ return "[" + value.map((v) => stringifyInner(v)).join(",") + "]";
66
+ }
67
+ if (typeof value === "object") {
68
+ const obj = value;
69
+ const keys = Object.keys(obj).sort();
70
+ return "{" + keys.map((k) => encodeString(k) + ":" + stringifyInner(obj[k])).join(",") + "}";
71
+ }
72
+ throw new TypeError(`canonicalStringify: unsupported type ${typeof value}`);
73
+ }
74
+ function encodeString(s) {
75
+ return JSON.stringify(s);
76
+ }
77
+ function canonicalArgsSha256(toolArgs) {
78
+ const canonical = canonicalStringify(toolArgs ?? {});
79
+ const bytes = new TextEncoder().encode(canonical);
80
+ const digest = sha256(bytes);
81
+ return bytesToHex(digest);
82
+ }
83
+ function bytesToHex(bytes) {
84
+ let out = "";
85
+ for (let i = 0; i < bytes.length; i++) {
86
+ out += bytes[i].toString(16).padStart(2, "0");
87
+ }
88
+ return out;
89
+ }
90
+ function base64urlEncode(bytes) {
91
+ const raw = typeof bytes === "string" ? new TextEncoder().encode(bytes) : bytes;
92
+ if (typeof Buffer !== "undefined") {
93
+ return Buffer.from(raw).toString("base64url");
94
+ }
95
+ let binary = "";
96
+ for (let i = 0; i < raw.length; i++) binary += String.fromCharCode(raw[i]);
97
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
98
+ }
99
+ function loadPrivateKey(key) {
100
+ if (key instanceof Uint8Array) {
101
+ if (key.length !== 32) {
102
+ throw new TypeError("ES256 raw private key must be 32 bytes.");
103
+ }
104
+ return key;
105
+ }
106
+ if (typeof key === "string" && key.includes("-----BEGIN")) {
107
+ const nodeCrypto = __require("crypto");
108
+ const keyObj = nodeCrypto.createPrivateKey({ key, format: "pem" });
109
+ const jwk = keyObj.export({ format: "jwk" });
110
+ if (jwk.crv !== "P-256" || !jwk.d) {
111
+ throw new TypeError("PEM is not a P-256 private key.");
112
+ }
113
+ return base64urlDecodeToBytes(jwk.d);
114
+ }
115
+ throw new TypeError("loadPrivateKey: expected Uint8Array(32) or PEM string.");
116
+ }
117
+ function base64urlDecodeToBytes(s) {
118
+ const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
119
+ const b64 = (s + pad).replace(/-/g, "+").replace(/_/g, "/");
120
+ if (typeof Buffer !== "undefined") {
121
+ return new Uint8Array(Buffer.from(b64, "base64"));
122
+ }
123
+ const binary = atob(b64);
124
+ const out = new Uint8Array(binary.length);
125
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
126
+ return out;
127
+ }
128
+ function signInvocation(toolName, toolArgs, options) {
129
+ const { kid, privateKey, handle } = options;
130
+ const nonce = options.nonce ?? base64urlEncode(randomBytes(24));
131
+ const iat = options.iatSeconds ?? Math.floor(Date.now() / 1e3);
132
+ const claims = {
133
+ tool: toolName,
134
+ args_sha256: canonicalArgsSha256(toolArgs ?? {}),
135
+ nonce,
136
+ iat,
137
+ iss: handle
138
+ };
139
+ const headerB64 = base64urlEncode(JSON.stringify({ alg: "ES256", kid }));
140
+ const payloadB64 = base64urlEncode(JSON.stringify(claims));
141
+ const signingInput = `${headerB64}.${payloadB64}`;
142
+ const signingBytes = new TextEncoder().encode(signingInput);
143
+ const dBytes = loadPrivateKey(privateKey);
144
+ const digest = sha256(signingBytes);
145
+ const sig = p256.sign(digest, dBytes, { prehash: false });
146
+ const sigBytes = sig.toCompactRawBytes();
147
+ const sigB64 = base64urlEncode(sigBytes);
148
+ return `${signingInput}.${sigB64}`;
149
+ }
150
+ var init_signing = __esm({
151
+ "src/signing.ts"() {
152
+ }
153
+ });
4
154
 
5
155
  // src/errors.ts
6
156
  var AlterError = class extends Error {
@@ -101,7 +251,12 @@ async function discover(domain, opts = {}) {
101
251
  try {
102
252
  const dnsHit = await tryDns(host);
103
253
  if (dnsHit) {
104
- const result = { url: dnsHit, transport: "streamable-http", source: "dns" };
254
+ const parsed = validateDiscoveredUrl(dnsHit, "dns");
255
+ const result = {
256
+ url: parsed.toString().replace(/\/$/, ""),
257
+ transport: "streamable-http",
258
+ source: "dns"
259
+ };
105
260
  if (cache) _cache.set(host, result);
106
261
  return result;
107
262
  }
@@ -142,6 +297,28 @@ function normaliseDomain(input) {
142
297
  if (!host) throw new AlterDiscoveryError(`Empty domain: "${input}"`);
143
298
  return host;
144
299
  }
300
+ function validateDiscoveredUrl(url, source) {
301
+ let parsed;
302
+ try {
303
+ parsed = new URL(url);
304
+ } catch {
305
+ throw new AlterDiscoveryError(`${source}: malformed URL ${url}`);
306
+ }
307
+ if (parsed.protocol !== "https:") {
308
+ throw new AlterDiscoveryError(
309
+ `${source}: non-https MCP endpoint rejected (got ${parsed.protocol}//${parsed.hostname})`
310
+ );
311
+ }
312
+ if (parsed.username || parsed.password) {
313
+ throw new AlterDiscoveryError(
314
+ `${source}: MCP endpoint must not contain userinfo (user:pass@host)`
315
+ );
316
+ }
317
+ if (!parsed.hostname) {
318
+ throw new AlterDiscoveryError(`${source}: MCP endpoint missing hostname`);
319
+ }
320
+ return parsed;
321
+ }
145
322
  async function tryDns(host) {
146
323
  let resolveTxt;
147
324
  try {
@@ -202,14 +379,17 @@ async function tryWellKnown(host, file, timeoutMs, fetchImpl) {
202
379
  if (file === "mcp.json") {
203
380
  const remotes = doc.remotes || [];
204
381
  const remote = remotes.find((r) => r.transportType === "streamable-http" || r.transportType === "http");
205
- const url2 = remote?.url || doc.url;
206
- if (!url2) return null;
207
- return { url: url2, transport: "streamable-http", source: "mcp.json", raw: doc };
382
+ const rawUrl = remote?.url || doc.url;
383
+ if (!rawUrl) return null;
384
+ const parsed = validateDiscoveredUrl(rawUrl, "mcp.json");
385
+ return { url: parsed.toString().replace(/\/$/, ""), transport: "streamable-http", source: "mcp.json", raw: doc };
208
386
  }
209
387
  const mcpHost = doc.mcp;
210
388
  if (!mcpHost) return null;
389
+ const normalised = ensureMcpPath(mcpHost);
390
+ validateDiscoveredUrl(normalised, "alter.json");
211
391
  return {
212
- url: ensureMcpPath(mcpHost),
392
+ url: normalised,
213
393
  transport: "streamable-http",
214
394
  source: "alter.json",
215
395
  publicKey: doc.pk,
@@ -255,11 +435,14 @@ var X402Client = class {
255
435
  if (!this.assets.has(envelope.asset)) {
256
436
  throw new AlterError("PAYMENT_REQUIRED", `asset ${envelope.asset} not permitted by client policy`);
257
437
  }
258
- if (this.maxPerQuery !== void 0 && Number(envelope.amount) > this.maxPerQuery) {
259
- throw new AlterError(
260
- "PAYMENT_REQUIRED",
261
- `quote ${envelope.amount} ${envelope.asset} exceeds maxPerQuery ${this.maxPerQuery}`
262
- );
438
+ if (this.maxPerQuery !== void 0) {
439
+ const amt = Number(envelope.amount);
440
+ if (!Number.isFinite(amt) || amt < 0 || amt > this.maxPerQuery) {
441
+ throw new AlterError(
442
+ "PAYMENT_REQUIRED",
443
+ `quote ${envelope.amount} ${envelope.asset} exceeds maxPerQuery ${this.maxPerQuery}`
444
+ );
445
+ }
263
446
  }
264
447
  if (!this.signer) {
265
448
  throw new AlterPaymentRequired(envelope.resource ?? "unknown", envelope);
@@ -317,6 +500,7 @@ var MCPClient = class {
317
500
  maxRetries;
318
501
  clientInfo;
319
502
  x402;
503
+ signing;
320
504
  requestCounter = 0;
321
505
  initialised = false;
322
506
  constructor(opts = {}) {
@@ -327,6 +511,7 @@ var MCPClient = class {
327
511
  this.maxRetries = opts.maxRetries ?? 2;
328
512
  this.clientInfo = opts.clientInfo ?? { name: "@truealter/sdk", version: "0.2.0" };
329
513
  this.x402 = opts.x402;
514
+ this.signing = opts.signing;
330
515
  }
331
516
  /**
332
517
  * Send the MCP `initialize` handshake and capture the resulting session
@@ -413,6 +598,7 @@ var MCPClient = class {
413
598
  method
414
599
  };
415
600
  if (params !== void 0) payload.params = params;
601
+ const signatureHeader = this.buildSignatureHeader(method, params);
416
602
  let attempt = 0;
417
603
  let lastErr = null;
418
604
  while (attempt <= this.maxRetries) {
@@ -423,7 +609,7 @@ var MCPClient = class {
423
609
  try {
424
610
  resp = await this.fetchImpl(this.endpoint, {
425
611
  method: "POST",
426
- headers: this.buildHeaders(),
612
+ headers: this.buildHeaders(signatureHeader),
427
613
  body: JSON.stringify(payload),
428
614
  signal: controller.signal
429
615
  });
@@ -450,7 +636,8 @@ var MCPClient = class {
450
636
  throw new AlterPaymentRequired(this.guessToolName(payload), envelope);
451
637
  }
452
638
  if (resp.status === 429) {
453
- const retryAfter = Number(resp.headers.get("Retry-After") ?? 60);
639
+ const rawRetryAfter = Number(resp.headers.get("Retry-After") ?? 60);
640
+ const retryAfter = Number.isFinite(rawRetryAfter) && rawRetryAfter >= 0 ? Math.min(rawRetryAfter, 300) : 60;
454
641
  if (attempt > this.maxRetries) {
455
642
  throw new AlterRateLimited(`HTTP 429 on ${method}`, retryAfter);
456
643
  }
@@ -486,7 +673,7 @@ var MCPClient = class {
486
673
  }
487
674
  throw lastErr ?? new AlterNetworkError(`MCP ${method}: exhausted retries`);
488
675
  }
489
- buildHeaders() {
676
+ buildHeaders(extra) {
490
677
  const headers = {
491
678
  "Content-Type": "application/json",
492
679
  Accept: "application/json",
@@ -494,8 +681,27 @@ var MCPClient = class {
494
681
  };
495
682
  if (this.apiKey) headers["X-ALTER-API-Key"] = this.apiKey;
496
683
  if (this.sessionId) headers["Mcp-Session-Id"] = this.sessionId;
684
+ if (extra) Object.assign(headers, extra);
497
685
  return headers;
498
686
  }
687
+ /**
688
+ * Produce the `Mcp-Invocation-Signature` header for a `tools/call`
689
+ * payload, when signing is configured. Returns `undefined` when no
690
+ * signing key is attached or the method is not `tools/call`.
691
+ */
692
+ buildSignatureHeader(method, params) {
693
+ if (!this.signing) return void 0;
694
+ if (method !== "tools/call") return void 0;
695
+ const p = params;
696
+ if (!p?.name) return void 0;
697
+ const { signInvocation: signInvocation2 } = (init_signing(), __toCommonJS(signing_exports));
698
+ const headerValue = signInvocation2(p.name, p.arguments ?? {}, {
699
+ kid: this.signing.kid,
700
+ privateKey: this.signing.privateKey,
701
+ handle: this.signing.handle
702
+ });
703
+ return { "Mcp-Invocation-Signature": headerValue };
704
+ }
499
705
  async extractPaymentEnvelope(resp) {
500
706
  const headerValue = resp.headers.get("X-402-Payment") ?? resp.headers.get("x-402-payment");
501
707
  if (headerValue) {
@@ -538,8 +744,8 @@ function generateKeypair() {
538
744
  const privateKey = randomBytes(32);
539
745
  const publicKey = ed25519.getPublicKey(privateKey);
540
746
  return {
541
- privateKey: bytesToHex(privateKey),
542
- publicKey: bytesToHex(publicKey),
747
+ privateKey: bytesToHex$1(privateKey),
748
+ publicKey: bytesToHex$1(publicKey),
543
749
  did: encodeDid(publicKey)
544
750
  };
545
751
  }
@@ -551,7 +757,7 @@ function keypairFromPrivateKey(privateKeyHex) {
551
757
  const publicKey = ed25519.getPublicKey(privateKey);
552
758
  return {
553
759
  privateKey: privateKeyHex,
554
- publicKey: bytesToHex(publicKey),
760
+ publicKey: bytesToHex$1(publicKey),
555
761
  did: encodeDid(publicKey)
556
762
  };
557
763
  }
@@ -559,7 +765,7 @@ async function sign(privateKeyHex, message) {
559
765
  const msgBytes = typeof message === "string" ? new TextEncoder().encode(message) : message;
560
766
  const privateKey = hexToBytes(privateKeyHex);
561
767
  const sig = await ed25519.signAsync(msgBytes, privateKey);
562
- return bytesToHex(sig);
768
+ return bytesToHex$1(sig);
563
769
  }
564
770
  async function verify(publicKeyHex, signatureHex, message) {
565
771
  try {
@@ -571,14 +777,14 @@ async function verify(publicKeyHex, signatureHex, message) {
571
777
  }
572
778
  function encodeDid(publicKey) {
573
779
  const bytes = typeof publicKey === "string" ? hexToBytes(publicKey) : publicKey;
574
- return `ed25519:${base64urlEncode(bytes)}`;
780
+ return `ed25519:${base64urlEncode2(bytes)}`;
575
781
  }
576
782
  function decodeDid(did) {
577
783
  const ed25519Match = did.match(/^ed25519:(.+)$/);
578
784
  if (ed25519Match) return base64urlDecode(ed25519Match[1]);
579
785
  throw new Error(`Unrecognised DID encoding: ${did}`);
580
786
  }
581
- function base64urlEncode(bytes) {
787
+ function base64urlEncode2(bytes) {
582
788
  let b64;
583
789
  if (typeof Buffer !== "undefined") {
584
790
  b64 = Buffer.from(bytes).toString("base64");
@@ -1047,6 +1253,9 @@ var AlterClient = class {
1047
1253
  }
1048
1254
  };
1049
1255
 
1256
+ // src/index.ts
1257
+ init_signing();
1258
+
1050
1259
  // src/adapters/generic-mcp.ts
1051
1260
  function generateGenericMcpConfig(opts = {}) {
1052
1261
  const serverName = opts.serverName ?? "alter";
@@ -1071,6 +1280,461 @@ function generateCursorConfig(opts = {}) {
1071
1280
  return generateGenericMcpConfig({ serverName: "alter", ...opts });
1072
1281
  }
1073
1282
 
1283
+ // src/adapters/claude-desktop.ts
1284
+ function generateClaudeDesktopConfig(opts = {}) {
1285
+ const serverName = opts.serverName ?? "alter";
1286
+ const bridgeCommand = opts.bridgeCommand ?? "alter-mcp-bridge";
1287
+ const env2 = {};
1288
+ env2.ALTER_MCP_ENDPOINT = opts.endpoint ?? DEFAULT_ENDPOINT;
1289
+ if (opts.apiKey) env2.ALTER_API_KEY = opts.apiKey;
1290
+ const entry = {
1291
+ command: bridgeCommand,
1292
+ env: env2,
1293
+ description: "ALTER Identity \u2014 psychometric identity field for AI agents"
1294
+ };
1295
+ if (opts.extraArgs && opts.extraArgs.length > 0) {
1296
+ entry.args = [...opts.extraArgs];
1297
+ }
1298
+ return { mcpServers: { [serverName]: entry } };
1299
+ }
1300
+
1301
+ // src/meta.ts
1302
+ var SDK_NAME = "@truealter/sdk";
1303
+ var SDK_VERSION = "0.3.0";
1304
+ var HOME = homedir();
1305
+ var PLAT = platform();
1306
+ function appData() {
1307
+ return env.APPDATA ?? join(HOME, "AppData", "Roaming");
1308
+ }
1309
+ function xdgConfig() {
1310
+ return env.XDG_CONFIG_HOME ?? join(HOME, ".config");
1311
+ }
1312
+ function macAppSupport() {
1313
+ return join(HOME, "Library", "Application Support");
1314
+ }
1315
+ function claudeDesktopConfigPath() {
1316
+ if (PLAT === "darwin") return join(macAppSupport(), "Claude", "claude_desktop_config.json");
1317
+ if (PLAT === "win32") return join(appData(), "Claude", "claude_desktop_config.json");
1318
+ return join(xdgConfig(), "Claude", "claude_desktop_config.json");
1319
+ }
1320
+ function claudeDesktopDir() {
1321
+ if (PLAT === "darwin") return join(macAppSupport(), "Claude");
1322
+ if (PLAT === "win32") return join(appData(), "Claude");
1323
+ return join(xdgConfig(), "Claude");
1324
+ }
1325
+ function vscodeConfigPath() {
1326
+ if (PLAT === "darwin") return join(macAppSupport(), "Code", "User", "mcp.json");
1327
+ if (PLAT === "win32") return join(appData(), "Code", "User", "mcp.json");
1328
+ return join(xdgConfig(), "Code", "User", "mcp.json");
1329
+ }
1330
+ function vscodeDir() {
1331
+ if (PLAT === "darwin") return join(macAppSupport(), "Code", "User");
1332
+ if (PLAT === "win32") return join(appData(), "Code", "User");
1333
+ return join(xdgConfig(), "Code", "User");
1334
+ }
1335
+ var cursorDir = join(HOME, ".cursor");
1336
+ var cursorConfigPath = join(cursorDir, "mcp.json");
1337
+ var claudeCodeProbeDir = join(HOME, ".claude");
1338
+ var CLAUDE_CODE = {
1339
+ id: "claude-code",
1340
+ label: "Claude Code",
1341
+ configPath: null,
1342
+ probeDir: claudeCodeProbeDir,
1343
+ rootKey: "mcpServers"
1344
+ };
1345
+ var CURSOR = {
1346
+ id: "cursor",
1347
+ label: "Cursor",
1348
+ configPath: cursorConfigPath,
1349
+ probeDir: cursorDir,
1350
+ rootKey: "mcpServers"
1351
+ };
1352
+ var CLAUDE_DESKTOP = {
1353
+ id: "claude-desktop",
1354
+ label: "Claude Desktop",
1355
+ configPath: claudeDesktopConfigPath(),
1356
+ probeDir: claudeDesktopDir(),
1357
+ rootKey: "mcpServers"
1358
+ };
1359
+ var VSCODE = {
1360
+ id: "vscode",
1361
+ label: "VS Code",
1362
+ configPath: vscodeConfigPath(),
1363
+ probeDir: vscodeDir(),
1364
+ // VS Code's user-scoped mcp.json uses `servers`, not `mcpServers`.
1365
+ rootKey: "servers"
1366
+ };
1367
+ var ALL_CLIENTS = [CLAUDE_CODE, CURSOR, CLAUDE_DESKTOP, VSCODE];
1368
+ function alterConfigDir() {
1369
+ return join(xdgConfig(), "alter");
1370
+ }
1371
+ function wireStatePath() {
1372
+ return join(alterConfigDir(), "wire-state.json");
1373
+ }
1374
+ function probeClaudeCode() {
1375
+ try {
1376
+ const result = spawnSync("claude", ["--version"], {
1377
+ encoding: "utf8",
1378
+ shell: process.platform === "win32",
1379
+ timeout: 5e3
1380
+ });
1381
+ if (result.error) {
1382
+ return {
1383
+ client: ALL_CLIENTS.find((c) => c.id === "claude-code"),
1384
+ installed: false,
1385
+ reason: `claude binary not on PATH (${result.error.message})`
1386
+ };
1387
+ }
1388
+ if (result.status === 0) {
1389
+ return {
1390
+ client: ALL_CLIENTS.find((c) => c.id === "claude-code"),
1391
+ installed: true,
1392
+ version: result.stdout.trim() || void 0,
1393
+ reason: "claude --version returned 0"
1394
+ };
1395
+ }
1396
+ return {
1397
+ client: ALL_CLIENTS.find((c) => c.id === "claude-code"),
1398
+ installed: false,
1399
+ reason: `claude --version exited ${String(result.status)}`
1400
+ };
1401
+ } catch (err) {
1402
+ return {
1403
+ client: ALL_CLIENTS.find((c) => c.id === "claude-code"),
1404
+ installed: false,
1405
+ reason: err.message
1406
+ };
1407
+ }
1408
+ }
1409
+ function probeByDir(id) {
1410
+ const client = ALL_CLIENTS.find((c) => c.id === id);
1411
+ if (!client) throw new Error(`unknown client id: ${id}`);
1412
+ const installed = existsSync(client.probeDir);
1413
+ return {
1414
+ client,
1415
+ installed,
1416
+ reason: installed ? `found ${client.probeDir}` : `no directory at ${client.probeDir}`
1417
+ };
1418
+ }
1419
+ function probeAll() {
1420
+ return [
1421
+ probeClaudeCode(),
1422
+ probeByDir("cursor"),
1423
+ probeByDir("claude-desktop"),
1424
+ probeByDir("vscode")
1425
+ ];
1426
+ }
1427
+ var SYNC_PREFIXES = [
1428
+ // iCloud Drive — both the new and legacy mounts.
1429
+ "Library/Mobile Documents/com~apple~CloudDocs",
1430
+ "iCloud Drive",
1431
+ // OneDrive variants Microsoft ships across editions.
1432
+ "OneDrive",
1433
+ "OneDrive - ",
1434
+ // Dropbox standard + enterprise mounts.
1435
+ "Dropbox",
1436
+ "Dropbox (",
1437
+ // Google Drive (ALTER does not integrate with Google; still refuse).
1438
+ "Google Drive",
1439
+ "GoogleDrive",
1440
+ "CloudStorage/GoogleDrive",
1441
+ // Box, pCloud, Sync.com, MEGA — high-signal names worth refusing.
1442
+ "Box Sync",
1443
+ "pCloud Drive",
1444
+ "Sync.com",
1445
+ "MEGAsync"
1446
+ ];
1447
+ function detectSyncedVolume(path) {
1448
+ const absolute = resolve(path);
1449
+ const normalised = platform() === "win32" ? absolute.replace(/\\/g, "/") : absolute;
1450
+ for (const prefix of SYNC_PREFIXES) {
1451
+ if (normalised.includes(`/${prefix}/`) || normalised.includes(`/${prefix}`)) {
1452
+ return { refused: true, matchedPrefix: prefix, resolvedPath: absolute };
1453
+ }
1454
+ }
1455
+ return null;
1456
+ }
1457
+ var WIRE_STATE_VERSION = 1;
1458
+ function readWireState() {
1459
+ const path = wireStatePath();
1460
+ if (!existsSync(path)) return null;
1461
+ try {
1462
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
1463
+ if (parsed.version !== WIRE_STATE_VERSION) {
1464
+ throw new Error(
1465
+ `wire-state.json version ${String(parsed.version)} is not supported by this SDK (expected ${WIRE_STATE_VERSION})`
1466
+ );
1467
+ }
1468
+ return parsed;
1469
+ } catch (err) {
1470
+ throw new Error(`failed to parse wire-state.json: ${err.message}`);
1471
+ }
1472
+ }
1473
+ function writeWireState(state) {
1474
+ const path = wireStatePath();
1475
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
1476
+ writeFileSync(path, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
1477
+ }
1478
+ function sha2562(bytes) {
1479
+ return createHash("sha256").update(bytes).digest("hex");
1480
+ }
1481
+ function atomicJsonMerge(opts) {
1482
+ const { path, timestamp, merge, idempotent = true } = opts;
1483
+ const tmpPath = `${path}.alter-tmp-${timestamp}`;
1484
+ const backupPath = `${path}.alter-backup-${timestamp}`;
1485
+ let existed = false;
1486
+ let preBytes = null;
1487
+ let parsed = {};
1488
+ if (existsSync(path)) {
1489
+ existed = true;
1490
+ preBytes = readFileSync(path, "utf8");
1491
+ if (preBytes.trim().length > 0) {
1492
+ try {
1493
+ parsed = JSON.parse(preBytes);
1494
+ } catch (err) {
1495
+ throw new Error(
1496
+ `refusing to wire ${path}: existing file is not valid JSON (${err.message}). Hand-fix the file, then re-run \`alter-identity wire\`.`
1497
+ );
1498
+ }
1499
+ if (typeof parsed !== "object" || Array.isArray(parsed) || parsed === null) {
1500
+ throw new Error(`refusing to wire ${path}: existing JSON root is not an object`);
1501
+ }
1502
+ }
1503
+ }
1504
+ const merged = merge(parsed);
1505
+ const serialised = JSON.stringify(merged, null, 2) + "\n";
1506
+ if (idempotent && preBytes !== null && preBytes === serialised) {
1507
+ return {
1508
+ path,
1509
+ backupPath: null,
1510
+ preSha256: sha2562(preBytes),
1511
+ postSha256: sha2562(preBytes),
1512
+ noop: true
1513
+ };
1514
+ }
1515
+ mkdirSync(dirname(path), { recursive: true });
1516
+ writeFileSync(tmpPath, serialised, { mode: 384 });
1517
+ try {
1518
+ if (existed) copyFileSync(path, backupPath);
1519
+ renameSync(tmpPath, path);
1520
+ } catch (err) {
1521
+ try {
1522
+ unlinkSync(tmpPath);
1523
+ } catch {
1524
+ }
1525
+ throw err;
1526
+ }
1527
+ return {
1528
+ path,
1529
+ backupPath: existed ? backupPath : null,
1530
+ preSha256: preBytes === null ? null : sha2562(preBytes),
1531
+ postSha256: sha2562(serialised),
1532
+ noop: false
1533
+ };
1534
+ }
1535
+ function restoreFromBackup(path, backupPath) {
1536
+ if (backupPath === null) {
1537
+ if (existsSync(path)) unlinkSync(path);
1538
+ return;
1539
+ }
1540
+ if (!existsSync(backupPath)) {
1541
+ throw new Error(`cannot restore ${path}: backup missing at ${backupPath}`);
1542
+ }
1543
+ renameSync(backupPath, path);
1544
+ }
1545
+
1546
+ // src/wire/index.ts
1547
+ var TIMESTAMP = () => String(Math.floor(Date.now() / 1e3));
1548
+ var ISO_NOW = () => (/* @__PURE__ */ new Date()).toISOString();
1549
+ function clientById(id) {
1550
+ const hit = ALL_CLIENTS.find((c) => c.id === id);
1551
+ if (!hit) throw new Error(`unknown client id: ${id}`);
1552
+ return hit;
1553
+ }
1554
+ function wire(opts = {}) {
1555
+ const endpoint = opts.endpoint ?? DEFAULT_ENDPOINT;
1556
+ const apiKey = opts.apiKey;
1557
+ const probes = probeAll();
1558
+ const selection = opts.only ?? probes.filter((p) => p.installed).map((p) => p.client.id);
1559
+ const ts = TIMESTAMP();
1560
+ const targets = [];
1561
+ for (const id of selection) {
1562
+ const probe = id === "claude-code" ? probeClaudeCode() : probeByDir(id);
1563
+ if (!probe.installed && opts.skipMissing !== false) {
1564
+ targets.push({
1565
+ client: id,
1566
+ method: id === "claude-code" ? "cli" : "file",
1567
+ status: "skipped",
1568
+ ...id === "claude-code" ? { command: "" } : { path: clientById(id).configPath ?? "", backupPath: null, rootKey: clientById(id).rootKey, serverName: "alter", preSha256: null, postSha256: "" },
1569
+ reason: probe.reason
1570
+ });
1571
+ continue;
1572
+ }
1573
+ try {
1574
+ if (id === "claude-code") {
1575
+ targets.push(wireClaudeCode({ endpoint, apiKey }));
1576
+ } else {
1577
+ targets.push(wireFileTarget({ id, endpoint, apiKey, timestamp: ts }));
1578
+ }
1579
+ } catch (err) {
1580
+ const message = err.message;
1581
+ targets.push({
1582
+ client: id,
1583
+ method: id === "claude-code" ? "cli" : "file",
1584
+ status: "failed",
1585
+ ...id === "claude-code" ? { command: "" } : { path: clientById(id).configPath ?? "", backupPath: null, rootKey: clientById(id).rootKey, serverName: "alter", preSha256: null, postSha256: "" },
1586
+ reason: message
1587
+ });
1588
+ }
1589
+ }
1590
+ const state = {
1591
+ version: 1,
1592
+ sdkVersion: SDK_VERSION,
1593
+ writtenAt: ISO_NOW(),
1594
+ endpoint,
1595
+ targets
1596
+ };
1597
+ writeWireState(state);
1598
+ return { state, probes };
1599
+ }
1600
+ function wireFileTarget(args) {
1601
+ const client = clientById(args.id);
1602
+ if (!client.configPath) {
1603
+ throw new Error(`client ${client.id} has no file-based config path`);
1604
+ }
1605
+ const sync = detectSyncedVolume(client.configPath);
1606
+ if (sync) {
1607
+ throw new Error(
1608
+ `refusing to wire ${client.label}: config path ${sync.resolvedPath} lives under ${sync.matchedPrefix}. Synced volumes propagate credentials across devices \u2014 move the config off the sync root, or run wire on the device you want to target.`
1609
+ );
1610
+ }
1611
+ const entry = args.id === "claude-desktop" ? generateClaudeDesktopConfig({ endpoint: args.endpoint, apiKey: args.apiKey }) : generateGenericMcpConfig({ endpoint: args.endpoint, apiKey: args.apiKey });
1612
+ const rootKey = client.rootKey;
1613
+ const serverName = "alter";
1614
+ const result = atomicJsonMerge({
1615
+ path: client.configPath,
1616
+ timestamp: args.timestamp,
1617
+ merge: (existing) => {
1618
+ const bucket = existing[rootKey] ?? {};
1619
+ const source = entry.mcpServers.alter;
1620
+ return {
1621
+ ...existing,
1622
+ [rootKey]: {
1623
+ ...bucket,
1624
+ [serverName]: source
1625
+ }
1626
+ };
1627
+ }
1628
+ });
1629
+ return {
1630
+ client: args.id,
1631
+ method: "file",
1632
+ status: result.noop ? "already-wired" : "written",
1633
+ path: result.path,
1634
+ backupPath: result.backupPath,
1635
+ rootKey,
1636
+ serverName,
1637
+ preSha256: result.preSha256,
1638
+ postSha256: result.postSha256
1639
+ };
1640
+ }
1641
+ function wireClaudeCode(args) {
1642
+ const cmd = "claude";
1643
+ const argList = [
1644
+ "mcp",
1645
+ "add",
1646
+ "--scope",
1647
+ "user",
1648
+ "--transport",
1649
+ "http",
1650
+ "alter",
1651
+ args.endpoint
1652
+ ];
1653
+ if (args.apiKey) {
1654
+ argList.push("--header", `X-ALTER-API-Key:${args.apiKey}`);
1655
+ }
1656
+ const full = `${cmd} ${argList.join(" ")}`;
1657
+ const run = spawnSync(cmd, argList, {
1658
+ encoding: "utf8",
1659
+ shell: process.platform === "win32",
1660
+ timeout: 1e4
1661
+ });
1662
+ if (run.error) {
1663
+ return {
1664
+ client: "claude-code",
1665
+ method: "cli",
1666
+ status: "failed",
1667
+ command: full,
1668
+ stdout: run.stdout,
1669
+ stderr: run.stderr,
1670
+ reason: run.error.message
1671
+ };
1672
+ }
1673
+ const stderr = (run.stderr ?? "").toLowerCase();
1674
+ const alreadyExists = stderr.includes("already exists") || stderr.includes("already configured");
1675
+ if (run.status === 0) {
1676
+ return { client: "claude-code", method: "cli", status: "written", command: full, stdout: run.stdout, stderr: run.stderr };
1677
+ }
1678
+ if (alreadyExists) {
1679
+ return { client: "claude-code", method: "cli", status: "already-wired", command: full, stdout: run.stdout, stderr: run.stderr };
1680
+ }
1681
+ return {
1682
+ client: "claude-code",
1683
+ method: "cli",
1684
+ status: "failed",
1685
+ command: full,
1686
+ stdout: run.stdout,
1687
+ stderr: run.stderr,
1688
+ reason: `claude mcp add exited ${String(run.status)}`
1689
+ };
1690
+ }
1691
+ function unwire() {
1692
+ const state = readWireState();
1693
+ const undone = [];
1694
+ if (!state || state.targets.length === 0) {
1695
+ return { state, undone };
1696
+ }
1697
+ for (const target of state.targets) {
1698
+ try {
1699
+ if (target.method === "file") {
1700
+ if (target.status === "written") {
1701
+ restoreFromBackup(target.path, target.backupPath);
1702
+ undone.push({ client: target.client, action: target.backupPath ? "restored" : "removed" });
1703
+ } else {
1704
+ undone.push({ client: target.client, action: "skipped", reason: `target status was ${target.status}` });
1705
+ }
1706
+ } else if (target.method === "cli") {
1707
+ if (target.status === "written") {
1708
+ const run = spawnSync("claude", ["mcp", "remove", "--scope", "user", "alter"], {
1709
+ encoding: "utf8",
1710
+ shell: process.platform === "win32",
1711
+ timeout: 1e4
1712
+ });
1713
+ if (run.error) {
1714
+ undone.push({ client: target.client, action: "failed", reason: run.error.message });
1715
+ } else if (run.status === 0) {
1716
+ undone.push({ client: target.client, action: "cli-removed" });
1717
+ } else {
1718
+ undone.push({ client: target.client, action: "failed", reason: `claude mcp remove exited ${String(run.status)}` });
1719
+ }
1720
+ } else {
1721
+ undone.push({ client: target.client, action: "skipped", reason: `target status was ${target.status}` });
1722
+ }
1723
+ }
1724
+ } catch (err) {
1725
+ undone.push({ client: target.client, action: "failed", reason: err.message });
1726
+ }
1727
+ }
1728
+ writeWireState({
1729
+ version: 1,
1730
+ sdkVersion: state.sdkVersion,
1731
+ writtenAt: ISO_NOW(),
1732
+ endpoint: state.endpoint,
1733
+ targets: []
1734
+ });
1735
+ return { state, undone };
1736
+ }
1737
+
1074
1738
  // src/types.ts
1075
1739
  var FREE_TOOL_NAMES = [
1076
1740
  "hello_agent",
@@ -1226,8 +1890,4 @@ var TOOL_BLAST_RADIUS = {
1226
1890
  query_graph_similarity: "high"
1227
1891
  };
1228
1892
 
1229
- // src/index.ts
1230
- var SDK_NAME = "@truealter/sdk";
1231
- var SDK_VERSION = "0.2.4";
1232
-
1233
- export { AlterAuthError, AlterClient, AlterDiscoveryError, AlterError, AlterInvalidResponse, AlterNetworkError, AlterPaymentRequired, AlterProvenanceError, AlterRateLimited, AlterTimeoutError, AlterToolError, DEFAULT_DOMAIN, DEFAULT_ENDPOINT, DEFAULT_VERIFY_AT_ALLOWLIST, FREE_TOOL_NAMES, MCPClient, MCP_PROTOCOL_VERSION, PREMIUM_TOOL_NAMES, SDK_NAME, SDK_VERSION, TOOL_BLAST_RADIUS, TOOL_COSTS, TOOL_TIERS, X402Client, base64urlDecode, base64urlEncode, clearDiscoveryCache, decodeDid, discover, encodeDid, fetchPublicKeys, generateClaudeConfig, generateCursorConfig, generateGenericMcpConfig, generateKeypair, keypairFromPrivateKey, parsePaymentHeader, resolveVerifyAt, sign, verify, verifyProvenance, verifyToolSignatures };
1893
+ export { ALL_CLIENTS, AlterAuthError, AlterClient, AlterDiscoveryError, AlterError, AlterInvalidResponse, AlterNetworkError, AlterPaymentRequired, AlterProvenanceError, AlterRateLimited, AlterTimeoutError, AlterToolError, CLAUDE_CODE, CLAUDE_DESKTOP, CURSOR, DEFAULT_DOMAIN, DEFAULT_ENDPOINT, DEFAULT_VERIFY_AT_ALLOWLIST, FREE_TOOL_NAMES, MCPClient, MCP_PROTOCOL_VERSION, PREMIUM_TOOL_NAMES, SDK_NAME, SDK_VERSION, TOOL_BLAST_RADIUS, TOOL_COSTS, TOOL_TIERS, VSCODE, X402Client, base64urlDecode, base64urlEncode2 as base64urlEncode, canonicalArgsSha256, canonicalStringify, clearDiscoveryCache, decodeDid, detectSyncedVolume, discover, encodeDid, fetchPublicKeys, generateClaudeConfig, generateClaudeDesktopConfig, generateCursorConfig, generateGenericMcpConfig, generateKeypair, keypairFromPrivateKey, loadPrivateKey, parsePaymentHeader, probeAll, probeByDir, probeClaudeCode, readWireState, resolveVerifyAt, sha2562 as sha256, sign, signInvocation, unwire, verify, verifyProvenance, verifyToolSignatures, wire, writeWireState };