@bahulam/code 2.6.13 → 2.6.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 +4 -4
- package/src/agents/scaffold.mjs +1 -0
- package/src/commands/agent.mjs +3 -2
- package/src/core/approval-log.mjs +22 -4
- package/src/core/approval.mjs +172 -24
- package/src/core/headless.mjs +14 -3
- package/src/core/local-agent.mjs +3 -2
- package/src/core/tool-executor.mjs +13 -3
- package/src/index.mjs +1 -1
- package/src/terminal/agents.mjs +194 -18
- package/src/terminal/repl-render.mjs +22 -4
- package/src/terminal/repl-state.mjs +1 -0
- package/src/terminal/repl.mjs +395 -21
- package/src/terminal/tool-display.mjs +135 -2
- package/src/ui/approval.mjs +200 -14
- package/src/ui/icons.mjs +11 -5
- package/src/ui/input-dock.mjs +90 -19
- package/src/ui/slash-commands.mjs +10 -0
- package/src/ui/tool-card.mjs +109 -21
- package/src/ui/tool-details.mjs +110 -11
- package/src/ui/transcript-block.mjs +2 -3
package/src/terminal/agents.mjs
CHANGED
|
@@ -10,9 +10,8 @@
|
|
|
10
10
|
* - architect: Feature architect — designs implementations, file plans
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { c
|
|
13
|
+
import { c } from './ansi.mjs';
|
|
14
14
|
import { TarangStreamClient } from '../core/stream-client.mjs';
|
|
15
|
-
import { resolveBackendUrl } from '../core/backend-url.mjs';
|
|
16
15
|
|
|
17
16
|
// ── Agent Definitions ────────────────────────────────────────
|
|
18
17
|
|
|
@@ -105,20 +104,172 @@ Rules:
|
|
|
105
104
|
|
|
106
105
|
// ── Agent Runner ─────────────────────────────────────────────
|
|
107
106
|
|
|
107
|
+
const TOOL_ALIASES = new Map([
|
|
108
|
+
['bash', 'shell'],
|
|
109
|
+
['shell_command', 'shell'],
|
|
110
|
+
['read', 'read_file'],
|
|
111
|
+
['write', 'write_file'],
|
|
112
|
+
['edit', 'edit_file'],
|
|
113
|
+
['grep', 'search_code'],
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
function canonicalToolName(value) {
|
|
117
|
+
const key = String(value || '').trim().toLowerCase();
|
|
118
|
+
return TOOL_ALIASES.get(key) || key;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const RUNTIME_PLACEHOLDER_CWDS = new Set([
|
|
122
|
+
'/workspace',
|
|
123
|
+
'/workspace/kepler-code',
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
function normalizeScopedArgs(toolName, args = {}, { projectRoot = null } = {}) {
|
|
127
|
+
if (canonicalToolName(toolName) !== 'shell' || !projectRoot) return args;
|
|
128
|
+
const next = { ...(args || {}) };
|
|
129
|
+
const cwd = String(next.cwd || '').trim();
|
|
130
|
+
if (!cwd || RUNTIME_PLACEHOLDER_CWDS.has(cwd)) {
|
|
131
|
+
next.cwd = projectRoot;
|
|
132
|
+
}
|
|
133
|
+
return next;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function createScopedToolExecutor(baseExecutor, agent, { projectRoot = null } = {}) {
|
|
137
|
+
const tools = Array.isArray(agent.tools) ? agent.tools : [];
|
|
138
|
+
const allowed = new Set(tools.map(canonicalToolName).filter(Boolean));
|
|
139
|
+
if (!allowed.size) return baseExecutor;
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
...baseExecutor,
|
|
143
|
+
execute: async (toolName, args = {}, options = {}) => {
|
|
144
|
+
const canonical = canonicalToolName(toolName);
|
|
145
|
+
if (!allowed.has(canonical)) {
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
output: `Tool '${toolName}' is not allowed for agent '${agent.name}'. Allowed tools: ${tools.join(', ')}`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return baseExecutor.execute.call(
|
|
152
|
+
baseExecutor,
|
|
153
|
+
toolName,
|
|
154
|
+
normalizeScopedArgs(toolName, args, { projectRoot }),
|
|
155
|
+
options,
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function findBuiltinAgent(agentName) {
|
|
162
|
+
const target = String(agentName || '').trim().toLowerCase();
|
|
163
|
+
return BUILTIN_AGENTS.find(agent => agent.command === target || agent.name.toLowerCase() === target) || null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function localAgentMatches(agent, target) {
|
|
167
|
+
const needle = String(target || '').trim().toLowerCase();
|
|
168
|
+
if (!needle) return false;
|
|
169
|
+
return [
|
|
170
|
+
agent.slug,
|
|
171
|
+
agent.id,
|
|
172
|
+
agent.name,
|
|
173
|
+
].some(value => String(value || '').trim().toLowerCase() === needle);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function normalizeRunnableAgent(agent) {
|
|
177
|
+
const spec = agent?.spec || {};
|
|
178
|
+
const config = agent?.config || spec.config || agent?.raw_config || {};
|
|
179
|
+
const configAgent = config.agent || {};
|
|
180
|
+
const tools = Array.isArray(agent?.tools)
|
|
181
|
+
? agent.tools
|
|
182
|
+
: Array.isArray(spec.tools)
|
|
183
|
+
? spec.tools
|
|
184
|
+
: Array.isArray(config.tools)
|
|
185
|
+
? config.tools
|
|
186
|
+
: [];
|
|
187
|
+
|
|
188
|
+
const name = agent?.name || spec.name || agent?.slug || agent?.command || 'agent';
|
|
189
|
+
return {
|
|
190
|
+
command: agent?.command || agent?.slug || spec.slug || name,
|
|
191
|
+
slug: agent?.slug || spec.slug || agent?.command || name,
|
|
192
|
+
name,
|
|
193
|
+
description: agent?.description || spec.description || '',
|
|
194
|
+
role: agent?.role || spec.role || 'specialist',
|
|
195
|
+
icon: agent?.icon || '◇',
|
|
196
|
+
systemPrompt: agent?.systemPrompt || agent?.system_prompt || agent?.prompt || spec.system_prompt || configAgent.system_prompt || '',
|
|
197
|
+
readOnly: Boolean(agent?.readOnly),
|
|
198
|
+
model: agent?.model || spec.model || configAgent.model || null,
|
|
199
|
+
models: agent?.models || spec.models || configAgent.models || null,
|
|
200
|
+
tools,
|
|
201
|
+
source: agent?.source || spec.source || '',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function agentInstructionPrefix(agent, execContext = {}) {
|
|
206
|
+
const lines = [];
|
|
207
|
+
if (agent.systemPrompt) {
|
|
208
|
+
lines.push(agent.systemPrompt);
|
|
209
|
+
} else {
|
|
210
|
+
lines.push(`You are ${agent.name}, a Bahulam Code sub-agent.`);
|
|
211
|
+
}
|
|
212
|
+
if (agent.description) lines.push(`\nAgent description: ${agent.description}`);
|
|
213
|
+
if (agent.role) lines.push(`Agent role: ${agent.role}`);
|
|
214
|
+
if (agent.tools.length) {
|
|
215
|
+
lines.push(
|
|
216
|
+
`Allowed tools for this sub-agent: ${agent.tools.join(', ')}. ` +
|
|
217
|
+
'Do not request tools outside this list.',
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (execContext.project_root) {
|
|
221
|
+
lines.push(`Runtime project root: ${execContext.project_root}. Use this as the cwd for shell commands unless the task explicitly requires a different registered project root.`);
|
|
222
|
+
}
|
|
223
|
+
return lines.join('\n');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function displayEventForDirectAgent(event, agent) {
|
|
227
|
+
if (!event || !event.type) return event;
|
|
228
|
+
if (!['tool_call', 'tool_request', 'tool_result', 'tool_done', 'sub_agent_tool'].includes(event.type)) {
|
|
229
|
+
return event;
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
...event,
|
|
233
|
+
data: {
|
|
234
|
+
...(event.data || {}),
|
|
235
|
+
internal: true,
|
|
236
|
+
sub_agent: event.data?.sub_agent || agent.slug || agent.command || agent.name || 'agent',
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
108
241
|
/**
|
|
109
|
-
* Run a
|
|
110
|
-
* @param {
|
|
242
|
+
* Run a normalized agent definition with the given instruction.
|
|
243
|
+
* @param {Object} agentDefinition - Built-in or .bahulam/agents definition
|
|
111
244
|
* @param {string} instruction - User's instruction
|
|
112
245
|
* @param {Object} ctx - { auth, toolExecutor, approval }
|
|
113
246
|
* @param {Object} session - Session state
|
|
114
247
|
* @param {Function} renderEvent - Event renderer function
|
|
248
|
+
* @param {Object} [options]
|
|
115
249
|
*/
|
|
116
|
-
export async function
|
|
117
|
-
const agent =
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
250
|
+
export async function runAgentDefinition(agentDefinition, instruction, ctx, session, renderEvent, options = {}) {
|
|
251
|
+
const agent = normalizeRunnableAgent(agentDefinition);
|
|
252
|
+
const userInstruction = String(instruction || '').trim() || 'Run your assigned task now.';
|
|
253
|
+
const suppliedContext = options.execContext || options.context || {};
|
|
254
|
+
const baseCwd = suppliedContext.cwd || options.cwd || process.cwd();
|
|
255
|
+
const suppliedSubAgent = suppliedContext.sub_agent && typeof suppliedContext.sub_agent === 'object'
|
|
256
|
+
? suppliedContext.sub_agent
|
|
257
|
+
: {};
|
|
258
|
+
const projectRoot = suppliedContext.project_root || null;
|
|
259
|
+
const execContext = {
|
|
260
|
+
...suppliedContext,
|
|
261
|
+
cwd: suppliedContext.cwd || baseCwd,
|
|
262
|
+
...(projectRoot ? { project_root: projectRoot } : {}),
|
|
263
|
+
sub_agent: {
|
|
264
|
+
...suppliedSubAgent,
|
|
265
|
+
name: agent.name,
|
|
266
|
+
slug: agent.slug,
|
|
267
|
+
role: agent.role,
|
|
268
|
+
description: agent.description,
|
|
269
|
+
tools: agent.tools,
|
|
270
|
+
source: agent.source,
|
|
271
|
+
},
|
|
272
|
+
};
|
|
122
273
|
|
|
123
274
|
const creds = ctx.auth.loadCredentials();
|
|
124
275
|
if (!creds.token) {
|
|
@@ -129,49 +280,74 @@ export async function runAgent(agentName, instruction, ctx, session, renderEvent
|
|
|
129
280
|
// Header
|
|
130
281
|
process.stderr.write(`\n ${agent.icon} ${c.bold(c.brand(agent.name))}\n`);
|
|
131
282
|
process.stderr.write(` ${c.gray('─'.repeat(40))}\n`);
|
|
132
|
-
process.stderr.write(` ${c.gray(
|
|
283
|
+
if (agent.description) process.stderr.write(` ${c.gray(agent.description)}\n`);
|
|
284
|
+
if (agent.source) process.stderr.write(` ${c.dim(agent.source)}\n`);
|
|
285
|
+
process.stderr.write(` ${c.gray(userInstruction)}\n\n`);
|
|
133
286
|
|
|
134
287
|
// Prepend agent system prompt to instruction
|
|
135
|
-
const fullInstruction = `${agent
|
|
288
|
+
const fullInstruction = `${agentInstructionPrefix(agent, execContext)}\n\n---\n\nUser request: ${userInstruction}`;
|
|
136
289
|
|
|
137
290
|
// For read-only agents, use a restricted approval manager
|
|
138
291
|
const { ApprovalManager } = await import('../core/approval.mjs');
|
|
139
292
|
const agentApproval = agent.readOnly
|
|
140
293
|
? new ApprovalManager({ planMode: true }) // planMode blocks all writes
|
|
141
294
|
: ctx.approval;
|
|
295
|
+
const toolExecutor = createScopedToolExecutor(ctx.toolExecutor, agent, {
|
|
296
|
+
projectRoot: execContext.project_root || null,
|
|
297
|
+
});
|
|
142
298
|
|
|
143
299
|
const client = new TarangStreamClient({
|
|
144
300
|
baseUrl: creds.backendUrl,
|
|
145
301
|
token: creds.token,
|
|
146
|
-
toolExecutor
|
|
302
|
+
toolExecutor,
|
|
147
303
|
approvalManager: agentApproval,
|
|
148
304
|
});
|
|
149
305
|
|
|
150
306
|
session.turns++;
|
|
151
307
|
session.toolCalls = 0;
|
|
152
308
|
let assistantContent = '';
|
|
309
|
+
if (agent.model) execContext.model_override = agent.model;
|
|
310
|
+
if (agent.models && typeof agent.models === 'object' && Object.keys(agent.models).length) {
|
|
311
|
+
execContext.model_overrides = agent.models;
|
|
312
|
+
}
|
|
153
313
|
|
|
154
314
|
try {
|
|
155
|
-
for await (const event of client.execute(fullInstruction,
|
|
156
|
-
renderEvent(event);
|
|
315
|
+
for await (const event of client.execute(fullInstruction, execContext)) {
|
|
316
|
+
renderEvent(displayEventForDirectAgent(event, agent));
|
|
157
317
|
|
|
158
318
|
if (event.type === 'content' || event.type === 'content_partial') {
|
|
159
319
|
const text = event.data?.text || '';
|
|
160
|
-
if (text) assistantContent
|
|
320
|
+
if (text) assistantContent += text;
|
|
161
321
|
}
|
|
162
322
|
}
|
|
163
323
|
} catch (err) {
|
|
164
|
-
inPlace('');
|
|
165
324
|
process.stderr.write(` ${c.red('Agent error: ' + err.message)}\n`);
|
|
166
325
|
}
|
|
167
326
|
|
|
168
327
|
// Save to conversation history
|
|
169
328
|
if (assistantContent) {
|
|
170
329
|
session.history.push(
|
|
171
|
-
{ role: 'user', content: `[${agent.name}] ${
|
|
330
|
+
{ role: 'user', content: `[${agent.name}] ${userInstruction}` },
|
|
172
331
|
{ role: 'assistant', content: assistantContent }
|
|
173
332
|
);
|
|
174
333
|
}
|
|
175
334
|
|
|
176
335
|
process.stderr.write('\n');
|
|
177
336
|
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Run a built-in agent with the given instruction.
|
|
340
|
+
* @param {string} agentName - e.g. 'explore', 'review', 'architect'
|
|
341
|
+
* @param {string} instruction - User's instruction
|
|
342
|
+
* @param {Object} ctx - { auth, toolExecutor, approval }
|
|
343
|
+
* @param {Object} session - Session state
|
|
344
|
+
* @param {Function} renderEvent - Event renderer function
|
|
345
|
+
*/
|
|
346
|
+
export async function runAgent(agentName, instruction, ctx, session, renderEvent) {
|
|
347
|
+
const agent = findBuiltinAgent(agentName);
|
|
348
|
+
if (!agent) {
|
|
349
|
+
process.stderr.write(` ${c.red('Unknown agent: ' + agentName)}\n`);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
return runAgentDefinition(agent, instruction, ctx, session, renderEvent);
|
|
353
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - Content streaming (SSE partials → debounced markdown flush)
|
|
10
10
|
* - Spinner (single animated line, shared across concerns)
|
|
11
11
|
* - Stagnation banner
|
|
12
|
-
* - Detail expansion (/last, /expand N,
|
|
12
|
+
* - Detail expansion (/last, /expand N, F2)
|
|
13
13
|
*
|
|
14
14
|
* All mutable state lives in repl-state.mjs (`runtime`). Consumers of this
|
|
15
15
|
* module (renderEvent, handleCommand, keypress handlers) import the specific
|
|
@@ -22,6 +22,8 @@ import { runtime, session } from './repl-state.mjs';
|
|
|
22
22
|
import { fitAnsiLine } from './repl-format.mjs';
|
|
23
23
|
import { exploreCategory, isExploreTool } from './repl-explore.mjs';
|
|
24
24
|
import {
|
|
25
|
+
clearPinnedStatus,
|
|
26
|
+
drawPinnedStatus,
|
|
25
27
|
isInputDockMounted,
|
|
26
28
|
moveToContent,
|
|
27
29
|
} from '../ui/input-dock.mjs';
|
|
@@ -285,7 +287,9 @@ export function renderToolResult(data, eventType = 'tool_result') {
|
|
|
285
287
|
|
|
286
288
|
const { text, tone: t } = summarizeResult(tool, data);
|
|
287
289
|
// Em dash reads more like prose than a system arrow.
|
|
288
|
-
const arrow =
|
|
290
|
+
const arrow = shellResultTool(tool)
|
|
291
|
+
? `${paint.text.dim('result')} ${paint.text.dim('—')}`
|
|
292
|
+
: paint.text.dim('—');
|
|
289
293
|
const painter = t === 'success' ? paint.state.success
|
|
290
294
|
: t === 'warn' ? paint.state.warn
|
|
291
295
|
: t === 'danger' ? paint.state.danger
|
|
@@ -352,6 +356,13 @@ export function renderToolResult(data, eventType = 'tool_result') {
|
|
|
352
356
|
}
|
|
353
357
|
}
|
|
354
358
|
|
|
359
|
+
function shellResultTool(tool) {
|
|
360
|
+
return [
|
|
361
|
+
'shell', 'run_tests', 'validate_build', 'lint_check',
|
|
362
|
+
'validate_file', 'validate_structure',
|
|
363
|
+
].includes(String(tool || '').toLowerCase());
|
|
364
|
+
}
|
|
365
|
+
|
|
355
366
|
// ── Expand handler — `d`, `/last`, `/expand` ───────────────────────────
|
|
356
367
|
//
|
|
357
368
|
// All three call into the same renderer so output is consistent across
|
|
@@ -461,7 +472,10 @@ export function startSpinner(text) {
|
|
|
461
472
|
if (isExploreActive && isInputDockMounted()) {
|
|
462
473
|
return;
|
|
463
474
|
}
|
|
464
|
-
if (isInputDockMounted())
|
|
475
|
+
if (isInputDockMounted()) {
|
|
476
|
+
drawPinnedStatus(rendered);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
465
479
|
inPlace(rendered);
|
|
466
480
|
}, 80);
|
|
467
481
|
}
|
|
@@ -477,7 +491,11 @@ export function stopSpinner() {
|
|
|
477
491
|
if (runtime.exploreRun && runtime.exploreRun.lineActive) return;
|
|
478
492
|
if (runtime.spinInterval) { clearInterval(runtime.spinInterval); runtime.spinInterval = null; }
|
|
479
493
|
runtime.spinText = '';
|
|
480
|
-
if (isInputDockMounted())
|
|
494
|
+
if (isInputDockMounted()) {
|
|
495
|
+
clearPinnedStatus();
|
|
496
|
+
moveToContent();
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
481
499
|
inPlace('');
|
|
482
500
|
}
|
|
483
501
|
|
|
@@ -42,6 +42,7 @@ export const runtime = {
|
|
|
42
42
|
|
|
43
43
|
// Explore-run collapse (read/list/search/index bursts as concise progress).
|
|
44
44
|
exploreRun: { counts: {}, recent: [], lineActive: false, lastPrintedSummary: '', lastPrintedTotal: 0, lastPrintedAt: 0 },
|
|
45
|
+
foldedSubAgentTools: null, // { agentType, entries, startedAt } for default/quiet folded sub-agent tools
|
|
45
46
|
|
|
46
47
|
// Animated spinner state (single shared interval; text/frame drive inPlace).
|
|
47
48
|
spinInterval: null,
|