@truealter/sdk 0.2.4 → 0.5.0

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