promethios-bridge 1.5.1 → 1.6.0
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/executor.js +239 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "promethios-bridge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Run Promethios agent frameworks locally on your computer with full file, terminal, browser access, and Native Framework Mode (run OpenClaw and other frameworks in their native interface via the bridge).",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/executor.js
CHANGED
|
@@ -15,11 +15,46 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
const fs = require('fs').promises;
|
|
18
|
+
const fsSync = require('fs');
|
|
18
19
|
const path = require('path');
|
|
20
|
+
const os = require('os');
|
|
19
21
|
const { execSync, exec } = require('child_process');
|
|
20
22
|
const { promisify } = require('util');
|
|
21
23
|
const execAsync = promisify(exec);
|
|
22
24
|
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
// Custom Tool Registry
|
|
27
|
+
// Tools the agent invents at runtime are stored here and persisted to disk.
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const CUSTOM_TOOLS_DIR = path.join(os.homedir(), '.promethios');
|
|
31
|
+
const CUSTOM_TOOLS_FILE = path.join(CUSTOM_TOOLS_DIR, 'custom_tools.json');
|
|
32
|
+
|
|
33
|
+
// In-memory registry: { [toolName]: { name, description, parameters, type, implementation, createdAt } }
|
|
34
|
+
if (!global.__customToolRegistry) {
|
|
35
|
+
global.__customToolRegistry = {};
|
|
36
|
+
// Load persisted tools on first require
|
|
37
|
+
try {
|
|
38
|
+
if (fsSync.existsSync(CUSTOM_TOOLS_FILE)) {
|
|
39
|
+
const saved = JSON.parse(fsSync.readFileSync(CUSTOM_TOOLS_FILE, 'utf8'));
|
|
40
|
+
Object.assign(global.__customToolRegistry, saved);
|
|
41
|
+
const count = Object.keys(saved).length;
|
|
42
|
+
if (count > 0) console.log(`[Bridge] Loaded ${count} custom tool(s) from ${CUSTOM_TOOLS_FILE}`);
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.warn('[Bridge] Could not load custom tools:', e.message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function saveCustomToolRegistry() {
|
|
50
|
+
try {
|
|
51
|
+
fsSync.mkdirSync(CUSTOM_TOOLS_DIR, { recursive: true });
|
|
52
|
+
fsSync.writeFileSync(CUSTOM_TOOLS_FILE, JSON.stringify(global.__customToolRegistry, null, 2), 'utf8');
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.warn('[Bridge] Could not save custom tools:', e.message);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
23
58
|
async function executeLocalTool({ toolName, args, frameworkId, dev }) {
|
|
24
59
|
const log = dev ? (...a) => console.log('[executor]', ...a) : () => {};
|
|
25
60
|
|
|
@@ -45,6 +80,79 @@ async function executeLocalTool({ toolName, args, frameworkId, dev }) {
|
|
|
45
80
|
if (toolName === 'local_browser_control') {
|
|
46
81
|
return executeLocalTool({ toolName: 'browser_control', args, frameworkId, dev });
|
|
47
82
|
}
|
|
83
|
+
if (toolName === 'local_create_folder') {
|
|
84
|
+
return executeLocalTool({ toolName: 'create_folder', args, frameworkId, dev });
|
|
85
|
+
}
|
|
86
|
+
if (toolName === 'local_move') {
|
|
87
|
+
return executeLocalTool({ toolName: 'move_path', args, frameworkId, dev });
|
|
88
|
+
}
|
|
89
|
+
if (toolName === 'local_copy') {
|
|
90
|
+
return executeLocalTool({ toolName: 'copy_path', args, frameworkId, dev });
|
|
91
|
+
}
|
|
92
|
+
if (toolName === 'local_delete') {
|
|
93
|
+
return executeLocalTool({ toolName: 'delete_path', args, frameworkId, dev });
|
|
94
|
+
}
|
|
95
|
+
if (toolName === 'local_define_tool') {
|
|
96
|
+
return executeLocalTool({ toolName: 'define_tool', args, frameworkId, dev });
|
|
97
|
+
}
|
|
98
|
+
if (toolName === 'local_list_tools') {
|
|
99
|
+
return executeLocalTool({ toolName: 'list_tools', args, frameworkId, dev });
|
|
100
|
+
}
|
|
101
|
+
if (toolName === 'local_remove_tool') {
|
|
102
|
+
return executeLocalTool({ toolName: 'remove_tool', args, frameworkId, dev });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Dynamic dispatch: check custom tool registry before the built-in switch ──
|
|
106
|
+
if (global.__customToolRegistry[toolName]) {
|
|
107
|
+
const toolDef = global.__customToolRegistry[toolName];
|
|
108
|
+
log(`Dispatching custom tool: ${toolName} (type=${toolDef.type})`);
|
|
109
|
+
|
|
110
|
+
if (toolDef.type === 'shell') {
|
|
111
|
+
// Level 1: shell template — substitute {param} placeholders with args values
|
|
112
|
+
let cmd = toolDef.implementation;
|
|
113
|
+
for (const [key, val] of Object.entries(args || {})) {
|
|
114
|
+
// Escape the value for shell safety (basic quoting)
|
|
115
|
+
const safe = String(val).replace(/"/g, '\\"');
|
|
116
|
+
cmd = cmd.replace(new RegExp(`\\{${key}\\}`, 'g'), safe);
|
|
117
|
+
}
|
|
118
|
+
log(`Custom shell tool cmd: ${cmd}`);
|
|
119
|
+
const timeoutMs = 30_000;
|
|
120
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
121
|
+
timeout: timeoutMs,
|
|
122
|
+
cwd: args.cwd ? resolveSafePath(args.cwd) : os.homedir(),
|
|
123
|
+
maxBuffer: 1024 * 1024,
|
|
124
|
+
});
|
|
125
|
+
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0, toolName };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (toolDef.type === 'js') {
|
|
129
|
+
// Level 2: JS function body — run in vm sandbox
|
|
130
|
+
const vm = require('vm');
|
|
131
|
+
const context = vm.createContext({
|
|
132
|
+
args: { ...args },
|
|
133
|
+
fetch,
|
|
134
|
+
require,
|
|
135
|
+
process: { env: process.env, platform: process.platform },
|
|
136
|
+
console,
|
|
137
|
+
Buffer,
|
|
138
|
+
URL,
|
|
139
|
+
URLSearchParams,
|
|
140
|
+
crypto: require('crypto'),
|
|
141
|
+
fs: require('fs').promises,
|
|
142
|
+
path: require('path'),
|
|
143
|
+
os: require('os'),
|
|
144
|
+
execAsync,
|
|
145
|
+
resolveSafePath,
|
|
146
|
+
result: undefined,
|
|
147
|
+
});
|
|
148
|
+
const script = new vm.Script(`(async () => { ${toolDef.implementation} })().then(r => { result = r; });`);
|
|
149
|
+
await script.runInContext(context);
|
|
150
|
+
await new Promise(r => setTimeout(r, 200));
|
|
151
|
+
return context.result !== undefined ? context.result : { success: true, toolName };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
throw new Error(`Custom tool "${toolName}" has unknown type: ${toolDef.type}. Expected 'shell' or 'js'.`);
|
|
155
|
+
}
|
|
48
156
|
|
|
49
157
|
// ── local_execute is the built-in tool injected by the backend when the bridge
|
|
50
158
|
// is connected. It uses an `action` field to dispatch to the right handler.
|
|
@@ -410,6 +518,68 @@ async function executeLocalTool({ toolName, args, frameworkId, dev }) {
|
|
|
410
518
|
};
|
|
411
519
|
}
|
|
412
520
|
|
|
521
|
+
// ── File management ────────────────────────────────────────────────────────────
|
|
522
|
+
case 'create_folder': {
|
|
523
|
+
const folderPath = resolveSafePath(args.path);
|
|
524
|
+
log('create_folder', folderPath);
|
|
525
|
+
await fs.mkdir(folderPath, { recursive: true });
|
|
526
|
+
return { success: true, path: folderPath, created: true };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
case 'move_path': {
|
|
530
|
+
const srcPath = resolveSafePath(args.src);
|
|
531
|
+
const dstPath = resolveSafePath(args.dst);
|
|
532
|
+
log('move_path', srcPath, '->', dstPath);
|
|
533
|
+
// Ensure destination parent directory exists
|
|
534
|
+
await fs.mkdir(path.dirname(dstPath), { recursive: true });
|
|
535
|
+
await fs.rename(srcPath, dstPath);
|
|
536
|
+
return { success: true, src: srcPath, dst: dstPath };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
case 'copy_path': {
|
|
540
|
+
const srcPath = resolveSafePath(args.src);
|
|
541
|
+
const dstPath = resolveSafePath(args.dst);
|
|
542
|
+
log('copy_path', srcPath, '->', dstPath);
|
|
543
|
+
// Ensure destination parent directory exists
|
|
544
|
+
await fs.mkdir(path.dirname(dstPath), { recursive: true });
|
|
545
|
+
// Check if source is a directory
|
|
546
|
+
const srcStat = await fs.stat(srcPath);
|
|
547
|
+
if (srcStat.isDirectory()) {
|
|
548
|
+
// Recursive directory copy
|
|
549
|
+
async function copyDir(src, dst) {
|
|
550
|
+
await fs.mkdir(dst, { recursive: true });
|
|
551
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
552
|
+
for (const entry of entries) {
|
|
553
|
+
const srcEntry = path.join(src, entry.name);
|
|
554
|
+
const dstEntry = path.join(dst, entry.name);
|
|
555
|
+
if (entry.isDirectory()) {
|
|
556
|
+
await copyDir(srcEntry, dstEntry);
|
|
557
|
+
} else {
|
|
558
|
+
await fs.copyFile(srcEntry, dstEntry);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
await copyDir(srcPath, dstPath);
|
|
563
|
+
return { success: true, src: srcPath, dst: dstPath, type: 'directory' };
|
|
564
|
+
} else {
|
|
565
|
+
await fs.copyFile(srcPath, dstPath);
|
|
566
|
+
return { success: true, src: srcPath, dst: dstPath, type: 'file' };
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
case 'delete_path': {
|
|
571
|
+
const targetPath = resolveSafePath(args.path);
|
|
572
|
+
log('delete_path', targetPath);
|
|
573
|
+
const targetStat = await fs.stat(targetPath);
|
|
574
|
+
if (targetStat.isDirectory()) {
|
|
575
|
+
await fs.rm(targetPath, { recursive: true, force: true });
|
|
576
|
+
return { success: true, path: targetPath, type: 'directory' };
|
|
577
|
+
} else {
|
|
578
|
+
await fs.unlink(targetPath);
|
|
579
|
+
return { success: true, path: targetPath, type: 'file' };
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
413
583
|
// ── Browser ────────────────────────────────────────────────────────────────────
|
|
414
584
|
case 'open_browser': {
|
|
415
585
|
const url = args.url;
|
|
@@ -553,6 +723,75 @@ async function executeLocalTool({ toolName, args, frameworkId, dev }) {
|
|
|
553
723
|
}
|
|
554
724
|
}
|
|
555
725
|
|
|
726
|
+
// ── Dynamic tool synthesis ───────────────────────────────────────────────────────────────
|
|
727
|
+
case 'define_tool': {
|
|
728
|
+
const { name: tName, description: tDesc, parameters: tParams, type: tType, implementation: tImpl } = args;
|
|
729
|
+
if (!tName) throw new Error('name is required');
|
|
730
|
+
if (!tType || !['shell', 'js'].includes(tType)) throw new Error('type must be "shell" or "js"');
|
|
731
|
+
if (!tImpl) throw new Error('implementation is required');
|
|
732
|
+
// Validate tool name: lowercase alphanumeric + underscores only
|
|
733
|
+
if (!/^[a-z][a-z0-9_]*$/.test(tName)) {
|
|
734
|
+
throw new Error(`Tool name "${tName}" is invalid. Use lowercase letters, numbers, and underscores only (must start with a letter).`);
|
|
735
|
+
}
|
|
736
|
+
// Prevent overwriting built-in tools
|
|
737
|
+
const RESERVED = ['local_shell','local_file_read','local_file_write','local_file_read_binary',
|
|
738
|
+
'local_file_upload_to_thread','local_browser_control','local_create_folder','local_move',
|
|
739
|
+
'local_copy','local_delete','local_define_tool','local_list_tools','local_remove_tool'];
|
|
740
|
+
if (RESERVED.includes(tName)) throw new Error(`Cannot redefine built-in tool: ${tName}`);
|
|
741
|
+
|
|
742
|
+
const toolEntry = {
|
|
743
|
+
name: tName,
|
|
744
|
+
description: tDesc || '',
|
|
745
|
+
parameters: tParams || { type: 'object', properties: {}, required: [] },
|
|
746
|
+
type: tType,
|
|
747
|
+
implementation: tImpl,
|
|
748
|
+
createdAt: new Date().toISOString(),
|
|
749
|
+
};
|
|
750
|
+
global.__customToolRegistry[tName] = toolEntry;
|
|
751
|
+
saveCustomToolRegistry();
|
|
752
|
+
log(`Defined custom tool: ${tName} (type=${tType})`);
|
|
753
|
+
return {
|
|
754
|
+
success: true,
|
|
755
|
+
toolName: tName,
|
|
756
|
+
type: tType,
|
|
757
|
+
message: `Tool "${tName}" registered successfully. You can now call it directly by name.`,
|
|
758
|
+
totalCustomTools: Object.keys(global.__customToolRegistry).length,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
case 'list_tools': {
|
|
763
|
+
const tools = Object.values(global.__customToolRegistry);
|
|
764
|
+
if (tools.length === 0) {
|
|
765
|
+
return { tools: [], message: 'No custom tools defined yet. Use local_define_tool to create one.' };
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
tools: tools.map(t => ({
|
|
769
|
+
name: t.name,
|
|
770
|
+
description: t.description,
|
|
771
|
+
type: t.type,
|
|
772
|
+
parameters: t.parameters,
|
|
773
|
+
createdAt: t.createdAt,
|
|
774
|
+
})),
|
|
775
|
+
count: tools.length,
|
|
776
|
+
message: `${tools.length} custom tool(s) available: ${tools.map(t => t.name).join(', ')}`,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
case 'remove_tool': {
|
|
781
|
+
const { name: rName } = args;
|
|
782
|
+
if (!rName) throw new Error('name is required');
|
|
783
|
+
if (!global.__customToolRegistry[rName]) {
|
|
784
|
+
return { success: false, message: `Tool "${rName}" not found in custom registry.` };
|
|
785
|
+
}
|
|
786
|
+
delete global.__customToolRegistry[rName];
|
|
787
|
+
saveCustomToolRegistry();
|
|
788
|
+
return {
|
|
789
|
+
success: true,
|
|
790
|
+
message: `Tool "${rName}" removed from registry.`,
|
|
791
|
+
totalCustomTools: Object.keys(global.__customToolRegistry).length,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
556
795
|
// ── Custom tool (developer-written code from framework definition) ────
|
|
557
796
|
case 'custom':
|
|
558
797
|
default: {
|