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 +40 -856
- package/dist/cli/embed.d.ts +1 -1
- package/dist/cli/embed.js +2 -2
- package/dist/cli/embed.js.map +1 -1
- package/dist/cli/main.d.ts +1 -1
- package/dist/cli/main.js +1 -1
- package/dist/overlay/client.js +3 -1
- package/dist/overlay/client.js.map +1 -1
- package/dist/react/agent-chat.css +39 -0
- package/dist/react/agent-chat.d.ts +2 -0
- package/dist/react/agent-chat.js +2 -2
- package/dist/react/agent-chat.js.map +1 -1
- package/dist/react/markdown.js +69 -7
- package/dist/react/markdown.js.map +1 -1
- package/dist/ui/index.js +70 -68
- package/dist/ui/web-client.js +50 -11
- package/docs/CHANGELOG.md +12 -0
- package/docs/agents.md +51 -0
- package/docs/app-agent.md +71 -0
- package/docs/cli.md +105 -0
- package/docs/llm.md +460 -0
- package/docs/react.md +134 -0
- package/{skills → docs/skills}/genaicode-embed/SKILL.md +2 -1
- package/docs/vite.md +42 -0
- package/package.json +1 -2
package/docs/llm.md
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
# LLM layer (`genaicode`, `genaicode/providers`)
|
|
2
|
+
|
|
3
|
+
[← README](../README.md)
|
|
4
|
+
|
|
5
|
+
Besides driving agents, GenAIcode is a small TypeScript toolkit for calling LLMs from backend
|
|
6
|
+
code. It sits between raw provider SDKs and full agent frameworks: one prompt representation,
|
|
7
|
+
thin provider adapters, a convenient request API, and lightweight conversation chains. This
|
|
8
|
+
layer does not inspect repositories, execute shell commands, edit files, or run an agent UI.
|
|
9
|
+
|
|
10
|
+
**Like jQuery**, the common case starts with one small function and becomes more specific
|
|
11
|
+
through chaining—configure a request, follow up across multiple prompts, and keep history
|
|
12
|
+
portable without adopting a full agent framework.
|
|
13
|
+
|
|
14
|
+
## A prompt in three lines
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { genaicode } from 'genaicode';
|
|
18
|
+
import { openai } from 'genaicode/providers';
|
|
19
|
+
|
|
20
|
+
const ai = genaicode(openai({ model: 'your-model-name' }));
|
|
21
|
+
const answer = await ai('Explain why the sky is blue in two sentences.').text();
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The client is callable on purpose. Configuration is ordinary method chaining—system
|
|
25
|
+
instructions, temperature, token limits, and more—without a separate options object or
|
|
26
|
+
framework setup:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
const result = await ai('Create a release note from these commits')
|
|
30
|
+
.system('You are a concise technical writer.')
|
|
31
|
+
.temperature(0.2)
|
|
32
|
+
.maxOutputTokens(500)
|
|
33
|
+
.text();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Builders are immutable, so a configured base request can be safely reused:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const releaseNote = ai('Create a release note from these commits')
|
|
40
|
+
.system('You are a concise technical writer.')
|
|
41
|
+
.temperature(0.2);
|
|
42
|
+
|
|
43
|
+
const short = await releaseNote.maxOutputTokens(200).text();
|
|
44
|
+
const long = await releaseNote.maxOutputTokens(800).text();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Why this vs a raw SDK
|
|
48
|
+
|
|
49
|
+
Provider SDKs are the right tool when you call one vendor and own every request shape
|
|
50
|
+
yourself. The cost shows up when the same backend needs a second provider, a test double,
|
|
51
|
+
or shared policy (timeouts, JSON parsing, rate limits, fallback) without copying glue.
|
|
52
|
+
|
|
53
|
+
GenAIcode is that thin shared layer:
|
|
54
|
+
|
|
55
|
+
- One portable prompt/tool IR (`PromptItem`) instead of OpenAI messages vs Anthropic
|
|
56
|
+
blocks vs Gemini contents.
|
|
57
|
+
- One request API (`.text()`, `.json()`, `.stream()`, chains) over those adapters.
|
|
58
|
+
- A tiny `ModelProvider` seam so tests and custom gateways do not mock vendor HTTP.
|
|
59
|
+
- Opt-in middleware and retry helpers—no hidden tool runners, no silent retries, no
|
|
60
|
+
agent loop.
|
|
61
|
+
|
|
62
|
+
If you are happy importing one SDK and never swapping models or providers, stay on the
|
|
63
|
+
SDK. If you want the call site to look the same while the edge stays replaceable, use
|
|
64
|
+
GenAIcode.
|
|
65
|
+
|
|
66
|
+
## Chaining prompts
|
|
67
|
+
|
|
68
|
+
A chain remembers successful user and assistant turns. Each new prompt sees the complete
|
|
69
|
+
history, so multi-step work stays in ordinary application code:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { system } from 'genaicode';
|
|
73
|
+
|
|
74
|
+
const chain = ai.chain(system('You are helping refine an API design.'));
|
|
75
|
+
|
|
76
|
+
const answer1 = await chain.text('Prompt 1: propose a minimal endpoint.');
|
|
77
|
+
const answer2 = await chain.text('Prompt 2: add idempotency to that design.');
|
|
78
|
+
const answer3 = await chain.text('Prompt 3: summarize the final contract.');
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This is `prompt1 → response1 → prompt2 → response2 → prompt3 → response3`. The history
|
|
82
|
+
is available as portable `PromptItem[]` through `chain.history()`.
|
|
83
|
+
|
|
84
|
+
## Using a chain in a loop
|
|
85
|
+
|
|
86
|
+
A chain is ordinary application state, so normal control flow works:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const chain = ai.chain(system('Improve the draft while preserving its meaning.'));
|
|
90
|
+
let draft = 'The deployment had a problem and we fixed it.';
|
|
91
|
+
|
|
92
|
+
for (const instruction of ['Make it specific.', 'Make it concise.', 'Use a professional tone.']) {
|
|
93
|
+
draft = await chain.text(`${instruction}\n\nCurrent draft:\n${draft}`);
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Calls on one chain are serialized, even if application code starts them concurrently.
|
|
98
|
+
Failed provider calls are not added to history.
|
|
99
|
+
|
|
100
|
+
Validation and repair are also plain loops rather than a special agent abstraction:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { parseJsonResult, resultText, system } from 'genaicode';
|
|
104
|
+
|
|
105
|
+
const repair = ai.chain(system('Return only valid JSON with a non-negative count.'));
|
|
106
|
+
let count: number | undefined;
|
|
107
|
+
|
|
108
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
109
|
+
const result = await repair.ask(
|
|
110
|
+
attempt === 1 ? 'Count the actionable items in this text: ...' : 'Correct the previous response.',
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const value = parseJsonResult<{ count: number }>(result);
|
|
115
|
+
if (value.count < 0) throw new Error('count must be non-negative');
|
|
116
|
+
count = value.count;
|
|
117
|
+
break;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (attempt === 3) throw error;
|
|
120
|
+
console.warn('Invalid model response:', resultText(result));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The application owns validation, attempt limits, and failure policy.
|
|
126
|
+
|
|
127
|
+
## Schema adapters
|
|
128
|
+
|
|
129
|
+
`json(...)` and `parseJsonResult(...)` accept either a parser function or a schema adapter
|
|
130
|
+
with a `parse(value)` method. This keeps schema validation library-agnostic:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const value = await ai('Return {"count": 3}.').json({
|
|
134
|
+
parse(input) {
|
|
135
|
+
if (typeof input !== 'object' || input === null || typeof input.count !== 'number') {
|
|
136
|
+
throw new Error('Invalid shape');
|
|
137
|
+
}
|
|
138
|
+
return input;
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Calling `.json()` also sets `responseFormat: { type: 'json' }` on the request when you have
|
|
144
|
+
not already chosen a format, so providers that support JSON mode (OpenAI, Gemini/Vertex)
|
|
145
|
+
are asked for JSON rather than free text.
|
|
146
|
+
|
|
147
|
+
## Response format and thinking
|
|
148
|
+
|
|
149
|
+
Portable request fields cover the two knobs backends usually poke through provider-specific
|
|
150
|
+
config:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const verdict = await ai(promptText)
|
|
154
|
+
.responseFormat({ type: 'json' })
|
|
155
|
+
.thinking({ level: 'minimal' }) // or { budgetTokens: 0 } / false to disable
|
|
156
|
+
.temperature(0)
|
|
157
|
+
.json((value) => VerdictSchema.parse(value));
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- `responseFormat`: `{ type: 'text' | 'json' }` or
|
|
161
|
+
`{ type: 'json_schema', name, schema, strict? }`.
|
|
162
|
+
- `thinking`: `false` to disable, or `{ budgetTokens?, level? }` (`minimal` |
|
|
163
|
+
`low` | `medium` | `high`). Prefer one of budget or level — some providers reject both.
|
|
164
|
+
On Google/Gemini, `false` and `budgetTokens: 0` map to `thinkingLevel: MINIMAL`
|
|
165
|
+
(or `LOW` for models like Gemini 3.7 and Gemini 3 Pro where `MINIMAL` is unsupported)
|
|
166
|
+
because Gemini 3 rejects `thinkingBudget: 0`. JSON `responseFormat` without an
|
|
167
|
+
explicit `thinking` setting or provider `generationConfig.thinkingConfig` default also
|
|
168
|
+
defaults Google thinking to `MINIMAL` (or `LOW` on Gemini 3.7 / Pro).
|
|
169
|
+
On Anthropic, `level` becomes `output_config.effort` with adaptive thinking (Opus and
|
|
170
|
+
Sonnet 4.6+), and `false` uses whatever the model accepts: `disabled`, `between_tools`
|
|
171
|
+
(Sonnet 5.5), or effort `low` where thinking is always on (Opus 5.5, Fable). Budgets
|
|
172
|
+
become adaptive thinking on models that removed them.
|
|
173
|
+
|
|
174
|
+
Providers map what they support and ignore the rest. `ProviderCapabilities.jsonResponse`
|
|
175
|
+
and `ProviderCapabilities.thinking` advertise support. Vendor-specific escapes such as
|
|
176
|
+
Vertex `generationConfig` remain available for anything not covered here.
|
|
177
|
+
|
|
178
|
+
## PromptItem: the portable prompt IR
|
|
179
|
+
|
|
180
|
+
`PromptItem` is GenAIcode's provider-neutral intermediate representation:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import { image, prompt, system, toolResults, user } from 'genaicode';
|
|
184
|
+
|
|
185
|
+
const items = prompt(
|
|
186
|
+
system('Return a one-sentence caption.'),
|
|
187
|
+
user('Describe this image.', {
|
|
188
|
+
images: [image(base64Png, 'image/png')],
|
|
189
|
+
}),
|
|
190
|
+
toolResults({
|
|
191
|
+
callId: 'lookup-1',
|
|
192
|
+
name: 'lookup',
|
|
193
|
+
content: JSON.stringify({ locale: 'pl-PL' }),
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const caption = await ai(items).text();
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Applications can make domain objects prompt-aware without coupling them to a provider:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
import { asPrompt, system, user } from 'genaicode';
|
|
204
|
+
|
|
205
|
+
const supportTicket = (ticket: Ticket) =>
|
|
206
|
+
asPrompt(() => [system('Triage support tickets.'), user(JSON.stringify(ticket))]);
|
|
207
|
+
|
|
208
|
+
await ai(supportTicket(ticket)).text();
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Tools
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const calls = await ai('What is the weather in Warsaw?')
|
|
215
|
+
.tools([
|
|
216
|
+
{
|
|
217
|
+
name: 'weather',
|
|
218
|
+
description: 'Get current weather',
|
|
219
|
+
parameters: {
|
|
220
|
+
type: 'object',
|
|
221
|
+
properties: { city: { type: 'string' } },
|
|
222
|
+
required: ['city'],
|
|
223
|
+
additionalProperties: false,
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
])
|
|
227
|
+
.toolCalls();
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
GenAIcode normalizes tool calls but deliberately does not execute them. The application
|
|
231
|
+
owns permissions, retries, and side effects.
|
|
232
|
+
|
|
233
|
+
## Streaming
|
|
234
|
+
|
|
235
|
+
Providers that support native streaming expose a provider-neutral `StreamEvent` IR.
|
|
236
|
+
Request builders and chains offer `.stream()` and `.streamText()`. If a provider has no
|
|
237
|
+
`stream` method, GenAIcode synthesizes a short stream from `generate`.
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
for await (const event of ai('Write a haiku about queues.').stream()) {
|
|
241
|
+
if (event.type === 'text-delta') process.stdout.write(event.text);
|
|
242
|
+
if (event.type === 'done') console.log('\n', event.result.usage);
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Built-in adapters declare capability metadata:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
ai.provider.capabilities;
|
|
250
|
+
// { streaming: true, tools: true, images: 'input', systemPrompt: true }
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Middleware
|
|
254
|
+
|
|
255
|
+
Hook-style plugins remain the extension point. Built-in helpers cover common backend
|
|
256
|
+
needs without restoring a global registry:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import { cachePlugin, fallbackPlugin, genaicode, rateLimitPlugin, timingPlugin } from 'genaicode';
|
|
260
|
+
import { anthropic, openai } from 'genaicode/providers';
|
|
261
|
+
|
|
262
|
+
const ai = genaicode(openai({ model: 'your-model-name' }), {
|
|
263
|
+
plugins: [
|
|
264
|
+
timingPlugin(),
|
|
265
|
+
rateLimitPlugin({ concurrency: 2, minIntervalMs: 50 }),
|
|
266
|
+
cachePlugin({ maxEntries: 64 }),
|
|
267
|
+
fallbackPlugin({ providers: [anthropic({ model: 'your-claude-model' })] }),
|
|
268
|
+
],
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Retries
|
|
273
|
+
|
|
274
|
+
Retries stay in application code. Use `classifyError`, `isRetryable`, and `withRetry`
|
|
275
|
+
when you want shared classification without hidden policy:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
import { withRetry } from 'genaicode';
|
|
279
|
+
|
|
280
|
+
const text = await withRetry(() => ai('Summarize the deploy notes.').text(), {
|
|
281
|
+
attempts: 3,
|
|
282
|
+
delayMs: 250,
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
See [retry guidance](retry.md), [semver policy](semver.md), and the
|
|
287
|
+
[provider package evaluation](provider-packages.md).
|
|
288
|
+
|
|
289
|
+
## Providers
|
|
290
|
+
|
|
291
|
+
Models are provided in the adapter, through `.model(...)`, or with environment variables:
|
|
292
|
+
|
|
293
|
+
| Adapter | Factory | Environment defaults |
|
|
294
|
+
| ---------------------- | -------------------- | ------------------------------------------------------------------ |
|
|
295
|
+
| OpenAI | `openai()` | `OPENAI_API_KEY`, `OPENAI_MODEL` |
|
|
296
|
+
| Anthropic | `anthropic()` | `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL` |
|
|
297
|
+
| Gemini API / AI Studio | `gemini()` | `GEMINI_API_KEY` or `API_KEY`, `GEMINI_MODEL` |
|
|
298
|
+
| Vertex AI | `vertexAI()` | `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_LOCATION`, `VERTEX_AI_MODEL` |
|
|
299
|
+
| OpenAI-compatible | `openaiCompatible()` | Explicit options; OpenAI defaults also work |
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
import { anthropic, gemini, openai, vertexAI } from 'genaicode/providers';
|
|
303
|
+
|
|
304
|
+
const viaOpenAI = genaicode(openai({ model: 'your-openai-model' }));
|
|
305
|
+
const viaClaude = genaicode(anthropic({ model: 'your-claude-model' }));
|
|
306
|
+
const viaGemini = genaicode(gemini({ model: 'your-gemini-model' }));
|
|
307
|
+
const viaVertex = genaicode(
|
|
308
|
+
vertexAI({
|
|
309
|
+
project: 'my-google-cloud-project',
|
|
310
|
+
location: 'global',
|
|
311
|
+
model: 'your-vertex-model',
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Provider-specific options stay at provider construction. For example, Anthropic exposes
|
|
317
|
+
thinking and output-token defaults, while Gemini and Vertex expose SDK HTTP, auth, API
|
|
318
|
+
version, and generation configuration options.
|
|
319
|
+
|
|
320
|
+
GitHub Models, Ollama, vLLM, and other compatible gateways use the OpenAI-compatible
|
|
321
|
+
adapter:
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
import { openaiCompatible } from 'genaicode/providers';
|
|
325
|
+
|
|
326
|
+
const local = genaicode(
|
|
327
|
+
openaiCompatible({
|
|
328
|
+
name: 'local',
|
|
329
|
+
baseURL: 'http://localhost:11434/v1',
|
|
330
|
+
apiKey: 'local',
|
|
331
|
+
model: 'qwen3',
|
|
332
|
+
}),
|
|
333
|
+
);
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Implementing a provider directly is intentionally small:
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
import type { ModelProvider } from 'genaicode';
|
|
340
|
+
|
|
341
|
+
const provider: ModelProvider = {
|
|
342
|
+
name: 'internal-gateway',
|
|
343
|
+
async generate(request) {
|
|
344
|
+
// Convert request.prompt to your API and normalize the response.
|
|
345
|
+
return { parts: [{ type: 'text', text: 'response' }] };
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
All Anthropic, Google, and OpenAI conversion functions are public from
|
|
351
|
+
`genaicode/providers`, so gateways and tests can reuse them without instantiating clients.
|
|
352
|
+
|
|
353
|
+
## Provider E2E tests
|
|
354
|
+
|
|
355
|
+
Run real-provider E2E tests locally with:
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
npm run test:e2e
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
These tests are credential-gated and run only when provider-specific environment variables
|
|
362
|
+
are set:
|
|
363
|
+
|
|
364
|
+
- OpenAI: `OPENAI_API_KEY`, `OPENAI_MODEL`
|
|
365
|
+
- Anthropic: `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL`
|
|
366
|
+
- Gemini: `GEMINI_API_KEY`, `GEMINI_MODEL`
|
|
367
|
+
|
|
368
|
+
Beyond the smoke call, E2E also covers portable `responseFormat: { type: 'json' }`
|
|
369
|
+
(OpenAI, Gemini) and `thinking` (Anthropic disable; Gemini disable / `level: 'minimal'`).
|
|
370
|
+
|
|
371
|
+
CI/CD is configured in `.github/workflows/provider-e2e.yaml`. Each provider runs in its own
|
|
372
|
+
job and only starts when both required secrets are configured in GitHub Actions.
|
|
373
|
+
|
|
374
|
+
## Plugins and hooks
|
|
375
|
+
|
|
376
|
+
A provider plugin no longer needs registration in global configuration. It exports the
|
|
377
|
+
same small `ModelProvider` contract as a built-in adapter:
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
// @acme/genaicode-bedrock
|
|
381
|
+
import type { ModelProvider } from 'genaicode';
|
|
382
|
+
|
|
383
|
+
export const bedrock = (options: BedrockOptions): ModelProvider => ({
|
|
384
|
+
name: 'bedrock',
|
|
385
|
+
capabilities: { streaming: false, tools: true, systemPrompt: true },
|
|
386
|
+
async generate(request) {
|
|
387
|
+
// Convert PromptItem[], call Bedrock, and return GenerationResult.
|
|
388
|
+
return { parts: [{ type: 'text', text: 'response' }] };
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Hook-style plugins use middleware. They can observe or rewrite requests and results,
|
|
394
|
+
handle errors, implement caches or fallbacks, and intentionally short-circuit a call.
|
|
395
|
+
Optional `stream` middleware follows the same registration order.
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
import { definePlugin, genaicode } from 'genaicode';
|
|
399
|
+
import { openai } from 'genaicode/providers';
|
|
400
|
+
|
|
401
|
+
const timing = definePlugin({
|
|
402
|
+
name: 'timing',
|
|
403
|
+
async generate(request, next) {
|
|
404
|
+
const startedAt = performance.now();
|
|
405
|
+
try {
|
|
406
|
+
return await next(request);
|
|
407
|
+
} finally {
|
|
408
|
+
console.log('LLM request took', performance.now() - startedAt, 'ms');
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const ai = genaicode(openai({ model: 'your-model-name' }), {
|
|
414
|
+
plugins: [timing],
|
|
415
|
+
});
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Plugins run in registration order, and each plugin may call `next()` once. This keeps the
|
|
419
|
+
extension mechanism compatible with npm packages and ordinary imports without restoring
|
|
420
|
+
the 1.x runtime TypeScript loader or process-global plugin registry.
|
|
421
|
+
|
|
422
|
+
Framework-shaped examples live under [`docs/examples/`](examples/) (`http-handler`, `queue-worker`,
|
|
423
|
+
`cron-job`).
|
|
424
|
+
|
|
425
|
+
## Design boundaries
|
|
426
|
+
|
|
427
|
+
- Backend library, not a coding agent. `genaicode/agents` drives external agent CLIs; it
|
|
428
|
+
adds no agent loop, prompts, or repository tools of its own.
|
|
429
|
+
- Provider-neutral core with no global configuration.
|
|
430
|
+
- Explicit models and credentials; environment variables are only provider defaults.
|
|
431
|
+
- Immutable request builders.
|
|
432
|
+
- Conversation history is explicit; loops remain ordinary application code.
|
|
433
|
+
- No hidden tool execution or hidden retries.
|
|
434
|
+
- Third-party providers and middleware use stable TypeScript contracts.
|
|
435
|
+
- Provider SDKs stay behind the `genaicode/providers` subpath; process spawning stays
|
|
436
|
+
behind `genaicode/agents`.
|
|
437
|
+
|
|
438
|
+
## Migration from 1.x
|
|
439
|
+
|
|
440
|
+
GenAIcode 1.x was a coding agent. Version 2.0 deliberately replaces that product with a
|
|
441
|
+
small backend LLM toolkit: a jQuery-like layer for portable prompts, provider adapters,
|
|
442
|
+
conversation chains, and plugins.
|
|
443
|
+
|
|
444
|
+
The 1.x coding agent, browser UI, repository tools, and shell execution are not
|
|
445
|
+
deprecated compatibility features; they have been removed from 2.0. The 2.x
|
|
446
|
+
`genaicode` command only drives external agent CLIs (see [the CLI docs](cli.md)). The GenAIcode name, `PromptItem` model, provider converters, and extensibility
|
|
447
|
+
continue here in a smaller and more focused form.
|
|
448
|
+
|
|
449
|
+
The original coding agent remains available from the preserved
|
|
450
|
+
[`1.x` branch](https://github.com/gtanczyk/genaicode/tree/1.x) and the 1.x npm releases:
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
npx genaicode@1
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
In 2.x, `npx genaicode` opens a chat with the coding-agent CLIs you have installed
|
|
457
|
+
(Claude Code, Codex, opencode...) instead of the 1.x built-in agent.
|
|
458
|
+
|
|
459
|
+
See [the pivot plan](pivot.md) for the full scope, migration decisions, and the
|
|
460
|
+
roadmap.
|
package/docs/react.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# React components (`genaicode/react`)
|
|
2
|
+
|
|
3
|
+
[← README](../README.md)
|
|
4
|
+
|
|
5
|
+
`genaicode/react` is the same chat as a component. It draws a `SessionState` your app gives
|
|
6
|
+
it and calls you back; it runs, fetches and stores nothing, so your server owns the session,
|
|
7
|
+
its MCP servers and its prompts. React 18 or newer comes from your app: genaicode does not
|
|
8
|
+
declare or install it.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
// server (Node): one session, streamed to the page
|
|
12
|
+
import { createAgentSession, claude } from 'genaicode/agents';
|
|
13
|
+
|
|
14
|
+
const session = createAgentSession({
|
|
15
|
+
agent: claude(),
|
|
16
|
+
cwd: process.cwd(),
|
|
17
|
+
task: { mcpServers: [{ name: 'ops', url: 'http://127.0.0.1:4000/mcp' }] },
|
|
18
|
+
// The agent gets the context; the transcript keeps what the user typed.
|
|
19
|
+
transformPrompt: (text) => `${briefing}\n\nOperator message:\n${text}`,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
app.get('/api/agent/events', (req, res) => {
|
|
23
|
+
res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-store' });
|
|
24
|
+
const push = (state) => res.write(`data: ${JSON.stringify(state)}\n\n`);
|
|
25
|
+
push(session.get());
|
|
26
|
+
res.on('close', session.subscribe(push));
|
|
27
|
+
});
|
|
28
|
+
app.post('/api/agent/send', (req, res) => {
|
|
29
|
+
session.send(req.body.text);
|
|
30
|
+
res.json({ ok: true });
|
|
31
|
+
});
|
|
32
|
+
app.post('/api/agent/stop', (req, res) => {
|
|
33
|
+
session.stop();
|
|
34
|
+
res.json({ ok: true });
|
|
35
|
+
});
|
|
36
|
+
app.post('/api/agent/approve', (req, res) => res.json({ ok: session.approve(req.body.id, req.body.decision) }));
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
// page
|
|
41
|
+
import { useEffect, useState } from 'react';
|
|
42
|
+
import { AgentChat, type SessionState } from 'genaicode/react';
|
|
43
|
+
import 'genaicode/react/styles.css';
|
|
44
|
+
|
|
45
|
+
const post = (path: string, body: object = {}) =>
|
|
46
|
+
fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
|
|
47
|
+
|
|
48
|
+
export function AgentDrawer() {
|
|
49
|
+
const [state, setState] = useState<SessionState | null>(null);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const events = new EventSource('/api/agent/events');
|
|
52
|
+
events.onmessage = (event) => setState(JSON.parse(event.data));
|
|
53
|
+
return () => events.close();
|
|
54
|
+
}, []);
|
|
55
|
+
return (
|
|
56
|
+
<AgentChat
|
|
57
|
+
state={state}
|
|
58
|
+
onSend={(text) => post('/api/agent/send', { text })}
|
|
59
|
+
onStop={() => post('/api/agent/stop')}
|
|
60
|
+
onApprove={(id, decision) => post('/api/agent/approve', { id, decision })}
|
|
61
|
+
formatToolName={(name) => name.replace(/^mcp__ops__/, '')}
|
|
62
|
+
renderFooter={() => <MyApprovalCards />}
|
|
63
|
+
headerExtras={<button onClick={() => post('/api/agent/reset')}>New</button>}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`<AgentChat>` fills its container's height. It shows turns with streaming Markdown (no raw
|
|
70
|
+
HTML; links open in a new tab; GFM tables, strikethrough and task lists), collapsible tool calls, edited files, errors, the agent's
|
|
71
|
+
own approval requests, queued prompts and a working indicator, and an input where Enter
|
|
72
|
+
sends, Shift+Enter adds a line and Esc stops. Props:
|
|
73
|
+
|
|
74
|
+
| Prop | |
|
|
75
|
+
| ------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
|
|
76
|
+
| `state` | `SessionState`, or null before there is a session |
|
|
77
|
+
| `onSend(text)`, `onStop()` | Enter or Send; Stop or Esc. No `onStop`, no Stop button |
|
|
78
|
+
| `onApprove(id, decision)` | answers `state.approvals`; without it they show as waiting |
|
|
79
|
+
| `renderPrompt(text, turn?)` | how a sent prompt shows (return null to hide it) |
|
|
80
|
+
| `renderAfterTurn(turn, i)`, `renderFooter()` | your content after a turn, or at the end |
|
|
81
|
+
| `notices` | `{ id, afterTurn, text, tone? }[]`: your own lines between turns |
|
|
82
|
+
| `foldSteps` | fold each turn's tools and edits into one "N steps" line |
|
|
83
|
+
| `header`, `headerExtras` | replace the header (null for none), or add to its right end |
|
|
84
|
+
| `emptyState`, `placeholder`, `hint` | text before the first turn, in and under the input |
|
|
85
|
+
| `formatToolName(name)`, `disabled`, `autoFocus`, `theme`, `className`, `style` | |
|
|
86
|
+
|
|
87
|
+
Theme it with CSS custom properties on the component or any ancestor: `--gc-bg`,
|
|
88
|
+
`--gc-panel`, `--gc-border`, `--gc-text`, `--gc-muted`, `--gc-accent`, `--gc-accent-text`,
|
|
89
|
+
`--gc-error`, `--gc-font`, `--gc-mono`, `--gc-font-size`, `--gc-radius`, `--gc-max-width`
|
|
90
|
+
(and `--gc-ok`, `--gc-warn`, plus soft shades mixed from these unless set). Unset ones keep
|
|
91
|
+
the `genaicode ui` look, light or dark by `prefers-color-scheme`, or by `theme="light"` /
|
|
92
|
+
`"dark"`. `genaicode ui` and the Vite panel draw this same component.
|
|
93
|
+
|
|
94
|
+
Two smaller pieces come along for hosts that build their own panels around a transcript:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { installResizeHandle, storedSize, useFollowScroll } from 'genaicode/react';
|
|
98
|
+
|
|
99
|
+
// A log that stays at its newest line until the user scrolls up (and again after follow()).
|
|
100
|
+
const { ref, onScroll, follow } = useFollowScroll(lines);
|
|
101
|
+
|
|
102
|
+
// A drag handle on a panel's left edge; the width survives reloads.
|
|
103
|
+
const stored = storedSize('panel-width', [300, 900]);
|
|
104
|
+
let width = stored.get();
|
|
105
|
+
const handle = installResizeHandle(edge, {
|
|
106
|
+
axis: 'x',
|
|
107
|
+
direction: () => -1, // dragging left makes the panel wider
|
|
108
|
+
bounds: () => [300, Math.min(900, innerWidth - 24)],
|
|
109
|
+
read: () => width ?? 390,
|
|
110
|
+
write: (next, commit) => {
|
|
111
|
+
width = next;
|
|
112
|
+
panel.style.setProperty('--width', `${width}px`);
|
|
113
|
+
if (commit) stored.set(width);
|
|
114
|
+
},
|
|
115
|
+
reset: () => {
|
|
116
|
+
width = undefined;
|
|
117
|
+
panel.style.removeProperty('--width');
|
|
118
|
+
stored.clear();
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
// The CSS caps the shown width, so the saved one comes back when the window grows again:
|
|
122
|
+
// .panel { width: min(var(--width, 390px), calc(100vw - 24px)); }
|
|
123
|
+
window.addEventListener('resize', handle.sync);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The handle keeps its `aria-value*` range current and takes the arrow keys (Shift for bigger
|
|
127
|
+
steps), Home and End, Escape to undo a drag, and Enter or double-click to reset. While a drag
|
|
128
|
+
runs, `document.body` (or `root`) has `data-resizing="true"`, so the page can keep the cursor
|
|
129
|
+
and turn off pointer events on iframes. `read()` must return what `write` or `reset` just set,
|
|
130
|
+
so keep the size in a variable or a ref rather than only in React state.
|
|
131
|
+
|
|
132
|
+
To show the whole `genaicode ui` page in a frame instead, `startEmbeddedWeb` from
|
|
133
|
+
`genaicode/ui` serves it on 127.0.0.1 with your `task` (`mcpServers`, `timeoutMs`...), `env`
|
|
134
|
+
and `transformPrompt`, and returns its tokenized `url` and a `controller` to send prompts.
|
|
@@ -80,7 +80,8 @@ allow, batch what must run into few commands, and do not run the dev server just
|
|
|
80
80
|
you created that the project does not track.
|
|
81
81
|
6. **Report**, starting with how to see it, before anything else: one line with the exact
|
|
82
82
|
command to run and what happens next, e.g. "Run `npm run agent`: the game opens in your
|
|
83
|
-
browser with the genaicode wolf in the bottom-right corner.
|
|
83
|
+
browser with the genaicode wolf in the bottom-right corner. You can also type
|
|
84
|
+
`!npm run agent` right here." The developer may not know
|
|
84
85
|
the project's scripts or that a new one exists, so never leave this for the end of a
|
|
85
86
|
long summary. Then what you changed, file by file, briefly. Say that the agent uses its
|
|
86
87
|
own CLI login and billing.
|
package/docs/vite.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Dev overlay (`genaicode/vite`, `createDevOverlay`)
|
|
2
|
+
|
|
3
|
+
[← README](../README.md)
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
// vite.config.ts
|
|
7
|
+
import { defineConfig } from 'vite';
|
|
8
|
+
import genaicode from 'genaicode/vite';
|
|
9
|
+
|
|
10
|
+
export default defineConfig({ plugins: [genaicode()] });
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
<picture>
|
|
15
|
+
<source media="(prefers-color-scheme: dark)" srcset="media/screenshots/vite-dark.png">
|
|
16
|
+
<source media="(prefers-color-scheme: light)" srcset="media/screenshots/vite-light.png">
|
|
17
|
+
<img alt="The genaicode panel over a Vite app: the agent fixed the page's two errors." src="media/screenshots/vite-light.png" width="80%">
|
|
18
|
+
</picture>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
In `vite dev`, the app gets the wolf in its corner. It opens the same chat as `genaicode ui`
|
|
22
|
+
in a panel, with the agent working in the project root. The wolf counts the page's errors
|
|
23
|
+
(build errors, uncaught exceptions, unhandled rejections, `console.error`), and "Fix N
|
|
24
|
+
errors" sends them to the agent as one prompt. Options: `agent`, `model`, `effort`,
|
|
25
|
+
`approveAll`, `agents`, `port`, and `captureErrors: false` to leave the page's errors alone.
|
|
26
|
+
Builds are untouched. The chat runs on its own port on 127.0.0.1 even when Vite runs with
|
|
27
|
+
`--host`, lets only the app's origins frame it, and hands its token (and takes "fix" requests)
|
|
28
|
+
only from the app's own pages on this machine.
|
|
29
|
+
|
|
30
|
+
Any other Node dev server (Express, webpack-dev-server, a script that serves static files)
|
|
31
|
+
gets the same overlay from `createDevOverlay` in `genaicode/ui`:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { createDevOverlay } from 'genaicode/ui';
|
|
35
|
+
|
|
36
|
+
const overlay = createDevOverlay(); // same options as the plugin, plus `cwd`
|
|
37
|
+
app.use(overlay.middleware); // Express; or `if (await overlay.handle(req, res)) return;`
|
|
38
|
+
// ...and in the HTML it serves: overlay.injectHtml(html), or the overlay.script tag.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Development only. The token goes only to pages opened at `localhost`, `*.localhost`,
|
|
42
|
+
`127.0.0.1` or `[::1]`, and without an agent CLI the overlay stays off.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genaicode",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.0",
|
|
4
4
|
"description": "Drive the coding-agent CLIs you already have (Claude Code, Codex, Copilot, Cursor, Gemini, opencode...) behind one API and one CLI, plus a thin provider-neutral LLM layer.",
|
|
5
5
|
"author": "Grzegorz Tańczyk",
|
|
6
6
|
"repository": {
|
|
@@ -35,7 +35,6 @@
|
|
|
35
35
|
"docs",
|
|
36
36
|
"!docs/examples",
|
|
37
37
|
"!docs/media",
|
|
38
|
-
"skills",
|
|
39
38
|
"README.md",
|
|
40
39
|
"LICENSE"
|
|
41
40
|
],
|