@pyxmate/memory 1.21.0 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-contract.d.ts +3 -2
- package/dist/agent-contract.mjs +3 -1
- package/dist/{chunk-QWLTZIIK.mjs → chunk-7YHCNO3N.mjs} +1 -1
- package/dist/{chunk-BUKDWHTY.mjs → chunk-C46EE5BH.mjs} +5 -0
- package/dist/{chunk-EZERG25I.mjs → chunk-QD7G23CN.mjs} +12 -7
- package/dist/{chunk-HVJZDJGH.mjs → chunk-SGL73IY5.mjs} +1 -1
- package/dist/cli/pyx-mem.mjs +1382 -113
- package/dist/dashboard.mjs +3 -3
- package/dist/{data-plane-contract-webbeMs5.d.ts → data-plane-contract-C5VqxrpF.d.ts} +5 -0
- package/dist/data-plane-contract.d.ts +1 -1
- package/dist/data-plane-contract.mjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +2 -2
- package/dist/react.mjs +3 -3
- package/package.json +3 -1
package/dist/cli/pyx-mem.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
PERSISTENT_MEMORY_SECTION,
|
|
4
4
|
buildDesignGuide
|
|
5
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-QD7G23CN.mjs";
|
|
6
6
|
import {
|
|
7
7
|
MEMORY_PROJECT_LABEL_MAX_CHARS,
|
|
8
8
|
TAXONOMY_MAX_CATEGORIES,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
TAXONOMY_MAX_SAMPLE_TOPICS,
|
|
11
11
|
TAXONOMY_MAX_TOP_ENTITIES
|
|
12
12
|
} from "../chunk-KZKGRND2.mjs";
|
|
13
|
-
import "../chunk-
|
|
13
|
+
import "../chunk-C46EE5BH.mjs";
|
|
14
14
|
|
|
15
15
|
// src/cli/exit-codes.ts
|
|
16
16
|
var EXIT = {
|
|
@@ -19,9 +19,15 @@ var EXIT = {
|
|
|
19
19
|
USAGE: 2,
|
|
20
20
|
NOT_LOGGED_IN: 3,
|
|
21
21
|
CRED_UNAVAILABLE: 4,
|
|
22
|
-
PROVIDER_BROKEN: 5
|
|
22
|
+
PROVIDER_BROKEN: 5,
|
|
23
|
+
BUSY: 6,
|
|
24
|
+
CLEANUP_PENDING: 7,
|
|
25
|
+
CANCELLED: 8
|
|
23
26
|
};
|
|
24
27
|
|
|
28
|
+
// src/cli/lifecycle/journal.ts
|
|
29
|
+
import { createHash } from "crypto";
|
|
30
|
+
|
|
25
31
|
// src/cli/keychain/errors.ts
|
|
26
32
|
var PLATFORM_GUIDANCE = {
|
|
27
33
|
darwin: {
|
|
@@ -90,19 +96,447 @@ function causeMessage(cause) {
|
|
|
90
96
|
return "";
|
|
91
97
|
}
|
|
92
98
|
|
|
99
|
+
// src/cli/lifecycle/api.ts
|
|
100
|
+
var REQUEST_TIMEOUT_MS = 15e3;
|
|
101
|
+
var MAX_RESPONSE_BYTES = 64 * 1024;
|
|
102
|
+
var MAX_ENDPOINT_CHARS = 256;
|
|
103
|
+
var REQUEST_ID_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
104
|
+
var PROJECT_KEY_RE = /^pyx_proj_[A-Za-z0-9_-]{16,160}$/;
|
|
105
|
+
var ERROR_MESSAGES = {
|
|
106
|
+
busy: { message: "Another pyx-mem sign-in or sign-out is in progress.", retryable: true },
|
|
107
|
+
cleanup_pending: {
|
|
108
|
+
message: "Cleanup of an earlier sign-in or sign-out is still pending. Run: pyx-mem login-recovery",
|
|
109
|
+
retryable: true
|
|
110
|
+
},
|
|
111
|
+
invalid_endpoint: {
|
|
112
|
+
message: "Endpoint must be an https URL (http only for loopback) without credentials, query or fragment.",
|
|
113
|
+
retryable: false
|
|
114
|
+
},
|
|
115
|
+
untrusted_authorization_url: {
|
|
116
|
+
message: "The server returned a browser sign-in address that is not trusted for this endpoint.",
|
|
117
|
+
retryable: false
|
|
118
|
+
},
|
|
119
|
+
browser_login_unsupported: {
|
|
120
|
+
message: "This endpoint does not support browser sign-in. Use: pyx-mem login --api-key",
|
|
121
|
+
retryable: false
|
|
122
|
+
},
|
|
123
|
+
network_error: { message: "Could not reach the pyx-memory server.", retryable: true },
|
|
124
|
+
rate_limited: { message: "Too many sign-in attempts. Wait a minute and retry.", retryable: true },
|
|
125
|
+
expired: { message: "The sign-in request expired before it was completed.", retryable: true },
|
|
126
|
+
project_not_eligible: {
|
|
127
|
+
message: "That project can no longer be connected from this account.",
|
|
128
|
+
retryable: true
|
|
129
|
+
},
|
|
130
|
+
credential_changed: {
|
|
131
|
+
message: "The stored pyx-memory credential was changed by another program. Nothing was overwritten.",
|
|
132
|
+
retryable: true
|
|
133
|
+
},
|
|
134
|
+
lock_lost: {
|
|
135
|
+
message: "The sign-in was interrupted. Run: pyx-mem login-recovery",
|
|
136
|
+
retryable: true
|
|
137
|
+
},
|
|
138
|
+
credential_store_unavailable: {
|
|
139
|
+
message: "The OS credential store is unavailable.",
|
|
140
|
+
retryable: false
|
|
141
|
+
},
|
|
142
|
+
journal_invalid: {
|
|
143
|
+
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).',
|
|
144
|
+
retryable: false
|
|
145
|
+
},
|
|
146
|
+
login_failed: { message: "Browser sign-in failed.", retryable: true }
|
|
147
|
+
};
|
|
148
|
+
var SERVER_CODES = {
|
|
149
|
+
rate_limited: "rate_limited",
|
|
150
|
+
cli_login_expired_or_unknown: "expired",
|
|
151
|
+
cli_login_project_not_eligible: "project_not_eligible",
|
|
152
|
+
cli_login_cancelled: "cancelled",
|
|
153
|
+
cli_login_denied: "cancelled"
|
|
154
|
+
};
|
|
155
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
|
|
156
|
+
var BROWSER_ORIGINS = {
|
|
157
|
+
"https://memory.api.pyxmate.com": "https://memory.pyxmate.com"
|
|
158
|
+
};
|
|
159
|
+
function parseSafeUrl(input) {
|
|
160
|
+
let url;
|
|
161
|
+
try {
|
|
162
|
+
url = new URL(input);
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const secure = url.protocol === "https:" || url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
|
|
167
|
+
if (!secure || url.username !== "" || url.password !== "" || url.hash !== "") return null;
|
|
168
|
+
return url;
|
|
169
|
+
}
|
|
170
|
+
function parseEndpoint(input) {
|
|
171
|
+
const url = parseSafeUrl(input);
|
|
172
|
+
if (url === null || url.search !== "" || input.includes("?") || input.includes("#")) return null;
|
|
173
|
+
const normalized = url.href.replace(/\/+$/, "");
|
|
174
|
+
return normalized.length <= MAX_ENDPOINT_CHARS ? normalized : null;
|
|
175
|
+
}
|
|
176
|
+
function boundLabel(value, maxChars) {
|
|
177
|
+
let label = value.replace(/\p{Cc}/gu, "").slice(0, maxChars);
|
|
178
|
+
while (JSON.stringify(label).length - 2 > maxChars) label = label.slice(0, -1);
|
|
179
|
+
return label;
|
|
180
|
+
}
|
|
181
|
+
function isTrustedAuthorizationUrl(authorizationUrl, endpoint, requestId) {
|
|
182
|
+
const url = parseSafeUrl(authorizationUrl);
|
|
183
|
+
if (url === null) return false;
|
|
184
|
+
const api = new URL(endpoint);
|
|
185
|
+
const trustedOrigin = LOOPBACK_HOSTS.has(api.hostname) ? LOOPBACK_HOSTS.has(url.hostname) : url.origin === (BROWSER_ORIGINS[api.origin] ?? api.origin);
|
|
186
|
+
const params = [...url.searchParams];
|
|
187
|
+
return trustedOrigin && url.pathname === "/cli/authorize" && params.length === 1 && params[0]?.[0] === "request" && params[0][1] === requestId;
|
|
188
|
+
}
|
|
189
|
+
async function readBounded(res) {
|
|
190
|
+
const reader = res.body?.getReader();
|
|
191
|
+
if (!reader) return void 0;
|
|
192
|
+
const chunks = [];
|
|
193
|
+
let size = 0;
|
|
194
|
+
for (; ; ) {
|
|
195
|
+
const { done, value } = await reader.read();
|
|
196
|
+
if (done) break;
|
|
197
|
+
size += value.byteLength;
|
|
198
|
+
if (size > MAX_RESPONSE_BYTES) {
|
|
199
|
+
await reader.cancel();
|
|
200
|
+
return void 0;
|
|
201
|
+
}
|
|
202
|
+
chunks.push(value);
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
206
|
+
} catch {
|
|
207
|
+
return void 0;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function field(json, key) {
|
|
211
|
+
return typeof json === "object" && json !== null ? json[key] : void 0;
|
|
212
|
+
}
|
|
213
|
+
function failure(status, json) {
|
|
214
|
+
const serverCode = field(field(json, "error"), "code");
|
|
215
|
+
const mapped = typeof serverCode === "string" ? SERVER_CODES[serverCode] : void 0;
|
|
216
|
+
return { ok: false, code: mapped ?? (status === 429 ? "rate_limited" : "login_failed") };
|
|
217
|
+
}
|
|
218
|
+
function createCliLoginApi(fetchImpl = fetch) {
|
|
219
|
+
async function post(url, body, bearer, signal) {
|
|
220
|
+
const ctl = new AbortController();
|
|
221
|
+
const abort = () => ctl.abort();
|
|
222
|
+
const timer = setTimeout(abort, REQUEST_TIMEOUT_MS);
|
|
223
|
+
signal?.addEventListener("abort", abort);
|
|
224
|
+
try {
|
|
225
|
+
if (signal?.aborted) return null;
|
|
226
|
+
const res = await fetchImpl(url, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: {
|
|
229
|
+
Accept: "application/json",
|
|
230
|
+
"Content-Type": "application/json",
|
|
231
|
+
...bearer === void 0 ? {} : { Authorization: `Bearer ${bearer}` }
|
|
232
|
+
},
|
|
233
|
+
body: JSON.stringify(body),
|
|
234
|
+
// A redirect would forward the proof or the key to another origin.
|
|
235
|
+
redirect: "error",
|
|
236
|
+
signal: ctl.signal
|
|
237
|
+
});
|
|
238
|
+
return { status: res.status, json: await readBounded(res) };
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
} finally {
|
|
242
|
+
clearTimeout(timer);
|
|
243
|
+
signal?.removeEventListener("abort", abort);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const base = (endpoint) => `${endpoint}/auth/cli-login`;
|
|
247
|
+
return {
|
|
248
|
+
async start(endpoint, body, signal) {
|
|
249
|
+
const res = await post(`${base(endpoint)}/start`, body, void 0, signal);
|
|
250
|
+
if (res === null) return { ok: false, code: "network_error" };
|
|
251
|
+
if (res.status === 404) return { ok: false, code: "browser_login_unsupported" };
|
|
252
|
+
if (res.status !== 201) return failure(res.status, res.json);
|
|
253
|
+
const requestId = field(res.json, "requestId");
|
|
254
|
+
const authorizationUrl = field(res.json, "authorizationUrl");
|
|
255
|
+
const expiresAt = new Date(String(field(res.json, "expiresAt")));
|
|
256
|
+
if (typeof requestId !== "string" || !REQUEST_ID_RE.test(requestId) || typeof authorizationUrl !== "string" || Number.isNaN(expiresAt.getTime())) {
|
|
257
|
+
return { ok: false, code: "login_failed" };
|
|
258
|
+
}
|
|
259
|
+
if (!isTrustedAuthorizationUrl(authorizationUrl, endpoint, requestId)) {
|
|
260
|
+
return { ok: false, code: "untrusted_authorization_url" };
|
|
261
|
+
}
|
|
262
|
+
return { ok: true, requestId, authorizationUrl, expiresAt };
|
|
263
|
+
},
|
|
264
|
+
async redeem(login, code, signal) {
|
|
265
|
+
const res = await post(
|
|
266
|
+
`${base(login.endpoint)}/${login.requestId}/redeem`,
|
|
267
|
+
{ code, codeVerifier: login.codeVerifier },
|
|
268
|
+
void 0,
|
|
269
|
+
signal
|
|
270
|
+
);
|
|
271
|
+
if (res === null) return { ok: false, code: "network_error" };
|
|
272
|
+
if (res.status !== 200) return failure(res.status, res.json);
|
|
273
|
+
const credential = field(res.json, "credential");
|
|
274
|
+
const apiKey = field(credential, "apiKey");
|
|
275
|
+
const connection = field(credential, "connection");
|
|
276
|
+
const accountLabel = field(connection, "accountLabel");
|
|
277
|
+
const projectId = field(connection, "projectId");
|
|
278
|
+
const projectName = field(connection, "projectName");
|
|
279
|
+
if (typeof apiKey !== "string" || !PROJECT_KEY_RE.test(apiKey) || typeof accountLabel !== "string" || typeof projectId !== "string" || typeof projectName !== "string") {
|
|
280
|
+
return { ok: false, code: "login_failed" };
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
ok: true,
|
|
284
|
+
apiKey,
|
|
285
|
+
// Display metadata only; bounded so the shared record fits every OS store.
|
|
286
|
+
connection: {
|
|
287
|
+
accountLabel: boundLabel(accountLabel, 254),
|
|
288
|
+
projectId: boundLabel(projectId, 64),
|
|
289
|
+
projectName: boundLabel(projectName, 128)
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
async confirm(login, signal) {
|
|
294
|
+
const res = await post(
|
|
295
|
+
`${base(login.endpoint)}/${login.requestId}/confirm`,
|
|
296
|
+
{ codeVerifier: login.codeVerifier },
|
|
297
|
+
login.apiKey,
|
|
298
|
+
signal
|
|
299
|
+
);
|
|
300
|
+
if (res === null) return { ok: false, code: "network_error" };
|
|
301
|
+
if (res.status !== 200 || field(res.json, "status") !== "confirmed") {
|
|
302
|
+
return failure(res.status, res.json);
|
|
303
|
+
}
|
|
304
|
+
return { ok: true };
|
|
305
|
+
},
|
|
306
|
+
async cancel(login) {
|
|
307
|
+
const res = await post(`${base(login.endpoint)}/${login.requestId}/cancel`, {
|
|
308
|
+
codeVerifier: login.codeVerifier
|
|
309
|
+
});
|
|
310
|
+
if (res === null) return false;
|
|
311
|
+
if (res.status === 200) return field(res.json, "status") === "cancelled";
|
|
312
|
+
return res.status === 404 && field(field(res.json, "error"), "code") === "cli_login_expired_or_unknown";
|
|
313
|
+
},
|
|
314
|
+
async revoke(key) {
|
|
315
|
+
const res = await post(`${base(key.endpoint)}/revoke`, {}, key.apiKey);
|
|
316
|
+
return res !== null && res.status === 200 && field(res.json, "status") === "revoked";
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/cli/lifecycle/journal.ts
|
|
322
|
+
var MAX_STORE_CHARS = 1200;
|
|
323
|
+
var MAX_REVOKE_ENTRIES = 8;
|
|
324
|
+
var TOKEN_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
325
|
+
var FINGERPRINT_RE = /^[0-9a-f]{16}$/;
|
|
326
|
+
var PLACEHOLDER_KEY = `pyx_proj_${"x".repeat(160)}`;
|
|
327
|
+
var SERVER_CONVERGENCE_SECONDS = 30;
|
|
328
|
+
var JournalInvalidError = class extends Error {
|
|
329
|
+
};
|
|
330
|
+
var JournalFullError = class extends Error {
|
|
331
|
+
};
|
|
332
|
+
function credentialFingerprint(endpoint, apiKey) {
|
|
333
|
+
return createHash("sha256").update(`${endpoint}
|
|
334
|
+
${apiKey}`).digest("hex").slice(0, 16);
|
|
335
|
+
}
|
|
336
|
+
function isEmpty(journal) {
|
|
337
|
+
return journal.login === null && journal.logout === null && journal.revoke.length === 0;
|
|
338
|
+
}
|
|
339
|
+
function emptyJournal() {
|
|
340
|
+
return { schemaVersion: 1, login: null, logout: null, revoke: [] };
|
|
341
|
+
}
|
|
342
|
+
function convergenceSeconds(journal, now = Date.now()) {
|
|
343
|
+
if (journal.revokedAt === void 0) return null;
|
|
344
|
+
const elapsed = Math.max(now - journal.revokedAt, 0);
|
|
345
|
+
const left = Math.ceil(SERVER_CONVERGENCE_SECONDS - elapsed / 1e3);
|
|
346
|
+
return left > 0 ? left : null;
|
|
347
|
+
}
|
|
348
|
+
function isExactObject(value, required, optional = []) {
|
|
349
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
350
|
+
const keys = Object.keys(value);
|
|
351
|
+
return required.every((key) => keys.includes(key)) && keys.every((key) => required.includes(key) || optional.includes(key));
|
|
352
|
+
}
|
|
353
|
+
function isKeyRef(value) {
|
|
354
|
+
if (!isExactObject(value, ["endpoint", "apiKey"])) return false;
|
|
355
|
+
const v = value;
|
|
356
|
+
return typeof v.endpoint === "string" && parseEndpoint(v.endpoint) === v.endpoint && typeof v.apiKey === "string" && PROJECT_KEY_RE.test(v.apiKey);
|
|
357
|
+
}
|
|
358
|
+
function isLoginRecord(value) {
|
|
359
|
+
if (!isExactObject(
|
|
360
|
+
value,
|
|
361
|
+
["phase", "endpoint", "requestId", "codeVerifier", "expiresAt"],
|
|
362
|
+
["apiKey", "previous"]
|
|
363
|
+
)) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
const v = value;
|
|
367
|
+
const held = v.phase === "issued" || v.phase === "committing";
|
|
368
|
+
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));
|
|
369
|
+
}
|
|
370
|
+
function isLogoutRecord(value) {
|
|
371
|
+
if (!isExactObject(value, ["fingerprint", "revoke"])) return false;
|
|
372
|
+
const v = value;
|
|
373
|
+
return typeof v.fingerprint === "string" && FINGERPRINT_RE.test(v.fingerprint) && (v.revoke === null || isKeyRef(v.revoke));
|
|
374
|
+
}
|
|
375
|
+
function isJournal(value) {
|
|
376
|
+
if (!isExactObject(value, ["schemaVersion", "login", "logout", "revoke"], ["revokedAt"])) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
const v = value;
|
|
380
|
+
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);
|
|
381
|
+
}
|
|
382
|
+
function hasRoomForLogin(journal, endpoint, previous) {
|
|
383
|
+
const largest = {
|
|
384
|
+
...journal,
|
|
385
|
+
login: {
|
|
386
|
+
phase: "committing",
|
|
387
|
+
endpoint,
|
|
388
|
+
requestId: "x".repeat(43),
|
|
389
|
+
codeVerifier: "x".repeat(43),
|
|
390
|
+
expiresAt: Number.MAX_SAFE_INTEGER,
|
|
391
|
+
apiKey: PLACEHOLDER_KEY,
|
|
392
|
+
...previous ? { previous } : {}
|
|
393
|
+
},
|
|
394
|
+
revokedAt: Number.MAX_SAFE_INTEGER
|
|
395
|
+
};
|
|
396
|
+
return JSON.stringify(largest).length <= MAX_STORE_CHARS;
|
|
397
|
+
}
|
|
398
|
+
async function loadJournal(provider) {
|
|
399
|
+
const raw = await provider.readJournal();
|
|
400
|
+
if (raw === null) return emptyJournal();
|
|
401
|
+
if (raw.length > MAX_STORE_CHARS) throw new JournalInvalidError();
|
|
402
|
+
let parsed;
|
|
403
|
+
try {
|
|
404
|
+
parsed = JSON.parse(raw);
|
|
405
|
+
} catch {
|
|
406
|
+
throw new JournalInvalidError();
|
|
407
|
+
}
|
|
408
|
+
if (!isJournal(parsed)) throw new JournalInvalidError();
|
|
409
|
+
return parsed;
|
|
410
|
+
}
|
|
411
|
+
async function saveJournal(provider, journal) {
|
|
412
|
+
if (isEmpty(journal) && convergenceSeconds(journal) === null) return provider.clearJournal();
|
|
413
|
+
const raw = JSON.stringify(journal);
|
|
414
|
+
if (raw.length > MAX_STORE_CHARS) throw new JournalFullError();
|
|
415
|
+
if (!isJournal(JSON.parse(raw))) throw new Error("refusing to write an invalid journal");
|
|
416
|
+
await provider.writeJournal(raw);
|
|
417
|
+
}
|
|
418
|
+
async function supersedeLogout(provider, endpoint, apiKey) {
|
|
419
|
+
let journal;
|
|
420
|
+
try {
|
|
421
|
+
journal = await loadJournal(provider);
|
|
422
|
+
} catch (err) {
|
|
423
|
+
if (err instanceof JournalInvalidError) return;
|
|
424
|
+
throw err;
|
|
425
|
+
}
|
|
426
|
+
if (journal.logout?.fingerprint !== credentialFingerprint(endpoint, apiKey)) return;
|
|
427
|
+
journal.logout = null;
|
|
428
|
+
await saveJournal(provider, journal);
|
|
429
|
+
}
|
|
430
|
+
function guardKeychain(provider) {
|
|
431
|
+
return {
|
|
432
|
+
write: (credentials) => provider.write(credentials),
|
|
433
|
+
delete: () => provider.delete(),
|
|
434
|
+
check: () => provider.check(),
|
|
435
|
+
readJournal: () => provider.readJournal(),
|
|
436
|
+
writeJournal: (raw) => provider.writeJournal(raw),
|
|
437
|
+
clearJournal: () => provider.clearJournal(),
|
|
438
|
+
acquireLock: (onLost) => provider.acquireLock(onLost),
|
|
439
|
+
isLocked: () => provider.isLocked(),
|
|
440
|
+
async read() {
|
|
441
|
+
const credentials = await provider.read();
|
|
442
|
+
if (credentials === null) return null;
|
|
443
|
+
let blocked;
|
|
444
|
+
try {
|
|
445
|
+
const { logout } = await loadJournal(provider);
|
|
446
|
+
blocked = logout?.fingerprint === credentialFingerprint(credentials.endpoint, credentials.apiKey);
|
|
447
|
+
} catch (err) {
|
|
448
|
+
if (!(err instanceof JournalInvalidError)) throw err;
|
|
449
|
+
blocked = true;
|
|
450
|
+
}
|
|
451
|
+
if (blocked) {
|
|
452
|
+
throw new KeychainError({
|
|
453
|
+
code: "logout-incomplete",
|
|
454
|
+
message: "A pyx-memory sign-out did not finish on this computer.",
|
|
455
|
+
guidance: "Run: pyx-mem login-recovery",
|
|
456
|
+
exit: EXIT.NOT_LOGGED_IN
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
return credentials;
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/cli/keychain/napi-provider.ts
|
|
465
|
+
import { homedir } from "os";
|
|
466
|
+
import { join } from "path";
|
|
467
|
+
|
|
468
|
+
// src/cli/lifecycle/lock.ts
|
|
469
|
+
import { mkdir } from "fs/promises";
|
|
470
|
+
import { dirname } from "path";
|
|
471
|
+
import lockfile from "proper-lockfile";
|
|
472
|
+
var LOCK_OPTIONS = { realpath: false, stale: 15e3 };
|
|
473
|
+
function createFileLock(file) {
|
|
474
|
+
return {
|
|
475
|
+
async acquire(onLost) {
|
|
476
|
+
await mkdir(dirname(file), { recursive: true, mode: 448 });
|
|
477
|
+
let release;
|
|
478
|
+
try {
|
|
479
|
+
release = await lockfile.lock(file, { ...LOCK_OPTIONS, onCompromised: () => onLost() });
|
|
480
|
+
} catch (err) {
|
|
481
|
+
if (err.code === "ELOCKED") return null;
|
|
482
|
+
throw err;
|
|
483
|
+
}
|
|
484
|
+
return async () => {
|
|
485
|
+
try {
|
|
486
|
+
await release();
|
|
487
|
+
} catch (err) {
|
|
488
|
+
const code = err.code;
|
|
489
|
+
if (code !== "ERELEASED" && code !== "ENOTACQUIRED") throw err;
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
},
|
|
493
|
+
isLocked: () => lockfile.check(file, LOCK_OPTIONS)
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
var BUSY = /* @__PURE__ */ Symbol("lifecycle-lock-busy");
|
|
497
|
+
var LockLostError = class extends Error {
|
|
498
|
+
};
|
|
499
|
+
async function releaseLifecycleLock(release) {
|
|
500
|
+
try {
|
|
501
|
+
await release();
|
|
502
|
+
} catch (err) {
|
|
503
|
+
const code = err?.code;
|
|
504
|
+
const reason = typeof code === "string" && /^[A-Z_]{1,32}$/.test(code) ? code : "unknown";
|
|
505
|
+
process.stderr.write(
|
|
506
|
+
`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.
|
|
507
|
+
`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async function withLifecycleLock(provider, fn) {
|
|
512
|
+
let lost = false;
|
|
513
|
+
const release = await provider.acquireLock(() => {
|
|
514
|
+
lost = true;
|
|
515
|
+
});
|
|
516
|
+
if (release === null) return BUSY;
|
|
517
|
+
try {
|
|
518
|
+
return await fn(() => lost);
|
|
519
|
+
} finally {
|
|
520
|
+
await releaseLifecycleLock(release);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
93
524
|
// src/cli/keychain/types.ts
|
|
94
525
|
var KEYCHAIN_SERVICE = "pyx-memory";
|
|
95
526
|
var KEYCHAIN_ACCOUNT = "default";
|
|
96
527
|
var CREDENTIAL_SCHEMA_VERSION = 1;
|
|
528
|
+
var JOURNAL_ACCOUNT = "lifecycle-journal";
|
|
97
529
|
|
|
98
530
|
// src/cli/keychain/napi-provider.ts
|
|
99
|
-
var cached =
|
|
100
|
-
async function loadEntry() {
|
|
101
|
-
|
|
531
|
+
var cached = /* @__PURE__ */ new Map();
|
|
532
|
+
async function loadEntry(account = KEYCHAIN_ACCOUNT) {
|
|
533
|
+
const hit = cached.get(account);
|
|
534
|
+
if (hit) return hit;
|
|
102
535
|
try {
|
|
103
536
|
const mod = await import("@napi-rs/keyring");
|
|
104
|
-
|
|
105
|
-
|
|
537
|
+
const entry = new mod.Entry(KEYCHAIN_SERVICE, account);
|
|
538
|
+
cached.set(account, entry);
|
|
539
|
+
return entry;
|
|
106
540
|
} catch (err) {
|
|
107
541
|
throw providerImportError(err);
|
|
108
542
|
}
|
|
@@ -120,9 +554,16 @@ function parseRecord(raw) {
|
|
|
120
554
|
function isValidRecord(value) {
|
|
121
555
|
if (typeof value !== "object" || value === null) return false;
|
|
122
556
|
const v = value;
|
|
123
|
-
return v.schemaVersion === CREDENTIAL_SCHEMA_VERSION && typeof v.endpoint === "string" && v.endpoint.length > 0 && typeof v.apiKey === "string" && v.apiKey.length > 0;
|
|
557
|
+
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));
|
|
558
|
+
}
|
|
559
|
+
function isConnection(value) {
|
|
560
|
+
if (typeof value !== "object" || value === null) return false;
|
|
561
|
+
const c = value;
|
|
562
|
+
return typeof c.accountLabel === "string" && typeof c.projectId === "string" && typeof c.projectName === "string";
|
|
124
563
|
}
|
|
125
564
|
var NapiKeychainProvider = class {
|
|
565
|
+
// The lock file holds no secrets; credentials and the journal stay in the OS store.
|
|
566
|
+
lock = createFileLock(join(homedir(), ".pyx-memory", "lifecycle"));
|
|
126
567
|
async read() {
|
|
127
568
|
const entry = await loadEntry();
|
|
128
569
|
let raw;
|
|
@@ -139,7 +580,8 @@ var NapiKeychainProvider = class {
|
|
|
139
580
|
const payload = JSON.stringify({
|
|
140
581
|
schemaVersion: CREDENTIAL_SCHEMA_VERSION,
|
|
141
582
|
endpoint: credentials.endpoint,
|
|
142
|
-
apiKey: credentials.apiKey
|
|
583
|
+
apiKey: credentials.apiKey,
|
|
584
|
+
...credentials.provenance === "browser-login" ? { connection: credentials.connection, provenance: credentials.provenance } : {}
|
|
143
585
|
});
|
|
144
586
|
try {
|
|
145
587
|
entry.setPassword(payload);
|
|
@@ -155,6 +597,36 @@ var NapiKeychainProvider = class {
|
|
|
155
597
|
throw platformAccessError(err);
|
|
156
598
|
}
|
|
157
599
|
}
|
|
600
|
+
async readJournal() {
|
|
601
|
+
const entry = await loadEntry(JOURNAL_ACCOUNT);
|
|
602
|
+
try {
|
|
603
|
+
return entry.getPassword();
|
|
604
|
+
} catch (err) {
|
|
605
|
+
throw platformAccessError(err);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
async writeJournal(raw) {
|
|
609
|
+
const entry = await loadEntry(JOURNAL_ACCOUNT);
|
|
610
|
+
try {
|
|
611
|
+
entry.setPassword(raw);
|
|
612
|
+
} catch (err) {
|
|
613
|
+
throw platformAccessError(err);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
async clearJournal() {
|
|
617
|
+
const entry = await loadEntry(JOURNAL_ACCOUNT);
|
|
618
|
+
try {
|
|
619
|
+
entry.deletePassword();
|
|
620
|
+
} catch (err) {
|
|
621
|
+
throw platformAccessError(err);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
acquireLock(onLost) {
|
|
625
|
+
return this.lock.acquire(onLost);
|
|
626
|
+
}
|
|
627
|
+
isLocked() {
|
|
628
|
+
return this.lock.isLocked();
|
|
629
|
+
}
|
|
158
630
|
async check() {
|
|
159
631
|
try {
|
|
160
632
|
const entry = await loadEntry();
|
|
@@ -172,6 +644,9 @@ var NapiKeychainProvider = class {
|
|
|
172
644
|
|
|
173
645
|
// src/cli/keychain/index.ts
|
|
174
646
|
function getDefaultKeychain() {
|
|
647
|
+
return guardKeychain(new NapiKeychainProvider());
|
|
648
|
+
}
|
|
649
|
+
function getLifecycleKeychain() {
|
|
175
650
|
return new NapiKeychainProvider();
|
|
176
651
|
}
|
|
177
652
|
|
|
@@ -511,6 +986,7 @@ async function readSurface(opts) {
|
|
|
511
986
|
state: "error",
|
|
512
987
|
endpoint: null,
|
|
513
988
|
keySource: null,
|
|
989
|
+
credentialFingerprint: null,
|
|
514
990
|
problem: error instanceof KeychainError ? [error.message, error.guidance].filter(Boolean).join(" ") : "The OS credential store could not be read."
|
|
515
991
|
};
|
|
516
992
|
}
|
|
@@ -521,9 +997,11 @@ async function readSurface(opts) {
|
|
|
521
997
|
state: "signed_out",
|
|
522
998
|
endpoint: null,
|
|
523
999
|
keySource: null,
|
|
1000
|
+
credentialFingerprint: null,
|
|
524
1001
|
problem: null
|
|
525
1002
|
};
|
|
526
1003
|
}
|
|
1004
|
+
const fingerprint = credentialFingerprint(credentials.endpoint, credentials.apiKey);
|
|
527
1005
|
if (!isHttpUrl(credentials.endpoint)) {
|
|
528
1006
|
return {
|
|
529
1007
|
ok: false,
|
|
@@ -531,6 +1009,7 @@ async function readSurface(opts) {
|
|
|
531
1009
|
state: "error",
|
|
532
1010
|
endpoint: null,
|
|
533
1011
|
keySource: "keychain",
|
|
1012
|
+
credentialFingerprint: fingerprint,
|
|
534
1013
|
problem: "The stored pyx-memory endpoint is invalid. Run: pyx-mem login"
|
|
535
1014
|
};
|
|
536
1015
|
}
|
|
@@ -544,6 +1023,7 @@ async function readSurface(opts) {
|
|
|
544
1023
|
opts.surface,
|
|
545
1024
|
response.status,
|
|
546
1025
|
credentials.endpoint,
|
|
1026
|
+
fingerprint,
|
|
547
1027
|
opts.minimumServiceVersion,
|
|
548
1028
|
http
|
|
549
1029
|
);
|
|
@@ -556,12 +1036,18 @@ async function readSurface(opts) {
|
|
|
556
1036
|
state: "error",
|
|
557
1037
|
endpoint: credentials.endpoint,
|
|
558
1038
|
keySource: "keychain",
|
|
1039
|
+
credentialFingerprint: fingerprint,
|
|
559
1040
|
problem: `pyx-memory returned an invalid ${opts.surface} response. Run: pyx-mem doctor`
|
|
560
1041
|
};
|
|
561
1042
|
}
|
|
562
|
-
return {
|
|
1043
|
+
return {
|
|
1044
|
+
ok: true,
|
|
1045
|
+
endpoint: credentials.endpoint,
|
|
1046
|
+
credentialFingerprint: fingerprint,
|
|
1047
|
+
data: parsed.data
|
|
1048
|
+
};
|
|
563
1049
|
}
|
|
564
|
-
async function failedSurfaceRequest(surface, status, endpoint, minimumServiceVersion, http) {
|
|
1050
|
+
async function failedSurfaceRequest(surface, status, endpoint, credentialFingerprint2, minimumServiceVersion, http) {
|
|
565
1051
|
if (status === 401) {
|
|
566
1052
|
return {
|
|
567
1053
|
ok: false,
|
|
@@ -569,6 +1055,7 @@ async function failedSurfaceRequest(surface, status, endpoint, minimumServiceVer
|
|
|
569
1055
|
state: "signed_out",
|
|
570
1056
|
endpoint,
|
|
571
1057
|
keySource: "keychain",
|
|
1058
|
+
credentialFingerprint: credentialFingerprint2,
|
|
572
1059
|
problem: requestFailure(surface, status)
|
|
573
1060
|
};
|
|
574
1061
|
}
|
|
@@ -580,6 +1067,7 @@ async function failedSurfaceRequest(surface, status, endpoint, minimumServiceVer
|
|
|
580
1067
|
state: serviceOutdated ? "service_outdated" : "error",
|
|
581
1068
|
endpoint,
|
|
582
1069
|
keySource: "keychain",
|
|
1070
|
+
credentialFingerprint: credentialFingerprint2,
|
|
583
1071
|
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)
|
|
584
1072
|
};
|
|
585
1073
|
}
|
|
@@ -699,8 +1187,14 @@ async function insightsCommand(opts = {}) {
|
|
|
699
1187
|
keychain: opts.keychain,
|
|
700
1188
|
fetchImpl: opts.fetchImpl
|
|
701
1189
|
});
|
|
1190
|
+
const withIdentity = opts.withIdentity === true;
|
|
702
1191
|
if (!result.ok) {
|
|
703
|
-
emit(
|
|
1192
|
+
emit(
|
|
1193
|
+
report(result.state, result.endpoint, result.keySource, result.problem, {
|
|
1194
|
+
withIdentity,
|
|
1195
|
+
credentialFingerprint: result.credentialFingerprint
|
|
1196
|
+
})
|
|
1197
|
+
);
|
|
704
1198
|
return result.exit;
|
|
705
1199
|
}
|
|
706
1200
|
emit({
|
|
@@ -708,17 +1202,22 @@ async function insightsCommand(opts = {}) {
|
|
|
708
1202
|
state: "ready",
|
|
709
1203
|
endpoint: result.endpoint,
|
|
710
1204
|
keySource: "keychain",
|
|
1205
|
+
...identityField({ withIdentity, credentialFingerprint: result.credentialFingerprint }),
|
|
711
1206
|
...result.data,
|
|
712
1207
|
problem: null
|
|
713
1208
|
});
|
|
714
1209
|
return EXIT.OK;
|
|
715
1210
|
}
|
|
716
|
-
function
|
|
1211
|
+
function identityField(identity) {
|
|
1212
|
+
return identity.withIdentity ? { credentialFingerprint: identity.credentialFingerprint } : {};
|
|
1213
|
+
}
|
|
1214
|
+
function report(state, endpoint, keySource, problem, identity) {
|
|
717
1215
|
return {
|
|
718
1216
|
schemaVersion: INSIGHTS_SCHEMA_VERSION,
|
|
719
1217
|
state,
|
|
720
1218
|
endpoint,
|
|
721
1219
|
keySource,
|
|
1220
|
+
...identityField(identity),
|
|
722
1221
|
generatedAt: null,
|
|
723
1222
|
scope: "connected_memory",
|
|
724
1223
|
coverage: null,
|
|
@@ -812,7 +1311,7 @@ async function readSingleLineFromStdin() {
|
|
|
812
1311
|
// src/cli/commands/login.ts
|
|
813
1312
|
var DEFAULT_ENDPOINT = "https://memory.api.pyxmate.com";
|
|
814
1313
|
async function loginCommand(opts = {}) {
|
|
815
|
-
const provider = opts.keychain ??
|
|
1314
|
+
const provider = opts.keychain ?? getLifecycleKeychain();
|
|
816
1315
|
const fetcher = opts.fetchImpl ?? fetch;
|
|
817
1316
|
const endpoint = normalizeEndpoint(
|
|
818
1317
|
opts.endpoint ?? await readExistingEndpoint(provider) ?? DEFAULT_ENDPOINT
|
|
@@ -844,7 +1343,16 @@ No credentials were written.
|
|
|
844
1343
|
return EXIT.NOT_LOGGED_IN;
|
|
845
1344
|
}
|
|
846
1345
|
try {
|
|
847
|
-
|
|
1346
|
+
const written = await withLifecycleLock(provider, async () => {
|
|
1347
|
+
await supersedeLogout(provider, endpoint, apiKey);
|
|
1348
|
+
await provider.write({ schemaVersion: 1, endpoint, apiKey });
|
|
1349
|
+
});
|
|
1350
|
+
if (written === BUSY) {
|
|
1351
|
+
process.stderr.write(
|
|
1352
|
+
"Error: another pyx-mem sign-in or sign-out is in progress. Retry in a moment.\nNo credentials were written.\n"
|
|
1353
|
+
);
|
|
1354
|
+
return EXIT.BUSY;
|
|
1355
|
+
}
|
|
848
1356
|
} catch (err) {
|
|
849
1357
|
if (err instanceof KeychainError) {
|
|
850
1358
|
process.stderr.write(
|
|
@@ -888,21 +1396,617 @@ function isHttpUrl2(value) {
|
|
|
888
1396
|
}
|
|
889
1397
|
}
|
|
890
1398
|
|
|
1399
|
+
// src/cli/commands/login-browser.ts
|
|
1400
|
+
import { spawn } from "child_process";
|
|
1401
|
+
import { createHash as createHash3, randomBytes } from "crypto";
|
|
1402
|
+
import { hostname } from "os";
|
|
1403
|
+
import { createInterface } from "readline";
|
|
1404
|
+
|
|
1405
|
+
// src/cli/lifecycle/callback-server.ts
|
|
1406
|
+
import { createHash as createHash2, timingSafeEqual } from "crypto";
|
|
1407
|
+
import { createServer } from "http";
|
|
1408
|
+
var CALLBACK_PATH = "/pyx-memory/callback";
|
|
1409
|
+
var CODE_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
1410
|
+
var STYLES = `
|
|
1411
|
+
:root { color-scheme: dark; font-family: "Avenir Next", "Trebuchet MS", sans-serif; }
|
|
1412
|
+
* { box-sizing: border-box; }
|
|
1413
|
+
body {
|
|
1414
|
+
margin: 0; min-height: 100vh; min-height: 100svh; padding: 32px 20px;
|
|
1415
|
+
display: flex; color: #f7f8fb; background: #0b0b0e;
|
|
1416
|
+
background-image: radial-gradient(ellipse at 50% 55%, #0071e31c, transparent 65%);
|
|
1417
|
+
}
|
|
1418
|
+
main { width: 100%; max-width: 496px; margin: auto; }
|
|
1419
|
+
.brand { margin: 0 0 20px 8px; font-size: 15px; font-weight: 600; letter-spacing: -.02em; }
|
|
1420
|
+
.brand span { margin-left: 8px; color: #a9adb8; font-weight: 400; }
|
|
1421
|
+
.card {
|
|
1422
|
+
padding: clamp(24px, 6vw, 40px); border: 1px solid #30333c; border-radius: 28px;
|
|
1423
|
+
background: #15161c; box-shadow: 0 24px 80px #0006;
|
|
1424
|
+
}
|
|
1425
|
+
.symbol {
|
|
1426
|
+
display: grid; place-items: center; width: 64px; height: 64px; margin-bottom: 32px;
|
|
1427
|
+
border: 1px solid #2c4059; border-radius: 20px; color: #6ab4ff; background: #12263a;
|
|
1428
|
+
}
|
|
1429
|
+
.continue .symbol { animation: arrive 360ms cubic-bezier(.22, 1, .36, 1); }
|
|
1430
|
+
.cancelled .symbol { color: #a9adb8; background: #20222b; border-color: #383b47; }
|
|
1431
|
+
.invalid .symbol { color: #f4bd7a; background: #302719; border-color: #59462d; }
|
|
1432
|
+
h1 {
|
|
1433
|
+
margin: 0 0 20px; font-size: clamp(28px, 6vw, 40px); font-weight: 600;
|
|
1434
|
+
line-height: 1.12; letter-spacing: -.035em; overflow-wrap: break-word;
|
|
1435
|
+
}
|
|
1436
|
+
p { margin: 0; color: #a9adb8; font-size: 16px; line-height: 1.65; }
|
|
1437
|
+
.note { margin-top: 32px; padding-top: 24px; border-top: 1px solid #30333c; font-size: 14px; }
|
|
1438
|
+
@keyframes arrive { from { opacity: .65; transform: translateY(4px); } }
|
|
1439
|
+
@media (prefers-reduced-motion: reduce) { .continue .symbol { animation: none; } }
|
|
1440
|
+
`;
|
|
1441
|
+
var PAGE_CSP = `default-src 'none'; style-src 'sha256-${createHash2("sha256").update(STYLES).digest("base64")}'; base-uri 'none'; form-action 'none'`;
|
|
1442
|
+
var page = (state, title, text, icon) => `<!doctype html>
|
|
1443
|
+
<html lang="en">
|
|
1444
|
+
<head>
|
|
1445
|
+
<meta charset="utf-8">
|
|
1446
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1447
|
+
<title>${title} \xB7 Pyx Memory</title>
|
|
1448
|
+
<style>${STYLES}</style>
|
|
1449
|
+
</head>
|
|
1450
|
+
<body>
|
|
1451
|
+
<main class="${state}">
|
|
1452
|
+
<div class="brand">pyx<span>Memory</span></div>
|
|
1453
|
+
<section class="card" aria-labelledby="title">
|
|
1454
|
+
<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>
|
|
1455
|
+
<h1 id="title">${title}</h1>
|
|
1456
|
+
<p>${text}</p>
|
|
1457
|
+
<p class="note">You can close this tab.</p>
|
|
1458
|
+
</section>
|
|
1459
|
+
</main>
|
|
1460
|
+
</body>
|
|
1461
|
+
</html>`;
|
|
1462
|
+
var ACCEPTED_PAGE = page(
|
|
1463
|
+
"continue",
|
|
1464
|
+
"Continue in your app",
|
|
1465
|
+
"Your approval was received. Return to the app that started this sign-in to finish connecting.",
|
|
1466
|
+
'<path d="m9 4-5 5 5 5M4 9h10a6 6 0 0 1 0 12h-2"/>'
|
|
1467
|
+
);
|
|
1468
|
+
var DENIED_PAGE = page(
|
|
1469
|
+
"cancelled",
|
|
1470
|
+
"Sign-in cancelled",
|
|
1471
|
+
"Return to the app that started this sign-in. You can try again there whenever you\u2019re ready.",
|
|
1472
|
+
'<path d="m7 7 10 10M17 7 7 17"/>'
|
|
1473
|
+
);
|
|
1474
|
+
var REJECTED_PAGE = page(
|
|
1475
|
+
"invalid",
|
|
1476
|
+
"This sign-in link isn\u2019t valid",
|
|
1477
|
+
"Return to the app that started this sign-in to continue or try again.",
|
|
1478
|
+
'<path d="M12 6v7m0 4v.01"/><circle cx="12" cy="12" r="10"/>'
|
|
1479
|
+
);
|
|
1480
|
+
function sameSecret(a, b) {
|
|
1481
|
+
const left = Buffer.from(a);
|
|
1482
|
+
const right = Buffer.from(b);
|
|
1483
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
1484
|
+
}
|
|
1485
|
+
function single(params, key) {
|
|
1486
|
+
const values = params.getAll(key);
|
|
1487
|
+
return values.length === 1 ? values[0] : null;
|
|
1488
|
+
}
|
|
1489
|
+
async function startCallbackServer() {
|
|
1490
|
+
let expected = null;
|
|
1491
|
+
let settle2 = () => {
|
|
1492
|
+
};
|
|
1493
|
+
const result = new Promise((resolve3) => {
|
|
1494
|
+
settle2 = resolve3;
|
|
1495
|
+
});
|
|
1496
|
+
function accept(method, host, rawUrl) {
|
|
1497
|
+
if (method !== "GET" || expected === null || host !== `127.0.0.1:${port}`) return null;
|
|
1498
|
+
const url = new URL(rawUrl, `http://127.0.0.1:${port}`);
|
|
1499
|
+
if (url.pathname !== CALLBACK_PATH) return null;
|
|
1500
|
+
const params = url.searchParams;
|
|
1501
|
+
const keys = new Set(params.keys());
|
|
1502
|
+
const code = single(params, "code");
|
|
1503
|
+
const error = single(params, "error");
|
|
1504
|
+
if (keys.size !== 3 || single(params, "request") !== expected.requestId || !sameSecret(single(params, "state") ?? "", expected.state)) {
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1507
|
+
if (code !== null && CODE_RE.test(code)) return { code };
|
|
1508
|
+
if (error === "access_denied") return { denied: true };
|
|
1509
|
+
return null;
|
|
1510
|
+
}
|
|
1511
|
+
const server = createServer({ maxHeaderSize: 8192 }, (req, res) => {
|
|
1512
|
+
const accepted = accept(req.method, req.headers.host, req.url ?? "");
|
|
1513
|
+
if (accepted !== null) {
|
|
1514
|
+
expected = null;
|
|
1515
|
+
settle2(accepted);
|
|
1516
|
+
}
|
|
1517
|
+
res.writeHead(accepted === null ? 400 : 200, {
|
|
1518
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1519
|
+
"Cache-Control": "no-store",
|
|
1520
|
+
"Content-Security-Policy": PAGE_CSP,
|
|
1521
|
+
"Referrer-Policy": "no-referrer",
|
|
1522
|
+
"X-Content-Type-Options": "nosniff",
|
|
1523
|
+
Connection: "close"
|
|
1524
|
+
});
|
|
1525
|
+
res.end(accepted === null ? REJECTED_PAGE : "denied" in accepted ? DENIED_PAGE : ACCEPTED_PAGE);
|
|
1526
|
+
});
|
|
1527
|
+
server.requestTimeout = 5e3;
|
|
1528
|
+
server.headersTimeout = 5e3;
|
|
1529
|
+
await new Promise((resolve3, reject) => {
|
|
1530
|
+
server.once("error", reject);
|
|
1531
|
+
server.listen(0, "127.0.0.1", resolve3);
|
|
1532
|
+
});
|
|
1533
|
+
const port = server.address().port;
|
|
1534
|
+
return {
|
|
1535
|
+
port,
|
|
1536
|
+
expect(requestId, state) {
|
|
1537
|
+
expected = { requestId, state };
|
|
1538
|
+
},
|
|
1539
|
+
result,
|
|
1540
|
+
close() {
|
|
1541
|
+
server.close();
|
|
1542
|
+
server.closeAllConnections();
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// src/cli/lifecycle/settle.ts
|
|
1548
|
+
var STARTED_EXPIRY_MARGIN_MS = 15 * 6e4;
|
|
1549
|
+
async function settle(keychain, api, journal, lockLost) {
|
|
1550
|
+
const holdsLock = () => {
|
|
1551
|
+
if (lockLost()) throw new LockLostError();
|
|
1552
|
+
};
|
|
1553
|
+
const save = async () => {
|
|
1554
|
+
holdsLock();
|
|
1555
|
+
await saveJournal(keychain, journal);
|
|
1556
|
+
};
|
|
1557
|
+
const readShared = () => keychain.read().catch((err) => {
|
|
1558
|
+
if (err instanceof KeychainError && err.code === "credential-record-invalid") return null;
|
|
1559
|
+
throw err;
|
|
1560
|
+
});
|
|
1561
|
+
const queueRevoke = (key) => {
|
|
1562
|
+
const queued = journal.revoke.some(
|
|
1563
|
+
(pending) => pending.endpoint === key.endpoint && pending.apiKey === key.apiKey
|
|
1564
|
+
);
|
|
1565
|
+
if (!queued) journal.revoke.push(key);
|
|
1566
|
+
};
|
|
1567
|
+
const login = journal.login;
|
|
1568
|
+
if (login?.phase === "started") {
|
|
1569
|
+
holdsLock();
|
|
1570
|
+
if (Date.now() > login.expiresAt + STARTED_EXPIRY_MARGIN_MS || await api.cancel(login)) {
|
|
1571
|
+
journal.login = null;
|
|
1572
|
+
await save();
|
|
1573
|
+
}
|
|
1574
|
+
} else if (login?.apiKey !== void 0) {
|
|
1575
|
+
const own = { endpoint: login.endpoint, apiKey: login.apiKey };
|
|
1576
|
+
const shared = login.phase === "committing" ? await readShared() : null;
|
|
1577
|
+
const committed = shared !== null && credentialFingerprint(shared.endpoint, shared.apiKey) === credentialFingerprint(own.endpoint, own.apiKey);
|
|
1578
|
+
journal.login = null;
|
|
1579
|
+
if (!committed) queueRevoke(own);
|
|
1580
|
+
if (login.previous !== void 0 && shared?.apiKey !== login.previous.apiKey) {
|
|
1581
|
+
queueRevoke(login.previous);
|
|
1582
|
+
}
|
|
1583
|
+
await save();
|
|
1584
|
+
}
|
|
1585
|
+
if (journal.logout !== null) {
|
|
1586
|
+
const { fingerprint, revoke } = journal.logout;
|
|
1587
|
+
const shared = await readShared();
|
|
1588
|
+
if (shared !== null && credentialFingerprint(shared.endpoint, shared.apiKey) === fingerprint) {
|
|
1589
|
+
holdsLock();
|
|
1590
|
+
await keychain.delete();
|
|
1591
|
+
}
|
|
1592
|
+
journal.logout = null;
|
|
1593
|
+
if (revoke !== null) queueRevoke(revoke);
|
|
1594
|
+
await save();
|
|
1595
|
+
}
|
|
1596
|
+
for (const key of [...journal.revoke]) {
|
|
1597
|
+
holdsLock();
|
|
1598
|
+
if (await api.revoke(key)) {
|
|
1599
|
+
journal.revoke = journal.revoke.filter((pending) => pending !== key);
|
|
1600
|
+
journal.revokedAt = Date.now();
|
|
1601
|
+
await save();
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
return journal;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// src/cli/commands/login-browser.ts
|
|
1608
|
+
var DEFAULT_ENDPOINT2 = "https://memory.api.pyxmate.com";
|
|
1609
|
+
var MAX_STDIN_LINE = 1024;
|
|
1610
|
+
var Stop = class extends Error {
|
|
1611
|
+
constructor(outcome) {
|
|
1612
|
+
super(outcome);
|
|
1613
|
+
this.outcome = outcome;
|
|
1614
|
+
}
|
|
1615
|
+
outcome;
|
|
1616
|
+
};
|
|
1617
|
+
function stopFor(failure2) {
|
|
1618
|
+
return new Stop(failure2.code === "cancelled" ? "requested" : failure2.code);
|
|
1619
|
+
}
|
|
1620
|
+
function printHuman(e) {
|
|
1621
|
+
if (e.event === "awaiting_browser") {
|
|
1622
|
+
process.stdout.write(`Sign in with your browser:
|
|
1623
|
+
${e.authorizationUrl}
|
|
1624
|
+
`);
|
|
1625
|
+
} else if (e.event === "connecting") {
|
|
1626
|
+
process.stdout.write("Finishing sign-in\u2026\n");
|
|
1627
|
+
} else if (e.event === "connected") {
|
|
1628
|
+
process.stdout.write(
|
|
1629
|
+
`Connected to ${e.connection.projectName} as ${e.connection.accountLabel}. Run: pyx-mem doctor
|
|
1630
|
+
`
|
|
1631
|
+
);
|
|
1632
|
+
} else if (e.event === "cancelled") {
|
|
1633
|
+
process.stdout.write("Sign-in cancelled. Stored credentials were not changed.\n");
|
|
1634
|
+
} else {
|
|
1635
|
+
process.stderr.write(`Error: ${e.message}
|
|
1636
|
+
`);
|
|
1637
|
+
}
|
|
1638
|
+
if ("cleanupPending" in e && e.cleanupPending && e.event !== "error") {
|
|
1639
|
+
process.stderr.write("Server-side cleanup is pending. Run: pyx-mem login-recovery\n");
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
function openInBrowser(url) {
|
|
1643
|
+
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
|
|
1644
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
1645
|
+
child.on("error", () => {
|
|
1646
|
+
process.stderr.write(
|
|
1647
|
+
"Could not open a browser automatically. Open the sign-in URL yourself.\n"
|
|
1648
|
+
);
|
|
1649
|
+
});
|
|
1650
|
+
child.unref();
|
|
1651
|
+
}
|
|
1652
|
+
function exitFor(outcome) {
|
|
1653
|
+
if (outcome === "requested" || outcome === "denied") return EXIT.CANCELLED;
|
|
1654
|
+
if (outcome === "busy") return EXIT.BUSY;
|
|
1655
|
+
if (outcome === "cleanup_pending") return EXIT.CLEANUP_PENDING;
|
|
1656
|
+
if (outcome === "invalid_endpoint") return EXIT.USAGE;
|
|
1657
|
+
return EXIT.DOCTOR_FAIL;
|
|
1658
|
+
}
|
|
1659
|
+
async function snapshot(keychain) {
|
|
1660
|
+
try {
|
|
1661
|
+
const credentials = await keychain.read();
|
|
1662
|
+
return {
|
|
1663
|
+
token: credentials === null ? "none" : credentialFingerprint(credentials.endpoint, credentials.apiKey),
|
|
1664
|
+
credentials
|
|
1665
|
+
};
|
|
1666
|
+
} catch (err) {
|
|
1667
|
+
if (err instanceof KeychainError && err.code === "credential-record-invalid") {
|
|
1668
|
+
return { token: "invalid", credentials: null };
|
|
1669
|
+
}
|
|
1670
|
+
throw err;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
async function browserLoginCommand(opts) {
|
|
1674
|
+
const keychain = opts.keychain ?? getLifecycleKeychain();
|
|
1675
|
+
const api = createCliLoginApi(opts.fetchImpl);
|
|
1676
|
+
const emit4 = opts.json ? (e) => process.stdout.write(`${JSON.stringify(e)}
|
|
1677
|
+
`) : printHuman;
|
|
1678
|
+
const stop = new AbortController();
|
|
1679
|
+
let lockLost = false;
|
|
1680
|
+
const cancel = () => stop.abort();
|
|
1681
|
+
const stopped = new Promise((_, reject) => {
|
|
1682
|
+
stop.signal.addEventListener("abort", () => reject(new Stop("requested")));
|
|
1683
|
+
});
|
|
1684
|
+
stopped.catch(() => {
|
|
1685
|
+
});
|
|
1686
|
+
const check = () => {
|
|
1687
|
+
if (lockLost) throw new Stop("lock_lost");
|
|
1688
|
+
if (stop.signal.aborted) throw new Stop("requested");
|
|
1689
|
+
};
|
|
1690
|
+
process.once("SIGINT", cancel);
|
|
1691
|
+
process.once("SIGTERM", cancel);
|
|
1692
|
+
const lines = opts.json ? createInterface({ input: opts.input ?? process.stdin }) : null;
|
|
1693
|
+
lines?.on("line", (line) => {
|
|
1694
|
+
if (line.length > MAX_STDIN_LINE) return;
|
|
1695
|
+
try {
|
|
1696
|
+
if (JSON.parse(line)?.action === "cancel") cancel();
|
|
1697
|
+
} catch {
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
if (opts.noOpen) lines?.on("close", cancel);
|
|
1701
|
+
let release = null;
|
|
1702
|
+
let listener = null;
|
|
1703
|
+
let journal = null;
|
|
1704
|
+
let connected = null;
|
|
1705
|
+
let outcome = null;
|
|
1706
|
+
let storeError = null;
|
|
1707
|
+
try {
|
|
1708
|
+
release = await keychain.acquireLock(() => {
|
|
1709
|
+
lockLost = true;
|
|
1710
|
+
cancel();
|
|
1711
|
+
});
|
|
1712
|
+
if (release === null) throw new Stop("busy");
|
|
1713
|
+
journal = await settle(keychain, api, await loadJournal(keychain), () => lockLost);
|
|
1714
|
+
if (journal.login !== null) throw new Stop("cleanup_pending");
|
|
1715
|
+
const base = await snapshot(keychain);
|
|
1716
|
+
const endpoint = parseEndpoint(opts.endpoint ?? base.credentials?.endpoint ?? DEFAULT_ENDPOINT2);
|
|
1717
|
+
if (endpoint === null) throw new Stop("invalid_endpoint");
|
|
1718
|
+
const replaced = base.credentials ? { endpoint: base.credentials.endpoint, apiKey: base.credentials.apiKey } : null;
|
|
1719
|
+
const previous = replaced !== null && base.credentials?.provenance === "browser-login" && isKeyRef(replaced) ? replaced : void 0;
|
|
1720
|
+
if (!hasRoomForLogin(journal, endpoint, previous)) throw new Stop("cleanup_pending");
|
|
1721
|
+
listener = await startCallbackServer();
|
|
1722
|
+
const codeVerifier = randomBytes(32).toString("base64url");
|
|
1723
|
+
const state = randomBytes(32).toString("base64url");
|
|
1724
|
+
const started = await api.start(
|
|
1725
|
+
endpoint,
|
|
1726
|
+
{
|
|
1727
|
+
codeChallenge: createHash3("sha256").update(codeVerifier).digest("base64url"),
|
|
1728
|
+
callbackPort: listener.port,
|
|
1729
|
+
state,
|
|
1730
|
+
deviceLabel: hostname().replace(/\p{Cc}/gu, "").trim().slice(0, 64) || "this computer"
|
|
1731
|
+
},
|
|
1732
|
+
stop.signal
|
|
1733
|
+
);
|
|
1734
|
+
check();
|
|
1735
|
+
if (!started.ok) throw stopFor(started);
|
|
1736
|
+
const login = {
|
|
1737
|
+
phase: "started",
|
|
1738
|
+
endpoint,
|
|
1739
|
+
requestId: started.requestId,
|
|
1740
|
+
codeVerifier,
|
|
1741
|
+
expiresAt: started.expiresAt.getTime()
|
|
1742
|
+
};
|
|
1743
|
+
journal.login = login;
|
|
1744
|
+
await saveJournal(keychain, journal);
|
|
1745
|
+
check();
|
|
1746
|
+
listener.expect(started.requestId, state);
|
|
1747
|
+
emit4({
|
|
1748
|
+
event: "awaiting_browser",
|
|
1749
|
+
authorizationUrl: started.authorizationUrl,
|
|
1750
|
+
expiresAt: started.expiresAt.toISOString()
|
|
1751
|
+
});
|
|
1752
|
+
if (!opts.noOpen) (opts.openBrowser ?? openInBrowser)(started.authorizationUrl);
|
|
1753
|
+
let expiry;
|
|
1754
|
+
const expired = new Promise((_, reject) => {
|
|
1755
|
+
const ms = Math.min(Math.max(started.expiresAt.getTime() - Date.now(), 0), 6e5);
|
|
1756
|
+
expiry = setTimeout(() => reject(new Stop("expired")), ms);
|
|
1757
|
+
});
|
|
1758
|
+
const callback = await Promise.race([listener.result, stopped, expired]).finally(
|
|
1759
|
+
() => clearTimeout(expiry)
|
|
1760
|
+
);
|
|
1761
|
+
check();
|
|
1762
|
+
if ("denied" in callback) throw new Stop("denied");
|
|
1763
|
+
emit4({ event: "connecting" });
|
|
1764
|
+
const redeemed = await api.redeem(login, callback.code, stop.signal);
|
|
1765
|
+
check();
|
|
1766
|
+
if (!redeemed.ok) throw stopFor(redeemed);
|
|
1767
|
+
const issued = { ...login, phase: "issued", apiKey: redeemed.apiKey };
|
|
1768
|
+
journal.login = issued;
|
|
1769
|
+
await saveJournal(keychain, journal);
|
|
1770
|
+
check();
|
|
1771
|
+
const confirmed = await api.confirm(issued, stop.signal);
|
|
1772
|
+
check();
|
|
1773
|
+
if (!confirmed.ok) throw stopFor(confirmed);
|
|
1774
|
+
const current = (await loadJournal(keychain)).login;
|
|
1775
|
+
if (current?.requestId !== issued.requestId || current.phase !== "issued") {
|
|
1776
|
+
throw new Stop("lock_lost");
|
|
1777
|
+
}
|
|
1778
|
+
journal.login = { ...issued, phase: "committing", ...previous ? { previous } : {} };
|
|
1779
|
+
await saveJournal(keychain, journal);
|
|
1780
|
+
check();
|
|
1781
|
+
if ((await snapshot(keychain)).token !== base.token) throw new Stop("credential_changed");
|
|
1782
|
+
check();
|
|
1783
|
+
const fingerprint = credentialFingerprint(endpoint, redeemed.apiKey);
|
|
1784
|
+
try {
|
|
1785
|
+
await keychain.write({
|
|
1786
|
+
schemaVersion: 1,
|
|
1787
|
+
endpoint,
|
|
1788
|
+
apiKey: redeemed.apiKey,
|
|
1789
|
+
connection: redeemed.connection,
|
|
1790
|
+
provenance: "browser-login"
|
|
1791
|
+
});
|
|
1792
|
+
} catch (err) {
|
|
1793
|
+
if ((await snapshot(keychain)).token !== fingerprint) throw err;
|
|
1794
|
+
}
|
|
1795
|
+
connected = { connection: redeemed.connection, fingerprint };
|
|
1796
|
+
} catch (err) {
|
|
1797
|
+
if (err instanceof Stop) outcome = err.outcome;
|
|
1798
|
+
else if (err instanceof JournalInvalidError) outcome = "journal_invalid";
|
|
1799
|
+
else if (err instanceof KeychainError) {
|
|
1800
|
+
storeError = err;
|
|
1801
|
+
outcome = "credential_store_unavailable";
|
|
1802
|
+
} else outcome = "login_failed";
|
|
1803
|
+
}
|
|
1804
|
+
listener?.close();
|
|
1805
|
+
lines?.close();
|
|
1806
|
+
process.off("SIGINT", cancel);
|
|
1807
|
+
process.off("SIGTERM", cancel);
|
|
1808
|
+
let cleanupPending = outcome !== "busy";
|
|
1809
|
+
if (journal !== null && !lockLost) {
|
|
1810
|
+
try {
|
|
1811
|
+
cleanupPending = !isEmpty(await settle(keychain, api, journal, () => lockLost));
|
|
1812
|
+
} catch {
|
|
1813
|
+
cleanupPending = true;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
if (release !== null) await releaseLifecycleLock(release);
|
|
1817
|
+
if (connected !== null) {
|
|
1818
|
+
emit4({
|
|
1819
|
+
event: "connected",
|
|
1820
|
+
connection: connected.connection,
|
|
1821
|
+
credentialFingerprint: connected.fingerprint,
|
|
1822
|
+
cleanupPending
|
|
1823
|
+
});
|
|
1824
|
+
return EXIT.OK;
|
|
1825
|
+
}
|
|
1826
|
+
if (outcome === "requested" || outcome === "denied") {
|
|
1827
|
+
emit4({ event: "cancelled", reason: outcome, cleanupPending });
|
|
1828
|
+
return EXIT.CANCELLED;
|
|
1829
|
+
}
|
|
1830
|
+
const code = outcome ?? "login_failed";
|
|
1831
|
+
emit4({
|
|
1832
|
+
event: "error",
|
|
1833
|
+
code,
|
|
1834
|
+
message: storeError ? `${storeError.message} ${storeError.guidance}` : ERROR_MESSAGES[code].message,
|
|
1835
|
+
retryable: ERROR_MESSAGES[code].retryable,
|
|
1836
|
+
cleanupPending
|
|
1837
|
+
});
|
|
1838
|
+
return storeError?.exit ?? exitFor(code);
|
|
1839
|
+
}
|
|
1840
|
+
|
|
891
1841
|
// src/cli/commands/logout.ts
|
|
1842
|
+
function lifecycleFailure(code, cleanupPending, err) {
|
|
1843
|
+
return {
|
|
1844
|
+
ok: false,
|
|
1845
|
+
code,
|
|
1846
|
+
// Keychain guidance is static text; any other error's text is never shown.
|
|
1847
|
+
message: err instanceof KeychainError ? `${err.message} ${err.guidance}` : ERROR_MESSAGES[code].message,
|
|
1848
|
+
retryable: ERROR_MESSAGES[code].retryable,
|
|
1849
|
+
cleanupPending
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
async function cleanupPendingAfterFailure(keychain) {
|
|
1853
|
+
try {
|
|
1854
|
+
return !isEmpty(await loadJournal(keychain));
|
|
1855
|
+
} catch {
|
|
1856
|
+
return true;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
892
1859
|
async function logoutCommand(opts = {}) {
|
|
893
|
-
const
|
|
1860
|
+
const keychain = opts.keychain ?? getLifecycleKeychain();
|
|
1861
|
+
const api = createCliLoginApi(opts.fetchImpl);
|
|
1862
|
+
const fail = (failure2, exit) => {
|
|
1863
|
+
if (opts.json) process.stdout.write(`${JSON.stringify(failure2)}
|
|
1864
|
+
`);
|
|
1865
|
+
else process.stderr.write(`Error: ${failure2.message}
|
|
1866
|
+
`);
|
|
1867
|
+
return exit;
|
|
1868
|
+
};
|
|
894
1869
|
try {
|
|
895
|
-
await
|
|
896
|
-
|
|
897
|
-
return EXIT.OK;
|
|
1870
|
+
const result = await withLifecycleLock(keychain, (lockLost) => logout(lockLost));
|
|
1871
|
+
return result === BUSY ? fail(lifecycleFailure("busy", false), EXIT.BUSY) : result;
|
|
898
1872
|
} catch (err) {
|
|
1873
|
+
const pending = await cleanupPendingAfterFailure(keychain);
|
|
899
1874
|
if (err instanceof KeychainError) {
|
|
900
|
-
|
|
901
|
-
|
|
1875
|
+
return fail(lifecycleFailure("credential_store_unavailable", pending, err), err.exit);
|
|
1876
|
+
}
|
|
1877
|
+
if (err instanceof LockLostError) {
|
|
1878
|
+
return fail(lifecycleFailure("lock_lost", pending), EXIT.DOCTOR_FAIL);
|
|
1879
|
+
}
|
|
1880
|
+
if (err instanceof JournalInvalidError) {
|
|
1881
|
+
return fail(lifecycleFailure("journal_invalid", true), EXIT.DOCTOR_FAIL);
|
|
1882
|
+
}
|
|
1883
|
+
return fail(lifecycleFailure("cleanup_pending", true), EXIT.CLEANUP_PENDING);
|
|
1884
|
+
}
|
|
1885
|
+
async function logout(lockLost) {
|
|
1886
|
+
const journal = await loadJournal(keychain);
|
|
1887
|
+
const selected = await keychain.read().catch((err) => {
|
|
1888
|
+
if (err instanceof KeychainError && err.code === "credential-record-invalid") {
|
|
1889
|
+
return "unreadable";
|
|
1890
|
+
}
|
|
1891
|
+
throw err;
|
|
1892
|
+
});
|
|
1893
|
+
const managed = selected !== null && selected !== "unreadable" && selected.provenance === "browser-login" && isKeyRef({ endpoint: selected.endpoint, apiKey: selected.apiKey }) ? { endpoint: selected.endpoint, apiKey: selected.apiKey } : null;
|
|
1894
|
+
if (selected === null || selected === "unreadable") {
|
|
1895
|
+
await keychain.delete();
|
|
1896
|
+
} else {
|
|
1897
|
+
journal.logout = {
|
|
1898
|
+
fingerprint: credentialFingerprint(selected.endpoint, selected.apiKey),
|
|
1899
|
+
revoke: managed
|
|
1900
|
+
};
|
|
1901
|
+
await saveJournal(keychain, journal);
|
|
1902
|
+
}
|
|
1903
|
+
const cleanupPending = !isEmpty(await settle(keychain, api, journal, lockLost));
|
|
1904
|
+
if (await keychain.read() !== null) {
|
|
1905
|
+
return fail(lifecycleFailure("credential_changed", cleanupPending), EXIT.DOCTOR_FAIL);
|
|
1906
|
+
}
|
|
1907
|
+
if (lockLost()) throw new LockLostError();
|
|
1908
|
+
const convergence = convergenceSeconds(journal);
|
|
1909
|
+
const revocationPending = journal.revoke.length > 0;
|
|
1910
|
+
const serverRevocation = revocationPending ? "pending" : convergence !== null ? "revoked" : "not_attempted";
|
|
1911
|
+
const legacy = selected !== null && managed === null;
|
|
1912
|
+
const report4 = {
|
|
1913
|
+
ok: true,
|
|
1914
|
+
credentialKind: selected === null ? null : managed !== null ? "browser" : "legacy",
|
|
1915
|
+
localDeleted: selected !== null,
|
|
1916
|
+
serverRevocation,
|
|
1917
|
+
cleanupPending,
|
|
1918
|
+
reconnectRequired: selected !== null || serverRevocation !== "not_attempted",
|
|
1919
|
+
// Proxies older than the per-request credential check hold the key in
|
|
1920
|
+
// memory. A key that is still valid on the server keeps them working.
|
|
1921
|
+
pendingLegacySessions: legacy || revocationPending,
|
|
1922
|
+
serverConvergenceSeconds: convergence
|
|
1923
|
+
};
|
|
1924
|
+
if (opts.json) {
|
|
1925
|
+
process.stdout.write(`${JSON.stringify(report4)}
|
|
902
1926
|
`);
|
|
903
|
-
|
|
1927
|
+
} else {
|
|
1928
|
+
process.stdout.write(
|
|
1929
|
+
selected === null ? "No pyx-memory credentials were stored.\n" : "pyx-memory credentials deleted from OS credential store.\n"
|
|
1930
|
+
);
|
|
1931
|
+
if (cleanupPending) {
|
|
1932
|
+
process.stderr.write(
|
|
1933
|
+
"The server-side key could not be revoked yet. Run: pyx-mem login-recovery\n"
|
|
1934
|
+
);
|
|
1935
|
+
} else if (report4.pendingLegacySessions) {
|
|
1936
|
+
process.stdout.write(
|
|
1937
|
+
"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"
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
904
1940
|
}
|
|
1941
|
+
return cleanupPending ? EXIT.CLEANUP_PENDING : EXIT.OK;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// src/cli/commands/login-recovery.ts
|
|
1946
|
+
async function signOutJournal(keychain) {
|
|
1947
|
+
const journal = emptyJournal();
|
|
1948
|
+
const shared = await keychain.read().catch((err) => {
|
|
1949
|
+
if (err instanceof KeychainError && err.code === "credential-record-invalid") return null;
|
|
905
1950
|
throw err;
|
|
1951
|
+
});
|
|
1952
|
+
if (shared !== null) {
|
|
1953
|
+
const key = { endpoint: shared.endpoint, apiKey: shared.apiKey };
|
|
1954
|
+
journal.logout = {
|
|
1955
|
+
fingerprint: credentialFingerprint(shared.endpoint, shared.apiKey),
|
|
1956
|
+
revoke: shared.provenance === "browser-login" && isKeyRef(key) ? key : null
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
await saveJournal(keychain, journal);
|
|
1960
|
+
return journal;
|
|
1961
|
+
}
|
|
1962
|
+
async function loginRecoveryCommand(opts = {}) {
|
|
1963
|
+
const keychain = opts.keychain ?? getLifecycleKeychain();
|
|
1964
|
+
const fail = (failure2, exit) => {
|
|
1965
|
+
if (opts.json) process.stdout.write(`${JSON.stringify(failure2)}
|
|
1966
|
+
`);
|
|
1967
|
+
else process.stderr.write(`Error: ${failure2.message}
|
|
1968
|
+
`);
|
|
1969
|
+
return exit;
|
|
1970
|
+
};
|
|
1971
|
+
try {
|
|
1972
|
+
const journal = await withLifecycleLock(keychain, async (lockLost) => {
|
|
1973
|
+
const loaded = await loadJournal(keychain).catch((err) => {
|
|
1974
|
+
if (err instanceof JournalInvalidError && opts.discardUnreadableJournal) {
|
|
1975
|
+
return signOutJournal(keychain);
|
|
1976
|
+
}
|
|
1977
|
+
throw err;
|
|
1978
|
+
});
|
|
1979
|
+
const settled = await settle(keychain, createCliLoginApi(opts.fetchImpl), loaded, lockLost);
|
|
1980
|
+
if (lockLost()) throw new LockLostError();
|
|
1981
|
+
return settled;
|
|
1982
|
+
});
|
|
1983
|
+
if (journal === BUSY) return fail(lifecycleFailure("busy", true), EXIT.BUSY);
|
|
1984
|
+
if (!isEmpty(journal)) {
|
|
1985
|
+
return fail(lifecycleFailure("cleanup_pending", true), EXIT.CLEANUP_PENDING);
|
|
1986
|
+
}
|
|
1987
|
+
const convergence = convergenceSeconds(journal);
|
|
1988
|
+
const report4 = {
|
|
1989
|
+
ok: true,
|
|
1990
|
+
cleanupPending: false,
|
|
1991
|
+
serverRevocation: convergence !== null ? "revoked" : "not_attempted",
|
|
1992
|
+
serverConvergenceSeconds: convergence
|
|
1993
|
+
};
|
|
1994
|
+
if (opts.json) process.stdout.write(`${JSON.stringify(report4)}
|
|
1995
|
+
`);
|
|
1996
|
+
else process.stdout.write("Nothing is pending.\n");
|
|
1997
|
+
return EXIT.OK;
|
|
1998
|
+
} catch (err) {
|
|
1999
|
+
if (err instanceof JournalInvalidError) {
|
|
2000
|
+
return fail(lifecycleFailure("journal_invalid", true), EXIT.DOCTOR_FAIL);
|
|
2001
|
+
}
|
|
2002
|
+
const pending = await cleanupPendingAfterFailure(keychain);
|
|
2003
|
+
if (err instanceof KeychainError) {
|
|
2004
|
+
return fail(lifecycleFailure("credential_store_unavailable", pending, err), err.exit);
|
|
2005
|
+
}
|
|
2006
|
+
if (err instanceof LockLostError) {
|
|
2007
|
+
return fail(lifecycleFailure("lock_lost", pending), EXIT.DOCTOR_FAIL);
|
|
2008
|
+
}
|
|
2009
|
+
return fail(lifecycleFailure("cleanup_pending", true), EXIT.CLEANUP_PENDING);
|
|
906
2010
|
}
|
|
907
2011
|
}
|
|
908
2012
|
|
|
@@ -925,7 +2029,7 @@ function createReadCredentials(providerFactory) {
|
|
|
925
2029
|
}
|
|
926
2030
|
|
|
927
2031
|
// src/mcp/proxy-server.ts
|
|
928
|
-
import { createHash, randomUUID as randomUUID3 } from "crypto";
|
|
2032
|
+
import { createHash as createHash4, randomUUID as randomUUID3 } from "crypto";
|
|
929
2033
|
import { readFile, stat } from "fs/promises";
|
|
930
2034
|
import { basename, isAbsolute, resolve } from "path";
|
|
931
2035
|
import { Client as McpClient } from "@modelcontextprotocol/sdk/client/index.js";
|
|
@@ -954,13 +2058,39 @@ function createMcpIdempotentFetch(fetchImpl = fetch, sessionNonce = randomUUID3(
|
|
|
954
2058
|
if (typeof body !== "string") {
|
|
955
2059
|
throw new Error("remote MCP POST body must be text for stable request idempotency");
|
|
956
2060
|
}
|
|
957
|
-
const key =
|
|
2061
|
+
const key = createHash4("sha256").update("pyx-mcp-idempotency-v1\0").update(sessionNonce).update("\0").update(body).digest("hex");
|
|
958
2062
|
headers.set("Idempotency-Key", `mcp-${key}`);
|
|
959
2063
|
}
|
|
960
2064
|
return fetchImpl(input, { ...init, method, headers, body });
|
|
961
2065
|
};
|
|
962
2066
|
return Object.assign(wrapped, { preconnect: fetch.preconnect });
|
|
963
2067
|
}
|
|
2068
|
+
function resultText(result) {
|
|
2069
|
+
return result.content.map((c) => c.type === "text" ? c.text : "").join(" ").trim();
|
|
2070
|
+
}
|
|
2071
|
+
function createCredentialGuard(readCredentials, snapshot2) {
|
|
2072
|
+
return async () => {
|
|
2073
|
+
const read = await readCredentials();
|
|
2074
|
+
if (!read.ok) {
|
|
2075
|
+
throw new Error(
|
|
2076
|
+
`${resultText(read.result) || "pyx-memory credentials are unavailable. Run: pyx-mem login"}
|
|
2077
|
+
This attempt was not sent. Then reconnect the pyx-memory MCP server.`
|
|
2078
|
+
);
|
|
2079
|
+
}
|
|
2080
|
+
if (read.credentials.endpoint !== snapshot2.endpoint || read.credentials.apiKey !== snapshot2.apiKey) {
|
|
2081
|
+
throw new Error(
|
|
2082
|
+
"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."
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
function createGuardedFetch(guard, fetchImpl = fetch) {
|
|
2088
|
+
const wrapped = async (input, init) => {
|
|
2089
|
+
await guard();
|
|
2090
|
+
return fetchImpl(input, init);
|
|
2091
|
+
};
|
|
2092
|
+
return Object.assign(wrapped, { preconnect: fetch.preconnect });
|
|
2093
|
+
}
|
|
964
2094
|
function extractLocalUpload(meta) {
|
|
965
2095
|
if (meta === null || typeof meta !== "object") return void 0;
|
|
966
2096
|
const descriptor = meta[LOCAL_UPLOAD_META_KEY];
|
|
@@ -1003,24 +2133,31 @@ async function readCappedLocalFile(rawPath, descriptor) {
|
|
|
1003
2133
|
};
|
|
1004
2134
|
}
|
|
1005
2135
|
}
|
|
1006
|
-
function createLocalFileUploader(credentials, fetchImpl = fetch) {
|
|
2136
|
+
function createLocalFileUploader(credentials, readCredentials, fetchImpl = fetch) {
|
|
2137
|
+
const guard = createCredentialGuard(readCredentials, credentials);
|
|
1007
2138
|
return async (descriptor, args) => {
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
const
|
|
1018
|
-
|
|
2139
|
+
try {
|
|
2140
|
+
await guard();
|
|
2141
|
+
const file = await readCappedLocalFile(args[descriptor.pathArg], descriptor);
|
|
2142
|
+
if (!file.ok) return file.result;
|
|
2143
|
+
const http = createHttpClient(credentials, fetchImpl);
|
|
2144
|
+
const res = await http.requestMultipart({
|
|
2145
|
+
path: descriptor.endpoint,
|
|
2146
|
+
formData: async () => {
|
|
2147
|
+
await guard();
|
|
2148
|
+
const form = new FormData();
|
|
2149
|
+
form.set(descriptor.fileField, new File([new Uint8Array(file.bytes)], file.name));
|
|
2150
|
+
for (const key of descriptor.optionalArgs ?? []) {
|
|
2151
|
+
const value = args[key];
|
|
2152
|
+
if (typeof value === "string" && value.length > 0) form.set(key, value);
|
|
2153
|
+
}
|
|
2154
|
+
return form;
|
|
1019
2155
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
})
|
|
1023
|
-
|
|
2156
|
+
});
|
|
2157
|
+
return res.ok ? mcpJson(res.data) : res.result;
|
|
2158
|
+
} catch (err) {
|
|
2159
|
+
return mcpText(err instanceof Error ? err.message : String(err), true);
|
|
2160
|
+
}
|
|
1024
2161
|
};
|
|
1025
2162
|
}
|
|
1026
2163
|
function createProxyServer(client, version, uploadLocalFile) {
|
|
@@ -1086,18 +2223,21 @@ function createProxyServer(client, version, uploadLocalFile) {
|
|
|
1086
2223
|
return server;
|
|
1087
2224
|
}
|
|
1088
2225
|
async function runMcpProxyServer(opts) {
|
|
1089
|
-
const version = opts.version ?? (true ? "1.
|
|
2226
|
+
const version = opts.version ?? (true ? "1.23.0" : "0.0.0-dev");
|
|
1090
2227
|
const read = await opts.readCredentials();
|
|
1091
2228
|
if (!read.ok) {
|
|
1092
|
-
|
|
1093
|
-
|
|
2229
|
+
throw new Error(
|
|
2230
|
+
resultText(read.result) || "pyx-memory credentials are unavailable. Run: pyx-mem login"
|
|
2231
|
+
);
|
|
1094
2232
|
}
|
|
1095
2233
|
const { endpoint, apiKey } = read.credentials;
|
|
1096
2234
|
const url = new URL(`${endpoint.replace(/\/+$/, "")}${REMOTE_MCP_PATH}`);
|
|
1097
2235
|
const client = new McpClient({ name: "pyx-mem-proxy", version }, { capabilities: {} });
|
|
1098
2236
|
const clientTransport = new StreamableHTTPClientTransport(url, {
|
|
1099
2237
|
requestInit: { headers: { Authorization: `Bearer ${apiKey}` } },
|
|
1100
|
-
fetch: createMcpIdempotentFetch(
|
|
2238
|
+
fetch: createMcpIdempotentFetch(
|
|
2239
|
+
createGuardedFetch(createCredentialGuard(opts.readCredentials, read.credentials))
|
|
2240
|
+
)
|
|
1101
2241
|
});
|
|
1102
2242
|
try {
|
|
1103
2243
|
await client.connect(clientTransport);
|
|
@@ -1108,12 +2248,18 @@ async function runMcpProxyServer(opts) {
|
|
|
1108
2248
|
);
|
|
1109
2249
|
}
|
|
1110
2250
|
try {
|
|
1111
|
-
const server = createProxyServer(
|
|
2251
|
+
const server = createProxyServer(
|
|
2252
|
+
client,
|
|
2253
|
+
version,
|
|
2254
|
+
createLocalFileUploader(read.credentials, opts.readCredentials)
|
|
2255
|
+
);
|
|
1112
2256
|
const transport = new StdioServerTransport();
|
|
1113
2257
|
const closed = new Promise((resolve3) => {
|
|
1114
2258
|
transport.onclose = () => resolve3();
|
|
1115
2259
|
});
|
|
2260
|
+
process.stdin.once("end", () => void transport.close());
|
|
1116
2261
|
await server.connect(transport);
|
|
2262
|
+
void opts.onConnected?.(client);
|
|
1117
2263
|
await closed;
|
|
1118
2264
|
} finally {
|
|
1119
2265
|
await client.close().catch(() => {
|
|
@@ -1124,12 +2270,12 @@ async function runMcpProxyServer(opts) {
|
|
|
1124
2270
|
// src/cli/commands/mcp-install.ts
|
|
1125
2271
|
import { spawnSync as nodeSpawnSync } from "child_process";
|
|
1126
2272
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
1127
|
-
import { homedir } from "os";
|
|
1128
|
-
import { join } from "path";
|
|
2273
|
+
import { homedir as homedir2 } from "os";
|
|
2274
|
+
import { join as join2 } from "path";
|
|
1129
2275
|
|
|
1130
2276
|
// src/cli/commands/mcp-install-merge.ts
|
|
1131
2277
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
1132
|
-
import { dirname } from "path";
|
|
2278
|
+
import { dirname as dirname2 } from "path";
|
|
1133
2279
|
function mergeWriteJsonMcpEntry(opts) {
|
|
1134
2280
|
const { filePath, serverName, entry } = opts;
|
|
1135
2281
|
let existingDoc = {};
|
|
@@ -1191,7 +2337,7 @@ Remove or rename the existing entry, then re-run.
|
|
|
1191
2337
|
const nextDoc = { ...existingDoc, mcpServers: servers };
|
|
1192
2338
|
const payload = `${JSON.stringify(nextDoc, null, 2)}
|
|
1193
2339
|
`;
|
|
1194
|
-
const parent =
|
|
2340
|
+
const parent = dirname2(filePath);
|
|
1195
2341
|
if (!existsSync(parent)) {
|
|
1196
2342
|
mkdirSync(parent, { recursive: true });
|
|
1197
2343
|
}
|
|
@@ -1244,7 +2390,7 @@ function indent(text) {
|
|
|
1244
2390
|
|
|
1245
2391
|
// src/cli/commands/memory-rules-merge.ts
|
|
1246
2392
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1247
|
-
import { dirname as
|
|
2393
|
+
import { dirname as dirname3 } from "path";
|
|
1248
2394
|
var RULES_BEGIN_MARKER = "<!-- pyx-memory:begin (managed by `pyx-mem mcp install`; edit outside these markers) -->";
|
|
1249
2395
|
var RULES_END_MARKER = "<!-- pyx-memory:end -->";
|
|
1250
2396
|
var BEGIN_MARKER_PREFIX = "<!-- pyx-memory:begin";
|
|
@@ -1375,7 +2521,7 @@ function writeMemoryRulesFile(opts) {
|
|
|
1375
2521
|
`
|
|
1376
2522
|
};
|
|
1377
2523
|
}
|
|
1378
|
-
const parent =
|
|
2524
|
+
const parent = dirname3(filePath);
|
|
1379
2525
|
if (!existsSync2(parent)) mkdirSync2(parent, { recursive: true });
|
|
1380
2526
|
const mode = fileExists ? statSync(filePath).mode & 511 : 420;
|
|
1381
2527
|
const tmpPath = `${filePath}.tmp`;
|
|
@@ -1446,18 +2592,27 @@ function mcpInstallCodexCommand(opts = {}) {
|
|
|
1446
2592
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
1447
2593
|
const remote = opts.remote ?? false;
|
|
1448
2594
|
const spawnSync = opts._spawnSync ?? nodeSpawnSync;
|
|
1449
|
-
const home = opts._homeDir ??
|
|
2595
|
+
const home = opts._homeDir ?? homedir2();
|
|
2596
|
+
const codexHome = (opts._env ?? process.env).CODEX_HOME;
|
|
1450
2597
|
const probe = spawnSync("codex", ["--version"], { stdio: "pipe" });
|
|
1451
2598
|
if (probe.error || probe.status !== 0) {
|
|
1452
|
-
printCodexManualInstructions(home, remote);
|
|
2599
|
+
printCodexManualInstructions(home, remote, codexHome);
|
|
1453
2600
|
return EXIT.OK;
|
|
1454
2601
|
}
|
|
1455
|
-
const cmd = [
|
|
2602
|
+
const cmd = [
|
|
2603
|
+
"mcp",
|
|
2604
|
+
"add",
|
|
2605
|
+
SERVER_NAME,
|
|
2606
|
+
...codexHome ? ["--env", `CODEX_HOME=${codexHome}`] : [],
|
|
2607
|
+
"--",
|
|
2608
|
+
"pyx-mem",
|
|
2609
|
+
...pyxMemArgs(remote)
|
|
2610
|
+
];
|
|
1456
2611
|
const res = spawnSync("codex", cmd, { stdio: "inherit" });
|
|
1457
2612
|
if (res.status !== 0) {
|
|
1458
2613
|
process.stderr.write(`\`codex ${cmd.join(" ")}\` exited with code ${res.status ?? "null"}.
|
|
1459
2614
|
`);
|
|
1460
|
-
printCodexManualInstructions(home, remote);
|
|
2615
|
+
printCodexManualInstructions(home, remote, codexHome);
|
|
1461
2616
|
return EXIT.USAGE;
|
|
1462
2617
|
}
|
|
1463
2618
|
process.stdout.write(
|
|
@@ -1467,20 +2622,21 @@ Restart Codex CLI to make the tools available. No API key was written to config.
|
|
|
1467
2622
|
);
|
|
1468
2623
|
return EXIT.OK;
|
|
1469
2624
|
}
|
|
1470
|
-
function printCodexManualInstructions(home, remote) {
|
|
1471
|
-
const configPath =
|
|
2625
|
+
function printCodexManualInstructions(home, remote, codexHome) {
|
|
2626
|
+
const configPath = join2(codexHome ?? join2(home, ".codex"), "config.toml");
|
|
1472
2627
|
process.stdout.write(
|
|
1473
2628
|
[
|
|
1474
2629
|
"The `codex` CLI is not on PATH, or the install command failed.",
|
|
1475
2630
|
"",
|
|
1476
2631
|
"Run this once Codex CLI is installed:",
|
|
1477
|
-
` codex mcp add ${SERVER_NAME} -- pyx-mem ${pyxMemArgs(remote).join(" ")}`,
|
|
2632
|
+
` codex mcp add ${SERVER_NAME}${codexHome ? ` --env ${JSON.stringify(`CODEX_HOME=${codexHome}`)}` : ""} -- pyx-mem ${pyxMemArgs(remote).join(" ")}`,
|
|
1478
2633
|
"",
|
|
1479
2634
|
`Or add this section manually to ${configPath}:`,
|
|
1480
2635
|
"",
|
|
1481
2636
|
`[mcp_servers.${SERVER_NAME}]`,
|
|
1482
2637
|
`command = "pyx-mem"`,
|
|
1483
2638
|
`args = ${JSON.stringify(pyxMemArgs(remote))}`,
|
|
2639
|
+
...codexHome ? [`env = { CODEX_HOME = ${JSON.stringify(codexHome)} }`] : [],
|
|
1484
2640
|
"",
|
|
1485
2641
|
"Do NOT add your pyx-memory API key here. Run `pyx-mem login` to store it in the OS credential store.",
|
|
1486
2642
|
""
|
|
@@ -1490,8 +2646,8 @@ function printCodexManualInstructions(home, remote) {
|
|
|
1490
2646
|
function mcpInstallCursorCommand(opts = {}) {
|
|
1491
2647
|
const scope = opts.scope ?? "user";
|
|
1492
2648
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
1493
|
-
const home = opts._homeDir ??
|
|
1494
|
-
const filePath = scope === "project" ?
|
|
2649
|
+
const home = opts._homeDir ?? homedir2();
|
|
2650
|
+
const filePath = scope === "project" ? join2(process.cwd(), ".cursor", "mcp.json") : join2(home, ".cursor", "mcp.json");
|
|
1495
2651
|
return writeJsonAndReport(filePath, "Cursor", mcpEntry(opts.remote ?? false));
|
|
1496
2652
|
}
|
|
1497
2653
|
function mcpInstallClineCommand(opts = {}) {
|
|
@@ -1522,8 +2678,8 @@ function mcpInstallContinueCommand(opts = {}) {
|
|
|
1522
2678
|
const scope = opts.scope ?? "user";
|
|
1523
2679
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
1524
2680
|
const remote = opts.remote ?? false;
|
|
1525
|
-
const home = opts._homeDir ??
|
|
1526
|
-
const yamlPath =
|
|
2681
|
+
const home = opts._homeDir ?? homedir2();
|
|
2682
|
+
const yamlPath = join2(home, ".continue", "mcpServers", `${SERVER_NAME}.yaml`);
|
|
1527
2683
|
process.stdout.write(
|
|
1528
2684
|
[
|
|
1529
2685
|
"Continue uses YAML config. The recommended way to add an MCP server is a",
|
|
@@ -1554,8 +2710,8 @@ function mcpInstallContinueCommand(opts = {}) {
|
|
|
1554
2710
|
function mcpInstallWindsurfCommand(opts = {}) {
|
|
1555
2711
|
const scope = opts.scope ?? "user";
|
|
1556
2712
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
1557
|
-
const home = opts._homeDir ??
|
|
1558
|
-
const filePath =
|
|
2713
|
+
const home = opts._homeDir ?? homedir2();
|
|
2714
|
+
const filePath = join2(home, ".codeium", "windsurf", "mcp_config.json");
|
|
1559
2715
|
return writeJsonAndReport(filePath, "Windsurf", mcpEntry(opts.remote ?? false));
|
|
1560
2716
|
}
|
|
1561
2717
|
function mcpInstallGeminiCliCommand(opts = {}) {
|
|
@@ -1563,7 +2719,7 @@ function mcpInstallGeminiCliCommand(opts = {}) {
|
|
|
1563
2719
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
1564
2720
|
const remote = opts.remote ?? false;
|
|
1565
2721
|
const spawnSync = opts._spawnSync ?? nodeSpawnSync;
|
|
1566
|
-
const home = opts._homeDir ??
|
|
2722
|
+
const home = opts._homeDir ?? homedir2();
|
|
1567
2723
|
const probe = spawnSync("gemini", ["--version"], { stdio: "pipe" });
|
|
1568
2724
|
if (probe.error || probe.status !== 0) {
|
|
1569
2725
|
return writeJsonAndReport(geminiConfigPath(home, scope), "Gemini CLI", mcpEntry(remote));
|
|
@@ -1584,13 +2740,13 @@ Restart Gemini CLI to make the tools available. No API key was written to settin
|
|
|
1584
2740
|
return EXIT.OK;
|
|
1585
2741
|
}
|
|
1586
2742
|
function geminiConfigPath(home, scope) {
|
|
1587
|
-
return scope === "project" ?
|
|
2743
|
+
return scope === "project" ? join2(process.cwd(), ".gemini", "settings.json") : join2(home, ".gemini", "settings.json");
|
|
1588
2744
|
}
|
|
1589
2745
|
function mcpInstallPiCommand(opts = {}) {
|
|
1590
2746
|
const scope = opts.scope ?? "user";
|
|
1591
2747
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
1592
|
-
const home = opts._homeDir ??
|
|
1593
|
-
const filePath = scope === "user" ?
|
|
2748
|
+
const home = opts._homeDir ?? homedir2();
|
|
2749
|
+
const filePath = scope === "user" ? join2(home, ".pi", "agent", "mcp.json") : join2(process.cwd(), ".pi", "mcp.json");
|
|
1594
2750
|
return writeJsonAndReport(filePath, "Pi", mcpEntry(opts.remote ?? false), {
|
|
1595
2751
|
prereq: "pi has no native MCP support \u2014 install the third-party adapter once:\n pi install npm:pi-mcp-adapter"
|
|
1596
2752
|
});
|
|
@@ -1598,8 +2754,8 @@ function mcpInstallPiCommand(opts = {}) {
|
|
|
1598
2754
|
function mcpInstallOhMyPiCommand(opts = {}) {
|
|
1599
2755
|
const scope = opts.scope ?? "user";
|
|
1600
2756
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
1601
|
-
const home = opts._homeDir ??
|
|
1602
|
-
const filePath = scope === "user" ?
|
|
2757
|
+
const home = opts._homeDir ?? homedir2();
|
|
2758
|
+
const filePath = scope === "user" ? join2(home, ".omp", "agent", "mcp.json") : join2(process.cwd(), ".omp", "mcp.json");
|
|
1603
2759
|
return writeJsonAndReport(filePath, "oh-my-pi", mcpEntry(opts.remote ?? false));
|
|
1604
2760
|
}
|
|
1605
2761
|
function mcpInstallOpenClawCommand(opts = {}) {
|
|
@@ -1697,35 +2853,49 @@ function printHermesManualInstructions(remote) {
|
|
|
1697
2853
|
);
|
|
1698
2854
|
}
|
|
1699
2855
|
var RULES_FILE_TARGETS = {
|
|
1700
|
-
"claude-code": [".claude", "CLAUDE.md"
|
|
1701
|
-
codex: [".codex", "AGENTS.md"
|
|
1702
|
-
"gemini-cli": [".gemini", "GEMINI.md"
|
|
1703
|
-
"oh-my-pi": [".omp", "agent", "RULES.md"
|
|
2856
|
+
"claude-code": { envDir: "CLAUDE_CONFIG_DIR", dir: [".claude"], file: "CLAUDE.md" },
|
|
2857
|
+
codex: { envDir: "CODEX_HOME", dir: [".codex"], file: "AGENTS.md" },
|
|
2858
|
+
"gemini-cli": { dir: [".gemini"], file: "GEMINI.md" },
|
|
2859
|
+
"oh-my-pi": { dir: [".omp", "agent"], file: "RULES.md" }
|
|
1704
2860
|
};
|
|
2861
|
+
function rulesFilePath(target, opts) {
|
|
2862
|
+
if (!Object.hasOwn(RULES_FILE_TARGETS, target)) return void 0;
|
|
2863
|
+
const spec = RULES_FILE_TARGETS[target];
|
|
2864
|
+
const env = opts._env ?? (opts._homeDir === void 0 ? process.env : {});
|
|
2865
|
+
const configDir = spec.envDir ? env[spec.envDir] : void 0;
|
|
2866
|
+
return configDir ? join2(configDir, spec.file) : join2(opts._homeDir ?? homedir2(), ...spec.dir, spec.file);
|
|
2867
|
+
}
|
|
1705
2868
|
function installMemoryRules(opts) {
|
|
1706
|
-
const
|
|
1707
|
-
if (!
|
|
2869
|
+
const filePath = rulesFilePath(opts.target, opts);
|
|
2870
|
+
if (!filePath) return;
|
|
1708
2871
|
if (opts.noRules) {
|
|
1709
2872
|
process.stdout.write(
|
|
1710
2873
|
'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'
|
|
1711
2874
|
);
|
|
1712
2875
|
return;
|
|
1713
2876
|
}
|
|
1714
|
-
const
|
|
1715
|
-
const result = writeMemoryRulesFile({
|
|
1716
|
-
filePath: join(home, ...segs),
|
|
1717
|
-
section: PERSISTENT_MEMORY_SECTION
|
|
1718
|
-
});
|
|
2877
|
+
const result = writeMemoryRulesFile({ filePath, section: PERSISTENT_MEMORY_SECTION });
|
|
1719
2878
|
process.stdout.write(result.message);
|
|
1720
2879
|
}
|
|
2880
|
+
function installMemoryRulesOnly(target, opts = {}) {
|
|
2881
|
+
if (target === void 0 || !rulesFilePath(target, opts)) {
|
|
2882
|
+
process.stderr.write(
|
|
2883
|
+
`Error: \`pyx-mem mcp rules\` needs a host with a known rules file: ${Object.keys(RULES_FILE_TARGETS).join(", ")}.
|
|
2884
|
+
`
|
|
2885
|
+
);
|
|
2886
|
+
return EXIT.USAGE;
|
|
2887
|
+
}
|
|
2888
|
+
installMemoryRules({ ...opts, target, noRules: false });
|
|
2889
|
+
return EXIT.OK;
|
|
2890
|
+
}
|
|
1721
2891
|
function reconcileMemoryRules(opts = {}) {
|
|
1722
|
-
const
|
|
1723
|
-
for (const
|
|
1724
|
-
const filePath =
|
|
2892
|
+
const section = opts.section ?? PERSISTENT_MEMORY_SECTION;
|
|
2893
|
+
for (const target of Object.keys(RULES_FILE_TARGETS)) {
|
|
2894
|
+
const filePath = rulesFilePath(target, opts);
|
|
1725
2895
|
try {
|
|
1726
2896
|
if (!existsSync3(filePath)) continue;
|
|
1727
2897
|
if (!hasPyxMemoryRules(readFileSync3(filePath, "utf8"))) continue;
|
|
1728
|
-
const result = writeMemoryRulesFile({ filePath, section
|
|
2898
|
+
const result = writeMemoryRulesFile({ filePath, section });
|
|
1729
2899
|
if (result.action !== "unchanged") {
|
|
1730
2900
|
process.stderr.write(
|
|
1731
2901
|
`pyx-memory: refreshed the "## Persistent Memory" rules in ${filePath}
|
|
@@ -1736,6 +2906,31 @@ function reconcileMemoryRules(opts = {}) {
|
|
|
1736
2906
|
}
|
|
1737
2907
|
}
|
|
1738
2908
|
}
|
|
2909
|
+
var MAX_SERVER_RULES_BYTES = 16 * 1024;
|
|
2910
|
+
async function syncMemoryRulesFromServer(client, opts = {}) {
|
|
2911
|
+
try {
|
|
2912
|
+
const { tools } = await client.listTools();
|
|
2913
|
+
if (!tools.some((tool) => tool.name === "get_memory_rules")) {
|
|
2914
|
+
reconcileMemoryRules(opts);
|
|
2915
|
+
return;
|
|
2916
|
+
}
|
|
2917
|
+
const result = await client.callTool({ name: "get_memory_rules", arguments: {} });
|
|
2918
|
+
const [first] = result.content ?? [];
|
|
2919
|
+
const section = first?.type === "text" ? first.text : void 0;
|
|
2920
|
+
if (result.isError || !section || !isRulesSection(section)) {
|
|
2921
|
+
throw new Error('the server returned no valid "## Persistent Memory" section');
|
|
2922
|
+
}
|
|
2923
|
+
reconcileMemoryRules({ ...opts, section });
|
|
2924
|
+
} catch (err) {
|
|
2925
|
+
process.stderr.write(
|
|
2926
|
+
`pyx-memory: left the installed memory rules unchanged \u2014 could not fetch current rules (${err instanceof Error ? err.message : String(err)}).
|
|
2927
|
+
`
|
|
2928
|
+
);
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
function isRulesSection(text) {
|
|
2932
|
+
return text.startsWith("## Persistent Memory\n") && Buffer.byteLength(text, "utf8") <= MAX_SERVER_RULES_BYTES && !text.includes("<!-- pyx-memory:") && !/^\s*(```|~~~)/m.test(text);
|
|
2933
|
+
}
|
|
1739
2934
|
function validateScope(scope) {
|
|
1740
2935
|
if (VALID_SCOPES.has(scope)) return true;
|
|
1741
2936
|
process.stderr.write(`Error: invalid --scope \`${scope}\`. Expected: local | user | project.
|
|
@@ -1765,14 +2960,12 @@ function writeJsonAndReport(filePath, agentLabel, entry, opts = {}) {
|
|
|
1765
2960
|
|
|
1766
2961
|
// src/cli/commands/mcp.ts
|
|
1767
2962
|
async function mcpCommand() {
|
|
1768
|
-
reconcileMemoryRules();
|
|
1769
2963
|
const readCredentials = createReadCredentials(() => getDefaultKeychain());
|
|
1770
|
-
const onStdinEnd = new Promise((resolve3) => {
|
|
1771
|
-
process.stdin.once("end", resolve3);
|
|
1772
|
-
process.stdin.once("close", resolve3);
|
|
1773
|
-
});
|
|
1774
2964
|
try {
|
|
1775
|
-
await
|
|
2965
|
+
await runMcpProxyServer({
|
|
2966
|
+
readCredentials,
|
|
2967
|
+
onConnected: (client) => syncMemoryRulesFromServer(client)
|
|
2968
|
+
});
|
|
1776
2969
|
return EXIT.OK;
|
|
1777
2970
|
} catch (err) {
|
|
1778
2971
|
process.stderr.write(`Internal error: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -1869,7 +3062,7 @@ function boundedLabel(value, fallback, maxChars) {
|
|
|
1869
3062
|
|
|
1870
3063
|
// src/cli/commands/scaffold.ts
|
|
1871
3064
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1872
|
-
import { basename as basename2, join as
|
|
3065
|
+
import { basename as basename2, join as join3, resolve as resolve2 } from "path";
|
|
1873
3066
|
var SERVER_IMAGE = "ghcr.io/pyx-corp/pyx-memory-v1:latest";
|
|
1874
3067
|
var DOCKER_COMPOSE = `services:
|
|
1875
3068
|
pyx-memory:
|
|
@@ -1998,11 +3191,11 @@ function resolveTarget(args) {
|
|
|
1998
3191
|
}
|
|
1999
3192
|
function buildFiles(targetDir, appName) {
|
|
2000
3193
|
return [
|
|
2001
|
-
{ path:
|
|
2002
|
-
{ path:
|
|
2003
|
-
{ path:
|
|
3194
|
+
{ path: join3(targetDir, "docker-compose.yml"), contents: DOCKER_COMPOSE },
|
|
3195
|
+
{ path: join3(targetDir, ".env.example"), contents: ENV_EXAMPLE },
|
|
3196
|
+
{ path: join3(targetDir, "memory.ts"), contents: MEMORY_TS },
|
|
2004
3197
|
{
|
|
2005
|
-
path:
|
|
3198
|
+
path: join3(targetDir, "PYX_MEMORY_DESIGN_GUIDE.md"),
|
|
2006
3199
|
contents: buildDesignGuide({ appName })
|
|
2007
3200
|
}
|
|
2008
3201
|
];
|
|
@@ -2045,9 +3238,19 @@ function scaffoldCommand(args = {}) {
|
|
|
2045
3238
|
}
|
|
2046
3239
|
|
|
2047
3240
|
// src/cli/commands/status.ts
|
|
2048
|
-
|
|
3241
|
+
async function readLifecycle(provider) {
|
|
3242
|
+
let serverConvergenceSeconds = null;
|
|
3243
|
+
try {
|
|
3244
|
+
const journal = await loadJournal(provider);
|
|
3245
|
+
serverConvergenceSeconds = convergenceSeconds(journal);
|
|
3246
|
+
if (isEmpty(journal)) return { cleanupPending: false, serverConvergenceSeconds };
|
|
3247
|
+
} catch (err) {
|
|
3248
|
+
if (!(err instanceof JournalInvalidError)) throw err;
|
|
3249
|
+
}
|
|
3250
|
+
return { cleanupPending: !await provider.isLocked(), serverConvergenceSeconds };
|
|
3251
|
+
}
|
|
2049
3252
|
async function statusCommand(opts = {}) {
|
|
2050
|
-
const provider = opts.keychain
|
|
3253
|
+
const provider = opts.keychain ? guardKeychain(opts.keychain) : getDefaultKeychain();
|
|
2051
3254
|
let report4;
|
|
2052
3255
|
try {
|
|
2053
3256
|
const creds = await provider.read();
|
|
@@ -2056,8 +3259,10 @@ async function statusCommand(opts = {}) {
|
|
|
2056
3259
|
keyPresent: creds !== null,
|
|
2057
3260
|
keySource: creds !== null ? "keychain" : null,
|
|
2058
3261
|
loggedIn: creds !== null,
|
|
2059
|
-
credentialFingerprint: creds === null ? null :
|
|
2060
|
-
|
|
3262
|
+
credentialFingerprint: creds === null ? null : credentialFingerprint(creds.endpoint, creds.apiKey),
|
|
3263
|
+
lifecycleVersion: 1,
|
|
3264
|
+
connection: creds?.provenance === "browser-login" ? creds.connection ?? null : null,
|
|
3265
|
+
...await readLifecycle(provider)
|
|
2061
3266
|
};
|
|
2062
3267
|
} catch (err) {
|
|
2063
3268
|
if (err instanceof KeychainError) return emitError(err, opts.json);
|
|
@@ -2135,23 +3340,35 @@ async function taxonomyCommand(opts = {}) {
|
|
|
2135
3340
|
keychain: opts.keychain,
|
|
2136
3341
|
fetchImpl: opts.fetchImpl
|
|
2137
3342
|
});
|
|
3343
|
+
const withIdentity = opts.withIdentity === true;
|
|
2138
3344
|
if (!result.ok) {
|
|
2139
|
-
emit3(
|
|
3345
|
+
emit3(
|
|
3346
|
+
report3(result.state, result.endpoint, result.keySource, result.problem, {
|
|
3347
|
+
withIdentity,
|
|
3348
|
+
credentialFingerprint: result.credentialFingerprint
|
|
3349
|
+
})
|
|
3350
|
+
);
|
|
2140
3351
|
return result.exit;
|
|
2141
3352
|
}
|
|
2142
3353
|
emit3({
|
|
2143
|
-
...report3("ready", result.endpoint, "keychain", null
|
|
3354
|
+
...report3("ready", result.endpoint, "keychain", null, {
|
|
3355
|
+
withIdentity,
|
|
3356
|
+
credentialFingerprint: result.credentialFingerprint
|
|
3357
|
+
}),
|
|
2144
3358
|
clusters: result.data.clusters,
|
|
2145
3359
|
totals: result.data.totals
|
|
2146
3360
|
});
|
|
2147
3361
|
return EXIT.OK;
|
|
2148
3362
|
}
|
|
2149
|
-
function report3(state, endpoint, keySource, problem) {
|
|
3363
|
+
function report3(state, endpoint, keySource, problem, identity) {
|
|
2150
3364
|
return {
|
|
2151
3365
|
schemaVersion: TAXONOMY_SCHEMA_VERSION,
|
|
2152
3366
|
state,
|
|
2153
3367
|
endpoint,
|
|
2154
3368
|
keySource,
|
|
3369
|
+
// Spread, not `undefined`: JSON.stringify would drop an undefined value,
|
|
3370
|
+
// but the key must be absent from the object too so exact-shape checks hold.
|
|
3371
|
+
...identity.withIdentity ? { credentialFingerprint: identity.credentialFingerprint } : {},
|
|
2155
3372
|
clusters: [],
|
|
2156
3373
|
totals: { clusters: 0, unnamed: 0, nodesConsidered: 0 },
|
|
2157
3374
|
problem
|
|
@@ -2170,11 +3387,20 @@ Usage:
|
|
|
2170
3387
|
|
|
2171
3388
|
Commands:
|
|
2172
3389
|
login [--endpoint <url>] [--api-key <key>] Store endpoint and API key in OS credential store.
|
|
3390
|
+
login --browser [--json] [--no-open] Sign in with your browser; the approved project key is
|
|
3391
|
+
stored in the same place. --json emits JSONL events
|
|
3392
|
+
for host apps; --no-open leaves opening the URL to them.
|
|
2173
3393
|
status [--json] Show endpoint, key presence, MCP config status.
|
|
2174
3394
|
overview --json Read memory count and three recent safe labels.
|
|
2175
|
-
insights --json
|
|
2176
|
-
taxonomy --json
|
|
2177
|
-
|
|
3395
|
+
insights --json [--with-identity] Read body-free memory usage and coverage aggregates.
|
|
3396
|
+
taxonomy --json [--with-identity] Read knowledge-graph cluster composition (read-only).
|
|
3397
|
+
--with-identity adds credentialFingerprint (the same
|
|
3398
|
+
value as status --json) so a host can tell which
|
|
3399
|
+
account a read was served for. Off by default.
|
|
3400
|
+
logout [--json] Delete stored pyx-memory credentials. A browser sign-in
|
|
3401
|
+
key is also revoked on the server; a pasted key is not.
|
|
3402
|
+
login-recovery [--json] Finish pending sign-in/sign-out cleanup. Never signs in.
|
|
3403
|
+
[--discard-unreadable-journal] Only if told to: replace an unreadable cleanup record; signs this computer out.
|
|
2178
3404
|
doctor [--json] Diagnose keychain, credentials, backend, MCP startup.
|
|
2179
3405
|
scaffold [--name <dir>] Generate Docker, env, SDK, and memory design-guide starter files.
|
|
2180
3406
|
mcp Start the stdio MCP proxy to the hosted server, so
|
|
@@ -2193,6 +3419,10 @@ Commands:
|
|
|
2193
3419
|
your other content is preserved) so memory fires by
|
|
2194
3420
|
default. Pass --no-rules to skip that write.
|
|
2195
3421
|
Targets: claude-code, codex, cursor, cline, continue, windsurf, gemini-cli, pi, oh-my-pi, openclaw, hermes.
|
|
3422
|
+
mcp rules <target> Write or refresh only the always-on rules for
|
|
3423
|
+
claude-code, codex, gemini-cli, or oh-my-pi (no MCP
|
|
3424
|
+
registration). Honors CLAUDE_CONFIG_DIR / CODEX_HOME;
|
|
3425
|
+
'pyx-mem mcp' keeps them current from the server.
|
|
2196
3426
|
|
|
2197
3427
|
Notes:
|
|
2198
3428
|
- Credentials are stored only in the OS credential store (Keychain / libsecret / Credential Manager).
|
|
@@ -2296,12 +3526,43 @@ function runMcpCommand(parsed) {
|
|
|
2296
3526
|
parsed.flags["no-rules"] === true
|
|
2297
3527
|
);
|
|
2298
3528
|
}
|
|
3529
|
+
if (parsed.subcommand === "rules") {
|
|
3530
|
+
if (Object.keys(parsed.flags).length > 0) {
|
|
3531
|
+
process.stderr.write("Error: `pyx-mem mcp rules <host>` takes no flags.\n");
|
|
3532
|
+
return EXIT.USAGE;
|
|
3533
|
+
}
|
|
3534
|
+
return installMemoryRulesOnly(parsed.positional[0]);
|
|
3535
|
+
}
|
|
2299
3536
|
process.stderr.write(
|
|
2300
3537
|
`Error: unknown subcommand \`mcp ${parsed.subcommand}\`. Run \`pyx-mem --help\`.
|
|
2301
3538
|
`
|
|
2302
3539
|
);
|
|
2303
3540
|
return EXIT.USAGE;
|
|
2304
3541
|
}
|
|
3542
|
+
var LOGIN_FLAGS = /* @__PURE__ */ new Set(["endpoint", "api-key", "browser", "json", "no-open"]);
|
|
3543
|
+
function runLogin(parsed) {
|
|
3544
|
+
const { flags } = parsed;
|
|
3545
|
+
const browser = flags.browser === true;
|
|
3546
|
+
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;
|
|
3547
|
+
if (invalid) {
|
|
3548
|
+
process.stderr.write(
|
|
3549
|
+
"Error: usage: pyx-mem login [--endpoint <url>] [--api-key <key>] | pyx-mem login --browser [--json] [--no-open] [--endpoint <url>]\n"
|
|
3550
|
+
);
|
|
3551
|
+
return EXIT.USAGE;
|
|
3552
|
+
}
|
|
3553
|
+
const endpoint = typeof flags.endpoint === "string" ? flags.endpoint : void 0;
|
|
3554
|
+
if (browser) {
|
|
3555
|
+
return browserLoginCommand({
|
|
3556
|
+
endpoint,
|
|
3557
|
+
json: flags.json === true,
|
|
3558
|
+
noOpen: flags["no-open"] === true
|
|
3559
|
+
});
|
|
3560
|
+
}
|
|
3561
|
+
return loginCommand({
|
|
3562
|
+
endpoint,
|
|
3563
|
+
apiKey: typeof flags["api-key"] === "string" ? flags["api-key"] : void 0
|
|
3564
|
+
});
|
|
3565
|
+
}
|
|
2305
3566
|
async function main() {
|
|
2306
3567
|
const parsed = parseArgs(process.argv.slice(2));
|
|
2307
3568
|
if (parsed.flags.help === true || parsed.flags.h === true) {
|
|
@@ -2314,10 +3575,18 @@ async function main() {
|
|
|
2314
3575
|
process.stdout.write(HELP_TEXT);
|
|
2315
3576
|
return EXIT.OK;
|
|
2316
3577
|
case "login":
|
|
2317
|
-
return
|
|
2318
|
-
|
|
2319
|
-
|
|
3578
|
+
return runLogin(parsed);
|
|
3579
|
+
case "login-recovery":
|
|
3580
|
+
return loginRecoveryCommand({
|
|
3581
|
+
json: parsed.flags.json === true,
|
|
3582
|
+
discardUnreadableJournal: parsed.flags["discard-unreadable-journal"] === true
|
|
2320
3583
|
});
|
|
3584
|
+
case "lifecycle-capabilities":
|
|
3585
|
+
process.stdout.write(
|
|
3586
|
+
`${JSON.stringify({ schemaVersion: 1, browserLogin: true, sharedCredentialGuard: true })}
|
|
3587
|
+
`
|
|
3588
|
+
);
|
|
3589
|
+
return EXIT.OK;
|
|
2321
3590
|
case "status":
|
|
2322
3591
|
return statusCommand({ json: parsed.flags.json === true });
|
|
2323
3592
|
case "overview":
|
|
@@ -2331,15 +3600,15 @@ async function main() {
|
|
|
2331
3600
|
process.stderr.write("Error: `pyx-mem insights` requires --json.\n");
|
|
2332
3601
|
return EXIT.USAGE;
|
|
2333
3602
|
}
|
|
2334
|
-
return insightsCommand();
|
|
3603
|
+
return insightsCommand({ withIdentity: parsed.flags["with-identity"] === true });
|
|
2335
3604
|
case "taxonomy":
|
|
2336
3605
|
if (parsed.flags.json !== true) {
|
|
2337
3606
|
process.stderr.write("Error: `pyx-mem taxonomy` requires --json.\n");
|
|
2338
3607
|
return EXIT.USAGE;
|
|
2339
3608
|
}
|
|
2340
|
-
return taxonomyCommand();
|
|
3609
|
+
return taxonomyCommand({ withIdentity: parsed.flags["with-identity"] === true });
|
|
2341
3610
|
case "logout":
|
|
2342
|
-
return logoutCommand();
|
|
3611
|
+
return logoutCommand({ json: parsed.flags.json === true });
|
|
2343
3612
|
case "doctor":
|
|
2344
3613
|
return doctorCommand({ json: parsed.flags.json === true });
|
|
2345
3614
|
case "scaffold":
|