agentboss 0.1.2 → 0.1.4

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.
@@ -32,6 +32,7 @@ const { loadAdvice } = require('../llm/advice');
32
32
  const { loadProjectAdvice } = require('../llm/project-advice');
33
33
  const { saveDb } = require('../db/connection');
34
34
  const { runExecutor, canSpawn } = require('./runner');
35
+ const { detectAllCli } = require('../llm/cli-runner');
35
36
  const {
36
37
  buildExecutionPrompt,
37
38
  buildProjectExecutionPrompt,
@@ -318,6 +319,56 @@ function cleanupOrphans(db) {
318
319
  // blocked.
319
320
  // ---------------------------------------------------------------------------
320
321
 
322
+ const VALID_EXECUTORS = new Set(['opencode', 'claude']);
323
+ const VALID_CLI_PREFS = new Set(['auto', 'opencode', 'claude']);
324
+
325
+ /**
326
+ * Read the user's CLI preference from user_settings (no caching — the
327
+ * execution path is low-volume; freshness matters more than micro-perf).
328
+ */
329
+ function getCliPreference(db) {
330
+ const raw = String(getSetting(db, 'llm_tool_preference') || 'auto').toLowerCase();
331
+ return VALID_CLI_PREFS.has(raw) ? raw : 'auto';
332
+ }
333
+
334
+ /**
335
+ * Decide which executor to spawn. Priority:
336
+ * 1. Explicit request override (`executorOpt`) — always wins, even
337
+ * over user preference: an API caller that says "run with claude"
338
+ * gets claude. Validated against the whitelist.
339
+ * 2. User preference (`llm_tool_preference`) — when set to a concrete
340
+ * CLI (opencode / claude), honoured if it's installed; if not
341
+ * installed we fall back to whatever IS, so a stale preference
342
+ * can't silently disable execution.
343
+ * 3. The LLM's recommendation in `item.executor` — used iff the user
344
+ * preference is 'auto' (the default).
345
+ * 4. Hard default `'opencode'`.
346
+ *
347
+ * Returns `null` when no whitelisted CLI is installed at all.
348
+ */
349
+ async function resolveExecutor(db, executorOpt, item) {
350
+ // 1. Explicit override — accept as-is if whitelisted.
351
+ if (executorOpt && VALID_EXECUTORS.has(executorOpt)) return executorOpt;
352
+
353
+ const pref = getCliPreference(db);
354
+ const itemExec = (item && VALID_EXECUTORS.has(item.executor)) ? item.executor : null;
355
+
356
+ // 2. Concrete user preference: try to honour it, fall back if missing.
357
+ if (pref !== 'auto') {
358
+ const all = await detectAllCli();
359
+ const wanted = all.find((c) => c.name === pref);
360
+ if (wanted && wanted.available) return pref;
361
+ // preferred CLI vanished — fall through to itemExec / first available
362
+ const itemAvail = itemExec && all.find((c) => c.name === itemExec && c.available);
363
+ if (itemAvail) return itemExec;
364
+ const anyAvail = all.find((c) => c.available && VALID_EXECUTORS.has(c.name));
365
+ return anyAvail ? anyAvail.name : null;
366
+ }
367
+
368
+ // 3 + 4. Auto: trust the LLM's pick, else default to opencode.
369
+ return itemExec || 'opencode';
370
+ }
371
+
321
372
  function describeCliCommand(executor, cwd) {
322
373
  // What spawnRunAsync → runExecutor will actually invoke. Mirrors
323
374
  // server/execution/runner.js (`opencode run` / `claude -p`). The
@@ -340,7 +391,7 @@ function describeCliCommand(executor, cwd) {
340
391
  };
341
392
  }
342
393
 
343
- function previewExecution(db, { sessionId, adviceKey, executor: executorOpt }) {
394
+ async function previewExecution(db, { sessionId, adviceKey, executor: executorOpt }) {
344
395
  const session = getSessionById(db, sessionId);
345
396
  if (!session) return { ok: false, reason: 'no-session' };
346
397
 
@@ -350,7 +401,7 @@ function previewExecution(db, { sessionId, adviceKey, executor: executorOpt }) {
350
401
  const item = findAdviceItem(advice, adviceKey);
351
402
  if (!item) return { ok: false, reason: 'no-advice-item' };
352
403
 
353
- const executor = executorOpt || item.executor || 'opencode';
404
+ const executor = (await resolveExecutor(db, executorOpt, item)) || 'opencode';
354
405
  // Normalise — session.project may carry an OpenCode-source path like
355
406
  // "C//felix/code/X" that won't pass fs.existsSync; canonicalProject
356
407
  // re-inserts the missing ":". Mirrors previewProjectExecution.
@@ -382,7 +433,7 @@ function previewExecution(db, { sessionId, adviceKey, executor: executorOpt }) {
382
433
  };
383
434
  }
384
435
 
385
- function previewProjectExecution(db, {
436
+ async function previewProjectExecution(db, {
386
437
  project: projectRaw, scope, windowFrom = '', windowTo = '',
387
438
  adviceKey, executor: executorOpt,
388
439
  }) {
@@ -402,7 +453,7 @@ function previewProjectExecution(db, {
402
453
  const item = findAdviceItem(cached.payload, adviceKey);
403
454
  if (!item) return { ok: false, reason: 'no-advice-item' };
404
455
 
405
- const executor = executorOpt || item.executor || 'opencode';
456
+ const executor = (await resolveExecutor(db, executorOpt, item)) || 'opencode';
406
457
 
407
458
  const prompt = buildProjectExecutionPrompt({
408
459
  advice: item,
@@ -491,10 +542,10 @@ async function startExecution(db, opts) {
491
542
  return { ok: false, reason: 'not-whitelisted', extra: { project } };
492
543
  }
493
544
 
494
- // 7. Executor choice & availability.
495
- const executor = executorOpt || item.executor || 'opencode';
496
- if (executor !== 'opencode' && executor !== 'claude') {
497
- return { ok: false, reason: 'no-cli', extra: { executor } };
545
+ // 7. Executor choice & availability — honour user preference.
546
+ const executor = await resolveExecutor(db, executorOpt, item);
547
+ if (!executor || !VALID_EXECUTORS.has(executor)) {
548
+ return { ok: false, reason: 'no-cli', extra: { executor: executor || null } };
498
549
  }
499
550
  const cliOk = await canSpawn(executor);
500
551
  if (!cliOk) return { ok: false, reason: 'no-cli', extra: { executor } };
@@ -614,10 +665,10 @@ async function startProjectExecution(db, opts) {
614
665
  return { ok: false, reason: 'not-whitelisted', extra: { project } };
615
666
  }
616
667
 
617
- // 6. Executor.
618
- const executor = executorOpt || item.executor || 'opencode';
619
- if (executor !== 'opencode' && executor !== 'claude') {
620
- return { ok: false, reason: 'no-cli', extra: { executor } };
668
+ // 6. Executor — honour user preference.
669
+ const executor = await resolveExecutor(db, executorOpt, item);
670
+ if (!executor || !VALID_EXECUTORS.has(executor)) {
671
+ return { ok: false, reason: 'no-cli', extra: { executor: executor || null } };
621
672
  }
622
673
  const cliOk = await canSpawn(executor);
623
674
  if (!cliOk) return { ok: false, reason: 'no-cli', extra: { executor } };
@@ -49,21 +49,28 @@ let _settingsCache = null;
49
49
  let _settingsCacheAt = 0;
50
50
  const SETTINGS_TTL_MS = 10_000;
51
51
 
52
+ const VALID_CLI_PREFS = new Set(['auto', 'opencode', 'claude']);
53
+
52
54
  function getSettings(db) {
53
55
  const now = Date.now();
54
56
  if (_settingsCache && now - _settingsCacheAt < SETTINGS_TTL_MS) {
55
57
  return _settingsCache;
56
58
  }
57
59
  const rows = db.exec(
58
- "SELECT key, value FROM user_settings WHERE key = 'enable_llm_judge'"
60
+ "SELECT key, value FROM user_settings WHERE key IN ('enable_llm_judge', 'llm_tool_preference')"
59
61
  );
60
62
  let enable = false;
63
+ let pref = 'auto';
61
64
  if (rows[0]) {
62
- for (const [, v] of rows[0].values) {
63
- enable = String(v) === '1' || String(v).toLowerCase() === 'true';
65
+ for (const [k, v] of rows[0].values) {
66
+ if (k === 'enable_llm_judge') enable = String(v) === '1' || String(v).toLowerCase() === 'true';
67
+ if (k === 'llm_tool_preference') {
68
+ const p = String(v || '').toLowerCase();
69
+ pref = VALID_CLI_PREFS.has(p) ? p : 'auto';
70
+ }
64
71
  }
65
72
  }
66
- _settingsCache = { enable_llm_judge: enable };
73
+ _settingsCache = { enable_llm_judge: enable, llm_tool_preference: pref };
67
74
  _settingsCacheAt = now;
68
75
  return _settingsCache;
69
76
  }
@@ -257,8 +264,9 @@ async function generateAdvice(db, sessionId, opts = {}) {
257
264
  }
258
265
  }
259
266
 
260
- // 3. CLI detection
261
- const cli = await detectAvailableCli();
267
+ // 3. CLI detection — honour user preference
268
+ const pref = settings.llm_tool_preference || 'auto';
269
+ const cli = await detectAvailableCli(pref);
262
270
  if (!cli) { log('no CLI'); return { ok: false, reason: 'no-cli' }; }
263
271
 
264
272
  // 4–5. truncate + build prompt
@@ -267,7 +275,7 @@ async function generateAdvice(db, sessionId, opts = {}) {
267
275
  log('spawning', cli.name, 'prompt bytes=', prompt.length, 'truncated=', ctx.truncated);
268
276
 
269
277
  // 6. run under concurrency slot — 90 s is plenty for prompts up to ~80 KB
270
- const result = await withSlot(() => runJudge({ prompt, timeoutMs: 90_000 }));
278
+ const result = await withSlot(() => runJudge({ prompt, timeoutMs: 90_000, preferredCli: pref }));
271
279
  log('result ok=', result.ok, 'reason=', result.reason);
272
280
 
273
281
  if (!result.ok) {