@pyxmate/memory 1.23.2 → 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/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 +751 -139
- 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 +1 -1
- 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,37 +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
|
-
let text;
|
|
911
|
-
try {
|
|
912
|
-
text = await res.text();
|
|
913
|
-
} catch (err) {
|
|
914
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
915
|
-
return {
|
|
916
|
-
ok: false,
|
|
917
|
-
status: res.status,
|
|
918
|
-
result: mcpEnvelopeError(
|
|
919
|
-
`Failed to read pyx-memory response (HTTP ${res.status}): ${reason}`
|
|
920
|
-
)
|
|
921
|
-
};
|
|
922
|
-
}
|
|
923
|
-
return parseHttpResponse(res, text);
|
|
924
932
|
}
|
|
925
933
|
return {
|
|
926
934
|
async requestJson(input) {
|
|
@@ -931,46 +939,79 @@ function createHttpClient(credentials, fetchImpl = fetch) {
|
|
|
931
939
|
hasBody ? "application/json" : void 0,
|
|
932
940
|
input.idempotencyKey
|
|
933
941
|
);
|
|
934
|
-
return send(
|
|
942
|
+
return send(
|
|
943
|
+
url,
|
|
944
|
+
input.method,
|
|
945
|
+
headers,
|
|
946
|
+
hasBody ? JSON.stringify(input.body) : void 0,
|
|
947
|
+
input
|
|
948
|
+
);
|
|
935
949
|
},
|
|
936
950
|
async requestMultipart(input) {
|
|
937
951
|
const url = buildUrl(input.path);
|
|
938
952
|
const headers = buildHeaders(input.scope, void 0, input.idempotencyKey);
|
|
939
953
|
const form = await input.formData();
|
|
940
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);
|
|
941
960
|
}
|
|
942
961
|
};
|
|
943
962
|
}
|
|
944
|
-
function
|
|
945
|
-
if (res.status
|
|
946
|
-
return { ok: false, status: res.status, result:
|
|
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
|
+
};
|
|
947
979
|
}
|
|
980
|
+
return parseHttpResponse(res, text2);
|
|
981
|
+
}
|
|
982
|
+
function parseHttpResponse(res, text2) {
|
|
948
983
|
if (res.ok && res.headers.get("content-type")?.includes("application/x-ndjson")) {
|
|
949
|
-
return parseNdjsonOutcome(res.status,
|
|
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
|
+
};
|
|
994
|
+
if (res.status >= 500) {
|
|
995
|
+
return { ...failure2, result: mcpServerError(res.status, gateway?.message ?? text2) };
|
|
950
996
|
}
|
|
951
|
-
const parsed = parseJsonText(text);
|
|
952
997
|
if (!parsed.ok) {
|
|
953
998
|
if (!res.ok) {
|
|
954
|
-
return {
|
|
955
|
-
ok: false,
|
|
956
|
-
status: res.status,
|
|
957
|
-
result: mcpHttpError(res.status, text || "no body")
|
|
958
|
-
};
|
|
999
|
+
return { ...failure2, result: mcpHttpError(res.status, text2 || "no body") };
|
|
959
1000
|
}
|
|
960
|
-
return { ok: true, status: res.status, data:
|
|
1001
|
+
return { ok: true, status: res.status, data: text2 };
|
|
961
1002
|
}
|
|
962
1003
|
if (!res.ok) {
|
|
963
|
-
const message = extractEnvelopeError(parsed.value) ??
|
|
964
|
-
return {
|
|
1004
|
+
const message = extractEnvelopeError(parsed.value) ?? gateway?.message ?? text2 ?? `HTTP ${res.status}`;
|
|
1005
|
+
return { ...failure2, result: mcpHttpError(res.status, message) };
|
|
965
1006
|
}
|
|
966
1007
|
const envelopeError = extractEnvelopeError(parsed.value);
|
|
967
1008
|
if (envelopeError) {
|
|
968
|
-
return {
|
|
1009
|
+
return { ...failure2, result: mcpEnvelopeError(envelopeError) };
|
|
969
1010
|
}
|
|
970
1011
|
return { ok: true, status: res.status, data: parsed.value };
|
|
971
1012
|
}
|
|
972
|
-
function parseNdjsonOutcome(status,
|
|
973
|
-
for (const line of
|
|
1013
|
+
function parseNdjsonOutcome(status, text2) {
|
|
1014
|
+
for (const line of text2.split("\n")) {
|
|
974
1015
|
if (!line.trim()) continue;
|
|
975
1016
|
let frame;
|
|
976
1017
|
try {
|
|
@@ -994,14 +1035,27 @@ function parseNdjsonOutcome(status, text) {
|
|
|
994
1035
|
result: mcpEnvelopeError("File ingest stream ended without a server result")
|
|
995
1036
|
};
|
|
996
1037
|
}
|
|
997
|
-
function parseJsonText(
|
|
998
|
-
if (
|
|
1038
|
+
function parseJsonText(text2) {
|
|
1039
|
+
if (text2.length === 0) return { ok: true, value: void 0 };
|
|
999
1040
|
try {
|
|
1000
|
-
return { ok: true, value: JSON.parse(
|
|
1041
|
+
return { ok: true, value: JSON.parse(text2) };
|
|
1001
1042
|
} catch {
|
|
1002
1043
|
return { ok: false };
|
|
1003
1044
|
}
|
|
1004
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
|
+
}
|
|
1005
1059
|
function extractEnvelopeError(parsed) {
|
|
1006
1060
|
if (parsed === null || typeof parsed !== "object") return null;
|
|
1007
1061
|
const obj = parsed;
|
|
@@ -1280,9 +1334,9 @@ async function readApiKeySecret() {
|
|
|
1280
1334
|
}
|
|
1281
1335
|
return readSingleLineFromStdin();
|
|
1282
1336
|
}
|
|
1283
|
-
async function promptMasked(
|
|
1337
|
+
async function promptMasked(label2) {
|
|
1284
1338
|
const { stdin, stdout } = process;
|
|
1285
|
-
stdout.write(
|
|
1339
|
+
stdout.write(label2);
|
|
1286
1340
|
const hadRawMode = typeof stdin.setRawMode === "function";
|
|
1287
1341
|
if (!hadRawMode) {
|
|
1288
1342
|
const value = await readSingleLineFromStdin();
|
|
@@ -1290,7 +1344,7 @@ async function promptMasked(label) {
|
|
|
1290
1344
|
`);
|
|
1291
1345
|
return value;
|
|
1292
1346
|
}
|
|
1293
|
-
return new Promise((
|
|
1347
|
+
return new Promise((resolve4, reject) => {
|
|
1294
1348
|
const previousEncoding = stdin.readableEncoding;
|
|
1295
1349
|
stdin.setEncoding("utf8");
|
|
1296
1350
|
stdin.setRawMode(true);
|
|
@@ -1308,7 +1362,7 @@ async function promptMasked(label) {
|
|
|
1308
1362
|
if (ch === "\n" || ch === "\r") {
|
|
1309
1363
|
cleanup();
|
|
1310
1364
|
stdout.write("\n");
|
|
1311
|
-
|
|
1365
|
+
resolve4(buf.trim());
|
|
1312
1366
|
return;
|
|
1313
1367
|
}
|
|
1314
1368
|
if (ch === "") {
|
|
@@ -1479,7 +1533,7 @@ p { margin: 0; color: #a9adb8; font-size: 16px; line-height: 1.65; }
|
|
|
1479
1533
|
@media (prefers-reduced-motion: reduce) { .continue .symbol { animation: none; } }
|
|
1480
1534
|
`;
|
|
1481
1535
|
var PAGE_CSP = `default-src 'none'; style-src 'sha256-${createHash2("sha256").update(STYLES).digest("base64")}'; base-uri 'none'; form-action 'none'`;
|
|
1482
|
-
var page = (state, title,
|
|
1536
|
+
var page = (state, title, text2, icon) => `<!doctype html>
|
|
1483
1537
|
<html lang="en">
|
|
1484
1538
|
<head>
|
|
1485
1539
|
<meta charset="utf-8">
|
|
@@ -1493,7 +1547,7 @@ var page = (state, title, text, icon) => `<!doctype html>
|
|
|
1493
1547
|
<section class="card" aria-labelledby="title">
|
|
1494
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>
|
|
1495
1549
|
<h1 id="title">${title}</h1>
|
|
1496
|
-
<p>${
|
|
1550
|
+
<p>${text2}</p>
|
|
1497
1551
|
<p class="note">You can close this tab.</p>
|
|
1498
1552
|
</section>
|
|
1499
1553
|
</main>
|
|
@@ -1530,8 +1584,8 @@ async function startCallbackServer() {
|
|
|
1530
1584
|
let expected = null;
|
|
1531
1585
|
let settle2 = () => {
|
|
1532
1586
|
};
|
|
1533
|
-
const result = new Promise((
|
|
1534
|
-
settle2 =
|
|
1587
|
+
const result = new Promise((resolve4) => {
|
|
1588
|
+
settle2 = resolve4;
|
|
1535
1589
|
});
|
|
1536
1590
|
function accept(method, host, rawUrl) {
|
|
1537
1591
|
if (method !== "GET" || expected === null || host !== `127.0.0.1:${port}`) return null;
|
|
@@ -1566,9 +1620,9 @@ async function startCallbackServer() {
|
|
|
1566
1620
|
});
|
|
1567
1621
|
server.requestTimeout = 5e3;
|
|
1568
1622
|
server.headersTimeout = 5e3;
|
|
1569
|
-
await new Promise((
|
|
1623
|
+
await new Promise((resolve4, reject) => {
|
|
1570
1624
|
server.once("error", reject);
|
|
1571
|
-
server.listen(0, "127.0.0.1",
|
|
1625
|
+
server.listen(0, "127.0.0.1", resolve4);
|
|
1572
1626
|
});
|
|
1573
1627
|
const port = server.address().port;
|
|
1574
1628
|
return {
|
|
@@ -1899,7 +1953,7 @@ async function cleanupPendingAfterFailure(keychain) {
|
|
|
1899
1953
|
async function logoutCommand(opts = {}) {
|
|
1900
1954
|
const keychain = opts.keychain ?? getLifecycleKeychain();
|
|
1901
1955
|
const api = createCliLoginApi(opts.fetchImpl);
|
|
1902
|
-
const
|
|
1956
|
+
const fail2 = (failure2, exit) => {
|
|
1903
1957
|
if (opts.json) process.stdout.write(`${JSON.stringify(failure2)}
|
|
1904
1958
|
`);
|
|
1905
1959
|
else process.stderr.write(`Error: ${failure2.message}
|
|
@@ -1908,19 +1962,19 @@ async function logoutCommand(opts = {}) {
|
|
|
1908
1962
|
};
|
|
1909
1963
|
try {
|
|
1910
1964
|
const result = await withLifecycleLock(keychain, (lockLost) => logout(lockLost));
|
|
1911
|
-
return result === BUSY ?
|
|
1965
|
+
return result === BUSY ? fail2(lifecycleFailure("busy", false), EXIT.BUSY) : result;
|
|
1912
1966
|
} catch (err) {
|
|
1913
1967
|
const pending = await cleanupPendingAfterFailure(keychain);
|
|
1914
1968
|
if (err instanceof KeychainError) {
|
|
1915
|
-
return
|
|
1969
|
+
return fail2(lifecycleFailure("credential_store_unavailable", pending, err), err.exit);
|
|
1916
1970
|
}
|
|
1917
1971
|
if (err instanceof LockLostError) {
|
|
1918
|
-
return
|
|
1972
|
+
return fail2(lifecycleFailure("lock_lost", pending), EXIT.DOCTOR_FAIL);
|
|
1919
1973
|
}
|
|
1920
1974
|
if (err instanceof JournalInvalidError) {
|
|
1921
|
-
return
|
|
1975
|
+
return fail2(lifecycleFailure("journal_invalid", true), EXIT.DOCTOR_FAIL);
|
|
1922
1976
|
}
|
|
1923
|
-
return
|
|
1977
|
+
return fail2(lifecycleFailure("cleanup_pending", true), EXIT.CLEANUP_PENDING);
|
|
1924
1978
|
}
|
|
1925
1979
|
async function logout(lockLost) {
|
|
1926
1980
|
const journal = await loadJournal(keychain);
|
|
@@ -1942,7 +1996,7 @@ async function logoutCommand(opts = {}) {
|
|
|
1942
1996
|
}
|
|
1943
1997
|
const cleanupPending = !isEmpty(await settle(keychain, api, journal, lockLost));
|
|
1944
1998
|
if (await keychain.read() !== null) {
|
|
1945
|
-
return
|
|
1999
|
+
return fail2(lifecycleFailure("credential_changed", cleanupPending), EXIT.DOCTOR_FAIL);
|
|
1946
2000
|
}
|
|
1947
2001
|
if (lockLost()) throw new LockLostError();
|
|
1948
2002
|
const convergence = convergenceSeconds(journal);
|
|
@@ -2001,7 +2055,7 @@ async function signOutJournal(keychain) {
|
|
|
2001
2055
|
}
|
|
2002
2056
|
async function loginRecoveryCommand(opts = {}) {
|
|
2003
2057
|
const keychain = opts.keychain ?? getLifecycleKeychain();
|
|
2004
|
-
const
|
|
2058
|
+
const fail2 = (failure2, exit) => {
|
|
2005
2059
|
if (opts.json) process.stdout.write(`${JSON.stringify(failure2)}
|
|
2006
2060
|
`);
|
|
2007
2061
|
else process.stderr.write(`Error: ${failure2.message}
|
|
@@ -2020,9 +2074,9 @@ async function loginRecoveryCommand(opts = {}) {
|
|
|
2020
2074
|
if (lockLost()) throw new LockLostError();
|
|
2021
2075
|
return settled;
|
|
2022
2076
|
});
|
|
2023
|
-
if (journal === BUSY) return
|
|
2077
|
+
if (journal === BUSY) return fail2(lifecycleFailure("busy", true), EXIT.BUSY);
|
|
2024
2078
|
if (!isEmpty(journal)) {
|
|
2025
|
-
return
|
|
2079
|
+
return fail2(lifecycleFailure("cleanup_pending", true), EXIT.CLEANUP_PENDING);
|
|
2026
2080
|
}
|
|
2027
2081
|
const convergence = convergenceSeconds(journal);
|
|
2028
2082
|
const report4 = {
|
|
@@ -2037,16 +2091,16 @@ async function loginRecoveryCommand(opts = {}) {
|
|
|
2037
2091
|
return EXIT.OK;
|
|
2038
2092
|
} catch (err) {
|
|
2039
2093
|
if (err instanceof JournalInvalidError) {
|
|
2040
|
-
return
|
|
2094
|
+
return fail2(lifecycleFailure("journal_invalid", true), EXIT.DOCTOR_FAIL);
|
|
2041
2095
|
}
|
|
2042
2096
|
const pending = await cleanupPendingAfterFailure(keychain);
|
|
2043
2097
|
if (err instanceof KeychainError) {
|
|
2044
|
-
return
|
|
2098
|
+
return fail2(lifecycleFailure("credential_store_unavailable", pending, err), err.exit);
|
|
2045
2099
|
}
|
|
2046
2100
|
if (err instanceof LockLostError) {
|
|
2047
|
-
return
|
|
2101
|
+
return fail2(lifecycleFailure("lock_lost", pending), EXIT.DOCTOR_FAIL);
|
|
2048
2102
|
}
|
|
2049
|
-
return
|
|
2103
|
+
return fail2(lifecycleFailure("cleanup_pending", true), EXIT.CLEANUP_PENDING);
|
|
2050
2104
|
}
|
|
2051
2105
|
}
|
|
2052
2106
|
|
|
@@ -2069,9 +2123,9 @@ function createReadCredentials(providerFactory) {
|
|
|
2069
2123
|
}
|
|
2070
2124
|
|
|
2071
2125
|
// src/mcp/proxy-server.ts
|
|
2072
|
-
import { createHash as
|
|
2073
|
-
import { readFile, stat } from "fs/promises";
|
|
2074
|
-
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";
|
|
2075
2129
|
import { Client as McpClient } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2076
2130
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
2077
2131
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -2083,12 +2137,563 @@ import {
|
|
|
2083
2137
|
ListToolsRequestSchema
|
|
2084
2138
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
2085
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
|
+
|
|
2086
2691
|
// src/mcp/tools/shared.ts
|
|
2087
2692
|
var LOCAL_UPLOAD_META_KEY = "pyx.dev/local-upload";
|
|
2088
2693
|
|
|
2089
2694
|
// src/mcp/proxy-server.ts
|
|
2090
2695
|
var REMOTE_MCP_PATH = "/mcp";
|
|
2091
|
-
function createMcpIdempotentFetch(fetchImpl = fetch, sessionNonce =
|
|
2696
|
+
function createMcpIdempotentFetch(fetchImpl = fetch, sessionNonce = randomUUID5()) {
|
|
2092
2697
|
const wrapped = async (input, init) => {
|
|
2093
2698
|
const upstream = input instanceof Request ? input : new Request(String(input), init);
|
|
2094
2699
|
const method = init?.method ?? upstream.method;
|
|
@@ -2098,7 +2703,7 @@ function createMcpIdempotentFetch(fetchImpl = fetch, sessionNonce = randomUUID3(
|
|
|
2098
2703
|
if (typeof body !== "string") {
|
|
2099
2704
|
throw new Error("remote MCP POST body must be text for stable request idempotency");
|
|
2100
2705
|
}
|
|
2101
|
-
const key =
|
|
2706
|
+
const key = createHash5("sha256").update("pyx-mcp-idempotency-v1\0").update(sessionNonce).update("\0").update(body).digest("hex");
|
|
2102
2707
|
headers.set("Idempotency-Key", `mcp-${key}`);
|
|
2103
2708
|
}
|
|
2104
2709
|
return fetchImpl(input, { ...init, method, headers, body });
|
|
@@ -2151,10 +2756,10 @@ async function readCappedLocalFile(rawPath, descriptor) {
|
|
|
2151
2756
|
result: mcpText(`\`${descriptor.pathArg}\` must be a non-empty local file path.`, true)
|
|
2152
2757
|
};
|
|
2153
2758
|
}
|
|
2154
|
-
const abs =
|
|
2759
|
+
const abs = isAbsolute2(rawPath) ? rawPath : resolve2(rawPath);
|
|
2155
2760
|
try {
|
|
2156
2761
|
if (typeof descriptor.maxBytes === "number") {
|
|
2157
|
-
const info = await
|
|
2762
|
+
const info = await stat2(abs);
|
|
2158
2763
|
if (info.size > descriptor.maxBytes) {
|
|
2159
2764
|
return {
|
|
2160
2765
|
ok: false,
|
|
@@ -2162,7 +2767,7 @@ async function readCappedLocalFile(rawPath, descriptor) {
|
|
|
2162
2767
|
};
|
|
2163
2768
|
}
|
|
2164
2769
|
}
|
|
2165
|
-
return { ok: true, bytes: await
|
|
2770
|
+
return { ok: true, bytes: await readFile2(abs), name: basename2(abs) };
|
|
2166
2771
|
} catch (err) {
|
|
2167
2772
|
return {
|
|
2168
2773
|
ok: false,
|
|
@@ -2200,7 +2805,7 @@ function createLocalFileUploader(credentials, readCredentials, fetchImpl = fetch
|
|
|
2200
2805
|
}
|
|
2201
2806
|
};
|
|
2202
2807
|
}
|
|
2203
|
-
function createProxyServer(client, version, uploadLocalFile) {
|
|
2808
|
+
function createProxyServer(client, version, uploadLocalFile, hostedFileIngest) {
|
|
2204
2809
|
const caps = client.getServerCapabilities();
|
|
2205
2810
|
const server = new Server(client.getServerVersion() ?? { name: "pyx-memory", version }, {
|
|
2206
2811
|
capabilities: {
|
|
@@ -2213,7 +2818,14 @@ function createProxyServer(client, version, uploadLocalFile) {
|
|
|
2213
2818
|
},
|
|
2214
2819
|
instructions: client.getInstructions()
|
|
2215
2820
|
});
|
|
2216
|
-
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
|
+
};
|
|
2217
2829
|
let listedOnce = false;
|
|
2218
2830
|
let listingsStarted = 0;
|
|
2219
2831
|
let listingApplied = 0;
|
|
@@ -2224,30 +2836,30 @@ function createProxyServer(client, version, uploadLocalFile) {
|
|
|
2224
2836
|
if (!Array.isArray(tools) || listing < listingApplied) return result;
|
|
2225
2837
|
listingApplied = listing;
|
|
2226
2838
|
listedOnce = true;
|
|
2227
|
-
if (params?.cursor === void 0 && nextCursor === void 0)
|
|
2839
|
+
if (params?.cursor === void 0 && nextCursor === void 0) bridges.clear();
|
|
2228
2840
|
for (const tool of tools) {
|
|
2229
|
-
const
|
|
2230
|
-
if (
|
|
2231
|
-
else
|
|
2841
|
+
const bridge = bridgeOf(tool._meta);
|
|
2842
|
+
if (bridge) bridges.set(tool.name, bridge);
|
|
2843
|
+
else bridges.delete(tool.name);
|
|
2232
2844
|
}
|
|
2233
2845
|
return result;
|
|
2234
2846
|
};
|
|
2235
2847
|
if (caps?.tools) {
|
|
2236
2848
|
server.setRequestHandler(ListToolsRequestSchema, (req) => listAndRefresh(req.params));
|
|
2237
|
-
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
2238
|
-
if (
|
|
2849
|
+
server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
|
|
2850
|
+
if (bridging && !listedOnce) {
|
|
2239
2851
|
try {
|
|
2240
2852
|
await listAndRefresh();
|
|
2241
2853
|
} catch {
|
|
2242
2854
|
}
|
|
2243
2855
|
}
|
|
2244
|
-
const
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
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);
|
|
2251
2863
|
}
|
|
2252
2864
|
return client.callTool(req.params);
|
|
2253
2865
|
});
|
|
@@ -2265,7 +2877,7 @@ function createProxyServer(client, version, uploadLocalFile) {
|
|
|
2265
2877
|
return server;
|
|
2266
2878
|
}
|
|
2267
2879
|
async function runMcpProxyServer(opts) {
|
|
2268
|
-
const version = opts.version ?? (true ? "1.
|
|
2880
|
+
const version = opts.version ?? (true ? "1.24.0" : "0.0.0-dev");
|
|
2269
2881
|
const read = await opts.readCredentials();
|
|
2270
2882
|
if (!read.ok) {
|
|
2271
2883
|
throw new Error(
|
|
@@ -2274,12 +2886,11 @@ async function runMcpProxyServer(opts) {
|
|
|
2274
2886
|
}
|
|
2275
2887
|
const { endpoint, apiKey } = read.credentials;
|
|
2276
2888
|
const url = new URL(`${endpoint.replace(/\/+$/, "")}${REMOTE_MCP_PATH}`);
|
|
2889
|
+
const guard = createCredentialGuard(opts.readCredentials, read.credentials);
|
|
2277
2890
|
const client = new McpClient({ name: "pyx-mem-proxy", version }, { capabilities: {} });
|
|
2278
2891
|
const clientTransport = new StreamableHTTPClientTransport(url, {
|
|
2279
2892
|
requestInit: { headers: { Authorization: `Bearer ${apiKey}` } },
|
|
2280
|
-
fetch: createMcpIdempotentFetch(
|
|
2281
|
-
createGuardedFetch(createCredentialGuard(opts.readCredentials, read.credentials))
|
|
2282
|
-
)
|
|
2893
|
+
fetch: createMcpIdempotentFetch(createGuardedFetch(guard))
|
|
2283
2894
|
});
|
|
2284
2895
|
try {
|
|
2285
2896
|
await client.connect(clientTransport);
|
|
@@ -2293,11 +2904,12 @@ async function runMcpProxyServer(opts) {
|
|
|
2293
2904
|
const server = createProxyServer(
|
|
2294
2905
|
client,
|
|
2295
2906
|
version,
|
|
2296
|
-
createLocalFileUploader(read.credentials, opts.readCredentials)
|
|
2907
|
+
createLocalFileUploader(read.credentials, opts.readCredentials),
|
|
2908
|
+
createHostedFileIngestDriver(read.credentials, guard, createFileIngestIntentStore())
|
|
2297
2909
|
);
|
|
2298
2910
|
const transport = new StdioServerTransport();
|
|
2299
|
-
const closed = new Promise((
|
|
2300
|
-
transport.onclose = () =>
|
|
2911
|
+
const closed = new Promise((resolve4) => {
|
|
2912
|
+
transport.onclose = () => resolve4();
|
|
2301
2913
|
});
|
|
2302
2914
|
process.stdin.once("end", () => void transport.close());
|
|
2303
2915
|
await server.connect(transport);
|
|
@@ -2312,8 +2924,8 @@ async function runMcpProxyServer(opts) {
|
|
|
2312
2924
|
// src/cli/commands/mcp-install.ts
|
|
2313
2925
|
import { spawnSync as nodeSpawnSync } from "child_process";
|
|
2314
2926
|
import { existsSync as existsSync3 } from "fs";
|
|
2315
|
-
import { homedir as
|
|
2316
|
-
import { join as
|
|
2927
|
+
import { homedir as homedir3 } from "os";
|
|
2928
|
+
import { join as join3 } from "path";
|
|
2317
2929
|
|
|
2318
2930
|
// src/cli/commands/mcp-install-merge.ts
|
|
2319
2931
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
@@ -2426,12 +3038,12 @@ function recordsEqual(a, b) {
|
|
|
2426
3038
|
}
|
|
2427
3039
|
return true;
|
|
2428
3040
|
}
|
|
2429
|
-
function indent(
|
|
2430
|
-
return
|
|
3041
|
+
function indent(text2) {
|
|
3042
|
+
return text2.split("\n").map((line) => ` ${line}`).join("\n");
|
|
2431
3043
|
}
|
|
2432
3044
|
|
|
2433
3045
|
// src/cli/commands/memory-rules-merge.ts
|
|
2434
|
-
import { randomUUID as
|
|
3046
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
2435
3047
|
import {
|
|
2436
3048
|
chmodSync,
|
|
2437
3049
|
existsSync as existsSync2,
|
|
@@ -2628,7 +3240,7 @@ function writeMemoryRulesFile(opts) {
|
|
|
2628
3240
|
const parent = dirname3(target);
|
|
2629
3241
|
if (!existsSync2(parent)) mkdirSync2(parent, { recursive: true });
|
|
2630
3242
|
const mode = fileExists ? statSync(target).mode & 511 : 420;
|
|
2631
|
-
const tmpPath = `${target}.${process.pid}.${
|
|
3243
|
+
const tmpPath = `${target}.${process.pid}.${randomUUID6()}.tmp`;
|
|
2632
3244
|
try {
|
|
2633
3245
|
writeFileSync2(tmpPath, merged.content, { mode });
|
|
2634
3246
|
chmodSync(tmpPath, mode);
|
|
@@ -2713,7 +3325,7 @@ function mcpInstallCodexCommand(opts = {}) {
|
|
|
2713
3325
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
2714
3326
|
const remote = opts.remote ?? false;
|
|
2715
3327
|
const spawnSync = opts._spawnSync ?? nodeSpawnSync;
|
|
2716
|
-
const home = opts._homeDir ??
|
|
3328
|
+
const home = opts._homeDir ?? homedir3();
|
|
2717
3329
|
const codexHome = (opts._env ?? process.env).CODEX_HOME;
|
|
2718
3330
|
const probe = spawnSync("codex", ["--version"], { stdio: "pipe" });
|
|
2719
3331
|
if (probe.error || probe.status !== 0) {
|
|
@@ -2744,7 +3356,7 @@ Restart Codex CLI to make the tools available. No API key was written to config.
|
|
|
2744
3356
|
return EXIT.OK;
|
|
2745
3357
|
}
|
|
2746
3358
|
function printCodexManualInstructions(home, remote, codexHome) {
|
|
2747
|
-
const configPath =
|
|
3359
|
+
const configPath = join3(codexHome ?? join3(home, ".codex"), "config.toml");
|
|
2748
3360
|
process.stdout.write(
|
|
2749
3361
|
[
|
|
2750
3362
|
"The `codex` CLI is not on PATH, or the install command failed.",
|
|
@@ -2767,8 +3379,8 @@ function printCodexManualInstructions(home, remote, codexHome) {
|
|
|
2767
3379
|
function mcpInstallCursorCommand(opts = {}) {
|
|
2768
3380
|
const scope = opts.scope ?? "user";
|
|
2769
3381
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
2770
|
-
const home = opts._homeDir ??
|
|
2771
|
-
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");
|
|
2772
3384
|
return writeJsonAndReport(filePath, "Cursor", mcpEntry(opts.remote ?? false));
|
|
2773
3385
|
}
|
|
2774
3386
|
function mcpInstallClineCommand(opts = {}) {
|
|
@@ -2799,8 +3411,8 @@ function mcpInstallContinueCommand(opts = {}) {
|
|
|
2799
3411
|
const scope = opts.scope ?? "user";
|
|
2800
3412
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
2801
3413
|
const remote = opts.remote ?? false;
|
|
2802
|
-
const home = opts._homeDir ??
|
|
2803
|
-
const yamlPath =
|
|
3414
|
+
const home = opts._homeDir ?? homedir3();
|
|
3415
|
+
const yamlPath = join3(home, ".continue", "mcpServers", `${SERVER_NAME}.yaml`);
|
|
2804
3416
|
process.stdout.write(
|
|
2805
3417
|
[
|
|
2806
3418
|
"Continue uses YAML config. The recommended way to add an MCP server is a",
|
|
@@ -2831,8 +3443,8 @@ function mcpInstallContinueCommand(opts = {}) {
|
|
|
2831
3443
|
function mcpInstallWindsurfCommand(opts = {}) {
|
|
2832
3444
|
const scope = opts.scope ?? "user";
|
|
2833
3445
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
2834
|
-
const home = opts._homeDir ??
|
|
2835
|
-
const filePath =
|
|
3446
|
+
const home = opts._homeDir ?? homedir3();
|
|
3447
|
+
const filePath = join3(home, ".codeium", "windsurf", "mcp_config.json");
|
|
2836
3448
|
return writeJsonAndReport(filePath, "Windsurf", mcpEntry(opts.remote ?? false));
|
|
2837
3449
|
}
|
|
2838
3450
|
function mcpInstallGeminiCliCommand(opts = {}) {
|
|
@@ -2840,7 +3452,7 @@ function mcpInstallGeminiCliCommand(opts = {}) {
|
|
|
2840
3452
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
2841
3453
|
const remote = opts.remote ?? false;
|
|
2842
3454
|
const spawnSync = opts._spawnSync ?? nodeSpawnSync;
|
|
2843
|
-
const home = opts._homeDir ??
|
|
3455
|
+
const home = opts._homeDir ?? homedir3();
|
|
2844
3456
|
const probe = spawnSync("gemini", ["--version"], { stdio: "pipe" });
|
|
2845
3457
|
if (probe.error || probe.status !== 0) {
|
|
2846
3458
|
return writeJsonAndReport(geminiConfigPath(home, scope), "Gemini CLI", mcpEntry(remote));
|
|
@@ -2861,13 +3473,13 @@ Restart Gemini CLI to make the tools available. No API key was written to settin
|
|
|
2861
3473
|
return EXIT.OK;
|
|
2862
3474
|
}
|
|
2863
3475
|
function geminiConfigPath(home, scope) {
|
|
2864
|
-
return scope === "project" ?
|
|
3476
|
+
return scope === "project" ? join3(process.cwd(), ".gemini", "settings.json") : join3(home, ".gemini", "settings.json");
|
|
2865
3477
|
}
|
|
2866
3478
|
function mcpInstallPiCommand(opts = {}) {
|
|
2867
3479
|
const scope = opts.scope ?? "user";
|
|
2868
3480
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
2869
|
-
const home = opts._homeDir ??
|
|
2870
|
-
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");
|
|
2871
3483
|
return writeJsonAndReport(filePath, "Pi", mcpEntry(opts.remote ?? false), {
|
|
2872
3484
|
prereq: "pi has no native MCP support \u2014 install the third-party adapter once:\n pi install npm:pi-mcp-adapter"
|
|
2873
3485
|
});
|
|
@@ -2875,8 +3487,8 @@ function mcpInstallPiCommand(opts = {}) {
|
|
|
2875
3487
|
function mcpInstallOhMyPiCommand(opts = {}) {
|
|
2876
3488
|
const scope = opts.scope ?? "user";
|
|
2877
3489
|
if (!validateScope(scope)) return EXIT.USAGE;
|
|
2878
|
-
const home = opts._homeDir ??
|
|
2879
|
-
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");
|
|
2880
3492
|
return writeJsonAndReport(filePath, "oh-my-pi", mcpEntry(opts.remote ?? false));
|
|
2881
3493
|
}
|
|
2882
3494
|
function mcpInstallOpenClawCommand(opts = {}) {
|
|
@@ -2984,7 +3596,7 @@ function rulesFilePath(target, opts) {
|
|
|
2984
3596
|
const spec = RULES_FILE_TARGETS[target];
|
|
2985
3597
|
const env = opts._env ?? (opts._homeDir === void 0 ? process.env : {});
|
|
2986
3598
|
const configDir = spec.envDir ? env[spec.envDir] : void 0;
|
|
2987
|
-
return configDir ?
|
|
3599
|
+
return configDir ? join3(configDir, spec.file) : join3(opts._homeDir ?? homedir3(), ...spec.dir, spec.file);
|
|
2988
3600
|
}
|
|
2989
3601
|
function installMemoryRules(opts) {
|
|
2990
3602
|
const filePath = rulesFilePath(opts.target, opts);
|
|
@@ -3048,8 +3660,8 @@ async function syncMemoryRulesFromServer(client, opts = {}) {
|
|
|
3048
3660
|
);
|
|
3049
3661
|
}
|
|
3050
3662
|
}
|
|
3051
|
-
function isRulesSection(
|
|
3052
|
-
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);
|
|
3053
3665
|
}
|
|
3054
3666
|
function validateScope(scope) {
|
|
3055
3667
|
if (VALID_SCOPES.has(scope)) return true;
|
|
@@ -3182,7 +3794,7 @@ function boundedLabel(value, fallback, maxChars) {
|
|
|
3182
3794
|
|
|
3183
3795
|
// src/cli/commands/scaffold.ts
|
|
3184
3796
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
3185
|
-
import { basename as
|
|
3797
|
+
import { basename as basename3, join as join4, resolve as resolve3 } from "path";
|
|
3186
3798
|
var SERVER_IMAGE = "ghcr.io/pyx-corp/pyx-memory-v1:latest";
|
|
3187
3799
|
var DOCKER_COMPOSE = `services:
|
|
3188
3800
|
pyx-memory:
|
|
@@ -3305,17 +3917,17 @@ function resolveTarget(args) {
|
|
|
3305
3917
|
process.stderr.write("Error: --name requires a non-empty directory name.\n");
|
|
3306
3918
|
return null;
|
|
3307
3919
|
}
|
|
3308
|
-
const targetDir = trimmedName ?
|
|
3309
|
-
const appName = trimmedName ||
|
|
3920
|
+
const targetDir = trimmedName ? resolve3(cwd, trimmedName) : cwd;
|
|
3921
|
+
const appName = trimmedName || basename3(resolve3(cwd));
|
|
3310
3922
|
return { targetDir, appName };
|
|
3311
3923
|
}
|
|
3312
3924
|
function buildFiles(targetDir, appName) {
|
|
3313
3925
|
return [
|
|
3314
|
-
{ path:
|
|
3315
|
-
{ path:
|
|
3316
|
-
{ 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 },
|
|
3317
3929
|
{
|
|
3318
|
-
path:
|
|
3930
|
+
path: join4(targetDir, "PYX_MEMORY_DESIGN_GUIDE.md"),
|
|
3319
3931
|
contents: buildDesignGuide({ appName })
|
|
3320
3932
|
}
|
|
3321
3933
|
];
|
|
@@ -3332,7 +3944,7 @@ function scaffoldCommand(args = {}) {
|
|
|
3332
3944
|
const created = [];
|
|
3333
3945
|
const skipped = [];
|
|
3334
3946
|
for (const file of buildFiles(target.targetDir, target.appName)) {
|
|
3335
|
-
const relativePath =
|
|
3947
|
+
const relativePath = basename3(file.path);
|
|
3336
3948
|
if (existsSync4(file.path)) {
|
|
3337
3949
|
skipped.push(relativePath);
|
|
3338
3950
|
continue;
|
|
@@ -3553,7 +4165,7 @@ Notes:
|
|
|
3553
4165
|
|
|
3554
4166
|
// src/cli/parser.ts
|
|
3555
4167
|
var COMMANDS_WITH_SUBCOMMAND = /* @__PURE__ */ new Set(["mcp"]);
|
|
3556
|
-
function
|
|
4168
|
+
function parseArgs2(argv) {
|
|
3557
4169
|
const flags = /* @__PURE__ */ Object.create(null);
|
|
3558
4170
|
const positional = [];
|
|
3559
4171
|
let i = 0;
|
|
@@ -3684,7 +4296,7 @@ function runLogin(parsed) {
|
|
|
3684
4296
|
});
|
|
3685
4297
|
}
|
|
3686
4298
|
async function main() {
|
|
3687
|
-
const parsed =
|
|
4299
|
+
const parsed = parseArgs2(process.argv.slice(2));
|
|
3688
4300
|
if (parsed.flags.help === true || parsed.flags.h === true) {
|
|
3689
4301
|
process.stdout.write(HELP_TEXT);
|
|
3690
4302
|
return EXIT.OK;
|