mohdel 0.124.0 → 1.0.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/README.md +155 -30
- package/config/curated.schema.json +32 -10
- package/js/client/call.js +76 -19
- package/js/client/gate-binary.js +5 -0
- package/js/client/index.js +1 -0
- package/js/core/envelope.js +5 -1
- package/js/factory/bridge.js +2 -2
- package/js/session/adapters/_cancelled.js +0 -6
- package/js/session/adapters/_chat_completions.js +25 -0
- package/js/session/adapters/_output_cap.js +30 -0
- package/js/session/adapters/_registry.js +44 -0
- package/js/session/adapters/anthropic.js +8 -5
- package/js/session/adapters/gemini.js +4 -0
- package/js/session/adapters/openai.js +2 -1
- package/js/session/run.js +14 -4
- package/js/session/run_image.js +12 -3
- package/package.json +49 -19
- package/src/cli/aliases.js +20 -0
- package/src/cli/ask.js +58 -12
- package/src/cli/backup.js +2 -1
- package/src/cli/check.js +15 -86
- package/src/cli/complete.js +130 -0
- package/src/cli/default.js +34 -13
- package/src/cli/doctor.js +33 -14
- package/src/cli/entry.js +173 -0
- package/src/cli/index.js +77 -66
- package/src/cli/instructions.js +349 -0
- package/src/cli/local.js +14 -0
- package/src/cli/model.js +184 -37
- package/src/cli/onboard.js +186 -121
- package/src/cli/rank.js +2 -1
- package/src/cli/ratelimit.js +3 -3
- package/src/cli/tag.js +2 -0
- package/src/lib/assistants.js +93 -0
- package/src/lib/catalog/openrouter.js +6 -1
- package/src/lib/catalog-review.js +195 -0
- package/src/lib/common.js +14 -1
- package/src/lib/creators.js +35 -0
- package/src/lib/index.js +17 -3
- package/src/lib/local-conventions.js +120 -0
- package/src/lib/provider-info.js +98 -0
- package/src/lib/providers.js +69 -14
- package/src/lib/schema.js +15 -3
- package/src/lib/select.js +125 -67
- package/js/session/adapters/image/index.js +0 -40
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import providerDefs from '../lib/providers.js'
|
|
2
|
+
import { fieldDefs } from '../lib/schema.js'
|
|
3
|
+
import { getCuratedModels, catalogEntries } from '../lib/common.js'
|
|
4
|
+
import { ALIASES } from './aliases.js'
|
|
5
|
+
|
|
6
|
+
// Completion must never import the factory (`src/lib/index.js`), which pulls
|
|
7
|
+
// every adapter, provider SDK and the OTel stack — 350ms that would be paid on
|
|
8
|
+
// every press of the tab key. Reading the catalog costs about 30ms.
|
|
9
|
+
|
|
10
|
+
const NOUNS = ['model', 'provider', 'creator', 'tag', 'ratelimit', 'ask', 'transcribe', 'default', 'doctor']
|
|
11
|
+
|
|
12
|
+
const VERBS = {
|
|
13
|
+
model: ['list', 'search', 'stats', 'show', 'get', 'set', 'rm', 'add', 'instructions',
|
|
14
|
+
'check', 'apply', 'rank', 'bench', 'curate', 'backup'],
|
|
15
|
+
provider: ['list', 'show', 'models', 'setup', 'rm'],
|
|
16
|
+
creator: ['list', 'show'],
|
|
17
|
+
tag: ['list', 'show', 'add', 'rm'],
|
|
18
|
+
ratelimit: ['show', 'set', 'rm', 'provider']
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// What the argument at a given offset after `<noun> <verb>` names. `model` is
|
|
22
|
+
// an id already in the catalog, so `model add` — which takes a new one — is
|
|
23
|
+
// deliberately absent.
|
|
24
|
+
const ARGUMENT = {
|
|
25
|
+
ask: ['model'],
|
|
26
|
+
transcribe: ['model'],
|
|
27
|
+
'model show': ['model'],
|
|
28
|
+
'model get': ['model', 'field'],
|
|
29
|
+
'model set': ['model', 'field'],
|
|
30
|
+
'model rm': ['model', 'field'],
|
|
31
|
+
'model bench': ['model'],
|
|
32
|
+
'model curate': ['provider'],
|
|
33
|
+
'model instructions': ['provider'],
|
|
34
|
+
'provider list': ['provider'],
|
|
35
|
+
'provider show': ['provider'],
|
|
36
|
+
'provider models': ['provider'],
|
|
37
|
+
'provider setup': ['provider'],
|
|
38
|
+
'provider rm': ['provider'],
|
|
39
|
+
'creator show': ['creator'],
|
|
40
|
+
'tag list': ['model'],
|
|
41
|
+
'tag show': ['tag'],
|
|
42
|
+
'tag add': ['model', 'tag'],
|
|
43
|
+
'tag rm': ['model', 'tag'],
|
|
44
|
+
'ratelimit show': ['model'],
|
|
45
|
+
'ratelimit set': ['model'],
|
|
46
|
+
'ratelimit rm': ['model']
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const fromCatalog = async (pick) => {
|
|
50
|
+
const catalog = await getCuratedModels()
|
|
51
|
+
const out = new Set()
|
|
52
|
+
for (const [key, spec] of catalogEntries(catalog)) {
|
|
53
|
+
if (spec.deprecated) continue
|
|
54
|
+
pick(key, spec, out)
|
|
55
|
+
}
|
|
56
|
+
return [...out]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const kinds = {
|
|
60
|
+
model: () => fromCatalog((key, _spec, out) => out.add(key)),
|
|
61
|
+
creator: () => fromCatalog((_key, spec, out) => spec.creator && out.add(spec.creator)),
|
|
62
|
+
tag: () => fromCatalog((_key, spec, out) => (spec.tags || []).forEach(t => out.add(t))),
|
|
63
|
+
provider: async () => Object.keys(providerDefs),
|
|
64
|
+
field: async () => Object.keys(fieldDefs)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {number} cword Index of the word being completed (bash COMP_CWORD).
|
|
69
|
+
* @param {string[]} words The full command line, split (bash COMP_WORDS).
|
|
70
|
+
* @returns {Promise<string[]>}
|
|
71
|
+
*/
|
|
72
|
+
export const candidates = async (cword, words) => {
|
|
73
|
+
const current = words[cword] ?? ''
|
|
74
|
+
const before = words.slice(1, cword).filter(w => !w.startsWith('-'))
|
|
75
|
+
|
|
76
|
+
const offer = (list) => list.filter(c => c.startsWith(current)).sort()
|
|
77
|
+
|
|
78
|
+
if (!before.length) {
|
|
79
|
+
return offer([...NOUNS, ...Object.keys(ALIASES)])
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Resolve the alias exactly as the router does, so completion and dispatch
|
|
83
|
+
// never disagree about what a word means.
|
|
84
|
+
const alias = ALIASES[before[0]]
|
|
85
|
+
const chain = alias ? [alias.noun, ...alias.inject, ...before.slice(1)] : before
|
|
86
|
+
const [noun, verb, ...rest] = chain
|
|
87
|
+
|
|
88
|
+
if (chain.length === 1 && VERBS[noun]) return offer(VERBS[noun])
|
|
89
|
+
|
|
90
|
+
// `model show <id>` keys on two words; `ask <id>` on one. Whichever matched
|
|
91
|
+
// decides where the argument list starts.
|
|
92
|
+
const paired = ARGUMENT[`${noun} ${verb}`]
|
|
93
|
+
const shape = paired ?? ARGUMENT[noun]
|
|
94
|
+
if (!shape) return []
|
|
95
|
+
const typed = paired ? rest : [verb, ...rest].filter(Boolean)
|
|
96
|
+
|
|
97
|
+
const kind = shape[typed.length]
|
|
98
|
+
if (!kind || !kinds[kind]) return []
|
|
99
|
+
return offer(await kinds[kind]())
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const BASH_SCRIPT = `# mohdel completion for bash. Add to ~/.bashrc:
|
|
103
|
+
# source <(mo completion bash)
|
|
104
|
+
_mo_completion() {
|
|
105
|
+
local IFS=$'\\n'
|
|
106
|
+
COMPREPLY=($(mo __complete "$COMP_CWORD" "\${COMP_WORDS[@]}" 2>/dev/null))
|
|
107
|
+
}
|
|
108
|
+
complete -o default -F _mo_completion mo
|
|
109
|
+
`
|
|
110
|
+
|
|
111
|
+
export async function runComplete (args) {
|
|
112
|
+
// A crash here would break the user's tab key, so an unreadable or corrupt
|
|
113
|
+
// catalog offers nothing rather than an error.
|
|
114
|
+
try {
|
|
115
|
+
const cword = Number.parseInt(args[0], 10)
|
|
116
|
+
if (!Number.isFinite(cword)) return
|
|
117
|
+
const list = await candidates(cword, args.slice(1))
|
|
118
|
+
if (list.length) process.stdout.write(`${list.join('\n')}\n`)
|
|
119
|
+
} catch {}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function runCompletionScript (args) {
|
|
123
|
+
const shell = args[0]
|
|
124
|
+
if (shell === 'bash') {
|
|
125
|
+
process.stdout.write(BASH_SCRIPT)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
console.error(`Usage: mo completion bash${shell ? ` (got "${shell}")` : ''}`)
|
|
129
|
+
process.exit(1)
|
|
130
|
+
}
|
package/src/cli/default.js
CHANGED
|
@@ -1,11 +1,37 @@
|
|
|
1
1
|
import { intro, outro, select, isCancel, cancel } from '@clack/prompts'
|
|
2
|
-
import {
|
|
3
|
-
import providers from '../lib/providers.js'
|
|
2
|
+
import { getConfig, getCuratedModels, saveConfig, catalogEntries } from '../lib/common.js'
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
// CLI-only. The default model is a convenience for someone typing `mo ask`;
|
|
5
|
+
// the library and the gate never read it. A program picks a model by id or by
|
|
6
|
+
// tag, where the choice is explicit and reviewable — a machine inheriting a
|
|
7
|
+
// human's terminal preference is how a call ends up on a model nobody chose.
|
|
8
|
+
export async function runDefault (args = []) {
|
|
9
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
10
|
+
console.log(`mohdel default — the model "mo ask" uses when you name none
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
default Pick one from your catalog (interactive)
|
|
14
|
+
default <model> Set it directly
|
|
15
|
+
|
|
16
|
+
Stored in ~/.config/mohdel/default.json. The library and the gate never
|
|
17
|
+
read it: a program names its model, or selects one by tag.`)
|
|
18
|
+
process.exit(0)
|
|
19
|
+
}
|
|
7
20
|
|
|
8
21
|
const curated = await getCuratedModels()
|
|
22
|
+
|
|
23
|
+
const named = args.find(a => !a.startsWith('-'))
|
|
24
|
+
if (named) {
|
|
25
|
+
if (!curated[named] || curated[named].deprecated) {
|
|
26
|
+
console.error(`'${named}' is not a model in your catalog. "mo ls" lists them.`)
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
await saveConfig({ ...(await getConfig()), defaultModel: named })
|
|
30
|
+
console.log(`Default set to ${named} — "mo ask" uses it when you give no model.`)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
intro('mohdel — set the default model')
|
|
9
35
|
const modelOptions = catalogEntries(curated).map(([modelId, info]) => ({
|
|
10
36
|
value: modelId,
|
|
11
37
|
label: `${info.label} (${modelId})`
|
|
@@ -22,16 +48,11 @@ export async function runDefault () {
|
|
|
22
48
|
process.exit(0)
|
|
23
49
|
}
|
|
24
50
|
|
|
25
|
-
const [providerName] = selectedModelId.split('/')
|
|
26
|
-
|
|
27
51
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
await saveConfig(config)
|
|
34
|
-
outro(`Default set to ${selectedModelId} — saved to ${CONFIG_PATH}`)
|
|
52
|
+
// Merge: the file also holds the chosen coding agent, and replacing the
|
|
53
|
+
// object wholesale would drop it.
|
|
54
|
+
await saveConfig({ ...(await getConfig()), defaultModel: selectedModelId })
|
|
55
|
+
outro(`Default set to ${selectedModelId} — "mo ask" uses it when you give no model.`)
|
|
35
56
|
} catch (err) {
|
|
36
57
|
cancel(`Error: ${err.message}`)
|
|
37
58
|
process.exit(1)
|
package/src/cli/doctor.js
CHANGED
|
@@ -3,7 +3,7 @@ import { label, meta, ok, warn, err, inactive } from './colors.js'
|
|
|
3
3
|
import providers from '../lib/providers.js'
|
|
4
4
|
import { validate, isValidTag } from '../lib/schema.js'
|
|
5
5
|
import {
|
|
6
|
-
CONFIG_DIR, CURATED_PATH, CONFIG_PATH, ENV_PATH,
|
|
6
|
+
CONFIG_DIR, CURATED_PATH, CONFIG_PATH, ENV_PATH, tildePath,
|
|
7
7
|
getCuratedModels, getConfig, loadDefaultEnv, getAPIKey, catalogEntries
|
|
8
8
|
} from '../lib/common.js'
|
|
9
9
|
|
|
@@ -11,11 +11,15 @@ const row = (status, name, detail = '') =>
|
|
|
11
11
|
` ${status} ${name.padEnd(20)} ${meta(detail)}`
|
|
12
12
|
|
|
13
13
|
export async function runDoctor (args) {
|
|
14
|
+
const all = args.includes('--all')
|
|
14
15
|
if (args.includes('-h') || args.includes('--help')) {
|
|
15
16
|
console.log(`mohdel doctor — check that your install is wired up
|
|
16
17
|
|
|
17
18
|
Usage:
|
|
18
|
-
mo doctor [--json]
|
|
19
|
+
mo doctor [--all] [--json]
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--all List every provider's API key variable, set or not
|
|
19
23
|
|
|
20
24
|
What it checks:
|
|
21
25
|
- Config directory and environment file exist
|
|
@@ -45,9 +49,10 @@ Exit code:
|
|
|
45
49
|
|
|
46
50
|
// 1. Config dir + env file
|
|
47
51
|
report.configDir.ok = existsSync(CONFIG_DIR)
|
|
52
|
+
report.assistant = (await getConfig()).assistant || null
|
|
48
53
|
report.envFile.ok = existsSync(ENV_PATH)
|
|
49
54
|
if (!report.configDir.ok) report.warnings.push('config directory does not exist (will be created on first save)')
|
|
50
|
-
if (!report.envFile.ok) report.warnings.push(`no ${ENV_PATH} — set API keys there or via shell env`)
|
|
55
|
+
if (!report.envFile.ok) report.warnings.push(`no ${tildePath(ENV_PATH)} — set API keys there or via shell env`)
|
|
51
56
|
|
|
52
57
|
// 2. API keys per provider
|
|
53
58
|
for (const [name, def] of Object.entries(providers)) {
|
|
@@ -70,6 +75,9 @@ Exit code:
|
|
|
70
75
|
const entries = catalogEntries(curated)
|
|
71
76
|
report.curatedFile.active = entries.filter(([, s]) => !s.deprecated).length
|
|
72
77
|
report.curatedFile.deprecated = entries.length - report.curatedFile.active
|
|
78
|
+
if (report.curatedFile.active === 0) {
|
|
79
|
+
report.warnings.push('catalog is empty — "mo curate <provider>" adds a provider\'s models, then "mo model instructions <provider>" fills in the prices')
|
|
80
|
+
}
|
|
73
81
|
|
|
74
82
|
// Schema validation (same logic as 'mo check', condensed)
|
|
75
83
|
const knownProviders = new Set(Object.keys(providers))
|
|
@@ -126,13 +134,15 @@ Exit code:
|
|
|
126
134
|
}
|
|
127
135
|
|
|
128
136
|
// Pretty output
|
|
129
|
-
console.log(label('
|
|
137
|
+
console.log(label('mohdel doctor\n'))
|
|
130
138
|
|
|
131
139
|
console.log(label('Configuration'))
|
|
132
|
-
console.log(row(report.configDir.ok ? ok('✓') : warn('!'), 'Config dir', report.configDir.path))
|
|
133
|
-
console.log(row(
|
|
140
|
+
console.log(row(report.configDir.ok ? ok('✓') : warn('!'), 'Config dir', tildePath(report.configDir.path)))
|
|
141
|
+
if (report.assistant) console.log(row(ok('✓'), 'Coding agent', report.assistant))
|
|
142
|
+
console.log(row(report.envFile.ok ? ok('✓') : warn('!'), 'Env file', report.envFile.ok ? tildePath(report.envFile.path) : `${tildePath(report.envFile.path)} (missing)`))
|
|
134
143
|
if (report.curatedFile.ok) {
|
|
135
|
-
|
|
144
|
+
const mark = report.curatedFile.active === 0 ? warn('!') : ok('✓')
|
|
145
|
+
console.log(row(mark, 'curated.json', `${report.curatedFile.active} active, ${report.curatedFile.deprecated} deprecated`))
|
|
136
146
|
} else {
|
|
137
147
|
console.log(row(err('✗'), 'curated.json', 'failed to load'))
|
|
138
148
|
}
|
|
@@ -142,13 +152,22 @@ Exit code:
|
|
|
142
152
|
for (const k of report.keys.configured) {
|
|
143
153
|
console.log(row(ok('✓'), k.provider, k.envVar))
|
|
144
154
|
}
|
|
145
|
-
|
|
146
|
-
|
|
155
|
+
// A row per unset provider is a dozen lines of nothing on a fresh install,
|
|
156
|
+
// but the variable names are what you came for when a key isn't picked up.
|
|
157
|
+
if (all) {
|
|
158
|
+
for (const k of report.keys.missing) {
|
|
159
|
+
console.log(row(inactive('○'), k.provider, `${k.envVar} (unset)`))
|
|
160
|
+
}
|
|
161
|
+
} else if (report.keys.missing.length) {
|
|
162
|
+
console.log(row(inactive('○'), `${report.keys.missing.length} unset`,
|
|
163
|
+
meta('"mo providers" lists them, "mo doctor --all" their variables')))
|
|
147
164
|
}
|
|
148
165
|
|
|
149
166
|
console.log()
|
|
150
167
|
console.log(label('Catalog validation'))
|
|
151
|
-
if (report.
|
|
168
|
+
if (report.curatedFile.ok && report.curatedFile.active === 0 && report.curatedFile.deprecated === 0) {
|
|
169
|
+
console.log(` ${inactive('○')} nothing to validate`)
|
|
170
|
+
} else if (report.schema.errors.length) {
|
|
152
171
|
console.log(` ${err('✗')} ${report.schema.errors.length} error(s) ${meta('— run "mo check" for details')}`)
|
|
153
172
|
} else {
|
|
154
173
|
console.log(` ${ok('✓')} no errors`)
|
|
@@ -171,11 +190,11 @@ Exit code:
|
|
|
171
190
|
if (report.errors.length) {
|
|
172
191
|
console.log(`${err('✗')} ${report.errors.length} error(s):`)
|
|
173
192
|
for (const e of report.errors) console.log(` ${err('✗')} ${e}`)
|
|
174
|
-
|
|
175
|
-
|
|
193
|
+
}
|
|
194
|
+
if (report.warnings.length) {
|
|
176
195
|
console.log(`${warn('!')} ${report.warnings.length} warning(s) — install is usable but not fully configured`)
|
|
177
196
|
for (const w of report.warnings) console.log(` ${warn('!')} ${w}`)
|
|
178
|
-
} else {
|
|
179
|
-
console.log(`${ok('✓')} ready`)
|
|
180
197
|
}
|
|
198
|
+
if (report.errors.length) process.exit(1)
|
|
199
|
+
if (!report.warnings.length) console.log(`${ok('✓')} ready`)
|
|
181
200
|
}
|
package/src/cli/entry.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { readFile, rm } from 'node:fs/promises'
|
|
2
|
+
import { CURATED_PATH, getCuratedModels, saveCuratedModels, tildePath } from '../lib/common.js'
|
|
3
|
+
import { parseCandidates, reviewCandidates } from '../lib/catalog-review.js'
|
|
4
|
+
import { localConventionsOrExit } from './local.js'
|
|
5
|
+
import { id, label, meta, err, warn, ok } from './colors.js'
|
|
6
|
+
|
|
7
|
+
const readStdin = async () => {
|
|
8
|
+
const chunks = []
|
|
9
|
+
for await (const chunk of process.stdin) chunks.push(chunk)
|
|
10
|
+
return Buffer.concat(chunks).toString('utf8')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const readCandidateFile = async (path) => {
|
|
14
|
+
if (path === '-') return readStdin()
|
|
15
|
+
return readFile(path, 'utf8')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const show = (value) => JSON.stringify(value)
|
|
19
|
+
|
|
20
|
+
const renderChange = ({ field, from, to }) => {
|
|
21
|
+
const name = field.padEnd(22)
|
|
22
|
+
if (from === undefined) return ` ${ok('+')} ${name} ${show(to)}`
|
|
23
|
+
if (to === undefined) return ` ${err('-')} ${name} ${show(from)} ${meta('→ (removed)')}`
|
|
24
|
+
return ` ${warn('~')} ${name} ${show(from)} ${meta('→')} ${show(to)}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const printReview = (reviews, ignored) => {
|
|
28
|
+
for (const key of ignored) {
|
|
29
|
+
console.log(meta(`skipping meta key ${key}`))
|
|
30
|
+
}
|
|
31
|
+
for (const review of reviews) {
|
|
32
|
+
console.log(`\n${id(review.key)} ${label(review.status)}`)
|
|
33
|
+
for (const change of review.changes) console.log(renderChange(change))
|
|
34
|
+
for (const e of review.errors) console.log(` ${err('✗')} ${e}`)
|
|
35
|
+
for (const w of review.warnings) console.log(` ${warn('!')} ${w}`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const summarize = (reviews) => ({
|
|
40
|
+
candidates: reviews.length,
|
|
41
|
+
new: reviews.filter(r => r.status === 'new').length,
|
|
42
|
+
changed: reviews.filter(r => r.status === 'changed').length,
|
|
43
|
+
unchanged: reviews.filter(r => r.status === 'unchanged').length,
|
|
44
|
+
errors: reviews.reduce((n, r) => n + r.errors.length, 0),
|
|
45
|
+
warnings: reviews.reduce((n, r) => n + r.warnings.length, 0)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const printSummary = (counts) => {
|
|
49
|
+
const parts = [`${counts.new} new`, `${counts.changed} changed`, `${counts.unchanged} unchanged`]
|
|
50
|
+
console.log(`\n${label(`${counts.candidates} candidate(s):`)} ${parts.join(', ')} ${meta('│')} ` +
|
|
51
|
+
`${counts.errors ? err(`${counts.errors} error(s)`) : ok('0 errors')}, ${counts.warnings} warning(s)`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const loadReview = async (path) => {
|
|
55
|
+
const { entries, ignored } = parseCandidates(await readCandidateFile(path))
|
|
56
|
+
const curated = await getCuratedModels()
|
|
57
|
+
const local = await localConventionsOrExit()
|
|
58
|
+
return { curated, entries, ignored, reviews: reviewCandidates(curated, entries, { local }) }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function runCheckEntry (args) {
|
|
62
|
+
const path = args[args.indexOf('--entry') + 1]
|
|
63
|
+
if (!path) {
|
|
64
|
+
console.error('Usage: mo model check --entry <file|->')
|
|
65
|
+
process.exit(1)
|
|
66
|
+
}
|
|
67
|
+
const json = args.includes('--json')
|
|
68
|
+
|
|
69
|
+
let review
|
|
70
|
+
try {
|
|
71
|
+
review = await loadReview(path)
|
|
72
|
+
} catch (e) {
|
|
73
|
+
if (json) console.log(JSON.stringify({ error: e.message }, null, 2))
|
|
74
|
+
else console.error(err(e.message))
|
|
75
|
+
process.exit(1)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const counts = summarize(review.reviews)
|
|
79
|
+
if (json) {
|
|
80
|
+
console.log(JSON.stringify({ counts, ignored: review.ignored, candidates: review.reviews }, null, 2))
|
|
81
|
+
} else {
|
|
82
|
+
printReview(review.reviews, review.ignored)
|
|
83
|
+
printSummary(counts)
|
|
84
|
+
if (!counts.errors) {
|
|
85
|
+
console.log(`${meta('Next:')} mo model apply ${path === '-' ? '<file>' : path}`)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (counts.errors) process.exit(1)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function runApply (args) {
|
|
92
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
93
|
+
console.log(`mohdel model apply — write reviewed catalog entries
|
|
94
|
+
|
|
95
|
+
Usage:
|
|
96
|
+
model apply <file|-> [--yes] [--rm]
|
|
97
|
+
|
|
98
|
+
Reads entries in curated.json shape, validates them, prints the diff
|
|
99
|
+
against ${tildePath(CURATED_PATH)}, and writes after confirmation. Nothing is
|
|
100
|
+
written when validation reports an error.
|
|
101
|
+
|
|
102
|
+
The candidate file is left alone unless you say otherwise — at a terminal
|
|
103
|
+
it asks, and --rm removes it without asking. Undo uses the catalog's own
|
|
104
|
+
backups, not the candidate.
|
|
105
|
+
|
|
106
|
+
Options:
|
|
107
|
+
--yes Skip the confirmation prompt
|
|
108
|
+
--rm Delete the candidate file once it has been applied
|
|
109
|
+
|
|
110
|
+
Write an entry with your coding agent: mo model instructions <provider>`)
|
|
111
|
+
process.exit(0)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const path = args.find(a => !a.startsWith('--'))
|
|
115
|
+
if (!path) {
|
|
116
|
+
console.error('Usage: mo model apply <file|-> [--yes] [--rm]')
|
|
117
|
+
process.exit(1)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let review
|
|
121
|
+
try {
|
|
122
|
+
review = await loadReview(path)
|
|
123
|
+
} catch (e) {
|
|
124
|
+
console.error(err(e.message))
|
|
125
|
+
process.exit(1)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const counts = summarize(review.reviews)
|
|
129
|
+
printReview(review.reviews, review.ignored)
|
|
130
|
+
printSummary(counts)
|
|
131
|
+
|
|
132
|
+
if (counts.errors) {
|
|
133
|
+
console.error(`\n${err('Not written')} — fix the errors above and run again.`)
|
|
134
|
+
process.exit(1)
|
|
135
|
+
}
|
|
136
|
+
const nothingToDo = !counts.new && !counts.changed
|
|
137
|
+
if (nothingToDo) console.log(`\n${ok('Nothing to write')} — the catalog already matches.`)
|
|
138
|
+
|
|
139
|
+
if (!nothingToDo && !args.includes('--yes')) {
|
|
140
|
+
if (!process.stdout.isTTY || path === '-') {
|
|
141
|
+
console.error(`\n${err('Not written')} — no terminal to confirm on. Re-run in a terminal, or pass --yes.`)
|
|
142
|
+
process.exit(1)
|
|
143
|
+
}
|
|
144
|
+
const { confirm, isCancel } = await import('@clack/prompts')
|
|
145
|
+
const go = await confirm({ message: `Write ${counts.new + counts.changed} entry(ies) to ${tildePath(CURATED_PATH)}?` })
|
|
146
|
+
if (isCancel(go) || !go) {
|
|
147
|
+
console.log(meta('Not written.'))
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!nothingToDo) {
|
|
153
|
+
await saveCuratedModels({ ...review.curated, ...review.entries })
|
|
154
|
+
console.log(`${ok('✓')} ${counts.new + counts.changed} entry(ies) written to ${tildePath(CURATED_PATH)}`)
|
|
155
|
+
console.log(`${meta('Undo:')} mo model backup diff prev ${meta('│')} mo model backup restore prev`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The candidate was written by the agent, not by mohdel, so it is never
|
|
159
|
+
// removed unasked. It has served its purpose though, and the catalog's own
|
|
160
|
+
// backups are what an undo uses — not this file.
|
|
161
|
+
if (path === '-') return
|
|
162
|
+
if (args.includes('--rm')) {
|
|
163
|
+
await rm(path, { force: true })
|
|
164
|
+
console.log(`${ok('✓')} removed ${path}`)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
if (!process.stdout.isTTY) return
|
|
168
|
+
const { confirm, isCancel } = await import('@clack/prompts')
|
|
169
|
+
const drop = await confirm({ message: `Delete ${path}?`, initialValue: false })
|
|
170
|
+
if (isCancel(drop) || !drop) return
|
|
171
|
+
await rm(path, { force: true })
|
|
172
|
+
console.log(`${ok('✓')} removed ${path}`)
|
|
173
|
+
}
|