@pyxmate/memory 1.26.6 → 1.27.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,4185 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import "../chunk-6EEEOV6B.mjs";
3
2
  import {
4
- PERSISTENT_MEMORY_SECTION,
5
- buildDesignGuide
6
- } from "../chunk-QD7G23CN.mjs";
7
- import {
8
- MEMORY_PROJECT_LABEL_MAX_CHARS,
9
- TAXONOMY_MAX_CATEGORIES,
10
- TAXONOMY_MAX_PROJECTS,
11
- TAXONOMY_MAX_SAMPLE_TOPICS,
12
- TAXONOMY_MAX_TOP_ENTITIES
13
- } from "../chunk-KZKGRND2.mjs";
14
- import {
15
- HOSTED_FILE_INGEST_PROTOCOL,
16
- HOSTED_FILE_INGEST_RETRYABLE_CODES,
17
- parseHostedFileIngestAuthorization,
18
- parseHostedFileIngestCommitResult,
19
- parseHostedFileIngestJobView,
20
- parseHostedFileIngestPublishResult,
21
- requireHostedFileIngestFilename
22
- } from "../chunk-HTQBKJTD.mjs";
23
-
24
- // src/cli/exit-codes.ts
25
- var EXIT = {
26
- OK: 0,
27
- DOCTOR_FAIL: 1,
28
- USAGE: 2,
29
- NOT_LOGGED_IN: 3,
30
- CRED_UNAVAILABLE: 4,
31
- PROVIDER_BROKEN: 5,
32
- BUSY: 6,
33
- CLEANUP_PENDING: 7,
34
- CANCELLED: 8
35
- };
36
-
37
- // src/cli/lifecycle/journal.ts
38
- import { createHash } from "crypto";
39
-
40
- // src/cli/keychain/errors.ts
41
- var PLATFORM_GUIDANCE = {
42
- darwin: {
43
- code: "macos-keychain-denied",
44
- message: "macOS Keychain is locked or access was denied.",
45
- guidance: "Unlock Keychain Access, approve the pyx-mem prompt, then run: pyx-mem login"
46
- },
47
- linuxMissing: {
48
- code: "linux-secret-service-missing",
49
- message: "Linux Secret Service is unavailable.",
50
- guidance: "Install and start a Secret Service provider such as gnome-keyring or KWallet, then run: pyx-mem login"
51
- },
52
- linuxDaemon: {
53
- code: "linux-secret-service-not-running",
54
- message: "Linux Secret Service is installed but no keyring daemon is reachable.",
55
- guidance: "Start a DBus session and unlock gnome-keyring or KWallet, then run: pyx-mem login"
56
- },
57
- win32: {
58
- code: "windows-credential-manager-unavailable",
59
- message: "Windows Credential Manager is unavailable for this user session.",
60
- guidance: "Enable Credential Manager and run pyx-mem from a normal Windows user session, then run: pyx-mem login"
61
- }
62
- };
63
- var PROVIDER_IMPORT = {
64
- code: "provider-import-failed",
65
- message: "pyx-mem credential provider failed to load (@napi-rs/keyring).",
66
- guidance: "Reinstall @pyxmate/memory; optional native keyring binaries may be missing for this platform. Re-run with `npm install --include=optional` if a package manager omitted them."
67
- };
68
- var RECORD_INVALID = {
69
- code: "credential-record-invalid",
70
- message: "Stored pyx-memory credentials are unreadable (schema mismatch or JSON corruption).",
71
- guidance: "Run: pyx-mem logout && pyx-mem login"
72
- };
73
- var KeychainError = class extends Error {
74
- code;
75
- guidance;
76
- exit;
77
- constructor(args) {
78
- super(args.message);
79
- this.name = "KeychainError";
80
- this.code = args.code;
81
- this.guidance = args.guidance;
82
- this.exit = args.exit;
83
- if (args.cause !== void 0) this.cause = args.cause;
84
- }
85
- };
86
- function platformAccessError(cause) {
87
- const platform = process.platform;
88
- const profile = platform === "darwin" ? PLATFORM_GUIDANCE.darwin : platform === "win32" ? PLATFORM_GUIDANCE.win32 : isHeadlessLinuxDaemon(cause) ? PLATFORM_GUIDANCE.linuxDaemon : PLATFORM_GUIDANCE.linuxMissing;
89
- return new KeychainError({ ...profile, exit: EXIT.CRED_UNAVAILABLE, cause });
90
- }
91
- function providerImportError(cause) {
92
- return new KeychainError({ ...PROVIDER_IMPORT, exit: EXIT.PROVIDER_BROKEN, cause });
93
- }
94
- function recordInvalidError(cause) {
95
- return new KeychainError({ ...RECORD_INVALID, exit: EXIT.NOT_LOGGED_IN, cause });
96
- }
97
- function isHeadlessLinuxDaemon(cause) {
98
- if (process.platform !== "linux") return false;
99
- const msg = causeMessage(cause).toLowerCase();
100
- return msg.includes("dbus") || msg.includes("session bus") || msg.includes("no such interface");
101
- }
102
- function causeMessage(cause) {
103
- if (cause instanceof Error) return cause.message;
104
- if (typeof cause === "string") return cause;
105
- return "";
106
- }
107
-
108
- // src/cli/lifecycle/api.ts
109
- var REQUEST_TIMEOUT_MS = 15e3;
110
- var MAX_RESPONSE_BYTES = 64 * 1024;
111
- var MAX_ENDPOINT_CHARS = 256;
112
- var REQUEST_ID_RE = /^[A-Za-z0-9_-]{43}$/;
113
- var PROJECT_KEY_RE = /^pyx_proj_[A-Za-z0-9_-]{16,160}$/;
114
- var ERROR_MESSAGES = {
115
- busy: { message: "Another pyx-mem sign-in or sign-out is in progress.", retryable: true },
116
- cleanup_pending: {
117
- message: "Cleanup of an earlier sign-in or sign-out is still pending. Run: pyx-mem login-recovery",
118
- retryable: true
119
- },
120
- invalid_endpoint: {
121
- message: "Endpoint must be an https URL (http only for loopback) without credentials, query or fragment.",
122
- retryable: false
123
- },
124
- untrusted_authorization_url: {
125
- message: "The server returned a browser sign-in address that is not trusted for this endpoint.",
126
- retryable: false
127
- },
128
- browser_login_unsupported: {
129
- message: "This endpoint does not support browser sign-in. Use: pyx-mem login --api-key",
130
- retryable: false
131
- },
132
- network_error: { message: "Could not reach the pyx-memory server.", retryable: true },
133
- rate_limited: { message: "Too many sign-in attempts. Wait a minute and retry.", retryable: true },
134
- expired: { message: "The sign-in request expired before it was completed.", retryable: true },
135
- project_not_eligible: {
136
- message: "That project can no longer be connected from this account.",
137
- retryable: true
138
- },
139
- credential_changed: {
140
- message: "The stored pyx-memory credential was changed by another program. Nothing was overwritten.",
141
- retryable: true
142
- },
143
- lock_lost: {
144
- message: "The sign-in was interrupted. Run: pyx-mem login-recovery",
145
- retryable: true
146
- },
147
- credential_store_unavailable: {
148
- message: "The OS credential store is unavailable.",
149
- retryable: false
150
- },
151
- journal_invalid: {
152
- message: 'The pending-cleanup record in the OS credential store is unreadable. Nothing was deleted, and pyx-memory stays blocked on this computer. To reset: revoke unexpected "Pyx CLI" keys in the dashboard, then run: pyx-mem login-recovery --discard-unreadable-journal (this signs this computer out).',
153
- retryable: false
154
- },
155
- login_failed: { message: "Browser sign-in failed.", retryable: true }
156
- };
157
- var SERVER_CODES = {
158
- rate_limited: "rate_limited",
159
- cli_login_expired_or_unknown: "expired",
160
- cli_login_project_not_eligible: "project_not_eligible",
161
- cli_login_cancelled: "cancelled",
162
- cli_login_denied: "cancelled"
163
- };
164
- var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
165
- var BROWSER_ORIGINS = {
166
- "https://memory.api.pyxmate.com": "https://memory.pyxmate.com"
167
- };
168
- function parseSafeUrl(input) {
169
- let url;
170
- try {
171
- url = new URL(input);
172
- } catch {
173
- return null;
174
- }
175
- const secure = url.protocol === "https:" || url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
176
- if (!secure || url.username !== "" || url.password !== "" || url.hash !== "") return null;
177
- return url;
178
- }
179
- function parseEndpoint(input) {
180
- const url = parseSafeUrl(input);
181
- if (url === null || url.search !== "" || input.includes("?") || input.includes("#")) return null;
182
- const normalized = url.href.replace(/\/+$/, "");
183
- return normalized.length <= MAX_ENDPOINT_CHARS ? normalized : null;
184
- }
185
- function boundLabel(value, maxChars) {
186
- let label2 = value.replace(/\p{Cc}/gu, "").slice(0, maxChars);
187
- while (JSON.stringify(label2).length - 2 > maxChars) label2 = label2.slice(0, -1);
188
- return label2;
189
- }
190
- function isTrustedAuthorizationUrl(authorizationUrl, endpoint, requestId) {
191
- const url = parseSafeUrl(authorizationUrl);
192
- if (url === null) return false;
193
- const api = new URL(endpoint);
194
- const trustedOrigin = LOOPBACK_HOSTS.has(api.hostname) ? LOOPBACK_HOSTS.has(url.hostname) : url.origin === (BROWSER_ORIGINS[api.origin] ?? api.origin);
195
- const params = [...url.searchParams];
196
- return trustedOrigin && url.pathname === "/cli/authorize" && params.length === 1 && params[0]?.[0] === "request" && params[0][1] === requestId;
197
- }
198
- async function readBounded(res) {
199
- const reader = res.body?.getReader();
200
- if (!reader) return void 0;
201
- const chunks = [];
202
- let size = 0;
203
- for (; ; ) {
204
- const { done, value } = await reader.read();
205
- if (done) break;
206
- size += value.byteLength;
207
- if (size > MAX_RESPONSE_BYTES) {
208
- await reader.cancel();
209
- return void 0;
210
- }
211
- chunks.push(value);
212
- }
213
- try {
214
- return JSON.parse(Buffer.concat(chunks).toString("utf8"));
215
- } catch {
216
- return void 0;
217
- }
218
- }
219
- function field(json, key) {
220
- return typeof json === "object" && json !== null ? json[key] : void 0;
221
- }
222
- function failure(status, json) {
223
- const serverCode = field(field(json, "error"), "code");
224
- const mapped = typeof serverCode === "string" ? SERVER_CODES[serverCode] : void 0;
225
- return { ok: false, code: mapped ?? (status === 429 ? "rate_limited" : "login_failed") };
226
- }
227
- function createCliLoginApi(fetchImpl = fetch) {
228
- async function post(url, body, bearer, signal) {
229
- const ctl = new AbortController();
230
- const abort = () => ctl.abort();
231
- const timer = setTimeout(abort, REQUEST_TIMEOUT_MS);
232
- signal?.addEventListener("abort", abort);
233
- try {
234
- if (signal?.aborted) return null;
235
- const res = await fetchImpl(url, {
236
- method: "POST",
237
- headers: {
238
- Accept: "application/json",
239
- "Content-Type": "application/json",
240
- ...bearer === void 0 ? {} : { Authorization: `Bearer ${bearer}` }
241
- },
242
- body: JSON.stringify(body),
243
- // A redirect would forward the proof or the key to another origin.
244
- redirect: "error",
245
- signal: ctl.signal
246
- });
247
- return { status: res.status, json: await readBounded(res) };
248
- } catch {
249
- return null;
250
- } finally {
251
- clearTimeout(timer);
252
- signal?.removeEventListener("abort", abort);
253
- }
254
- }
255
- const base = (endpoint) => `${endpoint}/auth/cli-login`;
256
- return {
257
- async start(endpoint, body, signal) {
258
- const res = await post(`${base(endpoint)}/start`, body, void 0, signal);
259
- if (res === null) return { ok: false, code: "network_error" };
260
- if (res.status === 404) return { ok: false, code: "browser_login_unsupported" };
261
- if (res.status !== 201) return failure(res.status, res.json);
262
- const requestId = field(res.json, "requestId");
263
- const authorizationUrl = field(res.json, "authorizationUrl");
264
- const expiresAt = new Date(String(field(res.json, "expiresAt")));
265
- if (typeof requestId !== "string" || !REQUEST_ID_RE.test(requestId) || typeof authorizationUrl !== "string" || Number.isNaN(expiresAt.getTime())) {
266
- return { ok: false, code: "login_failed" };
267
- }
268
- if (!isTrustedAuthorizationUrl(authorizationUrl, endpoint, requestId)) {
269
- return { ok: false, code: "untrusted_authorization_url" };
270
- }
271
- return { ok: true, requestId, authorizationUrl, expiresAt };
272
- },
273
- async redeem(login, code, signal) {
274
- const res = await post(
275
- `${base(login.endpoint)}/${login.requestId}/redeem`,
276
- { code, codeVerifier: login.codeVerifier },
277
- void 0,
278
- signal
279
- );
280
- if (res === null) return { ok: false, code: "network_error" };
281
- if (res.status !== 200) return failure(res.status, res.json);
282
- const credential = field(res.json, "credential");
283
- const apiKey = field(credential, "apiKey");
284
- const connection = field(credential, "connection");
285
- const accountLabel = field(connection, "accountLabel");
286
- const projectId = field(connection, "projectId");
287
- const projectName = field(connection, "projectName");
288
- if (typeof apiKey !== "string" || !PROJECT_KEY_RE.test(apiKey) || typeof accountLabel !== "string" || typeof projectId !== "string" || typeof projectName !== "string") {
289
- return { ok: false, code: "login_failed" };
290
- }
291
- return {
292
- ok: true,
293
- apiKey,
294
- // Display metadata only; bounded so the shared record fits every OS store.
295
- connection: {
296
- accountLabel: boundLabel(accountLabel, 254),
297
- projectId: boundLabel(projectId, 64),
298
- projectName: boundLabel(projectName, 128)
299
- }
300
- };
301
- },
302
- async confirm(login, signal) {
303
- const res = await post(
304
- `${base(login.endpoint)}/${login.requestId}/confirm`,
305
- { codeVerifier: login.codeVerifier },
306
- login.apiKey,
307
- signal
308
- );
309
- if (res === null) return { ok: false, code: "network_error" };
310
- if (res.status !== 200 || field(res.json, "status") !== "confirmed") {
311
- return failure(res.status, res.json);
312
- }
313
- return { ok: true };
314
- },
315
- async cancel(login) {
316
- const res = await post(`${base(login.endpoint)}/${login.requestId}/cancel`, {
317
- codeVerifier: login.codeVerifier
318
- });
319
- if (res === null) return false;
320
- if (res.status === 200) return field(res.json, "status") === "cancelled";
321
- return res.status === 404 && field(field(res.json, "error"), "code") === "cli_login_expired_or_unknown";
322
- },
323
- async revoke(key) {
324
- const res = await post(`${base(key.endpoint)}/revoke`, {}, key.apiKey);
325
- return res !== null && res.status === 200 && field(res.json, "status") === "revoked";
326
- }
327
- };
328
- }
329
-
330
- // src/cli/lifecycle/journal.ts
331
- var MAX_STORE_CHARS = 1200;
332
- var MAX_REVOKE_ENTRIES = 8;
333
- var TOKEN_RE = /^[A-Za-z0-9_-]{43}$/;
334
- var FINGERPRINT_RE = /^[0-9a-f]{16}$/;
335
- var PLACEHOLDER_KEY = `pyx_proj_${"x".repeat(160)}`;
336
- var SERVER_CONVERGENCE_SECONDS = 30;
337
- var JournalInvalidError = class extends Error {
338
- };
339
- var JournalFullError = class extends Error {
340
- };
341
- function credentialFingerprint(endpoint, apiKey) {
342
- return createHash("sha256").update(`${endpoint}
343
- ${apiKey}`).digest("hex").slice(0, 16);
344
- }
345
- function isEmpty(journal) {
346
- return journal.login === null && journal.logout === null && journal.revoke.length === 0;
347
- }
348
- function emptyJournal() {
349
- return { schemaVersion: 1, login: null, logout: null, revoke: [] };
350
- }
351
- function convergenceSeconds(journal, now = Date.now()) {
352
- if (journal.revokedAt === void 0) return null;
353
- const elapsed = Math.max(now - journal.revokedAt, 0);
354
- const left = Math.ceil(SERVER_CONVERGENCE_SECONDS - elapsed / 1e3);
355
- return left > 0 ? left : null;
356
- }
357
- function isExactObject(value, required, optional = []) {
358
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
359
- const keys = Object.keys(value);
360
- return required.every((key) => keys.includes(key)) && keys.every((key) => required.includes(key) || optional.includes(key));
361
- }
362
- function isKeyRef(value) {
363
- if (!isExactObject(value, ["endpoint", "apiKey"])) return false;
364
- const v = value;
365
- return typeof v.endpoint === "string" && parseEndpoint(v.endpoint) === v.endpoint && typeof v.apiKey === "string" && PROJECT_KEY_RE.test(v.apiKey);
366
- }
367
- function isLoginRecord(value) {
368
- if (!isExactObject(
369
- value,
370
- ["phase", "endpoint", "requestId", "codeVerifier", "expiresAt"],
371
- ["apiKey", "previous"]
372
- )) {
373
- return false;
374
- }
375
- const v = value;
376
- const held = v.phase === "issued" || v.phase === "committing";
377
- return (held || v.phase === "started") && typeof v.requestId === "string" && TOKEN_RE.test(v.requestId) && typeof v.codeVerifier === "string" && TOKEN_RE.test(v.codeVerifier) && Number.isSafeInteger(v.expiresAt) && (held ? isKeyRef({ endpoint: v.endpoint, apiKey: v.apiKey }) : v.apiKey === void 0 && isKeyRef({ endpoint: v.endpoint, apiKey: PLACEHOLDER_KEY })) && (v.previous === void 0 || v.phase === "committing" && isKeyRef(v.previous));
378
- }
379
- function isLogoutRecord(value) {
380
- if (!isExactObject(value, ["fingerprint", "revoke"])) return false;
381
- const v = value;
382
- return typeof v.fingerprint === "string" && FINGERPRINT_RE.test(v.fingerprint) && (v.revoke === null || isKeyRef(v.revoke));
383
- }
384
- function isJournal(value) {
385
- if (!isExactObject(value, ["schemaVersion", "login", "logout", "revoke"], ["revokedAt"])) {
386
- return false;
387
- }
388
- const v = value;
389
- return v.schemaVersion === 1 && (v.login === null || isLoginRecord(v.login)) && (v.logout === null || isLogoutRecord(v.logout)) && Array.isArray(v.revoke) && v.revoke.length <= MAX_REVOKE_ENTRIES && v.revoke.every(isKeyRef) && (v.revokedAt === void 0 || Number.isSafeInteger(v.revokedAt) && v.revokedAt >= 0);
390
- }
391
- function hasRoomForLogin(journal, endpoint, previous) {
392
- const largest = {
393
- ...journal,
394
- login: {
395
- phase: "committing",
396
- endpoint,
397
- requestId: "x".repeat(43),
398
- codeVerifier: "x".repeat(43),
399
- expiresAt: Number.MAX_SAFE_INTEGER,
400
- apiKey: PLACEHOLDER_KEY,
401
- ...previous ? { previous } : {}
402
- },
403
- revokedAt: Number.MAX_SAFE_INTEGER
404
- };
405
- return JSON.stringify(largest).length <= MAX_STORE_CHARS;
406
- }
407
- async function loadJournal(provider) {
408
- const raw = await provider.readJournal();
409
- if (raw === null) return emptyJournal();
410
- if (raw.length > MAX_STORE_CHARS) throw new JournalInvalidError();
411
- let parsed;
412
- try {
413
- parsed = JSON.parse(raw);
414
- } catch {
415
- throw new JournalInvalidError();
416
- }
417
- if (!isJournal(parsed)) throw new JournalInvalidError();
418
- return parsed;
419
- }
420
- async function saveJournal(provider, journal) {
421
- if (isEmpty(journal) && convergenceSeconds(journal) === null) return provider.clearJournal();
422
- const raw = JSON.stringify(journal);
423
- if (raw.length > MAX_STORE_CHARS) throw new JournalFullError();
424
- if (!isJournal(JSON.parse(raw))) throw new Error("refusing to write an invalid journal");
425
- await provider.writeJournal(raw);
426
- }
427
- async function supersedeLogout(provider, endpoint, apiKey) {
428
- let journal;
429
- try {
430
- journal = await loadJournal(provider);
431
- } catch (err) {
432
- if (err instanceof JournalInvalidError) return;
433
- throw err;
434
- }
435
- if (journal.logout?.fingerprint !== credentialFingerprint(endpoint, apiKey)) return;
436
- journal.logout = null;
437
- await saveJournal(provider, journal);
438
- }
439
- function guardKeychain(provider) {
440
- return {
441
- write: (credentials) => provider.write(credentials),
442
- delete: () => provider.delete(),
443
- check: () => provider.check(),
444
- readJournal: () => provider.readJournal(),
445
- writeJournal: (raw) => provider.writeJournal(raw),
446
- clearJournal: () => provider.clearJournal(),
447
- acquireLock: (onLost) => provider.acquireLock(onLost),
448
- isLocked: () => provider.isLocked(),
449
- async read() {
450
- const credentials = await provider.read();
451
- if (credentials === null) return null;
452
- let blocked;
453
- try {
454
- const { logout } = await loadJournal(provider);
455
- blocked = logout?.fingerprint === credentialFingerprint(credentials.endpoint, credentials.apiKey);
456
- } catch (err) {
457
- if (!(err instanceof JournalInvalidError)) throw err;
458
- blocked = true;
459
- }
460
- if (blocked) {
461
- throw new KeychainError({
462
- code: "logout-incomplete",
463
- message: "A pyx-memory sign-out did not finish on this computer.",
464
- guidance: "Run: pyx-mem login-recovery",
465
- exit: EXIT.NOT_LOGGED_IN
466
- });
467
- }
468
- return credentials;
469
- }
470
- };
471
- }
472
-
473
- // src/cli/keychain/napi-provider.ts
474
- import { homedir } from "os";
475
- import { join } from "path";
476
-
477
- // src/cli/lifecycle/lock.ts
478
- import { mkdir } from "fs/promises";
479
- import { dirname } from "path";
480
- import lockfile from "proper-lockfile";
481
- var LOCK_OPTIONS = { realpath: false, stale: 15e3 };
482
- function createFileLock(file) {
483
- return {
484
- async acquire(onLost) {
485
- await mkdir(dirname(file), { recursive: true, mode: 448 });
486
- let release;
487
- try {
488
- release = await lockfile.lock(file, { ...LOCK_OPTIONS, onCompromised: () => onLost() });
489
- } catch (err) {
490
- if (err.code === "ELOCKED") return null;
491
- throw err;
492
- }
493
- return async () => {
494
- try {
495
- await release();
496
- } catch (err) {
497
- const code = err.code;
498
- if (code !== "ERELEASED" && code !== "ENOTACQUIRED") throw err;
499
- }
500
- };
501
- },
502
- isLocked: () => lockfile.check(file, LOCK_OPTIONS)
503
- };
504
- }
505
- var BUSY = /* @__PURE__ */ Symbol("lifecycle-lock-busy");
506
- var LockLostError = class extends Error {
507
- };
508
- async function releaseLifecycleLock(release) {
509
- try {
510
- await release();
511
- } catch (err) {
512
- const code = err?.code;
513
- const reason = typeof code === "string" && /^[A-Z_]{1,32}$/.test(code) ? code : "unknown";
514
- process.stderr.write(
515
- `Warning: the pyx-mem lifecycle lock could not be released (${reason}). The reported result stands; other pyx-mem sign-in or sign-out commands may answer busy for up to ${LOCK_OPTIONS.stale / 1e3} seconds.
516
- `
517
- );
518
- }
519
- }
520
- async function withLifecycleLock(provider, fn) {
521
- let lost = false;
522
- const release = await provider.acquireLock(() => {
523
- lost = true;
524
- });
525
- if (release === null) return BUSY;
526
- try {
527
- return await fn(() => lost);
528
- } finally {
529
- await releaseLifecycleLock(release);
530
- }
531
- }
532
-
533
- // src/cli/keychain/types.ts
534
- var KEYCHAIN_SERVICE = "pyx-memory";
535
- var KEYCHAIN_ACCOUNT = "default";
536
- var CREDENTIAL_SCHEMA_VERSION = 1;
537
- var JOURNAL_ACCOUNT = "lifecycle-journal";
538
-
539
- // src/cli/keychain/napi-provider.ts
540
- var cached = /* @__PURE__ */ new Map();
541
- async function loadEntry(account = KEYCHAIN_ACCOUNT) {
542
- const hit = cached.get(account);
543
- if (hit) return hit;
544
- try {
545
- const mod = await import("@napi-rs/keyring");
546
- const entry = new mod.Entry(KEYCHAIN_SERVICE, account);
547
- cached.set(account, entry);
548
- return entry;
549
- } catch (err) {
550
- throw providerImportError(err);
551
- }
552
- }
553
- function parseRecord(raw) {
554
- let parsed;
555
- try {
556
- parsed = JSON.parse(raw);
557
- } catch (err) {
558
- throw recordInvalidError(err);
559
- }
560
- if (!isValidRecord(parsed)) throw recordInvalidError();
561
- return parsed;
562
- }
563
- function isValidRecord(value) {
564
- if (typeof value !== "object" || value === null) return false;
565
- const v = value;
566
- return v.schemaVersion === CREDENTIAL_SCHEMA_VERSION && typeof v.endpoint === "string" && v.endpoint.length > 0 && typeof v.apiKey === "string" && v.apiKey.length > 0 && (v.provenance === void 0 || v.provenance === "browser-login" && isConnection(v.connection));
567
- }
568
- function isConnection(value) {
569
- if (typeof value !== "object" || value === null) return false;
570
- const c = value;
571
- return typeof c.accountLabel === "string" && typeof c.projectId === "string" && typeof c.projectName === "string";
572
- }
573
- var NapiKeychainProvider = class {
574
- // The lock file holds no secrets; credentials and the journal stay in the OS store.
575
- lock = createFileLock(join(homedir(), ".pyx-memory", "lifecycle"));
576
- async read() {
577
- const entry = await loadEntry();
578
- let raw;
579
- try {
580
- raw = entry.getPassword();
581
- } catch (err) {
582
- throw platformAccessError(err);
583
- }
584
- if (raw === null) return null;
585
- return parseRecord(raw);
586
- }
587
- async write(credentials) {
588
- const entry = await loadEntry();
589
- const payload = JSON.stringify({
590
- schemaVersion: CREDENTIAL_SCHEMA_VERSION,
591
- endpoint: credentials.endpoint,
592
- apiKey: credentials.apiKey,
593
- ...credentials.provenance === "browser-login" ? { connection: credentials.connection, provenance: credentials.provenance } : {}
594
- });
595
- try {
596
- entry.setPassword(payload);
597
- } catch (err) {
598
- throw platformAccessError(err);
599
- }
600
- }
601
- async delete() {
602
- const entry = await loadEntry();
603
- try {
604
- entry.deletePassword();
605
- } catch (err) {
606
- throw platformAccessError(err);
607
- }
608
- }
609
- async readJournal() {
610
- const entry = await loadEntry(JOURNAL_ACCOUNT);
611
- try {
612
- return entry.getPassword();
613
- } catch (err) {
614
- throw platformAccessError(err);
615
- }
616
- }
617
- async writeJournal(raw) {
618
- const entry = await loadEntry(JOURNAL_ACCOUNT);
619
- try {
620
- entry.setPassword(raw);
621
- } catch (err) {
622
- throw platformAccessError(err);
623
- }
624
- }
625
- async clearJournal() {
626
- const entry = await loadEntry(JOURNAL_ACCOUNT);
627
- try {
628
- entry.deletePassword();
629
- } catch (err) {
630
- throw platformAccessError(err);
631
- }
632
- }
633
- acquireLock(onLost) {
634
- return this.lock.acquire(onLost);
635
- }
636
- isLocked() {
637
- return this.lock.isLocked();
638
- }
639
- async check() {
640
- try {
641
- const entry = await loadEntry();
642
- entry.getPassword();
643
- return { ok: true, message: "OS credential store reachable" };
644
- } catch (err) {
645
- if (err instanceof Error && "code" in err && "guidance" in err) {
646
- const ke = err;
647
- return { ok: false, code: ke.code, message: ke.message, guidance: ke.guidance };
648
- }
649
- return { ok: false, message: err instanceof Error ? err.message : String(err) };
650
- }
651
- }
652
- };
653
-
654
- // src/cli/keychain/index.ts
655
- function getDefaultKeychain() {
656
- return guardKeychain(new NapiKeychainProvider());
657
- }
658
- function getLifecycleKeychain() {
659
- return new NapiKeychainProvider();
660
- }
661
-
662
- // src/cli/probe.ts
663
- import { randomUUID } from "crypto";
664
- var PROBE_PATH = "/api/memory/entries?limit=1";
665
- var PROBE_TIMEOUT_MS = 8e3;
666
- async function probeCredentials(args) {
667
- const fetcher = args.fetcher ?? fetch;
668
- const url = `${args.endpoint}${PROBE_PATH}`;
669
- const ctl = new AbortController();
670
- const timer = setTimeout(() => ctl.abort(), PROBE_TIMEOUT_MS);
671
- const idempotencyKey = `cli-probe-${randomUUID()}`;
672
- try {
673
- const res = await fetcher(url, {
674
- method: "GET",
675
- headers: {
676
- Authorization: `Bearer ${args.apiKey}`,
677
- Accept: "application/json",
678
- "Idempotency-Key": idempotencyKey
679
- },
680
- signal: ctl.signal
681
- });
682
- if (res.status === 401 || res.status === 403) return { kind: "invalid", status: res.status };
683
- if (res.ok) return { kind: "ok", status: res.status };
684
- return { kind: "unreachable", reason: `HTTP ${res.status}` };
685
- } catch (err) {
686
- const msg = err instanceof Error ? err.message : String(err);
687
- return { kind: "unreachable", reason: msg };
688
- } finally {
689
- clearTimeout(timer);
690
- }
691
- }
692
-
693
- // src/cli/commands/doctor.ts
694
- async function doctorCommand(opts = {}) {
695
- const provider = opts.keychain ?? getDefaultKeychain();
696
- const fetcher = opts.fetchImpl ?? fetch;
697
- const checks = [];
698
- const accessCheck = await provider.check();
699
- if (accessCheck.ok) {
700
- checks.push({ id: "provider.import", status: "pass", message: "native keyring module loaded" });
701
- checks.push({ id: "keychain.access", status: "pass", message: accessCheck.message });
702
- } else if (accessCheck.code === "provider-import-failed") {
703
- checks.push({
704
- id: "provider.import",
705
- status: "fail",
706
- code: accessCheck.code,
707
- message: accessCheck.message,
708
- guidance: accessCheck.guidance
709
- });
710
- checks.push({
711
- id: "keychain.access",
712
- status: "skip",
713
- message: "skipped \u2014 provider import failed"
714
- });
715
- } else {
716
- checks.push({ id: "provider.import", status: "pass", message: "native keyring module loaded" });
717
- checks.push({
718
- id: "keychain.access",
719
- status: "fail",
720
- code: accessCheck.code,
721
- message: accessCheck.message,
722
- guidance: accessCheck.guidance
723
- });
724
- }
725
- const accessOk = accessCheck.ok;
726
- let creds = null;
727
- if (accessOk) {
728
- try {
729
- creds = await provider.read();
730
- if (creds) {
731
- checks.push({
732
- id: "credentials.present",
733
- status: "pass",
734
- message: "credentials present in keychain",
735
- endpoint: creds.endpoint
736
- });
737
- } else {
738
- checks.push({
739
- id: "credentials.present",
740
- status: "fail",
741
- code: "not-logged-in",
742
- message: "no pyx-memory credentials stored",
743
- guidance: "Run: pyx-mem login"
744
- });
745
- }
746
- } catch (err) {
747
- if (err instanceof KeychainError) {
748
- checks.push({
749
- id: "credentials.present",
750
- status: "fail",
751
- code: err.code,
752
- message: err.message,
753
- guidance: err.guidance
754
- });
755
- } else {
756
- throw err;
757
- }
758
- }
759
- } else {
760
- checks.push({
761
- id: "credentials.present",
762
- status: "skip",
763
- message: "skipped \u2014 keychain access failed"
764
- });
765
- }
766
- if (creds) {
767
- const probe = await probeCredentials({
768
- endpoint: creds.endpoint,
769
- apiKey: creds.apiKey,
770
- fetcher
771
- });
772
- if (probe.kind === "ok") {
773
- checks.push({
774
- id: "backend.reachable",
775
- status: "pass",
776
- message: `pyx-memory API reachable (HTTP ${probe.status})`,
777
- endpoint: creds.endpoint
778
- });
779
- } else if (probe.kind === "invalid") {
780
- checks.push({
781
- id: "backend.reachable",
782
- status: "fail",
783
- code: "key-rejected",
784
- message: `API key rejected (HTTP ${probe.status})`,
785
- endpoint: creds.endpoint,
786
- guidance: "Re-login with a valid key: pyx-mem login"
787
- });
788
- } else {
789
- checks.push({
790
- id: "backend.reachable",
791
- status: "fail",
792
- code: "unreachable",
793
- message: `no pyx-memory API at this endpoint (${probe.reason})`,
794
- endpoint: creds.endpoint,
795
- guidance: "Verify the endpoint is the API backend, not a web UI: pyx-mem login --endpoint <API_URL>"
796
- });
797
- }
798
- } else {
799
- checks.push({
800
- id: "backend.reachable",
801
- status: "skip",
802
- message: "skipped \u2014 no credentials to probe"
803
- });
804
- }
805
- const ok = checks.every((c) => c.status !== "fail");
806
- if (opts.json) {
807
- process.stdout.write(`${JSON.stringify({ ok, checks })}
808
- `);
809
- } else {
810
- for (const c of checks) {
811
- const tag = c.status === "pass" ? "OK " : c.status === "skip" ? "SKIP" : "FAIL";
812
- process.stdout.write(`[${tag}] ${c.id}: ${c.message}
813
- `);
814
- if (c.endpoint) process.stdout.write(` endpoint: ${c.endpoint}
815
- `);
816
- if (c.status === "fail" && c.guidance) process.stdout.write(` ${c.guidance}
817
- `);
818
- }
819
- }
820
- return ok ? EXIT.OK : EXIT.DOCTOR_FAIL;
821
- }
822
-
823
- // src/cli/commands/insights.ts
824
- import { z } from "zod";
825
-
826
- // src/cli/commands/read-surface.ts
827
- import { randomUUID as randomUUID2 } from "crypto";
828
-
829
- // src/mcp/errors.ts
830
- var RUN_LOGIN = "Run: pyx-mem login";
831
- var RUN_DOCTOR = "Run: pyx-mem doctor";
832
- function mcpText(text2, isError = false) {
833
- return { isError, content: [{ type: "text", text: text2 }] };
834
- }
835
- function mcpMissingCredentials() {
836
- return mcpText(`pyx-memory credentials are not configured.
837
- ${RUN_LOGIN}`, true);
838
- }
839
- function mcpKeychainError(message, guidance) {
840
- const lines = ["pyx-memory credentials could not be read from the OS credential store."];
841
- if (message) lines.push(message);
842
- if (guidance) lines.push(guidance);
843
- lines.push(RUN_LOGIN);
844
- return mcpText(lines.join("\n"), true);
845
- }
846
- function mcpAuthFailed(status) {
847
- return mcpText(`Authentication failed (HTTP ${status}).
848
- ${RUN_LOGIN}`, true);
849
- }
850
- function mcpUnreachable(endpoint, reason) {
851
- return mcpText(`Unable to reach pyx-memory at ${endpoint}: ${reason}.
852
- ${RUN_DOCTOR}`, true);
853
- }
854
- function mcpHttpError(status, body) {
855
- return mcpText(`${status}: ${body}`, true);
856
- }
857
- function mcpServerError(status, body) {
858
- return mcpText(`pyx-memory server error (HTTP ${status}): ${body || "no body"}`, true);
859
- }
860
- function mcpEnvelopeError(message) {
861
- return mcpText(message, true);
862
- }
863
- function mcpJson(payload) {
864
- return mcpText(JSON.stringify(payload));
865
- }
866
-
867
- // src/mcp/http-client.ts
868
- var DEFAULT_TIMEOUT_MS = 15e3;
869
- function parseRetryAfterSeconds(header) {
870
- const value = header?.trim() ?? "";
871
- return /^\d+$/.test(value) ? Number(value) : void 0;
872
- }
873
- var HTTP_DATE = /^(?:[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT|[A-Z][a-z]{5,8}, \d{2}-[A-Z][a-z]{2}-\d{2} \d{2}:\d{2}:\d{2} GMT|[A-Z][a-z]{2} [A-Z][a-z]{2} [ \d]\d \d{2}:\d{2}:\d{2} \d{4})$/;
874
- function parseRetryAfterMs(header) {
875
- const seconds = parseRetryAfterSeconds(header);
876
- if (seconds !== void 0) return seconds * 1e3;
877
- const value = header?.trim() ?? "";
878
- if (!HTTP_DATE.test(value)) return void 0;
879
- const at = Date.parse(value.endsWith(" GMT") ? value : `${value} GMT`);
880
- return Number.isNaN(at) ? void 0 : Math.max(0, at - Date.now());
881
- }
882
- function validateIdempotencyKey(key) {
883
- if (key.length === 0 || !key.isWellFormed() || new TextEncoder().encode(key).byteLength > 256) {
884
- throw new Error("idempotencyKey must be 1-256 well-formed UTF-8 bytes");
885
- }
886
- return key;
887
- }
888
- function createHttpClient(credentials, fetchImpl = fetch) {
889
- const baseUrl = credentials.endpoint.replace(/\/+$/, "");
890
- function buildUrl(path, query) {
891
- const url = new URL(`${baseUrl}${path}`);
892
- if (query) {
893
- for (const [k, v] of Object.entries(query)) {
894
- if (v === void 0) continue;
895
- url.searchParams.set(k, String(v));
896
- }
897
- }
898
- return url.toString();
899
- }
900
- function buildHeaders(scope, contentType, idempotencyKey) {
901
- const headers = new Headers();
902
- headers.set("Authorization", `Bearer ${credentials.apiKey}`);
903
- headers.set("Accept", "application/json");
904
- if (contentType) headers.set("Content-Type", contentType);
905
- if (scope?.tenantId) headers.set("X-Tenant-Id", scope.tenantId);
906
- if (scope?.namespaceId) headers.set("X-Namespace-Id", scope.namespaceId);
907
- if (scope?.userId) headers.set("X-User-Id", scope.userId);
908
- if (scope?.teamId) headers.set("X-Team-Id", scope.teamId);
909
- if (scope?.callerAccessLevel) headers.set("X-Caller-Access-Level", scope.callerAccessLevel);
910
- if (idempotencyKey !== void 0) {
911
- headers.set("Idempotency-Key", validateIdempotencyKey(idempotencyKey));
912
- }
913
- return headers;
914
- }
915
- async function send(url, method, headers, body, deadline = {}) {
916
- const { timeoutMs, signal } = deadline;
917
- const ctl = new AbortController();
918
- const timer = setTimeout(
919
- () => timeoutMs === void 0 ? ctl.abort() : ctl.abort(
920
- new DOMException(`no complete response within ${timeoutMs / 1e3} s`, "TimeoutError")
921
- ),
922
- timeoutMs ?? DEFAULT_TIMEOUT_MS
923
- );
924
- try {
925
- let res;
926
- try {
927
- res = await fetchImpl(url, {
928
- method,
929
- headers,
930
- body,
931
- signal: signal ? AbortSignal.any([ctl.signal, signal]) : ctl.signal
932
- });
933
- } catch (err) {
934
- return {
935
- ok: false,
936
- result: mcpUnreachable(baseUrl, err instanceof Error ? err.message : String(err))
937
- };
938
- } finally {
939
- if (timeoutMs === void 0) clearTimeout(timer);
940
- }
941
- return await readResponse(res);
942
- } finally {
943
- clearTimeout(timer);
944
- }
945
- }
946
- return {
947
- async requestJson(input) {
948
- const url = buildUrl(input.path, input.query);
949
- const hasBody = input.body !== void 0;
950
- const headers = buildHeaders(
951
- input.scope,
952
- hasBody ? "application/json" : void 0,
953
- input.idempotencyKey
954
- );
955
- return send(
956
- url,
957
- input.method,
958
- headers,
959
- hasBody ? JSON.stringify(input.body) : void 0,
960
- input
961
- );
962
- },
963
- async requestMultipart(input) {
964
- const url = buildUrl(input.path);
965
- const headers = buildHeaders(input.scope, void 0, input.idempotencyKey);
966
- const form = await input.formData();
967
- return send(url, "POST", headers, form);
968
- },
969
- async requestBytes(input) {
970
- const headers = buildHeaders(input.scope, "application/octet-stream", input.idempotencyKey);
971
- for (const [name, value] of Object.entries(input.headers)) headers.set(name, value);
972
- return send(buildUrl(input.path), input.method, headers, input.body, input);
973
- }
974
- };
975
- }
976
- async function readResponse(res) {
977
- if (res.status === 401 || res.status === 403) {
978
- return { ok: false, status: res.status, result: mcpAuthFailed(res.status) };
979
- }
980
- let text2;
981
- try {
982
- text2 = await res.text();
983
- } catch (err) {
984
- const reason = err instanceof Error ? err.message : String(err);
985
- return {
986
- ok: false,
987
- status: res.status,
988
- result: mcpEnvelopeError(
989
- `Failed to read pyx-memory response (HTTP ${res.status}): ${reason}`
990
- )
991
- };
992
- }
993
- return parseHttpResponse(res, text2);
994
- }
995
- function parseHttpResponse(res, text2) {
996
- if (res.ok && res.headers.get("content-type")?.includes("application/x-ndjson")) {
997
- return parseNdjsonOutcome(res.status, text2);
998
- }
999
- const parsed = parseJsonText(text2);
1000
- const gateway = parsed.ok ? gatewayError(parsed.value) : void 0;
1001
- const failure2 = {
1002
- ok: false,
1003
- status: res.status,
1004
- code: parsed.ok ? extractEnvelopeCode(parsed.value) : void 0,
1005
- retryAfter: res.headers.get("retry-after") ?? void 0
1006
- };
1007
- if (res.status >= 500) {
1008
- return { ...failure2, result: mcpServerError(res.status, gateway?.message ?? text2) };
1009
- }
1010
- if (!parsed.ok) {
1011
- if (!res.ok) {
1012
- return { ...failure2, result: mcpHttpError(res.status, text2 || "no body") };
1013
- }
1014
- return { ok: true, status: res.status, data: text2 };
1015
- }
1016
- if (!res.ok) {
1017
- const message = extractEnvelopeError(parsed.value) ?? gateway?.message ?? text2 ?? `HTTP ${res.status}`;
1018
- return { ...failure2, result: mcpHttpError(res.status, message) };
1019
- }
1020
- const envelopeError = extractEnvelopeError(parsed.value);
1021
- if (envelopeError) {
1022
- return { ...failure2, result: mcpEnvelopeError(envelopeError) };
1023
- }
1024
- return { ok: true, status: res.status, data: parsed.value };
1025
- }
1026
- function parseNdjsonOutcome(status, text2) {
1027
- for (const line of text2.split("\n")) {
1028
- if (!line.trim()) continue;
1029
- let frame;
1030
- try {
1031
- frame = JSON.parse(line);
1032
- } catch (err) {
1033
- const reason = err instanceof Error ? err.message : String(err);
1034
- return {
1035
- ok: false,
1036
- status,
1037
- result: mcpEnvelopeError(`pyx-memory sent an invalid NDJSON line: ${reason}`)
1038
- };
1039
- }
1040
- if (frame?.type === "result") return { ok: true, status, data: frame };
1041
- if (frame?.type === "error") {
1042
- return { ok: false, status, result: mcpEnvelopeError(JSON.stringify(frame)) };
1043
- }
1044
- }
1045
- return {
1046
- ok: false,
1047
- status,
1048
- result: mcpEnvelopeError("File ingest stream ended without a server result")
1049
- };
1050
- }
1051
- function parseJsonText(text2) {
1052
- if (text2.length === 0) return { ok: true, value: void 0 };
1053
- try {
1054
- return { ok: true, value: JSON.parse(text2) };
1055
- } catch {
1056
- return { ok: false };
1057
- }
1058
- }
1059
- function gatewayError(parsed) {
1060
- if (parsed === null || typeof parsed !== "object") return void 0;
1061
- const error = parsed.error;
1062
- if (error === null || typeof error !== "object") return void 0;
1063
- const { code, message } = error;
1064
- return typeof code === "string" && typeof message === "string" ? { code, message } : void 0;
1065
- }
1066
- function extractEnvelopeCode(parsed) {
1067
- if (parsed === null || typeof parsed !== "object") return void 0;
1068
- const obj = parsed;
1069
- if (obj.success === false && typeof obj.code === "string") return obj.code;
1070
- return gatewayError(parsed)?.code;
1071
- }
1072
- function extractEnvelopeError(parsed) {
1073
- if (parsed === null || typeof parsed !== "object") return null;
1074
- const obj = parsed;
1075
- if (obj.success === false) {
1076
- if (typeof obj.error === "string") return obj.error;
1077
- if (typeof obj.message === "string") return obj.message;
1078
- return "pyx-memory returned success: false";
1079
- }
1080
- return null;
1081
- }
1082
-
1083
- // src/cli/commands/read-surface.ts
1084
- async function readSurface(opts) {
1085
- const provider = opts.keychain ?? getDefaultKeychain();
1086
- let credentials;
1087
- try {
1088
- credentials = await provider.read();
1089
- } catch (error) {
1090
- return {
1091
- ok: false,
1092
- exit: error instanceof KeychainError ? error.exit : EXIT.CRED_UNAVAILABLE,
1093
- state: "error",
1094
- endpoint: null,
1095
- keySource: null,
1096
- credentialFingerprint: null,
1097
- problem: error instanceof KeychainError ? [error.message, error.guidance].filter(Boolean).join(" ") : "The OS credential store could not be read."
1098
- };
1099
- }
1100
- if (!credentials) {
1101
- return {
1102
- ok: false,
1103
- exit: EXIT.NOT_LOGGED_IN,
1104
- state: "signed_out",
1105
- endpoint: null,
1106
- keySource: null,
1107
- credentialFingerprint: null,
1108
- problem: null
1109
- };
1110
- }
1111
- const fingerprint = credentialFingerprint(credentials.endpoint, credentials.apiKey);
1112
- if (!isHttpUrl(credentials.endpoint)) {
1113
- return {
1114
- ok: false,
1115
- exit: EXIT.DOCTOR_FAIL,
1116
- state: "error",
1117
- endpoint: null,
1118
- keySource: "keychain",
1119
- credentialFingerprint: fingerprint,
1120
- problem: "The stored pyx-memory endpoint is invalid. Run: pyx-mem login"
1121
- };
1122
- }
1123
- const http = createHttpClient(credentials, opts.fetchImpl ?? fetch);
1124
- const response = await http.requestJson({
1125
- ...opts.request,
1126
- idempotencyKey: `cli-${opts.surface}-${randomUUID2()}`
1127
- });
1128
- if (!response.ok) {
1129
- return failedSurfaceRequest(
1130
- opts.surface,
1131
- response.status,
1132
- credentials.endpoint,
1133
- fingerprint,
1134
- opts.minimumServiceVersion,
1135
- http
1136
- );
1137
- }
1138
- const parsed = opts.schema.safeParse(response.data);
1139
- if (!parsed.success) {
1140
- return {
1141
- ok: false,
1142
- exit: EXIT.DOCTOR_FAIL,
1143
- state: "error",
1144
- endpoint: credentials.endpoint,
1145
- keySource: "keychain",
1146
- credentialFingerprint: fingerprint,
1147
- problem: `pyx-memory returned an invalid ${opts.surface} response. Run: pyx-mem doctor`
1148
- };
1149
- }
1150
- return {
1151
- ok: true,
1152
- endpoint: credentials.endpoint,
1153
- credentialFingerprint: fingerprint,
1154
- data: parsed.data
1155
- };
1156
- }
1157
- async function failedSurfaceRequest(surface, status, endpoint, credentialFingerprint2, minimumServiceVersion, http) {
1158
- if (status === 401) {
1159
- return {
1160
- ok: false,
1161
- exit: EXIT.NOT_LOGGED_IN,
1162
- state: "signed_out",
1163
- endpoint,
1164
- keySource: "keychain",
1165
- credentialFingerprint: credentialFingerprint2,
1166
- problem: requestFailure(surface, status)
1167
- };
1168
- }
1169
- const serviceVersion = minimumServiceVersion ? await probeServiceVersion(http) : null;
1170
- const serviceOutdated = serviceVersion !== null && minimumServiceVersion !== void 0 && isVersionOlder(serviceVersion, minimumServiceVersion);
1171
- return {
1172
- ok: false,
1173
- exit: EXIT.DOCTOR_FAIL,
1174
- state: serviceOutdated ? "service_outdated" : "error",
1175
- endpoint,
1176
- keySource: "keychain",
1177
- credentialFingerprint: credentialFingerprint2,
1178
- problem: serviceOutdated ? `pyx-memory ${surface} requires service ${minimumServiceVersion} or newer; the connected service reports ${serviceVersion}. Update the hosted pyx-memory service or dashboard integration, then retry.` : requestFailure(surface, status)
1179
- };
1180
- }
1181
- async function probeServiceVersion(http) {
1182
- const response = await http.requestJson({ method: "GET", path: "/status" });
1183
- if (!response.ok) return null;
1184
- const payload = plainObject(response.data);
1185
- const topology = plainObject(payload?.success === true ? plainObject(payload.data) : payload);
1186
- const version = payload?.version ?? plainObject(topology?.service)?.version;
1187
- return typeof version === "string" && parseVersion(version) ? version : null;
1188
- }
1189
- function plainObject(value) {
1190
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
1191
- }
1192
- function isVersionOlder(actual, minimum) {
1193
- const left = parseVersion(actual);
1194
- const right = parseVersion(minimum);
1195
- if (!left || !right) return false;
1196
- if (left.core[0] !== right.core[0]) return left.core[0] < right.core[0];
1197
- if (left.core[1] !== right.core[1]) return left.core[1] < right.core[1];
1198
- if (left.core[2] !== right.core[2]) return left.core[2] < right.core[2];
1199
- return left.prerelease !== null && right.prerelease === null;
1200
- }
1201
- function parseVersion(value) {
1202
- if (value.length > 64) return null;
1203
- const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
1204
- if (!match) return null;
1205
- const core = [Number(match[1]), Number(match[2]), Number(match[3])];
1206
- return core.every(Number.isSafeInteger) ? { core, prerelease: match[4] ?? null } : null;
1207
- }
1208
- function requestFailure(surface, status) {
1209
- if (status === 401) {
1210
- return "Stored pyx-memory credentials were rejected. Run: pyx-mem login";
1211
- }
1212
- if (status === 403) {
1213
- return "pyx-memory denied access (HTTP 403). Check the key scope and project access.";
1214
- }
1215
- if (status === void 0) {
1216
- return "Unable to reach pyx-memory. Run: pyx-mem doctor";
1217
- }
1218
- if (status >= 500) {
1219
- return `pyx-memory is unavailable (HTTP ${status}). Run: pyx-mem doctor`;
1220
- }
1221
- return `pyx-memory ${surface} failed (HTTP ${status}). Run: pyx-mem doctor`;
1222
- }
1223
- function isHttpUrl(value) {
1224
- try {
1225
- const url = new URL(value);
1226
- return url.protocol === "http:" || url.protocol === "https:";
1227
- } catch {
1228
- return false;
1229
- }
1230
- }
1231
-
1232
- // src/cli/commands/insights.ts
1233
- var INSIGHTS_SCHEMA_VERSION = 1;
1234
- var metricsSchema = z.object({
1235
- activeEntries: z.number().int().nonnegative(),
1236
- retainedAdditions7d: z.number().int().nonnegative(),
1237
- appliedRecalls: z.number().int().nonnegative(),
1238
- reinforcedEntries: z.number().int().nonnegative(),
1239
- retrievedEntries: z.number().int().nonnegative(),
1240
- retrievals: z.number().int().nonnegative(),
1241
- lastAppliedAt: z.string().nullable(),
1242
- lastRetrievedAt: z.string().nullable()
1243
- });
1244
- var insightsSchema = z.object({
1245
- generatedAt: z.string(),
1246
- scope: z.literal("connected_memory"),
1247
- coverage: z.object({
1248
- retainedAdditions: z.object({
1249
- state: z.enum(["measured", "partial"]),
1250
- trackingSince: z.string().nullable(),
1251
- trackedEntries: z.number().int().nonnegative(),
1252
- totalEntries: z.number().int().nonnegative(),
1253
- reason: z.string().nullable()
1254
- }),
1255
- projectLabels: z.object({ state: z.literal("measured") }),
1256
- retrievalUsage: z.object({ state: z.literal("partial"), reason: z.string() }),
1257
- tokenEfficiency: z.object({ state: z.literal("not_instrumented"), reason: z.string() }),
1258
- contextAccuracy: z.object({ state: z.literal("not_instrumented"), reason: z.string() })
1259
- }),
1260
- summary: z.object({
1261
- activeEntries: z.number().int().nonnegative(),
1262
- observedProjectLabels: z.number().int().nonnegative(),
1263
- retainedAdditions7d: z.number().int().nonnegative().nullable(),
1264
- trackedEntries: z.number().int().nonnegative(),
1265
- appliedRecalls: z.number().int().nonnegative(),
1266
- reinforcedEntries: z.number().int().nonnegative(),
1267
- retrievedEntries: z.number().int().nonnegative(),
1268
- retrievals: z.number().int().nonnegative()
1269
- }),
1270
- projects: z.array(
1271
- metricsSchema.extend({
1272
- label: z.string().min(1).max(MEMORY_PROJECT_LABEL_MAX_CHARS)
1273
- })
1274
- ).max(TAXONOMY_MAX_PROJECTS),
1275
- other: metricsSchema.nullable(),
1276
- recentApplied: z.array(
1277
- z.object({
1278
- topic: z.string().min(1).max(MEMORY_PROJECT_LABEL_MAX_CHARS).nullable(),
1279
- project: z.string().min(1).max(MEMORY_PROJECT_LABEL_MAX_CHARS).nullable(),
1280
- at: z.string()
1281
- })
1282
- ).max(5)
1283
- });
1284
- var insightsResponseSchema = z.union([
1285
- insightsSchema,
1286
- z.object({ success: z.literal(true), data: insightsSchema }).transform(({ data }) => data)
1287
- ]);
1288
- async function insightsCommand(opts = {}) {
1289
- const result = await readSurface({
1290
- surface: "insights",
1291
- request: { method: "GET", path: "/api/memory/insights" },
1292
- schema: insightsResponseSchema,
1293
- minimumServiceVersion: "1.19.0",
1294
- keychain: opts.keychain,
1295
- fetchImpl: opts.fetchImpl
1296
- });
1297
- const withIdentity = opts.withIdentity === true;
1298
- if (!result.ok) {
1299
- emit(
1300
- report(result.state, result.endpoint, result.keySource, result.problem, {
1301
- withIdentity,
1302
- credentialFingerprint: result.credentialFingerprint
1303
- })
1304
- );
1305
- return result.exit;
1306
- }
1307
- emit({
1308
- schemaVersion: INSIGHTS_SCHEMA_VERSION,
1309
- state: "ready",
1310
- endpoint: result.endpoint,
1311
- keySource: "keychain",
1312
- ...identityField({ withIdentity, credentialFingerprint: result.credentialFingerprint }),
1313
- ...result.data,
1314
- problem: null
1315
- });
1316
- return EXIT.OK;
1317
- }
1318
- function identityField(identity) {
1319
- return identity.withIdentity ? { credentialFingerprint: identity.credentialFingerprint } : {};
1320
- }
1321
- function report(state, endpoint, keySource, problem, identity) {
1322
- return {
1323
- schemaVersion: INSIGHTS_SCHEMA_VERSION,
1324
- state,
1325
- endpoint,
1326
- keySource,
1327
- ...identityField(identity),
1328
- generatedAt: null,
1329
- scope: "connected_memory",
1330
- coverage: null,
1331
- summary: null,
1332
- projects: [],
1333
- other: null,
1334
- recentApplied: [],
1335
- problem
1336
- };
1337
- }
1338
- function emit(value) {
1339
- process.stdout.write(`${JSON.stringify(value)}
1340
- `);
1341
- }
1342
-
1343
- // src/cli/prompt.ts
1344
- async function readApiKeySecret() {
1345
- if (process.stdin.isTTY) {
1346
- return promptMasked("API key: ");
1347
- }
1348
- return readSingleLineFromStdin();
1349
- }
1350
- async function promptMasked(label2) {
1351
- const { stdin, stdout } = process;
1352
- stdout.write(label2);
1353
- const hadRawMode = typeof stdin.setRawMode === "function";
1354
- if (!hadRawMode) {
1355
- const value = await readSingleLineFromStdin();
1356
- stdout.write(`${"\u2022".repeat(Math.min(value.length, 80))}
1357
- `);
1358
- return value;
1359
- }
1360
- return new Promise((resolve4, reject) => {
1361
- const previousEncoding = stdin.readableEncoding;
1362
- stdin.setEncoding("utf8");
1363
- stdin.setRawMode(true);
1364
- stdin.resume();
1365
- let buf = "";
1366
- const cleanup = () => {
1367
- stdin.removeListener("data", onData);
1368
- stdin.removeListener("error", onError);
1369
- stdin.setRawMode(false);
1370
- stdin.pause();
1371
- if (previousEncoding) stdin.setEncoding(previousEncoding);
1372
- };
1373
- const onData = (chunk) => {
1374
- for (const ch of chunk) {
1375
- if (ch === "\n" || ch === "\r") {
1376
- cleanup();
1377
- stdout.write("\n");
1378
- resolve4(buf.trim());
1379
- return;
1380
- }
1381
- if (ch === "") {
1382
- cleanup();
1383
- stdout.write("\n");
1384
- process.exit(130);
1385
- }
1386
- if (ch === "\x7F" || ch === "\b") {
1387
- if (buf.length > 0) {
1388
- buf = buf.slice(0, -1);
1389
- stdout.write("\b \b");
1390
- }
1391
- continue;
1392
- }
1393
- if (ch < " ") {
1394
- continue;
1395
- }
1396
- buf += ch;
1397
- stdout.write("\u2022");
1398
- }
1399
- };
1400
- const onError = (err) => {
1401
- cleanup();
1402
- reject(err instanceof Error ? err : new Error(String(err)));
1403
- };
1404
- stdin.on("data", onData);
1405
- stdin.on("error", onError);
1406
- });
1407
- }
1408
- async function readSingleLineFromStdin() {
1409
- let buf = "";
1410
- for await (const chunk of process.stdin) {
1411
- buf += chunk.toString("utf8");
1412
- const nl = buf.indexOf("\n");
1413
- if (nl >= 0) return buf.slice(0, nl).trim();
1414
- }
1415
- return buf.trim();
1416
- }
1417
-
1418
- // src/cli/commands/login.ts
1419
- var DEFAULT_ENDPOINT = "https://memory.api.pyxmate.com";
1420
- async function loginCommand(opts = {}) {
1421
- const provider = opts.keychain ?? getLifecycleKeychain();
1422
- const fetcher = opts.fetchImpl ?? fetch;
1423
- const endpoint = normalizeEndpoint(
1424
- opts.endpoint ?? await readExistingEndpoint(provider) ?? DEFAULT_ENDPOINT
1425
- );
1426
- if (!isHttpUrl2(endpoint)) {
1427
- process.stderr.write(`Error: invalid --endpoint \`${endpoint}\` (expected http(s) URL).
1428
- `);
1429
- return EXIT.USAGE;
1430
- }
1431
- let apiKey;
1432
- try {
1433
- apiKey = opts.apiKey?.trim() || await readApiKeySecret();
1434
- } catch (err) {
1435
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1436
- `);
1437
- return EXIT.USAGE;
1438
- }
1439
- if (!apiKey) {
1440
- process.stderr.write("Error: API key is empty.\n");
1441
- return EXIT.USAGE;
1442
- }
1443
- const probe = await probeCredentials({ endpoint, apiKey, fetcher });
1444
- if (probe.kind === "invalid") {
1445
- process.stderr.write(
1446
- `Error: API key was rejected by ${endpoint} (HTTP ${probe.status}).
1447
- No credentials were written.
1448
- `
1449
- );
1450
- return EXIT.NOT_LOGGED_IN;
1451
- }
1452
- try {
1453
- const written = await withLifecycleLock(provider, async () => {
1454
- await supersedeLogout(provider, endpoint, apiKey);
1455
- await provider.write({ schemaVersion: 1, endpoint, apiKey });
1456
- });
1457
- if (written === BUSY) {
1458
- process.stderr.write(
1459
- "Error: another pyx-mem sign-in or sign-out is in progress. Retry in a moment.\nNo credentials were written.\n"
1460
- );
1461
- return EXIT.BUSY;
1462
- }
1463
- } catch (err) {
1464
- if (err instanceof KeychainError) {
1465
- process.stderr.write(
1466
- `Error: ${err.message}
1467
- ${err.guidance}
1468
- No credentials were written. pyx-mem does not write plaintext token files.
1469
- `
1470
- );
1471
- return err.exit;
1472
- }
1473
- throw err;
1474
- }
1475
- if (probe.kind === "unreachable") {
1476
- process.stdout.write(
1477
- `Stored credentials for ${endpoint}. Warning: validation probe failed (${probe.reason}). Run: pyx-mem doctor
1478
- `
1479
- );
1480
- } else {
1481
- process.stdout.write(`Logged in to ${endpoint}. Run: pyx-mem doctor
1482
- `);
1483
- }
1484
- return EXIT.OK;
1485
- }
1486
- async function readExistingEndpoint(provider) {
1487
- try {
1488
- const creds = await provider.read();
1489
- return creds?.endpoint ?? null;
1490
- } catch {
1491
- return null;
1492
- }
1493
- }
1494
- function normalizeEndpoint(input) {
1495
- return input.replace(/\/+$/, "");
1496
- }
1497
- function isHttpUrl2(value) {
1498
- try {
1499
- const u = new URL(value);
1500
- return u.protocol === "http:" || u.protocol === "https:";
1501
- } catch {
1502
- return false;
1503
- }
1504
- }
1505
-
1506
- // src/cli/commands/login-browser.ts
1507
- import { spawn } from "child_process";
1508
- import { createHash as createHash3, randomBytes } from "crypto";
1509
- import { hostname } from "os";
1510
- import { createInterface } from "readline";
1511
-
1512
- // src/cli/lifecycle/callback-server.ts
1513
- import { createHash as createHash2, timingSafeEqual } from "crypto";
1514
- import { createServer } from "http";
1515
- var CALLBACK_PATH = "/pyx-memory/callback";
1516
- var CODE_RE = /^[A-Za-z0-9_-]{43}$/;
1517
- var STYLES = `
1518
- :root { color-scheme: dark; font-family: "Avenir Next", "Trebuchet MS", sans-serif; }
1519
- * { box-sizing: border-box; }
1520
- body {
1521
- margin: 0; min-height: 100vh; min-height: 100svh; padding: 32px 20px;
1522
- display: flex; color: #f7f8fb; background: #0b0b0e;
1523
- background-image: radial-gradient(ellipse at 50% 55%, #0071e31c, transparent 65%);
1524
- }
1525
- main { width: 100%; max-width: 496px; margin: auto; }
1526
- .brand { margin: 0 0 20px 8px; font-size: 15px; font-weight: 600; letter-spacing: -.02em; }
1527
- .brand span { margin-left: 8px; color: #a9adb8; font-weight: 400; }
1528
- .card {
1529
- padding: clamp(24px, 6vw, 40px); border: 1px solid #30333c; border-radius: 28px;
1530
- background: #15161c; box-shadow: 0 24px 80px #0006;
1531
- }
1532
- .symbol {
1533
- display: grid; place-items: center; width: 64px; height: 64px; margin-bottom: 32px;
1534
- border: 1px solid #2c4059; border-radius: 20px; color: #6ab4ff; background: #12263a;
1535
- }
1536
- .continue .symbol { animation: arrive 360ms cubic-bezier(.22, 1, .36, 1); }
1537
- .cancelled .symbol { color: #a9adb8; background: #20222b; border-color: #383b47; }
1538
- .invalid .symbol { color: #f4bd7a; background: #302719; border-color: #59462d; }
1539
- h1 {
1540
- margin: 0 0 20px; font-size: clamp(28px, 6vw, 40px); font-weight: 600;
1541
- line-height: 1.12; letter-spacing: -.035em; overflow-wrap: break-word;
1542
- }
1543
- p { margin: 0; color: #a9adb8; font-size: 16px; line-height: 1.65; }
1544
- .note { margin-top: 32px; padding-top: 24px; border-top: 1px solid #30333c; font-size: 14px; }
1545
- @keyframes arrive { from { opacity: .65; transform: translateY(4px); } }
1546
- @media (prefers-reduced-motion: reduce) { .continue .symbol { animation: none; } }
1547
- `;
1548
- var PAGE_CSP = `default-src 'none'; style-src 'sha256-${createHash2("sha256").update(STYLES).digest("base64")}'; base-uri 'none'; form-action 'none'`;
1549
- var page = (state, title, text2, icon) => `<!doctype html>
1550
- <html lang="en">
1551
- <head>
1552
- <meta charset="utf-8">
1553
- <meta name="viewport" content="width=device-width, initial-scale=1">
1554
- <title>${title} \xB7 Pyx Memory</title>
1555
- <style>${STYLES}</style>
1556
- </head>
1557
- <body>
1558
- <main class="${state}">
1559
- <div class="brand">pyx<span>Memory</span></div>
1560
- <section class="card" aria-labelledby="title">
1561
- <div class="symbol" aria-hidden="true"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">${icon}</svg></div>
1562
- <h1 id="title">${title}</h1>
1563
- <p>${text2}</p>
1564
- <p class="note">You can close this tab.</p>
1565
- </section>
1566
- </main>
1567
- </body>
1568
- </html>`;
1569
- var ACCEPTED_PAGE = page(
1570
- "continue",
1571
- "Continue in your app",
1572
- "Your approval was received. Return to the app that started this sign-in to finish connecting.",
1573
- '<path d="m9 4-5 5 5 5M4 9h10a6 6 0 0 1 0 12h-2"/>'
1574
- );
1575
- var DENIED_PAGE = page(
1576
- "cancelled",
1577
- "Sign-in cancelled",
1578
- "Return to the app that started this sign-in. You can try again there whenever you\u2019re ready.",
1579
- '<path d="m7 7 10 10M17 7 7 17"/>'
1580
- );
1581
- var REJECTED_PAGE = page(
1582
- "invalid",
1583
- "This sign-in link isn\u2019t valid",
1584
- "Return to the app that started this sign-in to continue or try again.",
1585
- '<path d="M12 6v7m0 4v.01"/><circle cx="12" cy="12" r="10"/>'
1586
- );
1587
- function sameSecret(a, b) {
1588
- const left = Buffer.from(a);
1589
- const right = Buffer.from(b);
1590
- return left.length === right.length && timingSafeEqual(left, right);
1591
- }
1592
- function single(params, key) {
1593
- const values = params.getAll(key);
1594
- return values.length === 1 ? values[0] : null;
1595
- }
1596
- async function startCallbackServer() {
1597
- let expected = null;
1598
- let settle2 = () => {
1599
- };
1600
- const result = new Promise((resolve4) => {
1601
- settle2 = resolve4;
1602
- });
1603
- function accept(method, host, rawUrl) {
1604
- if (method !== "GET" || expected === null || host !== `127.0.0.1:${port}`) return null;
1605
- const url = new URL(rawUrl, `http://127.0.0.1:${port}`);
1606
- if (url.pathname !== CALLBACK_PATH) return null;
1607
- const params = url.searchParams;
1608
- const keys = new Set(params.keys());
1609
- const code = single(params, "code");
1610
- const error = single(params, "error");
1611
- if (keys.size !== 3 || single(params, "request") !== expected.requestId || !sameSecret(single(params, "state") ?? "", expected.state)) {
1612
- return null;
1613
- }
1614
- if (code !== null && CODE_RE.test(code)) return { code };
1615
- if (error === "access_denied") return { denied: true };
1616
- return null;
1617
- }
1618
- const server = createServer({ maxHeaderSize: 8192 }, (req, res) => {
1619
- const accepted = accept(req.method, req.headers.host, req.url ?? "");
1620
- if (accepted !== null) {
1621
- expected = null;
1622
- settle2(accepted);
1623
- }
1624
- res.writeHead(accepted === null ? 400 : 200, {
1625
- "Content-Type": "text/html; charset=utf-8",
1626
- "Cache-Control": "no-store",
1627
- "Content-Security-Policy": PAGE_CSP,
1628
- "Referrer-Policy": "no-referrer",
1629
- "X-Content-Type-Options": "nosniff",
1630
- Connection: "close"
1631
- });
1632
- res.end(accepted === null ? REJECTED_PAGE : "denied" in accepted ? DENIED_PAGE : ACCEPTED_PAGE);
1633
- });
1634
- server.requestTimeout = 5e3;
1635
- server.headersTimeout = 5e3;
1636
- await new Promise((resolve4, reject) => {
1637
- server.once("error", reject);
1638
- server.listen(0, "127.0.0.1", resolve4);
1639
- });
1640
- const port = server.address().port;
1641
- return {
1642
- port,
1643
- expect(requestId, state) {
1644
- expected = { requestId, state };
1645
- },
1646
- result,
1647
- close() {
1648
- server.close();
1649
- server.closeAllConnections();
1650
- }
1651
- };
1652
- }
1653
-
1654
- // src/cli/lifecycle/settle.ts
1655
- var STARTED_EXPIRY_MARGIN_MS = 15 * 6e4;
1656
- async function settle(keychain, api, journal, lockLost) {
1657
- const holdsLock = () => {
1658
- if (lockLost()) throw new LockLostError();
1659
- };
1660
- const save = async () => {
1661
- holdsLock();
1662
- await saveJournal(keychain, journal);
1663
- };
1664
- const readShared = () => keychain.read().catch((err) => {
1665
- if (err instanceof KeychainError && err.code === "credential-record-invalid") return null;
1666
- throw err;
1667
- });
1668
- const queueRevoke = (key) => {
1669
- const queued = journal.revoke.some(
1670
- (pending) => pending.endpoint === key.endpoint && pending.apiKey === key.apiKey
1671
- );
1672
- if (!queued) journal.revoke.push(key);
1673
- };
1674
- const login = journal.login;
1675
- if (login?.phase === "started") {
1676
- holdsLock();
1677
- if (Date.now() > login.expiresAt + STARTED_EXPIRY_MARGIN_MS || await api.cancel(login)) {
1678
- journal.login = null;
1679
- await save();
1680
- }
1681
- } else if (login?.apiKey !== void 0) {
1682
- const own = { endpoint: login.endpoint, apiKey: login.apiKey };
1683
- const shared = login.phase === "committing" ? await readShared() : null;
1684
- const committed = shared !== null && credentialFingerprint(shared.endpoint, shared.apiKey) === credentialFingerprint(own.endpoint, own.apiKey);
1685
- journal.login = null;
1686
- if (!committed) queueRevoke(own);
1687
- if (login.previous !== void 0 && shared?.apiKey !== login.previous.apiKey) {
1688
- queueRevoke(login.previous);
1689
- }
1690
- await save();
1691
- }
1692
- if (journal.logout !== null) {
1693
- const { fingerprint, revoke } = journal.logout;
1694
- const shared = await readShared();
1695
- if (shared !== null && credentialFingerprint(shared.endpoint, shared.apiKey) === fingerprint) {
1696
- holdsLock();
1697
- await keychain.delete();
1698
- }
1699
- journal.logout = null;
1700
- if (revoke !== null) queueRevoke(revoke);
1701
- await save();
1702
- }
1703
- for (const key of [...journal.revoke]) {
1704
- holdsLock();
1705
- if (await api.revoke(key)) {
1706
- journal.revoke = journal.revoke.filter((pending) => pending !== key);
1707
- journal.revokedAt = Date.now();
1708
- await save();
1709
- }
1710
- }
1711
- return journal;
1712
- }
1713
-
1714
- // src/cli/commands/login-browser.ts
1715
- var DEFAULT_ENDPOINT2 = "https://memory.api.pyxmate.com";
1716
- var MAX_STDIN_LINE = 1024;
1717
- var Stop = class extends Error {
1718
- constructor(outcome) {
1719
- super(outcome);
1720
- this.outcome = outcome;
1721
- }
1722
- outcome;
1723
- };
1724
- function stopFor(failure2) {
1725
- return new Stop(failure2.code === "cancelled" ? "requested" : failure2.code);
1726
- }
1727
- function printHuman(e) {
1728
- if (e.event === "awaiting_browser") {
1729
- process.stdout.write(`Sign in with your browser:
1730
- ${e.authorizationUrl}
1731
- `);
1732
- } else if (e.event === "connecting") {
1733
- process.stdout.write("Finishing sign-in\u2026\n");
1734
- } else if (e.event === "connected") {
1735
- process.stdout.write(
1736
- `Connected to ${e.connection.projectName} as ${e.connection.accountLabel}. Run: pyx-mem doctor
1737
- `
1738
- );
1739
- } else if (e.event === "cancelled") {
1740
- process.stdout.write("Sign-in cancelled. Stored credentials were not changed.\n");
1741
- } else {
1742
- process.stderr.write(`Error: ${e.message}
1743
- `);
1744
- }
1745
- if ("cleanupPending" in e && e.cleanupPending && e.event !== "error") {
1746
- process.stderr.write("Server-side cleanup is pending. Run: pyx-mem login-recovery\n");
1747
- }
1748
- }
1749
- function openInBrowser(url) {
1750
- const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
1751
- const child = spawn(command, args, { stdio: "ignore", detached: true });
1752
- child.on("error", () => {
1753
- process.stderr.write(
1754
- "Could not open a browser automatically. Open the sign-in URL yourself.\n"
1755
- );
1756
- });
1757
- child.unref();
1758
- }
1759
- function exitFor(outcome) {
1760
- if (outcome === "requested" || outcome === "denied") return EXIT.CANCELLED;
1761
- if (outcome === "busy") return EXIT.BUSY;
1762
- if (outcome === "cleanup_pending") return EXIT.CLEANUP_PENDING;
1763
- if (outcome === "invalid_endpoint") return EXIT.USAGE;
1764
- return EXIT.DOCTOR_FAIL;
1765
- }
1766
- async function snapshot(keychain) {
1767
- try {
1768
- const credentials = await keychain.read();
1769
- return {
1770
- token: credentials === null ? "none" : credentialFingerprint(credentials.endpoint, credentials.apiKey),
1771
- credentials
1772
- };
1773
- } catch (err) {
1774
- if (err instanceof KeychainError && err.code === "credential-record-invalid") {
1775
- return { token: "invalid", credentials: null };
1776
- }
1777
- throw err;
1778
- }
1779
- }
1780
- async function browserLoginCommand(opts) {
1781
- const keychain = opts.keychain ?? getLifecycleKeychain();
1782
- const api = createCliLoginApi(opts.fetchImpl);
1783
- const emit4 = opts.json ? (e) => process.stdout.write(`${JSON.stringify(e)}
1784
- `) : printHuman;
1785
- const stop = new AbortController();
1786
- let lockLost = false;
1787
- const cancel = () => stop.abort();
1788
- const stopped = new Promise((_, reject) => {
1789
- stop.signal.addEventListener("abort", () => reject(new Stop("requested")));
1790
- });
1791
- stopped.catch(() => {
1792
- });
1793
- const check = () => {
1794
- if (lockLost) throw new Stop("lock_lost");
1795
- if (stop.signal.aborted) throw new Stop("requested");
1796
- };
1797
- process.once("SIGINT", cancel);
1798
- process.once("SIGTERM", cancel);
1799
- const lines = opts.json ? createInterface({ input: opts.input ?? process.stdin }) : null;
1800
- lines?.on("line", (line) => {
1801
- if (line.length > MAX_STDIN_LINE) return;
1802
- try {
1803
- if (JSON.parse(line)?.action === "cancel") cancel();
1804
- } catch {
1805
- }
1806
- });
1807
- if (opts.noOpen) lines?.on("close", cancel);
1808
- let release = null;
1809
- let listener = null;
1810
- let journal = null;
1811
- let connected = null;
1812
- let outcome = null;
1813
- let storeError = null;
1814
- try {
1815
- release = await keychain.acquireLock(() => {
1816
- lockLost = true;
1817
- cancel();
1818
- });
1819
- if (release === null) throw new Stop("busy");
1820
- journal = await settle(keychain, api, await loadJournal(keychain), () => lockLost);
1821
- if (journal.login !== null) throw new Stop("cleanup_pending");
1822
- const base = await snapshot(keychain);
1823
- const endpoint = parseEndpoint(opts.endpoint ?? base.credentials?.endpoint ?? DEFAULT_ENDPOINT2);
1824
- if (endpoint === null) throw new Stop("invalid_endpoint");
1825
- const replaced = base.credentials ? { endpoint: base.credentials.endpoint, apiKey: base.credentials.apiKey } : null;
1826
- const previous = replaced !== null && base.credentials?.provenance === "browser-login" && isKeyRef(replaced) ? replaced : void 0;
1827
- if (!hasRoomForLogin(journal, endpoint, previous)) throw new Stop("cleanup_pending");
1828
- listener = await startCallbackServer();
1829
- const codeVerifier = randomBytes(32).toString("base64url");
1830
- const state = randomBytes(32).toString("base64url");
1831
- const started = await api.start(
1832
- endpoint,
1833
- {
1834
- codeChallenge: createHash3("sha256").update(codeVerifier).digest("base64url"),
1835
- callbackPort: listener.port,
1836
- state,
1837
- deviceLabel: hostname().replace(/\p{Cc}/gu, "").trim().slice(0, 64) || "this computer"
1838
- },
1839
- stop.signal
1840
- );
1841
- check();
1842
- if (!started.ok) throw stopFor(started);
1843
- const login = {
1844
- phase: "started",
1845
- endpoint,
1846
- requestId: started.requestId,
1847
- codeVerifier,
1848
- expiresAt: started.expiresAt.getTime()
1849
- };
1850
- journal.login = login;
1851
- await saveJournal(keychain, journal);
1852
- check();
1853
- listener.expect(started.requestId, state);
1854
- emit4({
1855
- event: "awaiting_browser",
1856
- authorizationUrl: started.authorizationUrl,
1857
- expiresAt: started.expiresAt.toISOString()
1858
- });
1859
- if (!opts.noOpen) (opts.openBrowser ?? openInBrowser)(started.authorizationUrl);
1860
- let expiry;
1861
- const expired = new Promise((_, reject) => {
1862
- const ms = Math.min(Math.max(started.expiresAt.getTime() - Date.now(), 0), 6e5);
1863
- expiry = setTimeout(() => reject(new Stop("expired")), ms);
1864
- });
1865
- const callback = await Promise.race([listener.result, stopped, expired]).finally(
1866
- () => clearTimeout(expiry)
1867
- );
1868
- check();
1869
- if ("denied" in callback) throw new Stop("denied");
1870
- emit4({ event: "connecting" });
1871
- const redeemed = await api.redeem(login, callback.code, stop.signal);
1872
- check();
1873
- if (!redeemed.ok) throw stopFor(redeemed);
1874
- const issued = { ...login, phase: "issued", apiKey: redeemed.apiKey };
1875
- journal.login = issued;
1876
- await saveJournal(keychain, journal);
1877
- check();
1878
- const confirmed = await api.confirm(issued, stop.signal);
1879
- check();
1880
- if (!confirmed.ok) throw stopFor(confirmed);
1881
- const current = (await loadJournal(keychain)).login;
1882
- if (current?.requestId !== issued.requestId || current.phase !== "issued") {
1883
- throw new Stop("lock_lost");
1884
- }
1885
- journal.login = { ...issued, phase: "committing", ...previous ? { previous } : {} };
1886
- await saveJournal(keychain, journal);
1887
- check();
1888
- if ((await snapshot(keychain)).token !== base.token) throw new Stop("credential_changed");
1889
- check();
1890
- const fingerprint = credentialFingerprint(endpoint, redeemed.apiKey);
1891
- try {
1892
- await keychain.write({
1893
- schemaVersion: 1,
1894
- endpoint,
1895
- apiKey: redeemed.apiKey,
1896
- connection: redeemed.connection,
1897
- provenance: "browser-login"
1898
- });
1899
- } catch (err) {
1900
- if ((await snapshot(keychain)).token !== fingerprint) throw err;
1901
- }
1902
- connected = { connection: redeemed.connection, fingerprint };
1903
- } catch (err) {
1904
- if (err instanceof Stop) outcome = err.outcome;
1905
- else if (err instanceof JournalInvalidError) outcome = "journal_invalid";
1906
- else if (err instanceof KeychainError) {
1907
- storeError = err;
1908
- outcome = "credential_store_unavailable";
1909
- } else outcome = "login_failed";
1910
- }
1911
- listener?.close();
1912
- lines?.close();
1913
- process.off("SIGINT", cancel);
1914
- process.off("SIGTERM", cancel);
1915
- let cleanupPending = outcome !== "busy";
1916
- if (journal !== null && !lockLost) {
1917
- try {
1918
- cleanupPending = !isEmpty(await settle(keychain, api, journal, () => lockLost));
1919
- } catch {
1920
- cleanupPending = true;
1921
- }
1922
- }
1923
- if (release !== null) await releaseLifecycleLock(release);
1924
- if (connected !== null) {
1925
- emit4({
1926
- event: "connected",
1927
- connection: connected.connection,
1928
- credentialFingerprint: connected.fingerprint,
1929
- cleanupPending
1930
- });
1931
- return EXIT.OK;
1932
- }
1933
- if (outcome === "requested" || outcome === "denied") {
1934
- emit4({ event: "cancelled", reason: outcome, cleanupPending });
1935
- return EXIT.CANCELLED;
1936
- }
1937
- const code = outcome ?? "login_failed";
1938
- emit4({
1939
- event: "error",
1940
- code,
1941
- message: storeError ? `${storeError.message} ${storeError.guidance}` : ERROR_MESSAGES[code].message,
1942
- retryable: ERROR_MESSAGES[code].retryable,
1943
- cleanupPending
1944
- });
1945
- return storeError?.exit ?? exitFor(code);
1946
- }
1947
-
1948
- // src/cli/commands/logout.ts
1949
- function lifecycleFailure(code, cleanupPending, err) {
1950
- return {
1951
- ok: false,
1952
- code,
1953
- // Keychain guidance is static text; any other error's text is never shown.
1954
- message: err instanceof KeychainError ? `${err.message} ${err.guidance}` : ERROR_MESSAGES[code].message,
1955
- retryable: ERROR_MESSAGES[code].retryable,
1956
- cleanupPending
1957
- };
1958
- }
1959
- async function cleanupPendingAfterFailure(keychain) {
1960
- try {
1961
- return !isEmpty(await loadJournal(keychain));
1962
- } catch {
1963
- return true;
1964
- }
1965
- }
1966
- async function logoutCommand(opts = {}) {
1967
- const keychain = opts.keychain ?? getLifecycleKeychain();
1968
- const api = createCliLoginApi(opts.fetchImpl);
1969
- const fail2 = (failure2, exit) => {
1970
- if (opts.json) process.stdout.write(`${JSON.stringify(failure2)}
1971
- `);
1972
- else process.stderr.write(`Error: ${failure2.message}
1973
- `);
1974
- return exit;
1975
- };
1976
- try {
1977
- const result = await withLifecycleLock(keychain, (lockLost) => logout(lockLost));
1978
- return result === BUSY ? fail2(lifecycleFailure("busy", false), EXIT.BUSY) : result;
1979
- } catch (err) {
1980
- const pending = await cleanupPendingAfterFailure(keychain);
1981
- if (err instanceof KeychainError) {
1982
- return fail2(lifecycleFailure("credential_store_unavailable", pending, err), err.exit);
1983
- }
1984
- if (err instanceof LockLostError) {
1985
- return fail2(lifecycleFailure("lock_lost", pending), EXIT.DOCTOR_FAIL);
1986
- }
1987
- if (err instanceof JournalInvalidError) {
1988
- return fail2(lifecycleFailure("journal_invalid", true), EXIT.DOCTOR_FAIL);
1989
- }
1990
- return fail2(lifecycleFailure("cleanup_pending", true), EXIT.CLEANUP_PENDING);
1991
- }
1992
- async function logout(lockLost) {
1993
- const journal = await loadJournal(keychain);
1994
- const selected = await keychain.read().catch((err) => {
1995
- if (err instanceof KeychainError && err.code === "credential-record-invalid") {
1996
- return "unreadable";
1997
- }
1998
- throw err;
1999
- });
2000
- const managed = selected !== null && selected !== "unreadable" && selected.provenance === "browser-login" && isKeyRef({ endpoint: selected.endpoint, apiKey: selected.apiKey }) ? { endpoint: selected.endpoint, apiKey: selected.apiKey } : null;
2001
- if (selected === null || selected === "unreadable") {
2002
- await keychain.delete();
2003
- } else {
2004
- journal.logout = {
2005
- fingerprint: credentialFingerprint(selected.endpoint, selected.apiKey),
2006
- revoke: managed
2007
- };
2008
- await saveJournal(keychain, journal);
2009
- }
2010
- const cleanupPending = !isEmpty(await settle(keychain, api, journal, lockLost));
2011
- if (await keychain.read() !== null) {
2012
- return fail2(lifecycleFailure("credential_changed", cleanupPending), EXIT.DOCTOR_FAIL);
2013
- }
2014
- if (lockLost()) throw new LockLostError();
2015
- const convergence = convergenceSeconds(journal);
2016
- const revocationPending = journal.revoke.length > 0;
2017
- const serverRevocation = revocationPending ? "pending" : convergence !== null ? "revoked" : "not_attempted";
2018
- const legacy = selected !== null && managed === null;
2019
- const report4 = {
2020
- ok: true,
2021
- credentialKind: selected === null ? null : managed !== null ? "browser" : "legacy",
2022
- localDeleted: selected !== null,
2023
- serverRevocation,
2024
- cleanupPending,
2025
- reconnectRequired: selected !== null || serverRevocation !== "not_attempted",
2026
- // Proxies older than the per-request credential check hold the key in
2027
- // memory. A key that is still valid on the server keeps them working.
2028
- pendingLegacySessions: legacy || revocationPending,
2029
- serverConvergenceSeconds: convergence
2030
- };
2031
- if (opts.json) {
2032
- process.stdout.write(`${JSON.stringify(report4)}
2033
- `);
2034
- } else {
2035
- process.stdout.write(
2036
- selected === null ? "No pyx-memory credentials were stored.\n" : "pyx-memory credentials deleted from OS credential store.\n"
2037
- );
2038
- if (cleanupPending) {
2039
- process.stderr.write(
2040
- "The server-side key could not be revoked yet. Run: pyx-mem login-recovery\n"
2041
- );
2042
- } else if (report4.pendingLegacySessions) {
2043
- process.stdout.write(
2044
- "The API key itself was not revoked (other computers may use it). Restart running agents; revoke the key in the dashboard to end it everywhere.\n"
2045
- );
2046
- }
2047
- }
2048
- return cleanupPending ? EXIT.CLEANUP_PENDING : EXIT.OK;
2049
- }
2050
- }
2051
-
2052
- // src/cli/commands/login-recovery.ts
2053
- async function signOutJournal(keychain) {
2054
- const journal = emptyJournal();
2055
- const shared = await keychain.read().catch((err) => {
2056
- if (err instanceof KeychainError && err.code === "credential-record-invalid") return null;
2057
- throw err;
2058
- });
2059
- if (shared !== null) {
2060
- const key = { endpoint: shared.endpoint, apiKey: shared.apiKey };
2061
- journal.logout = {
2062
- fingerprint: credentialFingerprint(shared.endpoint, shared.apiKey),
2063
- revoke: shared.provenance === "browser-login" && isKeyRef(key) ? key : null
2064
- };
2065
- }
2066
- await saveJournal(keychain, journal);
2067
- return journal;
2068
- }
2069
- async function loginRecoveryCommand(opts = {}) {
2070
- const keychain = opts.keychain ?? getLifecycleKeychain();
2071
- const fail2 = (failure2, exit) => {
2072
- if (opts.json) process.stdout.write(`${JSON.stringify(failure2)}
2073
- `);
2074
- else process.stderr.write(`Error: ${failure2.message}
2075
- `);
2076
- return exit;
2077
- };
2078
- try {
2079
- const journal = await withLifecycleLock(keychain, async (lockLost) => {
2080
- const loaded = await loadJournal(keychain).catch((err) => {
2081
- if (err instanceof JournalInvalidError && opts.discardUnreadableJournal) {
2082
- return signOutJournal(keychain);
2083
- }
2084
- throw err;
2085
- });
2086
- const settled = await settle(keychain, createCliLoginApi(opts.fetchImpl), loaded, lockLost);
2087
- if (lockLost()) throw new LockLostError();
2088
- return settled;
2089
- });
2090
- if (journal === BUSY) return fail2(lifecycleFailure("busy", true), EXIT.BUSY);
2091
- if (!isEmpty(journal)) {
2092
- return fail2(lifecycleFailure("cleanup_pending", true), EXIT.CLEANUP_PENDING);
2093
- }
2094
- const convergence = convergenceSeconds(journal);
2095
- const report4 = {
2096
- ok: true,
2097
- cleanupPending: false,
2098
- serverRevocation: convergence !== null ? "revoked" : "not_attempted",
2099
- serverConvergenceSeconds: convergence
2100
- };
2101
- if (opts.json) process.stdout.write(`${JSON.stringify(report4)}
2102
- `);
2103
- else process.stdout.write("Nothing is pending.\n");
2104
- return EXIT.OK;
2105
- } catch (err) {
2106
- if (err instanceof JournalInvalidError) {
2107
- return fail2(lifecycleFailure("journal_invalid", true), EXIT.DOCTOR_FAIL);
2108
- }
2109
- const pending = await cleanupPendingAfterFailure(keychain);
2110
- if (err instanceof KeychainError) {
2111
- return fail2(lifecycleFailure("credential_store_unavailable", pending, err), err.exit);
2112
- }
2113
- if (err instanceof LockLostError) {
2114
- return fail2(lifecycleFailure("lock_lost", pending), EXIT.DOCTOR_FAIL);
2115
- }
2116
- return fail2(lifecycleFailure("cleanup_pending", true), EXIT.CLEANUP_PENDING);
2117
- }
2118
- }
2119
-
2120
- // src/mcp/credentials.ts
2121
- function createReadCredentials(providerFactory) {
2122
- return async () => {
2123
- const provider = providerFactory();
2124
- try {
2125
- const creds = await provider.read();
2126
- if (!creds) return { ok: false, result: mcpMissingCredentials() };
2127
- return { ok: true, credentials: creds };
2128
- } catch (err) {
2129
- if (err instanceof KeychainError) {
2130
- return { ok: false, result: mcpKeychainError(err.message, err.guidance) };
2131
- }
2132
- const msg = err instanceof Error ? err.message : String(err);
2133
- return { ok: false, result: mcpKeychainError(msg, "") };
2134
- }
2135
- };
2136
- }
2137
-
2138
- // src/mcp/proxy-server.ts
2139
- import { createHash as createHash5, randomUUID as randomUUID5 } from "crypto";
2140
- import { readFile as readFile2, stat as stat2 } from "fs/promises";
2141
- import { basename as basename2, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
2142
- import { setTimeout as delay2 } from "timers/promises";
2143
- import { Client as McpClient } from "@modelcontextprotocol/sdk/client/index.js";
2144
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
2145
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2146
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2147
- import {
2148
- CallToolRequestSchema,
2149
- GetPromptRequestSchema,
2150
- ListPromptsRequestSchema,
2151
- ListToolsRequestSchema
2152
- } from "@modelcontextprotocol/sdk/types.js";
2153
-
2154
- // src/mcp/file-ingest-intents.ts
2155
- import { randomUUID as randomUUID3 } from "crypto";
2156
- import { mkdir as mkdir2, readdir, readFile, rename, rm, writeFile } from "fs/promises";
2157
- import { homedir as homedir2 } from "os";
2158
- import { join as join2 } from "path";
2159
- var DELIVERED_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
2160
- var DEFAULT_FILE_INGEST_INTENT_DIR = join2(homedir2(), ".pyx-memory", "file-ingests");
2161
- function parseIntent(raw, file) {
2162
- const value = JSON.parse(raw);
2163
- const fp = value?.fingerprint;
2164
- if (value?.version !== 1 || typeof value.jobId !== "string" || typeof value.endpoint !== "string" || typeof value.delivered !== "boolean" || typeof value.createdAt !== "string" || typeof value.updatedAt !== "string" || typeof fp?.path !== "string" || typeof fp.byteLength !== "number" || typeof fp.sha256 !== "string") {
2165
- throw new Error(`file ingest intent ${file} is not a version 1 record; remove it to continue`);
2166
- }
2167
- return value;
2168
- }
2169
- async function readIntents(dir) {
2170
- let names;
2171
- try {
2172
- names = await readdir(dir);
2173
- } catch (err) {
2174
- if (err.code === "ENOENT") return [];
2175
- throw err;
2176
- }
2177
- return Promise.all(
2178
- names.filter((name) => name.endsWith(".json")).map(async (name) => {
2179
- const file = join2(dir, name);
2180
- return { file, intent: parseIntent(await readFile(file, "utf8"), file) };
2181
- })
2182
- );
2183
- }
2184
- function createFileIngestIntentStore(dir = DEFAULT_FILE_INGEST_INTENT_DIR) {
2185
- return {
2186
- async findUndelivered(endpoint, path, namespaceId) {
2187
- const records = await readIntents(dir);
2188
- const prunedBefore = Date.now() - DELIVERED_RETENTION_MS;
2189
- await Promise.all(
2190
- records.filter(({ intent }) => intent.delivered && Date.parse(intent.updatedAt) < prunedBefore).map(({ file }) => rm(file, { force: true }))
2191
- );
2192
- return records.map(({ intent }) => intent).filter(
2193
- (intent) => !intent.delivered && intent.endpoint === endpoint && intent.fingerprint.path === path && intent.fingerprint.namespaceId === namespaceId
2194
- ).sort((a, b) => a.createdAt.localeCompare(b.createdAt))[0];
2195
- },
2196
- async save(intent) {
2197
- await mkdir2(dir, { recursive: true, mode: 448 });
2198
- const file = join2(dir, `${intent.jobId}.json`);
2199
- const temp = join2(dir, `.${intent.jobId}.${randomUUID3()}.tmp`);
2200
- await writeFile(temp, `${JSON.stringify(intent)}
2201
- `, { mode: 384, flag: "wx" });
2202
- await rename(temp, file);
2203
- }
2204
- };
2205
- }
2206
-
2207
- // src/mcp/hosted-file-ingest.ts
2208
- import { createHash as createHash4, randomUUID as randomUUID4 } from "crypto";
2209
- import { createReadStream, openAsBlob } from "fs";
2210
- import { stat } from "fs/promises";
2211
- import { basename, isAbsolute, resolve } from "path";
2212
- import { setTimeout as delay } from "timers/promises";
2213
- function parseHostedFileIngestDescriptor(meta) {
2214
- if (meta === null || typeof meta !== "object") return void 0;
2215
- const value = meta[HOSTED_FILE_INGEST_PROTOCOL.metaKey];
2216
- if (value === null || typeof value !== "object") return void 0;
2217
- const { protocolVersion, endpoint, maxBytes } = value;
2218
- if (protocolVersion !== HOSTED_FILE_INGEST_PROTOCOL.version || typeof endpoint !== "string" || // Same-origin absolute path only: the keychain bearer goes with every request.
2219
- !endpoint.startsWith("/") || endpoint.startsWith("//") || typeof maxBytes !== "number" || !Number.isSafeInteger(maxBytes) || maxBytes < 1) {
2220
- return void 0;
2221
- }
2222
- return { endpoint, maxBytes };
2223
- }
2224
- function toMicrocents(cents) {
2225
- const microcents = typeof cents === "number" && cents >= 0 ? Math.round(cents * 1e6) : Number.NaN;
2226
- return Number.isSafeInteger(microcents) ? String(microcents) : void 0;
2227
- }
2228
- function parseArgs(args) {
2229
- const { path, description, namespaceId, maxCostCents, cancel } = args;
2230
- const maxCostMicrocents = maxCostCents === void 0 ? void 0 : toMicrocents(maxCostCents);
2231
- if (typeof path !== "string" || path.length === 0) {
2232
- return "`path` must be a non-empty local file path.";
2233
- }
2234
- if (description !== void 0 && typeof description !== "string") {
2235
- return "`description` must be a string.";
2236
- }
2237
- if (namespaceId !== void 0 && (typeof namespaceId !== "string" || namespaceId.length === 0)) {
2238
- return "`namespaceId` must be a non-empty string.";
2239
- }
2240
- if (cancel !== void 0 && typeof cancel !== "boolean") return "`cancel` must be a boolean.";
2241
- if (maxCostCents !== void 0 && maxCostMicrocents === void 0) {
2242
- return "`maxCostCents` must be a non-negative number of cents.";
2243
- }
2244
- return {
2245
- path: isAbsolute(path) ? path : resolve(path),
2246
- description: description || null,
2247
- namespaceId: namespaceId ?? null,
2248
- maxCostMicrocents,
2249
- cancel: cancel === true
2250
- };
2251
- }
2252
- var CATALOG_STEPS = /* @__PURE__ */ new Set(["begin", "prepare", "commit", "publish"]);
2253
- var DEADLINE_MS = {
2254
- begin: 15e3,
2255
- authorize: 15e3,
2256
- cancel: 15e3,
2257
- upload: 12e4,
2258
- prepare: 9e4,
2259
- commit: 16e4,
2260
- publish: 6e4
2261
- };
2262
- var RETRY_BUDGET_MS = 5 * 6e4;
2263
- var DEFAULT_RETRY_AFTER_S = 2;
2264
- var MAX_RETRY_AFTER_S = 30;
2265
- var RESUME = "Call this tool again with the same arguments to resume the job.";
2266
- var NEW_IMPORT = "Call this tool again to start a new import.";
2267
- var NEXT_BY_CODE = /* @__PURE__ */ new Map([
2268
- [
2269
- "runtime_upgrade_required",
2270
- "The pyx-memory instance needs a runtime upgrade before file ingest can continue; call this tool again with the same arguments once it is upgraded."
2271
- ],
2272
- [
2273
- "file_ingest_job_limit",
2274
- "This instance already has as many unfinished imports as it allows: finish or cancel another one (cancel: true with its path), then call this tool again with the same arguments."
2275
- ],
2276
- [
2277
- "file_ingest_disabled",
2278
- "Hosted file ingest is switched off on the server; call this tool again with the same arguments later to resume."
2279
- ],
2280
- [
2281
- "file_too_large",
2282
- "The file is larger than hosted file ingest accepts: call with cancel: true to drop this import, then import a smaller file."
2283
- ],
2284
- [
2285
- "file_ingest_cancel_requested",
2286
- "The job is being cancelled: call this tool with cancel: true to confirm."
2287
- ]
2288
- ]);
2289
- var ENDED_CODES = /* @__PURE__ */ new Set(["file_ingest_cancelled", "file_ingest_expired"]);
2290
- var COST_CODES = /* @__PURE__ */ new Set([
2291
- "file_ingest_authorization_required",
2292
- "file_ingest_ceiling_exceeded",
2293
- "file_ingest_quota_paused",
2294
- "file_ingest_limit_exceeded"
2295
- ]);
2296
- function nextAfter(res) {
2297
- if (res.status === 402) {
2298
- return "The wallet balance is too low: top up, then call this tool again with the same arguments to resume.";
2299
- }
2300
- return NEXT_BY_CODE.get(res.code ?? "") ?? RESUME;
2301
- }
2302
- var errorText = (err) => err instanceof Error ? err.message : String(err);
2303
- var text = (result) => result.content.map((part) => part.text).join("\n");
2304
- var fail = (result) => ({ ok: false, result });
2305
- var refuse = (message) => fail(mcpText(message, true));
2306
- var label = (intent) => `${basename(intent.fingerprint.path)} (job ${intent.jobId})`;
2307
- async function hashFile(path) {
2308
- const hash = createHash4("sha256");
2309
- let byteLength = 0;
2310
- for await (const chunk of createReadStream(path)) {
2311
- hash.update(chunk);
2312
- byteLength += chunk.length;
2313
- }
2314
- return { byteLength, sha256: hash.digest("hex") };
2315
- }
2316
- async function matchIntent(intent, args, size) {
2317
- const fp = intent.fingerprint;
2318
- const changed = args.description !== fp.description || typeof size === "number" && (size !== fp.byteLength || (await hashFile(args.path)).sha256 !== fp.sha256);
2319
- if (!changed) return { ok: true, value: intent };
2320
- return refuse(
2321
- `${args.path} no longer matches ${label(intent)}: its content or description changed since that import started.
2322
- Call this tool with cancel: true to abandon job ${intent.jobId}, then call again to import the file as it is now.`
2323
- );
2324
- }
2325
- function isRetryable(res) {
2326
- return res.status === 429 || res.status === 503 || HOSTED_FILE_INGEST_RETRYABLE_CODES.includes(res.code ?? "");
2327
- }
2328
- async function pause(ms, signal) {
2329
- await delay(ms, void 0, { signal }).catch((err) => {
2330
- if (!signal?.aborted) throw err;
2331
- });
2332
- }
2333
- function retryAfterMs(header) {
2334
- const seconds = parseRetryAfterSeconds(header) ?? DEFAULT_RETRY_AFTER_S;
2335
- return Math.min(seconds, MAX_RETRY_AFTER_S) * 1e3;
2336
- }
2337
- function formatCents(microcents, decimals) {
2338
- const step = 10n ** BigInt(6 - decimals);
2339
- const units = (BigInt(microcents) + step - 1n) / step;
2340
- const scale = 10n ** BigInt(decimals);
2341
- return `${units / scale}.${String(units % scale).padStart(decimals, "0")}`;
2342
- }
2343
- function unwrapEnvelope(body) {
2344
- if (body === null || typeof body !== "object" || body.success !== true) {
2345
- throw new Error("the response is not a {success: true, data} envelope");
2346
- }
2347
- return body.data;
2348
- }
2349
- function createHostedFileIngestDriver(credentials, guard, intents, fetchImpl = fetch) {
2350
- const http = createHttpClient(credentials, fetchImpl);
2351
- const origin = new URL(credentials.endpoint).origin;
2352
- const stopped = (call, step, detail, next = RESUME) => mcpText(`Import of ${label(call.intent)} stopped at ${step}: ${detail}
2353
- ${next}`, true);
2354
- const interrupted = (call) => refuse(`The call was cancelled; ${label(call.intent)} keeps its progress.
2355
- ${RESUME}`);
2356
- const published = (call, publication) => mcpText(
2357
- `Ingested ${call.filename}: ${publication.entryCount} chunks stored (replaced ${publication.retiredCount}). job ${call.intent.jobId}`
2358
- );
2359
- async function record(call, state) {
2360
- if (call.intent.lastState === state) return;
2361
- call.intent.lastState = state;
2362
- call.intent.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2363
- await intents.save(call.intent);
2364
- }
2365
- async function deliver(call, result) {
2366
- call.intent.delivered = true;
2367
- call.intent.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2368
- await intents.save(call.intent);
2369
- return result;
2370
- }
2371
- function accept(call, step, body, parse) {
2372
- try {
2373
- return { ok: true, value: parse(unwrapEnvelope(body)) };
2374
- } catch (err) {
2375
- return fail(stopped(call, step, `invalid server response: ${errorText(err)}`));
2376
- }
2377
- }
2378
- async function exchange(call, step, attempt, parse) {
2379
- for (; ; ) {
2380
- await guard();
2381
- const res = await attempt(randomUUID4());
2382
- if (call.signal?.aborted) return interrupted(call);
2383
- if (res.ok) return accept(call, step, res.data, parse);
2384
- const waitMs = retryAfterMs(res.retryAfter);
2385
- if (!isRetryable(res) || Date.now() + waitMs > call.retryUntil)
2386
- return refused(call, step, res);
2387
- await pause(waitMs, call.signal);
2388
- }
2389
- }
2390
- function post(call, step, route, body, parse) {
2391
- const { namespaceId } = call.intent.fingerprint;
2392
- const scoped = CATALOG_STEPS.has(step) && namespaceId !== null;
2393
- return exchange(
2394
- call,
2395
- step,
2396
- (idempotencyKey) => http.requestJson({
2397
- method: "POST",
2398
- path: `${call.descriptor.endpoint}${route}`,
2399
- body: scoped ? { ...body, namespaceId } : body,
2400
- idempotencyKey,
2401
- timeoutMs: DEADLINE_MS[step],
2402
- signal: call.signal
2403
- }),
2404
- parse
2405
- );
2406
- }
2407
- async function upload(call, job) {
2408
- const token = job.upload?.token;
2409
- if (token === void 0) {
2410
- return fail(stopped(call, "upload", "the server issued no upload token"));
2411
- }
2412
- const { path, byteLength, sha256 } = call.intent.fingerprint;
2413
- let body;
2414
- try {
2415
- const current = await hashFile(path);
2416
- body = await openAsBlob(path);
2417
- if (current.sha256 !== sha256 || current.byteLength !== byteLength || body.size !== byteLength) {
2418
- throw new Error("it changed since the import started");
2419
- }
2420
- } catch (err) {
2421
- return fail(
2422
- stopped(
2423
- call,
2424
- "upload",
2425
- `cannot send ${path}: ${errorText(err)}`,
2426
- `Restore the file and call again to resume, or call with cancel: true to abandon job ${call.intent.jobId}.`
2427
- )
2428
- );
2429
- }
2430
- return exchange(
2431
- call,
2432
- "upload",
2433
- (idempotencyKey) => http.requestBytes({
2434
- method: "PUT",
2435
- path: `${call.descriptor.endpoint}/${call.intent.jobId}/content`,
2436
- body,
2437
- headers: { [HOSTED_FILE_INGEST_PROTOCOL.uploadTokenHeader]: token },
2438
- idempotencyKey,
2439
- timeoutMs: DEADLINE_MS.upload,
2440
- signal: call.signal
2441
- }),
2442
- parseHostedFileIngestJobView
2443
- );
2444
- }
2445
- async function refused(call, step, res) {
2446
- const detail = res.code === void 0 ? text(res.result) : `${text(res.result)} (${res.code})`;
2447
- if (CATALOG_STEPS.has(step) && COST_CODES.has(res.code ?? "")) {
2448
- return fail(await costRefusal(call, step, detail));
2449
- }
2450
- if (step === "prepare" && res.status === 422 || ENDED_CODES.has(res.code ?? "")) {
2451
- return fail(await deliver(call, stopped(call, step, detail, NEW_IMPORT)));
2452
- }
2453
- return { ok: false, status: res.status, result: stopped(call, step, detail, nextAfter(res)) };
2454
- }
2455
- async function costRefusal(call, step, detail) {
2456
- const auth = await post(
2457
- call,
2458
- "authorize",
2459
- `/${call.intent.jobId}/authorize`,
2460
- {},
2461
- parseHostedFileIngestAuthorization
2462
- );
2463
- if (!auth.ok) return stopped(call, step, detail);
2464
- await record(call, auth.value.state);
2465
- const { ceilingMicrocents, estimateMicrocents, settledMicrocents } = auth.value;
2466
- const cents = (microcents) => microcents === null ? "unknown" : `${formatCents(microcents, 2)} cents`;
2467
- return await authorizationStop(call, step, auth.value) ?? stopped(
2468
- call,
2469
- step,
2470
- `${detail}. The spending ceiling is ${cents(ceilingMicrocents)}, the estimated total ${cents(estimateMicrocents)}; charged so far ${formatCents(settledMicrocents, 4)} cents.`,
2471
- `Call this tool again with the same arguments and a maxCostCents above ${cents(ceilingMicrocents)} to continue.`
2472
- );
2473
- }
2474
- async function authorizationStop(call, step, auth, entryCount) {
2475
- const charged = `Charged so far: ${formatCents(auth.settledMicrocents, 4)} cents.`;
2476
- const chunks = entryCount === void 0 ? "its chunks" : `its ${entryCount} chunks`;
2477
- const stop = (detail, next) => stopped(call, step, `${detail} ${charged}`, next);
2478
- const resume = `call this tool again with the same arguments and maxCostCents of at least ${auth.estimateMicrocents === null ? "the estimate" : formatCents(auth.estimateMicrocents, 2)} to resume`;
2479
- switch (auth.state) {
2480
- case "quota_paused":
2481
- return stop(
2482
- `the import was paused before storing ${chunks}.`,
2483
- `${resume[0]?.toUpperCase()}${resume.slice(1)}.`
2484
- );
2485
- case "limit_exceeded":
2486
- return stop(
2487
- "the document exceeds the hosted limits for extracted text or chunks.",
2488
- `Call with cancel: true to abandon job ${call.intent.jobId}, then import the document in smaller parts.`
2489
- );
2490
- case "cancel_requested":
2491
- return stop(
2492
- "the job is being cancelled.",
2493
- "Call this tool with cancel: true to confirm the cancellation."
2494
- );
2495
- case "cancelled":
2496
- case "expired":
2497
- case "failed":
2498
- return deliver(call, stop(`the job is ${auth.state}.`, NEW_IMPORT));
2499
- }
2500
- if (auth.authorized) return void 0;
2501
- if (auth.estimateMicrocents === null) {
2502
- return stop("the server has no cost estimate for the job yet.", RESUME);
2503
- }
2504
- const estimate = formatCents(auth.estimateMicrocents, 2);
2505
- const ceiling = auth.ceilingMicrocents === null ? "" : ` The current ceiling is ${formatCents(auth.ceilingMicrocents, 2)} cents.`;
2506
- return stop(
2507
- `storing ${chunks} needs your approval: the estimated cost is ${estimate} cents.${ceiling}`,
2508
- `Call this tool again with the same arguments and maxCostCents of at least ${estimate} to continue.`
2509
- );
2510
- }
2511
- async function commitPages(call, job, manifest) {
2512
- const committed = new Set(job.committedPages);
2513
- for (const page2 of manifest.pages) {
2514
- if (committed.has(page2.pageIndex)) continue;
2515
- const commit = await post(
2516
- call,
2517
- "commit",
2518
- `/${call.intent.jobId}/commit`,
2519
- { manifestSha256: manifest.manifestSha256, page: page2 },
2520
- parseHostedFileIngestCommitResult
2521
- );
2522
- if (!commit.ok) return commit.result;
2523
- }
2524
- return void 0;
2525
- }
2526
- async function finish(call, job, maxCostMicrocents) {
2527
- const manifest = job.manifest;
2528
- if (!manifest) return stopped(call, "prepare", "the prepared job has no manifest");
2529
- const auth = await post(
2530
- call,
2531
- "authorize",
2532
- `/${call.intent.jobId}/authorize`,
2533
- maxCostMicrocents === void 0 ? {} : { maxCostMicrocents },
2534
- parseHostedFileIngestAuthorization
2535
- );
2536
- if (!auth.ok) return auth.result;
2537
- await record(call, auth.value.state);
2538
- const halted = await authorizationStop(call, "authorize", auth.value, manifest.entryCount) ?? await commitPages(call, job, manifest);
2539
- if (halted) return halted;
2540
- const publish = await post(
2541
- call,
2542
- "publish",
2543
- `/${call.intent.jobId}/publish`,
2544
- { manifestSha256: manifest.manifestSha256, entryCount: manifest.entryCount },
2545
- parseHostedFileIngestPublishResult
2546
- );
2547
- if (!publish.ok) return publish.result;
2548
- await record(call, "published");
2549
- return deliver(call, published(call, publish.value));
2550
- }
2551
- async function ended(call, job) {
2552
- if (job.state === "published") {
2553
- return job.publication ? deliver(call, published(call, job.publication)) : stopped(call, "begin", "the published job has no publication result");
2554
- }
2555
- const what = job.state === "failed" ? `failed: ${job.error ? `${job.error.message} (${job.error.code})` : "no reason given"}` : `is ${job.state}.`;
2556
- return deliver(call, mcpText(`Import of ${label(call.intent)} ${what}
2557
- ${NEW_IMPORT}`, true));
2558
- }
2559
- async function ingest(call, maxCostMicrocents) {
2560
- const { fingerprint, jobId } = call.intent;
2561
- let next = await post(
2562
- call,
2563
- "begin",
2564
- "",
2565
- {
2566
- jobId,
2567
- filename: call.filename,
2568
- byteLength: fingerprint.byteLength,
2569
- sha256: fingerprint.sha256,
2570
- ...fingerprint.description === null ? {} : { description: fingerprint.description }
2571
- },
2572
- parseHostedFileIngestJobView
2573
- );
2574
- while (next.ok) {
2575
- const job = next.value;
2576
- await record(call, job.state);
2577
- switch (job.state) {
2578
- case "awaiting_upload":
2579
- next = await upload(call, job);
2580
- break;
2581
- case "uploaded":
2582
- next = await post(call, "prepare", `/${jobId}/prepare`, {}, parseHostedFileIngestJobView);
2583
- break;
2584
- case "prepared":
2585
- return finish(call, job, maxCostMicrocents);
2586
- default:
2587
- return ended(call, job);
2588
- }
2589
- }
2590
- return next.result;
2591
- }
2592
- async function cancel(call) {
2593
- const res = await post(
2594
- call,
2595
- "cancel",
2596
- `/${call.intent.jobId}/cancel`,
2597
- {},
2598
- parseHostedFileIngestAuthorization
2599
- );
2600
- if (!res.ok) {
2601
- if (res.status !== 404) return res.result;
2602
- return deliver(
2603
- call,
2604
- mcpText(`The server has no ${label(call.intent)}; nothing to cancel.
2605
- ${NEW_IMPORT}`, true)
2606
- );
2607
- }
2608
- const { state, settledMicrocents } = res.value;
2609
- await record(call, state);
2610
- const charged = `Charged: ${formatCents(settledMicrocents, 4)} cents.`;
2611
- switch (state) {
2612
- case "cancelled":
2613
- return deliver(
2614
- call,
2615
- mcpText(`Cancelled ${label(call.intent)}. ${charged}
2616
- ${NEW_IMPORT}`, true)
2617
- );
2618
- case "published":
2619
- case "expired":
2620
- case "failed":
2621
- return deliver(
2622
- call,
2623
- mcpText(
2624
- `${label(call.intent)} was already ${state}; nothing to cancel. ${charged}
2625
- ${NEW_IMPORT}`,
2626
- true
2627
- )
2628
- );
2629
- default:
2630
- return mcpText(
2631
- `Cancel of ${label(call.intent)} is ${state}: it stops once outstanding work settles. ${charged}
2632
- Call this tool with cancel: true again to confirm.`,
2633
- true
2634
- );
2635
- }
2636
- }
2637
- async function openIntent(descriptor, args, existing) {
2638
- const size = await stat(args.path).then(
2639
- (info) => info.size,
2640
- (err) => err
2641
- );
2642
- if (existing) return matchIntent(existing, args, size);
2643
- if (typeof size !== "number") return refuse(`Failed to read ${args.path}: ${size.message}.`);
2644
- if (size === 0) return refuse(`${args.path} is empty.`);
2645
- if (size > descriptor.maxBytes) {
2646
- return refuse(`File too large: ${size} bytes > ${descriptor.maxBytes} cap.`);
2647
- }
2648
- try {
2649
- requireHostedFileIngestFilename(basename(args.path), "the file name");
2650
- } catch (err) {
2651
- return refuse(`Cannot import ${args.path}: ${errorText(err)}.`);
2652
- }
2653
- const { byteLength, sha256 } = await hashFile(args.path);
2654
- const now = (/* @__PURE__ */ new Date()).toISOString();
2655
- const intent = {
2656
- version: 1,
2657
- jobId: randomUUID4(),
2658
- endpoint: origin,
2659
- fingerprint: {
2660
- path: args.path,
2661
- byteLength,
2662
- sha256,
2663
- description: args.description,
2664
- namespaceId: args.namespaceId
2665
- },
2666
- lastState: null,
2667
- delivered: false,
2668
- createdAt: now,
2669
- updatedAt: now
2670
- };
2671
- await intents.save(intent);
2672
- return { ok: true, value: intent };
2673
- }
2674
- return async (descriptor, rawArgs, signal) => {
2675
- const args = parseArgs(rawArgs);
2676
- if (typeof args === "string") return mcpText(args, true);
2677
- let intent;
2678
- const callOn = (value) => ({
2679
- descriptor,
2680
- intent: value,
2681
- filename: basename(value.fingerprint.path),
2682
- signal,
2683
- retryUntil: Date.now() + RETRY_BUDGET_MS
2684
- });
2685
- try {
2686
- await guard();
2687
- const existing = await intents.findUndelivered(origin, args.path, args.namespaceId);
2688
- if (args.cancel) {
2689
- if (!existing) return mcpText(`No hosted import of ${args.path} to cancel.`, true);
2690
- intent = existing;
2691
- return await cancel(callOn(intent));
2692
- }
2693
- const opened = await openIntent(descriptor, args, existing);
2694
- if (!opened.ok) return opened.result;
2695
- intent = opened.value;
2696
- return await ingest(callOn(intent), args.maxCostMicrocents);
2697
- } catch (err) {
2698
- return mcpText(intent ? `${errorText(err)}
2699
- job ${intent.jobId}` : errorText(err), true);
2700
- }
2701
- };
2702
- }
2703
-
2704
- // src/mcp/tools/shared.ts
2705
- var LOCAL_UPLOAD_META_KEY = "pyx.dev/local-upload";
2706
-
2707
- // src/mcp/proxy-server.ts
2708
- var REMOTE_MCP_PATH = "/mcp";
2709
- function createMcpIdempotentFetch(fetchImpl = fetch, sessionNonce = randomUUID5()) {
2710
- const wrapped = async (input, init) => {
2711
- const upstream = input instanceof Request ? input : new Request(String(input), init);
2712
- const method = init?.method ?? upstream.method;
2713
- const body = init?.body ?? (input instanceof Request ? await input.clone().text() : void 0);
2714
- const headers = new Headers(init?.headers ?? upstream.headers);
2715
- if (method.toUpperCase() === "POST" && !headers.has("Idempotency-Key")) {
2716
- if (typeof body !== "string") {
2717
- throw new Error("remote MCP POST body must be text for stable request idempotency");
2718
- }
2719
- const key = createHash5("sha256").update("pyx-mcp-idempotency-v1\0").update(sessionNonce).update("\0").update(body).digest("hex");
2720
- headers.set("Idempotency-Key", `mcp-${key}`);
2721
- }
2722
- return fetchImpl(input, { ...init, method, headers, body });
2723
- };
2724
- return Object.assign(wrapped, { preconnect: fetch.preconnect });
2725
- }
2726
- function resultText(result) {
2727
- return result.content.map((c) => c.type === "text" ? c.text : "").join(" ").trim();
2728
- }
2729
- function createCredentialGuard(readCredentials, snapshot2) {
2730
- return async () => {
2731
- const read = await readCredentials();
2732
- if (!read.ok) {
2733
- throw new Error(
2734
- `${resultText(read.result) || "pyx-memory credentials are unavailable. Run: pyx-mem login"}
2735
- This attempt was not sent. Then reconnect the pyx-memory MCP server.`
2736
- );
2737
- }
2738
- if (read.credentials.endpoint !== snapshot2.endpoint || read.credentials.apiKey !== snapshot2.apiKey) {
2739
- throw new Error(
2740
- "pyx-memory credentials changed since this MCP session started (different endpoint or account). This attempt was not sent. Reconnect the pyx-memory MCP server to use the current sign-in."
2741
- );
2742
- }
2743
- };
2744
- }
2745
- function createGuardedFetch(guard, fetchImpl = fetch) {
2746
- const wrapped = async (input, init) => {
2747
- await guard();
2748
- return fetchImpl(input, init);
2749
- };
2750
- return Object.assign(wrapped, { preconnect: fetch.preconnect });
2751
- }
2752
- var RETRYABLE_MCP_METHODS = /* @__PURE__ */ new Set([
2753
- "initialize",
2754
- "notifications/initialized",
2755
- "tools/list",
2756
- "prompts/list"
2757
- ]);
2758
- var MCP_RETRY_BUDGET_MS = 2e4;
2759
- var MIN_RESEND_SPACING_MS = 1e3;
2760
- function isRetryableMcpMessage(input, init) {
2761
- const method = init?.method ?? (input instanceof Request ? input.method : "GET");
2762
- if (method.toUpperCase() !== "POST" || typeof init?.body !== "string") return false;
2763
- let message;
2764
- try {
2765
- message = JSON.parse(init.body);
2766
- } catch {
2767
- return false;
2768
- }
2769
- if (typeof message !== "object" || message === null) return false;
2770
- const { method: rpc } = message;
2771
- return typeof rpc === "string" && RETRYABLE_MCP_METHODS.has(rpc);
2772
- }
2773
- function resendDelayMs(res, deadline) {
2774
- if (res.status !== 429 && res.status !== 503) return void 0;
2775
- const retryAfterMs2 = parseRetryAfterMs(res.headers.get("retry-after"));
2776
- if (retryAfterMs2 === void 0) return void 0;
2777
- const waitMs = Math.max(retryAfterMs2, MIN_RESEND_SPACING_MS);
2778
- return Date.now() + waitMs > deadline ? void 0 : waitMs;
2779
- }
2780
- function createMcpRetryAfterFetch(fetchImpl = fetch, retryUntil = () => Date.now() + MCP_RETRY_BUDGET_MS) {
2781
- const wrapped = async (input, init) => {
2782
- if (!isRetryableMcpMessage(input, init)) return fetchImpl(input, init);
2783
- const deadline = retryUntil();
2784
- const signal = init?.signal ?? (input instanceof Request ? input.signal : void 0);
2785
- for (; ; ) {
2786
- const res = await fetchImpl(input, init);
2787
- const waitMs = resendDelayMs(res, deadline);
2788
- if (waitMs === void 0) return res;
2789
- await res.body?.cancel();
2790
- await delay2(waitMs, void 0, { signal });
2791
- }
2792
- };
2793
- return Object.assign(wrapped, { preconnect: fetch.preconnect });
2794
- }
2795
- async function connectRemoteMcp(client, url, apiKey, fetchImpl, budgetMs = MCP_RETRY_BUDGET_MS) {
2796
- const connectUntil = Date.now() + budgetMs;
2797
- let connecting = true;
2798
- const transport = new StreamableHTTPClientTransport(url, {
2799
- requestInit: { headers: { Authorization: `Bearer ${apiKey}` } },
2800
- fetch: createMcpIdempotentFetch(
2801
- createMcpRetryAfterFetch(
2802
- fetchImpl,
2803
- () => connecting ? connectUntil : Date.now() + budgetMs
2804
- )
2805
- )
2806
- });
2807
- try {
2808
- await client.connect(transport);
2809
- } finally {
2810
- connecting = false;
2811
- }
2812
- }
2813
- function extractLocalUpload(meta) {
2814
- if (meta === null || typeof meta !== "object") return void 0;
2815
- const descriptor = meta[LOCAL_UPLOAD_META_KEY];
2816
- if (descriptor === null || typeof descriptor !== "object") return void 0;
2817
- const obj = descriptor;
2818
- if (typeof obj.pathArg !== "string" || typeof obj.endpoint !== "string" || typeof obj.fileField !== "string") {
2819
- return void 0;
2820
- }
2821
- if (!obj.endpoint.startsWith("/") || obj.endpoint.startsWith("//")) {
2822
- return void 0;
2823
- }
2824
- return obj;
2825
- }
2826
- async function readCappedLocalFile(rawPath, descriptor) {
2827
- if (typeof rawPath !== "string" || rawPath.length === 0) {
2828
- return {
2829
- ok: false,
2830
- result: mcpText(`\`${descriptor.pathArg}\` must be a non-empty local file path.`, true)
2831
- };
2832
- }
2833
- const abs = isAbsolute2(rawPath) ? rawPath : resolve2(rawPath);
2834
- try {
2835
- if (typeof descriptor.maxBytes === "number") {
2836
- const info = await stat2(abs);
2837
- if (info.size > descriptor.maxBytes) {
2838
- return {
2839
- ok: false,
2840
- result: mcpText(`File too large: ${info.size} bytes > ${descriptor.maxBytes} cap.`, true)
2841
- };
2842
- }
2843
- }
2844
- return { ok: true, bytes: await readFile2(abs), name: basename2(abs) };
2845
- } catch (err) {
2846
- return {
2847
- ok: false,
2848
- result: mcpText(
2849
- `Failed to read ${abs}: ${err instanceof Error ? err.message : String(err)}.`,
2850
- true
2851
- )
2852
- };
2853
- }
2854
- }
2855
- function createLocalFileUploader(credentials, readCredentials, fetchImpl = fetch) {
2856
- const guard = createCredentialGuard(readCredentials, credentials);
2857
- return async (descriptor, args) => {
2858
- try {
2859
- await guard();
2860
- const file = await readCappedLocalFile(args[descriptor.pathArg], descriptor);
2861
- if (!file.ok) return file.result;
2862
- const http = createHttpClient(credentials, fetchImpl);
2863
- const res = await http.requestMultipart({
2864
- path: descriptor.endpoint,
2865
- formData: async () => {
2866
- await guard();
2867
- const form = new FormData();
2868
- form.set(descriptor.fileField, new File([new Uint8Array(file.bytes)], file.name));
2869
- for (const key of descriptor.optionalArgs ?? []) {
2870
- const value = args[key];
2871
- if (typeof value === "string" && value.length > 0) form.set(key, value);
2872
- }
2873
- return form;
2874
- }
2875
- });
2876
- return res.ok ? mcpJson(res.data) : res.result;
2877
- } catch (err) {
2878
- return mcpText(err instanceof Error ? err.message : String(err), true);
2879
- }
2880
- };
2881
- }
2882
- function createProxyServer(client, version, uploadLocalFile, hostedFileIngest) {
2883
- const caps = client.getServerCapabilities();
2884
- const server = new Server(client.getServerVersion() ?? { name: "pyx-memory", version }, {
2885
- capabilities: {
2886
- // Advertise the PRESENCE of tools/prompts, but NOT their `listChanged`
2887
- // sub-capability: this proxy does not relay the remote's list_changed
2888
- // notifications, so claiming listChanged would be a capability the host
2889
- // could rely on but we never honor (Codex: keep the proxy transparent).
2890
- ...caps?.tools ? { tools: {} } : {},
2891
- ...caps?.prompts ? { prompts: {} } : {}
2892
- },
2893
- instructions: client.getInstructions()
2894
- });
2895
- const bridges = /* @__PURE__ */ new Map();
2896
- const bridging = uploadLocalFile !== void 0 || hostedFileIngest !== void 0;
2897
- const bridgeOf = (meta) => {
2898
- const hosted = hostedFileIngest ? parseHostedFileIngestDescriptor(meta) : void 0;
2899
- if (hosted) return { kind: "hosted", descriptor: hosted };
2900
- const local = uploadLocalFile ? extractLocalUpload(meta) : void 0;
2901
- return local ? { kind: "local", descriptor: local } : void 0;
2902
- };
2903
- let listedOnce = false;
2904
- let listingsStarted = 0;
2905
- let listingApplied = 0;
2906
- const listAndRefresh = async (params) => {
2907
- const listing = ++listingsStarted;
2908
- const result = await client.listTools(params);
2909
- const { tools, nextCursor } = result;
2910
- if (!Array.isArray(tools) || listing < listingApplied) return result;
2911
- listingApplied = listing;
2912
- listedOnce = true;
2913
- if (params?.cursor === void 0 && nextCursor === void 0) bridges.clear();
2914
- for (const tool of tools) {
2915
- const bridge = bridgeOf(tool._meta);
2916
- if (bridge) bridges.set(tool.name, bridge);
2917
- else bridges.delete(tool.name);
2918
- }
2919
- return result;
2920
- };
2921
- if (caps?.tools) {
2922
- server.setRequestHandler(ListToolsRequestSchema, (req) => listAndRefresh(req.params));
2923
- server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
2924
- if (bridging && !listedOnce) {
2925
- try {
2926
- await listAndRefresh();
2927
- } catch {
2928
- }
2929
- }
2930
- const bridge = bridges.get(req.params.name);
2931
- const args = req.params.arguments ?? {};
2932
- if (bridge?.kind === "hosted" && hostedFileIngest) {
2933
- return await hostedFileIngest(bridge.descriptor, args, extra.signal);
2934
- }
2935
- if (bridge?.kind === "local" && uploadLocalFile) {
2936
- return await uploadLocalFile(bridge.descriptor, args);
2937
- }
2938
- return client.callTool(req.params);
2939
- });
2940
- }
2941
- if (caps?.prompts) {
2942
- server.setRequestHandler(
2943
- ListPromptsRequestSchema,
2944
- (req) => client.listPrompts(req.params)
2945
- );
2946
- server.setRequestHandler(
2947
- GetPromptRequestSchema,
2948
- (req) => client.getPrompt(req.params)
2949
- );
2950
- }
2951
- return server;
2952
- }
2953
- async function runMcpProxyServer(opts) {
2954
- const version = opts.version ?? (true ? "1.26.6" : "0.0.0-dev");
2955
- const read = await opts.readCredentials();
2956
- if (!read.ok) {
2957
- throw new Error(
2958
- resultText(read.result) || "pyx-memory credentials are unavailable. Run: pyx-mem login"
2959
- );
2960
- }
2961
- const { endpoint, apiKey } = read.credentials;
2962
- const url = new URL(`${endpoint.replace(/\/+$/, "")}${REMOTE_MCP_PATH}`);
2963
- const guard = createCredentialGuard(opts.readCredentials, read.credentials);
2964
- const client = new McpClient({ name: "pyx-mem-proxy", version }, { capabilities: {} });
2965
- try {
2966
- await connectRemoteMcp(client, url, apiKey, createGuardedFetch(guard));
2967
- } catch (err) {
2968
- const msg = err instanceof Error ? err.message : String(err);
2969
- throw new Error(
2970
- `Could not reach the pyx-memory remote MCP at ${url.href}: ${msg}. Verify the endpoint and key with \`pyx-mem doctor\`.`
2971
- );
2972
- }
2973
- try {
2974
- const server = createProxyServer(
2975
- client,
2976
- version,
2977
- createLocalFileUploader(read.credentials, opts.readCredentials),
2978
- createHostedFileIngestDriver(read.credentials, guard, createFileIngestIntentStore())
2979
- );
2980
- const transport = new StdioServerTransport();
2981
- const closed = new Promise((resolve4) => {
2982
- transport.onclose = () => resolve4();
2983
- });
2984
- process.stdin.once("end", () => void transport.close());
2985
- await server.connect(transport);
2986
- void opts.onConnected?.(client);
2987
- await closed;
2988
- } finally {
2989
- await client.close().catch(() => {
2990
- });
2991
- }
2992
- }
2993
-
2994
- // src/cli/commands/mcp-install.ts
2995
- import { spawnSync as nodeSpawnSync } from "child_process";
2996
- import { existsSync as existsSync3 } from "fs";
2997
- import { homedir as homedir3 } from "os";
2998
- import { join as join3 } from "path";
2999
-
3000
- // src/cli/commands/mcp-install-merge.ts
3001
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
3002
- import { dirname as dirname2 } from "path";
3003
- function mergeWriteJsonMcpEntry(opts) {
3004
- const { filePath, serverName, entry } = opts;
3005
- let existingDoc = {};
3006
- if (existsSync(filePath)) {
3007
- const raw = readFileSync(filePath, "utf8").trim();
3008
- if (raw.length > 0) {
3009
- try {
3010
- const parsed = JSON.parse(raw);
3011
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
3012
- return {
3013
- exitCode: EXIT.USAGE,
3014
- status: "parse-error",
3015
- message: `Refusing to write: ${filePath} is not a JSON object at its root.
3016
- Fix the file manually, then re-run.
3017
- `
3018
- };
3019
- }
3020
- existingDoc = parsed;
3021
- } catch (err) {
3022
- const reason = err instanceof Error ? err.message : String(err);
3023
- return {
3024
- exitCode: EXIT.USAGE,
3025
- status: "parse-error",
3026
- message: `Refusing to write: ${filePath} contains invalid JSON (${reason}).
3027
- Fix the file manually, then re-run.
3028
- `
3029
- };
3030
- }
3031
- }
3032
- }
3033
- const rawServers = existingDoc.mcpServers;
3034
- const servers = rawServers !== null && typeof rawServers === "object" && !Array.isArray(rawServers) ? { ...rawServers } : {};
3035
- const existingEntry = servers[serverName];
3036
- if (existingEntry !== void 0) {
3037
- if (entriesEqual(existingEntry, entry)) {
3038
- return {
3039
- exitCode: EXIT.OK,
3040
- status: "already-installed",
3041
- message: `pyx-memory is already installed in ${filePath} (no changes written).
3042
- `
3043
- };
3044
- }
3045
- return {
3046
- exitCode: EXIT.USAGE,
3047
- status: "conflict",
3048
- message: `Refusing to overwrite existing \`${serverName}\` entry in ${filePath}.
3049
-
3050
- Found:
3051
- ${indent(JSON.stringify(existingEntry, null, 2))}
3052
-
3053
- Proposed:
3054
- ${indent(JSON.stringify(entry, null, 2))}
3055
-
3056
- Remove or rename the existing entry, then re-run.
3057
- `
3058
- };
3059
- }
3060
- servers[serverName] = entry;
3061
- const nextDoc = { ...existingDoc, mcpServers: servers };
3062
- const payload = `${JSON.stringify(nextDoc, null, 2)}
3063
- `;
3064
- const parent = dirname2(filePath);
3065
- if (!existsSync(parent)) {
3066
- mkdirSync(parent, { recursive: true });
3067
- }
3068
- const tmpPath = `${filePath}.tmp`;
3069
- writeFileSync(tmpPath, payload, { mode: 384 });
3070
- renameSync(tmpPath, filePath);
3071
- return {
3072
- exitCode: EXIT.OK,
3073
- status: "installed",
3074
- message: `Installed \`${serverName}\` MCP server entry in ${filePath}.
3075
- `
3076
- };
3077
- }
3078
- function entriesEqual(a, b) {
3079
- if (a === null || typeof a !== "object") return false;
3080
- const left = a;
3081
- if (left.command !== b.command) return false;
3082
- if (!arraysEqual(left.args, b.args)) return false;
3083
- if (!recordsEqual(left.env, b.env)) return false;
3084
- return true;
3085
- }
3086
- function arraysEqual(a, b) {
3087
- const left = Array.isArray(a) ? a : void 0;
3088
- const right = b;
3089
- if (left === void 0 && right === void 0) return true;
3090
- if (left === void 0 || right === void 0)
3091
- return (left ?? []).length === 0 && (right ?? []).length === 0;
3092
- if (left.length !== right.length) return false;
3093
- for (let i = 0; i < left.length; i++) {
3094
- if (left[i] !== right[i]) return false;
3095
- }
3096
- return true;
3097
- }
3098
- function recordsEqual(a, b) {
3099
- const left = a !== null && typeof a === "object" && !Array.isArray(a) ? a : void 0;
3100
- const right = b;
3101
- if (left === void 0 && right === void 0) return true;
3102
- const leftKeys = left ? Object.keys(left) : [];
3103
- const rightKeys = right ? Object.keys(right) : [];
3104
- if (leftKeys.length === 0 && rightKeys.length === 0) return true;
3105
- if (leftKeys.length !== rightKeys.length) return false;
3106
- for (const k of leftKeys) {
3107
- if (left?.[k] !== right?.[k]) return false;
3108
- }
3109
- return true;
3110
- }
3111
- function indent(text2) {
3112
- return text2.split("\n").map((line) => ` ${line}`).join("\n");
3113
- }
3114
-
3115
- // src/cli/commands/memory-rules-merge.ts
3116
- import { randomUUID as randomUUID6 } from "crypto";
3117
- import {
3118
- chmodSync,
3119
- existsSync as existsSync2,
3120
- linkSync,
3121
- lstatSync,
3122
- mkdirSync as mkdirSync2,
3123
- readFileSync as readFileSync2,
3124
- realpathSync,
3125
- renameSync as renameSync2,
3126
- rmSync,
3127
- statSync,
3128
- writeFileSync as writeFileSync2
3129
- } from "fs";
3130
- import { dirname as dirname3 } from "path";
3131
- var RULES_BEGIN_MARKER = "<!-- pyx-memory:begin (managed by `pyx-mem mcp install`; edit outside these markers) -->";
3132
- var RULES_END_MARKER = "<!-- pyx-memory:end -->";
3133
- var BEGIN_MARKER_PREFIX = "<!-- pyx-memory:begin";
3134
- var PM_HEADING_RE = /^ {0,3}##[ \t]+Persistent Memory[ \t]*$/;
3135
- var H1_OR_H2_RE = /^ {0,3}#{1,2}[ \t]+\S/;
3136
- var FENCE_RE = /^ {0,3}(`{3,}|~{3,})/;
3137
- var CLOSING_FENCE_RE = /^ {0,3}(`{3,}|~{3,})[ \t]*$/;
3138
- var BEGIN_MARKER_RE = /^ {0,3}<!-- pyx-memory:begin(?:[ \t][^>]*)?-->[ \t]*$/;
3139
- var END_MARKER_RE = /^ {0,3}<!-- pyx-memory:end -->[ \t]*$/;
3140
- function isBeginMarker(line) {
3141
- return BEGIN_MARKER_RE.test(line);
3142
- }
3143
- function isEndMarker(line) {
3144
- return END_MARKER_RE.test(line);
3145
- }
3146
- function fenceMask(lines) {
3147
- const mask = new Array(lines.length).fill(false);
3148
- let openIdx = -1;
3149
- let delim = "";
3150
- let openLen = 0;
3151
- for (let i = 0; i < lines.length; i++) {
3152
- const fence = FENCE_RE.exec(lines[i] ?? "")?.[1];
3153
- if (!fence) continue;
3154
- if (openIdx === -1) {
3155
- openIdx = i;
3156
- delim = fence.charAt(0);
3157
- openLen = fence.length;
3158
- continue;
3159
- }
3160
- if (CLOSING_FENCE_RE.test(lines[i] ?? "") && fence.charAt(0) === delim && fence.length >= openLen) {
3161
- for (let j = openIdx; j <= i; j++) mask[j] = true;
3162
- openIdx = -1;
3163
- delim = "";
3164
- openLen = 0;
3165
- }
3166
- }
3167
- return mask;
3168
- }
3169
- function nextH1OrH2(lines, mask, from) {
3170
- for (let i = from; i < lines.length; i++) {
3171
- const line = lines[i];
3172
- if (line !== void 0 && !mask[i] && H1_OR_H2_RE.test(line)) return i;
3173
- }
3174
- return lines.length;
3175
- }
3176
- function findCompleteManagedRange(lines, mask) {
3177
- let begin = -1;
3178
- for (let i = 0; i < lines.length; i++) {
3179
- const line = lines[i];
3180
- if (line === void 0 || mask[i]) continue;
3181
- if (isBeginMarker(line)) begin = i;
3182
- else if (begin !== -1 && isEndMarker(line)) return [begin, i + 1];
3183
- }
3184
- return null;
3185
- }
3186
- function findBeginLines(lines, mask) {
3187
- const found = [];
3188
- for (let i = 0; i < lines.length; i++) {
3189
- const line = lines[i];
3190
- if (line !== void 0 && !mask[i] && isBeginMarker(line)) found.push(i);
3191
- }
3192
- return found;
3193
- }
3194
- function findLegacyPyxRange(lines, mask) {
3195
- for (let i = 0; i < lines.length; i++) {
3196
- const line = lines[i];
3197
- if (line === void 0 || mask[i] || !PM_HEADING_RE.test(line)) continue;
3198
- const end = nextH1OrH2(lines, mask, i + 1);
3199
- const own = lines.slice(i, end).filter((l, k) => !mask[i + k] && !isBeginMarker(l) && !isEndMarker(l));
3200
- if (own.join("\n").toLowerCase().includes("pyx-memory")) return [i, end];
3201
- }
3202
- return null;
3203
- }
3204
- function spliceBlock(lines, from, toExclusive, blockLines) {
3205
- const before = lines.slice(0, from);
3206
- const after = lines.slice(toExclusive);
3207
- while (before.length && (before[before.length - 1] ?? "").trim() === "") before.pop();
3208
- while (after.length && (after[0] ?? "").trim() === "") after.shift();
3209
- const out = [...before];
3210
- if (out.length) out.push("");
3211
- out.push(...blockLines);
3212
- if (after.length) out.push("");
3213
- out.push(...after);
3214
- return out;
3215
- }
3216
- function joinWithTrailingNewline(lines, eol) {
3217
- const body = lines.join(eol);
3218
- return body.endsWith(eol) ? body : `${body}${eol}`;
3219
- }
3220
- var BOM = "\uFEFF";
3221
- function mergeMemoryRules(existing, section) {
3222
- const bom = existing.startsWith(BOM) ? BOM : "";
3223
- const merged = mergeBody(existing.slice(bom.length), section);
3224
- return { ...merged, content: bom + merged.content };
3225
- }
3226
- function mergeBody(existing, section) {
3227
- if (section.includes(BEGIN_MARKER_PREFIX) || section.includes(RULES_END_MARKER)) {
3228
- throw new Error("pyx-memory rules section must not contain the managed markers.");
3229
- }
3230
- const eol = existing.includes("\r\n") ? "\r\n" : "\n";
3231
- const blockLines = [RULES_BEGIN_MARKER, ...section.split(/\r?\n/), RULES_END_MARKER];
3232
- const normalized = existing.replace(/\r\n/g, "\n");
3233
- if (normalized.trim().length === 0) {
3234
- return { content: joinWithTrailingNewline(blockLines, eol), action: "created" };
3235
- }
3236
- let lines = normalized.split("\n");
3237
- let mask = fenceMask(lines);
3238
- const complete = findCompleteManagedRange(lines, mask);
3239
- if (complete) {
3240
- const next2 = spliceBlock(lines, complete[0], complete[1], blockLines);
3241
- return { content: joinWithTrailingNewline(next2, eol), action: "updated" };
3242
- }
3243
- const orphans = new Set(findBeginLines(lines, mask));
3244
- if (orphans.size > 0) {
3245
- lines = lines.filter((_, i) => !orphans.has(i));
3246
- mask = fenceMask(lines);
3247
- }
3248
- const legacy = findLegacyPyxRange(lines, mask);
3249
- if (legacy) {
3250
- const next2 = spliceBlock(lines, legacy[0], legacy[1], blockLines);
3251
- return { content: joinWithTrailingNewline(next2, eol), action: "updated" };
3252
- }
3253
- const next = spliceBlock(lines, lines.length, lines.length, blockLines);
3254
- return { content: joinWithTrailingNewline(next, eol), action: "appended" };
3255
- }
3256
- function refreshMemoryRules(file, section) {
3257
- const bom = file.startsWith(BOM) ? BOM : "";
3258
- const refreshed = refreshBody(file.slice(bom.length), section);
3259
- return refreshed === null ? null : bom + refreshed;
3260
- }
3261
- function refreshBody(existing, section) {
3262
- const lines = existing.replace(/\r\n/g, "\n").split("\n");
3263
- const complete = findCompleteManagedRange(lines, fenceMask(lines));
3264
- if (!complete) return null;
3265
- const [first, last] = [complete[0], complete[1] - 1];
3266
- const starts = [0];
3267
- for (let i = 0; i < existing.length; i++) if (existing[i] === "\n") starts.push(i + 1);
3268
- const from = starts[first] ?? 0;
3269
- const lastStart = starts[last] ?? 0;
3270
- const to = lastStart + (lines[last] ?? "").length;
3271
- const eol = existing.slice(from).match(/\r?\n/)?.[0] ?? "\n";
3272
- const block = [RULES_BEGIN_MARKER, ...section.split(/\r?\n/), RULES_END_MARKER].join(eol);
3273
- return existing.slice(0, from) + block + existing.slice(to);
3274
- }
3275
- function changedDuringWrite(filePath) {
3276
- return new Error(`${filePath} changed while updating the pyx-memory rules; retry.`);
3277
- }
3278
- function readUtf8Strict(path) {
3279
- try {
3280
- return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(readFileSync2(path));
3281
- } catch (err) {
3282
- if (err instanceof TypeError) {
3283
- throw new Error(`${path} is not valid UTF-8; the pyx-memory rules were not changed.`);
3284
- }
3285
- throw err;
3286
- }
3287
- }
3288
- function writeMemoryRulesFile(opts) {
3289
- const { filePath, section } = opts;
3290
- const fileExists = existsSync2(filePath);
3291
- if (!fileExists && lstatSync(filePath, { throwIfNoEntry: false })?.isSymbolicLink()) {
3292
- throw new Error(`${filePath} is a broken link; fix or remove it, then retry.`);
3293
- }
3294
- const target = fileExists ? realpathSync(filePath) : filePath;
3295
- const existing = fileExists ? readUtf8Strict(target) : "";
3296
- let merged = null;
3297
- if (!opts.refreshOnly) merged = mergeMemoryRules(existing, section);
3298
- else {
3299
- const content = refreshMemoryRules(existing, section);
3300
- if (content !== null) merged = { content, action: "updated" };
3301
- }
3302
- if (merged === null || merged.content === existing) {
3303
- return {
3304
- action: "unchanged",
3305
- filePath,
3306
- message: `pyx-memory rules already current in ${filePath} (no changes written).
3307
- `
3308
- };
3309
- }
3310
- const parent = dirname3(target);
3311
- if (!existsSync2(parent)) mkdirSync2(parent, { recursive: true });
3312
- const mode = fileExists ? statSync(target).mode & 511 : 420;
3313
- const tmpPath = `${target}.${process.pid}.${randomUUID6()}.tmp`;
3314
- try {
3315
- writeFileSync2(tmpPath, merged.content, { mode });
3316
- chmodSync(tmpPath, mode);
3317
- if (fileExists) {
3318
- if (readUtf8Strict(target) !== existing) throw changedDuringWrite(filePath);
3319
- renameSync2(tmpPath, target);
3320
- } else {
3321
- try {
3322
- linkSync(tmpPath, target);
3323
- } catch (err) {
3324
- if (err.code === "EEXIST") throw changedDuringWrite(filePath);
3325
- throw err;
3326
- }
3327
- rmSync(tmpPath);
3328
- }
3329
- } catch (err) {
3330
- rmSync(tmpPath, { force: true });
3331
- throw err;
3332
- }
3333
- const verb = merged.action === "created" ? "Wrote" : merged.action === "appended" ? "Added" : "Updated";
3334
- return {
3335
- action: merged.action,
3336
- filePath,
3337
- message: `${verb} the pyx-memory rules in ${filePath}.
3338
- `
3339
- };
3340
- }
3341
-
3342
- // src/cli/commands/mcp-install.ts
3343
- var VALID_SCOPES = /* @__PURE__ */ new Set(["local", "user", "project"]);
3344
- var SERVER_NAME = "pyx-memory";
3345
- function pyxMemArgs(remote) {
3346
- return remote ? ["mcp", "--remote"] : ["mcp"];
3347
- }
3348
- function mcpEntry(remote) {
3349
- return { command: "pyx-mem", args: pyxMemArgs(remote) };
3350
- }
3351
- function mcpInstallClaudeCodeCommand(opts = {}) {
3352
- const scope = opts.scope ?? "local";
3353
- if (!validateScope(scope)) return EXIT.USAGE;
3354
- const remote = opts.remote ?? false;
3355
- const spawnSync = opts._spawnSync ?? nodeSpawnSync;
3356
- const probe = spawnSync("claude", ["--version"], { stdio: "pipe" });
3357
- if (probe.error || probe.status !== 0) {
3358
- printClaudeCodeManualInstructions(scope, remote);
3359
- return EXIT.OK;
3360
- }
3361
- const cmd = ["mcp", "add", SERVER_NAME, "-s", scope, "--", "pyx-mem", ...pyxMemArgs(remote)];
3362
- const res = spawnSync("claude", cmd, { stdio: "inherit" });
3363
- if (res.status !== 0) {
3364
- process.stderr.write(`\`claude ${cmd.join(" ")}\` exited with code ${res.status ?? "null"}.
3365
- `);
3366
- printClaudeCodeManualInstructions(scope, remote);
3367
- return EXIT.USAGE;
3368
- }
3369
- process.stdout.write(
3370
- `Installed pyx-memory MCP server in Claude Code (scope: ${scope}).
3371
- Restart Claude Code to make the tools available. No API key was written to .mcp.json \u2014 credentials live in the OS credential store.
3372
- To populate the knowledge graph, YOU pass entities and relationships (or triples) \u2014 a multi-entity store needs at least one connecting edge or it is refused. The server does not extract for you unless a self-host operator configured a BYO extraction endpoint (then store_memory can forward extractEntities:true). Images require caller-provided descriptions/hooks.
3373
- `
3374
- );
3375
- return EXIT.OK;
3376
- }
3377
- function printClaudeCodeManualInstructions(scope, remote) {
3378
- process.stdout.write(
3379
- [
3380
- "The `claude` CLI is not on PATH, or the install command failed.",
3381
- "",
3382
- "Run this once Claude Code is installed:",
3383
- ` claude mcp add ${SERVER_NAME} -s ${scope} -- pyx-mem ${pyxMemArgs(remote).join(" ")}`,
3384
- "",
3385
- "Or add this entry manually to your Claude Code MCP config (.mcp.json):",
3386
- JSON.stringify({ mcpServers: { [SERVER_NAME]: mcpEntry(remote) } }, null, 2),
3387
- "",
3388
- "Do NOT add your pyx-memory API key here. Run `pyx-mem login` to store it in the OS credential store.",
3389
- ""
3390
- ].join("\n")
3391
- );
3392
- }
3393
- function mcpInstallCodexCommand(opts = {}) {
3394
- const scope = opts.scope ?? "user";
3395
- if (!validateScope(scope)) return EXIT.USAGE;
3396
- const remote = opts.remote ?? false;
3397
- const spawnSync = opts._spawnSync ?? nodeSpawnSync;
3398
- const home = opts._homeDir ?? homedir3();
3399
- const codexHome = (opts._env ?? process.env).CODEX_HOME;
3400
- const probe = spawnSync("codex", ["--version"], { stdio: "pipe" });
3401
- if (probe.error || probe.status !== 0) {
3402
- printCodexManualInstructions(home, remote, codexHome);
3403
- return EXIT.OK;
3404
- }
3405
- const cmd = [
3406
- "mcp",
3407
- "add",
3408
- SERVER_NAME,
3409
- ...codexHome ? ["--env", `CODEX_HOME=${codexHome}`] : [],
3410
- "--",
3411
- "pyx-mem",
3412
- ...pyxMemArgs(remote)
3413
- ];
3414
- const res = spawnSync("codex", cmd, { stdio: "inherit" });
3415
- if (res.status !== 0) {
3416
- process.stderr.write(`\`codex ${cmd.join(" ")}\` exited with code ${res.status ?? "null"}.
3417
- `);
3418
- printCodexManualInstructions(home, remote, codexHome);
3419
- return EXIT.USAGE;
3420
- }
3421
- process.stdout.write(
3422
- `Installed pyx-memory MCP server in Codex CLI.
3423
- Restart Codex CLI to make the tools available. No API key was written to config.toml \u2014 credentials live in the OS credential store.
3424
- `
3425
- );
3426
- return EXIT.OK;
3427
- }
3428
- function printCodexManualInstructions(home, remote, codexHome) {
3429
- const configPath = join3(codexHome ?? join3(home, ".codex"), "config.toml");
3430
- process.stdout.write(
3431
- [
3432
- "The `codex` CLI is not on PATH, or the install command failed.",
3433
- "",
3434
- "Run this once Codex CLI is installed:",
3435
- ` codex mcp add ${SERVER_NAME}${codexHome ? ` --env ${JSON.stringify(`CODEX_HOME=${codexHome}`)}` : ""} -- pyx-mem ${pyxMemArgs(remote).join(" ")}`,
3436
- "",
3437
- `Or add this section manually to ${configPath}:`,
3438
- "",
3439
- `[mcp_servers.${SERVER_NAME}]`,
3440
- `command = "pyx-mem"`,
3441
- `args = ${JSON.stringify(pyxMemArgs(remote))}`,
3442
- ...codexHome ? [`env = { CODEX_HOME = ${JSON.stringify(codexHome)} }`] : [],
3443
- "",
3444
- "Do NOT add your pyx-memory API key here. Run `pyx-mem login` to store it in the OS credential store.",
3445
- ""
3446
- ].join("\n")
3447
- );
3448
- }
3449
- function mcpInstallCursorCommand(opts = {}) {
3450
- const scope = opts.scope ?? "user";
3451
- if (!validateScope(scope)) return EXIT.USAGE;
3452
- const home = opts._homeDir ?? homedir3();
3453
- const filePath = scope === "project" ? join3(process.cwd(), ".cursor", "mcp.json") : join3(home, ".cursor", "mcp.json");
3454
- return writeJsonAndReport(filePath, "Cursor", mcpEntry(opts.remote ?? false));
3455
- }
3456
- function mcpInstallClineCommand(opts = {}) {
3457
- const scope = opts.scope ?? "user";
3458
- if (!validateScope(scope)) return EXIT.USAGE;
3459
- const remote = opts.remote ?? false;
3460
- process.stdout.write(
3461
- [
3462
- "Cline MCP config is managed through the Cline UI in VS Code (the file",
3463
- "path differs by OS, VS Code variant, and Cline version).",
3464
- "",
3465
- "1. Open VS Code (or Cursor / Windsurf running the Cline extension).",
3466
- "2. Click the Cline icon in the sidebar.",
3467
- '3. Click "MCP Servers" \u2192 "Configure MCP Servers".',
3468
- `4. Add this entry under "mcpServers":`,
3469
- "",
3470
- JSON.stringify({ mcpServers: { [SERVER_NAME]: mcpEntry(remote) } }, null, 2),
3471
- "",
3472
- "5. Save the file. Cline will reload the MCP servers automatically.",
3473
- "",
3474
- "Do NOT add your pyx-memory API key here. Run `pyx-mem login` to store it in the OS credential store.",
3475
- ""
3476
- ].join("\n")
3477
- );
3478
- return EXIT.OK;
3479
- }
3480
- function mcpInstallContinueCommand(opts = {}) {
3481
- const scope = opts.scope ?? "user";
3482
- if (!validateScope(scope)) return EXIT.USAGE;
3483
- const remote = opts.remote ?? false;
3484
- const home = opts._homeDir ?? homedir3();
3485
- const yamlPath = join3(home, ".continue", "mcpServers", `${SERVER_NAME}.yaml`);
3486
- process.stdout.write(
3487
- [
3488
- "Continue uses YAML config. The recommended way to add an MCP server is a",
3489
- "standalone block file:",
3490
- "",
3491
- ` ${yamlPath}`,
3492
- "",
3493
- "Create that file with these contents:",
3494
- "",
3495
- `name: ${SERVER_NAME}`,
3496
- "version: 0.0.1",
3497
- "schema: v1",
3498
- "mcpServers:",
3499
- ` - name: ${SERVER_NAME}`,
3500
- ` command: pyx-mem`,
3501
- ` args:`,
3502
- ...pyxMemArgs(remote).map((arg) => ` - ${arg}`),
3503
- "",
3504
- "Continue auto-loads new YAML blocks on next chat session \u2014 no restart",
3505
- "needed in most setups.",
3506
- "",
3507
- "Do NOT add your pyx-memory API key here. Run `pyx-mem login` to store it in the OS credential store.",
3508
- ""
3509
- ].join("\n")
3510
- );
3511
- return EXIT.OK;
3512
- }
3513
- function mcpInstallWindsurfCommand(opts = {}) {
3514
- const scope = opts.scope ?? "user";
3515
- if (!validateScope(scope)) return EXIT.USAGE;
3516
- const home = opts._homeDir ?? homedir3();
3517
- const filePath = join3(home, ".codeium", "windsurf", "mcp_config.json");
3518
- return writeJsonAndReport(filePath, "Windsurf", mcpEntry(opts.remote ?? false));
3519
- }
3520
- function mcpInstallGeminiCliCommand(opts = {}) {
3521
- const scope = opts.scope ?? "user";
3522
- if (!validateScope(scope)) return EXIT.USAGE;
3523
- const remote = opts.remote ?? false;
3524
- const spawnSync = opts._spawnSync ?? nodeSpawnSync;
3525
- const home = opts._homeDir ?? homedir3();
3526
- const probe = spawnSync("gemini", ["--version"], { stdio: "pipe" });
3527
- if (probe.error || probe.status !== 0) {
3528
- return writeJsonAndReport(geminiConfigPath(home, scope), "Gemini CLI", mcpEntry(remote));
3529
- }
3530
- const geminiScope = scope === "local" ? "project" : scope;
3531
- const cmd = ["mcp", "add", "-s", geminiScope, SERVER_NAME, "pyx-mem", ...pyxMemArgs(remote)];
3532
- const res = spawnSync("gemini", cmd, { stdio: "inherit" });
3533
- if (res.status !== 0) {
3534
- process.stderr.write(`\`gemini ${cmd.join(" ")}\` exited with code ${res.status ?? "null"}.
3535
- `);
3536
- return writeJsonAndReport(geminiConfigPath(home, scope), "Gemini CLI", mcpEntry(remote));
3537
- }
3538
- process.stdout.write(
3539
- `Installed pyx-memory MCP server in Gemini CLI (scope: ${geminiScope}).
3540
- Restart Gemini CLI to make the tools available. No API key was written to settings.json \u2014 credentials live in the OS credential store.
3541
- `
3542
- );
3543
- return EXIT.OK;
3544
- }
3545
- function geminiConfigPath(home, scope) {
3546
- return scope === "project" ? join3(process.cwd(), ".gemini", "settings.json") : join3(home, ".gemini", "settings.json");
3547
- }
3548
- function mcpInstallPiCommand(opts = {}) {
3549
- const scope = opts.scope ?? "user";
3550
- if (!validateScope(scope)) return EXIT.USAGE;
3551
- const home = opts._homeDir ?? homedir3();
3552
- const filePath = scope === "user" ? join3(home, ".pi", "agent", "mcp.json") : join3(process.cwd(), ".pi", "mcp.json");
3553
- return writeJsonAndReport(filePath, "Pi", mcpEntry(opts.remote ?? false), {
3554
- prereq: "pi has no native MCP support \u2014 install the third-party adapter once:\n pi install npm:pi-mcp-adapter"
3555
- });
3556
- }
3557
- function mcpInstallOhMyPiCommand(opts = {}) {
3558
- const scope = opts.scope ?? "user";
3559
- if (!validateScope(scope)) return EXIT.USAGE;
3560
- const home = opts._homeDir ?? homedir3();
3561
- const filePath = scope === "user" ? join3(home, ".omp", "agent", "mcp.json") : join3(process.cwd(), ".omp", "mcp.json");
3562
- return writeJsonAndReport(filePath, "oh-my-pi", mcpEntry(opts.remote ?? false));
3563
- }
3564
- function mcpInstallOpenClawCommand(opts = {}) {
3565
- const scope = opts.scope ?? "user";
3566
- if (!validateScope(scope)) return EXIT.USAGE;
3567
- const remote = opts.remote ?? false;
3568
- const spawnSync = opts._spawnSync ?? nodeSpawnSync;
3569
- const probe = spawnSync("openclaw", ["--version"], { stdio: "pipe" });
3570
- if (probe.error || probe.status !== 0) {
3571
- printOpenClawManualInstructions(remote);
3572
- return EXIT.OK;
3573
- }
3574
- const entryJson = JSON.stringify(mcpEntry(remote));
3575
- const cmd = ["mcp", "set", SERVER_NAME, entryJson];
3576
- const res = spawnSync("openclaw", cmd, { stdio: "inherit" });
3577
- if (res.status !== 0) {
3578
- process.stderr.write(
3579
- `\`openclaw ${cmd.join(" ")}\` exited with code ${res.status ?? "null"}.
3580
- `
3581
- );
3582
- printOpenClawManualInstructions(remote);
3583
- return EXIT.USAGE;
3584
- }
3585
- process.stdout.write(
3586
- `Installed pyx-memory MCP server in OpenClaw.
3587
- Restart OpenClaw to make the tools available. No API key was written to openclaw.json \u2014 credentials live in the OS credential store.
3588
- `
3589
- );
3590
- return EXIT.OK;
3591
- }
3592
- function printOpenClawManualInstructions(remote) {
3593
- const entry = mcpEntry(remote);
3594
- const entryJson = JSON.stringify(entry);
3595
- process.stdout.write(
3596
- [
3597
- "The `openclaw` CLI is not on PATH, or the install command failed.",
3598
- "",
3599
- "Run this once OpenClaw is installed:",
3600
- ` openclaw mcp set ${SERVER_NAME} '${entryJson}'`,
3601
- "",
3602
- "Or add this entry manually under `mcp.servers` in your OpenClaw config",
3603
- "(run `openclaw config file` to print the active config path):",
3604
- "",
3605
- JSON.stringify({ mcp: { servers: { [SERVER_NAME]: entry } } }, null, 2),
3606
- "",
3607
- "Do NOT add your pyx-memory API key here. Run `pyx-mem login` to store it in the OS credential store.",
3608
- ""
3609
- ].join("\n")
3610
- );
3611
- }
3612
- function mcpInstallHermesCommand(opts = {}) {
3613
- const scope = opts.scope ?? "user";
3614
- if (!validateScope(scope)) return EXIT.USAGE;
3615
- const remote = opts.remote ?? false;
3616
- const spawnSync = opts._spawnSync ?? nodeSpawnSync;
3617
- const probe = spawnSync("hermes", ["--version"], { stdio: "pipe" });
3618
- if (probe.error || probe.status !== 0) {
3619
- printHermesManualInstructions(remote);
3620
- return EXIT.OK;
3621
- }
3622
- const cmd = ["mcp", "add", SERVER_NAME, "--command", "pyx-mem", "--args", ...pyxMemArgs(remote)];
3623
- const res = spawnSync("hermes", cmd, { stdio: "inherit" });
3624
- if (res.status !== 0) {
3625
- process.stderr.write(`\`hermes ${cmd.join(" ")}\` exited with code ${res.status ?? "null"}.
3626
- `);
3627
- printHermesManualInstructions(remote);
3628
- return EXIT.USAGE;
3629
- }
3630
- process.stdout.write(
3631
- `Installed pyx-memory MCP server in Hermes.
3632
- Restart Hermes to make the tools available. No API key was written to config.yaml \u2014 credentials live in the OS credential store.
3633
- `
3634
- );
3635
- return EXIT.OK;
3636
- }
3637
- function printHermesManualInstructions(remote) {
3638
- process.stdout.write(
3639
- [
3640
- "The `hermes` CLI is not on PATH, or the install command failed.",
3641
- "",
3642
- "Run this once Hermes is installed:",
3643
- ` hermes mcp add ${SERVER_NAME} --command pyx-mem --args ${pyxMemArgs(remote).join(" ")}`,
3644
- "",
3645
- "Or add this block under `mcp_servers:` in your Hermes config",
3646
- "(typically ~/.hermes/config.yaml):",
3647
- "",
3648
- `${SERVER_NAME}:`,
3649
- " command: pyx-mem",
3650
- " args:",
3651
- ...pyxMemArgs(remote).map((arg) => ` - ${arg}`),
3652
- "",
3653
- "Do NOT add your pyx-memory API key here. Run `pyx-mem login` to store it in the OS credential store.",
3654
- ""
3655
- ].join("\n")
3656
- );
3657
- }
3658
- var RULES_FILE_TARGETS = {
3659
- "claude-code": { envDir: "CLAUDE_CONFIG_DIR", dir: [".claude"], file: "CLAUDE.md" },
3660
- codex: { envDir: "CODEX_HOME", dir: [".codex"], file: "AGENTS.md" },
3661
- "gemini-cli": { dir: [".gemini"], file: "GEMINI.md" },
3662
- "oh-my-pi": { dir: [".omp", "agent"], file: "RULES.md" }
3663
- };
3664
- function rulesFilePath(target, opts) {
3665
- if (!Object.hasOwn(RULES_FILE_TARGETS, target)) return void 0;
3666
- const spec = RULES_FILE_TARGETS[target];
3667
- const env = opts._env ?? (opts._homeDir === void 0 ? process.env : {});
3668
- const configDir = spec.envDir ? env[spec.envDir] : void 0;
3669
- return configDir ? join3(configDir, spec.file) : join3(opts._homeDir ?? homedir3(), ...spec.dir, spec.file);
3670
- }
3671
- function installMemoryRules(opts) {
3672
- const filePath = rulesFilePath(opts.target, opts);
3673
- if (!filePath) return;
3674
- if (opts.noRules) {
3675
- process.stdout.write(
3676
- 'Skipped writing the memory rules (--no-rules). The tools are installed, but the agent will reach for memory only when you ask; add the "## Persistent Memory" rules yourself to change that.\n'
3677
- );
3678
- return;
3679
- }
3680
- const result = writeMemoryRulesFile({ filePath, section: PERSISTENT_MEMORY_SECTION });
3681
- process.stdout.write(result.message);
3682
- }
3683
- function installMemoryRulesOnly(target, opts = {}) {
3684
- if (target === void 0 || !rulesFilePath(target, opts)) {
3685
- process.stderr.write(
3686
- `Error: \`pyx-mem mcp rules\` needs a host with a known rules file: ${Object.keys(RULES_FILE_TARGETS).join(", ")}.
3687
- `
3688
- );
3689
- return EXIT.USAGE;
3690
- }
3691
- installMemoryRules({ ...opts, target, noRules: false });
3692
- return EXIT.OK;
3693
- }
3694
- function reconcileMemoryRules(opts = {}) {
3695
- const section = opts.section ?? PERSISTENT_MEMORY_SECTION;
3696
- for (const target of Object.keys(RULES_FILE_TARGETS)) {
3697
- const filePath = rulesFilePath(target, opts);
3698
- try {
3699
- if (!existsSync3(filePath)) continue;
3700
- const result = writeMemoryRulesFile({ filePath, section, refreshOnly: true });
3701
- if (result.action !== "unchanged") {
3702
- process.stderr.write(
3703
- `pyx-memory: refreshed the "## Persistent Memory" rules in ${filePath}
3704
- `
3705
- );
3706
- }
3707
- } catch {
3708
- }
3709
- }
3710
- }
3711
- var MAX_SERVER_RULES_BYTES = 16 * 1024;
3712
- async function syncMemoryRulesFromServer(client, opts = {}) {
3713
- try {
3714
- const { tools } = await client.listTools();
3715
- if (!tools.some((tool) => tool.name === "get_memory_rules")) {
3716
- reconcileMemoryRules(opts);
3717
- return;
3718
- }
3719
- const result = await client.callTool({ name: "get_memory_rules", arguments: {} });
3720
- const [first] = result.content ?? [];
3721
- const section = first?.type === "text" ? first.text : void 0;
3722
- if (result.isError || !section || !isRulesSection(section)) {
3723
- throw new Error('the server returned no valid "## Persistent Memory" section');
3724
- }
3725
- reconcileMemoryRules({ ...opts, section });
3726
- } catch (err) {
3727
- process.stderr.write(
3728
- `pyx-memory: left the installed memory rules unchanged \u2014 could not fetch current rules (${err instanceof Error ? err.message : String(err)}).
3729
- `
3730
- );
3731
- }
3732
- }
3733
- function isRulesSection(text2) {
3734
- return text2.startsWith("## Persistent Memory\n") && Buffer.byteLength(text2, "utf8") <= MAX_SERVER_RULES_BYTES && !text2.includes("<!-- pyx-memory:") && !/^\s*(```|~~~)/m.test(text2);
3735
- }
3736
- function validateScope(scope) {
3737
- if (VALID_SCOPES.has(scope)) return true;
3738
- process.stderr.write(`Error: invalid --scope \`${scope}\`. Expected: local | user | project.
3739
- `);
3740
- return false;
3741
- }
3742
- function writeJsonAndReport(filePath, agentLabel, entry, opts = {}) {
3743
- const result = mergeWriteJsonMcpEntry({
3744
- filePath,
3745
- serverName: SERVER_NAME,
3746
- entry
3747
- });
3748
- const stream = result.exitCode === EXIT.OK ? process.stdout : process.stderr;
3749
- stream.write(result.message);
3750
- if (result.status === "installed") {
3751
- process.stdout.write(
3752
- `Restart ${agentLabel} to make the tools available. No API key was written \u2014 credentials live in the OS credential store.
3753
- `
3754
- );
3755
- }
3756
- if ((result.status === "installed" || result.status === "already-installed") && opts.prereq) {
3757
- process.stdout.write(`Prerequisite: ${opts.prereq}
3758
- `);
3759
- }
3760
- return result.exitCode;
3761
- }
3762
-
3763
- // src/cli/commands/mcp.ts
3764
- async function mcpCommand() {
3765
- const readCredentials = createReadCredentials(() => getDefaultKeychain());
3766
- try {
3767
- await runMcpProxyServer({
3768
- readCredentials,
3769
- onConnected: (client) => syncMemoryRulesFromServer(client)
3770
- });
3771
- return EXIT.OK;
3772
- } catch (err) {
3773
- process.stderr.write(`Internal error: ${err instanceof Error ? err.message : String(err)}
3774
- `);
3775
- return EXIT.USAGE;
3776
- }
3777
- }
3778
- function resolveMcpMode(flags) {
3779
- if (flags.bundled === true) {
3780
- return {
3781
- ok: false,
3782
- error: "The bundled stdio MCP server was removed in v1.3.0. `pyx-mem mcp` now uses the hosted zero-touch path (server-owned tools + instructions; your key stays in the OS keychain). Drop `--bundled`. For a self-hosted server, point at it with `pyx-mem login --endpoint <url>` first."
3783
- };
3784
- }
3785
- return { ok: true };
3786
- }
3787
-
3788
- // src/cli/commands/overview.ts
3789
- import { z as z2 } from "zod";
3790
- var OVERVIEW_SCHEMA_VERSION = 1;
3791
- var RECENT_ENTRY_LIMIT = 3;
3792
- var MAX_TOPIC_CHARS = 160;
3793
- var MAX_PROJECT_CHARS = 80;
3794
- var entrySchema = z2.object({
3795
- id: z2.string().min(1),
3796
- metadata: z2.record(z2.string(), z2.unknown())
3797
- }).passthrough();
3798
- var hostedEntriesResponseSchema = z2.object({
3799
- memories: z2.array(entrySchema),
3800
- total: z2.number().int().nonnegative()
3801
- }).passthrough();
3802
- var selfHostedEntriesResponseSchema = z2.object({
3803
- success: z2.literal(true),
3804
- data: z2.object({
3805
- entries: z2.array(entrySchema),
3806
- totalCount: z2.number().int().nonnegative()
3807
- }).passthrough()
3808
- }).passthrough();
3809
- var entriesResponseSchema = z2.union([
3810
- hostedEntriesResponseSchema.transform(({ memories, total }) => ({
3811
- entries: memories,
3812
- totalCount: total
3813
- })),
3814
- selfHostedEntriesResponseSchema.transform(({ data }) => data)
3815
- ]);
3816
- async function overviewCommand(opts = {}) {
3817
- const result = await readSurface({
3818
- surface: "overview",
3819
- request: {
3820
- method: "GET",
3821
- path: "/api/memory/entries",
3822
- query: { status: "active", limit: RECENT_ENTRY_LIMIT }
3823
- },
3824
- schema: entriesResponseSchema,
3825
- keychain: opts.keychain,
3826
- fetchImpl: opts.fetchImpl
3827
- });
3828
- if (!result.ok) {
3829
- emit2(report2(result.state, result.endpoint, result.keySource, result.problem));
3830
- return result.exit;
3831
- }
3832
- emit2({
3833
- ...report2("ready", result.endpoint, "keychain", null),
3834
- totalCount: result.data.totalCount,
3835
- entries: result.data.entries.slice(0, RECENT_ENTRY_LIMIT).map((entry) => ({
3836
- id: entry.id,
3837
- topic: boundedLabel(entry.metadata.topic, "Untitled", MAX_TOPIC_CHARS),
3838
- project: boundedLabel(entry.metadata.project, "Unscoped", MAX_PROJECT_CHARS)
3839
- }))
3840
- });
3841
- return EXIT.OK;
3842
- }
3843
- function report2(state, endpoint, keySource, problem) {
3844
- return {
3845
- schemaVersion: OVERVIEW_SCHEMA_VERSION,
3846
- state,
3847
- endpoint,
3848
- keySource,
3849
- totalCount: 0,
3850
- entries: [],
3851
- problem
3852
- };
3853
- }
3854
- function emit2(value) {
3855
- process.stdout.write(`${JSON.stringify(value)}
3856
- `);
3857
- }
3858
- function boundedLabel(value, fallback, maxChars) {
3859
- if (typeof value !== "string" || value.trim().length === 0) return fallback;
3860
- const normalized = value.trim();
3861
- const chars = [...normalized];
3862
- return chars.length > maxChars ? `${chars.slice(0, maxChars).join("")}\u2026` : normalized;
3863
- }
3864
-
3865
- // src/cli/commands/scaffold.ts
3866
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
3867
- import { basename as basename3, join as join4, resolve as resolve3 } from "path";
3868
- var SERVER_IMAGE = "ghcr.io/pyx-corp/pyx-memory-v1:latest";
3869
- var DOCKER_COMPOSE = `services:
3870
- pyx-memory:
3871
- image: ${SERVER_IMAGE}
3872
- ports:
3873
- - "7822:7822"
3874
- volumes:
3875
- - pyx-memory-data:/data
3876
- environment:
3877
- DATA_DIR: "\${DATA_DIR:-/data}"
3878
- API_KEY: "\${API_KEY:-}"
3879
- ADMIN_API_KEY: "\${ADMIN_API_KEY:-}"
3880
- TENANT_MODE: "\${TENANT_MODE:-single}"
3881
- NEO4J_URL: "\${NEO4J_URL:-bolt://neo4j:7687}"
3882
- NEO4J_USERNAME: "\${NEO4J_USERNAME:-neo4j}"
3883
- NEO4J_PASSWORD: "\${NEO4J_PASSWORD:?Set NEO4J_PASSWORD in .env or the shell.}"
3884
- EMBEDDING_PROVIDER: "\${EMBEDDING_PROVIDER:-local}"
3885
- EMBEDDING_ENDPOINT: "\${EMBEDDING_ENDPOINT:-}"
3886
- EMBEDDING_DIMENSIONS: "\${EMBEDDING_DIMENSIONS:-}"
3887
- # Graph is built by the CALLING AGENT by default (it passes entities +
3888
- # relationships / triples) \u2014 the server runs no LLM. To opt into
3889
- # server-side BYO extraction, set EXTRACTION_PROVIDER=http and supply
3890
- # EXTRACTION_ENDPOINT in .env.
3891
- EXTRACTION_PROVIDER: "\${EXTRACTION_PROVIDER:-none}"
3892
- EXTRACTION_ENDPOINT: "\${EXTRACTION_ENDPOINT:-}"
3893
- EXTRACTION_API_KEY: "\${EXTRACTION_API_KEY:-}"
3894
- EXTRACTION_MODEL: "\${EXTRACTION_MODEL:-}"
3895
- ENCRYPTION_KEY: "\${ENCRYPTION_KEY:-}"
3896
- depends_on:
3897
- neo4j:
3898
- condition: service_healthy
3899
-
3900
- neo4j:
3901
- image: neo4j:5-community
3902
- ports:
3903
- - "7474:7474"
3904
- - "7687:7687"
3905
- volumes:
3906
- - pyx-memory-neo4j-data:/data
3907
- - pyx-memory-neo4j-logs:/logs
3908
- environment:
3909
- NEO4J_AUTH: "neo4j/\${NEO4J_PASSWORD:?Set NEO4J_PASSWORD in .env or the shell.}"
3910
- NEO4J_PLUGINS: '["apoc"]'
3911
- healthcheck:
3912
- test: ["CMD", "neo4j", "status"]
3913
- interval: 10s
3914
- timeout: 10s
3915
- retries: 10
3916
-
3917
- volumes:
3918
- pyx-memory-data:
3919
- pyx-memory-neo4j-data:
3920
- pyx-memory-neo4j-logs:
3921
- `;
3922
- var ENV_EXAMPLE = `# pyx-memory operator config
3923
- # Copy this file to .env, then fill the required values before running Docker Compose.
3924
-
3925
- # OPTIONAL. Graph is built by the calling agent by default (the server runs no LLM).
3926
- # Set to http ONLY to opt into server-side BYO entity/relationship extraction.
3927
- EXTRACTION_PROVIDER=none
3928
-
3929
- # REQUIRED only when EXTRACTION_PROVIDER=http. HTTP chat/completions-compatible base URL.
3930
- # Leave empty for the default agent-owns-the-graph setup.
3931
- EXTRACTION_ENDPOINT=
3932
-
3933
- # OPTIONAL. Bearer token sent to EXTRACTION_ENDPOINT when your extraction service requires auth.
3934
- EXTRACTION_API_KEY=
3935
-
3936
- # OPTIONAL. Model identifier sent to the extraction service.
3937
- EXTRACTION_MODEL=
3938
-
3939
- # OPTIONAL for local embeddings. Set to http only when using a remote embedding service.
3940
- EMBEDDING_PROVIDER=local
3941
-
3942
- # REQUIRED only when EMBEDDING_PROVIDER=http. Base URL for the HTTP embedding service.
3943
- EMBEDDING_ENDPOINT=
3944
-
3945
- # REQUIRED only when EMBEDDING_PROVIDER=http. Must match the remote embedding vector width.
3946
- # Example for embeddinggemma: 768.
3947
- EMBEDDING_DIMENSIONS=
3948
-
3949
- # RECOMMENDED for any protected or production deployment. Empty means unauthenticated local access.
3950
- API_KEY=
3951
-
3952
- # OPTIONAL. Destructive/admin operations use this; when empty, the server falls back to API_KEY.
3953
- ADMIN_API_KEY=
3954
-
3955
- # REQUIRED only when SENSITIVITY_POLICY=encrypt is configured on the server.
3956
- # 32 bytes as 64 hex chars or 44 base64 chars.
3957
- ENCRYPTION_KEY=
3958
-
3959
- # REQUIRED by the scaffolded container.
3960
- DATA_DIR=/data
3961
-
3962
- # REQUIRED. Use single for one app/tenant; use multi only behind authenticated tenant routing.
3963
- TENANT_MODE=single
3964
-
3965
- # REQUIRED for graph storage in this compose stack.
3966
- NEO4J_URL=bolt://neo4j:7687
3967
-
3968
- # OPTIONAL. Defaults to neo4j when unset.
3969
- NEO4J_USERNAME=neo4j
3970
-
3971
- # REQUIRED by docker-compose.yml for the Neo4j service and pyx-memory graph connection.
3972
- NEO4J_PASSWORD=
3973
- `;
3974
- var MEMORY_TS = `import { createPyxMemory } from '@pyxmate/memory';
3975
-
3976
- // Reads PYX_MEMORY_URL and PYX_MEMORY_API_KEY by default.
3977
- export const memory = createPyxMemory();
3978
- `;
3979
- function resolveTarget(args) {
3980
- const cwd = args.cwd ?? process.cwd();
3981
- if (args.name === true) {
3982
- process.stderr.write("Error: --name requires a directory name.\n");
3983
- return null;
3984
- }
3985
- const trimmedName = typeof args.name === "string" ? args.name.trim() : "";
3986
- if (typeof args.name === "string" && trimmedName.length === 0) {
3987
- process.stderr.write("Error: --name requires a non-empty directory name.\n");
3988
- return null;
3989
- }
3990
- const targetDir = trimmedName ? resolve3(cwd, trimmedName) : cwd;
3991
- const appName = trimmedName || basename3(resolve3(cwd));
3992
- return { targetDir, appName };
3993
- }
3994
- function buildFiles(targetDir, appName) {
3995
- return [
3996
- { path: join4(targetDir, "docker-compose.yml"), contents: DOCKER_COMPOSE },
3997
- { path: join4(targetDir, ".env.example"), contents: ENV_EXAMPLE },
3998
- { path: join4(targetDir, "memory.ts"), contents: MEMORY_TS },
3999
- {
4000
- path: join4(targetDir, "PYX_MEMORY_DESIGN_GUIDE.md"),
4001
- contents: buildDesignGuide({ appName })
4002
- }
4003
- ];
4004
- }
4005
- function scaffoldCommand(args = {}) {
4006
- const target = resolveTarget(args);
4007
- if (target === null) return EXIT.USAGE;
4008
- if (existsSync4(target.targetDir) && !statSync2(target.targetDir).isDirectory()) {
4009
- process.stderr.write(`Error: target exists and is not a directory: ${target.targetDir}
4010
- `);
4011
- return EXIT.USAGE;
4012
- }
4013
- mkdirSync3(target.targetDir, { recursive: true });
4014
- const created = [];
4015
- const skipped = [];
4016
- for (const file of buildFiles(target.targetDir, target.appName)) {
4017
- const relativePath = basename3(file.path);
4018
- if (existsSync4(file.path)) {
4019
- skipped.push(relativePath);
4020
- continue;
4021
- }
4022
- writeFileSync3(file.path, file.contents, "utf8");
4023
- created.push(relativePath);
4024
- }
4025
- process.stdout.write(
4026
- [
4027
- `pyx-memory scaffold: ${target.targetDir}`,
4028
- `created: ${created.length > 0 ? created.join(", ") : "(none)"}`,
4029
- `skipped: ${skipped.length > 0 ? skipped.join(", ") : "(none)"}`,
4030
- "",
4031
- "Next steps:",
4032
- "1. Copy .env.example to .env and set NEO4J_PASSWORD (graph is built by the calling agent by default \u2014 set EXTRACTION_* only to opt into server-side BYO extraction).",
4033
- "2. Set API_KEY before using this beyond local development.",
4034
- "3. Run: docker compose up -d",
4035
- "4. Set PYX_MEMORY_URL=http://localhost:7822 and PYX_MEMORY_API_KEY to match API_KEY.",
4036
- ""
4037
- ].join("\n")
4038
- );
4039
- return EXIT.OK;
4040
- }
4041
-
4042
- // src/cli/commands/status.ts
4043
- async function readLifecycle(provider) {
4044
- let serverConvergenceSeconds = null;
4045
- try {
4046
- const journal = await loadJournal(provider);
4047
- serverConvergenceSeconds = convergenceSeconds(journal);
4048
- if (isEmpty(journal)) return { cleanupPending: false, serverConvergenceSeconds };
4049
- } catch (err) {
4050
- if (!(err instanceof JournalInvalidError)) throw err;
4051
- }
4052
- return { cleanupPending: !await provider.isLocked(), serverConvergenceSeconds };
4053
- }
4054
- async function statusCommand(opts = {}) {
4055
- const provider = opts.keychain ? guardKeychain(opts.keychain) : getDefaultKeychain();
4056
- let report4;
4057
- try {
4058
- const creds = await provider.read();
4059
- report4 = {
4060
- endpoint: creds?.endpoint ?? null,
4061
- keyPresent: creds !== null,
4062
- keySource: creds !== null ? "keychain" : null,
4063
- loggedIn: creds !== null,
4064
- credentialFingerprint: creds === null ? null : credentialFingerprint(creds.endpoint, creds.apiKey),
4065
- lifecycleVersion: 1,
4066
- connection: creds?.provenance === "browser-login" ? creds.connection ?? null : null,
4067
- ...await readLifecycle(provider)
4068
- };
4069
- } catch (err) {
4070
- if (err instanceof KeychainError) return emitError(err, opts.json);
4071
- throw err;
4072
- }
4073
- if (opts.json) {
4074
- process.stdout.write(`${JSON.stringify(report4)}
4075
- `);
4076
- } else {
4077
- const lines = [
4078
- `endpoint: ${report4.endpoint ?? "(not set)"}`,
4079
- `credentials: ${report4.loggedIn ? "present (keychain)" : "not logged in \u2014 run: pyx-mem login"}`
4080
- ];
4081
- process.stdout.write(`${lines.join("\n")}
4082
- `);
4083
- }
4084
- return report4.loggedIn ? EXIT.OK : EXIT.NOT_LOGGED_IN;
4085
- }
4086
- function emitError(err, json) {
4087
- if (json) {
4088
- process.stdout.write(
4089
- `${JSON.stringify({ ok: false, code: err.code, message: err.message, guidance: err.guidance })}
4090
- `
4091
- );
4092
- } else {
4093
- process.stderr.write(`Error: ${err.message}
4094
- ${err.guidance}
4095
- `);
4096
- }
4097
- return err.exit;
4098
- }
4099
-
4100
- // src/cli/commands/taxonomy.ts
4101
- import { z as z3 } from "zod";
4102
- var TAXONOMY_SCHEMA_VERSION = 1;
4103
- var TAXONOMY_MAX_RETURNED_CATEGORIES = TAXONOMY_MAX_CATEGORIES + 1;
4104
- var taxonomyStateSchema = z3.object({
4105
- clusters: z3.array(
4106
- z3.object({
4107
- size: z3.number().int().nonnegative(),
4108
- currentName: z3.string().nullable(),
4109
- categoryNodeId: z3.string().nullable(),
4110
- projects: z3.array(
4111
- z3.object({
4112
- name: z3.string().min(1).max(MEMORY_PROJECT_LABEL_MAX_CHARS),
4113
- memories: z3.number().int().positive()
4114
- })
4115
- ).max(TAXONOMY_MAX_PROJECTS),
4116
- sampleTopics: z3.array(z3.string()).max(TAXONOMY_MAX_SAMPLE_TOPICS),
4117
- topEntities: z3.array(
4118
- z3.object({
4119
- name: z3.string(),
4120
- type: z3.string(),
4121
- degree: z3.number().int().nonnegative()
4122
- })
4123
- ).max(TAXONOMY_MAX_TOP_ENTITIES)
4124
- })
4125
- ).max(TAXONOMY_MAX_RETURNED_CATEGORIES),
4126
- totals: z3.object({
4127
- clusters: z3.number().int().nonnegative(),
4128
- unnamed: z3.number().int().nonnegative(),
4129
- nodesConsidered: z3.number().int().nonnegative()
4130
- })
4131
- });
4132
- var taxonomyResponseSchema = z3.union([
4133
- taxonomyStateSchema,
4134
- z3.object({ success: z3.literal(true), data: taxonomyStateSchema }).transform(({ data }) => data)
4135
- ]);
4136
- async function taxonomyCommand(opts = {}) {
4137
- const result = await readSurface({
4138
- surface: "taxonomy",
4139
- request: { method: "GET", path: "/api/memory/graph/taxonomy" },
4140
- schema: taxonomyResponseSchema,
4141
- minimumServiceVersion: "1.18.1",
4142
- keychain: opts.keychain,
4143
- fetchImpl: opts.fetchImpl
4144
- });
4145
- const withIdentity = opts.withIdentity === true;
4146
- if (!result.ok) {
4147
- emit3(
4148
- report3(result.state, result.endpoint, result.keySource, result.problem, {
4149
- withIdentity,
4150
- credentialFingerprint: result.credentialFingerprint
4151
- })
4152
- );
4153
- return result.exit;
4154
- }
4155
- emit3({
4156
- ...report3("ready", result.endpoint, "keychain", null, {
4157
- withIdentity,
4158
- credentialFingerprint: result.credentialFingerprint
4159
- }),
4160
- clusters: result.data.clusters,
4161
- totals: result.data.totals
4162
- });
4163
- return EXIT.OK;
4164
- }
4165
- function report3(state, endpoint, keySource, problem, identity) {
4166
- return {
4167
- schemaVersion: TAXONOMY_SCHEMA_VERSION,
4168
- state,
4169
- endpoint,
4170
- keySource,
4171
- // Spread, not `undefined`: JSON.stringify would drop an undefined value,
4172
- // but the key must be absent from the object too so exact-shape checks hold.
4173
- ...identity.withIdentity ? { credentialFingerprint: identity.credentialFingerprint } : {},
4174
- clusters: [],
4175
- totals: { clusters: 0, unnamed: 0, nodesConsidered: 0 },
4176
- problem
4177
- };
4178
- }
4179
- function emit3(value) {
4180
- process.stdout.write(`${JSON.stringify(value)}
4181
- `);
4182
- }
3
+ EXIT
4
+ } from "../chunk-4DXEQC6X.mjs";
4183
5
 
4184
6
  // src/cli/help.ts
4185
7
  var HELP_TEXT = `pyx-mem \u2014 pyx-memory CLI
@@ -4195,6 +17,7 @@ Commands:
4195
17
  status [--json] Show endpoint, key presence, MCP config status.
4196
18
  overview --json Read memory count and three recent safe labels.
4197
19
  insights --json [--with-identity] Read body-free memory usage and coverage aggregates.
20
+ [--with-provenance] Add last writer, save time and applier to recent items.
4198
21
  taxonomy --json [--with-identity] Read knowledge-graph cluster composition (read-only).
4199
22
  --with-identity adds credentialFingerprint (the same
4200
23
  value as status --json) so a host can tell which
@@ -4235,7 +58,7 @@ Notes:
4235
58
 
4236
59
  // src/cli/parser.ts
4237
60
  var COMMANDS_WITH_SUBCOMMAND = /* @__PURE__ */ new Set(["mcp"]);
4238
- function parseArgs2(argv) {
61
+ function parseArgs(argv) {
4239
62
  const flags = /* @__PURE__ */ Object.create(null);
4240
63
  const positional = [];
4241
64
  let i = 0;
@@ -4278,21 +101,35 @@ function parseArgs2(argv) {
4278
101
  }
4279
102
 
4280
103
  // src/cli/pyx-mem.ts
4281
- var INSTALL_TARGETS = {
4282
- "claude-code": mcpInstallClaudeCodeCommand,
4283
- codex: mcpInstallCodexCommand,
4284
- cursor: mcpInstallCursorCommand,
4285
- cline: mcpInstallClineCommand,
4286
- continue: mcpInstallContinueCommand,
4287
- windsurf: mcpInstallWindsurfCommand,
4288
- "gemini-cli": mcpInstallGeminiCliCommand,
4289
- pi: mcpInstallPiCommand,
4290
- "oh-my-pi": mcpInstallOhMyPiCommand,
4291
- openclaw: mcpInstallOpenClawCommand,
4292
- hermes: mcpInstallHermesCommand
4293
- };
4294
- var VALID_TARGETS = Object.keys(INSTALL_TARGETS);
4295
- function runMcpInstall(target, scope, remote, noRules) {
104
+ async function runMcpInstall(target, scope, remote, noRules) {
105
+ const {
106
+ installMemoryRules,
107
+ mcpInstallClaudeCodeCommand,
108
+ mcpInstallClineCommand,
109
+ mcpInstallCodexCommand,
110
+ mcpInstallContinueCommand,
111
+ mcpInstallCursorCommand,
112
+ mcpInstallGeminiCliCommand,
113
+ mcpInstallHermesCommand,
114
+ mcpInstallOhMyPiCommand,
115
+ mcpInstallOpenClawCommand,
116
+ mcpInstallPiCommand,
117
+ mcpInstallWindsurfCommand
118
+ } = await import("../mcp-install-KBB45OB2.mjs");
119
+ const INSTALL_TARGETS = {
120
+ "claude-code": mcpInstallClaudeCodeCommand,
121
+ codex: mcpInstallCodexCommand,
122
+ cursor: mcpInstallCursorCommand,
123
+ cline: mcpInstallClineCommand,
124
+ continue: mcpInstallContinueCommand,
125
+ windsurf: mcpInstallWindsurfCommand,
126
+ "gemini-cli": mcpInstallGeminiCliCommand,
127
+ pi: mcpInstallPiCommand,
128
+ "oh-my-pi": mcpInstallOhMyPiCommand,
129
+ openclaw: mcpInstallOpenClawCommand,
130
+ hermes: mcpInstallHermesCommand
131
+ };
132
+ const VALID_TARGETS = Object.keys(INSTALL_TARGETS);
4296
133
  if (target === void 0 || !Object.hasOwn(INSTALL_TARGETS, target)) {
4297
134
  process.stderr.write(
4298
135
  `Error: unknown install target \`${target ?? ""}\`. Expected: ${VALID_TARGETS.join(", ")}.
@@ -4305,8 +142,9 @@ function runMcpInstall(target, scope, remote, noRules) {
4305
142
  if (code === EXIT.OK) installMemoryRules({ target, noRules });
4306
143
  return code;
4307
144
  }
4308
- function runMcpCommand(parsed) {
145
+ async function runMcpCommand(parsed) {
4309
146
  if (parsed.subcommand === void 0) {
147
+ const { mcpCommand, resolveMcpMode } = await import("../mcp-64KVRH7F.mjs");
4310
148
  const resolved = resolveMcpMode({
4311
149
  remote: parsed.flags.remote === true,
4312
150
  bundled: parsed.flags.bundled === true
@@ -4333,6 +171,7 @@ function runMcpCommand(parsed) {
4333
171
  process.stderr.write("Error: `pyx-mem mcp rules <host>` takes no flags.\n");
4334
172
  return EXIT.USAGE;
4335
173
  }
174
+ const { installMemoryRulesOnly } = await import("../mcp-install-KBB45OB2.mjs");
4336
175
  return installMemoryRulesOnly(parsed.positional[0]);
4337
176
  }
4338
177
  process.stderr.write(
@@ -4342,7 +181,7 @@ function runMcpCommand(parsed) {
4342
181
  return EXIT.USAGE;
4343
182
  }
4344
183
  var LOGIN_FLAGS = /* @__PURE__ */ new Set(["endpoint", "api-key", "browser", "json", "no-open"]);
4345
- function runLogin(parsed) {
184
+ async function runLogin(parsed) {
4346
185
  const { flags } = parsed;
4347
186
  const browser = flags.browser === true;
4348
187
  const invalid = Object.keys(flags).some((flag) => !LOGIN_FLAGS.has(flag)) || parsed.positional.length > 0 || flags.browser !== void 0 && !browser || (flags.json !== void 0 || flags["no-open"] !== void 0) && !browser || browser && flags["api-key"] !== void 0 || flags.json !== void 0 && flags.json !== true || flags["no-open"] !== void 0 && flags["no-open"] !== true;
@@ -4354,19 +193,21 @@ function runLogin(parsed) {
4354
193
  }
4355
194
  const endpoint = typeof flags.endpoint === "string" ? flags.endpoint : void 0;
4356
195
  if (browser) {
196
+ const { browserLoginCommand } = await import("../login-browser-DO2O2Q2G.mjs");
4357
197
  return browserLoginCommand({
4358
198
  endpoint,
4359
199
  json: flags.json === true,
4360
200
  noOpen: flags["no-open"] === true
4361
201
  });
4362
202
  }
203
+ const { loginCommand } = await import("../login-4ALCAN4M.mjs");
4363
204
  return loginCommand({
4364
205
  endpoint,
4365
206
  apiKey: typeof flags["api-key"] === "string" ? flags["api-key"] : void 0
4366
207
  });
4367
208
  }
4368
209
  async function main() {
4369
- const parsed = parseArgs2(process.argv.slice(2));
210
+ const parsed = parseArgs(process.argv.slice(2));
4370
211
  if (parsed.flags.help === true || parsed.flags.h === true) {
4371
212
  process.stdout.write(HELP_TEXT);
4372
213
  return EXIT.OK;
@@ -4378,43 +219,62 @@ async function main() {
4378
219
  return EXIT.OK;
4379
220
  case "login":
4380
221
  return runLogin(parsed);
4381
- case "login-recovery":
222
+ case "login-recovery": {
223
+ const { loginRecoveryCommand } = await import("../login-recovery-AOO76PYX.mjs");
4382
224
  return loginRecoveryCommand({
4383
225
  json: parsed.flags.json === true,
4384
226
  discardUnreadableJournal: parsed.flags["discard-unreadable-journal"] === true
4385
227
  });
228
+ }
4386
229
  case "lifecycle-capabilities":
4387
230
  process.stdout.write(
4388
231
  `${JSON.stringify({ schemaVersion: 1, browserLogin: true, sharedCredentialGuard: true })}
4389
232
  `
4390
233
  );
4391
234
  return EXIT.OK;
4392
- case "status":
235
+ case "status": {
236
+ const { statusCommand } = await import("../status-UHJZLEPL.mjs");
4393
237
  return statusCommand({ json: parsed.flags.json === true });
4394
- case "overview":
238
+ }
239
+ case "overview": {
4395
240
  if (parsed.flags.json !== true) {
4396
241
  process.stderr.write("Error: `pyx-mem overview` requires --json.\n");
4397
242
  return EXIT.USAGE;
4398
243
  }
244
+ const { overviewCommand } = await import("../overview-COBXHLP3.mjs");
4399
245
  return overviewCommand();
4400
- case "insights":
246
+ }
247
+ case "insights": {
4401
248
  if (parsed.flags.json !== true) {
4402
249
  process.stderr.write("Error: `pyx-mem insights` requires --json.\n");
4403
250
  return EXIT.USAGE;
4404
251
  }
4405
- return insightsCommand({ withIdentity: parsed.flags["with-identity"] === true });
4406
- case "taxonomy":
252
+ const { insightsCommand } = await import("../insights-PD7Y6QDX.mjs");
253
+ return insightsCommand({
254
+ withIdentity: parsed.flags["with-identity"] === true,
255
+ withProvenance: parsed.flags["with-provenance"] === true
256
+ });
257
+ }
258
+ case "taxonomy": {
4407
259
  if (parsed.flags.json !== true) {
4408
260
  process.stderr.write("Error: `pyx-mem taxonomy` requires --json.\n");
4409
261
  return EXIT.USAGE;
4410
262
  }
263
+ const { taxonomyCommand } = await import("../taxonomy-EKDFDFXW.mjs");
4411
264
  return taxonomyCommand({ withIdentity: parsed.flags["with-identity"] === true });
4412
- case "logout":
265
+ }
266
+ case "logout": {
267
+ const { logoutCommand } = await import("../logout-4CO53LNA.mjs");
4413
268
  return logoutCommand({ json: parsed.flags.json === true });
4414
- case "doctor":
269
+ }
270
+ case "doctor": {
271
+ const { doctorCommand } = await import("../doctor-AERNUHEZ.mjs");
4415
272
  return doctorCommand({ json: parsed.flags.json === true });
4416
- case "scaffold":
273
+ }
274
+ case "scaffold": {
275
+ const { scaffoldCommand } = await import("../scaffold-2FSCWF22.mjs");
4417
276
  return scaffoldCommand({ name: parsed.flags.name });
277
+ }
4418
278
  case "mcp":
4419
279
  return runMcpCommand(parsed);
4420
280
  default: