@tekmidian/pai 1.0.0 → 1.0.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 (218) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +67 -5
  3. package/dist/aibroker-client-BxzeEHPF.mjs +3 -0
  4. package/dist/{aibroker-client-CXEYBzbf.mjs → aibroker-client-CQPGqLxL.mjs} +1 -1
  5. package/dist/atomic-file-BorMa4T3.mjs +21 -0
  6. package/dist/{auto-route-D5SXrMpB.mjs → auto-route-B4azPk4l.mjs} +3 -3
  7. package/dist/auto-route-BjoMHKyI.mjs +11 -0
  8. package/dist/budget-Dod6rnA9.mjs +15 -0
  9. package/dist/{caller-identity-CVTuiLSs.mjs → caller-identity-yLFb7CKq.mjs} +6 -5
  10. package/dist/{capacity-DaXrVj3J.mjs → capacity-DEveQdFV.mjs} +3 -3
  11. package/dist/{chain-BcQlIlEd.mjs → chain-D-yvET_y.mjs} +5 -5
  12. package/dist/cli/index.mjs +1621 -357
  13. package/dist/cockpit-actions-BWunx3rY.mjs +20 -0
  14. package/dist/{cockpit-actions-ODmt8yF9.mjs → cockpit-actions-C8GlTzts.mjs} +84 -42
  15. package/dist/cockpit-memory-vTlFWV7F.mjs +20 -0
  16. package/dist/{config-BSXKyeJ1.mjs → config-Bu1_vbY8.mjs} +42 -3
  17. package/dist/{config-B955yYmo.mjs → config-Cxi-aBf4.mjs} +2 -2
  18. package/dist/{config-TaWmdTLx.mjs → config-DtENJ6cL.mjs} +1 -1
  19. package/dist/{config-D5chi2LO.mjs → config-XpZWOYOG.mjs} +2 -2
  20. package/dist/{controls-BGkK_Bdi.mjs → controls-BcKFHeG1.mjs} +37 -22
  21. package/dist/daemon/index.mjs +43 -42
  22. package/dist/{daemon-CorGh6Ub.mjs → daemon-ColVHwGZ.mjs} +103 -55
  23. package/dist/daemon-N7WhYi6I.mjs +45 -0
  24. package/dist/daemon-mcp/index.mjs +40 -39
  25. package/dist/{define-DGFu-sCf.mjs → define-CjJsmq-6.mjs} +1 -1
  26. package/dist/{detect-C1d5cUkT.mjs → detect-9CZNO0yf.mjs} +2 -2
  27. package/dist/detect-D1ziFFfA.mjs +10 -0
  28. package/dist/{embed-pass-CeQD8kRE.mjs → embed-pass-DIWjA68P.mjs} +5 -5
  29. package/dist/{embedding-gate-BHuBsgHD.mjs → embedding-gate-6rZrWRXa.mjs} +5 -5
  30. package/dist/{embedding-gate-Z7CJSOJk.mjs → embedding-gate-Cnk4YCyX.mjs} +3 -3
  31. package/dist/{embeddings-D4t6L-EH.mjs → embeddings-BVq3esBF.mjs} +1 -1
  32. package/dist/{embeddings-OF9Pt4by.mjs → embeddings-CIkJBKd-.mjs} +1 -1
  33. package/dist/{endreason-x1KOSrcO.mjs → endreason-C577xIQz.mjs} +7 -6
  34. package/dist/{env-DArqGq-Z.mjs → env-BE8mrsxr.mjs} +1 -1
  35. package/dist/factory-Bsib-qDG.mjs +9 -0
  36. package/dist/{factory-BtjbQ0np.mjs → factory-DjAAsqr_.mjs} +15 -13
  37. package/dist/{fallback-BIyEpPsQ.mjs → fallback-DFlItw1Z.mjs} +5 -5
  38. package/dist/{fmt-age-BiSHrTKN.mjs → fmt-age-ByDkvkoU.mjs} +1 -1
  39. package/dist/{health-C6jZWvRG.mjs → health-Dnym-qcT.mjs} +4 -4
  40. package/dist/{helpers-DFmYRCOa.mjs → helpers-BD6V-p9w.mjs} +2 -2
  41. package/dist/helpers-CMGjy_0N.mjs +10 -0
  42. package/dist/{helpers-CJpb_MNB.mjs → helpers-CRuBUas3.mjs} +87 -38
  43. package/dist/hooks/capture-all-events.mjs +52 -11
  44. package/dist/hooks/capture-session-summary.mjs +64 -47
  45. package/dist/hooks/capture-tool-output.mjs +49 -8
  46. package/dist/hooks/context-compression-hook.mjs +30 -10
  47. package/dist/hooks/initialize-session.mjs +1 -0
  48. package/dist/hooks/load-core-context.mjs +1 -1
  49. package/dist/hooks/load-project-context.mjs +32 -29
  50. package/dist/hooks/security-validator.mjs +45 -13
  51. package/dist/hooks/stop-hook.mjs +54 -16
  52. package/dist/hooks/supervision-banner.mjs +9 -0
  53. package/dist/hooks/sync-todo-to-md.mjs +147 -113
  54. package/dist/hooks/worker-edit-repair.mjs +2 -1
  55. package/dist/hooks/worker-guard.mjs +40 -16
  56. package/dist/hooks/worker-note.mjs +12 -5
  57. package/dist/hooks/worker-path-repair.mjs +1 -1
  58. package/dist/hooks/worker-pool-host.mjs +214 -116
  59. package/dist/hooks/worker-status-line.mjs +195 -114
  60. package/dist/index.d.mts +18 -0
  61. package/dist/index.mjs +12 -12
  62. package/dist/{indexer-backend-BM2CUSu8.mjs → indexer-backend-_a6UNhXJ.mjs} +149 -96
  63. package/dist/{ipc-client-BMqWw_gv.mjs → ipc-client-CaiGJnSM.mjs} +1 -1
  64. package/dist/{ipc-client-YYDD6zzU.mjs → ipc-client-ZRmDKd5Z.mjs} +1 -1
  65. package/dist/kill-tree-BG7dRVtp.mjs +10 -0
  66. package/dist/{kill-tree-BjYz5BxK.mjs → kill-tree-EWYaEk5v.mjs} +8 -8
  67. package/dist/{launch-CLp0OJxV.mjs → launch-CXVlZjHt.mjs} +8 -8
  68. package/dist/{launch-DgqQdAjI.mjs → launch-Jh5NVeNe.mjs} +3 -3
  69. package/dist/local-candidate-DLdwQt0s.mjs +112 -0
  70. package/dist/local-instances-DOxSgT_L.mjs +210 -0
  71. package/dist/local-instances-ZeRkkuS5.mjs +6 -0
  72. package/dist/{local-tiers-DPZYqv9m.mjs → local-tiers-BE4jGaMa.mjs} +2 -2
  73. package/dist/{local-tiers-DnBIAkbK.mjs → local-tiers-DNMI5W86.mjs} +3 -3
  74. package/dist/{main-config-ops-C0SflOWk.mjs → main-config-ops-BEMpuhDz.mjs} +3 -3
  75. package/dist/{main-resolver-DVPpSWVy.mjs → main-resolver-3Ns1HPmS.mjs} +18 -17
  76. package/dist/{migrate-BUJHadA8.mjs → migrate-DrA_OwlZ.mjs} +45 -14
  77. package/dist/min-max-DrSRUaWQ.mjs +16 -0
  78. package/dist/{model-inventory-Dasber3S.mjs → model-inventory-BFVSNFT3.mjs} +13 -12
  79. package/dist/{models-add-Bt7aUHDs.mjs → models-add-BtB3Se6f.mjs} +39 -38
  80. package/dist/operator-BTqvTIc8.mjs +7 -0
  81. package/dist/{operator-DZxcP-g8.mjs → operator-CyuB3aVw.mjs} +3 -3
  82. package/dist/{pai-marker-kIYuNt3c.mjs → pai-marker-DurzIbw8.mjs} +7 -6
  83. package/dist/{pane-audit-OGYrOAik.mjs → pane-audit-BQytYa60.mjs} +7 -7
  84. package/dist/{paths-DoRuFc60.mjs → paths-CFHTloSe.mjs} +3 -3
  85. package/dist/{paths-DoIyRnhx.mjs → paths-CHifubDh.mjs} +3 -3
  86. package/dist/{planner-DLGOYlVb.mjs → planner-BM27GBfC.mjs} +31 -30
  87. package/dist/{pool-CZ5MZaXg.mjs → pool-BRvoOkv6.mjs} +3 -3
  88. package/dist/{pool-cli-BxNSPzzY.mjs → pool-cli-B_YR8_Gl.mjs} +5 -5
  89. package/dist/{pool-cli-CbJfl37y.mjs → pool-cli-DkZTAlSW.mjs} +6 -6
  90. package/dist/postgres-BdBNkL_l.mjs +7 -0
  91. package/dist/{postgres-CDnd_k5f.mjs → postgres-C6ti4haA.mjs} +55 -34
  92. package/dist/{project-setup-DM4npnL3.mjs → project-setup-CfpApz5o.mjs} +131 -74
  93. package/dist/providers-BeDiVEb-.mjs +16 -0
  94. package/dist/{providers-UsKtLcUR.mjs → providers-CblTFrhz.mjs} +6 -6
  95. package/dist/{query-feedback-I-txSnld.mjs → query-feedback-BRfPtqki.mjs} +2 -2
  96. package/dist/registrable-C7b6givx.mjs +9 -0
  97. package/dist/{registrable-LrRIVYQc.mjs → registrable-Uzgmfyhz.mjs} +4 -4
  98. package/dist/render-Cc2j5QG9.mjs +13 -0
  99. package/dist/{render-BDbb4CF-.mjs → render-CwIrgxLi.mjs} +8 -8
  100. package/dist/{reranker-B4r4vykM.mjs → reranker-8g6xMwP7.mjs} +1 -1
  101. package/dist/{reranker-DxxMCtA0.mjs → reranker-CqCbpGnX.mjs} +1 -1
  102. package/dist/{root-BMeJuHOb.mjs → root-BWy0c2Lq.mjs} +11 -11
  103. package/dist/root-wC_6Fx0B.mjs +36 -0
  104. package/dist/route-table-B7zpoypi.mjs +15 -0
  105. package/dist/{router-L8Nu-OHF.mjs → router-CQSc3lk3.mjs} +17 -7
  106. package/dist/{router-D6Ec6Fid.mjs → router-DwHJRgG9.mjs} +1 -1
  107. package/dist/{routing-CUyiglfu.mjs → routing-BAAST9vT.mjs} +27 -308
  108. package/dist/{routing-ZuGlc0ge.mjs → routing-CENt4HH1.mjs} +13 -12
  109. package/dist/{run-ulAi5dFD.mjs → run-6Q9kCPem.mjs} +35 -33
  110. package/dist/{scan-B7I0gPe-.mjs → scan-ZQWm-_se.mjs} +6 -6
  111. package/dist/{local-candidate-jOjTNlOL.mjs → scheduler-CtUyWJ3O.mjs} +7 -109
  112. package/dist/scheduler-dmHXoRvm.mjs +3 -0
  113. package/dist/{scope-D5VSQxe4.mjs → scope-D-h6F6DN.mjs} +4 -4
  114. package/dist/{search-CPoE0830.mjs → search-RSyeQqj1.mjs} +18 -18
  115. package/dist/session-Bok2PqRS.mjs +35 -0
  116. package/dist/{session-CNXF6DLP.mjs → session-DVUExUnX.mjs} +55 -31
  117. package/dist/session-scan-BJI_ZO8M.mjs +9 -0
  118. package/dist/{session-scan-BDLEc4w1.mjs → session-scan-W-gbwJHM.mjs} +6 -6
  119. package/dist/{sources-CjhUKKtO.mjs → sources-ESUkZaWv.mjs} +1 -1
  120. package/dist/{status-DYydKQUI.mjs → status-Dm4dYciN.mjs} +4 -4
  121. package/dist/{status-CvVi9cDf.mjs → status-ggJ7lgVg.mjs} +17 -9
  122. package/dist/{stop-words-SdSL1WKH.mjs → stop-words-DSEYQdNe.mjs} +1 -1
  123. package/dist/{test-isolation-CoDeOVXb.mjs → test-isolation-lqSmYxr0.mjs} +1 -1
  124. package/dist/{timeouts-OwEONmc8.mjs → timeouts-1MLooEBB.mjs} +1 -1
  125. package/dist/{tool-errors-AeRQuG3Y.mjs → tool-errors-7E13StWd.mjs} +4 -3
  126. package/dist/{tool-errors-R5E0fCgf.mjs → tool-errors-CJWODvlo.mjs} +6 -6
  127. package/dist/{viewer-DtWj5Vti.mjs → viewer-DcpJwGKE.mjs} +15 -15
  128. package/dist/viewer-MWBVJP3u.mjs +22 -0
  129. package/dist/{wait-BMUR-U-U.mjs → wait-ByV0OyMZ.mjs} +3 -3
  130. package/dist/{worker-config-BLqa1yiD.mjs → worker-config-BDYNxbYz.mjs} +294 -92
  131. package/dist/worker-config-gmAq_-rI.mjs +41 -0
  132. package/dist/{workers-config-DEI9lrV4.mjs → workers-config-f8KrkY4L.mjs} +2 -2
  133. package/dist/{worktree-zmRf556h.mjs → worktree-CrnOqTBW.mjs} +37 -7
  134. package/dist/{zettelkasten-nigq7fqs.mjs → zettelkasten-B_rRRRvD.mjs} +5 -5
  135. package/dist/{zettelkasten-Cr_Xj0mG.mjs → zettelkasten-BzMm8REH.mjs} +29 -19
  136. package/docs/README.md +2 -0
  137. package/docs/cockpit.md +10 -4
  138. package/docs/commands/README.md +4 -4
  139. package/docs/commands/backup.md +2 -0
  140. package/docs/commands/install.md +29 -4
  141. package/docs/commands/providers.md +1 -1
  142. package/docs/commands/restore.md +4 -0
  143. package/docs/commands/setup.md +29 -4
  144. package/docs/commands/worker.md +1 -1
  145. package/docs/config.md +2 -0
  146. package/docs/features.md +299 -0
  147. package/docs/images/architecture.drawio +51 -0
  148. package/docs/images/architecture.svg +3 -0
  149. package/docs/images/cockpit-01-system-tokens.jpg +0 -0
  150. package/docs/images/cockpit-02-totals-cache.jpg +0 -0
  151. package/docs/images/cockpit-03-local-servers-models-pool.jpg +0 -0
  152. package/docs/images/cockpit-04-workers-memory.jpg +0 -0
  153. package/docs/images/cockpit-05-performance.jpg +0 -0
  154. package/docs/images/cockpit-06-runs-speed-routing.jpg +0 -0
  155. package/docs/images/cockpit-07-settings-registry.jpg +0 -0
  156. package/docs/install/index.md +29 -0
  157. package/docs/three-tier-coding.md +1 -1
  158. package/gemini-extension.json +1 -1
  159. package/package.json +1 -5
  160. package/pai-plugin.json +1 -1
  161. package/plugins/context-preservation/plugin.json +1 -1
  162. package/plugins/core/plugin.json +1 -1
  163. package/plugins/core/skills/setup/SKILL.md +41 -9
  164. package/plugins/creative/plugin.json +1 -1
  165. package/plugins/observability/plugin.json +1 -1
  166. package/plugins/productivity/plugin.json +1 -1
  167. package/plugins/semantic-search/plugin.json +1 -1
  168. package/plugins/ui/plugin.json +1 -1
  169. package/plugins/zettelkasten/plugin.json +1 -1
  170. package/src/hooks/session-autosave.sh +8 -1
  171. package/src/hooks/session-stop.sh +8 -1
  172. package/src/hooks/ts/capture-all-events.ts +7 -12
  173. package/src/hooks/ts/lib/context-fill.ts +2 -1
  174. package/src/hooks/ts/lib/path-repair.ts +1 -1
  175. package/src/hooks/ts/lib/project-utils/notify.ts +1 -0
  176. package/src/hooks/ts/lib/project-utils/session-notes.ts +23 -6
  177. package/src/hooks/ts/lib/raw-capture.ts +36 -0
  178. package/src/hooks/ts/lib/session-summary.ts +129 -0
  179. package/src/hooks/ts/lib/todo-sync.ts +120 -0
  180. package/src/hooks/ts/lib/worker-git-guard.ts +1 -1
  181. package/src/hooks/ts/post-tool-use/capture-tool-output.ts +6 -10
  182. package/src/hooks/ts/post-tool-use/sync-todo-to-md.ts +5 -105
  183. package/src/hooks/ts/pre-compact/context-compression-hook.ts +1 -1
  184. package/src/hooks/ts/pre-tool-use/security-validator.ts +35 -14
  185. package/src/hooks/ts/session-end/capture-session-summary.ts +20 -128
  186. package/src/hooks/ts/session-start/load-core-context.ts +3 -3
  187. package/src/hooks/ts/session-start/load-project-context.ts +18 -22
  188. package/src/hooks/ts/stop/stop-hook.ts +13 -5
  189. package/src/hooks/ztk-auto.sh +7 -2
  190. package/dist/aibroker-client-JjE74jo0.mjs +0 -3
  191. package/dist/auto-route-BRk8NEFv.mjs +0 -11
  192. package/dist/budget-DuhQ2zzG.mjs +0 -14
  193. package/dist/cockpit-actions-DcQ-ucU8.mjs +0 -19
  194. package/dist/cockpit-memory-KV45PPO2.mjs +0 -19
  195. package/dist/daemon-C7yt_ptC.mjs +0 -44
  196. package/dist/detect-DRUkmHnb.mjs +0 -10
  197. package/dist/factory-DAGG9_n8.mjs +0 -9
  198. package/dist/helpers-DPJEHPq6.mjs +0 -10
  199. package/dist/kill-tree-LV0Q7Mte.mjs +0 -10
  200. package/dist/operator-CfL0HiBK.mjs +0 -7
  201. package/dist/postgres-DgqWyqhN.mjs +0 -7
  202. package/dist/providers-CTu2s1z2.mjs +0 -15
  203. package/dist/registrable-D9aNZOTh.mjs +0 -9
  204. package/dist/render-BSfVlYHj.mjs +0 -13
  205. package/dist/root-xI9DqVIe.mjs +0 -35
  206. package/dist/route-table-HjvnLVEU.mjs +0 -14
  207. package/dist/session-BJHzmUg2.mjs +0 -34
  208. package/dist/session-scan-YORKE1NO.mjs +0 -9
  209. package/dist/viewer-B8709jDN.mjs +0 -22
  210. package/dist/worker-config-D8tRuvo7.mjs +0 -40
  211. package/docs/images/cockpit-limits-routing.jpg +0 -0
  212. package/docs/images/cockpit-models.jpg +0 -0
  213. package/docs/images/cockpit-usage.jpg +0 -0
  214. package/docs/images/cockpit-workers-system.jpg +0 -0
  215. package/scripts/build-docs.mjs +0 -41
  216. package/scripts/build-hooks.mjs +0 -311
  217. package/scripts/build-skill-stubs.mjs +0 -216
  218. package/scripts/copy-defaults.mjs +0 -46
@@ -52,6 +52,35 @@ pai daemon logs
52
52
  pai daemon serve # foreground, where there is no service manager
53
53
  ```
54
54
 
55
+ ### AIBroker (messaging)
56
+
57
+ [AIBroker](https://github.com/mnott/AIBroker) is a separate project and PAI's messaging layer: messages between sessions, worker reports to the session that started them, terminal tab names, and WhatsApp, Telegram and PAILot. PAI runs without it, but none of these work until it is installed:
58
+
59
+ ```bash
60
+ npm install -g aibroker@^0.62.4
61
+ aibroker setup # service, MCP registration, Claude Code hooks
62
+ aibroker doctor # every check green, or it says exactly what to fix
63
+ ```
64
+
65
+ ### Setup questions and flags
66
+
67
+ `pai setup` asks the questions below in this order. Every one has a flag, so `pai setup --yes <flags>` answers any of them without a terminal; a question takes its default when the flag is absent, except one that has no default (for example the Todoist token with `--task-bus`), which needs the value as a flag or in the config. `pai setup --questions` (add `--json` for machines) prints this list for the current machine, with the value already in place where there is one. The setup skill uses it: it runs `pai setup --questions --json`, asks you in chat, then runs `pai setup --yes` with the matching flags.
68
+
69
+ | Question | Flag | Default | Allowed |
70
+ |---|---|---|---|
71
+ | Where to store data | `--storage` | `sqlite` (`postgres` if a local one answers) | `sqlite`, `postgres` |
72
+ | PostgreSQL connection string | `--pg-url` | the PAI Docker container | a `postgresql://` URL (implies `--storage postgres`) |
73
+ | Embedding model | `--embedding` | `snowflake` (Ollama is used when it is faster) | `snowflake`, `bge`, `nomic`, `ollama`, `none` |
74
+ | CLAUDE.md, PAI skill, steering rules, skill stubs, hooks, TypeScript hooks, settings.json, daemon, MCP, workers | `--no-claude-md`, `--no-skill`, `--no-steering-rules`, `--no-skill-stubs`, `--no-hooks`, `--no-ts-hooks`, `--no-settings`, `--no-daemon`, `--no-mcp`, `--no-workers` | yes | |
75
+ | Registry scan after setup, task bus (Todoist) | `--registry-scan`, `--task-bus` | no | |
76
+ | Assistant name | `--da-name` | `PAI` | any text |
77
+ | Router and cockpit | `--router` / `--no-router` | enabled | |
78
+ | Cockpit port | `--router-port` | `router.port` (8790); a port that is in use is refused and its holder shown | 1024 to 65535 |
79
+ | Who can reach the cockpit | `--router-bind` | `loopback` | `loopback`, `tailscale`, an IPv4 address (never 0.0.0.0) |
80
+ | AIBroker | `--no-aibroker` | installed | |
81
+
82
+ The router answers land in `config.yaml` as `router.enabled`, `router.port` and `router.bind` (see [config](../config.md)). Beyond `loopback` the cockpit is reachable from other devices, behind the token in `~/.pai/router-cockpit.token`.
83
+
55
84
  ### Updating
56
85
 
57
86
  ```bash
@@ -109,7 +109,7 @@ pai worker pending # finished branches not merged yet
109
109
  The router serves a cockpit page at `http://127.0.0.1:8790/cockpit`. It reads the router's usage log and the worker status
110
110
  files and shows, for a chosen window:
111
111
 
112
- ![The PAI Cockpit: cloud limits, local model status, system load and power, worker pool, tokens and cache hit ratio per provider, speed per model](images/cockpit-limits-routing.jpg)
112
+ ![The PAI Cockpit: worker runs, speed per model, and the routing of the three tiers](images/cockpit-06-runs-speed-routing.jpg)
113
113
 
114
114
 
115
115
  - **Cloud** and **Local** totals, **Providers**, **Speed per model**;
@@ -2,7 +2,7 @@
2
2
  "name": "@tekmidian/pai",
3
3
  "displayName": "PAI Knowledge OS",
4
4
  "description": "Personal AI Infrastructure — persistent memory and session continuity for AI coding assistants",
5
- "version": "1.0.0",
5
+ "version": "1.0.2",
6
6
  "author": "Matthias Nott",
7
7
  "license": "MIT",
8
8
  "homepage": "https://github.com/mnott/PAI",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekmidian/pai",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "PAI Knowledge OS — Personal AI Infrastructure with federated memory and project management",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -23,10 +23,6 @@
23
23
  "gemini-extension.json",
24
24
  "statusline-command.sh",
25
25
  "tab-color-command.sh",
26
- "scripts/build-hooks.mjs",
27
- "scripts/build-skill-stubs.mjs",
28
- "scripts/build-docs.mjs",
29
- "scripts/copy-defaults.mjs",
30
26
  "docker/init.sql",
31
27
  "docker/docker-compose.yml",
32
28
  "src/hooks",
package/pai-plugin.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "@tekmidian/pai",
4
4
  "displayName": "PAI Knowledge OS",
5
5
  "description": "Personal AI Infrastructure — persistent memory, session continuity, and knowledge graph",
6
- "version": "1.0.0",
6
+ "version": "1.0.2",
7
7
  "author": "Matthias Nott",
8
8
  "license": "MIT",
9
9
  "homepage": "https://github.com/mnott/PAI",
@@ -2,6 +2,6 @@
2
2
  "name": "pai-context-preservation",
3
3
  "displayName": "PAI Context Preservation",
4
4
  "description": "Context compression and relay across compaction cycles",
5
- "version": "1.0.0",
5
+ "version": "1.0.2",
6
6
  "hooks": "./hooks/hooks.json"
7
7
  }
@@ -2,6 +2,6 @@
2
2
  "name": "pai-core",
3
3
  "displayName": "PAI Core",
4
4
  "description": "Core memory engine, session management, and project registry",
5
- "version": "1.0.0",
5
+ "version": "1.0.2",
6
6
  "hooks": "./hooks/hooks.json"
7
7
  }
@@ -9,7 +9,9 @@ description: >
9
9
 
10
10
  # PAI setup
11
11
 
12
- Work through the phases in order. Run the check of a phase first; if it already passes, say so and move on. Prefer `pai` commands over editing files by hand; `pai <command> --help` is the reference. Ask the user before anything that installs a service, needs an API key, or starts a large download. Never print API keys back.
12
+ Work through the phases in order. Run the check of a phase first; if it already passes, say so and move on.
13
+
14
+ **One install route, however the user started.** Whether they said "clone https://github.com/mnott/PAI and set it up for me", installed the plugin, or asked from scratch, install the same way: the released npm package (phase 2), the `pai` plugin from the marketplace (phase 2a), then `pai setup` (phase 3), which also installs AIBroker. A clone of the repository is only where these instructions came from: do not build, link or run PAI from it (that would run unreleased code). Tell the user they can delete the clone afterwards, or keep it if they want to work on PAI itself. Prefer `pai` commands over editing files by hand; `pai <command> --help` is the reference. Ask the user before anything that installs a service, needs an API key, or starts a large download — except AIBroker, which setup installs without asking because PAI's messaging depends on it. Never print API keys back.
13
15
 
14
16
  ## 1. Prerequisites
15
17
 
@@ -33,18 +35,36 @@ npm i -g @tekmidian/pai@latest # upgrade
33
35
 
34
36
  Verify: `pai --version` prints a version and `pai more` lists commands. If `pai` is not found, add `$(npm prefix -g)/bin` to PATH in the shell profile and open a new shell.
35
37
 
38
+ ## 2a. Install the plugin
39
+
40
+ Check: `claude plugin list` shows `pai`.
41
+
42
+ ```
43
+ claude plugin marketplace add mnott/PAI
44
+ claude plugin install pai@pai
45
+ ```
46
+
47
+ The plugin brings PAI's skills (this one included) and keeps them current with `claude plugin update pai`. Tell the user the plugin is active from the next Claude Code session on.
48
+
36
49
  ## 3. Run setup
37
50
 
51
+ Never run the interactive `pai setup` from a Bash tool: there is no terminal, so it cannot be answered. Ask the user in chat instead:
52
+
38
53
  ```
39
- pai setup # interactive
40
- pai setup --yes --storage sqlite
54
+ pai setup --questions --json
41
55
  ```
42
56
 
43
- Tell the user that setup also installs the background daemon as a login service (launchd on macOS, a systemd user unit on Linux), and ask before running it. Setup covers storage, search embeddings, Claude Code hooks and settings, the background daemon and the first index. It is safe to run again. `--yes` takes every default; without `--storage` it picks sqlite unless a local Postgres answers.
57
+ It prints every question setup would ask on this machine, in order, each with `id`, `step`, `question`, `flag`, `default`, `allowed` and, where the machine already has an answer (storage, embedding, router), `current`. Ask the user those questions in chat, grouped by `step`, with each default pre-filled; accept "defaults" for all of them, and skip questions that have a `current` value unless the user wants to change it. Then run setup once with the matching flags (omit a flag to take its default):
58
+
59
+ ```
60
+ pai setup --yes --storage sqlite --embedding none --router-port 9000 --router-bind loopback --no-hooks
61
+ ```
62
+
63
+ Tell the user that setup also installs the background daemon as a login service (launchd on macOS, a systemd user unit on Linux) and AIBroker, and that the cockpit is reachable from other devices when its reach is not `loopback`. Setup covers storage, search embeddings, Claude Code hooks and settings, the background daemon, the router and cockpit, and the first index. It is safe to run again. Without `--storage`, `--yes` picks sqlite unless a local Postgres answers.
44
64
 
45
65
  Storage choice, ask the user:
46
66
  - **sqlite**: keyword search, no extra software. Right for most people and for small machines.
47
- - **postgres**: keyword plus semantic search. Needs Docker; setup starts a `pai-pgvector` container itself. On a small machine export `PAI_PG_SHARED_BUFFERS=256MB` first.
67
+ - **postgres**: keyword plus semantic search. Needs Docker; setup starts a `pai-pgvector` container itself (or pass `--pg-url`). On a small machine export `PAI_PG_SHARED_BUFFERS=256MB` first.
48
68
 
49
69
  Verify: setup ends without an error and `pai memory search "test"` runs (it prints hits or no results, but no error; results appear once the first index is done). If it stops, fix the named cause and run `pai setup` again.
50
70
 
@@ -61,6 +81,16 @@ pai daemon restart
61
81
 
62
82
  On Linux without systemd (containers) run `pai daemon serve` in a terminal or process manager instead. If it still fails, read `pai daemon logs`.
63
83
 
84
+ ## 4a. AIBroker (messaging)
85
+
86
+ Check: `aibroker doctor` exits 0. AIBroker is a separate package and PAI's messaging layer (messages between sessions, worker reports, terminal tab names, WhatsApp, Telegram, PAILot). `pai setup` installs it automatically (unless run with `--no-aibroker`); this phase only confirms it. If `aibroker` is not on the PATH or doctor fails:
87
+
88
+ ```
89
+ npm install -g aibroker@^0.62.4
90
+ aibroker setup # service, MCP registration, Claude Code hooks
91
+ aibroker doctor
92
+ ```
93
+
64
94
  ## 5. Workers: first provider
65
95
 
66
96
  Workers are Claude Code runs on a provider you choose. Setup turns them on with the built-in `anthropic` provider.
@@ -97,15 +127,16 @@ Verify: the table shows a model you have access to on every row. If a row names
97
127
 
98
128
  ## 7. Optional: router and cockpit
99
129
 
100
- Skip unless the user wants the cockpit. The router is off by default. It gives usage accounting, the local-model scheduler and the cockpit page.
130
+ The router and cockpit are asked in phase 3 (`router`, `router-port`, `router-bind` in `pai setup --questions`; flags `--router`/`--no-router`, `--router-port`, `--router-bind`), so this phase only confirms them, or changes them later. Default port 8790, reach `loopback`; `tailscale` or an address makes the cockpit reachable from other devices, so say so before choosing it. It gives usage accounting, the local-model scheduler and the cockpit page.
101
131
 
102
132
  ```
103
- pai config set router.enabled true
133
+ pai config set router.enabled true # only if phase 3 left it off
134
+ pai config set router.port 9000 # only to change the port
104
135
  pai reload
105
136
  pai router status
106
137
  ```
107
138
 
108
- Then open `http://127.0.0.1:8790/cockpit` in a browser.
139
+ Then open `http://127.0.0.1:<port>/cockpit` (8790 unless changed) in a browser.
109
140
 
110
141
  Verify: `pai router status` says running and the page loads. If not, `pai router start`, then check that nothing else uses port 8790.
111
142
 
@@ -136,7 +167,7 @@ pai reload
136
167
  Report each line as pass or fail with the command output:
137
168
 
138
169
  1. `node --version` prints v20 or newer; `claude --version` prints a version
139
- 2. `pai --version` prints a version
170
+ 2. `pai --version` prints a version; `claude plugin list` shows `pai`
140
171
  3. `pai daemon status` prints `Daemon running`
141
172
  4. `pai memory search "test"` ends without error
142
173
  5. `pai worker providers test` prints a reply and a time (`pai worker providers test anthropic` probes the local `claude` CLI; no key involved)
@@ -144,5 +175,6 @@ Report each line as pass or fail with the command output:
144
175
  7. `pai worker route` shows a model on every tier
145
176
  8. Only if phase 7 was done: `pai router status` says running and the cockpit page loads
146
177
  9. `pai` opens the session picker (Esc quits)
178
+ 10. `aibroker doctor` exits 0
147
179
 
148
180
  Details: `docs/install/index.md`, `docs/install/first-workers.md`.
@@ -2,5 +2,5 @@
2
2
  "name": "pai-creative",
3
3
  "displayName": "PAI Creative Studio",
4
4
  "description": "Art direction, story explanation, voice/prosody configuration",
5
- "version": "1.0.0"
5
+ "version": "1.0.2"
6
6
  }
@@ -2,6 +2,6 @@
2
2
  "name": "pai-observability",
3
3
  "displayName": "PAI Observability Suite",
4
4
  "description": "Automatic observation capture, classification, session summaries, and search history",
5
- "version": "1.0.0",
5
+ "version": "1.0.2",
6
6
  "hooks": "./hooks/hooks.json"
7
7
  }
@@ -2,7 +2,7 @@
2
2
  "name": "pai-productivity",
3
3
  "displayName": "PAI Productivity",
4
4
  "description": "Planning, journaling, reviewing, research, sharing, and cross-session task routing",
5
- "version": "1.0.0",
5
+ "version": "1.0.2",
6
6
  "hooks": "./hooks/hooks.json",
7
7
  "skills": ["./skills/"]
8
8
  }
@@ -2,5 +2,5 @@
2
2
  "name": "pai-semantic-search",
3
3
  "displayName": "PAI Semantic Search",
4
4
  "description": "Vector search with pgvector, cross-encoder reranking, hybrid mode, recency boost",
5
- "version": "1.0.0"
5
+ "version": "1.0.2"
6
6
  }
@@ -2,6 +2,6 @@
2
2
  "name": "pai-ui",
3
3
  "displayName": "PAI UI Customization",
4
4
  "description": "Terminal tab titles, statusline, tab coloring, and visual customization",
5
- "version": "1.0.0",
5
+ "version": "1.0.2",
6
6
  "hooks": "./hooks/hooks.json"
7
7
  }
@@ -2,5 +2,5 @@
2
2
  "name": "pai-zettelkasten",
3
3
  "displayName": "PAI Zettelkasten Intelligence",
4
4
  "description": "Luhmann-inspired vault intelligence with graph operations and Obsidian integration",
5
- "version": "1.0.0"
5
+ "version": "1.0.2"
6
6
  }
@@ -49,7 +49,14 @@ fi
49
49
 
50
50
  CLAUDE_SESSION_UUID=""
51
51
  if [ ! -t 0 ]; then
52
- HOOK_INPUT=$(timeout 2 cat 2>/dev/null || true)
52
+ if command -v timeout &>/dev/null; then
53
+ HOOK_INPUT=$(timeout 2 cat 2>/dev/null || true)
54
+ elif command -v perl &>/dev/null; then
55
+ # stock macOS has no GNU timeout
56
+ HOOK_INPUT=$(perl -e 'alarm 2; exec "cat"' 2>/dev/null || true)
57
+ else
58
+ HOOK_INPUT=$(cat 2>/dev/null || true)
59
+ fi
53
60
  if [ -n "$HOOK_INPUT" ]; then
54
61
  if command -v jq &>/dev/null; then
55
62
  CLAUDE_SESSION_UUID=$(printf '%s' "$HOOK_INPUT" | jq -r '.session_id // empty' 2>/dev/null || true)
@@ -27,7 +27,14 @@ command -v "$PAI_OS" &>/dev/null || exit 0
27
27
 
28
28
  CLAUDE_SESSION_UUID=""
29
29
  if [ ! -t 0 ]; then
30
- HOOK_INPUT=$(timeout 2 cat 2>/dev/null || true)
30
+ if command -v timeout &>/dev/null; then
31
+ HOOK_INPUT=$(timeout 2 cat 2>/dev/null || true)
32
+ elif command -v perl &>/dev/null; then
33
+ # stock macOS has no GNU timeout
34
+ HOOK_INPUT=$(perl -e 'alarm 2; exec "cat"' 2>/dev/null || true)
35
+ else
36
+ HOOK_INPUT=$(cat 2>/dev/null || true)
37
+ fi
31
38
  if [ -n "$HOOK_INPUT" ]; then
32
39
  if command -v jq &>/dev/null; then
33
40
  CLAUDE_SESSION_UUID=$(printf '%s' "$HOOK_INPUT" | jq -r '.session_id // empty' 2>/dev/null || true)
@@ -6,11 +6,12 @@
6
6
  * Enhanced with agent instance metadata extraction
7
7
  */
8
8
 
9
- import { readFileSync, appendFileSync, mkdirSync, existsSync } from 'fs';
9
+ import { readFileSync, existsSync } from 'fs';
10
10
  import { join } from 'path';
11
11
  import { historyDir, agentSessionsPath } from './lib/pai-paths.js';
12
12
  import { enrichEventWithAgentMetadata, isAgentSpawningCall } from './lib/metadata-extraction.js';
13
13
  import { isProbeSession } from './lib/project-utils.js';
14
+ import { appendRawCapture, redactDeep } from './lib/raw-capture.js';
14
15
  import { writeJsonAtomic } from '../../config/json-store.js';
15
16
 
16
17
  interface HookEvent {
@@ -39,7 +40,7 @@ function getLocalTimestamp(): string {
39
40
  }
40
41
 
41
42
  // Get current events file path
42
- function getEventsFilePath(): string {
43
+ function getEventsFilePath(): { dir: string; file: string } {
43
44
  const now = new Date();
44
45
  const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;
45
46
  const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));
@@ -49,12 +50,7 @@ function getEventsFilePath(): string {
49
50
 
50
51
  const monthDir = join(historyDir(), 'raw-outputs', `${year}-${month}`);
51
52
 
52
- // Ensure directory exists
53
- if (!existsSync(monthDir)) {
54
- mkdirSync(monthDir, { recursive: true });
55
- }
56
-
57
- return join(monthDir, `${year}-${month}-${day}_all-events.jsonl`);
53
+ return { dir: monthDir, file: join(monthDir, `${year}-${month}-${day}_all-events.jsonl`) };
58
54
  }
59
55
 
60
56
  // Session-to-agent mapping functions
@@ -162,7 +158,7 @@ async function main() {
162
158
  source_app: agentName,
163
159
  session_id: hookData.session_id || 'main',
164
160
  hook_event_type: eventType,
165
- payload: hookData,
161
+ payload: redactDeep(hookData),
166
162
  timestamp: Date.now(),
167
163
  timestamp_local: getLocalTimestamp()
168
164
  };
@@ -177,9 +173,8 @@ async function main() {
177
173
  }
178
174
 
179
175
  // Append to events file
180
- const eventsFile = getEventsFilePath();
181
- const jsonLine = JSON.stringify(event) + '\n';
182
- appendFileSync(eventsFile, jsonLine, 'utf-8');
176
+ const events = getEventsFilePath();
177
+ appendRawCapture(events.file, events.dir, JSON.stringify(event) + '\n');
183
178
 
184
179
  } catch (error) {
185
180
  // Silently fail - don't block Claude Code
@@ -30,6 +30,7 @@
30
30
  */
31
31
 
32
32
  import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
33
+ import { minOf } from "../../../utils/min-max.js";
33
34
  import { readTailLines, scanIncremental } from "./transcript-scan.js";
34
35
  import { paiCacheDir } from "../../../runtime-paths.js";
35
36
  import { claudeProjectsDir } from "../../../config/pai-home.js";
@@ -664,7 +665,7 @@ export function measureCompactionTrigger(
664
665
  if (admissible.length === 0) return null;
665
666
  }
666
667
 
667
- const trigger = Math.min(...admissible.map((s) => s.preTokens));
668
+ const trigger = minOf(admissible.map((s) => s.preTokens));
668
669
  console.error(
669
670
  `[context-fill] measured trigger for ${cwd}: ${trigger} (minimum of ` +
670
671
  admissible.map((s) => `${s.preTokens}@${s.timestamp}`).join(", ") + ")"
@@ -339,7 +339,7 @@ function cdReason(command: string, cwd: string): string | null {
339
339
  if (existsSync(target)) continue;
340
340
  const fixed = repairValue(target, cwd, true, existsSync);
341
341
  if (!fixed) continue;
342
- const next = command.replace(target, fixed);
342
+ const next = command.replace(target, () => fixed);
343
343
  return `Wrong path in cd: ${target}. You are already in your worktree ${cwd}; drop the cd. Corrected command: ${next}`;
344
344
  }
345
345
  return null;
@@ -50,6 +50,7 @@ export async function sendNtfyNotification(message: string, retries = 2): Promis
50
50
  try {
51
51
  const response = await fetch(`https://ntfy.sh/${topic}`, {
52
52
  method: 'POST',
53
+ signal: AbortSignal.timeout(5000),
53
54
  body: message,
54
55
  headers: {
55
56
  'Title': 'Claude Code',
@@ -10,6 +10,11 @@ import { join, basename } from 'path';
10
10
  // ---------------------------------------------------------------------------
11
11
 
12
12
  /** Get or create the YYYY/MM subdirectory for the current month inside notesDir. */
13
+ /** Local YYYY-MM-DD, consistent with the month directory. */
14
+ function localDateString(now = new Date()): string {
15
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
16
+ }
17
+
13
18
  function getMonthDir(notesDir: string): string {
14
19
  const now = new Date();
15
20
  const year = String(now.getFullYear());
@@ -94,7 +99,7 @@ export function getCurrentNotePath(notesDir: string): string | null {
94
99
  */
95
100
  export function createSessionNote(notesDir: string, description: string): string {
96
101
  const noteNumber = getNextNoteNumber(notesDir);
97
- const date = new Date().toISOString().split('T')[0];
102
+ const date = localDateString();
98
103
  const monthDir = getMonthDir(notesDir);
99
104
  const filename = `${noteNumber} - ${date} - New Session.md`;
100
105
  const filepath = join(monthDir, filename);
@@ -137,7 +142,7 @@ export function appendCheckpoint(notePath: string, checkpoint: string): void {
137
142
  const noteFilename = basename(notePath);
138
143
  const numberMatch = noteFilename.match(/^(\d+)/);
139
144
  const noteNumber = numberMatch ? numberMatch[1] : '0000';
140
- const date = new Date().toISOString().split('T')[0];
145
+ const date = localDateString();
141
146
  const content = `# Session ${noteNumber}: Recovered\n\n**Date:** ${date}\n**Status:** In Progress\n\n---\n\n## Work Done\n\n<!-- PAI will add completed work here during session -->\n\n---\n\n## Next Steps\n\n<!-- To be filled at session end -->\n\n---\n\n**Tags:** #Session\n`;
142
147
  writeFileSync(notePath, content);
143
148
  console.error(`Recreated session note: ${noteFilename}`);
@@ -325,11 +330,19 @@ export function renameSessionNote(notePath: string, meaningfulName: string): str
325
330
  .trim();
326
331
 
327
332
  const paddedNumber = noteNumber.padStart(4, '0');
328
- const newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;
329
- const newPath = join(dir, newFilename);
333
+ let newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;
334
+ let newPath = join(dir, newFilename);
330
335
 
331
336
  if (newFilename === oldFilename) return notePath;
332
337
 
338
+ // Never overwrite another note: pick a free name.
339
+ if (existsSync(newPath)) {
340
+ let n = 2;
341
+ while (existsSync(join(dir, `${paiNumberedBase(paddedNumber, date, titleCaseName)} (${n}).md`))) n++;
342
+ newFilename = `${paiNumberedBase(paddedNumber, date, titleCaseName)} (${n}).md`;
343
+ newPath = join(dir, newFilename);
344
+ }
345
+
333
346
  try {
334
347
  renameSync(notePath, newPath);
335
348
  console.error(`Renamed note: ${oldFilename} → ${newFilename}`);
@@ -340,6 +353,10 @@ export function renameSessionNote(notePath: string, meaningfulName: string): str
340
353
  }
341
354
  }
342
355
 
356
+ function paiNumberedBase(num: string, date: string, name: string): string {
357
+ return `${num} - ${date} - ${name}`;
358
+ }
359
+
343
360
  /** Update the session note's H1 title and rename the file. */
344
361
  export function updateSessionNoteTitle(notePath: string, newTitle: string): void {
345
362
  if (!existsSync(notePath)) {
@@ -380,7 +397,7 @@ export function finalizeSessionNote(notePath: string, summary: string): string {
380
397
  const completionTime = new Date().toISOString();
381
398
  content = content.replace(
382
399
  '---\n\n## Work Done',
383
- `**Completed:** ${completionTime}\n\n---\n\n## Work Done`
400
+ () => `**Completed:** ${completionTime}\n\n---\n\n## Work Done`
384
401
  );
385
402
  }
386
403
 
@@ -388,7 +405,7 @@ export function finalizeSessionNote(notePath: string, summary: string): string {
388
405
  if (nextStepsMatch) {
389
406
  content = content.replace(
390
407
  nextStepsMatch[0],
391
- `## Next Steps\n\n${summary || 'Session completed.'}`
408
+ () => `## Next Steps\n\n${summary || 'Session completed.'}`
392
409
  );
393
410
  }
394
411
 
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Safe append for the history/raw-outputs JSONL captures: secrets masked,
3
+ * private modes, bounded size.
4
+ */
5
+
6
+ import { appendFileSync, chmodSync, existsSync, mkdirSync, renameSync, statSync } from 'fs';
7
+ import { maskSecrets } from './loop-guard.js';
8
+
9
+ export const RAW_CAPTURE_MAX_BYTES = 10 * 1024 * 1024;
10
+
11
+ /** Deep copy with every string value passed through the loop guard's secret-line masking. */
12
+ export function redactDeep<T>(value: T): T {
13
+ if (typeof value === 'string') return maskSecrets(value) as unknown as T;
14
+ if (Array.isArray(value)) return value.map(redactDeep) as unknown as T;
15
+ if (value && typeof value === 'object') {
16
+ const out: Record<string, unknown> = {};
17
+ for (const [k, v] of Object.entries(value)) out[k] = redactDeep(v);
18
+ return out as T;
19
+ }
20
+ return value;
21
+ }
22
+
23
+ /**
24
+ * Append one JSON line to `file` (directory 0700, file 0600). When the file
25
+ * would exceed `maxBytes` it is rotated to `<file>.1` (one backup kept).
26
+ */
27
+ export function appendRawCapture(file: string, dir: string, line: string, maxBytes = RAW_CAPTURE_MAX_BYTES): void {
28
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
29
+ try {
30
+ if (statSync(file).size + Buffer.byteLength(line) > maxBytes) renameSync(file, `${file}.1`);
31
+ } catch {
32
+ // no file yet
33
+ }
34
+ appendFileSync(file, line, { mode: 0o600 });
35
+ try { chmodSync(file, 0o600); } catch { /* best effort */ }
36
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Session summary building for the SessionEnd hook.
3
+ */
4
+
5
+ import { existsSync, readFileSync, readdirSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { historyDir } from './pai-paths.js';
8
+
9
+ export async function analyzeSession(sessionId: string, yearMonth: string): Promise<any> {
10
+ // Try to read raw outputs for this session
11
+ const rawOutputsDir = join(historyDir(), 'raw-outputs', yearMonth);
12
+
13
+ let filesChanged: string[] = [];
14
+ let commandsExecuted: string[] = [];
15
+ let toolsUsed: Set<string> = new Set();
16
+
17
+ try {
18
+ if (existsSync(rawOutputsDir)) {
19
+ // Only scan today's file — not the entire month (which can be 400MB+).
20
+ // JSONL filenames are prefixed with YYYY-MM-DD.
21
+ const todayPrefix = new Date().toISOString().substring(0, 10);
22
+ const files = readdirSync(rawOutputsDir).filter(
23
+ f => f.endsWith('.jsonl') && f.startsWith(todayPrefix)
24
+ );
25
+
26
+ for (const file of files) {
27
+ const filePath = join(rawOutputsDir, file);
28
+ const content = readFileSync(filePath, 'utf-8');
29
+ const lines = content.split('\n').filter(l => l.trim());
30
+
31
+ for (const line of lines) {
32
+ try {
33
+ const entry = JSON.parse(line);
34
+ if (entry.session === sessionId) {
35
+ toolsUsed.add(entry.tool);
36
+
37
+ // Extract file changes
38
+ if (entry.tool === 'Edit' || entry.tool === 'Write') {
39
+ if (entry.input?.file_path) {
40
+ filesChanged.push(entry.input.file_path);
41
+ }
42
+ }
43
+
44
+ // Extract bash commands
45
+ if (entry.tool === 'Bash' && entry.input?.command) {
46
+ commandsExecuted.push(entry.input.command);
47
+ }
48
+ }
49
+ } catch (e) {
50
+ // Skip invalid JSON lines
51
+ }
52
+ }
53
+ }
54
+ }
55
+ } catch (error) {
56
+ // Silent failure
57
+ }
58
+
59
+ return {
60
+ focus: 'general-work',
61
+ filesChanged: [...new Set(filesChanged)].slice(0, 10), // Unique, max 10
62
+ commandsExecuted: commandsExecuted.slice(0, 10), // Max 10
63
+ toolsUsed: Array.from(toolsUsed),
64
+ duration: 0 // Unknown
65
+ };
66
+ }
67
+
68
+ export function formatSessionDocument(timestamp: string, data: { session_id: string }, info: any): string {
69
+ const date = timestamp.substring(0, 10); // YYYY-MM-DD
70
+ const hhmmss = timestamp.substring(11); // HHMMSS
71
+ const time = `${hhmmss.slice(0, 2)}:${hhmmss.slice(2, 4)}:${hhmmss.slice(4, 6)}`;
72
+ const da = process.env.DA || 'PAI';
73
+
74
+ return `---
75
+ capture_type: SESSION
76
+ timestamp: ${new Date().toISOString()}
77
+ session_id: ${data.session_id}
78
+ duration_minutes: ${info.duration}
79
+ executor: ${da}
80
+ ---
81
+
82
+ # Session: ${info.focus}
83
+
84
+ **Date:** ${date}
85
+ **Time:** ${time}
86
+ **Session ID:** ${data.session_id}
87
+
88
+ ---
89
+
90
+ ## Session Overview
91
+
92
+ **Focus:** General development work
93
+ **Duration:** ${info.duration > 0 ? `${info.duration} minutes` : 'Unknown'}
94
+
95
+ ---
96
+
97
+ ## Tools Used
98
+
99
+ ${info.toolsUsed.length > 0 ? info.toolsUsed.map((t: string) => `- ${t}`).join('\n') : '- None recorded'}
100
+
101
+ ---
102
+
103
+ ## Files Modified
104
+
105
+ ${info.filesChanged.length > 0 ? info.filesChanged.map((f: string) => `- \`${f}\``).join('\n') : '- None recorded'}
106
+
107
+ **Total Files Changed:** ${info.filesChanged.length}
108
+
109
+ ---
110
+
111
+ ## Commands Executed
112
+
113
+ ${info.commandsExecuted.length > 0 ? '```bash\n' + info.commandsExecuted.join('\n') + '\n```' : 'None recorded'}
114
+
115
+ ---
116
+
117
+ ## Notes
118
+
119
+ This session summary was automatically generated by the UOCS SessionEnd hook.
120
+
121
+ For detailed tool outputs, see: \`${join(historyDir(), 'raw-outputs', timestamp.substring(0, 7))}/\`
122
+
123
+ ---
124
+
125
+ **Session Outcome:** Completed
126
+ **Generated:** ${new Date().toISOString()}
127
+ `;
128
+ }
129
+