@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
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
9
10
  import { createRequire } from 'node:module';
10
11
  import { BahulamAuth } from '../auth/bahulam-auth.mjs';
11
12
  import { AgentHistoryTurnBuilder } from '../core/agent-history.mjs';
@@ -23,6 +24,66 @@ import { BrowserApprovalManager } from './approval-bridge.mjs';
23
24
  const __require = createRequire(import.meta.url);
24
25
  const VERSION = __require('../../package.json').version;
25
26
 
27
+ function envList(name) {
28
+ return String(process.env[name] || '')
29
+ .split(',')
30
+ .map(item => item.trim())
31
+ .filter(Boolean);
32
+ }
33
+
34
+ function activePluginNamesFromSession(session = {}) {
35
+ const names = [];
36
+ if (session.plugin?.name) names.push(session.plugin.name);
37
+ if (session.plugin?.metadata_name) names.push(session.plugin.metadata_name);
38
+ const plugins = Array.isArray(session.plugins)
39
+ ? session.plugins
40
+ : (Array.isArray(session.active_plugins) ? session.active_plugins : []);
41
+ for (const plugin of plugins) {
42
+ if (typeof plugin === 'string') names.push(plugin);
43
+ else if (plugin?.name) names.push(plugin.name);
44
+ else if (plugin?.metadata_name) names.push(plugin.metadata_name);
45
+ }
46
+ return [...new Set(names.map(name => String(name).trim()).filter(Boolean))];
47
+ }
48
+
49
+ function pluginScanDirsFromSession(session = {}) {
50
+ const dirs = [];
51
+ const addPluginParent = (plugin) => {
52
+ const pluginDir = String(plugin?.plugin_dir || plugin?.dir || '').trim();
53
+ if (pluginDir) dirs.push(path.dirname(pluginDir));
54
+ };
55
+ if (session.plugin) addPluginParent(session.plugin);
56
+ const plugins = Array.isArray(session.plugins)
57
+ ? session.plugins
58
+ : (Array.isArray(session.active_plugins) ? session.active_plugins : []);
59
+ for (const plugin of plugins) {
60
+ if (plugin && typeof plugin === 'object') addPluginParent(plugin);
61
+ }
62
+ return [...new Set(dirs.filter(Boolean))];
63
+ }
64
+
65
+ /**
66
+ * ${VAR} expansion for MCP server configs — args, env values, headers.
67
+ * Matches how the existing ~/.bahulam/config.json mcpServers block is
68
+ * handled so a plugin author with a working Claude Desktop MCP config
69
+ * gets the same env-var behavior after drop-in.
70
+ */
71
+ function _expandEnvInMcpConfig(config) {
72
+ const sub = (v) => typeof v === 'string'
73
+ ? v.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => process.env[name] ?? '')
74
+ : v;
75
+ const walk = (v) => {
76
+ if (Array.isArray(v)) return v.map(walk);
77
+ if (v && typeof v === 'object') {
78
+ const out = {};
79
+ for (const [k, val] of Object.entries(v)) out[k] = walk(val);
80
+ return out;
81
+ }
82
+ return sub(v);
83
+ };
84
+ return walk(config);
85
+ }
86
+
26
87
  export class LocalAgentRelay {
27
88
  constructor({ session, emit } = {}) {
28
89
  if (!session?.id) throw new Error('LocalAgentRelay requires a session');
@@ -69,17 +130,16 @@ export class LocalAgentRelay {
69
130
  err.code = 'CONFLICT';
70
131
  throw err;
71
132
  }
72
- if (this.turnCount > 0) {
73
- const err = new Error('Start a new local workspace session to switch history after a turn has run');
74
- err.code = 'CONFLICT';
75
- throw err;
76
- }
77
133
  this.resumeLoaded = true;
78
134
  this.resumeSessionId = null;
135
+ this.turnCount = 0;
79
136
  this.displayHistory = [];
80
137
  this.agentHistory = [];
81
138
  this.jsonlWriter = null;
82
- if (this.client) this.client.sessionId = null;
139
+ if (this.client) {
140
+ this.client.sessionId = null;
141
+ this.client.currentTaskId = null;
142
+ }
83
143
  this.emit('agent_history_new', {
84
144
  root_path: this.session.root_path,
85
145
  });
@@ -104,11 +164,6 @@ export class LocalAgentRelay {
104
164
  err.code = 'CONFLICT';
105
165
  throw err;
106
166
  }
107
- if (this.turnCount > 0 && this.resumeSessionId !== requestedSessionId) {
108
- const err = new Error('Start a new local workspace session to switch history after a turn has run');
109
- err.code = 'CONFLICT';
110
- throw err;
111
- }
112
167
  if (this.resumeLoaded && this.resumeSessionId === requestedSessionId) return this._historySnapshot();
113
168
 
114
169
  try {
@@ -131,6 +186,7 @@ export class LocalAgentRelay {
131
186
  this.displayHistory = history.displayHistory || [];
132
187
  this.agentHistory = history.agentHistory || [];
133
188
  this.resumeLoaded = true;
189
+ this.turnCount = 0;
134
190
  if (this.client) this.client.sessionId = this.resumeSessionId;
135
191
  if (this.jsonlWriter?.sessionId && this.jsonlWriter.sessionId !== this.resumeSessionId) {
136
192
  this.jsonlWriter = null;
@@ -344,6 +400,14 @@ export class LocalAgentRelay {
344
400
 
345
401
  async close() {
346
402
  this.approvalManager?.rejectAll?.();
403
+ // Tear down plugin-owned MCP clients — the executor's registered
404
+ // <server>.<tool> entries are removed here so a rerun in the same
405
+ // process picks up any manifest edits cleanly.
406
+ for (const { plugin, name, client } of (this._mcpClients || [])) {
407
+ try { this.toolExecutor?.unregisterMcpServer?.(plugin, name); } catch { /* ignore */ }
408
+ try { await client.disconnect(); } catch { /* ignore */ }
409
+ }
410
+ this._mcpClients = [];
347
411
  try {
348
412
  await this.jsonlWriter?.close?.();
349
413
  } catch {}
@@ -501,6 +565,14 @@ export class LocalAgentRelay {
501
565
  return payload;
502
566
  }
503
567
 
568
+ async executeTool(name, args = {}) {
569
+ await this._ensureReady();
570
+ if (!this.toolExecutor) {
571
+ throw new Error('Tool executor is not initialized');
572
+ }
573
+ return this.toolExecutor.execute(name, args);
574
+ }
575
+
504
576
  async _ensureReady() {
505
577
  if (this.ready) return this.ready;
506
578
  this.ready = this._initialize().catch((err) => {
@@ -526,10 +598,63 @@ export class LocalAgentRelay {
526
598
  process.chdir(this.session.root_path);
527
599
  }
528
600
 
529
- const toolExecutor = createToolExecutor();
601
+ const { PluginRegistry } = await import('../plugins/registry.mjs');
602
+ const activePlugins = activePluginNamesFromSession(this.session);
603
+ const pluginDirs = pluginScanDirsFromSession(this.session);
604
+ const pluginRegistry = new PluginRegistry({
605
+ disabled: envList('BAHULAM_DISABLE_PLUGINS'),
606
+ enabled: activePlugins,
607
+ ...(pluginDirs.length ? { pluginDirs } : {}),
608
+ });
609
+ pluginRegistry.scan();
610
+
611
+ // Reactive pulse: every plugin state write bubbles up here and lands
612
+ // on the shared SSE bus as `plugin_state_changed`. Views subscribed
613
+ // to /api/events re-render as the agent works — the Shared
614
+ // Blackboard's live-update moment. `this.emit` is the same event
615
+ // sink server.mjs uses for tool_call / tool_done / agent events.
616
+ const stateEmit = (evt) => {
617
+ try { this.emit('plugin_state_changed', evt); }
618
+ catch { /* never let SSE failure break a tool call */ }
619
+ };
620
+ const toolExecutor = createToolExecutor({ pluginRegistry, stateEmit, channel: 'workspace' });
530
621
  await toolExecutor.waitForAutoRegister?.();
531
622
  await toolExecutor.registerProjectRoots?.([this.session.root_path], { forceRefresh: false });
532
623
 
624
+ // Plugin=MCP+UX: every plugin can declare mcpServers in its
625
+ // plugin.yaml (or a sibling mcp.json). Spawn each, discover its
626
+ // tools, and register them as `<server>.<tool>` in the shared
627
+ // executor. Failure of one server never blocks the workspace —
628
+ // its tools drop out, everything else keeps running.
629
+ this._mcpClients = []; // {plugin, name, client} — for teardown on close()
630
+ for (const entry of pluginRegistry.listMcpServers?.() || []) {
631
+ try {
632
+ const { McpClient } = await import('../mcp/client.mjs');
633
+ const client = new McpClient(_expandEnvInMcpConfig(entry.config));
634
+ await client.connect();
635
+ const tools = await client.listTools();
636
+ let registered = 0;
637
+ for (const t of tools) {
638
+ if (toolExecutor.registerMcpTool?.(entry.plugin, entry.name, t.name, client, t.inputSchema)) {
639
+ registered++;
640
+ }
641
+ }
642
+ this._mcpClients.push({ plugin: entry.plugin, name: entry.name, client });
643
+ this.emit('plugin_mcp_ready', {
644
+ plugin: entry.plugin,
645
+ server: entry.name,
646
+ tools: registered,
647
+ });
648
+ } catch (err) {
649
+ // Never fatal: the plugin's JS tools + views still work.
650
+ this.emit('plugin_mcp_failed', {
651
+ plugin: entry.plugin,
652
+ server: entry.name,
653
+ error: String(err.message || err),
654
+ });
655
+ }
656
+ }
657
+
533
658
  const approval = new BrowserApprovalManager({
534
659
  emit: this.emit,
535
660
  cwd: this.session.root_path,
@@ -545,6 +670,7 @@ export class LocalAgentRelay {
545
670
  toolExecutor,
546
671
  approvalManager: approval,
547
672
  mode: 'remote',
673
+ pluginRegistry,
548
674
  });
549
675
  if (this.resumeSessionId) this.client.sessionId = this.resumeSessionId;
550
676
  this._ensureJsonlWriter();
@@ -553,6 +679,7 @@ export class LocalAgentRelay {
553
679
  backend_url: creds.backendUrl,
554
680
  root_path: this.session.root_path,
555
681
  tools: toolExecutor.listTools?.().length || 0,
682
+ plugins: pluginRegistry.list?.().map(plugin => plugin.metadata?.name).filter(Boolean) || [],
556
683
  backend_session_id: this.client.sessionId || null,
557
684
  approval_mode: this.approvalAutoMode ? 'auto' : 'ask',
558
685
  });