@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
|
@@ -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)
|
|
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
|
|
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
|
});
|