@pyxmate/memory 1.26.6 → 1.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,7 @@ import {
13
13
  unreachableHealth
14
14
  } from "./chunk-N4KDRRLY.mjs";
15
15
  import "./chunk-M67SXB7C.mjs";
16
- import "./chunk-KZKGRND2.mjs";
16
+ import "./chunk-RWQTBXUC.mjs";
17
17
  import "./chunk-HTQBKJTD.mjs";
18
18
  export {
19
19
  DashboardClient,
@@ -0,0 +1,143 @@
1
+ import {
2
+ probeCredentials
3
+ } from "./chunk-S3CX323Z.mjs";
4
+ import {
5
+ KeychainError,
6
+ getDefaultKeychain
7
+ } from "./chunk-7TATCSSN.mjs";
8
+ import {
9
+ EXIT
10
+ } from "./chunk-4DXEQC6X.mjs";
11
+
12
+ // src/cli/commands/doctor.ts
13
+ async function doctorCommand(opts = {}) {
14
+ const provider = opts.keychain ?? getDefaultKeychain();
15
+ const fetcher = opts.fetchImpl ?? fetch;
16
+ const checks = [];
17
+ const accessCheck = await provider.check();
18
+ if (accessCheck.ok) {
19
+ checks.push({ id: "provider.import", status: "pass", message: "native keyring module loaded" });
20
+ checks.push({ id: "keychain.access", status: "pass", message: accessCheck.message });
21
+ } else if (accessCheck.code === "provider-import-failed") {
22
+ checks.push({
23
+ id: "provider.import",
24
+ status: "fail",
25
+ code: accessCheck.code,
26
+ message: accessCheck.message,
27
+ guidance: accessCheck.guidance
28
+ });
29
+ checks.push({
30
+ id: "keychain.access",
31
+ status: "skip",
32
+ message: "skipped \u2014 provider import failed"
33
+ });
34
+ } else {
35
+ checks.push({ id: "provider.import", status: "pass", message: "native keyring module loaded" });
36
+ checks.push({
37
+ id: "keychain.access",
38
+ status: "fail",
39
+ code: accessCheck.code,
40
+ message: accessCheck.message,
41
+ guidance: accessCheck.guidance
42
+ });
43
+ }
44
+ const accessOk = accessCheck.ok;
45
+ let creds = null;
46
+ if (accessOk) {
47
+ try {
48
+ creds = await provider.read();
49
+ if (creds) {
50
+ checks.push({
51
+ id: "credentials.present",
52
+ status: "pass",
53
+ message: "credentials present in keychain",
54
+ endpoint: creds.endpoint
55
+ });
56
+ } else {
57
+ checks.push({
58
+ id: "credentials.present",
59
+ status: "fail",
60
+ code: "not-logged-in",
61
+ message: "no pyx-memory credentials stored",
62
+ guidance: "Run: pyx-mem login"
63
+ });
64
+ }
65
+ } catch (err) {
66
+ if (err instanceof KeychainError) {
67
+ checks.push({
68
+ id: "credentials.present",
69
+ status: "fail",
70
+ code: err.code,
71
+ message: err.message,
72
+ guidance: err.guidance
73
+ });
74
+ } else {
75
+ throw err;
76
+ }
77
+ }
78
+ } else {
79
+ checks.push({
80
+ id: "credentials.present",
81
+ status: "skip",
82
+ message: "skipped \u2014 keychain access failed"
83
+ });
84
+ }
85
+ if (creds) {
86
+ const probe = await probeCredentials({
87
+ endpoint: creds.endpoint,
88
+ apiKey: creds.apiKey,
89
+ fetcher
90
+ });
91
+ if (probe.kind === "ok") {
92
+ checks.push({
93
+ id: "backend.reachable",
94
+ status: "pass",
95
+ message: `pyx-memory API reachable (HTTP ${probe.status})`,
96
+ endpoint: creds.endpoint
97
+ });
98
+ } else if (probe.kind === "invalid") {
99
+ checks.push({
100
+ id: "backend.reachable",
101
+ status: "fail",
102
+ code: "key-rejected",
103
+ message: `API key rejected (HTTP ${probe.status})`,
104
+ endpoint: creds.endpoint,
105
+ guidance: "Re-login with a valid key: pyx-mem login"
106
+ });
107
+ } else {
108
+ checks.push({
109
+ id: "backend.reachable",
110
+ status: "fail",
111
+ code: "unreachable",
112
+ message: `no pyx-memory API at this endpoint (${probe.reason})`,
113
+ endpoint: creds.endpoint,
114
+ guidance: "Verify the endpoint is the API backend, not a web UI: pyx-mem login --endpoint <API_URL>"
115
+ });
116
+ }
117
+ } else {
118
+ checks.push({
119
+ id: "backend.reachable",
120
+ status: "skip",
121
+ message: "skipped \u2014 no credentials to probe"
122
+ });
123
+ }
124
+ const ok = checks.every((c) => c.status !== "fail");
125
+ if (opts.json) {
126
+ process.stdout.write(`${JSON.stringify({ ok, checks })}
127
+ `);
128
+ } else {
129
+ for (const c of checks) {
130
+ const tag = c.status === "pass" ? "OK " : c.status === "skip" ? "SKIP" : "FAIL";
131
+ process.stdout.write(`[${tag}] ${c.id}: ${c.message}
132
+ `);
133
+ if (c.endpoint) process.stdout.write(` endpoint: ${c.endpoint}
134
+ `);
135
+ if (c.status === "fail" && c.guidance) process.stdout.write(` ${c.guidance}
136
+ `);
137
+ }
138
+ }
139
+ return ok ? EXIT.OK : EXIT.DOCTOR_FAIL;
140
+ }
141
+ export {
142
+ doctorCommand
143
+ };
package/dist/index.d.ts CHANGED
@@ -776,6 +776,9 @@ interface MemoryInsights {
776
776
  topic: string | null;
777
777
  project: string | null;
778
778
  at: string;
779
+ writtenBy?: string | null;
780
+ writtenAt?: string | null;
781
+ appliedBy?: string | null;
779
782
  }>;
780
783
  }
781
784
  interface GraphRepairResult {
@@ -1534,6 +1537,8 @@ declare const TAXONOMY_MAX_TOP_ENTITIES = 15;
1534
1537
  declare const TAXONOMY_MAX_SAMPLE_TOPICS = 8;
1535
1538
  declare const TAXONOMY_MAX_PROJECTS = 8;
1536
1539
  declare const MEMORY_PROJECT_LABEL_MAX_CHARS = 80;
1540
+ declare const MCP_ACTOR_META_KEY = "pyx.dev/actor";
1541
+ declare const MEMORY_ACTOR_LABEL_MAX_CHARS = 80;
1537
1542
 
1538
1543
  /**
1539
1544
  * Result shape returned by an LLM entity extractor — entities + relations.
@@ -1599,6 +1604,8 @@ declare function mergeExtractedEntities(callerEntities: IngestEntity[] | undefin
1599
1604
  relationships: IngestRelationship[];
1600
1605
  };
1601
1606
 
1607
+ declare function normalizeMemoryActorLabel(value: unknown): string | null;
1608
+
1602
1609
  declare function projectSearchResponseForMcp(payload: unknown): unknown;
1603
1610
 
1604
1611
  /**
@@ -2004,4 +2011,4 @@ interface CreatePyxMemoryOptions {
2004
2011
  }
2005
2012
  declare function createPyxMemory(opts?: CreatePyxMemoryOptions): MemoryClient;
2006
2013
 
2007
- export { type AgentId, type ApiResponse, type ConsolidationJudgementsResult, type ConsolidationMerge, type ConsolidationPair, type ConsolidationPairSide, type ConsolidationRunResult, type CorrectionInput, type CorrectionRecord, type CreatePyxMemoryOptions, DEFAULTS, DEPRECATED_RAG_STRATEGIES, DisabledMemory, type DroppedGraphRelationship, type DueScanInput, EmbeddingProviderName, type EnrichmentCallbacks, type EnrichmentPurpose, type EntityExtractionResult, type ExpandAnchorStatus, type ExpandParams, type ExpandResult, type ExtendedMemoryInterface, type FetchCorrectionsInput, type FileDownloadOptions, type GraphEnrichEvent, type GraphEnrichFileOptions, type GraphEnrichPreparedEvent, type GraphEnrichResult, type GraphEnrichResultEvent, type GraphEnrichment, type GraphEnrichmentStatus, type GraphExtractionPayload, type GraphFailureMode, type GraphNode, type GraphRelationship, type GraphRepairResult, type GraphTelemetrySnapshot, type GraphTraversalResult, type IngestEntity, type IngestErrorEvent, type IngestEvent, type IngestFileOptions, type IngestHeartbeatEvent, type IngestProgressEvent, type IngestRelationship, type IngestResultEvent, type IngestStage, type IngestionResult, type LineageParams, type LineageResult, type LineageVersion, MEMORY_PROJECT_LABEL_MAX_CHARS, MemoryClient, type MemoryClientOptions, type MemoryEntry, type MemoryIngestRequest, type MemoryInsightMetrics, type MemoryInsights, type MemoryInterface, type MemoryListParams, type MemoryListResult, type MemoryLogFilters, type MemoryProjectInsight, type MemorySearchParams, type MemorySearchResult, MemoryServerError, type MemoryStats, MemoryType, type MoveEntriesFilter, MoveFailureReason, type MoveResult, type MoveTarget, NamespaceIsolation, type PrincipalContext, RAGStrategy, type ReinforceParams, type ReinforceResult, type ReinforceSignal, SESSION_ORDINAL_MAX, SINGLE_TENANT_ID, type SecretElevationAggregate, type SecretElevationNotice, SensitivityLevel, type SourceEvidence, type StoreInput, StoreTarget, TAXONOMY_MAX_CATEGORIES, TAXONOMY_MAX_PROJECTS, TAXONOMY_MAX_SAMPLE_TOPICS, TAXONOMY_MAX_TOP_ENTITIES, type TemporalQueryFilters, type TenantScopeOptions, type Timestamp, type Topology, type TopologyExtractionProvider, type TopologyServiceVariant, type UsageHygieneSnapshot, VectorProvider, type VectorStatus, type WikiLintReport, assertGraphExtractionPayload, createPyxMemory, isSessionOrdinal, mergeExtractedEntities, normalizeGraphLabel, normalizeNameKey, projectSearchResponseForMcp, secretElevationAggregate, secretElevationNoticeFor, withSecretElevationNotice };
2014
+ export { type AgentId, type ApiResponse, type ConsolidationJudgementsResult, type ConsolidationMerge, type ConsolidationPair, type ConsolidationPairSide, type ConsolidationRunResult, type CorrectionInput, type CorrectionRecord, type CreatePyxMemoryOptions, DEFAULTS, DEPRECATED_RAG_STRATEGIES, DisabledMemory, type DroppedGraphRelationship, type DueScanInput, EmbeddingProviderName, type EnrichmentCallbacks, type EnrichmentPurpose, type EntityExtractionResult, type ExpandAnchorStatus, type ExpandParams, type ExpandResult, type ExtendedMemoryInterface, type FetchCorrectionsInput, type FileDownloadOptions, type GraphEnrichEvent, type GraphEnrichFileOptions, type GraphEnrichPreparedEvent, type GraphEnrichResult, type GraphEnrichResultEvent, type GraphEnrichment, type GraphEnrichmentStatus, type GraphExtractionPayload, type GraphFailureMode, type GraphNode, type GraphRelationship, type GraphRepairResult, type GraphTelemetrySnapshot, type GraphTraversalResult, type IngestEntity, type IngestErrorEvent, type IngestEvent, type IngestFileOptions, type IngestHeartbeatEvent, type IngestProgressEvent, type IngestRelationship, type IngestResultEvent, type IngestStage, type IngestionResult, type LineageParams, type LineageResult, type LineageVersion, MCP_ACTOR_META_KEY, MEMORY_ACTOR_LABEL_MAX_CHARS, MEMORY_PROJECT_LABEL_MAX_CHARS, MemoryClient, type MemoryClientOptions, type MemoryEntry, type MemoryIngestRequest, type MemoryInsightMetrics, type MemoryInsights, type MemoryInterface, type MemoryListParams, type MemoryListResult, type MemoryLogFilters, type MemoryProjectInsight, type MemorySearchParams, type MemorySearchResult, MemoryServerError, type MemoryStats, MemoryType, type MoveEntriesFilter, MoveFailureReason, type MoveResult, type MoveTarget, NamespaceIsolation, type PrincipalContext, RAGStrategy, type ReinforceParams, type ReinforceResult, type ReinforceSignal, SESSION_ORDINAL_MAX, SINGLE_TENANT_ID, type SecretElevationAggregate, type SecretElevationNotice, SensitivityLevel, type SourceEvidence, type StoreInput, StoreTarget, TAXONOMY_MAX_CATEGORIES, TAXONOMY_MAX_PROJECTS, TAXONOMY_MAX_SAMPLE_TOPICS, TAXONOMY_MAX_TOP_ENTITIES, type TemporalQueryFilters, type TenantScopeOptions, type Timestamp, type Topology, type TopologyExtractionProvider, type TopologyServiceVariant, type UsageHygieneSnapshot, VectorProvider, type VectorStatus, type WikiLintReport, assertGraphExtractionPayload, createPyxMemory, isSessionOrdinal, mergeExtractedEntities, normalizeGraphLabel, normalizeMemoryActorLabel, normalizeNameKey, projectSearchResponseForMcp, secretElevationAggregate, secretElevationNoticeFor, withSecretElevationNotice };
package/dist/index.mjs CHANGED
@@ -5,6 +5,8 @@ import {
5
5
  } from "./chunk-M67SXB7C.mjs";
6
6
  import {
7
7
  DEFAULTS,
8
+ MCP_ACTOR_META_KEY,
9
+ MEMORY_ACTOR_LABEL_MAX_CHARS,
8
10
  MEMORY_PROJECT_LABEL_MAX_CHARS,
9
11
  MoveFailureReason,
10
12
  NamespaceIsolation,
@@ -16,11 +18,12 @@ import {
16
18
  documentContentSource,
17
19
  documentGraphSource,
18
20
  documentImageSource,
21
+ normalizeMemoryActorLabel,
19
22
  projectSearchResponseForMcp,
20
23
  secretElevationAggregate,
21
24
  secretElevationNoticeFor,
22
25
  withSecretElevationNotice
23
- } from "./chunk-KZKGRND2.mjs";
26
+ } from "./chunk-RWQTBXUC.mjs";
24
27
  import {
25
28
  DEPRECATED_RAG_STRATEGIES,
26
29
  EmbeddingProviderName,
@@ -57,6 +60,8 @@ export {
57
60
  DEPRECATED_RAG_STRATEGIES,
58
61
  DisabledMemory,
59
62
  EmbeddingProviderName,
63
+ MCP_ACTOR_META_KEY,
64
+ MEMORY_ACTOR_LABEL_MAX_CHARS,
60
65
  MEMORY_PROJECT_LABEL_MAX_CHARS,
61
66
  MemoryClient,
62
67
  MemoryServerError,
@@ -82,6 +87,7 @@ export {
82
87
  isSessionOrdinal,
83
88
  mergeExtractedEntities,
84
89
  normalizeGraphLabel,
90
+ normalizeMemoryActorLabel,
85
91
  normalizeNameKey,
86
92
  parseListCursorToken,
87
93
  projectSearchResponseForMcp,
@@ -0,0 +1,139 @@
1
+ import {
2
+ readSurface
3
+ } from "./chunk-UESIQ47D.mjs";
4
+ import "./chunk-THLUBOGD.mjs";
5
+ import "./chunk-7TATCSSN.mjs";
6
+ import {
7
+ MEMORY_PROJECT_LABEL_MAX_CHARS,
8
+ TAXONOMY_MAX_PROJECTS,
9
+ normalizeMemoryActorLabel
10
+ } from "./chunk-RWQTBXUC.mjs";
11
+ import "./chunk-HTQBKJTD.mjs";
12
+ import {
13
+ EXIT
14
+ } from "./chunk-4DXEQC6X.mjs";
15
+
16
+ // src/cli/commands/insights.ts
17
+ import { z } from "zod";
18
+ var INSIGHTS_SCHEMA_VERSION = 1;
19
+ var metricsSchema = z.object({
20
+ activeEntries: z.number().int().nonnegative(),
21
+ retainedAdditions7d: z.number().int().nonnegative(),
22
+ appliedRecalls: z.number().int().nonnegative(),
23
+ reinforcedEntries: z.number().int().nonnegative(),
24
+ retrievedEntries: z.number().int().nonnegative(),
25
+ retrievals: z.number().int().nonnegative(),
26
+ lastAppliedAt: z.string().nullable(),
27
+ lastRetrievedAt: z.string().nullable()
28
+ });
29
+ var insightsSchema = z.object({
30
+ generatedAt: z.string(),
31
+ scope: z.literal("connected_memory"),
32
+ coverage: z.object({
33
+ retainedAdditions: z.object({
34
+ state: z.enum(["measured", "partial"]),
35
+ trackingSince: z.string().nullable(),
36
+ trackedEntries: z.number().int().nonnegative(),
37
+ totalEntries: z.number().int().nonnegative(),
38
+ reason: z.string().nullable()
39
+ }),
40
+ projectLabels: z.object({ state: z.literal("measured") }),
41
+ retrievalUsage: z.object({ state: z.literal("partial"), reason: z.string() }),
42
+ tokenEfficiency: z.object({ state: z.literal("not_instrumented"), reason: z.string() }),
43
+ contextAccuracy: z.object({ state: z.literal("not_instrumented"), reason: z.string() })
44
+ }),
45
+ summary: z.object({
46
+ activeEntries: z.number().int().nonnegative(),
47
+ observedProjectLabels: z.number().int().nonnegative(),
48
+ retainedAdditions7d: z.number().int().nonnegative().nullable(),
49
+ trackedEntries: z.number().int().nonnegative(),
50
+ appliedRecalls: z.number().int().nonnegative(),
51
+ reinforcedEntries: z.number().int().nonnegative(),
52
+ retrievedEntries: z.number().int().nonnegative(),
53
+ retrievals: z.number().int().nonnegative()
54
+ }),
55
+ projects: z.array(
56
+ metricsSchema.extend({
57
+ label: z.string().min(1).max(MEMORY_PROJECT_LABEL_MAX_CHARS)
58
+ })
59
+ ).max(TAXONOMY_MAX_PROJECTS),
60
+ other: metricsSchema.nullable(),
61
+ recentApplied: z.array(
62
+ z.object({
63
+ topic: z.string().min(1).max(MEMORY_PROJECT_LABEL_MAX_CHARS).nullable(),
64
+ project: z.string().min(1).max(MEMORY_PROJECT_LABEL_MAX_CHARS).nullable(),
65
+ at: z.string()
66
+ })
67
+ ).max(5)
68
+ });
69
+ var actorSchema = z.string().refine((value) => normalizeMemoryActorLabel(value) === value).nullable().default(null);
70
+ var insightsWithProvenanceSchema = insightsSchema.extend({
71
+ recentApplied: z.array(
72
+ insightsSchema.shape.recentApplied.element.extend({
73
+ writtenBy: actorSchema,
74
+ writtenAt: z.iso.datetime({ offset: true }).nullable().default(null),
75
+ appliedBy: actorSchema
76
+ })
77
+ ).max(5)
78
+ });
79
+ async function insightsCommand(opts = {}) {
80
+ const schema = opts.withProvenance === true ? insightsWithProvenanceSchema : insightsSchema;
81
+ const result = await readSurface({
82
+ surface: "insights",
83
+ request: { method: "GET", path: "/api/memory/insights" },
84
+ schema: z.union([
85
+ schema,
86
+ z.object({ success: z.literal(true), data: schema }).transform(({ data }) => data)
87
+ ]),
88
+ minimumServiceVersion: "1.19.0",
89
+ keychain: opts.keychain,
90
+ fetchImpl: opts.fetchImpl
91
+ });
92
+ const withIdentity = opts.withIdentity === true;
93
+ if (!result.ok) {
94
+ emit(
95
+ report(result.state, result.endpoint, result.keySource, result.problem, {
96
+ withIdentity,
97
+ credentialFingerprint: result.credentialFingerprint
98
+ })
99
+ );
100
+ return result.exit;
101
+ }
102
+ emit({
103
+ schemaVersion: INSIGHTS_SCHEMA_VERSION,
104
+ state: "ready",
105
+ endpoint: result.endpoint,
106
+ keySource: "keychain",
107
+ ...identityField({ withIdentity, credentialFingerprint: result.credentialFingerprint }),
108
+ ...result.data,
109
+ problem: null
110
+ });
111
+ return EXIT.OK;
112
+ }
113
+ function identityField(identity) {
114
+ return identity.withIdentity ? { credentialFingerprint: identity.credentialFingerprint } : {};
115
+ }
116
+ function report(state, endpoint, keySource, problem, identity) {
117
+ return {
118
+ schemaVersion: INSIGHTS_SCHEMA_VERSION,
119
+ state,
120
+ endpoint,
121
+ keySource,
122
+ ...identityField(identity),
123
+ generatedAt: null,
124
+ scope: "connected_memory",
125
+ coverage: null,
126
+ summary: null,
127
+ projects: [],
128
+ other: null,
129
+ recentApplied: [],
130
+ problem
131
+ };
132
+ }
133
+ function emit(value) {
134
+ process.stdout.write(`${JSON.stringify(value)}
135
+ `);
136
+ }
137
+ export {
138
+ insightsCommand
139
+ };
@@ -0,0 +1,179 @@
1
+ import {
2
+ probeCredentials
3
+ } from "./chunk-S3CX323Z.mjs";
4
+ import {
5
+ BUSY,
6
+ KeychainError,
7
+ getLifecycleKeychain,
8
+ supersedeLogout,
9
+ withLifecycleLock
10
+ } from "./chunk-7TATCSSN.mjs";
11
+ import {
12
+ EXIT
13
+ } from "./chunk-4DXEQC6X.mjs";
14
+
15
+ // src/cli/prompt.ts
16
+ async function readApiKeySecret() {
17
+ if (process.stdin.isTTY) {
18
+ return promptMasked("API key: ");
19
+ }
20
+ return readSingleLineFromStdin();
21
+ }
22
+ async function promptMasked(label) {
23
+ const { stdin, stdout } = process;
24
+ stdout.write(label);
25
+ const hadRawMode = typeof stdin.setRawMode === "function";
26
+ if (!hadRawMode) {
27
+ const value = await readSingleLineFromStdin();
28
+ stdout.write(`${"\u2022".repeat(Math.min(value.length, 80))}
29
+ `);
30
+ return value;
31
+ }
32
+ return new Promise((resolve, reject) => {
33
+ const previousEncoding = stdin.readableEncoding;
34
+ stdin.setEncoding("utf8");
35
+ stdin.setRawMode(true);
36
+ stdin.resume();
37
+ let buf = "";
38
+ const cleanup = () => {
39
+ stdin.removeListener("data", onData);
40
+ stdin.removeListener("error", onError);
41
+ stdin.setRawMode(false);
42
+ stdin.pause();
43
+ if (previousEncoding) stdin.setEncoding(previousEncoding);
44
+ };
45
+ const onData = (chunk) => {
46
+ for (const ch of chunk) {
47
+ if (ch === "\n" || ch === "\r") {
48
+ cleanup();
49
+ stdout.write("\n");
50
+ resolve(buf.trim());
51
+ return;
52
+ }
53
+ if (ch === "") {
54
+ cleanup();
55
+ stdout.write("\n");
56
+ process.exit(130);
57
+ }
58
+ if (ch === "\x7F" || ch === "\b") {
59
+ if (buf.length > 0) {
60
+ buf = buf.slice(0, -1);
61
+ stdout.write("\b \b");
62
+ }
63
+ continue;
64
+ }
65
+ if (ch < " ") {
66
+ continue;
67
+ }
68
+ buf += ch;
69
+ stdout.write("\u2022");
70
+ }
71
+ };
72
+ const onError = (err) => {
73
+ cleanup();
74
+ reject(err instanceof Error ? err : new Error(String(err)));
75
+ };
76
+ stdin.on("data", onData);
77
+ stdin.on("error", onError);
78
+ });
79
+ }
80
+ async function readSingleLineFromStdin() {
81
+ let buf = "";
82
+ for await (const chunk of process.stdin) {
83
+ buf += chunk.toString("utf8");
84
+ const nl = buf.indexOf("\n");
85
+ if (nl >= 0) return buf.slice(0, nl).trim();
86
+ }
87
+ return buf.trim();
88
+ }
89
+
90
+ // src/cli/commands/login.ts
91
+ var DEFAULT_ENDPOINT = "https://memory.api.pyxmate.com";
92
+ async function loginCommand(opts = {}) {
93
+ const provider = opts.keychain ?? getLifecycleKeychain();
94
+ const fetcher = opts.fetchImpl ?? fetch;
95
+ const endpoint = normalizeEndpoint(
96
+ opts.endpoint ?? await readExistingEndpoint(provider) ?? DEFAULT_ENDPOINT
97
+ );
98
+ if (!isHttpUrl(endpoint)) {
99
+ process.stderr.write(`Error: invalid --endpoint \`${endpoint}\` (expected http(s) URL).
100
+ `);
101
+ return EXIT.USAGE;
102
+ }
103
+ let apiKey;
104
+ try {
105
+ apiKey = opts.apiKey?.trim() || await readApiKeySecret();
106
+ } catch (err) {
107
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
108
+ `);
109
+ return EXIT.USAGE;
110
+ }
111
+ if (!apiKey) {
112
+ process.stderr.write("Error: API key is empty.\n");
113
+ return EXIT.USAGE;
114
+ }
115
+ const probe = await probeCredentials({ endpoint, apiKey, fetcher });
116
+ if (probe.kind === "invalid") {
117
+ process.stderr.write(
118
+ `Error: API key was rejected by ${endpoint} (HTTP ${probe.status}).
119
+ No credentials were written.
120
+ `
121
+ );
122
+ return EXIT.NOT_LOGGED_IN;
123
+ }
124
+ try {
125
+ const written = await withLifecycleLock(provider, async () => {
126
+ await supersedeLogout(provider, endpoint, apiKey);
127
+ await provider.write({ schemaVersion: 1, endpoint, apiKey });
128
+ });
129
+ if (written === BUSY) {
130
+ process.stderr.write(
131
+ "Error: another pyx-mem sign-in or sign-out is in progress. Retry in a moment.\nNo credentials were written.\n"
132
+ );
133
+ return EXIT.BUSY;
134
+ }
135
+ } catch (err) {
136
+ if (err instanceof KeychainError) {
137
+ process.stderr.write(
138
+ `Error: ${err.message}
139
+ ${err.guidance}
140
+ No credentials were written. pyx-mem does not write plaintext token files.
141
+ `
142
+ );
143
+ return err.exit;
144
+ }
145
+ throw err;
146
+ }
147
+ if (probe.kind === "unreachable") {
148
+ process.stdout.write(
149
+ `Stored credentials for ${endpoint}. Warning: validation probe failed (${probe.reason}). Run: pyx-mem doctor
150
+ `
151
+ );
152
+ } else {
153
+ process.stdout.write(`Logged in to ${endpoint}. Run: pyx-mem doctor
154
+ `);
155
+ }
156
+ return EXIT.OK;
157
+ }
158
+ async function readExistingEndpoint(provider) {
159
+ try {
160
+ const creds = await provider.read();
161
+ return creds?.endpoint ?? null;
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+ function normalizeEndpoint(input) {
167
+ return input.replace(/\/+$/, "");
168
+ }
169
+ function isHttpUrl(value) {
170
+ try {
171
+ const u = new URL(value);
172
+ return u.protocol === "http:" || u.protocol === "https:";
173
+ } catch {
174
+ return false;
175
+ }
176
+ }
177
+ export {
178
+ loginCommand
179
+ };