@bahulam/code 0.1.24 → 0.1.26
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/package.json +1 -1
- package/src/commands/install.mjs +88 -6
- package/src/commands/plugin-manage.mjs +146 -5
- package/src/config/cli-args.mjs +23 -3
- package/src/config/model-catalog.mjs +9 -0
- package/src/config/model-defaults.mjs +16 -0
- package/src/core/approval.mjs +8 -1
- package/src/core/attachments.mjs +4 -0
- package/src/core/backend-url.mjs +16 -0
- package/src/core/context-reduction.mjs +207 -0
- package/src/core/headless.mjs +85 -13
- package/src/core/local-agent.mjs +352 -44
- package/src/core/model-selection.mjs +72 -0
- package/src/core/request-retry.mjs +106 -0
- package/src/core/tool-error.mjs +155 -0
- package/src/core/usage-normalization.mjs +32 -0
- package/src/local-service/agent-relay.mjs +279 -7
- package/src/local-service/server.mjs +37 -0
- package/src/orchestration/node-runner.mjs +7 -3
- package/src/plugins/executor.mjs +4 -4
- package/src/plugins/lifecycle.mjs +337 -0
- package/src/plugins/manifest.mjs +37 -0
- package/src/plugins/pi-compat/requirements.mjs +30 -17
- package/src/plugins/preflight.mjs +37 -0
- package/src/terminal/main.mjs +10 -1
- package/src/terminal/paste-input.mjs +52 -0
- package/src/terminal/repl.mjs +169 -36
- package/src/tools/registry.mjs +1 -16
- package/src/core/agent-loop.mjs +0 -503
- package/src/core/context-manager.mjs +0 -198
- package/src/tools/agent.mjs +0 -142
package/package.json
CHANGED
package/src/commands/install.mjs
CHANGED
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
resolveComposeDependencies,
|
|
33
33
|
} from './plugin-manage.mjs';
|
|
34
34
|
import { preflightPlugin, existingInstalledNames } from '../plugins/preflight.mjs';
|
|
35
|
+
import { runMigrations, runSeed, runPostInstall, purgeData } from '../plugins/lifecycle.mjs';
|
|
36
|
+
import { makePluginState } from '../plugins/state.mjs';
|
|
35
37
|
|
|
36
38
|
const RESET = '\x1b[0m';
|
|
37
39
|
const BOLD = '\x1b[1m';
|
|
@@ -68,6 +70,8 @@ function parseArgs(argv) {
|
|
|
68
70
|
case '--slug': case '--name': parsed.slug = argv[++i]; break;
|
|
69
71
|
case '--no-state': parsed.state = false; break;
|
|
70
72
|
case '--no-workspace': parsed.workspace = false; break;
|
|
73
|
+
case '--no-seed': parsed.no_seed = true; break;
|
|
74
|
+
case '--reseed': parsed.reseed = true; break;
|
|
71
75
|
default:
|
|
72
76
|
if (!arg.startsWith('-')) positional.push(arg);
|
|
73
77
|
break;
|
|
@@ -171,6 +175,8 @@ export async function handleInstallCommand(argv, { cwd = process.cwd() } = {}) {
|
|
|
171
175
|
--slug <name> Override the scaffolded pack slug (pi sources only)
|
|
172
176
|
--no-state Skip the persistent state layer (pi sources only)
|
|
173
177
|
--no-workspace Skip the reactive workspace panel (pi sources only)
|
|
178
|
+
--no-seed Skip the lifecycle seed hook on install
|
|
179
|
+
--reseed Re-run the seed hook even if it already ran
|
|
174
180
|
--project Install into ./.bahulam/plugins/ instead of ~/.bahulam/plugins/
|
|
175
181
|
--ref <ref> Git branch/tag/commit (git or registry sources)
|
|
176
182
|
--json Machine-readable output
|
|
@@ -417,6 +423,11 @@ async function preflightAndReport({ dest, args, cwd, meta = null }) {
|
|
|
417
423
|
}
|
|
418
424
|
}
|
|
419
425
|
|
|
426
|
+
// Lifecycle — migrations → seed → post_install. Runs after preflight OK.
|
|
427
|
+
// --force also purges the state dir so a clean reinstall starts from zero;
|
|
428
|
+
// omit --force to preserve state across reinstalls (common upgrade case).
|
|
429
|
+
const lifecycleReport = await runLifecycleOnInstall(m, dest, args);
|
|
430
|
+
|
|
420
431
|
if (args.json) {
|
|
421
432
|
process.stdout.write(JSON.stringify({
|
|
422
433
|
ok: true,
|
|
@@ -425,6 +436,7 @@ async function preflightAndReport({ dest, args, cwd, meta = null }) {
|
|
|
425
436
|
directory: dest,
|
|
426
437
|
scaffolded: Boolean(meta),
|
|
427
438
|
...(meta ? { pi_package: meta.packageName, namespace: meta.namespace, composed_tools: meta.exposeTools, agent: meta.agentSlug } : {}),
|
|
439
|
+
lifecycle: lifecycleReport,
|
|
428
440
|
}, null, 2) + '\n');
|
|
429
441
|
return;
|
|
430
442
|
}
|
|
@@ -441,9 +453,63 @@ async function preflightAndReport({ dest, args, cwd, meta = null }) {
|
|
|
441
453
|
process.stderr.write(` ${DIM}scaffolded${RESET} from ${CYAN}pi:${meta.packageName}${RESET} (namespace ${CYAN}${meta.namespace}${RESET}, ${meta.exposeTools.length} composed tool${meta.exposeTools.length === 1 ? '' : 's'})\n`);
|
|
442
454
|
process.stderr.write(` ${DIM}Edit the pack under ${dest} to customize.${RESET}\n`);
|
|
443
455
|
}
|
|
456
|
+
if (lifecycleReport.migrations?.ran || lifecycleReport.seed?.ran || lifecycleReport.post_install?.ran) {
|
|
457
|
+
const parts = [];
|
|
458
|
+
if (lifecycleReport.migrations?.ran) parts.push(`migrations: ${lifecycleReport.migrations.ran} applied`);
|
|
459
|
+
if (lifecycleReport.seed?.ran) parts.push('seed: ran');
|
|
460
|
+
if (lifecycleReport.post_install?.ran) parts.push('post_install: ran');
|
|
461
|
+
process.stderr.write(` ${DIM}lifecycle${RESET} ${parts.join(', ')}\n`);
|
|
462
|
+
}
|
|
444
463
|
process.stderr.write(`\n ${DIM}Open with:${RESET} ${CYAN}bahulam plugin ${m.metadata.name}${RESET}\n\n`);
|
|
445
464
|
}
|
|
446
465
|
|
|
466
|
+
/**
|
|
467
|
+
* Run migrations → seed → post_install after a successful preflight.
|
|
468
|
+
* Callers pass `args.force` to also wipe the state dir first (true clean
|
|
469
|
+
* reinstall) and `args.no_seed` to skip seeding.
|
|
470
|
+
*
|
|
471
|
+
* Any lifecycle failure rolls back the plugin directory so the workspace
|
|
472
|
+
* is not left in a half-installed state — same policy as preflight
|
|
473
|
+
* rollback above.
|
|
474
|
+
*/
|
|
475
|
+
async function runLifecycleOnInstall(manifest, dest, args) {
|
|
476
|
+
const lifecycle = manifest.config?.lifecycle || null;
|
|
477
|
+
if (!lifecycle) return { skipped: true, reason: 'no config.lifecycle declared' };
|
|
478
|
+
const pluginName = manifest.metadata?.name;
|
|
479
|
+
if (!pluginName) return { skipped: true, reason: 'manifest missing name' };
|
|
480
|
+
|
|
481
|
+
// --force is a true clean reinstall: wipe both the plugin dir (already
|
|
482
|
+
// done by the installer's --force path) AND the state dir. Without
|
|
483
|
+
// --force we keep state across reinstalls so an upgrade doesn't
|
|
484
|
+
// silently discard user data.
|
|
485
|
+
if (args.force) {
|
|
486
|
+
const purged = purgeData(pluginName);
|
|
487
|
+
if (purged.purged) process.stderr.write(` ${YELLOW}!${RESET} --force: purged state dir ${purged.dir}\n`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const stateFactory = () => makePluginState(pluginName, { tables: manifest.config?.state?.tables || [] });
|
|
491
|
+
const report = { migrations: null, seed: null, post_install: null };
|
|
492
|
+
try {
|
|
493
|
+
report.migrations = await runMigrations({ pluginName, pluginDir: dest, manifest, stateFactory });
|
|
494
|
+
if (!args.no_seed) {
|
|
495
|
+
report.seed = await runSeed({
|
|
496
|
+
pluginName, pluginDir: dest, manifest,
|
|
497
|
+
args: { force: Boolean(args.reseed || args.force) },
|
|
498
|
+
stateFactory,
|
|
499
|
+
});
|
|
500
|
+
} else {
|
|
501
|
+
report.seed = { ran: false, reason: '--no-seed flag' };
|
|
502
|
+
}
|
|
503
|
+
report.post_install = await runPostInstall({ pluginName, pluginDir: dest, manifest, args, stateFactory });
|
|
504
|
+
} catch (err) {
|
|
505
|
+
// Roll back the plugin dir. State was intentionally not touched by us
|
|
506
|
+
// during the migration failure (snapshot-rollback took care of it).
|
|
507
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
508
|
+
throw new Error(`lifecycle failed — rolled back ${dest}: ${err.message}`);
|
|
509
|
+
}
|
|
510
|
+
return report;
|
|
511
|
+
}
|
|
512
|
+
|
|
447
513
|
/**
|
|
448
514
|
* Install-time host check: read the ingredient's requirements sidecar,
|
|
449
515
|
* verify each detected binary is on PATH. Throws with an actionable
|
|
@@ -480,15 +546,31 @@ async function enforceHostRequirements({ piDir, packageName, args }) {
|
|
|
480
546
|
}
|
|
481
547
|
|
|
482
548
|
const host = checkRequirementsAgainstHost(reqs);
|
|
549
|
+
const optionalByName = new Map((reqs.system_binaries || []).map(b => [b.name, b.optional === true]));
|
|
483
550
|
const missing = host.binaries.filter(b => !b.found);
|
|
484
|
-
|
|
551
|
+
const missingRequired = missing.filter(b => !optionalByName.get(b.name));
|
|
552
|
+
const missingOptional = missing.filter(b => optionalByName.get(b.name));
|
|
553
|
+
|
|
554
|
+
const platformKey = process.platform === 'win32' ? 'win32'
|
|
555
|
+
: process.platform === 'darwin' ? 'darwin'
|
|
556
|
+
: 'linux';
|
|
557
|
+
const hintFor = (b) => b.install_hints?.[platformKey] || b.install_hints?.darwin || b.install_hints?.linux;
|
|
558
|
+
|
|
559
|
+
if (missingOptional.length) {
|
|
560
|
+
process.stderr.write(`${YELLOW}!${RESET} ${packageName} is missing ${missingOptional.length} optional binar${missingOptional.length === 1 ? 'y' : 'ies'} — some features will be disabled:\n`);
|
|
561
|
+
for (const b of missingOptional) {
|
|
562
|
+
const hint = hintFor(b);
|
|
563
|
+
process.stderr.write(` ${DIM}·${RESET} ${BOLD}${b.name}${RESET}${hint ? ` — install: ${CYAN}${hint}${RESET}` : ''}\n`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (missingRequired.length === 0) return;
|
|
485
568
|
|
|
486
|
-
const platformKey = process.platform === 'darwin' ? 'darwin' : 'linux';
|
|
487
569
|
const lines = [];
|
|
488
|
-
lines.push(`${packageName} needs ${
|
|
489
|
-
for (const b of
|
|
490
|
-
const hint = b
|
|
491
|
-
lines.push(` · ${b.name}${hint ? ` — install: ${CYAN}${hint}${RESET}` : ''}`);
|
|
570
|
+
lines.push(`${packageName} needs ${missingRequired.length} system binar${missingRequired.length === 1 ? 'y' : 'ies'} not found on your PATH:`);
|
|
571
|
+
for (const b of missingRequired) {
|
|
572
|
+
const hint = hintFor(b);
|
|
573
|
+
lines.push(` · ${BOLD}${b.name}${RESET}${hint ? ` — install: ${CYAN}${hint}${RESET}` : ''}`);
|
|
492
574
|
}
|
|
493
575
|
if (args.force) {
|
|
494
576
|
process.stderr.write(`${YELLOW}!${RESET} ${lines.join('\n')}\n`);
|
|
@@ -26,6 +26,8 @@ import { parsePluginManifestFile } from '../plugins/manifest.mjs';
|
|
|
26
26
|
import { preflightPlugin, existingInstalledNames } from '../plugins/preflight.mjs';
|
|
27
27
|
import { parsePiSource } from '../plugins/pi-compose.mjs';
|
|
28
28
|
import { bahulamHome, pluginDirs, pluginInstallDir } from '../core/paths.mjs';
|
|
29
|
+
import { runPreUninstall, runMigrations, runPostInstall, runSeed, purgeData, readLifecycleRecord } from '../plugins/lifecycle.mjs';
|
|
30
|
+
import { makePluginState } from '../plugins/state.mjs';
|
|
29
31
|
|
|
30
32
|
const RESET = '\x1b[0m';
|
|
31
33
|
const BOLD = '\x1b[1m';
|
|
@@ -275,6 +277,15 @@ async function surfacePackRequirements(packDir, displayName = null) {
|
|
|
275
277
|
process.stderr.write(` ${icon} ${l.text}\n`);
|
|
276
278
|
}
|
|
277
279
|
if (reqs.system_binaries?.length) {
|
|
280
|
+
const platformKey = process.platform === 'win32' ? 'win32'
|
|
281
|
+
: process.platform === 'darwin' ? 'darwin'
|
|
282
|
+
: 'linux';
|
|
283
|
+
for (const b of reqs.system_binaries) {
|
|
284
|
+
const hint = b.install_hints?.[platformKey] || b.install_hints?.darwin || b.install_hints?.linux;
|
|
285
|
+
if (!hint) continue;
|
|
286
|
+
const tag = b.optional === true ? ` ${DIM}(optional)${RESET}` : '';
|
|
287
|
+
process.stderr.write(` ${DIM}·${RESET} ${BOLD}${b.name}${RESET}${tag} — install: ${CYAN}${hint}${RESET}\n`);
|
|
288
|
+
}
|
|
278
289
|
const name = displayName || path.basename(packDir);
|
|
279
290
|
process.stderr.write(` ${DIM}run${RESET} ${CYAN}bahulam plugin doctor ${name}${RESET} ${DIM}to check your environment${RESET}\n`);
|
|
280
291
|
}
|
|
@@ -540,16 +551,116 @@ async function cmdList(args, cwd) {
|
|
|
540
551
|
|
|
541
552
|
// ── remove ──────────────────────────────────────────────────────────
|
|
542
553
|
|
|
543
|
-
function cmdRemove(args, cwd) {
|
|
554
|
+
async function cmdRemove(args, cwd) {
|
|
544
555
|
if (!args.pluginName) throw new Error('remove requires a plugin name');
|
|
545
556
|
const found = findByName(args.pluginName, cwd);
|
|
546
557
|
if (!found) throw new Error(`plugin not found: ${args.pluginName}`);
|
|
558
|
+
|
|
559
|
+
// Load the manifest first — if the plugin has a pre_uninstall hook, we
|
|
560
|
+
// must run it BEFORE deleting the plugin dir (hook code lives there).
|
|
561
|
+
const scan = readManifest(found.directory);
|
|
562
|
+
const manifest = scan?.manifest || null;
|
|
563
|
+
const pluginName = manifest?.metadata?.name || found.name;
|
|
564
|
+
const stateFactory = manifest ? () => makePluginState(pluginName, { tables: manifest.config?.state?.tables || [] }) : null;
|
|
565
|
+
|
|
566
|
+
let preHookResult = { keepData: null, warnings: [] };
|
|
567
|
+
if (manifest?.config?.lifecycle?.pre_uninstall) {
|
|
568
|
+
try {
|
|
569
|
+
const r = await runPreUninstall({ pluginName, pluginDir: found.directory, manifest, args, stateFactory });
|
|
570
|
+
preHookResult = r.result || {};
|
|
571
|
+
if (Array.isArray(preHookResult.warnings)) {
|
|
572
|
+
for (const w of preHookResult.warnings) process.stderr.write(` ${YELLOW}!${RESET} ${w}\n`);
|
|
573
|
+
}
|
|
574
|
+
} catch (err) {
|
|
575
|
+
// The hook explicitly failed — that is often intentional
|
|
576
|
+
// ("cannot remove: there is unsaved work"). Do not proceed.
|
|
577
|
+
throw new Error(`pre_uninstall hook failed: ${err.message}. Uninstall aborted; nothing was removed.`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Decide the data-cleanup behavior. Precedence:
|
|
582
|
+
// 1. Explicit CLI flag: --purge → wipe; --keep-data → keep.
|
|
583
|
+
// 2. pre_uninstall hook result.keepData (bool).
|
|
584
|
+
// 3. TTY → interactive prompt (default keep).
|
|
585
|
+
// 4. Non-TTY (piped/CI) → keep, and print the exact --purge command.
|
|
586
|
+
const dataDir = path.join(bahulamHome(), 'data', pluginName);
|
|
587
|
+
const hasDataDir = fs.existsSync(dataDir);
|
|
588
|
+
let purgeChoice = null; // true = purge, false = keep
|
|
589
|
+
const reasons = [];
|
|
590
|
+
if (args.purge) { purgeChoice = true; reasons.push('--purge'); }
|
|
591
|
+
else if (args.keep_data) { purgeChoice = false; reasons.push('--keep-data'); }
|
|
592
|
+
else if (preHookResult && typeof preHookResult.keepData === 'boolean') {
|
|
593
|
+
purgeChoice = !preHookResult.keepData;
|
|
594
|
+
reasons.push(`pre_uninstall.keepData=${preHookResult.keepData}`);
|
|
595
|
+
} else if (hasDataDir && process.stdin.isTTY && process.stdout.isTTY) {
|
|
596
|
+
purgeChoice = await promptYesNo(
|
|
597
|
+
`Also delete state at ${dataDir}? [y/N] `,
|
|
598
|
+
false,
|
|
599
|
+
);
|
|
600
|
+
reasons.push(`prompt=${purgeChoice ? 'y' : 'n'}`);
|
|
601
|
+
} else {
|
|
602
|
+
purgeChoice = false;
|
|
603
|
+
if (hasDataDir) reasons.push('non-TTY default: keep-data');
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Remove the plugin directory.
|
|
547
607
|
rmrf(found.directory);
|
|
608
|
+
|
|
609
|
+
// Purge or keep the data dir.
|
|
610
|
+
let purged = { purged: false, dir: dataDir };
|
|
611
|
+
if (purgeChoice && hasDataDir) {
|
|
612
|
+
purged = purgeData(pluginName);
|
|
613
|
+
}
|
|
614
|
+
|
|
548
615
|
if (args.json) {
|
|
549
|
-
process.stdout.write(JSON.stringify({
|
|
616
|
+
process.stdout.write(JSON.stringify({
|
|
617
|
+
ok: true,
|
|
618
|
+
removed: found.name,
|
|
619
|
+
directory: found.directory,
|
|
620
|
+
data: {
|
|
621
|
+
kept: !purgeChoice,
|
|
622
|
+
purged: purged.purged,
|
|
623
|
+
directory: dataDir,
|
|
624
|
+
reason: reasons.join(' '),
|
|
625
|
+
},
|
|
626
|
+
pre_uninstall_ran: Boolean(manifest?.config?.lifecycle?.pre_uninstall),
|
|
627
|
+
}) + '\n');
|
|
550
628
|
return;
|
|
551
629
|
}
|
|
630
|
+
|
|
552
631
|
process.stderr.write(`${GREEN}✓${RESET} Removed ${BOLD}${found.name}${RESET} (${found.directory})\n`);
|
|
632
|
+
if (purged.purged) {
|
|
633
|
+
process.stderr.write(` ${DIM}data${RESET} purged ${dataDir}\n`);
|
|
634
|
+
} else if (hasDataDir) {
|
|
635
|
+
process.stderr.write(
|
|
636
|
+
` ${DIM}data${RESET} kept ${dataDir}${reasons.length ? ` ${DIM}(${reasons.join(' ')})${RESET}` : ''}\n` +
|
|
637
|
+
` ${DIM}To purge later:${RESET} ${CYAN}bahulam plugin remove ${found.name} --purge${RESET}` +
|
|
638
|
+
` ${DIM}(plugin dir is gone; this variant only purges the data dir).${RESET}\n`,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Minimal yes/no prompt — no external deps. Reads a single line from
|
|
645
|
+
* stdin; empty answer takes the default.
|
|
646
|
+
*/
|
|
647
|
+
function promptYesNo(prompt, defaultAnswer) {
|
|
648
|
+
return new Promise((resolve) => {
|
|
649
|
+
try { process.stdout.write(prompt); } catch { /* ok */ }
|
|
650
|
+
let input = '';
|
|
651
|
+
const onData = (chunk) => {
|
|
652
|
+
input += chunk.toString();
|
|
653
|
+
const nl = input.indexOf('\n');
|
|
654
|
+
if (nl < 0) return;
|
|
655
|
+
process.stdin.removeListener('data', onData);
|
|
656
|
+
try { process.stdin.pause(); } catch { /* ok */ }
|
|
657
|
+
const answer = input.slice(0, nl).trim().toLowerCase();
|
|
658
|
+
if (!answer) return resolve(defaultAnswer);
|
|
659
|
+
resolve(answer === 'y' || answer === 'yes');
|
|
660
|
+
};
|
|
661
|
+
try { process.stdin.resume(); } catch { /* ok */ }
|
|
662
|
+
process.stdin.on('data', onData);
|
|
663
|
+
});
|
|
553
664
|
}
|
|
554
665
|
|
|
555
666
|
// ── enable / disable ────────────────────────────────────────────────
|
|
@@ -620,7 +731,15 @@ async function cmdUpdate(args, cwd) {
|
|
|
620
731
|
const ref = args.ref || found.origin.ref;
|
|
621
732
|
const subdir = found.origin.subdir;
|
|
622
733
|
process.stderr.write(`${DIM}Updating ${found.name} from ${found.origin.url}${ref ? ` @ ${ref}` : ''}${subdir ? ` (subdir ${subdir})` : ''}...${RESET}\n`);
|
|
623
|
-
|
|
734
|
+
|
|
735
|
+
// --force wipes the plugin dir before the fetch so a stale local edit or
|
|
736
|
+
// corrupt install doesn't block the reset. Data dir is preserved.
|
|
737
|
+
if (args.force && !subdir) {
|
|
738
|
+
rmrf(found.directory);
|
|
739
|
+
fs.mkdirSync(found.directory, { recursive: true });
|
|
740
|
+
// Fresh clone into the empty dir.
|
|
741
|
+
await run('git', ['clone', '--depth', '1', ...(ref ? ['--branch', ref] : []), found.origin.url, found.directory]);
|
|
742
|
+
} else if (subdir) {
|
|
624
743
|
// Monorepo: no .git in place, so re-fetch subdir contents into tmp
|
|
625
744
|
// and rsync them over the install dir (preserves .bahulam-plugin.json).
|
|
626
745
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'bahulam-plugin-'));
|
|
@@ -640,7 +759,29 @@ async function cmdUpdate(args, cwd) {
|
|
|
640
759
|
}
|
|
641
760
|
writeStamp(found.directory, { origin: { ...found.origin, ref: ref || found.origin.ref } });
|
|
642
761
|
const scan = readManifest(found.directory);
|
|
643
|
-
|
|
762
|
+
const manifest = scan?.manifest;
|
|
763
|
+
process.stderr.write(`${GREEN}✓${RESET} Updated ${BOLD}${found.name}${RESET}${manifest?.metadata?.version ? ` to v${manifest.metadata.version}` : ''}\n`);
|
|
764
|
+
|
|
765
|
+
// Lifecycle: run pending migrations, then post_install. Seed does NOT
|
|
766
|
+
// re-run on update (it's a one-shot). If the update failed at
|
|
767
|
+
// migration time, the state DB is auto-restored via snapshot.
|
|
768
|
+
if (manifest) {
|
|
769
|
+
const pluginName = manifest.metadata?.name || found.name;
|
|
770
|
+
const stateFactory = () => makePluginState(pluginName, { tables: manifest.config?.state?.tables || [] });
|
|
771
|
+
try {
|
|
772
|
+
const migReport = await runMigrations({ pluginName, pluginDir: found.directory, manifest, stateFactory });
|
|
773
|
+
if (migReport.ran) process.stderr.write(` ${DIM}migrations${RESET} ${migReport.ran} applied\n`);
|
|
774
|
+
const postReport = await runPostInstall({ pluginName, pluginDir: found.directory, manifest, args, stateFactory });
|
|
775
|
+
if (postReport.ran) process.stderr.write(` ${DIM}post_install${RESET} ran\n`);
|
|
776
|
+
// Stamp last_upgrade_at
|
|
777
|
+
const rec = readLifecycleRecord(pluginName);
|
|
778
|
+
rec.last_upgrade_at = new Date().toISOString();
|
|
779
|
+
rec.installed_version = manifest.metadata?.version || rec.installed_version;
|
|
780
|
+
fs.writeFileSync(path.join(bahulamHome(), 'data', pluginName, '_bahulam_lifecycle.json'), JSON.stringify(rec, null, 2), 'utf-8');
|
|
781
|
+
} catch (err) {
|
|
782
|
+
throw new Error(`update completed but lifecycle failed: ${err.message}`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
644
785
|
}
|
|
645
786
|
|
|
646
787
|
// ── dispatcher ──────────────────────────────────────────────────────
|
|
@@ -823,7 +964,7 @@ export async function handlePluginManagementCommand(args, { cwd = process.cwd(),
|
|
|
823
964
|
switch (args.action) {
|
|
824
965
|
case 'validate': case 'check': case 'lint': await cmdValidate(args, cwd); return;
|
|
825
966
|
case 'list': case 'ls': await cmdList(args, cwd); return;
|
|
826
|
-
case 'remove': case 'rm': case 'uninstall': cmdRemove(args, cwd); return;
|
|
967
|
+
case 'remove': case 'rm': case 'uninstall': await cmdRemove(args, cwd); return;
|
|
827
968
|
case 'enable': toggle(args, cwd, true); return;
|
|
828
969
|
case 'disable': toggle(args, cwd, false); return;
|
|
829
970
|
case 'info': cmdInfo(args, cwd); return;
|
package/src/config/cli-args.mjs
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
export function parseArgs(args) {
|
|
23
23
|
const result = {
|
|
24
24
|
prompt: null,
|
|
25
|
+
runtimeMode: null,
|
|
25
26
|
model: null,
|
|
26
27
|
permissionMode: null,
|
|
27
28
|
outputFormat: null,
|
|
@@ -45,6 +46,13 @@ export function parseArgs(args) {
|
|
|
45
46
|
showHelp: false,
|
|
46
47
|
};
|
|
47
48
|
|
|
49
|
+
const setRuntimeMode = (mode) => {
|
|
50
|
+
if (result.runtimeMode && result.runtimeMode !== mode) {
|
|
51
|
+
throw new Error(`Runtime modes are mutually exclusive: --${result.runtimeMode} and --${mode}`);
|
|
52
|
+
}
|
|
53
|
+
result.runtimeMode = mode;
|
|
54
|
+
};
|
|
55
|
+
|
|
48
56
|
for (let i = 0; i < args.length; i++) {
|
|
49
57
|
const arg = args[i];
|
|
50
58
|
|
|
@@ -128,10 +136,22 @@ export function parseArgs(args) {
|
|
|
128
136
|
break;
|
|
129
137
|
|
|
130
138
|
case '--local':
|
|
131
|
-
// Force
|
|
132
|
-
//
|
|
133
|
-
// wiring, prompt-cache stats, etc.
|
|
139
|
+
// Force CLI-side orchestration. Model calls still use the
|
|
140
|
+
// shared Bahulam Gateway; this bypasses backend /api/execute.
|
|
134
141
|
result.local = true;
|
|
142
|
+
setRuntimeMode('local');
|
|
143
|
+
break;
|
|
144
|
+
|
|
145
|
+
case '--remote':
|
|
146
|
+
setRuntimeMode('remote');
|
|
147
|
+
break;
|
|
148
|
+
|
|
149
|
+
case '--bundled':
|
|
150
|
+
setRuntimeMode('bundled');
|
|
151
|
+
break;
|
|
152
|
+
|
|
153
|
+
case '--direct':
|
|
154
|
+
setRuntimeMode('direct');
|
|
135
155
|
break;
|
|
136
156
|
|
|
137
157
|
case '--vision': {
|
|
@@ -94,3 +94,12 @@ export function readShippedCatalog() {
|
|
|
94
94
|
_cache = null;
|
|
95
95
|
return null;
|
|
96
96
|
}
|
|
97
|
+
|
|
98
|
+
export function findShippedModel(model) {
|
|
99
|
+
const wanted = String(model || '').trim();
|
|
100
|
+
if (!wanted) return null;
|
|
101
|
+
const catalog = readShippedCatalog() || [];
|
|
102
|
+
return catalog.find(row => row.id === wanted)
|
|
103
|
+
|| catalog.find(row => row.id.endsWith(`/${wanted}`))
|
|
104
|
+
|| null;
|
|
105
|
+
}
|
|
@@ -40,3 +40,19 @@ export const SHIPPED_MODEL_DEFAULTS = readShippedModelDefaults();
|
|
|
40
40
|
export const DEFAULT_REASONING_MODEL = SHIPPED_MODEL_DEFAULTS.reasoning;
|
|
41
41
|
export const DEFAULT_FAST_MODEL = SHIPPED_MODEL_DEFAULTS.fast;
|
|
42
42
|
export const DEFAULT_PLANNING_MODEL = SHIPPED_MODEL_DEFAULTS.planning;
|
|
43
|
+
|
|
44
|
+
function envModel(names, fallback) {
|
|
45
|
+
for (const name of names) {
|
|
46
|
+
const value = typeof process !== 'undefined' ? process.env?.[name] : null;
|
|
47
|
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
48
|
+
}
|
|
49
|
+
return fallback;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Keep the named /model modes aligned with the backend chat mode matrix.
|
|
53
|
+
export const CHAT_MODE_DEFAULTS = Object.freeze({
|
|
54
|
+
fast: envModel(['BAHULAM_CHAT_FAST_MODEL'], DEFAULT_FAST_MODEL),
|
|
55
|
+
thinking: envModel(['BAHULAM_CHAT_THINKING_MODEL'], DEFAULT_PLANNING_MODEL),
|
|
56
|
+
extra_thinking: envModel(['BAHULAM_CHAT_EXTRA_THINKING_MODEL'], 'minimax/minimax-m3'),
|
|
57
|
+
max_thinking: envModel(['BAHULAM_CHAT_MAX_THINKING_MODEL'], 'deepseek/deepseek-v4-pro'),
|
|
58
|
+
});
|
package/src/core/approval.mjs
CHANGED
|
@@ -154,9 +154,10 @@ export class ApprovalManager {
|
|
|
154
154
|
|
|
155
155
|
setReadline(rl) { this._rl = rl; }
|
|
156
156
|
|
|
157
|
-
setExecutionHooks({ onPause, onResume, onApprovalPromptEnd } = {}) {
|
|
157
|
+
setExecutionHooks({ onPause, onResume, onApprovalPromptStart, onApprovalPromptEnd } = {}) {
|
|
158
158
|
this._execPause = onPause || null;
|
|
159
159
|
this._execResume = onResume || null;
|
|
160
|
+
this._approvalPromptStart = onApprovalPromptStart || null;
|
|
160
161
|
this._approvalPromptEnd = onApprovalPromptEnd || null;
|
|
161
162
|
}
|
|
162
163
|
|
|
@@ -279,6 +280,12 @@ export class ApprovalManager {
|
|
|
279
280
|
|
|
280
281
|
const isInteractive = process.stdin.isTTY;
|
|
281
282
|
|
|
283
|
+
// Shared lifecycle hook for every runtime. Hosts can leave a
|
|
284
|
+
// persistent approval marker before the transient dock is painted.
|
|
285
|
+
try {
|
|
286
|
+
this._approvalPromptStart?.({ tool: toolName, args, tier, why });
|
|
287
|
+
} catch { /* rendering must never block approval */ }
|
|
288
|
+
|
|
282
289
|
// In rich TTY mode approval lives in the fixed input dock, replacing
|
|
283
290
|
// "+ add instruction" until the user decides. Fallback/plain mode
|
|
284
291
|
// still renders in the transcript.
|
package/src/core/attachments.mjs
CHANGED
|
@@ -465,6 +465,10 @@ function looksLikeAttachment(value) {
|
|
|
465
465
|
return looksLikeImagePath(value) || looksLikeDocumentPath(value) || isClipboardAlias(value);
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
+
export function looksLikeAttachmentReference(value) {
|
|
469
|
+
return looksLikeAttachment(value);
|
|
470
|
+
}
|
|
471
|
+
|
|
468
472
|
/**
|
|
469
473
|
* Parse @path references from `input` for BOTH images and documents.
|
|
470
474
|
* Returns a cleaned instruction (with @refs removed) plus separate
|
package/src/core/backend-url.mjs
CHANGED
|
@@ -65,3 +65,19 @@ export function resolveBackendUrl() {
|
|
|
65
65
|
// 3. Fallback to production
|
|
66
66
|
return BACKEND_URLS.production;
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the Gateway URL used by npm-owned orchestration.
|
|
71
|
+
*
|
|
72
|
+
* A local backend token must be validated by a Gateway configured against
|
|
73
|
+
* that same local backend; sending it to the production Gateway produces a
|
|
74
|
+
* misleading 401. Keep the explicit override for custom deployments.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveGatewayUrl() {
|
|
77
|
+
if (process.env.BAHULAM_GATEWAY_URL) {
|
|
78
|
+
return process.env.BAHULAM_GATEWAY_URL.replace(/\/+$/, '');
|
|
79
|
+
}
|
|
80
|
+
const env = (process.env.TARANG_ENV || process.env.NODE_ENV || 'production').toLowerCase();
|
|
81
|
+
if (env === 'local' || env === 'docker') return 'http://127.0.0.1:8180/v1';
|
|
82
|
+
return 'https://gateway.bahulam.ai/v1';
|
|
83
|
+
}
|