@pyxmate/memory 1.23.1 → 1.24.0

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