@geraldmaron/construct 1.0.0
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/.env.example +69 -0
- package/LICENSE +21 -0
- package/README.md +344 -0
- package/agents/prompts/cx-accessibility.md +29 -0
- package/agents/prompts/cx-ai-engineer.md +31 -0
- package/agents/prompts/cx-architect.md +40 -0
- package/agents/prompts/cx-business-strategist.md +25 -0
- package/agents/prompts/cx-data-analyst.md +30 -0
- package/agents/prompts/cx-data-engineer.md +32 -0
- package/agents/prompts/cx-debugger.md +27 -0
- package/agents/prompts/cx-designer.md +33 -0
- package/agents/prompts/cx-devil-advocate.md +36 -0
- package/agents/prompts/cx-docs-keeper.md +50 -0
- package/agents/prompts/cx-engineer.md +36 -0
- package/agents/prompts/cx-evaluator.md +26 -0
- package/agents/prompts/cx-explorer.md +38 -0
- package/agents/prompts/cx-legal-compliance.md +25 -0
- package/agents/prompts/cx-operations.md +28 -0
- package/agents/prompts/cx-orchestrator.md +30 -0
- package/agents/prompts/cx-platform-engineer.md +25 -0
- package/agents/prompts/cx-product-manager.md +30 -0
- package/agents/prompts/cx-qa.md +42 -0
- package/agents/prompts/cx-rd-lead.md +25 -0
- package/agents/prompts/cx-release-manager.md +31 -0
- package/agents/prompts/cx-researcher.md +31 -0
- package/agents/prompts/cx-reviewer.md +36 -0
- package/agents/prompts/cx-security.md +37 -0
- package/agents/prompts/cx-sre.md +26 -0
- package/agents/prompts/cx-test-automation.md +35 -0
- package/agents/prompts/cx-trace-reviewer.md +76 -0
- package/agents/prompts/cx-ux-researcher.md +30 -0
- package/agents/registry.json +879 -0
- package/agents/teams.json +94 -0
- package/bin/construct +613 -0
- package/commands/build/feature.md +21 -0
- package/commands/build/fix.md +20 -0
- package/commands/design/access.md +19 -0
- package/commands/design/flow.md +16 -0
- package/commands/design/ui.md +18 -0
- package/commands/measure/experiment.md +18 -0
- package/commands/measure/metrics.md +18 -0
- package/commands/measure/results.md +18 -0
- package/commands/plan/api.md +19 -0
- package/commands/plan/challenge.md +18 -0
- package/commands/plan/decide.md +20 -0
- package/commands/plan/feature.md +49 -0
- package/commands/plan/requirements.md +24 -0
- package/commands/remember/context.md +26 -0
- package/commands/remember/handoff.md +19 -0
- package/commands/remember/runbook.md +20 -0
- package/commands/review/code.md +21 -0
- package/commands/review/quality.md +19 -0
- package/commands/review/security.md +16 -0
- package/commands/ship/ready.md +19 -0
- package/commands/ship/release.md +19 -0
- package/commands/ship/status.md +18 -0
- package/commands/understand/docs.md +22 -0
- package/commands/understand/research.md +22 -0
- package/commands/understand/this.md +23 -0
- package/commands/understand/why.md +18 -0
- package/commands/work/clean.md +32 -0
- package/commands/work/drive.md +69 -0
- package/commands/work/optimize-prompts.md +46 -0
- package/commands/work/parallel-review.md +40 -0
- package/db/migrations/001_init.sql +38 -0
- package/langfuse/docker-compose.yml +82 -0
- package/lib/audit-skills.mjs +118 -0
- package/lib/auto-docs.mjs +293 -0
- package/lib/cli-commands.mjs +409 -0
- package/lib/codex-config.mjs +114 -0
- package/lib/comment-lint.mjs +191 -0
- package/lib/completions.mjs +170 -0
- package/lib/context-state.mjs +99 -0
- package/lib/cost.mjs +213 -0
- package/lib/distill.mjs +403 -0
- package/lib/efficiency.mjs +139 -0
- package/lib/env-config.mjs +53 -0
- package/lib/eval-harness.mjs +59 -0
- package/lib/features.mjs +209 -0
- package/lib/headhunt.mjs +597 -0
- package/lib/hooks/adaptive-lint.mjs +112 -0
- package/lib/hooks/agent-tracker.mjs +50 -0
- package/lib/hooks/bootstrap-guard.mjs +90 -0
- package/lib/hooks/comment-lint.mjs +26 -0
- package/lib/hooks/config-protection.mjs +52 -0
- package/lib/hooks/console-warn.mjs +43 -0
- package/lib/hooks/context-window-recovery.mjs +90 -0
- package/lib/hooks/continuation-enforcer.mjs +72 -0
- package/lib/hooks/dep-audit.mjs +155 -0
- package/lib/hooks/drive-guard.mjs +89 -0
- package/lib/hooks/edit-accumulator.mjs +36 -0
- package/lib/hooks/edit-error-recovery.mjs +46 -0
- package/lib/hooks/edit-guard.mjs +109 -0
- package/lib/hooks/env-check.mjs +80 -0
- package/lib/hooks/guard-bash.mjs +83 -0
- package/lib/hooks/mcp-audit.mjs +57 -0
- package/lib/hooks/mcp-health-check.mjs +51 -0
- package/lib/hooks/mcp-task-scope.mjs +47 -0
- package/lib/hooks/model-fallback.mjs +105 -0
- package/lib/hooks/pre-compact.mjs +212 -0
- package/lib/hooks/pre-push-gate.mjs +129 -0
- package/lib/hooks/read-tracker.mjs +129 -0
- package/lib/hooks/registry-sync.mjs +23 -0
- package/lib/hooks/scan-secrets.mjs +76 -0
- package/lib/hooks/session-start.mjs +95 -0
- package/lib/hooks/stop-notify.mjs +191 -0
- package/lib/hooks/stop-typecheck.mjs +83 -0
- package/lib/hooks/task-completed-guard.mjs +43 -0
- package/lib/hooks/teammate-idle-guard.mjs +54 -0
- package/lib/hooks/workflow-guard.mjs +62 -0
- package/lib/host-capabilities.mjs +123 -0
- package/lib/init-docs.mjs +382 -0
- package/lib/mcp/server.mjs +1123 -0
- package/lib/mcp-catalog.json +243 -0
- package/lib/mcp-manager.mjs +433 -0
- package/lib/mcp-platform-config.mjs +104 -0
- package/lib/model-router.mjs +810 -0
- package/lib/opencode-config.mjs +44 -0
- package/lib/opencode-runtime-plugin.mjs +909 -0
- package/lib/opencode-telemetry.mjs +433 -0
- package/lib/orchestration-policy.mjs +258 -0
- package/lib/prompt-composer.mjs +196 -0
- package/lib/prompt-metadata.mjs +68 -0
- package/lib/review.mjs +429 -0
- package/lib/role-preload.mjs +52 -0
- package/lib/schemas/decision.json +50 -0
- package/lib/schemas/implementation.json +51 -0
- package/lib/schemas/review-report.json +32 -0
- package/lib/schemas/test-report.json +43 -0
- package/lib/server/index.mjs +334 -0
- package/lib/server/static/app.js +841 -0
- package/lib/server/static/index.html +820 -0
- package/lib/service-manager.mjs +225 -0
- package/lib/setup.mjs +319 -0
- package/lib/status.mjs +707 -0
- package/lib/storage/backend.mjs +44 -0
- package/lib/storage/embeddings.mjs +70 -0
- package/lib/storage/hybrid-query.mjs +184 -0
- package/lib/storage/sql-store.mjs +55 -0
- package/lib/storage/state-source.mjs +101 -0
- package/lib/storage/sync.mjs +164 -0
- package/lib/storage/vector-store.mjs +59 -0
- package/lib/telemetry/backends/langfuse.mjs +58 -0
- package/lib/telemetry/backends/noop.mjs +16 -0
- package/lib/telemetry/langfuse-ingest.mjs +108 -0
- package/lib/telemetry/langfuse-model-sync.mjs +270 -0
- package/lib/telemetry/team-rollup.mjs +143 -0
- package/lib/toolkit-env.mjs +30 -0
- package/lib/validator.mjs +181 -0
- package/lib/workflow-state.mjs +794 -0
- package/package.json +53 -0
- package/personas/construct.md +94 -0
- package/platforms/claude/CLAUDE.md +30 -0
- package/platforms/claude/settings.template.json +472 -0
- package/platforms/opencode/config.template.json +65 -0
- package/platforms/opencode/plugins/construct-fallback.js +5 -0
- package/platforms/opencode/sync-config.mjs +69 -0
- package/rules/common/agents.md +55 -0
- package/rules/common/code-review.md +129 -0
- package/rules/common/coding-style.md +95 -0
- package/rules/common/comments.md +139 -0
- package/rules/common/cx-agent-routing.md +61 -0
- package/rules/common/cx-skill-routing.md +11 -0
- package/rules/common/development-workflow.md +49 -0
- package/rules/common/git-workflow.md +29 -0
- package/rules/common/hooks.md +35 -0
- package/rules/common/patterns.md +36 -0
- package/rules/common/performance.md +71 -0
- package/rules/common/security.md +34 -0
- package/rules/common/testing.md +62 -0
- package/rules/golang/coding-style.md +37 -0
- package/rules/golang/hooks.md +22 -0
- package/rules/golang/patterns.md +50 -0
- package/rules/golang/security.md +39 -0
- package/rules/golang/testing.md +36 -0
- package/rules/python/coding-style.md +47 -0
- package/rules/python/hooks.md +24 -0
- package/rules/python/patterns.md +44 -0
- package/rules/python/security.md +35 -0
- package/rules/python/testing.md +43 -0
- package/rules/swift/coding-style.md +52 -0
- package/rules/swift/hooks.md +25 -0
- package/rules/swift/patterns.md +71 -0
- package/rules/swift/security.md +38 -0
- package/rules/swift/testing.md +50 -0
- package/rules/typescript/coding-style.md +204 -0
- package/rules/typescript/hooks.md +27 -0
- package/rules/typescript/patterns.md +57 -0
- package/rules/typescript/security.md +33 -0
- package/rules/typescript/testing.md +23 -0
- package/rules/web/coding-style.md +101 -0
- package/rules/web/design-quality.md +68 -0
- package/rules/web/hooks.md +125 -0
- package/rules/web/patterns.md +84 -0
- package/rules/web/performance.md +69 -0
- package/rules/web/security.md +62 -0
- package/rules/web/testing.md +60 -0
- package/rules/zh/README.md +113 -0
- package/rules/zh/agents.md +55 -0
- package/rules/zh/code-review.md +129 -0
- package/rules/zh/coding-style.md +53 -0
- package/rules/zh/development-workflow.md +49 -0
- package/rules/zh/git-workflow.md +29 -0
- package/rules/zh/hooks.md +35 -0
- package/rules/zh/patterns.md +36 -0
- package/rules/zh/performance.md +60 -0
- package/rules/zh/security.md +34 -0
- package/rules/zh/testing.md +34 -0
- package/skills/ai/agent-dev.md +102 -0
- package/skills/ai/llm-security.md +105 -0
- package/skills/ai/ml-ops.md +169 -0
- package/skills/ai/orchestration-workflow.md +87 -0
- package/skills/ai/prompt-and-eval.md +114 -0
- package/skills/ai/prompt-optimizer.md +114 -0
- package/skills/ai/rag-system.md +104 -0
- package/skills/architecture/api-design.md +100 -0
- package/skills/architecture/caching.md +101 -0
- package/skills/architecture/cloud-native.md +97 -0
- package/skills/architecture/message-queue.md +100 -0
- package/skills/architecture/security-arch.md +105 -0
- package/skills/development/cpp.md +127 -0
- package/skills/development/go.md +129 -0
- package/skills/development/java.md +134 -0
- package/skills/development/kotlin.md +140 -0
- package/skills/development/mobile-crossplatform.md +144 -0
- package/skills/development/python.md +109 -0
- package/skills/development/rust.md +127 -0
- package/skills/development/shell.md +127 -0
- package/skills/development/swift.md +129 -0
- package/skills/development/typescript.md +129 -0
- package/skills/devops/ci-cd.md +108 -0
- package/skills/devops/containerization.md +106 -0
- package/skills/devops/cost-optimization.md +105 -0
- package/skills/devops/data-engineering.md +181 -0
- package/skills/devops/database.md +95 -0
- package/skills/devops/dependency-management.md +91 -0
- package/skills/devops/devsecops.md +96 -0
- package/skills/devops/git-workflow.md +100 -0
- package/skills/devops/incident-response.md +167 -0
- package/skills/devops/monorepo.md +87 -0
- package/skills/devops/observability.md +103 -0
- package/skills/devops/performance.md +104 -0
- package/skills/devops/testing.md +104 -0
- package/skills/docs/adr-workflow.md +32 -0
- package/skills/docs/backlog-proposal-workflow.md +19 -0
- package/skills/docs/customer-profile-workflow.md +22 -0
- package/skills/docs/evidence-ingest-workflow.md +23 -0
- package/skills/docs/init-docs.md +160 -0
- package/skills/docs/init-project.md +50 -0
- package/skills/docs/prd-workflow.md +57 -0
- package/skills/docs/prfaq-workflow.md +25 -0
- package/skills/docs/product-intelligence-review.md +22 -0
- package/skills/docs/product-intelligence-workflow.md +62 -0
- package/skills/docs/product-signal-workflow.md +27 -0
- package/skills/docs/research-workflow.md +28 -0
- package/skills/docs/runbook-workflow.md +24 -0
- package/skills/exploration/repo-map.md +297 -0
- package/skills/frameworks/django.md +159 -0
- package/skills/frameworks/nextjs.md +148 -0
- package/skills/frameworks/react.md +132 -0
- package/skills/frameworks/spring-boot.md +165 -0
- package/skills/frontend-design/accessibility.md +153 -0
- package/skills/frontend-design/component-patterns.md +111 -0
- package/skills/frontend-design/engineering.md +122 -0
- package/skills/frontend-design/state-management.md +118 -0
- package/skills/frontend-design/ui-aesthetics.md +102 -0
- package/skills/frontend-design/ux-principles.md +126 -0
- package/skills/quality-gates/review-work.md +90 -0
- package/skills/quality-gates/verify-change.md +94 -0
- package/skills/quality-gates/verify-module.md +96 -0
- package/skills/quality-gates/verify-quality.md +93 -0
- package/skills/quality-gates/verify-security.md +95 -0
- package/skills/roles/architect.ai-systems.md +37 -0
- package/skills/roles/architect.data.md +37 -0
- package/skills/roles/architect.enterprise.md +37 -0
- package/skills/roles/architect.integration.md +37 -0
- package/skills/roles/architect.md +68 -0
- package/skills/roles/architect.platform.md +37 -0
- package/skills/roles/data-analyst.experiment.md +37 -0
- package/skills/roles/data-analyst.md +68 -0
- package/skills/roles/data-analyst.product-intelligence.md +37 -0
- package/skills/roles/data-analyst.product.md +37 -0
- package/skills/roles/data-analyst.telemetry.md +37 -0
- package/skills/roles/data-engineer.pipeline.md +37 -0
- package/skills/roles/data-engineer.vector-retrieval.md +37 -0
- package/skills/roles/data-engineer.warehouse.md +37 -0
- package/skills/roles/debugger.md +68 -0
- package/skills/roles/designer.accessibility.md +43 -0
- package/skills/roles/designer.md +68 -0
- package/skills/roles/engineer.ai.md +49 -0
- package/skills/roles/engineer.data.md +49 -0
- package/skills/roles/engineer.md +85 -0
- package/skills/roles/engineer.platform.md +49 -0
- package/skills/roles/operator.docs.md +43 -0
- package/skills/roles/operator.md +68 -0
- package/skills/roles/operator.release.md +43 -0
- package/skills/roles/operator.sre.md +43 -0
- package/skills/roles/orchestrator.md +66 -0
- package/skills/roles/product-manager.ai-product.md +37 -0
- package/skills/roles/product-manager.business-strategy.md +43 -0
- package/skills/roles/product-manager.enterprise.md +37 -0
- package/skills/roles/product-manager.growth.md +37 -0
- package/skills/roles/product-manager.md +67 -0
- package/skills/roles/product-manager.platform.md +37 -0
- package/skills/roles/product-manager.product.md +37 -0
- package/skills/roles/qa.ai-eval.md +37 -0
- package/skills/roles/qa.api-contract.md +37 -0
- package/skills/roles/qa.data-pipeline.md +37 -0
- package/skills/roles/qa.md +68 -0
- package/skills/roles/qa.test-automation.md +49 -0
- package/skills/roles/qa.web-ui.md +37 -0
- package/skills/roles/researcher.explorer.md +43 -0
- package/skills/roles/researcher.md +68 -0
- package/skills/roles/researcher.ux.md +43 -0
- package/skills/roles/reviewer.devil-advocate.md +43 -0
- package/skills/roles/reviewer.evaluator.md +43 -0
- package/skills/roles/reviewer.md +68 -0
- package/skills/roles/reviewer.trace.md +43 -0
- package/skills/roles/security.ai.md +37 -0
- package/skills/roles/security.appsec.md +37 -0
- package/skills/roles/security.cloud.md +37 -0
- package/skills/roles/security.legal-compliance.md +49 -0
- package/skills/roles/security.md +68 -0
- package/skills/roles/security.privacy.md +37 -0
- package/skills/roles/security.supply-chain.md +37 -0
- package/skills/routing.md +139 -0
- package/skills/security/blue-team.md +83 -0
- package/skills/security/code-audit.md +94 -0
- package/skills/security/pentest.md +84 -0
- package/skills/security/red-team.md +81 -0
- package/skills/security/threat-intel.md +89 -0
- package/skills/security/vuln-research.md +87 -0
- package/skills/utility/clean-code.md +165 -0
- package/sync-agents.mjs +899 -0
- package/templates/docs/adr.md +27 -0
- package/templates/docs/backlog-proposal.md +26 -0
- package/templates/docs/customer-profile.md +34 -0
- package/templates/docs/evidence-brief.md +37 -0
- package/templates/docs/incident-report.md +46 -0
- package/templates/docs/memo.md +27 -0
- package/templates/docs/meta-prd.md +56 -0
- package/templates/docs/one-pager.md +24 -0
- package/templates/docs/prd-business.md +61 -0
- package/templates/docs/prd-platform.md +62 -0
- package/templates/docs/prd.md +47 -0
- package/templates/docs/prfaq.md +31 -0
- package/templates/docs/product-intelligence-report.md +31 -0
- package/templates/docs/research-brief.md +29 -0
- package/templates/docs/rfc-platform.md +60 -0
- package/templates/docs/rfc.md +46 -0
- package/templates/docs/runbook.md +33 -0
- package/templates/docs/signal-brief.md +25 -0
package/sync-agents.mjs
ADDED
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* sync-agents.mjs — regenerate agent adapters for all platforms from agents/registry.json.
|
|
4
|
+
*
|
|
5
|
+
* Reads registry.json, resolves env vars and model tiers, then writes Claude Code,
|
|
6
|
+
* OpenCode, Codex, Copilot, VS Code, and Cursor adapters. Called by 'construct sync'.
|
|
7
|
+
* Protected file — read the CLAUDE.md constraints before editing.
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import { generateCompletions } from "./lib/completions.mjs";
|
|
13
|
+
import {
|
|
14
|
+
buildCodexMcpEntry,
|
|
15
|
+
getCodexConfigPath,
|
|
16
|
+
readCodexConfig,
|
|
17
|
+
removeDanglingConstructMcpMarkers,
|
|
18
|
+
removeDanglingConstructMcpTimeouts,
|
|
19
|
+
removeTomlTables,
|
|
20
|
+
serializeCodexMcpTable,
|
|
21
|
+
tomlString,
|
|
22
|
+
writeCodexConfig,
|
|
23
|
+
} from "./lib/codex-config.mjs";
|
|
24
|
+
import { findOpenCodeConfigPath, readOpenCodeConfig, writeOpenCodeConfig } from "./lib/opencode-config.mjs";
|
|
25
|
+
import { resolvePromptContract } from "./lib/prompt-composer.mjs";
|
|
26
|
+
import {
|
|
27
|
+
buildClaudeMcpEntry,
|
|
28
|
+
buildOpenCodeMcpEntry,
|
|
29
|
+
getOpenCodeMcpId,
|
|
30
|
+
} from "./lib/mcp-platform-config.mjs";
|
|
31
|
+
import { loadConstructEnv } from "./lib/env-config.mjs";
|
|
32
|
+
import { inlineRoleAntiPatterns, PROMPT_WORD_CAP } from "./lib/role-preload.mjs";
|
|
33
|
+
import { resolveTiersForPrimary } from "./lib/model-router.mjs";
|
|
34
|
+
|
|
35
|
+
const home = os.homedir();
|
|
36
|
+
const root = path.resolve(import.meta.dirname);
|
|
37
|
+
|
|
38
|
+
const mergedEnv = loadConstructEnv({ rootDir: root, homeDir: home, env: process.env });
|
|
39
|
+
for (const [key, value] of Object.entries(mergedEnv)) {
|
|
40
|
+
if (!(key in process.env)) process.env[key] = value;
|
|
41
|
+
}
|
|
42
|
+
const registryPath = path.join(root, "agents", "registry.json");
|
|
43
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, "utf8"));
|
|
44
|
+
|
|
45
|
+
function validateRegistry(registry) {
|
|
46
|
+
const errors = [];
|
|
47
|
+
if (!registry.version) errors.push("Missing 'version' field");
|
|
48
|
+
if (!registry.system) errors.push("Missing 'system' field");
|
|
49
|
+
if (!registry.prefix) errors.push("Missing 'prefix' field");
|
|
50
|
+
if (!Array.isArray(registry.agents)) errors.push("Missing or invalid 'agents' array");
|
|
51
|
+
if (!Array.isArray(registry.personas)) errors.push("Missing or invalid 'personas' array");
|
|
52
|
+
const validTiers = new Set(["reasoning", "standard", "fast"]);
|
|
53
|
+
const names = new Set();
|
|
54
|
+
|
|
55
|
+
for (const persona of registry.personas ?? []) {
|
|
56
|
+
if (!persona.name) { errors.push("Persona missing 'name'"); continue; }
|
|
57
|
+
if (names.has(persona.name)) errors.push(`Duplicate name: ${persona.name}`);
|
|
58
|
+
names.add(persona.name);
|
|
59
|
+
if (!persona.description) errors.push(`${persona.name}: missing 'description'`);
|
|
60
|
+
if (!persona.promptFile) errors.push(`${persona.name}: missing 'promptFile'`);
|
|
61
|
+
if (!persona.displayName) errors.push(`${persona.name}: missing 'displayName'`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const agent of registry.agents ?? []) {
|
|
65
|
+
if (!agent.name) { errors.push("Agent missing 'name'"); continue; }
|
|
66
|
+
if (names.has(agent.name)) errors.push(`Duplicate name: ${agent.name}`);
|
|
67
|
+
names.add(agent.name);
|
|
68
|
+
if (!agent.prompt && !agent.promptFile) errors.push(`${agent.name}: missing 'prompt' or 'promptFile'`);
|
|
69
|
+
if (!agent.description) errors.push(`${agent.name}: missing 'description'`);
|
|
70
|
+
if (!agent.model && !agent.modelTier) errors.push(`${agent.name}: needs 'model' or 'modelTier'`);
|
|
71
|
+
if (agent.modelTier && !validTiers.has(agent.modelTier)) errors.push(`${agent.name}: invalid modelTier '${agent.modelTier}'`);
|
|
72
|
+
if (!agent.claudeTools) errors.push(`${agent.name}: missing 'claudeTools'`);
|
|
73
|
+
if (agent.modelGuidance && typeof agent.modelGuidance !== "object") {
|
|
74
|
+
errors.push(`${agent.name}: modelGuidance must be an object`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (registry.modelGuidance) {
|
|
79
|
+
if (typeof registry.modelGuidance !== "object" || Array.isArray(registry.modelGuidance)) {
|
|
80
|
+
errors.push("Top-level modelGuidance must be an object");
|
|
81
|
+
} else {
|
|
82
|
+
for (const [key, val] of Object.entries(registry.modelGuidance)) {
|
|
83
|
+
if (typeof val !== "string") errors.push(`modelGuidance.${key}: value must be a string`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!registry.models || typeof registry.models !== "object") {
|
|
89
|
+
errors.push("Missing or invalid 'models' object");
|
|
90
|
+
} else {
|
|
91
|
+
for (const tier of ["reasoning", "standard", "fast"]) {
|
|
92
|
+
const t = registry.models[tier];
|
|
93
|
+
if (!t || typeof t !== "object") {
|
|
94
|
+
errors.push(`models.${tier}: missing tier object`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!t.primary || typeof t.primary !== "string") {
|
|
98
|
+
errors.push(`models.${tier}: primary model must be a non-empty string`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return errors;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const validationErrors = validateRegistry(registry);
|
|
107
|
+
if (validationErrors.length > 0) {
|
|
108
|
+
console.error("Registry validation failed:");
|
|
109
|
+
for (const err of validationErrors) console.error(` - ${err}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const systemName = registry.system;
|
|
114
|
+
const agentPrefix = `${registry.prefix}-`;
|
|
115
|
+
const sharedGuidance = registry.sharedGuidance ?? [];
|
|
116
|
+
const platformGuidance = registry.platformGuidance ?? {};
|
|
117
|
+
const globalModelGuidance = registry.modelGuidance ?? {};
|
|
118
|
+
|
|
119
|
+
const generatedHeader = `# Generated by ${systemName}/sync-agents.mjs. Edit agents/registry.json instead.`;
|
|
120
|
+
const generatedMarkdownNote = [
|
|
121
|
+
'<!--',
|
|
122
|
+
`Generated by construct sync from agents/registry.json.`,
|
|
123
|
+
'Do not edit this file directly — changes will be overwritten on next sync.',
|
|
124
|
+
'Regenerate: construct sync',
|
|
125
|
+
'-->',
|
|
126
|
+
'',
|
|
127
|
+
'> Generated from `agents/registry.json`. Edit the registry, then run `construct sync`.',
|
|
128
|
+
].join('\n');
|
|
129
|
+
|
|
130
|
+
const standardConstructTools = [
|
|
131
|
+
"list_skills",
|
|
132
|
+
"get_skill",
|
|
133
|
+
"search_skills",
|
|
134
|
+
"workflow_status",
|
|
135
|
+
"workflow_update_task",
|
|
136
|
+
"workflow_needs_main_input",
|
|
137
|
+
"memory_search",
|
|
138
|
+
"memory_add_observations",
|
|
139
|
+
"cx_trace",
|
|
140
|
+
"cx_score",
|
|
141
|
+
].join(",");
|
|
142
|
+
const managedStart = `# BEGIN ${systemName.toUpperCase()} AGENTS`;
|
|
143
|
+
const managedEnd = `# END ${systemName.toUpperCase()} AGENTS`;
|
|
144
|
+
const mdManagedStart = `<!-- BEGIN ${systemName.toUpperCase()} AGENTS -->`;
|
|
145
|
+
const mdManagedEnd = `<!-- END ${systemName.toUpperCase()} AGENTS -->`;
|
|
146
|
+
|
|
147
|
+
const registryModels = registry.models ?? {};
|
|
148
|
+
|
|
149
|
+
const envPrefix = registry.prefix.toUpperCase();
|
|
150
|
+
|
|
151
|
+
// Substitute __VAR_NAME__ placeholders with actual env vars.
|
|
152
|
+
// Falls back to the placeholder string if the env var is not set.
|
|
153
|
+
function resolveEnvBlock(envObj) {
|
|
154
|
+
if (!envObj) return undefined;
|
|
155
|
+
const result = {};
|
|
156
|
+
for (const [k, v] of Object.entries(envObj)) {
|
|
157
|
+
if (typeof v === "string") {
|
|
158
|
+
result[k] = v.replace(/__([A-Z0-9_]+)__/g, (_, name) => process.env[name] ?? v);
|
|
159
|
+
} else {
|
|
160
|
+
result[k] = v;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function resolveArgs(args) {
|
|
167
|
+
if (!Array.isArray(args)) return args;
|
|
168
|
+
return args.map((a) => (typeof a === "string"
|
|
169
|
+
? a.replace(/__([A-Z0-9_]+)__/g, (_, name) => process.env[name] ?? `__${name}__`)
|
|
170
|
+
: a));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function extractFallbackChain(tierDef) {
|
|
174
|
+
if (typeof tierDef === "string") return [tierDef];
|
|
175
|
+
if (tierDef && typeof tierDef === "object") {
|
|
176
|
+
const chain = [];
|
|
177
|
+
if (tierDef.primary) chain.push(tierDef.primary);
|
|
178
|
+
if (Array.isArray(tierDef.fallback)) chain.push(...tierDef.fallback);
|
|
179
|
+
return chain;
|
|
180
|
+
}
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const hardDefaults = {
|
|
185
|
+
reasoning: "openrouter/deepseek/deepseek-r1",
|
|
186
|
+
standard: "openrouter/qwen/qwen3-coder:free",
|
|
187
|
+
fast: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Primary model auto-detection: if the user picked a model in OpenCode config,
|
|
191
|
+
// derive tiered siblings from the same provider family so subagents share the
|
|
192
|
+
// primary's provider. Explicit CX_MODEL_* env wins if set.
|
|
193
|
+
const primaryFromOpenCode = (() => {
|
|
194
|
+
try {
|
|
195
|
+
const cfg = readOpenCodeConfig(findOpenCodeConfigPath()) ?? {};
|
|
196
|
+
return cfg.model || cfg.defaultModel || null;
|
|
197
|
+
} catch { return null; }
|
|
198
|
+
})();
|
|
199
|
+
const familyTiers = primaryFromOpenCode ? (resolveTiersForPrimary(primaryFromOpenCode) || {}) : {};
|
|
200
|
+
|
|
201
|
+
const resolvedModels = {
|
|
202
|
+
reasoning: process.env[`${envPrefix}_MODEL_REASONING`]
|
|
203
|
+
|| familyTiers.reasoning
|
|
204
|
+
|| extractFallbackChain(registryModels.reasoning)[0]
|
|
205
|
+
|| hardDefaults.reasoning,
|
|
206
|
+
standard: process.env[`${envPrefix}_MODEL_STANDARD`]
|
|
207
|
+
|| familyTiers.standard
|
|
208
|
+
|| extractFallbackChain(registryModels.standard)[0]
|
|
209
|
+
|| hardDefaults.standard,
|
|
210
|
+
fast: process.env[`${envPrefix}_MODEL_FAST`]
|
|
211
|
+
|| familyTiers.fast
|
|
212
|
+
|| extractFallbackChain(registryModels.fast)[0]
|
|
213
|
+
|| hardDefaults.fast,
|
|
214
|
+
};
|
|
215
|
+
if (primaryFromOpenCode && (familyTiers.reasoning || familyTiers.standard || familyTiers.fast)) {
|
|
216
|
+
console.log(`[sync] Tier models derived from primary '${primaryFromOpenCode}': reasoning=${resolvedModels.reasoning} standard=${resolvedModels.standard} fast=${resolvedModels.fast}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Full ordered fallback chains per tier (env override → registry chain → hard default)
|
|
220
|
+
const resolvedFallbackChains = {
|
|
221
|
+
reasoning: [
|
|
222
|
+
...(process.env[`${envPrefix}_MODEL_REASONING`] ? [process.env[`${envPrefix}_MODEL_REASONING`]] : []),
|
|
223
|
+
...extractFallbackChain(registryModels.reasoning),
|
|
224
|
+
hardDefaults.reasoning,
|
|
225
|
+
].filter((v, i, a) => v && a.indexOf(v) === i),
|
|
226
|
+
standard: [
|
|
227
|
+
...(process.env[`${envPrefix}_MODEL_STANDARD`] ? [process.env[`${envPrefix}_MODEL_STANDARD`]] : []),
|
|
228
|
+
...extractFallbackChain(registryModels.standard),
|
|
229
|
+
hardDefaults.standard,
|
|
230
|
+
].filter((v, i, a) => v && a.indexOf(v) === i),
|
|
231
|
+
fast: [
|
|
232
|
+
...(process.env[`${envPrefix}_MODEL_FAST`] ? [process.env[`${envPrefix}_MODEL_FAST`]] : []),
|
|
233
|
+
...extractFallbackChain(registryModels.fast),
|
|
234
|
+
hardDefaults.fast,
|
|
235
|
+
].filter((v, i, a) => v && a.indexOf(v) === i),
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
function resolveModel(entry) {
|
|
239
|
+
if (entry.model) return entry.model;
|
|
240
|
+
const tier = entry.modelTier && resolvedModels[entry.modelTier] ? entry.modelTier : "standard";
|
|
241
|
+
return resolvedModels[tier];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function resolveModelChain(entry) {
|
|
245
|
+
if (entry.model) return [entry.model];
|
|
246
|
+
const tier = entry.modelTier && resolvedFallbackChains[entry.modelTier] ? entry.modelTier : "standard";
|
|
247
|
+
return resolvedFallbackChains[tier];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function mkdirp(dir) { fs.mkdirSync(dir, { recursive: true }); }
|
|
251
|
+
function writeFile(file, content) { mkdirp(path.dirname(file)); fs.writeFileSync(file, content); }
|
|
252
|
+
|
|
253
|
+
function adapterName(entry) {
|
|
254
|
+
return entry.isPersona ? entry.name : `${agentPrefix}${entry.name}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function loadPersonaPrompt(persona) {
|
|
258
|
+
const promptPath = persona.promptFile ? path.join(root, persona.promptFile) : null;
|
|
259
|
+
const fallback = `You are ${persona.displayName}. ${persona.description}`;
|
|
260
|
+
const { prompt } = resolvePromptContract(persona, {
|
|
261
|
+
rootDir: root,
|
|
262
|
+
registry,
|
|
263
|
+
fallback,
|
|
264
|
+
});
|
|
265
|
+
if (!prompt) {
|
|
266
|
+
console.warn(`Warning: prompt file not found for persona ${persona.name}: ${promptPath}`);
|
|
267
|
+
return fallback;
|
|
268
|
+
}
|
|
269
|
+
return prompt;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function buildAgentRoster(allEntries) {
|
|
273
|
+
return allEntries.map((e) => `- ${adapterName(e)}: ${e.description}`).join("\n");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function buildModelGuidanceBlock(entry) {
|
|
277
|
+
const merged = { ...globalModelGuidance, ...(entry.modelGuidance ?? {}) };
|
|
278
|
+
const families = Object.keys(merged);
|
|
279
|
+
if (families.length === 0) return "";
|
|
280
|
+
const lines = families.map((family) => `- ${merged[family]}`).join("\n");
|
|
281
|
+
return `\n\nModel-specific guidance (apply only the section that matches your model family):\n${lines}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function buildRoleFooter(entry) {
|
|
285
|
+
const lines = [];
|
|
286
|
+
const collaborators = Array.isArray(entry.collaborators) ? entry.collaborators.filter(Boolean) : [];
|
|
287
|
+
if (collaborators.length > 0) {
|
|
288
|
+
lines.push(`Collaborators: ${collaborators.map((c) => (c.startsWith("cx-") ? c : `cx-${c}`)).join(", ")}.`);
|
|
289
|
+
}
|
|
290
|
+
if (entry.isPersona !== true && entry.canEdit === false) {
|
|
291
|
+
lines.push("Do not implement code or edit source files.");
|
|
292
|
+
}
|
|
293
|
+
if (entry.returnsStructured !== false) {
|
|
294
|
+
lines.push("Return exactly one terminal state per task: DONE (with evidence) | BLOCKED (with concrete blocker) | NEEDS_MAIN_INPUT (with question + safe default).");
|
|
295
|
+
}
|
|
296
|
+
if (lines.length === 0) return "";
|
|
297
|
+
return `\n\n${lines.join("\n")}`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function buildPrompt(entry, allEntries, platform) {
|
|
301
|
+
let prompt = resolvePromptContract(entry, {
|
|
302
|
+
rootDir: root,
|
|
303
|
+
registry,
|
|
304
|
+
fallback: entry.prompt || '',
|
|
305
|
+
}).prompt;
|
|
306
|
+
|
|
307
|
+
prompt = inlineRoleAntiPatterns(prompt, root, entry.name);
|
|
308
|
+
|
|
309
|
+
if (entry.injectAgentRoster && allEntries) {
|
|
310
|
+
const roster = buildAgentRoster(allEntries);
|
|
311
|
+
prompt = `Available specialist agents:\n${roster}\n\n${prompt}`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
prompt += buildRoleFooter(entry);
|
|
315
|
+
|
|
316
|
+
const platformItems = platformGuidance[platform] ?? [];
|
|
317
|
+
const allGuidance = [...sharedGuidance, ...platformItems];
|
|
318
|
+
if (allGuidance.length > 0) {
|
|
319
|
+
const guidance = allGuidance.map((item) => `- ${item}`).join("\n");
|
|
320
|
+
prompt = `${prompt}\n\nOperating guidance:\n${guidance}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
prompt += buildModelGuidanceBlock(entry);
|
|
324
|
+
|
|
325
|
+
const wordCount = prompt.split(/\s+/).filter(Boolean).length;
|
|
326
|
+
const effectiveCap = Number(entry.wordCapOverride) > 0 ? entry.wordCapOverride : PROMPT_WORD_CAP;
|
|
327
|
+
if (wordCount > effectiveCap) {
|
|
328
|
+
console.warn(`[sync] ${entry.name}: prompt is ${wordCount} words (cap ${effectiveCap})`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return prompt;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
|
|
335
|
+
|
|
336
|
+
function replaceManagedBlock(text, block, start = managedStart, end = managedEnd) {
|
|
337
|
+
const pattern = new RegExp(`\\n?${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}\\n?`, "m");
|
|
338
|
+
const normalizedBlock = `${start}\n${block.trimEnd()}\n${end}\n`;
|
|
339
|
+
if (pattern.test(text)) return text.replace(pattern, `\n${normalizedBlock}`);
|
|
340
|
+
return `${text.trimEnd()}\n\n${normalizedBlock}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const MANIFEST_FILE = ".construct-manifest";
|
|
344
|
+
|
|
345
|
+
function readManifest(dir) {
|
|
346
|
+
const p = path.join(dir, MANIFEST_FILE);
|
|
347
|
+
if (!fs.existsSync(p)) return new Set();
|
|
348
|
+
return new Set(fs.readFileSync(p, "utf8").split("\n").filter(Boolean));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function writeManifest(dir, files) {
|
|
352
|
+
fs.writeFileSync(path.join(dir, MANIFEST_FILE), [...files].sort().join("\n") + "\n");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function removeStaleAdapters(dir, ext, entries) {
|
|
356
|
+
if (!fs.existsSync(dir)) return;
|
|
357
|
+
|
|
358
|
+
const expected = new Set();
|
|
359
|
+
for (const e of entries) {
|
|
360
|
+
expected.add(`${adapterName(e)}${ext}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Read what construct wrote last time — delete anything no longer expected
|
|
364
|
+
const previouslyWritten = readManifest(dir);
|
|
365
|
+
for (const file of previouslyWritten) {
|
|
366
|
+
if (!expected.has(file) && fs.existsSync(path.join(dir, file))) {
|
|
367
|
+
fs.unlinkSync(path.join(dir, file));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
writeManifest(dir, expected);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// --- Unified entry list: personas + agents ---
|
|
375
|
+
|
|
376
|
+
function buildEntries() {
|
|
377
|
+
const entries = [];
|
|
378
|
+
|
|
379
|
+
for (const persona of registry.personas ?? []) {
|
|
380
|
+
const personaCanEdit = persona.permissions?.edit === "allow";
|
|
381
|
+
entries.push({
|
|
382
|
+
...persona,
|
|
383
|
+
isPersona: true,
|
|
384
|
+
prompt: loadPersonaPrompt(persona),
|
|
385
|
+
codexSandbox: persona.codexSandbox ?? (personaCanEdit ? "workspace-write" : "read-only"),
|
|
386
|
+
reasoningEffort: persona.reasoningEffort ?? "high",
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
for (const agent of registry.agents ?? []) {
|
|
391
|
+
entries.push({
|
|
392
|
+
...agent,
|
|
393
|
+
isPersona: false,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return entries;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// --- Claude Code adapter ---
|
|
401
|
+
|
|
402
|
+
function claudeAgentMarkdown(entry, allEntries) {
|
|
403
|
+
const name = adapterName(entry);
|
|
404
|
+
const baseTools = entry.claudeTools ?? "Read,Grep,Glob,LS";
|
|
405
|
+
// Merge base tools with standard construct tools, ensuring no duplicates
|
|
406
|
+
const toolSet = new Set([
|
|
407
|
+
...baseTools.split(",").map((t) => t.trim()),
|
|
408
|
+
...standardConstructTools.split(","),
|
|
409
|
+
]);
|
|
410
|
+
const tools = Array.from(toolSet).filter(Boolean).join(",");
|
|
411
|
+
|
|
412
|
+
return `---
|
|
413
|
+
name: ${name}
|
|
414
|
+
description: ${entry.description}
|
|
415
|
+
tools: ${tools}
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
${generatedMarkdownNote}
|
|
419
|
+
|
|
420
|
+
${buildPrompt(entry, allEntries, "claude")}
|
|
421
|
+
`;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function syncClaude(entries, targetDir = null) {
|
|
425
|
+
const claudeAgentsDir = targetDir
|
|
426
|
+
? path.join(targetDir, ".claude", "agents")
|
|
427
|
+
: path.join(home, ".claude", "agents");
|
|
428
|
+
mkdirp(claudeAgentsDir);
|
|
429
|
+
|
|
430
|
+
for (const entry of entries) {
|
|
431
|
+
const name = adapterName(entry);
|
|
432
|
+
const md = claudeAgentMarkdown(entry, entries);
|
|
433
|
+
writeFile(path.join(claudeAgentsDir, `${name}.md`), md);
|
|
434
|
+
}
|
|
435
|
+
removeStaleAdapters(claudeAgentsDir, ".md", entries);
|
|
436
|
+
|
|
437
|
+
if (!targetDir) {
|
|
438
|
+
const claudeMdPath = path.join(home, ".claude", "CLAUDE.md");
|
|
439
|
+
const existing = fs.existsSync(claudeMdPath) ? fs.readFileSync(claudeMdPath, "utf8") : "# Claude Global Instructions\n";
|
|
440
|
+
const personaList = entries.filter((e) => e.isPersona).map((e) => `- \`${adapterName(e)}\`: ${e.role} — ${e.description}`).join("\n");
|
|
441
|
+
// Only surface non-internal agents in CLAUDE.md — internal agents are dispatched by Construct, not invoked by users
|
|
442
|
+
const agentList = entries.filter((e) => !e.isPersona && !e.internal).map((e) => `- \`${adapterName(e)}\`: ${e.description}`).join("\n");
|
|
443
|
+
const note = `## ${systemName.charAt(0).toUpperCase() + systemName.slice(1)} Personas
|
|
444
|
+
|
|
445
|
+
${personaList}
|
|
446
|
+
|
|
447
|
+
## Internal Specialists
|
|
448
|
+
|
|
449
|
+
${agentList || "(all specialists are internal — routed through Construct)"}`;
|
|
450
|
+
writeFile(claudeMdPath, replaceManagedBlock(existing, note, mdManagedStart, mdManagedEnd));
|
|
451
|
+
|
|
452
|
+
// Sync MCP servers into ~/.claude/settings.json if it exists
|
|
453
|
+
const claudeSettingsPath = path.join(home, ".claude", "settings.json");
|
|
454
|
+
if (fs.existsSync(claudeSettingsPath)) {
|
|
455
|
+
const settings = JSON.parse(fs.readFileSync(claudeSettingsPath, "utf8"));
|
|
456
|
+
const templatePath = path.join(root, "platforms", "claude", "settings.template.json");
|
|
457
|
+
if (fs.existsSync(templatePath)) {
|
|
458
|
+
const template = JSON.parse(fs.readFileSync(templatePath, "utf8"));
|
|
459
|
+
if (template.hooks) {
|
|
460
|
+
// Resolve $HOME/.construct to the real path so hook commands survive
|
|
461
|
+
// symlink traversal inside Claude Code's hook runner environment.
|
|
462
|
+
const constructReal = (() => {
|
|
463
|
+
try { return fs.realpathSync(path.join(home, ".construct")); } catch { return path.join(home, ".construct"); }
|
|
464
|
+
})();
|
|
465
|
+
const hookStr = JSON.stringify(template.hooks)
|
|
466
|
+
.replace(/\$HOME\/\.construct/g, constructReal.replace(/\\/g, "/"));
|
|
467
|
+
settings.hooks = JSON.parse(hookStr);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (!settings.mcpServers) settings.mcpServers = {};
|
|
471
|
+
const registryMcp = registry.mcpServers ?? {};
|
|
472
|
+
for (const [id, mcpDef] of Object.entries(registryMcp)) {
|
|
473
|
+
const existing = JSON.stringify(settings.mcpServers[id] ?? "");
|
|
474
|
+
const hasPlaceholder = existing.includes("__");
|
|
475
|
+
if (settings.mcpServers[id] && !hasPlaceholder) continue;
|
|
476
|
+
settings.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
|
|
477
|
+
}
|
|
478
|
+
fs.writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// --- Codex adapter ---
|
|
484
|
+
|
|
485
|
+
function codexAgentToml(entry, allEntries) {
|
|
486
|
+
const name = adapterName(entry);
|
|
487
|
+
return `${generatedHeader}
|
|
488
|
+
name = ${tomlString(name)}
|
|
489
|
+
description = ${tomlString(entry.description)}
|
|
490
|
+
model = ${tomlString(resolveModel(entry))}
|
|
491
|
+
model_reasoning_effort = ${tomlString(entry.reasoningEffort ?? "medium")}
|
|
492
|
+
sandbox_mode = ${tomlString(entry.codexSandbox ?? "read-only")}
|
|
493
|
+
|
|
494
|
+
developer_instructions = ${tomlString(buildPrompt(entry, allEntries, "codex"))}
|
|
495
|
+
`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function removeCodexAgentTables(text, names) {
|
|
499
|
+
let next = text
|
|
500
|
+
.replace(/\n?# BEGIN GLOBAL AI AGENTS\n[\s\S]*?# END GLOBAL AI AGENTS\n?/m, "\n")
|
|
501
|
+
.replace(new RegExp(`\\n?${escapeRegExp(managedStart)}\\n[\\s\\S]*?${escapeRegExp(managedEnd)}\\n?`), "\n");
|
|
502
|
+
for (const name of names) {
|
|
503
|
+
const pattern = new RegExp(`\\n?\\[agents\\.${escapeRegExp(name)}\\]\\n[\\s\\S]*?(?=\\n\\[|\\n?${escapeRegExp(managedStart)}|(?![\\s\\S]))`);
|
|
504
|
+
next = next.replace(pattern, "\n");
|
|
505
|
+
}
|
|
506
|
+
return next.replace(/\n{3,}/g, "\n\n");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function hasCodexMcpTable(text, id) {
|
|
510
|
+
return new RegExp(`^\\[mcp_servers\\.(?:${escapeRegExp(id)}|${escapeRegExp(tomlString(id))})\\]`, "m").test(text);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function syncCodex(entries) {
|
|
514
|
+
const codexDir = path.join(home, ".codex");
|
|
515
|
+
const codexAgentsDir = path.join(codexDir, "agents");
|
|
516
|
+
mkdirp(codexAgentsDir);
|
|
517
|
+
|
|
518
|
+
for (const entry of entries) {
|
|
519
|
+
writeFile(path.join(codexAgentsDir, `${adapterName(entry)}.toml`), codexAgentToml(entry, entries));
|
|
520
|
+
}
|
|
521
|
+
removeStaleAdapters(codexAgentsDir, ".toml", entries);
|
|
522
|
+
|
|
523
|
+
const configPath = getCodexConfigPath(home);
|
|
524
|
+
const existing = removeDanglingConstructMcpMarkers(removeDanglingConstructMcpTimeouts(readCodexConfig(configPath)));
|
|
525
|
+
const entryNames = entries.map(adapterName);
|
|
526
|
+
const registryMcp = registry.mcpServers ?? {};
|
|
527
|
+
const mcpIds = Object.keys(registryMcp).filter((id) => hasCodexMcpTable(existing, id));
|
|
528
|
+
const withoutManagedTables = removeDanglingConstructMcpMarkers(removeTomlTables(
|
|
529
|
+
removeCodexAgentTables(existing, entryNames),
|
|
530
|
+
mcpIds.flatMap((id) => [`mcp_servers.${id}`, `mcp_servers.${tomlString(id)}`]),
|
|
531
|
+
));
|
|
532
|
+
const hasAgentsRoot = /^\[agents\]\s*$/m.test(withoutManagedTables);
|
|
533
|
+
const rootBlock = hasAgentsRoot ? "" : "[agents]\nmax_threads = 6\nmax_depth = 1\n\n";
|
|
534
|
+
// Only expose non-internal agents in Codex config; internal agents are dispatched by Construct
|
|
535
|
+
const blocks = entries.filter((e) => !e.internal).map((e) => `[agents.${adapterName(e)}]
|
|
536
|
+
description = ${tomlString(e.description)}
|
|
537
|
+
config_file = ${tomlString(`agents/${adapterName(e)}.toml`)}
|
|
538
|
+
`).join("\n");
|
|
539
|
+
|
|
540
|
+
const mcpBlock = mcpIds
|
|
541
|
+
.map((id) => serializeCodexMcpTable(id, buildCodexMcpEntry(id, registryMcp[id], process.env)))
|
|
542
|
+
.join("\n\n");
|
|
543
|
+
const withAgents = replaceManagedBlock(withoutManagedTables, `${rootBlock}${blocks}`);
|
|
544
|
+
writeCodexConfig(replaceManagedBlock(
|
|
545
|
+
withAgents,
|
|
546
|
+
mcpBlock,
|
|
547
|
+
`# BEGIN ${systemName.toUpperCase()} MCP SERVERS`,
|
|
548
|
+
`# END ${systemName.toUpperCase()} MCP SERVERS`,
|
|
549
|
+
), configPath);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// --- Copilot adapter ---
|
|
553
|
+
|
|
554
|
+
function copilotPrompt(entry, allEntries) {
|
|
555
|
+
const name = adapterName(entry);
|
|
556
|
+
return `---
|
|
557
|
+
mode: agent
|
|
558
|
+
description: ${entry.description}
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
${generatedMarkdownNote}
|
|
562
|
+
|
|
563
|
+
# ${name}
|
|
564
|
+
|
|
565
|
+
${buildPrompt(entry, allEntries, "copilot")}
|
|
566
|
+
|
|
567
|
+
When using this prompt, stay within the role above and adapt to the current repository instructions.
|
|
568
|
+
`;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function syncCopilot(entries) {
|
|
572
|
+
const promptsDir = path.join(home, ".github", "prompts");
|
|
573
|
+
mkdirp(promptsDir);
|
|
574
|
+
for (const entry of entries) {
|
|
575
|
+
writeFile(path.join(promptsDir, `${adapterName(entry)}.prompt.md`), copilotPrompt(entry, entries));
|
|
576
|
+
}
|
|
577
|
+
removeStaleAdapters(promptsDir, ".prompt.md", entries);
|
|
578
|
+
|
|
579
|
+
const instructionsPath = path.join(home, ".github", "copilot-instructions.md");
|
|
580
|
+
const existing = fs.existsSync(instructionsPath)
|
|
581
|
+
? fs.readFileSync(instructionsPath, "utf8")
|
|
582
|
+
: "# GitHub Copilot Instructions\n";
|
|
583
|
+
const list = entries.filter((e) => !e.internal).map((e) => `- \`${adapterName(e)}\`: use \`~/.github/prompts/${adapterName(e)}.prompt.md\`.`).join("\n");
|
|
584
|
+
const note = `## ${systemName.charAt(0).toUpperCase() + systemName.slice(1)} Agent Prompts
|
|
585
|
+
|
|
586
|
+
Copilot does not expose true spawnable subagents. Use these reusable prompt profiles for role-specific passes:
|
|
587
|
+
|
|
588
|
+
${list || "(all specialists are internal — use construct for all tasks)"}`;
|
|
589
|
+
writeFile(instructionsPath, replaceManagedBlock(existing, note, mdManagedStart, mdManagedEnd));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// --- VS Code adapter ---
|
|
593
|
+
|
|
594
|
+
function getVSCodeSettingsPaths() {
|
|
595
|
+
const platform = os.platform();
|
|
596
|
+
const candidates = [];
|
|
597
|
+
if (platform === "darwin") {
|
|
598
|
+
candidates.push(
|
|
599
|
+
path.join(home, "Library", "Application Support", "Code", "User", "settings.json"),
|
|
600
|
+
path.join(home, "Library", "Application Support", "Code - Insiders", "User", "settings.json"),
|
|
601
|
+
);
|
|
602
|
+
} else if (platform === "linux") {
|
|
603
|
+
candidates.push(
|
|
604
|
+
path.join(home, ".config", "Code", "User", "settings.json"),
|
|
605
|
+
path.join(home, ".config", "Code - Insiders", "User", "settings.json"),
|
|
606
|
+
);
|
|
607
|
+
} else if (platform === "win32") {
|
|
608
|
+
const appData = process.env.APPDATA ?? path.join(home, "AppData", "Roaming");
|
|
609
|
+
candidates.push(
|
|
610
|
+
path.join(appData, "Code", "User", "settings.json"),
|
|
611
|
+
path.join(appData, "Code - Insiders", "User", "settings.json"),
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
return candidates.filter(fs.existsSync);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function syncVSCode() {
|
|
618
|
+
const settingsPaths = getVSCodeSettingsPaths();
|
|
619
|
+
if (settingsPaths.length === 0) return false;
|
|
620
|
+
|
|
621
|
+
const registryMcp = registry.mcpServers ?? {};
|
|
622
|
+
if (Object.keys(registryMcp).length === 0) return false;
|
|
623
|
+
|
|
624
|
+
let synced = false;
|
|
625
|
+
for (const settingsPath of settingsPaths) {
|
|
626
|
+
try {
|
|
627
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
628
|
+
if (!settings["github.copilot.mcpServers"]) settings["github.copilot.mcpServers"] = {};
|
|
629
|
+
const mcpServers = settings["github.copilot.mcpServers"];
|
|
630
|
+
for (const [id, mcpDef] of Object.entries(registryMcp)) {
|
|
631
|
+
const existing = JSON.stringify(mcpServers[id] ?? "");
|
|
632
|
+
const hasPlaceholder = existing.includes("__");
|
|
633
|
+
if (mcpServers[id] && !hasPlaceholder) continue;
|
|
634
|
+
mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
|
|
635
|
+
}
|
|
636
|
+
settings["github.copilot.mcpServers"] = mcpServers;
|
|
637
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
638
|
+
synced = true;
|
|
639
|
+
} catch {
|
|
640
|
+
// Skip unreadable settings files
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return synced;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// --- Cursor adapter ---
|
|
647
|
+
|
|
648
|
+
function syncCursor() {
|
|
649
|
+
const cursorMcpPath = path.join(home, ".cursor", "mcp.json");
|
|
650
|
+
if (!fs.existsSync(cursorMcpPath)) return false;
|
|
651
|
+
|
|
652
|
+
const registryMcp = registry.mcpServers ?? {};
|
|
653
|
+
if (Object.keys(registryMcp).length === 0) return false;
|
|
654
|
+
|
|
655
|
+
try {
|
|
656
|
+
const config = JSON.parse(fs.readFileSync(cursorMcpPath, "utf8"));
|
|
657
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
658
|
+
for (const [id, mcpDef] of Object.entries(registryMcp)) {
|
|
659
|
+
const existing = JSON.stringify(config.mcpServers[id] ?? "");
|
|
660
|
+
const hasPlaceholder = existing.includes("__");
|
|
661
|
+
if (config.mcpServers[id] && !hasPlaceholder) continue;
|
|
662
|
+
config.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
|
|
663
|
+
}
|
|
664
|
+
fs.writeFileSync(cursorMcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
665
|
+
return true;
|
|
666
|
+
} catch {
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// --- OpenCode adapter ---
|
|
672
|
+
|
|
673
|
+
function opencodePermissions(entry) {
|
|
674
|
+
if (entry.permissions) {
|
|
675
|
+
return Object.fromEntries(
|
|
676
|
+
Object.entries(entry.permissions).map(([k, v]) => [k, v])
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
return { edit: "allow", bash: "allow" };
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function opencodeTaskPermissions(entry) {
|
|
683
|
+
if (entry.permissions?.task) return entry.permissions.task;
|
|
684
|
+
return {
|
|
685
|
+
"*": "allow",
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function syncOpencode(entries) {
|
|
690
|
+
const configPath = findOpenCodeConfigPath();
|
|
691
|
+
if (!fs.existsSync(configPath)) return false;
|
|
692
|
+
const pluginsDir = path.join(home, ".config", "opencode", "plugins");
|
|
693
|
+
const managedPluginPath = path.join(pluginsDir, "construct-fallback.js");
|
|
694
|
+
const toolkitPluginPath = path.join(root, "platforms", "opencode", "plugins", "construct-fallback.js");
|
|
695
|
+
|
|
696
|
+
const { config } = readOpenCodeConfig();
|
|
697
|
+
if (!config.agent) config.agent = {};
|
|
698
|
+
if (!Array.isArray(config.plugin)) config.plugin = [];
|
|
699
|
+
config.plugin = config.plugin.filter((entry) => {
|
|
700
|
+
if (typeof entry !== "string") return true;
|
|
701
|
+
return entry !== managedPluginPath && entry !== toolkitPluginPath;
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// Sync providers
|
|
705
|
+
const registryProviders = registry.providers ?? {};
|
|
706
|
+
if (Object.keys(registryProviders).length > 0) {
|
|
707
|
+
if (!config.provider) config.provider = {};
|
|
708
|
+
for (const [id, providerDef] of Object.entries(registryProviders)) {
|
|
709
|
+
const existing = config.provider[id] ?? {};
|
|
710
|
+
const existingAuth = existing.options?.headers?.Authorization;
|
|
711
|
+
const existingModels = existing.models ?? {};
|
|
712
|
+
config.provider[id] = {
|
|
713
|
+
...providerDef,
|
|
714
|
+
options: {
|
|
715
|
+
...providerDef.options,
|
|
716
|
+
headers: {
|
|
717
|
+
...providerDef.options?.headers,
|
|
718
|
+
...(existingAuth ? { Authorization: existingAuth } : {}),
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
models: Object.fromEntries(
|
|
722
|
+
Object.entries({ ...(providerDef.models ?? {}), ...existingModels })
|
|
723
|
+
.sort((a, b) => (a[1].name ?? a[0]).localeCompare(b[1].name ?? b[0]))
|
|
724
|
+
),
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Derive anthropic models from registry tier definitions
|
|
730
|
+
const tierModels = Object.values(registry.models ?? {}).flatMap((t) =>
|
|
731
|
+
[t.primary, ...(t.fallback ?? [])].filter((m) => m?.startsWith('anthropic/'))
|
|
732
|
+
);
|
|
733
|
+
if (tierModels.length > 0) {
|
|
734
|
+
if (!config.provider) config.provider = {};
|
|
735
|
+
if (!config.provider.anthropic) config.provider.anthropic = {};
|
|
736
|
+
const existing = config.provider.anthropic.models ?? {};
|
|
737
|
+
const derived = {};
|
|
738
|
+
for (const full of [...new Set(tierModels)]) {
|
|
739
|
+
const id = full.replace(/^anthropic\//, '');
|
|
740
|
+
if (!existing[id]) {
|
|
741
|
+
const parts = id.replace(/^claude-/, '').split('-');
|
|
742
|
+
const family = parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
|
|
743
|
+
const version = parts.slice(1).filter((p) => !/^\d{8,}$/.test(p)).join('.');
|
|
744
|
+
derived[id] = { name: `Claude ${family} ${version}`.trim() };
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
config.provider.anthropic.models = Object.fromEntries(
|
|
748
|
+
Object.entries({ ...derived, ...existing })
|
|
749
|
+
.sort((a, b) => (a[1].name ?? a[0]).localeCompare(b[1].name ?? b[0]))
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Sync MCP servers
|
|
754
|
+
const registryMcp = registry.mcpServers ?? {};
|
|
755
|
+
if (Object.keys(registryMcp).length > 0) {
|
|
756
|
+
if (!config.mcp) config.mcp = {};
|
|
757
|
+
for (const [id, mcpDef] of Object.entries(registryMcp)) {
|
|
758
|
+
const openCodeId = getOpenCodeMcpId(id);
|
|
759
|
+
if (openCodeId !== id) delete config.mcp[id];
|
|
760
|
+
const existing = JSON.stringify(config.mcp[openCodeId] ?? "");
|
|
761
|
+
const hasPlaceholder = existing.includes("__") && existing.includes("__");
|
|
762
|
+
const argsHaveTemplates = (mcpDef.args ?? []).some((a) => typeof a === 'string' && a.includes('__'));
|
|
763
|
+
if (!config.mcp[openCodeId] || hasPlaceholder || argsHaveTemplates) {
|
|
764
|
+
config.mcp[openCodeId] = buildOpenCodeMcpEntry(id, mcpDef, process.env).entry;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Remove stale agents that Construct manages.
|
|
770
|
+
const prefixes = [agentPrefix];
|
|
771
|
+
for (const key of Object.keys(config.agent)) {
|
|
772
|
+
const isManaged = prefixes.some((p) => key.startsWith(p));
|
|
773
|
+
const isPersona = registry.personas.some((p) => p.name === key);
|
|
774
|
+
if ((isManaged || isPersona) && !entries.find((e) => adapterName(e) === key)) {
|
|
775
|
+
delete config.agent[key];
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Write agents — no model/modelFallback set; agents inherit the global model
|
|
780
|
+
for (const entry of entries) {
|
|
781
|
+
const name = adapterName(entry);
|
|
782
|
+
const perms = opencodePermissions(entry);
|
|
783
|
+
config.agent[name] = {
|
|
784
|
+
description: entry.isPersona
|
|
785
|
+
? `${entry.role} — ${entry.description}`
|
|
786
|
+
: entry.description,
|
|
787
|
+
mode: entry.isPersona ? "all" : "subagent",
|
|
788
|
+
prompt: buildPrompt(entry, entries, "opencode"),
|
|
789
|
+
permission: {
|
|
790
|
+
...perms,
|
|
791
|
+
task: opencodeTaskPermissions(entry),
|
|
792
|
+
},
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
writeOpenCodeConfig(config, configPath);
|
|
797
|
+
|
|
798
|
+
const sourcePluginsDir = path.join(root, "platforms", "opencode", "plugins");
|
|
799
|
+
if (fs.existsSync(sourcePluginsDir)) {
|
|
800
|
+
mkdirp(pluginsDir);
|
|
801
|
+
for (const file of fs.readdirSync(sourcePluginsDir)) {
|
|
802
|
+
if (!file.endsWith(".js") && !file.endsWith(".ts")) continue;
|
|
803
|
+
const source = path.join(sourcePluginsDir, file);
|
|
804
|
+
const target = path.join(pluginsDir, file);
|
|
805
|
+
const content = fs.readFileSync(source, "utf8").replaceAll("__CX_TOOLKIT_DIR__", root);
|
|
806
|
+
fs.writeFileSync(target, content);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
config.plugin = [...config.plugin, managedPluginPath];
|
|
811
|
+
writeOpenCodeConfig(config, configPath);
|
|
812
|
+
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// --- Slash commands adapter ---
|
|
817
|
+
|
|
818
|
+
function syncCommands(targetDir = null) {
|
|
819
|
+
const sourceCommandsDir = path.join(root, "commands");
|
|
820
|
+
if (!fs.existsSync(sourceCommandsDir)) return 0;
|
|
821
|
+
|
|
822
|
+
const claudeCommandsDir = targetDir
|
|
823
|
+
? path.join(targetDir, ".claude", "commands")
|
|
824
|
+
: path.join(home, ".claude", "commands");
|
|
825
|
+
|
|
826
|
+
let count = 0;
|
|
827
|
+
for (const domain of fs.readdirSync(sourceCommandsDir, { withFileTypes: true })) {
|
|
828
|
+
if (!domain.isDirectory()) continue;
|
|
829
|
+
const domainDir = path.join(sourceCommandsDir, domain.name);
|
|
830
|
+
const targetDomainDir = path.join(claudeCommandsDir, domain.name);
|
|
831
|
+
mkdirp(targetDomainDir);
|
|
832
|
+
|
|
833
|
+
for (const file of fs.readdirSync(domainDir)) {
|
|
834
|
+
if (!file.endsWith(".md")) continue;
|
|
835
|
+
const source = path.join(domainDir, file);
|
|
836
|
+
const target = path.join(targetDomainDir, file);
|
|
837
|
+
fs.copyFileSync(source, target);
|
|
838
|
+
count++;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Clean up stale command files not in source
|
|
843
|
+
if (fs.existsSync(claudeCommandsDir)) {
|
|
844
|
+
for (const domain of fs.readdirSync(claudeCommandsDir, { withFileTypes: true })) {
|
|
845
|
+
if (!domain.isDirectory()) continue;
|
|
846
|
+
const sourceDomainDir = path.join(sourceCommandsDir, domain.name);
|
|
847
|
+
if (!fs.existsSync(sourceDomainDir)) {
|
|
848
|
+
fs.rmSync(path.join(claudeCommandsDir, domain.name), { recursive: true });
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
const sourceFiles = new Set(fs.readdirSync(sourceDomainDir).filter((f) => f.endsWith(".md")));
|
|
852
|
+
for (const file of fs.readdirSync(path.join(claudeCommandsDir, domain.name))) {
|
|
853
|
+
if (file.endsWith(".md") && !sourceFiles.has(file)) {
|
|
854
|
+
fs.unlinkSync(path.join(claudeCommandsDir, domain.name, file));
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
return count;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// --- Main ---
|
|
864
|
+
|
|
865
|
+
const projectDir = process.argv.includes("--project") ? process.cwd() : null;
|
|
866
|
+
const entries = buildEntries();
|
|
867
|
+
|
|
868
|
+
if (projectDir) {
|
|
869
|
+
syncClaude(entries, projectDir);
|
|
870
|
+
const cmdCount = syncCommands(projectDir);
|
|
871
|
+
console.log(`Synced ${entries.length} agents + ${cmdCount} commands to ${path.join(projectDir, ".claude/")} (project mode).`);
|
|
872
|
+
} else {
|
|
873
|
+
const personaCount = entries.filter((e) => e.isPersona).length;
|
|
874
|
+
const agentCount = entries.filter((e) => !e.isPersona).length;
|
|
875
|
+
|
|
876
|
+
syncCodex(entries);
|
|
877
|
+
syncClaude(entries);
|
|
878
|
+
syncCopilot(entries);
|
|
879
|
+
const opencodeOk = syncOpencode(entries);
|
|
880
|
+
const vscodeOk = syncVSCode();
|
|
881
|
+
const cursorOk = syncCursor();
|
|
882
|
+
const cmdCount = syncCommands();
|
|
883
|
+
|
|
884
|
+
const targets = [
|
|
885
|
+
"Codex",
|
|
886
|
+
"Claude Code",
|
|
887
|
+
"Copilot",
|
|
888
|
+
opencodeOk && "OpenCode",
|
|
889
|
+
vscodeOk && "VS Code",
|
|
890
|
+
cursorOk && "Cursor",
|
|
891
|
+
].filter(Boolean).join(", ");
|
|
892
|
+
console.log(`Synced ${personaCount} personas + ${agentCount} specialists + ${cmdCount} commands to ${targets}.`);
|
|
893
|
+
|
|
894
|
+
// Regenerate shell completions so new commands are immediately tab-completable
|
|
895
|
+
const completionsDir = generateCompletions();
|
|
896
|
+
if (completionsDir) {
|
|
897
|
+
console.log(`Completions updated → ${completionsDir}`);
|
|
898
|
+
}
|
|
899
|
+
}
|