@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
@@ -58,9 +58,13 @@ export const AgentTool = {
58
58
  _backgroundAgents: new Map(),
59
59
  _nextBgId: 0,
60
60
 
61
- async call(input) {
61
+ async call(input, options = {}) {
62
62
  const model = input.model || process.env.SUBAGENT_MODEL || 'claude-sonnet-4-6';
63
- const tools = createToolRegistry();
63
+ const tools = createToolRegistry({
64
+ pluginRegistry: options.pluginRegistry || null,
65
+ stateEmit: options.stateEmit || null,
66
+ exposePluginTools: true,
67
+ });
64
68
  const permissions = createPermissionChecker({ defaultMode: 'bypassPermissions' });
65
69
 
66
70
  // Build type-specific system prompt prefix
@@ -92,7 +92,7 @@ export const AnalyzeImageTool = {
92
92
  if (!creds.backendUrl || !creds.token) {
93
93
  return {
94
94
  success: false,
95
- output: 'analyze_image requires CLI auth. Run `bahulam-code login` or set B0_TOKEN/KEPLER_TOKEN.',
95
+ output: 'analyze_image requires CLI auth. Run `bahulam-code login` or set B0_TOKEN.',
96
96
  _tool: 'analyze_image',
97
97
  };
98
98
  }
@@ -73,7 +73,7 @@ const PROJECT_MARKERS = [
73
73
  'pom.xml', 'build.gradle', 'build.gradle.kts', 'settings.gradle', // Java/Kotlin
74
74
  'Makefile', 'CMakeLists.txt', // C/C++
75
75
  'Dockerfile', 'docker-compose.yml', 'docker-compose.yaml',
76
- 'AGENTS.md', 'CLAUDE.md', 'KEPLER.md', // Agent config lives at root
76
+ 'AGENTS.md', 'BAHULAM.md', 'CLAUDE.md', // Agent config lives at root
77
77
  '.editorconfig', // Broad but a strong "this is a repo" signal
78
78
  ];
79
79
 
@@ -350,7 +350,7 @@ function defaultScratchRoots() {
350
350
  '/private/tmp',
351
351
  os.tmpdir(),
352
352
  process.env.TMPDIR,
353
- ...(process.env.KEPLER_SCRATCH_ROOTS || '')
353
+ ...(process.env.BAHULAM_SCRATCH_ROOTS || '')
354
354
  .split(path.delimiter)
355
355
  .map(s => s.trim())
356
356
  .filter(Boolean),
@@ -399,13 +399,13 @@ export class ProjectRegistry {
399
399
 
400
400
  // PRD-69 project context is live metadata, not index cache. Re-read it on
401
401
  // every registration attempt so repeated get_project_overview calls pick up
402
- // .bahulam/KEPLER.md, goal/plan/style, skills, AGENTS.md, etc. changes.
402
+ // .bahulam/BAHULAM.md, goal/plan/style, skills, AGENTS.md, etc. changes.
403
403
  _attachLiveContext(resource, root) {
404
- // Resolver — prefers .bahulam/, falls back to .kepler/ for legacy projects.
405
404
  const bahulamDir = projectConfigDir(root);
406
405
  resource.environment = detectEnvironment();
407
- resource.project_context = _readIfExists(bahulamDir, 'KEPLER.md', 10000) ||
408
- _readIfExists(root, 'KEPLER.md', 10000) ||
406
+ resource.project_context = _readIfExists(root, 'AGENTS.md', 10000) ||
407
+ _readIfExists(bahulamDir, 'BAHULAM.md', 10000) ||
408
+ _readIfExists(root, 'BAHULAM.md', 10000) ||
409
409
  _readIfExists(bahulamDir, 'project.md', 8000);
410
410
  resource.style = _readIfExists(bahulamDir, 'style.md', 4000);
411
411
  resource.goal = _readIfExists(bahulamDir, 'goal.md', 2000);
@@ -413,7 +413,7 @@ export class ProjectRegistry {
413
413
  resource.skills_index = _scanSkills(bahulamDir);
414
414
 
415
415
  if (!resource.project_context) {
416
- for (const name of ['.bahulam.md', 'AGENTS.md', 'CLAUDE.md']) {
416
+ for (const name of ['.bahulam.md', 'BAHULAM.md', 'AGENTS.md', 'CLAUDE.md']) {
417
417
  const content = _readIfExists(root, name, 8000);
418
418
  if (content) { resource.project_context = content; break; }
419
419
  }
@@ -39,6 +39,7 @@ import { ExploreTool, PlanTool, VerifyTool, DebugTool, RefactorTool } from './me
39
39
  import { RememberTool } from './remember.mjs';
40
40
  import { GenerateImageTool } from './generate-image.mjs';
41
41
  import { AnalyzeImageTool } from './analyze-image.mjs';
42
+ import { loadPluginTool } from '../plugins/executor.mjs';
42
43
 
43
44
  const BUILTIN_TOOLS = [
44
45
  BashTool,
@@ -82,12 +83,95 @@ const BUILTIN_TOOLS = [
82
83
  RefactorTool,
83
84
  ];
84
85
 
85
- export function createToolRegistry() {
86
+ export function createToolRegistry({
87
+ pluginRegistry = null,
88
+ stateEmit = null,
89
+ exposePluginTools = false,
90
+ } = {}) {
86
91
  const tools = new Map();
87
92
  for (const Tool of BUILTIN_TOOLS) {
88
- tools.set(Tool.name, Tool);
93
+ if (Tool === AgentTool) {
94
+ tools.set(Tool.name, {
95
+ ...Tool,
96
+ async call(input, options = {}) {
97
+ return Tool.call(input, {
98
+ ...options,
99
+ pluginRegistry: options.pluginRegistry || pluginRegistry,
100
+ stateEmit: options.stateEmit || stateEmit,
101
+ });
102
+ },
103
+ });
104
+ } else {
105
+ tools.set(Tool.name, Tool);
106
+ }
89
107
  }
90
108
 
109
+ const pluginStateHandles = new Map();
110
+ async function pluginStateFor(pluginName) {
111
+ if (!pluginName) return null;
112
+ if (pluginStateHandles.has(pluginName)) return pluginStateHandles.get(pluginName);
113
+ const { makePluginState } = await import('../plugins/state.mjs');
114
+ const state = makePluginState(pluginName, { emit: stateEmit });
115
+ pluginStateHandles.set(pluginName, state);
116
+ return state;
117
+ }
118
+
119
+ function registerPluginToolsFromRegistry() {
120
+ if (!pluginRegistry) return;
121
+ for (const toolDef of pluginRegistry.listTools?.() || []) {
122
+ const name = String(toolDef.name || '').trim();
123
+ if (!name || tools.has(name)) continue;
124
+ const pluginName = toolDef._plugin_name || toolDef.plugin_name || null;
125
+ tools.set(name, {
126
+ name,
127
+ description: toolDef.description || '',
128
+ inputSchema: toolDef.input_schema || toolDef.parameters || { type: 'object', properties: {} },
129
+ validateInput() { return []; },
130
+ async call(input, options = {}) {
131
+ const handler = await loadPluginTool(toolDef._plugin_dir, toolDef.tool || toolDef.handler);
132
+ if (!handler) {
133
+ return {
134
+ success: false,
135
+ output: `Plugin tool module could not be loaded: ${name}`,
136
+ _tool: name,
137
+ _plugin: pluginName,
138
+ };
139
+ }
140
+ const handlerOpts = {
141
+ ...options,
142
+ pluginName,
143
+ get state() {
144
+ if (this._stateP) return this._stateP;
145
+ this._stateP = pluginStateFor(pluginName);
146
+ return this._stateP;
147
+ },
148
+ };
149
+ try {
150
+ const result = await handler.call(input || {}, handlerOpts);
151
+ if (result && typeof result === 'object' && 'success' in result) {
152
+ return { ...result, _tool: name, _plugin: pluginName };
153
+ }
154
+ return {
155
+ success: true,
156
+ output: typeof result === 'string' ? result : JSON.stringify(result),
157
+ _tool: name,
158
+ _plugin: pluginName,
159
+ };
160
+ } catch (err) {
161
+ return {
162
+ success: false,
163
+ output: `Plugin tool error (${name}): ${err.message}`,
164
+ _tool: name,
165
+ _plugin: pluginName,
166
+ };
167
+ }
168
+ },
169
+ });
170
+ }
171
+ }
172
+
173
+ if (exposePluginTools) registerPluginToolsFromRegistry();
174
+
91
175
  const registry = {
92
176
  list() {
93
177
  return [...tools.values()].map(t => ({
@@ -97,12 +181,12 @@ export function createToolRegistry() {
97
181
  }));
98
182
  },
99
183
 
100
- async call(name, input) {
184
+ async call(name, input, options = {}) {
101
185
  const tool = tools.get(name);
102
186
  if (!tool) throw new Error(`Unknown tool: ${name}`);
103
187
  const errors = tool.validateInput?.(input) || [];
104
188
  if (errors.length > 0) return `Validation error: ${errors.join(', ')}`;
105
- const result = await tool.call(input);
189
+ const result = await tool.call(input, options);
106
190
 
107
191
  return result;
108
192
  },
@@ -47,6 +47,8 @@ export const COMMANDS = {
47
47
  '/agents': 'List available agents',
48
48
  '/subagents': 'Alias of /agents (list/create/edit/sync sub-agents)',
49
49
  '/skills': 'List / install / view / remove skills (SKILL.md bundles)',
50
+ '/plugins': 'List / install / info / update / remove plugins (tools + agents + views)',
51
+ '/plugin': 'Alias of /plugins',
50
52
  '/run': 'Run a sub-agent or synced workflow',
51
53
  '/explore': 'Code explorer agent',
52
54
  '/review': 'Code review agent',
@@ -141,7 +143,7 @@ export const HELP_GROUPS = [
141
143
  ['/agents', 'List built-in and local agents'],
142
144
  ['/agents create <name>', 'Create .bahulam/agents/<name>.yaml'],
143
145
  ['/agents edit <name>', 'Open local agent YAML'],
144
- ['/agents sync [name]', 'Sync all or one local agent to cloud'],
146
+ ['/agents sync [name]', 'Publish local agents to backend/account reuse'],
145
147
  ['/run <agent> [instruction]', 'Run a local or built-in agent'],
146
148
  ['/explore <instruction>', 'Explore code'],
147
149
  ['/review <instruction>', 'Review code'],
@@ -162,6 +164,22 @@ export const HELP_GROUPS = [
162
164
  ['/skills update <name>', 'Reinstall from the recorded source'],
163
165
  ],
164
166
  },
167
+ {
168
+ key: 'plugins',
169
+ title: 'Plugins',
170
+ summary: 'community-authored tools, agents, and workspace views',
171
+ commands: [
172
+ ['/plugins', 'List installed plugins'],
173
+ ['/plugins install <git-url|path|name>', 'Install from git URL, local path, or awesome-bahulam-plugins name'],
174
+ ['/plugins install <src> --project', 'Install into .bahulam/plugins (project scope)'],
175
+ ['/plugins install <src> --ref <tag>', 'Pin a git ref'],
176
+ ['/plugins validate <path|name>', 'Preflight a plugin without installing (schema + handlers + collisions)'],
177
+ ['/plugins info <name>', 'Show manifest details + install origin'],
178
+ ['/plugins enable|disable <name>', 'Toggle without deleting'],
179
+ ['/plugins update <name>', 'Pull the latest for git-installed plugins'],
180
+ ['/plugins remove <name>', 'Uninstall a plugin'],
181
+ ],
182
+ },
165
183
  {
166
184
  key: 'workflows',
167
185
  title: 'Workflows',
@@ -70,11 +70,12 @@ export function displayQuery(query, max = 140) {
70
70
 
71
71
  export function renderSubAgentOpen({ id, type, query, parentDepth } = {}) {
72
72
  const t = type || 'sub-agent';
73
- const depthBefore = _stack.length;
74
- _stack.push({ id: id || `${t}-${depthBefore}-${tag()}`, type: t, startedAt: Date.now() });
73
+ const depthBefore = Number.isFinite(parentDepth) ? Math.max(0, parentDepth) : _stack.length;
74
+ _stack.push({ id: id || `${t}-${depthBefore}-${tag()}`, type: t, depth: depthBefore, startedAt: Date.now() });
75
75
 
76
76
  const indent = ' '.repeat(2 + depthBefore * 3);
77
- const iconChar = SUB_ICONS[t] || icons.subAgent;
77
+ // Ordinal labels (explore#2) still get their base type's icon.
78
+ const iconChar = SUB_ICONS[t] || SUB_ICONS[String(t).replace(/#\d+$/, '')] || icons.subAgent;
78
79
  const shown = displayQuery(query);
79
80
  const head = `${indent}${iconChar} ${paint.brand.data(t)} ${paint.text.dim(`"${shown}"`)}`;
80
81
  const tag1 = paint.text.dim('▸ running');
@@ -95,6 +96,7 @@ export function renderSubAgentOpen({ id, type, query, parentDepth } = {}) {
95
96
  * and any of `{ costUsd, tokens, durationS, toolCalls, iterations }`.
96
97
  */
97
98
  export function renderSubAgentClose({
99
+ id,
98
100
  type,
99
101
  success = true,
100
102
  summary = '',
@@ -105,12 +107,16 @@ export function renderSubAgentClose({
105
107
  iterations,
106
108
  error,
107
109
  } = {}) {
108
- // Match-pop: if the type doesn't match the top of stack we still pop the
109
- // top entry backends never emit interleaved open/close, so this is the
110
- // safe behavior.
111
- const opened = _stack.pop();
110
+ // Parallel sub-agents can complete out of stack order. Prefer exact id,
111
+ // then type, and fall back to the latest open entry for legacy streams.
112
+ let idx = -1;
113
+ if (id) idx = _stack.findLastIndex(entry => entry.id === id);
114
+ if (idx < 0 && type) idx = _stack.findLastIndex(entry => entry.type === type);
115
+ if (idx < 0) idx = _stack.length - 1;
116
+ const opened = idx >= 0 ? _stack.splice(idx, 1)[0] : null;
112
117
  const t = type || opened?.type || 'sub-agent';
113
- const indent = ' '.repeat(2 + _stack.length * 3);
118
+ const closeDepth = Number.isFinite(opened?.depth) ? opened.depth : _stack.length;
119
+ const indent = ' '.repeat(2 + closeDepth * 3);
114
120
 
115
121
  if (!success) {
116
122
  const line = `${indent}${paint.text.dim('└')} ${paint.state.danger('✗')} ${paint.text.dim(`${t} agent failed`)}`;