@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.
Files changed (52) hide show
  1. package/package.json +5 -2
  2. package/src/agents/loader.mjs +26 -4
  3. package/src/auth/bahulam-auth.mjs +2 -13
  4. package/src/auth/tarang-auth.mjs +313 -0
  5. package/src/commands/agent.mjs +7 -7
  6. package/src/commands/plugin-manage.mjs +449 -0
  7. package/src/commands/plugin.mjs +247 -0
  8. package/src/config/cli-args.mjs +16 -0
  9. package/src/config/env.mjs +2 -0
  10. package/src/config/hook-runner.mjs +8 -8
  11. package/src/config/memory-loader.mjs +7 -3
  12. package/src/config/settings-loader.mjs +5 -3
  13. package/src/core/attachments.mjs +2 -2
  14. package/src/core/background-tasks.mjs +186 -0
  15. package/src/core/headless.mjs +59 -3
  16. package/src/core/local-agent.mjs +10 -1
  17. package/src/core/local-store.mjs +10 -10
  18. package/src/core/paths.mjs +10 -96
  19. package/src/core/policy-resolver.mjs +1 -1
  20. package/src/core/project-context-loader.mjs +2 -2
  21. package/src/core/risk-tier.mjs +1 -0
  22. package/src/core/stream-client.mjs +148 -1
  23. package/src/core/system-prompt.mjs +31 -12
  24. package/src/core/tool-executor.mjs +457 -12
  25. package/src/local-service/agent-relay.mjs +139 -12
  26. package/src/local-service/server.mjs +345 -20
  27. package/src/orchestration/approval.mjs +30 -0
  28. package/src/orchestration/completion-triggers.mjs +40 -0
  29. package/src/orchestration/dispatch.mjs +118 -0
  30. package/src/orchestration/events.mjs +19 -0
  31. package/src/orchestration/graph.mjs +126 -0
  32. package/src/orchestration/node-runner.mjs +193 -0
  33. package/src/orchestration/runner.mjs +200 -0
  34. package/src/plugins/executor.mjs +121 -0
  35. package/src/plugins/loader.mjs +123 -123
  36. package/src/plugins/manifest.mjs +291 -0
  37. package/src/plugins/preflight.mjs +227 -0
  38. package/src/plugins/registry.mjs +233 -0
  39. package/src/plugins/state.mjs +290 -0
  40. package/src/terminal/agents.mjs +26 -4
  41. package/src/terminal/init.mjs +2 -2
  42. package/src/terminal/main.mjs +83 -4
  43. package/src/terminal/repl-explore.mjs +1 -1
  44. package/src/terminal/repl-render.mjs +67 -12
  45. package/src/terminal/repl-state.mjs +4 -2
  46. package/src/terminal/repl.mjs +621 -99
  47. package/src/tools/agent.mjs +6 -2
  48. package/src/tools/analyze-image.mjs +1 -1
  49. package/src/tools/project-overview.mjs +7 -7
  50. package/src/tools/registry.mjs +88 -4
  51. package/src/ui/slash-commands.mjs +19 -1
  52. package/src/ui/sub-agent.mjs +14 -8
@@ -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] Sync all or one local agent to Supabase
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
- KEPLER_CONFIG_DIR Legacy config directory override
306
- KEPLER_RECONNECT_MAX_ELAPSED_MS
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
- if (effectivePrompt && (daemonSpawned || process.argv.includes('--headless') || !process.stdin.isTTY)) {
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
  }
@@ -25,7 +25,7 @@ const EXPLORE_TOOL_CATEGORY = new Map([
25
25
  ]);
26
26
 
27
27
  export function exploreCollapseEnabled() {
28
- return process.env.KEPLER_EXPLORE_COLLAPSE !== '0';
28
+ return process.env.BAHULAM_EXPLORE_COLLAPSE !== '0';
29
29
  }
30
30
 
31
31
  export function isExploreTool(tool) {
@@ -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 inner-tool lines shown under the spinner during a sub-agent run.
38
- const SUB_AGENT_WINDOW_ROWS = 7;
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
- if (win?.active && win.lines.length) {
84
+ const subAgentLines = subAgentWindowLines(win);
85
+ if (win?.active && subAgentLines.length) {
73
86
  queue.statusBlock([
74
87
  fitted,
75
- ...fitStatusLines(win.lines.slice(-SUB_AGENT_WINDOW_ROWS).map(l => ` ${c.dim(l)}`)),
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.KEPLER_EXPLORE_SNAPSHOT_EVERY || '8', 10);
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.KEPLER_EXPLORE_SNAPSHOT_MS || '900', 10);
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
- // Sub-agent live window (queue mode): inner tool calls stream into the
332
- // fixed-height status block instead of appending transcript lines. The
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
- // Sub-agent live window: the call line is already streaming in the
425
- // status block; the result stays card-only (close card summarizes).
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, // { agentType, entries, startedAt } for default/quiet folded sub-agent tools
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