@pyxmate/memory 1.27.0 → 1.27.2

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.
@@ -0,0 +1,34 @@
1
+ // src/cli/probe.ts
2
+ import { randomUUID } from "crypto";
3
+ var PROBE_PATH = "/api/memory/entries?limit=1";
4
+ var PROBE_TIMEOUT_MS = 8e3;
5
+ async function probeCredentials(args) {
6
+ const fetcher = args.fetcher ?? fetch;
7
+ const url = `${args.endpoint}${PROBE_PATH}`;
8
+ const ctl = new AbortController();
9
+ const timer = setTimeout(() => ctl.abort(), PROBE_TIMEOUT_MS);
10
+ const idempotencyKey = `cli-probe-${randomUUID()}`;
11
+ try {
12
+ const res = await fetcher(url, {
13
+ method: "GET",
14
+ headers: {
15
+ Authorization: `Bearer ${args.apiKey}`,
16
+ Accept: "application/json",
17
+ "Idempotency-Key": idempotencyKey
18
+ },
19
+ signal: ctl.signal
20
+ });
21
+ if (res.status === 401 || res.status === 403) return { kind: "invalid", status: res.status };
22
+ if (res.ok) return { kind: "ok", status: res.status };
23
+ return { kind: "unreachable", reason: `HTTP ${res.status}` };
24
+ } catch (err) {
25
+ const msg = err instanceof Error ? err.message : String(err);
26
+ return { kind: "unreachable", reason: msg };
27
+ } finally {
28
+ clearTimeout(timer);
29
+ }
30
+ }
31
+
32
+ export {
33
+ probeCredentials
34
+ };
@@ -0,0 +1,263 @@
1
+ // src/mcp/errors.ts
2
+ var RUN_LOGIN = "Run: pyx-mem login";
3
+ var RUN_DOCTOR = "Run: pyx-mem doctor";
4
+ function mcpText(text, isError = false) {
5
+ return { isError, content: [{ type: "text", text }] };
6
+ }
7
+ function mcpMissingCredentials() {
8
+ return mcpText(`pyx-memory credentials are not configured.
9
+ ${RUN_LOGIN}`, true);
10
+ }
11
+ function mcpKeychainError(message, guidance) {
12
+ const lines = ["pyx-memory credentials could not be read from the OS credential store."];
13
+ if (message) lines.push(message);
14
+ if (guidance) lines.push(guidance);
15
+ lines.push(RUN_LOGIN);
16
+ return mcpText(lines.join("\n"), true);
17
+ }
18
+ function mcpAuthFailed(status) {
19
+ return mcpText(`Authentication failed (HTTP ${status}).
20
+ ${RUN_LOGIN}`, true);
21
+ }
22
+ function mcpUnreachable(endpoint, reason) {
23
+ return mcpText(`Unable to reach pyx-memory at ${endpoint}: ${reason}.
24
+ ${RUN_DOCTOR}`, true);
25
+ }
26
+ function mcpHttpError(status, body) {
27
+ return mcpText(`${status}: ${body}`, true);
28
+ }
29
+ function mcpServerError(status, body) {
30
+ return mcpText(`pyx-memory server error (HTTP ${status}): ${body || "no body"}`, true);
31
+ }
32
+ function mcpEnvelopeError(message) {
33
+ return mcpText(message, true);
34
+ }
35
+ function mcpJson(payload) {
36
+ return mcpText(JSON.stringify(payload));
37
+ }
38
+
39
+ // src/mcp/http-client.ts
40
+ var DEFAULT_TIMEOUT_MS = 15e3;
41
+ function parseRetryAfterSeconds(header) {
42
+ const value = header?.trim() ?? "";
43
+ return /^\d+$/.test(value) ? Number(value) : void 0;
44
+ }
45
+ var HTTP_DATE = /^(?:[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT|[A-Z][a-z]{5,8}, \d{2}-[A-Z][a-z]{2}-\d{2} \d{2}:\d{2}:\d{2} GMT|[A-Z][a-z]{2} [A-Z][a-z]{2} [ \d]\d \d{2}:\d{2}:\d{2} \d{4})$/;
46
+ function parseRetryAfterMs(header) {
47
+ const seconds = parseRetryAfterSeconds(header);
48
+ if (seconds !== void 0) return seconds * 1e3;
49
+ const value = header?.trim() ?? "";
50
+ if (!HTTP_DATE.test(value)) return void 0;
51
+ const at = Date.parse(value.endsWith(" GMT") ? value : `${value} GMT`);
52
+ return Number.isNaN(at) ? void 0 : Math.max(0, at - Date.now());
53
+ }
54
+ function validateIdempotencyKey(key) {
55
+ if (key.length === 0 || !key.isWellFormed() || new TextEncoder().encode(key).byteLength > 256) {
56
+ throw new Error("idempotencyKey must be 1-256 well-formed UTF-8 bytes");
57
+ }
58
+ return key;
59
+ }
60
+ function createHttpClient(credentials, fetchImpl = fetch) {
61
+ const baseUrl = credentials.endpoint.replace(/\/+$/, "");
62
+ function buildUrl(path, query) {
63
+ const url = new URL(`${baseUrl}${path}`);
64
+ if (query) {
65
+ for (const [k, v] of Object.entries(query)) {
66
+ if (v === void 0) continue;
67
+ url.searchParams.set(k, String(v));
68
+ }
69
+ }
70
+ return url.toString();
71
+ }
72
+ function buildHeaders(scope, contentType, idempotencyKey) {
73
+ const headers = new Headers();
74
+ headers.set("Authorization", `Bearer ${credentials.apiKey}`);
75
+ headers.set("Accept", "application/json");
76
+ if (contentType) headers.set("Content-Type", contentType);
77
+ if (scope?.tenantId) headers.set("X-Tenant-Id", scope.tenantId);
78
+ if (scope?.namespaceId) headers.set("X-Namespace-Id", scope.namespaceId);
79
+ if (scope?.userId) headers.set("X-User-Id", scope.userId);
80
+ if (scope?.teamId) headers.set("X-Team-Id", scope.teamId);
81
+ if (scope?.callerAccessLevel) headers.set("X-Caller-Access-Level", scope.callerAccessLevel);
82
+ if (idempotencyKey !== void 0) {
83
+ headers.set("Idempotency-Key", validateIdempotencyKey(idempotencyKey));
84
+ }
85
+ return headers;
86
+ }
87
+ async function send(url, method, headers, body, deadline = {}) {
88
+ const { timeoutMs, signal } = deadline;
89
+ const ctl = new AbortController();
90
+ const timer = setTimeout(
91
+ () => timeoutMs === void 0 ? ctl.abort() : ctl.abort(
92
+ new DOMException(`no complete response within ${timeoutMs / 1e3} s`, "TimeoutError")
93
+ ),
94
+ timeoutMs ?? DEFAULT_TIMEOUT_MS
95
+ );
96
+ try {
97
+ let res;
98
+ try {
99
+ res = await fetchImpl(url, {
100
+ method,
101
+ headers,
102
+ body,
103
+ signal: signal ? AbortSignal.any([ctl.signal, signal]) : ctl.signal
104
+ });
105
+ } catch (err) {
106
+ return {
107
+ ok: false,
108
+ result: mcpUnreachable(baseUrl, err instanceof Error ? err.message : String(err))
109
+ };
110
+ } finally {
111
+ if (timeoutMs === void 0) clearTimeout(timer);
112
+ }
113
+ return await readResponse(res);
114
+ } finally {
115
+ clearTimeout(timer);
116
+ }
117
+ }
118
+ return {
119
+ async requestJson(input) {
120
+ const url = buildUrl(input.path, input.query);
121
+ const hasBody = input.body !== void 0;
122
+ const headers = buildHeaders(
123
+ input.scope,
124
+ hasBody ? "application/json" : void 0,
125
+ input.idempotencyKey
126
+ );
127
+ return send(
128
+ url,
129
+ input.method,
130
+ headers,
131
+ hasBody ? JSON.stringify(input.body) : void 0,
132
+ input
133
+ );
134
+ },
135
+ async requestMultipart(input) {
136
+ const url = buildUrl(input.path);
137
+ const headers = buildHeaders(input.scope, void 0, input.idempotencyKey);
138
+ const form = await input.formData();
139
+ return send(url, "POST", headers, form);
140
+ },
141
+ async requestBytes(input) {
142
+ const headers = buildHeaders(input.scope, "application/octet-stream", input.idempotencyKey);
143
+ for (const [name, value] of Object.entries(input.headers)) headers.set(name, value);
144
+ return send(buildUrl(input.path), input.method, headers, input.body, input);
145
+ }
146
+ };
147
+ }
148
+ async function readResponse(res) {
149
+ if (res.status === 401 || res.status === 403) {
150
+ return { ok: false, status: res.status, result: mcpAuthFailed(res.status) };
151
+ }
152
+ let text;
153
+ try {
154
+ text = await res.text();
155
+ } catch (err) {
156
+ const reason = err instanceof Error ? err.message : String(err);
157
+ return {
158
+ ok: false,
159
+ status: res.status,
160
+ result: mcpEnvelopeError(
161
+ `Failed to read pyx-memory response (HTTP ${res.status}): ${reason}`
162
+ )
163
+ };
164
+ }
165
+ return parseHttpResponse(res, text);
166
+ }
167
+ function parseHttpResponse(res, text) {
168
+ if (res.ok && res.headers.get("content-type")?.includes("application/x-ndjson")) {
169
+ return parseNdjsonOutcome(res.status, text);
170
+ }
171
+ const parsed = parseJsonText(text);
172
+ const gateway = parsed.ok ? gatewayError(parsed.value) : void 0;
173
+ const failure = {
174
+ ok: false,
175
+ status: res.status,
176
+ code: parsed.ok ? extractEnvelopeCode(parsed.value) : void 0,
177
+ retryAfter: res.headers.get("retry-after") ?? void 0
178
+ };
179
+ if (res.status >= 500) {
180
+ return { ...failure, result: mcpServerError(res.status, gateway?.message ?? text) };
181
+ }
182
+ if (!parsed.ok) {
183
+ if (!res.ok) {
184
+ return { ...failure, result: mcpHttpError(res.status, text || "no body") };
185
+ }
186
+ return { ok: true, status: res.status, data: text };
187
+ }
188
+ if (!res.ok) {
189
+ const message = extractEnvelopeError(parsed.value) ?? gateway?.message ?? text ?? `HTTP ${res.status}`;
190
+ return { ...failure, result: mcpHttpError(res.status, message) };
191
+ }
192
+ const envelopeError = extractEnvelopeError(parsed.value);
193
+ if (envelopeError) {
194
+ return { ...failure, result: mcpEnvelopeError(envelopeError) };
195
+ }
196
+ return { ok: true, status: res.status, data: parsed.value };
197
+ }
198
+ function parseNdjsonOutcome(status, text) {
199
+ for (const line of text.split("\n")) {
200
+ if (!line.trim()) continue;
201
+ let frame;
202
+ try {
203
+ frame = JSON.parse(line);
204
+ } catch (err) {
205
+ const reason = err instanceof Error ? err.message : String(err);
206
+ return {
207
+ ok: false,
208
+ status,
209
+ result: mcpEnvelopeError(`pyx-memory sent an invalid NDJSON line: ${reason}`)
210
+ };
211
+ }
212
+ if (frame?.type === "result") return { ok: true, status, data: frame };
213
+ if (frame?.type === "error") {
214
+ return { ok: false, status, result: mcpEnvelopeError(JSON.stringify(frame)) };
215
+ }
216
+ }
217
+ return {
218
+ ok: false,
219
+ status,
220
+ result: mcpEnvelopeError("File ingest stream ended without a server result")
221
+ };
222
+ }
223
+ function parseJsonText(text) {
224
+ if (text.length === 0) return { ok: true, value: void 0 };
225
+ try {
226
+ return { ok: true, value: JSON.parse(text) };
227
+ } catch {
228
+ return { ok: false };
229
+ }
230
+ }
231
+ function gatewayError(parsed) {
232
+ if (parsed === null || typeof parsed !== "object") return void 0;
233
+ const error = parsed.error;
234
+ if (error === null || typeof error !== "object") return void 0;
235
+ const { code, message } = error;
236
+ return typeof code === "string" && typeof message === "string" ? { code, message } : void 0;
237
+ }
238
+ function extractEnvelopeCode(parsed) {
239
+ if (parsed === null || typeof parsed !== "object") return void 0;
240
+ const obj = parsed;
241
+ if (obj.success === false && typeof obj.code === "string") return obj.code;
242
+ return gatewayError(parsed)?.code;
243
+ }
244
+ function extractEnvelopeError(parsed) {
245
+ if (parsed === null || typeof parsed !== "object") return null;
246
+ const obj = parsed;
247
+ if (obj.success === false) {
248
+ if (typeof obj.error === "string") return obj.error;
249
+ if (typeof obj.message === "string") return obj.message;
250
+ return "pyx-memory returned success: false";
251
+ }
252
+ return null;
253
+ }
254
+
255
+ export {
256
+ mcpText,
257
+ mcpMissingCredentials,
258
+ mcpKeychainError,
259
+ mcpJson,
260
+ parseRetryAfterSeconds,
261
+ parseRetryAfterMs,
262
+ createHttpClient
263
+ };
@@ -0,0 +1,165 @@
1
+ import {
2
+ createHttpClient
3
+ } from "./chunk-THLUBOGD.mjs";
4
+ import {
5
+ KeychainError,
6
+ credentialFingerprint,
7
+ getDefaultKeychain
8
+ } from "./chunk-7TATCSSN.mjs";
9
+ import {
10
+ EXIT
11
+ } from "./chunk-4DXEQC6X.mjs";
12
+
13
+ // src/cli/commands/read-surface.ts
14
+ import { randomUUID } from "crypto";
15
+ async function readSurface(opts) {
16
+ const provider = opts.keychain ?? getDefaultKeychain();
17
+ let credentials;
18
+ try {
19
+ credentials = await provider.read();
20
+ } catch (error) {
21
+ return {
22
+ ok: false,
23
+ exit: error instanceof KeychainError ? error.exit : EXIT.CRED_UNAVAILABLE,
24
+ state: "error",
25
+ endpoint: null,
26
+ keySource: null,
27
+ credentialFingerprint: null,
28
+ problem: error instanceof KeychainError ? [error.message, error.guidance].filter(Boolean).join(" ") : "The OS credential store could not be read."
29
+ };
30
+ }
31
+ if (!credentials) {
32
+ return {
33
+ ok: false,
34
+ exit: EXIT.NOT_LOGGED_IN,
35
+ state: "signed_out",
36
+ endpoint: null,
37
+ keySource: null,
38
+ credentialFingerprint: null,
39
+ problem: null
40
+ };
41
+ }
42
+ const fingerprint = credentialFingerprint(credentials.endpoint, credentials.apiKey);
43
+ if (!isHttpUrl(credentials.endpoint)) {
44
+ return {
45
+ ok: false,
46
+ exit: EXIT.DOCTOR_FAIL,
47
+ state: "error",
48
+ endpoint: null,
49
+ keySource: "keychain",
50
+ credentialFingerprint: fingerprint,
51
+ problem: "The stored pyx-memory endpoint is invalid. Run: pyx-mem login"
52
+ };
53
+ }
54
+ const http = createHttpClient(credentials, opts.fetchImpl ?? fetch);
55
+ const response = await http.requestJson({
56
+ ...opts.request,
57
+ idempotencyKey: `cli-${opts.surface}-${randomUUID()}`
58
+ });
59
+ if (!response.ok) {
60
+ return failedSurfaceRequest(
61
+ opts.surface,
62
+ response.status,
63
+ credentials.endpoint,
64
+ fingerprint,
65
+ opts.minimumServiceVersion,
66
+ http
67
+ );
68
+ }
69
+ const parsed = opts.schema.safeParse(response.data);
70
+ if (!parsed.success) {
71
+ return {
72
+ ok: false,
73
+ exit: EXIT.DOCTOR_FAIL,
74
+ state: "error",
75
+ endpoint: credentials.endpoint,
76
+ keySource: "keychain",
77
+ credentialFingerprint: fingerprint,
78
+ problem: `pyx-memory returned an invalid ${opts.surface} response. Run: pyx-mem doctor`
79
+ };
80
+ }
81
+ return {
82
+ ok: true,
83
+ endpoint: credentials.endpoint,
84
+ credentialFingerprint: fingerprint,
85
+ data: parsed.data
86
+ };
87
+ }
88
+ async function failedSurfaceRequest(surface, status, endpoint, credentialFingerprint2, minimumServiceVersion, http) {
89
+ if (status === 401) {
90
+ return {
91
+ ok: false,
92
+ exit: EXIT.NOT_LOGGED_IN,
93
+ state: "signed_out",
94
+ endpoint,
95
+ keySource: "keychain",
96
+ credentialFingerprint: credentialFingerprint2,
97
+ problem: requestFailure(surface, status)
98
+ };
99
+ }
100
+ const serviceVersion = minimumServiceVersion ? await probeServiceVersion(http) : null;
101
+ const serviceOutdated = serviceVersion !== null && minimumServiceVersion !== void 0 && isVersionOlder(serviceVersion, minimumServiceVersion);
102
+ return {
103
+ ok: false,
104
+ exit: EXIT.DOCTOR_FAIL,
105
+ state: serviceOutdated ? "service_outdated" : "error",
106
+ endpoint,
107
+ keySource: "keychain",
108
+ credentialFingerprint: credentialFingerprint2,
109
+ 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)
110
+ };
111
+ }
112
+ async function probeServiceVersion(http) {
113
+ const response = await http.requestJson({ method: "GET", path: "/status" });
114
+ if (!response.ok) return null;
115
+ const payload = plainObject(response.data);
116
+ const topology = plainObject(payload?.success === true ? plainObject(payload.data) : payload);
117
+ const version = payload?.version ?? plainObject(topology?.service)?.version;
118
+ return typeof version === "string" && parseVersion(version) ? version : null;
119
+ }
120
+ function plainObject(value) {
121
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
122
+ }
123
+ function isVersionOlder(actual, minimum) {
124
+ const left = parseVersion(actual);
125
+ const right = parseVersion(minimum);
126
+ if (!left || !right) return false;
127
+ if (left.core[0] !== right.core[0]) return left.core[0] < right.core[0];
128
+ if (left.core[1] !== right.core[1]) return left.core[1] < right.core[1];
129
+ if (left.core[2] !== right.core[2]) return left.core[2] < right.core[2];
130
+ return left.prerelease !== null && right.prerelease === null;
131
+ }
132
+ function parseVersion(value) {
133
+ if (value.length > 64) return null;
134
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
135
+ if (!match) return null;
136
+ const core = [Number(match[1]), Number(match[2]), Number(match[3])];
137
+ return core.every(Number.isSafeInteger) ? { core, prerelease: match[4] ?? null } : null;
138
+ }
139
+ function requestFailure(surface, status) {
140
+ if (status === 401) {
141
+ return "Stored pyx-memory credentials were rejected. Run: pyx-mem login";
142
+ }
143
+ if (status === 403) {
144
+ return "pyx-memory denied access (HTTP 403). Check the key scope and project access.";
145
+ }
146
+ if (status === void 0) {
147
+ return "Unable to reach pyx-memory. Run: pyx-mem doctor";
148
+ }
149
+ if (status >= 500) {
150
+ return `pyx-memory is unavailable (HTTP ${status}). Run: pyx-mem doctor`;
151
+ }
152
+ return `pyx-memory ${surface} failed (HTTP ${status}). Run: pyx-mem doctor`;
153
+ }
154
+ function isHttpUrl(value) {
155
+ try {
156
+ const url = new URL(value);
157
+ return url.protocol === "http:" || url.protocol === "https:";
158
+ } catch {
159
+ return false;
160
+ }
161
+ }
162
+
163
+ export {
164
+ readSurface
165
+ };
@@ -0,0 +1,132 @@
1
+ import {
2
+ settle
3
+ } from "./chunk-7IGU3HJO.mjs";
4
+ import {
5
+ BUSY,
6
+ ERROR_MESSAGES,
7
+ JournalInvalidError,
8
+ KeychainError,
9
+ LockLostError,
10
+ convergenceSeconds,
11
+ createCliLoginApi,
12
+ credentialFingerprint,
13
+ getLifecycleKeychain,
14
+ isEmpty,
15
+ isKeyRef,
16
+ loadJournal,
17
+ saveJournal,
18
+ withLifecycleLock
19
+ } from "./chunk-7TATCSSN.mjs";
20
+ import {
21
+ EXIT
22
+ } from "./chunk-4DXEQC6X.mjs";
23
+
24
+ // src/cli/commands/logout.ts
25
+ function lifecycleFailure(code, cleanupPending, err) {
26
+ return {
27
+ ok: false,
28
+ code,
29
+ // Keychain guidance is static text; any other error's text is never shown.
30
+ message: err instanceof KeychainError ? `${err.message} ${err.guidance}` : ERROR_MESSAGES[code].message,
31
+ retryable: ERROR_MESSAGES[code].retryable,
32
+ cleanupPending
33
+ };
34
+ }
35
+ async function cleanupPendingAfterFailure(keychain) {
36
+ try {
37
+ return !isEmpty(await loadJournal(keychain));
38
+ } catch {
39
+ return true;
40
+ }
41
+ }
42
+ async function logoutCommand(opts = {}) {
43
+ const keychain = opts.keychain ?? getLifecycleKeychain();
44
+ const api = createCliLoginApi(opts.fetchImpl);
45
+ const fail = (failure, exit) => {
46
+ if (opts.json) process.stdout.write(`${JSON.stringify(failure)}
47
+ `);
48
+ else process.stderr.write(`Error: ${failure.message}
49
+ `);
50
+ return exit;
51
+ };
52
+ try {
53
+ const result = await withLifecycleLock(keychain, (lockLost) => logout(lockLost));
54
+ return result === BUSY ? fail(lifecycleFailure("busy", false), EXIT.BUSY) : result;
55
+ } catch (err) {
56
+ const pending = await cleanupPendingAfterFailure(keychain);
57
+ if (err instanceof KeychainError) {
58
+ return fail(lifecycleFailure("credential_store_unavailable", pending, err), err.exit);
59
+ }
60
+ if (err instanceof LockLostError) {
61
+ return fail(lifecycleFailure("lock_lost", pending), EXIT.DOCTOR_FAIL);
62
+ }
63
+ if (err instanceof JournalInvalidError) {
64
+ return fail(lifecycleFailure("journal_invalid", true), EXIT.DOCTOR_FAIL);
65
+ }
66
+ return fail(lifecycleFailure("cleanup_pending", true), EXIT.CLEANUP_PENDING);
67
+ }
68
+ async function logout(lockLost) {
69
+ const journal = await loadJournal(keychain);
70
+ const selected = await keychain.read().catch((err) => {
71
+ if (err instanceof KeychainError && err.code === "credential-record-invalid") {
72
+ return "unreadable";
73
+ }
74
+ throw err;
75
+ });
76
+ const managed = selected !== null && selected !== "unreadable" && selected.provenance === "browser-login" && isKeyRef({ endpoint: selected.endpoint, apiKey: selected.apiKey }) ? { endpoint: selected.endpoint, apiKey: selected.apiKey } : null;
77
+ if (selected === null || selected === "unreadable") {
78
+ await keychain.delete();
79
+ } else {
80
+ journal.logout = {
81
+ fingerprint: credentialFingerprint(selected.endpoint, selected.apiKey),
82
+ revoke: managed
83
+ };
84
+ await saveJournal(keychain, journal);
85
+ }
86
+ const cleanupPending = !isEmpty(await settle(keychain, api, journal, lockLost));
87
+ if (await keychain.read() !== null) {
88
+ return fail(lifecycleFailure("credential_changed", cleanupPending), EXIT.DOCTOR_FAIL);
89
+ }
90
+ if (lockLost()) throw new LockLostError();
91
+ const convergence = convergenceSeconds(journal);
92
+ const revocationPending = journal.revoke.length > 0;
93
+ const serverRevocation = revocationPending ? "pending" : convergence !== null ? "revoked" : "not_attempted";
94
+ const legacy = selected !== null && managed === null;
95
+ const report = {
96
+ ok: true,
97
+ credentialKind: selected === null ? null : managed !== null ? "browser" : "legacy",
98
+ localDeleted: selected !== null,
99
+ serverRevocation,
100
+ cleanupPending,
101
+ reconnectRequired: selected !== null || serverRevocation !== "not_attempted",
102
+ // Proxies older than the per-request credential check hold the key in
103
+ // memory. A key that is still valid on the server keeps them working.
104
+ pendingLegacySessions: legacy || revocationPending,
105
+ serverConvergenceSeconds: convergence
106
+ };
107
+ if (opts.json) {
108
+ process.stdout.write(`${JSON.stringify(report)}
109
+ `);
110
+ } else {
111
+ process.stdout.write(
112
+ selected === null ? "No pyx-memory credentials were stored.\n" : "pyx-memory credentials deleted from OS credential store.\n"
113
+ );
114
+ if (cleanupPending) {
115
+ process.stderr.write(
116
+ "The server-side key could not be revoked yet. Run: pyx-mem login-recovery\n"
117
+ );
118
+ } else if (report.pendingLegacySessions) {
119
+ process.stdout.write(
120
+ "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"
121
+ );
122
+ }
123
+ }
124
+ return cleanupPending ? EXIT.CLEANUP_PENDING : EXIT.OK;
125
+ }
126
+ }
127
+
128
+ export {
129
+ lifecycleFailure,
130
+ cleanupPendingAfterFailure,
131
+ logoutCommand
132
+ };