@pyxmate/memory 1.23.1 → 1.24.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/README.md +6 -1
- package/dist/chunk-6EEEOV6B.mjs +0 -0
- package/dist/{chunk-7YHCNO3N.mjs → chunk-L2RHO7DP.mjs} +1 -1
- package/dist/{chunk-SGL73IY5.mjs → chunk-LF22PCY6.mjs} +1 -1
- package/dist/{chunk-C46EE5BH.mjs → chunk-VXXRCAKS.mjs} +416 -3
- package/dist/cli/pyx-mem.mjs +787 -133
- package/dist/dashboard.mjs +3 -3
- package/dist/{data-plane-contract-C5VqxrpF.d.ts → data-plane-contract-CGU0qjv4.d.ts} +121 -1
- package/dist/data-plane-contract.d.ts +1 -1
- package/dist/data-plane-contract.mjs +26 -3
- package/dist/index.d.ts +6 -2
- package/dist/index.mjs +2 -2
- package/dist/react.mjs +3 -3
- package/package.json +1 -1
package/dist/cli/pyx-mem.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import "../chunk-6EEEOV6B.mjs";
|
|
2
3
|
import {
|
|
3
4
|
PERSISTENT_MEMORY_SECTION,
|
|
4
5
|
buildDesignGuide
|
|
@@ -10,7 +11,15 @@ import {
|
|
|
10
11
|
TAXONOMY_MAX_SAMPLE_TOPICS,
|
|
11
12
|
TAXONOMY_MAX_TOP_ENTITIES
|
|
12
13
|
} from "../chunk-KZKGRND2.mjs";
|
|
13
|
-
import
|
|
14
|
+
import {
|
|
15
|
+
HOSTED_FILE_INGEST_PROTOCOL,
|
|
16
|
+
HOSTED_FILE_INGEST_RETRYABLE_CODES,
|
|
17
|
+
parseHostedFileIngestAuthorization,
|
|
18
|
+
parseHostedFileIngestCommitResult,
|
|
19
|
+
parseHostedFileIngestJobView,
|
|
20
|
+
parseHostedFileIngestPublishResult,
|
|
21
|
+
requireHostedFileIngestFilename
|
|
22
|
+
} from "../chunk-VXXRCAKS.mjs";
|
|
14
23
|
|
|
15
24
|
// src/cli/exit-codes.ts
|
|
16
25
|
var EXIT = {
|
|
@@ -174,9 +183,9 @@ function parseEndpoint(input) {
|
|
|
174
183
|
return normalized.length <= MAX_ENDPOINT_CHARS ? normalized : null;
|
|
175
184
|
}
|
|
176
185
|
function boundLabel(value, maxChars) {
|
|
177
|
-
let
|
|
178
|
-
while (JSON.stringify(
|
|
179
|
-
return
|
|
186
|
+
let label2 = value.replace(/\p{Cc}/gu, "").slice(0, maxChars);
|
|
187
|
+
while (JSON.stringify(label2).length - 2 > maxChars) label2 = label2.slice(0, -1);
|
|
188
|
+
return label2;
|
|
180
189
|
}
|
|
181
190
|
function isTrustedAuthorizationUrl(authorizationUrl, endpoint, requestId) {
|
|
182
191
|
const url = parseSafeUrl(authorizationUrl);
|
|
@@ -820,8 +829,8 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
820
829
|
// src/mcp/errors.ts
|
|
821
830
|
var RUN_LOGIN = "Run: pyx-mem login";
|
|
822
831
|
var RUN_DOCTOR = "Run: pyx-mem doctor";
|
|
823
|
-
function mcpText(
|
|
824
|
-
return { isError, content: [{ type: "text", text }] };
|
|
832
|
+
function mcpText(text2, isError = false) {
|
|
833
|
+
return { isError, content: [{ type: "text", text: text2 }] };
|
|
825
834
|
}
|
|
826
835
|
function mcpMissingCredentials() {
|
|
827
836
|
return mcpText(`pyx-memory credentials are not configured.
|
|
@@ -890,25 +899,36 @@ function createHttpClient(credentials, fetchImpl = fetch) {
|
|
|
890
899
|
}
|
|
891
900
|
return headers;
|
|
892
901
|
}
|
|
893
|
-
async function send(url, method, headers, body) {
|
|
902
|
+
async function send(url, method, headers, body, deadline = {}) {
|
|
903
|
+
const { timeoutMs, signal } = deadline;
|
|
894
904
|
const ctl = new AbortController();
|
|
895
|
-
const timer = setTimeout(
|
|
896
|
-
|
|
905
|
+
const timer = setTimeout(
|
|
906
|
+
() => timeoutMs === void 0 ? ctl.abort() : ctl.abort(
|
|
907
|
+
new DOMException(`no complete response within ${timeoutMs / 1e3} s`, "TimeoutError")
|
|
908
|
+
),
|
|
909
|
+
timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
910
|
+
);
|
|
897
911
|
try {
|
|
898
|
-
res
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
912
|
+
let res;
|
|
913
|
+
try {
|
|
914
|
+
res = await fetchImpl(url, {
|
|
915
|
+
method,
|
|
916
|
+
headers,
|
|
917
|
+
body,
|
|
918
|
+
signal: signal ? AbortSignal.any([ctl.signal, signal]) : ctl.signal
|
|
919
|
+
});
|
|
920
|
+
} catch (err) {
|
|
921
|
+
return {
|
|
922
|
+
ok: false,
|
|
923
|
+
result: mcpUnreachable(baseUrl, err instanceof Error ? err.message : String(err))
|
|
924
|
+
};
|
|
925
|
+
} finally {
|
|
926
|
+
if (timeoutMs === void 0) clearTimeout(timer);
|
|
927
|
+
}
|
|
928
|
+
return await readResponse(res);
|
|
904
929
|
} finally {
|
|
905
930
|
clearTimeout(timer);
|
|
906
931
|
}
|
|
907
|
-
if (res.status === 401 || res.status === 403) {
|
|
908
|
-
return { ok: false, status: res.status, result: mcpAuthFailed(res.status) };
|
|
909
|
-
}
|
|
910
|
-
const text = await res.text().catch(() => "");
|
|
911
|
-
return parseHttpResponse(res, text);
|
|
912
932
|
}
|
|
913
933
|
return {
|
|
914
934
|
async requestJson(input) {
|
|
@@ -919,49 +939,123 @@ function createHttpClient(credentials, fetchImpl = fetch) {
|
|
|
919
939
|
hasBody ? "application/json" : void 0,
|
|
920
940
|
input.idempotencyKey
|
|
921
941
|
);
|
|
922
|
-
return send(
|
|
942
|
+
return send(
|
|
943
|
+
url,
|
|
944
|
+
input.method,
|
|
945
|
+
headers,
|
|
946
|
+
hasBody ? JSON.stringify(input.body) : void 0,
|
|
947
|
+
input
|
|
948
|
+
);
|
|
923
949
|
},
|
|
924
950
|
async requestMultipart(input) {
|
|
925
951
|
const url = buildUrl(input.path);
|
|
926
952
|
const headers = buildHeaders(input.scope, void 0, input.idempotencyKey);
|
|
927
953
|
const form = await input.formData();
|
|
928
954
|
return send(url, "POST", headers, form);
|
|
955
|
+
},
|
|
956
|
+
async requestBytes(input) {
|
|
957
|
+
const headers = buildHeaders(input.scope, "application/octet-stream", input.idempotencyKey);
|
|
958
|
+
for (const [name, value] of Object.entries(input.headers)) headers.set(name, value);
|
|
959
|
+
return send(buildUrl(input.path), input.method, headers, input.body, input);
|
|
929
960
|
}
|
|
930
961
|
};
|
|
931
962
|
}
|
|
932
|
-
function
|
|
963
|
+
async function readResponse(res) {
|
|
964
|
+
if (res.status === 401 || res.status === 403) {
|
|
965
|
+
return { ok: false, status: res.status, result: mcpAuthFailed(res.status) };
|
|
966
|
+
}
|
|
967
|
+
let text2;
|
|
968
|
+
try {
|
|
969
|
+
text2 = await res.text();
|
|
970
|
+
} catch (err) {
|
|
971
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
972
|
+
return {
|
|
973
|
+
ok: false,
|
|
974
|
+
status: res.status,
|
|
975
|
+
result: mcpEnvelopeError(
|
|
976
|
+
`Failed to read pyx-memory response (HTTP ${res.status}): ${reason}`
|
|
977
|
+
)
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
return parseHttpResponse(res, text2);
|
|
981
|
+
}
|
|
982
|
+
function parseHttpResponse(res, text2) {
|
|
983
|
+
if (res.ok && res.headers.get("content-type")?.includes("application/x-ndjson")) {
|
|
984
|
+
return parseNdjsonOutcome(res.status, text2);
|
|
985
|
+
}
|
|
986
|
+
const parsed = parseJsonText(text2);
|
|
987
|
+
const gateway = parsed.ok ? gatewayError(parsed.value) : void 0;
|
|
988
|
+
const failure2 = {
|
|
989
|
+
ok: false,
|
|
990
|
+
status: res.status,
|
|
991
|
+
code: parsed.ok ? extractEnvelopeCode(parsed.value) : void 0,
|
|
992
|
+
retryAfter: res.headers.get("retry-after") ?? void 0
|
|
993
|
+
};
|
|
933
994
|
if (res.status >= 500) {
|
|
934
|
-
return {
|
|
995
|
+
return { ...failure2, result: mcpServerError(res.status, gateway?.message ?? text2) };
|
|
935
996
|
}
|
|
936
|
-
const parsed = parseJsonText(text);
|
|
937
997
|
if (!parsed.ok) {
|
|
938
998
|
if (!res.ok) {
|
|
939
|
-
return {
|
|
940
|
-
ok: false,
|
|
941
|
-
status: res.status,
|
|
942
|
-
result: mcpHttpError(res.status, text || "no body")
|
|
943
|
-
};
|
|
999
|
+
return { ...failure2, result: mcpHttpError(res.status, text2 || "no body") };
|
|
944
1000
|
}
|
|
945
|
-
return { ok: true, status: res.status, data:
|
|
1001
|
+
return { ok: true, status: res.status, data: text2 };
|
|
946
1002
|
}
|
|
947
1003
|
if (!res.ok) {
|
|
948
|
-
const message = extractEnvelopeError(parsed.value) ??
|
|
949
|
-
return {
|
|
1004
|
+
const message = extractEnvelopeError(parsed.value) ?? gateway?.message ?? text2 ?? `HTTP ${res.status}`;
|
|
1005
|
+
return { ...failure2, result: mcpHttpError(res.status, message) };
|
|
950
1006
|
}
|
|
951
1007
|
const envelopeError = extractEnvelopeError(parsed.value);
|
|
952
1008
|
if (envelopeError) {
|
|
953
|
-
return {
|
|
1009
|
+
return { ...failure2, result: mcpEnvelopeError(envelopeError) };
|
|
954
1010
|
}
|
|
955
1011
|
return { ok: true, status: res.status, data: parsed.value };
|
|
956
1012
|
}
|
|
957
|
-
function
|
|
958
|
-
|
|
1013
|
+
function parseNdjsonOutcome(status, text2) {
|
|
1014
|
+
for (const line of text2.split("\n")) {
|
|
1015
|
+
if (!line.trim()) continue;
|
|
1016
|
+
let frame;
|
|
1017
|
+
try {
|
|
1018
|
+
frame = JSON.parse(line);
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1021
|
+
return {
|
|
1022
|
+
ok: false,
|
|
1023
|
+
status,
|
|
1024
|
+
result: mcpEnvelopeError(`pyx-memory sent an invalid NDJSON line: ${reason}`)
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
if (frame?.type === "result") return { ok: true, status, data: frame };
|
|
1028
|
+
if (frame?.type === "error") {
|
|
1029
|
+
return { ok: false, status, result: mcpEnvelopeError(JSON.stringify(frame)) };
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return {
|
|
1033
|
+
ok: false,
|
|
1034
|
+
status,
|
|
1035
|
+
result: mcpEnvelopeError("File ingest stream ended without a server result")
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function parseJsonText(text2) {
|
|
1039
|
+
if (text2.length === 0) return { ok: true, value: void 0 };
|
|
959
1040
|
try {
|
|
960
|
-
return { ok: true, value: JSON.parse(
|
|
1041
|
+
return { ok: true, value: JSON.parse(text2) };
|
|
961
1042
|
} catch {
|
|
962
1043
|
return { ok: false };
|
|
963
1044
|
}
|
|
964
1045
|
}
|
|
1046
|
+
function gatewayError(parsed) {
|
|
1047
|
+
if (parsed === null || typeof parsed !== "object") return void 0;
|
|
1048
|
+
const error = parsed.error;
|
|
1049
|
+
if (error === null || typeof error !== "object") return void 0;
|
|
1050
|
+
const { code, message } = error;
|
|
1051
|
+
return typeof code === "string" && typeof message === "string" ? { code, message } : void 0;
|
|
1052
|
+
}
|
|
1053
|
+
function extractEnvelopeCode(parsed) {
|
|
1054
|
+
if (parsed === null || typeof parsed !== "object") return void 0;
|
|
1055
|
+
const obj = parsed;
|
|
1056
|
+
if (obj.success === false && typeof obj.code === "string") return obj.code;
|
|
1057
|
+
return gatewayError(parsed)?.code;
|
|
1058
|
+
}
|
|
965
1059
|
function extractEnvelopeError(parsed) {
|
|
966
1060
|
if (parsed === null || typeof parsed !== "object") return null;
|
|
967
1061
|
const obj = parsed;
|
|
@@ -1240,9 +1334,9 @@ async function readApiKeySecret() {
|
|
|
1240
1334
|
}
|
|
1241
1335
|
return readSingleLineFromStdin();
|
|
1242
1336
|
}
|
|
1243
|
-
async function promptMasked(
|
|
1337
|
+
async function promptMasked(label2) {
|
|
1244
1338
|
const { stdin, stdout } = process;
|
|
1245
|
-
stdout.write(
|
|
1339
|
+
stdout.write(label2);
|
|
1246
1340
|
const hadRawMode = typeof stdin.setRawMode === "function";
|
|
1247
1341
|
if (!hadRawMode) {
|
|
1248
1342
|
const value = await readSingleLineFromStdin();
|
|
@@ -1250,7 +1344,7 @@ async function promptMasked(label) {
|
|
|
1250
1344
|
`);
|
|
1251
1345
|
return value;
|
|
1252
1346
|
}
|
|
1253
|
-
return new Promise((
|
|
1347
|
+
return new Promise((resolve4, reject) => {
|
|
1254
1348
|
const previousEncoding = stdin.readableEncoding;
|
|
1255
1349
|
stdin.setEncoding("utf8");
|
|
1256
1350
|
stdin.setRawMode(true);
|
|
@@ -1268,7 +1362,7 @@ async function promptMasked(label) {
|
|
|
1268
1362
|
if (ch === "\n" || ch === "\r") {
|
|
1269
1363
|
cleanup();
|
|
1270
1364
|
stdout.write("\n");
|
|
1271
|
-
|
|
1365
|
+
resolve4(buf.trim());
|
|
1272
1366
|
return;
|
|
1273
1367
|
}
|
|
1274
1368
|
if (ch === "") {
|
|
@@ -1439,7 +1533,7 @@ p { margin: 0; color: #a9adb8; font-size: 16px; line-height: 1.65; }
|
|
|
1439
1533
|
@media (prefers-reduced-motion: reduce) { .continue .symbol { animation: none; } }
|
|
1440
1534
|
`;
|
|
1441
1535
|
var PAGE_CSP = `default-src 'none'; style-src 'sha256-${createHash2("sha256").update(STYLES).digest("base64")}'; base-uri 'none'; form-action 'none'`;
|
|
1442
|
-
var page = (state, title,
|
|
1536
|
+
var page = (state, title, text2, icon) => `<!doctype html>
|
|
1443
1537
|
<html lang="en">
|
|
1444
1538
|
<head>
|
|
1445
1539
|
<meta charset="utf-8">
|
|
@@ -1453,7 +1547,7 @@ var page = (state, title, text, icon) => `<!doctype html>
|
|
|
1453
1547
|
<section class="card" aria-labelledby="title">
|
|
1454
1548
|
<div class="symbol" aria-hidden="true"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">${icon}</svg></div>
|
|
1455
1549
|
<h1 id="title">${title}</h1>
|
|
1456
|
-
<p>${
|
|
1550
|
+
<p>${text2}</p>
|
|
1457
1551
|
<p class="note">You can close this tab.</p>
|
|
1458
1552
|
</section>
|
|
1459
1553
|
</main>
|
|
@@ -1490,8 +1584,8 @@ async function startCallbackServer() {
|
|
|
1490
1584
|
let expected = null;
|
|
1491
1585
|
let settle2 = () => {
|
|
1492
1586
|
};
|
|
1493
|
-
const result = new Promise((
|
|
1494
|
-
settle2 =
|
|
1587
|
+
const result = new Promise((resolve4) => {
|
|
1588
|
+
settle2 = resolve4;
|
|
1495
1589
|
});
|
|
1496
1590
|
function accept(method, host, rawUrl) {
|
|
1497
1591
|
if (method !== "GET" || expected === null || host !== `127.0.0.1:${port}`) return null;
|
|
@@ -1526,9 +1620,9 @@ async function startCallbackServer() {
|
|
|
1526
1620
|
});
|
|
1527
1621
|
server.requestTimeout = 5e3;
|
|
1528
1622
|
server.headersTimeout = 5e3;
|
|
1529
|
-
await new Promise((
|
|
1623
|
+
await new Promise((resolve4, reject) => {
|
|
1530
1624
|
server.once("error", reject);
|
|
1531
|
-
server.listen(0, "127.0.0.1",
|
|
1625
|
+
server.listen(0, "127.0.0.1", resolve4);
|
|
1532
1626
|
});
|
|
1533
1627
|
const port = server.address().port;
|
|
1534
1628
|
return {
|
|
@@ -1859,7 +1953,7 @@ async function cleanupPendingAfterFailure(keychain) {
|
|
|
1859
1953
|
async function logoutCommand(opts = {}) {
|
|
1860
1954
|
const keychain = opts.keychain ?? getLifecycleKeychain();
|
|
1861
1955
|
const api = createCliLoginApi(opts.fetchImpl);
|
|
1862
|
-
const
|
|
1956
|
+
const fail2 = (failure2, exit) => {
|
|
1863
1957
|
if (opts.json) process.stdout.write(`${JSON.stringify(failure2)}
|
|
1864
1958
|
`);
|
|
1865
1959
|
else process.stderr.write(`Error: ${failure2.message}
|
|
@@ -1868,19 +1962,19 @@ async function logoutCommand(opts = {}) {
|
|
|
1868
1962
|
};
|
|
1869
1963
|
try {
|
|
1870
1964
|
const result = await withLifecycleLock(keychain, (lockLost) => logout(lockLost));
|
|
1871
|
-
return result === BUSY ?
|
|
1965
|
+
return result === BUSY ? fail2(lifecycleFailure("busy", false), EXIT.BUSY) : result;
|
|
1872
1966
|
} catch (err) {
|
|
1873
1967
|
const pending = await cleanupPendingAfterFailure(keychain);
|
|
1874
1968
|
if (err instanceof KeychainError) {
|
|
1875
|
-
return
|
|
1969
|
+
return fail2(lifecycleFailure("credential_store_unavailable", pending, err), err.exit);
|
|
1876
1970
|
}
|
|
1877
1971
|
if (err instanceof LockLostError) {
|
|
1878
|
-
return
|
|
1972
|
+
return fail2(lifecycleFailure("lock_lost", pending), EXIT.DOCTOR_FAIL);
|
|
1879
1973
|
}
|
|
1880
1974
|
if (err instanceof JournalInvalidError) {
|
|
1881
|
-
return
|
|
1975
|
+
return fail2(lifecycleFailure("journal_invalid", true), EXIT.DOCTOR_FAIL);
|
|
1882
1976
|
}
|
|
1883
|
-
return
|
|
1977
|
+
return fail2(lifecycleFailure("cleanup_pending", true), EXIT.CLEANUP_PENDING);
|
|
1884
1978
|
}
|
|
1885
1979
|
async function logout(lockLost) {
|
|
1886
1980
|
const journal = await loadJournal(keychain);
|
|
@@ -1902,7 +1996,7 @@ async function logoutCommand(opts = {}) {
|
|
|
1902
1996
|
}
|
|
1903
1997
|
const cleanupPending = !isEmpty(await settle(keychain, api, journal, lockLost));
|
|
1904
1998
|
if (await keychain.read() !== null) {
|
|
1905
|
-
return
|
|
1999
|
+
return fail2(lifecycleFailure("credential_changed", cleanupPending), EXIT.DOCTOR_FAIL);
|
|
1906
2000
|
}
|
|
1907
2001
|
if (lockLost()) throw new LockLostError();
|
|
1908
2002
|
const convergence = convergenceSeconds(journal);
|
|
@@ -1961,7 +2055,7 @@ async function signOutJournal(keychain) {
|
|
|
1961
2055
|
}
|
|
1962
2056
|
async function loginRecoveryCommand(opts = {}) {
|
|
1963
2057
|
const keychain = opts.keychain ?? getLifecycleKeychain();
|
|
1964
|
-
const
|
|
2058
|
+
const fail2 = (failure2, exit) => {
|
|
1965
2059
|
if (opts.json) process.stdout.write(`${JSON.stringify(failure2)}
|
|
1966
2060
|
`);
|
|
1967
2061
|
else process.stderr.write(`Error: ${failure2.message}
|
|
@@ -1980,9 +2074,9 @@ async function loginRecoveryCommand(opts = {}) {
|
|
|
1980
2074
|
if (lockLost()) throw new LockLostError();
|
|
1981
2075
|
return settled;
|
|
1982
2076
|
});
|
|
1983
|
-
if (journal === BUSY) return
|
|
2077
|
+
if (journal === BUSY) return fail2(lifecycleFailure("busy", true), EXIT.BUSY);
|
|
1984
2078
|
if (!isEmpty(journal)) {
|
|
1985
|
-
return
|
|
2079
|
+
return fail2(lifecycleFailure("cleanup_pending", true), EXIT.CLEANUP_PENDING);
|
|
1986
2080
|
}
|
|
1987
2081
|
const convergence = convergenceSeconds(journal);
|
|
1988
2082
|
const report4 = {
|
|
@@ -1997,16 +2091,16 @@ async function loginRecoveryCommand(opts = {}) {
|
|
|
1997
2091
|
return EXIT.OK;
|
|
1998
2092
|
} catch (err) {
|
|
1999
2093
|
if (err instanceof JournalInvalidError) {
|
|
2000
|
-
return
|
|
2094
|
+
return fail2(lifecycleFailure("journal_invalid", true), EXIT.DOCTOR_FAIL);
|
|
2001
2095
|
}
|
|
2002
2096
|
const pending = await cleanupPendingAfterFailure(keychain);
|
|
2003
2097
|
if (err instanceof KeychainError) {
|
|
2004
|
-
return
|
|
2098
|
+
return fail2(lifecycleFailure("credential_store_unavailable", pending, err), err.exit);
|
|
2005
2099
|
}
|
|
2006
2100
|
if (err instanceof LockLostError) {
|
|
2007
|
-
return
|
|
2101
|
+
return fail2(lifecycleFailure("lock_lost", pending), EXIT.DOCTOR_FAIL);
|
|
2008
2102
|
}
|
|
2009
|
-
return
|
|
2103
|
+
return fail2(lifecycleFailure("cleanup_pending", true), EXIT.CLEANUP_PENDING);
|
|
2010
2104
|
}
|
|
2011
2105
|
}
|
|
2012
2106
|
|
|
@@ -2029,9 +2123,9 @@ function createReadCredentials(providerFactory) {
|
|
|
2029
2123
|
}
|
|
2030
2124
|
|
|
2031
2125
|
// src/mcp/proxy-server.ts
|
|
2032
|
-
import { createHash as
|
|
2033
|
-
import { readFile, stat } from "fs/promises";
|
|
2034
|
-
import { basename, isAbsolute, resolve } from "path";
|
|
2126
|
+
import { createHash as createHash5, randomUUID as randomUUID5 } from "crypto";
|
|
2127
|
+
import { readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
2128
|
+
import { basename as basename2, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
|
|
2035
2129
|
import { Client as McpClient } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2036
2130
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
2037
2131
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -2043,12 +2137,563 @@ import {
|
|
|
2043
2137
|
ListToolsRequestSchema
|
|
2044
2138
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
2045
2139
|
|
|
2140
|
+
// src/mcp/file-ingest-intents.ts
|
|
2141
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2142
|
+
import { mkdir as mkdir2, readdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
2143
|
+
import { homedir as homedir2 } from "os";
|
|
2144
|
+
import { join as join2 } from "path";
|
|
2145
|
+
var DELIVERED_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2146
|
+
var DEFAULT_FILE_INGEST_INTENT_DIR = join2(homedir2(), ".pyx-memory", "file-ingests");
|
|
2147
|
+
function parseIntent(raw, file) {
|
|
2148
|
+
const value = JSON.parse(raw);
|
|
2149
|
+
const fp = value?.fingerprint;
|
|
2150
|
+
if (value?.version !== 1 || typeof value.jobId !== "string" || typeof value.endpoint !== "string" || typeof value.delivered !== "boolean" || typeof value.createdAt !== "string" || typeof value.updatedAt !== "string" || typeof fp?.path !== "string" || typeof fp.byteLength !== "number" || typeof fp.sha256 !== "string") {
|
|
2151
|
+
throw new Error(`file ingest intent ${file} is not a version 1 record; remove it to continue`);
|
|
2152
|
+
}
|
|
2153
|
+
return value;
|
|
2154
|
+
}
|
|
2155
|
+
async function readIntents(dir) {
|
|
2156
|
+
let names;
|
|
2157
|
+
try {
|
|
2158
|
+
names = await readdir(dir);
|
|
2159
|
+
} catch (err) {
|
|
2160
|
+
if (err.code === "ENOENT") return [];
|
|
2161
|
+
throw err;
|
|
2162
|
+
}
|
|
2163
|
+
return Promise.all(
|
|
2164
|
+
names.filter((name) => name.endsWith(".json")).map(async (name) => {
|
|
2165
|
+
const file = join2(dir, name);
|
|
2166
|
+
return { file, intent: parseIntent(await readFile(file, "utf8"), file) };
|
|
2167
|
+
})
|
|
2168
|
+
);
|
|
2169
|
+
}
|
|
2170
|
+
function createFileIngestIntentStore(dir = DEFAULT_FILE_INGEST_INTENT_DIR) {
|
|
2171
|
+
return {
|
|
2172
|
+
async findUndelivered(endpoint, path, namespaceId) {
|
|
2173
|
+
const records = await readIntents(dir);
|
|
2174
|
+
const prunedBefore = Date.now() - DELIVERED_RETENTION_MS;
|
|
2175
|
+
await Promise.all(
|
|
2176
|
+
records.filter(({ intent }) => intent.delivered && Date.parse(intent.updatedAt) < prunedBefore).map(({ file }) => rm(file, { force: true }))
|
|
2177
|
+
);
|
|
2178
|
+
return records.map(({ intent }) => intent).filter(
|
|
2179
|
+
(intent) => !intent.delivered && intent.endpoint === endpoint && intent.fingerprint.path === path && intent.fingerprint.namespaceId === namespaceId
|
|
2180
|
+
).sort((a, b) => a.createdAt.localeCompare(b.createdAt))[0];
|
|
2181
|
+
},
|
|
2182
|
+
async save(intent) {
|
|
2183
|
+
await mkdir2(dir, { recursive: true, mode: 448 });
|
|
2184
|
+
const file = join2(dir, `${intent.jobId}.json`);
|
|
2185
|
+
const temp = join2(dir, `.${intent.jobId}.${randomUUID3()}.tmp`);
|
|
2186
|
+
await writeFile(temp, `${JSON.stringify(intent)}
|
|
2187
|
+
`, { mode: 384, flag: "wx" });
|
|
2188
|
+
await rename(temp, file);
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
// src/mcp/hosted-file-ingest.ts
|
|
2194
|
+
import { createHash as createHash4, randomUUID as randomUUID4 } from "crypto";
|
|
2195
|
+
import { createReadStream, openAsBlob } from "fs";
|
|
2196
|
+
import { stat } from "fs/promises";
|
|
2197
|
+
import { basename, isAbsolute, resolve } from "path";
|
|
2198
|
+
import { setTimeout as delay } from "timers/promises";
|
|
2199
|
+
function parseHostedFileIngestDescriptor(meta) {
|
|
2200
|
+
if (meta === null || typeof meta !== "object") return void 0;
|
|
2201
|
+
const value = meta[HOSTED_FILE_INGEST_PROTOCOL.metaKey];
|
|
2202
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
2203
|
+
const { protocolVersion, endpoint, maxBytes } = value;
|
|
2204
|
+
if (protocolVersion !== HOSTED_FILE_INGEST_PROTOCOL.version || typeof endpoint !== "string" || // Same-origin absolute path only: the keychain bearer goes with every request.
|
|
2205
|
+
!endpoint.startsWith("/") || endpoint.startsWith("//") || typeof maxBytes !== "number" || !Number.isSafeInteger(maxBytes) || maxBytes < 1) {
|
|
2206
|
+
return void 0;
|
|
2207
|
+
}
|
|
2208
|
+
return { endpoint, maxBytes };
|
|
2209
|
+
}
|
|
2210
|
+
function toMicrocents(cents) {
|
|
2211
|
+
const microcents = typeof cents === "number" && cents >= 0 ? Math.round(cents * 1e6) : Number.NaN;
|
|
2212
|
+
return Number.isSafeInteger(microcents) ? String(microcents) : void 0;
|
|
2213
|
+
}
|
|
2214
|
+
function parseArgs(args) {
|
|
2215
|
+
const { path, description, namespaceId, maxCostCents, cancel } = args;
|
|
2216
|
+
const maxCostMicrocents = maxCostCents === void 0 ? void 0 : toMicrocents(maxCostCents);
|
|
2217
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
2218
|
+
return "`path` must be a non-empty local file path.";
|
|
2219
|
+
}
|
|
2220
|
+
if (description !== void 0 && typeof description !== "string") {
|
|
2221
|
+
return "`description` must be a string.";
|
|
2222
|
+
}
|
|
2223
|
+
if (namespaceId !== void 0 && (typeof namespaceId !== "string" || namespaceId.length === 0)) {
|
|
2224
|
+
return "`namespaceId` must be a non-empty string.";
|
|
2225
|
+
}
|
|
2226
|
+
if (cancel !== void 0 && typeof cancel !== "boolean") return "`cancel` must be a boolean.";
|
|
2227
|
+
if (maxCostCents !== void 0 && maxCostMicrocents === void 0) {
|
|
2228
|
+
return "`maxCostCents` must be a non-negative number of cents.";
|
|
2229
|
+
}
|
|
2230
|
+
return {
|
|
2231
|
+
path: isAbsolute(path) ? path : resolve(path),
|
|
2232
|
+
description: description || null,
|
|
2233
|
+
namespaceId: namespaceId ?? null,
|
|
2234
|
+
maxCostMicrocents,
|
|
2235
|
+
cancel: cancel === true
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
var CATALOG_STEPS = /* @__PURE__ */ new Set(["begin", "prepare", "commit", "publish"]);
|
|
2239
|
+
var DEADLINE_MS = {
|
|
2240
|
+
begin: 15e3,
|
|
2241
|
+
authorize: 15e3,
|
|
2242
|
+
cancel: 15e3,
|
|
2243
|
+
upload: 12e4,
|
|
2244
|
+
prepare: 9e4,
|
|
2245
|
+
commit: 16e4,
|
|
2246
|
+
publish: 6e4
|
|
2247
|
+
};
|
|
2248
|
+
var RETRY_BUDGET_MS = 5 * 6e4;
|
|
2249
|
+
var DEFAULT_RETRY_AFTER_S = 2;
|
|
2250
|
+
var MAX_RETRY_AFTER_S = 30;
|
|
2251
|
+
var RESUME = "Call this tool again with the same arguments to resume the job.";
|
|
2252
|
+
var NEW_IMPORT = "Call this tool again to start a new import.";
|
|
2253
|
+
var NEXT_BY_CODE = /* @__PURE__ */ new Map([
|
|
2254
|
+
[
|
|
2255
|
+
"runtime_upgrade_required",
|
|
2256
|
+
"The pyx-memory instance needs a runtime upgrade before file ingest can continue; call this tool again with the same arguments once it is upgraded."
|
|
2257
|
+
],
|
|
2258
|
+
[
|
|
2259
|
+
"file_ingest_job_limit",
|
|
2260
|
+
"Your plan allows no more unfinished imports on this instance: finish or cancel another one (cancel: true with its path), then call this tool again with the same arguments."
|
|
2261
|
+
],
|
|
2262
|
+
[
|
|
2263
|
+
"file_ingest_disabled",
|
|
2264
|
+
"Hosted file ingest is switched off on the server; call this tool again with the same arguments later to resume."
|
|
2265
|
+
],
|
|
2266
|
+
[
|
|
2267
|
+
"file_too_large",
|
|
2268
|
+
"The file exceeds your plan's file-size cap: upgrade the plan and call again with the same arguments, or call with cancel: true to drop this import."
|
|
2269
|
+
],
|
|
2270
|
+
[
|
|
2271
|
+
"file_ingest_cancel_requested",
|
|
2272
|
+
"The job is being cancelled: call this tool with cancel: true to confirm."
|
|
2273
|
+
]
|
|
2274
|
+
]);
|
|
2275
|
+
var ENDED_CODES = /* @__PURE__ */ new Set(["file_ingest_cancelled", "file_ingest_expired"]);
|
|
2276
|
+
var COST_CODES = /* @__PURE__ */ new Set([
|
|
2277
|
+
"file_ingest_authorization_required",
|
|
2278
|
+
"file_ingest_ceiling_exceeded",
|
|
2279
|
+
"file_ingest_quota_paused",
|
|
2280
|
+
"file_ingest_limit_exceeded"
|
|
2281
|
+
]);
|
|
2282
|
+
function nextAfter(res) {
|
|
2283
|
+
if (res.status === 402) {
|
|
2284
|
+
return "The wallet balance is too low: top up, then call this tool again with the same arguments to resume.";
|
|
2285
|
+
}
|
|
2286
|
+
return NEXT_BY_CODE.get(res.code ?? "") ?? RESUME;
|
|
2287
|
+
}
|
|
2288
|
+
var errorText = (err) => err instanceof Error ? err.message : String(err);
|
|
2289
|
+
var text = (result) => result.content.map((part) => part.text).join("\n");
|
|
2290
|
+
var fail = (result) => ({ ok: false, result });
|
|
2291
|
+
var refuse = (message) => fail(mcpText(message, true));
|
|
2292
|
+
var label = (intent) => `${basename(intent.fingerprint.path)} (job ${intent.jobId})`;
|
|
2293
|
+
async function hashFile(path) {
|
|
2294
|
+
const hash = createHash4("sha256");
|
|
2295
|
+
let byteLength = 0;
|
|
2296
|
+
for await (const chunk of createReadStream(path)) {
|
|
2297
|
+
hash.update(chunk);
|
|
2298
|
+
byteLength += chunk.length;
|
|
2299
|
+
}
|
|
2300
|
+
return { byteLength, sha256: hash.digest("hex") };
|
|
2301
|
+
}
|
|
2302
|
+
async function matchIntent(intent, args, size) {
|
|
2303
|
+
const fp = intent.fingerprint;
|
|
2304
|
+
const changed = args.description !== fp.description || typeof size === "number" && (size !== fp.byteLength || (await hashFile(args.path)).sha256 !== fp.sha256);
|
|
2305
|
+
if (!changed) return { ok: true, value: intent };
|
|
2306
|
+
return refuse(
|
|
2307
|
+
`${args.path} no longer matches ${label(intent)}: its content or description changed since that import started.
|
|
2308
|
+
Call this tool with cancel: true to abandon job ${intent.jobId}, then call again to import the file as it is now.`
|
|
2309
|
+
);
|
|
2310
|
+
}
|
|
2311
|
+
function isRetryable(res) {
|
|
2312
|
+
return res.status === 429 || res.status === 503 || HOSTED_FILE_INGEST_RETRYABLE_CODES.includes(res.code ?? "");
|
|
2313
|
+
}
|
|
2314
|
+
async function pause(ms, signal) {
|
|
2315
|
+
await delay(ms, void 0, { signal }).catch((err) => {
|
|
2316
|
+
if (!signal?.aborted) throw err;
|
|
2317
|
+
});
|
|
2318
|
+
}
|
|
2319
|
+
function retryAfterMs(header) {
|
|
2320
|
+
const value = header?.trim() ?? "";
|
|
2321
|
+
const seconds = /^\d+$/.test(value) ? Number(value) : DEFAULT_RETRY_AFTER_S;
|
|
2322
|
+
return Math.min(seconds, MAX_RETRY_AFTER_S) * 1e3;
|
|
2323
|
+
}
|
|
2324
|
+
function formatCents(microcents, decimals) {
|
|
2325
|
+
const step = 10n ** BigInt(6 - decimals);
|
|
2326
|
+
const units = (BigInt(microcents) + step - 1n) / step;
|
|
2327
|
+
const scale = 10n ** BigInt(decimals);
|
|
2328
|
+
return `${units / scale}.${String(units % scale).padStart(decimals, "0")}`;
|
|
2329
|
+
}
|
|
2330
|
+
function unwrapEnvelope(body) {
|
|
2331
|
+
if (body === null || typeof body !== "object" || body.success !== true) {
|
|
2332
|
+
throw new Error("the response is not a {success: true, data} envelope");
|
|
2333
|
+
}
|
|
2334
|
+
return body.data;
|
|
2335
|
+
}
|
|
2336
|
+
function createHostedFileIngestDriver(credentials, guard, intents, fetchImpl = fetch) {
|
|
2337
|
+
const http = createHttpClient(credentials, fetchImpl);
|
|
2338
|
+
const origin = new URL(credentials.endpoint).origin;
|
|
2339
|
+
const stopped = (call, step, detail, next = RESUME) => mcpText(`Import of ${label(call.intent)} stopped at ${step}: ${detail}
|
|
2340
|
+
${next}`, true);
|
|
2341
|
+
const interrupted = (call) => refuse(`The call was cancelled; ${label(call.intent)} keeps its progress.
|
|
2342
|
+
${RESUME}`);
|
|
2343
|
+
const published = (call, publication) => mcpText(
|
|
2344
|
+
`Ingested ${call.filename}: ${publication.entryCount} chunks stored (replaced ${publication.retiredCount}). job ${call.intent.jobId}`
|
|
2345
|
+
);
|
|
2346
|
+
async function record(call, state) {
|
|
2347
|
+
if (call.intent.lastState === state) return;
|
|
2348
|
+
call.intent.lastState = state;
|
|
2349
|
+
call.intent.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2350
|
+
await intents.save(call.intent);
|
|
2351
|
+
}
|
|
2352
|
+
async function deliver(call, result) {
|
|
2353
|
+
call.intent.delivered = true;
|
|
2354
|
+
call.intent.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2355
|
+
await intents.save(call.intent);
|
|
2356
|
+
return result;
|
|
2357
|
+
}
|
|
2358
|
+
function accept(call, step, body, parse) {
|
|
2359
|
+
try {
|
|
2360
|
+
return { ok: true, value: parse(unwrapEnvelope(body)) };
|
|
2361
|
+
} catch (err) {
|
|
2362
|
+
return fail(stopped(call, step, `invalid server response: ${errorText(err)}`));
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
async function exchange(call, step, attempt, parse) {
|
|
2366
|
+
for (; ; ) {
|
|
2367
|
+
await guard();
|
|
2368
|
+
const res = await attempt(randomUUID4());
|
|
2369
|
+
if (call.signal?.aborted) return interrupted(call);
|
|
2370
|
+
if (res.ok) return accept(call, step, res.data, parse);
|
|
2371
|
+
const waitMs = retryAfterMs(res.retryAfter);
|
|
2372
|
+
if (!isRetryable(res) || Date.now() + waitMs > call.retryUntil)
|
|
2373
|
+
return refused(call, step, res);
|
|
2374
|
+
await pause(waitMs, call.signal);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
function post(call, step, route, body, parse) {
|
|
2378
|
+
const { namespaceId } = call.intent.fingerprint;
|
|
2379
|
+
const scoped = CATALOG_STEPS.has(step) && namespaceId !== null;
|
|
2380
|
+
return exchange(
|
|
2381
|
+
call,
|
|
2382
|
+
step,
|
|
2383
|
+
(idempotencyKey) => http.requestJson({
|
|
2384
|
+
method: "POST",
|
|
2385
|
+
path: `${call.descriptor.endpoint}${route}`,
|
|
2386
|
+
body: scoped ? { ...body, namespaceId } : body,
|
|
2387
|
+
idempotencyKey,
|
|
2388
|
+
timeoutMs: DEADLINE_MS[step],
|
|
2389
|
+
signal: call.signal
|
|
2390
|
+
}),
|
|
2391
|
+
parse
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
async function upload(call, job) {
|
|
2395
|
+
const token = job.upload?.token;
|
|
2396
|
+
if (token === void 0) {
|
|
2397
|
+
return fail(stopped(call, "upload", "the server issued no upload token"));
|
|
2398
|
+
}
|
|
2399
|
+
const { path, byteLength, sha256 } = call.intent.fingerprint;
|
|
2400
|
+
let body;
|
|
2401
|
+
try {
|
|
2402
|
+
const current = await hashFile(path);
|
|
2403
|
+
body = await openAsBlob(path);
|
|
2404
|
+
if (current.sha256 !== sha256 || current.byteLength !== byteLength || body.size !== byteLength) {
|
|
2405
|
+
throw new Error("it changed since the import started");
|
|
2406
|
+
}
|
|
2407
|
+
} catch (err) {
|
|
2408
|
+
return fail(
|
|
2409
|
+
stopped(
|
|
2410
|
+
call,
|
|
2411
|
+
"upload",
|
|
2412
|
+
`cannot send ${path}: ${errorText(err)}`,
|
|
2413
|
+
`Restore the file and call again to resume, or call with cancel: true to abandon job ${call.intent.jobId}.`
|
|
2414
|
+
)
|
|
2415
|
+
);
|
|
2416
|
+
}
|
|
2417
|
+
return exchange(
|
|
2418
|
+
call,
|
|
2419
|
+
"upload",
|
|
2420
|
+
(idempotencyKey) => http.requestBytes({
|
|
2421
|
+
method: "PUT",
|
|
2422
|
+
path: `${call.descriptor.endpoint}/${call.intent.jobId}/content`,
|
|
2423
|
+
body,
|
|
2424
|
+
headers: { [HOSTED_FILE_INGEST_PROTOCOL.uploadTokenHeader]: token },
|
|
2425
|
+
idempotencyKey,
|
|
2426
|
+
timeoutMs: DEADLINE_MS.upload,
|
|
2427
|
+
signal: call.signal
|
|
2428
|
+
}),
|
|
2429
|
+
parseHostedFileIngestJobView
|
|
2430
|
+
);
|
|
2431
|
+
}
|
|
2432
|
+
async function refused(call, step, res) {
|
|
2433
|
+
const detail = res.code === void 0 ? text(res.result) : `${text(res.result)} (${res.code})`;
|
|
2434
|
+
if (CATALOG_STEPS.has(step) && COST_CODES.has(res.code ?? "")) {
|
|
2435
|
+
return fail(await costRefusal(call, step, detail));
|
|
2436
|
+
}
|
|
2437
|
+
if (step === "prepare" && res.status === 422 || ENDED_CODES.has(res.code ?? "")) {
|
|
2438
|
+
return fail(await deliver(call, stopped(call, step, detail, NEW_IMPORT)));
|
|
2439
|
+
}
|
|
2440
|
+
return { ok: false, status: res.status, result: stopped(call, step, detail, nextAfter(res)) };
|
|
2441
|
+
}
|
|
2442
|
+
async function costRefusal(call, step, detail) {
|
|
2443
|
+
const auth = await post(
|
|
2444
|
+
call,
|
|
2445
|
+
"authorize",
|
|
2446
|
+
`/${call.intent.jobId}/authorize`,
|
|
2447
|
+
{},
|
|
2448
|
+
parseHostedFileIngestAuthorization
|
|
2449
|
+
);
|
|
2450
|
+
if (!auth.ok) return stopped(call, step, detail);
|
|
2451
|
+
await record(call, auth.value.state);
|
|
2452
|
+
const { ceilingMicrocents, estimateMicrocents, settledMicrocents } = auth.value;
|
|
2453
|
+
const cents = (microcents) => microcents === null ? "unknown" : `${formatCents(microcents, 2)} cents`;
|
|
2454
|
+
return await authorizationStop(call, step, auth.value) ?? stopped(
|
|
2455
|
+
call,
|
|
2456
|
+
step,
|
|
2457
|
+
`${detail}. The spending ceiling is ${cents(ceilingMicrocents)}, the estimated total ${cents(estimateMicrocents)}; charged so far ${formatCents(settledMicrocents, 4)} cents.`,
|
|
2458
|
+
`Call this tool again with the same arguments and a maxCostCents above ${cents(ceilingMicrocents)} to continue.`
|
|
2459
|
+
);
|
|
2460
|
+
}
|
|
2461
|
+
async function authorizationStop(call, step, auth, entryCount) {
|
|
2462
|
+
const charged = `Charged so far: ${formatCents(auth.settledMicrocents, 4)} cents.`;
|
|
2463
|
+
const chunks = entryCount === void 0 ? "its chunks" : `its ${entryCount} chunks`;
|
|
2464
|
+
const stop = (detail, next) => stopped(call, step, `${detail} ${charged}`, next);
|
|
2465
|
+
const resume = `call this tool again with the same arguments and maxCostCents of at least ${auth.estimateMicrocents === null ? "the estimate" : formatCents(auth.estimateMicrocents, 2)} to resume`;
|
|
2466
|
+
switch (auth.state) {
|
|
2467
|
+
case "quota_paused":
|
|
2468
|
+
return stop(
|
|
2469
|
+
`the project's memory quota has no room for ${chunks}.`,
|
|
2470
|
+
`Free memory space or upgrade the plan, then ${resume}.`
|
|
2471
|
+
);
|
|
2472
|
+
case "limit_exceeded":
|
|
2473
|
+
return stop(
|
|
2474
|
+
"the document exceeds the plan limits for extracted text or chunks.",
|
|
2475
|
+
`Upgrade the plan, then ${resume}; or call with cancel: true to abandon job ${call.intent.jobId}.`
|
|
2476
|
+
);
|
|
2477
|
+
case "cancel_requested":
|
|
2478
|
+
return stop(
|
|
2479
|
+
"the job is being cancelled.",
|
|
2480
|
+
"Call this tool with cancel: true to confirm the cancellation."
|
|
2481
|
+
);
|
|
2482
|
+
case "cancelled":
|
|
2483
|
+
case "expired":
|
|
2484
|
+
case "failed":
|
|
2485
|
+
return deliver(call, stop(`the job is ${auth.state}.`, NEW_IMPORT));
|
|
2486
|
+
}
|
|
2487
|
+
if (auth.authorized) return void 0;
|
|
2488
|
+
if (auth.estimateMicrocents === null) {
|
|
2489
|
+
return stop("the server has no cost estimate for the job yet.", RESUME);
|
|
2490
|
+
}
|
|
2491
|
+
const estimate = formatCents(auth.estimateMicrocents, 2);
|
|
2492
|
+
const ceiling = auth.ceilingMicrocents === null ? "" : ` The current ceiling is ${formatCents(auth.ceilingMicrocents, 2)} cents.`;
|
|
2493
|
+
return stop(
|
|
2494
|
+
`storing ${chunks} needs your approval: the estimated cost is ${estimate} cents.${ceiling}`,
|
|
2495
|
+
`Call this tool again with the same arguments and maxCostCents of at least ${estimate} to continue.`
|
|
2496
|
+
);
|
|
2497
|
+
}
|
|
2498
|
+
async function commitPages(call, job, manifest) {
|
|
2499
|
+
const committed = new Set(job.committedPages);
|
|
2500
|
+
for (const page2 of manifest.pages) {
|
|
2501
|
+
if (committed.has(page2.pageIndex)) continue;
|
|
2502
|
+
const commit = await post(
|
|
2503
|
+
call,
|
|
2504
|
+
"commit",
|
|
2505
|
+
`/${call.intent.jobId}/commit`,
|
|
2506
|
+
{ manifestSha256: manifest.manifestSha256, page: page2 },
|
|
2507
|
+
parseHostedFileIngestCommitResult
|
|
2508
|
+
);
|
|
2509
|
+
if (!commit.ok) return commit.result;
|
|
2510
|
+
}
|
|
2511
|
+
return void 0;
|
|
2512
|
+
}
|
|
2513
|
+
async function finish(call, job, maxCostMicrocents) {
|
|
2514
|
+
const manifest = job.manifest;
|
|
2515
|
+
if (!manifest) return stopped(call, "prepare", "the prepared job has no manifest");
|
|
2516
|
+
const auth = await post(
|
|
2517
|
+
call,
|
|
2518
|
+
"authorize",
|
|
2519
|
+
`/${call.intent.jobId}/authorize`,
|
|
2520
|
+
maxCostMicrocents === void 0 ? {} : { maxCostMicrocents },
|
|
2521
|
+
parseHostedFileIngestAuthorization
|
|
2522
|
+
);
|
|
2523
|
+
if (!auth.ok) return auth.result;
|
|
2524
|
+
await record(call, auth.value.state);
|
|
2525
|
+
const halted = await authorizationStop(call, "authorize", auth.value, manifest.entryCount) ?? await commitPages(call, job, manifest);
|
|
2526
|
+
if (halted) return halted;
|
|
2527
|
+
const publish = await post(
|
|
2528
|
+
call,
|
|
2529
|
+
"publish",
|
|
2530
|
+
`/${call.intent.jobId}/publish`,
|
|
2531
|
+
{ manifestSha256: manifest.manifestSha256, entryCount: manifest.entryCount },
|
|
2532
|
+
parseHostedFileIngestPublishResult
|
|
2533
|
+
);
|
|
2534
|
+
if (!publish.ok) return publish.result;
|
|
2535
|
+
await record(call, "published");
|
|
2536
|
+
return deliver(call, published(call, publish.value));
|
|
2537
|
+
}
|
|
2538
|
+
async function ended(call, job) {
|
|
2539
|
+
if (job.state === "published") {
|
|
2540
|
+
return job.publication ? deliver(call, published(call, job.publication)) : stopped(call, "begin", "the published job has no publication result");
|
|
2541
|
+
}
|
|
2542
|
+
const what = job.state === "failed" ? `failed: ${job.error ? `${job.error.message} (${job.error.code})` : "no reason given"}` : `is ${job.state}.`;
|
|
2543
|
+
return deliver(call, mcpText(`Import of ${label(call.intent)} ${what}
|
|
2544
|
+
${NEW_IMPORT}`, true));
|
|
2545
|
+
}
|
|
2546
|
+
async function ingest(call, maxCostMicrocents) {
|
|
2547
|
+
const { fingerprint, jobId } = call.intent;
|
|
2548
|
+
let next = await post(
|
|
2549
|
+
call,
|
|
2550
|
+
"begin",
|
|
2551
|
+
"",
|
|
2552
|
+
{
|
|
2553
|
+
jobId,
|
|
2554
|
+
filename: call.filename,
|
|
2555
|
+
byteLength: fingerprint.byteLength,
|
|
2556
|
+
sha256: fingerprint.sha256,
|
|
2557
|
+
...fingerprint.description === null ? {} : { description: fingerprint.description }
|
|
2558
|
+
},
|
|
2559
|
+
parseHostedFileIngestJobView
|
|
2560
|
+
);
|
|
2561
|
+
while (next.ok) {
|
|
2562
|
+
const job = next.value;
|
|
2563
|
+
await record(call, job.state);
|
|
2564
|
+
switch (job.state) {
|
|
2565
|
+
case "awaiting_upload":
|
|
2566
|
+
next = await upload(call, job);
|
|
2567
|
+
break;
|
|
2568
|
+
case "uploaded":
|
|
2569
|
+
next = await post(call, "prepare", `/${jobId}/prepare`, {}, parseHostedFileIngestJobView);
|
|
2570
|
+
break;
|
|
2571
|
+
case "prepared":
|
|
2572
|
+
return finish(call, job, maxCostMicrocents);
|
|
2573
|
+
default:
|
|
2574
|
+
return ended(call, job);
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
return next.result;
|
|
2578
|
+
}
|
|
2579
|
+
async function cancel(call) {
|
|
2580
|
+
const res = await post(
|
|
2581
|
+
call,
|
|
2582
|
+
"cancel",
|
|
2583
|
+
`/${call.intent.jobId}/cancel`,
|
|
2584
|
+
{},
|
|
2585
|
+
parseHostedFileIngestAuthorization
|
|
2586
|
+
);
|
|
2587
|
+
if (!res.ok) {
|
|
2588
|
+
if (res.status !== 404) return res.result;
|
|
2589
|
+
return deliver(
|
|
2590
|
+
call,
|
|
2591
|
+
mcpText(`The server has no ${label(call.intent)}; nothing to cancel.
|
|
2592
|
+
${NEW_IMPORT}`, true)
|
|
2593
|
+
);
|
|
2594
|
+
}
|
|
2595
|
+
const { state, settledMicrocents } = res.value;
|
|
2596
|
+
await record(call, state);
|
|
2597
|
+
const charged = `Charged: ${formatCents(settledMicrocents, 4)} cents.`;
|
|
2598
|
+
switch (state) {
|
|
2599
|
+
case "cancelled":
|
|
2600
|
+
return deliver(
|
|
2601
|
+
call,
|
|
2602
|
+
mcpText(`Cancelled ${label(call.intent)}. ${charged}
|
|
2603
|
+
${NEW_IMPORT}`, true)
|
|
2604
|
+
);
|
|
2605
|
+
case "published":
|
|
2606
|
+
case "expired":
|
|
2607
|
+
case "failed":
|
|
2608
|
+
return deliver(
|
|
2609
|
+
call,
|
|
2610
|
+
mcpText(
|
|
2611
|
+
`${label(call.intent)} was already ${state}; nothing to cancel. ${charged}
|
|
2612
|
+
${NEW_IMPORT}`,
|
|
2613
|
+
true
|
|
2614
|
+
)
|
|
2615
|
+
);
|
|
2616
|
+
default:
|
|
2617
|
+
return mcpText(
|
|
2618
|
+
`Cancel of ${label(call.intent)} is ${state}: it stops once outstanding work settles. ${charged}
|
|
2619
|
+
Call this tool with cancel: true again to confirm.`,
|
|
2620
|
+
true
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
async function openIntent(descriptor, args, existing) {
|
|
2625
|
+
const size = await stat(args.path).then(
|
|
2626
|
+
(info) => info.size,
|
|
2627
|
+
(err) => err
|
|
2628
|
+
);
|
|
2629
|
+
if (existing) return matchIntent(existing, args, size);
|
|
2630
|
+
if (typeof size !== "number") return refuse(`Failed to read ${args.path}: ${size.message}.`);
|
|
2631
|
+
if (size === 0) return refuse(`${args.path} is empty.`);
|
|
2632
|
+
if (size > descriptor.maxBytes) {
|
|
2633
|
+
return refuse(`File too large: ${size} bytes > ${descriptor.maxBytes} cap.`);
|
|
2634
|
+
}
|
|
2635
|
+
try {
|
|
2636
|
+
requireHostedFileIngestFilename(basename(args.path), "the file name");
|
|
2637
|
+
} catch (err) {
|
|
2638
|
+
return refuse(`Cannot import ${args.path}: ${errorText(err)}.`);
|
|
2639
|
+
}
|
|
2640
|
+
const { byteLength, sha256 } = await hashFile(args.path);
|
|
2641
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2642
|
+
const intent = {
|
|
2643
|
+
version: 1,
|
|
2644
|
+
jobId: randomUUID4(),
|
|
2645
|
+
endpoint: origin,
|
|
2646
|
+
fingerprint: {
|
|
2647
|
+
path: args.path,
|
|
2648
|
+
byteLength,
|
|
2649
|
+
sha256,
|
|
2650
|
+
description: args.description,
|
|
2651
|
+
namespaceId: args.namespaceId
|
|
2652
|
+
},
|
|
2653
|
+
lastState: null,
|
|
2654
|
+
delivered: false,
|
|
2655
|
+
createdAt: now,
|
|
2656
|
+
updatedAt: now
|
|
2657
|
+
};
|
|
2658
|
+
await intents.save(intent);
|
|
2659
|
+
return { ok: true, value: intent };
|
|
2660
|
+
}
|
|
2661
|
+
return async (descriptor, rawArgs, signal) => {
|
|
2662
|
+
const args = parseArgs(rawArgs);
|
|
2663
|
+
if (typeof args === "string") return mcpText(args, true);
|
|
2664
|
+
let intent;
|
|
2665
|
+
const callOn = (value) => ({
|
|
2666
|
+
descriptor,
|
|
2667
|
+
intent: value,
|
|
2668
|
+
filename: basename(value.fingerprint.path),
|
|
2669
|
+
signal,
|
|
2670
|
+
retryUntil: Date.now() + RETRY_BUDGET_MS
|
|
2671
|
+
});
|
|
2672
|
+
try {
|
|
2673
|
+
await guard();
|
|
2674
|
+
const existing = await intents.findUndelivered(origin, args.path, args.namespaceId);
|
|
2675
|
+
if (args.cancel) {
|
|
2676
|
+
if (!existing) return mcpText(`No hosted import of ${args.path} to cancel.`, true);
|
|
2677
|
+
intent = existing;
|
|
2678
|
+
return await cancel(callOn(intent));
|
|
2679
|
+
}
|
|
2680
|
+
const opened = await openIntent(descriptor, args, existing);
|
|
2681
|
+
if (!opened.ok) return opened.result;
|
|
2682
|
+
intent = opened.value;
|
|
2683
|
+
return await ingest(callOn(intent), args.maxCostMicrocents);
|
|
2684
|
+
} catch (err) {
|
|
2685
|
+
return mcpText(intent ? `${errorText(err)}
|
|
2686
|
+
job ${intent.jobId}` : errorText(err), true);
|
|
2687
|
+
}
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2046
2691
|
// src/mcp/tools/shared.ts
|
|
2047
2692
|
var LOCAL_UPLOAD_META_KEY = "pyx.dev/local-upload";
|
|
2048
2693
|
|
|
2049
2694
|
// src/mcp/proxy-server.ts
|
|
2050
2695
|
var REMOTE_MCP_PATH = "/mcp";
|
|
2051
|
-
function createMcpIdempotentFetch(fetchImpl = fetch, sessionNonce =
|
|
2696
|
+
function createMcpIdempotentFetch(fetchImpl = fetch, sessionNonce = randomUUID5()) {
|
|
2052
2697
|
const wrapped = async (input, init) => {
|
|
2053
2698
|
const upstream = input instanceof Request ? input : new Request(String(input), init);
|
|
2054
2699
|
const method = init?.method ?? upstream.method;
|
|
@@ -2058,7 +2703,7 @@ function createMcpIdempotentFetch(fetchImpl = fetch, sessionNonce = randomUUID3(
|
|
|
2058
2703
|
if (typeof body !== "string") {
|
|
2059
2704
|
throw new Error("remote MCP POST body must be text for stable request idempotency");
|
|
2060
2705
|
}
|
|
2061
|
-
const key =
|
|
2706
|
+
const key = createHash5("sha256").update("pyx-mcp-idempotency-v1\0").update(sessionNonce).update("\0").update(body).digest("hex");
|
|
2062
2707
|
headers.set("Idempotency-Key", `mcp-${key}`);
|
|
2063
2708
|
}
|
|
2064
2709
|
return fetchImpl(input, { ...init, method, headers, body });
|
|
@@ -2111,10 +2756,10 @@ async function readCappedLocalFile(rawPath, descriptor) {
|
|
|
2111
2756
|
result: mcpText(`\`${descriptor.pathArg}\` must be a non-empty local file path.`, true)
|
|
2112
2757
|
};
|
|
2113
2758
|
}
|
|
2114
|
-
const abs =
|
|
2759
|
+
const abs = isAbsolute2(rawPath) ? rawPath : resolve2(rawPath);
|
|
2115
2760
|
try {
|
|
2116
2761
|
if (typeof descriptor.maxBytes === "number") {
|
|
2117
|
-
const info = await
|
|
2762
|
+
const info = await stat2(abs);
|
|
2118
2763
|
if (info.size > descriptor.maxBytes) {
|
|
2119
2764
|
return {
|
|
2120
2765
|
ok: false,
|
|
@@ -2122,7 +2767,7 @@ async function readCappedLocalFile(rawPath, descriptor) {
|
|
|
2122
2767
|
};
|
|
2123
2768
|
}
|
|
2124
2769
|
}
|
|
2125
|
-
return { ok: true, bytes: await
|
|
2770
|
+
return { ok: true, bytes: await readFile2(abs), name: basename2(abs) };
|
|
2126
2771
|
} catch (err) {
|
|
2127
2772
|
return {
|
|
2128
2773
|
ok: false,
|
|
@@ -2160,7 +2805,7 @@ function createLocalFileUploader(credentials, readCredentials, fetchImpl = fetch
|
|
|
2160
2805
|
}
|
|
2161
2806
|
};
|
|
2162
2807
|
}
|
|
2163
|
-
function createProxyServer(client, version, uploadLocalFile) {
|
|
2808
|
+
function createProxyServer(client, version, uploadLocalFile, hostedFileIngest) {
|
|
2164
2809
|
const caps = client.getServerCapabilities();
|
|
2165
2810
|
const server = new Server(client.getServerVersion() ?? { name: "pyx-memory", version }, {
|
|
2166
2811
|
capabilities: {
|
|
@@ -2173,39 +2818,48 @@ function createProxyServer(client, version, uploadLocalFile) {
|
|
|
2173
2818
|
},
|
|
2174
2819
|
instructions: client.getInstructions()
|
|
2175
2820
|
});
|
|
2176
|
-
const
|
|
2821
|
+
const bridges = /* @__PURE__ */ new Map();
|
|
2822
|
+
const bridging = uploadLocalFile !== void 0 || hostedFileIngest !== void 0;
|
|
2823
|
+
const bridgeOf = (meta) => {
|
|
2824
|
+
const hosted = hostedFileIngest ? parseHostedFileIngestDescriptor(meta) : void 0;
|
|
2825
|
+
if (hosted) return { kind: "hosted", descriptor: hosted };
|
|
2826
|
+
const local = uploadLocalFile ? extractLocalUpload(meta) : void 0;
|
|
2827
|
+
return local ? { kind: "local", descriptor: local } : void 0;
|
|
2828
|
+
};
|
|
2177
2829
|
let listedOnce = false;
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2830
|
+
let listingsStarted = 0;
|
|
2831
|
+
let listingApplied = 0;
|
|
2832
|
+
const listAndRefresh = async (params) => {
|
|
2833
|
+
const listing = ++listingsStarted;
|
|
2834
|
+
const result = await client.listTools(params);
|
|
2835
|
+
const { tools, nextCursor } = result;
|
|
2836
|
+
if (!Array.isArray(tools) || listing < listingApplied) return result;
|
|
2837
|
+
listingApplied = listing;
|
|
2838
|
+
listedOnce = true;
|
|
2839
|
+
if (params?.cursor === void 0 && nextCursor === void 0) bridges.clear();
|
|
2840
|
+
for (const tool of tools) {
|
|
2841
|
+
const bridge = bridgeOf(tool._meta);
|
|
2842
|
+
if (bridge) bridges.set(tool.name, bridge);
|
|
2843
|
+
else bridges.delete(tool.name);
|
|
2187
2844
|
}
|
|
2188
2845
|
return result;
|
|
2189
2846
|
};
|
|
2190
2847
|
if (caps?.tools) {
|
|
2191
|
-
server.setRequestHandler(
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
);
|
|
2195
|
-
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
2196
|
-
if (uploadLocalFile && !listedOnce) {
|
|
2848
|
+
server.setRequestHandler(ListToolsRequestSchema, (req) => listAndRefresh(req.params));
|
|
2849
|
+
server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
|
|
2850
|
+
if (bridging && !listedOnce) {
|
|
2197
2851
|
try {
|
|
2198
|
-
|
|
2852
|
+
await listAndRefresh();
|
|
2199
2853
|
} catch {
|
|
2200
2854
|
}
|
|
2201
2855
|
}
|
|
2202
|
-
const
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
return
|
|
2856
|
+
const bridge = bridges.get(req.params.name);
|
|
2857
|
+
const args = req.params.arguments ?? {};
|
|
2858
|
+
if (bridge?.kind === "hosted" && hostedFileIngest) {
|
|
2859
|
+
return await hostedFileIngest(bridge.descriptor, args, extra.signal);
|
|
2860
|
+
}
|
|
2861
|
+
if (bridge?.kind === "local" && uploadLocalFile) {
|
|
2862
|
+
return await uploadLocalFile(bridge.descriptor, args);
|
|
2209
2863
|
}
|
|
2210
2864
|
return client.callTool(req.params);
|
|
2211
2865
|
});
|
|
@@ -2223,7 +2877,7 @@ function createProxyServer(client, version, uploadLocalFile) {
|
|
|
2223
2877
|
return server;
|
|
2224
2878
|
}
|
|
2225
2879
|
async function runMcpProxyServer(opts) {
|
|
2226
|
-
const version = opts.version ?? (true ? "1.
|
|
2880
|
+
const version = opts.version ?? (true ? "1.24.0" : "0.0.0-dev");
|
|
2227
2881
|
const read = await opts.readCredentials();
|
|
2228
2882
|
if (!read.ok) {
|
|
2229
2883
|
throw new Error(
|
|
@@ -2232,12 +2886,11 @@ async function runMcpProxyServer(opts) {
|
|
|
2232
2886
|
}
|
|
2233
2887
|
const { endpoint, apiKey } = read.credentials;
|
|
2234
2888
|
const url = new URL(`${endpoint.replace(/\/+$/, "")}${REMOTE_MCP_PATH}`);
|
|
2889
|
+
const guard = createCredentialGuard(opts.readCredentials, read.credentials);
|
|
2235
2890
|
const client = new McpClient({ name: "pyx-mem-proxy", version }, { capabilities: {} });
|
|
2236
2891
|
const clientTransport = new StreamableHTTPClientTransport(url, {
|
|
2237
2892
|
requestInit: { headers: { Authorization: `Bearer ${apiKey}` } },
|
|
2238
|
-
fetch: createMcpIdempotentFetch(
|
|
2239
|
-
createGuardedFetch(createCredentialGuard(opts.readCredentials, read.credentials))
|
|
2240
|
-
)
|
|
2893
|
+
fetch: createMcpIdempotentFetch(createGuardedFetch(guard))
|
|
2241
2894
|
});
|
|
2242
2895
|
try {
|
|
2243
2896
|
await client.connect(clientTransport);
|
|
@@ -2251,11 +2904,12 @@ async function runMcpProxyServer(opts) {
|
|
|
2251
2904
|
const server = createProxyServer(
|
|
2252
2905
|
client,
|
|
2253
2906
|
version,
|
|
2254
|
-
createLocalFileUploader(read.credentials, opts.readCredentials)
|
|
2907
|
+
createLocalFileUploader(read.credentials, opts.readCredentials),
|
|
2908
|
+
createHostedFileIngestDriver(read.credentials, guard, createFileIngestIntentStore())
|
|
2255
2909
|
);
|
|
2256
2910
|
const transport = new StdioServerTransport();
|
|
2257
|
-
const closed = new Promise((
|
|
2258
|
-
transport.onclose = () =>
|
|
2911
|
+
const closed = new Promise((resolve4) => {
|
|
2912
|
+
transport.onclose = () => resolve4();
|
|
2259
2913
|
});
|
|
2260
2914
|
process.stdin.once("end", () => void transport.close());
|
|
2261
2915
|
await server.connect(transport);
|
|
@@ -2270,8 +2924,8 @@ async function runMcpProxyServer(opts) {
|
|
|
2270
2924
|
// src/cli/commands/mcp-install.ts
|
|
2271
2925
|
import { spawnSync as nodeSpawnSync } from "child_process";
|
|
2272
2926
|
import { existsSync as existsSync3 } from "fs";
|
|
2273
|
-
import { homedir as
|
|
2274
|
-
import { join as
|
|
2927
|
+
import { homedir as homedir3 } from "os";
|
|
2928
|
+
import { join as join3 } from "path";
|
|
2275
2929
|
|
|
2276
2930
|
// src/cli/commands/mcp-install-merge.ts
|
|
2277
2931
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
@@ -2384,12 +3038,12 @@ function recordsEqual(a, b) {
|
|
|
2384
3038
|
}
|
|
2385
3039
|
return true;
|
|
2386
3040
|
}
|
|
2387
|
-
function indent(
|
|
2388
|
-
return
|
|
3041
|
+
function indent(text2) {
|
|
3042
|
+
return text2.split("\n").map((line) => ` ${line}`).join("\n");
|
|
2389
3043
|
}
|
|
2390
3044
|
|
|
2391
3045
|
// src/cli/commands/memory-rules-merge.ts
|
|
2392
|
-
import { randomUUID as
|
|
3046
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
2393
3047
|
import {
|
|
2394
3048
|
chmodSync,
|
|
2395
3049
|
existsSync as existsSync2,
|
|
@@ -2586,7 +3240,7 @@ function writeMemoryRulesFile(opts) {
|
|
|
2586
3240
|
const parent = dirname3(target);
|
|
2587
3241
|
if (!existsSync2(parent)) mkdirSync2(parent, { recursive: true });
|
|
2588
3242
|
const mode = fileExists ? statSync(target).mode & 511 : 420;
|
|
2589
|
-
const tmpPath = `${target}.${process.pid}.${
|
|
3243
|
+
const tmpPath = `${target}.${process.pid}.${randomUUID6()}.tmp`;
|
|
2590
3244
|
try {
|
|
2591
3245
|
writeFileSync2(tmpPath, merged.content, { mode });
|
|
2592
3246
|
chmodSync(tmpPath, mode);
|
|
@@ -2671,7 +3325,7 @@ function mcpInstallCodexCommand(opts = {}) {
|
|
|
2671
3325
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
2672
3326
|
const remote = opts.remote ?? false;
|
|
2673
3327
|
const spawnSync = opts._spawnSync ?? nodeSpawnSync;
|
|
2674
|
-
const home = opts._homeDir ??
|
|
3328
|
+
const home = opts._homeDir ?? homedir3();
|
|
2675
3329
|
const codexHome = (opts._env ?? process.env).CODEX_HOME;
|
|
2676
3330
|
const probe = spawnSync("codex", ["--version"], { stdio: "pipe" });
|
|
2677
3331
|
if (probe.error || probe.status !== 0) {
|
|
@@ -2702,7 +3356,7 @@ Restart Codex CLI to make the tools available. No API key was written to config.
|
|
|
2702
3356
|
return EXIT.OK;
|
|
2703
3357
|
}
|
|
2704
3358
|
function printCodexManualInstructions(home, remote, codexHome) {
|
|
2705
|
-
const configPath =
|
|
3359
|
+
const configPath = join3(codexHome ?? join3(home, ".codex"), "config.toml");
|
|
2706
3360
|
process.stdout.write(
|
|
2707
3361
|
[
|
|
2708
3362
|
"The `codex` CLI is not on PATH, or the install command failed.",
|
|
@@ -2725,8 +3379,8 @@ function printCodexManualInstructions(home, remote, codexHome) {
|
|
|
2725
3379
|
function mcpInstallCursorCommand(opts = {}) {
|
|
2726
3380
|
const scope = opts.scope ?? "user";
|
|
2727
3381
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
2728
|
-
const home = opts._homeDir ??
|
|
2729
|
-
const filePath = scope === "project" ?
|
|
3382
|
+
const home = opts._homeDir ?? homedir3();
|
|
3383
|
+
const filePath = scope === "project" ? join3(process.cwd(), ".cursor", "mcp.json") : join3(home, ".cursor", "mcp.json");
|
|
2730
3384
|
return writeJsonAndReport(filePath, "Cursor", mcpEntry(opts.remote ?? false));
|
|
2731
3385
|
}
|
|
2732
3386
|
function mcpInstallClineCommand(opts = {}) {
|
|
@@ -2757,8 +3411,8 @@ function mcpInstallContinueCommand(opts = {}) {
|
|
|
2757
3411
|
const scope = opts.scope ?? "user";
|
|
2758
3412
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
2759
3413
|
const remote = opts.remote ?? false;
|
|
2760
|
-
const home = opts._homeDir ??
|
|
2761
|
-
const yamlPath =
|
|
3414
|
+
const home = opts._homeDir ?? homedir3();
|
|
3415
|
+
const yamlPath = join3(home, ".continue", "mcpServers", `${SERVER_NAME}.yaml`);
|
|
2762
3416
|
process.stdout.write(
|
|
2763
3417
|
[
|
|
2764
3418
|
"Continue uses YAML config. The recommended way to add an MCP server is a",
|
|
@@ -2789,8 +3443,8 @@ function mcpInstallContinueCommand(opts = {}) {
|
|
|
2789
3443
|
function mcpInstallWindsurfCommand(opts = {}) {
|
|
2790
3444
|
const scope = opts.scope ?? "user";
|
|
2791
3445
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
2792
|
-
const home = opts._homeDir ??
|
|
2793
|
-
const filePath =
|
|
3446
|
+
const home = opts._homeDir ?? homedir3();
|
|
3447
|
+
const filePath = join3(home, ".codeium", "windsurf", "mcp_config.json");
|
|
2794
3448
|
return writeJsonAndReport(filePath, "Windsurf", mcpEntry(opts.remote ?? false));
|
|
2795
3449
|
}
|
|
2796
3450
|
function mcpInstallGeminiCliCommand(opts = {}) {
|
|
@@ -2798,7 +3452,7 @@ function mcpInstallGeminiCliCommand(opts = {}) {
|
|
|
2798
3452
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
2799
3453
|
const remote = opts.remote ?? false;
|
|
2800
3454
|
const spawnSync = opts._spawnSync ?? nodeSpawnSync;
|
|
2801
|
-
const home = opts._homeDir ??
|
|
3455
|
+
const home = opts._homeDir ?? homedir3();
|
|
2802
3456
|
const probe = spawnSync("gemini", ["--version"], { stdio: "pipe" });
|
|
2803
3457
|
if (probe.error || probe.status !== 0) {
|
|
2804
3458
|
return writeJsonAndReport(geminiConfigPath(home, scope), "Gemini CLI", mcpEntry(remote));
|
|
@@ -2819,13 +3473,13 @@ Restart Gemini CLI to make the tools available. No API key was written to settin
|
|
|
2819
3473
|
return EXIT.OK;
|
|
2820
3474
|
}
|
|
2821
3475
|
function geminiConfigPath(home, scope) {
|
|
2822
|
-
return scope === "project" ?
|
|
3476
|
+
return scope === "project" ? join3(process.cwd(), ".gemini", "settings.json") : join3(home, ".gemini", "settings.json");
|
|
2823
3477
|
}
|
|
2824
3478
|
function mcpInstallPiCommand(opts = {}) {
|
|
2825
3479
|
const scope = opts.scope ?? "user";
|
|
2826
3480
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
2827
|
-
const home = opts._homeDir ??
|
|
2828
|
-
const filePath = scope === "user" ?
|
|
3481
|
+
const home = opts._homeDir ?? homedir3();
|
|
3482
|
+
const filePath = scope === "user" ? join3(home, ".pi", "agent", "mcp.json") : join3(process.cwd(), ".pi", "mcp.json");
|
|
2829
3483
|
return writeJsonAndReport(filePath, "Pi", mcpEntry(opts.remote ?? false), {
|
|
2830
3484
|
prereq: "pi has no native MCP support \u2014 install the third-party adapter once:\n pi install npm:pi-mcp-adapter"
|
|
2831
3485
|
});
|
|
@@ -2833,8 +3487,8 @@ function mcpInstallPiCommand(opts = {}) {
|
|
|
2833
3487
|
function mcpInstallOhMyPiCommand(opts = {}) {
|
|
2834
3488
|
const scope = opts.scope ?? "user";
|
|
2835
3489
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
2836
|
-
const home = opts._homeDir ??
|
|
2837
|
-
const filePath = scope === "user" ?
|
|
3490
|
+
const home = opts._homeDir ?? homedir3();
|
|
3491
|
+
const filePath = scope === "user" ? join3(home, ".omp", "agent", "mcp.json") : join3(process.cwd(), ".omp", "mcp.json");
|
|
2838
3492
|
return writeJsonAndReport(filePath, "oh-my-pi", mcpEntry(opts.remote ?? false));
|
|
2839
3493
|
}
|
|
2840
3494
|
function mcpInstallOpenClawCommand(opts = {}) {
|
|
@@ -2942,7 +3596,7 @@ function rulesFilePath(target, opts) {
|
|
|
2942
3596
|
const spec = RULES_FILE_TARGETS[target];
|
|
2943
3597
|
const env = opts._env ?? (opts._homeDir === void 0 ? process.env : {});
|
|
2944
3598
|
const configDir = spec.envDir ? env[spec.envDir] : void 0;
|
|
2945
|
-
return configDir ?
|
|
3599
|
+
return configDir ? join3(configDir, spec.file) : join3(opts._homeDir ?? homedir3(), ...spec.dir, spec.file);
|
|
2946
3600
|
}
|
|
2947
3601
|
function installMemoryRules(opts) {
|
|
2948
3602
|
const filePath = rulesFilePath(opts.target, opts);
|
|
@@ -3006,8 +3660,8 @@ async function syncMemoryRulesFromServer(client, opts = {}) {
|
|
|
3006
3660
|
);
|
|
3007
3661
|
}
|
|
3008
3662
|
}
|
|
3009
|
-
function isRulesSection(
|
|
3010
|
-
return
|
|
3663
|
+
function isRulesSection(text2) {
|
|
3664
|
+
return text2.startsWith("## Persistent Memory\n") && Buffer.byteLength(text2, "utf8") <= MAX_SERVER_RULES_BYTES && !text2.includes("<!-- pyx-memory:") && !/^\s*(```|~~~)/m.test(text2);
|
|
3011
3665
|
}
|
|
3012
3666
|
function validateScope(scope) {
|
|
3013
3667
|
if (VALID_SCOPES.has(scope)) return true;
|
|
@@ -3140,7 +3794,7 @@ function boundedLabel(value, fallback, maxChars) {
|
|
|
3140
3794
|
|
|
3141
3795
|
// src/cli/commands/scaffold.ts
|
|
3142
3796
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
3143
|
-
import { basename as
|
|
3797
|
+
import { basename as basename3, join as join4, resolve as resolve3 } from "path";
|
|
3144
3798
|
var SERVER_IMAGE = "ghcr.io/pyx-corp/pyx-memory-v1:latest";
|
|
3145
3799
|
var DOCKER_COMPOSE = `services:
|
|
3146
3800
|
pyx-memory:
|
|
@@ -3263,17 +3917,17 @@ function resolveTarget(args) {
|
|
|
3263
3917
|
process.stderr.write("Error: --name requires a non-empty directory name.\n");
|
|
3264
3918
|
return null;
|
|
3265
3919
|
}
|
|
3266
|
-
const targetDir = trimmedName ?
|
|
3267
|
-
const appName = trimmedName ||
|
|
3920
|
+
const targetDir = trimmedName ? resolve3(cwd, trimmedName) : cwd;
|
|
3921
|
+
const appName = trimmedName || basename3(resolve3(cwd));
|
|
3268
3922
|
return { targetDir, appName };
|
|
3269
3923
|
}
|
|
3270
3924
|
function buildFiles(targetDir, appName) {
|
|
3271
3925
|
return [
|
|
3272
|
-
{ path:
|
|
3273
|
-
{ path:
|
|
3274
|
-
{ path:
|
|
3926
|
+
{ path: join4(targetDir, "docker-compose.yml"), contents: DOCKER_COMPOSE },
|
|
3927
|
+
{ path: join4(targetDir, ".env.example"), contents: ENV_EXAMPLE },
|
|
3928
|
+
{ path: join4(targetDir, "memory.ts"), contents: MEMORY_TS },
|
|
3275
3929
|
{
|
|
3276
|
-
path:
|
|
3930
|
+
path: join4(targetDir, "PYX_MEMORY_DESIGN_GUIDE.md"),
|
|
3277
3931
|
contents: buildDesignGuide({ appName })
|
|
3278
3932
|
}
|
|
3279
3933
|
];
|
|
@@ -3290,7 +3944,7 @@ function scaffoldCommand(args = {}) {
|
|
|
3290
3944
|
const created = [];
|
|
3291
3945
|
const skipped = [];
|
|
3292
3946
|
for (const file of buildFiles(target.targetDir, target.appName)) {
|
|
3293
|
-
const relativePath =
|
|
3947
|
+
const relativePath = basename3(file.path);
|
|
3294
3948
|
if (existsSync4(file.path)) {
|
|
3295
3949
|
skipped.push(relativePath);
|
|
3296
3950
|
continue;
|
|
@@ -3511,7 +4165,7 @@ Notes:
|
|
|
3511
4165
|
|
|
3512
4166
|
// src/cli/parser.ts
|
|
3513
4167
|
var COMMANDS_WITH_SUBCOMMAND = /* @__PURE__ */ new Set(["mcp"]);
|
|
3514
|
-
function
|
|
4168
|
+
function parseArgs2(argv) {
|
|
3515
4169
|
const flags = /* @__PURE__ */ Object.create(null);
|
|
3516
4170
|
const positional = [];
|
|
3517
4171
|
let i = 0;
|
|
@@ -3642,7 +4296,7 @@ function runLogin(parsed) {
|
|
|
3642
4296
|
});
|
|
3643
4297
|
}
|
|
3644
4298
|
async function main() {
|
|
3645
|
-
const parsed =
|
|
4299
|
+
const parsed = parseArgs2(process.argv.slice(2));
|
|
3646
4300
|
if (parsed.flags.help === true || parsed.flags.h === true) {
|
|
3647
4301
|
process.stdout.write(HELP_TEXT);
|
|
3648
4302
|
return EXIT.OK;
|