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.
- package/dist/cli/convoy/engine.d.ts +3 -0
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +22 -1
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +148 -0
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/export.d.ts.map +1 -1
- package/dist/cli/convoy/export.js +5 -0
- package/dist/cli/convoy/export.js.map +1 -1
- package/dist/cli/convoy/export.test.js +31 -0
- package/dist/cli/convoy/export.test.js.map +1 -1
- package/dist/cli/convoy/store.d.ts +5 -3
- package/dist/cli/convoy/store.d.ts.map +1 -1
- package/dist/cli/convoy/store.js +37 -11
- package/dist/cli/convoy/store.js.map +1 -1
- package/dist/cli/convoy/store.test.js +204 -4
- package/dist/cli/convoy/store.test.js.map +1 -1
- package/dist/cli/convoy/types.d.ts +6 -0
- package/dist/cli/convoy/types.d.ts.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +25 -30
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/run/adapters/claude-code.d.ts.map +1 -1
- package/dist/cli/run/adapters/claude-code.js +13 -0
- package/dist/cli/run/adapters/claude-code.js.map +1 -1
- package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
- package/dist/cli/run/adapters/copilot.js +8 -0
- package/dist/cli/run/adapters/copilot.js.map +1 -1
- package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
- package/dist/cli/run/adapters/cursor.js +13 -0
- package/dist/cli/run/adapters/cursor.js.map +1 -1
- package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
- package/dist/cli/run/adapters/opencode.js +13 -0
- package/dist/cli/run/adapters/opencode.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +26 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/types.d.ts +8 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/convoy/engine.test.ts +177 -0
- package/src/cli/convoy/engine.ts +22 -1
- package/src/cli/convoy/export.test.ts +37 -0
- package/src/cli/convoy/export.ts +5 -0
- package/src/cli/convoy/store.test.ts +220 -4
- package/src/cli/convoy/store.ts +46 -20
- package/src/cli/convoy/types.ts +6 -0
- package/src/cli/init.ts +25 -30
- package/src/cli/run/adapters/claude-code.ts +13 -1
- package/src/cli/run/adapters/copilot.ts +8 -0
- package/src/cli/run/adapters/cursor.ts +13 -1
- package/src/cli/run/adapters/opencode.ts +13 -1
- package/src/cli/run.ts +23 -0
- package/src/cli/types.ts +9 -0
- package/src/dashboard/dist/index.html +11 -1
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/src/pages/index.astro +11 -1
package/src/cli/convoy/store.ts
CHANGED
|
@@ -9,21 +9,21 @@ import type {
|
|
|
9
9
|
EventRecord,
|
|
10
10
|
} from './types.js'
|
|
11
11
|
|
|
12
|
-
const SCHEMA_VERSION =
|
|
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
|
-
|
|
65
|
-
if (
|
|
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
|
|
76
|
-
spec_yaml
|
|
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
|
|
96
|
-
max_retries
|
|
97
|
-
files
|
|
98
|
-
depends_on
|
|
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 (
|
|
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) {
|
package/src/cli/convoy/types.ts
CHANGED
|
@@ -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
|
-
// ──
|
|
53
|
+
// ── IDE (single select) ────────────────────────────────────────
|
|
54
54
|
console.log(` ${c.bold('── IDEs ──────────────────────────────────────')}`)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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": "
|
|
2
|
+
"hash": "31a1a7a6",
|
|
3
3
|
"configHash": "30f8ea04",
|
|
4
|
-
"lockfileHash": "
|
|
5
|
-
"browserHash": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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>';
|