@bahulam/code 0.1.16 → 0.1.17
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/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 +62 -56
- package/src/tools/bash.mjs +17 -1
package/src/plugins/manifest.mjs
CHANGED
|
@@ -27,6 +27,37 @@ function normalizeToolNames(value) {
|
|
|
27
27
|
}).filter(Boolean);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function normalizePathList(value) {
|
|
31
|
+
if (typeof value === 'string' && value.trim()) return [value.trim()];
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
return value.map(item => String(item || '').trim()).filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function addAgent(agents, seen, agent) {
|
|
39
|
+
if (!agent?.slug) return;
|
|
40
|
+
const key = String(agent.slug).trim().toLowerCase();
|
|
41
|
+
if (!key || seen.has(key)) return;
|
|
42
|
+
seen.add(key);
|
|
43
|
+
agents.push(agent);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeViews(value) {
|
|
47
|
+
return Array.isArray(value) ? value.filter(view => view && typeof view === 'object') : [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeWorkspaceDeclaration(value) {
|
|
51
|
+
if (typeof value === 'string' && value.trim()) {
|
|
52
|
+
return { agentPath: value.trim(), views: [] };
|
|
53
|
+
}
|
|
54
|
+
if (value && typeof value === 'object') {
|
|
55
|
+
const agentPath = String(value.agent || value.file || value.source || '').trim();
|
|
56
|
+
return { agentPath, views: normalizeViews(value.views) };
|
|
57
|
+
}
|
|
58
|
+
return { agentPath: '', views: [] };
|
|
59
|
+
}
|
|
60
|
+
|
|
30
61
|
function loadAgentFile(agentDef, pluginDir) {
|
|
31
62
|
const file = String(agentDef.file || agentDef.handler || '').trim();
|
|
32
63
|
if (!file || !pluginDir) return {};
|
|
@@ -45,21 +76,36 @@ function loadAgentFile(agentDef, pluginDir) {
|
|
|
45
76
|
}
|
|
46
77
|
|
|
47
78
|
function normalizeAgentDef(agentDef, pluginName, pluginDir) {
|
|
48
|
-
const
|
|
79
|
+
const loadedConfig = loadAgentFile(agentDef, pluginDir);
|
|
80
|
+
const hasLoadedConfig = loadedConfig && Object.keys(loadedConfig).length > 0;
|
|
81
|
+
const fileConfig = hasLoadedConfig ? loadedConfig : (agentDef || {});
|
|
49
82
|
const metadata = fileConfig.metadata || fileConfig.meta || {};
|
|
50
|
-
const agent = fileConfig.agent || fileConfig.
|
|
83
|
+
const agent = fileConfig.agent || fileConfig.config?.agent || {};
|
|
51
84
|
const fileTools = (
|
|
52
85
|
fileConfig.tools
|
|
53
|
-
|| fileConfig.
|
|
86
|
+
|| fileConfig.config?.tools
|
|
54
87
|
|| agent.tools
|
|
55
88
|
|| []
|
|
56
89
|
);
|
|
57
90
|
const inlineTools = normalizeToolNames(agentDef.tools);
|
|
91
|
+
const slug = (
|
|
92
|
+
agentDef.slug
|
|
93
|
+
|| metadata.slug
|
|
94
|
+
|| fileConfig.slug
|
|
95
|
+
|| agent.slug
|
|
96
|
+
|| agentDef.id
|
|
97
|
+
|| metadata.name
|
|
98
|
+
|| fileConfig.name
|
|
99
|
+
|| agentDef.name
|
|
100
|
+
|| metadata.role
|
|
101
|
+
|| fileConfig.role
|
|
102
|
+
|| ''
|
|
103
|
+
);
|
|
58
104
|
|
|
59
105
|
return {
|
|
60
|
-
slug
|
|
61
|
-
name: agentDef.name || metadata.name || fileConfig.name ||
|
|
62
|
-
description: agentDef.description || metadata.description || fileConfig.description || '',
|
|
106
|
+
slug,
|
|
107
|
+
name: agentDef.name || metadata.name || fileConfig.name || agent.name || slug || '',
|
|
108
|
+
description: agentDef.description || metadata.description || fileConfig.description || agent.description || '',
|
|
63
109
|
role: agentDef.role || metadata.role || fileConfig.role || 'specialist',
|
|
64
110
|
system_prompt: (
|
|
65
111
|
agentDef.system_prompt
|
|
@@ -77,12 +123,39 @@ function normalizeAgentDef(agentDef, pluginName, pluginDir) {
|
|
|
77
123
|
models: agentDef.models || agent.models || fileConfig.models || undefined,
|
|
78
124
|
max_tokens: agentDef.max_tokens || agent.max_tokens || fileConfig.max_tokens || undefined,
|
|
79
125
|
max_iterations: agentDef.max_iterations || agent.max_iterations || fileConfig.max_iterations || undefined,
|
|
126
|
+
disallowed_tools: metadata.disallowedTools || metadata.disallowed_tools || fileConfig.disallowedTools || fileConfig.disallowed_tools || [],
|
|
127
|
+
can_delegate: agentDef.can_delegate ?? agent.can_delegate ?? fileConfig.can_delegate ?? false,
|
|
128
|
+
can_be_delegated_to: agentDef.can_be_delegated_to ?? agent.can_be_delegated_to ?? fileConfig.can_be_delegated_to ?? true,
|
|
129
|
+
apiVersion: fileConfig.apiVersion || fileConfig.api_version || undefined,
|
|
130
|
+
kind: fileConfig.kind || undefined,
|
|
80
131
|
file: agentDef.file || agentDef.handler || '',
|
|
81
132
|
source: `plugin:${pluginName}`,
|
|
82
133
|
source_scope: 'plugin',
|
|
83
134
|
};
|
|
84
135
|
}
|
|
85
136
|
|
|
137
|
+
function loadAgentPath(agentPath, pluginDir, pluginName, label) {
|
|
138
|
+
if (!agentPath || !pluginDir) return null;
|
|
139
|
+
const fullPath = path.resolve(pluginDir, agentPath);
|
|
140
|
+
try {
|
|
141
|
+
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) return null;
|
|
142
|
+
const agentDef = parseYaml(fs.readFileSync(fullPath, 'utf-8'));
|
|
143
|
+
if (!agentDef || typeof agentDef !== 'object') {
|
|
144
|
+
console.warn(`Skipping ${label} ${fullPath}: not a mapping`);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const agent = normalizeAgentDef(agentDef, pluginName, pluginDir);
|
|
148
|
+
if (!agent.slug) {
|
|
149
|
+
console.warn(`Skipping ${label} ${fullPath}: no slug`);
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return agent;
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.warn(`Failed to load ${label} ${fullPath}: ${err.message}`);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
86
159
|
/**
|
|
87
160
|
* Parse a plugin manifest from YAML text.
|
|
88
161
|
* @param {string} yamlText - Raw YAML content
|
|
@@ -141,8 +214,8 @@ export function normalizeManifest(raw, source = '') {
|
|
|
141
214
|
}
|
|
142
215
|
|
|
143
216
|
const meta = raw.metadata || raw.meta || {};
|
|
144
|
-
const
|
|
145
|
-
const name = meta.name ||
|
|
217
|
+
const config = raw.config || raw.plugin || {};
|
|
218
|
+
const name = meta.name || config.name || '';
|
|
146
219
|
if (!name) {
|
|
147
220
|
if (process.env.DEBUG) {
|
|
148
221
|
console.warn(`Plugin manifest missing name: ${source}`);
|
|
@@ -152,15 +225,62 @@ export function normalizeManifest(raw, source = '') {
|
|
|
152
225
|
|
|
153
226
|
// Normalize agents
|
|
154
227
|
const agents = [];
|
|
228
|
+
const agentSlugs = new Set();
|
|
155
229
|
const pluginDir = source ? path.dirname(source) : '';
|
|
156
|
-
for (const agentDef of (
|
|
157
|
-
|
|
158
|
-
|
|
230
|
+
for (const agentDef of (config.agents || [])) {
|
|
231
|
+
addAgent(agents, agentSlugs, normalizeAgentDef(agentDef, name, pluginDir));
|
|
232
|
+
}
|
|
233
|
+
const workspaceDecl = normalizeWorkspaceDeclaration(config.workspace);
|
|
234
|
+
if (workspaceDecl.agentPath) {
|
|
235
|
+
const workspaceAgent = loadAgentPath(workspaceDecl.agentPath, pluginDir, name, 'workspace');
|
|
236
|
+
if (workspaceAgent) {
|
|
237
|
+
workspaceAgent.entry_agent = true;
|
|
238
|
+
addAgent(agents, agentSlugs, workspaceAgent);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Optional authoring convenience: `config.agents_from: <dir|string[]>`.
|
|
242
|
+
// This is for delegated sub-agents. The primary/entry agent should
|
|
243
|
+
// live at `config.workspace: ./config/workspace.yaml`.
|
|
244
|
+
const agentsFrom = normalizePathList(config.agents_from);
|
|
245
|
+
for (const agentsFromPath of agentsFrom) {
|
|
246
|
+
if (!pluginDir) continue;
|
|
247
|
+
const agentsDir = path.resolve(pluginDir, agentsFromPath);
|
|
248
|
+
try {
|
|
249
|
+
if (fs.existsSync(agentsDir) && fs.statSync(agentsDir).isDirectory()) {
|
|
250
|
+
const files = fs.readdirSync(agentsDir)
|
|
251
|
+
.filter(f => /\.(ya?ml)$/i.test(f))
|
|
252
|
+
.sort();
|
|
253
|
+
for (const f of files) {
|
|
254
|
+
const filePath = path.join(agentsDir, f);
|
|
255
|
+
let agentDef;
|
|
256
|
+
try {
|
|
257
|
+
agentDef = parseYaml(fs.readFileSync(filePath, 'utf-8'));
|
|
258
|
+
} catch (err) {
|
|
259
|
+
console.warn(`Failed to parse plugin agent file ${filePath}: ${err.message}`);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (!agentDef || typeof agentDef !== 'object') {
|
|
263
|
+
console.warn(`Skipping plugin agent file ${filePath}: not a mapping`);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const agent = normalizeAgentDef(agentDef, name, pluginDir);
|
|
267
|
+
if (!agent.slug) {
|
|
268
|
+
console.warn(`Skipping plugin agent file ${filePath}: no slug`);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
addAgent(agents, agentSlugs, agent);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} catch (err) {
|
|
275
|
+
if (process.env.DEBUG) {
|
|
276
|
+
console.error(`Failed to load agents_from ${agentsDir}: ${err.message}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
159
279
|
}
|
|
160
280
|
|
|
161
281
|
// Normalize tools
|
|
162
282
|
const tools = [];
|
|
163
|
-
for (const toolDef of (
|
|
283
|
+
for (const toolDef of (config.tools || [])) {
|
|
164
284
|
const tool = {
|
|
165
285
|
name: toolDef.name || '',
|
|
166
286
|
description: toolDef.description || '',
|
|
@@ -171,11 +291,12 @@ export function normalizeManifest(raw, source = '') {
|
|
|
171
291
|
if (tool.name) tools.push(tool);
|
|
172
292
|
}
|
|
173
293
|
|
|
174
|
-
// Normalize workspace
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
294
|
+
// Normalize browser workspace views. New manifests use `config.views`.
|
|
295
|
+
// Older installed manifests with `config.workspace.views` still render.
|
|
296
|
+
const views = [
|
|
297
|
+
...workspaceDecl.views,
|
|
298
|
+
...normalizeViews(config.views),
|
|
299
|
+
];
|
|
179
300
|
|
|
180
301
|
// Normalize MCP servers — the Plugin=MCP+UX story. Two sources are
|
|
181
302
|
// merged so authors can either:
|
|
@@ -185,8 +306,8 @@ export function normalizeManifest(raw, source = '') {
|
|
|
185
306
|
// transfers with zero edits)
|
|
186
307
|
// Inline wins on name collision so authors can override a portable
|
|
187
308
|
// config for the local plugin without editing mcp.json.
|
|
188
|
-
const mcpServers = _readMcpServers(
|
|
189
|
-
const composes = normalizeComposes(
|
|
309
|
+
const mcpServers = _readMcpServers(config.mcpServers, source);
|
|
310
|
+
const composes = normalizeComposes(config.composes);
|
|
190
311
|
|
|
191
312
|
return {
|
|
192
313
|
apiVersion,
|
|
@@ -198,10 +319,12 @@ export function normalizeManifest(raw, source = '') {
|
|
|
198
319
|
author: meta.author || '',
|
|
199
320
|
repository: meta.repository || '',
|
|
200
321
|
},
|
|
201
|
-
|
|
322
|
+
config: {
|
|
202
323
|
tools,
|
|
203
324
|
agents,
|
|
204
|
-
|
|
325
|
+
...(agentsFrom.length ? { agents_from: agentsFrom } : {}),
|
|
326
|
+
workspace: workspaceDecl.agentPath,
|
|
327
|
+
views,
|
|
205
328
|
mcpServers,
|
|
206
329
|
composes,
|
|
207
330
|
},
|
|
@@ -277,12 +400,12 @@ export function validatePluginManifest(manifest) {
|
|
|
277
400
|
errors.push('Plugin metadata.name is required');
|
|
278
401
|
}
|
|
279
402
|
|
|
280
|
-
if (manifest.
|
|
281
|
-
for (const tool of (manifest.
|
|
403
|
+
if (manifest.config) {
|
|
404
|
+
for (const tool of (manifest.config.tools || [])) {
|
|
282
405
|
if (!tool.name) errors.push('Tool missing name');
|
|
283
406
|
if (!tool.tool) errors.push(`Tool "${tool.name || '(unnamed)'}" missing tool module path (tool: ./tools/<name>.mjs)`);
|
|
284
407
|
}
|
|
285
|
-
for (const agent of (manifest.
|
|
408
|
+
for (const agent of (manifest.config.agents || [])) {
|
|
286
409
|
if (!agent.slug && !agent.name) errors.push('Agent missing slug or name');
|
|
287
410
|
}
|
|
288
411
|
}
|
|
@@ -77,11 +77,22 @@ function migratePeersForPi(pkgPath) {
|
|
|
77
77
|
let pkg = {};
|
|
78
78
|
try { pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); } catch { return {}; }
|
|
79
79
|
const peers = pkg.peerDependencies || {};
|
|
80
|
-
if (!Object.keys(peers).length) return pkg;
|
|
81
80
|
const merged = { ...(pkg.dependencies || {}) };
|
|
81
|
+
let changed = false;
|
|
82
82
|
for (const [name, range] of Object.entries(peers)) {
|
|
83
|
-
if (!merged[name])
|
|
83
|
+
if (!merged[name]) {
|
|
84
|
+
merged[name] = range === '*' ? 'latest' : range;
|
|
85
|
+
changed = true;
|
|
86
|
+
}
|
|
84
87
|
}
|
|
88
|
+
// Some current pi packages import the Pi server runtime transitively
|
|
89
|
+
// through @earendil-works/pi-coding-agent without declaring it. Materialize
|
|
90
|
+
// it here so discovery can import the extension and capture tools.
|
|
91
|
+
if (merged['@earendil-works/pi-coding-agent'] && !merged['@earendil-works/pi-server']) {
|
|
92
|
+
merged['@earendil-works/pi-server'] = 'latest';
|
|
93
|
+
changed = true;
|
|
94
|
+
}
|
|
95
|
+
if (!changed && !Object.keys(peers).length) return pkg;
|
|
85
96
|
const rewritten = { ...pkg, dependencies: merged };
|
|
86
97
|
delete rewritten.peerDependencies;
|
|
87
98
|
fs.writeFileSync(pkgPath, JSON.stringify(rewritten, null, 2));
|
|
@@ -185,7 +185,7 @@ function generatePrompt(packageName, namespace, toolNames, hasState, requirement
|
|
|
185
185
|
* (quoted keys, over-escaping); a small emitter here yields a diff-
|
|
186
186
|
* friendly manifest the user can edit.
|
|
187
187
|
*/
|
|
188
|
-
function renderManifest({ slug, packageName, versionRange, namespace, exposeTools,
|
|
188
|
+
function renderManifest({ slug, packageName, versionRange, namespace, exposeTools, hasState, hasWorkspace }) {
|
|
189
189
|
const versionSpec = versionRange ? `${packageName}@${versionRange}` : packageName;
|
|
190
190
|
const tools = hasState ? [
|
|
191
191
|
' tools:',
|
|
@@ -235,30 +235,11 @@ function renderManifest({ slug, packageName, versionRange, namespace, exposeTool
|
|
|
235
235
|
'',
|
|
236
236
|
];
|
|
237
237
|
|
|
238
|
-
const agentToolRefs = [
|
|
239
|
-
...(hasState ? ['save_item', 'list_items', 'drop_item'] : []),
|
|
240
|
-
...exposeTools.map(t => `${namespace}${COMPOSED_TOOL_SEPARATOR}${t}`),
|
|
241
|
-
];
|
|
242
|
-
|
|
243
|
-
const agentBlock = [
|
|
244
|
-
' agents:',
|
|
245
|
-
` - slug: ${agentSlug}`,
|
|
246
|
-
` name: ${yamlString(agentSlug.replace(/-/g, ' '))}`,
|
|
247
|
-
' role: specialist',
|
|
248
|
-
' description: >',
|
|
249
|
-
` ${agentDescription}`,
|
|
250
|
-
' tools:',
|
|
251
|
-
...agentToolRefs.map(t => ` - ${t}`),
|
|
252
|
-
` system_prompt: ${yamlBlock(systemPrompt, 8)}`,
|
|
253
|
-
'',
|
|
254
|
-
];
|
|
255
|
-
|
|
256
238
|
const workspaceBlock = hasWorkspace ? [
|
|
257
|
-
'
|
|
258
|
-
'
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
' source: ./workspace/panel.html',
|
|
239
|
+
' views:',
|
|
240
|
+
' - type: panel',
|
|
241
|
+
` name: ${yamlString(slug.replace(/-/g, ' '))}`,
|
|
242
|
+
' source: ./workspace/panel.html',
|
|
262
243
|
'',
|
|
263
244
|
] : [];
|
|
264
245
|
|
|
@@ -272,14 +253,41 @@ function renderManifest({ slug, packageName, versionRange, namespace, exposeTool
|
|
|
272
253
|
` Auto-scaffolded pack composing pi:${packageName}.`,
|
|
273
254
|
` Edit tools/, workspace/, and this manifest to customize.`,
|
|
274
255
|
'',
|
|
275
|
-
'
|
|
256
|
+
'config:',
|
|
276
257
|
...tools,
|
|
277
258
|
...composesBlock,
|
|
278
|
-
|
|
259
|
+
' workspace: ./config/workspace.yaml',
|
|
260
|
+
'',
|
|
279
261
|
...workspaceBlock,
|
|
280
262
|
].join('\n');
|
|
281
263
|
}
|
|
282
264
|
|
|
265
|
+
function renderAgentFile({ namespace, exposeTools, agentSlug, agentDescription, hasState, systemPrompt }) {
|
|
266
|
+
const agentToolRefs = [
|
|
267
|
+
...(hasState ? ['save_item', 'list_items', 'drop_item'] : []),
|
|
268
|
+
...exposeTools.map(t => `${namespace}${COMPOSED_TOOL_SEPARATOR}${t}`),
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
return [
|
|
272
|
+
'# config/workspace.yaml — entry agent loaded when plugin.yaml declares',
|
|
273
|
+
'# config.workspace: ./config/workspace.yaml.',
|
|
274
|
+
'apiVersion: agent.framework/v1',
|
|
275
|
+
'kind: SingleAgent',
|
|
276
|
+
'metadata:',
|
|
277
|
+
` slug: ${agentSlug}`,
|
|
278
|
+
` name: ${yamlString(agentSlug.replace(/-/g, ' '))}`,
|
|
279
|
+
' role: specialist',
|
|
280
|
+
' description: >',
|
|
281
|
+
` ${agentDescription}`,
|
|
282
|
+
'agent:',
|
|
283
|
+
' max_iterations: 10',
|
|
284
|
+
` system_prompt: ${yamlBlock(systemPrompt, 4)}`,
|
|
285
|
+
'tools:',
|
|
286
|
+
...agentToolRefs.map(t => ` - ${t}`),
|
|
287
|
+
'',
|
|
288
|
+
].join('\n');
|
|
289
|
+
}
|
|
290
|
+
|
|
283
291
|
const SAVE_ITEM_TOOL = `/**
|
|
284
292
|
* save_item — persist a single item to the pack's notebook.
|
|
285
293
|
* Append-style records so a topic can accumulate many entries over time.
|
|
@@ -537,14 +545,22 @@ export function scaffoldPiPack({
|
|
|
537
545
|
versionRange,
|
|
538
546
|
namespace,
|
|
539
547
|
exposeTools: toolNames,
|
|
540
|
-
agentSlug,
|
|
541
|
-
agentDescription,
|
|
542
548
|
hasState: state,
|
|
543
549
|
hasWorkspace: workspace,
|
|
544
|
-
systemPrompt,
|
|
545
550
|
});
|
|
546
551
|
fs.writeFileSync(path.join(dest, 'plugin.yaml'), manifest);
|
|
547
552
|
|
|
553
|
+
const configDir = path.join(dest, 'config');
|
|
554
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
555
|
+
fs.writeFileSync(path.join(configDir, 'workspace.yaml'), renderAgentFile({
|
|
556
|
+
namespace,
|
|
557
|
+
exposeTools: toolNames,
|
|
558
|
+
agentSlug,
|
|
559
|
+
agentDescription,
|
|
560
|
+
hasState: state,
|
|
561
|
+
systemPrompt,
|
|
562
|
+
}));
|
|
563
|
+
|
|
548
564
|
if (state) {
|
|
549
565
|
const toolsDir = path.join(dest, 'tools');
|
|
550
566
|
fs.mkdirSync(toolsDir, { recursive: true });
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import * as fs from 'node:fs';
|
|
21
|
-
import * as os from 'node:os';
|
|
22
21
|
import * as path from 'node:path';
|
|
23
22
|
import { pathToFileURL } from 'node:url';
|
|
24
23
|
import { parsePluginManifestFile, validatePluginManifest } from './manifest.mjs';
|
|
25
24
|
import { composedToolName, validateCompose } from './pi-compose.mjs';
|
|
25
|
+
import { bahulamHome } from '../core/paths.mjs';
|
|
26
26
|
|
|
27
27
|
const TOOL_NAME_RE = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
|
|
28
28
|
const AGENT_SLUG_RE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
@@ -84,12 +84,21 @@ export async function preflightPlugin(pluginDir, opts = {}) {
|
|
|
84
84
|
warnings.push(`metadata.name "${name}" should be lowercase kebab-case for registry compatibility`);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
const tools = manifest.
|
|
88
|
-
const agents = manifest.
|
|
89
|
-
const
|
|
90
|
-
const
|
|
87
|
+
const tools = manifest.config?.tools || [];
|
|
88
|
+
const agents = manifest.config?.agents || [];
|
|
89
|
+
const workspacePath = manifest.config?.workspace || '';
|
|
90
|
+
const views = manifest.config?.views || [];
|
|
91
|
+
const mcpServers = manifest.config?.mcpServers || {};
|
|
91
92
|
const mcpServerNames = new Set(Object.keys(mcpServers));
|
|
92
|
-
const composes = manifest.
|
|
93
|
+
const composes = manifest.config?.composes || [];
|
|
94
|
+
|
|
95
|
+
if (workspacePath) {
|
|
96
|
+
const full = path.resolve(pluginDir, workspacePath);
|
|
97
|
+
const inside = full === pluginDir || full.startsWith(pluginDir + path.sep);
|
|
98
|
+
if (!inside) errors.push(`Workspace entry agent path escapes the plugin directory: ${workspacePath}`);
|
|
99
|
+
else if (!fs.existsSync(full)) errors.push(`Workspace entry agent not found: ${workspacePath}`);
|
|
100
|
+
else if (!fs.statSync(full).isFile()) errors.push(`Workspace entry agent is not a file: ${workspacePath}`);
|
|
101
|
+
}
|
|
93
102
|
|
|
94
103
|
// MCP server sanity — every server should have EITHER command (stdio)
|
|
95
104
|
// OR url (remote). Anything else is meaningless config.
|
|
@@ -240,7 +249,7 @@ export function existingInstalledNames(cwd = process.cwd()) {
|
|
|
240
249
|
const names = [];
|
|
241
250
|
for (const dir of [
|
|
242
251
|
path.join(cwd, '.bahulam', 'plugins'),
|
|
243
|
-
path.join(
|
|
252
|
+
path.join(bahulamHome(), 'plugins'),
|
|
244
253
|
]) {
|
|
245
254
|
if (!fs.existsSync(dir)) continue;
|
|
246
255
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
package/src/plugins/registry.mjs
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
|
|
8
8
|
import fs from 'fs';
|
|
9
9
|
import path from 'path';
|
|
10
|
-
import os from 'os';
|
|
11
10
|
import { parsePluginManifestFile, validatePluginManifest } from './manifest.mjs';
|
|
12
11
|
import { expandComposedTools } from './pi-compose.mjs';
|
|
12
|
+
import { bahulamHome } from '../core/paths.mjs';
|
|
13
13
|
|
|
14
14
|
const DEFAULT_PLUGIN_DIRS = () => [
|
|
15
15
|
path.join(process.cwd(), '.bahulam', 'plugins'),
|
|
16
|
-
path.join(
|
|
16
|
+
path.join(bahulamHome(), 'plugins'),
|
|
17
17
|
];
|
|
18
18
|
|
|
19
19
|
export class PluginRegistry {
|
|
@@ -153,7 +153,7 @@ export class PluginRegistry {
|
|
|
153
153
|
listTools() {
|
|
154
154
|
const tools = [];
|
|
155
155
|
for (const plugin of this.plugins.values()) {
|
|
156
|
-
for (const tool of (plugin.
|
|
156
|
+
for (const tool of (plugin.config?.tools || [])) {
|
|
157
157
|
tools.push({
|
|
158
158
|
...tool,
|
|
159
159
|
_plugin_name: plugin.metadata?.name,
|
|
@@ -163,7 +163,7 @@ export class PluginRegistry {
|
|
|
163
163
|
tools.push(...expandComposedTools(
|
|
164
164
|
plugin.metadata?.name || '',
|
|
165
165
|
plugin._dir,
|
|
166
|
-
plugin.
|
|
166
|
+
plugin.config?.composes || [],
|
|
167
167
|
));
|
|
168
168
|
}
|
|
169
169
|
return tools;
|
|
@@ -183,7 +183,7 @@ export class PluginRegistry {
|
|
|
183
183
|
listMcpServers() {
|
|
184
184
|
const out = [];
|
|
185
185
|
for (const plugin of this.plugins.values()) {
|
|
186
|
-
const servers = plugin.
|
|
186
|
+
const servers = plugin.config?.mcpServers || {};
|
|
187
187
|
const pluginName = plugin.metadata?.name || '';
|
|
188
188
|
for (const [name, config] of Object.entries(servers)) {
|
|
189
189
|
if (config && typeof config === 'object') {
|
|
@@ -201,7 +201,7 @@ export class PluginRegistry {
|
|
|
201
201
|
listAgents() {
|
|
202
202
|
const agents = [];
|
|
203
203
|
for (const plugin of this.plugins.values()) {
|
|
204
|
-
for (const agent of (plugin.
|
|
204
|
+
for (const agent of (plugin.config?.agents || [])) {
|
|
205
205
|
agents.push({
|
|
206
206
|
...agent,
|
|
207
207
|
_plugin_name: plugin.metadata?.name,
|
package/src/plugins/state.mjs
CHANGED
|
@@ -33,7 +33,10 @@
|
|
|
33
33
|
import fs from 'node:fs';
|
|
34
34
|
import os from 'node:os';
|
|
35
35
|
import path from 'node:path';
|
|
36
|
-
import {
|
|
36
|
+
import { createRequire } from 'node:module';
|
|
37
|
+
|
|
38
|
+
const require = createRequire(import.meta.url);
|
|
39
|
+
let DatabaseSync = null;
|
|
37
40
|
|
|
38
41
|
// Silence the single "SQLite is an experimental feature" warning that
|
|
39
42
|
// node:sqlite emits at first import. Users would see it on every plugin
|
|
@@ -49,6 +52,14 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
49
52
|
};
|
|
50
53
|
}
|
|
51
54
|
|
|
55
|
+
if (process.env.BAHULAM_PLUGIN_STATE_BACKEND !== 'json') {
|
|
56
|
+
try {
|
|
57
|
+
({ DatabaseSync } = require('node:sqlite'));
|
|
58
|
+
} catch {
|
|
59
|
+
DatabaseSync = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
const DATA_ROOT = () => path.join(os.homedir(), '.bahulam', 'data');
|
|
53
64
|
const PLUGIN_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
54
65
|
const DEBOUNCE_MS = 50;
|
|
@@ -58,6 +69,134 @@ const DEBOUNCE_MS = 50;
|
|
|
58
69
|
// for the lifetime of the CLI; explicit close() is available for tests.
|
|
59
70
|
const _handles = new Map(); // pluginName -> { db, dir, path }
|
|
60
71
|
|
|
72
|
+
class JsonStatement {
|
|
73
|
+
constructor(db, sql) {
|
|
74
|
+
this.db = db;
|
|
75
|
+
this.sql = String(sql || '').trim().replace(/\s+/g, ' ').toUpperCase();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get(...args) {
|
|
79
|
+
if (this.sql === 'SELECT VALUE FROM KV WHERE KEY = ?') {
|
|
80
|
+
const key = String(args[0]);
|
|
81
|
+
return this.db.store.kv[key] ? { value: this.db.store.kv[key].value } : undefined;
|
|
82
|
+
}
|
|
83
|
+
throw new Error('Raw SELECT is only available with native node:sqlite');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
all(...args) {
|
|
87
|
+
if (this.sql === 'SELECT KEY FROM KV ORDER BY KEY') {
|
|
88
|
+
return Object.keys(this.db.store.kv).sort().map(key => ({ key }));
|
|
89
|
+
}
|
|
90
|
+
if (this.sql === 'SELECT ID, PAYLOAD, CREATED_AT FROM RECORDS WHERE STREAM = ? ORDER BY ID ASC LIMIT ?') {
|
|
91
|
+
return this.db.records(String(args[0]), Number(args[1]), 'asc');
|
|
92
|
+
}
|
|
93
|
+
if (this.sql === 'SELECT ID, PAYLOAD, CREATED_AT FROM RECORDS WHERE STREAM = ? ORDER BY ID DESC LIMIT ?') {
|
|
94
|
+
return this.db.records(String(args[0]), Number(args[1]), 'desc');
|
|
95
|
+
}
|
|
96
|
+
if (this.sql === 'SELECT COUNT(*) AS N FROM RECORDS WHERE STREAM = ?') {
|
|
97
|
+
return [{ n: this.db.store.records.filter(row => row.stream === String(args[0])).length }];
|
|
98
|
+
}
|
|
99
|
+
throw new Error('Raw SELECT is only available with native node:sqlite');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
run(...args) {
|
|
103
|
+
if (this.sql.startsWith('INSERT INTO KV(')) {
|
|
104
|
+
const [key, value, updated_at] = args;
|
|
105
|
+
this.db.store.kv[String(key)] = { value: String(value), updated_at: String(updated_at) };
|
|
106
|
+
this.db.save();
|
|
107
|
+
return { changes: 1, lastInsertRowid: 0 };
|
|
108
|
+
}
|
|
109
|
+
if (this.sql === 'DELETE FROM KV WHERE KEY = ?') {
|
|
110
|
+
const key = String(args[0]);
|
|
111
|
+
const existed = Object.prototype.hasOwnProperty.call(this.db.store.kv, key);
|
|
112
|
+
if (existed) {
|
|
113
|
+
delete this.db.store.kv[key];
|
|
114
|
+
this.db.save();
|
|
115
|
+
}
|
|
116
|
+
return { changes: existed ? 1 : 0, lastInsertRowid: 0 };
|
|
117
|
+
}
|
|
118
|
+
if (this.sql === 'INSERT INTO RECORDS(STREAM, PAYLOAD, CREATED_AT) VALUES(?, ?, ?)') {
|
|
119
|
+
const row = {
|
|
120
|
+
id: this.db.store.nextRecordId++,
|
|
121
|
+
stream: String(args[0]),
|
|
122
|
+
payload: String(args[1]),
|
|
123
|
+
created_at: String(args[2]),
|
|
124
|
+
};
|
|
125
|
+
this.db.store.records.push(row);
|
|
126
|
+
this.db.save();
|
|
127
|
+
return { changes: 1, lastInsertRowid: row.id };
|
|
128
|
+
}
|
|
129
|
+
if (this.sql === 'DELETE FROM RECORDS WHERE STREAM = ?') {
|
|
130
|
+
const stream = String(args[0]);
|
|
131
|
+
const before = this.db.store.records.length;
|
|
132
|
+
this.db.store.records = this.db.store.records.filter(row => row.stream !== stream);
|
|
133
|
+
const changes = before - this.db.store.records.length;
|
|
134
|
+
if (changes) this.db.save();
|
|
135
|
+
return { changes, lastInsertRowid: 0 };
|
|
136
|
+
}
|
|
137
|
+
if (this.sql === 'DELETE FROM RECORDS WHERE STREAM = ? AND ID = ?') {
|
|
138
|
+
const stream = String(args[0]);
|
|
139
|
+
const id = Number(args[1]);
|
|
140
|
+
const before = this.db.store.records.length;
|
|
141
|
+
this.db.store.records = this.db.store.records.filter(row => !(row.stream === stream && row.id === id));
|
|
142
|
+
const changes = before - this.db.store.records.length;
|
|
143
|
+
if (changes) this.db.save();
|
|
144
|
+
return { changes, lastInsertRowid: 0 };
|
|
145
|
+
}
|
|
146
|
+
throw new Error('Raw DML is only available with native node:sqlite');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
class JsonStateDb {
|
|
151
|
+
constructor(dbPath) {
|
|
152
|
+
this.path = dbPath;
|
|
153
|
+
this.store = {
|
|
154
|
+
kv: {},
|
|
155
|
+
records: [],
|
|
156
|
+
nextRecordId: 1,
|
|
157
|
+
};
|
|
158
|
+
try {
|
|
159
|
+
if (fs.existsSync(dbPath)) {
|
|
160
|
+
const parsed = JSON.parse(fs.readFileSync(dbPath, 'utf-8'));
|
|
161
|
+
if (parsed && typeof parsed === 'object') {
|
|
162
|
+
this.store = {
|
|
163
|
+
kv: parsed.kv && typeof parsed.kv === 'object' ? parsed.kv : {},
|
|
164
|
+
records: Array.isArray(parsed.records) ? parsed.records : [],
|
|
165
|
+
nextRecordId: Number(parsed.nextRecordId) || 1,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
this.save();
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
this.save();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
exec() {}
|
|
177
|
+
|
|
178
|
+
prepare(sql) {
|
|
179
|
+
return new JsonStatement(this, sql);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
records(stream, limit, order) {
|
|
183
|
+
const cap = Math.max(1, Math.min(10000, Math.floor(limit) || 50));
|
|
184
|
+
const rows = this.store.records
|
|
185
|
+
.filter(row => row.stream === stream)
|
|
186
|
+
.sort((a, b) => order === 'asc' ? a.id - b.id : b.id - a.id)
|
|
187
|
+
.slice(0, cap);
|
|
188
|
+
return rows.map(row => ({ id: row.id, payload: row.payload, created_at: row.created_at }));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
save() {
|
|
192
|
+
fs.writeFileSync(this.path, JSON.stringify(this.store, null, 2));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
close() {
|
|
196
|
+
this.save();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
61
200
|
function pluginDataDir(pluginName) {
|
|
62
201
|
if (!PLUGIN_NAME_RE.test(pluginName)) {
|
|
63
202
|
throw new Error(`invalid plugin name for state dir: ${pluginName}`);
|
|
@@ -71,7 +210,7 @@ function openDb(pluginName) {
|
|
|
71
210
|
if (_handles.has(pluginName)) return _handles.get(pluginName);
|
|
72
211
|
const dir = pluginDataDir(pluginName);
|
|
73
212
|
const dbPath = path.join(dir, 'state.db');
|
|
74
|
-
const db = new DatabaseSync(dbPath);
|
|
213
|
+
const db = DatabaseSync ? new DatabaseSync(dbPath) : new JsonStateDb(dbPath);
|
|
75
214
|
// WAL: multiple readers, one writer; robust against concurrent view+agent.
|
|
76
215
|
db.exec('PRAGMA journal_mode = WAL');
|
|
77
216
|
db.exec('PRAGMA synchronous = NORMAL');
|