@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.
@@ -1,11 +1,158 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
3
- import { homedir } from 'os';
4
- import { join, dirname } from 'path';
5
- import { env, stderr, exit, argv, stdout } from 'process';
2
+ import { p256 } from '@noble/curves/p256';
3
+ import { sha256 } from '@noble/hashes/sha256';
4
+ import { hexToBytes, bytesToHex as bytesToHex$1, randomBytes } from '@noble/hashes/utils';
5
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, renameSync, copyFileSync } from 'fs';
6
+ import { homedir, platform } from 'os';
7
+ import { join, dirname, resolve } from 'path';
8
+ import { env, stderr, exit, argv, stdout, stdin } from 'process';
9
+ import { createInterface } from 'readline';
6
10
  import * as ed25519 from '@noble/ed25519';
7
11
  import { sha512 } from '@noble/hashes/sha512';
8
- import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
12
+ import { spawnSync } from 'child_process';
13
+ import { createHash } from 'crypto';
14
+
15
+ var __defProp = Object.defineProperty;
16
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
17
+ var __getOwnPropNames = Object.getOwnPropertyNames;
18
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
19
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
20
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
21
+ }) : x)(function(x) {
22
+ if (typeof require !== "undefined") return require.apply(this, arguments);
23
+ throw Error('Dynamic require of "' + x + '" is not supported');
24
+ });
25
+ var __esm = (fn, res) => function __init() {
26
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
27
+ };
28
+ var __export = (target, all) => {
29
+ for (var name in all)
30
+ __defProp(target, name, { get: all[name], enumerable: true });
31
+ };
32
+ var __copyProps = (to, from, except, desc) => {
33
+ if (from && typeof from === "object" || typeof from === "function") {
34
+ for (let key of __getOwnPropNames(from))
35
+ if (!__hasOwnProp.call(to, key) && key !== except)
36
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
37
+ }
38
+ return to;
39
+ };
40
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
41
+
42
+ // src/signing.ts
43
+ var signing_exports = {};
44
+ __export(signing_exports, {
45
+ canonicalArgsSha256: () => canonicalArgsSha256,
46
+ canonicalStringify: () => canonicalStringify,
47
+ loadPrivateKey: () => loadPrivateKey,
48
+ signInvocation: () => signInvocation
49
+ });
50
+ function canonicalStringify(value) {
51
+ return stringifyInner(value);
52
+ }
53
+ function stringifyInner(value) {
54
+ if (value === null) return "null";
55
+ if (value === void 0) {
56
+ throw new TypeError("canonicalStringify: undefined is not representable in JSON");
57
+ }
58
+ if (typeof value === "boolean") return value ? "true" : "false";
59
+ if (typeof value === "number") {
60
+ if (!Number.isFinite(value)) {
61
+ throw new TypeError("canonicalStringify: non-finite numbers are not representable");
62
+ }
63
+ return JSON.stringify(value);
64
+ }
65
+ if (typeof value === "string") return encodeString(value);
66
+ if (Array.isArray(value)) {
67
+ return "[" + value.map((v) => stringifyInner(v)).join(",") + "]";
68
+ }
69
+ if (typeof value === "object") {
70
+ const obj = value;
71
+ const keys = Object.keys(obj).sort();
72
+ return "{" + keys.map((k) => encodeString(k) + ":" + stringifyInner(obj[k])).join(",") + "}";
73
+ }
74
+ throw new TypeError(`canonicalStringify: unsupported type ${typeof value}`);
75
+ }
76
+ function encodeString(s) {
77
+ return JSON.stringify(s);
78
+ }
79
+ function canonicalArgsSha256(toolArgs) {
80
+ const canonical = canonicalStringify(toolArgs ?? {});
81
+ const bytes = new TextEncoder().encode(canonical);
82
+ const digest = sha256(bytes);
83
+ return bytesToHex(digest);
84
+ }
85
+ function bytesToHex(bytes) {
86
+ let out = "";
87
+ for (let i = 0; i < bytes.length; i++) {
88
+ out += bytes[i].toString(16).padStart(2, "0");
89
+ }
90
+ return out;
91
+ }
92
+ function base64urlEncode(bytes) {
93
+ const raw = typeof bytes === "string" ? new TextEncoder().encode(bytes) : bytes;
94
+ if (typeof Buffer !== "undefined") {
95
+ return Buffer.from(raw).toString("base64url");
96
+ }
97
+ let binary = "";
98
+ for (let i = 0; i < raw.length; i++) binary += String.fromCharCode(raw[i]);
99
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
100
+ }
101
+ function loadPrivateKey(key) {
102
+ if (key instanceof Uint8Array) {
103
+ if (key.length !== 32) {
104
+ throw new TypeError("ES256 raw private key must be 32 bytes.");
105
+ }
106
+ return key;
107
+ }
108
+ if (typeof key === "string" && key.includes("-----BEGIN")) {
109
+ const nodeCrypto = __require("crypto");
110
+ const keyObj = nodeCrypto.createPrivateKey({ key, format: "pem" });
111
+ const jwk = keyObj.export({ format: "jwk" });
112
+ if (jwk.crv !== "P-256" || !jwk.d) {
113
+ throw new TypeError("PEM is not a P-256 private key.");
114
+ }
115
+ return base64urlDecodeToBytes(jwk.d);
116
+ }
117
+ throw new TypeError("loadPrivateKey: expected Uint8Array(32) or PEM string.");
118
+ }
119
+ function base64urlDecodeToBytes(s) {
120
+ const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
121
+ const b64 = (s + pad).replace(/-/g, "+").replace(/_/g, "/");
122
+ if (typeof Buffer !== "undefined") {
123
+ return new Uint8Array(Buffer.from(b64, "base64"));
124
+ }
125
+ const binary = atob(b64);
126
+ const out = new Uint8Array(binary.length);
127
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
128
+ return out;
129
+ }
130
+ function signInvocation(toolName, toolArgs, options) {
131
+ const { kid, privateKey, handle } = options;
132
+ const nonce = options.nonce ?? base64urlEncode(randomBytes(24));
133
+ const iat = options.iatSeconds ?? Math.floor(Date.now() / 1e3);
134
+ const claims = {
135
+ tool: toolName,
136
+ args_sha256: canonicalArgsSha256(toolArgs ?? {}),
137
+ nonce,
138
+ iat,
139
+ iss: handle
140
+ };
141
+ const headerB64 = base64urlEncode(JSON.stringify({ alg: "ES256", kid }));
142
+ const payloadB64 = base64urlEncode(JSON.stringify(claims));
143
+ const signingInput = `${headerB64}.${payloadB64}`;
144
+ const signingBytes = new TextEncoder().encode(signingInput);
145
+ const dBytes = loadPrivateKey(privateKey);
146
+ const digest = sha256(signingBytes);
147
+ const sig = p256.sign(digest, dBytes, { prehash: false });
148
+ const sigBytes = sig.toCompactRawBytes();
149
+ const sigB64 = base64urlEncode(sigBytes);
150
+ return `${signingInput}.${sigB64}`;
151
+ }
152
+ var init_signing = __esm({
153
+ "src/signing.ts"() {
154
+ }
155
+ });
9
156
 
10
157
  // src/errors.ts
11
158
  var AlterError = class extends Error {
@@ -106,7 +253,12 @@ async function discover(domain, opts = {}) {
106
253
  try {
107
254
  const dnsHit = await tryDns(host);
108
255
  if (dnsHit) {
109
- const result = { url: dnsHit, transport: "streamable-http", source: "dns" };
256
+ const parsed = validateDiscoveredUrl(dnsHit, "dns");
257
+ const result = {
258
+ url: parsed.toString().replace(/\/$/, ""),
259
+ transport: "streamable-http",
260
+ source: "dns"
261
+ };
110
262
  if (cache) _cache.set(host, result);
111
263
  return result;
112
264
  }
@@ -144,6 +296,28 @@ function normaliseDomain(input) {
144
296
  if (!host) throw new AlterDiscoveryError(`Empty domain: "${input}"`);
145
297
  return host;
146
298
  }
299
+ function validateDiscoveredUrl(url, source) {
300
+ let parsed;
301
+ try {
302
+ parsed = new URL(url);
303
+ } catch {
304
+ throw new AlterDiscoveryError(`${source}: malformed URL ${url}`);
305
+ }
306
+ if (parsed.protocol !== "https:") {
307
+ throw new AlterDiscoveryError(
308
+ `${source}: non-https MCP endpoint rejected (got ${parsed.protocol}//${parsed.hostname})`
309
+ );
310
+ }
311
+ if (parsed.username || parsed.password) {
312
+ throw new AlterDiscoveryError(
313
+ `${source}: MCP endpoint must not contain userinfo (user:pass@host)`
314
+ );
315
+ }
316
+ if (!parsed.hostname) {
317
+ throw new AlterDiscoveryError(`${source}: MCP endpoint missing hostname`);
318
+ }
319
+ return parsed;
320
+ }
147
321
  async function tryDns(host) {
148
322
  let resolveTxt;
149
323
  try {
@@ -204,14 +378,17 @@ async function tryWellKnown(host, file, timeoutMs, fetchImpl) {
204
378
  if (file === "mcp.json") {
205
379
  const remotes = doc.remotes || [];
206
380
  const remote = remotes.find((r) => r.transportType === "streamable-http" || r.transportType === "http");
207
- const url2 = remote?.url || doc.url;
208
- if (!url2) return null;
209
- return { url: url2, transport: "streamable-http", source: "mcp.json", raw: doc };
381
+ const rawUrl = remote?.url || doc.url;
382
+ if (!rawUrl) return null;
383
+ const parsed = validateDiscoveredUrl(rawUrl, "mcp.json");
384
+ return { url: parsed.toString().replace(/\/$/, ""), transport: "streamable-http", source: "mcp.json", raw: doc };
210
385
  }
211
386
  const mcpHost = doc.mcp;
212
387
  if (!mcpHost) return null;
388
+ const normalised = ensureMcpPath(mcpHost);
389
+ validateDiscoveredUrl(normalised, "alter.json");
213
390
  return {
214
- url: ensureMcpPath(mcpHost),
391
+ url: normalised,
215
392
  transport: "streamable-http",
216
393
  source: "alter.json",
217
394
  publicKey: doc.pk,
@@ -257,11 +434,14 @@ var X402Client = class {
257
434
  if (!this.assets.has(envelope.asset)) {
258
435
  throw new AlterError("PAYMENT_REQUIRED", `asset ${envelope.asset} not permitted by client policy`);
259
436
  }
260
- if (this.maxPerQuery !== void 0 && Number(envelope.amount) > this.maxPerQuery) {
261
- throw new AlterError(
262
- "PAYMENT_REQUIRED",
263
- `quote ${envelope.amount} ${envelope.asset} exceeds maxPerQuery ${this.maxPerQuery}`
264
- );
437
+ if (this.maxPerQuery !== void 0) {
438
+ const amt = Number(envelope.amount);
439
+ if (!Number.isFinite(amt) || amt < 0 || amt > this.maxPerQuery) {
440
+ throw new AlterError(
441
+ "PAYMENT_REQUIRED",
442
+ `quote ${envelope.amount} ${envelope.asset} exceeds maxPerQuery ${this.maxPerQuery}`
443
+ );
444
+ }
265
445
  }
266
446
  if (!this.signer) {
267
447
  throw new AlterPaymentRequired(envelope.resource ?? "unknown", envelope);
@@ -319,6 +499,7 @@ var MCPClient = class {
319
499
  maxRetries;
320
500
  clientInfo;
321
501
  x402;
502
+ signing;
322
503
  requestCounter = 0;
323
504
  initialised = false;
324
505
  constructor(opts = {}) {
@@ -329,6 +510,7 @@ var MCPClient = class {
329
510
  this.maxRetries = opts.maxRetries ?? 2;
330
511
  this.clientInfo = opts.clientInfo ?? { name: "@truealter/sdk", version: "0.2.0" };
331
512
  this.x402 = opts.x402;
513
+ this.signing = opts.signing;
332
514
  }
333
515
  /**
334
516
  * Send the MCP `initialize` handshake and capture the resulting session
@@ -415,6 +597,7 @@ var MCPClient = class {
415
597
  method
416
598
  };
417
599
  if (params !== void 0) payload.params = params;
600
+ const signatureHeader = this.buildSignatureHeader(method, params);
418
601
  let attempt = 0;
419
602
  let lastErr = null;
420
603
  while (attempt <= this.maxRetries) {
@@ -425,7 +608,7 @@ var MCPClient = class {
425
608
  try {
426
609
  resp = await this.fetchImpl(this.endpoint, {
427
610
  method: "POST",
428
- headers: this.buildHeaders(),
611
+ headers: this.buildHeaders(signatureHeader),
429
612
  body: JSON.stringify(payload),
430
613
  signal: controller.signal
431
614
  });
@@ -452,7 +635,8 @@ var MCPClient = class {
452
635
  throw new AlterPaymentRequired(this.guessToolName(payload), envelope);
453
636
  }
454
637
  if (resp.status === 429) {
455
- const retryAfter = Number(resp.headers.get("Retry-After") ?? 60);
638
+ const rawRetryAfter = Number(resp.headers.get("Retry-After") ?? 60);
639
+ const retryAfter = Number.isFinite(rawRetryAfter) && rawRetryAfter >= 0 ? Math.min(rawRetryAfter, 300) : 60;
456
640
  if (attempt > this.maxRetries) {
457
641
  throw new AlterRateLimited(`HTTP 429 on ${method}`, retryAfter);
458
642
  }
@@ -488,7 +672,7 @@ var MCPClient = class {
488
672
  }
489
673
  throw lastErr ?? new AlterNetworkError(`MCP ${method}: exhausted retries`);
490
674
  }
491
- buildHeaders() {
675
+ buildHeaders(extra) {
492
676
  const headers = {
493
677
  "Content-Type": "application/json",
494
678
  Accept: "application/json",
@@ -496,8 +680,27 @@ var MCPClient = class {
496
680
  };
497
681
  if (this.apiKey) headers["X-ALTER-API-Key"] = this.apiKey;
498
682
  if (this.sessionId) headers["Mcp-Session-Id"] = this.sessionId;
683
+ if (extra) Object.assign(headers, extra);
499
684
  return headers;
500
685
  }
686
+ /**
687
+ * Produce the `Mcp-Invocation-Signature` header for a `tools/call`
688
+ * payload, when signing is configured. Returns `undefined` when no
689
+ * signing key is attached or the method is not `tools/call`.
690
+ */
691
+ buildSignatureHeader(method, params) {
692
+ if (!this.signing) return void 0;
693
+ if (method !== "tools/call") return void 0;
694
+ const p = params;
695
+ if (!p?.name) return void 0;
696
+ const { signInvocation: signInvocation2 } = (init_signing(), __toCommonJS(signing_exports));
697
+ const headerValue = signInvocation2(p.name, p.arguments ?? {}, {
698
+ kid: this.signing.kid,
699
+ privateKey: this.signing.privateKey,
700
+ handle: this.signing.handle
701
+ });
702
+ return { "Mcp-Invocation-Signature": headerValue };
703
+ }
501
704
  async extractPaymentEnvelope(resp) {
502
705
  const headerValue = resp.headers.get("X-402-Payment") ?? resp.headers.get("x-402-payment");
503
706
  if (headerValue) {
@@ -540,8 +743,8 @@ function generateKeypair() {
540
743
  const privateKey = randomBytes(32);
541
744
  const publicKey = ed25519.getPublicKey(privateKey);
542
745
  return {
543
- privateKey: bytesToHex(privateKey),
544
- publicKey: bytesToHex(publicKey),
746
+ privateKey: bytesToHex$1(privateKey),
747
+ publicKey: bytesToHex$1(publicKey),
545
748
  did: encodeDid(publicKey)
546
749
  };
547
750
  }
@@ -553,15 +756,15 @@ function keypairFromPrivateKey(privateKeyHex) {
553
756
  const publicKey = ed25519.getPublicKey(privateKey);
554
757
  return {
555
758
  privateKey: privateKeyHex,
556
- publicKey: bytesToHex(publicKey),
759
+ publicKey: bytesToHex$1(publicKey),
557
760
  did: encodeDid(publicKey)
558
761
  };
559
762
  }
560
763
  function encodeDid(publicKey) {
561
764
  const bytes = typeof publicKey === "string" ? hexToBytes(publicKey) : publicKey;
562
- return `ed25519:${base64urlEncode(bytes)}`;
765
+ return `ed25519:${base64urlEncode2(bytes)}`;
563
766
  }
564
- function base64urlEncode(bytes) {
767
+ function base64urlEncode2(bytes) {
565
768
  let b64;
566
769
  if (typeof Buffer !== "undefined") {
567
770
  b64 = Buffer.from(bytes).toString("base64");
@@ -1054,9 +1257,460 @@ function generateCursorConfig(opts = {}) {
1054
1257
  return generateGenericMcpConfig({ serverName: "alter", ...opts });
1055
1258
  }
1056
1259
 
1057
- // src/index.ts
1260
+ // src/adapters/claude-desktop.ts
1261
+ function generateClaudeDesktopConfig(opts = {}) {
1262
+ const serverName = opts.serverName ?? "alter";
1263
+ const bridgeCommand = opts.bridgeCommand ?? "alter-mcp-bridge";
1264
+ const env3 = {};
1265
+ env3.ALTER_MCP_ENDPOINT = opts.endpoint ?? DEFAULT_ENDPOINT;
1266
+ if (opts.apiKey) env3.ALTER_API_KEY = opts.apiKey;
1267
+ const entry = {
1268
+ command: bridgeCommand,
1269
+ env: env3,
1270
+ description: "ALTER Identity \u2014 psychometric identity field for AI agents"
1271
+ };
1272
+ if (opts.extraArgs && opts.extraArgs.length > 0) {
1273
+ entry.args = [...opts.extraArgs];
1274
+ }
1275
+ return { mcpServers: { [serverName]: entry } };
1276
+ }
1277
+
1278
+ // src/meta.ts
1058
1279
  var SDK_NAME = "@truealter/sdk";
1059
- var SDK_VERSION = "0.2.4";
1280
+ var SDK_VERSION = "0.3.0";
1281
+ var HOME = homedir();
1282
+ var PLAT = platform();
1283
+ function appData() {
1284
+ return env.APPDATA ?? join(HOME, "AppData", "Roaming");
1285
+ }
1286
+ function xdgConfig() {
1287
+ return env.XDG_CONFIG_HOME ?? join(HOME, ".config");
1288
+ }
1289
+ function macAppSupport() {
1290
+ return join(HOME, "Library", "Application Support");
1291
+ }
1292
+ function claudeDesktopConfigPath() {
1293
+ if (PLAT === "darwin") return join(macAppSupport(), "Claude", "claude_desktop_config.json");
1294
+ if (PLAT === "win32") return join(appData(), "Claude", "claude_desktop_config.json");
1295
+ return join(xdgConfig(), "Claude", "claude_desktop_config.json");
1296
+ }
1297
+ function claudeDesktopDir() {
1298
+ if (PLAT === "darwin") return join(macAppSupport(), "Claude");
1299
+ if (PLAT === "win32") return join(appData(), "Claude");
1300
+ return join(xdgConfig(), "Claude");
1301
+ }
1302
+ function vscodeConfigPath() {
1303
+ if (PLAT === "darwin") return join(macAppSupport(), "Code", "User", "mcp.json");
1304
+ if (PLAT === "win32") return join(appData(), "Code", "User", "mcp.json");
1305
+ return join(xdgConfig(), "Code", "User", "mcp.json");
1306
+ }
1307
+ function vscodeDir() {
1308
+ if (PLAT === "darwin") return join(macAppSupport(), "Code", "User");
1309
+ if (PLAT === "win32") return join(appData(), "Code", "User");
1310
+ return join(xdgConfig(), "Code", "User");
1311
+ }
1312
+ var cursorDir = join(HOME, ".cursor");
1313
+ var cursorConfigPath = join(cursorDir, "mcp.json");
1314
+ var claudeCodeProbeDir = join(HOME, ".claude");
1315
+ var CLAUDE_CODE = {
1316
+ id: "claude-code",
1317
+ label: "Claude Code",
1318
+ configPath: null,
1319
+ probeDir: claudeCodeProbeDir,
1320
+ rootKey: "mcpServers"
1321
+ };
1322
+ var CURSOR = {
1323
+ id: "cursor",
1324
+ label: "Cursor",
1325
+ configPath: cursorConfigPath,
1326
+ probeDir: cursorDir,
1327
+ rootKey: "mcpServers"
1328
+ };
1329
+ var CLAUDE_DESKTOP = {
1330
+ id: "claude-desktop",
1331
+ label: "Claude Desktop",
1332
+ configPath: claudeDesktopConfigPath(),
1333
+ probeDir: claudeDesktopDir(),
1334
+ rootKey: "mcpServers"
1335
+ };
1336
+ var VSCODE = {
1337
+ id: "vscode",
1338
+ label: "VS Code",
1339
+ configPath: vscodeConfigPath(),
1340
+ probeDir: vscodeDir(),
1341
+ // VS Code's user-scoped mcp.json uses `servers`, not `mcpServers`.
1342
+ rootKey: "servers"
1343
+ };
1344
+ var ALL_CLIENTS = [CLAUDE_CODE, CURSOR, CLAUDE_DESKTOP, VSCODE];
1345
+ function alterConfigDir() {
1346
+ return join(xdgConfig(), "alter");
1347
+ }
1348
+ function wireStatePath() {
1349
+ return join(alterConfigDir(), "wire-state.json");
1350
+ }
1351
+ function probeClaudeCode() {
1352
+ try {
1353
+ const result = spawnSync("claude", ["--version"], {
1354
+ encoding: "utf8",
1355
+ shell: process.platform === "win32",
1356
+ timeout: 5e3
1357
+ });
1358
+ if (result.error) {
1359
+ return {
1360
+ client: ALL_CLIENTS.find((c) => c.id === "claude-code"),
1361
+ installed: false,
1362
+ reason: `claude binary not on PATH (${result.error.message})`
1363
+ };
1364
+ }
1365
+ if (result.status === 0) {
1366
+ return {
1367
+ client: ALL_CLIENTS.find((c) => c.id === "claude-code"),
1368
+ installed: true,
1369
+ version: result.stdout.trim() || void 0,
1370
+ reason: "claude --version returned 0"
1371
+ };
1372
+ }
1373
+ return {
1374
+ client: ALL_CLIENTS.find((c) => c.id === "claude-code"),
1375
+ installed: false,
1376
+ reason: `claude --version exited ${String(result.status)}`
1377
+ };
1378
+ } catch (err) {
1379
+ return {
1380
+ client: ALL_CLIENTS.find((c) => c.id === "claude-code"),
1381
+ installed: false,
1382
+ reason: err.message
1383
+ };
1384
+ }
1385
+ }
1386
+ function probeByDir(id) {
1387
+ const client = ALL_CLIENTS.find((c) => c.id === id);
1388
+ if (!client) throw new Error(`unknown client id: ${id}`);
1389
+ const installed = existsSync(client.probeDir);
1390
+ return {
1391
+ client,
1392
+ installed,
1393
+ reason: installed ? `found ${client.probeDir}` : `no directory at ${client.probeDir}`
1394
+ };
1395
+ }
1396
+ function probeAll() {
1397
+ return [
1398
+ probeClaudeCode(),
1399
+ probeByDir("cursor"),
1400
+ probeByDir("claude-desktop"),
1401
+ probeByDir("vscode")
1402
+ ];
1403
+ }
1404
+ var SYNC_PREFIXES = [
1405
+ // iCloud Drive — both the new and legacy mounts.
1406
+ "Library/Mobile Documents/com~apple~CloudDocs",
1407
+ "iCloud Drive",
1408
+ // OneDrive variants Microsoft ships across editions.
1409
+ "OneDrive",
1410
+ "OneDrive - ",
1411
+ // Dropbox standard + enterprise mounts.
1412
+ "Dropbox",
1413
+ "Dropbox (",
1414
+ // Google Drive (ALTER does not integrate with Google; still refuse).
1415
+ "Google Drive",
1416
+ "GoogleDrive",
1417
+ "CloudStorage/GoogleDrive",
1418
+ // Box, pCloud, Sync.com, MEGA — high-signal names worth refusing.
1419
+ "Box Sync",
1420
+ "pCloud Drive",
1421
+ "Sync.com",
1422
+ "MEGAsync"
1423
+ ];
1424
+ function detectSyncedVolume(path) {
1425
+ const absolute = resolve(path);
1426
+ const normalised = platform() === "win32" ? absolute.replace(/\\/g, "/") : absolute;
1427
+ for (const prefix of SYNC_PREFIXES) {
1428
+ if (normalised.includes(`/${prefix}/`) || normalised.includes(`/${prefix}`)) {
1429
+ return { refused: true, matchedPrefix: prefix, resolvedPath: absolute };
1430
+ }
1431
+ }
1432
+ return null;
1433
+ }
1434
+ var WIRE_STATE_VERSION = 1;
1435
+ function readWireState() {
1436
+ const path = wireStatePath();
1437
+ if (!existsSync(path)) return null;
1438
+ try {
1439
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
1440
+ if (parsed.version !== WIRE_STATE_VERSION) {
1441
+ throw new Error(
1442
+ `wire-state.json version ${String(parsed.version)} is not supported by this SDK (expected ${WIRE_STATE_VERSION})`
1443
+ );
1444
+ }
1445
+ return parsed;
1446
+ } catch (err) {
1447
+ throw new Error(`failed to parse wire-state.json: ${err.message}`);
1448
+ }
1449
+ }
1450
+ function writeWireState(state) {
1451
+ const path = wireStatePath();
1452
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
1453
+ writeFileSync(path, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
1454
+ }
1455
+ function sha2562(bytes) {
1456
+ return createHash("sha256").update(bytes).digest("hex");
1457
+ }
1458
+ function atomicJsonMerge(opts) {
1459
+ const { path, timestamp, merge, idempotent = true } = opts;
1460
+ const tmpPath = `${path}.alter-tmp-${timestamp}`;
1461
+ const backupPath = `${path}.alter-backup-${timestamp}`;
1462
+ let existed = false;
1463
+ let preBytes = null;
1464
+ let parsed = {};
1465
+ if (existsSync(path)) {
1466
+ existed = true;
1467
+ preBytes = readFileSync(path, "utf8");
1468
+ if (preBytes.trim().length > 0) {
1469
+ try {
1470
+ parsed = JSON.parse(preBytes);
1471
+ } catch (err) {
1472
+ throw new Error(
1473
+ `refusing to wire ${path}: existing file is not valid JSON (${err.message}). Hand-fix the file, then re-run \`alter-identity wire\`.`
1474
+ );
1475
+ }
1476
+ if (typeof parsed !== "object" || Array.isArray(parsed) || parsed === null) {
1477
+ throw new Error(`refusing to wire ${path}: existing JSON root is not an object`);
1478
+ }
1479
+ }
1480
+ }
1481
+ const merged = merge(parsed);
1482
+ const serialised = JSON.stringify(merged, null, 2) + "\n";
1483
+ if (idempotent && preBytes !== null && preBytes === serialised) {
1484
+ return {
1485
+ path,
1486
+ backupPath: null,
1487
+ preSha256: sha2562(preBytes),
1488
+ postSha256: sha2562(preBytes),
1489
+ noop: true
1490
+ };
1491
+ }
1492
+ mkdirSync(dirname(path), { recursive: true });
1493
+ writeFileSync(tmpPath, serialised, { mode: 384 });
1494
+ try {
1495
+ if (existed) copyFileSync(path, backupPath);
1496
+ renameSync(tmpPath, path);
1497
+ } catch (err) {
1498
+ try {
1499
+ unlinkSync(tmpPath);
1500
+ } catch {
1501
+ }
1502
+ throw err;
1503
+ }
1504
+ return {
1505
+ path,
1506
+ backupPath: existed ? backupPath : null,
1507
+ preSha256: preBytes === null ? null : sha2562(preBytes),
1508
+ postSha256: sha2562(serialised),
1509
+ noop: false
1510
+ };
1511
+ }
1512
+ function restoreFromBackup(path, backupPath) {
1513
+ if (backupPath === null) {
1514
+ if (existsSync(path)) unlinkSync(path);
1515
+ return;
1516
+ }
1517
+ if (!existsSync(backupPath)) {
1518
+ throw new Error(`cannot restore ${path}: backup missing at ${backupPath}`);
1519
+ }
1520
+ renameSync(backupPath, path);
1521
+ }
1522
+
1523
+ // src/wire/index.ts
1524
+ var TIMESTAMP = () => String(Math.floor(Date.now() / 1e3));
1525
+ var ISO_NOW = () => (/* @__PURE__ */ new Date()).toISOString();
1526
+ function clientById(id) {
1527
+ const hit = ALL_CLIENTS.find((c) => c.id === id);
1528
+ if (!hit) throw new Error(`unknown client id: ${id}`);
1529
+ return hit;
1530
+ }
1531
+ function wire(opts = {}) {
1532
+ const endpoint = opts.endpoint ?? DEFAULT_ENDPOINT;
1533
+ const apiKey = opts.apiKey;
1534
+ const probes = probeAll();
1535
+ const selection = opts.only ?? probes.filter((p) => p.installed).map((p) => p.client.id);
1536
+ const ts = TIMESTAMP();
1537
+ const targets = [];
1538
+ for (const id of selection) {
1539
+ const probe = id === "claude-code" ? probeClaudeCode() : probeByDir(id);
1540
+ if (!probe.installed && opts.skipMissing !== false) {
1541
+ targets.push({
1542
+ client: id,
1543
+ method: id === "claude-code" ? "cli" : "file",
1544
+ status: "skipped",
1545
+ ...id === "claude-code" ? { command: "" } : { path: clientById(id).configPath ?? "", backupPath: null, rootKey: clientById(id).rootKey, serverName: "alter", preSha256: null, postSha256: "" },
1546
+ reason: probe.reason
1547
+ });
1548
+ continue;
1549
+ }
1550
+ try {
1551
+ if (id === "claude-code") {
1552
+ targets.push(wireClaudeCode({ endpoint, apiKey }));
1553
+ } else {
1554
+ targets.push(wireFileTarget({ id, endpoint, apiKey, timestamp: ts }));
1555
+ }
1556
+ } catch (err) {
1557
+ const message = err.message;
1558
+ targets.push({
1559
+ client: id,
1560
+ method: id === "claude-code" ? "cli" : "file",
1561
+ status: "failed",
1562
+ ...id === "claude-code" ? { command: "" } : { path: clientById(id).configPath ?? "", backupPath: null, rootKey: clientById(id).rootKey, serverName: "alter", preSha256: null, postSha256: "" },
1563
+ reason: message
1564
+ });
1565
+ }
1566
+ }
1567
+ const state = {
1568
+ version: 1,
1569
+ sdkVersion: SDK_VERSION,
1570
+ writtenAt: ISO_NOW(),
1571
+ endpoint,
1572
+ targets
1573
+ };
1574
+ writeWireState(state);
1575
+ return { state, probes };
1576
+ }
1577
+ function wireFileTarget(args) {
1578
+ const client = clientById(args.id);
1579
+ if (!client.configPath) {
1580
+ throw new Error(`client ${client.id} has no file-based config path`);
1581
+ }
1582
+ const sync = detectSyncedVolume(client.configPath);
1583
+ if (sync) {
1584
+ throw new Error(
1585
+ `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.`
1586
+ );
1587
+ }
1588
+ const entry = args.id === "claude-desktop" ? generateClaudeDesktopConfig({ endpoint: args.endpoint, apiKey: args.apiKey }) : generateGenericMcpConfig({ endpoint: args.endpoint, apiKey: args.apiKey });
1589
+ const rootKey = client.rootKey;
1590
+ const serverName = "alter";
1591
+ const result = atomicJsonMerge({
1592
+ path: client.configPath,
1593
+ timestamp: args.timestamp,
1594
+ merge: (existing) => {
1595
+ const bucket = existing[rootKey] ?? {};
1596
+ const source = entry.mcpServers.alter;
1597
+ return {
1598
+ ...existing,
1599
+ [rootKey]: {
1600
+ ...bucket,
1601
+ [serverName]: source
1602
+ }
1603
+ };
1604
+ }
1605
+ });
1606
+ return {
1607
+ client: args.id,
1608
+ method: "file",
1609
+ status: result.noop ? "already-wired" : "written",
1610
+ path: result.path,
1611
+ backupPath: result.backupPath,
1612
+ rootKey,
1613
+ serverName,
1614
+ preSha256: result.preSha256,
1615
+ postSha256: result.postSha256
1616
+ };
1617
+ }
1618
+ function wireClaudeCode(args) {
1619
+ const cmd = "claude";
1620
+ const argList = [
1621
+ "mcp",
1622
+ "add",
1623
+ "--scope",
1624
+ "user",
1625
+ "--transport",
1626
+ "http",
1627
+ "alter",
1628
+ args.endpoint
1629
+ ];
1630
+ if (args.apiKey) {
1631
+ argList.push("--header", `X-ALTER-API-Key:${args.apiKey}`);
1632
+ }
1633
+ const full = `${cmd} ${argList.join(" ")}`;
1634
+ const run = spawnSync(cmd, argList, {
1635
+ encoding: "utf8",
1636
+ shell: process.platform === "win32",
1637
+ timeout: 1e4
1638
+ });
1639
+ if (run.error) {
1640
+ return {
1641
+ client: "claude-code",
1642
+ method: "cli",
1643
+ status: "failed",
1644
+ command: full,
1645
+ stdout: run.stdout,
1646
+ stderr: run.stderr,
1647
+ reason: run.error.message
1648
+ };
1649
+ }
1650
+ const stderr2 = (run.stderr ?? "").toLowerCase();
1651
+ const alreadyExists = stderr2.includes("already exists") || stderr2.includes("already configured");
1652
+ if (run.status === 0) {
1653
+ return { client: "claude-code", method: "cli", status: "written", command: full, stdout: run.stdout, stderr: run.stderr };
1654
+ }
1655
+ if (alreadyExists) {
1656
+ return { client: "claude-code", method: "cli", status: "already-wired", command: full, stdout: run.stdout, stderr: run.stderr };
1657
+ }
1658
+ return {
1659
+ client: "claude-code",
1660
+ method: "cli",
1661
+ status: "failed",
1662
+ command: full,
1663
+ stdout: run.stdout,
1664
+ stderr: run.stderr,
1665
+ reason: `claude mcp add exited ${String(run.status)}`
1666
+ };
1667
+ }
1668
+ function unwire() {
1669
+ const state = readWireState();
1670
+ const undone = [];
1671
+ if (!state || state.targets.length === 0) {
1672
+ return { state, undone };
1673
+ }
1674
+ for (const target of state.targets) {
1675
+ try {
1676
+ if (target.method === "file") {
1677
+ if (target.status === "written") {
1678
+ restoreFromBackup(target.path, target.backupPath);
1679
+ undone.push({ client: target.client, action: target.backupPath ? "restored" : "removed" });
1680
+ } else {
1681
+ undone.push({ client: target.client, action: "skipped", reason: `target status was ${target.status}` });
1682
+ }
1683
+ } else if (target.method === "cli") {
1684
+ if (target.status === "written") {
1685
+ const run = spawnSync("claude", ["mcp", "remove", "--scope", "user", "alter"], {
1686
+ encoding: "utf8",
1687
+ shell: process.platform === "win32",
1688
+ timeout: 1e4
1689
+ });
1690
+ if (run.error) {
1691
+ undone.push({ client: target.client, action: "failed", reason: run.error.message });
1692
+ } else if (run.status === 0) {
1693
+ undone.push({ client: target.client, action: "cli-removed" });
1694
+ } else {
1695
+ undone.push({ client: target.client, action: "failed", reason: `claude mcp remove exited ${String(run.status)}` });
1696
+ }
1697
+ } else {
1698
+ undone.push({ client: target.client, action: "skipped", reason: `target status was ${target.status}` });
1699
+ }
1700
+ }
1701
+ } catch (err) {
1702
+ undone.push({ client: target.client, action: "failed", reason: err.message });
1703
+ }
1704
+ }
1705
+ writeWireState({
1706
+ version: 1,
1707
+ sdkVersion: state.sdkVersion,
1708
+ writtenAt: ISO_NOW(),
1709
+ endpoint: state.endpoint,
1710
+ targets: []
1711
+ });
1712
+ return { state, undone };
1713
+ }
1060
1714
 
1061
1715
  // bin/alter-identity.ts
1062
1716
  var CONFIG_DIR = join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "alter");
@@ -1076,6 +1730,12 @@ async function main() {
1076
1730
  case "config":
1077
1731
  await runConfig(rest);
1078
1732
  break;
1733
+ case "wire":
1734
+ await runWire(rest);
1735
+ break;
1736
+ case "unwire":
1737
+ await runUnwire();
1738
+ break;
1079
1739
  case "message":
1080
1740
  await runMessage(rest);
1081
1741
  break;
@@ -1103,11 +1763,15 @@ function printHelp() {
1103
1763
  stdout.write(`${SDK_NAME} ${SDK_VERSION}
1104
1764
 
1105
1765
  Usage:
1106
- alter-identity init Generate Ed25519 keypair, discover MCP, write config
1766
+ alter-identity init [--wire|--no-wire] [--yes]
1767
+ Generate keypair, discover MCP, optionally wire detected AI clients
1107
1768
  alter-identity verify <~handle|email> Verify an identity
1108
1769
  alter-identity status Show connection state
1109
- alter-identity config [--claude|--cursor|--generic]
1770
+ alter-identity config [--claude|--cursor|--claude-desktop|--generic]
1110
1771
  Print MCP config snippet
1772
+ alter-identity wire [--only=<ids>] [--yes]
1773
+ Merge ALTER into detected AI clients (Claude Code, Cursor, Claude Desktop)
1774
+ alter-identity unwire Restore every target from its backup sibling
1111
1775
 
1112
1776
  Alter-to-Alter Messaging:
1113
1777
  alter-identity message send <~handle> <body> Send a direct message (body '-' = stdin)
@@ -1121,6 +1785,13 @@ Config: ${CONFIG_PATH}
1121
1785
  }
1122
1786
  async function runInit(args) {
1123
1787
  const force = args.includes("--force") || args.includes("-f");
1788
+ const wireFlag = args.includes("--wire");
1789
+ const noWireFlag = args.includes("--no-wire");
1790
+ const yesFlag = args.includes("--yes") || args.includes("-y");
1791
+ if (wireFlag && noWireFlag) {
1792
+ stderr.write("error: --wire and --no-wire are mutually exclusive\n");
1793
+ exit(2);
1794
+ }
1124
1795
  const existing = readConfig();
1125
1796
  if (existing && !force) {
1126
1797
  stdout.write(`already initialised at ${CONFIG_PATH} (re-run with --force to overwrite)
@@ -1147,6 +1818,28 @@ async function runInit(args) {
1147
1818
  `);
1148
1819
  stdout.write(` did: ${keypair.did}
1149
1820
  `);
1821
+ let shouldWire = false;
1822
+ if (noWireFlag) {
1823
+ shouldWire = false;
1824
+ } else if (wireFlag || yesFlag) {
1825
+ shouldWire = true;
1826
+ } else if (stdin.isTTY) {
1827
+ const probes = probeAll();
1828
+ const found = probes.filter((p) => p.installed).map((p) => p.client.label);
1829
+ if (found.length === 0) {
1830
+ stdout.write("\nNo MCP-aware clients detected on this machine \u2014 skipping wire.\n");
1831
+ } else {
1832
+ stdout.write(`
1833
+ Detected MCP-aware clients: ${found.join(", ")}
1834
+ `);
1835
+ shouldWire = await confirm("Wire detected AI clients to ALTER?", true);
1836
+ }
1837
+ }
1838
+ if (shouldWire) {
1839
+ stdout.write("\n\u2022 Wiring detected AI clients...\n");
1840
+ const report = wire({ endpoint });
1841
+ printWireReport(report);
1842
+ }
1150
1843
  stdout.write(`
1151
1844
  Next: alter-identity verify ~truealter
1152
1845
  `);
@@ -1205,10 +1898,121 @@ async function runConfig(args) {
1205
1898
  const opts = { endpoint: cfg.endpoint, apiKey: cfg.apiKey };
1206
1899
  let out;
1207
1900
  if (args.includes("--cursor")) out = generateCursorConfig(opts);
1901
+ else if (args.includes("--claude-desktop")) out = generateClaudeDesktopConfig(opts);
1208
1902
  else if (args.includes("--generic")) out = generateGenericMcpConfig(opts);
1209
1903
  else out = generateClaudeConfig(opts);
1210
1904
  stdout.write(JSON.stringify(out, null, 2) + "\n");
1211
1905
  }
1906
+ async function runWire(args) {
1907
+ const yesFlag = args.includes("--yes") || args.includes("-y");
1908
+ const onlyArg = args.find((a) => a.startsWith("--only="));
1909
+ const only = onlyArg ? onlyArg.slice("--only=".length).split(",").filter(Boolean) : void 0;
1910
+ const cfg = readConfig() ?? {};
1911
+ if (!cfg.endpoint) {
1912
+ stderr.write("error: no endpoint \u2014 run `alter-identity init` first\n");
1913
+ exit(2);
1914
+ }
1915
+ if (!yesFlag && stdin.isTTY) {
1916
+ const probes = probeAll();
1917
+ const found = probes.filter((p) => p.installed).map((p) => p.client.label);
1918
+ if (found.length === 0) {
1919
+ stdout.write("No MCP-aware clients detected on this machine. Nothing to do.\n");
1920
+ return;
1921
+ }
1922
+ stdout.write(`Detected: ${found.join(", ")}
1923
+ `);
1924
+ const proceed = await confirm("Wire these clients to ALTER?", true);
1925
+ if (!proceed) {
1926
+ stdout.write("aborted.\n");
1927
+ return;
1928
+ }
1929
+ }
1930
+ const report = wire({ endpoint: cfg.endpoint, apiKey: cfg.apiKey, only });
1931
+ printWireReport(report);
1932
+ }
1933
+ async function runUnwire() {
1934
+ const report = unwire();
1935
+ printUnwireReport(report);
1936
+ }
1937
+ function printWireReport(report) {
1938
+ for (const target of report.state.targets) {
1939
+ const tag = `[${target.client}]`;
1940
+ switch (target.status) {
1941
+ case "written":
1942
+ if (target.method === "file") {
1943
+ stdout.write(` \u2713 ${tag} wrote ${target.path} (backup: ${target.backupPath ?? "(none \u2014 created new file)"})
1944
+ `);
1945
+ } else {
1946
+ stdout.write(` \u2713 ${tag} registered via \`${target.command}\`
1947
+ `);
1948
+ }
1949
+ break;
1950
+ case "already-wired":
1951
+ stdout.write(` \xB7 ${tag} already wired \u2014 no change
1952
+ `);
1953
+ break;
1954
+ case "skipped":
1955
+ stdout.write(` - ${tag} skipped (${target.reason ?? "not installed"})
1956
+ `);
1957
+ break;
1958
+ case "failed":
1959
+ stderr.write(` \u2717 ${tag} failed: ${target.reason ?? "unknown"}
1960
+ `);
1961
+ break;
1962
+ }
1963
+ }
1964
+ stdout.write(`
1965
+ wire-state \u2192 ${join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "alter", "wire-state.json")}
1966
+ `);
1967
+ stdout.write("run `alter-identity unwire` to reverse.\n");
1968
+ }
1969
+ function printUnwireReport(report) {
1970
+ if (!report.state) {
1971
+ stdout.write("nothing to unwire \u2014 no wire-state.json found\n");
1972
+ return;
1973
+ }
1974
+ if (report.state.targets.length === 0) {
1975
+ stdout.write("wire-state.json is empty \u2014 nothing to unwire\n");
1976
+ return;
1977
+ }
1978
+ for (const entry of report.undone) {
1979
+ const tag = `[${entry.client}]`;
1980
+ switch (entry.action) {
1981
+ case "restored":
1982
+ stdout.write(` \u2713 ${tag} restored from backup
1983
+ `);
1984
+ break;
1985
+ case "removed":
1986
+ stdout.write(` \u2713 ${tag} removed (file was created by wire)
1987
+ `);
1988
+ break;
1989
+ case "cli-removed":
1990
+ stdout.write(` \u2713 ${tag} removed via \`claude mcp remove\`
1991
+ `);
1992
+ break;
1993
+ case "skipped":
1994
+ stdout.write(` \xB7 ${tag} skipped (${entry.reason ?? ""})
1995
+ `);
1996
+ break;
1997
+ case "failed":
1998
+ stderr.write(` \u2717 ${tag} failed: ${entry.reason ?? ""}
1999
+ `);
2000
+ break;
2001
+ }
2002
+ }
2003
+ }
2004
+ async function confirm(question, defaultYes) {
2005
+ if (!stdin.isTTY) return false;
2006
+ const rl = createInterface({ input: stdin, output: stdout });
2007
+ const suffix = " [Y/n] " ;
2008
+ const answer = await new Promise((resolve2) => {
2009
+ rl.question(question + suffix, (ans) => resolve2(ans));
2010
+ });
2011
+ rl.close();
2012
+ const trimmed = answer.trim().toLowerCase();
2013
+ if (!trimmed) return defaultYes;
2014
+ return trimmed === "y" || trimmed === "yes";
2015
+ }
1212
2016
  async function runMessage(args) {
1213
2017
  const [sub, ...rest] = args;
1214
2018
  if (!sub) {