@agent-native/core 0.113.0 → 0.114.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/agent-native.eject.json +164 -0
- package/corpus/README.md +3 -3
- package/corpus/core/CHANGELOG.md +11 -0
- package/corpus/core/README.md +61 -174
- package/corpus/core/agent-native.eject.json +164 -0
- package/corpus/core/docs/content/agent-native-toolkit.mdx +38 -0
- package/corpus/core/docs/content/package-lifecycle.mdx +54 -4
- package/corpus/core/package.json +15 -2
- package/corpus/core/src/cli/eject.ts +1594 -0
- package/corpus/core/src/cli/index.ts +16 -0
- package/corpus/core/src/client/index.ts +4 -0
- package/corpus/core/src/client/resources/McpConnectionSuggestion.tsx +8 -2
- package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +7 -2
- package/corpus/core/src/client/resources/ResourcesPanel.tsx +17 -3
- package/corpus/core/src/client/resources/index.ts +19 -0
- package/corpus/core/src/client/resources/mcp-integration-catalog.ts +9 -1
- package/corpus/core/src/client/resources/runtime.ts +1 -0
- package/corpus/core/src/client/setup-connections/catalog.ts +6 -0
- package/corpus/core/src/client/setup-connections/index.ts +1 -0
- package/corpus/core/src/client/setup-connections/runtime.ts +1 -0
- package/corpus/core/src/connections/catalog.ts +41 -24
- package/corpus/core/src/connections/index.ts +1 -0
- package/corpus/core/src/connections/runtime.ts +1 -0
- package/corpus/core/src/eject/provider-api-definitions.ts +193 -0
- package/corpus/core/src/eject/remote-mcp-presets.ts +137 -0
- package/corpus/core/src/eject/setup-readiness-ui.tsx +29 -0
- package/corpus/core/src/eject/workspace-connections.ts +92 -0
- package/corpus/core/src/index.ts +1 -0
- package/corpus/core/src/integrations/adapter-overrides.ts +12 -0
- package/corpus/core/src/integrations/eject/messaging-adapters.ts +85 -0
- package/corpus/core/src/integrations/index.ts +5 -1
- package/corpus/core/src/integrations/plugin.ts +34 -12
- package/corpus/core/src/integrations/runtime.ts +1 -0
- package/corpus/core/src/integrations/types.ts +3 -1
- package/corpus/core/src/package-lifecycle/eject-manifest.ts +345 -0
- package/corpus/core/src/package-lifecycle/index.ts +1 -0
- package/corpus/core/src/provider-api/index.ts +46 -12
- package/corpus/core/src/provider-api/runtime.ts +1 -0
- package/corpus/core/src/setup-connections/index.ts +1 -0
- package/corpus/core/src/shared/merge-by-id.ts +14 -0
- package/corpus/core/src/templates/default/.agents/skills/agent-native-docs/SKILL.md +15 -13
- package/corpus/core/src/templates/default/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/corpus/core/src/templates/default/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/corpus/core/src/templates/default/AGENTS.md +6 -5
- package/corpus/core/src/templates/headless/.agents/skills/agent-native-docs/SKILL.md +15 -13
- package/corpus/core/src/templates/headless/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/corpus/core/src/templates/headless/AGENTS.md +5 -4
- package/corpus/core/src/templates/workspace-core/.agents/skills/agent-native-docs/SKILL.md +15 -13
- package/corpus/core/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +3 -2
- package/corpus/core/src/templates/workspace-core/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/corpus/core/src/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/corpus/core/src/templates/workspace-core/AGENTS.md +4 -4
- package/corpus/core/src/templates/workspace-root/AGENTS.md +4 -4
- package/corpus/core/src/vite/client.ts +25 -2
- package/corpus/templates/analytics/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/corpus/templates/analytics/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/corpus/templates/assets/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/corpus/templates/assets/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/corpus/templates/brain/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/corpus/templates/brain/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/corpus/templates/brain/actions/remediate-legacy-capture-audiences.ts +167 -0
- package/corpus/templates/brain/app/lib/brain-chat-readiness.ts +6 -0
- package/corpus/templates/brain/app/routes/_index.tsx +8 -0
- package/corpus/templates/brain/app/routes.ts +1 -0
- package/corpus/templates/brain/changelog/2026-07-19-ask-starts-immediately-when-builder-s-managed-ai-connection-.md +6 -0
- package/corpus/templates/brain/changelog/2026-07-19-slack-and-granola-backfills-now-process-captures-concurrentl.md +6 -0
- package/corpus/templates/brain/changelog/2026-07-19-source-review-settings-now-control-knowledge-approval.md +6 -0
- package/corpus/templates/brain/changelog/2026-07-19-the-manage-agent-page-now-opens-correctly-from-brain-navigat.md +6 -0
- package/corpus/templates/brain/server/db/schema.ts +3 -0
- package/corpus/templates/brain/server/lib/brain.ts +41 -2
- package/corpus/templates/brain/server/lib/connectors.ts +379 -113
- package/corpus/templates/brain/server/lib/meeting-audience.ts +38 -0
- package/corpus/templates/brain/server/plugins/db.ts +10 -0
- package/corpus/templates/brain/server/routes/api/_agent-native/brain/ingest.post.ts +6 -4
- package/corpus/templates/calendar/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/corpus/templates/calendar/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/corpus/templates/chat/.agents/skills/agent-native-docs/SKILL.md +15 -13
- package/corpus/templates/chat/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/corpus/templates/chat/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/corpus/templates/chat/AGENTS.md +3 -2
- package/corpus/templates/clips/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/corpus/templates/clips/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/corpus/templates/content/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/corpus/templates/content/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/corpus/templates/design/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/corpus/templates/design/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/corpus/templates/dispatch/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/corpus/templates/dispatch/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/corpus/templates/forms/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/corpus/templates/forms/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/corpus/templates/macros/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/corpus/templates/mail/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/corpus/templates/mail/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/corpus/templates/plan/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/corpus/templates/plan/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/corpus/templates/slides/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/corpus/templates/slides/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/corpus/templates/tasks/.agents/skills/agent-native-docs/SKILL.md +15 -13
- package/corpus/templates/tasks/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/corpus/templates/tasks/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/agent-native.eject.json +411 -0
- package/corpus/toolkit/package.json +5 -2
- package/dist/cli/eject.d.ts +21 -0
- package/dist/cli/eject.d.ts.map +1 -0
- package/dist/cli/eject.js +1145 -0
- package/dist/cli/eject.js.map +1 -0
- package/dist/cli/index.js +15 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/resources/McpConnectionSuggestion.d.ts +4 -3
- package/dist/client/resources/McpConnectionSuggestion.d.ts.map +1 -1
- package/dist/client/resources/McpConnectionSuggestion.js +3 -3
- package/dist/client/resources/McpConnectionSuggestion.js.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.d.ts +4 -3
- package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.js +2 -2
- package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
- package/dist/client/resources/ResourcesPanel.d.ts +4 -1
- package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
- package/dist/client/resources/ResourcesPanel.js +9 -7
- package/dist/client/resources/ResourcesPanel.js.map +1 -1
- package/dist/client/resources/index.d.ts +3 -0
- package/dist/client/resources/index.d.ts.map +1 -1
- package/dist/client/resources/index.js +3 -0
- package/dist/client/resources/index.js.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.d.ts +2 -1
- package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.js +6 -2
- package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
- package/dist/client/resources/runtime.d.ts +2 -0
- package/dist/client/resources/runtime.d.ts.map +1 -0
- package/dist/client/resources/runtime.js +2 -0
- package/dist/client/resources/runtime.js.map +1 -0
- package/dist/client/setup-connections/catalog.d.ts +2 -0
- package/dist/client/setup-connections/catalog.d.ts.map +1 -0
- package/dist/client/setup-connections/catalog.js +7 -0
- package/dist/client/setup-connections/catalog.js.map +1 -0
- package/dist/client/setup-connections/index.d.ts +1 -0
- package/dist/client/setup-connections/index.d.ts.map +1 -1
- package/dist/client/setup-connections/index.js +1 -0
- package/dist/client/setup-connections/index.js.map +1 -1
- package/dist/client/setup-connections/runtime.d.ts +2 -0
- package/dist/client/setup-connections/runtime.d.ts.map +1 -0
- package/dist/client/setup-connections/runtime.js +2 -0
- package/dist/client/setup-connections/runtime.js.map +1 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/connections/catalog.d.ts +5 -3
- package/dist/connections/catalog.d.ts.map +1 -1
- package/dist/connections/catalog.js +16 -8
- package/dist/connections/catalog.js.map +1 -1
- package/dist/connections/index.d.ts +1 -1
- package/dist/connections/index.d.ts.map +1 -1
- package/dist/connections/index.js +1 -1
- package/dist/connections/index.js.map +1 -1
- package/dist/connections/runtime.d.ts +2 -0
- package/dist/connections/runtime.d.ts.map +1 -0
- package/dist/connections/runtime.js +2 -0
- package/dist/connections/runtime.js.map +1 -0
- package/dist/eject/provider-api-definitions.d.ts +34 -0
- package/dist/eject/provider-api-definitions.d.ts.map +1 -0
- package/dist/eject/provider-api-definitions.js +88 -0
- package/dist/eject/provider-api-definitions.js.map +1 -0
- package/dist/eject/remote-mcp-presets.d.ts +17 -0
- package/dist/eject/remote-mcp-presets.d.ts.map +1 -0
- package/dist/eject/remote-mcp-presets.js +81 -0
- package/dist/eject/remote-mcp-presets.js.map +1 -0
- package/dist/eject/setup-readiness-ui.d.ts +7 -0
- package/dist/eject/setup-readiness-ui.d.ts.map +1 -0
- package/dist/eject/setup-readiness-ui.js +13 -0
- package/dist/eject/setup-readiness-ui.js.map +1 -0
- package/dist/eject/workspace-connections.d.ts +11 -0
- package/dist/eject/workspace-connections.d.ts.map +1 -0
- package/dist/eject/workspace-connections.js +47 -0
- package/dist/eject/workspace-connections.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/adapter-overrides.d.ts +3 -0
- package/dist/integrations/adapter-overrides.d.ts.map +1 -0
- package/dist/integrations/adapter-overrides.js +7 -0
- package/dist/integrations/adapter-overrides.js.map +1 -0
- package/dist/integrations/eject/messaging-adapters.d.ts +12 -0
- package/dist/integrations/eject/messaging-adapters.d.ts.map +1 -0
- package/dist/integrations/eject/messaging-adapters.js +49 -0
- package/dist/integrations/eject/messaging-adapters.js.map +1 -0
- package/dist/integrations/index.d.ts +3 -2
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js +2 -1
- package/dist/integrations/index.js.map +1 -1
- package/dist/integrations/plugin.d.ts +32 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +19 -12
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/runtime.d.ts +2 -0
- package/dist/integrations/runtime.d.ts.map +1 -0
- package/dist/integrations/runtime.js +2 -0
- package/dist/integrations/runtime.js.map +1 -0
- package/dist/integrations/types.d.ts +3 -1
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/integrations/types.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/package-lifecycle/eject-manifest.d.ts +43 -0
- package/dist/package-lifecycle/eject-manifest.d.ts.map +1 -0
- package/dist/package-lifecycle/eject-manifest.js +219 -0
- package/dist/package-lifecycle/eject-manifest.js.map +1 -0
- package/dist/package-lifecycle/index.d.ts +1 -0
- package/dist/package-lifecycle/index.d.ts.map +1 -1
- package/dist/package-lifecycle/index.js +1 -0
- package/dist/package-lifecycle/index.js.map +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/provider-api/index.d.ts +6 -2
- package/dist/provider-api/index.d.ts.map +1 -1
- package/dist/provider-api/index.js +25 -12
- package/dist/provider-api/index.js.map +1 -1
- package/dist/provider-api/runtime.d.ts +2 -0
- package/dist/provider-api/runtime.d.ts.map +1 -0
- package/dist/provider-api/runtime.js +2 -0
- package/dist/provider-api/runtime.js.map +1 -0
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/setup-connections/index.d.ts +1 -1
- package/dist/setup-connections/index.d.ts.map +1 -1
- package/dist/setup-connections/index.js +1 -1
- package/dist/setup-connections/index.js.map +1 -1
- package/dist/shared/merge-by-id.d.ts +5 -0
- package/dist/shared/merge-by-id.d.ts.map +1 -0
- package/dist/shared/merge-by-id.js +7 -0
- package/dist/shared/merge-by-id.js.map +1 -0
- package/dist/templates/chat/.agents/skills/agent-native-docs/SKILL.md +15 -13
- package/dist/templates/chat/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/dist/templates/chat/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/dist/templates/chat/AGENTS.md +3 -2
- package/dist/templates/default/.agents/skills/agent-native-docs/SKILL.md +15 -13
- package/dist/templates/default/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/dist/templates/default/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/dist/templates/default/AGENTS.md +6 -5
- package/dist/templates/headless/.agents/skills/agent-native-docs/SKILL.md +15 -13
- package/dist/templates/headless/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/dist/templates/headless/AGENTS.md +5 -4
- package/dist/templates/workspace-core/.agents/skills/agent-native-docs/SKILL.md +15 -13
- package/dist/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +3 -2
- package/dist/templates/workspace-core/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/dist/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/dist/templates/workspace-core/AGENTS.md +4 -4
- package/dist/templates/workspace-root/AGENTS.md +4 -4
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +22 -2
- package/dist/vite/client.js.map +1 -1
- package/docs/content/agent-native-toolkit.mdx +38 -0
- package/docs/content/package-lifecycle.mdx +54 -4
- package/package.json +15 -2
- package/src/a2a/agent-card.ts +83 -0
- package/src/a2a/artifact-response.ts +1648 -0
- package/src/a2a/auth-policy.ts +66 -0
- package/src/a2a/caller-auth.ts +102 -0
- package/src/a2a/client.ts +876 -0
- package/src/a2a/correlation.ts +50 -0
- package/src/a2a/handlers.ts +1285 -0
- package/src/a2a/index.ts +50 -0
- package/src/a2a/invoke.ts +348 -0
- package/src/a2a/response-text.ts +53 -0
- package/src/a2a/server.ts +586 -0
- package/src/a2a/task-store.ts +917 -0
- package/src/a2a/types.ts +221 -0
- package/src/a2a-claims.ts +52 -0
- package/src/action-change-marker.ts +74 -0
- package/src/action-ui.ts +38 -0
- package/src/action.ts +1406 -0
- package/src/adapters/cli/index.ts +6 -0
- package/src/adapters/cli/registry.ts +73 -0
- package/src/adapters/cli/shell-adapter.ts +92 -0
- package/src/adapters/cli/types.ts +27 -0
- package/src/agent/action-continuation-guidance.ts +38 -0
- package/src/agent/app-model-defaults.ts +201 -0
- package/src/agent/context-xray/actions/context-evict.ts +56 -0
- package/src/agent/context-xray/actions/context-manifest-get.ts +75 -0
- package/src/agent/context-xray/actions/context-pin.ts +56 -0
- package/src/agent/context-xray/actions/context-preview-get.ts +134 -0
- package/src/agent/context-xray/actions/context-report.ts +75 -0
- package/src/agent/context-xray/actions/context-restore.ts +51 -0
- package/src/agent/context-xray/actions/errors.ts +40 -0
- package/src/agent/context-xray/apply-directives.ts +137 -0
- package/src/agent/context-xray/directives-store.ts +178 -0
- package/src/agent/context-xray/identity.ts +37 -0
- package/src/agent/context-xray/manifest.ts +284 -0
- package/src/agent/context-xray/migrations.ts +32 -0
- package/src/agent/context-xray/plugin.ts +23 -0
- package/src/agent/context-xray/schema.ts +17 -0
- package/src/agent/context-xray/segments.ts +194 -0
- package/src/agent/context-xray/tokenize.ts +93 -0
- package/src/agent/default-model.ts +5 -0
- package/src/agent/durable-background.ts +547 -0
- package/src/agent/engine/ai-sdk-engine.ts +565 -0
- package/src/agent/engine/anthropic-engine.ts +320 -0
- package/src/agent/engine/builder-engine.ts +1110 -0
- package/src/agent/engine/builder-gateway-headers.ts +44 -0
- package/src/agent/engine/builtin.ts +122 -0
- package/src/agent/engine/context-directives-transform.ts +73 -0
- package/src/agent/engine/continuation-dispatch-retry.ts +168 -0
- package/src/agent/engine/credential-errors.ts +69 -0
- package/src/agent/engine/index.ts +54 -0
- package/src/agent/engine/openai-compatible-endpoint.ts +25 -0
- package/src/agent/engine/output-tokens.ts +181 -0
- package/src/agent/engine/provider-env-vars.ts +69 -0
- package/src/agent/engine/registry.ts +853 -0
- package/src/agent/engine/tool-call-journal-seed.ts +85 -0
- package/src/agent/engine/translate-ai-sdk.ts +407 -0
- package/src/agent/engine/translate-anthropic.ts +611 -0
- package/src/agent/engine/types.ts +310 -0
- package/src/agent/harness/acp-adapter.ts +893 -0
- package/src/agent/harness/acp-builtin.ts +70 -0
- package/src/agent/harness/ai-sdk-adapter.ts +509 -0
- package/src/agent/harness/background.ts +365 -0
- package/src/agent/harness/builtin.ts +50 -0
- package/src/agent/harness/index.ts +86 -0
- package/src/agent/harness/lifecycle.ts +498 -0
- package/src/agent/harness/registry.ts +97 -0
- package/src/agent/harness/runner.ts +182 -0
- package/src/agent/harness/store.ts +466 -0
- package/src/agent/harness/translate.ts +130 -0
- package/src/agent/harness/types.ts +129 -0
- package/src/agent/index.ts +32 -0
- package/src/agent/loop-settings.ts +186 -0
- package/src/agent/model-config.ts +363 -0
- package/src/agent/observational-memory/compactor.ts +78 -0
- package/src/agent/observational-memory/config.ts +78 -0
- package/src/agent/observational-memory/index.ts +79 -0
- package/src/agent/observational-memory/internal-run.ts +89 -0
- package/src/agent/observational-memory/message-text.ts +49 -0
- package/src/agent/observational-memory/migrations.ts +44 -0
- package/src/agent/observational-memory/observer.ts +128 -0
- package/src/agent/observational-memory/plugin.ts +35 -0
- package/src/agent/observational-memory/prompts.ts +53 -0
- package/src/agent/observational-memory/read.ts +131 -0
- package/src/agent/observational-memory/reflector.ts +119 -0
- package/src/agent/observational-memory/schema.ts +48 -0
- package/src/agent/observational-memory/store.ts +275 -0
- package/src/agent/observational-memory/types.ts +65 -0
- package/src/agent/processors.ts +209 -0
- package/src/agent/production-agent.ts +8254 -0
- package/src/agent/run-loop-with-resume.ts +394 -0
- package/src/agent/run-manager.ts +1825 -0
- package/src/agent/run-ownership.ts +75 -0
- package/src/agent/run-store.ts +2601 -0
- package/src/agent/runtime-context.ts +169 -0
- package/src/agent/thread-data-builder.ts +1584 -0
- package/src/agent/tool-call-journal.ts +368 -0
- package/src/agent/tool-error-redaction.ts +52 -0
- package/src/agent/tool-result-images.ts +178 -0
- package/src/agent/tool-search.ts +376 -0
- package/src/agent/types.ts +372 -0
- package/src/agent-native/index.ts +198 -0
- package/src/agent-web/config.ts +253 -0
- package/src/agent-web/generator.ts +382 -0
- package/src/agent-web/index.ts +36 -0
- package/src/appearance/actions/change-appearance.ts +35 -0
- package/src/application-state/emitter.ts +81 -0
- package/src/application-state/handlers.ts +121 -0
- package/src/application-state/index.ts +44 -0
- package/src/application-state/script-helpers.ts +155 -0
- package/src/application-state/store.ts +274 -0
- package/src/assets/branding/agent-native.icon/Assets/background.svg +3 -0
- package/src/assets/branding/agent-native.icon/Assets/foreground.svg +12 -0
- package/src/assets/branding/agent-native.icon/icon.json +49 -0
- package/src/assets/branding/favicon.png +0 -0
- package/src/assets/branding/favicon.svg +1 -0
- package/src/assets/branding/icon-on-dark.svg +10 -0
- package/src/assets/branding/icon-on-light.svg +10 -0
- package/src/assets/branding/mac-app-icon.svg +1 -0
- package/src/assets/branding/slack-bot/agent-native-1024.png +0 -0
- package/src/assets/branding/slack-bot/agent-native-512.png +0 -0
- package/src/assets/branding/tray-icon.svg +6 -0
- package/src/assets/branding/wordmark-on-dark.svg +21 -0
- package/src/assets/branding/wordmark-on-light.svg +21 -0
- package/src/assets/fonts/LICENSE_LIBERATION +96 -0
- package/src/assets/fonts/LICENSE_NOTO_NASKH_ARABIC +93 -0
- package/src/assets/fonts/LiberationSans-Bold.ttf +0 -0
- package/src/assets/fonts/LiberationSans-Regular.ttf +0 -0
- package/src/assets/fonts/NotoNaskhArabic-Variable.ttf +0 -0
- package/src/audit/actions/export-audit-events.ts +162 -0
- package/src/audit/actions/get-audit-event.ts +24 -0
- package/src/audit/actions/list-audit-events.ts +66 -0
- package/src/audit/cleanup-job.ts +100 -0
- package/src/audit/config.ts +90 -0
- package/src/audit/index.ts +43 -0
- package/src/audit/record.ts +180 -0
- package/src/audit/redact.ts +109 -0
- package/src/audit/store.ts +346 -0
- package/src/audit/types.ts +141 -0
- package/src/automation/index.ts +802 -0
- package/src/brand-kit/brand-signals.ts +124 -0
- package/src/brand-kit/fig/index.ts +64 -0
- package/src/brand-kit/index.ts +72 -0
- package/src/brand-kit/types.ts +113 -0
- package/src/browser-sessions/actions.ts +302 -0
- package/src/browser-sessions/routes.ts +234 -0
- package/src/browser-sessions/store.ts +679 -0
- package/src/browser-sessions/types.ts +84 -0
- package/src/changelog/parse.ts +328 -0
- package/src/chat-threads/emitter.ts +22 -0
- package/src/chat-threads/schema.ts +43 -0
- package/src/chat-threads/store.ts +1311 -0
- package/src/checkpoints/index.ts +15 -0
- package/src/checkpoints/service.ts +142 -0
- package/src/checkpoints/store.ts +153 -0
- package/src/cli/add.ts +425 -0
- package/src/cli/agent.ts +424 -0
- package/src/cli/agents.ts +122 -0
- package/src/cli/app-skill.ts +1455 -0
- package/src/cli/atomic-json-file.ts +324 -0
- package/src/cli/audit-agent-web.ts +324 -0
- package/src/cli/changelog.ts +199 -0
- package/src/cli/claude-code-participant.ts +515 -0
- package/src/cli/code-agent-commands.ts +270 -0
- package/src/cli/code-agent-connector.ts +1109 -0
- package/src/cli/code-agent-executor.ts +1970 -0
- package/src/cli/code-agent-output-smoother.ts +140 -0
- package/src/cli/code-agent-runs.concurrent-worker.ts +33 -0
- package/src/cli/code-agent-runs.ts +548 -0
- package/src/cli/code.ts +1805 -0
- package/src/cli/codex-cli-participant.ts +496 -0
- package/src/cli/connect.ts +2714 -0
- package/src/cli/content-local.ts +442 -0
- package/src/cli/context-xray-local.ts +2193 -0
- package/src/cli/create-workspace.ts +30 -0
- package/src/cli/create.ts +2247 -0
- package/src/cli/deploy-build.ts +50 -0
- package/src/cli/design-connect.ts +3187 -0
- package/src/cli/doctor.ts +472 -0
- package/src/cli/eject.ts +1594 -0
- package/src/cli/eval.ts +134 -0
- package/src/cli/gateway-helpers.ts +70 -0
- package/src/cli/index.ts +1217 -0
- package/src/cli/info.ts +105 -0
- package/src/cli/invoke.ts +288 -0
- package/src/cli/mcp-config-writers.ts +857 -0
- package/src/cli/mcp.ts +594 -0
- package/src/cli/migrate.ts +1813 -0
- package/src/cli/migration-codemod.ts +646 -0
- package/src/cli/multi-frontier-runs.ts +1952 -0
- package/src/cli/package-lifecycle.ts +823 -0
- package/src/cli/plan-blocks.ts +143 -0
- package/src/cli/plan-local.ts +3189 -0
- package/src/cli/plan-publish-store.ts +138 -0
- package/src/cli/pr-visual-recap-workflow.ts +2 -0
- package/src/cli/recap.ts +5 -0
- package/src/cli/setup-agents.ts +120 -0
- package/src/cli/skills-content/assets-skill.ts +83 -0
- package/src/cli/skills-content/canvas.ts +153 -0
- package/src/cli/skills-content/connection.ts +52 -0
- package/src/cli/skills-content/content-skill.ts +120 -0
- package/src/cli/skills-content/design-exploration-skill.ts +118 -0
- package/src/cli/skills-content/design-visual-edit-skill.ts +228 -0
- package/src/cli/skills-content/document-quality.ts +196 -0
- package/src/cli/skills-content/exemplar.ts +69 -0
- package/src/cli/skills-content/help.ts +77 -0
- package/src/cli/skills-content/index.ts +22 -0
- package/src/cli/skills-content/local-files.ts +100 -0
- package/src/cli/skills-content/plan-setup-auth.ts +80 -0
- package/src/cli/skills-content/visual-plan-skill.ts +493 -0
- package/src/cli/skills-content/visual-recap-skill.ts +553 -0
- package/src/cli/skills-content/visualize-repo-skill.ts +93 -0
- package/src/cli/skills-content/wireframe.ts +349 -0
- package/src/cli/skills.ts +4340 -0
- package/src/cli/sync-builder-starter-manifest.ts +631 -0
- package/src/cli/telemetry.ts +136 -0
- package/src/cli/templates-meta.ts +269 -0
- package/src/cli/upgrade.ts +956 -0
- package/src/cli/visualize-repo.ts +715 -0
- package/src/cli/workspace-dev.ts +1486 -0
- package/src/cli/workspacify.ts +258 -0
- package/src/client/AgentAskPopover.tsx +89 -0
- package/src/client/AgentChatHome.tsx +66 -0
- package/src/client/AgentNative.tsx +161 -0
- package/src/client/AgentNativeEmbedded.tsx +288 -0
- package/src/client/AgentNativeFrame.tsx +151 -0
- package/src/client/AgentPanel.tsx +3437 -0
- package/src/client/AgentTaskCard.tsx +273 -0
- package/src/client/AppearancePicker.tsx +79 -0
- package/src/client/AssistantChat.tsx +5793 -0
- package/src/client/ClientOnly.tsx +22 -0
- package/src/client/CommandMenu.tsx +726 -0
- package/src/client/ConnectBuilderCard.tsx +390 -0
- package/src/client/DefaultSpinner.tsx +47 -0
- package/src/client/ErrorBoundary.tsx +359 -0
- package/src/client/ErrorReportActions.tsx +69 -0
- package/src/client/FeedbackButton.tsx +553 -0
- package/src/client/HighlightedCodeBlock.tsx +173 -0
- package/src/client/IframeEmbed.tsx +151 -0
- package/src/client/KeepTabOpenNotice.tsx +181 -0
- package/src/client/MultiTabAssistantChat.tsx +2576 -0
- package/src/client/NewWorkspaceAppFlow.tsx +525 -0
- package/src/client/PoweredByBadge.tsx +284 -0
- package/src/client/RunStuckBanner.tsx +431 -0
- package/src/client/StarfieldBackground.tsx +466 -0
- package/src/client/Turnstile.tsx +125 -0
- package/src/client/active-run-state.ts +139 -0
- package/src/client/agent-chat/index.ts +279 -0
- package/src/client/agent-chat-adapter.ts +3692 -0
- package/src/client/agent-chat.ts +1233 -0
- package/src/client/agent-engine-key.ts +122 -0
- package/src/client/agent-page/AgentContextTab.tsx +487 -0
- package/src/client/agent-page/AgentEmptyState.tsx +44 -0
- package/src/client/agent-page/AgentJobsTab.tsx +441 -0
- package/src/client/agent-page/AgentTabFrame.tsx +39 -0
- package/src/client/agent-page/AgentTabsPage.tsx +1096 -0
- package/src/client/agent-page/types.ts +18 -0
- package/src/client/agent-page/use-jobs.ts +150 -0
- package/src/client/agent-sidebar-state.ts +166 -0
- package/src/client/analytics/index.ts +40 -0
- package/src/client/analytics-session.ts +70 -0
- package/src/client/analytics.ts +1757 -0
- package/src/client/api-path.ts +124 -0
- package/src/client/app-providers.tsx +230 -0
- package/src/client/appearance.ts +130 -0
- package/src/client/application-state.ts +154 -0
- package/src/client/assistant-ui-recovery.tsx +205 -0
- package/src/client/automation/index.ts +4 -0
- package/src/client/automation.ts +22 -0
- package/src/client/blocks/AiEditableField.tsx +30 -0
- package/src/client/blocks/BlockView.tsx +145 -0
- package/src/client/blocks/RegistryBlockDataProvider.tsx +121 -0
- package/src/client/blocks/SchemaBlockEditor.tsx +457 -0
- package/src/client/blocks/agent.ts +95 -0
- package/src/client/blocks/code-block-direction.ts +21 -0
- package/src/client/blocks/index.ts +308 -0
- package/src/client/blocks/library/AnnotatedCodeBlock.tsx +710 -0
- package/src/client/blocks/library/ApiEndpointBlock.tsx +1120 -0
- package/src/client/blocks/library/DataModelBlock.tsx +775 -0
- package/src/client/blocks/library/DiffBlock.tsx +1854 -0
- package/src/client/blocks/library/FileTreeBlock.tsx +868 -0
- package/src/client/blocks/library/HighlightedCode.tsx +260 -0
- package/src/client/blocks/library/JsonExplorerBlock.tsx +608 -0
- package/src/client/blocks/library/MermaidBlock.tsx +362 -0
- package/src/client/blocks/library/OpenApiSpecBlock.tsx +998 -0
- package/src/client/blocks/library/annotated-code.config.ts +101 -0
- package/src/client/blocks/library/annotation-rail.tsx +1020 -0
- package/src/client/blocks/library/api-endpoint.config.ts +207 -0
- package/src/client/blocks/library/block-copy.ts +399 -0
- package/src/client/blocks/library/callout.config.ts +57 -0
- package/src/client/blocks/library/callout.tsx +149 -0
- package/src/client/blocks/library/checklist.config.ts +56 -0
- package/src/client/blocks/library/checklist.tsx +208 -0
- package/src/client/blocks/library/code-filename-label.tsx +59 -0
- package/src/client/blocks/library/code-highlight.tsx +178 -0
- package/src/client/blocks/library/code-tabs.config.ts +70 -0
- package/src/client/blocks/library/code-tabs.tsx +606 -0
- package/src/client/blocks/library/code.config.ts +69 -0
- package/src/client/blocks/library/code.tsx +440 -0
- package/src/client/blocks/library/columns.config.ts +85 -0
- package/src/client/blocks/library/columns.tsx +334 -0
- package/src/client/blocks/library/data-model.config.ts +152 -0
- package/src/client/blocks/library/dev-doc-ui.tsx +191 -0
- package/src/client/blocks/library/diagram.config.ts +222 -0
- package/src/client/blocks/library/diagram.tsx +958 -0
- package/src/client/blocks/library/diff.config.ts +115 -0
- package/src/client/blocks/library/file-tree.config.ts +92 -0
- package/src/client/blocks/library/html.config.ts +79 -0
- package/src/client/blocks/library/html.tsx +258 -0
- package/src/client/blocks/library/json-explorer.config.ts +74 -0
- package/src/client/blocks/library/mermaid.config.ts +49 -0
- package/src/client/blocks/library/narrow-container.tsx +37 -0
- package/src/client/blocks/library/openapi-spec.config.ts +66 -0
- package/src/client/blocks/library/question-form.config.ts +127 -0
- package/src/client/blocks/library/question-form.tsx +944 -0
- package/src/client/blocks/library/sanitize-html.ts +343 -0
- package/src/client/blocks/library/server-specs.ts +338 -0
- package/src/client/blocks/library/specs.tsx +308 -0
- package/src/client/blocks/library/table.config.ts +61 -0
- package/src/client/blocks/library/table.tsx +449 -0
- package/src/client/blocks/library/tabs.config.ts +96 -0
- package/src/client/blocks/library/tabs.tsx +665 -0
- package/src/client/blocks/library/wireframe-icons.ts +210 -0
- package/src/client/blocks/library/wireframe-kit.tsx +1850 -0
- package/src/client/blocks/library/wireframe.config.ts +539 -0
- package/src/client/blocks/library/wireframe.tsx +535 -0
- package/src/client/blocks/mdx.ts +364 -0
- package/src/client/blocks/provider.tsx +51 -0
- package/src/client/blocks/registry.ts +83 -0
- package/src/client/blocks/schema-form/introspect.ts +201 -0
- package/src/client/blocks/server.ts +170 -0
- package/src/client/blocks/types.ts +415 -0
- package/src/client/browser-session-bridge.ts +548 -0
- package/src/client/browser-tab-id.ts +36 -0
- package/src/client/builder-frame.ts +194 -0
- package/src/client/builder-mark.tsx +21 -0
- package/src/client/changelog/Changelog.tsx +333 -0
- package/src/client/changelog/index.ts +1 -0
- package/src/client/chat/ChatHistoryList.tsx +7 -0
- package/src/client/chat/attachment-adapters.ts +426 -0
- package/src/client/chat/connectors.ts +1579 -0
- package/src/client/chat/index.ts +160 -0
- package/src/client/chat/markdown-renderer.tsx +693 -0
- package/src/client/chat/message-components.tsx +1326 -0
- package/src/client/chat/repo-helpers.ts +281 -0
- package/src/client/chat/run-recovery.tsx +994 -0
- package/src/client/chat/runtime.ts +1810 -0
- package/src/client/chat/tool-call-display.tsx +1270 -0
- package/src/client/chat/tool-render-registry.tsx +144 -0
- package/src/client/chat/use-agent-chat-lifecycle-tracking.ts +74 -0
- package/src/client/chat/use-reconnect-reader-owner.ts +28 -0
- package/src/client/chat/widgets/DataChartRenderer.tsx +242 -0
- package/src/client/chat/widgets/DataChartWidget.tsx +51 -0
- package/src/client/chat/widgets/DataInsightsWidget.tsx +59 -0
- package/src/client/chat/widgets/DataTableWidget.tsx +240 -0
- package/src/client/chat/widgets/InlineExtensionWidget.tsx +60 -0
- package/src/client/chat/widgets/builtin-tool-renderers.tsx +213 -0
- package/src/client/chat/widgets/data-widget-types.ts +1 -0
- package/src/client/chat/widgets/inline-extension-result.ts +86 -0
- package/src/client/chat-model-groups.ts +171 -0
- package/src/client/chat-thread-url.ts +11 -0
- package/src/client/chat-view-transition.ts +213 -0
- package/src/client/client-action.ts +21 -0
- package/src/client/client-bootstrap.ts +7 -0
- package/src/client/client-surface.ts +41 -0
- package/src/client/clipboard.ts +89 -0
- package/src/client/code-agent-chat-adapter.ts +537 -0
- package/src/client/collab/index.ts +60 -0
- package/src/client/command-navigation/index.ts +15 -0
- package/src/client/comments-review/index.ts +1 -0
- package/src/client/components/AgentPresenceChip.tsx +4 -0
- package/src/client/components/ApiKeySettings.tsx +309 -0
- package/src/client/components/CodeAgentIndicator.tsx +59 -0
- package/src/client/components/CodeRequiredDialog.tsx +470 -0
- package/src/client/components/LiveCursorOverlay.tsx +5 -0
- package/src/client/components/MissingKeyCard.tsx +61 -0
- package/src/client/components/PresenceBar.tsx +4 -0
- package/src/client/components/RecentEditHighlights.tsx +4 -0
- package/src/client/components/RemoteSelectionRings.tsx +5 -0
- package/src/client/components/icons/AgentNativeIcon.tsx +46 -0
- package/src/client/components/ui/dialog.tsx +1 -0
- package/src/client/components/ui/dropdown-menu.tsx +1 -0
- package/src/client/components/ui/hover-card.tsx +1 -0
- package/src/client/components/ui/message-scroller.tsx +131 -0
- package/src/client/components/ui/popover.tsx +1 -0
- package/src/client/components/ui/sheet.tsx +1 -0
- package/src/client/components/ui/tooltip.tsx +1 -0
- package/src/client/composer/index.ts +11 -0
- package/src/client/composer/runtime-adapters.tsx +123 -0
- package/src/client/composer/use-mention-search.ts +7 -0
- package/src/client/composer/wired-components.tsx +53 -0
- package/src/client/context-xray/ContextMeter.tsx +124 -0
- package/src/client/context-xray/ContextSegmentRow.tsx +27 -0
- package/src/client/context-xray/ContextTreemap.tsx +24 -0
- package/src/client/context-xray/ContextXRayPanel.tsx +59 -0
- package/src/client/context-xray/SegmentProvenancePopover.tsx +18 -0
- package/src/client/context-xray/format.ts +27 -0
- package/src/client/conversation/AgentConversation.tsx +555 -0
- package/src/client/conversation/code-agent-transcript.ts +283 -0
- package/src/client/conversation/index.ts +22 -0
- package/src/client/conversation/types.ts +85 -0
- package/src/client/conversation/use-near-bottom-autoscroll.ts +278 -0
- package/src/client/create-query-client.ts +86 -0
- package/src/client/db-admin/DataGrid.tsx +551 -0
- package/src/client/db-admin/DbAdminPage.tsx +218 -0
- package/src/client/db-admin/DevDatabaseLink.tsx +16 -0
- package/src/client/db-admin/EditableCell.tsx +451 -0
- package/src/client/db-admin/FilterBar.tsx +203 -0
- package/src/client/db-admin/ResultsGrid.tsx +114 -0
- package/src/client/db-admin/RowSidePanel.tsx +337 -0
- package/src/client/db-admin/SqlEditor.tsx +840 -0
- package/src/client/db-admin/TableBrowser.tsx +212 -0
- package/src/client/db-admin/TableEditor.tsx +760 -0
- package/src/client/db-admin/cell-format.ts +246 -0
- package/src/client/db-admin/changeset.ts +296 -0
- package/src/client/db-admin/export-utils.ts +78 -0
- package/src/client/db-admin/index.ts +33 -0
- package/src/client/db-admin/sql-storage.ts +134 -0
- package/src/client/db-admin/storage.ts +68 -0
- package/src/client/db-admin/useAgentSync.ts +226 -0
- package/src/client/db-admin/useDbAdmin.ts +294 -0
- package/src/client/dev-overlay/DevOverlay.tsx +576 -0
- package/src/client/dev-overlay/builtins.ts +40 -0
- package/src/client/dev-overlay/index.ts +23 -0
- package/src/client/dev-overlay/registry.ts +67 -0
- package/src/client/dev-overlay/types.ts +64 -0
- package/src/client/dev-overlay/use-dev-option.ts +78 -0
- package/src/client/dev-overlay/use-dev-overlay-shortcut.ts +31 -0
- package/src/client/dynamic-suggestions.ts +455 -0
- package/src/client/embed-auth.ts +544 -0
- package/src/client/embed.ts +54 -0
- package/src/client/error-capture.ts +690 -0
- package/src/client/error-format.ts +211 -0
- package/src/client/error-reporting.ts +100 -0
- package/src/client/extensions/AgentNativeExtensionFrame.e2e-host.tsx +136 -0
- package/src/client/extensions/AgentNativeExtensionFrame.tsx +708 -0
- package/src/client/extensions/EmbeddedExtension.tsx +604 -0
- package/src/client/extensions/ExtensionEditor.tsx +450 -0
- package/src/client/extensions/ExtensionQueryErrorState.tsx +47 -0
- package/src/client/extensions/ExtensionSlot.tsx +345 -0
- package/src/client/extensions/ExtensionViewer.tsx +1502 -0
- package/src/client/extensions/ExtensionViewerPage.tsx +32 -0
- package/src/client/extensions/ExtensionsListPage.tsx +432 -0
- package/src/client/extensions/ExtensionsSidebarSection.tsx +1259 -0
- package/src/client/extensions/InlineExtensionFrame.tsx +660 -0
- package/src/client/extensions/agent-native-extension-runtime.ts +495 -0
- package/src/client/extensions/delete-extension.ts +76 -0
- package/src/client/extensions/extension-load-error.ts +26 -0
- package/src/client/extensions/extension-order.ts +49 -0
- package/src/client/extensions/extension-popularity.ts +61 -0
- package/src/client/extensions/iframe-bridge.ts +377 -0
- package/src/client/extensions/index.ts +84 -0
- package/src/client/extensions/portable-extension.ts +712 -0
- package/src/client/feature-flags/FeatureFlagsPanel.tsx +519 -0
- package/src/client/feature-flags/helpers.ts +77 -0
- package/src/client/feature-flags/index.ts +13 -0
- package/src/client/feature-flags/types.ts +29 -0
- package/src/client/feature-flags/use-feature-flag.ts +26 -0
- package/src/client/feedback-context.ts +90 -0
- package/src/client/frame-protocol.ts +168 -0
- package/src/client/frame.ts +329 -0
- package/src/client/guided-questions.tsx +1152 -0
- package/src/client/history/VersionHistoryPanel.tsx +175 -0
- package/src/client/history/index.ts +21 -0
- package/src/client/history/use-history.ts +114 -0
- package/src/client/hooks/index.ts +46 -0
- package/src/client/host/index.ts +170 -0
- package/src/client/host-bridge.ts +1272 -0
- package/src/client/host-tools.ts +190 -0
- package/src/client/i18n.tsx +798 -0
- package/src/client/index.ts +370 -0
- package/src/client/integrations/IntegrationCard.tsx +186 -0
- package/src/client/integrations/IntegrationsPanel.tsx +633 -0
- package/src/client/integrations/api.ts +295 -0
- package/src/client/integrations/index.ts +29 -0
- package/src/client/integrations/useIntegrationStatus.ts +52 -0
- package/src/client/mcp-app-host.ts +720 -0
- package/src/client/mcp-apps/McpAppRenderer.tsx +760 -0
- package/src/client/navigation/index.ts +54 -0
- package/src/client/notifications/NotificationsBell.tsx +383 -0
- package/src/client/notifications/index.ts +1 -0
- package/src/client/observability/ObservabilityDashboard.tsx +890 -0
- package/src/client/observability/ThumbsFeedback.tsx +160 -0
- package/src/client/observability/index.ts +20 -0
- package/src/client/observability/useObservability.ts +266 -0
- package/src/client/onboarding/OnboardingBanner.tsx +73 -0
- package/src/client/onboarding/OnboardingPanel.tsx +1036 -0
- package/src/client/onboarding/SetupButton.tsx +64 -0
- package/src/client/onboarding/index.ts +25 -0
- package/src/client/onboarding/use-onboarding.ts +164 -0
- package/src/client/onboarding/use-preview-mode.ts +40 -0
- package/src/client/org/InvitationBanner.tsx +148 -0
- package/src/client/org/OrgSwitcher.tsx +754 -0
- package/src/client/org/RequireActiveOrg.tsx +337 -0
- package/src/client/org/TeamPage.tsx +1432 -0
- package/src/client/org/hooks.ts +359 -0
- package/src/client/org/index.ts +70 -0
- package/src/client/org/workspace-app-links.ts +367 -0
- package/src/client/org-team/index.ts +1 -0
- package/src/client/progress/RunsTray.tsx +680 -0
- package/src/client/progress/index.ts +1 -0
- package/src/client/require-session.tsx +142 -0
- package/src/client/resources/BuiltinCapabilityDetail.tsx +258 -0
- package/src/client/resources/McpConnectionSuggestion.tsx +276 -0
- package/src/client/resources/McpIntegrationDialog.tsx +808 -0
- package/src/client/resources/McpServerDetail.tsx +230 -0
- package/src/client/resources/ResourceEditor.tsx +872 -0
- package/src/client/resources/ResourceTree.tsx +860 -0
- package/src/client/resources/ResourcesPanel.tsx +1960 -0
- package/src/client/resources/index.ts +25 -0
- package/src/client/resources/mcp-integration-catalog.ts +771 -0
- package/src/client/resources/mcp-integration-logos.ts +108 -0
- package/src/client/resources/runtime.ts +1 -0
- package/src/client/resources/use-builtin-capabilities.ts +113 -0
- package/src/client/resources/use-mcp-servers.ts +217 -0
- package/src/client/resources/use-resources.ts +402 -0
- package/src/client/review/ReviewCommentComposer.tsx +123 -0
- package/src/client/review/ReviewStatusBadge.tsx +39 -0
- package/src/client/review/ReviewThreadPanel.tsx +625 -0
- package/src/client/review/index.ts +38 -0
- package/src/client/review/use-review.ts +186 -0
- package/src/client/route-chunk-recovery/index.ts +1 -0
- package/src/client/route-chunk-recovery.ts +331 -0
- package/src/client/route-state.ts +455 -0
- package/src/client/route-warmup.tsx +543 -0
- package/src/client/session-replay-iframe.e2e-host.tsx +100 -0
- package/src/client/session-replay.ts +3417 -0
- package/src/client/settings/AgentsSection.tsx +441 -0
- package/src/client/settings/AutomationsSection.tsx +516 -0
- package/src/client/settings/BackgroundAgentSection.tsx +189 -0
- package/src/client/settings/BrowserSection.tsx +110 -0
- package/src/client/settings/DemoModeSection.tsx +48 -0
- package/src/client/settings/SecretsSection.tsx +875 -0
- package/src/client/settings/SettingsPanel.tsx +3162 -0
- package/src/client/settings/SettingsSection.tsx +283 -0
- package/src/client/settings/SettingsTabsPage.tsx +592 -0
- package/src/client/settings/UsageSection.tsx +401 -0
- package/src/client/settings/VoiceTranscriptionSection.tsx +960 -0
- package/src/client/settings/agent-settings-search.ts +220 -0
- package/src/client/settings/index.ts +27 -0
- package/src/client/settings/useBuilderStatus.ts +1055 -0
- package/src/client/setup-connections/BuilderConnectCard.tsx +89 -0
- package/src/client/setup-connections/ProviderReadinessBadge.tsx +73 -0
- package/src/client/setup-connections/SetupConnectionsPage.tsx +94 -0
- package/src/client/setup-connections/catalog.ts +6 -0
- package/src/client/setup-connections/index.ts +47 -0
- package/src/client/setup-connections/runtime.ts +1 -0
- package/src/client/sharing/ShareButton.tsx +1748 -0
- package/src/client/sharing/ShareDialog.tsx +846 -0
- package/src/client/sharing/VisibilityBadge.tsx +4 -0
- package/src/client/sharing/index.ts +2 -0
- package/src/client/sse-event-processor.ts +2002 -0
- package/src/client/terminal/AgentTerminal.tsx +488 -0
- package/src/client/terminal/index.ts +7 -0
- package/src/client/theme.ts +27 -0
- package/src/client/tombstone/agent-presence-chip.ts +17 -0
- package/src/client/tombstone/editor.ts +265 -0
- package/src/client/tombstone/live-cursor-overlay.ts +21 -0
- package/src/client/tombstone/presence-bar.ts +17 -0
- package/src/client/tombstone/recent-edit-highlights.ts +17 -0
- package/src/client/tombstone/remote-selection-rings.ts +21 -0
- package/src/client/tombstone/rich-markdown-editor.ts +265 -0
- package/src/client/tombstone/ui-dialog.ts +49 -0
- package/src/client/tombstone/ui-dropdown-menu.ts +69 -0
- package/src/client/tombstone/ui-hover-card.ts +21 -0
- package/src/client/tombstone/ui-popover.ts +25 -0
- package/src/client/tombstone/ui-sheet.ts +49 -0
- package/src/client/tombstone/ui-tooltip.ts +29 -0
- package/src/client/tombstone/visual-style-controls.ts +61 -0
- package/src/client/tool-cells/BashCell.tsx +164 -0
- package/src/client/tool-cells/EditCell.tsx +303 -0
- package/src/client/tool-cells/FilesChangedSummary.tsx +208 -0
- package/src/client/tool-cells/WriteCell.tsx +145 -0
- package/src/client/tool-cells/index.ts +7 -0
- package/src/client/tool-display.ts +41 -0
- package/src/client/track.ts +57 -0
- package/src/client/transcription/BuilderTranscriptionCta.tsx +120 -0
- package/src/client/transcription/use-live-transcription.ts +292 -0
- package/src/client/ui/index.ts +55 -0
- package/src/client/uploads/index.ts +6 -0
- package/src/client/uploads/upload-editor-image.ts +79 -0
- package/src/client/uploads/use-upload-resource.ts +24 -0
- package/src/client/url-scrub.ts +51 -0
- package/src/client/use-action.ts +766 -0
- package/src/client/use-agent-chat-context.ts +63 -0
- package/src/client/use-agent-chat-home-handoff.ts +207 -0
- package/src/client/use-agent-chat.ts +53 -0
- package/src/client/use-agent-engine-configured.ts +211 -0
- package/src/client/use-avatar.ts +127 -0
- package/src/client/use-change-version.ts +150 -0
- package/src/client/use-chat-models.ts +252 -0
- package/src/client/use-chat-threads.ts +1262 -0
- package/src/client/use-db-sync.ts +1112 -0
- package/src/client/use-demo-mode-status.ts +30 -0
- package/src/client/use-dev-mode.ts +177 -0
- package/src/client/use-external-value.ts +40 -0
- package/src/client/use-pausing-interval.ts +65 -0
- package/src/client/use-pinch-zoom.ts +202 -0
- package/src/client/use-run-stuck-detection.ts +427 -0
- package/src/client/use-send-to-agent-chat.ts +85 -0
- package/src/client/use-session.ts +114 -0
- package/src/client/utils.ts +7 -0
- package/src/client/visual-style-controls.tsx +1 -0
- package/src/client/vite-dev-recovery-script.ts +226 -0
- package/src/client/voice-provider-status.ts +44 -0
- package/src/client/widgets/index.ts +41 -0
- package/src/code-agents/background-controller.ts +398 -0
- package/src/code-agents/background-run.ts +156 -0
- package/src/code-agents/index.ts +95 -0
- package/src/code-agents/prompt-attachments.ts +39 -0
- package/src/code-agents/transcript-normalizer.ts +750 -0
- package/src/code-agents/transcript-order.ts +73 -0
- package/src/coding-tools/index.ts +742 -0
- package/src/coding-tools/run-code.ts +1662 -0
- package/src/coding-tools/sandbox/adapter.ts +82 -0
- package/src/coding-tools/sandbox/background.ts +406 -0
- package/src/coding-tools/sandbox/executions-store.ts +517 -0
- package/src/coding-tools/sandbox/index.ts +132 -0
- package/src/coding-tools/sandbox/local-child-process-adapter.ts +217 -0
- package/src/collab/agent-identity.ts +5 -0
- package/src/collab/agent-presence.ts +341 -0
- package/src/collab/awareness-store.ts +196 -0
- package/src/collab/awareness.ts +317 -0
- package/src/collab/client-struct.ts +366 -0
- package/src/collab/client.ts +1205 -0
- package/src/collab/emitter.ts +31 -0
- package/src/collab/follow-mode.ts +107 -0
- package/src/collab/index.ts +151 -0
- package/src/collab/json-to-yjs.ts +504 -0
- package/src/collab/presence.ts +207 -0
- package/src/collab/recent-edits.ts +215 -0
- package/src/collab/routes.ts +191 -0
- package/src/collab/storage.ts +228 -0
- package/src/collab/struct-routes.ts +133 -0
- package/src/collab/text-to-yjs.ts +79 -0
- package/src/collab/undo.ts +432 -0
- package/src/collab/xml-ops.ts +68 -0
- package/src/collab/ydoc-manager.ts +544 -0
- package/src/command-navigation/actions.ts +100 -0
- package/src/command-navigation/index.ts +12 -0
- package/src/comments-review/index.ts +1 -0
- package/src/connections/catalog.ts +451 -0
- package/src/connections/index.ts +48 -0
- package/src/connections/reader.ts +911 -0
- package/src/connections/runtime.ts +1 -0
- package/src/credentials/index.ts +187 -0
- package/src/data-programs/actions.ts +335 -0
- package/src/data-programs/contract.ts +284 -0
- package/src/data-programs/execute.ts +693 -0
- package/src/data-programs/index.ts +72 -0
- package/src/data-programs/schema.ts +148 -0
- package/src/data-programs/store.ts +755 -0
- package/src/data-widgets/index.ts +376 -0
- package/src/db/client.ts +1627 -0
- package/src/db/create-get-db.ts +579 -0
- package/src/db/ddl-guard.ts +302 -0
- package/src/db/drizzle-config.ts +190 -0
- package/src/db/ensure-additive-columns.ts +430 -0
- package/src/db/index.ts +57 -0
- package/src/db/migrations.ts +691 -0
- package/src/db/request-telemetry.ts +154 -0
- package/src/db/runtime-diagnostics.ts +331 -0
- package/src/db/schema.ts +121 -0
- package/src/db/widen-columns.ts +75 -0
- package/src/db-admin/agent-tools.ts +202 -0
- package/src/db-admin/index.ts +26 -0
- package/src/db-admin/operations.ts +803 -0
- package/src/db-admin/routes.ts +174 -0
- package/src/db-admin/types.ts +110 -0
- package/src/demo/browser-state.ts +47 -0
- package/src/demo/fetch-interceptor.ts +175 -0
- package/src/demo/redact.ts +575 -0
- package/src/deploy/build.ts +3935 -0
- package/src/deploy/immutable-assets.ts +65 -0
- package/src/deploy/route-discovery.ts +458 -0
- package/src/deploy/workspace-core.ts +282 -0
- package/src/deploy/workspace-deploy.ts +1488 -0
- package/src/eject/provider-api-definitions.ts +193 -0
- package/src/eject/remote-mcp-presets.ts +137 -0
- package/src/eject/setup-readiness-ui.tsx +29 -0
- package/src/eject/workspace-connections.ts +92 -0
- package/src/embedding/agent.ts +159 -0
- package/src/embedding/bridge.ts +297 -0
- package/src/embedding/index.ts +43 -0
- package/src/embedding/protocol.ts +185 -0
- package/src/embedding/react.tsx +319 -0
- package/src/embeddings/index.ts +233 -0
- package/src/eval/agent-runner.ts +210 -0
- package/src/eval/define-eval.ts +55 -0
- package/src/eval/index.ts +49 -0
- package/src/eval/report.ts +75 -0
- package/src/eval/runner.ts +369 -0
- package/src/eval/scorer.ts +244 -0
- package/src/eval/types.ts +188 -0
- package/src/event-bus/bus.ts +140 -0
- package/src/event-bus/index.ts +3 -0
- package/src/event-bus/registry.ts +79 -0
- package/src/event-bus/types.ts +29 -0
- package/src/extensions/actions.ts +1636 -0
- package/src/extensions/change-marker.ts +54 -0
- package/src/extensions/content-patch.ts +468 -0
- package/src/extensions/fetch-tool.ts +461 -0
- package/src/extensions/html-shell.ts +803 -0
- package/src/extensions/local.ts +462 -0
- package/src/extensions/path.ts +40 -0
- package/src/extensions/proxy-security.ts +171 -0
- package/src/extensions/routes.ts +1080 -0
- package/src/extensions/schema.ts +269 -0
- package/src/extensions/session-replay-iframe.ts +131 -0
- package/src/extensions/slots/routes.ts +126 -0
- package/src/extensions/slots/schema.ts +87 -0
- package/src/extensions/slots/store.ts +450 -0
- package/src/extensions/store.ts +1347 -0
- package/src/extensions/theme.ts +114 -0
- package/src/extensions/url-safety.ts +316 -0
- package/src/extensions/web-content.ts +663 -0
- package/src/extensions/web-search-tool.ts +437 -0
- package/src/feature-flags/a2a-action-route.ts +56 -0
- package/src/feature-flags/actions/get-feature-flags.ts +24 -0
- package/src/feature-flags/actions/list-feature-flags.ts +69 -0
- package/src/feature-flags/actions/set-feature-flag.ts +89 -0
- package/src/feature-flags/index.ts +21 -0
- package/src/feature-flags/permissions.ts +53 -0
- package/src/feature-flags/plugin.ts +44 -0
- package/src/feature-flags/registry.ts +96 -0
- package/src/feature-flags/store.ts +176 -0
- package/src/file-upload/actions/upload-image.ts +213 -0
- package/src/file-upload/builder.ts +431 -0
- package/src/file-upload/index.ts +24 -0
- package/src/file-upload/pre-upload-attachments.ts +305 -0
- package/src/file-upload/registry.ts +125 -0
- package/src/file-upload/types.ts +89 -0
- package/src/guards/db-tool-scoping.ts +129 -0
- package/src/guards/index.ts +23 -0
- package/src/guards/no-drizzle-push.ts +116 -0
- package/src/guards/no-env-credentials.ts +229 -0
- package/src/guards/no-env-mutation.ts +114 -0
- package/src/guards/no-localhost-fallback.ts +124 -0
- package/src/guards/no-unscoped-credentials.ts +227 -0
- package/src/guards/no-unscoped-queries.ts +834 -0
- package/src/guards/scan-utils.ts +105 -0
- package/src/guards/types.ts +30 -0
- package/src/history/actions/create-resource-version.ts +92 -0
- package/src/history/actions/get-resource-version.ts +60 -0
- package/src/history/actions/list-resource-history.ts +53 -0
- package/src/history/actions/list-resource-versions.ts +43 -0
- package/src/history/actions/restore-resource-version.ts +96 -0
- package/src/history/index.ts +26 -0
- package/src/history/registry.ts +111 -0
- package/src/history/store.ts +379 -0
- package/src/history/types.ts +69 -0
- package/src/index.browser.ts +155 -0
- package/src/index.ts +408 -0
- package/src/ingestion/docx.ts +158 -0
- package/src/ingestion/figma-node-to-html.ts +1860 -0
- package/src/ingestion/figma.ts +548 -0
- package/src/ingestion/index.ts +107 -0
- package/src/ingestion/media.ts +264 -0
- package/src/ingestion/notion.ts +121 -0
- package/src/ingestion/office.ts +213 -0
- package/src/ingestion/orchestration.ts +160 -0
- package/src/ingestion/pptx.ts +331 -0
- package/src/ingestion/selection.ts +81 -0
- package/src/ingestion/website.ts +151 -0
- package/src/integrations/a2a-continuation-marker.ts +2 -0
- package/src/integrations/a2a-continuation-processor.ts +888 -0
- package/src/integrations/a2a-continuations-store.ts +643 -0
- package/src/integrations/adapter-overrides.ts +12 -0
- package/src/integrations/adapters/discord.ts +364 -0
- package/src/integrations/adapters/email.ts +1164 -0
- package/src/integrations/adapters/google-docs.ts +371 -0
- package/src/integrations/adapters/index.ts +29 -0
- package/src/integrations/adapters/microsoft-teams.ts +413 -0
- package/src/integrations/adapters/slack.ts +1914 -0
- package/src/integrations/adapters/telegram.ts +386 -0
- package/src/integrations/adapters/whatsapp.ts +358 -0
- package/src/integrations/awaiting-input-store.ts +130 -0
- package/src/integrations/catalog.ts +571 -0
- package/src/integrations/computer-supervision-store.ts +423 -0
- package/src/integrations/computer-supervision.ts +213 -0
- package/src/integrations/config-store.ts +131 -0
- package/src/integrations/controls-store.ts +187 -0
- package/src/integrations/eject/messaging-adapters.ts +85 -0
- package/src/integrations/google-docs-poller.ts +671 -0
- package/src/integrations/identity-links-store.ts +210 -0
- package/src/integrations/identity.ts +196 -0
- package/src/integrations/index.ts +220 -0
- package/src/integrations/installations-store.ts +677 -0
- package/src/integrations/integration-memory.ts +187 -0
- package/src/integrations/internal-token.ts +106 -0
- package/src/integrations/pending-tasks-retry-job.ts +286 -0
- package/src/integrations/pending-tasks-store.ts +468 -0
- package/src/integrations/plugin.ts +3048 -0
- package/src/integrations/remote-commands-store.ts +690 -0
- package/src/integrations/remote-devices-store.ts +492 -0
- package/src/integrations/remote-json-safety.ts +127 -0
- package/src/integrations/remote-push-delivery-job.ts +48 -0
- package/src/integrations/remote-push-delivery.ts +370 -0
- package/src/integrations/remote-push-store.ts +664 -0
- package/src/integrations/remote-retry-job.ts +55 -0
- package/src/integrations/remote-run-events-store.ts +222 -0
- package/src/integrations/remote-types.ts +186 -0
- package/src/integrations/runtime.ts +1 -0
- package/src/integrations/scope-store.ts +498 -0
- package/src/integrations/slack-manifest.ts +78 -0
- package/src/integrations/slack-oauth.ts +344 -0
- package/src/integrations/task-queue-stats.ts +144 -0
- package/src/integrations/thread-mapping-store.ts +134 -0
- package/src/integrations/types.ts +485 -0
- package/src/integrations/usage-budget-store.ts +906 -0
- package/src/integrations/webhook-delivery.ts +55 -0
- package/src/integrations/webhook-handler.ts +1939 -0
- package/src/jobs/actions/list-recurring-jobs.ts +105 -0
- package/src/jobs/actions/manage-recurring-job.ts +85 -0
- package/src/jobs/cron.ts +126 -0
- package/src/jobs/index.ts +9 -0
- package/src/jobs/scheduler.ts +703 -0
- package/src/jobs/tools.ts +367 -0
- package/src/local-artifacts/index.ts +1372 -0
- package/src/localization/actions/get-localization-preference.ts +24 -0
- package/src/localization/actions/set-localization-preference.ts +32 -0
- package/src/localization/default-messages.ts +1009 -0
- package/src/localization/index.ts +2 -0
- package/src/localization/server.ts +131 -0
- package/src/localization/shared.ts +208 -0
- package/src/mcp/actions/call-mcp-tool.ts +19 -0
- package/src/mcp/actions/create-org-service-token.ts +87 -0
- package/src/mcp/actions/list-mcp-tools.ts +18 -0
- package/src/mcp/actions/list-org-service-tokens.ts +44 -0
- package/src/mcp/actions/revoke-org-service-token.ts +30 -0
- package/src/mcp/actions/service-token-access.ts +121 -0
- package/src/mcp/ask-app-inline-tasks.ts +125 -0
- package/src/mcp/build-server.ts +2356 -0
- package/src/mcp/builtin-tools.ts +1506 -0
- package/src/mcp/connect-route.ts +1460 -0
- package/src/mcp/connect-store.ts +680 -0
- package/src/mcp/embed-app.ts +2051 -0
- package/src/mcp/embed-route.ts +84 -0
- package/src/mcp/external-agent-policy.ts +18 -0
- package/src/mcp/index.ts +47 -0
- package/src/mcp/oauth-route.ts +1016 -0
- package/src/mcp/oauth-store.ts +581 -0
- package/src/mcp/oauth-token.ts +227 -0
- package/src/mcp/org-directory.ts +343 -0
- package/src/mcp/route-paths.ts +34 -0
- package/src/mcp/screen-memory-stdio.ts +290 -0
- package/src/mcp/server.ts +507 -0
- package/src/mcp/stdio.ts +328 -0
- package/src/mcp/workspace-resolve.ts +261 -0
- package/src/mcp-client/app-api.ts +136 -0
- package/src/mcp-client/app-result.ts +64 -0
- package/src/mcp-client/builtin-capabilities.ts +146 -0
- package/src/mcp-client/builtin-store.ts +93 -0
- package/src/mcp-client/config.ts +281 -0
- package/src/mcp-client/errors.ts +54 -0
- package/src/mcp-client/hub-client.ts +172 -0
- package/src/mcp-client/hub-routes.ts +198 -0
- package/src/mcp-client/index.ts +504 -0
- package/src/mcp-client/manager.ts +968 -0
- package/src/mcp-client/oauth-client.ts +462 -0
- package/src/mcp-client/oauth-routes.ts +307 -0
- package/src/mcp-client/remote-store.ts +674 -0
- package/src/mcp-client/remote-url.ts +97 -0
- package/src/mcp-client/routes.ts +1093 -0
- package/src/mcp-client/screen-memory-local.ts +461 -0
- package/src/mcp-client/tool-policy.ts +182 -0
- package/src/mcp-client/visibility.ts +56 -0
- package/src/mcp-client/workspace-servers.ts +379 -0
- package/src/navigation/index.ts +170 -0
- package/src/notifications/actions.ts +151 -0
- package/src/notifications/channels.ts +410 -0
- package/src/notifications/index.ts +22 -0
- package/src/notifications/registry.ts +216 -0
- package/src/notifications/routes.ts +106 -0
- package/src/notifications/store.ts +236 -0
- package/src/notifications/types.ts +52 -0
- package/src/oauth-tokens/google-refresh.ts +175 -0
- package/src/oauth-tokens/index.ts +15 -0
- package/src/oauth-tokens/store.ts +384 -0
- package/src/observability/cleanup-job.ts +124 -0
- package/src/observability/evals.ts +507 -0
- package/src/observability/experiments.ts +359 -0
- package/src/observability/feedback.ts +332 -0
- package/src/observability/hosted-model-experiment.ts +118 -0
- package/src/observability/index.ts +62 -0
- package/src/observability/plugin.ts +31 -0
- package/src/observability/routes.ts +431 -0
- package/src/observability/sentiment.ts +261 -0
- package/src/observability/store.ts +1347 -0
- package/src/observability/traces.ts +789 -0
- package/src/observability/tracing.ts +157 -0
- package/src/observability/tracking-identity.ts +55 -0
- package/src/observability/types.ts +211 -0
- package/src/onboarding/default-steps.ts +428 -0
- package/src/onboarding/index.ts +22 -0
- package/src/onboarding/plugin.ts +262 -0
- package/src/onboarding/registry.ts +47 -0
- package/src/onboarding/types.ts +96 -0
- package/src/org/accept-pending.ts +98 -0
- package/src/org/auto-join-domain.ts +113 -0
- package/src/org/context.ts +614 -0
- package/src/org/free-email-providers.ts +124 -0
- package/src/org/handlers.ts +1153 -0
- package/src/org/index.ts +63 -0
- package/src/org/migrations.ts +105 -0
- package/src/org/permissions.ts +30 -0
- package/src/org/plugin.ts +255 -0
- package/src/org/schema.ts +28 -0
- package/src/org/types.ts +57 -0
- package/src/org-team/index.ts +1 -0
- package/src/package-lifecycle/deprecated-imports.ts +179 -0
- package/src/package-lifecycle/eject-manifest.ts +345 -0
- package/src/package-lifecycle/index.ts +7 -0
- package/src/package-lifecycle/manifest.ts +120 -0
- package/src/package-lifecycle/migration-manifest.ts +159 -0
- package/src/package-lifecycle/migration-message.ts +6 -0
- package/src/package-lifecycle/tombstone.ts +58 -0
- package/src/package-lifecycle/upgrade-error.ts +37 -0
- package/src/private-blob/index.ts +18 -0
- package/src/private-blob/registry.ts +242 -0
- package/src/private-blob/types.ts +55 -0
- package/src/progress/actions.ts +179 -0
- package/src/progress/index.ts +16 -0
- package/src/progress/registry.ts +127 -0
- package/src/progress/routes.ts +89 -0
- package/src/progress/store.ts +353 -0
- package/src/progress/types.ts +59 -0
- package/src/provider-api/actions/custom-provider-registration.ts +197 -0
- package/src/provider-api/actions/delete-staged-dataset.ts +5 -0
- package/src/provider-api/actions/github-repo-files.ts +266 -0
- package/src/provider-api/actions/list-staged-datasets.ts +5 -0
- package/src/provider-api/actions/provider-api-audit.ts +88 -0
- package/src/provider-api/actions/provider-api.ts +460 -0
- package/src/provider-api/actions/query-staged-dataset.ts +5 -0
- package/src/provider-api/actions/staged-datasets.ts +359 -0
- package/src/provider-api/corpus-jobs-store.ts +522 -0
- package/src/provider-api/corpus-jobs.ts +1817 -0
- package/src/provider-api/custom-registry.ts +456 -0
- package/src/provider-api/github-repo.ts +675 -0
- package/src/provider-api/gong.ts +108 -0
- package/src/provider-api/index.ts +5444 -0
- package/src/provider-api/quota-governor.ts +562 -0
- package/src/provider-api/runtime.ts +1 -0
- package/src/provider-api/staged-datasets-aggregate.ts +304 -0
- package/src/provider-api/staged-datasets-store.ts +441 -0
- package/src/provider-api/staging.ts +483 -0
- package/src/resources/agents.ts +65 -0
- package/src/resources/emitter.ts +54 -0
- package/src/resources/handlers.ts +728 -0
- package/src/resources/index.ts +4 -0
- package/src/resources/metadata.ts +252 -0
- package/src/resources/script-helpers.ts +160 -0
- package/src/resources/store.ts +1618 -0
- package/src/review/actions/consume-review-feedback.ts +48 -0
- package/src/review/actions/create-review-comment.ts +108 -0
- package/src/review/actions/delete-review-comment.ts +67 -0
- package/src/review/actions/get-review-feedback.ts +49 -0
- package/src/review/actions/list-review-comments.ts +87 -0
- package/src/review/actions/reply-review-comment.ts +118 -0
- package/src/review/actions/resolve-review-thread.ts +94 -0
- package/src/review/actions/send-review-thread-to-agent.ts +62 -0
- package/src/review/actions/set-review-status.ts +58 -0
- package/src/review/identity.ts +101 -0
- package/src/review/index.ts +45 -0
- package/src/review/mentions.ts +44 -0
- package/src/review/registry.ts +111 -0
- package/src/review/store.ts +997 -0
- package/src/review/types.ts +101 -0
- package/src/router/index.ts +24 -0
- package/src/scripts/agent-engines/list-agent-engines.ts +130 -0
- package/src/scripts/agent-engines/manage-agent-engine.ts +197 -0
- package/src/scripts/agent-engines/set-agent-engine.ts +84 -0
- package/src/scripts/agent-engines/test-agent-engine.ts +174 -0
- package/src/scripts/call-agent.ts +792 -0
- package/src/scripts/chat/index.ts +8 -0
- package/src/scripts/chat/open-chat.ts +56 -0
- package/src/scripts/chat/search-chats.ts +121 -0
- package/src/scripts/core-scripts.ts +22 -0
- package/src/scripts/db/check-scoping.ts +212 -0
- package/src/scripts/db/exec.ts +791 -0
- package/src/scripts/db/index.ts +19 -0
- package/src/scripts/db/migrate-encrypt-credentials.ts +223 -0
- package/src/scripts/db/migrate-encrypt-oauth-tokens.ts +213 -0
- package/src/scripts/db/migrate-user-api-keys.ts +257 -0
- package/src/scripts/db/patch.ts +820 -0
- package/src/scripts/db/query.ts +299 -0
- package/src/scripts/db/reset-dev-owner.ts +285 -0
- package/src/scripts/db/safety.ts +286 -0
- package/src/scripts/db/schema.ts +364 -0
- package/src/scripts/db/scoping.ts +341 -0
- package/src/scripts/db/sqlite-client.ts +78 -0
- package/src/scripts/db/tool-mode.ts +23 -0
- package/src/scripts/db/tool-schemas.ts +31 -0
- package/src/scripts/db/wipe-leaked-builder-keys.ts +201 -0
- package/src/scripts/dev/index.ts +296 -0
- package/src/scripts/dev/list-files.ts +119 -0
- package/src/scripts/dev/read-file.ts +77 -0
- package/src/scripts/dev/search-files.ts +151 -0
- package/src/scripts/dev/shell.ts +111 -0
- package/src/scripts/dev/write-file.ts +56 -0
- package/src/scripts/dev-session.ts +96 -0
- package/src/scripts/docs/index.ts +8 -0
- package/src/scripts/docs/search.ts +274 -0
- package/src/scripts/docs/source-search.ts +363 -0
- package/src/scripts/index.ts +15 -0
- package/src/scripts/manage-agent-loop-settings.ts +84 -0
- package/src/scripts/parse-args.ts +46 -0
- package/src/scripts/resources/delete-memory.ts +67 -0
- package/src/scripts/resources/delete.ts +69 -0
- package/src/scripts/resources/effective.ts +71 -0
- package/src/scripts/resources/index.ts +18 -0
- package/src/scripts/resources/list.ts +103 -0
- package/src/scripts/resources/migrate-learnings.ts +40 -0
- package/src/scripts/resources/read.ts +107 -0
- package/src/scripts/resources/save-memory.ts +99 -0
- package/src/scripts/resources/write.ts +158 -0
- package/src/scripts/runner.ts +339 -0
- package/src/scripts/utils.ts +130 -0
- package/src/search/index.ts +413 -0
- package/src/search-utils/index.ts +112 -0
- package/src/secrets/crypto.ts +228 -0
- package/src/secrets/index.ts +65 -0
- package/src/secrets/onboarding.ts +101 -0
- package/src/secrets/register-framework-secrets.ts +228 -0
- package/src/secrets/register.ts +129 -0
- package/src/secrets/routes.ts +697 -0
- package/src/secrets/schema.ts +52 -0
- package/src/secrets/storage.ts +561 -0
- package/src/secrets/substitution.ts +305 -0
- package/src/server/__snapshots__/agent-chat-plugin.surface.spec.ts.snap +150 -0
- package/src/server/action-change.ts +66 -0
- package/src/server/action-discovery.ts +774 -0
- package/src/server/action-routes.ts +608 -0
- package/src/server/agent-access.ts +72 -0
- package/src/server/agent-chat/action-filters-a2a.ts +339 -0
- package/src/server/agent-chat/browser-team-tools.ts +510 -0
- package/src/server/agent-chat/context-tools.ts +599 -0
- package/src/server/agent-chat/framework-prompts.ts +467 -0
- package/src/server/agent-chat/lazy-fs.ts +11 -0
- package/src/server/agent-chat/mcp-glue.ts +91 -0
- package/src/server/agent-chat/plugin-options.ts +379 -0
- package/src/server/agent-chat/process-run-failure.ts +62 -0
- package/src/server/agent-chat/prompt-resources.ts +1004 -0
- package/src/server/agent-chat/recurring-jobs-runtime.ts +80 -0
- package/src/server/agent-chat/request-surface.ts +71 -0
- package/src/server/agent-chat/run-code-tools.ts +67 -0
- package/src/server/agent-chat/script-entries.ts +796 -0
- package/src/server/agent-chat/shared-thread.ts +301 -0
- package/src/server/agent-chat/skill-frontmatter.ts +41 -0
- package/src/server/agent-chat-plugin.ts +5930 -0
- package/src/server/agent-discovery.ts +706 -0
- package/src/server/agent-engine-api-key-route.ts +236 -0
- package/src/server/agent-run-context.ts +204 -0
- package/src/server/agent-teams-run-queue.ts +344 -0
- package/src/server/agent-teams.ts +2257 -0
- package/src/server/agents-bundle.ts +464 -0
- package/src/server/analytics.ts +105 -0
- package/src/server/app-base-path.ts +79 -0
- package/src/server/app-name.ts +53 -0
- package/src/server/app-url.ts +184 -0
- package/src/server/attachment-actions.ts +276 -0
- package/src/server/attribution.ts +214 -0
- package/src/server/auth-marketing.ts +260 -0
- package/src/server/auth-plugin.ts +27 -0
- package/src/server/auth.ts +3803 -0
- package/src/server/better-auth-instance.ts +1197 -0
- package/src/server/builder-browser.ts +1701 -0
- package/src/server/builder-design-systems.ts +668 -0
- package/src/server/builder-space.ts +176 -0
- package/src/server/captcha.ts +63 -0
- package/src/server/capture-error.ts +64 -0
- package/src/server/cli-capture.ts +141 -0
- package/src/server/collab-plugin.ts +391 -0
- package/src/server/complete-text.ts +231 -0
- package/src/server/cookie-namespace.ts +159 -0
- package/src/server/core-routes-plugin.ts +4122 -0
- package/src/server/cors-origins.ts +60 -0
- package/src/server/create-server.ts +268 -0
- package/src/server/credential-provider.ts +1202 -0
- package/src/server/csrf.ts +229 -0
- package/src/server/date-utils.ts +42 -0
- package/src/server/deep-link.ts +119 -0
- package/src/server/derived-secret.ts +32 -0
- package/src/server/design-token-utils.ts +1032 -0
- package/src/server/desktop-sso.ts +83 -0
- package/src/server/edge.ts +69 -0
- package/src/server/email-actions.ts +218 -0
- package/src/server/email-template.ts +192 -0
- package/src/server/email-templates.ts +148 -0
- package/src/server/email.ts +219 -0
- package/src/server/embed-route.ts +283 -0
- package/src/server/embed-session.ts +805 -0
- package/src/server/embedded.ts +252 -0
- package/src/server/entry-server.tsx +154 -0
- package/src/server/framework-request-handler.ts +848 -0
- package/src/server/fusion-app.ts +447 -0
- package/src/server/google-auth-mode.ts +34 -0
- package/src/server/google-auth-plugin.ts +496 -0
- package/src/server/google-oauth-credentials.ts +128 -0
- package/src/server/google-oauth.ts +972 -0
- package/src/server/google-realtime-session.ts +182 -0
- package/src/server/h3-helpers.ts +174 -0
- package/src/server/http-response-telemetry.ts +396 -0
- package/src/server/identity-sso-store.ts +304 -0
- package/src/server/identity-sso.ts +514 -0
- package/src/server/index.ts +593 -0
- package/src/server/missing-key.ts +36 -0
- package/src/server/oauth-helpers.ts +44 -0
- package/src/server/oauth-public-origin.ts +44 -0
- package/src/server/oauth-return-url.ts +120 -0
- package/src/server/og-fonts-data.ts +15 -0
- package/src/server/og-fonts.ts +75 -0
- package/src/server/onboarding-html.ts +4040 -0
- package/src/server/open-route.ts +311 -0
- package/src/server/org-admin.ts +28 -0
- package/src/server/poll-events.ts +88 -0
- package/src/server/poll.ts +1224 -0
- package/src/server/prompts/framework-core-compact.ts +107 -0
- package/src/server/prompts/framework-core.ts +143 -0
- package/src/server/prompts/index.ts +16 -0
- package/src/server/prompts/model-overlays.ts +54 -0
- package/src/server/prompts/shared-rules.ts +104 -0
- package/src/server/realtime-voice.ts +967 -0
- package/src/server/recap-image-route.ts +242 -0
- package/src/server/recap-image-store.ts +185 -0
- package/src/server/request-context.ts +416 -0
- package/src/server/request-origin.ts +67 -0
- package/src/server/resources-plugin.ts +119 -0
- package/src/server/schema-prompt.ts +429 -0
- package/src/server/scoped-key-storage.ts +223 -0
- package/src/server/security-headers.ts +170 -0
- package/src/server/self-dispatch.ts +224 -0
- package/src/server/sentry-config.ts +73 -0
- package/src/server/sentry-plugin.ts +138 -0
- package/src/server/sentry.ts +444 -0
- package/src/server/short-lived-token.ts +170 -0
- package/src/server/social-og-image.ts +454 -0
- package/src/server/sse.ts +102 -0
- package/src/server/ssr-handler.ts +466 -0
- package/src/server/transcribe-voice.ts +1076 -0
- package/src/server/voice-providers-status.ts +118 -0
- package/src/server/workspace-oauth.ts +31 -0
- package/src/server/workspace-provider-oauth.ts +970 -0
- package/src/session-replay-iframe-protocol.ts +58 -0
- package/src/settings/handlers.ts +44 -0
- package/src/settings/index.ts +34 -0
- package/src/settings/org-settings.ts +83 -0
- package/src/settings/script-helpers.ts +24 -0
- package/src/settings/store.ts +247 -0
- package/src/settings/user-settings.ts +47 -0
- package/src/setup-connections/index.ts +90 -0
- package/src/setup-connections/onboarding.ts +47 -0
- package/src/shared/agent-access.ts +112 -0
- package/src/shared/agent-chat.ts +124 -0
- package/src/shared/agent-env.ts +46 -0
- package/src/shared/agent-readable-resource.ts +111 -0
- package/src/shared/agent-sidebar-url.ts +39 -0
- package/src/shared/cache-control.ts +25 -0
- package/src/shared/cloudflare-globals.d.ts +34 -0
- package/src/shared/context-xray.ts +172 -0
- package/src/shared/embed-auth.ts +6 -0
- package/src/shared/index.ts +83 -0
- package/src/shared/llm-connection.ts +42 -0
- package/src/shared/markdown-block-split.ts +129 -0
- package/src/shared/mcp-connect-content.ts +130 -0
- package/src/shared/mcp-embed-headers.ts +175 -0
- package/src/shared/mcp-integration-config.ts +101 -0
- package/src/shared/merge-by-id.ts +14 -0
- package/src/shared/oauth-state.ts +39 -0
- package/src/shared/reasoning-effort.ts +228 -0
- package/src/shared/route-warmup-config.ts +128 -0
- package/src/shared/runtime.ts +31 -0
- package/src/shared/social-meta.ts +82 -0
- package/src/shared/streaming-text-smoothing.ts +184 -0
- package/src/shared/truncate.ts +13 -0
- package/src/shared/workspace-app-audience.ts +177 -0
- package/src/shared/workspace-app-id.ts +54 -0
- package/src/sharing/access.ts +585 -0
- package/src/sharing/actions/create-agent-resource-link.ts +91 -0
- package/src/sharing/actions/extension-change.ts +22 -0
- package/src/sharing/actions/list-resource-shares.ts +93 -0
- package/src/sharing/actions/set-resource-visibility.ts +91 -0
- package/src/sharing/actions/share-resource.ts +322 -0
- package/src/sharing/actions/unshare-resource.ts +72 -0
- package/src/sharing/index.ts +35 -0
- package/src/sharing/registry.ts +201 -0
- package/src/sharing/schema.ts +99 -0
- package/src/styles/agent-conversation.css +766 -0
- package/src/styles/agent-native.css +938 -0
- package/src/styles/blocks.css +1717 -0
- package/src/styles/chat-history-list.css +1 -0
- package/src/styles/rich-markdown-editor.css +1 -0
- package/src/tailwind.preset.ts +140 -0
- package/src/templates/chat/.agents/skills/agent-native-docs/SKILL.md +15 -13
- package/src/templates/chat/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/src/templates/chat/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/src/templates/chat/AGENTS.md +3 -2
- package/src/templates/default/.agents/skills/agent-native-docs/SKILL.md +15 -13
- package/src/templates/default/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/src/templates/default/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/src/templates/default/AGENTS.md +6 -5
- package/src/templates/headless/.agents/skills/agent-native-docs/SKILL.md +15 -13
- package/src/templates/headless/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/src/templates/headless/AGENTS.md +5 -4
- package/src/templates/workspace-core/.agents/skills/agent-native-docs/SKILL.md +15 -13
- package/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +3 -2
- package/src/templates/workspace-core/.agents/skills/customizing-agent-native/SKILL.md +63 -20
- package/src/templates/workspace-core/.agents/skills/self-modifying-code/SKILL.md +9 -6
- package/src/templates/workspace-core/AGENTS.md +4 -4
- package/src/templates/workspace-root/AGENTS.md +4 -4
- package/src/terminal/cli-registry.ts +57 -0
- package/src/terminal/index.ts +22 -0
- package/src/terminal/pty-server.ts +389 -0
- package/src/terminal/terminal-plugin.ts +265 -0
- package/src/testing.ts +23 -0
- package/src/tracking/index.ts +10 -0
- package/src/tracking/providers.ts +422 -0
- package/src/tracking/registry.ts +102 -0
- package/src/tracking/route.ts +108 -0
- package/src/tracking/types.ts +16 -0
- package/src/transcription/builder-transcription.ts +118 -0
- package/src/triggers/actions/list-automations.ts +99 -0
- package/src/triggers/actions/manage-automation.ts +78 -0
- package/src/triggers/actions.ts +333 -0
- package/src/triggers/condition-evaluator.ts +163 -0
- package/src/triggers/dispatcher.ts +536 -0
- package/src/triggers/index.ts +13 -0
- package/src/triggers/routes.ts +322 -0
- package/src/triggers/types.ts +42 -0
- package/src/types/pglite.d.ts +26 -0
- package/src/usage/store.ts +698 -0
- package/src/vite/action-types-plugin.ts +481 -0
- package/src/vite/agent-web-plugin.ts +78 -0
- package/src/vite/agents-bundle-plugin.ts +185 -0
- package/src/vite/client.ts +3039 -0
- package/src/vite/index.ts +21 -0
- package/src/voice/index.ts +18 -0
- package/src/voice/voice-cleanup-prompt.ts +38 -0
- package/src/voice/voice-context.ts +217 -0
- package/src/voice/voice-replacements.ts +75 -0
- package/src/workspace-connections/credentials.ts +603 -0
- package/src/workspace-connections/index.ts +72 -0
- package/src/workspace-connections/lifecycle.ts +43 -0
- package/src/workspace-connections/store.ts +1943 -0
- package/src/workspace-files/index.ts +18 -0
- package/src/workspace-files/store.ts +339 -0
- package/src/workspace-files/tool.ts +266 -0
|
@@ -0,0 +1,3189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan helper commands.
|
|
3
|
+
*
|
|
4
|
+
* The `plan local` commands are intentionally separate from the Plan app
|
|
5
|
+
* actions. They do not call MCP, hosted write actions, SQLite, or hosted
|
|
6
|
+
* storage; they only read local files or serve them from a localhost bridge so
|
|
7
|
+
* privacy-focused users have an auditable no-DB path. The top-level
|
|
8
|
+
* `plan blocks` command is a schema-only, no-auth helper for fetching the
|
|
9
|
+
* public block catalog before authoring local MDX; it never sends plan content.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import crypto from "node:crypto";
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import http, {
|
|
16
|
+
type IncomingMessage,
|
|
17
|
+
type Server,
|
|
18
|
+
type ServerResponse,
|
|
19
|
+
} from "node:http";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { pathToFileURL } from "node:url";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_PLAN_APP_URL,
|
|
25
|
+
defaultPlanBlocksOut,
|
|
26
|
+
fetchPlanBlockCatalog,
|
|
27
|
+
normalizePlanBlockFormat,
|
|
28
|
+
planActionEndpoint,
|
|
29
|
+
} from "./plan-blocks.js";
|
|
30
|
+
|
|
31
|
+
type LocalPlanKind = "plan" | "recap";
|
|
32
|
+
|
|
33
|
+
type LocalPlanFiles = {
|
|
34
|
+
dir: string;
|
|
35
|
+
planMdx: string;
|
|
36
|
+
canvasMdx?: string;
|
|
37
|
+
prototypeMdx?: string;
|
|
38
|
+
stateJson?: string;
|
|
39
|
+
assets?: Record<string, string>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type LocalPlanValidationIssue = {
|
|
43
|
+
file: string;
|
|
44
|
+
line: number;
|
|
45
|
+
message: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type LocalPlanPreviewInput = {
|
|
49
|
+
dir: string;
|
|
50
|
+
kind?: LocalPlanKind;
|
|
51
|
+
title?: string;
|
|
52
|
+
brief?: string;
|
|
53
|
+
appUrl?: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type LocalPlanPreviewResult = {
|
|
57
|
+
ok: true;
|
|
58
|
+
dir: string;
|
|
59
|
+
out?: string;
|
|
60
|
+
url: string;
|
|
61
|
+
title: string;
|
|
62
|
+
kind: LocalPlanKind;
|
|
63
|
+
files: string[];
|
|
64
|
+
opened?: boolean;
|
|
65
|
+
openCommand?: string;
|
|
66
|
+
openError?: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type LocalPlanBridgeMdxFolder = {
|
|
70
|
+
"plan.mdx": string;
|
|
71
|
+
"canvas.mdx"?: string;
|
|
72
|
+
"prototype.mdx"?: string;
|
|
73
|
+
".plan-state.json"?: string;
|
|
74
|
+
"assets/"?: Record<string, string>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
type LocalPlanBridgeComment = {
|
|
78
|
+
id: string;
|
|
79
|
+
planId: string;
|
|
80
|
+
parentCommentId: string | null;
|
|
81
|
+
sectionId: string | null;
|
|
82
|
+
kind: string;
|
|
83
|
+
status: string;
|
|
84
|
+
anchor: string | null;
|
|
85
|
+
message: string;
|
|
86
|
+
createdBy: string;
|
|
87
|
+
authorEmail: string | null;
|
|
88
|
+
authorName: string | null;
|
|
89
|
+
resolutionTarget: string | null;
|
|
90
|
+
mentions: unknown[];
|
|
91
|
+
mentionsJson: string | null;
|
|
92
|
+
resolvedBy: string | null;
|
|
93
|
+
resolvedAt: string | null;
|
|
94
|
+
consumedAt: string | null;
|
|
95
|
+
deletedAt: string | null;
|
|
96
|
+
deletedBy: string | null;
|
|
97
|
+
createdAt: string;
|
|
98
|
+
updatedAt: string;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
type LocalPlanBridgeCommentInput = {
|
|
102
|
+
id?: string;
|
|
103
|
+
parentCommentId?: string | null;
|
|
104
|
+
sectionId?: string | null;
|
|
105
|
+
kind?: string;
|
|
106
|
+
status?: string;
|
|
107
|
+
anchor?: string | null;
|
|
108
|
+
message?: string;
|
|
109
|
+
createdBy?: string;
|
|
110
|
+
authorEmail?: string | null;
|
|
111
|
+
authorName?: string | null;
|
|
112
|
+
resolutionTarget?: string | null;
|
|
113
|
+
mentions?: unknown[];
|
|
114
|
+
resolvedBy?: string | null;
|
|
115
|
+
resolvedAt?: string | null;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
type LocalPlanBridgeCommentUpdate = {
|
|
119
|
+
comments?: LocalPlanBridgeCommentInput[];
|
|
120
|
+
deletedCommentIds?: string[];
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
type VisualAnswerSourcePayload = {
|
|
124
|
+
question?: string;
|
|
125
|
+
title?: string;
|
|
126
|
+
brief?: string;
|
|
127
|
+
repoPath?: string;
|
|
128
|
+
sourceUrl?: string;
|
|
129
|
+
sourceType?: string;
|
|
130
|
+
mdx: {
|
|
131
|
+
"plan.mdx": string;
|
|
132
|
+
"canvas.mdx"?: string;
|
|
133
|
+
"prototype.mdx"?: string;
|
|
134
|
+
"assets/"?: Record<string, string>;
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export type PublishVisualAnswerInput = {
|
|
139
|
+
appUrl: string;
|
|
140
|
+
token: string;
|
|
141
|
+
question?: string;
|
|
142
|
+
sourcePath?: string;
|
|
143
|
+
out?: string;
|
|
144
|
+
prevPlanId?: string;
|
|
145
|
+
repo?: string;
|
|
146
|
+
sourceUrl?: string;
|
|
147
|
+
sourceType?: string;
|
|
148
|
+
visibility?: "private" | "org" | "public";
|
|
149
|
+
fetchFn?: typeof fetch;
|
|
150
|
+
cwd?: string;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export type LocalPlanBridgePayload = {
|
|
154
|
+
ok: true;
|
|
155
|
+
version: 1;
|
|
156
|
+
source: "agent-native-local-bridge";
|
|
157
|
+
localOnly: true;
|
|
158
|
+
slug: string;
|
|
159
|
+
dir: string;
|
|
160
|
+
title: string;
|
|
161
|
+
brief: string;
|
|
162
|
+
kind: LocalPlanKind;
|
|
163
|
+
updatedAt: string;
|
|
164
|
+
files: string[];
|
|
165
|
+
mdx: LocalPlanBridgeMdxFolder;
|
|
166
|
+
comments: LocalPlanBridgeComment[];
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export type LocalPlanServeResult = {
|
|
170
|
+
ok: true;
|
|
171
|
+
dir: string;
|
|
172
|
+
url: string;
|
|
173
|
+
urlFile?: string;
|
|
174
|
+
bridgeUrl: string;
|
|
175
|
+
appUrl: string;
|
|
176
|
+
title: string;
|
|
177
|
+
kind: LocalPlanKind;
|
|
178
|
+
files: string[];
|
|
179
|
+
host: string;
|
|
180
|
+
port: number;
|
|
181
|
+
opened?: boolean;
|
|
182
|
+
openCommand?: string;
|
|
183
|
+
openError?: string;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export type LocalPlanBridgeServer = {
|
|
187
|
+
server: Server;
|
|
188
|
+
result: LocalPlanServeResult;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export type RendererValidationIssue = {
|
|
192
|
+
path: string;
|
|
193
|
+
message: string;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export type RendererValidation = {
|
|
197
|
+
/** Whether a loopback renderer validate endpoint was reachable and answered. */
|
|
198
|
+
ran: boolean;
|
|
199
|
+
endpoint: string;
|
|
200
|
+
/** Renderer verdict — present only when `ran` is true. */
|
|
201
|
+
valid?: boolean;
|
|
202
|
+
issues?: RendererValidationIssue[];
|
|
203
|
+
/** Transport/endpoint error when `ran` is false (unreachable, 404, old deploy). */
|
|
204
|
+
error?: string;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export type LocalPlanVerifyResult = {
|
|
208
|
+
ok: boolean;
|
|
209
|
+
dir: string;
|
|
210
|
+
url: string;
|
|
211
|
+
urlFile?: string;
|
|
212
|
+
bridgeUrl: string;
|
|
213
|
+
appUrl: string;
|
|
214
|
+
title: string;
|
|
215
|
+
kind: LocalPlanKind;
|
|
216
|
+
files: string[];
|
|
217
|
+
preflight: {
|
|
218
|
+
status: number;
|
|
219
|
+
allowOrigin: string | null;
|
|
220
|
+
allowPrivateNetwork: string | null;
|
|
221
|
+
};
|
|
222
|
+
bridge: {
|
|
223
|
+
status: number;
|
|
224
|
+
ok: boolean;
|
|
225
|
+
source?: string;
|
|
226
|
+
localOnly?: boolean;
|
|
227
|
+
files?: string[];
|
|
228
|
+
mdxFiles?: string[];
|
|
229
|
+
error?: string;
|
|
230
|
+
};
|
|
231
|
+
validation: RendererValidation;
|
|
232
|
+
warnings: string[];
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
type OpenLocalUrlResult = {
|
|
236
|
+
ok: boolean;
|
|
237
|
+
command: string;
|
|
238
|
+
error?: string;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const LOCAL_PLAN_COMMENTS_FILE = "comments.json";
|
|
242
|
+
const LOCAL_PLAN_OWNER_EMAIL = "local@agent-native.local";
|
|
243
|
+
const LOCAL_PLAN_BRIDGE_COMMENT_BODY_LIMIT = 1024 * 1024;
|
|
244
|
+
|
|
245
|
+
const LOCAL_PLAN_ASSET_MAX_SINGLE_BYTES = 2 * 1024 * 1024;
|
|
246
|
+
const LOCAL_PLAN_ASSET_MAX_TOTAL_BYTES = 10 * 1024 * 1024;
|
|
247
|
+
const AGENT_NATIVE_MANIFEST_FILE = "agent-native.json";
|
|
248
|
+
const VISUAL_ANSWER_SOURCE_FILENAME = "visual-answer-source.json";
|
|
249
|
+
const VISUAL_ANSWER_URL_FILENAME = "visual-answer-url.txt";
|
|
250
|
+
const PLAN_ACTION_HTTP_TIMEOUT_MS = 45_000;
|
|
251
|
+
const LOCAL_PLAN_ASSET_EXTENSIONS = new Set([
|
|
252
|
+
"png",
|
|
253
|
+
"jpg",
|
|
254
|
+
"jpeg",
|
|
255
|
+
"gif",
|
|
256
|
+
"webp",
|
|
257
|
+
"svg",
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
function parseArgs(argv: string[]): Record<string, string | boolean> {
|
|
261
|
+
const out: Record<string, string | boolean> = {};
|
|
262
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
263
|
+
const token = argv[i];
|
|
264
|
+
if (!token.startsWith("--")) continue;
|
|
265
|
+
const key = token.slice(2);
|
|
266
|
+
const next = argv[i + 1];
|
|
267
|
+
if (next === undefined || next.startsWith("--")) out[key] = true;
|
|
268
|
+
else {
|
|
269
|
+
out[key] = next;
|
|
270
|
+
i += 1;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function stringArg(
|
|
277
|
+
args: Record<string, string | boolean>,
|
|
278
|
+
key: string,
|
|
279
|
+
): string {
|
|
280
|
+
const value = args[key];
|
|
281
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
282
|
+
throw new Error(`Missing --${key}`);
|
|
283
|
+
}
|
|
284
|
+
return value;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function optionalArg(
|
|
288
|
+
args: Record<string, string | boolean>,
|
|
289
|
+
key: string,
|
|
290
|
+
): string | undefined {
|
|
291
|
+
const value = args[key];
|
|
292
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function boolArg(args: Record<string, string | boolean>, key: string): boolean {
|
|
296
|
+
return args[key] === true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function sanitizeHttpDetail(value: string, maxLength: number): string {
|
|
300
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
301
|
+
if (normalized.length <= maxLength) return normalized;
|
|
302
|
+
return `${normalized.slice(0, maxLength - 3)}...`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function normalizeVisibility(
|
|
306
|
+
value: string | undefined,
|
|
307
|
+
): "private" | "org" | "public" | undefined {
|
|
308
|
+
if (!value) return undefined;
|
|
309
|
+
if (value === "private" || value === "org" || value === "public") {
|
|
310
|
+
return value;
|
|
311
|
+
}
|
|
312
|
+
throw new Error(
|
|
313
|
+
`Invalid --visibility "${value}" (expected private, org, or public)`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function normalizeVisualAnswerSourceType(value: string | undefined): string {
|
|
318
|
+
if (!value) return "code";
|
|
319
|
+
if (
|
|
320
|
+
[
|
|
321
|
+
"code",
|
|
322
|
+
"pull-request",
|
|
323
|
+
"commit",
|
|
324
|
+
"branch",
|
|
325
|
+
"diff",
|
|
326
|
+
"issue",
|
|
327
|
+
"page",
|
|
328
|
+
].includes(value)
|
|
329
|
+
) {
|
|
330
|
+
return value;
|
|
331
|
+
}
|
|
332
|
+
throw new Error(
|
|
333
|
+
`Invalid --source-type "${value}" (expected code, pull-request, commit, branch, diff, issue, or page)`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function visualAnswerUrlFromPublishResult(
|
|
338
|
+
result: unknown,
|
|
339
|
+
appUrl: string,
|
|
340
|
+
): string | null {
|
|
341
|
+
const base = appUrl.replace(/\/$/, "");
|
|
342
|
+
if (!result || typeof result !== "object") return null;
|
|
343
|
+
const record = result as Record<string, unknown>;
|
|
344
|
+
const rawUrl =
|
|
345
|
+
typeof record.url === "string"
|
|
346
|
+
? record.url
|
|
347
|
+
: record.path && typeof record.path === "string"
|
|
348
|
+
? record.path
|
|
349
|
+
: null;
|
|
350
|
+
if (rawUrl) {
|
|
351
|
+
try {
|
|
352
|
+
return new URL(rawUrl, `${base}/`).toString();
|
|
353
|
+
} catch {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const planId =
|
|
358
|
+
typeof record.planId === "string"
|
|
359
|
+
? record.planId
|
|
360
|
+
: record.plan &&
|
|
361
|
+
typeof record.plan === "object" &&
|
|
362
|
+
typeof (record.plan as Record<string, unknown>).id === "string"
|
|
363
|
+
? ((record.plan as Record<string, unknown>).id as string)
|
|
364
|
+
: null;
|
|
365
|
+
return planId ? `${base}/plans/${encodeURIComponent(planId)}` : null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function fetchActionWithTimeout(
|
|
369
|
+
url: string,
|
|
370
|
+
init: RequestInit,
|
|
371
|
+
fetchFn: typeof fetch,
|
|
372
|
+
): Promise<Response> {
|
|
373
|
+
return await fetchFn(url, {
|
|
374
|
+
...init,
|
|
375
|
+
signal: init.signal ?? AbortSignal.timeout(PLAN_ACTION_HTTP_TIMEOUT_MS),
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function localPlanFolderName(title: string): string {
|
|
380
|
+
const slug = title
|
|
381
|
+
.normalize("NFKD")
|
|
382
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
383
|
+
.toLowerCase()
|
|
384
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
385
|
+
.replace(/^-+|-+$/g, "")
|
|
386
|
+
.slice(0, 64)
|
|
387
|
+
.replace(/-+$/g, "");
|
|
388
|
+
return slug || "untitled-plan";
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function normalizeKind(value: string | undefined): LocalPlanKind {
|
|
392
|
+
if (!value) return "plan";
|
|
393
|
+
if (value === "plan" || value === "recap") return value;
|
|
394
|
+
throw new Error(`Invalid --kind "${value}" (expected plan or recap)`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function normalizeSlash(filePath: string): string {
|
|
398
|
+
return filePath.replace(/\\/g, "/");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function normalizeRelativePath(filePath: string): string | null {
|
|
402
|
+
if (!filePath.trim() || path.isAbsolute(filePath)) return null;
|
|
403
|
+
const normalized = path.posix
|
|
404
|
+
.normalize(normalizeSlash(filePath.trim()))
|
|
405
|
+
.replace(/\/+$/, "");
|
|
406
|
+
if (
|
|
407
|
+
!normalized ||
|
|
408
|
+
normalized === "." ||
|
|
409
|
+
normalized === ".." ||
|
|
410
|
+
normalized.startsWith("../") ||
|
|
411
|
+
normalized.split("/").some((part) => !part || part === "." || part === "..")
|
|
412
|
+
) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
return normalized;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function findUpward(startDir: string, filename: string): string | null {
|
|
419
|
+
let current = path.resolve(startDir);
|
|
420
|
+
for (;;) {
|
|
421
|
+
const candidate = path.join(current, filename);
|
|
422
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
423
|
+
const parent = path.dirname(current);
|
|
424
|
+
if (parent === current) return null;
|
|
425
|
+
current = parent;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
430
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function manifestRootPath(value: unknown): string | null {
|
|
434
|
+
if (typeof value === "string") return normalizeRelativePath(value);
|
|
435
|
+
if (isRecord(value) && typeof value.path === "string") {
|
|
436
|
+
return normalizeRelativePath(value.path);
|
|
437
|
+
}
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function planManifestConfig(): { rootDir: string; plansPath: string } | null {
|
|
442
|
+
const configuredManifest =
|
|
443
|
+
process.env.AGENT_NATIVE_MANIFEST?.trim() ||
|
|
444
|
+
process.env.AGENT_NATIVE_MANIFEST_PATH?.trim();
|
|
445
|
+
const manifestPath = configuredManifest
|
|
446
|
+
? path.resolve(configuredManifest)
|
|
447
|
+
: findUpward(process.cwd(), AGENT_NATIVE_MANIFEST_FILE);
|
|
448
|
+
if (!manifestPath) return null;
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
const parsed = JSON.parse(
|
|
452
|
+
fs.readFileSync(manifestPath, "utf-8"),
|
|
453
|
+
) as unknown;
|
|
454
|
+
const apps = isRecord(parsed) && isRecord(parsed.apps) ? parsed.apps : null;
|
|
455
|
+
const planApp = apps && isRecord(apps.plan) ? apps.plan : null;
|
|
456
|
+
const roots = planApp && Array.isArray(planApp.roots) ? planApp.roots : [];
|
|
457
|
+
const plansPath = roots
|
|
458
|
+
.map(manifestRootPath)
|
|
459
|
+
.find((item): item is string => Boolean(item));
|
|
460
|
+
return plansPath
|
|
461
|
+
? { rootDir: path.dirname(manifestPath), plansPath }
|
|
462
|
+
: null;
|
|
463
|
+
} catch {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function localPlanWorkspaceRoot(startDir = process.cwd()): string {
|
|
469
|
+
const manifestPath = findUpward(startDir, AGENT_NATIVE_MANIFEST_FILE);
|
|
470
|
+
if (manifestPath) return path.dirname(manifestPath);
|
|
471
|
+
const gitDir = findUpward(startDir, ".git");
|
|
472
|
+
if (gitDir) return path.dirname(gitDir);
|
|
473
|
+
|
|
474
|
+
const configuredManifest =
|
|
475
|
+
process.env.AGENT_NATIVE_MANIFEST?.trim() ||
|
|
476
|
+
process.env.AGENT_NATIVE_MANIFEST_PATH?.trim();
|
|
477
|
+
if (configuredManifest) return path.dirname(path.resolve(configuredManifest));
|
|
478
|
+
return process.cwd();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function defaultPlansDir(): string {
|
|
482
|
+
const configured = process.env.PLAN_LOCAL_DIR;
|
|
483
|
+
if (configured?.trim()) return path.resolve(configured.trim());
|
|
484
|
+
const manifest = planManifestConfig();
|
|
485
|
+
if (manifest) return path.resolve(manifest.rootDir, manifest.plansPath);
|
|
486
|
+
return path.resolve("plans");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function defaultLocalPlanAppUrl(): string {
|
|
490
|
+
return (
|
|
491
|
+
process.env.PLAN_LOCAL_APP_URL ||
|
|
492
|
+
process.env.PLAN_BASE_URL ||
|
|
493
|
+
"http://localhost:8096"
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function defaultLocalPlanBridgeAppUrl(): string {
|
|
498
|
+
return (
|
|
499
|
+
process.env.PLAN_LOCAL_BRIDGE_APP_URL ||
|
|
500
|
+
process.env.PLAN_BASE_URL ||
|
|
501
|
+
DEFAULT_PLAN_APP_URL
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function normalizeAppUrl(value: string | undefined): string {
|
|
506
|
+
return (value || defaultLocalPlanAppUrl()).replace(/\/+$/, "");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function normalizeBridgeAppUrl(value: string | undefined): string {
|
|
510
|
+
return (value || defaultLocalPlanBridgeAppUrl()).replace(/\/+$/, "");
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function localPlanPreviewUrl(dir: string, appUrl?: string): string {
|
|
514
|
+
const base = `${normalizeAppUrl(appUrl)}/local-plans/${encodeURIComponent(
|
|
515
|
+
path.basename(path.resolve(dir)),
|
|
516
|
+
)}`;
|
|
517
|
+
const repoPath = repoRelativePlanPath(dir);
|
|
518
|
+
if (!repoPath) return base;
|
|
519
|
+
return `${base}?${new URLSearchParams({ path: repoPath }).toString()}`;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function realpathIfExists(filePath: string): string {
|
|
523
|
+
try {
|
|
524
|
+
return fs.realpathSync.native(filePath);
|
|
525
|
+
} catch {
|
|
526
|
+
return path.resolve(filePath);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function repoRelativePlanPath(dir: string): string | null {
|
|
531
|
+
const resolved = realpathIfExists(dir);
|
|
532
|
+
const root = realpathIfExists(localPlanWorkspaceRoot(resolved));
|
|
533
|
+
const relative = path.relative(root, resolved);
|
|
534
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
return normalizeSlash(relative);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function localPlanBridgePageUrl(input: {
|
|
541
|
+
dir: string;
|
|
542
|
+
bridgeUrl: string;
|
|
543
|
+
appUrl?: string;
|
|
544
|
+
}): string {
|
|
545
|
+
// Keep the real local folder name out of the hosted request path. The bridge
|
|
546
|
+
// payload supplies the actual slug after the browser connects on loopback.
|
|
547
|
+
// The opaque id keeps simultaneous bridge sessions distinct without
|
|
548
|
+
// disclosing the folder name or access token to the hosted request.
|
|
549
|
+
const bridgeId = crypto
|
|
550
|
+
.createHash("sha256")
|
|
551
|
+
.update(input.bridgeUrl)
|
|
552
|
+
.digest("hex")
|
|
553
|
+
.slice(0, 16);
|
|
554
|
+
return `${normalizeBridgeAppUrl(input.appUrl)}/local-plans/local-${bridgeId}#bridge=${encodeURIComponent(
|
|
555
|
+
input.bridgeUrl,
|
|
556
|
+
)}`;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function writeLocalPlanUrlFile(dir: string, url: string, urlFile?: string) {
|
|
560
|
+
const file = path.resolve(urlFile || path.join(dir, ".plan-url"));
|
|
561
|
+
fs.writeFileSync(file, `${url}\n`, { encoding: "utf-8", mode: 0o600 });
|
|
562
|
+
return file;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function runOpenCommand(command: string, args: string[]): OpenLocalUrlResult {
|
|
566
|
+
const result = spawnSync(command, args, {
|
|
567
|
+
stdio: "ignore",
|
|
568
|
+
windowsHide: true,
|
|
569
|
+
});
|
|
570
|
+
const commandDisplay = [command, ...args].join(" ");
|
|
571
|
+
if (result.error) {
|
|
572
|
+
return {
|
|
573
|
+
ok: false,
|
|
574
|
+
command: commandDisplay,
|
|
575
|
+
error: result.error.message,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
579
|
+
return {
|
|
580
|
+
ok: false,
|
|
581
|
+
command: commandDisplay,
|
|
582
|
+
error: `exit code ${result.status}`,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
return { ok: true, command: commandDisplay };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function openLocalUrl(url: string): OpenLocalUrlResult {
|
|
589
|
+
const platform = process.platform;
|
|
590
|
+
if (platform === "darwin") {
|
|
591
|
+
for (const appName of [
|
|
592
|
+
"Google Chrome",
|
|
593
|
+
"Chromium",
|
|
594
|
+
"Microsoft Edge",
|
|
595
|
+
"Brave Browser",
|
|
596
|
+
]) {
|
|
597
|
+
const result = runOpenCommand("open", ["-a", appName, url]);
|
|
598
|
+
if (result.ok) return result;
|
|
599
|
+
}
|
|
600
|
+
return runOpenCommand("open", [url]);
|
|
601
|
+
}
|
|
602
|
+
if (platform === "win32") {
|
|
603
|
+
return runOpenCommand("cmd", ["/c", "start", "", url]);
|
|
604
|
+
}
|
|
605
|
+
return runOpenCommand("xdg-open", [url]);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function escapeHtml(value: string): string {
|
|
609
|
+
return value
|
|
610
|
+
.replace(/&/g, "&")
|
|
611
|
+
.replace(/</g, "<")
|
|
612
|
+
.replace(/>/g, ">")
|
|
613
|
+
.replace(/"/g, """)
|
|
614
|
+
.replace(/'/g, "'");
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function stripFrontmatter(source: string): {
|
|
618
|
+
body: string;
|
|
619
|
+
frontmatter: Record<string, string>;
|
|
620
|
+
} {
|
|
621
|
+
if (!source.startsWith("---\n")) return { body: source, frontmatter: {} };
|
|
622
|
+
const end = source.indexOf("\n---", 4);
|
|
623
|
+
if (end < 0) return { body: source, frontmatter: {} };
|
|
624
|
+
const frontmatterSource = source.slice(4, end).trim();
|
|
625
|
+
const body = source.slice(end + 4).replace(/^\r?\n/, "");
|
|
626
|
+
const frontmatter: Record<string, string> = {};
|
|
627
|
+
for (const line of frontmatterSource.split(/\r?\n/)) {
|
|
628
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
629
|
+
if (!match) continue;
|
|
630
|
+
const value = match[2]
|
|
631
|
+
.trim()
|
|
632
|
+
.replace(/^['"]|['"]$/g, "")
|
|
633
|
+
.trim();
|
|
634
|
+
frontmatter[match[1]] = value;
|
|
635
|
+
}
|
|
636
|
+
return { body, frontmatter };
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function firstHeading(source: string): string | null {
|
|
640
|
+
for (const line of source.split(/\r?\n/)) {
|
|
641
|
+
const match = line.match(/^#{1,3}\s+(.+)$/);
|
|
642
|
+
if (match) return match[1].trim();
|
|
643
|
+
}
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function splitMdxBlocks(source: string): Array<{
|
|
648
|
+
type: "markdown" | "component";
|
|
649
|
+
name?: string;
|
|
650
|
+
value: string;
|
|
651
|
+
}> {
|
|
652
|
+
const blocks: Array<{
|
|
653
|
+
type: "markdown" | "component";
|
|
654
|
+
name?: string;
|
|
655
|
+
value: string;
|
|
656
|
+
}> = [];
|
|
657
|
+
const lines = source.split(/\r?\n/);
|
|
658
|
+
let markdown: string[] = [];
|
|
659
|
+
let component: string[] | null = null;
|
|
660
|
+
let componentName = "";
|
|
661
|
+
|
|
662
|
+
function flushMarkdown() {
|
|
663
|
+
const value = markdown.join("\n").trim();
|
|
664
|
+
if (value) blocks.push({ type: "markdown", value });
|
|
665
|
+
markdown = [];
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function flushComponent() {
|
|
669
|
+
if (!component) return;
|
|
670
|
+
blocks.push({
|
|
671
|
+
type: "component",
|
|
672
|
+
name: componentName || "MDX component",
|
|
673
|
+
value: component.join("\n").trim(),
|
|
674
|
+
});
|
|
675
|
+
component = null;
|
|
676
|
+
componentName = "";
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
for (const line of lines) {
|
|
680
|
+
const start = line.match(/^<([A-Z][A-Za-z0-9_]*)\b/);
|
|
681
|
+
if (!component && start) {
|
|
682
|
+
flushMarkdown();
|
|
683
|
+
component = [line];
|
|
684
|
+
componentName = start[1];
|
|
685
|
+
if (/\/>\s*$/.test(line)) flushComponent();
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (component) {
|
|
690
|
+
component.push(line);
|
|
691
|
+
if (new RegExp(`</${componentName}>\\s*$`).test(line)) {
|
|
692
|
+
flushComponent();
|
|
693
|
+
}
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
markdown.push(line);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
flushComponent();
|
|
701
|
+
flushMarkdown();
|
|
702
|
+
return blocks;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function renderMarkdownish(source: string): string {
|
|
706
|
+
const lines = source.split(/\r?\n/);
|
|
707
|
+
const html: string[] = [];
|
|
708
|
+
let inCode = false;
|
|
709
|
+
let codeLines: string[] = [];
|
|
710
|
+
let listLines: string[] = [];
|
|
711
|
+
|
|
712
|
+
function flushList() {
|
|
713
|
+
if (listLines.length === 0) return;
|
|
714
|
+
html.push(
|
|
715
|
+
`<ul>${listLines.map((line) => `<li>${escapeHtml(line)}</li>`).join("")}</ul>`,
|
|
716
|
+
);
|
|
717
|
+
listLines = [];
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function flushCode() {
|
|
721
|
+
if (!inCode) return;
|
|
722
|
+
html.push(`<pre><code>${escapeHtml(codeLines.join("\n"))}</code></pre>`);
|
|
723
|
+
codeLines = [];
|
|
724
|
+
inCode = false;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
for (const line of lines) {
|
|
728
|
+
if (line.startsWith("```")) {
|
|
729
|
+
if (inCode) flushCode();
|
|
730
|
+
else {
|
|
731
|
+
flushList();
|
|
732
|
+
inCode = true;
|
|
733
|
+
codeLines = [];
|
|
734
|
+
}
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
if (inCode) {
|
|
738
|
+
codeLines.push(line);
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const heading = line.match(/^(#{1,4})\s+(.+)$/);
|
|
743
|
+
if (heading) {
|
|
744
|
+
flushList();
|
|
745
|
+
const level = Math.min(heading[1].length, 4);
|
|
746
|
+
html.push(`<h${level}>${escapeHtml(heading[2].trim())}</h${level}>`);
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const bullet = line.match(/^\s*[-*]\s+(.+)$/);
|
|
751
|
+
if (bullet) {
|
|
752
|
+
listLines.push(bullet[1]);
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (!line.trim()) {
|
|
757
|
+
flushList();
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
flushList();
|
|
762
|
+
html.push(`<p>${escapeHtml(line.trim())}</p>`);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
flushCode();
|
|
766
|
+
flushList();
|
|
767
|
+
return html.join("\n");
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function readLocalPlanAssets(dir: string): Record<string, string> | undefined {
|
|
771
|
+
const assetsDir = path.join(dir, "assets");
|
|
772
|
+
if (!fs.existsSync(assetsDir)) return undefined;
|
|
773
|
+
|
|
774
|
+
const assets: Record<string, string> = {};
|
|
775
|
+
let totalBytes = 0;
|
|
776
|
+
for (const entry of fs.readdirSync(assetsDir, { withFileTypes: true })) {
|
|
777
|
+
if (!entry.isFile()) continue;
|
|
778
|
+
const filename = path.basename(entry.name);
|
|
779
|
+
if (!filename || filename !== entry.name) continue;
|
|
780
|
+
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
781
|
+
if (!LOCAL_PLAN_ASSET_EXTENSIONS.has(ext)) continue;
|
|
782
|
+
|
|
783
|
+
const abs = path.join(assetsDir, filename);
|
|
784
|
+
const bytes = fs.readFileSync(abs);
|
|
785
|
+
if (bytes.byteLength > LOCAL_PLAN_ASSET_MAX_SINGLE_BYTES) continue;
|
|
786
|
+
if (totalBytes + bytes.byteLength > LOCAL_PLAN_ASSET_MAX_TOTAL_BYTES) {
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
totalBytes += bytes.byteLength;
|
|
790
|
+
assets[filename] = bytes.toString("base64");
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return Object.keys(assets).length > 0 ? assets : undefined;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export function readLocalPlanFiles(dir: string): LocalPlanFiles {
|
|
797
|
+
const resolved = path.resolve(dir);
|
|
798
|
+
const planPath = path.join(resolved, "plan.mdx");
|
|
799
|
+
if (!fs.existsSync(planPath)) {
|
|
800
|
+
throw new Error(`Missing local plan source: ${planPath}`);
|
|
801
|
+
}
|
|
802
|
+
const readOptional = (file: string) => {
|
|
803
|
+
const abs = path.join(resolved, file);
|
|
804
|
+
return fs.existsSync(abs) ? fs.readFileSync(abs, "utf-8") : undefined;
|
|
805
|
+
};
|
|
806
|
+
return {
|
|
807
|
+
dir: resolved,
|
|
808
|
+
planMdx: fs.readFileSync(planPath, "utf-8"),
|
|
809
|
+
canvasMdx: readOptional("canvas.mdx"),
|
|
810
|
+
prototypeMdx: readOptional("prototype.mdx"),
|
|
811
|
+
stateJson: readOptional(".plan-state.json"),
|
|
812
|
+
assets: readLocalPlanAssets(resolved),
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function localPlanMdxFolder(files: LocalPlanFiles): LocalPlanBridgeMdxFolder {
|
|
817
|
+
return {
|
|
818
|
+
"plan.mdx": files.planMdx,
|
|
819
|
+
...(files.canvasMdx ? { "canvas.mdx": files.canvasMdx } : {}),
|
|
820
|
+
...(files.prototypeMdx ? { "prototype.mdx": files.prototypeMdx } : {}),
|
|
821
|
+
...(files.stateJson ? { ".plan-state.json": files.stateJson } : {}),
|
|
822
|
+
...(files.assets ? { "assets/": files.assets } : {}),
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function localPlanFileList(files: LocalPlanFiles): string[] {
|
|
827
|
+
return [
|
|
828
|
+
"plan.mdx",
|
|
829
|
+
...(files.canvasMdx ? ["canvas.mdx"] : []),
|
|
830
|
+
...(files.prototypeMdx ? ["prototype.mdx"] : []),
|
|
831
|
+
...(files.stateJson ? [".plan-state.json"] : []),
|
|
832
|
+
...Object.keys(files.assets ?? {}).map((filename) => `assets/${filename}`),
|
|
833
|
+
];
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function localPlanSourceEntries(files: LocalPlanFiles): Array<{
|
|
837
|
+
file: string;
|
|
838
|
+
source: string;
|
|
839
|
+
}> {
|
|
840
|
+
return [
|
|
841
|
+
{ file: "plan.mdx", source: files.planMdx },
|
|
842
|
+
...(files.canvasMdx
|
|
843
|
+
? [{ file: "canvas.mdx", source: files.canvasMdx }]
|
|
844
|
+
: []),
|
|
845
|
+
...(files.prototypeMdx
|
|
846
|
+
? [{ file: "prototype.mdx", source: files.prototypeMdx }]
|
|
847
|
+
: []),
|
|
848
|
+
];
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function lineNumberAt(source: string, index: number): number {
|
|
852
|
+
let line = 1;
|
|
853
|
+
for (let i = 0; i < index; i += 1) {
|
|
854
|
+
if (source.charCodeAt(i) === 10) line += 1;
|
|
855
|
+
}
|
|
856
|
+
return line;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function maskFencedCode(source: string): string {
|
|
860
|
+
return source.replace(
|
|
861
|
+
/(^|\n)(```|~~~)[\s\S]*?(\n\2[^\n]*(?=\n|$))/g,
|
|
862
|
+
(match) => match.replace(/[^\n]/g, " "),
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function findJsxOpeningTagEnd(source: string, start: number): number {
|
|
867
|
+
let quote: string | null = null;
|
|
868
|
+
let braceDepth = 0;
|
|
869
|
+
for (let i = start; i < source.length; i += 1) {
|
|
870
|
+
const char = source[i];
|
|
871
|
+
if (quote) {
|
|
872
|
+
if (char === "\\" && i + 1 < source.length) {
|
|
873
|
+
i += 1;
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
if (char === quote) quote = null;
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
880
|
+
quote = char;
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
if (char === "{") {
|
|
884
|
+
braceDepth += 1;
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
if (char === "}") {
|
|
888
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
if (char === ">" && braceDepth === 0) return i;
|
|
892
|
+
}
|
|
893
|
+
return -1;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function addValidationIssue(
|
|
897
|
+
issues: LocalPlanValidationIssue[],
|
|
898
|
+
file: string,
|
|
899
|
+
source: string,
|
|
900
|
+
index: number,
|
|
901
|
+
message: string,
|
|
902
|
+
) {
|
|
903
|
+
issues.push({ file, line: lineNumberAt(source, index), message });
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const ENTITY_RE = /&(?:[a-z][a-z0-9]+|#[0-9]+|#x[0-9a-f]+);/gi;
|
|
907
|
+
const HTML_TEXT_ATTR_RE =
|
|
908
|
+
/\b(?:aria-label|alt|placeholder|title|value)=\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`)/gi;
|
|
909
|
+
const WIREFRAME_TEXT_ATTR_RE =
|
|
910
|
+
/\b(?:text|value|label|placeholder|title|note|due)=\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`)/gi;
|
|
911
|
+
|
|
912
|
+
function normalizeVisibleText(value: string): string {
|
|
913
|
+
return value
|
|
914
|
+
.replace(/ | |�*a0;/gi, " ")
|
|
915
|
+
.replace(ENTITY_RE, "x")
|
|
916
|
+
.replace(/\s+/g, " ")
|
|
917
|
+
.trim();
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function meaningfulTextLength(value: string | undefined): number {
|
|
921
|
+
return normalizeVisibleText(value ?? "").length;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function htmlMeaningfulTextLength(html: string): number {
|
|
925
|
+
let length = 0;
|
|
926
|
+
for (const match of html.matchAll(HTML_TEXT_ATTR_RE)) {
|
|
927
|
+
length += meaningfulTextLength(match[1] ?? match[2] ?? match[3]);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const visibleText = html
|
|
931
|
+
.replace(/<!--[\s\S]*?-->/g, " ")
|
|
932
|
+
.replace(/<script\b[\s\S]*?<\/script>/gi, " ")
|
|
933
|
+
.replace(/<style\b[\s\S]*?<\/style>/gi, " ")
|
|
934
|
+
.replace(/<[^>]+>/g, " ");
|
|
935
|
+
length += meaningfulTextLength(visibleText);
|
|
936
|
+
|
|
937
|
+
return length;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function hasSkeletonGeometry(html: string): boolean {
|
|
941
|
+
return (
|
|
942
|
+
/<(?:div|span|section|main|article|ul|li)\b/i.test(html) &&
|
|
943
|
+
/\b(?:height|width|background|border|padding|wf-card|wf-box|wf-pill|wf-chip)\b/i.test(
|
|
944
|
+
html,
|
|
945
|
+
)
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function stringAttributeValues(source: string, name: string): string[] {
|
|
950
|
+
const values: string[] = [];
|
|
951
|
+
const re = new RegExp(
|
|
952
|
+
`\\b${name}\\s*=\\s*(?:\\{\\s*\`([\\s\\S]*?)\`\\s*\\}|\\{\\s*"([^"]*)"\\s*\\}|\\{\\s*'([^']*)'\\s*\\}|"([^"]*)"|'([^']*)')`,
|
|
953
|
+
"g",
|
|
954
|
+
);
|
|
955
|
+
for (const match of source.matchAll(re)) {
|
|
956
|
+
const value = match[1] ?? match[2] ?? match[3] ?? match[4] ?? match[5];
|
|
957
|
+
if (value !== undefined) values.push(value);
|
|
958
|
+
}
|
|
959
|
+
return values;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function hasUnparsedAttributeExpression(source: string, name: string): boolean {
|
|
963
|
+
return new RegExp(`\\b${name}\\s*=\\s*\\{`).test(source);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function hasMeaningfulWireframeHtml(screenOpening: string): boolean | null {
|
|
967
|
+
const htmlValues = stringAttributeValues(screenOpening, "html");
|
|
968
|
+
if (htmlValues.length === 0) {
|
|
969
|
+
return hasUnparsedAttributeExpression(screenOpening, "html") ? null : false;
|
|
970
|
+
}
|
|
971
|
+
return htmlValues.some(
|
|
972
|
+
(html) => htmlMeaningfulTextLength(html) >= 2 || hasSkeletonGeometry(html),
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function hasMeaningfulKitScreen(screenSource: string): boolean {
|
|
977
|
+
for (const match of screenSource.matchAll(WIREFRAME_TEXT_ATTR_RE)) {
|
|
978
|
+
if (meaningfulTextLength(match[1] ?? match[2] ?? match[3]) >= 2) {
|
|
979
|
+
return true;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if (/\bitems\s*=\s*\{[\s\S]*?\blabel\s*:/i.test(screenSource)) return true;
|
|
983
|
+
if (/\brows\s*=\s*\{[\s\S]*?\b[klv]\s*:/i.test(screenSource)) return true;
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function hasMeaningfulWireframeScreen(blockSource: string): boolean | null {
|
|
988
|
+
const screenMatch = /<Screen\b/.exec(blockSource);
|
|
989
|
+
if (!screenMatch) return false;
|
|
990
|
+
const screenStart = screenMatch.index;
|
|
991
|
+
const screenOpeningEnd = findJsxOpeningTagEnd(blockSource, screenStart);
|
|
992
|
+
if (screenOpeningEnd < 0) return false;
|
|
993
|
+
const screenOpening = blockSource.slice(screenStart, screenOpeningEnd + 1);
|
|
994
|
+
const htmlMeaningful = hasMeaningfulWireframeHtml(screenOpening);
|
|
995
|
+
if (htmlMeaningful === true) return true;
|
|
996
|
+
|
|
997
|
+
const selfClosing = /\/\s*>$/.test(screenOpening);
|
|
998
|
+
const closeIndex = selfClosing
|
|
999
|
+
? -1
|
|
1000
|
+
: blockSource.indexOf("</Screen>", screenOpeningEnd + 1);
|
|
1001
|
+
const screenSource =
|
|
1002
|
+
closeIndex >= 0
|
|
1003
|
+
? blockSource.slice(screenStart, closeIndex + "</Screen>".length)
|
|
1004
|
+
: screenOpening;
|
|
1005
|
+
if (hasMeaningfulKitScreen(screenSource)) return true;
|
|
1006
|
+
|
|
1007
|
+
return htmlMeaningful === null ? null : false;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function lintWireframeBlocks(
|
|
1011
|
+
file: string,
|
|
1012
|
+
source: string,
|
|
1013
|
+
issues: LocalPlanValidationIssue[],
|
|
1014
|
+
) {
|
|
1015
|
+
const scanSource = maskFencedCode(source);
|
|
1016
|
+
const re = /<WireframeBlock\b/g;
|
|
1017
|
+
let match: RegExpExecArray | null;
|
|
1018
|
+
while ((match = re.exec(scanSource))) {
|
|
1019
|
+
const start = match.index;
|
|
1020
|
+
const openingEnd = findJsxOpeningTagEnd(scanSource, start);
|
|
1021
|
+
if (openingEnd < 0) {
|
|
1022
|
+
addValidationIssue(
|
|
1023
|
+
issues,
|
|
1024
|
+
file,
|
|
1025
|
+
source,
|
|
1026
|
+
start,
|
|
1027
|
+
"WireframeBlock opening tag is not closed.",
|
|
1028
|
+
);
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const opening = scanSource.slice(start, openingEnd + 1);
|
|
1033
|
+
const unsupportedAttr = opening.match(
|
|
1034
|
+
/\b(data|screens|screen|elements)\s*=/,
|
|
1035
|
+
);
|
|
1036
|
+
if (unsupportedAttr) {
|
|
1037
|
+
addValidationIssue(
|
|
1038
|
+
issues,
|
|
1039
|
+
file,
|
|
1040
|
+
source,
|
|
1041
|
+
start,
|
|
1042
|
+
`WireframeBlock uses unsupported "${unsupportedAttr[1]}" prop. Put content inside a <Screen> child instead.`,
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const selfClosing = /\/\s*>$/.test(opening);
|
|
1047
|
+
const closeTag = "</WireframeBlock>";
|
|
1048
|
+
const closeIndex = selfClosing
|
|
1049
|
+
? -1
|
|
1050
|
+
: scanSource.indexOf(closeTag, openingEnd + 1);
|
|
1051
|
+
const blockSource = selfClosing
|
|
1052
|
+
? opening
|
|
1053
|
+
: closeIndex >= 0
|
|
1054
|
+
? scanSource.slice(start, closeIndex + closeTag.length)
|
|
1055
|
+
: scanSource.slice(start, openingEnd + 1);
|
|
1056
|
+
|
|
1057
|
+
if (!selfClosing && closeIndex < 0) {
|
|
1058
|
+
addValidationIssue(
|
|
1059
|
+
issues,
|
|
1060
|
+
file,
|
|
1061
|
+
source,
|
|
1062
|
+
start,
|
|
1063
|
+
"WireframeBlock must have a closing </WireframeBlock> tag.",
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (selfClosing || !/<Screen\b/.test(blockSource)) {
|
|
1068
|
+
addValidationIssue(
|
|
1069
|
+
issues,
|
|
1070
|
+
file,
|
|
1071
|
+
source,
|
|
1072
|
+
start,
|
|
1073
|
+
'WireframeBlock must wrap a <Screen> child; self-closing wireframes render empty. Use <WireframeBlock><Screen surface="browser">...</Screen></WireframeBlock>.',
|
|
1074
|
+
);
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const meaningfulScreen = hasMeaningfulWireframeScreen(blockSource);
|
|
1079
|
+
if (meaningfulScreen === false) {
|
|
1080
|
+
addValidationIssue(
|
|
1081
|
+
issues,
|
|
1082
|
+
file,
|
|
1083
|
+
source,
|
|
1084
|
+
start,
|
|
1085
|
+
'WireframeBlock contains an empty <Screen>; local previews render blank wireframes. Add visible html text/controls or kit nodes such as <Title text="Checkout" /> and <Btn label="Pay" />.',
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function lintColumnsBlocks(
|
|
1092
|
+
file: string,
|
|
1093
|
+
source: string,
|
|
1094
|
+
issues: LocalPlanValidationIssue[],
|
|
1095
|
+
) {
|
|
1096
|
+
const scanSource = maskFencedCode(source);
|
|
1097
|
+
const re = /<Columns\b/g;
|
|
1098
|
+
let match: RegExpExecArray | null;
|
|
1099
|
+
while ((match = re.exec(scanSource))) {
|
|
1100
|
+
const start = match.index;
|
|
1101
|
+
const openingEnd = findJsxOpeningTagEnd(scanSource, start);
|
|
1102
|
+
if (openingEnd < 0) continue;
|
|
1103
|
+
const opening = source.slice(start, openingEnd + 1);
|
|
1104
|
+
const columnsAttr = readJsxAttribute(opening, "columns");
|
|
1105
|
+
if (!columnsAttr) continue;
|
|
1106
|
+
const entries =
|
|
1107
|
+
columnsAttr.kind === "expression"
|
|
1108
|
+
? extractTopLevelObjectLiterals(columnsAttr.value)
|
|
1109
|
+
: null;
|
|
1110
|
+
if (entries && entries.length > 0) continue;
|
|
1111
|
+
addValidationIssue(
|
|
1112
|
+
issues,
|
|
1113
|
+
file,
|
|
1114
|
+
source,
|
|
1115
|
+
start,
|
|
1116
|
+
'Columns must use <Column> children or a columns={[{ id, label, blocks }]} array, not a bare columns= prop. Use <Columns><Column label="Before">...</Column><Column label="After">...</Column></Columns>.',
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
type JsxAttributeValue = {
|
|
1122
|
+
name: string;
|
|
1123
|
+
kind: "expression" | "string" | "bare" | "boolean";
|
|
1124
|
+
value: string;
|
|
1125
|
+
start: number;
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
function findBalancedEnd(
|
|
1129
|
+
source: string,
|
|
1130
|
+
start: number,
|
|
1131
|
+
open: string,
|
|
1132
|
+
close: string,
|
|
1133
|
+
): number {
|
|
1134
|
+
let depth = 0;
|
|
1135
|
+
let quote: string | null = null;
|
|
1136
|
+
for (let i = start; i < source.length; i += 1) {
|
|
1137
|
+
const char = source[i];
|
|
1138
|
+
if (quote) {
|
|
1139
|
+
if (char === "\\" && i + 1 < source.length) {
|
|
1140
|
+
i += 1;
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
if (char === quote) quote = null;
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
1147
|
+
quote = char;
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
if (char === open) {
|
|
1151
|
+
depth += 1;
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
if (char === close) {
|
|
1155
|
+
depth -= 1;
|
|
1156
|
+
if (depth === 0) return i;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return -1;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function readJsxAttribute(
|
|
1163
|
+
opening: string,
|
|
1164
|
+
name: string,
|
|
1165
|
+
): JsxAttributeValue | null {
|
|
1166
|
+
let i = 0;
|
|
1167
|
+
while (i < opening.length) {
|
|
1168
|
+
if (!/[A-Za-z_:]/.test(opening[i] ?? "")) {
|
|
1169
|
+
i += 1;
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
const nameStart = i;
|
|
1173
|
+
i += 1;
|
|
1174
|
+
while (/[\w:.-]/.test(opening[i] ?? "")) i += 1;
|
|
1175
|
+
const attrName = opening.slice(nameStart, i);
|
|
1176
|
+
while (/\s/.test(opening[i] ?? "")) i += 1;
|
|
1177
|
+
if (opening[i] !== "=") {
|
|
1178
|
+
if (attrName === name) {
|
|
1179
|
+
return { name, kind: "boolean", value: "true", start: nameStart };
|
|
1180
|
+
}
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
i += 1;
|
|
1184
|
+
while (/\s/.test(opening[i] ?? "")) i += 1;
|
|
1185
|
+
const valueStart = i;
|
|
1186
|
+
const quote = opening[i];
|
|
1187
|
+
if (quote === '"' || quote === "'" || quote === "`") {
|
|
1188
|
+
i += 1;
|
|
1189
|
+
while (i < opening.length) {
|
|
1190
|
+
if (opening[i] === "\\" && i + 1 < opening.length) {
|
|
1191
|
+
i += 2;
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
if (opening[i] === quote) break;
|
|
1195
|
+
i += 1;
|
|
1196
|
+
}
|
|
1197
|
+
const value = opening.slice(valueStart + 1, i);
|
|
1198
|
+
i += 1;
|
|
1199
|
+
if (attrName === name) {
|
|
1200
|
+
return { name, kind: "string", value, start: valueStart + 1 };
|
|
1201
|
+
}
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
if (quote === "{") {
|
|
1205
|
+
const end = findBalancedEnd(opening, i, "{", "}");
|
|
1206
|
+
if (end < 0) {
|
|
1207
|
+
if (attrName === name) {
|
|
1208
|
+
return {
|
|
1209
|
+
name,
|
|
1210
|
+
kind: "expression",
|
|
1211
|
+
value: opening.slice(valueStart + 1),
|
|
1212
|
+
start: valueStart + 1,
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
break;
|
|
1216
|
+
}
|
|
1217
|
+
const value = opening.slice(valueStart + 1, end);
|
|
1218
|
+
i = end + 1;
|
|
1219
|
+
if (attrName === name) {
|
|
1220
|
+
return { name, kind: "expression", value, start: valueStart + 1 };
|
|
1221
|
+
}
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
while (i < opening.length && !/[\s>]/.test(opening[i] ?? "")) i += 1;
|
|
1225
|
+
if (attrName === name) {
|
|
1226
|
+
return {
|
|
1227
|
+
name,
|
|
1228
|
+
kind: "bare",
|
|
1229
|
+
value: opening.slice(valueStart, i),
|
|
1230
|
+
start: valueStart,
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return null;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function expressionOffset(expression: string): number {
|
|
1238
|
+
return expression.search(/\S/);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function extractTopLevelObjectLiterals(expression: string): Array<{
|
|
1242
|
+
source: string;
|
|
1243
|
+
start: number;
|
|
1244
|
+
}> | null {
|
|
1245
|
+
const leading = expressionOffset(expression);
|
|
1246
|
+
if (leading < 0 || expression[leading] !== "[") return null;
|
|
1247
|
+
const arrayEnd = findBalancedEnd(expression, leading, "[", "]");
|
|
1248
|
+
if (arrayEnd < 0) return null;
|
|
1249
|
+
const objects: Array<{ source: string; start: number }> = [];
|
|
1250
|
+
let i = leading + 1;
|
|
1251
|
+
while (i < arrayEnd) {
|
|
1252
|
+
const char = expression[i];
|
|
1253
|
+
if (/\s|,/.test(char ?? "")) {
|
|
1254
|
+
i += 1;
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
if (char !== "{") {
|
|
1258
|
+
return null;
|
|
1259
|
+
}
|
|
1260
|
+
const objectEnd = findBalancedEnd(expression, i, "{", "}");
|
|
1261
|
+
if (objectEnd < 0 || objectEnd > arrayEnd) return null;
|
|
1262
|
+
objects.push({
|
|
1263
|
+
source: expression.slice(i, objectEnd + 1),
|
|
1264
|
+
start: i,
|
|
1265
|
+
});
|
|
1266
|
+
i = objectEnd + 1;
|
|
1267
|
+
}
|
|
1268
|
+
return objects;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function findValueEnd(source: string, start: number): number {
|
|
1272
|
+
let i = start;
|
|
1273
|
+
let quote: string | null = null;
|
|
1274
|
+
let depth = 0;
|
|
1275
|
+
while (i < source.length) {
|
|
1276
|
+
const char = source[i];
|
|
1277
|
+
if (quote) {
|
|
1278
|
+
if (char === "\\" && i + 1 < source.length) {
|
|
1279
|
+
i += 2;
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
if (char === quote) quote = null;
|
|
1283
|
+
i += 1;
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
1287
|
+
quote = char;
|
|
1288
|
+
i += 1;
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
if (char === "{" || char === "[" || char === "(") {
|
|
1292
|
+
depth += 1;
|
|
1293
|
+
i += 1;
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
if (char === "}" || char === "]" || char === ")") {
|
|
1297
|
+
if (depth === 0) return i;
|
|
1298
|
+
depth -= 1;
|
|
1299
|
+
i += 1;
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
if (char === "," && depth === 0) return i;
|
|
1303
|
+
i += 1;
|
|
1304
|
+
}
|
|
1305
|
+
return source.length;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function readObjectKey(
|
|
1309
|
+
source: string,
|
|
1310
|
+
start: number,
|
|
1311
|
+
): {
|
|
1312
|
+
key: string;
|
|
1313
|
+
keyStart: number;
|
|
1314
|
+
colon: number;
|
|
1315
|
+
} | null {
|
|
1316
|
+
let i = start;
|
|
1317
|
+
while (/\s|,/.test(source[i] ?? "")) i += 1;
|
|
1318
|
+
const keyStart = i;
|
|
1319
|
+
const quote = source[i];
|
|
1320
|
+
let key = "";
|
|
1321
|
+
if (quote === '"' || quote === "'") {
|
|
1322
|
+
i += 1;
|
|
1323
|
+
const valueStart = i;
|
|
1324
|
+
while (i < source.length) {
|
|
1325
|
+
if (source[i] === "\\" && i + 1 < source.length) {
|
|
1326
|
+
i += 2;
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
if (source[i] === quote) break;
|
|
1330
|
+
i += 1;
|
|
1331
|
+
}
|
|
1332
|
+
key = source.slice(valueStart, i);
|
|
1333
|
+
i += 1;
|
|
1334
|
+
} else if (/[A-Za-z_$]/.test(quote ?? "")) {
|
|
1335
|
+
i += 1;
|
|
1336
|
+
while (/[\w$]/.test(source[i] ?? "")) i += 1;
|
|
1337
|
+
key = source.slice(keyStart, i);
|
|
1338
|
+
} else {
|
|
1339
|
+
return null;
|
|
1340
|
+
}
|
|
1341
|
+
while (/\s/.test(source[i] ?? "")) i += 1;
|
|
1342
|
+
if (source[i] !== ":") return null;
|
|
1343
|
+
return { key, keyStart, colon: i };
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function readTopLevelObjectProperty(
|
|
1347
|
+
objectSource: string,
|
|
1348
|
+
name: string,
|
|
1349
|
+
): { value: string; valueStart: number } | null {
|
|
1350
|
+
const body = objectSource.trim().startsWith("{")
|
|
1351
|
+
? objectSource.slice(1, objectSource.lastIndexOf("}"))
|
|
1352
|
+
: objectSource;
|
|
1353
|
+
let i = 0;
|
|
1354
|
+
while (i < body.length) {
|
|
1355
|
+
const key = readObjectKey(body, i);
|
|
1356
|
+
if (!key) {
|
|
1357
|
+
i += 1;
|
|
1358
|
+
continue;
|
|
1359
|
+
}
|
|
1360
|
+
const valueStart = key.colon + 1;
|
|
1361
|
+
const valueEnd = findValueEnd(body, valueStart);
|
|
1362
|
+
if (key.key === name) {
|
|
1363
|
+
return { value: body.slice(valueStart, valueEnd), valueStart };
|
|
1364
|
+
}
|
|
1365
|
+
i = valueEnd + 1;
|
|
1366
|
+
}
|
|
1367
|
+
return null;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function isStaticNonEmptyStringLiteral(value: string): boolean {
|
|
1371
|
+
const trimmed = value.trim();
|
|
1372
|
+
const quote = trimmed[0];
|
|
1373
|
+
if (quote !== '"' && quote !== "'" && quote !== "`") return false;
|
|
1374
|
+
if (!trimmed.endsWith(quote)) return false;
|
|
1375
|
+
if (quote === "`" && /\$\{/.test(trimmed)) return false;
|
|
1376
|
+
return trimmed.slice(1, -1).trim().length > 0;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function hasRequiredStaticString(objectSource: string, name: string): boolean {
|
|
1380
|
+
const prop = readTopLevelObjectProperty(objectSource, name);
|
|
1381
|
+
return prop ? isStaticNonEmptyStringLiteral(prop.value) : false;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function hasRequiredStaticId(objectSource: string): boolean {
|
|
1385
|
+
return hasRequiredStaticString(objectSource, "id");
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function hasRequiredEnumLiteral(
|
|
1389
|
+
objectSource: string,
|
|
1390
|
+
name: string,
|
|
1391
|
+
allowed: readonly string[],
|
|
1392
|
+
): boolean {
|
|
1393
|
+
const prop = readTopLevelObjectProperty(objectSource, name);
|
|
1394
|
+
if (!prop) return false;
|
|
1395
|
+
const trimmed = prop.value.trim();
|
|
1396
|
+
const quote = trimmed[0];
|
|
1397
|
+
if (quote !== '"' && quote !== "'" && quote !== "`") return false;
|
|
1398
|
+
if (!trimmed.endsWith(quote)) return false;
|
|
1399
|
+
if (quote === "`" && /\$\{/.test(trimmed)) return false;
|
|
1400
|
+
return allowed.includes(trimmed.slice(1, -1).trim());
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// Shared per-item checks so the top-level `<Checklist items={[…]} />` tag and
|
|
1404
|
+
// blocks authored as JSON inside a container's `tabs={[…]}` / `columns={[…]}`
|
|
1405
|
+
// array enforce the SAME required fields. `listBase` is the absolute offset of
|
|
1406
|
+
// the items array in the original source so line numbers point near the item.
|
|
1407
|
+
function checkChecklistItemList(
|
|
1408
|
+
items: Array<{ source: string; start: number }>,
|
|
1409
|
+
listBase: number,
|
|
1410
|
+
file: string,
|
|
1411
|
+
source: string,
|
|
1412
|
+
issues: LocalPlanValidationIssue[],
|
|
1413
|
+
) {
|
|
1414
|
+
items.forEach((item, index) => {
|
|
1415
|
+
const base = listBase + item.start;
|
|
1416
|
+
if (!hasRequiredStaticId(item.source)) {
|
|
1417
|
+
addValidationIssue(
|
|
1418
|
+
issues,
|
|
1419
|
+
file,
|
|
1420
|
+
source,
|
|
1421
|
+
base,
|
|
1422
|
+
`Checklist items[${index}].id is required by the Plan renderer schema; add a stable string id.`,
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
if (!hasRequiredStaticString(item.source, "label")) {
|
|
1426
|
+
addValidationIssue(
|
|
1427
|
+
issues,
|
|
1428
|
+
file,
|
|
1429
|
+
source,
|
|
1430
|
+
base,
|
|
1431
|
+
`Checklist items[${index}].label is required by the Plan renderer schema; add a non-empty string label.`,
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
function checkQuestionObjectList(
|
|
1438
|
+
questionObjects: Array<{ source: string; start: number }>,
|
|
1439
|
+
listBase: number,
|
|
1440
|
+
tag: string,
|
|
1441
|
+
file: string,
|
|
1442
|
+
source: string,
|
|
1443
|
+
issues: LocalPlanValidationIssue[],
|
|
1444
|
+
) {
|
|
1445
|
+
questionObjects.forEach((question, questionIndex) => {
|
|
1446
|
+
const questionBase = listBase + question.start;
|
|
1447
|
+
if (!hasRequiredStaticId(question.source)) {
|
|
1448
|
+
addValidationIssue(
|
|
1449
|
+
issues,
|
|
1450
|
+
file,
|
|
1451
|
+
source,
|
|
1452
|
+
questionBase,
|
|
1453
|
+
`${tag} questions[${questionIndex}].id is required by the Plan renderer schema; add a stable string id.`,
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
if (!hasRequiredStaticString(question.source, "title")) {
|
|
1457
|
+
addValidationIssue(
|
|
1458
|
+
issues,
|
|
1459
|
+
file,
|
|
1460
|
+
source,
|
|
1461
|
+
questionBase,
|
|
1462
|
+
`${tag} questions[${questionIndex}].title is required by the Plan renderer schema; add a non-empty string title.`,
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
if (
|
|
1466
|
+
!hasRequiredEnumLiteral(question.source, "mode", [
|
|
1467
|
+
"single",
|
|
1468
|
+
"multi",
|
|
1469
|
+
"freeform",
|
|
1470
|
+
])
|
|
1471
|
+
) {
|
|
1472
|
+
addValidationIssue(
|
|
1473
|
+
issues,
|
|
1474
|
+
file,
|
|
1475
|
+
source,
|
|
1476
|
+
questionBase,
|
|
1477
|
+
`${tag} questions[${questionIndex}].mode is required by the Plan renderer schema; use "single", "multi", or "freeform".`,
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
const options = readTopLevelObjectProperty(question.source, "options");
|
|
1481
|
+
if (!options) return;
|
|
1482
|
+
const optionObjects = extractTopLevelObjectLiterals(options.value);
|
|
1483
|
+
if (!optionObjects) {
|
|
1484
|
+
addValidationIssue(
|
|
1485
|
+
issues,
|
|
1486
|
+
file,
|
|
1487
|
+
source,
|
|
1488
|
+
questionBase + options.valueStart,
|
|
1489
|
+
`${tag} questions[${questionIndex}].options must be an inline array of object literals.`,
|
|
1490
|
+
);
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
optionObjects.forEach((option, optionIndex) => {
|
|
1494
|
+
const optionBase = questionBase + options.valueStart + option.start;
|
|
1495
|
+
if (!hasRequiredStaticId(option.source)) {
|
|
1496
|
+
addValidationIssue(
|
|
1497
|
+
issues,
|
|
1498
|
+
file,
|
|
1499
|
+
source,
|
|
1500
|
+
optionBase,
|
|
1501
|
+
`${tag} questions[${questionIndex}].options[${optionIndex}].id is required by the Plan renderer schema; add a stable string id.`,
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
if (!hasRequiredStaticString(option.source, "label")) {
|
|
1505
|
+
addValidationIssue(
|
|
1506
|
+
issues,
|
|
1507
|
+
file,
|
|
1508
|
+
source,
|
|
1509
|
+
optionBase,
|
|
1510
|
+
`${tag} questions[${questionIndex}].options[${optionIndex}].label is required by the Plan renderer schema; add a non-empty string label.`,
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
});
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function lintChecklistShape(
|
|
1518
|
+
file: string,
|
|
1519
|
+
source: string,
|
|
1520
|
+
issues: LocalPlanValidationIssue[],
|
|
1521
|
+
) {
|
|
1522
|
+
const scanSource = maskCodeRegions(source);
|
|
1523
|
+
const re = /<Checklist\b/g;
|
|
1524
|
+
let match: RegExpExecArray | null;
|
|
1525
|
+
while ((match = re.exec(scanSource))) {
|
|
1526
|
+
const start = match.index;
|
|
1527
|
+
const openingEnd = findJsxOpeningTagEnd(scanSource, start);
|
|
1528
|
+
if (openingEnd < 0) continue;
|
|
1529
|
+
const opening = source.slice(start, openingEnd + 1);
|
|
1530
|
+
const items = readJsxAttribute(opening, "items");
|
|
1531
|
+
if (!items) continue;
|
|
1532
|
+
if (items.kind !== "expression") {
|
|
1533
|
+
addValidationIssue(
|
|
1534
|
+
issues,
|
|
1535
|
+
file,
|
|
1536
|
+
source,
|
|
1537
|
+
start + items.start,
|
|
1538
|
+
"Checklist items must be an inline array expression with stable item ids.",
|
|
1539
|
+
);
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
const objects = extractTopLevelObjectLiterals(items.value);
|
|
1543
|
+
if (!objects) {
|
|
1544
|
+
addValidationIssue(
|
|
1545
|
+
issues,
|
|
1546
|
+
file,
|
|
1547
|
+
source,
|
|
1548
|
+
start + items.start,
|
|
1549
|
+
"Checklist items must be an inline array of object literals so local check can validate the renderer schema.",
|
|
1550
|
+
);
|
|
1551
|
+
continue;
|
|
1552
|
+
}
|
|
1553
|
+
checkChecklistItemList(objects, start + items.start, file, source, issues);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function lintQuestionFormShape(
|
|
1558
|
+
file: string,
|
|
1559
|
+
source: string,
|
|
1560
|
+
issues: LocalPlanValidationIssue[],
|
|
1561
|
+
) {
|
|
1562
|
+
const scanSource = maskCodeRegions(source);
|
|
1563
|
+
const re = /<(QuestionForm|VisualQuestions)\b/g;
|
|
1564
|
+
let match: RegExpExecArray | null;
|
|
1565
|
+
while ((match = re.exec(scanSource))) {
|
|
1566
|
+
const start = match.index;
|
|
1567
|
+
const tag = match[1] ?? "QuestionForm";
|
|
1568
|
+
const openingEnd = findJsxOpeningTagEnd(scanSource, start);
|
|
1569
|
+
if (openingEnd < 0) continue;
|
|
1570
|
+
const opening = source.slice(start, openingEnd + 1);
|
|
1571
|
+
const questions = readJsxAttribute(opening, "questions");
|
|
1572
|
+
if (!questions) {
|
|
1573
|
+
addValidationIssue(
|
|
1574
|
+
issues,
|
|
1575
|
+
file,
|
|
1576
|
+
source,
|
|
1577
|
+
start,
|
|
1578
|
+
`${tag} requires a questions array with at least one question object.`,
|
|
1579
|
+
);
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1582
|
+
if (questions.kind !== "expression") {
|
|
1583
|
+
addValidationIssue(
|
|
1584
|
+
issues,
|
|
1585
|
+
file,
|
|
1586
|
+
source,
|
|
1587
|
+
start + questions.start,
|
|
1588
|
+
`${tag} questions must be an inline array expression with stable question ids.`,
|
|
1589
|
+
);
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
const questionObjects = extractTopLevelObjectLiterals(questions.value);
|
|
1593
|
+
if (!questionObjects || questionObjects.length === 0) {
|
|
1594
|
+
addValidationIssue(
|
|
1595
|
+
issues,
|
|
1596
|
+
file,
|
|
1597
|
+
source,
|
|
1598
|
+
start + questions.start,
|
|
1599
|
+
`${tag} questions must be a non-empty inline array of object literals.`,
|
|
1600
|
+
);
|
|
1601
|
+
continue;
|
|
1602
|
+
}
|
|
1603
|
+
checkQuestionObjectList(
|
|
1604
|
+
questionObjects,
|
|
1605
|
+
start + questions.start,
|
|
1606
|
+
tag,
|
|
1607
|
+
file,
|
|
1608
|
+
source,
|
|
1609
|
+
issues,
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
function staticStringLiteralValue(
|
|
1615
|
+
value: string | undefined,
|
|
1616
|
+
): string | undefined {
|
|
1617
|
+
if (value === undefined) return undefined;
|
|
1618
|
+
const trimmed = value.trim();
|
|
1619
|
+
const quote = trimmed[0];
|
|
1620
|
+
if (quote !== '"' && quote !== "'" && quote !== "`") return undefined;
|
|
1621
|
+
if (!trimmed.endsWith(quote) || trimmed.length < 2) return undefined;
|
|
1622
|
+
if (quote === "`" && /\$\{/.test(trimmed)) return undefined;
|
|
1623
|
+
return trimmed.slice(1, -1);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// Container blocks authored with a JSON attribute array (`<TabsBlock tabs={[…]}
|
|
1627
|
+
// />`) carry their children as nested block objects, not top-level JSX tags, so
|
|
1628
|
+
// the tag-based linters above never inspect them — the exact "false green" the
|
|
1629
|
+
// renderer catches because it validates the whole parsed block tree. Walk those
|
|
1630
|
+
// nested `blocks: [...]` arrays and enforce the same per-block required fields,
|
|
1631
|
+
// recursing through nested containers. This is still a subset of the renderer
|
|
1632
|
+
// schema (which is why `verify` is the authority), but it closes the common
|
|
1633
|
+
// nested checklist / question-form / missing-id case offline.
|
|
1634
|
+
const NESTED_CONTAINER_TAGS: ReadonlyArray<{ tag: string; attr: string }> = [
|
|
1635
|
+
{ tag: "TabsBlock", attr: "tabs" },
|
|
1636
|
+
{ tag: "Tabs", attr: "tabs" },
|
|
1637
|
+
{ tag: "Columns", attr: "columns" },
|
|
1638
|
+
];
|
|
1639
|
+
|
|
1640
|
+
function lintNestedContainerEntry(
|
|
1641
|
+
entrySource: string,
|
|
1642
|
+
entryBase: number,
|
|
1643
|
+
file: string,
|
|
1644
|
+
source: string,
|
|
1645
|
+
issues: LocalPlanValidationIssue[],
|
|
1646
|
+
) {
|
|
1647
|
+
const blocksProp = readTopLevelObjectProperty(entrySource, "blocks");
|
|
1648
|
+
if (!blocksProp) return;
|
|
1649
|
+
const blockObjects = extractTopLevelObjectLiterals(blocksProp.value);
|
|
1650
|
+
if (!blockObjects) return;
|
|
1651
|
+
const blocksBase = entryBase + blocksProp.valueStart;
|
|
1652
|
+
for (const block of blockObjects) {
|
|
1653
|
+
lintNestedBlock(
|
|
1654
|
+
block.source,
|
|
1655
|
+
blocksBase + block.start,
|
|
1656
|
+
file,
|
|
1657
|
+
source,
|
|
1658
|
+
issues,
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
function lintNestedBlock(
|
|
1664
|
+
blockSource: string,
|
|
1665
|
+
blockBase: number,
|
|
1666
|
+
file: string,
|
|
1667
|
+
source: string,
|
|
1668
|
+
issues: LocalPlanValidationIssue[],
|
|
1669
|
+
) {
|
|
1670
|
+
const typeProp = readTopLevelObjectProperty(blockSource, "type");
|
|
1671
|
+
const type = staticStringLiteralValue(typeProp?.value);
|
|
1672
|
+
if (!hasRequiredStaticId(blockSource)) {
|
|
1673
|
+
addValidationIssue(
|
|
1674
|
+
issues,
|
|
1675
|
+
file,
|
|
1676
|
+
source,
|
|
1677
|
+
blockBase,
|
|
1678
|
+
`Nested ${type ?? "block"} is missing a required string \`id\`; the Plan renderer schema validates every nested block.`,
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
const dataProp = readTopLevelObjectProperty(blockSource, "data");
|
|
1682
|
+
if (!dataProp) return;
|
|
1683
|
+
const dataBase = blockBase + dataProp.valueStart;
|
|
1684
|
+
|
|
1685
|
+
if (type === "checklist") {
|
|
1686
|
+
const itemsProp = readTopLevelObjectProperty(dataProp.value, "items");
|
|
1687
|
+
if (!itemsProp) return;
|
|
1688
|
+
const items = extractTopLevelObjectLiterals(itemsProp.value);
|
|
1689
|
+
if (items) {
|
|
1690
|
+
checkChecklistItemList(
|
|
1691
|
+
items,
|
|
1692
|
+
dataBase + itemsProp.valueStart,
|
|
1693
|
+
file,
|
|
1694
|
+
source,
|
|
1695
|
+
issues,
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
// visual-questions shares question-form's schema; lint both.
|
|
1701
|
+
if (type === "question-form" || type === "visual-questions") {
|
|
1702
|
+
const questionsProp = readTopLevelObjectProperty(
|
|
1703
|
+
dataProp.value,
|
|
1704
|
+
"questions",
|
|
1705
|
+
);
|
|
1706
|
+
if (!questionsProp) return;
|
|
1707
|
+
const questionObjects = extractTopLevelObjectLiterals(questionsProp.value);
|
|
1708
|
+
if (questionObjects) {
|
|
1709
|
+
checkQuestionObjectList(
|
|
1710
|
+
questionObjects,
|
|
1711
|
+
dataBase + questionsProp.valueStart,
|
|
1712
|
+
type,
|
|
1713
|
+
file,
|
|
1714
|
+
source,
|
|
1715
|
+
issues,
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
if (type === "tabs" || type === "columns") {
|
|
1721
|
+
const innerAttr = type === "tabs" ? "tabs" : "columns";
|
|
1722
|
+
const innerProp = readTopLevelObjectProperty(dataProp.value, innerAttr);
|
|
1723
|
+
if (!innerProp) return;
|
|
1724
|
+
const innerEntries = extractTopLevelObjectLiterals(innerProp.value);
|
|
1725
|
+
if (!innerEntries) return;
|
|
1726
|
+
const innerBase = dataBase + innerProp.valueStart;
|
|
1727
|
+
for (const entry of innerEntries) {
|
|
1728
|
+
lintNestedContainerEntry(
|
|
1729
|
+
entry.source,
|
|
1730
|
+
innerBase + entry.start,
|
|
1731
|
+
file,
|
|
1732
|
+
source,
|
|
1733
|
+
issues,
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
function lintNestedContainerBlocks(
|
|
1740
|
+
file: string,
|
|
1741
|
+
source: string,
|
|
1742
|
+
issues: LocalPlanValidationIssue[],
|
|
1743
|
+
) {
|
|
1744
|
+
const scanSource = maskCodeRegions(source);
|
|
1745
|
+
for (const { tag, attr } of NESTED_CONTAINER_TAGS) {
|
|
1746
|
+
const re = new RegExp(`<${tag}\\b`, "g");
|
|
1747
|
+
let match: RegExpExecArray | null;
|
|
1748
|
+
while ((match = re.exec(scanSource))) {
|
|
1749
|
+
const start = match.index;
|
|
1750
|
+
const openingEnd = findJsxOpeningTagEnd(scanSource, start);
|
|
1751
|
+
if (openingEnd < 0) continue;
|
|
1752
|
+
const opening = source.slice(start, openingEnd + 1);
|
|
1753
|
+
const attrValue = readJsxAttribute(opening, attr);
|
|
1754
|
+
if (!attrValue || attrValue.kind !== "expression") continue;
|
|
1755
|
+
const entries = extractTopLevelObjectLiterals(attrValue.value);
|
|
1756
|
+
if (!entries) continue;
|
|
1757
|
+
const entriesBase = start + attrValue.start;
|
|
1758
|
+
for (const entry of entries) {
|
|
1759
|
+
lintNestedContainerEntry(
|
|
1760
|
+
entry.source,
|
|
1761
|
+
entriesBase + entry.start,
|
|
1762
|
+
file,
|
|
1763
|
+
source,
|
|
1764
|
+
issues,
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// Blank out fenced code blocks and inline code spans (preserving newlines and
|
|
1772
|
+
// length) so block-tag linters don't trip on documentation examples written in
|
|
1773
|
+
// prose — e.g. an inline `<WireframeBlock><Screen>...</Screen></WireframeBlock>`
|
|
1774
|
+
// example is not a real block to validate. Real blocks (outside code) are left
|
|
1775
|
+
// intact, so their offsets/line numbers stay correct. Without this the default
|
|
1776
|
+
// `plan local init` scaffold fails its own `plan local check`/`serve` lint.
|
|
1777
|
+
function maskCodeRegions(source: string): string {
|
|
1778
|
+
const blank = (s: string) => s.replace(/[^\n]/g, " ");
|
|
1779
|
+
return source.replace(/```[\s\S]*?```/g, blank).replace(/`[^`\n]*`/g, blank);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
export function validateLocalPlanFiles(
|
|
1783
|
+
files: LocalPlanFiles,
|
|
1784
|
+
): LocalPlanValidationIssue[] {
|
|
1785
|
+
const issues: LocalPlanValidationIssue[] = [];
|
|
1786
|
+
for (const entry of localPlanSourceEntries(files)) {
|
|
1787
|
+
const source = maskCodeRegions(entry.source);
|
|
1788
|
+
lintWireframeBlocks(entry.file, source, issues);
|
|
1789
|
+
lintColumnsBlocks(entry.file, source, issues);
|
|
1790
|
+
lintChecklistShape(entry.file, entry.source, issues);
|
|
1791
|
+
lintQuestionFormShape(entry.file, entry.source, issues);
|
|
1792
|
+
lintNestedContainerBlocks(entry.file, entry.source, issues);
|
|
1793
|
+
}
|
|
1794
|
+
return issues;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
export function assertLocalPlanFilesValid(files: LocalPlanFiles): void {
|
|
1798
|
+
const issues = validateLocalPlanFiles(files);
|
|
1799
|
+
if (issues.length === 0) return;
|
|
1800
|
+
const details = issues
|
|
1801
|
+
.slice(0, 8)
|
|
1802
|
+
.map((issue) => `${issue.file}:${issue.line} ${issue.message}`)
|
|
1803
|
+
.join("\n");
|
|
1804
|
+
const overflow =
|
|
1805
|
+
issues.length > 8 ? `\n...plus ${issues.length - 8} more issues` : "";
|
|
1806
|
+
throw new Error(
|
|
1807
|
+
`Local plan source validation failed:\n${details}${overflow}\nRun \`npx @agent-native/core@latest plan blocks --out plan-blocks.md\` and update the MDX to the documented block shapes.`,
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
export function buildLocalPlanPreviewHtml(
|
|
1812
|
+
input: LocalPlanPreviewInput,
|
|
1813
|
+
): string {
|
|
1814
|
+
const files = readLocalPlanFiles(input.dir);
|
|
1815
|
+
assertLocalPlanFilesValid(files);
|
|
1816
|
+
const parsed = stripFrontmatter(files.planMdx);
|
|
1817
|
+
const title =
|
|
1818
|
+
input.title ||
|
|
1819
|
+
parsed.frontmatter.title ||
|
|
1820
|
+
firstHeading(parsed.body) ||
|
|
1821
|
+
path.basename(files.dir);
|
|
1822
|
+
const brief = input.brief || parsed.frontmatter.brief || "";
|
|
1823
|
+
const kind = input.kind || normalizeKind(parsed.frontmatter.kind);
|
|
1824
|
+
const blocks = splitMdxBlocks(parsed.body);
|
|
1825
|
+
const sourceFiles = [
|
|
1826
|
+
["plan.mdx", files.planMdx],
|
|
1827
|
+
["canvas.mdx", files.canvasMdx],
|
|
1828
|
+
["prototype.mdx", files.prototypeMdx],
|
|
1829
|
+
[".plan-state.json", files.stateJson],
|
|
1830
|
+
].filter((entry): entry is [string, string] => Boolean(entry[1]));
|
|
1831
|
+
|
|
1832
|
+
return `<!doctype html>
|
|
1833
|
+
<html lang="en">
|
|
1834
|
+
<head>
|
|
1835
|
+
<meta charset="utf-8" />
|
|
1836
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1837
|
+
<title>${escapeHtml(title)}</title>
|
|
1838
|
+
<style>
|
|
1839
|
+
:root {
|
|
1840
|
+
color-scheme: light dark;
|
|
1841
|
+
--bg: #f8fafc;
|
|
1842
|
+
--paper: #ffffff;
|
|
1843
|
+
--ink: #0f172a;
|
|
1844
|
+
--muted: #64748b;
|
|
1845
|
+
--line: #cbd5e1;
|
|
1846
|
+
--accent: #2563eb;
|
|
1847
|
+
--soft: #eff6ff;
|
|
1848
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1849
|
+
}
|
|
1850
|
+
@media (prefers-color-scheme: dark) {
|
|
1851
|
+
:root {
|
|
1852
|
+
--bg: #020617;
|
|
1853
|
+
--paper: #0f172a;
|
|
1854
|
+
--ink: #e2e8f0;
|
|
1855
|
+
--muted: #94a3b8;
|
|
1856
|
+
--line: #334155;
|
|
1857
|
+
--soft: #172554;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
body { margin: 0; background: var(--bg); color: var(--ink); }
|
|
1861
|
+
main { max-width: 1040px; margin: 0 auto; padding: 40px 20px 56px; }
|
|
1862
|
+
header { margin-bottom: 28px; }
|
|
1863
|
+
h1 { font-size: clamp(2rem, 5vw, 3.5rem); line-height: 1; margin: 0 0 12px; }
|
|
1864
|
+
h2 { font-size: 1.35rem; margin: 28px 0 10px; }
|
|
1865
|
+
h3 { font-size: 1.05rem; margin: 0 0 8px; }
|
|
1866
|
+
p, li { line-height: 1.65; }
|
|
1867
|
+
.meta { display: flex; flex-wrap: wrap; gap: 8px; color: var(--muted); font-size: 0.9rem; }
|
|
1868
|
+
.pill { border: 1px solid var(--line); border-radius: 999px; padding: 4px 10px; background: var(--paper); }
|
|
1869
|
+
.notice { background: var(--soft); border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; margin: 18px 0; }
|
|
1870
|
+
.block, details { background: var(--paper); border: 1px solid var(--line); border-radius: 8px; padding: 18px; margin: 14px 0; }
|
|
1871
|
+
.component summary { cursor: pointer; color: var(--accent); font-weight: 650; }
|
|
1872
|
+
pre { overflow: auto; border-radius: 8px; border: 1px solid var(--line); padding: 14px; background: rgba(148, 163, 184, 0.12); }
|
|
1873
|
+
code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.9rem; }
|
|
1874
|
+
.source-tabs { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; }
|
|
1875
|
+
</style>
|
|
1876
|
+
</head>
|
|
1877
|
+
<body>
|
|
1878
|
+
<main>
|
|
1879
|
+
<header>
|
|
1880
|
+
<div class="meta">
|
|
1881
|
+
<span class="pill">${kind === "recap" ? "Visual recap" : "Visual plan"}</span>
|
|
1882
|
+
<span class="pill">Local-files mode</span>
|
|
1883
|
+
<span class="pill">No DB writes</span>
|
|
1884
|
+
</div>
|
|
1885
|
+
<h1>${escapeHtml(title)}</h1>
|
|
1886
|
+
${brief ? `<p>${escapeHtml(brief)}</p>` : ""}
|
|
1887
|
+
<div class="notice">
|
|
1888
|
+
This preview was generated entirely from local files. It does not call
|
|
1889
|
+
the Plan MCP server, the Plan app action surface, a hosted service, or a
|
|
1890
|
+
database. Edit the MDX files and regenerate this preview to update it.
|
|
1891
|
+
</div>
|
|
1892
|
+
</header>
|
|
1893
|
+
<section>
|
|
1894
|
+
${blocks
|
|
1895
|
+
.map((block) =>
|
|
1896
|
+
block.type === "component"
|
|
1897
|
+
? `<details class="component block" open><summary>${escapeHtml(
|
|
1898
|
+
block.name || "MDX component",
|
|
1899
|
+
)}</summary><pre><code>${escapeHtml(block.value)}</code></pre></details>`
|
|
1900
|
+
: `<article class="block">${renderMarkdownish(block.value)}</article>`,
|
|
1901
|
+
)
|
|
1902
|
+
.join("\n")}
|
|
1903
|
+
</section>
|
|
1904
|
+
<section>
|
|
1905
|
+
<h2>Local Source Files</h2>
|
|
1906
|
+
<div class="source-tabs">
|
|
1907
|
+
${sourceFiles
|
|
1908
|
+
.map(
|
|
1909
|
+
([name, source]) =>
|
|
1910
|
+
`<details><summary>${escapeHtml(name)}</summary><pre><code>${escapeHtml(
|
|
1911
|
+
source,
|
|
1912
|
+
)}</code></pre></details>`,
|
|
1913
|
+
)
|
|
1914
|
+
.join("\n")}
|
|
1915
|
+
</div>
|
|
1916
|
+
</section>
|
|
1917
|
+
</main>
|
|
1918
|
+
</body>
|
|
1919
|
+
</html>
|
|
1920
|
+
`;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
export function writeLocalPlanPreview(input: {
|
|
1924
|
+
dir: string;
|
|
1925
|
+
out?: string;
|
|
1926
|
+
kind?: LocalPlanKind;
|
|
1927
|
+
title?: string;
|
|
1928
|
+
brief?: string;
|
|
1929
|
+
appUrl?: string;
|
|
1930
|
+
open?: boolean;
|
|
1931
|
+
openUrl?: (url: string) => OpenLocalUrlResult;
|
|
1932
|
+
}): LocalPlanPreviewResult {
|
|
1933
|
+
const dir = path.resolve(input.dir);
|
|
1934
|
+
const files = readLocalPlanFiles(dir);
|
|
1935
|
+
assertLocalPlanFilesValid(files);
|
|
1936
|
+
const parsed = stripFrontmatter(files.planMdx);
|
|
1937
|
+
const kind = input.kind || normalizeKind(parsed.frontmatter.kind);
|
|
1938
|
+
const title =
|
|
1939
|
+
input.title ||
|
|
1940
|
+
parsed.frontmatter.title ||
|
|
1941
|
+
firstHeading(parsed.body) ||
|
|
1942
|
+
path.basename(dir);
|
|
1943
|
+
const out = input.out ? path.resolve(input.out) : undefined;
|
|
1944
|
+
if (out) {
|
|
1945
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
1946
|
+
fs.writeFileSync(out, buildLocalPlanPreviewHtml({ ...input, dir, kind }));
|
|
1947
|
+
}
|
|
1948
|
+
const result: LocalPlanPreviewResult = {
|
|
1949
|
+
ok: true,
|
|
1950
|
+
dir,
|
|
1951
|
+
...(out ? { out } : {}),
|
|
1952
|
+
url: out ? pathToFileURL(out).href : localPlanPreviewUrl(dir, input.appUrl),
|
|
1953
|
+
title,
|
|
1954
|
+
kind,
|
|
1955
|
+
files: localPlanFileList(files),
|
|
1956
|
+
};
|
|
1957
|
+
if (!input.open) return result;
|
|
1958
|
+
|
|
1959
|
+
const openResult = (input.openUrl || openLocalUrl)(result.url);
|
|
1960
|
+
return {
|
|
1961
|
+
...result,
|
|
1962
|
+
opened: openResult.ok,
|
|
1963
|
+
openCommand: openResult.command,
|
|
1964
|
+
...(openResult.error ? { openError: openResult.error } : {}),
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
function readLocalPlanBridgeComments(dir: string): LocalPlanBridgeComment[] {
|
|
1969
|
+
try {
|
|
1970
|
+
const raw = fs.readFileSync(
|
|
1971
|
+
path.join(dir, LOCAL_PLAN_COMMENTS_FILE),
|
|
1972
|
+
"utf-8",
|
|
1973
|
+
);
|
|
1974
|
+
const parsed = JSON.parse(raw);
|
|
1975
|
+
return Array.isArray(parsed)
|
|
1976
|
+
? (parsed as LocalPlanBridgeComment[]).filter(
|
|
1977
|
+
(comment) => comment && typeof comment.id === "string",
|
|
1978
|
+
)
|
|
1979
|
+
: [];
|
|
1980
|
+
} catch {
|
|
1981
|
+
return [];
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
function writeLocalPlanBridgeComments(
|
|
1986
|
+
dir: string,
|
|
1987
|
+
comments: LocalPlanBridgeComment[],
|
|
1988
|
+
): void {
|
|
1989
|
+
const target = path.join(dir, LOCAL_PLAN_COMMENTS_FILE);
|
|
1990
|
+
if (comments.length === 0) {
|
|
1991
|
+
fs.rmSync(target, { force: true });
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
fs.writeFileSync(target, `${JSON.stringify(comments, null, 2)}\n`, "utf-8");
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
function normalizeBridgeCommentString(value: unknown, fallback = ""): string {
|
|
1998
|
+
return typeof value === "string" ? value : fallback;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
function normalizeBridgeCommentNullableString(value: unknown): string | null {
|
|
2002
|
+
const text = typeof value === "string" ? value.trim() : "";
|
|
2003
|
+
return text || null;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
function normalizeBridgeCommentStatus(value: unknown): string {
|
|
2007
|
+
return value === "resolved" ? "resolved" : "open";
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
function newLocalBridgeCommentId(): string {
|
|
2011
|
+
return `cmt_${crypto.randomBytes(8).toString("hex")}`;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
function applyLocalPlanBridgeCommentUpdate(
|
|
2015
|
+
dir: string,
|
|
2016
|
+
update: LocalPlanBridgeCommentUpdate,
|
|
2017
|
+
): LocalPlanBridgeComment[] {
|
|
2018
|
+
const existing = readLocalPlanBridgeComments(dir);
|
|
2019
|
+
const byId = new Map(existing.map((comment) => [comment.id, comment]));
|
|
2020
|
+
const now = new Date().toISOString();
|
|
2021
|
+
const planId = `local-${path.basename(dir)}`;
|
|
2022
|
+
|
|
2023
|
+
for (const input of update.comments ?? []) {
|
|
2024
|
+
const id =
|
|
2025
|
+
normalizeBridgeCommentNullableString(input.id) ??
|
|
2026
|
+
newLocalBridgeCommentId();
|
|
2027
|
+
const prev = byId.get(id);
|
|
2028
|
+
const status = normalizeBridgeCommentStatus(input.status ?? prev?.status);
|
|
2029
|
+
const message =
|
|
2030
|
+
typeof input.message === "string" ? input.message : prev?.message;
|
|
2031
|
+
if (!message?.trim()) continue;
|
|
2032
|
+
|
|
2033
|
+
if (prev) {
|
|
2034
|
+
byId.set(id, {
|
|
2035
|
+
...prev,
|
|
2036
|
+
parentCommentId:
|
|
2037
|
+
normalizeBridgeCommentNullableString(input.parentCommentId) ??
|
|
2038
|
+
prev.parentCommentId,
|
|
2039
|
+
sectionId:
|
|
2040
|
+
normalizeBridgeCommentNullableString(input.sectionId) ??
|
|
2041
|
+
prev.sectionId,
|
|
2042
|
+
kind: normalizeBridgeCommentString(input.kind, prev.kind),
|
|
2043
|
+
status,
|
|
2044
|
+
anchor:
|
|
2045
|
+
typeof input.anchor === "string" || input.anchor === null
|
|
2046
|
+
? input.anchor
|
|
2047
|
+
: prev.anchor,
|
|
2048
|
+
message,
|
|
2049
|
+
authorEmail:
|
|
2050
|
+
normalizeBridgeCommentNullableString(input.authorEmail) ??
|
|
2051
|
+
prev.authorEmail,
|
|
2052
|
+
authorName:
|
|
2053
|
+
normalizeBridgeCommentNullableString(input.authorName) ??
|
|
2054
|
+
prev.authorName,
|
|
2055
|
+
resolutionTarget: "agent",
|
|
2056
|
+
mentions: [],
|
|
2057
|
+
mentionsJson: null,
|
|
2058
|
+
resolvedAt:
|
|
2059
|
+
status === "resolved"
|
|
2060
|
+
? (input.resolvedAt ?? prev.resolvedAt ?? now)
|
|
2061
|
+
: null,
|
|
2062
|
+
resolvedBy:
|
|
2063
|
+
status === "resolved"
|
|
2064
|
+
? (input.resolvedBy ?? prev.resolvedBy ?? LOCAL_PLAN_OWNER_EMAIL)
|
|
2065
|
+
: null,
|
|
2066
|
+
updatedAt: now,
|
|
2067
|
+
});
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
byId.set(id, {
|
|
2072
|
+
id,
|
|
2073
|
+
planId,
|
|
2074
|
+
parentCommentId: normalizeBridgeCommentNullableString(
|
|
2075
|
+
input.parentCommentId,
|
|
2076
|
+
),
|
|
2077
|
+
sectionId: normalizeBridgeCommentNullableString(input.sectionId),
|
|
2078
|
+
kind: normalizeBridgeCommentString(input.kind, "annotation"),
|
|
2079
|
+
status,
|
|
2080
|
+
anchor: normalizeBridgeCommentNullableString(input.anchor),
|
|
2081
|
+
message,
|
|
2082
|
+
createdBy: "human",
|
|
2083
|
+
authorEmail:
|
|
2084
|
+
normalizeBridgeCommentNullableString(input.authorEmail) ??
|
|
2085
|
+
LOCAL_PLAN_OWNER_EMAIL,
|
|
2086
|
+
authorName: normalizeBridgeCommentNullableString(input.authorName),
|
|
2087
|
+
resolutionTarget: "agent",
|
|
2088
|
+
mentions: [],
|
|
2089
|
+
mentionsJson: null,
|
|
2090
|
+
resolvedBy: status === "resolved" ? LOCAL_PLAN_OWNER_EMAIL : null,
|
|
2091
|
+
resolvedAt: status === "resolved" ? now : null,
|
|
2092
|
+
consumedAt: null,
|
|
2093
|
+
deletedAt: null,
|
|
2094
|
+
deletedBy: null,
|
|
2095
|
+
createdAt: now,
|
|
2096
|
+
updatedAt: now,
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
for (const id of update.deletedCommentIds ?? []) {
|
|
2101
|
+
byId.delete(id);
|
|
2102
|
+
for (const comment of byId.values()) {
|
|
2103
|
+
if (comment.parentCommentId === id) byId.delete(comment.id);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
const comments = [...byId.values()].sort((a, b) =>
|
|
2108
|
+
a.createdAt === b.createdAt
|
|
2109
|
+
? a.id.localeCompare(b.id)
|
|
2110
|
+
: a.createdAt.localeCompare(b.createdAt),
|
|
2111
|
+
);
|
|
2112
|
+
writeLocalPlanBridgeComments(dir, comments);
|
|
2113
|
+
return comments;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
function buildLocalPlanBridgePayload(input: {
|
|
2117
|
+
dir: string;
|
|
2118
|
+
kind?: LocalPlanKind;
|
|
2119
|
+
title?: string;
|
|
2120
|
+
brief?: string;
|
|
2121
|
+
// `verify` brings the bridge up only to exercise transport + the authoritative
|
|
2122
|
+
// renderer check, so it must not be short-circuited by the weaker offline lint.
|
|
2123
|
+
skipSourceValidation?: boolean;
|
|
2124
|
+
}): LocalPlanBridgePayload {
|
|
2125
|
+
const dir = path.resolve(input.dir);
|
|
2126
|
+
const files = readLocalPlanFiles(dir);
|
|
2127
|
+
if (!input.skipSourceValidation) assertLocalPlanFilesValid(files);
|
|
2128
|
+
const parsed = stripFrontmatter(files.planMdx);
|
|
2129
|
+
const kind = input.kind || normalizeKind(parsed.frontmatter.kind);
|
|
2130
|
+
const title =
|
|
2131
|
+
input.title ||
|
|
2132
|
+
parsed.frontmatter.title ||
|
|
2133
|
+
firstHeading(parsed.body) ||
|
|
2134
|
+
path.basename(dir);
|
|
2135
|
+
const brief = input.brief || parsed.frontmatter.brief || "";
|
|
2136
|
+
const comments = readLocalPlanBridgeComments(dir);
|
|
2137
|
+
|
|
2138
|
+
return {
|
|
2139
|
+
ok: true,
|
|
2140
|
+
version: 1,
|
|
2141
|
+
source: "agent-native-local-bridge",
|
|
2142
|
+
localOnly: true,
|
|
2143
|
+
slug: path.basename(dir),
|
|
2144
|
+
dir,
|
|
2145
|
+
title,
|
|
2146
|
+
brief,
|
|
2147
|
+
kind,
|
|
2148
|
+
updatedAt: latestLocalPlanMtime(dir, files),
|
|
2149
|
+
files: localPlanFileList(files),
|
|
2150
|
+
mdx: localPlanMdxFolder(files),
|
|
2151
|
+
comments,
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
function latestLocalPlanMtime(dir: string, files: LocalPlanFiles): string {
|
|
2156
|
+
const candidates = [
|
|
2157
|
+
path.join(dir, "plan.mdx"),
|
|
2158
|
+
...(files.canvasMdx ? [path.join(dir, "canvas.mdx")] : []),
|
|
2159
|
+
...(files.prototypeMdx ? [path.join(dir, "prototype.mdx")] : []),
|
|
2160
|
+
...(files.stateJson ? [path.join(dir, ".plan-state.json")] : []),
|
|
2161
|
+
path.join(dir, LOCAL_PLAN_COMMENTS_FILE),
|
|
2162
|
+
...Object.keys(files.assets ?? {}).map((filename) =>
|
|
2163
|
+
path.join(dir, "assets", filename),
|
|
2164
|
+
),
|
|
2165
|
+
];
|
|
2166
|
+
let latest = 0;
|
|
2167
|
+
for (const file of candidates) {
|
|
2168
|
+
try {
|
|
2169
|
+
latest = Math.max(latest, fs.statSync(file).mtimeMs);
|
|
2170
|
+
} catch {
|
|
2171
|
+
// Ignore files deleted between the read and stat passes.
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
return new Date(latest || Date.now()).toISOString();
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
function sendBridgeJson(
|
|
2178
|
+
res: ServerResponse,
|
|
2179
|
+
status: number,
|
|
2180
|
+
payload: unknown,
|
|
2181
|
+
): void {
|
|
2182
|
+
res.writeHead(status, {
|
|
2183
|
+
"access-control-allow-origin": "*",
|
|
2184
|
+
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
2185
|
+
"access-control-allow-headers": "content-type",
|
|
2186
|
+
// Required when the hosted HTTPS Plan UI fetches this localhost bridge.
|
|
2187
|
+
"access-control-allow-private-network": "true",
|
|
2188
|
+
"cache-control": "no-store",
|
|
2189
|
+
"content-type": "application/json; charset=utf-8",
|
|
2190
|
+
"x-agent-native-local-bridge": "1",
|
|
2191
|
+
});
|
|
2192
|
+
res.end(`${JSON.stringify(payload)}\n`);
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
function readBridgeJsonBody(req: IncomingMessage): Promise<unknown> {
|
|
2196
|
+
return new Promise((resolve, reject) => {
|
|
2197
|
+
let raw = "";
|
|
2198
|
+
req.setEncoding("utf-8");
|
|
2199
|
+
req.on("data", (chunk) => {
|
|
2200
|
+
raw += chunk;
|
|
2201
|
+
if (raw.length > LOCAL_PLAN_BRIDGE_COMMENT_BODY_LIMIT) {
|
|
2202
|
+
reject(new Error("Local plan comment update is too large."));
|
|
2203
|
+
req.destroy();
|
|
2204
|
+
}
|
|
2205
|
+
});
|
|
2206
|
+
req.on("end", () => {
|
|
2207
|
+
if (!raw.trim()) {
|
|
2208
|
+
resolve({});
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
try {
|
|
2212
|
+
resolve(JSON.parse(raw));
|
|
2213
|
+
} catch {
|
|
2214
|
+
reject(new Error("Local plan comment update must be JSON."));
|
|
2215
|
+
}
|
|
2216
|
+
});
|
|
2217
|
+
req.on("error", reject);
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
function normalizeBridgeCommentUpdatePayload(
|
|
2222
|
+
payload: unknown,
|
|
2223
|
+
): LocalPlanBridgeCommentUpdate {
|
|
2224
|
+
if (!payload || typeof payload !== "object") return {};
|
|
2225
|
+
const input = payload as LocalPlanBridgeCommentUpdate;
|
|
2226
|
+
return {
|
|
2227
|
+
comments: Array.isArray(input.comments) ? input.comments : [],
|
|
2228
|
+
deletedCommentIds: Array.isArray(input.deletedCommentIds)
|
|
2229
|
+
? input.deletedCommentIds.filter((id) => typeof id === "string")
|
|
2230
|
+
: [],
|
|
2231
|
+
};
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
function bridgeRequestUrl(req: IncomingMessage): URL {
|
|
2235
|
+
return new URL(req.url || "/", "http://127.0.0.1");
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
function bridgeHostForUrl(host: string): string {
|
|
2239
|
+
if (host === "0.0.0.0" || host === "::") return "127.0.0.1";
|
|
2240
|
+
return host;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
export async function startLocalPlanBridge(input: {
|
|
2244
|
+
dir: string;
|
|
2245
|
+
kind?: LocalPlanKind;
|
|
2246
|
+
title?: string;
|
|
2247
|
+
brief?: string;
|
|
2248
|
+
appUrl?: string;
|
|
2249
|
+
host?: string;
|
|
2250
|
+
port?: number;
|
|
2251
|
+
token?: string;
|
|
2252
|
+
open?: boolean;
|
|
2253
|
+
urlFile?: string | false;
|
|
2254
|
+
openUrl?: (url: string) => OpenLocalUrlResult;
|
|
2255
|
+
skipSourceValidation?: boolean;
|
|
2256
|
+
}): Promise<LocalPlanBridgeServer> {
|
|
2257
|
+
const dir = path.resolve(input.dir);
|
|
2258
|
+
const initialPayload = buildLocalPlanBridgePayload({
|
|
2259
|
+
dir,
|
|
2260
|
+
kind: input.kind,
|
|
2261
|
+
title: input.title,
|
|
2262
|
+
brief: input.brief,
|
|
2263
|
+
skipSourceValidation: input.skipSourceValidation,
|
|
2264
|
+
});
|
|
2265
|
+
const token = input.token || crypto.randomBytes(24).toString("base64url");
|
|
2266
|
+
const host = input.host || "127.0.0.1";
|
|
2267
|
+
const appUrl = normalizeBridgeAppUrl(input.appUrl);
|
|
2268
|
+
const server = http.createServer((req, res) => {
|
|
2269
|
+
if (req.method === "OPTIONS") {
|
|
2270
|
+
sendBridgeJson(res, 204, "");
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
const url = bridgeRequestUrl(req);
|
|
2275
|
+
if (
|
|
2276
|
+
url.pathname !== "/local-plan.json" &&
|
|
2277
|
+
url.pathname !== "/local-plan-comments.json"
|
|
2278
|
+
) {
|
|
2279
|
+
sendBridgeJson(res, 404, { ok: false, error: "Not found." });
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
if (url.searchParams.get("token") !== token) {
|
|
2283
|
+
sendBridgeJson(res, 403, { ok: false, error: "Invalid bridge token." });
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
if (url.pathname === "/local-plan.json" && req.method !== "GET") {
|
|
2287
|
+
sendBridgeJson(res, 405, { ok: false, error: "Method not allowed." });
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
if (url.pathname === "/local-plan-comments.json" && req.method !== "POST") {
|
|
2291
|
+
sendBridgeJson(res, 405, { ok: false, error: "Method not allowed." });
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
void (async () => {
|
|
2296
|
+
try {
|
|
2297
|
+
if (url.pathname === "/local-plan-comments.json") {
|
|
2298
|
+
const body = await readBridgeJsonBody(req);
|
|
2299
|
+
applyLocalPlanBridgeCommentUpdate(
|
|
2300
|
+
dir,
|
|
2301
|
+
normalizeBridgeCommentUpdatePayload(body),
|
|
2302
|
+
);
|
|
2303
|
+
}
|
|
2304
|
+
sendBridgeJson(
|
|
2305
|
+
res,
|
|
2306
|
+
200,
|
|
2307
|
+
buildLocalPlanBridgePayload({
|
|
2308
|
+
dir,
|
|
2309
|
+
kind: input.kind,
|
|
2310
|
+
title: input.title,
|
|
2311
|
+
brief: input.brief,
|
|
2312
|
+
skipSourceValidation: input.skipSourceValidation,
|
|
2313
|
+
}),
|
|
2314
|
+
);
|
|
2315
|
+
} catch (error) {
|
|
2316
|
+
sendBridgeJson(res, 500, {
|
|
2317
|
+
ok: false,
|
|
2318
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
})();
|
|
2322
|
+
});
|
|
2323
|
+
|
|
2324
|
+
await new Promise<void>((resolve, reject) => {
|
|
2325
|
+
const onError = (error: Error) => {
|
|
2326
|
+
server.off("listening", onListening);
|
|
2327
|
+
reject(error);
|
|
2328
|
+
};
|
|
2329
|
+
const onListening = () => {
|
|
2330
|
+
server.off("error", onError);
|
|
2331
|
+
resolve();
|
|
2332
|
+
};
|
|
2333
|
+
server.once("error", onError);
|
|
2334
|
+
server.once("listening", onListening);
|
|
2335
|
+
server.listen(input.port ?? 0, host);
|
|
2336
|
+
});
|
|
2337
|
+
|
|
2338
|
+
const address = server.address();
|
|
2339
|
+
if (!address || typeof address === "string") {
|
|
2340
|
+
server.close();
|
|
2341
|
+
throw new Error("Local plan bridge did not bind to a TCP port.");
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
const bridgeUrl = `http://${bridgeHostForUrl(host)}:${address.port}/local-plan.json?token=${encodeURIComponent(
|
|
2345
|
+
token,
|
|
2346
|
+
)}`;
|
|
2347
|
+
const url = localPlanBridgePageUrl({ dir, bridgeUrl, appUrl });
|
|
2348
|
+
const urlFile =
|
|
2349
|
+
input.urlFile === false
|
|
2350
|
+
? undefined
|
|
2351
|
+
: writeLocalPlanUrlFile(dir, url, input.urlFile);
|
|
2352
|
+
const openResult = input.open
|
|
2353
|
+
? (input.openUrl || openLocalUrl)(url)
|
|
2354
|
+
: undefined;
|
|
2355
|
+
|
|
2356
|
+
return {
|
|
2357
|
+
server,
|
|
2358
|
+
result: {
|
|
2359
|
+
ok: true,
|
|
2360
|
+
dir,
|
|
2361
|
+
url,
|
|
2362
|
+
...(urlFile ? { urlFile } : {}),
|
|
2363
|
+
bridgeUrl,
|
|
2364
|
+
appUrl,
|
|
2365
|
+
title: initialPayload.title,
|
|
2366
|
+
kind: initialPayload.kind,
|
|
2367
|
+
files: initialPayload.files,
|
|
2368
|
+
host,
|
|
2369
|
+
port: address.port,
|
|
2370
|
+
...(openResult
|
|
2371
|
+
? {
|
|
2372
|
+
opened: openResult.ok,
|
|
2373
|
+
openCommand: openResult.command,
|
|
2374
|
+
...(openResult.error ? { openError: openResult.error } : {}),
|
|
2375
|
+
}
|
|
2376
|
+
: {}),
|
|
2377
|
+
},
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
function localPlanBridgeWarnings(input: {
|
|
2382
|
+
appUrl: string;
|
|
2383
|
+
bridgeUrl: string;
|
|
2384
|
+
}): string[] {
|
|
2385
|
+
const warnings: string[] = [];
|
|
2386
|
+
try {
|
|
2387
|
+
const appUrl = new URL(input.appUrl);
|
|
2388
|
+
const bridgeUrl = new URL(input.bridgeUrl);
|
|
2389
|
+
if (appUrl.protocol === "https:" && bridgeUrl.protocol === "http:") {
|
|
2390
|
+
warnings.push(
|
|
2391
|
+
`Chrome/Edge will ask for Local Network access so ${appUrl.hostname} can read this loopback bridge. Allow it; if you denied it, re-enable Local Network access in the ${appUrl.hostname} site settings, then retry.`,
|
|
2392
|
+
);
|
|
2393
|
+
warnings.push(
|
|
2394
|
+
"Safari may block the hosted HTTPS Plan UI from reading the HTTP localhost bridge. Use Chrome/Chromium/Edge, or pass --app-url http://localhost:8096 when running a local Plan app.",
|
|
2395
|
+
);
|
|
2396
|
+
}
|
|
2397
|
+
} catch {
|
|
2398
|
+
// The URLs were normalized earlier; ignore defensive parse failures.
|
|
2399
|
+
}
|
|
2400
|
+
return warnings;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
const VALIDATE_LOCAL_PLAN_ACTION = "validate-local-plan-source";
|
|
2404
|
+
|
|
2405
|
+
function isLoopbackAppUrl(value: string): boolean {
|
|
2406
|
+
try {
|
|
2407
|
+
const hostname = new URL(value).hostname.toLowerCase();
|
|
2408
|
+
return (
|
|
2409
|
+
hostname === "localhost" ||
|
|
2410
|
+
hostname === "[::1]" ||
|
|
2411
|
+
hostname === "::1" ||
|
|
2412
|
+
hostname === "0.0.0.0" ||
|
|
2413
|
+
hostname.startsWith("127.")
|
|
2414
|
+
);
|
|
2415
|
+
} catch {
|
|
2416
|
+
return false;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
/**
|
|
2421
|
+
* Ask a loopback Plan app to validate the folder against its real renderer schema
|
|
2422
|
+
* (`parsePlanMdxFolder` + `planContentSchema`) via the public, no-DB
|
|
2423
|
+
* `validate-local-plan-source` action. This is what makes `verify`
|
|
2424
|
+
* authoritative without transmitting local plan source off-device. Remote app
|
|
2425
|
+
* URLs are intentionally skipped: local-files privacy mode must never POST MDX
|
|
2426
|
+
* or assets to a hosted validation action. Degrades gracefully (`ran: false`)
|
|
2427
|
+
* when no local Plan app is running or it predates the action.
|
|
2428
|
+
*/
|
|
2429
|
+
export async function fetchRendererValidation(input: {
|
|
2430
|
+
appUrl: string;
|
|
2431
|
+
files: LocalPlanFiles;
|
|
2432
|
+
fetchFn?: typeof fetch;
|
|
2433
|
+
}): Promise<RendererValidation> {
|
|
2434
|
+
const fetchFn = input.fetchFn ?? fetch;
|
|
2435
|
+
const endpoint = planActionEndpoint(input.appUrl, VALIDATE_LOCAL_PLAN_ACTION);
|
|
2436
|
+
if (!isLoopbackAppUrl(input.appUrl)) {
|
|
2437
|
+
return {
|
|
2438
|
+
ran: false,
|
|
2439
|
+
endpoint,
|
|
2440
|
+
error:
|
|
2441
|
+
"Skipped remote renderer validation to keep local plan source on this device.",
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
try {
|
|
2445
|
+
const response = await fetchActionWithTimeout(
|
|
2446
|
+
endpoint,
|
|
2447
|
+
{
|
|
2448
|
+
method: "POST",
|
|
2449
|
+
headers: {
|
|
2450
|
+
accept: "application/json",
|
|
2451
|
+
"content-type": "application/json",
|
|
2452
|
+
},
|
|
2453
|
+
body: JSON.stringify({ mdx: localPlanMdxFolder(input.files) }),
|
|
2454
|
+
},
|
|
2455
|
+
fetchFn,
|
|
2456
|
+
);
|
|
2457
|
+
if (!response.ok) {
|
|
2458
|
+
const detail = await response.text().catch(() => "");
|
|
2459
|
+
return {
|
|
2460
|
+
ran: false,
|
|
2461
|
+
endpoint,
|
|
2462
|
+
error: `${VALIDATE_LOCAL_PLAN_ACTION} failed ${response.status} ${
|
|
2463
|
+
response.statusText
|
|
2464
|
+
}: ${sanitizeHttpDetail(detail, 300)}`,
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
const json = (await response.json().catch(() => null)) as
|
|
2468
|
+
| (Record<string, unknown> & { result?: Record<string, unknown> })
|
|
2469
|
+
| null;
|
|
2470
|
+
const body =
|
|
2471
|
+
json && isRecord(json.result) ? json.result : (json ?? undefined);
|
|
2472
|
+
if (!body || typeof body.valid !== "boolean") {
|
|
2473
|
+
return {
|
|
2474
|
+
ran: false,
|
|
2475
|
+
endpoint,
|
|
2476
|
+
error: `${VALIDATE_LOCAL_PLAN_ACTION} returned an unexpected payload.`,
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2479
|
+
const issues = Array.isArray(body.issues)
|
|
2480
|
+
? body.issues.filter(isRecord).map((issue) => ({
|
|
2481
|
+
path: typeof issue.path === "string" ? issue.path : "",
|
|
2482
|
+
message:
|
|
2483
|
+
typeof issue.message === "string" ? issue.message : "Invalid value",
|
|
2484
|
+
}))
|
|
2485
|
+
: [];
|
|
2486
|
+
return { ran: true, endpoint, valid: body.valid, issues };
|
|
2487
|
+
} catch (err) {
|
|
2488
|
+
return {
|
|
2489
|
+
ran: false,
|
|
2490
|
+
endpoint,
|
|
2491
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
function rendererValidationWarnings(
|
|
2497
|
+
validation: RendererValidation,
|
|
2498
|
+
offlineIssues: LocalPlanValidationIssue[],
|
|
2499
|
+
): string[] {
|
|
2500
|
+
if (!validation.ran) {
|
|
2501
|
+
const base = `Plan content was NOT validated against a local renderer schema (${
|
|
2502
|
+
validation.error ?? "validate endpoint unavailable"
|
|
2503
|
+
}). Fell back to the offline lint. For authoritative validation without data egress, run a local Plan app that exposes ${VALIDATE_LOCAL_PLAN_ACTION} and pass --app-url http://localhost:8096.`;
|
|
2504
|
+
if (offlineIssues.length === 0) return [base];
|
|
2505
|
+
const preview = offlineIssues
|
|
2506
|
+
.slice(0, 8)
|
|
2507
|
+
.map((issue) => `${issue.file}:${issue.line} ${issue.message}`)
|
|
2508
|
+
.join("; ");
|
|
2509
|
+
const overflow =
|
|
2510
|
+
offlineIssues.length > 8 ? ` (+${offlineIssues.length - 8} more)` : "";
|
|
2511
|
+
return [base, `Offline lint rejected this plan: ${preview}${overflow}`];
|
|
2512
|
+
}
|
|
2513
|
+
if (validation.valid) return [];
|
|
2514
|
+
const issues = validation.issues ?? [];
|
|
2515
|
+
const preview = issues
|
|
2516
|
+
.slice(0, 8)
|
|
2517
|
+
.map((issue) =>
|
|
2518
|
+
issue.path ? `${issue.path}: ${issue.message}` : issue.message,
|
|
2519
|
+
)
|
|
2520
|
+
.join("; ");
|
|
2521
|
+
const overflow = issues.length > 8 ? ` (+${issues.length - 8} more)` : "";
|
|
2522
|
+
return [
|
|
2523
|
+
`Renderer schema rejected this plan — it will not render. ${preview}${overflow}`,
|
|
2524
|
+
];
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
export async function verifyLocalPlanBridge(input: {
|
|
2528
|
+
dir: string;
|
|
2529
|
+
kind?: LocalPlanKind;
|
|
2530
|
+
title?: string;
|
|
2531
|
+
brief?: string;
|
|
2532
|
+
appUrl?: string;
|
|
2533
|
+
host?: string;
|
|
2534
|
+
port?: number;
|
|
2535
|
+
token?: string;
|
|
2536
|
+
urlFile?: string | false;
|
|
2537
|
+
fetchFn?: typeof fetch;
|
|
2538
|
+
}): Promise<LocalPlanVerifyResult> {
|
|
2539
|
+
const fetchFn = input.fetchFn ?? fetch;
|
|
2540
|
+
const files = readLocalPlanFiles(input.dir);
|
|
2541
|
+
const bridge = await startLocalPlanBridge({
|
|
2542
|
+
dir: input.dir,
|
|
2543
|
+
kind: input.kind,
|
|
2544
|
+
title: input.title,
|
|
2545
|
+
brief: input.brief,
|
|
2546
|
+
appUrl: input.appUrl,
|
|
2547
|
+
host: input.host,
|
|
2548
|
+
port: input.port,
|
|
2549
|
+
token: input.token,
|
|
2550
|
+
urlFile: input.urlFile,
|
|
2551
|
+
// Don't let the weaker offline lint abort the bridge before the authoritative
|
|
2552
|
+
// renderer check runs — verify reports the renderer's verdict, not the lint's.
|
|
2553
|
+
skipSourceValidation: true,
|
|
2554
|
+
});
|
|
2555
|
+
|
|
2556
|
+
try {
|
|
2557
|
+
const preflight = await fetchFn(bridge.result.bridgeUrl, {
|
|
2558
|
+
method: "OPTIONS",
|
|
2559
|
+
headers: {
|
|
2560
|
+
origin: bridge.result.appUrl,
|
|
2561
|
+
"access-control-request-method": "GET",
|
|
2562
|
+
"access-control-request-private-network": "true",
|
|
2563
|
+
},
|
|
2564
|
+
});
|
|
2565
|
+
const response = await fetchFn(bridge.result.bridgeUrl, {
|
|
2566
|
+
method: "GET",
|
|
2567
|
+
headers: { accept: "application/json" },
|
|
2568
|
+
});
|
|
2569
|
+
const payload = (await response.json().catch(() => null)) as
|
|
2570
|
+
| (LocalPlanBridgePayload & { mdx?: LocalPlanBridgeMdxFolder })
|
|
2571
|
+
| null;
|
|
2572
|
+
const mdxFiles = payload?.mdx
|
|
2573
|
+
? Object.keys(payload.mdx).filter((file) => file !== "assets/")
|
|
2574
|
+
: undefined;
|
|
2575
|
+
const bridgeOk =
|
|
2576
|
+
response.ok &&
|
|
2577
|
+
payload?.ok === true &&
|
|
2578
|
+
payload.source === "agent-native-local-bridge" &&
|
|
2579
|
+
payload.localOnly === true &&
|
|
2580
|
+
Boolean(payload.mdx?.["plan.mdx"]);
|
|
2581
|
+
const preflightOk =
|
|
2582
|
+
preflight.status === 204 &&
|
|
2583
|
+
preflight.headers.get("access-control-allow-origin") === "*" &&
|
|
2584
|
+
preflight.headers.get("access-control-allow-private-network") === "true";
|
|
2585
|
+
const validation = await fetchRendererValidation({
|
|
2586
|
+
appUrl: bridge.result.appUrl,
|
|
2587
|
+
files,
|
|
2588
|
+
fetchFn,
|
|
2589
|
+
});
|
|
2590
|
+
// The renderer's verdict is authoritative and gates `ok`. When it could not
|
|
2591
|
+
// run (old/unreachable Plan app), fall back to the offline lint as the
|
|
2592
|
+
// content signal so verify still catches the common cases offline instead
|
|
2593
|
+
// of silently passing unvalidated content.
|
|
2594
|
+
const offlineIssues = validation.ran ? [] : validateLocalPlanFiles(files);
|
|
2595
|
+
const contentOk = validation.ran
|
|
2596
|
+
? validation.valid === true
|
|
2597
|
+
: offlineIssues.length === 0;
|
|
2598
|
+
return {
|
|
2599
|
+
ok: bridgeOk && preflightOk && contentOk,
|
|
2600
|
+
dir: bridge.result.dir,
|
|
2601
|
+
url: bridge.result.url,
|
|
2602
|
+
...(bridge.result.urlFile ? { urlFile: bridge.result.urlFile } : {}),
|
|
2603
|
+
bridgeUrl: bridge.result.bridgeUrl,
|
|
2604
|
+
appUrl: bridge.result.appUrl,
|
|
2605
|
+
title: bridge.result.title,
|
|
2606
|
+
kind: bridge.result.kind,
|
|
2607
|
+
files: bridge.result.files,
|
|
2608
|
+
preflight: {
|
|
2609
|
+
status: preflight.status,
|
|
2610
|
+
allowOrigin: preflight.headers.get("access-control-allow-origin"),
|
|
2611
|
+
allowPrivateNetwork: preflight.headers.get(
|
|
2612
|
+
"access-control-allow-private-network",
|
|
2613
|
+
),
|
|
2614
|
+
},
|
|
2615
|
+
bridge: {
|
|
2616
|
+
status: response.status,
|
|
2617
|
+
ok: bridgeOk,
|
|
2618
|
+
...(payload?.source ? { source: payload.source } : {}),
|
|
2619
|
+
...(typeof payload?.localOnly === "boolean"
|
|
2620
|
+
? { localOnly: payload.localOnly }
|
|
2621
|
+
: {}),
|
|
2622
|
+
...(Array.isArray(payload?.files) ? { files: payload.files } : {}),
|
|
2623
|
+
...(mdxFiles ? { mdxFiles } : {}),
|
|
2624
|
+
...(payload?.error ? { error: payload.error } : {}),
|
|
2625
|
+
},
|
|
2626
|
+
validation,
|
|
2627
|
+
warnings: [
|
|
2628
|
+
...localPlanBridgeWarnings({
|
|
2629
|
+
appUrl: bridge.result.appUrl,
|
|
2630
|
+
bridgeUrl: bridge.result.bridgeUrl,
|
|
2631
|
+
}),
|
|
2632
|
+
...rendererValidationWarnings(validation, offlineIssues),
|
|
2633
|
+
],
|
|
2634
|
+
};
|
|
2635
|
+
} finally {
|
|
2636
|
+
await new Promise<void>((resolve) => bridge.server.close(() => resolve()));
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
function writeLocalPlanSkeleton(input: {
|
|
2641
|
+
dir?: string;
|
|
2642
|
+
title: string;
|
|
2643
|
+
brief?: string;
|
|
2644
|
+
kind: LocalPlanKind;
|
|
2645
|
+
force?: boolean;
|
|
2646
|
+
}): { ok: true; dir: string; files: string[] } {
|
|
2647
|
+
const dir = path.resolve(
|
|
2648
|
+
input.dir || path.join(defaultPlansDir(), localPlanFolderName(input.title)),
|
|
2649
|
+
);
|
|
2650
|
+
const planPath = path.join(dir, "plan.mdx");
|
|
2651
|
+
if (fs.existsSync(planPath) && !input.force) {
|
|
2652
|
+
throw new Error(
|
|
2653
|
+
`${planPath} already exists. Pass --force to replace the skeleton.`,
|
|
2654
|
+
);
|
|
2655
|
+
}
|
|
2656
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2657
|
+
const title = input.title;
|
|
2658
|
+
const brief =
|
|
2659
|
+
input.brief ||
|
|
2660
|
+
(input.kind === "recap"
|
|
2661
|
+
? "Local visual recap generated without Plan app database writes."
|
|
2662
|
+
: "Local visual plan generated without Plan app database writes.");
|
|
2663
|
+
const mdx = [
|
|
2664
|
+
"---",
|
|
2665
|
+
`title: "${title.replace(/"/g, '\\"')}"`,
|
|
2666
|
+
`brief: "${brief.replace(/"/g, '\\"')}"`,
|
|
2667
|
+
`kind: "${input.kind}"`,
|
|
2668
|
+
"localOnly: true",
|
|
2669
|
+
"---",
|
|
2670
|
+
"",
|
|
2671
|
+
`# ${title}`,
|
|
2672
|
+
"",
|
|
2673
|
+
brief,
|
|
2674
|
+
"",
|
|
2675
|
+
"## Review Surface",
|
|
2676
|
+
"",
|
|
2677
|
+
"Author the structured plan or recap here. You can add Agent-Native Plan MDX",
|
|
2678
|
+
'blocks such as `<WireframeBlock><Screen surface="browser">...</Screen></WireframeBlock>`,',
|
|
2679
|
+
"`<Diagram />`, `<TabsBlock />`, `<FileTree />`, or `<Diff />`; the local",
|
|
2680
|
+
"preview will show the source without publishing it to the Plan app.",
|
|
2681
|
+
"",
|
|
2682
|
+
].join("\n");
|
|
2683
|
+
fs.writeFileSync(planPath, mdx, "utf-8");
|
|
2684
|
+
fs.writeFileSync(
|
|
2685
|
+
path.join(dir, ".plan-state.json"),
|
|
2686
|
+
JSON.stringify(
|
|
2687
|
+
{
|
|
2688
|
+
localOnly: true,
|
|
2689
|
+
kind: input.kind,
|
|
2690
|
+
createdAt: new Date().toISOString(),
|
|
2691
|
+
},
|
|
2692
|
+
null,
|
|
2693
|
+
2,
|
|
2694
|
+
) + "\n",
|
|
2695
|
+
"utf-8",
|
|
2696
|
+
);
|
|
2697
|
+
return { ok: true, dir, files: ["plan.mdx", ".plan-state.json"] };
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
function runInit(args: Record<string, string | boolean>): void {
|
|
2701
|
+
const title = optionalArg(args, "title") || "Untitled local visual plan";
|
|
2702
|
+
const result = writeLocalPlanSkeleton({
|
|
2703
|
+
dir: optionalArg(args, "dir"),
|
|
2704
|
+
title,
|
|
2705
|
+
brief: optionalArg(args, "brief"),
|
|
2706
|
+
kind: normalizeKind(optionalArg(args, "kind")),
|
|
2707
|
+
force: boolArg(args, "force"),
|
|
2708
|
+
});
|
|
2709
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
function runCheck(args: Record<string, string | boolean>): void {
|
|
2713
|
+
const dir = stringArg(args, "dir");
|
|
2714
|
+
const files = readLocalPlanFiles(dir);
|
|
2715
|
+
assertLocalPlanFilesValid(files);
|
|
2716
|
+
const parsed = stripFrontmatter(files.planMdx);
|
|
2717
|
+
const result = {
|
|
2718
|
+
ok: true,
|
|
2719
|
+
noDb: true,
|
|
2720
|
+
validation: "lint-passed",
|
|
2721
|
+
validationScope: "offline-lint",
|
|
2722
|
+
note: "Quick offline lint only — NOT the full Plan renderer schema. A green here does not guarantee the plan renders. Run `plan local verify` against a Plan app for authoritative validation before handing off.",
|
|
2723
|
+
dir: files.dir,
|
|
2724
|
+
title: parsed.frontmatter.title || firstHeading(parsed.body),
|
|
2725
|
+
kind: normalizeKind(parsed.frontmatter.kind),
|
|
2726
|
+
files: {
|
|
2727
|
+
"plan.mdx": Buffer.byteLength(files.planMdx),
|
|
2728
|
+
...(files.canvasMdx
|
|
2729
|
+
? { "canvas.mdx": Buffer.byteLength(files.canvasMdx) }
|
|
2730
|
+
: {}),
|
|
2731
|
+
...(files.prototypeMdx
|
|
2732
|
+
? { "prototype.mdx": Buffer.byteLength(files.prototypeMdx) }
|
|
2733
|
+
: {}),
|
|
2734
|
+
...(files.stateJson
|
|
2735
|
+
? { ".plan-state.json": Buffer.byteLength(files.stateJson) }
|
|
2736
|
+
: {}),
|
|
2737
|
+
...(files.assets
|
|
2738
|
+
? Object.fromEntries(
|
|
2739
|
+
Object.entries(files.assets).map(([filename, base64]) => [
|
|
2740
|
+
`assets/${filename}`,
|
|
2741
|
+
Buffer.byteLength(base64, "base64"),
|
|
2742
|
+
]),
|
|
2743
|
+
)
|
|
2744
|
+
: {}),
|
|
2745
|
+
},
|
|
2746
|
+
};
|
|
2747
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
export function readVisualAnswerSourcePayload(
|
|
2751
|
+
sourcePath: string,
|
|
2752
|
+
): VisualAnswerSourcePayload {
|
|
2753
|
+
let parsed: unknown;
|
|
2754
|
+
try {
|
|
2755
|
+
parsed = JSON.parse(fs.readFileSync(sourcePath, "utf-8"));
|
|
2756
|
+
} catch (error) {
|
|
2757
|
+
throw new Error(
|
|
2758
|
+
`Could not read ${sourcePath}: ${
|
|
2759
|
+
error instanceof Error ? error.message : String(error)
|
|
2760
|
+
}`,
|
|
2761
|
+
);
|
|
2762
|
+
}
|
|
2763
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2764
|
+
throw new Error(`${sourcePath} must contain a JSON object.`);
|
|
2765
|
+
}
|
|
2766
|
+
const source = parsed as Record<string, unknown>;
|
|
2767
|
+
const mdx = source.mdx;
|
|
2768
|
+
if (!mdx || typeof mdx !== "object" || Array.isArray(mdx)) {
|
|
2769
|
+
throw new Error(`${sourcePath} must contain an mdx object.`);
|
|
2770
|
+
}
|
|
2771
|
+
const planMdx = (mdx as Record<string, unknown>)["plan.mdx"];
|
|
2772
|
+
if (typeof planMdx !== "string" || planMdx.trim().length === 0) {
|
|
2773
|
+
throw new Error(
|
|
2774
|
+
`${sourcePath} mdx["plan.mdx"] must be a non-empty string.`,
|
|
2775
|
+
);
|
|
2776
|
+
}
|
|
2777
|
+
return source as VisualAnswerSourcePayload;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
export async function publishVisualAnswerSource(
|
|
2781
|
+
input: PublishVisualAnswerInput,
|
|
2782
|
+
): Promise<{ ok: true; url: string; out: string }> {
|
|
2783
|
+
const cwd = input.cwd ?? process.cwd();
|
|
2784
|
+
const sourcePath =
|
|
2785
|
+
input.sourcePath ?? path.join(cwd, VISUAL_ANSWER_SOURCE_FILENAME);
|
|
2786
|
+
const out = input.out ?? path.join(cwd, VISUAL_ANSWER_URL_FILENAME);
|
|
2787
|
+
const token = input.token.trim();
|
|
2788
|
+
if (!token) throw new Error("Plan publish token is empty.");
|
|
2789
|
+
|
|
2790
|
+
const source = readVisualAnswerSourcePayload(sourcePath);
|
|
2791
|
+
const question = input.question ?? source.question;
|
|
2792
|
+
if (!question?.trim()) {
|
|
2793
|
+
throw new Error(
|
|
2794
|
+
`Missing visual answer question. Pass --question or set question in ${sourcePath}.`,
|
|
2795
|
+
);
|
|
2796
|
+
}
|
|
2797
|
+
const body = {
|
|
2798
|
+
question,
|
|
2799
|
+
...(input.prevPlanId ? { planId: input.prevPlanId } : {}),
|
|
2800
|
+
...(source.title ? { title: source.title } : {}),
|
|
2801
|
+
...(source.brief ? { brief: source.brief } : {}),
|
|
2802
|
+
visibility: input.visibility ?? "org",
|
|
2803
|
+
source: "imported",
|
|
2804
|
+
...((input.repo ?? source.repoPath)
|
|
2805
|
+
? { repoPath: input.repo ?? source.repoPath }
|
|
2806
|
+
: {}),
|
|
2807
|
+
...((input.sourceUrl ?? source.sourceUrl)
|
|
2808
|
+
? { sourceUrl: input.sourceUrl ?? source.sourceUrl }
|
|
2809
|
+
: {}),
|
|
2810
|
+
sourceType: normalizeVisualAnswerSourceType(
|
|
2811
|
+
input.sourceType ?? source.sourceType,
|
|
2812
|
+
),
|
|
2813
|
+
currentFocus: `visual answer: ${question.trim().slice(0, 120)}`,
|
|
2814
|
+
status: "review",
|
|
2815
|
+
mdx: source.mdx,
|
|
2816
|
+
};
|
|
2817
|
+
|
|
2818
|
+
const endpoint = planActionEndpoint(input.appUrl, "visual-answer");
|
|
2819
|
+
const fetchFn = input.fetchFn ?? fetch;
|
|
2820
|
+
const response = await fetchActionWithTimeout(
|
|
2821
|
+
endpoint,
|
|
2822
|
+
{
|
|
2823
|
+
method: "POST",
|
|
2824
|
+
headers: {
|
|
2825
|
+
accept: "application/json",
|
|
2826
|
+
"content-type": "application/json",
|
|
2827
|
+
authorization: `Bearer ${token}`,
|
|
2828
|
+
},
|
|
2829
|
+
body: JSON.stringify(body),
|
|
2830
|
+
},
|
|
2831
|
+
fetchFn,
|
|
2832
|
+
);
|
|
2833
|
+
const text = await response.text().catch((error) => String(error));
|
|
2834
|
+
if (!response.ok) {
|
|
2835
|
+
throw new Error(
|
|
2836
|
+
`visual-answer failed ${response.status} ${
|
|
2837
|
+
response.statusText
|
|
2838
|
+
}: ${sanitizeHttpDetail(text, 800)}`,
|
|
2839
|
+
);
|
|
2840
|
+
}
|
|
2841
|
+
let result: unknown = null;
|
|
2842
|
+
try {
|
|
2843
|
+
result = text ? JSON.parse(text) : null;
|
|
2844
|
+
} catch {
|
|
2845
|
+
throw new Error("visual-answer returned non-JSON output.");
|
|
2846
|
+
}
|
|
2847
|
+
const url = visualAnswerUrlFromPublishResult(result, input.appUrl);
|
|
2848
|
+
if (!url) {
|
|
2849
|
+
throw new Error(
|
|
2850
|
+
"visual-answer succeeded but did not return a usable /plans/<id> URL or plan id.",
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2853
|
+
fs.writeFileSync(path.resolve(out), `${url}\n`, "utf-8");
|
|
2854
|
+
return { ok: true, url, out };
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
async function runVisualAnswer(
|
|
2858
|
+
subcommand: string | undefined,
|
|
2859
|
+
args: Record<string, string | boolean>,
|
|
2860
|
+
): Promise<void> {
|
|
2861
|
+
if (
|
|
2862
|
+
subcommand === undefined ||
|
|
2863
|
+
subcommand === "help" ||
|
|
2864
|
+
subcommand === "--help" ||
|
|
2865
|
+
subcommand === "-h" ||
|
|
2866
|
+
args.help === true ||
|
|
2867
|
+
args.h === true
|
|
2868
|
+
) {
|
|
2869
|
+
process.stdout.write(HELP);
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2872
|
+
if (subcommand !== "publish") {
|
|
2873
|
+
process.stderr.write(
|
|
2874
|
+
`Unknown plan visual-answer subcommand: ${subcommand}\n${HELP}`,
|
|
2875
|
+
);
|
|
2876
|
+
process.exit(1);
|
|
2877
|
+
}
|
|
2878
|
+
const appUrl =
|
|
2879
|
+
optionalArg(args, "app-url") ||
|
|
2880
|
+
process.env.PLAN_VISUAL_ANSWER_APP_URL ||
|
|
2881
|
+
process.env.PLAN_RECAP_APP_URL ||
|
|
2882
|
+
DEFAULT_PLAN_APP_URL;
|
|
2883
|
+
const token =
|
|
2884
|
+
optionalArg(args, "token") ||
|
|
2885
|
+
process.env.PLAN_VISUAL_ANSWER_TOKEN ||
|
|
2886
|
+
process.env.PLAN_RECAP_TOKEN ||
|
|
2887
|
+
"";
|
|
2888
|
+
try {
|
|
2889
|
+
const result = await publishVisualAnswerSource({
|
|
2890
|
+
appUrl,
|
|
2891
|
+
token,
|
|
2892
|
+
question: optionalArg(args, "question"),
|
|
2893
|
+
sourcePath: optionalArg(args, "source") ?? VISUAL_ANSWER_SOURCE_FILENAME,
|
|
2894
|
+
out: optionalArg(args, "out") ?? VISUAL_ANSWER_URL_FILENAME,
|
|
2895
|
+
prevPlanId: optionalArg(args, "prev-plan-id"),
|
|
2896
|
+
repo: optionalArg(args, "repo"),
|
|
2897
|
+
sourceUrl: optionalArg(args, "source-url"),
|
|
2898
|
+
sourceType: optionalArg(args, "source-type"),
|
|
2899
|
+
visibility: normalizeVisibility(optionalArg(args, "visibility")),
|
|
2900
|
+
});
|
|
2901
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
2902
|
+
} catch (error) {
|
|
2903
|
+
process.stdout.write(
|
|
2904
|
+
`${JSON.stringify(
|
|
2905
|
+
{
|
|
2906
|
+
ok: false,
|
|
2907
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
2908
|
+
},
|
|
2909
|
+
null,
|
|
2910
|
+
2,
|
|
2911
|
+
)}\n`,
|
|
2912
|
+
);
|
|
2913
|
+
process.exit(1);
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
function runPreview(args: Record<string, string | boolean>): void {
|
|
2918
|
+
const result = writeLocalPlanPreview({
|
|
2919
|
+
dir: stringArg(args, "dir"),
|
|
2920
|
+
out: optionalArg(args, "out"),
|
|
2921
|
+
appUrl: optionalArg(args, "app-url"),
|
|
2922
|
+
title: optionalArg(args, "title"),
|
|
2923
|
+
brief: optionalArg(args, "brief"),
|
|
2924
|
+
open: boolArg(args, "open"),
|
|
2925
|
+
kind: optionalArg(args, "kind")
|
|
2926
|
+
? normalizeKind(optionalArg(args, "kind"))
|
|
2927
|
+
: undefined,
|
|
2928
|
+
});
|
|
2929
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
async function runServe(args: Record<string, string | boolean>): Promise<void> {
|
|
2933
|
+
const portValue = optionalArg(args, "port");
|
|
2934
|
+
const port = portValue ? Number(portValue) : undefined;
|
|
2935
|
+
if (portValue && (!Number.isInteger(port) || port! < 0 || port! > 65535)) {
|
|
2936
|
+
throw new Error("--port must be an integer between 0 and 65535.");
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
const bridge = await startLocalPlanBridge({
|
|
2940
|
+
dir: stringArg(args, "dir"),
|
|
2941
|
+
appUrl: optionalArg(args, "app-url"),
|
|
2942
|
+
title: optionalArg(args, "title"),
|
|
2943
|
+
brief: optionalArg(args, "brief"),
|
|
2944
|
+
host: optionalArg(args, "host"),
|
|
2945
|
+
port,
|
|
2946
|
+
open: boolArg(args, "open"),
|
|
2947
|
+
urlFile: optionalArg(args, "url-file") || optionalArg(args, "out"),
|
|
2948
|
+
kind: optionalArg(args, "kind")
|
|
2949
|
+
? normalizeKind(optionalArg(args, "kind"))
|
|
2950
|
+
: undefined,
|
|
2951
|
+
});
|
|
2952
|
+
|
|
2953
|
+
process.stdout.write(`${JSON.stringify(bridge.result, null, 2)}\n`);
|
|
2954
|
+
process.stderr.write(
|
|
2955
|
+
[
|
|
2956
|
+
`Local Plan bridge running at ${bridge.result.bridgeUrl}`,
|
|
2957
|
+
bridge.result.urlFile
|
|
2958
|
+
? `Open URL written to ${bridge.result.urlFile}`
|
|
2959
|
+
: "",
|
|
2960
|
+
...localPlanBridgeWarnings({
|
|
2961
|
+
appUrl: bridge.result.appUrl,
|
|
2962
|
+
bridgeUrl: bridge.result.bridgeUrl,
|
|
2963
|
+
}),
|
|
2964
|
+
"Keep this bridge command running while the Plan page is open; stopping it makes this URL unreachable.",
|
|
2965
|
+
"Press Ctrl+C to stop.",
|
|
2966
|
+
]
|
|
2967
|
+
.filter(Boolean)
|
|
2968
|
+
.join("\n") + "\n",
|
|
2969
|
+
);
|
|
2970
|
+
|
|
2971
|
+
await new Promise<void>((resolve) => {
|
|
2972
|
+
let stopped = false;
|
|
2973
|
+
const cleanup = () => {
|
|
2974
|
+
process.off("SIGINT", stop);
|
|
2975
|
+
process.off("SIGTERM", stop);
|
|
2976
|
+
};
|
|
2977
|
+
const stop = () => {
|
|
2978
|
+
if (stopped) return;
|
|
2979
|
+
stopped = true;
|
|
2980
|
+
cleanup();
|
|
2981
|
+
bridge.server.close(() => resolve());
|
|
2982
|
+
};
|
|
2983
|
+
process.once("SIGINT", stop);
|
|
2984
|
+
process.once("SIGTERM", stop);
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
async function runVerify(
|
|
2989
|
+
args: Record<string, string | boolean>,
|
|
2990
|
+
): Promise<void> {
|
|
2991
|
+
const portValue = optionalArg(args, "port");
|
|
2992
|
+
const port = portValue ? Number(portValue) : undefined;
|
|
2993
|
+
if (portValue && (!Number.isInteger(port) || port! < 0 || port! > 65535)) {
|
|
2994
|
+
throw new Error("--port must be an integer between 0 and 65535.");
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
const result = await verifyLocalPlanBridge({
|
|
2998
|
+
dir: stringArg(args, "dir"),
|
|
2999
|
+
appUrl: optionalArg(args, "app-url"),
|
|
3000
|
+
title: optionalArg(args, "title"),
|
|
3001
|
+
brief: optionalArg(args, "brief"),
|
|
3002
|
+
host: optionalArg(args, "host"),
|
|
3003
|
+
port,
|
|
3004
|
+
urlFile: optionalArg(args, "url-file") || optionalArg(args, "out") || false,
|
|
3005
|
+
kind: optionalArg(args, "kind")
|
|
3006
|
+
? normalizeKind(optionalArg(args, "kind"))
|
|
3007
|
+
: undefined,
|
|
3008
|
+
});
|
|
3009
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
3010
|
+
if (!result.ok) process.exit(1);
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
async function runBlocks(
|
|
3014
|
+
args: Record<string, string | boolean>,
|
|
3015
|
+
): Promise<void> {
|
|
3016
|
+
const format = normalizePlanBlockFormat(optionalArg(args, "format"));
|
|
3017
|
+
const appUrl =
|
|
3018
|
+
optionalArg(args, "app-url") ||
|
|
3019
|
+
process.env.PLAN_BLOCKS_APP_URL ||
|
|
3020
|
+
process.env.PLAN_RECAP_APP_URL ||
|
|
3021
|
+
DEFAULT_PLAN_APP_URL;
|
|
3022
|
+
const out = optionalArg(args, "out") || defaultPlanBlocksOut(format);
|
|
3023
|
+
const useClack = Boolean(process.stdout.isTTY) && !boolArg(args, "json");
|
|
3024
|
+
let stopSpinner: ((message?: string) => void) | undefined;
|
|
3025
|
+
let clack: typeof import("@clack/prompts") | undefined;
|
|
3026
|
+
|
|
3027
|
+
if (useClack) {
|
|
3028
|
+
clack = await import("@clack/prompts");
|
|
3029
|
+
const spinner = clack.spinner();
|
|
3030
|
+
spinner.start("Fetching Plan block catalog");
|
|
3031
|
+
stopSpinner = (message?: string) => spinner.stop(message);
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
try {
|
|
3035
|
+
const result = await fetchPlanBlockCatalog({
|
|
3036
|
+
appUrl,
|
|
3037
|
+
format,
|
|
3038
|
+
out,
|
|
3039
|
+
});
|
|
3040
|
+
if (!useClack || !clack) {
|
|
3041
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
3042
|
+
return;
|
|
3043
|
+
}
|
|
3044
|
+
stopSpinner?.("Fetched Plan block catalog");
|
|
3045
|
+
clack.note(
|
|
3046
|
+
[
|
|
3047
|
+
`Output ${path.resolve(result.out)}`,
|
|
3048
|
+
`Format ${result.format}`,
|
|
3049
|
+
typeof result.count === "number" ? `Blocks ${result.count}` : "",
|
|
3050
|
+
"Privacy No plan content sent",
|
|
3051
|
+
]
|
|
3052
|
+
.filter(Boolean)
|
|
3053
|
+
.join("\n"),
|
|
3054
|
+
"Plan block catalog",
|
|
3055
|
+
);
|
|
3056
|
+
clack.outro(`Wrote ${result.out}`);
|
|
3057
|
+
} catch (error) {
|
|
3058
|
+
if (useClack && clack) {
|
|
3059
|
+
stopSpinner?.("Plan block catalog fetch failed");
|
|
3060
|
+
clack.cancel(error instanceof Error ? error.message : String(error));
|
|
3061
|
+
process.exit(1);
|
|
3062
|
+
}
|
|
3063
|
+
throw error;
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
const HELP = `agent-native plan — local Agent-Native Plan helpers
|
|
3068
|
+
|
|
3069
|
+
Usage:
|
|
3070
|
+
agent-native plan blocks [--format reference|schema] [--app-url <url>] [--out <file>] [--json]
|
|
3071
|
+
agent-native plan visual-answer publish --question <text> [--source visual-answer-source.json] [--out visual-answer-url.txt] [--repo owner/name] [--source-url <url>] [--app-url <url>] [--token <planToken>]
|
|
3072
|
+
agent-native plan serve --dir <folder> [--app-url <url>] [--kind plan|recap] [--open] [--port <port>] [--url-file <file>]
|
|
3073
|
+
agent-native plan local init --title <title> [--brief <text>] [--kind plan|recap] [--dir <folder>] [--force]
|
|
3074
|
+
agent-native plan local check --dir <folder>
|
|
3075
|
+
agent-native plan local serve --dir <folder> [--app-url <url>] [--kind plan|recap] [--open] [--port <port>] [--url-file <file>]
|
|
3076
|
+
agent-native plan local verify --dir <folder> [--app-url <url>] [--kind plan|recap] [--port <port>]
|
|
3077
|
+
agent-native plan local preview --dir <folder> [--app-url <url>] [--kind plan|recap] [--open] [--out preview.html]
|
|
3078
|
+
|
|
3079
|
+
The blocks command fetches the no-auth, read-only get-plan-blocks catalog from
|
|
3080
|
+
the Plan app and writes plan-blocks.md (or plan-blocks.schema.json). It sends no
|
|
3081
|
+
plan content and is safe for local-files authoring before writing MDX. It uses a
|
|
3082
|
+
clack UI in interactive terminals and prints JSON for non-interactive shells or
|
|
3083
|
+
when --json is passed.
|
|
3084
|
+
|
|
3085
|
+
The visual-answer publish command sends a coding-agent-authored
|
|
3086
|
+
visual-answer-source.json file to the Plan app's visual-answer action and writes
|
|
3087
|
+
visual-answer-url.txt. The source file should contain a question, optional
|
|
3088
|
+
title/brief/repoPath/sourceUrl, and MDX files under mdx. It is the terminal
|
|
3089
|
+
handoff for /visual-answer workflows after a coding agent has inspected code via
|
|
3090
|
+
the local repo, the Plan bridge, or GitHub and authored structured Plan blocks.
|
|
3091
|
+
|
|
3092
|
+
The local subcommands are the privacy-focused no-DB path. They only read and
|
|
3093
|
+
write local files: plan.mdx, optional canvas.mdx, optional prototype.mdx, and
|
|
3094
|
+
optional .plan-state.json. They do not call the Plan MCP server, the Plan app
|
|
3095
|
+
write actions, hosted storage, or SQLite.
|
|
3096
|
+
|
|
3097
|
+
Common flow:
|
|
3098
|
+
agent-native plan blocks --out plan-blocks.md
|
|
3099
|
+
agent-native plan local init --title "Checkout review" --kind plan
|
|
3100
|
+
agent-native plan local serve --dir plans/checkout-review --open
|
|
3101
|
+
|
|
3102
|
+
\`plan local serve\` starts a tiny localhost bridge and opens the hosted Plan UI
|
|
3103
|
+
against that local-only source. The hosted app fetches the MDX from localhost in
|
|
3104
|
+
the browser; it does not write plan content to the hosted database. The served
|
|
3105
|
+
URL is written to \`.plan-url\` by default; pass \`--url-file\` to choose a
|
|
3106
|
+
different local-only file. On macOS, \`--open\` prefers Chromium browsers.
|
|
3107
|
+
Chrome/Edge will ask for Local Network access to read the loopback bridge; if
|
|
3108
|
+
you deny it, re-enable Local Network access in the plan.agent-native.com site
|
|
3109
|
+
settings and retry. Keep the bridge command running while the page is open.
|
|
3110
|
+
Safari may block the hosted HTTPS page from reading the HTTP localhost bridge;
|
|
3111
|
+
use a Chromium browser or a local Plan app via \`--app-url http://localhost:8096\`.
|
|
3112
|
+
Use \`plan local verify\` for headless bridge/CORS diagnostics that exit cleanly.
|
|
3113
|
+
Use \`plan local preview\` for a local Plan dev server route. \`preview --out\` is
|
|
3114
|
+
a legacy/debug escape hatch that writes a standalone static HTML file.
|
|
3115
|
+
\`plan serve\` is kept as a compatibility alias for \`plan local serve\`.
|
|
3116
|
+
`;
|
|
3117
|
+
|
|
3118
|
+
export async function runPlan(argv: string[]): Promise<void> {
|
|
3119
|
+
const [area, sub, ...rest] = argv;
|
|
3120
|
+
if (area === "blocks") {
|
|
3121
|
+
await runBlocks(parseArgs(argv.slice(1)));
|
|
3122
|
+
return;
|
|
3123
|
+
}
|
|
3124
|
+
if (area === "serve") {
|
|
3125
|
+
const args = parseArgs(argv.slice(1));
|
|
3126
|
+
if (args.help === true || args.h === true) {
|
|
3127
|
+
process.stdout.write(HELP);
|
|
3128
|
+
return;
|
|
3129
|
+
}
|
|
3130
|
+
await runServe(args);
|
|
3131
|
+
return;
|
|
3132
|
+
}
|
|
3133
|
+
if (area === "visual-answer") {
|
|
3134
|
+
await runVisualAnswer(sub, parseArgs(rest));
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
if (area !== "local") {
|
|
3138
|
+
// Bare `agent-native plan` / `plan help` / `plan --help` → show help on
|
|
3139
|
+
// stdout and exit 0 (informational, not an error).
|
|
3140
|
+
if (
|
|
3141
|
+
area === undefined ||
|
|
3142
|
+
area === "help" ||
|
|
3143
|
+
area === "--help" ||
|
|
3144
|
+
area === "-h"
|
|
3145
|
+
) {
|
|
3146
|
+
process.stdout.write(HELP);
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
// A non-empty, unrecognised area (e.g. `agent-native plan lokal`) is an
|
|
3150
|
+
// error: print to stderr so the CI log captures it, and exit 1 so callers
|
|
3151
|
+
// can detect the failure. This mirrors the existing behaviour for unknown
|
|
3152
|
+
// subcommands inside `plan local`.
|
|
3153
|
+
process.stderr.write(`Unknown plan area: ${area}\n${HELP}`);
|
|
3154
|
+
process.exit(1);
|
|
3155
|
+
}
|
|
3156
|
+
const args = parseArgs(rest);
|
|
3157
|
+
// `plan local <sub> --help` / `-h` shows help instead of running the
|
|
3158
|
+
// subcommand (e.g. `plan local init --help` must not scaffold a folder).
|
|
3159
|
+
if (args.help === true || args.h === true) {
|
|
3160
|
+
process.stdout.write(HELP);
|
|
3161
|
+
return;
|
|
3162
|
+
}
|
|
3163
|
+
switch (sub) {
|
|
3164
|
+
case "init":
|
|
3165
|
+
runInit(args);
|
|
3166
|
+
return;
|
|
3167
|
+
case "check":
|
|
3168
|
+
runCheck(args);
|
|
3169
|
+
return;
|
|
3170
|
+
case "preview":
|
|
3171
|
+
runPreview(args);
|
|
3172
|
+
return;
|
|
3173
|
+
case "serve":
|
|
3174
|
+
await runServe(args);
|
|
3175
|
+
return;
|
|
3176
|
+
case "verify":
|
|
3177
|
+
await runVerify(args);
|
|
3178
|
+
return;
|
|
3179
|
+
case "help":
|
|
3180
|
+
case "--help":
|
|
3181
|
+
case "-h":
|
|
3182
|
+
case undefined:
|
|
3183
|
+
process.stdout.write(HELP);
|
|
3184
|
+
return;
|
|
3185
|
+
default:
|
|
3186
|
+
process.stderr.write(`Unknown plan local subcommand: ${sub}\n${HELP}`);
|
|
3187
|
+
process.exit(1);
|
|
3188
|
+
}
|
|
3189
|
+
}
|