@bahulam/code 0.1.13 → 0.1.14
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 +87 -0
- package/src/commands/plugin-manage.mjs +215 -29
- package/src/config/model-catalog-default.json +0 -4
- package/src/core/resume-mode.mjs +0 -1
- package/src/core/stream-client.mjs +18 -0
- package/src/daemon/session-core.mjs +3 -0
- package/src/local-service/agent-relay.mjs +6 -1
- package/src/local-service/file-access.mjs +116 -2
- package/src/local-service/server.mjs +209 -20
- package/src/plugins/npm-install.mjs +138 -0
- package/src/plugins/pi-compat/requirements.mjs +465 -0
- package/src/plugins/pi-compat/scaffold.mjs +86 -10
- package/src/plugins/pi-compat/shim.mjs +29 -1
- package/src/plugins/preflight.mjs +2 -0
- package/src/terminal/main.mjs +2 -1
- package/src/terminal/repl.mjs +52 -23
- package/src/ui/input-dock.mjs +48 -10
- package/src/ui/text-layout.mjs +4 -3
package/package.json
CHANGED
package/src/commands/install.mjs
CHANGED
|
@@ -231,6 +231,12 @@ async function installPiWithScaffolding({ classified, targetDir, cwd, args }) {
|
|
|
231
231
|
// Step 2: ensure the tools cache is present.
|
|
232
232
|
const discovered = await discoverPiTools(piDir, { pluginName: classified.package_name });
|
|
233
233
|
|
|
234
|
+
// Step 2b: host check for required binaries. Install (unlike pull)
|
|
235
|
+
// implies "use this now", so a missing ffmpeg-class dep will fail at
|
|
236
|
+
// first tool call — better to fail loudly here. `--force` bypasses
|
|
237
|
+
// for offline provisioning / CI where deps land later.
|
|
238
|
+
await enforceHostRequirements({ piDir, packageName: classified.package_name, args });
|
|
239
|
+
|
|
234
240
|
// Step 3: generate the pack directory (composes + state + agent + panel).
|
|
235
241
|
process.stderr.write(`${DIM}scaffolding pack…${RESET}\n`);
|
|
236
242
|
const { dest, slug, namespace, exposeTools, agentSlug } = scaffoldPiPack({
|
|
@@ -267,6 +273,28 @@ async function preflightAndReport({ dest, args, cwd, meta = null }) {
|
|
|
267
273
|
// Do it in one command; the scaffolder path already has the pi ingredient.
|
|
268
274
|
await resolveComposeDependencies(m, { targetDir: path.dirname(dest) });
|
|
269
275
|
|
|
276
|
+
// Host check for each composed pi ingredient (same policy as the
|
|
277
|
+
// scaffolder path). Blocks install if a required binary is missing.
|
|
278
|
+
const composes = m.spec?.composes || [];
|
|
279
|
+
if (composes.length && !meta) {
|
|
280
|
+
// meta present == scaffolder path already did this pre-scaffold
|
|
281
|
+
const { bahulamHome } = await import('../core/paths.mjs');
|
|
282
|
+
const piBaseDir = path.join(bahulamHome(), 'plugins-pi');
|
|
283
|
+
for (const compose of composes) {
|
|
284
|
+
if (!compose.package_name) continue;
|
|
285
|
+
const safeName = compose.package_name.replace(/[/@]/g, '_');
|
|
286
|
+
const piDir = path.join(piBaseDir, safeName);
|
|
287
|
+
if (!fs.existsSync(piDir)) continue;
|
|
288
|
+
try {
|
|
289
|
+
await enforceHostRequirements({ piDir, packageName: compose.package_name, args });
|
|
290
|
+
} catch (err) {
|
|
291
|
+
// Roll back the pack install — the composed dep won't work.
|
|
292
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
293
|
+
throw err;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
270
298
|
if (args.json) {
|
|
271
299
|
process.stdout.write(JSON.stringify({
|
|
272
300
|
ok: true,
|
|
@@ -293,3 +321,62 @@ async function preflightAndReport({ dest, args, cwd, meta = null }) {
|
|
|
293
321
|
}
|
|
294
322
|
process.stderr.write(`\n ${DIM}Open with:${RESET} ${CYAN}bahulam plugin ${m.metadata.name}${RESET}\n\n`);
|
|
295
323
|
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Install-time host check: read the ingredient's requirements sidecar,
|
|
327
|
+
* verify each detected binary is on PATH. Throws with an actionable
|
|
328
|
+
* message (per-OS install hints) if anything required is missing.
|
|
329
|
+
* `--force` bypasses (for CI, offline provisioning, dev workflows where
|
|
330
|
+
* deps land later).
|
|
331
|
+
*
|
|
332
|
+
* Env vars / credentials are warn-only — many pi tools have optional
|
|
333
|
+
* features and blocking on a missing PEXELS_API_KEY when the user only
|
|
334
|
+
* wants media_probe is too aggressive.
|
|
335
|
+
*/
|
|
336
|
+
async function enforceHostRequirements({ piDir, packageName, args }) {
|
|
337
|
+
const { checkRequirementsAgainstHost, REQUIREMENTS_FILE, analyzeRequirements } =
|
|
338
|
+
await import('../plugins/pi-compat/requirements.mjs');
|
|
339
|
+
|
|
340
|
+
const sidecar = path.join(piDir, REQUIREMENTS_FILE);
|
|
341
|
+
let reqs = null;
|
|
342
|
+
if (fs.existsSync(sidecar)) {
|
|
343
|
+
try { reqs = JSON.parse(fs.readFileSync(sidecar, 'utf-8')); } catch { /* re-analyze */ }
|
|
344
|
+
}
|
|
345
|
+
if (!reqs) {
|
|
346
|
+
// Sidecar was missing (older ingredient install or analyzer crash) —
|
|
347
|
+
// synthesize on the fly so the check is never silently skipped.
|
|
348
|
+
let discoveredTools = null;
|
|
349
|
+
const toolsCache = path.join(piDir, '.bahulam-tools.json');
|
|
350
|
+
if (fs.existsSync(toolsCache)) {
|
|
351
|
+
try { discoveredTools = JSON.parse(fs.readFileSync(toolsCache, 'utf-8')); } catch { /* ignore */ }
|
|
352
|
+
}
|
|
353
|
+
reqs = analyzeRequirements(piDir, { discoveredTools });
|
|
354
|
+
}
|
|
355
|
+
if (!reqs?.system_binaries?.length) {
|
|
356
|
+
// Nothing to check.
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const host = checkRequirementsAgainstHost(reqs);
|
|
361
|
+
const missing = host.binaries.filter(b => !b.found);
|
|
362
|
+
if (missing.length === 0) return;
|
|
363
|
+
|
|
364
|
+
const platformKey = process.platform === 'darwin' ? 'darwin' : 'linux';
|
|
365
|
+
const lines = [];
|
|
366
|
+
lines.push(`${packageName} needs ${missing.length} system binar${missing.length === 1 ? 'y' : 'ies'} not found on your PATH:`);
|
|
367
|
+
for (const b of missing) {
|
|
368
|
+
const hint = b.install_hints?.[platformKey];
|
|
369
|
+
lines.push(` · ${b.name}${hint ? ` — install: ${CYAN}${hint}${RESET}` : ''}`);
|
|
370
|
+
}
|
|
371
|
+
if (args.force) {
|
|
372
|
+
process.stderr.write(`${YELLOW}!${RESET} ${lines.join('\n')}\n`);
|
|
373
|
+
process.stderr.write(`${YELLOW}!${RESET} ${DIM}--force set — continuing anyway. Composed tools using these binaries will fail at first call.${RESET}\n`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const err = new Error(
|
|
377
|
+
`${lines.join('\n')}\n\n` +
|
|
378
|
+
`Install the missing binaries, then rerun. Or use ${CYAN}--force${RESET} to install without them (composed tools using these will fail at first call).\n` +
|
|
379
|
+
`Verify anytime with: ${CYAN}bahulam plugin doctor pi:${packageName}${RESET}`,
|
|
380
|
+
);
|
|
381
|
+
throw err;
|
|
382
|
+
}
|
|
@@ -206,6 +206,7 @@ export async function installFromGit({ url, targetDir, name, ref, subdir, force
|
|
|
206
206
|
await run('git', ['clone', '--depth', '1', ...(ref ? ['--branch', ref] : []), url, dest]);
|
|
207
207
|
}
|
|
208
208
|
writeStamp(dest, { origin: { kind: 'git', url, ref: ref || null, subdir: subdir || null } });
|
|
209
|
+
await installPackNpmDeps(dest, name);
|
|
209
210
|
return dest;
|
|
210
211
|
}
|
|
211
212
|
|
|
@@ -226,10 +227,53 @@ export async function installFromTarball({ url, targetDir, name, force }) {
|
|
|
226
227
|
else await run('tar', ['-xzf', tmp, '-C', dest, '--strip-components=1']);
|
|
227
228
|
fs.unlinkSync(tmp);
|
|
228
229
|
writeStamp(dest, { origin: { kind: 'tarball', url } });
|
|
230
|
+
await installPackNpmDeps(dest, guessed);
|
|
229
231
|
return dest;
|
|
230
232
|
}
|
|
231
233
|
|
|
232
|
-
|
|
234
|
+
// Materialize a hand-authored pack's node_modules/ after clone/copy/extract.
|
|
235
|
+
// Shared by installFromGit / installFromTarball / installFromLocal so a pack
|
|
236
|
+
// with `package.json::dependencies` doesn't need `cd ~/.bahulam/plugins/x && npm install`.
|
|
237
|
+
async function installPackNpmDeps(packDir, displayName = null) {
|
|
238
|
+
const pkgPath = path.join(packDir, 'package.json');
|
|
239
|
+
if (fs.existsSync(pkgPath)) {
|
|
240
|
+
const { installPackageDependencies } = await import('../plugins/npm-install.mjs');
|
|
241
|
+
await installPackageDependencies({
|
|
242
|
+
dir: packDir,
|
|
243
|
+
packageName: displayName || path.basename(packDir),
|
|
244
|
+
kind: 'pack',
|
|
245
|
+
forceScripts: false, // hand-authored packs default to --ignore-scripts
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
// Requirements preflight for the pack itself. Walks the pack's own
|
|
249
|
+
// source (tools/*.mjs etc.) to surface system binaries the pack shells
|
|
250
|
+
// out to and env vars it reads. Same analyzer that pi ingredients use.
|
|
251
|
+
await surfacePackRequirements(packDir, displayName);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function surfacePackRequirements(packDir, displayName = null) {
|
|
255
|
+
try {
|
|
256
|
+
const { analyzeRequirements, formatRequirementsReport } =
|
|
257
|
+
await import('../plugins/pi-compat/requirements.mjs');
|
|
258
|
+
const reqs = analyzeRequirements(packDir);
|
|
259
|
+
if (!reqs || (!reqs.system_binaries.length && !reqs.env_vars.length && !reqs.readme_sections.length && !reqs.skills_available.length)) {
|
|
260
|
+
return; // Nothing worth telling the user about.
|
|
261
|
+
}
|
|
262
|
+
const lines = formatRequirementsReport(reqs, { verbose: false });
|
|
263
|
+
for (const l of lines) {
|
|
264
|
+
const icon = l.level === 'warn' ? `${YELLOW}!${RESET}` : l.level === 'ok' ? `${GREEN}✓${RESET}` : `${DIM}·${RESET}`;
|
|
265
|
+
process.stderr.write(` ${icon} ${l.text}\n`);
|
|
266
|
+
}
|
|
267
|
+
if (reqs.system_binaries?.length) {
|
|
268
|
+
const name = displayName || path.basename(packDir);
|
|
269
|
+
process.stderr.write(` ${DIM}run${RESET} ${CYAN}bahulam plugin doctor ${name}${RESET} ${DIM}to check your environment${RESET}\n`);
|
|
270
|
+
}
|
|
271
|
+
} catch (err) {
|
|
272
|
+
if (process.env.DEBUG) process.stderr.write(` ${DIM}requirements analyzer skipped: ${err.message}${RESET}\n`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function installFromPi({ packageName, versionRange, force, forceScripts = false }) {
|
|
233
277
|
// Pi packages live in ~/.bahulam/plugins-pi/ (or $BAHULAM_HOME/plugins-pi/)
|
|
234
278
|
// so `bahulam plugin list` doesn't confuse them with our own packs. The
|
|
235
279
|
// tool executor reads from the same canonical path via bahulamHome().
|
|
@@ -255,31 +299,17 @@ export async function installFromPi({ packageName, versionRange, force }) {
|
|
|
255
299
|
if (!tarballs.length) throw new Error(`npm pack produced no tarball for ${spec}`);
|
|
256
300
|
await run('tar', ['-xzf', path.join(tmp, tarballs[0]), '-C', dest, '--strip-components=1']);
|
|
257
301
|
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const merged = { ...(pkgJson.dependencies || {}) };
|
|
270
|
-
for (const [name, range] of Object.entries(pkgJson.peerDependencies || {})) {
|
|
271
|
-
if (!merged[name]) merged[name] = range === '*' ? 'latest' : range;
|
|
272
|
-
}
|
|
273
|
-
if (Object.keys(merged).length) {
|
|
274
|
-
const rewritten = { ...pkgJson, dependencies: merged };
|
|
275
|
-
delete rewritten.peerDependencies;
|
|
276
|
-
fs.writeFileSync(pkgPath, JSON.stringify(rewritten, null, 2));
|
|
277
|
-
process.stderr.write(` ${DIM}installing ${Object.keys(merged).length} pi runtime deps…${RESET}\n`);
|
|
278
|
-
await run('npm', ['install', '--no-audit', '--no-fund', '--legacy-peer-deps', '--silent'], {
|
|
279
|
-
cwd: dest,
|
|
280
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
281
|
-
});
|
|
282
|
-
}
|
|
302
|
+
// Materialize node_modules for the ingredient. Handles pi's peer-dep
|
|
303
|
+
// quirk and gates postinstall scripts on the verified-package list
|
|
304
|
+
// (§13.6.1c of PRD-102) so unreviewed pi packages can't run arbitrary
|
|
305
|
+
// code at pull time.
|
|
306
|
+
const { installPackageDependencies } = await import('../plugins/npm-install.mjs');
|
|
307
|
+
await installPackageDependencies({
|
|
308
|
+
dir: dest,
|
|
309
|
+
packageName,
|
|
310
|
+
kind: 'pi',
|
|
311
|
+
forceScripts,
|
|
312
|
+
});
|
|
283
313
|
|
|
284
314
|
// Read the resolved version so the stamp captures what we actually got.
|
|
285
315
|
let resolvedVersion = null;
|
|
@@ -303,15 +333,36 @@ export async function installFromPi({ packageName, versionRange, force }) {
|
|
|
303
333
|
// `cat` and lets executor invocations skip the first-run probe cost.
|
|
304
334
|
// Best-effort: failure here is non-fatal (returns tools:[] until next
|
|
305
335
|
// invocation retries) so a broken extension can still be diagnosed.
|
|
336
|
+
let discoveredShape = null;
|
|
306
337
|
try {
|
|
307
338
|
const { discoverPiTools } = await import('../plugins/pi-compat/probe.mjs');
|
|
308
|
-
|
|
309
|
-
process.stderr.write(` ${DIM}discovered${RESET} ${(
|
|
339
|
+
discoveredShape = await discoverPiTools(dest, { pluginName: packageName, force: true });
|
|
340
|
+
process.stderr.write(` ${DIM}discovered${RESET} ${(discoveredShape.tools || []).length} tool(s), ${(discoveredShape.commands || []).length} command(s)\n`);
|
|
310
341
|
} catch (probeErr) {
|
|
311
342
|
process.stderr.write(` ${YELLOW}!${RESET} Tool discovery failed: ${probeErr.message}\n`);
|
|
312
343
|
process.stderr.write(` ${DIM}The package installed but no tools were probed. Re-probe with:${RESET}\n`);
|
|
313
344
|
process.stderr.write(` ${DIM} node -e "import('${path.resolve('src/plugins/pi-compat/probe.mjs')}').then(m => m.discoverPiTools('${dest}', { pluginName: '${packageName}', force: true }))"${RESET}\n`);
|
|
314
345
|
}
|
|
346
|
+
|
|
347
|
+
// Requirements preflight (§13.6.1i). Never blocks — this is a heads-up
|
|
348
|
+
// before the user commits to composing the ingredient. Findings land
|
|
349
|
+
// in <dest>/.bahulam-requirements.json for the scaffolder + doctor.
|
|
350
|
+
try {
|
|
351
|
+
const { analyzeRequirements, formatRequirementsReport } = await import('../plugins/pi-compat/requirements.mjs');
|
|
352
|
+
const reqs = analyzeRequirements(dest, { discoveredTools: discoveredShape });
|
|
353
|
+
const lines = formatRequirementsReport(reqs, { verbose: false });
|
|
354
|
+
for (const l of lines) {
|
|
355
|
+
const icon = l.level === 'warn' ? `${YELLOW}!${RESET}` : l.level === 'ok' ? `${GREEN}✓${RESET}` : `${DIM}·${RESET}`;
|
|
356
|
+
process.stderr.write(` ${icon} ${l.text}\n`);
|
|
357
|
+
}
|
|
358
|
+
const missingBin = (reqs.system_binaries || []).length;
|
|
359
|
+
if (missingBin > 0) {
|
|
360
|
+
process.stderr.write(` ${DIM}run${RESET} ${CYAN}bahulam plugin doctor ${packageName}${RESET} ${DIM}to check your environment${RESET}\n`);
|
|
361
|
+
}
|
|
362
|
+
} catch (reqErr) {
|
|
363
|
+
// Non-fatal — analyzer is a nice-to-have.
|
|
364
|
+
if (process.env.DEBUG) process.stderr.write(` ${DIM}requirements analyzer skipped: ${reqErr.message}${RESET}\n`);
|
|
365
|
+
}
|
|
315
366
|
} catch (err) {
|
|
316
367
|
rmrf(dest);
|
|
317
368
|
throw new Error(`pi install failed for ${spec}: ${err.message}`);
|
|
@@ -330,8 +381,11 @@ export async function installFromLocal({ src, targetDir, force }) {
|
|
|
330
381
|
rmrf(dest);
|
|
331
382
|
}
|
|
332
383
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
333
|
-
|
|
384
|
+
// Copy the pack but skip node_modules — we'll materialize fresh below
|
|
385
|
+
// (source's node_modules can be stale, platform-specific, or bloat).
|
|
386
|
+
fs.cpSync(src, dest, { recursive: true, filter: (s) => path.basename(s) !== 'node_modules' });
|
|
334
387
|
writeStamp(dest, { origin: { kind: 'local', path: src } });
|
|
388
|
+
await installPackNpmDeps(dest, name);
|
|
335
389
|
return dest;
|
|
336
390
|
}
|
|
337
391
|
|
|
@@ -621,6 +675,137 @@ async function cmdValidate(args, cwd) {
|
|
|
621
675
|
if (!result.ok) process.exit(1);
|
|
622
676
|
}
|
|
623
677
|
|
|
678
|
+
// ── doctor ─────────────────────────────────────────────────────────
|
|
679
|
+
//
|
|
680
|
+
// Check that an installed pack's composed ingredients (pi:) have their
|
|
681
|
+
// required system binaries and env vars present on the host. Exits non-
|
|
682
|
+
// zero on missing required items (script-friendly for CI setup checks).
|
|
683
|
+
|
|
684
|
+
async function cmdDoctor(args, cwd) {
|
|
685
|
+
const target = args.pluginName;
|
|
686
|
+
const { bahulamHome } = await import('../core/paths.mjs');
|
|
687
|
+
const { analyzeRequirements, formatRequirementsReport, checkRequirementsAgainstHost, REQUIREMENTS_FILE } =
|
|
688
|
+
await import('../plugins/pi-compat/requirements.mjs');
|
|
689
|
+
const piBaseDir = path.join(bahulamHome(), 'plugins-pi');
|
|
690
|
+
|
|
691
|
+
// Which ingredient dirs to check?
|
|
692
|
+
// - `bahulam plugin doctor <pack-slug>` → all pi: composes referenced by that pack
|
|
693
|
+
// - `bahulam plugin doctor pi:<name>` → that specific pi ingredient
|
|
694
|
+
// - `bahulam plugin doctor` (no arg) → every pi ingredient installed
|
|
695
|
+
let ingredientDirs = [];
|
|
696
|
+
if (!target) {
|
|
697
|
+
if (fs.existsSync(piBaseDir)) {
|
|
698
|
+
for (const entry of fs.readdirSync(piBaseDir, { withFileTypes: true })) {
|
|
699
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
700
|
+
ingredientDirs.push({ label: entry.name, dir: path.join(piBaseDir, entry.name) });
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
} else if (target.startsWith('pi:')) {
|
|
705
|
+
const packageName = target.slice(3);
|
|
706
|
+
const safe = packageName.replace(/[/@]/g, '_');
|
|
707
|
+
const dir = path.join(piBaseDir, safe);
|
|
708
|
+
if (!fs.existsSync(dir)) throw new Error(`pi ingredient not installed: ${packageName}`);
|
|
709
|
+
ingredientDirs.push({ label: packageName, dir });
|
|
710
|
+
} else {
|
|
711
|
+
const found = findByName(target, cwd);
|
|
712
|
+
if (!found) throw new Error(`plugin not found: ${target}`);
|
|
713
|
+
const scan = readManifest(found.directory);
|
|
714
|
+
const composes = scan?.manifest?.spec?.composes || [];
|
|
715
|
+
for (const c of composes) {
|
|
716
|
+
if (!c.package_name) continue;
|
|
717
|
+
const safe = c.package_name.replace(/[/@]/g, '_');
|
|
718
|
+
const dir = path.join(piBaseDir, safe);
|
|
719
|
+
if (!fs.existsSync(dir)) {
|
|
720
|
+
process.stderr.write(` ${YELLOW}!${RESET} pi ingredient ${c.package_name} referenced by ${found.name} but not installed\n`);
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
ingredientDirs.push({ label: `${found.name} ← pi:${c.package_name}`, dir });
|
|
724
|
+
}
|
|
725
|
+
// Also check the pack's OWN source — hand-authored packs may shell
|
|
726
|
+
// out to system binaries (manim, docker, ffmpeg) or read env vars
|
|
727
|
+
// regardless of whether they compose any pi ingredients.
|
|
728
|
+
ingredientDirs.push({ label: found.name, dir: found.directory, isPack: true });
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const report = [];
|
|
732
|
+
let missingRequired = 0;
|
|
733
|
+
|
|
734
|
+
for (const { label, dir } of ingredientDirs) {
|
|
735
|
+
// Load or (re)compute the requirements sidecar.
|
|
736
|
+
let reqs = null;
|
|
737
|
+
const sidecar = path.join(dir, REQUIREMENTS_FILE);
|
|
738
|
+
if (fs.existsSync(sidecar)) {
|
|
739
|
+
try { reqs = JSON.parse(fs.readFileSync(sidecar, 'utf-8')); } catch { /* re-analyze */ }
|
|
740
|
+
}
|
|
741
|
+
if (!reqs) {
|
|
742
|
+
try {
|
|
743
|
+
let tools = null;
|
|
744
|
+
const toolsPath = path.join(dir, '.bahulam-tools.json');
|
|
745
|
+
if (fs.existsSync(toolsPath)) tools = JSON.parse(fs.readFileSync(toolsPath, 'utf-8'));
|
|
746
|
+
reqs = analyzeRequirements(dir, { discoveredTools: tools });
|
|
747
|
+
} catch (err) {
|
|
748
|
+
report.push({ label, dir, error: err.message });
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const host = checkRequirementsAgainstHost(reqs);
|
|
753
|
+
const missing = host.binaries.filter(b => !b.found).length;
|
|
754
|
+
missingRequired += missing;
|
|
755
|
+
report.push({ label, dir, reqs, host, missing });
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (args.json) {
|
|
759
|
+
process.stdout.write(JSON.stringify({
|
|
760
|
+
ok: missingRequired === 0,
|
|
761
|
+
missing_binaries: missingRequired,
|
|
762
|
+
checked: report,
|
|
763
|
+
}, null, 2) + '\n');
|
|
764
|
+
if (missingRequired > 0) process.exit(1);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (report.length === 0) {
|
|
769
|
+
process.stderr.write(`${DIM}No pi ingredients found to check.${RESET}\n`);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
for (const r of report) {
|
|
774
|
+
process.stderr.write(`\n${BOLD}${CYAN}${r.label}${RESET} ${DIM}${r.dir}${RESET}\n`);
|
|
775
|
+
if (r.error) { process.stderr.write(` ${RED}✗${RESET} ${r.error}\n`); continue; }
|
|
776
|
+
const lines = formatRequirementsReport(r.reqs, { verbose: false });
|
|
777
|
+
for (const l of lines) {
|
|
778
|
+
const icon = l.level === 'warn' ? `${YELLOW}!${RESET}` : l.level === 'ok' ? `${GREEN}✓${RESET}` : `${DIM}·${RESET}`;
|
|
779
|
+
process.stderr.write(` ${icon} ${l.text}\n`);
|
|
780
|
+
}
|
|
781
|
+
if (r.host.binaries.length) {
|
|
782
|
+
process.stderr.write(` ${DIM}binaries:${RESET}\n`);
|
|
783
|
+
for (const b of r.host.binaries) {
|
|
784
|
+
if (b.found) {
|
|
785
|
+
process.stderr.write(` ${GREEN}✓${RESET} ${b.name}${b.version ? ` ${DIM}${b.version}${RESET}` : ''}${b.path ? ` ${DIM}${b.path}${RESET}` : ''}\n`);
|
|
786
|
+
} else {
|
|
787
|
+
const hint = b.install_hints?.[process.platform === 'darwin' ? 'darwin' : 'linux'];
|
|
788
|
+
process.stderr.write(` ${RED}✗${RESET} ${b.name}${hint ? ` ${DIM}install:${RESET} ${CYAN}${hint}${RESET}` : ''}\n`);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (r.host.env_vars.length) {
|
|
793
|
+
process.stderr.write(` ${DIM}env vars:${RESET}\n`);
|
|
794
|
+
for (const v of r.host.env_vars) {
|
|
795
|
+
const status = v.set ? `${GREEN}✓${RESET}` : (v.credential ? `${RED}✗${RESET}` : `${YELLOW}⚠${RESET}`);
|
|
796
|
+
process.stderr.write(` ${status} ${v.name}${v.credential ? ` ${DIM}(credential)${RESET}` : ''}\n`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
process.stderr.write('\n');
|
|
801
|
+
if (missingRequired > 0) {
|
|
802
|
+
process.stderr.write(`${RED}✗${RESET} ${missingRequired} required binar${missingRequired === 1 ? 'y' : 'ies'} missing.\n\n`);
|
|
803
|
+
process.exit(1);
|
|
804
|
+
} else {
|
|
805
|
+
process.stderr.write(`${GREEN}✓${RESET} all detected requirements satisfied.\n\n`);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
624
809
|
export async function handlePluginManagementCommand(args, { cwd = process.cwd(), throwOnError = false } = {}) {
|
|
625
810
|
try {
|
|
626
811
|
switch (args.action) {
|
|
@@ -630,6 +815,7 @@ export async function handlePluginManagementCommand(args, { cwd = process.cwd(),
|
|
|
630
815
|
case 'enable': toggle(args, cwd, true); return;
|
|
631
816
|
case 'disable': toggle(args, cwd, false); return;
|
|
632
817
|
case 'info': cmdInfo(args, cwd); return;
|
|
818
|
+
case 'doctor': await cmdDoctor(args, cwd); return;
|
|
633
819
|
case 'update': case 'upgrade': await cmdUpdate(args, cwd); return;
|
|
634
820
|
default: throw new Error(`unknown plugin action: ${args.action}`);
|
|
635
821
|
}
|
|
@@ -33,9 +33,6 @@
|
|
|
33
33
|
{"value": "qwen/qwq-32b", "label": "QwQ 32B", "provider": "qwen", "inputCost": 0.2, "outputCost": 0.2, "context": 131072, "maxOutput": 32768, "supportsTools": true, "supportsReasoning": true, "harnessValidated": false},
|
|
34
34
|
{"value": "minimax/minimax-m3", "label": "MiniMax M3", "provider": "minimax", "inputCost": 0.5, "outputCost": 2.0, "context": 1048576, "maxOutput": 512000, "supportsTools": true, "supportsReasoning": false, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
|
|
35
35
|
{"value": "minimax/minimax-m2.1", "label": "MiniMax M2.1", "provider": "minimax", "inputCost": 0.3, "outputCost": 1.1, "context": 204800, "maxOutput": 131072, "supportsTools": true, "supportsReasoning": false, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
|
|
36
|
-
{"value": "xiaomi/mimo-v2.5-pro", "label": "MiMo V2.5 Pro", "provider": "xiaomi", "inputCost": 0.5, "outputCost": 2.0, "context": 1050000, "maxOutput": 131072, "supportsTools": true, "supportsReasoning": false, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
|
|
37
|
-
{"value": "xiaomi/mimo-v2.5", "label": "MiMo V2.5", "provider": "xiaomi", "inputCost": 0.3, "outputCost": 1.0, "context": 1050000, "maxOutput": 131072, "supportsTools": true, "supportsReasoning": false, "harnessValidated": true, "cacheProfile": "{\"type\": \"prefix_hash\", \"provider_hint\": \"Xiaomi\"}", "platformAccessTier": ["pro", "tier_49", "tier_99"]},
|
|
38
|
-
{"value": "xiaomi/mimo-v2-flash", "label": "MiMo V2 Flash", "provider": "xiaomi", "inputCost": 0.1, "outputCost": 0.3, "context": 131072, "maxOutput": 16384, "supportsTools": true, "supportsReasoning": false, "harnessValidated": false},
|
|
39
36
|
{"value": "moonshotai/kimi-k2.5", "label": "Kimi K2.5", "provider": "kimi", "inputCost": 0.44, "outputCost": 2.0, "context": 262144, "maxOutput": 262144, "supportsTools": true, "supportsReasoning": false, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
|
|
40
37
|
{"value": "moonshotai/kimi-k2", "label": "Kimi K2", "provider": "kimi", "inputCost": 0.44, "outputCost": 2.0, "context": 131072, "maxOutput": 100352, "supportsTools": true, "supportsReasoning": false, "harnessValidated": false},
|
|
41
38
|
{"value": "moonshotai/kimi-k2-instruct", "label": "Kimi K2 Instruct", "provider": "kimi", "inputCost": 0.44, "outputCost": 2.0, "context": 131072, "maxOutput": 65536, "supportsTools": true, "supportsReasoning": false, "harnessValidated": false},
|
|
@@ -60,7 +57,6 @@
|
|
|
60
57
|
{"value": "stealth/ox-alpha", "label": "Stealth OX Alpha", "provider": "stealth", "inputCost": 0.3, "outputCost": 1.0, "context": 1048576, "maxOutput": 131072, "supportsTools": true, "supportsReasoning": false, "harnessValidated": true, "cacheProfile": "{\"type\": \"prefix_hash\", \"provider_hint\": \"Stealth\"}", "platformAccessTier": ["pro", "tier_49", "tier_99"]},
|
|
61
58
|
{"value": "upstage/solar-pro4", "label": "Solar Pro 4", "provider": "upstage", "inputCost": 0.3, "outputCost": 1.0, "context": 524288, "maxOutput": 131072, "supportsTools": true, "supportsReasoning": false, "harnessValidated": false, "cacheProfile": "{\"type\": \"prefix_hash\", \"provider_hint\": \"Upstage\"}", "platformAccessTier": ["pro", "tier_49", "tier_99"]},
|
|
62
59
|
{"value": "qwen/qwen3-coder:free", "label": "Qwen3 Coder (Free)", "provider": "qwen", "inputCost": 0, "outputCost": 0, "context": 131072, "maxOutput": 65536, "supportsTools": true, "supportsReasoning": false, "harnessValidated": false},
|
|
63
|
-
{"value": "xiaomi/mimo-v2-flash:free", "label": "MiMo V2 Flash (Free)", "provider": "xiaomi", "inputCost": 0, "outputCost": 0, "context": 131072, "maxOutput": 16384, "supportsTools": true, "supportsReasoning": false, "harnessValidated": false},
|
|
64
60
|
{"value": "meta-llama/llama-3.3-70b-instruct:free", "label": "Llama 3.3 70B (Free)", "provider": "meta", "inputCost": 0, "outputCost": 0, "context": 131072, "maxOutput": 8192, "supportsTools": true, "supportsReasoning": false, "harnessValidated": false},
|
|
65
61
|
{"value": "qwen/qwen3-8b:free", "label": "Qwen3 8B (Free)", "provider": "qwen", "inputCost": 0, "outputCost": 0, "context": 131072, "maxOutput": 8192, "supportsTools": true, "supportsReasoning": false, "harnessValidated": false},
|
|
66
62
|
{"value": "nvidia/nemotron-3-ultra-550b-a55b:free", "label": "Nemotron 3 Ultra 550B (Free)", "provider": "nvidia", "inputCost": 0, "outputCost": 0, "context": 1000000, "maxOutput": 65536, "supportsTools": true, "supportsReasoning": false, "harnessValidated": true, "cacheProfile": "{\"type\": \"prefix_hash\", \"provider_hint\": \"NVIDIA\"}", "platformAccessTier": ["pro", "tier_49", "tier_99"]},
|
package/src/core/resume-mode.mjs
CHANGED
|
@@ -171,6 +171,7 @@ export class BahulamStreamClient {
|
|
|
171
171
|
this._pauseWaiters = new Set();
|
|
172
172
|
this._abort = null;
|
|
173
173
|
this._toolAbort = null;
|
|
174
|
+
this._sawComplete = false;
|
|
174
175
|
|
|
175
176
|
// Transport mode:
|
|
176
177
|
// 'remote' → cloud backend runs the agent loop server-side.
|
|
@@ -352,6 +353,7 @@ export class BahulamStreamClient {
|
|
|
352
353
|
this._cancelled = false;
|
|
353
354
|
this.currentTaskId = null;
|
|
354
355
|
this._firstEventTimedOut = false;
|
|
356
|
+
this._sawComplete = false;
|
|
355
357
|
|
|
356
358
|
// Bundled mode: spawn the local Python runtime on first turn.
|
|
357
359
|
// After this, this.baseUrl points at http://127.0.0.1:<random-port>
|
|
@@ -500,6 +502,21 @@ export class BahulamStreamClient {
|
|
|
500
502
|
if (this._cancelled) {
|
|
501
503
|
return;
|
|
502
504
|
}
|
|
505
|
+
if (this._sawComplete) {
|
|
506
|
+
telemetry.track('stream.post_complete_error_ignored', {
|
|
507
|
+
task_id: this.currentTaskId || null,
|
|
508
|
+
last_event_id: this.lastEventId || null,
|
|
509
|
+
message: err?.message || 'stream closed after complete',
|
|
510
|
+
error_code: err?.cause?.code || err?.code || '',
|
|
511
|
+
});
|
|
512
|
+
transportDebug('execute.post_complete_error_ignored', {
|
|
513
|
+
task_id: this.currentTaskId || null,
|
|
514
|
+
last_event_id: this.lastEventId || null,
|
|
515
|
+
error: err?.message || 'stream closed after complete',
|
|
516
|
+
code: err?.cause?.code || err?.code || '',
|
|
517
|
+
});
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
503
520
|
if (this._firstEventTimedOut) {
|
|
504
521
|
yield {
|
|
505
522
|
type: EVENT_TYPES.ERROR,
|
|
@@ -579,6 +596,7 @@ export class BahulamStreamClient {
|
|
|
579
596
|
}
|
|
580
597
|
|
|
581
598
|
if (event === EVENT_TYPES.COMPLETE) {
|
|
599
|
+
this._sawComplete = true;
|
|
582
600
|
this._persistMemoryFactsFromComplete(data);
|
|
583
601
|
}
|
|
584
602
|
|
|
@@ -99,10 +99,13 @@ export const DAEMON_OWNED_FIELDS = Object.freeze([
|
|
|
99
99
|
* • It's input state tied to a keyboard (`inputHistory`).
|
|
100
100
|
* • It's a rendering flag whose value depends on the current visible
|
|
101
101
|
* transcript, not the session's actual state (`inSubAgent`).
|
|
102
|
+
* • It's an active renderer collection that does not JSON round-trip
|
|
103
|
+
* safely (`activeSubAgentRuns` is a Map).
|
|
102
104
|
*/
|
|
103
105
|
export const CLIENT_OWNED_SESSION_FIELDS = Object.freeze([
|
|
104
106
|
'inputHistory',
|
|
105
107
|
'inSubAgent',
|
|
108
|
+
'activeSubAgentRuns',
|
|
106
109
|
'creditsLowWarned',
|
|
107
110
|
'msgsLowWarned',
|
|
108
111
|
'_lastEmittedThinking',
|
|
@@ -748,6 +748,8 @@ export class LocalAgentRelay {
|
|
|
748
748
|
lines.push(
|
|
749
749
|
'Use analyze_image(path=..., question=...) for images.',
|
|
750
750
|
'Use read_table(path=...) for CSV/TSV/Excel-style tables.',
|
|
751
|
+
'Use browser/web inspection tools for HTML and browser-rendered web assets when available.',
|
|
752
|
+
'For video and audio, inspect file metadata and ask for a transcript or frame extraction when content analysis is needed.',
|
|
751
753
|
'Use read_attachment(path=...) for text, PDFs, Markdown, JSON/YAML, and Jupyter notebooks.',
|
|
752
754
|
'For unsupported binary files, inspect metadata or ask before attempting lossy conversion.',
|
|
753
755
|
);
|
|
@@ -902,7 +904,10 @@ function attachmentToolHint(file) {
|
|
|
902
904
|
const ext = String(file?.path || file?.name || '').split('.').pop().toLowerCase();
|
|
903
905
|
if (kind === 'image' || mime.startsWith('image/')) return 'analyze_image';
|
|
904
906
|
if (kind === 'spreadsheet' || kind === 'table' || ['csv', 'tsv', 'xlsx', 'xls', 'ods'].includes(ext)) return 'read_table';
|
|
905
|
-
if (
|
|
907
|
+
if (kind === 'web' || ['html', 'htm'].includes(ext)) return 'web_preview';
|
|
908
|
+
if (kind === 'video' || mime.startsWith('video/')) return 'media_metadata';
|
|
909
|
+
if (kind === 'audio' || mime.startsWith('audio/')) return 'media_metadata';
|
|
910
|
+
if (['pdf', 'markdown', 'text', 'code', 'config', 'notebook'].includes(kind) || ['txt', 'md', 'mdx', 'pdf', 'json', 'yaml', 'yml', 'toml', 'xml', 'ipynb', 'log', 'rst', 'sql', 'sh'].includes(ext)) return 'read_attachment';
|
|
906
911
|
return '';
|
|
907
912
|
}
|
|
908
913
|
|