genaicode 2.18.3 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,15 +2,14 @@
2
2
  <picture>
3
3
  <source media="(prefers-color-scheme: dark)" srcset="docs/media/logo-dark.png">
4
4
  <source media="(prefers-color-scheme: light)" srcset="docs/media/logo.png">
5
- <img alt="GenAIcode Logo." src="docs/media/logo.png" width="100%" height="auto">
5
+ <img alt="GenAIcode Logo." src="docs/media/logo.png" width="320">
6
6
  </picture>
7
7
  </p>
8
8
 
9
- # GenAIcode
10
-
11
- GenAIcode drives the coding-agent CLIs you already have (Claude Code, Codex, GitHub Copilot
12
- CLI, Cursor, Gemini CLI, opencode, Muse, Mistral Vibe, Antigravity) behind one API and one
13
- CLI. Script them, swap them with one flag, and let your tests decide when the job is done.
9
+ <p align="center">
10
+ <b>One API and one CLI for the coding agents you already have.</b><br>
11
+ Claude Code · Codex · Copilot CLI · Cursor · Gemini CLI · opencode · Muse · Mistral Vibe · Antigravity
12
+ </p>
14
13
 
15
14
  <p align="center">
16
15
  <a href="docs/media/showcase.mp4">
@@ -20,189 +19,43 @@ CLI. Script them, swap them with one flag, and let your tests decide when the jo
20
19
  <sub>Recorded live with Claude Code and Codex. <a href="docs/media/showcase.mp4">Full video (87 s)</a>.</sub>
21
20
  </p>
22
21
 
23
- ```bash
24
- npx genaicode run --verify "npm test" "fix the failing tests"
25
- ```
26
-
27
- That runs the first agent CLI installed on your machine in the current directory, then runs
28
- `npm test`. If the check fails, its output goes back to the agent for another attempt. Add
29
- `-a codex` (or `claude`, `copilot`, `opencode`...) to pick the agent; the command stays the
30
- same. From there, the same drivers are a TypeScript API, a chat in your terminal or browser,
31
- a panel inside your Vite app, and an agent inside your own app.
32
-
33
- GenAIcode also ships [a thin LLM layer](#also-a-thin-llm-layer) for calling model providers
34
- from backend code. The two halves are separate subpaths: importing `genaicode` never spawns an
35
- agent, and `genaicode/agents` loads no provider SDK.
36
-
37
- ## Install
22
+ ## Quick start
38
23
 
39
24
  ```bash
40
- npm install genaicode
25
+ npx genaicode # chat with your agent in this terminal
26
+ npx genaicode ui # the same chat in your browser
27
+ npx genaicode run --verify "npm test" "fix the failing tests"
28
+ npx genaicode embed # let your agent wire genaicode into your app
41
29
  ```
42
30
 
43
- Node.js 20 or newer is required. The agent CLIs are yours to install and log in to;
44
- `npx genaicode agents` lists which ones it finds.
31
+ `run --verify` runs the first agent CLI it finds, then your check; a failing check goes back
32
+ to the agent for another try. `-a codex` (or `claude`, `copilot`, `opencode`...) picks the
33
+ agent, and the command stays the same. Node.js 20+; the agent CLIs are yours to install and
34
+ log in to (`npx genaicode agents` lists what it finds).
45
35
 
46
- ## Coding agents
36
+ ## What's inside
47
37
 
48
- `genaicode/agents` turns the coding-agent CLIs installed on a machine into one API. Give
49
- any of them a task (a prompt and a working directory) and get back the same event stream
50
- and result, whichever agent does the work. It is how you script, compare, or chain coding
51
- agents from a job, a bot, or a CI step without writing a parser per vendor.
38
+ | Use it as | Import / command | Docs |
39
+ | ------------------------------ | -------------------------------------- | --------------------------------- |
40
+ | A TypeScript API over agents | `genaicode/agents` | [agents.md](docs/agents.md) |
41
+ | CLI, terminal and browser chat | `genaicode run`, `chat`, `ui`, `embed` | [cli.md](docs/cli.md) |
42
+ | A panel in your dev server | `genaicode/vite`, `createDevOverlay` | [vite.md](docs/vite.md) |
43
+ | A chat component | `genaicode/react` `<AgentChat>` | [react.md](docs/react.md) |
44
+ | An agent inside your own app | `genaicode/app-agent` | [app-agent.md](docs/app-agent.md) |
45
+ | A thin LLM layer | `genaicode`, `genaicode/providers` | [llm.md](docs/llm.md) |
52
46
 
53
47
  ```ts
54
- import { claude, codex, copilot, cursor, detectAgents } from 'genaicode/agents';
55
-
56
- const [found] = detectAgents([claude(), codex(), copilot(), cursor()]).filter(({ path }) => path);
57
- if (!found) throw new Error('No coding agent installed.');
58
-
59
- const run = found.agent.run({ prompt: 'Add a unit test for parseDate', cwd: '/path/to/repo', timeoutMs: 20 * 60_000 });
60
- for await (const event of run) {
61
- if (event.type === 'tool-start') console.log('⚙', event.name);
62
- if (event.type === 'file-change') console.log('edited', event.paths.join(', '));
63
- }
64
- const result = await run.result; // { status, ok, text, sessionId, usage, error, ... }
65
- ```
66
-
67
- | Agent | Driver | Extras |
68
- | ---------------- | ----------------------------- | ----------------------------------------------- |
69
- | Claude Code | `claude()` | MCP servers, approvals, max turns, effort, cost |
70
- | Codex | `codex()`, `codexLive()` | MCP servers; live: `steer()`, approvals |
71
- | GitHub Copilot | `copilot()` | MCP servers, allow/deny tool patterns |
72
- | Cursor | `cursor()` | streamed text deltas |
73
- | Gemini CLI | `gemini()` | approval mode |
74
- | opencode | `opencode()` | `provider/model` ids, variants |
75
- | Mistral Vibe | `vibe()` | agent profile, max turns, max price |
76
- | Antigravity CLI | `antigravity()` | sandbox, effort, usage |
77
- | Muse | `muse()`, `museLive()` | max turns; live: `steer()`, approvals |
78
- | Your own CLI/API | `cliAgent()`, `hostedAgent()` | plug in any JSON-lines CLI or hosted agent |
79
-
80
- On top of the drivers:
81
-
82
- - **Steer a running task** with `run.steer(text)` (live drivers), and answer permission
83
- prompts with `onApproval` (Claude, live Codex and Muse).
84
- - **Pick approval and sandbox policy** with `permissions: { approval, sandbox }` (or
85
- `'yolo'`); each driver maps it to the agent's own flags and refuses what it cannot honor.
86
- - **Attach MCP servers per task** with `mcpServers`. Header secrets stay out of argv.
87
- - **Verify and repair**: `runWithVerify` runs your check (tests, lint) after the agent and
88
- sends failures back for another attempt.
89
- - **Keep credentials apart**: `withoutProviderCredentials()` strips your app's LLM API keys
90
- from the agent's environment, so it bills its own login.
91
-
92
- Importing `genaicode` never spawns anything; only `genaicode/agents` does. The agent uses
93
- its own credentials and billing, and its own permission settings unless `permissions` sets
94
- them; any sandbox is the agent's own. GenAIcode does not choose a model or retry it. See [docs/agents.md](docs/agents.md) for events, options per
95
- driver, and writing your own driver.
96
-
97
- ### From the command line
98
-
99
- The `genaicode` bin runs the same headless drivers without writing any code:
100
-
101
- ```bash
102
- npx genaicode agents # which agent CLIs are installed
103
- npx genaicode run "Add a unit test for parseDate" # first installed agent, current dir
104
- npx genaicode run -a codex -C ../api --verify "npm test" "Fix the failing date tests"
105
- git diff | npx genaicode run --json -a claude - # prompt from stdin, JSON-lines events
106
- ```
107
-
108
- Assistant text goes to stdout and tool calls, edits, and the result line go to stderr.
109
- `--verify` runs a shell command after the agent finishes and sends its output back for up
110
- to `--max-repairs` more attempts (`runWithVerify`). The exit code is 0 on success, 1 when
111
- the agent or the check fails, 2 on a usage error, and 130 when interrupted.
112
- `npx genaicode --help` lists every option.
113
-
114
- ### Chat with an agent: terminal and browser
115
-
116
- ```bash
117
- npx genaicode # chat in this terminal (same as "genaicode chat")
118
- npx genaicode chat -a claude -C ../api
119
- npx genaicode ui # the same chat in your browser, served on 127.0.0.1
120
- npx genaicode chat --web # both: the terminal and the browser on one session
121
- ```
122
-
123
- The terminal and the browser can show one session. `/web` in the terminal chat opens it in
124
- the browser (`/web off` stops the server), and Enter in the terminal that runs
125
- `genaicode ui` brings it into that terminal. A prompt, an approval or a stop from either
126
- side shows up in both.
127
-
128
- Each prompt after the first continues the agent's own session (`AgentTask.resume`), so you
129
- can say "now add a test for that". Prompts sent while the agent works wait in a queue and
130
- run next; Esc stops the agent. When the agent asks for permission, `y` approves, `n`
131
- denies and `a` approves everything from then on (`/auto`, or the Auto-approve chip in the
132
- browser; `-y` starts that way). `/agent`, `/model` and `/new` switch agent, model and
133
- session; `/help` lists the rest.
48
+ import { codex } from 'genaicode/agents';
134
49
 
135
- <p align="center">
136
- <picture>
137
- <source media="(prefers-reduced-motion: reduce)" srcset="docs/media/screenshots/chat-terminal.png">
138
- <img alt="genaicode chat in a terminal: the agent fixes a failing test, then takes queued follow-ups." src="docs/media/screenshots/chat-terminal.gif" width="49%">
139
- </picture>
140
- <picture>
141
- <source media="(prefers-reduced-motion: reduce) and (prefers-color-scheme: dark)" srcset="docs/media/screenshots/wolf-dark.png">
142
- <source media="(prefers-reduced-motion: reduce)" srcset="docs/media/screenshots/wolf-light.png">
143
- <source media="(prefers-color-scheme: dark)" srcset="docs/media/screenshots/chat-browser-dark.gif">
144
- <source media="(prefers-color-scheme: light)" srcset="docs/media/screenshots/chat-browser-light.gif">
145
- <img alt="genaicode ui in a browser: the same fix and a follow-up." src="docs/media/screenshots/chat-browser-light.gif" width="49%">
146
- </picture>
147
- </p>
148
-
149
- The wolf from genaicode 1.0 is back. When the agent asks for approval or finishes, the
150
- browser UI barks if its tab is in the background and counts those events in the tab title.
151
- The terminal UI rings the terminal bell instead. `/bark off` (or the Bark button) quiets it.
152
-
153
- <p align="center">
154
- <picture>
155
- <source media="(prefers-color-scheme: dark)" srcset="docs/media/screenshots/wolf-dark.png">
156
- <source media="(prefers-color-scheme: light)" srcset="docs/media/screenshots/wolf-light.png">
157
- <img alt="The browser UI with the wolf behind a finished turn." src="docs/media/screenshots/wolf-light.png" width="80%">
158
- </picture>
159
- </p>
160
-
161
- `genaicode ui` listens on the loopback address and prints a link with a random access
162
- token. The page, the event stream and every command need that token; only the bundled
163
- script and the four mascot files (`/app.js`, `/assets/…`) are served without it, since they
164
- hold nothing about your session. Both front ends are bundled into the package and load
165
- only for these commands; importing `genaicode` or `genaicode/agents` pulls in no UI code.
166
- In code, the same conversation is `createAgentSession` from `genaicode/agents` (see
167
- [docs/agents.md](docs/agents.md#chat-sessions)); with a live driver (`codexLive`,
168
- `museLive`) it also steers the running agent and asks you about permission requests.
169
-
170
- ### Let your agent embed it
171
-
172
- ```bash
173
- npx genaicode embed # in your project: your agent wires genaicode in
174
- npx genaicode embed -a codex "only the dev panel"
175
- npx genaicode embed --print | pbcopy # just the instructions, for any other agent
50
+ const run = codex().run({ prompt: 'Add a unit test for parseDate', cwd: '/path/to/repo' });
51
+ for await (const event of run) if (event.type === 'file-change') console.log('edited', event.paths);
52
+ const result = await run.result; // { status, ok, text, sessionId, usage, ... }
176
53
  ```
177
54
 
178
- `genaicode embed` does not inspect your project itself. It starts your agent with
179
- instructions on how to embed genaicode, and the agent reads the project and picks the way
180
- in: the Vite overlay below for an app on Vite; for any other Node dev server (webpack,
181
- Next.js, Express...) the same overlay through `createDevOverlay` from `genaicode/ui`; for
182
- pages with no dev server at all, a small dev-only server of its own with that overlay; the in-app agent (`genaicode/app-agent` with
183
- `<AppAgentChat>`) when the agent should act on the app itself; and `npx genaicode ui` next
184
- to the dev server when there is no Node process to host it. Words after `embed` go to the
185
- agent as your request. You watch and answer in the same chat as `genaicode chat` (`--ui`
186
- for the browser); without a terminal it runs headless like `run`. The agent installs the
187
- package, edits the config, runs your checks and reports; it does not commit.
188
-
189
- The instructions are also a Claude Code skill. Copy
190
- [`skills/genaicode-embed`](skills/genaicode-embed/SKILL.md) into `.claude/skills/` (or
191
- `~/.claude/skills/`) and ask Claude to "embed genaicode":
192
-
193
- ```bash
194
- mkdir -p .claude/skills && cp -r node_modules/genaicode/skills/genaicode-embed .claude/skills/
195
- ```
196
-
197
- ### Inside a Vite app
198
-
199
- ```ts
200
- // vite.config.ts
201
- import { defineConfig } from 'vite';
202
- import genaicode from 'genaicode/vite';
203
-
204
- export default defineConfig({ plugins: [genaicode()] });
205
- ```
55
+ Every driver gives the same events and result. On top: steering a running task, approvals
56
+ and sandbox policy (`permissions`, or `'yolo'`), per-task MCP servers, and verify-and-repair.
57
+ The agent keeps its own login, billing and sandbox; GenAIcode picks no model and adds no
58
+ agent loop of its own.
206
59
 
207
60
  <p align="center">
208
61
  <picture>
@@ -212,687 +65,18 @@ export default defineConfig({ plugins: [genaicode()] });
212
65
  </picture>
213
66
  </p>
214
67
 
215
- In `vite dev`, the app gets the wolf in its corner. It opens the same chat as `genaicode ui`
216
- in a panel, with the agent working in the project root. The wolf counts the page's errors
217
- (build errors, uncaught exceptions, unhandled rejections, `console.error`), and "Fix N
218
- errors" sends them to the agent as one prompt. Options: `agent`, `model`, `effort`,
219
- `approveAll`, `agents`, `port`, and `captureErrors: false` to leave the page's errors alone.
220
- Builds are untouched. The chat runs on its own port on 127.0.0.1 even when Vite runs with
221
- `--host`, lets only the app's origins frame it, and hands its token (and takes "fix" requests)
222
- only from the app's own pages on this machine.
223
-
224
- Any other Node dev server (Express, webpack-dev-server, a script that serves static files)
225
- gets the same overlay from `createDevOverlay` in `genaicode/ui`:
226
-
227
- ```ts
228
- import { createDevOverlay } from 'genaicode/ui';
229
-
230
- const overlay = createDevOverlay(); // same options as the plugin, plus `cwd`
231
- app.use(overlay.middleware); // Express; or `if (await overlay.handle(req, res)) return;`
232
- // ...and in the HTML it serves: overlay.injectHtml(html), or the overlay.script tag.
233
- ```
234
-
235
- Development only. The token goes only to pages opened at `localhost`, `*.localhost`,
236
- `127.0.0.1` or `[::1]`, and without an agent CLI the overlay stays off.
237
-
238
- ### In your own React app
239
-
240
- `genaicode/react` is the same chat as a component. It draws a `SessionState` your app gives
241
- it and calls you back; it runs, fetches and stores nothing, so your server owns the session,
242
- its MCP servers and its prompts. React 18 or newer comes from your app: genaicode does not
243
- declare or install it.
244
-
245
- ```ts
246
- // server (Node): one session, streamed to the page
247
- import { createAgentSession, claude } from 'genaicode/agents';
248
-
249
- const session = createAgentSession({
250
- agent: claude(),
251
- cwd: process.cwd(),
252
- task: { mcpServers: [{ name: 'ops', url: 'http://127.0.0.1:4000/mcp' }] },
253
- // The agent gets the context; the transcript keeps what the user typed.
254
- transformPrompt: (text) => `${briefing}\n\nOperator message:\n${text}`,
255
- });
256
-
257
- app.get('/api/agent/events', (req, res) => {
258
- res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-store' });
259
- const push = (state) => res.write(`data: ${JSON.stringify(state)}\n\n`);
260
- push(session.get());
261
- res.on('close', session.subscribe(push));
262
- });
263
- app.post('/api/agent/send', (req, res) => {
264
- session.send(req.body.text);
265
- res.json({ ok: true });
266
- });
267
- app.post('/api/agent/stop', (req, res) => {
268
- session.stop();
269
- res.json({ ok: true });
270
- });
271
- app.post('/api/agent/approve', (req, res) => res.json({ ok: session.approve(req.body.id, req.body.decision) }));
272
- ```
273
-
274
- ```tsx
275
- // page
276
- import { useEffect, useState } from 'react';
277
- import { AgentChat, type SessionState } from 'genaicode/react';
278
- import 'genaicode/react/styles.css';
279
-
280
- const post = (path: string, body: object = {}) =>
281
- fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
282
-
283
- export function AgentDrawer() {
284
- const [state, setState] = useState<SessionState | null>(null);
285
- useEffect(() => {
286
- const events = new EventSource('/api/agent/events');
287
- events.onmessage = (event) => setState(JSON.parse(event.data));
288
- return () => events.close();
289
- }, []);
290
- return (
291
- <AgentChat
292
- state={state}
293
- onSend={(text) => post('/api/agent/send', { text })}
294
- onStop={() => post('/api/agent/stop')}
295
- onApprove={(id, decision) => post('/api/agent/approve', { id, decision })}
296
- formatToolName={(name) => name.replace(/^mcp__ops__/, '')}
297
- renderFooter={() => <MyApprovalCards />}
298
- headerExtras={<button onClick={() => post('/api/agent/reset')}>New</button>}
299
- />
300
- );
301
- }
302
- ```
303
-
304
- `<AgentChat>` fills its container's height. It shows turns with streaming Markdown (no raw
305
- HTML; links open in a new tab), collapsible tool calls, edited files, errors, the agent's
306
- own approval requests, queued prompts and a working indicator, and an input where Enter
307
- sends, Shift+Enter adds a line and Esc stops. Props:
308
-
309
- | Prop | |
310
- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
311
- | `state` | `SessionState`, or null before there is a session |
312
- | `onSend(text)`, `onStop()` | Enter or Send; Stop or Esc. No `onStop`, no Stop button |
313
- | `onApprove(id, decision)` | answers `state.approvals`; without it they show as waiting |
314
- | `renderPrompt(text, turn?)` | how a sent prompt shows (return null to hide it) |
315
- | `renderAfterTurn(turn, i)`, `renderFooter()` | your content after a turn, or at the end |
316
- | `notices` | `{ id, afterTurn, text, tone? }[]`: your own lines between turns |
317
- | `foldSteps` | fold each turn's tools and edits into one "N steps" line |
318
- | `header`, `headerExtras` | replace the header (null for none), or add to its right end |
319
- | `emptyState`, `placeholder`, `hint` | text before the first turn, in and under the input |
320
- | `formatToolName(name)`, `disabled`, `autoFocus`, `theme`, `className`, `style` | |
321
-
322
- Theme it with CSS custom properties on the component or any ancestor: `--gc-bg`,
323
- `--gc-panel`, `--gc-border`, `--gc-text`, `--gc-muted`, `--gc-accent`, `--gc-accent-text`,
324
- `--gc-error`, `--gc-font`, `--gc-mono`, `--gc-font-size`, `--gc-radius`, `--gc-max-width`
325
- (and `--gc-ok`, `--gc-warn`, plus soft shades mixed from these unless set). Unset ones keep
326
- the `genaicode ui` look, light or dark by `prefers-color-scheme`, or by `theme="light"` /
327
- `"dark"`. `genaicode ui` and the Vite panel draw this same component.
328
-
329
- Two smaller pieces come along for hosts that build their own panels around a transcript:
330
-
331
- ```ts
332
- import { installResizeHandle, storedSize, useFollowScroll } from 'genaicode/react';
333
-
334
- // A log that stays at its newest line until the user scrolls up (and again after follow()).
335
- const { ref, onScroll, follow } = useFollowScroll(lines);
336
-
337
- // A drag handle on a panel's left edge; the width survives reloads.
338
- const stored = storedSize('panel-width', [300, 900]);
339
- let width = stored.get();
340
- const handle = installResizeHandle(edge, {
341
- axis: 'x',
342
- direction: () => -1, // dragging left makes the panel wider
343
- bounds: () => [300, Math.min(900, innerWidth - 24)],
344
- read: () => width ?? 390,
345
- write: (next, commit) => {
346
- width = next;
347
- panel.style.setProperty('--width', `${width}px`);
348
- if (commit) stored.set(width);
349
- },
350
- reset: () => {
351
- width = undefined;
352
- panel.style.removeProperty('--width');
353
- stored.clear();
354
- },
355
- });
356
- // The CSS caps the shown width, so the saved one comes back when the window grows again:
357
- // .panel { width: min(var(--width, 390px), calc(100vw - 24px)); }
358
- window.addEventListener('resize', handle.sync);
359
- ```
360
-
361
- The handle keeps its `aria-value*` range current and takes the arrow keys (Shift for bigger
362
- steps), Home and End, Escape to undo a drag, and Enter or double-click to reset. While a drag
363
- runs, `document.body` (or `root`) has `data-resizing="true"`, so the page can keep the cursor
364
- and turn off pointer events on iframes. `read()` must return what `write` or `reset` just set,
365
- so keep the size in a variable or a ref rather than only in React state.
366
-
367
- To show the whole `genaicode ui` page in a frame instead, `startEmbeddedWeb` from
368
- `genaicode/ui` serves it on 127.0.0.1 with your `task` (`mcpServers`, `timeoutMs`...), `env`
369
- and `transformPrompt`, and returns its tokenized `url` and a `controller` to send prompts.
370
-
371
- ### Inside your own local app
372
-
373
- `genaicode/app-agent` puts an agent in a local app you run on your machine (an admin
374
- console, an internal tool, a game editor). The user chats with an agent CLI they already
375
- have; the agent works the app through tools the app serves over MCP, changes wait for the
376
- user's approval, and in `dev` mode the agent also edits the app's code and proposes a commit.
377
-
378
- ```ts
379
- import { createServer } from 'node:http';
380
- import { createAppAgent, createAppAgentHandler } from 'genaicode/app-agent';
381
-
382
- const agent = createAppAgent({
383
- name: 'the shop admin',
384
- mcpUrl: 'http://127.0.0.1:3000/api/agent/mcp',
385
- tools: [
386
- { name: 'orders', description: 'List open orders.', run: () => JSON.stringify(listOrders()) },
387
- {
388
- name: 'refund',
389
- description: 'Refund an order.',
390
- write: true, // a card with Approve and Reject; runs only after Approve
391
- inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
392
- run: ({ id }) => refund(String(id)),
393
- },
394
- ],
395
- pages: () => ['/orders', '/customers'], // page context, link chips, "take me there"
396
- dev: { cwd: process.cwd(), commands: ['npm test'] }, // optional: the agent edits the app
397
- });
398
- const handle = createAppAgentHandler(agent, { authorize: (req) => isSignedIn(req) });
399
-
400
- createServer(async (req, res) => {
401
- if (await handle(req, res)) return;
402
- // ...the rest of the app
403
- }).listen(3000, '127.0.0.1');
404
- ```
405
-
406
- On the page, `AppAgentChat` from `genaicode/react` is the whole drawer: the chat, the agent's
407
- page chips under its replies, Approve and Reject cards (with the diff of a proposed commit),
408
- and the agent and mode pickers. It reports `currentPath` to the agent and calls `onNavigate`
409
- when a chip is clicked or the agent is asked to go somewhere.
410
-
411
- ```tsx
412
- import { AppAgentChat } from 'genaicode/react';
413
- import 'genaicode/react/styles.css';
414
-
415
- <AppAgentChat currentPath={location.pathname} onNavigate={(path) => router.push(path)} />;
416
- ```
417
-
418
- <p align="center">
419
- <img alt="A shop admin with the agent drawer: the agent proposed a refund, waiting for Approve." src="docs/media/screenshots/app-agent.png" width="80%">
420
- </p>
421
-
422
- Under it, `/api/agent` serves `GET /events` (a stream of `AppAgentState`: the session, earlier
423
- turns, pending actions, link chips, navigation requests) and `POST /send`, `/stop`, `/reset`,
424
- `/config`, `/view`, `/actions/:id` and `/approvals/:id` (the agent CLI's own permission
425
- prompts); `useAppAgent` wraps them for a page of your own. Switching agent or mode keeps the
426
- conversation: the next agent gets a short transcript. `trace` receives every prompt, MCP call,
427
- decision and finished turn, for a log. Default agents are Claude Code, Codex and
428
- Copilot CLI, locked down per mode: in `use` mode they get only the app's tools (no shell, no
429
- file edits); in `dev` mode they edit files and run the listed commands, never commit or push (Claude and
430
- Copilot enforce the command list; Codex has no per-command allowlist, so it runs in its
431
- `workspace-write` sandbox: writes only inside `cwd`, no network).
432
- `propose_commit` snapshots the worktree, the user reviews that diff, and Approve commits
433
- exactly that tree on the reviewed HEAD, or refuses if the files or HEAD changed since, a hook
434
- changed the files, or a merge or rebase is in progress. The repository's commit hooks run.
435
- The agent sees no MCP server but the app's: Claude runs with `--strict-mcp-config`, and Codex
436
- and Copilot get the user's own configured servers switched off. The MCP endpoint takes a per-run
437
- bearer and refuses browser requests; every other route asks `authorize` and refuses
438
- cross-site requests. Without `authorize` only a loopback `Host` is accepted (add names with
439
- `allowedHosts`), so a DNS-rebinding page cannot reach the agent.
440
-
441
- ## Also: a thin LLM layer
442
-
443
- Besides driving agents, GenAIcode is a small TypeScript toolkit for calling LLMs from backend
444
- code. It sits between raw provider SDKs and full agent frameworks: one prompt representation,
445
- thin provider adapters, a convenient request API, and lightweight conversation chains. This
446
- layer does not inspect repositories, execute shell commands, edit files, or run an agent UI.
447
-
448
- **Like jQuery**, the common case starts with one small function and becomes more specific
449
- through chaining—configure a request, follow up across multiple prompts, and keep history
450
- portable without adopting a full agent framework.
451
-
452
- ### A prompt in three lines
453
-
454
- ```ts
455
- import { genaicode } from 'genaicode';
456
- import { openai } from 'genaicode/providers';
457
-
458
- const ai = genaicode(openai({ model: 'your-model-name' }));
459
- const answer = await ai('Explain why the sky is blue in two sentences.').text();
460
- ```
461
-
462
- The client is callable on purpose. Configuration is ordinary method chaining—system
463
- instructions, temperature, token limits, and more—without a separate options object or
464
- framework setup:
465
-
466
- ```ts
467
- const result = await ai('Create a release note from these commits')
468
- .system('You are a concise technical writer.')
469
- .temperature(0.2)
470
- .maxOutputTokens(500)
471
- .text();
472
- ```
473
-
474
- Builders are immutable, so a configured base request can be safely reused:
475
-
476
- ```ts
477
- const releaseNote = ai('Create a release note from these commits')
478
- .system('You are a concise technical writer.')
479
- .temperature(0.2);
480
-
481
- const short = await releaseNote.maxOutputTokens(200).text();
482
- const long = await releaseNote.maxOutputTokens(800).text();
483
- ```
484
-
485
- ### Why this vs a raw SDK
486
-
487
- Provider SDKs are the right tool when you call one vendor and own every request shape
488
- yourself. The cost shows up when the same backend needs a second provider, a test double,
489
- or shared policy (timeouts, JSON parsing, rate limits, fallback) without copying glue.
490
-
491
- GenAIcode is that thin shared layer:
492
-
493
- - One portable prompt/tool IR (`PromptItem`) instead of OpenAI messages vs Anthropic
494
- blocks vs Gemini contents.
495
- - One request API (`.text()`, `.json()`, `.stream()`, chains) over those adapters.
496
- - A tiny `ModelProvider` seam so tests and custom gateways do not mock vendor HTTP.
497
- - Opt-in middleware and retry helpers—no hidden tool runners, no silent retries, no
498
- agent loop.
499
-
500
- If you are happy importing one SDK and never swapping models or providers, stay on the
501
- SDK. If you want the call site to look the same while the edge stays replaceable, use
502
- GenAIcode.
503
-
504
- ### Chaining prompts
505
-
506
- A chain remembers successful user and assistant turns. Each new prompt sees the complete
507
- history, so multi-step work stays in ordinary application code:
508
-
509
- ```ts
510
- import { system } from 'genaicode';
511
-
512
- const chain = ai.chain(system('You are helping refine an API design.'));
513
-
514
- const answer1 = await chain.text('Prompt 1: propose a minimal endpoint.');
515
- const answer2 = await chain.text('Prompt 2: add idempotency to that design.');
516
- const answer3 = await chain.text('Prompt 3: summarize the final contract.');
517
- ```
518
-
519
- This is `prompt1 → response1 → prompt2 → response2 → prompt3 → response3`. The history
520
- is available as portable `PromptItem[]` through `chain.history()`.
521
-
522
- ### Using a chain in a loop
523
-
524
- A chain is ordinary application state, so normal control flow works:
525
-
526
- ```ts
527
- const chain = ai.chain(system('Improve the draft while preserving its meaning.'));
528
- let draft = 'The deployment had a problem and we fixed it.';
529
-
530
- for (const instruction of ['Make it specific.', 'Make it concise.', 'Use a professional tone.']) {
531
- draft = await chain.text(`${instruction}\n\nCurrent draft:\n${draft}`);
532
- }
533
- ```
534
-
535
- Calls on one chain are serialized, even if application code starts them concurrently.
536
- Failed provider calls are not added to history.
537
-
538
- Validation and repair are also plain loops rather than a special agent abstraction:
539
-
540
- ```ts
541
- import { parseJsonResult, resultText, system } from 'genaicode';
542
-
543
- const repair = ai.chain(system('Return only valid JSON with a non-negative count.'));
544
- let count: number | undefined;
545
-
546
- for (let attempt = 1; attempt <= 3; attempt += 1) {
547
- const result = await repair.ask(
548
- attempt === 1 ? 'Count the actionable items in this text: ...' : 'Correct the previous response.',
549
- );
550
-
551
- try {
552
- const value = parseJsonResult<{ count: number }>(result);
553
- if (value.count < 0) throw new Error('count must be non-negative');
554
- count = value.count;
555
- break;
556
- } catch (error) {
557
- if (attempt === 3) throw error;
558
- console.warn('Invalid model response:', resultText(result));
559
- }
560
- }
561
- ```
562
-
563
- The application owns validation, attempt limits, and failure policy.
564
-
565
- ### Schema adapters
566
-
567
- `json(...)` and `parseJsonResult(...)` accept either a parser function or a schema adapter
568
- with a `parse(value)` method. This keeps schema validation library-agnostic:
569
-
570
- ```ts
571
- const value = await ai('Return {"count": 3}.').json({
572
- parse(input) {
573
- if (typeof input !== 'object' || input === null || typeof input.count !== 'number') {
574
- throw new Error('Invalid shape');
575
- }
576
- return input;
577
- },
578
- });
579
- ```
580
-
581
- Calling `.json()` also sets `responseFormat: { type: 'json' }` on the request when you have
582
- not already chosen a format, so providers that support JSON mode (OpenAI, Gemini/Vertex)
583
- are asked for JSON rather than free text.
584
-
585
- ### Response format and thinking
586
-
587
- Portable request fields cover the two knobs backends usually poke through provider-specific
588
- config:
589
-
590
- ```ts
591
- const verdict = await ai(promptText)
592
- .responseFormat({ type: 'json' })
593
- .thinking({ level: 'minimal' }) // or { budgetTokens: 0 } / false to disable
594
- .temperature(0)
595
- .json((value) => VerdictSchema.parse(value));
596
- ```
597
-
598
- - `responseFormat`: `{ type: 'text' | 'json' }` or
599
- `{ type: 'json_schema', name, schema, strict? }`.
600
- - `thinking`: `false` to disable, or `{ budgetTokens?, level? }` (`minimal` |
601
- `low` | `medium` | `high`). Prefer one of budget or level — some providers reject both.
602
- On Google/Gemini, `false` and `budgetTokens: 0` map to `thinkingLevel: MINIMAL`
603
- (or `LOW` for models like Gemini 3.7 and Gemini 3 Pro where `MINIMAL` is unsupported)
604
- because Gemini 3 rejects `thinkingBudget: 0`. JSON `responseFormat` without an
605
- explicit `thinking` setting or provider `generationConfig.thinkingConfig` default also
606
- defaults Google thinking to `MINIMAL` (or `LOW` on Gemini 3.7 / Pro).
607
- On Anthropic, `level` becomes `output_config.effort` with adaptive thinking (Opus and
608
- Sonnet 4.6+), and `false` uses whatever the model accepts: `disabled`, `between_tools`
609
- (Sonnet 5.5), or effort `low` where thinking is always on (Opus 5.5, Fable). Budgets
610
- become adaptive thinking on models that removed them.
611
-
612
- Providers map what they support and ignore the rest. `ProviderCapabilities.jsonResponse`
613
- and `ProviderCapabilities.thinking` advertise support. Vendor-specific escapes such as
614
- Vertex `generationConfig` remain available for anything not covered here.
615
-
616
- ### PromptItem: the portable prompt IR
617
-
618
- `PromptItem` is GenAIcode's provider-neutral intermediate representation:
619
-
620
- ```ts
621
- import { image, prompt, system, toolResults, user } from 'genaicode';
622
-
623
- const items = prompt(
624
- system('Return a one-sentence caption.'),
625
- user('Describe this image.', {
626
- images: [image(base64Png, 'image/png')],
627
- }),
628
- toolResults({
629
- callId: 'lookup-1',
630
- name: 'lookup',
631
- content: JSON.stringify({ locale: 'pl-PL' }),
632
- }),
633
- );
634
-
635
- const caption = await ai(items).text();
636
- ```
637
-
638
- Applications can make domain objects prompt-aware without coupling them to a provider:
639
-
640
- ```ts
641
- import { asPrompt, system, user } from 'genaicode';
642
-
643
- const supportTicket = (ticket: Ticket) =>
644
- asPrompt(() => [system('Triage support tickets.'), user(JSON.stringify(ticket))]);
645
-
646
- await ai(supportTicket(ticket)).text();
647
- ```
648
-
649
- ### Tools
650
-
651
- ```ts
652
- const calls = await ai('What is the weather in Warsaw?')
653
- .tools([
654
- {
655
- name: 'weather',
656
- description: 'Get current weather',
657
- parameters: {
658
- type: 'object',
659
- properties: { city: { type: 'string' } },
660
- required: ['city'],
661
- additionalProperties: false,
662
- },
663
- },
664
- ])
665
- .toolCalls();
666
- ```
667
-
668
- GenAIcode normalizes tool calls but deliberately does not execute them. The application
669
- owns permissions, retries, and side effects.
670
-
671
- ### Streaming
672
-
673
- Providers that support native streaming expose a provider-neutral `StreamEvent` IR.
674
- Request builders and chains offer `.stream()` and `.streamText()`. If a provider has no
675
- `stream` method, GenAIcode synthesizes a short stream from `generate`.
676
-
677
- ```ts
678
- for await (const event of ai('Write a haiku about queues.').stream()) {
679
- if (event.type === 'text-delta') process.stdout.write(event.text);
680
- if (event.type === 'done') console.log('\n', event.result.usage);
681
- }
682
- ```
683
-
684
- Built-in adapters declare capability metadata:
685
-
686
- ```ts
687
- ai.provider.capabilities;
688
- // { streaming: true, tools: true, images: 'input', systemPrompt: true }
689
- ```
690
-
691
- ### Middleware
692
-
693
- Hook-style plugins remain the extension point. Built-in helpers cover common backend
694
- needs without restoring a global registry:
695
-
696
- ```ts
697
- import { cachePlugin, fallbackPlugin, genaicode, rateLimitPlugin, timingPlugin } from 'genaicode';
698
- import { anthropic, openai } from 'genaicode/providers';
699
-
700
- const ai = genaicode(openai({ model: 'your-model-name' }), {
701
- plugins: [
702
- timingPlugin(),
703
- rateLimitPlugin({ concurrency: 2, minIntervalMs: 50 }),
704
- cachePlugin({ maxEntries: 64 }),
705
- fallbackPlugin({ providers: [anthropic({ model: 'your-claude-model' })] }),
706
- ],
707
- });
708
- ```
709
-
710
- ### Retries
711
-
712
- Retries stay in application code. Use `classifyError`, `isRetryable`, and `withRetry`
713
- when you want shared classification without hidden policy:
714
-
715
- ```ts
716
- import { withRetry } from 'genaicode';
717
-
718
- const text = await withRetry(() => ai('Summarize the deploy notes.').text(), {
719
- attempts: 3,
720
- delayMs: 250,
721
- });
722
- ```
723
-
724
- See [retry guidance](docs/retry.md), [semver policy](docs/semver.md), and the
725
- [provider package evaluation](docs/provider-packages.md).
726
-
727
- ### Providers
728
-
729
- Models are provided in the adapter, through `.model(...)`, or with environment variables:
730
-
731
- | Adapter | Factory | Environment defaults |
732
- | ---------------------- | -------------------- | ------------------------------------------------------------------ |
733
- | OpenAI | `openai()` | `OPENAI_API_KEY`, `OPENAI_MODEL` |
734
- | Anthropic | `anthropic()` | `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL` |
735
- | Gemini API / AI Studio | `gemini()` | `GEMINI_API_KEY` or `API_KEY`, `GEMINI_MODEL` |
736
- | Vertex AI | `vertexAI()` | `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_LOCATION`, `VERTEX_AI_MODEL` |
737
- | OpenAI-compatible | `openaiCompatible()` | Explicit options; OpenAI defaults also work |
738
-
739
- ```ts
740
- import { anthropic, gemini, openai, vertexAI } from 'genaicode/providers';
741
-
742
- const viaOpenAI = genaicode(openai({ model: 'your-openai-model' }));
743
- const viaClaude = genaicode(anthropic({ model: 'your-claude-model' }));
744
- const viaGemini = genaicode(gemini({ model: 'your-gemini-model' }));
745
- const viaVertex = genaicode(
746
- vertexAI({
747
- project: 'my-google-cloud-project',
748
- location: 'global',
749
- model: 'your-vertex-model',
750
- }),
751
- );
752
- ```
753
-
754
- Provider-specific options stay at provider construction. For example, Anthropic exposes
755
- thinking and output-token defaults, while Gemini and Vertex expose SDK HTTP, auth, API
756
- version, and generation configuration options.
757
-
758
- GitHub Models, Ollama, vLLM, and other compatible gateways use the OpenAI-compatible
759
- adapter:
760
-
761
- ```ts
762
- import { openaiCompatible } from 'genaicode/providers';
763
-
764
- const local = genaicode(
765
- openaiCompatible({
766
- name: 'local',
767
- baseURL: 'http://localhost:11434/v1',
768
- apiKey: 'local',
769
- model: 'qwen3',
770
- }),
771
- );
772
- ```
773
-
774
- Implementing a provider directly is intentionally small:
775
-
776
- ```ts
777
- import type { ModelProvider } from 'genaicode';
778
-
779
- const provider: ModelProvider = {
780
- name: 'internal-gateway',
781
- async generate(request) {
782
- // Convert request.prompt to your API and normalize the response.
783
- return { parts: [{ type: 'text', text: 'response' }] };
784
- },
785
- };
786
- ```
787
-
788
- All Anthropic, Google, and OpenAI conversion functions are public from
789
- `genaicode/providers`, so gateways and tests can reuse them without instantiating clients.
790
-
791
- ### Provider E2E tests
792
-
793
- Run real-provider E2E tests locally with:
68
+ ## Install
794
69
 
795
70
  ```bash
796
- npm run test:e2e
797
- ```
798
-
799
- These tests are credential-gated and run only when provider-specific environment variables
800
- are set:
801
-
802
- - OpenAI: `OPENAI_API_KEY`, `OPENAI_MODEL`
803
- - Anthropic: `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL`
804
- - Gemini: `GEMINI_API_KEY`, `GEMINI_MODEL`
805
-
806
- Beyond the smoke call, E2E also covers portable `responseFormat: { type: 'json' }`
807
- (OpenAI, Gemini) and `thinking` (Anthropic disable; Gemini disable / `level: 'minimal'`).
808
-
809
- CI/CD is configured in `.github/workflows/provider-e2e.yaml`. Each provider runs in its own
810
- job and only starts when both required secrets are configured in GitHub Actions.
811
-
812
- ### Plugins and hooks
813
-
814
- A provider plugin no longer needs registration in global configuration. It exports the
815
- same small `ModelProvider` contract as a built-in adapter:
816
-
817
- ```ts
818
- // @acme/genaicode-bedrock
819
- import type { ModelProvider } from 'genaicode';
820
-
821
- export const bedrock = (options: BedrockOptions): ModelProvider => ({
822
- name: 'bedrock',
823
- capabilities: { streaming: false, tools: true, systemPrompt: true },
824
- async generate(request) {
825
- // Convert PromptItem[], call Bedrock, and return GenerationResult.
826
- return { parts: [{ type: 'text', text: 'response' }] };
827
- },
828
- });
829
- ```
830
-
831
- Hook-style plugins use middleware. They can observe or rewrite requests and results,
832
- handle errors, implement caches or fallbacks, and intentionally short-circuit a call.
833
- Optional `stream` middleware follows the same registration order.
834
-
835
- ```ts
836
- import { definePlugin, genaicode } from 'genaicode';
837
- import { openai } from 'genaicode/providers';
838
-
839
- const timing = definePlugin({
840
- name: 'timing',
841
- async generate(request, next) {
842
- const startedAt = performance.now();
843
- try {
844
- return await next(request);
845
- } finally {
846
- console.log('LLM request took', performance.now() - startedAt, 'ms');
847
- }
848
- },
849
- });
850
-
851
- const ai = genaicode(openai({ model: 'your-model-name' }), {
852
- plugins: [timing],
853
- });
71
+ npm install genaicode
854
72
  ```
855
73
 
856
- Plugins run in registration order, and each plugin may call `next()` once. This keeps the
857
- extension mechanism compatible with npm packages and ordinary imports without restoring
858
- the 1.x runtime TypeScript loader or process-global plugin registry.
859
-
860
- Framework-shaped examples live under [`docs/examples/`](docs/examples/) (`http-handler`, `queue-worker`,
861
- `cron-job`).
862
-
863
- ## Design boundaries
864
-
865
- - Backend library, not a coding agent. `genaicode/agents` drives external agent CLIs; it
866
- adds no agent loop, prompts, or repository tools of its own.
867
- - Provider-neutral core with no global configuration.
868
- - Explicit models and credentials; environment variables are only provider defaults.
869
- - Immutable request builders.
870
- - Conversation history is explicit; loops remain ordinary application code.
871
- - No hidden tool execution or hidden retries.
872
- - Third-party providers and middleware use stable TypeScript contracts.
873
- - Provider SDKs stay behind the `genaicode/providers` subpath; process spawning stays
874
- behind `genaicode/agents`.
875
-
876
- ## Migration from 1.x
877
-
878
- GenAIcode 1.x was a coding agent. Version 2.0 deliberately replaces that product with a
879
- small backend LLM toolkit: a jQuery-like layer for portable prompts, provider adapters,
880
- conversation chains, and plugins.
881
-
882
- The 1.x coding agent, browser UI, repository tools, and shell execution are not
883
- deprecated compatibility features; they have been removed from 2.0. The 2.x
884
- `genaicode` command only drives external agent CLIs (see [From the command line](#from-the-command-line)). The GenAIcode name, `PromptItem` model, provider converters, and extensibility
885
- continue here in a smaller and more focused form.
886
-
887
- The original coding agent remains available from the preserved
888
- [`1.x` branch](https://github.com/gtanczyk/genaicode/tree/1.x) and the 1.x npm releases:
889
-
890
- ```bash
891
- npx genaicode@1
892
- ```
74
+ Importing `genaicode` never spawns an agent, and `genaicode/agents` loads no provider SDK
75
+ and no UI code.
893
76
 
894
- In 2.x, `npx genaicode` opens a chat with the coding-agent CLIs you have installed
895
- (Claude Code, Codex, opencode...) instead of the 1.x built-in agent.
77
+ ## More
896
78
 
897
- See [the pivot plan](docs/pivot.md) for the full scope, migration decisions, and the
898
- roadmap.
79
+ - [Changelog](docs/CHANGELOG.md), [semver policy](docs/semver.md), [releasing](docs/releasing.md)
80
+ - Coming from 1.x? It was a built-in coding agent; it lives on the
81
+ [`1.x` branch](https://github.com/gtanczyk/genaicode/tree/1.x) and as `npx genaicode@1`.
82
+ See [the pivot plan](docs/pivot.md).