@pyxmate/memory 1.17.19 → 1.18.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.
- package/dist/{chunk-T35ZOOES.mjs → chunk-FIRMGTFX.mjs} +1 -1
- package/dist/chunk-KVYCISUI.mjs +206 -0
- package/dist/{chunk-H6ZLMPZH.mjs → chunk-LWFCE4OS.mjs} +54 -202
- package/dist/{chunk-3OLH3HYR.mjs → chunk-VORP6NHJ.mjs} +7 -0
- package/dist/cli/pyx-mem.mjs +527 -264
- package/dist/dashboard.mjs +4 -3
- package/dist/{data-plane-contract-fDvTm9bF.d.ts → data-plane-contract-CmosA5XV.d.ts} +6 -0
- package/dist/data-plane-contract.d.ts +1 -1
- package/dist/data-plane-contract.mjs +1 -1
- package/dist/index.d.ts +64 -3
- package/dist/index.mjs +15 -5
- package/dist/react.mjs +4 -3
- package/package.json +1 -1
package/dist/cli/pyx-mem.mjs
CHANGED
|
@@ -3,6 +3,14 @@ import {
|
|
|
3
3
|
PERSISTENT_MEMORY_SECTION,
|
|
4
4
|
buildDesignGuide
|
|
5
5
|
} from "../chunk-EZERG25I.mjs";
|
|
6
|
+
import {
|
|
7
|
+
MEMORY_PROJECT_LABEL_MAX_CHARS,
|
|
8
|
+
TAXONOMY_MAX_CATEGORIES,
|
|
9
|
+
TAXONOMY_MAX_PROJECTS,
|
|
10
|
+
TAXONOMY_MAX_SAMPLE_TOPICS,
|
|
11
|
+
TAXONOMY_MAX_TOP_ENTITIES
|
|
12
|
+
} from "../chunk-KVYCISUI.mjs";
|
|
13
|
+
import "../chunk-VORP6NHJ.mjs";
|
|
6
14
|
|
|
7
15
|
// src/cli/exit-codes.ts
|
|
8
16
|
var EXIT = {
|
|
@@ -328,6 +336,396 @@ async function doctorCommand(opts = {}) {
|
|
|
328
336
|
return ok ? EXIT.OK : EXIT.DOCTOR_FAIL;
|
|
329
337
|
}
|
|
330
338
|
|
|
339
|
+
// src/cli/commands/insights.ts
|
|
340
|
+
import { z } from "zod";
|
|
341
|
+
|
|
342
|
+
// src/cli/commands/read-surface.ts
|
|
343
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
344
|
+
|
|
345
|
+
// src/mcp/errors.ts
|
|
346
|
+
var RUN_LOGIN = "Run: pyx-mem login";
|
|
347
|
+
var RUN_DOCTOR = "Run: pyx-mem doctor";
|
|
348
|
+
function mcpText(text, isError = false) {
|
|
349
|
+
return { isError, content: [{ type: "text", text }] };
|
|
350
|
+
}
|
|
351
|
+
function mcpMissingCredentials() {
|
|
352
|
+
return mcpText(`pyx-memory credentials are not configured.
|
|
353
|
+
${RUN_LOGIN}`, true);
|
|
354
|
+
}
|
|
355
|
+
function mcpKeychainError(message, guidance) {
|
|
356
|
+
const lines = ["pyx-memory credentials could not be read from the OS credential store."];
|
|
357
|
+
if (message) lines.push(message);
|
|
358
|
+
if (guidance) lines.push(guidance);
|
|
359
|
+
lines.push(RUN_LOGIN);
|
|
360
|
+
return mcpText(lines.join("\n"), true);
|
|
361
|
+
}
|
|
362
|
+
function mcpAuthFailed(status) {
|
|
363
|
+
return mcpText(`Authentication failed (HTTP ${status}).
|
|
364
|
+
${RUN_LOGIN}`, true);
|
|
365
|
+
}
|
|
366
|
+
function mcpUnreachable(endpoint, reason) {
|
|
367
|
+
return mcpText(`Unable to reach pyx-memory at ${endpoint}: ${reason}.
|
|
368
|
+
${RUN_DOCTOR}`, true);
|
|
369
|
+
}
|
|
370
|
+
function mcpHttpError(status, body) {
|
|
371
|
+
return mcpText(`${status}: ${body}`, true);
|
|
372
|
+
}
|
|
373
|
+
function mcpServerError(status, body) {
|
|
374
|
+
return mcpText(`pyx-memory server error (HTTP ${status}): ${body || "no body"}`, true);
|
|
375
|
+
}
|
|
376
|
+
function mcpEnvelopeError(message) {
|
|
377
|
+
return mcpText(message, true);
|
|
378
|
+
}
|
|
379
|
+
function mcpJson(payload) {
|
|
380
|
+
return mcpText(JSON.stringify(payload));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/mcp/http-client.ts
|
|
384
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
385
|
+
function validateIdempotencyKey(key) {
|
|
386
|
+
if (key.length === 0 || !key.isWellFormed() || new TextEncoder().encode(key).byteLength > 256) {
|
|
387
|
+
throw new Error("idempotencyKey must be 1-256 well-formed UTF-8 bytes");
|
|
388
|
+
}
|
|
389
|
+
return key;
|
|
390
|
+
}
|
|
391
|
+
function createHttpClient(credentials, fetchImpl = fetch) {
|
|
392
|
+
const baseUrl = credentials.endpoint.replace(/\/+$/, "");
|
|
393
|
+
function buildUrl(path, query) {
|
|
394
|
+
const url = new URL(`${baseUrl}${path}`);
|
|
395
|
+
if (query) {
|
|
396
|
+
for (const [k, v] of Object.entries(query)) {
|
|
397
|
+
if (v === void 0) continue;
|
|
398
|
+
url.searchParams.set(k, String(v));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return url.toString();
|
|
402
|
+
}
|
|
403
|
+
function buildHeaders(scope, contentType, idempotencyKey) {
|
|
404
|
+
const headers = new Headers();
|
|
405
|
+
headers.set("Authorization", `Bearer ${credentials.apiKey}`);
|
|
406
|
+
headers.set("Accept", "application/json");
|
|
407
|
+
if (contentType) headers.set("Content-Type", contentType);
|
|
408
|
+
if (scope?.tenantId) headers.set("X-Tenant-Id", scope.tenantId);
|
|
409
|
+
if (scope?.namespaceId) headers.set("X-Namespace-Id", scope.namespaceId);
|
|
410
|
+
if (scope?.userId) headers.set("X-User-Id", scope.userId);
|
|
411
|
+
if (scope?.teamId) headers.set("X-Team-Id", scope.teamId);
|
|
412
|
+
if (scope?.callerAccessLevel) headers.set("X-Caller-Access-Level", scope.callerAccessLevel);
|
|
413
|
+
if (idempotencyKey !== void 0) {
|
|
414
|
+
headers.set("Idempotency-Key", validateIdempotencyKey(idempotencyKey));
|
|
415
|
+
}
|
|
416
|
+
return headers;
|
|
417
|
+
}
|
|
418
|
+
async function send(url, method, headers, body) {
|
|
419
|
+
const ctl = new AbortController();
|
|
420
|
+
const timer = setTimeout(() => ctl.abort(), DEFAULT_TIMEOUT_MS);
|
|
421
|
+
let res;
|
|
422
|
+
try {
|
|
423
|
+
res = await fetchImpl(url, { method, headers, body, signal: ctl.signal });
|
|
424
|
+
} catch (err) {
|
|
425
|
+
return {
|
|
426
|
+
ok: false,
|
|
427
|
+
result: mcpUnreachable(baseUrl, err instanceof Error ? err.message : String(err))
|
|
428
|
+
};
|
|
429
|
+
} finally {
|
|
430
|
+
clearTimeout(timer);
|
|
431
|
+
}
|
|
432
|
+
if (res.status === 401 || res.status === 403) {
|
|
433
|
+
return { ok: false, status: res.status, result: mcpAuthFailed(res.status) };
|
|
434
|
+
}
|
|
435
|
+
const text = await res.text().catch(() => "");
|
|
436
|
+
return parseHttpResponse(res, text);
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
async requestJson(input) {
|
|
440
|
+
const url = buildUrl(input.path, input.query);
|
|
441
|
+
const hasBody = input.body !== void 0;
|
|
442
|
+
const headers = buildHeaders(
|
|
443
|
+
input.scope,
|
|
444
|
+
hasBody ? "application/json" : void 0,
|
|
445
|
+
input.idempotencyKey
|
|
446
|
+
);
|
|
447
|
+
return send(url, input.method, headers, hasBody ? JSON.stringify(input.body) : void 0);
|
|
448
|
+
},
|
|
449
|
+
async requestMultipart(input) {
|
|
450
|
+
const url = buildUrl(input.path);
|
|
451
|
+
const headers = buildHeaders(input.scope, void 0, input.idempotencyKey);
|
|
452
|
+
const form = await input.formData();
|
|
453
|
+
return send(url, "POST", headers, form);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function parseHttpResponse(res, text) {
|
|
458
|
+
if (res.status >= 500) {
|
|
459
|
+
return { ok: false, status: res.status, result: mcpServerError(res.status, text) };
|
|
460
|
+
}
|
|
461
|
+
const parsed = parseJsonText(text);
|
|
462
|
+
if (!parsed.ok) {
|
|
463
|
+
if (!res.ok) {
|
|
464
|
+
return {
|
|
465
|
+
ok: false,
|
|
466
|
+
status: res.status,
|
|
467
|
+
result: mcpHttpError(res.status, text || "no body")
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
return { ok: true, status: res.status, data: text };
|
|
471
|
+
}
|
|
472
|
+
if (!res.ok) {
|
|
473
|
+
const message = extractEnvelopeError(parsed.value) ?? text ?? `HTTP ${res.status}`;
|
|
474
|
+
return { ok: false, status: res.status, result: mcpHttpError(res.status, message) };
|
|
475
|
+
}
|
|
476
|
+
const envelopeError = extractEnvelopeError(parsed.value);
|
|
477
|
+
if (envelopeError) {
|
|
478
|
+
return { ok: false, status: res.status, result: mcpEnvelopeError(envelopeError) };
|
|
479
|
+
}
|
|
480
|
+
return { ok: true, status: res.status, data: parsed.value };
|
|
481
|
+
}
|
|
482
|
+
function parseJsonText(text) {
|
|
483
|
+
if (text.length === 0) return { ok: true, value: void 0 };
|
|
484
|
+
try {
|
|
485
|
+
return { ok: true, value: JSON.parse(text) };
|
|
486
|
+
} catch {
|
|
487
|
+
return { ok: false };
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function extractEnvelopeError(parsed) {
|
|
491
|
+
if (parsed === null || typeof parsed !== "object") return null;
|
|
492
|
+
const obj = parsed;
|
|
493
|
+
if (obj.success === false) {
|
|
494
|
+
if (typeof obj.error === "string") return obj.error;
|
|
495
|
+
if (typeof obj.message === "string") return obj.message;
|
|
496
|
+
return "pyx-memory returned success: false";
|
|
497
|
+
}
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/cli/commands/read-surface.ts
|
|
502
|
+
async function readSurface(opts) {
|
|
503
|
+
const provider = opts.keychain ?? getDefaultKeychain();
|
|
504
|
+
let credentials;
|
|
505
|
+
try {
|
|
506
|
+
credentials = await provider.read();
|
|
507
|
+
} catch (error) {
|
|
508
|
+
return {
|
|
509
|
+
ok: false,
|
|
510
|
+
exit: error instanceof KeychainError ? error.exit : EXIT.CRED_UNAVAILABLE,
|
|
511
|
+
state: "error",
|
|
512
|
+
endpoint: null,
|
|
513
|
+
keySource: null,
|
|
514
|
+
problem: error instanceof KeychainError ? [error.message, error.guidance].filter(Boolean).join(" ") : "The OS credential store could not be read."
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
if (!credentials) {
|
|
518
|
+
return {
|
|
519
|
+
ok: false,
|
|
520
|
+
exit: EXIT.NOT_LOGGED_IN,
|
|
521
|
+
state: "signed_out",
|
|
522
|
+
endpoint: null,
|
|
523
|
+
keySource: null,
|
|
524
|
+
problem: null
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
if (!isHttpUrl(credentials.endpoint)) {
|
|
528
|
+
return {
|
|
529
|
+
ok: false,
|
|
530
|
+
exit: EXIT.DOCTOR_FAIL,
|
|
531
|
+
state: "error",
|
|
532
|
+
endpoint: null,
|
|
533
|
+
keySource: "keychain",
|
|
534
|
+
problem: "The stored pyx-memory endpoint is invalid. Run: pyx-mem login"
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
const http = createHttpClient(credentials, opts.fetchImpl ?? fetch);
|
|
538
|
+
const response = await http.requestJson({
|
|
539
|
+
...opts.request,
|
|
540
|
+
idempotencyKey: `cli-${opts.surface}-${randomUUID2()}`
|
|
541
|
+
});
|
|
542
|
+
if (!response.ok) {
|
|
543
|
+
return failedSurfaceRequest(
|
|
544
|
+
opts.surface,
|
|
545
|
+
response.status,
|
|
546
|
+
credentials.endpoint,
|
|
547
|
+
opts.minimumServiceVersion,
|
|
548
|
+
http
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
const parsed = opts.schema.safeParse(response.data);
|
|
552
|
+
if (!parsed.success) {
|
|
553
|
+
return {
|
|
554
|
+
ok: false,
|
|
555
|
+
exit: EXIT.DOCTOR_FAIL,
|
|
556
|
+
state: "error",
|
|
557
|
+
endpoint: credentials.endpoint,
|
|
558
|
+
keySource: "keychain",
|
|
559
|
+
problem: `pyx-memory returned an invalid ${opts.surface} response. Run: pyx-mem doctor`
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
return { ok: true, endpoint: credentials.endpoint, data: parsed.data };
|
|
563
|
+
}
|
|
564
|
+
async function failedSurfaceRequest(surface, status, endpoint, minimumServiceVersion, http) {
|
|
565
|
+
if (status === 401) {
|
|
566
|
+
return {
|
|
567
|
+
ok: false,
|
|
568
|
+
exit: EXIT.NOT_LOGGED_IN,
|
|
569
|
+
state: "signed_out",
|
|
570
|
+
endpoint,
|
|
571
|
+
keySource: "keychain",
|
|
572
|
+
problem: requestFailure(surface, status)
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
const serviceVersion = minimumServiceVersion ? await probeServiceVersion(http) : null;
|
|
576
|
+
const serviceOutdated = serviceVersion !== null && minimumServiceVersion !== void 0 && isVersionOlder(serviceVersion, minimumServiceVersion);
|
|
577
|
+
return {
|
|
578
|
+
ok: false,
|
|
579
|
+
exit: EXIT.DOCTOR_FAIL,
|
|
580
|
+
state: serviceOutdated ? "service_outdated" : "error",
|
|
581
|
+
endpoint,
|
|
582
|
+
keySource: "keychain",
|
|
583
|
+
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
|
+
};
|
|
585
|
+
}
|
|
586
|
+
async function probeServiceVersion(http) {
|
|
587
|
+
const response = await http.requestJson({ method: "GET", path: "/status" });
|
|
588
|
+
if (!response.ok) return null;
|
|
589
|
+
const payload = plainObject(response.data);
|
|
590
|
+
const topology = plainObject(payload?.success === true ? plainObject(payload.data) : payload);
|
|
591
|
+
const version = payload?.version ?? plainObject(topology?.service)?.version;
|
|
592
|
+
return typeof version === "string" && parseVersion(version) ? version : null;
|
|
593
|
+
}
|
|
594
|
+
function plainObject(value) {
|
|
595
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
596
|
+
}
|
|
597
|
+
function isVersionOlder(actual, minimum) {
|
|
598
|
+
const left = parseVersion(actual);
|
|
599
|
+
const right = parseVersion(minimum);
|
|
600
|
+
if (!left || !right) return false;
|
|
601
|
+
if (left.core[0] !== right.core[0]) return left.core[0] < right.core[0];
|
|
602
|
+
if (left.core[1] !== right.core[1]) return left.core[1] < right.core[1];
|
|
603
|
+
if (left.core[2] !== right.core[2]) return left.core[2] < right.core[2];
|
|
604
|
+
return left.prerelease !== null && right.prerelease === null;
|
|
605
|
+
}
|
|
606
|
+
function parseVersion(value) {
|
|
607
|
+
if (value.length > 64) return null;
|
|
608
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
|
|
609
|
+
if (!match) return null;
|
|
610
|
+
const core = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
611
|
+
return core.every(Number.isSafeInteger) ? { core, prerelease: match[4] ?? null } : null;
|
|
612
|
+
}
|
|
613
|
+
function requestFailure(surface, status) {
|
|
614
|
+
if (status === 401) {
|
|
615
|
+
return "Stored pyx-memory credentials were rejected. Run: pyx-mem login";
|
|
616
|
+
}
|
|
617
|
+
if (status === 403) {
|
|
618
|
+
return "pyx-memory denied access (HTTP 403). Check the key scope and project access.";
|
|
619
|
+
}
|
|
620
|
+
if (status === void 0) {
|
|
621
|
+
return "Unable to reach pyx-memory. Run: pyx-mem doctor";
|
|
622
|
+
}
|
|
623
|
+
if (status >= 500) {
|
|
624
|
+
return `pyx-memory is unavailable (HTTP ${status}). Run: pyx-mem doctor`;
|
|
625
|
+
}
|
|
626
|
+
return `pyx-memory ${surface} failed (HTTP ${status}). Run: pyx-mem doctor`;
|
|
627
|
+
}
|
|
628
|
+
function isHttpUrl(value) {
|
|
629
|
+
try {
|
|
630
|
+
const url = new URL(value);
|
|
631
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
632
|
+
} catch {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/cli/commands/insights.ts
|
|
638
|
+
var INSIGHTS_SCHEMA_VERSION = 1;
|
|
639
|
+
var metricsSchema = z.object({
|
|
640
|
+
activeEntries: z.number().int().nonnegative(),
|
|
641
|
+
retainedAdditions7d: z.number().int().nonnegative(),
|
|
642
|
+
appliedRecalls: z.number().int().nonnegative(),
|
|
643
|
+
reinforcedEntries: z.number().int().nonnegative(),
|
|
644
|
+
retrievedEntries: z.number().int().nonnegative(),
|
|
645
|
+
retrievals: z.number().int().nonnegative(),
|
|
646
|
+
lastAppliedAt: z.string().nullable(),
|
|
647
|
+
lastRetrievedAt: z.string().nullable()
|
|
648
|
+
});
|
|
649
|
+
var insightsSchema = z.object({
|
|
650
|
+
generatedAt: z.string(),
|
|
651
|
+
scope: z.literal("connected_memory"),
|
|
652
|
+
coverage: z.object({
|
|
653
|
+
retainedAdditions: z.object({
|
|
654
|
+
state: z.enum(["measured", "partial"]),
|
|
655
|
+
trackingSince: z.string().nullable(),
|
|
656
|
+
trackedEntries: z.number().int().nonnegative(),
|
|
657
|
+
totalEntries: z.number().int().nonnegative(),
|
|
658
|
+
reason: z.string().nullable()
|
|
659
|
+
}),
|
|
660
|
+
projectLabels: z.object({ state: z.literal("measured") }),
|
|
661
|
+
retrievalUsage: z.object({ state: z.literal("partial"), reason: z.string() }),
|
|
662
|
+
tokenEfficiency: z.object({ state: z.literal("not_instrumented"), reason: z.string() }),
|
|
663
|
+
contextAccuracy: z.object({ state: z.literal("not_instrumented"), reason: z.string() })
|
|
664
|
+
}),
|
|
665
|
+
summary: z.object({
|
|
666
|
+
activeEntries: z.number().int().nonnegative(),
|
|
667
|
+
observedProjectLabels: z.number().int().nonnegative(),
|
|
668
|
+
retainedAdditions7d: z.number().int().nonnegative().nullable(),
|
|
669
|
+
trackedEntries: z.number().int().nonnegative(),
|
|
670
|
+
appliedRecalls: z.number().int().nonnegative(),
|
|
671
|
+
reinforcedEntries: z.number().int().nonnegative(),
|
|
672
|
+
retrievedEntries: z.number().int().nonnegative(),
|
|
673
|
+
retrievals: z.number().int().nonnegative()
|
|
674
|
+
}),
|
|
675
|
+
projects: z.array(
|
|
676
|
+
metricsSchema.extend({
|
|
677
|
+
label: z.string().min(1).max(MEMORY_PROJECT_LABEL_MAX_CHARS)
|
|
678
|
+
})
|
|
679
|
+
).max(TAXONOMY_MAX_PROJECTS),
|
|
680
|
+
other: metricsSchema.nullable()
|
|
681
|
+
});
|
|
682
|
+
var insightsResponseSchema = z.union([
|
|
683
|
+
insightsSchema,
|
|
684
|
+
z.object({ success: z.literal(true), data: insightsSchema }).transform(({ data }) => data)
|
|
685
|
+
]);
|
|
686
|
+
async function insightsCommand(opts = {}) {
|
|
687
|
+
const result = await readSurface({
|
|
688
|
+
surface: "insights",
|
|
689
|
+
request: { method: "GET", path: "/api/memory/insights" },
|
|
690
|
+
schema: insightsResponseSchema,
|
|
691
|
+
minimumServiceVersion: "1.18.1",
|
|
692
|
+
keychain: opts.keychain,
|
|
693
|
+
fetchImpl: opts.fetchImpl
|
|
694
|
+
});
|
|
695
|
+
if (!result.ok) {
|
|
696
|
+
emit(report(result.state, result.endpoint, result.keySource, result.problem));
|
|
697
|
+
return result.exit;
|
|
698
|
+
}
|
|
699
|
+
emit({
|
|
700
|
+
schemaVersion: INSIGHTS_SCHEMA_VERSION,
|
|
701
|
+
state: "ready",
|
|
702
|
+
endpoint: result.endpoint,
|
|
703
|
+
keySource: "keychain",
|
|
704
|
+
...result.data,
|
|
705
|
+
problem: null
|
|
706
|
+
});
|
|
707
|
+
return EXIT.OK;
|
|
708
|
+
}
|
|
709
|
+
function report(state, endpoint, keySource, problem) {
|
|
710
|
+
return {
|
|
711
|
+
schemaVersion: INSIGHTS_SCHEMA_VERSION,
|
|
712
|
+
state,
|
|
713
|
+
endpoint,
|
|
714
|
+
keySource,
|
|
715
|
+
generatedAt: null,
|
|
716
|
+
scope: "connected_memory",
|
|
717
|
+
coverage: null,
|
|
718
|
+
summary: null,
|
|
719
|
+
projects: [],
|
|
720
|
+
other: null,
|
|
721
|
+
problem
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function emit(value) {
|
|
725
|
+
process.stdout.write(`${JSON.stringify(value)}
|
|
726
|
+
`);
|
|
727
|
+
}
|
|
728
|
+
|
|
331
729
|
// src/cli/prompt.ts
|
|
332
730
|
async function readApiKeySecret() {
|
|
333
731
|
if (process.stdin.isTTY) {
|
|
@@ -411,7 +809,7 @@ async function loginCommand(opts = {}) {
|
|
|
411
809
|
const endpoint = normalizeEndpoint(
|
|
412
810
|
opts.endpoint ?? await readExistingEndpoint(provider) ?? DEFAULT_ENDPOINT
|
|
413
811
|
);
|
|
414
|
-
if (!
|
|
812
|
+
if (!isHttpUrl2(endpoint)) {
|
|
415
813
|
process.stderr.write(`Error: invalid --endpoint \`${endpoint}\` (expected http(s) URL).
|
|
416
814
|
`);
|
|
417
815
|
return EXIT.USAGE;
|
|
@@ -473,7 +871,7 @@ async function readExistingEndpoint(provider) {
|
|
|
473
871
|
function normalizeEndpoint(input) {
|
|
474
872
|
return input.replace(/\/+$/, "");
|
|
475
873
|
}
|
|
476
|
-
function
|
|
874
|
+
function isHttpUrl2(value) {
|
|
477
875
|
try {
|
|
478
876
|
const u = new URL(value);
|
|
479
877
|
return u.protocol === "http:" || u.protocol === "https:";
|
|
@@ -500,44 +898,6 @@ ${err.guidance}
|
|
|
500
898
|
}
|
|
501
899
|
}
|
|
502
900
|
|
|
503
|
-
// src/mcp/errors.ts
|
|
504
|
-
var RUN_LOGIN = "Run: pyx-mem login";
|
|
505
|
-
var RUN_DOCTOR = "Run: pyx-mem doctor";
|
|
506
|
-
function mcpText(text, isError = false) {
|
|
507
|
-
return { isError, content: [{ type: "text", text }] };
|
|
508
|
-
}
|
|
509
|
-
function mcpMissingCredentials() {
|
|
510
|
-
return mcpText(`pyx-memory credentials are not configured.
|
|
511
|
-
${RUN_LOGIN}`, true);
|
|
512
|
-
}
|
|
513
|
-
function mcpKeychainError(message, guidance) {
|
|
514
|
-
const lines = ["pyx-memory credentials could not be read from the OS credential store."];
|
|
515
|
-
if (message) lines.push(message);
|
|
516
|
-
if (guidance) lines.push(guidance);
|
|
517
|
-
lines.push(RUN_LOGIN);
|
|
518
|
-
return mcpText(lines.join("\n"), true);
|
|
519
|
-
}
|
|
520
|
-
function mcpAuthFailed(status) {
|
|
521
|
-
return mcpText(`Authentication failed (HTTP ${status}).
|
|
522
|
-
${RUN_LOGIN}`, true);
|
|
523
|
-
}
|
|
524
|
-
function mcpUnreachable(endpoint, reason) {
|
|
525
|
-
return mcpText(`Unable to reach pyx-memory at ${endpoint}: ${reason}.
|
|
526
|
-
${RUN_DOCTOR}`, true);
|
|
527
|
-
}
|
|
528
|
-
function mcpHttpError(status, body) {
|
|
529
|
-
return mcpText(`${status}: ${body}`, true);
|
|
530
|
-
}
|
|
531
|
-
function mcpServerError(status, body) {
|
|
532
|
-
return mcpText(`pyx-memory server error (HTTP ${status}): ${body || "no body"}`, true);
|
|
533
|
-
}
|
|
534
|
-
function mcpEnvelopeError(message) {
|
|
535
|
-
return mcpText(message, true);
|
|
536
|
-
}
|
|
537
|
-
function mcpJson(payload) {
|
|
538
|
-
return mcpText(JSON.stringify(payload));
|
|
539
|
-
}
|
|
540
|
-
|
|
541
901
|
// src/mcp/credentials.ts
|
|
542
902
|
function createReadCredentials(providerFactory) {
|
|
543
903
|
return async () => {
|
|
@@ -557,7 +917,7 @@ function createReadCredentials(providerFactory) {
|
|
|
557
917
|
}
|
|
558
918
|
|
|
559
919
|
// src/mcp/proxy-server.ts
|
|
560
|
-
import { createHash, randomUUID as
|
|
920
|
+
import { createHash, randomUUID as randomUUID3 } from "crypto";
|
|
561
921
|
import { readFile, stat } from "fs/promises";
|
|
562
922
|
import { basename, isAbsolute, resolve } from "path";
|
|
563
923
|
import { Client as McpClient } from "@modelcontextprotocol/sdk/client/index.js";
|
|
@@ -571,130 +931,12 @@ import {
|
|
|
571
931
|
ListToolsRequestSchema
|
|
572
932
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
573
933
|
|
|
574
|
-
// src/mcp/http-client.ts
|
|
575
|
-
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
576
|
-
function validateIdempotencyKey(key) {
|
|
577
|
-
if (key.length === 0 || !key.isWellFormed() || new TextEncoder().encode(key).byteLength > 256) {
|
|
578
|
-
throw new Error("idempotencyKey must be 1-256 well-formed UTF-8 bytes");
|
|
579
|
-
}
|
|
580
|
-
return key;
|
|
581
|
-
}
|
|
582
|
-
function createHttpClient(credentials, fetchImpl = fetch) {
|
|
583
|
-
const baseUrl = credentials.endpoint.replace(/\/+$/, "");
|
|
584
|
-
function buildUrl(path, query) {
|
|
585
|
-
const url = new URL(`${baseUrl}${path}`);
|
|
586
|
-
if (query) {
|
|
587
|
-
for (const [k, v] of Object.entries(query)) {
|
|
588
|
-
if (v === void 0) continue;
|
|
589
|
-
url.searchParams.set(k, String(v));
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
return url.toString();
|
|
593
|
-
}
|
|
594
|
-
function buildHeaders(scope, contentType, idempotencyKey) {
|
|
595
|
-
const headers = new Headers();
|
|
596
|
-
headers.set("Authorization", `Bearer ${credentials.apiKey}`);
|
|
597
|
-
headers.set("Accept", "application/json");
|
|
598
|
-
if (contentType) headers.set("Content-Type", contentType);
|
|
599
|
-
if (scope?.tenantId) headers.set("X-Tenant-Id", scope.tenantId);
|
|
600
|
-
if (scope?.namespaceId) headers.set("X-Namespace-Id", scope.namespaceId);
|
|
601
|
-
if (scope?.userId) headers.set("X-User-Id", scope.userId);
|
|
602
|
-
if (scope?.teamId) headers.set("X-Team-Id", scope.teamId);
|
|
603
|
-
if (scope?.callerAccessLevel) headers.set("X-Caller-Access-Level", scope.callerAccessLevel);
|
|
604
|
-
if (idempotencyKey !== void 0) {
|
|
605
|
-
headers.set("Idempotency-Key", validateIdempotencyKey(idempotencyKey));
|
|
606
|
-
}
|
|
607
|
-
return headers;
|
|
608
|
-
}
|
|
609
|
-
async function send(url, method, headers, body) {
|
|
610
|
-
const ctl = new AbortController();
|
|
611
|
-
const timer = setTimeout(() => ctl.abort(), DEFAULT_TIMEOUT_MS);
|
|
612
|
-
let res;
|
|
613
|
-
try {
|
|
614
|
-
res = await fetchImpl(url, { method, headers, body, signal: ctl.signal });
|
|
615
|
-
} catch (err) {
|
|
616
|
-
return {
|
|
617
|
-
ok: false,
|
|
618
|
-
result: mcpUnreachable(baseUrl, err instanceof Error ? err.message : String(err))
|
|
619
|
-
};
|
|
620
|
-
} finally {
|
|
621
|
-
clearTimeout(timer);
|
|
622
|
-
}
|
|
623
|
-
if (res.status === 401 || res.status === 403) {
|
|
624
|
-
return { ok: false, status: res.status, result: mcpAuthFailed(res.status) };
|
|
625
|
-
}
|
|
626
|
-
const text = await res.text().catch(() => "");
|
|
627
|
-
return parseHttpResponse(res, text);
|
|
628
|
-
}
|
|
629
|
-
return {
|
|
630
|
-
async requestJson(input) {
|
|
631
|
-
const url = buildUrl(input.path, input.query);
|
|
632
|
-
const hasBody = input.body !== void 0;
|
|
633
|
-
const headers = buildHeaders(
|
|
634
|
-
input.scope,
|
|
635
|
-
hasBody ? "application/json" : void 0,
|
|
636
|
-
input.idempotencyKey
|
|
637
|
-
);
|
|
638
|
-
return send(url, input.method, headers, hasBody ? JSON.stringify(input.body) : void 0);
|
|
639
|
-
},
|
|
640
|
-
async requestMultipart(input) {
|
|
641
|
-
const url = buildUrl(input.path);
|
|
642
|
-
const headers = buildHeaders(input.scope, void 0, input.idempotencyKey);
|
|
643
|
-
const form = await input.formData();
|
|
644
|
-
return send(url, "POST", headers, form);
|
|
645
|
-
}
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
function parseHttpResponse(res, text) {
|
|
649
|
-
if (res.status >= 500) {
|
|
650
|
-
return { ok: false, status: res.status, result: mcpServerError(res.status, text) };
|
|
651
|
-
}
|
|
652
|
-
const parsed = parseJsonText(text);
|
|
653
|
-
if (!parsed.ok) {
|
|
654
|
-
if (!res.ok) {
|
|
655
|
-
return {
|
|
656
|
-
ok: false,
|
|
657
|
-
status: res.status,
|
|
658
|
-
result: mcpHttpError(res.status, text || "no body")
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
return { ok: true, status: res.status, data: text };
|
|
662
|
-
}
|
|
663
|
-
if (!res.ok) {
|
|
664
|
-
const message = extractEnvelopeError(parsed.value) ?? text ?? `HTTP ${res.status}`;
|
|
665
|
-
return { ok: false, status: res.status, result: mcpHttpError(res.status, message) };
|
|
666
|
-
}
|
|
667
|
-
const envelopeError = extractEnvelopeError(parsed.value);
|
|
668
|
-
if (envelopeError) {
|
|
669
|
-
return { ok: false, status: res.status, result: mcpEnvelopeError(envelopeError) };
|
|
670
|
-
}
|
|
671
|
-
return { ok: true, status: res.status, data: parsed.value };
|
|
672
|
-
}
|
|
673
|
-
function parseJsonText(text) {
|
|
674
|
-
if (text.length === 0) return { ok: true, value: void 0 };
|
|
675
|
-
try {
|
|
676
|
-
return { ok: true, value: JSON.parse(text) };
|
|
677
|
-
} catch {
|
|
678
|
-
return { ok: false };
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
function extractEnvelopeError(parsed) {
|
|
682
|
-
if (parsed === null || typeof parsed !== "object") return null;
|
|
683
|
-
const obj = parsed;
|
|
684
|
-
if (obj.success === false) {
|
|
685
|
-
if (typeof obj.error === "string") return obj.error;
|
|
686
|
-
if (typeof obj.message === "string") return obj.message;
|
|
687
|
-
return "pyx-memory returned success: false";
|
|
688
|
-
}
|
|
689
|
-
return null;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
934
|
// src/mcp/tools/shared.ts
|
|
693
935
|
var LOCAL_UPLOAD_META_KEY = "pyx.dev/local-upload";
|
|
694
936
|
|
|
695
937
|
// src/mcp/proxy-server.ts
|
|
696
938
|
var REMOTE_MCP_PATH = "/mcp";
|
|
697
|
-
function createMcpIdempotentFetch(fetchImpl = fetch, sessionNonce =
|
|
939
|
+
function createMcpIdempotentFetch(fetchImpl = fetch, sessionNonce = randomUUID3()) {
|
|
698
940
|
const wrapped = async (input, init) => {
|
|
699
941
|
const upstream = input instanceof Request ? input : new Request(String(input), init);
|
|
700
942
|
const method = init?.method ?? upstream.method;
|
|
@@ -836,7 +1078,7 @@ function createProxyServer(client, version, uploadLocalFile) {
|
|
|
836
1078
|
return server;
|
|
837
1079
|
}
|
|
838
1080
|
async function runMcpProxyServer(opts) {
|
|
839
|
-
const version = opts.version ?? (true ? "1.
|
|
1081
|
+
const version = opts.version ?? (true ? "1.18.1" : "0.0.0-dev");
|
|
840
1082
|
const read = await opts.readCredentials();
|
|
841
1083
|
if (!read.ok) {
|
|
842
1084
|
const text = read.result.content.map((c) => c.type === "text" ? c.text : "").join(" ").trim();
|
|
@@ -1541,28 +1783,27 @@ function resolveMcpMode(flags) {
|
|
|
1541
1783
|
}
|
|
1542
1784
|
|
|
1543
1785
|
// src/cli/commands/overview.ts
|
|
1544
|
-
import {
|
|
1545
|
-
import { z } from "zod";
|
|
1786
|
+
import { z as z2 } from "zod";
|
|
1546
1787
|
var OVERVIEW_SCHEMA_VERSION = 1;
|
|
1547
1788
|
var RECENT_ENTRY_LIMIT = 3;
|
|
1548
1789
|
var MAX_TOPIC_CHARS = 160;
|
|
1549
1790
|
var MAX_PROJECT_CHARS = 80;
|
|
1550
|
-
var entrySchema =
|
|
1551
|
-
id:
|
|
1552
|
-
metadata:
|
|
1791
|
+
var entrySchema = z2.object({
|
|
1792
|
+
id: z2.string().min(1),
|
|
1793
|
+
metadata: z2.record(z2.string(), z2.unknown())
|
|
1553
1794
|
}).passthrough();
|
|
1554
|
-
var hostedEntriesResponseSchema =
|
|
1555
|
-
memories:
|
|
1556
|
-
total:
|
|
1795
|
+
var hostedEntriesResponseSchema = z2.object({
|
|
1796
|
+
memories: z2.array(entrySchema),
|
|
1797
|
+
total: z2.number().int().nonnegative()
|
|
1557
1798
|
}).passthrough();
|
|
1558
|
-
var selfHostedEntriesResponseSchema =
|
|
1559
|
-
success:
|
|
1560
|
-
data:
|
|
1561
|
-
entries:
|
|
1562
|
-
totalCount:
|
|
1799
|
+
var selfHostedEntriesResponseSchema = z2.object({
|
|
1800
|
+
success: z2.literal(true),
|
|
1801
|
+
data: z2.object({
|
|
1802
|
+
entries: z2.array(entrySchema),
|
|
1803
|
+
totalCount: z2.number().int().nonnegative()
|
|
1563
1804
|
}).passthrough()
|
|
1564
1805
|
}).passthrough();
|
|
1565
|
-
var entriesResponseSchema =
|
|
1806
|
+
var entriesResponseSchema = z2.union([
|
|
1566
1807
|
hostedEntriesResponseSchema.transform(({ memories, total }) => ({
|
|
1567
1808
|
entries: memories,
|
|
1568
1809
|
totalCount: total
|
|
@@ -1570,65 +1811,25 @@ var entriesResponseSchema = z.union([
|
|
|
1570
1811
|
selfHostedEntriesResponseSchema.transform(({ data }) => data)
|
|
1571
1812
|
]);
|
|
1572
1813
|
async function overviewCommand(opts = {}) {
|
|
1573
|
-
const
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
emit(report("signed_out", null, null, null));
|
|
1584
|
-
return EXIT.NOT_LOGGED_IN;
|
|
1585
|
-
}
|
|
1586
|
-
if (!isHttpUrl2(credentials.endpoint)) {
|
|
1587
|
-
emit(
|
|
1588
|
-
report(
|
|
1589
|
-
"error",
|
|
1590
|
-
null,
|
|
1591
|
-
"keychain",
|
|
1592
|
-
"The stored pyx-memory endpoint is invalid. Run: pyx-mem login"
|
|
1593
|
-
)
|
|
1594
|
-
);
|
|
1595
|
-
return EXIT.DOCTOR_FAIL;
|
|
1596
|
-
}
|
|
1597
|
-
const http = createHttpClient(credentials, opts.fetchImpl ?? fetch);
|
|
1598
|
-
const response = await http.requestJson({
|
|
1599
|
-
method: "GET",
|
|
1600
|
-
path: "/api/memory/entries",
|
|
1601
|
-
query: { status: "active", limit: RECENT_ENTRY_LIMIT },
|
|
1602
|
-
idempotencyKey: `cli-overview-${randomUUID3()}`
|
|
1814
|
+
const result = await readSurface({
|
|
1815
|
+
surface: "overview",
|
|
1816
|
+
request: {
|
|
1817
|
+
method: "GET",
|
|
1818
|
+
path: "/api/memory/entries",
|
|
1819
|
+
query: { status: "active", limit: RECENT_ENTRY_LIMIT }
|
|
1820
|
+
},
|
|
1821
|
+
schema: entriesResponseSchema,
|
|
1822
|
+
keychain: opts.keychain,
|
|
1823
|
+
fetchImpl: opts.fetchImpl
|
|
1603
1824
|
});
|
|
1604
|
-
if (!
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
)
|
|
1613
|
-
);
|
|
1614
|
-
return signedOut ? EXIT.NOT_LOGGED_IN : EXIT.DOCTOR_FAIL;
|
|
1615
|
-
}
|
|
1616
|
-
const parsed = entriesResponseSchema.safeParse(response.data);
|
|
1617
|
-
if (!parsed.success) {
|
|
1618
|
-
emit(
|
|
1619
|
-
report(
|
|
1620
|
-
"error",
|
|
1621
|
-
credentials.endpoint,
|
|
1622
|
-
"keychain",
|
|
1623
|
-
"pyx-memory returned an invalid overview response. Run: pyx-mem doctor"
|
|
1624
|
-
)
|
|
1625
|
-
);
|
|
1626
|
-
return EXIT.DOCTOR_FAIL;
|
|
1627
|
-
}
|
|
1628
|
-
emit({
|
|
1629
|
-
...report("ready", credentials.endpoint, "keychain", null),
|
|
1630
|
-
totalCount: parsed.data.totalCount,
|
|
1631
|
-
entries: parsed.data.entries.slice(0, RECENT_ENTRY_LIMIT).map((entry) => ({
|
|
1825
|
+
if (!result.ok) {
|
|
1826
|
+
emit2(report2(result.state, result.endpoint, result.keySource, result.problem));
|
|
1827
|
+
return result.exit;
|
|
1828
|
+
}
|
|
1829
|
+
emit2({
|
|
1830
|
+
...report2("ready", result.endpoint, "keychain", null),
|
|
1831
|
+
totalCount: result.data.totalCount,
|
|
1832
|
+
entries: result.data.entries.slice(0, RECENT_ENTRY_LIMIT).map((entry) => ({
|
|
1632
1833
|
id: entry.id,
|
|
1633
1834
|
topic: boundedLabel(entry.metadata.topic, "Untitled", MAX_TOPIC_CHARS),
|
|
1634
1835
|
project: boundedLabel(entry.metadata.project, "Unscoped", MAX_PROJECT_CHARS)
|
|
@@ -1636,7 +1837,7 @@ async function overviewCommand(opts = {}) {
|
|
|
1636
1837
|
});
|
|
1637
1838
|
return EXIT.OK;
|
|
1638
1839
|
}
|
|
1639
|
-
function
|
|
1840
|
+
function report2(state, endpoint, keySource, problem) {
|
|
1640
1841
|
return {
|
|
1641
1842
|
schemaVersion: OVERVIEW_SCHEMA_VERSION,
|
|
1642
1843
|
state,
|
|
@@ -1647,39 +1848,16 @@ function report(state, endpoint, keySource, problem) {
|
|
|
1647
1848
|
problem
|
|
1648
1849
|
};
|
|
1649
1850
|
}
|
|
1650
|
-
function
|
|
1851
|
+
function emit2(value) {
|
|
1651
1852
|
process.stdout.write(`${JSON.stringify(value)}
|
|
1652
1853
|
`);
|
|
1653
1854
|
}
|
|
1654
|
-
function overviewFailure(status) {
|
|
1655
|
-
if (status === 401) {
|
|
1656
|
-
return "Stored pyx-memory credentials were rejected. Run: pyx-mem login";
|
|
1657
|
-
}
|
|
1658
|
-
if (status === 403) {
|
|
1659
|
-
return "pyx-memory denied access (HTTP 403). Check the key scope and project access.";
|
|
1660
|
-
}
|
|
1661
|
-
if (status === void 0) {
|
|
1662
|
-
return "Unable to reach pyx-memory. Run: pyx-mem doctor";
|
|
1663
|
-
}
|
|
1664
|
-
if (status >= 500) {
|
|
1665
|
-
return `pyx-memory is unavailable (HTTP ${status}). Run: pyx-mem doctor`;
|
|
1666
|
-
}
|
|
1667
|
-
return `pyx-memory overview failed (HTTP ${status}). Run: pyx-mem doctor`;
|
|
1668
|
-
}
|
|
1669
1855
|
function boundedLabel(value, fallback, maxChars) {
|
|
1670
1856
|
if (typeof value !== "string" || value.trim().length === 0) return fallback;
|
|
1671
1857
|
const normalized = value.trim();
|
|
1672
1858
|
const chars = [...normalized];
|
|
1673
1859
|
return chars.length > maxChars ? `${chars.slice(0, maxChars).join("")}\u2026` : normalized;
|
|
1674
1860
|
}
|
|
1675
|
-
function isHttpUrl2(value) {
|
|
1676
|
-
try {
|
|
1677
|
-
const url = new URL(value);
|
|
1678
|
-
return url.protocol === "http:" || url.protocol === "https:";
|
|
1679
|
-
} catch {
|
|
1680
|
-
return false;
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
1861
|
|
|
1684
1862
|
// src/cli/commands/scaffold.ts
|
|
1685
1863
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
@@ -1861,10 +2039,10 @@ function scaffoldCommand(args = {}) {
|
|
|
1861
2039
|
// src/cli/commands/status.ts
|
|
1862
2040
|
async function statusCommand(opts = {}) {
|
|
1863
2041
|
const provider = opts.keychain ?? getDefaultKeychain();
|
|
1864
|
-
let
|
|
2042
|
+
let report4;
|
|
1865
2043
|
try {
|
|
1866
2044
|
const creds = await provider.read();
|
|
1867
|
-
|
|
2045
|
+
report4 = {
|
|
1868
2046
|
endpoint: creds?.endpoint ?? null,
|
|
1869
2047
|
keyPresent: creds !== null,
|
|
1870
2048
|
keySource: creds !== null ? "keychain" : null,
|
|
@@ -1875,17 +2053,17 @@ async function statusCommand(opts = {}) {
|
|
|
1875
2053
|
throw err;
|
|
1876
2054
|
}
|
|
1877
2055
|
if (opts.json) {
|
|
1878
|
-
process.stdout.write(`${JSON.stringify(
|
|
2056
|
+
process.stdout.write(`${JSON.stringify(report4)}
|
|
1879
2057
|
`);
|
|
1880
2058
|
} else {
|
|
1881
2059
|
const lines = [
|
|
1882
|
-
`endpoint: ${
|
|
1883
|
-
`credentials: ${
|
|
2060
|
+
`endpoint: ${report4.endpoint ?? "(not set)"}`,
|
|
2061
|
+
`credentials: ${report4.loggedIn ? "present (keychain)" : "not logged in \u2014 run: pyx-mem login"}`
|
|
1884
2062
|
];
|
|
1885
2063
|
process.stdout.write(`${lines.join("\n")}
|
|
1886
2064
|
`);
|
|
1887
2065
|
}
|
|
1888
|
-
return
|
|
2066
|
+
return report4.loggedIn ? EXIT.OK : EXIT.NOT_LOGGED_IN;
|
|
1889
2067
|
}
|
|
1890
2068
|
function emitError(err, json) {
|
|
1891
2069
|
if (json) {
|
|
@@ -1901,6 +2079,77 @@ ${err.guidance}
|
|
|
1901
2079
|
return err.exit;
|
|
1902
2080
|
}
|
|
1903
2081
|
|
|
2082
|
+
// src/cli/commands/taxonomy.ts
|
|
2083
|
+
import { z as z3 } from "zod";
|
|
2084
|
+
var TAXONOMY_SCHEMA_VERSION = 1;
|
|
2085
|
+
var taxonomyStateSchema = z3.object({
|
|
2086
|
+
clusters: z3.array(
|
|
2087
|
+
z3.object({
|
|
2088
|
+
size: z3.number().int().nonnegative(),
|
|
2089
|
+
currentName: z3.string().nullable(),
|
|
2090
|
+
categoryNodeId: z3.string().nullable(),
|
|
2091
|
+
projects: z3.array(
|
|
2092
|
+
z3.object({
|
|
2093
|
+
name: z3.string().min(1).max(MEMORY_PROJECT_LABEL_MAX_CHARS),
|
|
2094
|
+
memories: z3.number().int().positive()
|
|
2095
|
+
})
|
|
2096
|
+
).max(TAXONOMY_MAX_PROJECTS),
|
|
2097
|
+
sampleTopics: z3.array(z3.string()).max(TAXONOMY_MAX_SAMPLE_TOPICS),
|
|
2098
|
+
topEntities: z3.array(
|
|
2099
|
+
z3.object({
|
|
2100
|
+
name: z3.string(),
|
|
2101
|
+
type: z3.string(),
|
|
2102
|
+
degree: z3.number().int().nonnegative()
|
|
2103
|
+
})
|
|
2104
|
+
).max(TAXONOMY_MAX_TOP_ENTITIES)
|
|
2105
|
+
})
|
|
2106
|
+
).max(TAXONOMY_MAX_CATEGORIES),
|
|
2107
|
+
totals: z3.object({
|
|
2108
|
+
clusters: z3.number().int().nonnegative(),
|
|
2109
|
+
unnamed: z3.number().int().nonnegative(),
|
|
2110
|
+
nodesConsidered: z3.number().int().nonnegative()
|
|
2111
|
+
})
|
|
2112
|
+
});
|
|
2113
|
+
var taxonomyResponseSchema = z3.union([
|
|
2114
|
+
taxonomyStateSchema,
|
|
2115
|
+
z3.object({ success: z3.literal(true), data: taxonomyStateSchema }).transform(({ data }) => data)
|
|
2116
|
+
]);
|
|
2117
|
+
async function taxonomyCommand(opts = {}) {
|
|
2118
|
+
const result = await readSurface({
|
|
2119
|
+
surface: "taxonomy",
|
|
2120
|
+
request: { method: "GET", path: "/api/memory/graph/taxonomy" },
|
|
2121
|
+
schema: taxonomyResponseSchema,
|
|
2122
|
+
minimumServiceVersion: "1.18.1",
|
|
2123
|
+
keychain: opts.keychain,
|
|
2124
|
+
fetchImpl: opts.fetchImpl
|
|
2125
|
+
});
|
|
2126
|
+
if (!result.ok) {
|
|
2127
|
+
emit3(report3(result.state, result.endpoint, result.keySource, result.problem));
|
|
2128
|
+
return result.exit;
|
|
2129
|
+
}
|
|
2130
|
+
emit3({
|
|
2131
|
+
...report3("ready", result.endpoint, "keychain", null),
|
|
2132
|
+
clusters: result.data.clusters,
|
|
2133
|
+
totals: result.data.totals
|
|
2134
|
+
});
|
|
2135
|
+
return EXIT.OK;
|
|
2136
|
+
}
|
|
2137
|
+
function report3(state, endpoint, keySource, problem) {
|
|
2138
|
+
return {
|
|
2139
|
+
schemaVersion: TAXONOMY_SCHEMA_VERSION,
|
|
2140
|
+
state,
|
|
2141
|
+
endpoint,
|
|
2142
|
+
keySource,
|
|
2143
|
+
clusters: [],
|
|
2144
|
+
totals: { clusters: 0, unnamed: 0, nodesConsidered: 0 },
|
|
2145
|
+
problem
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
function emit3(value) {
|
|
2149
|
+
process.stdout.write(`${JSON.stringify(value)}
|
|
2150
|
+
`);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
1904
2153
|
// src/cli/help.ts
|
|
1905
2154
|
var HELP_TEXT = `pyx-mem \u2014 pyx-memory CLI
|
|
1906
2155
|
|
|
@@ -1911,6 +2160,8 @@ Commands:
|
|
|
1911
2160
|
login [--endpoint <url>] [--api-key <key>] Store endpoint and API key in OS credential store.
|
|
1912
2161
|
status [--json] Show endpoint, key presence, MCP config status.
|
|
1913
2162
|
overview --json Read memory count and three recent safe labels.
|
|
2163
|
+
insights --json Read body-free memory usage and coverage aggregates.
|
|
2164
|
+
taxonomy --json Read knowledge-graph cluster composition (read-only).
|
|
1914
2165
|
logout Delete stored pyx-memory credentials.
|
|
1915
2166
|
doctor [--json] Diagnose keychain, credentials, backend, MCP startup.
|
|
1916
2167
|
scaffold [--name <dir>] Generate Docker, env, SDK, and memory design-guide starter files.
|
|
@@ -2063,6 +2314,18 @@ async function main() {
|
|
|
2063
2314
|
return EXIT.USAGE;
|
|
2064
2315
|
}
|
|
2065
2316
|
return overviewCommand();
|
|
2317
|
+
case "insights":
|
|
2318
|
+
if (parsed.flags.json !== true) {
|
|
2319
|
+
process.stderr.write("Error: `pyx-mem insights` requires --json.\n");
|
|
2320
|
+
return EXIT.USAGE;
|
|
2321
|
+
}
|
|
2322
|
+
return insightsCommand();
|
|
2323
|
+
case "taxonomy":
|
|
2324
|
+
if (parsed.flags.json !== true) {
|
|
2325
|
+
process.stderr.write("Error: `pyx-mem taxonomy` requires --json.\n");
|
|
2326
|
+
return EXIT.USAGE;
|
|
2327
|
+
}
|
|
2328
|
+
return taxonomyCommand();
|
|
2066
2329
|
case "logout":
|
|
2067
2330
|
return logoutCommand();
|
|
2068
2331
|
case "doctor":
|