@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.
- package/package.json +1 -1
- package/src/commands/install.mjs +88 -6
- package/src/commands/plugin-manage.mjs +146 -5
- package/src/config/cli-args.mjs +23 -3
- package/src/config/model-catalog.mjs +9 -0
- package/src/config/model-defaults.mjs +16 -0
- package/src/core/approval.mjs +8 -1
- package/src/core/attachments.mjs +4 -0
- package/src/core/backend-url.mjs +16 -0
- package/src/core/context-reduction.mjs +207 -0
- package/src/core/headless.mjs +85 -13
- package/src/core/local-agent.mjs +352 -44
- package/src/core/mode-selector.mjs +5 -4
- package/src/core/model-selection.mjs +72 -0
- package/src/core/request-retry.mjs +106 -0
- package/src/core/tool-error.mjs +155 -0
- package/src/core/usage-normalization.mjs +32 -0
- package/src/local-service/agent-relay.mjs +279 -7
- package/src/local-service/server.mjs +37 -0
- package/src/orchestration/node-runner.mjs +7 -3
- package/src/plugins/executor.mjs +4 -4
- package/src/plugins/lifecycle.mjs +337 -0
- package/src/plugins/manifest.mjs +37 -0
- package/src/plugins/pi-compat/requirements.mjs +30 -17
- package/src/plugins/preflight.mjs +37 -0
- package/src/terminal/main.mjs +10 -1
- package/src/terminal/paste-input.mjs +52 -0
- package/src/terminal/repl.mjs +169 -36
- package/src/tools/registry.mjs +1 -16
- package/src/core/agent-loop.mjs +0 -503
- package/src/core/context-manager.mjs +0 -198
- package/src/tools/agent.mjs +0 -142
|
@@ -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
|
+
}
|
package/src/terminal/repl.mjs
CHANGED
|
@@ -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 {
|
|
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
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
5023
|
-
//
|
|
5024
|
-
//
|
|
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
|
-
|
|
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
|
|
5579
|
+
const pasted = classifyPastedPromptPayload([...pastedLines, trailing].join('\n'), { looksLikeAttachmentReference });
|
|
5541
5580
|
_promptHasInsertedPaste = true;
|
|
5542
|
-
_pastedInputValue = text;
|
|
5543
|
-
_pastedInputLabel =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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: () => {
|
|
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;
|
package/src/tools/registry.mjs
CHANGED
|
@@ -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
|
-
|
|
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();
|