@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.
- package/README.md +50 -36
- package/dist/bin/alter-identity.js +841 -31
- package/dist/bin/mcp-bridge.js +201 -9
- package/dist/index.cjs +785 -24
- package/dist/index.d.cts +677 -41
- package/dist/index.d.ts +677 -41
- package/dist/index.js +712 -28
- package/package.json +4 -3
|
@@ -1,11 +1,151 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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 {
|
|
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
|
|
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
|
|
208
|
-
if (!
|
|
209
|
-
|
|
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:
|
|
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
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
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:${
|
|
761
|
+
return `ed25519:${base64urlEncode2(bytes)}`;
|
|
563
762
|
}
|
|
564
|
-
function
|
|
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("@") ? {
|
|
874
|
-
// ~handle — server resolves these via the
|
|
875
|
-
{
|
|
876
|
-
) : {
|
|
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/
|
|
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.
|
|
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
|
|
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) {
|