@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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `spectral serve` — relay orchestration entry point.
3
3
  *
4
- * Architecture (post-Batch 2 cutover):
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
- * 6. Until Batch 3 ships, every inbound non-pong frame is acked back as
14
- * `{kind:"ack", echo:<frame>}`. This proves the WS is bidirectional
15
- * end-to-end and gives operators a smoke check before envelope routing.
16
- * 7. SIGINT/SIGTERM trigger graceful shutdown: dispose the relay (close 1000),
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;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,CA0SlF;AAiED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAa5E"}
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"}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `spectral serve` — relay orchestration entry point.
3
3
  *
4
- * Architecture (post-Batch 2 cutover):
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
- * 6. Until Batch 3 ships, every inbound non-pong frame is acked back as
14
- * `{kind:"ack", echo:<frame>}`. This proves the WS is bidirectional
15
- * end-to-end and gives operators a smoke check before envelope routing.
16
- * 7. SIGINT/SIGTERM trigger graceful shutdown: dispose the relay (close 1000),
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
- // Envelope dispatcher. We only act on `rest_request` and
223
- // `client_message`; everything else (welcome, pong, error, etc.) is
224
- // either handled by RelayClient itself or is informational.
225
- //
226
- // Frames arrive typed loosely from `RelayClient` (which is protocol-
227
- // agnostic it just forwards JSON). We re-narrow via the `kind`
228
- // discriminator and cast to the strict envelope types from
229
- // `@aexol/relay-protocol`. The protocol package's own `parseFrame()`
230
- // already validated `kind` membership in RelayClient itself; here we
231
- // additionally trust that the inner shape matches its discriminator
232
- // (the backend is the only producer and is exercised by tests).
233
- relay.on("frame", (frame) => {
234
- if (frame.kind === "rest_request") {
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
- return;
253
- }
254
- if (frame.kind === "subscribe") {
255
- handleSubscribe(frame, {
256
- manager,
257
- relay,
258
- subscribers,
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
- return;
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
- return;
275
- }
276
- if (frame.kind === "config_patch") {
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
- // Other frames (error, machine_disconnected addressed to us, etc.)
286
- // are ignored at this layer. Future batches may surface them in
287
- // structured logs.
288
- });
289
- // On every relay close, drop the subscriber registry. The manager
290
- // keeps streams alive (so `dispose` is the only place that tears
291
- // them down), but the previous subscribers are pointing at a stale
292
- // socketlet the next `client_message` attach a fresh one.
293
- relay.on("close", () => {
294
- detachAllSubscribers(manager, subscribers);
295
- });
296
- // Send native extensions info when the relay connects
297
- relay.on("welcome", () => {
298
- if (!silent)
299
- process.stdout.write("✓ Relay connected\n");
300
- broadcastNativeExtensionsInfo(cwd, relay, registration.record.machineId);
301
- });
302
- if (!silent) {
303
- relay.on("close", ({ code, reason }) => {
304
- process.stdout.write(`Relay closed (${code}${reason ? `: ${reason}` : ""}); will reconnect…\n`);
305
- });
306
- relay.on("reconnect-scheduled", ({ delayMs, attempt }) => {
307
- process.stdout.write(`Reconnect attempt ${attempt} in ${delayMs}ms\n`);
308
- });
309
- relay.on("error", (err) => {
310
- const msg = err instanceof Error ? err.message : String(err);
311
- process.stderr.write(`Relay error: ${msg}\n`);
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,CAmLN"}
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
- manager.prompt(sessionId, taskContent, storedModelId).catch((err) => {
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);
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;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;IAEjC,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;IAgHlB,OAAO,CAAC,aAAa;IAsBrB,OAAO,CAAC,YAAY;IAOpB;;;;;OAKG;YACW,WAAW;YA8BX,iBAAiB;CAmDhC"}
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"}
@@ -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":"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"}
1
+ {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/relay/dispatcher.ts"],"names":[],"mappings":"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"}
@@ -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): Promise<void>;
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
- let stored;
623
- try {
624
- stored = this.store.appendMessage(sessionId, {
625
- role: "user",
626
- content,
627
- images,
628
- });
629
- }
630
- catch (err) {
631
- const e = err instanceof Error ? err : new Error(String(err));
632
- this.broadcast(stream, {
633
- type: "error",
634
- message: `Failed to persist user message: ${e.message}`,
635
- });
636
- return;
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 = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.9.6",
3
+ "version": "0.9.8",
4
4
  "description": "AI coding agent for Aexol with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,