bobo-ai-cli 1.3.0 ā 1.4.2
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/dist/agent.d.ts +9 -0
- package/dist/agent.js +86 -13
- package/dist/agent.js.map +1 -1
- package/dist/completer.d.ts +2 -1
- package/dist/completer.js +20 -4
- package/dist/completer.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +370 -113
- package/dist/index.js.map +1 -1
- package/dist/statusbar.d.ts +11 -6
- package/dist/statusbar.js +28 -61
- package/dist/statusbar.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command } from 'commander';
|
|
2
|
+
import { Command, Option } from 'commander';
|
|
3
3
|
import { createInterface } from 'node:readline';
|
|
4
4
|
import { readFileSync, existsSync, mkdirSync, copyFileSync, writeFileSync, readdirSync, statSync, cpSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
@@ -20,7 +20,7 @@ import { registerStructuredTemplateCommand } from './structured-template-command
|
|
|
20
20
|
import { saveSession, listSessions, loadSession, getRecentSession } from './sessions.js';
|
|
21
21
|
import { generateInsight } from './insight.js';
|
|
22
22
|
import { spawnSubAgent, listSubAgents, getSubAgent } from './sub-agents.js';
|
|
23
|
-
import { enableStatusBar, disableStatusBar, setupResizeHandler } from './statusbar.js';
|
|
23
|
+
import { enableStatusBar, disableStatusBar, updateStatusBar, setupResizeHandler, renderStatusBar } from './statusbar.js';
|
|
24
24
|
import { slashCompleter } from './completer.js';
|
|
25
25
|
import chalk from 'chalk';
|
|
26
26
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
@@ -36,13 +36,46 @@ program
|
|
|
36
36
|
.description('š Bobo CLI ā Portable AI Engineering Assistant')
|
|
37
37
|
.version(version)
|
|
38
38
|
.argument('[prompt...]', 'Run a one-shot prompt without entering REPL')
|
|
39
|
-
.
|
|
39
|
+
.addOption(new Option('-p, --print', 'Non-interactive mode: print response and exit (supports piped input)'))
|
|
40
|
+
.addOption(new Option('-c, --continue', 'Continue most recent conversation'))
|
|
41
|
+
.addOption(new Option('-r, --resume <session>', 'Resume a specific session by ID'))
|
|
42
|
+
.addOption(new Option('--model <model>', 'Override model for this session'))
|
|
43
|
+
.addOption(new Option('--effort <level>', 'Set effort level').choices(['low', 'medium', 'high']))
|
|
44
|
+
.addOption(new Option('--full-auto', 'Auto-approve all tool calls'))
|
|
45
|
+
.addOption(new Option('--yolo', 'No sandbox, no approvals (dangerous!)'))
|
|
46
|
+
.action(async (promptParts, opts) => {
|
|
40
47
|
const prompt = promptParts.join(' ').trim();
|
|
48
|
+
// Determine permission mode
|
|
49
|
+
let permissionMode = 'ask';
|
|
50
|
+
if (opts.fullAuto)
|
|
51
|
+
permissionMode = 'auto';
|
|
52
|
+
if (opts.yolo)
|
|
53
|
+
permissionMode = 'yolo';
|
|
54
|
+
// -p mode: non-interactive, supports piped input
|
|
55
|
+
if (opts.print) {
|
|
56
|
+
await runPrintMode(prompt, {
|
|
57
|
+
model: opts.model,
|
|
58
|
+
effort: opts.effort,
|
|
59
|
+
permissionMode,
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Interactive mode
|
|
41
64
|
if (prompt) {
|
|
42
|
-
await runOneShot(prompt
|
|
65
|
+
await runOneShot(prompt, {
|
|
66
|
+
model: opts.model,
|
|
67
|
+
effort: opts.effort,
|
|
68
|
+
permissionMode,
|
|
69
|
+
});
|
|
43
70
|
}
|
|
44
71
|
else {
|
|
45
|
-
await runRepl(
|
|
72
|
+
await runRepl({
|
|
73
|
+
continueSession: opts.continue,
|
|
74
|
+
resumeId: opts.resume,
|
|
75
|
+
model: opts.model,
|
|
76
|
+
effort: opts.effort,
|
|
77
|
+
permissionMode,
|
|
78
|
+
});
|
|
46
79
|
}
|
|
47
80
|
});
|
|
48
81
|
// āāā Config subcommand āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
@@ -114,7 +147,7 @@ program
|
|
|
114
147
|
}
|
|
115
148
|
}
|
|
116
149
|
initSkills();
|
|
117
|
-
// Copy bundled skills
|
|
150
|
+
// Copy bundled skills (including scripts/ subdirs)
|
|
118
151
|
const bundledSkillsDir = join(__dirname, '..', 'bundled-skills');
|
|
119
152
|
const userSkillsDir = join(getConfigDir(), 'skills');
|
|
120
153
|
if (existsSync(bundledSkillsDir)) {
|
|
@@ -133,13 +166,11 @@ program
|
|
|
133
166
|
continue;
|
|
134
167
|
}
|
|
135
168
|
if (!existsSync(dest)) {
|
|
136
|
-
// Use cpSync recursive ā copies everything including scripts/
|
|
137
169
|
cpSync(src, dest, { recursive: true });
|
|
138
170
|
installed++;
|
|
139
171
|
}
|
|
140
172
|
}
|
|
141
173
|
if (installed > 0) {
|
|
142
|
-
// All skills enabled by default ā passive triggering based on context
|
|
143
174
|
const manifestPath = join(getConfigDir(), 'skills-manifest.json');
|
|
144
175
|
let manifest = {};
|
|
145
176
|
try {
|
|
@@ -159,6 +190,22 @@ program
|
|
|
159
190
|
printSuccess(`${installed} skills installed (all enabled, passive triggering)`);
|
|
160
191
|
}
|
|
161
192
|
}
|
|
193
|
+
// Create BOBO.md template if not exists
|
|
194
|
+
const boboMdPath = join(process.cwd(), 'BOBO.md');
|
|
195
|
+
if (!existsSync(boboMdPath)) {
|
|
196
|
+
writeFileSync(boboMdPath, `# Project Instructions
|
|
197
|
+
|
|
198
|
+
<!-- Bobo reads this file at the start of every session. -->
|
|
199
|
+
<!-- Add coding standards, architecture decisions, and project-specific rules here. -->
|
|
200
|
+
|
|
201
|
+
## Build & Test
|
|
202
|
+
<!-- e.g.: npm run build, npm test -->
|
|
203
|
+
|
|
204
|
+
## Style Guide
|
|
205
|
+
<!-- e.g.: Use TypeScript strict mode, prefer const over let -->
|
|
206
|
+
`);
|
|
207
|
+
printSuccess('Created BOBO.md (project instructions)');
|
|
208
|
+
}
|
|
162
209
|
printSuccess(`Initialized ${getConfigDir()}`);
|
|
163
210
|
printLine(`Knowledge: ${knowledgeDir}`);
|
|
164
211
|
printWarning('Configure your API key: bobo config set apiKey <your-key>');
|
|
@@ -192,7 +239,6 @@ program
|
|
|
192
239
|
allGood = false;
|
|
193
240
|
}
|
|
194
241
|
}
|
|
195
|
-
// Check API key
|
|
196
242
|
const config = loadConfig();
|
|
197
243
|
if (config.apiKey) {
|
|
198
244
|
printLine(` ${chalk.green('ā')} ${'API Key'.padEnd(12)} ${chalk.dim('configured')}`);
|
|
@@ -201,7 +247,9 @@ program
|
|
|
201
247
|
printLine(` ${chalk.red('ā')} ${'API Key'.padEnd(12)} ${chalk.red('not set ā run: bobo config set apiKey <key>')}`);
|
|
202
248
|
allGood = false;
|
|
203
249
|
}
|
|
204
|
-
// Check
|
|
250
|
+
// Check BOBO.md
|
|
251
|
+
const boboMd = existsSync(join(process.cwd(), 'BOBO.md'));
|
|
252
|
+
printLine(` ${boboMd ? chalk.green('ā') : chalk.yellow('ā')} ${'BOBO.md'.padEnd(12)} ${boboMd ? chalk.dim('found') : chalk.yellow('not found ā run: bobo init')}`);
|
|
205
253
|
const skillsDir = join(getConfigDir(), 'skills');
|
|
206
254
|
if (existsSync(skillsDir)) {
|
|
207
255
|
const count = readdirSync(skillsDir).filter(f => {
|
|
@@ -226,7 +274,7 @@ program
|
|
|
226
274
|
}
|
|
227
275
|
printLine();
|
|
228
276
|
});
|
|
229
|
-
// āāā Spawn subcommand
|
|
277
|
+
// āāā Spawn subcommand āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
230
278
|
program
|
|
231
279
|
.command('spawn <task>')
|
|
232
280
|
.description('Spawn a background sub-agent to run a task')
|
|
@@ -284,7 +332,6 @@ agentsCmd
|
|
|
284
332
|
}
|
|
285
333
|
printLine();
|
|
286
334
|
});
|
|
287
|
-
// Default agents action: list
|
|
288
335
|
agentsCmd.action(() => {
|
|
289
336
|
const agents = listSubAgents();
|
|
290
337
|
if (agents.length === 0) {
|
|
@@ -315,10 +362,7 @@ program
|
|
|
315
362
|
});
|
|
316
363
|
// āāā Skill subcommand āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
317
364
|
const skillCmd = program.command('skill').description('Manage skills');
|
|
318
|
-
skillCmd
|
|
319
|
-
.command('list')
|
|
320
|
-
.description('List all skills')
|
|
321
|
-
.action(() => {
|
|
365
|
+
skillCmd.command('list').description('List all skills').action(() => {
|
|
322
366
|
const skills = listSkills();
|
|
323
367
|
console.log(chalk.cyan.bold('\nš§© Skills:\n'));
|
|
324
368
|
for (const s of skills) {
|
|
@@ -328,29 +372,15 @@ skillCmd
|
|
|
328
372
|
}
|
|
329
373
|
console.log();
|
|
330
374
|
});
|
|
331
|
-
skillCmd
|
|
332
|
-
.
|
|
333
|
-
.description('Enable a skill')
|
|
334
|
-
.action((name) => {
|
|
335
|
-
const result = setSkillEnabled(name, true);
|
|
336
|
-
console.log(result);
|
|
375
|
+
skillCmd.command('enable <name>').description('Enable a skill').action((name) => {
|
|
376
|
+
console.log(setSkillEnabled(name, true));
|
|
337
377
|
});
|
|
338
|
-
skillCmd
|
|
339
|
-
.
|
|
340
|
-
.description('Disable a skill')
|
|
341
|
-
.action((name) => {
|
|
342
|
-
const result = setSkillEnabled(name, false);
|
|
343
|
-
console.log(result);
|
|
378
|
+
skillCmd.command('disable <name>').description('Disable a skill').action((name) => {
|
|
379
|
+
console.log(setSkillEnabled(name, false));
|
|
344
380
|
});
|
|
345
|
-
skillCmd
|
|
346
|
-
.
|
|
347
|
-
.
|
|
348
|
-
.action((path) => {
|
|
349
|
-
const resolved = path.startsWith('~')
|
|
350
|
-
? join(process.env.HOME || '', path.slice(1))
|
|
351
|
-
: path;
|
|
352
|
-
const result = importSkills(resolved);
|
|
353
|
-
console.log(result);
|
|
381
|
+
skillCmd.command('import <path>').description('Batch import skills').action((path) => {
|
|
382
|
+
const resolved = path.startsWith('~') ? join(process.env.HOME || '', path.slice(1)) : path;
|
|
383
|
+
console.log(importSkills(resolved));
|
|
354
384
|
});
|
|
355
385
|
// āāā Structured knowledge commands āāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
356
386
|
registerKnowledgeCommand(program);
|
|
@@ -359,17 +389,47 @@ registerStructuredSkillsCommand(program);
|
|
|
359
389
|
registerStructuredTemplateCommand(program);
|
|
360
390
|
// āāā Project subcommand āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
361
391
|
const projectCmd = program.command('project').description('Manage project configuration');
|
|
362
|
-
projectCmd
|
|
363
|
-
|
|
364
|
-
.description('Initialize .bobo/ project config in current directory')
|
|
365
|
-
.action(() => {
|
|
366
|
-
const result = initProject();
|
|
367
|
-
printSuccess(result);
|
|
392
|
+
projectCmd.command('init').description('Initialize .bobo/ project config').action(() => {
|
|
393
|
+
printSuccess(initProject());
|
|
368
394
|
});
|
|
395
|
+
async function runPrintMode(prompt, opts) {
|
|
396
|
+
// Read piped stdin if available
|
|
397
|
+
let input = prompt;
|
|
398
|
+
if (!process.stdin.isTTY) {
|
|
399
|
+
const chunks = [];
|
|
400
|
+
for await (const chunk of process.stdin) {
|
|
401
|
+
chunks.push(chunk);
|
|
402
|
+
}
|
|
403
|
+
const piped = Buffer.concat(chunks).toString('utf-8');
|
|
404
|
+
input = piped + (prompt ? `\n\n${prompt}` : '');
|
|
405
|
+
}
|
|
406
|
+
if (!input.trim()) {
|
|
407
|
+
printError('No input provided. Usage: bobo -p "query" or cat file | bobo -p "explain"');
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
await runAgent(input, [], {
|
|
412
|
+
quiet: false,
|
|
413
|
+
model: opts.model,
|
|
414
|
+
effort: opts.effort,
|
|
415
|
+
permissionMode: opts.permissionMode,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
catch (e) {
|
|
419
|
+
if (e.message !== 'Aborted') {
|
|
420
|
+
printError(e.message);
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
369
425
|
// āāā One-shot mode āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
370
|
-
async function runOneShot(prompt) {
|
|
426
|
+
async function runOneShot(prompt, opts) {
|
|
371
427
|
try {
|
|
372
|
-
await runAgent(prompt, []
|
|
428
|
+
await runAgent(prompt, [], {
|
|
429
|
+
model: opts.model,
|
|
430
|
+
effort: opts.effort,
|
|
431
|
+
permissionMode: opts.permissionMode,
|
|
432
|
+
});
|
|
373
433
|
}
|
|
374
434
|
catch (e) {
|
|
375
435
|
if (e.message !== 'Aborted') {
|
|
@@ -378,51 +438,84 @@ async function runOneShot(prompt) {
|
|
|
378
438
|
}
|
|
379
439
|
}
|
|
380
440
|
}
|
|
381
|
-
|
|
382
|
-
async function runRepl() {
|
|
441
|
+
async function runRepl(opts) {
|
|
383
442
|
const config = loadConfig();
|
|
384
443
|
const skills = listSkills();
|
|
385
444
|
const knowledgeFiles = listKnowledgeFiles();
|
|
386
445
|
const sessionStartTime = Date.now();
|
|
387
446
|
const matchedSkills = [];
|
|
447
|
+
// Runtime overrides
|
|
448
|
+
let currentModel = opts.model || config.model;
|
|
449
|
+
let currentEffort = opts.effort || config.effort;
|
|
450
|
+
let currentPermissionMode = opts.permissionMode || config.permissionMode;
|
|
451
|
+
let sessionName = '';
|
|
388
452
|
printWelcome({
|
|
389
453
|
version,
|
|
390
|
-
model:
|
|
454
|
+
model: currentModel,
|
|
391
455
|
toolCount: toolDefinitions.length,
|
|
392
456
|
skillsActive: skills.filter(s => s.enabled).length,
|
|
393
457
|
skillsTotal: skills.length,
|
|
394
458
|
knowledgeCount: knowledgeFiles.length,
|
|
395
459
|
cwd: process.cwd(),
|
|
396
460
|
});
|
|
461
|
+
// Check BOBO.md
|
|
462
|
+
const boboMdExists = existsSync(join(process.cwd(), 'BOBO.md'));
|
|
463
|
+
if (boboMdExists) {
|
|
464
|
+
printLine(chalk.dim(' š BOBO.md loaded'));
|
|
465
|
+
}
|
|
397
466
|
if (!config.apiKey) {
|
|
398
467
|
printWarning('API key not configured. Run: bobo config set apiKey <your-key>');
|
|
399
468
|
printLine();
|
|
400
469
|
}
|
|
401
|
-
//
|
|
470
|
+
// Restore session
|
|
402
471
|
let history = [];
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
472
|
+
if (opts.continueSession) {
|
|
473
|
+
// -c flag: continue most recent session
|
|
474
|
+
const recent = getRecentSession(86400000); // 24 hours
|
|
475
|
+
if (recent && recent.messages.length > 0) {
|
|
476
|
+
history = recent.messages;
|
|
477
|
+
printSuccess(`Continuing session (${history.length} messages, "${recent.firstUserMessage.slice(0, 40)}...")`);
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
printWarning('No recent session found.');
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
else if (opts.resumeId) {
|
|
484
|
+
// -r flag: resume specific session
|
|
485
|
+
const session = loadSession(opts.resumeId);
|
|
486
|
+
if (session) {
|
|
487
|
+
history = session.messages;
|
|
488
|
+
printSuccess(`Resumed session ${opts.resumeId} (${history.length} messages)`);
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
printWarning(`Session not found: ${opts.resumeId}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
// Auto-resume prompt
|
|
496
|
+
const recentSession = getRecentSession(3600000);
|
|
497
|
+
if (recentSession && recentSession.messages.length > 0) {
|
|
498
|
+
printLine(chalk.yellow(`š¾ Found recent session (${recentSession.messageCount} messages, ${recentSession.firstUserMessage.slice(0, 50)}...)`));
|
|
499
|
+
printLine(chalk.dim(' Resume? (y/n)'));
|
|
500
|
+
const answer = await new Promise((resolve) => {
|
|
501
|
+
const tmpRl = createInterface({ input: process.stdin, output: process.stdout });
|
|
502
|
+
tmpRl.question(chalk.green('> '), (ans) => {
|
|
503
|
+
tmpRl.close();
|
|
504
|
+
resolve(ans.trim().toLowerCase());
|
|
505
|
+
});
|
|
413
506
|
});
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
507
|
+
if (answer === 'y' || answer === 'yes') {
|
|
508
|
+
history = recentSession.messages;
|
|
509
|
+
printSuccess(`Resumed session (${history.length} messages)`);
|
|
510
|
+
}
|
|
418
511
|
}
|
|
419
512
|
}
|
|
420
|
-
// Enable
|
|
513
|
+
// Enable status bar
|
|
421
514
|
if (process.stdout.isTTY) {
|
|
422
515
|
setupResizeHandler();
|
|
423
516
|
enableStatusBar({
|
|
424
|
-
model:
|
|
425
|
-
thinkingLevel:
|
|
517
|
+
model: currentModel,
|
|
518
|
+
thinkingLevel: currentEffort,
|
|
426
519
|
skillsCount: skills.filter(s => s.enabled).length,
|
|
427
520
|
cwd: process.cwd(),
|
|
428
521
|
});
|
|
@@ -434,7 +527,15 @@ async function runRepl() {
|
|
|
434
527
|
completer: slashCompleter,
|
|
435
528
|
});
|
|
436
529
|
let abortController = null;
|
|
437
|
-
|
|
530
|
+
let lastResponse = '';
|
|
531
|
+
let autoCompactTriggered = false;
|
|
532
|
+
// Wrapper that renders status bar before prompt
|
|
533
|
+
const showPrompt = () => {
|
|
534
|
+
const bar = renderStatusBar();
|
|
535
|
+
if (bar)
|
|
536
|
+
printLine(bar);
|
|
537
|
+
rl.prompt();
|
|
538
|
+
};
|
|
438
539
|
const autoSave = () => {
|
|
439
540
|
if (history.length > 0) {
|
|
440
541
|
const id = saveSession(history, process.cwd());
|
|
@@ -446,11 +547,11 @@ async function runRepl() {
|
|
|
446
547
|
abortController.abort();
|
|
447
548
|
abortController = null;
|
|
448
549
|
printLine(chalk.dim('\n(cancelled)'));
|
|
449
|
-
|
|
550
|
+
showPrompt();
|
|
450
551
|
}
|
|
451
552
|
else {
|
|
452
553
|
printLine(chalk.dim('\n(press Ctrl+C again or Ctrl+D to exit)'));
|
|
453
|
-
|
|
554
|
+
showPrompt();
|
|
454
555
|
}
|
|
455
556
|
});
|
|
456
557
|
rl.on('close', () => {
|
|
@@ -459,30 +560,156 @@ async function runRepl() {
|
|
|
459
560
|
printLine(chalk.dim('\nGoodbye! š'));
|
|
460
561
|
process.exit(0);
|
|
461
562
|
});
|
|
462
|
-
|
|
563
|
+
showPrompt();
|
|
463
564
|
for await (const line of rl) {
|
|
464
565
|
const input = line.trim();
|
|
465
566
|
if (!input) {
|
|
466
|
-
|
|
567
|
+
showPrompt();
|
|
467
568
|
continue;
|
|
468
569
|
}
|
|
570
|
+
// āāā Exit āāā
|
|
469
571
|
if (input === '/quit' || input === '/exit') {
|
|
470
572
|
autoSave();
|
|
471
573
|
disableStatusBar();
|
|
472
574
|
printLine(chalk.dim('Goodbye! š'));
|
|
473
575
|
process.exit(0);
|
|
474
576
|
}
|
|
577
|
+
// āāā /new, /clear āāā
|
|
475
578
|
if (input === '/clear' || input === '/new') {
|
|
476
579
|
history = [];
|
|
477
580
|
matchedSkills.length = 0;
|
|
581
|
+
lastResponse = '';
|
|
582
|
+
autoCompactTriggered = false;
|
|
478
583
|
resetPlan();
|
|
479
584
|
printSuccess('Conversation cleared');
|
|
480
|
-
|
|
585
|
+
showPrompt();
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
// āāā /model [name] āāā
|
|
589
|
+
if (input.startsWith('/model')) {
|
|
590
|
+
const newModel = input.replace('/model', '').trim();
|
|
591
|
+
if (!newModel) {
|
|
592
|
+
printLine(chalk.cyan('Current model: ') + currentModel);
|
|
593
|
+
printLine(chalk.dim('Usage: /model <model-name>'));
|
|
594
|
+
printLine(chalk.dim(' Examples: claude-sonnet-4-20250514, gpt-4o, deepseek-chat'));
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
currentModel = newModel;
|
|
598
|
+
updateStatusBar({ model: currentModel });
|
|
599
|
+
printSuccess(`Model switched to: ${currentModel}`);
|
|
600
|
+
}
|
|
601
|
+
showPrompt();
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
// āāā /effort [level] āāā
|
|
605
|
+
if (input.startsWith('/effort')) {
|
|
606
|
+
const level = input.replace('/effort', '').trim().toLowerCase();
|
|
607
|
+
if (!level) {
|
|
608
|
+
printLine(chalk.cyan('Current effort: ') + currentEffort);
|
|
609
|
+
printLine(chalk.dim(' /effort low ā Quick, concise answers'));
|
|
610
|
+
printLine(chalk.dim(' /effort medium ā Balanced (default)'));
|
|
611
|
+
printLine(chalk.dim(' /effort high ā Deep analysis, thorough'));
|
|
612
|
+
}
|
|
613
|
+
else if (['low', 'medium', 'high'].includes(level)) {
|
|
614
|
+
currentEffort = level;
|
|
615
|
+
updateStatusBar({ thinkingLevel: currentEffort });
|
|
616
|
+
printSuccess(`Effort level: ${currentEffort}`);
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
printError('Invalid effort level. Use: low, medium, high');
|
|
620
|
+
}
|
|
621
|
+
showPrompt();
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
// āāā /copy [n] āāā
|
|
625
|
+
if (input.startsWith('/copy')) {
|
|
626
|
+
const indexStr = input.replace('/copy', '').trim();
|
|
627
|
+
let textToCopy = lastResponse;
|
|
628
|
+
if (indexStr) {
|
|
629
|
+
const idx = parseInt(indexStr, 10);
|
|
630
|
+
const assistantMsgs = history.filter(m => m.role === 'assistant' && typeof m.content === 'string');
|
|
631
|
+
if (idx > 0 && idx <= assistantMsgs.length) {
|
|
632
|
+
textToCopy = assistantMsgs[assistantMsgs.length - idx].content;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (!textToCopy) {
|
|
636
|
+
printWarning('Nothing to copy.');
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
// Try platform clipboard
|
|
640
|
+
try {
|
|
641
|
+
const clipCmd = process.platform === 'darwin' ? 'pbcopy'
|
|
642
|
+
: process.platform === 'win32' ? 'clip'
|
|
643
|
+
: 'xclip -selection clipboard';
|
|
644
|
+
execSync(clipCmd, { input: textToCopy, timeout: 3000 });
|
|
645
|
+
printSuccess('Copied to clipboard!');
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
// Fallback: write to file
|
|
649
|
+
const copyPath = join(getConfigDir(), 'last-copy.txt');
|
|
650
|
+
writeFileSync(copyPath, textToCopy);
|
|
651
|
+
printWarning(`Clipboard unavailable. Saved to ${copyPath}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
showPrompt();
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
// āāā /context āāā
|
|
658
|
+
if (input === '/context') {
|
|
659
|
+
const msgCount = history.length;
|
|
660
|
+
let totalChars = 0;
|
|
661
|
+
let toolResultChars = 0;
|
|
662
|
+
const roleCounts = {};
|
|
663
|
+
for (const msg of history) {
|
|
664
|
+
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
665
|
+
totalChars += content.length;
|
|
666
|
+
roleCounts[msg.role] = (roleCounts[msg.role] || 0) + 1;
|
|
667
|
+
if (msg.role === 'tool')
|
|
668
|
+
toolResultChars += content.length;
|
|
669
|
+
}
|
|
670
|
+
const estTokens = Math.ceil(totalChars / 3.5);
|
|
671
|
+
const maxContext = 200000; // approximate
|
|
672
|
+
const usage = (estTokens / maxContext * 100).toFixed(1);
|
|
673
|
+
printLine(chalk.cyan.bold('\nš Context Analysis\n'));
|
|
674
|
+
printLine(` Messages: ${msgCount}`);
|
|
675
|
+
printLine(` Est. Tokens: ~${estTokens.toLocaleString()} / ${maxContext.toLocaleString()} (${usage}%)`);
|
|
676
|
+
printLine('');
|
|
677
|
+
for (const [role, count] of Object.entries(roleCounts)) {
|
|
678
|
+
printLine(` ${role.padEnd(12)} ${count} messages`);
|
|
679
|
+
}
|
|
680
|
+
if (toolResultChars > totalChars * 0.6) {
|
|
681
|
+
printLine(chalk.yellow('\n ā Tool results are >60% of context. Consider /compact to free space.'));
|
|
682
|
+
}
|
|
683
|
+
if (estTokens > maxContext * 0.75) {
|
|
684
|
+
printLine(chalk.red('\n š“ Context usage >75%. Run /compact soon!'));
|
|
685
|
+
}
|
|
686
|
+
else if (estTokens > maxContext * 0.5) {
|
|
687
|
+
printLine(chalk.yellow('\n š” Context usage >50%. Keep an eye on it.'));
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
printLine(chalk.green('\n š¢ Context usage healthy.'));
|
|
691
|
+
}
|
|
692
|
+
printLine();
|
|
693
|
+
showPrompt();
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
// āāā /rename <name> āāā
|
|
697
|
+
if (input.startsWith('/rename')) {
|
|
698
|
+
const name = input.replace('/rename', '').trim();
|
|
699
|
+
if (!name) {
|
|
700
|
+
printLine(chalk.dim(`Current name: ${sessionName || '(unnamed)'}`));
|
|
701
|
+
printLine(chalk.dim('Usage: /rename <name>'));
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
sessionName = name;
|
|
705
|
+
printSuccess(`Session renamed: ${sessionName}`);
|
|
706
|
+
}
|
|
707
|
+
showPrompt();
|
|
481
708
|
continue;
|
|
482
709
|
}
|
|
483
710
|
if (input === '/history') {
|
|
484
711
|
printLine(`Turns: ${history.filter(m => m.role === 'user').length}`);
|
|
485
|
-
|
|
712
|
+
showPrompt();
|
|
486
713
|
continue;
|
|
487
714
|
}
|
|
488
715
|
// āāā /resume āāā
|
|
@@ -490,7 +717,7 @@ async function runRepl() {
|
|
|
490
717
|
const sessions = listSessions(10);
|
|
491
718
|
if (sessions.length === 0) {
|
|
492
719
|
printWarning('No saved sessions.');
|
|
493
|
-
|
|
720
|
+
showPrompt();
|
|
494
721
|
continue;
|
|
495
722
|
}
|
|
496
723
|
printLine(chalk.cyan.bold('\nš¾ Recent Sessions:\n'));
|
|
@@ -518,20 +745,20 @@ async function runRepl() {
|
|
|
518
745
|
printError('Failed to load session.');
|
|
519
746
|
}
|
|
520
747
|
}
|
|
521
|
-
|
|
748
|
+
showPrompt();
|
|
522
749
|
continue;
|
|
523
750
|
}
|
|
524
751
|
// āāā /insight āāā
|
|
525
752
|
if (input === '/insight') {
|
|
526
753
|
printLine(generateInsight(history, sessionStartTime, [...new Set(matchedSkills)]));
|
|
527
|
-
|
|
754
|
+
showPrompt();
|
|
528
755
|
continue;
|
|
529
756
|
}
|
|
530
|
-
// āāā /agents
|
|
757
|
+
// āāā /agents, /bg āāā
|
|
531
758
|
if (input === '/agents' || input === '/bg') {
|
|
532
759
|
const agents = listSubAgents(10);
|
|
533
760
|
if (agents.length === 0) {
|
|
534
|
-
printLine(chalk.dim('No sub-agents. Use:
|
|
761
|
+
printLine(chalk.dim('No sub-agents. Use: /spawn <task>'));
|
|
535
762
|
}
|
|
536
763
|
else {
|
|
537
764
|
printLine(chalk.cyan.bold('\nš¤ Sub-Agents:\n'));
|
|
@@ -542,10 +769,9 @@ async function runRepl() {
|
|
|
542
769
|
}
|
|
543
770
|
}
|
|
544
771
|
printLine();
|
|
545
|
-
|
|
772
|
+
showPrompt();
|
|
546
773
|
continue;
|
|
547
774
|
}
|
|
548
|
-
// āāā /agents show <id> āāā
|
|
549
775
|
if (input.startsWith('/agents show ')) {
|
|
550
776
|
const id = input.replace('/agents show ', '').trim();
|
|
551
777
|
const agent = getSubAgent(id);
|
|
@@ -561,10 +787,9 @@ async function runRepl() {
|
|
|
561
787
|
printLine(chalk.red(`Error: ${agent.error}`));
|
|
562
788
|
}
|
|
563
789
|
printLine();
|
|
564
|
-
|
|
790
|
+
showPrompt();
|
|
565
791
|
continue;
|
|
566
792
|
}
|
|
567
|
-
// āāā /spawn <task> āāā
|
|
568
793
|
if (input.startsWith('/spawn ')) {
|
|
569
794
|
const task = input.replace('/spawn ', '').trim();
|
|
570
795
|
if (!task) {
|
|
@@ -579,9 +804,10 @@ async function runRepl() {
|
|
|
579
804
|
printSuccess(`Sub-agent ${result.id} spawned! Check with /agents`);
|
|
580
805
|
}
|
|
581
806
|
}
|
|
582
|
-
|
|
807
|
+
showPrompt();
|
|
583
808
|
continue;
|
|
584
809
|
}
|
|
810
|
+
// āāā /compact āāā
|
|
585
811
|
if (input === '/compact') {
|
|
586
812
|
const userCount = history.filter(m => m.role === 'user').length;
|
|
587
813
|
if (userCount > 4) {
|
|
@@ -591,11 +817,12 @@ async function runRepl() {
|
|
|
591
817
|
const compactResult = await runAgent('Perform a nine-section context compression. Analyze the conversation so far and produce a structured summary covering: ' +
|
|
592
818
|
'1. Main requests/intent 2. Key technical concepts 3. Files and code 4. Errors and fixes 5. Problem resolution ' +
|
|
593
819
|
'6. All user messages 7. Pending tasks 8. Current work state 9. Next steps (with citations). ' +
|
|
594
|
-
'Output the summary directly, do not call any tools.', history, { signal: abortController.signal });
|
|
820
|
+
'Output the summary directly, do not call any tools.', history, { signal: abortController.signal, model: currentModel, effort: currentEffort });
|
|
595
821
|
history = [
|
|
596
822
|
{ role: 'user', content: 'Below is a compressed summary of our prior conversation. Continue from here.' },
|
|
597
823
|
{ role: 'assistant', content: compactResult.response },
|
|
598
824
|
];
|
|
825
|
+
autoCompactTriggered = false;
|
|
599
826
|
printSuccess('Context compacted (nine-section summary)');
|
|
600
827
|
}
|
|
601
828
|
catch (e) {
|
|
@@ -609,13 +836,14 @@ async function runRepl() {
|
|
|
609
836
|
else {
|
|
610
837
|
printWarning('Conversation too short to compact');
|
|
611
838
|
}
|
|
612
|
-
|
|
839
|
+
showPrompt();
|
|
613
840
|
continue;
|
|
614
841
|
}
|
|
842
|
+
// āāā /dream āāā
|
|
615
843
|
if (input === '/dream') {
|
|
616
844
|
abortController = new AbortController();
|
|
617
845
|
try {
|
|
618
|
-
const result = await runAgent('Perform memory consolidation: scan recent memories and conversations, extract recurring patterns and promote to long-term memory, merge redundant entries, clean up completed tasks. Use search_memory and save_memory tools. Report what you consolidated.', history, { signal: abortController.signal });
|
|
846
|
+
const result = await runAgent('Perform memory consolidation: scan recent memories and conversations, extract recurring patterns and promote to long-term memory, merge redundant entries, clean up completed tasks. Use search_memory and save_memory tools. Report what you consolidated.', history, { signal: abortController.signal, model: currentModel });
|
|
619
847
|
history = result.history;
|
|
620
848
|
}
|
|
621
849
|
catch (e) {
|
|
@@ -624,23 +852,27 @@ async function runRepl() {
|
|
|
624
852
|
}
|
|
625
853
|
abortController = null;
|
|
626
854
|
printLine();
|
|
627
|
-
|
|
855
|
+
showPrompt();
|
|
628
856
|
continue;
|
|
629
857
|
}
|
|
858
|
+
// āāā /status āāā
|
|
630
859
|
if (input === '/status') {
|
|
631
|
-
const cfg = loadConfig();
|
|
632
860
|
const turns = history.filter(m => m.role === 'user').length;
|
|
633
861
|
printLine(chalk.cyan('š Session Status:'));
|
|
634
|
-
printLine(` Model:
|
|
635
|
-
printLine(`
|
|
636
|
-
printLine(`
|
|
637
|
-
printLine(`
|
|
638
|
-
|
|
862
|
+
printLine(` Model: ${currentModel}`);
|
|
863
|
+
printLine(` Effort: ${currentEffort}`);
|
|
864
|
+
printLine(` Permission: ${currentPermissionMode}`);
|
|
865
|
+
printLine(` Turns: ${turns}`);
|
|
866
|
+
printLine(` Messages: ${history.length}`);
|
|
867
|
+
printLine(` CWD: ${process.cwd()}`);
|
|
868
|
+
if (sessionName)
|
|
869
|
+
printLine(` Name: ${sessionName}`);
|
|
870
|
+
showPrompt();
|
|
639
871
|
continue;
|
|
640
872
|
}
|
|
641
873
|
if (input === '/plan') {
|
|
642
874
|
printLine(getCurrentPlan());
|
|
643
|
-
|
|
875
|
+
showPrompt();
|
|
644
876
|
continue;
|
|
645
877
|
}
|
|
646
878
|
if (input === '/knowledge') {
|
|
@@ -649,7 +881,7 @@ async function runRepl() {
|
|
|
649
881
|
const icon = f.type === 'always' ? 'šµ' : f.type === 'on-demand' ? 'š”' : 'š¢';
|
|
650
882
|
printLine(` ${icon} ${f.file} (${f.type})`);
|
|
651
883
|
}
|
|
652
|
-
|
|
884
|
+
showPrompt();
|
|
653
885
|
continue;
|
|
654
886
|
}
|
|
655
887
|
if (input === '/skills') {
|
|
@@ -658,44 +890,69 @@ async function runRepl() {
|
|
|
658
890
|
const icon = s.enabled ? 'ā
' : 'ā';
|
|
659
891
|
printLine(` ${icon} ${s.name} ā ${s.description}`);
|
|
660
892
|
}
|
|
661
|
-
|
|
893
|
+
showPrompt();
|
|
662
894
|
continue;
|
|
663
895
|
}
|
|
896
|
+
// āāā /help āāā
|
|
664
897
|
if (input === '/help') {
|
|
665
898
|
printLine(chalk.cyan.bold('Commands:'));
|
|
666
899
|
printLine('');
|
|
667
900
|
printLine(chalk.dim(' Session'));
|
|
668
|
-
printLine(' /new
|
|
669
|
-
printLine(' /clear
|
|
670
|
-
printLine(' /compact
|
|
671
|
-
printLine(' /resume
|
|
672
|
-
printLine(' /
|
|
901
|
+
printLine(' /new Start new conversation');
|
|
902
|
+
printLine(' /clear Clear conversation history');
|
|
903
|
+
printLine(' /compact Compress context (nine-section)');
|
|
904
|
+
printLine(' /resume Restore a previous session');
|
|
905
|
+
printLine(' /rename <n> Rename current session');
|
|
906
|
+
printLine(' /quit Exit');
|
|
907
|
+
printLine('');
|
|
908
|
+
printLine(chalk.dim(' Model & Effort'));
|
|
909
|
+
printLine(' /model <n> Switch model');
|
|
910
|
+
printLine(' /effort <l> Set thinking effort (low/medium/high)');
|
|
673
911
|
printLine('');
|
|
674
912
|
printLine(chalk.dim(' Analysis'));
|
|
675
|
-
printLine(' /insight
|
|
676
|
-
printLine(' /
|
|
677
|
-
printLine(' /
|
|
913
|
+
printLine(' /insight Session analytics (tokens, tools, skills)');
|
|
914
|
+
printLine(' /context Context usage analysis');
|
|
915
|
+
printLine(' /status Session status');
|
|
916
|
+
printLine(' /copy [n] Copy last response to clipboard');
|
|
917
|
+
printLine(' /plan Show current task plan');
|
|
678
918
|
printLine('');
|
|
679
919
|
printLine(chalk.dim(' Sub-Agents'));
|
|
680
|
-
printLine(' /spawn <
|
|
681
|
-
printLine(' /agents
|
|
682
|
-
printLine(' /agents show <id>
|
|
920
|
+
printLine(' /spawn <t> Run task in background sub-agent');
|
|
921
|
+
printLine(' /agents List sub-agents');
|
|
922
|
+
printLine(' /agents show <id> Show sub-agent result');
|
|
683
923
|
printLine('');
|
|
684
924
|
printLine(chalk.dim(' Knowledge'));
|
|
685
|
-
printLine(' /knowledge
|
|
686
|
-
printLine(' /skills
|
|
687
|
-
printLine(' /dream
|
|
688
|
-
printLine('
|
|
689
|
-
|
|
925
|
+
printLine(' /knowledge List knowledge files');
|
|
926
|
+
printLine(' /skills List skills');
|
|
927
|
+
printLine(' /dream Memory consolidation');
|
|
928
|
+
printLine('');
|
|
929
|
+
printLine(chalk.dim(' CLI Flags'));
|
|
930
|
+
printLine(' bobo -p "q" Non-interactive (supports piping)');
|
|
931
|
+
printLine(' bobo -c Continue last conversation');
|
|
932
|
+
printLine(' bobo -r <id> Resume specific session');
|
|
933
|
+
printLine(' bobo --full-auto Auto-approve tool calls');
|
|
934
|
+
printLine(' bobo --yolo No sandbox, no approvals');
|
|
935
|
+
showPrompt();
|
|
690
936
|
continue;
|
|
691
937
|
}
|
|
938
|
+
// āāā Run agent āāā
|
|
692
939
|
abortController = new AbortController();
|
|
693
940
|
try {
|
|
694
941
|
const result = await runAgent(input, history, {
|
|
695
942
|
signal: abortController.signal,
|
|
696
943
|
matchedSkills,
|
|
944
|
+
model: currentModel,
|
|
945
|
+
effort: currentEffort,
|
|
946
|
+
permissionMode: currentPermissionMode,
|
|
947
|
+
onAutoCompact: () => {
|
|
948
|
+
if (!autoCompactTriggered) {
|
|
949
|
+
autoCompactTriggered = true;
|
|
950
|
+
printLine(chalk.yellow('\nā Context is getting large. Consider running /compact to free space.\n'));
|
|
951
|
+
}
|
|
952
|
+
},
|
|
697
953
|
});
|
|
698
954
|
history = result.history;
|
|
955
|
+
lastResponse = result.response;
|
|
699
956
|
}
|
|
700
957
|
catch (e) {
|
|
701
958
|
if (e.message !== 'Aborted') {
|
|
@@ -704,7 +961,7 @@ async function runRepl() {
|
|
|
704
961
|
}
|
|
705
962
|
abortController = null;
|
|
706
963
|
printLine();
|
|
707
|
-
|
|
964
|
+
showPrompt();
|
|
708
965
|
}
|
|
709
966
|
}
|
|
710
967
|
program.parse();
|