otterly 0.1.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/LICENSE +21 -0
- package/README.md +247 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +75 -0
- package/dist/engine.d.ts +38 -0
- package/dist/engine.js +169 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +32 -0
- package/dist/events.d.ts +13 -0
- package/dist/events.js +168 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +10 -0
- package/dist/permissions.d.ts +16 -0
- package/dist/permissions.js +43 -0
- package/dist/server/circuit-breaker.d.ts +20 -0
- package/dist/server/circuit-breaker.js +54 -0
- package/dist/server/index.d.ts +19 -0
- package/dist/server/index.js +275 -0
- package/dist/server/logger.d.ts +18 -0
- package/dist/server/logger.js +22 -0
- package/dist/server/middleware.d.ts +20 -0
- package/dist/server/middleware.js +80 -0
- package/dist/server/openai-compat.d.ts +110 -0
- package/dist/server/openai-compat.js +158 -0
- package/dist/server/request-queue.d.ts +33 -0
- package/dist/server/request-queue.js +79 -0
- package/dist/server/routes-native.d.ts +28 -0
- package/dist/server/routes-native.js +215 -0
- package/dist/server/routes-openai.d.ts +7 -0
- package/dist/server/routes-openai.js +203 -0
- package/dist/server/session-store.d.ts +36 -0
- package/dist/server/session-store.js +87 -0
- package/dist/server/ws-handler.d.ts +7 -0
- package/dist/server/ws-handler.js +155 -0
- package/dist/session.d.ts +43 -0
- package/dist/session.js +255 -0
- package/dist/types.d.ts +100 -0
- package/dist/types.js +2 -0
- package/package.json +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Harsh Joshi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# otterly
|
|
2
|
+
|
|
3
|
+
Drop a Claude Code agent into your app in one line. Like Ollama, but instead of running a model for inference, you get a full coding agent that reads, writes, and runs code on your local machine.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install otterly @anthropic-ai/claude-code
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { claude } from 'otterly';
|
|
11
|
+
|
|
12
|
+
const result = await claude.run("Add error handling to server.ts", {
|
|
13
|
+
cwd: "./my-project",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
console.log(result.text); // "I've added try-catch blocks to all route handlers..."
|
|
17
|
+
console.log(result.cost); // 0.03
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- Node.js 18+
|
|
23
|
+
- Claude Code installed and authenticated (`claude login`)
|
|
24
|
+
- `@anthropic-ai/claude-code` installed as a peer dependency
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### One-shot
|
|
29
|
+
|
|
30
|
+
Run a task, get the result. Simplest way to use it.
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { claude } from 'otterly';
|
|
34
|
+
|
|
35
|
+
const result = await claude.run("Fix the login bug in auth.ts", {
|
|
36
|
+
cwd: "./my-project",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
console.log(result.text); // Final output text
|
|
40
|
+
console.log(result.cost); // Cost in USD
|
|
41
|
+
console.log(result.duration); // Duration in ms
|
|
42
|
+
console.log(result.sessionId); // Save this to resume later
|
|
43
|
+
console.log(result.tools); // Every tool that was used
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Streaming
|
|
47
|
+
|
|
48
|
+
Get real-time events as Claude works.
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { claude } from 'otterly';
|
|
52
|
+
|
|
53
|
+
for await (const event of claude.stream("Refactor the auth module", { cwd: "." })) {
|
|
54
|
+
switch (event.type) {
|
|
55
|
+
case "text_delta":
|
|
56
|
+
process.stdout.write(event.delta);
|
|
57
|
+
break;
|
|
58
|
+
case "tool_use":
|
|
59
|
+
console.log(`\n> ${event.description}`);
|
|
60
|
+
break;
|
|
61
|
+
case "tool_result":
|
|
62
|
+
if (event.isError) console.error(`Tool error: ${event.output}`);
|
|
63
|
+
break;
|
|
64
|
+
case "result":
|
|
65
|
+
console.log(`\nDone! Cost: $${event.cost}`);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Multi-turn Sessions
|
|
72
|
+
|
|
73
|
+
Keep conversation context alive across multiple messages.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { claude } from 'otterly';
|
|
77
|
+
|
|
78
|
+
const session = claude.session({ cwd: "./my-project" });
|
|
79
|
+
|
|
80
|
+
const r1 = await session.send("Create a REST API for users");
|
|
81
|
+
console.log(r1.text);
|
|
82
|
+
|
|
83
|
+
const r2 = await session.send("Now add authentication to it");
|
|
84
|
+
console.log(r2.text);
|
|
85
|
+
|
|
86
|
+
const r3 = await session.send("Write tests for the auth middleware");
|
|
87
|
+
console.log(r3.text);
|
|
88
|
+
|
|
89
|
+
// Save the session ID to resume later
|
|
90
|
+
console.log(session.id);
|
|
91
|
+
|
|
92
|
+
session.close();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Resume a previous session:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
const session = claude.session({
|
|
99
|
+
cwd: "./my-project",
|
|
100
|
+
resume: "previous-session-id",
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await session.send("What did we work on last time?");
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Custom Permissions
|
|
107
|
+
|
|
108
|
+
By default, otterly runs in autopilot mode (no permission prompts). You can control what Claude is allowed to do.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { claude, READONLY } from 'otterly';
|
|
112
|
+
|
|
113
|
+
// Read-only: Claude can read files but can't modify anything
|
|
114
|
+
const analysis = await claude.run("Analyze the codebase architecture", {
|
|
115
|
+
cwd: ".",
|
|
116
|
+
onPermission: READONLY,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Custom: fine-grained control
|
|
120
|
+
const result = await claude.run("Deploy to staging", {
|
|
121
|
+
cwd: ".",
|
|
122
|
+
onPermission: ({ tool, input }) => {
|
|
123
|
+
// Allow file reads and edits
|
|
124
|
+
if (["Read", "Edit", "Write", "Glob", "Grep"].includes(tool)) {
|
|
125
|
+
return { allow: true };
|
|
126
|
+
}
|
|
127
|
+
// Allow specific commands only
|
|
128
|
+
if (tool === "Bash" && input.command?.includes("npm run deploy")) {
|
|
129
|
+
return { allow: true };
|
|
130
|
+
}
|
|
131
|
+
// Deny everything else
|
|
132
|
+
return { allow: false, message: `${tool} not allowed in this context` };
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Custom Engine Instance
|
|
138
|
+
|
|
139
|
+
Set defaults for all calls.
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { ClaudeEngine } from 'otterly';
|
|
143
|
+
|
|
144
|
+
const engine = new ClaudeEngine({
|
|
145
|
+
cwd: "./my-project",
|
|
146
|
+
model: "claude-sonnet-4-20250514",
|
|
147
|
+
maxTurns: 10,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// All calls inherit the defaults
|
|
151
|
+
const r1 = await engine.run("Fix lint errors");
|
|
152
|
+
const r2 = await engine.run("Add missing types");
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Abort / Timeout
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
const controller = new AbortController();
|
|
159
|
+
setTimeout(() => controller.abort(), 30_000); // 30s timeout
|
|
160
|
+
|
|
161
|
+
const result = await claude.run("Refactor the entire test suite", {
|
|
162
|
+
cwd: ".",
|
|
163
|
+
signal: controller.signal,
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Event Types
|
|
168
|
+
|
|
169
|
+
When using `stream()` or `session.sendStream()`, you receive these events:
|
|
170
|
+
|
|
171
|
+
| Event | Fields | Description |
|
|
172
|
+
|-------|--------|-------------|
|
|
173
|
+
| `text` | `text` | Complete text from an assistant message block |
|
|
174
|
+
| `text_delta` | `delta` | Streaming text chunk (arrives in real-time) |
|
|
175
|
+
| `tool_use` | `id`, `tool`, `input`, `description` | Claude is using a tool |
|
|
176
|
+
| `tool_result` | `toolUseId`, `tool`, `output`, `isError` | Tool execution result |
|
|
177
|
+
| `system` | `sessionId`, `model`, `cwd`, `tools` | Session initialized |
|
|
178
|
+
| `result` | `text`, `cost`, `duration`, `sessionId`, `usage` | Turn complete |
|
|
179
|
+
| `error` | `error` | Something went wrong |
|
|
180
|
+
|
|
181
|
+
## Error Handling
|
|
182
|
+
|
|
183
|
+
Errors are classified with a `code` field for programmatic handling:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { claude, AgentError } from 'otterly';
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
await claude.run("Do something");
|
|
190
|
+
} catch (err) {
|
|
191
|
+
if (err instanceof AgentError) {
|
|
192
|
+
switch (err.code) {
|
|
193
|
+
case "NOT_AUTHENTICATED":
|
|
194
|
+
console.log("Run `claude login` to authenticate");
|
|
195
|
+
break;
|
|
196
|
+
case "RATE_LIMITED":
|
|
197
|
+
console.log("Wait and retry");
|
|
198
|
+
break;
|
|
199
|
+
case "SDK_NOT_FOUND":
|
|
200
|
+
console.log("npm install @anthropic-ai/claude-code");
|
|
201
|
+
break;
|
|
202
|
+
case "BILLING":
|
|
203
|
+
console.log("Check your Anthropic account");
|
|
204
|
+
break;
|
|
205
|
+
case "NETWORK":
|
|
206
|
+
console.log("Check your internet connection");
|
|
207
|
+
break;
|
|
208
|
+
case "ABORTED":
|
|
209
|
+
console.log("Operation was cancelled");
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Options
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
interface EngineOptions {
|
|
220
|
+
cwd?: string; // Working directory (default: process.cwd())
|
|
221
|
+
model?: string; // Model to use
|
|
222
|
+
permissionMode?: PermissionMode; // "default" | "acceptEdits" | "bypassPermissions" | "plan"
|
|
223
|
+
systemPrompt?: string; // Custom system prompt
|
|
224
|
+
maxTurns?: number; // Max agent turns
|
|
225
|
+
allowedTools?: string[]; // Tool whitelist
|
|
226
|
+
disallowedTools?: string[]; // Tool blacklist
|
|
227
|
+
mcpServers?: Record<string, any>;// MCP server configs
|
|
228
|
+
signal?: AbortSignal; // Cancellation signal
|
|
229
|
+
onPermission?: PermissionHandler;// Custom permission handler
|
|
230
|
+
resume?: string; // Session ID to resume
|
|
231
|
+
effort?: "low" | "medium" | "high"; // Reasoning effort
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## How It Works
|
|
236
|
+
|
|
237
|
+
otterly wraps the `@anthropic-ai/claude-code` SDK's `query()` function. It piggybacks on your existing Claude Code installation — if you've run `claude login`, you're already authenticated. No API keys to manage.
|
|
238
|
+
|
|
239
|
+
1. **`run()`** calls `query()` with your prompt, collects all events, returns the final result
|
|
240
|
+
2. **`stream()`** calls `query()` and yields normalized events as they arrive
|
|
241
|
+
3. **`session()`** uses the SDK's streaming input mode — an async generator that yields user messages on demand, keeping conversation context alive across turns in a single long-lived `query()` call
|
|
242
|
+
|
|
243
|
+
No API keys. No server. No HTTP. No WebSocket. The SDK runs in-process using your local Claude Code auth.
|
|
244
|
+
|
|
245
|
+
## License
|
|
246
|
+
|
|
247
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `otterly serve` — CLI entry point.
|
|
3
|
+
// Uses node:util parseArgs (built-in since Node 18). No commander dependency.
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { startApiServer } from "./server/index.js";
|
|
6
|
+
const { values, positionals } = parseArgs({
|
|
7
|
+
allowPositionals: true,
|
|
8
|
+
options: {
|
|
9
|
+
port: { type: "string", short: "p", default: "11434" },
|
|
10
|
+
dir: { type: "string", short: "d", default: process.cwd() },
|
|
11
|
+
"max-concurrent": { type: "string", default: "5" },
|
|
12
|
+
"max-queue": { type: "string", default: "50" },
|
|
13
|
+
"rate-limit": { type: "string", default: "60" },
|
|
14
|
+
help: { type: "boolean", short: "h", default: false },
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
const command = positionals[0] || "serve";
|
|
18
|
+
if (values.help || command === "help") {
|
|
19
|
+
console.log(`
|
|
20
|
+
otterly — local inference server for Claude Code
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
otterly serve [options] Start the API server
|
|
24
|
+
otterly help Show this help
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
-p, --port <number> Port to listen on (default: 11434)
|
|
28
|
+
-d, --dir <path> Working directory for Claude (default: cwd)
|
|
29
|
+
--max-concurrent <number> Max concurrent requests (default: 5)
|
|
30
|
+
--max-queue <number> Max queued requests (default: 50)
|
|
31
|
+
--rate-limit <number> Requests per minute per client (default: 60)
|
|
32
|
+
-h, --help Show this help
|
|
33
|
+
|
|
34
|
+
Environment:
|
|
35
|
+
OTTERLY_API_KEY Set to require Bearer auth on all requests
|
|
36
|
+
|
|
37
|
+
Endpoints:
|
|
38
|
+
POST /v1/chat/completions OpenAI-compatible (use any OpenAI client)
|
|
39
|
+
POST /api/run Native one-shot execution
|
|
40
|
+
POST /api/stream Native NDJSON streaming
|
|
41
|
+
GET /api/status Health check + queue/circuit stats
|
|
42
|
+
WS /ws Multi-turn WebSocket sessions
|
|
43
|
+
`);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
if (command === "serve") {
|
|
47
|
+
startApiServer({
|
|
48
|
+
port: parseInt(values.port, 10),
|
|
49
|
+
workingDir: values.dir,
|
|
50
|
+
maxConcurrent: parseInt(values["max-concurrent"], 10),
|
|
51
|
+
maxQueueSize: parseInt(values["max-queue"], 10),
|
|
52
|
+
requestsPerMinute: parseInt(values["rate-limit"], 10),
|
|
53
|
+
}).then((handle) => {
|
|
54
|
+
// Graceful shutdown on SIGTERM/SIGINT
|
|
55
|
+
let shutdownInitiated = false;
|
|
56
|
+
const onSignal = async () => {
|
|
57
|
+
if (shutdownInitiated) {
|
|
58
|
+
console.log("\nForced exit.");
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
shutdownInitiated = true;
|
|
62
|
+
await handle.shutdown(10_000);
|
|
63
|
+
process.exit(0);
|
|
64
|
+
};
|
|
65
|
+
process.on("SIGTERM", onSignal);
|
|
66
|
+
process.on("SIGINT", onSignal);
|
|
67
|
+
}).catch((err) => {
|
|
68
|
+
console.error("Failed to start:", err.message);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.error(`Unknown command: ${command}. Run 'otterly help' for usage.`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AgentEvent, AgentResult, EngineOptions } from "./types.js";
|
|
2
|
+
import { Session } from "./session.js";
|
|
3
|
+
export declare class ClaudeEngine {
|
|
4
|
+
private defaults;
|
|
5
|
+
constructor(defaults?: Partial<EngineOptions>);
|
|
6
|
+
private mergeOptions;
|
|
7
|
+
/**
|
|
8
|
+
* Run a prompt and get the final result. Blocks until complete.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* const result = await claude.run("Fix the login bug", { cwd: "./app" });
|
|
12
|
+
* console.log(result.text, result.cost);
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
run(prompt: string, options?: EngineOptions): Promise<AgentResult>;
|
|
16
|
+
/**
|
|
17
|
+
* Stream events from a prompt in real-time.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* for await (const event of claude.stream("Refactor auth", { cwd: "." })) {
|
|
21
|
+
* if (event.type === "text_delta") process.stdout.write(event.delta);
|
|
22
|
+
* if (event.type === "tool_use") console.log(`Using ${event.tool}...`);
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
stream(prompt: string, options?: EngineOptions): AsyncGenerator<AgentEvent>;
|
|
27
|
+
/**
|
|
28
|
+
* Create a multi-turn session. Context persists across send() calls.
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* const session = claude.session({ cwd: "./my-project" });
|
|
32
|
+
* await session.send("Create a REST API");
|
|
33
|
+
* await session.send("Now add auth to it");
|
|
34
|
+
* session.close();
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
session(options?: EngineOptions): Session;
|
|
38
|
+
}
|
package/dist/engine.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { normalizeEvents, createEventContext } from "./events.js";
|
|
2
|
+
import { classifyError, AgentError } from "./errors.js";
|
|
3
|
+
import { wrapPermissionHandler } from "./permissions.js";
|
|
4
|
+
import { Session } from "./session.js";
|
|
5
|
+
let cachedQueryFn = null;
|
|
6
|
+
async function resolveSDK() {
|
|
7
|
+
if (cachedQueryFn)
|
|
8
|
+
return cachedQueryFn;
|
|
9
|
+
// Try the primary SDK first, then the alternative package name
|
|
10
|
+
for (const pkg of ["@anthropic-ai/claude-code", "@anthropic-ai/claude-agent-sdk"]) {
|
|
11
|
+
try {
|
|
12
|
+
const mod = await import(pkg);
|
|
13
|
+
const fn = mod.query || mod.default?.query;
|
|
14
|
+
if (typeof fn === "function") {
|
|
15
|
+
cachedQueryFn = fn;
|
|
16
|
+
return cachedQueryFn;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Try next
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
throw new AgentError("SDK_NOT_FOUND", "Could not find Claude Code SDK. Install it:\n npm install @anthropic-ai/claude-code");
|
|
24
|
+
}
|
|
25
|
+
export class ClaudeEngine {
|
|
26
|
+
defaults;
|
|
27
|
+
constructor(defaults) {
|
|
28
|
+
this.defaults = defaults || {};
|
|
29
|
+
}
|
|
30
|
+
mergeOptions(options) {
|
|
31
|
+
return { ...this.defaults, ...options };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Run a prompt and get the final result. Blocks until complete.
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* const result = await claude.run("Fix the login bug", { cwd: "./app" });
|
|
38
|
+
* console.log(result.text, result.cost);
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
async run(prompt, options) {
|
|
42
|
+
const tools = [];
|
|
43
|
+
let resultText = "";
|
|
44
|
+
let cost = 0;
|
|
45
|
+
let duration = 0;
|
|
46
|
+
let sessionId = "";
|
|
47
|
+
let usage = { input_tokens: 0, output_tokens: 0 };
|
|
48
|
+
const pendingToolUses = new Map();
|
|
49
|
+
for await (const event of this.stream(prompt, options)) {
|
|
50
|
+
switch (event.type) {
|
|
51
|
+
case "text":
|
|
52
|
+
resultText = event.text;
|
|
53
|
+
break;
|
|
54
|
+
case "tool_use":
|
|
55
|
+
pendingToolUses.set(event.id, { tool: event.tool, input: event.input });
|
|
56
|
+
break;
|
|
57
|
+
case "tool_result": {
|
|
58
|
+
const pending = pendingToolUses.get(event.toolUseId);
|
|
59
|
+
tools.push({
|
|
60
|
+
tool: pending?.tool || event.tool,
|
|
61
|
+
input: pending?.input || {},
|
|
62
|
+
output: event.output,
|
|
63
|
+
isError: event.isError,
|
|
64
|
+
});
|
|
65
|
+
pendingToolUses.delete(event.toolUseId);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case "result":
|
|
69
|
+
resultText = event.text || resultText;
|
|
70
|
+
cost = event.cost;
|
|
71
|
+
duration = event.duration;
|
|
72
|
+
sessionId = event.sessionId;
|
|
73
|
+
usage = event.usage;
|
|
74
|
+
break;
|
|
75
|
+
case "error":
|
|
76
|
+
throw event.error instanceof AgentError
|
|
77
|
+
? event.error
|
|
78
|
+
: classifyError(event.error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { text: resultText, cost, duration, sessionId, usage, tools };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Stream events from a prompt in real-time.
|
|
85
|
+
*
|
|
86
|
+
* ```ts
|
|
87
|
+
* for await (const event of claude.stream("Refactor auth", { cwd: "." })) {
|
|
88
|
+
* if (event.type === "text_delta") process.stdout.write(event.delta);
|
|
89
|
+
* if (event.type === "tool_use") console.log(`Using ${event.tool}...`);
|
|
90
|
+
* }
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
async *stream(prompt, options) {
|
|
94
|
+
const opts = this.mergeOptions(options);
|
|
95
|
+
const queryFn = await resolveSDK();
|
|
96
|
+
const abortController = new AbortController();
|
|
97
|
+
if (opts.signal) {
|
|
98
|
+
opts.signal.addEventListener("abort", () => abortController.abort());
|
|
99
|
+
}
|
|
100
|
+
const queryOptions = {
|
|
101
|
+
abortController,
|
|
102
|
+
cwd: opts.cwd || process.cwd(),
|
|
103
|
+
permissionMode: opts.permissionMode || "bypassPermissions",
|
|
104
|
+
includePartialMessages: true,
|
|
105
|
+
};
|
|
106
|
+
if (opts.model)
|
|
107
|
+
queryOptions.model = opts.model;
|
|
108
|
+
if (opts.maxTurns)
|
|
109
|
+
queryOptions.maxTurns = opts.maxTurns;
|
|
110
|
+
if (opts.allowedTools)
|
|
111
|
+
queryOptions.allowedTools = opts.allowedTools;
|
|
112
|
+
if (opts.disallowedTools)
|
|
113
|
+
queryOptions.disallowedTools = opts.disallowedTools;
|
|
114
|
+
if (opts.mcpServers)
|
|
115
|
+
queryOptions.mcpServers = opts.mcpServers;
|
|
116
|
+
if (opts.effort)
|
|
117
|
+
queryOptions.effort = opts.effort;
|
|
118
|
+
if (opts.resume)
|
|
119
|
+
queryOptions.resume = opts.resume;
|
|
120
|
+
if (opts.systemPrompt)
|
|
121
|
+
queryOptions.systemPrompt = opts.systemPrompt;
|
|
122
|
+
if (opts.onPermission) {
|
|
123
|
+
queryOptions.permissionMode = "default";
|
|
124
|
+
queryOptions.canUseTool = wrapPermissionHandler(opts.onPermission);
|
|
125
|
+
}
|
|
126
|
+
const ctx = createEventContext();
|
|
127
|
+
try {
|
|
128
|
+
const queryInstance = queryFn({ prompt, options: queryOptions });
|
|
129
|
+
for await (const raw of queryInstance) {
|
|
130
|
+
if (abortController.signal.aborted)
|
|
131
|
+
break;
|
|
132
|
+
const events = normalizeEvents(raw, ctx);
|
|
133
|
+
for (const event of events) {
|
|
134
|
+
yield event;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
if (err instanceof Error &&
|
|
140
|
+
(err.name === "AbortError" || abortController.signal.aborted)) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
throw classifyError(err);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Create a multi-turn session. Context persists across send() calls.
|
|
148
|
+
*
|
|
149
|
+
* ```ts
|
|
150
|
+
* const session = claude.session({ cwd: "./my-project" });
|
|
151
|
+
* await session.send("Create a REST API");
|
|
152
|
+
* await session.send("Now add auth to it");
|
|
153
|
+
* session.close();
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
session(options) {
|
|
157
|
+
const opts = this.mergeOptions(options);
|
|
158
|
+
// Session needs the query function — resolve it lazily on first send()
|
|
159
|
+
// We wrap it in a lazy resolver to avoid top-level await
|
|
160
|
+
let resolvedFn = null;
|
|
161
|
+
const lazyQueryFn = async function* (args) {
|
|
162
|
+
if (!resolvedFn) {
|
|
163
|
+
resolvedFn = await resolveSDK();
|
|
164
|
+
}
|
|
165
|
+
yield* resolvedFn(args);
|
|
166
|
+
};
|
|
167
|
+
return new Session(lazyQueryFn, opts);
|
|
168
|
+
}
|
|
169
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type ErrorCode = "NOT_AUTHENTICATED" | "RATE_LIMITED" | "BILLING" | "NETWORK" | "ABORTED" | "SDK_NOT_FOUND" | "UNKNOWN";
|
|
2
|
+
export declare class AgentError extends Error {
|
|
3
|
+
code: ErrorCode;
|
|
4
|
+
original?: Error;
|
|
5
|
+
constructor(code: ErrorCode, message: string, original?: Error);
|
|
6
|
+
}
|
|
7
|
+
export declare function classifyError(err: unknown): AgentError;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export class AgentError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
original;
|
|
4
|
+
constructor(code, message, original) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "AgentError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.original = original;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function classifyError(err) {
|
|
12
|
+
if (err instanceof AgentError)
|
|
13
|
+
return err;
|
|
14
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
15
|
+
const msg = e.message || String(err);
|
|
16
|
+
if (msg.includes("ANTHROPIC_API_KEY") || msg.includes("authentication") || msg.includes("not logged in") || msg.includes("claude login")) {
|
|
17
|
+
return new AgentError("NOT_AUTHENTICATED", "Not authenticated. Run `claude login` to sign in.", e);
|
|
18
|
+
}
|
|
19
|
+
if (msg.includes("rate_limit") || msg.includes("429")) {
|
|
20
|
+
return new AgentError("RATE_LIMITED", "Rate limited by the API. Wait a moment and try again.", e);
|
|
21
|
+
}
|
|
22
|
+
if (msg.includes("billing") || msg.includes("402")) {
|
|
23
|
+
return new AgentError("BILLING", "Billing issue with your API key. Check your Anthropic account.", e);
|
|
24
|
+
}
|
|
25
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
26
|
+
return new AgentError("NETWORK", "Network error. Check your internet connection.", e);
|
|
27
|
+
}
|
|
28
|
+
if (e.name === "AbortError" || msg.includes("aborted")) {
|
|
29
|
+
return new AgentError("ABORTED", "Operation was cancelled.", e);
|
|
30
|
+
}
|
|
31
|
+
return new AgentError("UNKNOWN", msg, e);
|
|
32
|
+
}
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AgentEvent } from "./types.js";
|
|
2
|
+
export interface EventContext {
|
|
3
|
+
sessionId: string | null;
|
|
4
|
+
toolNames: Map<string, string>;
|
|
5
|
+
accumulatedText: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function createEventContext(): EventContext;
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a raw SDK message into clean AgentEvent(s).
|
|
10
|
+
* Returns an array because one SDK message can contain multiple content blocks.
|
|
11
|
+
*/
|
|
12
|
+
export declare function normalizeEvents(raw: Record<string, unknown>, ctx: EventContext): AgentEvent[];
|
|
13
|
+
export declare function describeToolUse(name: string, input: Record<string, unknown>): string;
|