bobo-ai-cli 1.2.0 → 1.4.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/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,6 +20,8 @@ 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, updateStatusBar, setupResizeHandler, renderStatusBar } from './statusbar.js';
24
+ import { slashCompleter } from './completer.js';
23
25
  import chalk from 'chalk';
24
26
  const __dirname = fileURLToPath(new URL('.', import.meta.url));
25
27
  let version = '0.1.0';
@@ -34,13 +36,46 @@ program
34
36
  .description('šŸ• Bobo CLI — Portable AI Engineering Assistant')
35
37
  .version(version)
36
38
  .argument('[prompt...]', 'Run a one-shot prompt without entering REPL')
37
- .action(async (promptParts) => {
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) => {
38
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
39
64
  if (prompt) {
40
- await runOneShot(prompt);
65
+ await runOneShot(prompt, {
66
+ model: opts.model,
67
+ effort: opts.effort,
68
+ permissionMode,
69
+ });
41
70
  }
42
71
  else {
43
- await runRepl();
72
+ await runRepl({
73
+ continueSession: opts.continue,
74
+ resumeId: opts.resume,
75
+ model: opts.model,
76
+ effort: opts.effort,
77
+ permissionMode,
78
+ });
44
79
  }
45
80
  });
46
81
  // ─── Config subcommand ───────────────────────────────────────
@@ -112,7 +147,7 @@ program
112
147
  }
113
148
  }
114
149
  initSkills();
115
- // Copy bundled skills to ~/.bobo/skills/ (including scripts/ subdirs)
150
+ // Copy bundled skills (including scripts/ subdirs)
116
151
  const bundledSkillsDir = join(__dirname, '..', 'bundled-skills');
117
152
  const userSkillsDir = join(getConfigDir(), 'skills');
118
153
  if (existsSync(bundledSkillsDir)) {
@@ -131,13 +166,11 @@ program
131
166
  continue;
132
167
  }
133
168
  if (!existsSync(dest)) {
134
- // Use cpSync recursive — copies everything including scripts/
135
169
  cpSync(src, dest, { recursive: true });
136
170
  installed++;
137
171
  }
138
172
  }
139
173
  if (installed > 0) {
140
- // All skills enabled by default — passive triggering based on context
141
174
  const manifestPath = join(getConfigDir(), 'skills-manifest.json');
142
175
  let manifest = {};
143
176
  try {
@@ -157,6 +190,22 @@ program
157
190
  printSuccess(`${installed} skills installed (all enabled, passive triggering)`);
158
191
  }
159
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
+ }
160
209
  printSuccess(`Initialized ${getConfigDir()}`);
161
210
  printLine(`Knowledge: ${knowledgeDir}`);
162
211
  printWarning('Configure your API key: bobo config set apiKey <your-key>');
@@ -190,7 +239,6 @@ program
190
239
  allGood = false;
191
240
  }
192
241
  }
193
- // Check API key
194
242
  const config = loadConfig();
195
243
  if (config.apiKey) {
196
244
  printLine(` ${chalk.green('āœ“')} ${'API Key'.padEnd(12)} ${chalk.dim('configured')}`);
@@ -199,7 +247,9 @@ program
199
247
  printLine(` ${chalk.red('āœ—')} ${'API Key'.padEnd(12)} ${chalk.red('not set — run: bobo config set apiKey <key>')}`);
200
248
  allGood = false;
201
249
  }
202
- // Check skills directory
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')}`);
203
253
  const skillsDir = join(getConfigDir(), 'skills');
204
254
  if (existsSync(skillsDir)) {
205
255
  const count = readdirSync(skillsDir).filter(f => {
@@ -224,7 +274,7 @@ program
224
274
  }
225
275
  printLine();
226
276
  });
227
- // ─── Spawn subcommand (sub-agent) ────────────────────────────
277
+ // ─── Spawn subcommand ────────────────────────────────────────
228
278
  program
229
279
  .command('spawn <task>')
230
280
  .description('Spawn a background sub-agent to run a task')
@@ -282,7 +332,6 @@ agentsCmd
282
332
  }
283
333
  printLine();
284
334
  });
285
- // Default agents action: list
286
335
  agentsCmd.action(() => {
287
336
  const agents = listSubAgents();
288
337
  if (agents.length === 0) {
@@ -313,10 +362,7 @@ program
313
362
  });
314
363
  // ─── Skill subcommand ────────────────────────────────────────
315
364
  const skillCmd = program.command('skill').description('Manage skills');
316
- skillCmd
317
- .command('list')
318
- .description('List all skills')
319
- .action(() => {
365
+ skillCmd.command('list').description('List all skills').action(() => {
320
366
  const skills = listSkills();
321
367
  console.log(chalk.cyan.bold('\n🧩 Skills:\n'));
322
368
  for (const s of skills) {
@@ -326,29 +372,15 @@ skillCmd
326
372
  }
327
373
  console.log();
328
374
  });
329
- skillCmd
330
- .command('enable <name>')
331
- .description('Enable a skill')
332
- .action((name) => {
333
- const result = setSkillEnabled(name, true);
334
- console.log(result);
375
+ skillCmd.command('enable <name>').description('Enable a skill').action((name) => {
376
+ console.log(setSkillEnabled(name, true));
335
377
  });
336
- skillCmd
337
- .command('disable <name>')
338
- .description('Disable a skill')
339
- .action((name) => {
340
- const result = setSkillEnabled(name, false);
341
- console.log(result);
378
+ skillCmd.command('disable <name>').description('Disable a skill').action((name) => {
379
+ console.log(setSkillEnabled(name, false));
342
380
  });
343
- skillCmd
344
- .command('import <path>')
345
- .description('Batch import skills from an OpenClaw skills directory')
346
- .action((path) => {
347
- const resolved = path.startsWith('~')
348
- ? join(process.env.HOME || '', path.slice(1))
349
- : path;
350
- const result = importSkills(resolved);
351
- 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));
352
384
  });
353
385
  // ─── Structured knowledge commands ──────────────────────────
354
386
  registerKnowledgeCommand(program);
@@ -357,17 +389,47 @@ registerStructuredSkillsCommand(program);
357
389
  registerStructuredTemplateCommand(program);
358
390
  // ─── Project subcommand ──────────────────────────────────────
359
391
  const projectCmd = program.command('project').description('Manage project configuration');
360
- projectCmd
361
- .command('init')
362
- .description('Initialize .bobo/ project config in current directory')
363
- .action(() => {
364
- const result = initProject();
365
- printSuccess(result);
392
+ projectCmd.command('init').description('Initialize .bobo/ project config').action(() => {
393
+ printSuccess(initProject());
366
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
+ }
367
425
  // ─── One-shot mode ───────────────────────────────────────────
368
- async function runOneShot(prompt) {
426
+ async function runOneShot(prompt, opts) {
369
427
  try {
370
- await runAgent(prompt, []);
428
+ await runAgent(prompt, [], {
429
+ model: opts.model,
430
+ effort: opts.effort,
431
+ permissionMode: opts.permissionMode,
432
+ });
371
433
  }
372
434
  catch (e) {
373
435
  if (e.message !== 'Aborted') {
@@ -376,52 +438,104 @@ async function runOneShot(prompt) {
376
438
  }
377
439
  }
378
440
  }
379
- // ─── REPL mode ───────────────────────────────────────────────
380
- async function runRepl() {
441
+ async function runRepl(opts) {
381
442
  const config = loadConfig();
382
443
  const skills = listSkills();
383
444
  const knowledgeFiles = listKnowledgeFiles();
384
445
  const sessionStartTime = Date.now();
385
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 = '';
386
452
  printWelcome({
387
453
  version,
388
- model: config.model,
454
+ model: currentModel,
389
455
  toolCount: toolDefinitions.length,
390
456
  skillsActive: skills.filter(s => s.enabled).length,
391
457
  skillsTotal: skills.length,
392
458
  knowledgeCount: knowledgeFiles.length,
393
459
  cwd: process.cwd(),
394
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
+ }
395
466
  if (!config.apiKey) {
396
467
  printWarning('API key not configured. Run: bobo config set apiKey <your-key>');
397
468
  printLine();
398
469
  }
399
- // Check for resumable session
470
+ // Restore session
400
471
  let history = [];
401
- const recentSession = getRecentSession(3600000); // 1 hour
402
- if (recentSession && recentSession.messages.length > 0) {
403
- printLine(chalk.yellow(`šŸ’¾ Found recent session (${recentSession.messageCount} messages, ${recentSession.firstUserMessage.slice(0, 50)}...)`));
404
- printLine(chalk.dim(' Resume? (y/n)'));
405
- // Quick y/n prompt
406
- const answer = await new Promise((resolve) => {
407
- const tmpRl = createInterface({ input: process.stdin, output: process.stdout });
408
- tmpRl.question(chalk.green('> '), (ans) => {
409
- tmpRl.close();
410
- resolve(ans.trim().toLowerCase());
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
+ });
411
506
  });
412
- });
413
- if (answer === 'y' || answer === 'yes') {
414
- history = recentSession.messages;
415
- printSuccess(`Resumed session (${history.length} messages)`);
507
+ if (answer === 'y' || answer === 'yes') {
508
+ history = recentSession.messages;
509
+ printSuccess(`Resumed session (${history.length} messages)`);
510
+ }
416
511
  }
417
512
  }
513
+ // Enable status bar
514
+ if (process.stdout.isTTY) {
515
+ setupResizeHandler();
516
+ enableStatusBar({
517
+ model: currentModel,
518
+ thinkingLevel: currentEffort,
519
+ skillsCount: skills.filter(s => s.enabled).length,
520
+ cwd: process.cwd(),
521
+ });
522
+ }
418
523
  const rl = createInterface({
419
524
  input: process.stdin,
420
525
  output: process.stdout,
421
526
  prompt: chalk.green('> '),
527
+ completer: slashCompleter,
422
528
  });
423
529
  let abortController = null;
424
- // Auto-save on exit
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
+ showPrompt();
538
+ };
425
539
  const autoSave = () => {
426
540
  if (history.length > 0) {
427
541
  const id = saveSession(history, process.cwd());
@@ -433,41 +547,169 @@ async function runRepl() {
433
547
  abortController.abort();
434
548
  abortController = null;
435
549
  printLine(chalk.dim('\n(cancelled)'));
436
- rl.prompt();
550
+ showPrompt();
437
551
  }
438
552
  else {
439
553
  printLine(chalk.dim('\n(press Ctrl+C again or Ctrl+D to exit)'));
440
- rl.prompt();
554
+ showPrompt();
441
555
  }
442
556
  });
443
557
  rl.on('close', () => {
444
558
  autoSave();
559
+ disableStatusBar();
445
560
  printLine(chalk.dim('\nGoodbye! šŸ•'));
446
561
  process.exit(0);
447
562
  });
448
- rl.prompt();
563
+ showPrompt();
449
564
  for await (const line of rl) {
450
565
  const input = line.trim();
451
566
  if (!input) {
452
- rl.prompt();
567
+ showPrompt();
453
568
  continue;
454
569
  }
570
+ // ─── Exit ───
455
571
  if (input === '/quit' || input === '/exit') {
456
572
  autoSave();
573
+ disableStatusBar();
457
574
  printLine(chalk.dim('Goodbye! šŸ•'));
458
575
  process.exit(0);
459
576
  }
577
+ // ─── /new, /clear ───
460
578
  if (input === '/clear' || input === '/new') {
461
579
  history = [];
462
580
  matchedSkills.length = 0;
581
+ lastResponse = '';
582
+ autoCompactTriggered = false;
463
583
  resetPlan();
464
584
  printSuccess('Conversation cleared');
465
- rl.prompt();
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();
466
708
  continue;
467
709
  }
468
710
  if (input === '/history') {
469
711
  printLine(`Turns: ${history.filter(m => m.role === 'user').length}`);
470
- rl.prompt();
712
+ showPrompt();
471
713
  continue;
472
714
  }
473
715
  // ─── /resume ───
@@ -475,7 +717,7 @@ async function runRepl() {
475
717
  const sessions = listSessions(10);
476
718
  if (sessions.length === 0) {
477
719
  printWarning('No saved sessions.');
478
- rl.prompt();
720
+ showPrompt();
479
721
  continue;
480
722
  }
481
723
  printLine(chalk.cyan.bold('\nšŸ’¾ Recent Sessions:\n'));
@@ -503,20 +745,20 @@ async function runRepl() {
503
745
  printError('Failed to load session.');
504
746
  }
505
747
  }
506
- rl.prompt();
748
+ showPrompt();
507
749
  continue;
508
750
  }
509
751
  // ─── /insight ───
510
752
  if (input === '/insight') {
511
753
  printLine(generateInsight(history, sessionStartTime, [...new Set(matchedSkills)]));
512
- rl.prompt();
754
+ showPrompt();
513
755
  continue;
514
756
  }
515
- // ─── /agents or /bg ───
757
+ // ─── /agents, /bg ───
516
758
  if (input === '/agents' || input === '/bg') {
517
759
  const agents = listSubAgents(10);
518
760
  if (agents.length === 0) {
519
- printLine(chalk.dim('No sub-agents. Use: bobo spawn "task" or type: /spawn <task>'));
761
+ printLine(chalk.dim('No sub-agents. Use: /spawn <task>'));
520
762
  }
521
763
  else {
522
764
  printLine(chalk.cyan.bold('\nšŸ¤– Sub-Agents:\n'));
@@ -527,10 +769,9 @@ async function runRepl() {
527
769
  }
528
770
  }
529
771
  printLine();
530
- rl.prompt();
772
+ showPrompt();
531
773
  continue;
532
774
  }
533
- // ─── /agents show <id> ───
534
775
  if (input.startsWith('/agents show ')) {
535
776
  const id = input.replace('/agents show ', '').trim();
536
777
  const agent = getSubAgent(id);
@@ -546,10 +787,9 @@ async function runRepl() {
546
787
  printLine(chalk.red(`Error: ${agent.error}`));
547
788
  }
548
789
  printLine();
549
- rl.prompt();
790
+ showPrompt();
550
791
  continue;
551
792
  }
552
- // ─── /spawn <task> ───
553
793
  if (input.startsWith('/spawn ')) {
554
794
  const task = input.replace('/spawn ', '').trim();
555
795
  if (!task) {
@@ -564,9 +804,10 @@ async function runRepl() {
564
804
  printSuccess(`Sub-agent ${result.id} spawned! Check with /agents`);
565
805
  }
566
806
  }
567
- rl.prompt();
807
+ showPrompt();
568
808
  continue;
569
809
  }
810
+ // ─── /compact ───
570
811
  if (input === '/compact') {
571
812
  const userCount = history.filter(m => m.role === 'user').length;
572
813
  if (userCount > 4) {
@@ -576,11 +817,12 @@ async function runRepl() {
576
817
  const compactResult = await runAgent('Perform a nine-section context compression. Analyze the conversation so far and produce a structured summary covering: ' +
577
818
  '1. Main requests/intent 2. Key technical concepts 3. Files and code 4. Errors and fixes 5. Problem resolution ' +
578
819
  '6. All user messages 7. Pending tasks 8. Current work state 9. Next steps (with citations). ' +
579
- '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 });
580
821
  history = [
581
822
  { role: 'user', content: 'Below is a compressed summary of our prior conversation. Continue from here.' },
582
823
  { role: 'assistant', content: compactResult.response },
583
824
  ];
825
+ autoCompactTriggered = false;
584
826
  printSuccess('Context compacted (nine-section summary)');
585
827
  }
586
828
  catch (e) {
@@ -594,13 +836,14 @@ async function runRepl() {
594
836
  else {
595
837
  printWarning('Conversation too short to compact');
596
838
  }
597
- rl.prompt();
839
+ showPrompt();
598
840
  continue;
599
841
  }
842
+ // ─── /dream ───
600
843
  if (input === '/dream') {
601
844
  abortController = new AbortController();
602
845
  try {
603
- 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 });
604
847
  history = result.history;
605
848
  }
606
849
  catch (e) {
@@ -609,23 +852,27 @@ async function runRepl() {
609
852
  }
610
853
  abortController = null;
611
854
  printLine();
612
- rl.prompt();
855
+ showPrompt();
613
856
  continue;
614
857
  }
858
+ // ─── /status ───
615
859
  if (input === '/status') {
616
- const cfg = loadConfig();
617
860
  const turns = history.filter(m => m.role === 'user').length;
618
861
  printLine(chalk.cyan('šŸ“Š Session Status:'));
619
- printLine(` Model: ${cfg.model}`);
620
- printLine(` Turns: ${turns}`);
621
- printLine(` Messages: ${history.length}`);
622
- printLine(` CWD: ${process.cwd()}`);
623
- rl.prompt();
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();
624
871
  continue;
625
872
  }
626
873
  if (input === '/plan') {
627
874
  printLine(getCurrentPlan());
628
- rl.prompt();
875
+ showPrompt();
629
876
  continue;
630
877
  }
631
878
  if (input === '/knowledge') {
@@ -634,7 +881,7 @@ async function runRepl() {
634
881
  const icon = f.type === 'always' ? 'šŸ”µ' : f.type === 'on-demand' ? '🟔' : '🟢';
635
882
  printLine(` ${icon} ${f.file} (${f.type})`);
636
883
  }
637
- rl.prompt();
884
+ showPrompt();
638
885
  continue;
639
886
  }
640
887
  if (input === '/skills') {
@@ -643,44 +890,69 @@ async function runRepl() {
643
890
  const icon = s.enabled ? 'āœ…' : 'āŒ';
644
891
  printLine(` ${icon} ${s.name} — ${s.description}`);
645
892
  }
646
- rl.prompt();
893
+ showPrompt();
647
894
  continue;
648
895
  }
896
+ // ─── /help ───
649
897
  if (input === '/help') {
650
898
  printLine(chalk.cyan.bold('Commands:'));
651
899
  printLine('');
652
900
  printLine(chalk.dim(' Session'));
653
- printLine(' /new — Start new conversation');
654
- printLine(' /clear — Clear conversation history');
655
- printLine(' /compact — Compress context (nine-section)');
656
- printLine(' /resume — Restore a previous session');
657
- printLine(' /quit — Exit');
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)');
658
911
  printLine('');
659
912
  printLine(chalk.dim(' Analysis'));
660
- printLine(' /insight — Session analytics (tokens, tools, skills)');
661
- printLine(' /status — Session status');
662
- printLine(' /plan — Show current task plan');
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');
663
918
  printLine('');
664
919
  printLine(chalk.dim(' Sub-Agents'));
665
- printLine(' /spawn <task> — Run a task in background sub-agent');
666
- printLine(' /agents — List sub-agents');
667
- printLine(' /agents show <id> — Show sub-agent result');
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');
668
923
  printLine('');
669
924
  printLine(chalk.dim(' Knowledge'));
670
- printLine(' /knowledge — List knowledge files');
671
- printLine(' /skills — List skills');
672
- printLine(' /dream — Memory consolidation');
673
- printLine(' /help — Show this help');
674
- rl.prompt();
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();
675
936
  continue;
676
937
  }
938
+ // ─── Run agent ───
677
939
  abortController = new AbortController();
678
940
  try {
679
941
  const result = await runAgent(input, history, {
680
942
  signal: abortController.signal,
681
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
+ },
682
953
  });
683
954
  history = result.history;
955
+ lastResponse = result.response;
684
956
  }
685
957
  catch (e) {
686
958
  if (e.message !== 'Aborted') {
@@ -689,7 +961,7 @@ async function runRepl() {
689
961
  }
690
962
  abortController = null;
691
963
  printLine();
692
- rl.prompt();
964
+ showPrompt();
693
965
  }
694
966
  }
695
967
  program.parse();