feat: add manual export provider and ReviewRequest pattern
Implement Task 8.0 — Manual Export Provider with full provider abstraction:
Source files:
- src/providers/manual-export.js (104 lines) — new provider that wraps
pre-built prompts in copy-ready format for ChatGPT Web/Business use
- src/providers/factory.js — register 'manual' provider in whitelist
- src/providers/openai.js — update JSDoc for ProviderRequest parameter
All 5 handlers updated with ReviewRequest pattern:
- src/tools/ask-chatgpt.js
- src/tools/debug-issue.js
- src/tools/review-code.js
- src/tools/review-plan.js
- src/tools/architecture-review.js
Each handler now packages shared data as { prompt, input } between step 5
and 6 of the orchestration flow.
Tests:
- test/providers/manual-export.test.js (56 tests) — covers structure,
copy-ready box formatting, tool name detection, long prompts, Unicode,
edge cases, provider contract compliance, idempotency
- test/providers/factory.test.js (+9 tests) — manual provider whitelist,
factory routing, send delegation
Docs:
- ARCHITECTURE.md — provider selection table with openai/manual values
- README.md — provider comparison table and manual workflow description
This commit is contained in:
@@ -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}".`);
|
||||
|
||||
@@ -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) };
|
||||
},
|
||||
};
|
||||
@@ -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 }>}
|
||||
*/
|
||||
|
||||
@@ -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: [] };
|
||||
}
|
||||
|
||||
@@ -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: [] };
|
||||
|
||||
@@ -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: [] };
|
||||
}
|
||||
|
||||
@@ -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: [] };
|
||||
}
|
||||
|
||||
@@ -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: [] };
|
||||
|
||||
Reference in New Issue
Block a user