@tekmidian/pai 0.66.6 → 0.66.8

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 (139) hide show
  1. package/dist/{aibroker-client-WCZi_tn-.mjs → aibroker-client-BfnVwGZl.mjs} +3 -3
  2. package/dist/{aibroker-client-WCZi_tn-.mjs.map → aibroker-client-BfnVwGZl.mjs.map} +1 -1
  3. package/dist/{aibroker-client-Ck3p3k_-.mjs → aibroker-client-C-O3Fflh.mjs} +1 -1
  4. package/dist/{auto-route-BnizyALK.mjs → auto-route-D8TBDNqb.mjs} +3 -3
  5. package/dist/{auto-route-BnizyALK.mjs.map → auto-route-D8TBDNqb.mjs.map} +1 -1
  6. package/dist/auto-route-DCxTKz19.mjs +7 -0
  7. package/dist/{chain-2_uoiEdD.mjs → chain-BL4khGgy.mjs} +4 -3
  8. package/dist/{chain-2_uoiEdD.mjs.map → chain-BL4khGgy.mjs.map} +1 -1
  9. package/dist/cli/index.mjs +20 -16
  10. package/dist/cli/index.mjs.map +1 -1
  11. package/dist/cli/program.mjs +19 -15
  12. package/dist/{config-Ddc4DIPk.mjs → config-DjZELvoA.mjs} +12 -3
  13. package/dist/config-DjZELvoA.mjs.map +1 -0
  14. package/dist/{run-env-CQRnxNht.mjs → config-Dpe2JlbY.mjs} +5 -114
  15. package/dist/config-Dpe2JlbY.mjs.map +1 -0
  16. package/dist/{config-CnsDMETf.mjs → config-yomayAwe.mjs} +1 -1
  17. package/dist/{context-handover-cache-SFkWucuT.mjs → context-handover-cache-DFqxNh-H.mjs} +27 -13
  18. package/dist/context-handover-cache-DFqxNh-H.mjs.map +1 -0
  19. package/dist/daemon/index.mjs +20 -15
  20. package/dist/daemon/index.mjs.map +1 -1
  21. package/dist/{daemon-vLtT3yaI.mjs → daemon-CsR1S_Qr.mjs} +762 -24
  22. package/dist/daemon-CsR1S_Qr.mjs.map +1 -0
  23. package/dist/daemon-WrncnGua.mjs +23 -0
  24. package/dist/daemon-mcp/index.mjs +15 -12
  25. package/dist/daemon-mcp/index.mjs.map +1 -1
  26. package/dist/embedding-gate-DqbKvn56.mjs +378 -0
  27. package/dist/embedding-gate-DqbKvn56.mjs.map +1 -0
  28. package/dist/embedding-gate-SJMCPwxG.mjs +5 -0
  29. package/dist/{embeddings-DMVzpvRZ.mjs → embeddings-BUjZMD9F.mjs} +16 -2
  30. package/dist/{embeddings-DMVzpvRZ.mjs.map → embeddings-BUjZMD9F.mjs.map} +1 -1
  31. package/dist/embeddings-DrJ-ewfi.mjs +3 -0
  32. package/dist/{env-DiolswKQ.mjs → env-CPCNHz3i.mjs} +1 -1
  33. package/dist/{env-DiolswKQ.mjs.map → env-CPCNHz3i.mjs.map} +1 -1
  34. package/dist/factory-DjtXElOI.mjs +9 -0
  35. package/dist/{factory-CXFHwvcv.mjs → factory-yElzJc6f.mjs} +31 -9
  36. package/dist/factory-yElzJc6f.mjs.map +1 -0
  37. package/dist/{fallback-DNzd3jJc.mjs → fallback-2yWDDYiT.mjs} +5 -4
  38. package/dist/{fallback-DNzd3jJc.mjs.map → fallback-2yWDDYiT.mjs.map} +1 -1
  39. package/dist/{helpers-CZsi_49C.mjs → helpers-B9LHRJ8R.mjs} +1 -1
  40. package/dist/{helpers-CZsi_49C.mjs.map → helpers-B9LHRJ8R.mjs.map} +1 -1
  41. package/dist/hooks/block-sleep-poll.mjs +7 -2
  42. package/dist/hooks/block-sleep-poll.mjs.map +3 -3
  43. package/dist/hooks/context-compression-hook.mjs +2 -0
  44. package/dist/hooks/context-compression-hook.mjs.map +2 -2
  45. package/dist/hooks/load-core-context.mjs +5 -5
  46. package/dist/hooks/load-core-context.mjs.map +2 -2
  47. package/dist/hooks/load-project-context.mjs +2 -0
  48. package/dist/hooks/load-project-context.mjs.map +2 -2
  49. package/dist/hooks/post-compact-inject.mjs +2 -0
  50. package/dist/hooks/post-compact-inject.mjs.map +2 -2
  51. package/dist/hooks/route-agents-to-worker.mjs +2 -0
  52. package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
  53. package/dist/hooks/security-validator.mjs +2 -0
  54. package/dist/hooks/security-validator.mjs.map +2 -2
  55. package/dist/hooks/whisper-rules.mjs +2 -0
  56. package/dist/hooks/whisper-rules.mjs.map +2 -2
  57. package/dist/hooks/worker-guard.mjs +82 -5
  58. package/dist/hooks/worker-guard.mjs.map +4 -4
  59. package/dist/hooks/worker-proxy.mjs +2 -0
  60. package/dist/hooks/worker-proxy.mjs.map +2 -2
  61. package/dist/hooks/worker-status-line.mjs +7 -2
  62. package/dist/hooks/worker-status-line.mjs.map +3 -3
  63. package/dist/hooks/worker-supervision.mjs +2 -0
  64. package/dist/hooks/worker-supervision.mjs.map +2 -2
  65. package/dist/index.d.mts.map +1 -1
  66. package/dist/index.mjs +7 -4
  67. package/dist/{ipc-client-DkxRay-o.mjs → ipc-client-DVZ_EGnu.mjs} +2 -2
  68. package/dist/{ipc-client-DkxRay-o.mjs.map → ipc-client-DVZ_EGnu.mjs.map} +1 -1
  69. package/dist/main-resolver-DjQsexge.mjs +8 -0
  70. package/dist/{main-resolver-SfGGg1DB.mjs → main-resolver-Dxvn4Mv8.mjs} +7 -6
  71. package/dist/{main-resolver-SfGGg1DB.mjs.map → main-resolver-Dxvn4Mv8.mjs.map} +1 -1
  72. package/dist/{pai-marker-DXVpFsYz.mjs → pai-marker-1B7iC7oq.mjs} +121 -3
  73. package/dist/pai-marker-1B7iC7oq.mjs.map +1 -0
  74. package/dist/{planner-C-gzg7ye.mjs → planner-CspP59ya.mjs} +13 -10
  75. package/dist/{planner-C-gzg7ye.mjs.map → planner-CspP59ya.mjs.map} +1 -1
  76. package/dist/{postgres-BxmyQHoA.mjs → postgres-CCAGq1qp.mjs} +86 -5
  77. package/dist/postgres-CCAGq1qp.mjs.map +1 -0
  78. package/dist/postgres-CzkEPfAm.mjs +7 -0
  79. package/dist/{program-CfG1T_ko.mjs → program-cWqZI0_L.mjs} +231 -125
  80. package/dist/program-cWqZI0_L.mjs.map +1 -0
  81. package/dist/{query-feedback-BPdapXKd.mjs → query-feedback-GPeVGvIa.mjs} +2 -2
  82. package/dist/{query-feedback-BPdapXKd.mjs.map → query-feedback-GPeVGvIa.mjs.map} +1 -1
  83. package/dist/{reranker-3lnggwgq.mjs → reranker-BTiOia8Y.mjs} +1 -1
  84. package/dist/{reranker-3lnggwgq.mjs.map → reranker-BTiOia8Y.mjs.map} +1 -1
  85. package/dist/{reranker-CZ2mP4cf.mjs → reranker-YwDT2n5e.mjs} +1 -1
  86. package/dist/{router-BaTbc9VX.mjs → router-Bk_ySIZT.mjs} +2 -2
  87. package/dist/{router-BaTbc9VX.mjs.map → router-Bk_ySIZT.mjs.map} +1 -1
  88. package/dist/router-DyiZNI6o.mjs +3 -0
  89. package/dist/{run-ChlIbDJ0.mjs → run-BZNdpUGQ.mjs} +226 -1797
  90. package/dist/run-BZNdpUGQ.mjs.map +1 -0
  91. package/dist/run-env-Cd0Son-Y.mjs +115 -0
  92. package/dist/run-env-Cd0Son-Y.mjs.map +1 -0
  93. package/dist/{pai-home-UncxWxlX.mjs → runtime-paths-DkZoxofH.mjs} +50 -2
  94. package/dist/runtime-paths-DkZoxofH.mjs.map +1 -0
  95. package/dist/{search-CNAGTiJP.mjs → search-cPDj-9fO.mjs} +2 -2
  96. package/dist/{search-CNAGTiJP.mjs.map → search-cPDj-9fO.mjs.map} +1 -1
  97. package/dist/{sources-CM2g-CLT.mjs → sources-CvvzNZad.mjs} +1 -1
  98. package/dist/{sources-CM2g-CLT.mjs.map → sources-CvvzNZad.mjs.map} +1 -1
  99. package/dist/{stop-words-DtxaTWU_.mjs → stop-words-LtkS-Pej.mjs} +1 -1
  100. package/dist/{stop-words-DtxaTWU_.mjs.map → stop-words-LtkS-Pej.mjs.map} +1 -1
  101. package/dist/worktree-CwMRTa0H.mjs +1578 -0
  102. package/dist/worktree-CwMRTa0H.mjs.map +1 -0
  103. package/dist/{zettelkasten-BRIaHzFm.mjs → zettelkasten-DSIe6wpF.mjs} +6 -5
  104. package/dist/zettelkasten-DSIe6wpF.mjs.map +1 -0
  105. package/dist/{zettelkasten-G0-ZTybr.mjs → zettelkasten-DtkAvA18.mjs} +5 -3
  106. package/docker/docker-compose.yml +4 -0
  107. package/docs/commands/README.md +6 -0
  108. package/docs/commands/memory.md +52 -0
  109. package/docs/commands/worker.md +22 -0
  110. package/docs/install-linux.md +1 -0
  111. package/docs/memory.md +69 -0
  112. package/package.json +1 -1
  113. package/src/hooks/ts/lib/sleep-poll-gate.test.ts +2 -2
  114. package/src/hooks/ts/lib/sleep-poll-gate.ts +4 -4
  115. package/src/hooks/ts/lib/worker-guard-longwait.test.ts +48 -0
  116. package/src/hooks/ts/lib/worker-guard.ts +50 -6
  117. package/src/hooks/ts/pre-tool-use/block-sleep-poll.test.ts +1 -1
  118. package/src/hooks/ts/session-start/load-core-context.ts +11 -8
  119. package/src/hooks/ts/session-start/session-start-worker-guard.test.ts +34 -2
  120. package/dist/auto-route-D6fW1Q8z.mjs +0 -3
  121. package/dist/config-Ddc4DIPk.mjs.map +0 -1
  122. package/dist/context-handover-cache-SFkWucuT.mjs.map +0 -1
  123. package/dist/daemon-BHQoEjt0.mjs +0 -18
  124. package/dist/daemon-vLtT3yaI.mjs.map +0 -1
  125. package/dist/embeddings-D0WOQpYR.mjs +0 -3
  126. package/dist/factory-CXFHwvcv.mjs.map +0 -1
  127. package/dist/factory-CnKsU6zX.mjs +0 -8
  128. package/dist/main-resolver-DrHtx8iW.mjs +0 -7
  129. package/dist/pai-home-UncxWxlX.mjs.map +0 -1
  130. package/dist/pai-marker-DXVpFsYz.mjs.map +0 -1
  131. package/dist/postgres-B2BEBT-J.mjs +0 -5
  132. package/dist/postgres-BxmyQHoA.mjs.map +0 -1
  133. package/dist/program-CfG1T_ko.mjs.map +0 -1
  134. package/dist/router-BK-hFeQ6.mjs +0 -3
  135. package/dist/run-ChlIbDJ0.mjs.map +0 -1
  136. package/dist/run-env-CQRnxNht.mjs.map +0 -1
  137. package/dist/runtime-paths-Bw3Jk8qJ.mjs +0 -52
  138. package/dist/runtime-paths-Bw3Jk8qJ.mjs.map +0 -1
  139. package/dist/zettelkasten-BRIaHzFm.mjs.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"config-Ddc4DIPk.mjs","names":[],"sources":["../src/notifications/types.ts","../src/tasks/types.ts","../src/config/json-store.ts","../src/config/yaml-store.ts","../src/config/main-config.ts","../src/daemon/config.ts"],"sourcesContent":["/**\n * types.ts — Unified Notification Framework type definitions\n *\n * Defines the channel registry, event routing, and configuration schema\n * for PAI's notification subsystem.\n */\n\n// ---------------------------------------------------------------------------\n// Channel identifiers\n// ---------------------------------------------------------------------------\n\nexport type ChannelId = \"ntfy\" | \"whatsapp\" | \"macos\" | \"voice\" | \"cli\";\n\n// ---------------------------------------------------------------------------\n// Notification event types\n// ---------------------------------------------------------------------------\n\n/**\n * The semantic type of a notification event.\n * Used to route events to the appropriate channels.\n */\nexport type NotificationEvent =\n | \"error\"\n | \"progress\"\n | \"completion\"\n | \"info\"\n | \"debug\";\n\n// ---------------------------------------------------------------------------\n// Notification mode\n// ---------------------------------------------------------------------------\n\n/**\n * The current notification mode.\n *\n * - \"auto\" — Use the per-event routing table (default)\n * - \"voice\" — All events go to voice (WhatsApp TTS)\n * - \"whatsapp\" — All events go to WhatsApp text\n * - \"ntfy\" — All events go to ntfy.sh\n * - \"macos\" — All events go to macOS notifications\n * - \"cli\" — All events go to CLI stdout only\n * - \"off\" — Suppress all notifications\n */\nexport type NotificationMode =\n | \"auto\"\n | \"voice\"\n | \"whatsapp\"\n | \"ntfy\"\n | \"macos\"\n | \"cli\"\n | \"off\";\n\n// ---------------------------------------------------------------------------\n// Per-channel configuration\n// ---------------------------------------------------------------------------\n\nexport interface NtfyChannelConfig {\n enabled: boolean;\n /** ntfy.sh topic URL, e.g. \"https://ntfy.sh/my-topic\" */\n url?: string;\n /** ntfy priority: min | low | default | high | urgent */\n priority?: \"min\" | \"low\" | \"default\" | \"high\" | \"urgent\";\n}\n\nexport interface WhatsAppChannelConfig {\n enabled: boolean;\n /** Optional recipient (phone, JID, or contact name). Omit for self-chat. */\n recipient?: string;\n}\n\nexport interface MacOsChannelConfig {\n enabled: boolean;\n}\n\nexport interface VoiceChannelConfig {\n enabled: boolean;\n /** Kokoro voice name, e.g. \"bm_george\", \"af_bella\". Default: \"bm_george\" */\n voiceName?: string;\n}\n\nexport interface CliChannelConfig {\n enabled: boolean;\n}\n\nexport interface ChannelConfigs {\n ntfy: NtfyChannelConfig;\n whatsapp: WhatsAppChannelConfig;\n macos: MacOsChannelConfig;\n voice: VoiceChannelConfig;\n cli: CliChannelConfig;\n}\n\n// ---------------------------------------------------------------------------\n// Routing table\n// ---------------------------------------------------------------------------\n\n/**\n * Maps each event type to the ordered list of channels that should receive it.\n * Only channels that are enabled in `channels` and present in this list are used.\n */\nexport type RoutingTable = {\n [K in NotificationEvent]: ChannelId[];\n};\n\nexport const DEFAULT_ROUTING: RoutingTable = {\n error: [\"whatsapp\", \"macos\", \"ntfy\", \"cli\"],\n completion: [\"whatsapp\", \"macos\", \"ntfy\", \"cli\"],\n info: [\"cli\"],\n progress: [\"cli\"],\n debug: [],\n};\n\n// ---------------------------------------------------------------------------\n// Top-level notification config (embedded in PaiDaemonConfig)\n// ---------------------------------------------------------------------------\n\nexport interface NotificationConfig {\n /** Current routing mode. Default: \"auto\" */\n mode: NotificationMode;\n /** Per-channel configuration */\n channels: ChannelConfigs;\n /** Event → channel routing (used in \"auto\" mode) */\n routing: RoutingTable;\n}\n\nexport const DEFAULT_CHANNELS: ChannelConfigs = {\n ntfy: {\n enabled: false,\n url: undefined,\n priority: \"default\",\n },\n whatsapp: {\n enabled: true,\n recipient: undefined,\n },\n macos: {\n enabled: true,\n },\n voice: {\n enabled: false,\n voiceName: \"bm_george\",\n },\n cli: {\n enabled: true,\n },\n};\n\nexport const DEFAULT_NOTIFICATION_CONFIG: NotificationConfig = {\n mode: \"auto\",\n channels: DEFAULT_CHANNELS,\n routing: DEFAULT_ROUTING,\n};\n\n// ---------------------------------------------------------------------------\n// Notification payload\n// ---------------------------------------------------------------------------\n\nexport interface NotificationPayload {\n /** Semantic event type — used for routing */\n event: NotificationEvent;\n /** The notification message body */\n message: string;\n /** Optional title (used by macOS, ntfy) */\n title?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Provider interface\n// ---------------------------------------------------------------------------\n\nexport interface NotificationProvider {\n readonly channelId: ChannelId;\n /**\n * Send a notification.\n * Returns true on success, false on failure (failure is non-fatal).\n */\n send(payload: NotificationPayload, config: NotificationConfig): Promise<boolean>;\n}\n\n// ---------------------------------------------------------------------------\n// Send result\n// ---------------------------------------------------------------------------\n\nexport interface SendResult {\n channelsAttempted: ChannelId[];\n channelsSucceeded: ChannelId[];\n channelsFailed: ChannelId[];\n mode: NotificationMode;\n}\n","/**\n * types.ts — Task Bus type definitions\n *\n * Defines the provider registry, ownership resolution, and configuration schema\n * for PAI's cross-session task subsystem.\n *\n * The task bus routes work between PAI sessions through an external tracker.\n * A session files a task; a routine reads it later and dispatches it to the\n * session that owns it — spawning one if none is running.\n *\n * See docs/task-bus.md for the architecture and its constraints.\n */\n\n// ---------------------------------------------------------------------------\n// Provider identifiers\n// ---------------------------------------------------------------------------\n\nexport type ProviderId = \"todoist\";\n\n// ---------------------------------------------------------------------------\n// Ownership\n// ---------------------------------------------------------------------------\n\n/**\n * Prefix marking a tracker label as a PAI ownership assertion.\n * A task labelled `pai:acme-api` is owned by the `acme-api` project.\n */\nexport const OWNER_LABEL_PREFIX = \"pai:\";\n\n/**\n * How a task's owner was determined. Recorded so the routine can explain\n * itself, and so a mis-resolution is diagnosable rather than silent.\n *\n * - \"label\" — an explicit `pai:<project>` label (authoritative)\n * - \"container\" — the enclosing sub-project name matched a PAI alias (fallback)\n * - \"none\" — unresolved; the task stays in the findings inbox\n */\nexport type OwnerSource = \"label\" | \"container\" | \"none\";\n\nexport interface TaskOwner {\n /** Resolved PAI project short name, e.g. \"acme-api\". Null when UNROUTED. */\n project: string | null;\n /** Absolute path to the project root. Null when UNROUTED. */\n rootPath: string | null;\n source: OwnerSource;\n /**\n * The raw string that resolution was attempted against, kept for diagnostics\n * when `source` is \"none\" — e.g. a \"Reading List 📚\" container matches no\n * PAI project, which is expected rather than a fault.\n */\n rawHint?: string;\n}\n\n/** An unresolved owner. UNROUTED is a normal state, not an error. */\nexport const UNROUTED: TaskOwner = {\n project: null,\n rootPath: null,\n source: \"none\",\n};\n\n// ---------------------------------------------------------------------------\n// Tasks\n// ---------------------------------------------------------------------------\n\nexport type TaskPriority = \"p1\" | \"p2\" | \"p3\" | \"p4\";\n\nexport interface Task {\n /** Provider-native task ID. Opaque; never parsed. */\n id: string;\n title: string;\n /**\n * Full procedure AND reasoning — enough that the task is actionable months\n * later, or by the user alone, without re-deriving anything. Enforced at\n * filing time rather than left to discipline.\n */\n body: string;\n owner: TaskOwner;\n /** ISO 8601 date or datetime. Null when the task has no due date. */\n due: string | null;\n /**\n * The tracker's own recurrence text, e.g. \"every day at 08:00\". Null for a\n * one-off.\n *\n * Kept verbatim rather than parsed into a rule because it is also the only\n * way to write a recurrence back: Todoist re-parses this string, and it is\n * what lets a due date be restored without destroying the recurrence.\n */\n recurrence?: string | null;\n priority: TaskPriority;\n labels: string[];\n /**\n * Stable reference to the artifact this task is about. Prefer a `hook://`\n * URL over a filesystem path — it survives renames and moves, and opens in\n * DEVONthink To Go on iOS.\n */\n sourceUrl?: string;\n /** True for organizational headers that cannot be completed. */\n isHeader?: boolean;\n}\n\n/** A task being filed. `owner` is a project short name, resolved on write. */\nexport interface NewTask {\n title: string;\n body: string;\n owner?: string | null;\n due?: string;\n priority?: TaskPriority;\n labels?: string[];\n sourceUrl?: string;\n /**\n * Sub-project to file into, created if absent.\n *\n * The convention is one sub-project per PAI project under the bus root. It\n * previously existed only in the shape of the data, so every session had to\n * re-derive it — and a session that inferred cautiously filed flat instead,\n * which is exactly the pile the convention prevents.\n */\n into?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Provider interface\n// ---------------------------------------------------------------------------\n\nexport interface ListOptions {\n /** Only tasks due on or before this ISO date. Omit for all open tasks. */\n dueBefore?: string;\n /** Restrict to one resolved owner. Omit for every owner. */\n owner?: string;\n /** Include tasks that resolved to UNROUTED. Default: true. */\n includeUnrouted?: boolean;\n limit?: number;\n}\n\nexport interface TaskProvider {\n readonly providerId: ProviderId;\n\n /**\n * False when no credential is configured. The bus degrades to a no-op\n * rather than failing — a user without a tracker still gets working PAI.\n */\n isConfigured(): boolean;\n\n listOpen(opts: ListOptions): Promise<Task[]>;\n add(task: NewTask): Promise<Task>;\n complete(id: string): Promise<void>;\n\n /**\n * Rewrite a task's due date through the tracker's natural-language field.\n *\n * Optional, and deliberately expressed as a string rather than a date: a\n * recurring task's schedule and its next occurrence are the same field, so\n * moving the date without the rule silently downgrades a routine to a one-off.\n * A provider that cannot express both at once should not offer this.\n */\n setDue?(id: string, dueString: string): Promise<void>;\n\n /**\n * Sub-projects under the bus root — the set of addresses a task can be filed\n * against, one per session.\n *\n * Optional because it is not universal: a tracker may address work by tag or\n * list rather than by nested project, and forcing a nesting concept onto one\n * that has none would mean faking it. A provider without these simply does\n * not offer session-scoped inboxes, and callers say so rather than failing.\n */\n listSubProjects?(): Promise<Array<{ id: string; name: string }>>;\n findOrCreateSubProject?(name: string): Promise<{ id: string; created: boolean }>;\n\n /**\n * The comment thread on a task, oldest first.\n *\n * Optional because not every tracker has threaded comments. Where it exists,\n * the thread is usually where the reasoning lives — the question, the answer,\n * the correction — and completing the task takes it out of view. That is what\n * the archive exists to keep.\n */\n listComments?(taskId: string): Promise<Array<{ id: string; content: string; postedAt?: string }>>;\n\n /**\n * One task by id, whether open or completed.\n *\n * Needed because archiving runs at or after completion, and a completed task\n * is gone from `listOpen` — which is exactly the moment its discussion stops\n * being visible and most needs keeping.\n */\n getTask?(id: string): Promise<Task | null>;\n}\n\n// ---------------------------------------------------------------------------\n// Configuration\n// ---------------------------------------------------------------------------\n\nexport interface TodoistProviderConfig {\n enabled: boolean;\n /**\n * API token. Resolution order is apiKey → TODOIST_API_KEY env → unconfigured.\n *\n * Never read this from another tool's config file. PAI ships as a product;\n * scraping ~/.claude.json for a key belonging to the Todoist MCP is not\n * acceptable even though the key is sitting there.\n */\n apiKey?: string;\n /**\n * Tracker project ID that roots the bus (the \"Claude 🤖\" project).\n *\n * Stored as an ID, never a name. Todoist's project search silently returns\n * zero results for names containing emoji — resolving by name would report\n * \"no tasks\" instead of failing, which is the exact class of silent failure\n * this subsystem exists to surface.\n */\n rootProjectId?: string;\n /** Section ID for the findings inbox. Tasks land here when UNROUTED. */\n findingsSectionId?: string;\n}\n\nexport interface TaskConfig {\n /** Master switch. When false the bus is inert. */\n enabled: boolean;\n providers: {\n todoist: TodoistProviderConfig;\n };\n /**\n * Dispatch work to the owning session automatically, spawning one if absent.\n * Requires AIBroker. When false — or when AIBroker is unavailable — PAI\n * reports which project owns each task and leaves acting to the user.\n */\n autoDispatch: boolean;\n\n /**\n * Seconds AIBroker may spend on a single dispatch, spawn included.\n *\n * Passed down to the transport so both sides share one deadline. Raise it on\n * a loaded machine where sessions are slow to start accepting input.\n */\n dispatchTimeoutSecs?: number;\n\n /**\n * Project a task goes to when it carries the bare `pai` marker and its\n * location says nothing — an Inbox capture, typically.\n *\n * This is the one thing a task's location cannot express: \"an AI should take\n * this, and I do not know which one yet\". Everything else is answered by the\n * project the task sits in.\n *\n * Unset means such a task stays UNROUTED, which is a legitimate choice: it\n * then surfaces in the findings inbox for triage rather than being guessed at.\n */\n defaultOwner?: string;\n}\n\nexport const DEFAULT_TASK_CONFIG: TaskConfig = {\n enabled: false,\n providers: {\n todoist: {\n enabled: false,\n },\n },\n autoDispatch: false,\n};\n\n// ---------------------------------------------------------------------------\n// Dispatch results\n// ---------------------------------------------------------------------------\n\n/**\n * What happened to one task during a dispatch run.\n *\n * - \"delivered\" — sent to an already-running session, and confirmed submitted\n * - \"queued\" — typed into a live session that was mid-turn, so submission\n * could not be confirmed inside the window. This is delivery:\n * Claude Code holds typed input until the current turn ends.\n * Never retried — the text is already in the input box, so a\n * second attempt is a second copy, not a retry. One trigger\n * arrived three times on 2026-08-01 for exactly that reason.\n * - \"spawned\" — none running; one was launched, came up, and received it\n * - \"unrouted\" — no owner resolved; left in the findings inbox\n * - \"unlaunchable\" — an owner resolved but no PAI alias exists to launch it\n * - \"unreachable\" — a session was launched but never became ready to accept input\n * - \"skipped\" — autoDispatch is off, or no transport; reported only\n *\n * `unlaunchable` and `unreachable` are distinct because the fixes differ:\n * the first is a setup gap (register an alias), the second is a runtime\n * failure (find out why the session did not come up). Collapsing them would\n * send users looking in the wrong place.\n *\n * None of these are errors. A task that could not be delivered is a routing\n * result to report, not an exception to throw.\n */\nexport type DispatchOutcome =\n | \"delivered\"\n | \"queued\"\n | \"spawned\"\n | \"unrouted\"\n | \"unlaunchable\"\n | \"unreachable\"\n | \"skipped\";\n\nexport interface DispatchResult {\n task: Task;\n outcome: DispatchOutcome;\n /** Session the task reached, when it reached one. */\n session?: string;\n /** Why the task did not reach a session. Present on failure outcomes. */\n reason?: string;\n}\n","/**\n * json-store.ts — read/write JSON config files without destroying them\n *\n * The failure this exists to prevent:\n *\n * try { return JSON.parse(read(path)); } catch { return {}; }\n * ... later ...\n * write(path, JSON.stringify(ourData));\n *\n * An unreadable file becomes an empty object, and the next write makes that\n * permanent. Silently, exit code 0. This shape appeared three times in this\n * repo against three different files, and twice in AIBroker.\n *\n * The distinction that matters is between *missing* and *unreadable*:\n *\n * missing — legitimate first run. Start fresh; writing is safe.\n * unreadable — the file exists and we could not parse it. Those bytes are\n * the only copy of something. Never overwrite them.\n *\n * Collapsing the second into the first is the bug.\n *\n * NOT everything deserves this guard. For a transient buffer — an undelivered\n * message queue, a cache — starting fresh IS the correct recovery, and\n * refusing to write would disable the feature permanently. Use `writeJsonAtomic`\n * alone there: it still prevents a crash from truncating a good file, without\n * blocking recovery. Reserve `readJsonStrict` for data a user cannot rebuild.\n */\n\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n copyFileSync,\n renameSync,\n unlinkSync,\n mkdirSync,\n chmodSync,\n} from \"node:fs\";\nimport { dirname } from \"node:path\";\n\n/**\n * Read a JSON file, distinguishing \"absent\" from \"damaged\".\n *\n * @param path file to read\n * @param label how to name it to the user, e.g. \"~/.claude.json\"\n * @throws if the file exists but cannot be read or parsed\n */\nexport function readJsonStrict(path: string, label = path): Record<string, unknown> {\n if (!existsSync(path)) return {};\n\n let raw: string;\n try {\n raw = readFileSync(path, \"utf8\");\n } catch (e) {\n throw new Error(\n `Could not read ${label}: ${e instanceof Error ? e.message : String(e)}\\n` +\n `Refusing to continue — writing now would replace its contents with ours alone.`\n );\n }\n\n try {\n return JSON.parse(raw) as Record<string, unknown>;\n } catch (e) {\n throw new Error(\n `${label} exists but is not valid JSON: ${e instanceof Error ? e.message : String(e)}\\n` +\n `Refusing to continue — overwriting it would destroy whatever it holds.\\n` +\n `Repair the file, or move it aside and re-run this command.`\n );\n }\n}\n\n/**\n * Leaf values that mean \"type word\", not data.\n *\n * A config whose top-level keys map to bare type words is a serialized schema,\n * not a configuration — the exact shape found over the live config on\n * 2026-09-18 (399 bytes: `socketPath: string`, `indexIntervalSecs: int`, …).\n * No real config carries three of these at its top level; a dump carries\n * nothing else.\n */\nconst SCHEMA_TYPE_WORDS = new Set([\n \"string\", \"int\", \"integer\", \"number\", \"bool\", \"boolean\",\n \"float\", \"double\", \"object\", \"array\", \"any\", \"unknown\",\n]);\n\nfunction schemaDumpHits(data: Record<string, unknown>): number {\n let hits = 0;\n for (const v of Object.values(data)) {\n if (typeof v === \"string\" && SCHEMA_TYPE_WORDS.has(v.trim())) hits += 1;\n }\n return hits;\n}\n\n/**\n * Write JSON without risking the existing file.\n *\n * Keeps a `.bak-pai` copy of the previous contents, then writes to a temp file\n * and renames. Rename is atomic within a filesystem, so a crash mid-write\n * leaves the original intact rather than truncated — which is how these files\n * become corrupt in the first place.\n */\nexport function writeJsonAtomic(\n path: string,\n data: Record<string, unknown>,\n opts: { backup?: boolean; label?: string } = {}\n): void {\n const { backup = true, label = path } = opts;\n if (schemaDumpHits(data) >= 3) {\n throw new Error(\n `${label}: refusing to write — the object looks like a type schema, not values ` +\n `(several top-level keys map to bare type words). That is the corruption ` +\n `signature of 2026-09-18: the caller serialized the schema instead of the ` +\n `config. Nothing was written.`\n );\n }\n const serialized = JSON.stringify(data, null, 2) + \"\\n\";\n\n const dir = dirname(path);\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n\n if (backup && existsSync(path)) {\n try {\n copyFileSync(path, `${path}.bak-pai`);\n chmodSync(`${path}.bak-pai`, 0o600);\n } catch (e) {\n throw new Error(\n `Could not back up ${label}: ${e instanceof Error ? e.message : String(e)}\\n` +\n `Refusing to write without a backup.`\n );\n }\n }\n\n const tmp = `${path}.tmp-pai-${process.pid}`;\n try {\n writeFileSync(tmp, serialized, { encoding: \"utf8\", mode: 0o600 });\n renameSync(tmp, path);\n chmodSync(path, 0o600);\n } catch (e) {\n try { if (existsSync(tmp)) unlinkSync(tmp); } catch { /* best effort */ }\n throw new Error(\n `Failed to write ${label}: ${e instanceof Error ? e.message : String(e)}\\n` +\n `The original is unchanged.`\n );\n }\n}\n","/**\n * yaml-store.ts — generic, comment-preserving YAML file helpers shared by\n * every PAI config file that has a YAML form (config.yaml, voices.yaml;\n * workers.yaml keeps its own writer in workers-config.ts because it needs\n * provider-specific key-quoting, but its atomic-write tail is this module's\n * writeYamlFileAtomic).\n */\n\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n copyFileSync,\n renameSync,\n unlinkSync,\n mkdirSync,\n chmodSync,\n} from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { Document, YAMLMap, parseDocument, type Node, type Pair, type Scalar } from \"yaml\";\n\nexport class YamlStoreError extends Error {}\n\n/**\n * Validate `text` parses as YAML, back up any existing file to `.bak-pai`,\n * then write via temp file + rename (atomic within a filesystem) at mode\n * 0600 — every PAI-owned YAML file can carry secrets, key or no key.\n */\nexport function writeYamlFileAtomic(path: string, text: string, opts: { label?: string } = {}): void {\n const label = opts.label ?? path;\n const parsed = parseDocument(text);\n if (parsed.errors.length) {\n throw new YamlStoreError(`refusing to write ${label}: ${parsed.errors[0].message} (previous file left unchanged)`);\n }\n\n const dir = dirname(path);\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n if (existsSync(path)) {\n try {\n copyFileSync(path, `${path}.bak-pai`);\n } catch (e) {\n throw new YamlStoreError(\n `Could not back up ${label}: ${e instanceof Error ? e.message : String(e)}\\nRefusing to write without a backup.`\n );\n }\n }\n const tmp = `${path}.tmp-pai-${process.pid}`;\n try {\n writeFileSync(tmp, text, { encoding: \"utf8\", mode: 0o600 });\n renameSync(tmp, path);\n chmodSync(path, 0o600);\n } catch (e) {\n try {\n if (existsSync(tmp)) unlinkSync(tmp);\n } catch {\n /* best effort */\n }\n throw new YamlStoreError(`Failed to write ${label}: ${e instanceof Error ? e.message : String(e)}`);\n }\n}\n\n/** Deep-equal for plain JSON-shaped values (objects/arrays/scalars). */\nexport function deepEqualJson(a: unknown, b: unknown): boolean {\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\n/**\n * Apply the diff between `before` and `after` (arbitrary nested plain JSON\n * objects) onto a YAML Document, touching only the leaves/keys that actually\n * changed — this is what lets hand-written comments elsewhere in the file\n * survive a `set`/`unset`. Arrays and non-object values are replaced\n * wholesale when they differ; only plain objects are diffed key by key.\n */\nexport function syncPlainObjectIntoYamlDoc(\n doc: Document,\n before: unknown,\n after: unknown,\n path: (string | number)[] = []\n): void {\n if (isPlainObject(after) && isPlainObject(before)) {\n for (const key of Object.keys(after)) {\n syncPlainObjectIntoYamlDoc(doc, before[key], after[key], [...path, key]);\n }\n for (const key of Object.keys(before)) {\n if (!(key in after)) doc.deleteIn([...path, key]);\n }\n return;\n }\n if (after === undefined) {\n if (before !== undefined) doc.deleteIn(path);\n return;\n }\n if (!deepEqualJson(before, after)) {\n doc.setIn(path, after);\n }\n}\n\n/**\n * Attach a `# text` comment directly above the key at `path` inside a YAML\n * Document — used for the annotated section headers `pai config yaml`\n * writes. No-op if the path does not resolve to a mapping entry (e.g. the\n * key was omitted from the document).\n */\nexport function setYamlKeyComment(doc: Document, path: (string | number)[], text: string): void {\n const parentPath = path.slice(0, -1);\n const lastKey = path[path.length - 1];\n const parent = parentPath.length ? doc.getIn(parentPath, true) : doc.contents;\n if (!(parent instanceof YAMLMap)) return;\n const pair = (parent.items as Pair[]).find((p) => (p.key as Scalar)?.value === lastKey);\n if (pair) {\n (pair.key as Node & { commentBefore?: string }).commentBefore = ` ${text}`;\n }\n}\n","/**\n * main-config.ts — the dual-format (config.yaml / config.json) engine behind\n * the main PAI config file. Deliberately has NO dependency on\n * src/daemon/config.ts (which owns path resolution, PaiDaemonConfig and\n * DEFAULTS) so that module can import this one for loadConfig() without a\n * cycle; every path this module touches is passed in explicitly.\n *\n * Precedence, same shape as workers.yaml/workers/config.ts: config.yaml wins\n * when it exists, config.json otherwise. `pai config yaml` (see\n * src/cli/commands/config.ts) performs the one-time JSON→YAML conversion;\n * every writer that goes through readMainConfigRaw/writeMainConfigRaw (see\n * src/daemon/config.ts) keeps working unmodified either way.\n */\n\nimport { existsSync, readFileSync, renameSync, chmodSync } from \"node:fs\";\nimport { dirname, join, basename } from \"node:path\";\nimport { Document, parseDocument } from \"yaml\";\nimport { readJsonStrict, writeJsonAtomic } from \"./json-store.js\";\nimport { writeYamlFileAtomic, syncPlainObjectIntoYamlDoc, deepEqualJson, setYamlKeyComment } from \"./yaml-store.js\";\n\nexport class MainConfigError extends Error {}\n\n/** The config.yaml sibling of a given config.json path (same directory). */\nexport function yamlSiblingPath(jsonPath: string): string {\n return join(dirname(jsonPath), \"config.yaml\");\n}\n\n/** The path a user should be pointed at: config.yaml when it exists (same\n * precedence as readDualFormatConfigRaw), else config.json. */\nexport function resolvedMainConfigPath(jsonPath: string, yamlPath: string = yamlSiblingPath(jsonPath)): string {\n return existsSync(yamlPath) ? yamlPath : jsonPath;\n}\n\n/**\n * Read the raw main config: config.yaml if it exists, else config.json\n * (readJsonStrict — missing is `{}`, damaged throws), matching\n * readWorkersSection's precedence for workers.yaml/JSON.\n */\nexport function readDualFormatConfigRaw(jsonPath: string, yamlPath: string = yamlSiblingPath(jsonPath)): Record<string, unknown> {\n if (existsSync(yamlPath)) {\n let text: string;\n try {\n text = readFileSync(yamlPath, \"utf8\");\n } catch (e) {\n throw new MainConfigError(`Could not read ${yamlPath}: ${e instanceof Error ? e.message : String(e)}`);\n }\n const doc = parseDocument(text);\n if (doc.errors.length) {\n throw new MainConfigError(`${yamlPath}: ${doc.errors[0].message}`);\n }\n const parsed = doc.toJS();\n if (parsed === null || parsed === undefined) return {};\n if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new MainConfigError(`${yamlPath}: top level must be a mapping`);\n }\n return parsed as Record<string, unknown>;\n }\n return readJsonStrict(jsonPath, jsonPath);\n}\n\n/**\n * Write the raw main config back: comment-preserving diff-sync into\n * config.yaml when it exists, else plain writeJsonAtomic to config.json —\n * this is the one seam every writer of the main config (CLI commands,\n * identity, notifications, setup, workers) routes through, so a config.yaml\n * on disk is honored no matter which of them made the change.\n */\nexport function writeDualFormatConfigRaw(\n jsonPath: string,\n raw: Record<string, unknown>,\n yamlPath: string = yamlSiblingPath(jsonPath)\n): void {\n if (existsSync(yamlPath)) {\n let text: string;\n try {\n text = readFileSync(yamlPath, \"utf8\");\n } catch (e) {\n throw new MainConfigError(`Could not read ${yamlPath}: ${e instanceof Error ? e.message : String(e)}`);\n }\n const doc = parseDocument(text);\n if (doc.errors.length) {\n throw new MainConfigError(`${yamlPath}: ${doc.errors[0].message}`);\n }\n const before = (doc.toJS() ?? {}) as Record<string, unknown>;\n syncPlainObjectIntoYamlDoc(doc, before, raw);\n try {\n writeYamlFileAtomic(yamlPath, String(doc), { label: yamlPath });\n } catch (e) {\n throw new MainConfigError(e instanceof Error ? e.message : String(e));\n }\n return;\n }\n writeJsonAtomic(jsonPath, raw, { label: jsonPath });\n}\n\n// ---------------------------------------------------------------------------\n// JSON → YAML migration (\"pai config yaml\")\n// ---------------------------------------------------------------------------\n\n/**\n * Short annotation for every top-level PaiDaemonConfig field, shown as a `#`\n * comment above the field in the generated config.yaml. Mirrors (and must be\n * kept in sync with) the doc comments on PaiDaemonConfig in\n * src/daemon/config.ts — kept as a separate short-form table rather than\n * parsed from the TS source at runtime.\n */\nexport const MAIN_CONFIG_SECTION_COMMENTS: Record<string, string> = {\n socketPath: \"Unix Domain Socket path for daemon IPC.\",\n indexIntervalSecs: \"How often the daemon re-indexes changed files, in seconds.\",\n embedIntervalSecs: \"How often the daemon runs the embedding pass, in seconds.\",\n embedOnStartup:\n \"Run an embed pass 60s after daemon start. Off by default: with a large backlog it makes every restart a CPU storm.\",\n passPriority: 'Priority of scheduled index/embed passes: \"background\" (default; low-priority child process) or \"normal\" (in-process).',\n maintenanceHour: \"Local hour (0-23) to anchor the recurring index/embed cycle to, so maintenance runs in a fixed window.\",\n storageBackend: 'Storage backend: \"sqlite\" (default) or \"postgres\".',\n postgres: 'PostgreSQL connection settings, used when storageBackend is \"postgres\".',\n embeddingModel: \"Embedding model name, used for semantic/hybrid search.\",\n logLevel: \"Daemon log level: debug, info, warn, or error.\",\n vaultPath: \"Obsidian vault root path for zettelkasten indexing, if any.\",\n vaultProjectId: \"Registry project_id used for vault chunks in memory_chunks. Default: auto-detected.\",\n notifications: \"Notification subsystem configuration.\",\n search: \"Search defaults, applied when an MCP tool or CLI call doesn't specify one.\",\n tasks: \"Task bus — optional external tracker for cross-session work.\",\n identity:\n 'Who \"me\" is — addresses that count as the user\\'s own. Empty by default and never guessed: nothing is self-addressed until this is set.',\n workers:\n \"Non-provider worker settings (pane, routing, tree, cache keepalive, fallback). Providers/classes/mcp_sets live in workers.yaml, not here.\",\n};\n\n/** Keys whose leading-underscore string value is a JSON stand-in for a\n * comment (e.g. `\"_comment\": \"...\"`, `\"_deliverToNote\": \"...\"`). Stripped\n * from the generated YAML and re-attached as a real `#` comment above the\n * nearest remaining sibling key in the same object. */\nfunction isCommentKey(key: string): boolean {\n return key.startsWith(\"_\");\n}\n\ninterface CollectedComment {\n path: (string | number)[];\n text: string;\n}\n\n/** Recursively strip `_`-prefixed comment keys out of a plain JSON value,\n * returning the cleaned value plus where each stripped comment belongs. */\nfunction stripUnderscoreComments(value: unknown, path: (string | number)[] = []): { cleaned: unknown; comments: CollectedComment[] } {\n if (Array.isArray(value)) {\n const comments: CollectedComment[] = [];\n const cleaned = value.map((v, i) => {\n const r = stripUnderscoreComments(v, [...path, i]);\n comments.push(...r.comments);\n return r.cleaned;\n });\n return { cleaned, comments };\n }\n if (typeof value === \"object\" && value !== null) {\n const obj = value as Record<string, unknown>;\n const cleaned: Record<string, unknown> = {};\n const comments: CollectedComment[] = [];\n const localComments: string[] = [];\n for (const [k, v] of Object.entries(obj)) {\n if (isCommentKey(k)) {\n if (typeof v === \"string\" && v.trim()) localComments.push(v.trim());\n continue;\n }\n const r = stripUnderscoreComments(v, [...path, k]);\n cleaned[k] = r.cleaned;\n comments.push(...r.comments);\n }\n // Attach every local comment above the first remaining key in this\n // object (in source order) — good enough for the JSON-workaround shape\n // this exists to convert (a `_comment` sibling explaining the object).\n const firstKey = Object.keys(cleaned)[0];\n if (localComments.length && firstKey !== undefined) {\n comments.unshift({ path: [...path, firstKey], text: localComments.join(\" \") });\n }\n return { cleaned, comments };\n }\n return { cleaned: value, comments: [] };\n}\n\nexport interface MainConfigMigrateResult {\n yamlPath: string;\n yamlText: string;\n /** Path the pre-migration JSON was renamed to; null on a dry run. */\n backupPath: string | null;\n dryRun: boolean;\n}\n\n/**\n * `pai config yaml`: convert config.json to config.yaml. Strips `_`-prefixed\n * JSON-comment-workaround keys into real `#` comments, adds a short\n * annotation above every top-level section (MAIN_CONFIG_SECTION_COMMENTS),\n * verifies the generated YAML re-parses to the exact same data before\n * writing anything, then renames the JSON aside to\n * `config.json.migrated-<YYYY-MM-DD>` (never deleted). Refuses when\n * config.yaml already exists unless `force`. `dryRun` computes and returns\n * the would-be YAML text without touching either file.\n */\nexport function migrateMainConfigToYaml(\n jsonPath: string,\n opts: { dryRun?: boolean; force?: boolean; yamlPath?: string; sectionComments?: Record<string, string> } = {}\n): MainConfigMigrateResult {\n const yamlPath = opts.yamlPath ?? yamlSiblingPath(jsonPath);\n if (existsSync(yamlPath) && !opts.force) {\n throw new MainConfigError(`${yamlPath} already exists — edit it directly, or pass --force to regenerate it from ${jsonPath}`);\n }\n if (!existsSync(jsonPath)) {\n throw new MainConfigError(`${jsonPath} does not exist — nothing to migrate`);\n }\n\n const raw = readJsonStrict(jsonPath, jsonPath);\n const { cleaned, comments } = stripUnderscoreComments(raw);\n const cleanedObj = cleaned as Record<string, unknown>;\n\n const doc = new Document(cleanedObj);\n const sectionComments = opts.sectionComments ?? MAIN_CONFIG_SECTION_COMMENTS;\n for (const [key, text] of Object.entries(sectionComments)) {\n if (key in cleanedObj) setYamlKeyComment(doc, [key], text);\n }\n for (const c of comments) {\n setYamlKeyComment(doc, c.path, c.text);\n }\n const yamlText = String(doc);\n\n // Round-trip check: the generated YAML must re-parse to exactly the data\n // it was built from before anything is written or renamed.\n const reparsed = parseDocument(yamlText).toJS();\n if (!deepEqualJson(reparsed, cleanedObj)) {\n throw new MainConfigError(\n `${yamlPath}: generated YAML does not re-parse to the same data as ${jsonPath} — aborting, nothing written`\n );\n }\n\n if (opts.dryRun) {\n return { yamlPath, yamlText, backupPath: null, dryRun: true };\n }\n\n writeYamlFileAtomic(yamlPath, yamlText, { label: yamlPath });\n\n const stamp = new Date().toISOString().slice(0, 10);\n const backupPath = join(dirname(jsonPath), `${basename(jsonPath)}.migrated-${stamp}`);\n renameSync(jsonPath, backupPath);\n chmodSync(backupPath, 0o600);\n\n return { yamlPath, yamlText, backupPath, dryRun: false };\n}\n","/**\n * config.ts — Configuration loader for PAI Daemon\n *\n * Loads config from ~/.claude/pai/config.json (the pre-2026-09-19 location\n * was ~/.config/pai/config.json, briefly ~/.claude/pai.json in between —\n * see paiConfigFilePath/migrateConfigFile).\n * Deep-merges with defaults so partial configs work fine.\n * Expands ~ in path values at runtime.\n */\n\nimport { existsSync, mkdirSync, writeFileSync, chmodSync, readdirSync } from \"node:fs\";\nimport { homedir, userInfo } from \"node:os\";\nimport { join, dirname, basename } from \"node:path\";\nimport type { NotificationConfig } from \"../notifications/types.js\";\nimport { DEFAULT_NOTIFICATION_CONFIG } from \"../notifications/types.js\";\nimport type { TaskConfig } from \"../tasks/types.js\";\nimport { DEFAULT_TASK_CONFIG } from \"../tasks/types.js\";\nimport { paiSocketPath } from \"../runtime-paths.js\";\nimport {\n paiHomePath,\n resolvePaiFile,\n migratePaiFile,\n PaiFileMigrationError,\n type MigrateFileResult,\n} from \"../config/pai-home.js\";\nimport {\n yamlSiblingPath,\n readDualFormatConfigRaw,\n writeDualFormatConfigRaw,\n MainConfigError,\n} from \"../config/main-config.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SearchConfig {\n /** Default search mode: 'keyword', 'semantic', or 'hybrid'. Default: 'keyword'. */\n mode: \"keyword\" | \"semantic\" | \"hybrid\";\n /** Enable cross-encoder reranking by default. Default: true. */\n rerank: boolean;\n /** Recency boost half-life in days. 0 = off. Default: 90. */\n recencyBoostDays: number;\n /** Default max results. Default: 10. */\n defaultLimit: number;\n /** Default snippet length for MCP results. Default: 200. */\n snippetLength: number;\n}\n\nexport interface PostgresConfig {\n /** Connection string — if set, overrides individual host/port/etc. fields */\n connectionString?: string;\n /** Postgres host (default: \"localhost\") */\n host?: string;\n /** Postgres port (default: 5432) */\n port?: number;\n /** Postgres database name (default: \"pai\") */\n database?: string;\n /** Postgres user (default: \"pai\") */\n user?: string;\n /** Postgres password (default: \"pai\") */\n password?: string;\n /** Maximum pool connections (default: 5) */\n maxConnections?: number;\n /** Connection timeout in ms (default: 5000) */\n connectionTimeoutMs?: number;\n}\n\n/**\n * Idle-triggered prompt-cache keepalive for interactive Claude Code sessions\n * (see docs/cache-keepalive.md, \"Interactive sessions\"). Distinct from\n * `workers.cacheKeepaliveSecs` (src/workers/config.ts), which re-arms a\n * *worker provider's* cache via trivial worker spawns on a fixed timer: this\n * beats a live interactive session only when it has actually gone idle long\n * enough to risk its 1h ephemeral cache expiring, and only within working\n * hours — a fixed timer would beat while the user is active (no-op, wasted\n * quota) or overnight (never pays back before the next real prompt anyway).\n */\nexport interface SessionsCacheKeepaliveConfig {\n /** Off by default — arming it is the operator's explicit call. */\n enabled: boolean;\n /** Beat a session once it has been idle at least this long. Must be < the\n * provider's cache TTL (60 for the 1h ephemeral cache) or the beat is too\n * late to matter. */\n idleMinutes: number;\n /** Cap on consecutive beats per idle stretch; resets when the user prompts\n * the session again for real (not with the keepalive word itself). */\n maxBeats: number;\n /** Local time window \"HH:MM-HH:MM\" outside which no beats are sent. */\n activeHours: string;\n /** Skip sessions whose context is too small to be worth a beat. */\n minContextTokens: number;\n /** The exact word typed into the session; kept to one word so the\n * UserPromptSubmit hook can recognise it and answer minimally. */\n prompt: string;\n}\n\nexport interface SessionsConfig {\n cacheKeepalive: SessionsCacheKeepaliveConfig;\n}\n\nexport interface PaiDaemonConfig {\n /** Unix Domain Socket path for IPC */\n socketPath: string;\n\n /** Index schedule interval in seconds (default: 300 = 5 minutes) */\n indexIntervalSecs: number;\n\n /** Embedding schedule interval in seconds (default: 600 = 10 minutes) */\n embedIntervalSecs: number;\n /** Run an embed pass 60s after daemon start. Off by default: with a large\n * backlog it makes every restart a CPU storm, and it ignores the interval. */\n embedOnStartup: boolean;\n\n /** Local hour (0-23) to anchor the recurring index/embed cycle to. When unset,\n * the cycle is anchored to daemon start, so a daytime restart pins every\n * later pass to daytime too — a 24h interval does not by itself mean \"at\n * night\". Set this to run maintenance in a fixed window regardless of when\n * the machine last booted. */\n maintenanceHour?: number;\n\n /** Priority of the scheduled index/embed passes. \"background\" (default) runs\n * them in a child under `taskpolicy -b nice -n 19` (macOS) or\n * `nice -n 19 ionice -c3` (Linux); the daemon itself, which serves MCP,\n * keeps normal priority. \"normal\" runs them in-process. */\n passPriority: \"background\" | \"normal\";\n\n /** Storage backend: \"sqlite\" (default) or \"postgres\" */\n storageBackend: \"sqlite\" | \"postgres\";\n\n /** PostgreSQL connection config (used when storageBackend = \"postgres\") */\n postgres?: PostgresConfig;\n\n /** Embedding model name (used for semantic/hybrid search) */\n embeddingModel: string;\n\n /** Log level */\n logLevel: \"debug\" | \"info\" | \"warn\" | \"error\";\n\n /** Obsidian vault root path for zettelkasten indexing. If set, vault indexing runs alongside project indexing. */\n vaultPath?: string;\n\n /** Registry project_id to use for vault chunks in memory_chunks. Default: auto-detected. */\n vaultProjectId?: number;\n\n /** Notification subsystem configuration */\n notifications: NotificationConfig;\n\n /** Search defaults — applied when MCP tool or CLI doesn't specify a value */\n search: SearchConfig;\n\n /** Task bus — optional external tracker for cross-session work */\n tasks: TaskConfig;\n\n /** Who \"me\" is — addresses that count as the user's own. */\n identity: IdentityConfig;\n\n /** Interactive-session settings (currently just the cache keepalive). */\n sessions: SessionsConfig;\n}\n\n/**\n * The user's own identity, for anything that delivers back to them.\n *\n * This exists so \"my own address\" is a fact the system can check rather than\n * something a model infers from context. An assistant deciding on the spot\n * whether an address looks like the user's is exactly the judgement that should\n * not be re-made per message.\n *\n * Empty by default and never guessed at install time: an empty `selfEmails`\n * means nothing is self-addressed, so anything reading this fails closed.\n */\nexport interface IdentityConfig {\n /**\n * Where digests and \"mail me X\" requests are delivered.\n *\n * Must be a mailbox separate from the account doing the sending. Gmail files\n * a message sent from an account to itself — or to one of its own domain\n * aliases — under Sent only, and it never reaches the inbox. The send reports\n * success, so this fails silently and looks exactly like delivery. Observed\n * 2026-08-01: owner@example.ch → owner@example.de, sent fine, invisible.\n *\n * Where a separate mailbox is not available, deliver by writing the message\n * and adding the INBOX label to it rather than relying on the send path.\n */\n deliverTo?: string;\n\n /**\n * Every address that counts as the user's own.\n *\n * Used as an allowlist by anything that may act without review — outbound\n * mail being the case that motivated it. Membership is the whole test: an\n * address that is not listed is not the user's, however similar it looks.\n * Plus-aliases and domain aliases must be listed explicitly rather than\n * pattern-matched, because the patterns that would match them also match\n * addresses belonging to other people.\n */\n selfEmails: string[];\n\n /** The account used to send on the user's behalf, when one is configured. */\n sendingAccount?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Per-user Postgres isolation\n// ---------------------------------------------------------------------------\n\n/** Derive a per-user Postgres database name: pai_<username> */\nfunction perUserDbName(): string {\n const username = userInfo().username;\n // Sanitize: only allow alphanumeric and underscore for Postgres identifiers\n const safe = username.replace(/[^a-zA-Z0-9_]/g, \"_\").toLowerCase();\n return `pai_${safe}`;\n}\n\n/** Derive the per-user connection string */\nfunction perUserConnectionString(): string {\n const db = perUserDbName();\n return `postgresql://pai:pai@localhost:5432/${db}`;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\n/**\n * idleMinutes=50 sits under the 60-minute ephemeral-1h TTL with margin for\n * scheduler jitter; maxBeats=6 (~5h of coverage at one beat per idle stretch)\n * and activeHours 08:00-22:00 keep an unattended overnight machine from\n * beating all night for a session nobody will return to before the cache\n * would have expired anyway.\n */\nexport const DEFAULT_SESSIONS_CACHE_KEEPALIVE: SessionsCacheKeepaliveConfig = {\n enabled: false,\n idleMinutes: 50,\n maxBeats: 6,\n activeHours: \"08:00-22:00\",\n minContextTokens: 20_000,\n prompt: \"keepalive\",\n};\n\nexport const DEFAULTS: PaiDaemonConfig = {\n socketPath: paiSocketPath(),\n indexIntervalSecs: 300,\n embedIntervalSecs: 600,\n embedOnStartup: false,\n passPriority: \"background\",\n storageBackend: \"sqlite\",\n postgres: {\n connectionString: perUserConnectionString(),\n maxConnections: 5,\n connectionTimeoutMs: 5000,\n },\n embeddingModel: \"Snowflake/snowflake-arctic-embed-m-v1.5\",\n logLevel: \"info\",\n notifications: DEFAULT_NOTIFICATION_CONFIG,\n tasks: DEFAULT_TASK_CONFIG,\n // Deliberately empty. An install must not guess who the user is: a wrong\n // guess here is an address that can be mailed without review.\n identity: { selfEmails: [] },\n sessions: { cacheKeepalive: { ...DEFAULT_SESSIONS_CACHE_KEEPALIVE } },\n search: {\n mode: \"keyword\",\n rerank: true,\n recencyBoostDays: 90,\n defaultLimit: 10,\n snippetLength: 200,\n },\n};\n\n/** Config template — generated at runtime so the DB name is per-user */\nfunction configTemplate(): string {\n return `{\n \"socketPath\": \"/tmp/pai.sock\",\n \"indexIntervalSecs\": 300,\n \"embedIntervalSecs\": 600,\n \"storageBackend\": \"sqlite\",\n \"postgres\": {\n \"connectionString\": \"${perUserConnectionString()}\",\n \"maxConnections\": 5,\n \"connectionTimeoutMs\": 5000\n },\n \"embeddingModel\": \"Snowflake/snowflake-arctic-embed-m-v1.5\",\n \"logLevel\": \"info\",\n \"vaultPath\": \"\",\n \"vaultProjectId\": 0,\n \"search\": {\n \"mode\": \"keyword\",\n \"rerank\": true,\n \"recencyBoostDays\": 90,\n \"defaultLimit\": 10,\n \"snippetLength\": 200\n }\n}\n`;\n}\n\n// ---------------------------------------------------------------------------\n// Path helpers\n// ---------------------------------------------------------------------------\n\n/** Expand a leading ~ to the real home directory */\nexport function expandHome(p: string): string {\n if (p === \"~\" || p.startsWith(\"~/\") || p.startsWith(\"~\\\\\")) {\n return join(homedir(), p.slice(1));\n }\n return p;\n}\n\n/** Canonical location since 2026-09-19: under the PAI_HOME namespace dir,\n * so nothing PAI writes can collide with a file Claude Code itself owns. */\nconst NEW_CONFIG_FILE = paiHomePath(\"config.json\");\n\n/** Briefly the canonical location between 2026-09-19's two migrations —\n * read during the transition, never written to again. */\nconst OLD_CONFIG_FILE = join(homedir(), \".claude\", \"pai.json\");\n\n/** Where the config lived before 2026-09-19 — read during the transition,\n * never written to once NEW_CONFIG_FILE exists (see `pai config migrate`). */\nconst LEGACY_CONFIG_FILE = join(homedir(), \".config\", \"pai\", \"config.json\");\n\n/**\n * The path any read/write of the PAI config actually uses: PAI_CONFIG_FILE\n * (tests, power users) first, else the new PAI_HOME location if it exists,\n * else the most recent old location that is actually on disk (printing a\n * one-time notice), else the new location (the target a first write creates).\n */\nexport function paiConfigFilePath(): string {\n const override = process.env.PAI_CONFIG_FILE;\n if (override) return override;\n return resolvePaiFile(NEW_CONFIG_FILE, [OLD_CONFIG_FILE, LEGACY_CONFIG_FILE], \"pai config migrate\");\n}\n\nexport const CONFIG_FILE = paiConfigFilePath();\nexport const CONFIG_DIR = dirname(CONFIG_FILE);\n\n/** config.yaml — the canonical main config once `pai config yaml` has run,\n * read/written by readMainConfigRaw/writeMainConfigRaw instead of\n * CONFIG_FILE whenever it exists (see src/config/main-config.ts). */\nexport function paiConfigYamlFilePath(): string {\n return yamlSiblingPath(CONFIG_FILE);\n}\n\n/**\n * Read the raw main config object exactly as every non-typed writer\n * (identity, notifications, obsidian, setup, workers, memory settings) needs\n * it: config.yaml when it exists, else CONFIG_FILE. `path` overrides the\n * JSON location (tests, or workers/config.ts's parametrized CONFIG_FILE) —\n * its YAML sibling (same dir, `config.yaml`) is what gets preferred.\n */\nexport function readMainConfigRaw(path: string = CONFIG_FILE): Record<string, unknown> {\n try {\n return readDualFormatConfigRaw(path);\n } catch (e) {\n throw e instanceof MainConfigError ? new Error(e.message) : e;\n }\n}\n\n/**\n * Write the raw main config object back through the same seam: a\n * comment-preserving sync into config.yaml when it exists, else a plain\n * writeJsonAtomic to `path`. Every writer of the main config must call this\n * instead of touching CONFIG_FILE directly, so a config.yaml on disk is\n * honored no matter which of them made the change.\n */\nexport function writeMainConfigRaw(raw: Record<string, unknown>, path: string = CONFIG_FILE): void {\n try {\n writeDualFormatConfigRaw(path, raw);\n } catch (e) {\n throw e instanceof MainConfigError ? new Error(e.message) : e;\n }\n}\n\nexport const ConfigMigrationError = PaiFileMigrationError;\nexport type ConfigMigrateResult = MigrateFileResult;\n\n/**\n * `pai config migrate`: move config.json (from ~/.claude/pai.json or\n * ~/.config/pai/config.json, whichever is found) to ~/.claude/pai/config.json\n * byte-for-byte, verify the copy, then rename the old file aside as\n * config.json.migrated-<YYYYMMDD> (never deleted).\n */\nexport function migrateConfigFile(opts: { dryRun?: boolean } = {}): ConfigMigrateResult {\n const result = migratePaiFile(NEW_CONFIG_FILE, [OLD_CONFIG_FILE, LEGACY_CONFIG_FILE], opts);\n if (!result.dryRun && result.fromPath) {\n // config.json can hold secrets (e.g. the Postgres password) — lock down\n // both the new location and the `.migrated-<stamp>` copy left behind.\n if (existsSync(result.toPath)) chmodSync(result.toPath, 0o600);\n const oldDir = dirname(result.fromPath);\n const oldName = basename(result.fromPath);\n for (const entry of readdirSync(oldDir)) {\n if (entry.startsWith(`${oldName}.migrated-`)) chmodSync(join(oldDir, entry), 0o600);\n }\n }\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Deep merge (handles nested objects, not arrays)\n// ---------------------------------------------------------------------------\n\nfunction deepMerge<T extends object>(\n target: T,\n source: Record<string, unknown>\n): T {\n const result = { ...target };\n for (const key of Object.keys(source)) {\n const srcVal = source[key];\n if (srcVal === undefined || srcVal === null) continue;\n const tgtVal = (target as Record<string, unknown>)[key];\n if (\n typeof srcVal === \"object\" &&\n !Array.isArray(srcVal) &&\n typeof tgtVal === \"object\" &&\n tgtVal !== null &&\n !Array.isArray(tgtVal)\n ) {\n (result as Record<string, unknown>)[key] = deepMerge(\n tgtVal as object,\n srcVal as Record<string, unknown>\n );\n } else {\n (result as Record<string, unknown>)[key] = srcVal;\n }\n }\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Config loader\n// ---------------------------------------------------------------------------\n\n/**\n * Load configuration: config.yaml (see paiConfigYamlFilePath) if it exists,\n * else CONFIG_FILE (see paiConfigFilePath), else defaults. Returns defaults\n * deep-merged with any values found in the file.\n */\nexport function loadConfig(): PaiDaemonConfig {\n if (!existsSync(CONFIG_FILE) && !existsSync(paiConfigYamlFilePath())) {\n return { ...DEFAULTS };\n }\n\n let parsed: Record<string, unknown>;\n try {\n parsed = readMainConfigRaw();\n } catch (e) {\n process.stderr.write(\n `[pai-daemon] Could not read config: ${e instanceof Error ? e.message : String(e)}\\n`\n );\n return { ...DEFAULTS };\n }\n\n // Compat: config.json may use \"obsidianVaultPath\" (legacy key) instead of \"vaultPath\".\n // Map it across so the daemon picks it up correctly.\n if (parsed.obsidianVaultPath && !parsed.vaultPath) {\n parsed.vaultPath = parsed.obsidianVaultPath;\n process.stderr.write(\n `[pai-daemon] Config: mapped obsidianVaultPath → vaultPath (${parsed.vaultPath})\\n`\n );\n }\n\n return deepMerge(DEFAULTS, parsed);\n}\n\n/**\n * Ensure CONFIG_DIR exists and write a default config template to CONFIG_FILE\n * if none exists yet. Call this only from the `serve` command.\n */\nexport function ensureConfigDir(): void {\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true });\n process.stderr.write(\n `[pai-daemon] Created config directory: ${CONFIG_DIR}\\n`\n );\n }\n\n if (!existsSync(CONFIG_FILE) && !existsSync(paiConfigYamlFilePath())) {\n try {\n writeFileSync(CONFIG_FILE, configTemplate(), \"utf-8\");\n process.stderr.write(\n `[pai-daemon] Wrote default config to: ${CONFIG_FILE}\\n`\n );\n } catch (e) {\n process.stderr.write(\n `[pai-daemon] Could not write default config: ${e}\\n`\n );\n }\n }\n}\n"],"mappings":"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"}