@onmars/lunar-agent-claude 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 +13 -0
- package/package.json +31 -0
- package/src/__tests__/adapter.test.ts +1383 -0
- package/src/adapter.ts +514 -0
- package/src/index.ts +2 -0
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import type { Agent, AgentEvent, AgentInput, AgentUsage } from '@onmars/lunar-core'
|
|
2
|
+
import { log } from '@onmars/lunar-core'
|
|
3
|
+
import type { SecurityConfig } from '@onmars/lunar-core/lib/config-loader'
|
|
4
|
+
import { buildSafeEnv } from '@onmars/lunar-core/lib/security'
|
|
5
|
+
|
|
6
|
+
export interface ClaudeAgentOptions {
|
|
7
|
+
/** Agent ID for routing — defaults to 'claude' */
|
|
8
|
+
id?: string
|
|
9
|
+
/** Display name — defaults to 'Claude Code (CLI)' */
|
|
10
|
+
name?: string
|
|
11
|
+
/** Working directory for Claude Code */
|
|
12
|
+
cwd: string
|
|
13
|
+
/** Path to claude binary (default: 'claude') */
|
|
14
|
+
binaryPath?: string
|
|
15
|
+
/** Model override (e.g., 'opus', 'sonnet') */
|
|
16
|
+
model?: string
|
|
17
|
+
/** Max turns for agentic loop */
|
|
18
|
+
maxTurns?: number
|
|
19
|
+
/** System prompt to prepend */
|
|
20
|
+
systemPrompt?: string
|
|
21
|
+
/**
|
|
22
|
+
* Auth mode:
|
|
23
|
+
* - 'stored' (default): use credentials from ~/.claude/ (claude login)
|
|
24
|
+
* - 'api-key': use ANTHROPIC_API_KEY env var
|
|
25
|
+
* - 'oauth-token': use ANTHROPIC_AUTH_TOKEN env var
|
|
26
|
+
*/
|
|
27
|
+
authMode?: 'stored' | 'api-key' | 'oauth-token'
|
|
28
|
+
/** Environment variables to pass to the Claude process */
|
|
29
|
+
env?: Record<string, string>
|
|
30
|
+
/** Security config from workspace config.yaml */
|
|
31
|
+
security?: SecurityConfig
|
|
32
|
+
/**
|
|
33
|
+
* Enable 1M context window (extended context).
|
|
34
|
+
* Appends [1m] suffix to the model flag, which tells Claude Code CLI
|
|
35
|
+
* to send the anthropic-beta: context-1m-2025-08-07 header.
|
|
36
|
+
* Pricing: same up to 200K tokens, 2x input / 1.5x output above 200K.
|
|
37
|
+
*/
|
|
38
|
+
context1m?: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Model alias → full ID mapping for Claude Code CLI.
|
|
43
|
+
*
|
|
44
|
+
* When appending [1m] for extended context, the CLI requires the full
|
|
45
|
+
* model ID (e.g., "claude-opus-4-6[1m]"), not just the alias ("opus[1m]").
|
|
46
|
+
* This map resolves common aliases before appending the suffix.
|
|
47
|
+
*/
|
|
48
|
+
export const CLAUDE_MODEL_ALIASES: Record<string, string> = {
|
|
49
|
+
opus: 'claude-opus-4-6',
|
|
50
|
+
sonnet: 'claude-sonnet-4-6',
|
|
51
|
+
'sonnet-4.5': 'claude-sonnet-4-5',
|
|
52
|
+
haiku: 'claude-haiku-4-5',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Resolve a model alias to its full ID. Returns the input if not an alias. */
|
|
56
|
+
export function resolveModelAlias(model: string): string {
|
|
57
|
+
return CLAUDE_MODEL_ALIASES[model] ?? model
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface ClaudeJsonMessage {
|
|
61
|
+
type: string
|
|
62
|
+
subtype?: string
|
|
63
|
+
session_id?: string
|
|
64
|
+
message?: {
|
|
65
|
+
role: string
|
|
66
|
+
content: Array<{
|
|
67
|
+
type: string
|
|
68
|
+
text?: string
|
|
69
|
+
thinking?: string
|
|
70
|
+
name?: string
|
|
71
|
+
input?: unknown
|
|
72
|
+
content?: unknown
|
|
73
|
+
}>
|
|
74
|
+
usage?: {
|
|
75
|
+
input_tokens?: number
|
|
76
|
+
output_tokens?: number
|
|
77
|
+
cache_read_input_tokens?: number
|
|
78
|
+
cache_creation_input_tokens?: number
|
|
79
|
+
}
|
|
80
|
+
model?: string
|
|
81
|
+
}
|
|
82
|
+
tool_use_id?: string
|
|
83
|
+
duration_ms?: number
|
|
84
|
+
duration_api_ms?: number
|
|
85
|
+
num_turns?: number
|
|
86
|
+
result?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Claude Agent — CLI spawn adapter.
|
|
91
|
+
*
|
|
92
|
+
* Spawns the official Claude Code CLI binary via Bun.spawn.
|
|
93
|
+
* Uses --output-format stream-json for real-time streaming.
|
|
94
|
+
* Auth: by default uses stored credentials from `claude login` (~/.claude/).
|
|
95
|
+
*/
|
|
96
|
+
export class ClaudeAgent implements Agent {
|
|
97
|
+
readonly id: string
|
|
98
|
+
readonly name: string
|
|
99
|
+
|
|
100
|
+
private activeProcess: ReturnType<typeof Bun.spawn> | null = null
|
|
101
|
+
|
|
102
|
+
constructor(private options: ClaudeAgentOptions) {
|
|
103
|
+
this.id = options.id ?? 'claude'
|
|
104
|
+
this.name = options.name ?? 'Claude Code (CLI)'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async init(): Promise<void> {
|
|
108
|
+
const binary = this.options.binaryPath ?? 'claude'
|
|
109
|
+
|
|
110
|
+
// Check binary exists
|
|
111
|
+
const which = Bun.spawn(['which', binary], { stdout: 'pipe', stderr: 'pipe' })
|
|
112
|
+
const exitCode = await which.exited
|
|
113
|
+
if (exitCode !== 0) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Claude CLI not found at '${binary}'. Install with: npm install -g @anthropic-ai/claude-code`,
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Verify auth works with a minimal test query
|
|
120
|
+
const env = this.buildEnv()
|
|
121
|
+
const testProc = Bun.spawn(
|
|
122
|
+
[
|
|
123
|
+
binary,
|
|
124
|
+
'-p',
|
|
125
|
+
'Reply with OK',
|
|
126
|
+
'--output-format',
|
|
127
|
+
'json',
|
|
128
|
+
'--max-turns',
|
|
129
|
+
'1',
|
|
130
|
+
'--permission-mode',
|
|
131
|
+
'bypassPermissions',
|
|
132
|
+
],
|
|
133
|
+
{ cwd: this.options.cwd, env, stdout: 'pipe', stderr: 'pipe' },
|
|
134
|
+
)
|
|
135
|
+
const testExit = await testProc.exited
|
|
136
|
+
if (testExit !== 0) {
|
|
137
|
+
const stderr = await new Response(testProc.stderr).text()
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Claude CLI auth check failed (exit ${testExit}). Run 'claude login' first.\nDetails: ${stderr.slice(0, 200)}`,
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check response for auth errors
|
|
144
|
+
const testOutput = await new Response(testProc.stdout).text()
|
|
145
|
+
try {
|
|
146
|
+
const result = JSON.parse(testOutput)
|
|
147
|
+
if (result.is_error || result.result?.includes('Invalid API key')) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
'Claude CLI auth failed: Invalid API key detected. If you have ANTHROPIC_API_KEY set in your environment, remove it — Claude Code uses stored credentials from "claude login".',
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
if (err instanceof SyntaxError) {
|
|
154
|
+
log.warn('Could not parse auth check response, proceeding anyway')
|
|
155
|
+
} else {
|
|
156
|
+
throw err
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
log.info(
|
|
161
|
+
{ cwd: this.options.cwd, binary, auth: this.options.authMode ?? 'stored' },
|
|
162
|
+
'Claude CLI agent initialized (auth verified)',
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async destroy(): Promise<void> {
|
|
167
|
+
if (this.activeProcess) {
|
|
168
|
+
this.activeProcess.kill()
|
|
169
|
+
this.activeProcess = null
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
abort(): void {
|
|
174
|
+
if (this.activeProcess) {
|
|
175
|
+
log.info('Aborting active Claude CLI process')
|
|
176
|
+
this.activeProcess.kill()
|
|
177
|
+
// Don't null activeProcess — the query() generator's finally block handles that
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async *query(input: AgentInput): AsyncGenerator<AgentEvent> {
|
|
182
|
+
const startTime = Date.now()
|
|
183
|
+
const args = this.buildArgs(input)
|
|
184
|
+
const env = this.buildEnv()
|
|
185
|
+
|
|
186
|
+
log.debug(
|
|
187
|
+
{ args: args.join(' ').slice(0, 200), sessionId: input.sessionId },
|
|
188
|
+
'Spawning claude CLI',
|
|
189
|
+
)
|
|
190
|
+
log.debug(
|
|
191
|
+
{
|
|
192
|
+
hasApiKey: 'ANTHROPIC_API_KEY' in env,
|
|
193
|
+
hasAuthToken: 'ANTHROPIC_AUTH_TOKEN' in env,
|
|
194
|
+
authMode: this.options.authMode ?? 'stored',
|
|
195
|
+
},
|
|
196
|
+
'Claude env check',
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
const proc = Bun.spawn(args, {
|
|
200
|
+
cwd: this.options.cwd,
|
|
201
|
+
env,
|
|
202
|
+
stdout: 'pipe',
|
|
203
|
+
stderr: 'pipe',
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
this.activeProcess = proc
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
yield* this.processStream(proc, startTime, input)
|
|
210
|
+
} finally {
|
|
211
|
+
this.activeProcess = null
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async health(): Promise<{ ok: boolean; latencyMs?: number; error?: string }> {
|
|
216
|
+
const start = Date.now()
|
|
217
|
+
try {
|
|
218
|
+
const proc = Bun.spawn([this.options.binaryPath ?? 'claude', '--version'], {
|
|
219
|
+
stdout: 'pipe',
|
|
220
|
+
stderr: 'pipe',
|
|
221
|
+
})
|
|
222
|
+
const exitCode = await proc.exited
|
|
223
|
+
return {
|
|
224
|
+
ok: exitCode === 0,
|
|
225
|
+
latencyMs: Date.now() - start,
|
|
226
|
+
error: exitCode !== 0 ? `Exit code ${exitCode}` : undefined,
|
|
227
|
+
}
|
|
228
|
+
} catch (err) {
|
|
229
|
+
return {
|
|
230
|
+
ok: false,
|
|
231
|
+
latencyMs: Date.now() - start,
|
|
232
|
+
error: err instanceof Error ? err.message : String(err),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// --- Private ---
|
|
238
|
+
|
|
239
|
+
private buildArgs(input: AgentInput): string[] {
|
|
240
|
+
const binary = this.options.binaryPath ?? 'claude'
|
|
241
|
+
const args: string[] = [binary]
|
|
242
|
+
|
|
243
|
+
// Print mode with prompt
|
|
244
|
+
args.push('-p', input.prompt)
|
|
245
|
+
|
|
246
|
+
// Streaming JSON output + verbose (required by CLI)
|
|
247
|
+
args.push('--output-format', 'stream-json', '--verbose')
|
|
248
|
+
|
|
249
|
+
// Session resumption
|
|
250
|
+
if (input.sessionId) {
|
|
251
|
+
args.push('--resume', input.sessionId)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// System prompt
|
|
255
|
+
const systemPrompt = input.systemPrompt ?? this.options.systemPrompt
|
|
256
|
+
if (systemPrompt) {
|
|
257
|
+
args.push('--append-system-prompt', systemPrompt)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Model override — per-query (moon) takes priority over agent-level (global)
|
|
261
|
+
const effectiveModel = input.model ?? this.options.model
|
|
262
|
+
const effectiveContext1m = input.context1m ?? this.options.context1m
|
|
263
|
+
|
|
264
|
+
if (effectiveModel) {
|
|
265
|
+
let model = effectiveModel
|
|
266
|
+
if (effectiveContext1m) {
|
|
267
|
+
// Resolve alias to full ID before appending [1m].
|
|
268
|
+
// CLI requires full ID format: "claude-opus-4-6[1m]", not "opus[1m]"
|
|
269
|
+
model = resolveModelAlias(model)
|
|
270
|
+
if (!model.includes('[1m]')) {
|
|
271
|
+
model = `${model}[1m]`
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
args.push('--model', model)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Max turns
|
|
278
|
+
if (this.options.maxTurns) {
|
|
279
|
+
args.push('--max-turns', String(this.options.maxTurns))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Permission mode
|
|
283
|
+
args.push('--permission-mode', 'bypassPermissions')
|
|
284
|
+
|
|
285
|
+
return args
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private buildEnv(): Record<string, string> {
|
|
289
|
+
const authMode = this.options.authMode ?? 'stored'
|
|
290
|
+
|
|
291
|
+
// Auth-specific env overrides
|
|
292
|
+
const authEnv: Record<string, string> = {}
|
|
293
|
+
|
|
294
|
+
if (authMode === 'stored') {
|
|
295
|
+
// Explicitly clear auth env vars so CLI uses stored credentials (~/.claude/)
|
|
296
|
+
// These are set to empty string to override any inherited values
|
|
297
|
+
} else if (authMode === 'api-key') {
|
|
298
|
+
const apiKey = process.env.ANTHROPIC_API_KEY
|
|
299
|
+
if (apiKey) authEnv.ANTHROPIC_API_KEY = apiKey
|
|
300
|
+
} else if (authMode === 'oauth-token') {
|
|
301
|
+
const authToken = process.env.ANTHROPIC_AUTH_TOKEN
|
|
302
|
+
if (authToken) authEnv.ANTHROPIC_AUTH_TOKEN = authToken
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 1M context control via CLAUDE_CODE_DISABLE_1M_CONTEXT env var.
|
|
306
|
+
// Since Claude Code v2.1.50+, 1M context is enabled by default for Opus 4.6.
|
|
307
|
+
// We only set the disable var when context1m is explicitly false.
|
|
308
|
+
const context1mEnv: Record<string, string> = {}
|
|
309
|
+
if (this.options.context1m === false) {
|
|
310
|
+
context1mEnv.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1'
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// If security config is provided, use allowlist filtering
|
|
314
|
+
if (this.options.security) {
|
|
315
|
+
const env = buildSafeEnv(
|
|
316
|
+
process.env as Record<string, string | undefined>,
|
|
317
|
+
this.options.security,
|
|
318
|
+
{ ...authEnv, ...context1mEnv, ...(this.options.env ?? {}) },
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
// For stored auth, ensure auth env vars are NOT present
|
|
322
|
+
if (authMode === 'stored') {
|
|
323
|
+
delete env.ANTHROPIC_API_KEY
|
|
324
|
+
delete env.ANTHROPIC_AUTH_TOKEN
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return env
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Fallback: legacy behavior (blocklist approach) when no security config
|
|
331
|
+
log.warn('No security config provided — using legacy blocklist env filtering')
|
|
332
|
+
const keysToRemove = new Set<string>()
|
|
333
|
+
if (authMode === 'stored') {
|
|
334
|
+
keysToRemove.add('ANTHROPIC_API_KEY')
|
|
335
|
+
keysToRemove.add('ANTHROPIC_AUTH_TOKEN')
|
|
336
|
+
} else if (authMode === 'api-key') {
|
|
337
|
+
keysToRemove.add('ANTHROPIC_AUTH_TOKEN')
|
|
338
|
+
} else if (authMode === 'oauth-token') {
|
|
339
|
+
keysToRemove.add('ANTHROPIC_API_KEY')
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const env: Record<string, string> = {}
|
|
343
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
344
|
+
if (value !== undefined && !keysToRemove.has(key)) {
|
|
345
|
+
env[key] = value
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
Object.assign(env, context1mEnv)
|
|
349
|
+
if (this.options.env) Object.assign(env, this.options.env)
|
|
350
|
+
return env
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private async *processStream(
|
|
354
|
+
proc: ReturnType<typeof Bun.spawn>,
|
|
355
|
+
startTime: number,
|
|
356
|
+
input: AgentInput,
|
|
357
|
+
): AsyncGenerator<AgentEvent> {
|
|
358
|
+
const reader = (proc.stdout as ReadableStream<Uint8Array>).getReader()
|
|
359
|
+
const decoder = new TextDecoder()
|
|
360
|
+
|
|
361
|
+
let buffer = ''
|
|
362
|
+
let sessionId = ''
|
|
363
|
+
let totalInputTokens = 0
|
|
364
|
+
let totalOutputTokens = 0
|
|
365
|
+
let totalCacheReadTokens = 0
|
|
366
|
+
let totalCacheWriteTokens = 0
|
|
367
|
+
// Last API call values — for accurate context window calculation
|
|
368
|
+
let lastCallInputTokens = 0
|
|
369
|
+
let lastCallCacheReadTokens = 0
|
|
370
|
+
let lastCallCacheWriteTokens = 0
|
|
371
|
+
let model: string | undefined
|
|
372
|
+
let hasAssistantText = false
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
while (true) {
|
|
376
|
+
const { done, value } = await reader.read()
|
|
377
|
+
if (done) break
|
|
378
|
+
|
|
379
|
+
buffer += decoder.decode(value, { stream: true })
|
|
380
|
+
|
|
381
|
+
// Process complete JSON lines
|
|
382
|
+
const lines = buffer.split('\n')
|
|
383
|
+
buffer = lines.pop() ?? ''
|
|
384
|
+
|
|
385
|
+
for (const line of lines) {
|
|
386
|
+
const trimmed = line.trim()
|
|
387
|
+
if (!trimmed) continue
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
const msg: ClaudeJsonMessage = JSON.parse(trimmed)
|
|
391
|
+
for (const evt of this.processMessage(msg)) {
|
|
392
|
+
if (evt.type === 'text') hasAssistantText = true
|
|
393
|
+
yield evt
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Fallback: if result type has text and no assistant blocks emitted text
|
|
397
|
+
if (msg.type === 'result' && msg.result && !hasAssistantText) {
|
|
398
|
+
yield { type: 'text', content: msg.result }
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (msg.session_id) sessionId = msg.session_id
|
|
402
|
+
if (msg.message?.usage) {
|
|
403
|
+
totalInputTokens += msg.message.usage.input_tokens ?? 0
|
|
404
|
+
totalOutputTokens += msg.message.usage.output_tokens ?? 0
|
|
405
|
+
totalCacheReadTokens += msg.message.usage.cache_read_input_tokens ?? 0
|
|
406
|
+
totalCacheWriteTokens += msg.message.usage.cache_creation_input_tokens ?? 0
|
|
407
|
+
// Overwrite (not accumulate) — last API call = current context state
|
|
408
|
+
lastCallInputTokens = msg.message.usage.input_tokens ?? 0
|
|
409
|
+
lastCallCacheReadTokens = msg.message.usage.cache_read_input_tokens ?? 0
|
|
410
|
+
lastCallCacheWriteTokens = msg.message.usage.cache_creation_input_tokens ?? 0
|
|
411
|
+
}
|
|
412
|
+
if (msg.message?.model) model = msg.message.model
|
|
413
|
+
} catch {
|
|
414
|
+
log.debug({ line: trimmed.slice(0, 100) }, 'Skipping non-JSON line')
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
} finally {
|
|
419
|
+
reader.releaseLock()
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const exitCode = await proc.exited
|
|
423
|
+
|
|
424
|
+
if (exitCode !== 0) {
|
|
425
|
+
const stderr = await new Response(proc.stderr as ReadableStream).text()
|
|
426
|
+
if (stderr.trim()) {
|
|
427
|
+
log.error({ exitCode, stderr: stderr.slice(0, 500) }, 'Claude CLI error')
|
|
428
|
+
if (stderr.includes('rate limit') || stderr.includes("you've hit your limit")) {
|
|
429
|
+
yield { type: 'error', error: 'Rate limited. Please wait a moment.', recoverable: true }
|
|
430
|
+
} else {
|
|
431
|
+
yield { type: 'error', error: stderr.trim(), recoverable: false }
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Only return sessionId if the query actually produced content
|
|
437
|
+
// This prevents saving corrupted sessions from failed auth/errors
|
|
438
|
+
const validSession = hasAssistantText || totalOutputTokens > 0
|
|
439
|
+
|
|
440
|
+
// Tag model with [1m] for internal catalog lookup when 1M context is active.
|
|
441
|
+
// Since Claude Code v2.1.50+, 1M is the default for Opus 4.6, so the CLI
|
|
442
|
+
// model name is unchanged. We tag internally for correct pricing/window size.
|
|
443
|
+
// Only tag real Claude models — skip synthetic/error responses.
|
|
444
|
+
const isContext1m = input.context1m ?? this.options.context1m
|
|
445
|
+
let reportedModel = model
|
|
446
|
+
if (
|
|
447
|
+
isContext1m &&
|
|
448
|
+
reportedModel &&
|
|
449
|
+
reportedModel.startsWith('claude-') &&
|
|
450
|
+
!reportedModel.includes('[1m]')
|
|
451
|
+
) {
|
|
452
|
+
reportedModel = `${reportedModel}[1m]`
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
yield {
|
|
456
|
+
type: 'done',
|
|
457
|
+
sessionId: validSession ? sessionId : '',
|
|
458
|
+
usage: {
|
|
459
|
+
inputTokens: totalInputTokens,
|
|
460
|
+
outputTokens: totalOutputTokens,
|
|
461
|
+
cacheReadTokens: totalCacheReadTokens,
|
|
462
|
+
cacheWriteTokens: totalCacheWriteTokens,
|
|
463
|
+
// Context from last API call only (not accumulated across turns)
|
|
464
|
+
contextTokens: lastCallInputTokens + lastCallCacheReadTokens + lastCallCacheWriteTokens,
|
|
465
|
+
model: reportedModel,
|
|
466
|
+
durationMs: Date.now() - startTime,
|
|
467
|
+
},
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private *processMessage(msg: ClaudeJsonMessage): Generator<AgentEvent> {
|
|
472
|
+
switch (msg.type) {
|
|
473
|
+
case 'assistant': {
|
|
474
|
+
if (!msg.message?.content) break
|
|
475
|
+
for (const block of msg.message.content) {
|
|
476
|
+
if (block.type === 'text' && block.text) {
|
|
477
|
+
yield { type: 'text', content: block.text }
|
|
478
|
+
} else if (block.type === 'thinking' && block.thinking) {
|
|
479
|
+
yield { type: 'thinking', content: block.thinking }
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
break
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
case 'tool_use': {
|
|
486
|
+
if (!msg.message?.content) break
|
|
487
|
+
for (const block of msg.message.content) {
|
|
488
|
+
if (block.type === 'tool_use' && block.name) {
|
|
489
|
+
yield { type: 'tool_use', tool: block.name, input: block.input }
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
break
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
case 'tool_result': {
|
|
496
|
+
if (!msg.message?.content) break
|
|
497
|
+
for (const block of msg.message.content) {
|
|
498
|
+
if (block.type === 'tool_result') {
|
|
499
|
+
yield { type: 'tool_result', tool: '', output: block.content }
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
break
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
case 'error': {
|
|
506
|
+
yield { type: 'error', error: msg.result ?? 'Unknown CLI error', recoverable: false }
|
|
507
|
+
break
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
case 'result':
|
|
511
|
+
break // Final result — already captured from assistant messages
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
package/src/index.ts
ADDED