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 +9 -2
- package/src/bridge.js +100 -7
- package/src/cli.js +66 -5
- package/src/executor.js +37 -1
- package/src/index.js +10 -0
- package/src/uriScheme.js +146 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "promethios-bridge",
|
|
3
|
-
"version": "1.
|
|
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": [
|
|
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.
|
|
8
|
-
* 5. Executes tools locally and
|
|
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
|
-
//
|
|
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
|
|
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
|
|
7
|
-
* npx promethios-bridge --token <token>
|
|
8
|
-
* npx promethios-bridge --port 7823
|
|
9
|
-
* npx promethios-bridge --dev
|
|
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:
|
|
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 };
|
package/src/uriScheme.js
ADDED
|
@@ -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 };
|