@tekmidian/pai 1.0.0 → 1.0.3
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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +67 -5
- package/dist/aibroker-client-BxzeEHPF.mjs +3 -0
- package/dist/{aibroker-client-CXEYBzbf.mjs → aibroker-client-CQPGqLxL.mjs} +1 -1
- package/dist/atomic-file-BorMa4T3.mjs +21 -0
- package/dist/{auto-route-D5SXrMpB.mjs → auto-route-B7Yu6Uam.mjs} +3 -3
- package/dist/auto-route-CjjQqU1o.mjs +11 -0
- package/dist/budget-Dod6rnA9.mjs +15 -0
- package/dist/{caller-identity-CVTuiLSs.mjs → caller-identity-yLFb7CKq.mjs} +6 -5
- package/dist/{capacity-DaXrVj3J.mjs → capacity-DEveQdFV.mjs} +3 -3
- package/dist/{chain-BcQlIlEd.mjs → chain-7n9Gx56r.mjs} +5 -5
- package/dist/cli/index.mjs +1628 -370
- package/dist/{cockpit-actions-ODmt8yF9.mjs → cockpit-actions-BBHeyEZI.mjs} +84 -42
- package/dist/cockpit-actions-DR9X514g.mjs +20 -0
- package/dist/cockpit-memory--dnShXBA.mjs +20 -0
- package/dist/{config-BSXKyeJ1.mjs → config-Bu1_vbY8.mjs} +42 -3
- package/dist/{config-B955yYmo.mjs → config-Cxi-aBf4.mjs} +2 -2
- package/dist/{config-TaWmdTLx.mjs → config-DtENJ6cL.mjs} +1 -1
- package/dist/{config-D5chi2LO.mjs → config-XpZWOYOG.mjs} +2 -2
- package/dist/{controls-BGkK_Bdi.mjs → controls-CPoeudou.mjs} +37 -22
- package/dist/daemon/index.mjs +43 -42
- package/dist/daemon-DKdBNKfa.mjs +45 -0
- package/dist/{daemon-CorGh6Ub.mjs → daemon-DPAo6WTa.mjs} +103 -55
- package/dist/daemon-mcp/index.mjs +40 -39
- package/dist/{define-DGFu-sCf.mjs → define-CjJsmq-6.mjs} +1 -1
- package/dist/{detect-C1d5cUkT.mjs → detect-9CZNO0yf.mjs} +2 -2
- package/dist/detect-D1ziFFfA.mjs +10 -0
- package/dist/{embed-pass-CeQD8kRE.mjs → embed-pass-DIWjA68P.mjs} +5 -5
- package/dist/{embedding-gate-BHuBsgHD.mjs → embedding-gate-6rZrWRXa.mjs} +5 -5
- package/dist/{embedding-gate-Z7CJSOJk.mjs → embedding-gate-Cnk4YCyX.mjs} +3 -3
- package/dist/{embeddings-D4t6L-EH.mjs → embeddings-BVq3esBF.mjs} +1 -1
- package/dist/{embeddings-OF9Pt4by.mjs → embeddings-CIkJBKd-.mjs} +1 -1
- package/dist/{endreason-x1KOSrcO.mjs → endreason-C577xIQz.mjs} +7 -6
- package/dist/{env-DArqGq-Z.mjs → env-BE8mrsxr.mjs} +1 -1
- package/dist/factory-Bsib-qDG.mjs +9 -0
- package/dist/{factory-BtjbQ0np.mjs → factory-DjAAsqr_.mjs} +15 -13
- package/dist/{fallback-BIyEpPsQ.mjs → fallback-DFlItw1Z.mjs} +5 -5
- package/dist/{fmt-age-BiSHrTKN.mjs → fmt-age-ByDkvkoU.mjs} +1 -1
- package/dist/{health-C6jZWvRG.mjs → health-Dnym-qcT.mjs} +4 -4
- package/dist/{helpers-DFmYRCOa.mjs → helpers-BD6V-p9w.mjs} +2 -2
- package/dist/helpers-CMGjy_0N.mjs +10 -0
- package/dist/{helpers-CJpb_MNB.mjs → helpers-CRuBUas3.mjs} +87 -38
- package/dist/hooks/capture-all-events.mjs +52 -11
- package/dist/hooks/capture-session-summary.mjs +64 -47
- package/dist/hooks/capture-tool-output.mjs +49 -8
- package/dist/hooks/context-compression-hook.mjs +30 -10
- package/dist/hooks/initialize-session.mjs +1 -0
- package/dist/hooks/load-core-context.mjs +1 -1
- package/dist/hooks/load-project-context.mjs +32 -29
- package/dist/hooks/security-validator.mjs +45 -13
- package/dist/hooks/stop-hook.mjs +54 -16
- package/dist/hooks/supervision-banner.mjs +9 -0
- package/dist/hooks/sync-todo-to-md.mjs +147 -113
- package/dist/hooks/worker-edit-repair.mjs +2 -1
- package/dist/hooks/worker-guard.mjs +40 -16
- package/dist/hooks/worker-note.mjs +12 -5
- package/dist/hooks/worker-path-repair.mjs +1 -1
- package/dist/hooks/worker-pool-host.mjs +329 -231
- package/dist/hooks/worker-status-line.mjs +195 -114
- package/dist/index.d.mts +18 -0
- package/dist/index.mjs +12 -12
- package/dist/{indexer-backend-BM2CUSu8.mjs → indexer-backend-Cll7le4P.mjs} +149 -96
- package/dist/{ipc-client-BMqWw_gv.mjs → ipc-client-CaiGJnSM.mjs} +1 -1
- package/dist/{ipc-client-YYDD6zzU.mjs → ipc-client-ZRmDKd5Z.mjs} +1 -1
- package/dist/kill-tree-BG7dRVtp.mjs +10 -0
- package/dist/{kill-tree-BjYz5BxK.mjs → kill-tree-EWYaEk5v.mjs} +8 -8
- package/dist/{launch-CLp0OJxV.mjs → launch-CXVlZjHt.mjs} +8 -8
- package/dist/{launch-DgqQdAjI.mjs → launch-Jh5NVeNe.mjs} +3 -3
- package/dist/local-candidate-DLdwQt0s.mjs +112 -0
- package/dist/local-instances-DOxSgT_L.mjs +210 -0
- package/dist/local-instances-ZeRkkuS5.mjs +6 -0
- package/dist/{local-tiers-DPZYqv9m.mjs → local-tiers-BE4jGaMa.mjs} +2 -2
- package/dist/{local-tiers-DnBIAkbK.mjs → local-tiers-DNMI5W86.mjs} +3 -3
- package/dist/{main-config-ops-C0SflOWk.mjs → main-config-ops-BEMpuhDz.mjs} +3 -3
- package/dist/{main-resolver-DVPpSWVy.mjs → main-resolver-3Ns1HPmS.mjs} +18 -17
- package/dist/{migrate-BUJHadA8.mjs → migrate-DrA_OwlZ.mjs} +45 -14
- package/dist/min-max-DrSRUaWQ.mjs +16 -0
- package/dist/{model-inventory-Dasber3S.mjs → model-inventory-BFVSNFT3.mjs} +13 -12
- package/dist/{models-add-Bt7aUHDs.mjs → models-add-C1QIc1DA.mjs} +39 -38
- package/dist/operator-BTqvTIc8.mjs +7 -0
- package/dist/{operator-DZxcP-g8.mjs → operator-CyuB3aVw.mjs} +3 -3
- package/dist/{pai-marker-kIYuNt3c.mjs → pai-marker-BcZyJguM.mjs} +7 -6
- package/dist/{pane-audit-OGYrOAik.mjs → pane-audit-DKQt71s4.mjs} +7 -7
- package/dist/{paths-DoRuFc60.mjs → paths-CFHTloSe.mjs} +3 -3
- package/dist/{paths-DoIyRnhx.mjs → paths-CHifubDh.mjs} +3 -3
- package/dist/{planner-DLGOYlVb.mjs → planner-BmFACdLa.mjs} +31 -30
- package/dist/{pool-CZ5MZaXg.mjs → pool-Dh-q3iJi.mjs} +45 -6
- package/dist/{pool-cli-BxNSPzzY.mjs → pool-cli-BggU8GRp.mjs} +5 -5
- package/dist/{pool-cli-CbJfl37y.mjs → pool-cli-DQ6OzRy2.mjs} +6 -6
- package/dist/postgres-BdBNkL_l.mjs +7 -0
- package/dist/{postgres-CDnd_k5f.mjs → postgres-C6ti4haA.mjs} +55 -34
- package/dist/{project-setup-DM4npnL3.mjs → project-setup-jvfONJTQ.mjs} +131 -74
- package/dist/providers-BeDiVEb-.mjs +16 -0
- package/dist/{providers-UsKtLcUR.mjs → providers-CblTFrhz.mjs} +6 -6
- package/dist/{query-feedback-I-txSnld.mjs → query-feedback-BRfPtqki.mjs} +2 -2
- package/dist/registrable-BzH4TObg.mjs +9 -0
- package/dist/{registrable-LrRIVYQc.mjs → registrable-DpyTtajg.mjs} +4 -4
- package/dist/{render-BDbb4CF-.mjs → render-BYmQkuLd.mjs} +8 -8
- package/dist/render-CoMvwMPs.mjs +13 -0
- package/dist/{reranker-B4r4vykM.mjs → reranker-8g6xMwP7.mjs} +1 -1
- package/dist/{reranker-DxxMCtA0.mjs → reranker-CqCbpGnX.mjs} +1 -1
- package/dist/root-FtJsxYnm.mjs +36 -0
- package/dist/{root-BMeJuHOb.mjs → root-Xglu0OPD.mjs} +11 -11
- package/dist/route-table-B7zpoypi.mjs +15 -0
- package/dist/{router-L8Nu-OHF.mjs → router-CQSc3lk3.mjs} +17 -7
- package/dist/{router-D6Ec6Fid.mjs → router-DwHJRgG9.mjs} +1 -1
- package/dist/{routing-CUyiglfu.mjs → routing-BAAST9vT.mjs} +27 -308
- package/dist/{routing-ZuGlc0ge.mjs → routing-CENt4HH1.mjs} +13 -12
- package/dist/{run-ulAi5dFD.mjs → run-CSC7mgPf.mjs} +35 -33
- package/dist/{scan-B7I0gPe-.mjs → scan-C_Y5W9py.mjs} +6 -6
- package/dist/{local-candidate-jOjTNlOL.mjs → scheduler-CtUyWJ3O.mjs} +7 -109
- package/dist/scheduler-dmHXoRvm.mjs +3 -0
- package/dist/{scope-D5VSQxe4.mjs → scope-D-h6F6DN.mjs} +4 -4
- package/dist/{search-CPoE0830.mjs → search-RSyeQqj1.mjs} +18 -18
- package/dist/{session-CNXF6DLP.mjs → session-8iqQDkqb.mjs} +55 -31
- package/dist/session-BeqAbNPd.mjs +35 -0
- package/dist/session-scan-BJI_ZO8M.mjs +9 -0
- package/dist/{session-scan-BDLEc4w1.mjs → session-scan-W-gbwJHM.mjs} +6 -6
- package/dist/{sources-CjhUKKtO.mjs → sources-ESUkZaWv.mjs} +1 -1
- package/dist/{status-DYydKQUI.mjs → status-Dm4dYciN.mjs} +4 -4
- package/dist/{status-CvVi9cDf.mjs → status-ggJ7lgVg.mjs} +17 -9
- package/dist/{stop-words-SdSL1WKH.mjs → stop-words-DSEYQdNe.mjs} +1 -1
- package/dist/{test-isolation-CoDeOVXb.mjs → test-isolation-lqSmYxr0.mjs} +1 -1
- package/dist/{timeouts-OwEONmc8.mjs → timeouts-1MLooEBB.mjs} +1 -1
- package/dist/{tool-errors-AeRQuG3Y.mjs → tool-errors-7E13StWd.mjs} +4 -3
- package/dist/{tool-errors-R5E0fCgf.mjs → tool-errors-CJWODvlo.mjs} +6 -6
- package/dist/viewer-CqozWW39.mjs +22 -0
- package/dist/{viewer-DtWj5Vti.mjs → viewer-mDXUNHNy.mjs} +15 -15
- package/dist/{wait-BMUR-U-U.mjs → wait-ByV0OyMZ.mjs} +3 -3
- package/dist/{worker-config-BLqa1yiD.mjs → worker-config-CJhEMk19.mjs} +294 -92
- package/dist/worker-config-DESkIjMF.mjs +41 -0
- package/dist/{workers-config-DEI9lrV4.mjs → workers-config-f8KrkY4L.mjs} +2 -2
- package/dist/{worktree-zmRf556h.mjs → worktree-CRs2gyg0.mjs} +37 -7
- package/dist/{zettelkasten-nigq7fqs.mjs → zettelkasten-B_rRRRvD.mjs} +5 -5
- package/dist/{zettelkasten-Cr_Xj0mG.mjs → zettelkasten-BzMm8REH.mjs} +29 -19
- package/docs/README.md +2 -0
- package/docs/cockpit.md +10 -4
- package/docs/commands/README.md +4 -4
- package/docs/commands/backup.md +2 -0
- package/docs/commands/install.md +29 -4
- package/docs/commands/providers.md +1 -1
- package/docs/commands/restore.md +4 -0
- package/docs/commands/setup.md +29 -4
- package/docs/commands/worker.md +1 -1
- package/docs/config.md +2 -0
- package/docs/features.md +299 -0
- package/docs/images/architecture.drawio +51 -0
- package/docs/images/architecture.png +0 -0
- package/docs/images/architecture.svg +3 -0
- package/docs/images/cockpit-01-system-tokens.jpg +0 -0
- package/docs/images/cockpit-02-totals-cache.jpg +0 -0
- package/docs/images/cockpit-03-local-servers-models-pool.jpg +0 -0
- package/docs/images/cockpit-04-workers-memory.jpg +0 -0
- package/docs/images/cockpit-05-performance.jpg +0 -0
- package/docs/images/cockpit-06-runs-speed-routing.jpg +0 -0
- package/docs/images/cockpit-07-settings-registry.jpg +0 -0
- package/docs/install/index.md +29 -0
- package/docs/three-tier-coding.md +1 -1
- package/gemini-extension.json +1 -1
- package/package.json +1 -5
- package/pai-plugin.json +1 -1
- package/plugins/context-preservation/plugin.json +1 -1
- package/plugins/core/plugin.json +1 -1
- package/plugins/core/skills/setup/SKILL.md +41 -9
- package/plugins/creative/plugin.json +1 -1
- package/plugins/observability/plugin.json +1 -1
- package/plugins/productivity/plugin.json +1 -1
- package/plugins/semantic-search/plugin.json +1 -1
- package/plugins/ui/plugin.json +1 -1
- package/plugins/zettelkasten/plugin.json +1 -1
- package/src/hooks/session-autosave.sh +8 -1
- package/src/hooks/session-stop.sh +8 -1
- package/src/hooks/ts/capture-all-events.ts +7 -12
- package/src/hooks/ts/lib/context-fill.ts +2 -1
- package/src/hooks/ts/lib/path-repair.ts +1 -1
- package/src/hooks/ts/lib/project-utils/notify.ts +1 -0
- package/src/hooks/ts/lib/project-utils/session-notes.ts +23 -6
- package/src/hooks/ts/lib/raw-capture.ts +36 -0
- package/src/hooks/ts/lib/session-summary.ts +129 -0
- package/src/hooks/ts/lib/todo-sync.ts +120 -0
- package/src/hooks/ts/lib/worker-git-guard.ts +1 -1
- package/src/hooks/ts/post-tool-use/capture-tool-output.ts +6 -10
- package/src/hooks/ts/post-tool-use/sync-todo-to-md.ts +5 -105
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +1 -1
- package/src/hooks/ts/pre-tool-use/security-validator.ts +35 -14
- package/src/hooks/ts/session-end/capture-session-summary.ts +20 -128
- package/src/hooks/ts/session-start/load-core-context.ts +3 -3
- package/src/hooks/ts/session-start/load-project-context.ts +18 -22
- package/src/hooks/ts/stop/stop-hook.ts +13 -5
- package/src/hooks/ztk-auto.sh +7 -2
- package/dist/aibroker-client-JjE74jo0.mjs +0 -3
- package/dist/auto-route-BRk8NEFv.mjs +0 -11
- package/dist/budget-DuhQ2zzG.mjs +0 -14
- package/dist/cockpit-actions-DcQ-ucU8.mjs +0 -19
- package/dist/cockpit-memory-KV45PPO2.mjs +0 -19
- package/dist/daemon-C7yt_ptC.mjs +0 -44
- package/dist/detect-DRUkmHnb.mjs +0 -10
- package/dist/factory-DAGG9_n8.mjs +0 -9
- package/dist/helpers-DPJEHPq6.mjs +0 -10
- package/dist/kill-tree-LV0Q7Mte.mjs +0 -10
- package/dist/operator-CfL0HiBK.mjs +0 -7
- package/dist/postgres-DgqWyqhN.mjs +0 -7
- package/dist/providers-CTu2s1z2.mjs +0 -15
- package/dist/registrable-D9aNZOTh.mjs +0 -9
- package/dist/render-BSfVlYHj.mjs +0 -13
- package/dist/root-xI9DqVIe.mjs +0 -35
- package/dist/route-table-HjvnLVEU.mjs +0 -14
- package/dist/session-BJHzmUg2.mjs +0 -34
- package/dist/session-scan-YORKE1NO.mjs +0 -9
- package/dist/viewer-B8709jDN.mjs +0 -22
- package/dist/worker-config-D8tRuvo7.mjs +0 -40
- package/docs/images/cockpit-limits-routing.jpg +0 -0
- package/docs/images/cockpit-models.jpg +0 -0
- package/docs/images/cockpit-usage.jpg +0 -0
- package/docs/images/cockpit-workers-system.jpg +0 -0
- package/scripts/build-docs.mjs +0 -41
- package/scripts/build-hooks.mjs +0 -311
- package/scripts/build-skill-stubs.mjs +0 -216
- package/scripts/copy-defaults.mjs +0 -46
package/docs/install/index.md
CHANGED
|
@@ -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
|
-

|
|
113
113
|
|
|
114
114
|
|
|
115
115
|
- **Cloud** and **Local** totals, **Providers**, **Speed per model**;
|
package/gemini-extension.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "1.0.3",
|
|
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.
|
|
3
|
+
"version": "1.0.3",
|
|
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.
|
|
6
|
+
"version": "1.0.3",
|
|
7
7
|
"author": "Matthias Nott",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"homepage": "https://github.com/mnott/PAI",
|
package/plugins/core/plugin.json
CHANGED
|
@@ -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.
|
|
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
|
|
40
|
-
pai setup --yes --storage sqlite
|
|
54
|
+
pai setup --questions --json
|
|
41
55
|
```
|
|
42
56
|
|
|
43
|
-
|
|
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
|
-
|
|
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
|
|
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,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.
|
|
5
|
+
"version": "1.0.3",
|
|
6
6
|
"hooks": "./hooks/hooks.json",
|
|
7
7
|
"skills": ["./skills/"]
|
|
8
8
|
}
|
package/plugins/ui/plugin.json
CHANGED
|
@@ -49,7 +49,14 @@ fi
|
|
|
49
49
|
|
|
50
50
|
CLAUDE_SESSION_UUID=""
|
|
51
51
|
if [ ! -t 0 ]; then
|
|
52
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
181
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
329
|
-
|
|
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
|
+
|