@tekmidian/pai 0.66.0 → 0.66.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 (165) hide show
  1. package/dist/{aibroker-client-B8c42Lh8.mjs → aibroker-client-CHEEvJZW.mjs} +2 -2
  2. package/dist/{aibroker-client-C5Fw7DNz.mjs → aibroker-client-Dfv7j1Od.mjs} +3 -3
  3. package/dist/{aibroker-client-C5Fw7DNz.mjs.map → aibroker-client-Dfv7j1Od.mjs.map} +1 -1
  4. package/dist/{auto-route-o0BOsXn7.mjs → auto-route-BnizyALK.mjs} +65 -4
  5. package/dist/auto-route-BnizyALK.mjs.map +1 -0
  6. package/dist/auto-route-D6fW1Q8z.mjs +3 -0
  7. package/dist/{chain-D4PuQ2c-.mjs → chain-D5876UQX.mjs} +3 -4
  8. package/dist/{chain-D4PuQ2c-.mjs.map → chain-D5876UQX.mjs.map} +1 -1
  9. package/dist/cli/index.mjs +18 -23
  10. package/dist/cli/index.mjs.map +1 -1
  11. package/dist/cli/program.mjs +17 -22
  12. package/dist/{config-BbLFD7Uf.mjs → config-C-LiGlop.mjs} +2 -2
  13. package/dist/{config-BbLFD7Uf.mjs.map → config-C-LiGlop.mjs.map} +1 -1
  14. package/dist/{config-YinjgXEJ.mjs → config-DKIA7wgF.mjs} +1 -1
  15. package/dist/{context-handover-cache-pkHzmL3c.mjs → context-handover-cache-9PGvXRIw.mjs} +984 -10
  16. package/dist/context-handover-cache-9PGvXRIw.mjs.map +1 -0
  17. package/dist/daemon/index.mjs +17 -19
  18. package/dist/daemon/index.mjs.map +1 -1
  19. package/dist/daemon-BUgNvMUh.mjs +5029 -0
  20. package/dist/daemon-BUgNvMUh.mjs.map +1 -0
  21. package/dist/daemon-DL0eglMJ.mjs +18 -0
  22. package/dist/daemon-mcp/index.mjs +15 -10
  23. package/dist/daemon-mcp/index.mjs.map +1 -1
  24. package/dist/{embeddings-CcscYWwk.mjs → embeddings-CEBGrzwu.mjs} +1 -1
  25. package/dist/{embeddings-Bx3q0QOY.mjs → embeddings-DOLZnT1X.mjs} +1 -1
  26. package/dist/{embeddings-Bx3q0QOY.mjs.map → embeddings-DOLZnT1X.mjs.map} +1 -1
  27. package/dist/{env-JNEIrQWg.mjs → env-DiolswKQ.mjs} +1 -1
  28. package/dist/{env-JNEIrQWg.mjs.map → env-DiolswKQ.mjs.map} +1 -1
  29. package/dist/factory-DrBg24UT.mjs +8 -0
  30. package/dist/factory-Ypf8r0fK.mjs +2892 -0
  31. package/dist/factory-Ypf8r0fK.mjs.map +1 -0
  32. package/dist/{fallback-B5svk75f.mjs → fallback-D51R4shY.mjs} +27 -13
  33. package/dist/fallback-D51R4shY.mjs.map +1 -0
  34. package/dist/{chunker-BH4i-F2b.mjs → helpers-CZsi_49C.mjs} +221 -2
  35. package/dist/helpers-CZsi_49C.mjs.map +1 -0
  36. package/dist/hooks/block-sleep-poll.mjs.map +2 -2
  37. package/dist/hooks/load-project-context.mjs.map +2 -2
  38. package/dist/hooks/worker-guard.mjs +5 -0
  39. package/dist/hooks/worker-guard.mjs.map +2 -2
  40. package/dist/hooks/worker-status-line.mjs +1 -0
  41. package/dist/hooks/worker-status-line.mjs.map +2 -2
  42. package/dist/index.mjs +6 -7
  43. package/dist/{ipc-client-D16Xw6Uo.mjs → ipc-client-BLYX51nG.mjs} +2 -2
  44. package/dist/{ipc-client-D16Xw6Uo.mjs.map → ipc-client-BLYX51nG.mjs.map} +1 -1
  45. package/dist/main-resolver-BYgP86C1.mjs +7 -0
  46. package/dist/{main-resolver-DPPwHNtn.mjs → main-resolver-kVkMJZUD.mjs} +6 -6
  47. package/dist/{main-resolver-DPPwHNtn.mjs.map → main-resolver-kVkMJZUD.mjs.map} +1 -1
  48. package/dist/{migrate-BD7D8EEh.mjs → migrate-Bq0esMOa.mjs} +2 -2
  49. package/dist/{migrate-BD7D8EEh.mjs.map → migrate-Bq0esMOa.mjs.map} +1 -1
  50. package/dist/{pai-marker-D1MMswkz.mjs → pai-marker-DXVpFsYz.mjs} +1 -1
  51. package/dist/{pai-marker-D1MMswkz.mjs.map → pai-marker-DXVpFsYz.mjs.map} +1 -1
  52. package/dist/{planner-EJPZnnif.mjs → planner-D5yt0EfA.mjs} +10 -7
  53. package/dist/{planner-EJPZnnif.mjs.map → planner-D5yt0EfA.mjs.map} +1 -1
  54. package/dist/postgres-BmLr0MUm.mjs +5 -0
  55. package/dist/{postgres-Ceqsa64C.mjs → postgres-D3xc2RB4.mjs} +364 -9
  56. package/dist/postgres-D3xc2RB4.mjs.map +1 -0
  57. package/dist/{program-DSETzrWg.mjs → program-DIXRtAFy.mjs} +80 -70
  58. package/dist/program-DIXRtAFy.mjs.map +1 -0
  59. package/dist/{query-feedback-BIaZTTFO.mjs → query-feedback-B7FYE4JR.mjs} +1 -1
  60. package/dist/{query-feedback-BIaZTTFO.mjs.map → query-feedback-B7FYE4JR.mjs.map} +1 -1
  61. package/dist/{reranker-DKv80KO5.mjs → reranker-3lnggwgq.mjs} +1 -1
  62. package/dist/{reranker-DKv80KO5.mjs.map → reranker-3lnggwgq.mjs.map} +1 -1
  63. package/dist/{reranker-CFiEzHQu.mjs → reranker-CZ2mP4cf.mjs} +1 -1
  64. package/dist/router-BK-hFeQ6.mjs +3 -0
  65. package/dist/{router-a5G7q8_3.mjs → router-BaTbc9VX.mjs} +2 -2
  66. package/dist/{router-a5G7q8_3.mjs.map → router-BaTbc9VX.mjs.map} +1 -1
  67. package/dist/{run-Z4akoa8G.mjs → run-CLYTOm0x.mjs} +764 -25
  68. package/dist/run-CLYTOm0x.mjs.map +1 -0
  69. package/dist/{run-env-DRcp7A8K.mjs → run-env-Bnmf6bRI.mjs} +2 -2
  70. package/dist/{run-env-DRcp7A8K.mjs.map → run-env-Bnmf6bRI.mjs.map} +1 -1
  71. package/dist/{runtime-paths-CHTg3ywb.mjs → runtime-paths-D3FKAm69.mjs} +1 -1
  72. package/dist/{runtime-paths-CHTg3ywb.mjs.map → runtime-paths-D3FKAm69.mjs.map} +1 -1
  73. package/dist/{search-BOSphCJ1.mjs → search-CNAGTiJP.mjs} +2 -2
  74. package/dist/{search-BOSphCJ1.mjs.map → search-CNAGTiJP.mjs.map} +1 -1
  75. package/dist/{sources-CijVso3n.mjs → sources-CM2g-CLT.mjs} +2 -2
  76. package/dist/{sources-CijVso3n.mjs.map → sources-CM2g-CLT.mjs.map} +1 -1
  77. package/dist/{stop-words-BdQuaE9K.mjs → stop-words-DtxaTWU_.mjs} +1 -1
  78. package/dist/{stop-words-BdQuaE9K.mjs.map → stop-words-DtxaTWU_.mjs.map} +1 -1
  79. package/dist/{utils-C9HsYDpO.mjs → utils-DddRwMsG.mjs} +2 -2
  80. package/dist/{utils-C9HsYDpO.mjs.map → utils-DddRwMsG.mjs.map} +1 -1
  81. package/dist/{zettelkasten-CtGHQWPU.mjs → zettelkasten-BhZMvmoK.mjs} +149 -7
  82. package/dist/zettelkasten-BhZMvmoK.mjs.map +1 -0
  83. package/dist/zettelkasten-BwV1pluL.mjs +5 -0
  84. package/docs/commands/worker.md +2 -0
  85. package/package.json +1 -1
  86. package/src/hooks/ts/lib/worker-guard.test.ts +12 -0
  87. package/src/hooks/ts/lib/worker-guard.ts +9 -0
  88. package/dist/async-C2Bm_Lal.mjs +0 -300
  89. package/dist/async-C2Bm_Lal.mjs.map +0 -1
  90. package/dist/auto-route-o0BOsXn7.mjs.map +0 -1
  91. package/dist/chunker-BH4i-F2b.mjs.map +0 -1
  92. package/dist/clusters-BCtD3fbe.mjs +0 -169
  93. package/dist/clusters-BCtD3fbe.mjs.map +0 -1
  94. package/dist/context-handover-cache-pkHzmL3c.mjs.map +0 -1
  95. package/dist/daemon-BEoVgVfJ.mjs +0 -1741
  96. package/dist/daemon-BEoVgVfJ.mjs.map +0 -1
  97. package/dist/daemon-DPAYU9hp.mjs +0 -19
  98. package/dist/detector-BEPFsINR.mjs +0 -3
  99. package/dist/detector-BRTtAWrM.mjs +0 -65
  100. package/dist/detector-BRTtAWrM.mjs.map +0 -1
  101. package/dist/factory-CNpPQQ-5.mjs +0 -249
  102. package/dist/factory-CNpPQQ-5.mjs.map +0 -1
  103. package/dist/factory-DY7x73mE.mjs +0 -3
  104. package/dist/fallback-B5svk75f.mjs.map +0 -1
  105. package/dist/federation-db-BTyoufBh.mjs +0 -139
  106. package/dist/federation-db-BTyoufBh.mjs.map +0 -1
  107. package/dist/federation-db-HfIFc7FG.mjs +0 -3
  108. package/dist/helpers-BRCJg0G3.mjs +0 -223
  109. package/dist/helpers-BRCJg0G3.mjs.map +0 -1
  110. package/dist/indexer-backend-DFF2FrYx.mjs +0 -5
  111. package/dist/indexer-backend-isSLg6yE.mjs +0 -1
  112. package/dist/kg-entity-DCOcsVFD.mjs +0 -29
  113. package/dist/kg-entity-DCOcsVFD.mjs.map +0 -1
  114. package/dist/latent-ideas-wwAXquGe.mjs +0 -191
  115. package/dist/latent-ideas-wwAXquGe.mjs.map +0 -1
  116. package/dist/link-boost-CnI7UVMJ.mjs +0 -34
  117. package/dist/link-boost-CnI7UVMJ.mjs.map +0 -1
  118. package/dist/main-resolver-BKz_OuEh.mjs +0 -7
  119. package/dist/merge-2gqRPFu2.mjs +0 -3
  120. package/dist/merge-DgU9OgZy.mjs +0 -6
  121. package/dist/merge-DgU9OgZy.mjs.map +0 -1
  122. package/dist/module-paths-DdRzbkUI.mjs +0 -44
  123. package/dist/module-paths-DdRzbkUI.mjs.map +0 -1
  124. package/dist/neighborhood-2FsmzoxG.mjs +0 -114
  125. package/dist/neighborhood-2FsmzoxG.mjs.map +0 -1
  126. package/dist/note-context-De63k8na.mjs +0 -106
  127. package/dist/note-context-De63k8na.mjs.map +0 -1
  128. package/dist/postgres-Ceqsa64C.mjs.map +0 -1
  129. package/dist/program-DSETzrWg.mjs.map +0 -1
  130. package/dist/query-feedback-B_iigYj-.mjs +0 -3
  131. package/dist/registry-db-C7voqML9.mjs +0 -213
  132. package/dist/registry-db-C7voqML9.mjs.map +0 -1
  133. package/dist/registry-db-JHPhA8vF.mjs +0 -3
  134. package/dist/registry-postgres-nOfyBSIW.mjs +0 -795
  135. package/dist/registry-postgres-nOfyBSIW.mjs.map +0 -1
  136. package/dist/registry-sqlite-DrCW1aRK.mjs +0 -593
  137. package/dist/registry-sqlite-DrCW1aRK.mjs.map +0 -1
  138. package/dist/router-CXUGsv85.mjs +0 -3
  139. package/dist/run-Z4akoa8G.mjs.map +0 -1
  140. package/dist/search-Bf3Kub3F.mjs +0 -3
  141. package/dist/server-DgmAHyFK.mjs +0 -403
  142. package/dist/server-DgmAHyFK.mjs.map +0 -1
  143. package/dist/session-keepalive-BDhytXEo.mjs +0 -582
  144. package/dist/session-keepalive-BDhytXEo.mjs.map +0 -1
  145. package/dist/sqlite-CqsTy6xo.mjs +0 -936
  146. package/dist/sqlite-CqsTy6xo.mjs.map +0 -1
  147. package/dist/state-DW8zdweW.mjs +0 -3
  148. package/dist/state-HyjqTihC.mjs +0 -76
  149. package/dist/state-HyjqTihC.mjs.map +0 -1
  150. package/dist/themes-CTaOj3e1.mjs +0 -148
  151. package/dist/themes-CTaOj3e1.mjs.map +0 -1
  152. package/dist/tools-BcOKPBtg.mjs +0 -1489
  153. package/dist/tools-BcOKPBtg.mjs.map +0 -1
  154. package/dist/tools-C-_l0o8Z.mjs +0 -6
  155. package/dist/trace-CtV0RO6n.mjs +0 -137
  156. package/dist/trace-CtV0RO6n.mjs.map +0 -1
  157. package/dist/utils-BrX9FLeK.mjs +0 -3
  158. package/dist/vault-indexer-BG1sEVyM.mjs +0 -537
  159. package/dist/vault-indexer-BG1sEVyM.mjs.map +0 -1
  160. package/dist/wakeup-D8n33hYV.mjs +0 -335
  161. package/dist/wakeup-D8n33hYV.mjs.map +0 -1
  162. package/dist/work-queue-worker-4tLa5gpQ.mjs +0 -567
  163. package/dist/work-queue-worker-4tLa5gpQ.mjs.map +0 -1
  164. package/dist/work-queue-worker-rFZvTO3p.mjs +0 -11
  165. package/dist/zettelkasten-CtGHQWPU.mjs.map +0 -1
@@ -47,4 +47,4 @@ function schedulerLogPath() {
47
47
 
48
48
  //#endregion
49
49
  export { schedulerLogPath as a, paiSocketPath as i, daemonLogPath as n, daemonPidPath as r, aibrokerSocketPath as t };
50
- //# sourceMappingURL=runtime-paths-CHTg3ywb.mjs.map
50
+ //# sourceMappingURL=runtime-paths-D3FKAm69.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-paths-CHTg3ywb.mjs","names":[],"sources":["../src/runtime-paths.ts"],"sourcesContent":["/**\n * runtime-paths.ts — the shared files PAI talks to the running daemon through.\n *\n * Sockets, logs and pidfiles live at fixed absolute paths under /tmp, and every\n * one of them is shared with a *live* daemon. That makes them the same hazard\n * as any other piece of real user state a test can reach: writing one from a\n * test does not fail, it corrupts something already running, silently.\n *\n * The home guard in test/setup-home-guard.ts does not cover them. It works by\n * redirecting HOME, so it protects paths *derived* from the home directory and\n * nothing else — a hardcoded \"/tmp/pai.sock\" walks straight past it. Verified\n * on 2026-08-01 by snapshotting all five and running the suite: nothing writes\n * to them today. But \"no test does this yet\" is not a property, and the next\n * daemon test to be written would clobber the live socket with nothing to stop\n * it.\n *\n * So each path is read from an environment variable with the historical /tmp\n * value as its default. Behaviour is unchanged for every real invocation; the\n * test setup overrides the variables and the whole class becomes unreachable\n * rather than merely absent, the same way the home guard works.\n *\n * Read at call time, not at module load: the test setup file runs before any\n * module is imported, but resolving these eagerly would still bake in whatever\n * the environment held at import and make the override order matter.\n */\n\n/** IPC socket for the PAI daemon. */\nexport function paiSocketPath(): string {\n return process.env.PAI_SOCKET_PATH ?? \"/tmp/pai.sock\";\n}\n\n/** IPC socket for the AIBroker daemon — read by PAI, owned by AIBroker. */\nexport function aibrokerSocketPath(): string {\n return process.env.PAI_AIBROKER_SOCKET_PATH ?? \"/tmp/aibroker.sock\";\n}\n\n/** Where the daemon's stdout and stderr are collected. */\nexport function daemonLogPath(): string {\n return process.env.PAI_DAEMON_LOG_PATH ?? \"/tmp/pai-daemon.log\";\n}\n\n/** Pidfile for the running daemon. */\nexport function daemonPidPath(): string {\n return process.env.PAI_DAEMON_PID_PATH ?? \"/tmp/pai-daemon.pid\";\n}\n\n/** Where the launchd scheduler tick writes its output. */\nexport function schedulerLogPath(): string {\n return process.env.PAI_SCHEDULER_LOG_PATH ?? \"/tmp/pai-scheduler.log\";\n}\n\n/**\n * Where the status line caches what it may reuse between renders: the\n * per-provider plan-usage snapshots and the Claude Code version. Shared with\n * every live session's status line, so a test writing here would be feeding\n * fixtures to the real thing. Read by statusline-command.sh as PAI_CACHE_DIR.\n */\nexport function paiCacheDir(): string {\n return process.env.PAI_CACHE_DIR ?? \"/tmp/claude\";\n}\n\n/**\n * Every runtime path, for the test guard to redirect in one place.\n *\n * Listed here rather than in the guard so that adding a path and forgetting to\n * protect it is not possible: the guard iterates this.\n */\nexport const RUNTIME_PATH_ENV_VARS = [\n \"PAI_SOCKET_PATH\",\n \"PAI_AIBROKER_SOCKET_PATH\",\n \"PAI_DAEMON_LOG_PATH\",\n \"PAI_DAEMON_PID_PATH\",\n \"PAI_SCHEDULER_LOG_PATH\",\n \"PAI_CACHE_DIR\",\n] as const;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,gBAAwB;AACtC,QAAO,QAAQ,IAAI,mBAAmB;;;AAIxC,SAAgB,qBAA6B;AAC3C,QAAO,QAAQ,IAAI,4BAA4B;;;AAIjD,SAAgB,gBAAwB;AACtC,QAAO,QAAQ,IAAI,uBAAuB;;;AAI5C,SAAgB,gBAAwB;AACtC,QAAO,QAAQ,IAAI,uBAAuB;;;AAI5C,SAAgB,mBAA2B;AACzC,QAAO,QAAQ,IAAI,0BAA0B"}
1
+ {"version":3,"file":"runtime-paths-D3FKAm69.mjs","names":[],"sources":["../src/runtime-paths.ts"],"sourcesContent":["/**\n * runtime-paths.ts — the shared files PAI talks to the running daemon through.\n *\n * Sockets, logs and pidfiles live at fixed absolute paths under /tmp, and every\n * one of them is shared with a *live* daemon. That makes them the same hazard\n * as any other piece of real user state a test can reach: writing one from a\n * test does not fail, it corrupts something already running, silently.\n *\n * The home guard in test/setup-home-guard.ts does not cover them. It works by\n * redirecting HOME, so it protects paths *derived* from the home directory and\n * nothing else — a hardcoded \"/tmp/pai.sock\" walks straight past it. Verified\n * on 2026-08-01 by snapshotting all five and running the suite: nothing writes\n * to them today. But \"no test does this yet\" is not a property, and the next\n * daemon test to be written would clobber the live socket with nothing to stop\n * it.\n *\n * So each path is read from an environment variable with the historical /tmp\n * value as its default. Behaviour is unchanged for every real invocation; the\n * test setup overrides the variables and the whole class becomes unreachable\n * rather than merely absent, the same way the home guard works.\n *\n * Read at call time, not at module load: the test setup file runs before any\n * module is imported, but resolving these eagerly would still bake in whatever\n * the environment held at import and make the override order matter.\n */\n\n/** IPC socket for the PAI daemon. */\nexport function paiSocketPath(): string {\n return process.env.PAI_SOCKET_PATH ?? \"/tmp/pai.sock\";\n}\n\n/** IPC socket for the AIBroker daemon — read by PAI, owned by AIBroker. */\nexport function aibrokerSocketPath(): string {\n return process.env.PAI_AIBROKER_SOCKET_PATH ?? \"/tmp/aibroker.sock\";\n}\n\n/** Where the daemon's stdout and stderr are collected. */\nexport function daemonLogPath(): string {\n return process.env.PAI_DAEMON_LOG_PATH ?? \"/tmp/pai-daemon.log\";\n}\n\n/** Pidfile for the running daemon. */\nexport function daemonPidPath(): string {\n return process.env.PAI_DAEMON_PID_PATH ?? \"/tmp/pai-daemon.pid\";\n}\n\n/** Where the launchd scheduler tick writes its output. */\nexport function schedulerLogPath(): string {\n return process.env.PAI_SCHEDULER_LOG_PATH ?? \"/tmp/pai-scheduler.log\";\n}\n\n/**\n * Where the status line caches what it may reuse between renders: the\n * per-provider plan-usage snapshots and the Claude Code version. Shared with\n * every live session's status line, so a test writing here would be feeding\n * fixtures to the real thing. Read by statusline-command.sh as PAI_CACHE_DIR.\n */\nexport function paiCacheDir(): string {\n return process.env.PAI_CACHE_DIR ?? \"/tmp/claude\";\n}\n\n/**\n * Every runtime path, for the test guard to redirect in one place.\n *\n * Listed here rather than in the guard so that adding a path and forgetting to\n * protect it is not possible: the guard iterates this.\n */\nexport const RUNTIME_PATH_ENV_VARS = [\n \"PAI_SOCKET_PATH\",\n \"PAI_AIBROKER_SOCKET_PATH\",\n \"PAI_DAEMON_LOG_PATH\",\n \"PAI_DAEMON_PID_PATH\",\n \"PAI_SCHEDULER_LOG_PATH\",\n \"PAI_CACHE_DIR\",\n] as const;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,gBAAwB;AACtC,QAAO,QAAQ,IAAI,mBAAmB;;;AAIxC,SAAgB,qBAA6B;AAC3C,QAAO,QAAQ,IAAI,4BAA4B;;;AAIjD,SAAgB,gBAAwB;AACtC,QAAO,QAAQ,IAAI,uBAAuB;;;AAI5C,SAAgB,gBAAwB;AACtC,QAAO,QAAQ,IAAI,uBAAuB;;;AAI5C,SAAgB,mBAA2B;AACzC,QAAO,QAAQ,IAAI,0BAA0B"}
@@ -1,4 +1,4 @@
1
- import { t as STOP_WORDS } from "./stop-words-BdQuaE9K.mjs";
1
+ import { t as STOP_WORDS } from "./stop-words-DtxaTWU_.mjs";
2
2
 
3
3
  //#region src/memory/search.ts
4
4
  /**
@@ -104,4 +104,4 @@ function applyRecencyBoost(results, halfLifeDays = 90) {
104
104
 
105
105
  //#endregion
106
106
  export { populateSlugs as i, buildFtsQuery as n, isQuerySyntaxError as r, applyRecencyBoost as t };
107
- //# sourceMappingURL=search-BOSphCJ1.mjs.map
107
+ //# sourceMappingURL=search-CNAGTiJP.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"search-BOSphCJ1.mjs","names":[],"sources":["../src/memory/search.ts"],"sourcesContent":["/**\n * Query building and result post-processing for the PAI federation memory\n * index — everything here is pure logic (no SQL). The raw FTS5/vector\n * queries live under src/storage/{sqlite,postgres}/search.ts, which call\n * back into buildFtsQuery()/isQuerySyntaxError() from this module.\n */\n\nimport { STOP_WORDS } from \"../utils/stop-words.js\";\nimport type { RegistryBackend } from \"../storage/registry-interface.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SearchResult {\n projectId: number;\n projectSlug?: string; // populated from registry after search when available\n path: string;\n startLine: number;\n endLine: number;\n snippet: string;\n score: number; // raw BM25 score (lower = more relevant in FTS5)\n tier: string;\n source: string;\n updatedAt?: number; // Unix ms from memory_chunks.updated_at\n lastAccessedAt?: number; // Unix ms from memory_chunks.last_accessed_at (QW2)\n chunkId?: string; // chunk ID for last_accessed_at update (QW2)\n}\n\nexport interface SearchOptions {\n /** Restrict search to these project IDs. */\n projectIds?: number[];\n /** Restrict to 'memory' or 'notes' sources. */\n sources?: string[];\n /** Restrict to specific tier(s): 'evergreen' | 'daily' | 'topic' | 'session' */\n tiers?: string[];\n /** Maximum number of results to return. Default 10. */\n maxResults?: number;\n /** Minimum BM25 score threshold (FTS5 scores are negative; 0.0 means no filter). */\n minScore?: number;\n}\n\n// STOP_WORDS imported from utils/stop-words.ts\n\n// ---------------------------------------------------------------------------\n// Query builder\n// ---------------------------------------------------------------------------\n\n/**\n * Convert a free-text query into an FTS5 query string.\n *\n * Strategy:\n * 1. Tokenise by whitespace and punctuation\n * 2. Remove stop words and tokens shorter than 2 characters\n * 3. Double-quote each remaining token (exact word form)\n * 4. Join with OR so that any matching token returns a result\n *\n * Using OR instead of AND is critical for multi-word queries: the words rarely\n * all appear in the same chunk, so AND would return zero results. FTS5 BM25\n * scoring naturally ranks chunks where more terms match higher, so the most\n * relevant chunks still surface at the top.\n *\n * Example: \"Synchrotech interview follow-up Gilles\"\n * → `\"synchrotech\" OR \"interview\" OR \"follow\" OR \"gilles\"`\n * → chunks matching any term, ranked by how many terms match\n */\n/**\n * Did SQLite reject the QUERY, or fail at the STORE?\n *\n * Only the first justifies an empty result. FTS5 reports a bad MATCH expression\n * with a recognisable message; a missing table, a corrupt index or a locked\n * database do not, and must not be reported as \"nothing found\".\n *\n * Matching on message text is unlovely, and the alternative — treating every\n * failure as empty — is what produced a confidently wrong answer to a human.\n */\nexport function isQuerySyntaxError(e: unknown): boolean {\n const msg = e instanceof Error ? e.message : String(e);\n return /fts5|malformed MATCH|syntax error|unterminated string|no such column/i.test(msg);\n}\n\nexport function buildFtsQuery(query: string): string {\n const tokens = query\n .toLowerCase()\n .split(/[\\s\\p{P}]+/u)\n .filter(Boolean)\n .filter((t) => t.length >= 2)\n .filter((t) => !STOP_WORDS.has(t))\n // Escape any double-quotes inside the token (FTS5 uses them as delimiters)\n .map((t) => `\"${t.replace(/\"/g, '\"\"')}\"`)\n\n if (tokens.length === 0) {\n // Fallback: use original query as a raw string (may produce no results)\n return `\"${query.replace(/\"/g, '\"\"')}\"`;\n }\n\n return tokens.join(\" OR \");\n}\n\n// The raw searchMemory()/searchMemorySemantic()/searchMemoryHybrid()/\n// touchChunksLastAccessed() implementations live in storage/sqlite/search.ts\n// (SQLite) and storage/postgres/search.ts + backend.ts (Postgres).\n\n// ---------------------------------------------------------------------------\n// Slug lookup helper\n// ---------------------------------------------------------------------------\n\n/**\n * Populate the projectSlug field on search results by looking up project IDs\n * in the registry database.\n */\nexport async function populateSlugs(\n results: SearchResult[],\n registry: RegistryBackend,\n): Promise<SearchResult[]> {\n if (results.length === 0) return results;\n\n const ids = [...new Set(results.map((r) => r.projectId))];\n // No batch \"projects by ids\" method on RegistryBackend yet (flagged in the\n // report) — loops getProjectById, bounded by the distinct project count in\n // an already-capped result set.\n const slugMap = new Map<number, string>();\n for (const id of ids) {\n const project = await registry.getProjectById(id);\n if (project) slugMap.set(id, project.slug);\n }\n\n return results.map((r) => ({\n ...r,\n projectSlug: slugMap.get(r.projectId),\n }));\n}\n\n// ---------------------------------------------------------------------------\n// Recency boost\n// ---------------------------------------------------------------------------\n\n/**\n * Apply exponential recency boost to search scores.\n *\n * Scores are first min-max normalized to [0,1], then multiplied by an\n * exponential decay factor based on chunk age. Normalization is required\n * because the cross-encoder reranker produces negative logit scores — naive\n * multiplication of a negative score by a decay factor (0 < d ≤ 1) would\n * make the score *less* negative, effectively boosting old results instead\n * of penalizing them.\n *\n * Formula: score_final = normalized * exp(-lambda * age_days)\n * where lambda = ln(2) / halfLifeDays, normalized ∈ [0,1]\n *\n * With default halfLifeDays=90, a 3-month-old chunk retains 50% of its\n * normalized score, a 6-month-old retains 25%, and a 1-year-old ~6%.\n *\n * Results without an updatedAt timestamp receive no decay penalty.\n * Results are re-sorted by the boosted score after application.\n *\n * @param results Search results with optional updatedAt timestamps.\n * @param halfLifeDays Score halves every N days. Default 90 (~3 months).\n * @returns New array sorted by decayed normalized score (descending).\n */\nexport function applyRecencyBoost(\n results: SearchResult[],\n halfLifeDays = 90,\n): SearchResult[] {\n if (halfLifeDays <= 0 || results.length === 0) return results;\n\n const lambda = Math.LN2 / halfLifeDays;\n const now = Date.now();\n\n // Min-max normalize scores to [0,1] so multiplicative decay works\n // correctly regardless of the raw score sign/scale.\n const scores = results.map((r) => r.score);\n const minScore = Math.min(...scores);\n const maxScore = Math.max(...scores);\n const range = maxScore - minScore;\n\n return results\n .map((r) => {\n const normalized = range === 0 ? 1 : (r.score - minScore) / range;\n // QW2: use the more recent of updated_at and last_accessed_at for recency decay\n const effectiveTs = r.updatedAt != null && r.lastAccessedAt != null\n ? Math.max(r.updatedAt, r.lastAccessedAt)\n : (r.lastAccessedAt ?? r.updatedAt);\n const decay = effectiveTs\n ? Math.exp(-lambda * Math.max(0, (now - effectiveTs) / 86_400_000))\n : 1; // no timestamp → no penalty\n return { ...r, score: normalized * decay };\n })\n .sort((a, b) => b.score - a.score);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4EA,SAAgB,mBAAmB,GAAqB;CACtD,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,QAAO,wEAAwE,KAAK,IAAI;;AAG1F,SAAgB,cAAc,OAAuB;CACnD,MAAM,SAAS,MACZ,aAAa,CACb,MAAM,cAAc,CACpB,OAAO,QAAQ,CACf,QAAQ,MAAM,EAAE,UAAU,EAAE,CAC5B,QAAQ,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC,CAEjC,KAAK,MAAM,IAAI,EAAE,QAAQ,MAAM,OAAK,CAAC,GAAG;AAE3C,KAAI,OAAO,WAAW,EAEpB,QAAO,IAAI,MAAM,QAAQ,MAAM,OAAK,CAAC;AAGvC,QAAO,OAAO,KAAK,OAAO;;;;;;AAe5B,eAAsB,cACpB,SACA,UACyB;AACzB,KAAI,QAAQ,WAAW,EAAG,QAAO;CAEjC,MAAM,MAAM,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAK,MAAM,EAAE,UAAU,CAAC,CAAC;CAIzD,MAAM,0BAAU,IAAI,KAAqB;AACzC,MAAK,MAAM,MAAM,KAAK;EACpB,MAAM,UAAU,MAAM,SAAS,eAAe,GAAG;AACjD,MAAI,QAAS,SAAQ,IAAI,IAAI,QAAQ,KAAK;;AAG5C,QAAO,QAAQ,KAAK,OAAO;EACzB,GAAG;EACH,aAAa,QAAQ,IAAI,EAAE,UAAU;EACtC,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;AA8BL,SAAgB,kBACd,SACA,eAAe,IACC;AAChB,KAAI,gBAAgB,KAAK,QAAQ,WAAW,EAAG,QAAO;CAEtD,MAAM,SAAS,KAAK,MAAM;CAC1B,MAAM,MAAM,KAAK,KAAK;CAItB,MAAM,SAAS,QAAQ,KAAK,MAAM,EAAE,MAAM;CAC1C,MAAM,WAAW,KAAK,IAAI,GAAG,OAAO;CAEpC,MAAM,QADW,KAAK,IAAI,GAAG,OAAO,GACX;AAEzB,QAAO,QACJ,KAAK,MAAM;EACV,MAAM,aAAa,UAAU,IAAI,KAAK,EAAE,QAAQ,YAAY;EAE5D,MAAM,cAAc,EAAE,aAAa,QAAQ,EAAE,kBAAkB,OAC3D,KAAK,IAAI,EAAE,WAAW,EAAE,eAAe,GACtC,EAAE,kBAAkB,EAAE;EAC3B,MAAM,QAAQ,cACV,KAAK,IAAI,CAAC,SAAS,KAAK,IAAI,IAAI,MAAM,eAAe,MAAW,CAAC,GACjE;AACJ,SAAO;GAAE,GAAG;GAAG,OAAO,aAAa;GAAO;GAC1C,CACD,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM"}
1
+ {"version":3,"file":"search-CNAGTiJP.mjs","names":[],"sources":["../src/memory/search.ts"],"sourcesContent":["/**\n * Query building and result post-processing for the PAI federation memory\n * index — everything here is pure logic (no SQL). The raw FTS5/vector\n * queries live under src/storage/{sqlite,postgres}/search.ts, which call\n * back into buildFtsQuery()/isQuerySyntaxError() from this module.\n */\n\nimport { STOP_WORDS } from \"../utils/stop-words.js\";\nimport type { RegistryBackend } from \"../storage/registry-interface.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SearchResult {\n projectId: number;\n projectSlug?: string; // populated from registry after search when available\n path: string;\n startLine: number;\n endLine: number;\n snippet: string;\n score: number; // raw BM25 score (lower = more relevant in FTS5)\n tier: string;\n source: string;\n updatedAt?: number; // Unix ms from memory_chunks.updated_at\n lastAccessedAt?: number; // Unix ms from memory_chunks.last_accessed_at (QW2)\n chunkId?: string; // chunk ID for last_accessed_at update (QW2)\n}\n\nexport interface SearchOptions {\n /** Restrict search to these project IDs. */\n projectIds?: number[];\n /** Restrict to 'memory' or 'notes' sources. */\n sources?: string[];\n /** Restrict to specific tier(s): 'evergreen' | 'daily' | 'topic' | 'session' */\n tiers?: string[];\n /** Maximum number of results to return. Default 10. */\n maxResults?: number;\n /** Minimum BM25 score threshold (FTS5 scores are negative; 0.0 means no filter). */\n minScore?: number;\n}\n\n// STOP_WORDS imported from utils/stop-words.ts\n\n// ---------------------------------------------------------------------------\n// Query builder\n// ---------------------------------------------------------------------------\n\n/**\n * Convert a free-text query into an FTS5 query string.\n *\n * Strategy:\n * 1. Tokenise by whitespace and punctuation\n * 2. Remove stop words and tokens shorter than 2 characters\n * 3. Double-quote each remaining token (exact word form)\n * 4. Join with OR so that any matching token returns a result\n *\n * Using OR instead of AND is critical for multi-word queries: the words rarely\n * all appear in the same chunk, so AND would return zero results. FTS5 BM25\n * scoring naturally ranks chunks where more terms match higher, so the most\n * relevant chunks still surface at the top.\n *\n * Example: \"Synchrotech interview follow-up Gilles\"\n * → `\"synchrotech\" OR \"interview\" OR \"follow\" OR \"gilles\"`\n * → chunks matching any term, ranked by how many terms match\n */\n/**\n * Did SQLite reject the QUERY, or fail at the STORE?\n *\n * Only the first justifies an empty result. FTS5 reports a bad MATCH expression\n * with a recognisable message; a missing table, a corrupt index or a locked\n * database do not, and must not be reported as \"nothing found\".\n *\n * Matching on message text is unlovely, and the alternative — treating every\n * failure as empty — is what produced a confidently wrong answer to a human.\n */\nexport function isQuerySyntaxError(e: unknown): boolean {\n const msg = e instanceof Error ? e.message : String(e);\n return /fts5|malformed MATCH|syntax error|unterminated string|no such column/i.test(msg);\n}\n\nexport function buildFtsQuery(query: string): string {\n const tokens = query\n .toLowerCase()\n .split(/[\\s\\p{P}]+/u)\n .filter(Boolean)\n .filter((t) => t.length >= 2)\n .filter((t) => !STOP_WORDS.has(t))\n // Escape any double-quotes inside the token (FTS5 uses them as delimiters)\n .map((t) => `\"${t.replace(/\"/g, '\"\"')}\"`)\n\n if (tokens.length === 0) {\n // Fallback: use original query as a raw string (may produce no results)\n return `\"${query.replace(/\"/g, '\"\"')}\"`;\n }\n\n return tokens.join(\" OR \");\n}\n\n// The raw searchMemory()/searchMemorySemantic()/searchMemoryHybrid()/\n// touchChunksLastAccessed() implementations live in storage/sqlite/search.ts\n// (SQLite) and storage/postgres/search.ts + backend.ts (Postgres).\n\n// ---------------------------------------------------------------------------\n// Slug lookup helper\n// ---------------------------------------------------------------------------\n\n/**\n * Populate the projectSlug field on search results by looking up project IDs\n * in the registry database.\n */\nexport async function populateSlugs(\n results: SearchResult[],\n registry: RegistryBackend,\n): Promise<SearchResult[]> {\n if (results.length === 0) return results;\n\n const ids = [...new Set(results.map((r) => r.projectId))];\n // No batch \"projects by ids\" method on RegistryBackend yet (flagged in the\n // report) — loops getProjectById, bounded by the distinct project count in\n // an already-capped result set.\n const slugMap = new Map<number, string>();\n for (const id of ids) {\n const project = await registry.getProjectById(id);\n if (project) slugMap.set(id, project.slug);\n }\n\n return results.map((r) => ({\n ...r,\n projectSlug: slugMap.get(r.projectId),\n }));\n}\n\n// ---------------------------------------------------------------------------\n// Recency boost\n// ---------------------------------------------------------------------------\n\n/**\n * Apply exponential recency boost to search scores.\n *\n * Scores are first min-max normalized to [0,1], then multiplied by an\n * exponential decay factor based on chunk age. Normalization is required\n * because the cross-encoder reranker produces negative logit scores — naive\n * multiplication of a negative score by a decay factor (0 < d ≤ 1) would\n * make the score *less* negative, effectively boosting old results instead\n * of penalizing them.\n *\n * Formula: score_final = normalized * exp(-lambda * age_days)\n * where lambda = ln(2) / halfLifeDays, normalized ∈ [0,1]\n *\n * With default halfLifeDays=90, a 3-month-old chunk retains 50% of its\n * normalized score, a 6-month-old retains 25%, and a 1-year-old ~6%.\n *\n * Results without an updatedAt timestamp receive no decay penalty.\n * Results are re-sorted by the boosted score after application.\n *\n * @param results Search results with optional updatedAt timestamps.\n * @param halfLifeDays Score halves every N days. Default 90 (~3 months).\n * @returns New array sorted by decayed normalized score (descending).\n */\nexport function applyRecencyBoost(\n results: SearchResult[],\n halfLifeDays = 90,\n): SearchResult[] {\n if (halfLifeDays <= 0 || results.length === 0) return results;\n\n const lambda = Math.LN2 / halfLifeDays;\n const now = Date.now();\n\n // Min-max normalize scores to [0,1] so multiplicative decay works\n // correctly regardless of the raw score sign/scale.\n const scores = results.map((r) => r.score);\n const minScore = Math.min(...scores);\n const maxScore = Math.max(...scores);\n const range = maxScore - minScore;\n\n return results\n .map((r) => {\n const normalized = range === 0 ? 1 : (r.score - minScore) / range;\n // QW2: use the more recent of updated_at and last_accessed_at for recency decay\n const effectiveTs = r.updatedAt != null && r.lastAccessedAt != null\n ? Math.max(r.updatedAt, r.lastAccessedAt)\n : (r.lastAccessedAt ?? r.updatedAt);\n const decay = effectiveTs\n ? Math.exp(-lambda * Math.max(0, (now - effectiveTs) / 86_400_000))\n : 1; // no timestamp → no penalty\n return { ...r, score: normalized * decay };\n })\n .sort((a, b) => b.score - a.score);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4EA,SAAgB,mBAAmB,GAAqB;CACtD,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,QAAO,wEAAwE,KAAK,IAAI;;AAG1F,SAAgB,cAAc,OAAuB;CACnD,MAAM,SAAS,MACZ,aAAa,CACb,MAAM,cAAc,CACpB,OAAO,QAAQ,CACf,QAAQ,MAAM,EAAE,UAAU,EAAE,CAC5B,QAAQ,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC,CAEjC,KAAK,MAAM,IAAI,EAAE,QAAQ,MAAM,OAAK,CAAC,GAAG;AAE3C,KAAI,OAAO,WAAW,EAEpB,QAAO,IAAI,MAAM,QAAQ,MAAM,OAAK,CAAC;AAGvC,QAAO,OAAO,KAAK,OAAO;;;;;;AAe5B,eAAsB,cACpB,SACA,UACyB;AACzB,KAAI,QAAQ,WAAW,EAAG,QAAO;CAEjC,MAAM,MAAM,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAK,MAAM,EAAE,UAAU,CAAC,CAAC;CAIzD,MAAM,0BAAU,IAAI,KAAqB;AACzC,MAAK,MAAM,MAAM,KAAK;EACpB,MAAM,UAAU,MAAM,SAAS,eAAe,GAAG;AACjD,MAAI,QAAS,SAAQ,IAAI,IAAI,QAAQ,KAAK;;AAG5C,QAAO,QAAQ,KAAK,OAAO;EACzB,GAAG;EACH,aAAa,QAAQ,IAAI,EAAE,UAAU;EACtC,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;AA8BL,SAAgB,kBACd,SACA,eAAe,IACC;AAChB,KAAI,gBAAgB,KAAK,QAAQ,WAAW,EAAG,QAAO;CAEtD,MAAM,SAAS,KAAK,MAAM;CAC1B,MAAM,MAAM,KAAK,KAAK;CAItB,MAAM,SAAS,QAAQ,KAAK,MAAM,EAAE,MAAM;CAC1C,MAAM,WAAW,KAAK,IAAI,GAAG,OAAO;CAEpC,MAAM,QADW,KAAK,IAAI,GAAG,OAAO,GACX;AAEzB,QAAO,QACJ,KAAK,MAAM;EACV,MAAM,aAAa,UAAU,IAAI,KAAK,EAAE,QAAQ,YAAY;EAE5D,MAAM,cAAc,EAAE,aAAa,QAAQ,EAAE,kBAAkB,OAC3D,KAAK,IAAI,EAAE,WAAW,EAAE,eAAe,GACtC,EAAE,kBAAkB,EAAE;EAC3B,MAAM,QAAQ,cACV,KAAK,IAAI,CAAC,SAAS,KAAK,IAAI,IAAI,MAAM,eAAe,MAAW,CAAC,GACjE;AACJ,SAAO;GAAE,GAAG;GAAG,OAAO,aAAa;GAAO;GAC1C,CACD,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM"}
@@ -1,4 +1,4 @@
1
- import { _ as warn, c as ok, n as dim, o as header, t as bold, u as renderTable } from "./utils-C9HsYDpO.mjs";
1
+ import { c as ok, g as warn, l as renderTable, n as dim, o as header, t as bold } from "./utils-DddRwMsG.mjs";
2
2
 
3
3
  //#region src/cli/commands/memory/sources.ts
4
4
  /**
@@ -114,4 +114,4 @@ function tail(s, n) {
114
114
 
115
115
  //#endregion
116
116
  export { cmdMemorySources };
117
- //# sourceMappingURL=sources-CijVso3n.mjs.map
117
+ //# sourceMappingURL=sources-CM2g-CLT.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"sources-CijVso3n.mjs","names":[],"sources":["../src/cli/commands/memory/sources.ts"],"sourcesContent":["/**\n * `pai memory sources` — what the indexer is actually taking in.\n *\n * The index had grown to ~1.5M chunks against a vault of ~2,700 notes, and\n * nothing in the CLI could show why. `memory status` refuses to report at all\n * when the backend is not SQLite, and `daemon status` gives three totals with no\n * composition — so answering \"what is it indexing, and why is it re-indexing\"\n * meant hand-writing aggregate SQL against the container.\n *\n * The cause, when it was finally measured, was visible in one breakdown: the\n * vault indexer follows symlinks out of the vault into cloud-synced trees, and\n * those trees have mtimes rewritten by the sync client on content that has not\n * changed. Each rewrite re-chunks the file, and re-chunking assigns new chunk\n * ids, which discards the embeddings — so the embedder was re-doing work\n * indefinitely while never catching up.\n *\n * Hence the four sections below. Each one exists because it was needed:\n * composition — the vault dwarfing everything else is the first clue\n * roots — where content enters from, which is how symlink leakage shows\n * heaviest — single files contributing thousands of chunks (attachments)\n * churn — chunks rewritten per day, which is what distinguishes a\n * backlog that will finish from a treadmill that never will\n */\n\nimport { ok, warn, dim, bold, header, renderTable } from \"../../utils.js\";\nimport type { StorageBackend } from \"../../../storage/interface.js\";\n\nconst num = (v: unknown): number => Number(v ?? 0);\nconst pct = (part: number, whole: number): string =>\n whole === 0 ? \"—\" : `${Math.round((part / whole) * 100)}%`;\n\n/** Group the leading path segments, which is where content enters the index. */\nexport function rootOf(path: string, depth = 2): string {\n const parts = path.split(\"/\").filter(Boolean);\n if (parts.length <= depth) return path;\n return parts.slice(0, depth).join(\"/\") + \"/…\";\n}\n\nexport async function cmdMemorySources(\n backend: StorageBackend,\n opts: { limit?: number } = {}\n): Promise<void> {\n const limit = opts.limit ?? 8;\n\n const report = await backend.getMemorySourcesReport();\n const comp = report.composition;\n\n if (comp.length === 0) {\n console.log();\n console.log(warn(` Nothing indexed yet, or the backend is unreachable.`));\n console.log(dim(` Backend: ${backend.backendType}`));\n console.log();\n return;\n }\n\n const totalChunks = comp.reduce((s, r) => s + num(r.chunks), 0);\n const totalEmbedded = comp.reduce((s, r) => s + num(r.embedded), 0);\n\n console.log();\n console.log(header(`What the indexer has taken in`));\n console.log();\n console.log(\n ` ${bold(totalChunks.toLocaleString())} chunks ` +\n `${totalEmbedded.toLocaleString()} embedded (${pct(totalEmbedded, totalChunks)}) ` +\n dim(`backend: ${backend.backendType}`)\n );\n console.log();\n\n console.log(\n renderTable(\n [\"source / tier\", \"chunks\", \"share\", \"embedded\"],\n comp.map((r) => [\n `${r.source} / ${r.tier}`,\n num(r.chunks).toLocaleString(),\n pct(num(r.chunks), totalChunks),\n `${num(r.embedded).toLocaleString()} (${pct(num(r.embedded), num(r.chunks))})`,\n ])\n )\n );\n\n // ---- where it enters from ---------------------------------------------\n const paths = report.paths;\n\n const byRoot = new Map<string, number>();\n for (const r of paths) {\n const k = rootOf(String(r.path ?? \"\"));\n byRoot.set(k, (byRoot.get(k) ?? 0) + num(r.chunks));\n }\n const roots = [...byRoot.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);\n\n console.log();\n console.log(header(`Where it comes from`));\n console.log(dim(` A root you did not expect here is usually a symlink leading out of the vault.`));\n console.log();\n console.log(\n renderTable(\n [\"root\", \"chunks\", \"share\"],\n roots.map(([k, v]) => [k, v.toLocaleString(), pct(v, totalChunks)])\n )\n );\n\n // ---- heaviest single files -------------------------------------------\n const heaviest = [...paths]\n .sort((a, b) => num(b.chunks) - num(a.chunks))\n .slice(0, limit);\n\n console.log();\n console.log(header(`Heaviest single files`));\n console.log();\n console.log(\n renderTable(\n [\"chunks\", \"path\"],\n heaviest.map((r) => [num(r.chunks).toLocaleString(), tail(String(r.path ?? \"\"), 62)])\n )\n );\n\n // ---- churn ------------------------------------------------------------\n // The section that distinguishes a backlog from a treadmill. A day with a\n // large chunk count and a small embedded count means those chunks were\n // rewritten and their embeddings thrown away.\n const churn = report.churn;\n\n console.log();\n console.log(header(`Rewritten per day`));\n console.log(\n dim(` Many chunks with few embedded means they were re-chunked and their`)\n );\n console.log(dim(` embeddings discarded — work the embedder has to redo.`));\n console.log();\n console.log(\n renderTable(\n [\"day\", \"chunks touched\", \"of those embedded\"],\n churn.map((r) => [\n String(r.day),\n num(r.chunks).toLocaleString(),\n `${num(r.embedded).toLocaleString()} (${pct(num(r.embedded), num(r.chunks))})`,\n ])\n )\n );\n\n const missing = totalChunks - totalEmbedded;\n console.log();\n if (missing > 0) {\n console.log(` ${bold(missing.toLocaleString())} chunks still need embedding.`);\n } else {\n console.log(ok(` Everything indexed is embedded.`));\n }\n console.log();\n}\n\n/** Keep the informative end of a long path. */\nfunction tail(s: string, n: number): string {\n return s.length <= n ? s : \"…\" + s.slice(-(n - 1));\n}\n\n"],"mappings":"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"}
1
+ {"version":3,"file":"sources-CM2g-CLT.mjs","names":[],"sources":["../src/cli/commands/memory/sources.ts"],"sourcesContent":["/**\n * `pai memory sources` — what the indexer is actually taking in.\n *\n * The index had grown to ~1.5M chunks against a vault of ~2,700 notes, and\n * nothing in the CLI could show why. `memory status` refuses to report at all\n * when the backend is not SQLite, and `daemon status` gives three totals with no\n * composition — so answering \"what is it indexing, and why is it re-indexing\"\n * meant hand-writing aggregate SQL against the container.\n *\n * The cause, when it was finally measured, was visible in one breakdown: the\n * vault indexer follows symlinks out of the vault into cloud-synced trees, and\n * those trees have mtimes rewritten by the sync client on content that has not\n * changed. Each rewrite re-chunks the file, and re-chunking assigns new chunk\n * ids, which discards the embeddings — so the embedder was re-doing work\n * indefinitely while never catching up.\n *\n * Hence the four sections below. Each one exists because it was needed:\n * composition — the vault dwarfing everything else is the first clue\n * roots — where content enters from, which is how symlink leakage shows\n * heaviest — single files contributing thousands of chunks (attachments)\n * churn — chunks rewritten per day, which is what distinguishes a\n * backlog that will finish from a treadmill that never will\n */\n\nimport { ok, warn, dim, bold, header, renderTable } from \"../../utils.js\";\nimport type { StorageBackend } from \"../../../storage/interface.js\";\n\nconst num = (v: unknown): number => Number(v ?? 0);\nconst pct = (part: number, whole: number): string =>\n whole === 0 ? \"—\" : `${Math.round((part / whole) * 100)}%`;\n\n/** Group the leading path segments, which is where content enters the index. */\nexport function rootOf(path: string, depth = 2): string {\n const parts = path.split(\"/\").filter(Boolean);\n if (parts.length <= depth) return path;\n return parts.slice(0, depth).join(\"/\") + \"/…\";\n}\n\nexport async function cmdMemorySources(\n backend: StorageBackend,\n opts: { limit?: number } = {}\n): Promise<void> {\n const limit = opts.limit ?? 8;\n\n const report = await backend.getMemorySourcesReport();\n const comp = report.composition;\n\n if (comp.length === 0) {\n console.log();\n console.log(warn(` Nothing indexed yet, or the backend is unreachable.`));\n console.log(dim(` Backend: ${backend.backendType}`));\n console.log();\n return;\n }\n\n const totalChunks = comp.reduce((s, r) => s + num(r.chunks), 0);\n const totalEmbedded = comp.reduce((s, r) => s + num(r.embedded), 0);\n\n console.log();\n console.log(header(`What the indexer has taken in`));\n console.log();\n console.log(\n ` ${bold(totalChunks.toLocaleString())} chunks ` +\n `${totalEmbedded.toLocaleString()} embedded (${pct(totalEmbedded, totalChunks)}) ` +\n dim(`backend: ${backend.backendType}`)\n );\n console.log();\n\n console.log(\n renderTable(\n [\"source / tier\", \"chunks\", \"share\", \"embedded\"],\n comp.map((r) => [\n `${r.source} / ${r.tier}`,\n num(r.chunks).toLocaleString(),\n pct(num(r.chunks), totalChunks),\n `${num(r.embedded).toLocaleString()} (${pct(num(r.embedded), num(r.chunks))})`,\n ])\n )\n );\n\n // ---- where it enters from ---------------------------------------------\n const paths = report.paths;\n\n const byRoot = new Map<string, number>();\n for (const r of paths) {\n const k = rootOf(String(r.path ?? \"\"));\n byRoot.set(k, (byRoot.get(k) ?? 0) + num(r.chunks));\n }\n const roots = [...byRoot.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);\n\n console.log();\n console.log(header(`Where it comes from`));\n console.log(dim(` A root you did not expect here is usually a symlink leading out of the vault.`));\n console.log();\n console.log(\n renderTable(\n [\"root\", \"chunks\", \"share\"],\n roots.map(([k, v]) => [k, v.toLocaleString(), pct(v, totalChunks)])\n )\n );\n\n // ---- heaviest single files -------------------------------------------\n const heaviest = [...paths]\n .sort((a, b) => num(b.chunks) - num(a.chunks))\n .slice(0, limit);\n\n console.log();\n console.log(header(`Heaviest single files`));\n console.log();\n console.log(\n renderTable(\n [\"chunks\", \"path\"],\n heaviest.map((r) => [num(r.chunks).toLocaleString(), tail(String(r.path ?? \"\"), 62)])\n )\n );\n\n // ---- churn ------------------------------------------------------------\n // The section that distinguishes a backlog from a treadmill. A day with a\n // large chunk count and a small embedded count means those chunks were\n // rewritten and their embeddings thrown away.\n const churn = report.churn;\n\n console.log();\n console.log(header(`Rewritten per day`));\n console.log(\n dim(` Many chunks with few embedded means they were re-chunked and their`)\n );\n console.log(dim(` embeddings discarded — work the embedder has to redo.`));\n console.log();\n console.log(\n renderTable(\n [\"day\", \"chunks touched\", \"of those embedded\"],\n churn.map((r) => [\n String(r.day),\n num(r.chunks).toLocaleString(),\n `${num(r.embedded).toLocaleString()} (${pct(num(r.embedded), num(r.chunks))})`,\n ])\n )\n );\n\n const missing = totalChunks - totalEmbedded;\n console.log();\n if (missing > 0) {\n console.log(` ${bold(missing.toLocaleString())} chunks still need embedding.`);\n } else {\n console.log(ok(` Everything indexed is embedded.`));\n }\n console.log();\n}\n\n/** Keep the informative end of a long path. */\nfunction tail(s: string, n: number): string {\n return s.length <= n ? s : \"…\" + s.slice(-(n - 1));\n}\n\n"],"mappings":"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"}
@@ -323,4 +323,4 @@ const TITLE_STOP_WORDS = STOP_WORDS;
323
323
 
324
324
  //#endregion
325
325
  export { TITLE_STOP_WORDS as n, STOP_WORDS as t };
326
- //# sourceMappingURL=stop-words-BdQuaE9K.mjs.map
326
+ //# sourceMappingURL=stop-words-DtxaTWU_.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"stop-words-BdQuaE9K.mjs","names":[],"sources":["../src/utils/stop-words.ts"],"sourcesContent":["/**\n * Shared stop-word list used across memory search, slug generation, graph clustering,\n * and zettelkasten modules. This is the union of all per-file sets that previously\n * existed in 7 different files.\n */\n\nexport const STOP_WORDS = new Set([\n // Articles / determiners\n \"a\", \"an\", \"the\",\n // Conjunctions\n \"and\", \"or\", \"but\", \"if\", \"then\", \"else\", \"so\", \"because\", \"as\",\n \"although\", \"however\", \"therefore\", \"thus\", \"hence\", \"meanwhile\",\n \"moreover\", \"furthermore\", \"otherwise\", \"instead\", \"anyway\",\n // Prepositions\n \"at\", \"to\", \"for\", \"of\", \"with\", \"by\", \"from\", \"in\", \"on\", \"out\",\n \"off\", \"over\", \"under\", \"up\", \"into\", \"without\", \"per\", \"via\",\n // Pronouns\n \"i\", \"you\", \"we\", \"they\", \"he\", \"she\", \"it\", \"me\", \"us\", \"him\", \"her\",\n \"my\", \"your\", \"our\", \"their\", \"his\", \"its\", \"who\", \"whom\", \"what\",\n \"which\", \"this\", \"that\", \"these\", \"those\",\n // Common verbs\n \"is\", \"are\", \"was\", \"were\", \"be\", \"been\", \"being\",\n \"have\", \"has\", \"had\", \"do\", \"does\", \"did\",\n \"will\", \"would\", \"could\", \"should\", \"may\", \"might\", \"can\", \"shall\",\n \"want\", \"need\", \"know\", \"think\", \"see\", \"look\", \"make\", \"get\", \"go\",\n \"come\", \"take\", \"use\", \"find\", \"give\", \"tell\", \"say\", \"said\", \"try\",\n \"keep\", \"run\", \"set\", \"put\", \"add\", \"show\", \"check\", \"let\",\n // Negation / common function words\n \"not\", \"no\", \"yes\", \"just\", \"also\", \"very\", \"really\",\n \"about\", \"after\", \"before\",\n \"more\", \"most\", \"some\", \"any\", \"all\", \"each\", \"every\", \"both\", \"few\",\n \"many\", \"much\", \"other\", \"another\", \"such\", \"only\", \"own\", \"same\",\n \"than\", \"too\", \"ok\", \"okay\", \"sure\",\n \"please\", \"thanks\", \"thank\", \"here\", \"there\", \"now\", \"well\", \"like\",\n \"going\", \"done\", \"got\",\n // Tech/URL junk\n \"https\", \"http\", \"www\", \"com\", \"org\", \"net\", \"io\",\n \"null\", \"undefined\", \"true\", \"false\",\n // Contractions (de-apostrophe'd forms that appear after tokenisation)\n \"ll\", \"ve\", \"re\", \"don\", \"thats\", \"heres\", \"theres\",\n \"youre\", \"theyre\", \"didnt\", \"dont\", \"doesnt\", \"havent\", \"hasnt\",\n \"wont\", \"cant\", \"shouldnt\", \"wouldnt\", \"couldnt\", \"isnt\", \"arent\",\n \"wasnt\", \"werent\",\n // Filler adverbs\n \"never\", \"ever\", \"still\", \"already\", \"yet\", \"back\",\n \"away\", \"down\", \"right\", \"left\", \"next\", \"last\", \"first\", \"second\",\n \"third\",\n \"then\", \"again\", \"once\", \"twice\", \"since\", \"while\", \"though\",\n \"actually\", \"basically\", \"literally\", \"simply\", \"exactly\", \"probably\",\n \"possibly\", \"maybe\", \"perhaps\", \"certainly\", \"definitely\", \"absolutely\",\n \"completely\", \"totally\", \"quite\", \"rather\", \"fairly\", \"nearly\",\n \"almost\", \"barely\", \"hardly\", \"quickly\", \"slowly\", \"easily\", \"likely\",\n \"unlikely\",\n // Numbers (spelled out)\n \"one\", \"two\", \"three\", \"four\", \"five\", \"six\", \"seven\",\n \"eight\", \"nine\", \"ten\",\n // Common nouns (too generic for search/slug)\n \"time\", \"way\", \"thing\", \"something\", \"anything\", \"nothing\",\n \"everything\", \"someone\", \"anyone\", \"everyone\",\n // Zettelkasten / vault-specific noise\n \"new\", \"note\", \"untitled\", \"page\", \"file\", \"doc\", \"code\",\n \"session\", \"notes\", \"moc\", \"template\", \"content\", \"attachment\",\n // Misc abbreviations\n \"etc\", \"ie\", \"eg\", \"vs\",\n // French stop words (for multilingual vaults)\n \"les\", \"des\", \"une\", \"est\", \"que\", \"qui\", \"dans\", \"pour\", \"sur\",\n \"par\", \"pas\", \"son\", \"ses\", \"aux\", \"avec\", \"tout\", \"mais\",\n // German stop words (for multilingual vaults)\n \"und\", \"der\", \"die\", \"das\", \"ein\", \"eine\", \"ist\", \"den\", \"dem\",\n \"von\", \"mit\", \"auf\", \"nicht\", \"sich\", \"auch\", \"noch\", \"wie\",\n]);\n\n/**\n * Alias used by graph modules that need stop-word filtering specifically for\n * vault note titles (same set, just semantically named for clarity).\n */\nexport const TITLE_STOP_WORDS = STOP_WORDS;\n"],"mappings":";;;;;;AAMA,MAAa,aAAa,IAAI,IAAI;CAEhC;CAAK;CAAM;CAEX;CAAO;CAAM;CAAO;CAAM;CAAQ;CAAQ;CAAM;CAAW;CAC3D;CAAY;CAAW;CAAa;CAAQ;CAAS;CACrD;CAAY;CAAe;CAAa;CAAW;CAEnD;CAAM;CAAM;CAAO;CAAM;CAAQ;CAAM;CAAQ;CAAM;CAAM;CAC3D;CAAO;CAAQ;CAAS;CAAM;CAAQ;CAAW;CAAO;CAExD;CAAK;CAAO;CAAM;CAAQ;CAAM;CAAO;CAAM;CAAM;CAAM;CAAO;CAChE;CAAM;CAAQ;CAAO;CAAS;CAAO;CAAO;CAAO;CAAQ;CAC3D;CAAS;CAAQ;CAAQ;CAAS;CAElC;CAAM;CAAO;CAAO;CAAQ;CAAM;CAAQ;CAC1C;CAAQ;CAAO;CAAO;CAAM;CAAQ;CACpC;CAAQ;CAAS;CAAS;CAAU;CAAO;CAAS;CAAO;CAC3D;CAAQ;CAAQ;CAAQ;CAAS;CAAO;CAAQ;CAAQ;CAAO;CAC/D;CAAQ;CAAQ;CAAO;CAAQ;CAAQ;CAAQ;CAAO;CAAQ;CAC9D;CAAQ;CAAO;CAAO;CAAO;CAAO;CAAQ;CAAS;CAErD;CAAO;CAAM;CAAO;CAAQ;CAAQ;CAAQ;CAC5C;CAAS;CAAS;CAClB;CAAQ;CAAQ;CAAQ;CAAO;CAAO;CAAQ;CAAS;CAAQ;CAC/D;CAAQ;CAAQ;CAAS;CAAW;CAAQ;CAAQ;CAAO;CAC3D;CAAQ;CAAO;CAAM;CAAQ;CAC7B;CAAU;CAAU;CAAS;CAAQ;CAAS;CAAO;CAAQ;CAC7D;CAAS;CAAQ;CAEjB;CAAS;CAAQ;CAAO;CAAO;CAAO;CAAO;CAC7C;CAAQ;CAAa;CAAQ;CAE7B;CAAM;CAAM;CAAM;CAAO;CAAS;CAAS;CAC3C;CAAS;CAAU;CAAS;CAAQ;CAAU;CAAU;CACxD;CAAQ;CAAQ;CAAY;CAAW;CAAW;CAAQ;CAC1D;CAAS;CAET;CAAS;CAAQ;CAAS;CAAW;CAAO;CAC5C;CAAQ;CAAQ;CAAS;CAAQ;CAAQ;CAAQ;CAAS;CAC1D;CACA;CAAQ;CAAS;CAAQ;CAAS;CAAS;CAAS;CACpD;CAAY;CAAa;CAAa;CAAU;CAAW;CAC3D;CAAY;CAAS;CAAW;CAAa;CAAc;CAC3D;CAAc;CAAW;CAAS;CAAU;CAAU;CACtD;CAAU;CAAU;CAAU;CAAW;CAAU;CAAU;CAC7D;CAEA;CAAO;CAAO;CAAS;CAAQ;CAAQ;CAAO;CAC9C;CAAS;CAAQ;CAEjB;CAAQ;CAAO;CAAS;CAAa;CAAY;CACjD;CAAc;CAAW;CAAU;CAEnC;CAAO;CAAQ;CAAY;CAAQ;CAAQ;CAAO;CAClD;CAAW;CAAS;CAAO;CAAY;CAAW;CAElD;CAAO;CAAM;CAAM;CAEnB;CAAO;CAAO;CAAO;CAAO;CAAO;CAAO;CAAQ;CAAQ;CAC1D;CAAO;CAAO;CAAO;CAAO;CAAO;CAAQ;CAAQ;CAEnD;CAAO;CAAO;CAAO;CAAO;CAAO;CAAQ;CAAO;CAAO;CACzD;CAAO;CAAO;CAAO;CAAS;CAAQ;CAAQ;CAAQ;CACvD,CAAC;;;;;AAMF,MAAa,mBAAmB"}
1
+ {"version":3,"file":"stop-words-DtxaTWU_.mjs","names":[],"sources":["../src/utils/stop-words.ts"],"sourcesContent":["/**\n * Shared stop-word list used across memory search, slug generation, graph clustering,\n * and zettelkasten modules. This is the union of all per-file sets that previously\n * existed in 7 different files.\n */\n\nexport const STOP_WORDS = new Set([\n // Articles / determiners\n \"a\", \"an\", \"the\",\n // Conjunctions\n \"and\", \"or\", \"but\", \"if\", \"then\", \"else\", \"so\", \"because\", \"as\",\n \"although\", \"however\", \"therefore\", \"thus\", \"hence\", \"meanwhile\",\n \"moreover\", \"furthermore\", \"otherwise\", \"instead\", \"anyway\",\n // Prepositions\n \"at\", \"to\", \"for\", \"of\", \"with\", \"by\", \"from\", \"in\", \"on\", \"out\",\n \"off\", \"over\", \"under\", \"up\", \"into\", \"without\", \"per\", \"via\",\n // Pronouns\n \"i\", \"you\", \"we\", \"they\", \"he\", \"she\", \"it\", \"me\", \"us\", \"him\", \"her\",\n \"my\", \"your\", \"our\", \"their\", \"his\", \"its\", \"who\", \"whom\", \"what\",\n \"which\", \"this\", \"that\", \"these\", \"those\",\n // Common verbs\n \"is\", \"are\", \"was\", \"were\", \"be\", \"been\", \"being\",\n \"have\", \"has\", \"had\", \"do\", \"does\", \"did\",\n \"will\", \"would\", \"could\", \"should\", \"may\", \"might\", \"can\", \"shall\",\n \"want\", \"need\", \"know\", \"think\", \"see\", \"look\", \"make\", \"get\", \"go\",\n \"come\", \"take\", \"use\", \"find\", \"give\", \"tell\", \"say\", \"said\", \"try\",\n \"keep\", \"run\", \"set\", \"put\", \"add\", \"show\", \"check\", \"let\",\n // Negation / common function words\n \"not\", \"no\", \"yes\", \"just\", \"also\", \"very\", \"really\",\n \"about\", \"after\", \"before\",\n \"more\", \"most\", \"some\", \"any\", \"all\", \"each\", \"every\", \"both\", \"few\",\n \"many\", \"much\", \"other\", \"another\", \"such\", \"only\", \"own\", \"same\",\n \"than\", \"too\", \"ok\", \"okay\", \"sure\",\n \"please\", \"thanks\", \"thank\", \"here\", \"there\", \"now\", \"well\", \"like\",\n \"going\", \"done\", \"got\",\n // Tech/URL junk\n \"https\", \"http\", \"www\", \"com\", \"org\", \"net\", \"io\",\n \"null\", \"undefined\", \"true\", \"false\",\n // Contractions (de-apostrophe'd forms that appear after tokenisation)\n \"ll\", \"ve\", \"re\", \"don\", \"thats\", \"heres\", \"theres\",\n \"youre\", \"theyre\", \"didnt\", \"dont\", \"doesnt\", \"havent\", \"hasnt\",\n \"wont\", \"cant\", \"shouldnt\", \"wouldnt\", \"couldnt\", \"isnt\", \"arent\",\n \"wasnt\", \"werent\",\n // Filler adverbs\n \"never\", \"ever\", \"still\", \"already\", \"yet\", \"back\",\n \"away\", \"down\", \"right\", \"left\", \"next\", \"last\", \"first\", \"second\",\n \"third\",\n \"then\", \"again\", \"once\", \"twice\", \"since\", \"while\", \"though\",\n \"actually\", \"basically\", \"literally\", \"simply\", \"exactly\", \"probably\",\n \"possibly\", \"maybe\", \"perhaps\", \"certainly\", \"definitely\", \"absolutely\",\n \"completely\", \"totally\", \"quite\", \"rather\", \"fairly\", \"nearly\",\n \"almost\", \"barely\", \"hardly\", \"quickly\", \"slowly\", \"easily\", \"likely\",\n \"unlikely\",\n // Numbers (spelled out)\n \"one\", \"two\", \"three\", \"four\", \"five\", \"six\", \"seven\",\n \"eight\", \"nine\", \"ten\",\n // Common nouns (too generic for search/slug)\n \"time\", \"way\", \"thing\", \"something\", \"anything\", \"nothing\",\n \"everything\", \"someone\", \"anyone\", \"everyone\",\n // Zettelkasten / vault-specific noise\n \"new\", \"note\", \"untitled\", \"page\", \"file\", \"doc\", \"code\",\n \"session\", \"notes\", \"moc\", \"template\", \"content\", \"attachment\",\n // Misc abbreviations\n \"etc\", \"ie\", \"eg\", \"vs\",\n // French stop words (for multilingual vaults)\n \"les\", \"des\", \"une\", \"est\", \"que\", \"qui\", \"dans\", \"pour\", \"sur\",\n \"par\", \"pas\", \"son\", \"ses\", \"aux\", \"avec\", \"tout\", \"mais\",\n // German stop words (for multilingual vaults)\n \"und\", \"der\", \"die\", \"das\", \"ein\", \"eine\", \"ist\", \"den\", \"dem\",\n \"von\", \"mit\", \"auf\", \"nicht\", \"sich\", \"auch\", \"noch\", \"wie\",\n]);\n\n/**\n * Alias used by graph modules that need stop-word filtering specifically for\n * vault note titles (same set, just semantically named for clarity).\n */\nexport const TITLE_STOP_WORDS = STOP_WORDS;\n"],"mappings":";;;;;;AAMA,MAAa,aAAa,IAAI,IAAI;CAEhC;CAAK;CAAM;CAEX;CAAO;CAAM;CAAO;CAAM;CAAQ;CAAQ;CAAM;CAAW;CAC3D;CAAY;CAAW;CAAa;CAAQ;CAAS;CACrD;CAAY;CAAe;CAAa;CAAW;CAEnD;CAAM;CAAM;CAAO;CAAM;CAAQ;CAAM;CAAQ;CAAM;CAAM;CAC3D;CAAO;CAAQ;CAAS;CAAM;CAAQ;CAAW;CAAO;CAExD;CAAK;CAAO;CAAM;CAAQ;CAAM;CAAO;CAAM;CAAM;CAAM;CAAO;CAChE;CAAM;CAAQ;CAAO;CAAS;CAAO;CAAO;CAAO;CAAQ;CAC3D;CAAS;CAAQ;CAAQ;CAAS;CAElC;CAAM;CAAO;CAAO;CAAQ;CAAM;CAAQ;CAC1C;CAAQ;CAAO;CAAO;CAAM;CAAQ;CACpC;CAAQ;CAAS;CAAS;CAAU;CAAO;CAAS;CAAO;CAC3D;CAAQ;CAAQ;CAAQ;CAAS;CAAO;CAAQ;CAAQ;CAAO;CAC/D;CAAQ;CAAQ;CAAO;CAAQ;CAAQ;CAAQ;CAAO;CAAQ;CAC9D;CAAQ;CAAO;CAAO;CAAO;CAAO;CAAQ;CAAS;CAErD;CAAO;CAAM;CAAO;CAAQ;CAAQ;CAAQ;CAC5C;CAAS;CAAS;CAClB;CAAQ;CAAQ;CAAQ;CAAO;CAAO;CAAQ;CAAS;CAAQ;CAC/D;CAAQ;CAAQ;CAAS;CAAW;CAAQ;CAAQ;CAAO;CAC3D;CAAQ;CAAO;CAAM;CAAQ;CAC7B;CAAU;CAAU;CAAS;CAAQ;CAAS;CAAO;CAAQ;CAC7D;CAAS;CAAQ;CAEjB;CAAS;CAAQ;CAAO;CAAO;CAAO;CAAO;CAC7C;CAAQ;CAAa;CAAQ;CAE7B;CAAM;CAAM;CAAM;CAAO;CAAS;CAAS;CAC3C;CAAS;CAAU;CAAS;CAAQ;CAAU;CAAU;CACxD;CAAQ;CAAQ;CAAY;CAAW;CAAW;CAAQ;CAC1D;CAAS;CAET;CAAS;CAAQ;CAAS;CAAW;CAAO;CAC5C;CAAQ;CAAQ;CAAS;CAAQ;CAAQ;CAAQ;CAAS;CAC1D;CACA;CAAQ;CAAS;CAAQ;CAAS;CAAS;CAAS;CACpD;CAAY;CAAa;CAAa;CAAU;CAAW;CAC3D;CAAY;CAAS;CAAW;CAAa;CAAc;CAC3D;CAAc;CAAW;CAAS;CAAU;CAAU;CACtD;CAAU;CAAU;CAAU;CAAW;CAAU;CAAU;CAC7D;CAEA;CAAO;CAAO;CAAS;CAAQ;CAAQ;CAAO;CAC9C;CAAS;CAAQ;CAEjB;CAAQ;CAAO;CAAS;CAAa;CAAY;CACjD;CAAc;CAAW;CAAU;CAEnC;CAAO;CAAQ;CAAY;CAAQ;CAAQ;CAAO;CAClD;CAAW;CAAS;CAAO;CAAY;CAAW;CAElD;CAAO;CAAM;CAAM;CAEnB;CAAO;CAAO;CAAO;CAAO;CAAO;CAAO;CAAQ;CAAQ;CAC1D;CAAO;CAAO;CAAO;CAAO;CAAO;CAAQ;CAAQ;CAEnD;CAAO;CAAO;CAAO;CAAO;CAAO;CAAQ;CAAO;CAAO;CACzD;CAAO;CAAO;CAAO;CAAS;CAAQ;CAAQ;CAAQ;CACvD,CAAC;;;;;AAMF,MAAa,mBAAmB"}
@@ -196,5 +196,5 @@ function now() {
196
196
  }
197
197
 
198
198
  //#endregion
199
- export { warn as _, fmtDate as a, ok as c, resolvePath as d, scaffoldProjectDirs as f, smartDecodeDir as g, slugify as h, err as i, pad as l, slugFromPath as m, dim as n, header as o, shortenPath as p, encodeDir as r, now as s, bold as t, renderTable as u };
200
- //# sourceMappingURL=utils-C9HsYDpO.mjs.map
199
+ export { fmtDate as a, ok as c, scaffoldProjectDirs as d, shortenPath as f, warn as g, smartDecodeDir as h, err as i, renderTable as l, slugify as m, dim as n, header as o, slugFromPath as p, encodeDir as r, now as s, bold as t, resolvePath as u };
200
+ //# sourceMappingURL=utils-DddRwMsG.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils-C9HsYDpO.mjs","names":[],"sources":["../src/cli/utils.ts"],"sourcesContent":["/**\n * Shared utilities for CLI commands: formatting helpers, path encoding,\n * slug generation, and chalk colour wrappers.\n */\n\nimport chalk from \"chalk\";\nimport { resolve, basename, join } from \"node:path\";\nimport { mkdirSync, existsSync, writeFileSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\n\n// ---------------------------------------------------------------------------\n// Chalk colour helpers (thin wrappers so callers don't import chalk directly)\n// ---------------------------------------------------------------------------\n\nexport const ok = (msg: string) => chalk.green(msg);\nexport const warn = (msg: string) => chalk.yellow(msg);\nexport const err = (msg: string) => chalk.red(msg);\nexport const dim = (msg: string) => chalk.dim(msg);\nexport const bold = (msg: string) => chalk.bold(msg);\nexport const header = (msg: string) => chalk.bold.underline(msg);\n\n// ---------------------------------------------------------------------------\n// Path / slug helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Convert any path string into a kebab-case slug.\n * \"/Users/foo/my-project\" → \"my-project\"\n * \"Some Cool Project\" → \"some-cool-project\"\n */\nexport function slugify(input: string): string {\n return input\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\") // non-alphanum runs → single hyphen\n .replace(/^-+|-+$/g, \"\"); // strip leading/trailing hyphens\n}\n\n/**\n * Derive a default project slug from the last component of a path.\n * \"/home/user/my-project\" → \"my-project\"\n */\nexport function slugFromPath(projectPath: string): string {\n return slugify(basename(projectPath));\n}\n\n/**\n * Encode an absolute path into Claude Code's encoded-dir format.\n *\n * Claude Code's actual encoding rules (reverse-engineered from real data):\n * - Every `/`, ` ` (space), `.` (dot), and `-` (literal hyphen) → single `-`\n * - The result therefore starts with `-` (from the leading `/`)\n *\n * This is a lossy encoding — space, dot, hyphen, and path-separator all\n * collapse to the same token. The decode is therefore ambiguous; prefer\n * {@link buildEncodedDirMap} from migrate.ts to get authoritative mappings.\n *\n * Examples:\n * \"/Users/foo/my-project\" → \"-Users-foo-my-project\"\n * \"/Users/foo/04 - Ablage\" → \"-Users-foo-04---Ablage\"\n * \"/Users/foo/.ssh\" → \"-Users-foo--ssh\"\n * \"/Users/foo/MDF-System.de\" → \"-Users-foo-MDF-System-de\"\n *\n * NOTE: For `project add`, prefer {@link findExistingEncodedDir} to look up\n * whether Claude Code has already created a directory for this path — that\n * avoids any mismatch between our encoding and Claude's.\n */\nexport function encodeDir(absolutePath: string): string {\n // Every `/`, space, dot, and hyphen → single `-`\n // The leading `/` produces the leading `-` that all encoded dirs start with.\n return absolutePath.replace(/[\\/\\s.\\-]/g, \"-\");\n}\n\n/**\n * Look up an absolute path in ~/.claude/projects/ to find the encoded-dir\n * name that Claude Code actually uses for it.\n *\n * This is more reliable than {@link encodeDir} because it reads the real\n * filesystem rather than re-implementing Claude's encoding algorithm.\n *\n * Returns the encoded-dir string (e.g. \"-Users-foo-my-project\") if a match\n * is found in ~/.claude/projects/, or `null` if not present.\n */\nexport function findExistingEncodedDir(absolutePath: string): string | null {\n const claudeProjectsDir = join(homedir(), \".claude\", \"projects\");\n if (!existsSync(claudeProjectsDir)) return null;\n\n // Build the expected encoded form to compare against directory names\n const expected = encodeDir(absolutePath);\n\n try {\n const entries = readdirSync(claudeProjectsDir);\n // Exact match (our encoding matches Claude's)\n if (entries.includes(expected)) return expected;\n\n // Fallback: scan all entries and compare the decoded path.\n // Import decodeEncodedDir lazily to avoid circular dependency.\n for (const entry of entries) {\n const full = join(claudeProjectsDir, entry);\n try {\n if (!statSync(full).isDirectory()) continue;\n } catch {\n continue;\n }\n // Simple heuristic decode: `--` → `-`, single `-` → `/`\n // (good enough for finding exact matches via the registry JSON)\n if (entry === expected) return entry;\n }\n } catch {\n // Unreadable directory — ignore\n }\n\n return null;\n}\n\n/**\n * Decode a Claude encoded-dir back to an approximate absolute path.\n *\n * NOTE: This decode is best-effort only — the encoding is lossy (space, dot,\n * literal hyphen, and path-separator all collapse to `-`). Prefer reading\n * original_path from session-registry.json via buildEncodedDirMap() in\n * src/registry/migrate.ts for authoritative decoding.\n *\n * \"-Users-foo-my-project\" → \"/Users/foo/my-project\"\n */\nexport function decodeDir(encodedDir: string): string {\n if (!encodedDir) return \"/\";\n // Try filesystem-walking decode first (handles spaces, dots, hyphens correctly)\n const smart = smartDecodeDir(encodedDir);\n if (smart) return smart;\n // Fallback: treat every `-` as `/` (wrong for paths with spaces/dots/hyphens)\n return encodedDir.replace(/-/g, \"/\");\n}\n\n/**\n * Decode a Claude encoded-dir by walking the actual filesystem.\n *\n * Because the encoding is lossy (/, space, dot, and hyphen all → `-`), the\n * only reliable way to reverse it is to check what actually exists on disk.\n *\n * Algorithm: starting from `/`, read directory entries at each level, encode\n * each candidate, and greedily match the longest one against the remaining\n * encoded string. This correctly resolves e.g.:\n *\n * \"-Users-foo-09---Job-Search\" → \"/Users/foo/09 - Job Search\"\n * \"-Users-foo-87---DevonThink\" → \"/Users/foo/87 - DevonThink\"\n * \"-Users-foo-MDF-System-de\" → \"/Users/foo/MDF-System.de\"\n *\n * Returns `null` if the path cannot be resolved against the filesystem.\n */\nexport function smartDecodeDir(encoded: string): string | null {\n if (!encoded || !encoded.startsWith(\"-\")) return null;\n\n // Strip the leading `-` (encodes the leading `/`)\n let remaining = encoded.slice(1);\n let current = \"/\";\n\n while (remaining.length > 0) {\n let entries: string[];\n try {\n entries = readdirSync(current);\n } catch {\n return null; // Can't read directory\n }\n\n // Encode each candidate entry and find matches against remaining string.\n // Sort by encoded length descending so we prefer the longest (most specific) match.\n // Use case-insensitive comparison because macOS (HFS+/APFS) is case-insensitive\n // by default, and Claude Code may have encoded the dir with different casing\n // than what currently exists on disk (e.g. directory was renamed TEKmidian → TEKMidian).\n const candidates: { name: string; enc: string }[] = [];\n const remainingLower = remaining.toLowerCase();\n for (const name of entries) {\n // Encode this entry the same way Claude Code does (without the leading /)\n const enc = name.replace(/[\\s.\\-]/g, \"-\");\n const encLower = enc.toLowerCase();\n // Must match at start of remaining, followed by `-` separator or end of string\n if (remainingLower === encLower || remainingLower.startsWith(encLower + \"-\")) {\n candidates.push({ name, enc });\n }\n }\n\n if (candidates.length === 0) return null; // No match found\n\n // Prefer longest encoded match (most specific)\n candidates.sort((a, b) => b.enc.length - a.enc.length);\n\n // Try each candidate — pick the first one that is a real directory\n // (or the last segment which may be a file)\n let matched = false;\n for (const { name, enc } of candidates) {\n const nextPath = join(current, name);\n // Use enc.length to consume the right number of chars (case-insensitive match)\n const nextRemaining = remainingLower === enc.toLowerCase() ? \"\" : remaining.slice(enc.length + 1);\n\n // If nothing left, this is the final segment — accept it\n if (nextRemaining === \"\") {\n return nextPath;\n }\n\n // Otherwise, verify this is a directory we can descend into\n try {\n if (statSync(nextPath).isDirectory()) {\n current = nextPath;\n remaining = nextRemaining;\n matched = true;\n break;\n }\n } catch {\n continue;\n }\n }\n\n if (!matched) return null;\n }\n\n return current;\n}\n\n/**\n * Resolve a raw CLI path argument to an absolute path.\n */\nexport function resolvePath(rawPath: string): string {\n return resolve(rawPath);\n}\n\n// ---------------------------------------------------------------------------\n// Filesystem scaffolding\n// ---------------------------------------------------------------------------\n\nconst MEMORY_MD_SCAFFOLD = `# Memory\n\nProject-specific memory for PAI sessions.\nAdd persistent notes, reminders, and context here.\n`;\n\n/**\n * Ensure Notes/ and memory/ sub-directories exist under `projectRoot`.\n * Also creates a memory/MEMORY.md scaffold if it does not yet exist.\n */\nexport function scaffoldProjectDirs(projectRoot: string): void {\n const notesDir = `${projectRoot}/Notes`;\n const memoryDir = `${projectRoot}/memory`;\n const memoryFile = `${memoryDir}/MEMORY.md`;\n\n mkdirSync(notesDir, { recursive: true });\n mkdirSync(memoryDir, { recursive: true });\n\n if (!existsSync(memoryFile)) {\n writeFileSync(memoryFile, MEMORY_MD_SCAFFOLD, \"utf8\");\n }\n}\n\n// ---------------------------------------------------------------------------\n// Table rendering\n// ---------------------------------------------------------------------------\n\n/**\n * Pad a string to a minimum width (left-aligned).\n */\nexport function pad(str: string, width: number): string {\n const plain = stripAnsi(str);\n const extra = width - plain.length;\n return str + (extra > 0 ? \" \".repeat(extra) : \"\");\n}\n\n/**\n * Strip ANSI escape sequences to measure visible string length.\n */\nfunction stripAnsi(str: string): string {\n // eslint-disable-next-line no-control-regex\n return str.replace(/\\x1B\\[[0-9;]*m/g, \"\");\n}\n\n/**\n * Render a simple columnar table.\n *\n * @param headers Column header strings\n * @param rows Array of row arrays (each cell is a string, may include chalk sequences)\n */\nexport function renderTable(headers: string[], rows: string[][]): string {\n const allRows = [headers, ...rows];\n\n // Compute column widths\n const widths = headers.map((_, colIdx) =>\n Math.max(...allRows.map((row) => stripAnsi(row[colIdx] ?? \"\").length))\n );\n\n const divider = dim(\" \" + widths.map((w) => \"-\".repeat(w)).join(\" \"));\n const renderRow = (row: string[], isHeader = false) => {\n const cells = widths.map((w, i) => {\n const cell = row[i] ?? \"\";\n // pad() already strips ANSI internally to compute visible length,\n // so pass the target visible width directly — no fudge factor needed.\n return isHeader ? pad(bold(cell), w) : pad(cell, w);\n });\n return \" \" + cells.join(\" \");\n };\n\n const lines: string[] = [];\n lines.push(renderRow(headers, true));\n lines.push(divider);\n for (const row of rows) {\n lines.push(renderRow(row));\n }\n return lines.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Date helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Shorten an absolute path for display: replace home dir with ~,\n * truncate from left if still longer than maxLen.\n */\nexport function shortenPath(absolutePath: string, maxLen = 40): string {\n const home = homedir();\n let p = absolutePath;\n if (p.startsWith(home)) {\n p = \"~\" + p.slice(home.length);\n }\n if (p.length <= maxLen) return p;\n return \"...\" + p.slice(p.length - maxLen + 3);\n}\n\n/**\n * Format an epoch milliseconds timestamp as YYYY-MM-DD.\n */\nexport function fmtDate(epochMs: number | null | undefined): string {\n if (epochMs == null) return dim(\"—\");\n return new Date(epochMs).toISOString().slice(0, 10);\n}\n\n/**\n * Return the current epoch milliseconds.\n */\nexport function now(): number {\n return Date.now();\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"utils-DddRwMsG.mjs","names":[],"sources":["../src/cli/utils.ts"],"sourcesContent":["/**\n * Shared utilities for CLI commands: formatting helpers, path encoding,\n * slug generation, and chalk colour wrappers.\n */\n\nimport chalk from \"chalk\";\nimport { resolve, basename, join } from \"node:path\";\nimport { mkdirSync, existsSync, writeFileSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\n\n// ---------------------------------------------------------------------------\n// Chalk colour helpers (thin wrappers so callers don't import chalk directly)\n// ---------------------------------------------------------------------------\n\nexport const ok = (msg: string) => chalk.green(msg);\nexport const warn = (msg: string) => chalk.yellow(msg);\nexport const err = (msg: string) => chalk.red(msg);\nexport const dim = (msg: string) => chalk.dim(msg);\nexport const bold = (msg: string) => chalk.bold(msg);\nexport const header = (msg: string) => chalk.bold.underline(msg);\n\n// ---------------------------------------------------------------------------\n// Path / slug helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Convert any path string into a kebab-case slug.\n * \"/Users/foo/my-project\" → \"my-project\"\n * \"Some Cool Project\" → \"some-cool-project\"\n */\nexport function slugify(input: string): string {\n return input\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\") // non-alphanum runs → single hyphen\n .replace(/^-+|-+$/g, \"\"); // strip leading/trailing hyphens\n}\n\n/**\n * Derive a default project slug from the last component of a path.\n * \"/home/user/my-project\" → \"my-project\"\n */\nexport function slugFromPath(projectPath: string): string {\n return slugify(basename(projectPath));\n}\n\n/**\n * Encode an absolute path into Claude Code's encoded-dir format.\n *\n * Claude Code's actual encoding rules (reverse-engineered from real data):\n * - Every `/`, ` ` (space), `.` (dot), and `-` (literal hyphen) → single `-`\n * - The result therefore starts with `-` (from the leading `/`)\n *\n * This is a lossy encoding — space, dot, hyphen, and path-separator all\n * collapse to the same token. The decode is therefore ambiguous; prefer\n * {@link buildEncodedDirMap} from migrate.ts to get authoritative mappings.\n *\n * Examples:\n * \"/Users/foo/my-project\" → \"-Users-foo-my-project\"\n * \"/Users/foo/04 - Ablage\" → \"-Users-foo-04---Ablage\"\n * \"/Users/foo/.ssh\" → \"-Users-foo--ssh\"\n * \"/Users/foo/MDF-System.de\" → \"-Users-foo-MDF-System-de\"\n *\n * NOTE: For `project add`, prefer {@link findExistingEncodedDir} to look up\n * whether Claude Code has already created a directory for this path — that\n * avoids any mismatch between our encoding and Claude's.\n */\nexport function encodeDir(absolutePath: string): string {\n // Every `/`, space, dot, and hyphen → single `-`\n // The leading `/` produces the leading `-` that all encoded dirs start with.\n return absolutePath.replace(/[\\/\\s.\\-]/g, \"-\");\n}\n\n/**\n * Look up an absolute path in ~/.claude/projects/ to find the encoded-dir\n * name that Claude Code actually uses for it.\n *\n * This is more reliable than {@link encodeDir} because it reads the real\n * filesystem rather than re-implementing Claude's encoding algorithm.\n *\n * Returns the encoded-dir string (e.g. \"-Users-foo-my-project\") if a match\n * is found in ~/.claude/projects/, or `null` if not present.\n */\nexport function findExistingEncodedDir(absolutePath: string): string | null {\n const claudeProjectsDir = join(homedir(), \".claude\", \"projects\");\n if (!existsSync(claudeProjectsDir)) return null;\n\n // Build the expected encoded form to compare against directory names\n const expected = encodeDir(absolutePath);\n\n try {\n const entries = readdirSync(claudeProjectsDir);\n // Exact match (our encoding matches Claude's)\n if (entries.includes(expected)) return expected;\n\n // Fallback: scan all entries and compare the decoded path.\n // Import decodeEncodedDir lazily to avoid circular dependency.\n for (const entry of entries) {\n const full = join(claudeProjectsDir, entry);\n try {\n if (!statSync(full).isDirectory()) continue;\n } catch {\n continue;\n }\n // Simple heuristic decode: `--` → `-`, single `-` → `/`\n // (good enough for finding exact matches via the registry JSON)\n if (entry === expected) return entry;\n }\n } catch {\n // Unreadable directory — ignore\n }\n\n return null;\n}\n\n/**\n * Decode a Claude encoded-dir back to an approximate absolute path.\n *\n * NOTE: This decode is best-effort only — the encoding is lossy (space, dot,\n * literal hyphen, and path-separator all collapse to `-`). Prefer reading\n * original_path from session-registry.json via buildEncodedDirMap() in\n * src/registry/migrate.ts for authoritative decoding.\n *\n * \"-Users-foo-my-project\" → \"/Users/foo/my-project\"\n */\nexport function decodeDir(encodedDir: string): string {\n if (!encodedDir) return \"/\";\n // Try filesystem-walking decode first (handles spaces, dots, hyphens correctly)\n const smart = smartDecodeDir(encodedDir);\n if (smart) return smart;\n // Fallback: treat every `-` as `/` (wrong for paths with spaces/dots/hyphens)\n return encodedDir.replace(/-/g, \"/\");\n}\n\n/**\n * Decode a Claude encoded-dir by walking the actual filesystem.\n *\n * Because the encoding is lossy (/, space, dot, and hyphen all → `-`), the\n * only reliable way to reverse it is to check what actually exists on disk.\n *\n * Algorithm: starting from `/`, read directory entries at each level, encode\n * each candidate, and greedily match the longest one against the remaining\n * encoded string. This correctly resolves e.g.:\n *\n * \"-Users-foo-09---Job-Search\" → \"/Users/foo/09 - Job Search\"\n * \"-Users-foo-87---DevonThink\" → \"/Users/foo/87 - DevonThink\"\n * \"-Users-foo-MDF-System-de\" → \"/Users/foo/MDF-System.de\"\n *\n * Returns `null` if the path cannot be resolved against the filesystem.\n */\nexport function smartDecodeDir(encoded: string): string | null {\n if (!encoded || !encoded.startsWith(\"-\")) return null;\n\n // Strip the leading `-` (encodes the leading `/`)\n let remaining = encoded.slice(1);\n let current = \"/\";\n\n while (remaining.length > 0) {\n let entries: string[];\n try {\n entries = readdirSync(current);\n } catch {\n return null; // Can't read directory\n }\n\n // Encode each candidate entry and find matches against remaining string.\n // Sort by encoded length descending so we prefer the longest (most specific) match.\n // Use case-insensitive comparison because macOS (HFS+/APFS) is case-insensitive\n // by default, and Claude Code may have encoded the dir with different casing\n // than what currently exists on disk (e.g. directory was renamed TEKmidian → TEKMidian).\n const candidates: { name: string; enc: string }[] = [];\n const remainingLower = remaining.toLowerCase();\n for (const name of entries) {\n // Encode this entry the same way Claude Code does (without the leading /)\n const enc = name.replace(/[\\s.\\-]/g, \"-\");\n const encLower = enc.toLowerCase();\n // Must match at start of remaining, followed by `-` separator or end of string\n if (remainingLower === encLower || remainingLower.startsWith(encLower + \"-\")) {\n candidates.push({ name, enc });\n }\n }\n\n if (candidates.length === 0) return null; // No match found\n\n // Prefer longest encoded match (most specific)\n candidates.sort((a, b) => b.enc.length - a.enc.length);\n\n // Try each candidate — pick the first one that is a real directory\n // (or the last segment which may be a file)\n let matched = false;\n for (const { name, enc } of candidates) {\n const nextPath = join(current, name);\n // Use enc.length to consume the right number of chars (case-insensitive match)\n const nextRemaining = remainingLower === enc.toLowerCase() ? \"\" : remaining.slice(enc.length + 1);\n\n // If nothing left, this is the final segment — accept it\n if (nextRemaining === \"\") {\n return nextPath;\n }\n\n // Otherwise, verify this is a directory we can descend into\n try {\n if (statSync(nextPath).isDirectory()) {\n current = nextPath;\n remaining = nextRemaining;\n matched = true;\n break;\n }\n } catch {\n continue;\n }\n }\n\n if (!matched) return null;\n }\n\n return current;\n}\n\n/**\n * Resolve a raw CLI path argument to an absolute path.\n */\nexport function resolvePath(rawPath: string): string {\n return resolve(rawPath);\n}\n\n// ---------------------------------------------------------------------------\n// Filesystem scaffolding\n// ---------------------------------------------------------------------------\n\nconst MEMORY_MD_SCAFFOLD = `# Memory\n\nProject-specific memory for PAI sessions.\nAdd persistent notes, reminders, and context here.\n`;\n\n/**\n * Ensure Notes/ and memory/ sub-directories exist under `projectRoot`.\n * Also creates a memory/MEMORY.md scaffold if it does not yet exist.\n */\nexport function scaffoldProjectDirs(projectRoot: string): void {\n const notesDir = `${projectRoot}/Notes`;\n const memoryDir = `${projectRoot}/memory`;\n const memoryFile = `${memoryDir}/MEMORY.md`;\n\n mkdirSync(notesDir, { recursive: true });\n mkdirSync(memoryDir, { recursive: true });\n\n if (!existsSync(memoryFile)) {\n writeFileSync(memoryFile, MEMORY_MD_SCAFFOLD, \"utf8\");\n }\n}\n\n// ---------------------------------------------------------------------------\n// Table rendering\n// ---------------------------------------------------------------------------\n\n/**\n * Pad a string to a minimum width (left-aligned).\n */\nexport function pad(str: string, width: number): string {\n const plain = stripAnsi(str);\n const extra = width - plain.length;\n return str + (extra > 0 ? \" \".repeat(extra) : \"\");\n}\n\n/**\n * Strip ANSI escape sequences to measure visible string length.\n */\nfunction stripAnsi(str: string): string {\n // eslint-disable-next-line no-control-regex\n return str.replace(/\\x1B\\[[0-9;]*m/g, \"\");\n}\n\n/**\n * Render a simple columnar table.\n *\n * @param headers Column header strings\n * @param rows Array of row arrays (each cell is a string, may include chalk sequences)\n */\nexport function renderTable(headers: string[], rows: string[][]): string {\n const allRows = [headers, ...rows];\n\n // Compute column widths\n const widths = headers.map((_, colIdx) =>\n Math.max(...allRows.map((row) => stripAnsi(row[colIdx] ?? \"\").length))\n );\n\n const divider = dim(\" \" + widths.map((w) => \"-\".repeat(w)).join(\" \"));\n const renderRow = (row: string[], isHeader = false) => {\n const cells = widths.map((w, i) => {\n const cell = row[i] ?? \"\";\n // pad() already strips ANSI internally to compute visible length,\n // so pass the target visible width directly — no fudge factor needed.\n return isHeader ? pad(bold(cell), w) : pad(cell, w);\n });\n return \" \" + cells.join(\" \");\n };\n\n const lines: string[] = [];\n lines.push(renderRow(headers, true));\n lines.push(divider);\n for (const row of rows) {\n lines.push(renderRow(row));\n }\n return lines.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Date helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Shorten an absolute path for display: replace home dir with ~,\n * truncate from left if still longer than maxLen.\n */\nexport function shortenPath(absolutePath: string, maxLen = 40): string {\n const home = homedir();\n let p = absolutePath;\n if (p.startsWith(home)) {\n p = \"~\" + p.slice(home.length);\n }\n if (p.length <= maxLen) return p;\n return \"...\" + p.slice(p.length - maxLen + 3);\n}\n\n/**\n * Format an epoch milliseconds timestamp as YYYY-MM-DD.\n */\nexport function fmtDate(epochMs: number | null | undefined): string {\n if (epochMs == null) return dim(\"—\");\n return new Date(epochMs).toISOString().slice(0, 10);\n}\n\n/**\n * Return the current epoch milliseconds.\n */\nexport function now(): number {\n return Date.now();\n}\n"],"mappings":"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"}
@@ -1,6 +1,5 @@
1
- import { r as saveQueryResult } from "./query-feedback-BIaZTTFO.mjs";
2
- import { i as generateEmbedding, n as cosineSimilarity, r as deserializeEmbedding } from "./embeddings-Bx3q0QOY.mjs";
3
- import { t as zettelThemes } from "./themes-CTaOj3e1.mjs";
1
+ import { t as STOP_WORDS$1 } from "./stop-words-DtxaTWU_.mjs";
2
+ import { i as generateEmbedding, n as cosineSimilarity, r as deserializeEmbedding } from "./embeddings-DOLZnT1X.mjs";
4
3
  import { basename, dirname } from "node:path";
5
4
 
6
5
  //#region src/zettelkasten/explore.ts
@@ -109,10 +108,10 @@ async function zettelExplore(backend, opts) {
109
108
 
110
109
  //#endregion
111
110
  //#region src/zettelkasten/surprise.ts
112
- const MAX_CHUNKS$1 = 5e3;
111
+ const MAX_CHUNKS$2 = 5e3;
113
112
  const BFS_HOP_CAP = 20;
114
113
  async function getFileEmbeddings(backend, projectId) {
115
- const rows = await backend.getChunksWithEmbeddings(projectId, MAX_CHUNKS$1);
114
+ const rows = await backend.getChunksWithEmbeddings(projectId, MAX_CHUNKS$2);
116
115
  const byPath = /* @__PURE__ */ new Map();
117
116
  for (const row of rows) {
118
117
  const vec = deserializeEmbedding(row.embedding);
@@ -453,6 +452,149 @@ Think like a scholar who has deeply internalized these ideas and is now synthesi
453
452
  };
454
453
  }
455
454
 
455
+ //#endregion
456
+ //#region src/zettelkasten/themes.ts
457
+ const MAX_CHUNKS$1 = 5e3;
458
+ function getTopFolder$1(vaultPath) {
459
+ const parts = vaultPath.split("/");
460
+ return parts.length > 1 ? parts[0] : "";
461
+ }
462
+ function generateLabel(titles) {
463
+ const wordCounts = /* @__PURE__ */ new Map();
464
+ for (const title of titles) {
465
+ if (!title) continue;
466
+ const words = title.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS$1.has(w));
467
+ for (const word of words) wordCounts.set(word, (wordCounts.get(word) ?? 0) + 1);
468
+ }
469
+ return [...wordCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([w]) => w).join(" / ");
470
+ }
471
+ async function computeLinkedRatio(backend, paths) {
472
+ if (paths.length < 2) return 0;
473
+ const totalPairs = paths.length * (paths.length - 1) / 2;
474
+ const pathSet = new Set(paths);
475
+ let linkedPairs = 0;
476
+ for (const path of paths) {
477
+ const links = await backend.getLinksFromSource(path);
478
+ for (const link of links) if (link.targetPath && pathSet.has(link.targetPath)) linkedPairs++;
479
+ }
480
+ const uniquePairs = linkedPairs / 2;
481
+ return Math.min(1, uniquePairs / totalPairs);
482
+ }
483
+ function averageEmbeddings(embeddings) {
484
+ if (embeddings.length === 0) return new Float32Array(0);
485
+ const dim = embeddings[0].length;
486
+ const sum = new Float32Array(dim);
487
+ for (const vec of embeddings) for (let i = 0; i < dim; i++) sum[i] += vec[i];
488
+ const avg = new Float32Array(dim);
489
+ for (let i = 0; i < dim; i++) avg[i] = sum[i] / embeddings.length;
490
+ return avg;
491
+ }
492
+ /**
493
+ * Detect emerging themes in recently-modified notes using agglomerative single-linkage
494
+ * clustering of note-level embeddings.
495
+ */
496
+ async function zettelThemes(backend, opts) {
497
+ const lookbackDays = opts.lookbackDays ?? 30;
498
+ const minClusterSize = opts.minClusterSize ?? 3;
499
+ const maxThemes = opts.maxThemes ?? 10;
500
+ const similarityThreshold = opts.similarityThreshold ?? .65;
501
+ const now = Date.now();
502
+ const from = now - lookbackDays * 864e5;
503
+ const recentNotes = (await backend.getRecentVaultFiles(from)).map((f) => ({
504
+ vault_path: f.vaultPath,
505
+ title: f.title,
506
+ indexed_at: f.indexedAt
507
+ }));
508
+ const chunkRows = await backend.getChunksWithEmbeddings(opts.vaultProjectId, MAX_CHUNKS$1);
509
+ const embeddingsByPath = /* @__PURE__ */ new Map();
510
+ for (const row of chunkRows) {
511
+ const vec = deserializeEmbedding(row.embedding);
512
+ const arr = embeddingsByPath.get(row.path);
513
+ if (!arr) embeddingsByPath.set(row.path, [vec]);
514
+ else arr.push(vec);
515
+ }
516
+ const fileEmbeddings = /* @__PURE__ */ new Map();
517
+ for (const [path, vecs] of embeddingsByPath) fileEmbeddings.set(path, averageEmbeddings(vecs));
518
+ const clusters = [];
519
+ for (const note of recentNotes) {
520
+ const embedding = fileEmbeddings.get(note.vault_path);
521
+ if (!embedding) continue;
522
+ clusters.push({
523
+ paths: [note.vault_path],
524
+ titles: [note.title],
525
+ indexedAts: [note.indexed_at],
526
+ centroid: embedding
527
+ });
528
+ }
529
+ const totalNotesAnalyzed = clusters.length;
530
+ let merged = true;
531
+ while (merged && clusters.length > 1) {
532
+ merged = false;
533
+ let bestSim = similarityThreshold;
534
+ let bestI = -1;
535
+ let bestJ = -1;
536
+ for (let i = 0; i < clusters.length; i++) for (let j = i + 1; j < clusters.length; j++) {
537
+ const sim = cosineSimilarity(clusters[i].centroid, clusters[j].centroid);
538
+ if (sim > bestSim) {
539
+ bestSim = sim;
540
+ bestI = i;
541
+ bestJ = j;
542
+ }
543
+ }
544
+ if (bestI === -1) break;
545
+ const ci = clusters[bestI];
546
+ const cj = clusters[bestJ];
547
+ const mergedPaths = [...ci.paths, ...cj.paths];
548
+ const mergedTitles = [...ci.titles, ...cj.titles];
549
+ const mergedIndexedAts = [...ci.indexedAts, ...cj.indexedAts];
550
+ const memberEmbeddings = [];
551
+ for (const p of mergedPaths) {
552
+ const emb = fileEmbeddings.get(p);
553
+ if (emb) memberEmbeddings.push(emb);
554
+ }
555
+ clusters[bestI] = {
556
+ paths: mergedPaths,
557
+ titles: mergedTitles,
558
+ indexedAts: mergedIndexedAts,
559
+ centroid: averageEmbeddings(memberEmbeddings)
560
+ };
561
+ clusters.splice(bestJ, 1);
562
+ merged = true;
563
+ }
564
+ const themes = [];
565
+ let clusterIndex = 0;
566
+ for (const cluster of clusters) {
567
+ if (cluster.paths.length < minClusterSize) continue;
568
+ const label = generateLabel(cluster.titles) || `Theme ${clusterIndex + 1}`;
569
+ const avgRecency = cluster.indexedAts.reduce((sum, t) => sum + t, 0) / cluster.indexedAts.length;
570
+ const folderDiversity = new Set(cluster.paths.map(getTopFolder$1)).size / cluster.paths.length;
571
+ const linkedRatio = await computeLinkedRatio(backend, cluster.paths);
572
+ const suggestIndexNote = linkedRatio < .3 && cluster.paths.length >= 5;
573
+ themes.push({
574
+ id: clusterIndex++,
575
+ label,
576
+ notes: cluster.paths.map((path, idx) => ({
577
+ path,
578
+ title: cluster.titles[idx]
579
+ })),
580
+ size: cluster.paths.length,
581
+ folderDiversity,
582
+ avgRecency,
583
+ linkedRatio,
584
+ suggestIndexNote
585
+ });
586
+ }
587
+ themes.sort((a, b) => b.size * b.folderDiversity * (b.avgRecency / now) - a.size * a.folderDiversity * (a.avgRecency / now));
588
+ return {
589
+ themes: themes.slice(0, maxThemes),
590
+ totalNotesAnalyzed,
591
+ timeWindow: {
592
+ from,
593
+ to: now
594
+ }
595
+ };
596
+ }
597
+
456
598
  //#endregion
457
599
  //#region src/zettelkasten/health.ts
458
600
  function countComponents(nodes, edges) {
@@ -1059,5 +1201,5 @@ async function zettelCommunities(backend, opts) {
1059
1201
  }
1060
1202
 
1061
1203
  //#endregion
1062
- export { zettelCommunities, zettelConverse, zettelExplore, zettelGodNotes, zettelHealth, zettelSuggest, zettelSurprise, zettelThemes };
1063
- //# sourceMappingURL=zettelkasten-CtGHQWPU.mjs.map
1204
+ export { zettelThemes as a, zettelExplore as c, zettelHealth as i, zettelGodNotes as n, zettelConverse as o, zettelSuggest as r, zettelSurprise as s, zettelCommunities as t };
1205
+ //# sourceMappingURL=zettelkasten-BhZMvmoK.mjs.map