@bahulam/code 0.1.12 → 0.1.14
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/commands/install.mjs +382 -0
- package/src/commands/plugin-manage.mjs +467 -89
- package/src/config/model-catalog-default.json +0 -4
- package/src/config/settings-loader.mjs +15 -0
- package/src/core/resume-mode.mjs +0 -1
- package/src/core/stream-client.mjs +18 -0
- package/src/core/tool-executor.mjs +47 -0
- package/src/daemon/session-core.mjs +3 -0
- package/src/local-service/agent-relay.mjs +6 -1
- package/src/local-service/file-access.mjs +116 -2
- package/src/local-service/server.mjs +209 -20
- package/src/plugins/manifest.mjs +3 -0
- package/src/plugins/npm-install.mjs +138 -0
- package/src/plugins/pi-compat/loader-hook.mjs +45 -0
- package/src/plugins/pi-compat/probe.mjs +294 -0
- package/src/plugins/pi-compat/requirements.mjs +465 -0
- package/src/plugins/pi-compat/scaffold.mjs +563 -0
- package/src/plugins/pi-compat/shim.mjs +162 -0
- package/src/plugins/pi-compose.mjs +147 -0
- package/src/plugins/preflight.mjs +29 -2
- package/src/plugins/registry.mjs +6 -0
- package/src/terminal/main.mjs +33 -6
- package/src/terminal/paste-input.mjs +23 -0
- package/src/terminal/repl.mjs +104 -30
- package/src/tools/registry.mjs +19 -0
- package/src/ui/input-dock.mjs +53 -12
- package/src/ui/text-layout.mjs +4 -3
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi runtime shim — the synthetic `pi` module a composed pi package
|
|
3
|
+
* imports at load time.
|
|
4
|
+
*
|
|
5
|
+
* Pi's extension API is imperative: extensions do `import { pi } from
|
|
6
|
+
* 'pi'` and call `pi.registerTool(name, schema, handler)` /
|
|
7
|
+
* `pi.registerCommand(cmd, handler)` / `pi.ctx.ui.setWidget(...)`. We
|
|
8
|
+
* intercept the module resolution via a Node ESM loader hook
|
|
9
|
+
* (`loader-hook.mjs`) that returns a virtual module which imports THIS
|
|
10
|
+
* shim and instantiates it against a shared capture object.
|
|
11
|
+
*
|
|
12
|
+
* v1 scope:
|
|
13
|
+
* - registerTool: captured, exposed to our loop as a pluginToolMap entry
|
|
14
|
+
* - registerCommand: captured but not surfaced (no REPL command bridge)
|
|
15
|
+
* - pi.events.on/emit: no-op (cross-extension event bus, deferred)
|
|
16
|
+
* - pi.ctx.ui.setWidget/custom: no-op with debug warning (TUI widgets
|
|
17
|
+
* don't translate to our workspace canvas; author dedicated panels)
|
|
18
|
+
* - pi.ctx.log: forwards to stderr with plugin prefix
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export function createPiShim({ pluginName = 'pi', captured }) {
|
|
22
|
+
if (!captured || typeof captured !== 'object') {
|
|
23
|
+
throw new Error('createPiShim: captured object is required');
|
|
24
|
+
}
|
|
25
|
+
captured.tools ||= [];
|
|
26
|
+
captured.commands ||= [];
|
|
27
|
+
|
|
28
|
+
const pi = {
|
|
29
|
+
// Pi's canonical shape is registerTool({name, description, parameters,
|
|
30
|
+
// execute}) — a single descriptor with an `execute` function. Older
|
|
31
|
+
// examples use registerTool(name, schema, handler) with positional args.
|
|
32
|
+
// Accept both.
|
|
33
|
+
registerTool(arg1, arg2, arg3) {
|
|
34
|
+
if (arg1 && typeof arg1 === 'object' && !Array.isArray(arg1)) {
|
|
35
|
+
const desc = arg1;
|
|
36
|
+
const name = desc.name;
|
|
37
|
+
if (!name || typeof name !== 'string') return;
|
|
38
|
+
// Descriptor form: pi's canonical `{name, description, parameters,
|
|
39
|
+
// execute}`. Handler is called as execute(id, params).
|
|
40
|
+
captured.tools.push({
|
|
41
|
+
name,
|
|
42
|
+
description: desc.description || '',
|
|
43
|
+
schema: desc.parameters || desc.input_schema || desc.schema || { type: 'object', properties: {} },
|
|
44
|
+
handler: typeof desc.execute === 'function' ? desc.execute
|
|
45
|
+
: typeof desc.handler === 'function' ? desc.handler
|
|
46
|
+
: typeof desc.call === 'function' ? desc.call
|
|
47
|
+
: null,
|
|
48
|
+
_form: 'descriptor',
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Positional legacy form: (name, schema, handler). Handler is
|
|
53
|
+
// called as handler(args).
|
|
54
|
+
if (!arg1 || typeof arg1 !== 'string') return;
|
|
55
|
+
let name = arg1, schema = arg2, handler = arg3;
|
|
56
|
+
if (typeof handler !== 'function' && typeof schema === 'function') {
|
|
57
|
+
handler = schema;
|
|
58
|
+
schema = { type: 'object', properties: {} };
|
|
59
|
+
}
|
|
60
|
+
captured.tools.push({
|
|
61
|
+
name,
|
|
62
|
+
description: '',
|
|
63
|
+
schema: schema || { type: 'object', properties: {} },
|
|
64
|
+
handler,
|
|
65
|
+
_form: 'positional',
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// registerCommand(cmd, descriptor) in real pi; descriptor has
|
|
70
|
+
// {description, execute}. Older form: registerCommand(cmd, handler).
|
|
71
|
+
registerCommand(cmd, arg2) {
|
|
72
|
+
if (!cmd || typeof cmd !== 'string') return;
|
|
73
|
+
if (arg2 && typeof arg2 === 'object' && !Array.isArray(arg2)) {
|
|
74
|
+
const desc = arg2;
|
|
75
|
+
captured.commands.push({
|
|
76
|
+
cmd,
|
|
77
|
+
description: desc.description || '',
|
|
78
|
+
handler: typeof desc.execute === 'function' ? desc.execute
|
|
79
|
+
: typeof desc.handler === 'function' ? desc.handler
|
|
80
|
+
: null,
|
|
81
|
+
});
|
|
82
|
+
} else if (typeof arg2 === 'function') {
|
|
83
|
+
captured.commands.push({ cmd, handler: arg2 });
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
events: {
|
|
88
|
+
on() { /* no-op in v1 */ },
|
|
89
|
+
emit() { /* no-op in v1 */ },
|
|
90
|
+
off() { /* no-op in v1 */ },
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// Pi packages call pi.on(...) directly for lifecycle events (session
|
|
94
|
+
// start/end etc.) — no-op them so activation reaches registerTool.
|
|
95
|
+
// Same treatment for pi.off, pi.emit, pi.once.
|
|
96
|
+
on() { /* no-op */ },
|
|
97
|
+
off() { /* no-op */ },
|
|
98
|
+
emit() { /* no-op */ },
|
|
99
|
+
once() { /* no-op */ },
|
|
100
|
+
|
|
101
|
+
// Additional pi surfaces called at activation-time by real packages
|
|
102
|
+
// (pi-web-access, etc.). Stub them so activation completes and tools
|
|
103
|
+
// register; runtime callers that rely on these still throw at call
|
|
104
|
+
// time, which is the correct signal that a feature isn't supported.
|
|
105
|
+
registerShortcut() { /* no-op */ },
|
|
106
|
+
appendEntry() { /* no-op */ },
|
|
107
|
+
sendMessage() { /* no-op */ },
|
|
108
|
+
exec() {
|
|
109
|
+
throw new Error(`[pi:${pluginName}] pi.exec is not supported in Bahulam compat`);
|
|
110
|
+
},
|
|
111
|
+
// pi.fetch is pi's authenticated fetch. Delegate to global fetch —
|
|
112
|
+
// that's what the extension expects: an HTTP client. Auth headers
|
|
113
|
+
// are typically added by the extension itself using env credentials.
|
|
114
|
+
fetch(...args) { return globalThis.fetch(...args); },
|
|
115
|
+
|
|
116
|
+
ctx: {
|
|
117
|
+
ui: {
|
|
118
|
+
setWidget(widget) {
|
|
119
|
+
if (process.env.DEBUG) {
|
|
120
|
+
const title = widget?.title || widget?.name || 'untitled';
|
|
121
|
+
process.stderr.write(`[pi:${pluginName}] widget ignored: ${title}\n`);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
custom() { /* no-op */ },
|
|
125
|
+
clear() { /* no-op */ },
|
|
126
|
+
},
|
|
127
|
+
log(...args) {
|
|
128
|
+
process.stderr.write(`[pi:${pluginName}] ${args.map(String).join(' ')}\n`);
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Pi's ExtensionAPI is a moving target — packages call methods we haven't
|
|
134
|
+
// stubbed yet (registerMessageRenderer, registerRoute, registerHandler,
|
|
135
|
+
// …). Any unstubbed method call throws, aborting activation before
|
|
136
|
+
// registerTool ever runs, and the probe reports 0 tools.
|
|
137
|
+
//
|
|
138
|
+
// Fall back to a no-op returner for every unknown property so activation
|
|
139
|
+
// reaches its full extent. A tool's runtime call may still fail if it
|
|
140
|
+
// needed that surface — that's an accurate signal at execution time,
|
|
141
|
+
// not a silent black hole at load time.
|
|
142
|
+
return new Proxy(pi, {
|
|
143
|
+
get(target, prop, receiver) {
|
|
144
|
+
if (prop in target) return Reflect.get(target, prop, receiver);
|
|
145
|
+
if (typeof prop === 'symbol') return undefined;
|
|
146
|
+
if (process.env.DEBUG) {
|
|
147
|
+
process.stderr.write(`[pi:${pluginName}] shim: pi.${String(prop)} stubbed (no-op)\n`);
|
|
148
|
+
}
|
|
149
|
+
// Return a callable that also has method access (e.g. pi.foo.bar).
|
|
150
|
+
// Property access on the stub returns another stub, so chains never
|
|
151
|
+
// throw. Result is undefined so anything that reads a return value
|
|
152
|
+
// treats it as "not present" (typeof result === 'undefined').
|
|
153
|
+
const stub = function stub() { return undefined; };
|
|
154
|
+
return new Proxy(stub, {
|
|
155
|
+
get(t, p) {
|
|
156
|
+
if (typeof p === 'symbol') return t[p];
|
|
157
|
+
return stub;
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi composition helpers.
|
|
3
|
+
*
|
|
4
|
+
* This is the contract layer for PRD-102 §13.6.1b. It deliberately does
|
|
5
|
+
* not install or execute pi packages yet; it gives manifest/preflight/
|
|
6
|
+
* registry code one normalized shape to test against.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const PI_TOOLS_CACHE = '.bahulam-tools.json';
|
|
10
|
+
|
|
11
|
+
const TOOL_NAME_RE = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
|
|
12
|
+
const NAMESPACE_RE = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
|
|
13
|
+
const NPM_NAME_RE = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/i;
|
|
14
|
+
|
|
15
|
+
export function parsePiSource(source) {
|
|
16
|
+
const raw = String(source || '').trim();
|
|
17
|
+
if (!raw.startsWith('pi:')) return null;
|
|
18
|
+
const spec = raw.slice(3).trim();
|
|
19
|
+
if (!spec) return null;
|
|
20
|
+
|
|
21
|
+
let packageName = spec;
|
|
22
|
+
let versionRange = '';
|
|
23
|
+
if (spec.startsWith('@')) {
|
|
24
|
+
const slash = spec.indexOf('/');
|
|
25
|
+
const versionAt = slash >= 0 ? spec.indexOf('@', slash + 1) : -1;
|
|
26
|
+
if (versionAt > 0) {
|
|
27
|
+
packageName = spec.slice(0, versionAt);
|
|
28
|
+
versionRange = spec.slice(versionAt + 1);
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
const versionAt = spec.lastIndexOf('@');
|
|
32
|
+
if (versionAt > 0) {
|
|
33
|
+
packageName = spec.slice(0, versionAt);
|
|
34
|
+
versionRange = spec.slice(versionAt + 1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!NPM_NAME_RE.test(packageName)) return null;
|
|
39
|
+
return {
|
|
40
|
+
kind: 'pi',
|
|
41
|
+
source: raw,
|
|
42
|
+
spec,
|
|
43
|
+
package_name: packageName,
|
|
44
|
+
packageName,
|
|
45
|
+
version_range: versionRange || null,
|
|
46
|
+
versionRange: versionRange || null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function normalizeCompose(composeDef, index = 0) {
|
|
51
|
+
const source = String(composeDef?.source || '').trim();
|
|
52
|
+
const parsed = parsePiSource(source);
|
|
53
|
+
const expose = Array.isArray(composeDef?.expose)
|
|
54
|
+
? composeDef.expose.map(item => String(item || '').trim()).filter(Boolean)
|
|
55
|
+
: [];
|
|
56
|
+
const namespace = String(composeDef?.as || '').trim();
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
source,
|
|
60
|
+
as: namespace || '',
|
|
61
|
+
expose,
|
|
62
|
+
verified: composeDef?.verified === true,
|
|
63
|
+
package_name: parsed?.package_name || '',
|
|
64
|
+
packageName: parsed?.packageName || '',
|
|
65
|
+
version_range: parsed?.version_range || null,
|
|
66
|
+
versionRange: parsed?.versionRange || null,
|
|
67
|
+
_index: index,
|
|
68
|
+
_kind: 'pi',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function normalizeComposes(value) {
|
|
73
|
+
if (!Array.isArray(value)) return [];
|
|
74
|
+
return value
|
|
75
|
+
.map((item, index) => normalizeCompose(item, index))
|
|
76
|
+
.filter(item => item.source || item.expose.length || item.as);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Anthropic's tool-name regex (`^[a-zA-Z0-9_-]{1,64}$`) forbids dots, and
|
|
80
|
+
// the backend's client-tool sanitizer enforces the same shape. `__` is the
|
|
81
|
+
// convention Claude Code and MCP both use for namespaced tool names, so
|
|
82
|
+
// stay compatible: `namespace__tool`.
|
|
83
|
+
export const COMPOSED_TOOL_SEPARATOR = '__';
|
|
84
|
+
|
|
85
|
+
export function composedToolName(compose, exposedName) {
|
|
86
|
+
const name = String(exposedName || '').trim();
|
|
87
|
+
return compose?.as ? `${compose.as}${COMPOSED_TOOL_SEPARATOR}${name}` : name;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function validateCompose(compose) {
|
|
91
|
+
const errors = [];
|
|
92
|
+
const warnings = [];
|
|
93
|
+
const label = `Compose #${Number.isInteger(compose?._index) ? compose._index : '?'}`;
|
|
94
|
+
|
|
95
|
+
if (!parsePiSource(compose?.source)) {
|
|
96
|
+
errors.push(`${label}: source must be a pi npm spec, for example pi:@scope/package@^1.0.0`);
|
|
97
|
+
}
|
|
98
|
+
if (compose?.as && !NAMESPACE_RE.test(compose.as)) {
|
|
99
|
+
errors.push(`${label}: as "${compose.as}" must match ${NAMESPACE_RE}`);
|
|
100
|
+
}
|
|
101
|
+
if (!Array.isArray(compose?.expose) || compose.expose.length === 0) {
|
|
102
|
+
errors.push(`${label}: expose must list at least one pi tool`);
|
|
103
|
+
} else {
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
for (const exposed of compose.expose) {
|
|
106
|
+
if (!TOOL_NAME_RE.test(exposed)) {
|
|
107
|
+
errors.push(`${label}: expose "${exposed}" must match ${TOOL_NAME_RE}`);
|
|
108
|
+
}
|
|
109
|
+
if (seen.has(exposed)) {
|
|
110
|
+
errors.push(`${label}: duplicate exposed tool "${exposed}"`);
|
|
111
|
+
}
|
|
112
|
+
seen.add(exposed);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (compose?.verified !== true) {
|
|
116
|
+
warnings.push(`${label}: ${compose?.source || 'pi package'} is unverified; hosted Studios require verified pi packages`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { errors, warnings };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function expandComposedTools(pluginName, pluginDir, composes = []) {
|
|
123
|
+
const tools = [];
|
|
124
|
+
for (const compose of composes || []) {
|
|
125
|
+
for (const exposedName of compose.expose || []) {
|
|
126
|
+
tools.push({
|
|
127
|
+
name: composedToolName(compose, exposedName),
|
|
128
|
+
description: `Composed pi tool ${exposedName} from ${compose.source}`,
|
|
129
|
+
input_schema: { type: 'object', properties: {} },
|
|
130
|
+
tool: '',
|
|
131
|
+
plugin_name: pluginName,
|
|
132
|
+
_plugin_name: pluginName,
|
|
133
|
+
_plugin_dir: pluginDir,
|
|
134
|
+
_composed: {
|
|
135
|
+
kind: 'pi',
|
|
136
|
+
source: compose.source,
|
|
137
|
+
package_name: compose.package_name,
|
|
138
|
+
version_range: compose.version_range,
|
|
139
|
+
namespace: compose.as || null,
|
|
140
|
+
original_name: exposedName,
|
|
141
|
+
verified: compose.verified === true,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return tools;
|
|
147
|
+
}
|
|
@@ -22,6 +22,7 @@ import * as os from 'node:os';
|
|
|
22
22
|
import * as path from 'node:path';
|
|
23
23
|
import { pathToFileURL } from 'node:url';
|
|
24
24
|
import { parsePluginManifestFile, validatePluginManifest } from './manifest.mjs';
|
|
25
|
+
import { composedToolName, validateCompose } from './pi-compose.mjs';
|
|
25
26
|
|
|
26
27
|
const TOOL_NAME_RE = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
|
|
27
28
|
const AGENT_SLUG_RE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
@@ -38,6 +39,8 @@ export const RESERVED_TOOL_NAMES = new Set([
|
|
|
38
39
|
// write
|
|
39
40
|
'write_file', 'write_project', 'edit_file', 'delete_file', 'shell',
|
|
40
41
|
'analyze_image', 'generate_image',
|
|
42
|
+
// background jobs (PRD-102 §6.2.3) — long-running renders, builds, etc.
|
|
43
|
+
'job_output', 'job_kill', 'job_status', 'job_list',
|
|
41
44
|
// agent/skill/workflow admin
|
|
42
45
|
'ask_user', 'agent_create', 'agent_sync', 'agents_list',
|
|
43
46
|
'skill_install', 'skill_update', 'skill_remove', 'skill_view', 'skills_list',
|
|
@@ -86,6 +89,7 @@ export async function preflightPlugin(pluginDir, opts = {}) {
|
|
|
86
89
|
const views = manifest.spec?.workspace?.views || [];
|
|
87
90
|
const mcpServers = manifest.spec?.mcpServers || {};
|
|
88
91
|
const mcpServerNames = new Set(Object.keys(mcpServers));
|
|
92
|
+
const composes = manifest.spec?.composes || [];
|
|
89
93
|
|
|
90
94
|
// MCP server sanity — every server should have EITHER command (stdio)
|
|
91
95
|
// OR url (remote). Anything else is meaningless config.
|
|
@@ -101,6 +105,26 @@ export async function preflightPlugin(pluginDir, opts = {}) {
|
|
|
101
105
|
}
|
|
102
106
|
}
|
|
103
107
|
|
|
108
|
+
// Pi composition sanity. Composed tools become part of the agent-visible
|
|
109
|
+
// tool namespace, but they are not local files and are not imported here.
|
|
110
|
+
const composedToolNames = new Set();
|
|
111
|
+
for (const compose of composes) {
|
|
112
|
+
const validated = validateCompose(compose);
|
|
113
|
+
errors.push(...validated.errors);
|
|
114
|
+
warnings.push(...validated.warnings);
|
|
115
|
+
if (compose.as && mcpServerNames.has(compose.as)) {
|
|
116
|
+
errors.push(`Compose #${compose._index}: namespace "${compose.as}" collides with an MCP server name`);
|
|
117
|
+
}
|
|
118
|
+
for (const exposedName of compose.expose || []) {
|
|
119
|
+
const fullName = composedToolName(compose, exposedName);
|
|
120
|
+
if (composedToolNames.has(fullName)) errors.push(`Composed tool "${fullName}": duplicate name`);
|
|
121
|
+
if (RESERVED_TOOL_NAMES.has(fullName)) {
|
|
122
|
+
errors.push(`Composed tool "${fullName}": shadows a built-in tool`);
|
|
123
|
+
}
|
|
124
|
+
composedToolNames.add(fullName);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
104
128
|
// 2 + 3 + 4. Tool checks
|
|
105
129
|
const toolNames = new Set();
|
|
106
130
|
for (const [i, tool] of tools.entries()) {
|
|
@@ -114,6 +138,9 @@ export async function preflightPlugin(pluginDir, opts = {}) {
|
|
|
114
138
|
if (RESERVED_TOOL_NAMES.has(tool.name)) {
|
|
115
139
|
errors.push(`Tool "${t}": shadows a built-in tool — pick a different name (built-ins always win)`);
|
|
116
140
|
}
|
|
141
|
+
if (composedToolNames.has(tool.name)) {
|
|
142
|
+
errors.push(`Tool "${t}": collides with a composed pi tool`);
|
|
143
|
+
}
|
|
117
144
|
if (!tool.description || tool.description.length < 8) {
|
|
118
145
|
warnings.push(`Tool "${t}": description is missing or very short (<8 chars) — the model uses this to decide when to call it`);
|
|
119
146
|
}
|
|
@@ -166,12 +193,12 @@ export async function preflightPlugin(pluginDir, opts = {}) {
|
|
|
166
193
|
// plugin's mcpServers. The <tool> half is discovered live.
|
|
167
194
|
if (toolRef.includes('.')) {
|
|
168
195
|
const serverName = toolRef.split('.', 1)[0];
|
|
169
|
-
if (!mcpServerNames.has(serverName)) {
|
|
196
|
+
if (!mcpServerNames.has(serverName) && !composedToolNames.has(toolRef)) {
|
|
170
197
|
errors.push(`Agent "${slug}": tool "${toolRef}" references MCP server "${serverName}" which is not declared in mcpServers`);
|
|
171
198
|
}
|
|
172
199
|
continue;
|
|
173
200
|
}
|
|
174
|
-
if (!toolNames.has(toolRef) && !RESERVED_TOOL_NAMES.has(toolRef)) {
|
|
201
|
+
if (!toolNames.has(toolRef) && !composedToolNames.has(toolRef) && !RESERVED_TOOL_NAMES.has(toolRef)) {
|
|
175
202
|
errors.push(`Agent "${slug}": tool "${toolRef}" is not defined by this plugin and is not a built-in`);
|
|
176
203
|
}
|
|
177
204
|
}
|
package/src/plugins/registry.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import fs from 'fs';
|
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import os from 'os';
|
|
11
11
|
import { parsePluginManifestFile, validatePluginManifest } from './manifest.mjs';
|
|
12
|
+
import { expandComposedTools } from './pi-compose.mjs';
|
|
12
13
|
|
|
13
14
|
const DEFAULT_PLUGIN_DIRS = () => [
|
|
14
15
|
path.join(process.cwd(), '.bahulam', 'plugins'),
|
|
@@ -159,6 +160,11 @@ export class PluginRegistry {
|
|
|
159
160
|
_plugin_dir: plugin._dir,
|
|
160
161
|
});
|
|
161
162
|
}
|
|
163
|
+
tools.push(...expandComposedTools(
|
|
164
|
+
plugin.metadata?.name || '',
|
|
165
|
+
plugin._dir,
|
|
166
|
+
plugin.spec?.composes || [],
|
|
167
|
+
));
|
|
162
168
|
}
|
|
163
169
|
return tools;
|
|
164
170
|
}
|
package/src/terminal/main.mjs
CHANGED
|
@@ -24,9 +24,10 @@ const subcommand = process.argv[2];
|
|
|
24
24
|
const subcommandArgs = process.argv.slice(3);
|
|
25
25
|
|
|
26
26
|
const PLUGIN_MANAGEMENT_COMMANDS = new Set([
|
|
27
|
-
'
|
|
27
|
+
'validate', 'check', 'lint',
|
|
28
28
|
'list', 'ls', 'remove', 'rm', 'uninstall',
|
|
29
29
|
'enable', 'disable', 'info', 'update', 'upgrade',
|
|
30
|
+
'doctor',
|
|
30
31
|
]);
|
|
31
32
|
|
|
32
33
|
function parsePluginArgs(argv) {
|
|
@@ -63,14 +64,13 @@ function parsePluginArgs(argv) {
|
|
|
63
64
|
}
|
|
64
65
|
if (positional.length && PLUGIN_MANAGEMENT_COMMANDS.has(positional[0].toLowerCase())) {
|
|
65
66
|
parsed.action = positional.shift().toLowerCase();
|
|
66
|
-
if (
|
|
67
|
-
else if (['validate', 'check', 'lint'].includes(parsed.action)) {
|
|
67
|
+
if (['validate', 'check', 'lint'].includes(parsed.action)) {
|
|
68
68
|
// Accepts either a directory path or an installed plugin name.
|
|
69
69
|
const arg = positional.shift() || null;
|
|
70
70
|
if (arg && (arg.includes('/') || fs.existsSync?.(arg))) parsed.source = arg;
|
|
71
71
|
else parsed.pluginName = arg;
|
|
72
72
|
}
|
|
73
|
-
else if (['info', 'remove', 'rm', 'uninstall', 'enable', 'disable', 'update', 'upgrade'].includes(parsed.action)) {
|
|
73
|
+
else if (['info', 'remove', 'rm', 'uninstall', 'enable', 'disable', 'update', 'upgrade', 'doctor'].includes(parsed.action)) {
|
|
74
74
|
parsed.pluginName = positional.shift() || null;
|
|
75
75
|
}
|
|
76
76
|
} else {
|
|
@@ -279,7 +279,28 @@ async function main() {
|
|
|
279
279
|
return;
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
+
if (subcommand === 'pull') {
|
|
283
|
+
const { handlePullCommand } = await import('../commands/install.mjs');
|
|
284
|
+
await handlePullCommand(subcommandArgs, { cwd: process.cwd() });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (subcommand === 'install') {
|
|
289
|
+
const { handleInstallCommand } = await import('../commands/install.mjs');
|
|
290
|
+
await handleInstallCommand(subcommandArgs, { cwd: process.cwd() });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
282
294
|
if (subcommand === 'plugin' || subcommand === 'plugins') {
|
|
295
|
+
// `install`/`pull` moved to top-level. Detect the old form and redirect.
|
|
296
|
+
if (subcommandArgs[0] === 'install' || subcommandArgs[0] === 'pull') {
|
|
297
|
+
const verb = subcommandArgs[0];
|
|
298
|
+
const rest = subcommandArgs.slice(1);
|
|
299
|
+
process.stderr.write(`\x1b[33m!\x1b[0m \`bahulam plugin ${verb}\` moved to top-level. Use:\n`);
|
|
300
|
+
process.stderr.write(` \x1b[36mbahulam install ${rest.join(' ')}\x1b[0m (pack — scaffolds around pi:, installs git/tarball/local)\n`);
|
|
301
|
+
process.stderr.write(` \x1b[36mbahulam pull ${rest.join(' ')}\x1b[0m (ingredient only — pi: sources)\n`);
|
|
302
|
+
process.exit(2);
|
|
303
|
+
}
|
|
283
304
|
const args = parsePluginArgs(subcommandArgs);
|
|
284
305
|
if (args.action && args.action !== 'open') {
|
|
285
306
|
const { handlePluginManagementCommand } = await import('../commands/plugin-manage.mjs');
|
|
@@ -332,8 +353,14 @@ async function main() {
|
|
|
332
353
|
bahulam workspace list List recent local workspace sessions
|
|
333
354
|
bahulam local open [path] Alias for workspace open
|
|
334
355
|
|
|
335
|
-
\x1b[
|
|
336
|
-
bahulam
|
|
356
|
+
\x1b[1mPacks & ingredients:\x1b[0m
|
|
357
|
+
bahulam pull pi:<name> Pull a pi ingredient (composable, not runnable on its own)
|
|
358
|
+
bahulam install pi:<name> Pull ingredient + scaffold a full Bahulam pack around it
|
|
359
|
+
bahulam install <git-url> Install a hand-authored pack from git
|
|
360
|
+
bahulam install <local-path> Install a hand-authored pack from disk
|
|
361
|
+
bahulam plugin list List installed packs and pi ingredients
|
|
362
|
+
bahulam plugin remove <name> Remove an installed pack
|
|
363
|
+
bahulam plugin <name> [path] Open a workspace with an installed pack
|
|
337
364
|
|
|
338
365
|
\x1b[1mAnalytics:\x1b[0m
|
|
339
366
|
bahulam sessions List recent local sessions
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function isRawMultilinePasteChunk(text) {
|
|
2
|
+
const value = String(text || '');
|
|
3
|
+
if (!value) return false;
|
|
4
|
+
if (!/[\r\n]/.test(value)) return false;
|
|
5
|
+
|
|
6
|
+
const withoutLineBreaks = value.replace(/[\r\n]/g, '');
|
|
7
|
+
if (!withoutLineBreaks.length) return false;
|
|
8
|
+
|
|
9
|
+
// A single printable char followed by Enter can be delivered in one chunk
|
|
10
|
+
// by some terminals; keep that as normal line submission.
|
|
11
|
+
return withoutLineBreaks.length > 1 || value.split(/\r?\n|\r/).length > 2;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function normalizePastedText(text) {
|
|
15
|
+
return String(text || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function pastedTextLabel(text) {
|
|
19
|
+
const value = normalizePastedText(text);
|
|
20
|
+
const lines = value ? value.split('\n').length : 0;
|
|
21
|
+
if (lines > 1) return `[text copied · ${lines} lines]`;
|
|
22
|
+
return '[text copied]';
|
|
23
|
+
}
|