@aexol/spectral 0.9.6 → 0.9.8
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/dist/commands/serve.d.ts +7 -11
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +163 -116
- package/dist/relay/auto-research.d.ts.map +1 -1
- package/dist/relay/auto-research.js +4 -1
- package/dist/relay/client.d.ts +3 -0
- package/dist/relay/client.d.ts.map +1 -1
- package/dist/relay/client.js +22 -0
- package/dist/relay/dispatcher.d.ts +1 -1
- package/dist/relay/dispatcher.d.ts.map +1 -1
- package/dist/relay/dispatcher.js +9 -0
- package/dist/server/handlers/paths-pick-directory.d.ts +33 -0
- package/dist/server/handlers/paths-pick-directory.d.ts.map +1 -0
- package/dist/server/handlers/paths-pick-directory.js +180 -0
- package/dist/server/session-stream.d.ts +3 -1
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +24 -19
- package/package.json +1 -1
package/dist/commands/serve.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `spectral serve` — relay orchestration entry point.
|
|
3
3
|
*
|
|
4
|
-
* Architecture
|
|
4
|
+
* Architecture:
|
|
5
5
|
* 1. Pre-flight: `requireLogin()` — same UX as the main spawn path.
|
|
6
6
|
* 2. Pre-flight: `preflightSqlite()` — fail fast on a broken native module.
|
|
7
7
|
* 3. Open the SQLite store and construct a `SessionStreamManager`.
|
|
@@ -9,18 +9,14 @@
|
|
|
9
9
|
* which either reuses a fresh `machine.json` or POSTs to
|
|
10
10
|
* `/api/machines/register` and persists the issued JWT.
|
|
11
11
|
* 5. Open a single long-lived `RelayClient` to `/agent-connection` carrying
|
|
12
|
-
* the machine JWT. Reconnects forever with backoff.
|
|
13
|
-
*
|
|
14
|
-
* `
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* the machine JWT. Reconnects forever with backoff. When the JWT is
|
|
13
|
+
* rejected (HTTP 401/403 on WS upgrade), the relay emits `auth-failed`
|
|
14
|
+
* and `serve.ts` triggers a re-registration attempt. If re-registration
|
|
15
|
+
* succeeds, a fresh relay is spawned with the new JWT. If it fails (e.g.
|
|
16
|
+
* team API key rotated), the process exits with a clear message.
|
|
17
|
+
* 6. SIGINT/SIGTERM trigger graceful shutdown: dispose the relay (close 1000),
|
|
17
18
|
* dispose the manager (tear down agent streams), close the store.
|
|
18
19
|
*
|
|
19
|
-
* What this module is NOT (yet):
|
|
20
|
-
* - It does NOT translate relay envelopes into REST handler calls or WS
|
|
21
|
-
* event broadcasts. That dispatcher is Batch 3.
|
|
22
|
-
* - It does NOT bind any local port. The local HTTP server is gone.
|
|
23
|
-
*
|
|
24
20
|
* Flag surface:
|
|
25
21
|
* - `--machine-name <name>` — display name used at registration time.
|
|
26
22
|
* - `--port <n>` / `-p <n>` / `SPECTRAL_PORT` — accepted but warned-and-ignored
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAsBH,OAAO,EAAE,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AASlE,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGnE,OAAO,EAAmB,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAerE;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAazD;AAED,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,CA6B9D;AAED,MAAM,WAAW,eAAe;IAC9B,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,uCAAuC;IACvC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4CAA4C;IAC5C,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,qBAAqB,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;CAC/E;AAED,MAAM,WAAW,cAAc;IAC7B,qCAAqC;IACrC,KAAK,EAAE,YAAY,CAAC;IACpB,wDAAwD;IACxD,OAAO,EAAE,oBAAoB,CAAC;IAC9B,4CAA4C;IAC5C,KAAK,EAAE,WAAW,CAAC;IACnB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,QAAQ,CAAC,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CAqWlF;AAiED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAa5E"}
|
package/dist/commands/serve.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `spectral serve` — relay orchestration entry point.
|
|
3
3
|
*
|
|
4
|
-
* Architecture
|
|
4
|
+
* Architecture:
|
|
5
5
|
* 1. Pre-flight: `requireLogin()` — same UX as the main spawn path.
|
|
6
6
|
* 2. Pre-flight: `preflightSqlite()` — fail fast on a broken native module.
|
|
7
7
|
* 3. Open the SQLite store and construct a `SessionStreamManager`.
|
|
@@ -9,18 +9,14 @@
|
|
|
9
9
|
* which either reuses a fresh `machine.json` or POSTs to
|
|
10
10
|
* `/api/machines/register` and persists the issued JWT.
|
|
11
11
|
* 5. Open a single long-lived `RelayClient` to `/agent-connection` carrying
|
|
12
|
-
* the machine JWT. Reconnects forever with backoff.
|
|
13
|
-
*
|
|
14
|
-
* `
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* the machine JWT. Reconnects forever with backoff. When the JWT is
|
|
13
|
+
* rejected (HTTP 401/403 on WS upgrade), the relay emits `auth-failed`
|
|
14
|
+
* and `serve.ts` triggers a re-registration attempt. If re-registration
|
|
15
|
+
* succeeds, a fresh relay is spawned with the new JWT. If it fails (e.g.
|
|
16
|
+
* team API key rotated), the process exits with a clear message.
|
|
17
|
+
* 6. SIGINT/SIGTERM trigger graceful shutdown: dispose the relay (close 1000),
|
|
17
18
|
* dispose the manager (tear down agent streams), close the store.
|
|
18
19
|
*
|
|
19
|
-
* What this module is NOT (yet):
|
|
20
|
-
* - It does NOT translate relay envelopes into REST handler calls or WS
|
|
21
|
-
* event broadcasts. That dispatcher is Batch 3.
|
|
22
|
-
* - It does NOT bind any local port. The local HTTP server is gone.
|
|
23
|
-
*
|
|
24
20
|
* Flag surface:
|
|
25
21
|
* - `--machine-name <name>` — display name used at registration time.
|
|
26
22
|
* - `--port <n>` / `-p <n>` / `SPECTRAL_PORT` — accepted but warned-and-ignored
|
|
@@ -191,26 +187,6 @@ export async function runServe(opts = {}) {
|
|
|
191
187
|
process.stdout.write(`${action} machine "${registration.record.machineName}" (${registration.record.machineId})\n`);
|
|
192
188
|
process.stdout.write(`Connecting to relay: ${relayUrl} (cwd: ${cwd}, host: ${hostname()})\n`);
|
|
193
189
|
}
|
|
194
|
-
const relay = new RelayClient({
|
|
195
|
-
relayUrl,
|
|
196
|
-
machineJwt: registration.record.machineJwt,
|
|
197
|
-
backendUrl,
|
|
198
|
-
webSocketImpl: opts.webSocketImpl,
|
|
199
|
-
fetchImpl: opts.fetchImpl,
|
|
200
|
-
logger: silent ? { log: () => { }, warn: () => { }, error: () => { } } : console,
|
|
201
|
-
});
|
|
202
|
-
// Wire the meta publisher now that we have both the relay socket and
|
|
203
|
-
// the canonical machineId. The backend ignores the body's machineId
|
|
204
|
-
// field on `meta_event` (it stamps it from the connection identity),
|
|
205
|
-
// but we still send the right value for local debuggability and to
|
|
206
|
-
// avoid relying on backend behaviour that may evolve.
|
|
207
|
-
publishMetaEventRef = (event) => {
|
|
208
|
-
relay.send({
|
|
209
|
-
kind: "meta_event",
|
|
210
|
-
machineId: registration.record.machineId,
|
|
211
|
-
event,
|
|
212
|
-
});
|
|
213
|
-
};
|
|
214
190
|
// Per-session subscriber registry. The dispatcher attaches at most
|
|
215
191
|
// one subscriber per (sessionId) on the first `client_message`; we
|
|
216
192
|
// hand the same map back to it for every subsequent frame so attach
|
|
@@ -219,98 +195,169 @@ export async function runServe(opts = {}) {
|
|
|
219
195
|
// KEEP the underlying streams alive — a browser reconnect should
|
|
220
196
|
// resume mid-turn.
|
|
221
197
|
const subscribers = new Map();
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
void handleRestRequest(frame, {
|
|
236
|
-
store,
|
|
237
|
-
manager,
|
|
238
|
-
cwd,
|
|
239
|
-
publishMetaEvent: deferredPublishMetaEvent,
|
|
240
|
-
mcpState: getLiveMcpState() ?? undefined,
|
|
241
|
-
}).then((response) => {
|
|
242
|
-
relay.send(response);
|
|
243
|
-
});
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
if (frame.kind === "client_message") {
|
|
247
|
-
handleClientMessage(frame, {
|
|
248
|
-
manager,
|
|
249
|
-
relay,
|
|
250
|
-
subscribers,
|
|
198
|
+
let relay;
|
|
199
|
+
/** Wire event handlers on a (possibly new) RelayClient instance. */
|
|
200
|
+
function wireRelay(r) {
|
|
201
|
+
// Wire the meta publisher now that we have both the relay socket and
|
|
202
|
+
// the canonical machineId. The backend ignores the body's machineId
|
|
203
|
+
// field on `meta_event` (it stamps it from the connection identity),
|
|
204
|
+
// but we still send the right value for local debuggability and to
|
|
205
|
+
// avoid relying on backend behaviour that may evolve.
|
|
206
|
+
publishMetaEventRef = (event) => {
|
|
207
|
+
r.send({
|
|
208
|
+
kind: "meta_event",
|
|
209
|
+
machineId: registration.record.machineId,
|
|
210
|
+
event,
|
|
251
211
|
});
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
212
|
+
};
|
|
213
|
+
// Envelope dispatcher. We only act on `rest_request` and
|
|
214
|
+
// `client_message`; everything else (welcome, pong, error, etc.) is
|
|
215
|
+
// either handled by RelayClient itself or is informational.
|
|
216
|
+
//
|
|
217
|
+
// Frames arrive typed loosely from `RelayClient` (which is protocol-
|
|
218
|
+
// agnostic — it just forwards JSON). We re-narrow via the `kind`
|
|
219
|
+
// discriminator and cast to the strict envelope types from
|
|
220
|
+
// `@aexol/relay-protocol`. The protocol package's own `parseFrame()`
|
|
221
|
+
// already validated `kind` membership in RelayClient itself; here we
|
|
222
|
+
// additionally trust that the inner shape matches its discriminator
|
|
223
|
+
// (the backend is the only producer and is exercised by tests).
|
|
224
|
+
r.on("frame", (frame) => {
|
|
225
|
+
if (frame.kind === "rest_request") {
|
|
226
|
+
void handleRestRequest(frame, {
|
|
227
|
+
store,
|
|
228
|
+
manager,
|
|
229
|
+
cwd,
|
|
230
|
+
publishMetaEvent: deferredPublishMetaEvent,
|
|
231
|
+
mcpState: getLiveMcpState() ?? undefined,
|
|
232
|
+
}).then((response) => {
|
|
233
|
+
r.send(response);
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (frame.kind === "client_message") {
|
|
238
|
+
handleClientMessage(frame, {
|
|
239
|
+
manager,
|
|
240
|
+
relay: r,
|
|
241
|
+
subscribers,
|
|
242
|
+
});
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (frame.kind === "subscribe") {
|
|
246
|
+
handleSubscribe(frame, {
|
|
247
|
+
manager,
|
|
248
|
+
relay: r,
|
|
249
|
+
subscribers,
|
|
250
|
+
});
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (frame.kind === "cancel_turn") {
|
|
254
|
+
handleCancelTurn(frame, { manager });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (frame.kind === "auto_research") {
|
|
258
|
+
handleAutoResearchFrame(frame, {
|
|
259
|
+
store,
|
|
260
|
+
manager,
|
|
261
|
+
relay: r,
|
|
262
|
+
subscribers,
|
|
263
|
+
cwd,
|
|
264
|
+
});
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (frame.kind === "config_patch") {
|
|
268
|
+
handleConfigPatch(frame, {
|
|
269
|
+
cwd,
|
|
270
|
+
agentDir: getAgentDir(),
|
|
271
|
+
relay: r,
|
|
272
|
+
machineId: registration.record.machineId,
|
|
273
|
+
});
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
// Other frames (error, machine_disconnected addressed to us, etc.)
|
|
277
|
+
// are ignored at this layer. Future batches may surface them in
|
|
278
|
+
// structured logs.
|
|
279
|
+
});
|
|
280
|
+
// On every relay close, drop the subscriber registry. The manager
|
|
281
|
+
// keeps streams alive (so `dispose` is the only place that tears
|
|
282
|
+
// them down), but the previous subscribers are pointing at a stale
|
|
283
|
+
// socket — let the next `client_message` attach a fresh one.
|
|
284
|
+
r.on("close", () => {
|
|
285
|
+
detachAllSubscribers(manager, subscribers);
|
|
286
|
+
});
|
|
287
|
+
// Send native extensions info when the relay connects
|
|
288
|
+
r.on("welcome", () => {
|
|
289
|
+
if (!silent)
|
|
290
|
+
process.stdout.write("✓ Relay connected\n");
|
|
291
|
+
broadcastNativeExtensionsInfo(cwd, r, registration.record.machineId);
|
|
292
|
+
});
|
|
293
|
+
if (!silent) {
|
|
294
|
+
r.on("close", ({ code, reason }) => {
|
|
295
|
+
process.stdout.write(`Relay closed (${code}${reason ? `: ${reason}` : ""}); will reconnect…\n`);
|
|
259
296
|
});
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (frame.kind === "cancel_turn") {
|
|
263
|
-
handleCancelTurn(frame, { manager });
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
if (frame.kind === "auto_research") {
|
|
267
|
-
handleAutoResearchFrame(frame, {
|
|
268
|
-
store,
|
|
269
|
-
manager,
|
|
270
|
-
relay,
|
|
271
|
-
subscribers,
|
|
272
|
-
cwd,
|
|
297
|
+
r.on("reconnect-scheduled", ({ delayMs, attempt }) => {
|
|
298
|
+
process.stdout.write(`Reconnect attempt ${attempt} in ${delayMs}ms\n`);
|
|
273
299
|
});
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
handleConfigPatch(frame, {
|
|
278
|
-
cwd,
|
|
279
|
-
agentDir: getAgentDir(),
|
|
280
|
-
relay,
|
|
281
|
-
machineId: registration.record.machineId,
|
|
300
|
+
r.on("error", (err) => {
|
|
301
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
302
|
+
process.stderr.write(`Relay error: ${msg}\n`);
|
|
282
303
|
});
|
|
283
|
-
return;
|
|
284
304
|
}
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
305
|
+
// When the relay reports an auth failure (HTTP 401/403 on WS upgrade),
|
|
306
|
+
// the machine JWT is invalid. Attempt re-registration with the original
|
|
307
|
+
// credentials to obtain a fresh JWT, then restart the relay. If
|
|
308
|
+
// re-registration also fails (e.g. the team API key was rotated), exit
|
|
309
|
+
// with a clear message — the user needs to re-run `spectral login`.
|
|
310
|
+
r.on("auth-failed", () => {
|
|
311
|
+
if (!silent) {
|
|
312
|
+
process.stderr.write("Relay auth failed — machine JWT rejected. Re-registering…\n");
|
|
313
|
+
}
|
|
314
|
+
void (async () => {
|
|
315
|
+
try {
|
|
316
|
+
const newReg = await ensureMachineRegistered({
|
|
317
|
+
backendUrl,
|
|
318
|
+
apiKey: cfg.teamApiKey,
|
|
319
|
+
userJwt: cfg.userJwt,
|
|
320
|
+
machineNameOverride: opts.machineName,
|
|
321
|
+
version,
|
|
322
|
+
fetchImpl: opts.fetchImpl,
|
|
323
|
+
});
|
|
324
|
+
registration = newReg;
|
|
325
|
+
// Dispose the old relay and create a fresh one with the new JWT.
|
|
326
|
+
r.dispose();
|
|
327
|
+
relay = new RelayClient({
|
|
328
|
+
relayUrl,
|
|
329
|
+
machineJwt: newReg.record.machineJwt,
|
|
330
|
+
backendUrl,
|
|
331
|
+
webSocketImpl: opts.webSocketImpl,
|
|
332
|
+
fetchImpl: opts.fetchImpl,
|
|
333
|
+
logger: silent
|
|
334
|
+
? { log: () => { }, warn: () => { }, error: () => { } }
|
|
335
|
+
: console,
|
|
336
|
+
});
|
|
337
|
+
wireRelay(relay);
|
|
338
|
+
relay.connect();
|
|
339
|
+
if (!silent) {
|
|
340
|
+
process.stdout.write(`Re-registered machine "${newReg.record.machineName}" (${newReg.record.machineId}); reconnecting…\n`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
345
|
+
process.stderr.write(`✗ Re-registration failed: ${msg}\n`);
|
|
346
|
+
process.stderr.write(" Run `spectral login` if your team API key has been rotated.\n");
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
})();
|
|
312
350
|
});
|
|
313
351
|
}
|
|
352
|
+
relay = new RelayClient({
|
|
353
|
+
relayUrl,
|
|
354
|
+
machineJwt: registration.record.machineJwt,
|
|
355
|
+
backendUrl,
|
|
356
|
+
webSocketImpl: opts.webSocketImpl,
|
|
357
|
+
fetchImpl: opts.fetchImpl,
|
|
358
|
+
logger: silent ? { log: () => { }, warn: () => { }, error: () => { } } : console,
|
|
359
|
+
});
|
|
360
|
+
wireRelay(relay);
|
|
314
361
|
relay.connect();
|
|
315
362
|
let closed = false;
|
|
316
363
|
const close = async () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auto-research.d.ts","sourceRoot":"","sources":["../../src/relay/auto-research.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAEpF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAM/C,oDAAoD;AACpD,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,+CAA+C;AAC/C,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,YAAY,CAAC;IACpB,OAAO,EAAE,oBAAoB,CAAC;IAC9B,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IACjC;0DACsD;IACtD,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACrC,6EAA6E;IAC7E,GAAG,EAAE,MAAM,CAAC;IACZ,kDAAkD;IAClD,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;KAAE,CAAC;CAClD;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAksBD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,gBAAgB,GACrB,IAAI,
|
|
1
|
+
{"version":3,"file":"auto-research.d.ts","sourceRoot":"","sources":["../../src/relay/auto-research.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAEpF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAM/C,oDAAoD;AACpD,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,+CAA+C;AAC/C,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,YAAY,CAAC;IACpB,OAAO,EAAE,oBAAoB,CAAC;IAC9B,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IACjC;0DACsD;IACtD,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACrC,6EAA6E;IAC7E,GAAG,EAAE,MAAM,CAAC;IACZ,kDAAkD;IAClD,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;KAAE,CAAC;CAClD;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAksBD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,gBAAgB,GACrB,IAAI,CAsLN"}
|
|
@@ -840,7 +840,10 @@ export function handleAutoResearch(input, deps) {
|
|
|
840
840
|
// gated by watcherFired, so it's a no-op. The only issue is we don't
|
|
841
841
|
// clearTimeout on watcher fire — but that's fine, the timer is harmless.
|
|
842
842
|
// --- Send the prompt through the existing AgentBridge (backend proxy) ---
|
|
843
|
-
|
|
843
|
+
// skipPersistence: true — the auto-research task is an internal system
|
|
844
|
+
// instruction, not a user message. It should NOT appear in SQLite or
|
|
845
|
+
// be broadcast as a user_message_appended event.
|
|
846
|
+
manager.prompt(sessionId, taskContent, storedModelId, undefined, undefined, { skipPersistence: true }).catch((err) => {
|
|
844
847
|
if (watcherFired)
|
|
845
848
|
return; // already handled by watcher
|
|
846
849
|
const msg = err instanceof Error ? err.message : String(err);
|
package/dist/relay/client.d.ts
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
* - `close` - socket closed (we may reconnect after)
|
|
35
35
|
* - `error` - non-fatal error (parse failure, send failure)
|
|
36
36
|
* - `reconnect-scheduled` - emitted with the delay in ms before each retry
|
|
37
|
+
* - `auth-failed` - WS upgrade rejected with 401/403 (caller should re-register)
|
|
37
38
|
*
|
|
38
39
|
* Threading model: `node:events` is single-threaded; all callbacks fire on
|
|
39
40
|
* the same event loop turn. Listeners must be cheap.
|
|
@@ -91,6 +92,8 @@ export declare class RelayClient extends EventEmitter {
|
|
|
91
92
|
private reconnectAttempt;
|
|
92
93
|
private reconnectTimer;
|
|
93
94
|
private sendQueue;
|
|
95
|
+
/** Set true when the WS error handler sees an HTTP 401/403 upgrade rejection. */
|
|
96
|
+
private authFailed;
|
|
94
97
|
/** Timestamp of last received frame / ping — drives the watchdog. */
|
|
95
98
|
private lastActivityMs;
|
|
96
99
|
private watchdogTimer;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/relay/client.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/relay/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,SAAS,MAAM,IAAI,CAAC;AAqB3B,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,SAAS,CAAC;IACjC;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB;;OAEG;IACH,MAAM,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;IACjD;;;;OAIG;IACH,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,KAAK,CAAC;CAChC;AAED,6EAA6E;AAC7E,MAAM,WAAW,UAAU;IACzB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,qBAAa,WAAY,SAAQ,YAAY;IAC3C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAChD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAmB;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0C;IACjE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA0B;IAE/C,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,SAAS,CAAgB;IACjC,iFAAiF;IACjF,OAAO,CAAC,UAAU,CAAS;IAE3B,qEAAqE;IACrE,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,aAAa,CAA+B;gBAExC,IAAI,EAAE,kBAAkB;IAWpC,kEAAkE;IAClE,OAAO,IAAI,IAAI;IAMf;;;;;;;OAOG;IACH,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO;IAqBzC;;;OAGG;IACH,OAAO,IAAI,IAAI;IA2Bf,OAAO,CAAC,UAAU;IAsIlB,OAAO,CAAC,aAAa;IAsBrB,OAAO,CAAC,YAAY;IAOpB;;;;;OAKG;YACW,WAAW;YA8BX,iBAAiB;CAmDhC"}
|
package/dist/relay/client.js
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
* - `close` - socket closed (we may reconnect after)
|
|
35
35
|
* - `error` - non-fatal error (parse failure, send failure)
|
|
36
36
|
* - `reconnect-scheduled` - emitted with the delay in ms before each retry
|
|
37
|
+
* - `auth-failed` - WS upgrade rejected with 401/403 (caller should re-register)
|
|
37
38
|
*
|
|
38
39
|
* Threading model: `node:events` is single-threaded; all callbacks fire on
|
|
39
40
|
* the same event loop turn. Listeners must be cheap.
|
|
@@ -71,6 +72,8 @@ export class RelayClient extends EventEmitter {
|
|
|
71
72
|
reconnectAttempt = 0;
|
|
72
73
|
reconnectTimer = null;
|
|
73
74
|
sendQueue = [];
|
|
75
|
+
/** Set true when the WS error handler sees an HTTP 401/403 upgrade rejection. */
|
|
76
|
+
authFailed = false;
|
|
74
77
|
/** Timestamp of last received frame / ping — drives the watchdog. */
|
|
75
78
|
lastActivityMs = 0;
|
|
76
79
|
watchdogTimer = null;
|
|
@@ -154,6 +157,7 @@ export class RelayClient extends EventEmitter {
|
|
|
154
157
|
// Internals
|
|
155
158
|
// -------------------------------------------------------------------------
|
|
156
159
|
openSocket() {
|
|
160
|
+
this.authFailed = false;
|
|
157
161
|
const ws = new this.WS(this.relayUrl, {
|
|
158
162
|
headers: { Authorization: `Bearer ${this.machineJwt}` },
|
|
159
163
|
});
|
|
@@ -209,6 +213,15 @@ export class RelayClient extends EventEmitter {
|
|
|
209
213
|
this.emit("frame", parsed);
|
|
210
214
|
});
|
|
211
215
|
ws.on("error", (err) => {
|
|
216
|
+
// Detect HTTP auth failures during the WS upgrade handshake.
|
|
217
|
+
// The `ws` library surfaces these as `Error: Unexpected server
|
|
218
|
+
// response: 401` (or 403). Mark `authFailed` so the close handler
|
|
219
|
+
// signals `auth-failed` instead of entering the reconnect loop.
|
|
220
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
221
|
+
if (msg.includes("Unexpected server response: 401") ||
|
|
222
|
+
msg.includes("Unexpected server response: 403")) {
|
|
223
|
+
this.authFailed = true;
|
|
224
|
+
}
|
|
212
225
|
// `ws` emits 'error' before 'close'; we don't reconnect here — the
|
|
213
226
|
// 'close' handler is the single reconnect entry point so we don't
|
|
214
227
|
// double-schedule.
|
|
@@ -245,6 +258,15 @@ export class RelayClient extends EventEmitter {
|
|
|
245
258
|
this.exit(1);
|
|
246
259
|
return;
|
|
247
260
|
}
|
|
261
|
+
// Auth failure at the transport layer (HTTP 401/403 on WS upgrade).
|
|
262
|
+
// The machine JWT is invalid (expired, malformed, or revoked by key
|
|
263
|
+
// rotation) but the backend didn't send a structured close reason.
|
|
264
|
+
// Signal the caller so it can attempt re-registration instead of
|
|
265
|
+
// entering an infinite reconnect loop with a dead token.
|
|
266
|
+
if (this.authFailed) {
|
|
267
|
+
this.emit("auth-failed");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
248
270
|
this.scheduleReconnect();
|
|
249
271
|
});
|
|
250
272
|
}
|
|
@@ -48,7 +48,7 @@ import type { RelayClient } from "./client.js";
|
|
|
48
48
|
* `id` is populated when the route had an `:id` placeholder.
|
|
49
49
|
*/
|
|
50
50
|
interface RouteMatch {
|
|
51
|
-
route: "list_projects" | "create_project" | "update_project" | "delete_project" | "bind_studio" | "list_project_sessions" | "create_session" | "get_session" | "update_session" | "delete_session" | "get_session_memory" | "get_session_memory_details" | "compact_session" | "remember_and_delete_session" | "fork_session" | "list_path_autocomplete" | "enqueue_prompt" | "get_prompt_queue" | "remove_prompt" | "clear_prompt_queue" | "list_mcp_status" | "get_settings" | "put_settings";
|
|
51
|
+
route: "list_projects" | "create_project" | "update_project" | "delete_project" | "bind_studio" | "list_project_sessions" | "create_session" | "get_session" | "update_session" | "delete_session" | "get_session_memory" | "get_session_memory_details" | "compact_session" | "remember_and_delete_session" | "fork_session" | "list_path_autocomplete" | "pick_directory" | "enqueue_prompt" | "get_prompt_queue" | "remove_prompt" | "clear_prompt_queue" | "list_mcp_status" | "get_settings" | "put_settings";
|
|
52
52
|
id?: string;
|
|
53
53
|
/** Parsed query params, if the path carried a `?...` suffix. */
|
|
54
54
|
query?: URLSearchParams;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/relay/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAIH,OAAO,KAAK,EACV,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,SAAS,EACT,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACf,MAAM,uBAAuB,CAAC;AAI/B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/relay/dispatcher.ts"],"names":[],"mappings":"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"}
|
package/dist/relay/dispatcher.js
CHANGED
|
@@ -42,6 +42,7 @@ import { homedir } from "node:os";
|
|
|
42
42
|
import { BadRequestError, NotFoundError } from "../server/handlers/errors.js";
|
|
43
43
|
import { handleListMcpStatus } from "../server/handlers/mcp-status.js";
|
|
44
44
|
import { handlePathAutocomplete } from "../server/handlers/paths-autocomplete.js";
|
|
45
|
+
import { handlePickDirectory } from "../server/handlers/paths-pick-directory.js";
|
|
45
46
|
import { handleBindStudioProject, handleCreateProject, handleDeleteProject, handleListProjects, handleListSessionsByProject, handleUpdateProject, } from "../server/handlers/projects.js";
|
|
46
47
|
import { handleCompactSession, handleCreateSession, handleDeleteSession, handleForkSession, handleRememberAndDeleteSession, handleGetSessionDetail, handleGetSessionMemoryDetails, handleGetSessionMemoryStatus, handleUpdateSession, } from "../server/handlers/sessions.js";
|
|
47
48
|
import { handleClearPromptQueue, handleEnqueuePrompt, handleGetPromptQueue, handleRemovePrompt, } from "../server/handlers/queue.js";
|
|
@@ -80,6 +81,12 @@ export function matchRoute(method, path) {
|
|
|
80
81
|
return { route: "list_path_autocomplete", query };
|
|
81
82
|
return null;
|
|
82
83
|
}
|
|
84
|
+
// /api/paths/pick-directory
|
|
85
|
+
if (cleanPath === "/api/paths/pick-directory") {
|
|
86
|
+
if (method === "POST")
|
|
87
|
+
return { route: "pick_directory" };
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
83
90
|
// /api/projects
|
|
84
91
|
if (cleanPath === "/api/projects") {
|
|
85
92
|
if (method === "GET")
|
|
@@ -397,6 +404,8 @@ async function dispatchRoute(match, body, deps) {
|
|
|
397
404
|
const prefix = match.query?.get("prefix") ?? "";
|
|
398
405
|
return handlePathAutocomplete(prefix);
|
|
399
406
|
}
|
|
407
|
+
case "pick_directory":
|
|
408
|
+
return handlePickDirectory();
|
|
400
409
|
case "enqueue_prompt":
|
|
401
410
|
return handleEnqueuePrompt(store, manager, id, asObject(body));
|
|
402
411
|
case "get_prompt_queue":
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native OS folder picker dialog for project path selection.
|
|
3
|
+
*
|
|
4
|
+
* The "New Project" form in the browser has a "Browse..." button next to
|
|
5
|
+
* the typed-path autocomplete. When clicked, it sends
|
|
6
|
+
* `POST /api/paths/pick-directory` to this handler, which launches a
|
|
7
|
+
* native OS folder picker dialog on the user's machine and returns the
|
|
8
|
+
* absolute path of the selected directory (or `null` when cancelled).
|
|
9
|
+
*
|
|
10
|
+
* Platform strategy (tried in order):
|
|
11
|
+
* 1. macOS — osascript (built-in, zero deps, native look)
|
|
12
|
+
* 2. Linux — zenity (GTK), kdialog (KDE), then Python tkinter
|
|
13
|
+
* 3. Windows — PowerShell (COM object)
|
|
14
|
+
* 4. Fallback — Python tkinter (bundled with most Python installs)
|
|
15
|
+
*
|
|
16
|
+
* The first available tool wins; if none work, we return an error.
|
|
17
|
+
* All child processes are synchronous — the browser is waiting anyway.
|
|
18
|
+
*/
|
|
19
|
+
/** Result shape returned to the frontend. */
|
|
20
|
+
export interface PickDirectoryResult {
|
|
21
|
+
/** Absolute path of the selected directory, or null if cancelled. */
|
|
22
|
+
path: string | null;
|
|
23
|
+
/** Human-readable error when the picker couldn't be launched. */
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Attempt to open a native folder picker dialog and return the selected path.
|
|
28
|
+
*
|
|
29
|
+
* Synchronous and blocking by design — the browser form is waiting for a
|
|
30
|
+
* response and the user is interacting with the dialog.
|
|
31
|
+
*/
|
|
32
|
+
export declare function handlePickDirectory(): PickDirectoryResult;
|
|
33
|
+
//# sourceMappingURL=paths-pick-directory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths-pick-directory.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/paths-pick-directory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,6CAA6C;AAC7C,MAAM,WAAW,mBAAmB;IAClC,qEAAqE;IACrE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAqID;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,mBAAmB,CAgCzD"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native OS folder picker dialog for project path selection.
|
|
3
|
+
*
|
|
4
|
+
* The "New Project" form in the browser has a "Browse..." button next to
|
|
5
|
+
* the typed-path autocomplete. When clicked, it sends
|
|
6
|
+
* `POST /api/paths/pick-directory` to this handler, which launches a
|
|
7
|
+
* native OS folder picker dialog on the user's machine and returns the
|
|
8
|
+
* absolute path of the selected directory (or `null` when cancelled).
|
|
9
|
+
*
|
|
10
|
+
* Platform strategy (tried in order):
|
|
11
|
+
* 1. macOS — osascript (built-in, zero deps, native look)
|
|
12
|
+
* 2. Linux — zenity (GTK), kdialog (KDE), then Python tkinter
|
|
13
|
+
* 3. Windows — PowerShell (COM object)
|
|
14
|
+
* 4. Fallback — Python tkinter (bundled with most Python installs)
|
|
15
|
+
*
|
|
16
|
+
* The first available tool wins; if none work, we return an error.
|
|
17
|
+
* All child processes are synchronous — the browser is waiting anyway.
|
|
18
|
+
*/
|
|
19
|
+
import { execSync } from "node:child_process";
|
|
20
|
+
import { platform } from "node:os";
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Platform pickers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
/**
|
|
25
|
+
* macOS: Use `osascript` with AppleScript to show a native folder chooser.
|
|
26
|
+
* Returns the POSIX path or empty string on cancel.
|
|
27
|
+
*/
|
|
28
|
+
function pickMacOS() {
|
|
29
|
+
const script = `
|
|
30
|
+
set folderPath to POSIX path of (choose folder with prompt "Select project directory")
|
|
31
|
+
return folderPath
|
|
32
|
+
`.trim();
|
|
33
|
+
try {
|
|
34
|
+
const result = execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, {
|
|
35
|
+
encoding: "utf-8",
|
|
36
|
+
timeout: 180_000, // 3 min — user might take time to browse
|
|
37
|
+
});
|
|
38
|
+
const trimmed = result.trim();
|
|
39
|
+
return trimmed || null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// osascript exits non-zero on user cancel (error -128) — treat as silent cancel
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Run a command that shows a folder picker dialog. Returns `null` on user
|
|
48
|
+
* cancellation, and falls through (re-throws) if the tool is not installed.
|
|
49
|
+
*/
|
|
50
|
+
function tryDialog(cmd) {
|
|
51
|
+
try {
|
|
52
|
+
const result = execSync(cmd, { encoding: "utf-8", timeout: 180_000 });
|
|
53
|
+
return result.trim() || null;
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
// exit code 1 = user cancelled the dialog → stop here, return null
|
|
57
|
+
// exit code 127 = command not found → re-throw so the next picker tries
|
|
58
|
+
const status = err && typeof err === "object" && "status" in err
|
|
59
|
+
? err.status
|
|
60
|
+
: undefined;
|
|
61
|
+
if (status === 1)
|
|
62
|
+
return null;
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Linux: Try zenity (GTK), then kdialog (KDE), then Python tkinter.
|
|
68
|
+
*/
|
|
69
|
+
function pickLinux() {
|
|
70
|
+
try {
|
|
71
|
+
return tryDialog(`zenity --file-selection --directory --title="Select project directory"`);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// zenity not available
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
return tryDialog(`kdialog --getexistingdirectory "${process.env.HOME ?? "/"}" --title "Select project directory"`);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// kdialog not available
|
|
81
|
+
}
|
|
82
|
+
// Try Python tkinter
|
|
83
|
+
return pickTkinter();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Windows: Use PowerShell to invoke the COM Shell.Application folder picker.
|
|
87
|
+
*/
|
|
88
|
+
function pickWindows() {
|
|
89
|
+
const script = `
|
|
90
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
91
|
+
$f = New-Object System.Windows.Forms.FolderBrowserDialog
|
|
92
|
+
$f.Description = "Select project directory"
|
|
93
|
+
$f.ShowNewFolderButton = $true
|
|
94
|
+
if ($f.ShowDialog() -eq "OK") { $f.SelectedPath } else { "" }
|
|
95
|
+
`.trim();
|
|
96
|
+
try {
|
|
97
|
+
const result = execSync(`powershell -NoProfile -Command "${script.replace(/"/g, '\\"')}"`, { encoding: "utf-8", timeout: 180_000 });
|
|
98
|
+
return result.trim() || null;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Python tkinter fallback — works on any OS with Python + tkinter installed.
|
|
106
|
+
* tkinter is bundled with macOS Python from python.org and most Linux distros.
|
|
107
|
+
*/
|
|
108
|
+
function pickTkinter() {
|
|
109
|
+
const script = `import tkinter as tk
|
|
110
|
+
from tkinter import filedialog
|
|
111
|
+
root = tk.Tk()
|
|
112
|
+
root.withdraw()
|
|
113
|
+
root.attributes('-topmost', True)
|
|
114
|
+
d = filedialog.askdirectory(title="Select project directory")
|
|
115
|
+
root.destroy()
|
|
116
|
+
print(d if d else "")
|
|
117
|
+
`.replace(/\n/g, ";").replace(/"/g, '\\"');
|
|
118
|
+
try {
|
|
119
|
+
const result = execSync(`python3 -c "${script}"`, {
|
|
120
|
+
encoding: "utf-8",
|
|
121
|
+
timeout: 180_000,
|
|
122
|
+
});
|
|
123
|
+
return result.trim() || null;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// python3 not found — try 'python' (Windows)
|
|
127
|
+
try {
|
|
128
|
+
const result = execSync(`python -c "${script}"`, {
|
|
129
|
+
encoding: "utf-8",
|
|
130
|
+
timeout: 180_000,
|
|
131
|
+
});
|
|
132
|
+
return result.trim() || null;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Public handler
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
/**
|
|
143
|
+
* Attempt to open a native folder picker dialog and return the selected path.
|
|
144
|
+
*
|
|
145
|
+
* Synchronous and blocking by design — the browser form is waiting for a
|
|
146
|
+
* response and the user is interacting with the dialog.
|
|
147
|
+
*/
|
|
148
|
+
export function handlePickDirectory() {
|
|
149
|
+
const os = platform();
|
|
150
|
+
try {
|
|
151
|
+
let path = null;
|
|
152
|
+
if (os === "darwin") {
|
|
153
|
+
path = pickMacOS();
|
|
154
|
+
}
|
|
155
|
+
else if (os === "linux") {
|
|
156
|
+
path = pickLinux();
|
|
157
|
+
}
|
|
158
|
+
else if (os === "win32") {
|
|
159
|
+
path = pickWindows();
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
// Unknown platform — try Python tkinter as universal fallback
|
|
163
|
+
path = pickTkinter();
|
|
164
|
+
}
|
|
165
|
+
if (path === null) {
|
|
166
|
+
// User cancelled or no picker available. Distinguish:
|
|
167
|
+
// When no path AND no error in the platform pickers, it's a cancel.
|
|
168
|
+
return { path: null };
|
|
169
|
+
}
|
|
170
|
+
return { path };
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
// The platform picker failed entirely (not cancelled — process error).
|
|
174
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
175
|
+
return {
|
|
176
|
+
path: null,
|
|
177
|
+
error: `Could not open folder picker: ${msg}`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -252,7 +252,9 @@ export declare class SessionStreamManager {
|
|
|
252
252
|
* - When neither envelope nor SQLite have a value, we leave model
|
|
253
253
|
* selection to spectral's own settings file (pre-Phase-3 behaviour).
|
|
254
254
|
*/
|
|
255
|
-
prompt(sessionId: string, content: string, modelId?: string, images?: ImageAttachment[], reasoningEffort?: string
|
|
255
|
+
prompt(sessionId: string, content: string, modelId?: string, images?: ImageAttachment[], reasoningEffort?: string, opts?: {
|
|
256
|
+
skipPersistence?: boolean;
|
|
257
|
+
}): Promise<void>;
|
|
256
258
|
/**
|
|
257
259
|
* Tear down everything. Best-effort: disposes every bridge, drops all
|
|
258
260
|
* subscribers. After this the manager is unusable.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-stream.d.ts","sourceRoot":"","sources":["../../src/server/session-stream.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"session-stream.d.ts","sourceRoot":"","sources":["../../src/server/session-stream.ts"],"names":[],"mappings":"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"}
|
|
@@ -484,7 +484,7 @@ export class SessionStreamManager {
|
|
|
484
484
|
* - When neither envelope nor SQLite have a value, we leave model
|
|
485
485
|
* selection to spectral's own settings file (pre-Phase-3 behaviour).
|
|
486
486
|
*/
|
|
487
|
-
async prompt(sessionId, content, modelId, images, reasoningEffort) {
|
|
487
|
+
async prompt(sessionId, content, modelId, images, reasoningEffort, opts) {
|
|
488
488
|
if (this.disposed)
|
|
489
489
|
throw new Error("SessionStreamManager disposed");
|
|
490
490
|
let stream = this.streams.get(sessionId);
|
|
@@ -619,25 +619,30 @@ export class SessionStreamManager {
|
|
|
619
619
|
}
|
|
620
620
|
}
|
|
621
621
|
// 1. Persist user message first (survives mid-prompt failures).
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
622
|
+
// When skipPersistence is true (e.g. auto-research), the prompt is
|
|
623
|
+
// an internal system instruction — do NOT write it to SQLite or
|
|
624
|
+
// broadcast it as a user message.
|
|
625
|
+
if (!opts?.skipPersistence) {
|
|
626
|
+
let stored;
|
|
627
|
+
try {
|
|
628
|
+
stored = this.store.appendMessage(sessionId, {
|
|
629
|
+
role: "user",
|
|
630
|
+
content,
|
|
631
|
+
images,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
catch (err) {
|
|
635
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
636
|
+
this.broadcast(stream, {
|
|
637
|
+
type: "error",
|
|
638
|
+
message: `Failed to persist user message: ${e.message}`,
|
|
639
|
+
});
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
// 2. Broadcast the persisted message so every tab — including the one
|
|
643
|
+
// that sent the prompt — appends an authoritative user turn.
|
|
644
|
+
this.broadcast(stream, { type: "user_message_appended", message: stored });
|
|
637
645
|
}
|
|
638
|
-
// 2. Broadcast the persisted message so every tab — including the one
|
|
639
|
-
// that sent the prompt — appends an authoritative user turn.
|
|
640
|
-
this.broadcast(stream, { type: "user_message_appended", message: stored });
|
|
641
646
|
// 3. Open a new in-flight turn. Allocated even before spectral emits anything
|
|
642
647
|
// so a re-attach immediately after `prompt` sees the turn.
|
|
643
648
|
stream.currentTurn = {
|