@truealter/sdk 0.2.2 → 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");
@@ -586,6 +789,8 @@ function base64urlDecode(input) {
586
789
  // src/provenance.ts
587
790
  var _jwksCache = /* @__PURE__ */ new Map();
588
791
  var JWKS_TTL_MS = 5 * 60 * 1e3;
792
+ var JWKS_MAX_BYTES = 64 * 1024;
793
+ var JWKS_CACHE_MAX_ENTRIES = 32;
589
794
  var DEFAULT_VERIFY_AT_ALLOWLIST = Object.freeze([
590
795
  "api.truealter.com",
591
796
  "mcp.truealter.com"
@@ -695,7 +900,8 @@ async function fetchPublicKeys(jwksUrl, fetchImpl = fetch) {
695
900
  return fetchJwks(jwksUrl, fetchImpl);
696
901
  }
697
902
  async function fetchJwks(url, fetchImpl) {
698
- const cached = _jwksCache.get(url);
903
+ const cacheKey = jwksCacheKey(url);
904
+ const cached = _jwksCache.get(cacheKey);
699
905
  if (cached && Date.now() - cached.fetched < JWKS_TTL_MS) return cached.jwks;
700
906
  let resp;
701
907
  try {
@@ -712,13 +918,45 @@ async function fetchJwks(url, fetchImpl) {
712
918
  );
713
919
  }
714
920
  if (!resp.ok) throw new AlterNetworkError(`${url} \u2192 HTTP ${resp.status}`);
715
- const doc = await resp.json();
921
+ const contentLength = resp.headers.get("content-length");
922
+ if (contentLength !== null) {
923
+ const n = Number.parseInt(contentLength, 10);
924
+ if (Number.isFinite(n) && n > JWKS_MAX_BYTES) {
925
+ throw new AlterProvenanceError(
926
+ `${url} \u2192 JWKS too large: ${n} > ${JWKS_MAX_BYTES} bytes`
927
+ );
928
+ }
929
+ }
930
+ const body = await resp.text();
931
+ if (body.length > JWKS_MAX_BYTES) {
932
+ throw new AlterProvenanceError(
933
+ `${url} \u2192 JWKS too large: ${body.length} > ${JWKS_MAX_BYTES} bytes`
934
+ );
935
+ }
936
+ let doc;
937
+ try {
938
+ doc = JSON.parse(body);
939
+ } catch (err) {
940
+ throw new AlterProvenanceError(`invalid JWKS at ${url}: ${err.message}`);
941
+ }
716
942
  if (!doc || !Array.isArray(doc.keys)) {
717
943
  throw new AlterProvenanceError(`invalid JWKS at ${url}`);
718
944
  }
719
- _jwksCache.set(url, { fetched: Date.now(), jwks: doc });
945
+ if (_jwksCache.size >= JWKS_CACHE_MAX_ENTRIES && !_jwksCache.has(cacheKey)) {
946
+ const oldest = _jwksCache.keys().next().value;
947
+ if (oldest !== void 0) _jwksCache.delete(oldest);
948
+ }
949
+ _jwksCache.set(cacheKey, { fetched: Date.now(), jwks: doc });
720
950
  return doc;
721
951
  }
952
+ function jwksCacheKey(url) {
953
+ try {
954
+ const parsed = new URL(url);
955
+ return `${parsed.origin}${parsed.pathname}`;
956
+ } catch {
957
+ return url;
958
+ }
959
+ }
722
960
  function resolveVerifyAt(verifyAt, allowlist = DEFAULT_VERIFY_AT_ALLOWLIST) {
723
961
  if (typeof verifyAt !== "string" || verifyAt.length === 0) {
724
962
  throw new Error("verify_at must be a non-empty string");
@@ -741,6 +979,9 @@ function resolveVerifyAt(verifyAt, allowlist = DEFAULT_VERIFY_AT_ALLOWLIST) {
741
979
  if (parsed.protocol !== "https:") {
742
980
  throw new Error(`verify_at must be https: ${verifyAt}`);
743
981
  }
982
+ if (parsed.username || parsed.password) {
983
+ throw new Error(`verify_at must not contain userinfo: ${verifyAt}`);
984
+ }
744
985
  const host = parsed.hostname.toLowerCase();
745
986
  const allowed = allowlist.some((h) => h.toLowerCase() === host);
746
987
  if (!allowed) {
@@ -821,6 +1062,15 @@ var AlterClient = class {
821
1062
  await this.mcp.initialize();
822
1063
  }
823
1064
  // ── Free tier ────────────────────────────────────────────────────────
1065
+ /** First handshake — confirms the connection, returns trust tier and tool counts. */
1066
+ async helloAgent() {
1067
+ return this.mcp.callTool("hello_agent", {});
1068
+ }
1069
+ /** Resolve a ~handle (e.g. ~drew) to its canonical form and kind. No auth required. */
1070
+ async resolveHandle(args) {
1071
+ const payload = typeof args === "string" ? { query: args } : args;
1072
+ return this.mcp.callTool("alter_resolve_handle", payload);
1073
+ }
824
1074
  /** Verify a person is registered with ALTER (handle or id). */
825
1075
  async verify(handleOrId, claims) {
826
1076
  const args = handleOrId.includes("@") ? { candidate_id: "", email: handleOrId } : handleOrId.startsWith("~") ? (
@@ -857,12 +1107,6 @@ var AlterClient = class {
857
1107
  async getCompetencies(args) {
858
1108
  return this.mcp.callTool("get_competencies", args);
859
1109
  }
860
- async createIdentityStub(args) {
861
- return this.mcp.callTool("create_identity_stub", args);
862
- }
863
- async submitContext(args) {
864
- return this.mcp.callTool("submit_context", args);
865
- }
866
1110
  async searchIdentities(args) {
867
1111
  return this.mcp.callTool("search_identities", args);
868
1112
  }
@@ -887,9 +1131,6 @@ var AlterClient = class {
887
1131
  async getPrivacyBudget(args) {
888
1132
  return this.mcp.callTool("get_privacy_budget", args);
889
1133
  }
890
- async disputeAttestation(args) {
891
- return this.mcp.callTool("dispute_attestation", args);
892
- }
893
1134
  // ── Golden Thread ────────────────────────────────────────────────────
894
1135
  async goldenThreadStatus() {
895
1136
  return this.mcp.callTool("golden_thread_status", {});
@@ -925,18 +1166,6 @@ var AlterClient = class {
925
1166
  async generateMatchNarrative(args, opts) {
926
1167
  return this.mcp.callTool("generate_match_narrative", args, opts);
927
1168
  }
928
- async submitBatchContext(args, opts) {
929
- return this.mcp.callTool("submit_batch_context", args, opts);
930
- }
931
- async submitStructuredProfile(args, opts) {
932
- return this.mcp.callTool("submit_structured_profile", args, opts);
933
- }
934
- async submitSocialLinks(args, opts) {
935
- return this.mcp.callTool("submit_social_links", args, opts);
936
- }
937
- async attestDomain(args, opts) {
938
- return this.mcp.callTool("attest_domain", args, opts);
939
- }
940
1169
  async getSideQuestGraph(args, opts) {
941
1170
  return this.mcp.callTool("get_side_quest_graph", args, opts);
942
1171
  }
@@ -1028,9 +1257,460 @@ function generateCursorConfig(opts = {}) {
1028
1257
  return generateGenericMcpConfig({ serverName: "alter", ...opts });
1029
1258
  }
1030
1259
 
1031
- // 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
1032
1279
  var SDK_NAME = "@truealter/sdk";
1033
- var SDK_VERSION = "0.1.1";
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
+ }
1034
1714
 
1035
1715
  // bin/alter-identity.ts
1036
1716
  var CONFIG_DIR = join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "alter");
@@ -1050,6 +1730,12 @@ async function main() {
1050
1730
  case "config":
1051
1731
  await runConfig(rest);
1052
1732
  break;
1733
+ case "wire":
1734
+ await runWire(rest);
1735
+ break;
1736
+ case "unwire":
1737
+ await runUnwire();
1738
+ break;
1053
1739
  case "message":
1054
1740
  await runMessage(rest);
1055
1741
  break;
@@ -1077,11 +1763,15 @@ function printHelp() {
1077
1763
  stdout.write(`${SDK_NAME} ${SDK_VERSION}
1078
1764
 
1079
1765
  Usage:
1080
- 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
1081
1768
  alter-identity verify <~handle|email> Verify an identity
1082
1769
  alter-identity status Show connection state
1083
- alter-identity config [--claude|--cursor|--generic]
1770
+ alter-identity config [--claude|--cursor|--claude-desktop|--generic]
1084
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
1085
1775
 
1086
1776
  Alter-to-Alter Messaging:
1087
1777
  alter-identity message send <~handle> <body> Send a direct message (body '-' = stdin)
@@ -1095,6 +1785,13 @@ Config: ${CONFIG_PATH}
1095
1785
  }
1096
1786
  async function runInit(args) {
1097
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
+ }
1098
1795
  const existing = readConfig();
1099
1796
  if (existing && !force) {
1100
1797
  stdout.write(`already initialised at ${CONFIG_PATH} (re-run with --force to overwrite)
@@ -1121,6 +1818,28 @@ async function runInit(args) {
1121
1818
  `);
1122
1819
  stdout.write(` did: ${keypair.did}
1123
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
+ }
1124
1843
  stdout.write(`
1125
1844
  Next: alter-identity verify ~truealter
1126
1845
  `);
@@ -1179,10 +1898,121 @@ async function runConfig(args) {
1179
1898
  const opts = { endpoint: cfg.endpoint, apiKey: cfg.apiKey };
1180
1899
  let out;
1181
1900
  if (args.includes("--cursor")) out = generateCursorConfig(opts);
1901
+ else if (args.includes("--claude-desktop")) out = generateClaudeDesktopConfig(opts);
1182
1902
  else if (args.includes("--generic")) out = generateGenericMcpConfig(opts);
1183
1903
  else out = generateClaudeConfig(opts);
1184
1904
  stdout.write(JSON.stringify(out, null, 2) + "\n");
1185
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
+ }
1186
2016
  async function runMessage(args) {
1187
2017
  const [sub, ...rest] = args;
1188
2018
  if (!sub) {
@@ -1316,5 +2146,3 @@ main().catch((err) => {
1316
2146
  `);
1317
2147
  exit(1);
1318
2148
  });
1319
- //# sourceMappingURL=alter-identity.js.map
1320
- //# sourceMappingURL=alter-identity.js.map