opencode-onboard 0.3.3 → 0.4.1
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 +266 -214
- package/content/.agents/agents/basic-engineer.md +30 -0
- package/content/.agents/agents/devops-manager.md +38 -29
- package/content/.agents/session-log.json +41 -0
- package/content/.agents/skills/ob-default/SKILL.md +21 -0
- package/content/.agents/skills/ob-generic-guardrails/SKILL.md +32 -0
- package/content/.agents/skills/ob-global/SKILL.md +49 -0
- package/content/.agents/skills/ob-pullrequest-az/SKILL.md +11 -21
- package/content/.agents/skills/ob-pullrequest-gh/SKILL.md +14 -24
- package/content/.agents/skills/ob-userstory-az/SKILL.md +8 -14
- package/content/.agents/skills/ob-userstory-gh/SKILL.md +6 -14
- package/content/.opencode/commands/opsx-apply.md +50 -33
- package/content/.opencode/plugins/session-log.js +1 -1
- package/content/.opencode/skills/openspec-apply-change/SKILL.md +50 -33
- package/content/AGENTS.md +94 -144
- package/content/skills-lock.json +4 -0
- package/package.json +6 -1
- package/src/index.js +13 -47
- package/src/presets/browser.json +18 -0
- package/src/presets/clean.json +21 -0
- package/src/presets/models.json +33 -0
- package/src/presets/optimization.json +22 -0
- package/src/presets/platforms.json +29 -2
- package/src/presets/quota.json +14 -0
- package/src/presets/source.json +17 -0
- package/src/steps/browser/browser.test.js +81 -0
- package/src/steps/{install-browser.js → browser/index.js} +12 -15
- package/src/steps/{__tests__/clean-ai-files.test.js → clean/clean.test.js} +28 -13
- package/src/steps/{clean-ai-files.js → clean/index.js} +32 -30
- package/src/steps/copy/agents.js +106 -0
- package/src/steps/{__tests__/copy-content.test.js → copy/copy.test.js} +10 -1
- package/src/steps/copy/index.js +33 -0
- package/src/steps/copy/skills.js +55 -0
- package/src/steps/{write-onboard-config.js → metadata/index.js} +3 -3
- package/src/steps/metadata/metadata.test.js +96 -0
- package/src/steps/models/format.js +60 -0
- package/src/steps/models/format.test.js +74 -0
- package/src/steps/models/index.js +52 -0
- package/src/steps/models/write.js +54 -0
- package/src/steps/models/write.test.js +119 -0
- package/src/steps/{init-openspec.js → openspec/ensemble.js} +20 -57
- package/src/steps/openspec/ensemble.test.js +79 -0
- package/src/steps/openspec/index.js +32 -0
- package/src/steps/optimization/caveman-guidance.js +11 -0
- package/src/steps/{install-caveman.js → optimization/caveman.js} +5 -19
- package/src/steps/optimization/global.js +64 -0
- package/src/steps/optimization/index.js +101 -0
- package/src/steps/{__tests__/token-optimization.test.js → optimization/optimization.test.js} +19 -24
- package/src/steps/{install-quota.js → optimization/quota.js} +12 -10
- package/src/steps/platform/index.js +81 -0
- package/src/steps/platform/platform.test.js +129 -0
- package/src/steps/{choose-source-scope.js → source/index.js} +11 -17
- package/src/steps/source/source.test.js +89 -0
- package/src/utils/__tests__/copy.test.js +12 -5
- package/src/utils/copy.js +4 -24
- package/src/utils/exec-spinner.js +47 -0
- package/src/utils/exec.js +120 -162
- package/src/utils/models-cache.js +25 -68
- package/src/utils/models-pricing.js +42 -0
- package/src/utils/models-pricing.test.js +94 -0
- package/content/.agents/agents/back-engineer.md +0 -87
- package/content/.agents/agents/front-engineer.md +0 -86
- package/content/.agents/agents/infra-engineer.md +0 -85
- package/content/.agents/agents/quality-engineer.md +0 -86
- package/content/.agents/agents/security-auditor.md +0 -86
- package/src/steps/__tests__/check-env.test.js +0 -70
- package/src/steps/__tests__/check-platform.test.js +0 -104
- package/src/steps/__tests__/check-rtk.test.js +0 -38
- package/src/steps/__tests__/choose-platform.test.js +0 -38
- package/src/steps/check-env.js +0 -26
- package/src/steps/check-platform.js +0 -80
- package/src/steps/check-rtk.js +0 -38
- package/src/steps/choose-models.js +0 -165
- package/src/steps/choose-platform.js +0 -22
- package/src/steps/choose-skills-provider.js +0 -79
- package/src/steps/copy-content.js +0 -89
- package/src/steps/enable-caveman-guidance.js +0 -78
- package/src/steps/patch-agents-md.js +0 -153
- package/src/steps/token-optimization.js +0 -59
package/src/utils/exec.js
CHANGED
|
@@ -1,173 +1,131 @@
|
|
|
1
|
-
import chalk from 'chalk'
|
|
2
|
-
import { execa } from 'execa'
|
|
3
|
-
import ora from 'ora'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { execa } from 'execa'
|
|
3
|
+
import ora from 'ora'
|
|
4
|
+
import { appendLine, redraw, rotateStep, startSpinner, stopSpinner } from './exec-spinner.js'
|
|
5
|
+
|
|
6
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Run a shell command with a spinner.
|
|
10
|
+
* Returns { success, stdout, stderr }
|
|
11
|
+
*/
|
|
12
|
+
export async function run(command, args = [], { label, cwd = process.cwd() } = {}) {
|
|
13
|
+
const spinner = ora(label ?? `${command} ${args.join(' ')}`).start();
|
|
14
|
+
try {
|
|
15
|
+
const result = await execa(command, args, { cwd, reject: false });
|
|
16
|
+
if (result.exitCode === 0) {
|
|
17
|
+
spinner.succeed();
|
|
18
|
+
} else {
|
|
19
|
+
spinner.fail();
|
|
20
|
+
}
|
|
21
|
+
return { success: result.exitCode === 0, stdout: result.stdout, stderr: result.stderr };
|
|
22
|
+
} catch (err) {
|
|
23
|
+
spinner.fail();
|
|
24
|
+
return { success: false, stdout: '', stderr: err.message };
|
|
25
|
+
}
|
|
20
26
|
}
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Check if a command is available on PATH.
|
|
30
|
+
* Returns true/false.
|
|
31
|
+
*/
|
|
32
|
+
export async function commandExists(command) {
|
|
33
|
+
try {
|
|
34
|
+
const result = await execa(command, ['--version'], { reject: false });
|
|
35
|
+
return result.exitCode === 0;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
25
39
|
}
|
|
26
|
-
|
|
27
|
-
function redraw() {
|
|
28
|
-
if (process.stdout.isTTY) console.clear()
|
|
29
|
-
|
|
30
|
-
// Show up to 2 previous steps dimmed
|
|
31
|
-
for (const stepLines of previousSteps) {
|
|
32
|
-
for (const line of stepLines) {
|
|
33
|
-
process.stdout.write(chalk.dim(line) + '\n')
|
|
34
|
-
}
|
|
35
|
-
process.stdout.write('\n')
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Current step output
|
|
39
|
-
for (const line of currentStepLines) {
|
|
40
|
-
process.stdout.write(line + '\n')
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ── Public API ───────────────────────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Run a shell command with a spinner.
|
|
48
|
-
* Returns { success, stdout, stderr }
|
|
49
|
-
*/
|
|
50
|
-
export async function run(command, args = [], { label, cwd = process.cwd() } = {}) {
|
|
51
|
-
const spinner = ora(label ?? `${command} ${args.join(' ')}`).start()
|
|
52
|
-
try {
|
|
53
|
-
const result = await execa(command, args, { cwd, reject: false })
|
|
54
|
-
if (result.exitCode === 0) {
|
|
55
|
-
spinner.succeed()
|
|
56
|
-
} else {
|
|
57
|
-
spinner.fail()
|
|
58
|
-
}
|
|
59
|
-
return { success: result.exitCode === 0, stdout: result.stdout, stderr: result.stderr }
|
|
60
|
-
} catch (err) {
|
|
61
|
-
spinner.fail()
|
|
62
|
-
return { success: false, stdout: '', stderr: err.message }
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Check if a command is available on PATH.
|
|
68
|
-
* Returns true/false.
|
|
69
|
-
*/
|
|
70
|
-
export async function commandExists(command) {
|
|
71
|
-
try {
|
|
72
|
-
const result = await execa(command, ['--version'], { reject: false })
|
|
73
|
-
return result.exitCode === 0
|
|
74
|
-
} catch {
|
|
75
|
-
return false
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Print a section header, clears screen, shows previous step dimmed, starts new step.
|
|
81
|
-
*/
|
|
82
|
-
export function header(text) {
|
|
83
|
-
// Rotate buffers, keep last 2 completed steps
|
|
84
|
-
previousSteps.push(currentStepLines)
|
|
85
|
-
if (previousSteps.length > 2) previousSteps.shift()
|
|
86
|
-
currentStepLines = []
|
|
87
|
-
|
|
88
|
-
const line1 = ''
|
|
89
|
-
const line2 = chalk.bold.hex('#fe3d57')(`━━ ${text}`)
|
|
90
|
-
const line3 = ''
|
|
91
|
-
|
|
92
|
-
appendLine(line1)
|
|
93
|
-
appendLine(line2)
|
|
94
|
-
appendLine(line3)
|
|
95
|
-
|
|
96
|
-
redraw()
|
|
97
40
|
|
|
98
|
-
|
|
99
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Print a section header, clears screen, shows previous step dimmed, starts new step.
|
|
43
|
+
*/
|
|
44
|
+
export function header(text) {
|
|
45
|
+
rotateStep();
|
|
46
|
+
|
|
47
|
+
const line1 = '';
|
|
48
|
+
const line2 = chalk.bold.hex('#fe3d57')(`━━ ${text}`);
|
|
49
|
+
const line3 = '';
|
|
50
|
+
|
|
51
|
+
appendLine(line1);
|
|
52
|
+
appendLine(line2);
|
|
53
|
+
appendLine(line3);
|
|
54
|
+
|
|
55
|
+
redraw();
|
|
56
|
+
|
|
57
|
+
startSpinner('working...');
|
|
100
58
|
}
|
|
101
59
|
|
|
102
60
|
/**
|
|
103
61
|
* Restart the step spinner after prompts or logs.
|
|
104
62
|
*/
|
|
105
63
|
export function loading(text = 'working...') {
|
|
106
|
-
startSpinner(text)
|
|
64
|
+
startSpinner(text);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Print a success line.
|
|
69
|
+
*/
|
|
70
|
+
export function success(text) {
|
|
71
|
+
stopSpinner();
|
|
72
|
+
const line = chalk.green('✓ ') + text;
|
|
73
|
+
appendLine(line);
|
|
74
|
+
console.log(line);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Print a warning line.
|
|
79
|
+
*/
|
|
80
|
+
export function warn(text) {
|
|
81
|
+
stopSpinner();
|
|
82
|
+
const line = chalk.yellow('⚠ ') + text;
|
|
83
|
+
appendLine(line);
|
|
84
|
+
console.log(line);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Print an error line.
|
|
89
|
+
*/
|
|
90
|
+
export function error(text) {
|
|
91
|
+
stopSpinner();
|
|
92
|
+
const line = chalk.red('✗ ') + text;
|
|
93
|
+
appendLine(line);
|
|
94
|
+
console.log(line);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Print an info line.
|
|
99
|
+
*/
|
|
100
|
+
export function info(text) {
|
|
101
|
+
stopSpinner();
|
|
102
|
+
const line = chalk.dim(' ' + text);
|
|
103
|
+
appendLine(line);
|
|
104
|
+
console.log(line);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Print an action prompt line (white bold, requires user interaction).
|
|
109
|
+
*/
|
|
110
|
+
export function prompt(text) {
|
|
111
|
+
stopSpinner();
|
|
112
|
+
const line = chalk.bold(' ' + text);
|
|
113
|
+
appendLine(line);
|
|
114
|
+
console.log(line);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Print a code block.
|
|
119
|
+
*/
|
|
120
|
+
export function code(lines) {
|
|
121
|
+
stopSpinner();
|
|
122
|
+
appendLine('');
|
|
123
|
+
console.log();
|
|
124
|
+
for (const line of lines) {
|
|
125
|
+
const formatted = chalk.bgGray.white(' ' + line + ' ');
|
|
126
|
+
appendLine(formatted);
|
|
127
|
+
console.log(formatted);
|
|
128
|
+
}
|
|
129
|
+
appendLine('');
|
|
130
|
+
console.log();
|
|
107
131
|
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Print a success line.
|
|
111
|
-
*/
|
|
112
|
-
export function success(text) {
|
|
113
|
-
stopSpinner()
|
|
114
|
-
const line = chalk.green('✓ ') + text
|
|
115
|
-
appendLine(line)
|
|
116
|
-
console.log(line)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Print a warning line.
|
|
121
|
-
*/
|
|
122
|
-
export function warn(text) {
|
|
123
|
-
stopSpinner()
|
|
124
|
-
const line = chalk.yellow('⚠ ') + text
|
|
125
|
-
appendLine(line)
|
|
126
|
-
console.log(line)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Print an error line.
|
|
131
|
-
*/
|
|
132
|
-
export function error(text) {
|
|
133
|
-
stopSpinner()
|
|
134
|
-
const line = chalk.red('✗ ') + text
|
|
135
|
-
appendLine(line)
|
|
136
|
-
console.log(line)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Print an info line.
|
|
141
|
-
*/
|
|
142
|
-
export function info(text) {
|
|
143
|
-
stopSpinner()
|
|
144
|
-
const line = chalk.dim(' ' + text)
|
|
145
|
-
appendLine(line)
|
|
146
|
-
console.log(line)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Print an action prompt line (white bold, requires user interaction).
|
|
151
|
-
*/
|
|
152
|
-
export function prompt(text) {
|
|
153
|
-
stopSpinner()
|
|
154
|
-
const line = chalk.bold(' ' + text)
|
|
155
|
-
appendLine(line)
|
|
156
|
-
console.log(line)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Print a code block.
|
|
161
|
-
*/
|
|
162
|
-
export function code(lines) {
|
|
163
|
-
stopSpinner()
|
|
164
|
-
appendLine('')
|
|
165
|
-
console.log()
|
|
166
|
-
for (const line of lines) {
|
|
167
|
-
const formatted = chalk.bgGray.white(' ' + line + ' ')
|
|
168
|
-
appendLine(formatted)
|
|
169
|
-
console.log(formatted)
|
|
170
|
-
}
|
|
171
|
-
appendLine('')
|
|
172
|
-
console.log()
|
|
173
|
-
}
|
|
@@ -1,73 +1,30 @@
|
|
|
1
1
|
import fse from 'fs-extra'
|
|
2
2
|
import os from 'os'
|
|
3
3
|
import path from 'path'
|
|
4
|
+
import { parseModels } from './models-pricing.js'
|
|
4
5
|
|
|
5
|
-
const CACHE_DIR = path.join(os.homedir(), '.config', 'opencode-onboard')
|
|
6
|
-
const CACHE_FILE = path.join(CACHE_DIR, 'models-cache.json')
|
|
7
|
-
const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000 // 7 days
|
|
8
|
-
const MODELS_URL = 'https://models.dev/api.json'
|
|
9
|
-
|
|
10
|
-
// Providers considered "canonical" for reference pricing, in priority order.
|
|
11
|
-
// When a model's own provider has no cost (e.g. github-copilot shows $0),
|
|
12
|
-
// we look up the same model name in these providers and attach canonicalCost.
|
|
13
|
-
const CANONICAL_PROVIDERS = ['anthropic', 'openai', 'google', 'mistral', 'meta', 'cohere']
|
|
14
|
-
|
|
15
|
-
function parseModels(data) {
|
|
16
|
-
// Build name → canonical cost lookup from authoritative providers first
|
|
17
|
-
// name is the human-readable model name, e.g. "Claude Opus 4.6"
|
|
18
|
-
const canonicalCostByName = new Map()
|
|
19
|
-
for (const providerId of CANONICAL_PROVIDERS) {
|
|
20
|
-
const provider = data[providerId]
|
|
21
|
-
if (!provider?.models) continue
|
|
22
|
-
for (const model of Object.values(provider.models)) {
|
|
23
|
-
if (!model.tool_call) continue
|
|
24
|
-
const name = model.name
|
|
25
|
-
if (name && model.cost?.input !== undefined && !canonicalCostByName.has(name)) {
|
|
26
|
-
canonicalCostByName.set(name, model.cost.input)
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const models = []
|
|
32
|
-
for (const [providerId, provider] of Object.entries(data)) {
|
|
33
|
-
if (!provider.models) continue
|
|
34
|
-
for (const [modelId, model] of Object.entries(provider.models)) {
|
|
35
|
-
if (!model.tool_call) continue
|
|
36
|
-
const name = model.name || modelId
|
|
37
|
-
const cost = model.cost?.input
|
|
38
|
-
const canonicalCost = canonicalCostByName.get(name)
|
|
39
|
-
models.push({
|
|
40
|
-
id: `${providerId}/${modelId}`,
|
|
41
|
-
name,
|
|
42
|
-
cost,
|
|
43
|
-
// canonicalCost: cost from the authoritative provider for this model name.
|
|
44
|
-
// Defined when cost !== canonicalCost (different provider, reseller, or $0 subscription).
|
|
45
|
-
canonicalCost: canonicalCost !== undefined && canonicalCost !== cost ? canonicalCost : undefined,
|
|
46
|
-
context: model.limit?.context,
|
|
47
|
-
})
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
models.sort((a, b) => (a.cost ?? Infinity) - (b.cost ?? Infinity))
|
|
51
|
-
return models
|
|
52
|
-
}
|
|
6
|
+
const CACHE_DIR = path.join(os.homedir(), '.config', 'opencode-onboard');
|
|
7
|
+
const CACHE_FILE = path.join(CACHE_DIR, 'models-cache.json');
|
|
8
|
+
const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
9
|
+
const MODELS_URL = 'https://models.dev/api.json';
|
|
53
10
|
|
|
54
11
|
async function loadCache() {
|
|
55
12
|
try {
|
|
56
|
-
if (!await fse.pathExists(CACHE_FILE)) return null
|
|
57
|
-
const cache = await fse.readJson(CACHE_FILE)
|
|
58
|
-
if (!cache.timestamp || !cache.models) return null
|
|
59
|
-
const age = Date.now() - cache.timestamp
|
|
60
|
-
if (age > CACHE_TTL_MS) return null // expired
|
|
61
|
-
return cache.models
|
|
13
|
+
if (!await fse.pathExists(CACHE_FILE)) return null;
|
|
14
|
+
const cache = await fse.readJson(CACHE_FILE);
|
|
15
|
+
if (!cache.timestamp || !cache.models) return null;
|
|
16
|
+
const age = Date.now() - cache.timestamp;
|
|
17
|
+
if (age > CACHE_TTL_MS) return null; // expired
|
|
18
|
+
return cache.models;
|
|
62
19
|
} catch {
|
|
63
|
-
return null
|
|
20
|
+
return null;
|
|
64
21
|
}
|
|
65
22
|
}
|
|
66
23
|
|
|
67
24
|
async function saveCache(models) {
|
|
68
25
|
try {
|
|
69
|
-
await fse.ensureDir(CACHE_DIR)
|
|
70
|
-
await fse.writeJson(CACHE_FILE, { timestamp: Date.now(), models })
|
|
26
|
+
await fse.ensureDir(CACHE_DIR);
|
|
27
|
+
await fse.writeJson(CACHE_FILE, { timestamp: Date.now(), models });
|
|
71
28
|
} catch {
|
|
72
29
|
// cache write failure is non-fatal
|
|
73
30
|
}
|
|
@@ -75,27 +32,27 @@ async function saveCache(models) {
|
|
|
75
32
|
|
|
76
33
|
export async function fetchModels() {
|
|
77
34
|
// 1. Try cache first (fresh)
|
|
78
|
-
const cached = await loadCache()
|
|
79
|
-
if (cached) return { models: cached, source: 'cache' }
|
|
35
|
+
const cached = await loadCache();
|
|
36
|
+
if (cached) return { models: cached, source: 'cache' };
|
|
80
37
|
|
|
81
38
|
// 2. Try network
|
|
82
39
|
try {
|
|
83
|
-
const res = await fetch(MODELS_URL, { signal: AbortSignal.timeout(8000) })
|
|
84
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
85
|
-
const data = await res.json()
|
|
86
|
-
const models = parseModels(data)
|
|
87
|
-
await saveCache(models)
|
|
88
|
-
return { models, source: 'network' }
|
|
40
|
+
const res = await fetch(MODELS_URL, { signal: AbortSignal.timeout(8000) });
|
|
41
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
42
|
+
const data = await res.json();
|
|
43
|
+
const models = parseModels(data);
|
|
44
|
+
await saveCache(models);
|
|
45
|
+
return { models, source: 'network' };
|
|
89
46
|
} catch {
|
|
90
47
|
// 3. Network failed, fall back to stale cache if available
|
|
91
48
|
try {
|
|
92
49
|
if (await fse.pathExists(CACHE_FILE)) {
|
|
93
|
-
const cache = await fse.readJson(CACHE_FILE)
|
|
94
|
-
if (cache.models?.length) return { models: cache.models, source: 'stale-cache' }
|
|
50
|
+
const cache = await fse.readJson(CACHE_FILE);
|
|
51
|
+
if (cache.models?.length) return { models: cache.models, source: 'stale-cache' };
|
|
95
52
|
}
|
|
96
53
|
} catch {
|
|
97
54
|
// ignore
|
|
98
55
|
}
|
|
99
|
-
return { models: null, source: 'unavailable' }
|
|
56
|
+
return { models: null, source: 'unavailable' };
|
|
100
57
|
}
|
|
101
58
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Providers considered "canonical" for reference pricing, in priority order.
|
|
2
|
+
// When a model's own provider has no cost (e.g. github-copilot shows $0),
|
|
3
|
+
// we look up the same model name in these providers and attach canonicalCost.
|
|
4
|
+
export const CANONICAL_PROVIDERS = ['anthropic', 'openai', 'google', 'mistral', 'meta', 'cohere'];
|
|
5
|
+
|
|
6
|
+
export function parseModels(data) {
|
|
7
|
+
// Build name → canonical cost lookup from authoritative providers first
|
|
8
|
+
const canonicalCostByName = new Map();
|
|
9
|
+
for (const providerId of CANONICAL_PROVIDERS) {
|
|
10
|
+
const provider = data[providerId];
|
|
11
|
+
if (!provider?.models) continue;
|
|
12
|
+
for (const model of Object.values(provider.models)) {
|
|
13
|
+
if (!model.tool_call) continue;
|
|
14
|
+
const name = model.name;
|
|
15
|
+
if (name && model.cost?.input !== undefined && !canonicalCostByName.has(name)) {
|
|
16
|
+
canonicalCostByName.set(name, model.cost.input);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const models = [];
|
|
22
|
+
for (const [providerId, provider] of Object.entries(data)) {
|
|
23
|
+
if (!provider.models) continue;
|
|
24
|
+
for (const [modelId, model] of Object.entries(provider.models)) {
|
|
25
|
+
if (!model.tool_call) continue;
|
|
26
|
+
const name = model.name || modelId;
|
|
27
|
+
const cost = model.cost?.input;
|
|
28
|
+
const canonicalCost = canonicalCostByName.get(name);
|
|
29
|
+
models.push({
|
|
30
|
+
id: `${providerId}/${modelId}`,
|
|
31
|
+
name,
|
|
32
|
+
cost,
|
|
33
|
+
// canonicalCost: cost from the authoritative provider for this model name.
|
|
34
|
+
// Defined when cost !== canonicalCost (different provider, reseller, or $0 subscription).
|
|
35
|
+
canonicalCost: canonicalCost !== undefined && canonicalCost !== cost ? canonicalCost : undefined,
|
|
36
|
+
context: model.limit?.context,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
models.sort((a, b) => (a.cost ?? Infinity) - (b.cost ?? Infinity));
|
|
41
|
+
return models;
|
|
42
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { CANONICAL_PROVIDERS, parseModels } from './models-pricing.js'
|
|
3
|
+
|
|
4
|
+
describe('CANONICAL_PROVIDERS', () => {
|
|
5
|
+
it('contains expected major providers', () => {
|
|
6
|
+
expect(CANONICAL_PROVIDERS).toContain('anthropic')
|
|
7
|
+
expect(CANONICAL_PROVIDERS).toContain('openai')
|
|
8
|
+
expect(CANONICAL_PROVIDERS).toContain('google')
|
|
9
|
+
})
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe('parseModels()', () => {
|
|
13
|
+
it('filters to tool-calling models only', () => {
|
|
14
|
+
const data = {
|
|
15
|
+
anthropic: {
|
|
16
|
+
models: {
|
|
17
|
+
'claude-3-sonnet': { name: 'Claude 3 Sonnet', tool_call: true, cost: { input: 3 } },
|
|
18
|
+
'claude-3-haiku': { name: 'Claude 3 Haiku', tool_call: false, cost: { input: 0.25 } },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const result = parseModels(data)
|
|
24
|
+
|
|
25
|
+
expect(result).toHaveLength(1)
|
|
26
|
+
expect(result[0].id).toBe('anthropic/claude-3-sonnet')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('builds canonical cost lookup from authoritative providers', () => {
|
|
30
|
+
const data = {
|
|
31
|
+
anthropic: {
|
|
32
|
+
models: {
|
|
33
|
+
'claude-3-5-sonnet': { name: 'Claude 3.5 Sonnet', tool_call: true, cost: { input: 3 } },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
'some-reseller': {
|
|
37
|
+
models: {
|
|
38
|
+
'claude-3-5-sonnet': { name: 'Claude 3.5 Sonnet', tool_call: true, cost: { input: 0 } },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const result = parseModels(data)
|
|
44
|
+
|
|
45
|
+
expect(result).toHaveLength(2)
|
|
46
|
+
const reseller = result.find(m => m.id.startsWith('some-reseller'))
|
|
47
|
+
expect(reseller?.canonicalCost).toBe(3)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('sorts by cost ascending', () => {
|
|
51
|
+
const data = {
|
|
52
|
+
openai: {
|
|
53
|
+
models: {
|
|
54
|
+
'gpt-4': { name: 'GPT-4', tool_call: true, cost: { input: 30 } },
|
|
55
|
+
'gpt-3.5-turbo': { name: 'GPT-3.5 Turbo', tool_call: true, cost: { input: 0.5 } },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const result = parseModels(data)
|
|
61
|
+
|
|
62
|
+
expect(result[0].cost).toBe(0.5)
|
|
63
|
+
expect(result[1].cost).toBe(30)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('handles missing cost', () => {
|
|
67
|
+
const data = {
|
|
68
|
+
test: {
|
|
69
|
+
models: {
|
|
70
|
+
'unknown-model': { name: 'Unknown', tool_call: true },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const result = parseModels(data)
|
|
76
|
+
|
|
77
|
+
expect(result[0].cost).toBeUndefined()
|
|
78
|
+
expect(result[0].description).toContain('cost: ?')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('extracts context limit', () => {
|
|
82
|
+
const data = {
|
|
83
|
+
anthropic: {
|
|
84
|
+
models: {
|
|
85
|
+
'claude-3-opus': { name: 'Claude 3 Opus', tool_call: true, cost: { input: 15 }, limit: { context: 200000 } },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result = parseModels(data)
|
|
91
|
+
|
|
92
|
+
expect(result[0].context).toBe(200000)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Backend engineer. Implements APIs, services, data models, business logic, AI integrations. Anything that is not UI. Receives tasks from lead, implements, reports back.
|
|
3
|
-
mode: subagent
|
|
4
|
-
color: #68A063
|
|
5
|
-
temperature: 0.2
|
|
6
|
-
permission:
|
|
7
|
-
edit: allow
|
|
8
|
-
bash: allow
|
|
9
|
-
read: allow
|
|
10
|
-
glob: allow
|
|
11
|
-
grep: allow
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
# Back Engineer
|
|
15
|
-
|
|
16
|
-
Backend specialist, APIs, monoliths, data, AI, anything not UI. Spawned by the lead agent via opencode-ensemble.
|
|
17
|
-
|
|
18
|
-
## Domain
|
|
19
|
-
|
|
20
|
-
REST and GraphQL APIs, monolithic services, microservices, databases and data models, business logic, background jobs, queues, caching, AI/LLM integrations, third-party service integrations, authentication and authorization logic. Anything that runs server-side or outside the UI.
|
|
21
|
-
|
|
22
|
-
## RTK, MANDATORY
|
|
23
|
-
|
|
24
|
-
Use `rtk` for ALL CLI commands. Never run commands directly.
|
|
25
|
-
|
|
26
|
-
- `rtk dotnet test` NOT `dotnet test`
|
|
27
|
-
- `rtk bun test` NOT `bun test`
|
|
28
|
-
- `rtk npm run build` NOT `npm run build`
|
|
29
|
-
|
|
30
|
-
If `rtk` is not available, report it as a blocker. Do not run commands without it.
|
|
31
|
-
|
|
32
|
-
## Skills, Auto-Detection
|
|
33
|
-
|
|
34
|
-
Skills are located in `.agents/skills/`. Detect and use relevant skills automatically, the user will never tell you which skill to use.
|
|
35
|
-
|
|
36
|
-
1. If the spawn prompt lists specific skills to load, read those `SKILL.md` files FIRST before any implementation
|
|
37
|
-
2. Additionally, read the task and identify domain and platform
|
|
38
|
-
3. Scan `.agents/skills/` for available skills
|
|
39
|
-
4. Read each `SKILL.md` description to assess relevance
|
|
40
|
-
5. Load and follow any skill that applies, even partial match warrants loading
|
|
41
|
-
|
|
42
|
-
Rules:
|
|
43
|
-
- Never implement directly if a skill applies
|
|
44
|
-
- Follow skill instructions exactly, do not partially apply them
|
|
45
|
-
- If two skills apply, follow both, resolve conflicts by asking the lead
|
|
46
|
-
- Skills listed in the spawn prompt are MANDATORY, not optional
|
|
47
|
-
|
|
48
|
-
## Responsibilities
|
|
49
|
-
|
|
50
|
-
- API endpoints and controllers
|
|
51
|
-
- Data models and migrations
|
|
52
|
-
- Business logic and domain services
|
|
53
|
-
- Authentication and authorization
|
|
54
|
-
- Background jobs and workers
|
|
55
|
-
- AI/LLM integrations and prompt engineering
|
|
56
|
-
- Third-party service integrations
|
|
57
|
-
- Performance and query optimization
|
|
58
|
-
|
|
59
|
-
## Constraints
|
|
60
|
-
|
|
61
|
-
- Implement only what is in the assigned tasks, no scope creep
|
|
62
|
-
- Do not modify UI, infra, or pipeline files
|
|
63
|
-
- Do not push to `main`, feature branches only
|
|
64
|
-
- Do not merge PRs, human-only
|
|
65
|
-
- Do not force push
|
|
66
|
-
- Report blockers immediately rather than working around them
|
|
67
|
-
|
|
68
|
-
## Workflow
|
|
69
|
-
|
|
70
|
-
When spawned by the lead:
|
|
71
|
-
0. Read ALL skills listed in the spawn prompt FIRST. Do not proceed until every listed SKILL.md has been read. Reply to lead with `team_message` confirming which skills were loaded.
|
|
72
|
-
1. For each assigned task: call `team_claim task_id:<id>` before starting
|
|
73
|
-
2. Implement the task following loaded skill rules
|
|
74
|
-
3. Call `team_tasks_complete task_id:<id>` after finishing
|
|
75
|
-
4. When all tasks are done or blocked, send results to lead via `team_message`
|
|
76
|
-
|
|
77
|
-
## Output Format
|
|
78
|
-
|
|
79
|
-
Send via `team_message` to lead when done:
|
|
80
|
-
|
|
81
|
-
```
|
|
82
|
-
## Back Engineer, Done
|
|
83
|
-
|
|
84
|
-
**Tasks completed:** <count>
|
|
85
|
-
**Files changed:** <list>
|
|
86
|
-
**Blockers:** none | <description>
|
|
87
|
-
```
|