oh-my-harness 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/catalog/blocks/tdd-guard.js +5 -2
- package/dist/cli/tui/init-flow.js +15 -3
- package/dist/cli/tui/provider-setup.d.ts +2 -0
- package/dist/cli/tui/provider-setup.js +85 -0
- package/dist/detector/detectors/node.js +1 -1
- package/dist/nl/config-store.d.ts +11 -0
- package/dist/nl/config-store.js +33 -0
- package/dist/nl/parse-intent.d.ts +8 -2
- package/dist/nl/parse-intent.js +17 -30
- package/dist/nl/provider-registry.d.ts +22 -0
- package/dist/nl/provider-registry.js +90 -0
- package/dist/nl/providers/claude-api.d.ts +2 -0
- package/dist/nl/providers/claude-api.js +42 -0
- package/dist/nl/providers/claude-cli.d.ts +2 -0
- package/dist/nl/providers/claude-cli.js +40 -0
- package/dist/nl/providers/gemini-api.d.ts +2 -0
- package/dist/nl/providers/gemini-api.js +44 -0
- package/dist/nl/providers/openai-api.d.ts +2 -0
- package/dist/nl/providers/openai-api.js +41 -0
- package/package.json +1 -1
|
@@ -72,8 +72,11 @@ fi
|
|
|
72
72
|
|
|
73
73
|
# edit-history에서 테스트 파일 검색
|
|
74
74
|
if jq -e --arg b "\$BASENAME" '.edits[] | select(contains($b) and (contains(".test.") or contains(".spec.") or contains("test_")))' "\$HISTORY_FILE" >/dev/null 2>&1; then
|
|
75
|
-
# 테스트 먼저 수정됨 → 소스 기록 + 통과
|
|
76
|
-
UPDATED=$(jq --arg
|
|
75
|
+
# 테스트 먼저 수정됨 → 매칭 테스트 기록 소비(제거) + 소스 기록 + 통과
|
|
76
|
+
UPDATED=$(jq --arg b "\$BASENAME" --arg f "\$FILE_PATH" '
|
|
77
|
+
.edits |= [.[] | select((contains($b) and (contains(".test.") or contains(".spec.") or contains("test_"))) | not)]
|
|
78
|
+
| .edits += [$f] | .edits |= unique
|
|
79
|
+
' "\$HISTORY_FILE" 2>/dev/null) || true
|
|
77
80
|
if [[ -n "\$UPDATED" ]]; then
|
|
78
81
|
echo "\$UPDATED" > "\$HISTORY_FILE"
|
|
79
82
|
fi
|
|
@@ -9,7 +9,9 @@ import { PresetRegistry } from "../../core/preset-registry.js";
|
|
|
9
9
|
import { loadAndMergePresets, writeHarnessState } from "../commands/init.js";
|
|
10
10
|
import { mergePresets } from "../../core/config-merger.js";
|
|
11
11
|
import { generate } from "../../core/generator.js";
|
|
12
|
-
import { generateHarnessConfig } from "../../nl/parse-intent.js";
|
|
12
|
+
import { generateHarnessConfig, createDefaultRunner } from "../../nl/parse-intent.js";
|
|
13
|
+
import { hasProviderConfig } from "../../nl/config-store.js";
|
|
14
|
+
import { runProviderSetup } from "./provider-setup.js";
|
|
13
15
|
import { mergeEnforcementAndHooks } from "../../core/harness-converter.js";
|
|
14
16
|
import { HarnessConfigSchema } from "../../core/harness-schema.js";
|
|
15
17
|
import { detectProject } from "../../detector/project-detector.js";
|
|
@@ -187,7 +189,17 @@ export async function runInitTUI(options) {
|
|
|
187
189
|
let harnessConfig;
|
|
188
190
|
let presetNames;
|
|
189
191
|
if (mode === "nl") {
|
|
190
|
-
// Step 4a: NL Mode
|
|
192
|
+
// Step 4a: NL Mode — check provider config
|
|
193
|
+
const hasConfig = await hasProviderConfig();
|
|
194
|
+
if (!hasConfig) {
|
|
195
|
+
p.log.info("No AI provider configured yet. Let's set one up.");
|
|
196
|
+
const providerConfig = await runProviderSetup();
|
|
197
|
+
if (!providerConfig) {
|
|
198
|
+
p.cancel("Provider setup cancelled. Use preset mode instead.");
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const runner = await createDefaultRunner();
|
|
191
203
|
const description = await p.text({
|
|
192
204
|
message: "Describe your project:",
|
|
193
205
|
placeholder: "e.g., Next.js e-commerce app with Stripe and Tailwind",
|
|
@@ -208,7 +220,7 @@ export async function runInitTUI(options) {
|
|
|
208
220
|
description: b.description,
|
|
209
221
|
params: b.params.map((pp) => ({ name: pp.name, required: pp.required, default: pp.default, description: pp.description })),
|
|
210
222
|
}));
|
|
211
|
-
harnessConfig = await generateHarnessConfig(description,
|
|
223
|
+
harnessConfig = await generateHarnessConfig(description, runner, catalogBlocks, projectFacts);
|
|
212
224
|
genSpinner.stop("Configuration generated");
|
|
213
225
|
}
|
|
214
226
|
catch (err) {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { getAvailableProviders, getProviderDefinition, } from "../../nl/provider-registry.js";
|
|
3
|
+
import { saveProviderConfig, } from "../../nl/config-store.js";
|
|
4
|
+
export async function runProviderSetup() {
|
|
5
|
+
p.intro("AI Provider Setup");
|
|
6
|
+
const providers = getAvailableProviders();
|
|
7
|
+
// Step 1: Select provider
|
|
8
|
+
const providerName = await p.select({
|
|
9
|
+
message: "Select AI provider for natural language mode:",
|
|
10
|
+
options: providers.map((prov) => ({
|
|
11
|
+
value: prov.name,
|
|
12
|
+
label: prov.displayName,
|
|
13
|
+
})),
|
|
14
|
+
});
|
|
15
|
+
if (p.isCancel(providerName)) {
|
|
16
|
+
p.cancel("Provider setup cancelled.");
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const def = getProviderDefinition(providerName);
|
|
20
|
+
// Step 2: Select method (CLI or API)
|
|
21
|
+
let method;
|
|
22
|
+
if (def.supportsCli && def.supportsApi) {
|
|
23
|
+
const selected = await p.select({
|
|
24
|
+
message: "How would you like to connect?",
|
|
25
|
+
options: [
|
|
26
|
+
{ value: "cli", label: `CLI tool (${def.cliCommand ?? def.name})` },
|
|
27
|
+
{ value: "api", label: "API Key" },
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
if (p.isCancel(selected)) {
|
|
31
|
+
p.cancel("Provider setup cancelled.");
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
method = selected;
|
|
35
|
+
}
|
|
36
|
+
else if (def.supportsCli) {
|
|
37
|
+
method = "cli";
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
method = "api";
|
|
41
|
+
}
|
|
42
|
+
const config = {
|
|
43
|
+
provider: providerName,
|
|
44
|
+
method,
|
|
45
|
+
};
|
|
46
|
+
// Step 3: Get API key if needed
|
|
47
|
+
if (method === "api") {
|
|
48
|
+
const apiKey = await p.text({
|
|
49
|
+
message: `Enter your ${def.displayName} API key:`,
|
|
50
|
+
placeholder: "sk-...",
|
|
51
|
+
validate: (value) => {
|
|
52
|
+
if (!value || !value.trim())
|
|
53
|
+
return "API key is required";
|
|
54
|
+
return undefined;
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
if (p.isCancel(apiKey)) {
|
|
58
|
+
p.cancel("Provider setup cancelled.");
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
config.apiKey = apiKey;
|
|
62
|
+
// Select model from available list
|
|
63
|
+
const selectedModel = await p.select({
|
|
64
|
+
message: "Select model:",
|
|
65
|
+
options: def.availableModels.map((m) => ({
|
|
66
|
+
value: m.id,
|
|
67
|
+
label: m.label,
|
|
68
|
+
hint: m.id === def.defaultModel ? "default" : undefined,
|
|
69
|
+
})),
|
|
70
|
+
initialValue: def.defaultModel,
|
|
71
|
+
});
|
|
72
|
+
if (p.isCancel(selectedModel)) {
|
|
73
|
+
p.cancel("Provider setup cancelled.");
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
config.model = selectedModel;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
config.cliCommand = def.cliCommand ?? def.name;
|
|
80
|
+
}
|
|
81
|
+
// Save config
|
|
82
|
+
await saveProviderConfig(config);
|
|
83
|
+
p.log.success(`Provider saved: ${def.displayName} (${method})`);
|
|
84
|
+
return config;
|
|
85
|
+
}
|
|
@@ -165,7 +165,7 @@ export const nodeDetector = {
|
|
|
165
165
|
const tsconfigPath = path.join(projectDir, "tsconfig.json");
|
|
166
166
|
if (await fileExists(tsconfigPath)) {
|
|
167
167
|
languages.push("typescript");
|
|
168
|
-
typecheckCommands.push("tsc --noEmit");
|
|
168
|
+
typecheckCommands.push("npx tsc --noEmit");
|
|
169
169
|
detectedFiles.push("tsconfig.json");
|
|
170
170
|
}
|
|
171
171
|
else {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ProviderConfig {
|
|
2
|
+
provider: "claude" | "openai" | "gemini";
|
|
3
|
+
method: "cli" | "api";
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
model?: string;
|
|
6
|
+
cliCommand?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function getConfigDir(): string;
|
|
9
|
+
export declare function hasProviderConfig(): Promise<boolean>;
|
|
10
|
+
export declare function loadProviderConfig(): Promise<ProviderConfig | undefined>;
|
|
11
|
+
export declare function saveProviderConfig(config: ProviderConfig): Promise<void>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
export function getConfigDir() {
|
|
5
|
+
const home = process.env.HOME ?? os.homedir();
|
|
6
|
+
return path.join(home, ".omh");
|
|
7
|
+
}
|
|
8
|
+
function getConfigPath() {
|
|
9
|
+
return path.join(getConfigDir(), "config.json");
|
|
10
|
+
}
|
|
11
|
+
export async function hasProviderConfig() {
|
|
12
|
+
try {
|
|
13
|
+
await fs.access(getConfigPath());
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function loadProviderConfig() {
|
|
21
|
+
try {
|
|
22
|
+
const raw = await fs.readFile(getConfigPath(), "utf-8");
|
|
23
|
+
return JSON.parse(raw);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function saveProviderConfig(config) {
|
|
30
|
+
const dir = getConfigDir();
|
|
31
|
+
await fs.mkdir(dir, { recursive: true });
|
|
32
|
+
await fs.writeFile(getConfigPath(), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
33
|
+
}
|
|
@@ -6,7 +6,13 @@ export interface ParsedIntent {
|
|
|
6
6
|
confidence: number;
|
|
7
7
|
explanation: string;
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
export
|
|
9
|
+
/** Generic LLM runner type — takes a prompt and returns a response string */
|
|
10
|
+
export type LLMRunner = (prompt: string) => Promise<string>;
|
|
11
|
+
/** @deprecated Use LLMRunner instead. Kept for backward compatibility. */
|
|
12
|
+
export type ClaudeRunner = LLMRunner;
|
|
13
|
+
/** Creates a runner from the saved provider config (~/.omh/config.json) or falls back to claude CLI */
|
|
14
|
+
export declare function createDefaultRunner(): Promise<LLMRunner>;
|
|
15
|
+
/** Legacy default runner using claude CLI directly */
|
|
16
|
+
export declare const defaultClaudeRunner: LLMRunner;
|
|
11
17
|
export declare function parseNaturalLanguage(description: string, availablePresets: PresetInfo[], runner?: ClaudeRunner): Promise<ParsedIntent>;
|
|
12
18
|
export declare function generateHarnessConfig(description: string, runner?: ClaudeRunner, catalogBlocks?: CatalogBlockInfo[], projectFacts?: ProjectFacts): Promise<HarnessConfig>;
|
package/dist/nl/parse-intent.js
CHANGED
|
@@ -1,37 +1,24 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import yaml from "js-yaml";
|
|
3
2
|
import { buildPresetSelectionPrompt, buildHarnessGenerationPrompt } from "./prompt-templates.js";
|
|
4
3
|
import { HarnessConfigSchema } from "../core/harness-schema.js";
|
|
4
|
+
import { loadProviderConfig } from "./config-store.js";
|
|
5
|
+
import { createProvider } from "./provider-registry.js";
|
|
6
|
+
import { createClaudeCliProvider } from "./providers/claude-cli.js";
|
|
7
|
+
/** Creates a runner from the saved provider config (~/.omh/config.json) or falls back to claude CLI */
|
|
8
|
+
export async function createDefaultRunner() {
|
|
9
|
+
const config = await loadProviderConfig();
|
|
10
|
+
if (config) {
|
|
11
|
+
const provider = createProvider(config);
|
|
12
|
+
return (prompt) => provider.run(prompt);
|
|
13
|
+
}
|
|
14
|
+
// Fallback: claude CLI
|
|
15
|
+
const cliProvider = createClaudeCliProvider("claude");
|
|
16
|
+
return (prompt) => cliProvider.run(prompt);
|
|
17
|
+
}
|
|
18
|
+
/** Legacy default runner using claude CLI directly */
|
|
5
19
|
export const defaultClaudeRunner = async (prompt) => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
9
|
-
env: { ...process.env },
|
|
10
|
-
});
|
|
11
|
-
let stdout = "";
|
|
12
|
-
let stderr = "";
|
|
13
|
-
proc.stdout.on("data", (data) => { stdout += data.toString(); });
|
|
14
|
-
proc.stderr.on("data", (data) => { stderr += data.toString(); });
|
|
15
|
-
proc.on("error", (err) => {
|
|
16
|
-
if (err.code === "ENOENT") {
|
|
17
|
-
reject(new Error("claude CLI not found. Install it with: npm install -g @anthropic-ai/claude-code"));
|
|
18
|
-
}
|
|
19
|
-
else {
|
|
20
|
-
reject(err);
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
proc.on("close", (code) => {
|
|
24
|
-
if (code === 0) {
|
|
25
|
-
resolve(stdout);
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
reject(new Error(`claude exited with code ${code}: ${stderr || stdout}`));
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
// Write prompt to stdin and close
|
|
32
|
-
proc.stdin.write(prompt);
|
|
33
|
-
proc.stdin.end();
|
|
34
|
-
});
|
|
20
|
+
const cliProvider = createClaudeCliProvider("claude");
|
|
21
|
+
return cliProvider.run(prompt);
|
|
35
22
|
};
|
|
36
23
|
function extractJson(text) {
|
|
37
24
|
// Try to extract a JSON object from text that may contain extra content
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ProviderConfig } from "./config-store.js";
|
|
2
|
+
export interface LLMProvider {
|
|
3
|
+
name: string;
|
|
4
|
+
run(prompt: string): Promise<string>;
|
|
5
|
+
}
|
|
6
|
+
export interface ModelEntry {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ProviderDefinition {
|
|
11
|
+
name: string;
|
|
12
|
+
displayName: string;
|
|
13
|
+
supportsCli: boolean;
|
|
14
|
+
supportsApi: boolean;
|
|
15
|
+
defaultModel: string;
|
|
16
|
+
availableModels: ModelEntry[];
|
|
17
|
+
cliCommand?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function getAvailableProviders(): ProviderDefinition[];
|
|
20
|
+
export declare function getProviderDefinition(name: string): ProviderDefinition | undefined;
|
|
21
|
+
export declare function getAvailableModels(providerName: string): ModelEntry[];
|
|
22
|
+
export declare function createProvider(config: ProviderConfig): LLMProvider;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createClaudeCliProvider } from "./providers/claude-cli.js";
|
|
2
|
+
import { createClaudeApiProvider } from "./providers/claude-api.js";
|
|
3
|
+
import { createOpenaiApiProvider } from "./providers/openai-api.js";
|
|
4
|
+
import { createGeminiApiProvider } from "./providers/gemini-api.js";
|
|
5
|
+
const providers = [
|
|
6
|
+
{
|
|
7
|
+
name: "claude",
|
|
8
|
+
displayName: "Claude (Anthropic)",
|
|
9
|
+
supportsCli: true,
|
|
10
|
+
supportsApi: true,
|
|
11
|
+
defaultModel: "claude-sonnet-4-6",
|
|
12
|
+
availableModels: [
|
|
13
|
+
{ id: "claude-opus-4-6", label: "Claude Opus 4.6 — most capable, 1M context" },
|
|
14
|
+
{ id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6 — balanced, 1M context" },
|
|
15
|
+
{ id: "claude-haiku-4-5", label: "Claude Haiku 4.5 — fastest, 200k context" },
|
|
16
|
+
],
|
|
17
|
+
cliCommand: "claude",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "openai",
|
|
21
|
+
displayName: "OpenAI (GPT-5.4)",
|
|
22
|
+
supportsCli: false,
|
|
23
|
+
supportsApi: true,
|
|
24
|
+
defaultModel: "gpt-5.4",
|
|
25
|
+
availableModels: [
|
|
26
|
+
{ id: "gpt-5.4", label: "GPT-5.4 — flagship, agentic & coding" },
|
|
27
|
+
{ id: "gpt-5.4-mini", label: "GPT-5.4 Mini — strongest mini model" },
|
|
28
|
+
{ id: "gpt-5.4-nano", label: "GPT-5.4 Nano — cheapest GPT-5.4 class" },
|
|
29
|
+
{ id: "gpt-4.1", label: "GPT-4.1 — best non-reasoning, coding" },
|
|
30
|
+
{ id: "gpt-4.1-mini", label: "GPT-4.1 Mini — balanced speed/cost" },
|
|
31
|
+
{ id: "o3", label: "o3 — complex reasoning, math, science" },
|
|
32
|
+
{ id: "o4-mini", label: "o4-mini — fast reasoning" },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "gemini",
|
|
37
|
+
displayName: "Gemini (Google)",
|
|
38
|
+
supportsCli: false,
|
|
39
|
+
supportsApi: true,
|
|
40
|
+
defaultModel: "gemini-2.5-pro",
|
|
41
|
+
availableModels: [
|
|
42
|
+
{ id: "gemini-2.5-pro", label: "Gemini 2.5 Pro — most advanced stable" },
|
|
43
|
+
{ id: "gemini-2.5-flash", label: "Gemini 2.5 Flash — fastest stable" },
|
|
44
|
+
{ id: "gemini-2.5-flash-lite", label: "Gemini 2.5 Flash Lite — most cost-effective" },
|
|
45
|
+
{ id: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro Preview — cutting-edge (preview)" },
|
|
46
|
+
{ id: "gemini-3-flash-preview", label: "Gemini 3 Flash Preview — frontier performance (preview)" },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
export function getAvailableProviders() {
|
|
51
|
+
return [...providers];
|
|
52
|
+
}
|
|
53
|
+
export function getProviderDefinition(name) {
|
|
54
|
+
return providers.find((p) => p.name === name);
|
|
55
|
+
}
|
|
56
|
+
export function getAvailableModels(providerName) {
|
|
57
|
+
const def = providers.find((p) => p.name === providerName);
|
|
58
|
+
return def ? [...def.availableModels] : [];
|
|
59
|
+
}
|
|
60
|
+
export function createProvider(config) {
|
|
61
|
+
const def = getProviderDefinition(config.provider);
|
|
62
|
+
if (!def) {
|
|
63
|
+
throw new Error(`Unknown AI provider: "${config.provider}". Available: ${providers.map((p) => p.name).join(", ")}`);
|
|
64
|
+
}
|
|
65
|
+
if (config.method !== "cli" && config.method !== "api") {
|
|
66
|
+
throw new Error(`Unsupported provider method: "${String(config.method)}"`);
|
|
67
|
+
}
|
|
68
|
+
if (config.method === "cli") {
|
|
69
|
+
if (config.provider === "claude") {
|
|
70
|
+
return createClaudeCliProvider(config.cliCommand ?? "claude");
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`Provider "${config.provider}" does not support CLI mode`);
|
|
73
|
+
}
|
|
74
|
+
// API mode
|
|
75
|
+
const apiKey = config.apiKey?.trim();
|
|
76
|
+
if (!apiKey) {
|
|
77
|
+
throw new Error(`API key is required for "${config.provider}" API mode`);
|
|
78
|
+
}
|
|
79
|
+
const model = config.model?.trim() || def.defaultModel;
|
|
80
|
+
switch (config.provider) {
|
|
81
|
+
case "claude":
|
|
82
|
+
return createClaudeApiProvider(apiKey, model);
|
|
83
|
+
case "openai":
|
|
84
|
+
return createOpenaiApiProvider(apiKey, model);
|
|
85
|
+
case "gemini":
|
|
86
|
+
return createGeminiApiProvider(apiKey, model);
|
|
87
|
+
default:
|
|
88
|
+
throw new Error(`Unknown provider: "${config.provider}"`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const DEFAULT_MODEL = "claude-sonnet-4-20250514";
|
|
2
|
+
const API_URL = "https://api.anthropic.com/v1/messages";
|
|
3
|
+
const REQUEST_TIMEOUT_MS = 60_000;
|
|
4
|
+
export function createClaudeApiProvider(apiKey, model = DEFAULT_MODEL) {
|
|
5
|
+
return {
|
|
6
|
+
name: "claude",
|
|
7
|
+
run: async (prompt) => {
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
10
|
+
let response;
|
|
11
|
+
try {
|
|
12
|
+
response = await fetch(API_URL, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: {
|
|
15
|
+
"Content-Type": "application/json",
|
|
16
|
+
"x-api-key": apiKey,
|
|
17
|
+
"anthropic-version": "2023-06-01",
|
|
18
|
+
},
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
model,
|
|
21
|
+
max_tokens: 4096,
|
|
22
|
+
messages: [{ role: "user", content: prompt }],
|
|
23
|
+
}),
|
|
24
|
+
signal: controller.signal,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
clearTimeout(timeout);
|
|
29
|
+
}
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
const errorBody = await response.text();
|
|
32
|
+
throw new Error(`Anthropic API error (${response.status}): ${errorBody}`);
|
|
33
|
+
}
|
|
34
|
+
const data = (await response.json());
|
|
35
|
+
const textBlock = data.content?.find((c) => c.type === "text");
|
|
36
|
+
if (!textBlock) {
|
|
37
|
+
throw new Error("Anthropic API returned no text content");
|
|
38
|
+
}
|
|
39
|
+
return textBlock.text;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export function createClaudeCliProvider(command = "claude") {
|
|
3
|
+
return {
|
|
4
|
+
name: "claude",
|
|
5
|
+
run: async (prompt) => {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const proc = spawn(command, ["-p", "-"], {
|
|
8
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
9
|
+
env: { ...process.env },
|
|
10
|
+
});
|
|
11
|
+
let stdout = "";
|
|
12
|
+
let stderr = "";
|
|
13
|
+
proc.stdout.on("data", (data) => {
|
|
14
|
+
stdout += data.toString();
|
|
15
|
+
});
|
|
16
|
+
proc.stderr.on("data", (data) => {
|
|
17
|
+
stderr += data.toString();
|
|
18
|
+
});
|
|
19
|
+
proc.on("error", (err) => {
|
|
20
|
+
if (err.code === "ENOENT") {
|
|
21
|
+
reject(new Error(`${command} CLI not found. Install it with: npm install -g @anthropic-ai/claude-code`));
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
reject(err);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
proc.on("close", (code) => {
|
|
28
|
+
if (code === 0) {
|
|
29
|
+
resolve(stdout.trim());
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
reject(new Error(`${command} exited with code ${code}: ${stderr || stdout}`));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
proc.stdin.write(prompt);
|
|
36
|
+
proc.stdin.end();
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const DEFAULT_MODEL = "gemini-2.5-flash";
|
|
2
|
+
const REQUEST_TIMEOUT_MS = 60_000;
|
|
3
|
+
function getApiUrl(model) {
|
|
4
|
+
return `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
|
|
5
|
+
}
|
|
6
|
+
export function createGeminiApiProvider(apiKey, model = DEFAULT_MODEL) {
|
|
7
|
+
return {
|
|
8
|
+
name: "gemini",
|
|
9
|
+
run: async (prompt) => {
|
|
10
|
+
const controller = new AbortController();
|
|
11
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
12
|
+
let response;
|
|
13
|
+
try {
|
|
14
|
+
response = await fetch(getApiUrl(model), {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: {
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
"x-goog-api-key": apiKey,
|
|
19
|
+
},
|
|
20
|
+
body: JSON.stringify({
|
|
21
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
22
|
+
generationConfig: {
|
|
23
|
+
maxOutputTokens: 4096,
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
signal: controller.signal,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
clearTimeout(timeout);
|
|
31
|
+
}
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
const errorBody = await response.text();
|
|
34
|
+
throw new Error(`Gemini API error (${response.status}): ${errorBody}`);
|
|
35
|
+
}
|
|
36
|
+
const data = (await response.json());
|
|
37
|
+
const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
38
|
+
if (!text) {
|
|
39
|
+
throw new Error("Gemini API returned no content");
|
|
40
|
+
}
|
|
41
|
+
return text;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const DEFAULT_MODEL = "gpt-5.4";
|
|
2
|
+
const API_URL = "https://api.openai.com/v1/chat/completions";
|
|
3
|
+
const REQUEST_TIMEOUT_MS = 60_000;
|
|
4
|
+
export function createOpenaiApiProvider(apiKey, model = DEFAULT_MODEL) {
|
|
5
|
+
return {
|
|
6
|
+
name: "openai",
|
|
7
|
+
run: async (prompt) => {
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
10
|
+
let response;
|
|
11
|
+
try {
|
|
12
|
+
response = await fetch(API_URL, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: {
|
|
15
|
+
"Content-Type": "application/json",
|
|
16
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify({
|
|
19
|
+
model,
|
|
20
|
+
messages: [{ role: "user", content: prompt }],
|
|
21
|
+
max_completion_tokens: 4096,
|
|
22
|
+
}),
|
|
23
|
+
signal: controller.signal,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
clearTimeout(timeout);
|
|
28
|
+
}
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
const errorBody = await response.text();
|
|
31
|
+
throw new Error(`OpenAI API error (${response.status}): ${errorBody}`);
|
|
32
|
+
}
|
|
33
|
+
const data = (await response.json());
|
|
34
|
+
const content = data.choices?.[0]?.message?.content;
|
|
35
|
+
if (content == null) {
|
|
36
|
+
throw new Error("OpenAI API returned no content");
|
|
37
|
+
}
|
|
38
|
+
return content;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|