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.
@@ -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 f "\$FILE_PATH" '.edits += [$f] | .edits |= unique' "\$HISTORY_FILE" 2>/dev/null) || true
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, undefined, catalogBlocks, projectFacts);
223
+ harnessConfig = await generateHarnessConfig(description, runner, catalogBlocks, projectFacts);
212
224
  genSpinner.stop("Configuration generated");
213
225
  }
214
226
  catch (err) {
@@ -0,0 +1,2 @@
1
+ import { type ProviderConfig } from "../../nl/config-store.js";
2
+ export declare function runProviderSetup(): Promise<ProviderConfig | undefined>;
@@ -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
- export type ClaudeRunner = (prompt: string) => Promise<string>;
10
- export declare const defaultClaudeRunner: ClaudeRunner;
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>;
@@ -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
- return new Promise((resolve, reject) => {
7
- const proc = spawn("claude", ["-p", "-"], {
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,2 @@
1
+ import type { LLMProvider } from "../provider-registry.js";
2
+ export declare function createClaudeApiProvider(apiKey: string, model?: string): LLMProvider;
@@ -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,2 @@
1
+ import type { LLMProvider } from "../provider-registry.js";
2
+ export declare function createClaudeCliProvider(command?: string): LLMProvider;
@@ -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,2 @@
1
+ import type { LLMProvider } from "../provider-registry.js";
2
+ export declare function createGeminiApiProvider(apiKey: string, model?: string): LLMProvider;
@@ -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,2 @@
1
+ import type { LLMProvider } from "../provider-registry.js";
2
+ export declare function createOpenaiApiProvider(apiKey: string, model?: string): LLMProvider;
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-harness",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Tame your AI coding agents with natural language",
5
5
  "type": "module",
6
6
  "bin": {