omnara 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -21,8 +21,128 @@ import "@anthropic-ai/claude-agent-sdk";
21
21
  import fs from "fs";
22
22
  import * as Sentry from "@sentry/node";
23
23
  import {
24
- query
24
+ query as query2
25
25
  } from "@anthropic-ai/claude-agent-sdk";
26
+
27
+ // src/utils/claude-code-check.ts
28
+ import chalk from "chalk";
29
+ import { query } from "@anthropic-ai/claude-agent-sdk";
30
+ import { execSync } from "child_process";
31
+
32
+ // src/utils/executable.ts
33
+ function isBunSingleFileExecutable() {
34
+ const arg1 = process.argv[1] ?? "";
35
+ return arg1.startsWith("/$bunfs/");
36
+ }
37
+ function getSpawnArgs(commandArgs) {
38
+ if (isBunSingleFileExecutable()) {
39
+ return commandArgs;
40
+ }
41
+ const scriptPath = process.argv[1] ?? "";
42
+ return [scriptPath, ...commandArgs];
43
+ }
44
+ function getExecutablePath() {
45
+ return process.execPath;
46
+ }
47
+
48
+ // src/utils/claude-code-check.ts
49
+ function claudeExecutablePath() {
50
+ if (!isBunSingleFileExecutable()) {
51
+ return {};
52
+ }
53
+ try {
54
+ const command = process.platform === "win32" ? "where claude" : "which claude";
55
+ const result = execSync(command, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
56
+ const foundPath = result.trim().split("\n")[0];
57
+ if (foundPath) {
58
+ return { pathToClaudeCodeExecutable: foundPath };
59
+ }
60
+ } catch {
61
+ }
62
+ console.error(
63
+ "[Omnara] Claude Code executable not found in PATH. Please ensure Claude Code is installed: https://claude.ai/download"
64
+ );
65
+ return {};
66
+ }
67
+ async function performClaudeCodeCheck() {
68
+ if (process.env.OMNARA_SKIP_CLAUDE_CHECK === "true") {
69
+ return true;
70
+ }
71
+ const indicator = createClaudeCheckIndicator();
72
+ try {
73
+ indicator?.start();
74
+ for await (const message of query({
75
+ prompt: "Respond with one word.",
76
+ options: {
77
+ maxTurns: 1,
78
+ model: "haiku",
79
+ settingSources: ["user", "project", "local"],
80
+ // Required for Bun single-file executable compatibility
81
+ ...claudeExecutablePath()
82
+ }
83
+ })) {
84
+ void message;
85
+ }
86
+ indicator?.stop("ok");
87
+ return true;
88
+ } catch {
89
+ indicator?.stop("fail");
90
+ console.log(
91
+ chalk.red(
92
+ "\nError: Omnara couldn't run because it relies on Claude Code, which isn't installed or authenticated. Try running `claude /login`.\n"
93
+ )
94
+ );
95
+ return false;
96
+ }
97
+ }
98
+ function createClaudeCheckIndicator() {
99
+ if (!process.stdout.isTTY) {
100
+ return {
101
+ start() {
102
+ console.log("Checking Claude Code auth...");
103
+ },
104
+ stop(status) {
105
+ if (status !== "ok") {
106
+ console.log("Claude Code auth failed.");
107
+ }
108
+ }
109
+ };
110
+ }
111
+ const label = "Checking Claude Code auth";
112
+ const frames = ["-", "\\", "|", "/"];
113
+ let index = 0;
114
+ let interval = null;
115
+ let running = false;
116
+ const stopLine = (status) => {
117
+ if (!running) {
118
+ return;
119
+ }
120
+ running = false;
121
+ if (interval) {
122
+ clearInterval(interval);
123
+ }
124
+ const statusText = status === "ok" ? chalk.green("OK") : chalk.red("FAILED");
125
+ const padding = " ";
126
+ process.stdout.write(`\r${label} ${statusText}${padding}
127
+ `);
128
+ };
129
+ return {
130
+ start() {
131
+ if (running) {
132
+ return;
133
+ }
134
+ running = true;
135
+ process.stdout.write(`${label} ...`);
136
+ interval = setInterval(() => {
137
+ process.stdout.write(`\r${label} ${frames[index]} `);
138
+ index = (index + 1) % frames.length;
139
+ }, 120);
140
+ },
141
+ stop: stopLine
142
+ };
143
+ }
144
+
145
+ // src/orchestrator/ai-client.ts
26
146
  var MODEL_MAP = {
27
147
  haiku: "claude-haiku-4-5-20251001",
28
148
  sonnet: "claude-sonnet-4-5-20250929",
@@ -57,9 +177,11 @@ var BASE_ALLOWED_WORKER_TOOLS = [
57
177
  "Skill",
58
178
  "SlashCommand"
59
179
  ];
180
+ var INTERRUPT_TIMEOUT_MS = 5e3;
60
181
  var AIClient = class {
61
182
  baseOptions;
62
183
  query;
184
+ abortController;
63
185
  currentModel = "sonnet";
64
186
  constructor(config) {
65
187
  const defaultOptions = {
@@ -68,7 +190,9 @@ var AIClient = class {
68
190
  cwd: process.cwd(),
69
191
  includePartialMessages: false,
70
192
  allowedTools: [...BASE_ALLOWED_ORCHESTRATOR_TOOLS],
71
- settingSources: ["user", "project", "local"]
193
+ settingSources: ["user", "project", "local"],
194
+ // Required for Bun single-file executable compatibility
195
+ ...claudeExecutablePath()
72
196
  };
73
197
  this.baseOptions = {
74
198
  ...defaultOptions,
@@ -80,29 +204,50 @@ var AIClient = class {
80
204
  this.ensureWorkingDirectory(this.baseOptions.cwd);
81
205
  }
82
206
  async *streamUserMessage(prompt2, overrides) {
207
+ this.abortController = new AbortController();
83
208
  const q = this.createQuery(prompt2, overrides);
84
209
  this.query = q;
85
- for await (const message of q) {
86
- yield message;
210
+ try {
211
+ for await (const message of q) {
212
+ yield message;
213
+ }
214
+ } finally {
215
+ this.query = void 0;
216
+ this.abortController = void 0;
87
217
  }
88
- this.query = void 0;
89
218
  }
90
219
  createQuery(prompt2, overrides) {
91
220
  const merged = {
92
221
  ...this.baseOptions,
93
- ...overrides
222
+ ...overrides,
223
+ abortController: this.abortController
94
224
  };
95
225
  this.ensureWorkingDirectory(merged.cwd);
96
- return query({
226
+ return query2({
97
227
  prompt: prompt2,
98
228
  options: merged
99
229
  });
100
230
  }
101
231
  async cancel() {
102
- if (this.query) {
103
- await this.query.interrupt();
104
- } else {
105
- console.warn("No query to interrupt. Ignoring");
232
+ if (!this.query) {
233
+ console.warn("[AIClient] No query to interrupt. Ignoring");
234
+ return;
235
+ }
236
+ try {
237
+ const interruptPromise = this.query.interrupt();
238
+ const timeoutPromise = new Promise(
239
+ (resolve) => setTimeout(() => resolve("timeout"), INTERRUPT_TIMEOUT_MS)
240
+ );
241
+ const result = await Promise.race([interruptPromise, timeoutPromise]);
242
+ if (result === "timeout") {
243
+ console.warn(
244
+ `[AIClient] interrupt() timed out after ${INTERRUPT_TIMEOUT_MS}ms, falling back to abort`
245
+ );
246
+ this.abortController?.abort();
247
+ }
248
+ } catch (error) {
249
+ console.warn("[AIClient] interrupt() failed, falling back to abort:", error);
250
+ this.abortController?.abort();
106
251
  }
107
252
  }
108
253
  isGenerating() {
@@ -156,7 +301,7 @@ import { simpleGit as simpleGit2 } from "simple-git";
156
301
 
157
302
  // src/orchestrator/session.ts
158
303
  import {
159
- query as query2
304
+ query as query3
160
305
  } from "@anthropic-ai/claude-agent-sdk";
161
306
  import { setTimeout as delay } from "node:timers/promises";
162
307
 
@@ -480,8 +625,22 @@ function createEnvelope(sessionId, content, senderId, metadata, payloadMetadata)
480
625
  }
481
626
  function toMessageEnvelopes(message, sessionId) {
482
627
  const envelopes = [];
628
+ const messageAny = message;
629
+ const subagentSessionId = typeof messageAny.session_id === "string" ? messageAny.session_id : void 0;
630
+ const parentToolUseId = typeof messageAny.parent_tool_use_id === "string" ? messageAny.parent_tool_use_id : void 0;
631
+ const buildSubagentPayloadMetadata = () => {
632
+ const metadata = {};
633
+ if (parentToolUseId) {
634
+ metadata.parent_tool_use_id = parentToolUseId;
635
+ }
636
+ if (subagentSessionId && subagentSessionId !== sessionId) {
637
+ metadata.subagent_session_id = subagentSessionId;
638
+ }
639
+ return Object.keys(metadata).length > 0 ? metadata : void 0;
640
+ };
483
641
  switch (message.type) {
484
642
  case "assistant": {
643
+ const subagentMetadata = buildSubagentPayloadMetadata();
485
644
  for (const block of message.message.content) {
486
645
  if (isTextBlock(block)) {
487
646
  const content = {
@@ -489,9 +648,15 @@ function toMessageEnvelopes(message, sessionId) {
489
648
  text: block.text
490
649
  };
491
650
  envelopes.push(
492
- createEnvelope(sessionId, content, sessionId, {
493
- role: "assistant"
494
- })
651
+ createEnvelope(
652
+ sessionId,
653
+ content,
654
+ sessionId,
655
+ {
656
+ role: "assistant"
657
+ },
658
+ subagentMetadata
659
+ )
495
660
  );
496
661
  } else if (isToolUseBlock(block)) {
497
662
  const content = {
@@ -501,15 +666,29 @@ function toMessageEnvelopes(message, sessionId) {
501
666
  arguments: normalizeToolArguments(block.input)
502
667
  };
503
668
  envelopes.push(
504
- createEnvelope(sessionId, content, sessionId, {
505
- role: "assistant"
506
- })
669
+ createEnvelope(
670
+ sessionId,
671
+ content,
672
+ sessionId,
673
+ {
674
+ role: "assistant"
675
+ },
676
+ subagentMetadata
677
+ )
507
678
  );
508
679
  }
509
680
  }
510
681
  break;
511
682
  }
512
683
  case "user": {
684
+ if (parentToolUseId) {
685
+ break;
686
+ }
687
+ const firstBlock = message.message.content[0];
688
+ if (isTextBlock(firstBlock) && (firstBlock.text === "[Request interrupted by user]" || firstBlock.text === "[Request interrupted by user for tool use]")) {
689
+ break;
690
+ }
691
+ const subagentMetadata = buildSubagentPayloadMetadata();
513
692
  for (const block of message.message.content) {
514
693
  if (isToolResultBlock(block)) {
515
694
  const content = {
@@ -519,9 +698,15 @@ function toMessageEnvelopes(message, sessionId) {
519
698
  is_error: Boolean(block.is_error)
520
699
  };
521
700
  envelopes.push(
522
- createEnvelope(sessionId, content, sessionId, {
523
- role: "assistant"
524
- })
701
+ createEnvelope(
702
+ sessionId,
703
+ content,
704
+ sessionId,
705
+ {
706
+ role: "assistant"
707
+ },
708
+ subagentMetadata
709
+ )
525
710
  );
526
711
  } else if (isTextBlock(block)) {
527
712
  const content = {
@@ -529,9 +714,15 @@ function toMessageEnvelopes(message, sessionId) {
529
714
  text: block.text
530
715
  };
531
716
  envelopes.push(
532
- createEnvelope(sessionId, content, sessionId, {
533
- role: "user"
534
- })
717
+ createEnvelope(
718
+ sessionId,
719
+ content,
720
+ sessionId,
721
+ {
722
+ role: "user"
723
+ },
724
+ subagentMetadata
725
+ )
535
726
  );
536
727
  }
537
728
  }
@@ -544,6 +735,13 @@ function toMessageEnvelopes(message, sessionId) {
544
735
  type: "text",
545
736
  text: delta
546
737
  };
738
+ const streamPayloadMetadata = {
739
+ stream_delta: true
740
+ };
741
+ const subagentMeta = buildSubagentPayloadMetadata();
742
+ if (subagentMeta) {
743
+ Object.assign(streamPayloadMetadata, subagentMeta);
744
+ }
547
745
  envelopes.push(
548
746
  createEnvelope(
549
747
  sessionId,
@@ -552,9 +750,7 @@ function toMessageEnvelopes(message, sessionId) {
552
750
  {
553
751
  role: "assistant"
554
752
  },
555
- {
556
- stream_delta: true
557
- }
753
+ streamPayloadMetadata
558
754
  )
559
755
  );
560
756
  }
@@ -661,6 +857,7 @@ var Session = class {
661
857
  sessionInitSubscribers = [];
662
858
  queue = [];
663
859
  generating = false;
860
+ waitingForMessage = null;
664
861
  announcementPromise = null;
665
862
  announced = false;
666
863
  shouldSummarize = false;
@@ -682,19 +879,29 @@ var Session = class {
682
879
  sessionId: this.id,
683
880
  queueLength: this.queue.length
684
881
  });
685
- if (!this.generating && !this.ai.isGenerating()) {
882
+ if (this.waitingForMessage) {
883
+ const resolver = this.waitingForMessage;
884
+ this.waitingForMessage = null;
885
+ const batch = this.queue.splice(0, this.queue.length);
886
+ void this.emitProgress("running", "Generating response");
887
+ resolver(batch);
888
+ } else if (!this.generating && !this.ai.isGenerating()) {
686
889
  this.startGenerate();
687
890
  }
688
891
  }
689
892
  async *[Symbol.asyncIterator]() {
690
893
  while (true) {
691
- if (this.queue.length === 0) {
692
- logger.debug("[Session] Emptied queue", {
894
+ let batch;
895
+ if (this.queue.length > 0) {
896
+ batch = this.queue.splice(0, this.queue.length);
897
+ } else {
898
+ logger.debug("[Session] Iterator waiting for messages", {
693
899
  sessionId: this.id
694
900
  });
695
- return;
901
+ batch = await new Promise((resolve) => {
902
+ this.waitingForMessage = resolve;
903
+ });
696
904
  }
697
- const batch = this.queue.splice(0, this.queue.length);
698
905
  logger.debug("[Session] Yielding batch to SDK", {
699
906
  sessionId: this.id,
700
907
  batchSize: batch.length
@@ -727,28 +934,40 @@ var Session = class {
727
934
  const summaryTextBlocks = [];
728
935
  let resultMessage = null;
729
936
  await this.emitProgress("running", "Generating response");
730
- for await (const message of this.ai.streamUserMessage(this, {
731
- ...this.sdkSessionIdentifier ? { resume: this.sdkSessionIdentifier } : {}
732
- })) {
733
- this.captureSdkSessionId(message);
734
- if (message.type === "assistant") {
735
- this.collectAssistantSummaryBlocks(message, summaryTextBlocks);
736
- }
737
- if (message.type === "result") {
738
- resultMessage = message;
937
+ try {
938
+ for await (const message of this.ai.streamUserMessage(this, {
939
+ ...this.sdkSessionIdentifier ? { resume: this.sdkSessionIdentifier } : {}
940
+ })) {
941
+ this.captureSdkSessionId(message);
942
+ if (message.type === "assistant") {
943
+ this.collectAssistantSummaryBlocks(message, summaryTextBlocks);
944
+ }
945
+ if (message.type === "result") {
946
+ resultMessage = message;
947
+ this.resetRetryState();
948
+ const complete = await this.buildCompletionEnvelope(resultMessage, summaryTextBlocks);
949
+ await Promise.all([
950
+ this.emitProgress("idle", "Ready for next message"),
951
+ this.dispatchEnvelope(complete)
952
+ ]);
953
+ summaryTextBlocks.length = 0;
954
+ resultMessage = null;
955
+ }
956
+ for (const envelope of toMessageEnvelopes(message, this.id)) {
957
+ await this.dispatchEnvelope(envelope);
958
+ }
739
959
  }
740
- for (const envelope of toMessageEnvelopes(message, this.id)) {
741
- await this.dispatchEnvelope(envelope);
960
+ } catch (error) {
961
+ if (error instanceof Error && error.name === "AbortError") {
962
+ logger.info("[Session] Generation aborted", { sessionId: this.id });
963
+ await this.emitProgress("idle", "Generation cancelled");
964
+ return;
742
965
  }
966
+ throw error;
743
967
  }
744
- this.resetRetryState();
745
- const complete = await this.buildCompletionEnvelope(resultMessage, summaryTextBlocks);
746
- await Promise.all([
747
- this.emitProgress("idle", "Ready for next message"),
748
- this.dispatchEnvelope(complete)
749
- ]);
750
968
  } finally {
751
969
  this.generating = false;
970
+ this.waitingForMessage = null;
752
971
  logger.debug("[Session] Generation finished", {
753
972
  sessionId: this.id,
754
973
  pendingMessages: this.queue.length
@@ -868,7 +1087,7 @@ var Session = class {
868
1087
  }
869
1088
  const summarizationPrompt = `<assistant-output>${combined}<assistant-output>`;
870
1089
  let summary = "";
871
- for await (const message of query2({
1090
+ for await (const message of query3({
872
1091
  prompt: summarizationPrompt,
873
1092
  options: {
874
1093
  systemPrompt: `Provide a 3-5 sentence summary of the actions taken by the assistant.
@@ -878,7 +1097,8 @@ ONLY include content like key takeaways, actions (do NOT specify by whom), futur
878
1097
  This content should be sufficient to be inserted into something like a commit message or pull request description.`,
879
1098
  maxTurns: 1,
880
1099
  model: "haiku",
881
- allowedTools: []
1100
+ allowedTools: [],
1101
+ ...claudeExecutablePath()
882
1102
  }
883
1103
  })) {
884
1104
  if (message.type === "assistant") {
@@ -941,7 +1161,7 @@ This content should be sufficient to be inserted into something like a commit me
941
1161
  }
942
1162
  const prompt2 = this.buildCommitMessagePrompt(conversationContext, diff);
943
1163
  let commitMessage = "";
944
- for await (const message of query2({
1164
+ for await (const message of query3({
945
1165
  prompt: prompt2,
946
1166
  options: {
947
1167
  systemPrompt: `You generate clear, concise git commit messages based on conversation context and code changes.
@@ -956,7 +1176,8 @@ Guidelines:
956
1176
  Return ONLY the commit message, no explanation or markdown formatting.`,
957
1177
  maxTurns: 1,
958
1178
  model: "haiku",
959
- allowedTools: []
1179
+ allowedTools: [],
1180
+ ...claudeExecutablePath()
960
1181
  }
961
1182
  })) {
962
1183
  if (message.type === "assistant") {
@@ -2016,13 +2237,11 @@ Orchestrator: [message_worker: task="refactor database", message="User wants to
2016
2237
  });
2017
2238
  }
2018
2239
 
2019
- // src/orchestrator/orchestrator-prompt.ts
2020
- import { readFileSync } from "fs";
2021
- var orchestratorPromptUrl = new URL("./orchestrator-prompt.md", import.meta.url);
2022
- var ORCHESTRATOR_SYSTEM_PROMPT = readFileSync(orchestratorPromptUrl, "utf-8");
2240
+ // src/orchestrator/orchestrator-prompt.md
2241
+ var orchestrator_prompt_default = '# Role: Task Orchestrator\n\nYou are Omnara, an orchestrator agent. Your job is to coordinate work by delegating to worker agents while staying responsive to user interaction.\n\n## The Mental Model\n\n**You stay zoomed out. Workers go zoomed in.**\n\nYou are a coordinator who maintains the big picture. Workers are specialists who dive deep into specific domains. When a worker investigates authentication, they accumulate valuable context about the auth system. That context is an asset - reuse it.\n\n**Context is valuable. Accuracy over speed.**\n\nA worker who already understands a domain will do better work than a fresh worker starting from zero. Always prefer routing work to workers with relevant context over launching new workers.\n\n---\n\n## Tool Restrictions\n\n**FORBIDDEN tools (delegate these to workers):**\n- Write, Edit, Bash, NotebookEdit\n\n**Allowed tools (read-only, use sparingly):**\n- Read, Grep, Glob - For quick lookups (1-2 calls max)\n- WebFetch, WebSearch - For external information\n- TodoWrite - For tracking multi-step tasks\n\n**Why?** Using forbidden tools blocks your context. Workers execute operations without blocking you. Your job is to coordinate, not implement.\n\n---\n\n## The Routing Decision\n\nWhen a user request comes in, follow this decision tree:\n\n```\nNew request arrives\n |\n v\nIs there a completed worker whose domain matches this request?\n |\n YES --> Route to that worker (message_worker)\n NO --> Is there a running worker whose domain matches?\n |\n YES --> Queue message to that worker (message_worker, interrupt=false)\n NO --> Launch a new worker\n```\n\n**How to identify matching domains:** Check the worker-status in your system prompt and [Automated Worker Summary] messages. They show task names, status, worktree, and completion summaries.\n\n### When to Route to Existing Workers\n\n**Workers evolve over time.** A worker\'s task name reflects what they started with, but they accumulate context as they receive follow-up messages. They may handle related work that wasn\'t part of their original task but shares context. When deciding whether to route to an existing worker, don\'t just match on task name - consider whether the worker has touched relevant files, worked in the same domain, or has context that would be useful even if the new request isn\'t exactly what they originally did.\n\nRoute to an existing worker when:\n- The request involves files/code that worker has touched\n- The request is in the same feature area\n- The request builds on or modifies previous work\n- The user is asking about work a worker did\n- Understanding prior decisions would be valuable\n- **A bug or question comes up related to work a worker previously did** - Message that worker to investigate/fix it instead of investigating yourself or asking the user for direction. The worker has the context and will be more effective.\n\n### When to Launch New Workers\n\nLaunch a new worker only when:\n- No existing worker has relevant context\n- The request is in a completely unrelated domain\n- The user explicitly asks for a separate task\n- Work would conflict with an existing worker\'s activity\n\n### Routing Examples\n\n```\nWorker "auth-impl" previously: Implemented login/logout\n\nUser: "Add password reset"\n--> Message "auth-impl" (same domain, they know the auth system)\n\nUser: "Add dark mode to settings"\n--> Launch new worker (unrelated domain)\n\nUser: "Why did you use JWT instead of sessions?"\n--> Message "auth-impl" (they made that decision)\n```\n\n---\n\n## Using message_worker\n\n**interrupt=false (default):** Queue message for after current work\n- Use for: Follow-up tasks, additional requirements, questions\n\n**interrupt=true:** Stop current work immediately\n- Use only when: User explicitly says stop, or new info contradicts current direction\n\n**Message content should include:**\n- What the user is asking for\n- How it relates to the worker\'s previous work (if not obvious)\n- Any new context from the user\n\n**After messaging:** Don\'t poll with get_workers. You\'ll be automatically notified when the worker completes.\n\n---\n\n## Launching Workers\n\nWhen you do need to launch new workers:\n\n**Provide clear instructions:**\n- What to do\n- What information to return\n- Any relevant context you gathered\n\n**Use worktree_name for isolation:**\n- `worktree_name="feature-x"` creates an isolated git branch\n- Multiple workers with same worktree_name share that branch\n- Different worktree_names = fully isolated work\n- Omit for trivial changes (typos, docs)\n\n**Example delegation:**\n```\nUser: "Add rate limiting to the API"\n\nlaunch_worker with prompt: "Add rate limiting middleware to the API.\nApply globally with 100 requests per 15 minutes.\nDocument the configuration in comments."\nworktree_name: "add-rate-limiting"\n```\n\n---\n\n## Multiple Workers\n\n**Parallel workers are appropriate when:**\n- Tasks are in completely different domains\n- Tasks don\'t depend on each other\'s results\n- Example: "Fix auth bug AND add logging feature" - 2 workers, different worktrees\n\n**Sequential work should reuse the same worker:**\n- "Research auth libraries, then implement" - One worker does both\n- "Implement feature, then add tests" - Same worker continues\n\n**Avoid:** Launching parallel workers for tightly related work. The workers won\'t share context and may make inconsistent decisions.\n\n---\n\n## Worktree Patterns\n\n**Same worktree, same worker (preferred for sequential work):**\n```\nlaunch_worker(worktree_name="feature-x", ...)\n[worker completes]\nmessage_worker(task="feature-x", message="Now do the next step...")\n```\nContext preserved. Worker knows the domain.\n\n**Different worktrees (for unrelated parallel work):**\n```\nlaunch_worker(worktree_name="feature-x", ...)\nlaunch_worker(worktree_name="feature-y", ...)\n```\nFully isolated. Different features/branches.\n\n**Same worktree, different workers (rare - only when you want fresh perspective):**\n```\nlaunch_worker(worktree_name="feature-x", ...)\n[worker completes]\nlaunch_worker(worktree_name="feature-x", ...) # New worker, context discarded\n```\nUse sparingly. Loses accumulated knowledge.\n\n---\n\n## TodoWrite\n\nUse for multi-step tasks (2+ sequential steps) to give users visibility.\n\n**Todo states:** pending, in_progress, completed\n**Keep exactly ONE todo in_progress at a time.**\n\n```\nUser: "Add OAuth, then test it"\n\nTodoWrite:\n1. "Implement OAuth" - in_progress\n2. "Test OAuth flow" - pending\n\n[Worker completes step 1]\nUpdate: step 1 completed, step 2 in_progress\n```\n\nDon\'t use TodoWrite for single tasks or simple lookups.\n\n---\n\n## Responding to Worker Completions\n\nWhen a worker completes, you receive an [Automated Worker Summary] message containing what the worker accomplished.\n\n**Your response should:**\n1. Report ONLY the new completion (not previously reported ones)\n2. Keep it brief: "The auth worker completed. Login/logout endpoints are implemented."\n3. Note next steps if applicable\n\n**Don\'t:** Re-summarize all workers. Re-explain previous completions. Provide comprehensive recaps unless asked.\n\n---\n\n## Remember\n\n**Your role:** Coordinator, not implementer. Stay zoomed out.\n\n**Tool restrictions:** Never use Write, Edit, Bash, NotebookEdit directly. Always delegate.\n\n**Routing priority:** Existing workers with context > New workers. Context is valuable.\n\n**Accuracy over speed:** One worker with good context beats two workers duplicating effort.\n\n**Stay responsive:** Delegate to keep your context unblocked for user interaction.\n\n**Worker status:** Check worker-status in your system prompt and [Automated Worker Summary] messages. Don\'t poll with get_workers.\n\n**When in doubt:** Route to an existing worker if one has relevant context. Otherwise, delegate to a new worker.\n';
2023
2242
 
2024
2243
  // src/orchestrator/utils/completions-manager.ts
2025
- import { query as query3 } from "@anthropic-ai/claude-agent-sdk";
2244
+ import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
2026
2245
  import { glob } from "glob";
2027
2246
  import * as path6 from "path";
2028
2247
  import * as Sentry4 from "@sentry/node";
@@ -2034,9 +2253,14 @@ var CompletionsManager = class {
2034
2253
  return this.slashCommandsCache.slashCommands;
2035
2254
  }
2036
2255
  let commands = [];
2037
- for await (const message of query3({
2256
+ for await (const message of query4({
2038
2257
  prompt: "Respond with one word.",
2039
- options: { maxTurns: 1, model: "haiku", settingSources: ["user", "project", "local"] }
2258
+ options: {
2259
+ maxTurns: 1,
2260
+ model: "haiku",
2261
+ settingSources: ["user", "project", "local"],
2262
+ ...claudeExecutablePath()
2263
+ }
2040
2264
  })) {
2041
2265
  if (message.type === "system" && message.subtype === "init") {
2042
2266
  console.log("Fetched available slash commands:", message.slash_commands);
@@ -2269,7 +2493,7 @@ var AgentOrchestrator = class {
2269
2493
  mcpServers: {
2270
2494
  worker: this.workerMcpServer
2271
2495
  },
2272
- systemPrompt: ORCHESTRATOR_SYSTEM_PROMPT
2496
+ systemPrompt: orchestrator_prompt_default
2273
2497
  }
2274
2498
  }));
2275
2499
  this.aiClient = mainClientFactory();
@@ -2563,6 +2787,33 @@ var AgentOrchestrator = class {
2563
2787
  commit_sha: commitSha
2564
2788
  };
2565
2789
  }
2790
+ if (req.action === "commit_checkout") {
2791
+ try {
2792
+ const currentBranch = await git.revparse(["--abbrev-ref", "HEAD"]);
2793
+ const branchTrimmed = currentBranch.trim();
2794
+ const repoPath = this.workerManager.repoPath;
2795
+ const mainGit = simpleGit2(repoPath);
2796
+ await mainGit.raw(["checkout", "-d", branchTrimmed]);
2797
+ logger2.info(`[AgentOrchestrator] Checked out ${branchTrimmed} in main worktree`);
2798
+ return {
2799
+ type: "git_write",
2800
+ success: true,
2801
+ message: `Committed changes. The main workspace checked out the changes on this branch.`,
2802
+ commit_sha: commitSha
2803
+ };
2804
+ } catch (checkoutError) {
2805
+ logger2.error("[AgentOrchestrator] Failed to checkout in main worktree", {
2806
+ error: checkoutError
2807
+ });
2808
+ return {
2809
+ type: "git_write",
2810
+ success: true,
2811
+ message: `Committed successfully but failed to checkout in main worktree: ${checkoutError instanceof Error ? checkoutError.message : String(checkoutError)}`,
2812
+ commit_sha: commitSha,
2813
+ error: `Checkout failed: ${checkoutError instanceof Error ? checkoutError.message : String(checkoutError)}`
2814
+ };
2815
+ }
2816
+ }
2566
2817
  return {
2567
2818
  type: "git_write",
2568
2819
  success: true,
@@ -3022,7 +3273,7 @@ var AgentOrchestrator = class {
3022
3273
  const statusSnippet = this.buildCurrentWorkerStatus();
3023
3274
  return `${statusSnippet}
3024
3275
 
3025
- ${ORCHESTRATOR_SYSTEM_PROMPT}`;
3276
+ ${orchestrator_prompt_default}`;
3026
3277
  }
3027
3278
  buildCurrentWorkerStatus() {
3028
3279
  const workers = this.workerManager.listWorkers();
@@ -3090,7 +3341,7 @@ function extractSummary(complete) {
3090
3341
  // src/relay/relay-bridge.ts
3091
3342
  import { randomUUID as randomUUID5 } from "crypto";
3092
3343
  import EventEmitter from "events";
3093
- import WebSocket from "ws";
3344
+ import WebSocket2 from "ws";
3094
3345
  import { simpleGit as simpleGit3 } from "simple-git";
3095
3346
  import * as Sentry11 from "@sentry/node";
3096
3347
 
@@ -3526,7 +3777,7 @@ var control_message_schema_default = {
3526
3777
  },
3527
3778
  GitWriteRequest: {
3528
3779
  additionalProperties: false,
3529
- description: 'Request to commit and optionally push changes.\n\nThe action field specifies what operation to perform:\n- "commit": Create a git commit with a generated message\n- "commit_push": Create a commit and push to origin\n- "commit_push_pr": Create a commit, push, and create a PR (not yet implemented)',
3780
+ description: 'Request to commit and optionally push changes.\n\nThe action field specifies what operation to perform:\n- "commit": Create a git commit with a generated message\n- "commit_push": Create a commit and push to origin\n- "commit_push_pr": Create a commit, push, and create a PR (not yet implemented)\n- "commit_checkout": Create a commit and checkout the branch in the main worktree',
3530
3781
  properties: {
3531
3782
  type: {
3532
3783
  const: "git_write",
@@ -3538,7 +3789,8 @@ var control_message_schema_default = {
3538
3789
  enum: [
3539
3790
  "commit",
3540
3791
  "commit_push",
3541
- "commit_push_pr"
3792
+ "commit_push_pr",
3793
+ "commit_checkout"
3542
3794
  ],
3543
3795
  title: "Action",
3544
3796
  type: "string"
@@ -4620,12 +4872,23 @@ var machine_message_schema_default = {
4620
4872
  type: "string"
4621
4873
  },
4622
4874
  method: {
4623
- const: "start_session",
4875
+ enum: [
4876
+ "start_session",
4877
+ "stop_session"
4878
+ ],
4624
4879
  title: "Method",
4625
4880
  type: "string"
4626
4881
  },
4627
4882
  payload: {
4628
- $ref: "#/$defs/MachineStartSessionPayload"
4883
+ anyOf: [
4884
+ {
4885
+ $ref: "#/$defs/MachineStartSessionPayload"
4886
+ },
4887
+ {
4888
+ $ref: "#/$defs/MachineStopSessionPayload"
4889
+ }
4890
+ ],
4891
+ title: "Payload"
4629
4892
  }
4630
4893
  },
4631
4894
  required: [
@@ -4799,6 +5062,22 @@ var machine_message_schema_default = {
4799
5062
  title: "MachineStatus",
4800
5063
  type: "string"
4801
5064
  },
5065
+ MachineStopSessionPayload: {
5066
+ additionalProperties: false,
5067
+ description: "Payload instructing the daemon to terminate a running session.",
5068
+ properties: {
5069
+ user_session_id: {
5070
+ format: "uuid",
5071
+ title: "User Session Id",
5072
+ type: "string"
5073
+ }
5074
+ },
5075
+ required: [
5076
+ "user_session_id"
5077
+ ],
5078
+ title: "MachineStopSessionPayload",
5079
+ type: "object"
5080
+ },
4802
5081
  WorkspaceConfig: {
4803
5082
  additionalProperties: false,
4804
5083
  description: "Workspace-level configuration persisted alongside metadata.",
@@ -7487,7 +7766,7 @@ function tryOpenBrowser(url) {
7487
7766
  }
7488
7767
 
7489
7768
  // src/auth/login-flow.ts
7490
- import chalk from "chalk";
7769
+ import chalk2 from "chalk";
7491
7770
  async function performVerificationLogin({
7492
7771
  backendUrlOption,
7493
7772
  frontendUrlOption,
@@ -7496,7 +7775,7 @@ async function performVerificationLogin({
7496
7775
  const backendUrl = resolveBackendUrl(backendUrlOption);
7497
7776
  const frontendUrl = resolveFrontendUrl(frontendUrlOption);
7498
7777
  const client = new ApiClient({ baseUrl: backendUrl });
7499
- console.log(chalk.yellow("Authenticating CLI\u2026"));
7778
+ console.log(chalk2.yellow("Authenticating CLI\u2026"));
7500
7779
  const loopback = await LoopbackListener.create();
7501
7780
  const pkce = loopback ? generatePkcePair() : null;
7502
7781
  const sessionOptions = {
@@ -7529,7 +7808,7 @@ async function performVerificationLogin({
7529
7808
  await store.save(updated);
7530
7809
  latestCredentials = updated;
7531
7810
  if (!successLogged) {
7532
- console.log(chalk.green("\n\nLogin successful. Credentials saved."));
7811
+ console.log(chalk2.green("\n\nLogin successful. Credentials saved."));
7533
7812
  successLogged = true;
7534
7813
  }
7535
7814
  completed = true;
@@ -7545,7 +7824,7 @@ async function performVerificationLogin({
7545
7824
  }
7546
7825
  while (!completed) {
7547
7826
  const input2 = await prompt(
7548
- chalk.gray("\nInput fallback code if automatic login doesn't complete: ")
7827
+ chalk2.gray("\nInput fallback code if automatic login doesn't complete: ")
7549
7828
  );
7550
7829
  const cleaned = input2.trim();
7551
7830
  if (completed) {
@@ -7618,10 +7897,10 @@ function reportLoopbackError(error) {
7618
7897
  }
7619
7898
  function printSessionInstructions(session) {
7620
7899
  console.log(
7621
- chalk.blue.bold("\nOpen this link in your browser if it did not open automatically:")
7900
+ chalk2.blue.bold("\nOpen this link in your browser if it did not open automatically:")
7622
7901
  );
7623
- console.log(chalk.blueBright.underline(`${session.verificationUriComplete}`));
7624
- console.log(chalk.blue("\nApprove the request, then return to the CLI."));
7902
+ console.log(chalk2.blueBright.underline(`${session.verificationUriComplete}`));
7903
+ console.log(chalk2.blue("\nApprove the request, then return to the CLI."));
7625
7904
  }
7626
7905
  function extractErrorMessage4(error) {
7627
7906
  if (error instanceof Error && error.message) {
@@ -7729,6 +8008,27 @@ async function ensureUserSession(client, accessToken, options) {
7729
8008
  );
7730
8009
  }
7731
8010
 
8011
+ // src/utils/websocket.ts
8012
+ import WebSocket from "ws";
8013
+ function safeCloseWebSocket(ws) {
8014
+ try {
8015
+ ws.once("error", () => {
8016
+ });
8017
+ if (ws.readyState === WebSocket.CONNECTING) {
8018
+ ws.terminate();
8019
+ } else if (ws.readyState === WebSocket.OPEN) {
8020
+ ws.close();
8021
+ }
8022
+ } catch {
8023
+ }
8024
+ setImmediate(() => {
8025
+ try {
8026
+ ws.removeAllListeners();
8027
+ } catch {
8028
+ }
8029
+ });
8030
+ }
8031
+
7732
8032
  // src/relay/relay-bridge.ts
7733
8033
  var MAX_QUEUE_SIZE = 500;
7734
8034
  var LOGIN_WAIT_DELAY_MS = 3e4;
@@ -7773,7 +8073,7 @@ var RelayBridge = class extends EventEmitter {
7773
8073
  throw new Error("Relay bridge already started");
7774
8074
  }
7775
8075
  this.connectionHandle = this.orchestrator.addClient({
7776
- canSend: () => !this.canceled && this.ws?.readyState === WebSocket.OPEN,
8076
+ canSend: () => !this.canceled && this.ws?.readyState === WebSocket2.OPEN,
7777
8077
  sendEnvelope: async (message) => {
7778
8078
  this.sendFrame(message);
7779
8079
  },
@@ -7830,7 +8130,7 @@ var RelayBridge = class extends EventEmitter {
7830
8130
  this.connectionHandle.disconnect();
7831
8131
  this.connectionHandle = null;
7832
8132
  }
7833
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
8133
+ if (this.ws && this.ws.readyState === WebSocket2.OPEN) {
7834
8134
  this.ws.close(1e3, "CLI shutting down");
7835
8135
  }
7836
8136
  this.ws = null;
@@ -7847,7 +8147,7 @@ var RelayBridge = class extends EventEmitter {
7847
8147
  this.userSessionId = registration.userSessionId;
7848
8148
  const agentSessionId = this.ensureAgentSessionExists();
7849
8149
  const url = buildRelayWebsocketUrl(this.backendUrl, registration.relayWsPath);
7850
- const ws = new WebSocket(url, {
8150
+ const ws = new WebSocket2(url, {
7851
8151
  headers: {
7852
8152
  "x-relay-token": registration.relayToken
7853
8153
  }
@@ -7932,7 +8232,7 @@ var RelayBridge = class extends EventEmitter {
7932
8232
  }
7933
8233
  const serialized = JSON.stringify(frame);
7934
8234
  const target = socket ?? this.ws;
7935
- if (target && target.readyState === WebSocket.OPEN) {
8235
+ if (target && target.readyState === WebSocket2.OPEN) {
7936
8236
  try {
7937
8237
  target.send(serialized);
7938
8238
  return;
@@ -8327,7 +8627,7 @@ var RelayBridge = class extends EventEmitter {
8327
8627
  };
8328
8628
  }
8329
8629
  const socket = this.ws;
8330
- if (!socket || socket.readyState !== WebSocket.OPEN) {
8630
+ if (!socket || socket.readyState !== WebSocket2.OPEN) {
8331
8631
  return;
8332
8632
  }
8333
8633
  const frame = {
@@ -8398,7 +8698,7 @@ var RelayBridge = class extends EventEmitter {
8398
8698
  this.sendFrame(frame);
8399
8699
  }
8400
8700
  flushQueue() {
8401
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
8701
+ if (!this.ws || this.ws.readyState !== WebSocket2.OPEN) {
8402
8702
  return;
8403
8703
  }
8404
8704
  while (this.pendingOutbound.length > 0) {
@@ -8424,7 +8724,7 @@ var RelayBridge = class extends EventEmitter {
8424
8724
  const interval = Math.max(this.heartbeatIntervalMs, 5e3);
8425
8725
  this.pingTimer = setInterval(() => {
8426
8726
  const socket = this.ws;
8427
- if (!socket || socket.readyState !== WebSocket.OPEN) {
8727
+ if (!socket || socket.readyState !== WebSocket2.OPEN) {
8428
8728
  return;
8429
8729
  }
8430
8730
  const frame = {
@@ -8442,7 +8742,7 @@ var RelayBridge = class extends EventEmitter {
8442
8742
  }
8443
8743
  }
8444
8744
  closeCurrentSocket() {
8445
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
8745
+ if (this.ws && this.ws.readyState === WebSocket2.OPEN) {
8446
8746
  this.ws.close(1012, "Reconnecting");
8447
8747
  }
8448
8748
  }
@@ -8495,14 +8795,7 @@ function waitForOpen(ws) {
8495
8795
  const settle = (error) => {
8496
8796
  cleanup();
8497
8797
  if (error) {
8498
- try {
8499
- if (ws.readyState === WebSocket.CONNECTING) {
8500
- ws.terminate();
8501
- } else if (ws.readyState === WebSocket.OPEN) {
8502
- ws.close();
8503
- }
8504
- } catch {
8505
- }
8798
+ safeCloseWebSocket(ws);
8506
8799
  reject(error);
8507
8800
  } else {
8508
8801
  resolve();
@@ -9063,7 +9356,7 @@ function extractErrorMessage8(error) {
9063
9356
  }
9064
9357
 
9065
9358
  // src/daemon/daemon-launcher.ts
9066
- import chalk2 from "chalk";
9359
+ import chalk3 from "chalk";
9067
9360
 
9068
9361
  // src/daemon/session-pid-registry.ts
9069
9362
  import fs10 from "node:fs/promises";
@@ -9257,7 +9550,7 @@ async function ensureMachineNameConfigured(configStore, logger3) {
9257
9550
  const defaultName = os4.hostname();
9258
9551
  let machineName = defaultName;
9259
9552
  if (process.stdin.isTTY && process.stdout.isTTY) {
9260
- const answer = (await prompt(chalk2.blue(`Enter a name for this machine [${defaultName}]: `))).trim();
9553
+ const answer = (await prompt(chalk3.blue(`Enter a name for this machine [${defaultName}]: `))).trim();
9261
9554
  if (answer) {
9262
9555
  machineName = answer;
9263
9556
  }
@@ -9450,12 +9743,11 @@ async function registerDirectory(directoryPath, options) {
9450
9743
  await registry.ensureDirectory(normalized, { label: options.label, updateLastUsed: true });
9451
9744
  }
9452
9745
  async function spawnDaemonProcess(options, logger3) {
9453
- const entry = process.argv[1];
9454
- if (!entry) {
9455
- throw new Error("Unable to determine CLI entry path for daemon start");
9456
- }
9746
+ const args = getSpawnArgs(["daemon", "run-service"]);
9747
+ const execPath = getExecutablePath();
9457
9748
  logger3.info("Starting Omnara daemon...");
9458
- const child = spawn2(process.execPath, [entry, "daemon", "run-service"], {
9749
+ logger3.debug(`Spawning daemon: ${execPath} ${args.join(" ")}`);
9750
+ const child = spawn2(execPath, args, {
9459
9751
  detached: true,
9460
9752
  stdio: "ignore",
9461
9753
  env: {
@@ -9546,10 +9838,10 @@ function extractErrorMessage10(error) {
9546
9838
 
9547
9839
  // src/commands/daemon.ts
9548
9840
  import { Command as Command2 } from "commander";
9549
- import * as Sentry24 from "@sentry/node";
9841
+ import * as Sentry25 from "@sentry/node";
9550
9842
 
9551
9843
  // src/daemon/run-daemon.ts
9552
- import * as Sentry23 from "@sentry/node";
9844
+ import * as Sentry24 from "@sentry/node";
9553
9845
 
9554
9846
  // src/daemon/daemon-service.ts
9555
9847
  import * as Sentry22 from "@sentry/node";
@@ -9768,8 +10060,8 @@ function extractErrorMessage11(error) {
9768
10060
  // src/daemon/machine-supervisor.ts
9769
10061
  import { randomUUID as randomUUID7 } from "node:crypto";
9770
10062
  import os5 from "node:os";
9771
- import { setTimeout as delay4 } from "node:timers/promises";
9772
- import WebSocket2 from "ws";
10063
+ import { setTimeout as delay5 } from "node:timers/promises";
10064
+ import WebSocket3 from "ws";
9773
10065
  import * as Sentry21 from "@sentry/node";
9774
10066
 
9775
10067
  // ../shared-js/dist/utils/eff_wordlist.json
@@ -17595,9 +17887,11 @@ async function ensureWorkspace(client, accessToken, payload) {
17595
17887
 
17596
17888
  // src/daemon/session-runner.ts
17597
17889
  import { spawn as spawn3 } from "node:child_process";
17890
+ import { setTimeout as delay4 } from "node:timers/promises";
17598
17891
  import * as Sentry20 from "@sentry/node";
17599
17892
  import fs11 from "node:fs";
17600
17893
  import path13 from "node:path";
17894
+ var GRACEFUL_STOP_DELAY_MS = 2e3;
17601
17895
  var SessionManager = class {
17602
17896
  backendUrl;
17603
17897
  frontendUrl;
@@ -17630,6 +17924,47 @@ var SessionManager = class {
17630
17924
  throw error;
17631
17925
  }
17632
17926
  }
17927
+ /**
17928
+ * Stop a running session by user session ID.
17929
+ *
17930
+ * Looks up the session in the PID registry, sends SIGTERM for graceful
17931
+ * shutdown, waits briefly, then sends SIGKILL if still alive.
17932
+ *
17933
+ * @returns true if a running session was found and terminated, false if
17934
+ * the session was not running.
17935
+ */
17936
+ async stopSession(userSessionId) {
17937
+ return await withSessionRegistry(async (registry) => {
17938
+ const record = registry[userSessionId];
17939
+ if (!record) {
17940
+ return [registry, false];
17941
+ }
17942
+ const alive = await isSessionProcessAlive(record);
17943
+ if (!alive) {
17944
+ delete registry[userSessionId];
17945
+ return [registry, false];
17946
+ }
17947
+ try {
17948
+ process.kill(record.pid, "SIGTERM");
17949
+ this.logger.debug(`Sent SIGTERM to session ${userSessionId} (pid ${record.pid})`);
17950
+ } catch (error) {
17951
+ this.logger.debug(`SIGTERM failed for pid ${record.pid}: ${String(error)}`);
17952
+ delete registry[userSessionId];
17953
+ return [registry, false];
17954
+ }
17955
+ await delay4(GRACEFUL_STOP_DELAY_MS);
17956
+ const stillAlive = await isProcessAlive3(record.pid);
17957
+ if (stillAlive) {
17958
+ try {
17959
+ process.kill(record.pid, "SIGKILL");
17960
+ this.logger.debug(`Sent SIGKILL to session ${userSessionId} (pid ${record.pid})`);
17961
+ } catch {
17962
+ }
17963
+ }
17964
+ delete registry[userSessionId];
17965
+ return [registry, true];
17966
+ });
17967
+ }
17633
17968
  };
17634
17969
  var SessionRunner = class {
17635
17970
  spec;
@@ -17651,10 +17986,6 @@ var SessionRunner = class {
17651
17986
  if (this.child) {
17652
17987
  throw new Error("Session runner already started");
17653
17988
  }
17654
- const entry = process.argv[1];
17655
- if (!entry) {
17656
- throw new Error("Unable to determine CLI entry path for session runner");
17657
- }
17658
17989
  const sessionLogPath = await createSessionLogPath(this.spec.workspaceId, this.spec.launchId);
17659
17990
  this.logger.info(`Session ${this.spec.launchId} logs available at ${sessionLogPath}`);
17660
17991
  await withSessionRegistry(async (registry) => {
@@ -17668,8 +17999,8 @@ var SessionRunner = class {
17668
17999
  }
17669
18000
  return [registry, null];
17670
18001
  });
17671
- const args = [
17672
- entry,
18002
+ const execPath = getExecutablePath();
18003
+ const args = getSpawnArgs([
17673
18004
  "daemon",
17674
18005
  "run-session",
17675
18006
  "--user-session-id",
@@ -17678,7 +18009,8 @@ var SessionRunner = class {
17678
18009
  this.spec.workspaceId,
17679
18010
  "--directory",
17680
18011
  this.directory.path
17681
- ];
18012
+ ]);
18013
+ this.logger.debug(`Spawning session: ${execPath} ${args.join(" ")}`);
17682
18014
  if (this.spec.worktreeName) {
17683
18015
  args.push("--worktree-name", this.spec.worktreeName);
17684
18016
  }
@@ -17703,7 +18035,7 @@ var SessionRunner = class {
17703
18035
  let logFd = null;
17704
18036
  try {
17705
18037
  logFd = fs11.openSync(sessionLogPath, "a");
17706
- const child = spawn3(process.execPath, args, {
18038
+ const child = spawn3(execPath, args, {
17707
18039
  cwd: this.directory.path,
17708
18040
  stdio: ["ignore", logFd, logFd],
17709
18041
  env,
@@ -17722,7 +18054,7 @@ var SessionRunner = class {
17722
18054
  this.child = null;
17723
18055
  });
17724
18056
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
17725
- const command = `${process.execPath} ${args.join(" ")}`;
18057
+ const command = `${execPath} ${args.join(" ")}`;
17726
18058
  await withSessionRegistry(async (registry) => {
17727
18059
  registry[this.spec.userSessionId] = {
17728
18060
  pid: child.pid ?? -1,
@@ -17762,15 +18094,9 @@ async function createSessionLogPath(workspaceId, launchId) {
17762
18094
  }
17763
18095
 
17764
18096
  // src/package-info.ts
17765
- import { createRequire } from "node:module";
17766
- var require2 = createRequire(import.meta.url);
17767
- var pkg = require2("../package.json");
17768
- var FALLBACK_NAME = pkg.name ?? "omnara";
17769
- var FALLBACK_VERSION = pkg.version ?? "0.0.0";
17770
- var FALLBACK_DESCRIPTION = pkg.description ?? "Command-line interface for Omnara.";
17771
- var PACKAGE_NAME = true ? "omnara" : FALLBACK_NAME;
17772
- var PACKAGE_VERSION = true ? "0.5.2" : FALLBACK_VERSION;
17773
- var PACKAGE_DESCRIPTION = true ? "Command-line interface for Omnara." : FALLBACK_DESCRIPTION;
18097
+ var isBundled = true;
18098
+ var PACKAGE_VERSION = isBundled ? "0.6.0" : "0.0.0-dev";
18099
+ var PACKAGE_DESCRIPTION = isBundled ? "Command-line interface for Omnara." : "Command-line interface for Omnara.";
17774
18100
 
17775
18101
  // src/daemon/machine-supervisor.ts
17776
18102
  var HEARTBEAT_INTERVAL_MS = 1e4;
@@ -17878,13 +18204,13 @@ var MachineSupervisor = class {
17878
18204
  } else {
17879
18205
  this.logger.debug("Machine supervisor still waiting for login\u2026");
17880
18206
  }
17881
- await delay4(LOGIN_WAIT_DELAY_MS2);
18207
+ await delay5(LOGIN_WAIT_DELAY_MS2);
17882
18208
  } else {
17883
18209
  Sentry21.captureException(error);
17884
18210
  this.logger.warn(
17885
18211
  `Machine supervisor error: ${error.message}. Retrying in ${Math.round(retryDelayMs / 1e3)}s.`
17886
18212
  );
17887
- await delay4(retryDelayMs);
18213
+ await delay5(retryDelayMs);
17888
18214
  retryDelayMs = calculateBackoff(retryDelayMs);
17889
18215
  }
17890
18216
  }
@@ -17957,7 +18283,7 @@ var MachineSupervisor = class {
17957
18283
  const url = buildMachineWebsocketUrl(this.backendUrl, identity.wsPath);
17958
18284
  this.logger.info(`Connecting to machine websocket at ${url}`);
17959
18285
  await new Promise((resolve, reject) => {
17960
- const ws = new WebSocket2(url, {
18286
+ const ws = new WebSocket3(url, {
17961
18287
  headers: {
17962
18288
  authorization: `Bearer ${identity.token}`
17963
18289
  }
@@ -17974,17 +18300,7 @@ var MachineSupervisor = class {
17974
18300
  }
17975
18301
  this.clearHeartbeatTimer();
17976
18302
  this.websocket = null;
17977
- try {
17978
- if (ws.readyState === WebSocket2.CONNECTING) {
17979
- ws.terminate();
17980
- } else if (ws.readyState === WebSocket2.OPEN) {
17981
- ws.removeAllListeners();
17982
- ws.close();
17983
- } else {
17984
- ws.removeAllListeners();
17985
- }
17986
- } catch {
17987
- }
18303
+ safeCloseWebSocket(ws);
17988
18304
  if (error) {
17989
18305
  reject(error);
17990
18306
  } else {
@@ -18051,12 +18367,18 @@ var MachineSupervisor = class {
18051
18367
  }
18052
18368
  }
18053
18369
  async handleRpcRequest(message) {
18054
- if (message.method !== "start_session") {
18370
+ if (message.method === "start_session") {
18371
+ await this.handleStartSession(message);
18372
+ } else if (message.method === "stop_session") {
18373
+ await this.handleStopSession(message);
18374
+ } else {
18055
18375
  await this.sendRpcResponse(message.request_id, "error", `Unknown method ${message.method}`);
18056
- return;
18057
18376
  }
18377
+ }
18378
+ async handleStartSession(message) {
18058
18379
  try {
18059
- const result = await this.launchWorkspaceSessionFromRpc(message.payload);
18380
+ const payload = message.payload;
18381
+ const result = await this.launchWorkspaceSessionFromRpc(payload);
18060
18382
  await this.sendRpcResponse(message.request_id, "ok", void 0, {
18061
18383
  launch_id: result.launchId,
18062
18384
  user_session_id: result.userSessionId,
@@ -18075,6 +18397,25 @@ var MachineSupervisor = class {
18075
18397
  await this.sendRpcResponse(message.request_id, "error", messageText);
18076
18398
  }
18077
18399
  }
18400
+ async handleStopSession(message) {
18401
+ const payload = message.payload;
18402
+ const userSessionId = payload.user_session_id;
18403
+ try {
18404
+ const stopped = await this.sessionManager.stopSession(userSessionId);
18405
+ if (stopped) {
18406
+ this.logger.info(`Stopped session ${userSessionId}`);
18407
+ await this.sendRpcResponse(message.request_id, "ok");
18408
+ } else {
18409
+ this.logger.debug(`Session ${userSessionId} was not running`);
18410
+ await this.sendRpcResponse(message.request_id, "not_found", "Session not running");
18411
+ }
18412
+ } catch (error) {
18413
+ Sentry21.captureException(error);
18414
+ const messageText = error instanceof Error ? error.message : String(error);
18415
+ this.logger.warn(`Failed to stop session ${userSessionId}: ${messageText}`);
18416
+ await this.sendRpcResponse(message.request_id, "error", messageText);
18417
+ }
18418
+ }
18078
18419
  async sendRpcResponse(requestId, status, error, body) {
18079
18420
  const response = {
18080
18421
  type: "rpc_response",
@@ -18196,7 +18537,7 @@ var MachineSupervisor = class {
18196
18537
  return;
18197
18538
  }
18198
18539
  }
18199
- await delay4(250);
18540
+ await delay5(250);
18200
18541
  }
18201
18542
  throw new Error("Machine has not finished registering yet. Try again in a few seconds.");
18202
18543
  }
@@ -18232,7 +18573,7 @@ var MachineSupervisor = class {
18232
18573
  }
18233
18574
  async sendMessage(payload) {
18234
18575
  const ws = this.websocket;
18235
- if (!ws || ws.readyState !== WebSocket2.OPEN) {
18576
+ if (!ws || ws.readyState !== WebSocket3.OPEN) {
18236
18577
  throw new Error("Machine websocket is not connected");
18237
18578
  }
18238
18579
  await new Promise((resolve, reject) => {
@@ -18415,12 +18756,316 @@ var DaemonService = class {
18415
18756
  // src/daemon/run-daemon.ts
18416
18757
  import fs12 from "node:fs";
18417
18758
  import path14 from "node:path";
18418
- async function runDaemonProcess() {
18419
- await ensureOmnaraHome();
18420
- await ensureDirectory(DAEMON_LOG_DIR);
18421
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
18422
- const fileName = `${timestamp}-${process.pid}.log`;
18423
- const logPath = path14.join(DAEMON_LOG_DIR, fileName);
18759
+
18760
+ // src/utils/version-check.ts
18761
+ import chalk4 from "chalk";
18762
+ import { execSync as execSync2 } from "node:child_process";
18763
+ import { realpathSync } from "node:fs";
18764
+ import { platform, arch } from "node:os";
18765
+ import * as Sentry23 from "@sentry/node";
18766
+ var RELEASE_BASE_URL = process.env.OMNARA_RELEASE_URL ?? "https://releases.omnara.com";
18767
+ var FRONTEND_BASE_URL = process.env.OMNARA_FRONTEND_URL ?? "https://omnara.com";
18768
+ var IS_PRERELEASE = process.env.PRERELEASE === "1";
18769
+ var INSTALL_SCRIPT_URL_UNIX = `${FRONTEND_BASE_URL}/install/install.sh`;
18770
+ var INSTALL_SCRIPT_URL_WINDOWS = `${FRONTEND_BASE_URL}/install/install.ps1`;
18771
+ function detectPlatform() {
18772
+ const os6 = platform();
18773
+ const cpuArch = arch();
18774
+ let platformId;
18775
+ if (os6 === "darwin" && cpuArch === "arm64") {
18776
+ platformId = "darwin-arm64";
18777
+ } else if (os6 === "darwin" && cpuArch === "x64") {
18778
+ platformId = "darwin-x64";
18779
+ } else if (os6 === "linux" && cpuArch === "x64") {
18780
+ platformId = isMusl() ? "linux-x64-musl" : "linux-x64";
18781
+ } else if (os6 === "linux" && cpuArch === "arm64") {
18782
+ platformId = "linux-arm64";
18783
+ } else if (os6 === "win32" && cpuArch === "x64") {
18784
+ platformId = "windows-x64";
18785
+ } else {
18786
+ throw new Error(`Unsupported platform: ${os6}-${cpuArch}`);
18787
+ }
18788
+ return platformId;
18789
+ }
18790
+ function isMusl() {
18791
+ try {
18792
+ const result = execSync2("ldd --version 2>&1 || true", { encoding: "utf8" });
18793
+ return result.toLowerCase().includes("musl");
18794
+ } catch {
18795
+ return false;
18796
+ }
18797
+ }
18798
+ async function fetchLatestRelease() {
18799
+ const controller = new AbortController();
18800
+ const timeout = setTimeout(() => controller.abort(), 5e3);
18801
+ timeout.unref?.();
18802
+ try {
18803
+ const basePath = IS_PRERELEASE ? `${RELEASE_BASE_URL}/prerelease` : RELEASE_BASE_URL;
18804
+ const url = `${basePath}/latest/release.json`;
18805
+ const response = await fetch(url, { signal: controller.signal });
18806
+ if (!response.ok) {
18807
+ return null;
18808
+ }
18809
+ return await response.json();
18810
+ } catch {
18811
+ return null;
18812
+ } finally {
18813
+ clearTimeout(timeout);
18814
+ }
18815
+ }
18816
+ function compareVersions(v1, v2) {
18817
+ const clean1 = v1.replace(/^v/, "");
18818
+ const clean2 = v2.replace(/^v/, "");
18819
+ const parts1 = clean1.split(".").map(Number);
18820
+ const parts2 = clean2.split(".").map(Number);
18821
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
18822
+ const part1 = parts1[i] || 0;
18823
+ const part2 = parts2[i] || 0;
18824
+ if (part1 > part2) return 1;
18825
+ if (part1 < part2) return -1;
18826
+ }
18827
+ return 0;
18828
+ }
18829
+ function isNpmInstallation() {
18830
+ if (isBunSingleFileExecutable()) {
18831
+ return false;
18832
+ }
18833
+ const execPath = process.argv[1] ?? "";
18834
+ if (execPath.includes("node_modules")) {
18835
+ return true;
18836
+ }
18837
+ if (execPath.includes("/lib/node_modules/") || execPath.includes("\\node_modules\\")) {
18838
+ return true;
18839
+ }
18840
+ try {
18841
+ const realPath = realpathSync(execPath);
18842
+ if (realPath.includes("node_modules")) {
18843
+ return true;
18844
+ }
18845
+ } catch {
18846
+ }
18847
+ return false;
18848
+ }
18849
+ function uninstallNpmVersion() {
18850
+ console.log(chalk4.blue("Uninstalling npm version of Omnara..."));
18851
+ try {
18852
+ execSync2("npm uninstall -g omnara", { stdio: "inherit" });
18853
+ return true;
18854
+ } catch {
18855
+ try {
18856
+ if (platform() !== "win32") {
18857
+ execSync2("sudo npm uninstall -g omnara", { stdio: "inherit" });
18858
+ return true;
18859
+ }
18860
+ } catch {
18861
+ }
18862
+ }
18863
+ console.log(chalk4.yellow("Could not automatically uninstall npm version."));
18864
+ console.log(chalk4.yellow("Please run manually: npm uninstall -g omnara"));
18865
+ return false;
18866
+ }
18867
+ async function runInstallScript(options) {
18868
+ const os6 = platform();
18869
+ const silent = options?.silent ?? false;
18870
+ const stdio = silent ? "pipe" : "inherit";
18871
+ if (!silent) {
18872
+ if (IS_PRERELEASE) {
18873
+ console.log(chalk4.blue("\nRunning Omnara installation script (prerelease channel)...\n"));
18874
+ } else {
18875
+ console.log(chalk4.blue("\nRunning Omnara installation script...\n"));
18876
+ }
18877
+ }
18878
+ const env = { ...process.env };
18879
+ if (IS_PRERELEASE) {
18880
+ env.PRERELEASE = "1";
18881
+ }
18882
+ try {
18883
+ if (os6 === "win32") {
18884
+ const prereleaseEnv = IS_PRERELEASE ? '$env:PRERELEASE="1"; ' : "";
18885
+ const cmd = `powershell -ExecutionPolicy Bypass -Command "& { ${prereleaseEnv}iwr -useb '${INSTALL_SCRIPT_URL_WINDOWS}' | iex }"`;
18886
+ execSync2(cmd, { stdio, env });
18887
+ } else {
18888
+ const cmd = `curl -fsSL '${INSTALL_SCRIPT_URL_UNIX}' | bash`;
18889
+ execSync2(cmd, { stdio, shell: "/bin/bash", env });
18890
+ }
18891
+ return true;
18892
+ } catch (error) {
18893
+ Sentry23.captureException(error);
18894
+ if (!silent) {
18895
+ console.log(chalk4.red("\nInstallation script failed."));
18896
+ console.log(chalk4.yellow("Please install manually:"));
18897
+ const prereleasePrefix = IS_PRERELEASE ? "PRERELEASE=1 " : "";
18898
+ if (os6 === "win32") {
18899
+ const psPrefix = IS_PRERELEASE ? '$env:PRERELEASE="1"; ' : "";
18900
+ console.log(chalk4.cyan(` ${psPrefix}irm ${INSTALL_SCRIPT_URL_WINDOWS} | iex`));
18901
+ } else {
18902
+ console.log(
18903
+ chalk4.cyan(` ${prereleasePrefix}curl -fsSL ${INSTALL_SCRIPT_URL_UNIX} | bash`)
18904
+ );
18905
+ }
18906
+ }
18907
+ return false;
18908
+ }
18909
+ }
18910
+ async function handleNpmInstallation() {
18911
+ const os6 = platform();
18912
+ const installCmd = os6 === "win32" ? `irm ${INSTALL_SCRIPT_URL_WINDOWS} | iex` : `curl -fsSL ${INSTALL_SCRIPT_URL_UNIX} | bash`;
18913
+ const border = chalk4.yellow("=".repeat(60));
18914
+ console.log("\n" + border);
18915
+ console.log(chalk4.yellow.bold(" npm Installation Detected"));
18916
+ console.log(border);
18917
+ console.log(
18918
+ chalk4.white(`
18919
+ Omnara CLI is now distributed as a standalone executable
18920
+ with automatic updates and improved performance.
18921
+
18922
+ Migrating to the new installation method...
18923
+ `)
18924
+ );
18925
+ uninstallNpmVersion();
18926
+ const success = await runInstallScript();
18927
+ if (success) {
18928
+ console.log(chalk4.green("\nMigration complete!"));
18929
+ console.log(chalk4.white("Please restart your terminal and run `omnara` again.\n"));
18930
+ return true;
18931
+ } else {
18932
+ console.log(
18933
+ chalk4.yellow(`
18934
+ To complete migration manually, run:
18935
+
18936
+ ${chalk4.cyan(installCmd)}
18937
+ `)
18938
+ );
18939
+ return false;
18940
+ }
18941
+ }
18942
+ async function checkForUpdate() {
18943
+ const currentVersion = PACKAGE_VERSION;
18944
+ if (!currentVersion) {
18945
+ return { available: false, currentVersion: "0.0.0", isPrerelease: IS_PRERELEASE };
18946
+ }
18947
+ try {
18948
+ const release = await fetchLatestRelease();
18949
+ if (!release) {
18950
+ return { available: false, currentVersion, isPrerelease: IS_PRERELEASE };
18951
+ }
18952
+ const platformId = detectPlatform();
18953
+ const hasPlatformBinary = !!release.platforms[platformId];
18954
+ if (!hasPlatformBinary) {
18955
+ return { available: false, currentVersion, isPrerelease: IS_PRERELEASE };
18956
+ }
18957
+ const updateAvailable = compareVersions(release.version, currentVersion) > 0;
18958
+ return {
18959
+ available: updateAvailable,
18960
+ latestVersion: release.version,
18961
+ currentVersion,
18962
+ isPrerelease: IS_PRERELEASE
18963
+ };
18964
+ } catch {
18965
+ return { available: false, currentVersion, isPrerelease: IS_PRERELEASE };
18966
+ }
18967
+ }
18968
+ async function performUpdate(options) {
18969
+ const silent = options?.silent ?? false;
18970
+ try {
18971
+ if (!silent) {
18972
+ const client = new DaemonClient();
18973
+ const status = await client.getStatus().catch(() => null);
18974
+ if (status) {
18975
+ console.log(chalk4.yellow("Stopping daemon for update..."));
18976
+ await client.stop("UPDATING").catch(() => {
18977
+ });
18978
+ }
18979
+ }
18980
+ const success = await runInstallScript({ silent });
18981
+ if (success && !silent) {
18982
+ console.log(chalk4.green("\nUpdate complete!"));
18983
+ console.log(chalk4.white("The daemon will restart automatically.\n"));
18984
+ }
18985
+ return success;
18986
+ } catch (error) {
18987
+ Sentry23.captureException(error);
18988
+ return false;
18989
+ }
18990
+ }
18991
+ async function attemptUpdate() {
18992
+ try {
18993
+ const currentVersion = PACKAGE_VERSION;
18994
+ if (!currentVersion) {
18995
+ return false;
18996
+ }
18997
+ if (isNpmInstallation()) {
18998
+ return await handleNpmInstallation();
18999
+ }
19000
+ const updateInfo = await checkForUpdate();
19001
+ if (!updateInfo.available) {
19002
+ const channel = IS_PRERELEASE ? " (prerelease channel)" : "";
19003
+ console.log(chalk4.grey(`Omnara is up to date${channel}!
19004
+ `));
19005
+ return false;
19006
+ }
19007
+ const channelLabel = IS_PRERELEASE ? " (prerelease)" : "";
19008
+ console.log(
19009
+ chalk4.yellow(
19010
+ "\u2550".repeat(60) + `
19011
+ Update available for Omnara CLI${channelLabel}
19012
+ Current version: ${updateInfo.currentVersion}
19013
+ Latest version: ${updateInfo.latestVersion}
19014
+ ${"\u2550".repeat(60)}`
19015
+ )
19016
+ );
19017
+ const success = await performUpdate();
19018
+ if (success) {
19019
+ console.log(chalk4.white("Please restart `omnara` to use the new version.\n"));
19020
+ return true;
19021
+ }
19022
+ return false;
19023
+ } catch (e) {
19024
+ Sentry23.captureException(e);
19025
+ }
19026
+ return false;
19027
+ }
19028
+
19029
+ // src/daemon/run-daemon.ts
19030
+ var UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
19031
+ function startUpdateChecker(logger3, onUpdateInstalled) {
19032
+ const checkForUpdates = async () => {
19033
+ try {
19034
+ logger3.debug("Checking for updates...");
19035
+ const updateInfo = await checkForUpdate();
19036
+ if (updateInfo.available) {
19037
+ logger3.info(
19038
+ `Update available: ${updateInfo.currentVersion} -> ${updateInfo.latestVersion}`
19039
+ );
19040
+ logger3.info("Starting background update...");
19041
+ const success = await runInstallScript({ silent: true });
19042
+ if (success) {
19043
+ logger3.info("Update installed. Triggering graceful shutdown for restart...");
19044
+ onUpdateInstalled();
19045
+ } else {
19046
+ logger3.warn("Background update failed. Will retry on next check.");
19047
+ }
19048
+ } else {
19049
+ logger3.debug(`No updates available (current: ${updateInfo.currentVersion})`);
19050
+ }
19051
+ } catch (error) {
19052
+ Sentry24.captureException(error);
19053
+ logger3.error("Update check failed:", error);
19054
+ }
19055
+ };
19056
+ setTimeout(() => {
19057
+ void checkForUpdates();
19058
+ }, 1e4);
19059
+ return setInterval(() => {
19060
+ void checkForUpdates();
19061
+ }, UPDATE_CHECK_INTERVAL_MS);
19062
+ }
19063
+ async function runDaemonProcess() {
19064
+ await ensureOmnaraHome();
19065
+ await ensureDirectory(DAEMON_LOG_DIR);
19066
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
19067
+ const fileName = `${timestamp}-${process.pid}.log`;
19068
+ const logPath = path14.join(DAEMON_LOG_DIR, fileName);
18424
19069
  const stream = fs12.createWriteStream(logPath, {
18425
19070
  flags: "a",
18426
19071
  mode: DEFAULT_FILE_MODE2
@@ -18466,11 +19111,18 @@ async function runDaemonProcess() {
18466
19111
  machineSupervisor,
18467
19112
  logger: logger3
18468
19113
  });
19114
+ const updateCheckInterval = startUpdateChecker(logger3, () => {
19115
+ logger3.info("Update installed, initiating graceful shutdown...");
19116
+ void service.stop().catch((error) => {
19117
+ Sentry24.captureException(error);
19118
+ logger3.error("Failed to stop daemon after update:", error);
19119
+ });
19120
+ });
18469
19121
  const makeSignalHandler = (signal) => {
18470
19122
  return () => {
18471
19123
  logger3.warn(`Received ${signal}. Stopping daemon...`);
18472
19124
  void service.stop().catch((error) => {
18473
- Sentry23.captureException(error);
19125
+ Sentry24.captureException(error);
18474
19126
  logger3.error("Failed to stop daemon:", error);
18475
19127
  });
18476
19128
  };
@@ -18483,10 +19135,11 @@ async function runDaemonProcess() {
18483
19135
  await service.start();
18484
19136
  await service.waitForShutdown();
18485
19137
  } catch (error) {
18486
- Sentry23.captureException(error);
19138
+ Sentry24.captureException(error);
18487
19139
  logger3.error("Daemon exited with error:", error);
18488
19140
  process.exitCode = 1;
18489
19141
  } finally {
19142
+ clearInterval(updateCheckInterval);
18490
19143
  process.off("SIGINT", sigint);
18491
19144
  process.off("SIGTERM", sigterm);
18492
19145
  await service.stop().catch(() => void 0);
@@ -18543,7 +19196,7 @@ function buildDaemonCommand() {
18543
19196
  try {
18544
19197
  process.chdir(options.directory);
18545
19198
  } catch (error) {
18546
- Sentry24.captureException(error);
19199
+ Sentry25.captureException(error);
18547
19200
  logger.error(
18548
19201
  `Unable to switch to directory ${options.directory}: ${error.message}`
18549
19202
  );
@@ -18562,7 +19215,7 @@ function buildDaemonCommand() {
18562
19215
  worktreeName: options.worktreeName
18563
19216
  });
18564
19217
  } catch (error) {
18565
- Sentry24.captureException(error);
19218
+ Sentry25.captureException(error);
18566
19219
  logger.error("User session exited with error:", error);
18567
19220
  process.exitCode = process.exitCode || 1;
18568
19221
  }
@@ -18594,190 +19247,6 @@ function buildKillCommand() {
18594
19247
  return kill;
18595
19248
  }
18596
19249
 
18597
- // src/utils/version-check.ts
18598
- import chalk3 from "chalk";
18599
- import { execSync } from "child_process";
18600
- import * as Sentry25 from "@sentry/node";
18601
- async function fetchLatestVersion(packageName) {
18602
- const controller = new AbortController();
18603
- const timeout = setTimeout(() => controller.abort(), 3e3);
18604
- timeout.unref?.();
18605
- try {
18606
- const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;
18607
- const response = await fetch(url, { signal: controller.signal });
18608
- if (!response.ok) {
18609
- return null;
18610
- }
18611
- const packageInfo = await response.json();
18612
- return packageInfo["dist-tags"]?.latest ?? null;
18613
- } catch {
18614
- return null;
18615
- } finally {
18616
- clearTimeout(timeout);
18617
- }
18618
- }
18619
- function compareVersions(v1, v2) {
18620
- const parts1 = v1.split(".").map(Number);
18621
- const parts2 = v2.split(".").map(Number);
18622
- for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
18623
- const part1 = parts1[i] || 0;
18624
- const part2 = parts2[i] || 0;
18625
- if (part1 > part2) return 1;
18626
- if (part1 < part2) return -1;
18627
- }
18628
- return 0;
18629
- }
18630
- async function attemptUpdate() {
18631
- try {
18632
- const currentVersion = PACKAGE_VERSION;
18633
- const packageName = PACKAGE_NAME;
18634
- if (!currentVersion || !packageName) {
18635
- return false;
18636
- }
18637
- const latestVersion = await fetchLatestVersion(packageName);
18638
- if (!latestVersion) {
18639
- return false;
18640
- }
18641
- if (compareVersions(latestVersion, currentVersion) > 0) {
18642
- const client = new DaemonClient();
18643
- console.log(
18644
- chalk3.yellow(
18645
- "\u2550".repeat(60),
18646
- `
18647
- Update available for Omnara CLI`,
18648
- `
18649
- Current version: ${currentVersion}`,
18650
- `
18651
- Latest version: ${latestVersion}`,
18652
- `
18653
- ${"\u2550".repeat(60)}`
18654
- )
18655
- );
18656
- const status = await client.getStatus();
18657
- if (status) {
18658
- console.log(chalk3.yellow("\nPausing daemon..."));
18659
- client.stop("UPDATING");
18660
- }
18661
- console.log(
18662
- chalk3.blue(
18663
- "\nAuto-updating Omnara CLI:",
18664
- `
18665
-
18666
- $ npm install -g ${packageName}@latest`,
18667
- `
18668
-
18669
- Installing...
18670
-
18671
- `
18672
- )
18673
- );
18674
- try {
18675
- execSync("npm install -g omnara@latest");
18676
- console.log(chalk3.green("\n\nUpdate complete! Rerun `omnara` to restart session.\n"));
18677
- return true;
18678
- } catch (err) {
18679
- console.log(
18680
- chalk3.red(
18681
- "\nAuto-update failed. Please install manually:",
18682
- `
18683
-
18684
- $ npm install -g ${packageName}@latest`,
18685
- `
18686
-
18687
- If you see EACCES/permission errors, retry with:`,
18688
- `
18689
-
18690
- $ sudo npm install -g ${packageName}@latest`,
18691
- "\n"
18692
- )
18693
- );
18694
- Sentry25.captureException(err);
18695
- }
18696
- } else {
18697
- console.log(chalk3.grey("Omnara is up to date!\n"));
18698
- }
18699
- } catch (e) {
18700
- Sentry25.captureException(e);
18701
- console.log(e);
18702
- }
18703
- return false;
18704
- }
18705
-
18706
- // src/utils/claude-code-check.ts
18707
- import chalk4 from "chalk";
18708
- import { query as query4 } from "@anthropic-ai/claude-agent-sdk";
18709
- async function performClaudeCodeCheck() {
18710
- if (process.env.OMNARA_SKIP_CLAUDE_CHECK === "true") {
18711
- return true;
18712
- }
18713
- const indicator = createClaudeCheckIndicator();
18714
- try {
18715
- indicator?.start();
18716
- for await (const message of query4({
18717
- prompt: "Respond with one word.",
18718
- options: { maxTurns: 1, model: "haiku", settingSources: ["user", "project", "local"] }
18719
- })) {
18720
- void message;
18721
- }
18722
- indicator?.stop("ok");
18723
- return true;
18724
- } catch {
18725
- indicator?.stop("fail");
18726
- console.log(
18727
- chalk4.red(
18728
- "\nError: Omnara couldn't run because it relies on Claude Code, which isn't installed or authenticated. Try running `claude /login`.\n"
18729
- )
18730
- );
18731
- return false;
18732
- }
18733
- }
18734
- function createClaudeCheckIndicator() {
18735
- if (!process.stdout.isTTY) {
18736
- return {
18737
- start() {
18738
- console.log("Checking Claude Code auth...");
18739
- },
18740
- stop(status) {
18741
- if (status !== "ok") {
18742
- console.log("Claude Code auth failed.");
18743
- }
18744
- }
18745
- };
18746
- }
18747
- const label = "Checking Claude Code auth";
18748
- const frames = ["-", "\\", "|", "/"];
18749
- let index = 0;
18750
- let interval = null;
18751
- let running = false;
18752
- const stopLine = (status) => {
18753
- if (!running) {
18754
- return;
18755
- }
18756
- running = false;
18757
- if (interval) {
18758
- clearInterval(interval);
18759
- }
18760
- const statusText = status === "ok" ? chalk4.green("OK") : chalk4.red("FAILED");
18761
- const padding = " ";
18762
- process.stdout.write(`\r${label} ${statusText}${padding}
18763
- `);
18764
- };
18765
- return {
18766
- start() {
18767
- if (running) {
18768
- return;
18769
- }
18770
- running = true;
18771
- process.stdout.write(`${label} ...`);
18772
- interval = setInterval(() => {
18773
- process.stdout.write(`\r${label} ${frames[index]} `);
18774
- index = (index + 1) % frames.length;
18775
- }, 120);
18776
- },
18777
- stop: stopLine
18778
- };
18779
- }
18780
-
18781
19250
  // src/cli.ts
18782
19251
  import chalk5 from "chalk";
18783
19252
  function createProgram() {
@@ -18814,6 +19283,9 @@ async function run(argv) {
18814
19283
  }
18815
19284
  }
18816
19285
  function isMainModule() {
19286
+ if (isBunSingleFileExecutable()) {
19287
+ return true;
19288
+ }
18817
19289
  if (!import.meta.url || !process.argv[1]) {
18818
19290
  return false;
18819
19291
  }