diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 4f10b88..234d810 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -439,10 +439,17 @@ src/openai/responses.js — sendOpenAIResponse(client, params) API call wra
Configured via `CHATGPT_MCP_PROVIDER` env var (defaults to `"openai"`):
```env
-CHATGPT_MCP_PROVIDER=openai # current default; accepts any string but factory validates at runtime
+CHATGPT_MCP_PROVIDER=openai # or "manual" for copy-paste workflow
```
-The factory whitelists `"openai"`. Any unrecognized value throws at provider creation time (not config load time). Null/NaN/falsy values default to `"openai"` for safe fallback.
+Supported provider values:
+
+| Value | Description | Requires API key? |
+| -------- | -------------------------------------------------------------- | ----------------- |
+| `openai` | Default — calls ChatGPT via OpenAI API | Yes |
+| `manual` | Copy-paste — wraps prompts in a ready-to-copy format | No |
+
+The factory whitelists `"openai"` and `"manual"`. Any unrecognized value throws at provider creation time (not config load time). Null/NaN/falsy values default to `"openai"` for safe fallback.
### OpenAI-specific Environment Variables
@@ -453,7 +460,7 @@ OPENAI_TEMPERATURE=0.2
OPENAI_MAX_OUTPUT_TOKENS=2000
```
-The provider pattern makes it straightforward to add other providers (Ollama, Anthropic) — implement the `send(input, config)` interface and register it in the factory's whitelist.
+The provider pattern makes it straightforward to add other providers (Ollama, Anthropic, custom) — implement the `send(request, config)` interface and register it in the factory's whitelist.
---
diff --git a/README.md b/README.md
index 3141ec6..b05eb40 100644
--- a/README.md
+++ b/README.md
@@ -61,7 +61,16 @@ OpenAI Provider → OpenAI Responses API
Advisory Response
```
-The provider layer is configurable via `CHATGPT_MCP_PROVIDER` env var. Currently only `"openai"` is supported, but the factory pattern enables future providers without touching tool handlers.
+The provider layer is configurable via `CHATGPT_MCP_PROVIDER` env var. Two 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 |
+
+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.
+
+The factory pattern enables future providers (Ollama, Anthropic, custom) without touching tool handlers.
All responses are advisory only.
diff --git a/src/providers/factory.js b/src/providers/factory.js
index f666f26..cb6bfe4 100644
--- a/src/providers/factory.js
+++ b/src/providers/factory.js
@@ -2,15 +2,17 @@
// Returns a chat provider based on config value CHATGPT_MCP_PROVIDER.
import { openaiProvider } from "./openai.js";
+import { manualExportProvider } from "./manual-export.js";
-const SUPPORTED_PROVIDERS = new Set(["openai"]);
+const SUPPORTED_PROVIDERS = new Set(["openai", "manual"]);
/**
* Create a chat provider from configuration.
* Defaults to OpenAI if no provider is specified or value is invalid.
* @param {object} config - Loaded config object (must contain openaiApiKey).
* @param {string} config.chatgptMcpProvider - Provider name (default: "openai").
- * @returns {{ send: (input: object, cfg: object) => Promise<{ content: string }> }} Chat provider.
+ * @returns {{ send: (request: ProviderRequest, cfg: object) => Promise<{ content: string }> }} Chat provider.
+ * ProviderRequest = { prompt?: string, input?: object }
*/
export function createChatProvider(config) {
const providerName = config?.chatgptMcpProvider || "openai";
@@ -24,6 +26,8 @@ export function createChatProvider(config) {
switch (providerName) {
case "openai":
return openaiProvider;
+ case "manual":
+ return manualExportProvider;
default:
// Should not reach here because of the set check above.
throw new Error(`Unknown provider "${providerName}".`);
diff --git a/src/providers/manual-export.js b/src/providers/manual-export.js
new file mode 100644
index 0000000..3ef56a5
--- /dev/null
+++ b/src/providers/manual-export.js
@@ -0,0 +1,104 @@
+// Manual Export chat provider adapter.
+// Wraps the pre-built prompt into a copy-ready format for manual
+// use in ChatGPT Web, ChatGPT Business, or Claude Code.
+
+/**
+ * Detect which MCP tool produced this prompt from available input fields.
+ * Uses heuristic field detection — best-effort when only { prompt } is present.
+ * @param {object} request - ProviderRequest with optional { input } field.
+ * @returns {string} Lowercase tool name (e.g., "ask_chatgpt", "review_plan").
+ */
+function detectToolName(request) {
+ const input = request?.input;
+
+ if (!input) return "unknown";
+
+ if (input.logs) return "debug_issue";
+ if (input.relevantFiles && input.relevantFiles.some(f => f.path)) return "review_code";
+ if (input.context?.toString().toLowerCase().includes("architecture")) return "architecture_review";
+
+ // Distinguish review_plan from ask_chatgpt: review_plan typically has a projectSummary or taskSummary
+ if (input.projectSummary || input.taskSummary) return "review_plan";
+
+ return "ask_chatgpt";
+}
+
+/**
+ * Format prompt output with user instructions for manual copy/paste.
+ * @param {string} prompt - The pre-built prompt from the prompt builder.
+ * @param {string} toolName - Detected or default tool name.
+ * @returns {string} Formatted manual export content.
+ */
+function formatManualExport(prompt, toolName) {
+ const length = prompt.length.toLocaleString();
+ const lines = [
+ "═══════════════════════════════════════════════════",
+ `MANUAL EXPORT — ${toolName}`,
+ ];
+
+ if (prompt.length > 30_000) {
+ lines.push("⚠️ NOTE: Prompt is ~" + prompt.length.toLocaleString() + " characters — may approach ChatGPT context limits.");
+ }
+
+ lines.push("");
+ lines.push("📋 COPY THIS PROMPT INTO ChatGPT WEB / BUSINESS:");
+ lines.push("");
+ lines.push("┌───────────────────────────────────────────────────┐");
+
+ // The prompt itself — displayed as-is for accurate copy/paste
+ const promptLines = prompt.split("\n");
+ for (const pline of promptLines) {
+ lines.push("│ " + pline);
+ }
+
+ lines.push("└───────────────────────────────────────────────────┘");
+ lines.push("");
+ lines.push("📝 INSTRUCTIONS:");
+ lines.push("1. Open https://chat.openai.com (or your ChatGPT Business URL)");
+ lines.push("2. Paste the prompt above into the input box");
+ lines.push("3. Click Send");
+ lines.push("4. Review ChatGPT's second-opinion response");
+ lines.push("5. Compare with Claude Code's analysis — use both perspectives");
+ lines.push("");
+ lines.push("📊 METADATA:");
+ lines.push(`Tool: ${toolName}`);
+ lines.push("Provider: Manual Export (no API calls)");
+ lines.push(`Prompt Length: ${length} characters`);
+ lines.push("");
+ lines.push("═══════════════════════════════════════════════════");
+ lines.push("This is advisory output only. Claude Code remains the executor.");
+ lines.push("═══════════════════════════════════════════════════");
+
+ return lines.join("\n");
+}
+
+/**
+ * Chat provider that wraps the pre-built prompt for manual copy/paste
+ * into ChatGPT Web, ChatGPT Business, or Claude Code.
+ * Implements { send(request, config) => Promise<{ content: string }> }.
+ */
+export const manualExportProvider = {
+ /**
+ * @param {{ prompt?: string, input?: object }} request - ProviderRequest with pre-built prompt.
+ * @param {object} _config - Config (unused — Manual Export is stateless).
+ * @returns {Promise<{ content: string }>} Copy-ready prompt output.
+ */
+ async send(request, _config) {
+ const prompt = request?.prompt || "";
+
+ if (!prompt) {
+ return {
+ content: [
+ "⚠️ Manual Export Provider received no prompt to export.",
+ "",
+ "This indicates the handler did not pass the built prompt",
+ "in the provider request. Please check your handler",
+ "implementation and ensure reviewRequest includes { prompt }.",
+ ].join("\n"),
+ };
+ }
+
+ const toolName = detectToolName(request);
+ return { content: formatManualExport(prompt, toolName) };
+ },
+};
diff --git a/src/providers/openai.js b/src/providers/openai.js
index 6a5d3e1..45a9008 100644
--- a/src/providers/openai.js
+++ b/src/providers/openai.js
@@ -10,7 +10,7 @@ import { sendOpenAIResponse } from "../openai/responses.js";
*/
export const openaiProvider = {
/**
- * @param {object} input - validated tool input (post-budget check).
+ * @param {{ prompt?: string, input?: object }} request - ProviderRequest (prompt is unused by OpenAI provider).
* @param {object} config - full config from loadConfig().
* @returns {Promise<{ content: string }>}
*/
diff --git a/src/tools/architecture-review.js b/src/tools/architecture-review.js
index 960be6c..6c57aa1 100644
--- a/src/tools/architecture-review.js
+++ b/src/tools/architecture-review.js
@@ -15,7 +15,8 @@ import { buildArchitectureReviewPrompt } from "../prompts/architecture-review.js
* Raw tool input per ARCHITECTURE.md §7 schema.
* @param {{
* loadConfig: () => object,
- * createProvider: (config: object) => { send: (input: object, cfg: object) => Promise<{ content: string }> }
+ * createProvider: (config: object) => { send: (request: ProviderRequest, cfg: object) => Promise<{ content: string }> }
+ * ProviderRequest = { prompt?: string, input?: object }
* }} deps
* Injected external dependencies.
* @returns {Promise<{ ok: true, answer: string, warnings: string[] } | { ok: false, error: string, warnings: string[] }>}
@@ -57,11 +58,15 @@ export async function handleArchitectureReview(input, deps) {
return { ok: false, error: String(err), warnings: [] };
}
+ // --- 5b. Package shared data for provider ---
+
+ const reviewRequest = { prompt: promptMessages, input: budget.input };
+
// --- 6. Send via provider ---
let aiResult;
try {
- aiResult = await provider.send(budget.input, config);
+ aiResult = await provider.send(reviewRequest, config);
} catch (err) {
return { ok: false, error: String(err), warnings: [] };
}
diff --git a/src/tools/ask-chatgpt.js b/src/tools/ask-chatgpt.js
index c05d0ab..439c72c 100644
--- a/src/tools/ask-chatgpt.js
+++ b/src/tools/ask-chatgpt.js
@@ -15,7 +15,8 @@ import { buildAskChatGptPrompt } from "../prompts/ask-chatgpt.js";
* Raw tool input per ARCHITECTURE.md §7 schema.
* @param {{
* loadConfig: () => object,
- * createProvider: (config: object) => { send: (input: object, cfg: object) => Promise<{ content: string }> }
+ * createProvider: (config: object) => { send: (request: ProviderRequest, cfg: object) => Promise<{ content: string }> }
+ * ProviderRequest = { prompt?: string, input?: object }
* }} deps
* Injected external dependencies.
* @returns {Promise<{ ok: true, answer: string, warnings: string[] } | { ok: false, error: string, warnings: string[] }>}
@@ -57,11 +58,15 @@ export async function handleAskChatGpt(input, deps) {
return { ok: false, error: String(err), warnings: [] };
}
+ // --- 5b. Package shared data for provider ---
+
+ const reviewRequest = { prompt: promptMessages, input: budget.input };
+
// --- 6. Send via provider ---
let aiResult;
try {
- aiResult = await provider.send(budget.input, config);
+ aiResult = await provider.send(reviewRequest, config);
} catch (err) {
return { ok: false, error: String(err), warnings: [] };
diff --git a/src/tools/debug-issue.js b/src/tools/debug-issue.js
index fda9ab4..4795689 100644
--- a/src/tools/debug-issue.js
+++ b/src/tools/debug-issue.js
@@ -15,7 +15,8 @@ import { buildDebugIssuePrompt } from "../prompts/debug-issue.js";
* Raw tool input per ARCHITECTURE.md §7 schema.
* @param {{
* loadConfig: () => object,
- * createProvider: (config: object) => { send: (input: object, cfg: object) => Promise<{ content: string }> }
+ * createProvider: (config: object) => { send: (request: ProviderRequest, cfg: object) => Promise<{ content: string }> }
+ * ProviderRequest = { prompt?: string, input?: object }
* }} deps
* Injected external dependencies.
* @returns {Promise<{ ok: true, answer: string, warnings: string[] } | { ok: false, error: string, warnings: string[] }>}
@@ -57,11 +58,15 @@ export async function handleDebugIssue(input, deps) {
return { ok: false, error: String(err), warnings: [] };
}
+ // --- 5b. Package shared data for provider ---
+
+ const reviewRequest = { prompt: promptMessages, input: budget.input };
+
// --- 6. Send via provider ---
let aiResult;
try {
- aiResult = await provider.send(budget.input, config);
+ aiResult = await provider.send(reviewRequest, config);
} catch (err) {
return { ok: false, error: String(err), warnings: [] };
}
diff --git a/src/tools/review-code.js b/src/tools/review-code.js
index 69f4185..ccd3101 100644
--- a/src/tools/review-code.js
+++ b/src/tools/review-code.js
@@ -15,7 +15,8 @@ import { buildReviewCodePrompt } from "../prompts/review-code.js";
* Raw tool input per ARCHITECTURE.md §7 schema.
* @param {{
* loadConfig: () => object,
- * createProvider: (config: object) => { send: (input: object, cfg: object) => Promise<{ content: string }> }
+ * createProvider: (config: object) => { send: (request: ProviderRequest, cfg: object) => Promise<{ content: string }> }
+ * ProviderRequest = { prompt?: string, input?: object }
* }} deps
* Injected external dependencies.
* @returns {Promise<{ ok: true, answer: string, warnings: string[] } | { ok: false, error: string, warnings: string[] }>}
@@ -57,11 +58,15 @@ export async function handleReviewCode(input, deps) {
return { ok: false, error: String(err), warnings: [] };
}
+ // --- 5b. Package shared data for provider ---
+
+ const reviewRequest = { prompt: promptMessages, input: budget.input };
+
// --- 6. Send via provider ---
let aiResult;
try {
- aiResult = await provider.send(budget.input, config);
+ aiResult = await provider.send(reviewRequest, config);
} catch (err) {
return { ok: false, error: String(err), warnings: [] };
}
diff --git a/src/tools/review-plan.js b/src/tools/review-plan.js
index 3776424..fa2347d 100644
--- a/src/tools/review-plan.js
+++ b/src/tools/review-plan.js
@@ -15,7 +15,8 @@ import { buildReviewPlanPrompt } from "../prompts/review-plan.js";
* Raw tool input per ARCHITECTURE.md §7 schema.
* @param {{
* loadConfig: () => object,
- * createProvider: (config: object) => { send: (input: object, cfg: object) => Promise<{ content: string }> }
+ * createProvider: (config: object) => { send: (request: ProviderRequest, cfg: object) => Promise<{ content: string }> }
+ * ProviderRequest = { prompt?: string, input?: object }
* }} deps
* Injected external dependencies.
* @returns {Promise<{ ok: true, answer: string, warnings: string[] } | { ok: false, error: string, warnings: string[] }>}
@@ -57,11 +58,15 @@ export async function handleReviewPlan(input, deps) {
return { ok: false, error: String(err), warnings: [] };
}
+ // --- 5b. Package shared data for provider ---
+
+ const reviewRequest = { prompt: promptMessages, input: budget.input };
+
// --- 6. Send via provider ---
let aiResult;
try {
- aiResult = await provider.send(budget.input, config);
+ aiResult = await provider.send(reviewRequest, config);
} catch (err) {
return { ok: false, error: String(err), warnings: [] };
diff --git a/test/providers/factory.test.js b/test/providers/factory.test.js
index ed7b79f..8be2312 100644
--- a/test/providers/factory.test.js
+++ b/test/providers/factory.test.js
@@ -1,5 +1,6 @@
import { describe, it, expect, vi } from "vitest";
import { createChatProvider, openaiProvider } from "../../src/providers/factory.js";
+import { manualExportProvider } from "../../src/providers/manual-export.js";
function mockConfig(provider) {
const cfg = { chatgptMcpProvider: provider };
@@ -195,3 +196,66 @@ describe("repeatability", () => {
expect(p1).toBe(p2);
});
});
+
+// --- Manual provider ---
+
+describe("manual provider", () => {
+ it("supports manual provider via createChatProvider", () => {
+ const provider = createChatProvider({ chatgptMcpProvider: "manual" });
+ expect(provider.send).toBeDefined();
+ });
+
+ it("returns the same singleton instance for repeated calls with 'manual'", () => {
+ const p1 = createChatProvider({ chatgptMcpProvider: "manual" });
+ const p2 = createChatProvider({ chatgptMcpProvider: "manual" });
+ expect(p1).toBe(p2);
+ });
+
+ it("is different from openai provider", () => {
+ const manualP = createChatProvider({ chatgptMcpProvider: "manual" });
+ expect(manualP).not.toBe(openaiProvider);
+ });
+
+ it("can detect manual as a supported provider (not throw)", () => {
+ const provider = createChatProvider({ chatgptMcpProvider: "manual" });
+ expect(provider.send).toBeDefined();
+ });
+
+ it("manual provider send returns { content: string } shape", async () => {
+ const provider = createChatProvider({ chatgptMcpProvider: "manual" });
+ const result = await provider.send(
+ { prompt: "test prompt from manual env", input: { question: "hi" } },
+ {},
+ );
+ expect(result).toHaveProperty("content");
+ expect(typeof result.content).toBe("string");
+ });
+
+ it("manual provider output contains MANUAL EXPORT marker", async () => {
+ const provider = createChatProvider({ chatgptMcpProvider: "manual" });
+ const result = await provider.send({ prompt: "test", input: {} }, {});
+ expect(result.content).toContain("MANUAL EXPORT");
+ });
+
+ it("manual provider output contains COPY box delimiters", async () => {
+ const provider = createChatProvider({ chatgptMcpProvider: "manual" });
+ const result = await provider.send({ prompt: "test", input: {} }, {});
+ expect(result.content).toContain("┌");
+ expect(result.content).toContain("┐");
+ expect(result.content).toContain("┘");
+ });
+
+ it("can be switched from openai to manual and back", () => {
+ const p1 = createChatProvider({ chatgptMcpProvider: "openai" });
+ const p2 = createChatProvider({ chatgptMcpProvider: "manual" });
+ const p3 = createChatProvider({ chatgptMcpProvider: "openai" });
+ expect(p1).toBe(openaiProvider);
+ expect(p2).not.toBe(openaiProvider);
+ expect(p3).toBe(openaiProvider);
+ });
+
+ it("manual provider is not the same as openaiProvider singleton", () => {
+ const manualP = createChatProvider({ chatgptMcpProvider: "manual" });
+ expect(manualExportProvider).toBe(manualP);
+ });
+});
diff --git a/test/providers/manual-export.test.js b/test/providers/manual-export.test.js
new file mode 100644
index 0000000..f149875
--- /dev/null
+++ b/test/providers/manual-export.test.js
@@ -0,0 +1,506 @@
+import { describe, it, expect } from "vitest";
+import { manualExportProvider } from "../../src/providers/manual-export.js";
+
+// --- Basic structure ---
+
+describe("basic structure", () => {
+ it("exports a provider with send method", () => {
+ expect(manualExportProvider).toBeDefined();
+ expect(typeof manualExportProvider.send).toBe("function");
+ });
+
+ it("returns { content: string } shape on success", async () => {
+ const result = await manualExportProvider.send(
+ { prompt: "test prompt" },
+ {},
+ );
+ expect(result).toHaveProperty("content");
+ expect(typeof result.content).toBe("string");
+ });
+
+ it("returns non-empty content for valid prompt", async () => {
+ const result = await manualExportProvider.send(
+ { prompt: "test prompt" },
+ {},
+ );
+ expect(result.content.length).toBeGreaterThan(0);
+ });
+
+ it("is deterministic — same input produces same output", async () => {
+ const input = { prompt: "deterministic test", input: { question: "hi" } };
+ const r1 = await manualExportProvider.send(input, {});
+ const r2 = await manualExportProvider.send(input, {});
+ expect(r1.content).toBe(r2.content);
+ });
+
+ it("does not modify the request object", async () => {
+ const req = { prompt: "test", input: { question: "hi" } };
+ const original = JSON.stringify(req);
+ await manualExportProvider.send(req, {});
+ expect(JSON.stringify(req)).toBe(original);
+ });
+
+ it("does not modify the config object", async () => {
+ const config = { openaiApiKey: "sk-test" };
+ await manualExportProvider.send({ prompt: "test" }, config);
+ expect(config.openaiApiKey).toBe("sk-test");
+ });
+});
+
+// --- Copy-ready structure ---
+
+describe("copy-ready structure", () => {
+ it("includes MANUAL EXPORT header with tool name", async () => {
+ const result = await manualExportProvider.send(
+ { prompt: "test", input: { question: "hi" } },
+ {},
+ );
+ expect(result.content).toContain("MANUAL EXPORT");
+ });
+
+ it("includes COPY box with top-left corner delimiters (┌)", async () => {
+ const result = await manualExportProvider.send(
+ { prompt: "test", input: { question: "hi" } },
+ {},
+ );
+ expect(result.content).toContain("┌");
+ });
+
+ it("includes COPY box with bottom-right corner delimiters (┐ and ┘)", async () => {
+ const result = await manualExportProvider.send(
+ { prompt: "test", input: { question: "hi" } },
+ {},
+ );
+ expect(result.content).toContain("┐");
+ expect(result.content).toContain("└");
+ expect(result.content).toContain("┘");
+ });
+
+ it("includes instructions section with numbered steps", async () => {
+ const result = await manualExportProvider.send(
+ { prompt: "test", input: { question: "hi" } },
+ {},
+ );
+ expect(result.content).toContain("📝 INSTRUCTIONS:");
+ expect(result.content).toContain("1.");
+ expect(result.content).toContain("2.");
+ expect(result.content).toContain("3.");
+ });
+
+ it("includes metadata section", async () => {
+ const result = await manualExportProvider.send(
+ { prompt: "test", input: { question: "hi" } },
+ {},
+ );
+ expect(result.content).toContain("📊 METADATA:");
+ });
+
+ it("includes provider info in metadata", async () => {
+ const result = await manualExportProvider.send(
+ { prompt: "test", input: { question: "hi" } },
+ {},
+ );
+ expect(result.content).toContain("Manual Export");
+ });
+
+ it("includes tool name in metadata", async () => {
+ const result = await manualExportProvider.send(
+ { prompt: "test", input: { question: "hi" } },
+ {},
+ );
+ expect(result.content).toMatch(/Tool: \w+/);
+ });
+
+ it("includes prompt length in metadata with formatted thousands separator", async () => {
+ const longPrompt = "x".repeat(12345);
+ const result = await manualExportProvider.send(
+ { prompt: longPrompt, input: { question: "hi" } },
+ {},
+ );
+ expect(result.content).toContain("Prompt Length:");
+ expect(result.content).toContain("12,345 characters");
+ });
+
+ it("includes advisory footer", async () => {
+ const result = await manualExportProvider.send(
+ { prompt: "test", input: { question: "hi" } },
+ {},
+ );
+ expect(result.content).toContain("advisory output only");
+ });
+
+ it("wraps each line of prompt in box with │ delimiter", async () => {
+ const multiLine = "line one\nline two\nline three";
+ const result = await manualExportProvider.send(
+ { prompt: multiLine, input: { question: "hi" } },
+ {},
+ );
+ expect(result.content).toContain("│ line one");
+ expect(result.content).toContain("│ line two");
+ expect(result.content).toContain("│ line three");
+ });
+});
+
+// --- Per-tool detection ---
+
+describe("tool name detection", () => {
+ it("detects debug_issue when input has logs", async () => {
+ const result = await manualExportProvider.send(
+ { prompt: "test", input: { logs: "Error: boom" } },
+ {},
+ );
+ expect(result.content).toContain("Tool: debug_issue");
+ });
+
+ it("detects review_code when input has relevantFiles with paths", async () => {
+ const result = await manualExportProvider.send(
+ { prompt: "test", input: { relevantFiles: [{ path: "src/foo.js" }] } },
+ {},
+ );
+ expect(result.content).toContain("Tool: review_code");
+ });
+
+ it("detects architecture_review when context includes 'architecture'", async () => {
+ const result = await manualExportProvider.send(
+ { prompt: "test", input: { context: "Architecture decision here" } },
+ {},
+ );
+ expect(result.content).toContain("Tool: architecture_review");
+ });
+
+ it("detects review_plan when input has projectSummary", async () => {
+ const result = await manualExportProvider.send(
+ { prompt: "test", input: { projectSummary: "Project scope" } },
+ {},
+ );
+ expect(result.content).toContain("Tool: review_plan");
+ });
+
+ it("defaults to ask_chatgpt when no hints available", async () => {
+ const result = await manualExportProvider.send(
+ { prompt: "test", input: { question: "hi" } },
+ {},
+ );
+ expect(result.content).toContain("Tool: ask_chatgpt");
+ });
+
+ it("uses 'unknown' when input is absent in request", async () => {
+ const result = await manualExportProvider.send(
+ { prompt: "test" },
+ {},
+ );
+ expect(result.content).toContain("Tool: unknown");
+ });
+});
+
+// --- Long prompt handling ---
+
+describe("long prompt handling", () => {
+ it("adds warning banner for prompts over 30k characters", async () => {
+ const longPrompt = "x".repeat(31_000);
+ const result = await manualExportProvider.send({ prompt: longPrompt, input: {} }, {});
+ expect(result.content).toContain("⚠️ NOTE");
+ });
+
+ it("does NOT add warning for prompts under 30k characters", async () => {
+ const validPrompt = "x".repeat(29_999);
+ const result = await manualExportProvider.send({ prompt: validPrompt, input: {} }, {});
+ expect(result.content).not.toContain("⚠️ NOTE");
+ });
+
+ it("still returns valid output for very long prompts (>50k chars)", async () => {
+ const veryLongPrompt = "x".repeat(50_000);
+ const result = await manualExportProvider.send({ prompt: veryLongPrompt, input: {} }, {});
+ expect(result.content).toContain("MANUAL EXPORT");
+ expect(result.content.length).toBeGreaterThan(0);
+ });
+
+ it("shows correct character count in metadata for long prompts", async () => {
+ const length = 50_000;
+ const result = await manualExportProvider.send({ prompt: "x".repeat(length), input: {} }, {});
+ expect(result.content).toContain(`${length.toLocaleString()} characters`);
+ });
+
+ it("handles prompts at exactly the 30k boundary", async () => {
+ const exactPrompt = "x".repeat(30_000);
+ const result = await manualExportProvider.send({ prompt: exactPrompt, input: {} }, {});
+ expect(result.content).not.toContain("⚠️ NOTE"); // not over 30k, exactly at it
+ });
+});
+
+// --- Unicode and special characters ---
+
+describe("unicode and special characters", () => {
+ it("preserves Japanese characters in prompt", async () => {
+ const result = await manualExportProvider.send(
+ { prompt: "こんにちは世界", input: {} },
+ {},
+ );
+ expect(result.content).toContain("こんにちは世界");
+ });
+
+ it("preserves emoji in prompt", async () => {
+ const result = await manualExportProvider.send(
+ { prompt: "🚀 Launch plan 🎯", input: {} },
+ {},
+ );
+ expect(result.content).toContain("🚀");
+ expect(result.content).toContain("🎯");
+ });
+
+ it("preserves markdown code blocks in prompt", async () => {
+ const codePrompt = "```js\nconst x = 1;\n```";
+ const result = await manualExportProvider.send(
+ { prompt: codePrompt, input: {} },
+ {},
+ );
+ expect(result.content).toContain("```js");
+ expect(result.content).toContain("const x = 1;");
+ });
+
+ it("preserves JSON in prompt", async () => {
+ const jsonPrompt = '{"key": "value", "nested": {"a": 1}}';
+ const result = await manualExportProvider.send(
+ { prompt: jsonPrompt, input: {} },
+ {},
+ );
+ expect(result.content).toContain('"key"');
+ expect(result.content).toContain('"value"');
+ });
+
+ it("preserves special HTML-like characters", async () => {
+ const htmlPrompt = '';
+ const result = await manualExportProvider.send(
+ { prompt: htmlPrompt, input: {} },
+ {},
+ );
+ expect(result.content).toContain('