@jaguilar87/gaia 5.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +33 -0
- package/.claude-plugin/plugin.json +26 -0
- package/ARCHITECTURE.md +335 -0
- package/CHANGELOG.md +1298 -0
- package/CODE_OF_CONDUCT.md +11 -0
- package/CONTRIBUTING.md +146 -0
- package/INSTALL.md +436 -0
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/SECURITY.md +47 -0
- package/agents/README.md +78 -0
- package/agents/cloud-troubleshooter.md +73 -0
- package/agents/developer.md +65 -0
- package/agents/gaia-operator.md +64 -0
- package/agents/gaia-orchestrator.md +111 -0
- package/agents/gaia-planner.md +53 -0
- package/agents/gaia-system.md +71 -0
- package/agents/gitops-operator.md +61 -0
- package/agents/terraform-architect.md +63 -0
- package/bin/README.md +106 -0
- package/bin/cli/__init__.py +1 -0
- package/bin/cli/approvals.py +740 -0
- package/bin/cli/cleanup.py +562 -0
- package/bin/cli/context.py +283 -0
- package/bin/cli/doctor.py +651 -0
- package/bin/cli/history.py +305 -0
- package/bin/cli/memory.py +483 -0
- package/bin/cli/metrics.py +1068 -0
- package/bin/cli/plans.py +515 -0
- package/bin/cli/status.py +302 -0
- package/bin/cli/update.py +382 -0
- package/bin/gaia +112 -0
- package/bin/gaia-cleanup.js +531 -0
- package/bin/gaia-doctor.js +635 -0
- package/bin/gaia-evidence +126 -0
- package/bin/gaia-history.js +251 -0
- package/bin/gaia-metrics.js +1278 -0
- package/bin/gaia-review.js +269 -0
- package/bin/gaia-scan +44 -0
- package/bin/gaia-scan.py +589 -0
- package/bin/gaia-skills-diagnose.js +929 -0
- package/bin/gaia-status.js +278 -0
- package/bin/gaia-uninstall.js +111 -0
- package/bin/gaia-update.js +919 -0
- package/bin/pre-publish-validate.js +610 -0
- package/bin/python-detect.js +60 -0
- package/bin/validate-sandbox.sh +601 -0
- package/commands/README.md +64 -0
- package/commands/gaia.md +37 -0
- package/commands/scan-project.md +67 -0
- package/config/README.md +71 -0
- package/config/cloud/aws.json +134 -0
- package/config/cloud/gcp.json +139 -0
- package/config/context-contracts.json +158 -0
- package/config/crons-schema.md +81 -0
- package/config/git_standards.json +72 -0
- package/config/surface-routing.json +417 -0
- package/config/universal-rules.json +102 -0
- package/dist/gaia-ops/.claude-plugin/plugin.json +24 -0
- package/dist/gaia-ops/README.md +80 -0
- package/dist/gaia-ops/agents/cloud-troubleshooter.md +73 -0
- package/dist/gaia-ops/agents/developer.md +65 -0
- package/dist/gaia-ops/agents/gaia-operator.md +64 -0
- package/dist/gaia-ops/agents/gaia-orchestrator.md +111 -0
- package/dist/gaia-ops/agents/gaia-planner.md +53 -0
- package/dist/gaia-ops/agents/gaia-system.md +71 -0
- package/dist/gaia-ops/agents/gitops-operator.md +61 -0
- package/dist/gaia-ops/agents/terraform-architect.md +63 -0
- package/dist/gaia-ops/commands/gaia.md +37 -0
- package/dist/gaia-ops/config/README.md +71 -0
- package/dist/gaia-ops/config/cloud/aws.json +134 -0
- package/dist/gaia-ops/config/cloud/gcp.json +139 -0
- package/dist/gaia-ops/config/context-contracts.json +158 -0
- package/dist/gaia-ops/config/crons-schema.md +81 -0
- package/dist/gaia-ops/config/git_standards.json +72 -0
- package/dist/gaia-ops/config/surface-routing.json +417 -0
- package/dist/gaia-ops/config/universal-rules.json +102 -0
- package/dist/gaia-ops/hooks/adapters/__init__.py +52 -0
- package/dist/gaia-ops/hooks/adapters/base.py +219 -0
- package/dist/gaia-ops/hooks/adapters/channel.py +17 -0
- package/dist/gaia-ops/hooks/adapters/claude_code.py +1890 -0
- package/dist/gaia-ops/hooks/adapters/types.py +194 -0
- package/dist/gaia-ops/hooks/adapters/utils.py +25 -0
- package/dist/gaia-ops/hooks/hooks.json +192 -0
- package/dist/gaia-ops/hooks/modules/__init__.py +15 -0
- package/dist/gaia-ops/hooks/modules/agents/__init__.py +29 -0
- package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +647 -0
- package/dist/gaia-ops/hooks/modules/agents/response_contract.py +496 -0
- package/dist/gaia-ops/hooks/modules/agents/skill_injection_verifier.py +120 -0
- package/dist/gaia-ops/hooks/modules/agents/state_tracker.py +267 -0
- package/dist/gaia-ops/hooks/modules/agents/task_info_builder.py +74 -0
- package/dist/gaia-ops/hooks/modules/agents/transcript_analyzer.py +458 -0
- package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +152 -0
- package/dist/gaia-ops/hooks/modules/audit/__init__.py +28 -0
- package/dist/gaia-ops/hooks/modules/audit/event_detector.py +168 -0
- package/dist/gaia-ops/hooks/modules/audit/logger.py +131 -0
- package/dist/gaia-ops/hooks/modules/audit/metrics.py +134 -0
- package/dist/gaia-ops/hooks/modules/audit/workflow_auditor.py +611 -0
- package/dist/gaia-ops/hooks/modules/audit/workflow_recorder.py +296 -0
- package/dist/gaia-ops/hooks/modules/context/__init__.py +11 -0
- package/dist/gaia-ops/hooks/modules/context/agentic_loop_detector.py +165 -0
- package/dist/gaia-ops/hooks/modules/context/anchor_tracker.py +317 -0
- package/dist/gaia-ops/hooks/modules/context/compact_context_builder.py +218 -0
- package/dist/gaia-ops/hooks/modules/context/context_freshness.py +145 -0
- package/dist/gaia-ops/hooks/modules/context/context_injector.py +558 -0
- package/dist/gaia-ops/hooks/modules/context/context_writer.py +530 -0
- package/dist/gaia-ops/hooks/modules/context/contracts_loader.py +161 -0
- package/dist/gaia-ops/hooks/modules/core/__init__.py +40 -0
- package/dist/gaia-ops/hooks/modules/core/hook_entry.py +78 -0
- package/dist/gaia-ops/hooks/modules/core/paths.py +160 -0
- package/dist/gaia-ops/hooks/modules/core/plugin_mode.py +149 -0
- package/dist/gaia-ops/hooks/modules/core/plugin_setup.py +577 -0
- package/dist/gaia-ops/hooks/modules/core/state.py +179 -0
- package/dist/gaia-ops/hooks/modules/core/stdin.py +24 -0
- package/dist/gaia-ops/hooks/modules/events/__init__.py +1 -0
- package/dist/gaia-ops/hooks/modules/events/event_writer.py +210 -0
- package/dist/gaia-ops/hooks/modules/memory/__init__.py +8 -0
- package/dist/gaia-ops/hooks/modules/memory/episode_writer.py +216 -0
- package/dist/gaia-ops/hooks/modules/orchestrator/__init__.py +1 -0
- package/dist/gaia-ops/hooks/modules/orchestrator/delegate_mode.py +122 -0
- package/dist/gaia-ops/hooks/modules/scanning/__init__.py +8 -0
- package/dist/gaia-ops/hooks/modules/scanning/scan_trigger.py +84 -0
- package/dist/gaia-ops/hooks/modules/security/__init__.py +120 -0
- package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +87 -0
- package/dist/gaia-ops/hooks/modules/security/approval_constants.py +23 -0
- package/dist/gaia-ops/hooks/modules/security/approval_grants.py +1638 -0
- package/dist/gaia-ops/hooks/modules/security/approval_messages.py +71 -0
- package/dist/gaia-ops/hooks/modules/security/approval_scopes.py +222 -0
- package/dist/gaia-ops/hooks/modules/security/blocked_commands.py +595 -0
- package/dist/gaia-ops/hooks/modules/security/blocked_message_formatter.py +87 -0
- package/dist/gaia-ops/hooks/modules/security/command_semantics.py +181 -0
- package/dist/gaia-ops/hooks/modules/security/composition_rules.py +547 -0
- package/dist/gaia-ops/hooks/modules/security/flag_classifiers.py +873 -0
- package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +179 -0
- package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +1131 -0
- package/dist/gaia-ops/hooks/modules/security/network_hosts.py +481 -0
- package/dist/gaia-ops/hooks/modules/security/prompt_validator.py +40 -0
- package/dist/gaia-ops/hooks/modules/security/shell_unwrapper.py +165 -0
- package/dist/gaia-ops/hooks/modules/security/tiers.py +196 -0
- package/dist/gaia-ops/hooks/modules/session/__init__.py +10 -0
- package/dist/gaia-ops/hooks/modules/session/pending_scanner.py +174 -0
- package/dist/gaia-ops/hooks/modules/session/session_context_writer.py +100 -0
- package/dist/gaia-ops/hooks/modules/session/session_event_injector.py +160 -0
- package/dist/gaia-ops/hooks/modules/session/session_manager.py +31 -0
- package/dist/gaia-ops/hooks/modules/session/session_registry.py +333 -0
- package/dist/gaia-ops/hooks/modules/tools/__init__.py +29 -0
- package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +1008 -0
- package/dist/gaia-ops/hooks/modules/tools/cloud_pipe_validator.py +231 -0
- package/dist/gaia-ops/hooks/modules/tools/hook_response.py +55 -0
- package/dist/gaia-ops/hooks/modules/tools/shell_parser.py +227 -0
- package/dist/gaia-ops/hooks/modules/tools/stage_decomposer.py +315 -0
- package/dist/gaia-ops/hooks/modules/tools/task_validator.py +294 -0
- package/dist/gaia-ops/hooks/modules/validation/__init__.py +23 -0
- package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +380 -0
- package/dist/gaia-ops/hooks/post_compact.py +43 -0
- package/dist/gaia-ops/hooks/post_tool_use.py +54 -0
- package/dist/gaia-ops/hooks/pre_compact.py +60 -0
- package/dist/gaia-ops/hooks/pre_tool_use.py +413 -0
- package/dist/gaia-ops/hooks/session_end_hook.py +77 -0
- package/dist/gaia-ops/hooks/session_start.py +81 -0
- package/dist/gaia-ops/hooks/stop_hook.py +70 -0
- package/dist/gaia-ops/hooks/subagent_start.py +71 -0
- package/dist/gaia-ops/hooks/subagent_stop.py +295 -0
- package/dist/gaia-ops/hooks/task_completed.py +70 -0
- package/dist/gaia-ops/hooks/user_prompt_submit.py +246 -0
- package/dist/gaia-ops/settings.json +72 -0
- package/dist/gaia-ops/skills/README.md +158 -0
- package/dist/gaia-ops/skills/agent-creation/SKILL.md +87 -0
- package/dist/gaia-ops/skills/agent-creation/examples.md +170 -0
- package/dist/gaia-ops/skills/agent-creation/reference.md +191 -0
- package/dist/gaia-ops/skills/agent-protocol/SKILL.md +93 -0
- package/dist/gaia-ops/skills/agent-protocol/examples.md +223 -0
- package/dist/gaia-ops/skills/agent-response/SKILL.md +69 -0
- package/dist/gaia-ops/skills/agentic-loop/SKILL.md +80 -0
- package/dist/gaia-ops/skills/agentic-loop/reference.md +378 -0
- package/dist/gaia-ops/skills/blog-writing/SKILL.md +98 -0
- package/dist/gaia-ops/skills/blog-writing/reference.md +130 -0
- package/dist/gaia-ops/skills/brief-spec/SKILL.md +185 -0
- package/dist/gaia-ops/skills/command-execution/SKILL.md +64 -0
- package/dist/gaia-ops/skills/command-execution/reference.md +83 -0
- package/dist/gaia-ops/skills/context-updater/SKILL.md +87 -0
- package/dist/gaia-ops/skills/context-updater/examples.md +71 -0
- package/dist/gaia-ops/skills/developer-patterns/SKILL.md +50 -0
- package/dist/gaia-ops/skills/developer-patterns/reference.md +112 -0
- package/dist/gaia-ops/skills/execution/SKILL.md +99 -0
- package/dist/gaia-ops/skills/fast-queries/SKILL.md +43 -0
- package/dist/gaia-ops/skills/gaia-compact/SKILL.md +74 -0
- package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +108 -0
- package/dist/gaia-ops/skills/gaia-patterns/reference.md +395 -0
- package/dist/gaia-ops/skills/gaia-planner/SKILL.md +37 -0
- package/dist/gaia-ops/skills/gaia-planner/reference.md +107 -0
- package/dist/gaia-ops/skills/gaia-release/SKILL.md +85 -0
- package/dist/gaia-ops/skills/gaia-release/reference.md +92 -0
- package/dist/gaia-ops/skills/gaia-self-check/SKILL.md +114 -0
- package/dist/gaia-ops/skills/gaia-self-check/reference.md +453 -0
- package/dist/gaia-ops/skills/gaia-verify/SKILL.md +77 -0
- package/dist/gaia-ops/skills/gaia-verify/reference.md +80 -0
- package/dist/gaia-ops/skills/git-conventions/SKILL.md +47 -0
- package/dist/gaia-ops/skills/gitops-patterns/SKILL.md +60 -0
- package/dist/gaia-ops/skills/gitops-patterns/reference.md +183 -0
- package/dist/gaia-ops/skills/gmail-policy/SKILL.md +200 -0
- package/dist/gaia-ops/skills/gmail-policy/reference.md +150 -0
- package/dist/gaia-ops/skills/gmail-triage/SKILL.md +100 -0
- package/dist/gaia-ops/skills/gws-setup/SKILL.md +99 -0
- package/dist/gaia-ops/skills/gws-setup/reference.md +73 -0
- package/dist/gaia-ops/skills/investigation/SKILL.md +100 -0
- package/dist/gaia-ops/skills/memory-curation/SKILL.md +83 -0
- package/dist/gaia-ops/skills/memory-search/SKILL.md +88 -0
- package/dist/gaia-ops/skills/orchestrator-approval/SKILL.md +160 -0
- package/dist/gaia-ops/skills/orchestrator-approval/reference.md +174 -0
- package/dist/gaia-ops/skills/pending-approvals/SKILL.md +72 -0
- package/dist/gaia-ops/skills/pending-approvals/reference.md +214 -0
- package/dist/gaia-ops/skills/readme-writing/SKILL.md +71 -0
- package/dist/gaia-ops/skills/readme-writing/reference.md +188 -0
- package/dist/gaia-ops/skills/reference.md +135 -0
- package/dist/gaia-ops/skills/request-approval/SKILL.md +140 -0
- package/dist/gaia-ops/skills/request-approval/examples.md +140 -0
- package/dist/gaia-ops/skills/request-approval/reference.md +57 -0
- package/dist/gaia-ops/skills/schedule-task/SKILL.md +64 -0
- package/dist/gaia-ops/skills/schedule-task/reference.md +233 -0
- package/dist/gaia-ops/skills/security-tiers/SKILL.md +141 -0
- package/dist/gaia-ops/skills/security-tiers/destructive-commands-reference.md +623 -0
- package/dist/gaia-ops/skills/security-tiers/reference.md +39 -0
- package/dist/gaia-ops/skills/session-reflection/SKILL.md +69 -0
- package/dist/gaia-ops/skills/skill-creation/SKILL.md +92 -0
- package/dist/gaia-ops/skills/skill-creation/reference.md +29 -0
- package/dist/gaia-ops/skills/terraform-patterns/SKILL.md +89 -0
- package/dist/gaia-ops/skills/terraform-patterns/reference.md +93 -0
- package/dist/gaia-ops/tools/__init__.py +9 -0
- package/dist/gaia-ops/tools/agentic-loop/decide-status.py +210 -0
- package/dist/gaia-ops/tools/agentic-loop/parse-metric.py +106 -0
- package/dist/gaia-ops/tools/agentic-loop/record-iteration.py +221 -0
- package/dist/gaia-ops/tools/context/README.md +132 -0
- package/dist/gaia-ops/tools/context/__init__.py +42 -0
- package/dist/gaia-ops/tools/context/_paths.py +20 -0
- package/dist/gaia-ops/tools/context/context_provider.py +721 -0
- package/dist/gaia-ops/tools/context/context_section_reader.py +342 -0
- package/dist/gaia-ops/tools/context/deep_merge.py +159 -0
- package/dist/gaia-ops/tools/context/pending_updates.py +760 -0
- package/dist/gaia-ops/tools/context/surface_router.py +278 -0
- package/dist/gaia-ops/tools/fast-queries/README.md +65 -0
- package/dist/gaia-ops/tools/fast-queries/__init__.py +30 -0
- package/dist/gaia-ops/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
- package/dist/gaia-ops/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
- package/dist/gaia-ops/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
- package/dist/gaia-ops/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
- package/dist/gaia-ops/tools/fast-queries/run_triage.sh +59 -0
- package/dist/gaia-ops/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
- package/dist/gaia-ops/tools/gaia_simulator/__init__.py +33 -0
- package/dist/gaia-ops/tools/gaia_simulator/cli.py +354 -0
- package/dist/gaia-ops/tools/gaia_simulator/extractor.py +457 -0
- package/dist/gaia-ops/tools/gaia_simulator/reporter.py +258 -0
- package/dist/gaia-ops/tools/gaia_simulator/routing_simulator.py +334 -0
- package/dist/gaia-ops/tools/gaia_simulator/runner.py +539 -0
- package/dist/gaia-ops/tools/gaia_simulator/skills_mapper.py +264 -0
- package/dist/gaia-ops/tools/memory/README.md +0 -0
- package/dist/gaia-ops/tools/memory/__init__.py +20 -0
- package/dist/gaia-ops/tools/memory/backfill_fts5.py +107 -0
- package/dist/gaia-ops/tools/memory/conflict_detector.py +295 -0
- package/dist/gaia-ops/tools/memory/episodic.py +1210 -0
- package/dist/gaia-ops/tools/memory/git_invalidator.py +262 -0
- package/dist/gaia-ops/tools/memory/paths.py +102 -0
- package/dist/gaia-ops/tools/memory/scoring.py +193 -0
- package/dist/gaia-ops/tools/memory/search_store.py +375 -0
- package/dist/gaia-ops/tools/persist_transcript_analysis.py +85 -0
- package/dist/gaia-ops/tools/review/__init__.py +1 -0
- package/dist/gaia-ops/tools/review/review_engine.py +157 -0
- package/dist/gaia-ops/tools/scan/__init__.py +35 -0
- package/dist/gaia-ops/tools/scan/config.py +247 -0
- package/dist/gaia-ops/tools/scan/merge.py +212 -0
- package/dist/gaia-ops/tools/scan/orchestrator.py +549 -0
- package/dist/gaia-ops/tools/scan/registry.py +127 -0
- package/dist/gaia-ops/tools/scan/scanners/__init__.py +18 -0
- package/dist/gaia-ops/tools/scan/scanners/base.py +137 -0
- package/dist/gaia-ops/tools/scan/scanners/environment.py +349 -0
- package/dist/gaia-ops/tools/scan/scanners/git.py +570 -0
- package/dist/gaia-ops/tools/scan/scanners/infrastructure.py +875 -0
- package/dist/gaia-ops/tools/scan/scanners/orchestration.py +600 -0
- package/dist/gaia-ops/tools/scan/scanners/stack.py +1085 -0
- package/dist/gaia-ops/tools/scan/scanners/tools.py +260 -0
- package/dist/gaia-ops/tools/scan/setup.py +686 -0
- package/dist/gaia-ops/tools/scan/tests/__init__.py +1 -0
- package/dist/gaia-ops/tools/scan/tests/conftest.py +796 -0
- package/dist/gaia-ops/tools/scan/tests/test_environment.py +323 -0
- package/dist/gaia-ops/tools/scan/tests/test_git.py +419 -0
- package/dist/gaia-ops/tools/scan/tests/test_infrastructure.py +382 -0
- package/dist/gaia-ops/tools/scan/tests/test_integration.py +920 -0
- package/dist/gaia-ops/tools/scan/tests/test_merge.py +269 -0
- package/dist/gaia-ops/tools/scan/tests/test_orchestration.py +304 -0
- package/dist/gaia-ops/tools/scan/tests/test_stack.py +604 -0
- package/dist/gaia-ops/tools/scan/tests/test_tools.py +349 -0
- package/dist/gaia-ops/tools/scan/ui.py +624 -0
- package/dist/gaia-ops/tools/scan/verify.py +270 -0
- package/dist/gaia-ops/tools/scan/walk.py +118 -0
- package/dist/gaia-ops/tools/scan/workspace.py +85 -0
- package/dist/gaia-ops/tools/validation/README.md +244 -0
- package/dist/gaia-ops/tools/validation/__init__.py +17 -0
- package/dist/gaia-ops/tools/validation/approval_gate.py +321 -0
- package/dist/gaia-ops/tools/validation/validate_skills.py +189 -0
- package/dist/gaia-security/.claude-plugin/plugin.json +24 -0
- package/dist/gaia-security/README.md +90 -0
- package/dist/gaia-security/config/universal-rules.json +102 -0
- package/dist/gaia-security/hooks/adapters/__init__.py +52 -0
- package/dist/gaia-security/hooks/adapters/base.py +219 -0
- package/dist/gaia-security/hooks/adapters/channel.py +17 -0
- package/dist/gaia-security/hooks/adapters/claude_code.py +1890 -0
- package/dist/gaia-security/hooks/adapters/types.py +194 -0
- package/dist/gaia-security/hooks/adapters/utils.py +25 -0
- package/dist/gaia-security/hooks/hooks.json +113 -0
- package/dist/gaia-security/hooks/modules/__init__.py +15 -0
- package/dist/gaia-security/hooks/modules/agents/__init__.py +29 -0
- package/dist/gaia-security/hooks/modules/agents/contract_validator.py +647 -0
- package/dist/gaia-security/hooks/modules/agents/response_contract.py +496 -0
- package/dist/gaia-security/hooks/modules/agents/skill_injection_verifier.py +120 -0
- package/dist/gaia-security/hooks/modules/agents/state_tracker.py +267 -0
- package/dist/gaia-security/hooks/modules/agents/task_info_builder.py +74 -0
- package/dist/gaia-security/hooks/modules/agents/transcript_analyzer.py +458 -0
- package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +152 -0
- package/dist/gaia-security/hooks/modules/audit/__init__.py +28 -0
- package/dist/gaia-security/hooks/modules/audit/event_detector.py +168 -0
- package/dist/gaia-security/hooks/modules/audit/logger.py +131 -0
- package/dist/gaia-security/hooks/modules/audit/metrics.py +134 -0
- package/dist/gaia-security/hooks/modules/audit/workflow_auditor.py +611 -0
- package/dist/gaia-security/hooks/modules/audit/workflow_recorder.py +296 -0
- package/dist/gaia-security/hooks/modules/context/__init__.py +11 -0
- package/dist/gaia-security/hooks/modules/context/agentic_loop_detector.py +165 -0
- package/dist/gaia-security/hooks/modules/context/anchor_tracker.py +317 -0
- package/dist/gaia-security/hooks/modules/context/compact_context_builder.py +218 -0
- package/dist/gaia-security/hooks/modules/context/context_freshness.py +145 -0
- package/dist/gaia-security/hooks/modules/context/context_injector.py +558 -0
- package/dist/gaia-security/hooks/modules/context/context_writer.py +530 -0
- package/dist/gaia-security/hooks/modules/context/contracts_loader.py +161 -0
- package/dist/gaia-security/hooks/modules/core/__init__.py +40 -0
- package/dist/gaia-security/hooks/modules/core/hook_entry.py +78 -0
- package/dist/gaia-security/hooks/modules/core/paths.py +160 -0
- package/dist/gaia-security/hooks/modules/core/plugin_mode.py +149 -0
- package/dist/gaia-security/hooks/modules/core/plugin_setup.py +577 -0
- package/dist/gaia-security/hooks/modules/core/state.py +179 -0
- package/dist/gaia-security/hooks/modules/core/stdin.py +24 -0
- package/dist/gaia-security/hooks/modules/events/__init__.py +1 -0
- package/dist/gaia-security/hooks/modules/events/event_writer.py +210 -0
- package/dist/gaia-security/hooks/modules/memory/__init__.py +8 -0
- package/dist/gaia-security/hooks/modules/memory/episode_writer.py +216 -0
- package/dist/gaia-security/hooks/modules/orchestrator/__init__.py +1 -0
- package/dist/gaia-security/hooks/modules/orchestrator/delegate_mode.py +122 -0
- package/dist/gaia-security/hooks/modules/scanning/__init__.py +8 -0
- package/dist/gaia-security/hooks/modules/scanning/scan_trigger.py +84 -0
- package/dist/gaia-security/hooks/modules/security/__init__.py +120 -0
- package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +87 -0
- package/dist/gaia-security/hooks/modules/security/approval_constants.py +23 -0
- package/dist/gaia-security/hooks/modules/security/approval_grants.py +1638 -0
- package/dist/gaia-security/hooks/modules/security/approval_messages.py +71 -0
- package/dist/gaia-security/hooks/modules/security/approval_scopes.py +222 -0
- package/dist/gaia-security/hooks/modules/security/blocked_commands.py +595 -0
- package/dist/gaia-security/hooks/modules/security/blocked_message_formatter.py +87 -0
- package/dist/gaia-security/hooks/modules/security/command_semantics.py +181 -0
- package/dist/gaia-security/hooks/modules/security/composition_rules.py +547 -0
- package/dist/gaia-security/hooks/modules/security/flag_classifiers.py +873 -0
- package/dist/gaia-security/hooks/modules/security/gitops_validator.py +179 -0
- package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +1131 -0
- package/dist/gaia-security/hooks/modules/security/network_hosts.py +481 -0
- package/dist/gaia-security/hooks/modules/security/prompt_validator.py +40 -0
- package/dist/gaia-security/hooks/modules/security/shell_unwrapper.py +165 -0
- package/dist/gaia-security/hooks/modules/security/tiers.py +196 -0
- package/dist/gaia-security/hooks/modules/session/__init__.py +10 -0
- package/dist/gaia-security/hooks/modules/session/pending_scanner.py +174 -0
- package/dist/gaia-security/hooks/modules/session/session_context_writer.py +100 -0
- package/dist/gaia-security/hooks/modules/session/session_event_injector.py +160 -0
- package/dist/gaia-security/hooks/modules/session/session_manager.py +31 -0
- package/dist/gaia-security/hooks/modules/session/session_registry.py +333 -0
- package/dist/gaia-security/hooks/modules/tools/__init__.py +29 -0
- package/dist/gaia-security/hooks/modules/tools/bash_validator.py +1008 -0
- package/dist/gaia-security/hooks/modules/tools/cloud_pipe_validator.py +231 -0
- package/dist/gaia-security/hooks/modules/tools/hook_response.py +55 -0
- package/dist/gaia-security/hooks/modules/tools/shell_parser.py +227 -0
- package/dist/gaia-security/hooks/modules/tools/stage_decomposer.py +315 -0
- package/dist/gaia-security/hooks/modules/tools/task_validator.py +294 -0
- package/dist/gaia-security/hooks/modules/validation/__init__.py +23 -0
- package/dist/gaia-security/hooks/modules/validation/commit_validator.py +380 -0
- package/dist/gaia-security/hooks/post_tool_use.py +54 -0
- package/dist/gaia-security/hooks/pre_tool_use.py +413 -0
- package/dist/gaia-security/hooks/session_end_hook.py +77 -0
- package/dist/gaia-security/hooks/session_start.py +81 -0
- package/dist/gaia-security/hooks/stop_hook.py +70 -0
- package/dist/gaia-security/hooks/user_prompt_submit.py +246 -0
- package/dist/gaia-security/settings.json +58 -0
- package/git-hooks/commit-msg +41 -0
- package/hooks/README.md +100 -0
- package/hooks/adapters/__init__.py +52 -0
- package/hooks/adapters/base.py +219 -0
- package/hooks/adapters/channel.py +17 -0
- package/hooks/adapters/claude_code.py +1890 -0
- package/hooks/adapters/types.py +194 -0
- package/hooks/adapters/utils.py +25 -0
- package/hooks/elicitation_result.py +179 -0
- package/hooks/hooks.json +84 -0
- package/hooks/modules/README.md +189 -0
- package/hooks/modules/__init__.py +15 -0
- package/hooks/modules/agents/__init__.py +29 -0
- package/hooks/modules/agents/contract_validator.py +647 -0
- package/hooks/modules/agents/response_contract.py +496 -0
- package/hooks/modules/agents/skill_injection_verifier.py +120 -0
- package/hooks/modules/agents/state_tracker.py +267 -0
- package/hooks/modules/agents/task_info_builder.py +74 -0
- package/hooks/modules/agents/transcript_analyzer.py +458 -0
- package/hooks/modules/agents/transcript_reader.py +152 -0
- package/hooks/modules/audit/__init__.py +28 -0
- package/hooks/modules/audit/event_detector.py +168 -0
- package/hooks/modules/audit/logger.py +131 -0
- package/hooks/modules/audit/metrics.py +134 -0
- package/hooks/modules/audit/workflow_auditor.py +611 -0
- package/hooks/modules/audit/workflow_recorder.py +296 -0
- package/hooks/modules/context/__init__.py +11 -0
- package/hooks/modules/context/agentic_loop_detector.py +165 -0
- package/hooks/modules/context/anchor_tracker.py +317 -0
- package/hooks/modules/context/compact_context_builder.py +218 -0
- package/hooks/modules/context/context_freshness.py +145 -0
- package/hooks/modules/context/context_injector.py +558 -0
- package/hooks/modules/context/context_writer.py +530 -0
- package/hooks/modules/context/contracts_loader.py +161 -0
- package/hooks/modules/core/__init__.py +40 -0
- package/hooks/modules/core/hook_entry.py +78 -0
- package/hooks/modules/core/paths.py +160 -0
- package/hooks/modules/core/plugin_mode.py +149 -0
- package/hooks/modules/core/plugin_setup.py +577 -0
- package/hooks/modules/core/state.py +179 -0
- package/hooks/modules/core/stdin.py +24 -0
- package/hooks/modules/events/__init__.py +1 -0
- package/hooks/modules/events/event_writer.py +210 -0
- package/hooks/modules/evidence/__init__.py +34 -0
- package/hooks/modules/evidence/assertions.py +137 -0
- package/hooks/modules/evidence/index_writer.py +57 -0
- package/hooks/modules/evidence/loader.py +126 -0
- package/hooks/modules/evidence/runner.py +241 -0
- package/hooks/modules/memory/__init__.py +8 -0
- package/hooks/modules/memory/episode_writer.py +216 -0
- package/hooks/modules/orchestrator/__init__.py +1 -0
- package/hooks/modules/orchestrator/delegate_mode.py +122 -0
- package/hooks/modules/scanning/__init__.py +8 -0
- package/hooks/modules/scanning/scan_trigger.py +84 -0
- package/hooks/modules/security/__init__.py +120 -0
- package/hooks/modules/security/approval_cleanup.py +87 -0
- package/hooks/modules/security/approval_constants.py +23 -0
- package/hooks/modules/security/approval_grants.py +1638 -0
- package/hooks/modules/security/approval_messages.py +71 -0
- package/hooks/modules/security/approval_scopes.py +222 -0
- package/hooks/modules/security/blocked_commands.py +595 -0
- package/hooks/modules/security/blocked_message_formatter.py +87 -0
- package/hooks/modules/security/command_semantics.py +181 -0
- package/hooks/modules/security/composition_rules.py +547 -0
- package/hooks/modules/security/flag_classifiers.py +873 -0
- package/hooks/modules/security/gitops_validator.py +179 -0
- package/hooks/modules/security/mutative_verbs.py +1131 -0
- package/hooks/modules/security/network_hosts.py +481 -0
- package/hooks/modules/security/prompt_validator.py +40 -0
- package/hooks/modules/security/shell_unwrapper.py +165 -0
- package/hooks/modules/security/tiers.py +196 -0
- package/hooks/modules/session/__init__.py +10 -0
- package/hooks/modules/session/pending_scanner.py +174 -0
- package/hooks/modules/session/session_context_writer.py +100 -0
- package/hooks/modules/session/session_event_injector.py +160 -0
- package/hooks/modules/session/session_manager.py +31 -0
- package/hooks/modules/session/session_registry.py +333 -0
- package/hooks/modules/tools/__init__.py +29 -0
- package/hooks/modules/tools/bash_validator.py +1008 -0
- package/hooks/modules/tools/cloud_pipe_validator.py +231 -0
- package/hooks/modules/tools/hook_response.py +55 -0
- package/hooks/modules/tools/shell_parser.py +227 -0
- package/hooks/modules/tools/stage_decomposer.py +315 -0
- package/hooks/modules/tools/task_validator.py +294 -0
- package/hooks/modules/validation/__init__.py +23 -0
- package/hooks/modules/validation/commit_validator.py +380 -0
- package/hooks/post_compact.py +43 -0
- package/hooks/post_tool_use.py +54 -0
- package/hooks/pre_compact.py +60 -0
- package/hooks/pre_tool_use.py +413 -0
- package/hooks/session_end_hook.py +77 -0
- package/hooks/session_start.py +81 -0
- package/hooks/stop_hook.py +70 -0
- package/hooks/subagent_start.py +71 -0
- package/hooks/subagent_stop.py +295 -0
- package/hooks/task_completed.py +70 -0
- package/hooks/user_prompt_submit.py +246 -0
- package/index.js +83 -0
- package/package.json +103 -0
- package/pyproject.toml +32 -0
- package/skills/README.md +158 -0
- package/skills/agent-creation/SKILL.md +87 -0
- package/skills/agent-creation/examples.md +170 -0
- package/skills/agent-creation/reference.md +191 -0
- package/skills/agent-protocol/SKILL.md +93 -0
- package/skills/agent-protocol/examples.md +223 -0
- package/skills/agent-response/SKILL.md +69 -0
- package/skills/agentic-loop/SKILL.md +80 -0
- package/skills/agentic-loop/reference.md +378 -0
- package/skills/blog-writing/SKILL.md +98 -0
- package/skills/blog-writing/reference.md +130 -0
- package/skills/brief-spec/SKILL.md +185 -0
- package/skills/command-execution/SKILL.md +64 -0
- package/skills/command-execution/reference.md +83 -0
- package/skills/context-updater/SKILL.md +87 -0
- package/skills/context-updater/examples.md +71 -0
- package/skills/developer-patterns/SKILL.md +50 -0
- package/skills/developer-patterns/reference.md +112 -0
- package/skills/execution/SKILL.md +99 -0
- package/skills/fast-queries/SKILL.md +43 -0
- package/skills/gaia-compact/SKILL.md +74 -0
- package/skills/gaia-patterns/SKILL.md +108 -0
- package/skills/gaia-patterns/reference.md +395 -0
- package/skills/gaia-planner/SKILL.md +37 -0
- package/skills/gaia-planner/reference.md +107 -0
- package/skills/gaia-release/SKILL.md +85 -0
- package/skills/gaia-release/reference.md +92 -0
- package/skills/gaia-self-check/SKILL.md +114 -0
- package/skills/gaia-self-check/reference.md +453 -0
- package/skills/gaia-verify/SKILL.md +77 -0
- package/skills/gaia-verify/reference.md +80 -0
- package/skills/git-conventions/SKILL.md +47 -0
- package/skills/gitops-patterns/SKILL.md +60 -0
- package/skills/gitops-patterns/reference.md +183 -0
- package/skills/gmail-policy/SKILL.md +200 -0
- package/skills/gmail-policy/reference.md +150 -0
- package/skills/gmail-triage/SKILL.md +100 -0
- package/skills/gws-setup/SKILL.md +99 -0
- package/skills/gws-setup/reference.md +73 -0
- package/skills/investigation/SKILL.md +100 -0
- package/skills/memory-curation/SKILL.md +83 -0
- package/skills/memory-search/SKILL.md +88 -0
- package/skills/orchestrator-approval/SKILL.md +160 -0
- package/skills/orchestrator-approval/reference.md +174 -0
- package/skills/pending-approvals/SKILL.md +72 -0
- package/skills/pending-approvals/reference.md +214 -0
- package/skills/readme-writing/SKILL.md +71 -0
- package/skills/readme-writing/reference.md +188 -0
- package/skills/reference.md +135 -0
- package/skills/request-approval/SKILL.md +140 -0
- package/skills/request-approval/examples.md +140 -0
- package/skills/request-approval/reference.md +57 -0
- package/skills/schedule-task/SKILL.md +64 -0
- package/skills/schedule-task/reference.md +233 -0
- package/skills/security-tiers/SKILL.md +141 -0
- package/skills/security-tiers/destructive-commands-reference.md +623 -0
- package/skills/security-tiers/reference.md +39 -0
- package/skills/session-reflection/SKILL.md +69 -0
- package/skills/skill-creation/SKILL.md +92 -0
- package/skills/skill-creation/reference.md +29 -0
- package/skills/terraform-patterns/SKILL.md +89 -0
- package/skills/terraform-patterns/reference.md +93 -0
- package/templates/README.md +69 -0
- package/templates/managed-settings.template.json +43 -0
- package/tools/__init__.py +9 -0
- package/tools/agentic-loop/decide-status.py +210 -0
- package/tools/agentic-loop/parse-metric.py +106 -0
- package/tools/agentic-loop/record-iteration.py +221 -0
- package/tools/context/README.md +132 -0
- package/tools/context/__init__.py +42 -0
- package/tools/context/_paths.py +20 -0
- package/tools/context/context_provider.py +721 -0
- package/tools/context/context_section_reader.py +342 -0
- package/tools/context/deep_merge.py +159 -0
- package/tools/context/pending_updates.py +760 -0
- package/tools/context/surface_router.py +278 -0
- package/tools/fast-queries/README.md +65 -0
- package/tools/fast-queries/__init__.py +30 -0
- package/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
- package/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
- package/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
- package/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
- package/tools/fast-queries/run_triage.sh +59 -0
- package/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
- package/tools/gaia_simulator/__init__.py +33 -0
- package/tools/gaia_simulator/cli.py +354 -0
- package/tools/gaia_simulator/extractor.py +457 -0
- package/tools/gaia_simulator/reporter.py +258 -0
- package/tools/gaia_simulator/routing_simulator.py +334 -0
- package/tools/gaia_simulator/runner.py +539 -0
- package/tools/gaia_simulator/skills_mapper.py +264 -0
- package/tools/memory/README.md +0 -0
- package/tools/memory/__init__.py +20 -0
- package/tools/memory/backfill_fts5.py +107 -0
- package/tools/memory/conflict_detector.py +295 -0
- package/tools/memory/episodic.py +1210 -0
- package/tools/memory/git_invalidator.py +262 -0
- package/tools/memory/paths.py +102 -0
- package/tools/memory/scoring.py +193 -0
- package/tools/memory/search_store.py +375 -0
- package/tools/persist_transcript_analysis.py +85 -0
- package/tools/review/__init__.py +1 -0
- package/tools/review/review_engine.py +157 -0
- package/tools/scan/__init__.py +35 -0
- package/tools/scan/config.py +247 -0
- package/tools/scan/merge.py +212 -0
- package/tools/scan/orchestrator.py +549 -0
- package/tools/scan/registry.py +127 -0
- package/tools/scan/scanners/__init__.py +18 -0
- package/tools/scan/scanners/base.py +137 -0
- package/tools/scan/scanners/environment.py +349 -0
- package/tools/scan/scanners/git.py +570 -0
- package/tools/scan/scanners/infrastructure.py +875 -0
- package/tools/scan/scanners/orchestration.py +600 -0
- package/tools/scan/scanners/stack.py +1085 -0
- package/tools/scan/scanners/tools.py +260 -0
- package/tools/scan/setup.py +686 -0
- package/tools/scan/tests/__init__.py +1 -0
- package/tools/scan/tests/conftest.py +796 -0
- package/tools/scan/tests/test_environment.py +323 -0
- package/tools/scan/tests/test_git.py +419 -0
- package/tools/scan/tests/test_infrastructure.py +382 -0
- package/tools/scan/tests/test_integration.py +920 -0
- package/tools/scan/tests/test_merge.py +269 -0
- package/tools/scan/tests/test_orchestration.py +304 -0
- package/tools/scan/tests/test_stack.py +604 -0
- package/tools/scan/tests/test_tools.py +349 -0
- package/tools/scan/ui.py +624 -0
- package/tools/scan/verify.py +270 -0
- package/tools/scan/walk.py +118 -0
- package/tools/scan/workspace.py +85 -0
- package/tools/validation/README.md +244 -0
- package/tools/validation/__init__.py +17 -0
- package/tools/validation/approval_gate.py +321 -0
- package/tools/validation/validate_skills.py +189 -0
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @jaguilar87/gaia - Update script
|
|
5
|
+
*
|
|
6
|
+
* Runs automatically on npm install/update (postinstall hook).
|
|
7
|
+
* Also available as: npx gaia-update
|
|
8
|
+
*
|
|
9
|
+
* Behavior:
|
|
10
|
+
* - First-time install (.claude/ doesn't exist):
|
|
11
|
+
* 1. Check Python 3 is available
|
|
12
|
+
* 2. Run gaia-scan --npm-postinstall to create .claude/, symlinks, settings, project-context
|
|
13
|
+
* 3. Create plugin-registry.json
|
|
14
|
+
* 4. Merge permissions into settings.local.json
|
|
15
|
+
* 5. Merge hooks into settings.local.json
|
|
16
|
+
* 6. Fall through to verification
|
|
17
|
+
* - Update (.claude/ exists):
|
|
18
|
+
* 1. Show version transition (previous → current)
|
|
19
|
+
* 2. settings.json: create only if missing (non-invasive, never overwrites)
|
|
20
|
+
* 3. Merge permissions, env vars, and agent key into settings.local.json (union, preserves user config)
|
|
21
|
+
* 4. Merge hooks from hooks.json into settings.local.json (npm mode requires this)
|
|
22
|
+
* 5. Symlinks: recreate if missing, fix broken ones
|
|
23
|
+
* 5. Verify: hooks, python, project-context, config files
|
|
24
|
+
* 6. Report: summary with any issues found
|
|
25
|
+
*
|
|
26
|
+
* Usage:
|
|
27
|
+
* npm update @jaguilar87/gaia # Automatic via postinstall
|
|
28
|
+
* npx gaia-update # Manual trigger
|
|
29
|
+
* npx gaia-update --verbose # Show all checks
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { fileURLToPath } from 'url';
|
|
33
|
+
import { dirname, join, relative, isAbsolute, resolve as resolvePath } from 'path';
|
|
34
|
+
import fs from 'fs/promises';
|
|
35
|
+
import { existsSync, realpathSync, readdirSync, readlinkSync } from 'fs';
|
|
36
|
+
import { exec } from 'child_process';
|
|
37
|
+
import { promisify } from 'util';
|
|
38
|
+
import chalk from 'chalk';
|
|
39
|
+
import ora from 'ora';
|
|
40
|
+
import { findPython } from './python-detect.js';
|
|
41
|
+
|
|
42
|
+
const execAsync = promisify(exec);
|
|
43
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
44
|
+
const __dirname = dirname(__filename);
|
|
45
|
+
const CWD = process.env.INIT_CWD || process.cwd();
|
|
46
|
+
const VERBOSE = process.argv.includes('--verbose') || process.argv.includes('-v');
|
|
47
|
+
|
|
48
|
+
// Use junctions on Windows (no admin required), regular symlinks elsewhere
|
|
49
|
+
const LINK_TYPE = process.platform === 'win32' ? 'junction' : 'dir';
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Dynamic package resolution
|
|
53
|
+
// ============================================================================
|
|
54
|
+
//
|
|
55
|
+
// The gaia package was renamed from `@jaguilar87/gaia-ops` to `@jaguilar87/gaia`
|
|
56
|
+
// in v5. Hardcoding either name breaks the postinstall on the other variant.
|
|
57
|
+
// Resolve dynamically by scanning `node_modules/@jaguilar87/` for the first
|
|
58
|
+
// `gaia*` package installed in the consumer project.
|
|
59
|
+
//
|
|
60
|
+
// Returns the package name (e.g. `gaia` or `gaia-ops`) or `null` when the
|
|
61
|
+
// scope directory is missing / has no gaia package. Callers should fall back
|
|
62
|
+
// or report not-found rather than assume a default.
|
|
63
|
+
|
|
64
|
+
function resolveGaiaPackageName(cwd = CWD) {
|
|
65
|
+
const scopeDir = join(cwd, 'node_modules', '@jaguilar87');
|
|
66
|
+
if (!existsSync(scopeDir)) return null;
|
|
67
|
+
let entries;
|
|
68
|
+
try {
|
|
69
|
+
entries = readdirSync(scopeDir);
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
// Prefer exact canonical name when present, else first gaia* match.
|
|
74
|
+
if (entries.includes('gaia')) return 'gaia';
|
|
75
|
+
const legacy = entries.find((name) => name.startsWith('gaia'));
|
|
76
|
+
return legacy || null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// Version Detection
|
|
81
|
+
// ============================================================================
|
|
82
|
+
|
|
83
|
+
async function detectVersions() {
|
|
84
|
+
const current = await readPackageVersion(join(__dirname, '..', 'package.json'));
|
|
85
|
+
|
|
86
|
+
// Try to find previous version from the installed package.json backup or lock.
|
|
87
|
+
// Use the dynamically-resolved package name so both `@jaguilar87/gaia` (v5+)
|
|
88
|
+
// and the legacy `@jaguilar87/gaia-ops` are supported on upgrade paths.
|
|
89
|
+
let previous = null;
|
|
90
|
+
try {
|
|
91
|
+
const lockPath = join(CWD, 'package-lock.json');
|
|
92
|
+
if (existsSync(lockPath)) {
|
|
93
|
+
const lock = JSON.parse(await fs.readFile(lockPath, 'utf-8'));
|
|
94
|
+
const pkgName = resolveGaiaPackageName() || 'gaia';
|
|
95
|
+
const dep = lock.packages?.[`node_modules/@jaguilar87/${pkgName}`]
|
|
96
|
+
|| lock.dependencies?.[`@jaguilar87/${pkgName}`];
|
|
97
|
+
if (dep) previous = dep.version;
|
|
98
|
+
}
|
|
99
|
+
} catch { /* ignore */ }
|
|
100
|
+
|
|
101
|
+
return { previous, current };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function readPackageVersion(path) {
|
|
105
|
+
try {
|
|
106
|
+
const pkg = JSON.parse(await fs.readFile(path, 'utf-8'));
|
|
107
|
+
return pkg.version;
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// Update Steps
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
async function updateSettingsJson() {
|
|
118
|
+
const spinner = ora('Checking settings.json...').start();
|
|
119
|
+
try {
|
|
120
|
+
const settingsPath = join(CWD, '.claude', 'settings.json');
|
|
121
|
+
|
|
122
|
+
if (!existsSync(join(CWD, '.claude'))) {
|
|
123
|
+
spinner.info('Skipped (.claude/ not found)');
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Non-invasive: only create if missing. Never overwrite.
|
|
128
|
+
// Hooks come from hooks.json (auto-discovered via symlink).
|
|
129
|
+
// Env vars and permissions live in settings.local.json.
|
|
130
|
+
if (existsSync(settingsPath)) {
|
|
131
|
+
spinner.succeed('settings.json already exists (not overwriting)');
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await fs.writeFile(settingsPath, '{}\n');
|
|
136
|
+
spinner.succeed('settings.json created (minimal — hooks from hooks.json)');
|
|
137
|
+
return true;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
spinner.fail(`settings.json: ${error.message}`);
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function updateLocalPermissions() {
|
|
145
|
+
const spinner = ora('Merging permissions into settings.local.json...').start();
|
|
146
|
+
try {
|
|
147
|
+
const claudeDir = join(CWD, '.claude');
|
|
148
|
+
const localPath = join(claudeDir, 'settings.local.json');
|
|
149
|
+
|
|
150
|
+
if (!existsSync(claudeDir)) {
|
|
151
|
+
spinner.info('Skipped (.claude/ not found)');
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Load existing settings.local.json — preserve everything (enabledPlugins, MCP servers, etc.)
|
|
156
|
+
let existing = {};
|
|
157
|
+
if (existsSync(localPath)) {
|
|
158
|
+
try {
|
|
159
|
+
existing = JSON.parse(await fs.readFile(localPath, 'utf-8'));
|
|
160
|
+
} catch {
|
|
161
|
+
existing = {};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Track what changed
|
|
166
|
+
let changed = false;
|
|
167
|
+
|
|
168
|
+
// Set the orchestrator agent identity (always, even if Python extraction fails)
|
|
169
|
+
if (existing.agent !== 'gaia-orchestrator') {
|
|
170
|
+
existing.agent = 'gaia-orchestrator';
|
|
171
|
+
changed = true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Add env vars (smart merge: add if not present, don't overwrite)
|
|
175
|
+
existing.env = existing.env || {};
|
|
176
|
+
if (!('CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS' in existing.env)) {
|
|
177
|
+
existing.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1';
|
|
178
|
+
changed = true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Load permissions from plugin_setup.py — the single source of truth.
|
|
182
|
+
// We use ast.literal_eval to extract the constants without importing
|
|
183
|
+
// the module (which has relative imports that fail standalone).
|
|
184
|
+
let gaiaPerms;
|
|
185
|
+
try {
|
|
186
|
+
const setupPath = join(__dirname, '..', 'hooks', 'modules', 'core', 'plugin_setup.py');
|
|
187
|
+
const pythonCmd = findPython() || 'python3';
|
|
188
|
+
|
|
189
|
+
// Write the extraction script to a temp file instead of using -c with
|
|
190
|
+
// inline code. This avoids shell quoting issues on Windows where
|
|
191
|
+
// backslash paths and nested quotes break the inline Python string.
|
|
192
|
+
const tempScript = join(claudeDir, '.gaia-extract-perms.py');
|
|
193
|
+
const scriptContent = `
|
|
194
|
+
import ast, json, re, sys
|
|
195
|
+
|
|
196
|
+
setup_path = sys.argv[1]
|
|
197
|
+
source = open(setup_path, encoding="utf-8").read()
|
|
198
|
+
|
|
199
|
+
# Extract _DENY_RULES list
|
|
200
|
+
deny_match = re.search(r'^_DENY_RULES\\s*=\\s*\\[', source, re.MULTILINE)
|
|
201
|
+
if deny_match:
|
|
202
|
+
bracket_start = deny_match.start() + source[deny_match.start():].index('[')
|
|
203
|
+
depth, i = 0, bracket_start
|
|
204
|
+
for i, ch in enumerate(source[bracket_start:], bracket_start):
|
|
205
|
+
if ch == '[': depth += 1
|
|
206
|
+
elif ch == ']': depth -= 1
|
|
207
|
+
if depth == 0: break
|
|
208
|
+
deny_rules = ast.literal_eval(source[bracket_start:i+1])
|
|
209
|
+
else:
|
|
210
|
+
deny_rules = []
|
|
211
|
+
|
|
212
|
+
# Extract OPS_PERMISSIONS allow list
|
|
213
|
+
ops_match = re.search(r'^OPS_PERMISSIONS\\s*=', source, re.MULTILINE)
|
|
214
|
+
if ops_match:
|
|
215
|
+
bracket_start = source.index('{', ops_match.start())
|
|
216
|
+
depth, i = 0, bracket_start
|
|
217
|
+
for i, ch in enumerate(source[bracket_start:], bracket_start):
|
|
218
|
+
if ch == '{': depth += 1
|
|
219
|
+
elif ch == '}': depth -= 1
|
|
220
|
+
if depth == 0: break
|
|
221
|
+
# Replace _DENY_RULES reference with actual list for eval
|
|
222
|
+
ops_str = source[bracket_start:i+1].replace('_DENY_RULES', json.dumps(deny_rules))
|
|
223
|
+
ops_perms = ast.literal_eval(ops_str)
|
|
224
|
+
else:
|
|
225
|
+
ops_perms = {'permissions': {'allow': [], 'deny': deny_rules, 'ask': []}}
|
|
226
|
+
|
|
227
|
+
print(json.dumps(ops_perms))
|
|
228
|
+
`;
|
|
229
|
+
await fs.writeFile(tempScript, scriptContent);
|
|
230
|
+
try {
|
|
231
|
+
const { stdout } = await execAsync(
|
|
232
|
+
`${pythonCmd} "${tempScript}" "${setupPath}"`,
|
|
233
|
+
{ timeout: 10000 }
|
|
234
|
+
);
|
|
235
|
+
gaiaPerms = JSON.parse(stdout.trim());
|
|
236
|
+
} finally {
|
|
237
|
+
// Clean up temp script
|
|
238
|
+
try { await fs.unlink(tempScript); } catch { /* ignore */ }
|
|
239
|
+
}
|
|
240
|
+
} catch (pyError) {
|
|
241
|
+
spinner.warn(`Could not load permissions from Python — ${pyError.message || 'unknown error'}`);
|
|
242
|
+
// Still write agent and env changes even if permissions extraction fails
|
|
243
|
+
if (changed) {
|
|
244
|
+
await fs.writeFile(localPath, JSON.stringify(existing, null, 2) + '\n');
|
|
245
|
+
spinner.succeed('settings.local.json agent and env merged (permissions skipped)');
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const ourAllow = new Set(gaiaPerms.permissions.allow || []);
|
|
252
|
+
const ourDeny = new Set(gaiaPerms.permissions.deny || []);
|
|
253
|
+
|
|
254
|
+
const perms = existing.permissions || {};
|
|
255
|
+
const currentAllow = new Set(perms.allow || []);
|
|
256
|
+
const currentDeny = new Set(perms.deny || []);
|
|
257
|
+
|
|
258
|
+
// Union merge — add ours without removing user's
|
|
259
|
+
const mergedAllow = [...new Set([...currentAllow, ...ourAllow])].sort();
|
|
260
|
+
const mergedDeny = [...new Set([...currentDeny, ...ourDeny])].sort();
|
|
261
|
+
|
|
262
|
+
// Check if permissions changed
|
|
263
|
+
const allowChanged = mergedAllow.length !== currentAllow.size
|
|
264
|
+
|| mergedAllow.some(r => !currentAllow.has(r));
|
|
265
|
+
const denyChanged = mergedDeny.length !== currentDeny.size
|
|
266
|
+
|| mergedDeny.some(r => !currentDeny.has(r));
|
|
267
|
+
|
|
268
|
+
if (allowChanged || denyChanged) {
|
|
269
|
+
existing.permissions = existing.permissions || {};
|
|
270
|
+
existing.permissions.allow = mergedAllow;
|
|
271
|
+
existing.permissions.deny = mergedDeny;
|
|
272
|
+
existing.permissions.ask = existing.permissions.ask || [];
|
|
273
|
+
changed = true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!changed) {
|
|
277
|
+
spinner.succeed('settings.local.json permissions already up to date');
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
await fs.writeFile(localPath, JSON.stringify(existing, null, 2) + '\n');
|
|
282
|
+
spinner.succeed('settings.local.json permissions, env, and agent merged');
|
|
283
|
+
return true;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
spinner.fail(`settings.local.json: ${error.message}`);
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function updateLocalHooks() {
|
|
291
|
+
const spinner = ora('Merging hooks into settings.local.json...').start();
|
|
292
|
+
try {
|
|
293
|
+
const claudeDir = join(CWD, '.claude');
|
|
294
|
+
const localPath = join(claudeDir, 'settings.local.json');
|
|
295
|
+
|
|
296
|
+
if (!existsSync(claudeDir)) {
|
|
297
|
+
spinner.info('Skipped (.claude/ not found)');
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Read hooks.json from the installed package
|
|
302
|
+
const hooksJsonPath = join(__dirname, '..', 'hooks', 'hooks.json');
|
|
303
|
+
if (!existsSync(hooksJsonPath)) {
|
|
304
|
+
spinner.warn('hooks.json not found in package');
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let hooksData;
|
|
309
|
+
try {
|
|
310
|
+
hooksData = JSON.parse(await fs.readFile(hooksJsonPath, 'utf-8'));
|
|
311
|
+
} catch {
|
|
312
|
+
spinner.warn('hooks.json is invalid JSON');
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Unwrap outer "hooks" key if present
|
|
317
|
+
const sourceHooks = hooksData.hooks || hooksData;
|
|
318
|
+
|
|
319
|
+
// Resolve absolute path to hooks directory so hooks work regardless of
|
|
320
|
+
// CWD at execution time (Stop/PostCompact hooks may run from unknown CWD)
|
|
321
|
+
const hooksSymlink = join(claudeDir, 'hooks');
|
|
322
|
+
let hooksAbs;
|
|
323
|
+
try {
|
|
324
|
+
hooksAbs = realpathSync(hooksSymlink);
|
|
325
|
+
} catch {
|
|
326
|
+
hooksAbs = hooksSymlink; // Fallback if symlink not yet created
|
|
327
|
+
}
|
|
328
|
+
const convertCommand = (cmd) => {
|
|
329
|
+
return cmd.replace(/\$\{CLAUDE_PLUGIN_ROOT\}\/hooks\//g, `${hooksAbs}/`);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const convertedHooks = {};
|
|
333
|
+
for (const [event, entries] of Object.entries(sourceHooks)) {
|
|
334
|
+
convertedHooks[event] = entries.map(entry => {
|
|
335
|
+
const converted = { ...entry };
|
|
336
|
+
if (converted.hooks) {
|
|
337
|
+
converted.hooks = converted.hooks.map(h => ({
|
|
338
|
+
...h,
|
|
339
|
+
command: h.command ? convertCommand(h.command) : h.command,
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
return converted;
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Load existing settings.local.json
|
|
347
|
+
let existing = {};
|
|
348
|
+
if (existsSync(localPath)) {
|
|
349
|
+
try {
|
|
350
|
+
existing = JSON.parse(await fs.readFile(localPath, 'utf-8'));
|
|
351
|
+
} catch {
|
|
352
|
+
existing = {};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Migrate existing relative .claude/hooks/ paths to absolute
|
|
357
|
+
const existingHooks = existing.hooks || {};
|
|
358
|
+
let changed = false;
|
|
359
|
+
|
|
360
|
+
for (const [event, entries] of Object.entries(existingHooks)) {
|
|
361
|
+
for (const entry of entries) {
|
|
362
|
+
for (const h of (entry.hooks || [])) {
|
|
363
|
+
if (h.command && h.command.startsWith('.claude/hooks/')) {
|
|
364
|
+
h.command = h.command.replace('.claude/hooks/', `${hooksAbs}/`);
|
|
365
|
+
changed = true;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Smart merge: for each hook event, deduplicate by command string
|
|
372
|
+
for (const [event, newEntries] of Object.entries(convertedHooks)) {
|
|
373
|
+
if (!existingHooks[event]) {
|
|
374
|
+
existingHooks[event] = newEntries;
|
|
375
|
+
changed = true;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Collect existing command strings for deduplication
|
|
380
|
+
const existingCommands = new Set();
|
|
381
|
+
for (const entry of existingHooks[event]) {
|
|
382
|
+
for (const h of (entry.hooks || [])) {
|
|
383
|
+
if (h.command) existingCommands.add(h.command);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Add new entries whose commands are not already present
|
|
388
|
+
for (const newEntry of newEntries) {
|
|
389
|
+
const newCommands = (newEntry.hooks || []).map(h => h.command).filter(Boolean);
|
|
390
|
+
const allPresent = newCommands.length > 0 && newCommands.every(c => existingCommands.has(c));
|
|
391
|
+
if (!allPresent) {
|
|
392
|
+
existingHooks[event].push(newEntry);
|
|
393
|
+
changed = true;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (!changed) {
|
|
399
|
+
spinner.succeed('settings.local.json hooks already up to date');
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
existing.hooks = existingHooks;
|
|
404
|
+
await fs.writeFile(localPath, JSON.stringify(existing, null, 2) + '\n');
|
|
405
|
+
spinner.succeed('settings.local.json hooks merged');
|
|
406
|
+
return true;
|
|
407
|
+
} catch (error) {
|
|
408
|
+
spinner.fail(`hooks merge: ${error.message}`);
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function updateSymlinks() {
|
|
414
|
+
const spinner = ora('Checking symlinks...').start();
|
|
415
|
+
try {
|
|
416
|
+
const claudeDir = join(CWD, '.claude');
|
|
417
|
+
if (!existsSync(claudeDir)) {
|
|
418
|
+
spinner.info('Skipped (.claude/ not found)');
|
|
419
|
+
return { updated: false, fixed: 0, total: 0 };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Resolve the installed package name dynamically so this works for both
|
|
423
|
+
// `@jaguilar87/gaia` (v5+) and the legacy `@jaguilar87/gaia-ops` when the
|
|
424
|
+
// consumer happens to have the old name on disk.
|
|
425
|
+
const pkgName = resolveGaiaPackageName();
|
|
426
|
+
if (!pkgName) {
|
|
427
|
+
spinner.fail('Package not found in node_modules/@jaguilar87/');
|
|
428
|
+
return { updated: false, fixed: 0, total: 0 };
|
|
429
|
+
}
|
|
430
|
+
const packagePath = join(CWD, 'node_modules', '@jaguilar87', pkgName);
|
|
431
|
+
if (!existsSync(packagePath)) {
|
|
432
|
+
spinner.fail(`Package not found at ${packagePath}`);
|
|
433
|
+
return { updated: false, fixed: 0, total: 0 };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const relativePath = relative(claudeDir, packagePath);
|
|
437
|
+
const symlinks = ['agents', 'tools', 'hooks', 'commands', 'templates', 'config', 'skills'];
|
|
438
|
+
let fixed = 0;
|
|
439
|
+
|
|
440
|
+
// Helpers: detect a stale symlink by reading the raw link target and
|
|
441
|
+
// checking whether the (absolute-resolved) target exists. Also detect
|
|
442
|
+
// symlinks that still point at the legacy `gaia-ops` path after the
|
|
443
|
+
// rename and repair them to the current package path.
|
|
444
|
+
const isStaleOrLegacy = (link) => {
|
|
445
|
+
let raw;
|
|
446
|
+
try {
|
|
447
|
+
raw = readlinkSync(link);
|
|
448
|
+
} catch {
|
|
449
|
+
return { stale: false, reason: null };
|
|
450
|
+
}
|
|
451
|
+
const absTarget = isAbsolute(raw) ? raw : resolvePath(dirname(link), raw);
|
|
452
|
+
if (!existsSync(absTarget)) {
|
|
453
|
+
return { stale: true, reason: `target missing: ${raw}` };
|
|
454
|
+
}
|
|
455
|
+
// Legacy name detection: if the installed package is `gaia` but the
|
|
456
|
+
// symlink still references `@jaguilar87/gaia-ops`, repair it.
|
|
457
|
+
if (pkgName === 'gaia' && raw.includes('@jaguilar87/gaia-ops')) {
|
|
458
|
+
return { stale: true, reason: `legacy target: ${raw}` };
|
|
459
|
+
}
|
|
460
|
+
return { stale: false, reason: null };
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// Detect whether a path exists as a symlink (broken or not). Plain
|
|
464
|
+
// existsSync returns false for a broken symlink, so we need lstat to
|
|
465
|
+
// distinguish "no entry" from "entry whose target is missing".
|
|
466
|
+
const symlinkEntryExists = (p) => {
|
|
467
|
+
try {
|
|
468
|
+
return readlinkSync(p) !== undefined;
|
|
469
|
+
} catch {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
for (const name of symlinks) {
|
|
475
|
+
const link = join(claudeDir, name);
|
|
476
|
+
// Junctions on Windows require absolute targets; symlinks on Unix use relative
|
|
477
|
+
const target = process.platform === 'win32'
|
|
478
|
+
? join(packagePath, name)
|
|
479
|
+
: join(relativePath, name);
|
|
480
|
+
|
|
481
|
+
if (!existsSync(link) && !symlinkEntryExists(link)) {
|
|
482
|
+
try {
|
|
483
|
+
await fs.symlink(target, link, LINK_TYPE);
|
|
484
|
+
console.log(chalk.gray(`[gaia-update] Created symlink: ${link}`));
|
|
485
|
+
fixed++;
|
|
486
|
+
} catch { /* skip */ }
|
|
487
|
+
} else {
|
|
488
|
+
const { stale, reason } = isStaleOrLegacy(link);
|
|
489
|
+
if (stale) {
|
|
490
|
+
try {
|
|
491
|
+
await fs.unlink(link);
|
|
492
|
+
await fs.symlink(target, link, LINK_TYPE);
|
|
493
|
+
console.log(chalk.gray(`[gaia-update] Repaired stale symlink: ${link} (${reason})`));
|
|
494
|
+
fixed++;
|
|
495
|
+
} catch { /* skip */ }
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// CHANGELOG.md — same stale-detection contract, but file-copy on Windows
|
|
501
|
+
const changelogLink = join(claudeDir, 'CHANGELOG.md');
|
|
502
|
+
const changelogSrc = join(packagePath, 'CHANGELOG.md');
|
|
503
|
+
if (!existsSync(changelogLink)) {
|
|
504
|
+
try {
|
|
505
|
+
if (process.platform === 'win32') {
|
|
506
|
+
// Junctions only work for directories; copy the file on Windows
|
|
507
|
+
await fs.copyFile(changelogSrc, changelogLink);
|
|
508
|
+
} else {
|
|
509
|
+
await fs.symlink(join(relativePath, 'CHANGELOG.md'), changelogLink);
|
|
510
|
+
}
|
|
511
|
+
console.log(chalk.gray(`[gaia-update] Created CHANGELOG link: ${changelogLink}`));
|
|
512
|
+
fixed++;
|
|
513
|
+
} catch { /* skip */ }
|
|
514
|
+
} else if (process.platform !== 'win32') {
|
|
515
|
+
const { stale, reason } = isStaleOrLegacy(changelogLink);
|
|
516
|
+
if (stale) {
|
|
517
|
+
try {
|
|
518
|
+
await fs.unlink(changelogLink);
|
|
519
|
+
await fs.symlink(join(relativePath, 'CHANGELOG.md'), changelogLink);
|
|
520
|
+
console.log(chalk.gray(`[gaia-update] Repaired stale CHANGELOG link (${reason})`));
|
|
521
|
+
fixed++;
|
|
522
|
+
} catch { /* skip */ }
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const total = symlinks.length + 1;
|
|
527
|
+
if (fixed > 0) {
|
|
528
|
+
spinner.succeed(`Symlinks: fixed ${fixed}/${total}`);
|
|
529
|
+
} else {
|
|
530
|
+
spinner.succeed(`Symlinks: ${total}/${total} valid`);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return { updated: fixed > 0, fixed, total };
|
|
534
|
+
} catch (error) {
|
|
535
|
+
spinner.fail(`Symlinks: ${error.message}`);
|
|
536
|
+
return { updated: false, fixed: 0, total: 0 };
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ============================================================================
|
|
541
|
+
// FTS5 Backfill Safety-Net
|
|
542
|
+
// ============================================================================
|
|
543
|
+
//
|
|
544
|
+
// On upgrade paths, a project may already have episodes in
|
|
545
|
+
// `.claude/project-context/episodic-memory/index.json` but an empty FTS5
|
|
546
|
+
// `search.db`. This happens when episodes were produced before FTS5 was wired
|
|
547
|
+
// in, or when `search.db` was deleted for any reason. Fresh installs have
|
|
548
|
+
// zero episodes and will no-op through this check.
|
|
549
|
+
//
|
|
550
|
+
// Opt-out: pass `--no-fts5-backfill` (or set GAIA_SKIP_FTS5_BACKFILL=1).
|
|
551
|
+
|
|
552
|
+
async function maybeBackfillFts5() {
|
|
553
|
+
if (process.argv.includes('--no-fts5-backfill')
|
|
554
|
+
|| process.env.GAIA_SKIP_FTS5_BACKFILL === '1') {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const claudeDir = join(CWD, '.claude');
|
|
559
|
+
if (!existsSync(claudeDir)) return;
|
|
560
|
+
|
|
561
|
+
const memoryDir = join(claudeDir, 'project-context', 'episodic-memory');
|
|
562
|
+
const indexPath = join(memoryDir, 'index.json');
|
|
563
|
+
const dbPath = join(memoryDir, 'search.db');
|
|
564
|
+
|
|
565
|
+
if (!existsSync(indexPath)) return; // Fresh install — nothing to backfill
|
|
566
|
+
|
|
567
|
+
let total = 0;
|
|
568
|
+
try {
|
|
569
|
+
const idx = JSON.parse(await fs.readFile(indexPath, 'utf-8'));
|
|
570
|
+
total = Array.isArray(idx.episodes) ? idx.episodes.length : 0;
|
|
571
|
+
} catch {
|
|
572
|
+
return; // Unreadable index — let doctor handle it
|
|
573
|
+
}
|
|
574
|
+
if (total === 0) return; // No episodes to index
|
|
575
|
+
|
|
576
|
+
// If search.db doesn't exist yet but episodes do, fall through to the
|
|
577
|
+
// backfill step below. doctor --fix creates the db AND populates it.
|
|
578
|
+
// (Previously this returned early "doctor --fix will create it on first
|
|
579
|
+
// use", but nothing in the install flow runs doctor --fix automatically,
|
|
580
|
+
// so the index would stay empty until the user manually invoked it.)
|
|
581
|
+
|
|
582
|
+
// Query FTS5 count via python3 subprocess (sqlite3 binary may not be on PATH
|
|
583
|
+
// on Windows; python3 is already a hard requirement for gaia-ops hooks).
|
|
584
|
+
const pyCmd = findPython();
|
|
585
|
+
if (!pyCmd) return; // Python missing — the health check will report it
|
|
586
|
+
|
|
587
|
+
const spinner = ora('Checking FTS5 backfill status...').start();
|
|
588
|
+
let indexed = 0;
|
|
589
|
+
if (existsSync(dbPath)) {
|
|
590
|
+
try {
|
|
591
|
+
const probeScript = join(memoryDir, '.gaia-fts5-probe.py');
|
|
592
|
+
const probeContent = `
|
|
593
|
+
import sqlite3, sys
|
|
594
|
+
try:
|
|
595
|
+
con = sqlite3.connect(sys.argv[1])
|
|
596
|
+
cur = con.execute("SELECT COUNT(*) FROM episodes_fts")
|
|
597
|
+
print(cur.fetchone()[0])
|
|
598
|
+
except Exception:
|
|
599
|
+
print(-1)
|
|
600
|
+
`;
|
|
601
|
+
await fs.writeFile(probeScript, probeContent);
|
|
602
|
+
try {
|
|
603
|
+
const { stdout } = await execAsync(
|
|
604
|
+
`${pyCmd} "${probeScript}" "${dbPath}"`,
|
|
605
|
+
{ timeout: 10000 }
|
|
606
|
+
);
|
|
607
|
+
indexed = parseInt(stdout.trim(), 10);
|
|
608
|
+
} finally {
|
|
609
|
+
try { await fs.unlink(probeScript); } catch { /* ignore */ }
|
|
610
|
+
}
|
|
611
|
+
} catch {
|
|
612
|
+
spinner.info('FTS5 probe skipped (sqlite3/python issue)');
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (!Number.isFinite(indexed) || indexed < 0) {
|
|
617
|
+
// Table doesn't exist yet — treat as zero indexed, fall through to backfill.
|
|
618
|
+
indexed = 0;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// If indexed is already >=90% of total, no backfill needed — matches doctor
|
|
622
|
+
// threshold exactly so we don't loop users through unnecessary work.
|
|
623
|
+
if (indexed > 0 && indexed / total >= 0.9) {
|
|
624
|
+
spinner.succeed(`FTS5 backfill: ${indexed}/${total} episodes indexed (ok)`);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// else: search.db missing entirely — doctor --fix will create + backfill.
|
|
629
|
+
|
|
630
|
+
spinner.text = existsSync(dbPath)
|
|
631
|
+
? `FTS5 backfill: rebuilding ${total} episodes (had ${indexed})...`
|
|
632
|
+
: `FTS5 backfill: creating index for ${total} episodes...`;
|
|
633
|
+
|
|
634
|
+
// Invoke backfill via the gaia CLI doctor --fix. We call `python3 bin/gaia
|
|
635
|
+
// doctor --fix` from the installed package directory, with CWD set to the
|
|
636
|
+
// consumer project so doctor locates the right .claude/ tree.
|
|
637
|
+
const packageDir = join(__dirname, '..');
|
|
638
|
+
const gaiaEntry = join(packageDir, 'bin', 'gaia');
|
|
639
|
+
|
|
640
|
+
if (!existsSync(gaiaEntry)) {
|
|
641
|
+
spinner.warn('FTS5 backfill skipped (bin/gaia not found)');
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
try {
|
|
646
|
+
const { stdout, stderr } = await execAsync(
|
|
647
|
+
`${pyCmd} "${gaiaEntry}" doctor --fix`,
|
|
648
|
+
{ timeout: 120000, cwd: CWD }
|
|
649
|
+
);
|
|
650
|
+
if (VERBOSE) {
|
|
651
|
+
if (stdout) console.log(chalk.gray(stdout));
|
|
652
|
+
if (stderr) console.log(chalk.yellow(stderr));
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Re-probe to report outcome.
|
|
656
|
+
const probeScript = join(memoryDir, '.gaia-fts5-probe.py');
|
|
657
|
+
const probeContent = `
|
|
658
|
+
import sqlite3, sys
|
|
659
|
+
try:
|
|
660
|
+
con = sqlite3.connect(sys.argv[1])
|
|
661
|
+
cur = con.execute("SELECT COUNT(*) FROM episodes_fts")
|
|
662
|
+
print(cur.fetchone()[0])
|
|
663
|
+
except Exception:
|
|
664
|
+
print(-1)
|
|
665
|
+
`;
|
|
666
|
+
await fs.writeFile(probeScript, probeContent);
|
|
667
|
+
let newIndexed = -1;
|
|
668
|
+
try {
|
|
669
|
+
const { stdout: newOut } = await execAsync(
|
|
670
|
+
`${pyCmd} "${probeScript}" "${dbPath}"`,
|
|
671
|
+
{ timeout: 10000 }
|
|
672
|
+
);
|
|
673
|
+
newIndexed = parseInt(newOut.trim(), 10);
|
|
674
|
+
} finally {
|
|
675
|
+
try { await fs.unlink(probeScript); } catch { /* ignore */ }
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (Number.isFinite(newIndexed) && newIndexed > indexed) {
|
|
679
|
+
spinner.succeed(`FTS5 backfill: rebuilt ${newIndexed}/${total} episodes`);
|
|
680
|
+
} else {
|
|
681
|
+
spinner.warn(`FTS5 backfill completed but index still at ${newIndexed}/${total}`);
|
|
682
|
+
}
|
|
683
|
+
} catch (err) {
|
|
684
|
+
spinner.warn(`FTS5 backfill skipped: ${err.message || 'unknown error'}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ============================================================================
|
|
689
|
+
// Post-Update Verification
|
|
690
|
+
// ============================================================================
|
|
691
|
+
|
|
692
|
+
async function runVerification() {
|
|
693
|
+
const spinner = ora('Verifying installation health...').start();
|
|
694
|
+
const checks = [];
|
|
695
|
+
const issues = [];
|
|
696
|
+
|
|
697
|
+
// 1. Hooks exist and are reachable
|
|
698
|
+
const hookFiles = ['pre_tool_use.py', 'post_tool_use.py', 'subagent_stop.py'];
|
|
699
|
+
for (const hook of hookFiles) {
|
|
700
|
+
const path = join(CWD, '.claude', 'hooks', hook);
|
|
701
|
+
if (existsSync(path)) {
|
|
702
|
+
checks.push({ name: hook, ok: true });
|
|
703
|
+
} else {
|
|
704
|
+
checks.push({ name: hook, ok: false });
|
|
705
|
+
issues.push(`Hook missing: .claude/hooks/${hook}`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// 2. Python available (try python3 first, fall back to python on Windows)
|
|
710
|
+
{
|
|
711
|
+
const pyCmd = findPython();
|
|
712
|
+
if (pyCmd) {
|
|
713
|
+
const { stdout } = await execAsync(`${pyCmd} --version`, { timeout: 5000 });
|
|
714
|
+
checks.push({ name: 'python3', ok: true, detail: stdout.trim() });
|
|
715
|
+
} else {
|
|
716
|
+
checks.push({ name: 'python3', ok: false });
|
|
717
|
+
issues.push('Python 3 not found (required for hooks)');
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// 3. project-context.json exists and is valid
|
|
722
|
+
const ctxPath = join(CWD, '.claude', 'project-context', 'project-context.json');
|
|
723
|
+
if (existsSync(ctxPath)) {
|
|
724
|
+
try {
|
|
725
|
+
const ctx = JSON.parse(await fs.readFile(ctxPath, 'utf-8'));
|
|
726
|
+
const sections = Object.keys(ctx.sections || {}).length;
|
|
727
|
+
checks.push({ name: 'project-context.json', ok: sections >= 3, detail: `${sections} sections` });
|
|
728
|
+
if (sections < 3) issues.push('project-context.json has fewer than 3 sections');
|
|
729
|
+
} catch {
|
|
730
|
+
checks.push({ name: 'project-context.json', ok: false });
|
|
731
|
+
issues.push('project-context.json is invalid JSON');
|
|
732
|
+
}
|
|
733
|
+
} else {
|
|
734
|
+
checks.push({ name: 'project-context.json', ok: false });
|
|
735
|
+
issues.push('project-context.json not found (run gaia-scan)');
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// 4. Config files accessible
|
|
739
|
+
const configFiles = ['git_standards.json', 'universal-rules.json', 'surface-routing.json'];
|
|
740
|
+
for (const cfg of configFiles) {
|
|
741
|
+
const path = join(CWD, '.claude', 'config', cfg);
|
|
742
|
+
if (existsSync(path)) {
|
|
743
|
+
checks.push({ name: cfg, ok: true });
|
|
744
|
+
} else {
|
|
745
|
+
checks.push({ name: cfg, ok: false });
|
|
746
|
+
if (VERBOSE) issues.push(`Config missing: .claude/config/${cfg}`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// 5. Agent definitions accessible
|
|
751
|
+
const agentFiles = ['gaia-orchestrator.md', 'gaia-operator.md', 'terraform-architect.md', 'gitops-operator.md', 'cloud-troubleshooter.md', 'developer.md', 'gaia-system.md', 'gaia-planner.md'];
|
|
752
|
+
let agentsOk = 0;
|
|
753
|
+
for (const agent of agentFiles) {
|
|
754
|
+
if (existsSync(join(CWD, '.claude', 'agents', agent))) agentsOk++;
|
|
755
|
+
}
|
|
756
|
+
checks.push({ name: 'agent definitions', ok: agentsOk === agentFiles.length, detail: `${agentsOk}/${agentFiles.length}` });
|
|
757
|
+
if (agentsOk < agentFiles.length) issues.push(`${agentFiles.length - agentsOk} agent definition(s) missing`);
|
|
758
|
+
|
|
759
|
+
// 6. hooks.json exists (hooks are auto-discovered from hooks directory)
|
|
760
|
+
const hooksJsonPath = join(CWD, '.claude', 'hooks', 'hooks.json');
|
|
761
|
+
if (existsSync(hooksJsonPath)) {
|
|
762
|
+
try {
|
|
763
|
+
const hooksData = JSON.parse(await fs.readFile(hooksJsonPath, 'utf-8'));
|
|
764
|
+
const hasHooks = hooksData.hooks && Object.keys(hooksData.hooks).length > 0;
|
|
765
|
+
checks.push({ name: 'hooks.json', ok: hasHooks });
|
|
766
|
+
if (!hasHooks) issues.push('hooks.json has no hooks configured');
|
|
767
|
+
} catch {
|
|
768
|
+
checks.push({ name: 'hooks.json', ok: false });
|
|
769
|
+
issues.push('hooks.json is invalid');
|
|
770
|
+
}
|
|
771
|
+
} else {
|
|
772
|
+
checks.push({ name: 'hooks.json', ok: false });
|
|
773
|
+
issues.push('hooks.json not found (hooks symlink may be broken)');
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const passed = checks.filter(c => c.ok).length;
|
|
777
|
+
const total = checks.length;
|
|
778
|
+
|
|
779
|
+
if (issues.length === 0) {
|
|
780
|
+
spinner.succeed(`Health check: ${passed}/${total} passed`);
|
|
781
|
+
} else {
|
|
782
|
+
spinner.warn(`Health check: ${passed}/${total} passed, ${issues.length} issue(s)`);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return { checks, issues, passed, total };
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// ============================================================================
|
|
789
|
+
// Main
|
|
790
|
+
// ============================================================================
|
|
791
|
+
|
|
792
|
+
async function runFreshInstall() {
|
|
793
|
+
const packageDir = join(__dirname, '..');
|
|
794
|
+
const scanScript = join(packageDir, 'bin', 'gaia-scan.py');
|
|
795
|
+
const { current } = await detectVersions();
|
|
796
|
+
|
|
797
|
+
console.log(chalk.cyan(`\n gaia-ops ${chalk.green(current)} — fresh install\n`));
|
|
798
|
+
|
|
799
|
+
// 1. Check Python 3 is available (try python3, then python)
|
|
800
|
+
const spinner = ora('Checking Python 3...').start();
|
|
801
|
+
const pyCmd = findPython();
|
|
802
|
+
if (pyCmd) {
|
|
803
|
+
spinner.succeed(`Python 3 found (${pyCmd})`);
|
|
804
|
+
} else {
|
|
805
|
+
spinner.warn('Python 3 not found — skipping project setup');
|
|
806
|
+
console.log(chalk.gray(' Install Python 3.9+ and run: npx gaia-scan\n'));
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// 2. Run gaia-scan --npm-postinstall
|
|
811
|
+
const scanSpinner = ora('Running gaia-scan...').start();
|
|
812
|
+
try {
|
|
813
|
+
const { stdout, stderr } = await execAsync(
|
|
814
|
+
`${pyCmd} "${scanScript}" --npm-postinstall --root "${CWD}"`,
|
|
815
|
+
{ timeout: 60000 }
|
|
816
|
+
);
|
|
817
|
+
scanSpinner.succeed('Project scanned and configured');
|
|
818
|
+
if (VERBOSE && stdout) console.log(chalk.gray(stdout));
|
|
819
|
+
if (VERBOSE && stderr) console.log(chalk.yellow(stderr));
|
|
820
|
+
} catch (error) {
|
|
821
|
+
scanSpinner.warn('gaia-scan encountered issues (non-fatal)');
|
|
822
|
+
if (VERBOSE && error.stderr) console.log(chalk.gray(error.stderr));
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// 3. Create plugin-registry.json (in .claude/, same path Python hooks expect)
|
|
826
|
+
try {
|
|
827
|
+
const claudeDirPath = join(CWD, '.claude');
|
|
828
|
+
if (!existsSync(claudeDirPath)) {
|
|
829
|
+
await fs.mkdir(claudeDirPath, { recursive: true });
|
|
830
|
+
}
|
|
831
|
+
const registryPath = join(claudeDirPath, 'plugin-registry.json');
|
|
832
|
+
const registry = {
|
|
833
|
+
installed: [{ name: 'gaia-ops', version: current || 'unknown' }],
|
|
834
|
+
source: 'npm-postinstall',
|
|
835
|
+
};
|
|
836
|
+
await fs.writeFile(registryPath, JSON.stringify(registry, null, 2) + '\n');
|
|
837
|
+
} catch {
|
|
838
|
+
// Non-fatal — plugin-registry is a convenience, not critical
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// 4. Merge permissions into settings.local.json (same approach as plugin mode)
|
|
842
|
+
await updateLocalPermissions();
|
|
843
|
+
|
|
844
|
+
// 5. Merge hooks into settings.local.json (npm mode — Claude Code reads hooks from settings, not hooks.json)
|
|
845
|
+
await updateLocalHooks();
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async function main() {
|
|
849
|
+
process.stderr.write('[DEPRECATED] gaia-update.js is deprecated. Use: python3 bin/gaia update\n[DEPRECATED] Migration guide: see CHANGELOG.md\n');
|
|
850
|
+
|
|
851
|
+
const claudeDir = join(CWD, '.claude');
|
|
852
|
+
const isUpdate = existsSync(claudeDir);
|
|
853
|
+
|
|
854
|
+
if (!isUpdate) {
|
|
855
|
+
// First-time install — run gaia-scan to bootstrap everything
|
|
856
|
+
await runFreshInstall();
|
|
857
|
+
} else {
|
|
858
|
+
// Version info
|
|
859
|
+
const { previous, current } = await detectVersions();
|
|
860
|
+
const versionLine = previous && previous !== current
|
|
861
|
+
? `${chalk.gray(previous)} → ${chalk.green(current)}`
|
|
862
|
+
: chalk.green(current);
|
|
863
|
+
|
|
864
|
+
console.log(chalk.cyan(`\n gaia-ops update ${versionLine}\n`));
|
|
865
|
+
|
|
866
|
+
// Step 1-4: Update files
|
|
867
|
+
await updateSettingsJson();
|
|
868
|
+
await updateLocalPermissions();
|
|
869
|
+
await updateLocalHooks();
|
|
870
|
+
await updateSymlinks();
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Ensure plugin-registry.json exists in .claude/ (both fresh and update)
|
|
874
|
+
try {
|
|
875
|
+
const registryPath = join(CWD, '.claude', 'plugin-registry.json');
|
|
876
|
+
if (!existsSync(registryPath)) {
|
|
877
|
+
const { current } = await detectVersions();
|
|
878
|
+
const registry = {
|
|
879
|
+
installed: [{ name: 'gaia-ops', version: current || 'unknown' }],
|
|
880
|
+
source: 'npm-postinstall',
|
|
881
|
+
};
|
|
882
|
+
await fs.writeFile(registryPath, JSON.stringify(registry, null, 2) + '\n');
|
|
883
|
+
}
|
|
884
|
+
} catch { /* non-fatal */ }
|
|
885
|
+
|
|
886
|
+
// FTS5 backfill safety-net (no-op on fresh install; only fires when
|
|
887
|
+
// episodes exist in index.json but search.db is under-indexed)
|
|
888
|
+
await maybeBackfillFts5();
|
|
889
|
+
|
|
890
|
+
// Verify (runs for both fresh install and update)
|
|
891
|
+
const { issues, passed, total } = await runVerification();
|
|
892
|
+
|
|
893
|
+
console.log('');
|
|
894
|
+
if (issues.length > 0) {
|
|
895
|
+
console.log(chalk.yellow(` ${issues.length} issue(s) found:`));
|
|
896
|
+
for (const issue of issues) {
|
|
897
|
+
console.log(chalk.yellow(` - ${issue}`));
|
|
898
|
+
}
|
|
899
|
+
} else {
|
|
900
|
+
console.log(chalk.green(' Everything up to date'));
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
console.log(chalk.gray(`\n Health: ${passed}/${total} checks passed\n`));
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Only execute main() when this file is invoked directly (not when imported
|
|
907
|
+
// for testing). This lets unit tests import internal helpers without
|
|
908
|
+
// triggering the postinstall side effects.
|
|
909
|
+
const _invokedDirectly = process.argv[1]
|
|
910
|
+
&& fileURLToPath(import.meta.url) === resolvePath(process.argv[1]);
|
|
911
|
+
|
|
912
|
+
if (_invokedDirectly) {
|
|
913
|
+
main().catch(error => {
|
|
914
|
+
console.error(chalk.red(`\n Update failed: ${error.message}\n`));
|
|
915
|
+
process.exit(0); // Never fail npm install
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
export { resolveGaiaPackageName };
|