@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.
@@ -10,9 +10,8 @@
10
10
  * - architect: Feature architect — designs implementations, file plans
11
11
  */
12
12
 
13
- import { c, spinner, inPlace, hr } from './ansi.mjs';
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 built-in agent with the given instruction.
110
- * @param {string} agentName - e.g. 'explore', 'review', 'architect'
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 runAgent(agentName, instruction, ctx, session, renderEvent) {
117
- const agent = BUILTIN_AGENTS.find(a => a.command === agentName);
118
- if (!agent) {
119
- process.stderr.write(` ${c.red('Unknown agent: ' + agentName)}\n`);
120
- return;
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(instruction)}\n\n`);
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.systemPrompt}\n\n---\n\nUser request: ${instruction}`;
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: ctx.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, { cwd: process.cwd() })) {
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 = text;
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}] ${instruction}` },
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, Ctrl+D)
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 = paint.text.dim('—');
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()) moveToContent();
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()) moveToContent();
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,