@kitsy/coop-ai 2.1.0 → 2.1.2

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/index.cjs CHANGED
@@ -30,6 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ CliAgentClient: () => CliAgentClient,
34
+ CliCompletionProvider: () => CliCompletionProvider,
33
35
  buildIdeaRefinementPrompt: () => buildIdeaRefinementPrompt,
34
36
  buildTaskRefinementPrompt: () => buildTaskRefinementPrompt,
35
37
  build_contract: () => build_contract,
@@ -928,7 +930,15 @@ var DEFAULT_MODELS = {
928
930
  openai: "gpt-5-mini",
929
931
  anthropic: "claude-3-5-sonnet-latest",
930
932
  gemini: "gemini-2.0-flash",
931
- ollama: "llama3.2"
933
+ ollama: "llama3.2",
934
+ codex_cli: "gpt-5-codex",
935
+ claude_cli: "sonnet",
936
+ gemini_cli: "gemini-2.5-pro"
937
+ };
938
+ var DEFAULT_COMMAND = {
939
+ codex_cli: "codex",
940
+ claude_cli: "claude",
941
+ gemini_cli: "gemini"
932
942
  };
933
943
  var DEFAULT_KEY_ENV = {
934
944
  openai: "OPENAI_API_KEY",
@@ -958,7 +968,7 @@ function asFinite(value) {
958
968
  }
959
969
  function readProvider(value) {
960
970
  const normalized = asString(value)?.toLowerCase();
961
- if (normalized === "openai" || normalized === "anthropic" || normalized === "gemini" || normalized === "ollama" || normalized === "mock") {
971
+ if (normalized === "openai" || normalized === "anthropic" || normalized === "gemini" || normalized === "ollama" || normalized === "mock" || normalized === "codex_cli" || normalized === "claude_cli" || normalized === "gemini_cli") {
962
972
  return normalized;
963
973
  }
964
974
  return "mock";
@@ -979,15 +989,34 @@ function resolve_provider_config(config) {
979
989
  timeout_ms: 6e4
980
990
  };
981
991
  }
992
+ if (provider === "codex_cli" || provider === "claude_cli" || provider === "gemini_cli") {
993
+ const section2 = lookupProviderSection(ai, provider);
994
+ const model2 = asString(section2.model) ?? asString(ai.model) ?? DEFAULT_MODELS[provider];
995
+ const timeout_ms2 = asFinite(section2.timeout_ms) ?? asFinite(ai.timeout_ms) ?? 3e5;
996
+ const command = asString(section2.command) ?? DEFAULT_COMMAND[provider];
997
+ const argsRaw = Array.isArray(section2.args) ? section2.args : Array.isArray(ai.args) ? ai.args : [];
998
+ const args = argsRaw.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
999
+ return {
1000
+ provider,
1001
+ model: model2,
1002
+ command,
1003
+ args,
1004
+ temperature: 0.2,
1005
+ max_output_tokens: 4096,
1006
+ timeout_ms: timeout_ms2
1007
+ };
1008
+ }
982
1009
  const section = lookupProviderSection(ai, provider);
983
- const model = asString(section.model) ?? asString(ai.model) ?? DEFAULT_MODELS[provider];
984
- const base_url = asString(section.base_url) ?? asString(ai.base_url) ?? DEFAULT_BASE_URL[provider];
1010
+ const cloudProvider = provider;
1011
+ const model = asString(section.model) ?? asString(ai.model) ?? DEFAULT_MODELS[cloudProvider];
1012
+ const base_url = asString(section.base_url) ?? asString(ai.base_url) ?? DEFAULT_BASE_URL[cloudProvider];
985
1013
  const temperature = asFinite(section.temperature) ?? asFinite(ai.temperature) ?? 0.2;
986
1014
  const max_output_tokens = asFinite(section.max_output_tokens) ?? asFinite(ai.max_output_tokens) ?? 1024;
987
1015
  const timeout_ms = asFinite(section.timeout_ms) ?? asFinite(ai.timeout_ms) ?? 6e4;
988
1016
  let api_key;
989
1017
  if (provider !== "ollama") {
990
- const envName = asString(section.api_key_env) ?? asString(ai.api_key_env) ?? DEFAULT_KEY_ENV[provider];
1018
+ const keyedProvider = provider;
1019
+ const envName = asString(section.api_key_env) ?? asString(ai.api_key_env) ?? DEFAULT_KEY_ENV[keyedProvider];
991
1020
  const envValue = envName ? asString(process.env[envName]) : null;
992
1021
  api_key = asString(section.api_key) ?? asString(ai.api_key) ?? envValue ?? void 0;
993
1022
  if (!api_key) {
@@ -1007,6 +1036,250 @@ function resolve_provider_config(config) {
1007
1036
  };
1008
1037
  }
1009
1038
 
1039
+ // src/providers/cli.ts
1040
+ var import_node_fs2 = __toESM(require("fs"), 1);
1041
+ var import_node_os = __toESM(require("os"), 1);
1042
+ var import_node_path3 = __toESM(require("path"), 1);
1043
+ var import_node_child_process2 = require("child_process");
1044
+ function defaultRunner(invocation) {
1045
+ const result = (0, import_node_child_process2.spawnSync)(invocation.command, invocation.args, {
1046
+ cwd: invocation.cwd,
1047
+ encoding: "utf8",
1048
+ input: invocation.input,
1049
+ timeout: invocation.timeout_ms,
1050
+ windowsHide: true
1051
+ });
1052
+ return {
1053
+ status: result.status,
1054
+ stdout: result.stdout ?? "",
1055
+ stderr: result.stderr ?? "",
1056
+ error: result.error
1057
+ };
1058
+ }
1059
+ function asText(result) {
1060
+ return [result.stdout, result.stderr].map((value) => value.trim()).filter(Boolean).join("\n").trim();
1061
+ }
1062
+ function requireSuccess(provider, result) {
1063
+ if ((result.status ?? 1) === 0 && !result.error) {
1064
+ return;
1065
+ }
1066
+ const details = asText(result) || result.error?.message || "unknown error";
1067
+ throw new Error(`${provider} execution failed: ${details}`);
1068
+ }
1069
+ function tempOutputFile(prefix) {
1070
+ return import_node_path3.default.join(import_node_os.default.tmpdir(), `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`);
1071
+ }
1072
+ function combinedPrompt(input) {
1073
+ return [input.system.trim(), input.prompt.trim()].filter(Boolean).join("\n\n");
1074
+ }
1075
+ function completionInvocation(config, input, cwd) {
1076
+ if (config.provider === "codex_cli") {
1077
+ const outputFile = tempOutputFile("coop-codex-complete");
1078
+ return {
1079
+ command: config.command ?? "codex",
1080
+ args: [
1081
+ "exec",
1082
+ "-C",
1083
+ cwd,
1084
+ "--skip-git-repo-check",
1085
+ "--sandbox",
1086
+ "read-only",
1087
+ ...config.model ? ["--model", config.model] : [],
1088
+ "--output-last-message",
1089
+ outputFile,
1090
+ ...config.args ?? [],
1091
+ "-"
1092
+ ],
1093
+ cwd,
1094
+ input: combinedPrompt(input),
1095
+ timeout_ms: config.timeout_ms
1096
+ };
1097
+ }
1098
+ if (config.provider === "claude_cli") {
1099
+ return {
1100
+ command: config.command ?? "claude",
1101
+ args: [
1102
+ "-p",
1103
+ combinedPrompt(input),
1104
+ "--output-format",
1105
+ "text",
1106
+ "--permission-mode",
1107
+ "plan",
1108
+ ...config.model ? ["--model", config.model] : [],
1109
+ ...config.args ?? []
1110
+ ],
1111
+ cwd,
1112
+ timeout_ms: config.timeout_ms
1113
+ };
1114
+ }
1115
+ return {
1116
+ command: config.command ?? "gemini",
1117
+ args: [
1118
+ "-p",
1119
+ combinedPrompt(input),
1120
+ "--output-format",
1121
+ "text",
1122
+ "--approval-mode",
1123
+ "plan",
1124
+ ...config.model ? ["--model", config.model] : [],
1125
+ ...config.args ?? []
1126
+ ],
1127
+ cwd,
1128
+ timeout_ms: config.timeout_ms
1129
+ };
1130
+ }
1131
+ function readCodexOutput(invocation) {
1132
+ const index = invocation.args.indexOf("--output-last-message");
1133
+ const outputFile = index >= 0 ? invocation.args[index + 1] : void 0;
1134
+ if (!outputFile) return null;
1135
+ if (!import_node_fs2.default.existsSync(outputFile)) return null;
1136
+ try {
1137
+ return import_node_fs2.default.readFileSync(outputFile, "utf8").trim();
1138
+ } finally {
1139
+ import_node_fs2.default.rmSync(outputFile, { force: true });
1140
+ }
1141
+ }
1142
+ var CliCompletionProvider = class {
1143
+ name;
1144
+ config;
1145
+ cwd;
1146
+ runner;
1147
+ constructor(config, options = {}) {
1148
+ this.name = config.provider;
1149
+ this.config = config;
1150
+ this.cwd = import_node_path3.default.resolve(options.cwd ?? process.cwd());
1151
+ this.runner = options.runner ?? defaultRunner;
1152
+ }
1153
+ async complete(input) {
1154
+ const invocation = completionInvocation(this.config, input, this.cwd);
1155
+ const result = this.runner(invocation);
1156
+ requireSuccess(this.name, result);
1157
+ const codexOutput = this.config.provider === "codex_cli" ? readCodexOutput(invocation) : null;
1158
+ return {
1159
+ text: codexOutput || result.stdout.trim() || result.stderr.trim()
1160
+ };
1161
+ }
1162
+ };
1163
+ function buildExecutionPrompt(task, contract, step, prompt, mode) {
1164
+ const sections = [
1165
+ mode === "execute" ? "You are executing a COOP task inside the repo. Make the necessary file changes, run allowed commands when needed, and finish with a concise summary." : "You are reviewing a COOP task inside the repo. Inspect the current workspace and return concise review findings only.",
1166
+ `Task: ${task.id} - ${task.title}`,
1167
+ `Goal: ${contract.goal}`,
1168
+ `Step: ${step.step} (${step.action})`,
1169
+ contract.context.acceptance_criteria.length > 0 ? `Acceptance: ${contract.context.acceptance_criteria.join(" | ")}` : "",
1170
+ contract.context.tests_required.length > 0 ? `Tests Required: ${contract.context.tests_required.join(" | ")}` : "",
1171
+ contract.context.authority_refs.length > 0 ? `Authority Refs: ${contract.context.authority_refs.join(" | ")}` : "",
1172
+ contract.context.derived_refs.length > 0 ? `Derived Refs: ${contract.context.derived_refs.join(" | ")}` : "",
1173
+ contract.permissions.read_paths.length > 0 ? `Allowed Read Paths: ${contract.permissions.read_paths.join(", ")}` : "",
1174
+ contract.permissions.write_paths.length > 0 ? `Allowed Write Paths: ${contract.permissions.write_paths.join(", ")}` : "",
1175
+ contract.permissions.allowed_commands.length > 0 ? `Allowed Commands: ${contract.permissions.allowed_commands.join(", ")}` : "",
1176
+ contract.permissions.forbidden_commands.length > 0 ? `Forbidden Commands: ${contract.permissions.forbidden_commands.join(", ")}` : "",
1177
+ prompt
1178
+ ].filter(Boolean);
1179
+ return sections.join("\n\n");
1180
+ }
1181
+ function countGitChanges(repoRoot, runner, timeoutMs) {
1182
+ const result = runner({
1183
+ command: "git",
1184
+ args: ["status", "--porcelain"],
1185
+ cwd: repoRoot,
1186
+ timeout_ms: timeoutMs
1187
+ });
1188
+ if ((result.status ?? 1) !== 0) return 0;
1189
+ return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).length;
1190
+ }
1191
+ function executionInvocation(config, prompt, repoRoot, mode) {
1192
+ if (config.provider === "codex_cli") {
1193
+ const outputFile = tempOutputFile("coop-codex-run");
1194
+ return {
1195
+ command: config.command ?? "codex",
1196
+ args: [
1197
+ "exec",
1198
+ "-C",
1199
+ repoRoot,
1200
+ "--skip-git-repo-check",
1201
+ "--sandbox",
1202
+ mode === "execute" ? "workspace-write" : "read-only",
1203
+ ...mode === "execute" ? ["--full-auto"] : [],
1204
+ ...config.model ? ["--model", config.model] : [],
1205
+ "--output-last-message",
1206
+ outputFile,
1207
+ ...config.args ?? [],
1208
+ "-"
1209
+ ],
1210
+ cwd: repoRoot,
1211
+ input: prompt,
1212
+ timeout_ms: config.timeout_ms
1213
+ };
1214
+ }
1215
+ if (config.provider === "claude_cli") {
1216
+ return {
1217
+ command: config.command ?? "claude",
1218
+ args: [
1219
+ "-p",
1220
+ prompt,
1221
+ "--output-format",
1222
+ "text",
1223
+ "--permission-mode",
1224
+ mode === "execute" ? "bypassPermissions" : "plan",
1225
+ ...config.model ? ["--model", config.model] : [],
1226
+ ...config.args ?? []
1227
+ ],
1228
+ cwd: repoRoot,
1229
+ timeout_ms: config.timeout_ms
1230
+ };
1231
+ }
1232
+ return {
1233
+ command: config.command ?? "gemini",
1234
+ args: [
1235
+ "-p",
1236
+ prompt,
1237
+ "--output-format",
1238
+ "text",
1239
+ "--approval-mode",
1240
+ mode === "execute" ? "yolo" : "plan",
1241
+ ...config.model ? ["--model", config.model] : [],
1242
+ ...config.args ?? []
1243
+ ],
1244
+ cwd: repoRoot,
1245
+ timeout_ms: config.timeout_ms
1246
+ };
1247
+ }
1248
+ var CliAgentClient = class {
1249
+ config;
1250
+ repoRoot;
1251
+ runner;
1252
+ constructor(config, options = {}) {
1253
+ this.config = config;
1254
+ this.repoRoot = import_node_path3.default.resolve(options.cwd ?? process.cwd());
1255
+ this.runner = options.runner ?? defaultRunner;
1256
+ }
1257
+ async generate(input) {
1258
+ const before = countGitChanges(this.repoRoot, this.runner, this.config.timeout_ms);
1259
+ const prompt = buildExecutionPrompt(input.task, input.contract, input.step, input.prompt, "execute");
1260
+ const invocation = executionInvocation(this.config, prompt, this.repoRoot, "execute");
1261
+ const result = this.runner(invocation);
1262
+ requireSuccess(this.config.provider, result);
1263
+ const after = countGitChanges(this.repoRoot, this.runner, this.config.timeout_ms);
1264
+ const codexOutput = this.config.provider === "codex_cli" ? readCodexOutput(invocation) : null;
1265
+ return {
1266
+ summary: codexOutput || result.stdout.trim() || result.stderr.trim() || `Completed ${input.step.step}.`,
1267
+ file_changes: Math.max(0, after - before)
1268
+ };
1269
+ }
1270
+ async review(input) {
1271
+ const prompt = buildExecutionPrompt(input.task, input.contract, input.step, input.prompt, "review");
1272
+ const invocation = executionInvocation(this.config, prompt, this.repoRoot, "review");
1273
+ const result = this.runner(invocation);
1274
+ requireSuccess(this.config.provider, result);
1275
+ const codexOutput = this.config.provider === "codex_cli" ? readCodexOutput(invocation) : null;
1276
+ return {
1277
+ summary: codexOutput || result.stdout.trim() || result.stderr.trim() || `Reviewed ${input.step.step}.`,
1278
+ file_changes: 0
1279
+ };
1280
+ }
1281
+ };
1282
+
1010
1283
  // src/providers/http.ts
1011
1284
  async function post_json(url, init) {
1012
1285
  const controller = new AbortController();
@@ -1203,6 +1476,10 @@ function create_provider(config) {
1203
1476
  return new GeminiProvider(resolved);
1204
1477
  case "ollama":
1205
1478
  return new OllamaProvider(resolved);
1479
+ case "codex_cli":
1480
+ case "claude_cli":
1481
+ case "gemini_cli":
1482
+ return new CliCompletionProvider(resolved);
1206
1483
  case "mock":
1207
1484
  default:
1208
1485
  return new MockProvider();
@@ -1266,7 +1543,11 @@ function asAgentResponse(text, tokens) {
1266
1543
  tokens_used: tokens
1267
1544
  };
1268
1545
  }
1269
- function create_provider_agent_client(config) {
1546
+ function create_provider_agent_client(config, runtime = {}) {
1547
+ const resolved = resolve_provider_config(config);
1548
+ if (resolved.provider === "codex_cli" || resolved.provider === "claude_cli" || resolved.provider === "gemini_cli") {
1549
+ return new CliAgentClient(resolved, runtime);
1550
+ }
1270
1551
  const provider = create_provider(config);
1271
1552
  return {
1272
1553
  async generate(input) {
@@ -1322,6 +1603,8 @@ function create_provider_refinement_client(config) {
1322
1603
  }
1323
1604
  // Annotate the CommonJS export names for ESM import in node:
1324
1605
  0 && (module.exports = {
1606
+ CliAgentClient,
1607
+ CliCompletionProvider,
1325
1608
  buildIdeaRefinementPrompt,
1326
1609
  buildTaskRefinementPrompt,
1327
1610
  build_contract,
package/dist/index.d.cts CHANGED
@@ -216,12 +216,14 @@ declare function refine_idea_to_draft(input: IdeaRefinementInput, client?: Refin
216
216
  declare function refine_task_to_draft(input: TaskRefinementInput, client?: RefinementClient): Promise<RefinementDraft>;
217
217
  declare function parseRefinementDraftResponse(text: string, mode: "idea" | "task", sourceId: string, sourceTitle: string): RefinementDraft | null;
218
218
 
219
- type AiProviderName = "mock" | "openai" | "anthropic" | "gemini" | "ollama";
219
+ type AiProviderName = "mock" | "openai" | "anthropic" | "gemini" | "ollama" | "codex_cli" | "claude_cli" | "gemini_cli";
220
220
  interface ProviderConfig {
221
221
  provider: AiProviderName;
222
222
  model: string;
223
223
  base_url?: string;
224
224
  api_key?: string;
225
+ command?: string;
226
+ args?: string[];
225
227
  temperature: number;
226
228
  max_output_tokens: number;
227
229
  timeout_ms: number;
@@ -245,6 +247,52 @@ interface LlmProvider {
245
247
  */
246
248
  declare function resolve_provider_config(config: unknown): ProviderConfig;
247
249
 
250
+ type CliProviderName = Extract<ProviderConfig["provider"], "codex_cli" | "claude_cli" | "gemini_cli">;
251
+ interface CliRunnerInvocation {
252
+ command: string;
253
+ args: string[];
254
+ cwd: string;
255
+ input?: string;
256
+ timeout_ms: number;
257
+ }
258
+ interface CliRunnerResult {
259
+ status: number | null;
260
+ stdout: string;
261
+ stderr: string;
262
+ error?: Error;
263
+ }
264
+ type CliRunner = (invocation: CliRunnerInvocation) => CliRunnerResult;
265
+ interface CliRuntimeOptions {
266
+ cwd?: string;
267
+ runner?: CliRunner;
268
+ }
269
+ declare class CliCompletionProvider implements LlmProvider {
270
+ readonly name: CliProviderName;
271
+ private readonly config;
272
+ private readonly cwd;
273
+ private readonly runner;
274
+ constructor(config: ProviderConfig, options?: CliRuntimeOptions);
275
+ complete(input: CompletionInput): Promise<CompletionResult>;
276
+ }
277
+ declare class CliAgentClient implements AgentClient {
278
+ private readonly config;
279
+ private readonly repoRoot;
280
+ private readonly runner;
281
+ constructor(config: ProviderConfig, options?: CliRuntimeOptions);
282
+ generate(input: {
283
+ task: Task;
284
+ step: NonNullable<NonNullable<Task["execution"]>["runbook"]>[number];
285
+ contract: AgentContract;
286
+ prompt: string;
287
+ }): Promise<AgentResponse>;
288
+ review(input: {
289
+ task: Task;
290
+ step: NonNullable<NonNullable<Task["execution"]>["runbook"]>[number];
291
+ contract: AgentContract;
292
+ prompt: string;
293
+ }): Promise<AgentResponse>;
294
+ }
295
+
248
296
  /**
249
297
  * Creates an LLM provider instance from COOP config.
250
298
  * [SPEC: Architecture v2.0 §13]
@@ -255,7 +303,7 @@ declare function create_provider(config: unknown): LlmProvider;
255
303
  * Creates an executor client backed by configured LLM provider(s).
256
304
  * [SPEC: Architecture v2.0 §13]
257
305
  */
258
- declare function create_provider_agent_client(config: unknown): AgentClient;
306
+ declare function create_provider_agent_client(config: unknown, runtime?: CliRuntimeOptions): AgentClient;
259
307
  /**
260
308
  * Creates an idea decomposition client backed by configured LLM provider(s).
261
309
  * [SPEC: Architecture v2.0 §Phase 4]
@@ -263,4 +311,4 @@ declare function create_provider_agent_client(config: unknown): AgentClient;
263
311
  declare function create_provider_idea_decomposer(config: unknown): IdeaDecomposerClient;
264
312
  declare function create_provider_refinement_client(config: unknown): RefinementClient;
265
313
 
266
- export { type AgentClient, type AgentContract, type AgentContractConstraints, type AgentContractContext, type AgentContractPermissions, type AgentContractRelatedArtifacts, type AgentResponse, type AiProviderName, type CompletionInput, type CompletionResult, type DecomposedTaskDraft, type ExecuteTaskOptions, type FileAccessMode, type IdeaDecomposerClient, type IdeaDecompositionInput, type IdeaRefinementInput, type LlmProvider, type ProviderConfig, type RefinementClient, type RefinementDraft, type RefinementProposalAction, type RefinementTaskProposal, type RunResult, type SandboxRunState, type TaskRefinementInput, buildIdeaRefinementPrompt, buildTaskRefinementPrompt, build_contract, build_decomposition_prompt, constraint_violation_reasons, create_provider, create_provider_agent_client, create_provider_idea_decomposer, create_provider_refinement_client, create_run, decompose_idea_to_tasks, enforce_constraints, execute_task, finalize_run, log_step, parseRefinementDraftResponse, refine_idea_to_draft, refine_task_to_draft, resolve_provider_config, select_agent, validate_command, validate_file_access, write_run };
314
+ export { type AgentClient, type AgentContract, type AgentContractConstraints, type AgentContractContext, type AgentContractPermissions, type AgentContractRelatedArtifacts, type AgentResponse, type AiProviderName, CliAgentClient, CliCompletionProvider, type CliRunner, type CliRunnerInvocation, type CliRunnerResult, type CliRuntimeOptions, type CompletionInput, type CompletionResult, type DecomposedTaskDraft, type ExecuteTaskOptions, type FileAccessMode, type IdeaDecomposerClient, type IdeaDecompositionInput, type IdeaRefinementInput, type LlmProvider, type ProviderConfig, type RefinementClient, type RefinementDraft, type RefinementProposalAction, type RefinementTaskProposal, type RunResult, type SandboxRunState, type TaskRefinementInput, buildIdeaRefinementPrompt, buildTaskRefinementPrompt, build_contract, build_decomposition_prompt, constraint_violation_reasons, create_provider, create_provider_agent_client, create_provider_idea_decomposer, create_provider_refinement_client, create_run, decompose_idea_to_tasks, enforce_constraints, execute_task, finalize_run, log_step, parseRefinementDraftResponse, refine_idea_to_draft, refine_task_to_draft, resolve_provider_config, select_agent, validate_command, validate_file_access, write_run };
package/dist/index.d.ts CHANGED
@@ -216,12 +216,14 @@ declare function refine_idea_to_draft(input: IdeaRefinementInput, client?: Refin
216
216
  declare function refine_task_to_draft(input: TaskRefinementInput, client?: RefinementClient): Promise<RefinementDraft>;
217
217
  declare function parseRefinementDraftResponse(text: string, mode: "idea" | "task", sourceId: string, sourceTitle: string): RefinementDraft | null;
218
218
 
219
- type AiProviderName = "mock" | "openai" | "anthropic" | "gemini" | "ollama";
219
+ type AiProviderName = "mock" | "openai" | "anthropic" | "gemini" | "ollama" | "codex_cli" | "claude_cli" | "gemini_cli";
220
220
  interface ProviderConfig {
221
221
  provider: AiProviderName;
222
222
  model: string;
223
223
  base_url?: string;
224
224
  api_key?: string;
225
+ command?: string;
226
+ args?: string[];
225
227
  temperature: number;
226
228
  max_output_tokens: number;
227
229
  timeout_ms: number;
@@ -245,6 +247,52 @@ interface LlmProvider {
245
247
  */
246
248
  declare function resolve_provider_config(config: unknown): ProviderConfig;
247
249
 
250
+ type CliProviderName = Extract<ProviderConfig["provider"], "codex_cli" | "claude_cli" | "gemini_cli">;
251
+ interface CliRunnerInvocation {
252
+ command: string;
253
+ args: string[];
254
+ cwd: string;
255
+ input?: string;
256
+ timeout_ms: number;
257
+ }
258
+ interface CliRunnerResult {
259
+ status: number | null;
260
+ stdout: string;
261
+ stderr: string;
262
+ error?: Error;
263
+ }
264
+ type CliRunner = (invocation: CliRunnerInvocation) => CliRunnerResult;
265
+ interface CliRuntimeOptions {
266
+ cwd?: string;
267
+ runner?: CliRunner;
268
+ }
269
+ declare class CliCompletionProvider implements LlmProvider {
270
+ readonly name: CliProviderName;
271
+ private readonly config;
272
+ private readonly cwd;
273
+ private readonly runner;
274
+ constructor(config: ProviderConfig, options?: CliRuntimeOptions);
275
+ complete(input: CompletionInput): Promise<CompletionResult>;
276
+ }
277
+ declare class CliAgentClient implements AgentClient {
278
+ private readonly config;
279
+ private readonly repoRoot;
280
+ private readonly runner;
281
+ constructor(config: ProviderConfig, options?: CliRuntimeOptions);
282
+ generate(input: {
283
+ task: Task;
284
+ step: NonNullable<NonNullable<Task["execution"]>["runbook"]>[number];
285
+ contract: AgentContract;
286
+ prompt: string;
287
+ }): Promise<AgentResponse>;
288
+ review(input: {
289
+ task: Task;
290
+ step: NonNullable<NonNullable<Task["execution"]>["runbook"]>[number];
291
+ contract: AgentContract;
292
+ prompt: string;
293
+ }): Promise<AgentResponse>;
294
+ }
295
+
248
296
  /**
249
297
  * Creates an LLM provider instance from COOP config.
250
298
  * [SPEC: Architecture v2.0 §13]
@@ -255,7 +303,7 @@ declare function create_provider(config: unknown): LlmProvider;
255
303
  * Creates an executor client backed by configured LLM provider(s).
256
304
  * [SPEC: Architecture v2.0 §13]
257
305
  */
258
- declare function create_provider_agent_client(config: unknown): AgentClient;
306
+ declare function create_provider_agent_client(config: unknown, runtime?: CliRuntimeOptions): AgentClient;
259
307
  /**
260
308
  * Creates an idea decomposition client backed by configured LLM provider(s).
261
309
  * [SPEC: Architecture v2.0 §Phase 4]
@@ -263,4 +311,4 @@ declare function create_provider_agent_client(config: unknown): AgentClient;
263
311
  declare function create_provider_idea_decomposer(config: unknown): IdeaDecomposerClient;
264
312
  declare function create_provider_refinement_client(config: unknown): RefinementClient;
265
313
 
266
- export { type AgentClient, type AgentContract, type AgentContractConstraints, type AgentContractContext, type AgentContractPermissions, type AgentContractRelatedArtifacts, type AgentResponse, type AiProviderName, type CompletionInput, type CompletionResult, type DecomposedTaskDraft, type ExecuteTaskOptions, type FileAccessMode, type IdeaDecomposerClient, type IdeaDecompositionInput, type IdeaRefinementInput, type LlmProvider, type ProviderConfig, type RefinementClient, type RefinementDraft, type RefinementProposalAction, type RefinementTaskProposal, type RunResult, type SandboxRunState, type TaskRefinementInput, buildIdeaRefinementPrompt, buildTaskRefinementPrompt, build_contract, build_decomposition_prompt, constraint_violation_reasons, create_provider, create_provider_agent_client, create_provider_idea_decomposer, create_provider_refinement_client, create_run, decompose_idea_to_tasks, enforce_constraints, execute_task, finalize_run, log_step, parseRefinementDraftResponse, refine_idea_to_draft, refine_task_to_draft, resolve_provider_config, select_agent, validate_command, validate_file_access, write_run };
314
+ export { type AgentClient, type AgentContract, type AgentContractConstraints, type AgentContractContext, type AgentContractPermissions, type AgentContractRelatedArtifacts, type AgentResponse, type AiProviderName, CliAgentClient, CliCompletionProvider, type CliRunner, type CliRunnerInvocation, type CliRunnerResult, type CliRuntimeOptions, type CompletionInput, type CompletionResult, type DecomposedTaskDraft, type ExecuteTaskOptions, type FileAccessMode, type IdeaDecomposerClient, type IdeaDecompositionInput, type IdeaRefinementInput, type LlmProvider, type ProviderConfig, type RefinementClient, type RefinementDraft, type RefinementProposalAction, type RefinementTaskProposal, type RunResult, type SandboxRunState, type TaskRefinementInput, buildIdeaRefinementPrompt, buildTaskRefinementPrompt, build_contract, build_decomposition_prompt, constraint_violation_reasons, create_provider, create_provider_agent_client, create_provider_idea_decomposer, create_provider_refinement_client, create_run, decompose_idea_to_tasks, enforce_constraints, execute_task, finalize_run, log_step, parseRefinementDraftResponse, refine_idea_to_draft, refine_task_to_draft, resolve_provider_config, select_agent, validate_command, validate_file_access, write_run };
package/dist/index.js CHANGED
@@ -870,7 +870,15 @@ var DEFAULT_MODELS = {
870
870
  openai: "gpt-5-mini",
871
871
  anthropic: "claude-3-5-sonnet-latest",
872
872
  gemini: "gemini-2.0-flash",
873
- ollama: "llama3.2"
873
+ ollama: "llama3.2",
874
+ codex_cli: "gpt-5-codex",
875
+ claude_cli: "sonnet",
876
+ gemini_cli: "gemini-2.5-pro"
877
+ };
878
+ var DEFAULT_COMMAND = {
879
+ codex_cli: "codex",
880
+ claude_cli: "claude",
881
+ gemini_cli: "gemini"
874
882
  };
875
883
  var DEFAULT_KEY_ENV = {
876
884
  openai: "OPENAI_API_KEY",
@@ -900,7 +908,7 @@ function asFinite(value) {
900
908
  }
901
909
  function readProvider(value) {
902
910
  const normalized = asString(value)?.toLowerCase();
903
- if (normalized === "openai" || normalized === "anthropic" || normalized === "gemini" || normalized === "ollama" || normalized === "mock") {
911
+ if (normalized === "openai" || normalized === "anthropic" || normalized === "gemini" || normalized === "ollama" || normalized === "mock" || normalized === "codex_cli" || normalized === "claude_cli" || normalized === "gemini_cli") {
904
912
  return normalized;
905
913
  }
906
914
  return "mock";
@@ -921,15 +929,34 @@ function resolve_provider_config(config) {
921
929
  timeout_ms: 6e4
922
930
  };
923
931
  }
932
+ if (provider === "codex_cli" || provider === "claude_cli" || provider === "gemini_cli") {
933
+ const section2 = lookupProviderSection(ai, provider);
934
+ const model2 = asString(section2.model) ?? asString(ai.model) ?? DEFAULT_MODELS[provider];
935
+ const timeout_ms2 = asFinite(section2.timeout_ms) ?? asFinite(ai.timeout_ms) ?? 3e5;
936
+ const command = asString(section2.command) ?? DEFAULT_COMMAND[provider];
937
+ const argsRaw = Array.isArray(section2.args) ? section2.args : Array.isArray(ai.args) ? ai.args : [];
938
+ const args = argsRaw.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
939
+ return {
940
+ provider,
941
+ model: model2,
942
+ command,
943
+ args,
944
+ temperature: 0.2,
945
+ max_output_tokens: 4096,
946
+ timeout_ms: timeout_ms2
947
+ };
948
+ }
924
949
  const section = lookupProviderSection(ai, provider);
925
- const model = asString(section.model) ?? asString(ai.model) ?? DEFAULT_MODELS[provider];
926
- const base_url = asString(section.base_url) ?? asString(ai.base_url) ?? DEFAULT_BASE_URL[provider];
950
+ const cloudProvider = provider;
951
+ const model = asString(section.model) ?? asString(ai.model) ?? DEFAULT_MODELS[cloudProvider];
952
+ const base_url = asString(section.base_url) ?? asString(ai.base_url) ?? DEFAULT_BASE_URL[cloudProvider];
927
953
  const temperature = asFinite(section.temperature) ?? asFinite(ai.temperature) ?? 0.2;
928
954
  const max_output_tokens = asFinite(section.max_output_tokens) ?? asFinite(ai.max_output_tokens) ?? 1024;
929
955
  const timeout_ms = asFinite(section.timeout_ms) ?? asFinite(ai.timeout_ms) ?? 6e4;
930
956
  let api_key;
931
957
  if (provider !== "ollama") {
932
- const envName = asString(section.api_key_env) ?? asString(ai.api_key_env) ?? DEFAULT_KEY_ENV[provider];
958
+ const keyedProvider = provider;
959
+ const envName = asString(section.api_key_env) ?? asString(ai.api_key_env) ?? DEFAULT_KEY_ENV[keyedProvider];
933
960
  const envValue = envName ? asString(process.env[envName]) : null;
934
961
  api_key = asString(section.api_key) ?? asString(ai.api_key) ?? envValue ?? void 0;
935
962
  if (!api_key) {
@@ -949,6 +976,250 @@ function resolve_provider_config(config) {
949
976
  };
950
977
  }
951
978
 
979
+ // src/providers/cli.ts
980
+ import fs2 from "fs";
981
+ import os from "os";
982
+ import path3 from "path";
983
+ import { spawnSync as spawnSync2 } from "child_process";
984
+ function defaultRunner(invocation) {
985
+ const result = spawnSync2(invocation.command, invocation.args, {
986
+ cwd: invocation.cwd,
987
+ encoding: "utf8",
988
+ input: invocation.input,
989
+ timeout: invocation.timeout_ms,
990
+ windowsHide: true
991
+ });
992
+ return {
993
+ status: result.status,
994
+ stdout: result.stdout ?? "",
995
+ stderr: result.stderr ?? "",
996
+ error: result.error
997
+ };
998
+ }
999
+ function asText(result) {
1000
+ return [result.stdout, result.stderr].map((value) => value.trim()).filter(Boolean).join("\n").trim();
1001
+ }
1002
+ function requireSuccess(provider, result) {
1003
+ if ((result.status ?? 1) === 0 && !result.error) {
1004
+ return;
1005
+ }
1006
+ const details = asText(result) || result.error?.message || "unknown error";
1007
+ throw new Error(`${provider} execution failed: ${details}`);
1008
+ }
1009
+ function tempOutputFile(prefix) {
1010
+ return path3.join(os.tmpdir(), `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`);
1011
+ }
1012
+ function combinedPrompt(input) {
1013
+ return [input.system.trim(), input.prompt.trim()].filter(Boolean).join("\n\n");
1014
+ }
1015
+ function completionInvocation(config, input, cwd) {
1016
+ if (config.provider === "codex_cli") {
1017
+ const outputFile = tempOutputFile("coop-codex-complete");
1018
+ return {
1019
+ command: config.command ?? "codex",
1020
+ args: [
1021
+ "exec",
1022
+ "-C",
1023
+ cwd,
1024
+ "--skip-git-repo-check",
1025
+ "--sandbox",
1026
+ "read-only",
1027
+ ...config.model ? ["--model", config.model] : [],
1028
+ "--output-last-message",
1029
+ outputFile,
1030
+ ...config.args ?? [],
1031
+ "-"
1032
+ ],
1033
+ cwd,
1034
+ input: combinedPrompt(input),
1035
+ timeout_ms: config.timeout_ms
1036
+ };
1037
+ }
1038
+ if (config.provider === "claude_cli") {
1039
+ return {
1040
+ command: config.command ?? "claude",
1041
+ args: [
1042
+ "-p",
1043
+ combinedPrompt(input),
1044
+ "--output-format",
1045
+ "text",
1046
+ "--permission-mode",
1047
+ "plan",
1048
+ ...config.model ? ["--model", config.model] : [],
1049
+ ...config.args ?? []
1050
+ ],
1051
+ cwd,
1052
+ timeout_ms: config.timeout_ms
1053
+ };
1054
+ }
1055
+ return {
1056
+ command: config.command ?? "gemini",
1057
+ args: [
1058
+ "-p",
1059
+ combinedPrompt(input),
1060
+ "--output-format",
1061
+ "text",
1062
+ "--approval-mode",
1063
+ "plan",
1064
+ ...config.model ? ["--model", config.model] : [],
1065
+ ...config.args ?? []
1066
+ ],
1067
+ cwd,
1068
+ timeout_ms: config.timeout_ms
1069
+ };
1070
+ }
1071
+ function readCodexOutput(invocation) {
1072
+ const index = invocation.args.indexOf("--output-last-message");
1073
+ const outputFile = index >= 0 ? invocation.args[index + 1] : void 0;
1074
+ if (!outputFile) return null;
1075
+ if (!fs2.existsSync(outputFile)) return null;
1076
+ try {
1077
+ return fs2.readFileSync(outputFile, "utf8").trim();
1078
+ } finally {
1079
+ fs2.rmSync(outputFile, { force: true });
1080
+ }
1081
+ }
1082
+ var CliCompletionProvider = class {
1083
+ name;
1084
+ config;
1085
+ cwd;
1086
+ runner;
1087
+ constructor(config, options = {}) {
1088
+ this.name = config.provider;
1089
+ this.config = config;
1090
+ this.cwd = path3.resolve(options.cwd ?? process.cwd());
1091
+ this.runner = options.runner ?? defaultRunner;
1092
+ }
1093
+ async complete(input) {
1094
+ const invocation = completionInvocation(this.config, input, this.cwd);
1095
+ const result = this.runner(invocation);
1096
+ requireSuccess(this.name, result);
1097
+ const codexOutput = this.config.provider === "codex_cli" ? readCodexOutput(invocation) : null;
1098
+ return {
1099
+ text: codexOutput || result.stdout.trim() || result.stderr.trim()
1100
+ };
1101
+ }
1102
+ };
1103
+ function buildExecutionPrompt(task, contract, step, prompt, mode) {
1104
+ const sections = [
1105
+ mode === "execute" ? "You are executing a COOP task inside the repo. Make the necessary file changes, run allowed commands when needed, and finish with a concise summary." : "You are reviewing a COOP task inside the repo. Inspect the current workspace and return concise review findings only.",
1106
+ `Task: ${task.id} - ${task.title}`,
1107
+ `Goal: ${contract.goal}`,
1108
+ `Step: ${step.step} (${step.action})`,
1109
+ contract.context.acceptance_criteria.length > 0 ? `Acceptance: ${contract.context.acceptance_criteria.join(" | ")}` : "",
1110
+ contract.context.tests_required.length > 0 ? `Tests Required: ${contract.context.tests_required.join(" | ")}` : "",
1111
+ contract.context.authority_refs.length > 0 ? `Authority Refs: ${contract.context.authority_refs.join(" | ")}` : "",
1112
+ contract.context.derived_refs.length > 0 ? `Derived Refs: ${contract.context.derived_refs.join(" | ")}` : "",
1113
+ contract.permissions.read_paths.length > 0 ? `Allowed Read Paths: ${contract.permissions.read_paths.join(", ")}` : "",
1114
+ contract.permissions.write_paths.length > 0 ? `Allowed Write Paths: ${contract.permissions.write_paths.join(", ")}` : "",
1115
+ contract.permissions.allowed_commands.length > 0 ? `Allowed Commands: ${contract.permissions.allowed_commands.join(", ")}` : "",
1116
+ contract.permissions.forbidden_commands.length > 0 ? `Forbidden Commands: ${contract.permissions.forbidden_commands.join(", ")}` : "",
1117
+ prompt
1118
+ ].filter(Boolean);
1119
+ return sections.join("\n\n");
1120
+ }
1121
+ function countGitChanges(repoRoot, runner, timeoutMs) {
1122
+ const result = runner({
1123
+ command: "git",
1124
+ args: ["status", "--porcelain"],
1125
+ cwd: repoRoot,
1126
+ timeout_ms: timeoutMs
1127
+ });
1128
+ if ((result.status ?? 1) !== 0) return 0;
1129
+ return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).length;
1130
+ }
1131
+ function executionInvocation(config, prompt, repoRoot, mode) {
1132
+ if (config.provider === "codex_cli") {
1133
+ const outputFile = tempOutputFile("coop-codex-run");
1134
+ return {
1135
+ command: config.command ?? "codex",
1136
+ args: [
1137
+ "exec",
1138
+ "-C",
1139
+ repoRoot,
1140
+ "--skip-git-repo-check",
1141
+ "--sandbox",
1142
+ mode === "execute" ? "workspace-write" : "read-only",
1143
+ ...mode === "execute" ? ["--full-auto"] : [],
1144
+ ...config.model ? ["--model", config.model] : [],
1145
+ "--output-last-message",
1146
+ outputFile,
1147
+ ...config.args ?? [],
1148
+ "-"
1149
+ ],
1150
+ cwd: repoRoot,
1151
+ input: prompt,
1152
+ timeout_ms: config.timeout_ms
1153
+ };
1154
+ }
1155
+ if (config.provider === "claude_cli") {
1156
+ return {
1157
+ command: config.command ?? "claude",
1158
+ args: [
1159
+ "-p",
1160
+ prompt,
1161
+ "--output-format",
1162
+ "text",
1163
+ "--permission-mode",
1164
+ mode === "execute" ? "bypassPermissions" : "plan",
1165
+ ...config.model ? ["--model", config.model] : [],
1166
+ ...config.args ?? []
1167
+ ],
1168
+ cwd: repoRoot,
1169
+ timeout_ms: config.timeout_ms
1170
+ };
1171
+ }
1172
+ return {
1173
+ command: config.command ?? "gemini",
1174
+ args: [
1175
+ "-p",
1176
+ prompt,
1177
+ "--output-format",
1178
+ "text",
1179
+ "--approval-mode",
1180
+ mode === "execute" ? "yolo" : "plan",
1181
+ ...config.model ? ["--model", config.model] : [],
1182
+ ...config.args ?? []
1183
+ ],
1184
+ cwd: repoRoot,
1185
+ timeout_ms: config.timeout_ms
1186
+ };
1187
+ }
1188
+ var CliAgentClient = class {
1189
+ config;
1190
+ repoRoot;
1191
+ runner;
1192
+ constructor(config, options = {}) {
1193
+ this.config = config;
1194
+ this.repoRoot = path3.resolve(options.cwd ?? process.cwd());
1195
+ this.runner = options.runner ?? defaultRunner;
1196
+ }
1197
+ async generate(input) {
1198
+ const before = countGitChanges(this.repoRoot, this.runner, this.config.timeout_ms);
1199
+ const prompt = buildExecutionPrompt(input.task, input.contract, input.step, input.prompt, "execute");
1200
+ const invocation = executionInvocation(this.config, prompt, this.repoRoot, "execute");
1201
+ const result = this.runner(invocation);
1202
+ requireSuccess(this.config.provider, result);
1203
+ const after = countGitChanges(this.repoRoot, this.runner, this.config.timeout_ms);
1204
+ const codexOutput = this.config.provider === "codex_cli" ? readCodexOutput(invocation) : null;
1205
+ return {
1206
+ summary: codexOutput || result.stdout.trim() || result.stderr.trim() || `Completed ${input.step.step}.`,
1207
+ file_changes: Math.max(0, after - before)
1208
+ };
1209
+ }
1210
+ async review(input) {
1211
+ const prompt = buildExecutionPrompt(input.task, input.contract, input.step, input.prompt, "review");
1212
+ const invocation = executionInvocation(this.config, prompt, this.repoRoot, "review");
1213
+ const result = this.runner(invocation);
1214
+ requireSuccess(this.config.provider, result);
1215
+ const codexOutput = this.config.provider === "codex_cli" ? readCodexOutput(invocation) : null;
1216
+ return {
1217
+ summary: codexOutput || result.stdout.trim() || result.stderr.trim() || `Reviewed ${input.step.step}.`,
1218
+ file_changes: 0
1219
+ };
1220
+ }
1221
+ };
1222
+
952
1223
  // src/providers/http.ts
953
1224
  async function post_json(url, init) {
954
1225
  const controller = new AbortController();
@@ -1145,6 +1416,10 @@ function create_provider(config) {
1145
1416
  return new GeminiProvider(resolved);
1146
1417
  case "ollama":
1147
1418
  return new OllamaProvider(resolved);
1419
+ case "codex_cli":
1420
+ case "claude_cli":
1421
+ case "gemini_cli":
1422
+ return new CliCompletionProvider(resolved);
1148
1423
  case "mock":
1149
1424
  default:
1150
1425
  return new MockProvider();
@@ -1208,7 +1483,11 @@ function asAgentResponse(text, tokens) {
1208
1483
  tokens_used: tokens
1209
1484
  };
1210
1485
  }
1211
- function create_provider_agent_client(config) {
1486
+ function create_provider_agent_client(config, runtime = {}) {
1487
+ const resolved = resolve_provider_config(config);
1488
+ if (resolved.provider === "codex_cli" || resolved.provider === "claude_cli" || resolved.provider === "gemini_cli") {
1489
+ return new CliAgentClient(resolved, runtime);
1490
+ }
1212
1491
  const provider = create_provider(config);
1213
1492
  return {
1214
1493
  async generate(input) {
@@ -1263,6 +1542,8 @@ function create_provider_refinement_client(config) {
1263
1542
  };
1264
1543
  }
1265
1544
  export {
1545
+ CliAgentClient,
1546
+ CliCompletionProvider,
1266
1547
  buildIdeaRefinementPrompt,
1267
1548
  buildTaskRefinementPrompt,
1268
1549
  build_contract,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitsy/coop-ai",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "types": "./dist/index.d.ts",
@@ -17,7 +17,7 @@
17
17
  "LICENSE"
18
18
  ],
19
19
  "dependencies": {
20
- "@kitsy/coop-core": "2.1.0"
20
+ "@kitsy/coop-core": "2.1.2"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/node": "^24.12.0",