@bahulam/code 0.1.10 → 0.1.12
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 +5 -2
- package/src/agents/loader.mjs +26 -4
- package/src/auth/bahulam-auth.mjs +2 -13
- package/src/auth/tarang-auth.mjs +313 -0
- package/src/commands/agent.mjs +7 -7
- package/src/commands/plugin-manage.mjs +449 -0
- package/src/commands/plugin.mjs +247 -0
- package/src/config/cli-args.mjs +16 -0
- package/src/config/env.mjs +2 -0
- package/src/config/hook-runner.mjs +8 -8
- package/src/config/memory-loader.mjs +7 -3
- package/src/config/settings-loader.mjs +5 -3
- package/src/core/attachments.mjs +2 -2
- package/src/core/background-tasks.mjs +186 -0
- package/src/core/headless.mjs +59 -3
- package/src/core/local-agent.mjs +10 -1
- package/src/core/local-store.mjs +10 -10
- package/src/core/paths.mjs +10 -96
- package/src/core/policy-resolver.mjs +1 -1
- package/src/core/project-context-loader.mjs +2 -2
- package/src/core/risk-tier.mjs +1 -0
- package/src/core/stream-client.mjs +148 -1
- package/src/core/system-prompt.mjs +31 -12
- package/src/core/tool-executor.mjs +457 -12
- package/src/local-service/agent-relay.mjs +139 -12
- package/src/local-service/server.mjs +345 -20
- package/src/orchestration/approval.mjs +30 -0
- package/src/orchestration/completion-triggers.mjs +40 -0
- package/src/orchestration/dispatch.mjs +118 -0
- package/src/orchestration/events.mjs +19 -0
- package/src/orchestration/graph.mjs +126 -0
- package/src/orchestration/node-runner.mjs +193 -0
- package/src/orchestration/runner.mjs +200 -0
- package/src/plugins/executor.mjs +121 -0
- package/src/plugins/loader.mjs +123 -123
- package/src/plugins/manifest.mjs +291 -0
- package/src/plugins/preflight.mjs +227 -0
- package/src/plugins/registry.mjs +233 -0
- package/src/plugins/state.mjs +290 -0
- package/src/terminal/agents.mjs +26 -4
- package/src/terminal/init.mjs +2 -2
- package/src/terminal/main.mjs +83 -4
- package/src/terminal/repl-explore.mjs +1 -1
- package/src/terminal/repl-render.mjs +67 -12
- package/src/terminal/repl-state.mjs +4 -2
- package/src/terminal/repl.mjs +621 -99
- package/src/tools/agent.mjs +6 -2
- package/src/tools/analyze-image.mjs +1 -1
- package/src/tools/project-overview.mjs +7 -7
- package/src/tools/registry.mjs +88 -4
- package/src/ui/slash-commands.mjs +19 -1
- package/src/ui/sub-agent.mjs +14 -8
package/src/terminal/main.mjs
CHANGED
|
@@ -23,6 +23,64 @@ import { BahulamAuth as Auth } from '../auth/bahulam-auth.mjs';
|
|
|
23
23
|
const subcommand = process.argv[2];
|
|
24
24
|
const subcommandArgs = process.argv.slice(3);
|
|
25
25
|
|
|
26
|
+
const PLUGIN_MANAGEMENT_COMMANDS = new Set([
|
|
27
|
+
'install', 'validate', 'check', 'lint',
|
|
28
|
+
'list', 'ls', 'remove', 'rm', 'uninstall',
|
|
29
|
+
'enable', 'disable', 'info', 'update', 'upgrade',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
function parsePluginArgs(argv) {
|
|
33
|
+
const parsed = {
|
|
34
|
+
action: null, // 'open' (default) or a management verb
|
|
35
|
+
pluginName: null,
|
|
36
|
+
targetPath: null,
|
|
37
|
+
source: null, // install source: git url, tarball, local dir
|
|
38
|
+
port: 0,
|
|
39
|
+
open: true,
|
|
40
|
+
help: false,
|
|
41
|
+
json: false,
|
|
42
|
+
global: true, // install target: ~/.bahulam vs project .bahulam
|
|
43
|
+
force: false,
|
|
44
|
+
ref: null, // git branch/tag/commit
|
|
45
|
+
};
|
|
46
|
+
const positional = [];
|
|
47
|
+
for (let i = 0; i < argv.length; i++) {
|
|
48
|
+
const arg = argv[i];
|
|
49
|
+
switch (arg) {
|
|
50
|
+
case '--help':
|
|
51
|
+
case '-h': parsed.help = true; break;
|
|
52
|
+
case '--port': parsed.port = Number(argv[++i]) || 0; break;
|
|
53
|
+
case '--no-open': parsed.open = false; break;
|
|
54
|
+
case '--json': parsed.json = true; parsed.open = false; break;
|
|
55
|
+
case '--project': parsed.global = false; break;
|
|
56
|
+
case '--global': parsed.global = true; break;
|
|
57
|
+
case '--force': case '-f': parsed.force = true; break;
|
|
58
|
+
case '--ref': case '--tag': case '--branch': parsed.ref = argv[++i]; break;
|
|
59
|
+
default:
|
|
60
|
+
if (!arg.startsWith('-')) positional.push(arg);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (positional.length && PLUGIN_MANAGEMENT_COMMANDS.has(positional[0].toLowerCase())) {
|
|
65
|
+
parsed.action = positional.shift().toLowerCase();
|
|
66
|
+
if (parsed.action === 'install') parsed.source = positional.shift() || null;
|
|
67
|
+
else if (['validate', 'check', 'lint'].includes(parsed.action)) {
|
|
68
|
+
// Accepts either a directory path or an installed plugin name.
|
|
69
|
+
const arg = positional.shift() || null;
|
|
70
|
+
if (arg && (arg.includes('/') || fs.existsSync?.(arg))) parsed.source = arg;
|
|
71
|
+
else parsed.pluginName = arg;
|
|
72
|
+
}
|
|
73
|
+
else if (['info', 'remove', 'rm', 'uninstall', 'enable', 'disable', 'update', 'upgrade'].includes(parsed.action)) {
|
|
74
|
+
parsed.pluginName = positional.shift() || null;
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
parsed.action = 'open';
|
|
78
|
+
parsed.pluginName = positional.shift() || null;
|
|
79
|
+
parsed.targetPath = positional.shift() || null;
|
|
80
|
+
}
|
|
81
|
+
return parsed;
|
|
82
|
+
}
|
|
83
|
+
|
|
26
84
|
function parseKeplerSubcommandArgs(command, argv) {
|
|
27
85
|
const parsed = {
|
|
28
86
|
command,
|
|
@@ -221,6 +279,18 @@ async function main() {
|
|
|
221
279
|
return;
|
|
222
280
|
}
|
|
223
281
|
|
|
282
|
+
if (subcommand === 'plugin' || subcommand === 'plugins') {
|
|
283
|
+
const args = parsePluginArgs(subcommandArgs);
|
|
284
|
+
if (args.action && args.action !== 'open') {
|
|
285
|
+
const { handlePluginManagementCommand } = await import('../commands/plugin-manage.mjs');
|
|
286
|
+
await handlePluginManagementCommand(args, { cwd: process.cwd() });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const { handlePluginCommand } = await import('../commands/plugin.mjs');
|
|
290
|
+
await handlePluginCommand(args, { cwd: process.cwd() });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
224
294
|
if (subcommand === 'version' || subcommand === '--version' || subcommand === '-v') {
|
|
225
295
|
const { createRequire } = await import('node:module');
|
|
226
296
|
const require = createRequire(import.meta.url);
|
|
@@ -236,6 +306,8 @@ async function main() {
|
|
|
236
306
|
\x1b[1mUsage:\x1b[0m
|
|
237
307
|
bahulam Start interactive REPL
|
|
238
308
|
bahulam "instruction" Run a single instruction
|
|
309
|
+
bahulam --agent <slug> -p "x" Run a named agent (local deterministic graph)
|
|
310
|
+
bahulam --workflow <name> -p Run a named workflow (local deterministic graph)
|
|
239
311
|
bahulam --headless -p "x" Non-interactive: auto-approve, JSONL output
|
|
240
312
|
bahulam --headless -p "x" --vision screenshot.png
|
|
241
313
|
Attach an image via the vision analysis pipeline
|
|
@@ -260,6 +332,9 @@ async function main() {
|
|
|
260
332
|
bahulam workspace list List recent local workspace sessions
|
|
261
333
|
bahulam local open [path] Alias for workspace open
|
|
262
334
|
|
|
335
|
+
\x1b[1mPlugins:\x1b[0m
|
|
336
|
+
bahulam plugin <name> [path] Open a workspace with a named plugin
|
|
337
|
+
|
|
263
338
|
\x1b[1mAnalytics:\x1b[0m
|
|
264
339
|
bahulam sessions List recent local sessions
|
|
265
340
|
bahulam stats Show aggregate local session stats
|
|
@@ -287,7 +362,7 @@ async function main() {
|
|
|
287
362
|
/architect <query> Spawn architecture planning agent
|
|
288
363
|
/agents create <name> Create project-local user-defined agent YAML
|
|
289
364
|
/agents edit <name> Open a local agent YAML in your editor
|
|
290
|
-
/agents sync [name]
|
|
365
|
+
/agents sync [name] Optionally publish local agents to backend/account
|
|
291
366
|
/attach <image-path> Attach an image to next prompt
|
|
292
367
|
/attach clipboard Attach image copied to macOS/Windows clipboard
|
|
293
368
|
/exit Exit the REPL
|
|
@@ -302,8 +377,8 @@ async function main() {
|
|
|
302
377
|
ANTHROPIC_API_KEY Direct Anthropic API key
|
|
303
378
|
OPENROUTER_API_KEY OpenRouter API key
|
|
304
379
|
BAHULAM_CONFIG_DIR Override config directory (default: ~/.bahulam)
|
|
305
|
-
|
|
306
|
-
|
|
380
|
+
BAHULAM_CONFIG_DIR Legacy config directory override
|
|
381
|
+
BAHULAM_RECONNECT_MAX_ELAPSED_MS
|
|
307
382
|
Max reconnect window for dropped streams
|
|
308
383
|
BAHULAM_TTY_MODE=stable Scrollback-safe transcript if fixed dock redraws leak
|
|
309
384
|
BAHULAM_BLOCK_SEPARATOR Tool/content separator: space, dotted, or off
|
|
@@ -407,7 +482,9 @@ async function main() {
|
|
|
407
482
|
const daemonSpawned = process.env.BAHULAM_DAEMON_SPAWNED === '1';
|
|
408
483
|
const daemonPrompt = daemonSpawned ? (process.env.BAHULAM_DAEMON_INITIAL_PROMPT || '').trim() : '';
|
|
409
484
|
const effectivePrompt = args.prompt || (daemonSpawned && daemonPrompt) || '';
|
|
410
|
-
|
|
485
|
+
const hasGraphTarget = Boolean(args.agent || args.workflow);
|
|
486
|
+
if ((effectivePrompt || hasGraphTarget)
|
|
487
|
+
&& (daemonSpawned || process.argv.includes('--headless') || !process.stdin.isTTY || hasGraphTarget)) {
|
|
411
488
|
const { runHeadless } = await import('../core/headless.mjs');
|
|
412
489
|
await runHeadless({
|
|
413
490
|
instruction: effectivePrompt,
|
|
@@ -417,6 +494,8 @@ async function main() {
|
|
|
417
494
|
cacheReport: args.cacheReport,
|
|
418
495
|
local: args.local,
|
|
419
496
|
vision: args.vision,
|
|
497
|
+
agent: args.agent,
|
|
498
|
+
workflow: args.workflow,
|
|
420
499
|
});
|
|
421
500
|
return;
|
|
422
501
|
}
|
|
@@ -34,8 +34,8 @@ import * as queue from '../ui/render-queue.mjs';
|
|
|
34
34
|
// queue active) → coalesced last-wins status that can never interleave
|
|
35
35
|
// with content. Legacy dock path and bare-TTY inPlace stay as fallbacks
|
|
36
36
|
// until their write-sites migrate onto the queue too.
|
|
37
|
-
// Max
|
|
38
|
-
const SUB_AGENT_WINDOW_ROWS =
|
|
37
|
+
// Max live-window lines under the spinner during a sub-agent run.
|
|
38
|
+
const SUB_AGENT_WINDOW_ROWS = 8;
|
|
39
39
|
|
|
40
40
|
function statusWidth() {
|
|
41
41
|
return Math.max(8, (process.stderr.columns || process.stdout.columns || 120) - 1);
|
|
@@ -49,6 +49,18 @@ function fitStatusLines(lines) {
|
|
|
49
49
|
return (Array.isArray(lines) ? lines : [lines]).map(fitStatusLine);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function subAgentWindowLines(win) {
|
|
53
|
+
if (win?.groups instanceof Map && win.groups.size) {
|
|
54
|
+
const lines = [];
|
|
55
|
+
for (const group of win.groups.values()) {
|
|
56
|
+
if (group?.header) lines.push(group.header);
|
|
57
|
+
for (const line of group?.lines || []) lines.push(line);
|
|
58
|
+
}
|
|
59
|
+
return lines;
|
|
60
|
+
}
|
|
61
|
+
return (win?.lines || []).slice(-SUB_AGENT_WINDOW_ROWS);
|
|
62
|
+
}
|
|
63
|
+
|
|
52
64
|
function presentStatus(rendered) {
|
|
53
65
|
// Watch panel override: when active, render the watch panel entries as a
|
|
54
66
|
// multi-line status block instead of the normal spinner/status line.
|
|
@@ -69,10 +81,11 @@ function presentStatus(rendered) {
|
|
|
69
81
|
const fitted = fitStatusLine(rendered);
|
|
70
82
|
if (queue.isActive()) {
|
|
71
83
|
const win = runtime.subAgentWindow;
|
|
72
|
-
|
|
84
|
+
const subAgentLines = subAgentWindowLines(win);
|
|
85
|
+
if (win?.active && subAgentLines.length) {
|
|
73
86
|
queue.statusBlock([
|
|
74
87
|
fitted,
|
|
75
|
-
...fitStatusLines(
|
|
88
|
+
...fitStatusLines(subAgentLines.map(l => ` ${c.dim(l)}`)),
|
|
76
89
|
]);
|
|
77
90
|
return;
|
|
78
91
|
}
|
|
@@ -95,7 +108,7 @@ export function pushSubAgentWindowLine(line) {
|
|
|
95
108
|
}
|
|
96
109
|
|
|
97
110
|
export function setSubAgentWindowActive(active) {
|
|
98
|
-
runtime.subAgentWindow = { active: Boolean(active), lines: [] };
|
|
111
|
+
runtime.subAgentWindow = { active: Boolean(active), lines: [], groups: new Map() };
|
|
99
112
|
}
|
|
100
113
|
|
|
101
114
|
/**
|
|
@@ -109,6 +122,25 @@ export function rebuildSubAgentWindow(lines) {
|
|
|
109
122
|
const win = runtime.subAgentWindow;
|
|
110
123
|
if (!win?.active) return;
|
|
111
124
|
win.lines = Array.isArray(lines) ? lines.slice() : [];
|
|
125
|
+
win.groups = new Map();
|
|
126
|
+
repaintSpinnerStatus();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function rebuildSubAgentWindowGroups(groups) {
|
|
130
|
+
const win = runtime.subAgentWindow;
|
|
131
|
+
if (!win?.active) return;
|
|
132
|
+
win.groups = new Map();
|
|
133
|
+
win.lines = [];
|
|
134
|
+
for (const group of Array.isArray(groups) ? groups : []) {
|
|
135
|
+
const key = group?.runId || group?.key || group?.label;
|
|
136
|
+
if (!key) continue;
|
|
137
|
+
win.groups.set(key, {
|
|
138
|
+
runId: group.runId || null,
|
|
139
|
+
label: group.label || 'sub-agent',
|
|
140
|
+
header: group.header || group.label || 'sub-agent',
|
|
141
|
+
lines: Array.isArray(group.lines) ? group.lines.slice() : [],
|
|
142
|
+
});
|
|
143
|
+
}
|
|
112
144
|
repaintSpinnerStatus();
|
|
113
145
|
}
|
|
114
146
|
|
|
@@ -211,12 +243,12 @@ function exploreRunTotal() {
|
|
|
211
243
|
}
|
|
212
244
|
|
|
213
245
|
function exploreSnapshotEvery() {
|
|
214
|
-
const n = Number.parseInt(process.env.
|
|
246
|
+
const n = Number.parseInt(process.env.BAHULAM_EXPLORE_SNAPSHOT_EVERY || '8', 10);
|
|
215
247
|
return Number.isFinite(n) ? Math.max(1, n) : 8;
|
|
216
248
|
}
|
|
217
249
|
|
|
218
250
|
function exploreSnapshotMs() {
|
|
219
|
-
const n = Number.parseInt(process.env.
|
|
251
|
+
const n = Number.parseInt(process.env.BAHULAM_EXPLORE_SNAPSHOT_MS || '900', 10);
|
|
220
252
|
return Number.isFinite(n) ? Math.max(100, n) : 900;
|
|
221
253
|
}
|
|
222
254
|
|
|
@@ -308,11 +340,27 @@ export function renderToolCall(data) {
|
|
|
308
340
|
const args = data?.args || {};
|
|
309
341
|
const indent = subAgentIndent();
|
|
310
342
|
const callId = data?.call_id || data?._callId || `${tool}:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
343
|
+
const fromSubAgent = Boolean(data?.internal || data?.sub_agent || data?.sub_agent_label || data?.sub_agent_run_id);
|
|
311
344
|
|
|
312
345
|
// If a previous head is still pending (no result yet), flush it as a
|
|
313
346
|
// regular two-line shape before starting the next one.
|
|
314
347
|
flushPendingHead();
|
|
315
348
|
|
|
349
|
+
// Sub-agent live window (queue mode): inner tool calls stream into the
|
|
350
|
+
// fixed-height status block instead of appending transcript lines. Keep
|
|
351
|
+
// this ahead of explore-collapse so parallel explores do not merge into
|
|
352
|
+
// one global read/search spinner.
|
|
353
|
+
if (fromSubAgent && queue.isActive() && runtime.subAgentWindow?.active) {
|
|
354
|
+
recordCard({ id: callId, tool, args, startedAt: Date.now() });
|
|
355
|
+
session.toolCounts[tool] = (session.toolCounts[tool] || 0) + 1;
|
|
356
|
+
const label = readToolLabel(tool, { args });
|
|
357
|
+
const runId = data?.run_id || data?.sub_agent_run_id || '';
|
|
358
|
+
const runSuffix = runId ? ` run ${String(runId).slice(0, 8)}` : '';
|
|
359
|
+
const lane = data?.sub_agent_label ? `[${data.sub_agent_label}${runSuffix}] ` : '';
|
|
360
|
+
pushSubAgentWindowLine(label ? `${lane}→ ${tool} · ${label}` : `${lane}→ ${tool}`);
|
|
361
|
+
return; // the spinner tick paints the window block
|
|
362
|
+
}
|
|
363
|
+
|
|
316
364
|
// ── Explore-run collapse ────────────────────────────────────────────────
|
|
317
365
|
// For list/read/search/index tools, skip the per-call head entirely and
|
|
318
366
|
// update a single animated summary spinner. The transcript stays clean;
|
|
@@ -328,9 +376,8 @@ export function renderToolCall(data) {
|
|
|
328
376
|
return;
|
|
329
377
|
}
|
|
330
378
|
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
// card is still recorded so /expand, /last, and `d` show full detail.
|
|
379
|
+
// Legacy sub-agent live window fallback for events without explicit
|
|
380
|
+
// sub-agent metadata.
|
|
334
381
|
if (queue.isActive() && runtime.subAgentWindow?.active && inSubAgentBlock()) {
|
|
335
382
|
recordCard({ id: callId, tool, args, startedAt: Date.now() });
|
|
336
383
|
session.toolCounts[tool] = (session.toolCounts[tool] || 0) + 1;
|
|
@@ -410,6 +457,14 @@ export function renderToolResult(data, eventType = 'tool_result') {
|
|
|
410
457
|
indent: gutter,
|
|
411
458
|
columns: process.stderr.columns || 120,
|
|
412
459
|
});
|
|
460
|
+
const fromSubAgent = Boolean(data?.internal || data?.sub_agent || data?.sub_agent_label || data?.sub_agent_run_id);
|
|
461
|
+
|
|
462
|
+
// Sub-agent live window: the call line is already streaming in the
|
|
463
|
+
// status block; the result stays card-only (close card summarizes). This
|
|
464
|
+
// must run before explore-collapse for read/search tools used by explores.
|
|
465
|
+
if (fromSubAgent && queue.isActive() && runtime.subAgentWindow?.active) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
413
468
|
|
|
414
469
|
// Explore tools: the call already updated the summary spinner. Refresh
|
|
415
470
|
// the "latest" hint with the result's file if we have one, and skip the
|
|
@@ -421,8 +476,8 @@ export function renderToolResult(data, eventType = 'tool_result') {
|
|
|
421
476
|
return;
|
|
422
477
|
}
|
|
423
478
|
|
|
424
|
-
//
|
|
425
|
-
//
|
|
479
|
+
// Legacy sub-agent live window fallback for events without explicit
|
|
480
|
+
// sub-agent metadata.
|
|
426
481
|
if (queue.isActive() && runtime.subAgentWindow?.active && inSubAgentBlock()) {
|
|
427
482
|
return;
|
|
428
483
|
}
|
|
@@ -43,7 +43,8 @@ export const runtime = {
|
|
|
43
43
|
|
|
44
44
|
// Explore-run collapse (read/list/search/index bursts as concise progress).
|
|
45
45
|
exploreRun: { counts: {}, recent: [], lineActive: false, lastPrintedSummary: '', lastPrintedTotal: 0, lastPrintedAt: 0 },
|
|
46
|
-
foldedSubAgentTools: null, //
|
|
46
|
+
foldedSubAgentTools: null, // current folded sub-agent tool bucket
|
|
47
|
+
foldedSubAgentToolMap: new Map(), // run-aware buckets for parallel sub-agents
|
|
47
48
|
|
|
48
49
|
// Animated spinner state (single shared interval; text/frame drive inPlace).
|
|
49
50
|
spinInterval: null,
|
|
@@ -61,7 +62,7 @@ export const runtime = {
|
|
|
61
62
|
// its inner tool calls stream into a fixed-height status block (last
|
|
62
63
|
// N lines under the spinner) instead of appending to the transcript.
|
|
63
64
|
// Full detail stays on the recorded cards (/expand, /last, `d`).
|
|
64
|
-
subAgentWindow: { active: false, lines: [] },
|
|
65
|
+
subAgentWindow: { active: false, lines: [], groups: new Map() },
|
|
65
66
|
|
|
66
67
|
// PRD-092: Watch panel — toggled by /watch. When active, the spinner
|
|
67
68
|
// area renders a compact agent-activity summary instead of the spinner.
|
|
@@ -106,6 +107,7 @@ export const session = {
|
|
|
106
107
|
lastTurnDuration: 0,
|
|
107
108
|
toolCounts: {}, // per-tool histogram (mission report)
|
|
108
109
|
subAgentCounts: {}, // per-sub-agent histogram (mission report)
|
|
110
|
+
activeSubAgentRuns: new Map(), // run_id -> active sub-agent lane
|
|
109
111
|
savedUsd: 0, // total sub-agent cost (for "saved by routing")
|
|
110
112
|
lastTask: '', // most recent user prompt (mission report title)
|
|
111
113
|
lastReasoning: '', // captured from agent for /why
|