omnara 0.1.5 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +546 -93
- package/dist/cli.js.map +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/orchestrator/orchestrator-prompt.d.ts +1 -1
- package/dist/orchestrator/worker-manager.d.ts +1 -1
- package/dist/orchestrator-prompt.md +405 -0
- package/dist/relay/relay-bridge.d.ts +1 -0
- package/dist/schemas/index.d.ts +1 -1
- package/dist/utils/browser.d.ts +1 -0
- package/dist/utils/claude-code-check.d.ts +21 -0
- package/dist/utils/logger.d.ts +128 -0
- package/dist/utils/version-check.d.ts +9 -0
- package/package.json +4 -13
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// ESM shim for __filename and __dirname
|
|
2
|
-
import { fileURLToPath } from 'url';
|
|
3
|
-
import
|
|
4
|
-
const __filename =
|
|
5
|
-
const __dirname = dirname(__filename);
|
|
2
|
+
import { fileURLToPath as __omnaraFileURLToPath } from 'url';
|
|
3
|
+
import pathShim from 'path';
|
|
4
|
+
const __filename = __omnaraFileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = pathShim.dirname(__filename);
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
8
|
-
import
|
|
9
|
-
import { createRequire } from "node:module";
|
|
8
|
+
import path4 from "path";
|
|
9
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
10
10
|
import { Command as Command3 } from "commander";
|
|
11
11
|
|
|
12
12
|
// src/commands/agent.ts
|
|
@@ -17,7 +17,9 @@ import { randomUUID as randomUUID3 } from "crypto";
|
|
|
17
17
|
|
|
18
18
|
// src/orchestrator/ai-client.ts
|
|
19
19
|
import fs from "fs";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
query
|
|
22
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
21
23
|
var BASE_ALLOWED_ORCHESTRATOR_TOOLS = [
|
|
22
24
|
"Glob",
|
|
23
25
|
"Grep",
|
|
@@ -52,7 +54,7 @@ var AIClient = class {
|
|
|
52
54
|
constructor(config) {
|
|
53
55
|
const defaultOptions = {
|
|
54
56
|
maxTurns: 100,
|
|
55
|
-
model: "
|
|
57
|
+
model: "sonnet",
|
|
56
58
|
cwd: process.cwd(),
|
|
57
59
|
includePartialMessages: false,
|
|
58
60
|
allowedTools: [...BASE_ALLOWED_ORCHESTRATOR_TOOLS]
|
|
@@ -183,7 +185,11 @@ var Session = class {
|
|
|
183
185
|
}
|
|
184
186
|
subscribe(subscriber) {
|
|
185
187
|
this.subscribers.set(subscriber.id, subscriber);
|
|
186
|
-
this.safeSend(subscriber, {
|
|
188
|
+
this.safeSend(subscriber, {
|
|
189
|
+
type: "session_joined",
|
|
190
|
+
sessionId: this.id,
|
|
191
|
+
messageId: randomUUID()
|
|
192
|
+
});
|
|
187
193
|
}
|
|
188
194
|
unsubscribe(subscriberId) {
|
|
189
195
|
this.subscribers.delete(subscriberId);
|
|
@@ -740,7 +746,47 @@ var WorkerManager = class extends EventEmitter {
|
|
|
740
746
|
record.sessionId,
|
|
741
747
|
record.messageQueue
|
|
742
748
|
);
|
|
743
|
-
const query2 = client.createQuery(messageStream
|
|
749
|
+
const query2 = client.createQuery(messageStream, {
|
|
750
|
+
hooks: {
|
|
751
|
+
PostToolUse: [
|
|
752
|
+
{
|
|
753
|
+
// No matcher means it applies to all tools (Bash, Read, Write, etc.)
|
|
754
|
+
// If we wanted to only capture Bash results, we could add: matcher: 'Bash'
|
|
755
|
+
hooks: [
|
|
756
|
+
async (input2, toolUseID) => {
|
|
757
|
+
if (input2.hook_event_name !== "PostToolUse") {
|
|
758
|
+
return {};
|
|
759
|
+
}
|
|
760
|
+
const postToolInput = input2;
|
|
761
|
+
console.log("[WorkerManager] PostToolUse hook triggered:", {
|
|
762
|
+
toolName: postToolInput.tool_name,
|
|
763
|
+
toolUseID,
|
|
764
|
+
hasResponse: !!postToolInput.tool_response,
|
|
765
|
+
workerId: record.id,
|
|
766
|
+
task: record.task
|
|
767
|
+
});
|
|
768
|
+
if (postToolInput.tool_name === "Bash") {
|
|
769
|
+
console.log("[WorkerManager] Bash tool response structure:", {
|
|
770
|
+
responseType: typeof postToolInput.tool_response,
|
|
771
|
+
responseKeys: postToolInput.tool_response ? Object.keys(postToolInput.tool_response) : [],
|
|
772
|
+
fullResponse: postToolInput.tool_response
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
this.emit("message", {
|
|
776
|
+
record,
|
|
777
|
+
messageType: "tool_result",
|
|
778
|
+
toolUseId: toolUseID || randomUUID2(),
|
|
779
|
+
toolContent: postToolInput.tool_response,
|
|
780
|
+
isError: false
|
|
781
|
+
// PostToolUse is for successful executions
|
|
782
|
+
});
|
|
783
|
+
return {};
|
|
784
|
+
}
|
|
785
|
+
]
|
|
786
|
+
}
|
|
787
|
+
]
|
|
788
|
+
}
|
|
789
|
+
});
|
|
744
790
|
this.runningQueries.set(record.id, query2);
|
|
745
791
|
const summarizedChunks = [];
|
|
746
792
|
try {
|
|
@@ -821,7 +867,9 @@ var WorkerManager = class extends EventEmitter {
|
|
|
821
867
|
}
|
|
822
868
|
}
|
|
823
869
|
if (!hasToolUse && !record.messageQueue.isClosed()) {
|
|
824
|
-
console.log(
|
|
870
|
+
console.log(
|
|
871
|
+
"[WorkerManager] Assistant message without tool use - closing message queue to signal completion"
|
|
872
|
+
);
|
|
825
873
|
record.messageQueue.close();
|
|
826
874
|
}
|
|
827
875
|
break;
|
|
@@ -951,7 +999,9 @@ Usage notes:
|
|
|
951
999
|
- The worker's result is returned to you, not the user - you must summarize or forward relevant information
|
|
952
1000
|
- Worker progress status is automatically included in your context - you can see their current state without additional tools`,
|
|
953
1001
|
{
|
|
954
|
-
task: z2.string().min(1, "task name is required").describe(
|
|
1002
|
+
task: z2.string().min(1, "task name is required").describe(
|
|
1003
|
+
'A short, unique name for this task (e.g., "FizzBuzz Python", "Search codebase", "Fix bug #123"). Used to identify and track this worker.'
|
|
1004
|
+
),
|
|
955
1005
|
prompt: z2.string().min(1, "prompt is required"),
|
|
956
1006
|
metadata: z2.record(z2.any()).optional()
|
|
957
1007
|
},
|
|
@@ -966,7 +1016,10 @@ Usage notes:
|
|
|
966
1016
|
if (!parentSessionId && toolUseId) {
|
|
967
1017
|
parentSessionId = context.resolveSessionIdByToolUse(toolUseId);
|
|
968
1018
|
}
|
|
969
|
-
console.warn(
|
|
1019
|
+
console.warn(
|
|
1020
|
+
"[WorkerTools] Main coding session not available, falling back to SDK resolution:",
|
|
1021
|
+
parentSessionId
|
|
1022
|
+
);
|
|
970
1023
|
}
|
|
971
1024
|
console.log("[WorkerTools] Launching worker:", {
|
|
972
1025
|
task: args.task,
|
|
@@ -1041,11 +1094,19 @@ User: "Wait, actually don't touch the database - just refactor the API layer ins
|
|
|
1041
1094
|
Orchestrator: [message_worker: task="refactor database", message="User wants to change scope - refactor API layer instead, leave database alone", interrupt=true]`,
|
|
1042
1095
|
{
|
|
1043
1096
|
task: z2.string().min(1, "task name is required").describe("The task name of the worker to message"),
|
|
1044
|
-
message: z2.string().min(1, "message is required").describe(
|
|
1045
|
-
|
|
1097
|
+
message: z2.string().min(1, "message is required").describe(
|
|
1098
|
+
"The message to send to the worker. Be clear and specific about what has changed or what additional context is being provided."
|
|
1099
|
+
),
|
|
1100
|
+
interrupt: z2.boolean().default(false).describe(
|
|
1101
|
+
"If true, interrupt current generation immediately; if false (default), queue message for next turn"
|
|
1102
|
+
)
|
|
1046
1103
|
},
|
|
1047
1104
|
async (args) => {
|
|
1048
|
-
const success = await workerManager.messageWorker(
|
|
1105
|
+
const success = await workerManager.messageWorker(
|
|
1106
|
+
args.task,
|
|
1107
|
+
args.message,
|
|
1108
|
+
args.interrupt
|
|
1109
|
+
);
|
|
1049
1110
|
if (!success) {
|
|
1050
1111
|
const worker = workerManager.getWorkerByTask(args.task);
|
|
1051
1112
|
if (!worker) {
|
|
@@ -1092,8 +1153,10 @@ Orchestrator: [message_worker: task="refactor database", message="User wants to
|
|
|
1092
1153
|
});
|
|
1093
1154
|
}
|
|
1094
1155
|
|
|
1095
|
-
// src/orchestrator/orchestrator-prompt.
|
|
1096
|
-
var orchestrator_prompt_default = '# Role: Task Orchestrator\n\nYou are Omnara, an orchestrator agent responsible for coordinating work through delegation. Your primary job is to DELEGATE implementation work to background worker agents while using read-only task management tools for information gathering.\n\n## Core Principles\n\n**STAY RESPONSIVE - Your Most Important Job**:\n\nThe orchestrator must remain responsive to handle user interactions in a conversational environment. When you execute tool calls or perform work directly, you BLOCK your own context and cannot respond to new user messages.\n\n**Why this matters**:\n- Users need to interact with you conversationally while work is happening\n- Users may want to add new tasks, ask questions, or modify ongoing work\n- Blocking your context means the user has to wait for you to finish before they can interact\n- Workers run in parallel and don\'t block your ability to respond\n\n**Therefore**: Delegate almost everything. Even tasks that seem quick can block you from being responsive. Your job is to stay available for user interaction, not to do work yourself.\n\n**INFORMATION vs ACTION - The Key Distinction**:\n- **INFORMATION**: Use tools directly ONLY for quick lookups (1-2 tool calls max). If unsure, delegate.\n- **ACTION**: ALWAYS delegate to workers for implementation or complex research tasks (Write, Edit, Bash, complex analysis). Track high level todos via TodoWrite.\n\n**MAXIMIZE PARALLELIZATION - Speed Through Concurrency**:\n- Default assumption: Tasks are INDEPENDENT and should run in PARALLEL\n- Only group tasks together if one task absolutely REQUIRES the other\'s output\n- When user requests multiple things \u2192 Launch multiple workers (one per task)\n- More parallel workers = faster completion and better user experience\n- Don\'t make users wait for sequential execution when tasks could run simultaneously\n\n**Your workflow**:\n1. Understand what the user wants\n2. Optionally: Make 1-2 quick tool calls for context IF it helps (prefer skipping this and delegating)\n3. Launch worker(s) with complete context and clear instructions\n4. Return control immediately - stay responsive for user interaction\n\n**When to use tools yourself (RARE)**:\n- Single quick lookup (1-2 tool calls max, < 5 seconds)\n- Answering "what/where/how does X work?" questions IF it\'s a single file or simple pattern\n- If you\'re uncertain or it might take longer: delegate instead\n\n**When to delegate to workers (ALMOST ALWAYS)**:\n- ANY code modification, file writing, or command execution\n- Reading 2+ files or doing any analysis\n- Multi-step research or investigation tasks\n- Running tests, git operations, deployments\n- ANY task that takes more than a few seconds\n- When in doubt: delegate\n\n**Best practices**:\n- **MAXIMIZE PARALLELIZATION**: Launch separate workers for independent tasks - DO NOT group tasks together unless they truly depend on each other\n- **DEFAULT TO PARALLEL**: If tasks CAN run in parallel, they SHOULD run in parallel\n- Delegate liberally - keep your context free and responsive\n- Trust worker outputs - don\'t verify or double-check\n- Use TodoWrite for complex multi-step work (Any sequential worker tasks with 2+ distinct steps)\n- Reference code with `file_path:line_number` format\n\n## Using Your Tools Effectively\n\nYou have read-only tools available: **Read, Grep, Glob, WebFetch, WebSearch, TodoWrite**\n\n**Key tips**:\n- **Stay responsive**: Avoid blocking your context with work - delegate instead of using tools directly\n- **Limit tool use**: If you must use tools, limit to 1-2 calls maximum, then delegate\n- **Code references**: Use `file_path:line_number` format when explaining locations\n- **Prefer delegation**: When in doubt, delegate to a worker to keep your context responsive\n\n## Managing Todos with TodoWrite\n\nUse the TodoWrite tool to track and manage complex tasks. This is CRITICAL for giving users visibility into progress.\n\n### When to Use TodoWrite\n\n**Use TodoWrite for:**\n- Complex multi-step tasks requiring 2+ distinct sequential steps\n- Non-trivial tasks that need careful planning or multiple operations\n- When the user explicitly requests a todo list\n- When users provide multiple tasks (numbered list, comma-separated, etc.)\n- To track progress on ongoing work and give users visibility into what workers are doing\n\n**DO NOT use TodoWrite for:**\n- Single, straightforward tasks\n- Trivial tasks where tracking provides no benefit\n- Tasks completable in less than 2 trivial steps\n- Purely conversational or informational requests\n\n### Todo States\n\nTodos have three states:\n- **pending**: Task not yet started\n- **in_progress**: Currently working on this task (ONLY ONE todo should be in_progress at a time)\n- **completed**: Task finished successfully\n\n### Todo Descriptions\n\nEach todo needs two forms:\n- **content**: Imperative form - what needs to be done (e.g., "Implement authentication", "Run tests")\n- **activeForm**: Present continuous form - shown during execution (e.g., "Implementing authentication", "Running tests")\n\n### Best Practices\n\n**Todo Management**:\n- Update todo status in real-time as work progresses\n- Mark todos complete IMMEDIATELY after finishing (don\'t batch completions)\n- Exactly ONE todo should be in_progress at any time (not less, not more)\n- Complete current todos before starting new ones\n- Remove todos that are no longer relevant from the list entirely\n\n**Todo Completion Requirements**:\n- ONLY mark a todo as completed when you have FULLY accomplished it\n- If workers encounter errors, blockers, or cannot finish, keep the todo as in_progress\n- When blocked, create a new todo describing what needs to be resolved\n- Never mark a todo as completed if:\n - Workers reported failures or errors\n - Implementation is partial\n - Tests are failing\n - You couldn\'t complete the work for any reason\n\n**Todo Breakdown**:\n- Create specific, actionable items\n- Break complex tasks into smaller, manageable steps\n- Use clear, descriptive todo names\n- Always provide both content and activeForm\n\n### Examples\n\n**Example 1 - Multi-step implementation:**\n```\nUser: "Add authentication with Google OAuth to the API"\n\nYou: [TodoWrite with todos:]\n 1. content: "Research authentication implementation", activeForm: "Researching authentication implementation", status: in_progress\n 2. content: "Implement Google OAuth integration", activeForm: "Implementing Google OAuth integration", status: pending\n 3. content: "Add authentication middleware", activeForm: "Adding authentication middleware", status: pending\n 4. content: "Test authentication flow", activeForm: "Testing authentication flow", status: pending\n\n[Launch worker for step 1]\n... worker completes research ...\n[TodoWrite: mark todo 1 as completed, mark todo 2 as in_progress]\n[Launch worker for step 2]\n... and so on ...\n```\n\n**Example 2 - User provides list:**\n```\nUser: "I need you to: 1) refactor the database layer, 2) add error handling, 3) write tests"\n\nYou: [TodoWrite with todos:]\n 1. content: "Refactor database layer", activeForm: "Refactoring database layer", status: pending\n 2. content: "Add error handling to database layer", activeForm: "Adding error handling", status: pending\n 3. content: "Write tests for database layer", activeForm: "Writing tests", status: pending\n\n[Launch workers for each task - can be parallel or sequential depending on dependencies]\n```\n\n**Example 3 - DO NOT use todos for simple tasks:**\n```\nUser: "What authentication method does the API use?"\n\nYou: [No todos needed - single information lookup]\n [Launch worker or use Grep to find the answer]\n```\n\n### Todo Lifecycle\n\nWhen using todos with workers:\n1. Create todos before launching workers (gives user visibility)\n2. Mark the corresponding todo as `in_progress` when launching a worker\n3. When worker completes successfully \u2192 mark todo as `completed`\n4. When worker fails or encounters issues \u2192 keep as `in_progress`, possibly add new todo for resolution\n5. Only ONE todo should be `in_progress` at a time\n\n## How You Get Triggered\n\nYou will generate responses in two scenarios:\n\n1. **User sends a message** - Normal user interaction. You should acknowledge and launch workers if needed.\n\n2. **Worker completes** - When a worker finishes (success, error, or cancel), a [WORKER STATUS UPDATE] message is automatically added to the conversation, and you are triggered to generate a response. You should:\n - Look at the status update message to see ALL worker states\n - The completed worker is marked with >>> and "JUST COMPLETED"\n - Provide an INCREMENTAL summary of only the NEW completion\n - DO NOT re-summarize workers you\'ve already reported on\n\nThink of it like notifications: you\'re "woken up" when something changes, the status update message shows you what changed, and you report the change.\n\n## When to Launch Workers\n\nLaunch workers for ALMOST EVERYTHING to stay responsive:\n\n- **Writing or modifying code** - "Add error handling to the login function" \u2192 Delegate immediately\n- **Running commands or tests** - "Run the test suite and report any failures" \u2192 Delegate immediately\n- **Git operations** - "Create a commit with these changes" \u2192 Delegate immediately\n- **Multi-step implementation tasks** - "Implement a new feature for user notifications" \u2192 Delegate immediately\n- **Complex analysis requiring multiple files** - "Analyze the entire authentication flow" \u2192 Delegate immediately\n- **Complex sequential tasks** - "Research and implement an OAuth provider integration" \u2192 Delegate in sequence\n- **Research tasks** - "Find all API endpoints and document their parameters" \u2192 Delegate immediately\n- **Parallel work** - Launch multiple workers when tasks can happen simultaneously (DEFAULT: tasks are parallel unless proven otherwise)\n- **Independent changes** - "Fix bug X and add feature Y" \u2192 Launch 2 separate workers, don\'t combine\n- **Most questions beyond simple lookups** - "How does the auth system work?" \u2192 Delegate to worker to research\n\n**When launching workers:**\n- **Skip context gathering** - Let the worker handle it. Delegate immediately to stay responsive.\n- **Add a Todo** to track the worker\'s task (use TodoWrite for 2+ sequential steps)\n- **Trust workers** to read files, explore code, and gather their own context\n- **Provide clear instructions** - What to do, what to return, what the goal is\n\n**ONLY use tools directly for:**\n- Single, trivial information requests (1 grep, 1 file read - if truly quick)\n- If you\'re uncertain or it might block you: delegate instead\n\n## When to Message Existing Workers (Resteering)\n\nUse the **message_worker** tool to send additional context or requirements to running workers:\n\n**When to message an existing worker:**\n- User provides clarifications or additions to an in-progress task\n- User adds requirements that logically belong to the current task\n- User provides context that would help the running worker\n- The new request is clearly related to what the worker has been working on and would benefit from shared context\n\n**When to launch a NEW worker instead:**\n- The request is completely unrelated to existing tasks\n- The user explicitly wants a separate task\n- The work would conflict with or duplicate an existing worker\'s goal\n- Clean context would be beneficial to a subsequent task\n\n**Choosing interrupt vs queue:**\n- **interrupt=false (default)**: Queue the message to be delivered after the worker\'s current turn\n - Use for: Adding requirements, providing context, clarifications\n - The worker continues current work, then receives your message\n- **interrupt=true**: Stop current work immediately and redirect\n - Use ONLY when: User explicitly says to stop/interrupt, or the new requirement significantly contradicts the current direction\n - The worker\'s current generation is stopped and your message is delivered immediately\n\n**After Messaging a Worker:**\n- **DO NOT** use get_workers to check the status after messaging a worker\n- The worker will automatically notify you when it completes (you\'ll be triggered with a [WORKER STATUS UPDATE])\n- Just acknowledge that you sent the message and wait for the completion notification\n\n## How to Delegate Effectively\n\nWhen launching workers, provide:\n\n**Clear, autonomous instructions**: Tell the worker exactly what to do and what information to return. Workers cannot ask you for clarification.\n\n**Complete context**: Include all relevant details the worker needs to complete the task independently. Use Read/Grep/Glob tools first to gather context, then provide it to the worker.\n\n**Expected output format**: Specify what information you need back from the worker.\n\n**Gather context before delegating**: For implementation tasks, use read-only tools first to understand the current state, then provide that context to workers.\n\n**Example excellent delegation (with context gathering)**:\n```\nUser: "Add rate limiting to the API"\n\n1. [Read: src/api/server.ts] - See current API setup\n2. [Grep: pattern="middleware", output_mode="content"] - Check existing middleware\n\n3. launch_worker with prompt: "Add rate limiting middleware to the API. Current setup:\n - Express server in src/api/server.ts using middleware pattern\n - Existing middleware: helmet, cors, body-parser\n - Routes defined in src/api/routes/\n\n Add rate limiting using express-rate-limit. Apply globally with 100 requests per 15 minutes.\n Document the configuration in comments."\n```\n\n## Multiple Workers\n\n**CRITICAL: MAXIMIZE PARALLELIZATION**\n\nLaunch workers concurrently whenever possible by calling launch_worker multiple times in a single response. DO NOT group independent tasks into a single worker.\n\n**The Parallelization Rule**:\n- If tasks are independent and don\'t strictly depend on each other\'s results \u2192 Launch SEPARATE workers\n- Only combine tasks into one worker if the second task absolutely REQUIRES the first task\'s output\n- When in doubt, launch separate workers - err on the side of more parallelization\n\n**Examples of PARALLEL tasks** (launch separate workers):\n- "Add logging to the API and update the documentation" \u2192 2 workers (independent changes)\n- "Fix the auth bug and add unit tests" \u2192 2 workers (can test independently)\n- "Refactor component A and refactor component B" \u2192 2 workers (independent refactors)\n- "Update the database schema and update the API endpoints" \u2192 2 workers (likely independent unless explicitly linked)\n- "Check the tests and review the API docs" \u2192 2 workers (completely independent)\n\n**Examples of SEQUENTIAL tasks** (must be one after another):\n- "Research the best logging library, then implement it" \u2192 Worker 1 researches, then Worker 2 implements with that info\n- "Run the tests, then fix any failures found" \u2192 Worker 1 runs tests, then Worker 2 fixes based on results\n- "Analyze the performance bottleneck, then optimize it" \u2192 Worker 1 analyzes, Worker 2 uses those findings\n\n**Good Example** (maximizing parallelization):\n```\nUser: "Add error handling to the API, update the docs, and write tests"\n\nResponse:\n- Launch worker 1: "Add comprehensive error handling to the API"\n- Launch worker 2: "Update documentation to reflect new error handling patterns"\n- Launch worker 3: "Write tests for error handling scenarios"\n```\n\n**Bad Example** (unnecessary grouping):\n```\nUser: "Add error handling to the API, update the docs, and write tests"\n\nResponse:\n- Launch worker 1: "Add error handling, update docs, and write tests"\n \u274C This is BAD - these are independent tasks that could run in parallel\n```\n\n**Edge Case - Minor Dependencies**: Even if there\'s a soft dependency (e.g., docs should reflect the code), prefer parallel execution. Workers can read the latest code state, so documentation can be written based on the new code even if both workers run simultaneously.\n\n## Worker Status Tracking\n\n**Status in Message History**: When workers complete, a special status update message is added to the conversation history. This message shows:\n- ALL current worker statuses\n- Which worker just completed (marked with >>> and "JUST COMPLETED" note)\n- Which workers are still running\n- **FULL PROGRESS LOG** for completed workers - showing everything the worker did\n\n**CRITICAL: Read the Full Progress Log**\n\nWhen a worker completes, you receive the COMPLETE history of everything it did:\n- All status updates the worker sent\n- All orchestrator messages you sent to the worker (marked with [ORCHESTRATOR MESSAGE])\n- The entire sequence of work from start to finish\n\n**Before creating new workers:**\n1. **CHECK THE PROGRESS LOG** - See what the completed worker actually did\n2. **Look for [ORCHESTRATOR MESSAGE] entries** - These show you already resteered this worker\n3. **DO NOT create duplicate workers** - If you already messaged a worker to do something, it\'s done or in the log\n4. **Trust the progress log** - It shows you the complete picture of what was accomplished\n\n**DO NOT use get_workers tool**: All worker information is already in your conversation history via status update messages. Only use get_workers if you have a specific edge case reason. Specifically:\n- **Never** call get_workers immediately after launching a worker\n- **Never** call get_workers after messaging a worker with message_worker\n- **Never** poll workers - you will be automatically notified when they complete\n\n**Automatic Notifications**: When a worker completes, a status update message is automatically added to the conversation, and you are triggered to generate a response. This happens automatically - you don\'t need to check or poll.\n\n## Your Response Pattern\n\n### When User Makes Initial Request:\n1. **Acknowledge the request** - Briefly confirm what you\'ll do\n2. **Launch worker(s)** - Delegate with clear instructions using meaningful task names\n3. **Inform about tracking** - Let user know workers are running and they\'ll be notified upon completion\n\n### When Workers Complete (Automatic Trigger):\n1. **Check the "Current Worker Status" section** - Review what just completed and what\'s still running\n2. **Identify NEW completions** - Only summarize workers that just finished. DO NOT repeat summaries for workers you already reported on.\n3. **Provide incremental summary** - Report ONLY the new information:\n - Which worker(s) just completed\n - Their results (success/error)\n - Next steps if applicable\n4. **Avoid repetition** - If you\'ve already told the user about a worker\'s completion in a previous message, don\'t mention it again unless directly relevant to a new development\n\n### Incremental Summary Guidelines:\n\n**CRITICAL: DO NOT RE-SUMMARIZE ALREADY-REPORTED COMPLETIONS**\n\nYou will be automatically triggered when workers complete. Each time this happens, you receive a [WORKER STATUS UPDATE] message showing ALL workers (both completed and running). However, you should ONLY report on the worker that JUST completed (marked with >>> and "JUST COMPLETED").\n\n**Rules:**\n- **ONLY report the NEW completion** - The worker marked with >>> in the status update\n- **DO NOT re-explain or re-summarize** previous completions you\'ve already told the user about\n- **DO NOT provide recaps** of what previous workers did unless the user explicitly asks\n- **Keep responses minimal** - Just acknowledge the new completion briefly\n- **Trust your previous summaries** - You already explained things once, don\'t repeat\n\n**Example responses:**\n- **First completion**: "The [task name] worker completed. [Brief 1-2 sentence result summary]"\n- **Second completion**: "The [task name] worker also finished. [Brief result]." \u2190 DO NOT re-mention first worker\n- **Third completion**: "The [task name] worker is done. All tasks complete." \u2190 DO NOT re-mention first two workers\n\n**When ALL workers complete:**\n- If you\'ve been reporting each completion incrementally, just say: "All tasks complete."\n- DO NOT provide a comprehensive summary of everything unless user asks\n\n## Remember\n\n**Your primary goal: STAY RESPONSIVE**\n- Your context must remain unblocked for user interaction\n- Tool calls and work block your ability to respond to users\n- Delegate liberally - it\'s not just good practice, it\'s architecturally required\n- Users need a responsive conversational partner, not a busy worker\n\n**Your role:**\n- You are a COORDINATOR, not an IMPLEMENTER\n- Workers are your hands - you are the brain\n- Delegation is not optional - it\'s required for responsiveness\n- The faster you delegate, the faster work gets done AND the more responsive you stay\n- Never apologize for delegating - it\'s your job and keeps you responsive\n\n**About Worker Status:**\n- ALL worker statuses are in your conversation history via [WORKER STATUS UPDATE] messages\n- The completed worker is marked with >>> and "JUST COMPLETED (triggered this generation)"\n- DO NOT use get_workers tool unless absolutely necessary (you already have the info in message history)\n- You\'ll be automatically triggered when workers complete with a status update message\n\n**About Summaries:**\n- Provide INCREMENTAL summaries - only report NEW completions\n- DO NOT repeat information about workers you already told the user about\n- Focus on what changed since your last message\n- Avoid comprehensive recaps unless explicitly requested\n\n**When in doubt: DELEGATE**. Even if you think you can handle it quickly - delegate to stay responsive.\n';
|
|
1156
|
+
// src/orchestrator/orchestrator-prompt.ts
|
|
1157
|
+
import { readFileSync } from "fs";
|
|
1158
|
+
var orchestratorPromptUrl = new URL("./orchestrator-prompt.md", import.meta.url);
|
|
1159
|
+
var ORCHESTRATOR_SYSTEM_PROMPT = readFileSync(orchestratorPromptUrl, "utf-8");
|
|
1097
1160
|
|
|
1098
1161
|
// src/orchestrator/agent-orchestrator.ts
|
|
1099
1162
|
var WORKER_LAUNCH_TOOL = "mcp__worker__launch_worker";
|
|
@@ -1131,11 +1194,16 @@ var AgentOrchestrator = class {
|
|
|
1131
1194
|
});
|
|
1132
1195
|
const mainClientFactory = dependencies?.createMainClient ?? (() => new AIClient({
|
|
1133
1196
|
defaults: {
|
|
1134
|
-
allowedTools: [
|
|
1197
|
+
allowedTools: [
|
|
1198
|
+
...BASE_ALLOWED_ORCHESTRATOR_TOOLS,
|
|
1199
|
+
WORKER_LAUNCH_TOOL,
|
|
1200
|
+
WORKER_GET_TOOL,
|
|
1201
|
+
WORKER_MESSAGE_TOOL
|
|
1202
|
+
],
|
|
1135
1203
|
mcpServers: {
|
|
1136
1204
|
worker: workerMcpServer
|
|
1137
1205
|
},
|
|
1138
|
-
systemPrompt:
|
|
1206
|
+
systemPrompt: ORCHESTRATOR_SYSTEM_PROMPT
|
|
1139
1207
|
}
|
|
1140
1208
|
}));
|
|
1141
1209
|
this.aiClient = mainClientFactory();
|
|
@@ -1530,7 +1598,8 @@ var AgentOrchestrator = class {
|
|
|
1530
1598
|
toolName: event.toolName,
|
|
1531
1599
|
workerId: event.record.id,
|
|
1532
1600
|
task: event.record.task,
|
|
1533
|
-
targetSessionId
|
|
1601
|
+
targetSessionId,
|
|
1602
|
+
toolUseId: event.toolUseId
|
|
1534
1603
|
});
|
|
1535
1604
|
this.broadcastToAll({
|
|
1536
1605
|
type: "tool_call",
|
|
@@ -1547,6 +1616,13 @@ var AgentOrchestrator = class {
|
|
|
1547
1616
|
if (!event.toolUseId) {
|
|
1548
1617
|
return;
|
|
1549
1618
|
}
|
|
1619
|
+
console.log("[Orchestrator] Broadcasting worker tool_result:", {
|
|
1620
|
+
toolName: event.toolName,
|
|
1621
|
+
workerId: event.record.id,
|
|
1622
|
+
task: event.record.task,
|
|
1623
|
+
targetSessionId,
|
|
1624
|
+
toolUseId: event.toolUseId
|
|
1625
|
+
});
|
|
1550
1626
|
this.broadcastToAll({
|
|
1551
1627
|
type: "tool_result",
|
|
1552
1628
|
sessionId: targetSessionId,
|
|
@@ -1598,7 +1674,12 @@ var AgentOrchestrator = class {
|
|
|
1598
1674
|
metadata,
|
|
1599
1675
|
messageId: randomUUID3()
|
|
1600
1676
|
});
|
|
1601
|
-
session.logInbound({
|
|
1677
|
+
session.logInbound({
|
|
1678
|
+
type: "worker_result",
|
|
1679
|
+
workerId: record.id,
|
|
1680
|
+
workerTask: record.task,
|
|
1681
|
+
summary
|
|
1682
|
+
});
|
|
1602
1683
|
session.announceInboundMessage(summary, "worker", record.id, record.task);
|
|
1603
1684
|
const statusMessage = this.buildWorkerStatusMessage(record.id);
|
|
1604
1685
|
session.enqueueUserMessage(statusMessage, {
|
|
@@ -1784,8 +1865,8 @@ var ApiClient = class {
|
|
|
1784
1865
|
constructor(options) {
|
|
1785
1866
|
this.baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
1786
1867
|
}
|
|
1787
|
-
async getJson(
|
|
1788
|
-
const url = resolveUrl(this.baseUrl,
|
|
1868
|
+
async getJson(path5, options) {
|
|
1869
|
+
const url = resolveUrl(this.baseUrl, path5);
|
|
1789
1870
|
const headers = this.buildHeaders(options);
|
|
1790
1871
|
const response = await fetch(url, {
|
|
1791
1872
|
method: "GET",
|
|
@@ -1794,8 +1875,8 @@ var ApiClient = class {
|
|
|
1794
1875
|
});
|
|
1795
1876
|
return await this.handleResponse(response);
|
|
1796
1877
|
}
|
|
1797
|
-
async postJson(
|
|
1798
|
-
const url = resolveUrl(this.baseUrl,
|
|
1878
|
+
async postJson(path5, body, options) {
|
|
1879
|
+
const url = resolveUrl(this.baseUrl, path5);
|
|
1799
1880
|
const headers = this.buildHeaders(options, { includeJson: true });
|
|
1800
1881
|
const response = await fetch(url, {
|
|
1801
1882
|
method: "POST",
|
|
@@ -1853,12 +1934,12 @@ function normalizeBaseUrl(raw) {
|
|
|
1853
1934
|
}
|
|
1854
1935
|
return raw.endsWith("/") ? raw : `${raw}/`;
|
|
1855
1936
|
}
|
|
1856
|
-
function resolveUrl(base,
|
|
1937
|
+
function resolveUrl(base, path5) {
|
|
1857
1938
|
const baseUrl = new URL(base);
|
|
1858
|
-
if (!
|
|
1859
|
-
|
|
1939
|
+
if (!path5.startsWith("/")) {
|
|
1940
|
+
path5 = `/${path5}`;
|
|
1860
1941
|
}
|
|
1861
|
-
const resolved = new URL(
|
|
1942
|
+
const resolved = new URL(path5, baseUrl);
|
|
1862
1943
|
return resolved.toString();
|
|
1863
1944
|
}
|
|
1864
1945
|
function extractDetail(payload) {
|
|
@@ -2011,9 +2092,6 @@ function deriveAccessExpiry(session) {
|
|
|
2011
2092
|
return void 0;
|
|
2012
2093
|
}
|
|
2013
2094
|
|
|
2014
|
-
// src/auth/login-flow.ts
|
|
2015
|
-
import { spawn } from "child_process";
|
|
2016
|
-
|
|
2017
2095
|
// src/utils/prompt.ts
|
|
2018
2096
|
import readline from "readline/promises";
|
|
2019
2097
|
import { stdin as input, stdout as output } from "process";
|
|
@@ -2165,7 +2243,11 @@ var LoopbackListener = class _LoopbackListener {
|
|
|
2165
2243
|
const sessionId = url.searchParams.get("session");
|
|
2166
2244
|
const state = url.searchParams.get("state");
|
|
2167
2245
|
if (!code) {
|
|
2168
|
-
this.respond(
|
|
2246
|
+
this.respond(
|
|
2247
|
+
response,
|
|
2248
|
+
400,
|
|
2249
|
+
buildMessageHtml("Missing authorization code. Return to your CLI and try again.")
|
|
2250
|
+
);
|
|
2169
2251
|
return;
|
|
2170
2252
|
}
|
|
2171
2253
|
this.respond(response, 200, buildSuccessHtml());
|
|
@@ -2185,7 +2267,7 @@ var LoopbackListener = class _LoopbackListener {
|
|
|
2185
2267
|
}
|
|
2186
2268
|
};
|
|
2187
2269
|
function buildSuccessHtml() {
|
|
2188
|
-
const redirectTarget = "https://
|
|
2270
|
+
const redirectTarget = "https://www.omnara.com/dashboard";
|
|
2189
2271
|
return `<!doctype html>
|
|
2190
2272
|
<html lang="en">
|
|
2191
2273
|
<head>
|
|
@@ -2240,6 +2322,18 @@ function buildMessageHtml(message) {
|
|
|
2240
2322
|
</html>`;
|
|
2241
2323
|
}
|
|
2242
2324
|
|
|
2325
|
+
// src/utils/browser.ts
|
|
2326
|
+
import { spawn } from "child_process";
|
|
2327
|
+
function tryOpenBrowser(url) {
|
|
2328
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
2329
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
2330
|
+
try {
|
|
2331
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
2332
|
+
child.unref();
|
|
2333
|
+
} catch {
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2243
2337
|
// src/auth/login-flow.ts
|
|
2244
2338
|
async function performVerificationLogin({
|
|
2245
2339
|
backendUrlOption,
|
|
@@ -2339,8 +2433,8 @@ async function performVerificationLogin({
|
|
|
2339
2433
|
}
|
|
2340
2434
|
return latestCredentials;
|
|
2341
2435
|
}
|
|
2342
|
-
var DEFAULT_BACKEND_URL = "https://omnara
|
|
2343
|
-
var DEFAULT_FRONTEND_URL = "https://
|
|
2436
|
+
var DEFAULT_BACKEND_URL = "https://api.omnara.com/";
|
|
2437
|
+
var DEFAULT_FRONTEND_URL = "https://www.omnara.com/";
|
|
2344
2438
|
function resolveBackendUrl(option) {
|
|
2345
2439
|
const candidate = option ?? process.env.OMNARA_BACKEND_URL ?? DEFAULT_BACKEND_URL;
|
|
2346
2440
|
return normalizeBaseUrl(candidate);
|
|
@@ -2393,15 +2487,6 @@ function printSessionInstructions(session) {
|
|
|
2393
2487
|
console.log(` ${session.verificationUriComplete}`);
|
|
2394
2488
|
console.log("Approve the request, then return to the CLI.");
|
|
2395
2489
|
}
|
|
2396
|
-
function tryOpenBrowser(url) {
|
|
2397
|
-
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
2398
|
-
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
2399
|
-
try {
|
|
2400
|
-
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
2401
|
-
child.unref();
|
|
2402
|
-
} catch {
|
|
2403
|
-
}
|
|
2404
|
-
}
|
|
2405
2490
|
function extractErrorMessage4(error) {
|
|
2406
2491
|
if (error instanceof Error && error.message) {
|
|
2407
2492
|
return error.message;
|
|
@@ -4736,6 +4821,7 @@ var RelayBridge = class extends EventEmitter2 {
|
|
|
4736
4821
|
authProviderPromise = null;
|
|
4737
4822
|
userSessionId = null;
|
|
4738
4823
|
currentAgentSessionId = null;
|
|
4824
|
+
browserOpened = false;
|
|
4739
4825
|
constructor(orchestrator, options) {
|
|
4740
4826
|
super();
|
|
4741
4827
|
this.orchestrator = orchestrator;
|
|
@@ -4779,6 +4865,7 @@ var RelayBridge = class extends EventEmitter2 {
|
|
|
4779
4865
|
async stop() {
|
|
4780
4866
|
this.stopped = true;
|
|
4781
4867
|
this.currentAgentSessionId = null;
|
|
4868
|
+
this.browserOpened = false;
|
|
4782
4869
|
if (this.pingTimer) {
|
|
4783
4870
|
clearInterval(this.pingTimer);
|
|
4784
4871
|
this.pingTimer = null;
|
|
@@ -4818,6 +4905,13 @@ var RelayBridge = class extends EventEmitter2 {
|
|
|
4818
4905
|
this.heartbeatIntervalMs = registration.relayHeartbeatIntervalSec * 1e3;
|
|
4819
4906
|
this.startPingTimer();
|
|
4820
4907
|
await this.sendPrimarySessionInit(ws);
|
|
4908
|
+
if (this.frontendUrl && !this.browserOpened) {
|
|
4909
|
+
const dashboardUrl = new URL("dashboard", this.frontendUrl);
|
|
4910
|
+
dashboardUrl.searchParams.set("session", agentSessionId);
|
|
4911
|
+
tryOpenBrowser(dashboardUrl.toString());
|
|
4912
|
+
this.logger.log(`Opened dashboard at ${dashboardUrl.toString()}`);
|
|
4913
|
+
this.browserOpened = true;
|
|
4914
|
+
}
|
|
4821
4915
|
this.flushQueue();
|
|
4822
4916
|
await this.handleWebsocketLifecycle(ws);
|
|
4823
4917
|
}
|
|
@@ -4836,7 +4930,7 @@ var RelayBridge = class extends EventEmitter2 {
|
|
|
4836
4930
|
if (!this.currentAgentSessionId) {
|
|
4837
4931
|
this.currentAgentSessionId = randomUUID4();
|
|
4838
4932
|
}
|
|
4839
|
-
|
|
4933
|
+
this.orchestrator.ensureSessionExists(this.currentAgentSessionId);
|
|
4840
4934
|
this.orchestrator.setMainCodingSessionId(this.currentAgentSessionId);
|
|
4841
4935
|
return this.currentAgentSessionId;
|
|
4842
4936
|
}
|
|
@@ -5316,16 +5410,16 @@ var RelayBridge = class extends EventEmitter2 {
|
|
|
5316
5410
|
}
|
|
5317
5411
|
}
|
|
5318
5412
|
};
|
|
5319
|
-
function buildRelayWebsocketUrl(baseUrl,
|
|
5413
|
+
function buildRelayWebsocketUrl(baseUrl, path5) {
|
|
5320
5414
|
const url = new URL(baseUrl);
|
|
5321
5415
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
5322
5416
|
url.search = "";
|
|
5323
5417
|
url.hash = "";
|
|
5324
|
-
if (
|
|
5325
|
-
url.pathname =
|
|
5418
|
+
if (path5.startsWith("/")) {
|
|
5419
|
+
url.pathname = path5;
|
|
5326
5420
|
} else {
|
|
5327
5421
|
const basePath = url.pathname.endsWith("/") ? url.pathname : `${url.pathname}/`;
|
|
5328
|
-
url.pathname = `${basePath}${
|
|
5422
|
+
url.pathname = `${basePath}${path5}`;
|
|
5329
5423
|
}
|
|
5330
5424
|
return url.toString();
|
|
5331
5425
|
}
|
|
@@ -5536,13 +5630,17 @@ function handleOutboundMessage(message, onPrompt, onSessionChange, onTurnComplet
|
|
|
5536
5630
|
}
|
|
5537
5631
|
switch (message.type) {
|
|
5538
5632
|
case "session_created":
|
|
5539
|
-
console.log(
|
|
5540
|
-
|
|
5633
|
+
console.log(
|
|
5634
|
+
`
|
|
5635
|
+
${formatSessionPrefix(message.sessionId)} [server] Session created: ${message.sessionId}`
|
|
5636
|
+
);
|
|
5541
5637
|
onSessionChange(message.sessionId);
|
|
5542
5638
|
break;
|
|
5543
5639
|
case "session_joined":
|
|
5544
|
-
console.log(
|
|
5545
|
-
|
|
5640
|
+
console.log(
|
|
5641
|
+
`
|
|
5642
|
+
${formatSessionPrefix(message.sessionId)} [server] Joined session: ${message.sessionId}`
|
|
5643
|
+
);
|
|
5546
5644
|
onSessionChange(message.sessionId);
|
|
5547
5645
|
break;
|
|
5548
5646
|
case "session_init":
|
|
@@ -5620,10 +5718,8 @@ ${formatSessionPrefix(message.sessionId)} [system] ${message.subtype}`);
|
|
|
5620
5718
|
}
|
|
5621
5719
|
break;
|
|
5622
5720
|
case "error":
|
|
5623
|
-
console.error(
|
|
5624
|
-
|
|
5625
|
-
${formatSessionPrefix(message.sessionId)} [error] ${message.message}`
|
|
5626
|
-
);
|
|
5721
|
+
console.error(`
|
|
5722
|
+
${formatSessionPrefix(message.sessionId)} [error] ${message.message}`);
|
|
5627
5723
|
onTurnComplete();
|
|
5628
5724
|
break;
|
|
5629
5725
|
case "pong":
|
|
@@ -5666,7 +5762,7 @@ Commands:
|
|
|
5666
5762
|
|
|
5667
5763
|
// src/voice/voice-service.ts
|
|
5668
5764
|
async function launchVoiceForSession(client, accessToken, options) {
|
|
5669
|
-
const
|
|
5765
|
+
const path5 = `/api/v1/voice/sessions/${options.sessionId}/launch`;
|
|
5670
5766
|
const requestBody = {};
|
|
5671
5767
|
if (options.force) {
|
|
5672
5768
|
requestBody.force = true;
|
|
@@ -5675,7 +5771,7 @@ async function launchVoiceForSession(client, accessToken, options) {
|
|
|
5675
5771
|
requestBody.room_name = options.roomName;
|
|
5676
5772
|
}
|
|
5677
5773
|
const response = await client.postJson(
|
|
5678
|
-
|
|
5774
|
+
path5,
|
|
5679
5775
|
requestBody,
|
|
5680
5776
|
{ accessToken }
|
|
5681
5777
|
);
|
|
@@ -5693,6 +5789,291 @@ async function launchVoiceForSession(client, accessToken, options) {
|
|
|
5693
5789
|
};
|
|
5694
5790
|
}
|
|
5695
5791
|
|
|
5792
|
+
// src/utils/claude-code-check.ts
|
|
5793
|
+
import { execSync } from "child_process";
|
|
5794
|
+
import chalk from "chalk";
|
|
5795
|
+
function checkClaudeCodeInstallation() {
|
|
5796
|
+
try {
|
|
5797
|
+
execSync("claude --version", {
|
|
5798
|
+
stdio: "ignore",
|
|
5799
|
+
encoding: "utf-8"
|
|
5800
|
+
});
|
|
5801
|
+
return true;
|
|
5802
|
+
} catch {
|
|
5803
|
+
return false;
|
|
5804
|
+
}
|
|
5805
|
+
}
|
|
5806
|
+
function displayClaudeCodeWarning() {
|
|
5807
|
+
console.log(chalk.yellow("\nWarning: Claude Code is not installed or not found in PATH"));
|
|
5808
|
+
console.log(
|
|
5809
|
+
chalk.red("\nIMPORTANT: Claude Code is REQUIRED for authentication with your account.")
|
|
5810
|
+
);
|
|
5811
|
+
console.log(chalk.white("\nClaude Code provides:"));
|
|
5812
|
+
console.log(chalk.gray(" - AI-powered code assistance"));
|
|
5813
|
+
console.log(chalk.gray(" - Intelligent code completion"));
|
|
5814
|
+
console.log(chalk.gray(" - Natural language to code translation"));
|
|
5815
|
+
console.log(chalk.gray(" - User authentication for API access"));
|
|
5816
|
+
console.log(chalk.white("\nTo install Claude Code:"));
|
|
5817
|
+
console.log(chalk.cyan(" 1. Visit: https://claude.ai/download"));
|
|
5818
|
+
console.log(chalk.cyan(" 2. Download and install Claude Code for your platform"));
|
|
5819
|
+
console.log(chalk.cyan(' 3. Make sure the "claude" command is available in your PATH'));
|
|
5820
|
+
console.log(chalk.yellow("\nNote: Without Claude Code, you cannot authenticate and access"));
|
|
5821
|
+
console.log(chalk.yellow("personalized features that require your user credentials.\n"));
|
|
5822
|
+
}
|
|
5823
|
+
function performClaudeCodeCheck(options = {}) {
|
|
5824
|
+
const { silent = false, exitOnMissing = false } = options;
|
|
5825
|
+
if (process.env.OMNARA_SKIP_CLAUDE_CHECK === "true") {
|
|
5826
|
+
return true;
|
|
5827
|
+
}
|
|
5828
|
+
const isInstalled = checkClaudeCodeInstallation();
|
|
5829
|
+
if (!isInstalled && !silent) {
|
|
5830
|
+
displayClaudeCodeWarning();
|
|
5831
|
+
if (exitOnMissing) {
|
|
5832
|
+
console.error(chalk.red("Exiting: Claude Code is required for this operation."));
|
|
5833
|
+
process.exit(1);
|
|
5834
|
+
}
|
|
5835
|
+
}
|
|
5836
|
+
return isInstalled;
|
|
5837
|
+
}
|
|
5838
|
+
function getClaudeCodeVersion() {
|
|
5839
|
+
try {
|
|
5840
|
+
const version = execSync("claude --version", {
|
|
5841
|
+
encoding: "utf-8",
|
|
5842
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5843
|
+
}).trim();
|
|
5844
|
+
return version;
|
|
5845
|
+
} catch {
|
|
5846
|
+
return null;
|
|
5847
|
+
}
|
|
5848
|
+
}
|
|
5849
|
+
|
|
5850
|
+
// src/utils/logger.ts
|
|
5851
|
+
import path3 from "path";
|
|
5852
|
+
function isProduction() {
|
|
5853
|
+
if (process.env.NODE_ENV === "production") {
|
|
5854
|
+
return true;
|
|
5855
|
+
}
|
|
5856
|
+
try {
|
|
5857
|
+
const currentFile = typeof __filename !== "undefined" ? __filename : "";
|
|
5858
|
+
const isInDist = currentFile.includes(path3.sep + "dist" + path3.sep);
|
|
5859
|
+
if (isInDist && process.env.NODE_ENV !== "development") {
|
|
5860
|
+
return true;
|
|
5861
|
+
}
|
|
5862
|
+
} catch {
|
|
5863
|
+
}
|
|
5864
|
+
const logLevel = process.env.OMNARA_LOG_LEVEL?.toLowerCase();
|
|
5865
|
+
if (logLevel === "none" || logLevel === "silent") {
|
|
5866
|
+
return true;
|
|
5867
|
+
}
|
|
5868
|
+
return false;
|
|
5869
|
+
}
|
|
5870
|
+
function getLogLevel() {
|
|
5871
|
+
const envLevel = process.env.OMNARA_LOG_LEVEL?.toLowerCase();
|
|
5872
|
+
switch (envLevel) {
|
|
5873
|
+
case "none":
|
|
5874
|
+
case "silent":
|
|
5875
|
+
return 0 /* NONE */;
|
|
5876
|
+
case "error":
|
|
5877
|
+
return 1 /* ERROR */;
|
|
5878
|
+
case "warn":
|
|
5879
|
+
case "warning":
|
|
5880
|
+
return 2 /* WARN */;
|
|
5881
|
+
case "info":
|
|
5882
|
+
return 3 /* INFO */;
|
|
5883
|
+
case "debug":
|
|
5884
|
+
return 4 /* DEBUG */;
|
|
5885
|
+
case "verbose":
|
|
5886
|
+
case "trace":
|
|
5887
|
+
return 5 /* VERBOSE */;
|
|
5888
|
+
default:
|
|
5889
|
+
return isProduction() ? 1 /* ERROR */ : 3 /* INFO */;
|
|
5890
|
+
}
|
|
5891
|
+
}
|
|
5892
|
+
var ControlledLogger = class {
|
|
5893
|
+
logLevel;
|
|
5894
|
+
forceOutput;
|
|
5895
|
+
_console;
|
|
5896
|
+
// Console properties we need to implement
|
|
5897
|
+
Console;
|
|
5898
|
+
constructor(options) {
|
|
5899
|
+
this.logLevel = options?.logLevel ?? getLogLevel();
|
|
5900
|
+
this.forceOutput = options?.forceOutput ?? false;
|
|
5901
|
+
this._console = console;
|
|
5902
|
+
this.Console = console.Console;
|
|
5903
|
+
}
|
|
5904
|
+
/**
|
|
5905
|
+
* Standard log output (INFO level)
|
|
5906
|
+
*/
|
|
5907
|
+
log(...args) {
|
|
5908
|
+
if (this.shouldLog(3 /* INFO */)) {
|
|
5909
|
+
console.log(...args);
|
|
5910
|
+
}
|
|
5911
|
+
}
|
|
5912
|
+
/**
|
|
5913
|
+
* Error output (always shown unless NONE)
|
|
5914
|
+
*/
|
|
5915
|
+
error(...args) {
|
|
5916
|
+
if (this.shouldLog(1 /* ERROR */)) {
|
|
5917
|
+
console.error(...args);
|
|
5918
|
+
}
|
|
5919
|
+
}
|
|
5920
|
+
/**
|
|
5921
|
+
* Warning output
|
|
5922
|
+
*/
|
|
5923
|
+
warn(...args) {
|
|
5924
|
+
if (this.shouldLog(2 /* WARN */)) {
|
|
5925
|
+
console.warn(...args);
|
|
5926
|
+
}
|
|
5927
|
+
}
|
|
5928
|
+
/**
|
|
5929
|
+
* Info level output
|
|
5930
|
+
*/
|
|
5931
|
+
info(...args) {
|
|
5932
|
+
if (this.shouldLog(3 /* INFO */)) {
|
|
5933
|
+
console.info(...args);
|
|
5934
|
+
}
|
|
5935
|
+
}
|
|
5936
|
+
/**
|
|
5937
|
+
* Debug output (only in development)
|
|
5938
|
+
*/
|
|
5939
|
+
debug(...args) {
|
|
5940
|
+
if (this.shouldLog(4 /* DEBUG */)) {
|
|
5941
|
+
console.debug(...args);
|
|
5942
|
+
}
|
|
5943
|
+
}
|
|
5944
|
+
/**
|
|
5945
|
+
* Verbose/trace output
|
|
5946
|
+
*/
|
|
5947
|
+
trace(...args) {
|
|
5948
|
+
if (this.shouldLog(5 /* VERBOSE */)) {
|
|
5949
|
+
console.log("[TRACE]", ...args);
|
|
5950
|
+
}
|
|
5951
|
+
}
|
|
5952
|
+
/**
|
|
5953
|
+
* Important messages that should always be shown to user
|
|
5954
|
+
* (authentication status, connection info, etc.)
|
|
5955
|
+
*/
|
|
5956
|
+
important(...args) {
|
|
5957
|
+
if (this.logLevel !== 0 /* NONE */ || this.forceOutput) {
|
|
5958
|
+
console.log(...args);
|
|
5959
|
+
}
|
|
5960
|
+
}
|
|
5961
|
+
/**
|
|
5962
|
+
* Success messages that should typically be shown
|
|
5963
|
+
*/
|
|
5964
|
+
success(...args) {
|
|
5965
|
+
if (this.shouldLog(2 /* WARN */) || this.forceOutput) {
|
|
5966
|
+
console.log(...args);
|
|
5967
|
+
}
|
|
5968
|
+
}
|
|
5969
|
+
/**
|
|
5970
|
+
* Check if a message should be logged at the given level
|
|
5971
|
+
*/
|
|
5972
|
+
shouldLog(level) {
|
|
5973
|
+
return this.forceOutput || this.logLevel >= level;
|
|
5974
|
+
}
|
|
5975
|
+
/**
|
|
5976
|
+
* Update the log level dynamically
|
|
5977
|
+
*/
|
|
5978
|
+
setLogLevel(level) {
|
|
5979
|
+
this.logLevel = level;
|
|
5980
|
+
}
|
|
5981
|
+
/**
|
|
5982
|
+
* Get current log level
|
|
5983
|
+
*/
|
|
5984
|
+
getLogLevel() {
|
|
5985
|
+
return this.logLevel;
|
|
5986
|
+
}
|
|
5987
|
+
// Console methods we delegate or control based on log level
|
|
5988
|
+
assert(condition, ...data) {
|
|
5989
|
+
if (this.shouldLog(1 /* ERROR */)) {
|
|
5990
|
+
this._console.assert(condition, ...data);
|
|
5991
|
+
}
|
|
5992
|
+
}
|
|
5993
|
+
clear() {
|
|
5994
|
+
this._console.clear();
|
|
5995
|
+
}
|
|
5996
|
+
count(label) {
|
|
5997
|
+
if (this.shouldLog(4 /* DEBUG */)) {
|
|
5998
|
+
this._console.count(label);
|
|
5999
|
+
}
|
|
6000
|
+
}
|
|
6001
|
+
countReset(label) {
|
|
6002
|
+
if (this.shouldLog(4 /* DEBUG */)) {
|
|
6003
|
+
this._console.countReset(label);
|
|
6004
|
+
}
|
|
6005
|
+
}
|
|
6006
|
+
dir(item, options) {
|
|
6007
|
+
if (this.shouldLog(4 /* DEBUG */)) {
|
|
6008
|
+
this._console.dir(item, options);
|
|
6009
|
+
}
|
|
6010
|
+
}
|
|
6011
|
+
dirxml(...data) {
|
|
6012
|
+
if (this.shouldLog(4 /* DEBUG */)) {
|
|
6013
|
+
this._console.dirxml(...data);
|
|
6014
|
+
}
|
|
6015
|
+
}
|
|
6016
|
+
group(...data) {
|
|
6017
|
+
if (this.shouldLog(4 /* DEBUG */)) {
|
|
6018
|
+
this._console.group(...data);
|
|
6019
|
+
}
|
|
6020
|
+
}
|
|
6021
|
+
groupCollapsed(...data) {
|
|
6022
|
+
if (this.shouldLog(4 /* DEBUG */)) {
|
|
6023
|
+
this._console.groupCollapsed(...data);
|
|
6024
|
+
}
|
|
6025
|
+
}
|
|
6026
|
+
groupEnd() {
|
|
6027
|
+
if (this.shouldLog(4 /* DEBUG */)) {
|
|
6028
|
+
this._console.groupEnd();
|
|
6029
|
+
}
|
|
6030
|
+
}
|
|
6031
|
+
table(tabularData, properties) {
|
|
6032
|
+
if (this.shouldLog(3 /* INFO */)) {
|
|
6033
|
+
this._console.table(tabularData, properties);
|
|
6034
|
+
}
|
|
6035
|
+
}
|
|
6036
|
+
time(label) {
|
|
6037
|
+
if (this.shouldLog(4 /* DEBUG */)) {
|
|
6038
|
+
this._console.time(label);
|
|
6039
|
+
}
|
|
6040
|
+
}
|
|
6041
|
+
timeEnd(label) {
|
|
6042
|
+
if (this.shouldLog(4 /* DEBUG */)) {
|
|
6043
|
+
this._console.timeEnd(label);
|
|
6044
|
+
}
|
|
6045
|
+
}
|
|
6046
|
+
timeLog(label, ...data) {
|
|
6047
|
+
if (this.shouldLog(4 /* DEBUG */)) {
|
|
6048
|
+
this._console.timeLog(label, ...data);
|
|
6049
|
+
}
|
|
6050
|
+
}
|
|
6051
|
+
timeStamp(label) {
|
|
6052
|
+
if (this.shouldLog(4 /* DEBUG */)) {
|
|
6053
|
+
this._console.timeStamp(label);
|
|
6054
|
+
}
|
|
6055
|
+
}
|
|
6056
|
+
profile(label) {
|
|
6057
|
+
if (this.shouldLog(4 /* DEBUG */)) {
|
|
6058
|
+
const consoleWithProfile = this._console;
|
|
6059
|
+
consoleWithProfile.profile?.(label);
|
|
6060
|
+
}
|
|
6061
|
+
}
|
|
6062
|
+
profileEnd(label) {
|
|
6063
|
+
if (this.shouldLog(4 /* DEBUG */)) {
|
|
6064
|
+
const consoleWithProfileEnd = this._console;
|
|
6065
|
+
consoleWithProfileEnd.profileEnd?.(label);
|
|
6066
|
+
}
|
|
6067
|
+
}
|
|
6068
|
+
};
|
|
6069
|
+
var logger = new ControlledLogger();
|
|
6070
|
+
function extractErrorMessage6(error) {
|
|
6071
|
+
if (error instanceof Error) {
|
|
6072
|
+
return error.message;
|
|
6073
|
+
}
|
|
6074
|
+
return String(error);
|
|
6075
|
+
}
|
|
6076
|
+
|
|
5696
6077
|
// src/commands/agent.ts
|
|
5697
6078
|
function buildAgentCommand() {
|
|
5698
6079
|
const agent = new Command("agent").description(
|
|
@@ -5710,6 +6091,12 @@ function buildAgentCommand() {
|
|
|
5710
6091
|
return agent;
|
|
5711
6092
|
}
|
|
5712
6093
|
async function runAgentOrchestrator(options = {}) {
|
|
6094
|
+
const claudeVersion = getClaudeCodeVersion();
|
|
6095
|
+
if (claudeVersion) {
|
|
6096
|
+
logger.info(`\u2713 Claude Code detected: ${claudeVersion}`);
|
|
6097
|
+
} else {
|
|
6098
|
+
logger.info("\u2139 Running without Claude Code integration");
|
|
6099
|
+
}
|
|
5713
6100
|
const orchestrator = new AgentOrchestrator();
|
|
5714
6101
|
await orchestrator.start();
|
|
5715
6102
|
const backendUrl = resolveBackendUrl(options.backendUrl);
|
|
@@ -5731,7 +6118,7 @@ async function runAgentOrchestrator(options = {}) {
|
|
|
5731
6118
|
try {
|
|
5732
6119
|
await bridge.start();
|
|
5733
6120
|
} catch (error) {
|
|
5734
|
-
|
|
6121
|
+
logger.error(`Relay orchestrator terminated: ${extractErrorMessage6(error)}`);
|
|
5735
6122
|
process.exitCode = process.exitCode || 1;
|
|
5736
6123
|
} finally {
|
|
5737
6124
|
await stop();
|
|
@@ -5745,10 +6132,10 @@ function createSignalHandler(onStop) {
|
|
|
5745
6132
|
return;
|
|
5746
6133
|
}
|
|
5747
6134
|
stopping = true;
|
|
5748
|
-
|
|
6135
|
+
logger.important(`
|
|
5749
6136
|
Received ${signal}. Shutting down agent orchestrator...`);
|
|
5750
6137
|
void onStop().catch((error) => {
|
|
5751
|
-
|
|
6138
|
+
logger.error("Error while shutting down agent orchestrator:", error);
|
|
5752
6139
|
});
|
|
5753
6140
|
};
|
|
5754
6141
|
const remove = () => {
|
|
@@ -5757,12 +6144,6 @@ Received ${signal}. Shutting down agent orchestrator...`);
|
|
|
5757
6144
|
};
|
|
5758
6145
|
return { handler, remove };
|
|
5759
6146
|
}
|
|
5760
|
-
function extractErrorMessage6(error) {
|
|
5761
|
-
if (error instanceof Error) {
|
|
5762
|
-
return error.message;
|
|
5763
|
-
}
|
|
5764
|
-
return String(error);
|
|
5765
|
-
}
|
|
5766
6147
|
async function runRepl(options) {
|
|
5767
6148
|
const backendUrl = resolveBackendUrl(options.backendUrl);
|
|
5768
6149
|
const frontendUrl = resolveFrontendUrl(options.frontendUrl);
|
|
@@ -5780,7 +6161,7 @@ async function runRepl(options) {
|
|
|
5780
6161
|
clientKind: "cli"
|
|
5781
6162
|
});
|
|
5782
6163
|
const wsUrl = buildRelayWebsocketUrl2(backendUrl, registration.relayWsPath);
|
|
5783
|
-
|
|
6164
|
+
logger.important(
|
|
5784
6165
|
`Using agent session ${options.sessionId} (user session ${registration.userSessionId})`
|
|
5785
6166
|
);
|
|
5786
6167
|
await runTerminalRepl({
|
|
@@ -5808,30 +6189,30 @@ async function runVoice(options) {
|
|
|
5808
6189
|
force: options.force ?? false,
|
|
5809
6190
|
roomName: options.roomName
|
|
5810
6191
|
});
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
6192
|
+
logger.success("\nVoice agent dispatched successfully.");
|
|
6193
|
+
logger.important(`Session ID: ${launch.sessionId}`);
|
|
6194
|
+
logger.important(`LiveKit URL: ${launch.livekitUrl}`);
|
|
6195
|
+
logger.important(`Room name: ${launch.roomName}`);
|
|
6196
|
+
logger.important(`Participant identity: ${launch.accessTokenIdentity}`);
|
|
6197
|
+
logger.important("\nUse this token to join the room:");
|
|
6198
|
+
logger.important(launch.accessToken);
|
|
6199
|
+
logger.log("\nOnce connected (for example via the LiveKit Agents playground),");
|
|
6200
|
+
logger.log("the voice worker will coordinate with the CLI coding agent through the relay.");
|
|
6201
|
+
logger.debug(`Relay websocket: ${launch.relayWsUrl}`);
|
|
6202
|
+
logger.debug(`Relay token (expires in ${launch.relayTokenExpiresIn}s):`);
|
|
6203
|
+
logger.debug(launch.relayToken);
|
|
6204
|
+
logger.important("\nKeep this information secure\u2014tokens grant access to your agent session.");
|
|
5824
6205
|
}
|
|
5825
|
-
function buildRelayWebsocketUrl2(baseUrl,
|
|
6206
|
+
function buildRelayWebsocketUrl2(baseUrl, path5) {
|
|
5826
6207
|
const url = new URL(baseUrl);
|
|
5827
6208
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
5828
6209
|
url.search = "";
|
|
5829
6210
|
url.hash = "";
|
|
5830
|
-
if (
|
|
5831
|
-
url.pathname =
|
|
6211
|
+
if (path5.startsWith("/")) {
|
|
6212
|
+
url.pathname = path5;
|
|
5832
6213
|
} else {
|
|
5833
6214
|
const basePath = url.pathname.endsWith("/") ? url.pathname : `${url.pathname}/`;
|
|
5834
|
-
url.pathname = `${basePath}${
|
|
6215
|
+
url.pathname = `${basePath}${path5}`;
|
|
5835
6216
|
}
|
|
5836
6217
|
return url.toString();
|
|
5837
6218
|
}
|
|
@@ -5907,9 +6288,79 @@ function extractErrorMessage7(error) {
|
|
|
5907
6288
|
return String(error);
|
|
5908
6289
|
}
|
|
5909
6290
|
|
|
5910
|
-
// src/
|
|
6291
|
+
// src/utils/version-check.ts
|
|
6292
|
+
import https from "https";
|
|
6293
|
+
import { createRequire } from "node:module";
|
|
5911
6294
|
var require2 = createRequire(import.meta.url);
|
|
5912
|
-
|
|
6295
|
+
async function fetchLatestVersion(packageName) {
|
|
6296
|
+
return new Promise((resolve) => {
|
|
6297
|
+
const url = `https://registry.npmjs.org/${packageName}`;
|
|
6298
|
+
https.get(url, { timeout: 3e3 }, (res) => {
|
|
6299
|
+
let data = "";
|
|
6300
|
+
res.on("data", (chunk) => {
|
|
6301
|
+
data += chunk;
|
|
6302
|
+
});
|
|
6303
|
+
res.on("end", () => {
|
|
6304
|
+
try {
|
|
6305
|
+
if (res.statusCode !== 200) {
|
|
6306
|
+
resolve(null);
|
|
6307
|
+
return;
|
|
6308
|
+
}
|
|
6309
|
+
const packageInfo = JSON.parse(data);
|
|
6310
|
+
resolve(packageInfo["dist-tags"]?.latest || null);
|
|
6311
|
+
} catch {
|
|
6312
|
+
resolve(null);
|
|
6313
|
+
}
|
|
6314
|
+
});
|
|
6315
|
+
}).on("error", () => {
|
|
6316
|
+
resolve(null);
|
|
6317
|
+
}).on("timeout", () => {
|
|
6318
|
+
resolve(null);
|
|
6319
|
+
});
|
|
6320
|
+
});
|
|
6321
|
+
}
|
|
6322
|
+
function compareVersions(v1, v2) {
|
|
6323
|
+
const parts1 = v1.split(".").map(Number);
|
|
6324
|
+
const parts2 = v2.split(".").map(Number);
|
|
6325
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
6326
|
+
const part1 = parts1[i] || 0;
|
|
6327
|
+
const part2 = parts2[i] || 0;
|
|
6328
|
+
if (part1 > part2) return 1;
|
|
6329
|
+
if (part1 < part2) return -1;
|
|
6330
|
+
}
|
|
6331
|
+
return 0;
|
|
6332
|
+
}
|
|
6333
|
+
async function checkForUpdates() {
|
|
6334
|
+
try {
|
|
6335
|
+
const packageJson2 = require2("../../package.json");
|
|
6336
|
+
const currentVersion = packageJson2.version;
|
|
6337
|
+
const packageName = packageJson2.name;
|
|
6338
|
+
if (!currentVersion || !packageName) {
|
|
6339
|
+
return;
|
|
6340
|
+
}
|
|
6341
|
+
const latestVersion = await fetchLatestVersion(packageName);
|
|
6342
|
+
if (!latestVersion) {
|
|
6343
|
+
return;
|
|
6344
|
+
}
|
|
6345
|
+
if (compareVersions(latestVersion, currentVersion) > 0) {
|
|
6346
|
+
console.log("\n" + "\u2550".repeat(60));
|
|
6347
|
+
console.log("Update available!");
|
|
6348
|
+
console.log(`Current version: ${currentVersion}`);
|
|
6349
|
+
console.log(`Latest version: ${latestVersion}`);
|
|
6350
|
+
console.log(`Run \`npm install -g ${packageName}@latest\` to update`);
|
|
6351
|
+
console.log("\u2550".repeat(60) + "\n");
|
|
6352
|
+
}
|
|
6353
|
+
} catch {
|
|
6354
|
+
}
|
|
6355
|
+
}
|
|
6356
|
+
function checkForUpdatesInBackground() {
|
|
6357
|
+
checkForUpdates().catch(() => {
|
|
6358
|
+
});
|
|
6359
|
+
}
|
|
6360
|
+
|
|
6361
|
+
// src/cli.ts
|
|
6362
|
+
var require3 = createRequire2(import.meta.url);
|
|
6363
|
+
var packageJson = require3("../package.json");
|
|
5913
6364
|
function createProgram() {
|
|
5914
6365
|
const program = new Command3();
|
|
5915
6366
|
program.name("omnara").description(packageJson.description ?? "Command-line interface for Omnara.").version(packageJson.version ?? "0.0.0").option("--backend-url <url>", "Override backend API base URL used for login").option("--frontend-url <url>", "Override frontend URL opened during login").action(async () => {
|
|
@@ -5922,6 +6373,8 @@ function createProgram() {
|
|
|
5922
6373
|
}
|
|
5923
6374
|
async function run(argv) {
|
|
5924
6375
|
const program = createProgram();
|
|
6376
|
+
performClaudeCodeCheck({ silent: false, exitOnMissing: false });
|
|
6377
|
+
checkForUpdatesInBackground();
|
|
5925
6378
|
try {
|
|
5926
6379
|
await program.parseAsync(argv);
|
|
5927
6380
|
} catch (error) {
|
|
@@ -5932,13 +6385,13 @@ function isMainModule() {
|
|
|
5932
6385
|
if (!import.meta.url || !process.argv[1]) {
|
|
5933
6386
|
return false;
|
|
5934
6387
|
}
|
|
5935
|
-
const executedPath =
|
|
6388
|
+
const executedPath = path4.resolve(process.argv[1]);
|
|
5936
6389
|
const currentPath = import.meta.url.startsWith("file://") ? decodeURIComponent(import.meta.url.slice(7)) : import.meta.url;
|
|
5937
|
-
return
|
|
6390
|
+
return path4.normalize(currentPath) === path4.normalize(executedPath);
|
|
5938
6391
|
}
|
|
5939
6392
|
if (isMainModule()) {
|
|
5940
6393
|
run(process.argv).catch((error) => {
|
|
5941
|
-
|
|
6394
|
+
logger.error(error);
|
|
5942
6395
|
process.exitCode = 1;
|
|
5943
6396
|
});
|
|
5944
6397
|
}
|