@ottocode/server 0.1.177 → 0.1.179
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 +3 -3
- package/src/index.ts +7 -0
- package/src/routes/auth.ts +4 -1
- package/src/routes/files.ts +9 -8
- package/src/routes/root.ts +7 -0
- package/src/routes/setu.ts +2 -1
- package/src/routes/tunnel.ts +238 -0
- package/src/runtime/stream/error-handler.ts +1 -1
- package/src/state.ts +18 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.179",
|
|
4
4
|
"description": "HTTP API server for ottocode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"typecheck": "tsc --noEmit"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@ottocode/sdk": "0.1.
|
|
33
|
-
"@ottocode/database": "0.1.
|
|
32
|
+
"@ottocode/sdk": "0.1.179",
|
|
33
|
+
"@ottocode/database": "0.1.179",
|
|
34
34
|
"drizzle-orm": "^0.44.5",
|
|
35
35
|
"hono": "^4.9.9",
|
|
36
36
|
"zod": "^4.1.8"
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { registerResearchRoutes } from './routes/research.ts';
|
|
|
19
19
|
import { registerSessionApprovalRoute } from './routes/session-approval.ts';
|
|
20
20
|
import { registerSetuRoutes } from './routes/setu.ts';
|
|
21
21
|
import { registerAuthRoutes } from './routes/auth.ts';
|
|
22
|
+
import { registerTunnelRoutes } from './routes/tunnel.ts';
|
|
22
23
|
import type { AgentConfigEntry } from './runtime/agent/registry.ts';
|
|
23
24
|
|
|
24
25
|
const globalTerminalManager = new TerminalManager();
|
|
@@ -74,6 +75,7 @@ function initApp() {
|
|
|
74
75
|
registerResearchRoutes(app);
|
|
75
76
|
registerSetuRoutes(app);
|
|
76
77
|
registerAuthRoutes(app);
|
|
78
|
+
registerTunnelRoutes(app);
|
|
77
79
|
|
|
78
80
|
return app;
|
|
79
81
|
}
|
|
@@ -145,6 +147,7 @@ export function createStandaloneApp(_config?: StandaloneAppConfig) {
|
|
|
145
147
|
registerResearchRoutes(honoApp);
|
|
146
148
|
registerSetuRoutes(honoApp);
|
|
147
149
|
registerAuthRoutes(honoApp);
|
|
150
|
+
registerTunnelRoutes(honoApp);
|
|
148
151
|
|
|
149
152
|
return honoApp;
|
|
150
153
|
}
|
|
@@ -244,6 +247,7 @@ export function createEmbeddedApp(config: EmbeddedAppConfig = {}) {
|
|
|
244
247
|
registerResearchRoutes(honoApp);
|
|
245
248
|
registerSetuRoutes(honoApp);
|
|
246
249
|
registerAuthRoutes(honoApp);
|
|
250
|
+
registerTunnelRoutes(honoApp);
|
|
247
251
|
|
|
248
252
|
return honoApp;
|
|
249
253
|
}
|
|
@@ -279,3 +283,6 @@ export {
|
|
|
279
283
|
isTraceEnabled,
|
|
280
284
|
} from './runtime/debug/state.ts';
|
|
281
285
|
export { logger } from '@ottocode/sdk';
|
|
286
|
+
|
|
287
|
+
// Export server state management
|
|
288
|
+
export { setServerPort, getServerPort, getServerInfo } from './state.ts';
|
package/src/routes/auth.ts
CHANGED
|
@@ -529,7 +529,10 @@ export function registerAuthRoutes(app: Hono) {
|
|
|
529
529
|
if (!sessionId || !copilotDeviceSessions.has(sessionId)) {
|
|
530
530
|
return c.json({ error: 'Session expired or invalid' }, 400);
|
|
531
531
|
}
|
|
532
|
-
const session = copilotDeviceSessions.get(sessionId)
|
|
532
|
+
const session = copilotDeviceSessions.get(sessionId);
|
|
533
|
+
if (!session) {
|
|
534
|
+
return c.json({ error: 'Session expired or invalid' }, 400);
|
|
535
|
+
}
|
|
533
536
|
const result = await pollForCopilotTokenOnce(session.deviceCode);
|
|
534
537
|
if (result.status === 'complete') {
|
|
535
538
|
copilotDeviceSessions.delete(sessionId);
|
package/src/routes/files.ts
CHANGED
|
@@ -52,12 +52,7 @@ async function listFilesWithRg(
|
|
|
52
52
|
const rgBin = await resolveBinary('rg');
|
|
53
53
|
|
|
54
54
|
return new Promise((resolve) => {
|
|
55
|
-
const args = [
|
|
56
|
-
'--files',
|
|
57
|
-
'--hidden',
|
|
58
|
-
'--glob', '!.git/',
|
|
59
|
-
'--sort', 'path',
|
|
60
|
-
];
|
|
55
|
+
const args = ['--files', '--hidden', '--glob', '!.git/', '--sort', 'path'];
|
|
61
56
|
|
|
62
57
|
const proc = spawn(rgBin, args, { cwd: projectRoot });
|
|
63
58
|
let stdout = '';
|
|
@@ -73,7 +68,10 @@ async function listFilesWithRg(
|
|
|
73
68
|
|
|
74
69
|
proc.on('close', (code) => {
|
|
75
70
|
if (code !== 0 && code !== 1) {
|
|
76
|
-
logger.warn('rg --files failed, falling back', { stderr } as Record<
|
|
71
|
+
logger.warn('rg --files failed, falling back', { stderr } as Record<
|
|
72
|
+
string,
|
|
73
|
+
unknown
|
|
74
|
+
>);
|
|
77
75
|
resolve({ files: [], truncated: false });
|
|
78
76
|
return;
|
|
79
77
|
}
|
|
@@ -196,7 +194,10 @@ async function traverseDirectory(
|
|
|
196
194
|
}
|
|
197
195
|
}
|
|
198
196
|
} catch (err) {
|
|
199
|
-
logger.warn(
|
|
197
|
+
logger.warn(
|
|
198
|
+
`Failed to read directory ${dir}:`,
|
|
199
|
+
err as Record<string, unknown>,
|
|
200
|
+
);
|
|
200
201
|
}
|
|
201
202
|
|
|
202
203
|
return { files: collected, truncated: false };
|
package/src/routes/root.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
|
+
import { getServerInfo } from '../state.ts';
|
|
2
3
|
|
|
3
4
|
export function registerRootRoutes(app: Hono) {
|
|
4
5
|
app.get('/', (c) => c.text('otto server running'));
|
|
6
|
+
|
|
7
|
+
app.get('/v1/server/info', (c) => {
|
|
8
|
+
return c.json({
|
|
9
|
+
...getServerInfo(),
|
|
10
|
+
});
|
|
11
|
+
});
|
|
5
12
|
}
|
package/src/routes/setu.ts
CHANGED
|
@@ -19,7 +19,8 @@ import {
|
|
|
19
19
|
type TopupMethod,
|
|
20
20
|
} from '../runtime/topup/manager.ts';
|
|
21
21
|
|
|
22
|
-
const SETU_BASE_URL =
|
|
22
|
+
const SETU_BASE_URL =
|
|
23
|
+
process.env.SETU_BASE_URL || 'https://api.setu.ottocode.io';
|
|
23
24
|
|
|
24
25
|
function getSetuBaseUrl(): string {
|
|
25
26
|
return SETU_BASE_URL.endsWith('/')
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type { Hono } from 'hono';
|
|
2
|
+
import { streamSSE } from 'hono/streaming';
|
|
3
|
+
import {
|
|
4
|
+
OttoTunnel,
|
|
5
|
+
isTunnelBinaryInstalled,
|
|
6
|
+
generateQRCode,
|
|
7
|
+
killStaleTunnels,
|
|
8
|
+
logger,
|
|
9
|
+
} from '@ottocode/sdk';
|
|
10
|
+
import { getServerPort } from '../state.ts';
|
|
11
|
+
|
|
12
|
+
let activeTunnel: OttoTunnel | null = null;
|
|
13
|
+
let tunnelUrl: string | null = null;
|
|
14
|
+
let tunnelStatus: 'idle' | 'starting' | 'connected' | 'error' = 'idle';
|
|
15
|
+
let tunnelError: string | null = null;
|
|
16
|
+
let progressMessage: string | null = null;
|
|
17
|
+
|
|
18
|
+
export function registerTunnelRoutes(app: Hono) {
|
|
19
|
+
app.get('/v1/tunnel/status', async (c) => {
|
|
20
|
+
const binaryInstalled = await isTunnelBinaryInstalled();
|
|
21
|
+
|
|
22
|
+
return c.json({
|
|
23
|
+
status: tunnelStatus,
|
|
24
|
+
url: tunnelUrl,
|
|
25
|
+
error: tunnelError,
|
|
26
|
+
binaryInstalled,
|
|
27
|
+
isRunning: activeTunnel?.isRunning ?? false,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
app.post('/v1/tunnel/start', async (c) => {
|
|
32
|
+
if (activeTunnel?.isRunning) {
|
|
33
|
+
return c.json({
|
|
34
|
+
ok: true,
|
|
35
|
+
url: tunnelUrl,
|
|
36
|
+
message: 'Tunnel already running',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const body = await c.req.json().catch(() => ({}));
|
|
42
|
+
let port = body.port;
|
|
43
|
+
|
|
44
|
+
// Use server's known port if not explicitly provided
|
|
45
|
+
if (!port) {
|
|
46
|
+
port = getServerPort() || 9100;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
logger.debug('Starting tunnel on port:', port);
|
|
50
|
+
|
|
51
|
+
// Kill any stale tunnel processes first
|
|
52
|
+
await killStaleTunnels();
|
|
53
|
+
|
|
54
|
+
tunnelStatus = 'starting';
|
|
55
|
+
tunnelError = null;
|
|
56
|
+
progressMessage = 'Initializing...';
|
|
57
|
+
|
|
58
|
+
activeTunnel = new OttoTunnel();
|
|
59
|
+
|
|
60
|
+
const url = await activeTunnel.start(port, (msg) => {
|
|
61
|
+
progressMessage = msg;
|
|
62
|
+
logger.debug('Tunnel progress:', msg);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
tunnelUrl = url;
|
|
66
|
+
tunnelStatus = 'connected';
|
|
67
|
+
progressMessage = null;
|
|
68
|
+
|
|
69
|
+
activeTunnel.on('error', (err) => {
|
|
70
|
+
logger.error('Tunnel error:', err);
|
|
71
|
+
tunnelError = err.message;
|
|
72
|
+
tunnelStatus = 'error';
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
activeTunnel.on('exit', () => {
|
|
76
|
+
tunnelStatus = 'idle';
|
|
77
|
+
tunnelUrl = null;
|
|
78
|
+
activeTunnel = null;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return c.json({
|
|
82
|
+
ok: true,
|
|
83
|
+
url: tunnelUrl,
|
|
84
|
+
message: 'Tunnel started',
|
|
85
|
+
});
|
|
86
|
+
} catch (error) {
|
|
87
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
88
|
+
tunnelStatus = 'error';
|
|
89
|
+
tunnelError = message;
|
|
90
|
+
progressMessage = null;
|
|
91
|
+
|
|
92
|
+
logger.error('Failed to start tunnel:', error);
|
|
93
|
+
return c.json({ ok: false, error: message }, 500);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
app.post('/v1/tunnel/register', async (c) => {
|
|
98
|
+
try {
|
|
99
|
+
const body = await c.req.json().catch(() => ({}));
|
|
100
|
+
const { url } = body;
|
|
101
|
+
|
|
102
|
+
if (!url) {
|
|
103
|
+
return c.json({ ok: false, error: 'URL is required' }, 400);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
tunnelUrl = url;
|
|
107
|
+
tunnelStatus = 'connected';
|
|
108
|
+
tunnelError = null;
|
|
109
|
+
progressMessage = null;
|
|
110
|
+
|
|
111
|
+
logger.debug('External tunnel registered:', url);
|
|
112
|
+
|
|
113
|
+
return c.json({
|
|
114
|
+
ok: true,
|
|
115
|
+
url: tunnelUrl,
|
|
116
|
+
message: 'External tunnel registered',
|
|
117
|
+
});
|
|
118
|
+
} catch (error) {
|
|
119
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
120
|
+
logger.error('Failed to register external tunnel:', error);
|
|
121
|
+
return c.json({ ok: false, error: message }, 500);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
app.post('/v1/tunnel/stop', async (c) => {
|
|
126
|
+
if (!activeTunnel) {
|
|
127
|
+
return c.json({ ok: true, message: 'No tunnel running' });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
activeTunnel.stop();
|
|
132
|
+
activeTunnel = null;
|
|
133
|
+
tunnelUrl = null;
|
|
134
|
+
tunnelStatus = 'idle';
|
|
135
|
+
tunnelError = null;
|
|
136
|
+
|
|
137
|
+
return c.json({ ok: true, message: 'Tunnel stopped' });
|
|
138
|
+
} catch (error) {
|
|
139
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
140
|
+
return c.json({ ok: false, error: message }, 500);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
app.get('/v1/tunnel/qr', async (c) => {
|
|
145
|
+
if (!tunnelUrl) {
|
|
146
|
+
return c.json({ ok: false, error: 'No tunnel URL available' }, 400);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const qrCode = await generateQRCode(tunnelUrl);
|
|
151
|
+
return c.json({
|
|
152
|
+
ok: true,
|
|
153
|
+
url: tunnelUrl,
|
|
154
|
+
qrCode,
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
158
|
+
return c.json({ ok: false, error: message }, 500);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
app.get('/v1/tunnel/stream', async (c) => {
|
|
163
|
+
return streamSSE(c, async (stream) => {
|
|
164
|
+
const sendEvent = async (data: Record<string, unknown>) => {
|
|
165
|
+
try {
|
|
166
|
+
await stream.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
logger.error('SSE error writing event', error);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
await sendEvent({
|
|
173
|
+
type: 'status',
|
|
174
|
+
status: tunnelStatus,
|
|
175
|
+
url: tunnelUrl,
|
|
176
|
+
error: tunnelError,
|
|
177
|
+
progress: progressMessage,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const interval = setInterval(async () => {
|
|
181
|
+
await sendEvent({
|
|
182
|
+
type: 'status',
|
|
183
|
+
status: tunnelStatus,
|
|
184
|
+
url: tunnelUrl,
|
|
185
|
+
error: tunnelError,
|
|
186
|
+
progress: progressMessage,
|
|
187
|
+
});
|
|
188
|
+
}, 1000);
|
|
189
|
+
|
|
190
|
+
const onAbort = () => {
|
|
191
|
+
clearInterval(interval);
|
|
192
|
+
stream.close();
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
c.req.raw.signal.addEventListener('abort', onAbort, { once: true });
|
|
196
|
+
|
|
197
|
+
await new Promise<void>((resolve) => {
|
|
198
|
+
c.req.raw.signal.addEventListener('abort', () => resolve(), {
|
|
199
|
+
once: true,
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
clearInterval(interval);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function stopActiveTunnel() {
|
|
209
|
+
if (activeTunnel) {
|
|
210
|
+
activeTunnel.stop();
|
|
211
|
+
activeTunnel = null;
|
|
212
|
+
tunnelUrl = null;
|
|
213
|
+
tunnelStatus = 'idle';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function setExternalTunnel(tunnel: OttoTunnel, url: string) {
|
|
218
|
+
activeTunnel = tunnel;
|
|
219
|
+
tunnelUrl = url;
|
|
220
|
+
tunnelStatus = 'connected';
|
|
221
|
+
tunnelError = null;
|
|
222
|
+
progressMessage = null;
|
|
223
|
+
|
|
224
|
+
tunnel.on('error', (err) => {
|
|
225
|
+
tunnelError = err.message;
|
|
226
|
+
tunnelStatus = 'error';
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
tunnel.on('exit', () => {
|
|
230
|
+
tunnelStatus = 'idle';
|
|
231
|
+
tunnelUrl = null;
|
|
232
|
+
activeTunnel = null;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function getActiveTunnelUrl(): string | null {
|
|
237
|
+
return tunnelUrl;
|
|
238
|
+
}
|
|
@@ -52,7 +52,7 @@ export function createErrorHandler(
|
|
|
52
52
|
'';
|
|
53
53
|
|
|
54
54
|
// Also check error message for the exact fiat selection message
|
|
55
|
-
const
|
|
55
|
+
const _errorMessage =
|
|
56
56
|
(errObj?.message as string) ??
|
|
57
57
|
((errObj?.error as Record<string, unknown>)?.message as string) ??
|
|
58
58
|
((
|
package/src/state.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server state - tracks runtime information like the server's port
|
|
3
|
+
* This is the single source of truth for server configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
let serverPort: number | null = null;
|
|
7
|
+
|
|
8
|
+
export function setServerPort(port: number): void {
|
|
9
|
+
serverPort = port;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getServerPort(): number | null {
|
|
13
|
+
return serverPort;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getServerInfo(): { port: number | null } {
|
|
17
|
+
return { port: serverPort };
|
|
18
|
+
}
|