@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bahulam/code",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Bahulam Code — abundance, in your terminal. CLI-first, reliability-first, sub-agents, 65.6% SWE-bench Verified.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- export async function installFromPi({ packageName, versionRange, force }) {
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
- // Pi packages typically declare their runtime deps under
259
- // `peerDependencies` (assuming pi will provide them). We're the host
260
- // now, so materialize those. Two steps because npm arborist crashes
261
- // when peerDependencies use `*` versions during install:
262
- // 1. Rewrite package.json to move peers into dependencies (resolved
263
- // version), and drop the peers block so the resolver stops
264
- // reconciling.
265
- // 2. `npm install` — the dependencies section is normal for npm.
266
- const pkgPath = path.join(dest, 'package.json');
267
- let pkgJson = {};
268
- try { pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); } catch { /* ignore */ }
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
- const shape = await discoverPiTools(dest, { pluginName: packageName, force: true });
309
- process.stderr.write(` ${DIM}discovered${RESET} ${(shape.tools || []).length} tool(s), ${(shape.commands || []).length} command(s)\n`);
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
- fs.cpSync(src, dest, { recursive: true });
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"]},
@@ -30,7 +30,6 @@ const MODEL_CONTEXT_WINDOWS = {
30
30
  'openai/gpt-5': 400000,
31
31
  'openai/gpt-5-mini': 400000,
32
32
  'google/gemini-2.5-pro': 1000000,
33
- 'xiaomi/mimo-v2.5': 128000,
34
33
  };
35
34
  const DEFAULT_CONTEXT_WINDOW = 128000;
36
35
 
@@ -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 (['pdf', 'markdown', 'text', 'code', 'config', 'notebook'].includes(kind) || ['txt', 'md', 'mdx', 'pdf', 'json', 'yaml', 'yml', 'toml', 'html', 'xml', 'ipynb', 'log', 'rst', 'sql', 'sh'].includes(ext)) return 'read_attachment';
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