@tekmidian/pai 0.65.1 → 0.65.2

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 (89) hide show
  1. package/dist/{async-CNn36gm4.mjs → async-C2Bm_Lal.mjs} +2 -2
  2. package/dist/{async-CNn36gm4.mjs.map → async-C2Bm_Lal.mjs.map} +1 -1
  3. package/dist/{auto-route-sLMU-NnM.mjs → auto-route-o0BOsXn7.mjs} +2 -2
  4. package/dist/{auto-route-sLMU-NnM.mjs.map → auto-route-o0BOsXn7.mjs.map} +1 -1
  5. package/dist/{chain-CLb6hbFS.mjs → chain-CT3SI-3z.mjs} +2 -2
  6. package/dist/{chain-CLb6hbFS.mjs.map → chain-CT3SI-3z.mjs.map} +1 -1
  7. package/dist/cli/index.mjs +12 -10
  8. package/dist/cli/index.mjs.map +1 -1
  9. package/dist/cli/program.mjs +10 -10
  10. package/dist/{clusters-PgIUYT_v.mjs → clusters-BCtD3fbe.mjs} +1 -1
  11. package/dist/{clusters-PgIUYT_v.mjs.map → clusters-BCtD3fbe.mjs.map} +1 -1
  12. package/dist/{context-handover-cache-mGxq2-8f.mjs → context-handover-cache-CRSFdkNi.mjs} +2 -2
  13. package/dist/{context-handover-cache-mGxq2-8f.mjs.map → context-handover-cache-CRSFdkNi.mjs.map} +1 -1
  14. package/dist/daemon/index.mjs +12 -10
  15. package/dist/daemon/index.mjs.map +1 -1
  16. package/dist/daemon-DoTVK9z6.mjs +19 -0
  17. package/dist/{daemon-DqCB3fO-.mjs → daemon-SaPnmoyg.mjs} +23 -23
  18. package/dist/{daemon-DqCB3fO-.mjs.map → daemon-SaPnmoyg.mjs.map} +1 -1
  19. package/dist/daemon-mcp/index.mjs +6 -4
  20. package/dist/daemon-mcp/index.mjs.map +1 -1
  21. package/dist/detector-BEPFsINR.mjs +3 -0
  22. package/dist/{detector-CIHEXKsV.mjs → detector-BRTtAWrM.mjs} +1 -1
  23. package/dist/{detector-CIHEXKsV.mjs.map → detector-BRTtAWrM.mjs.map} +1 -1
  24. package/dist/{embeddings-BbNVXa_0.mjs → embeddings-Bx3q0QOY.mjs} +1 -1
  25. package/dist/{embeddings-BbNVXa_0.mjs.map → embeddings-Bx3q0QOY.mjs.map} +1 -1
  26. package/dist/{embeddings-CDfM63uC.mjs → embeddings-CcscYWwk.mjs} +1 -1
  27. package/dist/env-JNEIrQWg.mjs +14 -0
  28. package/dist/env-JNEIrQWg.mjs.map +1 -0
  29. package/dist/{factory-y36qGegI.mjs → factory-CNpPQQ-5.mjs} +7 -7
  30. package/dist/{factory-y36qGegI.mjs.map → factory-CNpPQQ-5.mjs.map} +1 -1
  31. package/dist/{factory-CaswxuJ0.mjs → factory-DY7x73mE.mjs} +1 -1
  32. package/dist/{fallback-CupzGkuJ.mjs → fallback-vHjFZP0Y.mjs} +2 -2
  33. package/dist/{fallback-CupzGkuJ.mjs.map → fallback-vHjFZP0Y.mjs.map} +1 -1
  34. package/dist/{indexer-backend-Cnc7Tf5C.mjs → indexer-backend-DFF2FrYx.mjs} +1 -1
  35. package/dist/{kg-entity-COTUj1ZC.mjs → kg-entity-DCOcsVFD.mjs} +1 -1
  36. package/dist/{kg-entity-COTUj1ZC.mjs.map → kg-entity-DCOcsVFD.mjs.map} +1 -1
  37. package/dist/{latent-ideas-ByvMjVcb.mjs → latent-ideas-wwAXquGe.mjs} +3 -3
  38. package/dist/{latent-ideas-ByvMjVcb.mjs.map → latent-ideas-wwAXquGe.mjs.map} +1 -1
  39. package/dist/{link-boost-UiE-uooT.mjs → link-boost-CnI7UVMJ.mjs} +1 -1
  40. package/dist/{link-boost-UiE-uooT.mjs.map → link-boost-CnI7UVMJ.mjs.map} +1 -1
  41. package/dist/{main-resolver-BeYWNzrt.mjs → main-resolver-CKNGux1B.mjs} +3 -3
  42. package/dist/{main-resolver-BeYWNzrt.mjs.map → main-resolver-CKNGux1B.mjs.map} +1 -1
  43. package/dist/{main-resolver-kHW6FewU.mjs → main-resolver-VSM5ytnA.mjs} +1 -1
  44. package/dist/{neighborhood-CvRHlqdR.mjs → neighborhood-2FsmzoxG.mjs} +2 -2
  45. package/dist/{neighborhood-CvRHlqdR.mjs.map → neighborhood-2FsmzoxG.mjs.map} +1 -1
  46. package/dist/{note-context-BrbfUIoP.mjs → note-context-De63k8na.mjs} +1 -1
  47. package/dist/{note-context-BrbfUIoP.mjs.map → note-context-De63k8na.mjs.map} +1 -1
  48. package/dist/{planner-DAq4Yx-H.mjs → planner-Dzs9-8AS.mjs} +4 -4
  49. package/dist/{planner-DAq4Yx-H.mjs.map → planner-Dzs9-8AS.mjs.map} +1 -1
  50. package/dist/{postgres-DYtZg7J7.mjs → postgres-Ceqsa64C.mjs} +3 -3
  51. package/dist/{postgres-DYtZg7J7.mjs.map → postgres-Ceqsa64C.mjs.map} +1 -1
  52. package/dist/{program-CWf9mT7Z.mjs → program-DK0mRRlf.mjs} +44 -44
  53. package/dist/{program-CWf9mT7Z.mjs.map → program-DK0mRRlf.mjs.map} +1 -1
  54. package/dist/{registry-postgres-DSrkxqfF.mjs → registry-postgres-nOfyBSIW.mjs} +1 -1
  55. package/dist/{registry-postgres-DSrkxqfF.mjs.map → registry-postgres-nOfyBSIW.mjs.map} +1 -1
  56. package/dist/{registry-sqlite-B2436JgX.mjs → registry-sqlite-DrCW1aRK.mjs} +1 -1
  57. package/dist/{registry-sqlite-B2436JgX.mjs.map → registry-sqlite-DrCW1aRK.mjs.map} +1 -1
  58. package/dist/router-CXUGsv85.mjs +3 -0
  59. package/dist/{router-aqLMvNMg.mjs → router-a5G7q8_3.mjs} +1 -1
  60. package/dist/{router-aqLMvNMg.mjs.map → router-a5G7q8_3.mjs.map} +1 -1
  61. package/dist/{run-uAkNItb6.mjs → run-DMaCpvMW.mjs} +3 -3
  62. package/dist/{run-uAkNItb6.mjs.map → run-DMaCpvMW.mjs.map} +1 -1
  63. package/dist/{session-keepalive-BWEjcRrh.mjs → session-keepalive-DVZ9-7qg.mjs} +2 -2
  64. package/dist/{session-keepalive-BWEjcRrh.mjs.map → session-keepalive-DVZ9-7qg.mjs.map} +1 -1
  65. package/dist/{sources-DX4ElmrE.mjs → sources-CijVso3n.mjs} +1 -1
  66. package/dist/{sources-DX4ElmrE.mjs.map → sources-CijVso3n.mjs.map} +1 -1
  67. package/dist/{sqlite-BrEu3avy.mjs → sqlite-CqsTy6xo.mjs} +3 -3
  68. package/dist/{sqlite-BrEu3avy.mjs.map → sqlite-CqsTy6xo.mjs.map} +1 -1
  69. package/dist/{themes-BgqahYVM.mjs → themes-CTaOj3e1.mjs} +2 -2
  70. package/dist/{themes-BgqahYVM.mjs.map → themes-CTaOj3e1.mjs.map} +1 -1
  71. package/dist/{tools-gMGDIpJ9.mjs → tools-BcOKPBtg.mjs} +16 -16
  72. package/dist/{tools-gMGDIpJ9.mjs.map → tools-BcOKPBtg.mjs.map} +1 -1
  73. package/dist/{tools-BbqQIHPe.mjs → tools-C-_l0o8Z.mjs} +4 -4
  74. package/dist/{trace-CmAB7iJZ.mjs → trace-CtV0RO6n.mjs} +1 -1
  75. package/dist/{trace-CmAB7iJZ.mjs.map → trace-CtV0RO6n.mjs.map} +1 -1
  76. package/dist/{vault-indexer-Ddq51X20.mjs → vault-indexer-BG1sEVyM.mjs} +1 -1
  77. package/dist/{vault-indexer-Ddq51X20.mjs.map → vault-indexer-BG1sEVyM.mjs.map} +1 -1
  78. package/dist/{wakeup-CZw88uXf.mjs → wakeup-D8n33hYV.mjs} +2 -2
  79. package/dist/{wakeup-CZw88uXf.mjs.map → wakeup-D8n33hYV.mjs.map} +1 -1
  80. package/dist/{work-queue-worker-Taba_vcA.mjs → work-queue-worker-D7GHuj2I.mjs} +3 -3
  81. package/dist/{work-queue-worker-DCzfH0d1.mjs → work-queue-worker-DZiEMvBQ.mjs} +3 -3
  82. package/dist/{work-queue-worker-DCzfH0d1.mjs.map → work-queue-worker-DZiEMvBQ.mjs.map} +1 -1
  83. package/dist/{zettelkasten-Dwj67e4p.mjs → zettelkasten-CtGHQWPU.mjs} +3 -3
  84. package/dist/{zettelkasten-Dwj67e4p.mjs.map → zettelkasten-CtGHQWPU.mjs.map} +1 -1
  85. package/package.json +1 -1
  86. package/statusline-command.sh +1 -1
  87. package/dist/daemon-BjaPR39W.mjs +0 -19
  88. package/dist/detector-C3Q7mxQU.mjs +0 -3
  89. package/dist/router-S6C5BxzZ.mjs +0 -3
@@ -1 +1 @@
1
- {"version":3,"file":"registry-sqlite-B2436JgX.mjs","names":[],"sources":["../src/storage/sqlite/registry-merge.ts","../src/storage/registry-sqlite.ts"],"sourcesContent":["/**\n * planMerge/applyMerge — fold one registered project into another without\n * losing anything, against a raw better-sqlite3 Database.\n *\n * Moved out of src/registry/merge.ts (which keeps only the pure MergePlan\n * type and MergeError class) so that the one place in the codebase touching\n * better-sqlite3 for this operation is under src/storage/. Wrapped by\n * SQLiteRegistryBackend.planProjectMerge/applyProjectMerge\n * (../registry-sqlite.ts).\n *\n * The registry accumulates rows that describe the same work twice: a project\n * registered under two spellings of one directory, a vault-notes twin of a code\n * project, a slug that collided and got a `-1`. Measured 2026-08-04 there were\n * six such rows, and between them they carried ten sessions.\n *\n * Those sessions are the only thing of value on the losing row, which is why\n * `archive` is the wrong remedy and a plain DELETE is worse. `pai project health`\n * suggested archiving for every unhealthy row, and archiving a duplicate leaves\n * the sessions attached to a row nobody will look at again.\n *\n * FIVE tables reference a project, and `PRAGMA foreign_keys` is 0 on this\n * database — so deleting a project row does not fail, it silently orphans rows in\n * sessions, project_tags, aliases, compaction_log and links. Anything that merges\n * by hand will miss at least one of them. Hence one function.\n *\n * Two constraints shape the implementation:\n *\n * sessions UNIQUE (project_id, number) — incoming sessions must be renumbered,\n * or the insert collides with the\n * target's own numbering\n * links UNIQUE (session_id, target_project_id)\n * project_tags PRIMARY KEY (project_id, tag_id)\n * — both can already hold the row we are\n * about to point at the target\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { MergeError, type MergePlan } from \"../../registry/merge.js\";\n\n/**\n * What a merge would do, without doing it.\n *\n * Built as a plan first because the session renumbering is the part a reader will\n * not predict, and printing \"0001 -> 0016\" is the difference between a command\n * that can be trusted and one that has to be taken on faith.\n */\nexport function planMerge(db: Database, fromSlug: string, intoSlug: string): MergePlan {\n if (fromSlug === intoSlug) {\n throw new MergeError(`Cannot merge ${fromSlug} into itself.`);\n }\n\n const from = db\n .prepare(\"SELECT id, slug FROM projects WHERE slug = ?\")\n .get(fromSlug) as { id: number; slug: string } | undefined;\n const into = db\n .prepare(\"SELECT id, slug FROM projects WHERE slug = ?\")\n .get(intoSlug) as { id: number; slug: string } | undefined;\n\n if (!from) throw new MergeError(`No project with slug \"${fromSlug}\".`);\n if (!into) throw new MergeError(`No project with slug \"${intoSlug}\".`);\n\n // Where the target's numbering currently ends. Incoming sessions continue from\n // there rather than keeping their own numbers, which would collide.\n const maxRow = db\n .prepare(\"SELECT COALESCE(MAX(number), 0) AS n FROM sessions WHERE project_id = ?\")\n .get(into.id) as { n: number };\n let next = maxRow.n;\n\n const incoming = db\n .prepare(\"SELECT id, number FROM sessions WHERE project_id = ? ORDER BY number ASC\")\n .all(from.id) as { id: number; number: number }[];\n\n const sessions = incoming.map((s) => ({ id: s.id, from: s.number, to: ++next }));\n\n const count = (sql: string, ...args: unknown[]): number =>\n (db.prepare(sql).get(...args) as { n: number }).n;\n\n // The losing slug becomes an alias of the winner, so anything that resolved by\n // that name keeps working — including a human who remembers the old name.\n const aliasTaken =\n count(\"SELECT COUNT(*) AS n FROM aliases WHERE alias = ?\", from.slug) > 0 ||\n count(\"SELECT COUNT(*) AS n FROM projects WHERE slug = ?\", from.slug) > 1;\n\n return {\n fromId: from.id,\n fromSlug: from.slug,\n intoId: into.id,\n intoSlug: into.slug,\n sessions,\n tags: count(\"SELECT COUNT(*) AS n FROM project_tags WHERE project_id = ?\", from.id),\n aliases: count(\"SELECT COUNT(*) AS n FROM aliases WHERE project_id = ?\", from.id),\n compactions: count(\n \"SELECT COUNT(*) AS n FROM compaction_log WHERE project_id = ?\",\n from.id\n ),\n links: count(\"SELECT COUNT(*) AS n FROM links WHERE target_project_id = ?\", from.id),\n aliasToAdd: aliasTaken ? undefined : from.slug,\n };\n}\n\n/**\n * Apply a plan. One transaction: either the whole row is folded in or nothing is.\n *\n * A half-merged project is the worst outcome available here — sessions moved but\n * the row still present, or the row gone and its tags orphaned — and with foreign\n * keys off nothing would complain.\n */\nexport function applyMerge(db: Database, plan: MergePlan): void {\n const run = db.transaction(() => {\n // Sessions, one at a time because each gets its own new number.\n const move = db.prepare(\n \"UPDATE sessions SET project_id = ?, number = ? WHERE id = ?\"\n );\n for (const s of plan.sessions) move.run(plan.intoId, s.to, s.id);\n\n // Tags: OR IGNORE because (project_id, tag_id) is the primary key and the\n // target may already carry the same tag.\n db.prepare(\n `INSERT OR IGNORE INTO project_tags (project_id, tag_id)\n SELECT ?, tag_id FROM project_tags WHERE project_id = ?`\n ).run(plan.intoId, plan.fromId);\n db.prepare(\"DELETE FROM project_tags WHERE project_id = ?\").run(plan.fromId);\n\n // Existing aliases of the loser now point at the winner.\n db.prepare(\"UPDATE aliases SET project_id = ? WHERE project_id = ?\").run(\n plan.intoId,\n plan.fromId\n );\n\n db.prepare(\"UPDATE compaction_log SET project_id = ? WHERE project_id = ?\").run(\n plan.intoId,\n plan.fromId\n );\n\n // Links: same collision shape as tags, plus one extra case — a session that\n // linked to the loser may now be linking to its own project, which is not a\n // relationship worth keeping.\n db.prepare(\n `UPDATE OR IGNORE links SET target_project_id = ? WHERE target_project_id = ?`\n ).run(plan.intoId, plan.fromId);\n db.prepare(\"DELETE FROM links WHERE target_project_id = ?\").run(plan.fromId);\n db.prepare(\n `DELETE FROM links WHERE target_project_id = ?\n AND session_id IN (SELECT id FROM sessions WHERE project_id = ?)`\n ).run(plan.intoId, plan.intoId);\n\n // And keep the old name resolvable.\n if (plan.aliasToAdd) {\n db.prepare(\"INSERT OR IGNORE INTO aliases (alias, project_id) VALUES (?, ?)\").run(\n plan.aliasToAdd,\n plan.intoId\n );\n }\n\n db.prepare(\"DELETE FROM projects WHERE id = ?\").run(plan.fromId);\n });\n\n run();\n}\n","/**\n * SQLiteRegistryBackend — wraps the existing better-sqlite3 registry.db\n * (src/registry/db.ts, src/registry/merge.ts) behind the RegistryBackend\n * interface.\n *\n * Thin adapter: every method is the same SQL the 64 direct callers run today,\n * just relocated here and wrapped in Promise.resolve(...) so the interface can\n * be async (matching StorageBackend, and Postgres in unit 4) without changing\n * SQLite behaviour.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { planMerge, applyMerge } from \"./sqlite/registry-merge.js\";\nimport type {\n RegistryBackend,\n Project,\n ProjectWithSessionStats,\n Session,\n SessionWithProject,\n NewProject,\n NewProjectWithSlugRetry,\n NewSession,\n NewCompactionLogEntry,\n ListProjectsOptions,\n ListProjectsByPathLengthOptions,\n ListProjectsWithStatsOptions,\n ListNamedProjectsOptions,\n ListSessionsOptions,\n ListSessionsForProjectOptions,\n ProjectType,\n ProjectStatus,\n SessionStatus,\n LinkType,\n MergePlan,\n} from \"./registry-interface.js\";\n\nexport class SQLiteRegistryBackend implements RegistryBackend {\n readonly backendType = \"sqlite\" as const;\n\n private db: Database;\n\n constructor(db: Database) {\n this.db = db;\n }\n\n /** Expose the raw better-sqlite3 Database handle for not-yet-converted callers. */\n getRawDb(): Database {\n return this.db;\n }\n\n async close(): Promise<void> {\n try {\n this.db.close();\n } catch {\n // ignore\n }\n }\n\n async resetRegistry(): Promise<void> {\n const run = this.db.transaction(() => {\n this.db.prepare(\"DELETE FROM links\").run();\n this.db.prepare(\"DELETE FROM compaction_log\").run();\n this.db.prepare(\"DELETE FROM session_tags\").run();\n this.db.prepare(\"DELETE FROM project_tags\").run();\n this.db.prepare(\"DELETE FROM aliases\").run();\n this.db.prepare(\"DELETE FROM sessions\").run();\n this.db.prepare(\"DELETE FROM tags\").run();\n this.db.prepare(\"DELETE FROM projects\").run();\n });\n run();\n }\n\n // -------------------------------------------------------------------------\n // Projects — reads\n // -------------------------------------------------------------------------\n\n async getProjectById(id: number): Promise<Project | null> {\n const row = this.db.prepare(\"SELECT * FROM projects WHERE id = ?\").get(id) as Project | undefined;\n return row ?? null;\n }\n\n async getProjectBySlug(\n slug: string,\n opts: { caseInsensitive?: boolean; excludeId?: number } = {}\n ): Promise<Project | null> {\n let sql = opts.caseInsensitive\n ? \"SELECT * FROM projects WHERE lower(slug) = lower(?)\"\n : \"SELECT * FROM projects WHERE slug = ?\";\n const params: unknown[] = [slug];\n if (opts.excludeId !== undefined) {\n sql += \" AND id != ?\";\n params.push(opts.excludeId);\n }\n const row = this.db.prepare(sql).get(...params) as Project | undefined;\n return row ?? null;\n }\n\n async getProjectByAlias(\n alias: string,\n opts: { caseInsensitive?: boolean } = {}\n ): Promise<Project | null> {\n const sql = opts.caseInsensitive\n ? \"SELECT p.* FROM projects p JOIN aliases a ON a.project_id = p.id WHERE lower(a.alias) = lower(?)\"\n : \"SELECT p.* FROM projects p JOIN aliases a ON a.project_id = p.id WHERE a.alias = ?\";\n const row = this.db.prepare(sql).get(alias) as Project | undefined;\n return row ?? null;\n }\n\n async getProjectByRootPath(rootPath: string, opts: { excludeId?: number } = {}): Promise<Project | null> {\n let sql = \"SELECT * FROM projects WHERE root_path = ?\";\n const params: unknown[] = [rootPath];\n if (opts.excludeId !== undefined) {\n sql += \" AND id != ?\";\n params.push(opts.excludeId);\n }\n const row = this.db.prepare(sql).get(...params) as Project | undefined;\n return row ?? null;\n }\n\n async getProjectByEncodedDir(encodedDir: string, opts: { excludeId?: number } = {}): Promise<Project | null> {\n let sql = \"SELECT * FROM projects WHERE encoded_dir = ?\";\n const params: unknown[] = [encodedDir];\n if (opts.excludeId !== undefined) {\n sql += \" AND id != ?\";\n params.push(opts.excludeId);\n }\n const row = this.db.prepare(sql).get(...params) as Project | undefined;\n return row ?? null;\n }\n\n async findProjectByCwdPrefix(cwd: string): Promise<Project | null> {\n const row = this.db\n .prepare(\n `SELECT * FROM projects WHERE status = 'active' AND ? LIKE root_path || '%'\n ORDER BY length(root_path) DESC LIMIT 1`\n )\n .get(cwd) as Project | undefined;\n return row ?? null;\n }\n\n async listProjects(opts: ListProjectsOptions = {}): Promise<Project[]> {\n const where: string[] = [];\n const params: unknown[] = [];\n\n if (opts.status) {\n where.push(\"p.status = ?\");\n params.push(opts.status);\n }\n if (opts.excludeArchived) {\n where.push(\"p.status != 'archived'\");\n }\n if (opts.tagName) {\n where.push(\n \"p.id IN (SELECT pt.project_id FROM project_tags pt JOIN tags t ON pt.tag_id = t.id WHERE t.name = ?)\"\n );\n params.push(opts.tagName);\n }\n\n let sql = \"SELECT p.* FROM projects p\";\n if (where.length) sql += \" WHERE \" + where.join(\" AND \");\n\n switch (opts.orderBy) {\n case \"updated_desc\":\n sql += \" ORDER BY p.updated_at DESC\";\n break;\n case \"slug\":\n sql += \" ORDER BY p.slug ASC\";\n break;\n case \"id\":\n sql += \" ORDER BY p.id\";\n break;\n case \"status_updated\":\n default:\n sql += \" ORDER BY p.status ASC, p.updated_at DESC\";\n break;\n }\n if (opts.limit !== undefined) {\n sql += \" LIMIT ?\";\n params.push(opts.limit);\n }\n\n return this.db.prepare(sql).all(...params) as Project[];\n }\n\n async listProjectsByPathLengthDesc(opts: ListProjectsByPathLengthOptions = {}): Promise<Project[]> {\n const where = opts.excludeArchived ? \"WHERE status != 'archived'\" : \"\";\n return this.db\n .prepare(`SELECT * FROM projects ${where} ORDER BY LENGTH(root_path) DESC`)\n .all() as Project[];\n }\n\n async listProjectsWithSessionStats(\n opts: ListProjectsWithStatsOptions = {}\n ): Promise<ProjectWithSessionStats[]> {\n const where: string[] = [];\n const params: unknown[] = [];\n\n if (opts.status) {\n where.push(\"p.status = ?\");\n params.push(opts.status);\n }\n if (opts.tagId !== undefined) {\n where.push(\"p.id IN (SELECT project_id FROM project_tags WHERE tag_id = ?)\");\n params.push(opts.tagId);\n }\n\n let sql = `\n SELECT p.*,\n (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count,\n (SELECT MAX(s.created_at) FROM sessions s WHERE s.project_id = p.id) AS last_active\n FROM projects p\n `;\n if (where.length) sql += \" WHERE \" + where.join(\" AND \");\n sql +=\n opts.orderBy === \"updated_desc\"\n ? \" ORDER BY p.updated_at DESC\"\n : \" ORDER BY p.status ASC, p.updated_at DESC\";\n\n return this.db.prepare(sql).all(...params) as ProjectWithSessionStats[];\n }\n\n async listNamedProjects(\n opts: ListNamedProjectsOptions = {}\n ): Promise<Array<ProjectWithSessionStats & { name: string | null }>> {\n const join = opts.includeUnnamed ? \"LEFT JOIN\" : \"JOIN\";\n const sql = `\n SELECT p.*, a.alias AS name,\n (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count,\n (SELECT MAX(s.created_at) FROM sessions s WHERE s.project_id = p.id) AS last_active\n FROM projects p\n ${join} aliases a ON a.project_id = p.id\n WHERE p.status = 'active'\n ORDER BY p.updated_at DESC\n `;\n return this.db.prepare(sql).all() as Array<ProjectWithSessionStats & { name: string | null }>;\n }\n\n async searchProjects(query: string, limit = 20): Promise<Project[]> {\n const q = `%${query}%`;\n return this.db\n .prepare(\n `SELECT * FROM projects WHERE slug LIKE ? OR display_name LIKE ? OR root_path LIKE ?\n ORDER BY updated_at DESC LIMIT ?`\n )\n .all(q, q, q, limit) as Project[];\n }\n\n async countProjects(opts: { status?: ProjectStatus } = {}): Promise<number> {\n const sql = opts.status\n ? \"SELECT COUNT(*) AS n FROM projects WHERE status = ?\"\n : \"SELECT COUNT(*) AS n FROM projects\";\n const row = opts.status\n ? (this.db.prepare(sql).get(opts.status) as { n: number })\n : (this.db.prepare(sql).get() as { n: number });\n return row.n;\n }\n\n async getMostRecentProjectUpdatedAt(): Promise<number | null> {\n const row = this.db.prepare(\"SELECT updated_at FROM projects ORDER BY updated_at DESC LIMIT 1\").get() as\n | { updated_at: number }\n | undefined;\n return row?.updated_at ?? null;\n }\n\n async countChildProjects(parentId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM projects WHERE parent_id = ?\").get(parentId) as {\n n: number;\n }).n;\n }\n\n async findSiblingProjectsBySlugPattern(\n excludeProjectId: number,\n slug: string,\n limit = 3\n ): Promise<Array<{ slug: string; root_path: string; session_count: number }>> {\n return this.db\n .prepare(\n `SELECT p.slug, p.root_path, COUNT(s.id) AS session_count\n FROM projects p LEFT JOIN sessions s ON s.project_id = p.id\n WHERE p.id != ? AND (p.slug = ? OR p.slug LIKE ? || '-%' OR ? LIKE p.slug || '-%')\n GROUP BY p.id HAVING session_count > 0 ORDER BY session_count DESC LIMIT ?`\n )\n .all(excludeProjectId, slug, slug, slug, limit) as Array<{\n slug: string;\n root_path: string;\n session_count: number;\n }>;\n }\n\n // -------------------------------------------------------------------------\n // Projects — writes\n // -------------------------------------------------------------------------\n\n async createProject(input: NewProject): Promise<Project> {\n const type: ProjectType = input.type ?? \"local\";\n const status: ProjectStatus = input.status ?? \"active\";\n const result = this.db\n .prepare(\n `INSERT INTO projects (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`\n )\n .run(\n input.slug,\n input.displayName,\n input.rootPath,\n input.encodedDir,\n type,\n status,\n input.createdAt,\n input.updatedAt\n );\n return (await this.getProjectById(result.lastInsertRowid as number))!;\n }\n\n async createProjectWithSlugRetry(\n input: NewProjectWithSlugRetry\n ): Promise<{ id: number; slug: string; created: boolean }> {\n let slug = input.baseSlug;\n let attempt = 0;\n const insert = this.db.prepare(\n `INSERT OR IGNORE INTO projects (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)\n VALUES (?, ?, ?, ?, 'local', 'active', ?, ?)`\n );\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const info = insert.run(\n slug,\n input.displayName,\n input.rootPath,\n input.encodedDir,\n input.createdAt,\n input.updatedAt\n );\n if (info.changes > 0) {\n return { id: info.lastInsertRowid as number, slug, created: true };\n }\n\n const existing = this.db.prepare(\"SELECT id FROM projects WHERE root_path = ?\").get(input.rootPath) as\n | { id: number }\n | undefined;\n if (existing) {\n return { id: existing.id, slug, created: false };\n }\n\n attempt++;\n slug = `${input.baseSlug}-${attempt}`;\n }\n }\n\n async updateProjectPath(\n id: number,\n patch: { rootPath?: string; encodedDir?: string },\n updatedAt?: number\n ): Promise<void> {\n const sets: string[] = [];\n const params: unknown[] = [];\n if (patch.rootPath !== undefined) {\n sets.push(\"root_path = ?\");\n params.push(patch.rootPath);\n }\n if (patch.encodedDir !== undefined) {\n sets.push(\"encoded_dir = ?\");\n params.push(patch.encodedDir);\n }\n if (updatedAt !== undefined) {\n sets.push(\"updated_at = ?\");\n params.push(updatedAt);\n }\n if (!sets.length) return;\n params.push(id);\n this.db.prepare(`UPDATE projects SET ${sets.join(\", \")} WHERE id = ?`).run(...params);\n }\n\n async updateProjectStatus(\n id: number,\n status: ProjectStatus,\n opts: { archivedAt?: number | null; updatedAt?: number; requireCurrentStatus?: ProjectStatus } = {}\n ): Promise<void> {\n const sets: string[] = [\"status = ?\"];\n const params: unknown[] = [status];\n if (opts.archivedAt !== undefined) {\n sets.push(\"archived_at = ?\");\n params.push(opts.archivedAt);\n }\n if (opts.updatedAt !== undefined) {\n sets.push(\"updated_at = ?\");\n params.push(opts.updatedAt);\n }\n let sql = `UPDATE projects SET ${sets.join(\", \")} WHERE id = ?`;\n params.push(id);\n if (opts.requireCurrentStatus) {\n sql += \" AND status = ?\";\n params.push(opts.requireCurrentStatus);\n }\n this.db.prepare(sql).run(...params);\n }\n\n async updateProjectDisplayName(id: number, displayName: string, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n this.db\n .prepare(\"UPDATE projects SET display_name = ?, updated_at = ? WHERE id = ?\")\n .run(displayName, updatedAt, id);\n } else {\n this.db.prepare(\"UPDATE projects SET display_name = ? WHERE id = ?\").run(displayName, id);\n }\n }\n\n async updateProjectType(id: number, type: ProjectType, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n this.db.prepare(\"UPDATE projects SET type = ?, updated_at = ? WHERE id = ?\").run(type, updatedAt, id);\n } else {\n this.db.prepare(\"UPDATE projects SET type = ? WHERE id = ?\").run(type, id);\n }\n }\n\n async updateProjectSessionConfig(id: number, config: string | null, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n this.db\n .prepare(\"UPDATE projects SET session_config = ?, updated_at = ? WHERE id = ?\")\n .run(config, updatedAt, id);\n } else {\n this.db.prepare(\"UPDATE projects SET session_config = ? WHERE id = ?\").run(config, id);\n }\n }\n\n async updateProjectClaudeNotesDir(id: number, dir: string | null, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n this.db\n .prepare(\"UPDATE projects SET claude_notes_dir = ?, updated_at = ? WHERE id = ?\")\n .run(dir, updatedAt, id);\n } else {\n this.db.prepare(\"UPDATE projects SET claude_notes_dir = ? WHERE id = ?\").run(dir, id);\n }\n }\n\n async updateProjectObsidianLink(id: number, link: string | null, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n this.db\n .prepare(\"UPDATE projects SET obsidian_link = ?, updated_at = ? WHERE id = ?\")\n .run(link, updatedAt, id);\n } else {\n this.db.prepare(\"UPDATE projects SET obsidian_link = ? WHERE id = ?\").run(link, id);\n }\n }\n\n async updateProjectSlug(id: number, slug: string, updatedAt: number): Promise<void> {\n this.db.prepare(\"UPDATE projects SET slug = ?, updated_at = ? WHERE id = ?\").run(slug, updatedAt, id);\n }\n\n async reassignProjectParent(fromParentId: number, toParentId: number): Promise<number> {\n return this.db.prepare(\"UPDATE projects SET parent_id = ? WHERE parent_id = ?\").run(toParentId, fromParentId)\n .changes;\n }\n\n async deleteProject(id: number): Promise<void> {\n this.db.prepare(\"DELETE FROM projects WHERE id = ?\").run(id);\n }\n\n async deleteProjectCascade(id: number): Promise<void> {\n const run = this.db.transaction(() => {\n this.db.prepare(\"DELETE FROM links WHERE target_project_id = ?\").run(id);\n this.db\n .prepare(\"DELETE FROM links WHERE session_id IN (SELECT id FROM sessions WHERE project_id = ?)\")\n .run(id);\n this.db.prepare(\"DELETE FROM compaction_log WHERE project_id = ?\").run(id);\n this.db.prepare(\"DELETE FROM project_tags WHERE project_id = ?\").run(id);\n this.db.prepare(\"DELETE FROM aliases WHERE project_id = ?\").run(id);\n this.db.prepare(\"DELETE FROM sessions WHERE project_id = ?\").run(id);\n this.db.prepare(\"DELETE FROM projects WHERE id = ?\").run(id);\n });\n run();\n }\n\n // -------------------------------------------------------------------------\n // Tags\n // -------------------------------------------------------------------------\n\n async listTagsForProject(projectId: number): Promise<string[]> {\n const rows = this.db\n .prepare(\n \"SELECT t.name FROM tags t JOIN project_tags pt ON pt.tag_id = t.id WHERE pt.project_id = ? ORDER BY t.name\"\n )\n .all(projectId) as Array<{ name: string }>;\n return rows.map((r) => r.name);\n }\n\n async listAllTags(): Promise<Array<{ id: number; name: string }>> {\n return this.db.prepare(\"SELECT id, name FROM tags ORDER BY name\").all() as Array<{\n id: number;\n name: string;\n }>;\n }\n\n async upsertTag(name: string): Promise<number> {\n this.db.prepare(\"INSERT OR IGNORE INTO tags (name) VALUES (?)\").run(name);\n return (this.db.prepare(\"SELECT id FROM tags WHERE name = ?\").get(name) as { id: number }).id;\n }\n\n async addProjectTag(projectId: number, tagId: number): Promise<void> {\n this.db.prepare(\"INSERT INTO project_tags (project_id, tag_id) VALUES (?, ?)\").run(projectId, tagId);\n }\n\n async projectHasTag(projectId: number, tagId: number): Promise<boolean> {\n return !!this.db.prepare(\"SELECT 1 FROM project_tags WHERE project_id = ? AND tag_id = ?\").get(projectId, tagId);\n }\n\n async deleteProjectTag(projectId: number, tagId: number): Promise<void> {\n this.db.prepare(\"DELETE FROM project_tags WHERE project_id = ? AND tag_id = ?\").run(projectId, tagId);\n }\n\n async reassignProjectTag(fromProjectId: number, toProjectId: number, tagId: number): Promise<void> {\n this.db\n .prepare(\"UPDATE project_tags SET project_id = ? WHERE project_id = ? AND tag_id = ?\")\n .run(toProjectId, fromProjectId, tagId);\n }\n\n async listProjectTagIds(projectId: number): Promise<number[]> {\n const rows = this.db.prepare(\"SELECT tag_id FROM project_tags WHERE project_id = ?\").all(projectId) as Array<{\n tag_id: number;\n }>;\n return rows.map((r) => r.tag_id);\n }\n\n async countProjectTagsForProject(projectId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM project_tags WHERE project_id = ?\").get(projectId) as {\n n: number;\n }).n;\n }\n\n async copyProjectTags(fromProjectId: number, toProjectId: number): Promise<void> {\n this.db\n .prepare(\n \"INSERT OR IGNORE INTO project_tags (project_id, tag_id) SELECT ?, tag_id FROM project_tags WHERE project_id = ?\"\n )\n .run(toProjectId, fromProjectId);\n }\n\n async deleteProjectTagsForProject(projectId: number): Promise<void> {\n this.db.prepare(\"DELETE FROM project_tags WHERE project_id = ?\").run(projectId);\n }\n\n // -------------------------------------------------------------------------\n // Aliases\n // -------------------------------------------------------------------------\n\n async resolveAlias(alias: string): Promise<number | null> {\n const row = this.db.prepare(\"SELECT project_id FROM aliases WHERE alias = ?\").get(alias) as\n | { project_id: number }\n | undefined;\n return row?.project_id ?? null;\n }\n\n async listAliasesForProject(projectId: number): Promise<string[]> {\n const rows = this.db.prepare(\"SELECT alias FROM aliases WHERE project_id = ? ORDER BY alias\").all(\n projectId\n ) as Array<{ alias: string }>;\n return rows.map((r) => r.alias);\n }\n\n async addAlias(alias: string, projectId: number): Promise<void> {\n this.db.prepare(\"INSERT INTO aliases (alias, project_id) VALUES (?, ?)\").run(alias, projectId);\n }\n\n async removeAlias(alias: string): Promise<void> {\n this.db.prepare(\"DELETE FROM aliases WHERE alias = ?\").run(alias);\n }\n\n async countAliasesForProject(projectId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM aliases WHERE project_id = ?\").get(projectId) as {\n n: number;\n }).n;\n }\n\n async moveProjectAliases(fromProjectId: number, toProjectId: number): Promise<void> {\n this.db.prepare(\"UPDATE aliases SET project_id = ? WHERE project_id = ?\").run(toProjectId, fromProjectId);\n }\n\n async reassignAlias(alias: string, toProjectId: number): Promise<void> {\n this.db.prepare(\"UPDATE aliases SET project_id = ? WHERE alias = ?\").run(toProjectId, alias);\n }\n\n async listAliasMap(): Promise<Array<{ alias: string; slug: string; root_path: string }>> {\n return this.db\n .prepare(\n `SELECT a.alias AS alias, p.slug AS slug, p.root_path AS root_path\n FROM aliases a JOIN projects p ON p.id = a.project_id WHERE p.status != 'archived'`\n )\n .all() as Array<{ alias: string; slug: string; root_path: string }>;\n }\n\n // -------------------------------------------------------------------------\n // Sessions\n // -------------------------------------------------------------------------\n\n async getSessionById(id: number): Promise<Session | null> {\n const row = this.db.prepare(\"SELECT * FROM sessions WHERE id = ?\").get(id) as Session | undefined;\n return row ?? null;\n }\n\n async getSessionByNumber(projectId: number, number: number): Promise<Session | null> {\n const row = this.db.prepare(\"SELECT * FROM sessions WHERE project_id = ? AND number = ?\").get(\n projectId,\n number\n ) as Session | undefined;\n return row ?? null;\n }\n\n async getLatestSessionForProject(projectId: number): Promise<Session | null> {\n const row = this.db\n .prepare(\"SELECT * FROM sessions WHERE project_id = ? ORDER BY number DESC LIMIT 1\")\n .get(projectId) as Session | undefined;\n return row ?? null;\n }\n\n async getMaxSessionNumber(projectId: number): Promise<number> {\n const row = this.db.prepare(\"SELECT COALESCE(MAX(number), 0) AS n FROM sessions WHERE project_id = ?\").get(\n projectId\n ) as { n: number };\n return row.n;\n }\n\n async listSessionsForProject(\n projectId: number,\n opts: ListSessionsForProjectOptions = {}\n ): Promise<Session[]> {\n const orderBy = opts.orderBy === \"created_desc\" ? \"created_at DESC\" : \"number ASC\";\n let sql = `SELECT * FROM sessions WHERE project_id = ? ORDER BY ${orderBy}`;\n const params: unknown[] = [projectId];\n if (opts.limit !== undefined) {\n sql += \" LIMIT ?\";\n params.push(opts.limit);\n }\n return this.db.prepare(sql).all(...params) as Session[];\n }\n\n async listSessions(opts: ListSessionsOptions = {}): Promise<SessionWithProject[]> {\n const where: string[] = [];\n const params: unknown[] = [];\n if (opts.projectId !== undefined) {\n where.push(\"s.project_id = ?\");\n params.push(opts.projectId);\n }\n if (opts.status) {\n where.push(\"s.status = ?\");\n params.push(opts.status);\n }\n let sql = `\n SELECT s.*, p.slug AS project_slug, p.display_name AS project_name\n FROM sessions s JOIN projects p ON p.id = s.project_id\n `;\n if (where.length) sql += \" WHERE \" + where.join(\" AND \");\n sql += \" ORDER BY s.date DESC, s.number DESC\";\n const limit = opts.limit ?? 20;\n sql += ` LIMIT ${limit}`;\n return this.db.prepare(sql).all(...params) as SessionWithProject[];\n }\n\n async findSessionByFilename(projectId: number, filename: string): Promise<{ id: number } | null> {\n const row = this.db\n .prepare(\"SELECT id FROM sessions WHERE project_id = ? AND filename = ? LIMIT 1\")\n .get(projectId, filename) as { id: number } | undefined;\n return row ?? null;\n }\n\n async sessionNumberTaken(projectId: number, number: number): Promise<boolean> {\n return !!this.db.prepare(\"SELECT 1 FROM sessions WHERE project_id = ? AND number = ? LIMIT 1\").get(\n projectId,\n number\n );\n }\n\n async createSession(input: NewSession): Promise<Session> {\n const status = input.status ?? \"completed\";\n const result = this.db\n .prepare(\n `INSERT INTO sessions (project_id, number, date, slug, title, filename, status, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`\n )\n .run(input.projectId, input.number, input.date, input.slug, input.title, input.filename, status, input.createdAt);\n return (await this.getSessionById(result.lastInsertRowid as number))!;\n }\n\n async upsertSessionIfAbsent(input: NewSession): Promise<boolean> {\n const existing = this.db\n .prepare(\"SELECT id FROM sessions WHERE project_id = ? AND number = ?\")\n .get(input.projectId, input.number);\n if (existing) return false;\n\n const status = input.status ?? \"completed\";\n this.db\n .prepare(\n `INSERT INTO sessions (project_id, number, date, slug, title, filename, status, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`\n )\n .run(input.projectId, input.number, input.date, input.slug, input.title, input.filename, status, input.createdAt);\n return true;\n }\n\n async updateSessionMeta(\n id: number,\n patch: { slug?: string; title?: string; filename?: string }\n ): Promise<void> {\n const sets: string[] = [];\n const params: unknown[] = [];\n if (patch.slug !== undefined) {\n sets.push(\"slug = ?\");\n params.push(patch.slug);\n }\n if (patch.title !== undefined) {\n sets.push(\"title = ?\");\n params.push(patch.title);\n }\n if (patch.filename !== undefined) {\n sets.push(\"filename = ?\");\n params.push(patch.filename);\n }\n if (!sets.length) return;\n params.push(id);\n this.db.prepare(`UPDATE sessions SET ${sets.join(\", \")} WHERE id = ?`).run(...params);\n }\n\n async updateSessionNumber(id: number, number: number, opts: { filename?: string } = {}): Promise<void> {\n if (opts.filename !== undefined) {\n this.db.prepare(\"UPDATE sessions SET number = ?, filename = ? WHERE id = ?\").run(number, opts.filename, id);\n } else {\n this.db.prepare(\"UPDATE sessions SET number = ? WHERE id = ?\").run(number, id);\n }\n }\n\n async updateSessionFilename(id: number, filename: string): Promise<void> {\n this.db.prepare(\"UPDATE sessions SET filename = ? WHERE id = ?\").run(filename, id);\n }\n\n async updateSessionStatus(id: number, status: SessionStatus, opts: { closedAt?: number } = {}): Promise<void> {\n if (opts.closedAt !== undefined) {\n this.db.prepare(\"UPDATE sessions SET status = ?, closed_at = ? WHERE id = ?\").run(status, opts.closedAt, id);\n } else {\n this.db.prepare(\"UPDATE sessions SET status = ? WHERE id = ?\").run(status, id);\n }\n }\n\n async moveSessionToProject(id: number, projectId: number, opts: { number?: number } = {}): Promise<void> {\n if (opts.number !== undefined) {\n this.db.prepare(\"UPDATE sessions SET project_id = ?, number = ? WHERE id = ?\").run(\n projectId,\n opts.number,\n id\n );\n } else {\n this.db.prepare(\"UPDATE sessions SET project_id = ? WHERE id = ?\").run(projectId, id);\n }\n }\n\n async deleteSession(id: number): Promise<void> {\n this.db.prepare(\"DELETE FROM sessions WHERE id = ?\").run(id);\n }\n\n async backfillFoldedSession(keepId: number, dropId: number): Promise<void> {\n this.db\n .prepare(\n `UPDATE sessions SET\n claude_session_id = COALESCE(claude_session_id,\n (SELECT claude_session_id FROM sessions WHERE id = ?)),\n token_count = COALESCE(token_count,\n (SELECT token_count FROM sessions WHERE id = ?)),\n closed_at = COALESCE(closed_at,\n (SELECT closed_at FROM sessions WHERE id = ?)),\n status = CASE WHEN status = 'open'\n AND (SELECT status FROM sessions WHERE id = ?) != 'open'\n THEN (SELECT status FROM sessions WHERE id = ?)\n ELSE status END\n WHERE id = ?`\n )\n .run(dropId, dropId, dropId, dropId, dropId, keepId);\n }\n\n async countSessionsForProject(projectId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM sessions WHERE project_id = ?\").get(projectId) as {\n n: number;\n }).n;\n }\n\n async countSessions(): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM sessions\").get() as { n: number }).n;\n }\n\n async getMostRecentSessionDate(projectId: number): Promise<string | null> {\n const row = this.db\n .prepare(\"SELECT date FROM sessions WHERE project_id = ? ORDER BY date DESC LIMIT 1\")\n .get(projectId) as { date: string } | undefined;\n return row?.date ?? null;\n }\n\n async getMostRecentSessionCreatedAt(): Promise<number | null> {\n const row = this.db.prepare(\"SELECT created_at FROM sessions ORDER BY created_at DESC LIMIT 1\").get() as\n | { created_at: number }\n | undefined;\n return row?.created_at ?? null;\n }\n\n // -------------------------------------------------------------------------\n // Session tags\n // -------------------------------------------------------------------------\n\n async listSessionTagIds(sessionId: number): Promise<number[]> {\n const rows = this.db.prepare(\"SELECT tag_id FROM session_tags WHERE session_id = ?\").all(sessionId) as Array<{\n tag_id: number;\n }>;\n return rows.map((r) => r.tag_id);\n }\n\n async sessionHasTag(sessionId: number, tagId: number): Promise<boolean> {\n return !!this.db.prepare(\"SELECT 1 FROM session_tags WHERE session_id = ? AND tag_id = ?\").get(\n sessionId,\n tagId\n );\n }\n\n async addSessionTag(sessionId: number, tagId: number): Promise<void> {\n this.db.prepare(\"INSERT INTO session_tags (session_id, tag_id) VALUES (?, ?)\").run(sessionId, tagId);\n }\n\n async deleteSessionTag(sessionId: number, tagId: number): Promise<void> {\n this.db.prepare(\"DELETE FROM session_tags WHERE session_id = ? AND tag_id = ?\").run(sessionId, tagId);\n }\n\n async reassignSessionTag(fromSessionId: number, toSessionId: number, tagId: number): Promise<void> {\n this.db\n .prepare(\"UPDATE session_tags SET session_id = ? WHERE session_id = ? AND tag_id = ?\")\n .run(toSessionId, fromSessionId, tagId);\n }\n\n async listTagsForSession(sessionId: number): Promise<string[]> {\n const rows = this.db\n .prepare(\n \"SELECT t.name FROM tags t JOIN session_tags st ON st.tag_id = t.id WHERE st.session_id = ? ORDER BY t.name\"\n )\n .all(sessionId) as Array<{ name: string }>;\n return rows.map((r) => r.name);\n }\n\n // -------------------------------------------------------------------------\n // Links\n // -------------------------------------------------------------------------\n\n async addLink(input: {\n sessionId: number;\n targetProjectId: number;\n linkType: LinkType;\n createdAt: number;\n }): Promise<void> {\n this.db\n .prepare(\"INSERT INTO links (session_id, target_project_id, link_type, created_at) VALUES (?, ?, ?, ?)\")\n .run(input.sessionId, input.targetProjectId, input.linkType, input.createdAt);\n }\n\n async listLinksForSession(sessionId: number): Promise<Array<{ id: number; target_project_id: number }>> {\n return this.db.prepare(\"SELECT id, target_project_id FROM links WHERE session_id = ?\").all(\n sessionId\n ) as Array<{ id: number; target_project_id: number }>;\n }\n\n async listLinksForProject(projectId: number): Promise<Array<{ id: number; session_id: number }>> {\n return this.db.prepare(\"SELECT id, session_id FROM links WHERE target_project_id = ?\").all(\n projectId\n ) as Array<{ id: number; session_id: number }>;\n }\n\n async linkExists(sessionId: number, targetProjectId: number): Promise<boolean> {\n return !!this.db.prepare(\"SELECT 1 FROM links WHERE session_id = ? AND target_project_id = ? LIMIT 1\").get(\n sessionId,\n targetProjectId\n );\n }\n\n async deleteLink(id: number): Promise<void> {\n this.db.prepare(\"DELETE FROM links WHERE id = ?\").run(id);\n }\n\n async deleteLinksTargetingProject(projectId: number): Promise<void> {\n this.db.prepare(\"DELETE FROM links WHERE target_project_id = ?\").run(projectId);\n }\n\n async deleteLinksFromProjectSessions(projectId: number): Promise<void> {\n this.db\n .prepare(\"DELETE FROM links WHERE session_id IN (SELECT id FROM sessions WHERE project_id = ?)\")\n .run(projectId);\n }\n\n async retargetLink(id: number, targetProjectId: number): Promise<void> {\n this.db.prepare(\"UPDATE links SET target_project_id = ? WHERE id = ?\").run(targetProjectId, id);\n }\n\n async moveLinkToSession(id: number, sessionId: number): Promise<void> {\n this.db.prepare(\"UPDATE links SET session_id = ? WHERE id = ?\").run(sessionId, id);\n }\n\n async reassignLinksTarget(fromProjectId: number, toProjectId: number): Promise<void> {\n this.db\n .prepare(\"UPDATE OR IGNORE links SET target_project_id = ? WHERE target_project_id = ?\")\n .run(toProjectId, fromProjectId);\n }\n\n async countLinksForProject(projectId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM links WHERE target_project_id = ?\").get(projectId) as {\n n: number;\n }).n;\n }\n\n async deleteSelfLinksForProject(projectId: number): Promise<void> {\n this.db\n .prepare(\n `DELETE FROM links WHERE target_project_id = ?\n AND session_id IN (SELECT id FROM sessions WHERE project_id = ?)`\n )\n .run(projectId, projectId);\n }\n\n // -------------------------------------------------------------------------\n // Compaction log\n // -------------------------------------------------------------------------\n\n async appendCompactionLog(entry: NewCompactionLogEntry): Promise<void> {\n this.db\n .prepare(\n `INSERT INTO compaction_log (project_id, session_id, trigger, files_written, token_count, created_at)\n VALUES (?, ?, ?, ?, ?, ?)`\n )\n .run(entry.projectId, entry.sessionId, entry.trigger, entry.filesWritten, entry.tokenCount, entry.createdAt);\n }\n\n async countCompactionLogsForProject(projectId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM compaction_log WHERE project_id = ?\").get(projectId) as {\n n: number;\n }).n;\n }\n\n async moveCompactionLogsByProject(fromProjectId: number, toProjectId: number): Promise<number> {\n return this.db\n .prepare(\"UPDATE compaction_log SET project_id = ? WHERE project_id = ?\")\n .run(toProjectId, fromProjectId).changes;\n }\n\n async moveCompactionLogsBySession(fromSessionId: number, toSessionId: number): Promise<void> {\n this.db\n .prepare(\"UPDATE compaction_log SET session_id = ? WHERE session_id = ?\")\n .run(toSessionId, fromSessionId);\n }\n\n async deleteCompactionLogsForProject(projectId: number): Promise<void> {\n this.db.prepare(\"DELETE FROM compaction_log WHERE project_id = ?\").run(projectId);\n }\n\n // -------------------------------------------------------------------------\n // Project merge\n // -------------------------------------------------------------------------\n\n async planProjectMerge(fromSlug: string, intoSlug: string): Promise<MergePlan> {\n return planMerge(this.db, fromSlug, intoSlug);\n }\n\n async applyProjectMerge(plan: MergePlan): Promise<void> {\n applyMerge(this.db, plan);\n }\n}\n"],"mappings":";;;;;;;;;;AA8CA,SAAgB,UAAU,IAAc,UAAkB,UAA6B;AACrF,KAAI,aAAa,SACf,OAAM,IAAI,WAAW,gBAAgB,SAAS,eAAe;CAG/D,MAAM,OAAO,GACV,QAAQ,+CAA+C,CACvD,IAAI,SAAS;CAChB,MAAM,OAAO,GACV,QAAQ,+CAA+C,CACvD,IAAI,SAAS;AAEhB,KAAI,CAAC,KAAM,OAAM,IAAI,WAAW,yBAAyB,SAAS,IAAI;AACtE,KAAI,CAAC,KAAM,OAAM,IAAI,WAAW,yBAAyB,SAAS,IAAI;CAOtE,IAAI,OAHW,GACZ,QAAQ,0EAA0E,CAClF,IAAI,KAAK,GAAG,CACG;CAMlB,MAAM,WAJW,GACd,QAAQ,2EAA2E,CACnF,IAAI,KAAK,GAAG,CAEW,KAAK,OAAO;EAAE,IAAI,EAAE;EAAI,MAAM,EAAE;EAAQ,IAAI,EAAE;EAAM,EAAE;CAEhF,MAAM,SAAS,KAAa,GAAG,SAC5B,GAAG,QAAQ,IAAI,CAAC,IAAI,GAAG,KAAK,CAAmB;CAIlD,MAAM,aACJ,MAAM,qDAAqD,KAAK,KAAK,GAAG,KACxE,MAAM,qDAAqD,KAAK,KAAK,GAAG;AAE1E,QAAO;EACL,QAAQ,KAAK;EACb,UAAU,KAAK;EACf,QAAQ,KAAK;EACb,UAAU,KAAK;EACf;EACA,MAAM,MAAM,+DAA+D,KAAK,GAAG;EACnF,SAAS,MAAM,0DAA0D,KAAK,GAAG;EACjF,aAAa,MACX,iEACA,KAAK,GACN;EACD,OAAO,MAAM,+DAA+D,KAAK,GAAG;EACpF,YAAY,aAAa,SAAY,KAAK;EAC3C;;;;;;;;;AAUH,SAAgB,WAAW,IAAc,MAAuB;AAkD9D,CAjDY,GAAG,kBAAkB;EAE/B,MAAM,OAAO,GAAG,QACd,8DACD;AACD,OAAK,MAAM,KAAK,KAAK,SAAU,MAAK,IAAI,KAAK,QAAQ,EAAE,IAAI,EAAE,GAAG;AAIhE,KAAG,QACD;gEAED,CAAC,IAAI,KAAK,QAAQ,KAAK,OAAO;AAC/B,KAAG,QAAQ,gDAAgD,CAAC,IAAI,KAAK,OAAO;AAG5E,KAAG,QAAQ,yDAAyD,CAAC,IACnE,KAAK,QACL,KAAK,OACN;AAED,KAAG,QAAQ,gEAAgE,CAAC,IAC1E,KAAK,QACL,KAAK,OACN;AAKD,KAAG,QACD,+EACD,CAAC,IAAI,KAAK,QAAQ,KAAK,OAAO;AAC/B,KAAG,QAAQ,gDAAgD,CAAC,IAAI,KAAK,OAAO;AAC5E,KAAG,QACD;2EAED,CAAC,IAAI,KAAK,QAAQ,KAAK,OAAO;AAG/B,MAAI,KAAK,WACP,IAAG,QAAQ,kEAAkE,CAAC,IAC5E,KAAK,YACL,KAAK,OACN;AAGH,KAAG,QAAQ,oCAAoC,CAAC,IAAI,KAAK,OAAO;GAChE,EAEG;;;;;ACzHP,IAAa,wBAAb,MAA8D;CAC5D,AAAS,cAAc;CAEvB,AAAQ;CAER,YAAY,IAAc;AACxB,OAAK,KAAK;;;CAIZ,WAAqB;AACnB,SAAO,KAAK;;CAGd,MAAM,QAAuB;AAC3B,MAAI;AACF,QAAK,GAAG,OAAO;UACT;;CAKV,MAAM,gBAA+B;AAWnC,EAVY,KAAK,GAAG,kBAAkB;AACpC,QAAK,GAAG,QAAQ,oBAAoB,CAAC,KAAK;AAC1C,QAAK,GAAG,QAAQ,6BAA6B,CAAC,KAAK;AACnD,QAAK,GAAG,QAAQ,2BAA2B,CAAC,KAAK;AACjD,QAAK,GAAG,QAAQ,2BAA2B,CAAC,KAAK;AACjD,QAAK,GAAG,QAAQ,sBAAsB,CAAC,KAAK;AAC5C,QAAK,GAAG,QAAQ,uBAAuB,CAAC,KAAK;AAC7C,QAAK,GAAG,QAAQ,mBAAmB,CAAC,KAAK;AACzC,QAAK,GAAG,QAAQ,uBAAuB,CAAC,KAAK;IAC7C,EACG;;CAOP,MAAM,eAAe,IAAqC;AAExD,SADY,KAAK,GAAG,QAAQ,sCAAsC,CAAC,IAAI,GAAG,IAC5D;;CAGhB,MAAM,iBACJ,MACA,OAA0D,EAAE,EACnC;EACzB,IAAI,MAAM,KAAK,kBACX,wDACA;EACJ,MAAM,SAAoB,CAAC,KAAK;AAChC,MAAI,KAAK,cAAc,QAAW;AAChC,UAAO;AACP,UAAO,KAAK,KAAK,UAAU;;AAG7B,SADY,KAAK,GAAG,QAAQ,IAAI,CAAC,IAAI,GAAG,OAAO,IACjC;;CAGhB,MAAM,kBACJ,OACA,OAAsC,EAAE,EACf;EACzB,MAAM,MAAM,KAAK,kBACb,qGACA;AAEJ,SADY,KAAK,GAAG,QAAQ,IAAI,CAAC,IAAI,MAAM,IAC7B;;CAGhB,MAAM,qBAAqB,UAAkB,OAA+B,EAAE,EAA2B;EACvG,IAAI,MAAM;EACV,MAAM,SAAoB,CAAC,SAAS;AACpC,MAAI,KAAK,cAAc,QAAW;AAChC,UAAO;AACP,UAAO,KAAK,KAAK,UAAU;;AAG7B,SADY,KAAK,GAAG,QAAQ,IAAI,CAAC,IAAI,GAAG,OAAO,IACjC;;CAGhB,MAAM,uBAAuB,YAAoB,OAA+B,EAAE,EAA2B;EAC3G,IAAI,MAAM;EACV,MAAM,SAAoB,CAAC,WAAW;AACtC,MAAI,KAAK,cAAc,QAAW;AAChC,UAAO;AACP,UAAO,KAAK,KAAK,UAAU;;AAG7B,SADY,KAAK,GAAG,QAAQ,IAAI,CAAC,IAAI,GAAG,OAAO,IACjC;;CAGhB,MAAM,uBAAuB,KAAsC;AAOjE,SANY,KAAK,GACd,QACC;kDAED,CACA,IAAI,IAAI,IACG;;CAGhB,MAAM,aAAa,OAA4B,EAAE,EAAsB;EACrE,MAAM,QAAkB,EAAE;EAC1B,MAAM,SAAoB,EAAE;AAE5B,MAAI,KAAK,QAAQ;AACf,SAAM,KAAK,eAAe;AAC1B,UAAO,KAAK,KAAK,OAAO;;AAE1B,MAAI,KAAK,gBACP,OAAM,KAAK,yBAAyB;AAEtC,MAAI,KAAK,SAAS;AAChB,SAAM,KACJ,uGACD;AACD,UAAO,KAAK,KAAK,QAAQ;;EAG3B,IAAI,MAAM;AACV,MAAI,MAAM,OAAQ,QAAO,YAAY,MAAM,KAAK,QAAQ;AAExD,UAAQ,KAAK,SAAb;GACE,KAAK;AACH,WAAO;AACP;GACF,KAAK;AACH,WAAO;AACP;GACF,KAAK;AACH,WAAO;AACP;GAEF;AACE,WAAO;AACP;;AAEJ,MAAI,KAAK,UAAU,QAAW;AAC5B,UAAO;AACP,UAAO,KAAK,KAAK,MAAM;;AAGzB,SAAO,KAAK,GAAG,QAAQ,IAAI,CAAC,IAAI,GAAG,OAAO;;CAG5C,MAAM,6BAA6B,OAAwC,EAAE,EAAsB;EACjG,MAAM,QAAQ,KAAK,kBAAkB,+BAA+B;AACpE,SAAO,KAAK,GACT,QAAQ,0BAA0B,MAAM,kCAAkC,CAC1E,KAAK;;CAGV,MAAM,6BACJ,OAAqC,EAAE,EACH;EACpC,MAAM,QAAkB,EAAE;EAC1B,MAAM,SAAoB,EAAE;AAE5B,MAAI,KAAK,QAAQ;AACf,SAAM,KAAK,eAAe;AAC1B,UAAO,KAAK,KAAK,OAAO;;AAE1B,MAAI,KAAK,UAAU,QAAW;AAC5B,SAAM,KAAK,iEAAiE;AAC5E,UAAO,KAAK,KAAK,MAAM;;EAGzB,IAAI,MAAM;;;;;;AAMV,MAAI,MAAM,OAAQ,QAAO,YAAY,MAAM,KAAK,QAAQ;AACxD,SACE,KAAK,YAAY,iBACb,gCACA;AAEN,SAAO,KAAK,GAAG,QAAQ,IAAI,CAAC,IAAI,GAAG,OAAO;;CAG5C,MAAM,kBACJ,OAAiC,EAAE,EACgC;EAEnE,MAAM,MAAM;;;;;QADC,KAAK,iBAAiB,cAAc,OAMxC;;;;AAIT,SAAO,KAAK,GAAG,QAAQ,IAAI,CAAC,KAAK;;CAGnC,MAAM,eAAe,OAAe,QAAQ,IAAwB;EAClE,MAAM,IAAI,IAAI,MAAM;AACpB,SAAO,KAAK,GACT,QACC;2CAED,CACA,IAAI,GAAG,GAAG,GAAG,MAAM;;CAGxB,MAAM,cAAc,OAAmC,EAAE,EAAmB;EAC1E,MAAM,MAAM,KAAK,SACb,wDACA;AAIJ,UAHY,KAAK,SACZ,KAAK,GAAG,QAAQ,IAAI,CAAC,IAAI,KAAK,OAAO,GACrC,KAAK,GAAG,QAAQ,IAAI,CAAC,KAAK,EACpB;;CAGb,MAAM,gCAAwD;AAI5D,SAHY,KAAK,GAAG,QAAQ,mEAAmE,CAAC,KAAK,EAGzF,cAAc;;CAG5B,MAAM,mBAAmB,UAAmC;AAC1D,SAAQ,KAAK,GAAG,QAAQ,yDAAyD,CAAC,IAAI,SAAS,CAE5F;;CAGL,MAAM,iCACJ,kBACA,MACA,QAAQ,GACoE;AAC5E,SAAO,KAAK,GACT,QACC;;;qFAID,CACA,IAAI,kBAAkB,MAAM,MAAM,MAAM,MAAM;;CAWnD,MAAM,cAAc,OAAqC;EACvD,MAAM,OAAoB,MAAM,QAAQ;EACxC,MAAM,SAAwB,MAAM,UAAU;EAC9C,MAAM,SAAS,KAAK,GACjB,QACC;0CAED,CACA,IACC,MAAM,MACN,MAAM,aACN,MAAM,UACN,MAAM,YACN,MACA,QACA,MAAM,WACN,MAAM,UACP;AACH,SAAQ,MAAM,KAAK,eAAe,OAAO,gBAA0B;;CAGrE,MAAM,2BACJ,OACyD;EACzD,IAAI,OAAO,MAAM;EACjB,IAAI,UAAU;EACd,MAAM,SAAS,KAAK,GAAG,QACrB;qDAED;AAGD,SAAO,MAAM;GACX,MAAM,OAAO,OAAO,IAClB,MACA,MAAM,aACN,MAAM,UACN,MAAM,YACN,MAAM,WACN,MAAM,UACP;AACD,OAAI,KAAK,UAAU,EACjB,QAAO;IAAE,IAAI,KAAK;IAA2B;IAAM,SAAS;IAAM;GAGpE,MAAM,WAAW,KAAK,GAAG,QAAQ,8CAA8C,CAAC,IAAI,MAAM,SAAS;AAGnG,OAAI,SACF,QAAO;IAAE,IAAI,SAAS;IAAI;IAAM,SAAS;IAAO;AAGlD;AACA,UAAO,GAAG,MAAM,SAAS,GAAG;;;CAIhC,MAAM,kBACJ,IACA,OACA,WACe;EACf,MAAM,OAAiB,EAAE;EACzB,MAAM,SAAoB,EAAE;AAC5B,MAAI,MAAM,aAAa,QAAW;AAChC,QAAK,KAAK,gBAAgB;AAC1B,UAAO,KAAK,MAAM,SAAS;;AAE7B,MAAI,MAAM,eAAe,QAAW;AAClC,QAAK,KAAK,kBAAkB;AAC5B,UAAO,KAAK,MAAM,WAAW;;AAE/B,MAAI,cAAc,QAAW;AAC3B,QAAK,KAAK,iBAAiB;AAC3B,UAAO,KAAK,UAAU;;AAExB,MAAI,CAAC,KAAK,OAAQ;AAClB,SAAO,KAAK,GAAG;AACf,OAAK,GAAG,QAAQ,uBAAuB,KAAK,KAAK,KAAK,CAAC,eAAe,CAAC,IAAI,GAAG,OAAO;;CAGvF,MAAM,oBACJ,IACA,QACA,OAAiG,EAAE,EACpF;EACf,MAAM,OAAiB,CAAC,aAAa;EACrC,MAAM,SAAoB,CAAC,OAAO;AAClC,MAAI,KAAK,eAAe,QAAW;AACjC,QAAK,KAAK,kBAAkB;AAC5B,UAAO,KAAK,KAAK,WAAW;;AAE9B,MAAI,KAAK,cAAc,QAAW;AAChC,QAAK,KAAK,iBAAiB;AAC3B,UAAO,KAAK,KAAK,UAAU;;EAE7B,IAAI,MAAM,uBAAuB,KAAK,KAAK,KAAK,CAAC;AACjD,SAAO,KAAK,GAAG;AACf,MAAI,KAAK,sBAAsB;AAC7B,UAAO;AACP,UAAO,KAAK,KAAK,qBAAqB;;AAExC,OAAK,GAAG,QAAQ,IAAI,CAAC,IAAI,GAAG,OAAO;;CAGrC,MAAM,yBAAyB,IAAY,aAAqB,WAAmC;AACjG,MAAI,cAAc,OAChB,MAAK,GACF,QAAQ,oEAAoE,CAC5E,IAAI,aAAa,WAAW,GAAG;MAElC,MAAK,GAAG,QAAQ,oDAAoD,CAAC,IAAI,aAAa,GAAG;;CAI7F,MAAM,kBAAkB,IAAY,MAAmB,WAAmC;AACxF,MAAI,cAAc,OAChB,MAAK,GAAG,QAAQ,4DAA4D,CAAC,IAAI,MAAM,WAAW,GAAG;MAErG,MAAK,GAAG,QAAQ,4CAA4C,CAAC,IAAI,MAAM,GAAG;;CAI9E,MAAM,2BAA2B,IAAY,QAAuB,WAAmC;AACrG,MAAI,cAAc,OAChB,MAAK,GACF,QAAQ,sEAAsE,CAC9E,IAAI,QAAQ,WAAW,GAAG;MAE7B,MAAK,GAAG,QAAQ,sDAAsD,CAAC,IAAI,QAAQ,GAAG;;CAI1F,MAAM,4BAA4B,IAAY,KAAoB,WAAmC;AACnG,MAAI,cAAc,OAChB,MAAK,GACF,QAAQ,wEAAwE,CAChF,IAAI,KAAK,WAAW,GAAG;MAE1B,MAAK,GAAG,QAAQ,wDAAwD,CAAC,IAAI,KAAK,GAAG;;CAIzF,MAAM,0BAA0B,IAAY,MAAqB,WAAmC;AAClG,MAAI,cAAc,OAChB,MAAK,GACF,QAAQ,qEAAqE,CAC7E,IAAI,MAAM,WAAW,GAAG;MAE3B,MAAK,GAAG,QAAQ,qDAAqD,CAAC,IAAI,MAAM,GAAG;;CAIvF,MAAM,kBAAkB,IAAY,MAAc,WAAkC;AAClF,OAAK,GAAG,QAAQ,4DAA4D,CAAC,IAAI,MAAM,WAAW,GAAG;;CAGvG,MAAM,sBAAsB,cAAsB,YAAqC;AACrF,SAAO,KAAK,GAAG,QAAQ,wDAAwD,CAAC,IAAI,YAAY,aAAa,CAC1G;;CAGL,MAAM,cAAc,IAA2B;AAC7C,OAAK,GAAG,QAAQ,oCAAoC,CAAC,IAAI,GAAG;;CAG9D,MAAM,qBAAqB,IAA2B;AAYpD,EAXY,KAAK,GAAG,kBAAkB;AACpC,QAAK,GAAG,QAAQ,gDAAgD,CAAC,IAAI,GAAG;AACxE,QAAK,GACF,QAAQ,uFAAuF,CAC/F,IAAI,GAAG;AACV,QAAK,GAAG,QAAQ,kDAAkD,CAAC,IAAI,GAAG;AAC1E,QAAK,GAAG,QAAQ,gDAAgD,CAAC,IAAI,GAAG;AACxE,QAAK,GAAG,QAAQ,2CAA2C,CAAC,IAAI,GAAG;AACnE,QAAK,GAAG,QAAQ,4CAA4C,CAAC,IAAI,GAAG;AACpE,QAAK,GAAG,QAAQ,oCAAoC,CAAC,IAAI,GAAG;IAC5D,EACG;;CAOP,MAAM,mBAAmB,WAAsC;AAM7D,SALa,KAAK,GACf,QACC,6GACD,CACA,IAAI,UAAU,CACL,KAAK,MAAM,EAAE,KAAK;;CAGhC,MAAM,cAA4D;AAChE,SAAO,KAAK,GAAG,QAAQ,0CAA0C,CAAC,KAAK;;CAMzE,MAAM,UAAU,MAA+B;AAC7C,OAAK,GAAG,QAAQ,+CAA+C,CAAC,IAAI,KAAK;AACzE,SAAQ,KAAK,GAAG,QAAQ,qCAAqC,CAAC,IAAI,KAAK,CAAoB;;CAG7F,MAAM,cAAc,WAAmB,OAA8B;AACnE,OAAK,GAAG,QAAQ,8DAA8D,CAAC,IAAI,WAAW,MAAM;;CAGtG,MAAM,cAAc,WAAmB,OAAiC;AACtE,SAAO,CAAC,CAAC,KAAK,GAAG,QAAQ,iEAAiE,CAAC,IAAI,WAAW,MAAM;;CAGlH,MAAM,iBAAiB,WAAmB,OAA8B;AACtE,OAAK,GAAG,QAAQ,+DAA+D,CAAC,IAAI,WAAW,MAAM;;CAGvG,MAAM,mBAAmB,eAAuB,aAAqB,OAA8B;AACjG,OAAK,GACF,QAAQ,6EAA6E,CACrF,IAAI,aAAa,eAAe,MAAM;;CAG3C,MAAM,kBAAkB,WAAsC;AAI5D,SAHa,KAAK,GAAG,QAAQ,uDAAuD,CAAC,IAAI,UAAU,CAGvF,KAAK,MAAM,EAAE,OAAO;;CAGlC,MAAM,2BAA2B,WAAoC;AACnE,SAAQ,KAAK,GAAG,QAAQ,8DAA8D,CAAC,IAAI,UAAU,CAElG;;CAGL,MAAM,gBAAgB,eAAuB,aAAoC;AAC/E,OAAK,GACF,QACC,kHACD,CACA,IAAI,aAAa,cAAc;;CAGpC,MAAM,4BAA4B,WAAkC;AAClE,OAAK,GAAG,QAAQ,gDAAgD,CAAC,IAAI,UAAU;;CAOjF,MAAM,aAAa,OAAuC;AAIxD,SAHY,KAAK,GAAG,QAAQ,iDAAiD,CAAC,IAAI,MAAM,EAG5E,cAAc;;CAG5B,MAAM,sBAAsB,WAAsC;AAIhE,SAHa,KAAK,GAAG,QAAQ,gEAAgE,CAAC,IAC5F,UACD,CACW,KAAK,MAAM,EAAE,MAAM;;CAGjC,MAAM,SAAS,OAAe,WAAkC;AAC9D,OAAK,GAAG,QAAQ,wDAAwD,CAAC,IAAI,OAAO,UAAU;;CAGhG,MAAM,YAAY,OAA8B;AAC9C,OAAK,GAAG,QAAQ,sCAAsC,CAAC,IAAI,MAAM;;CAGnE,MAAM,uBAAuB,WAAoC;AAC/D,SAAQ,KAAK,GAAG,QAAQ,yDAAyD,CAAC,IAAI,UAAU,CAE7F;;CAGL,MAAM,mBAAmB,eAAuB,aAAoC;AAClF,OAAK,GAAG,QAAQ,yDAAyD,CAAC,IAAI,aAAa,cAAc;;CAG3G,MAAM,cAAc,OAAe,aAAoC;AACrE,OAAK,GAAG,QAAQ,oDAAoD,CAAC,IAAI,aAAa,MAAM;;CAG9F,MAAM,eAAmF;AACvF,SAAO,KAAK,GACT,QACC;6FAED,CACA,KAAK;;CAOV,MAAM,eAAe,IAAqC;AAExD,SADY,KAAK,GAAG,QAAQ,sCAAsC,CAAC,IAAI,GAAG,IAC5D;;CAGhB,MAAM,mBAAmB,WAAmB,QAAyC;AAKnF,SAJY,KAAK,GAAG,QAAQ,6DAA6D,CAAC,IACxF,WACA,OACD,IACa;;CAGhB,MAAM,2BAA2B,WAA4C;AAI3E,SAHY,KAAK,GACd,QAAQ,2EAA2E,CACnF,IAAI,UAAU,IACH;;CAGhB,MAAM,oBAAoB,WAAoC;AAI5D,SAHY,KAAK,GAAG,QAAQ,0EAA0E,CAAC,IACrG,UACD,CACU;;CAGb,MAAM,uBACJ,WACA,OAAsC,EAAE,EACpB;EAEpB,IAAI,MAAM,wDADM,KAAK,YAAY,iBAAiB,oBAAoB;EAEtE,MAAM,SAAoB,CAAC,UAAU;AACrC,MAAI,KAAK,UAAU,QAAW;AAC5B,UAAO;AACP,UAAO,KAAK,KAAK,MAAM;;AAEzB,SAAO,KAAK,GAAG,QAAQ,IAAI,CAAC,IAAI,GAAG,OAAO;;CAG5C,MAAM,aAAa,OAA4B,EAAE,EAAiC;EAChF,MAAM,QAAkB,EAAE;EAC1B,MAAM,SAAoB,EAAE;AAC5B,MAAI,KAAK,cAAc,QAAW;AAChC,SAAM,KAAK,mBAAmB;AAC9B,UAAO,KAAK,KAAK,UAAU;;AAE7B,MAAI,KAAK,QAAQ;AACf,SAAM,KAAK,eAAe;AAC1B,UAAO,KAAK,KAAK,OAAO;;EAE1B,IAAI,MAAM;;;;AAIV,MAAI,MAAM,OAAQ,QAAO,YAAY,MAAM,KAAK,QAAQ;AACxD,SAAO;EACP,MAAM,QAAQ,KAAK,SAAS;AAC5B,SAAO,UAAU;AACjB,SAAO,KAAK,GAAG,QAAQ,IAAI,CAAC,IAAI,GAAG,OAAO;;CAG5C,MAAM,sBAAsB,WAAmB,UAAkD;AAI/F,SAHY,KAAK,GACd,QAAQ,wEAAwE,CAChF,IAAI,WAAW,SAAS,IACb;;CAGhB,MAAM,mBAAmB,WAAmB,QAAkC;AAC5E,SAAO,CAAC,CAAC,KAAK,GAAG,QAAQ,qEAAqE,CAAC,IAC7F,WACA,OACD;;CAGH,MAAM,cAAc,OAAqC;EACvD,MAAM,SAAS,MAAM,UAAU;EAC/B,MAAM,SAAS,KAAK,GACjB,QACC;0CAED,CACA,IAAI,MAAM,WAAW,MAAM,QAAQ,MAAM,MAAM,MAAM,MAAM,MAAM,OAAO,MAAM,UAAU,QAAQ,MAAM,UAAU;AACnH,SAAQ,MAAM,KAAK,eAAe,OAAO,gBAA0B;;CAGrE,MAAM,sBAAsB,OAAqC;AAI/D,MAHiB,KAAK,GACnB,QAAQ,8DAA8D,CACtE,IAAI,MAAM,WAAW,MAAM,OAAO,CACvB,QAAO;EAErB,MAAM,SAAS,MAAM,UAAU;AAC/B,OAAK,GACF,QACC;0CAED,CACA,IAAI,MAAM,WAAW,MAAM,QAAQ,MAAM,MAAM,MAAM,MAAM,MAAM,OAAO,MAAM,UAAU,QAAQ,MAAM,UAAU;AACnH,SAAO;;CAGT,MAAM,kBACJ,IACA,OACe;EACf,MAAM,OAAiB,EAAE;EACzB,MAAM,SAAoB,EAAE;AAC5B,MAAI,MAAM,SAAS,QAAW;AAC5B,QAAK,KAAK,WAAW;AACrB,UAAO,KAAK,MAAM,KAAK;;AAEzB,MAAI,MAAM,UAAU,QAAW;AAC7B,QAAK,KAAK,YAAY;AACtB,UAAO,KAAK,MAAM,MAAM;;AAE1B,MAAI,MAAM,aAAa,QAAW;AAChC,QAAK,KAAK,eAAe;AACzB,UAAO,KAAK,MAAM,SAAS;;AAE7B,MAAI,CAAC,KAAK,OAAQ;AAClB,SAAO,KAAK,GAAG;AACf,OAAK,GAAG,QAAQ,uBAAuB,KAAK,KAAK,KAAK,CAAC,eAAe,CAAC,IAAI,GAAG,OAAO;;CAGvF,MAAM,oBAAoB,IAAY,QAAgB,OAA8B,EAAE,EAAiB;AACrG,MAAI,KAAK,aAAa,OACpB,MAAK,GAAG,QAAQ,4DAA4D,CAAC,IAAI,QAAQ,KAAK,UAAU,GAAG;MAE3G,MAAK,GAAG,QAAQ,8CAA8C,CAAC,IAAI,QAAQ,GAAG;;CAIlF,MAAM,sBAAsB,IAAY,UAAiC;AACvE,OAAK,GAAG,QAAQ,gDAAgD,CAAC,IAAI,UAAU,GAAG;;CAGpF,MAAM,oBAAoB,IAAY,QAAuB,OAA8B,EAAE,EAAiB;AAC5G,MAAI,KAAK,aAAa,OACpB,MAAK,GAAG,QAAQ,6DAA6D,CAAC,IAAI,QAAQ,KAAK,UAAU,GAAG;MAE5G,MAAK,GAAG,QAAQ,8CAA8C,CAAC,IAAI,QAAQ,GAAG;;CAIlF,MAAM,qBAAqB,IAAY,WAAmB,OAA4B,EAAE,EAAiB;AACvG,MAAI,KAAK,WAAW,OAClB,MAAK,GAAG,QAAQ,8DAA8D,CAAC,IAC7E,WACA,KAAK,QACL,GACD;MAED,MAAK,GAAG,QAAQ,kDAAkD,CAAC,IAAI,WAAW,GAAG;;CAIzF,MAAM,cAAc,IAA2B;AAC7C,OAAK,GAAG,QAAQ,oCAAoC,CAAC,IAAI,GAAG;;CAG9D,MAAM,sBAAsB,QAAgB,QAA+B;AACzE,OAAK,GACF,QACC;;;;;;;;;;;uBAYD,CACA,IAAI,QAAQ,QAAQ,QAAQ,QAAQ,QAAQ,OAAO;;CAGxD,MAAM,wBAAwB,WAAoC;AAChE,SAAQ,KAAK,GAAG,QAAQ,0DAA0D,CAAC,IAAI,UAAU,CAE9F;;CAGL,MAAM,gBAAiC;AACrC,SAAQ,KAAK,GAAG,QAAQ,qCAAqC,CAAC,KAAK,CAAmB;;CAGxF,MAAM,yBAAyB,WAA2C;AAIxE,SAHY,KAAK,GACd,QAAQ,4EAA4E,CACpF,IAAI,UAAU,EACL,QAAQ;;CAGtB,MAAM,gCAAwD;AAI5D,SAHY,KAAK,GAAG,QAAQ,mEAAmE,CAAC,KAAK,EAGzF,cAAc;;CAO5B,MAAM,kBAAkB,WAAsC;AAI5D,SAHa,KAAK,GAAG,QAAQ,uDAAuD,CAAC,IAAI,UAAU,CAGvF,KAAK,MAAM,EAAE,OAAO;;CAGlC,MAAM,cAAc,WAAmB,OAAiC;AACtE,SAAO,CAAC,CAAC,KAAK,GAAG,QAAQ,iEAAiE,CAAC,IACzF,WACA,MACD;;CAGH,MAAM,cAAc,WAAmB,OAA8B;AACnE,OAAK,GAAG,QAAQ,8DAA8D,CAAC,IAAI,WAAW,MAAM;;CAGtG,MAAM,iBAAiB,WAAmB,OAA8B;AACtE,OAAK,GAAG,QAAQ,+DAA+D,CAAC,IAAI,WAAW,MAAM;;CAGvG,MAAM,mBAAmB,eAAuB,aAAqB,OAA8B;AACjG,OAAK,GACF,QAAQ,6EAA6E,CACrF,IAAI,aAAa,eAAe,MAAM;;CAG3C,MAAM,mBAAmB,WAAsC;AAM7D,SALa,KAAK,GACf,QACC,6GACD,CACA,IAAI,UAAU,CACL,KAAK,MAAM,EAAE,KAAK;;CAOhC,MAAM,QAAQ,OAKI;AAChB,OAAK,GACF,QAAQ,+FAA+F,CACvG,IAAI,MAAM,WAAW,MAAM,iBAAiB,MAAM,UAAU,MAAM,UAAU;;CAGjF,MAAM,oBAAoB,WAA8E;AACtG,SAAO,KAAK,GAAG,QAAQ,+DAA+D,CAAC,IACrF,UACD;;CAGH,MAAM,oBAAoB,WAAuE;AAC/F,SAAO,KAAK,GAAG,QAAQ,+DAA+D,CAAC,IACrF,UACD;;CAGH,MAAM,WAAW,WAAmB,iBAA2C;AAC7E,SAAO,CAAC,CAAC,KAAK,GAAG,QAAQ,6EAA6E,CAAC,IACrG,WACA,gBACD;;CAGH,MAAM,WAAW,IAA2B;AAC1C,OAAK,GAAG,QAAQ,iCAAiC,CAAC,IAAI,GAAG;;CAG3D,MAAM,4BAA4B,WAAkC;AAClE,OAAK,GAAG,QAAQ,gDAAgD,CAAC,IAAI,UAAU;;CAGjF,MAAM,+BAA+B,WAAkC;AACrE,OAAK,GACF,QAAQ,uFAAuF,CAC/F,IAAI,UAAU;;CAGnB,MAAM,aAAa,IAAY,iBAAwC;AACrE,OAAK,GAAG,QAAQ,sDAAsD,CAAC,IAAI,iBAAiB,GAAG;;CAGjG,MAAM,kBAAkB,IAAY,WAAkC;AACpE,OAAK,GAAG,QAAQ,+CAA+C,CAAC,IAAI,WAAW,GAAG;;CAGpF,MAAM,oBAAoB,eAAuB,aAAoC;AACnF,OAAK,GACF,QAAQ,+EAA+E,CACvF,IAAI,aAAa,cAAc;;CAGpC,MAAM,qBAAqB,WAAoC;AAC7D,SAAQ,KAAK,GAAG,QAAQ,8DAA8D,CAAC,IAAI,UAAU,CAElG;;CAGL,MAAM,0BAA0B,WAAkC;AAChE,OAAK,GACF,QACC;6EAED,CACA,IAAI,WAAW,UAAU;;CAO9B,MAAM,oBAAoB,OAA6C;AACrE,OAAK,GACF,QACC;oCAED,CACA,IAAI,MAAM,WAAW,MAAM,WAAW,MAAM,SAAS,MAAM,cAAc,MAAM,YAAY,MAAM,UAAU;;CAGhH,MAAM,8BAA8B,WAAoC;AACtE,SAAQ,KAAK,GAAG,QAAQ,gEAAgE,CAAC,IAAI,UAAU,CAEpG;;CAGL,MAAM,4BAA4B,eAAuB,aAAsC;AAC7F,SAAO,KAAK,GACT,QAAQ,gEAAgE,CACxE,IAAI,aAAa,cAAc,CAAC;;CAGrC,MAAM,4BAA4B,eAAuB,aAAoC;AAC3F,OAAK,GACF,QAAQ,gEAAgE,CACxE,IAAI,aAAa,cAAc;;CAGpC,MAAM,+BAA+B,WAAkC;AACrE,OAAK,GAAG,QAAQ,kDAAkD,CAAC,IAAI,UAAU;;CAOnF,MAAM,iBAAiB,UAAkB,UAAsC;AAC7E,SAAO,UAAU,KAAK,IAAI,UAAU,SAAS;;CAG/C,MAAM,kBAAkB,MAAgC;AACtD,aAAW,KAAK,IAAI,KAAK"}
1
+ {"version":3,"file":"registry-sqlite-DrCW1aRK.mjs","names":[],"sources":["../src/storage/sqlite/registry-merge.ts","../src/storage/registry-sqlite.ts"],"sourcesContent":["/**\n * planMerge/applyMerge — fold one registered project into another without\n * losing anything, against a raw better-sqlite3 Database.\n *\n * Moved out of src/registry/merge.ts (which keeps only the pure MergePlan\n * type and MergeError class) so that the one place in the codebase touching\n * better-sqlite3 for this operation is under src/storage/. Wrapped by\n * SQLiteRegistryBackend.planProjectMerge/applyProjectMerge\n * (../registry-sqlite.ts).\n *\n * The registry accumulates rows that describe the same work twice: a project\n * registered under two spellings of one directory, a vault-notes twin of a code\n * project, a slug that collided and got a `-1`. Measured 2026-08-04 there were\n * six such rows, and between them they carried ten sessions.\n *\n * Those sessions are the only thing of value on the losing row, which is why\n * `archive` is the wrong remedy and a plain DELETE is worse. `pai project health`\n * suggested archiving for every unhealthy row, and archiving a duplicate leaves\n * the sessions attached to a row nobody will look at again.\n *\n * FIVE tables reference a project, and `PRAGMA foreign_keys` is 0 on this\n * database — so deleting a project row does not fail, it silently orphans rows in\n * sessions, project_tags, aliases, compaction_log and links. Anything that merges\n * by hand will miss at least one of them. Hence one function.\n *\n * Two constraints shape the implementation:\n *\n * sessions UNIQUE (project_id, number) — incoming sessions must be renumbered,\n * or the insert collides with the\n * target's own numbering\n * links UNIQUE (session_id, target_project_id)\n * project_tags PRIMARY KEY (project_id, tag_id)\n * — both can already hold the row we are\n * about to point at the target\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { MergeError, type MergePlan } from \"../../registry/merge.js\";\n\n/**\n * What a merge would do, without doing it.\n *\n * Built as a plan first because the session renumbering is the part a reader will\n * not predict, and printing \"0001 -> 0016\" is the difference between a command\n * that can be trusted and one that has to be taken on faith.\n */\nexport function planMerge(db: Database, fromSlug: string, intoSlug: string): MergePlan {\n if (fromSlug === intoSlug) {\n throw new MergeError(`Cannot merge ${fromSlug} into itself.`);\n }\n\n const from = db\n .prepare(\"SELECT id, slug FROM projects WHERE slug = ?\")\n .get(fromSlug) as { id: number; slug: string } | undefined;\n const into = db\n .prepare(\"SELECT id, slug FROM projects WHERE slug = ?\")\n .get(intoSlug) as { id: number; slug: string } | undefined;\n\n if (!from) throw new MergeError(`No project with slug \"${fromSlug}\".`);\n if (!into) throw new MergeError(`No project with slug \"${intoSlug}\".`);\n\n // Where the target's numbering currently ends. Incoming sessions continue from\n // there rather than keeping their own numbers, which would collide.\n const maxRow = db\n .prepare(\"SELECT COALESCE(MAX(number), 0) AS n FROM sessions WHERE project_id = ?\")\n .get(into.id) as { n: number };\n let next = maxRow.n;\n\n const incoming = db\n .prepare(\"SELECT id, number FROM sessions WHERE project_id = ? ORDER BY number ASC\")\n .all(from.id) as { id: number; number: number }[];\n\n const sessions = incoming.map((s) => ({ id: s.id, from: s.number, to: ++next }));\n\n const count = (sql: string, ...args: unknown[]): number =>\n (db.prepare(sql).get(...args) as { n: number }).n;\n\n // The losing slug becomes an alias of the winner, so anything that resolved by\n // that name keeps working — including a human who remembers the old name.\n const aliasTaken =\n count(\"SELECT COUNT(*) AS n FROM aliases WHERE alias = ?\", from.slug) > 0 ||\n count(\"SELECT COUNT(*) AS n FROM projects WHERE slug = ?\", from.slug) > 1;\n\n return {\n fromId: from.id,\n fromSlug: from.slug,\n intoId: into.id,\n intoSlug: into.slug,\n sessions,\n tags: count(\"SELECT COUNT(*) AS n FROM project_tags WHERE project_id = ?\", from.id),\n aliases: count(\"SELECT COUNT(*) AS n FROM aliases WHERE project_id = ?\", from.id),\n compactions: count(\n \"SELECT COUNT(*) AS n FROM compaction_log WHERE project_id = ?\",\n from.id\n ),\n links: count(\"SELECT COUNT(*) AS n FROM links WHERE target_project_id = ?\", from.id),\n aliasToAdd: aliasTaken ? undefined : from.slug,\n };\n}\n\n/**\n * Apply a plan. One transaction: either the whole row is folded in or nothing is.\n *\n * A half-merged project is the worst outcome available here — sessions moved but\n * the row still present, or the row gone and its tags orphaned — and with foreign\n * keys off nothing would complain.\n */\nexport function applyMerge(db: Database, plan: MergePlan): void {\n const run = db.transaction(() => {\n // Sessions, one at a time because each gets its own new number.\n const move = db.prepare(\n \"UPDATE sessions SET project_id = ?, number = ? WHERE id = ?\"\n );\n for (const s of plan.sessions) move.run(plan.intoId, s.to, s.id);\n\n // Tags: OR IGNORE because (project_id, tag_id) is the primary key and the\n // target may already carry the same tag.\n db.prepare(\n `INSERT OR IGNORE INTO project_tags (project_id, tag_id)\n SELECT ?, tag_id FROM project_tags WHERE project_id = ?`\n ).run(plan.intoId, plan.fromId);\n db.prepare(\"DELETE FROM project_tags WHERE project_id = ?\").run(plan.fromId);\n\n // Existing aliases of the loser now point at the winner.\n db.prepare(\"UPDATE aliases SET project_id = ? WHERE project_id = ?\").run(\n plan.intoId,\n plan.fromId\n );\n\n db.prepare(\"UPDATE compaction_log SET project_id = ? WHERE project_id = ?\").run(\n plan.intoId,\n plan.fromId\n );\n\n // Links: same collision shape as tags, plus one extra case — a session that\n // linked to the loser may now be linking to its own project, which is not a\n // relationship worth keeping.\n db.prepare(\n `UPDATE OR IGNORE links SET target_project_id = ? WHERE target_project_id = ?`\n ).run(plan.intoId, plan.fromId);\n db.prepare(\"DELETE FROM links WHERE target_project_id = ?\").run(plan.fromId);\n db.prepare(\n `DELETE FROM links WHERE target_project_id = ?\n AND session_id IN (SELECT id FROM sessions WHERE project_id = ?)`\n ).run(plan.intoId, plan.intoId);\n\n // And keep the old name resolvable.\n if (plan.aliasToAdd) {\n db.prepare(\"INSERT OR IGNORE INTO aliases (alias, project_id) VALUES (?, ?)\").run(\n plan.aliasToAdd,\n plan.intoId\n );\n }\n\n db.prepare(\"DELETE FROM projects WHERE id = ?\").run(plan.fromId);\n });\n\n run();\n}\n","/**\n * SQLiteRegistryBackend — wraps the existing better-sqlite3 registry.db\n * (src/registry/db.ts, src/registry/merge.ts) behind the RegistryBackend\n * interface.\n *\n * Thin adapter: every method is the same SQL the 64 direct callers run today,\n * just relocated here and wrapped in Promise.resolve(...) so the interface can\n * be async (matching StorageBackend, and Postgres in unit 4) without changing\n * SQLite behaviour.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { planMerge, applyMerge } from \"./sqlite/registry-merge.js\";\nimport type {\n RegistryBackend,\n Project,\n ProjectWithSessionStats,\n Session,\n SessionWithProject,\n NewProject,\n NewProjectWithSlugRetry,\n NewSession,\n NewCompactionLogEntry,\n ListProjectsOptions,\n ListProjectsByPathLengthOptions,\n ListProjectsWithStatsOptions,\n ListNamedProjectsOptions,\n ListSessionsOptions,\n ListSessionsForProjectOptions,\n ProjectType,\n ProjectStatus,\n SessionStatus,\n LinkType,\n MergePlan,\n} from \"./registry-interface.js\";\n\nexport class SQLiteRegistryBackend implements RegistryBackend {\n readonly backendType = \"sqlite\" as const;\n\n private db: Database;\n\n constructor(db: Database) {\n this.db = db;\n }\n\n /** Expose the raw better-sqlite3 Database handle for not-yet-converted callers. */\n getRawDb(): Database {\n return this.db;\n }\n\n async close(): Promise<void> {\n try {\n this.db.close();\n } catch {\n // ignore\n }\n }\n\n async resetRegistry(): Promise<void> {\n const run = this.db.transaction(() => {\n this.db.prepare(\"DELETE FROM links\").run();\n this.db.prepare(\"DELETE FROM compaction_log\").run();\n this.db.prepare(\"DELETE FROM session_tags\").run();\n this.db.prepare(\"DELETE FROM project_tags\").run();\n this.db.prepare(\"DELETE FROM aliases\").run();\n this.db.prepare(\"DELETE FROM sessions\").run();\n this.db.prepare(\"DELETE FROM tags\").run();\n this.db.prepare(\"DELETE FROM projects\").run();\n });\n run();\n }\n\n // -------------------------------------------------------------------------\n // Projects — reads\n // -------------------------------------------------------------------------\n\n async getProjectById(id: number): Promise<Project | null> {\n const row = this.db.prepare(\"SELECT * FROM projects WHERE id = ?\").get(id) as Project | undefined;\n return row ?? null;\n }\n\n async getProjectBySlug(\n slug: string,\n opts: { caseInsensitive?: boolean; excludeId?: number } = {}\n ): Promise<Project | null> {\n let sql = opts.caseInsensitive\n ? \"SELECT * FROM projects WHERE lower(slug) = lower(?)\"\n : \"SELECT * FROM projects WHERE slug = ?\";\n const params: unknown[] = [slug];\n if (opts.excludeId !== undefined) {\n sql += \" AND id != ?\";\n params.push(opts.excludeId);\n }\n const row = this.db.prepare(sql).get(...params) as Project | undefined;\n return row ?? null;\n }\n\n async getProjectByAlias(\n alias: string,\n opts: { caseInsensitive?: boolean } = {}\n ): Promise<Project | null> {\n const sql = opts.caseInsensitive\n ? \"SELECT p.* FROM projects p JOIN aliases a ON a.project_id = p.id WHERE lower(a.alias) = lower(?)\"\n : \"SELECT p.* FROM projects p JOIN aliases a ON a.project_id = p.id WHERE a.alias = ?\";\n const row = this.db.prepare(sql).get(alias) as Project | undefined;\n return row ?? null;\n }\n\n async getProjectByRootPath(rootPath: string, opts: { excludeId?: number } = {}): Promise<Project | null> {\n let sql = \"SELECT * FROM projects WHERE root_path = ?\";\n const params: unknown[] = [rootPath];\n if (opts.excludeId !== undefined) {\n sql += \" AND id != ?\";\n params.push(opts.excludeId);\n }\n const row = this.db.prepare(sql).get(...params) as Project | undefined;\n return row ?? null;\n }\n\n async getProjectByEncodedDir(encodedDir: string, opts: { excludeId?: number } = {}): Promise<Project | null> {\n let sql = \"SELECT * FROM projects WHERE encoded_dir = ?\";\n const params: unknown[] = [encodedDir];\n if (opts.excludeId !== undefined) {\n sql += \" AND id != ?\";\n params.push(opts.excludeId);\n }\n const row = this.db.prepare(sql).get(...params) as Project | undefined;\n return row ?? null;\n }\n\n async findProjectByCwdPrefix(cwd: string): Promise<Project | null> {\n const row = this.db\n .prepare(\n `SELECT * FROM projects WHERE status = 'active' AND ? LIKE root_path || '%'\n ORDER BY length(root_path) DESC LIMIT 1`\n )\n .get(cwd) as Project | undefined;\n return row ?? null;\n }\n\n async listProjects(opts: ListProjectsOptions = {}): Promise<Project[]> {\n const where: string[] = [];\n const params: unknown[] = [];\n\n if (opts.status) {\n where.push(\"p.status = ?\");\n params.push(opts.status);\n }\n if (opts.excludeArchived) {\n where.push(\"p.status != 'archived'\");\n }\n if (opts.tagName) {\n where.push(\n \"p.id IN (SELECT pt.project_id FROM project_tags pt JOIN tags t ON pt.tag_id = t.id WHERE t.name = ?)\"\n );\n params.push(opts.tagName);\n }\n\n let sql = \"SELECT p.* FROM projects p\";\n if (where.length) sql += \" WHERE \" + where.join(\" AND \");\n\n switch (opts.orderBy) {\n case \"updated_desc\":\n sql += \" ORDER BY p.updated_at DESC\";\n break;\n case \"slug\":\n sql += \" ORDER BY p.slug ASC\";\n break;\n case \"id\":\n sql += \" ORDER BY p.id\";\n break;\n case \"status_updated\":\n default:\n sql += \" ORDER BY p.status ASC, p.updated_at DESC\";\n break;\n }\n if (opts.limit !== undefined) {\n sql += \" LIMIT ?\";\n params.push(opts.limit);\n }\n\n return this.db.prepare(sql).all(...params) as Project[];\n }\n\n async listProjectsByPathLengthDesc(opts: ListProjectsByPathLengthOptions = {}): Promise<Project[]> {\n const where = opts.excludeArchived ? \"WHERE status != 'archived'\" : \"\";\n return this.db\n .prepare(`SELECT * FROM projects ${where} ORDER BY LENGTH(root_path) DESC`)\n .all() as Project[];\n }\n\n async listProjectsWithSessionStats(\n opts: ListProjectsWithStatsOptions = {}\n ): Promise<ProjectWithSessionStats[]> {\n const where: string[] = [];\n const params: unknown[] = [];\n\n if (opts.status) {\n where.push(\"p.status = ?\");\n params.push(opts.status);\n }\n if (opts.tagId !== undefined) {\n where.push(\"p.id IN (SELECT project_id FROM project_tags WHERE tag_id = ?)\");\n params.push(opts.tagId);\n }\n\n let sql = `\n SELECT p.*,\n (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count,\n (SELECT MAX(s.created_at) FROM sessions s WHERE s.project_id = p.id) AS last_active\n FROM projects p\n `;\n if (where.length) sql += \" WHERE \" + where.join(\" AND \");\n sql +=\n opts.orderBy === \"updated_desc\"\n ? \" ORDER BY p.updated_at DESC\"\n : \" ORDER BY p.status ASC, p.updated_at DESC\";\n\n return this.db.prepare(sql).all(...params) as ProjectWithSessionStats[];\n }\n\n async listNamedProjects(\n opts: ListNamedProjectsOptions = {}\n ): Promise<Array<ProjectWithSessionStats & { name: string | null }>> {\n const join = opts.includeUnnamed ? \"LEFT JOIN\" : \"JOIN\";\n const sql = `\n SELECT p.*, a.alias AS name,\n (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count,\n (SELECT MAX(s.created_at) FROM sessions s WHERE s.project_id = p.id) AS last_active\n FROM projects p\n ${join} aliases a ON a.project_id = p.id\n WHERE p.status = 'active'\n ORDER BY p.updated_at DESC\n `;\n return this.db.prepare(sql).all() as Array<ProjectWithSessionStats & { name: string | null }>;\n }\n\n async searchProjects(query: string, limit = 20): Promise<Project[]> {\n const q = `%${query}%`;\n return this.db\n .prepare(\n `SELECT * FROM projects WHERE slug LIKE ? OR display_name LIKE ? OR root_path LIKE ?\n ORDER BY updated_at DESC LIMIT ?`\n )\n .all(q, q, q, limit) as Project[];\n }\n\n async countProjects(opts: { status?: ProjectStatus } = {}): Promise<number> {\n const sql = opts.status\n ? \"SELECT COUNT(*) AS n FROM projects WHERE status = ?\"\n : \"SELECT COUNT(*) AS n FROM projects\";\n const row = opts.status\n ? (this.db.prepare(sql).get(opts.status) as { n: number })\n : (this.db.prepare(sql).get() as { n: number });\n return row.n;\n }\n\n async getMostRecentProjectUpdatedAt(): Promise<number | null> {\n const row = this.db.prepare(\"SELECT updated_at FROM projects ORDER BY updated_at DESC LIMIT 1\").get() as\n | { updated_at: number }\n | undefined;\n return row?.updated_at ?? null;\n }\n\n async countChildProjects(parentId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM projects WHERE parent_id = ?\").get(parentId) as {\n n: number;\n }).n;\n }\n\n async findSiblingProjectsBySlugPattern(\n excludeProjectId: number,\n slug: string,\n limit = 3\n ): Promise<Array<{ slug: string; root_path: string; session_count: number }>> {\n return this.db\n .prepare(\n `SELECT p.slug, p.root_path, COUNT(s.id) AS session_count\n FROM projects p LEFT JOIN sessions s ON s.project_id = p.id\n WHERE p.id != ? AND (p.slug = ? OR p.slug LIKE ? || '-%' OR ? LIKE p.slug || '-%')\n GROUP BY p.id HAVING session_count > 0 ORDER BY session_count DESC LIMIT ?`\n )\n .all(excludeProjectId, slug, slug, slug, limit) as Array<{\n slug: string;\n root_path: string;\n session_count: number;\n }>;\n }\n\n // -------------------------------------------------------------------------\n // Projects — writes\n // -------------------------------------------------------------------------\n\n async createProject(input: NewProject): Promise<Project> {\n const type: ProjectType = input.type ?? \"local\";\n const status: ProjectStatus = input.status ?? \"active\";\n const result = this.db\n .prepare(\n `INSERT INTO projects (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`\n )\n .run(\n input.slug,\n input.displayName,\n input.rootPath,\n input.encodedDir,\n type,\n status,\n input.createdAt,\n input.updatedAt\n );\n return (await this.getProjectById(result.lastInsertRowid as number))!;\n }\n\n async createProjectWithSlugRetry(\n input: NewProjectWithSlugRetry\n ): Promise<{ id: number; slug: string; created: boolean }> {\n let slug = input.baseSlug;\n let attempt = 0;\n const insert = this.db.prepare(\n `INSERT OR IGNORE INTO projects (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)\n VALUES (?, ?, ?, ?, 'local', 'active', ?, ?)`\n );\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const info = insert.run(\n slug,\n input.displayName,\n input.rootPath,\n input.encodedDir,\n input.createdAt,\n input.updatedAt\n );\n if (info.changes > 0) {\n return { id: info.lastInsertRowid as number, slug, created: true };\n }\n\n const existing = this.db.prepare(\"SELECT id FROM projects WHERE root_path = ?\").get(input.rootPath) as\n | { id: number }\n | undefined;\n if (existing) {\n return { id: existing.id, slug, created: false };\n }\n\n attempt++;\n slug = `${input.baseSlug}-${attempt}`;\n }\n }\n\n async updateProjectPath(\n id: number,\n patch: { rootPath?: string; encodedDir?: string },\n updatedAt?: number\n ): Promise<void> {\n const sets: string[] = [];\n const params: unknown[] = [];\n if (patch.rootPath !== undefined) {\n sets.push(\"root_path = ?\");\n params.push(patch.rootPath);\n }\n if (patch.encodedDir !== undefined) {\n sets.push(\"encoded_dir = ?\");\n params.push(patch.encodedDir);\n }\n if (updatedAt !== undefined) {\n sets.push(\"updated_at = ?\");\n params.push(updatedAt);\n }\n if (!sets.length) return;\n params.push(id);\n this.db.prepare(`UPDATE projects SET ${sets.join(\", \")} WHERE id = ?`).run(...params);\n }\n\n async updateProjectStatus(\n id: number,\n status: ProjectStatus,\n opts: { archivedAt?: number | null; updatedAt?: number; requireCurrentStatus?: ProjectStatus } = {}\n ): Promise<void> {\n const sets: string[] = [\"status = ?\"];\n const params: unknown[] = [status];\n if (opts.archivedAt !== undefined) {\n sets.push(\"archived_at = ?\");\n params.push(opts.archivedAt);\n }\n if (opts.updatedAt !== undefined) {\n sets.push(\"updated_at = ?\");\n params.push(opts.updatedAt);\n }\n let sql = `UPDATE projects SET ${sets.join(\", \")} WHERE id = ?`;\n params.push(id);\n if (opts.requireCurrentStatus) {\n sql += \" AND status = ?\";\n params.push(opts.requireCurrentStatus);\n }\n this.db.prepare(sql).run(...params);\n }\n\n async updateProjectDisplayName(id: number, displayName: string, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n this.db\n .prepare(\"UPDATE projects SET display_name = ?, updated_at = ? WHERE id = ?\")\n .run(displayName, updatedAt, id);\n } else {\n this.db.prepare(\"UPDATE projects SET display_name = ? WHERE id = ?\").run(displayName, id);\n }\n }\n\n async updateProjectType(id: number, type: ProjectType, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n this.db.prepare(\"UPDATE projects SET type = ?, updated_at = ? WHERE id = ?\").run(type, updatedAt, id);\n } else {\n this.db.prepare(\"UPDATE projects SET type = ? WHERE id = ?\").run(type, id);\n }\n }\n\n async updateProjectSessionConfig(id: number, config: string | null, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n this.db\n .prepare(\"UPDATE projects SET session_config = ?, updated_at = ? WHERE id = ?\")\n .run(config, updatedAt, id);\n } else {\n this.db.prepare(\"UPDATE projects SET session_config = ? WHERE id = ?\").run(config, id);\n }\n }\n\n async updateProjectClaudeNotesDir(id: number, dir: string | null, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n this.db\n .prepare(\"UPDATE projects SET claude_notes_dir = ?, updated_at = ? WHERE id = ?\")\n .run(dir, updatedAt, id);\n } else {\n this.db.prepare(\"UPDATE projects SET claude_notes_dir = ? WHERE id = ?\").run(dir, id);\n }\n }\n\n async updateProjectObsidianLink(id: number, link: string | null, updatedAt?: number): Promise<void> {\n if (updatedAt !== undefined) {\n this.db\n .prepare(\"UPDATE projects SET obsidian_link = ?, updated_at = ? WHERE id = ?\")\n .run(link, updatedAt, id);\n } else {\n this.db.prepare(\"UPDATE projects SET obsidian_link = ? WHERE id = ?\").run(link, id);\n }\n }\n\n async updateProjectSlug(id: number, slug: string, updatedAt: number): Promise<void> {\n this.db.prepare(\"UPDATE projects SET slug = ?, updated_at = ? WHERE id = ?\").run(slug, updatedAt, id);\n }\n\n async reassignProjectParent(fromParentId: number, toParentId: number): Promise<number> {\n return this.db.prepare(\"UPDATE projects SET parent_id = ? WHERE parent_id = ?\").run(toParentId, fromParentId)\n .changes;\n }\n\n async deleteProject(id: number): Promise<void> {\n this.db.prepare(\"DELETE FROM projects WHERE id = ?\").run(id);\n }\n\n async deleteProjectCascade(id: number): Promise<void> {\n const run = this.db.transaction(() => {\n this.db.prepare(\"DELETE FROM links WHERE target_project_id = ?\").run(id);\n this.db\n .prepare(\"DELETE FROM links WHERE session_id IN (SELECT id FROM sessions WHERE project_id = ?)\")\n .run(id);\n this.db.prepare(\"DELETE FROM compaction_log WHERE project_id = ?\").run(id);\n this.db.prepare(\"DELETE FROM project_tags WHERE project_id = ?\").run(id);\n this.db.prepare(\"DELETE FROM aliases WHERE project_id = ?\").run(id);\n this.db.prepare(\"DELETE FROM sessions WHERE project_id = ?\").run(id);\n this.db.prepare(\"DELETE FROM projects WHERE id = ?\").run(id);\n });\n run();\n }\n\n // -------------------------------------------------------------------------\n // Tags\n // -------------------------------------------------------------------------\n\n async listTagsForProject(projectId: number): Promise<string[]> {\n const rows = this.db\n .prepare(\n \"SELECT t.name FROM tags t JOIN project_tags pt ON pt.tag_id = t.id WHERE pt.project_id = ? ORDER BY t.name\"\n )\n .all(projectId) as Array<{ name: string }>;\n return rows.map((r) => r.name);\n }\n\n async listAllTags(): Promise<Array<{ id: number; name: string }>> {\n return this.db.prepare(\"SELECT id, name FROM tags ORDER BY name\").all() as Array<{\n id: number;\n name: string;\n }>;\n }\n\n async upsertTag(name: string): Promise<number> {\n this.db.prepare(\"INSERT OR IGNORE INTO tags (name) VALUES (?)\").run(name);\n return (this.db.prepare(\"SELECT id FROM tags WHERE name = ?\").get(name) as { id: number }).id;\n }\n\n async addProjectTag(projectId: number, tagId: number): Promise<void> {\n this.db.prepare(\"INSERT INTO project_tags (project_id, tag_id) VALUES (?, ?)\").run(projectId, tagId);\n }\n\n async projectHasTag(projectId: number, tagId: number): Promise<boolean> {\n return !!this.db.prepare(\"SELECT 1 FROM project_tags WHERE project_id = ? AND tag_id = ?\").get(projectId, tagId);\n }\n\n async deleteProjectTag(projectId: number, tagId: number): Promise<void> {\n this.db.prepare(\"DELETE FROM project_tags WHERE project_id = ? AND tag_id = ?\").run(projectId, tagId);\n }\n\n async reassignProjectTag(fromProjectId: number, toProjectId: number, tagId: number): Promise<void> {\n this.db\n .prepare(\"UPDATE project_tags SET project_id = ? WHERE project_id = ? AND tag_id = ?\")\n .run(toProjectId, fromProjectId, tagId);\n }\n\n async listProjectTagIds(projectId: number): Promise<number[]> {\n const rows = this.db.prepare(\"SELECT tag_id FROM project_tags WHERE project_id = ?\").all(projectId) as Array<{\n tag_id: number;\n }>;\n return rows.map((r) => r.tag_id);\n }\n\n async countProjectTagsForProject(projectId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM project_tags WHERE project_id = ?\").get(projectId) as {\n n: number;\n }).n;\n }\n\n async copyProjectTags(fromProjectId: number, toProjectId: number): Promise<void> {\n this.db\n .prepare(\n \"INSERT OR IGNORE INTO project_tags (project_id, tag_id) SELECT ?, tag_id FROM project_tags WHERE project_id = ?\"\n )\n .run(toProjectId, fromProjectId);\n }\n\n async deleteProjectTagsForProject(projectId: number): Promise<void> {\n this.db.prepare(\"DELETE FROM project_tags WHERE project_id = ?\").run(projectId);\n }\n\n // -------------------------------------------------------------------------\n // Aliases\n // -------------------------------------------------------------------------\n\n async resolveAlias(alias: string): Promise<number | null> {\n const row = this.db.prepare(\"SELECT project_id FROM aliases WHERE alias = ?\").get(alias) as\n | { project_id: number }\n | undefined;\n return row?.project_id ?? null;\n }\n\n async listAliasesForProject(projectId: number): Promise<string[]> {\n const rows = this.db.prepare(\"SELECT alias FROM aliases WHERE project_id = ? ORDER BY alias\").all(\n projectId\n ) as Array<{ alias: string }>;\n return rows.map((r) => r.alias);\n }\n\n async addAlias(alias: string, projectId: number): Promise<void> {\n this.db.prepare(\"INSERT INTO aliases (alias, project_id) VALUES (?, ?)\").run(alias, projectId);\n }\n\n async removeAlias(alias: string): Promise<void> {\n this.db.prepare(\"DELETE FROM aliases WHERE alias = ?\").run(alias);\n }\n\n async countAliasesForProject(projectId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM aliases WHERE project_id = ?\").get(projectId) as {\n n: number;\n }).n;\n }\n\n async moveProjectAliases(fromProjectId: number, toProjectId: number): Promise<void> {\n this.db.prepare(\"UPDATE aliases SET project_id = ? WHERE project_id = ?\").run(toProjectId, fromProjectId);\n }\n\n async reassignAlias(alias: string, toProjectId: number): Promise<void> {\n this.db.prepare(\"UPDATE aliases SET project_id = ? WHERE alias = ?\").run(toProjectId, alias);\n }\n\n async listAliasMap(): Promise<Array<{ alias: string; slug: string; root_path: string }>> {\n return this.db\n .prepare(\n `SELECT a.alias AS alias, p.slug AS slug, p.root_path AS root_path\n FROM aliases a JOIN projects p ON p.id = a.project_id WHERE p.status != 'archived'`\n )\n .all() as Array<{ alias: string; slug: string; root_path: string }>;\n }\n\n // -------------------------------------------------------------------------\n // Sessions\n // -------------------------------------------------------------------------\n\n async getSessionById(id: number): Promise<Session | null> {\n const row = this.db.prepare(\"SELECT * FROM sessions WHERE id = ?\").get(id) as Session | undefined;\n return row ?? null;\n }\n\n async getSessionByNumber(projectId: number, number: number): Promise<Session | null> {\n const row = this.db.prepare(\"SELECT * FROM sessions WHERE project_id = ? AND number = ?\").get(\n projectId,\n number\n ) as Session | undefined;\n return row ?? null;\n }\n\n async getLatestSessionForProject(projectId: number): Promise<Session | null> {\n const row = this.db\n .prepare(\"SELECT * FROM sessions WHERE project_id = ? ORDER BY number DESC LIMIT 1\")\n .get(projectId) as Session | undefined;\n return row ?? null;\n }\n\n async getMaxSessionNumber(projectId: number): Promise<number> {\n const row = this.db.prepare(\"SELECT COALESCE(MAX(number), 0) AS n FROM sessions WHERE project_id = ?\").get(\n projectId\n ) as { n: number };\n return row.n;\n }\n\n async listSessionsForProject(\n projectId: number,\n opts: ListSessionsForProjectOptions = {}\n ): Promise<Session[]> {\n const orderBy = opts.orderBy === \"created_desc\" ? \"created_at DESC\" : \"number ASC\";\n let sql = `SELECT * FROM sessions WHERE project_id = ? ORDER BY ${orderBy}`;\n const params: unknown[] = [projectId];\n if (opts.limit !== undefined) {\n sql += \" LIMIT ?\";\n params.push(opts.limit);\n }\n return this.db.prepare(sql).all(...params) as Session[];\n }\n\n async listSessions(opts: ListSessionsOptions = {}): Promise<SessionWithProject[]> {\n const where: string[] = [];\n const params: unknown[] = [];\n if (opts.projectId !== undefined) {\n where.push(\"s.project_id = ?\");\n params.push(opts.projectId);\n }\n if (opts.status) {\n where.push(\"s.status = ?\");\n params.push(opts.status);\n }\n let sql = `\n SELECT s.*, p.slug AS project_slug, p.display_name AS project_name\n FROM sessions s JOIN projects p ON p.id = s.project_id\n `;\n if (where.length) sql += \" WHERE \" + where.join(\" AND \");\n sql += \" ORDER BY s.date DESC, s.number DESC\";\n const limit = opts.limit ?? 20;\n sql += ` LIMIT ${limit}`;\n return this.db.prepare(sql).all(...params) as SessionWithProject[];\n }\n\n async findSessionByFilename(projectId: number, filename: string): Promise<{ id: number } | null> {\n const row = this.db\n .prepare(\"SELECT id FROM sessions WHERE project_id = ? AND filename = ? LIMIT 1\")\n .get(projectId, filename) as { id: number } | undefined;\n return row ?? null;\n }\n\n async sessionNumberTaken(projectId: number, number: number): Promise<boolean> {\n return !!this.db.prepare(\"SELECT 1 FROM sessions WHERE project_id = ? AND number = ? LIMIT 1\").get(\n projectId,\n number\n );\n }\n\n async createSession(input: NewSession): Promise<Session> {\n const status = input.status ?? \"completed\";\n const result = this.db\n .prepare(\n `INSERT INTO sessions (project_id, number, date, slug, title, filename, status, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`\n )\n .run(input.projectId, input.number, input.date, input.slug, input.title, input.filename, status, input.createdAt);\n return (await this.getSessionById(result.lastInsertRowid as number))!;\n }\n\n async upsertSessionIfAbsent(input: NewSession): Promise<boolean> {\n const existing = this.db\n .prepare(\"SELECT id FROM sessions WHERE project_id = ? AND number = ?\")\n .get(input.projectId, input.number);\n if (existing) return false;\n\n const status = input.status ?? \"completed\";\n this.db\n .prepare(\n `INSERT INTO sessions (project_id, number, date, slug, title, filename, status, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`\n )\n .run(input.projectId, input.number, input.date, input.slug, input.title, input.filename, status, input.createdAt);\n return true;\n }\n\n async updateSessionMeta(\n id: number,\n patch: { slug?: string; title?: string; filename?: string }\n ): Promise<void> {\n const sets: string[] = [];\n const params: unknown[] = [];\n if (patch.slug !== undefined) {\n sets.push(\"slug = ?\");\n params.push(patch.slug);\n }\n if (patch.title !== undefined) {\n sets.push(\"title = ?\");\n params.push(patch.title);\n }\n if (patch.filename !== undefined) {\n sets.push(\"filename = ?\");\n params.push(patch.filename);\n }\n if (!sets.length) return;\n params.push(id);\n this.db.prepare(`UPDATE sessions SET ${sets.join(\", \")} WHERE id = ?`).run(...params);\n }\n\n async updateSessionNumber(id: number, number: number, opts: { filename?: string } = {}): Promise<void> {\n if (opts.filename !== undefined) {\n this.db.prepare(\"UPDATE sessions SET number = ?, filename = ? WHERE id = ?\").run(number, opts.filename, id);\n } else {\n this.db.prepare(\"UPDATE sessions SET number = ? WHERE id = ?\").run(number, id);\n }\n }\n\n async updateSessionFilename(id: number, filename: string): Promise<void> {\n this.db.prepare(\"UPDATE sessions SET filename = ? WHERE id = ?\").run(filename, id);\n }\n\n async updateSessionStatus(id: number, status: SessionStatus, opts: { closedAt?: number } = {}): Promise<void> {\n if (opts.closedAt !== undefined) {\n this.db.prepare(\"UPDATE sessions SET status = ?, closed_at = ? WHERE id = ?\").run(status, opts.closedAt, id);\n } else {\n this.db.prepare(\"UPDATE sessions SET status = ? WHERE id = ?\").run(status, id);\n }\n }\n\n async moveSessionToProject(id: number, projectId: number, opts: { number?: number } = {}): Promise<void> {\n if (opts.number !== undefined) {\n this.db.prepare(\"UPDATE sessions SET project_id = ?, number = ? WHERE id = ?\").run(\n projectId,\n opts.number,\n id\n );\n } else {\n this.db.prepare(\"UPDATE sessions SET project_id = ? WHERE id = ?\").run(projectId, id);\n }\n }\n\n async deleteSession(id: number): Promise<void> {\n this.db.prepare(\"DELETE FROM sessions WHERE id = ?\").run(id);\n }\n\n async backfillFoldedSession(keepId: number, dropId: number): Promise<void> {\n this.db\n .prepare(\n `UPDATE sessions SET\n claude_session_id = COALESCE(claude_session_id,\n (SELECT claude_session_id FROM sessions WHERE id = ?)),\n token_count = COALESCE(token_count,\n (SELECT token_count FROM sessions WHERE id = ?)),\n closed_at = COALESCE(closed_at,\n (SELECT closed_at FROM sessions WHERE id = ?)),\n status = CASE WHEN status = 'open'\n AND (SELECT status FROM sessions WHERE id = ?) != 'open'\n THEN (SELECT status FROM sessions WHERE id = ?)\n ELSE status END\n WHERE id = ?`\n )\n .run(dropId, dropId, dropId, dropId, dropId, keepId);\n }\n\n async countSessionsForProject(projectId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM sessions WHERE project_id = ?\").get(projectId) as {\n n: number;\n }).n;\n }\n\n async countSessions(): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM sessions\").get() as { n: number }).n;\n }\n\n async getMostRecentSessionDate(projectId: number): Promise<string | null> {\n const row = this.db\n .prepare(\"SELECT date FROM sessions WHERE project_id = ? ORDER BY date DESC LIMIT 1\")\n .get(projectId) as { date: string } | undefined;\n return row?.date ?? null;\n }\n\n async getMostRecentSessionCreatedAt(): Promise<number | null> {\n const row = this.db.prepare(\"SELECT created_at FROM sessions ORDER BY created_at DESC LIMIT 1\").get() as\n | { created_at: number }\n | undefined;\n return row?.created_at ?? null;\n }\n\n // -------------------------------------------------------------------------\n // Session tags\n // -------------------------------------------------------------------------\n\n async listSessionTagIds(sessionId: number): Promise<number[]> {\n const rows = this.db.prepare(\"SELECT tag_id FROM session_tags WHERE session_id = ?\").all(sessionId) as Array<{\n tag_id: number;\n }>;\n return rows.map((r) => r.tag_id);\n }\n\n async sessionHasTag(sessionId: number, tagId: number): Promise<boolean> {\n return !!this.db.prepare(\"SELECT 1 FROM session_tags WHERE session_id = ? AND tag_id = ?\").get(\n sessionId,\n tagId\n );\n }\n\n async addSessionTag(sessionId: number, tagId: number): Promise<void> {\n this.db.prepare(\"INSERT INTO session_tags (session_id, tag_id) VALUES (?, ?)\").run(sessionId, tagId);\n }\n\n async deleteSessionTag(sessionId: number, tagId: number): Promise<void> {\n this.db.prepare(\"DELETE FROM session_tags WHERE session_id = ? AND tag_id = ?\").run(sessionId, tagId);\n }\n\n async reassignSessionTag(fromSessionId: number, toSessionId: number, tagId: number): Promise<void> {\n this.db\n .prepare(\"UPDATE session_tags SET session_id = ? WHERE session_id = ? AND tag_id = ?\")\n .run(toSessionId, fromSessionId, tagId);\n }\n\n async listTagsForSession(sessionId: number): Promise<string[]> {\n const rows = this.db\n .prepare(\n \"SELECT t.name FROM tags t JOIN session_tags st ON st.tag_id = t.id WHERE st.session_id = ? ORDER BY t.name\"\n )\n .all(sessionId) as Array<{ name: string }>;\n return rows.map((r) => r.name);\n }\n\n // -------------------------------------------------------------------------\n // Links\n // -------------------------------------------------------------------------\n\n async addLink(input: {\n sessionId: number;\n targetProjectId: number;\n linkType: LinkType;\n createdAt: number;\n }): Promise<void> {\n this.db\n .prepare(\"INSERT INTO links (session_id, target_project_id, link_type, created_at) VALUES (?, ?, ?, ?)\")\n .run(input.sessionId, input.targetProjectId, input.linkType, input.createdAt);\n }\n\n async listLinksForSession(sessionId: number): Promise<Array<{ id: number; target_project_id: number }>> {\n return this.db.prepare(\"SELECT id, target_project_id FROM links WHERE session_id = ?\").all(\n sessionId\n ) as Array<{ id: number; target_project_id: number }>;\n }\n\n async listLinksForProject(projectId: number): Promise<Array<{ id: number; session_id: number }>> {\n return this.db.prepare(\"SELECT id, session_id FROM links WHERE target_project_id = ?\").all(\n projectId\n ) as Array<{ id: number; session_id: number }>;\n }\n\n async linkExists(sessionId: number, targetProjectId: number): Promise<boolean> {\n return !!this.db.prepare(\"SELECT 1 FROM links WHERE session_id = ? AND target_project_id = ? LIMIT 1\").get(\n sessionId,\n targetProjectId\n );\n }\n\n async deleteLink(id: number): Promise<void> {\n this.db.prepare(\"DELETE FROM links WHERE id = ?\").run(id);\n }\n\n async deleteLinksTargetingProject(projectId: number): Promise<void> {\n this.db.prepare(\"DELETE FROM links WHERE target_project_id = ?\").run(projectId);\n }\n\n async deleteLinksFromProjectSessions(projectId: number): Promise<void> {\n this.db\n .prepare(\"DELETE FROM links WHERE session_id IN (SELECT id FROM sessions WHERE project_id = ?)\")\n .run(projectId);\n }\n\n async retargetLink(id: number, targetProjectId: number): Promise<void> {\n this.db.prepare(\"UPDATE links SET target_project_id = ? WHERE id = ?\").run(targetProjectId, id);\n }\n\n async moveLinkToSession(id: number, sessionId: number): Promise<void> {\n this.db.prepare(\"UPDATE links SET session_id = ? WHERE id = ?\").run(sessionId, id);\n }\n\n async reassignLinksTarget(fromProjectId: number, toProjectId: number): Promise<void> {\n this.db\n .prepare(\"UPDATE OR IGNORE links SET target_project_id = ? WHERE target_project_id = ?\")\n .run(toProjectId, fromProjectId);\n }\n\n async countLinksForProject(projectId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM links WHERE target_project_id = ?\").get(projectId) as {\n n: number;\n }).n;\n }\n\n async deleteSelfLinksForProject(projectId: number): Promise<void> {\n this.db\n .prepare(\n `DELETE FROM links WHERE target_project_id = ?\n AND session_id IN (SELECT id FROM sessions WHERE project_id = ?)`\n )\n .run(projectId, projectId);\n }\n\n // -------------------------------------------------------------------------\n // Compaction log\n // -------------------------------------------------------------------------\n\n async appendCompactionLog(entry: NewCompactionLogEntry): Promise<void> {\n this.db\n .prepare(\n `INSERT INTO compaction_log (project_id, session_id, trigger, files_written, token_count, created_at)\n VALUES (?, ?, ?, ?, ?, ?)`\n )\n .run(entry.projectId, entry.sessionId, entry.trigger, entry.filesWritten, entry.tokenCount, entry.createdAt);\n }\n\n async countCompactionLogsForProject(projectId: number): Promise<number> {\n return (this.db.prepare(\"SELECT COUNT(*) AS n FROM compaction_log WHERE project_id = ?\").get(projectId) as {\n n: number;\n }).n;\n }\n\n async moveCompactionLogsByProject(fromProjectId: number, toProjectId: number): Promise<number> {\n return this.db\n .prepare(\"UPDATE compaction_log SET project_id = ? WHERE project_id = ?\")\n .run(toProjectId, fromProjectId).changes;\n }\n\n async moveCompactionLogsBySession(fromSessionId: number, toSessionId: number): Promise<void> {\n this.db\n .prepare(\"UPDATE compaction_log SET session_id = ? WHERE session_id = ?\")\n .run(toSessionId, fromSessionId);\n }\n\n async deleteCompactionLogsForProject(projectId: number): Promise<void> {\n this.db.prepare(\"DELETE FROM compaction_log WHERE project_id = ?\").run(projectId);\n }\n\n // -------------------------------------------------------------------------\n // Project merge\n // -------------------------------------------------------------------------\n\n async planProjectMerge(fromSlug: string, intoSlug: string): Promise<MergePlan> {\n return planMerge(this.db, fromSlug, intoSlug);\n }\n\n async applyProjectMerge(plan: MergePlan): Promise<void> {\n applyMerge(this.db, plan);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { t as routeNotification } from "./router-a5G7q8_3.mjs";
2
+
3
+ export { routeNotification };
@@ -247,4 +247,4 @@ async function routeNotification(payload, config) {
247
247
 
248
248
  //#endregion
249
249
  export { routeNotification as t };
250
- //# sourceMappingURL=router-aqLMvNMg.mjs.map
250
+ //# sourceMappingURL=router-a5G7q8_3.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"router-aqLMvNMg.mjs","names":[],"sources":["../src/notifications/providers/ntfy.ts","../src/notifications/providers/whatsapp.ts","../src/notifications/providers/macos.ts","../src/notifications/providers/cli.ts","../src/notifications/router.ts"],"sourcesContent":["/**\n * ntfy.ts — ntfy.sh notification provider\n *\n * Sends notifications to a configured ntfy.sh topic via HTTP.\n */\n\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class NtfyProvider implements NotificationProvider {\n readonly channelId = \"ntfy\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.ntfy;\n if (!cfg.enabled || !cfg.url) return false;\n\n try {\n const headers: Record<string, string> = {\n \"Content-Type\": \"text/plain; charset=utf-8\",\n };\n\n if (payload.title) {\n headers[\"Title\"] = payload.title;\n }\n\n if (cfg.priority && cfg.priority !== \"default\") {\n headers[\"Priority\"] = cfg.priority;\n }\n\n const response = await fetch(cfg.url, {\n method: \"POST\",\n headers,\n body: payload.message,\n });\n\n return response.ok;\n } catch {\n return false;\n }\n }\n}\n","/**\n * whatsapp.ts — WhatsApp notification provider (via the AIBroker hub)\n *\n * Sends through AIBroker's hub socket, which proxies to the whazaa adapter.\n *\n * It used to dial /tmp/whazaa.sock and call a method named `whatsapp_send`.\n * Both were wrong, and had been since AIBroker became the runtime hub and\n * adapters became thin transports: Whazaa no longer owns an IPC socket of its\n * own (it registers with the hub, currently at /tmp/whazaa-watcher.sock, which\n * is the hub's business and not ours), and `whatsapp_send` is an MCP TOOL name\n * — the adapter itself takes `send` and `tts`.\n *\n * So every WhatsApp notification failed. Silently, because a failed channel\n * writes one line to stderr and the router has no fallback: on 2026-08-04 four\n * task-bus escalations about a job that had not run for nine hours reached\n * /tmp/pai-scheduler.log and nowhere else, while the user had no idea.\n *\n * Routing through the hub rather than at the adapter directly is deliberate:\n * the hub knows where its adapters are, and that is exactly the knowledge whose\n * absence broke this. Connect-per-call, no persistent state.\n */\n\nimport { connect } from \"node:net\";\nimport { randomUUID } from \"node:crypto\";\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\n/**\n * AIBroker's hub socket.\n *\n * Hardcoded rather than imported: PAI must not depend on AIBroker. This is a\n * protocol constant between them, like the agent mark in tasks/poller.ts.\n */\nconst HUB_SOCKET = \"/tmp/aibroker.sock\";\nconst HUB_TIMEOUT_MS = 10_000;\n\n/**\n * Send a single IPC call to the hub.\n * Returns true on success, false if the hub is not available or errors.\n */\nfunction callHub(\n method: string,\n params: Record<string, unknown>\n): Promise<boolean> {\n return new Promise((resolve) => {\n let done = false;\n let buffer = \"\";\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function finish(ok: boolean): void {\n if (done) return;\n done = true;\n if (timer) { clearTimeout(timer); timer = null; }\n try { socket?.destroy(); } catch { /* ignore */ }\n resolve(ok);\n }\n\n const socket = connect(HUB_SOCKET, () => {\n const request = {\n jsonrpc: \"2.0\",\n id: randomUUID(),\n method,\n params,\n };\n socket.write(JSON.stringify(request) + \"\\n\");\n });\n\n socket.on(\"data\", (chunk: Buffer) => {\n buffer += chunk.toString();\n const nl = buffer.indexOf(\"\\n\");\n if (nl === -1) return;\n try {\n const resp = JSON.parse(buffer.slice(0, nl)) as { error?: unknown; ok?: boolean };\n // The hub reports failure as {ok:false,error}; JSON-RPC reports {error}.\n // Checking only `error` counted an {ok:false} reply as a delivered\n // notification, which is the one mistake this file must never make.\n finish(!resp.error && resp.ok !== false);\n } catch {\n finish(false);\n }\n });\n\n socket.on(\"error\", () => finish(false));\n socket.on(\"end\", () => finish(false));\n\n timer = setTimeout(() => finish(false), HUB_TIMEOUT_MS);\n });\n}\n\nexport class WhatsAppProvider implements NotificationProvider {\n readonly channelId = \"whatsapp\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.whatsapp;\n if (!cfg.enabled) return false;\n\n const isVoiceMode = config.mode === \"voice\" || config.channels.voice.enabled;\n const asVoice = isVoiceMode && config.mode === \"voice\";\n\n // The adapter's own vocabulary is `send` and `tts`, reached through the\n // hub's `adapter_call`. `whatsapp_send` — what this used to ask for — is the\n // name of the MCP TOOL that wraps it, and the adapter has never answered to\n // it. The two vocabularies are easy to confuse because the MCP tool exists\n // and works; it just is not this interface.\n const inner: Record<string, unknown> = asVoice\n ? { text: payload.message, voice: config.channels.voice.voiceName ?? \"bm_george\" }\n : { message: payload.message };\n\n if (cfg.recipient) {\n // `send` addresses by `recipient`, `tts` by `jid` — same destination,\n // different key, per the adapter's interface.\n inner[asVoice ? \"jid\" : \"recipient\"] = cfg.recipient;\n }\n\n return callHub(\"adapter_call\", {\n adapter: \"whazaa\",\n method: asVoice ? \"tts\" : \"send\",\n params: inner,\n });\n }\n}\n","/**\n * macos.ts — macOS notification provider\n *\n * Uses the `osascript` command to display a macOS system notification.\n * Non-blocking: spawns the process and returns success without waiting.\n */\n\nimport { spawn } from \"node:child_process\";\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class MacOsProvider implements NotificationProvider {\n readonly channelId = \"macos\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.macos;\n if (!cfg.enabled) return false;\n if (process.platform !== \"darwin\") return false;\n\n try {\n const title = payload.title ?? \"PAI\";\n // Escape single quotes in title and message for AppleScript\n const safeTitle = title.replace(/'/g, \"\\\\'\");\n const safeMessage = payload.message.replace(/'/g, \"\\\\'\");\n\n const script = `display notification \"${safeMessage}\" with title \"${safeTitle}\"`;\n\n return new Promise((resolve) => {\n const child = spawn(\"osascript\", [\"-e\", script], {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n\n // Give the process a moment to start, then assume success.\n // osascript is always present on macOS.\n child.on(\"error\", () => resolve(false));\n\n // Resolve after a short timeout — osascript exits quickly\n setTimeout(() => resolve(true), 200);\n });\n } catch {\n return false;\n }\n }\n}\n","/**\n * cli.ts — CLI notification provider\n *\n * Writes notifications to the PAI daemon log (stderr).\n * Always succeeds — it's the fallback channel.\n */\n\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class CliProvider implements NotificationProvider {\n readonly channelId = \"cli\" as const;\n\n async send(\n payload: NotificationPayload,\n _config: NotificationConfig\n ): Promise<boolean> {\n const prefix = `[pai-notify:${payload.event}]`;\n const title = payload.title ? ` ${payload.title}:` : \"\";\n process.stderr.write(`${prefix}${title} ${payload.message}\\n`);\n return true;\n }\n}\n","/**\n * router.ts — Notification router\n *\n * Routes notification events to the appropriate channels based on the\n * current mode and per-event routing config.\n *\n * Channel providers are instantiated lazily and cached.\n */\n\nimport type {\n NotificationPayload,\n NotificationConfig,\n NotificationProvider,\n ChannelId,\n SendResult,\n NotificationMode,\n} from \"./types.js\";\nimport { NtfyProvider } from \"./providers/ntfy.js\";\nimport { WhatsAppProvider } from \"./providers/whatsapp.js\";\nimport { MacOsProvider } from \"./providers/macos.js\";\nimport { CliProvider } from \"./providers/cli.js\";\n\n// ---------------------------------------------------------------------------\n// Provider registry (singletons — stateless, safe to reuse)\n// ---------------------------------------------------------------------------\n\nconst PROVIDERS: Record<ChannelId, NotificationProvider> = {\n ntfy: new NtfyProvider(),\n whatsapp: new WhatsAppProvider(),\n macos: new MacOsProvider(),\n voice: new WhatsAppProvider(), // Voice uses WhatsApp TTS; handled in WhatsAppProvider\n cli: new CliProvider(),\n};\n\n// ---------------------------------------------------------------------------\n// Channel resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Given the current config, resolve which channels should receive a\n * notification for the given event type.\n *\n * Mode overrides:\n * \"off\" → no channels\n * \"auto\" → use routing table, filtered by enabled channels\n * \"voice\" → whatsapp (TTS enabled in provider)\n * \"whatsapp\" → whatsapp\n * \"ntfy\" → ntfy\n * \"macos\" → macos\n * \"cli\" → cli\n */\nfunction resolveChannels(\n config: NotificationConfig,\n event: NotificationPayload[\"event\"]\n): ChannelId[] {\n const { mode, channels, routing } = config;\n\n if (mode === \"off\") return [];\n\n // Non-auto modes: force a single channel\n const modeToChannel: Partial<Record<NotificationMode, ChannelId>> = {\n voice: \"whatsapp\", // WhatsAppProvider checks mode === \"voice\" for TTS\n whatsapp: \"whatsapp\",\n ntfy: \"ntfy\",\n macos: \"macos\",\n cli: \"cli\",\n };\n\n if (mode !== \"auto\") {\n const ch = modeToChannel[mode];\n if (!ch) return [];\n // Check the channel is enabled\n const cfg = channels[ch];\n if (cfg && !cfg.enabled) return [ch]; // Still send — mode override bypasses enabled check\n return [ch];\n }\n\n // Auto mode: use routing table, filter to enabled channels\n const candidates = routing[event] ?? [];\n return candidates.filter((ch) => {\n const cfg = channels[ch];\n // \"voice\" channel is virtual — it overlaps with whatsapp.\n // Skip \"voice\" as an independent channel; voice is handled by checking config.mode.\n if (ch === \"voice\") return false;\n return cfg?.enabled === true;\n });\n}\n\n// ---------------------------------------------------------------------------\n// Router\n// ---------------------------------------------------------------------------\n\n/**\n * Route a notification to the appropriate channels.\n *\n * Sends to all resolved channels in parallel.\n * Individual channel failures are non-fatal and logged to stderr.\n *\n * @param payload The notification to send\n * @param config The current notification config (from daemon state)\n */\nexport async function routeNotification(\n payload: NotificationPayload,\n config: NotificationConfig\n): Promise<SendResult> {\n const channels = resolveChannels(config, payload.event);\n\n if (channels.length === 0) {\n return {\n channelsAttempted: [],\n channelsSucceeded: [],\n channelsFailed: [],\n mode: config.mode,\n };\n }\n\n const results = await Promise.allSettled(\n channels.map(async (ch) => {\n const provider = PROVIDERS[ch];\n const ok = await provider.send(payload, config);\n if (!ok) {\n process.stderr.write(\n `[pai-notify] Channel ${ch} failed for event ${payload.event}\\n`\n );\n }\n return { ch, ok };\n })\n );\n\n const succeeded: ChannelId[] = [];\n const failed: ChannelId[] = [];\n\n for (const r of results) {\n if (r.status === \"fulfilled\") {\n if (r.value.ok) {\n succeeded.push(r.value.ch);\n } else {\n failed.push(r.value.ch);\n }\n } else {\n // Provider threw — treat as failure\n failed.push(channels[results.indexOf(r)]);\n }\n }\n\n return {\n channelsAttempted: channels,\n channelsSucceeded: succeeded,\n channelsFailed: failed,\n mode: config.mode,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"router-a5G7q8_3.mjs","names":[],"sources":["../src/notifications/providers/ntfy.ts","../src/notifications/providers/whatsapp.ts","../src/notifications/providers/macos.ts","../src/notifications/providers/cli.ts","../src/notifications/router.ts"],"sourcesContent":["/**\n * ntfy.ts — ntfy.sh notification provider\n *\n * Sends notifications to a configured ntfy.sh topic via HTTP.\n */\n\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class NtfyProvider implements NotificationProvider {\n readonly channelId = \"ntfy\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.ntfy;\n if (!cfg.enabled || !cfg.url) return false;\n\n try {\n const headers: Record<string, string> = {\n \"Content-Type\": \"text/plain; charset=utf-8\",\n };\n\n if (payload.title) {\n headers[\"Title\"] = payload.title;\n }\n\n if (cfg.priority && cfg.priority !== \"default\") {\n headers[\"Priority\"] = cfg.priority;\n }\n\n const response = await fetch(cfg.url, {\n method: \"POST\",\n headers,\n body: payload.message,\n });\n\n return response.ok;\n } catch {\n return false;\n }\n }\n}\n","/**\n * whatsapp.ts — WhatsApp notification provider (via the AIBroker hub)\n *\n * Sends through AIBroker's hub socket, which proxies to the whazaa adapter.\n *\n * It used to dial /tmp/whazaa.sock and call a method named `whatsapp_send`.\n * Both were wrong, and had been since AIBroker became the runtime hub and\n * adapters became thin transports: Whazaa no longer owns an IPC socket of its\n * own (it registers with the hub, currently at /tmp/whazaa-watcher.sock, which\n * is the hub's business and not ours), and `whatsapp_send` is an MCP TOOL name\n * — the adapter itself takes `send` and `tts`.\n *\n * So every WhatsApp notification failed. Silently, because a failed channel\n * writes one line to stderr and the router has no fallback: on 2026-08-04 four\n * task-bus escalations about a job that had not run for nine hours reached\n * /tmp/pai-scheduler.log and nowhere else, while the user had no idea.\n *\n * Routing through the hub rather than at the adapter directly is deliberate:\n * the hub knows where its adapters are, and that is exactly the knowledge whose\n * absence broke this. Connect-per-call, no persistent state.\n */\n\nimport { connect } from \"node:net\";\nimport { randomUUID } from \"node:crypto\";\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\n/**\n * AIBroker's hub socket.\n *\n * Hardcoded rather than imported: PAI must not depend on AIBroker. This is a\n * protocol constant between them, like the agent mark in tasks/poller.ts.\n */\nconst HUB_SOCKET = \"/tmp/aibroker.sock\";\nconst HUB_TIMEOUT_MS = 10_000;\n\n/**\n * Send a single IPC call to the hub.\n * Returns true on success, false if the hub is not available or errors.\n */\nfunction callHub(\n method: string,\n params: Record<string, unknown>\n): Promise<boolean> {\n return new Promise((resolve) => {\n let done = false;\n let buffer = \"\";\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function finish(ok: boolean): void {\n if (done) return;\n done = true;\n if (timer) { clearTimeout(timer); timer = null; }\n try { socket?.destroy(); } catch { /* ignore */ }\n resolve(ok);\n }\n\n const socket = connect(HUB_SOCKET, () => {\n const request = {\n jsonrpc: \"2.0\",\n id: randomUUID(),\n method,\n params,\n };\n socket.write(JSON.stringify(request) + \"\\n\");\n });\n\n socket.on(\"data\", (chunk: Buffer) => {\n buffer += chunk.toString();\n const nl = buffer.indexOf(\"\\n\");\n if (nl === -1) return;\n try {\n const resp = JSON.parse(buffer.slice(0, nl)) as { error?: unknown; ok?: boolean };\n // The hub reports failure as {ok:false,error}; JSON-RPC reports {error}.\n // Checking only `error` counted an {ok:false} reply as a delivered\n // notification, which is the one mistake this file must never make.\n finish(!resp.error && resp.ok !== false);\n } catch {\n finish(false);\n }\n });\n\n socket.on(\"error\", () => finish(false));\n socket.on(\"end\", () => finish(false));\n\n timer = setTimeout(() => finish(false), HUB_TIMEOUT_MS);\n });\n}\n\nexport class WhatsAppProvider implements NotificationProvider {\n readonly channelId = \"whatsapp\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.whatsapp;\n if (!cfg.enabled) return false;\n\n const isVoiceMode = config.mode === \"voice\" || config.channels.voice.enabled;\n const asVoice = isVoiceMode && config.mode === \"voice\";\n\n // The adapter's own vocabulary is `send` and `tts`, reached through the\n // hub's `adapter_call`. `whatsapp_send` — what this used to ask for — is the\n // name of the MCP TOOL that wraps it, and the adapter has never answered to\n // it. The two vocabularies are easy to confuse because the MCP tool exists\n // and works; it just is not this interface.\n const inner: Record<string, unknown> = asVoice\n ? { text: payload.message, voice: config.channels.voice.voiceName ?? \"bm_george\" }\n : { message: payload.message };\n\n if (cfg.recipient) {\n // `send` addresses by `recipient`, `tts` by `jid` — same destination,\n // different key, per the adapter's interface.\n inner[asVoice ? \"jid\" : \"recipient\"] = cfg.recipient;\n }\n\n return callHub(\"adapter_call\", {\n adapter: \"whazaa\",\n method: asVoice ? \"tts\" : \"send\",\n params: inner,\n });\n }\n}\n","/**\n * macos.ts — macOS notification provider\n *\n * Uses the `osascript` command to display a macOS system notification.\n * Non-blocking: spawns the process and returns success without waiting.\n */\n\nimport { spawn } from \"node:child_process\";\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class MacOsProvider implements NotificationProvider {\n readonly channelId = \"macos\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.macos;\n if (!cfg.enabled) return false;\n if (process.platform !== \"darwin\") return false;\n\n try {\n const title = payload.title ?? \"PAI\";\n // Escape single quotes in title and message for AppleScript\n const safeTitle = title.replace(/'/g, \"\\\\'\");\n const safeMessage = payload.message.replace(/'/g, \"\\\\'\");\n\n const script = `display notification \"${safeMessage}\" with title \"${safeTitle}\"`;\n\n return new Promise((resolve) => {\n const child = spawn(\"osascript\", [\"-e\", script], {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n\n // Give the process a moment to start, then assume success.\n // osascript is always present on macOS.\n child.on(\"error\", () => resolve(false));\n\n // Resolve after a short timeout — osascript exits quickly\n setTimeout(() => resolve(true), 200);\n });\n } catch {\n return false;\n }\n }\n}\n","/**\n * cli.ts — CLI notification provider\n *\n * Writes notifications to the PAI daemon log (stderr).\n * Always succeeds — it's the fallback channel.\n */\n\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class CliProvider implements NotificationProvider {\n readonly channelId = \"cli\" as const;\n\n async send(\n payload: NotificationPayload,\n _config: NotificationConfig\n ): Promise<boolean> {\n const prefix = `[pai-notify:${payload.event}]`;\n const title = payload.title ? ` ${payload.title}:` : \"\";\n process.stderr.write(`${prefix}${title} ${payload.message}\\n`);\n return true;\n }\n}\n","/**\n * router.ts — Notification router\n *\n * Routes notification events to the appropriate channels based on the\n * current mode and per-event routing config.\n *\n * Channel providers are instantiated lazily and cached.\n */\n\nimport type {\n NotificationPayload,\n NotificationConfig,\n NotificationProvider,\n ChannelId,\n SendResult,\n NotificationMode,\n} from \"./types.js\";\nimport { NtfyProvider } from \"./providers/ntfy.js\";\nimport { WhatsAppProvider } from \"./providers/whatsapp.js\";\nimport { MacOsProvider } from \"./providers/macos.js\";\nimport { CliProvider } from \"./providers/cli.js\";\n\n// ---------------------------------------------------------------------------\n// Provider registry (singletons — stateless, safe to reuse)\n// ---------------------------------------------------------------------------\n\nconst PROVIDERS: Record<ChannelId, NotificationProvider> = {\n ntfy: new NtfyProvider(),\n whatsapp: new WhatsAppProvider(),\n macos: new MacOsProvider(),\n voice: new WhatsAppProvider(), // Voice uses WhatsApp TTS; handled in WhatsAppProvider\n cli: new CliProvider(),\n};\n\n// ---------------------------------------------------------------------------\n// Channel resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Given the current config, resolve which channels should receive a\n * notification for the given event type.\n *\n * Mode overrides:\n * \"off\" → no channels\n * \"auto\" → use routing table, filtered by enabled channels\n * \"voice\" → whatsapp (TTS enabled in provider)\n * \"whatsapp\" → whatsapp\n * \"ntfy\" → ntfy\n * \"macos\" → macos\n * \"cli\" → cli\n */\nfunction resolveChannels(\n config: NotificationConfig,\n event: NotificationPayload[\"event\"]\n): ChannelId[] {\n const { mode, channels, routing } = config;\n\n if (mode === \"off\") return [];\n\n // Non-auto modes: force a single channel\n const modeToChannel: Partial<Record<NotificationMode, ChannelId>> = {\n voice: \"whatsapp\", // WhatsAppProvider checks mode === \"voice\" for TTS\n whatsapp: \"whatsapp\",\n ntfy: \"ntfy\",\n macos: \"macos\",\n cli: \"cli\",\n };\n\n if (mode !== \"auto\") {\n const ch = modeToChannel[mode];\n if (!ch) return [];\n // Check the channel is enabled\n const cfg = channels[ch];\n if (cfg && !cfg.enabled) return [ch]; // Still send — mode override bypasses enabled check\n return [ch];\n }\n\n // Auto mode: use routing table, filter to enabled channels\n const candidates = routing[event] ?? [];\n return candidates.filter((ch) => {\n const cfg = channels[ch];\n // \"voice\" channel is virtual — it overlaps with whatsapp.\n // Skip \"voice\" as an independent channel; voice is handled by checking config.mode.\n if (ch === \"voice\") return false;\n return cfg?.enabled === true;\n });\n}\n\n// ---------------------------------------------------------------------------\n// Router\n// ---------------------------------------------------------------------------\n\n/**\n * Route a notification to the appropriate channels.\n *\n * Sends to all resolved channels in parallel.\n * Individual channel failures are non-fatal and logged to stderr.\n *\n * @param payload The notification to send\n * @param config The current notification config (from daemon state)\n */\nexport async function routeNotification(\n payload: NotificationPayload,\n config: NotificationConfig\n): Promise<SendResult> {\n const channels = resolveChannels(config, payload.event);\n\n if (channels.length === 0) {\n return {\n channelsAttempted: [],\n channelsSucceeded: [],\n channelsFailed: [],\n mode: config.mode,\n };\n }\n\n const results = await Promise.allSettled(\n channels.map(async (ch) => {\n const provider = PROVIDERS[ch];\n const ok = await provider.send(payload, config);\n if (!ok) {\n process.stderr.write(\n `[pai-notify] Channel ${ch} failed for event ${payload.event}\\n`\n );\n }\n return { ch, ok };\n })\n );\n\n const succeeded: ChannelId[] = [];\n const failed: ChannelId[] = [];\n\n for (const r of results) {\n if (r.status === \"fulfilled\") {\n if (r.value.ok) {\n succeeded.push(r.value.ch);\n } else {\n failed.push(r.value.ch);\n }\n } else {\n // Provider threw — treat as failure\n failed.push(channels[results.indexOf(r)]);\n }\n }\n\n return {\n channelsAttempted: channels,\n channelsSucceeded: succeeded,\n channelsFailed: failed,\n mode: config.mode,\n };\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { i as getRegistryBackend } from "./factory-y36qGegI.mjs";
1
+ import { i as getRegistryBackend } from "./factory-CNpPQQ-5.mjs";
2
2
  import { E as readWorkersSection, O as resolveModelCapability, T as providerKeyPath, b as isModelCapability, d as NATIVE_ANTHROPIC_MODELS, g as classModelCapability, h as assertProviderRunnable, k as resolveProviderKey, m as WorkersConfigError, n as claudeCommand, t as buildRunEnv } from "./run-env-BHWnXqle.mjs";
3
3
  import { _ as statusPath, d as resolveTarget, f as setCooldown, g as panesDir, h as ledgerPath, i as capabilityForRun, m as eventsPath, n as ensureProxyRunning, o as isQuotaFailure, p as ensureNoMcpConfig, s as nextAutoProvider, t as DEFAULT_PROXY_PORT, u as resolveCapabilityRun, v as workersLogDir } from "./server-DlL1QI2k.mjs";
4
4
  import { appendFileSync, closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
@@ -3523,7 +3523,7 @@ async function runWorker(opts) {
3523
3523
  if (gc?.length) process.stderr.write(`pai worker: archived ${gc.length} leftover worktree(s) (pai worker gc)\n`);
3524
3524
  } catch {}
3525
3525
  if (opts.className === "plan" && !opts._planner) {
3526
- const { runPlanner } = await import("./planner-DAq4Yx-H.mjs");
3526
+ const { runPlanner } = await import("./planner-Dzs9-8AS.mjs");
3527
3527
  return runPlanner(opts);
3528
3528
  }
3529
3529
  const parent = launchParent(opts.parent);
@@ -4691,4 +4691,4 @@ function resultFromOutput(out) {
4691
4691
 
4692
4692
  //#endregion
4693
4693
  export { expandMcpNames as $, ledgerSummary as A, loadStatuses as B, discardWorker as C, uncommittedPaths as D, mergeWorker as E, contextPercent as F, setWorkerLabel as G, nowStamp as H, elapsedOf as I, parseRunnerArgs as J, waitForTerminalStatus as K, isChatPane as L, ageOf as M, alive as N, worktreesDir as O, contextLabel as P, describeMcp as Q, isLive as R, workerInScope as S, git as T, ownsPid as U, newWorkerId as V, saveStatus as W, pinNotices as X, shortText as Y, projectLaunchConfig as Z, itermForClaudeSession as _, parseWorkerReport as a, sessionMapPath as b, readInbox as c, checkPaneForWorker as d, openFollowPane as f, currentTabKey as g, markControlsHeld as h, usageContextTokens as i, UNLABELED as j, appendLedger as k, sayToWorker as l, clickrControlsArgv as m, runWorker as n, renderReport as o, openPaneForWorker as p, longInlinePromptHint as q, testProvider as r, handoffFromInside as s, printResult as t, workerDepth as u, itermUuid as v, gcWorktrees as w, sessionTag as x, resolveSession as y, loadStatus as z };
4694
- //# sourceMappingURL=run-uAkNItb6.mjs.map
4694
+ //# sourceMappingURL=run-DMaCpvMW.mjs.map