@bahulam/code 0.1.24 → 0.1.25

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.
@@ -21,3 +21,55 @@ export function pastedTextLabel(text) {
21
21
  if (lines > 1) return `[text copied · ${lines} lines]`;
22
22
  return '[text copied]';
23
23
  }
24
+
25
+ function stripPastedPathQuotes(value) {
26
+ const text = String(value || '').trim();
27
+ if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
28
+ return text.slice(1, -1);
29
+ }
30
+ return text;
31
+ }
32
+
33
+ export function clipboardPathCandidate(value) {
34
+ const text = stripPastedPathQuotes(value);
35
+ if (!text) return '';
36
+ if (text.startsWith('file://')) {
37
+ try {
38
+ return decodeURIComponent(new URL(text).pathname);
39
+ } catch {
40
+ return text;
41
+ }
42
+ }
43
+ return text;
44
+ }
45
+
46
+ export function quotedAttachmentReference(value) {
47
+ const text = clipboardPathCandidate(value);
48
+ if (!/[\s"'\\]/.test(text)) return `@${text}`;
49
+ return `@"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
50
+ }
51
+
52
+ export function classifyPastedPromptPayload(payload, { looksLikeAttachmentReference = () => false } = {}) {
53
+ const text = normalizePastedText(payload || '');
54
+ if (!text) {
55
+ return { kind: 'clipboard_image', text: '@clipboard ', label: '[clipboard image]' };
56
+ }
57
+
58
+ const lines = text
59
+ .split('\n')
60
+ .map(line => line.trim())
61
+ .filter(Boolean);
62
+ if (lines.length && lines.every(line => looksLikeAttachmentReference(clipboardPathCandidate(line)))) {
63
+ return {
64
+ kind: lines.length === 1 ? 'clipboard_path' : 'clipboard_paths',
65
+ text: `${lines.map(quotedAttachmentReference).join('\n')} `,
66
+ label: lines.length === 1 ? '[clipboard path]' : `[clipboard paths · ${lines.length}]`,
67
+ };
68
+ }
69
+
70
+ return {
71
+ kind: 'clipboard_text',
72
+ text,
73
+ label: pastedTextLabel(text).replace('[text copied', '[clipboard text'),
74
+ };
75
+ }
@@ -23,6 +23,7 @@ import { Writable as _WritableStream } from 'node:stream';
23
23
  import { c, progressBar, spinner, inPlace, renderMarkdown, renderDiff, formatElapsed, formatCost, stripAnsi } from './ansi.mjs';
24
24
  import { calculateCost, formatCostValue, formatTokens, costToCredits, formatCredits } from '../core/pricing.mjs';
25
25
  import { BahulamStreamClient, EVENT_TYPES } from '../core/stream-client.mjs';
26
+ import { LocalAgent } from '../core/local-agent.mjs';
26
27
  import { AgentHistoryTurnBuilder } from '../core/agent-history.mjs';
27
28
  import { JsonlWriter } from '../core/jsonl-writer.mjs';
28
29
  import { tapSseEvent, registerBroadcaster } from '../daemon/event-tap.mjs';
@@ -56,7 +57,7 @@ import { persistProjectArtifacts } from '../core/project-artifacts.mjs';
56
57
  import { BahulamAuth } from '../auth/bahulam-auth.mjs';
57
58
  import { ApprovalManager } from '../core/approval.mjs';
58
59
  import * as telemetry from '../telemetry/index.mjs';
59
- import { resolveBackendUrl } from '../core/backend-url.mjs';
60
+ import { resolveBackendUrl, resolveGatewayUrl } from '../core/backend-url.mjs';
60
61
  import { formatMessageWindow, lowWindowStatus, messagesRemaining } from '../core/rate-limit-display.mjs';
61
62
  import { formatAgentErrorGuidance } from '../core/error-guidance.mjs';
62
63
  import { BUILTIN_AGENTS, runAgentDefinition } from './agents.mjs';
@@ -71,7 +72,7 @@ import { PluginRegistry } from '../plugins/registry.mjs';
71
72
  import { SessionManager } from '../core/session-manager.mjs';
72
73
  import { parseArgs } from '../config/cli-args.mjs';
73
74
  import { pickModelOverridesForm } from './repl-model-form.mjs';
74
- import { isRawMultilinePasteChunk, normalizePastedText, pastedTextLabel } from './paste-input.mjs';
75
+ import { classifyPastedPromptPayload, isRawMultilinePasteChunk, pastedTextLabel } from './paste-input.mjs';
75
76
  import {
76
77
  MODEL_CATEGORY_ORDER,
77
78
  formatCategoryBadge,
@@ -82,6 +83,8 @@ import {
82
83
  normalizeCatalogCategory,
83
84
  } from './model-catalog-display.mjs';
84
85
  import { loadEffectivePolicy, formatPolicySourceRows } from '../core/policy-resolver.mjs';
86
+ import { DEFAULT_REASONING_MODEL } from '../config/model-defaults.mjs';
87
+ import { applyModelSelection, resolveModelSelection } from '../core/model-selection.mjs';
85
88
  import { loadProjectContext } from '../core/project-context-loader.mjs';
86
89
  import { buildContextEnvelope } from '../core/context-envelope.mjs';
87
90
  import { buildResumeHistory, combineResumeSummaries, getRecentSessions, getSessionDetail, getTranscriptProjectRoots } from '../core/local-store.mjs';
@@ -94,6 +97,7 @@ import {
94
97
  appendDocumentsToInstruction,
95
98
  attachmentSummaryLine,
96
99
  documentSummaryLine,
100
+ looksLikeAttachmentReference,
97
101
  prepareImageAttachments,
98
102
  prepareDocumentAttachments,
99
103
  publicAttachmentMetadata,
@@ -3306,14 +3310,14 @@ async function prepareDirectAgentRunContext(ctx, instruction = '') {
3306
3310
  projectResources,
3307
3311
  }),
3308
3312
  };
3309
- const modelOverrides = Object.fromEntries(sessionModelOverrideEntries());
3310
- if (Object.keys(modelOverrides).length > 0) {
3311
- execContext.model_overrides = modelOverrides;
3312
- if (modelOverrides.reasoning) execContext.model_override = modelOverrides.reasoning;
3313
- }
3314
- if (session.modelMode) execContext.model_mode = session.modelMode;
3315
- if (session.routePreference) execContext.model_route = session.routePreference;
3316
- return execContext;
3313
+ const modelSelection = resolveModelSelection({
3314
+ explicitModel: null,
3315
+ modelOverrides: session.modelOverrides,
3316
+ modelMode: session.modelMode,
3317
+ modelRoute: session.routePreference,
3318
+ profileModels: { reasoning: session.model },
3319
+ });
3320
+ return applyModelSelection(execContext, modelSelection);
3317
3321
  }
3318
3322
 
3319
3323
  function makeDispatchContext(ctx) {
@@ -3329,6 +3333,11 @@ function makeDispatchContext(ctx) {
3329
3333
  apiKey: process.env.ANTHROPIC_API_KEY || creds.anthropicKey || null,
3330
3334
  openRouterKey: process.env.OPENROUTER_API_KEY || creds.openRouterKey || null,
3331
3335
  },
3336
+ modelTransport: ctx.runtimeMode === 'local' ? 'gateway' : 'direct',
3337
+ gatewayUrl: ctx.runtimeMode === 'local' ? ctx.gatewayUrl : null,
3338
+ gatewayToken: ctx.runtimeMode === 'local' ? creds.token : null,
3339
+ sessionId: session.id || ctx.localSessionId,
3340
+ defaultModel: session.model || cliArgs.model || null,
3332
3341
  cwd: safeCwd(),
3333
3342
  };
3334
3343
  }
@@ -4466,6 +4475,9 @@ export async function startTerminalRepl() {
4466
4475
  safeCwd(); // prime the cache in repl-utils.mjs for later recovery
4467
4476
 
4468
4477
  const cliArgs = parseArgs(process.argv.slice(2));
4478
+ const runtimeMode = cliArgs.runtimeMode || 'local';
4479
+ const gatewayUrl = resolveGatewayUrl();
4480
+ const localSessionId = `local_${Date.now()}_${Math.random().toString(16).slice(2, 10)}`;
4469
4481
  const auth = new BahulamAuth();
4470
4482
 
4471
4483
  // Projects are registered and indexed on demand through get_project_overview.
@@ -4548,8 +4560,23 @@ export async function startTerminalRepl() {
4548
4560
 
4549
4561
  // Persistent stream client — session_id captured from backend on first turn
4550
4562
  let streamClient = null;
4551
-
4552
- const ctx = { auth, toolExecutor: null, approval, jsonlWriter, sessionMgr, checkpoints, effectivePolicy, latestProjectContext, latestEnvelope, pendingVisionPaths: [] };
4563
+ let activeLocalAgent = null;
4564
+
4565
+ const ctx = {
4566
+ auth,
4567
+ toolExecutor: null,
4568
+ approval,
4569
+ jsonlWriter,
4570
+ sessionMgr,
4571
+ checkpoints,
4572
+ effectivePolicy,
4573
+ latestProjectContext,
4574
+ latestEnvelope,
4575
+ pendingVisionPaths: [],
4576
+ runtimeMode,
4577
+ gatewayUrl,
4578
+ localSessionId,
4579
+ };
4553
4580
 
4554
4581
  // Wake-on-finish: background jobs with on_complete dispatch their target
4555
4582
  // agent through the trigger funnel when they exit. The ctx builder runs
@@ -5018,19 +5045,16 @@ export async function startTerminalRepl() {
5018
5045
  initialContentRow: dockCursor.row,
5019
5046
  initialContentCol: dockCursor.col,
5020
5047
  });
5021
- // 1 Hz live-tick for the elapsed clock in the dock's top strip.
5022
- // Only fires when the user is idle (inputActive === true) — while the
5023
- // agent is streaming content, skipping the tick avoids ANSI writes
5024
- // interleaving with the stream. Cheap: one renderIdleDockInput per
5025
- // second, only if mounted + idle. `unref()` so the timer never blocks
5026
- // process exit.
5048
+ // 1 Hz live-tick for the elapsed clock in the dock's top strip. The render
5049
+ // queue serializes dock paints with agent/tool output, so the clock must
5050
+ // continue while the npm-owned local/direct loop is executing too.
5051
+ // `unref()` ensures the timer never blocks process exit.
5027
5052
  let _dockTickTimer = null;
5028
5053
  if (inputDockActive) {
5029
5054
  process.on('beforeExit', unmountInputDock);
5030
5055
  process.on('exit', unmountInputDock);
5031
5056
  _dockTickTimer = setInterval(() => {
5032
5057
  if (!isInputDockMounted()) return;
5033
- if (!inputActive) return;
5034
5058
  try { renderIdleDockInput(); } catch { /* one bad tick is not fatal */ }
5035
5059
  }, 1000);
5036
5060
  _dockTickTimer.unref?.();
@@ -5080,10 +5104,12 @@ export async function startTerminalRepl() {
5080
5104
  const baseCursor = typeof rl?.cursor === 'number' ? rl.cursor : baseLine.length;
5081
5105
  setImmediate(() => {
5082
5106
  try {
5083
- insertPromptText(normalizePastedText(s), {
5107
+ const pasted = classifyPastedPromptPayload(s, { looksLikeAttachmentReference });
5108
+ insertPromptText(pasted.text, {
5084
5109
  baseLine,
5085
5110
  baseCursor,
5086
5111
  fromPaste: true,
5112
+ pasteLabel: pasted.label,
5087
5113
  });
5088
5114
  } finally {
5089
5115
  _suppressRawPasteLines = false;
@@ -5150,7 +5176,7 @@ export async function startTerminalRepl() {
5150
5176
  }
5151
5177
 
5152
5178
  function idleInputTips() {
5153
- return '[Enter] send [/] commands [Tab] complete [F2] details';
5179
+ return '[Enter] send [/] commands [Tab] complete [@clipboard] image [F2] details';
5154
5180
  }
5155
5181
 
5156
5182
  function executionInputTips() {
@@ -5347,7 +5373,7 @@ export async function startTerminalRepl() {
5347
5373
  }
5348
5374
  }
5349
5375
 
5350
- function insertPromptText(text, { baseLine = rl.line || '', baseCursor = rl.cursor, fromPaste = false } = {}) {
5376
+ function insertPromptText(text, { baseLine = rl.line || '', baseCursor = rl.cursor, fromPaste = false, pasteLabel = null } = {}) {
5351
5377
  const payload = String(text || '');
5352
5378
  if (!payload) return;
5353
5379
  const line = String(baseLine || '');
@@ -5356,12 +5382,21 @@ export async function startTerminalRepl() {
5356
5382
  if (fromPaste) {
5357
5383
  _promptHasInsertedPaste = true;
5358
5384
  _pastedInputValue = next;
5359
- _pastedInputLabel = pastedTextLabel(payload);
5385
+ _pastedInputLabel = pasteLabel || pastedTextLabel(payload);
5360
5386
  }
5361
5387
  replaceReadlineLine(next, cursor + payload.length);
5362
5388
  renderIdleDockInput();
5363
5389
  }
5364
5390
 
5391
+ function insertClipboardImageReference({ baseLine = rl.line || '', baseCursor = rl.cursor, fromPaste = true } = {}) {
5392
+ const line = String(baseLine || '');
5393
+ const cursor = typeof baseCursor === 'number' ? Math.max(0, Math.min(line.length, baseCursor)) : line.length;
5394
+ const needsLeadingSpace = cursor > 0 && !/\s/.test(line[cursor - 1]);
5395
+ const needsTrailingSpace = cursor < line.length && !/\s/.test(line[cursor]);
5396
+ const token = `${needsLeadingSpace ? ' ' : ''}@clipboard${needsTrailingSpace ? ' ' : ' '}`;
5397
+ insertPromptText(token, { baseLine: line, baseCursor: cursor, fromPaste, pasteLabel: '[clipboard image]' });
5398
+ }
5399
+
5365
5400
  function acceptSlashHint() {
5366
5401
  const item = slashHintItems[slashHintSelected];
5367
5402
  if (!item) return false;
@@ -5468,6 +5503,10 @@ export async function startTerminalRepl() {
5468
5503
  if (!inputActive) return;
5469
5504
  if (_inBracketedPaste || _suppressBracketedPasteLines || _suppressRawPasteLines) return;
5470
5505
  if (key.name === 'return' || key.name === 'enter') return;
5506
+ if (key.ctrl && key.name === 'v') {
5507
+ insertClipboardImageReference();
5508
+ return;
5509
+ }
5471
5510
  if (key.name === 'f2') {
5472
5511
  clearSlashHint();
5473
5512
  if (isInputDockMounted()) moveToContent();
@@ -5537,11 +5576,11 @@ export async function startTerminalRepl() {
5537
5576
  const pastedLines = _pasteLines.slice();
5538
5577
  _pasteLines = [];
5539
5578
  if (pastedLines.length > 1 || trailing) {
5540
- const text = [...pastedLines, trailing].join('\n');
5579
+ const pasted = classifyPastedPromptPayload([...pastedLines, trailing].join('\n'), { looksLikeAttachmentReference });
5541
5580
  _promptHasInsertedPaste = true;
5542
- _pastedInputValue = text;
5543
- _pastedInputLabel = pastedTextLabel(text);
5544
- replaceReadlineLine(text);
5581
+ _pastedInputValue = pasted.text;
5582
+ _pastedInputLabel = pasted.label;
5583
+ replaceReadlineLine(pasted.text);
5545
5584
  renderIdleDockInput();
5546
5585
  return;
5547
5586
  }
@@ -5596,10 +5635,12 @@ export async function startTerminalRepl() {
5596
5635
  }
5597
5636
  // Readline has finished emitting synchronous `line` events by now.
5598
5637
  // Treat paste as editing the prompt buffer; Enter remains the submit.
5599
- insertPromptText(payload || '', {
5638
+ const pasted = classifyPastedPromptPayload(payload, { looksLikeAttachmentReference });
5639
+ insertPromptText(pasted.text, {
5600
5640
  baseLine: _bracketedPasteStartLine,
5601
5641
  baseCursor: _bracketedPasteStartCursor,
5602
5642
  fromPaste: true,
5643
+ pasteLabel: pasted.label,
5603
5644
  });
5604
5645
  });
5605
5646
 
@@ -5649,27 +5690,83 @@ export async function startTerminalRepl() {
5649
5690
 
5650
5691
  const originalInput = input;
5651
5692
  const creds = auth.loadCredentials();
5652
- if (!creds.token) {
5693
+ const anthKey = process.env.ANTHROPIC_API_KEY || creds.anthropicKey;
5694
+ const openRouterKey = process.env.OPENROUTER_API_KEY || creds.openRouterKey;
5695
+ if ((runtimeMode === 'remote' || runtimeMode === 'bundled' || runtimeMode === 'local') && !creds.token) {
5653
5696
  process.stderr.write(` ${c.red('Not logged in. Run /login first.')}\n`);
5654
5697
  showPrompt();
5655
5698
  return;
5656
5699
  }
5700
+ if (runtimeMode === 'direct' && !anthKey && !openRouterKey) {
5701
+ process.stderr.write(` ${c.red('Direct mode requires ANTHROPIC_API_KEY or OPENROUTER_API_KEY.')}\n`);
5702
+ showPrompt();
5703
+ return;
5704
+ }
5705
+
5706
+ // Remote/bundled retain the SSE client contract. Local/direct use the
5707
+ // same tool executor and event stream, but move the agent loop into npm.
5708
+ if (runtimeMode === 'local' || runtimeMode === 'direct') {
5709
+ const pluginSchemas = toolExecutor.listPluginToolSchemas?.() || [];
5710
+ const modelSelection = resolveModelSelection({
5711
+ explicitModel: cliArgs.model,
5712
+ modelOverrides: session.modelOverrides,
5713
+ modelMode: session.modelMode,
5714
+ modelRoute: session.routePreference,
5715
+ profileModels: { reasoning: session.model, local: creds.models?.local },
5716
+ modeModels: { fast: creds.models?.fast },
5717
+ fallbackModel: DEFAULT_REASONING_MODEL,
5718
+ });
5719
+ const model = modelSelection.model;
5720
+ const localAgent = new LocalAgent({
5721
+ apiKey: runtimeMode === 'direct' ? anthKey : null,
5722
+ openRouterKey: runtimeMode === 'direct' ? openRouterKey : null,
5723
+ model,
5724
+ toolExecutor,
5725
+ verbose: Boolean(cliArgs.verbose),
5726
+ cwd: safeCwd(),
5727
+ maxTurns: 50,
5728
+ gatewayUrl: runtimeMode === 'local' ? gatewayUrl : null,
5729
+ gatewayToken: runtimeMode === 'local' ? creds.token : null,
5730
+ sessionId: session.id || localSessionId,
5731
+ approvalManager: approval,
5732
+ extraToolSchemas: pluginSchemas,
5733
+ });
5734
+ activeLocalAgent = localAgent;
5735
+ const client = {
5736
+ execute: (instruction, context, history) => localAgent.execute(instruction, context, history),
5737
+ cancel: () => localAgent.cancel(),
5738
+ sendIntervention: (instruction, options) => localAgent.sendIntervention(instruction, options),
5739
+ get currentTaskId() { return null; },
5740
+ };
5741
+ process.stderr.write(` ${c.dim(`[${runtimeMode}] npm agent loop → ${runtimeMode === 'local' ? 'Bahulam Gateway' : 'provider'}`)}\n`);
5742
+ try {
5743
+ // ── Document and vision preparation continues below ──
5744
+ // The local/direct client uses the same execution call and history.
5745
+ await _executeWithClient(client);
5746
+ } finally {
5747
+ if (activeLocalAgent === localAgent) activeLocalAgent = null;
5748
+ }
5749
+ return;
5750
+ }
5657
5751
 
5658
5752
  // Create or reuse stream client — sessionId persists across turns.
5659
5753
  // The same client also owns the authenticated vision-analysis preflight.
5660
- if (!streamClient || streamClient.baseUrl !== creds.backendUrl || streamClient.token !== creds.token) {
5754
+ if (!streamClient || streamClient.baseUrl !== creds.backendUrl || streamClient.token !== creds.token || streamClient.mode !== runtimeMode) {
5661
5755
  streamClient = new BahulamStreamClient({
5662
5756
  baseUrl: creds.backendUrl,
5663
5757
  token: creds.token,
5664
5758
  toolExecutor,
5665
5759
  approvalManager: approval,
5666
5760
  pluginRegistry,
5761
+ mode: runtimeMode === 'bundled' ? 'bundled' : 'remote',
5667
5762
  });
5668
5763
  }
5669
5764
  const client = streamClient;
5670
- if (session.id && !client.sessionId) {
5671
- client.sessionId = session.id;
5672
- }
5765
+ if (session.id && !client.sessionId) client.sessionId = session.id;
5766
+
5767
+ await _executeWithClient(client);
5768
+
5769
+ async function _executeWithClient(client) {
5673
5770
 
5674
5771
  try {
5675
5772
  // ── Document attachments (client-side, PRD-091 shape 1) ──
@@ -5703,6 +5800,14 @@ export async function startTerminalRepl() {
5703
5800
  type: 'attachments',
5704
5801
  data: { attachments: prepared.attachments.map(publicAttachmentMetadata) },
5705
5802
  });
5803
+ // Vision analysis is a backend capability on the remote/bundled SSE
5804
+ // client. Local/direct still execute the coding turn locally, but do
5805
+ // not silently send an attachment to the backend for preprocessing.
5806
+ if (typeof client.analyzeVision !== 'function') {
5807
+ input = prepared.instruction || originalInput;
5808
+ pending.length = 0;
5809
+ process.stderr.write(` ${c.dim('Vision analysis is unavailable in this runtime mode; continuing without image analysis.')}\n`);
5810
+ } else {
5706
5811
  const approved = await confirmVisionUpload(ctx, prepared.attachments, { skip: skipPerms });
5707
5812
  pending.length = 0;
5708
5813
  if (!approved) {
@@ -5726,6 +5831,7 @@ export async function startTerminalRepl() {
5726
5831
  });
5727
5832
  input = appendVisionAnalysisToInstruction(prepared.instruction, analysis);
5728
5833
  }
5834
+ }
5729
5835
  } else {
5730
5836
  input = prepared.instruction || originalInput;
5731
5837
  }
@@ -5831,7 +5937,7 @@ export async function startTerminalRepl() {
5831
5937
  process.stderr.write('\n');
5832
5938
  }
5833
5939
  renderBlockBoundary('user', { compactSame: true });
5834
- process.stderr.write(`${transcriptHeader('you', { tone: 'user' })} ${paint.text.dim('follow-up')}\n`);
5940
+ process.stderr.write(`${transcriptHeader('you', { tone: 'user' })} ${paint.text.dim('added instruction')}\n`);
5835
5941
  for (const line of String(instruction || '').split('\n')) {
5836
5942
  process.stderr.write(`${transcriptLine(line, { tone: 'user' })}\n`);
5837
5943
  }
@@ -6130,8 +6236,24 @@ export async function startTerminalRepl() {
6130
6236
 
6131
6237
  // Let approval manager pause/resume this listener
6132
6238
  approval.setExecutionHooks({
6133
- onPause: () => { execListenerActive = false; },
6239
+ onPause: () => {
6240
+ execListenerActive = false;
6241
+ // The tool card/spinner is a transient status surface. Clear it
6242
+ // before ApprovalManager paints its dock overlay, otherwise the
6243
+ // render queue can keep repainting over the approval menu while
6244
+ // the approval key is still being consumed correctly.
6245
+ clearPinnedStatus();
6246
+ },
6134
6247
  onResume: () => { execListenerActive = true; },
6248
+ onApprovalPromptStart: ({ tool, args, tier }) => {
6249
+ clearPinnedStatus();
6250
+ renderBlockBoundary('status', { compactSame: true });
6251
+ const summary = toolDisplaySummary(tool, args || {});
6252
+ const label = toolDisplayLabel(tool);
6253
+ const subject = summary ? `${label} ${summary}` : label;
6254
+ process.stderr.write(` ${c.yellow('?')} ${c.dim(`approval required · ${subject} · ${tier || 'tool'}`)}\n`);
6255
+ runtime.lastRenderedBlock = 'status';
6256
+ },
6135
6257
  onApprovalPromptEnd: () => {
6136
6258
  if (!isInputDockMounted()) return;
6137
6259
  renderDockInput(executionInputPrefix(), executionInputBuffer, {
@@ -6224,6 +6346,15 @@ export async function startTerminalRepl() {
6224
6346
  }
6225
6347
  if (session.modelMode) execContext.model_mode = session.modelMode;
6226
6348
  if (session.routePreference) execContext.model_route = session.routePreference;
6349
+ const modelSelection = resolveModelSelection({
6350
+ explicitModel: cliArgs.model,
6351
+ modelOverrides: session.modelOverrides,
6352
+ modelMode: session.modelMode,
6353
+ modelRoute: session.routePreference,
6354
+ profileModels: { reasoning: session.model },
6355
+ modeModels: { fast: creds.models?.fast },
6356
+ });
6357
+ Object.assign(execContext, applyModelSelection({}, modelSelection));
6227
6358
  // PRD-071: seed work_scope from CLI so the backend has a byte-stable
6228
6359
  // scope block from turn 1. Uses projectResources already gathered by
6229
6360
  // the envelope above.
@@ -6243,7 +6374,7 @@ export async function startTerminalRepl() {
6243
6374
  // of the local bundled runtime. Session is bootstrapped lazily on
6244
6375
  // first turn and reused across the REPL. Falls through to the
6245
6376
  // existing local-agent path when the flag is unset (default today).
6246
- const _useGatewayLoop = process.env.BAHULAM_USE_GATEWAY_LOOP === '1';
6377
+ const _useGatewayLoop = runtimeMode === 'remote' && process.env.BAHULAM_USE_GATEWAY_LOOP === '1';
6247
6378
  let _turnIterable;
6248
6379
  if (_useGatewayLoop) {
6249
6380
  if (!session.gatewaySession) {
@@ -6364,6 +6495,8 @@ export async function startTerminalRepl() {
6364
6495
  showPrompt();
6365
6496
  }
6366
6497
 
6498
+ }
6499
+
6367
6500
  rl.on('close', async () => {
6368
6501
  clearSlashHint({ restoreCursor: false });
6369
6502
  inputActive = false;
@@ -10,7 +10,6 @@ import { EditTool } from './edit.mjs';
10
10
  import { WriteTool } from './write.mjs';
11
11
  import { GlobTool } from './glob.mjs';
12
12
  import { GrepTool } from './grep.mjs';
13
- import { AgentTool } from './agent.mjs';
14
13
  import { WebFetchTool } from './web-fetch.mjs';
15
14
  import { WebSearchTool } from './web-search.mjs';
16
15
  import { TodoWriteTool } from './todo-write.mjs';
@@ -48,7 +47,6 @@ const BUILTIN_TOOLS = [
48
47
  WriteTool,
49
48
  GlobTool,
50
49
  GrepTool,
51
- AgentTool,
52
50
  WebFetchTool,
53
51
  WebSearchTool,
54
52
  TodoWriteTool,
@@ -90,20 +88,7 @@ export function createToolRegistry({
90
88
  } = {}) {
91
89
  const tools = new Map();
92
90
  for (const Tool of BUILTIN_TOOLS) {
93
- if (Tool === AgentTool) {
94
- tools.set(Tool.name, {
95
- ...Tool,
96
- async call(input, options = {}) {
97
- return Tool.call(input, {
98
- ...options,
99
- pluginRegistry: options.pluginRegistry || pluginRegistry,
100
- stateEmit: options.stateEmit || stateEmit,
101
- });
102
- },
103
- });
104
- } else {
105
- tools.set(Tool.name, Tool);
106
- }
91
+ tools.set(Tool.name, Tool);
107
92
  }
108
93
 
109
94
  const pluginStateHandles = new Map();