@bahulam/code 0.1.5 → 0.1.6

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.5",
3
+ "version": "0.1.6",
4
4
  "description": "Bahulam Code \u2014 abundance, in your terminal. CLI-first, reliability-first, sub-agents, 65.6% SWE-bench Verified.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -76,6 +76,23 @@ function sleep(ms) {
76
76
  return new Promise(resolve => setTimeout(resolve, ms));
77
77
  }
78
78
 
79
+ function envMs(name, fallback) {
80
+ const raw = Number(process.env[name] || fallback);
81
+ return Number.isFinite(raw) && raw > 0 ? raw : fallback;
82
+ }
83
+
84
+ function transportDebugEnabled() {
85
+ const raw = String(process.env.BAHULAM_TRANSPORT_DEBUG || '').toLowerCase();
86
+ return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
87
+ }
88
+
89
+ function transportDebug(message, data = {}) {
90
+ if (!transportDebugEnabled()) return;
91
+ try {
92
+ process.stderr.write(`[transport] ${message} ${JSON.stringify(data)}\n`);
93
+ } catch {}
94
+ }
95
+
79
96
  // Full jitter around the scheduled delay: pick a value in [delay*0.5, delay*1.5].
80
97
  // Spreads out reconnect storms so N clients dropping simultaneously don't
81
98
  // synchronize their retries. Clamped to the same 30s ceiling as the base delay.
@@ -223,6 +240,7 @@ export class TarangStreamClient {
223
240
  async *execute(instruction, context = {}, messages = null) {
224
241
  this._cancelled = false;
225
242
  this.currentTaskId = null;
243
+ this._firstEventTimedOut = false;
226
244
 
227
245
  // Bundled mode: spawn the local Python runtime on first turn.
228
246
  // After this, this.baseUrl points at http://127.0.0.1:<random-port>
@@ -233,6 +251,7 @@ export class TarangStreamClient {
233
251
  const body = { instruction, context };
234
252
  if (messages && messages.length > 0) body.messages = messages;
235
253
  if (this.sessionId) body.session_id = this.sessionId;
254
+ const requestId = `cli-${_uuidLike()}`;
236
255
 
237
256
  // daemon cache-guard hook. If BAHULAM_CAPTURE_REQUEST is set to a file
238
257
  // path, serialize the exact body that would go to /api/execute, write
@@ -260,6 +279,8 @@ export class TarangStreamClient {
260
279
  const headers = this._headers({
261
280
  'Accept': 'text/event-stream',
262
281
  'Content-Type': 'application/json',
282
+ 'X-Bahulam-Request-ID': requestId,
283
+ 'X-Request-ID': requestId,
263
284
  });
264
285
 
265
286
  // Abort controller so cancel() can break out of a stalled reader
@@ -268,6 +289,21 @@ export class TarangStreamClient {
268
289
  this._toolAbort = new AbortController();
269
290
 
270
291
  let response;
292
+ let connectTimedOut = false;
293
+ const connectTimeoutMs = envMs('BAHULAM_EXECUTE_CONNECT_TIMEOUT_MS', 60_000);
294
+ const connectStarted = Date.now();
295
+ const connectTimer = setTimeout(() => {
296
+ connectTimedOut = true;
297
+ try { this._abort.abort(); } catch {}
298
+ }, connectTimeoutMs);
299
+ transportDebug('execute.start', {
300
+ request_id: requestId,
301
+ url,
302
+ mode: this.mode,
303
+ session_id: this.sessionId || null,
304
+ messages: Array.isArray(messages) ? messages.length : 0,
305
+ body_bytes: Buffer.byteLength(JSON.stringify(body), 'utf8'),
306
+ });
271
307
  try {
272
308
  response = await fetch(url, {
273
309
  method: 'POST',
@@ -276,15 +312,39 @@ export class TarangStreamClient {
276
312
  signal: this._abort.signal,
277
313
  });
278
314
  } catch (err) {
315
+ clearTimeout(connectTimer);
279
316
  if (err.name === 'AbortError') {
317
+ if (connectTimedOut) {
318
+ const message = `Backend did not accept /api/execute within ${Math.round(connectTimeoutMs / 1000)}s (request ${requestId}).`;
319
+ transportDebug('execute.connect_timeout', {
320
+ request_id: requestId,
321
+ ms: Date.now() - connectStarted,
322
+ });
323
+ yield { type: EVENT_TYPES.ERROR, data: { message, request_id: requestId, fatal: true } };
324
+ }
280
325
  return;
281
326
  }
282
- yield { type: EVENT_TYPES.ERROR, data: { message: `Network error: ${err.message}. Check your connection or use --local mode.`, fatal: true } };
327
+ transportDebug('execute.network_error', {
328
+ request_id: requestId,
329
+ ms: Date.now() - connectStarted,
330
+ error: err.message,
331
+ code: err?.cause?.code || err?.code || '',
332
+ });
333
+ yield { type: EVENT_TYPES.ERROR, data: { message: `Network error: ${err.message}. Check your connection or use --local mode.`, request_id: requestId, fatal: true } };
283
334
  return;
284
335
  }
336
+ clearTimeout(connectTimer);
337
+ const responseRequestId = response.headers.get('x-request-id') || requestId;
338
+ transportDebug('execute.response', {
339
+ request_id: responseRequestId,
340
+ status: response.status,
341
+ ok: response.ok,
342
+ ms: Date.now() - connectStarted,
343
+ task_id: response.headers.get('x-task-id') || null,
344
+ });
285
345
 
286
346
  if (response.status === 401) {
287
- yield { type: EVENT_TYPES.ERROR, data: { message: 'Authentication failed. Run `bahulam login` to re-authenticate.', fatal: true } };
347
+ yield { type: EVENT_TYPES.ERROR, data: { message: 'Authentication failed. Run `bahulam login` to re-authenticate.', request_id: responseRequestId, fatal: true } };
288
348
  return;
289
349
  }
290
350
  if (response.status === 429) {
@@ -305,6 +365,7 @@ export class TarangStreamClient {
305
365
  rate_limit: detail?.rate_limit || null,
306
366
  action: detail?.action || null,
307
367
  pricing_url: normalizeBillingBrandCopy(detail?.pricing_url || null),
368
+ request_id: responseRequestId,
308
369
  fatal: true,
309
370
  },
310
371
  };
@@ -312,7 +373,7 @@ export class TarangStreamClient {
312
373
  }
313
374
  if (!response.ok) {
314
375
  const text = await response.text().catch(() => 'Unknown error');
315
- yield { type: EVENT_TYPES.ERROR, data: { message: `Backend error ${response.status}: ${text}`, fatal: true } };
376
+ yield { type: EVENT_TYPES.ERROR, data: { message: `Backend error ${response.status}: ${text}`, request_id: responseRequestId, fatal: true } };
316
377
  return;
317
378
  }
318
379
 
@@ -322,6 +383,17 @@ export class TarangStreamClient {
322
383
  if (this._cancelled) {
323
384
  return;
324
385
  }
386
+ if (this._firstEventTimedOut) {
387
+ yield {
388
+ type: EVENT_TYPES.ERROR,
389
+ data: {
390
+ message: `Backend opened the stream but sent no SSE events within ${Math.round(envMs('BAHULAM_FIRST_EVENT_TIMEOUT_MS', 60_000) / 1000)}s (request ${responseRequestId}).`,
391
+ request_id: responseRequestId,
392
+ fatal: true,
393
+ },
394
+ };
395
+ return;
396
+ }
325
397
  yield* this._reconnectAfterDrop(err);
326
398
  }
327
399
  }
@@ -330,18 +402,37 @@ export class TarangStreamClient {
330
402
  const taskId = response.headers.get('X-Task-ID');
331
403
  if (taskId) this.currentTaskId = taskId;
332
404
 
333
- for await (const parsed of this._parseSSE(response)) {
334
- if (this._cancelled) return;
335
- await this._waitIfPaused();
336
- if (this._cancelled) return;
405
+ let sawFirstEvent = false;
406
+ const firstEventTimeoutMs = envMs('BAHULAM_FIRST_EVENT_TIMEOUT_MS', 60_000);
407
+ const firstEventTimer = setTimeout(() => {
408
+ if (sawFirstEvent || this._cancelled) return;
409
+ this._firstEventTimedOut = true;
410
+ try { this._abort?.abort(); } catch {}
411
+ }, firstEventTimeoutMs);
412
+ try {
413
+ for await (const parsed of this._parseSSE(response)) {
414
+ if (!sawFirstEvent) {
415
+ sawFirstEvent = true;
416
+ clearTimeout(firstEventTimer);
417
+ transportDebug('execute.first_event', {
418
+ task_id: this.currentTaskId || null,
419
+ event: parsed.event || null,
420
+ });
421
+ }
422
+ if (this._cancelled) return;
423
+ await this._waitIfPaused();
424
+ if (this._cancelled) return;
337
425
 
338
- if (parsed.id != null) this.lastEventId = parsed.id;
339
- if (parsed.retry != null) this.retryDelayMs = parsed.retry;
340
- if (parsed.data?.task_id) this.currentTaskId = parsed.data.task_id;
426
+ if (parsed.id != null) this.lastEventId = parsed.id;
427
+ if (parsed.retry != null) this.retryDelayMs = parsed.retry;
428
+ if (parsed.data?.task_id) this.currentTaskId = parsed.data.task_id;
341
429
 
342
- for await (const event of this._handleStreamEvent(parsed)) {
343
- yield event;
430
+ for await (const event of this._handleStreamEvent(parsed)) {
431
+ yield event;
432
+ }
344
433
  }
434
+ } finally {
435
+ clearTimeout(firstEventTimer);
345
436
  }
346
437
  }
347
438
 
@@ -44,6 +44,8 @@ export function createToolExecutor({
44
44
  checkpoints = null,
45
45
  hookRunner = null,
46
46
  interactionHandler = null,
47
+ onAutoRegisterStart = null,
48
+ onAutoRegisterDone = null,
47
49
  } = {}) {
48
50
  // Cross-session memory cache. Ships in getAgentContext() on every turn,
49
51
  // so we need it to be byte-identical when the underlying disk file hasn't
@@ -94,9 +96,19 @@ export function createToolExecutor({
94
96
  // for the current directory to be usable — the user chose to be here.
95
97
  // Opt out via BAHULAM_SKIP_AUTO_REGISTER=true for tests or headless
96
98
  // scripts that want a truly empty registry.
99
+ let autoRegisterPromise = Promise.resolve(null);
97
100
  if (process.env.BAHULAM_SKIP_AUTO_REGISTER !== 'true') {
98
- projectRegistry.register(process.cwd(), { bypassProjectMarkers: true })
99
- .catch(() => { /* silent model can register explicitly */ });
101
+ const autoRegisterRoot = process.cwd();
102
+ try { onAutoRegisterStart?.(autoRegisterRoot); } catch { /* status hooks are best-effort */ }
103
+ autoRegisterPromise = projectRegistry.register(autoRegisterRoot, { bypassProjectMarkers: true })
104
+ .then((result) => {
105
+ try { onAutoRegisterDone?.(null, result); } catch { /* status hooks are best-effort */ }
106
+ return result;
107
+ })
108
+ .catch((err) => {
109
+ try { onAutoRegisterDone?.(err, null); } catch { /* status hooks are best-effort */ }
110
+ return null;
111
+ });
100
112
  }
101
113
  let _searchCodeUsed = false; // tracks if search_code was called (for read_file nudge)
102
114
  let _readOnlyCacheGeneration = 0;
@@ -2203,6 +2215,10 @@ print('OK: replaced')
2203
2215
  return projectRegistry.resources();
2204
2216
  },
2205
2217
 
2218
+ waitForAutoRegister() {
2219
+ return autoRegisterPromise;
2220
+ },
2221
+
2206
2222
  async registerProjectRoots(roots, { forceRefresh = false } = {}) {
2207
2223
  const results = [];
2208
2224
  const seen = new Set();
@@ -74,6 +74,7 @@ import { buildResumeHistory, combineResumeSummaries, getRecentSessions, getSessi
74
74
  import { decideResumeMode, projectedTokensForChoice, formatTokens as formatCtxTokens } from '../core/resume-mode.mjs';
75
75
  import { appendTask, ensureTaskFiles, loadTaskBoard, moveTask, removeTask, taskCounts, TASK_FILES, updateTask } from '../core/tasks.mjs';
76
76
  import { applyCompactSummary, localCompactSummary, parseCompactTailCount, prepareCompactHistory } from '../core/compact-history.mjs';
77
+ import { startSpinner as startInlineSpinner } from '../ui/spinner.mjs';
77
78
  import {
78
79
  appendVisionAnalysisToInstruction,
79
80
  appendDocumentsToInstruction,
@@ -3674,7 +3675,28 @@ export async function startTerminalRepl() {
3674
3675
  return res;
3675
3676
  };
3676
3677
 
3677
- let toolExecutor = createToolExecutor({ checkpoints, hookRunner, interactionHandler: askUserInteraction });
3678
+ function makeToolExecutor({ showIndexStatus = false } = {}) {
3679
+ const shouldShowIndexStatus = showIndexStatus && process.stderr.isTTY && !term().plain;
3680
+ let stopIndexSpinner = null;
3681
+ return createToolExecutor({
3682
+ checkpoints,
3683
+ hookRunner,
3684
+ interactionHandler: askUserInteraction,
3685
+ onAutoRegisterStart: shouldShowIndexStatus ? (root) => {
3686
+ const name = path.basename(root || safeCwd()) || root || 'project';
3687
+ stopIndexSpinner?.();
3688
+ stopIndexSpinner = startInlineSpinner(
3689
+ `Indexing ${name} so tools can read and search this project...`
3690
+ );
3691
+ } : null,
3692
+ onAutoRegisterDone: shouldShowIndexStatus ? () => {
3693
+ stopIndexSpinner?.();
3694
+ stopIndexSpinner = null;
3695
+ } : null,
3696
+ });
3697
+ }
3698
+
3699
+ let toolExecutor = makeToolExecutor({ showIndexStatus: true });
3678
3700
  const skipPerms = cliArgs.skipPermissions;
3679
3701
  let approval = new ApprovalManager({ autoApprove: skipPerms, cwd: safeCwd(), policy: effectivePolicy.policy });
3680
3702
 
@@ -3768,7 +3790,7 @@ export async function startTerminalRepl() {
3768
3790
  checkpoints = new CheckpointManager(safeCwd());
3769
3791
  effectivePolicy = loadEffectivePolicy({ cwd: safeCwd() });
3770
3792
  hookRunner = new HookRunner({ cwd: safeCwd() });
3771
- toolExecutor = createToolExecutor({ checkpoints, hookRunner, interactionHandler: askUserInteraction });
3793
+ toolExecutor = makeToolExecutor();
3772
3794
  approval = new ApprovalManager({ autoApprove: skipPerms, cwd: safeCwd(), policy: effectivePolicy.policy });
3773
3795
  if (ctx._rl) approval.setReadline(ctx._rl);
3774
3796
  sessionMgr = new SessionManager(safeCwd());
@@ -3948,7 +3970,7 @@ export async function startTerminalRepl() {
3948
3970
  checkpoints = new CheckpointManager(safeCwd());
3949
3971
  effectivePolicy = loadEffectivePolicy({ cwd: safeCwd() });
3950
3972
  hookRunner = new HookRunner({ cwd: safeCwd(), sessionId });
3951
- toolExecutor = createToolExecutor({ checkpoints, hookRunner, interactionHandler: askUserInteraction });
3973
+ toolExecutor = makeToolExecutor();
3952
3974
  approval = new ApprovalManager({ autoApprove: skipPerms, cwd: safeCwd(), policy: effectivePolicy.policy });
3953
3975
  if (ctx._rl) approval.setReadline(ctx._rl);
3954
3976
  sessionMgr = new SessionManager(safeCwd());
@@ -54,6 +54,10 @@ const IGNORED_DIRS = new Set([
54
54
  '.git', '.bahulam', '.next', '.venv', '__pycache__',
55
55
  'build', 'dist', 'node_modules', 'venv',
56
56
  ]);
57
+ const ENV_PROBE_TIMEOUT_MS = Math.max(
58
+ 50,
59
+ Number(process.env.BAHULAM_ENV_PROBE_TIMEOUT_MS || 500) || 500,
60
+ );
57
61
 
58
62
  // Files or directories whose presence at the root implies this IS a project.
59
63
  // One is enough. Kept broad so we accept Node/Python/Rust/Go/Ruby/Java/C++
@@ -227,7 +231,7 @@ function commandVersion(command, args = ['--version']) {
227
231
  try {
228
232
  const result = spawnSync(command, args, {
229
233
  encoding: 'utf-8',
230
- timeout: 2000,
234
+ timeout: ENV_PROBE_TIMEOUT_MS,
231
235
  windowsHide: true,
232
236
  });
233
237
  if (result.error || result.status !== 0) return '';
@@ -244,7 +248,6 @@ function detectEnvironment() {
244
248
  ['git', 'git'],
245
249
  ['npm', 'npm'],
246
250
  ['uv', 'uv'],
247
- ['pytest', 'pytest'],
248
251
  ['docker', 'docker'],
249
252
  ];
250
253
  const tools = {};