@meistrari/tela-cli 0.7.0 → 0.7.5
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 +56 -106
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/context.d.ts +2 -0
- package/dist/commands/doctor-auth.d.ts +2 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/credentials.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7027 -0
- package/dist/output.d.ts +13 -0
- package/dist/program.d.ts +2 -0
- package/package.json +17 -24
- package/scripts/install-skills.cjs +127 -0
- package/skills/tela-cli/SKILL.md +20 -0
- package/skills/tela-cli/references/agent.md +111 -0
- package/cli.ts +0 -25
- package/postinstall.sh +0 -19
- package/src/agent-api.ts +0 -300
- package/src/assets.d.ts +0 -4
- package/src/auth.ts +0 -432
- package/src/claude.ts +0 -101
- package/src/commands/doctor.ts +0 -35
- package/src/commands/feedback.ts +0 -701
- package/src/commands/metadata.ts +0 -242
- package/src/commands/push.ts +0 -208
- package/src/commands/run.ts +0 -1699
- package/src/commands/status.ts +0 -109
- package/src/commands/sync.ts +0 -245
- package/src/commit-message.ts +0 -69
- package/src/config.ts +0 -799
- package/src/diagnostics.ts +0 -813
- package/src/errors.ts +0 -122
- package/src/forgejo.ts +0 -233
- package/src/git.ts +0 -457
- package/src/ink/assets/160092__jorickhoofd__dog-bark-1.mp3 +0 -0
- package/src/ink/bark.tsx +0 -241
- package/src/ink/components.tsx +0 -178
- package/src/ink/runtime.tsx +0 -90
- package/src/ink/screens/feedback-screen.tsx +0 -395
- package/src/ink/screens/push-screen.tsx +0 -232
- package/src/ink/screens/run-screen.tsx +0 -292
- package/src/ink/screens/setup/breadcrumb.tsx +0 -24
- package/src/ink/screens/setup/setup-screen.tsx +0 -612
- package/src/ink/screens/setup/step-frame.tsx +0 -31
- package/src/ink/screens/setup/steps/done-step.tsx +0 -29
- package/src/ink/screens/setup/steps/environment-step.tsx +0 -23
- package/src/ink/screens/setup/steps/organization-step.tsx +0 -35
- package/src/ink/screens/setup/steps/review-step.tsx +0 -78
- package/src/ink/screens/setup/steps/running-step.tsx +0 -37
- package/src/ink/screens/setup/steps/ssh-key-step.tsx +0 -48
- package/src/ink/screens/setup/steps/text-field-step.tsx +0 -18
- package/src/ink/screens/setup/steps.ts +0 -156
- package/src/ink/screens/setup/types.ts +0 -65
- package/src/ink/screens/sync-screen.tsx +0 -283
- package/src/process.ts +0 -251
- package/src/program.ts +0 -2105
- package/src/runtime.ts +0 -147
- package/src/types.ts +0 -165
- package/src/ui.ts +0 -295
- package/src/version.ts +0 -3
package/src/commands/run.ts
DELETED
|
@@ -1,1699 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import { readFile } from 'node:fs/promises'
|
|
3
|
-
import { basename, join } from 'node:path'
|
|
4
|
-
import {
|
|
5
|
-
formatClaudeCliVersion,
|
|
6
|
-
inspectClaudeCli,
|
|
7
|
-
MIN_CLAUDE_VERSION_FOR_PROJECT_SCOPED_RUN,
|
|
8
|
-
} from '../claude.ts'
|
|
9
|
-
import { getRepositoryRoot } from '../git.ts'
|
|
10
|
-
import { createUserFacingError } from '../errors.ts'
|
|
11
|
-
import {
|
|
12
|
-
commandExists,
|
|
13
|
-
runCommand,
|
|
14
|
-
runInteractiveCommand,
|
|
15
|
-
runStreamingCommand,
|
|
16
|
-
spawnStreamingCommand,
|
|
17
|
-
type CommandResult,
|
|
18
|
-
type RunStreamingCommandOptions,
|
|
19
|
-
type StreamingCommandHandle,
|
|
20
|
-
} from '../process.ts'
|
|
21
|
-
import { bulletList, joinBlocks, kvList, resultCard, section, type RenderOptions } from '../ui.ts'
|
|
22
|
-
|
|
23
|
-
const INITIAL_PROMPT = 'Follow the instructions and return the result.'
|
|
24
|
-
const REQUIRED_AGENT_FILES = [
|
|
25
|
-
'CLAUDE.md',
|
|
26
|
-
'instructions.md',
|
|
27
|
-
'output-format.json',
|
|
28
|
-
'agent.config.json',
|
|
29
|
-
] as const
|
|
30
|
-
const TOOL_PROGRESS_MILESTONES = [0, 5, 15, 30] as const
|
|
31
|
-
|
|
32
|
-
export type RunMode = 'chat' | 'one-shot'
|
|
33
|
-
export type RunOutputSurface = 'plain' | 'viewer' | 'json'
|
|
34
|
-
export type RunExecutionStatus = 'starting' | 'running' | 'success' | 'failed' | 'cancelled'
|
|
35
|
-
|
|
36
|
-
export interface AgentRunConfig {
|
|
37
|
-
model?: string
|
|
38
|
-
isMultiturn: boolean
|
|
39
|
-
maxRounds?: number
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface PreparedLocalAgentRun {
|
|
43
|
-
repoRoot: string
|
|
44
|
-
config: AgentRunConfig
|
|
45
|
-
resultKind: 'json' | 'markdown'
|
|
46
|
-
resultPath: string
|
|
47
|
-
mcpConfigPath?: string
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface RunStepSummary {
|
|
51
|
-
id: string
|
|
52
|
-
kind: 'assistant' | 'tool' | 'status' | 'retry' | 'hook' | 'task' | 'artifact' | 'result'
|
|
53
|
-
summary: string
|
|
54
|
-
detail?: string
|
|
55
|
-
notable?: boolean
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface RunActiveTool {
|
|
59
|
-
id: string
|
|
60
|
-
name: string
|
|
61
|
-
summary: string
|
|
62
|
-
detail?: string
|
|
63
|
-
elapsedSeconds: number
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface RunExecutionSummary {
|
|
67
|
-
status: RunExecutionStatus
|
|
68
|
-
durationMs?: number
|
|
69
|
-
durationApiMs?: number
|
|
70
|
-
numTurns?: number
|
|
71
|
-
stopReason?: string | null
|
|
72
|
-
totalCostUsd?: number
|
|
73
|
-
permissionDenials: string[]
|
|
74
|
-
errors: string[]
|
|
75
|
-
notableEvents: string[]
|
|
76
|
-
structuredOutputPresent: boolean
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface RunArtifactSummary {
|
|
80
|
-
path: string
|
|
81
|
-
kind: 'json' | 'markdown'
|
|
82
|
-
value: unknown
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export interface RunCommandResult {
|
|
86
|
-
ok: true
|
|
87
|
-
repoRoot: string
|
|
88
|
-
mode: RunMode
|
|
89
|
-
config: AgentRunConfig
|
|
90
|
-
execution: RunExecutionSummary
|
|
91
|
-
steps: RunStepSummary[]
|
|
92
|
-
artifact: RunArtifactSummary
|
|
93
|
-
resultPath: string
|
|
94
|
-
resultKind: 'json' | 'markdown'
|
|
95
|
-
resultValue: unknown
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export interface RunExecutionSnapshot {
|
|
99
|
-
repoRoot: string
|
|
100
|
-
mode: RunMode
|
|
101
|
-
model?: string
|
|
102
|
-
artifactPath: string
|
|
103
|
-
startedAtMs: number
|
|
104
|
-
status: RunExecutionStatus
|
|
105
|
-
currentActivity: string
|
|
106
|
-
steps: RunStepSummary[]
|
|
107
|
-
activeTools: RunActiveTool[]
|
|
108
|
-
execution: RunExecutionSummary
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export interface RunExecutionControl {
|
|
112
|
-
interrupt(): void
|
|
113
|
-
kill(): void
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export interface RunLocalAgentCommandOptions {
|
|
117
|
-
cwd?: string
|
|
118
|
-
contract?: PreparedLocalAgentRun
|
|
119
|
-
surface?: RunOutputSurface
|
|
120
|
-
commandExists?: typeof commandExists
|
|
121
|
-
inspectClaudeCli?: typeof inspectClaudeCli
|
|
122
|
-
runCommand?: typeof runCommand
|
|
123
|
-
runInteractiveCommand?: typeof runInteractiveCommand
|
|
124
|
-
runStreamingCommand?: typeof runStreamingCommand
|
|
125
|
-
spawnStreamingCommand?: typeof spawnStreamingCommand
|
|
126
|
-
writeStdout?: (message: string) => void
|
|
127
|
-
writeStderr?: (message: string) => void
|
|
128
|
-
onSnapshot?: (snapshot: RunExecutionSnapshot) => void
|
|
129
|
-
onControl?: (control: RunExecutionControl) => void
|
|
130
|
-
stdinIsTTY?: boolean
|
|
131
|
-
stdoutIsTTY?: boolean
|
|
132
|
-
verbose?: boolean
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
interface AssistantToolUse {
|
|
136
|
-
id: string
|
|
137
|
-
name: string
|
|
138
|
-
input: unknown
|
|
139
|
-
summary: string
|
|
140
|
-
detail?: string
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
interface ClaudeResultEvent {
|
|
144
|
-
type?: string
|
|
145
|
-
subtype?: string
|
|
146
|
-
duration_ms?: unknown
|
|
147
|
-
duration_api_ms?: unknown
|
|
148
|
-
is_error?: boolean
|
|
149
|
-
num_turns?: unknown
|
|
150
|
-
stop_reason?: unknown
|
|
151
|
-
total_cost_usd?: unknown
|
|
152
|
-
permission_denials?: unknown
|
|
153
|
-
errors?: unknown
|
|
154
|
-
structured_output?: unknown
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
type RunEvent =
|
|
158
|
-
| {
|
|
159
|
-
kind: 'session_init'
|
|
160
|
-
summary: string
|
|
161
|
-
detail?: string
|
|
162
|
-
model?: string
|
|
163
|
-
recordStep: boolean
|
|
164
|
-
}
|
|
165
|
-
| {
|
|
166
|
-
kind: 'assistant_text'
|
|
167
|
-
text: string
|
|
168
|
-
summary: string
|
|
169
|
-
detail?: string
|
|
170
|
-
recordStep: true
|
|
171
|
-
}
|
|
172
|
-
| {
|
|
173
|
-
kind: 'tool_batch'
|
|
174
|
-
summary: string
|
|
175
|
-
detail?: string
|
|
176
|
-
tools: RunActiveTool[]
|
|
177
|
-
recordStep: true
|
|
178
|
-
}
|
|
179
|
-
| {
|
|
180
|
-
kind: 'tool_progress'
|
|
181
|
-
toolUseId: string
|
|
182
|
-
toolName: string
|
|
183
|
-
summary: string
|
|
184
|
-
detail?: string
|
|
185
|
-
elapsedSeconds: number
|
|
186
|
-
recordStep: false
|
|
187
|
-
}
|
|
188
|
-
| {
|
|
189
|
-
kind: 'status_notice'
|
|
190
|
-
summary: string
|
|
191
|
-
detail?: string
|
|
192
|
-
recordStep: boolean
|
|
193
|
-
notable?: boolean
|
|
194
|
-
}
|
|
195
|
-
| {
|
|
196
|
-
kind: 'retry_notice'
|
|
197
|
-
summary: string
|
|
198
|
-
detail?: string
|
|
199
|
-
recordStep: true
|
|
200
|
-
notable: true
|
|
201
|
-
}
|
|
202
|
-
| {
|
|
203
|
-
kind: 'hook_notice'
|
|
204
|
-
summary: string
|
|
205
|
-
detail?: string
|
|
206
|
-
recordStep: boolean
|
|
207
|
-
}
|
|
208
|
-
| {
|
|
209
|
-
kind: 'task_notice'
|
|
210
|
-
summary: string
|
|
211
|
-
detail?: string
|
|
212
|
-
recordStep: true
|
|
213
|
-
notable?: boolean
|
|
214
|
-
}
|
|
215
|
-
| {
|
|
216
|
-
kind: 'artifact_notice'
|
|
217
|
-
summary: string
|
|
218
|
-
detail?: string
|
|
219
|
-
recordStep: boolean
|
|
220
|
-
notable?: boolean
|
|
221
|
-
}
|
|
222
|
-
| {
|
|
223
|
-
kind: 'result'
|
|
224
|
-
summary: string
|
|
225
|
-
detail?: string
|
|
226
|
-
recordStep: true
|
|
227
|
-
execution: Partial<RunExecutionSummary> & Pick<RunExecutionSummary, 'status'>
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function writeLine(
|
|
231
|
-
writer: ((message: string) => void) | undefined,
|
|
232
|
-
line: string,
|
|
233
|
-
): void {
|
|
234
|
-
const message = `${line}\n`
|
|
235
|
-
if (writer) {
|
|
236
|
-
writer(message)
|
|
237
|
-
return
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
process.stdout.write(message)
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function writeErrorLine(
|
|
244
|
-
writer: ((message: string) => void) | undefined,
|
|
245
|
-
line: string,
|
|
246
|
-
): void {
|
|
247
|
-
const message = `${line}\n`
|
|
248
|
-
if (writer) {
|
|
249
|
-
writer(message)
|
|
250
|
-
return
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
process.stderr.write(message)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function splitVisibleLines(value: string): string[] {
|
|
257
|
-
return value
|
|
258
|
-
.split(/\r?\n/)
|
|
259
|
-
.map(line => line.trim())
|
|
260
|
-
.filter(Boolean)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function truncate(value: string, maxLength = 96): string {
|
|
264
|
-
const normalized = value.replace(/\s+/g, ' ').trim()
|
|
265
|
-
if (normalized.length <= maxLength) {
|
|
266
|
-
return normalized
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function safeString(value: unknown): string | undefined {
|
|
273
|
-
return typeof value === 'string' && value.trim().length > 0
|
|
274
|
-
? value.trim()
|
|
275
|
-
: undefined
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function safeNumber(value: unknown): number | undefined {
|
|
279
|
-
return typeof value === 'number' && Number.isFinite(value)
|
|
280
|
-
? value
|
|
281
|
-
: undefined
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
285
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function formatDurationMs(durationMs: number | undefined): string {
|
|
289
|
-
if (!durationMs || durationMs <= 0) {
|
|
290
|
-
return '<unknown>'
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (durationMs < 1000) {
|
|
294
|
-
return `${durationMs}ms`
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const seconds = durationMs / 1000
|
|
298
|
-
if (seconds < 60) {
|
|
299
|
-
return `${seconds.toFixed(seconds >= 10 ? 1 : 2)}s`
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const minutes = Math.floor(seconds / 60)
|
|
303
|
-
const remainingSeconds = Math.round(seconds % 60)
|
|
304
|
-
return `${minutes}m ${remainingSeconds}s`
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function formatElapsedSeconds(seconds: number): string {
|
|
308
|
-
if (seconds < 60) {
|
|
309
|
-
return `${seconds}s`
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const minutes = Math.floor(seconds / 60)
|
|
313
|
-
const remainder = seconds % 60
|
|
314
|
-
return `${minutes}m ${remainder}s`
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function summarizePermissionDenial(value: unknown): string {
|
|
318
|
-
if (!isRecord(value)) {
|
|
319
|
-
return 'Permission denied for a tool call.'
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const toolName = safeString(value.tool_name) ?? 'tool'
|
|
323
|
-
if (!isRecord(value.tool_input)) {
|
|
324
|
-
return `Permission denied: ${toolName}`
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const inputSummary = summarizeToolInput(toolName, value.tool_input)
|
|
328
|
-
return inputSummary ? `Permission denied: ${toolName} ${inputSummary}` : `Permission denied: ${toolName}`
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function parsePermissionDenials(value: unknown): string[] {
|
|
332
|
-
if (!Array.isArray(value)) {
|
|
333
|
-
return []
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
return value.map(summarizePermissionDenial)
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function summarizeLines(value: string): { summary: string, detail?: string } {
|
|
340
|
-
const lines = splitVisibleLines(value)
|
|
341
|
-
if (lines.length === 0) {
|
|
342
|
-
return { summary: '<empty>' }
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const summary = truncate(lines[0] ?? '')
|
|
346
|
-
if (lines.length === 1 && summary === lines[0]) {
|
|
347
|
-
return { summary }
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return {
|
|
351
|
-
summary,
|
|
352
|
-
detail: lines.join('\n'),
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function summarizeStructuredOutputPresence(value: unknown): boolean {
|
|
357
|
-
return value !== undefined
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function summarizeToolInput(toolName: string, input: unknown): string | undefined {
|
|
361
|
-
if (!isRecord(input)) {
|
|
362
|
-
return undefined
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const pathValue = [
|
|
366
|
-
input.file_path,
|
|
367
|
-
input.path,
|
|
368
|
-
input.filename,
|
|
369
|
-
input.file,
|
|
370
|
-
input.target_file,
|
|
371
|
-
input.to_path,
|
|
372
|
-
input.from_path,
|
|
373
|
-
].find(candidate => typeof candidate === 'string' && candidate.trim().length > 0)
|
|
374
|
-
if (typeof pathValue === 'string') {
|
|
375
|
-
return truncate(basename(pathValue))
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const commandValue = [
|
|
379
|
-
input.command,
|
|
380
|
-
input.cmd,
|
|
381
|
-
input.script,
|
|
382
|
-
input.shell_command,
|
|
383
|
-
].find(candidate => typeof candidate === 'string' && candidate.trim().length > 0)
|
|
384
|
-
if (typeof commandValue === 'string') {
|
|
385
|
-
return truncate(commandValue.replace(/\s+/g, ' '))
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const queryValue = [
|
|
389
|
-
input.pattern,
|
|
390
|
-
input.query,
|
|
391
|
-
input.q,
|
|
392
|
-
input.search_query,
|
|
393
|
-
].find(candidate => typeof candidate === 'string' && candidate.trim().length > 0)
|
|
394
|
-
if (typeof queryValue === 'string') {
|
|
395
|
-
return truncate(queryValue)
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const urlValue = [
|
|
399
|
-
input.url,
|
|
400
|
-
input.href,
|
|
401
|
-
].find(candidate => typeof candidate === 'string' && candidate.trim().length > 0)
|
|
402
|
-
if (typeof urlValue === 'string') {
|
|
403
|
-
return truncate(urlValue)
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const locationValue = safeString(input.location)
|
|
407
|
-
if (locationValue) {
|
|
408
|
-
return truncate(locationValue)
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const titleValue = safeString(input.title)
|
|
412
|
-
if (titleValue) {
|
|
413
|
-
return truncate(titleValue)
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const pageIndex = safeNumber(input.page_index)
|
|
417
|
-
if (pageIndex != null) {
|
|
418
|
-
return `${toolName} page ${pageIndex}`
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
return undefined
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function summarizeToolUse(name: string, input: unknown): { summary: string, detail?: string } {
|
|
425
|
-
const detail = summarizeToolInput(name, input)
|
|
426
|
-
return detail
|
|
427
|
-
? { summary: truncate(`${name} ${detail}`), detail: `${name}: ${detail}` }
|
|
428
|
-
: { summary: name }
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function extractAssistantText(message: unknown): string {
|
|
432
|
-
if (!message || typeof message !== 'object') {
|
|
433
|
-
return ''
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const content = (message as { content?: unknown }).content
|
|
437
|
-
if (typeof content === 'string') {
|
|
438
|
-
return content
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
if (!Array.isArray(content)) {
|
|
442
|
-
return ''
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
return content
|
|
446
|
-
.flatMap((block) => {
|
|
447
|
-
if (!isRecord(block) || block.type !== 'text') {
|
|
448
|
-
return []
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
return typeof block.text === 'string' ? [block.text] : []
|
|
452
|
-
})
|
|
453
|
-
.join('\n')
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function extractAssistantToolUses(message: unknown): AssistantToolUse[] {
|
|
457
|
-
if (!message || typeof message !== 'object') {
|
|
458
|
-
return []
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const content = (message as { content?: unknown }).content
|
|
462
|
-
if (!Array.isArray(content)) {
|
|
463
|
-
return []
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
return content.flatMap((block, index) => {
|
|
467
|
-
if (!isRecord(block) || block.type !== 'tool_use') {
|
|
468
|
-
return []
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
const name = safeString(block.name)
|
|
472
|
-
if (!name) {
|
|
473
|
-
return []
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
const { summary, detail } = summarizeToolUse(name, block.input)
|
|
477
|
-
return [{
|
|
478
|
-
id: safeString(block.id) ?? `${name}-${index}`,
|
|
479
|
-
name,
|
|
480
|
-
input: block.input,
|
|
481
|
-
summary,
|
|
482
|
-
detail,
|
|
483
|
-
}]
|
|
484
|
-
})
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function capitalize(value: string): string {
|
|
488
|
-
return value.length > 0
|
|
489
|
-
? `${value[0]?.toUpperCase() ?? ''}${value.slice(1)}`
|
|
490
|
-
: value
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function pluralize(count: number, singular: string, plural = `${singular}s`): string {
|
|
494
|
-
return `${count} ${count === 1 ? singular : plural}`
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function categorizeToolName(name: string): 'searches' | 'reads' | 'writes' | 'commands' | 'other' {
|
|
498
|
-
const normalized = name.toLowerCase()
|
|
499
|
-
|
|
500
|
-
if (/(grep|glob|search|lsp|find)/.test(normalized)) {
|
|
501
|
-
return 'searches'
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (/(read|fetch|get|open|view)/.test(normalized)) {
|
|
505
|
-
return 'reads'
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
if (/(write|edit|patch|replace|update|insert|delete|move|format|apply)/.test(normalized)) {
|
|
509
|
-
return 'writes'
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
if (/(bash|shell|exec|command|tmux|terminal)/.test(normalized)) {
|
|
513
|
-
return 'commands'
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
return 'other'
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function buildDerivedToolSummary(toolUses: AssistantToolUse[]): string {
|
|
520
|
-
if (toolUses.length === 1) {
|
|
521
|
-
return toolUses[0]?.summary ?? 'Tool call'
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const counts = {
|
|
525
|
-
searches: 0,
|
|
526
|
-
reads: 0,
|
|
527
|
-
writes: 0,
|
|
528
|
-
commands: 0,
|
|
529
|
-
other: 0,
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
for (const toolUse of toolUses) {
|
|
533
|
-
counts[categorizeToolName(toolUse.name)] += 1
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
const parts: string[] = []
|
|
537
|
-
if (counts.searches > 0) {
|
|
538
|
-
parts.push(`searched ${pluralize(counts.searches, 'pattern')}`)
|
|
539
|
-
}
|
|
540
|
-
if (counts.reads > 0) {
|
|
541
|
-
parts.push(`read ${pluralize(counts.reads, 'file')}`)
|
|
542
|
-
}
|
|
543
|
-
if (counts.writes > 0) {
|
|
544
|
-
parts.push(`wrote ${pluralize(counts.writes, 'file')}`)
|
|
545
|
-
}
|
|
546
|
-
if (counts.commands > 0) {
|
|
547
|
-
parts.push(`ran ${pluralize(counts.commands, 'command')}`)
|
|
548
|
-
}
|
|
549
|
-
if (counts.other > 0) {
|
|
550
|
-
parts.push(`${pluralize(counts.other, 'other tool')}`)
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
return capitalize(parts.join(', '))
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
function buildToolBatchEvent(
|
|
557
|
-
summary: string,
|
|
558
|
-
toolUses: AssistantToolUse[],
|
|
559
|
-
): RunEvent {
|
|
560
|
-
const detailLines = toolUses
|
|
561
|
-
.map(toolUse => toolUse.detail ?? toolUse.summary)
|
|
562
|
-
.filter(Boolean)
|
|
563
|
-
return {
|
|
564
|
-
kind: 'tool_batch',
|
|
565
|
-
summary,
|
|
566
|
-
detail: detailLines.length > 0 ? detailLines.join('\n') : undefined,
|
|
567
|
-
tools: toolUses.map(toolUse => ({
|
|
568
|
-
id: toolUse.id,
|
|
569
|
-
name: toolUse.name,
|
|
570
|
-
summary: toolUse.summary,
|
|
571
|
-
detail: toolUse.detail,
|
|
572
|
-
elapsedSeconds: 0,
|
|
573
|
-
})),
|
|
574
|
-
recordStep: true,
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function normalizeClaudePrintEvent(event: Record<string, unknown>): RunEvent[] {
|
|
579
|
-
switch (event.type) {
|
|
580
|
-
case 'system': {
|
|
581
|
-
switch (event.subtype) {
|
|
582
|
-
case 'init':
|
|
583
|
-
return [{
|
|
584
|
-
kind: 'session_init',
|
|
585
|
-
summary: 'Claude session started.',
|
|
586
|
-
detail: safeString(event.cwd),
|
|
587
|
-
model: safeString(event.model),
|
|
588
|
-
recordStep: true,
|
|
589
|
-
}]
|
|
590
|
-
case 'status': {
|
|
591
|
-
const status = safeString(event.status)
|
|
592
|
-
if (!status) {
|
|
593
|
-
return []
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
return [{
|
|
597
|
-
kind: 'status_notice',
|
|
598
|
-
summary: status === 'compacting' ? 'Compacting conversation…' : `Status: ${status}`,
|
|
599
|
-
recordStep: status === 'compacting',
|
|
600
|
-
notable: status === 'compacting',
|
|
601
|
-
}]
|
|
602
|
-
}
|
|
603
|
-
case 'api_retry': {
|
|
604
|
-
const attempt = safeNumber(event.attempt) ?? '?'
|
|
605
|
-
const maxRetries = safeNumber(event.max_retries) ?? '?'
|
|
606
|
-
const error = safeString(event.error) ?? 'Retrying Claude API request.'
|
|
607
|
-
return [{
|
|
608
|
-
kind: 'retry_notice',
|
|
609
|
-
summary: `Retry ${attempt}/${maxRetries}`,
|
|
610
|
-
detail: error,
|
|
611
|
-
recordStep: true,
|
|
612
|
-
notable: true,
|
|
613
|
-
}]
|
|
614
|
-
}
|
|
615
|
-
case 'hook_started': {
|
|
616
|
-
const hookName = safeString(event.hook_name) ?? 'hook'
|
|
617
|
-
const hookEvent = safeString(event.hook_event)
|
|
618
|
-
return [{
|
|
619
|
-
kind: 'hook_notice',
|
|
620
|
-
summary: hookEvent ? `Hook started: ${hookName} (${hookEvent})` : `Hook started: ${hookName}`,
|
|
621
|
-
recordStep: false,
|
|
622
|
-
}]
|
|
623
|
-
}
|
|
624
|
-
case 'hook_progress': {
|
|
625
|
-
const hookName = safeString(event.hook_name) ?? 'hook'
|
|
626
|
-
const detail = [safeString(event.output), safeString(event.stdout), safeString(event.stderr)]
|
|
627
|
-
.find(Boolean)
|
|
628
|
-
return [{
|
|
629
|
-
kind: 'hook_notice',
|
|
630
|
-
summary: `Hook output: ${hookName}`,
|
|
631
|
-
detail,
|
|
632
|
-
recordStep: true,
|
|
633
|
-
}]
|
|
634
|
-
}
|
|
635
|
-
case 'task_notification': {
|
|
636
|
-
const status = safeString(event.status) ?? 'completed'
|
|
637
|
-
const summary = safeString(event.summary)
|
|
638
|
-
?? safeString(event.output_file)
|
|
639
|
-
?? safeString(event.task_id)
|
|
640
|
-
?? 'Task finished'
|
|
641
|
-
return [{
|
|
642
|
-
kind: 'task_notice',
|
|
643
|
-
summary: `Task ${status}: ${summary}`,
|
|
644
|
-
detail: safeString(event.output_file),
|
|
645
|
-
recordStep: true,
|
|
646
|
-
notable: status !== 'completed',
|
|
647
|
-
}]
|
|
648
|
-
}
|
|
649
|
-
case 'files_persisted': {
|
|
650
|
-
const failed = Array.isArray(event.failed)
|
|
651
|
-
? event.failed.filter(isRecord)
|
|
652
|
-
: []
|
|
653
|
-
if (failed.length === 0) {
|
|
654
|
-
const persistedCount = Array.isArray(event.files) ? event.files.length : 0
|
|
655
|
-
return [{
|
|
656
|
-
kind: 'artifact_notice',
|
|
657
|
-
summary: `Persisted ${pluralize(persistedCount, 'file')}.`,
|
|
658
|
-
recordStep: false,
|
|
659
|
-
}]
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
return [{
|
|
663
|
-
kind: 'artifact_notice',
|
|
664
|
-
summary: 'Some files could not be persisted.',
|
|
665
|
-
detail: failed
|
|
666
|
-
.map(entry => `${safeString(entry.filename) ?? '<unknown>'}: ${safeString(entry.error) ?? 'Unknown error'}`)
|
|
667
|
-
.join('\n'),
|
|
668
|
-
recordStep: true,
|
|
669
|
-
notable: true,
|
|
670
|
-
}]
|
|
671
|
-
}
|
|
672
|
-
default:
|
|
673
|
-
return []
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
case 'assistant': {
|
|
677
|
-
const normalized: RunEvent[] = []
|
|
678
|
-
const text = extractAssistantText(event.message)
|
|
679
|
-
if (text.trim()) {
|
|
680
|
-
const summary = summarizeLines(text)
|
|
681
|
-
normalized.push({
|
|
682
|
-
kind: 'assistant_text',
|
|
683
|
-
text,
|
|
684
|
-
summary: summary.summary,
|
|
685
|
-
detail: summary.detail,
|
|
686
|
-
recordStep: true,
|
|
687
|
-
})
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
const toolUses = extractAssistantToolUses(event.message)
|
|
691
|
-
if (toolUses.length > 0) {
|
|
692
|
-
normalized.push(buildToolBatchEvent(buildDerivedToolSummary(toolUses), toolUses))
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
return normalized
|
|
696
|
-
}
|
|
697
|
-
case 'streamlined_text': {
|
|
698
|
-
const text = safeString(event.text)
|
|
699
|
-
if (!text) {
|
|
700
|
-
return []
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const summary = summarizeLines(text)
|
|
704
|
-
return [{
|
|
705
|
-
kind: 'assistant_text',
|
|
706
|
-
text,
|
|
707
|
-
summary: summary.summary,
|
|
708
|
-
detail: summary.detail,
|
|
709
|
-
recordStep: true,
|
|
710
|
-
}]
|
|
711
|
-
}
|
|
712
|
-
case 'tool_use_summary': {
|
|
713
|
-
const summary = safeString(event.summary)
|
|
714
|
-
return summary
|
|
715
|
-
? [buildToolBatchEvent(summary, [])]
|
|
716
|
-
: []
|
|
717
|
-
}
|
|
718
|
-
case 'streamlined_tool_use_summary': {
|
|
719
|
-
const summary = safeString(event.tool_summary)
|
|
720
|
-
return summary
|
|
721
|
-
? [buildToolBatchEvent(summary, [])]
|
|
722
|
-
: []
|
|
723
|
-
}
|
|
724
|
-
case 'tool_progress': {
|
|
725
|
-
const toolUseId = safeString(event.tool_use_id)
|
|
726
|
-
const toolName = safeString(event.tool_name)
|
|
727
|
-
const elapsedSeconds = safeNumber(event.elapsed_time_seconds)
|
|
728
|
-
if (!toolUseId || !toolName || elapsedSeconds == null) {
|
|
729
|
-
return []
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
return [{
|
|
733
|
-
kind: 'tool_progress',
|
|
734
|
-
toolUseId,
|
|
735
|
-
toolName,
|
|
736
|
-
summary: `${toolName} running for ${formatElapsedSeconds(elapsedSeconds)}`,
|
|
737
|
-
detail: safeString(event.task_id),
|
|
738
|
-
elapsedSeconds,
|
|
739
|
-
recordStep: false,
|
|
740
|
-
}]
|
|
741
|
-
}
|
|
742
|
-
case 'result': {
|
|
743
|
-
const subtype = safeString(event.subtype) ?? 'success'
|
|
744
|
-
const status: RunExecutionStatus = subtype === 'success'
|
|
745
|
-
? 'success'
|
|
746
|
-
: 'failed'
|
|
747
|
-
const durationMs = safeNumber(event.duration_ms)
|
|
748
|
-
const detail = Array.isArray(event.errors)
|
|
749
|
-
? event.errors.filter((value): value is string => typeof value === 'string' && value.trim().length > 0).join('\n')
|
|
750
|
-
: undefined
|
|
751
|
-
|
|
752
|
-
return [{
|
|
753
|
-
kind: 'result',
|
|
754
|
-
summary: status === 'success'
|
|
755
|
-
? `Run completed in ${formatDurationMs(durationMs)}.`
|
|
756
|
-
: `Run failed after ${formatDurationMs(durationMs)}.`,
|
|
757
|
-
detail,
|
|
758
|
-
recordStep: true,
|
|
759
|
-
execution: {
|
|
760
|
-
status,
|
|
761
|
-
durationMs,
|
|
762
|
-
durationApiMs: safeNumber(event.duration_api_ms),
|
|
763
|
-
numTurns: safeNumber(event.num_turns),
|
|
764
|
-
stopReason: safeString(event.stop_reason) ?? null,
|
|
765
|
-
totalCostUsd: safeNumber(event.total_cost_usd),
|
|
766
|
-
permissionDenials: parsePermissionDenials(event.permission_denials),
|
|
767
|
-
errors: Array.isArray(event.errors)
|
|
768
|
-
? event.errors.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
|
769
|
-
: [],
|
|
770
|
-
structuredOutputPresent: summarizeStructuredOutputPresence(event.structured_output),
|
|
771
|
-
notableEvents: [],
|
|
772
|
-
},
|
|
773
|
-
}]
|
|
774
|
-
}
|
|
775
|
-
default:
|
|
776
|
-
return []
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
class RunStateTracker {
|
|
781
|
-
private readonly steps: RunStepSummary[] = []
|
|
782
|
-
private readonly activeTools = new Map<string, RunActiveTool>()
|
|
783
|
-
private readonly execution: RunExecutionSummary
|
|
784
|
-
private stepIndex = 0
|
|
785
|
-
private status: RunExecutionStatus = 'starting'
|
|
786
|
-
private currentActivity = 'Preparing Claude run…'
|
|
787
|
-
private model?: string
|
|
788
|
-
|
|
789
|
-
constructor(
|
|
790
|
-
private readonly prepared: PreparedLocalAgentRun,
|
|
791
|
-
private readonly mode: RunMode,
|
|
792
|
-
private readonly startedAtMs = Date.now(),
|
|
793
|
-
) {
|
|
794
|
-
this.execution = {
|
|
795
|
-
status: 'starting',
|
|
796
|
-
permissionDenials: [],
|
|
797
|
-
errors: [],
|
|
798
|
-
notableEvents: [],
|
|
799
|
-
structuredOutputPresent: false,
|
|
800
|
-
}
|
|
801
|
-
this.model = prepared.config.model
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
private addStep(
|
|
805
|
-
kind: RunStepSummary['kind'],
|
|
806
|
-
summary: string,
|
|
807
|
-
detail?: string,
|
|
808
|
-
notable = false,
|
|
809
|
-
): void {
|
|
810
|
-
this.steps.push({
|
|
811
|
-
id: `step-${++this.stepIndex}`,
|
|
812
|
-
kind,
|
|
813
|
-
summary,
|
|
814
|
-
detail,
|
|
815
|
-
notable,
|
|
816
|
-
})
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
private note(message: string | undefined): void {
|
|
820
|
-
if (!message) {
|
|
821
|
-
return
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
if (!this.execution.notableEvents.includes(message)) {
|
|
825
|
-
this.execution.notableEvents.push(message)
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
private clearActiveTools(): void {
|
|
830
|
-
this.activeTools.clear()
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
update(event: RunEvent): RunExecutionSnapshot {
|
|
834
|
-
switch (event.kind) {
|
|
835
|
-
case 'session_init':
|
|
836
|
-
this.status = 'running'
|
|
837
|
-
this.execution.status = 'running'
|
|
838
|
-
this.currentActivity = event.summary
|
|
839
|
-
this.model = event.model ?? this.model
|
|
840
|
-
if (event.recordStep) {
|
|
841
|
-
this.addStep('status', event.summary, event.detail)
|
|
842
|
-
}
|
|
843
|
-
break
|
|
844
|
-
case 'assistant_text':
|
|
845
|
-
this.clearActiveTools()
|
|
846
|
-
this.status = 'running'
|
|
847
|
-
this.execution.status = 'running'
|
|
848
|
-
this.currentActivity = event.summary
|
|
849
|
-
this.addStep('assistant', event.summary, event.detail)
|
|
850
|
-
break
|
|
851
|
-
case 'tool_batch':
|
|
852
|
-
this.clearActiveTools()
|
|
853
|
-
this.status = 'running'
|
|
854
|
-
this.execution.status = 'running'
|
|
855
|
-
this.currentActivity = event.summary
|
|
856
|
-
this.addStep('tool', event.summary, event.detail)
|
|
857
|
-
for (const tool of event.tools) {
|
|
858
|
-
this.activeTools.set(tool.id, tool)
|
|
859
|
-
}
|
|
860
|
-
break
|
|
861
|
-
case 'tool_progress': {
|
|
862
|
-
const previous = this.activeTools.get(event.toolUseId)
|
|
863
|
-
this.activeTools.set(event.toolUseId, {
|
|
864
|
-
id: event.toolUseId,
|
|
865
|
-
name: event.toolName,
|
|
866
|
-
summary: previous?.summary ?? event.toolName,
|
|
867
|
-
detail: previous?.detail ?? event.detail,
|
|
868
|
-
elapsedSeconds: event.elapsedSeconds,
|
|
869
|
-
})
|
|
870
|
-
this.status = 'running'
|
|
871
|
-
this.execution.status = 'running'
|
|
872
|
-
this.currentActivity = event.summary
|
|
873
|
-
break
|
|
874
|
-
}
|
|
875
|
-
case 'status_notice':
|
|
876
|
-
this.status = 'running'
|
|
877
|
-
this.execution.status = 'running'
|
|
878
|
-
this.currentActivity = event.summary
|
|
879
|
-
if (event.recordStep) {
|
|
880
|
-
this.addStep('status', event.summary, event.detail, Boolean(event.notable))
|
|
881
|
-
}
|
|
882
|
-
if (event.notable) {
|
|
883
|
-
this.note(event.summary)
|
|
884
|
-
}
|
|
885
|
-
break
|
|
886
|
-
case 'retry_notice':
|
|
887
|
-
this.status = 'running'
|
|
888
|
-
this.execution.status = 'running'
|
|
889
|
-
this.currentActivity = event.summary
|
|
890
|
-
this.addStep('retry', event.summary, event.detail, true)
|
|
891
|
-
this.note(event.summary)
|
|
892
|
-
break
|
|
893
|
-
case 'hook_notice':
|
|
894
|
-
this.status = 'running'
|
|
895
|
-
this.execution.status = 'running'
|
|
896
|
-
this.currentActivity = event.summary
|
|
897
|
-
if (event.recordStep) {
|
|
898
|
-
this.addStep('hook', event.summary, event.detail)
|
|
899
|
-
}
|
|
900
|
-
break
|
|
901
|
-
case 'task_notice':
|
|
902
|
-
this.status = 'running'
|
|
903
|
-
this.execution.status = 'running'
|
|
904
|
-
this.currentActivity = event.summary
|
|
905
|
-
this.addStep('task', event.summary, event.detail, Boolean(event.notable))
|
|
906
|
-
if (event.notable) {
|
|
907
|
-
this.note(event.summary)
|
|
908
|
-
}
|
|
909
|
-
break
|
|
910
|
-
case 'artifact_notice':
|
|
911
|
-
this.status = 'running'
|
|
912
|
-
this.execution.status = 'running'
|
|
913
|
-
this.currentActivity = event.summary
|
|
914
|
-
if (event.recordStep) {
|
|
915
|
-
this.addStep('artifact', event.summary, event.detail, Boolean(event.notable))
|
|
916
|
-
}
|
|
917
|
-
if (event.notable) {
|
|
918
|
-
this.note(event.summary)
|
|
919
|
-
}
|
|
920
|
-
break
|
|
921
|
-
case 'result':
|
|
922
|
-
this.clearActiveTools()
|
|
923
|
-
this.status = event.execution.status
|
|
924
|
-
this.currentActivity = event.summary
|
|
925
|
-
this.execution.status = event.execution.status
|
|
926
|
-
this.execution.durationMs = event.execution.durationMs
|
|
927
|
-
this.execution.durationApiMs = event.execution.durationApiMs
|
|
928
|
-
this.execution.numTurns = event.execution.numTurns
|
|
929
|
-
this.execution.stopReason = event.execution.stopReason
|
|
930
|
-
this.execution.totalCostUsd = event.execution.totalCostUsd
|
|
931
|
-
this.execution.permissionDenials = [...(event.execution.permissionDenials ?? [])]
|
|
932
|
-
this.execution.errors = [...(event.execution.errors ?? [])]
|
|
933
|
-
this.execution.structuredOutputPresent = Boolean(event.execution.structuredOutputPresent)
|
|
934
|
-
for (const message of event.execution.notableEvents ?? []) {
|
|
935
|
-
this.note(message)
|
|
936
|
-
}
|
|
937
|
-
this.addStep('result', event.summary, event.detail, event.execution.status !== 'success')
|
|
938
|
-
break
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
return this.snapshot()
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
finalize(status: Extract<RunExecutionStatus, 'success' | 'failed' | 'cancelled'>, detail?: {
|
|
945
|
-
errors?: string[]
|
|
946
|
-
durationMs?: number
|
|
947
|
-
durationApiMs?: number
|
|
948
|
-
numTurns?: number
|
|
949
|
-
stopReason?: string | null
|
|
950
|
-
totalCostUsd?: number
|
|
951
|
-
permissionDenials?: string[]
|
|
952
|
-
structuredOutputPresent?: boolean
|
|
953
|
-
summary?: string
|
|
954
|
-
}): RunExecutionSnapshot {
|
|
955
|
-
this.clearActiveTools()
|
|
956
|
-
this.status = status
|
|
957
|
-
this.execution.status = status
|
|
958
|
-
this.execution.errors = detail?.errors ?? this.execution.errors
|
|
959
|
-
this.execution.durationMs = detail?.durationMs ?? this.execution.durationMs
|
|
960
|
-
this.execution.durationApiMs = detail?.durationApiMs ?? this.execution.durationApiMs
|
|
961
|
-
this.execution.numTurns = detail?.numTurns ?? this.execution.numTurns
|
|
962
|
-
this.execution.stopReason = detail?.stopReason ?? this.execution.stopReason
|
|
963
|
-
this.execution.totalCostUsd = detail?.totalCostUsd ?? this.execution.totalCostUsd
|
|
964
|
-
this.execution.permissionDenials = detail?.permissionDenials ?? this.execution.permissionDenials
|
|
965
|
-
this.execution.structuredOutputPresent = detail?.structuredOutputPresent ?? this.execution.structuredOutputPresent
|
|
966
|
-
|
|
967
|
-
const summary = detail?.summary ?? (
|
|
968
|
-
status === 'success'
|
|
969
|
-
? `Run completed in ${formatDurationMs(this.execution.durationMs)}.`
|
|
970
|
-
: status === 'cancelled'
|
|
971
|
-
? 'Run cancelled.'
|
|
972
|
-
: `Run failed after ${formatDurationMs(this.execution.durationMs)}.`
|
|
973
|
-
)
|
|
974
|
-
this.currentActivity = summary
|
|
975
|
-
|
|
976
|
-
const lastStep = this.steps.at(-1)
|
|
977
|
-
if (!lastStep || lastStep.kind !== 'result' || lastStep.summary !== summary) {
|
|
978
|
-
this.addStep('result', summary, detail?.errors?.join('\n'), status !== 'success')
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
return this.snapshot()
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
snapshot(): RunExecutionSnapshot {
|
|
985
|
-
return {
|
|
986
|
-
repoRoot: this.prepared.repoRoot,
|
|
987
|
-
mode: this.mode,
|
|
988
|
-
model: this.model,
|
|
989
|
-
artifactPath: this.prepared.resultPath,
|
|
990
|
-
startedAtMs: this.startedAtMs,
|
|
991
|
-
status: this.status,
|
|
992
|
-
currentActivity: this.currentActivity,
|
|
993
|
-
steps: this.steps.map(step => ({ ...step })),
|
|
994
|
-
activeTools: Array.from(this.activeTools.values()).map(tool => ({ ...tool })),
|
|
995
|
-
execution: {
|
|
996
|
-
status: this.execution.status,
|
|
997
|
-
durationMs: this.execution.durationMs,
|
|
998
|
-
durationApiMs: this.execution.durationApiMs,
|
|
999
|
-
numTurns: this.execution.numTurns,
|
|
1000
|
-
stopReason: this.execution.stopReason,
|
|
1001
|
-
totalCostUsd: this.execution.totalCostUsd,
|
|
1002
|
-
permissionDenials: [...this.execution.permissionDenials],
|
|
1003
|
-
errors: [...this.execution.errors],
|
|
1004
|
-
notableEvents: [...this.execution.notableEvents],
|
|
1005
|
-
structuredOutputPresent: this.execution.structuredOutputPresent,
|
|
1006
|
-
},
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
class PlainTranscriptRenderer {
|
|
1012
|
-
private readonly nextMilestoneIndex = new Map<string, number>()
|
|
1013
|
-
|
|
1014
|
-
render(
|
|
1015
|
-
event: RunEvent,
|
|
1016
|
-
snapshot: RunExecutionSnapshot,
|
|
1017
|
-
verbose = false,
|
|
1018
|
-
): string[] {
|
|
1019
|
-
switch (event.kind) {
|
|
1020
|
-
case 'session_init':
|
|
1021
|
-
return [snapshot.model ? `Claude session started (${snapshot.model}).` : event.summary]
|
|
1022
|
-
case 'assistant_text':
|
|
1023
|
-
return splitVisibleLines(event.text).map(line => `Assistant: ${line}`)
|
|
1024
|
-
case 'tool_batch': {
|
|
1025
|
-
const lines = [`Tools: ${event.summary}`]
|
|
1026
|
-
if (verbose && event.detail) {
|
|
1027
|
-
lines.push(...splitVisibleLines(event.detail).map(line => ` ${line}`))
|
|
1028
|
-
}
|
|
1029
|
-
return lines
|
|
1030
|
-
}
|
|
1031
|
-
case 'tool_progress': {
|
|
1032
|
-
const nextIndex = this.nextMilestoneIndex.get(event.toolUseId) ?? 0
|
|
1033
|
-
const milestone = TOOL_PROGRESS_MILESTONES[nextIndex]
|
|
1034
|
-
const shouldEmit = milestone != null
|
|
1035
|
-
? event.elapsedSeconds >= milestone
|
|
1036
|
-
: event.elapsedSeconds % 60 === 0
|
|
1037
|
-
if (!shouldEmit) {
|
|
1038
|
-
return []
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
if (milestone != null) {
|
|
1042
|
-
this.nextMilestoneIndex.set(event.toolUseId, nextIndex + 1)
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
return [`Working: ${event.toolName} (${formatElapsedSeconds(event.elapsedSeconds)})`]
|
|
1046
|
-
}
|
|
1047
|
-
case 'status_notice':
|
|
1048
|
-
return event.recordStep ? [event.summary] : []
|
|
1049
|
-
case 'retry_notice': {
|
|
1050
|
-
const lines = [event.summary]
|
|
1051
|
-
if (event.detail) {
|
|
1052
|
-
lines.push(` ${event.detail}`)
|
|
1053
|
-
}
|
|
1054
|
-
return lines
|
|
1055
|
-
}
|
|
1056
|
-
case 'hook_notice':
|
|
1057
|
-
if (!event.recordStep) {
|
|
1058
|
-
return []
|
|
1059
|
-
}
|
|
1060
|
-
return [event.summary, ...(verbose && event.detail ? splitVisibleLines(event.detail).map(line => ` ${line}`) : [])]
|
|
1061
|
-
case 'task_notice':
|
|
1062
|
-
return [event.summary, ...(verbose && event.detail ? [` ${event.detail}`] : [])]
|
|
1063
|
-
case 'artifact_notice':
|
|
1064
|
-
if (!event.recordStep && !verbose) {
|
|
1065
|
-
return []
|
|
1066
|
-
}
|
|
1067
|
-
return [event.summary, ...(verbose && event.detail ? splitVisibleLines(event.detail).map(line => ` ${line}`) : [])]
|
|
1068
|
-
case 'result':
|
|
1069
|
-
return [event.summary]
|
|
1070
|
-
default:
|
|
1071
|
-
return []
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
function parseJsonObjectFile(
|
|
1077
|
-
path: string,
|
|
1078
|
-
content: string,
|
|
1079
|
-
invalidSummary: string,
|
|
1080
|
-
): Record<string, unknown> {
|
|
1081
|
-
let parsed: unknown
|
|
1082
|
-
try {
|
|
1083
|
-
parsed = JSON.parse(content)
|
|
1084
|
-
}
|
|
1085
|
-
catch (error) {
|
|
1086
|
-
throw createUserFacingError({
|
|
1087
|
-
code: 'run_invalid_json',
|
|
1088
|
-
summary: invalidSummary,
|
|
1089
|
-
details: error instanceof Error ? error.message : String(error),
|
|
1090
|
-
fixes: [`Fix the JSON syntax in \`${path}\`.`],
|
|
1091
|
-
})
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1095
|
-
throw createUserFacingError({
|
|
1096
|
-
code: 'run_invalid_json_shape',
|
|
1097
|
-
summary: invalidSummary,
|
|
1098
|
-
fixes: [`Update \`${path}\` so it contains a JSON object.`],
|
|
1099
|
-
})
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
return parsed as Record<string, unknown>
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
function parseAgentConfig(path: string, content: string): AgentRunConfig {
|
|
1106
|
-
const parsed = parseJsonObjectFile(path, content, 'agent.config.json must be a valid JSON object.')
|
|
1107
|
-
|
|
1108
|
-
if (parsed.model !== undefined && typeof parsed.model !== 'string') {
|
|
1109
|
-
throw createUserFacingError({
|
|
1110
|
-
code: 'run_invalid_model',
|
|
1111
|
-
summary: 'agent.config.json has an invalid `model` value.',
|
|
1112
|
-
fixes: ['Set `model` to a string, or remove it from agent.config.json.'],
|
|
1113
|
-
})
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
if (parsed.isMultiturn !== undefined && typeof parsed.isMultiturn !== 'boolean') {
|
|
1117
|
-
throw createUserFacingError({
|
|
1118
|
-
code: 'run_invalid_multiturn',
|
|
1119
|
-
summary: 'agent.config.json has an invalid `isMultiturn` value.',
|
|
1120
|
-
fixes: ['Set `isMultiturn` to `true` or `false`, or remove it from agent.config.json.'],
|
|
1121
|
-
})
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
if (parsed.maxRounds !== undefined && (!Number.isInteger(parsed.maxRounds) || Number(parsed.maxRounds) <= 0)) {
|
|
1125
|
-
throw createUserFacingError({
|
|
1126
|
-
code: 'run_invalid_max_rounds',
|
|
1127
|
-
summary: 'agent.config.json has an invalid `maxRounds` value.',
|
|
1128
|
-
fixes: ['Set `maxRounds` to a positive integer, or remove it from agent.config.json.'],
|
|
1129
|
-
})
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
const model = typeof parsed.model === 'string' && parsed.model.trim().length > 0
|
|
1133
|
-
? parsed.model.trim()
|
|
1134
|
-
: undefined
|
|
1135
|
-
|
|
1136
|
-
return {
|
|
1137
|
-
model,
|
|
1138
|
-
isMultiturn: parsed.isMultiturn === true,
|
|
1139
|
-
maxRounds: typeof parsed.maxRounds === 'number' ? parsed.maxRounds : undefined,
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
const CLAUDE_RUN_ENV_DENYLIST = new Set([
|
|
1144
|
-
'CLAUDE_CONFIG_DIR',
|
|
1145
|
-
'CLAUDE_ENV_FILE',
|
|
1146
|
-
'ANTHROPIC_MODEL',
|
|
1147
|
-
'ANTHROPIC_CUSTOM_MODEL_OPTION',
|
|
1148
|
-
'ANTHROPIC_CUSTOM_MODEL_OPTION_NAME',
|
|
1149
|
-
'ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION',
|
|
1150
|
-
'ANTHROPIC_CUSTOM_MODEL_OPTION_SUPPORTED_CAPABILITIES',
|
|
1151
|
-
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
1152
|
-
'ANTHROPIC_DEFAULT_OPUS_MODEL_NAME',
|
|
1153
|
-
'ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION',
|
|
1154
|
-
'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
|
|
1155
|
-
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
1156
|
-
'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME',
|
|
1157
|
-
'ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION',
|
|
1158
|
-
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
|
|
1159
|
-
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
1160
|
-
'ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME',
|
|
1161
|
-
'ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION',
|
|
1162
|
-
'ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES',
|
|
1163
|
-
'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
1164
|
-
])
|
|
1165
|
-
|
|
1166
|
-
function buildClaudeRunEnvironment(
|
|
1167
|
-
baseEnv: NodeJS.ProcessEnv = process.env,
|
|
1168
|
-
): Record<string, string | undefined> {
|
|
1169
|
-
return Object.fromEntries(
|
|
1170
|
-
Object.entries(baseEnv).filter(([key]) => !CLAUDE_RUN_ENV_DENYLIST.has(key)),
|
|
1171
|
-
)
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
function buildUnsupportedClaudeVersionError(detail: string | undefined): ReturnType<typeof createUserFacingError> {
|
|
1175
|
-
const requiredVersion = formatClaudeCliVersion(MIN_CLAUDE_VERSION_FOR_PROJECT_SCOPED_RUN)
|
|
1176
|
-
|
|
1177
|
-
return createUserFacingError({
|
|
1178
|
-
code: 'run_claude_version_unsupported',
|
|
1179
|
-
summary: `\`tela run\` requires Claude CLI ${requiredVersion} or newer for project-scoped runs.`,
|
|
1180
|
-
details: detail,
|
|
1181
|
-
fixes: [
|
|
1182
|
-
'Update Claude CLI before using `tela run`.',
|
|
1183
|
-
'Run `claude --version` to confirm the installed version.',
|
|
1184
|
-
],
|
|
1185
|
-
})
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
function buildClaudeBaseCommand(prepared: PreparedLocalAgentRun): string[] {
|
|
1189
|
-
const command = [
|
|
1190
|
-
'claude',
|
|
1191
|
-
'--setting-sources',
|
|
1192
|
-
'project',
|
|
1193
|
-
]
|
|
1194
|
-
|
|
1195
|
-
if (prepared.mcpConfigPath) {
|
|
1196
|
-
command.push('--strict-mcp-config', '--mcp-config', prepared.mcpConfigPath)
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
if (prepared.config.model) {
|
|
1200
|
-
command.push('--model', prepared.config.model)
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
return command
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
function buildClaudeCommand(
|
|
1207
|
-
prepared: PreparedLocalAgentRun,
|
|
1208
|
-
mode: RunMode,
|
|
1209
|
-
): string[] {
|
|
1210
|
-
const command = buildClaudeBaseCommand(prepared)
|
|
1211
|
-
|
|
1212
|
-
if (mode === 'one-shot') {
|
|
1213
|
-
command.push(
|
|
1214
|
-
'-p',
|
|
1215
|
-
'--output-format',
|
|
1216
|
-
'stream-json',
|
|
1217
|
-
'--verbose',
|
|
1218
|
-
)
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
command.push(INITIAL_PROMPT)
|
|
1222
|
-
return command
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
async function resolveRepositoryRoot(cwd: string): Promise<string> {
|
|
1226
|
-
try {
|
|
1227
|
-
return await getRepositoryRoot(cwd)
|
|
1228
|
-
}
|
|
1229
|
-
catch (error) {
|
|
1230
|
-
if (error instanceof Error && error.message.startsWith('Not a git repository:')) {
|
|
1231
|
-
throw createUserFacingError({
|
|
1232
|
-
code: 'run_outside_repo',
|
|
1233
|
-
summary: 'Run must be executed from inside a git repository.',
|
|
1234
|
-
fixes: [
|
|
1235
|
-
'Change into an agent repository, or run `tela sync <repo>` first.',
|
|
1236
|
-
],
|
|
1237
|
-
})
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
throw error
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
async function validateRequiredFiles(repoRoot: string): Promise<void> {
|
|
1245
|
-
const missing: string[] = []
|
|
1246
|
-
|
|
1247
|
-
for (const file of REQUIRED_AGENT_FILES) {
|
|
1248
|
-
try {
|
|
1249
|
-
await readFile(join(repoRoot, file), 'utf-8')
|
|
1250
|
-
}
|
|
1251
|
-
catch {
|
|
1252
|
-
missing.push(file)
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
if (missing.length === 0) {
|
|
1257
|
-
return
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
throw createUserFacingError({
|
|
1261
|
-
code: 'run_missing_contract_files',
|
|
1262
|
-
summary: 'Agent repository is missing required files.',
|
|
1263
|
-
fixes: missing.map(file => `Add \`${file}\` to the repository root.`),
|
|
1264
|
-
evidence: missing.map(file => `Missing: ${join(repoRoot, file)}`),
|
|
1265
|
-
})
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
export async function prepareLocalAgentRun(options: {
|
|
1269
|
-
cwd?: string
|
|
1270
|
-
commandExists?: typeof commandExists
|
|
1271
|
-
inspectClaudeCli?: typeof inspectClaudeCli
|
|
1272
|
-
runCommand?: typeof runCommand
|
|
1273
|
-
} = {}): Promise<PreparedLocalAgentRun> {
|
|
1274
|
-
const cwd = options.cwd ?? process.cwd()
|
|
1275
|
-
const repoRoot = await resolveRepositoryRoot(cwd)
|
|
1276
|
-
const inspectClaudeCliFn = options.inspectClaudeCli ?? inspectClaudeCli
|
|
1277
|
-
|
|
1278
|
-
const claudeInspection = await inspectClaudeCliFn({
|
|
1279
|
-
commandExistsFn: options.commandExists,
|
|
1280
|
-
runCommandFn: options.runCommand,
|
|
1281
|
-
})
|
|
1282
|
-
|
|
1283
|
-
if (!claudeInspection.available) {
|
|
1284
|
-
throw createUserFacingError({
|
|
1285
|
-
code: 'run_claude_missing',
|
|
1286
|
-
summary: 'Claude CLI is not available on PATH.',
|
|
1287
|
-
fixes: ['Install the Claude CLI before using `tela run`.'],
|
|
1288
|
-
})
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
if (!claudeInspection.supportsProjectScopedRun) {
|
|
1292
|
-
throw buildUnsupportedClaudeVersionError(claudeInspection.detail)
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
await validateRequiredFiles(repoRoot)
|
|
1296
|
-
|
|
1297
|
-
const [outputFormatContent, agentConfigContent] = await Promise.all([
|
|
1298
|
-
readFile(join(repoRoot, 'output-format.json'), 'utf-8'),
|
|
1299
|
-
readFile(join(repoRoot, 'agent.config.json'), 'utf-8'),
|
|
1300
|
-
])
|
|
1301
|
-
|
|
1302
|
-
const outputFormat = parseJsonObjectFile(
|
|
1303
|
-
'output-format.json',
|
|
1304
|
-
outputFormatContent,
|
|
1305
|
-
'output-format.json must be a valid JSON object.',
|
|
1306
|
-
)
|
|
1307
|
-
const config = parseAgentConfig('agent.config.json', agentConfigContent)
|
|
1308
|
-
const resultKind = Object.keys(outputFormat).length > 0 ? 'json' : 'markdown'
|
|
1309
|
-
|
|
1310
|
-
return {
|
|
1311
|
-
repoRoot,
|
|
1312
|
-
config,
|
|
1313
|
-
resultKind,
|
|
1314
|
-
resultPath: join(repoRoot, 'output', resultKind === 'json' ? 'result.json' : 'result.md'),
|
|
1315
|
-
mcpConfigPath: existsSync(join(repoRoot, '.mcp.json'))
|
|
1316
|
-
? join(repoRoot, '.mcp.json')
|
|
1317
|
-
: undefined,
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
export function createInitialRunSnapshot(
|
|
1322
|
-
prepared: PreparedLocalAgentRun,
|
|
1323
|
-
mode: RunMode = prepared.config.isMultiturn ? 'chat' : 'one-shot',
|
|
1324
|
-
): RunExecutionSnapshot {
|
|
1325
|
-
return {
|
|
1326
|
-
repoRoot: prepared.repoRoot,
|
|
1327
|
-
mode,
|
|
1328
|
-
model: prepared.config.model,
|
|
1329
|
-
artifactPath: prepared.resultPath,
|
|
1330
|
-
startedAtMs: Date.now(),
|
|
1331
|
-
status: 'starting',
|
|
1332
|
-
currentActivity: mode === 'chat'
|
|
1333
|
-
? 'Preparing Claude chat handoff…'
|
|
1334
|
-
: 'Validating repository contract…',
|
|
1335
|
-
steps: [],
|
|
1336
|
-
activeTools: [],
|
|
1337
|
-
execution: {
|
|
1338
|
-
status: 'starting',
|
|
1339
|
-
permissionDenials: [],
|
|
1340
|
-
errors: [],
|
|
1341
|
-
notableEvents: [],
|
|
1342
|
-
structuredOutputPresent: false,
|
|
1343
|
-
},
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
function snapshotStepEvidence(snapshot: RunExecutionSnapshot): string[] {
|
|
1348
|
-
return snapshot.steps
|
|
1349
|
-
.slice(-5)
|
|
1350
|
-
.map(step => step.summary)
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
function buildRunFailureError(
|
|
1354
|
-
result: CommandResult,
|
|
1355
|
-
snapshot: RunExecutionSnapshot,
|
|
1356
|
-
): ReturnType<typeof createUserFacingError> {
|
|
1357
|
-
const details = snapshot.execution.errors[0]
|
|
1358
|
-
?? result.stderr.trim()
|
|
1359
|
-
?? result.stdout.trim()
|
|
1360
|
-
?? `Claude exited with code ${result.exitCode}.`
|
|
1361
|
-
|
|
1362
|
-
return createUserFacingError({
|
|
1363
|
-
code: 'run_failed',
|
|
1364
|
-
summary: 'Claude run failed.',
|
|
1365
|
-
details,
|
|
1366
|
-
fixes: [
|
|
1367
|
-
'Review the streamed Claude output above for the failing step.',
|
|
1368
|
-
'Fix the repository instructions or inputs, then rerun `tela run`.',
|
|
1369
|
-
],
|
|
1370
|
-
evidence: [
|
|
1371
|
-
`Command: ${result.command.join(' ')}`,
|
|
1372
|
-
...snapshotStepEvidence(snapshot),
|
|
1373
|
-
...snapshot.execution.errors,
|
|
1374
|
-
result.stderr.trim() ? `stderr: ${result.stderr.trim()}` : '',
|
|
1375
|
-
].filter(Boolean),
|
|
1376
|
-
})
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
function buildRunCancelledError(snapshot: RunExecutionSnapshot): ReturnType<typeof createUserFacingError> {
|
|
1380
|
-
return createUserFacingError({
|
|
1381
|
-
code: 'run_cancelled',
|
|
1382
|
-
summary: 'Claude run was cancelled.',
|
|
1383
|
-
details: snapshot.currentActivity,
|
|
1384
|
-
fixes: ['Run `tela run` again when you are ready to continue.'],
|
|
1385
|
-
evidence: snapshotStepEvidence(snapshot),
|
|
1386
|
-
exitCode: 130,
|
|
1387
|
-
})
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
async function readResultArtifact(
|
|
1391
|
-
prepared: PreparedLocalAgentRun,
|
|
1392
|
-
snapshot: RunExecutionSnapshot,
|
|
1393
|
-
): Promise<RunCommandResult> {
|
|
1394
|
-
let content: string
|
|
1395
|
-
try {
|
|
1396
|
-
content = await readFile(prepared.resultPath, 'utf-8')
|
|
1397
|
-
}
|
|
1398
|
-
catch {
|
|
1399
|
-
throw createUserFacingError({
|
|
1400
|
-
code: 'run_missing_result',
|
|
1401
|
-
summary: `Expected result artifact was not created: ${prepared.resultPath}`,
|
|
1402
|
-
details: joinBlocks([
|
|
1403
|
-
`Last activity: ${snapshot.currentActivity}`,
|
|
1404
|
-
snapshot.execution.numTurns != null ? `Turns: ${snapshot.execution.numTurns}` : undefined,
|
|
1405
|
-
]),
|
|
1406
|
-
fixes: [
|
|
1407
|
-
'Ensure the agent writes the required output file before exiting.',
|
|
1408
|
-
'Rerun `tela run` after fixing the repository instructions.',
|
|
1409
|
-
],
|
|
1410
|
-
evidence: snapshotStepEvidence(snapshot),
|
|
1411
|
-
})
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
let resultValue: unknown = content
|
|
1415
|
-
if (prepared.resultKind === 'json') {
|
|
1416
|
-
try {
|
|
1417
|
-
resultValue = JSON.parse(content)
|
|
1418
|
-
}
|
|
1419
|
-
catch (error) {
|
|
1420
|
-
throw createUserFacingError({
|
|
1421
|
-
code: 'run_invalid_result_json',
|
|
1422
|
-
summary: 'output/result.json is not valid JSON.',
|
|
1423
|
-
details: error instanceof Error ? error.message : String(error),
|
|
1424
|
-
fixes: ['Ensure the agent writes valid JSON to output/result.json.'],
|
|
1425
|
-
evidence: snapshotStepEvidence(snapshot),
|
|
1426
|
-
})
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
const executionStatus = snapshot.execution.status === 'cancelled'
|
|
1431
|
-
? 'cancelled'
|
|
1432
|
-
: snapshot.execution.status === 'failed'
|
|
1433
|
-
? 'failed'
|
|
1434
|
-
: 'success'
|
|
1435
|
-
|
|
1436
|
-
return {
|
|
1437
|
-
ok: true,
|
|
1438
|
-
repoRoot: prepared.repoRoot,
|
|
1439
|
-
mode: prepared.config.isMultiturn ? 'chat' : 'one-shot',
|
|
1440
|
-
config: prepared.config,
|
|
1441
|
-
execution: {
|
|
1442
|
-
...snapshot.execution,
|
|
1443
|
-
status: executionStatus,
|
|
1444
|
-
},
|
|
1445
|
-
steps: snapshot.steps,
|
|
1446
|
-
artifact: {
|
|
1447
|
-
path: prepared.resultPath,
|
|
1448
|
-
kind: prepared.resultKind,
|
|
1449
|
-
value: resultValue,
|
|
1450
|
-
},
|
|
1451
|
-
resultPath: prepared.resultPath,
|
|
1452
|
-
resultKind: prepared.resultKind,
|
|
1453
|
-
resultValue,
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
async function runInteractiveAgent(
|
|
1458
|
-
prepared: PreparedLocalAgentRun,
|
|
1459
|
-
runInteractiveCommandFn: typeof runInteractiveCommand,
|
|
1460
|
-
options: RunLocalAgentCommandOptions,
|
|
1461
|
-
): Promise<void> {
|
|
1462
|
-
if (options.surface === 'json') {
|
|
1463
|
-
throw createUserFacingError({
|
|
1464
|
-
code: 'run_multiturn_json_unsupported',
|
|
1465
|
-
summary: 'Multiturn run requires an interactive TTY in the current hybrid design.',
|
|
1466
|
-
fixes: ['Run `tela run` from an interactive terminal without `--json`.'],
|
|
1467
|
-
})
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
const stdinIsTTY = options.stdinIsTTY ?? Boolean(process.stdin.isTTY)
|
|
1471
|
-
const stdoutIsTTY = options.stdoutIsTTY ?? Boolean(process.stdout.isTTY)
|
|
1472
|
-
if (!stdinIsTTY || !stdoutIsTTY) {
|
|
1473
|
-
throw createUserFacingError({
|
|
1474
|
-
code: 'run_multiturn_tty_required',
|
|
1475
|
-
summary: 'Multiturn run requires an interactive TTY in the current hybrid design.',
|
|
1476
|
-
fixes: ['Run `tela run` from an interactive terminal.'],
|
|
1477
|
-
})
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
if (options.surface === 'plain') {
|
|
1481
|
-
writeLine(options.writeStdout, 'Launching Claude in interactive chat mode. Tela will resume after Claude exits.')
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
await runInteractiveCommandFn(buildClaudeCommand(prepared, 'chat'), {
|
|
1485
|
-
cwd: prepared.repoRoot,
|
|
1486
|
-
env: buildClaudeRunEnvironment(),
|
|
1487
|
-
})
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
async function runOneShotAgent(
|
|
1491
|
-
prepared: PreparedLocalAgentRun,
|
|
1492
|
-
options: RunLocalAgentCommandOptions,
|
|
1493
|
-
tracker: RunStateTracker,
|
|
1494
|
-
): Promise<RunExecutionSnapshot> {
|
|
1495
|
-
const plainRenderer = new PlainTranscriptRenderer()
|
|
1496
|
-
const runStreamingCommandFn = options.runStreamingCommand ?? runStreamingCommand
|
|
1497
|
-
const spawnStreamingCommandFn = options.spawnStreamingCommand ?? ((command: string[], spawnOptions?: RunStreamingCommandOptions): StreamingCommandHandle => ({
|
|
1498
|
-
result: runStreamingCommandFn(command, spawnOptions),
|
|
1499
|
-
interrupt() {},
|
|
1500
|
-
kill() {},
|
|
1501
|
-
}))
|
|
1502
|
-
|
|
1503
|
-
const notifySnapshot = (snapshot: RunExecutionSnapshot) => {
|
|
1504
|
-
options.onSnapshot?.(snapshot)
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
notifySnapshot(tracker.snapshot())
|
|
1508
|
-
|
|
1509
|
-
if (options.surface === 'plain') {
|
|
1510
|
-
writeLine(options.writeStdout, 'Streaming Claude execution in one-shot mode.')
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
let cancelled = false
|
|
1514
|
-
const command = buildClaudeCommand(prepared, 'one-shot')
|
|
1515
|
-
|
|
1516
|
-
const handle = spawnStreamingCommandFn(command, {
|
|
1517
|
-
cwd: prepared.repoRoot,
|
|
1518
|
-
env: buildClaudeRunEnvironment(),
|
|
1519
|
-
allowFailure: true,
|
|
1520
|
-
onStdoutLine: async (line) => {
|
|
1521
|
-
const trimmed = line.trim()
|
|
1522
|
-
if (!trimmed) {
|
|
1523
|
-
return
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
try {
|
|
1527
|
-
const parsed = JSON.parse(trimmed) as Record<string, unknown>
|
|
1528
|
-
const events = normalizeClaudePrintEvent(parsed)
|
|
1529
|
-
for (const event of events) {
|
|
1530
|
-
const snapshot = tracker.update(event)
|
|
1531
|
-
notifySnapshot(snapshot)
|
|
1532
|
-
if (options.surface === 'plain') {
|
|
1533
|
-
for (const renderedLine of plainRenderer.render(event, snapshot, options.verbose)) {
|
|
1534
|
-
writeLine(options.writeStdout, renderedLine)
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
catch {
|
|
1540
|
-
if (options.surface === 'plain') {
|
|
1541
|
-
writeLine(options.writeStdout, `Claude: ${trimmed}`)
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
},
|
|
1545
|
-
onStderrLine: async (line) => {
|
|
1546
|
-
const trimmed = line.trim()
|
|
1547
|
-
if (!trimmed) {
|
|
1548
|
-
return
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
if (options.surface === 'plain') {
|
|
1552
|
-
writeErrorLine(options.writeStderr, trimmed)
|
|
1553
|
-
}
|
|
1554
|
-
},
|
|
1555
|
-
})
|
|
1556
|
-
|
|
1557
|
-
options.onControl?.({
|
|
1558
|
-
interrupt() {
|
|
1559
|
-
cancelled = true
|
|
1560
|
-
handle.interrupt()
|
|
1561
|
-
},
|
|
1562
|
-
kill() {
|
|
1563
|
-
cancelled = true
|
|
1564
|
-
handle.kill()
|
|
1565
|
-
},
|
|
1566
|
-
})
|
|
1567
|
-
|
|
1568
|
-
const result = await handle.result
|
|
1569
|
-
let snapshot = tracker.snapshot()
|
|
1570
|
-
|
|
1571
|
-
if (result.exitCode !== 0) {
|
|
1572
|
-
if (cancelled) {
|
|
1573
|
-
snapshot = tracker.finalize('cancelled', {
|
|
1574
|
-
summary: 'Run cancelled.',
|
|
1575
|
-
})
|
|
1576
|
-
notifySnapshot(snapshot)
|
|
1577
|
-
throw buildRunCancelledError(snapshot)
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
snapshot = tracker.finalize('failed')
|
|
1581
|
-
notifySnapshot(snapshot)
|
|
1582
|
-
throw buildRunFailureError(result, snapshot)
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
if (snapshot.execution.status === 'running' || snapshot.execution.status === 'starting') {
|
|
1586
|
-
snapshot = tracker.finalize('success')
|
|
1587
|
-
notifySnapshot(snapshot)
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
if (snapshot.execution.status === 'failed') {
|
|
1591
|
-
throw buildRunFailureError(result, snapshot)
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
return snapshot
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
export async function runLocalAgentCommand(
|
|
1598
|
-
options: RunLocalAgentCommandOptions = {},
|
|
1599
|
-
): Promise<RunCommandResult> {
|
|
1600
|
-
const surface = options.surface ?? 'plain'
|
|
1601
|
-
const effectiveOptions: RunLocalAgentCommandOptions = {
|
|
1602
|
-
...options,
|
|
1603
|
-
surface,
|
|
1604
|
-
}
|
|
1605
|
-
const prepared = options.contract ?? await prepareLocalAgentRun({
|
|
1606
|
-
cwd: options.cwd ?? process.cwd(),
|
|
1607
|
-
commandExists: options.commandExists,
|
|
1608
|
-
inspectClaudeCli: options.inspectClaudeCli,
|
|
1609
|
-
runCommand: options.runCommand,
|
|
1610
|
-
})
|
|
1611
|
-
const mode: RunMode = prepared.config.isMultiturn ? 'chat' : 'one-shot'
|
|
1612
|
-
const tracker = new RunStateTracker(prepared, mode)
|
|
1613
|
-
|
|
1614
|
-
let snapshot = tracker.snapshot()
|
|
1615
|
-
effectiveOptions.onSnapshot?.(snapshot)
|
|
1616
|
-
|
|
1617
|
-
if (prepared.config.isMultiturn) {
|
|
1618
|
-
await runInteractiveAgent(prepared, options.runInteractiveCommand ?? runInteractiveCommand, effectiveOptions)
|
|
1619
|
-
snapshot = tracker.finalize('success', {
|
|
1620
|
-
summary: 'Claude chat exited. Reading result artifact…',
|
|
1621
|
-
})
|
|
1622
|
-
effectiveOptions.onSnapshot?.(snapshot)
|
|
1623
|
-
}
|
|
1624
|
-
else {
|
|
1625
|
-
snapshot = await runOneShotAgent(prepared, effectiveOptions, tracker)
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
return await readResultArtifact(prepared, snapshot)
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
function renderExecutionSteps(result: RunCommandResult, options: RenderOptions): string | undefined {
|
|
1632
|
-
const recentSteps = result.steps.slice(-8).map(step => step.summary)
|
|
1633
|
-
if (recentSteps.length === 0) {
|
|
1634
|
-
return undefined
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
return section('Recent steps', bulletList(recentSteps), options)
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
export function renderRunExecutionRecap(
|
|
1641
|
-
result: RunCommandResult,
|
|
1642
|
-
options: RenderOptions = {},
|
|
1643
|
-
): string {
|
|
1644
|
-
return resultCard('Execution recap', [
|
|
1645
|
-
section('Execution', kvList([
|
|
1646
|
-
{ label: 'Repo root', value: result.repoRoot },
|
|
1647
|
-
{ label: 'Mode', value: result.mode },
|
|
1648
|
-
{ label: 'Model', value: result.config.model ?? '<default>' },
|
|
1649
|
-
{ label: 'Elapsed', value: formatDurationMs(result.execution.durationMs) },
|
|
1650
|
-
{ label: 'Turns', value: result.execution.numTurns != null ? String(result.execution.numTurns) : '<unknown>' },
|
|
1651
|
-
{ label: 'Status', value: result.execution.status },
|
|
1652
|
-
{ label: 'Stop reason', value: result.execution.stopReason ?? '<none>' },
|
|
1653
|
-
{ label: 'Cost', value: result.execution.totalCostUsd != null ? `$${result.execution.totalCostUsd.toFixed(4)}` : '<unknown>' },
|
|
1654
|
-
{
|
|
1655
|
-
label: 'maxRounds',
|
|
1656
|
-
value: result.config.maxRounds != null
|
|
1657
|
-
? `${result.config.maxRounds} (ignored in local CLI)`
|
|
1658
|
-
: '<unspecified>',
|
|
1659
|
-
},
|
|
1660
|
-
], options), options),
|
|
1661
|
-
result.execution.notableEvents.length > 0
|
|
1662
|
-
? section('Notable events', bulletList(result.execution.notableEvents), options)
|
|
1663
|
-
: undefined,
|
|
1664
|
-
result.execution.permissionDenials.length > 0
|
|
1665
|
-
? section('Permission denials', bulletList(result.execution.permissionDenials), options)
|
|
1666
|
-
: undefined,
|
|
1667
|
-
result.execution.errors.length > 0
|
|
1668
|
-
? section('Errors', bulletList(result.execution.errors), options)
|
|
1669
|
-
: undefined,
|
|
1670
|
-
renderExecutionSteps(result, options),
|
|
1671
|
-
], options)
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
export function renderRunArtifactCard(
|
|
1675
|
-
result: RunCommandResult,
|
|
1676
|
-
options: RenderOptions = {},
|
|
1677
|
-
): string {
|
|
1678
|
-
const resultContent = result.resultKind === 'json'
|
|
1679
|
-
? JSON.stringify(result.resultValue, null, 2)
|
|
1680
|
-
: String(result.resultValue)
|
|
1681
|
-
|
|
1682
|
-
return resultCard('Run result', [
|
|
1683
|
-
section('Artifact', kvList([
|
|
1684
|
-
{ label: 'Path', value: result.resultPath },
|
|
1685
|
-
{ label: 'Kind', value: result.resultKind },
|
|
1686
|
-
], options), options),
|
|
1687
|
-
section('Result', resultContent, options),
|
|
1688
|
-
], options)
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
export function renderRunResult(
|
|
1692
|
-
result: RunCommandResult,
|
|
1693
|
-
options: RenderOptions = {},
|
|
1694
|
-
): string {
|
|
1695
|
-
return joinBlocks([
|
|
1696
|
-
renderRunExecutionRecap(result, options),
|
|
1697
|
-
renderRunArtifactCard(result, options),
|
|
1698
|
-
])
|
|
1699
|
-
}
|