spudmobile-bridge 1.0.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/appicon.png +0 -0
- package/dist/autostart.d.ts +3 -0
- package/dist/autostart.js +234 -0
- package/dist/codex.d.ts +32 -0
- package/dist/codex.js +106 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +48 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +263 -0
- package/dist/pairing.d.ts +14 -0
- package/dist/pairing.js +137 -0
- package/dist/supabase.d.ts +96 -0
- package/dist/supabase.js +362 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import { getConfig, saveConfig } from './config.js';
|
|
5
|
+
import { SupabaseBridge } from './supabase.js';
|
|
6
|
+
import { findCodexCLI, runCodex } from './codex.js';
|
|
7
|
+
import { startPairingServer } from './pairing.js';
|
|
8
|
+
import { installAutostart, uninstallAutostart, isAutostartInstalled } from './autostart.js';
|
|
9
|
+
// ─── State ────────────────────────────────────────────────
|
|
10
|
+
let isFirstMessage = true;
|
|
11
|
+
// ─── CLI ──────────────────────────────────────────────────
|
|
12
|
+
program
|
|
13
|
+
.name('spudmobile-bridge')
|
|
14
|
+
.description('Bridge your mobile app with OpenAI Codex CLI')
|
|
15
|
+
.version('1.0.0')
|
|
16
|
+
.option('-p, --path <dir>', 'Project directory for Codex to work in')
|
|
17
|
+
.option('--port <number>', 'Pairing server port', '38474')
|
|
18
|
+
.option('--install', 'Install auto-launch (start on login)')
|
|
19
|
+
.option('--uninstall', 'Remove auto-launch')
|
|
20
|
+
.parse();
|
|
21
|
+
const opts = program.opts();
|
|
22
|
+
// Handle install/uninstall commands
|
|
23
|
+
if (opts.install) {
|
|
24
|
+
installAutostart(opts.path);
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
if (opts.uninstall) {
|
|
28
|
+
uninstallAutostart();
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
const config = getConfig({ path: opts.path, port: parseInt(opts.port) });
|
|
32
|
+
// ─── State ────────────────────────────────────────────────
|
|
33
|
+
let cliPath = null;
|
|
34
|
+
let bridge;
|
|
35
|
+
let activeProcess = null;
|
|
36
|
+
let activeMessageId = null;
|
|
37
|
+
let pairingServer = null;
|
|
38
|
+
// ─── Main ─────────────────────────────────────────────────
|
|
39
|
+
async function main() {
|
|
40
|
+
console.log('');
|
|
41
|
+
console.log('╔══════════════════════════════════════════╗');
|
|
42
|
+
console.log('║ SpudMobile Bridge v1.0 ║');
|
|
43
|
+
console.log('║ OpenAI Codex CLI ↔ iOS ║');
|
|
44
|
+
console.log('╚══════════════════════════════════════════╝');
|
|
45
|
+
console.log('');
|
|
46
|
+
// 1. Find Codex CLI
|
|
47
|
+
cliPath = findCodexCLI();
|
|
48
|
+
if (!cliPath) {
|
|
49
|
+
console.error('❌ Codex CLI not found!');
|
|
50
|
+
console.error(' Install it: npm i -g @openai/codex');
|
|
51
|
+
console.error(' Or visit: https://developers.openai.com/codex/cli');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
console.log(`✅ Codex CLI found: ${cliPath}`);
|
|
55
|
+
// 2. Init Supabase bridge
|
|
56
|
+
bridge = new SupabaseBridge(config);
|
|
57
|
+
// 3. Handle pairing
|
|
58
|
+
let needsPairing = !config.pairId;
|
|
59
|
+
// Verify saved pair is still active
|
|
60
|
+
if (config.pairId) {
|
|
61
|
+
const isActive = await bridge.isPairActive(config.pairId);
|
|
62
|
+
if (isActive) {
|
|
63
|
+
console.log(`✅ Using saved pair: ${config.pairId}`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
console.log('⚠️ Saved pair is no longer active. Re-pairing...');
|
|
67
|
+
saveConfig({ pairId: undefined, pairCode: undefined });
|
|
68
|
+
config.pairId = null;
|
|
69
|
+
config.pairCode = null;
|
|
70
|
+
bridge.pairId = null;
|
|
71
|
+
needsPairing = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (needsPairing) {
|
|
75
|
+
await waitForPairing();
|
|
76
|
+
// Auto-install background service after first successful pairing
|
|
77
|
+
if (!isAutostartInstalled()) {
|
|
78
|
+
console.log('');
|
|
79
|
+
installAutostart(config.projectPath || undefined);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Start operations with current pair
|
|
83
|
+
await startOperations();
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(`📁 Project: ${config.projectPath}`);
|
|
86
|
+
console.log('🤖 Waiting for messages from mobile app...');
|
|
87
|
+
console.log(' Press Ctrl+C to stop.\n');
|
|
88
|
+
// Always keep pairing server running in background for re-pairing
|
|
89
|
+
ensurePairingServerRunning();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Wait for initial pairing to complete (one-time, closes after pair)
|
|
93
|
+
*/
|
|
94
|
+
async function waitForPairing() {
|
|
95
|
+
console.log('🔗 No saved pair found. Starting pairing server...');
|
|
96
|
+
const { server, paired } = startPairingServer(config.port, async (pairCode) => {
|
|
97
|
+
console.log(`🔑 Received pair code: ${pairCode}`);
|
|
98
|
+
const pairId = await bridge.activatePair(pairCode);
|
|
99
|
+
console.log(`✅ Paired successfully! ID: ${pairId}`);
|
|
100
|
+
}, false); // one-time mode — closes after first pair
|
|
101
|
+
pairingServer = server;
|
|
102
|
+
await paired;
|
|
103
|
+
pairingServer = null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Start persistent pairing server for re-connections.
|
|
107
|
+
* Server stays alive and handles unlimited re-pairings.
|
|
108
|
+
*/
|
|
109
|
+
function ensurePairingServerRunning() {
|
|
110
|
+
if (pairingServer)
|
|
111
|
+
return;
|
|
112
|
+
const { server } = startPairingServer(config.port, async (pairCode) => {
|
|
113
|
+
console.log(`\n🔑 Re-pair request received: ${pairCode}`);
|
|
114
|
+
// End old session
|
|
115
|
+
await bridge.endSession();
|
|
116
|
+
isFirstMessage = true;
|
|
117
|
+
// Activate new pair
|
|
118
|
+
const pairId = await bridge.activatePair(pairCode);
|
|
119
|
+
console.log(`✅ Re-paired successfully! ID: ${pairId}`);
|
|
120
|
+
// Start fresh operations
|
|
121
|
+
await startOperations();
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log(`📁 Project: ${config.projectPath}`);
|
|
124
|
+
console.log('🤖 Waiting for messages from mobile app...\n');
|
|
125
|
+
}, true); // persistent mode — keeps running
|
|
126
|
+
pairingServer = server;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Start/restart all bridge operations (session, presence, subscriptions)
|
|
130
|
+
*/
|
|
131
|
+
async function startOperations() {
|
|
132
|
+
// Reuse active session or create new one
|
|
133
|
+
const session = await bridge.ensureSession();
|
|
134
|
+
// If session was resumed, continue Codex conversation (not fresh start)
|
|
135
|
+
if (session.resumed) {
|
|
136
|
+
isFirstMessage = false;
|
|
137
|
+
}
|
|
138
|
+
// Start presence heartbeat
|
|
139
|
+
bridge.startPresenceHeartbeat();
|
|
140
|
+
// Process any pending messages
|
|
141
|
+
const pending = await bridge.getPendingMessages();
|
|
142
|
+
if (pending.length > 0) {
|
|
143
|
+
console.log(`📬 Found ${pending.length} pending message(s)`);
|
|
144
|
+
for (const msg of pending) {
|
|
145
|
+
await processMessage(msg);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Subscribe to new messages
|
|
149
|
+
bridge.subscribeToMessages(async (message) => {
|
|
150
|
+
await processMessage(message);
|
|
151
|
+
}, (messageId) => {
|
|
152
|
+
handleCancellation(messageId);
|
|
153
|
+
});
|
|
154
|
+
// Listen for session changes (iOS "New Session" button)
|
|
155
|
+
bridge.subscribeToSessionChange(() => {
|
|
156
|
+
isFirstMessage = true;
|
|
157
|
+
console.log('🔄 Session reset — next message starts fresh Codex conversation');
|
|
158
|
+
});
|
|
159
|
+
// Listen for disconnect from mobile app
|
|
160
|
+
bridge.subscribeToDisconnect(async () => {
|
|
161
|
+
console.log('\n🔌 Disconnected by mobile app!');
|
|
162
|
+
console.log(' Waiting for re-connection...\n');
|
|
163
|
+
// End current session
|
|
164
|
+
await bridge.endSession();
|
|
165
|
+
isFirstMessage = true;
|
|
166
|
+
// Clear saved config
|
|
167
|
+
saveConfig({ pairId: undefined, pairCode: undefined });
|
|
168
|
+
config.pairId = null;
|
|
169
|
+
config.pairCode = null;
|
|
170
|
+
bridge.pairId = null;
|
|
171
|
+
// Pairing server is already running in background,
|
|
172
|
+
// so user can just tap Connect again on iOS
|
|
173
|
+
ensurePairingServerRunning();
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// ─── Process Message ──────────────────────────────────────
|
|
177
|
+
async function processMessage(message) {
|
|
178
|
+
const prompt = message.content;
|
|
179
|
+
const model = message.model;
|
|
180
|
+
console.log(`\n📩 New message: "${prompt.substring(0, 80)}${prompt.length > 80 ? '...' : ''}"`);
|
|
181
|
+
if (model)
|
|
182
|
+
console.log(` Model: ${model}`);
|
|
183
|
+
// Sync session (in case iOS created new session)
|
|
184
|
+
const oldSessionId = bridge.sessionId;
|
|
185
|
+
await bridge.refreshSessionId();
|
|
186
|
+
if (bridge.sessionId !== oldSessionId) {
|
|
187
|
+
isFirstMessage = true;
|
|
188
|
+
console.log('🔄 Session changed — starting fresh Codex conversation');
|
|
189
|
+
}
|
|
190
|
+
// Mark as processing
|
|
191
|
+
await bridge.markAsProcessing(message.id);
|
|
192
|
+
activeMessageId = message.id;
|
|
193
|
+
// Run Codex CLI
|
|
194
|
+
const { process: proc, result } = runCodex(prompt, cliPath, {
|
|
195
|
+
model: mapModel(model) || undefined,
|
|
196
|
+
cwd: config.projectPath || undefined,
|
|
197
|
+
continueSession: !isFirstMessage,
|
|
198
|
+
},
|
|
199
|
+
// Streaming callback
|
|
200
|
+
async (chunk, fullOutput) => {
|
|
201
|
+
// For streaming we just update, but for simplicity
|
|
202
|
+
// we'll send the final result once complete
|
|
203
|
+
});
|
|
204
|
+
isFirstMessage = false;
|
|
205
|
+
activeProcess = proc;
|
|
206
|
+
// Wait for result
|
|
207
|
+
const codexResult = await result;
|
|
208
|
+
activeProcess = null;
|
|
209
|
+
activeMessageId = null;
|
|
210
|
+
if (codexResult.killed) {
|
|
211
|
+
console.log('⏱️ Timed out');
|
|
212
|
+
await bridge.sendReply('⚠️ Request timed out after 10 minutes.', message.id, model, 'error');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// Send reply
|
|
216
|
+
const status = codexResult.exitCode === 0 ? 'completed' : 'error';
|
|
217
|
+
await bridge.sendReply(codexResult.output, message.id, model, status);
|
|
218
|
+
console.log(`✅ Reply sent (${codexResult.output.length} chars, status: ${status})`);
|
|
219
|
+
}
|
|
220
|
+
// ─── Cancellation ─────────────────────────────────────────
|
|
221
|
+
function handleCancellation(messageId) {
|
|
222
|
+
if (activeMessageId === messageId && activeProcess) {
|
|
223
|
+
console.log('🚫 Cancellation received — killing Codex process');
|
|
224
|
+
activeProcess.kill('SIGTERM');
|
|
225
|
+
activeProcess = null;
|
|
226
|
+
activeMessageId = null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// ─── Model Mapping ────────────────────────────────────────
|
|
230
|
+
function mapModel(model) {
|
|
231
|
+
if (!model || model.toLowerCase() === 'default')
|
|
232
|
+
return undefined;
|
|
233
|
+
// Map iOS model names to Codex CLI model flags
|
|
234
|
+
const mapping = {
|
|
235
|
+
'gpt-5.4': 'gpt-5.4',
|
|
236
|
+
'gpt-5.3-codex': 'gpt-5.3-codex',
|
|
237
|
+
'gpt-5.3-codex-spark': 'gpt-5.3-codex-spark',
|
|
238
|
+
'codex': 'gpt-5.3-codex',
|
|
239
|
+
'spark': 'gpt-5.3-codex-spark',
|
|
240
|
+
};
|
|
241
|
+
return mapping[model.toLowerCase()] || model;
|
|
242
|
+
}
|
|
243
|
+
// ─── Graceful Shutdown ────────────────────────────────────
|
|
244
|
+
async function shutdown() {
|
|
245
|
+
console.log('\n🛑 Shutting down...');
|
|
246
|
+
if (activeProcess) {
|
|
247
|
+
activeProcess.kill('SIGTERM');
|
|
248
|
+
}
|
|
249
|
+
if (bridge) {
|
|
250
|
+
await bridge.cleanup();
|
|
251
|
+
}
|
|
252
|
+
if (pairingServer) {
|
|
253
|
+
pairingServer.close();
|
|
254
|
+
}
|
|
255
|
+
process.exit(0);
|
|
256
|
+
}
|
|
257
|
+
process.on('SIGINT', shutdown);
|
|
258
|
+
process.on('SIGTERM', shutdown);
|
|
259
|
+
// ─── Run ──────────────────────────────────────────────────
|
|
260
|
+
main().catch((err) => {
|
|
261
|
+
console.error('💥 Fatal error:', err.message);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Server } from 'http';
|
|
2
|
+
/**
|
|
3
|
+
* Start a local HTTP server to receive pairing callbacks from the web page.
|
|
4
|
+
* The `connect` edge function redirects to http://127.0.0.1:38473/callback?token=PAIR_CODE
|
|
5
|
+
*
|
|
6
|
+
* @param port Port to listen on (default 38473)
|
|
7
|
+
* @param onPairCode Called when a valid pair code is received
|
|
8
|
+
* @param persistent If true, server stays running after pairing (for re-connections)
|
|
9
|
+
* @returns Server instance and a promise that resolves when pairing succeeds
|
|
10
|
+
*/
|
|
11
|
+
export declare function startPairingServer(port: number, onPairCode: (code: string) => Promise<void>, persistent?: boolean): {
|
|
12
|
+
server: Server;
|
|
13
|
+
paired: Promise<void>;
|
|
14
|
+
};
|
package/dist/pairing.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
/**
|
|
7
|
+
* Start a local HTTP server to receive pairing callbacks from the web page.
|
|
8
|
+
* The `connect` edge function redirects to http://127.0.0.1:38473/callback?token=PAIR_CODE
|
|
9
|
+
*
|
|
10
|
+
* @param port Port to listen on (default 38473)
|
|
11
|
+
* @param onPairCode Called when a valid pair code is received
|
|
12
|
+
* @param persistent If true, server stays running after pairing (for re-connections)
|
|
13
|
+
* @returns Server instance and a promise that resolves when pairing succeeds
|
|
14
|
+
*/
|
|
15
|
+
export function startPairingServer(port, onPairCode, persistent = false) {
|
|
16
|
+
let resolvePaired;
|
|
17
|
+
const paired = new Promise((resolve) => {
|
|
18
|
+
resolvePaired = resolve;
|
|
19
|
+
});
|
|
20
|
+
const server = createServer(async (req, res) => {
|
|
21
|
+
// CORS headers for browser requests
|
|
22
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
23
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
24
|
+
if (req.method === 'OPTIONS') {
|
|
25
|
+
res.writeHead(204);
|
|
26
|
+
res.end();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const url = new URL(req.url || '/', `http://localhost:${port}`);
|
|
30
|
+
if (url.pathname === '/callback') {
|
|
31
|
+
const token = url.searchParams.get('token');
|
|
32
|
+
if (!token) {
|
|
33
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
34
|
+
res.end('<h1>Missing token</h1>');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
await onPairCode(token);
|
|
39
|
+
// Send success response
|
|
40
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
41
|
+
res.end(`
|
|
42
|
+
<!DOCTYPE html>
|
|
43
|
+
<html>
|
|
44
|
+
<head>
|
|
45
|
+
<title>Connected!</title>
|
|
46
|
+
<meta charset="UTF-8">
|
|
47
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
48
|
+
<style>
|
|
49
|
+
body { font-family: -apple-system, system-ui, sans-serif; background: #0a0a0b; color: white; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
|
|
50
|
+
.container { text-align: center; }
|
|
51
|
+
.icon { width: 80px; height: 80px; margin-bottom: 20px; opacity: 0.85; }
|
|
52
|
+
h1 { font-size: 24px; margin-bottom: 8px; font-weight: 700; }
|
|
53
|
+
p { color: #71717a; font-size: 15px; line-height: 1.5; }
|
|
54
|
+
</style>
|
|
55
|
+
</head>
|
|
56
|
+
<body>
|
|
57
|
+
<div class="container">
|
|
58
|
+
<img class="icon" src="/icon" alt="App Icon" />
|
|
59
|
+
<h1>Connected!</h1>
|
|
60
|
+
<p>You can close this tab and return to your mobile app.</p>
|
|
61
|
+
</div>
|
|
62
|
+
</body>
|
|
63
|
+
</html>`);
|
|
64
|
+
if (!persistent) {
|
|
65
|
+
// Close server after successful pairing (one-time mode)
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
server.close();
|
|
68
|
+
resolvePaired();
|
|
69
|
+
}, 500);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// In persistent mode, just resolve the promise but keep server running
|
|
73
|
+
resolvePaired();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
78
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
79
|
+
res.end(`<h1>Pairing failed</h1><p>${message}</p>`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (url.pathname === '/icon') {
|
|
83
|
+
// Serve the app icon
|
|
84
|
+
try {
|
|
85
|
+
const iconPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'appicon.png');
|
|
86
|
+
const icon = readFileSync(iconPath);
|
|
87
|
+
res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=3600' });
|
|
88
|
+
res.end(icon);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
res.writeHead(404);
|
|
92
|
+
res.end();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else if (url.pathname === '/health') {
|
|
96
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
97
|
+
res.end(JSON.stringify({ status: 'waiting_for_pair' }));
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
res.writeHead(404);
|
|
101
|
+
res.end('Not found');
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
server.on('error', (err) => {
|
|
105
|
+
if (err.code === 'EADDRINUSE') {
|
|
106
|
+
if (persistent) {
|
|
107
|
+
// Already paired, just skip the pairing server
|
|
108
|
+
console.log(`\n⚠️ Port ${port} is already in use (another bridge may be running).`);
|
|
109
|
+
console.log(' Pairing server skipped — bridge continues working.\n');
|
|
110
|
+
resolvePaired();
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// Need to pair! Kill the existing process on port and retry
|
|
114
|
+
console.log(`\n⚠️ Port ${port} is busy. Freeing port...`);
|
|
115
|
+
try {
|
|
116
|
+
execSync(`lsof -ti:${port} | xargs kill -9 2>/dev/null`, { stdio: 'ignore' });
|
|
117
|
+
}
|
|
118
|
+
catch { /* ignore */ }
|
|
119
|
+
// Retry after a short delay
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
server.listen(port, '127.0.0.1', () => {
|
|
122
|
+
console.log(`\n🔗 Pairing server ready at http://127.0.0.1:${port}`);
|
|
123
|
+
console.log(' 👉 Now go to the mobile app and tap "Connect" on the setup page.\n');
|
|
124
|
+
});
|
|
125
|
+
}, 1000);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
console.error('❌ Pairing server error:', err.message);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
server.listen(port, '127.0.0.1', () => {
|
|
133
|
+
console.log(`\n🔗 Pairing server ready at http://127.0.0.1:${port}`);
|
|
134
|
+
console.log(' 👉 Now go to the mobile app and tap "Connect" on the setup page.\n');
|
|
135
|
+
});
|
|
136
|
+
return { server, paired };
|
|
137
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { BridgeConfig } from './config.js';
|
|
2
|
+
export interface Message {
|
|
3
|
+
id: string;
|
|
4
|
+
created_at: string;
|
|
5
|
+
role: 'user' | 'agent' | 'system';
|
|
6
|
+
content: string;
|
|
7
|
+
status: 'pending' | 'processing' | 'streaming' | 'completed' | 'error';
|
|
8
|
+
session_id: string | null;
|
|
9
|
+
model: string | null;
|
|
10
|
+
user_id: string | null;
|
|
11
|
+
client_type: string;
|
|
12
|
+
parent_message_id: string | null;
|
|
13
|
+
pair_id: string | null;
|
|
14
|
+
sender: string | null;
|
|
15
|
+
processed_by_mac_at: string | null;
|
|
16
|
+
is_cancelled: boolean;
|
|
17
|
+
cancelled_at: string | null;
|
|
18
|
+
}
|
|
19
|
+
export declare class SupabaseBridge {
|
|
20
|
+
private config;
|
|
21
|
+
private client;
|
|
22
|
+
private messageChannel;
|
|
23
|
+
private presenceInterval;
|
|
24
|
+
private pollingInterval;
|
|
25
|
+
private processedMessageIds;
|
|
26
|
+
private deviceId;
|
|
27
|
+
pairId: string | null;
|
|
28
|
+
sessionId: string | null;
|
|
29
|
+
constructor(config: BridgeConfig);
|
|
30
|
+
/**
|
|
31
|
+
* Create a new session, deactivating any old sessions for this pair
|
|
32
|
+
*/
|
|
33
|
+
createSession(): Promise<string>;
|
|
34
|
+
/**
|
|
35
|
+
* Reuse existing active session or create a new one.
|
|
36
|
+
* This preserves chat history across bridge restarts.
|
|
37
|
+
* Returns session id and project_name (if available).
|
|
38
|
+
*/
|
|
39
|
+
ensureSession(): Promise<{
|
|
40
|
+
id: string;
|
|
41
|
+
projectName: string | null;
|
|
42
|
+
resumed: boolean;
|
|
43
|
+
}>;
|
|
44
|
+
/**
|
|
45
|
+
* End the current session
|
|
46
|
+
*/
|
|
47
|
+
endSession(): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Subscribe to session changes (iOS creates new session)
|
|
50
|
+
*/
|
|
51
|
+
subscribeToSessionChange(onNewSession: (sessionId: string) => void): void;
|
|
52
|
+
/**
|
|
53
|
+
* Refresh sessionId from Supabase (polling fallback for session changes)
|
|
54
|
+
*/
|
|
55
|
+
refreshSessionId(): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Check if a pair is still active in Supabase
|
|
58
|
+
*/
|
|
59
|
+
isPairActive(pairId: string): Promise<boolean>;
|
|
60
|
+
/**
|
|
61
|
+
* Activate a device pair using the pair code from callback
|
|
62
|
+
*/
|
|
63
|
+
activatePair(pairCode: string): Promise<string>;
|
|
64
|
+
/**
|
|
65
|
+
* Subscribe to new user messages for processing
|
|
66
|
+
*/
|
|
67
|
+
subscribeToMessages(onNewMessage: (message: Message) => void, onCancellation: (messageId: string) => void): void;
|
|
68
|
+
/**
|
|
69
|
+
* Subscribe to disconnect events from mobile app
|
|
70
|
+
*/
|
|
71
|
+
subscribeToDisconnect(onDisconnect: () => void): void;
|
|
72
|
+
/**
|
|
73
|
+
* Mark message as being processed
|
|
74
|
+
*/
|
|
75
|
+
markAsProcessing(messageId: string): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Send a reply message from the bridge (agent)
|
|
78
|
+
*/
|
|
79
|
+
sendReply(content: string, parentMessageId: string, model: string | null, status?: 'streaming' | 'completed' | 'error'): Promise<string | null>;
|
|
80
|
+
/**
|
|
81
|
+
* Update an existing reply (for streaming updates)
|
|
82
|
+
*/
|
|
83
|
+
updateReply(replyId: string, content: string, status?: 'streaming' | 'completed' | 'error'): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Start heartbeat to ide_presence table
|
|
86
|
+
*/
|
|
87
|
+
startPresenceHeartbeat(): void;
|
|
88
|
+
/**
|
|
89
|
+
* Check if there are pending messages to process (on startup or polling)
|
|
90
|
+
*/
|
|
91
|
+
getPendingMessages(): Promise<Message[]>;
|
|
92
|
+
/**
|
|
93
|
+
* Clean up on shutdown
|
|
94
|
+
*/
|
|
95
|
+
cleanup(): Promise<void>;
|
|
96
|
+
}
|