opencode-onboard 0.4.3 → 0.4.4
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 +41 -40
- package/content/.agents/agents/devops-manager.md +123 -123
- package/content/.agents/skills/ob-default/SKILL.md +25 -21
- package/content/.agents/skills/ob-generic-guardrails/SKILL.md +36 -32
- package/content/.agents/skills/ob-global/SKILL.md +92 -84
- package/content/.agents/skills/ob-pullrequest-az/SKILL.md +168 -160
- package/content/.agents/skills/ob-pullrequest-gh/SKILL.md +140 -136
- package/content/.opencode/commands/create-engineer.md +109 -0
- package/content/.opencode/plugins/session-log.js +523 -519
- package/content/AGENTS.md +23 -21
- package/package.json +1 -1
- package/src/commands/wizard.js +124 -113
- package/src/presets/browser.json +22 -18
- package/src/presets/optimization.json +27 -22
- package/src/steps/browser/browser.test.js +115 -81
- package/src/steps/browser/index.js +62 -54
- package/src/steps/clean/index.js +108 -107
- package/src/steps/metadata/index.js +63 -62
- package/src/steps/models/format.js +61 -60
- package/src/steps/models/write.test.js +117 -117
- package/src/steps/openspec/ensemble.test.js +79 -79
- package/src/steps/openspec/index.js +121 -32
- package/src/steps/openspec/index.test.js +63 -0
- package/src/steps/optimization/caveman.js +34 -29
- package/src/steps/optimization/codegraph.js +52 -0
- package/src/steps/optimization/global.js +88 -64
- package/src/steps/optimization/global.test.js +99 -0
- package/src/steps/optimization/index.js +109 -101
- package/src/steps/optimization/optimization.test.js +101 -93
- package/src/steps/optimization/quota.js +84 -84
- package/src/steps/source/source.test.js +124 -124
- package/src/utils/__tests__/copy.test.js +117 -117
- package/src/utils/exec-spinner.js +47 -47
- package/src/utils/exec.js +134 -131
- package/src/utils/terminal.js +6 -0
|
@@ -1,54 +1,62 @@
|
|
|
1
|
-
import { execa } from 'execa'
|
|
2
|
-
import fse from 'fs-extra'
|
|
3
|
-
import { header,
|
|
4
|
-
import os from 'os'
|
|
5
|
-
import path from 'path'
|
|
6
|
-
import { fileURLToPath } from 'url'
|
|
7
|
-
|
|
8
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
-
const BROWSER_PRESET_PATH = path.resolve(__dirname, '../../presets/browser.json')
|
|
10
|
-
const browserPreset = await fse.readJson(BROWSER_PRESET_PATH)
|
|
11
|
-
|
|
12
|
-
export async function installBrowser() {
|
|
13
|
-
header('Step 9, Installing opencode-browser')
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import fse from 'fs-extra'
|
|
3
|
+
import { header, success, warn, error } from '../../utils/exec.js'
|
|
4
|
+
import os from 'os'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import { fileURLToPath } from 'url'
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const BROWSER_PRESET_PATH = path.resolve(__dirname, '../../presets/browser.json')
|
|
10
|
+
const browserPreset = await fse.readJson(BROWSER_PRESET_PATH)
|
|
11
|
+
|
|
12
|
+
export async function installBrowser(ctx = {}) {
|
|
13
|
+
header('Step 9, Installing opencode-browser')
|
|
14
|
+
|
|
15
|
+
const installScope = ctx.installScope || 'local'
|
|
16
|
+
const locationAnswer = browserPreset.locationChoices?.[installScope] ?? browserPreset.locationChoices?.local ?? '2'
|
|
17
|
+
|
|
18
|
+
const pendingTriggers = browserPreset.autoAnswers.map(a => ({
|
|
19
|
+
...a,
|
|
20
|
+
response: a.response === '__LOCATION__' ? locationAnswer : a.response,
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const child = execa(browserPreset.installer.command, browserPreset.installer.args, {
|
|
25
|
+
cwd: os.homedir(),
|
|
26
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
27
|
+
reject: false,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
let show = false
|
|
31
|
+
const triggers = [...pendingTriggers]
|
|
32
|
+
|
|
33
|
+
child.stdout.on('data', (chunk) => {
|
|
34
|
+
const text = chunk.toString()
|
|
35
|
+
|
|
36
|
+
if (text.includes(browserPreset.output.showAfter)) show = true
|
|
37
|
+
if (text.includes(browserPreset.output.hideAfter)) show = false
|
|
38
|
+
|
|
39
|
+
if (show) process.stdout.write(chunk)
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < triggers.length; i++) {
|
|
42
|
+
if (text.includes(triggers[i].trigger)) {
|
|
43
|
+
child.stdin.write(`${triggers[i].response}\n`)
|
|
44
|
+
triggers.splice(i, 1)
|
|
45
|
+
break
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
child.stderr.on('data', (chunk) => process.stderr.write(chunk))
|
|
51
|
+
|
|
52
|
+
const result = await child
|
|
53
|
+
|
|
54
|
+
if (result.exitCode === 0) {
|
|
55
|
+
success('opencode-browser installed')
|
|
56
|
+
} else {
|
|
57
|
+
warn('opencode-browser install exited with non-zero code')
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
error(`Failed to install opencode-browser: ${err.message}`)
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/steps/clean/index.js
CHANGED
|
@@ -1,107 +1,108 @@
|
|
|
1
|
-
import { checkbox } from '@inquirer/prompts'
|
|
2
|
-
import fse from 'fs-extra'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
import { fileURLToPath } from 'url'
|
|
5
|
-
import { findAiFiles } from '../../utils/copy.js'
|
|
6
|
-
import { header, info, success, warn } from '../../utils/exec.js'
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
if (trimmed
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (ctx.
|
|
58
|
-
if (ctx.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
1
|
+
import { checkbox } from '@inquirer/prompts'
|
|
2
|
+
import fse from 'fs-extra'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { findAiFiles } from '../../utils/copy.js'
|
|
6
|
+
import { header, info, success, warn } from '../../utils/exec.js'
|
|
7
|
+
import { MARKERS } from '../../utils/terminal.js'
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
const CLEAN_PRESET_PATH = path.resolve(__dirname, '../../presets/clean.json')
|
|
11
|
+
const cleanPreset = await fse.readJson(CLEAN_PRESET_PATH)
|
|
12
|
+
|
|
13
|
+
async function childrenExcludingPreserved(dir) {
|
|
14
|
+
const results = []
|
|
15
|
+
if (!await fse.pathExists(dir)) return results
|
|
16
|
+
const entries = await fse.readdir(dir)
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
if (cleanPreset.preserveSubfolders.includes(entry)) continue
|
|
19
|
+
results.push(path.join(dir, entry))
|
|
20
|
+
}
|
|
21
|
+
return results
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function isPopulated(filePath) {
|
|
25
|
+
if (!await fse.pathExists(filePath)) return false
|
|
26
|
+
const content = await fse.readFile(filePath, 'utf-8')
|
|
27
|
+
const trimmed = content.trim()
|
|
28
|
+
if (!trimmed) return false
|
|
29
|
+
if (trimmed.startsWith('<!-- onboard-prompt')) return false
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function hasOpenspecHistory(cwd) {
|
|
34
|
+
const changesDir = path.join(cwd, 'openspec', 'changes')
|
|
35
|
+
const archiveDir = path.join(cwd, 'openspec', 'archive')
|
|
36
|
+
if (await fse.pathExists(changesDir)) {
|
|
37
|
+
const entries = await fse.readdir(changesDir)
|
|
38
|
+
if (entries.length > 0) return true
|
|
39
|
+
}
|
|
40
|
+
if (await fse.pathExists(archiveDir)) {
|
|
41
|
+
const entries = await fse.readdir(archiveDir)
|
|
42
|
+
if (entries.length > 0) return true
|
|
43
|
+
}
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function cleanAiFiles() {
|
|
48
|
+
header('Step 2, Existing AI config files')
|
|
49
|
+
|
|
50
|
+
const cwd = process.cwd()
|
|
51
|
+
const ctx = {
|
|
52
|
+
hasDesign: await isPopulated(path.join(cwd, 'DESIGN.md')),
|
|
53
|
+
hasArchitecture: await isPopulated(path.join(cwd, 'ARCHITECTURE.md')),
|
|
54
|
+
hasOpenspec: await hasOpenspecHistory(cwd),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (ctx.hasDesign) info('DESIGN.md exists and is populated, keeping it')
|
|
58
|
+
if (ctx.hasArchitecture) info('ARCHITECTURE.md exists and is populated, keeping it')
|
|
59
|
+
if (ctx.hasOpenspec) info('openspec/ history exists, keeping it')
|
|
60
|
+
|
|
61
|
+
const flatFiles = await findAiFiles(cwd, cleanPreset.detectFiles)
|
|
62
|
+
const dirTargets = cleanPreset.directoryTargets
|
|
63
|
+
const dirEntries = []
|
|
64
|
+
for (const dirName of dirTargets) {
|
|
65
|
+
const dirPath = path.join(cwd, dirName)
|
|
66
|
+
const children = await childrenExcludingPreserved(dirPath)
|
|
67
|
+
dirEntries.push(...children)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const filteredFlat = flatFiles.filter(f => {
|
|
71
|
+
const rel = path.relative(cwd, f)
|
|
72
|
+
if (dirTargets.includes(rel)) return false
|
|
73
|
+
if (cleanPreset.preserve.some(p => rel === p || rel.startsWith(p + path.sep))) return false
|
|
74
|
+
return true
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const allToRemove = [...filteredFlat, ...dirEntries]
|
|
78
|
+
|
|
79
|
+
if (allToRemove.length === 0) {
|
|
80
|
+
success('No existing AI config files to remove')
|
|
81
|
+
return ctx
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const choices = allToRemove.map(f => ({
|
|
85
|
+
name: path.relative(cwd, f).replace(/\\/g, '/'),
|
|
86
|
+
value: f,
|
|
87
|
+
checked: true,
|
|
88
|
+
}))
|
|
89
|
+
|
|
90
|
+
const selected = await checkbox({
|
|
91
|
+
message: cleanPreset.selectionMessage,
|
|
92
|
+
choices,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
if (!selected || selected.length === 0) {
|
|
96
|
+
success('No AI config files selected for removal')
|
|
97
|
+
return ctx
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
warn('Removing selected AI config files:')
|
|
101
|
+
for (const f of selected) {
|
|
102
|
+
info(`${MARKERS.EMPTY}${f.replace(cwd + path.sep, '')}`)
|
|
103
|
+
await fse.remove(f)
|
|
104
|
+
}
|
|
105
|
+
success('Removed existing AI config files')
|
|
106
|
+
|
|
107
|
+
return ctx
|
|
108
|
+
}
|
|
@@ -1,62 +1,63 @@
|
|
|
1
|
-
import { execa } from 'execa'
|
|
2
|
-
import fse from 'fs-extra'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
import { createRequire } from 'node:module'
|
|
5
|
-
import { header, success, warn } from '../../utils/exec.js'
|
|
6
|
-
|
|
7
|
-
const require = createRequire(import.meta.url)
|
|
8
|
-
const { version: onboardVersion } = require('../../../package.json')
|
|
9
|
-
|
|
10
|
-
async function detectOpencodeVersion() {
|
|
11
|
-
try {
|
|
12
|
-
const result = await execa('opencode', ['--version'], { reject: false })
|
|
13
|
-
if (result.exitCode !== 0) return null
|
|
14
|
-
const output = (result.stdout || result.stderr || '').trim()
|
|
15
|
-
return output || null
|
|
16
|
-
} catch {
|
|
17
|
-
return null
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function writeOnboardConfig(data) {
|
|
22
|
-
header('Step 10, Writing onboarding metadata')
|
|
23
|
-
|
|
24
|
-
const opencodeVersion = await detectOpencodeVersion()
|
|
25
|
-
const target = path.join(process.cwd(), '.opencode', 'opencode-onboard.json')
|
|
26
|
-
|
|
27
|
-
const payload = {
|
|
28
|
-
schema: 1,
|
|
29
|
-
generatedAt: new Date().toISOString(),
|
|
30
|
-
onboardVersion,
|
|
31
|
-
opencodeVersion,
|
|
32
|
-
wizard: {
|
|
33
|
-
platform: data.platform,
|
|
34
|
-
sourceMode: data.sourceMode,
|
|
35
|
-
sourceRoots: data.sourceRoots,
|
|
36
|
-
maxConcurrentAgents: data.maxConcurrentAgents ?? 4,
|
|
37
|
-
preserved: {
|
|
38
|
-
design: !!data.hasDesign,
|
|
39
|
-
architecture: !!data.hasArchitecture,
|
|
40
|
-
openspec: !!data.hasOpenspec,
|
|
41
|
-
},
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
await fse.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import fse from 'fs-extra'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { header, success, warn } from '../../utils/exec.js'
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url)
|
|
8
|
+
const { version: onboardVersion } = require('../../../package.json')
|
|
9
|
+
|
|
10
|
+
async function detectOpencodeVersion() {
|
|
11
|
+
try {
|
|
12
|
+
const result = await execa('opencode', ['--version'], { reject: false })
|
|
13
|
+
if (result.exitCode !== 0) return null
|
|
14
|
+
const output = (result.stdout || result.stderr || '').trim()
|
|
15
|
+
return output || null
|
|
16
|
+
} catch {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function writeOnboardConfig(data) {
|
|
22
|
+
header('Step 10, Writing onboarding metadata')
|
|
23
|
+
|
|
24
|
+
const opencodeVersion = await detectOpencodeVersion()
|
|
25
|
+
const target = path.join(process.cwd(), '.opencode', 'opencode-onboard.json')
|
|
26
|
+
|
|
27
|
+
const payload = {
|
|
28
|
+
schema: 1,
|
|
29
|
+
generatedAt: new Date().toISOString(),
|
|
30
|
+
onboardVersion,
|
|
31
|
+
opencodeVersion,
|
|
32
|
+
wizard: {
|
|
33
|
+
platform: data.platform,
|
|
34
|
+
sourceMode: data.sourceMode,
|
|
35
|
+
sourceRoots: data.sourceRoots,
|
|
36
|
+
maxConcurrentAgents: data.maxConcurrentAgents ?? 4,
|
|
37
|
+
preserved: {
|
|
38
|
+
design: !!data.hasDesign,
|
|
39
|
+
architecture: !!data.hasArchitecture,
|
|
40
|
+
openspec: !!data.hasOpenspec,
|
|
41
|
+
},
|
|
42
|
+
openspec: data.openspec,
|
|
43
|
+
additionalSkillsProvider: data.additionalSkillsProvider,
|
|
44
|
+
models: {
|
|
45
|
+
plan: data.planModel,
|
|
46
|
+
build: data.buildModel,
|
|
47
|
+
fast: data.fastModel,
|
|
48
|
+
},
|
|
49
|
+
optionalTools: data.optionalTools ?? null,
|
|
50
|
+
cavemanGuidance: data.cavemanGuidance ?? null,
|
|
51
|
+
},
|
|
52
|
+
note: 'Informational file only. Editing this file does not change runtime behavior.',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
await fse.ensureDir(path.dirname(target))
|
|
57
|
+
await fse.writeJson(target, payload, { spaces: 2 })
|
|
58
|
+
success('Wrote .opencode/opencode-onboard.json')
|
|
59
|
+
if (!opencodeVersion) warn('Could not detect opencode version, saved as null')
|
|
60
|
+
} catch (err) {
|
|
61
|
+
warn(`Could not write onboarding metadata: ${err.message}`)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -1,60 +1,61 @@
|
|
|
1
|
-
import { search } from '@inquirer/prompts'
|
|
2
|
-
import fse from 'fs-extra'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
import { fileURLToPath } from 'url'
|
|
5
|
-
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const MODELS_PRESET_PATH = path.resolve(__dirname, '../../presets/models.json');
|
|
8
|
-
const modelsPreset = await fse.readJson(MODELS_PRESET_PATH);
|
|
9
|
-
|
|
10
|
-
function costTier(input) {
|
|
11
|
-
if (input === undefined || input === null) return '';
|
|
12
|
-
const tier = modelsPreset.costTiers.find(t => t.max === undefined || input <= t.max);
|
|
13
|
-
return tier ? ` ${tier.label}` : '';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function costTierDisplay(cost, canonicalCost) {
|
|
17
|
-
return costTier(canonicalCost !== undefined ? canonicalCost : cost);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function formatPrice(price) {
|
|
21
|
-
if (price === undefined || price === null) return '?';
|
|
22
|
-
if (price === 0) return '$0 (subscription)';
|
|
23
|
-
return `$${price}/M`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function buildDisplayModels(rawModels) {
|
|
27
|
-
return rawModels.map(m => {
|
|
28
|
-
const priceStr = formatPrice(m.cost);
|
|
29
|
-
const canonicalNote = m.canonicalCost !== undefined
|
|
30
|
-
? ` · official price: ${formatPrice(m.canonicalCost)}/M`
|
|
31
|
-
: '';
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
m.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
1
|
+
import { search } from '@inquirer/prompts'
|
|
2
|
+
import fse from 'fs-extra'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const MODELS_PRESET_PATH = path.resolve(__dirname, '../../presets/models.json');
|
|
8
|
+
const modelsPreset = await fse.readJson(MODELS_PRESET_PATH);
|
|
9
|
+
|
|
10
|
+
function costTier(input) {
|
|
11
|
+
if (input === undefined || input === null) return '';
|
|
12
|
+
const tier = modelsPreset.costTiers.find(t => t.max === undefined || input <= t.max);
|
|
13
|
+
return tier ? ` ${tier.label}` : '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function costTierDisplay(cost, canonicalCost) {
|
|
17
|
+
return costTier(canonicalCost !== undefined ? canonicalCost : cost);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatPrice(price) {
|
|
21
|
+
if (price === undefined || price === null) return '?';
|
|
22
|
+
if (price === 0) return '$0 (subscription)';
|
|
23
|
+
return `$${price}/M`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildDisplayModels(rawModels) {
|
|
27
|
+
return rawModels.map(m => {
|
|
28
|
+
const priceStr = formatPrice(m.cost);
|
|
29
|
+
const canonicalNote = m.canonicalCost !== undefined
|
|
30
|
+
? ` · official price: ${formatPrice(m.canonicalCost)}/M`
|
|
31
|
+
: '';
|
|
32
|
+
const context = m.context ? `${m.context / 1000}k` : '?';
|
|
33
|
+
return {
|
|
34
|
+
...m,
|
|
35
|
+
label: `${m.name}${costTierDisplay(m.cost, m.canonicalCost)}, ${m.id}`,
|
|
36
|
+
description: `${priceStr}${canonicalNote} · context: ${context}`,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function pickModel(message, models) {
|
|
42
|
+
return search({
|
|
43
|
+
message,
|
|
44
|
+
source: (input) => {
|
|
45
|
+
const q = (input || '').toLowerCase();
|
|
46
|
+
const filtered = q
|
|
47
|
+
? models.filter(m =>
|
|
48
|
+
m.label.toLowerCase().includes(q) ||
|
|
49
|
+
m.id.toLowerCase().includes(q)
|
|
50
|
+
)
|
|
51
|
+
: models;
|
|
52
|
+
return filtered.slice(0, 50).map(m => ({
|
|
53
|
+
name: m.label,
|
|
54
|
+
value: m.id,
|
|
55
|
+
description: m.description,
|
|
56
|
+
}));
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { modelsPreset };
|