opencastle 0.21.0 → 0.22.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 (57) hide show
  1. package/dist/cli/convoy/engine.d.ts +3 -0
  2. package/dist/cli/convoy/engine.d.ts.map +1 -1
  3. package/dist/cli/convoy/engine.js +22 -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.map +1 -1
  8. package/dist/cli/convoy/export.js +5 -0
  9. package/dist/cli/convoy/export.js.map +1 -1
  10. package/dist/cli/convoy/export.test.js +31 -0
  11. package/dist/cli/convoy/export.test.js.map +1 -1
  12. package/dist/cli/convoy/store.d.ts +5 -3
  13. package/dist/cli/convoy/store.d.ts.map +1 -1
  14. package/dist/cli/convoy/store.js +37 -11
  15. package/dist/cli/convoy/store.js.map +1 -1
  16. package/dist/cli/convoy/store.test.js +204 -4
  17. package/dist/cli/convoy/store.test.js.map +1 -1
  18. package/dist/cli/convoy/types.d.ts +6 -0
  19. package/dist/cli/convoy/types.d.ts.map +1 -1
  20. package/dist/cli/init.d.ts.map +1 -1
  21. package/dist/cli/init.js +25 -30
  22. package/dist/cli/init.js.map +1 -1
  23. package/dist/cli/run/adapters/claude-code.d.ts.map +1 -1
  24. package/dist/cli/run/adapters/claude-code.js +13 -0
  25. package/dist/cli/run/adapters/claude-code.js.map +1 -1
  26. package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
  27. package/dist/cli/run/adapters/copilot.js +8 -0
  28. package/dist/cli/run/adapters/copilot.js.map +1 -1
  29. package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
  30. package/dist/cli/run/adapters/cursor.js +13 -0
  31. package/dist/cli/run/adapters/cursor.js.map +1 -1
  32. package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
  33. package/dist/cli/run/adapters/opencode.js +13 -0
  34. package/dist/cli/run/adapters/opencode.js.map +1 -1
  35. package/dist/cli/run.d.ts.map +1 -1
  36. package/dist/cli/run.js +26 -0
  37. package/dist/cli/run.js.map +1 -1
  38. package/dist/cli/types.d.ts +8 -0
  39. package/dist/cli/types.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/cli/convoy/engine.test.ts +177 -0
  42. package/src/cli/convoy/engine.ts +22 -1
  43. package/src/cli/convoy/export.test.ts +37 -0
  44. package/src/cli/convoy/export.ts +5 -0
  45. package/src/cli/convoy/store.test.ts +220 -4
  46. package/src/cli/convoy/store.ts +46 -20
  47. package/src/cli/convoy/types.ts +6 -0
  48. package/src/cli/init.ts +25 -30
  49. package/src/cli/run/adapters/claude-code.ts +13 -1
  50. package/src/cli/run/adapters/copilot.ts +8 -0
  51. package/src/cli/run/adapters/cursor.ts +13 -1
  52. package/src/cli/run/adapters/opencode.ts +13 -1
  53. package/src/cli/run.ts +23 -0
  54. package/src/cli/types.ts +9 -0
  55. package/src/dashboard/dist/index.html +11 -1
  56. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  57. package/src/dashboard/src/pages/index.astro +11 -1
@@ -9,21 +9,21 @@ import type {
9
9
  EventRecord,
10
10
  } from './types.js'
11
11
 
12
- const SCHEMA_VERSION = 2
12
+ const SCHEMA_VERSION = 3
13
13
 
14
14
  export interface ConvoyStore {
15
- insertConvoy(record: Omit<ConvoyRecord, 'started_at' | 'finished_at'>): void
15
+ insertConvoy(record: Omit<ConvoyRecord, 'started_at' | 'finished_at' | 'total_tokens' | 'total_cost_usd'>): void
16
16
  getConvoy(id: string): ConvoyRecord | undefined
17
17
  getLatestConvoy(): ConvoyRecord | undefined
18
18
  updateConvoyStatus(
19
19
  id: string,
20
20
  status: ConvoyStatus,
21
- extra?: { started_at?: string; finished_at?: string },
21
+ extra?: { started_at?: string; finished_at?: string; total_tokens?: number | null; total_cost_usd?: string | null },
22
22
  ): void
23
23
  insertTask(
24
24
  record: Omit<
25
25
  TaskRecord,
26
- 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at'
26
+ 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at' | 'prompt_tokens' | 'completion_tokens' | 'total_tokens' | 'cost_usd'
27
27
  >,
28
28
  ): void
29
29
  getTask(id: string, convoyId: string): TaskRecord | undefined
@@ -33,7 +33,7 @@ export interface ConvoyStore {
33
33
  convoyId: string,
34
34
  status: ConvoyTaskStatus,
35
35
  extra?: Partial<
36
- Pick<TaskRecord, 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at' | 'retries'>
36
+ Pick<TaskRecord, 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at' | 'retries' | 'prompt_tokens' | 'completion_tokens' | 'total_tokens' | 'cost_usd'>
37
37
  >,
38
38
  ): void
39
39
  getReadyTasks(convoyId: string): TaskRecord[]
@@ -61,8 +61,8 @@ class ConvoyStoreImpl implements ConvoyStore {
61
61
  }
62
62
 
63
63
  private initSchema(): void {
64
- const row = this.db.prepare('PRAGMA user_version').get() as { user_version: number }
65
- if (row.user_version === 0) {
64
+ let version = (this.db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version
65
+ if (version === 0) {
66
66
  this.db.exec(`
67
67
  CREATE TABLE IF NOT EXISTS convoy (
68
68
  id TEXT PRIMARY KEY,
@@ -72,8 +72,10 @@ class ConvoyStoreImpl implements ConvoyStore {
72
72
  branch TEXT,
73
73
  created_at TEXT NOT NULL,
74
74
  started_at TEXT,
75
- finished_at TEXT,
76
- spec_yaml TEXT NOT NULL
75
+ finished_at TEXT,
76
+ spec_yaml TEXT NOT NULL,
77
+ total_tokens INTEGER,
78
+ total_cost_usd TEXT
77
79
  );
78
80
 
79
81
  CREATE TABLE IF NOT EXISTS task (
@@ -92,10 +94,14 @@ class ConvoyStoreImpl implements ConvoyStore {
92
94
  exit_code INTEGER,
93
95
  started_at TEXT,
94
96
  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
97
+ retries INTEGER NOT NULL DEFAULT 0,
98
+ max_retries INTEGER NOT NULL DEFAULT 1,
99
+ files TEXT,
100
+ depends_on TEXT,
101
+ prompt_tokens INTEGER,
102
+ completion_tokens INTEGER,
103
+ total_tokens INTEGER,
104
+ cost_usd TEXT
99
105
  );
100
106
 
101
107
  CREATE TABLE IF NOT EXISTS worker (
@@ -122,14 +128,26 @@ class ConvoyStoreImpl implements ConvoyStore {
122
128
  );
123
129
  `)
124
130
  this.db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`)
131
+ version = SCHEMA_VERSION
125
132
  }
126
- if (row.user_version === 1) {
133
+ if (version === 1) {
127
134
  this.db.exec('ALTER TABLE task ADD COLUMN adapter TEXT')
128
135
  this.db.exec('PRAGMA user_version = 2')
136
+ version = 2
137
+ }
138
+ if (version === 2) {
139
+ this.db.exec('ALTER TABLE task ADD COLUMN prompt_tokens INTEGER')
140
+ this.db.exec('ALTER TABLE task ADD COLUMN completion_tokens INTEGER')
141
+ this.db.exec('ALTER TABLE task ADD COLUMN total_tokens INTEGER')
142
+ this.db.exec('ALTER TABLE task ADD COLUMN cost_usd TEXT')
143
+ this.db.exec('ALTER TABLE convoy ADD COLUMN total_tokens INTEGER')
144
+ this.db.exec('ALTER TABLE convoy ADD COLUMN total_cost_usd TEXT')
145
+ this.db.exec('PRAGMA user_version = 3')
146
+ version = 3
129
147
  }
130
148
  }
131
149
 
132
- insertConvoy(record: Omit<ConvoyRecord, 'started_at' | 'finished_at'>): void {
150
+ insertConvoy(record: Omit<ConvoyRecord, 'started_at' | 'finished_at' | 'total_tokens' | 'total_cost_usd'>): void {
133
151
  this.db
134
152
  .prepare(
135
153
  `INSERT INTO convoy (id, name, spec_hash, status, branch, created_at, started_at, finished_at, spec_yaml)
@@ -153,10 +171,10 @@ class ConvoyStoreImpl implements ConvoyStore {
153
171
  updateConvoyStatus(
154
172
  id: string,
155
173
  status: ConvoyStatus,
156
- extra?: { started_at?: string; finished_at?: string },
174
+ extra?: { started_at?: string; finished_at?: string; total_tokens?: number | null; total_cost_usd?: string | null },
157
175
  ): void {
158
176
  const sets = ['status = :status']
159
- const params: Record<string, string | null> = { id, status }
177
+ const params: Record<string, string | number | null> = { id, status }
160
178
 
161
179
  if (extra?.started_at !== undefined) {
162
180
  sets.push('started_at = :started_at')
@@ -166,6 +184,14 @@ class ConvoyStoreImpl implements ConvoyStore {
166
184
  sets.push('finished_at = :finished_at')
167
185
  params.finished_at = extra.finished_at
168
186
  }
187
+ if (extra?.total_tokens !== undefined) {
188
+ sets.push('total_tokens = :total_tokens')
189
+ params.total_tokens = extra.total_tokens
190
+ }
191
+ if (extra?.total_cost_usd !== undefined) {
192
+ sets.push('total_cost_usd = :total_cost_usd')
193
+ params.total_cost_usd = extra.total_cost_usd
194
+ }
169
195
 
170
196
  this.db.prepare(`UPDATE convoy SET ${sets.join(', ')} WHERE id = :id`).run(params)
171
197
  }
@@ -173,7 +199,7 @@ class ConvoyStoreImpl implements ConvoyStore {
173
199
  insertTask(
174
200
  record: Omit<
175
201
  TaskRecord,
176
- 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at'
202
+ 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at' | 'prompt_tokens' | 'completion_tokens' | 'total_tokens' | 'cost_usd'
177
203
  >,
178
204
  ): void {
179
205
  this.db
@@ -207,12 +233,12 @@ class ConvoyStoreImpl implements ConvoyStore {
207
233
  convoyId: string,
208
234
  status: ConvoyTaskStatus,
209
235
  extra?: Partial<
210
- Pick<TaskRecord, 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at' | 'retries'>
236
+ Pick<TaskRecord, 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at' | 'retries' | 'prompt_tokens' | 'completion_tokens' | 'total_tokens' | 'cost_usd'>
211
237
  >,
212
238
  ): void {
213
239
  const sets = ['status = :status']
214
240
  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
241
+ const extraFields = ['worker_id', 'worktree', 'output', 'exit_code', 'started_at', 'finished_at', 'retries', 'prompt_tokens', 'completion_tokens', 'total_tokens', 'cost_usd'] as const
216
242
 
217
243
  if (extra) {
218
244
  for (const field of extraFields) {
@@ -21,6 +21,8 @@ export interface ConvoyRecord {
21
21
  started_at: string | null
22
22
  finished_at: string | null
23
23
  spec_yaml: string
24
+ total_tokens: number | null
25
+ total_cost_usd: string | null
24
26
  }
25
27
 
26
28
  export interface TaskRecord {
@@ -43,6 +45,10 @@ export interface TaskRecord {
43
45
  max_retries: number
44
46
  files: string | null
45
47
  depends_on: string | null
48
+ prompt_tokens: number | null
49
+ completion_tokens: number | null
50
+ total_tokens: number | null
51
+ cost_usd: string | null
46
52
  }
47
53
 
48
54
  export interface WorkerRecord {
package/src/cli/init.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { resolve } from 'node:path'
2
2
  import { readFile, unlink } from 'node:fs/promises'
3
3
  import { existsSync } from 'node:fs'
4
- import { multiselect, confirm, closePrompts, c } from './prompt.js'
4
+ import { select, multiselect, confirm, closePrompts, c } from './prompt.js'
5
5
  import { readManifest, writeManifest, createManifest } from './manifest.js'
6
6
  import { removeDirIfExists, copyDir, getOrchestratorRoot } from './copy.js'
7
7
  import { updateGitignore } from './gitignore.js'
@@ -50,36 +50,31 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
50
50
  console.log(` ${c.dim('No tooling detected (empty project?)')}\n`)
51
51
  }
52
52
 
53
- // ── IDEs (multiselect, at least 1) ─────────────────────────────
53
+ // ── IDE (single select) ────────────────────────────────────────
54
54
  console.log(` ${c.bold('── IDEs ──────────────────────────────────────')}`)
55
- let ides: string[] = []
56
- while (ides.length === 0) {
57
- ides = await multiselect('Which IDEs do you use?', [
58
- {
59
- label: 'VS Code',
60
- hint: 'GitHub Copilot agents, instructions, skills',
61
- value: 'vscode',
62
- },
63
- {
64
- label: 'Cursor',
65
- hint: '.cursorrules & .cursor/rules/*.mdc',
66
- value: 'cursor',
67
- },
68
- {
69
- label: 'Claude Code',
70
- hint: 'CLAUDE.md & .claude/ commands, skills',
71
- value: 'claude-code',
72
- },
73
- {
74
- label: 'OpenCode',
75
- hint: 'AGENTS.md & opencode.json',
76
- value: 'opencode',
77
- },
78
- ])
79
- if (ides.length === 0) {
80
- console.log(` ${c.yellow('Please select at least one IDE.')}`)
81
- }
82
- }
55
+ const selectedIde = await select('Which IDE do you use?', [
56
+ {
57
+ label: 'VS Code',
58
+ hint: 'GitHub Copilot agents, instructions, skills',
59
+ value: 'vscode',
60
+ },
61
+ {
62
+ label: 'Cursor',
63
+ hint: '.cursorrules & .cursor/rules/*.mdc',
64
+ value: 'cursor',
65
+ },
66
+ {
67
+ label: 'Claude Code',
68
+ hint: 'CLAUDE.md & .claude/ commands, skills',
69
+ value: 'claude-code',
70
+ },
71
+ {
72
+ label: 'OpenCode',
73
+ hint: 'AGENTS.md & opencode.json',
74
+ value: 'opencode',
75
+ },
76
+ ])
77
+ const ides = [selectedIde]
83
78
 
84
79
  // ── Tech Tools (multiselect, 0-N) ──────────────────────────────
85
80
  // Pre-select tools already detected in the repo
@@ -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
 
package/src/cli/run.ts CHANGED
@@ -8,6 +8,12 @@ import { createReporter, printExecutionPlan } from './run/reporter.js'
8
8
  import type { CliContext, RunOptions } from './types.js'
9
9
  import type { ConvoyResult } from './convoy/engine.js'
10
10
 
11
+ function formatTokens(n: number): string {
12
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M'
13
+ if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K'
14
+ return String(n)
15
+ }
16
+
11
17
  const HELP = `
12
18
  opencastle run [options]
13
19
 
@@ -153,6 +159,9 @@ function printConvoyResult(result: ConvoyResult): void {
153
159
  console.log(` ${g.passed ? '✓' : '✗'} ${g.command}`)
154
160
  }
155
161
  }
162
+ if (result.cost) {
163
+ console.log(` Tokens: ${formatTokens(result.cost.total_tokens)}`)
164
+ }
156
165
  }
157
166
 
158
167
  /**
@@ -199,6 +208,20 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
199
208
  console.log(` ${status}: ${count}`)
200
209
  }
201
210
  console.log(` total: ${tasks.length}`)
211
+ const totalTokens = tasks.reduce((sum, t) => sum + (t.total_tokens ?? 0), 0)
212
+ if (tasks.some(t => t.total_tokens != null)) {
213
+ console.log(`\n Tokens: ${formatTokens(totalTokens)}`)
214
+ const tasksWithTokens = tasks.filter(t => t.total_tokens != null)
215
+ if (tasksWithTokens.length > 0) {
216
+ console.log(`\n Token usage by task:`)
217
+ for (const t of tasksWithTokens) {
218
+ const parts = [formatTokens(t.total_tokens!)]
219
+ if (t.prompt_tokens != null) parts.push(`in: ${formatTokens(t.prompt_tokens)}`)
220
+ if (t.completion_tokens != null) parts.push(`out: ${formatTokens(t.completion_tokens)}`)
221
+ console.log(` ${t.id}: ${parts.join(' | ')}`)
222
+ }
223
+ }
224
+ }
202
225
  } finally {
203
226
  store.close()
204
227
  }
package/src/cli/types.ts CHANGED
@@ -238,6 +238,13 @@ export interface ExecuteOptions {
238
238
  cwd?: string;
239
239
  }
240
240
 
241
+ /** Token usage data from adapter execution. */
242
+ export interface TokenUsage {
243
+ prompt_tokens?: number;
244
+ completion_tokens?: number;
245
+ total_tokens?: number;
246
+ }
247
+
241
248
  /** Result from an agent adapter execution. */
242
249
  export interface ExecuteResult {
243
250
  success: boolean;
@@ -245,6 +252,8 @@ export interface ExecuteResult {
245
252
  exitCode: number;
246
253
  _timedOut?: boolean;
247
254
  taskId?: string;
255
+ /** Token usage data if available from the adapter. */
256
+ usage?: TokenUsage;
248
257
  }
249
258
 
250
259
  /** Reporter interface for the run command. */
@@ -66,6 +66,12 @@ Export
66
66
  return div.innerHTML;
67
67
  }
68
68
 
69
+ function formatTokens(n) {
70
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
71
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
72
+ return String(n);
73
+ }
74
+
69
75
  // ── SVG Icon Library (empty states) ────────────────────
70
76
 
71
77
  const EMPTY_ICONS = {
@@ -1133,6 +1139,9 @@ Export
1133
1139
  html += '<div class="convoy-stat"><span class="convoy-stat__label">Tasks</span><span class="convoy-stat__value">' + done + '/' + total + '</span></div>';
1134
1140
  html += '<div class="convoy-stat"><span class="convoy-stat__label">Events</span><span class="convoy-stat__value">' + (convoy.events_count || 0) + '</span></div>';
1135
1141
  html += '<div class="convoy-stat"><span class="convoy-stat__label">Started</span><span class="convoy-stat__value">' + (convoy.started_at ? formatTime(convoy.started_at) : '\u2014') + '</span></div>';
1142
+ if (convoy.total_tokens != null) {
1143
+ html += '<div class="convoy-stat"><span class="convoy-stat__label">Tokens</span><span class="convoy-stat__value">' + formatTokens(convoy.total_tokens) + '</span></div>';
1144
+ }
1136
1145
  html += '</div>';
1137
1146
 
1138
1147
  html += '<div class="convoy-progress">';
@@ -1142,7 +1151,7 @@ Export
1142
1151
 
1143
1152
  if (convoy.tasks && convoy.tasks.length > 0) {
1144
1153
  html += '<table class="sessions-table convoy-tasks">';
1145
- html += '<thead><tr><th>Task</th><th>Phase</th><th>Agent</th><th>Adapter</th><th>Status</th><th>Retries</th></tr></thead>';
1154
+ html += '<thead><tr><th>Task</th><th>Phase</th><th>Agent</th><th>Adapter</th><th>Status</th><th>Retries</th><th>Tokens</th></tr></thead>';
1146
1155
  html += '<tbody>';
1147
1156
  convoy.tasks.forEach(function(t) {
1148
1157
  const tStatus = t.status === 'done' ? 'success'
@@ -1155,6 +1164,7 @@ Export
1155
1164
  html += '<td>' + escapeHtml(t.adapter || '\u2014') + '</td>';
1156
1165
  html += '<td><span class="outcome-badge outcome-badge--' + tStatus + '">' + escapeHtml(t.status) + '</span></td>';
1157
1166
  html += '<td class="td-num">' + (t.retries || 0) + '</td>';
1167
+ html += '<td class="td-num">' + (t.total_tokens != null ? formatTokens(t.total_tokens) : '\u2014') + '</td>';
1158
1168
  html += '</tr>';
1159
1169
  });
1160
1170
  html += '</tbody></table>';
@@ -1,25 +1,25 @@
1
1
  {
2
- "hash": "efd7e9fc",
2
+ "hash": "31a1a7a6",
3
3
  "configHash": "30f8ea04",
4
- "lockfileHash": "851b1664",
5
- "browserHash": "1b9872e1",
4
+ "lockfileHash": "97475009",
5
+ "browserHash": "cb97a3a0",
6
6
  "optimized": {
7
7
  "astro > cssesc": {
8
8
  "src": "../../../../../node_modules/cssesc/cssesc.js",
9
9
  "file": "astro___cssesc.js",
10
- "fileHash": "bd1b469f",
10
+ "fileHash": "d4c8827f",
11
11
  "needsInterop": true
12
12
  },
13
13
  "astro > aria-query": {
14
14
  "src": "../../../../../node_modules/aria-query/lib/index.js",
15
15
  "file": "astro___aria-query.js",
16
- "fileHash": "796388ea",
16
+ "fileHash": "41875443",
17
17
  "needsInterop": true
18
18
  },
19
19
  "astro > axobject-query": {
20
20
  "src": "../../../../../node_modules/axobject-query/lib/index.js",
21
21
  "file": "astro___axobject-query.js",
22
- "fileHash": "deadbce3",
22
+ "fileHash": "d6770fdb",
23
23
  "needsInterop": true
24
24
  }
25
25
  },
@@ -309,6 +309,12 @@ const base = import.meta.env.BASE_URL;
309
309
  return div.innerHTML;
310
310
  }
311
311
 
312
+ function formatTokens(n) {
313
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
314
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
315
+ return String(n);
316
+ }
317
+
312
318
  // ── SVG Icon Library (empty states) ────────────────────
313
319
 
314
320
  const EMPTY_ICONS = {
@@ -1376,6 +1382,9 @@ const base = import.meta.env.BASE_URL;
1376
1382
  html += '<div class="convoy-stat"><span class="convoy-stat__label">Tasks</span><span class="convoy-stat__value">' + done + '/' + total + '</span></div>';
1377
1383
  html += '<div class="convoy-stat"><span class="convoy-stat__label">Events</span><span class="convoy-stat__value">' + (convoy.events_count || 0) + '</span></div>';
1378
1384
  html += '<div class="convoy-stat"><span class="convoy-stat__label">Started</span><span class="convoy-stat__value">' + (convoy.started_at ? formatTime(convoy.started_at) : '\u2014') + '</span></div>';
1385
+ if (convoy.total_tokens != null) {
1386
+ html += '<div class="convoy-stat"><span class="convoy-stat__label">Tokens</span><span class="convoy-stat__value">' + formatTokens(convoy.total_tokens) + '</span></div>';
1387
+ }
1379
1388
  html += '</div>';
1380
1389
 
1381
1390
  html += '<div class="convoy-progress">';
@@ -1385,7 +1394,7 @@ const base = import.meta.env.BASE_URL;
1385
1394
 
1386
1395
  if (convoy.tasks && convoy.tasks.length > 0) {
1387
1396
  html += '<table class="sessions-table convoy-tasks">';
1388
- html += '<thead><tr><th>Task</th><th>Phase</th><th>Agent</th><th>Adapter</th><th>Status</th><th>Retries</th></tr></thead>';
1397
+ html += '<thead><tr><th>Task</th><th>Phase</th><th>Agent</th><th>Adapter</th><th>Status</th><th>Retries</th><th>Tokens</th></tr></thead>';
1389
1398
  html += '<tbody>';
1390
1399
  convoy.tasks.forEach(function(t) {
1391
1400
  const tStatus = t.status === 'done' ? 'success'
@@ -1398,6 +1407,7 @@ const base = import.meta.env.BASE_URL;
1398
1407
  html += '<td>' + escapeHtml(t.adapter || '\u2014') + '</td>';
1399
1408
  html += '<td><span class="outcome-badge outcome-badge--' + tStatus + '">' + escapeHtml(t.status) + '</span></td>';
1400
1409
  html += '<td class="td-num">' + (t.retries || 0) + '</td>';
1410
+ html += '<td class="td-num">' + (t.total_tokens != null ? formatTokens(t.total_tokens) : '\u2014') + '</td>';
1401
1411
  html += '</tr>';
1402
1412
  });
1403
1413
  html += '</tbody></table>';