@kinqs/brainrouter-cli 0.3.6 → 0.3.8
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 +29 -52
- package/agents/architect.json +18 -0
- package/agents/explorer.json +18 -0
- package/agents/reviewer.json +18 -0
- package/agents/verifier.json +18 -0
- package/agents/worker.json +18 -0
- package/changelog/0.2.0.md +15 -0
- package/changelog/0.3.0.md +20 -0
- package/changelog/0.3.1.md +22 -0
- package/changelog/0.3.2.md +15 -0
- package/changelog/0.3.3.md +19 -0
- package/changelog/0.3.4.md +20 -0
- package/changelog/0.3.5.md +9 -0
- package/changelog/0.3.6.md +9 -0
- package/changelog/0.3.7.md +20 -0
- package/changelog/0.3.8.md +30 -0
- package/changelog/README.md +41 -0
- package/dist/agent/agent.d.ts +34 -1
- package/dist/agent/agent.js +372 -79
- package/dist/agent/toolCallRecovery.d.ts +57 -0
- package/dist/agent/toolCallRecovery.js +130 -0
- package/dist/agent/toolSafety.d.ts +17 -0
- package/dist/agent/toolSafety.js +102 -0
- package/dist/cli/banner.d.ts +20 -0
- package/dist/cli/banner.js +47 -14
- package/dist/cli/cliPrompt.d.ts +40 -3
- package/dist/cli/cliPrompt.js +117 -25
- package/dist/cli/commands/_context.d.ts +3 -1
- package/dist/cli/commands/_helpers.d.ts +1 -1
- package/dist/cli/commands/config.d.ts +46 -0
- package/dist/cli/commands/config.js +1042 -0
- package/dist/cli/commands/init.d.ts +20 -0
- package/dist/cli/commands/init.js +64 -0
- package/dist/cli/commands/login.d.ts +13 -0
- package/dist/cli/commands/login.js +179 -0
- package/dist/cli/commands/mcp.d.ts +13 -11
- package/dist/cli/commands/mcp.js +261 -74
- package/dist/cli/commands/mcpInstall.d.ts +20 -0
- package/dist/cli/commands/mcpInstall.js +87 -0
- package/dist/cli/commands/orchestration.js +51 -0
- package/dist/cli/commands/releaseNotes.d.ts +24 -0
- package/dist/cli/commands/releaseNotes.js +109 -0
- package/dist/cli/commands/schedule.d.ts +18 -0
- package/dist/cli/commands/schedule.js +189 -0
- package/dist/cli/commands/ui.js +119 -60
- package/dist/cli/commands/workflow.d.ts +2 -0
- package/dist/cli/commands/workflow.js +54 -8
- package/dist/cli/ink/ChatApp.d.ts +206 -0
- package/dist/cli/ink/ChatApp.js +493 -0
- package/dist/cli/ink/Frame.d.ts +26 -0
- package/dist/cli/ink/Frame.js +5 -0
- package/dist/cli/ink/Picker.d.ts +71 -0
- package/dist/cli/ink/Picker.js +168 -0
- package/dist/cli/ink/SlashPalette.d.ts +51 -0
- package/dist/cli/ink/SlashPalette.js +136 -0
- package/dist/cli/ink/TextField.d.ts +34 -0
- package/dist/cli/ink/TextField.js +47 -0
- package/dist/cli/ink/WizardApp.d.ts +7 -0
- package/dist/cli/ink/WizardApp.js +422 -0
- package/dist/cli/ink/ambientChat.d.ts +34 -0
- package/dist/cli/ink/ambientChat.js +7 -0
- package/dist/cli/ink/consoleCapture.d.ts +11 -0
- package/dist/cli/ink/consoleCapture.js +33 -0
- package/dist/cli/ink/markdownRender.d.ts +41 -0
- package/dist/cli/ink/markdownRender.js +278 -0
- package/dist/cli/ink/renderWithResizeClear.d.ts +14 -0
- package/dist/cli/ink/renderWithResizeClear.js +33 -0
- package/dist/cli/ink/runChat.d.ts +34 -0
- package/dist/cli/ink/runChat.js +682 -0
- package/dist/cli/ink/runPicker.d.ts +31 -0
- package/dist/cli/ink/runPicker.js +139 -0
- package/dist/cli/ink/runSlashPalette.d.ts +23 -0
- package/dist/cli/ink/runSlashPalette.js +33 -0
- package/dist/cli/ink/runWizard.d.ts +22 -0
- package/dist/cli/ink/runWizard.js +133 -0
- package/dist/cli/ink/stdinHandoff.d.ts +51 -0
- package/dist/cli/ink/stdinHandoff.js +78 -0
- package/dist/cli/ink/toolFormat.d.ts +75 -0
- package/dist/cli/ink/toolFormat.js +206 -0
- package/dist/cli/ink/useTerminalSize.d.ts +35 -0
- package/dist/cli/ink/useTerminalSize.js +26 -0
- package/dist/cli/repl.d.ts +25 -3
- package/dist/cli/repl.js +52 -714
- package/dist/cli/slashSuggest.d.ts +32 -0
- package/dist/cli/slashSuggest.js +146 -0
- package/dist/cli/wizard/modelsApi.d.ts +72 -0
- package/dist/cli/wizard/modelsApi.js +166 -0
- package/dist/cli/wizard/picker.d.ts +202 -0
- package/dist/cli/wizard/picker.js +547 -0
- package/dist/cli/wizard/providers.d.ts +86 -0
- package/dist/cli/wizard/providers.js +190 -0
- package/dist/cli/wizard/runner.d.ts +13 -0
- package/dist/cli/wizard/runner.js +488 -0
- package/dist/cli/wizard/types.d.ts +122 -0
- package/dist/cli/wizard/types.js +109 -0
- package/dist/config/config.d.ts +13 -1
- package/dist/config/config.js +45 -3
- package/dist/index.js +157 -206
- package/dist/memory/briefing.d.ts +1 -1
- package/dist/memory/briefing.js +4 -4
- package/dist/memory/consolidation.d.ts +1 -1
- package/dist/orchestration/agentRegistry.d.ts +36 -0
- package/dist/orchestration/agentRegistry.js +64 -0
- package/dist/orchestration/orchestrator.d.ts +7 -0
- package/dist/orchestration/orchestrator.js +2 -0
- package/dist/orchestration/tools.d.ts +105 -3
- package/dist/orchestration/tools.js +167 -8
- package/dist/prompt/skillCatalog.d.ts +11 -0
- package/dist/prompt/skillCatalog.js +134 -0
- package/dist/prompt/skillRunner.d.ts +2 -2
- package/dist/prompt/skillRunner.js +2 -31
- package/dist/prompt/systemPrompt.js +7 -2
- package/dist/runtime/anthropicAdapter.d.ts +100 -0
- package/dist/runtime/anthropicAdapter.js +293 -0
- package/dist/runtime/cronParser.d.ts +23 -0
- package/dist/runtime/cronParser.js +122 -0
- package/dist/runtime/mcpClient.js +14 -11
- package/dist/runtime/mcpPool.d.ts +170 -0
- package/dist/runtime/mcpPool.js +442 -0
- package/dist/runtime/mcpUtils.d.ts +17 -1
- package/dist/runtime/mcpUtils.js +23 -0
- package/dist/runtime/scheduleTicker.d.ts +33 -0
- package/dist/runtime/scheduleTicker.js +99 -0
- package/dist/runtime/vendorSnippets.d.ts +45 -0
- package/dist/runtime/vendorSnippets.js +153 -0
- package/dist/state/scheduleStore.d.ts +37 -0
- package/dist/state/scheduleStore.js +64 -0
- package/package.json +14 -5
- package/.env.example +0 -116
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/release-notes` slash command — show the changelog for the running CLI version.
|
|
3
|
+
*
|
|
4
|
+
* /release-notes → current version's notes
|
|
5
|
+
* /release-notes <version> → specific version
|
|
6
|
+
* /release-notes list → every shipped version, sorted descending
|
|
7
|
+
*
|
|
8
|
+
* Changelog files ship inside the published package at `changelog/<version>.md`.
|
|
9
|
+
* The repo-root `brainrouter-changelog/` is copied into `brainrouter-cli/changelog/`
|
|
10
|
+
* by `prepublishOnly` so users who install via npm see them.
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
const MAX_LINES = 200;
|
|
17
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
18
|
+
export async function tryHandleReleaseNotesCommand(ctx, deps = {}) {
|
|
19
|
+
if (ctx.command !== '/release-notes')
|
|
20
|
+
return false;
|
|
21
|
+
const out = runReleaseNotes(ctx.args, deps);
|
|
22
|
+
console.log(out);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Pure handler — returns the rendered string. Split from `tryHandle*` so unit
|
|
27
|
+
* tests can assert on the output without capturing stdout.
|
|
28
|
+
*/
|
|
29
|
+
export function runReleaseNotes(args, deps = {}) {
|
|
30
|
+
const dir = deps.changelogDir ?? defaultChangelogDir();
|
|
31
|
+
const sub = (args[0] ?? '').toLowerCase();
|
|
32
|
+
if (sub === 'list')
|
|
33
|
+
return renderList(dir);
|
|
34
|
+
let version;
|
|
35
|
+
if (sub) {
|
|
36
|
+
if (!SEMVER_RE.test(sub)) {
|
|
37
|
+
return chalk.red(`Not a valid semver: "${args[0]}". Try /release-notes list.`);
|
|
38
|
+
}
|
|
39
|
+
version = sub;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const v = deps.currentVersion ?? readCurrentVersion();
|
|
43
|
+
if (!v)
|
|
44
|
+
return chalk.red('Could not determine current CLI version.');
|
|
45
|
+
version = v;
|
|
46
|
+
}
|
|
47
|
+
const filePath = path.join(dir, `${version}.md`);
|
|
48
|
+
let body;
|
|
49
|
+
try {
|
|
50
|
+
body = fs.readFileSync(filePath, 'utf8');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return chalk.yellow(`no notes shipped for ${version}`);
|
|
54
|
+
}
|
|
55
|
+
return truncate(body, version);
|
|
56
|
+
}
|
|
57
|
+
function renderList(dir) {
|
|
58
|
+
let entries;
|
|
59
|
+
try {
|
|
60
|
+
entries = fs.readdirSync(dir);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return chalk.yellow('No bundled changelog directory found.');
|
|
64
|
+
}
|
|
65
|
+
const versions = entries
|
|
66
|
+
.filter((f) => f.endsWith('.md'))
|
|
67
|
+
.map((f) => f.slice(0, -3))
|
|
68
|
+
.filter((v) => SEMVER_RE.test(v))
|
|
69
|
+
.sort(compareSemverDesc);
|
|
70
|
+
if (versions.length === 0)
|
|
71
|
+
return chalk.yellow('No changelog versions bundled.');
|
|
72
|
+
return versions.join('\n');
|
|
73
|
+
}
|
|
74
|
+
function truncate(body, version) {
|
|
75
|
+
const lines = body.split('\n');
|
|
76
|
+
if (lines.length <= MAX_LINES)
|
|
77
|
+
return body;
|
|
78
|
+
const head = lines.slice(0, MAX_LINES).join('\n');
|
|
79
|
+
return `${head}\n\n…truncated at ${MAX_LINES} lines. Run \`/release-notes ${version}\` on its own to scroll the full file in a fresh paginator.`;
|
|
80
|
+
}
|
|
81
|
+
function compareSemverDesc(a, b) {
|
|
82
|
+
const pa = a.split(/[-+]/)[0].split('.').map(Number);
|
|
83
|
+
const pb = b.split(/[-+]/)[0].split('.').map(Number);
|
|
84
|
+
for (let i = 0; i < 3; i++) {
|
|
85
|
+
if (pa[i] !== pb[i])
|
|
86
|
+
return pb[i] - pa[i];
|
|
87
|
+
}
|
|
88
|
+
// Identical core → keep pre-release sort stable by string compare (descending).
|
|
89
|
+
return b.localeCompare(a);
|
|
90
|
+
}
|
|
91
|
+
// --- Package-root resolution -------------------------------------------------
|
|
92
|
+
/**
|
|
93
|
+
* `brainrouter-cli/changelog/` — relative to this compiled file. The dist
|
|
94
|
+
* layout mirrors src, so both `src/cli/commands/releaseNotes.ts` (dev/tsx)
|
|
95
|
+
* and `dist/cli/commands/releaseNotes.js` (built) resolve to the same root.
|
|
96
|
+
*/
|
|
97
|
+
function defaultChangelogDir() {
|
|
98
|
+
return fileURLToPath(new URL('../../../changelog', import.meta.url));
|
|
99
|
+
}
|
|
100
|
+
function readCurrentVersion() {
|
|
101
|
+
try {
|
|
102
|
+
const pkgPath = fileURLToPath(new URL('../../../package.json', import.meta.url));
|
|
103
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
104
|
+
return pkg.version;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/schedule` slash command — recurring cron + one-shot dispatch.
|
|
3
|
+
*
|
|
4
|
+
* Recurring : /schedule cron "*\/15 * * * *" /ci-status
|
|
5
|
+
* One-shot : /schedule in 30s /agents
|
|
6
|
+
* /schedule at 14:30 /agents
|
|
7
|
+
* Management : /schedule list
|
|
8
|
+
* /schedule remove <id>
|
|
9
|
+
* /schedule disable <id>
|
|
10
|
+
* /schedule enable <id>
|
|
11
|
+
*
|
|
12
|
+
* The dispatched command runs in the SAME session that registered the
|
|
13
|
+
* schedule (we use `agent.sessionKey` as the owner). The ticker filters
|
|
14
|
+
* by owner — if a different REPL is open against the same workspace,
|
|
15
|
+
* it won't fire someone else's jobs.
|
|
16
|
+
*/
|
|
17
|
+
import type { CommandContext } from './_context.js';
|
|
18
|
+
export declare function tryHandleScheduleCommand(ctx: CommandContext): Promise<boolean>;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/schedule` slash command — recurring cron + one-shot dispatch.
|
|
3
|
+
*
|
|
4
|
+
* Recurring : /schedule cron "*\/15 * * * *" /ci-status
|
|
5
|
+
* One-shot : /schedule in 30s /agents
|
|
6
|
+
* /schedule at 14:30 /agents
|
|
7
|
+
* Management : /schedule list
|
|
8
|
+
* /schedule remove <id>
|
|
9
|
+
* /schedule disable <id>
|
|
10
|
+
* /schedule enable <id>
|
|
11
|
+
*
|
|
12
|
+
* The dispatched command runs in the SAME session that registered the
|
|
13
|
+
* schedule (we use `agent.sessionKey` as the owner). The ticker filters
|
|
14
|
+
* by owner — if a different REPL is open against the same workspace,
|
|
15
|
+
* it won't fire someone else's jobs.
|
|
16
|
+
*/
|
|
17
|
+
import chalk from 'chalk';
|
|
18
|
+
import { parseInterval } from '../../runtime/loopRunner.js';
|
|
19
|
+
import { parseCron, nextCronFire } from '../../runtime/cronParser.js';
|
|
20
|
+
import { addSchedule, loadSchedules, removeSchedule, setScheduleEnabled, } from '../../state/scheduleStore.js';
|
|
21
|
+
export async function tryHandleScheduleCommand(ctx) {
|
|
22
|
+
if (ctx.command !== '/schedule')
|
|
23
|
+
return false;
|
|
24
|
+
const { args, agent } = ctx;
|
|
25
|
+
const sub = (args[0] ?? '').toLowerCase();
|
|
26
|
+
if (!sub || sub === 'list') {
|
|
27
|
+
renderList(agent.workspaceRoot, agent.sessionKey);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (sub === 'remove' || sub === 'rm') {
|
|
31
|
+
const id = args[1];
|
|
32
|
+
if (!id) {
|
|
33
|
+
console.log(chalk.red('\nUsage: /schedule remove <id>\n'));
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
const ok = removeSchedule(agent.workspaceRoot, id);
|
|
37
|
+
console.log(ok
|
|
38
|
+
? chalk.green(`\n✓ Removed ${id}.\n`)
|
|
39
|
+
: chalk.yellow(`\nNo schedule with id ${id}.\n`));
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (sub === 'disable' || sub === 'enable') {
|
|
43
|
+
const id = args[1];
|
|
44
|
+
if (!id) {
|
|
45
|
+
console.log(chalk.red(`\nUsage: /schedule ${sub} <id>\n`));
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
const ok = setScheduleEnabled(agent.workspaceRoot, id, sub === 'enable');
|
|
49
|
+
console.log(ok
|
|
50
|
+
? chalk.green(`\n✓ ${sub === 'enable' ? 'Enabled' : 'Disabled'} ${id}.\n`)
|
|
51
|
+
: chalk.yellow(`\nNo schedule with id ${id}.\n`));
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
if (sub === 'cron') {
|
|
55
|
+
// Need to re-join because the splitter cracked the quoted cron expr
|
|
56
|
+
// across tokens. Re-join args after the leading "cron".
|
|
57
|
+
const rest = args.slice(1).join(' ').trim();
|
|
58
|
+
const m = /^"([^"]+)"\s+(\/\S.*)$/.exec(rest);
|
|
59
|
+
if (!m) {
|
|
60
|
+
console.log(chalk.red('\nUsage: /schedule cron "<expr>" /command'));
|
|
61
|
+
console.log(chalk.gray(' e.g. /schedule cron "*/15 * * * *" /ci-status\n'));
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
const expr = m[1];
|
|
65
|
+
const command = m[2].trim();
|
|
66
|
+
if (!command.startsWith('/')) {
|
|
67
|
+
console.log(chalk.red('\nSchedule only dispatches slash commands (must start with `/`).\n'));
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
const cron = parseCron(expr);
|
|
71
|
+
if (!cron) {
|
|
72
|
+
console.log(chalk.red(`\nInvalid cron expression: "${expr}"`));
|
|
73
|
+
console.log(chalk.gray(' Expected 5 fields: minute hour dom month dow\n'));
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
const nextRun = nextCronFire(cron, new Date());
|
|
77
|
+
const rec = addSchedule(agent.workspaceRoot, {
|
|
78
|
+
kind: 'cron',
|
|
79
|
+
expr,
|
|
80
|
+
command,
|
|
81
|
+
owner: agent.sessionKey,
|
|
82
|
+
nextRun: nextRun.toISOString(),
|
|
83
|
+
});
|
|
84
|
+
console.log(chalk.green(`\n✓ Registered ${rec.id}: cron "${expr}" → ${command}`));
|
|
85
|
+
console.log(chalk.gray(` Next fire: ${formatWhen(nextRun)}\n`));
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
if (sub === 'in') {
|
|
89
|
+
const ms = parseInterval(args[1] ?? '');
|
|
90
|
+
const command = args.slice(2).join(' ').trim();
|
|
91
|
+
if (!ms || !command) {
|
|
92
|
+
console.log(chalk.red('\nUsage: /schedule in <duration> /command'));
|
|
93
|
+
console.log(chalk.gray(' e.g. /schedule in 5m /ci-status\n'));
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (!command.startsWith('/')) {
|
|
97
|
+
console.log(chalk.red('\nSchedule only dispatches slash commands.\n'));
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
const nextRun = new Date(Date.now() + ms);
|
|
101
|
+
const rec = addSchedule(agent.workspaceRoot, {
|
|
102
|
+
kind: 'once',
|
|
103
|
+
expr: nextRun.toISOString(),
|
|
104
|
+
command,
|
|
105
|
+
owner: agent.sessionKey,
|
|
106
|
+
nextRun: nextRun.toISOString(),
|
|
107
|
+
});
|
|
108
|
+
console.log(chalk.green(`\n✓ Registered ${rec.id}: one-shot in ${args[1]} → ${command}`));
|
|
109
|
+
console.log(chalk.gray(` Fires at: ${formatWhen(nextRun)}\n`));
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (sub === 'at') {
|
|
113
|
+
const time = args[1] ?? '';
|
|
114
|
+
const command = args.slice(2).join(' ').trim();
|
|
115
|
+
const tm = /^(\d{1,2}):(\d{2})$/.exec(time);
|
|
116
|
+
if (!tm || !command) {
|
|
117
|
+
console.log(chalk.red('\nUsage: /schedule at HH:MM /command'));
|
|
118
|
+
console.log(chalk.gray(' e.g. /schedule at 14:30 /agents\n'));
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (!command.startsWith('/')) {
|
|
122
|
+
console.log(chalk.red('\nSchedule only dispatches slash commands.\n'));
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
const h = Number(tm[1]);
|
|
126
|
+
const min = Number(tm[2]);
|
|
127
|
+
if (h > 23 || min > 59) {
|
|
128
|
+
console.log(chalk.red('\nInvalid time. Hours 0-23, minutes 0-59.\n'));
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
const now = new Date();
|
|
132
|
+
const target = new Date(now);
|
|
133
|
+
target.setHours(h, min, 0, 0);
|
|
134
|
+
if (target.getTime() <= now.getTime())
|
|
135
|
+
target.setDate(target.getDate() + 1);
|
|
136
|
+
const rec = addSchedule(agent.workspaceRoot, {
|
|
137
|
+
kind: 'once',
|
|
138
|
+
expr: target.toISOString(),
|
|
139
|
+
command,
|
|
140
|
+
owner: agent.sessionKey,
|
|
141
|
+
nextRun: target.toISOString(),
|
|
142
|
+
});
|
|
143
|
+
console.log(chalk.green(`\n✓ Registered ${rec.id}: one-shot at ${time} → ${command}`));
|
|
144
|
+
console.log(chalk.gray(` Fires at: ${formatWhen(target)}\n`));
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
console.log(chalk.red(`\nUnknown subcommand: /schedule ${sub}`));
|
|
148
|
+
console.log(chalk.gray(' Try: list | cron "<expr>" /cmd | in <dur> /cmd | at HH:MM /cmd | remove <id> | disable <id> | enable <id>\n'));
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
function renderList(workspaceRoot, sessionKey) {
|
|
152
|
+
const all = loadSchedules(workspaceRoot);
|
|
153
|
+
const mine = all.filter((s) => s.owner === sessionKey);
|
|
154
|
+
if (mine.length === 0) {
|
|
155
|
+
console.log(chalk.yellow('\nNo schedules registered for this session.'));
|
|
156
|
+
console.log(chalk.gray(' Add one with /schedule cron "<expr>" /command or /schedule in 5m /command\n'));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
console.log(chalk.bold('\nSchedules'));
|
|
160
|
+
for (const s of mine) {
|
|
161
|
+
const status = s.enabled ? chalk.green('●') : chalk.gray('○');
|
|
162
|
+
const kind = s.kind === 'cron' ? `cron "${s.expr}"` : `once`;
|
|
163
|
+
console.log(` ${status} ${chalk.cyan(s.id)} ${chalk.gray(kind.padEnd(28))} → ${s.command}`);
|
|
164
|
+
console.log(` ${chalk.gray(`next: ${formatWhen(new Date(s.nextRun))}${s.lastRun ? ` · last: ${formatWhen(new Date(s.lastRun))}` : ''}`)}`);
|
|
165
|
+
}
|
|
166
|
+
console.log();
|
|
167
|
+
}
|
|
168
|
+
function formatWhen(d) {
|
|
169
|
+
if (!Number.isFinite(d.getTime()))
|
|
170
|
+
return '(invalid)';
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
const delta = d.getTime() - now;
|
|
173
|
+
const abs = Math.abs(delta);
|
|
174
|
+
const human = humanDelta(abs);
|
|
175
|
+
const rel = delta >= 0 ? `in ${human}` : `${human} ago`;
|
|
176
|
+
return `${d.toISOString().replace('T', ' ').slice(0, 16)} (${rel})`;
|
|
177
|
+
}
|
|
178
|
+
function humanDelta(ms) {
|
|
179
|
+
const s = Math.round(ms / 1000);
|
|
180
|
+
if (s < 60)
|
|
181
|
+
return `${s}s`;
|
|
182
|
+
const m = Math.round(s / 60);
|
|
183
|
+
if (m < 60)
|
|
184
|
+
return `${m}m`;
|
|
185
|
+
const h = Math.round(m / 60);
|
|
186
|
+
if (h < 48)
|
|
187
|
+
return `${h}h`;
|
|
188
|
+
return `${Math.round(h / 24)}d`;
|
|
189
|
+
}
|
package/dist/cli/commands/ui.js
CHANGED
|
@@ -8,14 +8,21 @@ import { execSync } from 'node:child_process';
|
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { spinner as makeSpinner } from '../spinner.js';
|
|
10
10
|
import { LOCAL_TOOLS } from '../../agent/agent.js';
|
|
11
|
-
import { callMcpTool } from '../../runtime/mcpUtils.js';
|
|
11
|
+
import { callMcpTool, hasMcpTool } from '../../runtime/mcpUtils.js';
|
|
12
12
|
import { listSessions, reconcileStale } from '../../orchestration/orchestrator.js';
|
|
13
13
|
import { readPreferences, resolveEffort, writePreferences } from '../../state/preferencesStore.js';
|
|
14
14
|
import { readPlan } from '../../state/taskStore.js';
|
|
15
|
-
|
|
15
|
+
// initAgentMd usage moved to commands/init.ts (0.3.7 wizard). The
|
|
16
|
+
// legacy /config + /init switch cases here are gone — the dispatcher
|
|
17
|
+
// in repl.ts routes them to the new handlers first. getConfigPath
|
|
18
|
+
// stays in scope because /doctor still surfaces the path.
|
|
19
|
+
import { getConfigPath, saveConfig } from '../../config/config.js';
|
|
16
20
|
import { copyToClipboard } from '../../runtime/clipboard.js';
|
|
17
|
-
import { initAgentMd } from '../../prompt/initAgentMd.js';
|
|
18
21
|
import { completeWorkspacePath, renderHelp } from '../repl.js';
|
|
22
|
+
import { PROVIDER_CATALOG, findProvider } from '../wizard/providers.js';
|
|
23
|
+
import { selectModel } from '../wizard/modelsApi.js';
|
|
24
|
+
import { buildTheme } from '../theme.js';
|
|
25
|
+
import { listFilesystemSkills } from '../../prompt/skillCatalog.js';
|
|
19
26
|
export async function tryHandleUiCommand(ctx) {
|
|
20
27
|
const { command, args, agent, mcpClient, config, rl, repl } = ctx;
|
|
21
28
|
// 'ctx' alias to keep references to the old ReplContext name working
|
|
@@ -78,27 +85,10 @@ export async function tryHandleUiCommand(ctx) {
|
|
|
78
85
|
console.log();
|
|
79
86
|
return true;
|
|
80
87
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Print config without API keys
|
|
86
|
-
const scrubbedConfig = JSON.parse(JSON.stringify(config));
|
|
87
|
-
if (scrubbedConfig.llm?.apiKey) {
|
|
88
|
-
scrubbedConfig.llm.apiKey = 'br_••••••••••••••••';
|
|
89
|
-
}
|
|
90
|
-
for (const s of Object.values(scrubbedConfig.servers)) {
|
|
91
|
-
const srv = s;
|
|
92
|
-
if (srv.apiKey)
|
|
93
|
-
srv.apiKey = 'br_••••••••••••••••';
|
|
94
|
-
if (srv.env?.BRAINROUTER_API_KEY) {
|
|
95
|
-
srv.env.BRAINROUTER_API_KEY = 'br_••••••••••••••••';
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
console.log(chalk.gray(JSON.stringify(scrubbedConfig, null, 2)));
|
|
99
|
-
console.log();
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
88
|
+
// /config now lives in commands/config.ts (0.3.7 settings home panel
|
|
89
|
+
// + verb-overloaded get/set). The dispatcher in repl.ts routes it
|
|
90
|
+
// before this case, so leaving anything here is dead — removed.
|
|
91
|
+
// Use `/config raw` if you want the old scrubbed-JSON dump.
|
|
102
92
|
case '/doctor':
|
|
103
93
|
{
|
|
104
94
|
console.log(chalk.bold('\nBrainRouter Doctor:'));
|
|
@@ -126,7 +116,7 @@ export async function tryHandleUiCommand(ctx) {
|
|
|
126
116
|
const toolNames = new Set((res.tools || []).map((tool) => tool.name));
|
|
127
117
|
const memoryTools = ['memory_recall', 'memory_capture_turn', 'memory_working_offload'];
|
|
128
118
|
for (const name of memoryTools) {
|
|
129
|
-
const hasTool = toolNames
|
|
119
|
+
const hasTool = hasMcpTool(toolNames, name);
|
|
130
120
|
console.log(` ${name}: ${hasTool ? chalk.green('available') : chalk.yellow('not exposed')}`);
|
|
131
121
|
}
|
|
132
122
|
}
|
|
@@ -178,30 +168,69 @@ export async function tryHandleUiCommand(ctx) {
|
|
|
178
168
|
console.log();
|
|
179
169
|
return true;
|
|
180
170
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (result.status === 'created') {
|
|
185
|
-
console.log(chalk.green(`\n✓ Created ${result.path}`));
|
|
186
|
-
console.log(chalk.gray('Edit it to describe your project, conventions, and boundaries — any AGENT.md-aware coding agent will read it.\n'));
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
console.log(chalk.yellow(`\nFile already exists: ${result.path}`));
|
|
190
|
-
console.log(chalk.gray('Open it and edit by hand if you want to refresh it.\n'));
|
|
191
|
-
}
|
|
192
|
-
return true;
|
|
193
|
-
}
|
|
171
|
+
// /init is now the onboarding-wizard entrypoint (commands/init.ts).
|
|
172
|
+
// The AGENT.md-only path lives behind `/init agentmd` for back-compat.
|
|
173
|
+
// Routed before this case in repl.ts; no fall-through handler needed.
|
|
194
174
|
case '/model':
|
|
195
175
|
{
|
|
196
176
|
const newModel = args[0];
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
177
|
+
const previous = agent.getModel();
|
|
178
|
+
// Direct-switch form `/model <name>` stays for scripts and muscle
|
|
179
|
+
// memory. No-arg opens the picker (0.3.7).
|
|
180
|
+
if (newModel) {
|
|
181
|
+
agent.setModel(newModel);
|
|
182
|
+
if (config.llm) {
|
|
183
|
+
config.llm.model = newModel;
|
|
184
|
+
saveConfig(config);
|
|
185
|
+
}
|
|
186
|
+
console.log(chalk.green(`\n✓ Model switched: ${chalk.gray(previous)} → ${chalk.cyan(newModel)}\n`));
|
|
200
187
|
return true;
|
|
201
188
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
189
|
+
// No-arg → open the picker. Resolves provider by matching the
|
|
190
|
+
// saved endpoint against PROVIDER_CATALOG; falls back to the
|
|
191
|
+
// OpenAI entry when nothing matches (the agent loop also
|
|
192
|
+
// defaults to OpenAI-compatible shapes).
|
|
193
|
+
const themeMode = readPreferences(agent.workspaceRoot).theme;
|
|
194
|
+
const theme = buildTheme(themeMode === 'mono' ? 'mono' : themeMode === 'light' ? 'light' : 'dark');
|
|
195
|
+
const llm = config.llm;
|
|
196
|
+
const provider = (llm?.endpoint && PROVIDER_CATALOG.find((p) => p.endpoint.replace(/\/$/, '') === (llm.endpoint ?? '').replace(/\/$/, ''))) ||
|
|
197
|
+
findProvider('openai');
|
|
198
|
+
const result = await selectModel({
|
|
199
|
+
theme,
|
|
200
|
+
provider,
|
|
201
|
+
apiKey: llm?.apiKey ?? '',
|
|
202
|
+
endpointOverride: llm?.endpoint,
|
|
203
|
+
currentModel: previous,
|
|
204
|
+
title: '/model — quick-swap',
|
|
205
|
+
badge: provider.label,
|
|
206
|
+
});
|
|
207
|
+
if (!result) {
|
|
208
|
+
console.log(chalk.yellow('\n /model cancelled.\n'));
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
if (result.model === previous) {
|
|
212
|
+
console.log(chalk.gray(`\n Model unchanged (${previous}).\n`));
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
// Cross-provider sanity check — if the picked model looks like
|
|
216
|
+
// a different vendor's namespace (anthropic/*, google/*, etc.)
|
|
217
|
+
// and the active provider isn't a multi-vendor gateway, warn so
|
|
218
|
+
// the user doesn't hit a confusing 404 on the next turn.
|
|
219
|
+
if (looksLikeForeignModel(result.model, provider)) {
|
|
220
|
+
console.log(chalk.yellow(`\n ⚠ "${result.model}" looks like a different provider's namespace. ` +
|
|
221
|
+
`Active endpoint: ${provider.label}.` +
|
|
222
|
+
`\n Run /config provider <id> to switch endpoints, or /model again to pick a native model.\n`));
|
|
223
|
+
}
|
|
224
|
+
agent.setModel(result.model);
|
|
225
|
+
if (config.llm) {
|
|
226
|
+
config.llm.model = result.model;
|
|
227
|
+
saveConfig(config);
|
|
228
|
+
}
|
|
229
|
+
const sourceTag = result.source === 'live' ? `live · ${result.liveCount} models` :
|
|
230
|
+
result.source === 'fallback' ? `offline · static catalog (${result.liveError ?? 'unknown'})` :
|
|
231
|
+
'static catalog';
|
|
232
|
+
console.log(chalk.green(`\n✓ Model switched: ${chalk.gray(previous)} → ${chalk.cyan(result.model)}`));
|
|
233
|
+
console.log(chalk.gray(` Source: ${sourceTag}\n`));
|
|
205
234
|
return true;
|
|
206
235
|
}
|
|
207
236
|
// /mcp moved to its own command file (commands/mcp.ts) as part of 0.3.6
|
|
@@ -410,12 +439,22 @@ export async function tryHandleUiCommand(ctx) {
|
|
|
410
439
|
console.log(chalk.gray(' Drop a folder under skills/<category>/<name>/SKILL.md to register one.\n'));
|
|
411
440
|
return true;
|
|
412
441
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
console.log(chalk.cyan(`
|
|
442
|
+
const skills = listFilesystemSkills(agent.workspaceRoot);
|
|
443
|
+
if (skills.length > 0) {
|
|
444
|
+
console.log(chalk.gray(' Skills'));
|
|
445
|
+
for (const skill of skills) {
|
|
446
|
+
const category = skill.category ? `${skill.category}/` : '';
|
|
447
|
+
console.log(` • ${chalk.cyan(`${category}${skill.name}`)} (${chalk.gray(skill.scope ?? 'filesystem')})`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (fs.existsSync(pluginsRoot)) {
|
|
451
|
+
const entries = fs.readdirSync(pluginsRoot, { withFileTypes: true });
|
|
452
|
+
const pluginDirs = entries.filter((entry) => entry.isDirectory());
|
|
453
|
+
if (pluginDirs.length > 0) {
|
|
454
|
+
console.log(chalk.gray(' Plugin folders'));
|
|
455
|
+
for (const entry of pluginDirs) {
|
|
456
|
+
console.log(` • ${chalk.cyan(path.relative(agent.workspaceRoot, path.join(pluginsRoot, entry.name)))}`);
|
|
457
|
+
}
|
|
419
458
|
}
|
|
420
459
|
}
|
|
421
460
|
console.log();
|
|
@@ -496,23 +535,19 @@ export async function tryHandleUiCommand(ctx) {
|
|
|
496
535
|
case '/where':
|
|
497
536
|
{
|
|
498
537
|
const { gatherWhereInputs, renderWhere } = await import('../whereView.js');
|
|
538
|
+
const { resolveDisplayedMcpState } = await import('../banner.js');
|
|
499
539
|
const { resolveTheme } = await import('../theme.js');
|
|
500
540
|
const theme = resolveTheme(agent.workspaceRoot);
|
|
501
|
-
const
|
|
502
|
-
const server = config.servers[profileName];
|
|
541
|
+
const displayedMcp = resolveDisplayedMcpState(config, mcpClient);
|
|
503
542
|
const briefing = agent.getLastBriefing();
|
|
504
543
|
const inputs = gatherWhereInputs({
|
|
505
544
|
workspaceRoot: agent.workspaceRoot,
|
|
506
545
|
sessionKey: agent.sessionKey,
|
|
507
546
|
model: agent.getModel(),
|
|
508
|
-
mcpProfile:
|
|
509
|
-
mcpTransport:
|
|
510
|
-
mcpOnline:
|
|
511
|
-
|
|
512
|
-
// config field when present, otherwise 'unknown'.
|
|
513
|
-
mcpIdentity: typeof mcpClient.getIdentity === 'function'
|
|
514
|
-
? mcpClient.getIdentity()
|
|
515
|
-
: (server?.identity ?? 'unknown'),
|
|
547
|
+
mcpProfile: displayedMcp.profile,
|
|
548
|
+
mcpTransport: displayedMcp.transport,
|
|
549
|
+
mcpOnline: displayedMcp.online,
|
|
550
|
+
mcpIdentity: displayedMcp.identity,
|
|
516
551
|
accessMode: agent.getAccessMode(),
|
|
517
552
|
recalledRecords: agent.getRecalledRecords(),
|
|
518
553
|
briefingSources: briefing.sources,
|
|
@@ -527,3 +562,27 @@ export async function tryHandleUiCommand(ctx) {
|
|
|
527
562
|
}
|
|
528
563
|
return false;
|
|
529
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* Heuristic — does the picked model id look like it belongs to a
|
|
567
|
+
* different vendor than the active provider's endpoint? Catches the
|
|
568
|
+
* common foot-gun of picking `anthropic/claude-*` while pointed at
|
|
569
|
+
* OpenAI direct, where the request 404s at the endpoint and the user
|
|
570
|
+
* has no obvious "you needed to switch endpoints" signal.
|
|
571
|
+
*
|
|
572
|
+
* Returns false for gateway providers (OpenRouter, "anthropic-via-gateway")
|
|
573
|
+
* since multi-vendor namespaces are expected there.
|
|
574
|
+
*/
|
|
575
|
+
function looksLikeForeignModel(model, provider) {
|
|
576
|
+
// Gateways are vendor-agnostic by design.
|
|
577
|
+
if (provider.id === 'openrouter' || provider.id === 'anthropic-via-gateway')
|
|
578
|
+
return false;
|
|
579
|
+
const FOREIGN_PREFIXES = {
|
|
580
|
+
openai: ['anthropic/', 'google/', 'meta/', 'mistralai/', 'qwen/', 'deepseek/'],
|
|
581
|
+
deepseek: ['anthropic/', 'google/', 'openai/', 'meta/', 'mistralai/'],
|
|
582
|
+
gemini: ['anthropic/', 'openai/', 'meta/', 'mistralai/', 'deepseek/'],
|
|
583
|
+
lmstudio: [],
|
|
584
|
+
ollama: [],
|
|
585
|
+
};
|
|
586
|
+
const list = FOREIGN_PREFIXES[provider.id] ?? [];
|
|
587
|
+
return list.some((prefix) => model.startsWith(prefix));
|
|
588
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Hand-tune imports if the compiler complains.
|
|
4
4
|
*/
|
|
5
5
|
import type { CommandContext } from './_context.js';
|
|
6
|
+
import { type SkillListItem } from '../../prompt/skillCatalog.js';
|
|
6
7
|
/**
|
|
7
8
|
* Decide whether `/grill-me` should refuse to fire because the current
|
|
8
9
|
* workflow already has a written `spec.md`. The clarifying pass is meant to
|
|
@@ -22,3 +23,4 @@ export declare function shouldSkipGrillMe(workspaceRoot: string, force: boolean,
|
|
|
22
23
|
specPath?: string;
|
|
23
24
|
};
|
|
24
25
|
export declare function tryHandleWorkflowCommand(ctx: CommandContext): Promise<boolean>;
|
|
26
|
+
export declare function normalizeSkillsList(payload: any): SkillListItem[] | undefined;
|