@tekmidian/pai 1.0.5 → 1.0.6
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/dist/{aibroker-client-CQPGqLxL.mjs → aibroker-client-CXsyC7A8.mjs} +2 -2
- package/dist/aibroker-client-ChB7gSxV.mjs +3 -0
- package/dist/auto-route-Bc8xP6TQ.mjs +11 -0
- package/dist/{auto-route-DUYr57rs.mjs → auto-route-YaEGVrFl.mjs} +2 -2
- package/dist/budget-DEiebT2A.mjs +15 -0
- package/dist/{caller-identity-Bim8JT-D.mjs → caller-identity-C6Nc96iM.mjs} +4 -4
- package/dist/{capacity-CsZMkVyg.mjs → capacity-B2PgKAg4.mjs} +65 -5
- package/dist/{chain-Dzz6xcKH.mjs → chain-CtK49-Ma.mjs} +5 -5
- package/dist/cli/index.mjs +2102 -282
- package/dist/{cockpit-actions-BgsiNcUY.mjs → cockpit-actions-C73mJAJ8.mjs} +18 -18
- package/dist/{cockpit-actions-BTA1njMO.mjs → cockpit-actions-D1OPP6Es.mjs} +26 -26
- package/dist/cockpit-memory-CLHZD64W.mjs +20 -0
- package/dist/config/defaults/config.yaml +18 -1
- package/dist/{config-CST2uq1v.mjs → config-Bz8pAU5Q.mjs} +3 -3
- package/dist/{config-Dv3t7xnT.mjs → config-DHgPx3ER.mjs} +1 -1
- package/dist/{config-h5qK3fst.mjs → config-DLmyEK2q.mjs} +2 -2
- package/dist/{config-DYbyEe4-.mjs → config-SlZ-Z52i.mjs} +3 -2
- package/dist/{controls-OiVVbe-f.mjs → controls-rIItFeIJ.mjs} +59 -66
- package/dist/daemon/index.mjs +40 -40
- package/dist/daemon-176qA79N.mjs +46 -0
- package/dist/{daemon-C1dycZ3W.mjs → daemon-SoUvPUOf.mjs} +79 -31
- package/dist/daemon-mcp/index.mjs +49 -38
- package/dist/detect-C8nFP-kR.mjs +10 -0
- package/dist/{detect-CHuxFrLy.mjs → detect-DLjeYwnt.mjs} +2 -2
- package/dist/{embed-pass-DtVF2fOV.mjs → embed-pass-CN1D8MXl.mjs} +5 -5
- package/dist/{embedding-gate-B98PK_wz.mjs → embedding-gate-BVQvUmox.mjs} +2 -2
- package/dist/{embedding-gate-CTAk8STe.mjs → embedding-gate-iq0oQEkF.mjs} +3 -3
- package/dist/{endreason-BqyyU7BH.mjs → endreason-C2-hYK0_.mjs} +4 -4
- package/dist/{factory-BV0MQA3T.mjs → factory-CYwa0jYj.mjs} +6 -6
- package/dist/factory-DFwez99M.mjs +9 -0
- package/dist/{fallback-Bj2cyVk_.mjs → fallback-CwPSCrX8.mjs} +9 -231
- package/dist/{health-kufVIf6i.mjs → health-D0mUAVdw.mjs} +5 -5
- package/dist/{helpers-CvbYEKoD.mjs → helpers-Dj2nY56t.mjs} +2 -2
- package/dist/{helpers-WV8tdQHM.mjs → helpers-DyHKAwrW.mjs} +6 -6
- package/dist/hooks/capture-all-events.mjs +68 -3
- package/dist/hooks/capture-tool-output.mjs +68 -3
- package/dist/hooks/handoff-link.mjs +128 -0
- package/dist/hooks/load-project-context.mjs +1 -1
- package/dist/hooks/security-validator.mjs +840 -88
- package/dist/hooks/supervision-banner.mjs +186 -48
- package/dist/hooks/worker-guard.mjs +57 -1
- package/dist/hooks/worker-note.mjs +68 -3
- package/dist/hooks/worker-pool-host.mjs +1417 -224
- package/dist/hooks/worker-status-line.mjs +209 -70
- package/dist/index.mjs +9 -9
- package/dist/{ipc-client-CnYEF31R.mjs → ipc-client-CmagIFLh.mjs} +2 -2
- package/dist/{ipc-client-DN7EvoVo.mjs → ipc-client-DmbSIBUK.mjs} +1 -1
- package/dist/{kill-tree-CcpsRLCZ.mjs → kill-tree-DPiMscaY.mjs} +8 -8
- package/dist/kill-tree-Dxei6qlR.mjs +10 -0
- package/dist/{launch-UPQBoW2q.mjs → launch-BZT3JDoZ.mjs} +3 -3
- package/dist/{launch-DpQPZGsT.mjs → launch-Bn2Zf90h.mjs} +3 -3
- package/dist/{indexer-backend-B2vtFl38.mjs → lessons-CFVU0cwA.mjs} +262 -43
- package/dist/{local-candidate-Cp_bw4kK.mjs → local-candidate-DFd4pp-w.mjs} +3 -3
- package/dist/{local-instances-bdCNNSsL.mjs → local-instances-DhF4_sV2.mjs} +2 -2
- package/dist/{local-instances-B-c-CxzP.mjs → local-instances-KTh8IeOT.mjs} +4 -4
- package/dist/{local-tiers-BtdEEe5n.mjs → local-tiers-CqwGUcEn.mjs} +3 -3
- package/dist/{local-tiers-OBqAAfOg.mjs → local-tiers-DSlIYvvm.mjs} +3 -3
- package/dist/{main-config-ops-BYsK3BxM.mjs → main-config-ops-rda08BCD.mjs} +3 -3
- package/dist/{main-resolver-CAC3xfZM.mjs → main-resolver-BKwI0I6H.mjs} +18 -18
- package/dist/{migrate-DrA_OwlZ.mjs → migrate-C2z1YmWC.mjs} +2 -2
- package/dist/{model-inventory-BuTpi9a6.mjs → model-inventory-BXv15B0d.mjs} +13 -13
- package/dist/{models-add-CJx2YAwh.mjs → models-add-5yXTyz3g.mjs} +38 -38
- package/dist/operator-Bmv5tiHQ.mjs +7 -0
- package/dist/{operator-DrwWItVt.mjs → operator-Br6GeswF.mjs} +2 -2
- package/dist/{pai-marker-DLKJ9Yr5.mjs → pai-marker-D6OoVbM_.mjs} +2 -2
- package/dist/{pane-audit-wv9UkyCe.mjs → pane-audit-DLYy9-4g.mjs} +13 -23
- package/dist/{paths-DS5VQKoR.mjs → paths-CypxFTfn.mjs} +4 -4
- package/dist/{paths-DBd78I7Z.mjs → paths-DIOwNcj-.mjs} +3 -3
- package/dist/{planner-K0mFrCpL.mjs → planner-BzZ_2lc-.mjs} +29 -29
- package/dist/{pool-BZ94nFuq.mjs → pool-9EsbKD-U.mjs} +2 -2
- package/dist/{pool-cli-BjYQDfx6.mjs → pool-cli-Cy5mlycP.mjs} +6 -6
- package/dist/{pool-cli-B1eKySeN.mjs → pool-cli-Db0uDwB9.mjs} +5 -5
- package/dist/{postgres-CBHY5PIi.mjs → postgres-B1B80bcj.mjs} +3 -3
- package/dist/{postgres-C-T5OWer.mjs → postgres-BL5oBBgY.mjs} +3 -3
- package/dist/{project-setup-pIPHZaXf.mjs → project-setup-DHxdJjEc.mjs} +24 -25
- package/dist/providers-BH6hQYbU.mjs +16 -0
- package/dist/{providers-Dzx5GAUI.mjs → providers-BNV7CzC-.mjs} +6 -6
- package/dist/{query-feedback-DN52N9C5.mjs → query-feedback-BWl2HRwN.mjs} +2 -2
- package/dist/registrable-BwFiqazy.mjs +9 -0
- package/dist/{registrable-CXBBpUcz.mjs → registrable-Dxs4oKIj.mjs} +4 -4
- package/dist/render-DVGXpfaT.mjs +13 -0
- package/dist/render-jPpMrudf.mjs +2240 -0
- package/dist/root-C8cxhdn9.mjs +36 -0
- package/dist/{root-bG48WVpW.mjs → root-DmOzMeSv.mjs} +10 -10
- package/dist/route-table-COAmc1nB.mjs +15 -0
- package/dist/{router-CQSc3lk3.mjs → router-D2476pOp.mjs} +2 -2
- package/dist/{router-DwHJRgG9.mjs → router-ghujlEFb.mjs} +1 -1
- package/dist/{routing-D27jxj9x.mjs → routing-B_cg1_Xx.mjs} +13 -13
- package/dist/{routing-BlD_tKjW.mjs → routing-aJKMIagQ.mjs} +15 -15
- package/dist/{run-AZVwceiU.mjs → run-DE9Q0ldN.mjs} +47 -31
- package/dist/{runtime-paths-WsO9sJFc.mjs → runtime-paths-twfV23YL.mjs} +2 -2
- package/dist/{scan-C3uotg_f.mjs → scan-CNMcltOX.mjs} +7 -7
- package/dist/{scope-D8HK6vU7.mjs → scope-BTWstJ1B.mjs} +4 -4
- package/dist/session-DgZifjLx.mjs +35 -0
- package/dist/{session-BAfypQb9.mjs → session-HBYOABWU.mjs} +28 -28
- package/dist/session-scan-BZxnYN1v.mjs +9 -0
- package/dist/{session-scan-BmnjSReu.mjs → session-scan-CX3MKCNg.mjs} +6 -6
- package/dist/{session-usage-oycSdiD3.mjs → session-usage-CCmPFFa6.mjs} +3 -3
- package/dist/{status-COtzewKV.mjs → status-Cecau8Ps.mjs} +9 -5
- package/dist/status-D52xC_i0.mjs +6 -0
- package/dist/{tool-errors-DXJ70-sr.mjs → tool-errors-BFw1gv-z.mjs} +2 -2
- package/dist/{tool-errors-B2LnIqR0.mjs → tool-errors-COM2JGEr.mjs} +6 -6
- package/dist/viewer-Ce-Xa9L3.mjs +22 -0
- package/dist/{viewer-BgGRqXfD.mjs → viewer-DvBg6N22.mjs} +16 -14
- package/dist/{wait-ChpHKJZV.mjs → wait-C5iWILj_.mjs} +3 -3
- package/dist/worker-config-BmcJ9R0U.mjs +42 -0
- package/dist/{worker-config-DgdT0iTB.mjs → worker-config-DkONNk_H.mjs} +54 -103
- package/dist/{workers-config-BhZBrR-Q.mjs → workers-config-DwzrtdYu.mjs} +2 -2
- package/dist/{worktree-BY--Rgmy.mjs → worktree-O_kU6Q3y.mjs} +164 -8
- package/dist/{zettelkasten-BHVx5zj-.mjs → zettelkasten-BH8AdNh8.mjs} +4 -4
- package/dist/{zettelkasten-LoH9RBiN.mjs → zettelkasten-FFhQFESJ.mjs} +2 -2
- package/docs/commands/README.md +32 -2
- package/docs/commands/audit.md +34 -3
- package/docs/commands/backup.md +1 -1
- package/docs/commands/build.md +4 -4
- package/docs/commands/cd.md +1 -1
- package/docs/commands/clear-names.md +1 -1
- package/docs/commands/cockpit.md +1 -1
- package/docs/commands/config.md +1 -1
- package/docs/commands/daemon.md +1 -1
- package/docs/commands/db.md +1 -1
- package/docs/commands/doc.md +1 -1
- package/docs/commands/end.md +1 -1
- package/docs/commands/find.md +1 -1
- package/docs/commands/git.md +110 -0
- package/docs/commands/handoff.md +70 -0
- package/docs/commands/harness.md +74 -0
- package/docs/commands/help.md +1 -1
- package/docs/commands/home.md +1 -1
- package/docs/commands/hooks-db.md +1 -1
- package/docs/commands/identity.md +1 -1
- package/docs/commands/install.md +1 -1
- package/docs/commands/issue.md +1 -1
- package/docs/commands/kg.md +1 -1
- package/docs/commands/launch.md +1 -1
- package/docs/commands/lessons.md +201 -0
- package/docs/commands/list.md +1 -1
- package/docs/commands/ls.md +1 -1
- package/docs/commands/mcp.md +1 -1
- package/docs/commands/memory.md +1 -1
- package/docs/commands/models.md +1 -1
- package/docs/commands/more.md +1 -1
- package/docs/commands/new.md +1 -1
- package/docs/commands/notes.md +1 -1
- package/docs/commands/notify.md +1 -1
- package/docs/commands/observation.md +1 -1
- package/docs/commands/obsidian.md +1 -1
- package/docs/commands/open.md +1 -1
- package/docs/commands/pane.md +1 -1
- package/docs/commands/pause.md +1 -1
- package/docs/commands/plan.md +1 -1
- package/docs/commands/project.md +1 -1
- package/docs/commands/projects.md +1 -1
- package/docs/commands/providers.md +1 -1
- package/docs/commands/queue.md +1 -1
- package/docs/commands/registry.md +1 -1
- package/docs/commands/reload.md +1 -1
- package/docs/commands/rename.md +1 -1
- package/docs/commands/restore.md +1 -1
- package/docs/commands/resume.md +1 -1
- package/docs/commands/router.md +1 -1
- package/docs/commands/rules.md +70 -0
- package/docs/commands/session.md +1 -1
- package/docs/commands/sessions.md +1 -1
- package/docs/commands/setup.md +1 -1
- package/docs/commands/setups.md +1 -1
- package/docs/commands/shell-init.md +1 -1
- package/docs/commands/skill.md +1 -1
- package/docs/commands/task.md +1 -1
- package/docs/commands/tasks.md +1 -1
- package/docs/commands/topic.md +1 -1
- package/docs/commands/update.md +1 -1
- package/docs/commands/worker.md +1 -1
- package/docs/commands/zettel.md +1 -1
- package/docs/concepts/panes.md +3 -2
- package/gemini-extension.json +1 -1
- package/package.json +2 -2
- package/pai-plugin.json +1 -1
- package/plugins/context-preservation/plugin.json +1 -1
- package/plugins/core/plugin.json +1 -1
- 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/ts/lib/loop-guard.ts +3 -5
- package/src/hooks/ts/lib/rules/builtin/base64-decode-to-shell.rule.yaml +13 -0
- package/src/hooks/ts/lib/rules/builtin/chmod-world-writable.rule.yaml +13 -0
- package/src/hooks/ts/lib/rules/builtin/chmod-zero.rule.yaml +11 -0
- package/src/hooks/ts/lib/rules/builtin/credential-file-read.rule.yaml +13 -0
- package/src/hooks/ts/lib/rules/builtin/credential-read-then-network.rule.yaml +29 -0
- package/src/hooks/ts/lib/rules/builtin/crontab-persistence.rule.yaml +10 -0
- package/src/hooks/ts/lib/rules/builtin/curl-piped-to-shell.rule.yaml +15 -0
- package/src/hooks/ts/lib/rules/builtin/download-then-execute.rule.yaml +30 -0
- package/src/hooks/ts/lib/rules/builtin/find-root-delete.rule.yaml +11 -0
- package/src/hooks/ts/lib/rules/builtin/git-push-force.rule.yaml +24 -0
- package/src/hooks/ts/lib/rules/builtin/git-push-plus-refspec.rule.yaml +12 -0
- package/src/hooks/ts/lib/rules/builtin/git-reset-hard.rule.yaml +12 -0
- package/src/hooks/ts/lib/rules/builtin/injection-then-tool.rule.yaml +30 -0
- package/src/hooks/ts/lib/rules/builtin/instruction-override-disregard.rule.yaml +12 -0
- package/src/hooks/ts/lib/rules/builtin/instruction-override-ignore.rule.yaml +13 -0
- package/src/hooks/ts/lib/rules/builtin/launchagents-write-persistence.rule.yaml +12 -0
- package/src/hooks/ts/lib/rules/builtin/launchctl-load-persistence.rule.yaml +10 -0
- package/src/hooks/ts/lib/rules/builtin/reverse-shell-dev-tcp.rule.yaml +11 -0
- package/src/hooks/ts/lib/rules/builtin/reverse-shell-interactive-bash.rule.yaml +11 -0
- package/src/hooks/ts/lib/rules/builtin/rm-recursive-parent.rule.yaml +11 -0
- package/src/hooks/ts/lib/rules/builtin/rm-recursive-relative.rule.yaml +11 -0
- package/src/hooks/ts/lib/rules/builtin/rm-recursive-root.rule.yaml +24 -0
- package/src/hooks/ts/lib/rules/builtin/rm-trailing-tilde.rule.yaml +11 -0
- package/src/hooks/ts/lib/rules/builtin/sudoers-nopasswd.rule.yaml +15 -0
- package/src/hooks/ts/lib/rules/builtin.generated.ts +530 -0
- package/src/hooks/ts/lib/rules/eval.ts +74 -0
- package/src/hooks/ts/lib/rules/lint.ts +102 -0
- package/src/hooks/ts/lib/rules/load.ts +229 -0
- package/src/hooks/ts/lib/rules/types.ts +56 -0
- package/src/hooks/ts/pre-tool-use/security-validator.ts +27 -96
- package/src/hooks/ts/user-prompt/handoff-link.ts +34 -0
- package/dist/aibroker-client-BxzeEHPF.mjs +0 -3
- package/dist/auto-route-DWYK_0i0.mjs +0 -11
- package/dist/budget-DX-VIWGf.mjs +0 -15
- package/dist/cockpit-memory-DIrNs4xr.mjs +0 -20
- package/dist/daemon-ieo0lnpq.mjs +0 -46
- package/dist/detect-C0ZBXqDL.mjs +0 -10
- package/dist/factory-DeASMV7Z.mjs +0 -9
- package/dist/kill-tree-CXZ0WfVf.mjs +0 -10
- package/dist/operator-Bpxq_WB5.mjs +0 -7
- package/dist/providers-QJzpaNhX.mjs +0 -16
- package/dist/registrable-Drn8wIDO.mjs +0 -9
- package/dist/render-C4KR8qQ8.mjs +0 -13
- package/dist/render-Cz3BWIBO.mjs +0 -903
- package/dist/root-Bfb1zgwE.mjs +0 -36
- package/dist/route-table-ZVMNBsy7.mjs +0 -15
- package/dist/session-BcnhepLZ.mjs +0 -35
- package/dist/session-scan-CqUQdCac.mjs +0 -9
- package/dist/status-D_SWmOlH.mjs +0 -6
- package/dist/viewer-CbGN2Ri4.mjs +0 -22
- package/dist/worker-config-D1KVAMp4.mjs +0 -42
package/dist/cli/index.mjs
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { _ as warn$1, a as fmtDate, c as ok$1, d as scaffoldProjectDirs, f as shortenPath, g as stripAnsi, h as smartDecodeDir, i as err, l as renderTable$1, m as slugify, n as dim$1, o as header, r as encodeDir, s as now, t as bold$1, u as resolvePath } from "../utils-J2b9y8PC.mjs";
|
|
3
|
-
import { _ as
|
|
4
|
-
import { a as slugify$1, i as parseSessionFilename } from "../migrate-
|
|
5
|
-
import { Et as appendLedger, M as readWorkersSection, R as LOOPBACK_HOST, _t as DEFAULT_CONTEXT_WINDOW, bt as stripModelVariant, c as WorkersConfigError, et as relocateWorkersYaml, f as escalationCloud, m as isLocalProvider, p as getProviderOrNative, rt as workersYamlPath, t as ANTHROPIC_NATIVE, v as nativeAnthropicModels, yt as contextWindowFromModelId } from "../config-
|
|
6
|
-
import { A as DEFAULT_PG_CONNECTION_TIMEOUT_MS, C as migrateMainConfigToYaml, F as quoteIdent, M as isContainerTarget, N as postgresConnectionString, O as readJsonStrict, P as postgresFullTarget, S as MainConfigError, T as resolvedMainConfigPath, c as loadConfig, d as paiConfigYamlFilePath, f as readMainConfigRaw, j as assertPgName, k as writeJsonAtomic, l as migrateConfigFile, n as CONFIG_FILE$2, o as expandHome, p as writeMainConfigRaw, t as CONFIG_DIR, u as paiConfigFilePath } from "../config-
|
|
7
|
-
import { i as ledgerPath, n as ensureNoMcpConfig, u as workersLogDir } from "../paths-
|
|
8
|
-
import "../worktree-
|
|
9
|
-
import {
|
|
10
|
-
import { A as readClaudeJson, N as writeClaudeJson, S as inputSideTokens, h as poolSettings, i as bundleBuildStamp, j as registerMcpScript, k as mcpEntryRunsScript, v as readPoolState, w as expandMcpNames } from "../pool-
|
|
11
|
-
import { a as unregistrableReason, s as worktreeReason } from "../registrable-
|
|
12
|
-
import "../pai-marker-
|
|
3
|
+
import { _ as paiHomePath, a as paiCacheDir, c as PaiFileMigrationError, d as defaultPaiHomeDir, f as legacyConfigDir, g as paiHomeDir, h as migratePaiFile, l as claudeHomeDir, m as migratePaiDir, o as paiSocketPath, r as daemonLogPath, s as schedulerLogPath, u as claudeProjectsDir, v as resolvePaiFile } from "../runtime-paths-twfV23YL.mjs";
|
|
4
|
+
import { a as slugify$1, i as parseSessionFilename } from "../migrate-C2z1YmWC.mjs";
|
|
5
|
+
import { Et as appendLedger, M as readWorkersSection, R as LOOPBACK_HOST, _t as DEFAULT_CONTEXT_WINDOW, bt as stripModelVariant, c as WorkersConfigError, et as relocateWorkersYaml, f as escalationCloud, m as isLocalProvider, p as getProviderOrNative, rt as workersYamlPath, t as ANTHROPIC_NATIVE, v as nativeAnthropicModels, yt as contextWindowFromModelId } from "../config-Bz8pAU5Q.mjs";
|
|
6
|
+
import { A as DEFAULT_PG_CONNECTION_TIMEOUT_MS, C as migrateMainConfigToYaml, F as quoteIdent, M as isContainerTarget, N as postgresConnectionString, O as readJsonStrict, P as postgresFullTarget, S as MainConfigError, T as resolvedMainConfigPath, c as loadConfig, d as paiConfigYamlFilePath, f as readMainConfigRaw, h as effectiveMainConfig, j as assertPgName, k as writeJsonAtomic, l as migrateConfigFile, n as CONFIG_FILE$2, o as expandHome, p as writeMainConfigRaw, t as CONFIG_DIR, u as paiConfigFilePath } from "../config-SlZ-Z52i.mjs";
|
|
7
|
+
import { i as ledgerPath, n as ensureNoMcpConfig, r as eventsPath, u as workersLogDir } from "../paths-CypxFTfn.mjs";
|
|
8
|
+
import "../worktree-O_kU6Q3y.mjs";
|
|
9
|
+
import { K as writeSectionToForge, R as formatStale, V as loadStatusConfig, W as upsertSection, b as loadStatusesCached, g as isLive, h as isChatPane, v as loadStatus, y as loadStatuses } from "../status-Cecau8Ps.mjs";
|
|
10
|
+
import { A as readClaudeJson, N as writeClaudeJson, S as inputSideTokens, h as poolSettings, i as bundleBuildStamp, j as registerMcpScript, k as mcpEntryRunsScript, v as readPoolState, w as expandMcpNames } from "../pool-9EsbKD-U.mjs";
|
|
11
|
+
import { a as unregistrableReason, s as worktreeReason } from "../registrable-Dxs4oKIj.mjs";
|
|
12
|
+
import "../pai-marker-D6OoVbM_.mjs";
|
|
13
13
|
import "../helpers-CRuBUas3.mjs";
|
|
14
14
|
import { t as applyRecencyBoost } from "../search-RSyeQqj1.mjs";
|
|
15
15
|
import { t as dropItermBundleId } from "../env-BE8mrsxr.mjs";
|
|
16
|
-
import { a as ownerSessionName, d as terminalHasName } from "../scope-
|
|
17
|
-
import { At as resolveBindAddress, Bn as setParkConfigSource, Dn as allSetups, Dt as routerPort, E as budgetCache, Et as routerHandler, F as LimitsCache, Gn as appendUsage, Hn as recordDuration, In as setupsPath, Kn as formatSessionStats, Nn as saveSetup, On as downLocalProviders, Ot as defaultV1Guard, Sn as providerHostPort, U as computeInventory, Un as aggregate, Vn as sharedParker, W as currentInventory, Wn as aggregateBySession, Xn as parseSince, Y as inventoryText, Zn as readUsage, at as readInventoryInput, jn as loadSaved, kt as displayHost, m as readRoutingState, pn as readProfiles, pt as isLoopbackProvider, qn as formatStats, qt as sessionStartTarget, un as SESSION_NAME_ENV, vn as localProviderForDelegation, x as SESSION_WORKER_PROVIDER_ENV, xt as routeVerdictLine, zn as parkConfigFrom } from "../routing-
|
|
16
|
+
import { a as ownerSessionName, d as terminalHasName } from "../scope-BTWstJ1B.mjs";
|
|
17
|
+
import { At as resolveBindAddress, Bn as setParkConfigSource, Dn as allSetups, Dt as routerPort, E as budgetCache, Et as routerHandler, F as LimitsCache, Gn as appendUsage, Hn as recordDuration, In as setupsPath, Kn as formatSessionStats, Nn as saveSetup, On as downLocalProviders, Ot as defaultV1Guard, Sn as providerHostPort, U as computeInventory, Un as aggregate, Vn as sharedParker, W as currentInventory, Wn as aggregateBySession, Xn as parseSince, Y as inventoryText, Zn as readUsage, at as readInventoryInput, jn as loadSaved, kt as displayHost, m as readRoutingState, pn as readProfiles, pt as isLoopbackProvider, qn as formatStats, qt as sessionStartTarget, un as SESSION_NAME_ENV, vn as localProviderForDelegation, x as SESSION_WORKER_PROVIDER_ENV, xt as routeVerdictLine, zn as parkConfigFrom } from "../routing-aJKMIagQ.mjs";
|
|
18
18
|
import { n as minOf } from "../min-max-DrSRUaWQ.mjs";
|
|
19
|
-
import "../tool-errors-
|
|
20
|
-
import "../wait-
|
|
21
|
-
import "../local-tiers-
|
|
22
|
-
import { a as buildsDir, c as listBuilds, f as removeTicket, h as tryStart, i as buildNext, o as enqueue, p as renderBuildQueue, r as buildLogPath, s as getTicket, u as patchTicket, v as poolSlots, y as slotsFor } from "../caller-identity-
|
|
19
|
+
import "../tool-errors-BFw1gv-z.mjs";
|
|
20
|
+
import "../wait-C5iWILj_.mjs";
|
|
21
|
+
import "../local-tiers-CqwGUcEn.mjs";
|
|
22
|
+
import { a as buildsDir, c as listBuilds, f as removeTicket, h as tryStart, i as buildNext, o as enqueue, p as renderBuildQueue, r as buildLogPath, s as getTicket, u as patchTicket, v as poolSlots, y as slotsFor } from "../caller-identity-C6Nc96iM.mjs";
|
|
23
23
|
import { d as setSchedConfigSource, f as sharedScheduler, i as mirrorTo, s as schedConfigFrom } from "../scheduler-CtUyWJ3O.mjs";
|
|
24
|
-
import { i as effectiveCap, o as providerCap, r as capacityOf } from "../capacity-
|
|
25
|
-
import "../local-instances-
|
|
26
|
-
import { n as tierProviderNames, t as isLocalCandidate } from "../local-candidate-
|
|
24
|
+
import { i as effectiveCap, o as providerCap, r as capacityOf } from "../capacity-B2PgKAg4.mjs";
|
|
25
|
+
import "../local-instances-DhF4_sV2.mjs";
|
|
26
|
+
import { n as tierProviderNames, t as isLocalCandidate } from "../local-candidate-DFd4pp-w.mjs";
|
|
27
27
|
import { n as OPT, t as CliError } from "../define-CjJsmq-6.mjs";
|
|
28
|
-
import { a as getStorageBackend, c as MergeError, f as humanDuration, i as getRegistryBackend, l as federationDbPath, o as oldRegistryPath, r as createStorageBackend, s as registryDbPath, u as oldFederationPath } from "../factory-
|
|
29
|
-
import { c as resolveFromModule, i as retryTransient, n as embedChunksWithBackend, r as indexProjectWithBackend, s as packageRoot } from "../postgres-
|
|
30
|
-
import { d as OLLAMA_ID, f as ollamaDefaultUrl, i as measureThroughput, l as provisionOllama, o as createBackend, s as detectBackends, u as OLLAMA_DEFAULT_MODEL } from "../embedding-gate-
|
|
28
|
+
import { a as getStorageBackend, c as MergeError, f as humanDuration, i as getRegistryBackend, l as federationDbPath, o as oldRegistryPath, r as createStorageBackend, s as registryDbPath, u as oldFederationPath } from "../factory-CYwa0jYj.mjs";
|
|
29
|
+
import { c as resolveFromModule, i as retryTransient, n as embedChunksWithBackend, r as indexProjectWithBackend, s as packageRoot } from "../postgres-BL5oBBgY.mjs";
|
|
30
|
+
import { d as OLLAMA_ID, f as ollamaDefaultUrl, i as measureThroughput, l as provisionOllama, o as createBackend, s as detectBackends, u as OLLAMA_DEFAULT_MODEL } from "../embedding-gate-iq0oQEkF.mjs";
|
|
31
31
|
import "../embeddings-BVq3esBF.mjs";
|
|
32
|
-
import "../router-
|
|
33
|
-
import { t as detectProject } from "../detect-
|
|
34
|
-
import { n as getProject$1, o as requireProject, r as getProjectAliases, s as resolveIdentifier } from "../helpers-
|
|
35
|
-
import { a as resolveHome, c as upsertSession, d as scanTranscriptFolders, f as transcriptFiles, l as claudeProjectsDir$1, n as loadScanConfig, o as saveScanConfig, r as migrateScanConfig, s as upsertProject, t as cmdScan, u as findMovedProjects } from "../scan-
|
|
36
|
-
import { A as cmdAdd, B as cmdRebind, F as cmdEdit, H as cmdUnarchive, I as cmdGo, L as cmdInfo$1, M as cmdArchive, N as cmdConsolidate, P as cmdDetect, R as cmdList$1, S as legacy, U as findMovedPath, V as cmdTag$1, _ as cmdList, a as newTop, b as cmdSlug, f as sessionCommand, g as cmdInfo, h as cmdAutoRoute, i as lsCommand, j as cmdAlias, l as renameTop, m as cmdActive, r as collectView, s as openTop, v as cmdRename, x as cmdTag, y as cmdRoute, z as cmdMove } from "../session-
|
|
37
|
-
import { a as detectCurrentProviderName, i as detectCurrentModel, n as buildLaunchPlan, o as launchableProviderNames, r as buildLaunchRows, s as maskLaunchEnv } from "../launch-
|
|
38
|
-
import { a as colorEnabled, c as renderCommandHelp, d as renderOverview, f as renderTable$2, l as renderMarkdown, m as usageLine, o as makeStyle, p as suggest, s as mdCommand, u as renderMore } from "../health-
|
|
39
|
-
import { a as providerResumePlan, n as launchInDir, o as restoreTopLevel, r as launchPrompt, s as resumeArgvText, t as hasConversation } from "../launch-
|
|
40
|
-
import { _ as setupsTable, a as lastSetup, m as rootCommand } from "../root-
|
|
41
|
-
import { n as fetchLiveSessions, r as sendToSession, t as callAiBroker } from "../aibroker-client-
|
|
42
|
-
import { a as resolveSessionByNameOrId, o as scanCacheFilePath, r as migrateSessionScanCache, s as scanSessions, t as fmtAge } from "../session-scan-
|
|
43
|
-
import { A as routerPidFile, B as usageContextTokens, D as routerHandoverFile, E as probeRouter, F as startHandover, I as paneWaiver, L as launchPane, M as routerSwapArgs, P as spawnRouter, R as noTerminalReasons, S as usageJson, T as projectLaunchConfig, b as renderUsage, c as reportOriginWithHost, f as dirtyPaths, g as realRunnerDeps, j as routerRunningPid, k as routerLogFile, l as runMessenger, s as finishDetached, u as startDetached, w as pinNotices, x as transcriptUsage, y as priceTableText } from "../run-
|
|
44
|
-
import { C as readRunLog, D as resumeState, E as requestPlanStop, F as runLogPath, I as PlanTracker, M as dirtyConflicts, O as runExitCode, P as loadPlan, S as planStopPath, T as renderStatus, Y as readRunLock, ht as resolveHostSession, j as PlanError,
|
|
45
|
-
import "../endreason-
|
|
46
|
-
import { N as endBuildWait, j as beginBuildWait } from "../render-
|
|
47
|
-
import "../operator-
|
|
48
|
-
import "../kill-tree-
|
|
49
|
-
import { s as poolReloadLive } from "../pool-cli-
|
|
50
|
-
import "../chain-
|
|
51
|
-
import { _ as issueClaims, a as branchDiff, b as workerCommand, c as runQueue, d as loadSettings, f as move, g as issueClaim, h as renderReview, i as projectChecks, l as QueueStore, m as renderList, n as initProject, o as realQueueDeps, p as queueStatePath, r as isGitProject, s as acceptItems, t as formatCheck, u as again, v as issueStale } from "../project-setup-
|
|
52
|
-
import { m as runInPane, r as closeFinishedPanes } from "../pane-audit-
|
|
53
|
-
import "../viewer-
|
|
54
|
-
import "../
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
57
|
-
import {
|
|
58
|
-
import {
|
|
59
|
-
import { n as
|
|
60
|
-
import { a as
|
|
61
|
-
import
|
|
62
|
-
import {
|
|
63
|
-
import { n as queriesDirPath, t as migrateQueriesDir } from "../query-feedback-
|
|
32
|
+
import "../router-D2476pOp.mjs";
|
|
33
|
+
import { t as detectProject } from "../detect-DLjeYwnt.mjs";
|
|
34
|
+
import { n as getProject$1, o as requireProject, r as getProjectAliases, s as resolveIdentifier } from "../helpers-Dj2nY56t.mjs";
|
|
35
|
+
import { a as resolveHome, c as upsertSession, d as scanTranscriptFolders, f as transcriptFiles, l as claudeProjectsDir$1, n as loadScanConfig, o as saveScanConfig, r as migrateScanConfig, s as upsertProject, t as cmdScan, u as findMovedProjects } from "../scan-CNMcltOX.mjs";
|
|
36
|
+
import { A as cmdAdd, B as cmdRebind, F as cmdEdit, H as cmdUnarchive, I as cmdGo, L as cmdInfo$1, M as cmdArchive, N as cmdConsolidate, P as cmdDetect, R as cmdList$1, S as legacy, U as findMovedPath, V as cmdTag$1, _ as cmdList, a as newTop, b as cmdSlug, f as sessionCommand, g as cmdInfo, h as cmdAutoRoute, i as lsCommand, j as cmdAlias, l as renameTop, m as cmdActive, r as collectView, s as openTop, v as cmdRename, x as cmdTag, y as cmdRoute, z as cmdMove } from "../session-HBYOABWU.mjs";
|
|
37
|
+
import { a as detectCurrentProviderName, i as detectCurrentModel, n as buildLaunchPlan, o as launchableProviderNames, r as buildLaunchRows, s as maskLaunchEnv } from "../launch-Bn2Zf90h.mjs";
|
|
38
|
+
import { a as colorEnabled, c as renderCommandHelp, d as renderOverview, f as renderTable$2, l as renderMarkdown, m as usageLine, o as makeStyle, p as suggest, s as mdCommand, u as renderMore } from "../health-D0mUAVdw.mjs";
|
|
39
|
+
import { a as providerResumePlan, n as launchInDir, o as restoreTopLevel, r as launchPrompt, s as resumeArgvText, t as hasConversation } from "../launch-BZT3JDoZ.mjs";
|
|
40
|
+
import { _ as setupsTable, a as lastSetup, m as rootCommand } from "../root-DmOzMeSv.mjs";
|
|
41
|
+
import { n as fetchLiveSessions, r as sendToSession, t as callAiBroker } from "../aibroker-client-CXsyC7A8.mjs";
|
|
42
|
+
import { a as resolveSessionByNameOrId, o as scanCacheFilePath, r as migrateSessionScanCache, s as scanSessions, t as fmtAge } from "../session-scan-CX3MKCNg.mjs";
|
|
43
|
+
import { A as routerPidFile, B as usageContextTokens, D as routerHandoverFile, E as probeRouter, F as startHandover, I as paneWaiver, L as launchPane, M as routerSwapArgs, P as spawnRouter, R as noTerminalReasons, S as usageJson, T as projectLaunchConfig, b as renderUsage, c as reportOriginWithHost, f as dirtyPaths, g as realRunnerDeps, j as routerRunningPid, k as routerLogFile, l as runMessenger, s as finishDetached, u as startDetached, w as pinNotices, x as transcriptUsage, y as priceTableText } from "../run-DE9Q0ldN.mjs";
|
|
44
|
+
import { C as readRunLog, D as resumeState, E as requestPlanStop, F as runLogPath, I as PlanTracker, M as dirtyConflicts, O as runExitCode, Ot as runPaneExec, P as loadPlan, S as planStopPath, T as renderStatus, Y as readRunLock, ht as resolveHostSession, j as PlanError, k as runPlan, l as PlanReporter, nt as closePaneForWorker, ot as inVisiblePane, q as planLockPath, rt as closeSelfPane, u as QueueReporter, vt as writePaneTitle, w as renderSchedule, x as planStatus, y as blockedMessage } from "../controls-rIItFeIJ.mjs";
|
|
45
|
+
import "../endreason-C2-hYK0_.mjs";
|
|
46
|
+
import { B as scanTranscript, H as loadBuildsConfig, L as listRules, N as endBuildWait, P as claudeSessionTranscript, R as loadRules, V as maskSecrets, j as beginBuildWait, q as psTable, z as userRuleDirs } from "../render-jPpMrudf.mjs";
|
|
47
|
+
import "../operator-Br6GeswF.mjs";
|
|
48
|
+
import "../kill-tree-DPiMscaY.mjs";
|
|
49
|
+
import { s as poolReloadLive } from "../pool-cli-Db0uDwB9.mjs";
|
|
50
|
+
import "../chain-CtK49-Ma.mjs";
|
|
51
|
+
import { _ as issueClaims, a as branchDiff, b as workerCommand, c as runQueue, d as loadSettings, f as move, g as issueClaim, h as renderReview, i as projectChecks, l as QueueStore, m as renderList, n as initProject, o as realQueueDeps, p as queueStatePath, r as isGitProject, s as acceptItems, t as formatCheck, u as again, v as issueStale } from "../project-setup-DHxdJjEc.mjs";
|
|
52
|
+
import { m as runInPane, r as closeFinishedPanes } from "../pane-audit-DLYy9-4g.mjs";
|
|
53
|
+
import "../viewer-DvBg6N22.mjs";
|
|
54
|
+
import { g as setWorkersEnabled, s as describeProviders } from "../providers-BNV7CzC-.mjs";
|
|
55
|
+
import { $ as exportDefaultLayout, B as mergeSettings, G as cachedRows, J as handleCockpit, K as cachedSessionMap, Q as warmCockpit, W as SystemMonitor, X as setEndpointHealth, Y as setActionHooks, Z as setRuntimeSampler, ct as providersCommand, et as createEndpointHealth, it as withWorkerAttribution, n as WORKER_CONFIG_COMMANDS, nt as loadWorkerWindows, q as cachedWorkerWindows, rt as sessionDisplayName, t as PROVIDER_EDIT_COMMANDS, tt as loadSessionMap, z as installWorkers } from "../worker-config-DkONNk_H.mjs";
|
|
56
|
+
import { a as listConfigOp, i as getConfigValueOp, n as formatConfigGetOutput, o as setConfigValueOp, r as formatConfigSource, s as unsetConfigValueOp, t as MainConfigOpsError } from "../main-config-ops-rda08BCD.mjs";
|
|
57
|
+
import { H as acquireRun, U as readRun, V as cockpitProbeSettings, k as readRuntimeCommands } from "../cockpit-actions-D1OPP6Es.mjs";
|
|
58
|
+
import { n as PaiClient } from "../ipc-client-CmagIFLh.mjs";
|
|
59
|
+
import { a as EMBED_PASS_BATCH_SIZE, c as liveYieldCheck, n as DEFAULT_EMBED_MAX_LOAD, o as formatEmbedStatus, r as DEFAULT_EMBED_MAX_MINUTES, s as lastRecordedEmbedPass } from "../embed-pass-CN1D8MXl.mjs";
|
|
60
|
+
import { a as liveWorkersConfigShadows, i as totalUsageTokens, r as parseSessionUsage } from "../session-usage-CCmPFFa6.mjs";
|
|
61
|
+
import "../fallback-CwPSCrX8.mjs";
|
|
62
|
+
import { $t as readBodyFile, At as jobPaths, Ct as historyDir, D as migrateWorkQueue, Et as obsidianEnabled, Ft as sessionKeepaliveLedgerSummary, Gt as resolveDaemonPid, Ht as writeQuiet, Lt as describeQuiet, M as migrateSummaryCooldowns, Nt as loadSessionKeepaliveState, Qt as findNotesDir$1, Rt as nextOccurrence, Ut as LAUNCHD_LABEL, Xt as applyContinue, Yt as appendCheckpointToNote, Zt as findLatestNote, c as proposeFromTranscript, en as restartRouter, g as migrateKgBackfillState, h as backfillKgFromNotes, i as getLesson, jt as reapJob, kt as migrateIdentityFile, l as setState, n as LESSON_STATES, o as listLessons, p as reexecBackground, qt as readContextHandoverCache, r as addLesson, s as promoteLesson, t as LESSON_KINDS, wt as OBSIDIAN_DISABLED_MESSAGE, zt as readQuiet } from "../lessons-CFVU0cwA.mjs";
|
|
63
|
+
import { n as queriesDirPath, t as migrateQueriesDir } from "../query-feedback-BWl2HRwN.mjs";
|
|
64
64
|
import { appendFileSync, chmodSync, closeSync, copyFileSync, cpSync, createReadStream, createWriteStream, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
65
65
|
import { createHash, randomUUID } from "node:crypto";
|
|
66
|
-
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
66
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
67
67
|
import chalk from "chalk";
|
|
68
68
|
import { cpus, homedir, platform, userInfo } from "node:os";
|
|
69
69
|
import { execFile, execFileSync, execSync, spawn, spawnSync } from "node:child_process";
|
|
@@ -74,8 +74,8 @@ import pg from "pg";
|
|
|
74
74
|
import BetterSqlite3 from "better-sqlite3";
|
|
75
75
|
import { createInterface } from "node:readline";
|
|
76
76
|
import net$1, { createConnection as createConnection$1 } from "net";
|
|
77
|
-
import { promisify } from "node:util";
|
|
78
77
|
import { getEncoding } from "js-tiktoken";
|
|
78
|
+
import { promisify } from "node:util";
|
|
79
79
|
|
|
80
80
|
//#region src/cli/lib/exit.ts
|
|
81
81
|
/**
|
|
@@ -152,7 +152,7 @@ const GLOBAL_FLAGS = [{
|
|
|
152
152
|
type: "boolean",
|
|
153
153
|
summary: ""
|
|
154
154
|
}];
|
|
155
|
-
function parseArgs(cmd, argv) {
|
|
155
|
+
function parseArgs$1(cmd, argv) {
|
|
156
156
|
const defs = [...cmd.options ?? [], ...GLOBAL_FLAGS.filter((g) => !(cmd.options ?? []).some((o) => o.name === g.name))];
|
|
157
157
|
const byLong = new Map(defs.map((o) => [o.name, o]));
|
|
158
158
|
const byShort = new Map(defs.filter((o) => o.short).map((o) => [o.short, o]));
|
|
@@ -326,7 +326,7 @@ async function runCli(groups, argv, io = defaultIo(), root) {
|
|
|
326
326
|
rest: argv
|
|
327
327
|
} : resolve$1(groups, argv);
|
|
328
328
|
const { cmd, path, rest } = resolved;
|
|
329
|
-
const parsed = parseArgs(cmd, rest);
|
|
329
|
+
const parsed = parseArgs$1(cmd, rest);
|
|
330
330
|
const noArgs = rest.length === 0;
|
|
331
331
|
if (parsed.help || noArgs && !cmd.run && cmd.subcommands?.length) {
|
|
332
332
|
if (target) {
|
|
@@ -830,7 +830,7 @@ function routerSamplerDeps(sched, engineStatusTimeoutMs = () => DEFAULT_RUNTIME_
|
|
|
830
830
|
});
|
|
831
831
|
return {
|
|
832
832
|
inventory: async () => {
|
|
833
|
-
const { currentInventory } = await import("../model-inventory-
|
|
833
|
+
const { currentInventory } = await import("../model-inventory-BXv15B0d.mjs");
|
|
834
834
|
return currentInventory();
|
|
835
835
|
},
|
|
836
836
|
sched,
|
|
@@ -845,7 +845,7 @@ function routerSamplerDeps(sched, engineStatusTimeoutMs = () => DEFAULT_RUNTIME_
|
|
|
845
845
|
}
|
|
846
846
|
},
|
|
847
847
|
async rssMb(port) {
|
|
848
|
-
const { processTreeRssMb } = await import("../model-inventory-
|
|
848
|
+
const { processTreeRssMb } = await import("../model-inventory-BXv15B0d.mjs");
|
|
849
849
|
const pid = (await exec("lsof", [
|
|
850
850
|
"-ti",
|
|
851
851
|
`tcp:${port}`,
|
|
@@ -1214,9 +1214,9 @@ const runCommand = {
|
|
|
1214
1214
|
sampler.start();
|
|
1215
1215
|
setRuntimeSampler(sampler);
|
|
1216
1216
|
setActionHooks({ runLocalJob: async (kind, tier) => {
|
|
1217
|
-
const { startTier, stopAll } = await import("../worker-config-
|
|
1218
|
-
const { readLocalConfig } = await import("../local-tiers-
|
|
1219
|
-
const { workersYamlPath } = await import("../workers-config-
|
|
1217
|
+
const { startTier, stopAll } = await import("../worker-config-BmcJ9R0U.mjs");
|
|
1218
|
+
const { readLocalConfig } = await import("../local-tiers-DSlIYvvm.mjs");
|
|
1219
|
+
const { workersYamlPath } = await import("../workers-config-DwzrtdYu.mjs");
|
|
1220
1220
|
const cfg = readLocalConfig(workersYamlPath());
|
|
1221
1221
|
if (kind === "off") return void await stopAll(cfg);
|
|
1222
1222
|
const t = cfg.tiers[tier];
|
|
@@ -1593,7 +1593,7 @@ const modelsCommand = {
|
|
|
1593
1593
|
if (words[0] === "add") {
|
|
1594
1594
|
const source = words[1];
|
|
1595
1595
|
if (!source) throw new Error("usage: pai models add <hf-id|path> [--name n] [--download] [--try]");
|
|
1596
|
-
const { addModel } = await import("../models-add-
|
|
1596
|
+
const { addModel } = await import("../models-add-5yXTyz3g.mjs");
|
|
1597
1597
|
const r = await addModel({
|
|
1598
1598
|
source,
|
|
1599
1599
|
name: typeof ctx.opts.name === "string" ? ctx.opts.name : void 0,
|
|
@@ -1608,7 +1608,7 @@ const modelsCommand = {
|
|
|
1608
1608
|
if (op) {
|
|
1609
1609
|
const runtime = words[1];
|
|
1610
1610
|
if (!runtime) throw new Error(`usage: pai models ${op} <runtime> (configured: ${Object.keys(readRuntimeCommands()).join(", ") || "none"})`);
|
|
1611
|
-
const { defaultDeps, runtimeStartStop } = await import("../cockpit-actions-
|
|
1611
|
+
const { defaultDeps, runtimeStartStop } = await import("../cockpit-actions-C73mJAJ8.mjs");
|
|
1612
1612
|
const r = await runtimeStartStop(runtime, op, defaultDeps(), { model: typeof ctx.opts.model === "string" ? ctx.opts.model : void 0 });
|
|
1613
1613
|
process.stdout.write(r.message + "\n");
|
|
1614
1614
|
if (!r.ok) process.exitCode = 1;
|
|
@@ -1812,7 +1812,7 @@ const MAX_PROMPT_CHARS = 220;
|
|
|
1812
1812
|
/** Newest prompts kept per transcript — headroom over MAX_PROMPTS for the duplicate collapse. */
|
|
1813
1813
|
const KEEP_PROMPTS = 64;
|
|
1814
1814
|
/** Cap on listed working-tree entries, so a big refactor cannot flood TODO.md. */
|
|
1815
|
-
const MAX_FILES = 20;
|
|
1815
|
+
const MAX_FILES$1 = 20;
|
|
1816
1816
|
/**
|
|
1817
1817
|
* Text of a JSONL message, or null when the entry carries none.
|
|
1818
1818
|
* Mirrors the shape handled by slug-generator: content is either a plain
|
|
@@ -1998,8 +1998,8 @@ function readWorkingTree(cwd) {
|
|
|
1998
1998
|
"-1",
|
|
1999
1999
|
"--pretty=%h %s"
|
|
2000
2000
|
]),
|
|
2001
|
-
changes: all.slice(0, MAX_FILES),
|
|
2002
|
-
overflow: Math.max(0, all.length - MAX_FILES)
|
|
2001
|
+
changes: all.slice(0, MAX_FILES$1),
|
|
2002
|
+
overflow: Math.max(0, all.length - MAX_FILES$1)
|
|
2003
2003
|
};
|
|
2004
2004
|
}
|
|
2005
2005
|
/**
|
|
@@ -2923,7 +2923,7 @@ async function cmdPause(opts) {
|
|
|
2923
2923
|
process.exitCode = 1;
|
|
2924
2924
|
return;
|
|
2925
2925
|
}
|
|
2926
|
-
const { getRegistryBackend } = await import("../factory-
|
|
2926
|
+
const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
|
|
2927
2927
|
const registryBackend = await getRegistryBackend();
|
|
2928
2928
|
const cwd = process.cwd();
|
|
2929
2929
|
const project = await resolveProjectByCwd(registryBackend, cwd);
|
|
@@ -3023,7 +3023,7 @@ function touchSentinel(rootPath) {
|
|
|
3023
3023
|
async function cmdAutosave(opts) {
|
|
3024
3024
|
const minGap = parseInt(opts.minGap ?? String(DEFAULT_MIN_GAP), 10);
|
|
3025
3025
|
const cwd = process.cwd();
|
|
3026
|
-
const { getRegistryBackend } = await import("../factory-
|
|
3026
|
+
const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
|
|
3027
3027
|
const registryBackend = await getRegistryBackend();
|
|
3028
3028
|
const project = await resolveProjectByCwd(registryBackend, cwd);
|
|
3029
3029
|
if (!project) return;
|
|
@@ -3249,7 +3249,7 @@ function finalizeNote(notePath) {
|
|
|
3249
3249
|
}
|
|
3250
3250
|
async function cmdEnd(opts) {
|
|
3251
3251
|
await cmdPause(opts);
|
|
3252
|
-
const { getRegistryBackend } = await import("../factory-
|
|
3252
|
+
const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
|
|
3253
3253
|
const registryBackend = await getRegistryBackend();
|
|
3254
3254
|
const cwd = process.cwd();
|
|
3255
3255
|
const project = await registryBackend.findProjectByCwdPrefix(cwd);
|
|
@@ -3430,7 +3430,7 @@ function buildStopBody(project, sessionId) {
|
|
|
3430
3430
|
}
|
|
3431
3431
|
}
|
|
3432
3432
|
async function cmdHandover(projectSlug, numberOrLatest, sessionId) {
|
|
3433
|
-
const { getRegistryBackend } = await import("../factory-
|
|
3433
|
+
const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
|
|
3434
3434
|
const registryBackend = await getRegistryBackend();
|
|
3435
3435
|
let project;
|
|
3436
3436
|
if (projectSlug) {
|
|
@@ -4376,8 +4376,8 @@ async function analyzeProject(registryBackend, project) {
|
|
|
4376
4376
|
async function countVectorDbPaths(oldPaths) {
|
|
4377
4377
|
if (oldPaths.length === 0) return 0;
|
|
4378
4378
|
try {
|
|
4379
|
-
const { loadConfig } = await import("../config-
|
|
4380
|
-
const { createStorageBackend } = await import("../factory-
|
|
4379
|
+
const { loadConfig } = await import("../config-DHgPx3ER.mjs");
|
|
4380
|
+
const { createStorageBackend } = await import("../factory-DFwez99M.mjs");
|
|
4381
4381
|
const config = loadConfig();
|
|
4382
4382
|
if (config.storageBackend !== "postgres") return 0;
|
|
4383
4383
|
const backend = await createStorageBackend(config);
|
|
@@ -4394,8 +4394,8 @@ async function countVectorDbPaths(oldPaths) {
|
|
|
4394
4394
|
async function updateVectorDbPaths(moves) {
|
|
4395
4395
|
if (moves.length === 0) return 0;
|
|
4396
4396
|
try {
|
|
4397
|
-
const { loadConfig } = await import("../config-
|
|
4398
|
-
const { createStorageBackend } = await import("../factory-
|
|
4397
|
+
const { loadConfig } = await import("../config-DHgPx3ER.mjs");
|
|
4398
|
+
const { createStorageBackend } = await import("../factory-DFwez99M.mjs");
|
|
4399
4399
|
const config = loadConfig();
|
|
4400
4400
|
if (config.storageBackend !== "postgres") return 0;
|
|
4401
4401
|
const backend = await createStorageBackend(config);
|
|
@@ -4482,7 +4482,7 @@ async function displayDryRun(plans) {
|
|
|
4482
4482
|
console.log();
|
|
4483
4483
|
}
|
|
4484
4484
|
async function executeCleanup(plans, skipReindex) {
|
|
4485
|
-
const { getRegistryBackend } = await import("../factory-
|
|
4485
|
+
const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
|
|
4486
4486
|
const registryBackend = await getRegistryBackend();
|
|
4487
4487
|
let deleted = 0;
|
|
4488
4488
|
let renamed = 0;
|
|
@@ -4682,7 +4682,7 @@ async function executeCleanup(plans, skipReindex) {
|
|
|
4682
4682
|
* them into YYYY/MM/ folders and renumber. Dry run unless `execute` is set.
|
|
4683
4683
|
*/
|
|
4684
4684
|
async function runCleanup(projectSlug, opts) {
|
|
4685
|
-
const { getRegistryBackend } = await import("../factory-
|
|
4685
|
+
const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
|
|
4686
4686
|
const registryBackend = await getRegistryBackend();
|
|
4687
4687
|
const dryRun = !opts.execute;
|
|
4688
4688
|
const skipReindex = opts.reindex === false;
|
|
@@ -6664,7 +6664,7 @@ async function runRestore(backupPath, opts) {
|
|
|
6664
6664
|
* backup is written first. Any other content, or an unknown previous rendering,
|
|
6665
6665
|
* counts as user-edited: the new version goes beside it as CLAUDE.md.pai-new.
|
|
6666
6666
|
*/
|
|
6667
|
-
function stamp(d) {
|
|
6667
|
+
function stamp$1(d) {
|
|
6668
6668
|
return d.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
6669
6669
|
}
|
|
6670
6670
|
function writeAtomic(path, content) {
|
|
@@ -6683,7 +6683,7 @@ function refreshClaudeMd(claudeMd, newRendered, previousRendered, now = /* @__PU
|
|
|
6683
6683
|
newPath
|
|
6684
6684
|
};
|
|
6685
6685
|
}
|
|
6686
|
-
const backup = `${claudeMd}.bak-${stamp(now)}`;
|
|
6686
|
+
const backup = `${claudeMd}.bak-${stamp$1(now)}`;
|
|
6687
6687
|
copyFileSync(claudeMd, backup);
|
|
6688
6688
|
writeAtomic(claudeMd, newRendered);
|
|
6689
6689
|
return {
|
|
@@ -8258,6 +8258,10 @@ const SETTINGS_HOOKS = [
|
|
|
8258
8258
|
hookType: "UserPromptSubmit",
|
|
8259
8259
|
command: "${ADAPTER_DIR}/Hooks/local-model-hint.mjs"
|
|
8260
8260
|
},
|
|
8261
|
+
{
|
|
8262
|
+
hookType: "UserPromptSubmit",
|
|
8263
|
+
command: "${ADAPTER_DIR}/Hooks/handoff-link.mjs"
|
|
8264
|
+
},
|
|
8261
8265
|
{
|
|
8262
8266
|
hookType: "UserPromptSubmit",
|
|
8263
8267
|
command: "${ADAPTER_DIR}/Hooks/capture-all-events.mjs --event-type UserPromptSubmit"
|
|
@@ -8965,8 +8969,8 @@ function cmdLogs(opts) {
|
|
|
8965
8969
|
return 0;
|
|
8966
8970
|
}
|
|
8967
8971
|
async function daemonServeCmd() {
|
|
8968
|
-
const { serve } = await import("../daemon-
|
|
8969
|
-
const { loadConfig: lc, ensureConfigDir } = await import("../config-
|
|
8972
|
+
const { serve } = await import("../daemon-176qA79N.mjs");
|
|
8973
|
+
const { loadConfig: lc, ensureConfigDir } = await import("../config-DHgPx3ER.mjs");
|
|
8970
8974
|
ensureConfigDir();
|
|
8971
8975
|
await serve(lc());
|
|
8972
8976
|
return 0;
|
|
@@ -11137,6 +11141,232 @@ async function watchTicket(dir, id, deps = {}, intervalMs = 1e3, simctl, reap) {
|
|
|
11137
11141
|
}
|
|
11138
11142
|
}
|
|
11139
11143
|
|
|
11144
|
+
//#endregion
|
|
11145
|
+
//#region src/audit/tokens.ts
|
|
11146
|
+
/**
|
|
11147
|
+
* tokens.ts — the one tokenizer every audit section counts through.
|
|
11148
|
+
*
|
|
11149
|
+
* cl100k_base via js-tiktoken (pure JS, no native build). "all" + [] for the
|
|
11150
|
+
* allowed/disallowed-special-token arguments matches Python tiktoken's
|
|
11151
|
+
* `disallowed_special=()`: audited text (hook stdout, session transcripts)
|
|
11152
|
+
* routinely contains literal "<|...|>"-shaped substrings that are not actual
|
|
11153
|
+
* special tokens, and the strict default throws on sight of them.
|
|
11154
|
+
*/
|
|
11155
|
+
const TOKEN_ENCODING = "cl100k_base";
|
|
11156
|
+
let cached = null;
|
|
11157
|
+
function encoder() {
|
|
11158
|
+
if (!cached) cached = getEncoding(TOKEN_ENCODING);
|
|
11159
|
+
return cached;
|
|
11160
|
+
}
|
|
11161
|
+
function countTokens(text) {
|
|
11162
|
+
if (!text) return 0;
|
|
11163
|
+
return encoder().encode(text, "all", []).length;
|
|
11164
|
+
}
|
|
11165
|
+
|
|
11166
|
+
//#endregion
|
|
11167
|
+
//#region src/audit/hooks.ts
|
|
11168
|
+
/**
|
|
11169
|
+
* `pai audit tokens hooks` — token cost of the hooks that fire before the
|
|
11170
|
+
* model ever sees a token: SessionStart and UserPromptSubmit run on every
|
|
11171
|
+
* new session / every prompt respectively, and whatever they print to
|
|
11172
|
+
* stdout is injected into context.
|
|
11173
|
+
*/
|
|
11174
|
+
const HOOK_TIMEOUT_MS = 2e4;
|
|
11175
|
+
function settingsPaths(cwd) {
|
|
11176
|
+
return [join(claudeHomeDir(), "settings.json"), join(cwd, ".claude", "settings.json")].filter(existsSync);
|
|
11177
|
+
}
|
|
11178
|
+
function readSettings(path) {
|
|
11179
|
+
try {
|
|
11180
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
11181
|
+
} catch {
|
|
11182
|
+
return {};
|
|
11183
|
+
}
|
|
11184
|
+
}
|
|
11185
|
+
/** Expand ${PAI_DIR} and $HOME/${HOME} the way settings.json's own hooks do. */
|
|
11186
|
+
function expandCommand(command, env) {
|
|
11187
|
+
const paiDir = env.PAI_DIR ?? process.env.PAI_DIR ?? claudeHomeDir();
|
|
11188
|
+
return command.replace(/\$\{PAI_DIR\}/g, paiDir).replace(/\$\{HOME\}/g, homedir()).replace(/\$HOME\b/g, homedir());
|
|
11189
|
+
}
|
|
11190
|
+
function syntheticStdin(cwd, eventName) {
|
|
11191
|
+
const sessionId = randomUUID();
|
|
11192
|
+
return JSON.stringify({
|
|
11193
|
+
session_id: sessionId,
|
|
11194
|
+
cwd,
|
|
11195
|
+
hook_event_name: eventName,
|
|
11196
|
+
prompt: "audit",
|
|
11197
|
+
transcript_path: join(claudeProjectsDir(), "audit", `${sessionId}.jsonl`)
|
|
11198
|
+
});
|
|
11199
|
+
}
|
|
11200
|
+
function runHook(command, cwd, eventName) {
|
|
11201
|
+
const result = spawnSync(command, {
|
|
11202
|
+
shell: true,
|
|
11203
|
+
cwd,
|
|
11204
|
+
input: syntheticStdin(cwd, eventName),
|
|
11205
|
+
encoding: "utf8",
|
|
11206
|
+
timeout: HOOK_TIMEOUT_MS
|
|
11207
|
+
});
|
|
11208
|
+
if (result.error) return {
|
|
11209
|
+
stdout: "",
|
|
11210
|
+
error: result.error.message
|
|
11211
|
+
};
|
|
11212
|
+
if (result.signal) return {
|
|
11213
|
+
stdout: result.stdout ?? "",
|
|
11214
|
+
error: `killed by ${result.signal} (timeout?)`
|
|
11215
|
+
};
|
|
11216
|
+
return { stdout: result.stdout ?? "" };
|
|
11217
|
+
}
|
|
11218
|
+
const NOT_RUN = "not run (pass --run to execute hooks)";
|
|
11219
|
+
const AUDITED_EVENTS = ["SessionStart", "UserPromptSubmit"];
|
|
11220
|
+
/** Hook events the capture hook records, so a configured hook can be checked
|
|
11221
|
+
* against what actually fired. */
|
|
11222
|
+
const OBSERVABLE_EVENTS = [
|
|
11223
|
+
"PreToolUse",
|
|
11224
|
+
"PostToolUse",
|
|
11225
|
+
"UserPromptSubmit",
|
|
11226
|
+
"Stop",
|
|
11227
|
+
"SubagentStop",
|
|
11228
|
+
"SessionStart",
|
|
11229
|
+
"PreCompact",
|
|
11230
|
+
"Notification"
|
|
11231
|
+
];
|
|
11232
|
+
/** Default observation window for `--since`. */
|
|
11233
|
+
const DEFAULT_SINCE_HOURS = 24;
|
|
11234
|
+
/** Where capture-all-events writes its JSONL: PAI_HOME/History/raw-outputs. */
|
|
11235
|
+
function defaultCaptureDir() {
|
|
11236
|
+
return join(paiHomePath("History"), "raw-outputs");
|
|
11237
|
+
}
|
|
11238
|
+
/**
|
|
11239
|
+
* Counts hook events in the raw-capture JSONL files modified since `sinceMs`
|
|
11240
|
+
* (capture-all-events lines carry `hook_event_type` and `timestamp`). Files
|
|
11241
|
+
* are read line by line; malformed lines are skipped.
|
|
11242
|
+
*/
|
|
11243
|
+
async function observedHookEvents(captureDir, sinceMs) {
|
|
11244
|
+
const observed = /* @__PURE__ */ new Map();
|
|
11245
|
+
if (!existsSync(captureDir)) return observed;
|
|
11246
|
+
const monthDirs = readdirSync(captureDir, { withFileTypes: true }).filter((d) => d.isDirectory() && /^\d{4}-\d{2}$/.test(d.name)).map((d) => join(captureDir, d.name));
|
|
11247
|
+
for (const monthDir of monthDirs) for (const name of readdirSync(monthDir)) {
|
|
11248
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
11249
|
+
const file = join(monthDir, name);
|
|
11250
|
+
try {
|
|
11251
|
+
if (statSync(file).mtimeMs < sinceMs) continue;
|
|
11252
|
+
} catch {
|
|
11253
|
+
continue;
|
|
11254
|
+
}
|
|
11255
|
+
const rl = createInterface({
|
|
11256
|
+
input: createReadStream(file, "utf8"),
|
|
11257
|
+
crlfDelay: Infinity
|
|
11258
|
+
});
|
|
11259
|
+
for await (const raw of rl) foldObservedLine(raw, observed);
|
|
11260
|
+
}
|
|
11261
|
+
return observed;
|
|
11262
|
+
}
|
|
11263
|
+
/** True when the command references a file path that does not exist (a hook
|
|
11264
|
+
* script that was moved or deleted — the hook silently never runs). */
|
|
11265
|
+
function commandHasMissingFile(command) {
|
|
11266
|
+
for (const token of command.split(/\s+/)) {
|
|
11267
|
+
if (!token.includes("/")) continue;
|
|
11268
|
+
if (token.includes("$") || token.includes("*")) continue;
|
|
11269
|
+
if (!existsSync(token)) return true;
|
|
11270
|
+
}
|
|
11271
|
+
return false;
|
|
11272
|
+
}
|
|
11273
|
+
function auditHooks(cwd, run, maybeOpts) {
|
|
11274
|
+
const opts = typeof run === "function" ? maybeOpts ?? {} : run ?? {};
|
|
11275
|
+
const runner = typeof run === "function" ? run : void 0;
|
|
11276
|
+
const sinceHours = opts.sinceHours ?? DEFAULT_SINCE_HOURS;
|
|
11277
|
+
const now = opts.now ?? Date.now();
|
|
11278
|
+
const observed = observedHookEventsSync(opts.captureDir ?? defaultCaptureDir(), now - sinceHours * 36e5);
|
|
11279
|
+
const settingsFiles = settingsPaths(cwd);
|
|
11280
|
+
const readings = [];
|
|
11281
|
+
const totalsByEvent = {};
|
|
11282
|
+
const preToolUseHooks = [];
|
|
11283
|
+
let preToolUseRewritesBash = false;
|
|
11284
|
+
for (const settingsPath of settingsFiles) {
|
|
11285
|
+
const settings = readSettings(settingsPath);
|
|
11286
|
+
const env = settings.env ?? {};
|
|
11287
|
+
for (const eventName of AUDITED_EVENTS) {
|
|
11288
|
+
const groups = settings.hooks?.[eventName] ?? [];
|
|
11289
|
+
totalsByEvent[eventName] = totalsByEvent[eventName] ?? 0;
|
|
11290
|
+
for (const group of groups) for (const entry of group.hooks ?? []) {
|
|
11291
|
+
if (!entry.command) continue;
|
|
11292
|
+
const command = expandCommand(entry.command, env);
|
|
11293
|
+
const { stdout, error } = runner ? runner(command, cwd, eventName) : {
|
|
11294
|
+
stdout: "",
|
|
11295
|
+
error: NOT_RUN
|
|
11296
|
+
};
|
|
11297
|
+
const tokens = countTokens(stdout);
|
|
11298
|
+
totalsByEvent[eventName] += tokens;
|
|
11299
|
+
const seen = observed.get(eventName);
|
|
11300
|
+
readings.push({
|
|
11301
|
+
event: eventName,
|
|
11302
|
+
matcher: group.matcher,
|
|
11303
|
+
command,
|
|
11304
|
+
tokens,
|
|
11305
|
+
error,
|
|
11306
|
+
state: commandHasMissingFile(command) ? "missing-script" : seen ? "observed" : "configured",
|
|
11307
|
+
lastSeen: seen?.last
|
|
11308
|
+
});
|
|
11309
|
+
}
|
|
11310
|
+
}
|
|
11311
|
+
for (const group of settings.hooks?.PreToolUse ?? []) for (const entry of group.hooks ?? []) {
|
|
11312
|
+
if (!entry.command) continue;
|
|
11313
|
+
preToolUseHooks.push({
|
|
11314
|
+
matcher: group.matcher,
|
|
11315
|
+
command: expandCommand(entry.command, env)
|
|
11316
|
+
});
|
|
11317
|
+
if (group.matcher === "Bash") preToolUseRewritesBash = true;
|
|
11318
|
+
}
|
|
11319
|
+
}
|
|
11320
|
+
return {
|
|
11321
|
+
encoding: TOKEN_ENCODING,
|
|
11322
|
+
settingsFiles,
|
|
11323
|
+
readings,
|
|
11324
|
+
totalsByEvent,
|
|
11325
|
+
preToolUseHooks,
|
|
11326
|
+
preToolUseRewritesBash
|
|
11327
|
+
};
|
|
11328
|
+
}
|
|
11329
|
+
/** Sync variant used by auditHooks; same folding as observedHookEvents. */
|
|
11330
|
+
function observedHookEventsSync(captureDir, sinceMs) {
|
|
11331
|
+
const observed = /* @__PURE__ */ new Map();
|
|
11332
|
+
if (!existsSync(captureDir)) return observed;
|
|
11333
|
+
const monthDirs = readdirSync(captureDir, { withFileTypes: true }).filter((d) => d.isDirectory() && /^\d{4}-\d{2}$/.test(d.name)).map((d) => join(captureDir, d.name));
|
|
11334
|
+
for (const monthDir of monthDirs) for (const name of readdirSync(monthDir)) {
|
|
11335
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
11336
|
+
const file = join(monthDir, name);
|
|
11337
|
+
let text;
|
|
11338
|
+
try {
|
|
11339
|
+
if (statSync(file).mtimeMs < sinceMs) continue;
|
|
11340
|
+
text = readFileSync(file, "utf8");
|
|
11341
|
+
} catch {
|
|
11342
|
+
continue;
|
|
11343
|
+
}
|
|
11344
|
+
for (const raw of text.split("\n")) foldObservedLine(raw, observed);
|
|
11345
|
+
}
|
|
11346
|
+
return observed;
|
|
11347
|
+
}
|
|
11348
|
+
function foldObservedLine(raw, observed) {
|
|
11349
|
+
if (!raw.includes("hook_event_type")) return;
|
|
11350
|
+
let parsed;
|
|
11351
|
+
try {
|
|
11352
|
+
parsed = JSON.parse(raw);
|
|
11353
|
+
} catch {
|
|
11354
|
+
return;
|
|
11355
|
+
}
|
|
11356
|
+
const event = parsed.hook_event_type;
|
|
11357
|
+
if (!event) return;
|
|
11358
|
+
const ts = typeof parsed.timestamp === "number" ? parsed.timestamp : Date.now();
|
|
11359
|
+
const last = new Date(ts).toISOString();
|
|
11360
|
+
const entry = observed.get(event);
|
|
11361
|
+
if (entry) {
|
|
11362
|
+
entry.count += 1;
|
|
11363
|
+
if (last > entry.last) entry.last = last;
|
|
11364
|
+
} else observed.set(event, {
|
|
11365
|
+
count: 1,
|
|
11366
|
+
last
|
|
11367
|
+
});
|
|
11368
|
+
}
|
|
11369
|
+
|
|
11140
11370
|
//#endregion
|
|
11141
11371
|
//#region src/builds/doctor.ts
|
|
11142
11372
|
/**
|
|
@@ -11173,6 +11403,33 @@ function doctor(cwd) {
|
|
|
11173
11403
|
findings
|
|
11174
11404
|
};
|
|
11175
11405
|
}
|
|
11406
|
+
/**
|
|
11407
|
+
* One line per hook event: whether it is configured in settings.json and
|
|
11408
|
+
* whether a capture proves it actually ran in the last `sinceHours`. A hook
|
|
11409
|
+
* that is configured but never observed is the classic silent-failure case.
|
|
11410
|
+
*/
|
|
11411
|
+
async function hookEventFindings(cwd, opts = {}) {
|
|
11412
|
+
const sinceHours = opts.sinceHours ?? DEFAULT_SINCE_HOURS;
|
|
11413
|
+
const now = opts.now ?? Date.now();
|
|
11414
|
+
const report = auditHooks(cwd, {
|
|
11415
|
+
sinceHours,
|
|
11416
|
+
captureDir: opts.captureDir,
|
|
11417
|
+
now
|
|
11418
|
+
});
|
|
11419
|
+
const observed = await observedHookEvents(opts.captureDir ?? defaultCaptureDir(), now - sinceHours * 36e5);
|
|
11420
|
+
const configuredEvents = new Set(report.readings.map((r) => r.event));
|
|
11421
|
+
const findings = [];
|
|
11422
|
+
for (const event of OBSERVABLE_EVENTS) {
|
|
11423
|
+
const seen = observed.get(event);
|
|
11424
|
+
findings.push({
|
|
11425
|
+
event,
|
|
11426
|
+
configured: configuredEvents.has(event),
|
|
11427
|
+
observed: !!seen,
|
|
11428
|
+
lastSeen: seen?.last
|
|
11429
|
+
});
|
|
11430
|
+
}
|
|
11431
|
+
return findings;
|
|
11432
|
+
}
|
|
11176
11433
|
|
|
11177
11434
|
//#endregion
|
|
11178
11435
|
//#region src/cli/commands/build.ts
|
|
@@ -11313,14 +11570,19 @@ const nextCmd = {
|
|
|
11313
11570
|
};
|
|
11314
11571
|
const doctorCmd = {
|
|
11315
11572
|
name: "doctor",
|
|
11316
|
-
summary: "Check the project's registered build doors",
|
|
11317
|
-
description: "Reads .pai/builds.yaml above the current directory and flags every door that is missing or never calls `pai build lock`, which would bypass the queue.",
|
|
11573
|
+
summary: "Check the project's registered build doors and hook events",
|
|
11574
|
+
description: "Reads .pai/builds.yaml above the current directory and flags every door that is missing or never calls `pai build lock`, which would bypass the queue. Also prints one line per hook event: observed (a capture proves it ran) or configured-but-never-observed (warning — the hook may silently not be running).",
|
|
11318
11575
|
examples: ["pai build doctor"],
|
|
11319
|
-
run: (ctx) => {
|
|
11576
|
+
run: async (ctx) => {
|
|
11320
11577
|
const r = doctor(process.cwd());
|
|
11321
|
-
if (!r.root)
|
|
11322
|
-
|
|
11323
|
-
|
|
11578
|
+
if (!r.root) out$2("no .pai/builds.yaml here: no doors registered");
|
|
11579
|
+
else {
|
|
11580
|
+
for (const f of r.findings) out$2(`${f.ok ? "ok " : "FAIL"} ${f.door}${f.problem ? ` — ${f.problem}` : ""}`);
|
|
11581
|
+
if (!r.findings.length) out$2("no doors registered");
|
|
11582
|
+
}
|
|
11583
|
+
const sinceHours = 24;
|
|
11584
|
+
for (const h of await hookEventFindings(process.cwd(), { sinceHours })) if (h.observed) out$2(`ok ${h.event}: observed ${h.lastSeen ?? ""}`.trimEnd());
|
|
11585
|
+
else if (h.configured) out$2(`WARN ${h.event}: configured, never observed in ${sinceHours}h`);
|
|
11324
11586
|
return r.findings.every((f) => f.ok) ? 0 : 1;
|
|
11325
11587
|
}
|
|
11326
11588
|
};
|
|
@@ -11997,8 +12259,8 @@ async function escalate(task, headline, detail, state, now, opts) {
|
|
|
11997
12259
|
state.alarmedAt[task.id] = now;
|
|
11998
12260
|
let delivered = false;
|
|
11999
12261
|
try {
|
|
12000
|
-
const { routeNotification } = await import("../router-
|
|
12001
|
-
const { loadConfig } = await import("../config-
|
|
12262
|
+
const { routeNotification } = await import("../router-ghujlEFb.mjs");
|
|
12263
|
+
const { loadConfig } = await import("../config-DHgPx3ER.mjs");
|
|
12002
12264
|
delivered = (await routeNotification({
|
|
12003
12265
|
event: "error",
|
|
12004
12266
|
title: `PAI: ${headline}`,
|
|
@@ -14940,7 +15202,7 @@ async function migrateToPostgres(opts = {}) {
|
|
|
14940
15202
|
const dump = pgDump(container, pgConfig, dumpPath);
|
|
14941
15203
|
if (!dump.ok) return refused(dump.reason, dryRun);
|
|
14942
15204
|
}
|
|
14943
|
-
const { PostgresBackend } = await import("../postgres-
|
|
15205
|
+
const { PostgresBackend } = await import("../postgres-B1B80bcj.mjs");
|
|
14944
15206
|
await PostgresBackend.ensureDatabase(pgConfig);
|
|
14945
15207
|
const sqlite = new BetterSqlite3(sqliteFederationPath, { readonly: true });
|
|
14946
15208
|
const registrySqlite = sqliteRegistryPath === sqliteFederationPath ? sqlite : new BetterSqlite3(sqliteRegistryPath, { readonly: true });
|
|
@@ -16327,7 +16589,7 @@ const out = (s) => process.stdout.write(s + "\n");
|
|
|
16327
16589
|
async function repoOf(project) {
|
|
16328
16590
|
if (typeof project === "string" && project) {
|
|
16329
16591
|
if (existsSync(project)) return resolve(project);
|
|
16330
|
-
const { getProject } = await import("../helpers-
|
|
16592
|
+
const { getProject } = await import("../helpers-DyHKAwrW.mjs");
|
|
16331
16593
|
const row = await getProject(project);
|
|
16332
16594
|
if (!row) throw new CliError(`no project ${project}`, "pai projects list");
|
|
16333
16595
|
return row.root_path;
|
|
@@ -16556,7 +16818,7 @@ const reviewCmd = {
|
|
|
16556
16818
|
return 0;
|
|
16557
16819
|
}
|
|
16558
16820
|
};
|
|
16559
|
-
const listCmd = {
|
|
16821
|
+
const listCmd$2 = {
|
|
16560
16822
|
name: "list",
|
|
16561
16823
|
summary: "Every item of the project with its state",
|
|
16562
16824
|
description: "States: pending, running, ready, too-big, needs-decision, cloud, accepted, rejected.",
|
|
@@ -16650,7 +16912,7 @@ const queueCommand = {
|
|
|
16650
16912
|
addCmd,
|
|
16651
16913
|
runCmd,
|
|
16652
16914
|
reviewCmd,
|
|
16653
|
-
listCmd,
|
|
16915
|
+
listCmd$2,
|
|
16654
16916
|
acceptCmd,
|
|
16655
16917
|
rejectCmd,
|
|
16656
16918
|
againCmd
|
|
@@ -17150,7 +17412,7 @@ async function cmdPromote(opts) {
|
|
|
17150
17412
|
console.error(dim$1(` Pick a durable location for the new project.`));
|
|
17151
17413
|
process.exit(1);
|
|
17152
17414
|
}
|
|
17153
|
-
const { getRegistryBackend } = await import("../factory-
|
|
17415
|
+
const { getRegistryBackend } = await import("../factory-DFwez99M.mjs");
|
|
17154
17416
|
const registryBackend = await getRegistryBackend();
|
|
17155
17417
|
const encodedDir = encodeDir(targetPath);
|
|
17156
17418
|
if (await registryBackend.getProjectBySlug(slug) ?? await registryBackend.getProjectByRootPath(targetPath) ?? await registryBackend.getProjectByEncodedDir(encodedDir)) {
|
|
@@ -18394,7 +18656,7 @@ async function cmdUnregister(slug, opts = {}) {
|
|
|
18394
18656
|
* deprecated alias that forwards here with a stderr notice.
|
|
18395
18657
|
*/
|
|
18396
18658
|
async function projectListCmd(opts) {
|
|
18397
|
-
const { cmdMain } = await import("../main-resolver-
|
|
18659
|
+
const { cmdMain } = await import("../main-resolver-BKwI0I6H.mjs");
|
|
18398
18660
|
await cmdMain(void 0, void 0, opts);
|
|
18399
18661
|
return 0;
|
|
18400
18662
|
}
|
|
@@ -19817,7 +20079,7 @@ async function registryScanCmd(opts) {
|
|
|
19817
20079
|
return 0;
|
|
19818
20080
|
}
|
|
19819
20081
|
if (opts.filterReport) {
|
|
19820
|
-
const { reportUnregistrable } = await import("../registrable-
|
|
20082
|
+
const { reportUnregistrable } = await import("../registrable-BwFiqazy.mjs");
|
|
19821
20083
|
const r = await reportUnregistrable(await getRegistryBackend());
|
|
19822
20084
|
console.log(`Registry rows: ${r.total}`);
|
|
19823
20085
|
console.log(` matching the registration filter: ${r.matching} (${r.matchingMissing} with a missing path)`);
|
|
@@ -21660,7 +21922,7 @@ async function runEmbed(backend, projectId, projectSlug, batchSize = 50, pageSiz
|
|
|
21660
21922
|
const label = projectSlug ? `project ${projectSlug}` : "all projects";
|
|
21661
21923
|
console.log(dim$1(`Generating embeddings for ${label} (this may take a while on first run)...`));
|
|
21662
21924
|
const { serializeEmbedding } = await import("../embeddings-CIkJBKd-.mjs");
|
|
21663
|
-
const { gateBackend } = await import("../embedding-gate-
|
|
21925
|
+
const { gateBackend } = await import("../embedding-gate-BVQvUmox.mjs");
|
|
21664
21926
|
const gate = await gateBackend(backend, { record: true });
|
|
21665
21927
|
if (!gate.ok) {
|
|
21666
21928
|
console.error(err(gate.reason));
|
|
@@ -21989,7 +22251,7 @@ async function memorySearchCmd(query, opts) {
|
|
|
21989
22251
|
let results;
|
|
21990
22252
|
if (mode === "keyword") results = await backend.searchKeyword(query, searchOpts);
|
|
21991
22253
|
else {
|
|
21992
|
-
const { embedQuery } = await import("../embedding-gate-
|
|
22254
|
+
const { embedQuery } = await import("../embedding-gate-BVQvUmox.mjs");
|
|
21993
22255
|
console.log(dim$1("Generating query embedding..."));
|
|
21994
22256
|
const q = await embedQuery(backend, query);
|
|
21995
22257
|
const queryEmbedding = q.vec;
|
|
@@ -22069,8 +22331,8 @@ async function memorySearchCmd(query, opts) {
|
|
|
22069
22331
|
//#region src/cli/commands/memory/sources-cmd.ts
|
|
22070
22332
|
/** `pai memory sources` — what the indexer has taken in. */
|
|
22071
22333
|
async function memorySourcesCmd(opts) {
|
|
22072
|
-
const { createStorageBackend } = await import("../factory-
|
|
22073
|
-
const { loadConfig } = await import("../config-
|
|
22334
|
+
const { createStorageBackend } = await import("../factory-DFwez99M.mjs");
|
|
22335
|
+
const { loadConfig } = await import("../config-DHgPx3ER.mjs");
|
|
22074
22336
|
const { cmdMemorySources } = await import("../sources-CiE3rZek.mjs");
|
|
22075
22337
|
let backend;
|
|
22076
22338
|
try {
|
|
@@ -22830,7 +23092,7 @@ async function zettelExploreCmd(note, opts) {
|
|
|
22830
23092
|
const depth = opts.depth ?? 3;
|
|
22831
23093
|
const direction = opts.direction ?? "both";
|
|
22832
23094
|
const mode = opts.mode ?? "all";
|
|
22833
|
-
const { zettelExplore } = await import("../zettelkasten-
|
|
23095
|
+
const { zettelExplore } = await import("../zettelkasten-BH8AdNh8.mjs");
|
|
22834
23096
|
const result = await zettelExplore(await getFedBackend(), {
|
|
22835
23097
|
startNote: note,
|
|
22836
23098
|
depth,
|
|
@@ -22886,7 +23148,7 @@ async function zettelHealthCmd(opts) {
|
|
|
22886
23148
|
const projectPath = opts.project;
|
|
22887
23149
|
const recentDays = opts.days ?? 30;
|
|
22888
23150
|
const includeTypes = opts.include ? opts.include.split(",").map((s) => s.trim()) : void 0;
|
|
22889
|
-
const { zettelHealth } = await import("../zettelkasten-
|
|
23151
|
+
const { zettelHealth } = await import("../zettelkasten-BH8AdNh8.mjs");
|
|
22890
23152
|
const result = await zettelHealth(await getFedBackend(), {
|
|
22891
23153
|
scope,
|
|
22892
23154
|
projectPath,
|
|
@@ -22944,7 +23206,7 @@ async function zettelSurpriseCmd(note, opts) {
|
|
|
22944
23206
|
const limit = opts.limit ?? 10;
|
|
22945
23207
|
const minSimilarity = opts.minSimilarity ?? .3;
|
|
22946
23208
|
const minGraphDistance = opts.minDistance ?? 3;
|
|
22947
|
-
const { zettelSurprise } = await import("../zettelkasten-
|
|
23209
|
+
const { zettelSurprise } = await import("../zettelkasten-BH8AdNh8.mjs");
|
|
22948
23210
|
const backend = await getFedBackend();
|
|
22949
23211
|
console.log();
|
|
22950
23212
|
console.log(header(" PAI Zettel Surprise"));
|
|
@@ -22986,7 +23248,7 @@ async function zettelConverseCmd(question, opts) {
|
|
|
22986
23248
|
const vaultProjectId = Number(opts.vaultProjectId);
|
|
22987
23249
|
const depth = opts.depth ?? 2;
|
|
22988
23250
|
const limit = opts.limit ?? 15;
|
|
22989
|
-
const { zettelConverse } = await import("../zettelkasten-
|
|
23251
|
+
const { zettelConverse } = await import("../zettelkasten-BH8AdNh8.mjs");
|
|
22990
23252
|
const backend = await getFedBackend();
|
|
22991
23253
|
console.log();
|
|
22992
23254
|
console.log(header(" PAI Zettel Converse"));
|
|
@@ -23036,7 +23298,7 @@ async function zettelThemesCmd(opts) {
|
|
|
23036
23298
|
const minClusterSize = opts.minSize ?? 3;
|
|
23037
23299
|
const maxThemes = opts.maxThemes ?? 10;
|
|
23038
23300
|
const similarityThreshold = opts.threshold ?? .65;
|
|
23039
|
-
const { zettelThemes } = await import("../zettelkasten-
|
|
23301
|
+
const { zettelThemes } = await import("../zettelkasten-BH8AdNh8.mjs");
|
|
23040
23302
|
const backend = await getFedBackend();
|
|
23041
23303
|
console.log();
|
|
23042
23304
|
console.log(header(" PAI Zettel Themes"));
|
|
@@ -23084,7 +23346,7 @@ async function zettelSuggestCmd(note, opts) {
|
|
|
23084
23346
|
const vaultProjectId = Number(opts.vaultProjectId);
|
|
23085
23347
|
const limit = opts.limit ?? 5;
|
|
23086
23348
|
const excludeLinked = opts.excludeLinked !== true;
|
|
23087
|
-
const { zettelSuggest } = await import("../zettelkasten-
|
|
23349
|
+
const { zettelSuggest } = await import("../zettelkasten-BH8AdNh8.mjs");
|
|
23088
23350
|
const backend = await getFedBackend();
|
|
23089
23351
|
console.log();
|
|
23090
23352
|
console.log(header(" PAI Zettel Suggest"));
|
|
@@ -24083,28 +24345,6 @@ const reloadCommand = {
|
|
|
24083
24345
|
}
|
|
24084
24346
|
};
|
|
24085
24347
|
|
|
24086
|
-
//#endregion
|
|
24087
|
-
//#region src/audit/tokens.ts
|
|
24088
|
-
/**
|
|
24089
|
-
* tokens.ts — the one tokenizer every audit section counts through.
|
|
24090
|
-
*
|
|
24091
|
-
* cl100k_base via js-tiktoken (pure JS, no native build). "all" + [] for the
|
|
24092
|
-
* allowed/disallowed-special-token arguments matches Python tiktoken's
|
|
24093
|
-
* `disallowed_special=()`: audited text (hook stdout, session transcripts)
|
|
24094
|
-
* routinely contains literal "<|...|>"-shaped substrings that are not actual
|
|
24095
|
-
* special tokens, and the strict default throws on sight of them.
|
|
24096
|
-
*/
|
|
24097
|
-
const TOKEN_ENCODING = "cl100k_base";
|
|
24098
|
-
let cached = null;
|
|
24099
|
-
function encoder() {
|
|
24100
|
-
if (!cached) cached = getEncoding(TOKEN_ENCODING);
|
|
24101
|
-
return cached;
|
|
24102
|
-
}
|
|
24103
|
-
function countTokens(text) {
|
|
24104
|
-
if (!text) return 0;
|
|
24105
|
-
return encoder().encode(text, "all", []).length;
|
|
24106
|
-
}
|
|
24107
|
-
|
|
24108
24348
|
//#endregion
|
|
24109
24349
|
//#region src/audit/files.ts
|
|
24110
24350
|
/**
|
|
@@ -24198,149 +24438,41 @@ function auditFiles(paths) {
|
|
|
24198
24438
|
}
|
|
24199
24439
|
|
|
24200
24440
|
//#endregion
|
|
24201
|
-
//#region src/audit/
|
|
24441
|
+
//#region src/audit/first-turn.ts
|
|
24202
24442
|
/**
|
|
24203
|
-
*
|
|
24204
|
-
*
|
|
24205
|
-
*
|
|
24206
|
-
*
|
|
24443
|
+
* first-turn.ts — where a turn's context actually comes from, attributed
|
|
24444
|
+
* line by line from the transcript.
|
|
24445
|
+
*
|
|
24446
|
+
* The API reports one number per assistant turn (cache_read + cache_creation
|
|
24447
|
+
* + input). The transcript holds the pieces Claude Code injected since the
|
|
24448
|
+
* previous turn: SessionStart / UserPromptSubmit hook output, the skill
|
|
24449
|
+
* listing, the deferred-tool name list, MCP server instructions, the human
|
|
24450
|
+
* prompt, any isMeta expansion (a skill body, a pasted slash-command result),
|
|
24451
|
+
* tool results, and the previous turn's own visible output (re-sent as
|
|
24452
|
+
* context on every later turn). Each of those is counted in cl100k here;
|
|
24453
|
+
* what is left over is the system prompt, the built-in tool schemas, agents
|
|
24454
|
+
* and memory files, which never appear in the transcript.
|
|
24455
|
+
*
|
|
24456
|
+
* Only the live lineage counts. Lines chain by parentUuid, and a prompt the
|
|
24457
|
+
* user abandoned before a reply (Escape, re-submit) leaves a dead branch
|
|
24458
|
+
* whose lines were never sent — run 9 measured a session whose first
|
|
24459
|
+
* /Name was abandoned and read 24k where the true baseline is 30k, because
|
|
24460
|
+
* the 3.5k-token skill listing sat on the dead branch.
|
|
24461
|
+
*
|
|
24462
|
+
* `type:"system"` lines with subtype local_command hold a slash command's
|
|
24463
|
+
* raw stdout, and the model receives it verbatim in addition to the isMeta
|
|
24464
|
+
* markdown copy — run 11 measured both present on the same turn, so both are
|
|
24465
|
+
* counted.
|
|
24207
24466
|
*/
|
|
24208
|
-
|
|
24209
|
-
|
|
24210
|
-
|
|
24211
|
-
|
|
24212
|
-
|
|
24213
|
-
|
|
24214
|
-
|
|
24215
|
-
} catch {
|
|
24216
|
-
return {};
|
|
24467
|
+
function parseLines(path) {
|
|
24468
|
+
const lines = [];
|
|
24469
|
+
for (const raw of readFileSync(path, "utf8").split("\n")) {
|
|
24470
|
+
if (!raw.trim()) continue;
|
|
24471
|
+
try {
|
|
24472
|
+
lines.push(JSON.parse(raw));
|
|
24473
|
+
} catch {}
|
|
24217
24474
|
}
|
|
24218
|
-
|
|
24219
|
-
/** Expand ${PAI_DIR} and $HOME/${HOME} the way settings.json's own hooks do. */
|
|
24220
|
-
function expandCommand(command, env) {
|
|
24221
|
-
const paiDir = env.PAI_DIR ?? process.env.PAI_DIR ?? claudeHomeDir();
|
|
24222
|
-
return command.replace(/\$\{PAI_DIR\}/g, paiDir).replace(/\$\{HOME\}/g, homedir()).replace(/\$HOME\b/g, homedir());
|
|
24223
|
-
}
|
|
24224
|
-
function syntheticStdin(cwd, eventName) {
|
|
24225
|
-
const sessionId = randomUUID();
|
|
24226
|
-
return JSON.stringify({
|
|
24227
|
-
session_id: sessionId,
|
|
24228
|
-
cwd,
|
|
24229
|
-
hook_event_name: eventName,
|
|
24230
|
-
prompt: "audit",
|
|
24231
|
-
transcript_path: join(claudeProjectsDir(), "audit", `${sessionId}.jsonl`)
|
|
24232
|
-
});
|
|
24233
|
-
}
|
|
24234
|
-
function runHook(command, cwd, eventName) {
|
|
24235
|
-
const result = spawnSync(command, {
|
|
24236
|
-
shell: true,
|
|
24237
|
-
cwd,
|
|
24238
|
-
input: syntheticStdin(cwd, eventName),
|
|
24239
|
-
encoding: "utf8",
|
|
24240
|
-
timeout: HOOK_TIMEOUT_MS
|
|
24241
|
-
});
|
|
24242
|
-
if (result.error) return {
|
|
24243
|
-
stdout: "",
|
|
24244
|
-
error: result.error.message
|
|
24245
|
-
};
|
|
24246
|
-
if (result.signal) return {
|
|
24247
|
-
stdout: result.stdout ?? "",
|
|
24248
|
-
error: `killed by ${result.signal} (timeout?)`
|
|
24249
|
-
};
|
|
24250
|
-
return { stdout: result.stdout ?? "" };
|
|
24251
|
-
}
|
|
24252
|
-
const NOT_RUN = "not run (pass --run to execute hooks)";
|
|
24253
|
-
const AUDITED_EVENTS = ["SessionStart", "UserPromptSubmit"];
|
|
24254
|
-
/**
|
|
24255
|
-
* Lists every SessionStart / UserPromptSubmit hook. Without `run` the hooks are only
|
|
24256
|
-
* listed (tokens 0, marked not run): a project's settings.json is untrusted input and
|
|
24257
|
-
* its hooks execute with shell:true. Pass `runHook` (CLI `--run`) to measure them.
|
|
24258
|
-
*/
|
|
24259
|
-
function auditHooks(cwd, run) {
|
|
24260
|
-
const settingsFiles = settingsPaths(cwd);
|
|
24261
|
-
const readings = [];
|
|
24262
|
-
const totalsByEvent = {};
|
|
24263
|
-
const preToolUseHooks = [];
|
|
24264
|
-
let preToolUseRewritesBash = false;
|
|
24265
|
-
for (const settingsPath of settingsFiles) {
|
|
24266
|
-
const settings = readSettings(settingsPath);
|
|
24267
|
-
const env = settings.env ?? {};
|
|
24268
|
-
for (const eventName of AUDITED_EVENTS) {
|
|
24269
|
-
const groups = settings.hooks?.[eventName] ?? [];
|
|
24270
|
-
totalsByEvent[eventName] = totalsByEvent[eventName] ?? 0;
|
|
24271
|
-
for (const group of groups) for (const entry of group.hooks ?? []) {
|
|
24272
|
-
if (!entry.command) continue;
|
|
24273
|
-
const command = expandCommand(entry.command, env);
|
|
24274
|
-
const { stdout, error } = run ? run(command, cwd, eventName) : {
|
|
24275
|
-
stdout: "",
|
|
24276
|
-
error: NOT_RUN
|
|
24277
|
-
};
|
|
24278
|
-
const tokens = countTokens(stdout);
|
|
24279
|
-
totalsByEvent[eventName] += tokens;
|
|
24280
|
-
readings.push({
|
|
24281
|
-
event: eventName,
|
|
24282
|
-
matcher: group.matcher,
|
|
24283
|
-
command,
|
|
24284
|
-
tokens,
|
|
24285
|
-
error
|
|
24286
|
-
});
|
|
24287
|
-
}
|
|
24288
|
-
}
|
|
24289
|
-
for (const group of settings.hooks?.PreToolUse ?? []) for (const entry of group.hooks ?? []) {
|
|
24290
|
-
if (!entry.command) continue;
|
|
24291
|
-
preToolUseHooks.push({
|
|
24292
|
-
matcher: group.matcher,
|
|
24293
|
-
command: expandCommand(entry.command, env)
|
|
24294
|
-
});
|
|
24295
|
-
if (group.matcher === "Bash") preToolUseRewritesBash = true;
|
|
24296
|
-
}
|
|
24297
|
-
}
|
|
24298
|
-
return {
|
|
24299
|
-
encoding: TOKEN_ENCODING,
|
|
24300
|
-
settingsFiles,
|
|
24301
|
-
readings,
|
|
24302
|
-
totalsByEvent,
|
|
24303
|
-
preToolUseHooks,
|
|
24304
|
-
preToolUseRewritesBash
|
|
24305
|
-
};
|
|
24306
|
-
}
|
|
24307
|
-
|
|
24308
|
-
//#endregion
|
|
24309
|
-
//#region src/audit/first-turn.ts
|
|
24310
|
-
/**
|
|
24311
|
-
* first-turn.ts — where a turn's context actually comes from, attributed
|
|
24312
|
-
* line by line from the transcript.
|
|
24313
|
-
*
|
|
24314
|
-
* The API reports one number per assistant turn (cache_read + cache_creation
|
|
24315
|
-
* + input). The transcript holds the pieces Claude Code injected since the
|
|
24316
|
-
* previous turn: SessionStart / UserPromptSubmit hook output, the skill
|
|
24317
|
-
* listing, the deferred-tool name list, MCP server instructions, the human
|
|
24318
|
-
* prompt, any isMeta expansion (a skill body, a pasted slash-command result),
|
|
24319
|
-
* tool results, and the previous turn's own visible output (re-sent as
|
|
24320
|
-
* context on every later turn). Each of those is counted in cl100k here;
|
|
24321
|
-
* what is left over is the system prompt, the built-in tool schemas, agents
|
|
24322
|
-
* and memory files, which never appear in the transcript.
|
|
24323
|
-
*
|
|
24324
|
-
* Only the live lineage counts. Lines chain by parentUuid, and a prompt the
|
|
24325
|
-
* user abandoned before a reply (Escape, re-submit) leaves a dead branch
|
|
24326
|
-
* whose lines were never sent — run 9 measured a session whose first
|
|
24327
|
-
* /Name was abandoned and read 24k where the true baseline is 30k, because
|
|
24328
|
-
* the 3.5k-token skill listing sat on the dead branch.
|
|
24329
|
-
*
|
|
24330
|
-
* `type:"system"` lines with subtype local_command hold a slash command's
|
|
24331
|
-
* raw stdout, and the model receives it verbatim in addition to the isMeta
|
|
24332
|
-
* markdown copy — run 11 measured both present on the same turn, so both are
|
|
24333
|
-
* counted.
|
|
24334
|
-
*/
|
|
24335
|
-
function parseLines(path) {
|
|
24336
|
-
const lines = [];
|
|
24337
|
-
for (const raw of readFileSync(path, "utf8").split("\n")) {
|
|
24338
|
-
if (!raw.trim()) continue;
|
|
24339
|
-
try {
|
|
24340
|
-
lines.push(JSON.parse(raw));
|
|
24341
|
-
} catch {}
|
|
24342
|
-
}
|
|
24343
|
-
return lines;
|
|
24475
|
+
return lines;
|
|
24344
24476
|
}
|
|
24345
24477
|
function classify(line) {
|
|
24346
24478
|
if (line.type === "attachment") {
|
|
@@ -24759,8 +24891,8 @@ function deriveSessionTrigger(transcriptPath, env = process.env, now = Date.now(
|
|
|
24759
24891
|
* and the cache-TTL split (ephemeral 5m vs 1h) that says which cache the
|
|
24760
24892
|
* session is riding.
|
|
24761
24893
|
*/
|
|
24762
|
-
/** Newest *.jsonl under ~/.claude/projects, excluding subagents/. */
|
|
24763
|
-
function newestSessionLog() {
|
|
24894
|
+
/** Newest *.jsonl under ~/.claude/projects, excluding subagents/. With a session id, that session's file instead. */
|
|
24895
|
+
function newestSessionLog(sessionId) {
|
|
24764
24896
|
const projectsDir = claudeProjectsDir();
|
|
24765
24897
|
let best = null;
|
|
24766
24898
|
let projectDirs;
|
|
@@ -24779,6 +24911,7 @@ function newestSessionLog() {
|
|
|
24779
24911
|
}
|
|
24780
24912
|
for (const entry of entries) {
|
|
24781
24913
|
if (!entry.endsWith(".jsonl")) continue;
|
|
24914
|
+
if (sessionId && entry !== `${sessionId}.jsonl`) continue;
|
|
24782
24915
|
const filePath = join(dirPath, entry);
|
|
24783
24916
|
let mtime;
|
|
24784
24917
|
try {
|
|
@@ -26360,12 +26493,14 @@ function hookRunner(run) {
|
|
|
26360
26493
|
console.error(warn$1("--run executes every SessionStart/UserPromptSubmit hook in ~/.claude/settings.json and ./.claude/settings.json with a shell. Do not use it on a repository you do not trust."));
|
|
26361
26494
|
return runHook;
|
|
26362
26495
|
}
|
|
26363
|
-
function printHooks(report) {
|
|
26496
|
+
function printHooks(report, sinceHours) {
|
|
26364
26497
|
console.log(header(`Hook output (encoding: ${report.encoding})`));
|
|
26365
26498
|
console.log(dim$1(`settings.json: ${report.settingsFiles.join(", ") || "none found"}`));
|
|
26366
26499
|
const rows = report.readings.map((r) => [
|
|
26367
26500
|
r.event,
|
|
26368
26501
|
r.matcher ?? "",
|
|
26502
|
+
r.state,
|
|
26503
|
+
r.lastSeen ?? "",
|
|
26369
26504
|
String(r.tokens),
|
|
26370
26505
|
r.error ? err(r.error) : "",
|
|
26371
26506
|
r.command
|
|
@@ -26373,10 +26508,14 @@ function printHooks(report) {
|
|
|
26373
26508
|
console.log(renderTable$1([
|
|
26374
26509
|
"event",
|
|
26375
26510
|
"matcher",
|
|
26511
|
+
"state",
|
|
26512
|
+
"last seen",
|
|
26376
26513
|
"tokens",
|
|
26377
26514
|
"error",
|
|
26378
26515
|
"command"
|
|
26379
26516
|
], rows));
|
|
26517
|
+
for (const r of report.readings) if (r.state === "missing-script") console.log(err(` RED: ${r.event} hook script does not exist: ${r.command}`));
|
|
26518
|
+
else if (r.state === "configured") console.log(warn$1(` AMBER: ${r.event} hook configured, never observed in ${sinceHours}h — it may not be running`));
|
|
26380
26519
|
for (const [event, total] of Object.entries(report.totalsByEvent)) console.log(bold$1(`${event} total: ${total} tokens`));
|
|
26381
26520
|
console.log(dim$1(`PreToolUse hooks: ${report.preToolUseHooks.length}` + (report.preToolUseRewritesBash ? ", including a Bash-matcher rewrite hook" : "")));
|
|
26382
26521
|
}
|
|
@@ -26702,7 +26841,7 @@ const DEFAULT_CTX_THRESHOLD = 2e5;
|
|
|
26702
26841
|
async function gatherCombined() {
|
|
26703
26842
|
const cwd = process.cwd();
|
|
26704
26843
|
const files = auditFiles(defaultFileSet(cwd));
|
|
26705
|
-
const hooks = auditHooks(cwd);
|
|
26844
|
+
const hooks = auditHooks(cwd, { sinceHours: DEFAULT_SINCE_HOURS });
|
|
26706
26845
|
const session = await auditSession(newestSessionLog() ?? void 0).catch(() => null);
|
|
26707
26846
|
const daemon = auditDaemon();
|
|
26708
26847
|
const env = auditEnv();
|
|
@@ -26761,7 +26900,7 @@ function combinedSectionOutputs(data) {
|
|
|
26761
26900
|
});
|
|
26762
26901
|
sections.push({
|
|
26763
26902
|
name: "hooks",
|
|
26764
|
-
text: captureOutput(() => printHooks(data.hooks))
|
|
26903
|
+
text: captureOutput(() => printHooks(data.hooks, DEFAULT_SINCE_HOURS))
|
|
26765
26904
|
});
|
|
26766
26905
|
if (data.session) sections.push({
|
|
26767
26906
|
name: "session",
|
|
@@ -26844,6 +26983,71 @@ function recordCombinedReport(dir, data) {
|
|
|
26844
26983
|
for (const f of data.findings) counts[f.severity]++;
|
|
26845
26984
|
appendFileSync(runsPath, `- ${date} run${runNumber}: ${counts.RED} RED, ${counts.AMBER} AMBER, ${counts.GREEN} GREEN — ${baseName}.md\n`, "utf8");
|
|
26846
26985
|
}
|
|
26986
|
+
const SCAN_SEVERITIES = [
|
|
26987
|
+
"block",
|
|
26988
|
+
"confirm",
|
|
26989
|
+
"warn"
|
|
26990
|
+
];
|
|
26991
|
+
/**
|
|
26992
|
+
* `pai audit scan`: run the rule set over a transcript after the fact —
|
|
26993
|
+
* single-step rules per tool call, sequence rules correlated across the run.
|
|
26994
|
+
* Exit 0 when nothing hit, 1 when any block-severity finding did.
|
|
26995
|
+
*/
|
|
26996
|
+
async function auditScanCmd(opts) {
|
|
26997
|
+
let target = null;
|
|
26998
|
+
if (opts.worker) {
|
|
26999
|
+
const { workers } = readWorkersSection();
|
|
27000
|
+
const logDir = workersLogDir(workers);
|
|
27001
|
+
const st = loadStatus(logDir, opts.worker);
|
|
27002
|
+
if (!st) {
|
|
27003
|
+
console.error(err(` No worker named ${opts.worker}.`));
|
|
27004
|
+
return 1;
|
|
27005
|
+
}
|
|
27006
|
+
target = st.claudeSession ? claudeSessionTranscript(st.claudeSession, st.cwd) : eventsPath(logDir, opts.worker);
|
|
27007
|
+
if (target && !existsSync(target)) target = null;
|
|
27008
|
+
} else if (opts.session) target = newestSessionLog(opts.session);
|
|
27009
|
+
else if (opts.path) target = existsSync(opts.path) ? opts.path : null;
|
|
27010
|
+
else target = newestSessionLog();
|
|
27011
|
+
if (!target) {
|
|
27012
|
+
console.error(err(" No transcript found."));
|
|
27013
|
+
return 1;
|
|
27014
|
+
}
|
|
27015
|
+
const { rules, errors } = loadRules(userRuleDirs(process.cwd()));
|
|
27016
|
+
for (const e of errors) console.error(dim$1(`rule: ${e}`));
|
|
27017
|
+
const findings = await scanTranscript(target, rules);
|
|
27018
|
+
if (opts.json) {
|
|
27019
|
+
printJson({
|
|
27020
|
+
path: target,
|
|
27021
|
+
findings
|
|
27022
|
+
});
|
|
27023
|
+
return findings.some((f) => f.severity === "block") ? 1 : 0;
|
|
27024
|
+
}
|
|
27025
|
+
console.log(header(`Security scan: ${target}`));
|
|
27026
|
+
console.log(dim$1(`${rules.length} rules, ${findings.length} findings`));
|
|
27027
|
+
for (const sev of SCAN_SEVERITIES) {
|
|
27028
|
+
const hits = findings.filter((f) => f.severity === sev);
|
|
27029
|
+
if (!hits.length) continue;
|
|
27030
|
+
console.log(header(sev));
|
|
27031
|
+
const rows = hits.map((f) => [
|
|
27032
|
+
severityColor(sev === "block" ? "RED" : "AMBER", sev),
|
|
27033
|
+
f.ruleId,
|
|
27034
|
+
f.tool,
|
|
27035
|
+
String(f.line),
|
|
27036
|
+
f.ts ? f.ts.slice(11, 19) : "",
|
|
27037
|
+
f.evidence.replace(/\s+/g, " ").slice(0, 100)
|
|
27038
|
+
]);
|
|
27039
|
+
console.log(renderTable$1([
|
|
27040
|
+
"severity",
|
|
27041
|
+
"rule",
|
|
27042
|
+
"tool",
|
|
27043
|
+
"line",
|
|
27044
|
+
"time",
|
|
27045
|
+
"evidence"
|
|
27046
|
+
], rows));
|
|
27047
|
+
}
|
|
27048
|
+
if (!findings.length) console.log(ok$1("no findings"));
|
|
27049
|
+
return findings.some((f) => f.severity === "block") ? 1 : 0;
|
|
27050
|
+
}
|
|
26847
27051
|
/**
|
|
26848
27052
|
* `--json` is declared once, on `tokensCmd` itself, and read back from there
|
|
26849
27053
|
* in every subcommand action below — not redeclared per subcommand. The parser
|
|
@@ -26864,12 +27068,16 @@ async function auditFilesCmd(paths, opts) {
|
|
|
26864
27068
|
}
|
|
26865
27069
|
/** Exported function for audit tokens hooks command */
|
|
26866
27070
|
function auditHooksCmd(opts) {
|
|
26867
|
-
const
|
|
27071
|
+
const sinceHours = opts.since ? parseInt(opts.since, 10) : DEFAULT_SINCE_HOURS;
|
|
27072
|
+
const report = auditHooks(process.cwd(), {
|
|
27073
|
+
run: hookRunner(opts.run),
|
|
27074
|
+
sinceHours
|
|
27075
|
+
});
|
|
26868
27076
|
if (opts.json) {
|
|
26869
27077
|
printJson(report);
|
|
26870
27078
|
return 0;
|
|
26871
27079
|
}
|
|
26872
|
-
printHooks(report);
|
|
27080
|
+
printHooks(report, sinceHours);
|
|
26873
27081
|
return 0;
|
|
26874
27082
|
}
|
|
26875
27083
|
/** Exported function for audit tokens session command */
|
|
@@ -27057,7 +27265,8 @@ const t = (name, summary, description, extra = {}) => a({
|
|
|
27057
27265
|
if (name === "files") return auditFilesCmd(ctx.args.PATHS ?? [], { json: ctx.opts.json === true });
|
|
27058
27266
|
if (name === "hooks") return auditHooksCmd({
|
|
27059
27267
|
json: ctx.opts.json === true,
|
|
27060
|
-
run: ctx.opts.run === true
|
|
27268
|
+
run: ctx.opts.run === true,
|
|
27269
|
+
since: ctx.opts.since
|
|
27061
27270
|
});
|
|
27062
27271
|
if (name === "session") return auditSessionCmd(ctx.args.PATH, {
|
|
27063
27272
|
json: ctx.opts.json === true,
|
|
@@ -27130,13 +27339,22 @@ const auditCommand = {
|
|
|
27130
27339
|
}],
|
|
27131
27340
|
examples: ["pai audit tokens files", "pai audit tokens files ./CLAUDE.md --json"]
|
|
27132
27341
|
}),
|
|
27133
|
-
t("hooks", "Token cost of every start-up and prompt hook", "Lists each SessionStart and UserPromptSubmit hook. With --run it executes them and measures what each adds to the context; without it nothing is executed.", {
|
|
27342
|
+
t("hooks", "Token cost and observed state of every start-up and prompt hook", "Lists each SessionStart and UserPromptSubmit hook with its state: observed (a capture for that event exists in the window), configured (never observed — the hook may silently not be running), or missing-script. With --run it executes them and measures what each adds to the context; without it nothing is executed.", {
|
|
27134
27343
|
options: [{
|
|
27135
27344
|
name: "run",
|
|
27136
27345
|
type: "boolean",
|
|
27137
27346
|
summary: "Execute the hooks to measure their output (runs the repository's own hook commands; trusted repositories only)"
|
|
27347
|
+
}, {
|
|
27348
|
+
name: "since",
|
|
27349
|
+
type: "number",
|
|
27350
|
+
value: "HOURS",
|
|
27351
|
+
summary: "Observation window for the state column (default 24h)"
|
|
27138
27352
|
}],
|
|
27139
|
-
examples: [
|
|
27353
|
+
examples: [
|
|
27354
|
+
"pai audit tokens hooks",
|
|
27355
|
+
"pai audit tokens hooks --run",
|
|
27356
|
+
"pai audit tokens hooks --since 48"
|
|
27357
|
+
]
|
|
27140
27358
|
}),
|
|
27141
27359
|
t("session", "Token split for one session transcript", "Shows cache, input and output tokens of a session (default: the newest).", {
|
|
27142
27360
|
args: [{
|
|
@@ -27243,6 +27461,1606 @@ const auditCommand = {
|
|
|
27243
27461
|
examples: ["pai audit tokens ladder", "pai audit tokens ladder --live --model haiku"]
|
|
27244
27462
|
})
|
|
27245
27463
|
]
|
|
27464
|
+
}, a({
|
|
27465
|
+
name: "scan",
|
|
27466
|
+
summary: "After-the-fact security scan of a session or worker transcript",
|
|
27467
|
+
description: "Runs the rule set (the same rules the PreToolUse hook enforces, plus two-step sequence rules that only make sense after the fact) over a transcript: single-step rules against every tool call, sequence rules correlated across the run — credential read followed by an outbound call, download followed by execute, injected text followed by a shell call. Findings are grouped by severity; evidence is masked. Exit 1 when a block-severity finding hit.",
|
|
27468
|
+
args: [{
|
|
27469
|
+
name: "PATH",
|
|
27470
|
+
summary: "Transcript .jsonl (default: newest session under ~/.claude/projects)"
|
|
27471
|
+
}],
|
|
27472
|
+
options: [{
|
|
27473
|
+
name: "session",
|
|
27474
|
+
type: "string",
|
|
27475
|
+
value: "ID",
|
|
27476
|
+
summary: "Scan this Claude Code session's transcript"
|
|
27477
|
+
}, {
|
|
27478
|
+
name: "worker",
|
|
27479
|
+
type: "string",
|
|
27480
|
+
value: "ID",
|
|
27481
|
+
summary: "Scan a worker's transcript (its Claude session, else its event log)"
|
|
27482
|
+
}],
|
|
27483
|
+
examples: [
|
|
27484
|
+
"pai audit scan",
|
|
27485
|
+
"pai audit scan --worker w-20260101-abc",
|
|
27486
|
+
"pai audit scan ./session.jsonl --json"
|
|
27487
|
+
],
|
|
27488
|
+
seeAlso: ["pai rules list", "pai audit tokens session"]
|
|
27489
|
+
}, (ctx) => auditScanCmd({
|
|
27490
|
+
json: ctx.opts.json === true,
|
|
27491
|
+
session: ctx.opts.session,
|
|
27492
|
+
worker: ctx.opts.worker,
|
|
27493
|
+
path: ctx.args.PATH
|
|
27494
|
+
}))]
|
|
27495
|
+
};
|
|
27496
|
+
|
|
27497
|
+
//#endregion
|
|
27498
|
+
//#region src/session/git-notes.ts
|
|
27499
|
+
/** Link commits to the session/worker that wrote them via `git notes --ref=pai`. */
|
|
27500
|
+
const EDIT_TOOLS = new Set([
|
|
27501
|
+
"Edit",
|
|
27502
|
+
"Write",
|
|
27503
|
+
"MultiEdit",
|
|
27504
|
+
"NotebookEdit"
|
|
27505
|
+
]);
|
|
27506
|
+
const HOOK_MARKER = "# PAI-GIT-NOTES: managed by `pai git setup`";
|
|
27507
|
+
const NOTES_REF = "refs/notes/pai";
|
|
27508
|
+
const LOOKBACK_MS = 6 * 3600 * 1e3;
|
|
27509
|
+
const real = (p) => {
|
|
27510
|
+
try {
|
|
27511
|
+
return realpathSync(p);
|
|
27512
|
+
} catch {
|
|
27513
|
+
return resolve(p);
|
|
27514
|
+
}
|
|
27515
|
+
};
|
|
27516
|
+
function jsonlFiles(dir, sinceMs, out = []) {
|
|
27517
|
+
let names;
|
|
27518
|
+
try {
|
|
27519
|
+
names = readdirSync(dir);
|
|
27520
|
+
} catch {
|
|
27521
|
+
return out;
|
|
27522
|
+
}
|
|
27523
|
+
for (const n of names) {
|
|
27524
|
+
const p = join(dir, n);
|
|
27525
|
+
try {
|
|
27526
|
+
const st = statSync(p);
|
|
27527
|
+
if (st.isDirectory()) jsonlFiles(p, sinceMs, out);
|
|
27528
|
+
else if (/\.jsonl(\.1)?$/.test(n) && st.mtimeMs >= sinceMs) out.push(p);
|
|
27529
|
+
} catch {}
|
|
27530
|
+
}
|
|
27531
|
+
return out;
|
|
27532
|
+
}
|
|
27533
|
+
/** Sessions whose Edit/Write-style tool events (after `sinceMs`) touched any of `paths`. */
|
|
27534
|
+
function sessionsTouching(paths, sinceMs, captureDir) {
|
|
27535
|
+
const wanted = new Map(paths.map((p) => [real(p), p]));
|
|
27536
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
27537
|
+
for (const file of jsonlFiles(captureDir, sinceMs)) {
|
|
27538
|
+
let text;
|
|
27539
|
+
try {
|
|
27540
|
+
text = readFileSync(file, "utf8");
|
|
27541
|
+
} catch {
|
|
27542
|
+
continue;
|
|
27543
|
+
}
|
|
27544
|
+
for (const line of text.split("\n")) {
|
|
27545
|
+
if (!line.includes("file_path") && !line.includes("notebook_path")) continue;
|
|
27546
|
+
let e;
|
|
27547
|
+
try {
|
|
27548
|
+
e = JSON.parse(line);
|
|
27549
|
+
} catch {
|
|
27550
|
+
continue;
|
|
27551
|
+
}
|
|
27552
|
+
const tool = e.tool ?? e.payload?.tool_name;
|
|
27553
|
+
if (!EDIT_TOOLS.has(tool)) continue;
|
|
27554
|
+
const ts = typeof e.timestamp === "number" ? e.timestamp : Date.parse(e.timestamp);
|
|
27555
|
+
if (Number.isFinite(ts) && ts < sinceMs) continue;
|
|
27556
|
+
const input = e.input ?? e.payload?.tool_input ?? {};
|
|
27557
|
+
const fp = input.file_path ?? input.notebook_path;
|
|
27558
|
+
const sessionId = e.session ?? e.session_id;
|
|
27559
|
+
if (typeof fp !== "string" || typeof sessionId !== "string") continue;
|
|
27560
|
+
const orig = wanted.get(real(fp));
|
|
27561
|
+
if (!orig) continue;
|
|
27562
|
+
const workerId = e.workerId ?? e.worker_id ?? e.env?.PAI_WORKER_ID ?? e.payload?.env?.PAI_WORKER_ID;
|
|
27563
|
+
const cur = bySession.get(sessionId) ?? {
|
|
27564
|
+
sessionId,
|
|
27565
|
+
files: []
|
|
27566
|
+
};
|
|
27567
|
+
if (typeof workerId === "string" && !cur.workerId) cur.workerId = workerId;
|
|
27568
|
+
if (!cur.files.includes(orig)) cur.files.push(orig);
|
|
27569
|
+
bySession.set(sessionId, cur);
|
|
27570
|
+
}
|
|
27571
|
+
}
|
|
27572
|
+
return [...bySession.values()];
|
|
27573
|
+
}
|
|
27574
|
+
/** Note body: one `pai-session` line per session, plus `pai-worker` where known. */
|
|
27575
|
+
function noteText(entries) {
|
|
27576
|
+
const lines = [];
|
|
27577
|
+
for (const e of entries) {
|
|
27578
|
+
lines.push(`pai-session: ${e.sessionId}`);
|
|
27579
|
+
if (e.workerId) lines.push(`pai-worker: ${e.workerId}`);
|
|
27580
|
+
}
|
|
27581
|
+
return lines.join("\n");
|
|
27582
|
+
}
|
|
27583
|
+
/** Attach `text` to `commit` under refs/notes/pai; false on any error. */
|
|
27584
|
+
function writeNote(repo, commit, text) {
|
|
27585
|
+
try {
|
|
27586
|
+
execFileSync("git", [
|
|
27587
|
+
"notes",
|
|
27588
|
+
"--ref=pai",
|
|
27589
|
+
"add",
|
|
27590
|
+
"-f",
|
|
27591
|
+
"-m",
|
|
27592
|
+
text,
|
|
27593
|
+
commit
|
|
27594
|
+
], {
|
|
27595
|
+
cwd: repo,
|
|
27596
|
+
timeout: 3e3,
|
|
27597
|
+
stdio: "ignore"
|
|
27598
|
+
});
|
|
27599
|
+
return true;
|
|
27600
|
+
} catch {
|
|
27601
|
+
return false;
|
|
27602
|
+
}
|
|
27603
|
+
}
|
|
27604
|
+
const git = (repo, args, timeout = 1e3) => execFileSync("git", args, {
|
|
27605
|
+
cwd: repo,
|
|
27606
|
+
timeout,
|
|
27607
|
+
encoding: "utf8",
|
|
27608
|
+
stdio: [
|
|
27609
|
+
"ignore",
|
|
27610
|
+
"pipe",
|
|
27611
|
+
"ignore"
|
|
27612
|
+
]
|
|
27613
|
+
}).trim();
|
|
27614
|
+
const gitOk = (repo, args) => {
|
|
27615
|
+
try {
|
|
27616
|
+
git(repo, args);
|
|
27617
|
+
return true;
|
|
27618
|
+
} catch {
|
|
27619
|
+
return false;
|
|
27620
|
+
}
|
|
27621
|
+
};
|
|
27622
|
+
/** The post-commit work: note the sessions that edited the files of HEAD. Never throws. */
|
|
27623
|
+
function postCommitHook(repo, captureDir, env = process.env, now = Date.now()) {
|
|
27624
|
+
try {
|
|
27625
|
+
if (/rebase/.test(env.GIT_REFLOG_ACTION ?? "")) return false;
|
|
27626
|
+
if (gitOk(repo, [
|
|
27627
|
+
"rev-parse",
|
|
27628
|
+
"-q",
|
|
27629
|
+
"--verify",
|
|
27630
|
+
"REBASE_HEAD"
|
|
27631
|
+
]) || gitOk(repo, [
|
|
27632
|
+
"rev-parse",
|
|
27633
|
+
"-q",
|
|
27634
|
+
"--verify",
|
|
27635
|
+
"CHERRY_PICK_HEAD"
|
|
27636
|
+
])) return false;
|
|
27637
|
+
const top = git(repo, ["rev-parse", "--show-toplevel"]);
|
|
27638
|
+
const text = noteText(sessionsTouching(git(repo, [
|
|
27639
|
+
"diff-tree",
|
|
27640
|
+
"--root",
|
|
27641
|
+
"--no-commit-id",
|
|
27642
|
+
"--name-only",
|
|
27643
|
+
"-r",
|
|
27644
|
+
"HEAD"
|
|
27645
|
+
]).split("\n").filter(Boolean).map((f) => join(top, f)), now - LOOKBACK_MS, captureDir));
|
|
27646
|
+
return text ? writeNote(repo, "HEAD", text) : false;
|
|
27647
|
+
} catch {
|
|
27648
|
+
return false;
|
|
27649
|
+
}
|
|
27650
|
+
}
|
|
27651
|
+
function hookPath(repo) {
|
|
27652
|
+
const p = git(repo, [
|
|
27653
|
+
"rev-parse",
|
|
27654
|
+
"--git-path",
|
|
27655
|
+
"hooks/post-commit"
|
|
27656
|
+
]);
|
|
27657
|
+
return isAbsolute(p) ? p : resolve(repo, p);
|
|
27658
|
+
}
|
|
27659
|
+
/** Install the post-commit hook, chaining an existing foreign hook as post-commit.pre-pai. */
|
|
27660
|
+
function setupGitNotes(repo) {
|
|
27661
|
+
const hook = hookPath(repo);
|
|
27662
|
+
const old = `${hook}.pre-pai`;
|
|
27663
|
+
let chained = existsSync(old);
|
|
27664
|
+
if (existsSync(hook) && !readFileSync(hook, "utf8").includes(HOOK_MARKER)) {
|
|
27665
|
+
renameSync(hook, old);
|
|
27666
|
+
chained = true;
|
|
27667
|
+
}
|
|
27668
|
+
writeFileSync(hook, `#!/bin/sh\n${HOOK_MARKER}\n${chained ? "d=$(dirname \"$0\")\n[ -x \"$d/post-commit.pre-pai\" ] && \"$d/post-commit.pre-pai\" \"$@\"\n" : ""}pai git hook post-commit || true\n`);
|
|
27669
|
+
chmodSync(hook, 493);
|
|
27670
|
+
git(repo, [
|
|
27671
|
+
"config",
|
|
27672
|
+
"notes.rewriteRef",
|
|
27673
|
+
NOTES_REF
|
|
27674
|
+
]);
|
|
27675
|
+
return {
|
|
27676
|
+
hook,
|
|
27677
|
+
chained
|
|
27678
|
+
};
|
|
27679
|
+
}
|
|
27680
|
+
/** Remove the PAI hook and restore the chained one; false when PAI had no hook here. */
|
|
27681
|
+
function removeGitNotes(repo) {
|
|
27682
|
+
const hook = hookPath(repo);
|
|
27683
|
+
const old = `${hook}.pre-pai`;
|
|
27684
|
+
if (!existsSync(hook) || !readFileSync(hook, "utf8").includes(HOOK_MARKER)) return false;
|
|
27685
|
+
unlinkSync(hook);
|
|
27686
|
+
if (existsSync(old)) renameSync(old, hook);
|
|
27687
|
+
try {
|
|
27688
|
+
if (git(repo, ["config", "notes.rewriteRef"]) === NOTES_REF) git(repo, [
|
|
27689
|
+
"config",
|
|
27690
|
+
"--unset",
|
|
27691
|
+
"notes.rewriteRef"
|
|
27692
|
+
]);
|
|
27693
|
+
} catch {}
|
|
27694
|
+
return true;
|
|
27695
|
+
}
|
|
27696
|
+
/** The PAI note of `rev`, or undefined. */
|
|
27697
|
+
function showNote(repo, rev = "HEAD") {
|
|
27698
|
+
try {
|
|
27699
|
+
return git(repo, [
|
|
27700
|
+
"notes",
|
|
27701
|
+
"--ref=pai",
|
|
27702
|
+
"show",
|
|
27703
|
+
rev
|
|
27704
|
+
]) || void 0;
|
|
27705
|
+
} catch {
|
|
27706
|
+
return;
|
|
27707
|
+
}
|
|
27708
|
+
}
|
|
27709
|
+
|
|
27710
|
+
//#endregion
|
|
27711
|
+
//#region src/cli/commands/git.ts
|
|
27712
|
+
/** `pai git ...`: link commits to the session/worker that wrote them (git notes, ref pai). */
|
|
27713
|
+
const repoOrFail = () => process.cwd();
|
|
27714
|
+
const gitCommand = {
|
|
27715
|
+
name: "git",
|
|
27716
|
+
summary: "Link commits to the session that wrote them",
|
|
27717
|
+
description: "Installs a post-commit hook that records the session and worker that edited a commit's files as a git note (ref pai).",
|
|
27718
|
+
examples: [
|
|
27719
|
+
"pai git setup",
|
|
27720
|
+
"pai git notes",
|
|
27721
|
+
"pai git remove"
|
|
27722
|
+
],
|
|
27723
|
+
seeAlso: ["pai worker ps"],
|
|
27724
|
+
subcommands: [
|
|
27725
|
+
{
|
|
27726
|
+
name: "setup",
|
|
27727
|
+
summary: "Install the post-commit hook in the current repository",
|
|
27728
|
+
description: "Writes .git/hooks/post-commit, keeping an existing hook as post-commit.pre-pai and calling it first, and sets notes.rewriteRef so notes follow amends and rebases.",
|
|
27729
|
+
examples: ["pai git setup"],
|
|
27730
|
+
run: () => {
|
|
27731
|
+
try {
|
|
27732
|
+
const r = setupGitNotes(repoOrFail());
|
|
27733
|
+
process.stdout.write(`Installed ${r.hook}${r.chained ? " (existing hook chained)" : ""}\n`);
|
|
27734
|
+
return 0;
|
|
27735
|
+
} catch {
|
|
27736
|
+
throw new CliError("Not a git repository", "Run it inside a repository");
|
|
27737
|
+
}
|
|
27738
|
+
}
|
|
27739
|
+
},
|
|
27740
|
+
{
|
|
27741
|
+
name: "remove",
|
|
27742
|
+
summary: "Remove the hook and restore the previous one",
|
|
27743
|
+
description: "Deletes the PAI post-commit hook, puts a chained hook back and unsets notes.rewriteRef.",
|
|
27744
|
+
examples: ["pai git remove"],
|
|
27745
|
+
run: () => {
|
|
27746
|
+
process.stdout.write(removeGitNotes(repoOrFail()) ? "Removed the PAI post-commit hook\n" : "No PAI hook installed\n");
|
|
27747
|
+
return 0;
|
|
27748
|
+
}
|
|
27749
|
+
},
|
|
27750
|
+
{
|
|
27751
|
+
name: "notes",
|
|
27752
|
+
summary: "Show the PAI note of a commit",
|
|
27753
|
+
description: "Prints the session and worker recorded for a commit (default HEAD), or 'no PAI note'.",
|
|
27754
|
+
args: [{
|
|
27755
|
+
name: "REV",
|
|
27756
|
+
summary: "Commit to show (default HEAD)"
|
|
27757
|
+
}],
|
|
27758
|
+
examples: ["pai git notes", "pai git notes HEAD~1"],
|
|
27759
|
+
run: (ctx) => {
|
|
27760
|
+
process.stdout.write((showNote(repoOrFail(), ctx.args.REV) ?? "no PAI note") + "\n");
|
|
27761
|
+
return 0;
|
|
27762
|
+
}
|
|
27763
|
+
},
|
|
27764
|
+
{
|
|
27765
|
+
name: "hook",
|
|
27766
|
+
summary: "Git hook entry points (called by git)",
|
|
27767
|
+
description: "Internal: `pai git hook post-commit` is what the installed hook runs. Always exits 0.",
|
|
27768
|
+
examples: ["pai git hook post-commit"],
|
|
27769
|
+
hidden: true,
|
|
27770
|
+
subcommands: [{
|
|
27771
|
+
name: "post-commit",
|
|
27772
|
+
summary: "Record the sessions that edited the last commit's files",
|
|
27773
|
+
description: "Skips during rebase and cherry-pick; looks back 6 hours in the raw captures. Always exits 0.",
|
|
27774
|
+
examples: ["pai git hook post-commit"],
|
|
27775
|
+
run: () => {
|
|
27776
|
+
postCommitHook(process.cwd(), join(historyDir(), "raw-outputs"));
|
|
27777
|
+
return 0;
|
|
27778
|
+
}
|
|
27779
|
+
}]
|
|
27780
|
+
}
|
|
27781
|
+
]
|
|
27782
|
+
};
|
|
27783
|
+
|
|
27784
|
+
//#endregion
|
|
27785
|
+
//#region src/cli/commands/lessons.ts
|
|
27786
|
+
/** `pai lessons ...`: review-gated lessons (candidate, approved, rejected, superseded). */
|
|
27787
|
+
const ID = {
|
|
27788
|
+
name: "ID",
|
|
27789
|
+
required: true,
|
|
27790
|
+
summary: "Lesson id (its file name without .md)"
|
|
27791
|
+
};
|
|
27792
|
+
function state(v) {
|
|
27793
|
+
if (v === void 0) return void 0;
|
|
27794
|
+
if (!LESSON_STATES.includes(v)) throw new CliError(`unknown state "${v}" (${LESSON_STATES.join("|")})`);
|
|
27795
|
+
return v;
|
|
27796
|
+
}
|
|
27797
|
+
function row(l) {
|
|
27798
|
+
return `${l.id} [${l.state}] ${l.kind} ${l.project} ${l.title}`;
|
|
27799
|
+
}
|
|
27800
|
+
function wrap(f) {
|
|
27801
|
+
try {
|
|
27802
|
+
return f();
|
|
27803
|
+
} catch (e) {
|
|
27804
|
+
throw new CliError(e instanceof Error ? e.message : String(e));
|
|
27805
|
+
}
|
|
27806
|
+
}
|
|
27807
|
+
/** Newest transcript of the current directory under ~/.claude/projects. */
|
|
27808
|
+
function newestTranscriptOfCwd() {
|
|
27809
|
+
const dir = join(claudeProjectsDir(), process.cwd().replace(/[^a-zA-Z0-9]/g, "-"));
|
|
27810
|
+
if (!existsSync(dir)) return null;
|
|
27811
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => ({
|
|
27812
|
+
f: join(dir, f),
|
|
27813
|
+
m: statSync(join(dir, f)).mtimeMs
|
|
27814
|
+
}));
|
|
27815
|
+
files.sort((a, b) => b.m - a.m);
|
|
27816
|
+
return files[0]?.f ?? null;
|
|
27817
|
+
}
|
|
27818
|
+
const transition = (name, to, summary) => ({
|
|
27819
|
+
name,
|
|
27820
|
+
summary,
|
|
27821
|
+
description: `${summary}. Only candidates can be ${name}d.`,
|
|
27822
|
+
args: [ID],
|
|
27823
|
+
examples: [`pai lessons ${name} my-lesson`],
|
|
27824
|
+
run: (ctx) => {
|
|
27825
|
+
const l = wrap(() => setState(ctx.args.ID, to));
|
|
27826
|
+
console.log(`${l.id}: ${l.state}`);
|
|
27827
|
+
return 0;
|
|
27828
|
+
}
|
|
27829
|
+
});
|
|
27830
|
+
const lessonsCommand = {
|
|
27831
|
+
name: "lessons",
|
|
27832
|
+
summary: "Review-gated lessons for agents",
|
|
27833
|
+
description: "A short curated memory layer, one Markdown file per lesson in the PAI home. Candidates only reach agents once approved.",
|
|
27834
|
+
examples: [
|
|
27835
|
+
"pai lessons list --state candidate",
|
|
27836
|
+
"pai lessons propose",
|
|
27837
|
+
"pai lessons approve my-lesson"
|
|
27838
|
+
],
|
|
27839
|
+
seeAlso: ["pai memory"],
|
|
27840
|
+
subcommands: [
|
|
27841
|
+
{
|
|
27842
|
+
name: "list",
|
|
27843
|
+
summary: "List lessons",
|
|
27844
|
+
description: "Lists lessons, optionally filtered by state or project.",
|
|
27845
|
+
options: [
|
|
27846
|
+
{
|
|
27847
|
+
name: "state",
|
|
27848
|
+
type: "string",
|
|
27849
|
+
value: "S",
|
|
27850
|
+
summary: "candidate, approved, rejected or superseded"
|
|
27851
|
+
},
|
|
27852
|
+
{
|
|
27853
|
+
name: "project",
|
|
27854
|
+
type: "string",
|
|
27855
|
+
value: "P",
|
|
27856
|
+
summary: "Project slug or global"
|
|
27857
|
+
},
|
|
27858
|
+
{
|
|
27859
|
+
...OPT.json,
|
|
27860
|
+
summary: "Print JSON"
|
|
27861
|
+
}
|
|
27862
|
+
],
|
|
27863
|
+
examples: ["pai lessons list", "pai lessons list --state approved --project global"],
|
|
27864
|
+
run: (ctx) => {
|
|
27865
|
+
const ls = wrap(() => listLessons({
|
|
27866
|
+
state: state(ctx.opts.state),
|
|
27867
|
+
project: ctx.opts.project
|
|
27868
|
+
}));
|
|
27869
|
+
if (ctx.opts.json === true) console.log(JSON.stringify(ls, null, 2));
|
|
27870
|
+
else if (ls.length === 0) console.log("No lessons.");
|
|
27871
|
+
else for (const l of ls) console.log(row(l));
|
|
27872
|
+
return 0;
|
|
27873
|
+
}
|
|
27874
|
+
},
|
|
27875
|
+
{
|
|
27876
|
+
name: "show",
|
|
27877
|
+
summary: "Show one lesson",
|
|
27878
|
+
description: "Prints a lesson with its frontmatter fields and body.",
|
|
27879
|
+
args: [ID],
|
|
27880
|
+
examples: ["pai lessons show my-lesson"],
|
|
27881
|
+
run: (ctx) => {
|
|
27882
|
+
const l = getLesson(ctx.args.ID);
|
|
27883
|
+
if (!l) throw new CliError(`lesson not found: ${ctx.args.ID}`);
|
|
27884
|
+
console.log(row(l));
|
|
27885
|
+
console.log(`applies when: ${l.applies_when || "(not set)"}`);
|
|
27886
|
+
console.log(`evidence: ${l.evidence.join(", ") || "(none)"}`);
|
|
27887
|
+
if (l.superseded_by) console.log(`superseded by: ${l.superseded_by}`);
|
|
27888
|
+
console.log(`\n${l.body}`);
|
|
27889
|
+
return 0;
|
|
27890
|
+
}
|
|
27891
|
+
},
|
|
27892
|
+
{
|
|
27893
|
+
name: "add",
|
|
27894
|
+
summary: "Add a candidate lesson",
|
|
27895
|
+
description: "Creates a lesson in the candidate state; approve it after review.",
|
|
27896
|
+
options: [
|
|
27897
|
+
{
|
|
27898
|
+
name: "title",
|
|
27899
|
+
type: "string",
|
|
27900
|
+
value: "T",
|
|
27901
|
+
summary: "Short title"
|
|
27902
|
+
},
|
|
27903
|
+
{
|
|
27904
|
+
name: "kind",
|
|
27905
|
+
type: "string",
|
|
27906
|
+
value: "K",
|
|
27907
|
+
summary: LESSON_KINDS.join(", ")
|
|
27908
|
+
},
|
|
27909
|
+
{
|
|
27910
|
+
name: "applies-when",
|
|
27911
|
+
type: "string",
|
|
27912
|
+
value: "W",
|
|
27913
|
+
summary: "One sentence: when the lesson applies"
|
|
27914
|
+
},
|
|
27915
|
+
{
|
|
27916
|
+
name: "project",
|
|
27917
|
+
type: "string",
|
|
27918
|
+
value: "P",
|
|
27919
|
+
summary: "Project slug (default: global)"
|
|
27920
|
+
},
|
|
27921
|
+
{
|
|
27922
|
+
name: "body",
|
|
27923
|
+
type: "string",
|
|
27924
|
+
value: "B",
|
|
27925
|
+
summary: "The lesson in 1-5 sentences"
|
|
27926
|
+
}
|
|
27927
|
+
],
|
|
27928
|
+
examples: ["pai lessons add --title \"Run tsc first\" --kind workflow --applies-when \"before committing\""],
|
|
27929
|
+
run: (ctx) => {
|
|
27930
|
+
const { title, kind } = ctx.opts;
|
|
27931
|
+
const when = ctx.opts["applies-when"];
|
|
27932
|
+
if (!title || !kind || !when) throw new CliError("--title, --kind and --applies-when are required");
|
|
27933
|
+
const l = wrap(() => addLesson({
|
|
27934
|
+
title: String(title),
|
|
27935
|
+
kind,
|
|
27936
|
+
applies_when: String(when),
|
|
27937
|
+
project: ctx.opts.project,
|
|
27938
|
+
body: ctx.opts.body
|
|
27939
|
+
}));
|
|
27940
|
+
console.log(`added ${l.id} [candidate]`);
|
|
27941
|
+
return 0;
|
|
27942
|
+
}
|
|
27943
|
+
},
|
|
27944
|
+
transition("approve", "approved", "Approve a candidate"),
|
|
27945
|
+
transition("reject", "rejected", "Reject a candidate"),
|
|
27946
|
+
{
|
|
27947
|
+
name: "promote",
|
|
27948
|
+
summary: "Promote an approved lesson to a project skill",
|
|
27949
|
+
description: "Writes the lesson as a SKILL.md under <project>/.claude/skills/<slug>/ (and with --agents also <project>/.agents/skills/<slug>/). Refuses to overwrite an existing skill unless --force.",
|
|
27950
|
+
args: [ID],
|
|
27951
|
+
options: [
|
|
27952
|
+
{
|
|
27953
|
+
name: "project",
|
|
27954
|
+
type: "string",
|
|
27955
|
+
value: "DIR",
|
|
27956
|
+
summary: "Project directory (default: this directory)"
|
|
27957
|
+
},
|
|
27958
|
+
{
|
|
27959
|
+
name: "force",
|
|
27960
|
+
type: "boolean",
|
|
27961
|
+
summary: "Overwrite an existing SKILL.md"
|
|
27962
|
+
},
|
|
27963
|
+
{
|
|
27964
|
+
name: "agents",
|
|
27965
|
+
type: "boolean",
|
|
27966
|
+
summary: "Also write under .agents/skills"
|
|
27967
|
+
}
|
|
27968
|
+
],
|
|
27969
|
+
examples: ["pai lessons promote my-lesson --project ~/code/app --agents"],
|
|
27970
|
+
run: (ctx) => {
|
|
27971
|
+
const paths = wrap(() => promoteLesson(ctx.args.ID, {
|
|
27972
|
+
projectDir: ctx.opts.project ?? process.cwd(),
|
|
27973
|
+
force: ctx.opts.force === true,
|
|
27974
|
+
agentsDir: ctx.opts.agents === true
|
|
27975
|
+
}));
|
|
27976
|
+
for (const p of paths) console.log(`promoted ${p}`);
|
|
27977
|
+
return 0;
|
|
27978
|
+
}
|
|
27979
|
+
},
|
|
27980
|
+
{
|
|
27981
|
+
name: "supersede",
|
|
27982
|
+
summary: "Replace an approved lesson by another approved one",
|
|
27983
|
+
description: "Marks ID superseded; the replacement given with --by must exist and be approved.",
|
|
27984
|
+
args: [ID],
|
|
27985
|
+
options: [{
|
|
27986
|
+
name: "by",
|
|
27987
|
+
type: "string",
|
|
27988
|
+
value: "ID2",
|
|
27989
|
+
summary: "The approved lesson that replaces it"
|
|
27990
|
+
}],
|
|
27991
|
+
examples: ["pai lessons supersede old-lesson --by new-lesson"],
|
|
27992
|
+
run: (ctx) => {
|
|
27993
|
+
const by = ctx.opts.by;
|
|
27994
|
+
if (!by) throw new CliError("--by is required");
|
|
27995
|
+
const l = wrap(() => setState(ctx.args.ID, "superseded", { supersededBy: by }));
|
|
27996
|
+
console.log(`${l.id}: superseded by ${by}`);
|
|
27997
|
+
return 0;
|
|
27998
|
+
}
|
|
27999
|
+
},
|
|
28000
|
+
{
|
|
28001
|
+
name: "propose",
|
|
28002
|
+
summary: "Add candidates from a transcript",
|
|
28003
|
+
description: "Finds user messages that correct the assistant (no model call) and adds each as a candidate to review.",
|
|
28004
|
+
args: [{
|
|
28005
|
+
name: "TRANSCRIPT",
|
|
28006
|
+
summary: "Transcript file (default: newest of this directory)"
|
|
28007
|
+
}],
|
|
28008
|
+
examples: ["pai lessons propose", "pai lessons propose ./session.jsonl"],
|
|
28009
|
+
run: (ctx) => {
|
|
28010
|
+
const path = ctx.args.TRANSCRIPT ?? newestTranscriptOfCwd();
|
|
28011
|
+
if (!path) throw new CliError("no transcript found for this directory");
|
|
28012
|
+
const added = wrap(() => proposeFromTranscript(path).map(addLesson));
|
|
28013
|
+
if (added.length === 0) console.log("No corrections found.");
|
|
28014
|
+
for (const l of added) console.log(`added ${l.id} [candidate] ${l.title}`);
|
|
28015
|
+
return 0;
|
|
28016
|
+
}
|
|
28017
|
+
}
|
|
28018
|
+
]
|
|
28019
|
+
};
|
|
28020
|
+
|
|
28021
|
+
//#endregion
|
|
28022
|
+
//#region src/harness/claude-code.ts
|
|
28023
|
+
/**
|
|
28024
|
+
* claude-code.ts — adapter over Claude Code's JSONL transcripts:
|
|
28025
|
+
* `<claude home>/projects/<encoded cwd>/<session id>.jsonl`. The encoded-cwd
|
|
28026
|
+
* spelling is the one PAI already uses everywhere (encodeDir, src/cli/lib/utils.ts).
|
|
28027
|
+
*/
|
|
28028
|
+
/** Text of a tool_result's content: a string or its text blocks joined. */
|
|
28029
|
+
function resultText(c) {
|
|
28030
|
+
if (typeof c === "string") return c;
|
|
28031
|
+
if (Array.isArray(c)) return c.map((b) => b && typeof b === "object" && typeof b.text === "string" ? b.text : "").join("\n");
|
|
28032
|
+
return "";
|
|
28033
|
+
}
|
|
28034
|
+
/** Exit code a Bash result shows ("Exit code N" header or an exitCode field); else undefined. */
|
|
28035
|
+
function exitCodeOf(content) {
|
|
28036
|
+
if (typeof content === "string") {
|
|
28037
|
+
const m = /^Exit code (\d+)/m.exec(content);
|
|
28038
|
+
return m ? Number(m[1]) : void 0;
|
|
28039
|
+
}
|
|
28040
|
+
if (content && typeof content === "object" && !Array.isArray(content)) {
|
|
28041
|
+
const v = content.exitCode;
|
|
28042
|
+
if (typeof v === "number") return v;
|
|
28043
|
+
}
|
|
28044
|
+
}
|
|
28045
|
+
/** Events of one parsed transcript line; nothing for lines that carry no event. */
|
|
28046
|
+
function eventsOfLine(harness, sessionId, obj) {
|
|
28047
|
+
const ts = obj.timestamp ?? "";
|
|
28048
|
+
const content = obj.message?.content;
|
|
28049
|
+
const out = [];
|
|
28050
|
+
const blocks = Array.isArray(content) ? content : [];
|
|
28051
|
+
if (obj.type === "user") {
|
|
28052
|
+
for (const b of blocks) if (b.type === "tool_result") out.push({
|
|
28053
|
+
harness,
|
|
28054
|
+
sessionId,
|
|
28055
|
+
ts,
|
|
28056
|
+
kind: "tool_result",
|
|
28057
|
+
text: resultText(b.content),
|
|
28058
|
+
exitCode: exitCodeOf(b.content),
|
|
28059
|
+
source: "transcript",
|
|
28060
|
+
fidelity: "observed"
|
|
28061
|
+
});
|
|
28062
|
+
if (!obj.isMeta) {
|
|
28063
|
+
const text = typeof content === "string" ? content : blocks.filter((b) => b.type === "text").map((b) => b.text ?? "").join("\n");
|
|
28064
|
+
if (text) out.push({
|
|
28065
|
+
harness,
|
|
28066
|
+
sessionId,
|
|
28067
|
+
ts,
|
|
28068
|
+
kind: "user",
|
|
28069
|
+
text,
|
|
28070
|
+
source: "transcript",
|
|
28071
|
+
fidelity: "observed"
|
|
28072
|
+
});
|
|
28073
|
+
}
|
|
28074
|
+
return out;
|
|
28075
|
+
}
|
|
28076
|
+
if (obj.type === "assistant" && Array.isArray(content)) {
|
|
28077
|
+
for (const b of content) if (b.type === "text" && b.text) out.push({
|
|
28078
|
+
harness,
|
|
28079
|
+
sessionId,
|
|
28080
|
+
ts,
|
|
28081
|
+
kind: "assistant",
|
|
28082
|
+
text: b.text,
|
|
28083
|
+
source: "transcript",
|
|
28084
|
+
fidelity: "observed"
|
|
28085
|
+
});
|
|
28086
|
+
else if (b.type === "tool_use" && b.name) out.push({
|
|
28087
|
+
harness,
|
|
28088
|
+
sessionId,
|
|
28089
|
+
ts,
|
|
28090
|
+
kind: "tool_call",
|
|
28091
|
+
tool: b.name,
|
|
28092
|
+
input: b.input,
|
|
28093
|
+
source: "transcript",
|
|
28094
|
+
fidelity: "observed"
|
|
28095
|
+
});
|
|
28096
|
+
}
|
|
28097
|
+
return out;
|
|
28098
|
+
}
|
|
28099
|
+
/** Session files under a project dir: top-level `<id>.jsonl` only — the
|
|
28100
|
+
* `sessions/` archive holds hardlinks of the same files, and subagent logs
|
|
28101
|
+
* live in their own subdirs. */
|
|
28102
|
+
function sessionFiles(dir) {
|
|
28103
|
+
try {
|
|
28104
|
+
return readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
28105
|
+
} catch {
|
|
28106
|
+
return [];
|
|
28107
|
+
}
|
|
28108
|
+
}
|
|
28109
|
+
function toSession$1(harness, file, dir) {
|
|
28110
|
+
const id = file.slice(0, -6);
|
|
28111
|
+
let startedAt;
|
|
28112
|
+
let updatedAt;
|
|
28113
|
+
let cwd;
|
|
28114
|
+
try {
|
|
28115
|
+
for (const raw of readFileSync(join(dir, file), "utf8").split("\n")) {
|
|
28116
|
+
if (!raw.trim()) continue;
|
|
28117
|
+
let obj;
|
|
28118
|
+
try {
|
|
28119
|
+
obj = JSON.parse(raw);
|
|
28120
|
+
} catch {
|
|
28121
|
+
continue;
|
|
28122
|
+
}
|
|
28123
|
+
if (typeof obj.cwd === "string" && obj.cwd) cwd ??= obj.cwd;
|
|
28124
|
+
if (typeof obj.timestamp === "string" && obj.timestamp) {
|
|
28125
|
+
startedAt ??= obj.timestamp;
|
|
28126
|
+
updatedAt = obj.timestamp;
|
|
28127
|
+
}
|
|
28128
|
+
}
|
|
28129
|
+
} catch {}
|
|
28130
|
+
const mtime = new Date(statSync(join(dir, file)).mtimeMs).toISOString();
|
|
28131
|
+
return {
|
|
28132
|
+
harness,
|
|
28133
|
+
id,
|
|
28134
|
+
cwd,
|
|
28135
|
+
path: join(dir, file),
|
|
28136
|
+
startedAt,
|
|
28137
|
+
updatedAt: updatedAt ?? mtime
|
|
28138
|
+
};
|
|
28139
|
+
}
|
|
28140
|
+
const claudeCodeAdapter = {
|
|
28141
|
+
name: "claude-code",
|
|
28142
|
+
detect() {
|
|
28143
|
+
return existsSync(claudeProjectsDir());
|
|
28144
|
+
},
|
|
28145
|
+
listSessions(opts = {}) {
|
|
28146
|
+
const projectsDir = claudeProjectsDir();
|
|
28147
|
+
if (!existsSync(projectsDir)) return [];
|
|
28148
|
+
const dirs = opts.cwd ? [encodeDir(opts.cwd)] : readdirSafe(projectsDir);
|
|
28149
|
+
const out = [];
|
|
28150
|
+
for (const d of dirs) {
|
|
28151
|
+
const dir = join(projectsDir, d);
|
|
28152
|
+
if (!existsSync(dir)) continue;
|
|
28153
|
+
for (const f of sessionFiles(dir)) out.push(toSession$1(this.name, f, dir));
|
|
28154
|
+
}
|
|
28155
|
+
const since = opts.sinceMs !== void 0 ? Date.now() - opts.sinceMs : void 0;
|
|
28156
|
+
return out.filter((s) => since === void 0 || Date.parse(s.updatedAt) >= since).sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
|
|
28157
|
+
},
|
|
28158
|
+
*readEvents(session) {
|
|
28159
|
+
let text;
|
|
28160
|
+
try {
|
|
28161
|
+
text = readFileSync(session.path, "utf8");
|
|
28162
|
+
} catch {
|
|
28163
|
+
return;
|
|
28164
|
+
}
|
|
28165
|
+
for (const raw of text.split("\n")) {
|
|
28166
|
+
if (!raw.trim()) continue;
|
|
28167
|
+
let obj;
|
|
28168
|
+
try {
|
|
28169
|
+
obj = JSON.parse(raw);
|
|
28170
|
+
} catch {
|
|
28171
|
+
continue;
|
|
28172
|
+
}
|
|
28173
|
+
yield* eventsOfLine(this.name, session.id, obj);
|
|
28174
|
+
}
|
|
28175
|
+
}
|
|
28176
|
+
};
|
|
28177
|
+
function readdirSafe(dir) {
|
|
28178
|
+
try {
|
|
28179
|
+
return readdirSync(dir);
|
|
28180
|
+
} catch {
|
|
28181
|
+
return [];
|
|
28182
|
+
}
|
|
28183
|
+
}
|
|
28184
|
+
|
|
28185
|
+
//#endregion
|
|
28186
|
+
//#region src/harness/codex.ts
|
|
28187
|
+
/**
|
|
28188
|
+
* codex.ts — adapter over Codex CLI session logs:
|
|
28189
|
+
* `<codex home>/sessions/**\/rollout-*.jsonl`. Implemented against the
|
|
28190
|
+
* documented rollout shape (JSON lines; `type:"message"` with role
|
|
28191
|
+
* user/assistant, `function_call` / `function_call_output`); unknown entry
|
|
28192
|
+
* types and malformed lines are skipped rather than failing the session.
|
|
28193
|
+
*/
|
|
28194
|
+
/** Codex CLI's config dir: CODEX_HOME if set, else ~/.codex. */
|
|
28195
|
+
function codexHomeDir() {
|
|
28196
|
+
return process.env.CODEX_HOME || join(homedir(), ".codex");
|
|
28197
|
+
}
|
|
28198
|
+
/** Plain text of a message payload's content: a string, or text parts joined. */
|
|
28199
|
+
function contentText(c) {
|
|
28200
|
+
if (typeof c === "string") return c;
|
|
28201
|
+
if (Array.isArray(c)) return c.map((p) => {
|
|
28202
|
+
if (typeof p === "string") return p;
|
|
28203
|
+
if (p && typeof p === "object") {
|
|
28204
|
+
const o = p;
|
|
28205
|
+
if (typeof o.text === "string") return o.text;
|
|
28206
|
+
}
|
|
28207
|
+
return "";
|
|
28208
|
+
}).filter(Boolean).join("\n");
|
|
28209
|
+
return "";
|
|
28210
|
+
}
|
|
28211
|
+
/** function_call arguments: parsed as JSON when they are a JSON string. */
|
|
28212
|
+
function parseArgs(a) {
|
|
28213
|
+
if (typeof a !== "string") return a;
|
|
28214
|
+
try {
|
|
28215
|
+
return JSON.parse(a);
|
|
28216
|
+
} catch {
|
|
28217
|
+
return a;
|
|
28218
|
+
}
|
|
28219
|
+
}
|
|
28220
|
+
/** Events of one parsed rollout entry; nothing for entries that carry none. */
|
|
28221
|
+
function eventsOfEntry(sessionId, e) {
|
|
28222
|
+
const ts = e.timestamp ?? "";
|
|
28223
|
+
const p = e.payload;
|
|
28224
|
+
if (!p || !p.type) return [];
|
|
28225
|
+
if (p.type === "message" && (p.role === "user" || p.role === "assistant")) {
|
|
28226
|
+
const text = contentText(p.content);
|
|
28227
|
+
if (!text) return [];
|
|
28228
|
+
return [{
|
|
28229
|
+
harness: NAME,
|
|
28230
|
+
sessionId,
|
|
28231
|
+
ts,
|
|
28232
|
+
kind: p.role,
|
|
28233
|
+
text,
|
|
28234
|
+
source: "transcript",
|
|
28235
|
+
fidelity: "observed"
|
|
28236
|
+
}];
|
|
28237
|
+
}
|
|
28238
|
+
if (p.type === "function_call" && p.name) return [{
|
|
28239
|
+
harness: NAME,
|
|
28240
|
+
sessionId,
|
|
28241
|
+
ts,
|
|
28242
|
+
kind: "tool_call",
|
|
28243
|
+
tool: p.name,
|
|
28244
|
+
input: parseArgs(p.arguments),
|
|
28245
|
+
source: "transcript",
|
|
28246
|
+
fidelity: "observed"
|
|
28247
|
+
}];
|
|
28248
|
+
if (p.type === "function_call_output") return [{
|
|
28249
|
+
harness: NAME,
|
|
28250
|
+
sessionId,
|
|
28251
|
+
ts,
|
|
28252
|
+
kind: "tool_result",
|
|
28253
|
+
text: contentText(p.output),
|
|
28254
|
+
source: "transcript",
|
|
28255
|
+
fidelity: "observed"
|
|
28256
|
+
}];
|
|
28257
|
+
return [];
|
|
28258
|
+
}
|
|
28259
|
+
const NAME = "codex";
|
|
28260
|
+
/** rollout-*.jsonl under `dir`, recursing into subdirectories. */
|
|
28261
|
+
function rolloutFiles(dir) {
|
|
28262
|
+
const out = [];
|
|
28263
|
+
let entries;
|
|
28264
|
+
try {
|
|
28265
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
28266
|
+
} catch {
|
|
28267
|
+
return out;
|
|
28268
|
+
}
|
|
28269
|
+
for (const ent of entries) {
|
|
28270
|
+
const p = join(dir, ent.name);
|
|
28271
|
+
if (ent.isDirectory()) out.push(...rolloutFiles(p));
|
|
28272
|
+
else if (ent.name.startsWith("rollout-") && ent.name.endsWith(".jsonl")) out.push(p);
|
|
28273
|
+
}
|
|
28274
|
+
return out;
|
|
28275
|
+
}
|
|
28276
|
+
function toSession(file) {
|
|
28277
|
+
const id = file.slice(0, -6).split("/").pop() ?? file;
|
|
28278
|
+
let startedAt;
|
|
28279
|
+
let updatedAt;
|
|
28280
|
+
try {
|
|
28281
|
+
for (const raw of readFileSync(file, "utf8").split("\n")) {
|
|
28282
|
+
if (!raw.trim()) continue;
|
|
28283
|
+
let e;
|
|
28284
|
+
try {
|
|
28285
|
+
e = JSON.parse(raw);
|
|
28286
|
+
} catch {
|
|
28287
|
+
continue;
|
|
28288
|
+
}
|
|
28289
|
+
if (typeof e.timestamp === "string" && e.timestamp) {
|
|
28290
|
+
startedAt ??= e.timestamp;
|
|
28291
|
+
updatedAt = e.timestamp;
|
|
28292
|
+
}
|
|
28293
|
+
}
|
|
28294
|
+
} catch {}
|
|
28295
|
+
const mtime = new Date(statSync(file).mtimeMs).toISOString();
|
|
28296
|
+
return {
|
|
28297
|
+
harness: NAME,
|
|
28298
|
+
id,
|
|
28299
|
+
path: file,
|
|
28300
|
+
startedAt,
|
|
28301
|
+
updatedAt: updatedAt ?? mtime
|
|
28302
|
+
};
|
|
28303
|
+
}
|
|
28304
|
+
const codexAdapter = {
|
|
28305
|
+
name: NAME,
|
|
28306
|
+
detect() {
|
|
28307
|
+
return existsSync(join(codexHomeDir(), "sessions"));
|
|
28308
|
+
},
|
|
28309
|
+
listSessions(opts = {}) {
|
|
28310
|
+
const root = join(codexHomeDir(), "sessions");
|
|
28311
|
+
if (!existsSync(root)) return [];
|
|
28312
|
+
const out = rolloutFiles(root).map(toSession);
|
|
28313
|
+
const since = opts.sinceMs !== void 0 ? Date.now() - opts.sinceMs : void 0;
|
|
28314
|
+
return out.filter((s) => since === void 0 || Date.parse(s.updatedAt) >= since).sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
|
|
28315
|
+
},
|
|
28316
|
+
*readEvents(session) {
|
|
28317
|
+
let text;
|
|
28318
|
+
try {
|
|
28319
|
+
text = readFileSync(session.path, "utf8");
|
|
28320
|
+
} catch {
|
|
28321
|
+
return;
|
|
28322
|
+
}
|
|
28323
|
+
for (const raw of text.split("\n")) {
|
|
28324
|
+
if (!raw.trim()) continue;
|
|
28325
|
+
let e;
|
|
28326
|
+
try {
|
|
28327
|
+
e = JSON.parse(raw);
|
|
28328
|
+
} catch {
|
|
28329
|
+
continue;
|
|
28330
|
+
}
|
|
28331
|
+
yield* eventsOfEntry(session.id, e);
|
|
28332
|
+
}
|
|
28333
|
+
}
|
|
28334
|
+
};
|
|
28335
|
+
|
|
28336
|
+
//#endregion
|
|
28337
|
+
//#region src/harness/index.ts
|
|
28338
|
+
/**
|
|
28339
|
+
* index.ts — the adapter registry and the enabled set. Implementations are
|
|
28340
|
+
* listed here; which of them run is config (`harness.adapters` in the main
|
|
28341
|
+
* config, default ["claude-code"]), never a hardcoded enable list.
|
|
28342
|
+
*/
|
|
28343
|
+
/** Every implementation PAI knows, in display order. */
|
|
28344
|
+
const ADAPTERS = [claudeCodeAdapter, codexAdapter];
|
|
28345
|
+
/** Names enabled by config: `harness.adapters`, default ["claude-code"]. */
|
|
28346
|
+
function enabledHarnessNames(path = paiConfigFilePath()) {
|
|
28347
|
+
const list = effectiveMainConfig(path).harness?.adapters;
|
|
28348
|
+
if (!Array.isArray(list)) return ["claude-code"];
|
|
28349
|
+
return list.filter((n) => typeof n === "string");
|
|
28350
|
+
}
|
|
28351
|
+
|
|
28352
|
+
//#endregion
|
|
28353
|
+
//#region src/cli/commands/harness.ts
|
|
28354
|
+
/** `pai harness ...`: sessions PAI can read from agent harnesses (read-only). */
|
|
28355
|
+
const fmt = (iso) => Number.isFinite(Date.parse(iso)) ? new Date(iso).toISOString().slice(0, 16).replace("T", " ") : "—";
|
|
28356
|
+
function sessionsFor(harness, cwd) {
|
|
28357
|
+
const a = ADAPTERS.find((x) => x.name === harness);
|
|
28358
|
+
if (!a || !a.detect()) return [];
|
|
28359
|
+
return a.listSessions(cwd ? { cwd } : void 0);
|
|
28360
|
+
}
|
|
28361
|
+
function listCmd$1(opts) {
|
|
28362
|
+
const enabled = new Set(enabledHarnessNames());
|
|
28363
|
+
const rows = ADAPTERS.map((a) => {
|
|
28364
|
+
const detected = a.detect();
|
|
28365
|
+
return [
|
|
28366
|
+
a.name,
|
|
28367
|
+
detected ? ok$1("yes") : dim$1("no"),
|
|
28368
|
+
enabled.has(a.name) ? ok$1("yes") : dim$1("no"),
|
|
28369
|
+
detected ? String(sessionsFor(a.name).length) : dim$1("—")
|
|
28370
|
+
];
|
|
28371
|
+
});
|
|
28372
|
+
if (opts.json) {
|
|
28373
|
+
console.log(JSON.stringify(ADAPTERS.map((a) => ({
|
|
28374
|
+
adapter: a.name,
|
|
28375
|
+
detected: a.detect(),
|
|
28376
|
+
enabled: enabled.has(a.name),
|
|
28377
|
+
sessions: a.detect() ? sessionsFor(a.name).length : void 0
|
|
28378
|
+
})), null, 2));
|
|
28379
|
+
return 0;
|
|
28380
|
+
}
|
|
28381
|
+
console.log(renderTable$1([
|
|
28382
|
+
"adapter",
|
|
28383
|
+
"detected",
|
|
28384
|
+
"enabled",
|
|
28385
|
+
"sessions"
|
|
28386
|
+
], rows));
|
|
28387
|
+
console.log(dim$1(` enabled set comes from config key harness.adapters (pai config get harness.adapters)`));
|
|
28388
|
+
return 0;
|
|
28389
|
+
}
|
|
28390
|
+
function sessionsCmd(opts) {
|
|
28391
|
+
const names = opts.harness ? [opts.harness] : enabledHarnessNames();
|
|
28392
|
+
const out = [];
|
|
28393
|
+
for (const name of names) out.push(...sessionsFor(name, opts.cwd));
|
|
28394
|
+
out.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
|
|
28395
|
+
if (opts.json) {
|
|
28396
|
+
console.log(JSON.stringify(out, null, 2));
|
|
28397
|
+
return 0;
|
|
28398
|
+
}
|
|
28399
|
+
if (!out.length) {
|
|
28400
|
+
console.log(dim$1(` no sessions${opts.cwd ? ` under ${opts.cwd}` : ""} (${names.join(", ")})`));
|
|
28401
|
+
return 0;
|
|
28402
|
+
}
|
|
28403
|
+
console.log(renderTable$1([
|
|
28404
|
+
"harness",
|
|
28405
|
+
"id",
|
|
28406
|
+
"updated",
|
|
28407
|
+
"cwd"
|
|
28408
|
+
], out.map((s) => [
|
|
28409
|
+
s.harness,
|
|
28410
|
+
s.id,
|
|
28411
|
+
fmt(s.updatedAt),
|
|
28412
|
+
s.cwd ? maskSecrets(shortenPath(s.cwd, 48)) : dim$1("—")
|
|
28413
|
+
])));
|
|
28414
|
+
return 0;
|
|
28415
|
+
}
|
|
28416
|
+
const harnessCommand = {
|
|
28417
|
+
name: "harness",
|
|
28418
|
+
summary: "Sessions PAI can read from agent harnesses (Claude Code, Codex, ...)",
|
|
28419
|
+
description: "Read-only ingestion of other harnesses' session logs. `list` shows each adapter: detected (its store exists), enabled (config key harness.adapters), and its session count. `sessions` lists the sessions themselves.",
|
|
28420
|
+
examples: [
|
|
28421
|
+
"pai harness list",
|
|
28422
|
+
"pai harness sessions",
|
|
28423
|
+
"pai harness sessions --harness codex --cwd ~/proj"
|
|
28424
|
+
],
|
|
28425
|
+
seeAlso: ["pai config get harness.adapters"],
|
|
28426
|
+
subcommands: [{
|
|
28427
|
+
name: "list",
|
|
28428
|
+
summary: "Each adapter: detected, enabled, session count",
|
|
28429
|
+
description: "One row per adapter PAI knows. Only enabled adapters whose store exists are ever read.",
|
|
28430
|
+
options: [{
|
|
28431
|
+
name: "json",
|
|
28432
|
+
type: "boolean",
|
|
28433
|
+
default: false,
|
|
28434
|
+
summary: "Print JSON instead of a table"
|
|
28435
|
+
}],
|
|
28436
|
+
examples: ["pai harness list", "pai harness list --json"],
|
|
28437
|
+
run: (ctx) => listCmd$1({ json: ctx.opts.json === true })
|
|
28438
|
+
}, {
|
|
28439
|
+
name: "sessions",
|
|
28440
|
+
summary: "List sessions, newest first",
|
|
28441
|
+
description: "Sessions of the enabled adapters (or one harness with --harness), optionally limited to one working directory.",
|
|
28442
|
+
options: [
|
|
28443
|
+
{
|
|
28444
|
+
name: "harness",
|
|
28445
|
+
type: "string",
|
|
28446
|
+
value: "NAME",
|
|
28447
|
+
summary: "Only this adapter (default: the enabled set)"
|
|
28448
|
+
},
|
|
28449
|
+
{
|
|
28450
|
+
name: "cwd",
|
|
28451
|
+
type: "string",
|
|
28452
|
+
value: "DIR",
|
|
28453
|
+
summary: "Only sessions of this working directory"
|
|
28454
|
+
},
|
|
28455
|
+
{
|
|
28456
|
+
name: "json",
|
|
28457
|
+
type: "boolean",
|
|
28458
|
+
default: false,
|
|
28459
|
+
summary: "Print JSON instead of a table"
|
|
28460
|
+
}
|
|
28461
|
+
],
|
|
28462
|
+
examples: [
|
|
28463
|
+
"pai harness sessions",
|
|
28464
|
+
"pai harness sessions --harness codex",
|
|
28465
|
+
"pai harness sessions --cwd ~/proj"
|
|
28466
|
+
],
|
|
28467
|
+
run: (ctx) => {
|
|
28468
|
+
const cwd = ctx.opts.cwd;
|
|
28469
|
+
if (cwd && !existsSync(cwd)) {
|
|
28470
|
+
console.log(dim$1(` no such directory: ${cwd}`));
|
|
28471
|
+
return 0;
|
|
28472
|
+
}
|
|
28473
|
+
return sessionsCmd({
|
|
28474
|
+
harness: ctx.opts.harness,
|
|
28475
|
+
cwd,
|
|
28476
|
+
json: ctx.opts.json === true
|
|
28477
|
+
});
|
|
28478
|
+
}
|
|
28479
|
+
}]
|
|
28480
|
+
};
|
|
28481
|
+
|
|
28482
|
+
//#endregion
|
|
28483
|
+
//#region src/hooks/ts/lib/worker-git-guard.ts
|
|
28484
|
+
/** Splits on &&, ||, ;, | and newlines outside quotes. */
|
|
28485
|
+
function splitSegments(command) {
|
|
28486
|
+
const out = [];
|
|
28487
|
+
let cur = "";
|
|
28488
|
+
let quote = "";
|
|
28489
|
+
for (let i = 0; i < command.length; i++) {
|
|
28490
|
+
const c = command[i];
|
|
28491
|
+
if (quote) {
|
|
28492
|
+
if (c === quote) quote = "";
|
|
28493
|
+
cur += c;
|
|
28494
|
+
} else if (c === "\"" || c === "'") {
|
|
28495
|
+
quote = c;
|
|
28496
|
+
cur += c;
|
|
28497
|
+
} else if (c === "&" || c === "|" || c === ";" || c === "\n") {
|
|
28498
|
+
out.push(cur);
|
|
28499
|
+
cur = "";
|
|
28500
|
+
} else cur += c;
|
|
28501
|
+
}
|
|
28502
|
+
out.push(cur);
|
|
28503
|
+
return out.map((s) => s.trim()).filter(Boolean);
|
|
28504
|
+
}
|
|
28505
|
+
|
|
28506
|
+
//#endregion
|
|
28507
|
+
//#region src/hooks/ts/lib/rules/eval.ts
|
|
28508
|
+
/**
|
|
28509
|
+
* eval.ts — run rules against a tool call.
|
|
28510
|
+
*
|
|
28511
|
+
* Order is fixed: the first block hit wins, else the first confirm hit, else
|
|
28512
|
+
* the first warn hit. Text inside quotes is not a command, so rules test the
|
|
28513
|
+
* content with quoted strings removed; rules marked `quoted: true` also test
|
|
28514
|
+
* it with only the quote marks removed (as detectAttack did before);
|
|
28515
|
+
* confirm rules additionally test per-segment, so a quoted `git push -f` in a
|
|
28516
|
+
* commit message stays inert while a quoted `+main` refspec still counts.
|
|
28517
|
+
*/
|
|
28518
|
+
/** Quoted strings removed: text inside a commit message or echo is not a command. */
|
|
28519
|
+
function dropQuoted(segment) {
|
|
28520
|
+
return segment.replace(/"(?:\\.|[^"\\])*"|'[^']*'/g, "\"\"");
|
|
28521
|
+
}
|
|
28522
|
+
/** A quoted +refspec is still a forced push, so push segments also test with only the quote marks removed. */
|
|
28523
|
+
function gitPatternHit(pattern, segment) {
|
|
28524
|
+
const bare = dropQuoted(segment);
|
|
28525
|
+
if (pattern.test(bare)) return true;
|
|
28526
|
+
return /\bgit\b.*\spush\b/.test(bare) && pattern.test(segment.replace(/["']/g, ""));
|
|
28527
|
+
}
|
|
28528
|
+
const regexCache = /* @__PURE__ */ new Map();
|
|
28529
|
+
function ruleRegex(rule) {
|
|
28530
|
+
if (!rule.match) return null;
|
|
28531
|
+
const key = `${rule.match} ${rule.flags ?? ""}`;
|
|
28532
|
+
const cached = regexCache.get(key);
|
|
28533
|
+
if (cached) return cached;
|
|
28534
|
+
try {
|
|
28535
|
+
const re = new RegExp(rule.match, rule.flags ?? "");
|
|
28536
|
+
regexCache.set(key, re);
|
|
28537
|
+
return re;
|
|
28538
|
+
} catch {
|
|
28539
|
+
return null;
|
|
28540
|
+
}
|
|
28541
|
+
}
|
|
28542
|
+
function toolMatches(rule, toolName) {
|
|
28543
|
+
if (!rule.tool) return toolName === "Bash";
|
|
28544
|
+
try {
|
|
28545
|
+
return new RegExp(rule.tool).test(toolName);
|
|
28546
|
+
} catch {
|
|
28547
|
+
return false;
|
|
28548
|
+
}
|
|
28549
|
+
}
|
|
28550
|
+
/** First block hit, else first confirm hit, else first warn hit; null when none. */
|
|
28551
|
+
function evaluateRules(rules, toolName, content) {
|
|
28552
|
+
const unquoted = content.replace(/["']/g, "");
|
|
28553
|
+
const bare = dropQuoted(content);
|
|
28554
|
+
const segments = splitSegments(content);
|
|
28555
|
+
for (const severity of [
|
|
28556
|
+
"block",
|
|
28557
|
+
"confirm",
|
|
28558
|
+
"warn"
|
|
28559
|
+
]) for (const rule of rules) {
|
|
28560
|
+
if (rule.severity !== severity || !toolMatches(rule, toolName)) continue;
|
|
28561
|
+
const re = ruleRegex(rule);
|
|
28562
|
+
if (!re) continue;
|
|
28563
|
+
if (severity === "confirm" ? segments.some((seg) => gitPatternHit(re, seg)) : re.test(bare) || rule.quoted === true && (re.test(content) || re.test(unquoted))) return { rule };
|
|
28564
|
+
}
|
|
28565
|
+
return null;
|
|
28566
|
+
}
|
|
28567
|
+
|
|
28568
|
+
//#endregion
|
|
28569
|
+
//#region src/hooks/ts/lib/rules/lint.ts
|
|
28570
|
+
/**
|
|
28571
|
+
* lint.ts — run each rule's own fixtures.
|
|
28572
|
+
*
|
|
28573
|
+
* Every `tests.match` must hit the rule, every `tests.no_match` must not.
|
|
28574
|
+
* A single-step rule's fixtures are plain strings tested as Bash commands; a
|
|
28575
|
+
* sequence rule's are lists of step contents, checked step by step (the
|
|
28576
|
+
* window and ordering are the scan's business, not the fixtures'). A rule
|
|
28577
|
+
* with no fixtures is ok (nothing to check).
|
|
28578
|
+
*/
|
|
28579
|
+
function stepRegex(step) {
|
|
28580
|
+
try {
|
|
28581
|
+
return new RegExp(step.match, step.flags ?? "");
|
|
28582
|
+
} catch {
|
|
28583
|
+
return null;
|
|
28584
|
+
}
|
|
28585
|
+
}
|
|
28586
|
+
/**
|
|
28587
|
+
* A fixture sequence hits when every step's regex matches its content and its
|
|
28588
|
+
* tool pin, when set. A plain string is a Bash-shaped step; an object names
|
|
28589
|
+
* the tool, so a rule whose step is pinned to a fetched result (WebFetch,
|
|
28590
|
+
* Read) can still carry a fixture that hits. A step whose source
|
|
28591
|
+
* backreferences an earlier step's group (`\1`) is checked with that group's
|
|
28592
|
+
* capture substituted in — the same resolution the transcript scan applies —
|
|
28593
|
+
* so a fixture like download-then-execute's "same file twice" checks the way
|
|
28594
|
+
* the scan does.
|
|
28595
|
+
*/
|
|
28596
|
+
function sequenceHit(rule, steps) {
|
|
28597
|
+
const seqSteps = rule.sequence?.steps ?? [];
|
|
28598
|
+
if (steps.length !== seqSteps.length) return false;
|
|
28599
|
+
const prior = [];
|
|
28600
|
+
for (let i = 0; i < seqSteps.length; i++) {
|
|
28601
|
+
const step = seqSteps[i];
|
|
28602
|
+
const { tool, content } = steps[i];
|
|
28603
|
+
if (step.tool && !new RegExp(step.tool).test(tool ?? "Bash")) return false;
|
|
28604
|
+
if (step.match.includes("\\1") && prior.length) {
|
|
28605
|
+
const cap = prior[0][1];
|
|
28606
|
+
if (!cap || !new RegExp(step.match.replace(/\\1/g, cap), step.flags ?? "").test(content)) return false;
|
|
28607
|
+
continue;
|
|
28608
|
+
}
|
|
28609
|
+
const re = stepRegex(step);
|
|
28610
|
+
if (!re) return false;
|
|
28611
|
+
const m = content.match(re);
|
|
28612
|
+
if (!m) return false;
|
|
28613
|
+
prior.push(m);
|
|
28614
|
+
}
|
|
28615
|
+
return true;
|
|
28616
|
+
}
|
|
28617
|
+
function asFixtureSteps(t) {
|
|
28618
|
+
if (!Array.isArray(t)) return null;
|
|
28619
|
+
return t.map((s) => typeof s === "string" ? { content: s } : {
|
|
28620
|
+
tool: s.tool,
|
|
28621
|
+
content: s.content
|
|
28622
|
+
});
|
|
28623
|
+
}
|
|
28624
|
+
function lintRules(rules) {
|
|
28625
|
+
return rules.map((rule) => {
|
|
28626
|
+
const failures = [];
|
|
28627
|
+
const show = (t) => Array.isArray(t) ? t.map((s) => typeof s === "string" ? s : `${s.tool ?? "Bash"}: ${s.content}`).join(" | ") : t;
|
|
28628
|
+
if (rule.sequence) {
|
|
28629
|
+
for (const t of rule.tests?.match ?? []) {
|
|
28630
|
+
const steps = asFixtureSteps(t);
|
|
28631
|
+
if (!steps || !sequenceHit(rule, steps)) failures.push(`tests.match did not hit: ${show(t)}`);
|
|
28632
|
+
}
|
|
28633
|
+
for (const t of rule.tests?.no_match ?? []) {
|
|
28634
|
+
const steps = asFixtureSteps(t);
|
|
28635
|
+
if (steps && sequenceHit(rule, steps)) failures.push(`tests.no_match hit: ${show(t)}`);
|
|
28636
|
+
}
|
|
28637
|
+
return {
|
|
28638
|
+
id: rule.id,
|
|
28639
|
+
ok: failures.length === 0,
|
|
28640
|
+
failures
|
|
28641
|
+
};
|
|
28642
|
+
}
|
|
28643
|
+
for (const text of rule.tests?.match ?? []) {
|
|
28644
|
+
const hit = evaluateRules([rule], "Bash", text);
|
|
28645
|
+
if (!hit || hit.rule.id !== rule.id) failures.push(`tests.match did not hit: ${text}`);
|
|
28646
|
+
}
|
|
28647
|
+
for (const text of rule.tests?.no_match ?? []) {
|
|
28648
|
+
const hit = evaluateRules([rule], "Bash", text);
|
|
28649
|
+
if (hit && hit.rule.id === rule.id) failures.push(`tests.no_match hit: ${text}`);
|
|
28650
|
+
}
|
|
28651
|
+
return {
|
|
28652
|
+
id: rule.id,
|
|
28653
|
+
ok: failures.length === 0,
|
|
28654
|
+
failures
|
|
28655
|
+
};
|
|
28656
|
+
});
|
|
28657
|
+
}
|
|
28658
|
+
|
|
28659
|
+
//#endregion
|
|
28660
|
+
//#region src/cli/commands/rules.ts
|
|
28661
|
+
function listCmd(json) {
|
|
28662
|
+
const { rules, errors } = listRules(userRuleDirs(process.cwd()));
|
|
28663
|
+
if (json) {
|
|
28664
|
+
console.log(JSON.stringify({
|
|
28665
|
+
rules,
|
|
28666
|
+
errors
|
|
28667
|
+
}, null, 2));
|
|
28668
|
+
return 0;
|
|
28669
|
+
}
|
|
28670
|
+
console.log(renderTable$1([
|
|
28671
|
+
"id",
|
|
28672
|
+
"severity",
|
|
28673
|
+
"source",
|
|
28674
|
+
"enabled"
|
|
28675
|
+
], rules.map((r) => [
|
|
28676
|
+
r.id,
|
|
28677
|
+
r.severity,
|
|
28678
|
+
r.source,
|
|
28679
|
+
r.severity === "off" ? "off" : "on"
|
|
28680
|
+
])));
|
|
28681
|
+
for (const e of errors) console.log(err(`load error: ${e}`));
|
|
28682
|
+
return 0;
|
|
28683
|
+
}
|
|
28684
|
+
function lintCmd(json) {
|
|
28685
|
+
const { rules, errors } = listRules(userRuleDirs(process.cwd()));
|
|
28686
|
+
const enabled = rules.filter((r) => r.severity !== "off");
|
|
28687
|
+
const lints = lintRules(enabled);
|
|
28688
|
+
const failed = lints.filter((l) => !l.ok);
|
|
28689
|
+
if (json) {
|
|
28690
|
+
console.log(JSON.stringify({
|
|
28691
|
+
lints,
|
|
28692
|
+
errors
|
|
28693
|
+
}, null, 2));
|
|
28694
|
+
return failed.length || errors.length ? 1 : 0;
|
|
28695
|
+
}
|
|
28696
|
+
for (const l of lints) {
|
|
28697
|
+
if (l.ok) continue;
|
|
28698
|
+
console.log(err(`✖ ${l.id}`));
|
|
28699
|
+
for (const f of l.failures) console.log(` ${f}`);
|
|
28700
|
+
}
|
|
28701
|
+
for (const e of errors) console.log(err(`load error: ${e}`));
|
|
28702
|
+
if (!failed.length && !errors.length) {
|
|
28703
|
+
console.log(ok$1(`✔ ${enabled.length} rules, fixtures pass`));
|
|
28704
|
+
return 0;
|
|
28705
|
+
}
|
|
28706
|
+
return 1;
|
|
28707
|
+
}
|
|
28708
|
+
const rulesCommand = {
|
|
28709
|
+
name: "rules",
|
|
28710
|
+
summary: "Security rules: list them, lint their fixtures",
|
|
28711
|
+
description: "The PreToolUse security rules. Built-ins ship with the package; user rules live in PAI_HOME/rules or .pai/rules as *.rule.yaml files — a user rule with a built-in's id replaces it, severity: off disables it.",
|
|
28712
|
+
examples: ["pai rules list", "pai rules lint"],
|
|
28713
|
+
seeAlso: ["pai audit tokens hooks"],
|
|
28714
|
+
subcommands: [{
|
|
28715
|
+
name: "list",
|
|
28716
|
+
summary: "Every rule with severity, source and enabled state",
|
|
28717
|
+
description: "One row per rule id; a disabled (severity: off) rule shows as off.",
|
|
28718
|
+
options: [{
|
|
28719
|
+
name: "json",
|
|
28720
|
+
type: "boolean",
|
|
28721
|
+
default: false,
|
|
28722
|
+
summary: "Machine-readable JSON output"
|
|
28723
|
+
}],
|
|
28724
|
+
examples: ["pai rules list", "pai rules list --json"],
|
|
28725
|
+
run: (ctx) => listCmd(ctx.opts.json === true)
|
|
28726
|
+
}, {
|
|
28727
|
+
name: "lint",
|
|
28728
|
+
summary: "Run every rule's fixtures; exit 1 on a failure or load error",
|
|
28729
|
+
description: "Each rule's tests.match strings must hit and tests.no_match strings must not. Load errors count as failures.",
|
|
28730
|
+
options: [{
|
|
28731
|
+
name: "json",
|
|
28732
|
+
type: "boolean",
|
|
28733
|
+
default: false,
|
|
28734
|
+
summary: "Machine-readable JSON output"
|
|
28735
|
+
}],
|
|
28736
|
+
examples: ["pai rules lint", "pai rules lint --json"],
|
|
28737
|
+
run: (ctx) => lintCmd(ctx.opts.json === true)
|
|
28738
|
+
}]
|
|
28739
|
+
};
|
|
28740
|
+
|
|
28741
|
+
//#endregion
|
|
28742
|
+
//#region src/hooks/ts/lib/transcript-text.ts
|
|
28743
|
+
/**
|
|
28744
|
+
* transcript-text.ts — small, pure pieces of transcript parsing that are
|
|
28745
|
+
* worth testing in isolation.
|
|
28746
|
+
*
|
|
28747
|
+
* Split out of context-compression-hook.ts (a hook entrypoint that calls
|
|
28748
|
+
* `main()` and `process.exit()` at import time, which makes it unsafe to
|
|
28749
|
+
* import directly from a test) so the two bug fixes that live here can be
|
|
28750
|
+
* covered by a real regression test rather than only a manual before/after
|
|
28751
|
+
* digest.
|
|
28752
|
+
*/
|
|
28753
|
+
/**
|
|
28754
|
+
* Turn Claude content (string or content block array) into plain text.
|
|
28755
|
+
*
|
|
28756
|
+
* BUG (found in ~1 in 4 injected digests on this machine): a "user" turn is
|
|
28757
|
+
* not always something the user typed — a tool_result is also delivered as a
|
|
28758
|
+
* user-role message, and its `content` block looks like
|
|
28759
|
+
* `{ type: "tool_result", content: [...] }` where the inner `content` is
|
|
28760
|
+
* itself an array of blocks, not a string. The old code fell back to
|
|
28761
|
+
* `String(c.content)` for anything without a `.text` field, and `String()`
|
|
28762
|
+
* on an array of objects produces literal `"[object Object],[object
|
|
28763
|
+
* Object]"` — which is exactly what showed up under "Recent user requests".
|
|
28764
|
+
* Only ever take the `.text` of a `type: "text"` block; everything else
|
|
28765
|
+
* (tool_use, tool_result, image, and any nested content) is ignored rather
|
|
28766
|
+
* than stringified.
|
|
28767
|
+
*/
|
|
28768
|
+
function contentToText(content) {
|
|
28769
|
+
if (typeof content === "string") return content;
|
|
28770
|
+
if (Array.isArray(content)) return content.map((c) => {
|
|
28771
|
+
if (typeof c === "string") return c;
|
|
28772
|
+
if (c && typeof c === "object" && c.type === "text" && typeof c.text === "string") return c.text;
|
|
28773
|
+
return "";
|
|
28774
|
+
}).join(" ").trim();
|
|
28775
|
+
return "";
|
|
28776
|
+
}
|
|
28777
|
+
|
|
28778
|
+
//#endregion
|
|
28779
|
+
//#region src/session/handoff.ts
|
|
28780
|
+
/**
|
|
28781
|
+
* handoff.ts — a portable, bounded, redacted handoff brief for one session.
|
|
28782
|
+
*
|
|
28783
|
+
* `pai handoff export` scrapes a Claude Code transcript into a short Markdown
|
|
28784
|
+
* brief a successor session can continue from: what was asked, what changed,
|
|
28785
|
+
* what ran, and where it stopped. Unlike the model-written context handover
|
|
28786
|
+
* this is mechanical — no model call, no prose to hallucinate — and it is
|
|
28787
|
+
* redacted (secrets replaced by [REDACTED]) before it is bounded, so a brief
|
|
28788
|
+
* can be pasted, mailed or committed without leaking a token.
|
|
28789
|
+
*
|
|
28790
|
+
* The brief is assembled as sections, masked, then cut to `maxBytes` from the
|
|
28791
|
+
* oldest end (Recent commands first, then Last steps) with a marker line, so
|
|
28792
|
+
* the parts a successor needs most (Session, requests, files) survive first.
|
|
28793
|
+
*/
|
|
28794
|
+
/** Default brief size: comfortably under what a successor can read in one turn. */
|
|
28795
|
+
const HANDOFF_MAX_BYTES = 49152;
|
|
28796
|
+
/** Most distinct files listed under "Files changed". */
|
|
28797
|
+
const MAX_FILES = 50;
|
|
28798
|
+
/** Most Bash commands listed under "Recent commands". */
|
|
28799
|
+
const MAX_COMMANDS = 20;
|
|
28800
|
+
/** Most assistant text messages listed under "Last steps". */
|
|
28801
|
+
const MAX_STEPS = 10;
|
|
28802
|
+
/** Each "Last steps" entry is truncated to this many characters. */
|
|
28803
|
+
const STEP_CHARS = 400;
|
|
28804
|
+
/** Tools whose input carries a changed file. */
|
|
28805
|
+
const FILE_TOOLS = new Set([
|
|
28806
|
+
"Edit",
|
|
28807
|
+
"Write",
|
|
28808
|
+
"MultiEdit"
|
|
28809
|
+
]);
|
|
28810
|
+
/** The last assistant text message of a transcript, or null when there is none. */
|
|
28811
|
+
function lastAssistantText(entries) {
|
|
28812
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
28813
|
+
const entry = entries[i];
|
|
28814
|
+
if (entry.type !== "assistant" || !Array.isArray(entry.message?.content)) continue;
|
|
28815
|
+
const text = entry.message.content.filter((p) => p?.type === "text" && typeof p.text === "string").map((p) => p.text).join("\n").trim();
|
|
28816
|
+
if (text) return text;
|
|
28817
|
+
}
|
|
28818
|
+
return null;
|
|
28819
|
+
}
|
|
28820
|
+
/** First user text message that looks like something typed, not an injected block. */
|
|
28821
|
+
function firstUserText(entries) {
|
|
28822
|
+
for (const entry of entries) {
|
|
28823
|
+
if (entry.type !== "user") continue;
|
|
28824
|
+
const content = entry.message?.content;
|
|
28825
|
+
const text = typeof content === "string" ? content : contentToText(content);
|
|
28826
|
+
if (text.trim()) return text.trim();
|
|
28827
|
+
}
|
|
28828
|
+
return null;
|
|
28829
|
+
}
|
|
28830
|
+
/** Last user text message — the request the session was working on. */
|
|
28831
|
+
function lastUserText(entries) {
|
|
28832
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
28833
|
+
const entry = entries[i];
|
|
28834
|
+
if (entry.type !== "user") continue;
|
|
28835
|
+
const content = entry.message?.content;
|
|
28836
|
+
const text = typeof content === "string" ? content : contentToText(content);
|
|
28837
|
+
if (text.trim()) return text.trim();
|
|
28838
|
+
}
|
|
28839
|
+
return null;
|
|
28840
|
+
}
|
|
28841
|
+
/**
|
|
28842
|
+
* Exit code of a Bash tool_result, when the transcript records one.
|
|
28843
|
+
* Claude Code serialises Bash results as plain text; an interrupted command
|
|
28844
|
+
* reads "Exit code N", and a JSON payload carries `interrupted`/`success`.
|
|
28845
|
+
*/
|
|
28846
|
+
function bashExitCode(result) {
|
|
28847
|
+
if (result.is_error === true) return null;
|
|
28848
|
+
const text = contentToText(result.content);
|
|
28849
|
+
const m = text.match(/Exit code (\d+)/);
|
|
28850
|
+
if (m) return Number(m[1]);
|
|
28851
|
+
try {
|
|
28852
|
+
const parsed = JSON.parse(text);
|
|
28853
|
+
if (parsed.interrupted === true) return null;
|
|
28854
|
+
if (parsed.success === false) return null;
|
|
28855
|
+
} catch {}
|
|
28856
|
+
return 0;
|
|
28857
|
+
}
|
|
28858
|
+
/**
|
|
28859
|
+
* Build the handoff brief for one transcript: Markdown, secrets masked,
|
|
28860
|
+
* cut to `maxBytes` whole sections from the oldest end with a marker line.
|
|
28861
|
+
*
|
|
28862
|
+
* @param transcriptPath a Claude Code session transcript (.jsonl)
|
|
28863
|
+
*/
|
|
28864
|
+
function buildHandoffBrief(transcriptPath, opts = {}) {
|
|
28865
|
+
const maxBytes = opts.maxBytes ?? HANDOFF_MAX_BYTES;
|
|
28866
|
+
const raw = readFileSync(transcriptPath, "utf8");
|
|
28867
|
+
const entries = [];
|
|
28868
|
+
for (const line of raw.split("\n")) {
|
|
28869
|
+
if (!line.trim()) continue;
|
|
28870
|
+
try {
|
|
28871
|
+
entries.push(JSON.parse(line));
|
|
28872
|
+
} catch {}
|
|
28873
|
+
}
|
|
28874
|
+
const sessionId = basename(transcriptPath).replace(/\.jsonl$/, "") || "unknown";
|
|
28875
|
+
const cwd = entries.find((e) => e.cwd)?.cwd ?? "";
|
|
28876
|
+
const first = entries.find((e) => e.timestamp)?.timestamp ?? "";
|
|
28877
|
+
const last = [...entries].reverse().find((e) => e.timestamp)?.timestamp ?? "";
|
|
28878
|
+
const files = [];
|
|
28879
|
+
const seen = /* @__PURE__ */ new Set();
|
|
28880
|
+
const commands = [];
|
|
28881
|
+
const steps = [];
|
|
28882
|
+
const results = /* @__PURE__ */ new Map();
|
|
28883
|
+
for (const entry of entries) {
|
|
28884
|
+
const content = entry.message?.content;
|
|
28885
|
+
if (!Array.isArray(content)) continue;
|
|
28886
|
+
for (const part of content) if (part?.type === "tool_result" && typeof part.tool_use_id === "string") results.set(part.tool_use_id, part);
|
|
28887
|
+
}
|
|
28888
|
+
for (const entry of entries) {
|
|
28889
|
+
const content = entry.message?.content;
|
|
28890
|
+
if (!Array.isArray(content)) continue;
|
|
28891
|
+
for (const part of content) if (part?.type === "tool_use" && part.name && FILE_TOOLS.has(part.name)) {
|
|
28892
|
+
const path = part.input?.file_path;
|
|
28893
|
+
if (typeof path === "string" && !seen.has(path)) {
|
|
28894
|
+
seen.add(path);
|
|
28895
|
+
files.push(path);
|
|
28896
|
+
}
|
|
28897
|
+
} else if (part?.type === "tool_use" && part.name === "Bash") {
|
|
28898
|
+
const command = typeof part.input?.command === "string" ? part.input.command : "";
|
|
28899
|
+
if (command) {
|
|
28900
|
+
const code = part.id ? results.get(part.id) : void 0;
|
|
28901
|
+
const exit = code ? bashExitCode(code) : null;
|
|
28902
|
+
commands.push(exit === null ? command : `${command} [exit ${exit}]`);
|
|
28903
|
+
}
|
|
28904
|
+
} else if (part?.type === "text" && typeof part.text === "string" && entry.type === "assistant") {
|
|
28905
|
+
const text = part.text.trim();
|
|
28906
|
+
if (text) steps.push(text.length > STEP_CHARS ? `${text.slice(0, STEP_CHARS)}…` : text);
|
|
28907
|
+
}
|
|
28908
|
+
}
|
|
28909
|
+
const sections = {
|
|
28910
|
+
Session: [
|
|
28911
|
+
`- id: ${sessionId}`,
|
|
28912
|
+
`- cwd: ${cwd}`,
|
|
28913
|
+
`- start: ${first}`,
|
|
28914
|
+
`- end: ${last}`
|
|
28915
|
+
].join("\n"),
|
|
28916
|
+
"First request": firstUserText(entries) ?? "(none)",
|
|
28917
|
+
"Latest request": lastUserText(entries) ?? "(none)",
|
|
28918
|
+
"Files changed": files.slice(0, MAX_FILES).map((f) => `- ${f}`).join("\n") || "(none)",
|
|
28919
|
+
"Recent commands": commands.slice(-MAX_COMMANDS).map((c) => `- ${c}`).join("\n") || "(none)",
|
|
28920
|
+
"Last steps": steps.slice(-MAX_STEPS).map((s) => `- ${s}`).join("\n") || "(none)",
|
|
28921
|
+
"Where it stopped": lastAssistantText(entries) ?? "(no assistant message)"
|
|
28922
|
+
};
|
|
28923
|
+
const masked = maskSecrets(Object.entries(sections).map(([title, body]) => `## ${title}\n${body}`).join("\n\n"));
|
|
28924
|
+
if (Buffer.byteLength(masked, "utf8") <= maxBytes) return masked;
|
|
28925
|
+
const note = `\n\n[truncated to ${maxBytes} bytes]`;
|
|
28926
|
+
const budget = maxBytes - Buffer.byteLength(note, "utf8");
|
|
28927
|
+
const render = (parts) => Object.entries(parts).map(([title, body]) => `## ${title}\n${body}`).join("\n\n");
|
|
28928
|
+
const order = ["Recent commands", "Last steps"];
|
|
28929
|
+
const kept = { ...sections };
|
|
28930
|
+
for (const name of order) {
|
|
28931
|
+
delete kept[name];
|
|
28932
|
+
const text = render(kept);
|
|
28933
|
+
if (Buffer.byteLength(text, "utf8") <= budget) return text + note;
|
|
28934
|
+
}
|
|
28935
|
+
kept["Where it stopped"] = (kept["Where it stopped"] ?? "").split("\n")[0].slice(0, 200);
|
|
28936
|
+
const text = render(kept);
|
|
28937
|
+
if (Buffer.byteLength(text, "utf8") <= budget) return text + note;
|
|
28938
|
+
return Buffer.from(text, "utf8").subarray(0, Math.max(0, budget)).toString("utf8") + note;
|
|
28939
|
+
}
|
|
28940
|
+
/** `yyyymmdd-hhmm` in local time, for the handoff file name. */
|
|
28941
|
+
function stamp(d = /* @__PURE__ */ new Date()) {
|
|
28942
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
28943
|
+
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}`;
|
|
28944
|
+
}
|
|
28945
|
+
/** The handoffs directory under PAI_HOME. */
|
|
28946
|
+
function handoffsDir() {
|
|
28947
|
+
return paiHomePath("handoffs");
|
|
28948
|
+
}
|
|
28949
|
+
/**
|
|
28950
|
+
* Write a handoff brief into <PAI_HOME>/handoffs/<sessionId>-<yyyymmdd-hhmm>.md
|
|
28951
|
+
* (dir 0700, file 0600) and return its path.
|
|
28952
|
+
*/
|
|
28953
|
+
function writeHandoff(brief, sessionId) {
|
|
28954
|
+
const dir = handoffsDir();
|
|
28955
|
+
mkdirSync(dir, {
|
|
28956
|
+
recursive: true,
|
|
28957
|
+
mode: 448
|
|
28958
|
+
});
|
|
28959
|
+
const path = join(dir, `${sessionId}-${stamp()}.md`);
|
|
28960
|
+
writeFileSync(path, brief, { mode: 384 });
|
|
28961
|
+
return path;
|
|
28962
|
+
}
|
|
28963
|
+
/** Newest transcript of a cwd, newest first; null when the project has none. */
|
|
28964
|
+
function sessionTranscripts(cwd) {
|
|
28965
|
+
const dir = join(claudeProjectsDir(), encodeDir(cwd));
|
|
28966
|
+
let names;
|
|
28967
|
+
try {
|
|
28968
|
+
names = readdirSync(dir);
|
|
28969
|
+
} catch {
|
|
28970
|
+
return [];
|
|
28971
|
+
}
|
|
28972
|
+
return names.filter((n) => n.endsWith(".jsonl")).map((n) => join(dir, n)).map((path) => ({
|
|
28973
|
+
path,
|
|
28974
|
+
mtime: safeMtime(path)
|
|
28975
|
+
})).filter((f) => f.mtime !== null).sort((a, b) => b.mtime - a.mtime).map((f) => f.path);
|
|
28976
|
+
}
|
|
28977
|
+
function safeMtime(path) {
|
|
28978
|
+
try {
|
|
28979
|
+
return statSync(path).mtimeMs;
|
|
28980
|
+
} catch {
|
|
28981
|
+
return null;
|
|
28982
|
+
}
|
|
28983
|
+
}
|
|
28984
|
+
/** Resolve a session id (or transcript path) in a cwd to its transcript path. */
|
|
28985
|
+
function resolveTranscript(session, cwd) {
|
|
28986
|
+
if (session && session.endsWith(".jsonl")) return session;
|
|
28987
|
+
const dir = join(claudeProjectsDir(), encodeDir(cwd));
|
|
28988
|
+
if (session) {
|
|
28989
|
+
const direct = join(dir, `${session}.jsonl`);
|
|
28990
|
+
try {
|
|
28991
|
+
statSync(direct);
|
|
28992
|
+
return direct;
|
|
28993
|
+
} catch {
|
|
28994
|
+
const archived = join(dir, "sessions", `${session}.jsonl`);
|
|
28995
|
+
try {
|
|
28996
|
+
statSync(archived);
|
|
28997
|
+
return archived;
|
|
28998
|
+
} catch {
|
|
28999
|
+
return null;
|
|
29000
|
+
}
|
|
29001
|
+
}
|
|
29002
|
+
}
|
|
29003
|
+
return sessionTranscripts(cwd)[0] ?? null;
|
|
29004
|
+
}
|
|
29005
|
+
/** The session id a transcript path carries (its file name). */
|
|
29006
|
+
function sessionIdOf(transcriptPath) {
|
|
29007
|
+
return basename(transcriptPath).replace(/\.jsonl$/, "");
|
|
29008
|
+
}
|
|
29009
|
+
|
|
29010
|
+
//#endregion
|
|
29011
|
+
//#region src/cli/commands/handoff.ts
|
|
29012
|
+
/** `pai handoff ...`: export a session's work as a portable, redacted brief. */
|
|
29013
|
+
function exportCmd(ctx) {
|
|
29014
|
+
const cwd = process.cwd();
|
|
29015
|
+
const session = ctx.args.SESSION;
|
|
29016
|
+
const transcript = resolveTranscript(session, cwd);
|
|
29017
|
+
if (!transcript) throw new CliError(session ? `no transcript for session ${session} in ${cwd}` : `no session transcripts in ${cwd}`, "pai handoff export <SESSION> — or run it inside the project the session worked in");
|
|
29018
|
+
const sessionId = sessionIdOf(transcript);
|
|
29019
|
+
const path = writeHandoff(buildHandoffBrief(transcript), sessionId);
|
|
29020
|
+
console.log(path);
|
|
29021
|
+
console.log(`Continue with: claude "Read ${path} and continue. [pai-handoff from=${sessionId}]"`);
|
|
29022
|
+
return 0;
|
|
29023
|
+
}
|
|
29024
|
+
function showCmd(ctx) {
|
|
29025
|
+
const path = ctx.args.PATH;
|
|
29026
|
+
try {
|
|
29027
|
+
process.stdout.write(readFileSync(path, "utf8"));
|
|
29028
|
+
} catch {
|
|
29029
|
+
throw new CliError(`cannot read ${path}`, "pai handoff export first, then pai handoff show <PATH>");
|
|
29030
|
+
}
|
|
29031
|
+
return 0;
|
|
29032
|
+
}
|
|
29033
|
+
const handoffCommand = {
|
|
29034
|
+
name: "handoff",
|
|
29035
|
+
summary: "Carry a session's work to a new one",
|
|
29036
|
+
description: "Exports a session transcript as a short, redacted Markdown brief (requests, files changed, recent commands, where it stopped) and prints the line to continue with. The brief never goes on the command line — only its path does. A `[pai-handoff from=<id>]` marker in the next session's first prompt is recorded in the handoff link log.",
|
|
29037
|
+
examples: [
|
|
29038
|
+
"pai handoff export",
|
|
29039
|
+
"pai handoff export 2026-10-22-session-id",
|
|
29040
|
+
"pai handoff show ~/.pai/handoffs/abc-20261022-1430.md"
|
|
29041
|
+
],
|
|
29042
|
+
seeAlso: ["pai audit tokens session"],
|
|
29043
|
+
subcommands: [{
|
|
29044
|
+
name: "export",
|
|
29045
|
+
summary: "Write the brief for a session (default: newest here)",
|
|
29046
|
+
description: "Prints the brief's path and the one-line continue hint. Secrets in the transcript are replaced with [REDACTED]; the brief is bounded to 48 KB.",
|
|
29047
|
+
args: [{
|
|
29048
|
+
name: "SESSION",
|
|
29049
|
+
summary: "Session id or transcript path (default: newest transcript of the current directory)"
|
|
29050
|
+
}],
|
|
29051
|
+
examples: ["pai handoff export", "pai handoff export 2026-10-22-session-id"],
|
|
29052
|
+
run: exportCmd
|
|
29053
|
+
}, {
|
|
29054
|
+
name: "show",
|
|
29055
|
+
summary: "Print a handoff brief",
|
|
29056
|
+
description: "Prints a brief written by `pai handoff export`.",
|
|
29057
|
+
args: [{
|
|
29058
|
+
name: "PATH",
|
|
29059
|
+
required: true,
|
|
29060
|
+
summary: "Brief file"
|
|
29061
|
+
}],
|
|
29062
|
+
examples: ["pai handoff show ~/.pai/handoffs/abc-20261022-1430.md"],
|
|
29063
|
+
run: showCmd
|
|
27246
29064
|
}]
|
|
27247
29065
|
};
|
|
27248
29066
|
|
|
@@ -27306,7 +29124,8 @@ const GROUPS = [
|
|
|
27306
29124
|
endTop,
|
|
27307
29125
|
resumeTop,
|
|
27308
29126
|
clearNamesCmd,
|
|
27309
|
-
sessionsCommand
|
|
29127
|
+
sessionsCommand,
|
|
29128
|
+
handoffCommand
|
|
27310
29129
|
]
|
|
27311
29130
|
},
|
|
27312
29131
|
{
|
|
@@ -27344,9 +29163,10 @@ const GROUPS = [
|
|
|
27344
29163
|
]
|
|
27345
29164
|
}))
|
|
27346
29165
|
];
|
|
27347
|
-
GROUPS.find((g) => g.id === "memory").commands.push(memoryCommand, kgCommand, zettelCommand, observationCommand, obsidianCommand, skillCommand);
|
|
29166
|
+
GROUPS.find((g) => g.id === "memory").commands.push(memoryCommand, kgCommand, zettelCommand, observationCommand, obsidianCommand, skillCommand, lessonsCommand);
|
|
27348
29167
|
GROUPS.find((g) => g.id === "workers").commands.push(modelsCommand);
|
|
27349
|
-
GROUPS.find((g) => g.id === "daemon").commands.push(auditCommand, reloadCommand);
|
|
29168
|
+
GROUPS.find((g) => g.id === "daemon").commands.push(auditCommand, harnessCommand, reloadCommand, rulesCommand);
|
|
29169
|
+
GROUPS.find((g) => g.id === "memory").commands.push(gitCommand);
|
|
27350
29170
|
|
|
27351
29171
|
//#endregion
|
|
27352
29172
|
//#region src/cli/index.ts
|