opencastle 0.21.1 → 0.23.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.
Files changed (79) hide show
  1. package/dist/cli/convoy/engine.d.ts +4 -0
  2. package/dist/cli/convoy/engine.d.ts.map +1 -1
  3. package/dist/cli/convoy/engine.js +23 -1
  4. package/dist/cli/convoy/engine.js.map +1 -1
  5. package/dist/cli/convoy/engine.test.js +148 -0
  6. package/dist/cli/convoy/engine.test.js.map +1 -1
  7. package/dist/cli/convoy/export.d.ts +1 -0
  8. package/dist/cli/convoy/export.d.ts.map +1 -1
  9. package/dist/cli/convoy/export.js +39 -0
  10. package/dist/cli/convoy/export.js.map +1 -1
  11. package/dist/cli/convoy/export.test.js +31 -0
  12. package/dist/cli/convoy/export.test.js.map +1 -1
  13. package/dist/cli/convoy/pipeline.d.ts +35 -0
  14. package/dist/cli/convoy/pipeline.d.ts.map +1 -0
  15. package/dist/cli/convoy/pipeline.js +353 -0
  16. package/dist/cli/convoy/pipeline.js.map +1 -0
  17. package/dist/cli/convoy/pipeline.test.d.ts +2 -0
  18. package/dist/cli/convoy/pipeline.test.d.ts.map +1 -0
  19. package/dist/cli/convoy/pipeline.test.js +778 -0
  20. package/dist/cli/convoy/pipeline.test.js.map +1 -0
  21. package/dist/cli/convoy/store.d.ts +18 -4
  22. package/dist/cli/convoy/store.d.ts.map +1 -1
  23. package/dist/cli/convoy/store.js +119 -14
  24. package/dist/cli/convoy/store.js.map +1 -1
  25. package/dist/cli/convoy/store.test.js +413 -4
  26. package/dist/cli/convoy/store.test.js.map +1 -1
  27. package/dist/cli/convoy/types.d.ts +21 -0
  28. package/dist/cli/convoy/types.d.ts.map +1 -1
  29. package/dist/cli/dashboard.d.ts.map +1 -1
  30. package/dist/cli/dashboard.js +1 -0
  31. package/dist/cli/dashboard.js.map +1 -1
  32. package/dist/cli/run/adapters/claude-code.d.ts.map +1 -1
  33. package/dist/cli/run/adapters/claude-code.js +13 -0
  34. package/dist/cli/run/adapters/claude-code.js.map +1 -1
  35. package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
  36. package/dist/cli/run/adapters/copilot.js +8 -0
  37. package/dist/cli/run/adapters/copilot.js.map +1 -1
  38. package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
  39. package/dist/cli/run/adapters/cursor.js +13 -0
  40. package/dist/cli/run/adapters/cursor.js.map +1 -1
  41. package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
  42. package/dist/cli/run/adapters/opencode.js +13 -0
  43. package/dist/cli/run/adapters/opencode.js.map +1 -1
  44. package/dist/cli/run/schema.d.ts +5 -1
  45. package/dist/cli/run/schema.d.ts.map +1 -1
  46. package/dist/cli/run/schema.js +41 -8
  47. package/dist/cli/run/schema.js.map +1 -1
  48. package/dist/cli/run/schema.test.js +194 -5
  49. package/dist/cli/run/schema.test.js.map +1 -1
  50. package/dist/cli/run.d.ts.map +1 -1
  51. package/dist/cli/run.js +167 -2
  52. package/dist/cli/run.js.map +1 -1
  53. package/dist/cli/types.d.ts +11 -1
  54. package/dist/cli/types.d.ts.map +1 -1
  55. package/package.json +1 -1
  56. package/src/cli/convoy/engine.test.ts +177 -0
  57. package/src/cli/convoy/engine.ts +24 -1
  58. package/src/cli/convoy/export.test.ts +37 -0
  59. package/src/cli/convoy/export.ts +46 -0
  60. package/src/cli/convoy/pipeline.test.ts +939 -0
  61. package/src/cli/convoy/pipeline.ts +430 -0
  62. package/src/cli/convoy/store.test.ts +452 -4
  63. package/src/cli/convoy/store.ts +152 -23
  64. package/src/cli/convoy/types.ts +23 -0
  65. package/src/cli/dashboard.ts +1 -0
  66. package/src/cli/run/adapters/claude-code.ts +13 -1
  67. package/src/cli/run/adapters/copilot.ts +8 -0
  68. package/src/cli/run/adapters/cursor.ts +13 -1
  69. package/src/cli/run/adapters/opencode.ts +13 -1
  70. package/src/cli/run/schema.test.ts +244 -5
  71. package/src/cli/run/schema.ts +49 -8
  72. package/src/cli/run.ts +163 -2
  73. package/src/cli/types.ts +12 -1
  74. package/src/dashboard/dist/_astro/{index.DyyaCW8L.css → index.Cq68OHaZ.css} +1 -1
  75. package/src/dashboard/dist/index.html +225 -3
  76. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  77. package/src/dashboard/src/pages/index.astro +241 -2
  78. package/src/dashboard/src/styles/dashboard.css +116 -0
  79. package/src/orchestrator/customizations/KNOWN-ISSUES.md +1 -1
@@ -7,23 +7,25 @@ import type {
7
7
  WorkerRecord,
8
8
  WorkerStatus,
9
9
  EventRecord,
10
+ PipelineRecord,
11
+ PipelineStatus,
10
12
  } from './types.js'
11
13
 
12
- const SCHEMA_VERSION = 2
14
+ const SCHEMA_VERSION = 4
13
15
 
14
16
  export interface ConvoyStore {
15
- insertConvoy(record: Omit<ConvoyRecord, 'started_at' | 'finished_at'>): void
17
+ insertConvoy(record: Omit<ConvoyRecord, 'started_at' | 'finished_at' | 'total_tokens' | 'total_cost_usd' | 'pipeline_id'> & { pipeline_id?: string | null }): void
16
18
  getConvoy(id: string): ConvoyRecord | undefined
17
19
  getLatestConvoy(): ConvoyRecord | undefined
18
20
  updateConvoyStatus(
19
21
  id: string,
20
22
  status: ConvoyStatus,
21
- extra?: { started_at?: string; finished_at?: string },
23
+ extra?: { started_at?: string; finished_at?: string; total_tokens?: number | null; total_cost_usd?: string | null },
22
24
  ): void
23
25
  insertTask(
24
26
  record: Omit<
25
27
  TaskRecord,
26
- 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at'
28
+ 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at' | 'prompt_tokens' | 'completion_tokens' | 'total_tokens' | 'cost_usd'
27
29
  >,
28
30
  ): void
29
31
  getTask(id: string, convoyId: string): TaskRecord | undefined
@@ -33,7 +35,7 @@ export interface ConvoyStore {
33
35
  convoyId: string,
34
36
  status: ConvoyTaskStatus,
35
37
  extra?: Partial<
36
- Pick<TaskRecord, 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at' | 'retries'>
38
+ Pick<TaskRecord, 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at' | 'retries' | 'prompt_tokens' | 'completion_tokens' | 'total_tokens' | 'cost_usd'>
37
39
  >,
38
40
  ): void
39
41
  getReadyTasks(convoyId: string): TaskRecord[]
@@ -46,6 +48,15 @@ export interface ConvoyStore {
46
48
  ): void
47
49
  insertEvent(record: Omit<EventRecord, 'id'>): void
48
50
  getEvents(convoyId: string): EventRecord[]
51
+ insertPipeline(record: Omit<PipelineRecord, 'started_at' | 'finished_at' | 'total_tokens' | 'total_cost_usd'>): void
52
+ getPipeline(id: string): PipelineRecord | undefined
53
+ getLatestPipeline(): PipelineRecord | undefined
54
+ updatePipelineStatus(
55
+ id: string,
56
+ status: PipelineStatus,
57
+ extra?: { started_at?: string; finished_at?: string; total_tokens?: number | null; total_cost_usd?: string | null },
58
+ ): void
59
+ getConvoysByPipeline(pipelineId: string): ConvoyRecord[]
49
60
  withTransaction<T>(fn: () => T): T
50
61
  close(): void
51
62
  }
@@ -61,8 +72,8 @@ class ConvoyStoreImpl implements ConvoyStore {
61
72
  }
62
73
 
63
74
  private initSchema(): void {
64
- const row = this.db.prepare('PRAGMA user_version').get() as { user_version: number }
65
- if (row.user_version === 0) {
75
+ let version = (this.db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version
76
+ if (version === 0) {
66
77
  this.db.exec(`
67
78
  CREATE TABLE IF NOT EXISTS convoy (
68
79
  id TEXT PRIMARY KEY,
@@ -72,8 +83,25 @@ class ConvoyStoreImpl implements ConvoyStore {
72
83
  branch TEXT,
73
84
  created_at TEXT NOT NULL,
74
85
  started_at TEXT,
75
- finished_at TEXT,
76
- spec_yaml TEXT NOT NULL
86
+ finished_at TEXT,
87
+ spec_yaml TEXT NOT NULL,
88
+ total_tokens INTEGER,
89
+ total_cost_usd TEXT,
90
+ pipeline_id TEXT
91
+ );
92
+
93
+ CREATE TABLE IF NOT EXISTS pipeline (
94
+ id TEXT PRIMARY KEY,
95
+ name TEXT NOT NULL,
96
+ status TEXT NOT NULL DEFAULT 'pending',
97
+ branch TEXT,
98
+ spec_yaml TEXT NOT NULL,
99
+ convoy_specs TEXT NOT NULL,
100
+ created_at TEXT NOT NULL,
101
+ started_at TEXT,
102
+ finished_at TEXT,
103
+ total_tokens INTEGER,
104
+ total_cost_usd TEXT
77
105
  );
78
106
 
79
107
  CREATE TABLE IF NOT EXISTS task (
@@ -92,10 +120,14 @@ class ConvoyStoreImpl implements ConvoyStore {
92
120
  exit_code INTEGER,
93
121
  started_at TEXT,
94
122
  finished_at TEXT,
95
- retries INTEGER NOT NULL DEFAULT 0,
96
- max_retries INTEGER NOT NULL DEFAULT 1,
97
- files TEXT,
98
- depends_on TEXT
123
+ retries INTEGER NOT NULL DEFAULT 0,
124
+ max_retries INTEGER NOT NULL DEFAULT 1,
125
+ files TEXT,
126
+ depends_on TEXT,
127
+ prompt_tokens INTEGER,
128
+ completion_tokens INTEGER,
129
+ total_tokens INTEGER,
130
+ cost_usd TEXT
99
131
  );
100
132
 
101
133
  CREATE TABLE IF NOT EXISTS worker (
@@ -122,20 +154,52 @@ class ConvoyStoreImpl implements ConvoyStore {
122
154
  );
123
155
  `)
124
156
  this.db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`)
157
+ version = SCHEMA_VERSION
125
158
  }
126
- if (row.user_version === 1) {
159
+ if (version === 1) {
127
160
  this.db.exec('ALTER TABLE task ADD COLUMN adapter TEXT')
128
161
  this.db.exec('PRAGMA user_version = 2')
162
+ version = 2
163
+ }
164
+ if (version === 2) {
165
+ this.db.exec('ALTER TABLE task ADD COLUMN prompt_tokens INTEGER')
166
+ this.db.exec('ALTER TABLE task ADD COLUMN completion_tokens INTEGER')
167
+ this.db.exec('ALTER TABLE task ADD COLUMN total_tokens INTEGER')
168
+ this.db.exec('ALTER TABLE task ADD COLUMN cost_usd TEXT')
169
+ this.db.exec('ALTER TABLE convoy ADD COLUMN total_tokens INTEGER')
170
+ this.db.exec('ALTER TABLE convoy ADD COLUMN total_cost_usd TEXT')
171
+ this.db.exec('PRAGMA user_version = 3')
172
+ version = 3
173
+ }
174
+ if (version === 3) {
175
+ this.db.exec(`
176
+ CREATE TABLE IF NOT EXISTS pipeline (
177
+ id TEXT PRIMARY KEY,
178
+ name TEXT NOT NULL,
179
+ status TEXT NOT NULL DEFAULT 'pending',
180
+ branch TEXT,
181
+ spec_yaml TEXT NOT NULL,
182
+ convoy_specs TEXT NOT NULL,
183
+ created_at TEXT NOT NULL,
184
+ started_at TEXT,
185
+ finished_at TEXT,
186
+ total_tokens INTEGER,
187
+ total_cost_usd TEXT
188
+ )
189
+ `)
190
+ this.db.exec('ALTER TABLE convoy ADD COLUMN pipeline_id TEXT')
191
+ this.db.exec('PRAGMA user_version = 4')
192
+ version = 4
129
193
  }
130
194
  }
131
195
 
132
- insertConvoy(record: Omit<ConvoyRecord, 'started_at' | 'finished_at'>): void {
196
+ insertConvoy(record: Omit<ConvoyRecord, 'started_at' | 'finished_at' | 'total_tokens' | 'total_cost_usd' | 'pipeline_id'> & { pipeline_id?: string | null }): void {
133
197
  this.db
134
198
  .prepare(
135
- `INSERT INTO convoy (id, name, spec_hash, status, branch, created_at, started_at, finished_at, spec_yaml)
136
- VALUES (:id, :name, :spec_hash, :status, :branch, :created_at, NULL, NULL, :spec_yaml)`,
199
+ `INSERT INTO convoy (id, name, spec_hash, status, branch, created_at, started_at, finished_at, spec_yaml, pipeline_id)
200
+ VALUES (:id, :name, :spec_hash, :status, :branch, :created_at, NULL, NULL, :spec_yaml, :pipeline_id)`,
137
201
  )
138
- .run(record)
202
+ .run({ ...record, pipeline_id: record.pipeline_id ?? null })
139
203
  }
140
204
 
141
205
  getConvoy(id: string): ConvoyRecord | undefined {
@@ -153,10 +217,10 @@ class ConvoyStoreImpl implements ConvoyStore {
153
217
  updateConvoyStatus(
154
218
  id: string,
155
219
  status: ConvoyStatus,
156
- extra?: { started_at?: string; finished_at?: string },
220
+ extra?: { started_at?: string; finished_at?: string; total_tokens?: number | null; total_cost_usd?: string | null },
157
221
  ): void {
158
222
  const sets = ['status = :status']
159
- const params: Record<string, string | null> = { id, status }
223
+ const params: Record<string, string | number | null> = { id, status }
160
224
 
161
225
  if (extra?.started_at !== undefined) {
162
226
  sets.push('started_at = :started_at')
@@ -166,6 +230,14 @@ class ConvoyStoreImpl implements ConvoyStore {
166
230
  sets.push('finished_at = :finished_at')
167
231
  params.finished_at = extra.finished_at
168
232
  }
233
+ if (extra?.total_tokens !== undefined) {
234
+ sets.push('total_tokens = :total_tokens')
235
+ params.total_tokens = extra.total_tokens
236
+ }
237
+ if (extra?.total_cost_usd !== undefined) {
238
+ sets.push('total_cost_usd = :total_cost_usd')
239
+ params.total_cost_usd = extra.total_cost_usd
240
+ }
169
241
 
170
242
  this.db.prepare(`UPDATE convoy SET ${sets.join(', ')} WHERE id = :id`).run(params)
171
243
  }
@@ -173,7 +245,7 @@ class ConvoyStoreImpl implements ConvoyStore {
173
245
  insertTask(
174
246
  record: Omit<
175
247
  TaskRecord,
176
- 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at'
248
+ 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at' | 'prompt_tokens' | 'completion_tokens' | 'total_tokens' | 'cost_usd'
177
249
  >,
178
250
  ): void {
179
251
  this.db
@@ -207,12 +279,12 @@ class ConvoyStoreImpl implements ConvoyStore {
207
279
  convoyId: string,
208
280
  status: ConvoyTaskStatus,
209
281
  extra?: Partial<
210
- Pick<TaskRecord, 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at' | 'retries'>
282
+ Pick<TaskRecord, 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at' | 'retries' | 'prompt_tokens' | 'completion_tokens' | 'total_tokens' | 'cost_usd'>
211
283
  >,
212
284
  ): void {
213
285
  const sets = ['status = :status']
214
286
  const params: Record<string, string | number | null> = { id, convoy_id: convoyId, status }
215
- const extraFields = ['worker_id', 'worktree', 'output', 'exit_code', 'started_at', 'finished_at', 'retries'] as const
287
+ const extraFields = ['worker_id', 'worktree', 'output', 'exit_code', 'started_at', 'finished_at', 'retries', 'prompt_tokens', 'completion_tokens', 'total_tokens', 'cost_usd'] as const
216
288
 
217
289
  if (extra) {
218
290
  for (const field of extraFields) {
@@ -298,6 +370,63 @@ class ConvoyStoreImpl implements ConvoyStore {
298
370
  .all({ convoy_id: convoyId }) as unknown as EventRecord[]
299
371
  }
300
372
 
373
+ insertPipeline(record: Omit<PipelineRecord, 'started_at' | 'finished_at' | 'total_tokens' | 'total_cost_usd'>): void {
374
+ this.db
375
+ .prepare(
376
+ `INSERT INTO pipeline (id, name, status, branch, spec_yaml, convoy_specs, created_at,
377
+ started_at, finished_at, total_tokens, total_cost_usd)
378
+ VALUES (:id, :name, :status, :branch, :spec_yaml, :convoy_specs, :created_at,
379
+ NULL, NULL, NULL, NULL)`,
380
+ )
381
+ .run(record)
382
+ }
383
+
384
+ getPipeline(id: string): PipelineRecord | undefined {
385
+ return this.db
386
+ .prepare('SELECT * FROM pipeline WHERE id = :id')
387
+ .get({ id }) as PipelineRecord | undefined
388
+ }
389
+
390
+ getLatestPipeline(): PipelineRecord | undefined {
391
+ return this.db
392
+ .prepare('SELECT * FROM pipeline ORDER BY created_at DESC LIMIT 1')
393
+ .get() as PipelineRecord | undefined
394
+ }
395
+
396
+ updatePipelineStatus(
397
+ id: string,
398
+ status: PipelineStatus,
399
+ extra?: { started_at?: string; finished_at?: string; total_tokens?: number | null; total_cost_usd?: string | null },
400
+ ): void {
401
+ const sets = ['status = :status']
402
+ const params: Record<string, string | number | null> = { id, status }
403
+
404
+ if (extra?.started_at !== undefined) {
405
+ sets.push('started_at = :started_at')
406
+ params.started_at = extra.started_at
407
+ }
408
+ if (extra?.finished_at !== undefined) {
409
+ sets.push('finished_at = :finished_at')
410
+ params.finished_at = extra.finished_at
411
+ }
412
+ if (extra?.total_tokens !== undefined) {
413
+ sets.push('total_tokens = :total_tokens')
414
+ params.total_tokens = extra.total_tokens
415
+ }
416
+ if (extra?.total_cost_usd !== undefined) {
417
+ sets.push('total_cost_usd = :total_cost_usd')
418
+ params.total_cost_usd = extra.total_cost_usd
419
+ }
420
+
421
+ this.db.prepare(`UPDATE pipeline SET ${sets.join(', ')} WHERE id = :id`).run(params)
422
+ }
423
+
424
+ getConvoysByPipeline(pipelineId: string): ConvoyRecord[] {
425
+ return this.db
426
+ .prepare('SELECT * FROM convoy WHERE pipeline_id = :pipeline_id ORDER BY created_at')
427
+ .all({ pipeline_id: pipelineId }) as unknown as ConvoyRecord[]
428
+ }
429
+
301
430
  withTransaction<T>(fn: () => T): T {
302
431
  this.db.exec('BEGIN')
303
432
  try {
@@ -11,6 +11,8 @@ export type ConvoyTaskStatus =
11
11
 
12
12
  export type WorkerStatus = 'spawned' | 'running' | 'done' | 'failed' | 'killed'
13
13
 
14
+ export type PipelineStatus = 'pending' | 'running' | 'done' | 'failed'
15
+
14
16
  export interface ConvoyRecord {
15
17
  id: string
16
18
  name: string
@@ -21,6 +23,9 @@ export interface ConvoyRecord {
21
23
  started_at: string | null
22
24
  finished_at: string | null
23
25
  spec_yaml: string
26
+ total_tokens: number | null
27
+ total_cost_usd: string | null
28
+ pipeline_id: string | null
24
29
  }
25
30
 
26
31
  export interface TaskRecord {
@@ -43,6 +48,10 @@ export interface TaskRecord {
43
48
  max_retries: number
44
49
  files: string | null
45
50
  depends_on: string | null
51
+ prompt_tokens: number | null
52
+ completion_tokens: number | null
53
+ total_tokens: number | null
54
+ cost_usd: string | null
46
55
  }
47
56
 
48
57
  export interface WorkerRecord {
@@ -67,3 +76,17 @@ export interface EventRecord {
67
76
  data: string | null
68
77
  created_at: string
69
78
  }
79
+
80
+ export interface PipelineRecord {
81
+ id: string
82
+ name: string
83
+ status: PipelineStatus
84
+ branch: string | null
85
+ spec_yaml: string
86
+ convoy_specs: string
87
+ created_at: string
88
+ started_at: string | null
89
+ finished_at: string | null
90
+ total_tokens: number | null
91
+ total_cost_usd: string | null
92
+ }
@@ -21,6 +21,7 @@ const MIME_TYPES: Record<string, string> = {
21
21
  const DATA_FILES = [
22
22
  'events.ndjson',
23
23
  'convoys.ndjson',
24
+ 'pipelines.ndjson',
24
25
  // Legacy individual files — kept for backwards compatibility
25
26
  'sessions.ndjson',
26
27
  'delegations.ndjson',
@@ -1,5 +1,5 @@
1
1
  import { spawn } from 'node:child_process'
2
- import type { Task, ExecuteOptions, ExecuteResult } from '../../types.js'
2
+ import type { Task, ExecuteOptions, ExecuteResult, TokenUsage } from '../../types.js'
3
3
 
4
4
  /** Adapter name */
5
5
  export const name = 'claude-code'
@@ -60,10 +60,22 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
60
60
 
61
61
  proc.on('close', (code) => {
62
62
  const output = [stdout, stderr].filter(Boolean).join('\n')
63
+ let usage: TokenUsage | undefined
64
+ try {
65
+ const parsed = JSON.parse(stdout) as Record<string, unknown>
66
+ const u = parsed?.usage as Record<string, number> | undefined
67
+ if (u) {
68
+ const promptTokens = (u.input_tokens ?? u.prompt_tokens) as number | undefined
69
+ const completionTokens = (u.output_tokens ?? u.completion_tokens) as number | undefined
70
+ const total = ((promptTokens ?? 0) + (completionTokens ?? 0)) || undefined
71
+ usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: total }
72
+ }
73
+ } catch { /* not JSON or no usage — graceful degradation */ }
63
74
  resolve({
64
75
  success: code === 0,
65
76
  output: output.slice(0, 10000), // Cap output size
66
77
  exitCode: code ?? -1,
78
+ usage,
67
79
  })
68
80
  })
69
81
 
@@ -100,11 +100,19 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
100
100
  const timeoutMs = parseTimeout(task.timeout)
101
101
  const response = await session.sendAndWait({ prompt }, timeoutMs)
102
102
  const output = response?.data?.content ?? ''
103
+ const rawUsage = (response?.data as Record<string, unknown> | undefined)?.usage ?? (response as Record<string, unknown> | undefined)?.usage
104
+ const u = rawUsage as Record<string, number> | undefined
105
+ const usageResult = u ? {
106
+ prompt_tokens: u.prompt_tokens ?? u.promptTokens,
107
+ completion_tokens: u.completion_tokens ?? u.completionTokens,
108
+ total_tokens: u.total_tokens ?? u.totalTokens,
109
+ } : undefined
103
110
 
104
111
  return {
105
112
  success: true,
106
113
  output: output.slice(0, 10_000),
107
114
  exitCode: 0,
115
+ usage: usageResult,
108
116
  }
109
117
  } catch (err: unknown) {
110
118
  return {
@@ -1,5 +1,5 @@
1
1
  import { spawn } from 'node:child_process'
2
- import type { Task, ExecuteOptions, ExecuteResult } from '../../types.js'
2
+ import type { Task, ExecuteOptions, ExecuteResult, TokenUsage } from '../../types.js'
3
3
 
4
4
  /** Adapter name */
5
5
  export const name = 'cursor'
@@ -59,10 +59,22 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
59
59
 
60
60
  proc.on('close', (code) => {
61
61
  const output = [stdout, stderr].filter(Boolean).join('\n')
62
+ let usage: TokenUsage | undefined
63
+ try {
64
+ const parsed = JSON.parse(stdout) as Record<string, unknown>
65
+ const u = parsed?.usage as Record<string, number> | undefined
66
+ if (u) {
67
+ const promptTokens = (u.input_tokens ?? u.prompt_tokens) as number | undefined
68
+ const completionTokens = (u.output_tokens ?? u.completion_tokens) as number | undefined
69
+ const total = ((promptTokens ?? 0) + (completionTokens ?? 0)) || undefined
70
+ usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: total }
71
+ }
72
+ } catch { /* not JSON or no usage — graceful degradation */ }
62
73
  resolve({
63
74
  success: code === 0,
64
75
  output: output.slice(0, 10000), // Cap output size
65
76
  exitCode: code ?? -1,
77
+ usage,
66
78
  })
67
79
  })
68
80
 
@@ -1,5 +1,5 @@
1
1
  import { spawn } from 'node:child_process'
2
- import type { Task, ExecuteOptions, ExecuteResult } from '../../types.js'
2
+ import type { Task, ExecuteOptions, ExecuteResult, TokenUsage } from '../../types.js'
3
3
 
4
4
  /** Adapter name */
5
5
  export const name = 'opencode'
@@ -53,10 +53,22 @@ export async function execute(task: Task, options: ExecuteOptions = {}): Promise
53
53
 
54
54
  proc.on('close', (code) => {
55
55
  const output = [stdout, stderr].filter(Boolean).join('\n')
56
+ let usage: TokenUsage | undefined
57
+ try {
58
+ const parsed = JSON.parse(stdout) as Record<string, unknown>
59
+ const u = parsed?.usage as Record<string, number> | undefined
60
+ if (u) {
61
+ const promptTokens = (u.input_tokens ?? u.prompt_tokens) as number | undefined
62
+ const completionTokens = (u.output_tokens ?? u.completion_tokens) as number | undefined
63
+ const total = ((promptTokens ?? 0) + (completionTokens ?? 0)) || undefined
64
+ usage = { prompt_tokens: promptTokens, completion_tokens: completionTokens, total_tokens: total }
65
+ }
66
+ } catch { /* not JSON or no usage — graceful degradation */ }
56
67
  resolve({
57
68
  success: code === 0,
58
69
  output: output.slice(0, 10000), // Cap output size
59
70
  exitCode: code ?? -1,
71
+ usage,
60
72
  })
61
73
  })
62
74