@polderlabs/bizar 10.28.1 → 10.29.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/cli/bin.mjs +54 -0
- package/cli/commands/advisor.mjs +274 -0
- package/cli/commands/openkan.mjs +71 -0
- package/cli/commands/orchestrator.mjs +546 -0
- package/cli/commands/statusline.mjs +625 -0
- package/cli/commands/task.mjs +1 -1
- package/cli/openkan-store.mjs +18 -3
- package/config/claude/commands/advisor.md +10 -0
- package/config/claude/commands/statusline.md +19 -0
- package/config/skills/autopilot/SKILL.md +2 -2
- package/config/skills/bizplan/SKILL.md +1 -1
- package/config/skills/ralph/SKILL.md +2 -2
- package/config/skills/ultragoal/SKILL.md +3 -3
- package/config/skills/ultraqa/SKILL.md +2 -2
- package/config/skills/ultrawork/SKILL.md +2 -2
- package/package.json +1 -1
- package/packages/sdk/dist/handoff/bizplan.js +16 -1
- package/packages/sdk/dist/version.d.ts +1 -1
- package/packages/sdk/dist/version.js +1 -1
- package/packages/sdk/package.json +1 -1
package/cli/bin.mjs
CHANGED
|
@@ -113,6 +113,9 @@ function showHelp() {
|
|
|
113
113
|
restore Restore BizarHarness from a backup
|
|
114
114
|
validate Validate the Bizar install
|
|
115
115
|
setup-provider Configure the global provider used by Bizar and Claude Code
|
|
116
|
+
advisor Configure Claude Code's advisor tool
|
|
117
|
+
orchestrator Manage multi-select modelPicker entries for Claude Code
|
|
118
|
+
statusline Customized Claude Code status bar (render/install/remove/show/preview)
|
|
116
119
|
release-provenance Generate SBOM + provenance + minisig for a release (audit #83)
|
|
117
120
|
verify-release Verify a release artifact set against the pinned allowlist
|
|
118
121
|
spec-list List SDK schemas, policy docs, and mirror sync status (audit #84)
|
|
@@ -297,6 +300,57 @@ async function main() {
|
|
|
297
300
|
break;
|
|
298
301
|
}
|
|
299
302
|
|
|
303
|
+
case 'advisor': {
|
|
304
|
+
const mod = await importCommand('advisor');
|
|
305
|
+
if (!mod) {
|
|
306
|
+
console.error(chalk.red(` ✗ Could not load advisor command module`));
|
|
307
|
+
process.exit(EXIT_ERROR);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
dbg('loaded command module:', 'advisor');
|
|
311
|
+
const found = await mod.run(cmd, cmdArgs, isHelpRequest);
|
|
312
|
+
if (!found) {
|
|
313
|
+
console.error(chalk.red(` ✗ Unknown command: ${cmd}`));
|
|
314
|
+
showHelp();
|
|
315
|
+
process.exit(EXIT_ERROR);
|
|
316
|
+
}
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
case 'orchestrator': {
|
|
321
|
+
const mod = await importCommand('orchestrator');
|
|
322
|
+
if (!mod) {
|
|
323
|
+
console.error(chalk.red(` ✗ Could not load orchestrator command module`));
|
|
324
|
+
process.exit(EXIT_ERROR);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
dbg('loaded command module:', 'orchestrator');
|
|
328
|
+
const found = await mod.run(cmd, cmdArgs, isHelpRequest);
|
|
329
|
+
if (!found) {
|
|
330
|
+
console.error(chalk.red(` ✗ Unknown command: ${cmd}`));
|
|
331
|
+
showHelp();
|
|
332
|
+
process.exit(EXIT_ERROR);
|
|
333
|
+
}
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
case 'statusline': {
|
|
338
|
+
const mod = await importCommand('statusline');
|
|
339
|
+
if (!mod) {
|
|
340
|
+
console.error(chalk.red(` ✗ Could not load statusline command module`));
|
|
341
|
+
process.exit(EXIT_ERROR);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
dbg('loaded command module:', 'statusline');
|
|
345
|
+
const found = await mod.run(cmd, cmdArgs, isHelpRequest);
|
|
346
|
+
if (!found) {
|
|
347
|
+
console.error(chalk.red(` ✗ Unknown command: ${cmd}`));
|
|
348
|
+
showHelp();
|
|
349
|
+
process.exit(EXIT_ERROR);
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
|
|
300
354
|
case 'team':
|
|
301
355
|
case 'subagent':
|
|
302
356
|
case 'run': {
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export const ADVISOR_PAIRINGS = {
|
|
7
|
+
'haiku-4-5': ['fable', 'opus-4-5', 'sonnet-4-5'],
|
|
8
|
+
'sonnet-4-6': ['fable', 'opus-4-5', 'sonnet-4-5'],
|
|
9
|
+
'sonnet-5': ['fable', 'opus-4-5', 'sonnet-5'],
|
|
10
|
+
'opus-4-6': ['fable', 'opus-4-5', 'sonnet-5'],
|
|
11
|
+
'opus-4-7+': ['fable', 'opus-4-7+'],
|
|
12
|
+
'fable-5': ['fable-5'],
|
|
13
|
+
'fable-5-1': ['fable-5-1'],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function modelMatches(modelId, pattern) {
|
|
17
|
+
const id = String(modelId ?? '').toLowerCase();
|
|
18
|
+
const hasPlus = pattern.endsWith('+');
|
|
19
|
+
const prefix = pattern.toLowerCase().replace(/\+$/, '');
|
|
20
|
+
return id === prefix || id.startsWith(`${prefix}-`) || id.includes(`-${prefix}-`) || (hasPlus && (id.includes(prefix) || id.includes(`-${prefix}`))) || (hasPlus && prefix === 'opus-4-7' && /opus-4-[7-9]/.test(id));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function mainPairingKey(modelId) {
|
|
24
|
+
const id = String(modelId ?? '').toLowerCase();
|
|
25
|
+
if (/opus-(?:[5-9]|4-(?:[7-9]|[1-9][0-9]+))/.test(id)) return 'opus-4-7+';
|
|
26
|
+
return Object.keys(ADVISOR_PAIRINGS)
|
|
27
|
+
.sort((a, b) => b.length - a.length)
|
|
28
|
+
.find((key) => modelMatches(id, key));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function validateAdvisorPairing(mainId, advisorId) {
|
|
32
|
+
const mainKey = mainPairingKey(mainId);
|
|
33
|
+
if (!mainKey) return { verdict: 'unknown-main', accepted: [] };
|
|
34
|
+
const accepted = ADVISOR_PAIRINGS[mainKey];
|
|
35
|
+
const verdict = accepted.some((pattern) => modelMatches(advisorId, pattern)) ? 'ok' : 'rejected';
|
|
36
|
+
return { verdict, accepted };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function settingsPath() {
|
|
40
|
+
const root = process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), '.claude');
|
|
41
|
+
return join(root, 'settings.json');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function readSettings(path = settingsPath()) {
|
|
45
|
+
if (!existsSync(path)) return {};
|
|
46
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resolveMainModel(settings) {
|
|
50
|
+
const env = settings?.env ?? {};
|
|
51
|
+
return env.ANTHROPIC_MODEL || env.ANTHROPIC_DEFAULT_OPUS_MODEL || env.ANTHROPIC_DEFAULT_SONNET_MODEL || env.ANTHROPIC_DEFAULT_HAIKU_MODEL || env.ANTHROPIC_DEFAULT_MODEL;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function updateAdvisorSettings(current, options) {
|
|
55
|
+
const next = { ...current, env: { ...(current?.env ?? {}) } };
|
|
56
|
+
if (options.action === 'pick') next.advisorModel = options.model;
|
|
57
|
+
if (options.action === 'disable') next.env.CLAUDE_CODE_DISABLE_ADVISOR_TOOL = '1';
|
|
58
|
+
if (options.action === 'clear') {
|
|
59
|
+
delete next.advisorModel;
|
|
60
|
+
delete next.env.CLAUDE_CODE_DISABLE_ADVISOR_TOOL;
|
|
61
|
+
}
|
|
62
|
+
return next;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseAdvisorArgs(args = []) {
|
|
66
|
+
const nonFlags = args.filter((arg) => !arg.startsWith('-'));
|
|
67
|
+
return {
|
|
68
|
+
subcommand: nonFlags[0] || 'pick',
|
|
69
|
+
help: args.includes('--help') || args.includes('-h'),
|
|
70
|
+
list: args.includes('--list'),
|
|
71
|
+
json: args.includes('--json'),
|
|
72
|
+
yes: args.includes('--yes'),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function fetchAdvisorEndpointModels(url, authToken, apiKey) {
|
|
77
|
+
const endpoint = `${url.replace(/\/$/, '')}/models`;
|
|
78
|
+
const headers = { Accept: 'application/json' };
|
|
79
|
+
if (apiKey) headers['x-api-key'] = apiKey;
|
|
80
|
+
else if (authToken) headers.Authorization = `Bearer ${authToken}`;
|
|
81
|
+
const response = await fetch(endpoint, { headers });
|
|
82
|
+
if (!response.ok) throw new Error(`Models endpoint returned ${response.status} ${response.statusText}`);
|
|
83
|
+
const body = await response.json();
|
|
84
|
+
return Array.isArray(body) ? body : (body.data ?? []);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function providerName(url) {
|
|
88
|
+
const lower = url.toLowerCase();
|
|
89
|
+
if (lower.includes('bedrock') || lower.includes('amazonaws.com')) return 'AWS Bedrock';
|
|
90
|
+
if (lower.includes('vertex') || lower.includes('googleapis.com')) return 'GCP Vertex';
|
|
91
|
+
if (lower.includes('foundry') || lower.includes('azure.com')) return 'Microsoft Foundry';
|
|
92
|
+
return url;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isUnsupportedProvider(url) {
|
|
96
|
+
const lower = url.toLowerCase();
|
|
97
|
+
return lower.includes('bedrock') || lower.includes('vertex') || lower.includes('foundry') || lower.includes('amazonaws.com') || lower.includes('googleapis.com') || lower.includes('azure.com');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function isAgentContext() {
|
|
101
|
+
return process.env.BIZAR_AGENT !== undefined || process.env.CLAUDE_CODE_AGENT_NAME !== undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function fableWarning(model) {
|
|
105
|
+
return model?.toLowerCase().includes('fable')
|
|
106
|
+
? '⚠ Fable advisors require one-time consent to bill usage credits. Run: claude (then type /advisor off and /advisor <fable> to consent once).'
|
|
107
|
+
: null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function writeSettings(path, settings) {
|
|
111
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
112
|
+
writeFileSync(path, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function eligibleModels(models, mainModel) {
|
|
116
|
+
const key = mainPairingKey(mainModel);
|
|
117
|
+
if (!key) return { models, notice: '⚠ could not determine main model — showing all options' };
|
|
118
|
+
const accepted = ADVISOR_PAIRINGS[key];
|
|
119
|
+
const filtered = models.filter((model) => accepted.some((pattern) => modelMatches(model.id, pattern)));
|
|
120
|
+
if (filtered.length) return { models: filtered, notice: '' };
|
|
121
|
+
const opus = models.filter((model) => /opus/i.test(model.id ?? ''));
|
|
122
|
+
return { models: opus, notice: '⚠ no compatible models returned — showing all Opus-class options' };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function searchable(model, query) {
|
|
126
|
+
const needle = query.toLowerCase();
|
|
127
|
+
return [model.id, model.name, model.display_name, model.description].some((value) => String(value ?? '').toLowerCase().includes(needle));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function readInput() {
|
|
131
|
+
return new Promise((resolve) => process.stdin.once('data', (chunk) => resolve(chunk.toString())));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function pickAdvisorModel(models, options) {
|
|
135
|
+
const eligible = eligibleModels(models, options.mainModel);
|
|
136
|
+
const sorted = [...eligible.models].sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
137
|
+
if (options.list) return { ok: true, models: sorted, notice: eligible.notice };
|
|
138
|
+
if (options.json || !process.stdin.isTTY || !process.stdout.isTTY) return { ok: true, models: sorted, notice: eligible.notice };
|
|
139
|
+
|
|
140
|
+
let query = '';
|
|
141
|
+
let index = 0;
|
|
142
|
+
let offset = 0;
|
|
143
|
+
const visible = 12;
|
|
144
|
+
const wasRaw = process.stdin.isRaw;
|
|
145
|
+
const redraw = () => {
|
|
146
|
+
const filtered = sorted.filter((model) => !query || searchable(model, query));
|
|
147
|
+
index = Math.min(index, Math.max(0, filtered.length - 1));
|
|
148
|
+
offset = Math.min(offset, Math.max(0, filtered.length - visible));
|
|
149
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
150
|
+
process.stdout.write(chalk.bold(' Select Claude Code advisor model\n\n'));
|
|
151
|
+
if (eligible.notice) process.stdout.write(chalk.yellow(` ${eligible.notice}\n\n`));
|
|
152
|
+
process.stdout.write(` Search: ${query || chalk.dim('(type to filter)')}\n\n`);
|
|
153
|
+
for (let row = 0; row < Math.min(visible, filtered.length); row += 1) {
|
|
154
|
+
const model = filtered[offset + row];
|
|
155
|
+
process.stdout.write(`${offset + row === index ? chalk.cyan(' > ') : ' '}${model.id}${model.display_name ? chalk.dim(` — ${model.display_name}`) : ''}\n`);
|
|
156
|
+
}
|
|
157
|
+
if (!filtered.length) process.stdout.write(chalk.yellow(' No matching models\n'));
|
|
158
|
+
process.stdout.write(chalk.dim('\n ↑/↓ navigate enter select backspace edit esc cancel\n'));
|
|
159
|
+
};
|
|
160
|
+
const restore = () => {
|
|
161
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(Boolean(wasRaw));
|
|
162
|
+
process.stdin.pause();
|
|
163
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
164
|
+
};
|
|
165
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
166
|
+
process.stdin.resume();
|
|
167
|
+
try {
|
|
168
|
+
redraw();
|
|
169
|
+
while (true) {
|
|
170
|
+
const input = await readInput();
|
|
171
|
+
if (input === '' || input === '' || input === '') return { ok: false, error: 'cancelled' };
|
|
172
|
+
if (input === '\r' || input === '\n') {
|
|
173
|
+
const filtered = sorted.filter((model) => !query || searchable(model, query));
|
|
174
|
+
if (filtered[index]) return { ok: true, model: filtered[index].id, notice: eligible.notice };
|
|
175
|
+
} else if (input === '') query = query.slice(0, -1);
|
|
176
|
+
else if (input === '[A') index = Math.max(0, index - 1);
|
|
177
|
+
else if (input === '[B') index += 1;
|
|
178
|
+
else if (input.length === 1 && input >= ' ') query += input;
|
|
179
|
+
const filtered = sorted.filter((model) => !query || searchable(model, query));
|
|
180
|
+
index = Math.min(index, Math.max(0, filtered.length - 1));
|
|
181
|
+
if (index >= offset + visible) offset = index - visible + 1;
|
|
182
|
+
if (index < offset) offset = index;
|
|
183
|
+
redraw();
|
|
184
|
+
}
|
|
185
|
+
} finally {
|
|
186
|
+
restore();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export { pickAdvisorModel };
|
|
191
|
+
|
|
192
|
+
export function showAdvisorHelp() {
|
|
193
|
+
process.stdout.write(`\n bizar advisor — Configure Claude Code's advisor tool\n\n Usage:\n bizar advisor [pick|show|clear|validate|disable] [--list] [--json] [--yes]\n\n Subcommands:\n pick Search and select an advisor model (default)\n show Show the configured advisor and disabled state\n clear Remove advisorModel and the disable flag\n validate Validate the current main/advisor pairing\n disable Set CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1\n\n Options:\n --list Dump eligible model IDs without the interactive picker\n --json Emit machine-readable output\n --yes Skip confirmation prompts\n\n Settings: ~/.claude/settings.json or $CLAUDE_CONFIG_DIR/settings.json\n`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function showAdvisor(settings, json) {
|
|
197
|
+
const state = {
|
|
198
|
+
advisorModel: settings.advisorModel ?? null,
|
|
199
|
+
disabled: settings.env?.CLAUDE_CODE_DISABLE_ADVISOR_TOOL === '1',
|
|
200
|
+
};
|
|
201
|
+
if (json) process.stdout.write(`${JSON.stringify(state)}\n`);
|
|
202
|
+
else if (state.disabled && state.advisorModel) process.stdout.write(chalk.yellow(` Advisor disabled; disabled state wins over ${state.advisorModel}. Run bizar advisor clear to reset.\n`));
|
|
203
|
+
else if (state.disabled) process.stdout.write(chalk.yellow(' Advisor disabled. Run bizar advisor clear to re-enable.\n'));
|
|
204
|
+
else if (state.advisorModel) process.stdout.write(` Advisor: ${state.advisorModel}\n`);
|
|
205
|
+
else process.stdout.write(' No advisor configured. Run bizar advisor pick.\n');
|
|
206
|
+
if (state.advisorModel && fableWarning(state.advisorModel) && !json) process.stdout.write(chalk.yellow(` ${fableWarning(state.advisorModel)}\n`));
|
|
207
|
+
return state;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function runAdvisor(args = []) {
|
|
211
|
+
const options = parseAdvisorArgs(args);
|
|
212
|
+
if (options.help) { showAdvisorHelp(); return { ok: true }; }
|
|
213
|
+
if (isAgentContext()) {
|
|
214
|
+
process.stderr.write(chalk.red('✗ bizar advisor is user-only and cannot run in agent context.\n'));
|
|
215
|
+
return { ok: false, error: 'agent-context' };
|
|
216
|
+
}
|
|
217
|
+
const path = settingsPath();
|
|
218
|
+
let settings;
|
|
219
|
+
try { settings = readSettings(path); } catch (error) {
|
|
220
|
+
process.stderr.write(chalk.red(`✗ Invalid JSON in ${path}: ${error.message}\n`));
|
|
221
|
+
return { ok: false, error: 'invalid-settings' };
|
|
222
|
+
}
|
|
223
|
+
const baseUrl = settings.env?.ANTHROPIC_BASE_URL || 'https://api.anthropic.com';
|
|
224
|
+
if (isUnsupportedProvider(baseUrl)) {
|
|
225
|
+
process.stderr.write(chalk.red(`✗ Advisor tool requires the Anthropic API. Detected provider: ${providerName(baseUrl)} (${baseUrl}). Disable this command on Bedrock/Vertex/Foundry.\n`));
|
|
226
|
+
return { ok: false, error: 'unsupported-provider' };
|
|
227
|
+
}
|
|
228
|
+
const subcommand = options.subcommand;
|
|
229
|
+
if (subcommand === 'show') return { ok: true, state: showAdvisor(settings, options.json) };
|
|
230
|
+
if (subcommand === 'clear' || subcommand === 'disable') {
|
|
231
|
+
const next = updateAdvisorSettings(settings, { action: subcommand });
|
|
232
|
+
writeSettings(path, next);
|
|
233
|
+
process.stdout.write(chalk.green(`✓ Advisor ${subcommand === 'clear' ? 'configuration cleared' : 'disabled'} in ${path}\n`));
|
|
234
|
+
return { ok: true, settings: next };
|
|
235
|
+
}
|
|
236
|
+
if (subcommand === 'validate') {
|
|
237
|
+
const main = resolveMainModel(settings);
|
|
238
|
+
const advisor = settings.advisorModel;
|
|
239
|
+
const result = main && advisor ? validateAdvisorPairing(main, advisor) : { verdict: 'missing-model', accepted: [] };
|
|
240
|
+
if (options.json) process.stdout.write(`${JSON.stringify({ main, advisor, ...result })}\n`);
|
|
241
|
+
else process.stdout.write(` Main: ${main ?? '(unknown)'}\n Advisor: ${advisor ?? '(unset)'}\n Verdict: ${result.verdict}\n`);
|
|
242
|
+
return { ok: result.verdict === 'ok' || result.verdict === 'unknown-main', result };
|
|
243
|
+
}
|
|
244
|
+
const models = await fetchAdvisorEndpointModels(baseUrl, settings.env?.ANTHROPIC_AUTH_TOKEN, settings.env?.ANTHROPIC_API_KEY).catch((error) => {
|
|
245
|
+
process.stderr.write(chalk.red(`✗ Could not fetch advisor models: ${error.message}\n`));
|
|
246
|
+
return null;
|
|
247
|
+
});
|
|
248
|
+
if (!models) return { ok: false, error: 'models-fetch' };
|
|
249
|
+
const picked = await pickAdvisorModel(models, { ...options, mainModel: resolveMainModel(settings) });
|
|
250
|
+
if (!picked.ok) return picked;
|
|
251
|
+
if (options.list || options.json || picked.model === undefined) {
|
|
252
|
+
const output = options.json ? { models: picked.models.map((model) => model.id), notice: picked.notice } : picked.models.map((model) => model.id);
|
|
253
|
+
process.stdout.write(`${JSON.stringify(output, null, options.json ? 2 : 0)}\n`);
|
|
254
|
+
return { ok: true, models: picked.models, notice: picked.notice };
|
|
255
|
+
}
|
|
256
|
+
if (!options.yes) {
|
|
257
|
+
process.stdout.write(` Selected ${picked.model}. Write to settings? [y/N] `);
|
|
258
|
+
const answer = await readInput();
|
|
259
|
+
if (!/^y(?:es)?$/i.test(answer.trim())) return { ok: false, error: 'cancelled' };
|
|
260
|
+
}
|
|
261
|
+
const next = updateAdvisorSettings(settings, { action: 'pick', model: picked.model });
|
|
262
|
+
writeSettings(path, next);
|
|
263
|
+
process.stdout.write(chalk.green(`✓ Advisor set to ${picked.model}\n`));
|
|
264
|
+
const warning = fableWarning(picked.model);
|
|
265
|
+
if (warning) process.stdout.write(chalk.yellow(`${warning}\n`));
|
|
266
|
+
return { ok: true, model: picked.model, settings: next };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export async function run(name, args, isHelpRequest) {
|
|
270
|
+
if (name !== 'advisor') return false;
|
|
271
|
+
if (isHelpRequest) showAdvisorHelp();
|
|
272
|
+
else await runAdvisor(args);
|
|
273
|
+
return true;
|
|
274
|
+
}
|
package/cli/commands/openkan.mjs
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { dirname, join, resolve as resolvePath } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
2
4
|
import { ensureOpenKanProject, installOpenKanPromise, OpenKanError, resolveOpenKanDashboard, runOpenKanOk } from '../openkan.mjs';
|
|
3
5
|
|
|
6
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const MIGRATE_TASKS = join(HERE, '..', '..', 'scripts', 'openkan', 'migrate-tasks-to-v2.mts');
|
|
8
|
+
const MIGRATE_BOARD = join(HERE, '..', '..', 'scripts', 'openkan', 'migrate-board-to-v2.mts');
|
|
9
|
+
|
|
4
10
|
function print(result) {
|
|
5
11
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
6
12
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
@@ -18,6 +24,10 @@ Usage:
|
|
|
18
24
|
ok plan <add|list|show|update> Manage plans and phases
|
|
19
25
|
ok prd <add|list|show|update> Manage PRDs, goals, and milestones
|
|
20
26
|
ok doctor Validate the .ok/ workspace
|
|
27
|
+
bizar openkan project clean [--apply|--all|--dry-run] Clean project workspace
|
|
28
|
+
bizar openkan board delete <id> Delete a board
|
|
29
|
+
bizar openkan migrate [--apply] [--tasks|--board] Migrate v1 tasks/board to v2 layout
|
|
30
|
+
(default: dry-run; --apply to apply)
|
|
21
31
|
bizar openkan dashboard [args...] Forward to the OpenKan dashboard CLI
|
|
22
32
|
(legacy openkan.mjs on pre-v0.5.0
|
|
23
33
|
releases; ok serve on v0.5.0+ where
|
|
@@ -30,6 +40,25 @@ feature/progress files for live planning.
|
|
|
30
40
|
`);
|
|
31
41
|
}
|
|
32
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Run one of the vendored OpenKan migration scripts. The scripts are
|
|
45
|
+
* `.mts` files that need `--experimental-strip-types` because they
|
|
46
|
+
* preserve the upstream TypeScript source verbatim from the v0.7.0
|
|
47
|
+
* tag. See `scripts/openkan/` for the vendored files.
|
|
48
|
+
*/
|
|
49
|
+
function runMigrateScript(scriptPath, args) {
|
|
50
|
+
const resolved = resolvePath(scriptPath);
|
|
51
|
+
const result = spawnSync(process.execPath, ['--experimental-strip-types', resolved, ...args], {
|
|
52
|
+
cwd: process.cwd(),
|
|
53
|
+
encoding: 'utf8',
|
|
54
|
+
shell: false,
|
|
55
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
56
|
+
});
|
|
57
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
58
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
59
|
+
return result.status ?? 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
33
62
|
function runDashboard(args) {
|
|
34
63
|
const launcher = resolveOpenKanDashboard();
|
|
35
64
|
// OpenKan v0.5.0 dropped the legacy `openkan` dashboard launcher. The
|
|
@@ -74,6 +103,48 @@ export async function run(name, args, isHelpRequest) {
|
|
|
74
103
|
print(runOpenKanOk([subcommand, ...rest]));
|
|
75
104
|
return true;
|
|
76
105
|
}
|
|
106
|
+
if (subcommand === 'project') {
|
|
107
|
+
// Forward project subcommands (e.g., project clean)
|
|
108
|
+
print(runOpenKanOk([subcommand, ...rest]));
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
if (subcommand === 'board') {
|
|
112
|
+
// Forward board subcommands (e.g., board delete)
|
|
113
|
+
print(runOpenKanOk([subcommand, ...rest]));
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
if (subcommand === 'migrate') {
|
|
117
|
+
// Migrate v1 tasks/board to the v2 layout that OpenKan 0.7.0 ships.
|
|
118
|
+
// Default is dry-run; --apply actually moves files. --tasks / --board
|
|
119
|
+
// narrow scope. The vendored scripts live under scripts/openkan/ and
|
|
120
|
+
// run via --experimental-strip-types because they're .mts.
|
|
121
|
+
const apply = rest.includes('--apply');
|
|
122
|
+
const tasksOnly = rest.includes('--tasks');
|
|
123
|
+
const boardOnly = rest.includes('--board');
|
|
124
|
+
if (!apply) {
|
|
125
|
+
process.stdout.write('Running in dry-run mode. Pass --apply to actually migrate.\n\n');
|
|
126
|
+
}
|
|
127
|
+
let exitCode = 0;
|
|
128
|
+
if (!boardOnly) {
|
|
129
|
+
process.stdout.write('=== Migrating tasks (.ok/tasks/<id>.json → <id>/task.json) ===\n');
|
|
130
|
+
const code = runMigrateScript(MIGRATE_TASKS, apply ? [] : ['--dry-run']);
|
|
131
|
+
if (code !== 0) exitCode = code;
|
|
132
|
+
}
|
|
133
|
+
if (!tasksOnly) {
|
|
134
|
+
process.stdout.write('\n=== Migrating board.json → per-task directories ===\n');
|
|
135
|
+
const code = runMigrateScript(MIGRATE_BOARD, apply ? [] : ['--dry-run']);
|
|
136
|
+
if (code !== 0) exitCode = code;
|
|
137
|
+
}
|
|
138
|
+
if (exitCode === 0) {
|
|
139
|
+
process.stdout.write(apply
|
|
140
|
+
? '\n✓ Migration complete.\n'
|
|
141
|
+
: '\n✓ Dry-run complete. Re-run with --apply to execute.\n');
|
|
142
|
+
} else {
|
|
143
|
+
process.stderr.write('\n✗ Migration completed with errors. See output above.\n');
|
|
144
|
+
}
|
|
145
|
+
process.exitCode = exitCode;
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
77
148
|
if (subcommand === 'dashboard') { runDashboard(rest); return true; }
|
|
78
149
|
help();
|
|
79
150
|
process.exitCode = 2;
|