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/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
- .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) => {
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 to ~/.bobo/skills/ (including scripts/ subdirs)
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 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')}`);
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 (sub-agent) ────────────────────────────
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
- .command('enable <name>')
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
- .command('disable <name>')
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
- .command('import <path>')
347
- .description('Batch import skills from an OpenClaw skills directory')
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
- .command('init')
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
- // ─── REPL mode ───────────────────────────────────────────────
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: config.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
- // Check for resumable session
470
+ // Restore session
402
471
  let history = [];
403
- const recentSession = getRecentSession(3600000); // 1 hour
404
- if (recentSession && recentSession.messages.length > 0) {
405
- printLine(chalk.yellow(`šŸ’¾ Found recent session (${recentSession.messageCount} messages, ${recentSession.firstUserMessage.slice(0, 50)}...)`));
406
- printLine(chalk.dim(' Resume? (y/n)'));
407
- // Quick y/n prompt
408
- const answer = await new Promise((resolve) => {
409
- const tmpRl = createInterface({ input: process.stdin, output: process.stdout });
410
- tmpRl.question(chalk.green('> '), (ans) => {
411
- tmpRl.close();
412
- 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
+ });
413
506
  });
414
- });
415
- if (answer === 'y' || answer === 'yes') {
416
- history = recentSession.messages;
417
- printSuccess(`Resumed session (${history.length} messages)`);
507
+ if (answer === 'y' || answer === 'yes') {
508
+ history = recentSession.messages;
509
+ printSuccess(`Resumed session (${history.length} messages)`);
510
+ }
418
511
  }
419
512
  }
420
- // Enable bottom status bar (Claude Code style)
513
+ // Enable status bar
421
514
  if (process.stdout.isTTY) {
422
515
  setupResizeHandler();
423
516
  enableStatusBar({
424
- model: config.model,
425
- thinkingLevel: 'medium',
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
- // 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
+ 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
- rl.prompt();
550
+ showPrompt();
450
551
  }
451
552
  else {
452
553
  printLine(chalk.dim('\n(press Ctrl+C again or Ctrl+D to exit)'));
453
- rl.prompt();
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
- rl.prompt();
563
+ showPrompt();
463
564
  for await (const line of rl) {
464
565
  const input = line.trim();
465
566
  if (!input) {
466
- rl.prompt();
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
- 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();
481
708
  continue;
482
709
  }
483
710
  if (input === '/history') {
484
711
  printLine(`Turns: ${history.filter(m => m.role === 'user').length}`);
485
- rl.prompt();
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
- rl.prompt();
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
- rl.prompt();
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
- rl.prompt();
754
+ showPrompt();
528
755
  continue;
529
756
  }
530
- // ─── /agents or /bg ───
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: bobo spawn "task" or type: /spawn <task>'));
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
- rl.prompt();
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
- rl.prompt();
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
- rl.prompt();
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
- rl.prompt();
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
- rl.prompt();
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: ${cfg.model}`);
635
- printLine(` Turns: ${turns}`);
636
- printLine(` Messages: ${history.length}`);
637
- printLine(` CWD: ${process.cwd()}`);
638
- 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();
639
871
  continue;
640
872
  }
641
873
  if (input === '/plan') {
642
874
  printLine(getCurrentPlan());
643
- rl.prompt();
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
- rl.prompt();
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
- rl.prompt();
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 — Start new conversation');
669
- printLine(' /clear — Clear conversation history');
670
- printLine(' /compact — Compress context (nine-section)');
671
- printLine(' /resume — Restore a previous session');
672
- 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)');
673
911
  printLine('');
674
912
  printLine(chalk.dim(' Analysis'));
675
- printLine(' /insight — Session analytics (tokens, tools, skills)');
676
- printLine(' /status — Session status');
677
- 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');
678
918
  printLine('');
679
919
  printLine(chalk.dim(' Sub-Agents'));
680
- printLine(' /spawn <task> — Run a task in background sub-agent');
681
- printLine(' /agents — List sub-agents');
682
- 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');
683
923
  printLine('');
684
924
  printLine(chalk.dim(' Knowledge'));
685
- printLine(' /knowledge — List knowledge files');
686
- printLine(' /skills — List skills');
687
- printLine(' /dream — Memory consolidation');
688
- printLine(' /help — Show this help');
689
- 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();
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
- rl.prompt();
964
+ showPrompt();
708
965
  }
709
966
  }
710
967
  program.parse();