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,153 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { parseTOML } from "toml-eslint-parser";
|
|
3
|
+
|
|
4
|
+
import { copyBytes, copyInertJson, decode } from "./client-output-json.js";
|
|
5
|
+
import {
|
|
6
|
+
MEMORY_CONNECTION_FACTS, MEMORY_CONNECTION_VERSION, freezeMemoryConnectionData
|
|
7
|
+
} from "./memory-connection-descriptor.js";
|
|
8
|
+
import { containsSensitiveData } from "./sensitive-data.js";
|
|
9
|
+
|
|
10
|
+
export const MEMORY_CONNECTION_TOML_LIMITS = Object.freeze({
|
|
11
|
+
inputBytes: 1048576, astNodes: 30000, astDepth: 64,
|
|
12
|
+
stringBytes: 4096, args: 16, tools: 256, reportBytes: 65536
|
|
13
|
+
});
|
|
14
|
+
const FIELDS = new Set(["command", "args", "cwd", "enabled_tools", "enabled"]);
|
|
15
|
+
const CONTROL = /[\x00-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/u;
|
|
16
|
+
class Rejected extends Error {
|
|
17
|
+
constructor(code, state = "blocked") { super(code); this.code = code; this.state = state; }
|
|
18
|
+
}
|
|
19
|
+
const reject = (code, state) => { throw new Rejected(code, state); };
|
|
20
|
+
const keys = (key) => key.keys.map((part) => part.type === "TOMLBare" ? part.name : part.value);
|
|
21
|
+
|
|
22
|
+
// Traverse semantic nodes, not cyclic parent links, token text or comments.
|
|
23
|
+
// These bounds apply AFTER parsing; they do not sandbox the parser itself.
|
|
24
|
+
function boundAst(ast) {
|
|
25
|
+
const stack = [[ast, 0]]; let count = 0;
|
|
26
|
+
while (stack.length) {
|
|
27
|
+
const [node, depth] = stack.pop();
|
|
28
|
+
if (++count > MEMORY_CONNECTION_TOML_LIMITS.astNodes || depth > MEMORY_CONNECTION_TOML_LIMITS.astDepth) {
|
|
29
|
+
reject("MC_TOML_AST_LIMIT");
|
|
30
|
+
}
|
|
31
|
+
for (const [key, value] of Object.entries(node)) {
|
|
32
|
+
if (["parent", "tokens", "comments", "loc", "range"].includes(key)) continue;
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
for (const child of value) if (child && typeof child === "object" && typeof child.type === "string") {
|
|
35
|
+
stack.push([child, depth + 1]);
|
|
36
|
+
}
|
|
37
|
+
} else if (value && typeof value === "object" && typeof value.type === "string") {
|
|
38
|
+
stack.push([value, depth + 1]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function ownTable(ast, name) {
|
|
45
|
+
const matches = [];
|
|
46
|
+
for (const item of ast.body[0].body) {
|
|
47
|
+
if (item.type === "TOMLKeyValue") {
|
|
48
|
+
const path = keys(item.key);
|
|
49
|
+
if (path[0] !== "mcp_servers") continue;
|
|
50
|
+
// A sealed inline/scalar namespace cannot safely accept a future table.
|
|
51
|
+
if (path.length === 1) reject("MC_TOML_NAMESPACE");
|
|
52
|
+
if (path[1] === name) reject("MC_TOML_LAYOUT");
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (item.type !== "TOMLTable" || item.resolvedKey[0] !== "mcp_servers") continue;
|
|
56
|
+
const path = item.resolvedKey;
|
|
57
|
+
if (typeof path[1] === "number") reject("MC_TOML_NAMESPACE");
|
|
58
|
+
if (path.length === 1) {
|
|
59
|
+
if (item.body.some((field) => keys(field.key)[0] === name)) reject("MC_TOML_LAYOUT");
|
|
60
|
+
} else if (path[1] === name) {
|
|
61
|
+
if (path.length !== 2 || item.kind !== "standard") reject("MC_TOML_LAYOUT");
|
|
62
|
+
matches.push(item);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (matches.length > 1) reject("MC_TOML_LAYOUT");
|
|
66
|
+
return matches[0] ?? null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function textValue(node) {
|
|
70
|
+
if (node.type !== "TOMLValue" || node.kind !== "string" || typeof node.value !== "string") {
|
|
71
|
+
reject("MC_TOML_FIELD_TYPE");
|
|
72
|
+
}
|
|
73
|
+
const value = node.value;
|
|
74
|
+
if (Buffer.byteLength(value) > MEMORY_CONNECTION_TOML_LIMITS.stringBytes) reject("MC_TOML_STRING_LIMIT");
|
|
75
|
+
if (Buffer.from(value, "utf8").toString("utf8") !== value || CONTROL.test(value)) reject("MC_TOML_FIELD_VALUE");
|
|
76
|
+
if (containsSensitiveData(value)) reject("MC_TOML_SENSITIVE");
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function fieldValue(name, node) {
|
|
81
|
+
if (name === "enabled") {
|
|
82
|
+
if (node.type !== "TOMLValue" || node.kind !== "boolean" || typeof node.value !== "boolean") {
|
|
83
|
+
reject("MC_TOML_FIELD_TYPE");
|
|
84
|
+
}
|
|
85
|
+
return node.value;
|
|
86
|
+
}
|
|
87
|
+
if (name === "args" || name === "enabled_tools") {
|
|
88
|
+
if (node.type !== "TOMLArray") reject("MC_TOML_FIELD_TYPE");
|
|
89
|
+
if (node.elements.length > MEMORY_CONNECTION_TOML_LIMITS[name === "args" ? "args" : "tools"]) {
|
|
90
|
+
reject("MC_TOML_ARRAY_LIMIT");
|
|
91
|
+
}
|
|
92
|
+
return node.elements.map(textValue);
|
|
93
|
+
}
|
|
94
|
+
return textValue(node);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Inspect caller-provided TOML bytes for one explicit standard MCP table.
|
|
99
|
+
* No file discovery, reads, writes, client launch or public CLI integration.
|
|
100
|
+
* Hash/ranges attest these bytes only, never their origin or a live connection.
|
|
101
|
+
*/
|
|
102
|
+
export function readMemoryConnectionToml(input, options) {
|
|
103
|
+
let mcpName = null, configurationSha256 = null, byteSyntaxVerified = false;
|
|
104
|
+
const report = (state, server = null, ranges = null, reasons = []) => {
|
|
105
|
+
const result = {
|
|
106
|
+
schemaVersion: MEMORY_CONNECTION_VERSION, state, mcpName, configurationSha256,
|
|
107
|
+
server, ranges, reasons,
|
|
108
|
+
facts: { ...MEMORY_CONNECTION_FACTS, evidence: "provided-bytes", byteSyntaxVerified,
|
|
109
|
+
configurationSourceVerified: false }
|
|
110
|
+
};
|
|
111
|
+
if (Buffer.byteLength(JSON.stringify(result)) > MEMORY_CONNECTION_TOML_LIMITS.reportBytes) {
|
|
112
|
+
reject("MC_TOML_REPORT_LIMIT");
|
|
113
|
+
}
|
|
114
|
+
return freezeMemoryConnectionData(result);
|
|
115
|
+
};
|
|
116
|
+
try {
|
|
117
|
+
let opts;
|
|
118
|
+
try { opts = copyInertJson(options); } catch { reject("MC_TOML_OPTIONS", "invalid"); }
|
|
119
|
+
if (!opts || Array.isArray(opts) || Object.keys(opts).length !== 1 || !Object.hasOwn(opts, "mcpName") ||
|
|
120
|
+
typeof opts.mcpName !== "string" || !/^[a-z][a-z0-9-]{0,63}$/.test(opts.mcpName) ||
|
|
121
|
+
["constructor", "prototype", "__proto__"].includes(opts.mcpName)) reject("MC_TOML_OPTIONS", "invalid");
|
|
122
|
+
mcpName = opts.mcpName;
|
|
123
|
+
let bytes;
|
|
124
|
+
try { bytes = copyBytes(input, MEMORY_CONNECTION_TOML_LIMITS.inputBytes); }
|
|
125
|
+
catch (error) {
|
|
126
|
+
reject(error?.code === "byte-limit" ? "MC_TOML_INPUT_LIMIT" : "MC_TOML_BYTES", "invalid");
|
|
127
|
+
}
|
|
128
|
+
configurationSha256 = createHash("sha256").update(bytes).digest("hex");
|
|
129
|
+
let source;
|
|
130
|
+
try { source = decode(bytes); } catch { reject("MC_TOML_UTF8", "invalid"); }
|
|
131
|
+
if (source.startsWith("\ufeff")) reject("MC_TOML_BOM", "invalid");
|
|
132
|
+
let ast;
|
|
133
|
+
try { ast = parseTOML(source, { tomlVersion: "1.0.0" }); }
|
|
134
|
+
catch { reject("MC_TOML_SYNTAX", "invalid"); }
|
|
135
|
+
byteSyntaxVerified = true;
|
|
136
|
+
boundAst(ast);
|
|
137
|
+
const table = ownTable(ast, mcpName);
|
|
138
|
+
if (!table) return report("absent");
|
|
139
|
+
const server = {}, fields = {};
|
|
140
|
+
for (const field of table.body) {
|
|
141
|
+
const path = keys(field.key);
|
|
142
|
+
if (path.length !== 1 || !FIELDS.has(path[0])) reject("MC_TOML_FIELD");
|
|
143
|
+
const name = path[0];
|
|
144
|
+
server[name] = fieldValue(name, field.value);
|
|
145
|
+
fields[name] = { key: [...field.key.range], value: [...field.value.range] };
|
|
146
|
+
}
|
|
147
|
+
return report("observed", server, { table: [...table.range], fields });
|
|
148
|
+
} catch (error) {
|
|
149
|
+
// Never return parser diagnostics, arbitrary exception messages or source.
|
|
150
|
+
return report(error instanceof Rejected ? error.state : "invalid", null, null,
|
|
151
|
+
[error instanceof Rejected ? error.code : "MC_TOML_INVALID"]);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Finds the project memories this person already has, from the memory MCP servers registered in Claude Code, Codex
|
|
2
|
+
// and Antigravity, so a repository reuses its memory and a new one goes next to the others. Read-only. No new
|
|
3
|
+
// registry: the clients' own configuration is the source of truth.
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { getStaticTOMLValue, parseTOML } from 'toml-eslint-parser';
|
|
7
|
+
|
|
8
|
+
const MAX_CONFIG_BYTES = 2 * 1024 * 1024;
|
|
9
|
+
const MAX_MANIFEST_BYTES = 512 * 1024;
|
|
10
|
+
const CLIENT_ORDER = ['claude', 'codex', 'antigravity'];
|
|
11
|
+
const FILESYSTEM_PACKAGE = /^@modelcontextprotocol\/server-filesystem(?:@[^/]+)?$/u;
|
|
12
|
+
const MEMORY_SCRIPT = 'project-memory-mcp.js';
|
|
13
|
+
|
|
14
|
+
async function readBounded(file, maxBytes) {
|
|
15
|
+
try {
|
|
16
|
+
const info = await fs.lstat(file);
|
|
17
|
+
if (!info.isFile() || info.size > maxBytes) return null;
|
|
18
|
+
return await fs.readFile(file, 'utf8');
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function serversOf(client, { homeDirectory, codexHome }) {
|
|
25
|
+
try {
|
|
26
|
+
if (client === 'claude') {
|
|
27
|
+
const config = JSON.parse(await readBounded(path.join(homeDirectory, '.claude.json'), MAX_CONFIG_BYTES) ?? 'null');
|
|
28
|
+
const projects = Object.values(config?.projects ?? {}).map(project => project?.mcpServers ?? {});
|
|
29
|
+
return [config?.mcpServers ?? {}, ...projects].flatMap(Object.entries);
|
|
30
|
+
}
|
|
31
|
+
if (client === 'codex') {
|
|
32
|
+
const text = await readBounded(path.join(codexHome, 'config.toml'), MAX_CONFIG_BYTES);
|
|
33
|
+
return text ? Object.entries(getStaticTOMLValue(parseTOML(text, { tomlVersion: '1.0.0' }))?.mcp_servers ?? {}) : [];
|
|
34
|
+
}
|
|
35
|
+
const config = JSON.parse(await readBounded(path.join(homeDirectory, '.gemini', 'config', 'mcp_config.json'), MAX_CONFIG_BYTES) ?? 'null');
|
|
36
|
+
return Object.entries(config?.mcpServers ?? {});
|
|
37
|
+
} catch {
|
|
38
|
+
return []; // An unreadable configuration means no known memories from that client, never an error.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The manifest a server points at: the last argument of server-filesystem, or the --manifest of project-memory-mcp.
|
|
43
|
+
function manifestCandidate(server) {
|
|
44
|
+
const args = Array.isArray(server?.args) ? server.args.filter(arg => typeof arg === 'string') : [];
|
|
45
|
+
const script = args.findIndex(arg => arg.replaceAll('\\', '/').endsWith(`/${MEMORY_SCRIPT}`));
|
|
46
|
+
if (script >= 0 && args[script + 1] === '--manifest' && path.isAbsolute(args[script + 2] ?? '')) return args[script + 2];
|
|
47
|
+
if (args.some(arg => FILESYSTEM_PACKAGE.test(arg))) {
|
|
48
|
+
const last = args.at(-1);
|
|
49
|
+
if (last && path.isAbsolute(last)) return path.join(last, 'project-memory.json');
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function readManifest(manifestPath) {
|
|
55
|
+
const text = await readBounded(manifestPath, MAX_MANIFEST_BYTES); // lstat: a linked manifest is not accepted.
|
|
56
|
+
if (text === null) return null;
|
|
57
|
+
try {
|
|
58
|
+
const manifest = JSON.parse(text);
|
|
59
|
+
const id = manifest?.project?.id;
|
|
60
|
+
const repositoryPath = manifest?.source?.repositoryPath;
|
|
61
|
+
if (typeof id !== 'string' || typeof repositoryPath !== 'string') return null;
|
|
62
|
+
return manifest;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Every MCP server name registered in each agent, with the vault it serves when it is a memory (else null).
|
|
69
|
+
export async function registeredServers({ homeDirectory, codexHome = path.join(homeDirectory, '.codex') } = {}) {
|
|
70
|
+
const result = {};
|
|
71
|
+
for (const client of CLIENT_ORDER) {
|
|
72
|
+
result[client] = {};
|
|
73
|
+
for (const [name, server] of await serversOf(client, { homeDirectory, codexHome })) {
|
|
74
|
+
const candidate = manifestCandidate(server);
|
|
75
|
+
const vault = candidate ? await fs.realpath(path.dirname(candidate)).catch(() => null) : null;
|
|
76
|
+
const kind = !candidate ? null : (server.args ?? []).some(arg => typeof arg === 'string' && arg.replaceAll('\\', '/').endsWith(`/${MEMORY_SCRIPT}`)) ? 'guard' : 'filesystem';
|
|
77
|
+
result[client][name] = vault && { vault, kind };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function findKnownMemories({ homeDirectory, codexHome = path.join(homeDirectory, '.codex') } = {}) {
|
|
84
|
+
const byVault = new Map();
|
|
85
|
+
for (const client of CLIENT_ORDER) {
|
|
86
|
+
for (const [name, server] of await serversOf(client, { homeDirectory, codexHome })) {
|
|
87
|
+
const candidate = manifestCandidate(server);
|
|
88
|
+
if (!candidate) continue;
|
|
89
|
+
const manifest = await readManifest(candidate);
|
|
90
|
+
if (!manifest) continue;
|
|
91
|
+
const vaultPath = await fs.realpath(path.dirname(candidate)).catch(() => null);
|
|
92
|
+
if (!vaultPath) continue;
|
|
93
|
+
const entry = byVault.get(vaultPath) ?? {
|
|
94
|
+
vaultPath,
|
|
95
|
+
manifestPath: path.join(vaultPath, 'project-memory.json'),
|
|
96
|
+
projectId: manifest.project.id,
|
|
97
|
+
repositoryPath: manifest.source.repositoryPath,
|
|
98
|
+
mcpName: typeof manifest.memory?.mcpServerName === 'string' ? manifest.memory.mcpServerName : name,
|
|
99
|
+
clients: []
|
|
100
|
+
};
|
|
101
|
+
if (!entry.clients.includes(client)) entry.clients.push(client);
|
|
102
|
+
byVault.set(vaultPath, entry);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return [...byVault.values()].sort((a, b) => (a.vaultPath < b.vaultPath ? -1 : a.vaultPath > b.vaultPath ? 1 : 0));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function memoryForRepository(known, root) {
|
|
109
|
+
return known.find(entry => path.resolve(entry.repositoryPath) === path.resolve(root)) ?? null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Next to the memories already known when they share one parent folder; otherwise a folder of its own.
|
|
113
|
+
export function suggestMemoryLocation(known, { id, homeDirectory }) {
|
|
114
|
+
const parents = [...new Set(known.map(entry => path.dirname(entry.vaultPath)))];
|
|
115
|
+
return parents.length === 1
|
|
116
|
+
? path.join(parents[0], `${id.toUpperCase()}-MEMORY`)
|
|
117
|
+
: path.join(homeDirectory, 'nexakit-memorias', id);
|
|
118
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { IS_WINDOWS, warnPlatformPermissions, toGitPath } from './platform.js';
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
8
|
+
import { parseTaskDocument } from "./task-prepare-files.js";
|
|
9
|
+
import { containsSensitiveData, isCredentialPath } from "./sensitive-data.js";
|
|
10
|
+
|
|
11
|
+
export const MEMORY_READ_TOOLS = Object.freeze([
|
|
12
|
+
"read_text_file", "read_media_file", "read_multiple_files", "list_directory",
|
|
13
|
+
"list_directory_with_sizes", "directory_tree", "search_files", "get_file_info",
|
|
14
|
+
"list_allowed_directories"
|
|
15
|
+
]);
|
|
16
|
+
export const MEMORY_MCP_LIMITS = Object.freeze({
|
|
17
|
+
requestBytes: 1024 * 1024, responseBytes: 16 * 1024 * 1024,
|
|
18
|
+
fileBytes: 8 * 1024 * 1024, totalBytes: 12 * 1024 * 1024,
|
|
19
|
+
files: 100, pathChars: 4096, timeoutMs: 10000, manifestBytes: 512 * 1024
|
|
20
|
+
});
|
|
21
|
+
export class MemoryMcpError extends Error {
|
|
22
|
+
constructor(code, fatal = false) { super(code); this.code = code; this.fatal = fatal; }
|
|
23
|
+
}
|
|
24
|
+
const fail = (code, fatal = false) => { throw new MemoryMcpError(code, fatal); };
|
|
25
|
+
const control = /[\x00-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/u;
|
|
26
|
+
const forbidden = input => { const value = toGitPath(input); return isCredentialPath(value) ||
|
|
27
|
+
/(?:^|\/)(?:\.git|\.obsidian|\.codex|\.claude|\.gemini|\.agents|\.secrets|\.credentials|\.npmrc|\.netrc|\.envrc)(?:\/|$)/i.test(value) || /\.(?:keychain(?:-db)?|kdbx)$/i.test(value); };
|
|
28
|
+
const globCase = value => value.replace(/[a-z]/g, c => `[${c}${c.toUpperCase()}]`);
|
|
29
|
+
const recursiveExclusions = Object.freeze([
|
|
30
|
+
".git", ".obsidian", ".codex", ".claude", ".gemini", ".agents", ".secrets", ".credentials",
|
|
31
|
+
".env", ".env.*", ".envrc", ".npmrc", ".netrc", "secret", "secrets", "credential", "credentials", "id_rsa", "id_ed25519"
|
|
32
|
+
].flatMap(name => [`**/${globCase(name)}`, `**/${globCase(name)}/**`]).concat(
|
|
33
|
+
["pem", "key", "p12", "pfx", "keychain", "keychain-db", "kdbx"].map(ext => `**/*.${globCase(ext)}`)));
|
|
34
|
+
const text = { type: "string", minLength: 1, maxLength: MEMORY_MCP_LIMITS.pathChars };
|
|
35
|
+
const list = { type: "array", maxItems: MEMORY_MCP_LIMITS.files, items: text };
|
|
36
|
+
const object = (properties, required = []) => ({ type: "object", additionalProperties: false, properties, required });
|
|
37
|
+
export const MEMORY_TOOL_SCHEMAS = Object.freeze({
|
|
38
|
+
read_text_file: object({ path: text, head: { type: "integer", minimum: 1 }, tail: { type: "integer", minimum: 1 } }, ["path"]),
|
|
39
|
+
read_media_file: object({ path: text }, ["path"]),
|
|
40
|
+
read_multiple_files: object({ paths: { ...list, minItems: 1 } }, ["paths"]),
|
|
41
|
+
list_directory: object({ path: text }, ["path"]),
|
|
42
|
+
list_directory_with_sizes: object({ path: text, sortBy: { enum: ["name", "size"] } }, ["path"]),
|
|
43
|
+
directory_tree: object({ path: text, excludePatterns: list }, ["path"]),
|
|
44
|
+
search_files: object({ path: text, pattern: text, excludePatterns: list }, ["path", "pattern"]),
|
|
45
|
+
get_file_info: object({ path: text }, ["path"]),
|
|
46
|
+
list_allowed_directories: object({})
|
|
47
|
+
});
|
|
48
|
+
const ajv = new Ajv2020({ strict: true, allErrors: true, ownProperties: true });
|
|
49
|
+
const inputs = new Map(MEMORY_READ_TOOLS.map(name => [name, ajv.compile(MEMORY_TOOL_SCHEMAS[name])]));
|
|
50
|
+
let manifestValidator;
|
|
51
|
+
const getManifestValidator = () => manifestValidator ??= fs.readFile(new URL("../schemas/project-memory.schema.json", import.meta.url), "utf8")
|
|
52
|
+
.then(JSON.parse).then(schema => ajv.compile(schema));
|
|
53
|
+
const identity = s => JSON.stringify([s.dev, s.ino, s.uid, s.mode]);
|
|
54
|
+
const fileIdentity = s => JSON.stringify([s.dev, s.ino, s.uid, s.mode, s.nlink, s.size, s.mtimeMs, s.ctimeMs]);
|
|
55
|
+
const inside = (root, target) => { const rel = path.relative(root, target); return rel === "" || (!path.isAbsolute(rel) && rel !== ".." && !rel.startsWith(".." + path.sep)); };
|
|
56
|
+
const canonical = value => typeof value === "string" && value.length > 0 && value.length <= MEMORY_MCP_LIMITS.pathChars &&
|
|
57
|
+
value === value.trim() && !control.test(value) && (!IS_WINDOWS || !value.slice(path.parse(value).root.length).includes(':') && !/^\\\\[?.]\\/u.test(value)) && path.isAbsolute(value) && path.normalize(value) === value &&
|
|
58
|
+
value.split(path.sep).every(part => part !== "." && part !== "..");
|
|
59
|
+
|
|
60
|
+
async function physical(io, full) {
|
|
61
|
+
let current = path.parse(full).root, last;
|
|
62
|
+
const parts = full.slice(current.length).split(path.sep).filter(Boolean);
|
|
63
|
+
if (!parts.length || parts.length > 64) fail("RG_PATH_DENIED");
|
|
64
|
+
for (const [i, part] of parts.entries()) {
|
|
65
|
+
current = path.join(current, part); last = await io.lstat(current);
|
|
66
|
+
if (last.isSymbolicLink() || i < parts.length - 1 && !last.isDirectory()) fail("RG_PATH_DENIED");
|
|
67
|
+
}
|
|
68
|
+
return last;
|
|
69
|
+
}
|
|
70
|
+
function safe(s, uid, directory) {
|
|
71
|
+
if ((!IS_WINDOWS && (s.uid !== uid || (s.mode & 0o7022))) || s.isSymbolicLink() ||
|
|
72
|
+
(directory ? !s.isDirectory() : !s.isFile() || s.nlink !== 1)) fail("RG_PATH_DENIED");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The only file content read here is the explicitly supplied manifest. */
|
|
76
|
+
export async function loadMemoryPolicy(manifestPath, context = {}) {
|
|
77
|
+
const io = { ...fs, ...context.io }, uid = context.uid ?? process.getuid?.();
|
|
78
|
+
const platform = context.platform ?? process.platform;
|
|
79
|
+
if (!["darwin", "linux", "win32"].includes(platform) || platform !== "win32" && typeof uid !== "number") fail("RG_PLATFORM_UNSUPPORTED");
|
|
80
|
+
warnPlatformPermissions();
|
|
81
|
+
if (!canonical(manifestPath) || forbidden(manifestPath)) fail("RG_MANIFEST_INVALID");
|
|
82
|
+
let manifest;
|
|
83
|
+
try {
|
|
84
|
+
const before = await physical(io, manifestPath); safe(before, uid, false);
|
|
85
|
+
if (!before.size || before.size > MEMORY_MCP_LIMITS.manifestBytes) fail("RG_MANIFEST_INVALID");
|
|
86
|
+
const handle = await io.open(manifestPath, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
87
|
+
try {
|
|
88
|
+
if (fileIdentity(await handle.stat()) !== fileIdentity(before)) fail("RG_MANIFEST_INVALID");
|
|
89
|
+
const bytes = Buffer.alloc(before.size); let offset = 0;
|
|
90
|
+
while (offset < bytes.length) {
|
|
91
|
+
const { bytesRead } = await handle.read(bytes, offset, bytes.length - offset, offset);
|
|
92
|
+
if (!Number.isSafeInteger(bytesRead) || bytesRead <= 0 || bytesRead > bytes.length - offset) fail("RG_MANIFEST_INVALID");
|
|
93
|
+
offset += bytesRead;
|
|
94
|
+
}
|
|
95
|
+
if (fileIdentity(await handle.stat()) !== fileIdentity(before) || fileIdentity(await physical(io, manifestPath)) !== fileIdentity(before)) fail("RG_MANIFEST_INVALID");
|
|
96
|
+
manifest = parseTaskDocument(bytes);
|
|
97
|
+
} finally { await handle.close(); }
|
|
98
|
+
if (!(await getManifestValidator())(manifest) || manifest.security.mcpAccess !== "read-only" || containsSensitiveData(JSON.stringify(manifest))) fail("RG_MANIFEST_INVALID");
|
|
99
|
+
const root = path.resolve(path.dirname(manifestPath), manifest.memory.vaultPath);
|
|
100
|
+
const source = await io.realpath(path.resolve(path.dirname(manifestPath), manifest.source.repositoryPath));
|
|
101
|
+
if (!canonical(root) || forbidden(root) || root === path.parse(root).root || path.relative(root, context.home ?? os.homedir()) === "" ||
|
|
102
|
+
inside(root, source) || inside(source, root)) fail("RG_ROOT_DENIED");
|
|
103
|
+
if (!(await io.lstat(source)).isDirectory()) fail("RG_ROOT_DENIED");
|
|
104
|
+
const stat = await physical(io, root); safe(stat, uid, true);
|
|
105
|
+
return Object.freeze({ root, uid, rootIdentity: identity(stat) });
|
|
106
|
+
} catch (error) { if (error instanceof MemoryMcpError) throw error; fail("RG_MANIFEST_INVALID"); }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function recheckMemoryRoot(policy, context = {}) {
|
|
110
|
+
try {
|
|
111
|
+
const s = await physical({ ...fs, ...context.io }, policy.root);
|
|
112
|
+
safe(s, policy.uid, true);
|
|
113
|
+
if (identity(s) !== policy.rootIdentity) fail("RG_ROOT_CHANGED", true);
|
|
114
|
+
} catch { fail("RG_ROOT_CHANGED", true); }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Validate the whole batch before a backend is allowed to read any listed file. */
|
|
118
|
+
export async function validateMemoryCall(policy, name, args, context = {}) {
|
|
119
|
+
if (!inputs.has(name)) fail("RG_TOOL_DENIED");
|
|
120
|
+
if (!inputs.get(name)(args) || name === "read_text_file" && args.head !== undefined && args.tail !== undefined) fail("RG_INPUT_INVALID");
|
|
121
|
+
const io = { ...fs, ...context.io }, paths = args.paths ?? (args.path !== undefined ? [args.path] : []);
|
|
122
|
+
if ([args.pattern, ...(args.excludePatterns ?? [])].some(v => v !== undefined && control.test(v))) fail("RG_INPUT_INVALID");
|
|
123
|
+
const fileRead = ["read_text_file", "read_media_file", "read_multiple_files"].includes(name);
|
|
124
|
+
const snapshots = []; let total = 0;
|
|
125
|
+
for (const full of paths) {
|
|
126
|
+
if (!canonical(full) || forbidden(full) || !inside(policy.root, full)) fail("RG_PATH_DENIED");
|
|
127
|
+
try {
|
|
128
|
+
const s = await physical(io, full), directory = !fileRead && name !== "get_file_info" || s.isDirectory();
|
|
129
|
+
safe(s, policy.uid, directory);
|
|
130
|
+
if (fileRead && (!s.isFile() || !IS_WINDOWS && !(s.mode & 0o400))) fail("RG_PATH_DENIED");
|
|
131
|
+
if (fileRead) { total += s.size; if (s.size > MEMORY_MCP_LIMITS.fileBytes || total > MEMORY_MCP_LIMITS.totalBytes) fail("RG_READ_LIMIT"); }
|
|
132
|
+
snapshots.push({ path: full, signature: fileIdentity(s) });
|
|
133
|
+
} catch (error) { if (error instanceof MemoryMcpError) throw error; fail("RG_PATH_DENIED"); }
|
|
134
|
+
}
|
|
135
|
+
const forwarded = structuredClone(args);
|
|
136
|
+
if (["directory_tree", "search_files"].includes(name))
|
|
137
|
+
forwarded.excludePatterns = [...new Set([...(args.excludePatterns ?? []), ...recursiveExclusions])];
|
|
138
|
+
return { args: forwarded, snapshots };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function recheckMemoryEntries(snapshots, context = {}) {
|
|
142
|
+
try { for (const entry of snapshots) if (fileIdentity(await physical({ ...fs, ...context.io }, entry.path)) !== entry.signature) fail("RG_ENTRY_CHANGED"); }
|
|
143
|
+
catch { fail("RG_ENTRY_CHANGED"); }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function checkMemoryResult(result, mediaPaths = []) {
|
|
147
|
+
if (!result || !Array.isArray(result.content) || result.isError) fail("RG_BACKEND_ERROR");
|
|
148
|
+
if (Buffer.byteLength(JSON.stringify(result)) > MEMORY_MCP_LIMITS.responseBytes - 1024) fail("RG_RESPONSE_LIMIT", true);
|
|
149
|
+
const checkBlob = data => {
|
|
150
|
+
if (typeof data !== "string" || !/^[A-Za-z0-9+/]*={0,2}$/.test(data) || data.length % 4) fail("RG_BACKEND_PROTOCOL", true);
|
|
151
|
+
const bytes = Buffer.from(data, "base64");
|
|
152
|
+
if (bytes.toString("base64") !== data) fail("RG_BACKEND_PROTOCOL", true);
|
|
153
|
+
if (bytes.length > MEMORY_MCP_LIMITS.fileBytes) fail("RG_RESPONSE_LIMIT", true);
|
|
154
|
+
if (containsSensitiveData(bytes.toString("utf8"))) fail("RG_CONTENT_DENIED");
|
|
155
|
+
};
|
|
156
|
+
const content = result.content.map(block => {
|
|
157
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
158
|
+
if (containsSensitiveData(block.text)) fail("RG_CONTENT_DENIED");
|
|
159
|
+
return { type: "text", text: block.text };
|
|
160
|
+
}
|
|
161
|
+
if (["image", "audio"].includes(block.type) && mediaPaths.length && typeof block.data === "string" && typeof block.mimeType === "string") {
|
|
162
|
+
checkBlob(block.data);
|
|
163
|
+
return { type: block.type, data: block.data, mimeType: block.mimeType };
|
|
164
|
+
}
|
|
165
|
+
if (block.type === "resource" && mediaPaths.length && typeof block.resource?.blob === "string") {
|
|
166
|
+
let file; try { file = fileURLToPath(block.resource.uri); } catch { fail("RG_BACKEND_PROTOCOL", true); }
|
|
167
|
+
if (!mediaPaths.includes(file) || pathToFileURL(file).href !== block.resource.uri || typeof block.resource.mimeType !== "string") fail("RG_BACKEND_PROTOCOL", true);
|
|
168
|
+
checkBlob(block.resource.blob);
|
|
169
|
+
return { type: "resource", resource: { uri: block.resource.uri, mimeType: block.resource.mimeType, blob: block.resource.blob } };
|
|
170
|
+
}
|
|
171
|
+
fail("RG_BACKEND_PROTOCOL", true);
|
|
172
|
+
});
|
|
173
|
+
if (result.structuredContent !== undefined) {
|
|
174
|
+
if (Array.isArray(result.structuredContent?.content) && mediaPaths.length &&
|
|
175
|
+
JSON.stringify(result.structuredContent.content) === JSON.stringify(result.content))
|
|
176
|
+
return { content, structuredContent: { content } };
|
|
177
|
+
if (typeof result.structuredContent?.content !== "string" || containsSensitiveData(result.structuredContent.content)) fail("RG_CONTENT_DENIED");
|
|
178
|
+
return { content, structuredContent: { content: result.structuredContent.content } };
|
|
179
|
+
}
|
|
180
|
+
return { content };
|
|
181
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
6
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7
|
+
import { InitializeRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, McpError, SUPPORTED_PROTOCOL_VERSIONS } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import {
|
|
9
|
+
MEMORY_READ_TOOLS, MEMORY_TOOL_SCHEMAS, MEMORY_MCP_LIMITS, MemoryMcpError,
|
|
10
|
+
loadMemoryPolicy, recheckMemoryRoot, validateMemoryCall, recheckMemoryEntries, checkMemoryResult
|
|
11
|
+
} from "./memory-mcp-policy.js";
|
|
12
|
+
import { MemoryMcpTransport } from "./memory-mcp-transport.js";
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const codeOf = error => error instanceof MemoryMcpError ? error.code : "RG_BACKEND_ERROR";
|
|
16
|
+
const resultText = r => r?.structuredContent?.content ?? r?.content?.filter(b => b.type === "text").map(b => b.text).join("\n");
|
|
17
|
+
|
|
18
|
+
/** Runtime never resolves a package from npx, cwd, the manifest or client input. */
|
|
19
|
+
export function createFilesystemBackend(root, onFatal) {
|
|
20
|
+
const manifest = require.resolve("@modelcontextprotocol/server-filesystem/package.json");
|
|
21
|
+
const pkg = JSON.parse(fs.readFileSync(manifest, "utf8"));
|
|
22
|
+
if (pkg.version !== "2026.8.31" || pkg.bin?.["mcp-server-filesystem"] !== "dist/index.js") throw new MemoryMcpError("RG_BACKEND_VERSION", true);
|
|
23
|
+
const client = new Client({ name: "project-memory-guard-backend", version: "0.1.0" }, { capabilities: {} });
|
|
24
|
+
const transport = new StdioClientTransport({ command: process.execPath,
|
|
25
|
+
args: [path.join(path.dirname(manifest), "dist/index.js"), root], cwd: path.dirname(manifest),
|
|
26
|
+
stderr: "pipe", maxBufferSize: MEMORY_MCP_LIMITS.responseBytes });
|
|
27
|
+
// Drain, never print the child's paths, stack traces or payloads.
|
|
28
|
+
transport.stderr.resume();
|
|
29
|
+
let closing = false;
|
|
30
|
+
client.onerror = () => { if (!closing) onFatal("RG_BACKEND_PROTOCOL"); };
|
|
31
|
+
client.onclose = () => { if (!closing) onFatal("RG_BACKEND_CLOSED"); };
|
|
32
|
+
client.fallbackRequestHandler = async () => { onFatal("RG_BACKEND_CALLBACK"); throw new McpError(-32601, "RG_BACKEND_CALLBACK"); };
|
|
33
|
+
client.fallbackNotificationHandler = async () => { onFatal("RG_BACKEND_PROTOCOL"); };
|
|
34
|
+
return {
|
|
35
|
+
start: () => client.connect(transport, { timeout: MEMORY_MCP_LIMITS.timeoutMs }),
|
|
36
|
+
listTools: () => client.listTools(undefined, { timeout: MEMORY_MCP_LIMITS.timeoutMs }),
|
|
37
|
+
call: (name, args) => client.callTool({ name, arguments: args }, undefined, { timeout: MEMORY_MCP_LIMITS.timeoutMs }),
|
|
38
|
+
async close() { closing = true; try { await client.close(); } finally { await transport.close(); } },
|
|
39
|
+
get pid() { return transport.pid; }
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Only the trusted programmatic test context can replace a backend. No CLI/RPC selector exists. */
|
|
44
|
+
export async function createMemoryGateway(manifestPath, context = {}) {
|
|
45
|
+
const policy = await loadMemoryPolicy(manifestPath, context);
|
|
46
|
+
let closed = false, busy = false, closePromise, cleanupError, backend;
|
|
47
|
+
const timeout = context.timeoutMs ?? MEMORY_MCP_LIMITS.timeoutMs;
|
|
48
|
+
const requestClose = () => {
|
|
49
|
+
closed = true;
|
|
50
|
+
return closePromise ??= Promise.resolve().then(() => backend?.close())
|
|
51
|
+
.catch(() => { cleanupError = new MemoryMcpError("RG_CLOSE_FAILED", true); });
|
|
52
|
+
};
|
|
53
|
+
const close = async () => { await requestClose(); if (cleanupError) throw cleanupError; };
|
|
54
|
+
const fatal = () => { void requestClose(); };
|
|
55
|
+
backend = (context.backendFactory ?? createFilesystemBackend)(policy.root, fatal);
|
|
56
|
+
const active = () => { if (closed) throw new MemoryMcpError("RG_SESSION_CLOSED", true); };
|
|
57
|
+
async function deadline(work, signal) {
|
|
58
|
+
active(); let timer, aborted;
|
|
59
|
+
const stop = code => { fatal(); return new MemoryMcpError(code, true); };
|
|
60
|
+
if (signal?.aborted) throw stop("RG_CANCELLED");
|
|
61
|
+
try {
|
|
62
|
+
const limit = new Promise((_, reject) => {
|
|
63
|
+
timer = setTimeout(() => reject(stop("RG_TIMEOUT")), timeout);
|
|
64
|
+
aborted = () => reject(stop("RG_CANCELLED"));
|
|
65
|
+
if (signal?.aborted) aborted(); else signal?.addEventListener("abort", aborted, { once: true });
|
|
66
|
+
});
|
|
67
|
+
const value = await Promise.race([Promise.resolve().then(() => {
|
|
68
|
+
if (signal?.aborted) throw stop("RG_CANCELLED"); active(); return work();
|
|
69
|
+
}), limit]); active(); return value;
|
|
70
|
+
} finally { clearTimeout(timer); signal?.removeEventListener("abort", aborted); }
|
|
71
|
+
}
|
|
72
|
+
async function scope() {
|
|
73
|
+
await recheckMemoryRoot(policy, context);
|
|
74
|
+
const r = await backend.call("list_allowed_directories", {});
|
|
75
|
+
if (r?.isError || resultText(r) !== "Allowed directories:\n" + policy.root) throw new MemoryMcpError("RG_BACKEND_SCOPE", true);
|
|
76
|
+
}
|
|
77
|
+
let tools;
|
|
78
|
+
try {
|
|
79
|
+
await deadline(async () => {
|
|
80
|
+
await backend.start(); await scope();
|
|
81
|
+
const catalog = await backend.listTools();
|
|
82
|
+
const map = new Map(catalog.tools?.map(t => [t.name, t]));
|
|
83
|
+
if (!Array.isArray(catalog.tools) || map.size !== catalog.tools.length) throw new MemoryMcpError("RG_BACKEND_CATALOG", true);
|
|
84
|
+
tools = MEMORY_READ_TOOLS.map(name => {
|
|
85
|
+
const tool = map.get(name), expected = MEMORY_TOOL_SCHEMAS[name];
|
|
86
|
+
if (tool?.annotations?.readOnlyHint !== true || tool.inputSchema?.type !== "object" ||
|
|
87
|
+
(expected.required ?? []).some(key => !tool.inputSchema?.properties?.[key]) ||
|
|
88
|
+
(tool.inputSchema?.required ?? []).some(key => !(expected.required ?? []).includes(key)))
|
|
89
|
+
throw new MemoryMcpError("RG_BACKEND_CATALOG", true);
|
|
90
|
+
for (const [key, schema] of Object.entries(expected.properties)) {
|
|
91
|
+
const upstream = tool.inputSchema.properties?.[key];
|
|
92
|
+
if (!upstream || schema.type && !(upstream.type === schema.type || schema.type === "integer" && upstream.type === "number") ||
|
|
93
|
+
schema.items && upstream.items?.type !== "string" ||
|
|
94
|
+
schema.enum && upstream.enum && schema.enum.some(value => !upstream.enum.includes(value)))
|
|
95
|
+
throw new MemoryMcpError("RG_BACKEND_CATALOG", true);
|
|
96
|
+
}
|
|
97
|
+
return { name, description: `Lectura limitada: ${name}. Raíz fijada por el manifiesto.`,
|
|
98
|
+
inputSchema: structuredClone(expected), annotations: { readOnlyHint: true, openWorldHint: false } };
|
|
99
|
+
});
|
|
100
|
+
await recheckMemoryRoot(policy, context);
|
|
101
|
+
}, context.signal);
|
|
102
|
+
} catch (error) { await close(); throw error instanceof MemoryMcpError ? error : new MemoryMcpError("RG_BACKEND_START", true); }
|
|
103
|
+
async function operation(work, signal) {
|
|
104
|
+
active(); if (busy) throw new MemoryMcpError("RG_BUSY"); busy = true;
|
|
105
|
+
try { return await deadline(work, signal); }
|
|
106
|
+
catch (error) { if (!(error instanceof MemoryMcpError) || error.fatal) await close(); throw error instanceof MemoryMcpError ? error : new MemoryMcpError("RG_BACKEND_ERROR", true); }
|
|
107
|
+
finally { busy = false; }
|
|
108
|
+
}
|
|
109
|
+
return Object.freeze({
|
|
110
|
+
root: policy.root,
|
|
111
|
+
get closed() { return closed; },
|
|
112
|
+
get backendPid() { return backend.pid; },
|
|
113
|
+
close,
|
|
114
|
+
listTools: signal => operation(async () => { await scope(); return structuredClone(tools); }, signal),
|
|
115
|
+
call: (name, args, signal) => operation(async () => {
|
|
116
|
+
await recheckMemoryRoot(policy, context);
|
|
117
|
+
const checked = await validateMemoryCall(policy, name, args, context);
|
|
118
|
+
await scope();
|
|
119
|
+
const raw = name === "list_allowed_directories"
|
|
120
|
+
? { content: [{ type: "text", text: "Allowed directories:\n" + policy.root }] }
|
|
121
|
+
: await backend.call(name, checked.args);
|
|
122
|
+
// Filesystem encodes per-file failures as text in a successful batch.
|
|
123
|
+
// Do not report that partial result as a successful read or relay its raw errors.
|
|
124
|
+
if (name === "read_multiple_files" && checked.args.paths.some(file => resultText(raw)?.includes(file + ": Error -")))
|
|
125
|
+
throw new MemoryMcpError("RG_BACKEND_ERROR");
|
|
126
|
+
const result = checkMemoryResult(raw, name === "read_media_file" ? checked.snapshots.map(s => s.path) : []);
|
|
127
|
+
await recheckMemoryEntries(checked.snapshots, context); await recheckMemoryRoot(policy, context);
|
|
128
|
+
return result;
|
|
129
|
+
}, signal)
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function runMemoryMcp(manifestPath, context = {}) {
|
|
134
|
+
const gateway = await createMemoryGateway(manifestPath, context);
|
|
135
|
+
let resolveEnd, initialized = false;
|
|
136
|
+
const ended = new Promise(resolve => { resolveEnd = resolve; });
|
|
137
|
+
const server = new Server({ name: "project-memory-guard", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
138
|
+
const transport = new MemoryMcpTransport(context.stdin ?? process.stdin, context.stdout ?? process.stdout,
|
|
139
|
+
{ onClose: resolveEnd, onFatal: () => { void gateway.close().catch(() => {}); } });
|
|
140
|
+
const protocolError = code => new McpError(-32602, code);
|
|
141
|
+
server.setRequestHandler(InitializeRequestSchema, request => {
|
|
142
|
+
if (initialized) throw protocolError("RG_ALREADY_INITIALIZED");
|
|
143
|
+
if (!SUPPORTED_PROTOCOL_VERSIONS.includes(request.params.protocolVersion)) throw protocolError("RG_PROTOCOL_UNSUPPORTED");
|
|
144
|
+
initialized = true;
|
|
145
|
+
return { protocolVersion: request.params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: "project-memory-guard", version: "0.1.0" } };
|
|
146
|
+
});
|
|
147
|
+
server.setRequestHandler(ListToolsRequestSchema, async (_, extra) => {
|
|
148
|
+
if (!initialized) throw protocolError("RG_NOT_INITIALIZED");
|
|
149
|
+
try { return { tools: await gateway.listTools(extra.signal) }; }
|
|
150
|
+
catch (error) { throw protocolError(codeOf(error)); }
|
|
151
|
+
});
|
|
152
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
153
|
+
if (!initialized) throw protocolError("RG_NOT_INITIALIZED");
|
|
154
|
+
if (request.params.task) throw protocolError("RG_INPUT_INVALID");
|
|
155
|
+
try { return await gateway.call(request.params.name, request.params.arguments ?? {}, extra.signal); }
|
|
156
|
+
catch (error) { return { isError: true, content: [{ type: "text", text: codeOf(error) }] }; }
|
|
157
|
+
});
|
|
158
|
+
server.onerror = () => { void transport.close(); };
|
|
159
|
+
server.onclose = () => { void gateway.close().catch(() => {}); };
|
|
160
|
+
const abort = () => { void transport.close(); };
|
|
161
|
+
context.signal?.addEventListener("abort", abort, { once: true });
|
|
162
|
+
try {
|
|
163
|
+
if (context.signal?.aborted) throw new MemoryMcpError("RG_CANCELLED");
|
|
164
|
+
await server.connect(transport); await ended;
|
|
165
|
+
} finally { context.signal?.removeEventListener("abort", abort); await gateway.close(); await server.close(); }
|
|
166
|
+
}
|