@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.
@@ -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 "../chunk-C46EE5BH.mjs";
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 label = value.replace(/\p{Cc}/gu, "").slice(0, maxChars);
178
- while (JSON.stringify(label).length - 2 > maxChars) label = label.slice(0, -1);
179
- return label;
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(text, isError = false) {
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(() => ctl.abort(), DEFAULT_TIMEOUT_MS);
896
- let res;
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 = await fetchImpl(url, { method, headers, body, signal: ctl.signal });
899
- } catch (err) {
900
- return {
901
- ok: false,
902
- result: mcpUnreachable(baseUrl, err instanceof Error ? err.message : String(err))
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(url, input.method, headers, hasBody ? JSON.stringify(input.body) : void 0);
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 parseHttpResponse(res, text) {
945
- if (res.status >= 500) {
946
- return { ok: false, status: res.status, result: mcpServerError(res.status, text) };
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, text);
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: text };
1001
+ return { ok: true, status: res.status, data: text2 };
961
1002
  }
962
1003
  if (!res.ok) {
963
- const message = extractEnvelopeError(parsed.value) ?? text ?? `HTTP ${res.status}`;
964
- return { ok: false, status: res.status, result: mcpHttpError(res.status, message) };
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 { ok: false, status: res.status, result: mcpEnvelopeError(envelopeError) };
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, text) {
973
- for (const line of text.split("\n")) {
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(text) {
998
- if (text.length === 0) return { ok: true, value: void 0 };
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(text) };
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(label) {
1337
+ async function promptMasked(label2) {
1284
1338
  const { stdin, stdout } = process;
1285
- stdout.write(label);
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((resolve3, reject) => {
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
- resolve3(buf.trim());
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, text, icon) => `<!doctype html>
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>${text}</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((resolve3) => {
1534
- settle2 = resolve3;
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((resolve3, reject) => {
1623
+ await new Promise((resolve4, reject) => {
1570
1624
  server.once("error", reject);
1571
- server.listen(0, "127.0.0.1", resolve3);
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 fail = (failure2, exit) => {
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 ? fail(lifecycleFailure("busy", false), EXIT.BUSY) : result;
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 fail(lifecycleFailure("credential_store_unavailable", pending, err), err.exit);
1969
+ return fail2(lifecycleFailure("credential_store_unavailable", pending, err), err.exit);
1916
1970
  }
1917
1971
  if (err instanceof LockLostError) {
1918
- return fail(lifecycleFailure("lock_lost", pending), EXIT.DOCTOR_FAIL);
1972
+ return fail2(lifecycleFailure("lock_lost", pending), EXIT.DOCTOR_FAIL);
1919
1973
  }
1920
1974
  if (err instanceof JournalInvalidError) {
1921
- return fail(lifecycleFailure("journal_invalid", true), EXIT.DOCTOR_FAIL);
1975
+ return fail2(lifecycleFailure("journal_invalid", true), EXIT.DOCTOR_FAIL);
1922
1976
  }
1923
- return fail(lifecycleFailure("cleanup_pending", true), EXIT.CLEANUP_PENDING);
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 fail(lifecycleFailure("credential_changed", cleanupPending), EXIT.DOCTOR_FAIL);
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 fail = (failure2, exit) => {
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 fail(lifecycleFailure("busy", true), EXIT.BUSY);
2077
+ if (journal === BUSY) return fail2(lifecycleFailure("busy", true), EXIT.BUSY);
2024
2078
  if (!isEmpty(journal)) {
2025
- return fail(lifecycleFailure("cleanup_pending", true), EXIT.CLEANUP_PENDING);
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 fail(lifecycleFailure("journal_invalid", true), EXIT.DOCTOR_FAIL);
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 fail(lifecycleFailure("credential_store_unavailable", pending, err), err.exit);
2098
+ return fail2(lifecycleFailure("credential_store_unavailable", pending, err), err.exit);
2045
2099
  }
2046
2100
  if (err instanceof LockLostError) {
2047
- return fail(lifecycleFailure("lock_lost", pending), EXIT.DOCTOR_FAIL);
2101
+ return fail2(lifecycleFailure("lock_lost", pending), EXIT.DOCTOR_FAIL);
2048
2102
  }
2049
- return fail(lifecycleFailure("cleanup_pending", true), EXIT.CLEANUP_PENDING);
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 createHash4, randomUUID as randomUUID3 } from "crypto";
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 = randomUUID3()) {
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 = createHash4("sha256").update("pyx-mcp-idempotency-v1\0").update(sessionNonce).update("\0").update(body).digest("hex");
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 = isAbsolute(rawPath) ? rawPath : resolve(rawPath);
2759
+ const abs = isAbsolute2(rawPath) ? rawPath : resolve2(rawPath);
2155
2760
  try {
2156
2761
  if (typeof descriptor.maxBytes === "number") {
2157
- const info = await stat(abs);
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 readFile(abs), name: basename(abs) };
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 localUploads = /* @__PURE__ */ new Map();
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) localUploads.clear();
2839
+ if (params?.cursor === void 0 && nextCursor === void 0) bridges.clear();
2228
2840
  for (const tool of tools) {
2229
- const descriptor = uploadLocalFile ? extractLocalUpload(tool._meta) : void 0;
2230
- if (descriptor) localUploads.set(tool.name, descriptor);
2231
- else localUploads.delete(tool.name);
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 (uploadLocalFile && !listedOnce) {
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 descriptor = uploadLocalFile ? localUploads.get(req.params.name) : void 0;
2245
- if (uploadLocalFile && descriptor) {
2246
- const result = await uploadLocalFile(
2247
- descriptor,
2248
- req.params.arguments ?? {}
2249
- );
2250
- return result;
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.23.2" : "0.0.0-dev");
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((resolve3) => {
2300
- transport.onclose = () => resolve3();
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 homedir2 } from "os";
2316
- import { join as join2 } from "path";
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(text) {
2430
- return text.split("\n").map((line) => ` ${line}`).join("\n");
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 randomUUID4 } from "crypto";
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}.${randomUUID4()}.tmp`;
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 ?? homedir2();
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 = join2(codexHome ?? join2(home, ".codex"), "config.toml");
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 ?? homedir2();
2771
- const filePath = scope === "project" ? join2(process.cwd(), ".cursor", "mcp.json") : join2(home, ".cursor", "mcp.json");
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 ?? homedir2();
2803
- const yamlPath = join2(home, ".continue", "mcpServers", `${SERVER_NAME}.yaml`);
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 ?? homedir2();
2835
- const filePath = join2(home, ".codeium", "windsurf", "mcp_config.json");
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 ?? homedir2();
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" ? join2(process.cwd(), ".gemini", "settings.json") : join2(home, ".gemini", "settings.json");
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 ?? homedir2();
2870
- const filePath = scope === "user" ? join2(home, ".pi", "agent", "mcp.json") : join2(process.cwd(), ".pi", "mcp.json");
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 ?? homedir2();
2879
- const filePath = scope === "user" ? join2(home, ".omp", "agent", "mcp.json") : join2(process.cwd(), ".omp", "mcp.json");
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 ? join2(configDir, spec.file) : join2(opts._homeDir ?? homedir2(), ...spec.dir, spec.file);
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(text) {
3052
- return text.startsWith("## Persistent Memory\n") && Buffer.byteLength(text, "utf8") <= MAX_SERVER_RULES_BYTES && !text.includes("<!-- pyx-memory:") && !/^\s*(```|~~~)/m.test(text);
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 basename2, join as join3, resolve as resolve2 } from "path";
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 ? resolve2(cwd, trimmedName) : cwd;
3309
- const appName = trimmedName || basename2(resolve2(cwd));
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: join3(targetDir, "docker-compose.yml"), contents: DOCKER_COMPOSE },
3315
- { path: join3(targetDir, ".env.example"), contents: ENV_EXAMPLE },
3316
- { path: join3(targetDir, "memory.ts"), contents: MEMORY_TS },
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: join3(targetDir, "PYX_MEMORY_DESIGN_GUIDE.md"),
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 = basename2(file.path);
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 parseArgs(argv) {
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 = parseArgs(process.argv.slice(2));
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;