Skip to content

Commit 5a660f8

Browse files
committed
feat: add helper factories for claude and gemini
1 parent 4dc0359 commit 5a660f8

File tree

12 files changed

+119
-69
lines changed

12 files changed

+119
-69
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## [0.14.1] - 2025-11-10
4+
### ✨ Helper APIs
5+
- Added `createHeadlessClaude()` and `createHeadlessGemini()` convenience helpers, mirroring the Codex helper so consumers can get a coder without calling `registerAdapter` manually.
6+
- Both adapters now guard their runtime entry points to ensure they only execute on the server (Node) and emit clearer errors when imported in browser builds.
7+
- Documentation and smoke tests now demonstrate the helper-based workflows, keeping framework examples (Next.js, etc.) concise.
8+
39
## [0.14.0] - 2025-11-10
410
### 🚀 Enhancements
511
- Added `createHeadlessCodex()` helper that auto-registers the adapter and returns a coder, reducing the boilerplate needed in most server runtimes.

README.md

Lines changed: 17 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,9 @@ console.log(result.text);
6767
## 🌊 Streaming Example (Claude)
6868

6969
```ts
70-
import { registerAdapter, createCoder } from '@headless-coder-sdk/core/factory';
71-
import { CODER_NAME as CLAUDE_CODER, createAdapter as createClaudeAdapter } from '@headless-coder-sdk/claude-adapter';
72-
73-
registerAdapter(CLAUDE_CODER, createClaudeAdapter);
70+
import { createHeadlessClaude } from '@headless-coder-sdk/claude-adapter';
7471

75-
const claude = createCoder(CLAUDE_CODER, {
72+
const claude = createHeadlessClaude({
7673
workingDirectory: process.cwd(),
7774
permissionMode: 'bypassPermissions',
7875
});
@@ -94,12 +91,9 @@ console.log(followUp.text);
9491
## 🧩 Structured Output Example (Gemini)
9592

9693
```ts
97-
import { registerAdapter, createCoder } from '@headless-coder-sdk/core/factory';
98-
import { CODER_NAME as GEMINI_CODER, createAdapter as createGeminiAdapter } from '@headless-coder-sdk/gemini-adapter';
99-
100-
registerAdapter(GEMINI_CODER, createGeminiAdapter);
94+
import { createHeadlessGemini } from '@headless-coder-sdk/gemini-adapter';
10195

102-
const gemini = createCoder(GEMINI_CODER, {
96+
const gemini = createHeadlessGemini({
10397
workingDirectory: process.cwd(),
10498
includeDirectories: [process.cwd()],
10599
});
@@ -150,26 +144,13 @@ console.log(followUp.text);
150144
## 🔄 Multi-Provider Workflow
151145

152146
```ts
153-
import {
154-
registerAdapter,
155-
createCoder,
156-
} from '@headless-coder-sdk/core/factory';
157-
import {
158-
CODER_NAME as CODEX,
159-
createAdapter as createCodex,
160-
} from '@headless-coder-sdk/codex-adapter';
161-
import {
162-
CODER_NAME as CLAUDE,
163-
createAdapter as createClaude,
164-
} from '@headless-coder-sdk/claude-adapter';
165-
import {
166-
CODER_NAME as GEMINI,
167-
createAdapter as createGemini,
168-
} from '@headless-coder-sdk/gemini-adapter';
169-
170-
registerAdapter(CODEX, createCodex);
171-
registerAdapter(CLAUDE, createClaude);
172-
registerAdapter(GEMINI, createGemini);
147+
import { createHeadlessCodex } from '@headless-coder-sdk/codex-adapter';
148+
import { createHeadlessClaude } from '@headless-coder-sdk/claude-adapter';
149+
import { createHeadlessGemini } from '@headless-coder-sdk/gemini-adapter';
150+
151+
const codex = createHeadlessCodex();
152+
const claude = createHeadlessClaude();
153+
const gemini = createHeadlessGemini({ workingDirectory: process.cwd() });
173154

174155
// 1) Claude + Codex perform code review concurrently and emit structured findings.
175156
const reviewSchema = {
@@ -192,7 +173,6 @@ const reviewSchema = {
192173
} as const;
193174

194175
async function runMultiProviderReview(commitHash: string) {
195-
const [claude, codex] = [createCoder(CLAUDE), createCoder(CODEX)];
196176
const [claudeThread, codexThread] = await Promise.all([
197177
claude.startThread(),
198178
codex.startThread(),
@@ -212,7 +192,6 @@ async function runMultiProviderReview(commitHash: string) {
212192
];
213193

214194
// 2) Gemini waits for both reviewers, then fixes each issue sequentially.
215-
const gemini = createCoder(GEMINI, { workingDirectory: process.cwd() });
216195
const geminiThread = await gemini.startThread();
217196

218197
for (const issue of combinedIssues) {
@@ -228,7 +207,11 @@ async function runMultiProviderReview(commitHash: string) {
228207
]);
229208
}
230209

231-
await Promise.all([claude.close?.(claudeThread), codex.close?.(codexThread), gemini.close?.(geminiThread)]);
210+
await Promise.all([
211+
claude.close?.(claudeThread),
212+
codex.close?.(codexThread),
213+
gemini.close?.(geminiThread),
214+
]);
232215
}
233216
```
234217

@@ -329,6 +312,7 @@ Open an [issue](https://github.com/OhadAssulin/headless-coder-sdk/issues) or sub
329312

330313
- Every workspace now emits flattened entry points at `dist/*.js` (ESM) and `dist/*.cjs` (CommonJS), with `.d.ts` files sitting beside them for better editor support.
331314
- You can import `createCoder` or helper utilities directly from `@headless-coder-sdk/core` and `@headless-coder-sdk/codex-adapter` without deep `dist/*/src` paths—the `main`/`module` fields now point at those root files.
315+
- Helper factories (`createHeadlessCodex/Claude/Gemini`) register adapters and return coders in one call, making server-only integrations simpler.
332316
- `package.json` is exposed via the exports map (`import '@headless-coder-sdk/core/package.json'`) for tooling that needs to inspect versions at runtime.
333317
- `@headless-coder-sdk/codex-adapter` forks a worker via `fileURLToPath(new URL('./worker.js', import.meta.url))`; keep `dist/worker.js` adjacent when rebundling so that child processes can spawn correctly.
334318

packages/claude-adapter/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ npm install @headless-coder-sdk/core @headless-coder-sdk/claude-adapter @anthrop
1111
## Usage
1212

1313
```ts
14-
import { registerAdapter, createCoder } from '@headless-coder-sdk/core';
15-
import { CODER_NAME as CLAUDE, createAdapter } from '@headless-coder-sdk/claude-adapter';
14+
import { createHeadlessClaude } from '@headless-coder-sdk/claude-adapter';
1615

17-
registerAdapter(CLAUDE, createAdapter);
18-
const coder = createCoder(CLAUDE, { permissionMode: 'bypassPermissions' });
16+
const coder = createHeadlessClaude({ permissionMode: 'bypassPermissions' });
1917

2018
const thread = await coder.startThread({ workingDirectory: process.cwd() });
2119
const result = await thread.run('Summarise the feature flag rollout plan.');
2220
console.log(result.text);
2321
```
2422

23+
`createHeadlessClaude` registers the adapter (if needed) and returns a coder so you can skip the manual `registerAdapter` boilerplate.
24+
2525
> Heads up: the Anthropic SDK requires Node 18+. Make sure the `CLAUDE_API_KEY` environment variable is available before running the adapter.

packages/claude-adapter/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@headless-coder-sdk/claude-adapter",
3-
"version": "0.14.0",
3+
"version": "0.14.1",
44
"type": "module",
55
"main": "./dist/index.cjs",
66
"module": "./dist/index.js",
@@ -19,7 +19,7 @@
1919
},
2020
"peerDependencies": {
2121
"@anthropic-ai/claude-agent-sdk": "*",
22-
"@headless-coder-sdk/core": "^0.14.0"
22+
"@headless-coder-sdk/core": "^0.14.1"
2323
},
2424
"devDependencies": {
2525
"typescript": "^5.4.0",

packages/claude-adapter/src/index.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import {
1010
type PermissionMode,
1111
} from '@anthropic-ai/claude-agent-sdk';
1212
import { randomUUID } from 'node:crypto';
13-
import { now } from '@headless-coder-sdk/core';
13+
import {
14+
now,
15+
registerAdapter,
16+
getAdapterFactory,
17+
createCoder,
18+
} from '@headless-coder-sdk/core';
1419
import type {
1520
AdapterFactory,
1621
HeadlessCoder,
@@ -31,6 +36,16 @@ export function createAdapter(defaults?: StartOpts): HeadlessCoder {
3136
}
3237
(createAdapter as AdapterFactory).coderName = CODER_NAME;
3338

39+
const isNodeRuntime = typeof process !== 'undefined' && !!process.versions?.node;
40+
41+
export function createHeadlessClaude(defaults?: StartOpts): HeadlessCoder {
42+
ensureNodeRuntime('create a Claude coder');
43+
if (!getAdapterFactory(CODER_NAME)) {
44+
registerAdapter(createAdapter as AdapterFactory);
45+
}
46+
return createCoder(CODER_NAME, defaults);
47+
}
48+
3449
interface ClaudeThreadState {
3550
sessionId: string;
3651
opts: StartOpts;
@@ -49,6 +64,12 @@ interface ActiveClaudeRun {
4964
const STRUCTURED_OUTPUT_SUFFIX =
5065
'You must respond with valid JSON that satisfies the provided schema. Do not include prose before or after the JSON.';
5166

67+
function ensureNodeRuntime(action: string): void {
68+
if (!isNodeRuntime) {
69+
throw new Error(`@headless-coder-sdk/claude-adapter can only ${action} inside Node.js.`);
70+
}
71+
}
72+
5273
function applyOutputSchemaPrompt(input: PromptInput, schema?: object): PromptInput {
5374
if (!schema) return input;
5475
const schemaSnippet = JSON.stringify(schema, null, 2);
@@ -187,8 +208,9 @@ export class ClaudeAdapter implements HeadlessCoder {
187208
*
188209
* Raises:
189210
* Error: Propagated when the Claude Agent SDK surfaces a failure event.
190-
*/
191-
private async runInternal(thread: ThreadHandle, input: PromptInput, runOpts?: RunOpts): Promise<RunResult> {
211+
*/
212+
private async runInternal(thread: ThreadHandle, input: PromptInput, runOpts?: RunOpts): Promise<RunResult> {
213+
ensureNodeRuntime('run Claude');
192214
const state = thread.internal as ClaudeThreadState;
193215
this.assertIdle(state);
194216
const structuredPrompt = applyOutputSchemaPrompt(toPrompt(input), runOpts?.outputSchema);
@@ -250,12 +272,13 @@ export class ClaudeAdapter implements HeadlessCoder {
250272
*
251273
* Raises:
252274
* Error: Propagated when the Claude Agent SDK terminates with an error.
253-
*/
254-
private runStreamedInternal(
255-
thread: ThreadHandle,
256-
input: PromptInput,
257-
runOpts?: RunOpts,
258-
): EventIterator {
275+
*/
276+
private runStreamedInternal(
277+
thread: ThreadHandle,
278+
input: PromptInput,
279+
runOpts?: RunOpts,
280+
): EventIterator {
281+
ensureNodeRuntime('stream Claude events');
259282
const state = thread.internal as ClaudeThreadState;
260283
this.assertIdle(state);
261284
const structuredPrompt = applyOutputSchemaPrompt(toPrompt(input), runOpts?.outputSchema);

packages/codex-adapter/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@headless-coder-sdk/codex-adapter",
3-
"version": "0.14.0",
3+
"version": "0.14.1",
44
"type": "module",
55
"main": "./dist/index.cjs",
66
"module": "./dist/index.js",
@@ -27,7 +27,7 @@
2727
"build": "tsup --config tsup.config.ts"
2828
},
2929
"peerDependencies": {
30-
"@headless-coder-sdk/core": "^0.14.0",
30+
"@headless-coder-sdk/core": "^0.14.1",
3131
"@openai/codex-sdk": "^0.57.0"
3232
},
3333
"devDependencies": {

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@headless-coder-sdk/core",
3-
"version": "0.14.0",
3+
"version": "0.14.1",
44
"description": "Unified SDK for headless AI coders (Codex, Claude, Gemini) with standardized threading, streaming, and sandboxing.",
55
"type": "module",
66
"main": "./dist/index.cjs",

packages/gemini-adapter/README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,18 @@ You will also need the Gemini CLI installed somewhere on your PATH (or pass `gem
1313
## Usage
1414

1515
```ts
16-
import { registerAdapter, createCoder } from '@headless-coder-sdk/core';
17-
import { CODER_NAME as GEMINI, createAdapter } from '@headless-coder-sdk/gemini-adapter';
16+
import { createHeadlessGemini } from '@headless-coder-sdk/gemini-adapter';
1817

19-
registerAdapter(GEMINI, createAdapter);
20-
const coder = createCoder(GEMINI, { includeDirectories: [process.cwd()] });
18+
const coder = createHeadlessGemini({
19+
includeDirectories: [process.cwd()],
20+
workingDirectory: process.cwd(),
21+
});
2122

2223
const thread = await coder.startThread();
2324
const result = await thread.run('List the areas of the repo that need more tests.');
2425
console.log(result.text);
2526
```
2627

27-
> Note: resume support depends on the Gemini CLI version—check the package README or upstream release notes for the latest status.
28+
`createHeadlessGemini` registers the adapter and returns a coder, so you can instantiate it inside server code without touching the registry manually.
29+
30+
> Note: resume support depends on the Gemini CLI version—check the package README or upstream release notes for the latest status. The adapter shells out via Node’s `child_process`, so keep it on the server (Next.js API routes, background workers, etc.).

packages/gemini-adapter/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@headless-coder-sdk/gemini-adapter",
3-
"version": "0.14.0",
3+
"version": "0.14.1",
44
"type": "module",
55
"main": "./dist/index.cjs",
66
"module": "./dist/index.js",
@@ -18,7 +18,7 @@
1818
"build": "tsup --config tsup.config.ts"
1919
},
2020
"peerDependencies": {
21-
"@headless-coder-sdk/core": "^0.14.0"
21+
"@headless-coder-sdk/core": "^0.14.1"
2222
},
2323
"devDependencies": {
2424
"typescript": "^5.4.0",

packages/gemini-adapter/src/index.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
import { spawn, ChildProcess } from 'node:child_process';
66
import * as readline from 'node:readline';
77
import { once } from 'node:events';
8-
import { now } from '@headless-coder-sdk/core';
8+
import {
9+
now,
10+
registerAdapter,
11+
getAdapterFactory,
12+
createCoder,
13+
} from '@headless-coder-sdk/core';
914
import type {
1015
AdapterFactory,
1116
HeadlessCoder,
@@ -26,9 +31,25 @@ export function createAdapter(defaults?: StartOpts): HeadlessCoder {
2631
}
2732
(createAdapter as AdapterFactory).coderName = CODER_NAME;
2833

34+
const isNodeRuntime = typeof process !== 'undefined' && !!process.versions?.node;
35+
36+
export function createHeadlessGemini(defaults?: StartOpts): HeadlessCoder {
37+
ensureNodeRuntime('create a Gemini coder');
38+
if (!getAdapterFactory(CODER_NAME)) {
39+
registerAdapter(createAdapter as AdapterFactory);
40+
}
41+
return createCoder(CODER_NAME, defaults);
42+
}
43+
2944
const STRUCTURED_OUTPUT_SUFFIX =
3045
'Respond with JSON that matches the provided schema. Do not include explanatory text outside the JSON.';
3146

47+
function ensureNodeRuntime(action: string): void {
48+
if (!isNodeRuntime) {
49+
throw new Error(`@headless-coder-sdk/gemini-adapter can only ${action} inside Node.js.`);
50+
}
51+
}
52+
3253
const SOFT_KILL_DELAY_MS = 250;
3354
const HARD_KILL_DELAY_MS = 1500;
3455
const DONE = Symbol('gemini-stream-done');
@@ -161,6 +182,7 @@ export class GeminiAdapter implements HeadlessCoder {
161182
* Error: When the Gemini CLI exits with a non-zero status.
162183
*/
163184
private async runInternal(handle: ThreadHandle, input: PromptInput, opts?: RunOpts): Promise<RunResult> {
185+
ensureNodeRuntime('run Gemini');
164186
const state = handle.internal as GeminiThreadState;
165187
this.assertIdle(state);
166188
const prompt = applyOutputSchemaPrompt(input, opts?.outputSchema);
@@ -207,6 +229,7 @@ export class GeminiAdapter implements HeadlessCoder {
207229
* Error: When the Gemini CLI process fails before emitting events.
208230
*/
209231
private runStreamedInternal(handle: ThreadHandle, input: PromptInput, opts?: RunOpts): EventIterator {
232+
ensureNodeRuntime('stream Gemini events');
210233
const state = handle.internal as GeminiThreadState;
211234
this.assertIdle(state);
212235
const prompt = applyOutputSchemaPrompt(input, opts?.outputSchema);

0 commit comments

Comments
 (0)