omnara 0.5.0 → 0.5.1

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/cli.js CHANGED
@@ -158,6 +158,7 @@ import { simpleGit as simpleGit2 } from "simple-git";
158
158
  import {
159
159
  query as query2
160
160
  } from "@anthropic-ai/claude-agent-sdk";
161
+ import { setTimeout as delay } from "node:timers/promises";
161
162
 
162
163
  // src/utils/logger.ts
163
164
  import path from "path";
@@ -442,6 +443,16 @@ function extractErrorMessage(error) {
442
443
  return String(error);
443
444
  }
444
445
 
446
+ // src/utils/backoff.ts
447
+ var INITIAL_RETRY_DELAY_MS = 1e3;
448
+ var MAX_RETRY_DELAY_MS = 3e4;
449
+ function calculateBackoff(currentDelayMs) {
450
+ let nextDelay = currentDelayMs * 2;
451
+ const jitterRange = nextDelay * 0.25;
452
+ nextDelay = nextDelay - jitterRange + Math.random() * jitterRange * 2;
453
+ return Math.min(nextDelay, MAX_RETRY_DELAY_MS);
454
+ }
455
+
445
456
  // src/orchestrator/utils/envelope-utils.ts
446
457
  import { randomUUID } from "crypto";
447
458
  function createEnvelope(sessionId, content, senderId, metadata, payloadMetadata) {
@@ -629,6 +640,7 @@ function buildAgentCompletionMessage(resultText, summary, errorText) {
629
640
  }
630
641
 
631
642
  // src/orchestrator/session.ts
643
+ var MAX_GENERATION_RETRIES = 6;
632
644
  var Session = class {
633
645
  constructor(id, ai, options = {}) {
634
646
  this.id = id;
@@ -653,6 +665,8 @@ var Session = class {
653
665
  announced = false;
654
666
  shouldSummarize = false;
655
667
  conversationHistory = [];
668
+ retryDelayMs = INITIAL_RETRY_DELAY_MS;
669
+ retryCount = 0;
656
670
  sessionInitControl;
657
671
  enqueue(message) {
658
672
  this.queue.push(message);
@@ -727,6 +741,7 @@ var Session = class {
727
741
  await this.dispatchEnvelope(envelope);
728
742
  }
729
743
  }
744
+ this.resetRetryState();
730
745
  const complete = await this.buildCompletionEnvelope(resultMessage, summaryTextBlocks);
731
746
  await Promise.all([
732
747
  this.emitProgress("idle", "Ready for next message"),
@@ -738,14 +753,42 @@ var Session = class {
738
753
  sessionId: this.id,
739
754
  pendingMessages: this.queue.length
740
755
  });
741
- if (this.queue.length > 0) {
742
- this.startGenerate();
743
- }
744
756
  }
745
757
  }
746
758
  startGenerate() {
747
- void this.generate().catch((error) => {
748
- logger.error("[Session] Generation failed", { sessionId: this.id, error });
759
+ void this.generate().then(() => {
760
+ if (this.queue.length > 0) {
761
+ this.startGenerate();
762
+ }
763
+ }).catch(async (error) => {
764
+ this.retryCount++;
765
+ logger.error("[Session] Generation failed", {
766
+ sessionId: this.id,
767
+ error,
768
+ attempt: this.retryCount,
769
+ maxRetries: MAX_GENERATION_RETRIES
770
+ });
771
+ if (this.retryCount < MAX_GENERATION_RETRIES && this.queue.length > 0) {
772
+ logger.info(
773
+ `[Session] Retrying in ${Math.round(this.retryDelayMs / 1e3)}s (attempt ${this.retryCount}/${MAX_GENERATION_RETRIES})`,
774
+ { sessionId: this.id }
775
+ );
776
+ await delay(this.retryDelayMs);
777
+ this.retryDelayMs = calculateBackoff(this.retryDelayMs);
778
+ this.startGenerate();
779
+ } else if (this.queue.length > 0) {
780
+ logger.error(
781
+ `[Session] Max retries (${MAX_GENERATION_RETRIES}) reached, sending error to client`,
782
+ { sessionId: this.id }
783
+ );
784
+ const errorEnvelope = this.buildErrorCompletionEnvelope(error);
785
+ this.queue = [];
786
+ await Promise.all([
787
+ this.emitProgress("error", "Generation failed"),
788
+ this.dispatchEnvelope(errorEnvelope)
789
+ ]);
790
+ this.resetRetryState();
791
+ }
749
792
  });
750
793
  }
751
794
  captureSdkSessionId(message) {
@@ -795,6 +838,26 @@ var Session = class {
795
838
  };
796
839
  return createEnvelope(this.id, content, this.id);
797
840
  }
841
+ buildErrorCompletionEnvelope(error) {
842
+ const errorMessage = error instanceof Error ? error.message : String(error);
843
+ const content = {
844
+ type: "agent_complete",
845
+ success: false,
846
+ duration_ms: null,
847
+ cost_usd: null,
848
+ message: `Generation failed after ${this.retryCount} retries: ${errorMessage}`,
849
+ metadata: {
850
+ agent_session_id: this.id,
851
+ error: errorMessage,
852
+ retries: this.retryCount
853
+ }
854
+ };
855
+ return createEnvelope(this.id, content, this.id);
856
+ }
857
+ resetRetryState() {
858
+ this.retryDelayMs = INITIAL_RETRY_DELAY_MS;
859
+ this.retryCount = 0;
860
+ }
798
861
  async summarize(summaryTextBlocks) {
799
862
  if (summaryTextBlocks.length === 0) {
800
863
  return "No assistant output to summarize.";
@@ -1206,7 +1269,7 @@ import * as Sentry3 from "@sentry/node";
1206
1269
 
1207
1270
  // src/auth/credentials-store.ts
1208
1271
  import fs2 from "fs/promises";
1209
- import { setTimeout as delay } from "node:timers/promises";
1272
+ import { setTimeout as delay2 } from "node:timers/promises";
1210
1273
  import os2 from "node:os";
1211
1274
  import path3 from "node:path";
1212
1275
  import * as Sentry2 from "@sentry/node";
@@ -1349,7 +1412,7 @@ var CredentialsStore = class {
1349
1412
  `Timed out acquiring credentials lock ${lockPath}${formatLockHolder(holder)}`
1350
1413
  );
1351
1414
  }
1352
- await delay(LOCK_RETRY_INTERVAL_MS);
1415
+ await delay2(LOCK_RETRY_INTERVAL_MS);
1353
1416
  }
1354
1417
  }
1355
1418
  }
@@ -2398,9 +2461,11 @@ var AgentOrchestrator = class {
2398
2461
  this.started = false;
2399
2462
  }
2400
2463
  async cancel() {
2401
- await Promise.all(
2402
- this.workerManager.listWorkers().filter((worker) => worker.status === "running").map((worker) => this.workerManager.cancelWorker(worker.session.id).catch(() => false))
2403
- );
2464
+ if (this.sessionSettings.type === "orchestrator") {
2465
+ await Promise.all(
2466
+ this.workerManager.listWorkers().filter((worker) => worker.status === "running").map((worker) => this.workerManager.cancelWorker(worker.session.id).catch(() => false))
2467
+ );
2468
+ }
2404
2469
  await this.session?.cancel();
2405
2470
  }
2406
2471
  async getCompletions(request) {
@@ -7717,12 +7782,12 @@ var RelayBridge = class extends EventEmitter {
7717
7782
  }
7718
7783
  });
7719
7784
  this.ensureAgentSessionExists();
7720
- let backoffMs = 1e3;
7785
+ let backoffMs = INITIAL_RETRY_DELAY_MS;
7721
7786
  while (!this.stopped) {
7722
7787
  try {
7723
7788
  await this.establishConnection();
7724
7789
  this.clearLoginWait();
7725
- backoffMs = 1e3;
7790
+ backoffMs = INITIAL_RETRY_DELAY_MS;
7726
7791
  } catch (error) {
7727
7792
  if (isLoginRequiredError(error) || isUnauthorizedApiError(error)) {
7728
7793
  if (!this.waitingForLogin) {
@@ -7733,7 +7798,7 @@ var RelayBridge = class extends EventEmitter {
7733
7798
  } else {
7734
7799
  this.logger.debug("Relay bridge still waiting for credentials...");
7735
7800
  }
7736
- await delay2(LOGIN_WAIT_DELAY_MS);
7801
+ await delay3(LOGIN_WAIT_DELAY_MS);
7737
7802
  continue;
7738
7803
  }
7739
7804
  const aborted = isAbortError(error);
@@ -7747,8 +7812,8 @@ var RelayBridge = class extends EventEmitter {
7747
7812
  this.logger.error(
7748
7813
  `Relay connection failed: ${extractErrorMessage5(error)}. Retrying in ${Math.round(backoffMs / 1e3)}s.`
7749
7814
  );
7750
- await delay2(backoffMs);
7751
- backoffMs = Math.min(backoffMs * 2, 3e4);
7815
+ await delay3(backoffMs);
7816
+ backoffMs = calculateBackoff(backoffMs);
7752
7817
  }
7753
7818
  }
7754
7819
  }
@@ -8395,7 +8460,7 @@ function buildRelayWebsocketUrl(baseUrl, path16) {
8395
8460
  }
8396
8461
  return url.toString();
8397
8462
  }
8398
- function delay2(ms) {
8463
+ function delay3(ms) {
8399
8464
  return new Promise((resolve) => setTimeout(resolve, ms));
8400
8465
  }
8401
8466
  function extractErrorMessage5(error) {
@@ -8415,22 +8480,36 @@ function isAbortError(error) {
8415
8480
  const message = error.message ? error.message.toLowerCase() : "";
8416
8481
  return message.includes("aborted with ctrl+c");
8417
8482
  }
8483
+ var CONNECT_TIMEOUT_MS = 3e4;
8418
8484
  function waitForOpen(ws) {
8419
8485
  return new Promise((resolve, reject) => {
8420
- const onOpen = () => {
8421
- cleanup();
8422
- resolve();
8423
- };
8424
- const onError = (error) => {
8425
- cleanup();
8426
- reject(error);
8427
- };
8486
+ let timer = null;
8428
8487
  const cleanup = () => {
8488
+ if (timer) {
8489
+ clearTimeout(timer);
8490
+ timer = null;
8491
+ }
8429
8492
  ws.removeListener("open", onOpen);
8430
8493
  ws.removeListener("error", onError);
8431
8494
  };
8495
+ const settle = (error) => {
8496
+ cleanup();
8497
+ if (error) {
8498
+ if (ws.readyState === WebSocket.CONNECTING) {
8499
+ ws.terminate();
8500
+ }
8501
+ reject(error);
8502
+ } else {
8503
+ resolve();
8504
+ }
8505
+ };
8506
+ const onOpen = () => settle();
8507
+ const onError = (error) => settle(error);
8432
8508
  ws.once("open", onOpen);
8433
8509
  ws.once("error", onError);
8510
+ timer = setTimeout(() => {
8511
+ settle(new Error("WebSocket connection timed out"));
8512
+ }, CONNECT_TIMEOUT_MS);
8434
8513
  });
8435
8514
  }
8436
8515
  function isLoginRequiredError(error) {
@@ -8525,63 +8604,7 @@ function buildOrchestratorLogger() {
8525
8604
 
8526
8605
  // src/commands/auth.ts
8527
8606
  import { Command } from "commander";
8528
- import * as Sentry13 from "@sentry/node";
8529
- function buildAuthCommand() {
8530
- const auth = new Command("auth").description("Authenticate the Omnara CLI.");
8531
- auth.command("login").description("Authenticate via the browser-based verification flow.").action(async () => {
8532
- const store = new CredentialsStore();
8533
- const backendUrl = resolveBackendUrl();
8534
- try {
8535
- await performVerificationLogin({
8536
- backendUrlOption: backendUrl,
8537
- frontendUrlOption: resolveFrontendUrl(),
8538
- store
8539
- });
8540
- } catch (error) {
8541
- Sentry13.captureException(error);
8542
- if (error instanceof ApiError) {
8543
- console.error(`
8544
- Request failed (HTTP ${error.status}): ${error.message}`);
8545
- } else {
8546
- console.error(`
8547
- Login failed: ${extractErrorMessage6(error)}`);
8548
- }
8549
- process.exitCode = 1;
8550
- }
8551
- });
8552
- auth.command("logout").description("Remove stored CLI credentials.").action(async () => {
8553
- const store = new CredentialsStore();
8554
- await store.clear();
8555
- console.log("Credentials cleared.");
8556
- });
8557
- auth.command("status").description("Show stored CLI credential details.").action(async () => {
8558
- const store = new CredentialsStore();
8559
- const credentials = await store.load();
8560
- if (!credentials) {
8561
- console.log("No credentials stored.");
8562
- return;
8563
- }
8564
- printStatus(credentials);
8565
- });
8566
- return auth;
8567
- }
8568
- function printStatus(credentials) {
8569
- if (credentials.pat) {
8570
- console.log("Authentication: PAT stored");
8571
- return;
8572
- }
8573
- console.log("Authentication: Not configured");
8574
- }
8575
- function extractErrorMessage6(error) {
8576
- if (error instanceof Error) {
8577
- return error.message;
8578
- }
8579
- return String(error);
8580
- }
8581
-
8582
- // src/commands/daemon.ts
8583
- import { Command as Command2 } from "commander";
8584
- import * as Sentry24 from "@sentry/node";
8607
+ import * as Sentry18 from "@sentry/node";
8585
8608
 
8586
8609
  // src/daemon/daemon-launcher.ts
8587
8610
  import { spawn as spawn2, exec as execCb } from "node:child_process";
@@ -8591,12 +8614,12 @@ import { promisify as promisify2 } from "node:util";
8591
8614
 
8592
8615
  // src/daemon/daemon-client.ts
8593
8616
  import http2 from "node:http";
8594
- import * as Sentry15 from "@sentry/node";
8617
+ import * as Sentry14 from "@sentry/node";
8595
8618
 
8596
8619
  // src/daemon/state-store.ts
8597
8620
  import fs7 from "node:fs/promises";
8598
8621
  import path8 from "node:path";
8599
- import * as Sentry14 from "@sentry/node";
8622
+ import * as Sentry13 from "@sentry/node";
8600
8623
 
8601
8624
  // src/daemon/paths.ts
8602
8625
  import fs6 from "node:fs/promises";
@@ -8640,9 +8663,9 @@ var DaemonStateStore = class {
8640
8663
  if (isFileNotFound2(error)) {
8641
8664
  return null;
8642
8665
  }
8643
- Sentry14.captureException(error);
8666
+ Sentry13.captureException(error);
8644
8667
  throw new Error(
8645
- `Failed to read daemon state from ${this.filePath}: ${extractErrorMessage7(error)}`
8668
+ `Failed to read daemon state from ${this.filePath}: ${extractErrorMessage6(error)}`
8646
8669
  );
8647
8670
  }
8648
8671
  }
@@ -8661,7 +8684,7 @@ var DaemonStateStore = class {
8661
8684
  await fs7.unlink(this.filePath);
8662
8685
  } catch (error) {
8663
8686
  if (!isFileNotFound2(error)) {
8664
- Sentry14.captureException(error);
8687
+ Sentry13.captureException(error);
8665
8688
  throw error;
8666
8689
  }
8667
8690
  }
@@ -8672,7 +8695,7 @@ function isFileNotFound2(error) {
8672
8695
  error && typeof error === "object" && "code" in error && error.code === "ENOENT"
8673
8696
  );
8674
8697
  }
8675
- function extractErrorMessage7(error) {
8698
+ function extractErrorMessage6(error) {
8676
8699
  if (error instanceof Error && error.message) {
8677
8700
  return error.message;
8678
8701
  }
@@ -8699,7 +8722,7 @@ var DaemonClient = class {
8699
8722
  await this.stateStore.clear();
8700
8723
  return null;
8701
8724
  }
8702
- Sentry15.captureException(error);
8725
+ Sentry14.captureException(error);
8703
8726
  throw error;
8704
8727
  }
8705
8728
  }
@@ -8719,7 +8742,7 @@ var DaemonClient = class {
8719
8742
  await this.stateStore.clear();
8720
8743
  return null;
8721
8744
  }
8722
- Sentry15.captureException(error);
8745
+ Sentry14.captureException(error);
8723
8746
  throw error;
8724
8747
  }
8725
8748
  }
@@ -8742,7 +8765,7 @@ var DaemonClient = class {
8742
8765
  await this.stateStore.clear();
8743
8766
  return false;
8744
8767
  }
8745
- Sentry15.captureException(error);
8768
+ Sentry14.captureException(error);
8746
8769
  throw error;
8747
8770
  }
8748
8771
  }
@@ -8767,7 +8790,7 @@ var DaemonClient = class {
8767
8790
  await this.stateStore.clear();
8768
8791
  return null;
8769
8792
  }
8770
- Sentry15.captureException(error);
8793
+ Sentry14.captureException(error);
8771
8794
  throw error;
8772
8795
  }
8773
8796
  }
@@ -8841,7 +8864,7 @@ function isProcessAlive2(pid) {
8841
8864
  if (isErrno2(error, "ESRCH")) {
8842
8865
  return false;
8843
8866
  }
8844
- Sentry15.captureException(error);
8867
+ Sentry14.captureException(error);
8845
8868
  return false;
8846
8869
  }
8847
8870
  }
@@ -8874,13 +8897,13 @@ var HttpError = class extends Error {
8874
8897
  // src/daemon/directory-registry.ts
8875
8898
  import fs9 from "node:fs/promises";
8876
8899
  import path10 from "node:path";
8877
- import * as Sentry17 from "@sentry/node";
8900
+ import * as Sentry16 from "@sentry/node";
8878
8901
 
8879
8902
  // src/daemon/config-store.ts
8880
8903
  import fs8 from "node:fs/promises";
8881
8904
  import path9 from "node:path";
8882
8905
  import { randomUUID as randomUUID6 } from "node:crypto";
8883
- import * as Sentry16 from "@sentry/node";
8906
+ import * as Sentry15 from "@sentry/node";
8884
8907
  var DEFAULT_CONFIG = {
8885
8908
  directories: [],
8886
8909
  settings: {}
@@ -8898,9 +8921,9 @@ var DaemonConfigStore = class {
8898
8921
  if (isFileNotFound3(error)) {
8899
8922
  return cloneConfig(DEFAULT_CONFIG);
8900
8923
  }
8901
- Sentry16.captureException(error);
8924
+ Sentry15.captureException(error);
8902
8925
  throw new Error(
8903
- `Failed to read daemon config from ${this.filePath}: ${extractErrorMessage8(error)}`
8926
+ `Failed to read daemon config from ${this.filePath}: ${extractErrorMessage7(error)}`
8904
8927
  );
8905
8928
  }
8906
8929
  }
@@ -8982,7 +9005,7 @@ function isFileNotFound3(error) {
8982
9005
  error && typeof error === "object" && "code" in error && error.code === "ENOENT"
8983
9006
  );
8984
9007
  }
8985
- function extractErrorMessage8(error) {
9008
+ function extractErrorMessage7(error) {
8986
9009
  if (error instanceof Error && error.message) {
8987
9010
  return error.message;
8988
9011
  }
@@ -9020,14 +9043,14 @@ var DirectoryRegistry = class {
9020
9043
  throw new Error("Path is not a directory");
9021
9044
  }
9022
9045
  } catch (error) {
9023
- Sentry17.captureException(error);
9046
+ Sentry16.captureException(error);
9024
9047
  throw new Error(
9025
- `Directory ${directoryPath} is not accessible: ${extractErrorMessage9(error)}`
9048
+ `Directory ${directoryPath} is not accessible: ${extractErrorMessage8(error)}`
9026
9049
  );
9027
9050
  }
9028
9051
  }
9029
9052
  };
9030
- function extractErrorMessage9(error) {
9053
+ function extractErrorMessage8(error) {
9031
9054
  if (error instanceof Error && error.message) {
9032
9055
  return error.message;
9033
9056
  }
@@ -9043,7 +9066,7 @@ import path11 from "node:path";
9043
9066
  import os3 from "node:os";
9044
9067
  import { execFile } from "node:child_process";
9045
9068
  import { promisify } from "node:util";
9046
- import * as Sentry18 from "@sentry/node";
9069
+ import * as Sentry17 from "@sentry/node";
9047
9070
  var REGISTRY_PATH = path11.join(OMNARA_HOME_DIR, "session-processes.json");
9048
9071
  var LOCK_PATH = `${REGISTRY_PATH}.lock`;
9049
9072
  var execFileAsync = promisify(execFile);
@@ -9055,7 +9078,7 @@ async function acquireLock(retries = 20, delayMs = 50) {
9055
9078
  return handle;
9056
9079
  } catch (error) {
9057
9080
  if (error.code !== "EEXIST") {
9058
- Sentry18.captureException(error);
9081
+ Sentry17.captureException(error);
9059
9082
  throw error;
9060
9083
  }
9061
9084
  await new Promise((resolve) => setTimeout(resolve, delayMs));
@@ -9092,7 +9115,7 @@ async function readRegistry() {
9092
9115
  if (error.code === "ENOENT") {
9093
9116
  return {};
9094
9117
  }
9095
- Sentry18.captureException(error);
9118
+ Sentry17.captureException(error);
9096
9119
  throw error;
9097
9120
  }
9098
9121
  }
@@ -9120,7 +9143,7 @@ async function isProcessAlive3(pid) {
9120
9143
  if (error.code === "ESRCH") {
9121
9144
  return false;
9122
9145
  }
9123
- Sentry18.captureException(error);
9146
+ Sentry17.captureException(error);
9124
9147
  return true;
9125
9148
  }
9126
9149
  }
@@ -9329,13 +9352,13 @@ async function killAllDaemonProcesses(options = {}) {
9329
9352
  process.kill(pid, "SIGKILL");
9330
9353
  logger3.success(`Killed process ${pid}`);
9331
9354
  } catch (error) {
9332
- logger3.warn(`Failed to kill ${pid}: ${extractErrorMessage10(error)}`);
9355
+ logger3.warn(`Failed to kill ${pid}: ${extractErrorMessage9(error)}`);
9333
9356
  }
9334
9357
  }
9335
9358
  await stateStore.clear();
9336
9359
  logger3.debug(`Cleared ${envLabel} daemon state file`);
9337
9360
  } catch (error) {
9338
- logger3.error(`Failed to kill ${envLabel} daemon processes: ${extractErrorMessage10(error)}`);
9361
+ logger3.error(`Failed to kill ${envLabel} daemon processes: ${extractErrorMessage9(error)}`);
9339
9362
  }
9340
9363
  }
9341
9364
  }
@@ -9352,7 +9375,7 @@ async function killDaemonByState(options = {}) {
9352
9375
  process.kill(state.pid, "SIGKILL");
9353
9376
  logger3.success(`Killed daemon process ${state.pid}`);
9354
9377
  } catch (error) {
9355
- logger3.warn(`Failed to kill daemon ${state.pid}: ${extractErrorMessage10(error)}`);
9378
+ logger3.warn(`Failed to kill daemon ${state.pid}: ${extractErrorMessage9(error)}`);
9356
9379
  }
9357
9380
  await stateStore.clear();
9358
9381
  }
@@ -9372,13 +9395,13 @@ async function killSessionsByRegistry(options = {}) {
9372
9395
  process.kill(record.pid, "SIGKILL");
9373
9396
  logger3.success(`Killed session process ${record.pid}`);
9374
9397
  } catch (error) {
9375
- logger3.warn(`Failed to kill session ${record.pid}: ${extractErrorMessage10(error)}`);
9398
+ logger3.warn(`Failed to kill session ${record.pid}: ${extractErrorMessage9(error)}`);
9376
9399
  }
9377
9400
  }
9378
9401
  return [{}, void 0];
9379
9402
  });
9380
9403
  } catch (error) {
9381
- logger3.warn(`Failed to kill sessions via registry: ${extractErrorMessage10(error)}`);
9404
+ logger3.warn(`Failed to kill sessions via registry: ${extractErrorMessage9(error)}`);
9382
9405
  }
9383
9406
  }
9384
9407
  async function findDaemonRelatedPids() {
@@ -9415,7 +9438,7 @@ async function registerDirectory(directoryPath, options) {
9415
9438
  options.logger.warn("Daemon was unavailable; storing directory locally.");
9416
9439
  } catch (error) {
9417
9440
  options.logger.warn(
9418
- `Failed to register ${normalized} with daemon: ${extractErrorMessage10(error)}`
9441
+ `Failed to register ${normalized} with daemon: ${extractErrorMessage9(error)}`
9419
9442
  );
9420
9443
  }
9421
9444
  const registry = new DirectoryRegistry();
@@ -9453,13 +9476,73 @@ async function waitForReady(client, options = {}) {
9453
9476
  logger3.warn("Timed out waiting for daemon readiness.");
9454
9477
  return null;
9455
9478
  }
9456
- function extractErrorMessage10(error) {
9479
+ function extractErrorMessage9(error) {
9457
9480
  if (error instanceof Error && error.message) {
9458
9481
  return error.message;
9459
9482
  }
9460
9483
  return String(error);
9461
9484
  }
9462
9485
 
9486
+ // src/commands/auth.ts
9487
+ function buildAuthCommand() {
9488
+ const auth = new Command("auth").description("Authenticate the Omnara CLI.");
9489
+ auth.command("login").description("Authenticate via the browser-based verification flow.").action(async () => {
9490
+ const store = new CredentialsStore();
9491
+ const backendUrl = resolveBackendUrl();
9492
+ try {
9493
+ const configStore = new DaemonConfigStore();
9494
+ await ensureMachineNameConfigured(configStore, logger);
9495
+ await performVerificationLogin({
9496
+ backendUrlOption: backendUrl,
9497
+ frontendUrlOption: resolveFrontendUrl(),
9498
+ store
9499
+ });
9500
+ } catch (error) {
9501
+ Sentry18.captureException(error);
9502
+ if (error instanceof ApiError) {
9503
+ console.error(`
9504
+ Request failed (HTTP ${error.status}): ${error.message}`);
9505
+ } else {
9506
+ console.error(`
9507
+ Login failed: ${extractErrorMessage10(error)}`);
9508
+ }
9509
+ process.exitCode = 1;
9510
+ }
9511
+ });
9512
+ auth.command("logout").description("Remove stored CLI credentials.").action(async () => {
9513
+ const store = new CredentialsStore();
9514
+ await store.clear();
9515
+ console.log("Credentials cleared.");
9516
+ });
9517
+ auth.command("status").description("Show stored CLI credential details.").action(async () => {
9518
+ const store = new CredentialsStore();
9519
+ const credentials = await store.load();
9520
+ if (!credentials) {
9521
+ console.log("No credentials stored.");
9522
+ return;
9523
+ }
9524
+ printStatus(credentials);
9525
+ });
9526
+ return auth;
9527
+ }
9528
+ function printStatus(credentials) {
9529
+ if (credentials.pat) {
9530
+ console.log("Authentication: PAT stored");
9531
+ return;
9532
+ }
9533
+ console.log("Authentication: Not configured");
9534
+ }
9535
+ function extractErrorMessage10(error) {
9536
+ if (error instanceof Error) {
9537
+ return error.message;
9538
+ }
9539
+ return String(error);
9540
+ }
9541
+
9542
+ // src/commands/daemon.ts
9543
+ import { Command as Command2 } from "commander";
9544
+ import * as Sentry24 from "@sentry/node";
9545
+
9463
9546
  // src/daemon/run-daemon.ts
9464
9547
  import * as Sentry23 from "@sentry/node";
9465
9548
 
@@ -9680,7 +9763,7 @@ function extractErrorMessage11(error) {
9680
9763
  // src/daemon/machine-supervisor.ts
9681
9764
  import { randomUUID as randomUUID7 } from "node:crypto";
9682
9765
  import os5 from "node:os";
9683
- import { setTimeout as delay3 } from "node:timers/promises";
9766
+ import { setTimeout as delay4 } from "node:timers/promises";
9684
9767
  import WebSocket2 from "ws";
9685
9768
  import * as Sentry21 from "@sentry/node";
9686
9769
 
@@ -17681,14 +17764,13 @@ var FALLBACK_NAME = pkg.name ?? "omnara";
17681
17764
  var FALLBACK_VERSION = pkg.version ?? "0.0.0";
17682
17765
  var FALLBACK_DESCRIPTION = pkg.description ?? "Command-line interface for Omnara.";
17683
17766
  var PACKAGE_NAME = true ? "omnara" : FALLBACK_NAME;
17684
- var PACKAGE_VERSION = true ? "0.5.0" : FALLBACK_VERSION;
17767
+ var PACKAGE_VERSION = true ? "0.5.1" : FALLBACK_VERSION;
17685
17768
  var PACKAGE_DESCRIPTION = true ? "Command-line interface for Omnara." : FALLBACK_DESCRIPTION;
17686
17769
 
17687
17770
  // src/daemon/machine-supervisor.ts
17688
17771
  var HEARTBEAT_INTERVAL_MS = 1e4;
17689
- var RECONNECT_DELAY_MS = 2e3;
17690
17772
  var LOGIN_WAIT_DELAY_MS2 = 3e4;
17691
- var CONNECT_TIMEOUT_MS = 3e4;
17773
+ var CONNECT_TIMEOUT_MS2 = 3e4;
17692
17774
  var DAEMON_VERSION = PACKAGE_VERSION ?? "0.0.0";
17693
17775
  var MachineSupervisor = class {
17694
17776
  backendUrl;
@@ -17770,11 +17852,13 @@ var MachineSupervisor = class {
17770
17852
  this.running = false;
17771
17853
  }
17772
17854
  async runLoop() {
17855
+ let retryDelayMs = INITIAL_RETRY_DELAY_MS;
17773
17856
  while (!this.stopRequested) {
17774
17857
  try {
17775
17858
  const identity = await this.registerMachine();
17776
17859
  this.clearLoginWait();
17777
17860
  await this.runWebsocket(identity);
17861
+ retryDelayMs = INITIAL_RETRY_DELAY_MS;
17778
17862
  } catch (error) {
17779
17863
  if (this.stopRequested) {
17780
17864
  break;
@@ -17789,11 +17873,14 @@ var MachineSupervisor = class {
17789
17873
  } else {
17790
17874
  this.logger.debug("Machine supervisor still waiting for login\u2026");
17791
17875
  }
17792
- await delay3(LOGIN_WAIT_DELAY_MS2);
17876
+ await delay4(LOGIN_WAIT_DELAY_MS2);
17793
17877
  } else {
17794
17878
  Sentry21.captureException(error);
17795
- this.logger.warn(`Machine supervisor error: ${error.message}`);
17796
- await delay3(RECONNECT_DELAY_MS);
17879
+ this.logger.warn(
17880
+ `Machine supervisor error: ${error.message}. Retrying in ${Math.round(retryDelayMs / 1e3)}s.`
17881
+ );
17882
+ await delay4(retryDelayMs);
17883
+ retryDelayMs = calculateBackoff(retryDelayMs);
17797
17884
  }
17798
17885
  }
17799
17886
  }
@@ -17883,9 +17970,13 @@ var MachineSupervisor = class {
17883
17970
  this.clearHeartbeatTimer();
17884
17971
  this.websocket = null;
17885
17972
  try {
17886
- ws.removeAllListeners();
17887
- if (ws.readyState === WebSocket2.OPEN || ws.readyState === WebSocket2.CONNECTING) {
17973
+ if (ws.readyState === WebSocket2.CONNECTING) {
17974
+ ws.terminate();
17975
+ } else if (ws.readyState === WebSocket2.OPEN) {
17976
+ ws.removeAllListeners();
17888
17977
  ws.close();
17978
+ } else {
17979
+ ws.removeAllListeners();
17889
17980
  }
17890
17981
  } catch {
17891
17982
  }
@@ -17897,7 +17988,7 @@ var MachineSupervisor = class {
17897
17988
  };
17898
17989
  connectTimer = setTimeout(() => {
17899
17990
  finish(new Error("machine websocket connection timed out"));
17900
- }, CONNECT_TIMEOUT_MS);
17991
+ }, CONNECT_TIMEOUT_MS2);
17901
17992
  ws.on("open", () => {
17902
17993
  if (connectTimer) {
17903
17994
  clearTimeout(connectTimer);
@@ -18100,7 +18191,7 @@ var MachineSupervisor = class {
18100
18191
  return;
18101
18192
  }
18102
18193
  }
18103
- await delay3(250);
18194
+ await delay4(250);
18104
18195
  }
18105
18196
  throw new Error("Machine has not finished registering yet. Try again in a few seconds.");
18106
18197
  }
@@ -18418,6 +18509,8 @@ function buildDaemonCommand() {
18418
18509
  daemon.command("start").description("Start the daemon if it is not already running.").option("--no-wait", "Do not wait for the daemon to become ready").action(async (options) => {
18419
18510
  const backendUrl = resolveBackendUrl();
18420
18511
  const frontendUrl = resolveFrontendUrl();
18512
+ const configStore = new DaemonConfigStore();
18513
+ await ensureMachineNameConfigured(configStore, logger);
18421
18514
  await ensureCliCredentials({ backendUrl, frontendUrl, logger });
18422
18515
  await startDaemon({
18423
18516
  logger,
@@ -18734,6 +18827,8 @@ async function launchSessionViaDaemon(options = {}) {
18734
18827
  const backendUrl = resolveBackendUrl(options.backendUrl);
18735
18828
  const frontendUrl = resolveFrontendUrl(options.frontendUrl);
18736
18829
  try {
18830
+ const configStore = new DaemonConfigStore();
18831
+ await ensureMachineNameConfigured(configStore, logger);
18737
18832
  await ensureCliCredentials({ backendUrl, frontendUrl, logger });
18738
18833
  await ensureDaemonForSession({
18739
18834
  cwd: process.cwd(),