genaicode 2.1.0 → 2.4.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.
Files changed (95) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +150 -35
  3. package/dist/agents/cli-agent.d.ts +28 -0
  4. package/dist/agents/cli-agent.js +57 -0
  5. package/dist/agents/cli-agent.js.map +1 -0
  6. package/dist/agents/discovery.d.ts +10 -0
  7. package/dist/agents/discovery.js +31 -0
  8. package/dist/agents/discovery.js.map +1 -0
  9. package/dist/agents/drivers/claude.d.ts +25 -0
  10. package/dist/agents/drivers/claude.js +184 -0
  11. package/dist/agents/drivers/claude.js.map +1 -0
  12. package/dist/agents/drivers/codex-live.d.ts +17 -0
  13. package/dist/agents/drivers/codex-live.js +182 -0
  14. package/dist/agents/drivers/codex-live.js.map +1 -0
  15. package/dist/agents/drivers/codex.d.ts +27 -0
  16. package/dist/agents/drivers/codex.js +176 -0
  17. package/dist/agents/drivers/codex.js.map +1 -0
  18. package/dist/agents/drivers/copilot.d.ts +27 -0
  19. package/dist/agents/drivers/copilot.js +199 -0
  20. package/dist/agents/drivers/copilot.js.map +1 -0
  21. package/dist/agents/drivers/cursor.d.ts +23 -0
  22. package/dist/agents/drivers/cursor.js +174 -0
  23. package/dist/agents/drivers/cursor.js.map +1 -0
  24. package/dist/agents/drivers/gemini.d.ts +18 -0
  25. package/dist/agents/drivers/gemini.js +118 -0
  26. package/dist/agents/drivers/gemini.js.map +1 -0
  27. package/dist/agents/drivers/json.d.ts +6 -0
  28. package/dist/agents/drivers/json.js +20 -0
  29. package/dist/agents/drivers/json.js.map +1 -0
  30. package/dist/agents/drivers/muse-live.d.ts +17 -0
  31. package/dist/agents/drivers/muse-live.js +112 -0
  32. package/dist/agents/drivers/muse-live.js.map +1 -0
  33. package/dist/agents/drivers/muse.d.ts +12 -0
  34. package/dist/agents/drivers/muse.js +87 -0
  35. package/dist/agents/drivers/muse.js.map +1 -0
  36. package/dist/agents/drivers/opencode.d.ts +17 -0
  37. package/dist/agents/drivers/opencode.js +103 -0
  38. package/dist/agents/drivers/opencode.js.map +1 -0
  39. package/dist/agents/env.d.ts +16 -0
  40. package/dist/agents/env.js +46 -0
  41. package/dist/agents/env.js.map +1 -0
  42. package/dist/agents/event-queue.d.ts +44 -0
  43. package/dist/agents/event-queue.js +123 -0
  44. package/dist/agents/event-queue.js.map +1 -0
  45. package/dist/agents/hosted.d.ts +33 -0
  46. package/dist/agents/hosted.js +129 -0
  47. package/dist/agents/hosted.js.map +1 -0
  48. package/dist/agents/live-agent.d.ts +37 -0
  49. package/dist/agents/live-agent.js +142 -0
  50. package/dist/agents/live-agent.js.map +1 -0
  51. package/dist/agents/prepare.d.ts +33 -0
  52. package/dist/agents/prepare.js +52 -0
  53. package/dist/agents/prepare.js.map +1 -0
  54. package/dist/agents/process.d.ts +39 -0
  55. package/dist/agents/process.js +140 -0
  56. package/dist/agents/process.js.map +1 -0
  57. package/dist/agents/rpc.d.ts +29 -0
  58. package/dist/agents/rpc.js +101 -0
  59. package/dist/agents/rpc.js.map +1 -0
  60. package/dist/agents/runtime.d.ts +26 -0
  61. package/dist/agents/runtime.js +97 -0
  62. package/dist/agents/runtime.js.map +1 -0
  63. package/dist/agents/types.d.ts +158 -0
  64. package/dist/agents/types.js +2 -0
  65. package/dist/agents/types.js.map +1 -0
  66. package/dist/agents/verify.d.ts +36 -0
  67. package/dist/agents/verify.js +43 -0
  68. package/dist/agents/verify.js.map +1 -0
  69. package/dist/agents.d.ts +36 -0
  70. package/dist/agents.js +19 -0
  71. package/dist/agents.js.map +1 -0
  72. package/dist/core/client.d.ts +7 -1
  73. package/dist/core/client.js +18 -3
  74. package/dist/core/client.js.map +1 -1
  75. package/dist/core/middleware.js +2 -0
  76. package/dist/core/middleware.js.map +1 -1
  77. package/dist/core/types.d.ts +51 -0
  78. package/dist/providers/anthropic-converter.js +32 -6
  79. package/dist/providers/anthropic-converter.js.map +1 -1
  80. package/dist/providers/anthropic.js +3 -1
  81. package/dist/providers/anthropic.js.map +1 -1
  82. package/dist/providers/google-converter.js +70 -3
  83. package/dist/providers/google-converter.js.map +1 -1
  84. package/dist/providers/google.d.ts +8 -0
  85. package/dist/providers/google.js +3 -0
  86. package/dist/providers/google.js.map +1 -1
  87. package/dist/providers/openai-converter.d.ts +2 -0
  88. package/dist/providers/openai-converter.js +16 -0
  89. package/dist/providers/openai-converter.js.map +1 -1
  90. package/dist/providers/openai.js +1 -0
  91. package/dist/providers/openai.js.map +1 -1
  92. package/docs/agents.md +269 -0
  93. package/docs/pivot.md +8 -1
  94. package/docs/semver.md +6 -0
  95. package/package.json +7 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ### Added
6
+
7
+ - `genaicode/agents` subpath (experimental): run installed coding-agent CLIs behind one
8
+ `CodingAgent` interface. Drivers `claude()`, `codex()`, `muse()`; agent-neutral
9
+ `AgentEvent` stream and `AgentResult`; abort, timeout and process-group kill;
10
+ `detectAgents` / `findExecutable`; `cliAgent()` for custom drivers. See
11
+ [docs/agents.md](docs/agents.md).
12
+ - Live agent sessions over JSON-RPC: `codexLive()` (`codex app-server`) and `museLive()`
13
+ (`muse serve`) with `AgentRun.steer()` and `AgentTask.onApproval`; `liveAgent()` and
14
+ `RpcPeer` for custom live drivers; `approval-resolved` event.
15
+ - `AgentTask.mcpServers` (HTTP and stdio) for `claude`, `codex` and `codexLive`; header
16
+ secrets stay out of argv. Drivers can set up and clean up per task via `prepare`.
17
+ - `scrubEnv`, `withoutProviderCredentials` and `PROVIDER_CREDENTIAL_VARS` for child
18
+ environments; `runWithVerify` verify-and-repair loop.
19
+ - Drivers `gemini()`, `cursor()` and `opencode()`; `hostedAgent()` adapts a
20
+ `HostedAgentProvider` (start / poll / send / cancel) to `CodingAgent`.
21
+ - Driver `copilot()` for GitHub Copilot CLI (`--output-format json`), with MCP servers via
22
+ `--additional-mcp-config` and `allowTools` / `denyTools` permission patterns.
23
+
24
+ ### Notes
25
+
26
+ - The core `genaicode` entry is unchanged and still spawns nothing.
27
+
28
+ ## 2.2.0 — 2026-07-26
29
+
30
+ ### Added
31
+
32
+ - Portable `responseFormat` on `GenerationRequest` / request builders
33
+ (`text` | `json` | `json_schema`), mapped by OpenAI and Google adapters
34
+ - Portable `thinking` controls (`false` | `{ budgetTokens?, level? }`), mapped by
35
+ Anthropic (budget / disable) and Google (budget or level)
36
+ - `.json()` sets `responseFormat: { type: 'json' }` when no format was already chosen
37
+ - Capability flags: `jsonResponse`, `thinking`
38
+
39
+ ### Notes
40
+
41
+ - Additive only; provider `generationConfig` / Anthropic `thinking` factory options still
42
+ work as escape hatches for vendor-only knobs.
43
+ - On Google, `thinking: false` and `budgetTokens: 0` map to `thinkingLevel: MINIMAL`
44
+ (Gemini 3 rejects `thinkingBudget: 0`). JSON `responseFormat` also defaults Google
45
+ thinking to `MINIMAL` when unset, so small token budgets are not spent only on thoughts.
46
+ - Provider E2E covers JSON response format and thinking knobs when credentials are set.
47
+
3
48
  ## 2.1.0 — 2026-07-25
4
49
 
5
50
  Publishes the Phase 3–4 work already on `master`. npm `2.0.0` shipped the 2.0
package/README.md CHANGED
@@ -12,38 +12,44 @@ GenAIcode is a small TypeScript toolkit for using LLMs in backend code.
12
12
 
13
13
  It sits between raw provider SDKs and full agent frameworks: one prompt representation,
14
14
  thin provider adapters, a convenient request API, and lightweight conversation chains.
15
- It does not inspect repositories, execute shell commands, edit files, or run an agent UI.
15
+ The core does not inspect repositories, execute shell commands, edit files, or run an agent UI.
16
+ The opt-in [`genaicode/agents`](#coding-agents) subpath drives the coding-agent CLIs you
17
+ already have (Claude Code, Codex, GitHub Copilot CLI, Cursor, Gemini CLI, opencode, Muse)
18
+ behind one task and event API, so you can orchestrate them from backend code.
16
19
 
17
- ## GenAIcode 2.0 is a new direction
20
+ **Like jQuery**, the common case starts with one small function and becomes more specific
21
+ through chaining—configure a request, follow up across multiple prompts, and keep history
22
+ portable without adopting a full agent framework.
18
23
 
19
- GenAIcode 1.x was a coding agent. Version 2.0 deliberately replaces that product with a
20
- small backend LLM toolkit: a jQuery-like layer for portable prompts, provider adapters,
21
- conversation chains, and plugins.
24
+ GenAIcode 1.x was a coding agent; see [Migration from 1.x](#migration-from-1x) if you need
25
+ the old product or the 2.0 scope decisions.
22
26
 
23
- The coding-agent CLI, browser UI, repository tools, shell execution, and agent
24
- orchestration are not deprecated compatibility features; they have been removed from
25
- 2.0. The GenAIcode name, `PromptItem` model, provider converters, and extensibility
26
- continue here in a smaller and more focused form.
27
-
28
- The original coding agent remains available from the preserved
29
- [`1.x` branch](https://github.com/gtanczyk/genaicode/tree/1.x) and the 1.x npm releases:
27
+ ## Install
30
28
 
31
29
  ```bash
32
- npx genaicode@1
30
+ npm install genaicode
33
31
  ```
34
32
 
35
- Running `npx genaicode` with version 2.x prints this migration guidance instead of
36
- starting the old agent.
33
+ Node.js 20 or newer is required.
37
34
 
38
- See [the pivot plan](docs/pivot.md) for the full scope and migration decisions.
35
+ ## Why this vs a raw SDK
39
36
 
40
- ## Install
37
+ Provider SDKs are the right tool when you call one vendor and own every request shape
38
+ yourself. The cost shows up when the same backend needs a second provider, a test double,
39
+ or shared policy (timeouts, JSON parsing, rate limits, fallback) without copying glue.
41
40
 
42
- ```bash
43
- npm install genaicode
44
- ```
41
+ GenAIcode is that thin shared layer:
45
42
 
46
- Node.js 20 or newer is required.
43
+ - One portable prompt/tool IR (`PromptItem`) instead of OpenAI messages vs Anthropic
44
+ blocks vs Gemini contents.
45
+ - One request API (`.text()`, `.json()`, `.stream()`, chains) over those adapters.
46
+ - A tiny `ModelProvider` seam so tests and custom gateways do not mock vendor HTTP.
47
+ - Opt-in middleware and retry helpers—no hidden tool runners, no silent retries, no
48
+ agent loop.
49
+
50
+ If you are happy importing one SDK and never swapping models or providers, stay on the
51
+ SDK. If you want the call site to look the same while the edge stays replaceable, use
52
+ GenAIcode.
47
53
 
48
54
  ## A prompt in three lines
49
55
 
@@ -55,8 +61,9 @@ const ai = genaicode(openai({ model: 'your-model-name' }));
55
61
  const answer = await ai('Explain why the sky is blue in two sentences.').text();
56
62
  ```
57
63
 
58
- The client is callable on purpose. Like jQuery, the common case starts with one small
59
- function and becomes more specific through chaining:
64
+ The client is callable on purpose. Configuration is ordinary method chaining—system
65
+ instructions, temperature, token limits, and more—without a separate options object or
66
+ framework setup:
60
67
 
61
68
  ```ts
62
69
  const result = await ai('Create a release note from these commits')
@@ -66,12 +73,68 @@ const result = await ai('Create a release note from these commits')
66
73
  .text();
67
74
  ```
68
75
 
69
- Builders are immutable, so a configured base request can be safely reused.
76
+ Builders are immutable, so a configured base request can be safely reused:
77
+
78
+ ```ts
79
+ const releaseNote = ai('Create a release note from these commits')
80
+ .system('You are a concise technical writer.')
81
+ .temperature(0.2);
82
+
83
+ const short = await releaseNote.maxOutputTokens(200).text();
84
+ const long = await releaseNote.maxOutputTokens(800).text();
85
+ ```
86
+
87
+ ## Coding agents
88
+
89
+ `genaicode/agents` turns the coding-agent CLIs installed on a machine into one API. Give
90
+ any of them a task (a prompt and a working directory) and get back the same event stream
91
+ and result, whichever agent does the work. It is how you script, compare, or chain coding
92
+ agents from a job, a bot, or a CI step without writing a parser per vendor.
93
+
94
+ ```ts
95
+ import { claude, codex, copilot, cursor, detectAgents } from 'genaicode/agents';
96
+
97
+ const [found] = detectAgents([claude(), codex(), copilot(), cursor()]).filter(({ path }) => path);
98
+ if (!found) throw new Error('No coding agent installed.');
99
+
100
+ const run = found.agent.run({ prompt: 'Add a unit test for parseDate', cwd: '/path/to/repo', timeoutMs: 20 * 60_000 });
101
+ for await (const event of run) {
102
+ if (event.type === 'tool-start') console.log('⚙', event.name);
103
+ if (event.type === 'file-change') console.log('edited', event.paths.join(', '));
104
+ }
105
+ const result = await run.result; // { status, ok, text, sessionId, usage, error, ... }
106
+ ```
107
+
108
+ | Agent | Driver | Extras |
109
+ | ---------------- | ----------------------------- | ------------------------------------------ |
110
+ | Claude Code | `claude()` | MCP servers, max turns, effort, cost |
111
+ | Codex | `codex()`, `codexLive()` | MCP servers; live: `steer()`, approvals |
112
+ | GitHub Copilot | `copilot()` | MCP servers, allow/deny tool patterns |
113
+ | Cursor | `cursor()` | streamed text deltas |
114
+ | Gemini CLI | `gemini()` | approval mode |
115
+ | opencode | `opencode()` | `provider/model` ids, variants |
116
+ | Muse | `muse()`, `museLive()` | max turns; live: `steer()` |
117
+ | Your own CLI/API | `cliAgent()`, `hostedAgent()` | plug in any JSON-lines CLI or hosted agent |
118
+
119
+ On top of the drivers:
120
+
121
+ - **Steer a running task** with `run.steer(text)`, and answer permission prompts with
122
+ `onApproval` (live drivers).
123
+ - **Attach MCP servers per task** with `mcpServers`. Header secrets stay out of argv.
124
+ - **Verify and repair**: `runWithVerify` runs your check (tests, lint) after the agent and
125
+ sends failures back for another attempt.
126
+ - **Keep credentials apart**: `withoutProviderCredentials()` strips your app's LLM API keys
127
+ from the agent's environment, so it bills its own login.
128
+
129
+ Importing `genaicode` never spawns anything; only `genaicode/agents` does. The agent uses
130
+ its own credentials, billing, and permission settings. GenAIcode does not sandbox it,
131
+ choose a model, or retry it. See [docs/agents.md](docs/agents.md) for events, options per
132
+ driver, and writing your own driver.
70
133
 
71
134
  ## Chaining prompts
72
135
 
73
136
  A chain remembers successful user and assistant turns. Each new prompt sees the complete
74
- history:
137
+ history, so multi-step work stays in ordinary application code:
75
138
 
76
139
  ```ts
77
140
  import { system } from 'genaicode';
@@ -145,6 +208,37 @@ const value = await ai('Return {"count": 3}.').json({
145
208
  });
146
209
  ```
147
210
 
211
+ Calling `.json()` also sets `responseFormat: { type: 'json' }` on the request when you have
212
+ not already chosen a format, so providers that support JSON mode (OpenAI, Gemini/Vertex)
213
+ are asked for JSON rather than free text.
214
+
215
+ ## Response format and thinking
216
+
217
+ Portable request fields cover the two knobs backends usually poke through provider-specific
218
+ config:
219
+
220
+ ```ts
221
+ const verdict = await ai(promptText)
222
+ .responseFormat({ type: 'json' })
223
+ .thinking({ level: 'minimal' }) // or { budgetTokens: 0 } / false to disable
224
+ .temperature(0)
225
+ .json((value) => VerdictSchema.parse(value));
226
+ ```
227
+
228
+ - `responseFormat`: `{ type: 'text' | 'json' }` or
229
+ `{ type: 'json_schema', name, schema, strict? }`.
230
+ - `thinking`: `false` to disable, or `{ budgetTokens?, level? }` (`minimal` |
231
+ `low` | `medium` | `high`). Prefer one of budget or level — some providers reject both.
232
+ On Google/Gemini, `false` and `budgetTokens: 0` map to `thinkingLevel: MINIMAL`
233
+ (or `LOW` for models like Gemini 3.7 and Gemini 3 Pro where `MINIMAL` is unsupported)
234
+ because Gemini 3 rejects `thinkingBudget: 0`. JSON `responseFormat` without an
235
+ explicit `thinking` setting or provider `generationConfig.thinkingConfig` default also
236
+ defaults Google thinking to `MINIMAL` (or `LOW` on Gemini 3.7 / Pro).
237
+
238
+ Providers map what they support and ignore the rest. `ProviderCapabilities.jsonResponse`
239
+ and `ProviderCapabilities.thinking` advertise support. Vendor-specific escapes such as
240
+ Vertex `generationConfig` remain available for anything not covered here.
241
+
148
242
  ## PromptItem: the portable prompt IR
149
243
 
150
244
  `PromptItem` is GenAIcode's provider-neutral intermediate representation:
@@ -226,13 +320,7 @@ Hook-style plugins remain the extension point. Built-in helpers cover common bac
226
320
  needs without restoring a global registry:
227
321
 
228
322
  ```ts
229
- import {
230
- cachePlugin,
231
- fallbackPlugin,
232
- genaicode,
233
- rateLimitPlugin,
234
- timingPlugin,
235
- } from 'genaicode';
323
+ import { cachePlugin, fallbackPlugin, genaicode, rateLimitPlugin, timingPlugin } from 'genaicode';
236
324
  import { anthropic, openai } from 'genaicode/providers';
237
325
 
238
326
  const ai = genaicode(openai({ model: 'your-model-name' }), {
@@ -341,6 +429,9 @@ are set:
341
429
  - Anthropic: `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL`
342
430
  - Gemini: `GEMINI_API_KEY`, `GEMINI_MODEL`
343
431
 
432
+ Beyond the smoke call, E2E also covers portable `responseFormat: { type: 'json' }`
433
+ (OpenAI, Gemini) and `thinking` (Anthropic disable; Gemini disable / `level: 'minimal'`).
434
+
344
435
  CI/CD is configured in `.github/workflows/provider-e2e.yaml`. Each provider runs in its own
345
436
  job and only starts when both required secrets are configured in GitHub Actions.
346
437
 
@@ -397,13 +488,37 @@ Framework-shaped examples live under `examples/` (`http-handler`, `queue-worker`
397
488
 
398
489
  ## Design boundaries
399
490
 
400
- - Backend library, not a coding agent.
491
+ - Backend library, not a coding agent. `genaicode/agents` drives external agent CLIs; it
492
+ adds no agent loop, prompts, or repository tools of its own.
401
493
  - Provider-neutral core with no global configuration.
402
494
  - Explicit models and credentials; environment variables are only provider defaults.
403
495
  - Immutable request builders.
404
496
  - Conversation history is explicit; loops remain ordinary application code.
405
497
  - No hidden tool execution or hidden retries.
406
498
  - Third-party providers and middleware use stable TypeScript contracts.
407
- - Provider SDKs stay behind the `genaicode/providers` subpath.
499
+ - Provider SDKs stay behind the `genaicode/providers` subpath; process spawning stays
500
+ behind `genaicode/agents`.
501
+
502
+ ## Migration from 1.x
503
+
504
+ GenAIcode 1.x was a coding agent. Version 2.0 deliberately replaces that product with a
505
+ small backend LLM toolkit: a jQuery-like layer for portable prompts, provider adapters,
506
+ conversation chains, and plugins.
507
+
508
+ The coding-agent CLI, browser UI, repository tools, shell execution, and agent
509
+ orchestration are not deprecated compatibility features; they have been removed from
510
+ 2.0. The GenAIcode name, `PromptItem` model, provider converters, and extensibility
511
+ continue here in a smaller and more focused form.
512
+
513
+ The original coding agent remains available from the preserved
514
+ [`1.x` branch](https://github.com/gtanczyk/genaicode/tree/1.x) and the 1.x npm releases:
515
+
516
+ ```bash
517
+ npx genaicode@1
518
+ ```
519
+
520
+ Running `npx genaicode` with version 2.x prints this migration guidance instead of
521
+ starting the old agent.
408
522
 
409
- See [the pivot plan](docs/pivot.md) for product scope, migration decisions, and the roadmap.
523
+ See [the pivot plan](docs/pivot.md) for the full scope, migration decisions, and the
524
+ roadmap.
@@ -0,0 +1,28 @@
1
+ import { type PreparedRun } from './prepare.js';
2
+ import { type AgentOutcome } from './runtime.js';
3
+ import type { AgentCapabilities, AgentEvent, AgentTask, CodingAgent } from './types.js';
4
+ export type { AgentOutcome } from './runtime.js';
5
+ /** Per-run decoder for one vendor's JSON event stream. */
6
+ export interface AgentOutputParser {
7
+ /** Map one parsed stdout JSON value to zero or more events. */
8
+ event(value: unknown): AgentEvent[];
9
+ /** The terminal verdict the agent itself reported, if it reported one. */
10
+ outcome?(): AgentOutcome | undefined;
11
+ }
12
+ export interface CliAgentDefinition {
13
+ name: string;
14
+ command: string;
15
+ capabilities?: AgentCapabilities;
16
+ /** Full argument list for a task, prompt included. */
17
+ args(task: AgentTask): string[];
18
+ /** Replaces `args` when a task needs setup: extra env, temp files to clean up. */
19
+ prepare?(task: AgentTask): PreparedRun;
20
+ createParser(): AgentOutputParser;
21
+ /** Adjust the child environment (the caller's `task.env` or `process.env`). */
22
+ env?(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
23
+ }
24
+ /**
25
+ * Build a `CodingAgent` from a headless CLI: spawn, decode its JSON lines, and
26
+ * fold them into an `AgentResult`. Drivers such as `claude()` are thin definitions.
27
+ */
28
+ export declare function cliAgent(definition: CliAgentDefinition): CodingAgent;
@@ -0,0 +1,57 @@
1
+ import { planSpawn } from './prepare.js';
2
+ import { startProcess } from './process.js';
3
+ import { invalidCwd, RunRecorder } from './runtime.js';
4
+ /**
5
+ * Build a `CodingAgent` from a headless CLI: spawn, decode its JSON lines, and
6
+ * fold them into an `AgentResult`. Drivers such as `claude()` are thin definitions.
7
+ */
8
+ export function cliAgent(definition) {
9
+ return {
10
+ name: definition.name,
11
+ command: definition.command,
12
+ capabilities: definition.capabilities ?? {},
13
+ run: (task) => runCliAgent(definition, task),
14
+ };
15
+ }
16
+ function runCliAgent(definition, task) {
17
+ const recorder = new RunRecorder(definition.name, definition.command);
18
+ const iterate = () => recorder.events.iterate();
19
+ const plan = planSpawn(definition, task, invalidCwd(task.cwd));
20
+ if (!plan.ok) {
21
+ const result = recorder.finish({ exitCode: null, signal: null, reason: 'spawn-error', error: new Error(plan.error) }, undefined);
22
+ return { result: Promise.resolve(result), abort() { }, [Symbol.asyncIterator]: iterate };
23
+ }
24
+ const parser = definition.createParser();
25
+ const handle = startProcess({
26
+ command: definition.command,
27
+ args: plan.args,
28
+ cwd: task.cwd,
29
+ env: plan.env,
30
+ timeoutMs: task.timeoutMs,
31
+ signal: task.signal,
32
+ onLine(line) {
33
+ let value;
34
+ try {
35
+ value = JSON.parse(line);
36
+ }
37
+ catch {
38
+ recorder.emit({ type: 'raw', line });
39
+ return;
40
+ }
41
+ for (const event of parser.event(value))
42
+ recorder.emit(event);
43
+ },
44
+ onStderr: (text) => recorder.emit({ type: 'stderr', text }),
45
+ });
46
+ // A consumer that falls behind pauses the agent instead of buffering its output without bound.
47
+ recorder.events.onPressure = (paused) => (paused ? handle.pause() : handle.resume());
48
+ return {
49
+ result: handle.exit.then((exit) => {
50
+ plan.cleanup();
51
+ return recorder.finish(exit, parser.outcome?.());
52
+ }),
53
+ abort: handle.kill,
54
+ [Symbol.asyncIterator]: iterate,
55
+ };
56
+ }
57
+ //# sourceMappingURL=cli-agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-agent.js","sourceRoot":"","sources":["../../src/agents/cli-agent.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { CodingAgent } from './types.js';
2
+ /** Resolve an executable the way a shell would: absolute path, or the first match on PATH. */
3
+ export declare function findExecutable(command: string, env?: NodeJS.ProcessEnv): string | null;
4
+ export interface AgentAvailability {
5
+ agent: CodingAgent;
6
+ /** Resolved executable, or null when the agent's command is not installed. */
7
+ path: string | null;
8
+ }
9
+ /** Report which agents have their CLI installed. Presence only; login and flags are the agent's concern. */
10
+ export declare function detectAgents(agents: readonly CodingAgent[], env?: NodeJS.ProcessEnv): AgentAvailability[];
@@ -0,0 +1,31 @@
1
+ import { accessSync, constants, statSync } from 'node:fs';
2
+ import { delimiter, isAbsolute, join } from 'node:path';
3
+ /** Resolve an executable the way a shell would: absolute path, or the first match on PATH. */
4
+ export function findExecutable(command, env = process.env) {
5
+ const windows = process.platform === 'win32';
6
+ const extensions = windows ? ['', ...(env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM').split(';')] : [''];
7
+ const candidates = isAbsolute(command)
8
+ ? extensions.map((extension) => command + extension)
9
+ : (env.PATH ?? env.Path ?? '')
10
+ .split(delimiter)
11
+ .filter(Boolean)
12
+ .flatMap((dir) => extensions.map((extension) => join(dir, command + extension)));
13
+ for (const candidate of candidates) {
14
+ try {
15
+ if (!statSync(candidate).isFile())
16
+ continue;
17
+ if (!windows)
18
+ accessSync(candidate, constants.X_OK);
19
+ return candidate;
20
+ }
21
+ catch {
22
+ continue;
23
+ }
24
+ }
25
+ return null;
26
+ }
27
+ /** Report which agents have their CLI installed. Presence only; login and flags are the agent's concern. */
28
+ export function detectAgents(agents, env = process.env) {
29
+ return agents.map((agent) => ({ agent, path: findExecutable(agent.command, env) }));
30
+ }
31
+ //# sourceMappingURL=discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../src/agents/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGxD,8FAA8F;AAC9F,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE,MAAyB,OAAO,CAAC,GAAG;IAClF,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC;IAC7C,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,GAAG,CAAC,OAAO,IAAI,qBAAqB,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC/F,MAAM,UAAU,GAAG,UAAU,CAAC,OAAO,CAAC;QACpC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,OAAO,GAAG,SAAS,CAAC;QACpD,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;aACzB,KAAK,CAAC,SAAS,CAAC;aAChB,MAAM,CAAC,OAAO,CAAC;aACf,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;IACvF,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,CAAC;YACH,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE;gBAAE,SAAS;YAC5C,IAAI,CAAC,OAAO;gBAAE,UAAU,CAAC,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;YACpD,OAAO,SAAS,CAAC;QACnB,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAQD,4GAA4G;AAC5G,MAAM,UAAU,YAAY,CAC1B,MAA8B,EAC9B,MAAyB,OAAO,CAAC,GAAG;IAEpC,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,cAAc,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;AACtF,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { type AgentOutputParser } from '../cli-agent.js';
2
+ import type { AgentTask, CodingAgent, McpServer } from '../types.js';
3
+ export type ClaudePermissionMode = 'acceptEdits' | 'auto' | 'bypassPermissions' | 'dontAsk' | 'manual' | 'plan' | (string & Record<never, never>);
4
+ export interface ClaudeAgentOptions {
5
+ /** Executable name or path. Default `claude`. */
6
+ command?: string;
7
+ /** Default `acceptEdits`: file edits run unattended, other tools follow Claude Code settings. */
8
+ permissionMode?: ClaudePermissionMode;
9
+ /** Passed as `--allowedTools`. */
10
+ allowedTools?: readonly string[];
11
+ /** Passed as `--disallowedTools`. */
12
+ disallowedTools?: readonly string[];
13
+ /** With `task.mcpServers`: ignore MCP servers from the user's own Claude Code config. Default false. */
14
+ strictMcpConfig?: boolean;
15
+ /** With `task.mcpServers`: pre-allow every tool of those servers. Default true (headless runs cannot ask). */
16
+ allowMcpTools?: boolean;
17
+ }
18
+ /** Claude Code in print mode (`claude -p --output-format stream-json`). */
19
+ export declare function claude(options?: ClaudeAgentOptions): CodingAgent;
20
+ /** The `--mcp-config` document for a set of servers. */
21
+ export declare function claudeMcpConfig(servers: readonly McpServer[]): {
22
+ mcpServers: Record<string, unknown>;
23
+ };
24
+ export declare function claudeArgs(task: AgentTask, options?: ClaudeAgentOptions, mcpConfigPath?: string): string[];
25
+ export declare function createClaudeParser(): AgentOutputParser;
@@ -0,0 +1,184 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { cliAgent } from '../cli-agent.js';
5
+ import { isHttpServer } from '../prepare.js';
6
+ import { isObject, numberField, positionalPrompt, stringField } from './json.js';
7
+ const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
8
+ /** Claude Code in print mode (`claude -p --output-format stream-json`). */
9
+ export function claude(options = {}) {
10
+ return cliAgent({
11
+ name: 'claude',
12
+ command: options.command ?? 'claude',
13
+ capabilities: { effort: ['low', 'medium', 'high', 'xhigh', 'max'], maxTurns: true, usage: true, mcp: true },
14
+ args: (task) => claudeArgs(task, options),
15
+ prepare: (task) => prepareClaude(task, options),
16
+ createParser: createClaudeParser,
17
+ });
18
+ }
19
+ /** Write the task's MCP servers to a private temp file, so header secrets stay out of argv. */
20
+ function prepareClaude(task, options) {
21
+ if (!task.mcpServers?.length)
22
+ return { args: claudeArgs(task, options) };
23
+ const dir = mkdtempSync(join(tmpdir(), 'genaicode-claude-mcp-'));
24
+ const file = join(dir, 'mcp.json');
25
+ writeFileSync(file, JSON.stringify(claudeMcpConfig(task.mcpServers)), { mode: 0o600 });
26
+ return {
27
+ args: claudeArgs(task, options, file),
28
+ cleanup: () => rmSync(dir, { recursive: true, force: true }),
29
+ };
30
+ }
31
+ /** The `--mcp-config` document for a set of servers. */
32
+ export function claudeMcpConfig(servers) {
33
+ const mcpServers = {};
34
+ for (const server of servers) {
35
+ mcpServers[server.name] = isHttpServer(server)
36
+ ? { type: 'http', url: server.url, ...(server.headers ? { headers: server.headers } : {}) }
37
+ : {
38
+ command: server.command,
39
+ args: [...(server.args ?? [])],
40
+ ...(server.env ? { env: server.env } : {}),
41
+ };
42
+ }
43
+ return { mcpServers };
44
+ }
45
+ export function claudeArgs(task, options = {}, mcpConfigPath) {
46
+ const args = ['-p', '--verbose'];
47
+ // Multi-value flags come first: a single-value flag must follow them before the prompt.
48
+ if (mcpConfigPath)
49
+ args.push('--mcp-config', mcpConfigPath);
50
+ if (mcpConfigPath && options.strictMcpConfig)
51
+ args.push('--strict-mcp-config');
52
+ const allowed = [
53
+ ...(options.allowedTools ?? []),
54
+ ...(mcpConfigPath && (options.allowMcpTools ?? true) ? (task.mcpServers ?? []).map((s) => `mcp__${s.name}`) : []),
55
+ ];
56
+ if (allowed.length)
57
+ args.push('--allowedTools', allowed.join(','));
58
+ if (options.disallowedTools?.length)
59
+ args.push('--disallowedTools', options.disallowedTools.join(','));
60
+ args.push('--output-format', 'stream-json', '--permission-mode', options.permissionMode ?? 'acceptEdits');
61
+ if (task.model)
62
+ args.push('--model', task.model);
63
+ if (task.effort)
64
+ args.push('--effort', task.effort);
65
+ if (task.maxTurns !== undefined)
66
+ args.push('--max-turns', String(task.maxTurns));
67
+ if (task.extraArgs)
68
+ args.push(...task.extraArgs);
69
+ return [...args, ...positionalPrompt(task.prompt)];
70
+ }
71
+ export function createClaudeParser() {
72
+ const toolNames = new Map();
73
+ let outcome;
74
+ let sessionSent = false;
75
+ let lastMessage;
76
+ return {
77
+ outcome: () => outcome,
78
+ event(value) {
79
+ if (!isObject(value))
80
+ return [];
81
+ const events = [];
82
+ const sessionId = stringField(value, 'session_id');
83
+ if (sessionId && !sessionSent) {
84
+ sessionSent = true;
85
+ events.push({ type: 'session', sessionId, ...modelOf(value) });
86
+ }
87
+ switch (value.type) {
88
+ case 'assistant':
89
+ for (const block of contentBlocks(value.message)) {
90
+ if (block.type === 'text' && typeof block.text === 'string' && block.text.trim()) {
91
+ lastMessage = block.text;
92
+ events.push({ type: 'message', text: block.text });
93
+ }
94
+ else if (block.type === 'tool_use' && typeof block.name === 'string') {
95
+ const id = typeof block.id === 'string' ? block.id : undefined;
96
+ if (id)
97
+ toolNames.set(id, block.name);
98
+ events.push({ type: 'tool-start', ...(id ? { id } : {}), name: block.name, input: block.input });
99
+ const path = stringField(block.input, 'file_path') ?? stringField(block.input, 'notebook_path');
100
+ if (EDIT_TOOLS.has(block.name) && path)
101
+ events.push({ type: 'file-change', paths: [path] });
102
+ }
103
+ }
104
+ break;
105
+ case 'user':
106
+ for (const block of contentBlocks(value.message)) {
107
+ if (block.type !== 'tool_result')
108
+ continue;
109
+ const id = typeof block.tool_use_id === 'string' ? block.tool_use_id : undefined;
110
+ const name = id ? toolNames.get(id) : undefined;
111
+ const output = toolOutput(block.content);
112
+ events.push({
113
+ type: 'tool-end',
114
+ ...(id ? { id } : {}),
115
+ ...(name ? { name } : {}),
116
+ isError: block.is_error === true,
117
+ ...(output !== undefined ? { output } : {}),
118
+ });
119
+ }
120
+ break;
121
+ case 'stream_event': {
122
+ const delta = isObject(value.event) ? value.event.delta : undefined;
123
+ const text = stringField(delta, 'text');
124
+ if (stringField(delta, 'type') === 'text_delta' && text)
125
+ events.push({ type: 'text-delta', text });
126
+ break;
127
+ }
128
+ case 'result': {
129
+ const failed = value.is_error === true || (value.subtype !== undefined && value.subtype !== 'success');
130
+ const text = stringField(value, 'result');
131
+ outcome = failed
132
+ ? { ok: false, error: text || `Claude stopped: ${String(value.subtype ?? 'error')}.` }
133
+ : { ok: true };
134
+ const usage = value.usage;
135
+ if (isObject(usage)) {
136
+ const inputTokens = numberField(usage, 'input_tokens');
137
+ const outputTokens = numberField(usage, 'output_tokens');
138
+ const cachedInputTokens = numberField(usage, 'cache_read_input_tokens');
139
+ const costUsd = numberField(value, 'total_cost_usd');
140
+ events.push({
141
+ type: 'usage',
142
+ usage: {
143
+ ...(inputTokens !== undefined ? { inputTokens } : {}),
144
+ ...(outputTokens !== undefined ? { outputTokens } : {}),
145
+ ...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
146
+ ...(inputTokens !== undefined && outputTokens !== undefined
147
+ ? { totalTokens: inputTokens + outputTokens }
148
+ : {}),
149
+ },
150
+ ...(costUsd !== undefined ? { costUsd } : {}),
151
+ });
152
+ }
153
+ // The result repeats the last assistant message; only surface it when it differs.
154
+ if (!failed && text && text !== lastMessage)
155
+ events.push({ type: 'message', text });
156
+ if (failed)
157
+ events.push({ type: 'error', message: outcome.error });
158
+ break;
159
+ }
160
+ }
161
+ return events;
162
+ },
163
+ };
164
+ }
165
+ function contentBlocks(message) {
166
+ const content = isObject(message) ? message.content : undefined;
167
+ return Array.isArray(content) ? content.filter(isObject) : [];
168
+ }
169
+ function modelOf(value) {
170
+ const model = stringField(value, 'model');
171
+ return model ? { model } : {};
172
+ }
173
+ function toolOutput(content) {
174
+ if (typeof content === 'string')
175
+ return content;
176
+ if (!Array.isArray(content))
177
+ return undefined;
178
+ const text = content
179
+ .map((part) => stringField(part, 'text'))
180
+ .filter((part) => part !== undefined)
181
+ .join('\n');
182
+ return text || undefined;
183
+ }
184
+ //# sourceMappingURL=claude.js.map