@agimon-ai/doompi-core 0.0.1-alpha.119 → 0.0.1-alpha.121

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.
Files changed (67) hide show
  1. package/dist/contextDetailStore.cjs +6 -1
  2. package/dist/contextDetailStore.cjs.map +1 -1
  3. package/dist/contextDetailStore.d.mts +6 -0
  4. package/dist/contextDetailStore.d.mts.map +1 -1
  5. package/dist/contextDetailStore.mjs +5 -2
  6. package/dist/contextDetailStore.mjs.map +1 -1
  7. package/dist/headlessServer.cjs +29 -26
  8. package/dist/headlessServer.cjs.map +1 -1
  9. package/dist/headlessServer.d.mts.map +1 -1
  10. package/dist/headlessServer.mjs +22 -19
  11. package/dist/headlessServer.mjs.map +1 -1
  12. package/dist/packageApi.cjs +1 -0
  13. package/dist/packageApi.d.mts +2 -2
  14. package/dist/packageApi.mjs +2 -2
  15. package/dist/packageApiServer.cjs +1 -0
  16. package/dist/packageApiServer.cjs.map +1 -1
  17. package/dist/packageApiServer.d.mts +2 -0
  18. package/dist/packageApiServer.d.mts.map +1 -1
  19. package/dist/packageApiServer.mjs +1 -0
  20. package/dist/packageApiServer.mjs.map +1 -1
  21. package/dist/remoteApi.cjs +2 -0
  22. package/dist/remoteApi.cjs.map +1 -1
  23. package/dist/remoteApi.mjs +2 -0
  24. package/dist/remoteApi.mjs.map +1 -1
  25. package/dist/remoteRuntime.cjs +36 -15
  26. package/dist/remoteRuntime.cjs.map +1 -1
  27. package/dist/remoteRuntime.d.mts +3 -0
  28. package/dist/remoteRuntime.d.mts.map +1 -1
  29. package/dist/remoteRuntime.mjs +21 -0
  30. package/dist/remoteRuntime.mjs.map +1 -1
  31. package/dist/src/constants/sessionFiles.cjs +27 -0
  32. package/dist/src/constants/sessionFiles.cjs.map +1 -0
  33. package/dist/src/constants/sessionFiles.mjs +25 -0
  34. package/dist/src/constants/sessionFiles.mjs.map +1 -0
  35. package/dist/src/schemas/packageApi.cjs +3 -0
  36. package/dist/src/schemas/packageApi.cjs.map +1 -1
  37. package/dist/src/schemas/packageApi.d.mts +21 -0
  38. package/dist/src/schemas/packageApi.d.mts.map +1 -1
  39. package/dist/src/schemas/packageApi.mjs +3 -1
  40. package/dist/src/schemas/packageApi.mjs.map +1 -1
  41. package/dist/src/server/directHarnessRuntime.cjs +7 -1
  42. package/dist/src/server/directHarnessRuntime.cjs.map +1 -1
  43. package/dist/src/server/directHarnessRuntime.mjs +7 -1
  44. package/dist/src/server/directHarnessRuntime.mjs.map +1 -1
  45. package/dist/src/services/publicFileShares/index.cjs +181 -0
  46. package/dist/src/services/publicFileShares/index.cjs.map +1 -0
  47. package/dist/src/services/publicFileShares/index.d.mts +16 -0
  48. package/dist/src/services/publicFileShares/index.d.mts.map +1 -0
  49. package/dist/src/services/publicFileShares/index.mjs +177 -0
  50. package/dist/src/services/publicFileShares/index.mjs.map +1 -0
  51. package/dist/src/services/sessionAttachments/index.cjs +110 -0
  52. package/dist/src/services/sessionAttachments/index.cjs.map +1 -0
  53. package/dist/src/services/sessionAttachments/index.mjs +106 -0
  54. package/dist/src/services/sessionAttachments/index.mjs.map +1 -0
  55. package/dist/src/systems/child/adapters/headlessChildSessionService.cjs +1 -1
  56. package/dist/src/systems/child/adapters/headlessChildSessionService.cjs.map +1 -1
  57. package/dist/src/systems/child/adapters/headlessChildSessionService.mjs +1 -1
  58. package/dist/src/systems/child/adapters/headlessChildSessionService.mjs.map +1 -1
  59. package/dist/src/systems/child/types/childSession.cjs.map +1 -1
  60. package/dist/src/systems/child/types/childSession.d.mts +5 -0
  61. package/dist/src/systems/child/types/childSession.d.mts.map +1 -1
  62. package/dist/src/systems/child/types/childSession.mjs.map +1 -1
  63. package/dist/terminalPiChildSessionService.cjs +1 -1
  64. package/dist/terminalPiChildSessionService.cjs.map +1 -1
  65. package/dist/terminalPiChildSessionService.mjs +1 -1
  66. package/dist/terminalPiChildSessionService.mjs.map +1 -1
  67. package/package.json +14 -14
@@ -29,7 +29,10 @@ const PRIVATE_FILE_MODE = 384;
29
29
  function agentDirectory(environment) {
30
30
  return environment[AGENT_DIR_ENV] || node_path.default.join(node_os.default.homedir(), ".pi", "agent");
31
31
  }
32
- /** A session id reaches this from a query string, so it never becomes a path. */
32
+ /**
33
+ * A session id reaches this from a query string, so it never becomes a path.
34
+ * `.` and `..` pass through unchanged: a caller using the result as a directory must refuse them.
35
+ */
33
36
  function safeName(sessionId) {
34
37
  return sessionId.replace(/[^a-zA-Z0-9._-]/gu, "_");
35
38
  }
@@ -91,10 +94,12 @@ function findContextItem(file, itemKind, name) {
91
94
  return file.items.find((item) => item.itemKind === itemKind && item.name === name);
92
95
  }
93
96
  //#endregion
97
+ exports.agentDirectory = agentDirectory;
94
98
  exports.contextDetailPath = contextDetailPath;
95
99
  exports.findContextItem = findContextItem;
96
100
  exports.readContextDetail = readContextDetail;
97
101
  exports.removeContextDetail = removeContextDetail;
102
+ exports.safeName = safeName;
98
103
  exports.writeContextDetail = writeContextDetail;
99
104
 
100
105
  //# sourceMappingURL=contextDetailStore.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"contextDetailStore.cjs","names":["path","os","fs"],"sources":["../src/services/contextDetailStore/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { CONTEXT_DETAIL_VERSION } from '../../constants/contextApi';\nimport type { ContextDetailFile, ContextItemDetail, ContextItemKind } from '../../types/contextApi';\n\n/**\n * Where the agent leaves the composition's detail for the session API to find.\n *\n * The two are separate processes: the inventory only exists inside the agent,\n * and the API that answers the browser is served by the session's host. Every\n * other package-owned API in the repository bridges that gap through the\n * filesystem rather than through the frame protocol, and this one follows,\n * because a file is also what makes the answer survive the agent going quiet\n * between turns.\n *\n * One file per session, replaced whole. The detail is a projection of the\n * composition, not a log of it, so an older copy has no reader.\n */\n\nconst DIRECTORY_NAME = 'doom-context';\nconst AGENT_DIR_ENV = 'PI_CODING_AGENT_DIR';\n/** Owner-only: a tool schema is not secret, but it describes the machine. */\nconst PRIVATE_DIRECTORY_MODE = 0o700;\nconst PRIVATE_FILE_MODE = 0o600;\n\nfunction agentDirectory(environment: NodeJS.ProcessEnv): string {\n return environment[AGENT_DIR_ENV] || path.join(os.homedir(), '.pi', 'agent');\n}\n\n/** A session id reaches this from a query string, so it never becomes a path. */\nfunction safeName(sessionId: string): string {\n return sessionId.replace(/[^a-zA-Z0-9._-]/gu, '_');\n}\n\nexport function contextDetailPath(\n sessionId: string,\n environment: Readonly<Record<string, string | undefined>> = process.env,\n): string {\n return path.join(agentDirectory(environment), DIRECTORY_NAME, `${safeName(sessionId)}.json`);\n}\n\n/**\n * Publishes the detail for one session.\n *\n * Written through a temporary file so a reader never sees half a document, and\n * failures are reported to the caller rather than thrown: the panel works\n * without this, and a session must not be lost to a full disk.\n */\nexport function writeContextDetail(\n sessionId: string,\n revision: number,\n items: readonly ContextItemDetail[],\n environment: Readonly<Record<string, string | undefined>> = process.env,\n): boolean {\n const target = contextDetailPath(sessionId, environment);\n const payload: ContextDetailFile = { version: CONTEXT_DETAIL_VERSION, revision, items };\n const temporary = `${target}.${String(process.pid)}.tmp`;\n try {\n fs.mkdirSync(path.dirname(target), { recursive: true, mode: PRIVATE_DIRECTORY_MODE });\n fs.writeFileSync(temporary, JSON.stringify(payload), { mode: PRIVATE_FILE_MODE });\n fs.renameSync(temporary, target);\n return true;\n } catch {\n try {\n fs.rmSync(temporary, { force: true });\n } catch {\n // The temporary file is already unreachable; nothing further to do.\n }\n return false;\n }\n}\n\n/** Drops a session's file. Called when the session ends, so the directory does not grow forever. */\nexport function removeContextDetail(\n sessionId: string,\n environment: Readonly<Record<string, string | undefined>> = process.env,\n): void {\n try {\n fs.rmSync(contextDetailPath(sessionId, environment), { force: true });\n } catch {\n // A file that cannot be removed is stale, not fatal; the next session with\n // this id overwrites it.\n }\n}\n\nfunction isDetailFile(value: unknown): value is ContextDetailFile {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Record<string, unknown>;\n return candidate.version === CONTEXT_DETAIL_VERSION && Array.isArray(candidate.items);\n}\n\n/** What the session wrote, or undefined when it has not written yet. */\nexport function readContextDetail(\n sessionId: string,\n environment: Readonly<Record<string, string | undefined>> = process.env,\n): ContextDetailFile | undefined {\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(contextDetailPath(sessionId, environment), 'utf8'));\n return isDetailFile(parsed) ? parsed : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** One row's detail, matched the way the panel identifies it. */\nexport function findContextItem(\n file: ContextDetailFile,\n itemKind: ContextItemKind,\n name: string,\n): ContextItemDetail | undefined {\n return file.items.find((item) => item.itemKind === itemKind && item.name === name);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAqBA,MAAM,iBAAiB;AACvB,MAAM,gBAAgB;;AAEtB,MAAM,yBAAyB;AAC/B,MAAM,oBAAoB;AAE1B,SAAS,eAAe,aAAwC;CAC9D,OAAO,YAAY,kBAAkBA,UAAAA,QAAK,KAAKC,QAAAA,QAAG,QAAQ,GAAG,OAAO,OAAO;AAC7E;;AAGA,SAAS,SAAS,WAA2B;CAC3C,OAAO,UAAU,QAAQ,qBAAqB,GAAG;AACnD;AAEA,SAAgB,kBACd,WACA,cAA4D,QAAQ,KAC5D;CACR,OAAOD,UAAAA,QAAK,KAAK,eAAe,WAAW,GAAG,gBAAgB,GAAG,SAAS,SAAS,EAAE,MAAM;AAC7F;;;;;;;;AASA,SAAgB,mBACd,WACA,UACA,OACA,cAA4D,QAAQ,KAC3D;CACT,MAAM,SAAS,kBAAkB,WAAW,WAAW;CACvD,MAAM,UAA6B;EAAE,SAAA;EAAiC;EAAU;CAAM;CACtF,MAAM,YAAY,GAAG,OAAO,GAAG,OAAO,QAAQ,GAAG,EAAE;CACnD,IAAI;EACF,QAAA,QAAG,UAAUA,UAAAA,QAAK,QAAQ,MAAM,GAAG;GAAE,WAAW;GAAM,MAAM;EAAuB,CAAC;EACpF,QAAA,QAAG,cAAc,WAAW,KAAK,UAAU,OAAO,GAAG,EAAE,MAAM,kBAAkB,CAAC;EAChF,QAAA,QAAG,WAAW,WAAW,MAAM;EAC/B,OAAO;CACT,QAAQ;EACN,IAAI;GACF,QAAA,QAAG,OAAO,WAAW,EAAE,OAAO,KAAK,CAAC;EACtC,QAAQ,CAER;EACA,OAAO;CACT;AACF;;AAGA,SAAgB,oBACd,WACA,cAA4D,QAAQ,KAC9D;CACN,IAAI;EACF,QAAA,QAAG,OAAO,kBAAkB,WAAW,WAAW,GAAG,EAAE,OAAO,KAAK,CAAC;CACtE,QAAQ,CAGR;AACF;AAEA,SAAS,aAAa,OAA4C;CAChE,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CACxD,MAAM,YAAY;CAClB,OAAO,UAAU,YAAA,KAAsC,MAAM,QAAQ,UAAU,KAAK;AACtF;;AAGA,SAAgB,kBACd,WACA,cAA4D,QAAQ,KACrC;CAC/B,IAAI;EACF,MAAM,SAAkB,KAAK,MAAME,QAAAA,QAAG,aAAa,kBAAkB,WAAW,WAAW,GAAG,MAAM,CAAC;EACrG,OAAO,aAAa,MAAM,IAAI,SAAS,KAAA;CACzC,QAAQ;EACN;CACF;AACF;;AAGA,SAAgB,gBACd,MACA,UACA,MAC+B;CAC/B,OAAO,KAAK,MAAM,MAAM,SAAS,KAAK,aAAa,YAAY,KAAK,SAAS,IAAI;AACnF"}
1
+ {"version":3,"file":"contextDetailStore.cjs","names":["path","os","fs"],"sources":["../src/services/contextDetailStore/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { CONTEXT_DETAIL_VERSION } from '../../constants/contextApi';\nimport type { ContextDetailFile, ContextItemDetail, ContextItemKind } from '../../types/contextApi';\n\n/**\n * Where the agent leaves the composition's detail for the session API to find.\n *\n * The two are separate processes: the inventory only exists inside the agent,\n * and the API that answers the browser is served by the session's host. Every\n * other package-owned API in the repository bridges that gap through the\n * filesystem rather than through the frame protocol, and this one follows,\n * because a file is also what makes the answer survive the agent going quiet\n * between turns.\n *\n * One file per session, replaced whole. The detail is a projection of the\n * composition, not a log of it, so an older copy has no reader.\n */\n\nconst DIRECTORY_NAME = 'doom-context';\nconst AGENT_DIR_ENV = 'PI_CODING_AGENT_DIR';\n/** Owner-only: a tool schema is not secret, but it describes the machine. */\nconst PRIVATE_DIRECTORY_MODE = 0o700;\nconst PRIVATE_FILE_MODE = 0o600;\n\nexport function agentDirectory(environment: Readonly<Record<string, string | undefined>>): string {\n return environment[AGENT_DIR_ENV] || path.join(os.homedir(), '.pi', 'agent');\n}\n\n/**\n * A session id reaches this from a query string, so it never becomes a path.\n * `.` and `..` pass through unchanged: a caller using the result as a directory must refuse them.\n */\nexport function safeName(sessionId: string): string {\n return sessionId.replace(/[^a-zA-Z0-9._-]/gu, '_');\n}\n\nexport function contextDetailPath(\n sessionId: string,\n environment: Readonly<Record<string, string | undefined>> = process.env,\n): string {\n return path.join(agentDirectory(environment), DIRECTORY_NAME, `${safeName(sessionId)}.json`);\n}\n\n/**\n * Publishes the detail for one session.\n *\n * Written through a temporary file so a reader never sees half a document, and\n * failures are reported to the caller rather than thrown: the panel works\n * without this, and a session must not be lost to a full disk.\n */\nexport function writeContextDetail(\n sessionId: string,\n revision: number,\n items: readonly ContextItemDetail[],\n environment: Readonly<Record<string, string | undefined>> = process.env,\n): boolean {\n const target = contextDetailPath(sessionId, environment);\n const payload: ContextDetailFile = { version: CONTEXT_DETAIL_VERSION, revision, items };\n const temporary = `${target}.${String(process.pid)}.tmp`;\n try {\n fs.mkdirSync(path.dirname(target), { recursive: true, mode: PRIVATE_DIRECTORY_MODE });\n fs.writeFileSync(temporary, JSON.stringify(payload), { mode: PRIVATE_FILE_MODE });\n fs.renameSync(temporary, target);\n return true;\n } catch {\n try {\n fs.rmSync(temporary, { force: true });\n } catch {\n // The temporary file is already unreachable; nothing further to do.\n }\n return false;\n }\n}\n\n/** Drops a session's file. Called when the session ends, so the directory does not grow forever. */\nexport function removeContextDetail(\n sessionId: string,\n environment: Readonly<Record<string, string | undefined>> = process.env,\n): void {\n try {\n fs.rmSync(contextDetailPath(sessionId, environment), { force: true });\n } catch {\n // A file that cannot be removed is stale, not fatal; the next session with\n // this id overwrites it.\n }\n}\n\nfunction isDetailFile(value: unknown): value is ContextDetailFile {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Record<string, unknown>;\n return candidate.version === CONTEXT_DETAIL_VERSION && Array.isArray(candidate.items);\n}\n\n/** What the session wrote, or undefined when it has not written yet. */\nexport function readContextDetail(\n sessionId: string,\n environment: Readonly<Record<string, string | undefined>> = process.env,\n): ContextDetailFile | undefined {\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(contextDetailPath(sessionId, environment), 'utf8'));\n return isDetailFile(parsed) ? parsed : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** One row's detail, matched the way the panel identifies it. */\nexport function findContextItem(\n file: ContextDetailFile,\n itemKind: ContextItemKind,\n name: string,\n): ContextItemDetail | undefined {\n return file.items.find((item) => item.itemKind === itemKind && item.name === name);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAqBA,MAAM,iBAAiB;AACvB,MAAM,gBAAgB;;AAEtB,MAAM,yBAAyB;AAC/B,MAAM,oBAAoB;AAE1B,SAAgB,eAAe,aAAmE;CAChG,OAAO,YAAY,kBAAkBA,UAAAA,QAAK,KAAKC,QAAAA,QAAG,QAAQ,GAAG,OAAO,OAAO;AAC7E;;;;;AAMA,SAAgB,SAAS,WAA2B;CAClD,OAAO,UAAU,QAAQ,qBAAqB,GAAG;AACnD;AAEA,SAAgB,kBACd,WACA,cAA4D,QAAQ,KAC5D;CACR,OAAOD,UAAAA,QAAK,KAAK,eAAe,WAAW,GAAG,gBAAgB,GAAG,SAAS,SAAS,EAAE,MAAM;AAC7F;;;;;;;;AASA,SAAgB,mBACd,WACA,UACA,OACA,cAA4D,QAAQ,KAC3D;CACT,MAAM,SAAS,kBAAkB,WAAW,WAAW;CACvD,MAAM,UAA6B;EAAE,SAAA;EAAiC;EAAU;CAAM;CACtF,MAAM,YAAY,GAAG,OAAO,GAAG,OAAO,QAAQ,GAAG,EAAE;CACnD,IAAI;EACF,QAAA,QAAG,UAAUA,UAAAA,QAAK,QAAQ,MAAM,GAAG;GAAE,WAAW;GAAM,MAAM;EAAuB,CAAC;EACpF,QAAA,QAAG,cAAc,WAAW,KAAK,UAAU,OAAO,GAAG,EAAE,MAAM,kBAAkB,CAAC;EAChF,QAAA,QAAG,WAAW,WAAW,MAAM;EAC/B,OAAO;CACT,QAAQ;EACN,IAAI;GACF,QAAA,QAAG,OAAO,WAAW,EAAE,OAAO,KAAK,CAAC;EACtC,QAAQ,CAER;EACA,OAAO;CACT;AACF;;AAGA,SAAgB,oBACd,WACA,cAA4D,QAAQ,KAC9D;CACN,IAAI;EACF,QAAA,QAAG,OAAO,kBAAkB,WAAW,WAAW,GAAG,EAAE,OAAO,KAAK,CAAC;CACtE,QAAQ,CAGR;AACF;AAEA,SAAS,aAAa,OAA4C;CAChE,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CACxD,MAAM,YAAY;CAClB,OAAO,UAAU,YAAA,KAAsC,MAAM,QAAQ,UAAU,KAAK;AACtF;;AAGA,SAAgB,kBACd,WACA,cAA4D,QAAQ,KACrC;CAC/B,IAAI;EACF,MAAM,SAAkB,KAAK,MAAME,QAAAA,QAAG,aAAa,kBAAkB,WAAW,WAAW,GAAG,MAAM,CAAC;EACrG,OAAO,aAAa,MAAM,IAAI,SAAS,KAAA;CACzC,QAAQ;EACN;CACF;AACF;;AAGA,SAAgB,gBACd,MACA,UACA,MAC+B;CAC/B,OAAO,KAAK,MAAM,MAAM,SAAS,KAAK,aAAa,YAAY,KAAK,SAAS,IAAI;AACnF"}
@@ -1,5 +1,11 @@
1
1
  import { ContextDetailFile, ContextItemDetail, ContextItemKind } from "./typesContextApi.mjs";
2
2
  //#region src/services/contextDetailStore/index.d.ts
3
+ export declare function agentDirectory(environment: Readonly<Record<string, string | undefined>>): string;
4
+ /**
5
+ * A session id reaches this from a query string, so it never becomes a path.
6
+ * `.` and `..` pass through unchanged: a caller using the result as a directory must refuse them.
7
+ */
8
+ export declare function safeName(sessionId: string): string;
3
9
  export declare function contextDetailPath(sessionId: string, environment?: Readonly<Record<string, string | undefined>>): string;
4
10
  /**
5
11
  * Publishes the detail for one session.
@@ -1 +1 @@
1
- {"version":3,"file":"contextDetailStore.d.mts","names":[],"sources":["../src/services/contextDetailStore/index.ts"],"mappings":";;wBAoCgB,kBACd,mBACA,cAAa,SAAS;;;;;;;;wBAYR,mBACd,mBACA,kBACA,gBAAgB,qBAChB,cAAa,SAAS;;wBAqBR,oBACd,mBACA,cAAa,SAAS;;wBAiBR,kBACd,mBACA,cAAa,SAAS,sCACrB;;wBAUa,gBACd,MAAM,mBACN,UAAU,iBACV,eACC"}
1
+ {"version":3,"file":"contextDetailStore.d.mts","names":[],"sources":["../src/services/contextDetailStore/index.ts"],"mappings":";;wBA2BgB,eAAe,aAAa,SAAS;;;;;wBAQrC,SAAS;wBAIT,kBACd,mBACA,cAAa,SAAS;;;;;;;;wBAYR,mBACd,mBACA,kBACA,gBAAgB,qBAChB,cAAa,SAAS;;wBAqBR,oBACd,mBACA,cAAa,SAAS;;wBAiBR,kBACd,mBACA,cAAa,SAAS,sCACrB;;wBAUa,gBACd,MAAM,mBACN,UAAU,iBACV,eACC"}
@@ -24,7 +24,10 @@ const PRIVATE_FILE_MODE = 384;
24
24
  function agentDirectory(environment) {
25
25
  return environment[AGENT_DIR_ENV] || path.join(os.homedir(), ".pi", "agent");
26
26
  }
27
- /** A session id reaches this from a query string, so it never becomes a path. */
27
+ /**
28
+ * A session id reaches this from a query string, so it never becomes a path.
29
+ * `.` and `..` pass through unchanged: a caller using the result as a directory must refuse them.
30
+ */
28
31
  function safeName(sessionId) {
29
32
  return sessionId.replace(/[^a-zA-Z0-9._-]/gu, "_");
30
33
  }
@@ -86,6 +89,6 @@ function findContextItem(file, itemKind, name) {
86
89
  return file.items.find((item) => item.itemKind === itemKind && item.name === name);
87
90
  }
88
91
  //#endregion
89
- export { contextDetailPath, findContextItem, readContextDetail, removeContextDetail, writeContextDetail };
92
+ export { agentDirectory, contextDetailPath, findContextItem, readContextDetail, removeContextDetail, safeName, writeContextDetail };
90
93
 
91
94
  //# sourceMappingURL=contextDetailStore.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"contextDetailStore.mjs","names":[],"sources":["../src/services/contextDetailStore/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { CONTEXT_DETAIL_VERSION } from '../../constants/contextApi';\nimport type { ContextDetailFile, ContextItemDetail, ContextItemKind } from '../../types/contextApi';\n\n/**\n * Where the agent leaves the composition's detail for the session API to find.\n *\n * The two are separate processes: the inventory only exists inside the agent,\n * and the API that answers the browser is served by the session's host. Every\n * other package-owned API in the repository bridges that gap through the\n * filesystem rather than through the frame protocol, and this one follows,\n * because a file is also what makes the answer survive the agent going quiet\n * between turns.\n *\n * One file per session, replaced whole. The detail is a projection of the\n * composition, not a log of it, so an older copy has no reader.\n */\n\nconst DIRECTORY_NAME = 'doom-context';\nconst AGENT_DIR_ENV = 'PI_CODING_AGENT_DIR';\n/** Owner-only: a tool schema is not secret, but it describes the machine. */\nconst PRIVATE_DIRECTORY_MODE = 0o700;\nconst PRIVATE_FILE_MODE = 0o600;\n\nfunction agentDirectory(environment: NodeJS.ProcessEnv): string {\n return environment[AGENT_DIR_ENV] || path.join(os.homedir(), '.pi', 'agent');\n}\n\n/** A session id reaches this from a query string, so it never becomes a path. */\nfunction safeName(sessionId: string): string {\n return sessionId.replace(/[^a-zA-Z0-9._-]/gu, '_');\n}\n\nexport function contextDetailPath(\n sessionId: string,\n environment: Readonly<Record<string, string | undefined>> = process.env,\n): string {\n return path.join(agentDirectory(environment), DIRECTORY_NAME, `${safeName(sessionId)}.json`);\n}\n\n/**\n * Publishes the detail for one session.\n *\n * Written through a temporary file so a reader never sees half a document, and\n * failures are reported to the caller rather than thrown: the panel works\n * without this, and a session must not be lost to a full disk.\n */\nexport function writeContextDetail(\n sessionId: string,\n revision: number,\n items: readonly ContextItemDetail[],\n environment: Readonly<Record<string, string | undefined>> = process.env,\n): boolean {\n const target = contextDetailPath(sessionId, environment);\n const payload: ContextDetailFile = { version: CONTEXT_DETAIL_VERSION, revision, items };\n const temporary = `${target}.${String(process.pid)}.tmp`;\n try {\n fs.mkdirSync(path.dirname(target), { recursive: true, mode: PRIVATE_DIRECTORY_MODE });\n fs.writeFileSync(temporary, JSON.stringify(payload), { mode: PRIVATE_FILE_MODE });\n fs.renameSync(temporary, target);\n return true;\n } catch {\n try {\n fs.rmSync(temporary, { force: true });\n } catch {\n // The temporary file is already unreachable; nothing further to do.\n }\n return false;\n }\n}\n\n/** Drops a session's file. Called when the session ends, so the directory does not grow forever. */\nexport function removeContextDetail(\n sessionId: string,\n environment: Readonly<Record<string, string | undefined>> = process.env,\n): void {\n try {\n fs.rmSync(contextDetailPath(sessionId, environment), { force: true });\n } catch {\n // A file that cannot be removed is stale, not fatal; the next session with\n // this id overwrites it.\n }\n}\n\nfunction isDetailFile(value: unknown): value is ContextDetailFile {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Record<string, unknown>;\n return candidate.version === CONTEXT_DETAIL_VERSION && Array.isArray(candidate.items);\n}\n\n/** What the session wrote, or undefined when it has not written yet. */\nexport function readContextDetail(\n sessionId: string,\n environment: Readonly<Record<string, string | undefined>> = process.env,\n): ContextDetailFile | undefined {\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(contextDetailPath(sessionId, environment), 'utf8'));\n return isDetailFile(parsed) ? parsed : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** One row's detail, matched the way the panel identifies it. */\nexport function findContextItem(\n file: ContextDetailFile,\n itemKind: ContextItemKind,\n name: string,\n): ContextItemDetail | undefined {\n return file.items.find((item) => item.itemKind === itemKind && item.name === name);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAqBA,MAAM,iBAAiB;AACvB,MAAM,gBAAgB;;AAEtB,MAAM,yBAAyB;AAC/B,MAAM,oBAAoB;AAE1B,SAAS,eAAe,aAAwC;CAC9D,OAAO,YAAY,kBAAkB,KAAK,KAAK,GAAG,QAAQ,GAAG,OAAO,OAAO;AAC7E;;AAGA,SAAS,SAAS,WAA2B;CAC3C,OAAO,UAAU,QAAQ,qBAAqB,GAAG;AACnD;AAEA,SAAgB,kBACd,WACA,cAA4D,QAAQ,KAC5D;CACR,OAAO,KAAK,KAAK,eAAe,WAAW,GAAG,gBAAgB,GAAG,SAAS,SAAS,EAAE,MAAM;AAC7F;;;;;;;;AASA,SAAgB,mBACd,WACA,UACA,OACA,cAA4D,QAAQ,KAC3D;CACT,MAAM,SAAS,kBAAkB,WAAW,WAAW;CACvD,MAAM,UAA6B;EAAE,SAAA;EAAiC;EAAU;CAAM;CACtF,MAAM,YAAY,GAAG,OAAO,GAAG,OAAO,QAAQ,GAAG,EAAE;CACnD,IAAI;EACF,GAAG,UAAU,KAAK,QAAQ,MAAM,GAAG;GAAE,WAAW;GAAM,MAAM;EAAuB,CAAC;EACpF,GAAG,cAAc,WAAW,KAAK,UAAU,OAAO,GAAG,EAAE,MAAM,kBAAkB,CAAC;EAChF,GAAG,WAAW,WAAW,MAAM;EAC/B,OAAO;CACT,QAAQ;EACN,IAAI;GACF,GAAG,OAAO,WAAW,EAAE,OAAO,KAAK,CAAC;EACtC,QAAQ,CAER;EACA,OAAO;CACT;AACF;;AAGA,SAAgB,oBACd,WACA,cAA4D,QAAQ,KAC9D;CACN,IAAI;EACF,GAAG,OAAO,kBAAkB,WAAW,WAAW,GAAG,EAAE,OAAO,KAAK,CAAC;CACtE,QAAQ,CAGR;AACF;AAEA,SAAS,aAAa,OAA4C;CAChE,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CACxD,MAAM,YAAY;CAClB,OAAO,UAAU,YAAA,KAAsC,MAAM,QAAQ,UAAU,KAAK;AACtF;;AAGA,SAAgB,kBACd,WACA,cAA4D,QAAQ,KACrC;CAC/B,IAAI;EACF,MAAM,SAAkB,KAAK,MAAM,GAAG,aAAa,kBAAkB,WAAW,WAAW,GAAG,MAAM,CAAC;EACrG,OAAO,aAAa,MAAM,IAAI,SAAS,KAAA;CACzC,QAAQ;EACN;CACF;AACF;;AAGA,SAAgB,gBACd,MACA,UACA,MAC+B;CAC/B,OAAO,KAAK,MAAM,MAAM,SAAS,KAAK,aAAa,YAAY,KAAK,SAAS,IAAI;AACnF"}
1
+ {"version":3,"file":"contextDetailStore.mjs","names":[],"sources":["../src/services/contextDetailStore/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { CONTEXT_DETAIL_VERSION } from '../../constants/contextApi';\nimport type { ContextDetailFile, ContextItemDetail, ContextItemKind } from '../../types/contextApi';\n\n/**\n * Where the agent leaves the composition's detail for the session API to find.\n *\n * The two are separate processes: the inventory only exists inside the agent,\n * and the API that answers the browser is served by the session's host. Every\n * other package-owned API in the repository bridges that gap through the\n * filesystem rather than through the frame protocol, and this one follows,\n * because a file is also what makes the answer survive the agent going quiet\n * between turns.\n *\n * One file per session, replaced whole. The detail is a projection of the\n * composition, not a log of it, so an older copy has no reader.\n */\n\nconst DIRECTORY_NAME = 'doom-context';\nconst AGENT_DIR_ENV = 'PI_CODING_AGENT_DIR';\n/** Owner-only: a tool schema is not secret, but it describes the machine. */\nconst PRIVATE_DIRECTORY_MODE = 0o700;\nconst PRIVATE_FILE_MODE = 0o600;\n\nexport function agentDirectory(environment: Readonly<Record<string, string | undefined>>): string {\n return environment[AGENT_DIR_ENV] || path.join(os.homedir(), '.pi', 'agent');\n}\n\n/**\n * A session id reaches this from a query string, so it never becomes a path.\n * `.` and `..` pass through unchanged: a caller using the result as a directory must refuse them.\n */\nexport function safeName(sessionId: string): string {\n return sessionId.replace(/[^a-zA-Z0-9._-]/gu, '_');\n}\n\nexport function contextDetailPath(\n sessionId: string,\n environment: Readonly<Record<string, string | undefined>> = process.env,\n): string {\n return path.join(agentDirectory(environment), DIRECTORY_NAME, `${safeName(sessionId)}.json`);\n}\n\n/**\n * Publishes the detail for one session.\n *\n * Written through a temporary file so a reader never sees half a document, and\n * failures are reported to the caller rather than thrown: the panel works\n * without this, and a session must not be lost to a full disk.\n */\nexport function writeContextDetail(\n sessionId: string,\n revision: number,\n items: readonly ContextItemDetail[],\n environment: Readonly<Record<string, string | undefined>> = process.env,\n): boolean {\n const target = contextDetailPath(sessionId, environment);\n const payload: ContextDetailFile = { version: CONTEXT_DETAIL_VERSION, revision, items };\n const temporary = `${target}.${String(process.pid)}.tmp`;\n try {\n fs.mkdirSync(path.dirname(target), { recursive: true, mode: PRIVATE_DIRECTORY_MODE });\n fs.writeFileSync(temporary, JSON.stringify(payload), { mode: PRIVATE_FILE_MODE });\n fs.renameSync(temporary, target);\n return true;\n } catch {\n try {\n fs.rmSync(temporary, { force: true });\n } catch {\n // The temporary file is already unreachable; nothing further to do.\n }\n return false;\n }\n}\n\n/** Drops a session's file. Called when the session ends, so the directory does not grow forever. */\nexport function removeContextDetail(\n sessionId: string,\n environment: Readonly<Record<string, string | undefined>> = process.env,\n): void {\n try {\n fs.rmSync(contextDetailPath(sessionId, environment), { force: true });\n } catch {\n // A file that cannot be removed is stale, not fatal; the next session with\n // this id overwrites it.\n }\n}\n\nfunction isDetailFile(value: unknown): value is ContextDetailFile {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Record<string, unknown>;\n return candidate.version === CONTEXT_DETAIL_VERSION && Array.isArray(candidate.items);\n}\n\n/** What the session wrote, or undefined when it has not written yet. */\nexport function readContextDetail(\n sessionId: string,\n environment: Readonly<Record<string, string | undefined>> = process.env,\n): ContextDetailFile | undefined {\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(contextDetailPath(sessionId, environment), 'utf8'));\n return isDetailFile(parsed) ? parsed : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** One row's detail, matched the way the panel identifies it. */\nexport function findContextItem(\n file: ContextDetailFile,\n itemKind: ContextItemKind,\n name: string,\n): ContextItemDetail | undefined {\n return file.items.find((item) => item.itemKind === itemKind && item.name === name);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAqBA,MAAM,iBAAiB;AACvB,MAAM,gBAAgB;;AAEtB,MAAM,yBAAyB;AAC/B,MAAM,oBAAoB;AAE1B,SAAgB,eAAe,aAAmE;CAChG,OAAO,YAAY,kBAAkB,KAAK,KAAK,GAAG,QAAQ,GAAG,OAAO,OAAO;AAC7E;;;;;AAMA,SAAgB,SAAS,WAA2B;CAClD,OAAO,UAAU,QAAQ,qBAAqB,GAAG;AACnD;AAEA,SAAgB,kBACd,WACA,cAA4D,QAAQ,KAC5D;CACR,OAAO,KAAK,KAAK,eAAe,WAAW,GAAG,gBAAgB,GAAG,SAAS,SAAS,EAAE,MAAM;AAC7F;;;;;;;;AASA,SAAgB,mBACd,WACA,UACA,OACA,cAA4D,QAAQ,KAC3D;CACT,MAAM,SAAS,kBAAkB,WAAW,WAAW;CACvD,MAAM,UAA6B;EAAE,SAAA;EAAiC;EAAU;CAAM;CACtF,MAAM,YAAY,GAAG,OAAO,GAAG,OAAO,QAAQ,GAAG,EAAE;CACnD,IAAI;EACF,GAAG,UAAU,KAAK,QAAQ,MAAM,GAAG;GAAE,WAAW;GAAM,MAAM;EAAuB,CAAC;EACpF,GAAG,cAAc,WAAW,KAAK,UAAU,OAAO,GAAG,EAAE,MAAM,kBAAkB,CAAC;EAChF,GAAG,WAAW,WAAW,MAAM;EAC/B,OAAO;CACT,QAAQ;EACN,IAAI;GACF,GAAG,OAAO,WAAW,EAAE,OAAO,KAAK,CAAC;EACtC,QAAQ,CAER;EACA,OAAO;CACT;AACF;;AAGA,SAAgB,oBACd,WACA,cAA4D,QAAQ,KAC9D;CACN,IAAI;EACF,GAAG,OAAO,kBAAkB,WAAW,WAAW,GAAG,EAAE,OAAO,KAAK,CAAC;CACtE,QAAQ,CAGR;AACF;AAEA,SAAS,aAAa,OAA4C;CAChE,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CACxD,MAAM,YAAY;CAClB,OAAO,UAAU,YAAA,KAAsC,MAAM,QAAQ,UAAU,KAAK;AACtF;;AAGA,SAAgB,kBACd,WACA,cAA4D,QAAQ,KACrC;CAC/B,IAAI;EACF,MAAM,SAAkB,KAAK,MAAM,GAAG,aAAa,kBAAkB,WAAW,WAAW,GAAG,MAAM,CAAC;EACrG,OAAO,aAAa,MAAM,IAAI,SAAS,KAAA;CACzC,QAAQ;EACN;CACF;AACF;;AAGA,SAAgB,gBACd,MACA,UACA,MAC+B;CAC/B,OAAO,KAAK,MAAM,MAAM,SAAS,KAAK,aAAa,YAAY,KAAK,SAAS,IAAI;AACnF"}
@@ -5,9 +5,11 @@ const require_harnessErrorMessage = require("./src/server/harnessErrorMessage.cj
5
5
  const require_contextDetailStore = require("./contextDetailStore.cjs");
6
6
  const require_serverTelemetry = require("./serverTelemetry.cjs");
7
7
  const require_packageApi = require("./src/schemas/packageApi.cjs");
8
- const require_index = require("./src/services/sessionMcpActivity/index.cjs");
9
- const require_index$1 = require("./src/services/sessionMcpConversations/index.cjs");
10
- const require_index$2 = require("./src/services/sessionMcpRegistrationStore/index.cjs");
8
+ const require_sessionFiles = require("./src/constants/sessionFiles.cjs");
9
+ const require_index = require("./src/services/sessionAttachments/index.cjs");
10
+ const require_index$1 = require("./src/services/sessionMcpActivity/index.cjs");
11
+ const require_index$2 = require("./src/services/sessionMcpConversations/index.cjs");
12
+ const require_index$3 = require("./src/services/sessionMcpRegistrationStore/index.cjs");
11
13
  const require_runtimeContextApi = require("./runtimeContextApi.cjs");
12
14
  const require_headlessProtocol = require("./src/server/headlessProtocol.cjs");
13
15
  const require_sessionMcpRoutes = require("./src/server/sessionMcpRoutes.cjs");
@@ -40,27 +42,11 @@ const DOOM_PACKAGE_API_PATH_PATTERN = /^\/api(?:\/workspaces\/([^/]+)(?:\/sessio
40
42
  const HEALTH_ROLE = "hub";
41
43
  const PROTOCOL_VERSION = 1;
42
44
  const MAX_BODY_BYTES = 8388608;
43
- const MAX_SESSION_FILE_BYTES = 26214400;
44
45
  const DIRECTORY_SUGGESTION_LIMIT = 12;
45
46
  const DIRECTORY_CHILDREN_LIMIT = 500;
46
47
  const HOME_PREFIX = "~";
47
48
  const WORKSPACE_NAME_MAX_LENGTH = 80;
48
49
  const WORKSPACE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 ._-]*$/u;
49
- const SESSION_FILE_CONTENT_TYPES = {
50
- ".avif": "image/avif",
51
- ".bmp": "image/bmp",
52
- ".gif": "image/gif",
53
- ".jpeg": "image/jpeg",
54
- ".jpg": "image/jpeg",
55
- ".png": "image/png",
56
- ".svg": "image/svg+xml",
57
- ".webp": "image/webp",
58
- ".m4v": "video/mp4",
59
- ".mov": "video/quicktime",
60
- ".mp4": "video/mp4",
61
- ".webm": "video/webm",
62
- ".pdf": "application/pdf"
63
- };
64
50
  function sessionView(session) {
65
51
  return {
66
52
  id: session.id,
@@ -167,11 +153,11 @@ async function sessionFile(session, requestedPath) {
167
153
  try {
168
154
  const stat = await handle.stat();
169
155
  if (!stat.isFile()) return Response.json({ error: "Session file not found." }, { status: 404 });
170
- if (stat.size > MAX_SESSION_FILE_BYTES) return Response.json({ error: "Session file is too large." }, { status: 413 });
156
+ if (stat.size > 26214400) return Response.json({ error: "Session file is too large." }, { status: 413 });
171
157
  const bytes = await handle.readFile();
172
- if (bytes.byteLength > MAX_SESSION_FILE_BYTES) return Response.json({ error: "Session file is too large." }, { status: 413 });
158
+ if (bytes.byteLength > 26214400) return Response.json({ error: "Session file is too large." }, { status: 413 });
173
159
  return new Response(bytes, { headers: {
174
- "content-type": SESSION_FILE_CONTENT_TYPES[node_path.default.extname(filePath).toLowerCase()] ?? "application/octet-stream",
160
+ "content-type": require_sessionFiles.SESSION_FILE_CONTENT_TYPES[node_path.default.extname(filePath).toLowerCase()] ?? "application/octet-stream",
175
161
  "content-length": String(bytes.byteLength),
176
162
  "cache-control": "no-store",
177
163
  "x-content-type-options": "nosniff"
@@ -243,13 +229,18 @@ async function directoryChildren(requested) {
243
229
  directories
244
230
  };
245
231
  }
232
+ var RequestBodyTooLargeError = class extends Error {
233
+ constructor() {
234
+ super("Request body is too large.");
235
+ }
236
+ };
246
237
  async function readBody(request) {
247
238
  const chunks = [];
248
239
  let size = 0;
249
240
  for await (const chunk of request) {
250
241
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
251
242
  size += buffer.byteLength;
252
- if (size > MAX_BODY_BYTES) throw new Error("Request body is too large.");
243
+ if (size > MAX_BODY_BYTES) throw new RequestBodyTooLargeError();
253
244
  chunks.push(buffer);
254
245
  }
255
246
  return Buffer.concat(chunks);
@@ -367,9 +358,9 @@ async function serveHeadlessServer(options) {
367
358
  publicOriginRevision: options.sessionMcpPublicOriginRevision,
368
359
  onNotice: options.onNotice,
369
360
  isSessionPersisted: options.isSessionPersisted,
370
- activityStore: require_index.createSessionMcpActivityStore(sessionMcpStateDir),
371
- conversationStore: require_index$1.createSessionMcpConversationStore(sessionMcpStateDir),
372
- registrationStore: require_index$2.createSessionMcpRegistrationStore({
361
+ activityStore: require_index$1.createSessionMcpActivityStore(sessionMcpStateDir),
362
+ conversationStore: require_index$2.createSessionMcpConversationStore(sessionMcpStateDir),
363
+ registrationStore: require_index$3.createSessionMcpRegistrationStore({
373
364
  stateDir: sessionMcpStateDir,
374
365
  onNotice: options.onNotice
375
366
  })
@@ -793,6 +784,18 @@ async function serveHeadlessServer(options) {
793
784
  await writeResponse(response, await sessionFile(session, url.searchParams.get("path")));
794
785
  return;
795
786
  }
787
+ if (suffix === "/attachments" && request.method === "POST") {
788
+ let bytes;
789
+ try {
790
+ bytes = await readBody(request);
791
+ } catch (error) {
792
+ if (!(error instanceof RequestBodyTooLargeError)) throw error;
793
+ json(response, 413, { error: "The attachment exceeds the 8 MB upload limit." });
794
+ return;
795
+ }
796
+ json(response, 201, await require_index.writeSessionAttachment(sessionId, url.searchParams.get("name") ?? "file", bytes));
797
+ return;
798
+ }
796
799
  if (suffix === "/restart" && request.method === "POST" && options.restartSession) {
797
800
  const restart = options.restartSession(session);
798
801
  const settled = restart.then(() => void 0, () => void 0);