@bahulam/code 0.1.16 → 0.1.18
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 +1 -1
- package/src/agents/loader.mjs +37 -10
- package/src/agents/registry.mjs +240 -0
- package/src/commands/install.mjs +8 -8
- package/src/commands/plugin-manage.mjs +16 -15
- package/src/commands/plugin.mjs +15 -12
- package/src/core/background-tasks.mjs +29 -3
- package/src/core/headless.mjs +46 -2
- package/src/core/stream-client.mjs +1 -0
- package/src/core/tool-executor.mjs +80 -159
- package/src/local-service/agent-relay.mjs +56 -1
- package/src/local-service/server.mjs +2 -2
- package/src/onboarding/preflight.mjs +1 -1
- package/src/orchestration/dispatch.mjs +1 -1
- package/src/plugins/executor.mjs +2 -2
- package/src/plugins/loader.mjs +5 -5
- package/src/plugins/manifest.mjs +147 -24
- package/src/plugins/npm-install.mjs +13 -2
- package/src/plugins/pi-compat/scaffold.mjs +45 -29
- package/src/plugins/preflight.mjs +16 -7
- package/src/plugins/registry.mjs +6 -6
- package/src/plugins/state.mjs +141 -2
- package/src/terminal/repl.mjs +81 -64
- package/src/tools/bash.mjs +17 -1
- package/src/ui/input-dock.mjs +2 -1
package/package.json
CHANGED
package/src/agents/loader.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import { parseAgentDefinition } from './parser.mjs';
|
|
|
16
16
|
export class AgentLoader {
|
|
17
17
|
constructor() {
|
|
18
18
|
this.agents = new Map();
|
|
19
|
+
this.aliases = new Map();
|
|
19
20
|
this.searchPaths = [];
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -48,9 +49,7 @@ export class AgentLoader {
|
|
|
48
49
|
try {
|
|
49
50
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
50
51
|
const agent = parseAgentDefinition(content, ext);
|
|
51
|
-
|
|
52
|
-
this.agents.set(agent.name, { ...agent, source: filePath });
|
|
53
|
-
}
|
|
52
|
+
this._register(agent, filePath);
|
|
54
53
|
} catch (err) {
|
|
55
54
|
if (process.env.DEBUG) {
|
|
56
55
|
console.error(`Failed to load agent ${filePath}: ${err.message}`);
|
|
@@ -71,30 +70,50 @@ export class AgentLoader {
|
|
|
71
70
|
loadFromPlugins(plugins) {
|
|
72
71
|
if (!Array.isArray(plugins)) return this;
|
|
73
72
|
for (const plugin of plugins) {
|
|
74
|
-
const agents = plugin.
|
|
73
|
+
const agents = plugin.config?.agents || [];
|
|
75
74
|
for (const agentDef of agents) {
|
|
76
75
|
const slug = agentDef.slug || agentDef.name || '';
|
|
77
76
|
if (!slug) continue;
|
|
78
|
-
|
|
79
|
-
if (this.agents.has(slug)) continue;
|
|
80
|
-
this.agents.set(slug, {
|
|
77
|
+
this._register({
|
|
81
78
|
...agentDef,
|
|
82
79
|
slug,
|
|
83
80
|
source: `plugin:${plugin.metadata?.name || 'unknown'}`,
|
|
84
81
|
source_scope: 'plugin',
|
|
85
|
-
});
|
|
82
|
+
}, null);
|
|
86
83
|
}
|
|
87
84
|
}
|
|
88
85
|
return this;
|
|
89
86
|
}
|
|
90
87
|
|
|
88
|
+
_register(agent, sourcePath = null) {
|
|
89
|
+
if (!agent) return null;
|
|
90
|
+
const slug = normalizeKey(agent.slug || agent.id || agent.name);
|
|
91
|
+
if (!slug) return null;
|
|
92
|
+
const aliases = [
|
|
93
|
+
slug,
|
|
94
|
+
normalizeKey(agent.name),
|
|
95
|
+
normalizeKey(agent.id),
|
|
96
|
+
].filter(Boolean);
|
|
97
|
+
|
|
98
|
+
// Earlier search paths have higher precedence: project beats global,
|
|
99
|
+
// and both beat plugin-provided agents.
|
|
100
|
+
if (aliases.some(alias => this.aliases.has(alias))) return null;
|
|
101
|
+
|
|
102
|
+
const stored = { ...agent, slug: agent.slug || slug, ...(sourcePath ? { source: sourcePath } : {}) };
|
|
103
|
+
this.agents.set(slug, stored);
|
|
104
|
+
for (const alias of aliases) this.aliases.set(alias, slug);
|
|
105
|
+
return stored;
|
|
106
|
+
}
|
|
107
|
+
|
|
91
108
|
/**
|
|
92
109
|
* Get an agent definition by name.
|
|
93
110
|
* @param {string} name
|
|
94
111
|
* @returns {object|null}
|
|
95
112
|
*/
|
|
96
113
|
get(name) {
|
|
97
|
-
|
|
114
|
+
const key = normalizeKey(name);
|
|
115
|
+
const slug = this.aliases.get(key) || key;
|
|
116
|
+
return this.agents.get(slug) || null;
|
|
98
117
|
}
|
|
99
118
|
|
|
100
119
|
/**
|
|
@@ -111,6 +130,14 @@ export class AgentLoader {
|
|
|
111
130
|
* @returns {boolean}
|
|
112
131
|
*/
|
|
113
132
|
has(name) {
|
|
114
|
-
return this.
|
|
133
|
+
return Boolean(this.get(name));
|
|
115
134
|
}
|
|
116
135
|
}
|
|
136
|
+
|
|
137
|
+
function normalizeKey(value) {
|
|
138
|
+
return String(value || '')
|
|
139
|
+
.trim()
|
|
140
|
+
.toLowerCase()
|
|
141
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
142
|
+
.replace(/^-+|-+$/g, '');
|
|
143
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { BUILTIN_AGENTS } from '../terminal/agents.mjs';
|
|
2
|
+
import { loadBahulamSettings } from '../config/settings-loader.mjs';
|
|
3
|
+
import { agentToSpec, listLocalAgents } from './scaffold.mjs';
|
|
4
|
+
import * as crypto from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
export const SUB_AGENT_EVENT_SCHEMA = [
|
|
7
|
+
{ type: 'sub_agent_start', description: 'A delegated sub-agent run started.' },
|
|
8
|
+
{ type: 'sub_agent_tool', description: 'A sub-agent requested or completed a tool call.' },
|
|
9
|
+
{ type: 'sub_agent_complete', description: 'A delegated sub-agent run completed.' },
|
|
10
|
+
{ type: 'graph_run_start', description: 'A local workflow or delegated graph run started.' },
|
|
11
|
+
{ type: 'graph_node_start', description: 'One sub-agent, job, or service node started.' },
|
|
12
|
+
{ type: 'graph_node_result', description: 'One sub-agent, job, or service node completed.' },
|
|
13
|
+
{ type: 'graph_run_result', description: 'The local workflow or delegated graph run completed.' },
|
|
14
|
+
{ type: 'tool_call', description: 'A tool call was requested; sub_agent identifies delegated calls.' },
|
|
15
|
+
{ type: 'tool_result', description: 'A tool call completed; sub_agent identifies delegated calls.' },
|
|
16
|
+
{ type: 'content', description: 'A sub-agent emitted final or partial answer content.' },
|
|
17
|
+
{ type: 'error', description: 'A sub-agent, tool, or graph node failed.' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const READ_ONLY_TOOLS = ['read_file', 'search_code', 'list_files', 'search_files', 'get_file_info'];
|
|
21
|
+
|
|
22
|
+
export function compactAgentMetadata(agent) {
|
|
23
|
+
return {
|
|
24
|
+
slug: agent.slug,
|
|
25
|
+
name: agent.name,
|
|
26
|
+
description: agent.description || '',
|
|
27
|
+
role: agent.role || 'specialist',
|
|
28
|
+
model: agent.model || null,
|
|
29
|
+
models: agent.models || undefined,
|
|
30
|
+
tools: Array.isArray(agent.tools) ? agent.tools : [],
|
|
31
|
+
capabilities: Array.isArray(agent.capabilities) ? agent.capabilities : [],
|
|
32
|
+
domains: Array.isArray(agent.domains) ? agent.domains : [],
|
|
33
|
+
source_scope: agent.source_scope || 'unknown',
|
|
34
|
+
source: agent.source || '',
|
|
35
|
+
content_hash: agent.content_hash || '',
|
|
36
|
+
read_only: Boolean(agent.read_only || agent.readOnly),
|
|
37
|
+
runnable: agent.runnable !== false,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createAgentRegistry({
|
|
42
|
+
cwd = process.cwd(),
|
|
43
|
+
pluginRegistry = null,
|
|
44
|
+
channel = 'main',
|
|
45
|
+
settingsLoader = loadBahulamSettings,
|
|
46
|
+
} = {}) {
|
|
47
|
+
function currentCwd() {
|
|
48
|
+
return typeof cwd === 'function' ? cwd() : cwd || process.cwd();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pluginAgentAllowlist() {
|
|
52
|
+
try {
|
|
53
|
+
const loaded = settingsLoader({ cwd: currentCwd() });
|
|
54
|
+
const settings = loaded?.settings || loaded || {};
|
|
55
|
+
const allowlist = settings?.plugins?.agent_allowlist;
|
|
56
|
+
return Array.isArray(allowlist) ? allowlist.map(String) : [];
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeAgentTools(tools) {
|
|
63
|
+
if (Array.isArray(tools)) return tools.map(String).filter(Boolean);
|
|
64
|
+
if (typeof tools === 'string') {
|
|
65
|
+
return tools.split(',').map(item => item.trim()).filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
return READ_ONLY_TOOLS.slice(0, 3);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function pluginAgentToLocalShape(agentDef) {
|
|
71
|
+
const slug = agentDef.slug || agentDef.name || '';
|
|
72
|
+
const pluginName = agentDef._plugin_name || '';
|
|
73
|
+
const base = {
|
|
74
|
+
...agentDef,
|
|
75
|
+
slug,
|
|
76
|
+
name: agentDef.name || slug,
|
|
77
|
+
description: agentDef.description || '',
|
|
78
|
+
role: agentDef.role || 'specialist',
|
|
79
|
+
model: agentDef.model || null,
|
|
80
|
+
models: agentDef.models || null,
|
|
81
|
+
tools: normalizeAgentTools(agentDef.tools || agentDef.agent_tools),
|
|
82
|
+
capabilities: Array.isArray(agentDef.capabilities) ? agentDef.capabilities : [],
|
|
83
|
+
domains: Array.isArray(agentDef.domains) ? agentDef.domains : [],
|
|
84
|
+
prompt: agentDef.prompt || agentDef.system_prompt || agentDef.systemPrompt || '',
|
|
85
|
+
system_prompt: agentDef.system_prompt || agentDef.systemPrompt || agentDef.prompt || '',
|
|
86
|
+
source_scope: 'plugin',
|
|
87
|
+
source: agentDef.source || (pluginName ? `plugin:${pluginName}` : 'plugin'),
|
|
88
|
+
content_hash: agentDef.content_hash || '',
|
|
89
|
+
};
|
|
90
|
+
const spec = agentDef.spec || agentToSpec(base);
|
|
91
|
+
if (spec.config?.metadata && typeof spec.config.metadata === 'object') {
|
|
92
|
+
spec.config.metadata.source = base.source;
|
|
93
|
+
spec.config.metadata.source_scope = 'plugin';
|
|
94
|
+
}
|
|
95
|
+
spec.source = base.source;
|
|
96
|
+
spec.source_scope = 'plugin';
|
|
97
|
+
const content = JSON.stringify(spec);
|
|
98
|
+
return {
|
|
99
|
+
...base,
|
|
100
|
+
slug: spec.slug,
|
|
101
|
+
spec,
|
|
102
|
+
content_hash: agentDef.content_hash || crypto.createHash('sha256').update(content).digest('hex'),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function platformAgentToLocalShape(def) {
|
|
107
|
+
const base = {
|
|
108
|
+
slug: def.command,
|
|
109
|
+
command: def.command,
|
|
110
|
+
name: def.name || def.command,
|
|
111
|
+
description: def.description || '',
|
|
112
|
+
role: 'platform',
|
|
113
|
+
model: null,
|
|
114
|
+
models: undefined,
|
|
115
|
+
tools: Array.isArray(def.tools) && def.tools.length
|
|
116
|
+
? def.tools
|
|
117
|
+
: (def.readOnly ? READ_ONLY_TOOLS : []),
|
|
118
|
+
capabilities: [],
|
|
119
|
+
domains: [],
|
|
120
|
+
prompt: def.systemPrompt || '',
|
|
121
|
+
system_prompt: def.systemPrompt || '',
|
|
122
|
+
source_scope: 'platform',
|
|
123
|
+
source: 'platform:cli',
|
|
124
|
+
content_hash: '',
|
|
125
|
+
read_only: Boolean(def.readOnly),
|
|
126
|
+
readOnly: Boolean(def.readOnly),
|
|
127
|
+
runnable: true,
|
|
128
|
+
};
|
|
129
|
+
const spec = agentToSpec(base);
|
|
130
|
+
spec.source = base.source;
|
|
131
|
+
spec.source_scope = 'platform';
|
|
132
|
+
if (spec.config?.metadata && typeof spec.config.metadata === 'object') {
|
|
133
|
+
spec.config.metadata.source = base.source;
|
|
134
|
+
spec.config.metadata.source_scope = 'platform';
|
|
135
|
+
}
|
|
136
|
+
return { ...base, spec };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function listPlatformAgents() {
|
|
140
|
+
return BUILTIN_AGENTS.map(platformAgentToLocalShape);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function listPluginAgents() {
|
|
144
|
+
if (!pluginRegistry?.listAgents) return [];
|
|
145
|
+
return pluginRegistry.listAgents().map(pluginAgentToLocalShape).filter(agent => agent.slug);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function listRunnables() {
|
|
149
|
+
const bySlug = new Map();
|
|
150
|
+
for (const agent of listLocalAgents(currentCwd())) {
|
|
151
|
+
if (agent.slug && !bySlug.has(agent.slug)) {
|
|
152
|
+
bySlug.set(agent.slug, { ...agent, runnable: true });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const agent of listPlatformAgents()) {
|
|
156
|
+
if (agent.slug && !bySlug.has(agent.slug)) {
|
|
157
|
+
bySlug.set(agent.slug, agent);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const allowlist = new Set(pluginAgentAllowlist());
|
|
161
|
+
for (const agent of listPluginAgents()) {
|
|
162
|
+
if (!agent.slug || bySlug.has(agent.slug)) continue;
|
|
163
|
+
if (channel === 'workspace' || allowlist.has(agent.slug)) {
|
|
164
|
+
bySlug.set(agent.slug, { ...agent, runnable: true });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return [...bySlug.values()];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function listWorkspaceScopedPluginAgents() {
|
|
171
|
+
const runnableSlugs = new Set(listRunnables().map(agent => agent.slug));
|
|
172
|
+
return listPluginAgents()
|
|
173
|
+
.filter(agent => agent.slug && !runnableSlugs.has(agent.slug))
|
|
174
|
+
.map(agent => ({ ...agent, runnable: false }));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function listAvailableAgents() {
|
|
178
|
+
// Backend workspaces such as kepler-code already implement their
|
|
179
|
+
// platform roles (explore/plan/verify/debug/refactor) as reserved
|
|
180
|
+
// meta-tool targets. Only send extension agents here so we do not
|
|
181
|
+
// shadow the backend's native routing.
|
|
182
|
+
return listRunnables().filter(agent => agent.source_scope !== 'platform');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function findAgent(target) {
|
|
186
|
+
const needle = String(target || '').trim().toLowerCase();
|
|
187
|
+
if (!needle) return null;
|
|
188
|
+
return listRunnables().find(agent => [
|
|
189
|
+
agent.slug,
|
|
190
|
+
agent.id,
|
|
191
|
+
agent.command,
|
|
192
|
+
agent.name,
|
|
193
|
+
].some(value => String(value || '').trim().toLowerCase() === needle)) || null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function filterAgents(args = {}) {
|
|
197
|
+
let scope = String(args.scope || '').trim();
|
|
198
|
+
if (scope === 'builtin') scope = 'platform';
|
|
199
|
+
if (scope && !['project', 'global', 'plugin', 'platform'].includes(scope)) {
|
|
200
|
+
throw new Error('scope must be "project", "global", "plugin", "platform", or "builtin"');
|
|
201
|
+
}
|
|
202
|
+
const pool = scope === 'plugin'
|
|
203
|
+
? [...listRunnables(), ...listWorkspaceScopedPluginAgents()]
|
|
204
|
+
: listRunnables();
|
|
205
|
+
const query = String(args.query || args.name || '').trim().toLowerCase();
|
|
206
|
+
return pool
|
|
207
|
+
.filter(agent => !scope || agent.source_scope === scope)
|
|
208
|
+
.filter(agent => {
|
|
209
|
+
if (!query) return true;
|
|
210
|
+
return [
|
|
211
|
+
agent.slug,
|
|
212
|
+
agent.name,
|
|
213
|
+
agent.description,
|
|
214
|
+
agent.role,
|
|
215
|
+
...(Array.isArray(agent.capabilities) ? agent.capabilities : []),
|
|
216
|
+
...(Array.isArray(agent.domains) ? agent.domains : []),
|
|
217
|
+
].some(value => String(value || '').toLowerCase().includes(query));
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function observability() {
|
|
222
|
+
return {
|
|
223
|
+
version: 1,
|
|
224
|
+
events: SUB_AGENT_EVENT_SCHEMA,
|
|
225
|
+
correlation_fields: ['graph_run_id', 'node_id', 'sub_agent', 'sub_agent_run_id', 'call_id'],
|
|
226
|
+
tool_attribution_fields: ['internal', 'sub_agent', 'subAgent'],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
listRunnables,
|
|
232
|
+
listAvailableAgents,
|
|
233
|
+
listPlatformAgents,
|
|
234
|
+
listPluginAgents,
|
|
235
|
+
listWorkspaceScopedPluginAgents,
|
|
236
|
+
filterAgents,
|
|
237
|
+
findAgent,
|
|
238
|
+
observability,
|
|
239
|
+
};
|
|
240
|
+
}
|
package/src/commands/install.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* bahulam pull <src> — install an ingredient (currently pi:<name>).
|
|
5
5
|
* No pack scaffolding; the raw package lands in
|
|
6
6
|
* ~/.bahulam/plugins-pi/ and is only useful when
|
|
7
|
-
* referenced by a pack's
|
|
7
|
+
* referenced by a pack's config.composes:.
|
|
8
8
|
*
|
|
9
9
|
* bahulam install <src> — install a full pack.
|
|
10
10
|
* For pi:<name>: pulls the ingredient AND
|
|
@@ -88,7 +88,7 @@ export async function handlePullCommand(argv, { cwd = process.cwd() } = {}) {
|
|
|
88
88
|
|
|
89
89
|
Pull an ingredient (pi package) into ~/.bahulam/plugins-pi/. The
|
|
90
90
|
ingredient is composable but not directly runnable — reference it from
|
|
91
|
-
a pack's ${CYAN}
|
|
91
|
+
a pack's ${CYAN}config.composes:${RESET} block, or use ${CYAN}bahulam install pi:<name>${RESET}
|
|
92
92
|
to auto-scaffold a full pack around it.
|
|
93
93
|
|
|
94
94
|
Sources:
|
|
@@ -133,7 +133,7 @@ export async function handlePullCommand(argv, { cwd = process.cwd() } = {}) {
|
|
|
133
133
|
process.stderr.write(` ${DIM}location${RESET} ${dest}\n`);
|
|
134
134
|
process.stderr.write(` ${YELLOW}!${RESET} Pi packages run with your full system permissions. Bahulam does not audit pi packages.\n`);
|
|
135
135
|
process.stderr.write(` ${DIM}Wrap in a pack:${RESET} ${CYAN}bahulam install pi:${classified.package_name}${RESET}\n`);
|
|
136
|
-
process.stderr.write(` ${DIM}Compose in yours:${RESET} ${CYAN}
|
|
136
|
+
process.stderr.write(` ${DIM}Compose in yours:${RESET} ${CYAN}config.composes: [{source: pi:${classified.package_name}, expose: [...]}]${RESET}\n\n`);
|
|
137
137
|
} catch (err) {
|
|
138
138
|
process.stderr.write(`\x1b[31m✗\x1b[0m ${err.message}\n`);
|
|
139
139
|
process.exit(1);
|
|
@@ -397,7 +397,7 @@ async function preflightAndReport({ dest, args, cwd, meta = null }) {
|
|
|
397
397
|
|
|
398
398
|
// Host check for each composed pi ingredient (same policy as the
|
|
399
399
|
// scaffolder path). Blocks install if a required binary is missing.
|
|
400
|
-
const composes = m.
|
|
400
|
+
const composes = m.config?.composes || [];
|
|
401
401
|
if (composes.length && !meta) {
|
|
402
402
|
// meta present == scaffolder path already did this pre-scaffold
|
|
403
403
|
const { bahulamHome } = await import('../core/paths.mjs');
|
|
@@ -431,11 +431,11 @@ async function preflightAndReport({ dest, args, cwd, meta = null }) {
|
|
|
431
431
|
|
|
432
432
|
process.stderr.write(`\n${GREEN}✓${RESET} Installed ${BOLD}${m.metadata.name}${RESET} v${m.metadata.version}\n`);
|
|
433
433
|
process.stderr.write(` ${DIM}location${RESET} ${dest}\n`);
|
|
434
|
-
const nativeTools = (m.
|
|
435
|
-
const composedCount = (m.
|
|
434
|
+
const nativeTools = (m.config.tools || []).map(t => t.name);
|
|
435
|
+
const composedCount = (m.config.composes || []).reduce((n, c) => n + ((c.expose || []).length || 0), 0);
|
|
436
436
|
process.stderr.write(` ${DIM}tools${RESET} ${nativeTools.length ? nativeTools.join(', ') : '(none)'}${composedCount ? ` ${DIM}+ ${composedCount} composed${RESET}` : ''}\n`);
|
|
437
|
-
process.stderr.write(` ${DIM}agents${RESET} ${(m.
|
|
438
|
-
const views = m.
|
|
437
|
+
process.stderr.write(` ${DIM}agents${RESET} ${(m.config.agents || []).map(a => a.slug).join(', ') || '(none)'}\n`);
|
|
438
|
+
const views = m.config.views || [];
|
|
439
439
|
process.stderr.write(` ${DIM}views${RESET} ${views.length ? views.map(v => v.name).join(', ') : '(none)'}\n`);
|
|
440
440
|
if (meta) {
|
|
441
441
|
process.stderr.write(` ${DIM}scaffolded${RESET} from ${CYAN}pi:${meta.packageName}${RESET} (namespace ${CYAN}${meta.namespace}${RESET}, ${meta.exposeTools.length} composed tool${meta.exposeTools.length === 1 ? '' : 's'})\n`);
|
|
@@ -24,6 +24,7 @@ import { spawn } from 'node:child_process';
|
|
|
24
24
|
import { parsePluginManifestFile } from '../plugins/manifest.mjs';
|
|
25
25
|
import { preflightPlugin, existingInstalledNames } from '../plugins/preflight.mjs';
|
|
26
26
|
import { parsePiSource } from '../plugins/pi-compose.mjs';
|
|
27
|
+
import { bahulamHome } from '../core/paths.mjs';
|
|
27
28
|
|
|
28
29
|
const RESET = '\x1b[0m';
|
|
29
30
|
const BOLD = '\x1b[1m';
|
|
@@ -38,13 +39,13 @@ const INSTALL_STAMP = '.bahulam-plugin.json';
|
|
|
38
39
|
function searchDirs(cwd) {
|
|
39
40
|
return [
|
|
40
41
|
{ scope: 'project', dir: path.join(cwd, '.bahulam', 'plugins') },
|
|
41
|
-
{ scope: 'global', dir: path.join(
|
|
42
|
+
{ scope: 'global', dir: path.join(bahulamHome(), 'plugins') },
|
|
42
43
|
];
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export function pluginTargetDir({ global, cwd }) {
|
|
46
47
|
return global
|
|
47
|
-
? path.join(
|
|
48
|
+
? path.join(bahulamHome(), 'plugins')
|
|
48
49
|
: path.join(cwd, '.bahulam', 'plugins');
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -72,7 +73,7 @@ function scanInstalled(cwd) {
|
|
|
72
73
|
// else in the plugins/ dir is stray (readManifest returns null).
|
|
73
74
|
if (!parsed && !disabled) continue;
|
|
74
75
|
const stamp = readStamp(pluginDir);
|
|
75
|
-
const agentSlugs = (parsed?.manifest?.
|
|
76
|
+
const agentSlugs = (parsed?.manifest?.config?.agents || [])
|
|
76
77
|
.map(a => a.slug || a.name).filter(Boolean);
|
|
77
78
|
found.push({
|
|
78
79
|
scope,
|
|
@@ -81,10 +82,10 @@ function scanInstalled(cwd) {
|
|
|
81
82
|
name: parsed?.manifest?.metadata?.name || entry.name.replace(/\.disabled$/, ''),
|
|
82
83
|
version: parsed?.manifest?.metadata?.version || null,
|
|
83
84
|
description: parsed?.manifest?.metadata?.description || '',
|
|
84
|
-
tools: parsed?.manifest?.
|
|
85
|
-
agents: parsed?.manifest?.
|
|
86
|
-
views: parsed?.manifest?.
|
|
87
|
-
composes: parsed?.manifest?.
|
|
85
|
+
tools: parsed?.manifest?.config?.tools?.length || 0,
|
|
86
|
+
agents: parsed?.manifest?.config?.agents?.length || 0,
|
|
87
|
+
views: parsed?.manifest?.config?.views?.length || 0,
|
|
88
|
+
composes: parsed?.manifest?.config?.composes?.length || 0,
|
|
88
89
|
agentSlugs,
|
|
89
90
|
disabled,
|
|
90
91
|
origin: stamp?.origin || null,
|
|
@@ -390,12 +391,12 @@ export async function installFromLocal({ src, targetDir, force }) {
|
|
|
390
391
|
}
|
|
391
392
|
|
|
392
393
|
/**
|
|
393
|
-
* Auto-install pi packages referenced by a pack's
|
|
394
|
+
* Auto-install pi packages referenced by a pack's config.composes:. Callers
|
|
394
395
|
* invoke this after preflight so a hand-authored pack that composes
|
|
395
396
|
* missing pi ingredients still resolves in one command.
|
|
396
397
|
*/
|
|
397
398
|
export async function resolveComposeDependencies(manifest, { targetDir } = {}) {
|
|
398
|
-
const composes = manifest?.
|
|
399
|
+
const composes = manifest?.config?.composes || [];
|
|
399
400
|
if (!composes.length) return;
|
|
400
401
|
const { discoverPiTools } = await import('../plugins/pi-compat/probe.mjs');
|
|
401
402
|
const { bahulamHome } = await import('../core/paths.mjs');
|
|
@@ -446,7 +447,7 @@ async function cmdList(args, cwd) {
|
|
|
446
447
|
if (!p.composes) continue;
|
|
447
448
|
try {
|
|
448
449
|
const m = readManifest(p.directory);
|
|
449
|
-
const composes = m?.manifest?.
|
|
450
|
+
const composes = m?.manifest?.config?.composes || [];
|
|
450
451
|
pluginComposes.set(p.name, composes.map(c => c.package_name || c.packageName).filter(Boolean));
|
|
451
452
|
} catch { /* skip */ }
|
|
452
453
|
}
|
|
@@ -519,7 +520,7 @@ async function cmdList(args, cwd) {
|
|
|
519
520
|
const orphans = pi.filter(p => usedBy(p.name).length === 0);
|
|
520
521
|
if (orphans.length) {
|
|
521
522
|
process.stderr.write(`\n${YELLOW}!${RESET} ${orphans.length} pi ingredient${orphans.length === 1 ? '' : 's'} installed but not composed by any pack.\n`);
|
|
522
|
-
process.stderr.write(` Pi ingredients are unusable on their own — reference in a pack's ${CYAN}
|
|
523
|
+
process.stderr.write(` Pi ingredients are unusable on their own — reference in a pack's ${CYAN}config.composes:${RESET} block.\n`);
|
|
523
524
|
}
|
|
524
525
|
}
|
|
525
526
|
|
|
@@ -588,9 +589,9 @@ function cmdInfo(args, cwd) {
|
|
|
588
589
|
}
|
|
589
590
|
if (found.installed_at) process.stderr.write(` ${DIM}installed${RESET} ${found.installed_at}\n`);
|
|
590
591
|
if (m) {
|
|
591
|
-
process.stderr.write(`\n ${DIM}tools${RESET} ${(m.
|
|
592
|
-
process.stderr.write(` ${DIM}agents${RESET} ${(m.
|
|
593
|
-
const views = m.
|
|
592
|
+
process.stderr.write(`\n ${DIM}tools${RESET} ${(m.config.tools || []).map(t => t.name).join(', ') || '(none)'}\n`);
|
|
593
|
+
process.stderr.write(` ${DIM}agents${RESET} ${(m.config.agents || []).map(a => a.slug).join(', ') || '(none)'}\n`);
|
|
594
|
+
const views = m.config.views || [];
|
|
594
595
|
process.stderr.write(` ${DIM}views${RESET} ${views.length ? views.map(v => v.name).join(', ') : '(none)'}\n`);
|
|
595
596
|
}
|
|
596
597
|
process.stderr.write('\n');
|
|
@@ -711,7 +712,7 @@ async function cmdDoctor(args, cwd) {
|
|
|
711
712
|
const found = findByName(target, cwd);
|
|
712
713
|
if (!found) throw new Error(`plugin not found: ${target}`);
|
|
713
714
|
const scan = readManifest(found.directory);
|
|
714
|
-
const composes = scan?.manifest?.
|
|
715
|
+
const composes = scan?.manifest?.config?.composes || [];
|
|
715
716
|
for (const c of composes) {
|
|
716
717
|
if (!c.package_name) continue;
|
|
717
718
|
const safe = c.package_name.replace(/[/@]/g, '_');
|
package/src/commands/plugin.mjs
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import * as fs from 'node:fs';
|
|
14
|
-
import * as os from 'node:os';
|
|
15
14
|
import * as path from 'node:path';
|
|
16
15
|
import {
|
|
17
16
|
createLocalWorkspaceSession,
|
|
@@ -21,6 +20,7 @@ import {
|
|
|
21
20
|
} from '../local-service/session-store.mjs';
|
|
22
21
|
import { startLocalWorkspaceService } from '../local-service/server.mjs';
|
|
23
22
|
import { openLocalBrowser } from '../local-service/browser.mjs';
|
|
23
|
+
import { bahulamHome } from '../core/paths.mjs';
|
|
24
24
|
|
|
25
25
|
const RESET = '\x1b[0m';
|
|
26
26
|
const BOLD = '\x1b[1m';
|
|
@@ -33,20 +33,22 @@ const RED = '\x1b[31m';
|
|
|
33
33
|
/**
|
|
34
34
|
* Standard directories to search for plugins.
|
|
35
35
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
function pluginSearchDirs(cwd = process.cwd()) {
|
|
37
|
+
return [
|
|
38
|
+
path.join(cwd, '.bahulam', 'plugins'),
|
|
39
|
+
path.join(bahulamHome(), 'plugins'),
|
|
40
|
+
];
|
|
41
|
+
}
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
44
|
* Find a plugin directory by name across all standard search paths.
|
|
43
45
|
* Returns the directory path and manifests on success, null on miss.
|
|
44
46
|
*/
|
|
45
|
-
function findPluginDir(name) {
|
|
47
|
+
function findPluginDir(name, cwd = process.cwd()) {
|
|
46
48
|
const needle = String(name || '').trim().toLowerCase();
|
|
47
49
|
if (!needle) return null;
|
|
48
50
|
|
|
49
|
-
for (const searchDir of
|
|
51
|
+
for (const searchDir of pluginSearchDirs(cwd)) {
|
|
50
52
|
try {
|
|
51
53
|
if (!fs.existsSync(searchDir)) continue;
|
|
52
54
|
const entries = fs.readdirSync(searchDir, { withFileTypes: true });
|
|
@@ -113,17 +115,18 @@ export async function handlePluginCommand(args, { cwd = process.cwd() } = {}) {
|
|
|
113
115
|
const targetPath = String(args.targetPath || cwd).trim();
|
|
114
116
|
|
|
115
117
|
if (!pluginName || args.help) {
|
|
116
|
-
printPluginUsage();
|
|
118
|
+
printPluginUsage(cwd);
|
|
117
119
|
process.exit(args.help ? 0 : 1);
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
// 1. Find the plugin
|
|
121
|
-
const found = findPluginDir(pluginName);
|
|
123
|
+
const found = findPluginDir(pluginName, cwd);
|
|
122
124
|
if (!found) {
|
|
125
|
+
const searchDirs = pluginSearchDirs(cwd);
|
|
123
126
|
process.stderr.write(
|
|
124
127
|
`${RED}✗ Plugin "${pluginName}" not found.${RESET}\n` +
|
|
125
128
|
` ${DIM}Searched:${RESET}\n` +
|
|
126
|
-
|
|
129
|
+
searchDirs.map(d => ` ${d}`).join('\n') + '\n' +
|
|
127
130
|
` ${DIM}Create a plugin.yaml or plugin.json in one of these directories.${RESET}\n`
|
|
128
131
|
);
|
|
129
132
|
process.exit(1);
|
|
@@ -210,7 +213,7 @@ export async function handlePluginCommand(args, { cwd = process.cwd() } = {}) {
|
|
|
210
213
|
});
|
|
211
214
|
}
|
|
212
215
|
|
|
213
|
-
function printPluginUsage() {
|
|
216
|
+
function printPluginUsage(cwd = process.cwd()) {
|
|
214
217
|
process.stderr.write(
|
|
215
218
|
`${BOLD}PLUGIN COMMANDS${RESET}\n` +
|
|
216
219
|
` ${CYAN}bahulam plugin <name> [path]${RESET} Open a workspace with a plugin loaded\n` +
|
|
@@ -238,7 +241,7 @@ function printPluginUsage() {
|
|
|
238
241
|
` --force overwrite existing install\n` +
|
|
239
242
|
`\n` +
|
|
240
243
|
` ${DIM}Search paths (later overrides earlier):${RESET}\n` +
|
|
241
|
-
|
|
244
|
+
pluginSearchDirs(cwd).map(d => ` ${d}`).join('\n') + '\n' +
|
|
242
245
|
`\n` +
|
|
243
246
|
` ${DIM}Example:${RESET}\n` +
|
|
244
247
|
` bahulam plugin install https://github.com/community/seo-toolkit\n` +
|
|
@@ -14,11 +14,26 @@ const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
|
|
|
14
14
|
const KILL_ESCALATION_MS = 5000;
|
|
15
15
|
const MAX_TAIL_BYTES = 64 * 1024;
|
|
16
16
|
|
|
17
|
+
function trimShellBackgroundOperator(command) {
|
|
18
|
+
const raw = String(command || '');
|
|
19
|
+
let i = raw.length - 1;
|
|
20
|
+
while (i >= 0 && /\s/.test(raw[i])) i -= 1;
|
|
21
|
+
if (raw[i] !== '&') return raw;
|
|
22
|
+
if (raw[i - 1] === '&' || raw[i - 1] === '\\') return raw;
|
|
23
|
+
return raw.slice(0, i).trimEnd();
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
function stripAnsi(str) {
|
|
18
27
|
// eslint-disable-next-line no-control-regex
|
|
19
28
|
return String(str || '').replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
20
29
|
}
|
|
21
30
|
|
|
31
|
+
function setChildStdioRef(proc, ref) {
|
|
32
|
+
for (const stream of [proc?.stdout, proc?.stderr]) {
|
|
33
|
+
try { stream?.[ref ? 'ref' : 'unref']?.(); } catch { /* best effort */ }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
22
37
|
class BackgroundTasks {
|
|
23
38
|
constructor() {
|
|
24
39
|
this.jobs = new Map();
|
|
@@ -35,12 +50,15 @@ class BackgroundTasks {
|
|
|
35
50
|
start({ command, cwd = process.cwd(), timeoutMs = DEFAULT_TIMEOUT_MS, name = '', on_complete = null }) {
|
|
36
51
|
this._installExitHook();
|
|
37
52
|
const id = `job-${++this._seq}-${Date.now().toString(36)}`;
|
|
53
|
+
const originalCommand = String(command || '');
|
|
54
|
+
const managedCommand = trimShellBackgroundOperator(originalCommand);
|
|
38
55
|
const logDir = path.join(cwd, '.bahulam', 'tmp', 'jobs');
|
|
39
56
|
fs.mkdirSync(logDir, { recursive: true });
|
|
40
57
|
const logPath = path.join(logDir, `${id}.log`);
|
|
41
58
|
const logStream = fs.createWriteStream(logPath);
|
|
59
|
+
try { logStream.unref?.(); } catch { /* best effort */ }
|
|
42
60
|
|
|
43
|
-
const proc = spawn('bash', ['-c',
|
|
61
|
+
const proc = spawn('bash', ['-c', managedCommand], {
|
|
44
62
|
cwd,
|
|
45
63
|
env: { ...process.env },
|
|
46
64
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -49,8 +67,9 @@ class BackgroundTasks {
|
|
|
49
67
|
|
|
50
68
|
const job = {
|
|
51
69
|
id,
|
|
52
|
-
name: name ||
|
|
53
|
-
command,
|
|
70
|
+
name: name || managedCommand.slice(0, 60),
|
|
71
|
+
command: managedCommand,
|
|
72
|
+
original_command: originalCommand !== managedCommand ? originalCommand : null,
|
|
54
73
|
cwd,
|
|
55
74
|
pid: proc.pid,
|
|
56
75
|
status: 'running',
|
|
@@ -62,6 +81,7 @@ class BackgroundTasks {
|
|
|
62
81
|
timed_out: false,
|
|
63
82
|
on_complete,
|
|
64
83
|
_proc: proc,
|
|
84
|
+
_logStream: logStream,
|
|
65
85
|
_done: null,
|
|
66
86
|
};
|
|
67
87
|
|
|
@@ -106,6 +126,7 @@ class BackgroundTasks {
|
|
|
106
126
|
});
|
|
107
127
|
|
|
108
128
|
proc.unref();
|
|
129
|
+
setChildStdioRef(proc, false);
|
|
109
130
|
this.jobs.set(id, job);
|
|
110
131
|
return this.describe(id);
|
|
111
132
|
}
|
|
@@ -120,8 +141,12 @@ class BackgroundTasks {
|
|
|
120
141
|
// process so fast commands still get their close event before Node decides
|
|
121
142
|
// the top-level await is unsettled.
|
|
122
143
|
try { job._proc?.ref?.(); } catch { /* best effort */ }
|
|
144
|
+
try { job._logStream?.ref?.(); } catch { /* best effort */ }
|
|
145
|
+
setChildStdioRef(job._proc, true);
|
|
123
146
|
return job._done.finally(() => {
|
|
124
147
|
try { job._proc?.unref?.(); } catch { /* best effort */ }
|
|
148
|
+
try { job._logStream?.unref?.(); } catch { /* best effort */ }
|
|
149
|
+
setChildStdioRef(job._proc, false);
|
|
125
150
|
});
|
|
126
151
|
}
|
|
127
152
|
|
|
@@ -132,6 +157,7 @@ class BackgroundTasks {
|
|
|
132
157
|
id: job.id,
|
|
133
158
|
name: job.name,
|
|
134
159
|
command: job.command,
|
|
160
|
+
original_command: job.original_command,
|
|
135
161
|
pid: job.pid,
|
|
136
162
|
status: job.status,
|
|
137
163
|
exit_code: job.exit_code,
|