promethios-bridge 1.0.0 → 1.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promethios-bridge",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Run Promethios agent frameworks locally on your computer with full file, terminal, and browser access.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -10,7 +10,14 @@
10
10
  "start": "node src/cli.js",
11
11
  "dev": "node src/cli.js --dev"
12
12
  },
13
- "keywords": ["promethios", "ai", "agent", "local", "bridge", "framework"],
13
+ "keywords": [
14
+ "promethios",
15
+ "ai",
16
+ "agent",
17
+ "local",
18
+ "bridge",
19
+ "framework"
20
+ ],
14
21
  "author": "Promethios <hello@promethios.ai>",
15
22
  "license": "MIT",
16
23
  "repository": {
package/src/bridge.js CHANGED
@@ -2,11 +2,17 @@
2
2
  * Promethios Local Bridge — Core
3
3
  *
4
4
  * 1. Authenticates with Promethios using a setup token
5
- * 2. Starts a local HTTP server on the configured port
5
+ * 2. Starts a local HTTP server on the configured port (for health checks)
6
6
  * 3. Registers the bridge endpoint with the Promethios API
7
- * 4. Listens for tool-call requests from the cloud orchestrator
8
- * 5. Executes tools locally and returns results
7
+ * 4. Polls the Promethios API for pending tool calls (Firestore relay)
8
+ * 5. Executes tools locally and posts results back to the API
9
9
  * 6. Sends heartbeats every 30s to keep the connection alive
10
+ *
11
+ * ── Why polling instead of direct HTTP? ─────────────────────────────────────
12
+ * The bridge only listens on 127.0.0.1 (localhost) for security — it is NOT
13
+ * reachable from the internet. Instead of requiring a tunnel, the bridge polls
14
+ * the Promethios API for pending tool calls queued by the cloud orchestrator.
15
+ * This works behind firewalls, NAT, and corporate proxies with no setup.
10
16
  */
11
17
 
12
18
  const express = require('express');
@@ -16,6 +22,7 @@ const fetch = require('node-fetch');
16
22
  const { executeLocalTool } = require('./executor');
17
23
 
18
24
  const HEARTBEAT_INTERVAL = 30_000; // 30s
25
+ const POLL_INTERVAL = 1_000; // 1s — poll for pending tool calls
19
26
 
20
27
  async function startBridge({ setupToken, apiBase, port, dev }) {
21
28
  const log = dev
@@ -40,17 +47,18 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
40
47
  process.exit(1);
41
48
  }
42
49
 
43
- // ── Step 2: Start local HTTP server ──────────────────────────────────────
50
+ // ── Step 2: Start local HTTP server (health check only) ──────────────────
44
51
  const app = express();
45
52
  app.use(express.json());
46
53
 
47
54
  // Health check
48
55
  app.get('/health', (req, res) => res.json({ ok: true, version: require('../package.json').version }));
49
56
 
50
- // Tool call endpoint called by the Promethios cloud relay
57
+ // Legacy direct tool-call endpoint (kept for backward compatibility with
58
+ // older backend versions that call callbackUrl/tool-call directly)
51
59
  app.post('/tool-call', async (req, res) => {
52
60
  const { toolName, args, frameworkId, callId } = req.body;
53
- log('Tool call received:', toolName, JSON.stringify(args).slice(0, 100));
61
+ log('Tool call received (direct):', toolName, JSON.stringify(args).slice(0, 100));
54
62
 
55
63
  try {
56
64
  const result = await executeLocalTool({ toolName, args, frameworkId, dev });
@@ -62,7 +70,7 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
62
70
  }
63
71
  });
64
72
 
65
- // Capability query — cloud can ask what this bridge supports
73
+ // Capability query
66
74
  app.get('/capabilities', (req, res) => {
67
75
  res.json({ capabilities: getSupportedCapabilities() });
68
76
  });
@@ -108,10 +116,48 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
108
116
  }
109
117
  }, HEARTBEAT_INTERVAL);
110
118
 
119
+ // ── Step 5: Poll for pending tool calls (Firestore relay) ─────────────────
120
+ // The cloud backend queues tool calls in Firestore when the agent uses
121
+ // local_shell, local_file_read, or local_file_write. We poll for them here,
122
+ // execute locally, and post results back. This works behind firewalls/NAT.
123
+ let pollActive = true;
124
+
125
+ const pollLoop = async () => {
126
+ while (pollActive) {
127
+ try {
128
+ const res = await fetch(`${apiBase}/api/local-bridge/pending-calls`, {
129
+ headers: { Authorization: `Bearer ${authToken}` },
130
+ });
131
+
132
+ if (res.ok) {
133
+ const { calls } = await res.json();
134
+ if (calls && calls.length > 0) {
135
+ log(`Got ${calls.length} pending tool call(s)`);
136
+ // Execute all pending calls concurrently
137
+ await Promise.all(calls.map(call => executePendingCall({ call, authToken, apiBase, dev })));
138
+ }
139
+ } else {
140
+ log('Pending-calls poll returned', res.status);
141
+ }
142
+ } catch (err) {
143
+ log('Poll error:', err.message);
144
+ }
145
+
146
+ // Wait before next poll
147
+ await sleep(POLL_INTERVAL);
148
+ }
149
+ };
150
+
151
+ // Start polling in background (don't await — runs until shutdown)
152
+ pollLoop().catch(err => {
153
+ console.error(chalk.red(' ✗ Poll loop crashed:'), err.message);
154
+ });
155
+
111
156
  // Graceful shutdown
112
157
  const shutdown = async (signal) => {
113
158
  console.log('');
114
159
  console.log(chalk.yellow(` Disconnecting (${signal})...`));
160
+ pollActive = false;
115
161
  clearInterval(heartbeatTimer);
116
162
  try {
117
163
  await fetch(`${apiBase}/api/local-bridge/unregister`, {
@@ -127,6 +173,49 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
127
173
  process.on('SIGTERM', () => shutdown('SIGTERM'));
128
174
  }
129
175
 
176
+ // ─────────────────────────────────────────────────────────────────────────────
177
+ // Execute a single pending tool call and post the result back
178
+ // ─────────────────────────────────────────────────────────────────────────────
179
+ async function executePendingCall({ call, authToken, apiBase, dev }) {
180
+ const log = dev ? (...a) => console.log(chalk.gray(' [debug]'), ...a) : () => {};
181
+ const { callId, toolName, args } = call;
182
+
183
+ log(`Executing pending call ${callId}: ${toolName}`);
184
+ console.log(chalk.cyan(` → Running ${toolName}...`));
185
+
186
+ let result, error;
187
+ try {
188
+ result = await executeLocalTool({ toolName, args, dev });
189
+ log(`Result for ${callId}:`, JSON.stringify(result).slice(0, 200));
190
+ console.log(chalk.green(` ✓ ${toolName} completed`));
191
+ } catch (err) {
192
+ error = err.message;
193
+ console.error(chalk.red(` ✗ ${toolName} failed:`), err.message);
194
+ }
195
+
196
+ // Post result back to the API
197
+ try {
198
+ const body = error ? { error } : { result };
199
+ const res = await fetch(`${apiBase}/api/local-bridge/tool-result/${callId}`, {
200
+ method: 'POST',
201
+ headers: {
202
+ Authorization: `Bearer ${authToken}`,
203
+ 'Content-Type': 'application/json',
204
+ },
205
+ body: JSON.stringify(body),
206
+ });
207
+
208
+ if (!res.ok) {
209
+ const text = await res.text();
210
+ log(`Failed to post result for ${callId}: ${res.status} ${text}`);
211
+ } else {
212
+ log(`Result posted for ${callId}`);
213
+ }
214
+ } catch (err) {
215
+ log(`Error posting result for ${callId}:`, err.message);
216
+ }
217
+ }
218
+
130
219
  // ─────────────────────────────────────────────────────────────────────────────
131
220
  // Auth: exchange setup token for a session bearer token
132
221
  // ─────────────────────────────────────────────────────────────────────────────
@@ -191,4 +280,8 @@ function getSupportedCapabilities() {
191
280
  ];
192
281
  }
193
282
 
283
+ function sleep(ms) {
284
+ return new Promise(resolve => setTimeout(resolve, ms));
285
+ }
286
+
194
287
  module.exports = { startBridge };
package/src/cli.js CHANGED
@@ -3,16 +3,24 @@
3
3
  * promethios-bridge CLI
4
4
  *
5
5
  * Usage:
6
- * npx promethios-bridge # Interactive setup
7
- * npx promethios-bridge --token <token> # Pre-authenticated (from Promethios UI)
8
- * npx promethios-bridge --port 7823 # Custom local port
9
- * npx promethios-bridge --dev # Dev mode (verbose logging)
6
+ * npx promethios-bridge # Interactive setup
7
+ * npx promethios-bridge --token <token> # Pre-authenticated (from Promethios UI)
8
+ * npx promethios-bridge --port 7823 # Custom local port
9
+ * npx promethios-bridge --dev # Dev mode (verbose logging)
10
+ * npx promethios-bridge --install # Register promethios:// URI scheme on this OS
11
+ * npx promethios-bridge --uri <uri> # Handle a promethios:// deep link (called by OS)
12
+ *
13
+ * One-click connect flow:
14
+ * 1. User clicks "Connect My Computer" in the Promethios UI
15
+ * 2. Browser opens promethios://connect?token=<token>&api=<url>
16
+ * 3. OS routes it to this CLI via the registered URI handler
17
+ * 4. CLI parses the URI, extracts token + api, and starts the bridge
10
18
  */
11
19
 
12
20
  const { program } = require('commander');
13
21
  const chalk = require('chalk');
14
- const ora = require('ora');
15
22
  const { startBridge } = require('./bridge');
23
+ const { registerUriScheme } = require('./uriScheme');
16
24
 
17
25
  const VERSION = require('../package.json').version;
18
26
 
@@ -29,10 +37,63 @@ program
29
37
  .option('--api <url>', 'Promethios API base URL', 'https://api.promethios.ai')
30
38
  .option('--port <port>', 'Local port for the bridge server', '7823')
31
39
  .option('--dev', 'Enable verbose debug logging')
40
+ .option('--install', 'Register the promethios:// URI scheme on this computer (run once after install)')
41
+ .option('--uri <uri>', 'Handle a promethios:// deep link URI (called automatically by the OS)')
32
42
  .parse(process.argv);
33
43
 
34
44
  const opts = program.opts();
35
45
 
46
+ // ── --install: register URI scheme on this OS ────────────────────────────────
47
+ if (opts.install) {
48
+ registerUriScheme()
49
+ .then(() => {
50
+ console.log(chalk.green(' ✓ promethios:// URI scheme registered'));
51
+ console.log(chalk.gray(' Clicking "Connect My Computer" in Promethios will now launch this app automatically.'));
52
+ console.log('');
53
+ process.exit(0);
54
+ })
55
+ .catch((err) => {
56
+ console.error(chalk.red(' ✗ Failed to register URI scheme:'), err.message);
57
+ console.log(chalk.yellow(' You can still connect by running: npx promethios-bridge --token <token>'));
58
+ process.exit(1);
59
+ });
60
+ return;
61
+ }
62
+
63
+ // ── --uri: handle a promethios:// deep link from the OS ──────────────────────
64
+ if (opts.uri) {
65
+ const uri = opts.uri;
66
+ let token, apiBase;
67
+
68
+ try {
69
+ // promethios://connect?token=<token>&api=<encoded-url>
70
+ const url = new URL(uri);
71
+ token = url.searchParams.get('token');
72
+ apiBase = url.searchParams.get('api')
73
+ ? decodeURIComponent(url.searchParams.get('api'))
74
+ : 'https://api.promethios.ai';
75
+
76
+ if (!token) throw new Error('No token in URI');
77
+ } catch (err) {
78
+ console.error(chalk.red(' ✗ Invalid deep link URI:'), err.message);
79
+ console.log(chalk.gray(' URI received:'), uri);
80
+ process.exit(1);
81
+ }
82
+
83
+ console.log(chalk.cyan(' → Connecting via deep link…'));
84
+ startBridge({
85
+ setupToken: token,
86
+ apiBase,
87
+ port: parseInt(opts.port, 10),
88
+ dev: !!opts.dev,
89
+ }).catch((err) => {
90
+ console.error(chalk.red('\n ✗ Bridge failed to start:'), err.message);
91
+ process.exit(1);
92
+ });
93
+ return;
94
+ }
95
+
96
+ // ── Standard start (--token or interactive) ──────────────────────────────────
36
97
  startBridge({
37
98
  setupToken: opts.token,
38
99
  apiBase: opts.api,
package/src/executor.js CHANGED
@@ -23,6 +23,41 @@ const execAsync = promisify(exec);
23
23
  async function executeLocalTool({ toolName, args, frameworkId, dev }) {
24
24
  const log = dev ? (...a) => console.log('[executor]', ...a) : () => {};
25
25
 
26
+ // ── Aliases for the new universal bridge tool names ─────────────────────
27
+ // The Promethios backend injects local_shell, local_file_read, local_file_write
28
+ // into the agent's tool list when the bridge is connected. Map them to the
29
+ // existing executor handlers.
30
+ if (toolName === 'local_shell') {
31
+ return executeLocalTool({ toolName: 'run_command', args: { command: args.command, cwd: args.cwd, timeout: args.timeout }, frameworkId, dev });
32
+ }
33
+ if (toolName === 'local_file_read') {
34
+ return executeLocalTool({ toolName: 'read_file', args: { path: args.path, encoding: args.encoding }, frameworkId, dev });
35
+ }
36
+ if (toolName === 'local_file_write') {
37
+ return executeLocalTool({ toolName: 'write_file', args: { path: args.path, content: args.content, encoding: args.encoding }, frameworkId, dev });
38
+ }
39
+
40
+ // ── local_execute is the built-in tool injected by the backend when the bridge
41
+ // is connected. It uses an `action` field to dispatch to the right handler.
42
+ if (toolName === 'local_execute') {
43
+ const action = args.action;
44
+ log('local_execute action:', action);
45
+ switch (action) {
46
+ case 'shell':
47
+ return executeLocalTool({ toolName: 'run_command', args: { command: args.command, cwd: args.cwd }, frameworkId, dev });
48
+ case 'read_file':
49
+ return executeLocalTool({ toolName: 'read_file', args: { path: args.path }, frameworkId, dev });
50
+ case 'write_file':
51
+ return executeLocalTool({ toolName: 'write_file', args: { path: args.path, content: args.content }, frameworkId, dev });
52
+ case 'list_dir':
53
+ return executeLocalTool({ toolName: 'list_directory', args: { path: args.path || '.' }, frameworkId, dev });
54
+ case 'open_browser':
55
+ return executeLocalTool({ toolName: 'open_browser', args: { url: args.url }, frameworkId, dev });
56
+ default:
57
+ throw new Error(`Unknown local_execute action: ${action}`);
58
+ }
59
+ }
60
+
26
61
  switch (toolName) {
27
62
  // ── Filesystem ────────────────────────────────────────────────────────
28
63
  case 'read_file': {
@@ -74,8 +109,9 @@ async function executeLocalTool({ toolName, args, frameworkId, dev }) {
74
109
  throw new Error('Command blocked: potentially destructive operation');
75
110
  }
76
111
  log('run_command', cmd);
112
+ const timeoutMs = args.timeout ? Math.min(args.timeout * 1000, 120_000) : 30_000;
77
113
  const { stdout, stderr } = await execAsync(cmd, {
78
- timeout: 30_000,
114
+ timeout: timeoutMs,
79
115
  cwd: args.cwd ? resolveSafePath(args.cwd) : process.env.HOME,
80
116
  maxBuffer: 1024 * 1024, // 1MB output limit
81
117
  });
package/src/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * promethios-bridge — programmatic API
3
+ *
4
+ * This file is the package's main entry point (for require('promethios-bridge')).
5
+ * The CLI is invoked via the 'bin' field in package.json.
6
+ */
7
+
8
+ const { startBridge } = require('./bridge');
9
+
10
+ module.exports = { startBridge };
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Promethios Local Bridge — URI Scheme Registration
3
+ *
4
+ * Registers the promethios:// custom URI scheme on the current OS so that
5
+ * clicking "Connect My Computer" in the browser auto-launches this CLI.
6
+ *
7
+ * Platform implementations:
8
+ * macOS — writes a LaunchServices .plist to ~/Library/LaunchAgents/
9
+ * Windows — writes a registry key under HKEY_CURRENT_USER\Software\Classes\promethios
10
+ * Linux — writes a .desktop file to ~/.local/share/applications/
11
+ *
12
+ * Run once after install: npx promethios-bridge --install
13
+ */
14
+
15
+ const fs = require('fs').promises;
16
+ const path = require('path');
17
+ const os = require('os');
18
+ const { execSync } = require('child_process');
19
+
20
+ const SCHEME = 'promethios';
21
+ const CLI_PATH = process.execPath; // path to the node binary running this script
22
+ const SCRIPT_PATH = path.resolve(__dirname, 'cli.js');
23
+
24
+ async function registerUriScheme() {
25
+ const platform = process.platform;
26
+
27
+ if (platform === 'darwin') {
28
+ return registerMacOS();
29
+ } else if (platform === 'win32') {
30
+ return registerWindows();
31
+ } else {
32
+ return registerLinux();
33
+ }
34
+ }
35
+
36
+ // ─────────────────────────────────────────────────────────────────────────────
37
+ // macOS — LaunchServices plist
38
+ // ─────────────────────────────────────────────────────────────────────────────
39
+ async function registerMacOS() {
40
+ const appDir = path.join(os.homedir(), 'Applications', 'Promethios Bridge.app');
41
+ const contentsDir = path.join(appDir, 'Contents');
42
+ const macOSDir = path.join(contentsDir, 'MacOS');
43
+ const resourcesDir = path.join(contentsDir, 'Resources');
44
+
45
+ await fs.mkdir(macOSDir, { recursive: true });
46
+ await fs.mkdir(resourcesDir, { recursive: true });
47
+
48
+ // Launcher script
49
+ const launcher = `#!/bin/bash
50
+ # Promethios Bridge URI handler
51
+ # Called by macOS when a promethios:// link is clicked
52
+ exec "${CLI_PATH}" "${SCRIPT_PATH}" --uri "$1"
53
+ `;
54
+ const launcherPath = path.join(macOSDir, 'promethios-bridge');
55
+ await fs.writeFile(launcherPath, launcher, { mode: 0o755 });
56
+
57
+ // Info.plist
58
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
59
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
60
+ <plist version="1.0">
61
+ <dict>
62
+ <key>CFBundleIdentifier</key>
63
+ <string>ai.promethios.bridge</string>
64
+ <key>CFBundleName</key>
65
+ <string>Promethios Bridge</string>
66
+ <key>CFBundleExecutable</key>
67
+ <string>promethios-bridge</string>
68
+ <key>CFBundleURLTypes</key>
69
+ <array>
70
+ <dict>
71
+ <key>CFBundleURLName</key>
72
+ <string>Promethios Bridge</string>
73
+ <key>CFBundleURLSchemes</key>
74
+ <array>
75
+ <string>${SCHEME}</string>
76
+ </array>
77
+ </dict>
78
+ </array>
79
+ <key>LSMinimumSystemVersion</key>
80
+ <string>10.13</string>
81
+ <key>NSHighResolutionCapable</key>
82
+ <true/>
83
+ </dict>
84
+ </plist>`;
85
+ await fs.writeFile(path.join(contentsDir, 'Info.plist'), plist);
86
+
87
+ // Register with LaunchServices
88
+ try {
89
+ execSync(`/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f "${appDir}"`, { stdio: 'ignore' });
90
+ } catch {
91
+ // lsregister may not be available on all macOS versions; the plist is still written
92
+ }
93
+ }
94
+
95
+ // ─────────────────────────────────────────────────────────────────────────────
96
+ // Windows — Registry
97
+ // ─────────────────────────────────────────────────────────────────────────────
98
+ async function registerWindows() {
99
+ // Use reg.exe to write to HKEY_CURRENT_USER (no admin required).
100
+ // We use `cmd /c npx promethios-bridge --uri "%1"` rather than hardcoding
101
+ // the node/script path, because npx cache paths are temporary and get
102
+ // cleaned up after the install run — leaving a broken registry entry.
103
+ // Using npx ensures the handler always resolves to the installed package.
104
+ const command = `cmd /c npx --yes promethios-bridge --uri "%1"`;
105
+
106
+ const regCommands = [
107
+ `reg add "HKEY_CURRENT_USER\\Software\\Classes\\${SCHEME}" /ve /d "URL:Promethios Bridge Protocol" /f`,
108
+ `reg add "HKEY_CURRENT_USER\\Software\\Classes\\${SCHEME}" /v "URL Protocol" /d "" /f`,
109
+ `reg add "HKEY_CURRENT_USER\\Software\\Classes\\${SCHEME}\\shell\\open\\command" /ve /d "${command}" /f`,
110
+ ];
111
+
112
+ for (const cmd of regCommands) {
113
+ execSync(cmd, { stdio: 'ignore' });
114
+ }
115
+ }
116
+
117
+ // ─────────────────────────────────────────────────────────────────────────────
118
+ // Linux — .desktop file + xdg-mime
119
+ // ─────────────────────────────────────────────────────────────────────────────
120
+ async function registerLinux() {
121
+ const desktopDir = path.join(os.homedir(), '.local', 'share', 'applications');
122
+ await fs.mkdir(desktopDir, { recursive: true });
123
+
124
+ const desktopFile = `[Desktop Entry]
125
+ Name=Promethios Bridge
126
+ Exec="${CLI_PATH}" "${SCRIPT_PATH}" --uri %u
127
+ Type=Application
128
+ Terminal=true
129
+ MimeType=x-scheme-handler/${SCHEME};
130
+ Categories=Development;
131
+ Comment=Connect your computer to Promethios AI agent frameworks
132
+ `;
133
+
134
+ const desktopPath = path.join(desktopDir, 'promethios-bridge.desktop');
135
+ await fs.writeFile(desktopPath, desktopFile, { mode: 0o644 });
136
+
137
+ // Register with xdg-mime
138
+ try {
139
+ execSync(`xdg-mime default promethios-bridge.desktop x-scheme-handler/${SCHEME}`, { stdio: 'ignore' });
140
+ execSync('update-desktop-database ~/.local/share/applications/ 2>/dev/null || true', { stdio: 'ignore' });
141
+ } catch {
142
+ // xdg-mime may not be available; .desktop file is still written
143
+ }
144
+ }
145
+
146
+ module.exports = { registerUriScheme };