@bahulam/code 0.1.12 → 0.1.13
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 +295 -0
- package/src/commands/plugin-manage.mjs +280 -88
- package/src/config/settings-loader.mjs +15 -0
- package/src/core/tool-executor.mjs +47 -0
- package/src/plugins/manifest.mjs +3 -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/scaffold.mjs +487 -0
- package/src/plugins/pi-compat/shim.mjs +134 -0
- package/src/plugins/pi-compose.mjs +147 -0
- package/src/plugins/preflight.mjs +27 -2
- package/src/plugins/registry.mjs +6 -0
- package/src/terminal/main.mjs +31 -5
- package/src/terminal/paste-input.mjs +23 -0
- package/src/terminal/repl.mjs +52 -7
- package/src/tools/registry.mjs +19 -0
- package/src/ui/input-dock.mjs +5 -2
|
@@ -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}$/;
|
|
@@ -86,6 +87,7 @@ export async function preflightPlugin(pluginDir, opts = {}) {
|
|
|
86
87
|
const views = manifest.spec?.workspace?.views || [];
|
|
87
88
|
const mcpServers = manifest.spec?.mcpServers || {};
|
|
88
89
|
const mcpServerNames = new Set(Object.keys(mcpServers));
|
|
90
|
+
const composes = manifest.spec?.composes || [];
|
|
89
91
|
|
|
90
92
|
// MCP server sanity — every server should have EITHER command (stdio)
|
|
91
93
|
// OR url (remote). Anything else is meaningless config.
|
|
@@ -101,6 +103,26 @@ export async function preflightPlugin(pluginDir, opts = {}) {
|
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
// Pi composition sanity. Composed tools become part of the agent-visible
|
|
107
|
+
// tool namespace, but they are not local files and are not imported here.
|
|
108
|
+
const composedToolNames = new Set();
|
|
109
|
+
for (const compose of composes) {
|
|
110
|
+
const validated = validateCompose(compose);
|
|
111
|
+
errors.push(...validated.errors);
|
|
112
|
+
warnings.push(...validated.warnings);
|
|
113
|
+
if (compose.as && mcpServerNames.has(compose.as)) {
|
|
114
|
+
errors.push(`Compose #${compose._index}: namespace "${compose.as}" collides with an MCP server name`);
|
|
115
|
+
}
|
|
116
|
+
for (const exposedName of compose.expose || []) {
|
|
117
|
+
const fullName = composedToolName(compose, exposedName);
|
|
118
|
+
if (composedToolNames.has(fullName)) errors.push(`Composed tool "${fullName}": duplicate name`);
|
|
119
|
+
if (RESERVED_TOOL_NAMES.has(fullName)) {
|
|
120
|
+
errors.push(`Composed tool "${fullName}": shadows a built-in tool`);
|
|
121
|
+
}
|
|
122
|
+
composedToolNames.add(fullName);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
104
126
|
// 2 + 3 + 4. Tool checks
|
|
105
127
|
const toolNames = new Set();
|
|
106
128
|
for (const [i, tool] of tools.entries()) {
|
|
@@ -114,6 +136,9 @@ export async function preflightPlugin(pluginDir, opts = {}) {
|
|
|
114
136
|
if (RESERVED_TOOL_NAMES.has(tool.name)) {
|
|
115
137
|
errors.push(`Tool "${t}": shadows a built-in tool — pick a different name (built-ins always win)`);
|
|
116
138
|
}
|
|
139
|
+
if (composedToolNames.has(tool.name)) {
|
|
140
|
+
errors.push(`Tool "${t}": collides with a composed pi tool`);
|
|
141
|
+
}
|
|
117
142
|
if (!tool.description || tool.description.length < 8) {
|
|
118
143
|
warnings.push(`Tool "${t}": description is missing or very short (<8 chars) — the model uses this to decide when to call it`);
|
|
119
144
|
}
|
|
@@ -166,12 +191,12 @@ export async function preflightPlugin(pluginDir, opts = {}) {
|
|
|
166
191
|
// plugin's mcpServers. The <tool> half is discovered live.
|
|
167
192
|
if (toolRef.includes('.')) {
|
|
168
193
|
const serverName = toolRef.split('.', 1)[0];
|
|
169
|
-
if (!mcpServerNames.has(serverName)) {
|
|
194
|
+
if (!mcpServerNames.has(serverName) && !composedToolNames.has(toolRef)) {
|
|
170
195
|
errors.push(`Agent "${slug}": tool "${toolRef}" references MCP server "${serverName}" which is not declared in mcpServers`);
|
|
171
196
|
}
|
|
172
197
|
continue;
|
|
173
198
|
}
|
|
174
|
-
if (!toolNames.has(toolRef) && !RESERVED_TOOL_NAMES.has(toolRef)) {
|
|
199
|
+
if (!toolNames.has(toolRef) && !composedToolNames.has(toolRef) && !RESERVED_TOOL_NAMES.has(toolRef)) {
|
|
175
200
|
errors.push(`Agent "${slug}": tool "${toolRef}" is not defined by this plugin and is not a built-in`);
|
|
176
201
|
}
|
|
177
202
|
}
|
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,7 +24,7 @@ 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
30
|
]);
|
|
@@ -63,8 +63,7 @@ function parsePluginArgs(argv) {
|
|
|
63
63
|
}
|
|
64
64
|
if (positional.length && PLUGIN_MANAGEMENT_COMMANDS.has(positional[0].toLowerCase())) {
|
|
65
65
|
parsed.action = positional.shift().toLowerCase();
|
|
66
|
-
if (
|
|
67
|
-
else if (['validate', 'check', 'lint'].includes(parsed.action)) {
|
|
66
|
+
if (['validate', 'check', 'lint'].includes(parsed.action)) {
|
|
68
67
|
// Accepts either a directory path or an installed plugin name.
|
|
69
68
|
const arg = positional.shift() || null;
|
|
70
69
|
if (arg && (arg.includes('/') || fs.existsSync?.(arg))) parsed.source = arg;
|
|
@@ -279,7 +278,28 @@ async function main() {
|
|
|
279
278
|
return;
|
|
280
279
|
}
|
|
281
280
|
|
|
281
|
+
if (subcommand === 'pull') {
|
|
282
|
+
const { handlePullCommand } = await import('../commands/install.mjs');
|
|
283
|
+
await handlePullCommand(subcommandArgs, { cwd: process.cwd() });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (subcommand === 'install') {
|
|
288
|
+
const { handleInstallCommand } = await import('../commands/install.mjs');
|
|
289
|
+
await handleInstallCommand(subcommandArgs, { cwd: process.cwd() });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
282
293
|
if (subcommand === 'plugin' || subcommand === 'plugins') {
|
|
294
|
+
// `install`/`pull` moved to top-level. Detect the old form and redirect.
|
|
295
|
+
if (subcommandArgs[0] === 'install' || subcommandArgs[0] === 'pull') {
|
|
296
|
+
const verb = subcommandArgs[0];
|
|
297
|
+
const rest = subcommandArgs.slice(1);
|
|
298
|
+
process.stderr.write(`\x1b[33m!\x1b[0m \`bahulam plugin ${verb}\` moved to top-level. Use:\n`);
|
|
299
|
+
process.stderr.write(` \x1b[36mbahulam install ${rest.join(' ')}\x1b[0m (pack — scaffolds around pi:, installs git/tarball/local)\n`);
|
|
300
|
+
process.stderr.write(` \x1b[36mbahulam pull ${rest.join(' ')}\x1b[0m (ingredient only — pi: sources)\n`);
|
|
301
|
+
process.exit(2);
|
|
302
|
+
}
|
|
283
303
|
const args = parsePluginArgs(subcommandArgs);
|
|
284
304
|
if (args.action && args.action !== 'open') {
|
|
285
305
|
const { handlePluginManagementCommand } = await import('../commands/plugin-manage.mjs');
|
|
@@ -332,8 +352,14 @@ async function main() {
|
|
|
332
352
|
bahulam workspace list List recent local workspace sessions
|
|
333
353
|
bahulam local open [path] Alias for workspace open
|
|
334
354
|
|
|
335
|
-
\x1b[
|
|
336
|
-
bahulam
|
|
355
|
+
\x1b[1mPacks & ingredients:\x1b[0m
|
|
356
|
+
bahulam pull pi:<name> Pull a pi ingredient (composable, not runnable on its own)
|
|
357
|
+
bahulam install pi:<name> Pull ingredient + scaffold a full Bahulam pack around it
|
|
358
|
+
bahulam install <git-url> Install a hand-authored pack from git
|
|
359
|
+
bahulam install <local-path> Install a hand-authored pack from disk
|
|
360
|
+
bahulam plugin list List installed packs and pi ingredients
|
|
361
|
+
bahulam plugin remove <name> Remove an installed pack
|
|
362
|
+
bahulam plugin <name> [path] Open a workspace with an installed pack
|
|
337
363
|
|
|
338
364
|
\x1b[1mAnalytics:\x1b[0m
|
|
339
365
|
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
|
+
}
|
package/src/terminal/repl.mjs
CHANGED
|
@@ -71,6 +71,7 @@ import { PluginRegistry } from '../plugins/registry.mjs';
|
|
|
71
71
|
import { SessionManager } from '../core/session-manager.mjs';
|
|
72
72
|
import { parseArgs } from '../config/cli-args.mjs';
|
|
73
73
|
import { pickModelOverridesForm } from './repl-model-form.mjs';
|
|
74
|
+
import { isRawMultilinePasteChunk, normalizePastedText, pastedTextLabel } from './paste-input.mjs';
|
|
74
75
|
import {
|
|
75
76
|
MODEL_CATEGORY_ORDER,
|
|
76
77
|
formatCategoryBadge,
|
|
@@ -4857,6 +4858,9 @@ export async function startTerminalRepl() {
|
|
|
4857
4858
|
let _bracketedPasteStartLine = '';
|
|
4858
4859
|
let _bracketedPasteStartCursor = 0;
|
|
4859
4860
|
let _promptHasInsertedPaste = false;
|
|
4861
|
+
let _suppressRawPasteLines = false;
|
|
4862
|
+
let _pastedInputValue = '';
|
|
4863
|
+
let _pastedInputLabel = '';
|
|
4860
4864
|
const _pasteEndListeners = new Set();
|
|
4861
4865
|
function onBracketedPasteEnd(cb) { _pasteEndListeners.add(cb); return () => _pasteEndListeners.delete(cb); }
|
|
4862
4866
|
function isInBracketedPaste() { return _inBracketedPaste; }
|
|
@@ -4876,7 +4880,25 @@ export async function startTerminalRepl() {
|
|
|
4876
4880
|
while (i < s.length) {
|
|
4877
4881
|
if (!_inBracketedPaste) {
|
|
4878
4882
|
const start = s.indexOf(PASTE_BEGIN, i);
|
|
4879
|
-
if (start === -1)
|
|
4883
|
+
if (start === -1) {
|
|
4884
|
+
if (isRawMultilinePasteChunk(s)) {
|
|
4885
|
+
_suppressRawPasteLines = true;
|
|
4886
|
+
const baseLine = String(rl?.line || '');
|
|
4887
|
+
const baseCursor = typeof rl?.cursor === 'number' ? rl.cursor : baseLine.length;
|
|
4888
|
+
setImmediate(() => {
|
|
4889
|
+
try {
|
|
4890
|
+
insertPromptText(normalizePastedText(s), {
|
|
4891
|
+
baseLine,
|
|
4892
|
+
baseCursor,
|
|
4893
|
+
fromPaste: true,
|
|
4894
|
+
});
|
|
4895
|
+
} finally {
|
|
4896
|
+
_suppressRawPasteLines = false;
|
|
4897
|
+
}
|
|
4898
|
+
});
|
|
4899
|
+
}
|
|
4900
|
+
return;
|
|
4901
|
+
}
|
|
4880
4902
|
_inBracketedPaste = true;
|
|
4881
4903
|
_bracketedPasteBuffer = '';
|
|
4882
4904
|
_suppressBracketedPasteLines = true;
|
|
@@ -5138,7 +5160,11 @@ export async function startTerminalRepl() {
|
|
|
5138
5160
|
const line = String(baseLine || '');
|
|
5139
5161
|
const cursor = typeof baseCursor === 'number' ? Math.max(0, Math.min(line.length, baseCursor)) : line.length;
|
|
5140
5162
|
const next = `${line.slice(0, cursor)}${payload}${line.slice(cursor)}`;
|
|
5141
|
-
if (fromPaste)
|
|
5163
|
+
if (fromPaste) {
|
|
5164
|
+
_promptHasInsertedPaste = true;
|
|
5165
|
+
_pastedInputValue = next;
|
|
5166
|
+
_pastedInputLabel = pastedTextLabel(payload);
|
|
5167
|
+
}
|
|
5142
5168
|
replaceReadlineLine(next, cursor + payload.length);
|
|
5143
5169
|
renderIdleDockInput();
|
|
5144
5170
|
}
|
|
@@ -5178,15 +5204,28 @@ export async function startTerminalRepl() {
|
|
|
5178
5204
|
|
|
5179
5205
|
function renderIdleDockInput() {
|
|
5180
5206
|
if (!isInputDockMounted()) return false;
|
|
5207
|
+
const line = rl.line || '';
|
|
5208
|
+
let displayLine = line;
|
|
5209
|
+
let displayCursor = typeof rl.cursor === 'number' ? rl.cursor : null;
|
|
5210
|
+
let fixedRows = null;
|
|
5211
|
+
if (_pastedInputValue && line === _pastedInputValue) {
|
|
5212
|
+
displayLine = _pastedInputLabel;
|
|
5213
|
+
displayCursor = _pastedInputLabel.length;
|
|
5214
|
+
fixedRows = 1;
|
|
5215
|
+
} else if (_pastedInputValue) {
|
|
5216
|
+
_pastedInputValue = '';
|
|
5217
|
+
_pastedInputLabel = '';
|
|
5218
|
+
}
|
|
5181
5219
|
// rl.cursor is readline's byte offset within rl.line. Threading it
|
|
5182
5220
|
// through to focusDockInput makes arrow-key navigation visually move
|
|
5183
5221
|
// the terminal cursor within the buffer instead of always landing at
|
|
5184
5222
|
// the end of the string.
|
|
5185
|
-
return renderDockInput(userPrompt(),
|
|
5223
|
+
return renderDockInput(userPrompt(), displayLine, {
|
|
5186
5224
|
context: buildContextStrip(),
|
|
5187
5225
|
meta: buildDockMeta(),
|
|
5188
5226
|
tips: idleInputTips(),
|
|
5189
|
-
cursor:
|
|
5227
|
+
cursor: displayCursor,
|
|
5228
|
+
fixedRows,
|
|
5190
5229
|
});
|
|
5191
5230
|
}
|
|
5192
5231
|
|
|
@@ -5232,7 +5271,7 @@ export async function startTerminalRepl() {
|
|
|
5232
5271
|
readline.emitKeypressEvents(process.stdin, rl);
|
|
5233
5272
|
process.stdin.on('keypress', (_str, key = {}) => {
|
|
5234
5273
|
if (!inputActive) return;
|
|
5235
|
-
if (_inBracketedPaste || _suppressBracketedPasteLines) return;
|
|
5274
|
+
if (_inBracketedPaste || _suppressBracketedPasteLines || _suppressRawPasteLines) return;
|
|
5236
5275
|
if (key.name === 'return' || key.name === 'enter') return;
|
|
5237
5276
|
if (key.name === 'f2') {
|
|
5238
5277
|
clearSlashHint();
|
|
@@ -5243,7 +5282,7 @@ export async function startTerminalRepl() {
|
|
|
5243
5282
|
}
|
|
5244
5283
|
setImmediate(() => {
|
|
5245
5284
|
if (!inputActive) return;
|
|
5246
|
-
if (_inBracketedPaste || _suppressBracketedPasteLines) return;
|
|
5285
|
+
if (_inBracketedPaste || _suppressBracketedPasteLines || _suppressRawPasteLines) return;
|
|
5247
5286
|
if (slashHintVisible && key.name === 'tab' && acceptSlashHint()) return;
|
|
5248
5287
|
if (slashHintVisible && key.name === 'down' && moveSlashHintSelection(1)) return;
|
|
5249
5288
|
if (slashHintVisible && key.name === 'up' && moveSlashHintSelection(-1)) return;
|
|
@@ -5305,12 +5344,16 @@ export async function startTerminalRepl() {
|
|
|
5305
5344
|
if (pastedLines.length > 1 || trailing) {
|
|
5306
5345
|
const text = [...pastedLines, trailing].join('\n');
|
|
5307
5346
|
_promptHasInsertedPaste = true;
|
|
5347
|
+
_pastedInputValue = text;
|
|
5348
|
+
_pastedInputLabel = pastedTextLabel(text);
|
|
5308
5349
|
replaceReadlineLine(text);
|
|
5309
5350
|
renderIdleDockInput();
|
|
5310
5351
|
return;
|
|
5311
5352
|
}
|
|
5312
5353
|
const line = pastedLines.join('\n');
|
|
5313
5354
|
_promptHasInsertedPaste = false;
|
|
5355
|
+
_pastedInputValue = '';
|
|
5356
|
+
_pastedInputLabel = '';
|
|
5314
5357
|
queueOrRunLine(line);
|
|
5315
5358
|
}
|
|
5316
5359
|
|
|
@@ -5320,7 +5363,7 @@ export async function startTerminalRepl() {
|
|
|
5320
5363
|
// or the user pressed Enter normally), the debounce falls back to old
|
|
5321
5364
|
// behavior — a single Enter flushes almost instantly.
|
|
5322
5365
|
rl.on('line', async (line) => {
|
|
5323
|
-
if (_suppressBracketedPasteLines) {
|
|
5366
|
+
if (_suppressBracketedPasteLines || _suppressRawPasteLines) {
|
|
5324
5367
|
_pasteLines = [];
|
|
5325
5368
|
if (_pasteFlushTimer) {
|
|
5326
5369
|
clearTimeout(_pasteFlushTimer);
|
|
@@ -5369,6 +5412,8 @@ export async function startTerminalRepl() {
|
|
|
5369
5412
|
let input = line.trim();
|
|
5370
5413
|
const selectedSlashCommand = selectedSlashCommandFor(input);
|
|
5371
5414
|
inputActive = false;
|
|
5415
|
+
_pastedInputValue = '';
|
|
5416
|
+
_pastedInputLabel = '';
|
|
5372
5417
|
clearSlashHint();
|
|
5373
5418
|
if (selectedSlashCommand) input = selectedSlashCommand;
|
|
5374
5419
|
if (!input) {
|
package/src/tools/registry.mjs
CHANGED
|
@@ -122,6 +122,25 @@ export function createToolRegistry({
|
|
|
122
122
|
const name = String(toolDef.name || '').trim();
|
|
123
123
|
if (!name || tools.has(name)) continue;
|
|
124
124
|
const pluginName = toolDef._plugin_name || toolDef.plugin_name || null;
|
|
125
|
+
if (toolDef._composed?.kind === 'pi') {
|
|
126
|
+
tools.set(name, {
|
|
127
|
+
name,
|
|
128
|
+
description: toolDef.description || '',
|
|
129
|
+
inputSchema: toolDef.input_schema || toolDef.parameters || { type: 'object', properties: {} },
|
|
130
|
+
validateInput() { return []; },
|
|
131
|
+
async call() {
|
|
132
|
+
return {
|
|
133
|
+
success: false,
|
|
134
|
+
output: `Composed pi tool '${name}' is registered but pi runtime execution is not wired yet`,
|
|
135
|
+
_tool: name,
|
|
136
|
+
_plugin: pluginName,
|
|
137
|
+
_composed: toolDef._composed,
|
|
138
|
+
_blocked: true,
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
125
144
|
tools.set(name, {
|
|
126
145
|
name,
|
|
127
146
|
description: toolDef.description || '',
|
package/src/ui/input-dock.mjs
CHANGED
|
@@ -672,10 +672,13 @@ export function clearInputPrompt() {
|
|
|
672
672
|
return true;
|
|
673
673
|
}
|
|
674
674
|
|
|
675
|
-
export function renderDockInput(prefix, value, { context = '', tips = '', meta = '', cursor = null } = {}) {
|
|
675
|
+
export function renderDockInput(prefix, value, { context = '', tips = '', meta = '', cursor = null, fixedRows = null } = {}) {
|
|
676
676
|
if (!mounted) return false;
|
|
677
677
|
contentTrackingActive = false;
|
|
678
|
-
|
|
678
|
+
const requestedRows = fixedRows == null
|
|
679
|
+
? computeInputRowsForBuffer(prefix, value)
|
|
680
|
+
: Math.max(MIN_INPUT_ROWS, Math.min(inputRowsMax, Math.floor(Number(fixedRows) || MIN_INPUT_ROWS)));
|
|
681
|
+
setInputRowsTo(requestedRows);
|
|
679
682
|
renderFrame({ context, tips, meta, prefix, value, cursor, overlayLines: null });
|
|
680
683
|
const layout = layoutInput(prefix, value);
|
|
681
684
|
drawInputLines(layout.lines);
|