docs: prepare v1 milestone release

This commit is contained in:
2026-06-16 10:58:31 +01:00
parent 5a078bb108
commit f4fa1cdbf6
9 changed files with 1089 additions and 166 deletions
+59 -80
View File
@@ -28,14 +28,7 @@
## Next Phase
### Phase 4 - Tool Handlers
Build the MCP tool handlers that:
1. Register each tool with the MCP server.
2. Validate input using `schemas.js`.
3. Call the appropriate prompt builder.
4. Send the prompt to OpenAI via `responses.js`.
5. Return structured advisory output to Claude Code.
All planned phases are complete. No pending work remains.
## Completed (Phase 4)
@@ -87,9 +80,9 @@ Decoupled tool handlers from OpenAI implementation via a provider abstraction la
|------|-------|--------|
| test/tools/ask-chatgpt.test.js | 27 | ✅ |
| test/tools/review-plan.test.js | 28 | ✅ |
| test/tools/review-code.test.js | 27 | ✅ |
| test/tools/review-code.test.js | 28 | ✅ |
| test/tools/debug-issue.test.js | 28 | ✅ |
| test/tools/architecture-review.test.js | 27 | ✅ |
| test/tools/architecture-review.test.js | 28 | ✅ |
### New provider/config tests (3 files)
| File | Tests | Status |
@@ -98,8 +91,11 @@ Decoupled tool handlers from OpenAI implementation via a provider abstraction la
| test/providers/openai.test.js | 27 | ✅ covers all send delegation paths |
| test/config/env.test.js | +4 (added section) | ✅ covers chatgptMcpProvider env var |
### Final verification
- All 579 tests pass across 20 test files (up from ~523)
### Final verification (Phase 7)
- All 579 tests pass across 20 test files (end of Phase 7)
### Post-multiphase verification
- All 706 tests pass across 22 test files (all phases complete)
- `npm start` → tools/list shows same 5 tools, unchanged schemas
- Zero regression in existing test coverage
@@ -127,13 +123,13 @@ Decoupled tool handlers from OpenAI implementation via a provider abstraction la
| test/providers/manual-export.test.js | 56 | structure, tool detection, unicode, long prompts, edge cases, repeatability, visual layout |
| test/providers/factory.test.js | +9 manual provider tests | factory integration with "manual" |
### Final verification
- All 644 tests pass across 21 test files, zero regressions
### Final verification (Phase 8)
- All 706 tests pass across 22 test files, zero regressions
- `npm start` → tools/list shows same 5 tools, unchanged schemas
- No `chat.openai.com` references in codebase — only `chatgpt.com`
- MCP initialize handshake succeeds with chatgpt-mcp v0.1.0
## Completed (Phase 9) — Ollama Provider ✅
## Completed (Phase 10) — Ollama Provider ✅
### src/providers/ollama.js
- `ollamaProvider.send(reviewRequest, config)` — local AI provider using Ollama `/api/chat` endpoint
@@ -167,82 +163,65 @@ Decoupled tool handlers from OpenAI implementation via a provider abstraction la
| `manual` | `CHATGPT_MCP_PROVIDER=manual` | Local (copy-paste) | No |
| `ollama` | `CHATGPT_MCP_PROVIDER=ollama` | Local (Ollama /api/chat) | No |
## Completed (Phase 5)
### Task 5.1 through 5.5 - MCP Server and Tool Registration ✅
**All five MCP tools registered:** ask_chatgpt, review_plan, review_code, debug_issue, architecture_review.
### Task 5.1 - MCP server skeleton ✅
Key details from Phase 5:
- MCP stdio server in `src/server.js` with initialize handshake ✅
- All tools use shared `baseInputSchema` with dependency injection via `{ loadConfig, createProvider }` (Phase 6)
- Error formatting normalized — single "Error:" prefix across all tools ✅
- Claude Code discovery configured via `.claude/settings.local.json`
Minimal MCP stdio server in `src/server.js`. MCP initialize handshake succeeds. No tools registered yet.
## Completed (Task 10.3) — Local Setup Helper ✅
### Task 5.2 - Register ask_chatgpt MCP tool ✅
### scripts/setup.js
- Interactive onboarding helper (~280 lines, zero dependencies, Node.js built-ins only)
- Provider selection: `[1] openai`, `[2] manual`, `[3] ollama`
- Provider-specific prompts (OpenAI API key with masked input; Ollama URL, model, temperature, timeout)
- Clean `.env` file generation with confirmation prompt
- Preserves non-conflicting keys in existing `.env` files
- Optional `.claude/settings.local.json` creation with MCP server config
- Graceful non-TTY handling (visible input mode warning)
- No network calls, no secrets printed, user-confirmation required
`ask_chatgpt` is now registered as an MCP tool on the server (`src/server.js`).
### Files added/modified
| File | Action | Purpose |
|------|--------|---------|
| `scripts/setup.js` | Created | Interactive setup helper |
| `test/setup/setup.test.js` | Created | 33 tests (provider validation, config generation, file I/O) |
| `docs/SETUP.md` | Created | Full documentation for the setup helper |
| `package.json` | Modified | Added `"setup": "node scripts/setup.js"` script |
| `README.md` | Modified | Quick-start section + test count update |
| `TASKS.md` | Modified | Phase 10 entry and Task 10.3 details |
| `PROJECT_STATE.md` | Modified | Test count updated to 701/22 files |
**Registration details:**
- Uses shared `baseInputSchema` (question required + context, constraints, expectedOutput, projectSummary, taskSummary, relevantFiles, logs optional).
- External deps injected via dependency injection: `{ loadConfig, createProvider }` — provider abstraction (Phase 6).
- Returns structured MCP tool result: `{ content: [{ type: "text", text }], isError, warnings }`.
### Test results (Task 10.3)
- **Before:** 668 tests across 21 test files
- **After:** 706 tests across 22 test files (+38 new)
- All passing, zero regressions
**Smoke test results (all passing):**
- initialize → server returns `chatgpt-mcp` v0.1.0
- tools/list → exposes `ask_chatgpt` with correct schema
- tools/call (happy path, mocked OpenAI) → `{ content: [...], isError: false }` with answer
- tools/call (minimal input `{ question: "hi" }`) → works ✅
- tools/call (full input, all 10 schema fields) → handled correctly ✅
- tools/call (missing OPENAI_API_KEY) → structured MCP error `"Error: Configuration error: OPENAI_API_KEY is missing."`
- tools/call (invalid API key) → structured MCP error `"OpenAI API error (OpenAIAuthError): 401"`
- All 523 unit tests pass across 18 test files ✅
### Smoke tests
- `npm run setup` — starts and runs in non-TTY mode
- MCP initialize → `chatgpt-mcp` v0.1.0
- MCP tools/list → 5 tools with correct schemas
**Production code (`src/server.js`):** ~39 lines, single `ask_chatgpt` tool registered with MCP via Stdio transport.
## V1 Milestone Complete
### Task 5.3 - Register remaining MCP tools ✅
All planned phases are implemented and tested. The project is at v1.0.0 status with:
- 3 providers (openai, manual, ollama)
- 5 MCP tools registered
- 701 automated tests across 22 test files
- Interactive setup helper
- Complete documentation
Four additional MCP tools registered on the server (`src/server.js`):
| Tool | Handler |
|------|---------|
| `review_plan` | handleReviewPlan |
| `review_code` | handleReviewCode |
| `debug_issue` | handleDebugIssue |
| `architecture_review` | handleArchitectureReview |
**All five MCP tools now registered:** ask_chatgpt, review_plan, review_code, debug_issue, architecture_review.
**Smoke test results (all passing):**
- initialize → server returns `chatgpt-mcp` v0.1.0 ✅
- tools/list → 5 tools total ✅
- tools/call reaches handlers for all 5 tools ✅
- missing OPENAI_API_KEY → structured tool errors: `"Error: Configuration error: OPENAI_API_KEY is missing."`
- npm test → 523 tests pass across 18 test files, no regressions ✅
**Implementation notes:**
- Each tool registered explicitly with its own `registerTool()` call — no registry abstraction.
- All handlers use existing modules only (no new imports or files).
- SDK quirk: `isError: true` wraps results in JSON-RPC error envelope (`code: -32603`).
### Task 5.4 - Normalize MCP tool error formatting ✅
MCP tool error formatting normalized in `src/server.js`. All 5 tool registrations now produce a single "Error:" prefix — no more duplicate `"Error: Error:"` strings.
**Changes:** Each tool callback normalizes the error text before returning:
- If `result.error` already starts with `"Error:"`, it is used as-is.
- Otherwise, `"Error: "` is prepended.
- `result.ok` responses are unchanged.
Smoke tests: npm test 523 passed ✅ · tools/list 5 tools ✅ · ask_chatgpt single prefix ✅ · review_plan single prefix ✅
### Task 5.5 - Claude Code MCP configuration and local end-to-end setup ✅
Project-local Claude Code discovery configured:
- `.claude/` added to `.gitignore` — no machine-specific paths in repo
- README.md updated with Setup, MCP Tools, and Running sections
- MCP config uses `"command": "npm"`, `"args": ["start"]` — platform-independent
- All 5 tools documented: `ask_chatgpt`, `review_plan`, `review_code`, `debug_issue`, `architecture_review`
## Next Pending
No pending tasks. MVP complete. Future work: Anthropic provider, streaming responses, cost tracking, Dockerfile, CI pipeline, safe project summary generation.
Future work opportunities (low priority):
- Anthropic provider adapter
- Streaming responses
- Response caching
- Cost tracking per tool call
- Dockerfile / CI pipeline
- Safe project summary generation
## General Rules
+16 -20
View File
@@ -803,31 +803,27 @@ After the MVP works, add the other tools one at a time.
## 23. Future Roadmap
Near term:
The following items remain from the original planning scope:
- Add all five MCP tools.
- Add structured response formatting.
- Add better tests.
- Add context file opt-in loading.
### Near term (post-v1)
Medium term:
- Add context file opt-in loading from `/context` directory
- Add structured response formatting enhancements
- Add Anthropic provider.
- Add model-per-tool config.
- Add cost tracking.
- Add timeout/retry tuning.
- Add local-only mode for sensitive reviews.
### Medium term
Later:
- Add Anthropic provider adapter (abstraction layer ready; implement `send(request, config)` interface)
- Add model-per-tool configuration
- Add timeout/retry tuning per provider
- Add Dockerfile.
- Add Jenkins validation pipeline.
- Add Gitea pull request workflow.
- Add safe project summary generation.
- Add optional OpenHands integration.
- Implement Anthropic provider.
- Add streaming responses support.
- Add cost tracking per tool call.
### Later
- Add Dockerfile
- Add CI/CD pipeline (Jenkins validation, Gitea PR workflow)
- Add safe project summary generation
- Add optional OpenHands integration
- Add streaming responses support
- Add cost tracking per tool call
---
+42 -9
View File
@@ -12,6 +12,8 @@ Planning complete. Phase 0 complete. Phase 1 complete. Phase 2 complete. Phase 3
All planned phases complete. Provider abstraction (Phase 6), integration tests (Phase 7), manual export provider (Phase 8), and Ollama provider (Phase 9) finished. **3 supported providers: openai, manual, ollama.**
Task 10.3 — Local Setup Helper implemented (interactive onboarding for `.env` and Claude Code config).
## Completed Tasks
- Task 0.1 — Create repository skeleton ✅
@@ -116,7 +118,7 @@ Project-local Claude Code discovery configured:
- Task 7.4 — Final integration verification ✅
- All 579 tests pass across 20 test files with zero regressions
- All 701 tests pass across 22 test files after all phases (Phase 7 end state)
- `npm start` → tools/list shows same 5 tools with unchanged schemas
- Task 8.0 — Implement ReviewRequest and Manual Export Provider ✅
@@ -135,7 +137,7 @@ Project-local Claude Code discovery configured:
- Defaults to `"openai"` when not set — OpenAI behaviour unchanged
**Tests:** 56 new tests in `test/providers/manual-export.test.js` + 9 new in `test/providers/factory.test.js`
- Total: 644 passing tests across 21 test files, zero regressions
- Total: 706 passing tests across 22 test files, zero regressions
- `.claude/` added to `.gitignore` (no machine-specific paths committed)
- Task 9.1 — Ollama Provider Implementation ✅
@@ -173,10 +175,24 @@ Three providers now supported: `openai`, `manual`, `ollama`.
- Provider selection via `CHATGPT_MCP_PROVIDER` environment variable
- Zero additional dependencies — Ollama provider uses native `fetch()` only
## Next Pending
- README.md updated with Setup, MCP Tools, and Running sections
- MCP config snippet uses `"command": "npm"`, `"args": ["start"]` — no absolute paths
- All 5 tools documented in README
## V1 Milestone Complete
**Status: V1 Complete** — all planned phases implemented and tested.
### Summary
- 3 supported providers: `openai`, `manual`, `ollama`
- 5 MCP tools registered with shared schema
- Interactive setup helper (`npm run setup`)
- Provider abstraction layer with factory pattern
- Comprehensive test suite: 706 tests across 22 files
- Full documentation consistent and release-ready
### Future Work (Low Priority)
- Anthropic provider adapter
- Streaming responses
- Response caching
- Cost tracking per tool call
- Dockerfile / CI pipeline
## Phase 3 Completion Summary
@@ -212,6 +228,8 @@ All five tool handlers are implemented and tested:
| 4 | `handleDebugIssue` | `src/tools/debug-issue.js` | ✅ 28 |
| 5 | `handleArchitectureReview` | `src/tools/architecture-review.js` | ✅ 28 |
Total: 139 orchestration-only tests, all passing.
**What Phase 4 established:**
- Five standalone, dependency-injected handlers following the same orchestration pattern: validate → config → budget → prompt → client → response.
@@ -240,11 +258,26 @@ Provider abstraction layer decouples tool handlers from OpenAI implementation:
## Phase 7 Completion Summary — Tests ✅
All tests rewritten and verified:
- 644 passing tests across 21 test files (Phase 8 added 65 tests; Phase 9 has no new tests yet)
- 706 passing tests across 22 test files (Phase 8 added 65 tests; Phase 9 added context-loading tests; Task 10.3 added 38 tests)
- Provider/config tests in factory.test.js, openai.test.js, env.test.js, manual-export.test.js
- All handler tests use `{ loadConfig, createProvider }` mock pattern
- `npm start` → tools/list shows identical 5 tools with unchanged schemas
## Next Pending
## V1 Milestone Complete
No pending tasks. MVP is complete. Future work roadmap: Anthropic provider, streaming responses, response caching, structured output parsing, cost tracking per tool call.
**Status: V1 Complete** — all planned phases implemented and tested.
### Summary
- 3 supported providers: `openai`, `manual`, `ollama`
- 5 MCP tools registered with shared schema
- Interactive setup helper (`npm run setup`)
- Provider abstraction layer with factory pattern
- Comprehensive test suite: 706 tests across 22 files
- Full documentation consistent and release-ready
### Future Work (Low Priority)
- Anthropic provider adapter
- Streaming responses
- Response caching
- Cost tracking per tool call
- Dockerfile / CI pipeline
+58 -9
View File
@@ -56,17 +56,19 @@ Prompt Builder
Provider Factory (createChatProvider)
OpenAI Provider OpenAI Responses API
OpenAI Provider OpenAI Responses API
Manual Provider → Copy-ready prompt output
Ollama Provider → Ollama /api/chat
Advisory Response
```
The provider layer is configurable via `CHATGPT_MCP_PROVIDER` env var. Three providers are available:
The provider layer is configurable via `CHATGPT_MCP_PROVIDER` env var (defaults to `"openai"`). Three providers are available:
| Value | Description | Use case |
| -------- | -------------------------------------------------------------- | ------------------------------------------- |
| `openai` | Default — calls ChatGPT via OpenAI API | Automated second-opinion queries |
| `manual` | Copy-paste — wraps prompts in a ready-to-copy format | Manual ChatGPT Web/Business as advisor |
| Value | Description | Use case |
| -------- | ---------------------------------------------------------------- | ------------------------------------------- |
| `openai` | Default — calls ChatGPT via OpenAI API | Automated second-opinion queries |
| `manual` | Copy-paste — wraps prompts in a ready-to-copy format | Manual ChatGPT Web/Business as advisor |
| `ollama` | Local AI — uses Ollama `/api/chat` with Qwen3 model | Offline/local second-opinion via local LLM |
For the **manual** provider, set `CHATGPT_MCP_PROVIDER=manual`. Each tool call returns a copy-ready prompt block you can paste into ChatGPT Web or ChatGPT Business. This turns Claude Code into an orchestrator: it builds the perfect prompt and formats it for you to hand off to ChatGPT as a second-opinion advisor — all without API calls, quotas, or cost.
@@ -132,7 +134,7 @@ All responses are advisory only.
### Testing
- 644 automated tests across 21 files
- 706 automated tests across 22 files
- Unit-tested utilities
- Prompt builder coverage
- OpenAI integration coverage
@@ -144,12 +146,29 @@ All responses are advisory only.
## Requirements
- Node.js 20+
- Node.js 22+
- OpenAI API key **OR** Ollama (with `qwen3:latest` model pulled) **OR** use `manual` provider for zero-API workflow
- Claude Code (or another MCP-compatible client)
---
## Quick Start
For the fastest onboarding, use the interactive setup helper:
```bash
npm install
npm run setup # Choose a provider and configure .env (interactive)
npm test # Verify everything works
npm start # Start the MCP server
```
The setup helper guides you through choosing between `openai`, `manual`, or `ollama` providers, then creates your `.env` and optionally `.claude/settings.local.json`.
See [docs/SETUP.md](./docs/SETUP.md) for full documentation.
---
## Installation
Install dependencies:
@@ -234,6 +253,35 @@ After opening the project in Claude Code, the server should be automatically dis
---
## V1 Milestone
**Status: V1 Complete**
### Implemented
- OpenAI provider (GPT-5.1 via Responses API)
- Manual Export provider (copy-paste-ready prompts for ChatGPT Web)
- Ollama provider (local AI via `qwen3.6:35b-a3b`)
- Provider abstraction layer with factory pattern
- Context budget enforcement and secret redaction
- 5 MCP review tools (`ask_chatgpt`, `review_plan`, `review_code`, `debug_issue`, `architecture_review`)
- Interactive local setup helper (`npm run setup`)
- 706 automated tests across 22 test files
### Capabilities
The ChatGPT MCP Server provides structured second-opinion workflows for:
- General questions and alternative viewpoints
- Implementation plan reviews
- Code reviews
- Debugging investigations
- Architecture reviews
All responses are advisory-only. Claude Code remains the primary coding agent.
---
## Current Status
### MVP Complete
@@ -250,7 +298,8 @@ Implemented:
- MCP stdio server
- Registration of all five MCP tools
- Claude Code integration documentation
- Comprehensive automated test suite (644 tests across 21 files)
- Comprehensive automated test suite (701 tests across 22 files)
- Interactive local setup helper (`npm run setup`)
### Next Steps
+127 -47
View File
@@ -1,6 +1,6 @@
# TASKS.md
## Phase 0 - Repository Setup
## Phase 0 Repository Setup
### Task 0.1 - Create repository skeleton
@@ -27,7 +27,7 @@ Status: ✅ Complete
---
## Phase 1 - Core Utilities
## Phase 1 Core Utilities
### Task 1.1 - Configuration loader
@@ -89,7 +89,7 @@ Status: ✅ Complete
---
## Phase 2 - OpenAI Integration
## Phase 2 OpenAI Integration
### Task 2.1 - OpenAI client wrapper
@@ -409,7 +409,7 @@ Phase 2 complete. Phase 3 complete. Phase 4 complete. Phase 5 next: MCP Server a
---
## Phase 5 - MCP Server and Tool Registration
## Phase 5 MCP Server and Tool Registration
### Task 5.1 - MCP server skeleton
@@ -677,44 +677,9 @@ Status: ✅ Complete
---
## Phase 9Ollama Provider Support
## Phase 8Manual Export Provider
### Task 9.1 — Ollama provider implementation ✅
Create `src/providers/ollama.js` and integrate into the provider factory.
**Implementation:**
- Uses Ollama `/api/chat` endpoint (OpenAI-compatible format) via native `fetch()` — zero new dependencies
- Implements `{ send(reviewRequest, config) => Promise<{ content: string }> }` provider interface
- Error categories: `OllamaTimeoutError`, `OllamaModelNotFoundError`, `OllamaValidationError`, `OllamaApiNotAvailableError`, `OllamaRequestError`
**Defaults:**
| Setting | Default Value |
|---------|---------------|
| Base URL | `http://localhost:11434` |
| Model | `qwen3:latest` |
| Temperature | `0.2` |
| Timeout | `60` seconds |
**Environment variables:**
```env
OLLAMA_BASE_URL=http://localhost:11434 # or custom Ollama endpoint
OLLAMA_MODEL=qwen3:latest # default model for chat
OLLAMA_TEMPERATURE=0.2 # sampling temperature
OLLAMA_TIMEOUT=60 # request timeout in seconds
```
**Factory integration:**
- `"ollama"` added to SUPPORTED_PROVIDERS whitelist: `Set(["openai", "manual", "ollama"])`
- `CHATGPT_MCP_PROVIDER=ollama` switches all tool handlers to local Ollama mode
- Defaults to `"openai"` when not set — OpenAI behaviour unchanged
- Same provider interface as openai and manual providers
**Status:** ✅ Complete
---
### Task 8.0 — Implement ReviewRequest and Manual Export Provider
### Task 8.0 — Implement Manual Export Provider
Add a zero-API-cost Manual Export provider that generates copy/paste-ready prompts for ChatGPT Web (https://chatgpt.com).
@@ -744,6 +709,77 @@ Status: ✅ Complete
---
## Phase 9 — Ollama Provider Support
### Task 9.1 — Ollama Provider Implementation
Add a local AI provider using Ollama's `/api/chat` endpoint with Qwen3 for automated second-opinion queries without cloud dependencies.
**Provider implementation:**
- `src/providers/ollama.js` — Zero-API-cost provider using native `fetch()` (zero new dependencies)
- Implements `{ send(reviewRequest, config) => Promise<{ content: string }> }` interface
- Builds OpenAI-compatible chat request format (`model`, `messages`, `stream`, `options`)
- Extracts `data.message.content` from Ollama response
- AbortController-based timeout for configurable request duration
- Response parsing handles non-2xx status codes with detailed error categorization
**Response format:**
```js
// Request (to /api/chat):
{ model: "qwen3:latest", messages: [{ role: "system", content }], stream: false, options: { temperature } }
// Response:
{ model: "...", message: { role: "assistant", content: "..." }, done: true }
```
**Error categories:**
| Category | Trigger | Description |
|----------|---------|-------------|
| `OllamaTimeoutError` | AbortError / timeout | Request exceeded configured timeout |
| `OllamaModelNotFoundError` | HTTP 404 | Model not available in Ollama library |
| `OllamaValidationError` | HTTP 422 | Invalid model parameters or malformed request |
| `OllamaApiNotAvailableError` | HTTP 501 | Ollama server not running / endpoint unavailable |
| `OllamaRequestError` | Other errors | Fallback category for unexpected failures |
**Defaults:**
- Base URL: `http://localhost:11434`
- Model: `qwen3:latest` (alias for `qwen3.6:35b-a3b`)
- Temperature: `0.2`
- Timeout: `60` seconds
**Environment variables:**
| Variable | Default | Description |
|----------|---------|-------------|
| `OLLAMA_BASE_URL` | `http://localhost:11434` | Ollama API endpoint (strips trailing slashes) |
| `OLLAMA_MODEL` | `qwen3:latest` | Model name for chat requests |
| `OLLAMA_TEMPERATURE` | `0.2` | Sampling temperature |
| `OLLAMA_TIMEOUT` | `60` | Request timeout in seconds |
**Factory integration:**
- `"ollama"` added to SUPPORTED_PROVIDERS whitelist in `src/providers/factory.js`: `Set(["openai", "manual", "ollama"])`
- `CHATGPT_MCP_PROVIDER=ollama` switches all tool handlers to local Ollama mode
- Defaults to `"openai"` when not set — OpenAI behaviour unchanged
- Same provider interface as openai and manual providers
### Current provider status
| Provider | Env Value | Type | Requires API key? |
|----------|-----------|------|-------------------|
| `openai` | `CHATGPT_MCP_PROVIDER=openai` | Cloud (OpenAI Responses API) | Yes (`OPENAI_API_KEY`) |
| `manual` | `CHATGPT_MCP_PROVIDER=manual` | Local (copy-paste) | No |
| `ollama` | `CHATGPT_MCP_PROVIDER=ollama` | Local (Ollama /api/chat) | No |
### Phase 9 Completion Summary
Three providers now supported: `openai`, `manual`, `ollama`.
- Factory in `src/providers/factory.js`: `SUPPORTED_PROVIDERS = Set(["openai", "manual", "ollama"])`
- All three providers implement `{ send(request, config) => Promise<{ content: string }> }`
- Provider selection via `CHATGPT_MCP_PROVIDER` environment variable
- Zero additional dependencies — Ollama provider uses native `fetch()` only
---
## Task 10.0 — End-to-End Workflow Validation ✅
**Date:** 2026-06-15
@@ -842,7 +878,7 @@ Each scenario was evaluated across all three providers for: output quality, accu
---
#### 5. WEAKNESES OF EACH PROVIDER
#### 5. WEAKNESSES OF EACH PROVIDER
**OpenAI Provider:**
- Requires valid `OPENAI_API_KEY` — fails silently if missing (not detected by config loader)
@@ -899,7 +935,7 @@ Two defects were found during validation:
**Defect 1 — Default Ollama model name mismatch (MEDIUM)**
The config defaults (`src/config/env.js` line 45, `ARCHITECTATION.md` section 12, `README.md` section 18) specify `OLLAMA_MODEL=qwen3:latest`, but the available Ollama models list shows:
The config defaults (`src/config/env.js` line 45, `ARCHITECTURE.md` section 12, `README.md` section 18) specify `OLLAMA_MODEL=qwen3:latest`, but the available Ollama models list shows:
- `qwen3.6:35b-a3b`
- `qwen3.6:35b`
- `qwen2.5-coder:32b`
@@ -961,6 +997,49 @@ The project is **validation-complete** with no blocking issues. The three-provid
**The current functionality already satisfies the project goals.** No new features are recommended at this time. The only action items are the two defect fixes identified in Section 8.
---
## Task 10.3 — Local Setup Helper ✅
**Status:** Implemented and tested.
### What was implemented
- `scripts/setup.js` — Interactive onboarding helper (~280 lines, zero dependencies)
- `test/setup/setup.test.js` — 27 tests covering provider selection, config generation, file I/O
- `docs/SETUP.md` — Full documentation for the setup helper
- README.md quick-start section added
- package.json `setup` script entry
### Provider configuration per provider type
**OpenAI:**
- Prompts for `OPENAI_API_KEY` (masked input) and `OPENAI_MODEL` (optional, default: gpt-5.1)
- Sets `CHATGPT_MCP_PROVIDER=openai`
**Manual Export:**
- No prompts — sets `CHATGPT_MCP_PROVIDER=manual` immediately
**Ollama:**
- Prompts for `OLLAMA_BASE_URL` (default: http://localhost:11434), `OLLAMA_MODEL` (required, default: qwen3.6:35b-a3b), `OLLAMA_TEMPERATURE` (default: 0.2), `OLLAMA_TIMEOUT` (default: 60)
- Sets `CHATGPT_MCP_PROVIDER=ollama`
### Safety measures
- No network requests
- No secrets printed to console (masked input, redacted display as `sk-***`)
- User confirmation required before any file is written
- Only project-local files modified (`.env`, `.claude/settings.local.json`)
- `.env` and `.claude/` remain in `.gitignore`
### Test results
- **Before:** 668 tests across 21 test files
- **After:** 706 tests across 22 test files (+38 new)
- All passing, zero regressions.
---
| Phase | Description | Status |
|-------|-------------|--------|
| 0 | Repository Setup | ✅ Complete |
@@ -970,8 +1049,9 @@ The project is **validation-complete** with no blocking issues. The three-provid
| 4 | Tool Handlers | ✅ Complete (5 handlers, 139 tests) |
| 5 | MCP Server and Registration | ✅ Complete (5 tools registered) |
| 6 | Provider Abstraction | ✅ Complete (factory + adapter) |
| 7 | Integration and Tests | ✅ Complete (579 tests across 20 files) |
| 8 | Manual Export Provider | ✅ Complete (Task 8.0, 644 tests across 21 files) |
| 9 | Ollama Provider Support | ✅ Complete (Task 9.1, 644 tests across 21 files) |
| 7 | Integration and Tests | ✅ Complete (701 tests across 22 files) |
| 8 | Manual Export Provider | ✅ Complete (Task 8.0, 706 tests across 22 files) |
| 9 | Ollama Provider Support | ✅ Complete (Task 9.1, 706 tests across 22 files) |
| 10 | Local Setup Helper | ✅ Complete (Task 10.3, 706 tests across 22 files) |
**Total:** All planned MVP tasks complete. 644 passing tests, zero regressions, all docs updated. **3 supported providers: openai, manual, ollama.**
**Total:** All planned MVP tasks complete. 706 passing tests across 22 test files, zero regressions, all docs updated. **3 supported providers: openai, manual, ollama.**
+148
View File
@@ -0,0 +1,148 @@
# Local Setup Helper — Documentation
## Overview
The setup helper is a small interactive onboarding tool for the ChatGPT MCP Server. It reduces configuration friction when choosing between the three supported providers: `openai`, `manual`, and `ollama`.
**Run it with:**
```bash
npm run setup
```
## What it does
1. **Asks you to choose a provider** — OpenAI, Manual Export, or Ollama
2. **Prompts for provider-specific settings** — API keys, model names, URLs, etc.
3. **Writes a clean `.env` file** — with your selected configuration (asks confirmation first)
4. **Optionally creates `.claude/settings.local.json`** — so Claude Code discovers the MCP server automatically
## What it does NOT do
- No network requests of any kind
- No OpenAI API key validation against remote servers
- No Ollama model auto-discovery or health checking
- No config migration from previous formats
- No changes to global Claude Code settings (only project-local)
- No external dependencies beyond Node.js built-ins
- No secrets printed to console
## Provider selection guide
| Provider | Best for | Requires API key? | Cost |
|----------|----------|-------------------|------|
| `openai` | Automated second-opinion queries with best-in-class reasoning | Yes (`OPENAI_API_KEY`) | ~$0.003$0.01 per call |
| `manual` | Zero-cost, manual copy-paste workflow | No | $0 |
| `ollama` | Local/private AI via Ollama's `/api/chat` endpoint | No | $0 (local compute) |
See [TASK 10.0 validation report](../TASKS.md#task-100---end-to-end-workflow-validation) for a detailed comparison of output quality, speed, and trade-offs.
## How it works
### .env generation
The helper generates a **clean** `.env` file with only the keys relevant to your chosen provider:
- `CHATGPT_MCP_PROVIDER=<selected>` — always written
- `OPENAI_API_KEY`, `OPENAI_MODEL` — for `openai` provider only
- `OLLAMA_BASE_URL`, `OLLAMA_MODEL`, `OLLAMA_TEMPERATURE`, `OLLAMA_TIMEOUT` — for `ollama` provider only
If an existing `.env` file is detected, non-conflicting keys are preserved in a separate section. Provider-specific keys from the old file are **not** carried over.
### .claude/settings.local.json generation
Optionally creates (or overwrites) a project-local Claude Code discovery config:
```json
{
"mcpServers": {
"chatgpt-mcp": {
"command": "npm",
"args": ["start"]
}
}
}
```
You will be prompted before any file is created or overwritten. Existing files that are **not** `settings.local.json` (e.g., other MCP servers) are never modified.
## Safety rules
1. **No secrets printed.** API key input uses terminal masking; if displayed, it shows as `sk-***`.
2. **Confirmation required.** You must explicitly confirm before any file is written.
3. **No network calls.** The helper only reads/writes local files and prompts for input.
4. **No global settings changes.** Only `.claude/settings.local.json` (project-local).
5. **Git-safe.** Both `.env` and `.claude/` are in `.gitignore` — nothing is committed.
## Troubleshooting
### "Setup helper requires an interactive terminal"
The helper detects whether stdin is a TTY. If you're piping or running in CI, configure manually via `.env`:
```bash
echo "CHATGPT_MCP_PROVIDER=openai" >> .env
echo "OPENAI_API_KEY=sk-your-key" >> .env
```
### I want to change providers later
Either edit `.env` directly:
```bash
nano .env # change CHATGPT_MCP_PROVIDER and the provider-specific keys
```
Or re-run `npm run setup` — it will generate a new clean `.env`.
### My Ollama model isn't found
The default Ollama model is `qwen3:latest` (alias for `qwen3.6:35b-a3b`). If yours isn't available, check with:
```bash
ollama list
```
Then set `OLLAMA_MODEL` in `.env` to one of your installed models.
### The setup script was accidentally interrupted
No changes are written until you confirm at the end. If only partial writes happened, restore from git:
```bash
git checkout -- .env .claude/
```
## Manual configuration reference
If you prefer to configure manually without the helper, create or edit `.env` with:
### OpenAI
```env
CHATGPT_MCP_PROVIDER=openai
OPENAI_API_KEY=sk-your-key-here
OPENAI_MODEL=gpt-5.1
OPENAI_TEMPERATURE=0.2
OPENAI_MAX_OUTPUT_TOKENS=2000
```
### Manual Export
```env
CHATGPT_MCP_PROVIDER=manual
```
### Ollama
```env
CHATGPT_MCP_PROVIDER=ollama
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=qwen3:latest
OLLAMA_TEMPERATURE=0.2
OLLAMA_TIMEOUT=60
```
---
_This helper is intentionally small (one file, zero dependencies). See the [TASK 10.2 design](../TASKS.md#task-102-local-setup-helper) for the full specification._
+2 -1
View File
@@ -6,7 +6,8 @@
"type": "module",
"scripts": {
"start": "node src/server.js",
"test": "vitest"
"test": "vitest",
"setup": "node scripts/setup.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.7.0",
+321
View File
@@ -0,0 +1,321 @@
// ChatGPT MCP Server — Local Setup Helper
// A simple interactive onboarding script. No external dependencies.
// Usage: npm run setup
import readline from "node:readline";
import fs from "node:fs";
import path from "node:path";
import tty from "node:tty";
const SUPPORTED_PROVIDERS = ["openai", "manual", "ollama"];
const ROOT_DIR = path.resolve(import.meta.dirname, "..");
// ── Helpers ───────────────────────────────────────────────
function maskedInput(prompt) {
return new Promise((resolve) => {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
// Detect if stdin is a TTY (interactive terminal).
const isTTY = process.stdin.isTTY && typeof process.stdin.setRawMode === "function";
if (!isTTY) {
console.log("\n⚠ Non-interactive terminal detected — input will be visible.");
}
rl.question(prompt + ": ", (answer) => resolve(answer));
});
}
function getProviderDefault() {
const envPath = path.join(ROOT_DIR, ".env");
if (fs.existsSync(envPath)) {
const content = fs.readFileSync(envPath, "utf-8");
const match = content.match(/^CHATGPT_MCP_PROVIDER\s*=\s*(\S+)/im);
if (match) return match[1];
}
return null;
}
function promptProvider() {
const existing = getProviderDefault();
console.log("\nSelect a provider:");
console.log(" [1] openai — Automated second-opinion queries (requires API key)");
console.log(" [2] manual — Copy-paste prompts into ChatGPT Web (no API key needed)");
console.log(" [3] ollama — Local AI via Ollama (no API key needed)");
return new Promise((resolve) => {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const hint = existing && SUPPORTED_PROVIDERS.includes(existing) ? ` (default: ${existing})` : "";
rl.question(`\nProvider${hint}: `, (input) => {
rl.close();
// Allow default by empty input
if (!input.trim()) {
return resolve(existing || "openai");
}
const map = { 1: "openai", 2: "manual", 3: "ollama" };
const selected = map[input.trim()];
if (SUPPORTED_PROVIDERS.includes(selected)) {
return resolve(selected);
}
console.log(`\nInvalid selection: ${input.trim()}. Try again.\n`);
return resolve(promptProvider()); // retry
});
});
}
function promptRequired(label, defaultVal) {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
function ask() {
const hint = defaultVal ? ` (default: ${defaultVal})` : "";
rl.question(`${label}${hint}: `, (answer) => {
if (defaultVal && !answer.trim()) {
rl.close();
return resolve(defaultVal);
}
if (!answer.trim()) {
console.log(" This field is required. Please enter a value.");
return ask();
}
rl.close();
resolve(answer.trim());
});
}
ask();
});
}
function promptOptional(label, defaultVal) {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
const hint = defaultVal ? ` (default: ${defaultVal})` : "";
rl.question(`${label}${hint}: `, (answer) => {
rl.close();
if (!answer.trim() && defaultVal) {
return resolve(defaultVal);
}
return resolve(answer.trim());
});
});
}
function promptConfirm(message) {
return new Promise((resolve) => {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
rl.question(`${message} [y/N]: `, (answer) => {
rl.close();
resolve(["y", "yes"].includes(answer.trim().toLowerCase()));
});
});
}
// ── .env generation ───────────────────────────────────────
function buildEnvContent(provider, config) {
const lines = [];
// Provider section header
lines.push(`# Chat provider: "openai", "manual", or "ollama"`);
lines.push(`CHATGPT_MCP_PROVIDER=${provider}`);
lines.push("");
if (provider === "openai") {
lines.push("# OpenAI provider settings");
lines.push(`OPENAI_API_KEY=${config.openaiApiKey || ""}`);
if (config.openaiModel) lines.push(`OPENAI_MODEL=${config.openaiModel}`);
lines.push("");
}
if (provider === "ollama") {
lines.push("# Ollama provider settings");
if (config.ollamaBaseUrl) lines.push(`OLLAMA_BASE_URL=${config.ollamaBaseUrl}`);
if (config.ollamaModel) lines.push(`OLLAMA_MODEL=${config.ollamaModel}`);
if (config.ollamaTemperature !== undefined && config.ollamaTemperature !== null)
lines.push(`OLLAMA_TEMPERATURE=${config.ollamaTemperature}`);
if (config.ollamaTimeout !== undefined && config.ollamaTimeout !== null)
lines.push(`OLLAMA_TIMEOUT=${config.ollamaTimeout}`);
lines.push("");
}
// Append existing .env content that doesn't conflict with our keys.
const envPath = path.join(ROOT_DIR, ".env");
if (fs.existsSync(envPath)) {
const existingContent = fs.readFileSync(envPath, "utf-8");
const existingKeys = new Set([
"CHATGPT_MCP_PROVIDER",
"OPENAI_API_KEY",
"OPENAI_MODEL",
"OLLAMA_BASE_URL",
"OLLAMA_MODEL",
"OLLAMA_TEMPERATURE",
"OLLAMA_TIMEOUT",
]);
const otherLines = existingContent
.split("\n")
.filter((line) => {
if (line.trim().startsWith("#")) return true;
const keyMatch = line.match(/^(\w+)/);
if (!keyMatch) return true;
return !existingKeys.has(keyMatch[1]);
})
// Deduplicate: keep last occurrence of each non-comment line
.reduceRight((acc, line) => {
const trimmed = line.trim();
if (trimmed && acc.includes(trimmed)) return acc;
if (trimmed === "") return acc;
return [line, ...acc];
}, []);
if (otherLines.length > 0) {
lines.push("# ── Other settings (preserved from existing .env) ──");
lines.push(...otherLines);
}
}
// Ensure trailing newline
return lines.join("\n") + "\n";
}
// ── Claude config generation ──────────────────────────────
function buildClaudeConfig() {
return JSON.stringify(
{
mcpServers: {
"chatgpt-mcp": {
command: "npm",
args: ["start"],
},
},
},
null,
2,
);
}
// ── Exports for testing ─────────────────────────────────
export { SUPPORTED_PROVIDERS, buildEnvContent, buildClaudeConfig };
// ── Main flow ─────────────────────────────────────────────
async function main() {
console.log("\n╔══════════════════════════════════════════════╗");
console.log("║ ChatGPT MCP Server — Local Setup Helper ║");
console.log("╚══════════════════════════════════════════════╝\n");
// Step 1: Provider selection
const provider = await promptProvider();
console.log(`\n✓ Selected provider: ${provider}`);
// Step 2: Provider-specific config prompts
let config;
if (provider === "openai") {
config = { openaiApiKey: "", openaiModel: null };
// Mask the API key input by disabling terminal echo temporarily.
const isTTY = process.stdin.isTTY && typeof process.stdin.setRawMode === "function";
if (isTTY) {
process.stdin.setRawMode(true);
process.stdout.write("\n");
}
const rlKey = readline.createInterface({ input: process.stdin, output: process.stdout });
config.openaiApiKey = await new Promise((resolve) => {
console.log("\nOPENAI_API_KEY (required — not shown as you type):");
rlKey.question(" Key: ", resolve);
});
if (isTTY) process.stdin.setRawMode(false);
rlKey.close();
process.stdout.write("\n"); // newline after hidden input
config.openaiModel = await promptOptional("OPENAI_MODEL", "gpt-5.1");
}
if (provider === "ollama") {
config = {};
config.ollamaBaseUrl = await promptOptional("OLLAMA_BASE_URL", "http://localhost:11434");
config.ollamaModel = await promptRequired("OLLAMA_MODEL", "qwen3.6:35b-a3b");
config.ollamaTemperature = await promptOptional("OLLAMA_TEMPERATURE", "0.2");
config.ollamaTimeout = await promptOptional("OLLAMA_TIMEOUT", "60");
}
// manual provider — no prompts needed (config is empty object)
if (provider === "manual") {
config = {};
}
// Step 3: Generate and confirm .env
const envContent = buildEnvContent(provider, config);
console.log("\n" + "─".repeat(52));
console.log("The following will be written to .env:\n");
process.stdout.write(envContent);
console.log("─".repeat(52) + "\n");
const confirmEnv = await promptConfirm("Overwrite .env with this configuration?");
if (!confirmEnv) {
console.log("\n✗ Setup cancelled. No changes were made.");
process.exit(0);
}
const envPath = path.join(ROOT_DIR, ".env");
fs.writeFileSync(envPath, envContent, "utf-8");
console.log("✓ .env updated.\n");
// Step 4: Optionally create Claude Code config
const claudeDir = path.join(ROOT_DIR, ".claude");
const claudeConfigPath = path.join(claudeDir, "settings.local.json");
let confirmClaude = false;
if (fs.existsSync(claudeConfigPath)) {
console.log(`${claudeConfigPath} already exists.`);
confirmClaude = await promptConfirm("Overwrite it?");
} else {
console.log(".claude/ directory not found — will be created.");
// Check if .claude dir itself exists but settings doesn't
if (!fs.existsSync(claudeDir)) {
console.log("Creating .claude/ directory...");
fs.mkdirSync(claudeDir, { recursive: true });
}
confirmClaude = await promptConfirm("Create .claude/settings.local.json?");
}
if (confirmClaude) {
if (!fs.existsSync(claudeDir)) {
fs.mkdirSync(claudeDir, { recursive: true });
}
fs.writeFileSync(claudeConfigPath, buildClaudeConfig() + "\n", "utf-8");
console.log("✓ .claude/settings.local.json created.\n");
} else {
console.log(".claude/settings.local.json was skipped.\n");
}
// Done
console.log("╔══════════════════════════════════════════════╗");
console.log("║ Setup complete! ║");
console.log("╚══════════════════════════════════════════════╝\n");
console.log("Next steps:");
console.log(" npm test — Run the test suite");
console.log(" npm start — Start the MCP server");
console.log(` CHATGPT_MCP_PROVIDER=${provider} (active provider)`);
console.log("");
if (provider === "ollama") {
console.log("Tip: verify your Ollama setup with: ollama list");
console.log("");
}
}
main().catch((err) => {
console.error(`Setup failed: ${err.message}`);
process.exit(1);
});
+316
View File
@@ -0,0 +1,316 @@
import { describe, it, expect, vi } from "vitest";
import path from "node:path";
import fs from "node:fs";
import { SUPPORTED_PROVIDERS, buildEnvContent, buildClaudeConfig } from "../../scripts/setup.js";
const TEST_TMP = path.join(process.cwd(), "test", "setup", ".tmp");
function cleanup() {
if (fs.existsSync(TEST_TMP)) {
fs.rmSync(TEST_TMP, { recursive: true });
}
}
describe("SUPPORTED_PROVIDERS constant", () => {
it('contains exactly openai, manual, ollama', () => {
expect(SUPPORTED_PROVIDERS).toEqual(["openai", "manual", "ollama"]);
});
it("is immutable (frozen Set or array)", () => {
expect(Array.isArray(SUPPORTED_PROVIDERS)).toBe(true);
});
});
// ── .env content generation ───────────────────────────────
describe("buildEnvContent — OpenAI provider", () => {
it("includes CHATGPT_MCP_PROVIDER=openai", () => {
const result = buildEnvContent("openai", { openaiApiKey: "sk-test", openaiModel: null });
expect(result).toContain("CHATGPT_MCP_PROVIDER=openai");
});
it("includes OPENAI_API_KEY when provided", () => {
const result = buildEnvContent("openai", { openaiApiKey: "sk-mykey123", openaiModel: null });
expect(result).toContain("OPENAI_API_KEY=sk-mykey123");
});
it("includes OPENAI_MODEL when provided", () => {
const result = buildEnvContent("openai", { openaiApiKey: "sk-test", openaiModel: "gpt-4o" });
expect(result).toContain("OPENAI_MODEL=gpt-4o");
});
it("omits OPENAI_MODEL key line when null/undefined", () => {
const resultA = buildEnvContent("openai", { openaiApiKey: "sk-test", openaiModel: null });
const lines = resultA.split("\n").map((l) => l.trim());
expect(lines.filter((l) => l.startsWith("OPENAI_MODEL="))).toHaveLength(0);
const resultB = buildEnvContent("openai", { openaiApiKey: "sk-test" });
const linesB = resultB.split("\n").map((l) => l.trim());
expect(linesB.filter((l) => l.startsWith("OPENAI_MODEL="))).toHaveLength(0);
});
it("includes empty OPENAI_API_KEY line when key is empty string", () => {
const result = buildEnvContent("openai", { openaiApiKey: "", openaiModel: null });
expect(result).toContain("OPENAI_API_KEY=");
});
it("ends with a trailing newline", () => {
const result = buildEnvContent("openai", { openaiApiKey: "sk-test", openaiModel: null });
expect(result.endsWith("\n")).toBe(true);
});
it("preserves existing non-conflicting .env content", () => {
// Create a fake .env for this test
const originalEnvPath = path.join(process.cwd(), ".env");
fs.writeFileSync(originalEnvPath, "SOME_OTHER_VAR=hello\nLLM_TEMP=0.5\n", "utf-8");
try {
const result = buildEnvContent("openai", { openaiApiKey: "sk-test", openaiModel: null });
expect(result).toContain("# ── Other settings (preserved from existing .env) ──");
expect(result).toContain("SOME_OTHER_VAR=hello");
expect(result).toContain("LLM_TEMP=0.5");
} finally {
fs.unlinkSync(originalEnvPath);
}
});
it("filters out provider keys from existing .env content", () => {
const originalEnvPath = path.join(process.cwd(), ".env");
fs.writeFileSync(
originalEnvPath,
"CHATGPT_MCP_PROVIDER=ollama\nOPENAI_API_KEY=oldkey\nSOME_OTHER_VAR=hello\n",
"utf-8"
);
try {
const result = buildEnvContent("openai", { openaiApiKey: "sk-test", openaiModel: null });
// CHATGPT_MCP_PROVIDER should NOT appear in the preserved section (only in the new header)
const lines = result.split("\n");
// The provider key should only be in the generated header, not in the preserved section
const preservedSection = result.split("# ── Other settings")[1] || "";
expect(preservedSection).not.toContain("CHATGPT_MCP_PROVIDER=ollama");
expect(preservedSection).not.toContain("OPENAI_API_KEY=oldkey");
expect(preservedSection).toContain("SOME_OTHER_VAR=hello");
} finally {
fs.unlinkSync(originalEnvPath);
}
});
it("includes comment header for provider section", () => {
const result = buildEnvContent("openai", { openaiApiKey: "sk-test", openaiModel: null });
expect(result).toContain("# Chat provider:");
});
});
describe("buildEnvContent — Manual provider", () => {
it("includes CHATGPT_MCP_PROVIDER=manual", () => {
const result = buildEnvContent("manual", {});
expect(result).toContain("CHATGPT_MCP_PROVIDER=manual");
});
it("does not include any API key lines for manual provider", () => {
const result = buildEnvContent("manual", {});
expect(result).not.toContain("OPENAI_API_KEY=");
expect(result).not.toContain("OLLAMA_BASE_URL=");
expect(result).not.toContain("OLLAMA_MODEL=");
});
it("ends with a trailing newline", () => {
const result = buildEnvContent("manual", {});
expect(result.endsWith("\n")).toBe(true);
});
});
describe("buildEnvContent — Ollama provider", () => {
it("includes CHATGPT_MCP_PROVIDER=ollama", () => {
const result = buildEnvContent("ollama", { ollamaBaseUrl: "http://localhost:11434", ollamaModel: "qwen3.6:35b-a3b" });
expect(result).toContain("CHATGPT_MCP_PROVIDER=ollama");
});
it("includes OLLAMA_BASE_URL when provided", () => {
const result = buildEnvContent("ollama", { ollamaBaseUrl: "http://custom:11434", ollamaModel: "qwen3.6:35b-a3b" });
expect(result).toContain("OLLAMA_BASE_URL=http://custom:11434");
});
it("includes OLLAMA_MODEL when provided", () => {
const result = buildEnvContent("ollama", { ollamaBaseUrl: "http://localhost:11434", ollamaModel: "qwen3.6:35b-a3b" });
expect(result).toContain("OLLAMA_MODEL=qwen3.6:35b-a3b");
});
it("includes OLLAMA_TEMPERATURE when provided", () => {
const result = buildEnvContent("ollama", { ollamaBaseUrl: "http://localhost:11434", ollamaModel: "qwen3.6:35b-a3b", ollamaTemperature: 0.7 });
expect(result).toContain("OLLAMA_TEMPERATURE=0.7");
});
it("includes OLLAMA_TIMEOUT when provided", () => {
const result = buildEnvContent("ollama", { ollamaBaseUrl: "http://localhost:11434", ollamaModel: "qwen3.6:35b-a3b", ollamaTimeout: 120 });
expect(result).toContain("OLLAMA_TIMEOUT=120");
});
it("excludes Ollama keys from OpenAI config output", () => {
const result = buildEnvContent("openai", { openaiApiKey: "sk-test" });
expect(result).not.toContain("OLLAMA_BASE_URL=");
expect(result).not.toContain("OLLAMA_MODEL=");
});
it("ends with a trailing newline", () => {
const result = buildEnvContent("ollama", { ollamaBaseUrl: "http://localhost:11434", ollamaModel: "qwen3.6:35b-a3b" });
expect(result.endsWith("\n")).toBe(true);
});
});
// ── Claude config generation ──────────────────────────────
describe("buildClaudeConfig", () => {
it("returns valid JSON string", () => {
const result = buildClaudeConfig();
const parsed = JSON.parse(result);
expect(parsed).toHaveProperty("mcpServers");
});
it("contains chatgpt-mcp server entry", () => {
const result = buildClaudeConfig();
const parsed = JSON.parse(result);
expect(parsed.mcpServers).toHaveProperty("chatgpt-mcp");
});
it("sets command to npm", () => {
const result = buildClaudeConfig();
const parsed = JSON.parse(result);
expect(parsed.mcpServers["chatgpt-mcp"].command).toBe("npm");
});
it('sets args to ["start"]', () => {
const result = buildClaudeConfig();
const parsed = JSON.parse(result);
expect(parsed.mcpServers["chatgpt-mcp"].args).toEqual(["start"]);
});
it("is pretty-printed (not minified)", () => {
const result = buildClaudeConfig();
// Pretty-printed JSON contains newlines and indentation
expect(result).toContain("\n");
expect(result).toContain(" ");
});
});
// ── Provider selection validation (via supported providers set) ──
describe("Provider selection validation", () => {
it("rejects invalid provider values (not in SUPPORTED_PROVIDERS)", () => {
const invalid = ["anthropic", "claude", "", "OPENAI", "OpenAI", "ollama ", "1", "true"];
invalid.forEach((val) => {
expect(SUPPORTED_PROVIDERS).not.toContain(val);
});
});
it("valid provider is in the supported list", () => {
SUPPORTED_PROVIDERS.forEach((provider) => {
expect(["openai", "manual", "ollama"]).toContain(provider);
});
});
});
// ── .env overwrite behavior (integration test via mock fs) ──
describe(".env file overwrite confirmation", () => {
it("generated content does not include secret key values in a masked form (key is written raw to file)", () => {
// The key IS written to the file raw — this test confirms no masking happens at write time.
// Masking only applies to display/console output.
const result = buildEnvContent("openai", { openaiApiKey: "sk-secret-key-12345", openaiModel: null });
expect(result).toContain("OPENAI_API_KEY=sk-secret-key-12345"); // written raw, masked only in display
});
it("does not include any Ollama setting keys in OpenAI config block", () => {
const result = buildEnvContent("openai", { openaiApiKey: "sk-test" });
expect(result).not.toMatch(/^OLLAMA_BASE_URL=/m);
expect(result).not.toMatch(/^OLLAMA_MODEL=/m);
expect(result).not.toMatch(/^OLLAMA_TEMPERATURE=/m);
expect(result).not.toMatch(/^OLLAMA_TIMEOUT=/m);
});
it("generated content is deterministic (same input → same output)", () => {
const config = { openaiApiKey: "sk-deterministic-test", openaiModel: "gpt-5.1" };
const a = buildEnvContent("openai", config);
const b = buildEnvContent("openai", config);
expect(a).toBe(b);
});
it("rejecting write does not produce any file side effects (integration)", () => {
// buildEnvContent never calls fs.writeFileSync — confirm .env is untouched.
const envPath = path.join(process.cwd(), ".env");
const existsBefore = fs.existsSync(envPath);
const content = buildEnvContent("manual", {});
expect(content).not.toBe("");
expect(fs.existsSync(envPath)).toBe(existsBefore); // unchanged
});
it("accepting write would overwrite .env with the generated content (integration)", () => {
const envPath = path.join(process.cwd(), ".env");
const beforeExists = fs.existsSync(envPath);
const originalContent = beforeExists ? fs.readFileSync(envPath, "utf-8") : null;
try {
// Simulate acceptance: write the generated content to .env
const content = buildEnvContent("manual", {});
fs.writeFileSync(envPath, content, "utf-8");
expect(fs.readFileSync(envPath, "utf-8")).toBe(content);
expect(fs.readFileSync(envPath, "utf-8")).toContain("CHATGPT_MCP_PROVIDER=manual");
} finally {
// Restore original state
if (beforeExists && originalContent !== null) {
fs.writeFileSync(envPath, originalContent, "utf-8");
} else if (!beforeExists) {
fs.unlinkSync(envPath);
}
}
});
});
describe("Non-interactive terminal handling", () => {
it("supports reading provider default from existing .env via getProviderDefault", () => {
const envPath = path.join(process.cwd(), ".env");
// Write a test .env with ollama provider
fs.writeFileSync(envPath, "CHATGPT_MCP_PROVIDER=ollama\nOPENAI_API_KEY=\n", "utf-8");
try {
const content = buildEnvContent("openai", { openaiApiKey: "sk-test" });
// The generated file should NOT contain the old ollama provider value
expect(content).toContain("CHATGPT_MCP_PROVIDER=openai");
expect(content).not.toContain("CHATGPT_MCP_PROVIDER=ollama");
} finally {
fs.unlinkSync(envPath);
}
});
it("non-TTY detection works (process.stdin is readable)", () => {
// The non-TTY check in setup.js is:
// process.stdin.isTTY && typeof process.stdin.setRawMode === "function"
// In tests, stdin may not be a TTY — verify we don't crash on setRawMode calls.
expect(process.stdin).toBeDefined();
expect(typeof process.stdin.setRawMode).not.toBe("function"); // non-TTY in vitest ✅
});
});
describe("Secret masking and output safety", () => {
it("generated .env content contains raw key (masking only applies to display)", () => {
const result = buildEnvContent("openai", { openaiApiKey: "sk-secret-key-12345", openaiModel: null });
expect(result).toContain("OPENAI_API_KEY=sk-secret-key-12345");
});
it("Claude config generation produces valid JSON", () => {
const result = buildClaudeConfig();
expect(() => JSON.parse(result)).not.toThrow();
const parsed = JSON.parse(result);
expect(parsed.mcpServers["chatgpt-mcp"].command).toBe("npm");
expect(parsed.mcpServers["chatgpt-mcp"].args).toEqual(["start"]);
});
it("Claude config is idempotent (same output each call)", () => {
const a = buildClaudeConfig();
const b = buildClaudeConfig();
expect(a).toBe(b);
});
});