devloom 1.0.0 → 1.2.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/.ai/core.dsl +44 -0
- package/.ai/skills.dsl +39 -0
- package/.ai/verify.dsl +97 -0
- package/.ai/workflow.dsl +81 -0
- package/.opencode/themes/devloom-night-owl.json +103 -0
- package/GUIDE.md +572 -115
- package/README.md +583 -109
- package/SECURITY.md +93 -0
- package/agents/devloom-developer-flash.md +31 -0
- package/agents/devloom-developer-senior.md +32 -0
- package/agents/devloom-developer.md +21 -55
- package/agents/devloom-documenter-flash.md +28 -0
- package/agents/devloom-documenter.md +17 -56
- package/agents/devloom-orchestrator.md +236 -356
- package/agents/devloom-planner-flash.md +34 -0
- package/agents/devloom-planner-senior.md +35 -0
- package/agents/devloom-planner.md +36 -0
- package/agents/devloom-qa-flash.md +32 -0
- package/agents/devloom-qa.md +21 -83
- package/agents/devloom-security-senior.md +35 -0
- package/agents/devloom-security.md +34 -0
- package/agents/devloom-verifier.md +33 -0
- package/agents/devloom-vision.md +113 -0
- package/agents/devloom-visual-critic.md +102 -0
- package/agents/devloom-visual-director.md +84 -0
- package/commands/devloom-agents.md +49 -0
- package/commands/devloom-auto.md +11 -0
- package/commands/devloom-context.md +82 -0
- package/commands/devloom-deepseek-mimo.md +11 -0
- package/commands/devloom-deepseek.md +11 -0
- package/commands/devloom-free.md +11 -0
- package/commands/devloom-go-economy.md +11 -0
- package/commands/devloom-go-flash.md +11 -0
- package/commands/devloom-go.md +11 -0
- package/commands/devloom-init.md +28 -50
- package/commands/devloom-loop-status.md +25 -0
- package/commands/devloom-loop.md +36 -0
- package/commands/devloom-mimo.md +11 -0
- package/commands/devloom-plan.md +13 -0
- package/commands/devloom-refresh.md +22 -0
- package/commands/devloom-resume.md +30 -49
- package/commands/devloom-save.md +69 -0
- package/commands/devloom-status.md +9 -18
- package/commands/devloom.md +99 -67
- package/dist/agents.d.ts +30 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +139 -0
- package/dist/agents.js.map +1 -0
- package/dist/bootstrap.d.ts +56 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +116 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/constraints.d.ts +5 -0
- package/dist/constraints.d.ts.map +1 -0
- package/dist/constraints.js +45 -0
- package/dist/constraints.js.map +1 -0
- package/dist/context.d.ts +42 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +636 -0
- package/dist/context.js.map +1 -0
- package/dist/guard.d.ts +19 -0
- package/dist/guard.d.ts.map +1 -0
- package/dist/guard.js +418 -0
- package/dist/guard.js.map +1 -0
- package/dist/loop.d.ts +64 -0
- package/dist/loop.d.ts.map +1 -0
- package/dist/loop.js +164 -0
- package/dist/loop.js.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +101 -1
- package/dist/plugin.js.map +1 -1
- package/dist/tui-agents.d.ts +61 -0
- package/dist/tui-agents.d.ts.map +1 -0
- package/dist/tui-agents.js +89 -0
- package/dist/tui-agents.js.map +1 -0
- package/dist/tui.d.ts +6 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +92 -0
- package/dist/tui.js.map +1 -0
- package/dist/worktree.d.ts +46 -0
- package/dist/worktree.d.ts.map +1 -0
- package/dist/worktree.js +272 -0
- package/dist/worktree.js.map +1 -0
- package/package.json +46 -6
- package/patterns/changelog-drafter.md +38 -0
- package/patterns/ci-sweeper.md +40 -0
- package/patterns/daily-triage.md +37 -0
- package/patterns/dependency-sweeper.md +40 -0
- package/patterns/design-audit.md +63 -0
- package/patterns/issue-triage.md +37 -0
- package/patterns/post-merge-cleanup.md +34 -0
- package/patterns/pr-babysitter.md +37 -0
- package/patterns/registry.yaml +52 -0
- package/postinstall.mjs +264 -64
- package/project/README.md +29 -0
- package/protocol/agent-contracts.md +27 -0
- package/protocol/artifact-system.md +53 -0
- package/protocol/model-routing.md +194 -0
- package/protocol/orchestrator-core.md +43 -0
- package/protocol/project-system.md +45 -0
- package/protocol/rules.md +18 -0
- package/protocol/verification-policy.md +48 -0
- package/scripts/briefing.mjs +192 -0
- package/scripts/loop-run.mjs +205 -0
- package/scripts/model-capabilities.mjs +194 -0
- package/scripts/plugin-cache.mjs +269 -0
- package/scripts/profile.mjs +755 -0
- package/scripts/visual-benchmark.mjs +149 -0
- package/scripts/worktree.mjs +445 -0
- package/skills/build/development.md +33 -0
- package/skills/build/live-docs.md +38 -0
- package/skills/build/simplify.md +50 -0
- package/skills/build/vision-analysis.md +98 -0
- package/skills/design/app-design.md +31 -0
- package/skills/design/design-grounding.md +30 -0
- package/skills/design/design-system.md +27 -0
- package/skills/design/game-design.md +35 -0
- package/skills/design/motion-design.md +34 -0
- package/skills/design/visual-direction.md +32 -0
- package/skills/design/web-design.md +33 -0
- package/skills/loop/changelog-drafter.md +32 -0
- package/skills/loop/ci-sweeper.md +24 -0
- package/skills/loop/constraints.md +21 -0
- package/skills/loop/dependency-sweeper.md +24 -0
- package/skills/loop/design-audit.md +67 -0
- package/skills/loop/issue-triage.md +27 -0
- package/skills/loop/post-merge-cleanup.md +24 -0
- package/skills/loop/pr-babysitter.md +26 -0
- package/skills/loop/triage.md +25 -0
- package/skills/loop/verifier.md +20 -0
- package/skills/meta/skill-discovery.md +23 -0
- package/skills/plan/planning.md +27 -0
- package/skills/plan/verification-planning.md +40 -0
- package/skills/review/security-review.md +63 -0
- package/skills/ship/documentation.md +11 -0
- package/skills/verify/app-verification.md +19 -0
- package/skills/verify/quality-assurance.md +17 -0
- package/skills/verify/visual-critique.md +30 -0
- package/skills/verify/visual-quality-gate.md +29 -0
- package/skills/verify/visual-regression.md +25 -0
- package/agents/devloom-analyst.md +0 -83
- package/agents/devloom-architect.md +0 -88
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, cpSync } from "fs"
|
|
3
|
+
import { homedir } from "os"
|
|
4
|
+
import { resolve, dirname } from "path"
|
|
5
|
+
import { fileURLToPath } from "url"
|
|
6
|
+
import { execSync } from "child_process"
|
|
7
|
+
import {
|
|
8
|
+
pluginCacheStatus,
|
|
9
|
+
refreshOpenCodePluginAgentFiles,
|
|
10
|
+
} from "./plugin-cache.mjs"
|
|
11
|
+
import {
|
|
12
|
+
isValidModelId,
|
|
13
|
+
findAvailableCandidate,
|
|
14
|
+
resolveModelCandidate,
|
|
15
|
+
capabilitiesFor,
|
|
16
|
+
modelMeetsRole,
|
|
17
|
+
roleRequiresVision,
|
|
18
|
+
MIMO_V26_FLASH_CANDIDATES,
|
|
19
|
+
DEEPSEEK_V41_FLASH_CANDIDATES,
|
|
20
|
+
MODEL_FAMILIES,
|
|
21
|
+
VISION_FALLBACK_CANDIDATES,
|
|
22
|
+
} from "./model-capabilities.mjs"
|
|
23
|
+
|
|
24
|
+
// Provider-agnostic model helpers re-exported so consumers (commands, tests)
|
|
25
|
+
// can import them from the profile manager as the single entry point.
|
|
26
|
+
export {
|
|
27
|
+
isValidModelId,
|
|
28
|
+
findAvailableCandidate,
|
|
29
|
+
resolveModelCandidate,
|
|
30
|
+
capabilitiesFor,
|
|
31
|
+
modelMeetsRole,
|
|
32
|
+
roleRequiresVision,
|
|
33
|
+
MIMO_V26_FLASH_CANDIDATES,
|
|
34
|
+
DEEPSEEK_V41_FLASH_CANDIDATES,
|
|
35
|
+
MODEL_FAMILIES,
|
|
36
|
+
VISION_FALLBACK_CANDIDATES,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const CONFIG_PATH = ".opencode/devloom/config.json"
|
|
40
|
+
const AGENTS_DIR = homedir() + "/.config/opencode/agents"
|
|
41
|
+
const SOURCE_AGENTS_DIR = resolve(dirname(new URL(import.meta.url).pathname), "..", "agents")
|
|
42
|
+
|
|
43
|
+
export const TIERS = {
|
|
44
|
+
senior: {
|
|
45
|
+
orchestrator: "opencode-go/deepseek-v4-flash",
|
|
46
|
+
planner: "opencode-go/glm-5.2",
|
|
47
|
+
developer: "opencode-go/kimi-k3",
|
|
48
|
+
qa: "opencode-go/deepseek-v4-pro",
|
|
49
|
+
verifier: "opencode-go/deepseek-v4-pro",
|
|
50
|
+
security: "opencode-go/glm-5.2",
|
|
51
|
+
documenter: "opencode-go/qwen3.7-plus",
|
|
52
|
+
vision: "opencode-go/glm-5.3-flash",
|
|
53
|
+
"visual-director": "opencode-go/glm-5.2",
|
|
54
|
+
"visual-critic": "opencode-go/glm-5.3-flash"
|
|
55
|
+
},
|
|
56
|
+
standard: {
|
|
57
|
+
orchestrator: "opencode-go/deepseek-v4-flash",
|
|
58
|
+
planner: "opencode-go/qwen3.7-max",
|
|
59
|
+
developer: "opencode-go/glm-5.3-flash",
|
|
60
|
+
qa: "opencode-go/deepseek-v4-flash",
|
|
61
|
+
verifier: "opencode-go/deepseek-v4-flash",
|
|
62
|
+
security: "opencode-go/deepseek-v4-flash",
|
|
63
|
+
documenter: "opencode-go/deepseek-v4-flash",
|
|
64
|
+
vision: "opencode-go/glm-5.3-flash",
|
|
65
|
+
"visual-director": "opencode-go/glm-5.3-flash",
|
|
66
|
+
"visual-critic": "opencode-go/glm-5.3-flash"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const PROFILES = {
|
|
71
|
+
go: {
|
|
72
|
+
orchestrator: "opencode-go/deepseek-v4-flash",
|
|
73
|
+
planner: "opencode-go/qwen3.7-max",
|
|
74
|
+
developer: "opencode-go/glm-5.3-flash",
|
|
75
|
+
qa: "opencode-go/deepseek-v4-pro",
|
|
76
|
+
verifier: "opencode-go/deepseek-v4-pro",
|
|
77
|
+
security: "opencode-go/glm-5.2",
|
|
78
|
+
documenter: "opencode-go/qwen3.7-plus",
|
|
79
|
+
vision: "opencode-go/glm-5.3-flash",
|
|
80
|
+
"visual-director": "opencode-go/glm-5.2",
|
|
81
|
+
"visual-critic": "opencode-go/glm-5.3-flash"
|
|
82
|
+
},
|
|
83
|
+
"go-economy": {
|
|
84
|
+
orchestrator: "opencode-go/deepseek-v4-flash",
|
|
85
|
+
planner: "opencode-go/deepseek-v4-pro",
|
|
86
|
+
developer: "opencode-go/deepseek-v4-pro",
|
|
87
|
+
qa: "opencode-go/deepseek-v4-flash",
|
|
88
|
+
verifier: "opencode-go/deepseek-v4-flash",
|
|
89
|
+
security: "opencode-go/deepseek-v4-pro",
|
|
90
|
+
documenter: "opencode-go/qwen3.7-plus",
|
|
91
|
+
vision: "opencode-go/glm-5.3-flash",
|
|
92
|
+
"visual-director": "opencode-go/deepseek-v4-pro",
|
|
93
|
+
"visual-critic": "opencode-go/glm-5.3-flash"
|
|
94
|
+
},
|
|
95
|
+
deepseek: {
|
|
96
|
+
orchestrator: "opencode-go/deepseek-v4-pro",
|
|
97
|
+
planner: "opencode-go/deepseek-v4-pro",
|
|
98
|
+
developer: "opencode-go/deepseek-v4-pro",
|
|
99
|
+
qa: "opencode-go/deepseek-v4-pro",
|
|
100
|
+
verifier: "opencode-go/deepseek-v4-flash",
|
|
101
|
+
security: "opencode-go/deepseek-v4-pro",
|
|
102
|
+
documenter: "opencode-go/deepseek-v4-flash",
|
|
103
|
+
vision: "opencode-go/deepseek-v4-flash-vision-exp",
|
|
104
|
+
"visual-director": "opencode-go/deepseek-v4-pro",
|
|
105
|
+
"visual-critic": "opencode-go/deepseek-v4-flash-vision-exp"
|
|
106
|
+
},
|
|
107
|
+
glm: {
|
|
108
|
+
orchestrator: "opencode-go/glm-5.3-flash",
|
|
109
|
+
planner: "opencode-go/glm-5.3-flash",
|
|
110
|
+
developer: "opencode-go/glm-5.3-flash",
|
|
111
|
+
qa: "opencode-go/glm-5.3-flash",
|
|
112
|
+
verifier: "opencode-go/glm-5.3-flash",
|
|
113
|
+
security: "opencode-go/glm-5.3-flash",
|
|
114
|
+
documenter: "opencode-go/glm-5.3-flash",
|
|
115
|
+
vision: "opencode-go/glm-5.3-flash",
|
|
116
|
+
"visual-director": "opencode-go/glm-5.3-flash",
|
|
117
|
+
"visual-critic": "opencode-go/glm-5.3-flash"
|
|
118
|
+
},
|
|
119
|
+
"go-flash": {
|
|
120
|
+
orchestrator: "opencode-go/deepseek-v4-flash",
|
|
121
|
+
planner: "opencode-go/deepseek-v4-flash",
|
|
122
|
+
developer: "opencode-go/deepseek-v4-flash",
|
|
123
|
+
qa: "opencode-go/deepseek-v4-flash",
|
|
124
|
+
verifier: "opencode-go/deepseek-v4-flash",
|
|
125
|
+
security: "opencode-go/deepseek-v4-flash",
|
|
126
|
+
documenter: "opencode-go/deepseek-v4-flash",
|
|
127
|
+
vision: "opencode-go/glm-5.3-flash",
|
|
128
|
+
"visual-director": "opencode-go/deepseek-v4-flash",
|
|
129
|
+
"visual-critic": "opencode-go/glm-5.3-flash"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const FREE_CANDIDATES_BY_ROLE = {
|
|
134
|
+
orchestration: ["opencode/big-pickle", "opencode/muse-spark-1.3-contributor-free", "opencode/mimo-v2.5-free", "opencode/muse-spark-1.2-contributor-free", "opencode/nemotron-3.5-lightning-free", "opencode/nemotron-3-ultra-free", "opencode/ling-3.0-flash-fin-free"],
|
|
135
|
+
planning: ["opencode/nemotron-3-ultra-free", "opencode/muse-spark-1.3-contributor-free", "opencode/muse-spark-1.2-contributor-free", "opencode/big-pickle", "opencode/nemotron-3.5-lightning-free", "opencode/ling-3.0-flash-fin-free"],
|
|
136
|
+
implementation: ["opencode/big-pickle", "opencode/muse-spark-1.3-contributor-free", "opencode/nemotron-3.5-lightning-free", "opencode/mimo-v2.5-free", "opencode/nemotron-3-ultra-free", "opencode/ling-3.0-flash-fin-free"],
|
|
137
|
+
verification: ["opencode/big-pickle", "opencode/muse-spark-1.3-contributor-free", "opencode/nemotron-3.5-lightning-free", "opencode/mimo-v2.5-free", "opencode/nemotron-3-ultra-free", "opencode/ling-3.0-flash-fin-free"],
|
|
138
|
+
documentation: ["opencode/muse-spark-1.3-contributor-free", "opencode/muse-spark-1.2-contributor-free", "opencode/nemotron-3-ultra-free", "opencode/mimo-v2.5-free", "opencode/ling-3.0-flash-fin-free"],
|
|
139
|
+
vision: ["opencode/mimo-v2.5-free", "opencode-go/deepseek-v4-flash-vision-exp", "opencode-go/minimax-m3", "opencode-go/mimo-v2.5-pro"]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const FREE_ROLE_MAP = {
|
|
143
|
+
orchestrator: "orchestration",
|
|
144
|
+
planner: "planning",
|
|
145
|
+
developer: "implementation",
|
|
146
|
+
qa: "verification",
|
|
147
|
+
verifier: "verification",
|
|
148
|
+
security: "verification",
|
|
149
|
+
documenter: "documentation",
|
|
150
|
+
vision: "vision",
|
|
151
|
+
"visual-director": "vision",
|
|
152
|
+
"visual-critic": "vision"
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const ALL_AGENTS = Object.keys(FREE_ROLE_MAP)
|
|
156
|
+
|
|
157
|
+
// Profiles that pin every variant model to their profile definition —
|
|
158
|
+
// tier model overrides would silently break that promise, so they are locked.
|
|
159
|
+
export const LOCKED_TIER_PROFILES = new Set(["go-flash", "free", "mimo"])
|
|
160
|
+
|
|
161
|
+
function readJson(path, fallback = null) {
|
|
162
|
+
try { return JSON.parse(readFileSync(path, "utf8")) } catch { return fallback }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function writeJson(path, data) {
|
|
166
|
+
const dir = dirname(path)
|
|
167
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
168
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n")
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function run(cmd) {
|
|
172
|
+
try {
|
|
173
|
+
return execSync(cmd, { encoding: "utf8", timeout: 30000 }).trim()
|
|
174
|
+
} catch {
|
|
175
|
+
return ""
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function detectAvailableModels() {
|
|
180
|
+
const output = run("opencode models --refresh 2>/dev/null && opencode models 2>/dev/null")
|
|
181
|
+
if (!output) return []
|
|
182
|
+
const lines = output.split("\n").map(l => l.trim()).filter(l => l && !l.startsWith("lmstudio/"))
|
|
183
|
+
return [...new Set(lines)]
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function hasGoModels(available) {
|
|
187
|
+
return available.some(m => m.startsWith("opencode-go/"))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function getFreeModels(available) {
|
|
191
|
+
return available.filter(m => m.startsWith("opencode/") && m.endsWith("-free") || m === "opencode/big-pickle")
|
|
192
|
+
.concat(available.filter(m => m.startsWith("opencode-go/free-")))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function pickBestFree(available, role) {
|
|
196
|
+
const candidates = FREE_CANDIDATES_BY_ROLE[role] || FREE_CANDIDATES_BY_ROLE.verification
|
|
197
|
+
for (const c of candidates) {
|
|
198
|
+
if (available.includes(c)) return c
|
|
199
|
+
}
|
|
200
|
+
for (const c of candidates) {
|
|
201
|
+
for (const a of available) {
|
|
202
|
+
if (a.startsWith("opencode/") && (a.includes(c.split("/").pop()?.split("-free")[0] || c) || c.includes(a.split("/").pop()?.split("-free")[0] || a))) {
|
|
203
|
+
return a
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const freeModels = getFreeModels(available)
|
|
208
|
+
return freeModels.length > 0 ? freeModels[0] : "opencode/big-pickle"
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function resolveFreeProfile(available) {
|
|
212
|
+
const models = {}
|
|
213
|
+
for (const agent of ALL_AGENTS) {
|
|
214
|
+
const role = FREE_ROLE_MAP[agent]
|
|
215
|
+
models[agent] = pickBestFree(available, role)
|
|
216
|
+
}
|
|
217
|
+
return models
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Syntax validation is provider-agnostic: any `provider/model` id is valid.
|
|
221
|
+
// Availability stays a separate check against `opencode models` — a model that
|
|
222
|
+
// is well-formed but absent must still fail validation.
|
|
223
|
+
export function validateModels(models, available) {
|
|
224
|
+
const unavailable = []
|
|
225
|
+
for (const [agent, model] of Object.entries(models)) {
|
|
226
|
+
if (!isValidModelId(model)) {
|
|
227
|
+
unavailable.push({ agent, model, reason: "invalid format" })
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
const matches = available.filter(a => a === model)
|
|
231
|
+
if (matches.length === 0) {
|
|
232
|
+
unavailable.push({ agent, model, reason: "not found in available models" })
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return unavailable
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Capability routing: roles that require image input (vision, visual-critic)
|
|
239
|
+
// are moved to a vision-capable fallback when the assigned model has no
|
|
240
|
+
// evidence of vision support. Never swaps when no alternative is available —
|
|
241
|
+
// the caller reports the limitation instead of pretending.
|
|
242
|
+
export function enforceVisionRoles(models, available, fallbacks = {}) {
|
|
243
|
+
const result = { ...models }
|
|
244
|
+
const applied = { ...fallbacks }
|
|
245
|
+
for (const agent of Object.keys(result)) {
|
|
246
|
+
if (!roleRequiresVision(agent)) continue
|
|
247
|
+
if (modelMeetsRole(result[agent], agent).ok) continue
|
|
248
|
+
const replacement = findAvailableCandidate(VISION_FALLBACK_CANDIDATES, available)
|
|
249
|
+
if (!replacement) continue
|
|
250
|
+
applied[agent] = { from: result[agent], to: replacement, reason: "vision capability" }
|
|
251
|
+
result[agent] = replacement
|
|
252
|
+
}
|
|
253
|
+
return { result, fallbacks: applied }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function containsKey(obj, key) {
|
|
257
|
+
return Object.prototype.hasOwnProperty.call(obj, key)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Strict family resolution used by `deepseek-mimo`: resolves a candidate chain
|
|
261
|
+
// against the live catalog and FAILS FAST with diagnostics when nothing
|
|
262
|
+
// matches. Never substitutes unrelated models — a blocked dispatch is worse
|
|
263
|
+
// than a clear exit the user can act on.
|
|
264
|
+
function resolveStrictCandidate(label, candidates, available, familyPattern, profileName) {
|
|
265
|
+
const resolved = resolveModelCandidate(candidates, available, { familyPattern })
|
|
266
|
+
if (!resolved.model) {
|
|
267
|
+
console.error(`${label} is not available in this environment.`)
|
|
268
|
+
console.error("")
|
|
269
|
+
console.error("Attempted candidates:")
|
|
270
|
+
for (const candidate of resolved.attempted) {
|
|
271
|
+
console.error(` ${candidate} (not found)`)
|
|
272
|
+
}
|
|
273
|
+
const compatible = familyPattern ? available.filter((m) => familyPattern.test(m)) : []
|
|
274
|
+
console.error("")
|
|
275
|
+
if (compatible.length > 0) {
|
|
276
|
+
console.error(`Available compatible ${label} models:`)
|
|
277
|
+
for (const m of compatible) console.error(` ${m}`)
|
|
278
|
+
} else {
|
|
279
|
+
console.error(`No compatible ${label} models are available. Run: opencode models`)
|
|
280
|
+
}
|
|
281
|
+
console.error("")
|
|
282
|
+
console.error(`The ${profileName} profile was NOT activated. No unrelated models were substituted.`)
|
|
283
|
+
console.error("Valid profiles: auto, go, go-economy, deepseek, glm, go-flash, mimo, deepseek-mimo, free")
|
|
284
|
+
process.exit(1)
|
|
285
|
+
}
|
|
286
|
+
return resolved
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function applyFallbacks(models, unavailable, available) {
|
|
290
|
+
const result = { ...models }
|
|
291
|
+
const fallbacks = {}
|
|
292
|
+
for (const { agent, model } of unavailable) {
|
|
293
|
+
if (containsKey(FREE_ROLE_MAP, agent)) {
|
|
294
|
+
const role = FREE_ROLE_MAP[agent]
|
|
295
|
+
const fb = pickBestFree(available, role)
|
|
296
|
+
result[agent] = fb
|
|
297
|
+
fallbacks[agent] = { from: model, to: fb }
|
|
298
|
+
} else {
|
|
299
|
+
const fb = pickBestFree(available, "verification")
|
|
300
|
+
result[agent] = fb
|
|
301
|
+
fallbacks[agent] = { from: model, to: fb }
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return { result, fallbacks }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function readConfig() {
|
|
308
|
+
return readJson(CONFIG_PATH, {})
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function writeConfig(config) {
|
|
312
|
+
writeJson(CONFIG_PATH, config)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function ensureAgentFiles() {
|
|
316
|
+
if (!existsSync(AGENTS_DIR)) {
|
|
317
|
+
mkdirSync(AGENTS_DIR, { recursive: true })
|
|
318
|
+
}
|
|
319
|
+
const srcFiles = readdirSync(SOURCE_AGENTS_DIR).filter(f => f.startsWith("devloom-") && f.endsWith(".md"))
|
|
320
|
+
const existing = new Set(readdirSync(AGENTS_DIR).filter(f => f.startsWith("devloom-") && f.endsWith(".md")))
|
|
321
|
+
for (const f of srcFiles) {
|
|
322
|
+
if (!existing.has(f)) {
|
|
323
|
+
try { cpSync(resolve(SOURCE_AGENTS_DIR, f), resolve(AGENTS_DIR, f)) } catch {}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export const AGENT_VARIANTS = {
|
|
329
|
+
developer: ["developer", "developer-flash", "developer-senior"],
|
|
330
|
+
planner: ["planner", "planner-flash", "planner-senior"],
|
|
331
|
+
qa: ["qa", "qa-flash"],
|
|
332
|
+
verifier: ["verifier"],
|
|
333
|
+
security: ["security", "security-senior"],
|
|
334
|
+
documenter: ["documenter", "documenter-flash"],
|
|
335
|
+
orchestrator: ["orchestrator"],
|
|
336
|
+
vision: ["vision"],
|
|
337
|
+
"visual-director": ["visual-director"],
|
|
338
|
+
"visual-critic": ["visual-critic"]
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function applyModelsToAgentFiles(models, profileLabel = "") {
|
|
342
|
+
ensureAgentFiles()
|
|
343
|
+
for (const [agent, model] of Object.entries(models)) {
|
|
344
|
+
const variants = AGENT_VARIANTS[agent] || [agent]
|
|
345
|
+
for (const v of variants) {
|
|
346
|
+
const f = resolve(AGENTS_DIR, `devloom-${v}.md`)
|
|
347
|
+
if (!existsSync(f)) continue
|
|
348
|
+
try {
|
|
349
|
+
let content = readFileSync(f, "utf8")
|
|
350
|
+
content = content.replace(/^model: .*/m, `model: ${model}`)
|
|
351
|
+
if (v === "orchestrator" && profileLabel) {
|
|
352
|
+
content = content.replace(/^(description: "DevLoom Orchestrator:)([^"]*)"/m, (_, prefix, rest) => {
|
|
353
|
+
const cleaned = rest.replace(/\s*\(profile: [^)]*\)/, "")
|
|
354
|
+
return `${prefix}${cleaned} (profile: ${profileLabel})"`
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
writeFileSync(f, content)
|
|
358
|
+
} catch {}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function cmdApplyTier(tierName, available) {
|
|
364
|
+
const tier = TIERS[tierName]
|
|
365
|
+
if (!tier) {
|
|
366
|
+
console.error("Unknown tier:", tierName)
|
|
367
|
+
console.error("Valid tiers: senior, standard")
|
|
368
|
+
process.exit(1)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const config = readConfig()
|
|
372
|
+
if (LOCKED_TIER_PROFILES.has(config.resolvedProfile)) {
|
|
373
|
+
console.error(`Cannot apply tier "${tierName}" to profile "${config.resolvedProfile}".`)
|
|
374
|
+
console.error(`Profile "${config.resolvedProfile}" locks all models to its profile definition.`)
|
|
375
|
+
console.error("Switch to 'go' or 'go-economy' profile to use tier overrides.")
|
|
376
|
+
process.exit(1)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const models = { ...tier }
|
|
380
|
+
const unresolved = validateModels(models, available)
|
|
381
|
+
if (unresolved.length > 0) {
|
|
382
|
+
console.error(`Tier "${tierName}" has unavailable models:`)
|
|
383
|
+
for (const u of unresolved) console.error(` ${u.agent}: ${u.model}`)
|
|
384
|
+
process.exit(1)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
applyModelsToAgentFiles(models)
|
|
388
|
+
|
|
389
|
+
config.tier = tierName
|
|
390
|
+
config.tierModels = models
|
|
391
|
+
config.models = { ...models }
|
|
392
|
+
writeConfig(config)
|
|
393
|
+
|
|
394
|
+
return models
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function cmdSet(profileName) {
|
|
398
|
+
const available = detectAvailableModels()
|
|
399
|
+
const hasGo = hasGoModels(available)
|
|
400
|
+
|
|
401
|
+
if ((profileName === "go" || profileName === "go-economy" || profileName === "deepseek" || profileName === "glm" || profileName === "go-flash" || profileName === "deepseek-mimo") && !hasGo) {
|
|
402
|
+
console.error("No OpenCode Go models detected. Run: opencode /connect to add OpenCode Go.")
|
|
403
|
+
console.error("Fall back to: /devloom-auto or /devloom-free")
|
|
404
|
+
process.exit(1)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
let resolvedProfile = profileName
|
|
408
|
+
let models = {}
|
|
409
|
+
let fallbacks = {}
|
|
410
|
+
let unavailable = []
|
|
411
|
+
|
|
412
|
+
if (profileName === "go") {
|
|
413
|
+
models = { ...PROFILES.go }
|
|
414
|
+
unavailable = validateModels(models, available)
|
|
415
|
+
if (unavailable.length > 0) {
|
|
416
|
+
const fb = applyFallbacks(models, unavailable, available)
|
|
417
|
+
models = fb.result
|
|
418
|
+
fallbacks = fb.fallbacks
|
|
419
|
+
}
|
|
420
|
+
} else if (profileName === "go-economy") {
|
|
421
|
+
models = { ...PROFILES["go-economy"] }
|
|
422
|
+
unavailable = validateModels(models, available)
|
|
423
|
+
if (unavailable.length > 0) {
|
|
424
|
+
const fb = applyFallbacks(models, unavailable, available)
|
|
425
|
+
models = fb.result
|
|
426
|
+
fallbacks = fb.fallbacks
|
|
427
|
+
}
|
|
428
|
+
} else if (profileName === "free") {
|
|
429
|
+
models = resolveFreeProfile(available)
|
|
430
|
+
unavailable = validateModels(models, available)
|
|
431
|
+
} else if (profileName === "deepseek") {
|
|
432
|
+
models = { ...PROFILES.deepseek }
|
|
433
|
+
unavailable = validateModels(models, available)
|
|
434
|
+
if (unavailable.length > 0) {
|
|
435
|
+
const fb = applyFallbacks(models, unavailable, available)
|
|
436
|
+
models = fb.result
|
|
437
|
+
fallbacks = fb.fallbacks
|
|
438
|
+
}
|
|
439
|
+
} else if (profileName === "glm") {
|
|
440
|
+
models = { ...PROFILES.glm }
|
|
441
|
+
unavailable = validateModels(models, available)
|
|
442
|
+
if (unavailable.length > 0) {
|
|
443
|
+
const fb = applyFallbacks(models, unavailable, available)
|
|
444
|
+
models = fb.result
|
|
445
|
+
fallbacks = fb.fallbacks
|
|
446
|
+
}
|
|
447
|
+
} else if (profileName === "go-flash") {
|
|
448
|
+
models = { ...PROFILES["go-flash"] }
|
|
449
|
+
unavailable = validateModels(models, available)
|
|
450
|
+
if (unavailable.length > 0) {
|
|
451
|
+
const fb = applyFallbacks(models, unavailable, available)
|
|
452
|
+
models = fb.result
|
|
453
|
+
fallbacks = fb.fallbacks
|
|
454
|
+
}
|
|
455
|
+
} else if (profileName === "mimo") {
|
|
456
|
+
// First-class MiMo profile: resolve the V2.6 Flash candidate chain against
|
|
457
|
+
// the live catalog instead of hardcoding a provider prefix.
|
|
458
|
+
const resolved = resolveModelCandidate(MIMO_V26_FLASH_CANDIDATES, available, {
|
|
459
|
+
familyPattern: MODEL_FAMILIES.mimoV26Flash.familyPattern,
|
|
460
|
+
})
|
|
461
|
+
if (!resolved.model) {
|
|
462
|
+
console.error("MiMo V2.6 Flash is not available in this environment.")
|
|
463
|
+
console.error("")
|
|
464
|
+
console.error("Attempted candidates:")
|
|
465
|
+
for (const candidate of resolved.attempted) {
|
|
466
|
+
console.error(` ${candidate} (not found)`)
|
|
467
|
+
}
|
|
468
|
+
const compatible = available.filter((m) => MODEL_FAMILIES.mimoV26Flash.familyPattern.test(m))
|
|
469
|
+
console.error("")
|
|
470
|
+
if (compatible.length > 0) {
|
|
471
|
+
console.error("Available compatible MiMo V2.6 models:")
|
|
472
|
+
for (const m of compatible) console.error(` ${m}`)
|
|
473
|
+
} else {
|
|
474
|
+
const anyMimo = available.filter((m) => /mimo/i.test(m))
|
|
475
|
+
if (anyMimo.length > 0) {
|
|
476
|
+
console.error("Available MiMo models (different version — not used automatically):")
|
|
477
|
+
for (const m of anyMimo) console.error(` ${m}`)
|
|
478
|
+
} else {
|
|
479
|
+
console.error("No MiMo models are available. Run: opencode models")
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
console.error("")
|
|
483
|
+
console.error("The mimo profile was NOT activated. No unrelated models were substituted.")
|
|
484
|
+
console.error("Valid profiles: auto, go, go-economy, deepseek, glm, go-flash, mimo, deepseek-mimo, free")
|
|
485
|
+
process.exit(1)
|
|
486
|
+
}
|
|
487
|
+
models = Object.fromEntries(ALL_AGENTS.map((agent) => [agent, resolved.model]))
|
|
488
|
+
if (!resolved.exact) {
|
|
489
|
+
for (const agent of ALL_AGENTS) {
|
|
490
|
+
fallbacks[agent] = {
|
|
491
|
+
from: "mimo-v2.6-flash (candidate chain)",
|
|
492
|
+
to: resolved.model,
|
|
493
|
+
reason: "family fallback",
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
} else if (profileName === "deepseek-mimo") {
|
|
498
|
+
// Hybrid: DeepSeek V4.1 Flash is the fast lane for the 7 general roles;
|
|
499
|
+
// MiMo V2.6 Flash serves the graphical/complex visual roles. Both chains
|
|
500
|
+
// resolve strictly — exit 1 with diagnostics instead of substituting.
|
|
501
|
+
const deepseekResolved = resolveStrictCandidate(
|
|
502
|
+
"DeepSeek V4.1 Flash",
|
|
503
|
+
DEEPSEEK_V41_FLASH_CANDIDATES,
|
|
504
|
+
available,
|
|
505
|
+
MODEL_FAMILIES.deepseekV41Flash.familyPattern,
|
|
506
|
+
"deepseek-mimo"
|
|
507
|
+
)
|
|
508
|
+
const mimoResolved = resolveStrictCandidate(
|
|
509
|
+
"MiMo V2.6 Flash",
|
|
510
|
+
MIMO_V26_FLASH_CANDIDATES,
|
|
511
|
+
available,
|
|
512
|
+
MODEL_FAMILIES.mimoV26Flash.familyPattern,
|
|
513
|
+
"deepseek-mimo"
|
|
514
|
+
)
|
|
515
|
+
const visualRoles = new Set(["vision", "visual-director", "visual-critic"])
|
|
516
|
+
models = Object.fromEntries(
|
|
517
|
+
ALL_AGENTS.map((agent) => [agent, visualRoles.has(agent) ? mimoResolved.model : deepseekResolved.model])
|
|
518
|
+
)
|
|
519
|
+
const recordChainFallback = (agent, resolved, chainLabel) => {
|
|
520
|
+
if (resolved.exact) return
|
|
521
|
+
fallbacks[agent] = {
|
|
522
|
+
from: `${chainLabel} (candidate chain)`,
|
|
523
|
+
to: resolved.model,
|
|
524
|
+
reason: "family fallback",
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
for (const agent of ALL_AGENTS) {
|
|
528
|
+
recordChainFallback(agent, visualRoles.has(agent) ? mimoResolved : deepseekResolved, visualRoles.has(agent) ? "mimo-v2.6-flash" : "deepseek-v4.1-flash")
|
|
529
|
+
}
|
|
530
|
+
} else if (profileName === "auto") {
|
|
531
|
+
if (hasGo) {
|
|
532
|
+
models = { ...PROFILES["go-flash"] }
|
|
533
|
+
resolvedProfile = "go-flash"
|
|
534
|
+
} else {
|
|
535
|
+
models = resolveFreeProfile(available)
|
|
536
|
+
resolvedProfile = "free"
|
|
537
|
+
}
|
|
538
|
+
unavailable = validateModels(models, available)
|
|
539
|
+
if (unavailable.length > 0) {
|
|
540
|
+
const fb = applyFallbacks(models, unavailable, available)
|
|
541
|
+
models = fb.result
|
|
542
|
+
fallbacks = fb.fallbacks
|
|
543
|
+
}
|
|
544
|
+
} else {
|
|
545
|
+
console.error("Unknown profile:", profileName)
|
|
546
|
+
console.error("Valid profiles: auto, go, go-economy, deepseek, glm, go-flash, mimo, deepseek-mimo, free")
|
|
547
|
+
process.exit(1)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Capability routing: keep vision-required roles on vision-capable models.
|
|
551
|
+
const enforced = enforceVisionRoles(models, available, fallbacks)
|
|
552
|
+
models = enforced.result
|
|
553
|
+
fallbacks = enforced.fallbacks
|
|
554
|
+
|
|
555
|
+
const prev = readConfig()
|
|
556
|
+
const config = {
|
|
557
|
+
profile: profileName,
|
|
558
|
+
resolvedProfile,
|
|
559
|
+
models,
|
|
560
|
+
overrides: prev.overrides || {},
|
|
561
|
+
tier: LOCKED_TIER_PROFILES.has(resolvedProfile) ? null : (prev.tier || null),
|
|
562
|
+
tierModels: LOCKED_TIER_PROFILES.has(resolvedProfile) ? null : (prev.tierModels || null),
|
|
563
|
+
availableModelsSnapshot: available,
|
|
564
|
+
fallbacks
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
writeConfig(config)
|
|
568
|
+
applyModelsToAgentFiles(models, config.resolvedProfile ?? config.profile)
|
|
569
|
+
|
|
570
|
+
const cacheStatus = pluginCacheStatus()
|
|
571
|
+
if (cacheStatus === "ok") {
|
|
572
|
+
refreshOpenCodePluginAgentFiles(AGENTS_DIR)
|
|
573
|
+
console.log("OpenCode plugin cache refreshed with the current profile.")
|
|
574
|
+
} else {
|
|
575
|
+
console.log(`Note: DevLoom plugin cache is ${cacheStatus === "missing" ? "missing" : "stale"} — run /devloom-refresh once, then restart opencode to see the profile in the sidebar.`)
|
|
576
|
+
}
|
|
577
|
+
console.log("Restart opencode (or run: opencode --continue) to see the updated profile and agents in the sidebar.")
|
|
578
|
+
|
|
579
|
+
return config
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export function cmdCurrent() {
|
|
583
|
+
const config = readConfig()
|
|
584
|
+
if (!config || !config.profile) {
|
|
585
|
+
console.log("No DevLoom profile configured yet.")
|
|
586
|
+
console.log("Run: /devloom-init or /devloom-auto")
|
|
587
|
+
return
|
|
588
|
+
}
|
|
589
|
+
console.log(`Profile: ${config.profile}`)
|
|
590
|
+
if (config.tier) {
|
|
591
|
+
console.log(`Tier: ${config.tier}`)
|
|
592
|
+
} else if (LOCKED_TIER_PROFILES.has(config.resolvedProfile)) {
|
|
593
|
+
console.log(`Tier: locked (profile "${config.resolvedProfile}" does not support tier overrides)`)
|
|
594
|
+
}
|
|
595
|
+
console.log("")
|
|
596
|
+
const activeModels = config.tierModels || config.models
|
|
597
|
+
console.log("Resolved models:")
|
|
598
|
+
for (const [agent, model] of Object.entries(activeModels)) {
|
|
599
|
+
const tiered = config.tier && config.models[agent] !== model ? ` (tier: ${model})` : ""
|
|
600
|
+
const overridden = config.overrides && config.overrides[agent] ? " (override)" : ""
|
|
601
|
+
const fallbacked = config.fallbacks && config.fallbacks[agent] ? " (fallback)" : ""
|
|
602
|
+
const base = config.tier ? config.models[agent] : null
|
|
603
|
+
const label = base && base !== model ? `${base} → ${model}` : model
|
|
604
|
+
console.log(` ${agent}: ${label}${tiered}${overridden}${fallbacked}`)
|
|
605
|
+
}
|
|
606
|
+
if (config.unresolved && config.unresolved.length > 0) {
|
|
607
|
+
console.log("")
|
|
608
|
+
console.log("Unavailable models:")
|
|
609
|
+
for (const u of config.unresolved) {
|
|
610
|
+
console.log(` ${u.agent}: ${u.model} (${u.reason})`)
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (config.fallbacks && Object.keys(config.fallbacks).length > 0) {
|
|
614
|
+
console.log("")
|
|
615
|
+
console.log("Fallbacks applied:")
|
|
616
|
+
for (const [agent, fb] of Object.entries(config.fallbacks)) {
|
|
617
|
+
console.log(` ${agent}: ${fb.from} -> ${fb.to}`)
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export function cmdValidate() {
|
|
623
|
+
const available = detectAvailableModels()
|
|
624
|
+
const config = readConfig()
|
|
625
|
+
if (!config || !config.models) {
|
|
626
|
+
console.log("No DevLoom configuration to validate.")
|
|
627
|
+
process.exit(1)
|
|
628
|
+
}
|
|
629
|
+
const allUnavailable = []
|
|
630
|
+
for (const [agent, model] of Object.entries(config.models)) {
|
|
631
|
+
if (!available.includes(model)) {
|
|
632
|
+
allUnavailable.push({ agent, model, reason: "not available" })
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (allUnavailable.length > 0) {
|
|
636
|
+
console.log("Unavailable models:")
|
|
637
|
+
for (const u of allUnavailable) {
|
|
638
|
+
console.log(` ${u.agent}: ${u.model}`)
|
|
639
|
+
}
|
|
640
|
+
process.exit(1)
|
|
641
|
+
} else {
|
|
642
|
+
console.log("All models are available.")
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export function cmdApply() {
|
|
647
|
+
const config = readConfig()
|
|
648
|
+
if (!config || !config.profile) {
|
|
649
|
+
console.error("No profile configured. Run /devloom-init or /devloom-auto first.")
|
|
650
|
+
process.exit(1)
|
|
651
|
+
}
|
|
652
|
+
const available = detectAvailableModels()
|
|
653
|
+
const models = config.models || {}
|
|
654
|
+
applyModelsToAgentFiles(models, config.resolvedProfile ?? config.profile)
|
|
655
|
+
|
|
656
|
+
config.availableModelsSnapshot = available
|
|
657
|
+
config.requiresReload = true
|
|
658
|
+
writeConfig(config)
|
|
659
|
+
|
|
660
|
+
console.log("Profile applied to agent files.")
|
|
661
|
+
console.log("")
|
|
662
|
+
|
|
663
|
+
const cacheStatus = pluginCacheStatus()
|
|
664
|
+
if (cacheStatus === "ok") {
|
|
665
|
+
refreshOpenCodePluginAgentFiles(AGENTS_DIR)
|
|
666
|
+
console.log("OpenCode plugin cache refreshed with the current profile.")
|
|
667
|
+
} else {
|
|
668
|
+
console.log(`NOTE: the DevLoom plugin cache is ${cacheStatus === "missing" ? "missing (plugin not installed in OpenCode)" : "stale"} —`)
|
|
669
|
+
console.log("the profile will not show in the sidebar until the plugin code is refreshed.")
|
|
670
|
+
console.log("Run: /devloom-refresh (or: npm install -g devloom && opencode plugin devloom --global --force)")
|
|
671
|
+
}
|
|
672
|
+
console.log("")
|
|
673
|
+
console.log("Restart opencode (or run: opencode --continue) to see the updated profile and agents in the sidebar.")
|
|
674
|
+
console.log("")
|
|
675
|
+
|
|
676
|
+
if (existsSync(AGENTS_DIR)) {
|
|
677
|
+
const reopenScript = ".opencode/devloom/reopen.sh"
|
|
678
|
+
writeFileSync(reopenScript, "#!/usr/bin/env bash\nexec opencode --continue\n")
|
|
679
|
+
try { execSync(`chmod +x ${reopenScript}`) } catch {}
|
|
680
|
+
|
|
681
|
+
console.log("If the current OpenCode session does not pick up the new agent models immediately,")
|
|
682
|
+
console.log("continue the same session with:")
|
|
683
|
+
console.log("")
|
|
684
|
+
console.log(" opencode --continue")
|
|
685
|
+
console.log("")
|
|
686
|
+
console.log("Or run:")
|
|
687
|
+
console.log(` bash ${reopenScript}`)
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export function cmdDetect() {
|
|
692
|
+
const available = detectAvailableModels()
|
|
693
|
+
const hasGo = hasGoModels(available)
|
|
694
|
+
|
|
695
|
+
console.log("Available models:", available.length)
|
|
696
|
+
if (hasGo) {
|
|
697
|
+
console.log("OpenCode Go models detected: yes")
|
|
698
|
+
console.log("Recommended profile: go")
|
|
699
|
+
} else {
|
|
700
|
+
console.log("OpenCode Go models detected: no")
|
|
701
|
+
console.log("Recommended profile: free")
|
|
702
|
+
}
|
|
703
|
+
const freeCount = getFreeModels(available).length
|
|
704
|
+
console.log(`Free models available: ${freeCount}`)
|
|
705
|
+
const mimo = findAvailableCandidate(MIMO_V26_FLASH_CANDIDATES, available)
|
|
706
|
+
console.log(mimo ? `MiMo V2.6 Flash: available (${mimo})` : "MiMo V2.6 Flash: not available")
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function main() {
|
|
710
|
+
const args = process.argv.slice(2)
|
|
711
|
+
const command = args[0] || ""
|
|
712
|
+
|
|
713
|
+
switch (command) {
|
|
714
|
+
case "set":
|
|
715
|
+
if (!args[1]) { console.error("Usage: devloom profile set <auto|go|go-economy|deepseek|glm|go-flash|mimo|deepseek-mimo|free>"); process.exit(1) }
|
|
716
|
+
cmdSet(args[1])
|
|
717
|
+
console.log(`DevLoom profile changed to: ${args[1]}`)
|
|
718
|
+
if (args[1] !== "free") {
|
|
719
|
+
console.log(`Resolved profile: ${args[1]}`)
|
|
720
|
+
}
|
|
721
|
+
break
|
|
722
|
+
case "current":
|
|
723
|
+
cmdCurrent()
|
|
724
|
+
break
|
|
725
|
+
case "validate":
|
|
726
|
+
cmdValidate()
|
|
727
|
+
break
|
|
728
|
+
case "apply":
|
|
729
|
+
cmdApply()
|
|
730
|
+
break
|
|
731
|
+
case "detect":
|
|
732
|
+
cmdDetect()
|
|
733
|
+
break
|
|
734
|
+
case "tier":
|
|
735
|
+
if (!args[1]) { console.error("Usage: devloom profile tier <senior|balanced|junior>"); process.exit(1) }
|
|
736
|
+
if (!["senior","standard"].includes(args[1])) { console.error("Invalid tier. Valid: senior, standard"); process.exit(1) }
|
|
737
|
+
const available = detectAvailableModels()
|
|
738
|
+
cmdApplyTier(args[1], available)
|
|
739
|
+
console.log(`DevLoom tier changed to: ${args[1]}`)
|
|
740
|
+
break
|
|
741
|
+
default:
|
|
742
|
+
console.log("DevLoom Profile Manager")
|
|
743
|
+
console.log("")
|
|
744
|
+
console.log("Commands:")
|
|
745
|
+
console.log(" devloom profile set <auto|go|go-economy|deepseek|glm|go-flash|mimo|deepseek-mimo|free>")
|
|
746
|
+
console.log(" devloom profile tier <senior|standard>")
|
|
747
|
+
console.log(" devloom profile current")
|
|
748
|
+
console.log(" devloom profile validate")
|
|
749
|
+
console.log(" devloom profile apply")
|
|
750
|
+
console.log(" devloom profile detect")
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const isMain = process.argv[1] ? fileURLToPath(import.meta.url) === resolve(process.argv[1]) : false
|
|
755
|
+
if (isMain) main()
|