@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
|
@@ -4,7 +4,6 @@ const require = __paiCreateRequire(import.meta.url);
|
|
|
4
4
|
if (!process.env.PAI_QUIET_NOTICES) process.env.PAI_QUIET_NOTICES = '1';
|
|
5
5
|
|
|
6
6
|
// src/hooks/ts/post-tool-use/capture-tool-output.ts
|
|
7
|
-
import { appendFileSync, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
|
|
8
7
|
import { join as join3 } from "path";
|
|
9
8
|
|
|
10
9
|
// src/hooks/ts/lib/pai-paths.ts
|
|
@@ -136,6 +135,51 @@ function historyDir() {
|
|
|
136
135
|
return resolvePaiFile(paiHomePath("History"), [oldHistoryDir()], "pai config migrate --history");
|
|
137
136
|
}
|
|
138
137
|
|
|
138
|
+
// src/hooks/ts/lib/raw-capture.ts
|
|
139
|
+
import { appendFileSync, chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, renameSync as renameSync2, statSync as statSync2 } from "fs";
|
|
140
|
+
|
|
141
|
+
// src/workers/ps-table.ts
|
|
142
|
+
var PS_MAX_BUFFER = 64 * 1024 * 1024;
|
|
143
|
+
|
|
144
|
+
// src/hooks/ts/lib/loop-guard.ts
|
|
145
|
+
var LOOP_MAX_REPEATS = 4;
|
|
146
|
+
var LOOP_READ_STREAK = Number(process.env.PAI_LOOP_READ_STREAK) > 0 ? Number(process.env.PAI_LOOP_READ_STREAK) : 15;
|
|
147
|
+
var SELF = "If your part is done, commit and write your R report. If you are stuck, report the blocker in R x= (or use pai worker help if available).";
|
|
148
|
+
var LOOP_DENY_REASON = `loop guard: you ran this command ${LOOP_MAX_REPEATS} times with the same result and nothing changed. Stop repeating it. ${SELF}`;
|
|
149
|
+
var WAIT_CAP_MS = 45 * 6e4;
|
|
150
|
+
var FP_FULL_READ_BYTES = 256 * 1024;
|
|
151
|
+
var FP_TOTAL_BYTES = 4 * 1024 * 1024;
|
|
152
|
+
var SECRET_LINE = /pass(?:word|wd)|secret|token|api[_-]?key|credential|authorization|bearer|private key|^\s*(?:export\s+)?[A-Z][A-Z0-9_]*(?:KEY|SECRET|TOKEN|PASS\w*|CRED\w*|AUTH\w*)\s*=|\b(?:sk|ghp|gho|xox[bap])[-_][A-Za-z0-9_-]{16,}|\b[A-Za-z0-9+]{40,}={0,2}(?![\w/])|\b[a-z][a-z0-9+.-]*:\/\/[^\s\/:@]+:[^\s\/@]+@|\b(?:AKIA|ASIA|AGPA|AIDA|AROA)[A-Z0-9]{16}\b|\bgh[pousr]_[A-Za-z0-9]{20,}/i;
|
|
153
|
+
var MASKED_LINE = "[line masked: looks like a secret]";
|
|
154
|
+
function maskSecrets(text) {
|
|
155
|
+
return text.split("\n").map((l) => SECRET_LINE.test(l) ? MASKED_LINE : l).join("\n");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/hooks/ts/lib/raw-capture.ts
|
|
159
|
+
var RAW_CAPTURE_MAX_BYTES = 10 * 1024 * 1024;
|
|
160
|
+
function redactDeep(value) {
|
|
161
|
+
if (typeof value === "string") return maskSecrets(value);
|
|
162
|
+
if (Array.isArray(value)) return value.map(redactDeep);
|
|
163
|
+
if (value && typeof value === "object") {
|
|
164
|
+
const out = {};
|
|
165
|
+
for (const [k, v] of Object.entries(value)) out[k] = redactDeep(v);
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
function appendRawCapture(file, dir, line, maxBytes = RAW_CAPTURE_MAX_BYTES) {
|
|
171
|
+
if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
172
|
+
try {
|
|
173
|
+
if (statSync2(file).size + Buffer.byteLength(line) > maxBytes) renameSync2(file, `${file}.1`);
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
appendFileSync(file, line, { mode: 384 });
|
|
177
|
+
try {
|
|
178
|
+
chmodSync(file, 384);
|
|
179
|
+
} catch {
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
139
183
|
// src/hooks/ts/post-tool-use/capture-tool-output.ts
|
|
140
184
|
var CAPTURE_DIR = join3(historyDir(), "raw-outputs");
|
|
141
185
|
var INTERESTING_TOOLS = ["Bash", "Edit", "Write", "Read", "Task", "NotebookEdit"];
|
|
@@ -157,18 +201,15 @@ async function main() {
|
|
|
157
201
|
const today = now.toISOString().split("T")[0];
|
|
158
202
|
const yearMonth = today.substring(0, 7);
|
|
159
203
|
const dateDir = join3(CAPTURE_DIR, yearMonth);
|
|
160
|
-
if (!existsSync3(dateDir)) {
|
|
161
|
-
mkdirSync2(dateDir, { recursive: true });
|
|
162
|
-
}
|
|
163
204
|
const captureFile = join3(dateDir, `${today}_tool-outputs.jsonl`);
|
|
164
205
|
const captureEntry = JSON.stringify({
|
|
165
206
|
timestamp: data.timestamp || now.toISOString(),
|
|
166
207
|
tool: data.tool_name,
|
|
167
|
-
input: data.tool_input,
|
|
168
|
-
output: data.tool_response,
|
|
169
|
-
session: data.
|
|
208
|
+
input: redactDeep(data.tool_input),
|
|
209
|
+
output: redactDeep(data.tool_response),
|
|
210
|
+
session: data.session_id
|
|
170
211
|
}) + "\n";
|
|
171
|
-
|
|
212
|
+
appendRawCapture(captureFile, dateDir, captureEntry);
|
|
172
213
|
process.exit(0);
|
|
173
214
|
} catch (error) {
|
|
174
215
|
console.error(`[UOCS] PostToolUse hook error: ${error}`);
|
|
@@ -7598,6 +7598,7 @@ async function sendNtfyNotification(message, retries = 2) {
|
|
|
7598
7598
|
try {
|
|
7599
7599
|
const response = await fetch(`https://ntfy.sh/${topic}`, {
|
|
7600
7600
|
method: "POST",
|
|
7601
|
+
signal: AbortSignal.timeout(5e3),
|
|
7601
7602
|
body: message,
|
|
7602
7603
|
headers: {
|
|
7603
7604
|
"Title": "Claude Code",
|
|
@@ -7624,6 +7625,9 @@ async function sendNtfyNotification(message, retries = 2) {
|
|
|
7624
7625
|
// src/hooks/ts/lib/project-utils/session-notes.ts
|
|
7625
7626
|
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync4, writeFileSync, renameSync as renameSync2 } from "fs";
|
|
7626
7627
|
import { join as join5, basename as basename2 } from "path";
|
|
7628
|
+
function localDateString(now = /* @__PURE__ */ new Date()) {
|
|
7629
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
7630
|
+
}
|
|
7627
7631
|
function getMonthDir(notesDir) {
|
|
7628
7632
|
const now = /* @__PURE__ */ new Date();
|
|
7629
7633
|
const year = String(now.getFullYear());
|
|
@@ -7676,7 +7680,7 @@ function getCurrentNotePath(notesDir) {
|
|
|
7676
7680
|
}
|
|
7677
7681
|
function createSessionNote(notesDir, description) {
|
|
7678
7682
|
const noteNumber = getNextNoteNumber(notesDir);
|
|
7679
|
-
const date = (
|
|
7683
|
+
const date = localDateString();
|
|
7680
7684
|
const monthDir = getMonthDir(notesDir);
|
|
7681
7685
|
const filename = `${noteNumber} - ${date} - New Session.md`;
|
|
7682
7686
|
const filepath = join5(monthDir, filename);
|
|
@@ -7714,7 +7718,7 @@ function appendCheckpoint(notePath, checkpoint) {
|
|
|
7714
7718
|
const noteFilename = basename2(notePath);
|
|
7715
7719
|
const numberMatch = noteFilename.match(/^(\d+)/);
|
|
7716
7720
|
const noteNumber = numberMatch ? numberMatch[1] : "0000";
|
|
7717
|
-
const date = (
|
|
7721
|
+
const date = localDateString();
|
|
7718
7722
|
const content2 = `# Session ${noteNumber}: Recovered
|
|
7719
7723
|
|
|
7720
7724
|
**Date:** ${date}
|
|
@@ -7828,9 +7832,15 @@ function renameSessionNote(notePath, meaningfulName) {
|
|
|
7828
7832
|
const [, noteNumber, date] = match;
|
|
7829
7833
|
const titleCaseName = meaningfulName.replace(/[\/\\:\0]/g, " ").split(/[\s_-]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ").trim();
|
|
7830
7834
|
const paddedNumber = noteNumber.padStart(4, "0");
|
|
7831
|
-
|
|
7832
|
-
|
|
7835
|
+
let newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;
|
|
7836
|
+
let newPath = join5(dir, newFilename);
|
|
7833
7837
|
if (newFilename === oldFilename) return notePath;
|
|
7838
|
+
if (existsSync5(newPath)) {
|
|
7839
|
+
let n = 2;
|
|
7840
|
+
while (existsSync5(join5(dir, `${paiNumberedBase(paddedNumber, date, titleCaseName)} (${n}).md`))) n++;
|
|
7841
|
+
newFilename = `${paiNumberedBase(paddedNumber, date, titleCaseName)} (${n}).md`;
|
|
7842
|
+
newPath = join5(dir, newFilename);
|
|
7843
|
+
}
|
|
7834
7844
|
try {
|
|
7835
7845
|
renameSync2(notePath, newPath);
|
|
7836
7846
|
console.error(`Renamed note: ${oldFilename} \u2192 ${newFilename}`);
|
|
@@ -7840,6 +7850,9 @@ function renameSessionNote(notePath, meaningfulName) {
|
|
|
7840
7850
|
return notePath;
|
|
7841
7851
|
}
|
|
7842
7852
|
}
|
|
7853
|
+
function paiNumberedBase(num, date, name) {
|
|
7854
|
+
return `${num} - ${date} - ${name}`;
|
|
7855
|
+
}
|
|
7843
7856
|
|
|
7844
7857
|
// src/hooks/ts/lib/project-utils/todo.ts
|
|
7845
7858
|
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
|
|
@@ -8019,16 +8032,23 @@ function resolveToken() {
|
|
|
8019
8032
|
}
|
|
8020
8033
|
return process.env.FORGE_TOKEN || process.env.AIBROKER_FORGE_TOKEN || void 0;
|
|
8021
8034
|
}
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8035
|
+
function curlQuote(v) {
|
|
8036
|
+
if (/[\r\n]/.test(v)) throw new ForgeError("refusing newline in curl config value");
|
|
8037
|
+
return v.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
8038
|
+
}
|
|
8039
|
+
function buildCurlConfig(method, url, token, timeoutMs) {
|
|
8040
|
+
return [
|
|
8041
|
+
`url = "${curlQuote(url)}"`,
|
|
8042
|
+
`request = "${curlQuote(method)}"`,
|
|
8043
|
+
`header = "Authorization: token ${curlQuote(token)}"`,
|
|
8027
8044
|
`header = "Content-Type: application/json"`,
|
|
8028
8045
|
`max-time = ${Math.max(0.5, timeoutMs / 1e3)}`,
|
|
8029
8046
|
`silent`,
|
|
8030
8047
|
`write-out = "\\n%{http_code}"`
|
|
8031
8048
|
].join("\n");
|
|
8049
|
+
}
|
|
8050
|
+
var curlHttp = (method, url, token, body, timeoutMs) => {
|
|
8051
|
+
const cfg = buildCurlConfig(method, url, token, timeoutMs);
|
|
8032
8052
|
const argv = body !== void 0 ? ["-K", "-", "--data-raw", body] : ["-K", "-"];
|
|
8033
8053
|
const r = spawnSync("curl", argv, { input: cfg, encoding: "utf8", timeout: timeoutMs + 500, maxBuffer: 16 * 1024 * 1024 });
|
|
8034
8054
|
if (r.error || r.status !== 0) throw new ForgeError(`curl failed (${r.error?.message ?? `exit ${r.status}`})`);
|
|
@@ -9374,7 +9394,7 @@ ${state}` : `Context compression triggered at ~${tokenDisplay} tokens with ${sta
|
|
|
9374
9394
|
let noteContent = readFileSync14(newPath, "utf-8");
|
|
9375
9395
|
noteContent = noteContent.replace(
|
|
9376
9396
|
/^(# Session \d+:)\s*.*$/m,
|
|
9377
|
-
`$
|
|
9397
|
+
(_m, h1) => `${h1} ${title}`
|
|
9378
9398
|
);
|
|
9379
9399
|
writeFileSync8(newPath, noteContent);
|
|
9380
9400
|
console.error(`Updated note H1 to match rename`);
|
|
@@ -7541,6 +7541,7 @@ async function sendNtfyNotification(message, retries = 2) {
|
|
|
7541
7541
|
try {
|
|
7542
7542
|
const response = await fetch(`https://ntfy.sh/${topic}`, {
|
|
7543
7543
|
method: "POST",
|
|
7544
|
+
signal: AbortSignal.timeout(5e3),
|
|
7544
7545
|
body: message,
|
|
7545
7546
|
headers: {
|
|
7546
7547
|
"Title": "Claude Code",
|
|
@@ -7575,7 +7575,7 @@ async function main() {
|
|
|
7575
7575
|
const daName = process.env.DA || "PAI";
|
|
7576
7576
|
const daColor = process.env.DA_COLOR || "blue";
|
|
7577
7577
|
const engineerName = process.env.ENGINEER_NAME || "";
|
|
7578
|
-
coreContent = coreContent.replace(/\{\{DA\}\}/g, daName).replace(/\{\{DA_COLOR\}\}/g, daColor).replace(/\{\{ENGINEER_NAME\}\}/g, engineerName);
|
|
7578
|
+
coreContent = coreContent.replace(/\{\{DA\}\}/g, () => daName).replace(/\{\{DA_COLOR\}\}/g, () => daColor).replace(/\{\{ENGINEER_NAME\}\}/g, () => engineerName);
|
|
7579
7579
|
console.error(`Read ${coreContent.length} characters from ${coreSkillPath} (Personalized for ${engineerName} & ${daName})`);
|
|
7580
7580
|
const message = `<system-reminder>
|
|
7581
7581
|
PAI CORE CONTEXT
|
|
@@ -8964,7 +8964,7 @@ function statusPath(logDir, id) {
|
|
|
8964
8964
|
}
|
|
8965
8965
|
|
|
8966
8966
|
// src/workers/pending.ts
|
|
8967
|
-
import { existsSync as existsSync9, readFileSync as readFileSync9, statSync as statSync4
|
|
8967
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, statSync as statSync4 } from "node:fs";
|
|
8968
8968
|
|
|
8969
8969
|
// src/workers/status.ts
|
|
8970
8970
|
import { execFileSync } from "node:child_process";
|
|
@@ -9756,6 +9756,7 @@ async function sendNtfyNotification(message, retries = 2) {
|
|
|
9756
9756
|
try {
|
|
9757
9757
|
const response = await fetch(`https://ntfy.sh/${topic}`, {
|
|
9758
9758
|
method: "POST",
|
|
9759
|
+
signal: AbortSignal.timeout(5e3),
|
|
9759
9760
|
body: message,
|
|
9760
9761
|
headers: {
|
|
9761
9762
|
"Title": "Claude Code",
|
|
@@ -9780,8 +9781,11 @@ async function sendNtfyNotification(message, retries = 2) {
|
|
|
9780
9781
|
}
|
|
9781
9782
|
|
|
9782
9783
|
// src/hooks/ts/lib/project-utils/session-notes.ts
|
|
9783
|
-
import { existsSync as existsSync14, mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync13, writeFileSync as
|
|
9784
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync13, writeFileSync as writeFileSync5, renameSync as renameSync5 } from "fs";
|
|
9784
9785
|
import { join as join12, basename as basename5 } from "path";
|
|
9786
|
+
function localDateString(now = /* @__PURE__ */ new Date()) {
|
|
9787
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
9788
|
+
}
|
|
9785
9789
|
function getMonthDir(notesDir) {
|
|
9786
9790
|
const now = /* @__PURE__ */ new Date();
|
|
9787
9791
|
const year = String(now.getFullYear());
|
|
@@ -9834,7 +9838,7 @@ function getCurrentNotePath(notesDir) {
|
|
|
9834
9838
|
}
|
|
9835
9839
|
function createSessionNote(notesDir, description) {
|
|
9836
9840
|
const noteNumber = getNextNoteNumber(notesDir);
|
|
9837
|
-
const date = (
|
|
9841
|
+
const date = localDateString();
|
|
9838
9842
|
const monthDir = getMonthDir(notesDir);
|
|
9839
9843
|
const filename = `${noteNumber} - ${date} - New Session.md`;
|
|
9840
9844
|
const filepath = join12(monthDir, filename);
|
|
@@ -9859,7 +9863,7 @@ function createSessionNote(notesDir, description) {
|
|
|
9859
9863
|
|
|
9860
9864
|
**Tags:** #Session
|
|
9861
9865
|
`;
|
|
9862
|
-
|
|
9866
|
+
writeFileSync5(filepath, content);
|
|
9863
9867
|
console.error(`Created session note: ${filename}`);
|
|
9864
9868
|
return filepath;
|
|
9865
9869
|
}
|
|
@@ -9893,6 +9897,7 @@ function getRoutedNotesPath() {
|
|
|
9893
9897
|
}
|
|
9894
9898
|
return null;
|
|
9895
9899
|
}
|
|
9900
|
+
var PAI_CLI_TIMEOUT_MS = 1e4;
|
|
9896
9901
|
var PROJECT_SIGNALS = [
|
|
9897
9902
|
".git",
|
|
9898
9903
|
"package.json",
|
|
@@ -10020,17 +10025,22 @@ async function main() {
|
|
|
10020
10025
|
if (hasTodo) {
|
|
10021
10026
|
console.error(`TODO.md: ${todoPath}`);
|
|
10022
10027
|
} else {
|
|
10023
|
-
const newTodoPath =
|
|
10024
|
-
const { writeFileSync:
|
|
10025
|
-
|
|
10028
|
+
const newTodoPath = todoPath;
|
|
10029
|
+
const { writeFileSync: writeFileSync6, mkdirSync: mkdirSync7 } = await import("fs");
|
|
10030
|
+
mkdirSync7(dirname6(newTodoPath), { recursive: true });
|
|
10031
|
+
writeFileSync6(newTodoPath, `# TODO
|
|
10032
|
+
|
|
10033
|
+
## Current Session
|
|
10026
10034
|
|
|
10027
|
-
|
|
10035
|
+
- [ ] (Tasks will be tracked here)
|
|
10028
10036
|
|
|
10029
|
-
|
|
10037
|
+
## Backlog
|
|
10038
|
+
|
|
10039
|
+
- [ ] (Future tasks)
|
|
10030
10040
|
|
|
10031
10041
|
---
|
|
10032
10042
|
|
|
10033
|
-
*
|
|
10043
|
+
*Last updated: ${(/* @__PURE__ */ new Date()).toISOString()}*
|
|
10034
10044
|
`);
|
|
10035
10045
|
console.error(`Created TODO.md: ${newTodoPath}`);
|
|
10036
10046
|
}
|
|
@@ -10071,29 +10081,34 @@ Using existing session note: ${basename6(activeNotePath)}`);
|
|
|
10071
10081
|
} catch {
|
|
10072
10082
|
}
|
|
10073
10083
|
}
|
|
10074
|
-
await sendNtfyNotification(
|
|
10084
|
+
await sendNtfyNotification("Session started");
|
|
10075
10085
|
const paiBin = findPaiBinary();
|
|
10076
10086
|
let paiProjectBlock = "";
|
|
10087
|
+
let detectedRootPath;
|
|
10077
10088
|
try {
|
|
10078
10089
|
const { execFileSync: execFileSync3 } = await import("child_process");
|
|
10079
10090
|
const raw = execFileSync3(paiBin, ["project", "detect", "--json", cwd], {
|
|
10080
10091
|
encoding: "utf-8",
|
|
10081
|
-
env: process.env
|
|
10092
|
+
env: process.env,
|
|
10093
|
+
timeout: PAI_CLI_TIMEOUT_MS
|
|
10082
10094
|
}).trim();
|
|
10083
10095
|
if (raw) {
|
|
10084
10096
|
const detected = JSON.parse(raw);
|
|
10097
|
+
detectedRootPath = detected.root_path;
|
|
10085
10098
|
const tryAutoRegister = async () => {
|
|
10086
10099
|
if (isGuardedPath(cwd) || !hasProjectSignals(cwd)) return false;
|
|
10087
10100
|
try {
|
|
10088
10101
|
execFileSync3(paiBin, ["project", "add", cwd], {
|
|
10089
10102
|
encoding: "utf-8",
|
|
10090
|
-
env: process.env
|
|
10103
|
+
env: process.env,
|
|
10104
|
+
timeout: PAI_CLI_TIMEOUT_MS
|
|
10091
10105
|
});
|
|
10092
10106
|
console.error(`PAI auto-registered project at: ${cwd}`);
|
|
10093
10107
|
try {
|
|
10094
10108
|
const raw2 = execFileSync3(paiBin, ["project", "detect", "--json", cwd], {
|
|
10095
10109
|
encoding: "utf-8",
|
|
10096
|
-
env: process.env
|
|
10110
|
+
env: process.env,
|
|
10111
|
+
timeout: PAI_CLI_TIMEOUT_MS
|
|
10097
10112
|
}).trim();
|
|
10098
10113
|
if (raw2) {
|
|
10099
10114
|
const detected2 = JSON.parse(raw2);
|
|
@@ -10149,6 +10164,7 @@ WARNING: Project status is "${detected.status}". Run: pai project health --fix`
|
|
|
10149
10164
|
}
|
|
10150
10165
|
} catch (e) {
|
|
10151
10166
|
console.error("pai project detect failed:", e);
|
|
10167
|
+
detectedRootPath = cwd;
|
|
10152
10168
|
}
|
|
10153
10169
|
const reminder = `
|
|
10154
10170
|
<system-reminder>
|
|
@@ -10177,7 +10193,7 @@ ${paiProjectBlock}` : ""}
|
|
|
10177
10193
|
}
|
|
10178
10194
|
try {
|
|
10179
10195
|
const { execFileSync: run } = await import("child_process");
|
|
10180
|
-
const pins = run(paiBin, ["project", "pins"], { encoding: "utf-8", cwd }).trim();
|
|
10196
|
+
const pins = run(paiBin, ["project", "pins"], { encoding: "utf-8", cwd, timeout: PAI_CLI_TIMEOUT_MS }).trim();
|
|
10181
10197
|
if (pins) console.log(pins);
|
|
10182
10198
|
} catch {
|
|
10183
10199
|
}
|
|
@@ -10233,20 +10249,7 @@ THE ABOVE INSTRUCTIONS ARE MANDATORY. Follow them exactly.
|
|
|
10233
10249
|
console.error(`Injected CLAUDE.md content from: ${path}`);
|
|
10234
10250
|
}
|
|
10235
10251
|
try {
|
|
10236
|
-
|
|
10237
|
-
try {
|
|
10238
|
-
const { execFileSync: efs } = await import("child_process");
|
|
10239
|
-
const raw2 = efs(paiBin, ["project", "detect", "--json", cwd], {
|
|
10240
|
-
encoding: "utf-8",
|
|
10241
|
-
env: process.env
|
|
10242
|
-
}).trim();
|
|
10243
|
-
if (raw2) {
|
|
10244
|
-
const det = JSON.parse(raw2);
|
|
10245
|
-
if (det.root_path) wakeupRootPath = det.root_path;
|
|
10246
|
-
}
|
|
10247
|
-
} catch {
|
|
10248
|
-
wakeupRootPath = cwd;
|
|
10249
|
-
}
|
|
10252
|
+
const wakeupRootPath = detectedRootPath;
|
|
10250
10253
|
const wakeupBlock = buildWakeupContext(wakeupRootPath, void 0, { skipStory: handoverInjected });
|
|
10251
10254
|
if (wakeupBlock) {
|
|
10252
10255
|
const wakeupReminder = `
|
|
@@ -9078,7 +9078,7 @@ function currentWorkerId(env = process.env, logDir) {
|
|
|
9078
9078
|
}
|
|
9079
9079
|
|
|
9080
9080
|
// src/hooks/ts/pre-tool-use/security-validator.ts
|
|
9081
|
-
import { appendFileSync, mkdirSync as mkdirSync5, existsSync as existsSync11 } from "fs";
|
|
9081
|
+
import { appendFileSync, mkdirSync as mkdirSync5, existsSync as existsSync11, chmodSync as chmodSync4 } from "fs";
|
|
9082
9082
|
import { dirname as dirname7, resolve as resolve5 } from "path";
|
|
9083
9083
|
|
|
9084
9084
|
// src/hooks/ts/lib/pai-paths.ts
|
|
@@ -9208,6 +9208,23 @@ function isSharedCheckout(p) {
|
|
|
9208
9208
|
return statuses.some((s) => s.id !== p.selfId && isLive(s) && real(s.cwd) === top);
|
|
9209
9209
|
}
|
|
9210
9210
|
|
|
9211
|
+
// src/workers/ps-table.ts
|
|
9212
|
+
var PS_MAX_BUFFER = 64 * 1024 * 1024;
|
|
9213
|
+
|
|
9214
|
+
// src/hooks/ts/lib/loop-guard.ts
|
|
9215
|
+
var LOOP_MAX_REPEATS = 4;
|
|
9216
|
+
var LOOP_READ_STREAK = Number(process.env.PAI_LOOP_READ_STREAK) > 0 ? Number(process.env.PAI_LOOP_READ_STREAK) : 15;
|
|
9217
|
+
var SELF = "If your part is done, commit and write your R report. If you are stuck, report the blocker in R x= (or use pai worker help if available).";
|
|
9218
|
+
var LOOP_DENY_REASON = `loop guard: you ran this command ${LOOP_MAX_REPEATS} times with the same result and nothing changed. Stop repeating it. ${SELF}`;
|
|
9219
|
+
var WAIT_CAP_MS = 45 * 6e4;
|
|
9220
|
+
var FP_FULL_READ_BYTES = 256 * 1024;
|
|
9221
|
+
var FP_TOTAL_BYTES = 4 * 1024 * 1024;
|
|
9222
|
+
var SECRET_LINE = /pass(?:word|wd)|secret|token|api[_-]?key|credential|authorization|bearer|private key|^\s*(?:export\s+)?[A-Z][A-Z0-9_]*(?:KEY|SECRET|TOKEN|PASS\w*|CRED\w*|AUTH\w*)\s*=|\b(?:sk|ghp|gho|xox[bap])[-_][A-Za-z0-9_-]{16,}|\b[A-Za-z0-9+]{40,}={0,2}(?![\w/])|\b[a-z][a-z0-9+.-]*:\/\/[^\s\/:@]+:[^\s\/@]+@|\b(?:AKIA|ASIA|AGPA|AIDA|AROA)[A-Z0-9]{16}\b|\bgh[pousr]_[A-Za-z0-9]{20,}/i;
|
|
9223
|
+
var MASKED_LINE = "[line masked: looks like a secret]";
|
|
9224
|
+
function maskSecrets(text) {
|
|
9225
|
+
return text.split("\n").map((l) => SECRET_LINE.test(l) ? MASKED_LINE : l).join("\n");
|
|
9226
|
+
}
|
|
9227
|
+
|
|
9211
9228
|
// src/hooks/ts/lib/worker-git-guard.ts
|
|
9212
9229
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
9213
9230
|
import { existsSync as existsSync10 } from "node:fs";
|
|
@@ -9262,7 +9279,7 @@ function isInsideWorktree(cwd, root) {
|
|
|
9262
9279
|
}
|
|
9263
9280
|
var GIT_WRITE = /\bgit\b[^&;|\n]*?\s(?:commit|add|am|cherry-pick|apply|push|tag|rm|mv)(?![\w-])/;
|
|
9264
9281
|
var UNKNOWN_DIR = "/\0unresolved";
|
|
9265
|
-
function
|
|
9282
|
+
function splitSegments2(command) {
|
|
9266
9283
|
const out = [];
|
|
9267
9284
|
let cur = "";
|
|
9268
9285
|
let quote = "";
|
|
@@ -9295,7 +9312,7 @@ function worktreeEscape(command, cwd, worktreesRoot) {
|
|
|
9295
9312
|
const allowed = (dir) => isInsideWorktree(dir, cwd) || isInsideWorktree(dir, worktreesRoot) || /^\/(tmp|private\/tmp|var\/folders|private\/var\/folders)(\/|$)/.test(dir);
|
|
9296
9313
|
let here = cwd;
|
|
9297
9314
|
const env = {};
|
|
9298
|
-
for (const segment of
|
|
9315
|
+
for (const segment of splitSegments2(command)) {
|
|
9299
9316
|
const tokens = tokenize(segment);
|
|
9300
9317
|
const segEnv = {};
|
|
9301
9318
|
let i = 0;
|
|
@@ -9424,6 +9441,8 @@ var DANGEROUS_FILE_OPS_PATTERNS = [
|
|
|
9424
9441
|
var DANGEROUS_GIT_PATTERNS = [
|
|
9425
9442
|
/\bgit\b.*\spush\b.*(\s-f\b|\s--force(\s|$))/i,
|
|
9426
9443
|
// git push -f / --force; --force-with-lease and --follow-tags pass
|
|
9444
|
+
/\bgit\b.*\spush\b.*\s\+\S/i,
|
|
9445
|
+
// a plus-prefixed refspec forces the update without -f
|
|
9427
9446
|
/\bgit\s+reset\s+--hard/i
|
|
9428
9447
|
// git reset --hard
|
|
9429
9448
|
// Add your own git safety patterns here
|
|
@@ -9457,17 +9476,27 @@ function deny(reason) {
|
|
|
9457
9476
|
`);
|
|
9458
9477
|
process.exit(2);
|
|
9459
9478
|
}
|
|
9479
|
+
function dropQuoted(segment) {
|
|
9480
|
+
return segment.replace(/"(?:\\.|[^"\\])*"|'[^']*'/g, '""');
|
|
9481
|
+
}
|
|
9482
|
+
function gitPatternHit(pattern, segment) {
|
|
9483
|
+
const bare = dropQuoted(segment);
|
|
9484
|
+
if (pattern.test(bare)) return true;
|
|
9485
|
+
return /\bgit\b.*\spush\b/.test(bare) && pattern.test(segment.replace(/["']/g, ""));
|
|
9486
|
+
}
|
|
9460
9487
|
function detectAttack(content) {
|
|
9488
|
+
const unquoted = content.replace(/["']/g, "");
|
|
9461
9489
|
for (const { category, patterns } of ALL_BLOCK_PATTERNS) {
|
|
9462
9490
|
for (const pattern of patterns) {
|
|
9463
|
-
if (pattern.test(content)) {
|
|
9491
|
+
if (pattern.test(content) || pattern.test(unquoted)) {
|
|
9464
9492
|
return { blocked: true, category, pattern: pattern.source };
|
|
9465
9493
|
}
|
|
9466
9494
|
}
|
|
9467
9495
|
}
|
|
9496
|
+
const segments = splitSegments2(content);
|
|
9468
9497
|
for (const { category, patterns } of CONFIRM_PATTERNS) {
|
|
9469
9498
|
for (const pattern of patterns) {
|
|
9470
|
-
if (
|
|
9499
|
+
if (segments.some((seg) => gitPatternHit(pattern, seg))) {
|
|
9471
9500
|
return { blocked: false, requiresConfirmation: true, category, pattern: pattern.source };
|
|
9472
9501
|
}
|
|
9473
9502
|
}
|
|
@@ -9476,13 +9505,15 @@ function detectAttack(content) {
|
|
|
9476
9505
|
}
|
|
9477
9506
|
function logSecurityEvent(event) {
|
|
9478
9507
|
const logPath = securityEventsPath();
|
|
9508
|
+
if (typeof event.command === "string") event = { ...event, command: maskSecrets(event.command) };
|
|
9479
9509
|
const entry = JSON.stringify({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...event }) + "\n";
|
|
9480
9510
|
try {
|
|
9481
9511
|
const dir = dirname7(logPath);
|
|
9482
9512
|
if (!existsSync11(dir)) {
|
|
9483
9513
|
mkdirSync5(dir, { recursive: true });
|
|
9484
9514
|
}
|
|
9485
|
-
appendFileSync(logPath, entry);
|
|
9515
|
+
appendFileSync(logPath, entry, { mode: 384 });
|
|
9516
|
+
chmodSync4(logPath, 384);
|
|
9486
9517
|
} catch {
|
|
9487
9518
|
}
|
|
9488
9519
|
}
|
|
@@ -9490,9 +9521,10 @@ async function main() {
|
|
|
9490
9521
|
let input;
|
|
9491
9522
|
try {
|
|
9492
9523
|
const chunks = [];
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
|
|
9524
|
+
let timer;
|
|
9525
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
9526
|
+
timer = setTimeout(() => reject(new Error("timeout")), 2e3);
|
|
9527
|
+
});
|
|
9496
9528
|
const readPromise = (async () => {
|
|
9497
9529
|
for await (const chunk of process.stdin) {
|
|
9498
9530
|
chunks.push(chunk);
|
|
@@ -9504,8 +9536,9 @@ async function main() {
|
|
|
9504
9536
|
const result2 = await Promise.race([readPromise, timeoutPromise]);
|
|
9505
9537
|
text = Buffer.concat(result2).toString("utf-8");
|
|
9506
9538
|
} catch {
|
|
9507
|
-
|
|
9508
|
-
|
|
9539
|
+
deny("The hook input could not be read in time, so the command cannot be checked. Retry the command.");
|
|
9540
|
+
} finally {
|
|
9541
|
+
clearTimeout(timer);
|
|
9509
9542
|
}
|
|
9510
9543
|
if (!text.trim()) {
|
|
9511
9544
|
console.log(JSON.stringify({ permissionDecision: "allow" }));
|
|
@@ -9513,8 +9546,7 @@ async function main() {
|
|
|
9513
9546
|
}
|
|
9514
9547
|
input = JSON.parse(text);
|
|
9515
9548
|
} catch {
|
|
9516
|
-
|
|
9517
|
-
return;
|
|
9549
|
+
deny("The hook input could not be parsed, so the command cannot be checked. Retry the command.");
|
|
9518
9550
|
}
|
|
9519
9551
|
if (input.tool_name !== "Bash") {
|
|
9520
9552
|
console.log(JSON.stringify({ permissionDecision: "allow" }));
|
package/dist/hooks/stop-hook.mjs
CHANGED
|
@@ -7382,8 +7382,8 @@ function setTabTitle(title) {
|
|
|
7382
7382
|
}
|
|
7383
7383
|
|
|
7384
7384
|
// src/hooks/ts/stop/stop-hook.ts
|
|
7385
|
-
import { readFileSync as readFileSync10,
|
|
7386
|
-
import { join as join12, basename as basename6, dirname as
|
|
7385
|
+
import { readFileSync as readFileSync10, existsSync as existsSync11, unlinkSync as unlinkSync2 } from "fs";
|
|
7386
|
+
import { join as join12, basename as basename6, dirname as dirname5 } from "path";
|
|
7387
7387
|
import { connect } from "net";
|
|
7388
7388
|
import { randomUUID } from "crypto";
|
|
7389
7389
|
|
|
@@ -7636,6 +7636,7 @@ async function sendNtfyNotification(message, retries = 2) {
|
|
|
7636
7636
|
try {
|
|
7637
7637
|
const response = await fetch(`https://ntfy.sh/${topic}`, {
|
|
7638
7638
|
method: "POST",
|
|
7639
|
+
signal: AbortSignal.timeout(5e3),
|
|
7639
7640
|
body: message,
|
|
7640
7641
|
headers: {
|
|
7641
7642
|
"Title": "Claude Code",
|
|
@@ -7792,9 +7793,15 @@ function renameSessionNote(notePath, meaningfulName) {
|
|
|
7792
7793
|
const [, noteNumber, date] = match;
|
|
7793
7794
|
const titleCaseName = meaningfulName.replace(/[\/\\:\0]/g, " ").split(/[\s_-]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ").trim();
|
|
7794
7795
|
const paddedNumber = noteNumber.padStart(4, "0");
|
|
7795
|
-
|
|
7796
|
-
|
|
7796
|
+
let newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;
|
|
7797
|
+
let newPath = join5(dir, newFilename);
|
|
7797
7798
|
if (newFilename === oldFilename) return notePath;
|
|
7799
|
+
if (existsSync5(newPath)) {
|
|
7800
|
+
let n = 2;
|
|
7801
|
+
while (existsSync5(join5(dir, `${paiNumberedBase(paddedNumber, date, titleCaseName)} (${n}).md`))) n++;
|
|
7802
|
+
newFilename = `${paiNumberedBase(paddedNumber, date, titleCaseName)} (${n}).md`;
|
|
7803
|
+
newPath = join5(dir, newFilename);
|
|
7804
|
+
}
|
|
7798
7805
|
try {
|
|
7799
7806
|
renameSync2(notePath, newPath);
|
|
7800
7807
|
console.error(`Renamed note: ${oldFilename} \u2192 ${newFilename}`);
|
|
@@ -7804,6 +7811,9 @@ function renameSessionNote(notePath, meaningfulName) {
|
|
|
7804
7811
|
return notePath;
|
|
7805
7812
|
}
|
|
7806
7813
|
}
|
|
7814
|
+
function paiNumberedBase(num, date, name) {
|
|
7815
|
+
return `${num} - ${date} - ${name}`;
|
|
7816
|
+
}
|
|
7807
7817
|
function finalizeSessionNote(notePath, summary) {
|
|
7808
7818
|
if (!existsSync5(notePath)) {
|
|
7809
7819
|
console.error(`Note file not found: ${notePath}`);
|
|
@@ -7819,7 +7829,7 @@ function finalizeSessionNote(notePath, summary) {
|
|
|
7819
7829
|
const completionTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
7820
7830
|
content = content.replace(
|
|
7821
7831
|
"---\n\n## Work Done",
|
|
7822
|
-
`**Completed:** ${completionTime}
|
|
7832
|
+
() => `**Completed:** ${completionTime}
|
|
7823
7833
|
|
|
7824
7834
|
---
|
|
7825
7835
|
|
|
@@ -7830,7 +7840,7 @@ function finalizeSessionNote(notePath, summary) {
|
|
|
7830
7840
|
if (nextStepsMatch) {
|
|
7831
7841
|
content = content.replace(
|
|
7832
7842
|
nextStepsMatch[0],
|
|
7833
|
-
`## Next Steps
|
|
7843
|
+
() => `## Next Steps
|
|
7834
7844
|
|
|
7835
7845
|
${summary || "Session completed."}`
|
|
7836
7846
|
);
|
|
@@ -8022,16 +8032,23 @@ function resolveToken() {
|
|
|
8022
8032
|
}
|
|
8023
8033
|
return process.env.FORGE_TOKEN || process.env.AIBROKER_FORGE_TOKEN || void 0;
|
|
8024
8034
|
}
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8035
|
+
function curlQuote(v) {
|
|
8036
|
+
if (/[\r\n]/.test(v)) throw new ForgeError("refusing newline in curl config value");
|
|
8037
|
+
return v.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
8038
|
+
}
|
|
8039
|
+
function buildCurlConfig(method, url, token, timeoutMs) {
|
|
8040
|
+
return [
|
|
8041
|
+
`url = "${curlQuote(url)}"`,
|
|
8042
|
+
`request = "${curlQuote(method)}"`,
|
|
8043
|
+
`header = "Authorization: token ${curlQuote(token)}"`,
|
|
8030
8044
|
`header = "Content-Type: application/json"`,
|
|
8031
8045
|
`max-time = ${Math.max(0.5, timeoutMs / 1e3)}`,
|
|
8032
8046
|
`silent`,
|
|
8033
8047
|
`write-out = "\\n%{http_code}"`
|
|
8034
8048
|
].join("\n");
|
|
8049
|
+
}
|
|
8050
|
+
var curlHttp = (method, url, token, body, timeoutMs) => {
|
|
8051
|
+
const cfg = buildCurlConfig(method, url, token, timeoutMs);
|
|
8035
8052
|
const argv = body !== void 0 ? ["-K", "-", "--data-raw", body] : ["-K", "-"];
|
|
8036
8053
|
const r = spawnSync("curl", argv, { input: cfg, encoding: "utf8", timeout: timeoutMs + 500, maxBuffer: 16 * 1024 * 1024 });
|
|
8037
8054
|
if (r.error || r.status !== 0) throw new ForgeError(`curl failed (${r.error?.message ?? `exit ${r.status}`})`);
|
|
@@ -8563,6 +8580,22 @@ function sessionStateDir() {
|
|
|
8563
8580
|
}
|
|
8564
8581
|
var SESSION_STOP_LOCK_ABANDONED_AFTER_MS = 60 * 60 * 1e3;
|
|
8565
8582
|
|
|
8583
|
+
// src/config/atomic-file.ts
|
|
8584
|
+
import { mkdirSync as mkdirSync7, renameSync as renameSync6, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
8585
|
+
import { randomBytes } from "node:crypto";
|
|
8586
|
+
import { dirname as dirname4 } from "node:path";
|
|
8587
|
+
function writeFileAtomic(path, text) {
|
|
8588
|
+
mkdirSync7(dirname4(path), { recursive: true });
|
|
8589
|
+
const tmp = `${path}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
8590
|
+
try {
|
|
8591
|
+
writeFileSync5(tmp, text, "utf8");
|
|
8592
|
+
renameSync6(tmp, path);
|
|
8593
|
+
} catch (e) {
|
|
8594
|
+
rmSync(tmp, { force: true });
|
|
8595
|
+
throw e;
|
|
8596
|
+
}
|
|
8597
|
+
}
|
|
8598
|
+
|
|
8566
8599
|
// src/hooks/ts/stop/stop-hook.ts
|
|
8567
8600
|
var DAEMON_SOCKET = process.env.PAI_SOCKET ?? "/tmp/pai.sock";
|
|
8568
8601
|
var DAEMON_TIMEOUT_MS = 800;
|
|
@@ -8574,8 +8607,10 @@ var AUTO_SAVE_INTERVAL = (() => {
|
|
|
8574
8607
|
}
|
|
8575
8608
|
return 15;
|
|
8576
8609
|
})();
|
|
8610
|
+
var SAFE_SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
8577
8611
|
function readSessionState(sessionId) {
|
|
8578
8612
|
try {
|
|
8613
|
+
if (!SAFE_SESSION_ID.test(sessionId)) return { humanMessageCount: 0 };
|
|
8579
8614
|
const stateFile = join12(sessionStateDir(), `${sessionId}.json`);
|
|
8580
8615
|
if (!existsSync11(stateFile)) return { humanMessageCount: 0 };
|
|
8581
8616
|
const raw = readFileSync10(stateFile, "utf-8");
|
|
@@ -8589,16 +8624,19 @@ function readSessionState(sessionId) {
|
|
|
8589
8624
|
}
|
|
8590
8625
|
function writeSessionState(sessionId, state) {
|
|
8591
8626
|
try {
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8627
|
+
if (!SAFE_SESSION_ID.test(sessionId)) {
|
|
8628
|
+
console.error(`STOP-HOOK: Refusing session state for unsafe session id`);
|
|
8629
|
+
return;
|
|
8630
|
+
}
|
|
8631
|
+
const stateFile = join12(paiHomePath("session-state"), `${sessionId}.json`);
|
|
8632
|
+
writeFileAtomic(stateFile, JSON.stringify(state, null, 2));
|
|
8596
8633
|
} catch (e) {
|
|
8597
8634
|
console.error(`STOP-HOOK: Could not write session state: ${e}`);
|
|
8598
8635
|
}
|
|
8599
8636
|
}
|
|
8600
8637
|
function deleteSessionState(sessionId) {
|
|
8601
8638
|
try {
|
|
8639
|
+
if (!SAFE_SESSION_ID.test(sessionId)) return;
|
|
8602
8640
|
const stateFile = join12(sessionStateDir(), `${sessionId}.json`);
|
|
8603
8641
|
if (existsSync11(stateFile)) {
|
|
8604
8642
|
unlinkSync2(stateFile);
|
|
@@ -9057,7 +9095,7 @@ async function executeDirectly(lines, transcriptPath, cwd, message, lastUserQuer
|
|
|
9057
9095
|
console.error(`Could not finalize session note: ${noteError}`);
|
|
9058
9096
|
}
|
|
9059
9097
|
try {
|
|
9060
|
-
const transcriptDir =
|
|
9098
|
+
const transcriptDir = dirname5(transcriptPath);
|
|
9061
9099
|
const archivedCount = archiveSessionFilesToSessionsDir(transcriptDir);
|
|
9062
9100
|
if (archivedCount > 0) {
|
|
9063
9101
|
console.error(`Archived ${archivedCount} session file(s) to sessions/`);
|