@bahulam/code 0.1.12 → 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.
@@ -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"]},
@@ -1,4 +1,5 @@
1
1
  import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
2
3
  import * as path from 'node:path';
3
4
  import { deepMerge } from '../core/policy-resolver.mjs';
4
5
 
@@ -26,12 +27,26 @@ function readJson(filePath) {
26
27
  }
27
28
  }
28
29
 
30
+ // Global settings live alongside global plugins (~/.bahulam/), so an
31
+ // allowlist that applies to a globally-installed plugin belongs here.
32
+ // $BAHULAM_HOME overrides the default so tests and one-off installs
33
+ // don't touch the real home directory.
34
+ function globalSettingsPath() {
35
+ const home = process.env.BAHULAM_HOME || path.join(os.homedir(), '.bahulam');
36
+ return path.join(home, 'settings.json');
37
+ }
38
+
29
39
  export function loadBahulamSettings({ cwd = process.cwd() } = {}) {
30
40
  const base = path.join(cwd, '.bahulam');
31
41
  const layers = [
32
42
  { name: 'default', path: null, data: DEFAULT_BAHULAM_SETTINGS },
33
43
  ];
44
+ // Merge order (later overrides earlier): default → global → project → local.
45
+ // Global keeps values that stay constant across projects (plugins live
46
+ // in ~/.bahulam, so plugin allowlists sit here). Project & local remain
47
+ // the last word so a repo can tighten or loosen without touching global.
34
48
  for (const [name, file] of [
49
+ ['global', globalSettingsPath()],
35
50
  ['project', path.join(base, 'settings.json')],
36
51
  ['local', path.join(base, 'settings.local.json')],
37
52
  ]) {
@@ -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
 
@@ -850,6 +850,53 @@ export function createToolExecutor({
850
850
  const name = String(toolDef.name || '').trim();
851
851
  if (!name || toolMap[name]) continue;
852
852
  const pluginName = toolDef._plugin_name || toolDef.plugin_name || null;
853
+ if (toolDef._composed?.kind === 'pi') {
854
+ // Composed pi tools resolve at invocation time: look up the
855
+ // installed pi package's directory, load the specific handler
856
+ // via the shim-backed probe, invoke, return the result. Handler
857
+ // cache is per-session (per tool name) to amortize the ~50ms
858
+ // child-process overhead on repeat calls.
859
+ let _piInvokeP = null;
860
+ registerPluginTool(name, async (args, options = {}) => {
861
+ try {
862
+ if (!_piInvokeP) {
863
+ const { loadPiToolHandler } = await import('../plugins/pi-compat/probe.mjs');
864
+ const packageName = toolDef._composed.package_name;
865
+ const originalName = toolDef._composed.original_name;
866
+ const { bahulamHome } = await import('./paths.mjs');
867
+ const piBaseDir = path.join(bahulamHome(), 'plugins-pi');
868
+ const piDir = path.join(piBaseDir, packageName.replace(/[/@]/g, '_'));
869
+ if (!fs.existsSync(piDir)) {
870
+ return {
871
+ success: false,
872
+ output: `Composed pi tool '${name}' unavailable: pi package ${packageName} is not installed. Run \`bahulam plugin install pi:${packageName}\`.`,
873
+ _tool: name,
874
+ _plugin: pluginName,
875
+ _composed: toolDef._composed,
876
+ };
877
+ }
878
+ _piInvokeP = loadPiToolHandler(piDir, originalName, { pluginName: packageName });
879
+ }
880
+ const invoke = await _piInvokeP;
881
+ const result = await invoke(args || {});
882
+ return {
883
+ ...(result && typeof result === 'object' ? result : { success: true, output: String(result) }),
884
+ _tool: name,
885
+ _plugin: pluginName,
886
+ _composed: toolDef._composed,
887
+ };
888
+ } catch (err) {
889
+ return {
890
+ success: false,
891
+ output: `Composed pi tool '${name}' failed: ${err.message}`,
892
+ _tool: name,
893
+ _plugin: pluginName,
894
+ _composed: toolDef._composed,
895
+ };
896
+ }
897
+ }, { pluginName, source: 'pi', composed: toolDef._composed });
898
+ continue;
899
+ }
853
900
  registerPluginTool(name, async (args, options = {}) => {
854
901
  const handler = await loadPluginTool(toolDef._plugin_dir, toolDef.tool);
855
902
  if (!handler) {
@@ -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
 
@@ -6,6 +6,7 @@ import * as fs from 'node:fs';
6
6
  import * as path from 'node:path';
7
7
 
8
8
  const DEFAULT_MAX_ENTRIES = 300;
9
+ const DEFAULT_MAX_SEARCH_RESULTS = 200;
9
10
  const DEFAULT_MAX_TEXT_BYTES = 256 * 1024;
10
11
  export const DEFAULT_MAX_RAW_BYTES = 100 * 1024 * 1024;
11
12
 
@@ -42,8 +43,8 @@ const EXTENSION_FILE_TYPES = new Map([
42
43
  ['env', { kind: 'config', language: 'plaintext', label: 'Environment', textLike: true }],
43
44
  ['properties', { kind: 'config', language: 'plaintext', label: 'Properties', textLike: true }],
44
45
  ['xml', { kind: 'code', language: 'xml', label: 'XML', textLike: true }],
45
- ['html', { kind: 'code', language: 'html', label: 'HTML', textLike: true }],
46
- ['htm', { kind: 'code', language: 'html', label: 'HTML', textLike: true }],
46
+ ['html', { kind: 'web', language: 'html', label: 'HTML', textLike: true }],
47
+ ['htm', { kind: 'web', language: 'html', label: 'HTML', textLike: true }],
47
48
  ['css', { kind: 'code', language: 'css', label: 'CSS', textLike: true }],
48
49
  ['scss', { kind: 'code', language: 'scss', label: 'SCSS', textLike: true }],
49
50
  ['sass', { kind: 'code', language: 'scss', label: 'Sass', textLike: true }],
@@ -110,6 +111,18 @@ const EXTENSION_FILE_TYPES = new Map([
110
111
  ['svg', { kind: 'image', language: 'xml', label: 'SVG Image', textLike: false }],
111
112
  ['bmp', { kind: 'image', language: null, label: 'Bitmap Image', textLike: false }],
112
113
  ['ico', { kind: 'image', language: null, label: 'Icon', textLike: false }],
114
+ ['avif', { kind: 'image', language: null, label: 'AVIF Image', textLike: false }],
115
+ ['mp4', { kind: 'video', language: null, label: 'MP4 Video', textLike: false }],
116
+ ['webm', { kind: 'video', language: null, label: 'WebM Video', textLike: false }],
117
+ ['mov', { kind: 'video', language: null, label: 'QuickTime Video', textLike: false }],
118
+ ['m4v', { kind: 'video', language: null, label: 'M4V Video', textLike: false }],
119
+ ['ogv', { kind: 'video', language: null, label: 'Ogg Video', textLike: false }],
120
+ ['mp3', { kind: 'audio', language: null, label: 'MP3 Audio', textLike: false }],
121
+ ['m4a', { kind: 'audio', language: null, label: 'M4A Audio', textLike: false }],
122
+ ['wav', { kind: 'audio', language: null, label: 'WAV Audio', textLike: false }],
123
+ ['ogg', { kind: 'audio', language: null, label: 'Ogg Audio', textLike: false }],
124
+ ['flac', { kind: 'audio', language: null, label: 'FLAC Audio', textLike: false }],
125
+ ['aac', { kind: 'audio', language: null, label: 'AAC Audio', textLike: false }],
113
126
  ['pdf', { kind: 'pdf', language: null, label: 'PDF', textLike: false }],
114
127
  ['xlsx', { kind: 'spreadsheet', language: null, label: 'Excel Workbook', textLike: false }],
115
128
  ['xls', { kind: 'spreadsheet', language: null, label: 'Excel Workbook', textLike: false }],
@@ -165,12 +178,16 @@ export function listWorkspacePath(session, requestedPath = '.', { maxEntries = D
165
178
  try {
166
179
  childStat = fs.statSync(fullPath);
167
180
  } catch {}
181
+ const file = childStat?.isFile() ? describeFile(root, fullPath, childStat) : null;
168
182
  return {
169
183
  name: entry.name,
170
184
  path: normalizeRelative(root, fullPath),
171
185
  type: entry.isDirectory() ? 'directory' : entry.isFile() ? 'file' : 'other',
172
186
  size: childStat?.size ?? null,
173
187
  updated_at: childStat?.mtime ? childStat.mtime.toISOString() : null,
188
+ kind: file?.kind ?? (entry.isDirectory() ? 'directory' : null),
189
+ viewer: file?.viewer ?? (entry.isDirectory() ? 'directory' : null),
190
+ label: file?.label ?? null,
174
191
  };
175
192
  });
176
193
 
@@ -201,6 +218,88 @@ export function listWorkspacePath(session, requestedPath = '.', { maxEntries = D
201
218
  };
202
219
  }
203
220
 
221
+ export function searchWorkspaceFiles(session, {
222
+ query = '',
223
+ kinds = [],
224
+ maxResults = DEFAULT_MAX_SEARCH_RESULTS,
225
+ } = {}) {
226
+ const root = fs.realpathSync(session.root_path);
227
+ const wanted = new Set((Array.isArray(kinds) ? kinds : String(kinds || '').split(','))
228
+ .map((kind) => String(kind || '').trim().toLowerCase())
229
+ .filter(Boolean)
230
+ .filter((kind) => kind !== 'all'));
231
+ const term = String(query || '').trim().toLowerCase();
232
+ const limit = Math.max(1, Math.min(1000, Number(maxResults) || DEFAULT_MAX_SEARCH_RESULTS));
233
+ const results = [];
234
+
235
+ function visit(dir) {
236
+ if (results.length >= limit) return;
237
+ let entries = [];
238
+ try {
239
+ entries = fs.readdirSync(dir, { withFileTypes: true });
240
+ } catch {
241
+ return;
242
+ }
243
+ entries.sort((a, b) => {
244
+ if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
245
+ return a.name.localeCompare(b.name);
246
+ });
247
+
248
+ for (const entry of entries) {
249
+ if (results.length >= limit) break;
250
+ if (shouldHideEntry(entry.name)) continue;
251
+ const fullPath = path.join(dir, entry.name);
252
+ let stat = null;
253
+ try {
254
+ stat = fs.statSync(fullPath);
255
+ } catch {
256
+ continue;
257
+ }
258
+ const rel = normalizeRelative(root, fullPath);
259
+ if (entry.isDirectory()) {
260
+ if ((!term || entry.name.toLowerCase().includes(term) || rel.toLowerCase().includes(term))
261
+ && (!wanted.size || wanted.has('directory'))) {
262
+ results.push({
263
+ name: entry.name,
264
+ path: rel,
265
+ type: 'directory',
266
+ size: null,
267
+ updated_at: stat.mtime.toISOString(),
268
+ kind: 'directory',
269
+ viewer: 'directory',
270
+ });
271
+ }
272
+ visit(fullPath);
273
+ continue;
274
+ }
275
+ if (!entry.isFile()) continue;
276
+ const file = describeFile(root, fullPath, stat);
277
+ if (wanted.size && !wanted.has(file.kind) && !wanted.has(file.viewer)) continue;
278
+ if (term && !entry.name.toLowerCase().includes(term) && !rel.toLowerCase().includes(term)) continue;
279
+ results.push({
280
+ name: entry.name,
281
+ path: rel,
282
+ type: 'file',
283
+ size: stat.size,
284
+ updated_at: stat.mtime.toISOString(),
285
+ kind: file.kind,
286
+ viewer: file.viewer,
287
+ label: file.label,
288
+ });
289
+ }
290
+ }
291
+
292
+ visit(root);
293
+ return {
294
+ ok: true,
295
+ type: 'search',
296
+ query: term,
297
+ kinds: [...wanted],
298
+ results,
299
+ truncated: results.length >= limit,
300
+ };
301
+ }
302
+
204
303
  export function readWorkspaceFile(session, requestedPath, { maxBytes = DEFAULT_MAX_TEXT_BYTES } = {}) {
205
304
  const target = resolveWorkspacePath(session, requestedPath);
206
305
  const stat = fs.statSync(target);
@@ -252,6 +351,18 @@ export function contentTypeForPath(filePath) {
252
351
  case 'svg': return 'image/svg+xml';
253
352
  case 'bmp': return 'image/bmp';
254
353
  case 'ico': return 'image/x-icon';
354
+ case 'avif': return 'image/avif';
355
+ case 'mp4': return 'video/mp4';
356
+ case 'webm': return 'video/webm';
357
+ case 'mov': return 'video/quicktime';
358
+ case 'm4v': return 'video/x-m4v';
359
+ case 'ogv': return 'video/ogg';
360
+ case 'mp3': return 'audio/mpeg';
361
+ case 'm4a': return 'audio/mp4';
362
+ case 'wav': return 'audio/wav';
363
+ case 'ogg': return 'audio/ogg';
364
+ case 'flac': return 'audio/flac';
365
+ case 'aac': return 'audio/aac';
255
366
  case 'pdf': return 'application/pdf';
256
367
  case 'txt':
257
368
  case 'log':
@@ -351,7 +462,10 @@ function viewerForKind(kind) {
351
462
  case 'mermaid': return 'mermaid';
352
463
  case 'drawio': return 'drawio';
353
464
  case 'image': return 'image';
465
+ case 'video': return 'video';
466
+ case 'audio': return 'audio';
354
467
  case 'pdf': return 'pdf';
468
+ case 'web': return 'web';
355
469
  case 'spreadsheet': return 'spreadsheet';
356
470
  case 'document': return 'document';
357
471
  case 'presentation': return 'presentation';