@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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TODO.md section handling for the TodoWrite sync hook.
|
|
3
|
+
*
|
|
4
|
+
* Only the "## Current Session" section is managed; every other byte of the
|
|
5
|
+
* file (preamble, other sections, blank lines between them) is kept as is.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { writeFileSync, renameSync, unlinkSync } from 'fs';
|
|
9
|
+
import { dirname, join, basename } from 'path';
|
|
10
|
+
|
|
11
|
+
export interface TodoItem {
|
|
12
|
+
content: string;
|
|
13
|
+
status: 'pending' | 'in_progress' | 'completed';
|
|
14
|
+
activeForm: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const MANAGED_HEADING = /^## Current Session[ \t]*$/;
|
|
18
|
+
const FOOTER = /(\n---[ \t\r\n]*)?(\n\*Last updated:[^\n]*\*[ \t\r\n]*)?$/;
|
|
19
|
+
|
|
20
|
+
/** Format current session todos as markdown. */
|
|
21
|
+
export function formatSessionTodos(todos: TodoItem[]): string {
|
|
22
|
+
const inProgress = todos.filter(t => t.status === 'in_progress');
|
|
23
|
+
const pending = todos.filter(t => t.status === 'pending');
|
|
24
|
+
const completed = todos.filter(t => t.status === 'completed');
|
|
25
|
+
|
|
26
|
+
let content = '';
|
|
27
|
+
|
|
28
|
+
if (inProgress.length > 0) {
|
|
29
|
+
content += `### In Progress\n\n`;
|
|
30
|
+
for (const todo of inProgress) {
|
|
31
|
+
content += `- [ ] **${todo.content}** _(${todo.activeForm})_\n`;
|
|
32
|
+
}
|
|
33
|
+
content += '\n';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (pending.length > 0) {
|
|
37
|
+
content += `### Pending\n\n`;
|
|
38
|
+
for (const todo of pending) {
|
|
39
|
+
content += `- [ ] ${todo.content}\n`;
|
|
40
|
+
}
|
|
41
|
+
content += '\n';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (completed.length > 0) {
|
|
45
|
+
content += `### Completed\n\n`;
|
|
46
|
+
for (const todo of completed) {
|
|
47
|
+
content += `- [x] ${todo.content}\n`;
|
|
48
|
+
}
|
|
49
|
+
content += '\n';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (todos.length === 0) {
|
|
53
|
+
content += `_(No active session tasks)_\n\n`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return content;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Split into a preamble (text before the first "## " heading, possibly empty)
|
|
61
|
+
* and sections, each starting at its "## " line. Concatenating them
|
|
62
|
+
* reproduces the input exactly.
|
|
63
|
+
*/
|
|
64
|
+
function splitSections(content: string): { preamble: string; sections: string[] } {
|
|
65
|
+
const starts: number[] = [];
|
|
66
|
+
let offset = 0;
|
|
67
|
+
for (const line of content.split('\n')) {
|
|
68
|
+
if (line.startsWith('## ')) starts.push(offset);
|
|
69
|
+
offset += line.length + 1;
|
|
70
|
+
}
|
|
71
|
+
if (starts.length === 0) return { preamble: content, sections: [] };
|
|
72
|
+
const sections = starts.map((s, i) => content.slice(s, starts[i + 1] ?? content.length));
|
|
73
|
+
return { preamble: content.slice(0, starts[0]), sections };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Build the new TODO.md, replacing only the Current Session section. */
|
|
77
|
+
export function buildTodoContent(todos: TodoItem[], existingContent: string, now = new Date().toISOString()): string {
|
|
78
|
+
const body = existingContent.replace(FOOTER, '');
|
|
79
|
+
const managed = `## Current Session\n\n${formatSessionTodos(todos)}`;
|
|
80
|
+
const { preamble, sections } = splitSections(body);
|
|
81
|
+
|
|
82
|
+
let out: string;
|
|
83
|
+
const idx = sections.findIndex(s => MANAGED_HEADING.test(s.split('\n', 1)[0]));
|
|
84
|
+
if (idx >= 0) {
|
|
85
|
+
// Keep the separator that followed the old managed section.
|
|
86
|
+
const old = sections[idx];
|
|
87
|
+
const trailing = old.slice(old.trimEnd().length);
|
|
88
|
+
const replaced = sections.slice();
|
|
89
|
+
replaced[idx] = managed.trimEnd() + (idx < sections.length - 1 ? (trailing.length > 1 ? trailing : '\n\n') : '\n');
|
|
90
|
+
out = preamble + replaced.join('');
|
|
91
|
+
} else if (body.trim() === '') {
|
|
92
|
+
out = `# TODO\n\n${managed}`;
|
|
93
|
+
} else {
|
|
94
|
+
// No managed section yet: put it first, after the title if there is one.
|
|
95
|
+
const titleEnd = /^# [^\n]*\n\n?/.exec(preamble);
|
|
96
|
+
const head = titleEnd ? preamble.slice(0, titleEnd[0].length) : '';
|
|
97
|
+
const rest = preamble.slice(head.length);
|
|
98
|
+
out = head + managed + rest + sections.join('');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!out.endsWith('\n')) out += '\n';
|
|
102
|
+
return `${out}\n---\n\n*Last updated: ${now}*\n`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Fix malformed headings: Remove --- prefix from headings (---# → #). */
|
|
106
|
+
export function fixMalformedHeadings(content: string): string {
|
|
107
|
+
return content.replace(/^---#/gm, '#');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Write via temp file + rename so a crash never leaves a truncated file. */
|
|
111
|
+
export function writeFileAtomic(path: string, data: string): void {
|
|
112
|
+
const tmp = join(dirname(path), `.${basename(path)}.${process.pid}.tmp`);
|
|
113
|
+
try {
|
|
114
|
+
writeFileSync(tmp, data);
|
|
115
|
+
renameSync(tmp, path);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
try { unlinkSync(tmp); } catch { /* ignore */ }
|
|
118
|
+
throw e;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -95,7 +95,7 @@ const GIT_WRITE = /\bgit\b[^&;|\n]*?\s(?:commit|add|am|cherry-pick|apply|push|ta
|
|
|
95
95
|
const UNKNOWN_DIR = "/\0unresolved";
|
|
96
96
|
|
|
97
97
|
/** Splits on &&, ||, ;, | and newlines outside quotes. */
|
|
98
|
-
function splitSegments(command: string): string[] {
|
|
98
|
+
export function splitSegments(command: string): string[] {
|
|
99
99
|
const out: string[] = [];
|
|
100
100
|
let cur = "";
|
|
101
101
|
let quote = "";
|
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
* for later processing and analysis.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { appendFileSync, mkdirSync, existsSync } from 'fs';
|
|
11
10
|
import { join } from 'path';
|
|
12
11
|
import { historyDir } from '../lib/pai-paths.js';
|
|
12
|
+
import { appendRawCapture, redactDeep } from '../lib/raw-capture.js';
|
|
13
13
|
|
|
14
14
|
interface ToolUseData {
|
|
15
15
|
tool_name: string;
|
|
16
16
|
tool_input: Record<string, any>;
|
|
17
17
|
tool_response: Record<string, any>;
|
|
18
|
-
|
|
18
|
+
session_id: string;
|
|
19
19
|
timestamp: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -47,24 +47,20 @@ async function main() {
|
|
|
47
47
|
const today = now.toISOString().split('T')[0]; // YYYY-MM-DD
|
|
48
48
|
const yearMonth = today.substring(0, 7); // YYYY-MM
|
|
49
49
|
|
|
50
|
-
// Ensure capture directory exists
|
|
51
50
|
const dateDir = join(CAPTURE_DIR, yearMonth);
|
|
52
|
-
if (!existsSync(dateDir)) {
|
|
53
|
-
mkdirSync(dateDir, { recursive: true });
|
|
54
|
-
}
|
|
55
51
|
|
|
56
52
|
// Format output as JSONL (one JSON object per line)
|
|
57
53
|
const captureFile = join(dateDir, `${today}_tool-outputs.jsonl`);
|
|
58
54
|
const captureEntry = JSON.stringify({
|
|
59
55
|
timestamp: data.timestamp || now.toISOString(),
|
|
60
56
|
tool: data.tool_name,
|
|
61
|
-
input: data.tool_input,
|
|
62
|
-
output: data.tool_response,
|
|
63
|
-
session: data.
|
|
57
|
+
input: redactDeep(data.tool_input),
|
|
58
|
+
output: redactDeep(data.tool_response),
|
|
59
|
+
session: data.session_id
|
|
64
60
|
}) + '\n';
|
|
65
61
|
|
|
66
62
|
// Append to daily log
|
|
67
|
-
|
|
63
|
+
appendRawCapture(captureFile, dateDir, captureEntry);
|
|
68
64
|
|
|
69
65
|
// Exit successfully (code 0 = continue normally)
|
|
70
66
|
process.exit(0);
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { isWorkerSession } from "../lib/worker-session.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { existsSync, readFileSync, mkdirSync } from 'fs';
|
|
15
|
+
import { dirname } from 'path';
|
|
16
|
+
import { buildTodoContent, fixMalformedHeadings, writeFileAtomic, type TodoItem } from '../lib/todo-sync.js';
|
|
16
17
|
import {
|
|
17
18
|
findTodoPath,
|
|
18
19
|
findNotesDir,
|
|
@@ -21,12 +22,6 @@ import {
|
|
|
21
22
|
type WorkItem
|
|
22
23
|
} from '../lib/project-utils.js';
|
|
23
24
|
|
|
24
|
-
interface TodoItem {
|
|
25
|
-
content: string;
|
|
26
|
-
status: 'pending' | 'in_progress' | 'completed';
|
|
27
|
-
activeForm: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
25
|
interface HookInput {
|
|
31
26
|
session_id: string;
|
|
32
27
|
cwd: string;
|
|
@@ -36,101 +31,6 @@ interface HookInput {
|
|
|
36
31
|
};
|
|
37
32
|
}
|
|
38
33
|
|
|
39
|
-
/**
|
|
40
|
-
* Format current session todos as markdown
|
|
41
|
-
*/
|
|
42
|
-
function formatSessionTodos(todos: TodoItem[]): string {
|
|
43
|
-
const inProgress = todos.filter(t => t.status === 'in_progress');
|
|
44
|
-
const pending = todos.filter(t => t.status === 'pending');
|
|
45
|
-
const completed = todos.filter(t => t.status === 'completed');
|
|
46
|
-
|
|
47
|
-
let content = '';
|
|
48
|
-
|
|
49
|
-
if (inProgress.length > 0) {
|
|
50
|
-
content += `### In Progress\n\n`;
|
|
51
|
-
for (const todo of inProgress) {
|
|
52
|
-
content += `- [ ] **${todo.content}** _(${todo.activeForm})_\n`;
|
|
53
|
-
}
|
|
54
|
-
content += '\n';
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (pending.length > 0) {
|
|
58
|
-
content += `### Pending\n\n`;
|
|
59
|
-
for (const todo of pending) {
|
|
60
|
-
content += `- [ ] ${todo.content}\n`;
|
|
61
|
-
}
|
|
62
|
-
content += '\n';
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (completed.length > 0) {
|
|
66
|
-
content += `### Completed\n\n`;
|
|
67
|
-
for (const todo of completed) {
|
|
68
|
-
content += `- [x] ${todo.content}\n`;
|
|
69
|
-
}
|
|
70
|
-
content += '\n';
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (todos.length === 0) {
|
|
74
|
-
content += `_(No active session tasks)_\n\n`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return content;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Extract all sections from TODO.md EXCEPT "Current Session"
|
|
82
|
-
* These are user-managed sections that should be preserved.
|
|
83
|
-
*/
|
|
84
|
-
function extractPreservedSections(content: string): string {
|
|
85
|
-
let preserved = '';
|
|
86
|
-
|
|
87
|
-
// Match all ## sections that are NOT "Current Session"
|
|
88
|
-
const sectionRegex = /\n(## (?!Current Session)[^\n]+[\s\S]*?)(?=\n## |\n---\n+\*Last updated|$)/g;
|
|
89
|
-
const matches = content.matchAll(sectionRegex);
|
|
90
|
-
|
|
91
|
-
for (const match of matches) {
|
|
92
|
-
preserved += match[1];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return preserved;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Fix malformed headings: Remove --- prefix from headings (---# → #)
|
|
100
|
-
* Claude sometimes incorrectly merges horizontal rules with headings.
|
|
101
|
-
*/
|
|
102
|
-
function fixMalformedHeadings(content: string): string {
|
|
103
|
-
return content.replace(/^---#/gm, '#');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Build new TODO.md preserving user sections
|
|
108
|
-
*/
|
|
109
|
-
function buildTodoContent(todos: TodoItem[], existingContent: string): string {
|
|
110
|
-
const now = new Date().toISOString();
|
|
111
|
-
|
|
112
|
-
// Get all preserved sections (everything except Current Session)
|
|
113
|
-
const preserved = extractPreservedSections(existingContent);
|
|
114
|
-
|
|
115
|
-
// Build new content
|
|
116
|
-
let content = `# TODO
|
|
117
|
-
|
|
118
|
-
## Current Session
|
|
119
|
-
|
|
120
|
-
${formatSessionTodos(todos)}`;
|
|
121
|
-
|
|
122
|
-
// Add preserved sections
|
|
123
|
-
if (preserved.trim()) {
|
|
124
|
-
content += preserved;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Ensure we end with exactly one timestamp
|
|
128
|
-
content = content.replace(/(\n---\s*)*(\n\*Last updated:.*\*\s*)*$/, '');
|
|
129
|
-
content += `\n---\n\n*Last updated: ${now}*\n`;
|
|
130
|
-
|
|
131
|
-
return content;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
34
|
async function main() {
|
|
135
35
|
if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping
|
|
136
36
|
try {
|
|
@@ -165,7 +65,7 @@ async function main() {
|
|
|
165
65
|
|
|
166
66
|
// Create TODO.md if it doesn't exist
|
|
167
67
|
if (!existsSync(todoPath)) {
|
|
168
|
-
const parentDir = todoPath
|
|
68
|
+
const parentDir = dirname(todoPath);
|
|
169
69
|
mkdirSync(parentDir, { recursive: true });
|
|
170
70
|
console.error(`Creating TODO.md at ${todoPath}`);
|
|
171
71
|
}
|
|
@@ -181,7 +81,7 @@ async function main() {
|
|
|
181
81
|
// Build and write new content (with heading fix)
|
|
182
82
|
let newContent = buildTodoContent(todos, existingContent);
|
|
183
83
|
newContent = fixMalformedHeadings(newContent);
|
|
184
|
-
|
|
84
|
+
writeFileAtomic(todoPath, newContent);
|
|
185
85
|
|
|
186
86
|
const stats = {
|
|
187
87
|
inProgress: todos.filter(t => t.status === 'in_progress').length,
|
|
@@ -694,7 +694,7 @@ async function main() {
|
|
|
694
694
|
let noteContent = readFileSync(newPath, 'utf-8');
|
|
695
695
|
noteContent = noteContent.replace(
|
|
696
696
|
/^(# Session \d+:)\s*.*$/m,
|
|
697
|
-
`$
|
|
697
|
+
(_m, h1) => `${h1} ${title}`
|
|
698
698
|
);
|
|
699
699
|
writeFileSync(newPath, noteContent);
|
|
700
700
|
console.error(`Updated note H1 to match rename`);
|
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { currentWorkerId } from "../../../workers/worker-id.js";
|
|
21
|
-
import { appendFileSync, mkdirSync, existsSync } from 'fs';
|
|
21
|
+
import { appendFileSync, mkdirSync, existsSync, chmodSync } from 'fs';
|
|
22
22
|
import { dirname, resolve } from 'path';
|
|
23
23
|
import { securityEventsPath } from '../lib/pai-paths.js';
|
|
24
24
|
import { isSharedCheckout } from '../lib/shared-checkout.js';
|
|
25
|
-
import {
|
|
25
|
+
import { maskSecrets } from '../lib/loop-guard.js';
|
|
26
|
+
import { decideWorkerGitGuard, splitSegments, workerGitGuardMessage } from '../lib/worker-git-guard.js';
|
|
26
27
|
import { detectUnsafePlutil, plutilGuardMessage } from '../lib/plutil-guard.js';
|
|
27
28
|
import { readWorkersSection } from '../../../workers/config.js';
|
|
28
29
|
import { workersLogDir } from '../../../workers/paths.js';
|
|
@@ -75,6 +76,7 @@ const DANGEROUS_FILE_OPS_PATTERNS: RegExp[] = [
|
|
|
75
76
|
// OPTIONAL: Operations that require confirmation instead of blocking
|
|
76
77
|
const DANGEROUS_GIT_PATTERNS: RegExp[] = [
|
|
77
78
|
/\bgit\b.*\spush\b.*(\s-f\b|\s--force(\s|$))/i, // git push -f / --force; --force-with-lease and --follow-tags pass
|
|
79
|
+
/\bgit\b.*\spush\b.*\s\+\S/i, // a plus-prefixed refspec forces the update without -f
|
|
78
80
|
/\bgit\s+reset\s+--hard/i, // git reset --hard
|
|
79
81
|
// Add your own git safety patterns here
|
|
80
82
|
];
|
|
@@ -149,20 +151,35 @@ interface DetectionResult {
|
|
|
149
151
|
pattern?: string;
|
|
150
152
|
}
|
|
151
153
|
|
|
154
|
+
/** Quoted strings removed: text inside a commit message or echo is not a command. */
|
|
155
|
+
function dropQuoted(segment: string): string {
|
|
156
|
+
return segment.replace(/"(?:\\.|[^"\\])*"|'[^']*'/g, '""');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** A quoted +refspec is still a forced push, so push segments also test with only the quote marks removed. */
|
|
160
|
+
function gitPatternHit(pattern: RegExp, segment: string): boolean {
|
|
161
|
+
const bare = dropQuoted(segment);
|
|
162
|
+
if (pattern.test(bare)) return true;
|
|
163
|
+
return /\bgit\b.*\spush\b/.test(bare) && pattern.test(segment.replace(/["']/g, ''));
|
|
164
|
+
}
|
|
165
|
+
|
|
152
166
|
function detectAttack(content: string): DetectionResult {
|
|
167
|
+
// A quoted target is the same target: test the text with quote marks removed too.
|
|
168
|
+
const unquoted = content.replace(/["']/g, '');
|
|
153
169
|
// First check for hard blocks
|
|
154
170
|
for (const { category, patterns } of ALL_BLOCK_PATTERNS) {
|
|
155
171
|
for (const pattern of patterns) {
|
|
156
|
-
if (pattern.test(content)) {
|
|
172
|
+
if (pattern.test(content) || pattern.test(unquoted)) {
|
|
157
173
|
return { blocked: true, category, pattern: pattern.source };
|
|
158
174
|
}
|
|
159
175
|
}
|
|
160
176
|
}
|
|
161
177
|
|
|
162
178
|
// Then check for confirmation-required patterns
|
|
179
|
+
const segments = splitSegments(content);
|
|
163
180
|
for (const { category, patterns } of CONFIRM_PATTERNS) {
|
|
164
181
|
for (const pattern of patterns) {
|
|
165
|
-
if (
|
|
182
|
+
if (segments.some((seg) => gitPatternHit(pattern, seg))) {
|
|
166
183
|
return { blocked: false, requiresConfirmation: true, category, pattern: pattern.source };
|
|
167
184
|
}
|
|
168
185
|
}
|
|
@@ -177,6 +194,7 @@ function detectAttack(content: string): DetectionResult {
|
|
|
177
194
|
|
|
178
195
|
function logSecurityEvent(event: Record<string, unknown>): void {
|
|
179
196
|
const logPath = securityEventsPath();
|
|
197
|
+
if (typeof event.command === 'string') event = { ...event, command: maskSecrets(event.command) };
|
|
180
198
|
const entry = JSON.stringify({ timestamp: new Date().toISOString(), ...event }) + '\n';
|
|
181
199
|
|
|
182
200
|
try {
|
|
@@ -184,7 +202,8 @@ function logSecurityEvent(event: Record<string, unknown>): void {
|
|
|
184
202
|
if (!existsSync(dir)) {
|
|
185
203
|
mkdirSync(dir, { recursive: true });
|
|
186
204
|
}
|
|
187
|
-
appendFileSync(logPath, entry);
|
|
205
|
+
appendFileSync(logPath, entry, { mode: 0o600 });
|
|
206
|
+
chmodSync(logPath, 0o600);
|
|
188
207
|
} catch {
|
|
189
208
|
// Silently fail - logging should never break the hook
|
|
190
209
|
}
|
|
@@ -198,11 +217,12 @@ async function main(): Promise<void> {
|
|
|
198
217
|
let input: HookInput;
|
|
199
218
|
|
|
200
219
|
try {
|
|
201
|
-
// Read stdin
|
|
220
|
+
// Read stdin to end of input; the timer only bounds a hung pipe.
|
|
202
221
|
const chunks: Buffer[] = [];
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
222
|
+
let timer: NodeJS.Timeout | undefined;
|
|
223
|
+
const timeoutPromise = new Promise<Buffer[]>((_, reject) => {
|
|
224
|
+
timer = setTimeout(() => reject(new Error('timeout')), 2000);
|
|
225
|
+
});
|
|
206
226
|
const readPromise = (async () => {
|
|
207
227
|
for await (const chunk of process.stdin) {
|
|
208
228
|
chunks.push(chunk);
|
|
@@ -215,8 +235,10 @@ async function main(): Promise<void> {
|
|
|
215
235
|
const result = await Promise.race([readPromise, timeoutPromise]);
|
|
216
236
|
text = Buffer.concat(result).toString('utf-8');
|
|
217
237
|
} catch {
|
|
218
|
-
|
|
219
|
-
|
|
238
|
+
// Without the payload the delete and git guards cannot run: fail closed.
|
|
239
|
+
deny('The hook input could not be read in time, so the command cannot be checked. Retry the command.');
|
|
240
|
+
} finally {
|
|
241
|
+
clearTimeout(timer);
|
|
220
242
|
}
|
|
221
243
|
|
|
222
244
|
if (!text.trim()) {
|
|
@@ -226,9 +248,8 @@ async function main(): Promise<void> {
|
|
|
226
248
|
|
|
227
249
|
input = JSON.parse(text);
|
|
228
250
|
} catch {
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
return;
|
|
251
|
+
// Unparseable payload: the command is unknown, so the guards cannot run.
|
|
252
|
+
deny('The hook input could not be parsed, so the command cannot be checked. Retry the command.');
|
|
232
253
|
}
|
|
233
254
|
|
|
234
255
|
// Only validate Bash commands
|
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { paiSocketPath } from "../../../runtime-paths.js";
|
|
11
|
-
import { writeFileSync, mkdirSync, existsSync, readFileSync,
|
|
11
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync, unlinkSync } from 'fs';
|
|
12
|
+
import { tmpdir } from 'os';
|
|
12
13
|
import { join } from 'path';
|
|
13
14
|
import { historyDir } from '../lib/pai-paths.js';
|
|
15
|
+
import { analyzeSession, formatSessionDocument } from '../lib/session-summary.js';
|
|
14
16
|
|
|
15
17
|
interface SessionData {
|
|
16
|
-
|
|
18
|
+
session_id: string;
|
|
17
19
|
timestamp: string;
|
|
18
20
|
[key: string]: any;
|
|
19
21
|
}
|
|
@@ -27,25 +29,35 @@ async function main() {
|
|
|
27
29
|
chunks.push(chunk);
|
|
28
30
|
}
|
|
29
31
|
const input = Buffer.concat(chunks).toString('utf-8');
|
|
30
|
-
|
|
32
|
+
// The detached child has stdin ignored; its payload comes from the temp file.
|
|
33
|
+
if ((!input || input.trim() === '') && !process.env.__PAI_HOOK_INPUT_FILE) {
|
|
31
34
|
process.exit(0);
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
// Fork: re-exec ourselves with --background flag and
|
|
37
|
+
// Fork: re-exec ourselves with --background flag and hand the stdin data over via a private temp file
|
|
38
|
+
// (an env var is visible to ps eww and hits E2BIG on large payloads).
|
|
35
39
|
// This detaches the heavy work (JSONL scan, IPC) from CC's abort signal.
|
|
36
40
|
if (!process.env.__PAI_HOOK_BG) {
|
|
37
41
|
const { spawn } = await import('child_process');
|
|
42
|
+
const inputFile = join(tmpdir(), `pai-session-end-${process.pid}-${Date.now()}.json`);
|
|
43
|
+
writeFileSync(inputFile, input, { mode: 0o600 });
|
|
38
44
|
const child = spawn(process.execPath, [process.argv[1], '--background'], {
|
|
39
45
|
detached: true,
|
|
40
46
|
stdio: 'ignore',
|
|
41
|
-
env: { ...process.env, __PAI_HOOK_BG: '1',
|
|
47
|
+
env: { ...process.env, __PAI_HOOK_BG: '1', __PAI_HOOK_INPUT_FILE: inputFile },
|
|
42
48
|
});
|
|
43
49
|
child.unref();
|
|
44
50
|
process.exit(0); // Return immediately — CC sees success, abort signal is harmless
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
// Background mode: we're detached, safe from abort signals
|
|
48
|
-
const
|
|
54
|
+
const inputFile = process.env.__PAI_HOOK_INPUT_FILE;
|
|
55
|
+
let raw = input;
|
|
56
|
+
if (inputFile) {
|
|
57
|
+
raw = readFileSync(inputFile, 'utf-8');
|
|
58
|
+
try { unlinkSync(inputFile); } catch { /* ignore */ }
|
|
59
|
+
}
|
|
60
|
+
const data: SessionData = JSON.parse(raw);
|
|
49
61
|
|
|
50
62
|
// Generate timestamp for filename
|
|
51
63
|
const now = new Date();
|
|
@@ -57,7 +69,7 @@ async function main() {
|
|
|
57
69
|
const yearMonth = timestamp.substring(0, 7); // YYYY-MM
|
|
58
70
|
|
|
59
71
|
// Try to extract session info from raw outputs
|
|
60
|
-
const sessionInfo = await analyzeSession(data.
|
|
72
|
+
const sessionInfo = await analyzeSession(data.session_id, yearMonth);
|
|
61
73
|
|
|
62
74
|
// Generate filename
|
|
63
75
|
const filename = `${timestamp}_SESSION_${sessionInfo.focus}.md`;
|
|
@@ -75,7 +87,7 @@ async function main() {
|
|
|
75
87
|
writeFileSync(join(sessionDir, filename), sessionDoc);
|
|
76
88
|
|
|
77
89
|
// Also store structured summary via daemon IPC for the observations system
|
|
78
|
-
await storeStructuredSummary(data.
|
|
90
|
+
await storeStructuredSummary(data.session_id, sessionInfo);
|
|
79
91
|
|
|
80
92
|
// Exit successfully
|
|
81
93
|
process.exit(0);
|
|
@@ -86,126 +98,6 @@ async function main() {
|
|
|
86
98
|
}
|
|
87
99
|
}
|
|
88
100
|
|
|
89
|
-
async function analyzeSession(conversationId: string, yearMonth: string): Promise<any> {
|
|
90
|
-
// Try to read raw outputs for this session
|
|
91
|
-
const rawOutputsDir = join(historyDir(), 'raw-outputs', yearMonth);
|
|
92
|
-
|
|
93
|
-
let filesChanged: string[] = [];
|
|
94
|
-
let commandsExecuted: string[] = [];
|
|
95
|
-
let toolsUsed: Set<string> = new Set();
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
if (existsSync(rawOutputsDir)) {
|
|
99
|
-
// Only scan today's file — not the entire month (which can be 400MB+).
|
|
100
|
-
// JSONL filenames are prefixed with YYYY-MM-DD.
|
|
101
|
-
const todayPrefix = new Date().toISOString().substring(0, 10);
|
|
102
|
-
const files = readdirSync(rawOutputsDir).filter(
|
|
103
|
-
f => f.endsWith('.jsonl') && f.startsWith(todayPrefix)
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
for (const file of files) {
|
|
107
|
-
const filePath = join(rawOutputsDir, file);
|
|
108
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
109
|
-
const lines = content.split('\n').filter(l => l.trim());
|
|
110
|
-
|
|
111
|
-
for (const line of lines) {
|
|
112
|
-
try {
|
|
113
|
-
const entry = JSON.parse(line);
|
|
114
|
-
if (entry.session === conversationId) {
|
|
115
|
-
toolsUsed.add(entry.tool);
|
|
116
|
-
|
|
117
|
-
// Extract file changes
|
|
118
|
-
if (entry.tool === 'Edit' || entry.tool === 'Write') {
|
|
119
|
-
if (entry.input?.file_path) {
|
|
120
|
-
filesChanged.push(entry.input.file_path);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Extract bash commands
|
|
125
|
-
if (entry.tool === 'Bash' && entry.input?.command) {
|
|
126
|
-
commandsExecuted.push(entry.input.command);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
} catch (e) {
|
|
130
|
-
// Skip invalid JSON lines
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
} catch (error) {
|
|
136
|
-
// Silent failure
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
focus: 'general-work',
|
|
141
|
-
filesChanged: [...new Set(filesChanged)].slice(0, 10), // Unique, max 10
|
|
142
|
-
commandsExecuted: commandsExecuted.slice(0, 10), // Max 10
|
|
143
|
-
toolsUsed: Array.from(toolsUsed),
|
|
144
|
-
duration: 0 // Unknown
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function formatSessionDocument(timestamp: string, data: SessionData, info: any): string {
|
|
149
|
-
const date = timestamp.substring(0, 10); // YYYY-MM-DD
|
|
150
|
-
const time = timestamp.substring(11).replace(/-/g, ':'); // HH:MM:SS
|
|
151
|
-
const da = process.env.DA || 'PAI';
|
|
152
|
-
|
|
153
|
-
return `---
|
|
154
|
-
capture_type: SESSION
|
|
155
|
-
timestamp: ${new Date().toISOString()}
|
|
156
|
-
session_id: ${data.conversation_id}
|
|
157
|
-
duration_minutes: ${info.duration}
|
|
158
|
-
executor: ${da}
|
|
159
|
-
---
|
|
160
|
-
|
|
161
|
-
# Session: ${info.focus}
|
|
162
|
-
|
|
163
|
-
**Date:** ${date}
|
|
164
|
-
**Time:** ${time}
|
|
165
|
-
**Session ID:** ${data.conversation_id}
|
|
166
|
-
|
|
167
|
-
---
|
|
168
|
-
|
|
169
|
-
## Session Overview
|
|
170
|
-
|
|
171
|
-
**Focus:** General development work
|
|
172
|
-
**Duration:** ${info.duration > 0 ? `${info.duration} minutes` : 'Unknown'}
|
|
173
|
-
|
|
174
|
-
---
|
|
175
|
-
|
|
176
|
-
## Tools Used
|
|
177
|
-
|
|
178
|
-
${info.toolsUsed.length > 0 ? info.toolsUsed.map((t: string) => `- ${t}`).join('\n') : '- None recorded'}
|
|
179
|
-
|
|
180
|
-
---
|
|
181
|
-
|
|
182
|
-
## Files Modified
|
|
183
|
-
|
|
184
|
-
${info.filesChanged.length > 0 ? info.filesChanged.map((f: string) => `- \`${f}\``).join('\n') : '- None recorded'}
|
|
185
|
-
|
|
186
|
-
**Total Files Changed:** ${info.filesChanged.length}
|
|
187
|
-
|
|
188
|
-
---
|
|
189
|
-
|
|
190
|
-
## Commands Executed
|
|
191
|
-
|
|
192
|
-
${info.commandsExecuted.length > 0 ? '```bash\n' + info.commandsExecuted.join('\n') + '\n```' : 'None recorded'}
|
|
193
|
-
|
|
194
|
-
---
|
|
195
|
-
|
|
196
|
-
## Notes
|
|
197
|
-
|
|
198
|
-
This session summary was automatically generated by the UOCS SessionEnd hook.
|
|
199
|
-
|
|
200
|
-
For detailed tool outputs, see: \`${join(historyDir(), 'raw-outputs', timestamp.substring(0, 7))}/\`
|
|
201
|
-
|
|
202
|
-
---
|
|
203
|
-
|
|
204
|
-
**Session Outcome:** Completed
|
|
205
|
-
**Generated:** ${new Date().toISOString()}
|
|
206
|
-
`;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
101
|
async function storeStructuredSummary(
|
|
210
102
|
sessionId: string,
|
|
211
103
|
info: { focus: string; filesChanged: string[]; commandsExecuted: string[]; toolsUsed: string[]; duration: number }
|
|
@@ -74,9 +74,9 @@ async function main() {
|
|
|
74
74
|
|
|
75
75
|
// Replace placeholders {{DA}}, {{DA_COLOR}}, {{ENGINEER_NAME}}
|
|
76
76
|
coreContent = coreContent
|
|
77
|
-
.replace(/\{\{DA\}\}/g, daName)
|
|
78
|
-
.replace(/\{\{DA_COLOR\}\}/g, daColor)
|
|
79
|
-
.replace(/\{\{ENGINEER_NAME\}\}/g, engineerName);
|
|
77
|
+
.replace(/\{\{DA\}\}/g, () => daName)
|
|
78
|
+
.replace(/\{\{DA_COLOR\}\}/g, () => daColor)
|
|
79
|
+
.replace(/\{\{ENGINEER_NAME\}\}/g, () => engineerName);
|
|
80
80
|
|
|
81
81
|
console.error(`Read ${coreContent.length} characters from ${coreSkillPath} (Personalized for ${engineerName} & ${daName})`);
|
|
82
82
|
|