@winspan/claude-forge 8.50.6 → 8.51.1
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.md +7 -7
- package/dist/claudemd/claudemd-generator.d.ts.map +1 -1
- package/dist/claudemd/claudemd-generator.js +27 -237
- package/dist/claudemd/claudemd-generator.js.map +1 -1
- package/dist/claudemd/resume-manager.js +1 -1
- package/dist/claudemd/resume-manager.js.map +1 -1
- package/dist/claudemd/templates/swarm-protocol.md +222 -0
- package/dist/cli/commands/daemon.js +6 -6
- package/dist/cli/commands/daemon.js.map +1 -1
- package/dist/cli/commands/executions.d.ts.map +1 -1
- package/dist/cli/commands/executions.js +4 -3
- package/dist/cli/commands/executions.js.map +1 -1
- package/dist/cli/commands/init.js +2 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/logs.js.map +1 -1
- package/dist/cli/commands/mcp.d.ts.map +1 -1
- package/dist/cli/commands/mcp.js +3 -5
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/menu.d.ts.map +1 -1
- package/dist/cli/commands/menu.js +4 -3
- package/dist/cli/commands/menu.js.map +1 -1
- package/dist/cli/commands/stats.d.ts.map +1 -1
- package/dist/cli/commands/stats.js +2 -3
- package/dist/cli/commands/stats.js.map +1 -1
- package/dist/cli/commands/status.js +2 -2
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/trace.d.ts.map +1 -1
- package/dist/cli/commands/trace.js +11 -23
- package/dist/cli/commands/trace.js.map +1 -1
- package/dist/cli/init/hook-manager.d.ts.map +1 -1
- package/dist/cli/init/hook-manager.js +2 -2
- package/dist/cli/init/hook-manager.js.map +1 -1
- package/dist/core/ai/provider.js +2 -2
- package/dist/core/ai/provider.js.map +1 -1
- package/dist/core/constants.d.ts +12 -1
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/constants.js +15 -1
- package/dist/core/constants.js.map +1 -1
- package/dist/core/event-fields.d.ts +16 -0
- package/dist/core/event-fields.d.ts.map +1 -0
- package/dist/core/event-fields.js +19 -0
- package/dist/core/event-fields.js.map +1 -0
- package/dist/core/queue/index.d.ts.map +1 -1
- package/dist/core/queue/index.js +3 -4
- package/dist/core/queue/index.js.map +1 -1
- package/dist/core/storage/base.d.ts +36 -3
- package/dist/core/storage/base.d.ts.map +1 -1
- package/dist/core/storage/base.js +101 -58
- package/dist/core/storage/base.js.map +1 -1
- package/dist/core/storage/events.d.ts +92 -3
- package/dist/core/storage/events.d.ts.map +1 -1
- package/dist/core/storage/events.js +147 -0
- package/dist/core/storage/events.js.map +1 -1
- package/dist/core/storage/routing.d.ts +54 -1
- package/dist/core/storage/routing.d.ts.map +1 -1
- package/dist/core/storage/routing.js +99 -1
- package/dist/core/storage/routing.js.map +1 -1
- package/dist/core/storage/schema.sql +12 -2
- package/dist/core/storage/sessions.d.ts +20 -0
- package/dist/core/storage/sessions.d.ts.map +1 -1
- package/dist/core/storage/sessions.js +59 -0
- package/dist/core/storage/sessions.js.map +1 -1
- package/dist/core/storage/skills.d.ts +23 -0
- package/dist/core/storage/skills.d.ts.map +1 -1
- package/dist/core/storage/skills.js +47 -0
- package/dist/core/storage/skills.js.map +1 -1
- package/dist/core/storage/sqlite.d.ts +35 -2
- package/dist/core/storage/sqlite.d.ts.map +1 -1
- package/dist/core/storage/sqlite.js +93 -4
- package/dist/core/storage/sqlite.js.map +1 -1
- package/dist/core/storage/tasks.d.ts +49 -0
- package/dist/core/storage/tasks.d.ts.map +1 -1
- package/dist/core/storage/tasks.js +143 -1
- package/dist/core/storage/tasks.js.map +1 -1
- package/dist/core/storage/token-usage.d.ts +1 -1
- package/dist/core/storage/token-usage.d.ts.map +1 -1
- package/dist/core/storage/token-usage.js +1 -1
- package/dist/core/storage/token-usage.js.map +1 -1
- package/dist/core/types.d.ts +24 -3
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/utils/error-handler.d.ts.map +1 -1
- package/dist/core/utils/error-handler.js +3 -2
- package/dist/core/utils/error-handler.js.map +1 -1
- package/dist/core/utils/git.d.ts +10 -0
- package/dist/core/utils/git.d.ts.map +1 -0
- package/dist/core/utils/git.js +24 -0
- package/dist/core/utils/git.js.map +1 -0
- package/dist/core/utils/logger.d.ts.map +1 -1
- package/dist/core/utils/logger.js +15 -1
- package/dist/core/utils/logger.js.map +1 -1
- package/dist/core/utils/lru-cache.d.ts +1 -0
- package/dist/core/utils/lru-cache.d.ts.map +1 -1
- package/dist/core/utils/lru-cache.js +3 -0
- package/dist/core/utils/lru-cache.js.map +1 -1
- package/dist/core/utils/token-tracker.js +1 -1
- package/dist/core/utils/token-tracker.js.map +1 -1
- package/dist/daemon/event-parser.d.ts.map +1 -1
- package/dist/daemon/event-parser.js +2 -1
- package/dist/daemon/event-parser.js.map +1 -1
- package/dist/daemon/handlers/history-exporter.js.map +1 -1
- package/dist/daemon/handlers/post-tool-use.d.ts.map +1 -1
- package/dist/daemon/handlers/post-tool-use.js +7 -3
- package/dist/daemon/handlers/post-tool-use.js.map +1 -1
- package/dist/daemon/handlers/stop.d.ts +4 -0
- package/dist/daemon/handlers/stop.d.ts.map +1 -1
- package/dist/daemon/handlers/stop.js +23 -35
- package/dist/daemon/handlers/stop.js.map +1 -1
- package/dist/daemon/handlers/user-prompt.d.ts +3 -3
- package/dist/daemon/handlers/user-prompt.d.ts.map +1 -1
- package/dist/daemon/handlers/user-prompt.js +12 -22
- package/dist/daemon/handlers/user-prompt.js.map +1 -1
- package/dist/daemon/hook-sync.d.ts +17 -0
- package/dist/daemon/hook-sync.d.ts.map +1 -0
- package/dist/daemon/hook-sync.js +74 -0
- package/dist/daemon/hook-sync.js.map +1 -0
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +33 -9
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/lifecycle.js +3 -4
- package/dist/daemon/lifecycle.js.map +1 -1
- package/dist/daemon/server.d.ts +6 -4
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +76 -85
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/services/task-segmenter.js +1 -1
- package/dist/daemon/services/task-segmenter.js.map +1 -1
- package/dist/hooks/hook-lib.sh +37 -0
- package/dist/hooks/notification.sh +2 -2
- package/dist/hooks/post-tool-use.sh +2 -2
- package/dist/hooks/pre-tool-use.sh +2 -2
- package/dist/hooks/stop.sh +9 -6
- package/dist/hooks/user-prompt-submit.sh +2 -2
- package/dist/{daemon/services → web/analytics}/anti-pattern-detector.d.ts +3 -4
- package/dist/web/analytics/anti-pattern-detector.d.ts.map +1 -0
- package/dist/{daemon/services → web/analytics}/anti-pattern-detector.js +7 -46
- package/dist/{daemon/services → web/analytics}/anti-pattern-detector.js.map +1 -1
- package/dist/web/analytics/drift-detector.d.ts.map +1 -0
- package/dist/{daemon/services → web/analytics}/drift-detector.js +10 -13
- package/dist/web/analytics/drift-detector.js.map +1 -0
- package/dist/web/analytics/weekly-report.d.ts.map +1 -0
- package/dist/{daemon/services → web/analytics}/weekly-report.js +51 -50
- package/dist/web/analytics/weekly-report.js.map +1 -0
- package/dist/web/auth-middleware.d.ts.map +1 -1
- package/dist/web/auth-middleware.js +1 -2
- package/dist/web/auth-middleware.js.map +1 -1
- package/dist/web/routes/_helpers.d.ts +16 -0
- package/dist/web/routes/_helpers.d.ts.map +1 -0
- package/dist/web/routes/_helpers.js +32 -0
- package/dist/web/routes/_helpers.js.map +1 -0
- package/dist/web/routes/drift.js +1 -1
- package/dist/web/routes/drift.js.map +1 -1
- package/dist/web/routes/insights.js +1 -1
- package/dist/web/routes/insights.js.map +1 -1
- package/dist/web/routes/reports.js +1 -1
- package/dist/web/routes/reports.js.map +1 -1
- package/dist/web/routes/rules.d.ts +3 -0
- package/dist/web/routes/rules.d.ts.map +1 -1
- package/dist/web/routes/rules.js +28 -52
- package/dist/web/routes/rules.js.map +1 -1
- package/dist/web/routes/sessions.d.ts.map +1 -1
- package/dist/web/routes/sessions.js +16 -30
- package/dist/web/routes/sessions.js.map +1 -1
- package/dist/web/routes/skill-stats.d.ts +2 -0
- package/dist/web/routes/skill-stats.d.ts.map +1 -1
- package/dist/web/routes/skill-stats.js +28 -64
- package/dist/web/routes/skill-stats.js.map +1 -1
- package/dist/web/routes/skills.d.ts.map +1 -1
- package/dist/web/routes/skills.js +5 -4
- package/dist/web/routes/skills.js.map +1 -1
- package/dist/web/routes/stats.d.ts +4 -0
- package/dist/web/routes/stats.d.ts.map +1 -1
- package/dist/web/routes/stats.js +19 -21
- package/dist/web/routes/stats.js.map +1 -1
- package/dist/web/routes/tasks.d.ts.map +1 -1
- package/dist/web/routes/tasks.js +17 -42
- package/dist/web/routes/tasks.js.map +1 -1
- package/dist/web/routes/trace.d.ts.map +1 -1
- package/dist/web/routes/trace.js +7 -17
- package/dist/web/routes/trace.js.map +1 -1
- package/dist/web/routes/types.d.ts.map +1 -1
- package/dist/web/routes/types.js +4 -3
- package/dist/web/routes/types.js.map +1 -1
- package/dist/web/static/assets/{AIConfig-BQCAQE9D.js → AIConfig-CdDWzJyO.js} +2 -2
- package/dist/web/static/assets/{AIConfig-BQCAQE9D.js.map → AIConfig-CdDWzJyO.js.map} +1 -1
- package/dist/web/static/assets/{Dashboard-D7Bo6Kan.js → Dashboard-CoEmmIDt.js} +2 -2
- package/dist/web/static/assets/{Dashboard-D7Bo6Kan.js.map → Dashboard-CoEmmIDt.js.map} +1 -1
- package/dist/web/static/assets/{Drawer-BeHRQxUS.js → Drawer-DdRTzlLB.js} +2 -2
- package/dist/web/static/assets/{Drawer-BeHRQxUS.js.map → Drawer-DdRTzlLB.js.map} +1 -1
- package/dist/web/static/assets/{Events-K_tCY2ti.js → Events-DrIq1SUS.js} +2 -2
- package/dist/web/static/assets/{Events-K_tCY2ti.js.map → Events-DrIq1SUS.js.map} +1 -1
- package/dist/web/static/assets/{Reports-BJCmBnc_.js → Reports-DFBM3MDK.js} +2 -2
- package/dist/web/static/assets/{Reports-BJCmBnc_.js.map → Reports-DFBM3MDK.js.map} +1 -1
- package/dist/web/static/assets/{SearchInput-BX2KhMkw.js → SearchInput-qCj_jAcf.js} +2 -2
- package/dist/web/static/assets/{SearchInput-BX2KhMkw.js.map → SearchInput-qCj_jAcf.js.map} +1 -1
- package/dist/web/static/assets/{SessionDetail-Bkr-kC7V.js → SessionDetail-CCzwdoT7.js} +2 -2
- package/dist/web/static/assets/{SessionDetail-Bkr-kC7V.js.map → SessionDetail-CCzwdoT7.js.map} +1 -1
- package/dist/web/static/assets/{Sessions-Chx9OCLH.js → Sessions-FfLYkAw9.js} +2 -2
- package/dist/web/static/assets/{Sessions-Chx9OCLH.js.map → Sessions-FfLYkAw9.js.map} +1 -1
- package/dist/web/static/assets/{Skills-O0GT1i7m.js → Skills-C8Gvs3Qa.js} +2 -2
- package/dist/web/static/assets/{Skills-O0GT1i7m.js.map → Skills-C8Gvs3Qa.js.map} +1 -1
- package/dist/web/static/assets/TaskDetail-BS8pYhaR.js +2 -0
- package/dist/web/static/assets/TaskDetail-BS8pYhaR.js.map +1 -0
- package/dist/web/static/assets/Tasks-CyuhizG8.js +2 -0
- package/dist/web/static/assets/Tasks-CyuhizG8.js.map +1 -0
- package/dist/web/static/assets/index-CBX47X8l.js +3 -0
- package/dist/web/static/assets/{index-DxIbmNmr.js.map → index-CBX47X8l.js.map} +1 -1
- package/dist/web/static/assets/index-DjIoMdoR.css +1 -0
- package/dist/web/static/assets/{lucide-fJlPI3H7.js → lucide-Bs_edTLa.js} +44 -39
- package/dist/web/static/assets/lucide-Bs_edTLa.js.map +1 -0
- package/dist/web/static/assets/react-router-r79dBVy4.js +20 -0
- package/dist/web/static/assets/{react-router-I-HqunH7.js.map → react-router-r79dBVy4.js.map} +1 -1
- package/dist/web/static/assets/task-title-BhOcemuR.js +2 -0
- package/dist/web/static/assets/task-title-BhOcemuR.js.map +1 -0
- package/dist/web/static/index.html +4 -4
- package/docs/design/h1-storage-aggregation-spec-20260518-1121.md +299 -0
- package/docs/design/h2-getdatabase-encapsulation-spec-20260518-1450.md +191 -0
- package/docs/design/h3-fallback-removal-spec-20260518-1245.md +76 -0
- package/docs/design/h4-index-dedup-spec-20260518-1230.md +109 -0
- package/docs/design/h6-services-migration-spec-20260518-1355.md +82 -0
- package/docs/design/l1-swarm-protocol-extract-spec-20260518-1605.md +106 -0
- package/docs/design/m10-forge-paths-spec-20260518-1320.md +121 -0
- package/docs/design/m2-m3-tool-input-spec-20260518-1425.md +131 -0
- package/docs/design/m7-routing-event-association-spec-20260518-1545.md +103 -0
- package/docs/design/project-path-gitroot-spec-20260518-1715.md +134 -0
- package/docs/design/task-active-gc-spec-20260518-1745.md +146 -0
- package/docs/implementation/h1-storage-aggregation-changelog-20260518-1121.md +82 -0
- package/docs/implementation/h2-final-changelog-20260518-1530.md +61 -0
- package/docs/implementation/h2-phase1-safety-net-changelog-20260518-1450.md +70 -0
- package/docs/implementation/h2-phase2-operations-changelog-20260518-1450.md +120 -0
- package/docs/implementation/h2-phase3-callsites-changelog-20260518-1450.md +71 -0
- package/docs/implementation/h3-fallback-removal-changelog-20260518-1245.md +71 -0
- package/docs/implementation/h4-index-dedup-changelog-20260518-1230.md +60 -0
- package/docs/implementation/h6-services-migration-changelog-20260518-1355.md +46 -0
- package/docs/implementation/h7-m9-defaults-changelog-20260518-1300.md +46 -0
- package/docs/implementation/l1-swarm-protocol-extract-changelog-20260518-1605.md +45 -0
- package/docs/implementation/l3-l4-daemon-perf-changelog-20260518-1410.md +63 -0
- package/docs/implementation/l6-l8-final-cleanup-changelog-20260518-1640.md +38 -0
- package/docs/implementation/m1-m4-m5-l7-cleanup-changelog-20260518-1310.md +58 -0
- package/docs/implementation/m10-forge-paths-changelog-20260518-1320.md +60 -0
- package/docs/implementation/m2-m3-tool-input-changelog-20260518-1425.md +43 -0
- package/docs/implementation/m6-m8-naming-shutdown-changelog-20260518-1340.md +56 -0
- package/docs/implementation/m7-routing-association-changelog-20260518-1545.md +69 -0
- package/docs/implementation/project-path-gitroot-changelog-20260518-1715.md +63 -0
- package/docs/implementation/task-active-gc-changelog-20260518-1745.md +35 -0
- package/docs/implementation/task-title-summary-changelog-20260518-1130.md +39 -0
- package/docs/implementation/tasks-detail-back-loses-filters-changelog-20260518-1100.md +22 -0
- package/docs/implementation/tasks-page-white-screen-hotfix-changelog-20260518-1015.md +56 -0
- package/docs/reviews/task-title-summary.md +92 -0
- package/docs/reviews/tasks-detail-back-loses-filters.md +58 -0
- package/docs/reviews/tasks-page-white-screen-hotfix.md +126 -0
- package/package.json +2 -2
- package/src/claudemd/claudemd-generator.ts +29 -238
- package/src/claudemd/resume-manager.ts +1 -1
- package/src/claudemd/templates/swarm-protocol.md +222 -0
- package/src/cli/commands/daemon.ts +6 -6
- package/src/cli/commands/executions.ts +4 -3
- package/src/cli/commands/init.ts +2 -2
- package/src/cli/commands/logs.ts +1 -1
- package/src/cli/commands/mcp.ts +3 -5
- package/src/cli/commands/menu.ts +4 -3
- package/src/cli/commands/stats.ts +2 -3
- package/src/cli/commands/status.ts +2 -2
- package/src/cli/commands/trace.ts +10 -26
- package/src/cli/init/hook-manager.ts +2 -2
- package/src/core/ai/provider.ts +2 -2
- package/src/core/constants.ts +18 -1
- package/src/core/event-fields.ts +32 -0
- package/src/core/queue/index.ts +3 -4
- package/src/core/storage/base.ts +132 -56
- package/src/core/storage/events.ts +183 -4
- package/src/core/storage/routing.ts +129 -1
- package/src/core/storage/schema.sql +12 -2
- package/src/core/storage/sessions.ts +64 -0
- package/src/core/storage/skills.ts +69 -0
- package/src/core/storage/sqlite.ts +103 -4
- package/src/core/storage/tasks.ts +149 -1
- package/src/core/storage/token-usage.ts +1 -1
- package/src/core/types.ts +30 -3
- package/src/core/utils/error-handler.ts +3 -2
- package/src/core/utils/git.ts +23 -0
- package/src/core/utils/logger.ts +16 -1
- package/src/core/utils/lru-cache.ts +4 -0
- package/src/core/utils/token-tracker.ts +1 -1
- package/src/daemon/event-parser.ts +4 -3
- package/src/daemon/handlers/history-exporter.ts +1 -1
- package/src/daemon/handlers/post-tool-use.ts +7 -3
- package/src/daemon/handlers/stop.ts +32 -39
- package/src/daemon/handlers/user-prompt.ts +12 -22
- package/src/daemon/hook-sync.ts +91 -0
- package/src/daemon/index.ts +34 -10
- package/src/daemon/lifecycle.ts +3 -3
- package/src/daemon/server.ts +76 -89
- package/src/daemon/services/task-segmenter.ts +1 -1
- package/src/hooks/hook-lib.sh +37 -0
- package/src/hooks/notification.sh +2 -2
- package/src/hooks/post-tool-use.sh +2 -2
- package/src/hooks/pre-tool-use.sh +2 -2
- package/src/hooks/stop.sh +9 -6
- package/src/hooks/user-prompt-submit.sh +2 -2
- package/src/{daemon/services → web/analytics}/anti-pattern-detector.ts +9 -54
- package/src/{daemon/services → web/analytics}/drift-detector.ts +10 -23
- package/src/{daemon/services → web/analytics}/weekly-report.ts +52 -75
- package/src/web/auth-middleware.ts +1 -2
- package/src/web/routes/_helpers.ts +34 -0
- package/src/web/routes/drift.ts +1 -1
- package/src/web/routes/insights.ts +1 -1
- package/src/web/routes/reports.ts +1 -1
- package/src/web/routes/rules.ts +31 -56
- package/src/web/routes/sessions.ts +18 -30
- package/src/web/routes/skill-stats.ts +29 -69
- package/src/web/routes/skills.ts +5 -4
- package/src/web/routes/stats.ts +19 -29
- package/src/web/routes/tasks.ts +17 -42
- package/src/web/routes/trace.ts +7 -19
- package/src/web/routes/types.ts +4 -3
- package/tests/integration/claudemd-generator.test.ts +90 -0
- package/tests/integration/web-analytics.integration.test.ts +133 -0
- package/tests/integration/web-stats.integration.test.ts +135 -0
- package/tests/integration/web-trace.integration.test.ts +175 -0
- package/tests/unit/core/forge-paths.test.ts +99 -0
- package/tests/unit/daemon/hook-sync.test.ts +71 -0
- package/tests/unit/daemon/post-tool-use.test.ts +121 -0
- package/tests/unit/daemon/stop-handler-behavior-summary.test.ts +202 -0
- package/tests/unit/daemon/task-segmenter-recover.test.ts +84 -0
- package/tests/unit/event-fields.test.ts +88 -0
- package/tests/unit/event-parser.test.ts +55 -0
- package/tests/unit/hooks/resolve-project-path.test.ts +122 -0
- package/tests/unit/socket-server.test.ts +183 -0
- package/tests/unit/storage/event-operations-aggregates.test.ts +342 -0
- package/tests/unit/storage/migration-idempotent.test.ts +304 -0
- package/tests/unit/storage/routing-aggregates.test.ts +276 -0
- package/tests/unit/storage/routing.test.ts +117 -0
- package/tests/unit/storage/schema-missing.test.ts +81 -0
- package/tests/unit/storage/session-operations-aggregates.test.ts +120 -0
- package/tests/unit/storage/skill-operations-counts.test.ts +106 -0
- package/tests/unit/storage/skills-aggregates.test.ts +104 -0
- package/tests/unit/storage/sqlite-refactor-harness.test.ts +3 -3
- package/tests/unit/storage/task-operations-counts.test.ts +46 -0
- package/tests/unit/storage/tasks-getById.test.ts +343 -0
- package/tests/unit/storage/tasks-stale-gc.test.ts +86 -0
- package/tests/unit/token-usage.test.ts +6 -6
- package/tests/unit/web/navigation-back-contract.test.ts +134 -0
- package/tests/unit/web/routes-rules.test.ts +182 -0
- package/tests/unit/web/routes-tasks.test.ts +34 -0
- package/tests/unit/web/task-title-contract.test.ts +210 -0
- package/tests/unit/web/tasks-component-contract.test.ts +179 -0
- package/vitest.config.ts +1 -1
- package/web/src/pages/TaskDetail.tsx +9 -5
- package/web/src/pages/Tasks.tsx +315 -50
- package/web/src/utils/navigation.ts +25 -0
- package/web/src/utils/task-title.ts +49 -0
- package/dist/daemon/services/anti-pattern-detector.d.ts.map +0 -1
- package/dist/daemon/services/drift-detector.d.ts.map +0 -1
- package/dist/daemon/services/drift-detector.js.map +0 -1
- package/dist/daemon/services/weekly-report.d.ts.map +0 -1
- package/dist/daemon/services/weekly-report.js.map +0 -1
- package/dist/web/static/assets/TaskDetail-5SR8zGzv.js +0 -2
- package/dist/web/static/assets/TaskDetail-5SR8zGzv.js.map +0 -1
- package/dist/web/static/assets/Tasks-DCgDqvOZ.js +0 -2
- package/dist/web/static/assets/Tasks-DCgDqvOZ.js.map +0 -1
- package/dist/web/static/assets/index-D8AKj26b.css +0 -1
- package/dist/web/static/assets/index-DxIbmNmr.js +0 -3
- package/dist/web/static/assets/lucide-fJlPI3H7.js.map +0 -1
- package/dist/web/static/assets/react-router-I-HqunH7.js +0 -20
- /package/dist/{daemon/services → web/analytics}/drift-detector.d.ts +0 -0
- /package/dist/{daemon/services → web/analytics}/weekly-report.d.ts +0 -0
|
@@ -16,10 +16,13 @@ import type { SQLiteStorage } from '../../core/storage/sqlite.js';
|
|
|
16
16
|
import type { InvocationGuard } from '../../skills/invocation-guard.js';
|
|
17
17
|
import type { UserPromptHandler } from './user-prompt.js';
|
|
18
18
|
import { logger } from '../../core/utils/logger.js';
|
|
19
|
-
import {
|
|
19
|
+
import { execFile } from 'node:child_process';
|
|
20
|
+
import { promisify } from 'node:util';
|
|
20
21
|
import { formatError, truncateString } from '../../core/utils/format.js';
|
|
21
22
|
import { truncateSessionId } from '../../core/utils/session.js';
|
|
22
23
|
|
|
24
|
+
const execFileAsync = promisify(execFile);
|
|
25
|
+
|
|
23
26
|
export class StopHandler {
|
|
24
27
|
constructor(
|
|
25
28
|
private exporter: HistoryExporter,
|
|
@@ -80,8 +83,9 @@ export class StopHandler {
|
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
// 6. Write session_id to git note on HEAD commit
|
|
84
|
-
|
|
86
|
+
// 6. Write session_id to git note on HEAD commit (fire-and-forget, async)
|
|
87
|
+
// 不 await:git 操作不影响 handler 响应,失败已在内部 catch
|
|
88
|
+
void this.writeGitNote(event);
|
|
85
89
|
|
|
86
90
|
return { allow: true };
|
|
87
91
|
} catch (err) {
|
|
@@ -96,36 +100,40 @@ export class StopHandler {
|
|
|
96
100
|
* Write session_id into a git note on the current HEAD commit.
|
|
97
101
|
* Uses `git notes append` so multiple sessions can be associated with one commit.
|
|
98
102
|
* Silently ignores failures (no git repo, no commits, etc.).
|
|
103
|
+
*
|
|
104
|
+
* 异步实现:3 个 git 调用有序依赖(rev-parse HEAD 依赖 work-tree 检查通过;
|
|
105
|
+
* notes append 依赖 HEAD hash),因此 rev-parse 串行;但整体 fire-and-forget,
|
|
106
|
+
* 不阻塞 handler 主响应。
|
|
99
107
|
*/
|
|
100
|
-
private writeGitNote(event: StopEvent): void {
|
|
108
|
+
private async writeGitNote(event: StopEvent): Promise<void> {
|
|
101
109
|
const projectPath = event.project_path;
|
|
102
110
|
if (!projectPath) return;
|
|
103
111
|
|
|
112
|
+
const opts = {
|
|
113
|
+
cwd: projectPath,
|
|
114
|
+
timeout: 5000,
|
|
115
|
+
};
|
|
116
|
+
|
|
104
117
|
try {
|
|
105
118
|
// Check if inside a git work tree
|
|
106
|
-
|
|
107
|
-
cwd: projectPath,
|
|
108
|
-
stdio: 'pipe',
|
|
109
|
-
timeout: 5000,
|
|
110
|
-
});
|
|
119
|
+
await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], opts);
|
|
111
120
|
|
|
112
121
|
// Get HEAD commit hash
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
stdio: 'pipe',
|
|
116
|
-
timeout: 5000,
|
|
117
|
-
}).toString().trim();
|
|
122
|
+
const { stdout: headStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], opts);
|
|
123
|
+
const head = headStdout.toString().trim();
|
|
118
124
|
|
|
119
125
|
if (!head) return;
|
|
120
126
|
|
|
121
127
|
// Append session_id to git note on HEAD
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
128
|
+
await execFileAsync(
|
|
129
|
+
'git',
|
|
130
|
+
['notes', 'append', '-m', `forge-session: ${event.session_id}`, head],
|
|
131
|
+
opts,
|
|
132
|
+
);
|
|
127
133
|
|
|
128
|
-
logger.debug(
|
|
134
|
+
logger.debug(
|
|
135
|
+
`[Stop] Git note appended: forge-session: ${truncateSessionId(event.session_id)} on ${truncateString(head, 8)}`,
|
|
136
|
+
);
|
|
129
137
|
} catch (err) {
|
|
130
138
|
logger.debug(`[Stop] Git note write skipped: ${formatError(err)}`);
|
|
131
139
|
}
|
|
@@ -140,36 +148,21 @@ export class StopHandler {
|
|
|
140
148
|
if (!this.storage) return null;
|
|
141
149
|
|
|
142
150
|
try {
|
|
143
|
-
|
|
151
|
+
// H2 Phase 3: 改用 facade 聚合方法,消除 db.prepare 直写。
|
|
144
152
|
|
|
145
153
|
// 1. Tool usage stats
|
|
146
|
-
const toolStats =
|
|
147
|
-
SELECT tool_name, COUNT(*) as count FROM events
|
|
148
|
-
WHERE session_id = ? AND tool_name IS NOT NULL
|
|
149
|
-
GROUP BY tool_name ORDER BY count DESC
|
|
150
|
-
`).all(sessionId) as Array<{ tool_name: string; count: number }>;
|
|
151
|
-
|
|
154
|
+
const toolStats = this.storage.aggregateToolUsageBySession(sessionId);
|
|
152
155
|
const totalToolCalls = toolStats.reduce((sum, row) => sum + row.count, 0);
|
|
153
156
|
|
|
154
157
|
// 2. Agent/Task delegation stats
|
|
155
|
-
const agentStats =
|
|
156
|
-
SELECT json_extract(tool_input, '$.subagent_type') as agent_type, COUNT(*) as count
|
|
157
|
-
FROM events
|
|
158
|
-
WHERE session_id = ? AND tool_name IN ('Agent', 'Task') AND tool_input IS NOT NULL
|
|
159
|
-
GROUP BY agent_type
|
|
160
|
-
`).all(sessionId) as Array<{ agent_type: string | null; count: number }>;
|
|
161
|
-
|
|
158
|
+
const agentStats = this.storage.aggregateAgentTypeBySession(sessionId);
|
|
162
159
|
const totalAgentCalls = agentStats.reduce((sum, row) => sum + row.count, 0);
|
|
163
160
|
const agentNames = agentStats
|
|
164
161
|
.filter(row => row.agent_type)
|
|
165
162
|
.map(row => row.agent_type as string);
|
|
166
163
|
|
|
167
164
|
// 3. Skill invocation count
|
|
168
|
-
const
|
|
169
|
-
SELECT COUNT(*) as count FROM skill_invocations WHERE session_id = ?
|
|
170
|
-
`).get(sessionId) as { count: number } | undefined;
|
|
171
|
-
|
|
172
|
-
const skillCount = skillRow?.count ?? 0;
|
|
165
|
+
const skillCount = this.storage.countSkillInvocationsBySession(sessionId);
|
|
173
166
|
|
|
174
167
|
// Build summary
|
|
175
168
|
if (totalToolCalls === 0) return null;
|
|
@@ -14,11 +14,15 @@ import type { SkillRegistry } from '../../skills/registry.js';
|
|
|
14
14
|
import { logger } from '../../core/utils/logger.js';
|
|
15
15
|
import { randomUUID } from 'node:crypto';
|
|
16
16
|
import { truncateString } from '../../core/utils/format.js';
|
|
17
|
+
import { LRUCache } from '../../core/utils/lru-cache.js';
|
|
17
18
|
|
|
18
19
|
export class UserPromptHandler {
|
|
19
|
-
private resumeInjected = new Map<string, number>();
|
|
20
|
-
private conventionInjected = new Map<string, number>();
|
|
21
20
|
private static readonly MAX_INJECTION_KEYS = 1000;
|
|
21
|
+
// Two independent caches: resume vs convention have separate inject-once
|
|
22
|
+
// semantics, so we keep them as separate LRU instances rather than merging
|
|
23
|
+
// their key spaces. Both store sessionKey → injection timestamp.
|
|
24
|
+
private resumeInjected = new LRUCache<string, number>(UserPromptHandler.MAX_INJECTION_KEYS);
|
|
25
|
+
private conventionInjected = new LRUCache<string, number>(UserPromptHandler.MAX_INJECTION_KEYS);
|
|
22
26
|
|
|
23
27
|
constructor(
|
|
24
28
|
private resume: ResumeManager | null = null,
|
|
@@ -137,31 +141,17 @@ export class UserPromptHandler {
|
|
|
137
141
|
}
|
|
138
142
|
// ── Private: Injection tracking helpers ────────────────────────────────
|
|
139
143
|
|
|
140
|
-
/** Check if a sessionKey has been injected in the given
|
|
141
|
-
private hasInjected(
|
|
142
|
-
return
|
|
144
|
+
/** Check if a sessionKey has been injected in the given cache. */
|
|
145
|
+
private hasInjected(cache: LRUCache<string, number>, key: string): boolean {
|
|
146
|
+
return cache.has(key);
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
/**
|
|
146
150
|
* Mark a sessionKey as injected, recording the current timestamp.
|
|
147
|
-
*
|
|
151
|
+
* LRUCache transparently evicts the oldest entry when capacity is reached.
|
|
148
152
|
*/
|
|
149
|
-
private markInjected(
|
|
150
|
-
|
|
151
|
-
// Evict the entry with the smallest timestamp (oldest)
|
|
152
|
-
let oldestKey: string | undefined;
|
|
153
|
-
let oldestTs = Infinity;
|
|
154
|
-
for (const [k, ts] of map) {
|
|
155
|
-
if (ts < oldestTs) {
|
|
156
|
-
oldestTs = ts;
|
|
157
|
-
oldestKey = k;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
if (oldestKey !== undefined) {
|
|
161
|
-
map.delete(oldestKey);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
map.set(key, Date.now());
|
|
153
|
+
private markInjected(cache: LRUCache<string, number>, key: string): void {
|
|
154
|
+
cache.set(key, Date.now());
|
|
165
155
|
}
|
|
166
156
|
|
|
167
157
|
// ── Public: Session cleanup ──────────────────────────────────────────────
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { existsSync, readFileSync, copyFileSync, chmodSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { FORGE_PATHS } from '../core/constants.js';
|
|
6
|
+
import { logger } from '../core/utils/logger.js';
|
|
7
|
+
|
|
8
|
+
// All hook files to sync (5 hooks + shared lib)
|
|
9
|
+
const HOOK_FILES = [
|
|
10
|
+
'pre-tool-use.sh',
|
|
11
|
+
'post-tool-use.sh',
|
|
12
|
+
'user-prompt-submit.sh',
|
|
13
|
+
'notification.sh',
|
|
14
|
+
'stop.sh',
|
|
15
|
+
'hook-lib.sh',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function sha256(content: Buffer): string {
|
|
19
|
+
return createHash('sha256').update(content).digest('hex');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getSourceHooksDir(): string {
|
|
23
|
+
// Compiled: dist/daemon/hook-sync.js → dist/hooks
|
|
24
|
+
// Dev (vitest): src/daemon/hook-sync.ts → src/hooks
|
|
25
|
+
return join(dirname(fileURLToPath(import.meta.url)), '..', 'hooks');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SyncResult {
|
|
29
|
+
copied: number;
|
|
30
|
+
checked: number;
|
|
31
|
+
skipped: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Sync hooks from the package source (dist/hooks) to ~/.claude-forge/hooks/.
|
|
36
|
+
* Uses SHA-256 comparison to skip identical files.
|
|
37
|
+
*
|
|
38
|
+
* Accepts optional overrides for source/target dirs — used in unit tests.
|
|
39
|
+
* Any failure logs a warning and continues; never throws.
|
|
40
|
+
*/
|
|
41
|
+
export function syncHooks(opts?: { sourceDir?: string; targetDir?: string }): SyncResult {
|
|
42
|
+
const result: SyncResult = { copied: 0, checked: 0, skipped: 0 };
|
|
43
|
+
const sourceDir = opts?.sourceDir ?? getSourceHooksDir();
|
|
44
|
+
const targetDir = opts?.targetDir ?? FORGE_PATHS.hooks();
|
|
45
|
+
|
|
46
|
+
if (!existsSync(sourceDir)) {
|
|
47
|
+
logger.warn(`[HookSync] source hooks dir not found at ${sourceDir}, skipping`);
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!existsSync(targetDir)) {
|
|
52
|
+
// User has not run `claude-forge init` yet — skip silently
|
|
53
|
+
logger.debug(`[HookSync] target dir not found: ${targetDir} (user may not have run \`claude-forge init\`)`);
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const file of HOOK_FILES) {
|
|
58
|
+
const src = join(sourceDir, file);
|
|
59
|
+
const dest = join(targetDir, file);
|
|
60
|
+
|
|
61
|
+
if (!existsSync(src)) {
|
|
62
|
+
result.skipped++;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
result.checked++;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const srcContent = readFileSync(src);
|
|
70
|
+
if (existsSync(dest)) {
|
|
71
|
+
const destContent = readFileSync(dest);
|
|
72
|
+
if (sha256(srcContent) === sha256(destContent)) {
|
|
73
|
+
continue; // identical — no copy needed
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
copyFileSync(src, dest);
|
|
78
|
+
chmodSync(dest, 0o755);
|
|
79
|
+
result.copied++;
|
|
80
|
+
logger.info(`[HookSync] updated ${file}`);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
logger.warn(`[HookSync] failed to sync ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (result.copied > 0) {
|
|
87
|
+
logger.info(`[HookSync] synced ${result.copied}/${result.checked} hook files`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return result;
|
|
91
|
+
}
|
package/src/daemon/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
removeAuthToken,
|
|
21
21
|
} from './lifecycle.js';
|
|
22
22
|
import { routeEvent, type Handlers } from './router.js';
|
|
23
|
-
import { logger
|
|
23
|
+
import { logger } from '../core/utils/logger.js';
|
|
24
24
|
import { ConfigManager } from '../core/config.js';
|
|
25
25
|
import { SQLiteStorage } from '../core/storage/sqlite.js';
|
|
26
26
|
import { expandPath } from '../core/utils/path.js';
|
|
@@ -34,8 +34,11 @@ import { ConventionExtractor } from '../claudemd/convention-extractor.js';
|
|
|
34
34
|
import { UserPromptHandler } from './handlers/user-prompt.js';
|
|
35
35
|
import { PostToolUseHandler } from './handlers/post-tool-use.js';
|
|
36
36
|
import { StopHandler } from './handlers/stop.js';
|
|
37
|
+
import { syncHooks } from './hook-sync.js';
|
|
37
38
|
import { replayQueue } from '../core/queue/index.js';
|
|
39
|
+
import { DEFAULTS } from '../core/constants.js';
|
|
38
40
|
import type { ForgeEvent } from '../core/types.js';
|
|
41
|
+
import { getUserPrompt } from '../core/event-fields.js';
|
|
39
42
|
import type { HookResponse } from './server.js';
|
|
40
43
|
|
|
41
44
|
export interface DaemonOptions {
|
|
@@ -56,13 +59,8 @@ export async function startDaemon(foreground: boolean = false, options: DaemonOp
|
|
|
56
59
|
});
|
|
57
60
|
|
|
58
61
|
// ── 1. Config & Logging ────────────────────────────────────────────────────
|
|
62
|
+
// Log level is auto-parsed from LOG_LEVEL env var at logger module load time.
|
|
59
63
|
const config = new ConfigManager().get();
|
|
60
|
-
|
|
61
|
-
const envLevel = (process.env.LOG_LEVEL ?? '').toLowerCase();
|
|
62
|
-
const levelMap: Record<string, LogLevel> = {
|
|
63
|
-
debug: LogLevel.DEBUG, info: LogLevel.INFO, warn: LogLevel.WARN, error: LogLevel.ERROR,
|
|
64
|
-
};
|
|
65
|
-
setLogLevel(levelMap[envLevel] ?? LogLevel.INFO);
|
|
66
64
|
logger.info('Claude Forge daemon starting...');
|
|
67
65
|
|
|
68
66
|
// ── 2. Lifecycle (PID, socket, auth token) ─────────────────────────────────
|
|
@@ -70,13 +68,22 @@ export async function startDaemon(foreground: boolean = false, options: DaemonOp
|
|
|
70
68
|
writePidFile();
|
|
71
69
|
const authToken = writeAuthToken();
|
|
72
70
|
logger.info('[Security] Auth token generated');
|
|
73
|
-
syncMcpToken(authToken, config.web?.port ??
|
|
71
|
+
syncMcpToken(authToken, config.web?.port ?? DEFAULTS.WEB_PORT);
|
|
74
72
|
|
|
75
73
|
// ── 3. Storage ─────────────────────────────────────────────────────────────
|
|
76
74
|
const dbPath = expandPath(config.storage.path);
|
|
77
75
|
const storage = new SQLiteStorage(dbPath);
|
|
78
76
|
logger.info(`Storage initialized: ${dbPath}`);
|
|
79
77
|
|
|
78
|
+
// ── 3.5. Auto-sync hooks ────────────────────────────────────────────────
|
|
79
|
+
// npm upgrade 不会自动更新 ~/.claude-forge/hooks/,每次 daemon 启动
|
|
80
|
+
// 用 SHA-256 比对源 dist/hooks 与本地副本,不一致则覆盖。
|
|
81
|
+
try {
|
|
82
|
+
syncHooks();
|
|
83
|
+
} catch (err) {
|
|
84
|
+
logger.warn(`[HookSync] unexpected error: ${err}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
80
87
|
// ── 4. AI Provider ─────────────────────────────────────────────────────────
|
|
81
88
|
const apiKey = config.ai.api_key || process.env.ANTHROPIC_API_KEY || '';
|
|
82
89
|
if (!apiKey) {
|
|
@@ -108,7 +115,7 @@ export async function startDaemon(foreground: boolean = false, options: DaemonOp
|
|
|
108
115
|
// ── 5.5. Schedule daily maintenance ────────────────────────────────────────
|
|
109
116
|
// Clean old events every 24 hours
|
|
110
117
|
const MAINTENANCE_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
|
|
111
|
-
setInterval(() => {
|
|
118
|
+
const maintenanceInterval = setInterval(() => {
|
|
112
119
|
logger.info('[Daemon] Running daily maintenance: cleaning old events');
|
|
113
120
|
try {
|
|
114
121
|
storage.cleanOldEvents(30);
|
|
@@ -118,6 +125,21 @@ export async function startDaemon(foreground: boolean = false, options: DaemonOp
|
|
|
118
125
|
}, MAINTENANCE_INTERVAL);
|
|
119
126
|
logger.info('[Daemon] Scheduled daily maintenance (clean events older than 30 days)');
|
|
120
127
|
|
|
128
|
+
// ── 5.6. Stale task GC (every 5 minutes) ──────────────────────────────────
|
|
129
|
+
const STALE_TASK_GC_INTERVAL = 5 * 60 * 1000; // 5 分钟
|
|
130
|
+
const STALE_TASK_IDLE_MINUTES = 10; // idle 超 10 分钟视为滞留
|
|
131
|
+
|
|
132
|
+
const staleTaskGcInterval = setInterval(() => {
|
|
133
|
+
try {
|
|
134
|
+
const closed = storage.completeStaleActiveTasks(STALE_TASK_IDLE_MINUTES);
|
|
135
|
+
if (closed > 0) {
|
|
136
|
+
logger.info(`[Maintenance] Auto-completed ${closed} stale active task(s)`);
|
|
137
|
+
}
|
|
138
|
+
} catch (err) {
|
|
139
|
+
logger.error(`[Maintenance] Stale task GC failed: ${err}`);
|
|
140
|
+
}
|
|
141
|
+
}, STALE_TASK_GC_INTERVAL);
|
|
142
|
+
|
|
121
143
|
// ── 6. Create handlers ─────────────────────────────────────────────────────
|
|
122
144
|
const userPromptHandler = new UserPromptHandler(
|
|
123
145
|
resume,
|
|
@@ -157,7 +179,7 @@ export async function startDaemon(foreground: boolean = false, options: DaemonOp
|
|
|
157
179
|
logger.debug(`[Event] ${event.hook_type} | tool=${event.tool_name ?? 'N/A'}`);
|
|
158
180
|
|
|
159
181
|
// Task segmentation: UserPromptSubmit starts/continues tasks
|
|
160
|
-
const prompt =
|
|
182
|
+
const prompt = getUserPrompt(event);
|
|
161
183
|
if (event.hook_type === 'UserPromptSubmit' && prompt) {
|
|
162
184
|
taskSegmenter.processPrompt(event.session_id, prompt, event.timestamp, event.event_id);
|
|
163
185
|
} else if (event.event_id) {
|
|
@@ -250,6 +272,8 @@ export async function startDaemon(foreground: boolean = false, options: DaemonOp
|
|
|
250
272
|
|
|
251
273
|
if (ai) logger.info(`[AI Stats] ${ai.formatStats()}`);
|
|
252
274
|
|
|
275
|
+
clearInterval(maintenanceInterval);
|
|
276
|
+
clearInterval(staleTaskGcInterval);
|
|
253
277
|
if (webServer) await webServer.stop();
|
|
254
278
|
await server.close();
|
|
255
279
|
storage.close();
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -6,8 +6,8 @@ import { logger } from '../core/utils/logger.js';
|
|
|
6
6
|
import { FORGE_PATHS } from '../core/constants.js';
|
|
7
7
|
|
|
8
8
|
const FORGE_HOME = FORGE_PATHS.home();
|
|
9
|
-
const TOKEN_FILE =
|
|
10
|
-
const PID_FILE =
|
|
9
|
+
const TOKEN_FILE = FORGE_PATHS.daemonToken();
|
|
10
|
+
const PID_FILE = FORGE_PATHS.daemonPid();
|
|
11
11
|
|
|
12
12
|
export function writePidFile(): void {
|
|
13
13
|
if (!fs.existsSync(FORGE_HOME)) {
|
|
@@ -49,7 +49,7 @@ export function isRunning(): boolean {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export function getSocketPath(): string {
|
|
52
|
-
return
|
|
52
|
+
return FORGE_PATHS.daemonSocket();
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export function cleanSocket(): void {
|
package/src/daemon/server.ts
CHANGED
|
@@ -72,61 +72,39 @@ export class SocketServer {
|
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
} catch {
|
|
88
|
-
// JSON 解析失败,交给下面的 EventParser 处理
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const event = this.parser.parse(buffer);
|
|
93
|
-
buffer = '';
|
|
94
|
-
if (connectionTimeout) clearTimeout(connectionTimeout);
|
|
95
|
-
|
|
96
|
-
const result = await this.handler(event);
|
|
75
|
+
// 性能修复(L3):hook 端用 `echo` 发送 JSON,结尾自带换行符。
|
|
76
|
+
// 只在 buffer 中遇到换行符时才尝试解析,避免大事件(接近 512KB)
|
|
77
|
+
// 在分片到达时每个 chunk 都跑一次完整 JSON.parse 造成的 N² 行为。
|
|
78
|
+
// 兼容:如果数据无换行(旧客户端、其他来源),等到 socket 关闭时
|
|
79
|
+
// 由 'end' 事件做最后一次解析尝试。
|
|
80
|
+
const newlineIdx = buffer.indexOf('\n');
|
|
81
|
+
if (newlineIdx === -1) {
|
|
82
|
+
// 尚未收到完整消息,继续等待
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
97
85
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const cleaned = { ...result } as Record<string, unknown>;
|
|
102
|
-
if (cleaned['additionalContext'] && !this.isValidContent(cleaned['additionalContext'] as string)) {
|
|
103
|
-
delete cleaned['additionalContext'];
|
|
104
|
-
}
|
|
105
|
-
if (cleaned['systemMessage'] && !this.isValidContent(cleaned['systemMessage'] as string)) {
|
|
106
|
-
delete cleaned['systemMessage'];
|
|
107
|
-
}
|
|
86
|
+
// 取换行前的完整消息;忽略行后可能存在的多余数据(hook 单连接一事件)
|
|
87
|
+
const message = buffer.slice(0, newlineIdx);
|
|
88
|
+
buffer = '';
|
|
108
89
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
// 区分"JSON 不完整"和"格式错误"
|
|
116
|
-
if (err instanceof SyntaxError) {
|
|
117
|
-
const trimmed = buffer.trim();
|
|
118
|
-
if (trimmed.startsWith('{') && !this.isCompleteJSON(trimmed)) {
|
|
119
|
-
// JSON 不完整,继续等待
|
|
120
|
-
logger.debug(`缓冲区不完整(${buffer.length} 字节),等待更多数据`);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
90
|
+
try {
|
|
91
|
+
await this.processMessage(socket, message);
|
|
92
|
+
} finally {
|
|
93
|
+
if (connectionTimeout) clearTimeout(connectionTimeout);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
124
96
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
97
|
+
// 兜底:连接关闭时若 buffer 仍有内容(无换行结尾的旧客户端),尝试解析一次
|
|
98
|
+
socket.on('end', async () => {
|
|
99
|
+
const remaining = buffer.trim();
|
|
100
|
+
buffer = '';
|
|
101
|
+
if (remaining.length === 0) return;
|
|
102
|
+
try {
|
|
103
|
+
await this.processMessage(socket, remaining);
|
|
104
|
+
} catch {
|
|
105
|
+
// processMessage 内部已记录错误
|
|
106
|
+
} finally {
|
|
128
107
|
if (connectionTimeout) clearTimeout(connectionTimeout);
|
|
129
|
-
socket.destroy();
|
|
130
108
|
}
|
|
131
109
|
});
|
|
132
110
|
|
|
@@ -140,6 +118,53 @@ export class SocketServer {
|
|
|
140
118
|
});
|
|
141
119
|
}
|
|
142
120
|
|
|
121
|
+
/**
|
|
122
|
+
* 解析单条完整消息(一次性,无 N² 行为)并执行 handler。
|
|
123
|
+
*
|
|
124
|
+
* 注意:调用方需确保 message 是一条完整的 JSON 文本(已根据换行符切分)。
|
|
125
|
+
*/
|
|
126
|
+
private async processMessage(socket: net.Socket, message: string): Promise<void> {
|
|
127
|
+
try {
|
|
128
|
+
// 认证检查:在 EventParser 之前提取 _auth 字段(zod 会剥离未知字段)
|
|
129
|
+
if (this.authToken) {
|
|
130
|
+
try {
|
|
131
|
+
const raw = JSON.parse(message);
|
|
132
|
+
if (raw._auth !== this.authToken) {
|
|
133
|
+
logger.warn('[Socket] 认证失败,关闭连接');
|
|
134
|
+
socket.destroy();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// JSON 解析失败,交给下面的 EventParser 处理
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const event = this.parser.parse(message);
|
|
143
|
+
const result = await this.handler(event);
|
|
144
|
+
|
|
145
|
+
// 双向通信:如果 handler 返回了结果,写回给 hook 脚本
|
|
146
|
+
if (result) {
|
|
147
|
+
// 清理无意义内容,避免 Claude Code 注入干扰上下文
|
|
148
|
+
const cleaned = { ...result } as Record<string, unknown>;
|
|
149
|
+
if (cleaned['additionalContext'] && !this.isValidContent(cleaned['additionalContext'] as string)) {
|
|
150
|
+
delete cleaned['additionalContext'];
|
|
151
|
+
}
|
|
152
|
+
if (cleaned['systemMessage'] && !this.isValidContent(cleaned['systemMessage'] as string)) {
|
|
153
|
+
delete cleaned['systemMessage'];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const payload = JSON.stringify(cleaned);
|
|
157
|
+
socket.write(payload, () => socket.end());
|
|
158
|
+
} else {
|
|
159
|
+
socket.end();
|
|
160
|
+
}
|
|
161
|
+
} catch (err) {
|
|
162
|
+
logger.error(`事件解析失败:${formatError(err)}`);
|
|
163
|
+
logger.debug(`无效消息内容:${truncateString(message, 500)}`);
|
|
164
|
+
socket.destroy();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
143
168
|
/**
|
|
144
169
|
* 判断内容是否有意义,过滤空字符串、纯标点/符号等
|
|
145
170
|
*/
|
|
@@ -160,44 +185,6 @@ export class SocketServer {
|
|
|
160
185
|
return true;
|
|
161
186
|
}
|
|
162
187
|
|
|
163
|
-
/**
|
|
164
|
-
* 简单检查 JSON 是否完整(启发式方法)
|
|
165
|
-
*/
|
|
166
|
-
private isCompleteJSON(str: string): boolean {
|
|
167
|
-
let braceCount = 0;
|
|
168
|
-
let bracketCount = 0;
|
|
169
|
-
let inString = false;
|
|
170
|
-
let escaped = false;
|
|
171
|
-
|
|
172
|
-
for (let i = 0; i < str.length; i++) {
|
|
173
|
-
const char = str[i];
|
|
174
|
-
|
|
175
|
-
if (escaped) {
|
|
176
|
-
escaped = false;
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (char === '\\') {
|
|
181
|
-
escaped = true;
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (char === '"') {
|
|
186
|
-
inString = !inString;
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (inString) continue;
|
|
191
|
-
|
|
192
|
-
if (char === '{') braceCount++;
|
|
193
|
-
else if (char === '}') braceCount--;
|
|
194
|
-
else if (char === '[') bracketCount++;
|
|
195
|
-
else if (char === ']') bracketCount--;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return braceCount === 0 && bracketCount === 0 && !inString;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
188
|
close(): Promise<void> {
|
|
202
189
|
return new Promise((resolve) => {
|
|
203
190
|
this.server.close(() => {
|
|
@@ -75,7 +75,7 @@ export class TaskSegmenter {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
completeCurrentTask(sessionId: string, timestamp: string): void {
|
|
78
|
-
const current = this.currentTasks.get(sessionId);
|
|
78
|
+
const current = this.currentTasks.get(sessionId) ?? this.recoverActiveTask(sessionId);
|
|
79
79
|
if (!current) return;
|
|
80
80
|
this.storage.updateTask(current.id, {
|
|
81
81
|
status: 'completed',
|
package/src/hooks/hook-lib.sh
CHANGED
|
@@ -10,6 +10,43 @@
|
|
|
10
10
|
SOCKET_PATH="${CLAUDE_FORGE_SOCKET:-$HOME/.claude-forge/daemon.sock}"
|
|
11
11
|
QUEUE_DIR="$HOME/.claude-forge/queue"
|
|
12
12
|
|
|
13
|
+
# resolve_project_path <input_cwd>
|
|
14
|
+
#
|
|
15
|
+
# Walk up the directory tree from <input_cwd> to find the nearest ancestor that
|
|
16
|
+
# contains a `.git` entry (directory for normal clones, file for worktrees /
|
|
17
|
+
# submodules). Print the resolved git-root path on stdout.
|
|
18
|
+
#
|
|
19
|
+
# Fallback rules:
|
|
20
|
+
# - Empty input → start from $PWD
|
|
21
|
+
# - Relative input → prefixed with $PWD to absolutise
|
|
22
|
+
# - No .git found within 64 levels → print original input unchanged
|
|
23
|
+
#
|
|
24
|
+
# POSIX-only: uses `dirname` and `case`; does NOT depend on `realpath` or any
|
|
25
|
+
# GNU coreutils extensions, so it works on macOS BSD and Linux alike.
|
|
26
|
+
resolve_project_path() {
|
|
27
|
+
local input="${1:-}"
|
|
28
|
+
local dir="${input:-$PWD}"
|
|
29
|
+
|
|
30
|
+
# Absolutise: prefix relative paths with $PWD (no realpath dependency)
|
|
31
|
+
case "$dir" in
|
|
32
|
+
/*) ;;
|
|
33
|
+
*) dir="$PWD/$dir" ;;
|
|
34
|
+
esac
|
|
35
|
+
|
|
36
|
+
local guard=0
|
|
37
|
+
while [ "$dir" != "/" ] && [ "$dir" != "." ] && [ $guard -lt 64 ]; do
|
|
38
|
+
if [ -d "$dir/.git" ] || [ -f "$dir/.git" ]; then
|
|
39
|
+
printf '%s' "$dir"
|
|
40
|
+
return 0
|
|
41
|
+
fi
|
|
42
|
+
dir=$(dirname "$dir")
|
|
43
|
+
guard=$((guard + 1))
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
# No git ancestor found → fall back to the caller's original cwd
|
|
47
|
+
printf '%s' "${input:-$PWD}"
|
|
48
|
+
}
|
|
49
|
+
|
|
13
50
|
# send_event_or_enqueue <event_json> [timeout_seconds]
|
|
14
51
|
#
|
|
15
52
|
# 1. If socket file does not exist → enqueue in background → return empty string
|
|
@@ -10,8 +10,8 @@ AUTH_TOKEN=$(cat "$HOME/.claude-forge/daemon.token" 2>/dev/null || echo '')
|
|
|
10
10
|
INPUT=$(cat)
|
|
11
11
|
|
|
12
12
|
# 提取 cwd(jq 替代 python3)
|
|
13
|
-
|
|
14
|
-
PROJECT_PATH
|
|
13
|
+
RAW_CWD=$(echo "$INPUT" | jq -r '.cwd // ""')
|
|
14
|
+
PROJECT_PATH=$(resolve_project_path "${RAW_CWD:-$PWD}")
|
|
15
15
|
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // .sessionId // ""')
|
|
16
16
|
SESSION_ID="${SESSION_ID:-${CLAUDE_CODE_SESSION_ID:-cli}}"
|
|
17
17
|
|
|
@@ -14,8 +14,8 @@ INPUT=$(cat)
|
|
|
14
14
|
TOOL_NAME="${CLAUDE_TOOL_NAME:-$(echo "$INPUT" | jq -r '.tool_name // ""')}"
|
|
15
15
|
TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')
|
|
16
16
|
TOOL_OUTPUT=$(echo "$INPUT" | jq -c '.tool_response // {}')
|
|
17
|
-
|
|
18
|
-
PROJECT_PATH
|
|
17
|
+
RAW_CWD=$(echo "$INPUT" | jq -r '.cwd // ""')
|
|
18
|
+
PROJECT_PATH=$(resolve_project_path "${RAW_CWD:-$PWD}")
|
|
19
19
|
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // .sessionId // ""')
|
|
20
20
|
SESSION_ID="${SESSION_ID:-${CLAUDE_CODE_SESSION_ID:-cli}}"
|
|
21
21
|
|