@winspan/claude-forge 8.53.2 → 8.54.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEVELOPMENT.md +290 -221
- package/README.md +50 -8
- package/dist/cli/commands/skills.d.ts.map +1 -1
- package/dist/cli/commands/skills.js +7 -3
- package/dist/cli/commands/skills.js.map +1 -1
- package/dist/cli/init/hook-manager.d.ts +1 -1
- package/dist/cli/init/hook-manager.d.ts.map +1 -1
- package/dist/cli/init/hook-manager.js +1 -0
- package/dist/cli/init/hook-manager.js.map +1 -1
- package/dist/core/storage/events.d.ts.map +1 -1
- package/dist/core/storage/events.js +0 -1
- package/dist/core/storage/events.js.map +1 -1
- package/dist/core/storage/maintenance.d.ts +25 -3
- package/dist/core/storage/maintenance.d.ts.map +1 -1
- package/dist/core/storage/maintenance.js +33 -4
- package/dist/core/storage/maintenance.js.map +1 -1
- package/dist/core/storage/routing.d.ts +4 -0
- package/dist/core/storage/routing.d.ts.map +1 -1
- package/dist/core/storage/routing.js +10 -4
- package/dist/core/storage/routing.js.map +1 -1
- package/dist/core/storage/sessions.d.ts +17 -0
- package/dist/core/storage/sessions.d.ts.map +1 -1
- package/dist/core/storage/sessions.js +64 -0
- package/dist/core/storage/sessions.js.map +1 -1
- package/dist/core/storage/skills.d.ts +4 -0
- package/dist/core/storage/skills.d.ts.map +1 -1
- package/dist/core/storage/skills.js +10 -2
- package/dist/core/storage/skills.js.map +1 -1
- package/dist/core/storage/sqlite.d.ts +5 -0
- package/dist/core/storage/sqlite.d.ts.map +1 -1
- package/dist/core/storage/sqlite.js +6 -0
- package/dist/core/storage/sqlite.js.map +1 -1
- package/dist/core/storage/tasks.d.ts.map +1 -1
- package/dist/core/storage/tasks.js +2 -0
- package/dist/core/storage/tasks.js.map +1 -1
- package/dist/core/types.d.ts +7 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +19 -4
- package/dist/daemon/index.js.map +1 -1
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +13 -2
- package/dist/skills/registry.js.map +1 -1
- package/dist/skills/semantic-matcher.d.ts +2 -2
- package/dist/skills/semantic-matcher.d.ts.map +1 -1
- package/dist/skills/semantic-matcher.js +14 -19
- package/dist/skills/semantic-matcher.js.map +1 -1
- package/dist/skills/upgrade-engine.d.ts +3 -1
- package/dist/skills/upgrade-engine.d.ts.map +1 -1
- package/dist/skills/upgrade-engine.js +25 -14
- package/dist/skills/upgrade-engine.js.map +1 -1
- package/dist/web/analytics/weekly-report.d.ts.map +1 -1
- package/dist/web/analytics/weekly-report.js +21 -29
- package/dist/web/analytics/weekly-report.js.map +1 -1
- package/dist/web/routes/patch.d.ts.map +1 -1
- package/dist/web/routes/patch.js +32 -2
- package/dist/web/routes/patch.js.map +1 -1
- package/dist/web/routes/sessions.d.ts.map +1 -1
- package/dist/web/routes/sessions.js +9 -7
- package/dist/web/routes/sessions.js.map +1 -1
- package/dist/web/routes/trace.d.ts.map +1 -1
- package/dist/web/routes/trace.js +2 -3
- package/dist/web/routes/trace.js.map +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +3 -2
- package/dist/web/server.js.map +1 -1
- package/package.json +12 -2
- package/scripts/postinstall.cjs +21 -0
- package/.claude/CLAUDE.md +0 -17
- package/.eslintrc.js +0 -23
- package/.prettierrc +0 -8
- package/ARCHITECTURE_ISSUES.md +0 -249
- package/CLAUDE.md +0 -265
- package/CLAUDE.md.backup +0 -488
- package/docs/concurrent-agents.md +0 -129
- package/docs/design/architecture-review-20260516.md +0 -232
- package/docs/design/fix-skills-data-and-set-leak-spec-20260516-1300.md +0 -219
- package/docs/design/h1-storage-aggregation-spec-20260518-1121.md +0 -299
- package/docs/design/h2-getdatabase-encapsulation-spec-20260518-1450.md +0 -191
- package/docs/design/h3-fallback-removal-spec-20260518-1245.md +0 -76
- package/docs/design/h4-index-dedup-spec-20260518-1230.md +0 -109
- package/docs/design/h6-services-migration-spec-20260518-1355.md +0 -82
- package/docs/design/hook-failure-queue-spec-20260516-1530.md +0 -204
- package/docs/design/l1-swarm-protocol-extract-spec-20260518-1605.md +0 -106
- package/docs/design/m10-forge-paths-spec-20260518-1320.md +0 -121
- package/docs/design/m2-m3-tool-input-spec-20260518-1425.md +0 -131
- package/docs/design/m7-routing-event-association-spec-20260518-1545.md +0 -103
- package/docs/design/project-path-gitroot-spec-20260518-1715.md +0 -134
- package/docs/design/refactor-phase1-spec-20260515-1600.md +0 -543
- package/docs/design/refactor-phase2-spec-20260515-1700.md +0 -424
- package/docs/design/skill-ai-upgrade-spec-20260518-1930.md +0 -297
- package/docs/design/task-active-gc-spec-20260518-1745.md +0 -146
- package/docs/design/tasks-list-filter-pagination-spec-20260518-0930.md +0 -208
- package/docs/implementation/daemon-skill-sync-changelog-20260518-2000.md +0 -22
- package/docs/implementation/fix-skills-data-and-set-leak-changelog-20260516-1300.md +0 -104
- package/docs/implementation/h1-storage-aggregation-changelog-20260518-1121.md +0 -82
- package/docs/implementation/h2-final-changelog-20260518-1530.md +0 -61
- package/docs/implementation/h2-phase1-safety-net-changelog-20260518-1450.md +0 -70
- package/docs/implementation/h2-phase2-operations-changelog-20260518-1450.md +0 -120
- package/docs/implementation/h2-phase3-callsites-changelog-20260518-1450.md +0 -71
- package/docs/implementation/h3-fallback-removal-changelog-20260518-1245.md +0 -71
- package/docs/implementation/h4-index-dedup-changelog-20260518-1230.md +0 -60
- package/docs/implementation/h6-services-migration-changelog-20260518-1355.md +0 -46
- package/docs/implementation/h7-m9-defaults-changelog-20260518-1300.md +0 -46
- package/docs/implementation/hook-failure-queue-changelog-20260516-1530.md +0 -196
- package/docs/implementation/hotfix-daemon-event-reject-20260516-1430.md +0 -56
- package/docs/implementation/l1-swarm-protocol-extract-changelog-20260518-1605.md +0 -45
- package/docs/implementation/l3-l4-daemon-perf-changelog-20260518-1410.md +0 -63
- package/docs/implementation/l6-l8-final-cleanup-changelog-20260518-1640.md +0 -38
- package/docs/implementation/m1-m4-m5-l7-cleanup-changelog-20260518-1310.md +0 -58
- package/docs/implementation/m10-forge-paths-changelog-20260518-1320.md +0 -60
- package/docs/implementation/m2-m3-tool-input-changelog-20260518-1425.md +0 -43
- package/docs/implementation/m6-m8-naming-shutdown-changelog-20260518-1340.md +0 -56
- package/docs/implementation/m7-routing-association-changelog-20260518-1545.md +0 -69
- package/docs/implementation/project-path-gitroot-changelog-20260518-1715.md +0 -63
- package/docs/implementation/refactor-phase1-changelog-20260515-1630.md +0 -354
- package/docs/implementation/refactor-phase2-changelog-20260515-1705.md +0 -421
- package/docs/implementation/skill-ai-upgrade-changelog-20260518-1930.md +0 -49
- package/docs/implementation/task-active-gc-changelog-20260518-1745.md +0 -35
- package/docs/implementation/task-title-summary-changelog-20260518-1130.md +0 -39
- package/docs/implementation/tasks-detail-back-loses-filters-changelog-20260518-1100.md +0 -22
- package/docs/implementation/tasks-list-filter-pagination-changelog-20260518-0930.md +0 -72
- package/docs/implementation/tasks-page-white-screen-hotfix-changelog-20260518-1015.md +0 -56
- package/docs/reviews/claudemd-template-sync.md +0 -54
- package/docs/reviews/task-title-summary.md +0 -92
- package/docs/reviews/tasks-detail-back-loses-filters.md +0 -58
- package/docs/reviews/tasks-filter-pagination.md +0 -80
- package/docs/reviews/tasks-page-white-screen-hotfix.md +0 -126
- package/docs/ruflo-learning-strategy.md +0 -322
- package/docs/skills-deduplication-analysis.md +0 -83
- package/docs/skills-multiformat-support.md +0 -177
- package/docs/skills-third-party.md +0 -183
- package/docs/testing/tasks-filter-pagination-test-report.md +0 -86
- package/forge +0 -321
- package/playwright.config.ts +0 -40
- package/scripts/demo-v2.ts +0 -91
- package/scripts/dev-daemon.sh +0 -232
- package/scripts/dev-web.ts +0 -109
- package/scripts/e2e-mcp-link.ts +0 -423
- package/scripts/e2e-methodology-quality.ts +0 -253
- package/scripts/e2e-routing.ts +0 -456
- package/scripts/e2e-user-methodology.ts +0 -326
- package/scripts/e2e-web-workflows.ts +0 -299
- package/scripts/migrate-legacy-to-dynamic.sql +0 -108
- package/scripts/regenerate-execution-docs.ts +0 -116
- package/scripts/sync-agent-skills.ts +0 -193
- package/scripts/test-hook.sh +0 -71
- package/scripts/verify-skill-loading.ts +0 -62
- package/src/claudemd/claudemd-generator.ts +0 -568
- package/src/claudemd/convention-extractor.ts +0 -69
- package/src/claudemd/index.ts +0 -35
- package/src/claudemd/persona-manager.ts +0 -88
- package/src/claudemd/resume-manager.ts +0 -236
- package/src/claudemd/tech-detector.ts +0 -220
- package/src/claudemd/templates/swarm-protocol.md +0 -222
- package/src/cli/commands/claudemd.ts +0 -84
- package/src/cli/commands/config.ts +0 -46
- package/src/cli/commands/daemon.ts +0 -310
- package/src/cli/commands/executions.ts +0 -115
- package/src/cli/commands/init.ts +0 -204
- package/src/cli/commands/logs.ts +0 -181
- package/src/cli/commands/mcp.ts +0 -242
- package/src/cli/commands/menu.ts +0 -357
- package/src/cli/commands/skills.ts +0 -328
- package/src/cli/commands/stats.ts +0 -73
- package/src/cli/commands/status.ts +0 -69
- package/src/cli/commands/template.ts +0 -77
- package/src/cli/commands/trace.ts +0 -148
- package/src/cli/index.ts +0 -42
- package/src/cli/init/hook-manager.ts +0 -132
- package/src/core/ai/provider.ts +0 -308
- package/src/core/ai/types.ts +0 -51
- package/src/core/config.ts +0 -124
- package/src/core/constants.ts +0 -67
- package/src/core/event-fields.ts +0 -32
- package/src/core/queue/index.ts +0 -192
- package/src/core/storage/base.ts +0 -302
- package/src/core/storage/events.ts +0 -434
- package/src/core/storage/injections.ts +0 -78
- package/src/core/storage/maintenance.ts +0 -59
- package/src/core/storage/migrations/002_add_skill_tracking.sql +0 -6
- package/src/core/storage/migrations/003_add_skill_invocations.sql +0 -23
- package/src/core/storage/performance-indexes.sql +0 -23
- package/src/core/storage/routing.ts +0 -322
- package/src/core/storage/rows.ts +0 -112
- package/src/core/storage/schema.sql +0 -224
- package/src/core/storage/sessions.ts +0 -168
- package/src/core/storage/skills.ts +0 -233
- package/src/core/storage/sqlite.ts +0 -293
- package/src/core/storage/tasks.ts +0 -318
- package/src/core/storage/token-usage.ts +0 -93
- package/src/core/types.ts +0 -181
- package/src/core/utils/error-handler.ts +0 -257
- package/src/core/utils/forge-resume-block.ts +0 -74
- package/src/core/utils/format.ts +0 -69
- package/src/core/utils/git.ts +0 -23
- package/src/core/utils/logger.ts +0 -134
- package/src/core/utils/lru-cache.ts +0 -54
- package/src/core/utils/path.ts +0 -19
- package/src/core/utils/session.ts +0 -26
- package/src/core/utils/time.ts +0 -37
- package/src/core/utils/token-tracker.ts +0 -97
- package/src/daemon/event-parser.ts +0 -36
- package/src/daemon/handlers/history-exporter.ts +0 -117
- package/src/daemon/handlers/post-tool-use.ts +0 -54
- package/src/daemon/handlers/stop.ts +0 -208
- package/src/daemon/handlers/user-prompt.ts +0 -178
- package/src/daemon/hook-sync.ts +0 -91
- package/src/daemon/index.ts +0 -312
- package/src/daemon/launchd/com.claude-forge.daemon.plist.template +0 -47
- package/src/daemon/launchd-installer.ts +0 -260
- package/src/daemon/lifecycle.ts +0 -128
- package/src/daemon/router.ts +0 -40
- package/src/daemon/server.ts +0 -196
- package/src/daemon/services/task-segmenter.ts +0 -112
- package/src/daemon/skill-sync.ts +0 -88
- package/src/hooks/hook-lib.sh +0 -118
- package/src/hooks/notification.sh +0 -35
- package/src/hooks/post-tool-use.sh +0 -61
- package/src/hooks/pre-tool-use.sh +0 -63
- package/src/hooks/stop.sh +0 -43
- package/src/hooks/user-prompt-submit.sh +0 -69
- package/src/mcp/server.ts +0 -322
- package/src/skills/index.ts +0 -2
- package/src/skills/invocation-guard.ts +0 -177
- package/src/skills/matcher.ts +0 -148
- package/src/skills/official/code-simplifier.md +0 -52
- package/src/skills/official/find-skills.md +0 -142
- package/src/skills/official/official-api-design.md +0 -30
- package/src/skills/official/official-architecture-decision.md +0 -41
- package/src/skills/official/official-bmad.md +0 -118
- package/src/skills/official/official-db-schema-design.md +0 -34
- package/src/skills/official/official-debug.md +0 -25
- package/src/skills/official/official-doc-driven.md +0 -31
- package/src/skills/official/official-harness-engineering.md +0 -108
- package/src/skills/official/official-performance-optimization.md +0 -30
- package/src/skills/official/official-pr-review.md +0 -35
- package/src/skills/official/official-release-checklist.md +0 -30
- package/src/skills/official/official-security-hardening.md +0 -32
- package/src/skills/official/official-spec-driven-design.md +0 -31
- package/src/skills/official/planning-with-files.md +0 -241
- package/src/skills/official/ui-ux-pro-max.md +0 -105
- package/src/skills/official/webapp-testing.md +0 -96
- package/src/skills/official-skills.ts +0 -89
- package/src/skills/registry.ts +0 -355
- package/src/skills/semantic-matcher.ts +0 -234
- package/src/skills/tools/pipeline-suggest.ts +0 -226
- package/src/skills/tools/skill-invoke.ts +0 -168
- package/src/skills/tools/skill-list.ts +0 -59
- package/src/skills/upgrade-engine.ts +0 -541
- package/src/skills/upgrade-prompt.ts +0 -84
- package/src/templates/go.yaml +0 -53
- package/src/templates/python.yaml +0 -59
- package/src/templates/react.yaml +0 -55
- package/src/templates/template-manager.ts +0 -170
- package/src/web/analytics/anti-pattern-detector.ts +0 -367
- package/src/web/analytics/drift-detector.ts +0 -219
- package/src/web/analytics/weekly-report.ts +0 -431
- package/src/web/auth-middleware.ts +0 -54
- package/src/web/routes/_helpers.ts +0 -34
- package/src/web/routes/ai.ts +0 -204
- package/src/web/routes/auth.ts +0 -22
- package/src/web/routes/drift.ts +0 -25
- package/src/web/routes/error-handler.ts +0 -120
- package/src/web/routes/events.ts +0 -47
- package/src/web/routes/insights.ts +0 -43
- package/src/web/routes/patch.ts +0 -117
- package/src/web/routes/reports.ts +0 -34
- package/src/web/routes/rules.ts +0 -76
- package/src/web/routes/sessions.ts +0 -250
- package/src/web/routes/skill-stats.ts +0 -92
- package/src/web/routes/skills.ts +0 -350
- package/src/web/routes/static.ts +0 -67
- package/src/web/routes/stats.ts +0 -50
- package/src/web/routes/status.ts +0 -30
- package/src/web/routes/tasks.ts +0 -193
- package/src/web/routes/token-usage.ts +0 -20
- package/src/web/routes/trace.ts +0 -126
- package/src/web/routes/types.ts +0 -57
- package/src/web/server.ts +0 -134
- package/src/web/ssrf-guard.ts +0 -112
- package/src/web/static/index.html +0 -3251
- package/src/web/static/vendor/chart.umd.min.js +0 -20
- package/tests/e2e/dashboard.spec.ts +0 -205
- package/tests/e2e/routing-skill-e2e.test.ts +0 -39
- package/tests/helpers/mock-ai.ts +0 -92
- package/tests/helpers/mock-storage.ts +0 -159
- package/tests/integration/claudemd-generator.test.ts +0 -90
- package/tests/integration/queue-replay.integration.test.ts +0 -193
- package/tests/integration/tasks-filter.integration.test.ts +0 -154
- package/tests/integration/web-analytics.integration.test.ts +0 -133
- package/tests/integration/web-stats.integration.test.ts +0 -135
- package/tests/integration/web-trace.integration.test.ts +0 -175
- package/tests/performance/database.benchmark.ts +0 -161
- package/tests/semantic-matcher.test.ts +0 -99
- package/tests/skill-matcher.test.ts +0 -110
- package/tests/unit/ai-provider-retry.test.ts +0 -194
- package/tests/unit/ai-provider-vision.test.ts +0 -224
- package/tests/unit/claudemd-generator.test.ts +0 -68
- package/tests/unit/cli-mcp.test.ts +0 -141
- package/tests/unit/core/forge-paths.test.ts +0 -99
- package/tests/unit/daemon/hook-sync.test.ts +0 -71
- package/tests/unit/daemon/post-tool-use.test.ts +0 -121
- package/tests/unit/daemon/skill-sync.test.ts +0 -75
- package/tests/unit/daemon/stop-handler-behavior-summary.test.ts +0 -202
- package/tests/unit/daemon/task-segmenter-recover.test.ts +0 -84
- package/tests/unit/event-fields.test.ts +0 -88
- package/tests/unit/event-parser.test.ts +0 -55
- package/tests/unit/handlers.test.ts +0 -171
- package/tests/unit/hooks/resolve-project-path.test.ts +0 -122
- package/tests/unit/invocation-guard.test.ts +0 -125
- package/tests/unit/queue.test.ts +0 -272
- package/tests/unit/router.test.ts +0 -138
- package/tests/unit/security.test.ts +0 -128
- package/tests/unit/skill-invocations-workflow.test.ts +0 -495
- package/tests/unit/skill-registry.test.ts +0 -94
- package/tests/unit/skills/invocation-guard-ttl.test.ts +0 -211
- package/tests/unit/skills/official-skills-loader.test.ts +0 -126
- package/tests/unit/skills/registry-multiformat.test.ts +0 -92
- package/tests/unit/skills/upgrade-engine-parse.test.ts +0 -138
- package/tests/unit/skills/upgrade-engine.test.ts +0 -401
- package/tests/unit/skills/upgrade-prompt.test.ts +0 -89
- package/tests/unit/socket-server.test.ts +0 -183
- package/tests/unit/storage/event-operations-aggregates.test.ts +0 -342
- package/tests/unit/storage/migration-idempotent.test.ts +0 -304
- package/tests/unit/storage/routing-aggregates.test.ts +0 -276
- package/tests/unit/storage/routing.test.ts +0 -117
- package/tests/unit/storage/schema-missing.test.ts +0 -81
- package/tests/unit/storage/session-operations-aggregates.test.ts +0 -120
- package/tests/unit/storage/sessions-aggregate.test.ts +0 -435
- package/tests/unit/storage/skill-operations-counts.test.ts +0 -106
- package/tests/unit/storage/skills-aggregates.test.ts +0 -104
- package/tests/unit/storage/sqlite-refactor-harness.test.ts +0 -314
- package/tests/unit/storage/task-operations-counts.test.ts +0 -46
- package/tests/unit/storage/tasks-getById.test.ts +0 -343
- package/tests/unit/storage/tasks-stale-gc.test.ts +0 -86
- package/tests/unit/storage.test.ts +0 -172
- package/tests/unit/token-usage.test.ts +0 -144
- package/tests/unit/type-guards.test.ts +0 -201
- package/tests/unit/utils/format.test.ts +0 -189
- package/tests/unit/utils/session.test.ts +0 -89
- package/tests/unit/utils/time.test.ts +0 -112
- package/tests/unit/web/navigation-back-contract.test.ts +0 -134
- package/tests/unit/web/routes-auth.test.ts +0 -93
- package/tests/unit/web/routes-events.test.ts +0 -101
- package/tests/unit/web/routes-rules.test.ts +0 -182
- package/tests/unit/web/routes-sessions.test.ts +0 -181
- package/tests/unit/web/routes-skill-stats.test.ts +0 -179
- package/tests/unit/web/routes-stats.test.ts +0 -92
- package/tests/unit/web/routes-tasks.test.ts +0 -385
- package/tests/unit/web/task-title-contract.test.ts +0 -210
- package/tests/unit/web/tasks-component-contract.test.ts +0 -179
- package/tsconfig.json +0 -22
- package/vitest.config.ts +0 -21
- package/vitest.integration.config.ts +0 -16
- package/web/CLAUDE.md +0 -20
- package/web/index.html +0 -13
- package/web/package-lock.json +0 -4854
- package/web/package.json +0 -35
- package/web/postcss.config.js +0 -6
- package/web/src/App.tsx +0 -110
- package/web/src/components/CodeBlock.tsx +0 -31
- package/web/src/components/Confirm.tsx +0 -96
- package/web/src/components/Drawer.tsx +0 -60
- package/web/src/components/Layout.tsx +0 -145
- package/web/src/components/MarkdownRenderer.tsx +0 -77
- package/web/src/components/SearchInput.tsx +0 -31
- package/web/src/components/SessionDetailContent.tsx +0 -157
- package/web/src/components/Toast.tsx +0 -92
- package/web/src/index.css +0 -19
- package/web/src/main.tsx +0 -31
- package/web/src/pages/AIConfig.tsx +0 -233
- package/web/src/pages/Dashboard.tsx +0 -572
- package/web/src/pages/Events.tsx +0 -271
- package/web/src/pages/Reports.tsx +0 -428
- package/web/src/pages/SessionDetail.tsx +0 -162
- package/web/src/pages/Sessions.tsx +0 -205
- package/web/src/pages/Skills.tsx +0 -180
- package/web/src/pages/TaskDetail.tsx +0 -515
- package/web/src/pages/Tasks.tsx +0 -415
- package/web/src/utils/auth.ts +0 -59
- package/web/src/utils/export.ts +0 -54
- package/web/src/utils/navigation.ts +0 -25
- package/web/src/utils/task-title.ts +0 -49
- package/web/src/utils/time.ts +0 -13
- package/web/tailwind.config.js +0 -11
- package/web/tsconfig.json +0 -21
- package/web/tsconfig.node.json +0 -10
- package/web/vite.config.ts +0 -76
- package/winspan-claude-forge-8.43.0.tgz +0 -0
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* macOS LaunchAgent integration for the Forge daemon.
|
|
3
|
-
*
|
|
4
|
-
* Renders the plist template, installs/uninstalls the LaunchAgent via launchctl,
|
|
5
|
-
* and reports status. Intended to give the daemon supervisor-grade auto-restart
|
|
6
|
-
* behavior on darwin without depending on systemd.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import fs from 'fs';
|
|
10
|
-
import os from 'os';
|
|
11
|
-
import path from 'path';
|
|
12
|
-
import { execSync, execFileSync } from 'child_process';
|
|
13
|
-
import { fileURLToPath } from 'url';
|
|
14
|
-
|
|
15
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
|
|
17
|
-
export const LAUNCHD_LABEL = 'com.claude-forge.daemon';
|
|
18
|
-
export const LAUNCH_AGENTS_DIR = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
19
|
-
export const PLIST_PATH = path.join(LAUNCH_AGENTS_DIR, `${LAUNCHD_LABEL}.plist`);
|
|
20
|
-
|
|
21
|
-
export const TEMPLATE_PATH = path.resolve(
|
|
22
|
-
__dirname,
|
|
23
|
-
'launchd',
|
|
24
|
-
'com.claude-forge.daemon.plist.template'
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
export function ensureDarwin(): void {
|
|
28
|
-
if (process.platform !== 'darwin') {
|
|
29
|
-
throw new Error(
|
|
30
|
-
`LaunchAgent commands are only supported on macOS (current platform: ${process.platform}). ` +
|
|
31
|
-
`Use './scripts/dev-daemon.sh' or 'cf daemon start' on other platforms.`
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function getUid(): number {
|
|
37
|
-
if (typeof process.getuid === 'function') {
|
|
38
|
-
return process.getuid();
|
|
39
|
-
}
|
|
40
|
-
// Fallback: shell out (process.getuid is undefined on Windows but always defined on darwin).
|
|
41
|
-
return parseInt(execSync('id -u', { encoding: 'utf-8' }).trim(), 10);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface RenderOptions {
|
|
45
|
-
/**
|
|
46
|
-
* If true, points ProgramArguments at the local development build
|
|
47
|
-
* (`<repo>/dist/daemon/index.js` resolved from this module's location).
|
|
48
|
-
* Otherwise, resolves the daemon entry from the globally-installed package.
|
|
49
|
-
*/
|
|
50
|
-
dev?: boolean;
|
|
51
|
-
/**
|
|
52
|
-
* Optional explicit path to the daemon entrypoint.
|
|
53
|
-
* Takes precedence over `dev`.
|
|
54
|
-
*/
|
|
55
|
-
daemonPath?: string;
|
|
56
|
-
/**
|
|
57
|
-
* Optional explicit node path. Defaults to `process.execPath`.
|
|
58
|
-
*/
|
|
59
|
-
nodePath?: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Resolve the path to `dist/daemon/index.js`.
|
|
64
|
-
*
|
|
65
|
-
* - In dev mode: resolves relative to this module (works in both src and dist).
|
|
66
|
-
* - In production mode: prefers a globally-installed `claude-forge` binary,
|
|
67
|
-
* falling back to the path resolved from this module.
|
|
68
|
-
*/
|
|
69
|
-
export function resolveDaemonPath(opts: RenderOptions = {}): string {
|
|
70
|
-
if (opts.daemonPath) {
|
|
71
|
-
if (!fs.existsSync(opts.daemonPath)) {
|
|
72
|
-
throw new Error(`Provided daemon path does not exist: ${opts.daemonPath}`);
|
|
73
|
-
}
|
|
74
|
-
return opts.daemonPath;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// From `dist/daemon/launchd-installer.js` -> `dist/daemon/index.js`
|
|
78
|
-
// From `src/daemon/launchd-installer.ts` -> would resolve to src path; but we only ship dist.
|
|
79
|
-
const localGuess = path.resolve(__dirname, 'index.js');
|
|
80
|
-
|
|
81
|
-
if (opts.dev) {
|
|
82
|
-
if (!fs.existsSync(localGuess)) {
|
|
83
|
-
throw new Error(
|
|
84
|
-
`Local daemon entry not found at ${localGuess}. Run 'npm run build' first.`
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
return localGuess;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Production mode: try to find global install via `which claude-forge`.
|
|
91
|
-
try {
|
|
92
|
-
const cliPath = execSync('command -v claude-forge', { encoding: 'utf-8' }).trim();
|
|
93
|
-
if (cliPath) {
|
|
94
|
-
const resolved = fs.realpathSync(cliPath);
|
|
95
|
-
// The CLI lives at <pkg>/dist/cli/index.js; daemon at <pkg>/dist/daemon/index.js.
|
|
96
|
-
const pkgRoot = path.resolve(path.dirname(resolved), '..');
|
|
97
|
-
const candidate = path.join(pkgRoot, 'daemon', 'index.js');
|
|
98
|
-
if (fs.existsSync(candidate)) {
|
|
99
|
-
return candidate;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
} catch {
|
|
103
|
-
// Ignore — fall through to local guess.
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (fs.existsSync(localGuess)) {
|
|
107
|
-
return localGuess;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
throw new Error(
|
|
111
|
-
`Could not locate daemon entry. Install claude-forge globally (npm i -g @winspan/claude-forge) ` +
|
|
112
|
-
`or run with --dev after 'npm run build'.`
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function renderPlist(opts: RenderOptions = {}): string {
|
|
117
|
-
if (!fs.existsSync(TEMPLATE_PATH)) {
|
|
118
|
-
throw new Error(`plist template missing at ${TEMPLATE_PATH}. Did you run 'npm run build'?`);
|
|
119
|
-
}
|
|
120
|
-
const template = fs.readFileSync(TEMPLATE_PATH, 'utf-8');
|
|
121
|
-
const nodePath = opts.nodePath ?? process.execPath;
|
|
122
|
-
const daemonPath = resolveDaemonPath(opts);
|
|
123
|
-
const home = os.homedir();
|
|
124
|
-
|
|
125
|
-
return template
|
|
126
|
-
.replace(/{{NODE_PATH}}/g, nodePath)
|
|
127
|
-
.replace(/{{DAEMON_PATH}}/g, daemonPath)
|
|
128
|
-
.replace(/{{HOME}}/g, home);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function runLaunchctl(args: string[]): { stdout: string; stderr: string; code: number } {
|
|
132
|
-
try {
|
|
133
|
-
const stdout = execFileSync('launchctl', args, {
|
|
134
|
-
encoding: 'utf-8',
|
|
135
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
136
|
-
});
|
|
137
|
-
return { stdout, stderr: '', code: 0 };
|
|
138
|
-
} catch (err) {
|
|
139
|
-
const e = err as NodeJS.ErrnoException & { stdout?: Buffer; stderr?: Buffer; status?: number };
|
|
140
|
-
return {
|
|
141
|
-
stdout: e.stdout?.toString() ?? '',
|
|
142
|
-
stderr: e.stderr?.toString() ?? e.message ?? '',
|
|
143
|
-
code: e.status ?? 1,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export interface InstallResult {
|
|
149
|
-
plistPath: string;
|
|
150
|
-
nodePath: string;
|
|
151
|
-
daemonPath: string;
|
|
152
|
-
reloaded: boolean;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function install(opts: RenderOptions = {}): InstallResult {
|
|
156
|
-
ensureDarwin();
|
|
157
|
-
fs.mkdirSync(LAUNCH_AGENTS_DIR, { recursive: true });
|
|
158
|
-
|
|
159
|
-
const uid = getUid();
|
|
160
|
-
const target = `gui/${uid}/${LAUNCHD_LABEL}`;
|
|
161
|
-
|
|
162
|
-
// Idempotent: bootout existing service before re-installing.
|
|
163
|
-
let reloaded = false;
|
|
164
|
-
if (fs.existsSync(PLIST_PATH)) {
|
|
165
|
-
const r = runLaunchctl(['bootout', `gui/${uid}`, PLIST_PATH]);
|
|
166
|
-
if (r.code === 0 || /No such process|Could not find specified service/.test(r.stderr)) {
|
|
167
|
-
reloaded = true;
|
|
168
|
-
} else {
|
|
169
|
-
// Non-fatal; proceed and let bootstrap surface real errors.
|
|
170
|
-
reloaded = true;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const rendered = renderPlist(opts);
|
|
175
|
-
fs.writeFileSync(PLIST_PATH, rendered, { mode: 0o644 });
|
|
176
|
-
|
|
177
|
-
const bootstrap = runLaunchctl(['bootstrap', `gui/${uid}`, PLIST_PATH]);
|
|
178
|
-
if (bootstrap.code !== 0) {
|
|
179
|
-
throw new Error(
|
|
180
|
-
`launchctl bootstrap failed (exit ${bootstrap.code}): ${bootstrap.stderr.trim() || bootstrap.stdout.trim()}\n` +
|
|
181
|
-
`Service target: ${target}`
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Best-effort kickstart so the daemon comes up immediately.
|
|
186
|
-
runLaunchctl(['kickstart', '-k', target]);
|
|
187
|
-
|
|
188
|
-
// Read back rendered values to report.
|
|
189
|
-
const nodePath = opts.nodePath ?? process.execPath;
|
|
190
|
-
const daemonPath = resolveDaemonPath(opts);
|
|
191
|
-
|
|
192
|
-
return { plistPath: PLIST_PATH, nodePath, daemonPath, reloaded };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export interface UninstallResult {
|
|
196
|
-
removedPlist: boolean;
|
|
197
|
-
bootedOut: boolean;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export function uninstall(): UninstallResult {
|
|
201
|
-
ensureDarwin();
|
|
202
|
-
const uid = getUid();
|
|
203
|
-
|
|
204
|
-
let bootedOut = false;
|
|
205
|
-
if (fs.existsSync(PLIST_PATH)) {
|
|
206
|
-
const r = runLaunchctl(['bootout', `gui/${uid}`, PLIST_PATH]);
|
|
207
|
-
bootedOut =
|
|
208
|
-
r.code === 0 || /No such process|Could not find specified service/.test(r.stderr);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
let removedPlist = false;
|
|
212
|
-
if (fs.existsSync(PLIST_PATH)) {
|
|
213
|
-
fs.unlinkSync(PLIST_PATH);
|
|
214
|
-
removedPlist = true;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return { bootedOut, removedPlist };
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export interface LaunchdStatus {
|
|
221
|
-
installed: boolean;
|
|
222
|
-
loaded: boolean;
|
|
223
|
-
pid: number | null;
|
|
224
|
-
lastExitCode: number | null;
|
|
225
|
-
state: string | null;
|
|
226
|
-
raw: string;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
export function status(): LaunchdStatus {
|
|
230
|
-
ensureDarwin();
|
|
231
|
-
const uid = getUid();
|
|
232
|
-
const target = `gui/${uid}/${LAUNCHD_LABEL}`;
|
|
233
|
-
const installed = fs.existsSync(PLIST_PATH);
|
|
234
|
-
|
|
235
|
-
const r = runLaunchctl(['print', target]);
|
|
236
|
-
if (r.code !== 0) {
|
|
237
|
-
return {
|
|
238
|
-
installed,
|
|
239
|
-
loaded: false,
|
|
240
|
-
pid: null,
|
|
241
|
-
lastExitCode: null,
|
|
242
|
-
state: null,
|
|
243
|
-
raw: r.stderr.trim() || r.stdout.trim(),
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const out = r.stdout;
|
|
248
|
-
const pidMatch = out.match(/^\s*pid\s*=\s*(\d+)/m);
|
|
249
|
-
const stateMatch = out.match(/^\s*state\s*=\s*([\w-]+)/m);
|
|
250
|
-
const exitMatch = out.match(/^\s*last exit code\s*=\s*(-?\d+)/mi);
|
|
251
|
-
|
|
252
|
-
return {
|
|
253
|
-
installed,
|
|
254
|
-
loaded: true,
|
|
255
|
-
pid: pidMatch ? parseInt(pidMatch[1], 10) : null,
|
|
256
|
-
state: stateMatch ? stateMatch[1] : null,
|
|
257
|
-
lastExitCode: exitMatch ? parseInt(exitMatch[1], 10) : null,
|
|
258
|
-
raw: out,
|
|
259
|
-
};
|
|
260
|
-
}
|
package/src/daemon/lifecycle.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import crypto from 'crypto';
|
|
4
|
-
import { execSync, execFileSync } from 'child_process';
|
|
5
|
-
import { logger } from '../core/utils/logger.js';
|
|
6
|
-
import { FORGE_PATHS } from '../core/constants.js';
|
|
7
|
-
|
|
8
|
-
const FORGE_HOME = FORGE_PATHS.home();
|
|
9
|
-
const TOKEN_FILE = FORGE_PATHS.daemonToken();
|
|
10
|
-
const PID_FILE = FORGE_PATHS.daemonPid();
|
|
11
|
-
|
|
12
|
-
export function writePidFile(): void {
|
|
13
|
-
if (!fs.existsSync(FORGE_HOME)) {
|
|
14
|
-
fs.mkdirSync(FORGE_HOME, { recursive: true });
|
|
15
|
-
}
|
|
16
|
-
fs.writeFileSync(PID_FILE, String(process.pid));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function removePidFile(): void {
|
|
20
|
-
if (fs.existsSync(PID_FILE)) {
|
|
21
|
-
fs.unlinkSync(PID_FILE);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function readPid(): number | null {
|
|
26
|
-
if (!fs.existsSync(PID_FILE)) {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim(), 10);
|
|
32
|
-
|
|
33
|
-
// 检查进程是否存活
|
|
34
|
-
try {
|
|
35
|
-
process.kill(pid, 0);
|
|
36
|
-
return pid;
|
|
37
|
-
} catch {
|
|
38
|
-
// 进程不存在,清理过期 PID 文件
|
|
39
|
-
removePidFile();
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
} catch {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function isRunning(): boolean {
|
|
48
|
-
return readPid() !== null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function getSocketPath(): string {
|
|
52
|
-
return FORGE_PATHS.daemonSocket();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function cleanSocket(): void {
|
|
56
|
-
const socketPath = getSocketPath();
|
|
57
|
-
if (fs.existsSync(socketPath)) {
|
|
58
|
-
fs.unlinkSync(socketPath);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** daemon 启动时生成随机 auth token,写入 chmod 600 文件 */
|
|
63
|
-
export function writeAuthToken(): string {
|
|
64
|
-
const token = crypto.randomBytes(32).toString('hex');
|
|
65
|
-
if (!fs.existsSync(FORGE_HOME)) {
|
|
66
|
-
fs.mkdirSync(FORGE_HOME, { recursive: true });
|
|
67
|
-
}
|
|
68
|
-
fs.writeFileSync(TOKEN_FILE, token, { mode: 0o600 });
|
|
69
|
-
return token;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** 读取当前 auth token */
|
|
73
|
-
export function readAuthToken(): string | null {
|
|
74
|
-
if (!fs.existsSync(TOKEN_FILE)) return null;
|
|
75
|
-
try {
|
|
76
|
-
return fs.readFileSync(TOKEN_FILE, 'utf-8').trim();
|
|
77
|
-
} catch {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** daemon 关闭时清理 token 文件 */
|
|
83
|
-
export function removeAuthToken(): void {
|
|
84
|
-
if (fs.existsSync(TOKEN_FILE)) {
|
|
85
|
-
fs.unlinkSync(TOKEN_FILE);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Sync MCP registration token after daemon restart.
|
|
91
|
-
*
|
|
92
|
-
* Problem: Each daemon start generates a new token, but the MCP registration
|
|
93
|
-
* in Claude Code still has the old token → 401 → "Failed to connect".
|
|
94
|
-
*
|
|
95
|
-
* Solution: After writing the new token, re-register the MCP server with
|
|
96
|
-
* the updated Bearer header. This is a best-effort operation — if Claude CLI
|
|
97
|
-
* is not installed or registration fails, we log a warning and continue.
|
|
98
|
-
*/
|
|
99
|
-
export function syncMcpToken(token: string, port: number): void {
|
|
100
|
-
try {
|
|
101
|
-
execSync('command -v claude', { stdio: 'ignore' });
|
|
102
|
-
} catch {
|
|
103
|
-
return; // Claude CLI not installed, skip
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
const checkOutput = execSync('claude mcp get claude-forge', {
|
|
108
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
109
|
-
encoding: 'utf-8',
|
|
110
|
-
});
|
|
111
|
-
if (!checkOutput.includes('claude-forge')) return; // Not registered
|
|
112
|
-
} catch {
|
|
113
|
-
return; // Not registered, skip
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const mcpUrl = `http://127.0.0.1:${port}/mcp`;
|
|
117
|
-
try {
|
|
118
|
-
execFileSync('claude', ['mcp', 'remove', 'claude-forge', '-s', 'user'], { stdio: 'ignore' });
|
|
119
|
-
execFileSync('claude', [
|
|
120
|
-
'mcp', 'add', '--transport', 'http', '-s', 'user',
|
|
121
|
-
'claude-forge', mcpUrl,
|
|
122
|
-
'-H', `Authorization: Bearer ${token}`,
|
|
123
|
-
], { stdio: 'ignore' });
|
|
124
|
-
logger.info('[Lifecycle] MCP token synced successfully');
|
|
125
|
-
} catch (err) {
|
|
126
|
-
logger.warn(`[Lifecycle] MCP token sync failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
127
|
-
}
|
|
128
|
-
}
|
package/src/daemon/router.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { ForgeEvent, HookResult } from '../core/types.js';
|
|
2
|
-
import { isUserPromptSubmit, isPostToolUse, isStop } from '../core/types.js';
|
|
3
|
-
import type { UserPromptHandler } from './handlers/user-prompt.js';
|
|
4
|
-
import type { PostToolUseHandler } from './handlers/post-tool-use.js';
|
|
5
|
-
import type { StopHandler } from './handlers/stop.js';
|
|
6
|
-
import { logger } from '../core/utils/logger.js';
|
|
7
|
-
import { formatError } from '../core/utils/format.js';
|
|
8
|
-
import { truncateSessionId } from '../core/utils/session.js';
|
|
9
|
-
|
|
10
|
-
export interface Handlers {
|
|
11
|
-
UserPromptSubmit: UserPromptHandler;
|
|
12
|
-
PostToolUse: PostToolUseHandler;
|
|
13
|
-
Stop: StopHandler;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Dispatch an event to the matching handler with hook-type narrowing.
|
|
18
|
-
*
|
|
19
|
-
* The narrow union (PreToolUseEvent / PostToolUseEvent / ...) is enforced at
|
|
20
|
-
* the handler entry, not at storage / wire level — so handlers can rely on
|
|
21
|
-
* required fields without `!` assertions.
|
|
22
|
-
*
|
|
23
|
-
* Error boundary: catches and logs handler errors to prevent daemon crashes.
|
|
24
|
-
*/
|
|
25
|
-
export async function routeEvent(event: ForgeEvent, handlers: Handlers): Promise<HookResult | void> {
|
|
26
|
-
try {
|
|
27
|
-
if (isUserPromptSubmit(event)) return await handlers.UserPromptSubmit.handle(event);
|
|
28
|
-
if (isPostToolUse(event)) return await handlers.PostToolUse.handle(event);
|
|
29
|
-
if (isStop(event)) return await handlers.Stop.handle(event);
|
|
30
|
-
// PreToolUse / Notification: handled inline in daemon/index.ts (no dedicated handler).
|
|
31
|
-
} catch (err) {
|
|
32
|
-
logger.error(
|
|
33
|
-
`[Router] Handler error for ${event.hook_type}: ${formatError(err)} ` +
|
|
34
|
-
`(session: ${truncateSessionId(event.session_id)})`
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
// Return safe default to prevent daemon crash
|
|
38
|
-
return { allow: true };
|
|
39
|
-
}
|
|
40
|
-
}
|
package/src/daemon/server.ts
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import net from 'net';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import { EventParser } from './event-parser.js';
|
|
4
|
-
import { logger } from '../core/utils/logger.js';
|
|
5
|
-
import { formatError, truncateString } from '../core/utils/format.js';
|
|
6
|
-
|
|
7
|
-
export interface HookResponse {
|
|
8
|
-
allow: boolean;
|
|
9
|
-
reason?: string;
|
|
10
|
-
additionalContext?: string;
|
|
11
|
-
systemMessage?: string;
|
|
12
|
-
hookSpecificOutput?: {
|
|
13
|
-
hookEventName: string;
|
|
14
|
-
permissionDecision: 'allow' | 'deny' | 'ask' | 'defer';
|
|
15
|
-
permissionDecisionReason?: string;
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type EventHandler = (
|
|
20
|
-
event: import('../core/types.js').ForgeEvent
|
|
21
|
-
) => HookResponse | void | Promise<HookResponse | void>;
|
|
22
|
-
|
|
23
|
-
export class SocketServer {
|
|
24
|
-
private server: net.Server;
|
|
25
|
-
private parser: EventParser;
|
|
26
|
-
private handler: EventHandler;
|
|
27
|
-
private authToken: string | null;
|
|
28
|
-
private static readonly MAX_BUFFER_SIZE = 512 * 1024; // 512KB
|
|
29
|
-
|
|
30
|
-
constructor(socketPath: string, handler: EventHandler, authToken?: string) {
|
|
31
|
-
this.parser = new EventParser();
|
|
32
|
-
this.handler = handler;
|
|
33
|
-
this.authToken = authToken ?? null;
|
|
34
|
-
|
|
35
|
-
this.server = net.createServer(this.handleConnection.bind(this));
|
|
36
|
-
this.server.listen(socketPath, () => {
|
|
37
|
-
// 限制 socket 文件仅当前用户可读写,防止同机其他用户注入伪造事件
|
|
38
|
-
try {
|
|
39
|
-
fs.chmodSync(socketPath, 0o600);
|
|
40
|
-
} catch (err) {
|
|
41
|
-
logger.warn(`[Socket] 设置权限失败:${formatError(err)}`);
|
|
42
|
-
}
|
|
43
|
-
logger.info(`Socket 服务器已监听:${socketPath}`);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
this.server.on('error', (err) => {
|
|
47
|
-
logger.error(`Socket 服务器错误:${formatError(err)}`);
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
private handleConnection(socket: net.Socket): void {
|
|
52
|
-
let buffer = '';
|
|
53
|
-
let connectionTimeout: NodeJS.Timeout | null = null;
|
|
54
|
-
|
|
55
|
-
// 设置 10 秒超时,防止连接挂起
|
|
56
|
-
connectionTimeout = setTimeout(() => {
|
|
57
|
-
if (buffer.length > 0) {
|
|
58
|
-
logger.warn(`连接超时,数据不完整(${buffer.length} 字节)`);
|
|
59
|
-
}
|
|
60
|
-
socket.destroy();
|
|
61
|
-
}, 10000);
|
|
62
|
-
|
|
63
|
-
socket.on('data', async (chunk) => {
|
|
64
|
-
buffer += chunk.toString();
|
|
65
|
-
|
|
66
|
-
// 主动检查 buffer 大小,防止内存溢出
|
|
67
|
-
if (buffer.length > SocketServer.MAX_BUFFER_SIZE) {
|
|
68
|
-
logger.warn(`事件过大(${buffer.length} 字节),关闭连接`);
|
|
69
|
-
buffer = '';
|
|
70
|
-
if (connectionTimeout) clearTimeout(connectionTimeout);
|
|
71
|
-
socket.destroy();
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
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
|
-
}
|
|
85
|
-
|
|
86
|
-
// 取换行前的完整消息;忽略行后可能存在的多余数据(hook 单连接一事件)
|
|
87
|
-
const message = buffer.slice(0, newlineIdx);
|
|
88
|
-
buffer = '';
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
await this.processMessage(socket, message);
|
|
92
|
-
} finally {
|
|
93
|
-
if (connectionTimeout) clearTimeout(connectionTimeout);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
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 {
|
|
107
|
-
if (connectionTimeout) clearTimeout(connectionTimeout);
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
socket.on('error', (err) => {
|
|
112
|
-
logger.debug(`Socket 连接错误:${formatError(err)}`);
|
|
113
|
-
if (connectionTimeout) clearTimeout(connectionTimeout);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
socket.on('close', () => {
|
|
117
|
-
if (connectionTimeout) clearTimeout(connectionTimeout);
|
|
118
|
-
});
|
|
119
|
-
}
|
|
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
|
-
|
|
168
|
-
/**
|
|
169
|
-
* 判断内容是否有意义,过滤空字符串、纯标点/符号等
|
|
170
|
-
*/
|
|
171
|
-
private isValidContent(content: string): boolean {
|
|
172
|
-
if (!content) return false;
|
|
173
|
-
const trimmed = content.trim();
|
|
174
|
-
if (trimmed.length === 0) return false;
|
|
175
|
-
|
|
176
|
-
const stripped = trimmed
|
|
177
|
-
.split('\n')
|
|
178
|
-
.map(line => line.replace(/^>\s*/, '').trim())
|
|
179
|
-
.join('\n')
|
|
180
|
-
.trim();
|
|
181
|
-
|
|
182
|
-
if (stripped.length === 0) return false;
|
|
183
|
-
if (!/[\p{L}\p{N}]/u.test(stripped)) return false;
|
|
184
|
-
|
|
185
|
-
return true;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
close(): Promise<void> {
|
|
189
|
-
return new Promise((resolve) => {
|
|
190
|
-
this.server.close(() => {
|
|
191
|
-
logger.info('Socket 服务器已关闭');
|
|
192
|
-
resolve();
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|