gsd-pi 2.70.1 → 2.71.0-dev.06b86c6
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/README.md +24 -17
- package/dist/cli.js +12 -3
- package/dist/mcp-server.js +6 -6
- package/dist/provider-migrations.d.ts +10 -0
- package/dist/provider-migrations.js +12 -0
- package/dist/resource-loader.js +136 -13
- package/dist/resources/GSD-WORKFLOW.md +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +129 -30
- package/dist/resources/extensions/get-secrets-from-user.js +17 -1
- package/dist/resources/extensions/gsd/auto-start.js +4 -12
- package/dist/resources/extensions/gsd/auto-tool-tracking.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +6 -0
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +6 -0
- package/dist/resources/extensions/gsd/commands/context.js +15 -6
- package/dist/resources/extensions/gsd/commands/dispatcher.js +12 -2
- package/dist/resources/extensions/gsd/custom-workflow-engine.js +16 -12
- package/dist/resources/extensions/gsd/dispatch-guard.js +18 -1
- package/dist/resources/extensions/gsd/error-classifier.js +1 -1
- package/dist/resources/extensions/gsd/file-lock.js +60 -0
- package/dist/resources/extensions/gsd/guided-flow.js +12 -10
- package/dist/resources/extensions/gsd/init-wizard.js +3 -11
- package/dist/resources/extensions/gsd/notification-store.js +21 -1
- package/dist/resources/extensions/gsd/notification-widget.js +1 -1
- package/dist/resources/extensions/gsd/pre-execution-checks.js +35 -2
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +2 -2
- package/dist/resources/extensions/gsd/prompts/discuss.md +33 -13
- package/dist/resources/extensions/gsd/prompts/execute-task.md +20 -19
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +2 -0
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +3 -2
- package/dist/resources/extensions/gsd/prompts/system.md +1 -0
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -1
- package/dist/resources/extensions/gsd/state.js +234 -332
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +34 -0
- package/dist/resources/extensions/gsd/workflow-events.js +25 -13
- package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +56 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +1 -1
- package/dist/resources/skills/create-skill/SKILL.md +2 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
- package/dist/web/standalone/.next/build-manifest.json +4 -4
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
- package/dist/web/standalone/.next/required-server-files.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
- package/dist/web/standalone/.next/server/chunks/63.js +3 -3
- package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware.js +2 -2
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/2826.dd3dc8bbd3025fa5.js +9 -0
- package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/page-f1e30ab6bb269149.js +1 -0
- package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-6e4d7e9a4f57bed4.js → webpack-b868033a5834586d.js} +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
- package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
- package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/env-writer.d.ts +39 -0
- package/packages/mcp-server/dist/env-writer.d.ts.map +1 -0
- package/packages/mcp-server/dist/env-writer.js +158 -0
- package/packages/mcp-server/dist/env-writer.js.map +1 -0
- package/packages/mcp-server/dist/server.d.ts +11 -2
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +102 -2
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +21 -11
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/env-writer.test.ts +280 -0
- package/packages/mcp-server/src/env-writer.ts +183 -0
- package/packages/mcp-server/src/secure-env-collect.test.ts +265 -0
- package/packages/mcp-server/src/server.ts +137 -3
- package/packages/mcp-server/src/workflow-tools.test.ts +110 -0
- package/packages/mcp-server/src/workflow-tools.ts +31 -11
- package/packages/pi-ai/dist/providers/amazon-bedrock.js +11 -2
- package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +4 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.js +8 -3
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.test.js +44 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.test.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +11 -0
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/src/providers/amazon-bedrock.ts +13 -1
- package/packages/pi-ai/src/providers/anthropic-shared.test.ts +55 -1
- package/packages/pi-ai/src/providers/anthropic-shared.ts +14 -3
- package/packages/pi-ai/src/providers/openai-completions.ts +14 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +388 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +19 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +50 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +168 -23
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +6 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +58 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +468 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +58 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +2 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +198 -29
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +66 -2
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +1 -1
- package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +1 -0
- package/packages/pi-tui/dist/components/__tests__/input.test.js +9 -0
- package/packages/pi-tui/dist/components/__tests__/input.test.js.map +1 -1
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.d.ts +2 -0
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.d.ts.map +1 -0
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js +66 -0
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js.map +1 -0
- package/packages/pi-tui/dist/components/input.d.ts +2 -0
- package/packages/pi-tui/dist/components/input.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/input.js +7 -4
- package/packages/pi-tui/dist/components/input.js.map +1 -1
- package/packages/pi-tui/dist/components/markdown.d.ts +3 -0
- package/packages/pi-tui/dist/components/markdown.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/markdown.js +17 -1
- package/packages/pi-tui/dist/components/markdown.js.map +1 -1
- package/packages/pi-tui/src/components/__tests__/input.test.ts +11 -0
- package/packages/pi-tui/src/components/__tests__/markdown-maxlines.test.ts +75 -0
- package/packages/pi-tui/src/components/input.ts +7 -4
- package/packages/pi-tui/src/components/markdown.ts +22 -1
- package/pkg/package.json +1 -1
- package/src/resources/GSD-WORKFLOW.md +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +166 -31
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +145 -0
- package/src/resources/extensions/get-secrets-from-user.ts +24 -1
- package/src/resources/extensions/gsd/auto-start.ts +4 -14
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +6 -0
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +7 -0
- package/src/resources/extensions/gsd/commands/context.ts +16 -5
- package/src/resources/extensions/gsd/commands/dispatcher.ts +14 -2
- package/src/resources/extensions/gsd/custom-workflow-engine.ts +19 -14
- package/src/resources/extensions/gsd/dispatch-guard.ts +18 -1
- package/src/resources/extensions/gsd/error-classifier.ts +1 -1
- package/src/resources/extensions/gsd/file-lock.ts +59 -0
- package/src/resources/extensions/gsd/guided-flow.ts +12 -9
- package/src/resources/extensions/gsd/init-wizard.ts +3 -13
- package/src/resources/extensions/gsd/notification-store.ts +19 -1
- package/src/resources/extensions/gsd/notification-widget.ts +1 -1
- package/src/resources/extensions/gsd/pre-execution-checks.ts +39 -2
- package/src/resources/extensions/gsd/prompts/complete-slice.md +2 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +33 -13
- package/src/resources/extensions/gsd/prompts/execute-task.md +20 -19
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +2 -0
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +3 -2
- package/src/resources/extensions/gsd/prompts/system.md +1 -0
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -1
- package/src/resources/extensions/gsd/state.ts +274 -344
- package/src/resources/extensions/gsd/tests/auto-start-worktree-db-path.test.ts +28 -0
- package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/complete-slice-prompt-task-summary-layout.test.ts +18 -0
- package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +436 -0
- package/src/resources/extensions/gsd/tests/discuss-incremental-persistence.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/execute-task-prompt-existing-artifact-guard.test.ts +33 -0
- package/src/resources/extensions/gsd/tests/file-lock.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/gsd-no-project-error.test.ts +73 -0
- package/src/resources/extensions/gsd/tests/notification-store.test.ts +17 -0
- package/src/resources/extensions/gsd/tests/notification-widget.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/secure-env-collect.test.ts +45 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +18 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +76 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +155 -1
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +22 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +60 -25
- package/src/resources/extensions/gsd/workflow-events.ts +34 -25
- package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +76 -0
- package/src/resources/extensions/gsd/workflow-mcp.ts +1 -1
- package/src/resources/skills/create-skill/SKILL.md +2 -0
- package/dist/web/standalone/.next/static/chunks/2826.821e01b07d92e948.js +0 -9
- package/dist/web/standalone/.next/static/chunks/app/page-7115e62689b5fd84.js +0 -1
- package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
- /package/dist/web/standalone/.next/static/{9pw9EXtXjdM7EFrCXUEPf → dYVdRaunb2ZSEA8fjkT-V}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{9pw9EXtXjdM7EFrCXUEPf → dYVdRaunb2ZSEA8fjkT-V}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// @gsd-build/mcp-server — Tests for secure_env_collect MCP tool
|
|
2
|
+
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
3
|
+
//
|
|
4
|
+
// Tests the secure_env_collect tool registered in createMcpServer.
|
|
5
|
+
// Uses a mock MCP server to intercept tool registration and elicitInput calls.
|
|
6
|
+
|
|
7
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
import { createMcpServer } from './server.js';
|
|
14
|
+
import { SessionManager } from './session-manager.js';
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Mock infrastructure
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* We intercept McpServer construction by monkey-patching the dynamic import.
|
|
22
|
+
* Instead, we'll test the tool handler indirectly through the exported
|
|
23
|
+
* createMcpServer function — capturing the registered tool handlers.
|
|
24
|
+
*
|
|
25
|
+
* Since createMcpServer dynamically imports McpServer, we need to test at
|
|
26
|
+
* a level that exercises the tool handler logic. We do this by extracting
|
|
27
|
+
* the tool handler through the server.tool() calls.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
interface RegisteredTool {
|
|
31
|
+
name: string;
|
|
32
|
+
description: string;
|
|
33
|
+
params: Record<string, unknown>;
|
|
34
|
+
handler: (args: Record<string, unknown>) => Promise<unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ToolResult {
|
|
38
|
+
content?: Array<{ type: string; text: string }>;
|
|
39
|
+
isError?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Mock McpServer that captures tool registrations and provides
|
|
44
|
+
* a controllable elicitInput response.
|
|
45
|
+
*/
|
|
46
|
+
class MockMcpServer {
|
|
47
|
+
registeredTools: RegisteredTool[] = [];
|
|
48
|
+
elicitResponse: { action: string; content?: Record<string, unknown> } = { action: 'accept', content: {} };
|
|
49
|
+
|
|
50
|
+
server = {
|
|
51
|
+
elicitInput: async (_params: unknown) => {
|
|
52
|
+
return this.elicitResponse;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
tool(name: string, description: string, params: Record<string, unknown>, handler: (args: Record<string, unknown>) => Promise<unknown>) {
|
|
57
|
+
this.registeredTools.push({ name, description, params, handler });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async connect(_transport: unknown) { /* no-op */ }
|
|
61
|
+
async close() { /* no-op */ }
|
|
62
|
+
|
|
63
|
+
getToolHandler(name: string): ((args: Record<string, unknown>) => Promise<unknown>) | undefined {
|
|
64
|
+
return this.registeredTools.find((t) => t.name === name)?.handler;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Helper to create a mock MCP server with secure_env_collect registered
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Since createMcpServer uses dynamic import for McpServer, we can't easily
|
|
74
|
+
* mock it. Instead, we test the env-writer utilities directly (in env-writer.test.ts)
|
|
75
|
+
* and test the tool integration by verifying:
|
|
76
|
+
* 1. The tool exists in the registered tools list
|
|
77
|
+
* 2. The handler produces correct results with mock data
|
|
78
|
+
*
|
|
79
|
+
* For handler-level testing, we create a standalone test that replicates
|
|
80
|
+
* the tool handler logic with a controllable mock.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
function makeTempDir(prefix: string): string {
|
|
84
|
+
return mkdtempSync(join(tmpdir(), `${prefix}-`));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Integration test — verify tool is registered
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
describe('secure_env_collect tool registration', () => {
|
|
92
|
+
it('createMcpServer registers secure_env_collect tool', async () => {
|
|
93
|
+
// This test verifies the tool exists — createMcpServer internally calls
|
|
94
|
+
// server.tool('secure_env_collect', ...) which we can't intercept without
|
|
95
|
+
// module mocking, but we can verify the server creates successfully
|
|
96
|
+
const sm = new SessionManager();
|
|
97
|
+
try {
|
|
98
|
+
const { server } = await createMcpServer(sm);
|
|
99
|
+
assert.ok(server, 'server should be created');
|
|
100
|
+
// The McpServer internally tracks registered tools — we verify no error
|
|
101
|
+
} finally {
|
|
102
|
+
await sm.cleanup();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Handler logic tests — using env-writer directly to test the flow
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
describe('secure_env_collect handler logic', () => {
|
|
112
|
+
it('skips keys that already exist in .env', async () => {
|
|
113
|
+
const tmp = makeTempDir('sec-collect');
|
|
114
|
+
try {
|
|
115
|
+
const envPath = join(tmp, '.env');
|
|
116
|
+
writeFileSync(envPath, 'ALREADY_SET=existing-value\n');
|
|
117
|
+
|
|
118
|
+
// Import the utility directly to test the pre-check logic
|
|
119
|
+
const { checkExistingEnvKeys } = await import('./env-writer.js');
|
|
120
|
+
const existing = await checkExistingEnvKeys(['ALREADY_SET', 'NEW_KEY'], envPath);
|
|
121
|
+
assert.deepStrictEqual(existing, ['ALREADY_SET']);
|
|
122
|
+
} finally {
|
|
123
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('writes collected values to .env without returning secret values', async () => {
|
|
128
|
+
const tmp = makeTempDir('sec-collect');
|
|
129
|
+
try {
|
|
130
|
+
const envPath = join(tmp, '.env');
|
|
131
|
+
const savedKey = process.env.SEC_COLLECT_TEST_KEY;
|
|
132
|
+
|
|
133
|
+
const { applySecrets } = await import('./env-writer.js');
|
|
134
|
+
const { applied, errors } = await applySecrets(
|
|
135
|
+
[{ key: 'SEC_COLLECT_TEST_KEY', value: 'super-secret-value' }],
|
|
136
|
+
'dotenv',
|
|
137
|
+
{ envFilePath: envPath },
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
assert.deepStrictEqual(applied, ['SEC_COLLECT_TEST_KEY']);
|
|
141
|
+
assert.deepStrictEqual(errors, []);
|
|
142
|
+
|
|
143
|
+
// Verify the value was written
|
|
144
|
+
const content = readFileSync(envPath, 'utf8');
|
|
145
|
+
assert.ok(content.includes('SEC_COLLECT_TEST_KEY=super-secret-value'));
|
|
146
|
+
|
|
147
|
+
// Verify process.env was hydrated
|
|
148
|
+
assert.equal(process.env.SEC_COLLECT_TEST_KEY, 'super-secret-value');
|
|
149
|
+
|
|
150
|
+
// Cleanup
|
|
151
|
+
if (savedKey === undefined) delete process.env.SEC_COLLECT_TEST_KEY;
|
|
152
|
+
else process.env.SEC_COLLECT_TEST_KEY = savedKey;
|
|
153
|
+
} finally {
|
|
154
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('auto-detects vercel destination from vercel.json', async () => {
|
|
159
|
+
const tmp = makeTempDir('sec-collect');
|
|
160
|
+
try {
|
|
161
|
+
writeFileSync(join(tmp, 'vercel.json'), '{}');
|
|
162
|
+
const { detectDestination } = await import('./env-writer.js');
|
|
163
|
+
assert.equal(detectDestination(tmp), 'vercel');
|
|
164
|
+
} finally {
|
|
165
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('handles empty form values as skipped', async () => {
|
|
170
|
+
// Simulate what happens when user leaves a field empty in the form
|
|
171
|
+
const formContent: Record<string, string> = {
|
|
172
|
+
'API_KEY': 'provided-value',
|
|
173
|
+
'OPTIONAL_KEY': '', // empty = skip
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const provided: Array<{ key: string; value: string }> = [];
|
|
177
|
+
const skipped: string[] = [];
|
|
178
|
+
|
|
179
|
+
for (const [key, raw] of Object.entries(formContent)) {
|
|
180
|
+
const value = typeof raw === 'string' ? raw.trim() : '';
|
|
181
|
+
if (value.length > 0) {
|
|
182
|
+
provided.push({ key, value });
|
|
183
|
+
} else {
|
|
184
|
+
skipped.push(key);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
assert.deepStrictEqual(provided, [{ key: 'API_KEY', value: 'provided-value' }]);
|
|
189
|
+
assert.deepStrictEqual(skipped, ['OPTIONAL_KEY']);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('result text never contains secret values', async () => {
|
|
193
|
+
const tmp = makeTempDir('sec-collect');
|
|
194
|
+
try {
|
|
195
|
+
const envPath = join(tmp, '.env');
|
|
196
|
+
const savedKey = process.env.RESULT_TEXT_TEST;
|
|
197
|
+
|
|
198
|
+
const { applySecrets } = await import('./env-writer.js');
|
|
199
|
+
const { applied } = await applySecrets(
|
|
200
|
+
[{ key: 'RESULT_TEXT_TEST', value: 'sk-super-secret-abc123' }],
|
|
201
|
+
'dotenv',
|
|
202
|
+
{ envFilePath: envPath },
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Simulate building result text (same logic as the tool handler)
|
|
206
|
+
const lines: string[] = [
|
|
207
|
+
'destination: dotenv (auto-detected)',
|
|
208
|
+
...applied.map((k) => `✓ ${k}: applied`),
|
|
209
|
+
];
|
|
210
|
+
const resultText = lines.join('\n');
|
|
211
|
+
|
|
212
|
+
// The result MUST NOT contain the secret value
|
|
213
|
+
assert.ok(!resultText.includes('sk-super-secret-abc123'), 'result text must not contain secret value');
|
|
214
|
+
assert.ok(resultText.includes('RESULT_TEXT_TEST'), 'result text should contain key name');
|
|
215
|
+
|
|
216
|
+
// Cleanup
|
|
217
|
+
if (savedKey === undefined) delete process.env.RESULT_TEXT_TEST;
|
|
218
|
+
else process.env.RESULT_TEXT_TEST = savedKey;
|
|
219
|
+
} finally {
|
|
220
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('handles multiple keys with mixed existing/new/skipped', async () => {
|
|
225
|
+
const tmp = makeTempDir('sec-collect');
|
|
226
|
+
try {
|
|
227
|
+
const envPath = join(tmp, '.env');
|
|
228
|
+
writeFileSync(envPath, 'EXISTING_A=already-here\n');
|
|
229
|
+
const savedB = process.env.NEW_B;
|
|
230
|
+
const savedC = process.env.SKIP_C;
|
|
231
|
+
|
|
232
|
+
const { checkExistingEnvKeys, applySecrets } = await import('./env-writer.js');
|
|
233
|
+
|
|
234
|
+
const allKeys = ['EXISTING_A', 'NEW_B', 'SKIP_C'];
|
|
235
|
+
const existing = await checkExistingEnvKeys(allKeys, envPath);
|
|
236
|
+
assert.deepStrictEqual(existing, ['EXISTING_A']);
|
|
237
|
+
|
|
238
|
+
// Simulate form response: NEW_B has value, SKIP_C is empty
|
|
239
|
+
const formContent = { NEW_B: 'new-value', SKIP_C: '' };
|
|
240
|
+
const provided: Array<{ key: string; value: string }> = [];
|
|
241
|
+
const skipped: string[] = [];
|
|
242
|
+
|
|
243
|
+
for (const key of allKeys.filter((k) => !existing.includes(k))) {
|
|
244
|
+
const raw = formContent[key as keyof typeof formContent] ?? '';
|
|
245
|
+
if (raw.trim().length > 0) provided.push({ key, value: raw.trim() });
|
|
246
|
+
else skipped.push(key);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const { applied, errors } = await applySecrets(provided, 'dotenv', { envFilePath: envPath });
|
|
250
|
+
|
|
251
|
+
assert.deepStrictEqual(applied, ['NEW_B']);
|
|
252
|
+
assert.deepStrictEqual(skipped, ['SKIP_C']);
|
|
253
|
+
assert.deepStrictEqual(errors, []);
|
|
254
|
+
assert.deepStrictEqual(existing, ['EXISTING_A']);
|
|
255
|
+
|
|
256
|
+
// Cleanup
|
|
257
|
+
if (savedB === undefined) delete process.env.NEW_B;
|
|
258
|
+
else process.env.NEW_B = savedB;
|
|
259
|
+
if (savedC === undefined) delete process.env.SKIP_C;
|
|
260
|
+
else process.env.SKIP_C = savedC;
|
|
261
|
+
} finally {
|
|
262
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* MCP Server — registers GSD orchestration, project-state, and workflow tools.
|
|
3
3
|
*
|
|
4
4
|
* Session tools (6): gsd_execute, gsd_status, gsd_result, gsd_cancel, gsd_query, gsd_resolve_blocker
|
|
5
|
-
* Interactive tools (
|
|
5
|
+
* Interactive tools (2): ask_user_questions, secure_env_collect via MCP form elicitation
|
|
6
6
|
* Read-only tools (6): gsd_progress, gsd_roadmap, gsd_history, gsd_doctor, gsd_captures, gsd_knowledge
|
|
7
7
|
* Workflow tools (29): headless-safe planning, metadata persistence, replanning, completion, validation, reassessment, gate result, status, and journal tools
|
|
8
8
|
*
|
|
@@ -22,6 +22,7 @@ import { readCaptures } from './readers/captures.js';
|
|
|
22
22
|
import { readKnowledge } from './readers/knowledge.js';
|
|
23
23
|
import { runDoctorLite } from './readers/doctor-lite.js';
|
|
24
24
|
import { registerWorkflowTools } from './workflow-tools.js';
|
|
25
|
+
import { applySecrets, checkExistingEnvKeys, detectDestination } from './env-writer.js';
|
|
25
26
|
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
27
28
|
// Constants
|
|
@@ -112,11 +113,26 @@ async function fileExists(path: string): Promise<boolean> {
|
|
|
112
113
|
// MCP Server type — minimal interface for the dynamically-imported McpServer
|
|
113
114
|
// ---------------------------------------------------------------------------
|
|
114
115
|
|
|
116
|
+
interface ElicitResult {
|
|
117
|
+
action: 'accept' | 'decline' | 'cancel';
|
|
118
|
+
content?: Record<string, string | number | boolean | string[]>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface ElicitRequestFormParams {
|
|
122
|
+
mode?: 'form';
|
|
123
|
+
message: string;
|
|
124
|
+
requestedSchema: {
|
|
125
|
+
type: 'object';
|
|
126
|
+
properties: Record<string, Record<string, unknown>>;
|
|
127
|
+
required?: string[];
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
115
131
|
interface McpServerInstance {
|
|
116
132
|
tool(name: string, description: string, params: Record<string, unknown>, handler: (args: Record<string, unknown>) => Promise<unknown>): unknown;
|
|
117
133
|
server: {
|
|
118
134
|
elicitInput(
|
|
119
|
-
params: AskUserQuestionsElicitRequest,
|
|
135
|
+
params: AskUserQuestionsElicitRequest | ElicitRequestFormParams,
|
|
120
136
|
options?: unknown,
|
|
121
137
|
): Promise<AskUserQuestionsElicitResult>;
|
|
122
138
|
};
|
|
@@ -282,7 +298,7 @@ export async function createMcpServer(sessionManager: SessionManager): Promise<{
|
|
|
282
298
|
|
|
283
299
|
const server: McpServerInstance = new McpServer(
|
|
284
300
|
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
285
|
-
{ capabilities: { tools: {} } },
|
|
301
|
+
{ capabilities: { tools: {}, elicitation: {} } },
|
|
286
302
|
);
|
|
287
303
|
|
|
288
304
|
// -----------------------------------------------------------------------
|
|
@@ -472,6 +488,124 @@ export async function createMcpServer(sessionManager: SessionManager): Promise<{
|
|
|
472
488
|
},
|
|
473
489
|
);
|
|
474
490
|
|
|
491
|
+
// -----------------------------------------------------------------------
|
|
492
|
+
// secure_env_collect — collect secrets via MCP form elicitation
|
|
493
|
+
// -----------------------------------------------------------------------
|
|
494
|
+
server.tool(
|
|
495
|
+
'secure_env_collect',
|
|
496
|
+
'Collect environment variables securely via form input. Values are written directly to .env (or Vercel/Convex) and NEVER appear in tool output — only key names and applied/skipped status are returned. Use this instead of asking users to manually edit .env files or paste secrets into chat.',
|
|
497
|
+
{
|
|
498
|
+
projectDir: z.string().describe('Absolute path to the project directory'),
|
|
499
|
+
keys: z.array(z.object({
|
|
500
|
+
key: z.string().describe('Env var name, e.g. OPENAI_API_KEY'),
|
|
501
|
+
hint: z.string().optional().describe('Format hint shown to user, e.g. "starts with sk-"'),
|
|
502
|
+
guidance: z.array(z.string()).optional().describe('Step-by-step instructions for obtaining this key'),
|
|
503
|
+
})).min(1).describe('Environment variables to collect'),
|
|
504
|
+
destination: z.enum(['dotenv', 'vercel', 'convex']).optional().describe('Where to write secrets. Auto-detected from project files if omitted.'),
|
|
505
|
+
envFilePath: z.string().optional().describe('Path to .env file (dotenv only). Defaults to .env in projectDir.'),
|
|
506
|
+
environment: z.enum(['development', 'preview', 'production']).optional().describe('Target environment (vercel/convex only)'),
|
|
507
|
+
},
|
|
508
|
+
async (args: Record<string, unknown>) => {
|
|
509
|
+
const { projectDir, keys, destination, envFilePath, environment } = args as {
|
|
510
|
+
projectDir: string;
|
|
511
|
+
keys: Array<{ key: string; hint?: string; guidance?: string[] }>;
|
|
512
|
+
destination?: 'dotenv' | 'vercel' | 'convex';
|
|
513
|
+
envFilePath?: string;
|
|
514
|
+
environment?: 'development' | 'preview' | 'production';
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
try {
|
|
518
|
+
const resolvedProjectDir = resolve(projectDir);
|
|
519
|
+
const resolvedEnvPath = resolve(resolvedProjectDir, envFilePath ?? '.env');
|
|
520
|
+
|
|
521
|
+
// (1) Check which keys already exist
|
|
522
|
+
const allKeyNames = keys.map((k) => k.key);
|
|
523
|
+
const existingKeys = await checkExistingEnvKeys(allKeyNames, resolvedEnvPath);
|
|
524
|
+
const existingSet = new Set(existingKeys);
|
|
525
|
+
const pendingKeys = keys.filter((k) => !existingSet.has(k.key));
|
|
526
|
+
|
|
527
|
+
// If all keys already exist, return immediately
|
|
528
|
+
if (pendingKeys.length === 0) {
|
|
529
|
+
const lines = existingKeys.map((k) => `• ${k}: already set`);
|
|
530
|
+
return textContent(`All ${existingKeys.length} key(s) already set.\n${lines.join('\n')}`);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// (2) Build elicitation form — one string field per pending key
|
|
534
|
+
const properties: Record<string, Record<string, unknown>> = {};
|
|
535
|
+
const required: string[] = [];
|
|
536
|
+
|
|
537
|
+
for (const item of pendingKeys) {
|
|
538
|
+
const descParts: string[] = [];
|
|
539
|
+
if (item.hint) descParts.push(`Format: ${item.hint}`);
|
|
540
|
+
if (item.guidance && item.guidance.length > 0) {
|
|
541
|
+
descParts.push('How to get this:');
|
|
542
|
+
item.guidance.forEach((step, i) => descParts.push(`${i + 1}. ${step}`));
|
|
543
|
+
}
|
|
544
|
+
descParts.push('Leave empty to skip.');
|
|
545
|
+
|
|
546
|
+
properties[item.key] = {
|
|
547
|
+
type: 'string',
|
|
548
|
+
title: item.key,
|
|
549
|
+
description: descParts.join('\n'),
|
|
550
|
+
};
|
|
551
|
+
// Don't mark as required — empty string = skip
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// (3) Elicit input from the MCP client
|
|
555
|
+
const elicitation = await server.server.elicitInput({
|
|
556
|
+
message: `Enter values for ${pendingKeys.length} environment variable(s). Values are written directly to the project and never shown to the AI.`,
|
|
557
|
+
requestedSchema: {
|
|
558
|
+
type: 'object',
|
|
559
|
+
properties,
|
|
560
|
+
required,
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
if (elicitation.action !== 'accept' || !elicitation.content) {
|
|
565
|
+
return textContent('secure_env_collect was cancelled by user.');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// (4) Separate provided vs skipped from form response
|
|
569
|
+
const provided: Array<{ key: string; value: string }> = [];
|
|
570
|
+
const skipped: string[] = [];
|
|
571
|
+
|
|
572
|
+
for (const item of pendingKeys) {
|
|
573
|
+
const raw = elicitation.content[item.key];
|
|
574
|
+
const value = typeof raw === 'string' ? raw.trim() : '';
|
|
575
|
+
if (value.length > 0) {
|
|
576
|
+
provided.push({ key: item.key, value });
|
|
577
|
+
} else {
|
|
578
|
+
skipped.push(item.key);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// (5) Auto-detect destination if not specified
|
|
583
|
+
const resolvedDestination = destination ?? detectDestination(resolvedProjectDir);
|
|
584
|
+
|
|
585
|
+
// (6) Write secrets to destination
|
|
586
|
+
const { applied, errors } = await applySecrets(provided, resolvedDestination, {
|
|
587
|
+
envFilePath: resolvedEnvPath,
|
|
588
|
+
environment,
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// (7) Build result — NEVER include secret values
|
|
592
|
+
const lines: string[] = [
|
|
593
|
+
`destination: ${resolvedDestination}${!destination ? ' (auto-detected)' : ''}${environment ? ` (${environment})` : ''}`,
|
|
594
|
+
];
|
|
595
|
+
for (const k of applied) lines.push(`✓ ${k}: applied`);
|
|
596
|
+
for (const k of skipped) lines.push(`• ${k}: skipped`);
|
|
597
|
+
for (const k of existingKeys) lines.push(`• ${k}: already set`);
|
|
598
|
+
for (const e of errors) lines.push(`✗ ${e}`);
|
|
599
|
+
|
|
600
|
+
return errors.length > 0 && applied.length === 0
|
|
601
|
+
? errorContent(lines.join('\n'))
|
|
602
|
+
: textContent(lines.join('\n'));
|
|
603
|
+
} catch (err) {
|
|
604
|
+
return errorContent(err instanceof Error ? err.message : String(err));
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
);
|
|
608
|
+
|
|
475
609
|
// =======================================================================
|
|
476
610
|
// READ-ONLY TOOLS — no session required, pure filesystem reads
|
|
477
611
|
// =======================================================================
|
|
@@ -384,6 +384,116 @@ describe("workflow MCP tools", () => {
|
|
|
384
384
|
}
|
|
385
385
|
});
|
|
386
386
|
|
|
387
|
+
it("gsd_requirement_save opens the DB before inline requirement writes", async () => {
|
|
388
|
+
const base = makeTmpBase();
|
|
389
|
+
try {
|
|
390
|
+
const server = makeMockServer();
|
|
391
|
+
registerWorkflowTools(server as any);
|
|
392
|
+
const requirementTool = server.tools.find((t) => t.name === "gsd_requirement_save");
|
|
393
|
+
assert.ok(requirementTool, "requirement tool should be registered");
|
|
394
|
+
|
|
395
|
+
closeDatabase();
|
|
396
|
+
|
|
397
|
+
const result = await requirementTool!.handler({
|
|
398
|
+
projectDir: base,
|
|
399
|
+
class: "operability",
|
|
400
|
+
description: "Inline MCP requirement save regression",
|
|
401
|
+
why: "Reproduce missing ensureDbOpen in workflow-tools",
|
|
402
|
+
source: "user",
|
|
403
|
+
status: "active",
|
|
404
|
+
primary_owner: "M010/S10",
|
|
405
|
+
validation: "n/a",
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
assert.match((result as any).content[0].text as string, /Saved requirement R\d+/);
|
|
409
|
+
assert.ok(existsSync(join(base, ".gsd", "REQUIREMENTS.md")), "REQUIREMENTS.md should be written to disk");
|
|
410
|
+
const row = _getAdapter()!
|
|
411
|
+
.prepare("SELECT id, class, description FROM requirements WHERE description = ?")
|
|
412
|
+
.get("Inline MCP requirement save regression") as Record<string, unknown> | undefined;
|
|
413
|
+
assert.ok(row, "requirement should be written to the database");
|
|
414
|
+
assert.equal(row["class"], "operability");
|
|
415
|
+
} finally {
|
|
416
|
+
cleanup(base);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("gsd_plan_task reopens the DB before inline task planning writes", async () => {
|
|
421
|
+
const base = makeTmpBase();
|
|
422
|
+
try {
|
|
423
|
+
const server = makeMockServer();
|
|
424
|
+
registerWorkflowTools(server as any);
|
|
425
|
+
const milestoneTool = server.tools.find((t) => t.name === "gsd_plan_milestone");
|
|
426
|
+
const sliceTool = server.tools.find((t) => t.name === "gsd_plan_slice");
|
|
427
|
+
const taskTool = server.tools.find((t) => t.name === "gsd_plan_task");
|
|
428
|
+
assert.ok(milestoneTool, "milestone planning tool should be registered");
|
|
429
|
+
assert.ok(sliceTool, "slice planning tool should be registered");
|
|
430
|
+
assert.ok(taskTool, "task planning tool should be registered");
|
|
431
|
+
|
|
432
|
+
await milestoneTool!.handler({
|
|
433
|
+
projectDir: base,
|
|
434
|
+
milestoneId: "M010",
|
|
435
|
+
title: "Inline task planning DB reopen",
|
|
436
|
+
vision: "Seed a slice, close the DB, then plan another task inline.",
|
|
437
|
+
slices: [
|
|
438
|
+
{
|
|
439
|
+
sliceId: "S10",
|
|
440
|
+
title: "Inline task planning",
|
|
441
|
+
risk: "medium",
|
|
442
|
+
depends: [],
|
|
443
|
+
demo: "Inline gsd_plan_task reopens the DB after it was closed.",
|
|
444
|
+
goal: "Preserve MCP task planning after the DB adapter is closed.",
|
|
445
|
+
successCriteria: "The second task plan persists after a closed DB is reopened.",
|
|
446
|
+
proofLevel: "integration",
|
|
447
|
+
integrationClosure: "The inline MCP handler reopens the DB before planning.",
|
|
448
|
+
observabilityImpact: "workflow-tools MCP tests cover the inline reopen path.",
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
});
|
|
452
|
+
await sliceTool!.handler({
|
|
453
|
+
projectDir: base,
|
|
454
|
+
milestoneId: "M010",
|
|
455
|
+
sliceId: "S10",
|
|
456
|
+
goal: "Create the initial slice plan before closing the DB.",
|
|
457
|
+
tasks: [
|
|
458
|
+
{
|
|
459
|
+
taskId: "T10",
|
|
460
|
+
title: "Seed existing task",
|
|
461
|
+
description: "Create the initial task plan before closing the DB.",
|
|
462
|
+
estimate: "5m",
|
|
463
|
+
files: ["packages/mcp-server/src/workflow-tools.ts"],
|
|
464
|
+
verify: "node --test",
|
|
465
|
+
inputs: ["M010-ROADMAP.md"],
|
|
466
|
+
expectedOutput: ["T10-PLAN.md"],
|
|
467
|
+
},
|
|
468
|
+
],
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
closeDatabase();
|
|
472
|
+
|
|
473
|
+
const result = await taskTool!.handler({
|
|
474
|
+
projectDir: base,
|
|
475
|
+
milestoneId: "M010",
|
|
476
|
+
sliceId: "S10",
|
|
477
|
+
taskId: "T11",
|
|
478
|
+
title: "Reopen and plan",
|
|
479
|
+
description: "Exercise the inline plan-task path after the DB was closed.",
|
|
480
|
+
estimate: "5m",
|
|
481
|
+
files: ["packages/mcp-server/src/workflow-tools.ts"],
|
|
482
|
+
verify: "node --test",
|
|
483
|
+
inputs: ["M010-ROADMAP.md", "S10-PLAN.md"],
|
|
484
|
+
expectedOutput: ["T11-PLAN.md"],
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
assert.match((result as any).content[0].text as string, /Planned task T11/);
|
|
488
|
+
assert.ok(
|
|
489
|
+
existsSync(join(base, ".gsd", "milestones", "M010", "slices", "S10", "tasks", "T11-PLAN.md")),
|
|
490
|
+
"T11 plan should be written after reopening the DB",
|
|
491
|
+
);
|
|
492
|
+
} finally {
|
|
493
|
+
cleanup(base);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
387
497
|
it("gsd_replan_slice and gsd_slice_replan work end-to-end", async () => {
|
|
388
498
|
const base = makeTmpBase();
|
|
389
499
|
try {
|