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/agent.d.ts +9 -0
- package/dist/agent.js +86 -13
- package/dist/agent.js.map +1 -1
- package/dist/completer.d.ts +17 -0
- package/dist/completer.js +57 -0
- package/dist/completer.js.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +381 -109
- package/dist/index.js.map +1 -1
- package/dist/statusbar.d.ts +31 -0
- package/dist/statusbar.js +67 -0
- package/dist/statusbar.js.map +1 -0
- 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,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
|
-
.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
345
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
//
|
|
470
|
+
// Restore session
|
|
400
471
|
let history = [];
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
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
|
-
|
|
550
|
+
showPrompt();
|
|
437
551
|
}
|
|
438
552
|
else {
|
|
439
553
|
printLine(chalk.dim('\n(press Ctrl+C again or Ctrl+D to exit)'));
|
|
440
|
-
|
|
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
|
-
|
|
563
|
+
showPrompt();
|
|
449
564
|
for await (const line of rl) {
|
|
450
565
|
const input = line.trim();
|
|
451
566
|
if (!input) {
|
|
452
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
754
|
+
showPrompt();
|
|
513
755
|
continue;
|
|
514
756
|
}
|
|
515
|
-
// āāā /agents
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
620
|
-
printLine(`
|
|
621
|
-
printLine(`
|
|
622
|
-
printLine(`
|
|
623
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
654
|
-
printLine(' /clear
|
|
655
|
-
printLine(' /compact
|
|
656
|
-
printLine(' /resume
|
|
657
|
-
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)');
|
|
658
911
|
printLine('');
|
|
659
912
|
printLine(chalk.dim(' Analysis'));
|
|
660
|
-
printLine(' /insight
|
|
661
|
-
printLine(' /
|
|
662
|
-
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');
|
|
663
918
|
printLine('');
|
|
664
919
|
printLine(chalk.dim(' Sub-Agents'));
|
|
665
|
-
printLine(' /spawn <
|
|
666
|
-
printLine(' /agents
|
|
667
|
-
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');
|
|
668
923
|
printLine('');
|
|
669
924
|
printLine(chalk.dim(' Knowledge'));
|
|
670
|
-
printLine(' /knowledge
|
|
671
|
-
printLine(' /skills
|
|
672
|
-
printLine(' /dream
|
|
673
|
-
printLine('
|
|
674
|
-
|
|
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
|
-
|
|
964
|
+
showPrompt();
|
|
693
965
|
}
|
|
694
966
|
}
|
|
695
967
|
program.parse();
|