@pyxmate/memory 1.17.18 → 1.18.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/{chunk-T35ZOOES.mjs → chunk-ACFPMLNO.mjs} +1 -1
- package/dist/chunk-KVYCISUI.mjs +206 -0
- package/dist/{chunk-H6ZLMPZH.mjs → chunk-SLYCVO4C.mjs} +53 -201
- package/dist/cli/pyx-mem.mjs +531 -169
- package/dist/dashboard.mjs +3 -2
- package/dist/index.d.ts +63 -2
- package/dist/index.mjs +14 -4
- package/dist/react.mjs +3 -2
- 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-3OLH3HYR.mjs";
|
|
6
14
|
|
|
7
15
|
// src/cli/exit-codes.ts
|
|
8
16
|
var EXIT = {
|
|
@@ -328,6 +336,348 @@ 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
|
+
const signedOut = response.status === 401;
|
|
544
|
+
return {
|
|
545
|
+
ok: false,
|
|
546
|
+
exit: signedOut ? EXIT.NOT_LOGGED_IN : EXIT.DOCTOR_FAIL,
|
|
547
|
+
state: signedOut ? "signed_out" : "error",
|
|
548
|
+
endpoint: credentials.endpoint,
|
|
549
|
+
keySource: "keychain",
|
|
550
|
+
problem: requestFailure(opts.surface, response.status)
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
const parsed = opts.schema.safeParse(response.data);
|
|
554
|
+
if (!parsed.success) {
|
|
555
|
+
return {
|
|
556
|
+
ok: false,
|
|
557
|
+
exit: EXIT.DOCTOR_FAIL,
|
|
558
|
+
state: "error",
|
|
559
|
+
endpoint: credentials.endpoint,
|
|
560
|
+
keySource: "keychain",
|
|
561
|
+
problem: `pyx-memory returned an invalid ${opts.surface} response. Run: pyx-mem doctor`
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
return { ok: true, endpoint: credentials.endpoint, data: parsed.data };
|
|
565
|
+
}
|
|
566
|
+
function requestFailure(surface, status) {
|
|
567
|
+
if (status === 401) {
|
|
568
|
+
return "Stored pyx-memory credentials were rejected. Run: pyx-mem login";
|
|
569
|
+
}
|
|
570
|
+
if (status === 403) {
|
|
571
|
+
return "pyx-memory denied access (HTTP 403). Check the key scope and project access.";
|
|
572
|
+
}
|
|
573
|
+
if (status === void 0) {
|
|
574
|
+
return "Unable to reach pyx-memory. Run: pyx-mem doctor";
|
|
575
|
+
}
|
|
576
|
+
if (status >= 500) {
|
|
577
|
+
return `pyx-memory is unavailable (HTTP ${status}). Run: pyx-mem doctor`;
|
|
578
|
+
}
|
|
579
|
+
return `pyx-memory ${surface} failed (HTTP ${status}). Run: pyx-mem doctor`;
|
|
580
|
+
}
|
|
581
|
+
function isHttpUrl(value) {
|
|
582
|
+
try {
|
|
583
|
+
const url = new URL(value);
|
|
584
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
585
|
+
} catch {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/cli/commands/insights.ts
|
|
591
|
+
var INSIGHTS_SCHEMA_VERSION = 1;
|
|
592
|
+
var metricsSchema = z.object({
|
|
593
|
+
activeEntries: z.number().int().nonnegative(),
|
|
594
|
+
retainedAdditions7d: z.number().int().nonnegative(),
|
|
595
|
+
appliedRecalls: z.number().int().nonnegative(),
|
|
596
|
+
reinforcedEntries: z.number().int().nonnegative(),
|
|
597
|
+
retrievedEntries: z.number().int().nonnegative(),
|
|
598
|
+
retrievals: z.number().int().nonnegative(),
|
|
599
|
+
lastAppliedAt: z.string().nullable(),
|
|
600
|
+
lastRetrievedAt: z.string().nullable()
|
|
601
|
+
});
|
|
602
|
+
var insightsSchema = z.object({
|
|
603
|
+
generatedAt: z.string(),
|
|
604
|
+
scope: z.literal("connected_memory"),
|
|
605
|
+
coverage: z.object({
|
|
606
|
+
retainedAdditions: z.object({
|
|
607
|
+
state: z.enum(["measured", "partial"]),
|
|
608
|
+
trackingSince: z.string().nullable(),
|
|
609
|
+
trackedEntries: z.number().int().nonnegative(),
|
|
610
|
+
totalEntries: z.number().int().nonnegative(),
|
|
611
|
+
reason: z.string().nullable()
|
|
612
|
+
}),
|
|
613
|
+
projectLabels: z.object({ state: z.literal("measured") }),
|
|
614
|
+
retrievalUsage: z.object({ state: z.literal("partial"), reason: z.string() }),
|
|
615
|
+
tokenEfficiency: z.object({ state: z.literal("not_instrumented"), reason: z.string() }),
|
|
616
|
+
contextAccuracy: z.object({ state: z.literal("not_instrumented"), reason: z.string() })
|
|
617
|
+
}),
|
|
618
|
+
summary: z.object({
|
|
619
|
+
activeEntries: z.number().int().nonnegative(),
|
|
620
|
+
observedProjectLabels: z.number().int().nonnegative(),
|
|
621
|
+
retainedAdditions7d: z.number().int().nonnegative().nullable(),
|
|
622
|
+
trackedEntries: z.number().int().nonnegative(),
|
|
623
|
+
appliedRecalls: z.number().int().nonnegative(),
|
|
624
|
+
reinforcedEntries: z.number().int().nonnegative(),
|
|
625
|
+
retrievedEntries: z.number().int().nonnegative(),
|
|
626
|
+
retrievals: z.number().int().nonnegative()
|
|
627
|
+
}),
|
|
628
|
+
projects: z.array(
|
|
629
|
+
metricsSchema.extend({
|
|
630
|
+
label: z.string().min(1).max(MEMORY_PROJECT_LABEL_MAX_CHARS)
|
|
631
|
+
})
|
|
632
|
+
).max(TAXONOMY_MAX_PROJECTS),
|
|
633
|
+
other: metricsSchema.nullable()
|
|
634
|
+
});
|
|
635
|
+
var insightsResponseSchema = z.union([
|
|
636
|
+
insightsSchema,
|
|
637
|
+
z.object({ success: z.literal(true), data: insightsSchema }).transform(({ data }) => data)
|
|
638
|
+
]);
|
|
639
|
+
async function insightsCommand(opts = {}) {
|
|
640
|
+
const result = await readSurface({
|
|
641
|
+
surface: "insights",
|
|
642
|
+
request: { method: "GET", path: "/api/memory/insights" },
|
|
643
|
+
schema: insightsResponseSchema,
|
|
644
|
+
keychain: opts.keychain,
|
|
645
|
+
fetchImpl: opts.fetchImpl
|
|
646
|
+
});
|
|
647
|
+
if (!result.ok) {
|
|
648
|
+
emit(report(result.state, result.endpoint, result.keySource, result.problem));
|
|
649
|
+
return result.exit;
|
|
650
|
+
}
|
|
651
|
+
emit({
|
|
652
|
+
schemaVersion: INSIGHTS_SCHEMA_VERSION,
|
|
653
|
+
state: "ready",
|
|
654
|
+
endpoint: result.endpoint,
|
|
655
|
+
keySource: "keychain",
|
|
656
|
+
...result.data,
|
|
657
|
+
problem: null
|
|
658
|
+
});
|
|
659
|
+
return EXIT.OK;
|
|
660
|
+
}
|
|
661
|
+
function report(state, endpoint, keySource, problem) {
|
|
662
|
+
return {
|
|
663
|
+
schemaVersion: INSIGHTS_SCHEMA_VERSION,
|
|
664
|
+
state,
|
|
665
|
+
endpoint,
|
|
666
|
+
keySource,
|
|
667
|
+
generatedAt: null,
|
|
668
|
+
scope: "connected_memory",
|
|
669
|
+
coverage: null,
|
|
670
|
+
summary: null,
|
|
671
|
+
projects: [],
|
|
672
|
+
other: null,
|
|
673
|
+
problem
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
function emit(value) {
|
|
677
|
+
process.stdout.write(`${JSON.stringify(value)}
|
|
678
|
+
`);
|
|
679
|
+
}
|
|
680
|
+
|
|
331
681
|
// src/cli/prompt.ts
|
|
332
682
|
async function readApiKeySecret() {
|
|
333
683
|
if (process.stdin.isTTY) {
|
|
@@ -411,7 +761,7 @@ async function loginCommand(opts = {}) {
|
|
|
411
761
|
const endpoint = normalizeEndpoint(
|
|
412
762
|
opts.endpoint ?? await readExistingEndpoint(provider) ?? DEFAULT_ENDPOINT
|
|
413
763
|
);
|
|
414
|
-
if (!
|
|
764
|
+
if (!isHttpUrl2(endpoint)) {
|
|
415
765
|
process.stderr.write(`Error: invalid --endpoint \`${endpoint}\` (expected http(s) URL).
|
|
416
766
|
`);
|
|
417
767
|
return EXIT.USAGE;
|
|
@@ -473,7 +823,7 @@ async function readExistingEndpoint(provider) {
|
|
|
473
823
|
function normalizeEndpoint(input) {
|
|
474
824
|
return input.replace(/\/+$/, "");
|
|
475
825
|
}
|
|
476
|
-
function
|
|
826
|
+
function isHttpUrl2(value) {
|
|
477
827
|
try {
|
|
478
828
|
const u = new URL(value);
|
|
479
829
|
return u.protocol === "http:" || u.protocol === "https:";
|
|
@@ -500,44 +850,6 @@ ${err.guidance}
|
|
|
500
850
|
}
|
|
501
851
|
}
|
|
502
852
|
|
|
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
853
|
// src/mcp/credentials.ts
|
|
542
854
|
function createReadCredentials(providerFactory) {
|
|
543
855
|
return async () => {
|
|
@@ -557,7 +869,7 @@ function createReadCredentials(providerFactory) {
|
|
|
557
869
|
}
|
|
558
870
|
|
|
559
871
|
// src/mcp/proxy-server.ts
|
|
560
|
-
import { createHash, randomUUID as
|
|
872
|
+
import { createHash, randomUUID as randomUUID3 } from "crypto";
|
|
561
873
|
import { readFile, stat } from "fs/promises";
|
|
562
874
|
import { basename, isAbsolute, resolve } from "path";
|
|
563
875
|
import { Client as McpClient } from "@modelcontextprotocol/sdk/client/index.js";
|
|
@@ -571,130 +883,12 @@ import {
|
|
|
571
883
|
ListToolsRequestSchema
|
|
572
884
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
573
885
|
|
|
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
886
|
// src/mcp/tools/shared.ts
|
|
693
887
|
var LOCAL_UPLOAD_META_KEY = "pyx.dev/local-upload";
|
|
694
888
|
|
|
695
889
|
// src/mcp/proxy-server.ts
|
|
696
890
|
var REMOTE_MCP_PATH = "/mcp";
|
|
697
|
-
function createMcpIdempotentFetch(fetchImpl = fetch, sessionNonce =
|
|
891
|
+
function createMcpIdempotentFetch(fetchImpl = fetch, sessionNonce = randomUUID3()) {
|
|
698
892
|
const wrapped = async (input, init) => {
|
|
699
893
|
const upstream = input instanceof Request ? input : new Request(String(input), init);
|
|
700
894
|
const method = init?.method ?? upstream.method;
|
|
@@ -836,7 +1030,7 @@ function createProxyServer(client, version, uploadLocalFile) {
|
|
|
836
1030
|
return server;
|
|
837
1031
|
}
|
|
838
1032
|
async function runMcpProxyServer(opts) {
|
|
839
|
-
const version = opts.version ?? (true ? "1.
|
|
1033
|
+
const version = opts.version ?? (true ? "1.18.0" : "0.0.0-dev");
|
|
840
1034
|
const read = await opts.readCredentials();
|
|
841
1035
|
if (!read.ok) {
|
|
842
1036
|
const text = read.result.content.map((c) => c.type === "text" ? c.text : "").join(" ").trim();
|
|
@@ -1540,6 +1734,83 @@ function resolveMcpMode(flags) {
|
|
|
1540
1734
|
return { ok: true };
|
|
1541
1735
|
}
|
|
1542
1736
|
|
|
1737
|
+
// src/cli/commands/overview.ts
|
|
1738
|
+
import { z as z2 } from "zod";
|
|
1739
|
+
var OVERVIEW_SCHEMA_VERSION = 1;
|
|
1740
|
+
var RECENT_ENTRY_LIMIT = 3;
|
|
1741
|
+
var MAX_TOPIC_CHARS = 160;
|
|
1742
|
+
var MAX_PROJECT_CHARS = 80;
|
|
1743
|
+
var entrySchema = z2.object({
|
|
1744
|
+
id: z2.string().min(1),
|
|
1745
|
+
metadata: z2.record(z2.string(), z2.unknown())
|
|
1746
|
+
}).passthrough();
|
|
1747
|
+
var hostedEntriesResponseSchema = z2.object({
|
|
1748
|
+
memories: z2.array(entrySchema),
|
|
1749
|
+
total: z2.number().int().nonnegative()
|
|
1750
|
+
}).passthrough();
|
|
1751
|
+
var selfHostedEntriesResponseSchema = z2.object({
|
|
1752
|
+
success: z2.literal(true),
|
|
1753
|
+
data: z2.object({
|
|
1754
|
+
entries: z2.array(entrySchema),
|
|
1755
|
+
totalCount: z2.number().int().nonnegative()
|
|
1756
|
+
}).passthrough()
|
|
1757
|
+
}).passthrough();
|
|
1758
|
+
var entriesResponseSchema = z2.union([
|
|
1759
|
+
hostedEntriesResponseSchema.transform(({ memories, total }) => ({
|
|
1760
|
+
entries: memories,
|
|
1761
|
+
totalCount: total
|
|
1762
|
+
})),
|
|
1763
|
+
selfHostedEntriesResponseSchema.transform(({ data }) => data)
|
|
1764
|
+
]);
|
|
1765
|
+
async function overviewCommand(opts = {}) {
|
|
1766
|
+
const result = await readSurface({
|
|
1767
|
+
surface: "overview",
|
|
1768
|
+
request: {
|
|
1769
|
+
method: "GET",
|
|
1770
|
+
path: "/api/memory/entries",
|
|
1771
|
+
query: { status: "active", limit: RECENT_ENTRY_LIMIT }
|
|
1772
|
+
},
|
|
1773
|
+
schema: entriesResponseSchema,
|
|
1774
|
+
keychain: opts.keychain,
|
|
1775
|
+
fetchImpl: opts.fetchImpl
|
|
1776
|
+
});
|
|
1777
|
+
if (!result.ok) {
|
|
1778
|
+
emit2(report2(result.state, result.endpoint, result.keySource, result.problem));
|
|
1779
|
+
return result.exit;
|
|
1780
|
+
}
|
|
1781
|
+
emit2({
|
|
1782
|
+
...report2("ready", result.endpoint, "keychain", null),
|
|
1783
|
+
totalCount: result.data.totalCount,
|
|
1784
|
+
entries: result.data.entries.slice(0, RECENT_ENTRY_LIMIT).map((entry) => ({
|
|
1785
|
+
id: entry.id,
|
|
1786
|
+
topic: boundedLabel(entry.metadata.topic, "Untitled", MAX_TOPIC_CHARS),
|
|
1787
|
+
project: boundedLabel(entry.metadata.project, "Unscoped", MAX_PROJECT_CHARS)
|
|
1788
|
+
}))
|
|
1789
|
+
});
|
|
1790
|
+
return EXIT.OK;
|
|
1791
|
+
}
|
|
1792
|
+
function report2(state, endpoint, keySource, problem) {
|
|
1793
|
+
return {
|
|
1794
|
+
schemaVersion: OVERVIEW_SCHEMA_VERSION,
|
|
1795
|
+
state,
|
|
1796
|
+
endpoint,
|
|
1797
|
+
keySource,
|
|
1798
|
+
totalCount: 0,
|
|
1799
|
+
entries: [],
|
|
1800
|
+
problem
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
function emit2(value) {
|
|
1804
|
+
process.stdout.write(`${JSON.stringify(value)}
|
|
1805
|
+
`);
|
|
1806
|
+
}
|
|
1807
|
+
function boundedLabel(value, fallback, maxChars) {
|
|
1808
|
+
if (typeof value !== "string" || value.trim().length === 0) return fallback;
|
|
1809
|
+
const normalized = value.trim();
|
|
1810
|
+
const chars = [...normalized];
|
|
1811
|
+
return chars.length > maxChars ? `${chars.slice(0, maxChars).join("")}\u2026` : normalized;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1543
1814
|
// src/cli/commands/scaffold.ts
|
|
1544
1815
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1545
1816
|
import { basename as basename2, join as join2, resolve as resolve2 } from "path";
|
|
@@ -1720,10 +1991,10 @@ function scaffoldCommand(args = {}) {
|
|
|
1720
1991
|
// src/cli/commands/status.ts
|
|
1721
1992
|
async function statusCommand(opts = {}) {
|
|
1722
1993
|
const provider = opts.keychain ?? getDefaultKeychain();
|
|
1723
|
-
let
|
|
1994
|
+
let report4;
|
|
1724
1995
|
try {
|
|
1725
1996
|
const creds = await provider.read();
|
|
1726
|
-
|
|
1997
|
+
report4 = {
|
|
1727
1998
|
endpoint: creds?.endpoint ?? null,
|
|
1728
1999
|
keyPresent: creds !== null,
|
|
1729
2000
|
keySource: creds !== null ? "keychain" : null,
|
|
@@ -1734,17 +2005,17 @@ async function statusCommand(opts = {}) {
|
|
|
1734
2005
|
throw err;
|
|
1735
2006
|
}
|
|
1736
2007
|
if (opts.json) {
|
|
1737
|
-
process.stdout.write(`${JSON.stringify(
|
|
2008
|
+
process.stdout.write(`${JSON.stringify(report4)}
|
|
1738
2009
|
`);
|
|
1739
2010
|
} else {
|
|
1740
2011
|
const lines = [
|
|
1741
|
-
`endpoint: ${
|
|
1742
|
-
`credentials: ${
|
|
2012
|
+
`endpoint: ${report4.endpoint ?? "(not set)"}`,
|
|
2013
|
+
`credentials: ${report4.loggedIn ? "present (keychain)" : "not logged in \u2014 run: pyx-mem login"}`
|
|
1743
2014
|
];
|
|
1744
2015
|
process.stdout.write(`${lines.join("\n")}
|
|
1745
2016
|
`);
|
|
1746
2017
|
}
|
|
1747
|
-
return
|
|
2018
|
+
return report4.loggedIn ? EXIT.OK : EXIT.NOT_LOGGED_IN;
|
|
1748
2019
|
}
|
|
1749
2020
|
function emitError(err, json) {
|
|
1750
2021
|
if (json) {
|
|
@@ -1760,6 +2031,76 @@ ${err.guidance}
|
|
|
1760
2031
|
return err.exit;
|
|
1761
2032
|
}
|
|
1762
2033
|
|
|
2034
|
+
// src/cli/commands/taxonomy.ts
|
|
2035
|
+
import { z as z3 } from "zod";
|
|
2036
|
+
var TAXONOMY_SCHEMA_VERSION = 1;
|
|
2037
|
+
var taxonomyStateSchema = z3.object({
|
|
2038
|
+
clusters: z3.array(
|
|
2039
|
+
z3.object({
|
|
2040
|
+
size: z3.number().int().nonnegative(),
|
|
2041
|
+
currentName: z3.string().nullable(),
|
|
2042
|
+
categoryNodeId: z3.string().nullable(),
|
|
2043
|
+
projects: z3.array(
|
|
2044
|
+
z3.object({
|
|
2045
|
+
name: z3.string().min(1).max(MEMORY_PROJECT_LABEL_MAX_CHARS),
|
|
2046
|
+
memories: z3.number().int().positive()
|
|
2047
|
+
})
|
|
2048
|
+
).max(TAXONOMY_MAX_PROJECTS),
|
|
2049
|
+
sampleTopics: z3.array(z3.string()).max(TAXONOMY_MAX_SAMPLE_TOPICS),
|
|
2050
|
+
topEntities: z3.array(
|
|
2051
|
+
z3.object({
|
|
2052
|
+
name: z3.string(),
|
|
2053
|
+
type: z3.string(),
|
|
2054
|
+
degree: z3.number().int().nonnegative()
|
|
2055
|
+
})
|
|
2056
|
+
).max(TAXONOMY_MAX_TOP_ENTITIES)
|
|
2057
|
+
})
|
|
2058
|
+
).max(TAXONOMY_MAX_CATEGORIES),
|
|
2059
|
+
totals: z3.object({
|
|
2060
|
+
clusters: z3.number().int().nonnegative(),
|
|
2061
|
+
unnamed: z3.number().int().nonnegative(),
|
|
2062
|
+
nodesConsidered: z3.number().int().nonnegative()
|
|
2063
|
+
})
|
|
2064
|
+
});
|
|
2065
|
+
var taxonomyResponseSchema = z3.union([
|
|
2066
|
+
taxonomyStateSchema,
|
|
2067
|
+
z3.object({ success: z3.literal(true), data: taxonomyStateSchema }).transform(({ data }) => data)
|
|
2068
|
+
]);
|
|
2069
|
+
async function taxonomyCommand(opts = {}) {
|
|
2070
|
+
const result = await readSurface({
|
|
2071
|
+
surface: "taxonomy",
|
|
2072
|
+
request: { method: "GET", path: "/api/memory/graph/taxonomy" },
|
|
2073
|
+
schema: taxonomyResponseSchema,
|
|
2074
|
+
keychain: opts.keychain,
|
|
2075
|
+
fetchImpl: opts.fetchImpl
|
|
2076
|
+
});
|
|
2077
|
+
if (!result.ok) {
|
|
2078
|
+
emit3(report3(result.state, result.endpoint, result.keySource, result.problem));
|
|
2079
|
+
return result.exit;
|
|
2080
|
+
}
|
|
2081
|
+
emit3({
|
|
2082
|
+
...report3("ready", result.endpoint, "keychain", null),
|
|
2083
|
+
clusters: result.data.clusters,
|
|
2084
|
+
totals: result.data.totals
|
|
2085
|
+
});
|
|
2086
|
+
return EXIT.OK;
|
|
2087
|
+
}
|
|
2088
|
+
function report3(state, endpoint, keySource, problem) {
|
|
2089
|
+
return {
|
|
2090
|
+
schemaVersion: TAXONOMY_SCHEMA_VERSION,
|
|
2091
|
+
state,
|
|
2092
|
+
endpoint,
|
|
2093
|
+
keySource,
|
|
2094
|
+
clusters: [],
|
|
2095
|
+
totals: { clusters: 0, unnamed: 0, nodesConsidered: 0 },
|
|
2096
|
+
problem
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
function emit3(value) {
|
|
2100
|
+
process.stdout.write(`${JSON.stringify(value)}
|
|
2101
|
+
`);
|
|
2102
|
+
}
|
|
2103
|
+
|
|
1763
2104
|
// src/cli/help.ts
|
|
1764
2105
|
var HELP_TEXT = `pyx-mem \u2014 pyx-memory CLI
|
|
1765
2106
|
|
|
@@ -1769,6 +2110,9 @@ Usage:
|
|
|
1769
2110
|
Commands:
|
|
1770
2111
|
login [--endpoint <url>] [--api-key <key>] Store endpoint and API key in OS credential store.
|
|
1771
2112
|
status [--json] Show endpoint, key presence, MCP config status.
|
|
2113
|
+
overview --json Read memory count and three recent safe labels.
|
|
2114
|
+
insights --json Read body-free memory usage and coverage aggregates.
|
|
2115
|
+
taxonomy --json Read knowledge-graph cluster composition (read-only).
|
|
1772
2116
|
logout Delete stored pyx-memory credentials.
|
|
1773
2117
|
doctor [--json] Diagnose keychain, credentials, backend, MCP startup.
|
|
1774
2118
|
scaffold [--name <dir>] Generate Docker, env, SDK, and memory design-guide starter files.
|
|
@@ -1915,6 +2259,24 @@ async function main() {
|
|
|
1915
2259
|
});
|
|
1916
2260
|
case "status":
|
|
1917
2261
|
return statusCommand({ json: parsed.flags.json === true });
|
|
2262
|
+
case "overview":
|
|
2263
|
+
if (parsed.flags.json !== true) {
|
|
2264
|
+
process.stderr.write("Error: `pyx-mem overview` requires --json.\n");
|
|
2265
|
+
return EXIT.USAGE;
|
|
2266
|
+
}
|
|
2267
|
+
return overviewCommand();
|
|
2268
|
+
case "insights":
|
|
2269
|
+
if (parsed.flags.json !== true) {
|
|
2270
|
+
process.stderr.write("Error: `pyx-mem insights` requires --json.\n");
|
|
2271
|
+
return EXIT.USAGE;
|
|
2272
|
+
}
|
|
2273
|
+
return insightsCommand();
|
|
2274
|
+
case "taxonomy":
|
|
2275
|
+
if (parsed.flags.json !== true) {
|
|
2276
|
+
process.stderr.write("Error: `pyx-mem taxonomy` requires --json.\n");
|
|
2277
|
+
return EXIT.USAGE;
|
|
2278
|
+
}
|
|
2279
|
+
return taxonomyCommand();
|
|
1918
2280
|
case "logout":
|
|
1919
2281
|
return logoutCommand();
|
|
1920
2282
|
case "doctor":
|
|
@@ -1932,10 +2294,10 @@ async function main() {
|
|
|
1932
2294
|
}
|
|
1933
2295
|
}
|
|
1934
2296
|
main().then((code) => {
|
|
1935
|
-
process.
|
|
2297
|
+
process.exitCode = code;
|
|
1936
2298
|
}).catch((err) => {
|
|
1937
2299
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1938
2300
|
process.stderr.write(`Internal error: ${msg}
|
|
1939
2301
|
`);
|
|
1940
|
-
process.
|
|
2302
|
+
process.exitCode = 1;
|
|
1941
2303
|
});
|