@sanqianx/project-knowledge 5.0.24 → 5.0.26
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/.agents/plugins/marketplace.json +20 -20
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +1361 -1353
- package/LICENSE +201 -201
- package/NOTICE +5 -5
- package/README.md +349 -349
- package/_modules/knowledge-engine/package.json +1 -1
- package/_modules/knowledge-engine/src/coordinator.js +48 -3
- package/_modules/knowledge-engine/src/runtime/automation-config.js +140 -140
- package/_modules/knowledge-engine/src/runtime/commit-conversation-binder.js +238 -238
- package/_modules/knowledge-engine/src/runtime/commit-processing-ledger.js +26 -26
- package/_modules/knowledge-engine/src/runtime/commit-prompt.js +193 -193
- package/_modules/knowledge-engine/src/runtime/commit-reconciler.js +394 -394
- package/_modules/knowledge-engine/src/runtime/contracts.js +282 -282
- package/_modules/knowledge-engine/src/runtime/conversation-store.js +389 -389
- package/_modules/knowledge-engine/src/runtime/evidence-bundle.js +228 -228
- package/_modules/knowledge-engine/src/runtime/git-runner.js +62 -62
- package/_modules/knowledge-engine/src/runtime/knowledge-promotion.js +505 -505
- package/_modules/knowledge-engine/src/runtime/knowledge-retrieval-service.js +400 -400
- package/_modules/knowledge-engine/src/runtime/markdown-knowledge-indexer.js +171 -171
- package/_modules/knowledge-engine/src/runtime/scanner.js +298 -298
- package/_modules/knowledge-engine/ui/app.js +23 -2
- package/_site/README.md +30 -30
- package/_site/_test/agent-capture-mcp-ui-test.js +68 -68
- package/_site/_test/agent-capture-ui-test.js +101 -101
- package/_site/_test/ai-profile-resolver-test.js +137 -137
- package/_site/_test/ai-profile-test.js +75 -75
- package/_site/_test/ai-vendor-presets-test.js +91 -91
- package/_site/_test/automatic-context-test.js +70 -70
- package/_site/_test/automation-queue-test.js +14 -14
- package/_site/_test/automation-ui-test.js +40 -40
- package/_site/_test/background-task-registry-test.js +43 -43
- package/_site/_test/baseline-schema-test.js +91 -91
- package/_site/_test/bin-cli-test.js +230 -230
- package/_site/_test/bridge-adapter-test.js +95 -95
- package/_site/_test/bridge-consumer-service-test.js +310 -310
- package/_site/_test/candidate-bin-content-test.js +35 -35
- package/_site/_test/candidate-product-assets-test.js +13 -13
- package/_site/_test/candidate-release-contract-test.js +14 -14
- package/_site/_test/capture-hook-dedup-test.js +158 -158
- package/_site/_test/cdp-browser-failure-test.js +103 -103
- package/_site/_test/chat-claudecodeui-match-test.js +92 -92
- package/_site/_test/claude-executable-discovery-test.js +90 -90
- package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -252
- package/_site/_test/claude-workbench-test.js +174 -174
- package/_site/_test/cli-instance-management-test.js +62 -62
- package/_site/_test/client-capture-restart-e2e-test.js +189 -189
- package/_site/_test/codex-conversation-projection-test.js +60 -60
- package/_site/_test/codex-marketplace-scoped-path-test.js +186 -186
- package/_site/_test/codex-notify-capture-e2e-test.js +101 -101
- package/_site/_test/codex-notify-fanout-test.js +141 -134
- package/_site/_test/codex-notify-upgrade-test.js +253 -253
- package/_site/_test/commit-boundary-freeze-test.js +147 -147
- package/_site/_test/commit-conversation-binding-test.js +99 -99
- package/_site/_test/commit-evidence-test.js +110 -110
- package/_site/_test/configuration-generation-test.js +22 -22
- package/_site/_test/context-pack-test.js +200 -200
- package/_site/_test/conversation-api-test.js +119 -119
- package/_site/_test/conversation-origin-test.js +15 -15
- package/_site/_test/conversation-store-test.js +138 -138
- package/_site/_test/cross-repo-isolation-e2e-test.js +187 -187
- package/_site/_test/desktop-browser-compat-test.js +28 -28
- package/_site/_test/desktop-hook-runtime-regression-test.js +204 -204
- package/_site/_test/embedding-config-test.js +32 -32
- package/_site/_test/explicit-commit-processor-test.js +37 -37
- package/_site/_test/fixtures/fake-browser-exit.js +10 -10
- package/_site/_test/fixtures/fake-browser-hang.js +10 -10
- package/_site/_test/fixtures/integration-busy-worker.cjs +6 -6
- package/_site/_test/fixtures/make-git-repos.js +92 -92
- package/_site/_test/fixtures/retrieval-gold-queries.json +32 -32
- package/_site/_test/folder-picker-output-test.js +19 -19
- package/_site/_test/fresh-hook-reimport-test.js +53 -53
- package/_site/_test/fresh-product-data-test.js +56 -56
- package/_site/_test/full-integration-e2e-test.js +167 -167
- package/_site/_test/git-path-compatibility-test.js +80 -80
- package/_site/_test/git-validation-test.js +81 -81
- package/_site/_test/glm-ui-refresh-review-test.js +313 -404
- package/_site/_test/harness-isolation-test.js +138 -0
- package/_site/_test/helpers/cdp-browser.js +383 -0
- package/_site/_test/helpers/cdp-network-capture.js +66 -0
- package/_site/_test/helpers/find-chrome.js +34 -0
- package/_site/_test/helpers/free-port.js +8 -0
- package/_site/_test/helpers/logging-ui-fixture.js +131 -0
- package/_site/_test/helpers/module-fixture.js +282 -0
- package/_site/_test/helpers/parallel-engine-advanced-engine-child.js +45 -0
- package/_site/_test/helpers/parallel-engine-advanced-fault-writer.js +171 -0
- package/_site/_test/helpers/parallel-engine-advanced-harness.js +245 -0
- package/_site/_test/helpers/parallel-engine-advanced-standin-analyzer.js +80 -0
- package/_site/_test/helpers/parallel-engine-advanced-ui-host.js +177 -0
- package/_site/_test/helpers/parallel-engine-final-harness.js +314 -0
- package/_site/_test/helpers/parallel-engine-final-standin-analyzer.js +159 -0
- package/_site/_test/helpers/parallel-knowledge-fixture.js +255 -0
- package/_site/_test/helpers/parallel-logging-acceptance-fixture.js +111 -0
- package/_site/_test/helpers/parallel-logging-acceptance-proxy.js +106 -0
- package/_site/_test/helpers/parallel-logging-fix-r2-proxy.js +205 -0
- package/_site/_test/helpers/parallel-logging-integration-5025-env.js +432 -0
- package/_site/_test/helpers/parallel-memory-analyzer.js +81 -0
- package/_site/_test/helpers/parallel-memory-event-bridge.js +128 -0
- package/_site/_test/helpers/parallel-memory-shell.js +470 -0
- package/_site/_test/helpers/parallel-nav-isolation-env.js +389 -0
- package/_site/_test/helpers/parallel-nav-module-ui.js +186 -0
- package/_site/_test/helpers/parallel-nav-proxy.js +148 -0
- package/_site/_test/helpers/parallel-nav-shell.js +361 -0
- package/_site/_test/helpers/parallel-qa-engine-endpoint-guard.js +206 -0
- package/_site/_test/helpers/parallel-qa-engine-endpoint-isolate.js +257 -0
- package/_site/_test/helpers/parallel-retrieval-followup-corpus.js +293 -0
- package/_site/_test/helpers/parallel-retrieval-followup-next-session.js +70 -0
- package/_site/_test/helpers/parallel-retrieval-followup-r2-shell.js +737 -0
- package/_site/_test/helpers/parallel-retrieval-followup-r2-wb-host.js +82 -0
- package/_site/_test/helpers/parallel-retrieval-followup-shell.js +279 -0
- package/_site/_test/helpers/parallel-workbench-fixture.js +371 -0
- package/_site/_test/helpers/parallel-workbench-host.js +80 -0
- package/_site/_test/helpers/parallel-workbench-integration-5025-fixture.js +408 -0
- package/_site/_test/helpers/parallel-workbench-integration-5025-harness.js +593 -0
- package/_site/_test/helpers/parallel-workbench-integration-5025-host.js +70 -0
- package/_site/_test/helpers/parallel-workbench-integration-5025-launcher.js +240 -0
- package/_site/_test/helpers/parallel-workbench-integration-5025-runtime-harness.js +630 -0
- package/_site/_test/helpers/parallel-workbench-source-map.js +196 -0
- package/_site/_test/helpers/spawn-server.js +106 -0
- package/_site/_test/helpers/version-visibility-fixture.js +251 -0
- package/_site/_test/hook-runtime-endpoint-test.js +88 -88
- package/_site/_test/hook-status-repair-api-test.js +152 -152
- package/_site/_test/hook-trigger-test.js +85 -85
- package/_site/_test/hook-worktree-test.js +42 -42
- package/_site/_test/import-preflight-api-test.js +234 -234
- package/_site/_test/index-builder-test.js +41 -41
- package/_site/_test/index-error-display-contract-test.js +10 -10
- package/_site/_test/index-writer-concurrency-test.js +135 -135
- package/_site/_test/integration-adapters-test.js +262 -262
- package/_site/_test/integration-auto-repair-test.js +113 -113
- package/_site/_test/integration-capture-setup-test.js +118 -118
- package/_site/_test/integration-intent-test.js +81 -81
- package/_site/_test/integration-runtime-binding-a2-test.js +566 -566
- package/_site/_test/integration-runtime-binding-a3-test.js +227 -227
- package/_site/_test/integration-runtime-binding-test.js +473 -473
- package/_site/_test/integration-status-scan-test.js +29 -29
- package/_site/_test/integration-surface-coverage-test.js +164 -164
- package/_site/_test/kb-framework-test.js +57 -57
- package/_site/_test/kbpath-follow-test.js +63 -63
- package/_site/_test/knowledge-db-test.js +144 -144
- package/_site/_test/knowledge-engine-client-test.js +120 -109
- package/_site/_test/knowledge-language-control-test.js +170 -170
- package/_site/_test/knowledge-maintenance-test.js +40 -40
- package/_site/_test/knowledge-promotion-recovery-test.js +260 -260
- package/_site/_test/knowledge-query-test.js +54 -54
- package/_site/_test/knowledge-retrieval-service-test.js +77 -77
- package/_site/_test/knowledge-scopes-test.js +37 -37
- package/_site/_test/knowledge-storage-location-test.js +28 -28
- package/_site/_test/knowledge-storage-startup-test.js +52 -52
- package/_site/_test/knowledge-store-logs-supervision-test.js +89 -89
- package/_site/_test/launcher-readiness-test.js +80 -80
- package/_site/_test/legacy-claude-session-alias-test.js +106 -106
- package/_site/_test/legacy-routes-removed-test.js +47 -47
- package/_site/_test/legacy-workbench-exclusion-test.js +92 -92
- package/_site/_test/live-gateway-test.js +48 -48
- package/_site/_test/local-marketplace-test.js +16 -16
- package/_site/_test/log-redaction-test.js +58 -58
- package/_site/_test/logging-api-test.js +89 -89
- package/_site/_test/logging-sse-no-gap-test.js +88 -88
- package/_site/_test/logging-ui-test.js +107 -107
- package/_site/_test/markdown-delta-overlay-test.js +85 -85
- package/_site/_test/markdown-maintenance-api-test.js +75 -75
- package/_site/_test/markdown-maintenance-test.js +11 -11
- package/_site/_test/mcp-server-test.js +149 -149
- package/_site/_test/model-context-windows-test.js +89 -89
- package/_site/_test/module-agent-start-proxy-test.js +65 -65
- package/_site/_test/module-artifact-integrity-test.js +27 -27
- package/_site/_test/module-boundary-test.js +30 -30
- package/_site/_test/module-bridge-eventbridge-test.js +202 -202
- package/_site/_test/module-dynamic-port-test.js +118 -118
- package/_site/_test/module-index-proxy-test.js +33 -33
- package/_site/_test/module-model-configuration-test.js +44 -44
- package/_site/_test/module-process-lifecycle-test.js +35 -35
- package/_site/_test/module-stream-proxy-test.js +34 -34
- package/_site/_test/module-terminal-restart-test.js +63 -63
- package/_site/_test/module-wiring-unbind-race-test.js +59 -59
- package/_site/_test/native-context-integrations-test.js +86 -86
- package/_site/_test/non-release-ci-test.js +34 -34
- package/_site/_test/offline-boundary-isolation-test.js +32 -32
- package/_site/_test/orphaned-claim-recovery-test.js +80 -80
- package/_site/_test/package-boundary-test.js +43 -43
- package/_site/_test/package-startup-test.js +56 -56
- package/_site/_test/packaged-ui-smoke-test.js +88 -88
- package/_site/_test/parallel-ci-r9-posix-socket-budget-test.js +156 -0
- package/_site/_test/parallel-engine-advanced-eng005-test.js +136 -0
- package/_site/_test/parallel-engine-advanced-eng006-test.js +171 -0
- package/_site/_test/parallel-engine-advanced-eng007-reconcile-test.js +231 -0
- package/_site/_test/parallel-engine-advanced-eng009-test.js +208 -0
- package/_site/_test/parallel-engine-advanced-eng010-test.js +213 -0
- package/_site/_test/parallel-engine-advanced-eng011-ui-probe.js +459 -0
- package/_site/_test/parallel-engine-final-eng002-test.js +255 -0
- package/_site/_test/parallel-engine-final-eng007-test.js +269 -0
- package/_site/_test/parallel-engine-final-eng011-test.js +197 -0
- package/_site/_test/parallel-engine-final-eng012-test.js +354 -0
- package/_site/_test/parallel-knowledge-client-error-code-test.js +184 -0
- package/_site/_test/parallel-knowledge-engine-test.js +353 -0
- package/_site/_test/parallel-knowledge-retrieval-test.js +292 -0
- package/_site/_test/parallel-knowledge-vector-test.js +168 -0
- package/_site/_test/parallel-linux-ci-r7-log007-day-test.js +110 -0
- package/_site/_test/parallel-logging-acceptance-log003-test.js +224 -0
- package/_site/_test/parallel-logging-acceptance-log004-test.js +231 -0
- package/_site/_test/parallel-logging-acceptance-log005-test.js +208 -0
- package/_site/_test/parallel-logging-acceptance-log006-test.js +214 -0
- package/_site/_test/parallel-logging-acceptance-log007-test.js +223 -0
- package/_site/_test/parallel-logging-acceptance-log008-test.js +180 -0
- package/_site/_test/parallel-logging-fix-r2-crash-probe.js +271 -0
- package/_site/_test/parallel-logging-fix-r2-gap-test.js +294 -0
- package/_site/_test/parallel-logging-fix-r2-normalhook-test.js +330 -0
- package/_site/_test/parallel-logging-integration-5025-portability-test.js +554 -0
- package/_site/_test/parallel-memory-candidate-test.js +301 -0
- package/_site/_test/parallel-memory-manual-test.js +197 -0
- package/_site/_test/parallel-memory-scope-test.js +228 -0
- package/_site/_test/parallel-memory-source-test.js +227 -0
- package/_site/_test/parallel-memory-sync-test.js +238 -0
- package/_site/_test/parallel-memory-view-r8-reload-gate-test.js +123 -0
- package/_site/_test/parallel-nav-failure-test.js +119 -0
- package/_site/_test/parallel-nav-isolation-test.js +308 -0
- package/_site/_test/parallel-nav-isolation-vector-path-test.js +90 -0
- package/_site/_test/parallel-nav-offline-test.js +272 -0
- package/_site/_test/parallel-nav-operability-test.js +257 -0
- package/_site/_test/parallel-nav-switch-test.js +247 -0
- package/_site/_test/parallel-nav-theme-test.js +130 -0
- package/_site/_test/parallel-nav-viewport-test.js +310 -0
- package/_site/_test/parallel-qa-engine-endpoint-guard-test.js +273 -0
- package/_site/_test/parallel-retrieval-followup-citation-test.js +219 -0
- package/_site/_test/parallel-retrieval-followup-corpus-test.js +287 -0
- package/_site/_test/parallel-workbench-independent-host.js +262 -0
- package/_site/_test/parallel-workbench-integration-5025-abort-probe.js +340 -0
- package/_site/_test/parallel-workbench-integration-5025-drafts-probe.js +588 -0
- package/_site/_test/parallel-workbench-integration-5025-env-check.js +317 -0
- package/_site/_test/parallel-workbench-integration-5025-model-probe.js +342 -0
- package/_site/_test/parallel-workbench-integration-5025-ordering-probe.js +326 -0
- package/_site/_test/parallel-workbench-model-probe.js +336 -0
- package/_site/_test/parallel-workbench-offline-test.js +169 -0
- package/_site/_test/parallel-workbench-provenance-trace.js +134 -0
- package/_site/_test/parallel-workbench-source-match.js +206 -0
- package/_site/_test/parallel-workbench-standalone-turn.js +349 -0
- package/_site/_test/path-consistency-test.js +151 -151
- package/_site/_test/pending-sweep-test.js +9 -9
- package/_site/_test/post-commit-automation-test.js +87 -87
- package/_site/_test/pr-consumer-contract-test.js +55 -55
- package/_site/_test/product-brand-contract-test.js +25 -25
- package/_site/_test/product-diagnostics-ui-test.js +93 -93
- package/_site/_test/product-empty-state-test.js +148 -0
- package/_site/_test/product-import-ui-test.js +219 -104
- package/_site/_test/product-navigation-test.js +152 -0
- package/_site/_test/product-progress-status-test.js +84 -84
- package/_site/_test/project-delete-recovery-test.js +64 -64
- package/_site/_test/project-domain-create-test.js +91 -0
- package/_site/_test/project-domain-scope-retrieval-test.js +214 -0
- package/_site/_test/project-git-init-confirm-test.js +115 -0
- package/_site/_test/project-goal-editor-test.js +144 -144
- package/_site/_test/project-import-duplicate-alias-test.js +134 -0
- package/_site/_test/project-import-unicode-restart-test.js +140 -0
- package/_site/_test/project-lifecycle-transaction-test.js +106 -106
- package/_site/_test/project-module-partial-recovery-test.js +222 -0
- package/_site/_test/project-remove-running-guard-test.js +79 -79
- package/_site/_test/project-store-test.js +90 -90
- package/_site/_test/project-unbind-late-read-ui-test.js +250 -0
- package/_site/_test/project-unbind-writer-guard-test.js +231 -0
- package/_site/_test/prompt-settings-test.js +115 -115
- package/_site/_test/protected-architecture-gate-test.js +122 -122
- package/_site/_test/refactor-characterization-test.js +36 -36
- package/_site/_test/registry-events-test.js +38 -38
- package/_site/_test/release-version-sync-test.js +115 -115
- package/_site/_test/requirement-binding-test.js +113 -113
- package/_site/_test/requirement-recorder-test.js +173 -173
- package/_site/_test/run-all-tests.js +173 -161
- package/_site/_test/runtime-endpoint-test.js +64 -64
- package/_site/_test/scanner-test.js +83 -83
- package/_site/_test/server-security-test.js +96 -96
- package/_site/_test/sessions-stream-test.js +93 -93
- package/_site/_test/shared-contracts-test.js +58 -58
- package/_site/_test/shared-scope-root-binding-test.js +74 -67
- package/_site/_test/simple-import-test.js +98 -98
- package/_site/_test/startup-analysis-disabled-test.js +16 -16
- package/_site/_test/storage-foundation-test.js +78 -78
- package/_site/_test/structured-logger-test.js +101 -101
- package/_site/_test/t07b-cln-005-coverage-test.js +212 -0
- package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -196
- package/_site/_test/team-knowledge-store-test.js +132 -132
- package/_site/_test/test-server-cleanup-test.js +35 -35
- package/_site/_test/test-server-module-isolation-test.js +37 -37
- package/_site/_test/tracking-start-test.js +144 -144
- package/_site/_test/vector-storage-root-test.js +75 -75
- package/_site/_test/version-visibility-browser-probe.js +249 -0
- package/_site/_test/version-visibility-test.js +92 -0
- package/_site/_test/workbench-context-receipts-test.js +22 -22
- package/_site/_test/workbench-permission-test.js +117 -117
- package/_site/_test/zcode-capture-integration-test.js +112 -112
- package/_site/lib/ai-profile-resolver.js +78 -78
- package/_site/lib/ai-vendor-presets.js +83 -83
- package/_site/lib/ai-workspace.js +104 -104
- package/_site/lib/atomic-file.js +204 -204
- package/_site/lib/automation-config.js +140 -140
- package/_site/lib/bridge-adapter.js +260 -260
- package/_site/lib/bridge-consumer-service.js +402 -402
- package/_site/lib/capture-hook-config.js +108 -108
- package/_site/lib/claude-cli-runner.js +1599 -1599
- package/_site/lib/cli-instances.js +43 -43
- package/_site/lib/codex-context-status.js +17 -17
- package/_site/lib/codex-notify-config.js +193 -193
- package/_site/lib/commit-conversation-binder.js +247 -247
- package/_site/lib/commit-processing-ledger.js +27 -27
- package/_site/lib/commit-prompt.js +202 -202
- package/_site/lib/commit-reconciler.js +444 -444
- package/_site/lib/context-pack-builder.js +299 -299
- package/_site/lib/contracts.js +291 -291
- package/_site/lib/conversation-exclusions.js +204 -204
- package/_site/lib/conversation-query-service.js +161 -161
- package/_site/lib/conversation-store.js +387 -387
- package/_site/lib/data-dir.js +13 -13
- package/_site/lib/embedding-service.js +85 -85
- package/_site/lib/engine-connection.js +93 -93
- package/_site/lib/engine-knowledge-client.js +76 -72
- package/_site/lib/evidence-bundle.js +228 -228
- package/_site/lib/folder-picker-output.js +24 -24
- package/_site/lib/git-path.js +14 -14
- package/_site/lib/git-runner.js +62 -62
- package/_site/lib/github-team-store.js +1175 -1175
- package/_site/lib/hook-manager.js +203 -203
- package/_site/lib/index-builder.js +157 -157
- package/_site/lib/index-service.js +173 -173
- package/_site/lib/integration-auto-repair.js +77 -77
- package/_site/lib/integration-installer.js +1240 -1240
- package/_site/lib/integration-intent.js +88 -88
- package/_site/lib/integration-status-scan.js +99 -99
- package/_site/lib/integration-status-worker.js +14 -14
- package/_site/lib/kb-framework.js +191 -191
- package/_site/lib/kb-validator.js +124 -124
- package/_site/lib/knowledge-db.js +363 -363
- package/_site/lib/knowledge-promotion.js +435 -435
- package/_site/lib/knowledge-retrieval-service.js +401 -401
- package/_site/lib/knowledge-schema.js +121 -121
- package/_site/lib/knowledge-tool-runtime.js +366 -366
- package/_site/lib/live-gateway.js +78 -78
- package/_site/lib/llm-client.js +173 -173
- package/_site/lib/markdown-knowledge-indexer.js +230 -230
- package/_site/lib/model-context-windows.js +82 -82
- package/_site/lib/module-bridge.js +874 -874
- package/_site/lib/module-port-resolver.js +57 -57
- package/_site/lib/post-commit-automation.js +141 -141
- package/_site/lib/product-data.js +35 -35
- package/_site/lib/project-lifecycle-service.js +554 -554
- package/_site/lib/project-registry-store.js +129 -129
- package/_site/lib/project-store.js +268 -268
- package/_site/lib/prompt-registry.js +148 -148
- package/_site/lib/public-version-info.js +37 -0
- package/_site/lib/registry-events.js +16 -16
- package/_site/lib/requirement-adapters.js +10 -10
- package/_site/lib/requirement-binder.js +112 -112
- package/_site/lib/requirement-recorder.js +278 -278
- package/_site/lib/runtime-control.js +89 -89
- package/_site/lib/runtime-endpoint.js +160 -160
- package/_site/lib/scanner.js +298 -298
- package/_site/lib/server-app.js +1672 -1664
- package/_site/lib/settings-store.js +153 -153
- package/_site/lib/storage-layout.js +166 -166
- package/_site/lib/structured-logger.js +574 -574
- package/_site/lib/zcode-context-integration.js +60 -60
- package/_site/scripts/codex-notify-fanout.cjs +95 -95
- package/_site/scripts/folder-picker.ps1 +156 -156
- package/_site/scripts/hook-trigger.js +158 -158
- package/_site/scripts/install-module-candidates.js +69 -69
- package/_site/scripts/pack-module-candidates.js +89 -89
- package/_site/scripts/sync-product-content.js +28 -28
- package/_site/scripts/sync-release-version.js +164 -164
- package/_site/scripts/vendor-modules.js +49 -49
- package/_site/scripts/verify-module-runtime.js +50 -50
- package/_site/scripts/verify-product-runtime.js +36 -36
- package/_site/scripts/verify-retrieval-gold.js +83 -83
- package/_site/server.js +15 -15
- package/_site/start.bat +26 -26
- package/_site/stop.bat +11 -11
- package/bin/project-knowledge-integrations.js +254 -254
- package/bin/project-knowledge-kb.js +67 -67
- package/bin/project-knowledge-mcp.js +201 -201
- package/bin/project-knowledge.js +269 -170
- package/claude-prompts.json +21 -21
- package/docs/README.zh-CN.md +207 -205
- package/docs/assets/architecture.svg +181 -181
- package/docs/assets/logo.svg +49 -49
- package/docs/fresh-v5-start.md +44 -44
- package/docs/leader-glm-workbench-playbook.md +122 -122
- package/docs/project-registry-schema.md +33 -33
- package/docs/release-5.0.16-acceptance.md +176 -176
- package/docs/release-5.0.17-acceptance.md +117 -117
- package/docs/release-5.0.19-acceptance.md +39 -39
- package/docs/team-knowledge-mode-a-plan.md +73 -73
- package/docs/testing-strategy.md +33 -33
- package/module-runtime-manifest.json +68 -65
- package/package.json +113 -110
- package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.mcp.json +1 -1
- package/templates/change.md +30 -30
- package/templates/project-readme.md +52 -52
- package/ui/index.html +3 -2
- package/ui/version-info.css +44 -0
- package/ui/version-info.js +24 -0
- package/vendor-manifest.json +55 -55
|
@@ -1,1240 +1,1240 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const os = require('os');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const crypto = require('crypto');
|
|
5
|
-
const { createRequire } = require('node:module');
|
|
6
|
-
const { spawn, spawnSync } = require('child_process');
|
|
7
|
-
const crossSpawn = require('cross-spawn');
|
|
8
|
-
const { applyEdits, modify, parse } = require('jsonc-parser');
|
|
9
|
-
const packageInfo = require('../../package.json');
|
|
10
|
-
const { resolveDataDirPath } = require('./data-dir');
|
|
11
|
-
const { planNotify, writeNotify } = require('./codex-notify-config');
|
|
12
|
-
const { planCaptureHooks, repairCaptureHooks, removeCaptureHooks } = require('./capture-hook-config');
|
|
13
|
-
const { writeJsonAtomic } = require('./atomic-file');
|
|
14
|
-
|
|
15
|
-
// Fail-open module resolution: a missing/unloadable Bridge package must
|
|
16
|
-
// degrade Development Capture status, never crash server startup.
|
|
17
|
-
let bridgeModule = null;
|
|
18
|
-
try {
|
|
19
|
-
bridgeModule = require('@sanqianx/ai-coding-event-bridge-console/src/bridge');
|
|
20
|
-
} catch (_) {
|
|
21
|
-
bridgeModule = null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const INTEGRATION_NAME = 'project-knowledge';
|
|
25
|
-
const MARKETPLACE_NAME = 'project-knowledge';
|
|
26
|
-
const DEFAULT_MARKETPLACE_SOURCE = 'SanQianX/project-knowledge-base';
|
|
27
|
-
const INTEGRATION_RUNTIME_SCHEMA = 'project-knowledge/integration-runtime/v1';
|
|
28
|
-
const MCP_LAUNCHER_ERROR_SCHEMA = 'project-knowledge/mcp-launcher-error/v1';
|
|
29
|
-
const INTEGRATION_PACKAGE_NAME = '@sanqianx/project-knowledge';
|
|
30
|
-
const MCP_SERVER_NAME = 'project-knowledge';
|
|
31
|
-
const MCP_TOOL_NAMES = ['project_knowledge_resolve', 'project_knowledge_record_requirement', 'project_knowledge_search', 'project_knowledge_ask', 'project_knowledge_get', 'project_knowledge_history'];
|
|
32
|
-
const MCP_LAUNCHER_FILE = path.join('plugins', INTEGRATION_NAME, 'scripts', 'mcp-launcher.cjs');
|
|
33
|
-
const MCP_PROBE_TIMEOUT_MS = 8000;
|
|
34
|
-
const MCP_PROBE_STDERR_CAP_BYTES = 32768;
|
|
35
|
-
const MCP_PROBE_STDOUT_CAP_BYTES = 32768;
|
|
36
|
-
const MCP_PROBE_FAILURE_DETAILS = {
|
|
37
|
-
PROBE_SPAWN_FAILED: '无法启动本地 MCP 启动进程;请检查 Node 运行时。',
|
|
38
|
-
INITIALIZE_REJECTED: '本地 MCP 服务拒绝了 initialize 握手;未回显上游错误内容。',
|
|
39
|
-
SERVER_IDENTITY: 'MCP 服务身份不是 project-knowledge,判定未通过。',
|
|
40
|
-
VERSION_MISMATCH: 'MCP 服务版本与当前产品版本不一致。',
|
|
41
|
-
TOOLS_CONTRACT: 'MCP 服务未提供完整的六个知识工具。',
|
|
42
|
-
PROBE_OUTPUT_OVERFLOW: '探测输出超过大小上限,已立即失败并清理子进程。',
|
|
43
|
-
PROBE_TIMEOUT: 'MCP initialize 握手超时。',
|
|
44
|
-
PROBE_CRASH: '启动探测自身异常终止;已使用固定诊断,未回显异常内容。',
|
|
45
|
-
LAUNCHER_ERROR_UNKNOWN: '本地启动器返回了无法识别的错误代码;未回显其内容。',
|
|
46
|
-
BINDING_MISSING: '尚未写入本地运行时绑定;请先完成知识接入。',
|
|
47
|
-
BINDING_CORRUPT: '本地运行时绑定不可读;请修复知识接入。',
|
|
48
|
-
BINDING_PACKAGE_MISMATCH: '运行时绑定不是本产品包。',
|
|
49
|
-
BINDING_VERSION_MISMATCH: '运行时绑定版本不一致;请更新插件。',
|
|
50
|
-
};
|
|
51
|
-
// Launcher error codes the probe may adopt from stderr. Anything else is an
|
|
52
|
-
// untrusted value: it never becomes a result code and is never echoed.
|
|
53
|
-
const MCP_LAUNCHER_ERROR_CODE_ALLOWLIST = [
|
|
54
|
-
'BINDING_MISSING',
|
|
55
|
-
'BINDING_CORRUPT',
|
|
56
|
-
'BINDING_SCHEMA_MISMATCH',
|
|
57
|
-
'BINDING_PACKAGE_MISMATCH',
|
|
58
|
-
'BINDING_VERSION_MISMATCH',
|
|
59
|
-
'BINDING_INVALID_ROOT',
|
|
60
|
-
'BINDING_ENTRY_MISSING',
|
|
61
|
-
'BINDING_ENTRY_INVALID',
|
|
62
|
-
'LAUNCH_FAILED',
|
|
63
|
-
];
|
|
64
|
-
const SUPPORTED_CLIENTS = ['claude', 'opencode', 'codex', 'zcode'];
|
|
65
|
-
const DEFAULT_BRIDGE_CONSUMER = 'project-knowledge';
|
|
66
|
-
const CLIENT_LABELS = {
|
|
67
|
-
claude: 'Claude Code',
|
|
68
|
-
opencode: 'OpenCode',
|
|
69
|
-
codex: 'Codex',
|
|
70
|
-
zcode: 'ZCode',
|
|
71
|
-
};
|
|
72
|
-
const CLIENT_METHOD_SUFFIX = {
|
|
73
|
-
claude: 'Claude',
|
|
74
|
-
opencode: 'OpenCode',
|
|
75
|
-
codex: 'Codex',
|
|
76
|
-
zcode: 'ZCode',
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
function normalizeClient(value) {
|
|
80
|
-
const normalized = String(value || '').trim().toLowerCase();
|
|
81
|
-
if (normalized === 'claude-code' || normalized === 'claudecode') return 'claude';
|
|
82
|
-
if (normalized === 'codex-cli') return 'codex';
|
|
83
|
-
if (SUPPORTED_CLIENTS.includes(normalized)) return normalized;
|
|
84
|
-
throw new Error(`unsupported client: ${value}`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function defaultCommandResolver(name, env = process.env) {
|
|
88
|
-
const lookup = process.platform === 'win32' ? 'where.exe' : 'which';
|
|
89
|
-
const result = spawnSync(lookup, [name], {
|
|
90
|
-
encoding: 'utf8',
|
|
91
|
-
windowsHide: true,
|
|
92
|
-
env,
|
|
93
|
-
timeout: 5000,
|
|
94
|
-
});
|
|
95
|
-
if (result.status !== 0) return '';
|
|
96
|
-
const candidates = String(result.stdout || '')
|
|
97
|
-
.split(/\r?\n/)
|
|
98
|
-
.map(item => item.trim())
|
|
99
|
-
.filter(Boolean);
|
|
100
|
-
if (process.platform === 'win32') {
|
|
101
|
-
return candidates.find(item => /\.(cmd|exe)$/i.test(item)) || candidates[0] || '';
|
|
102
|
-
}
|
|
103
|
-
return candidates[0] || '';
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function defaultRunner(command, args, options = {}) {
|
|
107
|
-
return crossSpawn.sync(command, args, {
|
|
108
|
-
encoding: 'utf8',
|
|
109
|
-
windowsHide: true,
|
|
110
|
-
env: options.env || process.env,
|
|
111
|
-
cwd: options.cwd,
|
|
112
|
-
timeout: options.timeout || (process.env.KB_INTEGRATION_STATUS_WORKER === '1' ? 5000 : 120_000),
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function parseJson(text, fallback) {
|
|
117
|
-
try {
|
|
118
|
-
return JSON.parse(String(text || ''));
|
|
119
|
-
} catch {
|
|
120
|
-
return fallback;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function writeFileAtomic(filePath, content) {
|
|
125
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
126
|
-
const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
127
|
-
fs.writeFileSync(temporary, content, 'utf8');
|
|
128
|
-
fs.renameSync(temporary, filePath);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function updateJsoncValue(filePath, propertyPath, value) {
|
|
132
|
-
let source = fs.existsSync(filePath)
|
|
133
|
-
? fs.readFileSync(filePath, 'utf8')
|
|
134
|
-
: '{\n "$schema": "https://opencode.ai/config.json"\n}\n';
|
|
135
|
-
const formattingOptions = { insertSpaces: true, tabSize: 2, eol: source.includes('\r\n') ? '\r\n' : '\n' };
|
|
136
|
-
const edits = modify(source, propertyPath, value, { formattingOptions });
|
|
137
|
-
source = applyEdits(source, edits);
|
|
138
|
-
writeFileAtomic(filePath, source);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function openCodeConfigDir(homeDir = os.homedir(), env = process.env) {
|
|
142
|
-
if (env.OPENCODE_CONFIG_DIR) return path.resolve(env.OPENCODE_CONFIG_DIR);
|
|
143
|
-
if (env.XDG_CONFIG_HOME) return path.join(path.resolve(env.XDG_CONFIG_HOME), 'opencode');
|
|
144
|
-
return path.join(homeDir, '.config', 'opencode');
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function findOpenCodeConfig(configDir) {
|
|
148
|
-
const jsonc = path.join(configDir, 'opencode.jsonc');
|
|
149
|
-
const json = path.join(configDir, 'opencode.json');
|
|
150
|
-
if (fs.existsSync(jsonc)) return jsonc;
|
|
151
|
-
if (fs.existsSync(json)) return json;
|
|
152
|
-
return jsonc;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function readOpenCodeState(configDir) {
|
|
156
|
-
const configPath = findOpenCodeConfig(configDir);
|
|
157
|
-
let config = {};
|
|
158
|
-
if (fs.existsSync(configPath)) {
|
|
159
|
-
config = parse(fs.readFileSync(configPath, 'utf8')) || {};
|
|
160
|
-
}
|
|
161
|
-
const legacy = config?.mcp?.[INTEGRATION_NAME];
|
|
162
|
-
const v2 = config?.mcp?.servers?.[INTEGRATION_NAME];
|
|
163
|
-
const skillPath = path.join(configDir, 'skills', INTEGRATION_NAME, 'SKILL.md');
|
|
164
|
-
const instructionPath = path.join(configDir, 'instructions', `${INTEGRATION_NAME}.md`);
|
|
165
|
-
const configuredInstructions = Array.isArray(config?.instructions) ? config.instructions : [];
|
|
166
|
-
return {
|
|
167
|
-
configPath,
|
|
168
|
-
mcpInstalled: Boolean(legacy || v2),
|
|
169
|
-
skillInstalled: fs.existsSync(skillPath),
|
|
170
|
-
instructionInstalled: fs.existsSync(instructionPath)
|
|
171
|
-
&& configuredInstructions.some(item => path.resolve(String(item)) === path.resolve(instructionPath)),
|
|
172
|
-
skillPath,
|
|
173
|
-
instructionPath,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function ensureOpenCodeInstructionConfig(configDir, instructionPath) {
|
|
178
|
-
const configPath = findOpenCodeConfig(configDir);
|
|
179
|
-
const config = fs.existsSync(configPath) ? (parse(fs.readFileSync(configPath, 'utf8')) || {}) : {};
|
|
180
|
-
const instructions = Array.isArray(config.instructions) ? [...config.instructions] : [];
|
|
181
|
-
if (!instructions.some(item => path.resolve(String(item)) === path.resolve(instructionPath))) {
|
|
182
|
-
instructions.push(instructionPath);
|
|
183
|
-
updateJsoncValue(configPath, ['instructions'], instructions);
|
|
184
|
-
}
|
|
185
|
-
return configPath;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function removeOpenCodeInstructionConfig(configDir, instructionPath) {
|
|
189
|
-
const configPath = findOpenCodeConfig(configDir);
|
|
190
|
-
if (!fs.existsSync(configPath)) return false;
|
|
191
|
-
const config = parse(fs.readFileSync(configPath, 'utf8')) || {};
|
|
192
|
-
if (!Array.isArray(config.instructions)) return false;
|
|
193
|
-
const instructions = config.instructions.filter(item => path.resolve(String(item)) !== path.resolve(instructionPath));
|
|
194
|
-
if (instructions.length === config.instructions.length) return false;
|
|
195
|
-
updateJsoncValue(configPath, ['instructions'], instructions.length ? instructions : undefined);
|
|
196
|
-
return true;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function removeOpenCodeMcpConfig(configDir) {
|
|
200
|
-
const configPath = findOpenCodeConfig(configDir);
|
|
201
|
-
if (!fs.existsSync(configPath)) return false;
|
|
202
|
-
let source = fs.readFileSync(configPath, 'utf8');
|
|
203
|
-
const config = parse(source) || {};
|
|
204
|
-
const paths = [];
|
|
205
|
-
if (config?.mcp?.[INTEGRATION_NAME]) paths.push(['mcp', INTEGRATION_NAME]);
|
|
206
|
-
if (config?.mcp?.servers?.[INTEGRATION_NAME]) paths.push(['mcp', 'servers', INTEGRATION_NAME]);
|
|
207
|
-
if (!paths.length) return false;
|
|
208
|
-
const formattingOptions = { insertSpaces: true, tabSize: 2, eol: source.includes('\r\n') ? '\r\n' : '\n' };
|
|
209
|
-
for (const propertyPath of paths) {
|
|
210
|
-
const edits = modify(source, propertyPath, undefined, { formattingOptions });
|
|
211
|
-
source = applyEdits(source, edits);
|
|
212
|
-
}
|
|
213
|
-
writeFileAtomic(configPath, source);
|
|
214
|
-
return true;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
class IntegrationManager {
|
|
218
|
-
constructor(options = {}) {
|
|
219
|
-
this.rootDir = options.rootDir || path.resolve(__dirname, '..', '..');
|
|
220
|
-
this.homeDir = options.homeDir || os.homedir();
|
|
221
|
-
this.env = options.env || process.env;
|
|
222
|
-
this.version = options.version || packageInfo.version;
|
|
223
|
-
this.packageSpec = options.packageSpec || `${packageInfo.name}@${this.version}`;
|
|
224
|
-
this.marketplaceSource = options.marketplaceSource || DEFAULT_MARKETPLACE_SOURCE;
|
|
225
|
-
this.claudeMarketplaceSource = options.marketplaceSource || this.rootDir;
|
|
226
|
-
this.explicitMarketplace = Boolean(options.marketplaceSource);
|
|
227
|
-
this.codexMarketplaceAddSource = options.codexMarketplaceSource || options.marketplaceSource || this.rootDir;
|
|
228
|
-
this.explicitCodexMarketplace = Boolean(options.codexMarketplaceSource || options.marketplaceSource);
|
|
229
|
-
this.runner = options.runner || defaultRunner;
|
|
230
|
-
this.commandResolver = options.commandResolver || defaultCommandResolver;
|
|
231
|
-
this.codexHookProbe = options.codexHookProbe || require('./codex-context-status').readCodexHooks;
|
|
232
|
-
this.mcpProbe = options.mcpProbe || null;
|
|
233
|
-
this.dryRun = Boolean(options.dryRun);
|
|
234
|
-
this.logger = options.logger || null;
|
|
235
|
-
this.operationId = options.operationId || '';
|
|
236
|
-
this.planned = [];
|
|
237
|
-
this.bridge = options.bridgeModule || bridgeModule;
|
|
238
|
-
this.bridgeConsumerName = options.bridgeConsumerName || DEFAULT_BRIDGE_CONSUMER;
|
|
239
|
-
this.bridgeHomeDir = options.bridgeHomeDir
|
|
240
|
-
|| this.env.AI_CODING_EVENT_BRIDGE_HOME
|
|
241
|
-
|| path.join(resolveDataDirPath({ dataDir: this.env.KB_DATA_DIR || '', homeDir: this.homeDir }), 'events');
|
|
242
|
-
this.claudeSettingsFile = options.claudeSettingsFile || path.join(this.homeDir, '.claude', 'settings.json');
|
|
243
|
-
this.codexConfigFile = options.codexConfigFile || path.join(this.homeDir, '.codex', 'config.toml');
|
|
244
|
-
this.openCodePluginsDir = options.openCodePluginsDir || path.join(openCodeConfigDir(this.homeDir, this.env), 'plugin');
|
|
245
|
-
this.zcodeConfigFile = options.zcodeConfigFile || path.join(this.homeDir, '.zcode', 'cli', 'config.json');
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
_bridgeFacade() {
|
|
249
|
-
return this.bridge.createBridge({ homeDir: this.bridgeHomeDir });
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
_captureInstallerOptions(client) {
|
|
253
|
-
return {
|
|
254
|
-
homeDir: this.bridgeHomeDir,
|
|
255
|
-
settingsFile: this.claudeSettingsFile || undefined,
|
|
256
|
-
configFile: client === 'zcode' ? this.zcodeConfigFile : this.codexConfigFile,
|
|
257
|
-
pluginsDir: this.openCodePluginsDir || undefined,
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// ---- Bridge host consumer (host-level, never owned by one connector) ----
|
|
262
|
-
|
|
263
|
-
async registerBridgeConsumer(meta = {}) {
|
|
264
|
-
if (this.dryRun) {
|
|
265
|
-
this.planned.push({ command: 'bridge', args: ['registerConsumer', this.bridgeConsumerName] });
|
|
266
|
-
return { state: 'planned', consumerName: this.bridgeConsumerName };
|
|
267
|
-
}
|
|
268
|
-
const facade = this._bridgeFacade();
|
|
269
|
-
const consumer = await facade.registerConsumer(this.bridgeConsumerName, meta);
|
|
270
|
-
return { state: 'registered', consumerName: this.bridgeConsumerName, consumer };
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
async unregisterBridgeConsumer() {
|
|
274
|
-
if (this.dryRun) {
|
|
275
|
-
this.planned.push({ command: 'bridge', args: ['unregisterConsumer', this.bridgeConsumerName] });
|
|
276
|
-
return { state: 'planned', consumerName: this.bridgeConsumerName };
|
|
277
|
-
}
|
|
278
|
-
const facade = this._bridgeFacade();
|
|
279
|
-
await facade.unregisterConsumer(this.bridgeConsumerName);
|
|
280
|
-
return { state: 'unregistered', consumerName: this.bridgeConsumerName };
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
async bridgeStatus() {
|
|
284
|
-
const facade = this._bridgeFacade();
|
|
285
|
-
const [consumer, health] = await Promise.all([facade.getConsumer(this.bridgeConsumerName), facade.getHealth()]);
|
|
286
|
-
return {
|
|
287
|
-
consumerRegistered: Boolean(consumer),
|
|
288
|
-
consumerName: this.bridgeConsumerName,
|
|
289
|
-
bridgeHealthy: true,
|
|
290
|
-
health,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// ---- Development Capture connectors (Bridge hooks / notify / plugin) ----
|
|
295
|
-
|
|
296
|
-
async captureOperation(operation, client, options = {}) {
|
|
297
|
-
const startedAt = Date.now();
|
|
298
|
-
const context = { client, operation };
|
|
299
|
-
try {
|
|
300
|
-
const result = await this[`${operation}Capture${CLIENT_METHOD_SUFFIX[client]}`](options);
|
|
301
|
-
const ok = !result.conflict && result.state !== 'failed';
|
|
302
|
-
const repairContext = { ...context, ...(typeof result.changed === 'boolean' ? { changed: result.changed } : {}), ...(Number.isInteger(result.removedLegacy) ? { removedLegacy: result.removedLegacy, removedDuplicates: result.removedDuplicates } : {}) };
|
|
303
|
-
if (operation !== 'status' || !ok) this.log(ok ? 'info' : 'error', `integration.capture.${ok ? 'completed' : 'failed'}`, ok ? 'Agent 采集接线操作完成。' : 'Agent 采集接线未完成。', { operationId: this.operationId, durationMs: Date.now() - startedAt, error: ok ? undefined : Object.assign(new Error(result.detail), { code: 'CAPTURE_CONFLICT' }), context: repairContext });
|
|
304
|
-
return { ok, ...result, ...(!ok ? { error: { code: 'CAPTURE_CONFLICT', message: result.detail } } : {}) };
|
|
305
|
-
} catch (error) {
|
|
306
|
-
this.log('error', 'integration.capture.failed', 'Agent 采集接线操作失败。', { operationId: this.operationId, durationMs: Date.now() - startedAt, error, context });
|
|
307
|
-
return { ok: false, state: 'failed', installed: false, detail: error.message, error: { code: error.code || 'CAPTURE_FAILED', message: error.message } };
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
async installCaptureZCode() {
|
|
312
|
-
if (this.dryRun) {
|
|
313
|
-
this.planned.push({ command: 'bridge', args: ['installZcodeHook', this.bridgeHomeDir] });
|
|
314
|
-
return { state: 'planned', installed: false, detail: '计划接入 ZCode 会话采集,未修改配置。' };
|
|
315
|
-
}
|
|
316
|
-
planCaptureHooks(this.zcodeConfigFile, 'zcode', this);
|
|
317
|
-
await this.bridge.installers.zcode.installZcodeHook(this._captureInstallerOptions('zcode'));
|
|
318
|
-
const repaired = repairCaptureHooks(this.zcodeConfigFile, 'zcode', this);
|
|
319
|
-
const status = await this.statusCaptureZCode();
|
|
320
|
-
if (!status.installed) throw new Error('ZCode Hook 写入后校验失败,请检查 hooks.enabled 与事件配置。');
|
|
321
|
-
return { ...status, ...repaired };
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
async statusCaptureZCode() {
|
|
325
|
-
const status = await this.bridge.installers.zcode.statusZcodeHook(this._captureInstallerOptions('zcode'));
|
|
326
|
-
const plan = planCaptureHooks(this.zcodeConfigFile, 'zcode', this);
|
|
327
|
-
if (plan.repairNeeded || status.installed && !fs.existsSync(path.join(this.bridgeHomeDir, 'bin', 'bridge-hook.cjs'))) return { state: 'needs-repair', installed: false, conflict: false, repairNeeded: true, detail: '检测到重复、旧接线或缺失的 Bridge 入口,请执行接入/修复。' };
|
|
328
|
-
return { state: status.installed ? 'installed' : 'not-installed', installed: status.installed, conflict: false, detail: status.installed ? '已配置用户输入与回答采集;请重新打开 ZCode 会话。' : 'ZCode 会话采集尚未接入。' };
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
async uninstallCaptureZCode() {
|
|
332
|
-
if (this.dryRun) return { state: 'planned', detail: '计划移除 ZCode 采集,未修改配置。' };
|
|
333
|
-
removeCaptureHooks(this.zcodeConfigFile, 'zcode', this);
|
|
334
|
-
await this.bridge.installers.zcode.uninstallZcodeHook(this._captureInstallerOptions('zcode'));
|
|
335
|
-
return { state: 'removed', installed: false, detail: '已移除 ZCode 托管采集,保留第三方配置与已有记录。' };
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
async installCaptureClaude() {
|
|
339
|
-
if (this.dryRun) {
|
|
340
|
-
this.planned.push({ command: 'bridge', args: ['installClaudeCodeHook', this.bridgeHomeDir] });
|
|
341
|
-
return { state: 'planned', installed: false, conflict: false, detail: 'managed Bridge hooks (dry-run)' };
|
|
342
|
-
}
|
|
343
|
-
planCaptureHooks(this.claudeSettingsFile, 'claude', this);
|
|
344
|
-
const result = await this.bridge.installers.claudeCode.installClaudeCodeHook(this._captureInstallerOptions());
|
|
345
|
-
if (result.runtime?.conflict) throw new Error('Bridge 运行版本冲突,保留旧采集接线。');
|
|
346
|
-
const repaired = repairCaptureHooks(this.claudeSettingsFile, 'claude', this);
|
|
347
|
-
return { state: 'installed', installed: true, conflict: false, detail: `managed hooks: ${result.added.join(', ') || 'already present'}`, ...repaired };
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
async statusCaptureClaude() {
|
|
351
|
-
const status = await this.bridge.installers.claudeCode.statusClaudeCodeHook(this._captureInstallerOptions());
|
|
352
|
-
const plan = planCaptureHooks(this.claudeSettingsFile, 'claude', this);
|
|
353
|
-
if (plan.repairNeeded || status.installed && !fs.existsSync(path.join(this.bridgeHomeDir, 'bin', 'bridge-hook.cjs'))) return { state: 'needs-repair', installed: false, conflict: false, repairNeeded: true, detail: '检测到重复、旧接线或缺失的 Bridge 入口,请执行接入/修复。' };
|
|
354
|
-
return {
|
|
355
|
-
state: status.installed ? 'installed' : 'not-installed',
|
|
356
|
-
installed: status.installed,
|
|
357
|
-
conflict: false,
|
|
358
|
-
detail: status.installed ? '已配置用户输入与回答采集。' : '会话采集 Hook 尚未配置。',
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
async uninstallCaptureClaude(options = {}) {
|
|
363
|
-
if (this.dryRun) return { state: 'planned', detail: '计划移除 Claude Code 采集,未修改配置。' };
|
|
364
|
-
removeCaptureHooks(this.claudeSettingsFile, 'claude', this);
|
|
365
|
-
await this.bridge.installers.claudeCode.uninstallClaudeCodeHook({
|
|
366
|
-
...this._captureInstallerOptions(),
|
|
367
|
-
unregisterConsumer: options.unregisterConsumer === true,
|
|
368
|
-
consumerName: this.bridgeConsumerName,
|
|
369
|
-
});
|
|
370
|
-
return { state: 'removed', detail: options.unregisterConsumer === true ? 'managed hooks removed; host consumer unregistered' : 'managed hooks removed; host consumer kept' };
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
async installCaptureCodex() {
|
|
374
|
-
if (this.dryRun) {
|
|
375
|
-
this.planned.push({ command: 'bridge', args: ['installCodexNotify', this.bridgeHomeDir] });
|
|
376
|
-
return { state: 'planned', installed: false, conflict: false, detail: 'managed Codex notify (dry-run)' };
|
|
377
|
-
}
|
|
378
|
-
const plan = planNotify(this.codexConfigFile, this);
|
|
379
|
-
if (plan.conflict) return { state: 'conflict', installed: false, conflict: true, detail: plan.detail };
|
|
380
|
-
// The host owns composition and root-level TOML placement; Bridge only
|
|
381
|
-
// materializes its independent runtime, never rewrites the notify setting.
|
|
382
|
-
await this.bridge.ensureRuntimeHome({ homeDir: this.bridgeHomeDir });
|
|
383
|
-
const written = writeNotify(this.codexConfigFile, plan);
|
|
384
|
-
return { state: 'installed', installed: true, conflict: false, detail: written.changed ? '已接入 Codex 会话采集,保留有效的原通知;请重新打开 Codex 会话。' : 'Codex 会话采集已接入,通知链无需重复修改。', ...written };
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
async statusCaptureCodex() {
|
|
388
|
-
const plan = planNotify(this.codexConfigFile, this);
|
|
389
|
-
const runtimeReady = fs.existsSync(path.join(this.rootDir, '_site', 'scripts', 'codex-notify-fanout.cjs'))
|
|
390
|
-
&& fs.existsSync(path.join(this.bridgeHomeDir, 'bin', 'bridge-hook.cjs'));
|
|
391
|
-
const installed = !plan.conflict && !plan.changed && runtimeReady;
|
|
392
|
-
return {
|
|
393
|
-
state: installed ? 'installed' : plan.conflict ? 'conflict' : 'not-installed',
|
|
394
|
-
installed,
|
|
395
|
-
conflict: Boolean(plan.conflict),
|
|
396
|
-
detail: installed ? '已配置 Codex 会话采集,原通知保持有效。' : plan.conflict ? plan.detail : !runtimeReady && !plan.changed ? 'Codex 采集运行文件缺失,请执行接入/修复。' : 'Codex 会话采集尚未接入或旧接线需要修复。',
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
async uninstallCaptureCodex(options = {}) {
|
|
401
|
-
if (this.dryRun) return { state: 'planned', installed: false, detail: '计划移除 Codex 托管采集,保留原通知。' };
|
|
402
|
-
const plan = planNotify(this.codexConfigFile, this, false);
|
|
403
|
-
if (plan.conflict) return { state: 'conflict', installed: false, conflict: true, detail: plan.detail };
|
|
404
|
-
const written = writeNotify(this.codexConfigFile, plan);
|
|
405
|
-
if (options.unregisterConsumer === true) await this.unregisterBridgeConsumer();
|
|
406
|
-
return { state: 'removed', installed: false, conflict: false, detail: '已移除 Codex 托管采集,保留有效的原通知和已有记录。', ...written };
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
async installCaptureOpenCode() {
|
|
410
|
-
if (this.dryRun) {
|
|
411
|
-
this.planned.push({ command: 'bridge', args: ['installOpenCodePlugin', this.bridgeHomeDir] });
|
|
412
|
-
return { state: 'planned', installed: false, conflict: false, detail: 'managed OpenCode plugin (dry-run)' };
|
|
413
|
-
}
|
|
414
|
-
await this.bridge.installers.openCode.installOpenCodePlugin(this._captureInstallerOptions());
|
|
415
|
-
return { state: 'installed', installed: true, conflict: false, detail: 'managed plugin installed' };
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
async statusCaptureOpenCode() {
|
|
419
|
-
const status = await this.bridge.installers.openCode.statusOpenCodePlugin(this._captureInstallerOptions());
|
|
420
|
-
return {
|
|
421
|
-
state: status.installed ? 'installed' : 'not-installed',
|
|
422
|
-
installed: Boolean(status.installed),
|
|
423
|
-
conflict: false,
|
|
424
|
-
detail: status.installed ? '已配置会话采集插件。' : '会话采集插件尚未配置,保留其他插件。',
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
async uninstallCaptureOpenCode(options = {}) {
|
|
429
|
-
await this.bridge.installers.openCode.uninstallOpenCodePlugin({
|
|
430
|
-
...this._captureInstallerOptions(),
|
|
431
|
-
unregisterConsumer: options.unregisterConsumer === true,
|
|
432
|
-
consumerName: this.bridgeConsumerName,
|
|
433
|
-
});
|
|
434
|
-
return { state: 'removed', detail: 'managed plugin removed' };
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
log(level, event, message, input = {}) {
|
|
438
|
-
if (!this.logger || typeof this.logger[level] !== 'function') return;
|
|
439
|
-
Promise.resolve(this.logger[level](event, message, { component: 'integration-manager', operationId: this.operationId, ...input })).catch(() => {
|
|
440
|
-
try { process.stderr.write(`[integration-manager logger fallback] ${event}\n`); } catch {
|
|
441
|
-
// stderr is the final non-recursive observer fallback.
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
executable(client) {
|
|
447
|
-
const name = client === 'claude' ? 'claude' : client;
|
|
448
|
-
if (this.dryRun) return process.platform === 'win32' ? `${name}.cmd` : name;
|
|
449
|
-
const command = this.commandResolver(name, this.env);
|
|
450
|
-
if (command || client !== 'zcode' || this.commandResolver !== defaultCommandResolver) return command;
|
|
451
|
-
try { return require('claude-ai-workbench/packages/server/lib/drivers/zcode-driver').discoverZCodeCli({ env: this.env })?.path || ''; } catch { return ''; }
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
detectClients() {
|
|
455
|
-
return SUPPORTED_CLIENTS.filter(client => Boolean(this.executable(client)) || client === 'zcode' && this.captureConfigFound(client));
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
captureConfigFound(client) {
|
|
459
|
-
const target = { claude: this.claudeSettingsFile, codex: this.codexConfigFile, opencode: path.dirname(this.openCodePluginsDir), zcode: this.zcodeConfigFile }[client];
|
|
460
|
-
try { const stat = fs.statSync(target); return client === 'opencode' ? stat.isDirectory() : stat.isFile(); } catch { return false; }
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// ---- Local MCP runtime binding and startup diagnostics ----
|
|
464
|
-
|
|
465
|
-
integrationRuntimeBindingPath() {
|
|
466
|
-
return path.join(resolveDataDirPath({ dataDir: this.env.KB_DATA_DIR || '', homeDir: this.homeDir }), 'integration-runtime.json');
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
ensureIntegrationRuntimeBinding(options = {}) {
|
|
470
|
-
const manifest = JSON.parse(fs.readFileSync(path.join(this.rootDir, 'package.json'), 'utf8'));
|
|
471
|
-
if (manifest.name !== INTEGRATION_PACKAGE_NAME) throw new Error('知识接入绑定的运行包不是本产品包,已拒绝写入绑定。');
|
|
472
|
-
const bin = manifest.bin && typeof manifest.bin === 'object' && !Array.isArray(manifest.bin) ? manifest.bin['project-knowledge-mcp'] : manifest.bin;
|
|
473
|
-
const packageRoot = path.resolve(this.rootDir);
|
|
474
|
-
const entry = typeof bin === 'string' && bin ? path.resolve(packageRoot, bin) : '';
|
|
475
|
-
const entryError = 'package.json must declare an existing project-knowledge-mcp bin entry inside the package before binding the local MCP runtime.';
|
|
476
|
-
const lexical = entry ? path.relative(packageRoot, entry) : '..';
|
|
477
|
-
if (!entry || !lexical || lexical === '..' || lexical.startsWith('..' + path.sep) || path.isAbsolute(lexical)) throw new Error(entryError);
|
|
478
|
-
// The same realpath containment the launcher enforces at start: a link
|
|
479
|
-
// inside the package that resolves outside the real root must never be
|
|
480
|
-
// bound, and the entry must be a regular file.
|
|
481
|
-
let rootReal = '';
|
|
482
|
-
let entryReal = '';
|
|
483
|
-
try {
|
|
484
|
-
rootReal = fs.realpathSync(packageRoot);
|
|
485
|
-
entryReal = fs.realpathSync(entry);
|
|
486
|
-
} catch (error) { throw new Error(entryError); }
|
|
487
|
-
const relativeReal = path.relative(rootReal, entryReal);
|
|
488
|
-
if (!relativeReal || relativeReal === '..' || relativeReal.startsWith('..' + path.sep) || path.isAbsolute(relativeReal)) throw new Error(entryError);
|
|
489
|
-
let entryStat = null;
|
|
490
|
-
try { entryStat = fs.statSync(entryReal); } catch (error) { throw new Error(entryError); }
|
|
491
|
-
if (!entryStat.isFile()) throw new Error(entryError);
|
|
492
|
-
const bindingPath = this.integrationRuntimeBindingPath();
|
|
493
|
-
const binding = { schema: INTEGRATION_RUNTIME_SCHEMA, packageName: manifest.name, packageRoot, version: manifest.version, updatedAt: new Date().toISOString() };
|
|
494
|
-
try {
|
|
495
|
-
const existing = JSON.parse(fs.readFileSync(bindingPath, 'utf8'));
|
|
496
|
-
// An injected beforeRename means the caller explicitly wants the write
|
|
497
|
-
// path exercised (failure surfacing); only the quiet repair path may
|
|
498
|
-
// short-circuit to keep the binding byte- and mtime-idempotent.
|
|
499
|
-
const verifyWrite = typeof options.beforeRename === 'function';
|
|
500
|
-
if (!verifyWrite && existing && typeof existing === 'object' && existing.schema === INTEGRATION_RUNTIME_SCHEMA && existing.packageName === binding.packageName && existing.packageRoot === binding.packageRoot && existing.version === binding.version) {
|
|
501
|
-
return { state: 'unchanged', path: bindingPath, version: manifest.version };
|
|
502
|
-
}
|
|
503
|
-
} catch (error) { /* missing or unreadable bindings are rewritten below */ }
|
|
504
|
-
if (this.dryRun) {
|
|
505
|
-
this.planned.push({ command: 'runtime-binding', args: ['write', bindingPath] });
|
|
506
|
-
return { state: 'planned', path: bindingPath };
|
|
507
|
-
}
|
|
508
|
-
writeJsonAtomic(bindingPath, binding, typeof options.beforeRename === 'function' ? { beforeRename: options.beforeRename } : {});
|
|
509
|
-
return { state: 'written', path: bindingPath, version: manifest.version };
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
async probeMcpStartup(probeOptions = {}) {
|
|
513
|
-
const launcherPath = probeOptions.launcherPath || path.join(this.rootDir, MCP_LAUNCHER_FILE);
|
|
514
|
-
const scope = probeOptions.scope || 'product-runtime';
|
|
515
|
-
const expectedVersion = typeof probeOptions.version === 'string' && probeOptions.version ? probeOptions.version : this.version;
|
|
516
|
-
const timeoutMs = Number.isFinite(probeOptions.timeoutMs) ? probeOptions.timeoutMs : MCP_PROBE_TIMEOUT_MS;
|
|
517
|
-
const stderrCapBytes = Number.isFinite(probeOptions.stderrCapBytes) ? probeOptions.stderrCapBytes : MCP_PROBE_STDERR_CAP_BYTES;
|
|
518
|
-
const stdoutCapBytes = Number.isFinite(probeOptions.stdoutCapBytes) ? probeOptions.stdoutCapBytes : MCP_PROBE_STDOUT_CAP_BYTES;
|
|
519
|
-
const checkedAt = new Date().toISOString();
|
|
520
|
-
let Client = null;
|
|
521
|
-
let StdioClientTransport = null;
|
|
522
|
-
try {
|
|
523
|
-
const requireFromPackage = createRequire(path.join(this.rootDir, 'package.json'));
|
|
524
|
-
Client = requireFromPackage('@modelcontextprotocol/sdk/client/index.js').Client;
|
|
525
|
-
StdioClientTransport = requireFromPackage('@modelcontextprotocol/sdk/client/stdio.js').StdioClientTransport;
|
|
526
|
-
} catch (error) {
|
|
527
|
-
this.log('error', 'integration.mcp_probe_sdk_unavailable', 'The MCP SDK is unavailable; the probe fails explicitly instead of guessing.', { error, context: { scope } });
|
|
528
|
-
return { state: 'failed', checkedAt, scope, expectedVersion, code: 'PROBE_SDK_UNAVAILABLE', detail: '安装包内缺少 MCP SDK,无法执行启动探测;未据此判定客户端未接入。' };
|
|
529
|
-
}
|
|
530
|
-
return await new Promise(resolve => {
|
|
531
|
-
let client = null;
|
|
532
|
-
let timer = null;
|
|
533
|
-
let settled = false;
|
|
534
|
-
let stderrTail = '';
|
|
535
|
-
let stderrBytes = 0;
|
|
536
|
-
let stdoutBytes = 0;
|
|
537
|
-
const failed = (code, extra = {}) => {
|
|
538
|
-
if (settled) return;
|
|
539
|
-
settled = true;
|
|
540
|
-
if (timer) clearTimeout(timer);
|
|
541
|
-
resolve({ state: 'failed', checkedAt, scope, expectedVersion, code, ...extra, detail: MCP_PROBE_FAILURE_DETAILS[code] || '本地 MCP 启动探测未通过;诊断不回显上游文本。' });
|
|
542
|
-
};
|
|
543
|
-
const cleanup = async () => {
|
|
544
|
-
const closing = client;
|
|
545
|
-
client = null;
|
|
546
|
-
if (!closing) return;
|
|
547
|
-
try { await closing.close(); } catch (error) { /* the SDK escalates SIGTERM/SIGKILL on its own child only */ }
|
|
548
|
-
};
|
|
549
|
-
// The probe must exercise the manager's own isolated data dir; a host
|
|
550
|
-
// KB_DATA_DIR (for example a Workbench session export) must not leak in.
|
|
551
|
-
const probeEnv = { ...process.env, ...this.env, KB_DATA_DIR: path.dirname(this.integrationRuntimeBindingPath()) };
|
|
552
|
-
const transport = new StdioClientTransport({ command: process.execPath, args: [launcherPath], env: probeEnv, stderr: 'pipe' });
|
|
553
|
-
transport.stderr.on('data', chunk => {
|
|
554
|
-
stderrBytes += chunk.length;
|
|
555
|
-
stderrTail = (stderrTail + String(chunk)).slice(-4096);
|
|
556
|
-
if (stderrBytes > stderrCapBytes) {
|
|
557
|
-
failed('PROBE_OUTPUT_OVERFLOW');
|
|
558
|
-
cleanup();
|
|
559
|
-
}
|
|
560
|
-
});
|
|
561
|
-
// The SDK exposes no public stdout tap, so count bytes on the child
|
|
562
|
-
// stream right after start(): a hostile single-line flood fails at the
|
|
563
|
-
// cap instead of growing the SDK read buffer for the whole timeout.
|
|
564
|
-
const startTransport = transport.start.bind(transport);
|
|
565
|
-
transport.start = async () => {
|
|
566
|
-
await startTransport();
|
|
567
|
-
const stream = transport._process && transport._process.stdout;
|
|
568
|
-
if (stream) stream.on('data', chunk => {
|
|
569
|
-
stdoutBytes += chunk.length;
|
|
570
|
-
if (stdoutBytes > stdoutCapBytes) {
|
|
571
|
-
failed('PROBE_OUTPUT_OVERFLOW');
|
|
572
|
-
cleanup();
|
|
573
|
-
}
|
|
574
|
-
});
|
|
575
|
-
};
|
|
576
|
-
client = new Client({ name: 'project-knowledge-integration-probe', version: '1' }, { capabilities: {} });
|
|
577
|
-
timer = setTimeout(() => {
|
|
578
|
-
failed('PROBE_TIMEOUT');
|
|
579
|
-
cleanup();
|
|
580
|
-
}, timeoutMs);
|
|
581
|
-
(async () => {
|
|
582
|
-
try {
|
|
583
|
-
await client.connect(transport);
|
|
584
|
-
if (settled) return;
|
|
585
|
-
const serverVersion = client.getServerVersion();
|
|
586
|
-
if (!serverVersion || serverVersion.name !== MCP_SERVER_NAME) return failed('SERVER_IDENTITY');
|
|
587
|
-
// A foreign serverVersion string is untrusted text; only the fixed
|
|
588
|
-
// mismatch code is reported, never the value itself.
|
|
589
|
-
if (String(serverVersion.version || '') !== expectedVersion) return failed('VERSION_MISMATCH');
|
|
590
|
-
let listed = null;
|
|
591
|
-
try { listed = await client.listTools(); } catch (error) { return failed('TOOLS_CONTRACT', { toolCount: 0 }); }
|
|
592
|
-
if (settled) return;
|
|
593
|
-
const names = (listed && Array.isArray(listed.tools) ? listed.tools : []).map(tool => String((tool && tool.name) || '')).filter(Boolean);
|
|
594
|
-
if (names.length !== MCP_TOOL_NAMES.length || !MCP_TOOL_NAMES.every(name => names.includes(name))) return failed('TOOLS_CONTRACT', { toolCount: names.length });
|
|
595
|
-
settled = true;
|
|
596
|
-
clearTimeout(timer);
|
|
597
|
-
resolve({ state: 'ready', checkedAt, scope, expectedVersion, actualVersion: String(serverVersion.version || ''), serverName: serverVersion.name, tools: names, detail: '本地 MCP 服务已完成 initialize,并通过身份、版本与六工具契约校验。' });
|
|
598
|
-
} catch (error) {
|
|
599
|
-
if (!settled) {
|
|
600
|
-
const lines = stderrTail.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
601
|
-
const last = lines[lines.length - 1] || '';
|
|
602
|
-
try {
|
|
603
|
-
const payload = JSON.parse(last);
|
|
604
|
-
if (payload && payload.schema === MCP_LAUNCHER_ERROR_SCHEMA && payload.code) {
|
|
605
|
-
const launcherCode = String(payload.code);
|
|
606
|
-
return failed(MCP_LAUNCHER_ERROR_CODE_ALLOWLIST.includes(launcherCode) ? launcherCode : 'LAUNCHER_ERROR_UNKNOWN');
|
|
607
|
-
}
|
|
608
|
-
} catch { /* plain-text launcher diagnostics fall through to fixed codes */ }
|
|
609
|
-
if (error && (error.code === 'ENOENT' || /spawn/i.test(String((error && error.message) || error)))) return failed('PROBE_SPAWN_FAILED');
|
|
610
|
-
return failed('INITIALIZE_REJECTED');
|
|
611
|
-
}
|
|
612
|
-
} finally {
|
|
613
|
-
await cleanup();
|
|
614
|
-
}
|
|
615
|
-
})();
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
zcodeContextOptions() { return { configFile: this.zcodeConfigFile, rootDir: this.rootDir, dryRun: this.dryRun }; }
|
|
620
|
-
statusZCode() { return require('./zcode-context-integration').status(this.zcodeContextOptions()); }
|
|
621
|
-
installZCode() { return require('./zcode-context-integration').install(this.zcodeContextOptions()); }
|
|
622
|
-
updateZCode() { return this.installZCode(); }
|
|
623
|
-
uninstallZCode() { return require('./zcode-context-integration').uninstall(this.zcodeContextOptions()); }
|
|
624
|
-
|
|
625
|
-
execute(client, args, options = {}) {
|
|
626
|
-
const startedAt = Date.now();
|
|
627
|
-
const command = this.executable(client);
|
|
628
|
-
this.planned.push({ command: command || client, args: [...args] });
|
|
629
|
-
const argsHash = `sha256:${crypto.createHash('sha256').update(JSON.stringify(args), 'utf8').digest('hex')}`;
|
|
630
|
-
this.log('info', 'integration.command_started', 'Integration CLI command started.', { context: { client, argumentCount: args.length, argsHash, dryRun: this.dryRun } });
|
|
631
|
-
if (this.dryRun) {
|
|
632
|
-
this.log('info', 'integration.command_completed', 'Integration CLI command completed.', { durationMs: Date.now() - startedAt, context: { client, status: 0, argsHash, dryRun: true } });
|
|
633
|
-
return { status: 0, stdout: '', stderr: '' };
|
|
634
|
-
}
|
|
635
|
-
if (!command) {
|
|
636
|
-
this.log('error', 'integration.command_failed', 'Integration CLI command failed.', { durationMs: Date.now() - startedAt, error: Object.assign(new Error('Integration CLI is unavailable.'), { code: 'INTEGRATION_CLI_UNAVAILABLE' }), context: { client, argsHash } });
|
|
637
|
-
throw new Error(`${CLIENT_LABELS[client]} CLI is not installed or not on PATH`);
|
|
638
|
-
}
|
|
639
|
-
const result = this.runner(command, args, {
|
|
640
|
-
env: this.env,
|
|
641
|
-
cwd: options.cwd || this.rootDir,
|
|
642
|
-
timeout: options.timeout,
|
|
643
|
-
});
|
|
644
|
-
if (result.error) {
|
|
645
|
-
this.log('error', 'integration.command_failed', 'Integration CLI command failed.', { durationMs: Date.now() - startedAt, error: Object.assign(new Error('Integration CLI command failed.'), { code: result.error.code || 'INTEGRATION_COMMAND_FAILED' }), context: { client, argsHash } });
|
|
646
|
-
throw result.error;
|
|
647
|
-
}
|
|
648
|
-
const localRefFailure = client === 'codex' && String(result.stderr || '').trim() === 'Error: --ref is only supported for git marketplace sources';
|
|
649
|
-
if (result.status !== 0 && !options.allowFailure) {
|
|
650
|
-
const detail = String(result.stderr || result.stdout || '').trim();
|
|
651
|
-
this.log('error', 'integration.command_failed', 'Integration CLI command failed.', { durationMs: Date.now() - startedAt, error: Object.assign(new Error('Integration CLI command failed.'), { code: localRefFailure ? 'CODEX_LOCAL_SOURCE_REF' : 'INTEGRATION_COMMAND_FAILED' }), context: { client, status: result.status, argsHash, outputHash: `sha256:${crypto.createHash('sha256').update(detail, 'utf8').digest('hex')}` } });
|
|
652
|
-
throw new Error(detail || `${CLIENT_LABELS[client]} command failed with exit code ${result.status}`);
|
|
653
|
-
}
|
|
654
|
-
const completed = result.status === 0;
|
|
655
|
-
const safeError = completed ? undefined : Object.assign(new Error(localRefFailure
|
|
656
|
-
? 'Codex 把本地目录中的 @ 误当作 Git ref;请使用目录内相对路径重试。'
|
|
657
|
-
: 'Integration CLI command returned a failure.'), {
|
|
658
|
-
code: localRefFailure ? 'CODEX_LOCAL_SOURCE_REF' : 'INTEGRATION_COMMAND_FAILED',
|
|
659
|
-
});
|
|
660
|
-
this.log(completed ? 'info' : 'warn', completed ? 'integration.command_completed' : 'integration.command_failed', completed ? 'Integration CLI command completed.' : 'Integration CLI command returned a failure.', {
|
|
661
|
-
durationMs: Date.now() - startedAt, error: safeError,
|
|
662
|
-
context: { client, status: result.status, argsHash },
|
|
663
|
-
});
|
|
664
|
-
return result;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
claudePlugin() {
|
|
668
|
-
const result = this.execute('claude', ['plugin', 'list', '--json'], { allowFailure: true });
|
|
669
|
-
const plugins = parseJson(result.stdout, []);
|
|
670
|
-
return Array.isArray(plugins)
|
|
671
|
-
? plugins.find(item => item.id === `${INTEGRATION_NAME}@${MARKETPLACE_NAME}`)
|
|
672
|
-
: undefined;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
claudeMarketplaceExists() {
|
|
676
|
-
const result = this.execute('claude', ['plugin', 'marketplace', 'list', '--json'], { allowFailure: true });
|
|
677
|
-
const marketplaces = parseJson(result.stdout, []);
|
|
678
|
-
return Array.isArray(marketplaces) && marketplaces.some(item => item.name === MARKETPLACE_NAME);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
codexPlugin() {
|
|
682
|
-
const result = this.execute('codex', ['plugin', 'list', '--json'], { allowFailure: true });
|
|
683
|
-
const data = parseJson(result.stdout, {});
|
|
684
|
-
return Array.isArray(data?.installed)
|
|
685
|
-
? data.installed.find(item => item.pluginId === `${INTEGRATION_NAME}@${MARKETPLACE_NAME}`)
|
|
686
|
-
: undefined;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
codexMarketplaceExists() {
|
|
690
|
-
return this.codexMarketplaceRegisteredSource() !== null;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
codexMarketplaceRegisteredSource() {
|
|
694
|
-
const result = this.execute('codex', ['plugin', 'marketplace', 'list'], { allowFailure: true });
|
|
695
|
-
const line = String(result.stdout || '').split(/\r?\n/).map(item => item.trim()).find(item => item.startsWith(`${MARKETPLACE_NAME} `));
|
|
696
|
-
const source = line ? line.slice(MARKETPLACE_NAME.length).trim() : '';
|
|
697
|
-
return source || null;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
isOwnCodexMarketplaceSource(source) {
|
|
701
|
-
const text = String(source || '').trim();
|
|
702
|
-
if (!text) return false;
|
|
703
|
-
if (text.toLowerCase() === DEFAULT_MARKETPLACE_SOURCE.toLowerCase()) return true;
|
|
704
|
-
if (/^(?:git\+)?https:\/\/github\.com\/SanQianX\/project-knowledge-base(?:\.git)?\/?$/i.test(text)) return true;
|
|
705
|
-
if (!path.isAbsolute(text)) return false;
|
|
706
|
-
const directory = path.resolve(text);
|
|
707
|
-
let stat = null;
|
|
708
|
-
try { stat = fs.statSync(directory); } catch { return true; }
|
|
709
|
-
if (!stat.isDirectory()) return false;
|
|
710
|
-
try {
|
|
711
|
-
const manifest = JSON.parse(fs.readFileSync(path.join(directory, 'package.json'), 'utf8'));
|
|
712
|
-
return Boolean(manifest) && manifest.name === INTEGRATION_PACKAGE_NAME;
|
|
713
|
-
} catch { return false; }
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
ensureClaudeMarketplace() {
|
|
717
|
-
if (!this.explicitMarketplace && !fs.existsSync(path.join(this.rootDir, '.claude-plugin/marketplace.json'))) throw new Error('npm 包内的插件市场缺失;不会回退到 GitHub 下载。');
|
|
718
|
-
if (!this.dryRun && this.claudeMarketplaceExists()) {
|
|
719
|
-
if (this.explicitMarketplace) return;
|
|
720
|
-
let source;
|
|
721
|
-
try { source = JSON.parse(fs.readFileSync(path.join(this.homeDir, '.claude/plugins/known_marketplaces.json'), 'utf8'))[MARKETPLACE_NAME]?.source; } catch {}
|
|
722
|
-
const directory = source?.path || source?.directory;
|
|
723
|
-
if (directory && path.resolve(directory).toLowerCase() === path.resolve(this.rootDir).toLowerCase()) return;
|
|
724
|
-
const remote = source?.repo === DEFAULT_MARKETPLACE_SOURCE || /^https:\/\/github\.com\/SanQianX\/project-knowledge-base(?:\.git)?\/?$/i.test(source?.url || '');
|
|
725
|
-
if (!remote) throw new Error('同名插件市场来源不是当前 npm 包,且无法确认属于本产品;未覆盖第三方来源。请检查插件市场配置。');
|
|
726
|
-
this.execute('claude', ['plugin', 'marketplace', 'remove', MARKETPLACE_NAME]);
|
|
727
|
-
}
|
|
728
|
-
this.execute('claude', ['plugin', 'marketplace', 'add', this.claudeMarketplaceSource]);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
addCodexMarketplaceSource(source, options = {}) {
|
|
732
|
-
// Codex 0.137 parses @ in an absolute npm path as a Git ref. Resolve a
|
|
733
|
-
// real local directory via cwd instead; rollback must use its own cwd.
|
|
734
|
-
let local = false;
|
|
735
|
-
if (path.isAbsolute(source) && source.includes('@')) {
|
|
736
|
-
try { local = fs.statSync(source).isDirectory(); } catch {}
|
|
737
|
-
}
|
|
738
|
-
return this.execute('codex', ['plugin', 'marketplace', 'add', local ? '.' : source], local ? { ...options, cwd: source } : options);
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
ensureCodexMarketplace() {
|
|
742
|
-
const target = path.resolve(this.codexMarketplaceAddSource);
|
|
743
|
-
if (!this.dryRun) {
|
|
744
|
-
const existing = this.codexMarketplaceRegisteredSource();
|
|
745
|
-
if (existing !== null) {
|
|
746
|
-
if (this.explicitCodexMarketplace) return;
|
|
747
|
-
if (path.resolve(existing).toLowerCase() === target.toLowerCase()) return;
|
|
748
|
-
if (!this.isOwnCodexMarketplaceSource(existing)) throw new Error('同名 Codex 插件市场来源不是当前安装包,且无法确认属于本产品;未覆盖第三方来源。请检查 Codex 插件市场配置。');
|
|
749
|
-
this.execute('codex', ['plugin', 'marketplace', 'remove', MARKETPLACE_NAME]);
|
|
750
|
-
const added = this.addCodexMarketplaceSource(target, { allowFailure: true });
|
|
751
|
-
if (added.status !== 0) {
|
|
752
|
-
const restored = this.addCodexMarketplaceSource(existing, { allowFailure: true });
|
|
753
|
-
throw new Error(restored.status === 0
|
|
754
|
-
? 'Codex 插件市场更换为当前安装包失败,已恢复原市场来源;请重试接入。'
|
|
755
|
-
: 'Codex 插件市场更换为当前安装包失败,恢复原市场来源也未成功;请手动检查 codex 插件市场。');
|
|
756
|
-
}
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
this.addCodexMarketplaceSource(target);
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
installClaude(options = {}) {
|
|
764
|
-
this.ensureIntegrationRuntimeBinding();
|
|
765
|
-
let plugin = this.dryRun ? null : this.claudePlugin();
|
|
766
|
-
if (plugin?.enabled === false) throw new Error('知识库插件已被用户禁用,请在 Claude Code 中明确启用;未静默修改。');
|
|
767
|
-
this.ensureClaudeMarketplace();
|
|
768
|
-
plugin = this.dryRun ? null : this.claudePlugin();
|
|
769
|
-
if (plugin && (plugin.version !== this.version || !this.automaticContextStatus('claude', { state: 'installed', version: plugin.version }).configured)) return this.updateClaude(options);
|
|
770
|
-
if (plugin) {
|
|
771
|
-
return { state: 'installed', detail: 'plugin already installed' };
|
|
772
|
-
}
|
|
773
|
-
this.execute('claude', [
|
|
774
|
-
'plugin',
|
|
775
|
-
'install',
|
|
776
|
-
`${INTEGRATION_NAME}@${MARKETPLACE_NAME}`,
|
|
777
|
-
'--scope',
|
|
778
|
-
options.scope || 'user',
|
|
779
|
-
]);
|
|
780
|
-
if (!this.dryRun) this.verifyClaudeInstallation();
|
|
781
|
-
return { state: 'installed', detail: 'plugin, MCP, and Skill installed' };
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
updateClaude(options = {}) {
|
|
785
|
-
this.ensureIntegrationRuntimeBinding();
|
|
786
|
-
this.ensureClaudeMarketplace();
|
|
787
|
-
this.execute('claude', ['plugin', 'marketplace', 'update', MARKETPLACE_NAME]);
|
|
788
|
-
if (!this.dryRun && !this.claudePlugin()) return this.installClaude(options);
|
|
789
|
-
this.execute('claude', [
|
|
790
|
-
'plugin',
|
|
791
|
-
'update',
|
|
792
|
-
`${INTEGRATION_NAME}@${MARKETPLACE_NAME}`,
|
|
793
|
-
'--scope',
|
|
794
|
-
options.scope || 'user',
|
|
795
|
-
]);
|
|
796
|
-
if (!this.dryRun) {
|
|
797
|
-
this.verifyClaudeInstallation();
|
|
798
|
-
}
|
|
799
|
-
return { state: 'updated', detail: 'plugin marketplace and installed plugin updated' };
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
verifyClaudeInstallation() {
|
|
803
|
-
const plugin = this.claudePlugin();
|
|
804
|
-
if (!plugin || plugin.version !== this.version || plugin.enabled === false) throw new Error(`插件版本或启用状态未更新到 ${this.version},请检查 marketplace;未把旧插件标为成功。`);
|
|
805
|
-
const automatic = this.automaticContextStatus('claude', { state: 'installed', version: plugin.version });
|
|
806
|
-
if (!automatic.configured) throw new Error(`插件更新后前置读取仍未接线:${automatic.detail} 未误报全部成功。`);
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
uninstallClaude(options = {}) {
|
|
810
|
-
if (this.dryRun || this.claudePlugin()) {
|
|
811
|
-
this.execute('claude', [
|
|
812
|
-
'plugin',
|
|
813
|
-
'uninstall',
|
|
814
|
-
`${INTEGRATION_NAME}@${MARKETPLACE_NAME}`,
|
|
815
|
-
'--scope',
|
|
816
|
-
options.scope || 'user',
|
|
817
|
-
]);
|
|
818
|
-
}
|
|
819
|
-
if (this.dryRun || this.claudeMarketplaceExists()) {
|
|
820
|
-
this.execute('claude', ['plugin', 'marketplace', 'remove', MARKETPLACE_NAME]);
|
|
821
|
-
}
|
|
822
|
-
return { state: 'removed', detail: 'plugin and marketplace registration removed' };
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
statusClaude() {
|
|
826
|
-
const command = this.executable('claude');
|
|
827
|
-
if (!command) return { state: 'unavailable', installed: false, detail: 'Claude Code CLI not found' };
|
|
828
|
-
const plugin = this.claudePlugin();
|
|
829
|
-
return {
|
|
830
|
-
state: plugin?.enabled === false ? 'disabled' : plugin && plugin.version !== this.version ? 'needs-update' : plugin ? 'installed' : 'not-installed',
|
|
831
|
-
installed: Boolean(plugin),
|
|
832
|
-
version: plugin?.version,
|
|
833
|
-
expectedVersion: this.version,
|
|
834
|
-
detail: plugin?.enabled === false ? '插件已被禁用;未自动启用。' : plugin && plugin.version !== this.version ? `插件 ${plugin.version || '未知'} 与运行包 ${this.version} 不一致,请接入/修复。` : plugin ? '知识工具插件已配置;自动上下文状态单独验证。' : 'plugin not installed',
|
|
835
|
-
};
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
installCodex() {
|
|
839
|
-
this.ensureIntegrationRuntimeBinding();
|
|
840
|
-
this.ensureCodexMarketplace();
|
|
841
|
-
const plugin = this.dryRun ? null : this.codexPlugin();
|
|
842
|
-
if (plugin?.enabled === false) throw new Error('知识库插件已被用户禁用,请在 Codex 中明确启用,未静默修改。');
|
|
843
|
-
if (plugin && (plugin.version !== this.version || !this.codexContextFilesMatch(plugin))) return this.updateCodex();
|
|
844
|
-
if (plugin) {
|
|
845
|
-
return { state: 'installed', detail: 'plugin already installed' };
|
|
846
|
-
}
|
|
847
|
-
this.execute('codex', ['plugin', 'add', `${INTEGRATION_NAME}@${MARKETPLACE_NAME}`]);
|
|
848
|
-
if (!this.dryRun) this.verifyCodexInstallation();
|
|
849
|
-
return { state: 'installed', detail: '插件已安装;在原生 /hooks 中审查并信任前置读取 Hook 后才会运行。' };
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
updateCodex() {
|
|
853
|
-
this.ensureIntegrationRuntimeBinding();
|
|
854
|
-
if (!this.dryRun && this.codexPlugin()?.enabled === false) throw new Error('知识库插件已被用户禁用,请在 Codex 中明确启用,未静默修改。');
|
|
855
|
-
this.ensureCodexMarketplace();
|
|
856
|
-
const refreshed = this.execute('codex', ['plugin', 'marketplace', 'upgrade', MARKETPLACE_NAME], { allowFailure: true });
|
|
857
|
-
// Native refresh is Git-only. A local catalogue already points at current
|
|
858
|
-
// files; reinstall those files, but never hide a real Git/network failure.
|
|
859
|
-
if (refreshed.status !== 0 && !/^Error: marketplace `project-knowledge` is not configured as a Git marketplace\s*$/.test(String(refreshed.stderr || refreshed.stdout || '').trim())) {
|
|
860
|
-
throw new Error(String(refreshed.stderr || refreshed.stdout || '').trim() || 'Codex marketplace refresh failed.');
|
|
861
|
-
}
|
|
862
|
-
if (this.dryRun || this.codexPlugin()) {
|
|
863
|
-
this.execute('codex', ['plugin', 'remove', `${INTEGRATION_NAME}@${MARKETPLACE_NAME}`]);
|
|
864
|
-
}
|
|
865
|
-
this.execute('codex', ['plugin', 'add', `${INTEGRATION_NAME}@${MARKETPLACE_NAME}`]);
|
|
866
|
-
if (!this.dryRun) this.verifyCodexInstallation();
|
|
867
|
-
return { state: 'updated', detail: '知识插件已更新;新定义须在原生 /hooks 中审查并信任,未写入信任或跳过审查。' };
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
codexContextFilesMatch(plugin) {
|
|
871
|
-
const directory = plugin?.source?.path;
|
|
872
|
-
if (typeof directory !== 'string' || !path.isAbsolute(directory)) return false;
|
|
873
|
-
return ['hooks/codex.json', 'scripts/context-hook.cjs', '.mcp.json', '.codex-plugin/plugin.json'].every(file => {
|
|
874
|
-
try { const read = root => new TextDecoder('utf-8', { fatal: true }).decode(fs.readFileSync(path.join(root, file))).replace(/\r\n/g, '\n'); return read(directory) === read(path.join(this.rootDir, 'plugins', INTEGRATION_NAME)); } catch { return false; }
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
verifyCodexInstallation() {
|
|
879
|
-
const plugin = this.codexPlugin();
|
|
880
|
-
if (!plugin || plugin.version !== this.version || plugin.enabled === false || !this.codexContextFilesMatch(plugin)) throw new Error(`Codex 插件或前置读取文件未更新到 ${this.version},未把旧插件标为成功。`);
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
async automaticCodexContextStatus(knowledge, cwd = this.homeDir) {
|
|
884
|
-
const basic = this.automaticContextStatus('codex', knowledge);
|
|
885
|
-
if (!basic.filesVerified) return basic;
|
|
886
|
-
try {
|
|
887
|
-
const snapshot = await this.codexHookProbe({ command: this.executable('codex'), env: { ...this.env, CODEX_HOME: this.env.CODEX_HOME || path.dirname(this.codexConfigFile) }, cwd });
|
|
888
|
-
const entries = snapshot?.data || [];
|
|
889
|
-
const hooks = entries.flatMap(entry => entry.hooks || []).filter(hook => hook.eventName === 'userPromptSubmit' && hook.pluginId === `${INTEGRATION_NAME}@${MARKETPLACE_NAME}` && /context-hook\.cjs/.test(hook.command || ''));
|
|
890
|
-
const configured = hooks.length === 1 && hooks[0].enabled === true && ['trusted', 'managed'].includes(hooks[0].trustStatus) && entries.every(entry => !entry.errors?.length);
|
|
891
|
-
const disabled = hooks.some(h => h.enabled === false);
|
|
892
|
-
return { ...basic, configured, state: configured ? 'configured' : disabled ? 'disabled' : hooks.length ? 'waiting-trust' : 'needs-repair', trustStatus: hooks[0]?.trustStatus || 'unknown', detail: configured ? '原生前置读取定义已启用并信任;实际读取见知识上下文日志。' : disabled ? 'Codex 前置读取 Hook 已禁用,未擅自启用。' : hooks.length ? '待信任:请在 Codex 原生 /hooks 中审查 Project Knowledge 的 UserPromptSubmit 定义;更新后需重新审查。' : 'Codex 未加载前置读取 Hook,请检查客户端版本、Hook 开关及插件来源。' };
|
|
893
|
-
} catch { return { ...basic, state: 'check-failed', configured: false, detail: '无法验证原生 Hook 的信任状态;未将已安装插件当作自动读取已就绪。请在 Codex 中打开 /hooks 检查。' }; }
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
uninstallCodex() {
|
|
897
|
-
if (this.dryRun || this.codexPlugin()) {
|
|
898
|
-
this.execute('codex', ['plugin', 'remove', `${INTEGRATION_NAME}@${MARKETPLACE_NAME}`]);
|
|
899
|
-
}
|
|
900
|
-
if (this.dryRun || this.codexMarketplaceExists()) {
|
|
901
|
-
this.execute('codex', ['plugin', 'marketplace', 'remove', MARKETPLACE_NAME]);
|
|
902
|
-
}
|
|
903
|
-
return { state: 'removed', detail: 'plugin and marketplace registration removed' };
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
statusCodex() {
|
|
907
|
-
const command = this.executable('codex');
|
|
908
|
-
if (!command) return { state: 'unavailable', installed: false, detail: 'Codex CLI not found' };
|
|
909
|
-
const plugin = this.codexPlugin();
|
|
910
|
-
return {
|
|
911
|
-
state: plugin?.enabled === false ? 'disabled' : plugin && plugin.version !== this.version ? 'needs-update' : plugin ? 'installed' : 'not-installed',
|
|
912
|
-
installed: Boolean(plugin),
|
|
913
|
-
version: plugin?.version,
|
|
914
|
-
expectedVersion: this.version,
|
|
915
|
-
source: plugin?.source,
|
|
916
|
-
detail: plugin?.enabled === false ? '插件已禁用,未自动启用。' : plugin && plugin.version !== this.version ? '知识插件版本不匹配,请更新后重新审查原生 Hook。' : plugin ? '知识插件已安装;原生 Hook 信任状态单独验证。' : 'plugin not installed',
|
|
917
|
-
};
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
openCodeSkillSource() {
|
|
921
|
-
return path.join(
|
|
922
|
-
this.rootDir,
|
|
923
|
-
'plugins',
|
|
924
|
-
INTEGRATION_NAME,
|
|
925
|
-
'skills',
|
|
926
|
-
INTEGRATION_NAME,
|
|
927
|
-
);
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
openCodeInstructionSource() {
|
|
931
|
-
return path.join(this.rootDir, 'plugins', INTEGRATION_NAME, 'opencode', `${INTEGRATION_NAME}.md`);
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
openCodeConfigDir() {
|
|
935
|
-
return openCodeConfigDir(this.homeDir, this.env);
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
installOpenCode() {
|
|
939
|
-
const source = this.openCodeSkillSource();
|
|
940
|
-
const instructionSource = this.openCodeInstructionSource();
|
|
941
|
-
const configDir = this.openCodeConfigDir();
|
|
942
|
-
const destination = path.join(this.openCodeConfigDir(), 'skills', INTEGRATION_NAME);
|
|
943
|
-
const instructionDestination = path.join(configDir, 'instructions', `${INTEGRATION_NAME}.md`);
|
|
944
|
-
if (!this.dryRun) {
|
|
945
|
-
if (!fs.existsSync(path.join(source, 'SKILL.md'))) throw new Error(`bundled Skill not found: ${source}`);
|
|
946
|
-
if (!fs.existsSync(instructionSource)) throw new Error(`bundled OpenCode instructions not found: ${instructionSource}`);
|
|
947
|
-
fs.mkdirSync(destination, { recursive: true });
|
|
948
|
-
fs.cpSync(source, destination, { recursive: true, force: true });
|
|
949
|
-
fs.mkdirSync(path.dirname(instructionDestination), { recursive: true });
|
|
950
|
-
fs.copyFileSync(instructionSource, instructionDestination);
|
|
951
|
-
}
|
|
952
|
-
this.planned.push({ copy: source, destination });
|
|
953
|
-
this.planned.push({ copy: instructionSource, destination: instructionDestination });
|
|
954
|
-
this.execute('opencode', [
|
|
955
|
-
'mcp',
|
|
956
|
-
'add',
|
|
957
|
-
INTEGRATION_NAME,
|
|
958
|
-
'--',
|
|
959
|
-
'npx',
|
|
960
|
-
'-y',
|
|
961
|
-
'--package',
|
|
962
|
-
this.packageSpec,
|
|
963
|
-
'project-knowledge-mcp',
|
|
964
|
-
]);
|
|
965
|
-
if (!this.dryRun) ensureOpenCodeInstructionConfig(configDir, instructionDestination);
|
|
966
|
-
return { state: 'installed', detail: 'MCP, global Skill, and Instructions installed' };
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
updateOpenCode() {
|
|
970
|
-
const result = this.installOpenCode();
|
|
971
|
-
return { ...result, state: 'updated', detail: 'MCP configuration, global Skill, and Instructions updated' };
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
uninstallOpenCode() {
|
|
975
|
-
const configDir = this.openCodeConfigDir();
|
|
976
|
-
const skillDir = path.join(configDir, 'skills', INTEGRATION_NAME);
|
|
977
|
-
const instructionPath = path.join(configDir, 'instructions', `${INTEGRATION_NAME}.md`);
|
|
978
|
-
if (!this.dryRun) {
|
|
979
|
-
removeOpenCodeMcpConfig(configDir);
|
|
980
|
-
removeOpenCodeInstructionConfig(configDir, instructionPath);
|
|
981
|
-
if (fs.existsSync(skillDir)) fs.rmSync(skillDir, { recursive: true, force: true });
|
|
982
|
-
if (fs.existsSync(instructionPath)) fs.rmSync(instructionPath, { force: true });
|
|
983
|
-
}
|
|
984
|
-
this.planned.push({
|
|
985
|
-
removeConfig: findOpenCodeConfig(configDir),
|
|
986
|
-
removeDirectory: skillDir,
|
|
987
|
-
removeFile: instructionPath,
|
|
988
|
-
});
|
|
989
|
-
return { state: 'removed', detail: 'MCP configuration, global Skill, and Instructions removed' };
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
statusOpenCode() {
|
|
993
|
-
const available = Boolean(this.executable('opencode'));
|
|
994
|
-
const state = readOpenCodeState(this.openCodeConfigDir());
|
|
995
|
-
const installed = state.mcpInstalled && state.skillInstalled && state.instructionInstalled;
|
|
996
|
-
let detail = 'MCP, Skill, and Instructions not installed';
|
|
997
|
-
if (installed) detail = 'MCP, global Skill, and Instructions installed';
|
|
998
|
-
else if (state.mcpInstalled && state.skillInstalled) detail = 'MCP and Skill installed; Instructions missing';
|
|
999
|
-
else if (state.mcpInstalled) detail = 'MCP installed; Skill missing';
|
|
1000
|
-
else if (state.skillInstalled) detail = 'Skill installed; MCP missing';
|
|
1001
|
-
if (!available) detail = `OpenCode CLI not found; ${detail}`;
|
|
1002
|
-
return {
|
|
1003
|
-
state: installed ? 'installed' : available ? 'not-installed' : 'unavailable',
|
|
1004
|
-
installed,
|
|
1005
|
-
detail,
|
|
1006
|
-
configPath: state.configPath,
|
|
1007
|
-
skillPath: state.skillPath,
|
|
1008
|
-
instructionPath: state.instructionPath,
|
|
1009
|
-
};
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
async operate(operation, clients, options = {}) {
|
|
1013
|
-
const results = [];
|
|
1014
|
-
let bindingFailure = null;
|
|
1015
|
-
if (operation === 'install' || operation === 'update') {
|
|
1016
|
-
// The runtime binding is a precondition for knowledge installs: a
|
|
1017
|
-
// binding failure must fail the knowledge result, never report success.
|
|
1018
|
-
try {
|
|
1019
|
-
this.ensureIntegrationRuntimeBinding();
|
|
1020
|
-
} catch (error) {
|
|
1021
|
-
bindingFailure = Object.assign(new Error('本地 MCP 运行时绑定校验失败,已阻止知识接入;采集能力不受影响。'), { code: 'INTEGRATION_RUNTIME_BINDING_FAILED' });
|
|
1022
|
-
this.log('error', 'integration.runtime_binding_failed', 'Failed to write the local MCP runtime binding.', { error, context: { operation } });
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
for (const rawClient of clients) {
|
|
1026
|
-
const client = normalizeClient(rawClient);
|
|
1027
|
-
try {
|
|
1028
|
-
if (bindingFailure) throw bindingFailure;
|
|
1029
|
-
const suffix = CLIENT_METHOD_SUFFIX[client];
|
|
1030
|
-
let result;
|
|
1031
|
-
if (operation === 'status') result = this[`status${suffix}`]();
|
|
1032
|
-
else if (operation === 'install') result = this[`install${suffix}`](options);
|
|
1033
|
-
else if (operation === 'update') result = this[`update${suffix}`](options);
|
|
1034
|
-
else if (operation === 'uninstall') result = this[`uninstall${suffix}`](options);
|
|
1035
|
-
else throw new Error(`unsupported operation: ${operation}`);
|
|
1036
|
-
if (this.dryRun && operation !== 'status') {
|
|
1037
|
-
result = { ...result, state: 'planned', detail: `${result.detail} (dry-run)` };
|
|
1038
|
-
}
|
|
1039
|
-
results.push({ client, label: CLIENT_LABELS[client], ok: true, ...result });
|
|
1040
|
-
} catch (error) {
|
|
1041
|
-
results.push({
|
|
1042
|
-
client,
|
|
1043
|
-
label: CLIENT_LABELS[client],
|
|
1044
|
-
ok: false,
|
|
1045
|
-
state: 'failed',
|
|
1046
|
-
detail: error.message,
|
|
1047
|
-
...(error.code ? { code: error.code } : {}),
|
|
1048
|
-
});
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
return results;
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
async operateCapture(operation, clients, options = {}) {
|
|
1055
|
-
const results = [];
|
|
1056
|
-
for (const rawClient of clients) {
|
|
1057
|
-
const client = normalizeClient(rawClient);
|
|
1058
|
-
results.push({ client, label: CLIENT_LABELS[client], ...(await this.captureOperation(operation, client, options)) });
|
|
1059
|
-
}
|
|
1060
|
-
return results;
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
// One-click Integration Setup: register the host Bridge consumer once,
|
|
1064
|
-
// then install BOTH capabilities per client. Knowledge Integration and
|
|
1065
|
-
// Development Capture are reported separately; one failing component never
|
|
1066
|
-
// rolls back or blocks the others.
|
|
1067
|
-
async installAll(options = {}) {
|
|
1068
|
-
const clients = options.clients && options.clients.length ? options.clients.map(normalizeClient) : this.detectClients();
|
|
1069
|
-
const summary = { clients: [], bridge: null };
|
|
1070
|
-
try {
|
|
1071
|
-
summary.bridge = { ok: true, ...(await this.registerBridgeConsumer(options.bridgeConsumerMeta || {})) };
|
|
1072
|
-
} catch (error) {
|
|
1073
|
-
summary.bridge = { ok: false, state: 'failed', detail: error.message };
|
|
1074
|
-
}
|
|
1075
|
-
let bindingFailure = null;
|
|
1076
|
-
if (options.captureOnly !== true) {
|
|
1077
|
-
try {
|
|
1078
|
-
summary.runtimeBinding = { ok: true, ...this.ensureIntegrationRuntimeBinding() };
|
|
1079
|
-
} catch (error) {
|
|
1080
|
-
summary.runtimeBinding = { ok: false, state: 'failed', detail: error.message };
|
|
1081
|
-
bindingFailure = Object.assign(new Error('本地 MCP 运行时绑定校验失败,已阻止知识接入;采集能力不受影响。'), { code: 'INTEGRATION_RUNTIME_BINDING_FAILED' });
|
|
1082
|
-
this.log('error', 'integration.runtime_binding_failed', 'Failed to write the local MCP runtime binding.', { error, context: { operation: 'installAll' } });
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
for (const client of clients) {
|
|
1086
|
-
const suffix = CLIENT_METHOD_SUFFIX[client];
|
|
1087
|
-
const entry = { client, label: CLIENT_LABELS[client] };
|
|
1088
|
-
if (options.captureOnly !== true) {
|
|
1089
|
-
if (bindingFailure) {
|
|
1090
|
-
entry.knowledgeIntegration = { ok: false, state: 'failed', code: bindingFailure.code, detail: bindingFailure.message };
|
|
1091
|
-
} else {
|
|
1092
|
-
try {
|
|
1093
|
-
entry.knowledgeIntegration = { ok: true, ...(await this[`install${suffix}`](options)) };
|
|
1094
|
-
} catch (error) {
|
|
1095
|
-
entry.knowledgeIntegration = { ok: false, state: 'failed', detail: error.message };
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
if (options.knowledgeOnly !== true) {
|
|
1100
|
-
entry.developmentCapture = await this.captureOperation('install', client);
|
|
1101
|
-
}
|
|
1102
|
-
summary.clients.push(entry);
|
|
1103
|
-
}
|
|
1104
|
-
return summary;
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
installedMcpLauncherPaths(client) {
|
|
1108
|
-
const launchers = [];
|
|
1109
|
-
const seen = new Set();
|
|
1110
|
-
const consider = directory => {
|
|
1111
|
-
if (typeof directory !== 'string' || !path.isAbsolute(directory)) return;
|
|
1112
|
-
const launcher = path.join(directory, 'scripts', 'mcp-launcher.cjs');
|
|
1113
|
-
if (seen.has(launcher)) return;
|
|
1114
|
-
try { if (!fs.statSync(launcher).isFile()) return; } catch { return; }
|
|
1115
|
-
seen.add(launcher);
|
|
1116
|
-
launchers.push(launcher);
|
|
1117
|
-
};
|
|
1118
|
-
if (client === 'claude') {
|
|
1119
|
-
try {
|
|
1120
|
-
const index = JSON.parse(fs.readFileSync(path.join(this.homeDir, '.claude', 'plugins', 'installed_plugins.json'), 'utf8'));
|
|
1121
|
-
for (const record of (index.plugins?.[`${INTEGRATION_NAME}@${MARKETPLACE_NAME}`] || [])) {
|
|
1122
|
-
if (record && record.version === this.version) consider(record.installPath);
|
|
1123
|
-
}
|
|
1124
|
-
} catch { /* no native install index yet */ }
|
|
1125
|
-
consider(path.join(this.homeDir, '.claude', 'plugins', 'cache', MARKETPLACE_NAME, INTEGRATION_NAME, this.version));
|
|
1126
|
-
}
|
|
1127
|
-
if (client === 'codex' && this.executable('codex')) {
|
|
1128
|
-
try { consider(this.codexPlugin()?.source?.path); } catch { /* the client CLI failed to list plugins; there is no installed entry to probe */ }
|
|
1129
|
-
}
|
|
1130
|
-
return launchers;
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
async statusAll(options = {}) {
|
|
1134
|
-
const clients = options.clients && options.clients.length ? options.clients.map(normalizeClient) : SUPPORTED_CLIENTS;
|
|
1135
|
-
const summary = { clients: [], bridge: null };
|
|
1136
|
-
try {
|
|
1137
|
-
summary.bridge = { ok: true, ...(await this.bridgeStatus()) };
|
|
1138
|
-
} catch (error) {
|
|
1139
|
-
summary.bridge = { ok: false, state: 'failed', bridgeHealthy: false, detail: error.message };
|
|
1140
|
-
}
|
|
1141
|
-
// Probes start in parallel so one hung launcher costs a single timeout.
|
|
1142
|
-
// Client probes exercise the entry that client would actually launch
|
|
1143
|
-
// (never the host source tree); identical entries share one host check,
|
|
1144
|
-
// and the shared product-runtime probe covers the shipping runtime.
|
|
1145
|
-
const runProbe = info => (this.mcpProbe ? this.mcpProbe(info) : this.probeMcpStartup(info)).catch(() => ({ state: 'failed', checkedAt: new Date().toISOString(), code: 'PROBE_CRASH', expectedVersion: this.version, detail: MCP_PROBE_FAILURE_DETAILS.PROBE_CRASH }));
|
|
1146
|
-
const clientProbes = new Map();
|
|
1147
|
-
const hostProbe = options.captureOnly ? null : runProbe({ client: 'host', scope: 'product-runtime', version: this.version });
|
|
1148
|
-
if (!options.captureOnly) {
|
|
1149
|
-
const sharedByLauncher = new Map();
|
|
1150
|
-
for (const client of clients) {
|
|
1151
|
-
if (client !== 'claude' && client !== 'codex') continue;
|
|
1152
|
-
const launcherPath = this.installedMcpLauncherPaths(client)[0];
|
|
1153
|
-
if (!launcherPath) continue;
|
|
1154
|
-
if (!sharedByLauncher.has(launcherPath)) sharedByLauncher.set(launcherPath, runProbe({ client, scope: `client:${client}`, launcherPath, version: this.version }));
|
|
1155
|
-
clientProbes.set(client, sharedByLauncher.get(launcherPath).then(result => ({ ...result, scope: `client:${client}` })));
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
for (const client of clients) {
|
|
1159
|
-
const suffix = CLIENT_METHOD_SUFFIX[client];
|
|
1160
|
-
const entry = { client, label: CLIENT_LABELS[client], available: Boolean(this.executable(client)), configFound: this.captureConfigFound(client) };
|
|
1161
|
-
if (!options.captureOnly) {
|
|
1162
|
-
try {
|
|
1163
|
-
entry.knowledgeIntegration = { ok: true, ...(await this[`status${suffix}`]()) };
|
|
1164
|
-
} catch (error) {
|
|
1165
|
-
entry.knowledgeIntegration = { ok: false, state: 'failed', detail: error.message };
|
|
1166
|
-
}
|
|
1167
|
-
entry.automaticContext = client === 'codex' ? await this.automaticCodexContextStatus(entry.knowledgeIntegration, options.cwd) : this.automaticContextStatus(client, entry.knowledgeIntegration);
|
|
1168
|
-
if (client === 'claude' || client === 'codex') {
|
|
1169
|
-
entry.mcpStartup = clientProbes.has(client)
|
|
1170
|
-
? await clientProbes.get(client)
|
|
1171
|
-
: { state: 'failed', checkedAt: new Date().toISOString(), scope: `client:${client}`, code: 'CLIENT_PLUGIN_ENTRY_MISSING', expectedVersion: this.version, detail: '该客户端没有已安装的知识插件入口;未用宿主源码入口代替客户端实际启动入口做健康判定。' };
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
entry.developmentCapture = await this.captureOperation('status', client);
|
|
1175
|
-
summary.clients.push(entry);
|
|
1176
|
-
}
|
|
1177
|
-
if (hostProbe) summary.mcpRuntime = await hostProbe;
|
|
1178
|
-
return summary;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
automaticContextStatus(client, knowledge) {
|
|
1182
|
-
if (client === 'zcode') { const available = Boolean(this.executable('zcode')); return { supported: true, configured: available && knowledge?.state === 'installed', state: available ? knowledge?.state || 'not-installed' : 'unavailable', detail: available ? knowledge?.detail || '前置读取尚未接入。' : '未找到可运行的 ZCode;配置存在不等于原生入口可用。' }; }
|
|
1183
|
-
if (client === 'codex') {
|
|
1184
|
-
const filesVerified = knowledge?.state === 'installed' && this.codexContextFilesMatch({ source: knowledge.source });
|
|
1185
|
-
return { supported: true, configured: false, filesVerified, state: filesVerified ? 'waiting-trust' : knowledge?.state === 'installed' ? 'needs-repair' : knowledge?.state || 'not-installed', expectedVersion: this.version, detail: filesVerified ? '已安装,等待验证原生 Hook 的信任状态。' : '插件版本、启用或前置读取文件尚未通过验证,请接入/修复。' };
|
|
1186
|
-
}
|
|
1187
|
-
if (client !== 'claude') return { supported: false, state: 'not-supported', configured: false, detail: '当前版本尚未验收此客户端的自动前置读取;知识工具与采集状态单独显示。' };
|
|
1188
|
-
const state = knowledge?.state || 'not-installed';
|
|
1189
|
-
try { if (JSON.parse(fs.readFileSync(this.claudeSettingsFile, 'utf8')).disableAllHooks === true) return { supported: true, state: 'disabled', configured: false, detail: 'Claude Code 的 Hook 已被用户禁用,未擅自修改。' }; } catch {}
|
|
1190
|
-
if (state !== 'installed') return { supported: true, state, configured: false, expectedVersion: this.version, detail: state === 'needs-update' ? '知识插件版本不匹配,请接入/修复后重新打开 Claude Code。' : state === 'disabled' ? '插件已被禁用;请在客户端明确启用。' : '前置知识读取尚未接入。' };
|
|
1191
|
-
if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.-]+)?$/.test(knowledge.version || '')) return { supported: true, state: 'needs-update', configured: false, detail: '无法验证插件版本。' };
|
|
1192
|
-
let locations = [];
|
|
1193
|
-
try {
|
|
1194
|
-
const index = JSON.parse(fs.readFileSync(path.join(this.homeDir, '.claude/plugins/installed_plugins.json'), 'utf8'));
|
|
1195
|
-
locations = (index.plugins?.[`${INTEGRATION_NAME}@${MARKETPLACE_NAME}`] || []).filter(p => p.version === knowledge.version).map(p => p.installPath);
|
|
1196
|
-
} catch {}
|
|
1197
|
-
if (!locations.length) locations.push(path.join(this.homeDir, '.claude/plugins/cache', MARKETPLACE_NAME, INTEGRATION_NAME, knowledge.version));
|
|
1198
|
-
const files = ['hooks/claude.json', 'scripts/context-hook.cjs', '.mcp.json', '.claude-plugin/plugin.json'];
|
|
1199
|
-
const configured = locations.some(directory => typeof directory === 'string' && files.every(file => {
|
|
1200
|
-
// Native marketplaces use Git checkout newline rules; npm integrity
|
|
1201
|
-
// remains byte-exact elsewhere. Only normalize text line endings here.
|
|
1202
|
-
try {
|
|
1203
|
-
const text = target => new TextDecoder('utf-8', { fatal: true }).decode(fs.readFileSync(target)).replace(/\r\n/g, '\n');
|
|
1204
|
-
return text(path.join(directory, file)) === text(path.join(this.rootDir, 'plugins', INTEGRATION_NAME, file));
|
|
1205
|
-
} catch { return false; }
|
|
1206
|
-
}));
|
|
1207
|
-
return { supported: true, state: configured ? 'configured' : 'needs-repair', configured, expectedVersion: this.version, detail: configured ? '前置读取脚本已接线;请重新打开会话。具体需求是否已读取请查看 agent.context 与 knowledge.context 日志。' : '插件前置读取文件缺失或内容不匹配,请更新/修复插件。' };
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
// Global disable: remove every connector first, then (and only then)
|
|
1211
|
-
// unregister the host Bridge consumer (GATE INSTALL-003 semantics).
|
|
1212
|
-
async disableAllCapture(options = {}) {
|
|
1213
|
-
const clients = options.clients && options.clients.length ? options.clients.map(normalizeClient) : SUPPORTED_CLIENTS;
|
|
1214
|
-
const results = await this.operateCapture('uninstall', clients, { unregisterConsumer: false });
|
|
1215
|
-
if (results.some(result => !result.ok)) return { clients: results, consumer: { ok: false, state: 'kept', detail: '部分采集未能移除,保留宿主消费者,请修复失败项后重试。' } };
|
|
1216
|
-
let consumer = null;
|
|
1217
|
-
try {
|
|
1218
|
-
consumer = { ok: true, ...(await this.unregisterBridgeConsumer()) };
|
|
1219
|
-
} catch (error) {
|
|
1220
|
-
consumer = { ok: false, state: 'failed', detail: error.message };
|
|
1221
|
-
}
|
|
1222
|
-
return { clients: results, consumer };
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
module.exports = {
|
|
1227
|
-
CLIENT_LABELS,
|
|
1228
|
-
DEFAULT_MARKETPLACE_SOURCE,
|
|
1229
|
-
INTEGRATION_NAME,
|
|
1230
|
-
IntegrationManager,
|
|
1231
|
-
MARKETPLACE_NAME,
|
|
1232
|
-
SUPPORTED_CLIENTS,
|
|
1233
|
-
findOpenCodeConfig,
|
|
1234
|
-
ensureOpenCodeInstructionConfig,
|
|
1235
|
-
normalizeClient,
|
|
1236
|
-
openCodeConfigDir,
|
|
1237
|
-
readOpenCodeState,
|
|
1238
|
-
removeOpenCodeMcpConfig,
|
|
1239
|
-
removeOpenCodeInstructionConfig,
|
|
1240
|
-
};
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const { createRequire } = require('node:module');
|
|
6
|
+
const { spawn, spawnSync } = require('child_process');
|
|
7
|
+
const crossSpawn = require('cross-spawn');
|
|
8
|
+
const { applyEdits, modify, parse } = require('jsonc-parser');
|
|
9
|
+
const packageInfo = require('../../package.json');
|
|
10
|
+
const { resolveDataDirPath } = require('./data-dir');
|
|
11
|
+
const { planNotify, writeNotify } = require('./codex-notify-config');
|
|
12
|
+
const { planCaptureHooks, repairCaptureHooks, removeCaptureHooks } = require('./capture-hook-config');
|
|
13
|
+
const { writeJsonAtomic } = require('./atomic-file');
|
|
14
|
+
|
|
15
|
+
// Fail-open module resolution: a missing/unloadable Bridge package must
|
|
16
|
+
// degrade Development Capture status, never crash server startup.
|
|
17
|
+
let bridgeModule = null;
|
|
18
|
+
try {
|
|
19
|
+
bridgeModule = require('@sanqianx/ai-coding-event-bridge-console/src/bridge');
|
|
20
|
+
} catch (_) {
|
|
21
|
+
bridgeModule = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const INTEGRATION_NAME = 'project-knowledge';
|
|
25
|
+
const MARKETPLACE_NAME = 'project-knowledge';
|
|
26
|
+
const DEFAULT_MARKETPLACE_SOURCE = 'SanQianX/project-knowledge-base';
|
|
27
|
+
const INTEGRATION_RUNTIME_SCHEMA = 'project-knowledge/integration-runtime/v1';
|
|
28
|
+
const MCP_LAUNCHER_ERROR_SCHEMA = 'project-knowledge/mcp-launcher-error/v1';
|
|
29
|
+
const INTEGRATION_PACKAGE_NAME = '@sanqianx/project-knowledge';
|
|
30
|
+
const MCP_SERVER_NAME = 'project-knowledge';
|
|
31
|
+
const MCP_TOOL_NAMES = ['project_knowledge_resolve', 'project_knowledge_record_requirement', 'project_knowledge_search', 'project_knowledge_ask', 'project_knowledge_get', 'project_knowledge_history'];
|
|
32
|
+
const MCP_LAUNCHER_FILE = path.join('plugins', INTEGRATION_NAME, 'scripts', 'mcp-launcher.cjs');
|
|
33
|
+
const MCP_PROBE_TIMEOUT_MS = 8000;
|
|
34
|
+
const MCP_PROBE_STDERR_CAP_BYTES = 32768;
|
|
35
|
+
const MCP_PROBE_STDOUT_CAP_BYTES = 32768;
|
|
36
|
+
const MCP_PROBE_FAILURE_DETAILS = {
|
|
37
|
+
PROBE_SPAWN_FAILED: '无法启动本地 MCP 启动进程;请检查 Node 运行时。',
|
|
38
|
+
INITIALIZE_REJECTED: '本地 MCP 服务拒绝了 initialize 握手;未回显上游错误内容。',
|
|
39
|
+
SERVER_IDENTITY: 'MCP 服务身份不是 project-knowledge,判定未通过。',
|
|
40
|
+
VERSION_MISMATCH: 'MCP 服务版本与当前产品版本不一致。',
|
|
41
|
+
TOOLS_CONTRACT: 'MCP 服务未提供完整的六个知识工具。',
|
|
42
|
+
PROBE_OUTPUT_OVERFLOW: '探测输出超过大小上限,已立即失败并清理子进程。',
|
|
43
|
+
PROBE_TIMEOUT: 'MCP initialize 握手超时。',
|
|
44
|
+
PROBE_CRASH: '启动探测自身异常终止;已使用固定诊断,未回显异常内容。',
|
|
45
|
+
LAUNCHER_ERROR_UNKNOWN: '本地启动器返回了无法识别的错误代码;未回显其内容。',
|
|
46
|
+
BINDING_MISSING: '尚未写入本地运行时绑定;请先完成知识接入。',
|
|
47
|
+
BINDING_CORRUPT: '本地运行时绑定不可读;请修复知识接入。',
|
|
48
|
+
BINDING_PACKAGE_MISMATCH: '运行时绑定不是本产品包。',
|
|
49
|
+
BINDING_VERSION_MISMATCH: '运行时绑定版本不一致;请更新插件。',
|
|
50
|
+
};
|
|
51
|
+
// Launcher error codes the probe may adopt from stderr. Anything else is an
|
|
52
|
+
// untrusted value: it never becomes a result code and is never echoed.
|
|
53
|
+
const MCP_LAUNCHER_ERROR_CODE_ALLOWLIST = [
|
|
54
|
+
'BINDING_MISSING',
|
|
55
|
+
'BINDING_CORRUPT',
|
|
56
|
+
'BINDING_SCHEMA_MISMATCH',
|
|
57
|
+
'BINDING_PACKAGE_MISMATCH',
|
|
58
|
+
'BINDING_VERSION_MISMATCH',
|
|
59
|
+
'BINDING_INVALID_ROOT',
|
|
60
|
+
'BINDING_ENTRY_MISSING',
|
|
61
|
+
'BINDING_ENTRY_INVALID',
|
|
62
|
+
'LAUNCH_FAILED',
|
|
63
|
+
];
|
|
64
|
+
const SUPPORTED_CLIENTS = ['claude', 'opencode', 'codex', 'zcode'];
|
|
65
|
+
const DEFAULT_BRIDGE_CONSUMER = 'project-knowledge';
|
|
66
|
+
const CLIENT_LABELS = {
|
|
67
|
+
claude: 'Claude Code',
|
|
68
|
+
opencode: 'OpenCode',
|
|
69
|
+
codex: 'Codex',
|
|
70
|
+
zcode: 'ZCode',
|
|
71
|
+
};
|
|
72
|
+
const CLIENT_METHOD_SUFFIX = {
|
|
73
|
+
claude: 'Claude',
|
|
74
|
+
opencode: 'OpenCode',
|
|
75
|
+
codex: 'Codex',
|
|
76
|
+
zcode: 'ZCode',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function normalizeClient(value) {
|
|
80
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
81
|
+
if (normalized === 'claude-code' || normalized === 'claudecode') return 'claude';
|
|
82
|
+
if (normalized === 'codex-cli') return 'codex';
|
|
83
|
+
if (SUPPORTED_CLIENTS.includes(normalized)) return normalized;
|
|
84
|
+
throw new Error(`unsupported client: ${value}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function defaultCommandResolver(name, env = process.env) {
|
|
88
|
+
const lookup = process.platform === 'win32' ? 'where.exe' : 'which';
|
|
89
|
+
const result = spawnSync(lookup, [name], {
|
|
90
|
+
encoding: 'utf8',
|
|
91
|
+
windowsHide: true,
|
|
92
|
+
env,
|
|
93
|
+
timeout: 5000,
|
|
94
|
+
});
|
|
95
|
+
if (result.status !== 0) return '';
|
|
96
|
+
const candidates = String(result.stdout || '')
|
|
97
|
+
.split(/\r?\n/)
|
|
98
|
+
.map(item => item.trim())
|
|
99
|
+
.filter(Boolean);
|
|
100
|
+
if (process.platform === 'win32') {
|
|
101
|
+
return candidates.find(item => /\.(cmd|exe)$/i.test(item)) || candidates[0] || '';
|
|
102
|
+
}
|
|
103
|
+
return candidates[0] || '';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function defaultRunner(command, args, options = {}) {
|
|
107
|
+
return crossSpawn.sync(command, args, {
|
|
108
|
+
encoding: 'utf8',
|
|
109
|
+
windowsHide: true,
|
|
110
|
+
env: options.env || process.env,
|
|
111
|
+
cwd: options.cwd,
|
|
112
|
+
timeout: options.timeout || (process.env.KB_INTEGRATION_STATUS_WORKER === '1' ? 5000 : 120_000),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseJson(text, fallback) {
|
|
117
|
+
try {
|
|
118
|
+
return JSON.parse(String(text || ''));
|
|
119
|
+
} catch {
|
|
120
|
+
return fallback;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function writeFileAtomic(filePath, content) {
|
|
125
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
126
|
+
const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
127
|
+
fs.writeFileSync(temporary, content, 'utf8');
|
|
128
|
+
fs.renameSync(temporary, filePath);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function updateJsoncValue(filePath, propertyPath, value) {
|
|
132
|
+
let source = fs.existsSync(filePath)
|
|
133
|
+
? fs.readFileSync(filePath, 'utf8')
|
|
134
|
+
: '{\n "$schema": "https://opencode.ai/config.json"\n}\n';
|
|
135
|
+
const formattingOptions = { insertSpaces: true, tabSize: 2, eol: source.includes('\r\n') ? '\r\n' : '\n' };
|
|
136
|
+
const edits = modify(source, propertyPath, value, { formattingOptions });
|
|
137
|
+
source = applyEdits(source, edits);
|
|
138
|
+
writeFileAtomic(filePath, source);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function openCodeConfigDir(homeDir = os.homedir(), env = process.env) {
|
|
142
|
+
if (env.OPENCODE_CONFIG_DIR) return path.resolve(env.OPENCODE_CONFIG_DIR);
|
|
143
|
+
if (env.XDG_CONFIG_HOME) return path.join(path.resolve(env.XDG_CONFIG_HOME), 'opencode');
|
|
144
|
+
return path.join(homeDir, '.config', 'opencode');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function findOpenCodeConfig(configDir) {
|
|
148
|
+
const jsonc = path.join(configDir, 'opencode.jsonc');
|
|
149
|
+
const json = path.join(configDir, 'opencode.json');
|
|
150
|
+
if (fs.existsSync(jsonc)) return jsonc;
|
|
151
|
+
if (fs.existsSync(json)) return json;
|
|
152
|
+
return jsonc;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function readOpenCodeState(configDir) {
|
|
156
|
+
const configPath = findOpenCodeConfig(configDir);
|
|
157
|
+
let config = {};
|
|
158
|
+
if (fs.existsSync(configPath)) {
|
|
159
|
+
config = parse(fs.readFileSync(configPath, 'utf8')) || {};
|
|
160
|
+
}
|
|
161
|
+
const legacy = config?.mcp?.[INTEGRATION_NAME];
|
|
162
|
+
const v2 = config?.mcp?.servers?.[INTEGRATION_NAME];
|
|
163
|
+
const skillPath = path.join(configDir, 'skills', INTEGRATION_NAME, 'SKILL.md');
|
|
164
|
+
const instructionPath = path.join(configDir, 'instructions', `${INTEGRATION_NAME}.md`);
|
|
165
|
+
const configuredInstructions = Array.isArray(config?.instructions) ? config.instructions : [];
|
|
166
|
+
return {
|
|
167
|
+
configPath,
|
|
168
|
+
mcpInstalled: Boolean(legacy || v2),
|
|
169
|
+
skillInstalled: fs.existsSync(skillPath),
|
|
170
|
+
instructionInstalled: fs.existsSync(instructionPath)
|
|
171
|
+
&& configuredInstructions.some(item => path.resolve(String(item)) === path.resolve(instructionPath)),
|
|
172
|
+
skillPath,
|
|
173
|
+
instructionPath,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function ensureOpenCodeInstructionConfig(configDir, instructionPath) {
|
|
178
|
+
const configPath = findOpenCodeConfig(configDir);
|
|
179
|
+
const config = fs.existsSync(configPath) ? (parse(fs.readFileSync(configPath, 'utf8')) || {}) : {};
|
|
180
|
+
const instructions = Array.isArray(config.instructions) ? [...config.instructions] : [];
|
|
181
|
+
if (!instructions.some(item => path.resolve(String(item)) === path.resolve(instructionPath))) {
|
|
182
|
+
instructions.push(instructionPath);
|
|
183
|
+
updateJsoncValue(configPath, ['instructions'], instructions);
|
|
184
|
+
}
|
|
185
|
+
return configPath;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function removeOpenCodeInstructionConfig(configDir, instructionPath) {
|
|
189
|
+
const configPath = findOpenCodeConfig(configDir);
|
|
190
|
+
if (!fs.existsSync(configPath)) return false;
|
|
191
|
+
const config = parse(fs.readFileSync(configPath, 'utf8')) || {};
|
|
192
|
+
if (!Array.isArray(config.instructions)) return false;
|
|
193
|
+
const instructions = config.instructions.filter(item => path.resolve(String(item)) !== path.resolve(instructionPath));
|
|
194
|
+
if (instructions.length === config.instructions.length) return false;
|
|
195
|
+
updateJsoncValue(configPath, ['instructions'], instructions.length ? instructions : undefined);
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function removeOpenCodeMcpConfig(configDir) {
|
|
200
|
+
const configPath = findOpenCodeConfig(configDir);
|
|
201
|
+
if (!fs.existsSync(configPath)) return false;
|
|
202
|
+
let source = fs.readFileSync(configPath, 'utf8');
|
|
203
|
+
const config = parse(source) || {};
|
|
204
|
+
const paths = [];
|
|
205
|
+
if (config?.mcp?.[INTEGRATION_NAME]) paths.push(['mcp', INTEGRATION_NAME]);
|
|
206
|
+
if (config?.mcp?.servers?.[INTEGRATION_NAME]) paths.push(['mcp', 'servers', INTEGRATION_NAME]);
|
|
207
|
+
if (!paths.length) return false;
|
|
208
|
+
const formattingOptions = { insertSpaces: true, tabSize: 2, eol: source.includes('\r\n') ? '\r\n' : '\n' };
|
|
209
|
+
for (const propertyPath of paths) {
|
|
210
|
+
const edits = modify(source, propertyPath, undefined, { formattingOptions });
|
|
211
|
+
source = applyEdits(source, edits);
|
|
212
|
+
}
|
|
213
|
+
writeFileAtomic(configPath, source);
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
class IntegrationManager {
|
|
218
|
+
constructor(options = {}) {
|
|
219
|
+
this.rootDir = options.rootDir || path.resolve(__dirname, '..', '..');
|
|
220
|
+
this.homeDir = options.homeDir || os.homedir();
|
|
221
|
+
this.env = options.env || process.env;
|
|
222
|
+
this.version = options.version || packageInfo.version;
|
|
223
|
+
this.packageSpec = options.packageSpec || `${packageInfo.name}@${this.version}`;
|
|
224
|
+
this.marketplaceSource = options.marketplaceSource || DEFAULT_MARKETPLACE_SOURCE;
|
|
225
|
+
this.claudeMarketplaceSource = options.marketplaceSource || this.rootDir;
|
|
226
|
+
this.explicitMarketplace = Boolean(options.marketplaceSource);
|
|
227
|
+
this.codexMarketplaceAddSource = options.codexMarketplaceSource || options.marketplaceSource || this.rootDir;
|
|
228
|
+
this.explicitCodexMarketplace = Boolean(options.codexMarketplaceSource || options.marketplaceSource);
|
|
229
|
+
this.runner = options.runner || defaultRunner;
|
|
230
|
+
this.commandResolver = options.commandResolver || defaultCommandResolver;
|
|
231
|
+
this.codexHookProbe = options.codexHookProbe || require('./codex-context-status').readCodexHooks;
|
|
232
|
+
this.mcpProbe = options.mcpProbe || null;
|
|
233
|
+
this.dryRun = Boolean(options.dryRun);
|
|
234
|
+
this.logger = options.logger || null;
|
|
235
|
+
this.operationId = options.operationId || '';
|
|
236
|
+
this.planned = [];
|
|
237
|
+
this.bridge = options.bridgeModule || bridgeModule;
|
|
238
|
+
this.bridgeConsumerName = options.bridgeConsumerName || DEFAULT_BRIDGE_CONSUMER;
|
|
239
|
+
this.bridgeHomeDir = options.bridgeHomeDir
|
|
240
|
+
|| this.env.AI_CODING_EVENT_BRIDGE_HOME
|
|
241
|
+
|| path.join(resolveDataDirPath({ dataDir: this.env.KB_DATA_DIR || '', homeDir: this.homeDir }), 'events');
|
|
242
|
+
this.claudeSettingsFile = options.claudeSettingsFile || path.join(this.homeDir, '.claude', 'settings.json');
|
|
243
|
+
this.codexConfigFile = options.codexConfigFile || path.join(this.homeDir, '.codex', 'config.toml');
|
|
244
|
+
this.openCodePluginsDir = options.openCodePluginsDir || path.join(openCodeConfigDir(this.homeDir, this.env), 'plugin');
|
|
245
|
+
this.zcodeConfigFile = options.zcodeConfigFile || path.join(this.homeDir, '.zcode', 'cli', 'config.json');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
_bridgeFacade() {
|
|
249
|
+
return this.bridge.createBridge({ homeDir: this.bridgeHomeDir });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
_captureInstallerOptions(client) {
|
|
253
|
+
return {
|
|
254
|
+
homeDir: this.bridgeHomeDir,
|
|
255
|
+
settingsFile: this.claudeSettingsFile || undefined,
|
|
256
|
+
configFile: client === 'zcode' ? this.zcodeConfigFile : this.codexConfigFile,
|
|
257
|
+
pluginsDir: this.openCodePluginsDir || undefined,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---- Bridge host consumer (host-level, never owned by one connector) ----
|
|
262
|
+
|
|
263
|
+
async registerBridgeConsumer(meta = {}) {
|
|
264
|
+
if (this.dryRun) {
|
|
265
|
+
this.planned.push({ command: 'bridge', args: ['registerConsumer', this.bridgeConsumerName] });
|
|
266
|
+
return { state: 'planned', consumerName: this.bridgeConsumerName };
|
|
267
|
+
}
|
|
268
|
+
const facade = this._bridgeFacade();
|
|
269
|
+
const consumer = await facade.registerConsumer(this.bridgeConsumerName, meta);
|
|
270
|
+
return { state: 'registered', consumerName: this.bridgeConsumerName, consumer };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async unregisterBridgeConsumer() {
|
|
274
|
+
if (this.dryRun) {
|
|
275
|
+
this.planned.push({ command: 'bridge', args: ['unregisterConsumer', this.bridgeConsumerName] });
|
|
276
|
+
return { state: 'planned', consumerName: this.bridgeConsumerName };
|
|
277
|
+
}
|
|
278
|
+
const facade = this._bridgeFacade();
|
|
279
|
+
await facade.unregisterConsumer(this.bridgeConsumerName);
|
|
280
|
+
return { state: 'unregistered', consumerName: this.bridgeConsumerName };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async bridgeStatus() {
|
|
284
|
+
const facade = this._bridgeFacade();
|
|
285
|
+
const [consumer, health] = await Promise.all([facade.getConsumer(this.bridgeConsumerName), facade.getHealth()]);
|
|
286
|
+
return {
|
|
287
|
+
consumerRegistered: Boolean(consumer),
|
|
288
|
+
consumerName: this.bridgeConsumerName,
|
|
289
|
+
bridgeHealthy: true,
|
|
290
|
+
health,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---- Development Capture connectors (Bridge hooks / notify / plugin) ----
|
|
295
|
+
|
|
296
|
+
async captureOperation(operation, client, options = {}) {
|
|
297
|
+
const startedAt = Date.now();
|
|
298
|
+
const context = { client, operation };
|
|
299
|
+
try {
|
|
300
|
+
const result = await this[`${operation}Capture${CLIENT_METHOD_SUFFIX[client]}`](options);
|
|
301
|
+
const ok = !result.conflict && result.state !== 'failed';
|
|
302
|
+
const repairContext = { ...context, ...(typeof result.changed === 'boolean' ? { changed: result.changed } : {}), ...(Number.isInteger(result.removedLegacy) ? { removedLegacy: result.removedLegacy, removedDuplicates: result.removedDuplicates } : {}) };
|
|
303
|
+
if (operation !== 'status' || !ok) this.log(ok ? 'info' : 'error', `integration.capture.${ok ? 'completed' : 'failed'}`, ok ? 'Agent 采集接线操作完成。' : 'Agent 采集接线未完成。', { operationId: this.operationId, durationMs: Date.now() - startedAt, error: ok ? undefined : Object.assign(new Error(result.detail), { code: 'CAPTURE_CONFLICT' }), context: repairContext });
|
|
304
|
+
return { ok, ...result, ...(!ok ? { error: { code: 'CAPTURE_CONFLICT', message: result.detail } } : {}) };
|
|
305
|
+
} catch (error) {
|
|
306
|
+
this.log('error', 'integration.capture.failed', 'Agent 采集接线操作失败。', { operationId: this.operationId, durationMs: Date.now() - startedAt, error, context });
|
|
307
|
+
return { ok: false, state: 'failed', installed: false, detail: error.message, error: { code: error.code || 'CAPTURE_FAILED', message: error.message } };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async installCaptureZCode() {
|
|
312
|
+
if (this.dryRun) {
|
|
313
|
+
this.planned.push({ command: 'bridge', args: ['installZcodeHook', this.bridgeHomeDir] });
|
|
314
|
+
return { state: 'planned', installed: false, detail: '计划接入 ZCode 会话采集,未修改配置。' };
|
|
315
|
+
}
|
|
316
|
+
planCaptureHooks(this.zcodeConfigFile, 'zcode', this);
|
|
317
|
+
await this.bridge.installers.zcode.installZcodeHook(this._captureInstallerOptions('zcode'));
|
|
318
|
+
const repaired = repairCaptureHooks(this.zcodeConfigFile, 'zcode', this);
|
|
319
|
+
const status = await this.statusCaptureZCode();
|
|
320
|
+
if (!status.installed) throw new Error('ZCode Hook 写入后校验失败,请检查 hooks.enabled 与事件配置。');
|
|
321
|
+
return { ...status, ...repaired };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async statusCaptureZCode() {
|
|
325
|
+
const status = await this.bridge.installers.zcode.statusZcodeHook(this._captureInstallerOptions('zcode'));
|
|
326
|
+
const plan = planCaptureHooks(this.zcodeConfigFile, 'zcode', this);
|
|
327
|
+
if (plan.repairNeeded || status.installed && !fs.existsSync(path.join(this.bridgeHomeDir, 'bin', 'bridge-hook.cjs'))) return { state: 'needs-repair', installed: false, conflict: false, repairNeeded: true, detail: '检测到重复、旧接线或缺失的 Bridge 入口,请执行接入/修复。' };
|
|
328
|
+
return { state: status.installed ? 'installed' : 'not-installed', installed: status.installed, conflict: false, detail: status.installed ? '已配置用户输入与回答采集;请重新打开 ZCode 会话。' : 'ZCode 会话采集尚未接入。' };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async uninstallCaptureZCode() {
|
|
332
|
+
if (this.dryRun) return { state: 'planned', detail: '计划移除 ZCode 采集,未修改配置。' };
|
|
333
|
+
removeCaptureHooks(this.zcodeConfigFile, 'zcode', this);
|
|
334
|
+
await this.bridge.installers.zcode.uninstallZcodeHook(this._captureInstallerOptions('zcode'));
|
|
335
|
+
return { state: 'removed', installed: false, detail: '已移除 ZCode 托管采集,保留第三方配置与已有记录。' };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async installCaptureClaude() {
|
|
339
|
+
if (this.dryRun) {
|
|
340
|
+
this.planned.push({ command: 'bridge', args: ['installClaudeCodeHook', this.bridgeHomeDir] });
|
|
341
|
+
return { state: 'planned', installed: false, conflict: false, detail: 'managed Bridge hooks (dry-run)' };
|
|
342
|
+
}
|
|
343
|
+
planCaptureHooks(this.claudeSettingsFile, 'claude', this);
|
|
344
|
+
const result = await this.bridge.installers.claudeCode.installClaudeCodeHook(this._captureInstallerOptions());
|
|
345
|
+
if (result.runtime?.conflict) throw new Error('Bridge 运行版本冲突,保留旧采集接线。');
|
|
346
|
+
const repaired = repairCaptureHooks(this.claudeSettingsFile, 'claude', this);
|
|
347
|
+
return { state: 'installed', installed: true, conflict: false, detail: `managed hooks: ${result.added.join(', ') || 'already present'}`, ...repaired };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async statusCaptureClaude() {
|
|
351
|
+
const status = await this.bridge.installers.claudeCode.statusClaudeCodeHook(this._captureInstallerOptions());
|
|
352
|
+
const plan = planCaptureHooks(this.claudeSettingsFile, 'claude', this);
|
|
353
|
+
if (plan.repairNeeded || status.installed && !fs.existsSync(path.join(this.bridgeHomeDir, 'bin', 'bridge-hook.cjs'))) return { state: 'needs-repair', installed: false, conflict: false, repairNeeded: true, detail: '检测到重复、旧接线或缺失的 Bridge 入口,请执行接入/修复。' };
|
|
354
|
+
return {
|
|
355
|
+
state: status.installed ? 'installed' : 'not-installed',
|
|
356
|
+
installed: status.installed,
|
|
357
|
+
conflict: false,
|
|
358
|
+
detail: status.installed ? '已配置用户输入与回答采集。' : '会话采集 Hook 尚未配置。',
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async uninstallCaptureClaude(options = {}) {
|
|
363
|
+
if (this.dryRun) return { state: 'planned', detail: '计划移除 Claude Code 采集,未修改配置。' };
|
|
364
|
+
removeCaptureHooks(this.claudeSettingsFile, 'claude', this);
|
|
365
|
+
await this.bridge.installers.claudeCode.uninstallClaudeCodeHook({
|
|
366
|
+
...this._captureInstallerOptions(),
|
|
367
|
+
unregisterConsumer: options.unregisterConsumer === true,
|
|
368
|
+
consumerName: this.bridgeConsumerName,
|
|
369
|
+
});
|
|
370
|
+
return { state: 'removed', detail: options.unregisterConsumer === true ? 'managed hooks removed; host consumer unregistered' : 'managed hooks removed; host consumer kept' };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async installCaptureCodex() {
|
|
374
|
+
if (this.dryRun) {
|
|
375
|
+
this.planned.push({ command: 'bridge', args: ['installCodexNotify', this.bridgeHomeDir] });
|
|
376
|
+
return { state: 'planned', installed: false, conflict: false, detail: 'managed Codex notify (dry-run)' };
|
|
377
|
+
}
|
|
378
|
+
const plan = planNotify(this.codexConfigFile, this);
|
|
379
|
+
if (plan.conflict) return { state: 'conflict', installed: false, conflict: true, detail: plan.detail };
|
|
380
|
+
// The host owns composition and root-level TOML placement; Bridge only
|
|
381
|
+
// materializes its independent runtime, never rewrites the notify setting.
|
|
382
|
+
await this.bridge.ensureRuntimeHome({ homeDir: this.bridgeHomeDir });
|
|
383
|
+
const written = writeNotify(this.codexConfigFile, plan);
|
|
384
|
+
return { state: 'installed', installed: true, conflict: false, detail: written.changed ? '已接入 Codex 会话采集,保留有效的原通知;请重新打开 Codex 会话。' : 'Codex 会话采集已接入,通知链无需重复修改。', ...written };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async statusCaptureCodex() {
|
|
388
|
+
const plan = planNotify(this.codexConfigFile, this);
|
|
389
|
+
const runtimeReady = fs.existsSync(path.join(this.rootDir, '_site', 'scripts', 'codex-notify-fanout.cjs'))
|
|
390
|
+
&& fs.existsSync(path.join(this.bridgeHomeDir, 'bin', 'bridge-hook.cjs'));
|
|
391
|
+
const installed = !plan.conflict && !plan.changed && runtimeReady;
|
|
392
|
+
return {
|
|
393
|
+
state: installed ? 'installed' : plan.conflict ? 'conflict' : 'not-installed',
|
|
394
|
+
installed,
|
|
395
|
+
conflict: Boolean(plan.conflict),
|
|
396
|
+
detail: installed ? '已配置 Codex 会话采集,原通知保持有效。' : plan.conflict ? plan.detail : !runtimeReady && !plan.changed ? 'Codex 采集运行文件缺失,请执行接入/修复。' : 'Codex 会话采集尚未接入或旧接线需要修复。',
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async uninstallCaptureCodex(options = {}) {
|
|
401
|
+
if (this.dryRun) return { state: 'planned', installed: false, detail: '计划移除 Codex 托管采集,保留原通知。' };
|
|
402
|
+
const plan = planNotify(this.codexConfigFile, this, false);
|
|
403
|
+
if (plan.conflict) return { state: 'conflict', installed: false, conflict: true, detail: plan.detail };
|
|
404
|
+
const written = writeNotify(this.codexConfigFile, plan);
|
|
405
|
+
if (options.unregisterConsumer === true) await this.unregisterBridgeConsumer();
|
|
406
|
+
return { state: 'removed', installed: false, conflict: false, detail: '已移除 Codex 托管采集,保留有效的原通知和已有记录。', ...written };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async installCaptureOpenCode() {
|
|
410
|
+
if (this.dryRun) {
|
|
411
|
+
this.planned.push({ command: 'bridge', args: ['installOpenCodePlugin', this.bridgeHomeDir] });
|
|
412
|
+
return { state: 'planned', installed: false, conflict: false, detail: 'managed OpenCode plugin (dry-run)' };
|
|
413
|
+
}
|
|
414
|
+
await this.bridge.installers.openCode.installOpenCodePlugin(this._captureInstallerOptions());
|
|
415
|
+
return { state: 'installed', installed: true, conflict: false, detail: 'managed plugin installed' };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async statusCaptureOpenCode() {
|
|
419
|
+
const status = await this.bridge.installers.openCode.statusOpenCodePlugin(this._captureInstallerOptions());
|
|
420
|
+
return {
|
|
421
|
+
state: status.installed ? 'installed' : 'not-installed',
|
|
422
|
+
installed: Boolean(status.installed),
|
|
423
|
+
conflict: false,
|
|
424
|
+
detail: status.installed ? '已配置会话采集插件。' : '会话采集插件尚未配置,保留其他插件。',
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async uninstallCaptureOpenCode(options = {}) {
|
|
429
|
+
await this.bridge.installers.openCode.uninstallOpenCodePlugin({
|
|
430
|
+
...this._captureInstallerOptions(),
|
|
431
|
+
unregisterConsumer: options.unregisterConsumer === true,
|
|
432
|
+
consumerName: this.bridgeConsumerName,
|
|
433
|
+
});
|
|
434
|
+
return { state: 'removed', detail: 'managed plugin removed' };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
log(level, event, message, input = {}) {
|
|
438
|
+
if (!this.logger || typeof this.logger[level] !== 'function') return;
|
|
439
|
+
Promise.resolve(this.logger[level](event, message, { component: 'integration-manager', operationId: this.operationId, ...input })).catch(() => {
|
|
440
|
+
try { process.stderr.write(`[integration-manager logger fallback] ${event}\n`); } catch {
|
|
441
|
+
// stderr is the final non-recursive observer fallback.
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
executable(client) {
|
|
447
|
+
const name = client === 'claude' ? 'claude' : client;
|
|
448
|
+
if (this.dryRun) return process.platform === 'win32' ? `${name}.cmd` : name;
|
|
449
|
+
const command = this.commandResolver(name, this.env);
|
|
450
|
+
if (command || client !== 'zcode' || this.commandResolver !== defaultCommandResolver) return command;
|
|
451
|
+
try { return require('claude-ai-workbench/packages/server/lib/drivers/zcode-driver').discoverZCodeCli({ env: this.env })?.path || ''; } catch { return ''; }
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
detectClients() {
|
|
455
|
+
return SUPPORTED_CLIENTS.filter(client => Boolean(this.executable(client)) || client === 'zcode' && this.captureConfigFound(client));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
captureConfigFound(client) {
|
|
459
|
+
const target = { claude: this.claudeSettingsFile, codex: this.codexConfigFile, opencode: path.dirname(this.openCodePluginsDir), zcode: this.zcodeConfigFile }[client];
|
|
460
|
+
try { const stat = fs.statSync(target); return client === 'opencode' ? stat.isDirectory() : stat.isFile(); } catch { return false; }
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ---- Local MCP runtime binding and startup diagnostics ----
|
|
464
|
+
|
|
465
|
+
integrationRuntimeBindingPath() {
|
|
466
|
+
return path.join(resolveDataDirPath({ dataDir: this.env.KB_DATA_DIR || '', homeDir: this.homeDir }), 'integration-runtime.json');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
ensureIntegrationRuntimeBinding(options = {}) {
|
|
470
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(this.rootDir, 'package.json'), 'utf8'));
|
|
471
|
+
if (manifest.name !== INTEGRATION_PACKAGE_NAME) throw new Error('知识接入绑定的运行包不是本产品包,已拒绝写入绑定。');
|
|
472
|
+
const bin = manifest.bin && typeof manifest.bin === 'object' && !Array.isArray(manifest.bin) ? manifest.bin['project-knowledge-mcp'] : manifest.bin;
|
|
473
|
+
const packageRoot = path.resolve(this.rootDir);
|
|
474
|
+
const entry = typeof bin === 'string' && bin ? path.resolve(packageRoot, bin) : '';
|
|
475
|
+
const entryError = 'package.json must declare an existing project-knowledge-mcp bin entry inside the package before binding the local MCP runtime.';
|
|
476
|
+
const lexical = entry ? path.relative(packageRoot, entry) : '..';
|
|
477
|
+
if (!entry || !lexical || lexical === '..' || lexical.startsWith('..' + path.sep) || path.isAbsolute(lexical)) throw new Error(entryError);
|
|
478
|
+
// The same realpath containment the launcher enforces at start: a link
|
|
479
|
+
// inside the package that resolves outside the real root must never be
|
|
480
|
+
// bound, and the entry must be a regular file.
|
|
481
|
+
let rootReal = '';
|
|
482
|
+
let entryReal = '';
|
|
483
|
+
try {
|
|
484
|
+
rootReal = fs.realpathSync(packageRoot);
|
|
485
|
+
entryReal = fs.realpathSync(entry);
|
|
486
|
+
} catch (error) { throw new Error(entryError); }
|
|
487
|
+
const relativeReal = path.relative(rootReal, entryReal);
|
|
488
|
+
if (!relativeReal || relativeReal === '..' || relativeReal.startsWith('..' + path.sep) || path.isAbsolute(relativeReal)) throw new Error(entryError);
|
|
489
|
+
let entryStat = null;
|
|
490
|
+
try { entryStat = fs.statSync(entryReal); } catch (error) { throw new Error(entryError); }
|
|
491
|
+
if (!entryStat.isFile()) throw new Error(entryError);
|
|
492
|
+
const bindingPath = this.integrationRuntimeBindingPath();
|
|
493
|
+
const binding = { schema: INTEGRATION_RUNTIME_SCHEMA, packageName: manifest.name, packageRoot, version: manifest.version, updatedAt: new Date().toISOString() };
|
|
494
|
+
try {
|
|
495
|
+
const existing = JSON.parse(fs.readFileSync(bindingPath, 'utf8'));
|
|
496
|
+
// An injected beforeRename means the caller explicitly wants the write
|
|
497
|
+
// path exercised (failure surfacing); only the quiet repair path may
|
|
498
|
+
// short-circuit to keep the binding byte- and mtime-idempotent.
|
|
499
|
+
const verifyWrite = typeof options.beforeRename === 'function';
|
|
500
|
+
if (!verifyWrite && existing && typeof existing === 'object' && existing.schema === INTEGRATION_RUNTIME_SCHEMA && existing.packageName === binding.packageName && existing.packageRoot === binding.packageRoot && existing.version === binding.version) {
|
|
501
|
+
return { state: 'unchanged', path: bindingPath, version: manifest.version };
|
|
502
|
+
}
|
|
503
|
+
} catch (error) { /* missing or unreadable bindings are rewritten below */ }
|
|
504
|
+
if (this.dryRun) {
|
|
505
|
+
this.planned.push({ command: 'runtime-binding', args: ['write', bindingPath] });
|
|
506
|
+
return { state: 'planned', path: bindingPath };
|
|
507
|
+
}
|
|
508
|
+
writeJsonAtomic(bindingPath, binding, typeof options.beforeRename === 'function' ? { beforeRename: options.beforeRename } : {});
|
|
509
|
+
return { state: 'written', path: bindingPath, version: manifest.version };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async probeMcpStartup(probeOptions = {}) {
|
|
513
|
+
const launcherPath = probeOptions.launcherPath || path.join(this.rootDir, MCP_LAUNCHER_FILE);
|
|
514
|
+
const scope = probeOptions.scope || 'product-runtime';
|
|
515
|
+
const expectedVersion = typeof probeOptions.version === 'string' && probeOptions.version ? probeOptions.version : this.version;
|
|
516
|
+
const timeoutMs = Number.isFinite(probeOptions.timeoutMs) ? probeOptions.timeoutMs : MCP_PROBE_TIMEOUT_MS;
|
|
517
|
+
const stderrCapBytes = Number.isFinite(probeOptions.stderrCapBytes) ? probeOptions.stderrCapBytes : MCP_PROBE_STDERR_CAP_BYTES;
|
|
518
|
+
const stdoutCapBytes = Number.isFinite(probeOptions.stdoutCapBytes) ? probeOptions.stdoutCapBytes : MCP_PROBE_STDOUT_CAP_BYTES;
|
|
519
|
+
const checkedAt = new Date().toISOString();
|
|
520
|
+
let Client = null;
|
|
521
|
+
let StdioClientTransport = null;
|
|
522
|
+
try {
|
|
523
|
+
const requireFromPackage = createRequire(path.join(this.rootDir, 'package.json'));
|
|
524
|
+
Client = requireFromPackage('@modelcontextprotocol/sdk/client/index.js').Client;
|
|
525
|
+
StdioClientTransport = requireFromPackage('@modelcontextprotocol/sdk/client/stdio.js').StdioClientTransport;
|
|
526
|
+
} catch (error) {
|
|
527
|
+
this.log('error', 'integration.mcp_probe_sdk_unavailable', 'The MCP SDK is unavailable; the probe fails explicitly instead of guessing.', { error, context: { scope } });
|
|
528
|
+
return { state: 'failed', checkedAt, scope, expectedVersion, code: 'PROBE_SDK_UNAVAILABLE', detail: '安装包内缺少 MCP SDK,无法执行启动探测;未据此判定客户端未接入。' };
|
|
529
|
+
}
|
|
530
|
+
return await new Promise(resolve => {
|
|
531
|
+
let client = null;
|
|
532
|
+
let timer = null;
|
|
533
|
+
let settled = false;
|
|
534
|
+
let stderrTail = '';
|
|
535
|
+
let stderrBytes = 0;
|
|
536
|
+
let stdoutBytes = 0;
|
|
537
|
+
const failed = (code, extra = {}) => {
|
|
538
|
+
if (settled) return;
|
|
539
|
+
settled = true;
|
|
540
|
+
if (timer) clearTimeout(timer);
|
|
541
|
+
resolve({ state: 'failed', checkedAt, scope, expectedVersion, code, ...extra, detail: MCP_PROBE_FAILURE_DETAILS[code] || '本地 MCP 启动探测未通过;诊断不回显上游文本。' });
|
|
542
|
+
};
|
|
543
|
+
const cleanup = async () => {
|
|
544
|
+
const closing = client;
|
|
545
|
+
client = null;
|
|
546
|
+
if (!closing) return;
|
|
547
|
+
try { await closing.close(); } catch (error) { /* the SDK escalates SIGTERM/SIGKILL on its own child only */ }
|
|
548
|
+
};
|
|
549
|
+
// The probe must exercise the manager's own isolated data dir; a host
|
|
550
|
+
// KB_DATA_DIR (for example a Workbench session export) must not leak in.
|
|
551
|
+
const probeEnv = { ...process.env, ...this.env, KB_DATA_DIR: path.dirname(this.integrationRuntimeBindingPath()) };
|
|
552
|
+
const transport = new StdioClientTransport({ command: process.execPath, args: [launcherPath], env: probeEnv, stderr: 'pipe' });
|
|
553
|
+
transport.stderr.on('data', chunk => {
|
|
554
|
+
stderrBytes += chunk.length;
|
|
555
|
+
stderrTail = (stderrTail + String(chunk)).slice(-4096);
|
|
556
|
+
if (stderrBytes > stderrCapBytes) {
|
|
557
|
+
failed('PROBE_OUTPUT_OVERFLOW');
|
|
558
|
+
cleanup();
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
// The SDK exposes no public stdout tap, so count bytes on the child
|
|
562
|
+
// stream right after start(): a hostile single-line flood fails at the
|
|
563
|
+
// cap instead of growing the SDK read buffer for the whole timeout.
|
|
564
|
+
const startTransport = transport.start.bind(transport);
|
|
565
|
+
transport.start = async () => {
|
|
566
|
+
await startTransport();
|
|
567
|
+
const stream = transport._process && transport._process.stdout;
|
|
568
|
+
if (stream) stream.on('data', chunk => {
|
|
569
|
+
stdoutBytes += chunk.length;
|
|
570
|
+
if (stdoutBytes > stdoutCapBytes) {
|
|
571
|
+
failed('PROBE_OUTPUT_OVERFLOW');
|
|
572
|
+
cleanup();
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
};
|
|
576
|
+
client = new Client({ name: 'project-knowledge-integration-probe', version: '1' }, { capabilities: {} });
|
|
577
|
+
timer = setTimeout(() => {
|
|
578
|
+
failed('PROBE_TIMEOUT');
|
|
579
|
+
cleanup();
|
|
580
|
+
}, timeoutMs);
|
|
581
|
+
(async () => {
|
|
582
|
+
try {
|
|
583
|
+
await client.connect(transport);
|
|
584
|
+
if (settled) return;
|
|
585
|
+
const serverVersion = client.getServerVersion();
|
|
586
|
+
if (!serverVersion || serverVersion.name !== MCP_SERVER_NAME) return failed('SERVER_IDENTITY');
|
|
587
|
+
// A foreign serverVersion string is untrusted text; only the fixed
|
|
588
|
+
// mismatch code is reported, never the value itself.
|
|
589
|
+
if (String(serverVersion.version || '') !== expectedVersion) return failed('VERSION_MISMATCH');
|
|
590
|
+
let listed = null;
|
|
591
|
+
try { listed = await client.listTools(); } catch (error) { return failed('TOOLS_CONTRACT', { toolCount: 0 }); }
|
|
592
|
+
if (settled) return;
|
|
593
|
+
const names = (listed && Array.isArray(listed.tools) ? listed.tools : []).map(tool => String((tool && tool.name) || '')).filter(Boolean);
|
|
594
|
+
if (names.length !== MCP_TOOL_NAMES.length || !MCP_TOOL_NAMES.every(name => names.includes(name))) return failed('TOOLS_CONTRACT', { toolCount: names.length });
|
|
595
|
+
settled = true;
|
|
596
|
+
clearTimeout(timer);
|
|
597
|
+
resolve({ state: 'ready', checkedAt, scope, expectedVersion, actualVersion: String(serverVersion.version || ''), serverName: serverVersion.name, tools: names, detail: '本地 MCP 服务已完成 initialize,并通过身份、版本与六工具契约校验。' });
|
|
598
|
+
} catch (error) {
|
|
599
|
+
if (!settled) {
|
|
600
|
+
const lines = stderrTail.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
601
|
+
const last = lines[lines.length - 1] || '';
|
|
602
|
+
try {
|
|
603
|
+
const payload = JSON.parse(last);
|
|
604
|
+
if (payload && payload.schema === MCP_LAUNCHER_ERROR_SCHEMA && payload.code) {
|
|
605
|
+
const launcherCode = String(payload.code);
|
|
606
|
+
return failed(MCP_LAUNCHER_ERROR_CODE_ALLOWLIST.includes(launcherCode) ? launcherCode : 'LAUNCHER_ERROR_UNKNOWN');
|
|
607
|
+
}
|
|
608
|
+
} catch { /* plain-text launcher diagnostics fall through to fixed codes */ }
|
|
609
|
+
if (error && (error.code === 'ENOENT' || /spawn/i.test(String((error && error.message) || error)))) return failed('PROBE_SPAWN_FAILED');
|
|
610
|
+
return failed('INITIALIZE_REJECTED');
|
|
611
|
+
}
|
|
612
|
+
} finally {
|
|
613
|
+
await cleanup();
|
|
614
|
+
}
|
|
615
|
+
})();
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
zcodeContextOptions() { return { configFile: this.zcodeConfigFile, rootDir: this.rootDir, dryRun: this.dryRun }; }
|
|
620
|
+
statusZCode() { return require('./zcode-context-integration').status(this.zcodeContextOptions()); }
|
|
621
|
+
installZCode() { return require('./zcode-context-integration').install(this.zcodeContextOptions()); }
|
|
622
|
+
updateZCode() { return this.installZCode(); }
|
|
623
|
+
uninstallZCode() { return require('./zcode-context-integration').uninstall(this.zcodeContextOptions()); }
|
|
624
|
+
|
|
625
|
+
execute(client, args, options = {}) {
|
|
626
|
+
const startedAt = Date.now();
|
|
627
|
+
const command = this.executable(client);
|
|
628
|
+
this.planned.push({ command: command || client, args: [...args] });
|
|
629
|
+
const argsHash = `sha256:${crypto.createHash('sha256').update(JSON.stringify(args), 'utf8').digest('hex')}`;
|
|
630
|
+
this.log('info', 'integration.command_started', 'Integration CLI command started.', { context: { client, argumentCount: args.length, argsHash, dryRun: this.dryRun } });
|
|
631
|
+
if (this.dryRun) {
|
|
632
|
+
this.log('info', 'integration.command_completed', 'Integration CLI command completed.', { durationMs: Date.now() - startedAt, context: { client, status: 0, argsHash, dryRun: true } });
|
|
633
|
+
return { status: 0, stdout: '', stderr: '' };
|
|
634
|
+
}
|
|
635
|
+
if (!command) {
|
|
636
|
+
this.log('error', 'integration.command_failed', 'Integration CLI command failed.', { durationMs: Date.now() - startedAt, error: Object.assign(new Error('Integration CLI is unavailable.'), { code: 'INTEGRATION_CLI_UNAVAILABLE' }), context: { client, argsHash } });
|
|
637
|
+
throw new Error(`${CLIENT_LABELS[client]} CLI is not installed or not on PATH`);
|
|
638
|
+
}
|
|
639
|
+
const result = this.runner(command, args, {
|
|
640
|
+
env: this.env,
|
|
641
|
+
cwd: options.cwd || this.rootDir,
|
|
642
|
+
timeout: options.timeout,
|
|
643
|
+
});
|
|
644
|
+
if (result.error) {
|
|
645
|
+
this.log('error', 'integration.command_failed', 'Integration CLI command failed.', { durationMs: Date.now() - startedAt, error: Object.assign(new Error('Integration CLI command failed.'), { code: result.error.code || 'INTEGRATION_COMMAND_FAILED' }), context: { client, argsHash } });
|
|
646
|
+
throw result.error;
|
|
647
|
+
}
|
|
648
|
+
const localRefFailure = client === 'codex' && String(result.stderr || '').trim() === 'Error: --ref is only supported for git marketplace sources';
|
|
649
|
+
if (result.status !== 0 && !options.allowFailure) {
|
|
650
|
+
const detail = String(result.stderr || result.stdout || '').trim();
|
|
651
|
+
this.log('error', 'integration.command_failed', 'Integration CLI command failed.', { durationMs: Date.now() - startedAt, error: Object.assign(new Error('Integration CLI command failed.'), { code: localRefFailure ? 'CODEX_LOCAL_SOURCE_REF' : 'INTEGRATION_COMMAND_FAILED' }), context: { client, status: result.status, argsHash, outputHash: `sha256:${crypto.createHash('sha256').update(detail, 'utf8').digest('hex')}` } });
|
|
652
|
+
throw new Error(detail || `${CLIENT_LABELS[client]} command failed with exit code ${result.status}`);
|
|
653
|
+
}
|
|
654
|
+
const completed = result.status === 0;
|
|
655
|
+
const safeError = completed ? undefined : Object.assign(new Error(localRefFailure
|
|
656
|
+
? 'Codex 把本地目录中的 @ 误当作 Git ref;请使用目录内相对路径重试。'
|
|
657
|
+
: 'Integration CLI command returned a failure.'), {
|
|
658
|
+
code: localRefFailure ? 'CODEX_LOCAL_SOURCE_REF' : 'INTEGRATION_COMMAND_FAILED',
|
|
659
|
+
});
|
|
660
|
+
this.log(completed ? 'info' : 'warn', completed ? 'integration.command_completed' : 'integration.command_failed', completed ? 'Integration CLI command completed.' : 'Integration CLI command returned a failure.', {
|
|
661
|
+
durationMs: Date.now() - startedAt, error: safeError,
|
|
662
|
+
context: { client, status: result.status, argsHash },
|
|
663
|
+
});
|
|
664
|
+
return result;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
claudePlugin() {
|
|
668
|
+
const result = this.execute('claude', ['plugin', 'list', '--json'], { allowFailure: true });
|
|
669
|
+
const plugins = parseJson(result.stdout, []);
|
|
670
|
+
return Array.isArray(plugins)
|
|
671
|
+
? plugins.find(item => item.id === `${INTEGRATION_NAME}@${MARKETPLACE_NAME}`)
|
|
672
|
+
: undefined;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
claudeMarketplaceExists() {
|
|
676
|
+
const result = this.execute('claude', ['plugin', 'marketplace', 'list', '--json'], { allowFailure: true });
|
|
677
|
+
const marketplaces = parseJson(result.stdout, []);
|
|
678
|
+
return Array.isArray(marketplaces) && marketplaces.some(item => item.name === MARKETPLACE_NAME);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
codexPlugin() {
|
|
682
|
+
const result = this.execute('codex', ['plugin', 'list', '--json'], { allowFailure: true });
|
|
683
|
+
const data = parseJson(result.stdout, {});
|
|
684
|
+
return Array.isArray(data?.installed)
|
|
685
|
+
? data.installed.find(item => item.pluginId === `${INTEGRATION_NAME}@${MARKETPLACE_NAME}`)
|
|
686
|
+
: undefined;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
codexMarketplaceExists() {
|
|
690
|
+
return this.codexMarketplaceRegisteredSource() !== null;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
codexMarketplaceRegisteredSource() {
|
|
694
|
+
const result = this.execute('codex', ['plugin', 'marketplace', 'list'], { allowFailure: true });
|
|
695
|
+
const line = String(result.stdout || '').split(/\r?\n/).map(item => item.trim()).find(item => item.startsWith(`${MARKETPLACE_NAME} `));
|
|
696
|
+
const source = line ? line.slice(MARKETPLACE_NAME.length).trim() : '';
|
|
697
|
+
return source || null;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
isOwnCodexMarketplaceSource(source) {
|
|
701
|
+
const text = String(source || '').trim();
|
|
702
|
+
if (!text) return false;
|
|
703
|
+
if (text.toLowerCase() === DEFAULT_MARKETPLACE_SOURCE.toLowerCase()) return true;
|
|
704
|
+
if (/^(?:git\+)?https:\/\/github\.com\/SanQianX\/project-knowledge-base(?:\.git)?\/?$/i.test(text)) return true;
|
|
705
|
+
if (!path.isAbsolute(text)) return false;
|
|
706
|
+
const directory = path.resolve(text);
|
|
707
|
+
let stat = null;
|
|
708
|
+
try { stat = fs.statSync(directory); } catch { return true; }
|
|
709
|
+
if (!stat.isDirectory()) return false;
|
|
710
|
+
try {
|
|
711
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(directory, 'package.json'), 'utf8'));
|
|
712
|
+
return Boolean(manifest) && manifest.name === INTEGRATION_PACKAGE_NAME;
|
|
713
|
+
} catch { return false; }
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
ensureClaudeMarketplace() {
|
|
717
|
+
if (!this.explicitMarketplace && !fs.existsSync(path.join(this.rootDir, '.claude-plugin/marketplace.json'))) throw new Error('npm 包内的插件市场缺失;不会回退到 GitHub 下载。');
|
|
718
|
+
if (!this.dryRun && this.claudeMarketplaceExists()) {
|
|
719
|
+
if (this.explicitMarketplace) return;
|
|
720
|
+
let source;
|
|
721
|
+
try { source = JSON.parse(fs.readFileSync(path.join(this.homeDir, '.claude/plugins/known_marketplaces.json'), 'utf8'))[MARKETPLACE_NAME]?.source; } catch {}
|
|
722
|
+
const directory = source?.path || source?.directory;
|
|
723
|
+
if (directory && path.resolve(directory).toLowerCase() === path.resolve(this.rootDir).toLowerCase()) return;
|
|
724
|
+
const remote = source?.repo === DEFAULT_MARKETPLACE_SOURCE || /^https:\/\/github\.com\/SanQianX\/project-knowledge-base(?:\.git)?\/?$/i.test(source?.url || '');
|
|
725
|
+
if (!remote) throw new Error('同名插件市场来源不是当前 npm 包,且无法确认属于本产品;未覆盖第三方来源。请检查插件市场配置。');
|
|
726
|
+
this.execute('claude', ['plugin', 'marketplace', 'remove', MARKETPLACE_NAME]);
|
|
727
|
+
}
|
|
728
|
+
this.execute('claude', ['plugin', 'marketplace', 'add', this.claudeMarketplaceSource]);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
addCodexMarketplaceSource(source, options = {}) {
|
|
732
|
+
// Codex 0.137 parses @ in an absolute npm path as a Git ref. Resolve a
|
|
733
|
+
// real local directory via cwd instead; rollback must use its own cwd.
|
|
734
|
+
let local = false;
|
|
735
|
+
if (path.isAbsolute(source) && source.includes('@')) {
|
|
736
|
+
try { local = fs.statSync(source).isDirectory(); } catch {}
|
|
737
|
+
}
|
|
738
|
+
return this.execute('codex', ['plugin', 'marketplace', 'add', local ? '.' : source], local ? { ...options, cwd: source } : options);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
ensureCodexMarketplace() {
|
|
742
|
+
const target = path.resolve(this.codexMarketplaceAddSource);
|
|
743
|
+
if (!this.dryRun) {
|
|
744
|
+
const existing = this.codexMarketplaceRegisteredSource();
|
|
745
|
+
if (existing !== null) {
|
|
746
|
+
if (this.explicitCodexMarketplace) return;
|
|
747
|
+
if (path.resolve(existing).toLowerCase() === target.toLowerCase()) return;
|
|
748
|
+
if (!this.isOwnCodexMarketplaceSource(existing)) throw new Error('同名 Codex 插件市场来源不是当前安装包,且无法确认属于本产品;未覆盖第三方来源。请检查 Codex 插件市场配置。');
|
|
749
|
+
this.execute('codex', ['plugin', 'marketplace', 'remove', MARKETPLACE_NAME]);
|
|
750
|
+
const added = this.addCodexMarketplaceSource(target, { allowFailure: true });
|
|
751
|
+
if (added.status !== 0) {
|
|
752
|
+
const restored = this.addCodexMarketplaceSource(existing, { allowFailure: true });
|
|
753
|
+
throw new Error(restored.status === 0
|
|
754
|
+
? 'Codex 插件市场更换为当前安装包失败,已恢复原市场来源;请重试接入。'
|
|
755
|
+
: 'Codex 插件市场更换为当前安装包失败,恢复原市场来源也未成功;请手动检查 codex 插件市场。');
|
|
756
|
+
}
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
this.addCodexMarketplaceSource(target);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
installClaude(options = {}) {
|
|
764
|
+
this.ensureIntegrationRuntimeBinding();
|
|
765
|
+
let plugin = this.dryRun ? null : this.claudePlugin();
|
|
766
|
+
if (plugin?.enabled === false) throw new Error('知识库插件已被用户禁用,请在 Claude Code 中明确启用;未静默修改。');
|
|
767
|
+
this.ensureClaudeMarketplace();
|
|
768
|
+
plugin = this.dryRun ? null : this.claudePlugin();
|
|
769
|
+
if (plugin && (plugin.version !== this.version || !this.automaticContextStatus('claude', { state: 'installed', version: plugin.version }).configured)) return this.updateClaude(options);
|
|
770
|
+
if (plugin) {
|
|
771
|
+
return { state: 'installed', detail: 'plugin already installed' };
|
|
772
|
+
}
|
|
773
|
+
this.execute('claude', [
|
|
774
|
+
'plugin',
|
|
775
|
+
'install',
|
|
776
|
+
`${INTEGRATION_NAME}@${MARKETPLACE_NAME}`,
|
|
777
|
+
'--scope',
|
|
778
|
+
options.scope || 'user',
|
|
779
|
+
]);
|
|
780
|
+
if (!this.dryRun) this.verifyClaudeInstallation();
|
|
781
|
+
return { state: 'installed', detail: 'plugin, MCP, and Skill installed' };
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
updateClaude(options = {}) {
|
|
785
|
+
this.ensureIntegrationRuntimeBinding();
|
|
786
|
+
this.ensureClaudeMarketplace();
|
|
787
|
+
this.execute('claude', ['plugin', 'marketplace', 'update', MARKETPLACE_NAME]);
|
|
788
|
+
if (!this.dryRun && !this.claudePlugin()) return this.installClaude(options);
|
|
789
|
+
this.execute('claude', [
|
|
790
|
+
'plugin',
|
|
791
|
+
'update',
|
|
792
|
+
`${INTEGRATION_NAME}@${MARKETPLACE_NAME}`,
|
|
793
|
+
'--scope',
|
|
794
|
+
options.scope || 'user',
|
|
795
|
+
]);
|
|
796
|
+
if (!this.dryRun) {
|
|
797
|
+
this.verifyClaudeInstallation();
|
|
798
|
+
}
|
|
799
|
+
return { state: 'updated', detail: 'plugin marketplace and installed plugin updated' };
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
verifyClaudeInstallation() {
|
|
803
|
+
const plugin = this.claudePlugin();
|
|
804
|
+
if (!plugin || plugin.version !== this.version || plugin.enabled === false) throw new Error(`插件版本或启用状态未更新到 ${this.version},请检查 marketplace;未把旧插件标为成功。`);
|
|
805
|
+
const automatic = this.automaticContextStatus('claude', { state: 'installed', version: plugin.version });
|
|
806
|
+
if (!automatic.configured) throw new Error(`插件更新后前置读取仍未接线:${automatic.detail} 未误报全部成功。`);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
uninstallClaude(options = {}) {
|
|
810
|
+
if (this.dryRun || this.claudePlugin()) {
|
|
811
|
+
this.execute('claude', [
|
|
812
|
+
'plugin',
|
|
813
|
+
'uninstall',
|
|
814
|
+
`${INTEGRATION_NAME}@${MARKETPLACE_NAME}`,
|
|
815
|
+
'--scope',
|
|
816
|
+
options.scope || 'user',
|
|
817
|
+
]);
|
|
818
|
+
}
|
|
819
|
+
if (this.dryRun || this.claudeMarketplaceExists()) {
|
|
820
|
+
this.execute('claude', ['plugin', 'marketplace', 'remove', MARKETPLACE_NAME]);
|
|
821
|
+
}
|
|
822
|
+
return { state: 'removed', detail: 'plugin and marketplace registration removed' };
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
statusClaude() {
|
|
826
|
+
const command = this.executable('claude');
|
|
827
|
+
if (!command) return { state: 'unavailable', installed: false, detail: 'Claude Code CLI not found' };
|
|
828
|
+
const plugin = this.claudePlugin();
|
|
829
|
+
return {
|
|
830
|
+
state: plugin?.enabled === false ? 'disabled' : plugin && plugin.version !== this.version ? 'needs-update' : plugin ? 'installed' : 'not-installed',
|
|
831
|
+
installed: Boolean(plugin),
|
|
832
|
+
version: plugin?.version,
|
|
833
|
+
expectedVersion: this.version,
|
|
834
|
+
detail: plugin?.enabled === false ? '插件已被禁用;未自动启用。' : plugin && plugin.version !== this.version ? `插件 ${plugin.version || '未知'} 与运行包 ${this.version} 不一致,请接入/修复。` : plugin ? '知识工具插件已配置;自动上下文状态单独验证。' : 'plugin not installed',
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
installCodex() {
|
|
839
|
+
this.ensureIntegrationRuntimeBinding();
|
|
840
|
+
this.ensureCodexMarketplace();
|
|
841
|
+
const plugin = this.dryRun ? null : this.codexPlugin();
|
|
842
|
+
if (plugin?.enabled === false) throw new Error('知识库插件已被用户禁用,请在 Codex 中明确启用,未静默修改。');
|
|
843
|
+
if (plugin && (plugin.version !== this.version || !this.codexContextFilesMatch(plugin))) return this.updateCodex();
|
|
844
|
+
if (plugin) {
|
|
845
|
+
return { state: 'installed', detail: 'plugin already installed' };
|
|
846
|
+
}
|
|
847
|
+
this.execute('codex', ['plugin', 'add', `${INTEGRATION_NAME}@${MARKETPLACE_NAME}`]);
|
|
848
|
+
if (!this.dryRun) this.verifyCodexInstallation();
|
|
849
|
+
return { state: 'installed', detail: '插件已安装;在原生 /hooks 中审查并信任前置读取 Hook 后才会运行。' };
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
updateCodex() {
|
|
853
|
+
this.ensureIntegrationRuntimeBinding();
|
|
854
|
+
if (!this.dryRun && this.codexPlugin()?.enabled === false) throw new Error('知识库插件已被用户禁用,请在 Codex 中明确启用,未静默修改。');
|
|
855
|
+
this.ensureCodexMarketplace();
|
|
856
|
+
const refreshed = this.execute('codex', ['plugin', 'marketplace', 'upgrade', MARKETPLACE_NAME], { allowFailure: true });
|
|
857
|
+
// Native refresh is Git-only. A local catalogue already points at current
|
|
858
|
+
// files; reinstall those files, but never hide a real Git/network failure.
|
|
859
|
+
if (refreshed.status !== 0 && !/^Error: marketplace `project-knowledge` is not configured as a Git marketplace\s*$/.test(String(refreshed.stderr || refreshed.stdout || '').trim())) {
|
|
860
|
+
throw new Error(String(refreshed.stderr || refreshed.stdout || '').trim() || 'Codex marketplace refresh failed.');
|
|
861
|
+
}
|
|
862
|
+
if (this.dryRun || this.codexPlugin()) {
|
|
863
|
+
this.execute('codex', ['plugin', 'remove', `${INTEGRATION_NAME}@${MARKETPLACE_NAME}`]);
|
|
864
|
+
}
|
|
865
|
+
this.execute('codex', ['plugin', 'add', `${INTEGRATION_NAME}@${MARKETPLACE_NAME}`]);
|
|
866
|
+
if (!this.dryRun) this.verifyCodexInstallation();
|
|
867
|
+
return { state: 'updated', detail: '知识插件已更新;新定义须在原生 /hooks 中审查并信任,未写入信任或跳过审查。' };
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
codexContextFilesMatch(plugin) {
|
|
871
|
+
const directory = plugin?.source?.path;
|
|
872
|
+
if (typeof directory !== 'string' || !path.isAbsolute(directory)) return false;
|
|
873
|
+
return ['hooks/codex.json', 'scripts/context-hook.cjs', '.mcp.json', '.codex-plugin/plugin.json'].every(file => {
|
|
874
|
+
try { const read = root => new TextDecoder('utf-8', { fatal: true }).decode(fs.readFileSync(path.join(root, file))).replace(/\r\n/g, '\n'); return read(directory) === read(path.join(this.rootDir, 'plugins', INTEGRATION_NAME)); } catch { return false; }
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
verifyCodexInstallation() {
|
|
879
|
+
const plugin = this.codexPlugin();
|
|
880
|
+
if (!plugin || plugin.version !== this.version || plugin.enabled === false || !this.codexContextFilesMatch(plugin)) throw new Error(`Codex 插件或前置读取文件未更新到 ${this.version},未把旧插件标为成功。`);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
async automaticCodexContextStatus(knowledge, cwd = this.homeDir) {
|
|
884
|
+
const basic = this.automaticContextStatus('codex', knowledge);
|
|
885
|
+
if (!basic.filesVerified) return basic;
|
|
886
|
+
try {
|
|
887
|
+
const snapshot = await this.codexHookProbe({ command: this.executable('codex'), env: { ...this.env, CODEX_HOME: this.env.CODEX_HOME || path.dirname(this.codexConfigFile) }, cwd });
|
|
888
|
+
const entries = snapshot?.data || [];
|
|
889
|
+
const hooks = entries.flatMap(entry => entry.hooks || []).filter(hook => hook.eventName === 'userPromptSubmit' && hook.pluginId === `${INTEGRATION_NAME}@${MARKETPLACE_NAME}` && /context-hook\.cjs/.test(hook.command || ''));
|
|
890
|
+
const configured = hooks.length === 1 && hooks[0].enabled === true && ['trusted', 'managed'].includes(hooks[0].trustStatus) && entries.every(entry => !entry.errors?.length);
|
|
891
|
+
const disabled = hooks.some(h => h.enabled === false);
|
|
892
|
+
return { ...basic, configured, state: configured ? 'configured' : disabled ? 'disabled' : hooks.length ? 'waiting-trust' : 'needs-repair', trustStatus: hooks[0]?.trustStatus || 'unknown', detail: configured ? '原生前置读取定义已启用并信任;实际读取见知识上下文日志。' : disabled ? 'Codex 前置读取 Hook 已禁用,未擅自启用。' : hooks.length ? '待信任:请在 Codex 原生 /hooks 中审查 Project Knowledge 的 UserPromptSubmit 定义;更新后需重新审查。' : 'Codex 未加载前置读取 Hook,请检查客户端版本、Hook 开关及插件来源。' };
|
|
893
|
+
} catch { return { ...basic, state: 'check-failed', configured: false, detail: '无法验证原生 Hook 的信任状态;未将已安装插件当作自动读取已就绪。请在 Codex 中打开 /hooks 检查。' }; }
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
uninstallCodex() {
|
|
897
|
+
if (this.dryRun || this.codexPlugin()) {
|
|
898
|
+
this.execute('codex', ['plugin', 'remove', `${INTEGRATION_NAME}@${MARKETPLACE_NAME}`]);
|
|
899
|
+
}
|
|
900
|
+
if (this.dryRun || this.codexMarketplaceExists()) {
|
|
901
|
+
this.execute('codex', ['plugin', 'marketplace', 'remove', MARKETPLACE_NAME]);
|
|
902
|
+
}
|
|
903
|
+
return { state: 'removed', detail: 'plugin and marketplace registration removed' };
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
statusCodex() {
|
|
907
|
+
const command = this.executable('codex');
|
|
908
|
+
if (!command) return { state: 'unavailable', installed: false, detail: 'Codex CLI not found' };
|
|
909
|
+
const plugin = this.codexPlugin();
|
|
910
|
+
return {
|
|
911
|
+
state: plugin?.enabled === false ? 'disabled' : plugin && plugin.version !== this.version ? 'needs-update' : plugin ? 'installed' : 'not-installed',
|
|
912
|
+
installed: Boolean(plugin),
|
|
913
|
+
version: plugin?.version,
|
|
914
|
+
expectedVersion: this.version,
|
|
915
|
+
source: plugin?.source,
|
|
916
|
+
detail: plugin?.enabled === false ? '插件已禁用,未自动启用。' : plugin && plugin.version !== this.version ? '知识插件版本不匹配,请更新后重新审查原生 Hook。' : plugin ? '知识插件已安装;原生 Hook 信任状态单独验证。' : 'plugin not installed',
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
openCodeSkillSource() {
|
|
921
|
+
return path.join(
|
|
922
|
+
this.rootDir,
|
|
923
|
+
'plugins',
|
|
924
|
+
INTEGRATION_NAME,
|
|
925
|
+
'skills',
|
|
926
|
+
INTEGRATION_NAME,
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
openCodeInstructionSource() {
|
|
931
|
+
return path.join(this.rootDir, 'plugins', INTEGRATION_NAME, 'opencode', `${INTEGRATION_NAME}.md`);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
openCodeConfigDir() {
|
|
935
|
+
return openCodeConfigDir(this.homeDir, this.env);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
installOpenCode() {
|
|
939
|
+
const source = this.openCodeSkillSource();
|
|
940
|
+
const instructionSource = this.openCodeInstructionSource();
|
|
941
|
+
const configDir = this.openCodeConfigDir();
|
|
942
|
+
const destination = path.join(this.openCodeConfigDir(), 'skills', INTEGRATION_NAME);
|
|
943
|
+
const instructionDestination = path.join(configDir, 'instructions', `${INTEGRATION_NAME}.md`);
|
|
944
|
+
if (!this.dryRun) {
|
|
945
|
+
if (!fs.existsSync(path.join(source, 'SKILL.md'))) throw new Error(`bundled Skill not found: ${source}`);
|
|
946
|
+
if (!fs.existsSync(instructionSource)) throw new Error(`bundled OpenCode instructions not found: ${instructionSource}`);
|
|
947
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
948
|
+
fs.cpSync(source, destination, { recursive: true, force: true });
|
|
949
|
+
fs.mkdirSync(path.dirname(instructionDestination), { recursive: true });
|
|
950
|
+
fs.copyFileSync(instructionSource, instructionDestination);
|
|
951
|
+
}
|
|
952
|
+
this.planned.push({ copy: source, destination });
|
|
953
|
+
this.planned.push({ copy: instructionSource, destination: instructionDestination });
|
|
954
|
+
this.execute('opencode', [
|
|
955
|
+
'mcp',
|
|
956
|
+
'add',
|
|
957
|
+
INTEGRATION_NAME,
|
|
958
|
+
'--',
|
|
959
|
+
'npx',
|
|
960
|
+
'-y',
|
|
961
|
+
'--package',
|
|
962
|
+
this.packageSpec,
|
|
963
|
+
'project-knowledge-mcp',
|
|
964
|
+
]);
|
|
965
|
+
if (!this.dryRun) ensureOpenCodeInstructionConfig(configDir, instructionDestination);
|
|
966
|
+
return { state: 'installed', detail: 'MCP, global Skill, and Instructions installed' };
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
updateOpenCode() {
|
|
970
|
+
const result = this.installOpenCode();
|
|
971
|
+
return { ...result, state: 'updated', detail: 'MCP configuration, global Skill, and Instructions updated' };
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
uninstallOpenCode() {
|
|
975
|
+
const configDir = this.openCodeConfigDir();
|
|
976
|
+
const skillDir = path.join(configDir, 'skills', INTEGRATION_NAME);
|
|
977
|
+
const instructionPath = path.join(configDir, 'instructions', `${INTEGRATION_NAME}.md`);
|
|
978
|
+
if (!this.dryRun) {
|
|
979
|
+
removeOpenCodeMcpConfig(configDir);
|
|
980
|
+
removeOpenCodeInstructionConfig(configDir, instructionPath);
|
|
981
|
+
if (fs.existsSync(skillDir)) fs.rmSync(skillDir, { recursive: true, force: true });
|
|
982
|
+
if (fs.existsSync(instructionPath)) fs.rmSync(instructionPath, { force: true });
|
|
983
|
+
}
|
|
984
|
+
this.planned.push({
|
|
985
|
+
removeConfig: findOpenCodeConfig(configDir),
|
|
986
|
+
removeDirectory: skillDir,
|
|
987
|
+
removeFile: instructionPath,
|
|
988
|
+
});
|
|
989
|
+
return { state: 'removed', detail: 'MCP configuration, global Skill, and Instructions removed' };
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
statusOpenCode() {
|
|
993
|
+
const available = Boolean(this.executable('opencode'));
|
|
994
|
+
const state = readOpenCodeState(this.openCodeConfigDir());
|
|
995
|
+
const installed = state.mcpInstalled && state.skillInstalled && state.instructionInstalled;
|
|
996
|
+
let detail = 'MCP, Skill, and Instructions not installed';
|
|
997
|
+
if (installed) detail = 'MCP, global Skill, and Instructions installed';
|
|
998
|
+
else if (state.mcpInstalled && state.skillInstalled) detail = 'MCP and Skill installed; Instructions missing';
|
|
999
|
+
else if (state.mcpInstalled) detail = 'MCP installed; Skill missing';
|
|
1000
|
+
else if (state.skillInstalled) detail = 'Skill installed; MCP missing';
|
|
1001
|
+
if (!available) detail = `OpenCode CLI not found; ${detail}`;
|
|
1002
|
+
return {
|
|
1003
|
+
state: installed ? 'installed' : available ? 'not-installed' : 'unavailable',
|
|
1004
|
+
installed,
|
|
1005
|
+
detail,
|
|
1006
|
+
configPath: state.configPath,
|
|
1007
|
+
skillPath: state.skillPath,
|
|
1008
|
+
instructionPath: state.instructionPath,
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
async operate(operation, clients, options = {}) {
|
|
1013
|
+
const results = [];
|
|
1014
|
+
let bindingFailure = null;
|
|
1015
|
+
if (operation === 'install' || operation === 'update') {
|
|
1016
|
+
// The runtime binding is a precondition for knowledge installs: a
|
|
1017
|
+
// binding failure must fail the knowledge result, never report success.
|
|
1018
|
+
try {
|
|
1019
|
+
this.ensureIntegrationRuntimeBinding();
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
bindingFailure = Object.assign(new Error('本地 MCP 运行时绑定校验失败,已阻止知识接入;采集能力不受影响。'), { code: 'INTEGRATION_RUNTIME_BINDING_FAILED' });
|
|
1022
|
+
this.log('error', 'integration.runtime_binding_failed', 'Failed to write the local MCP runtime binding.', { error, context: { operation } });
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
for (const rawClient of clients) {
|
|
1026
|
+
const client = normalizeClient(rawClient);
|
|
1027
|
+
try {
|
|
1028
|
+
if (bindingFailure) throw bindingFailure;
|
|
1029
|
+
const suffix = CLIENT_METHOD_SUFFIX[client];
|
|
1030
|
+
let result;
|
|
1031
|
+
if (operation === 'status') result = this[`status${suffix}`]();
|
|
1032
|
+
else if (operation === 'install') result = this[`install${suffix}`](options);
|
|
1033
|
+
else if (operation === 'update') result = this[`update${suffix}`](options);
|
|
1034
|
+
else if (operation === 'uninstall') result = this[`uninstall${suffix}`](options);
|
|
1035
|
+
else throw new Error(`unsupported operation: ${operation}`);
|
|
1036
|
+
if (this.dryRun && operation !== 'status') {
|
|
1037
|
+
result = { ...result, state: 'planned', detail: `${result.detail} (dry-run)` };
|
|
1038
|
+
}
|
|
1039
|
+
results.push({ client, label: CLIENT_LABELS[client], ok: true, ...result });
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
results.push({
|
|
1042
|
+
client,
|
|
1043
|
+
label: CLIENT_LABELS[client],
|
|
1044
|
+
ok: false,
|
|
1045
|
+
state: 'failed',
|
|
1046
|
+
detail: error.message,
|
|
1047
|
+
...(error.code ? { code: error.code } : {}),
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return results;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
async operateCapture(operation, clients, options = {}) {
|
|
1055
|
+
const results = [];
|
|
1056
|
+
for (const rawClient of clients) {
|
|
1057
|
+
const client = normalizeClient(rawClient);
|
|
1058
|
+
results.push({ client, label: CLIENT_LABELS[client], ...(await this.captureOperation(operation, client, options)) });
|
|
1059
|
+
}
|
|
1060
|
+
return results;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// One-click Integration Setup: register the host Bridge consumer once,
|
|
1064
|
+
// then install BOTH capabilities per client. Knowledge Integration and
|
|
1065
|
+
// Development Capture are reported separately; one failing component never
|
|
1066
|
+
// rolls back or blocks the others.
|
|
1067
|
+
async installAll(options = {}) {
|
|
1068
|
+
const clients = options.clients && options.clients.length ? options.clients.map(normalizeClient) : this.detectClients();
|
|
1069
|
+
const summary = { clients: [], bridge: null };
|
|
1070
|
+
try {
|
|
1071
|
+
summary.bridge = { ok: true, ...(await this.registerBridgeConsumer(options.bridgeConsumerMeta || {})) };
|
|
1072
|
+
} catch (error) {
|
|
1073
|
+
summary.bridge = { ok: false, state: 'failed', detail: error.message };
|
|
1074
|
+
}
|
|
1075
|
+
let bindingFailure = null;
|
|
1076
|
+
if (options.captureOnly !== true) {
|
|
1077
|
+
try {
|
|
1078
|
+
summary.runtimeBinding = { ok: true, ...this.ensureIntegrationRuntimeBinding() };
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
summary.runtimeBinding = { ok: false, state: 'failed', detail: error.message };
|
|
1081
|
+
bindingFailure = Object.assign(new Error('本地 MCP 运行时绑定校验失败,已阻止知识接入;采集能力不受影响。'), { code: 'INTEGRATION_RUNTIME_BINDING_FAILED' });
|
|
1082
|
+
this.log('error', 'integration.runtime_binding_failed', 'Failed to write the local MCP runtime binding.', { error, context: { operation: 'installAll' } });
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
for (const client of clients) {
|
|
1086
|
+
const suffix = CLIENT_METHOD_SUFFIX[client];
|
|
1087
|
+
const entry = { client, label: CLIENT_LABELS[client] };
|
|
1088
|
+
if (options.captureOnly !== true) {
|
|
1089
|
+
if (bindingFailure) {
|
|
1090
|
+
entry.knowledgeIntegration = { ok: false, state: 'failed', code: bindingFailure.code, detail: bindingFailure.message };
|
|
1091
|
+
} else {
|
|
1092
|
+
try {
|
|
1093
|
+
entry.knowledgeIntegration = { ok: true, ...(await this[`install${suffix}`](options)) };
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
entry.knowledgeIntegration = { ok: false, state: 'failed', detail: error.message };
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
if (options.knowledgeOnly !== true) {
|
|
1100
|
+
entry.developmentCapture = await this.captureOperation('install', client);
|
|
1101
|
+
}
|
|
1102
|
+
summary.clients.push(entry);
|
|
1103
|
+
}
|
|
1104
|
+
return summary;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
installedMcpLauncherPaths(client) {
|
|
1108
|
+
const launchers = [];
|
|
1109
|
+
const seen = new Set();
|
|
1110
|
+
const consider = directory => {
|
|
1111
|
+
if (typeof directory !== 'string' || !path.isAbsolute(directory)) return;
|
|
1112
|
+
const launcher = path.join(directory, 'scripts', 'mcp-launcher.cjs');
|
|
1113
|
+
if (seen.has(launcher)) return;
|
|
1114
|
+
try { if (!fs.statSync(launcher).isFile()) return; } catch { return; }
|
|
1115
|
+
seen.add(launcher);
|
|
1116
|
+
launchers.push(launcher);
|
|
1117
|
+
};
|
|
1118
|
+
if (client === 'claude') {
|
|
1119
|
+
try {
|
|
1120
|
+
const index = JSON.parse(fs.readFileSync(path.join(this.homeDir, '.claude', 'plugins', 'installed_plugins.json'), 'utf8'));
|
|
1121
|
+
for (const record of (index.plugins?.[`${INTEGRATION_NAME}@${MARKETPLACE_NAME}`] || [])) {
|
|
1122
|
+
if (record && record.version === this.version) consider(record.installPath);
|
|
1123
|
+
}
|
|
1124
|
+
} catch { /* no native install index yet */ }
|
|
1125
|
+
consider(path.join(this.homeDir, '.claude', 'plugins', 'cache', MARKETPLACE_NAME, INTEGRATION_NAME, this.version));
|
|
1126
|
+
}
|
|
1127
|
+
if (client === 'codex' && this.executable('codex')) {
|
|
1128
|
+
try { consider(this.codexPlugin()?.source?.path); } catch { /* the client CLI failed to list plugins; there is no installed entry to probe */ }
|
|
1129
|
+
}
|
|
1130
|
+
return launchers;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
async statusAll(options = {}) {
|
|
1134
|
+
const clients = options.clients && options.clients.length ? options.clients.map(normalizeClient) : SUPPORTED_CLIENTS;
|
|
1135
|
+
const summary = { clients: [], bridge: null };
|
|
1136
|
+
try {
|
|
1137
|
+
summary.bridge = { ok: true, ...(await this.bridgeStatus()) };
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
summary.bridge = { ok: false, state: 'failed', bridgeHealthy: false, detail: error.message };
|
|
1140
|
+
}
|
|
1141
|
+
// Probes start in parallel so one hung launcher costs a single timeout.
|
|
1142
|
+
// Client probes exercise the entry that client would actually launch
|
|
1143
|
+
// (never the host source tree); identical entries share one host check,
|
|
1144
|
+
// and the shared product-runtime probe covers the shipping runtime.
|
|
1145
|
+
const runProbe = info => (this.mcpProbe ? this.mcpProbe(info) : this.probeMcpStartup(info)).catch(() => ({ state: 'failed', checkedAt: new Date().toISOString(), code: 'PROBE_CRASH', expectedVersion: this.version, detail: MCP_PROBE_FAILURE_DETAILS.PROBE_CRASH }));
|
|
1146
|
+
const clientProbes = new Map();
|
|
1147
|
+
const hostProbe = options.captureOnly ? null : runProbe({ client: 'host', scope: 'product-runtime', version: this.version });
|
|
1148
|
+
if (!options.captureOnly) {
|
|
1149
|
+
const sharedByLauncher = new Map();
|
|
1150
|
+
for (const client of clients) {
|
|
1151
|
+
if (client !== 'claude' && client !== 'codex') continue;
|
|
1152
|
+
const launcherPath = this.installedMcpLauncherPaths(client)[0];
|
|
1153
|
+
if (!launcherPath) continue;
|
|
1154
|
+
if (!sharedByLauncher.has(launcherPath)) sharedByLauncher.set(launcherPath, runProbe({ client, scope: `client:${client}`, launcherPath, version: this.version }));
|
|
1155
|
+
clientProbes.set(client, sharedByLauncher.get(launcherPath).then(result => ({ ...result, scope: `client:${client}` })));
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
for (const client of clients) {
|
|
1159
|
+
const suffix = CLIENT_METHOD_SUFFIX[client];
|
|
1160
|
+
const entry = { client, label: CLIENT_LABELS[client], available: Boolean(this.executable(client)), configFound: this.captureConfigFound(client) };
|
|
1161
|
+
if (!options.captureOnly) {
|
|
1162
|
+
try {
|
|
1163
|
+
entry.knowledgeIntegration = { ok: true, ...(await this[`status${suffix}`]()) };
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
entry.knowledgeIntegration = { ok: false, state: 'failed', detail: error.message };
|
|
1166
|
+
}
|
|
1167
|
+
entry.automaticContext = client === 'codex' ? await this.automaticCodexContextStatus(entry.knowledgeIntegration, options.cwd) : this.automaticContextStatus(client, entry.knowledgeIntegration);
|
|
1168
|
+
if (client === 'claude' || client === 'codex') {
|
|
1169
|
+
entry.mcpStartup = clientProbes.has(client)
|
|
1170
|
+
? await clientProbes.get(client)
|
|
1171
|
+
: { state: 'failed', checkedAt: new Date().toISOString(), scope: `client:${client}`, code: 'CLIENT_PLUGIN_ENTRY_MISSING', expectedVersion: this.version, detail: '该客户端没有已安装的知识插件入口;未用宿主源码入口代替客户端实际启动入口做健康判定。' };
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
entry.developmentCapture = await this.captureOperation('status', client);
|
|
1175
|
+
summary.clients.push(entry);
|
|
1176
|
+
}
|
|
1177
|
+
if (hostProbe) summary.mcpRuntime = await hostProbe;
|
|
1178
|
+
return summary;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
automaticContextStatus(client, knowledge) {
|
|
1182
|
+
if (client === 'zcode') { const available = Boolean(this.executable('zcode')); return { supported: true, configured: available && knowledge?.state === 'installed', state: available ? knowledge?.state || 'not-installed' : 'unavailable', detail: available ? knowledge?.detail || '前置读取尚未接入。' : '未找到可运行的 ZCode;配置存在不等于原生入口可用。' }; }
|
|
1183
|
+
if (client === 'codex') {
|
|
1184
|
+
const filesVerified = knowledge?.state === 'installed' && this.codexContextFilesMatch({ source: knowledge.source });
|
|
1185
|
+
return { supported: true, configured: false, filesVerified, state: filesVerified ? 'waiting-trust' : knowledge?.state === 'installed' ? 'needs-repair' : knowledge?.state || 'not-installed', expectedVersion: this.version, detail: filesVerified ? '已安装,等待验证原生 Hook 的信任状态。' : '插件版本、启用或前置读取文件尚未通过验证,请接入/修复。' };
|
|
1186
|
+
}
|
|
1187
|
+
if (client !== 'claude') return { supported: false, state: 'not-supported', configured: false, detail: '当前版本尚未验收此客户端的自动前置读取;知识工具与采集状态单独显示。' };
|
|
1188
|
+
const state = knowledge?.state || 'not-installed';
|
|
1189
|
+
try { if (JSON.parse(fs.readFileSync(this.claudeSettingsFile, 'utf8')).disableAllHooks === true) return { supported: true, state: 'disabled', configured: false, detail: 'Claude Code 的 Hook 已被用户禁用,未擅自修改。' }; } catch {}
|
|
1190
|
+
if (state !== 'installed') return { supported: true, state, configured: false, expectedVersion: this.version, detail: state === 'needs-update' ? '知识插件版本不匹配,请接入/修复后重新打开 Claude Code。' : state === 'disabled' ? '插件已被禁用;请在客户端明确启用。' : '前置知识读取尚未接入。' };
|
|
1191
|
+
if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.-]+)?$/.test(knowledge.version || '')) return { supported: true, state: 'needs-update', configured: false, detail: '无法验证插件版本。' };
|
|
1192
|
+
let locations = [];
|
|
1193
|
+
try {
|
|
1194
|
+
const index = JSON.parse(fs.readFileSync(path.join(this.homeDir, '.claude/plugins/installed_plugins.json'), 'utf8'));
|
|
1195
|
+
locations = (index.plugins?.[`${INTEGRATION_NAME}@${MARKETPLACE_NAME}`] || []).filter(p => p.version === knowledge.version).map(p => p.installPath);
|
|
1196
|
+
} catch {}
|
|
1197
|
+
if (!locations.length) locations.push(path.join(this.homeDir, '.claude/plugins/cache', MARKETPLACE_NAME, INTEGRATION_NAME, knowledge.version));
|
|
1198
|
+
const files = ['hooks/claude.json', 'scripts/context-hook.cjs', '.mcp.json', '.claude-plugin/plugin.json'];
|
|
1199
|
+
const configured = locations.some(directory => typeof directory === 'string' && files.every(file => {
|
|
1200
|
+
// Native marketplaces use Git checkout newline rules; npm integrity
|
|
1201
|
+
// remains byte-exact elsewhere. Only normalize text line endings here.
|
|
1202
|
+
try {
|
|
1203
|
+
const text = target => new TextDecoder('utf-8', { fatal: true }).decode(fs.readFileSync(target)).replace(/\r\n/g, '\n');
|
|
1204
|
+
return text(path.join(directory, file)) === text(path.join(this.rootDir, 'plugins', INTEGRATION_NAME, file));
|
|
1205
|
+
} catch { return false; }
|
|
1206
|
+
}));
|
|
1207
|
+
return { supported: true, state: configured ? 'configured' : 'needs-repair', configured, expectedVersion: this.version, detail: configured ? '前置读取脚本已接线;请重新打开会话。具体需求是否已读取请查看 agent.context 与 knowledge.context 日志。' : '插件前置读取文件缺失或内容不匹配,请更新/修复插件。' };
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Global disable: remove every connector first, then (and only then)
|
|
1211
|
+
// unregister the host Bridge consumer (GATE INSTALL-003 semantics).
|
|
1212
|
+
async disableAllCapture(options = {}) {
|
|
1213
|
+
const clients = options.clients && options.clients.length ? options.clients.map(normalizeClient) : SUPPORTED_CLIENTS;
|
|
1214
|
+
const results = await this.operateCapture('uninstall', clients, { unregisterConsumer: false });
|
|
1215
|
+
if (results.some(result => !result.ok)) return { clients: results, consumer: { ok: false, state: 'kept', detail: '部分采集未能移除,保留宿主消费者,请修复失败项后重试。' } };
|
|
1216
|
+
let consumer = null;
|
|
1217
|
+
try {
|
|
1218
|
+
consumer = { ok: true, ...(await this.unregisterBridgeConsumer()) };
|
|
1219
|
+
} catch (error) {
|
|
1220
|
+
consumer = { ok: false, state: 'failed', detail: error.message };
|
|
1221
|
+
}
|
|
1222
|
+
return { clients: results, consumer };
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
module.exports = {
|
|
1227
|
+
CLIENT_LABELS,
|
|
1228
|
+
DEFAULT_MARKETPLACE_SOURCE,
|
|
1229
|
+
INTEGRATION_NAME,
|
|
1230
|
+
IntegrationManager,
|
|
1231
|
+
MARKETPLACE_NAME,
|
|
1232
|
+
SUPPORTED_CLIENTS,
|
|
1233
|
+
findOpenCodeConfig,
|
|
1234
|
+
ensureOpenCodeInstructionConfig,
|
|
1235
|
+
normalizeClient,
|
|
1236
|
+
openCodeConfigDir,
|
|
1237
|
+
readOpenCodeState,
|
|
1238
|
+
removeOpenCodeMcpConfig,
|
|
1239
|
+
removeOpenCodeInstructionConfig,
|
|
1240
|
+
};
|