nexakit 0.0.0-stage → 0.2.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/.claude-plugin/marketplace.json +28 -0
- package/LICENSE +26 -0
- package/README.md +79 -2
- package/agents/claude/docs-researcher.md +9 -0
- package/agents/claude/implementation-tutor.md +9 -0
- package/agents/claude/project-explorer.md +9 -0
- package/agents/claude/quality-reviewer.md +9 -0
- package/agents/claude/requirements-analyst.md +9 -0
- package/agents/claude/solution-architect.md +9 -0
- package/agents/codex/docs-researcher.toml +10 -0
- package/agents/codex/implementation-tutor.toml +10 -0
- package/agents/codex/project-explorer.toml +10 -0
- package/agents/codex/quality-reviewer.toml +10 -0
- package/agents/codex/requirements-analyst.toml +11 -0
- package/agents/codex/solution-architect.toml +10 -0
- package/assets/brand/README.md +38 -0
- package/assets/brand/agent-toolkit-mark.png +0 -0
- package/assets/brand/agent-toolkit-mark.svg +13 -0
- package/assets/brand/agent-toolkit-tui-hero.png +0 -0
- package/assets/brand/nexakit-readme-hero.png +0 -0
- package/assets/brand/nexakit-readme-hero.svg +21 -0
- package/assets/runtime/package-lock.json +1425 -0
- package/bin/agent-task.js +14 -0
- package/bin/agent-toolkit-gemini.js +13 -0
- package/bin/agent-toolkit-inbox.js +14 -0
- package/bin/agent-toolkit-mcp.js +17 -0
- package/bin/agent-toolkit.js +13 -0
- package/bin/nexakit.js +13 -0
- package/bin/project-memory-mcp.js +22 -0
- package/bin/project-memory.js +7 -0
- package/catalogs/skills/api-design-principles/LICENSE.txt +21 -0
- package/catalogs/skills/api-design-principles/SKILL.md +110 -0
- package/catalogs/skills/api-design-principles/assets/api-design-checklist.md +155 -0
- package/catalogs/skills/api-design-principles/assets/rest-api-template.py +185 -0
- package/catalogs/skills/api-design-principles/references/details.md +413 -0
- package/catalogs/skills/api-design-principles/references/graphql-schema-design.md +589 -0
- package/catalogs/skills/api-design-principles/references/rest-best-practices.md +408 -0
- package/catalogs/skills/nodejs-backend-patterns/LICENSE.txt +21 -0
- package/catalogs/skills/nodejs-backend-patterns/SKILL.md +45 -0
- package/catalogs/skills/nodejs-backend-patterns/references/advanced-patterns.md +430 -0
- package/catalogs/skills/nodejs-backend-patterns/references/details.md +599 -0
- package/catalogs/skills/supabase-postgres-best-practices/CHANGELOG.md +73 -0
- package/catalogs/skills/supabase-postgres-best-practices/LICENSE.txt +21 -0
- package/catalogs/skills/supabase-postgres-best-practices/SKILL.md +64 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/_contributing.md +170 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/_sections.md +39 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/_template.md +34 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/security-rls-performance.md +63 -0
- package/catalogs/skills/vercel-composition-patterns/AGENTS.md +946 -0
- package/catalogs/skills/vercel-composition-patterns/LICENSE.txt +27 -0
- package/catalogs/skills/vercel-composition-patterns/README.md +60 -0
- package/catalogs/skills/vercel-composition-patterns/SKILL.md +89 -0
- package/catalogs/skills/vercel-composition-patterns/metadata.json +11 -0
- package/catalogs/skills/vercel-composition-patterns/rules/_sections.md +29 -0
- package/catalogs/skills/vercel-composition-patterns/rules/_template.md +24 -0
- package/catalogs/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md +100 -0
- package/catalogs/skills/vercel-composition-patterns/rules/architecture-compound-components.md +112 -0
- package/catalogs/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md +87 -0
- package/catalogs/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md +100 -0
- package/catalogs/skills/vercel-composition-patterns/rules/react19-no-forwardref.md +42 -0
- package/catalogs/skills/vercel-composition-patterns/rules/state-context-interface.md +191 -0
- package/catalogs/skills/vercel-composition-patterns/rules/state-decouple-implementation.md +113 -0
- package/catalogs/skills/vercel-composition-patterns/rules/state-lift-state.md +125 -0
- package/catalogs/skills/vercel-react-best-practices/AGENTS.md +3810 -0
- package/catalogs/skills/vercel-react-best-practices/LICENSE.txt +27 -0
- package/catalogs/skills/vercel-react-best-practices/README.md +123 -0
- package/catalogs/skills/vercel-react-best-practices/SKILL.md +149 -0
- package/catalogs/skills/vercel-react-best-practices/metadata.json +15 -0
- package/catalogs/skills/vercel-react-best-practices/rules/_sections.md +46 -0
- package/catalogs/skills/vercel-react-best-practices/rules/_template.md +28 -0
- package/catalogs/skills/vercel-react-best-practices/rules/advanced-effect-event-deps.md +56 -0
- package/catalogs/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/catalogs/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
- package/catalogs/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
- package/catalogs/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/catalogs/skills/vercel-react-best-practices/rules/async-cheap-condition-before-await.md +37 -0
- package/catalogs/skills/vercel-react-best-practices/rules/async-defer-await.md +82 -0
- package/catalogs/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
- package/catalogs/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/catalogs/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/catalogs/skills/vercel-react-best-practices/rules/bundle-analyzable-paths.md +63 -0
- package/catalogs/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +60 -0
- package/catalogs/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/catalogs/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/catalogs/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/catalogs/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/catalogs/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/catalogs/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/catalogs/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/catalogs/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-flatmap-filter.md +60 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-request-idle-callback.md +105 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-resource-hints.md +85 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md +68 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md +82 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md +64 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md +59 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/catalogs/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/catalogs/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
- package/catalogs/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/catalogs/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
- package/catalogs/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
- package/catalogs/skills/vercel-react-best-practices/rules/server-hoist-static-io.md +149 -0
- package/catalogs/skills/vercel-react-best-practices/rules/server-no-shared-module-state.md +50 -0
- package/catalogs/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/catalogs/skills/vercel-react-best-practices/rules/server-parallel-nested-fetching.md +34 -0
- package/catalogs/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/catalogs/skills/webapp-testing/LICENSE.txt +202 -0
- package/catalogs/skills/webapp-testing/SKILL.md +96 -0
- package/catalogs/skills/webapp-testing/examples/console_logging.py +35 -0
- package/catalogs/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/catalogs/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/catalogs/skills/webapp-testing/scripts/with_server.py +106 -0
- package/catalogs/skills.json +1115 -0
- package/docs/skills/evidencias/2026-10-28-evaluacion-inicial-mcp-seguridad.md +118 -0
- package/docs/skills/evidencias/2026-10-28-incorporacion-mcp-seguridad.json +152 -0
- package/docs/skills/evidencias/2026-10-28-recomendador-preparacion.md +50 -0
- package/package.json +73 -4
- package/plugins/nexakit-modelo/.claude-plugin/plugin.json +1 -0
- package/plugins/nexakit-modelo/hooks/hooks.json +1 -0
- package/plugins/nexakit-modelo/hooks/register.js +48 -0
- package/plugins/nexakit-modelo/hooks/status.js +8 -0
- package/plugins/nexakit-modelo/tests/register.test.ts +21 -0
- package/plugins/nexakit-panel/.claude-plugin/plugin.json +1 -0
- package/plugins/nexakit-panel/README.md +20 -0
- package/plugins/nexakit-panel/hooks/hooks.json +1 -0
- package/plugins/nexakit-panel/hooks/lienzo.js +382 -0
- package/plugins/nexakit-panel/hooks/panel.js +279 -0
- package/plugins/nexakit-panel/hooks/register.tsx +248 -0
- package/plugins/nexakit-panel/tests/lienzo.test.ts +142 -0
- package/plugins/nexakit-panel/tests/panel.test.ts +240 -0
- package/plugins/nexakit-panel/tests/register.test.ts +167 -0
- package/plugins/nexakit-panel/types/index.d.ts +41 -0
- package/plugins/nexakit-sesion/.claude-plugin/plugin.json +1 -0
- package/plugins/nexakit-sesion/README.md +12 -0
- package/plugins/nexakit-sesion/hooks/aviso.js +38 -0
- package/plugins/nexakit-sesion/hooks/hooks.json +1 -0
- package/plugins/nexakit-sesion/hooks/register.js +28 -0
- package/plugins/nexakit-sesion/tests/register.test.ts +96 -0
- package/plugins/niveles/.claude-plugin/plugin.json +5 -0
- package/plugins/niveles/agents/nivel-alto.md +13 -0
- package/plugins/niveles/agents/nivel-exigente.md +13 -0
- package/plugins/niveles/agents/nivel-ligero.md +13 -0
- package/plugins/niveles/agents/nivel-normal.md +13 -0
- package/schemas/agent-client-adapter.schema.json +786 -0
- package/schemas/agent-client-auth-diagnostic.schema.json +801 -0
- package/schemas/agent-client-isolated-variant.schema.json +105 -0
- package/schemas/agent-client-isolated-verification.schema.json +99 -0
- package/schemas/agent-client-managed-preferences-invocation.schema.json +67 -0
- package/schemas/agent-client-managed-preferences.schema.json +63 -0
- package/schemas/agent-client-native-diagnostics.schema.json +85 -0
- package/schemas/agent-client-native-evaluation.schema.json +171 -0
- package/schemas/agent-client-native-preflight.schema.json +90 -0
- package/schemas/agent-client-output.schema.json +551 -0
- package/schemas/agent-client-runtime-fixture.schema.json +90 -0
- package/schemas/agent-client-runtime-local.schema.json +95 -0
- package/schemas/agent-client-runtime.schema.json +1288 -0
- package/schemas/agent-client-selective-config.schema.json +77 -0
- package/schemas/agent-client-supervised-local.schema.json +135 -0
- package/schemas/agent-client-supervised.schema.json +186 -0
- package/schemas/agent-model-preference.schema.json +573 -0
- package/schemas/agent-model-selection.schema.json +311 -0
- package/schemas/agent-task-approvals.schema.json +362 -0
- package/schemas/agent-task-prepare.schema.json +1324 -0
- package/schemas/agent-task-verify.schema.json +198 -0
- package/schemas/project-memory-update.schema.json +33 -0
- package/schemas/project-memory.schema.json +298 -0
- package/schemas/skill-catalog.schema.json +246 -0
- package/schemas/task-workflow-plan-output.schema.json +35 -0
- package/scripts/prepare-package.js +6 -0
- package/scripts/validate-skill-catalog.js +41 -0
- package/skills/cerrar-avisens/SKILL.md +12 -0
- package/skills/cerrar-memoria/SKILL.md +96 -0
- package/skills/cerrar-memoria/references/cierre.md +76 -0
- package/skills/decision-records/SKILL.md +27 -0
- package/skills/decision-records/agents/openai.yaml +7 -0
- package/skills/decision-records/references/adr-template.md +35 -0
- package/skills/differential-review/LICENSE.txt +427 -0
- package/skills/differential-review/SKILL.md +225 -0
- package/skills/differential-review/adversarial.md +203 -0
- package/skills/differential-review/agents/openai.yaml +6 -0
- package/skills/differential-review/assets/trail-of-bits-mark.svg +1 -0
- package/skills/differential-review/methodology.md +234 -0
- package/skills/differential-review/patterns.md +300 -0
- package/skills/differential-review/reporting.md +369 -0
- package/skills/elegir-modelo/SKILL.md +68 -0
- package/skills/elegir-modelo/modelos.mjs +108 -0
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/SKILL.md +71 -0
- package/skills/gemini/SKILL.md +52 -0
- package/skills/imagen/SKILL.md +12 -0
- package/skills/investigar/SKILL.md +11 -0
- package/skills/mcp-builder/LICENSE.txt +202 -0
- package/skills/mcp-builder/SKILL.md +236 -0
- package/skills/mcp-builder/reference/evaluation.md +602 -0
- package/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
- package/skills/mcp-builder/reference/node_mcp_server.md +970 -0
- package/skills/mcp-builder/reference/python_mcp_server.md +719 -0
- package/skills/mcp-builder/scripts/connections.py +151 -0
- package/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/skills/ordenar-proyecto/SKILL.md +56 -0
- package/skills/project-handoff/SKILL.md +53 -0
- package/skills/project-handoff/agents/openai.yaml +7 -0
- package/skills/project-handoff/references/handoff-template.md +35 -0
- package/skills/quality-gate/LICENSE.txt +26 -0
- package/skills/quality-gate/SKILL.md +44 -0
- package/skills/quality-gate/agents/openai.yaml +7 -0
- package/skills/quality-gate/references/review-checklist.md +42 -0
- package/skills/revisar/SKILL.md +11 -0
- package/skills/revisar-skill/LICENSE-find-skills.txt +26 -0
- package/skills/revisar-skill/SKILL.md +94 -0
- package/skills/revision-cruzada/SKILL.md +59 -0
- package/skills/web-design-guidelines/AGENTS.md +156 -0
- package/skills/web-design-guidelines/LICENSE.txt +21 -0
- package/skills/web-design-guidelines/SKILL.md +29 -0
- package/src/agent-models.js +131 -0
- package/src/brand.js +117 -0
- package/src/claude-code-runner.js +642 -0
- package/src/cli-executable.js +88 -0
- package/src/cli.js +778 -0
- package/src/client-adapter-plan.js +271 -0
- package/src/client-auth-diagnostic-parser.js +211 -0
- package/src/client-auth-diagnostic.js +257 -0
- package/src/client-isolated-variant.js +106 -0
- package/src/client-isolated-verification.js +130 -0
- package/src/client-local-platform.js +14 -0
- package/src/client-managed-preferences-invocation.js +209 -0
- package/src/client-managed-preferences.js +78 -0
- package/src/client-native-access.js +134 -0
- package/src/client-native-diagnostics.js +253 -0
- package/src/client-native-evaluation.js +106 -0
- package/src/client-native-preflight-files.js +68 -0
- package/src/client-native-preflight.js +95 -0
- package/src/client-output-json.js +118 -0
- package/src/client-output-parser.js +278 -0
- package/src/client-runtime-admission.js +80 -0
- package/src/client-runtime-coordinator.js +70 -0
- package/src/client-runtime-core.js +92 -0
- package/src/client-runtime-fixture-permit.js +20 -0
- package/src/client-runtime-fixture-protocol.js +39 -0
- package/src/client-runtime-state.js +67 -0
- package/src/client-runtime-store-files.js +91 -0
- package/src/client-runtime-store.js +239 -0
- package/src/client-runtime-supervisor.js +146 -0
- package/src/client-selective-config.js +195 -0
- package/src/client-selective-files.js +126 -0
- package/src/client-supervised-codes.js +32 -0
- package/src/client-supervised-contract.js +52 -0
- package/src/client-supervised-coordinator.js +36 -0
- package/src/client-supervised-permit.js +20 -0
- package/src/client-supervised-protocol.js +37 -0
- package/src/client-supervised-store.js +167 -0
- package/src/client-supervised-supervisor.js +167 -0
- package/src/client-supervised.js +160 -0
- package/src/codex-cli-runner.js +358 -0
- package/src/codex-delivery.js +101 -0
- package/src/connect.js +703 -0
- package/src/doctor.js +795 -0
- package/src/gemini-cli.js +76 -0
- package/src/gemini-files.js +45 -0
- package/src/gemini-tools.js +249 -0
- package/src/init.js +473 -0
- package/src/install-lock.js +127 -0
- package/src/install-registry.js +61 -0
- package/src/legacy-repair.js +71 -0
- package/src/level-agents.js +111 -0
- package/src/link-states.js +172 -0
- package/src/memory-connection-artifacts.js +192 -0
- package/src/memory-connection-descriptor.js +158 -0
- package/src/memory-connection-diagnostic.js +177 -0
- package/src/memory-connection-files.js +191 -0
- package/src/memory-connection-manifest.js +118 -0
- package/src/memory-connection-package.js +176 -0
- package/src/memory-connection-plan.js +211 -0
- package/src/memory-connection-skill.js +211 -0
- package/src/memory-connection-toml.js +153 -0
- package/src/memory-locator.js +118 -0
- package/src/memory-mcp-policy.js +181 -0
- package/src/memory-mcp-server.js +166 -0
- package/src/memory-mcp-transport.js +59 -0
- package/src/missing-dependencies.js +21 -0
- package/src/model-command.js +147 -0
- package/src/model-preference.js +235 -0
- package/src/model-selection.js +368 -0
- package/src/native/managed-preferences.c +100 -0
- package/src/native/managed-preferences.h +17 -0
- package/src/package-lock-path.js +13 -0
- package/src/panel-cli.js +182 -0
- package/src/peer-handoff.js +27 -0
- package/src/peer-inbox-cli.js +211 -0
- package/src/peer-review-mcp.js +210 -0
- package/src/peer-review-store.js +532 -0
- package/src/platform.js +24 -0
- package/src/project-root.js +41 -0
- package/src/project-signals.js +339 -0
- package/src/recommend-report.js +77 -0
- package/src/review-instructions.js +211 -0
- package/src/review-policy.js +35 -0
- package/src/revision-adapters.js +157 -0
- package/src/revision-context.js +86 -0
- package/src/revision-isolation.js +222 -0
- package/src/revision-process.js +103 -0
- package/src/revision-provenance.js +89 -0
- package/src/revision-snapshot.js +120 -0
- package/src/revision.js +242 -0
- package/src/runtime-fixtures/runtime-supervisor/child.cjs +3 -0
- package/src/runtime-fixtures/runtime-supervisor/fixture.cjs +49 -0
- package/src/runtime-fixtures/supervised/child.cjs +3 -0
- package/src/runtime-fixtures/supervised/fixture.cjs +47 -0
- package/src/sensitive-data.js +21 -0
- package/src/setup-apply.js +231 -0
- package/src/setup-plan.js +231 -0
- package/src/setup-state.js +277 -0
- package/src/setup-tui.js +555 -0
- package/src/skill-catalog-add.js +91 -0
- package/src/skill-catalog-audit.js +68 -0
- package/src/skill-catalog.js +170 -0
- package/src/skill-files.js +39 -0
- package/src/skill-recommendations.js +1673 -0
- package/src/skill-scan-report.js +14 -0
- package/src/skill-scan.js +178 -0
- package/src/stable-command.js +53 -0
- package/src/stable-install.js +222 -0
- package/src/status.js +200 -0
- package/src/superpowers.js +133 -0
- package/src/sync-apply.js +227 -0
- package/src/sync-files.js +109 -0
- package/src/sync-inspect.js +394 -0
- package/src/sync-ledger.js +94 -0
- package/src/sync-markdown.js +151 -0
- package/src/sync-proposal.js +84 -0
- package/src/sync-record.js +459 -0
- package/src/sync.js +360 -0
- package/src/task-approvals.js +367 -0
- package/src/task-cli.js +79 -0
- package/src/task-external-verifier.js +584 -0
- package/src/task-prepare-files.js +115 -0
- package/src/task-prepare.js +180 -0
- package/src/task-verify-cli.js +67 -0
- package/src/task-verify-files.js +188 -0
- package/src/task-verify.js +118 -0
- package/src/task-workflow-mcp.js +544 -0
- package/src/task-workflow-store.js +254 -0
- package/src/terminal-text.js +68 -0
- package/src/toolkit-cli.js +543 -0
- package/src/toolkit-commands-guide.js +107 -0
- package/src/toolkit-setup.js +614 -0
- package/src/usage-metrics.js +43 -0
- package/templates/agent-team/01-encargo.md +158 -0
- package/templates/agent-team/02-propuesta.md +132 -0
- package/templates/agent-team/03-revision.md +150 -0
- package/templates/agent-team/README.md +213 -0
- package/templates/model-inventory/01-inventario.md +154 -0
- package/templates/model-inventory/README.md +115 -0
- package/templates/project-memory/README.md +67 -0
- package/templates/project-memory/project-memory.example.json +53 -0
- package/templates/project-memory/skill/project-id/SKILL.md +37 -0
- package/templates/project-memory/skill/project-id/references/project-profile.md +44 -0
- package/templates/project-memory/vault/00-INICIO/Estado actual.md +44 -0
- package/templates/project-memory/vault/00-INICIO/Fuentes de verdad.md +30 -0
- package/templates/project-memory/vault/00-INICIO/Inicio del proyecto.md +44 -0
- package/templates/project-memory/vault/00-INICIO/Mantenimiento de la memoria.md +26 -0
- package/templates/project-memory/vault/00-INICIO/Mapa de la memoria.md +33 -0
- package/templates/project-memory/vault/00-INICIO/Registro de sincronizaci/303/263n.md" +28 -0
- package/templates/project-memory/vault/01-PRODUCTO/Producto.md +16 -0
- package/templates/project-memory/vault/02-ARQUITECTURA/Arquitectura.md +16 -0
- package/templates/project-memory/vault/03-IMPLEMENTACION/Implementaci/303/263n.md +15 -0
- package/templates/project-memory/vault/04-DECISIONES/Decisiones.md +19 -0
- package/templates/project-memory/vault/05-ENTREGAS/Entregas.md +12 -0
- package/templates/project-memory/vault/06-OPERACION/Operaci/303/263n.md +15 -0
- package/templates/project-memory/vault/07-AGENTES/Protocolo de agentes.md +33 -0
- package/templates/project-memory/vault/08-RECURSOS/Recursos.md +12 -0
- package/templates/project-memory/vault/09-ARCHIVO/README.md +5 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import { parseTOML, getStaticTOMLValue } from 'toml-eslint-parser';
|
|
8
|
+
import { IS_WINDOWS } from './platform.js';
|
|
9
|
+
import { acquireLock } from './install-lock.js';
|
|
10
|
+
import { isRegistered, readRegistry, recordEntries } from './install-registry.js';
|
|
11
|
+
import {
|
|
12
|
+
ACTIONABLE_LINK_STATES,
|
|
13
|
+
READY_LINK_STATES,
|
|
14
|
+
REMOVED_BY_USER_STATE,
|
|
15
|
+
chooseActiveRoot,
|
|
16
|
+
classifyLink,
|
|
17
|
+
classifyReference,
|
|
18
|
+
isEphemeralPath,
|
|
19
|
+
legacyGroups,
|
|
20
|
+
recognizedRoot,
|
|
21
|
+
relinkIfUnchanged,
|
|
22
|
+
} from './link-states.js';
|
|
23
|
+
import { actualRoot, ensureStableInstall, nexakitDir } from './stable-install.js';
|
|
24
|
+
import { inspectSuperpowers } from './superpowers.js';
|
|
25
|
+
|
|
26
|
+
import packageJson from '../package.json' with { type: 'json' };
|
|
27
|
+
|
|
28
|
+
// The package declares a simple major-version floor (>=24).
|
|
29
|
+
const minimumNodeMajor = Number(packageJson.engines.node.match(/^>=(\d+)$/u)[1]);
|
|
30
|
+
export const isNodeSupported = (version) =>
|
|
31
|
+
Number(version.replace(/^v/u, '').split('.')[0]) >= minimumNodeMajor;
|
|
32
|
+
|
|
33
|
+
export const TOOLKIT_ROOT = fileURLToPath(new URL('../', import.meta.url));
|
|
34
|
+
const SKILL_HOMES = {
|
|
35
|
+
codex: ['.agents', 'skills'],
|
|
36
|
+
claude: ['.claude', 'skills'],
|
|
37
|
+
antigravity: ['.gemini', 'antigravity-cli', 'skills'],
|
|
38
|
+
};
|
|
39
|
+
// Only Claude and Codex run the coordination MCP; Antigravity does not have one in this phase.
|
|
40
|
+
const MCP_CLIENTS = ['codex', 'claude'];
|
|
41
|
+
// Skills nexakit no longer ships. Links it created are removed; anything else with the name is reported, never
|
|
42
|
+
// touched. The last three are now covered by Superpowers (brainstorming/writing-plans, systematic-debugging and
|
|
43
|
+
// test-driven-development), which is required.
|
|
44
|
+
export const RETIRED_SKILLS = Object.freeze(['programar', 'estado', 'spec-driven-development', 'depurar', 'desarrollo-con-pruebas']);
|
|
45
|
+
const MCP_SUFFIX = 'bin/agent-toolkit-mcp.js';
|
|
46
|
+
const COMMANDS = { codex: 'codex', claude: 'claude', antigravity: 'agy' };
|
|
47
|
+
export const globalSkillTarget = (home, client, name) => path.join(home, ...SKILL_HOMES[client], name);
|
|
48
|
+
// Registry key of an MCP entry: it has no path of its own in the home folder.
|
|
49
|
+
export const mcpRegistryTarget = (client, name) => `mcp:${client}:${name}`;
|
|
50
|
+
|
|
51
|
+
// `all` means Claude and Codex, plus Antigravity when `agy` is installed or its folder already exists:
|
|
52
|
+
// nothing is created for someone who does not use it.
|
|
53
|
+
async function clientsFor(agent, environment, home) {
|
|
54
|
+
if (!['codex', 'claude', 'antigravity', 'all'].includes(agent))
|
|
55
|
+
throw new Error('El agente debe ser codex, claude, antigravity o all.');
|
|
56
|
+
if (agent !== 'all') return [agent];
|
|
57
|
+
const antigravity =
|
|
58
|
+
(await executable('agy', environment)) ||
|
|
59
|
+
Boolean(await stat(path.join(home, '.gemini', 'antigravity-cli')));
|
|
60
|
+
return antigravity ? ['codex', 'claude', 'antigravity'] : ['codex', 'claude'];
|
|
61
|
+
}
|
|
62
|
+
const quote = (value) => "'" + value.replaceAll("'", "'\\''") + "'";
|
|
63
|
+
const stat = (file) =>
|
|
64
|
+
fs.lstat(file).catch((e) => {
|
|
65
|
+
if (e.code === 'ENOENT') return null;
|
|
66
|
+
throw e;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Links point at the root in use, except when it is an npx cache: then they point at the stable copy.
|
|
70
|
+
export async function resolveRoots({ toolkitRoot, homeDirectory }) {
|
|
71
|
+
const sourceRoot = await fs.realpath(toolkitRoot);
|
|
72
|
+
const ephemeral = isEphemeralPath(sourceRoot);
|
|
73
|
+
return { sourceRoot, linkRoot: ephemeral ? actualRoot(homeDirectory) : sourceRoot, ephemeral };
|
|
74
|
+
}
|
|
75
|
+
// A node inside an npx cache disappears with it, so it is never the one registered, not even through a link.
|
|
76
|
+
const outsideNpxCache = async (file) =>
|
|
77
|
+
!isEphemeralPath(file) && !isEphemeralPath(await fs.realpath(file).catch(() => file));
|
|
78
|
+
export async function resolveNodeExecutable(environment) {
|
|
79
|
+
const stable = await findExecutable('node', environment, outsideNpxCache);
|
|
80
|
+
if (stable) return stable;
|
|
81
|
+
return (await outsideNpxCache(process.execPath)) ? process.execPath : 'node';
|
|
82
|
+
}
|
|
83
|
+
// Skills de catálogo para stacks concretos: el recomendador las instala por proyecto, así que no son base global.
|
|
84
|
+
// Los enlaces globales que ya existían no se tocan.
|
|
85
|
+
export const PROJECT_ONLY_SKILLS = Object.freeze(['frontend-design', 'web-design-guidelines', 'mcp-builder']);
|
|
86
|
+
async function baseSkillNames(root) {
|
|
87
|
+
const names = (await fs.readdir(path.join(root, 'skills'), { withFileTypes: true }))
|
|
88
|
+
.filter((e) => e.isDirectory() && /^[a-z0-9-]+$/u.test(e.name) && !PROJECT_ONLY_SKILLS.includes(e.name))
|
|
89
|
+
.map((e) => e.name)
|
|
90
|
+
.sort();
|
|
91
|
+
for (const name of names) {
|
|
92
|
+
const file = await stat(path.join(root, 'skills', name, 'SKILL.md'));
|
|
93
|
+
if (!file?.isFile() || file.isSymbolicLink()) throw new Error('Skill fuente inválida.');
|
|
94
|
+
}
|
|
95
|
+
return names;
|
|
96
|
+
}
|
|
97
|
+
async function catalogNames(root) {
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(await fs.readFile(path.join(root, 'catalogs/skills.json'), 'utf8')).skills.map(
|
|
100
|
+
(s) => s.name,
|
|
101
|
+
);
|
|
102
|
+
} catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export async function packageVersion(root) {
|
|
107
|
+
return JSON.parse(await fs.readFile(path.join(root, 'package.json'), 'utf8')).version;
|
|
108
|
+
}
|
|
109
|
+
async function parents(home, target, create = false) {
|
|
110
|
+
let current = home;
|
|
111
|
+
for (const part of path.relative(home, path.dirname(target)).split(path.sep)) {
|
|
112
|
+
current = path.join(current, part);
|
|
113
|
+
let info = await stat(current);
|
|
114
|
+
if (!info && create) {
|
|
115
|
+
await fs.mkdir(current, { mode: 0o700 }).catch((e) => {
|
|
116
|
+
if (e.code !== 'EEXIST') throw e;
|
|
117
|
+
});
|
|
118
|
+
info = await stat(current);
|
|
119
|
+
}
|
|
120
|
+
if (info && (!info.isDirectory() || info.isSymbolicLink()))
|
|
121
|
+
throw new Error('Un directorio de instalación está ocupado o es un enlace.');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export function registrationCommand(client, current, nodePath, entry) {
|
|
125
|
+
const add =
|
|
126
|
+
client === 'codex'
|
|
127
|
+
? `codex mcp add agent-toolkit-workflow -- ${quote(nodePath)} ${quote(entry)}`
|
|
128
|
+
: `claude mcp add --scope user --transport stdio agent-toolkit-workflow -- ${quote(nodePath)} ${quote(entry)}`;
|
|
129
|
+
if (current.state === 'falta') return add;
|
|
130
|
+
// Moving or repairing an existing entry: remove it first under the name it has now.
|
|
131
|
+
return `${client === 'codex' ? `codex mcp remove ${current.name}` : `claude mcp remove ${current.name} -s user`} && ${add}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Classifies every global skill link and the coordination MCP, and with `apply` creates what is missing and
|
|
135
|
+
// moves or repairs only what nexakit can prove is its own. Conflicts are skipped and reported, never replaced.
|
|
136
|
+
export async function setupToolkit({
|
|
137
|
+
agent = 'all',
|
|
138
|
+
apply = false,
|
|
139
|
+
homeDirectory = os.homedir(),
|
|
140
|
+
toolkitRoot = TOOLKIT_ROOT,
|
|
141
|
+
environment = process.env,
|
|
142
|
+
codexHome = environment.CODEX_HOME ?? path.join(homeDirectory, '.codex'),
|
|
143
|
+
codexProfile,
|
|
144
|
+
restore = [],
|
|
145
|
+
omit = [],
|
|
146
|
+
runNpm,
|
|
147
|
+
packageSpec,
|
|
148
|
+
lockOptions,
|
|
149
|
+
acquire = acquireLock,
|
|
150
|
+
} = {}) {
|
|
151
|
+
const home = await fs.realpath(homeDirectory);
|
|
152
|
+
const clients = await clientsFor(agent, environment, home);
|
|
153
|
+
const { sourceRoot, linkRoot, ephemeral } = await resolveRoots({ toolkitRoot, homeDirectory: home });
|
|
154
|
+
const names = await baseSkillNames(sourceRoot);
|
|
155
|
+
const registry = await readRegistry(nexakitDir(home));
|
|
156
|
+
const isOwned = (target, literal) => isRegistered(registry, target, literal);
|
|
157
|
+
const activeRoots = [linkRoot, sourceRoot, actualRoot(home)];
|
|
158
|
+
const retiredSkills = [];
|
|
159
|
+
for (const client of clients)
|
|
160
|
+
for (const name of RETIRED_SKILLS) {
|
|
161
|
+
const target = globalSkillTarget(home, client, name);
|
|
162
|
+
await parents(home, target);
|
|
163
|
+
const info = await stat(target);
|
|
164
|
+
if (!info) continue;
|
|
165
|
+
const literal = info.isSymbolicLink() ? await fs.readlink(target) : null;
|
|
166
|
+
// Registered, or pointing at `<nexakit copy>/skills/<name>`: links made before the registry existed
|
|
167
|
+
// are nexakit's too. A link into anything else is reported, never removed.
|
|
168
|
+
const owned = Boolean(
|
|
169
|
+
literal &&
|
|
170
|
+
(isOwned(target, literal) ||
|
|
171
|
+
(await recognizedRoot(path.resolve(path.dirname(target), literal), path.join('skills', name)))),
|
|
172
|
+
);
|
|
173
|
+
retiredSkills.push({ client, name, target, literal, owned });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const skills = [];
|
|
177
|
+
for (const client of clients)
|
|
178
|
+
for (const name of names) {
|
|
179
|
+
const target = globalSkillTarget(home, client, name);
|
|
180
|
+
await parents(home, target);
|
|
181
|
+
const { suffix, ...state } = await classifyLink({
|
|
182
|
+
target,
|
|
183
|
+
suffix: `skills/${name}`,
|
|
184
|
+
activeRoots,
|
|
185
|
+
isOwned,
|
|
186
|
+
});
|
|
187
|
+
skills.push({ client, name, ...state });
|
|
188
|
+
}
|
|
189
|
+
// Un enlace que falta pero que nexakit registró como suyo lo quitó el usuario: se respeta. Solo si esa IA aún
|
|
190
|
+
// conserva otros enlaces de nexakit; si se quedó sin ninguno (carpeta borrada), se reconstruye entera.
|
|
191
|
+
// `omit` declara lo mismo de forma explícita para enlaces antiguos que el registro no llegó a conocer; se anota
|
|
192
|
+
// en el registro al aplicar. `restore` gana sobre ambos.
|
|
193
|
+
const clientsWithLinks = new Set(skills.filter((s) => READY_LINK_STATES.includes(s.state)).map((s) => s.client));
|
|
194
|
+
const toRestore = new Set(restore);
|
|
195
|
+
const toOmit = new Set(omit);
|
|
196
|
+
const toRecord = new Map();
|
|
197
|
+
for (const item of skills) {
|
|
198
|
+
if (item.state !== 'falta' || toRestore.has(item.name)) continue;
|
|
199
|
+
if (registry.entradas.some((entry) => entry.destino === item.target)) {
|
|
200
|
+
if (clientsWithLinks.has(item.client)) item.state = REMOVED_BY_USER_STATE;
|
|
201
|
+
} else if (toOmit.has(item.name)) {
|
|
202
|
+
item.state = REMOVED_BY_USER_STATE;
|
|
203
|
+
toRecord.set(item.target, item);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Origins are never mixed: if most links already use another valid copy, that copy stays the root to link to.
|
|
208
|
+
const activeRoot = chooseActiveRoot(skills, linkRoot);
|
|
209
|
+
const usingOtherCopy = activeRoot !== linkRoot;
|
|
210
|
+
const missingInActive = [];
|
|
211
|
+
if (usingOtherCopy) {
|
|
212
|
+
for (const name of names)
|
|
213
|
+
if (!(await stat(path.join(activeRoot, 'skills', name, 'SKILL.md')))) missingInActive.push(name);
|
|
214
|
+
}
|
|
215
|
+
for (const item of skills) item.source = path.join(activeRoot, 'skills', item.name);
|
|
216
|
+
|
|
217
|
+
const mcpStates = {};
|
|
218
|
+
for (const client of clients.filter((c) => MCP_CLIENTS.includes(c))) {
|
|
219
|
+
mcpStates[client] = await inspectMcp(client, home, codexHome, { activeRoots, isOwned });
|
|
220
|
+
}
|
|
221
|
+
const mcpDeadRoot = Object.values(mcpStates).find((s) => s.state === 'roto-ajeno')?.root;
|
|
222
|
+
const legacy = legacyGroups(skills, {
|
|
223
|
+
skillNames: new Set([...names, ...(await catalogNames(sourceRoot))]),
|
|
224
|
+
mcpDeadRoot,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const pending = skills.filter(
|
|
228
|
+
(s) => ACTIONABLE_LINK_STATES.includes(s.state) && !missingInActive.includes(s.name),
|
|
229
|
+
);
|
|
230
|
+
const mcpPending = Object.keys(mcpStates).filter((client) =>
|
|
231
|
+
ACTIONABLE_LINK_STATES.includes(mcpStates[client].state),
|
|
232
|
+
);
|
|
233
|
+
let applied = false,
|
|
234
|
+
changed = 0,
|
|
235
|
+
created = 0,
|
|
236
|
+
stableInstall;
|
|
237
|
+
if (apply) {
|
|
238
|
+
const lock = await acquire(nexakitDir(home), lockOptions);
|
|
239
|
+
try {
|
|
240
|
+
if (ephemeral && !usingOtherCopy && (pending.length || mcpPending.length)) {
|
|
241
|
+
stableInstall = await ensureStableInstall({
|
|
242
|
+
homeDirectory: home,
|
|
243
|
+
version: await packageVersion(sourceRoot),
|
|
244
|
+
lock,
|
|
245
|
+
...(runNpm && { runNpm }),
|
|
246
|
+
...(packageSpec && { packageSpec }),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
for (const item of retiredSkills.filter((s) => s.owned)) {
|
|
250
|
+
await lock.assertHeld();
|
|
251
|
+
const current = await stat(item.target);
|
|
252
|
+
if (current?.isSymbolicLink() && (await fs.readlink(item.target)) === item.literal)
|
|
253
|
+
await fs.unlink(item.target);
|
|
254
|
+
}
|
|
255
|
+
const entries = [];
|
|
256
|
+
for (const item of pending) {
|
|
257
|
+
await parents(home, item.target, true);
|
|
258
|
+
await lock.assertHeld();
|
|
259
|
+
const outcome = await relinkIfUnchanged({
|
|
260
|
+
target: item.target,
|
|
261
|
+
expectedLiteral: item.state === 'falta' ? null : item.literal,
|
|
262
|
+
source: item.source,
|
|
263
|
+
});
|
|
264
|
+
if (outcome === 'cambio') {
|
|
265
|
+
changed++;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
// The registry keeps what readlink returns, the value later compared.
|
|
269
|
+
const literal = await fs.readlink(item.target);
|
|
270
|
+
entries.push({ destino: item.target, apunta: literal, raiz: activeRoot });
|
|
271
|
+
Object.assign(item, { state: 'instalado', literal, root: activeRoot });
|
|
272
|
+
}
|
|
273
|
+
created = entries.length;
|
|
274
|
+
for (const [destino, item] of toRecord)
|
|
275
|
+
entries.push({ destino, apunta: path.join(activeRoot, 'skills', item.name), raiz: activeRoot });
|
|
276
|
+
await recordEntries(nexakitDir(home), entries, lock);
|
|
277
|
+
applied = true;
|
|
278
|
+
} finally {
|
|
279
|
+
await lock.release();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// A command is only suggested once the script it registers exists (after the stable copy is in place).
|
|
284
|
+
const nodePath = await resolveNodeExecutable(environment);
|
|
285
|
+
const entry = path.join(activeRoot, MCP_SUFFIX);
|
|
286
|
+
const entryExists = Boolean(await stat(entry));
|
|
287
|
+
const registrationCommands = {},
|
|
288
|
+
registrationPending = [];
|
|
289
|
+
for (const client of mcpPending) {
|
|
290
|
+
if (entryExists)
|
|
291
|
+
registrationCommands[client] = registrationCommand(client, mcpStates[client], nodePath, entry);
|
|
292
|
+
else registrationPending.push(client);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const obsolete = Object.entries(mcpStates)
|
|
296
|
+
.filter(([, state]) => state.name)
|
|
297
|
+
.map(([client, state]) => ({
|
|
298
|
+
message: 'Ya no se usa; si quieres quitarlo:',
|
|
299
|
+
command:
|
|
300
|
+
client === 'codex' ? `codex mcp remove ${state.name}` : `claude mcp remove ${state.name} -s user`,
|
|
301
|
+
}));
|
|
302
|
+
const agentsPath = path.join(codexHome, 'agents');
|
|
303
|
+
for (const name of await fs.readdir(agentsPath).catch(() => [])) {
|
|
304
|
+
if (!/^nivel-(ligero|normal|exigente|alto)\.(toml|md)$/u.test(name)) continue;
|
|
305
|
+
const file = path.join(agentsPath, name);
|
|
306
|
+
obsolete.push({
|
|
307
|
+
message: 'Ya no se usa; si quieres quitarlo:',
|
|
308
|
+
command: IS_WINDOWS
|
|
309
|
+
? `PowerShell: Remove-Item -LiteralPath '${file.replaceAll("'", "''")}'`
|
|
310
|
+
: `rm -- ${quote(file)}`,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
applied,
|
|
315
|
+
skills,
|
|
316
|
+
retiredSkills,
|
|
317
|
+
obsolete,
|
|
318
|
+
conflicts:
|
|
319
|
+
skills.filter((s) => ['conflicto', 'roto-ajeno'].includes(s.state)).length +
|
|
320
|
+
retiredSkills.filter((s) => !s.owned).length,
|
|
321
|
+
changed,
|
|
322
|
+
clients,
|
|
323
|
+
linkRoot,
|
|
324
|
+
ephemeral,
|
|
325
|
+
activeRoot,
|
|
326
|
+
legacy,
|
|
327
|
+
...(usingOtherCopy && { otherCopy: { root: activeRoot, missing: missingInActive } }),
|
|
328
|
+
...(stableInstall && { stableInstall }),
|
|
329
|
+
mcp: {
|
|
330
|
+
applied: false,
|
|
331
|
+
states: mcpStates,
|
|
332
|
+
registrationCommands,
|
|
333
|
+
...(registrationPending.length && { registrationPending }),
|
|
334
|
+
},
|
|
335
|
+
restartRequired: created > 0,
|
|
336
|
+
...(clients.includes('codex') && { codexSandbox: await codexSandbox(home, codexHome, codexProfile) }),
|
|
337
|
+
// Required, installed through each agent's plugin manager; `doctor` and `npx nexakit` check it.
|
|
338
|
+
superpowers: { included: false, required: true },
|
|
339
|
+
codexHome,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
// Codex runs commands in a workspace-write sandbox by default, so `registrar` cannot write its reviewer record
|
|
343
|
+
// under ~/.agent-toolkit unless that folder is inside a writable root. Only reported; the user runs the printed
|
|
344
|
+
// command. What cannot be evaluated (permission profiles) is reported as unknown instead of asking for a change.
|
|
345
|
+
// Resolves links through the deepest folder that exists, so a root written through a link still matches.
|
|
346
|
+
async function canonical(target) {
|
|
347
|
+
let current = path.resolve(target),
|
|
348
|
+
rest = [];
|
|
349
|
+
while (true) {
|
|
350
|
+
try {
|
|
351
|
+
return path.join(await fs.realpath(current), ...rest);
|
|
352
|
+
} catch {
|
|
353
|
+
const parent = path.dirname(current);
|
|
354
|
+
if (parent === current) return path.resolve(target);
|
|
355
|
+
rest.unshift(path.basename(current));
|
|
356
|
+
current = parent;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const PROFILE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
|
|
361
|
+
const layer = (base, top) => {
|
|
362
|
+
const out = { ...base };
|
|
363
|
+
for (const [key, value] of Object.entries(top)) {
|
|
364
|
+
out[key] =
|
|
365
|
+
value &&
|
|
366
|
+
typeof value === 'object' &&
|
|
367
|
+
!Array.isArray(value) &&
|
|
368
|
+
out[key] &&
|
|
369
|
+
typeof out[key] === 'object' &&
|
|
370
|
+
!Array.isArray(out[key])
|
|
371
|
+
? layer(out[key], value)
|
|
372
|
+
: value;
|
|
373
|
+
}
|
|
374
|
+
return out;
|
|
375
|
+
};
|
|
376
|
+
const readToml = async (file) => {
|
|
377
|
+
const bytes = await boundedConfig(file);
|
|
378
|
+
return bytes ? getStaticTOMLValue(parseTOML(bytes.toString('utf8'), { tomlVersion: '1.0.0' })) : null;
|
|
379
|
+
};
|
|
380
|
+
async function codexSandbox(home, codexHome, codexProfile) {
|
|
381
|
+
const state = path.join(home, '.agent-toolkit'),
|
|
382
|
+
file = path.join(codexHome, 'config.toml');
|
|
383
|
+
// A base64 payload keeps paths and JavaScript out of shell interpolation in cmd, PowerShell and POSIX shells.
|
|
384
|
+
const script =
|
|
385
|
+
`const fs = require('node:fs'); const file = ${JSON.stringify(file)}; ` +
|
|
386
|
+
`let content = ''; try { content = fs.readFileSync(file, 'utf8'); } catch (error) { if (error.code !== 'ENOENT') throw error; } ` +
|
|
387
|
+
`if (/^\\[sandbox_workspace_write\\]/mu.test(content)) console.log('Ya existe la sección: añade la ruta a writable_roots a mano'); ` +
|
|
388
|
+
`else fs.appendFileSync(file, ${JSON.stringify(`\n[sandbox_workspace_write]\nwritable_roots = [${JSON.stringify(state)}]\n`)});`;
|
|
389
|
+
const command = `node -e "eval(Buffer.from('${Buffer.from(script).toString('base64')}','base64').toString())"`;
|
|
390
|
+
const unknown = (reason) => ({
|
|
391
|
+
state: 'unknown',
|
|
392
|
+
writableRoot: state,
|
|
393
|
+
hint: `${reason}: no se puede saber si Codex deja escribir en ${state}. Si $revisar no consigue registrarse, permite esa carpeta.`,
|
|
394
|
+
});
|
|
395
|
+
let config = {},
|
|
396
|
+
profileFiles = [];
|
|
397
|
+
try {
|
|
398
|
+
config = (await readToml(file)) ?? {};
|
|
399
|
+
// `codex --profile <name>` layers $CODEX_HOME/<name>.config.toml over config.toml at launch time.
|
|
400
|
+
profileFiles = (await fs.readdir(codexHome).catch(() => []))
|
|
401
|
+
.filter((n) => n.endsWith('.config.toml'))
|
|
402
|
+
.map((n) => n.slice(0, -12))
|
|
403
|
+
.filter((n) => PROFILE.test(n))
|
|
404
|
+
.sort();
|
|
405
|
+
} catch {
|
|
406
|
+
return { state: 'unreadable', writableRoot: state };
|
|
407
|
+
}
|
|
408
|
+
let effective = config;
|
|
409
|
+
if (codexProfile !== undefined) {
|
|
410
|
+
if (!PROFILE.test(codexProfile)) throw new Error('Nombre de perfil de Codex no válido.');
|
|
411
|
+
let overlay;
|
|
412
|
+
try {
|
|
413
|
+
overlay = await readToml(path.join(codexHome, `${codexProfile}.config.toml`));
|
|
414
|
+
} catch {
|
|
415
|
+
return { state: 'unreadable', writableRoot: state };
|
|
416
|
+
}
|
|
417
|
+
if (!overlay) return unknown(`No existe ${codexProfile}.config.toml en ${codexHome}`);
|
|
418
|
+
effective = layer(config, overlay);
|
|
419
|
+
} else if (config.profile !== undefined) {
|
|
420
|
+
// Older layout: a [profiles.<name>] table selected with `profile = "<name>"`.
|
|
421
|
+
const profile = typeof config.profile === 'string' ? config.profiles?.[config.profile] : undefined;
|
|
422
|
+
if (!profile || typeof profile !== 'object')
|
|
423
|
+
return unknown('El perfil de configuración elegido no se encuentra');
|
|
424
|
+
effective = layer(config, profile);
|
|
425
|
+
}
|
|
426
|
+
const result = (value) =>
|
|
427
|
+
codexProfile === undefined && profileFiles.length
|
|
428
|
+
? {
|
|
429
|
+
...value,
|
|
430
|
+
profileFiles,
|
|
431
|
+
profileHint: `Hay perfiles de Codex (${profileFiles.join(', ')}). Si abres Codex con --profile, revisa con --perfil-codex <nombre>.`,
|
|
432
|
+
}
|
|
433
|
+
: value;
|
|
434
|
+
return result(await sandboxState(effective, home, state, file, command, unknown));
|
|
435
|
+
}
|
|
436
|
+
async function sandboxState(effective, home, state, file, command, unknown) {
|
|
437
|
+
const mode = effective.sandbox_mode;
|
|
438
|
+
// When sandbox_mode is present, Codex uses the legacy options and ignores default_permissions.
|
|
439
|
+
if (mode === undefined && effective.default_permissions !== undefined) {
|
|
440
|
+
if (effective.default_permissions === ':danger-full-access')
|
|
441
|
+
return { state: 'not-needed', writableRoot: state };
|
|
442
|
+
if (effective.default_permissions === ':read-only')
|
|
443
|
+
return {
|
|
444
|
+
state: 'read-only',
|
|
445
|
+
writableRoot: state,
|
|
446
|
+
hint: `El perfil de permisos :read-only no deja escribir: $revisar no puede registrarse. Usa sandbox_mode = "workspace-write" con ${state} en writable_roots.`,
|
|
447
|
+
};
|
|
448
|
+
return unknown('Codex usa un perfil de permisos propio');
|
|
449
|
+
}
|
|
450
|
+
if (mode === undefined && effective.permissions !== undefined)
|
|
451
|
+
return unknown('Codex usa perfiles de permisos');
|
|
452
|
+
if (mode === 'danger-full-access') return { state: 'not-needed', writableRoot: state };
|
|
453
|
+
if (mode === 'read-only')
|
|
454
|
+
return {
|
|
455
|
+
state: 'read-only',
|
|
456
|
+
writableRoot: state,
|
|
457
|
+
hint: `Codex está en solo lectura: $revisar no puede registrarse. Usa sandbox_mode = "workspace-write" con ${state} en writable_roots.`,
|
|
458
|
+
};
|
|
459
|
+
if (mode !== undefined && mode !== 'workspace-write')
|
|
460
|
+
return unknown(`sandbox_mode = ${JSON.stringify(mode)} no es conocido`);
|
|
461
|
+
const roots = effective.sandbox_workspace_write?.writable_roots;
|
|
462
|
+
const target = await canonical(state);
|
|
463
|
+
for (const root of Array.isArray(roots) ? roots : []) {
|
|
464
|
+
if (typeof root !== 'string' || !root) continue;
|
|
465
|
+
const relative = path.relative(await canonical(root.replace(/^~(?=$|\/)/u, home)), target);
|
|
466
|
+
if (
|
|
467
|
+
relative === '' ||
|
|
468
|
+
(relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative))
|
|
469
|
+
) {
|
|
470
|
+
return { state: 'allowed', writableRoot: state };
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return effective.sandbox_workspace_write
|
|
474
|
+
? {
|
|
475
|
+
state: 'missing-root',
|
|
476
|
+
writableRoot: state,
|
|
477
|
+
hint: `Añade ${JSON.stringify(state)} a writable_roots de [sandbox_workspace_write] en ${file}.`,
|
|
478
|
+
}
|
|
479
|
+
: { state: 'missing', writableRoot: state, command };
|
|
480
|
+
}
|
|
481
|
+
async function boundedConfig(file) {
|
|
482
|
+
const info = await stat(file);
|
|
483
|
+
if (!info) return null;
|
|
484
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 1_048_576) throw new Error('config');
|
|
485
|
+
return fs.readFile(file);
|
|
486
|
+
}
|
|
487
|
+
// The entry is recognized by the script it runs (`…/bin/agent-toolkit-mcp.js` under `node`), then classified like
|
|
488
|
+
// a link: installed only if that script exists in a root in use.
|
|
489
|
+
async function inspectMcp(client, home, codexHome, { activeRoots, isOwned }) {
|
|
490
|
+
let servers;
|
|
491
|
+
try {
|
|
492
|
+
const bytes = await boundedConfig(
|
|
493
|
+
client === 'codex' ? path.join(codexHome, 'config.toml') : path.join(home, '.claude.json'),
|
|
494
|
+
);
|
|
495
|
+
if (!bytes) return { state: 'falta' };
|
|
496
|
+
servers =
|
|
497
|
+
client === 'codex'
|
|
498
|
+
? getStaticTOMLValue(parseTOML(bytes.toString('utf8'), { tomlVersion: '1.0.0' })).mcp_servers
|
|
499
|
+
: JSON.parse(bytes.toString('utf8')).mcpServers;
|
|
500
|
+
} catch {
|
|
501
|
+
return { state: 'unreadable' };
|
|
502
|
+
}
|
|
503
|
+
for (const name of ['agent-toolkit-workflow', 'agent-toolkit']) {
|
|
504
|
+
const server = servers?.[name];
|
|
505
|
+
if (!server) continue;
|
|
506
|
+
if (server.enabled === false) return { state: 'disabled', name };
|
|
507
|
+
const entry = Array.isArray(server.args)
|
|
508
|
+
? server.args.find(
|
|
509
|
+
(arg) => typeof arg === 'string' && arg.replaceAll('\\', '/').endsWith(`/${MCP_SUFFIX}`),
|
|
510
|
+
)
|
|
511
|
+
: undefined;
|
|
512
|
+
const runsNode =
|
|
513
|
+
typeof server.command === 'string' && /^node(\.exe)?$/iu.test(path.basename(server.command));
|
|
514
|
+
if (!entry || !runsNode) return { state: 'conflicto', name };
|
|
515
|
+
const { state, root } = await classifyReference({
|
|
516
|
+
target: mcpRegistryTarget(client, name),
|
|
517
|
+
literal: entry,
|
|
518
|
+
suffix: MCP_SUFFIX,
|
|
519
|
+
activeRoots,
|
|
520
|
+
isOwned,
|
|
521
|
+
});
|
|
522
|
+
return { state, name, literal: entry, ...(root && { root }) };
|
|
523
|
+
}
|
|
524
|
+
return { state: 'falta' };
|
|
525
|
+
}
|
|
526
|
+
async function findExecutable(name, environment, accept = () => true) {
|
|
527
|
+
const extensions = IS_WINDOWS
|
|
528
|
+
? [
|
|
529
|
+
'',
|
|
530
|
+
...(environment.PATHEXT || '.COM;.EXE;.BAT;.CMD')
|
|
531
|
+
.split(';')
|
|
532
|
+
.filter(Boolean)
|
|
533
|
+
.map((extension) => extension.toLowerCase()),
|
|
534
|
+
]
|
|
535
|
+
: [''];
|
|
536
|
+
for (const folder of (environment.PATH ?? '').split(path.delimiter).filter(Boolean)) {
|
|
537
|
+
for (const extension of extensions) {
|
|
538
|
+
try {
|
|
539
|
+
const file = path.join(folder, name + extension);
|
|
540
|
+
await fs.access(file, constants.X_OK);
|
|
541
|
+
if ((await fs.stat(file)).isFile() && (await accept(file))) return file;
|
|
542
|
+
} catch {}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
export async function executable(name, environment) {
|
|
548
|
+
return Boolean(await findExecutable(name, environment));
|
|
549
|
+
}
|
|
550
|
+
export async function doctorToolkit({
|
|
551
|
+
agent = 'all',
|
|
552
|
+
homeDirectory = os.homedir(),
|
|
553
|
+
toolkitRoot = TOOLKIT_ROOT,
|
|
554
|
+
environment = process.env,
|
|
555
|
+
codexHome = environment.CODEX_HOME ?? path.join(homeDirectory, '.codex'),
|
|
556
|
+
codexProfile,
|
|
557
|
+
runCommand,
|
|
558
|
+
} = {}) {
|
|
559
|
+
const setup = await setupToolkit({
|
|
560
|
+
agent,
|
|
561
|
+
homeDirectory,
|
|
562
|
+
toolkitRoot,
|
|
563
|
+
environment,
|
|
564
|
+
codexHome,
|
|
565
|
+
codexProfile,
|
|
566
|
+
});
|
|
567
|
+
const clients = {};
|
|
568
|
+
for (const name of setup.clients)
|
|
569
|
+
clients[name] = { available: await executable(COMMANDS[name], environment) };
|
|
570
|
+
const mcp = setup.mcp.states;
|
|
571
|
+
const dependencies = {};
|
|
572
|
+
const require = createRequire(path.join(toolkitRoot, 'package.json'));
|
|
573
|
+
for (const name of [
|
|
574
|
+
'@modelcontextprotocol/server',
|
|
575
|
+
'@modelcontextprotocol/sdk',
|
|
576
|
+
'zod',
|
|
577
|
+
'ajv',
|
|
578
|
+
'toml-eslint-parser',
|
|
579
|
+
]) {
|
|
580
|
+
try {
|
|
581
|
+
require.resolve(name === '@modelcontextprotocol/sdk' ? name + '/client/index.js' : name);
|
|
582
|
+
dependencies[name] = true;
|
|
583
|
+
} catch {
|
|
584
|
+
dependencies[name] = false;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
node: { version: process.version, supported: isNodeSupported(process.versions.node) },
|
|
589
|
+
dependencies,
|
|
590
|
+
clients,
|
|
591
|
+
mcp,
|
|
592
|
+
skills: setup.skills,
|
|
593
|
+
obsolete: setup.obsolete,
|
|
594
|
+
retiredSkills: setup.retiredSkills,
|
|
595
|
+
registrationCommands: setup.mcp.registrationCommands,
|
|
596
|
+
...(setup.mcp.registrationPending && { registrationPending: setup.mcp.registrationPending }),
|
|
597
|
+
activeRoot: setup.activeRoot,
|
|
598
|
+
legacy: setup.legacy,
|
|
599
|
+
...(setup.otherCopy && { otherCopy: setup.otherCopy }),
|
|
600
|
+
...(setup.codexSandbox && { codexSandbox: setup.codexSandbox }),
|
|
601
|
+
gemini: { available: await executable('agy', environment), provider: 'antigravity', apiFallback: false },
|
|
602
|
+
superpowers: {
|
|
603
|
+
included: false,
|
|
604
|
+
required: true,
|
|
605
|
+
detection: 'client-plugin-cli',
|
|
606
|
+
states: await inspectSuperpowers({
|
|
607
|
+
agentsFound: Object.fromEntries(Object.entries(clients).map(([name, c]) => [name, c.available])),
|
|
608
|
+
environment,
|
|
609
|
+
...(runCommand && { runCommand }),
|
|
610
|
+
}),
|
|
611
|
+
},
|
|
612
|
+
nativeConnectionVerified: false,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Measurements are declared observations, never authenticated billing evidence.
|
|
2
|
+
export const USAGE_COUNTERS = Object.freeze(["inputTokens", "cacheReadTokens", "outputTokens", "totalTokens", "durationMs", "costUsd"]);
|
|
3
|
+
const KEYS = new Set(["id", "agent", "phase", "model", "source", ...USAGE_COUNTERS]);
|
|
4
|
+
export function normalizeMeasurement(value) {
|
|
5
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some(k => !KEYS.has(k)) ||
|
|
6
|
+
typeof value.id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,99}$/u.test(value.id) ||
|
|
7
|
+
!["codex", "claude", "gemini"].includes(value.agent) ||
|
|
8
|
+
!["planning", "implementation", "review", "research", "image", "analysis", "other"].includes(value.phase) ||
|
|
9
|
+
typeof value.model !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,99}$/u.test(value.model) ||
|
|
10
|
+
!["client-reported", "manual"].includes(value.source)) throw new Error("Medición inválida.");
|
|
11
|
+
const result = Object.fromEntries(["id", "agent", "phase", "model", "source"].map(k => [k, value[k]]));
|
|
12
|
+
for (const key of USAGE_COUNTERS) {
|
|
13
|
+
const count = value[key] ?? null;
|
|
14
|
+
if (count !== null && (typeof count !== "number" || !Number.isFinite(count) || count < 0 || count > Number.MAX_SAFE_INTEGER ||
|
|
15
|
+
(key !== "costUsd" && !Number.isSafeInteger(count)))) throw new Error("Contador inválido.");
|
|
16
|
+
result[key] = count;
|
|
17
|
+
}
|
|
18
|
+
if (USAGE_COUNTERS.every(key => result[key] === null)) throw new Error("La medición no contiene contadores.");
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function summarizeMeasurements(records) {
|
|
23
|
+
const counters = {};
|
|
24
|
+
for (const key of USAGE_COUNTERS) {
|
|
25
|
+
const values = records.map(r => r[key]).filter(v => typeof v === "number" && Number.isFinite(v));
|
|
26
|
+
const sum = values.reduce((a, b) => a + b, 0);
|
|
27
|
+
counters[key] = { known: values.length, missing: records.length - values.length,
|
|
28
|
+
sum: values.length && sum <= Number.MAX_SAFE_INTEGER ? sum : null,
|
|
29
|
+
complete: records.length > 0 && values.length === records.length && sum <= Number.MAX_SAFE_INTEGER };
|
|
30
|
+
}
|
|
31
|
+
return { calls: records.length, counters };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function consumptionTable(report) {
|
|
35
|
+
const rows = report.tasks.map(task => {
|
|
36
|
+
const total = task.summary.counters.totalTokens;
|
|
37
|
+
return `| ${task.taskId} | ${task.state} | ${task.summary.calls} | ${total.sum ?? "desconocido"}${total.sum !== null && !total.complete ? " (parcial)" : ""} |`;
|
|
38
|
+
});
|
|
39
|
+
return ["Consumo declarado por colaboración; caché no se suma a entrada ni al total.",
|
|
40
|
+
"| Tarea | Estado | Llamadas medidas | Tokens totales conocidos |", "| --- | --- | --- | --- |", ...rows,
|
|
41
|
+
"", ...Object.entries(report.summary.counters).map(([name, c]) => `${name}: ${c.sum ?? "desconocido"} (${c.known}/${report.summary.calls} llamadas con dato)`),
|
|
42
|
+
"Cobertura: solo llamadas registradas; no incluye consumo oculto de sesiones. Ahorro no verificado.", ""].join("\n");
|
|
43
|
+
}
|