@agentrix/cli 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,10 @@
1
1
  import yargs from 'yargs';
2
2
  import { hideBin } from 'yargs/helpers';
3
3
  import chalk from 'chalk';
4
- import { encodeBase64, createKeyPairWithUit8Array, encryptMachineEncryptionKey, generateAESKey, decodeBase64, decryptWithEphemeralKey, createEventId, encryptFileContent, machineAuth, encryptSdkMessage, decryptSdkMessage, loadAgentConfig, getAgentContext, workerAuth } from '@agentrix/shared';
5
- import { randomBytes, randomUUID as randomUUID$1 } from 'node:crypto';
4
+ import { encodeBase64, createKeyPairWithUit8Array, encryptMachineEncryptionKey, generateAESKey, decodeBase64, decryptWithEphemeralKey, createEventId, encryptFileContent, machineAuth, encryptSdkMessage, decryptSdkMessage, loadAgentConfig, getAgentContext, workerAuth, isAskUserResponseMessage } from '@agentrix/shared';
5
+ import { randomBytes, randomUUID } from 'node:crypto';
6
6
  import axios from 'axios';
7
- import { m as machine, l as logger, p as projectPath, a as packageJson, c as createLogger, g as getLogPath, b as logger$1 } from './logger-7E71dnBD.mjs';
8
- import * as fs from 'node:fs';
9
- import { existsSync, rmSync, readdirSync, mkdirSync, createWriteStream } from 'node:fs';
7
+ import { m as machine, l as logger, p as projectPath, a as packageJson, c as createLogger, g as getLogPath, b as logger$1 } from './logger-BzpMLIL-.mjs';
10
8
  import { createInterface } from 'node:readline';
11
9
  import fs$1, { readFileSync, existsSync as existsSync$1 } from 'fs';
12
10
  import path$1, { join } from 'path';
@@ -14,22 +12,26 @@ import os, { homedir } from 'node:os';
14
12
  import open from 'open';
15
13
  import { io } from 'socket.io-client';
16
14
  import { EventEmitter } from 'node:events';
15
+ import * as fs from 'node:fs';
16
+ import { existsSync, createWriteStream, readdirSync, mkdirSync } from 'node:fs';
17
17
  import * as path from 'node:path';
18
- import { join as join$1, dirname, extname } from 'node:path';
18
+ import { join as join$1, basename, extname, dirname } from 'node:path';
19
19
  import { spawn, execSync } from 'child_process';
20
20
  import psList from 'ps-list';
21
21
  import spawn$1 from 'cross-spawn';
22
+ import { isSupportedPlatform, checkSandboxDependencies, NetworkManager, SandboxManager } from '@xmz-ai/sandbox-runtime';
23
+ import { getPlatform } from '@xmz-ai/sandbox-runtime/dist/utils/platform.js';
22
24
  import fastify from 'fastify';
23
25
  import { z } from 'zod';
24
26
  import { validatorCompiler, serializerCompiler } from 'fastify-type-provider-zod';
25
27
  import { pathToFileURL } from 'url';
28
+ import { pipeline } from 'node:stream/promises';
26
29
  import { AbortError, query, createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
27
30
  import simpleGit, { CheckRepoActions } from 'simple-git';
28
31
  import { writeFile } from 'node:fs/promises';
29
- import { randomUUID } from 'crypto';
32
+ import { randomUUID as randomUUID$1 } from 'crypto';
30
33
  import { Codex } from '@openai/codex-sdk';
31
34
  import { ZodFirstPartyTypeKind } from 'zod/v3';
32
- import { pipeline } from 'node:stream/promises';
33
35
 
34
36
  async function delay(ms) {
35
37
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -44,22 +46,12 @@ async function daemonPost(path, body) {
44
46
  error: errorMessage
45
47
  };
46
48
  }
47
- try {
48
- process.kill(state.pid, 0);
49
- } catch (error) {
50
- const errorMessage = "Daemon is not running, file is stale";
51
- logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
52
- return {
53
- error: errorMessage
54
- };
55
- }
56
49
  try {
57
50
  const timeout = process.env.AGENTRIX_DAEMON_HTTP_TIMEOUT ? parseInt(process.env.AGENTRIX_DAEMON_HTTP_TIMEOUT) : 1e4;
58
- const response = await fetch(`http://127.0.0.1:${state.port}${path}`, {
51
+ const response = await fetch(`http://agentrix-local.xmz.ai:${state.port}${path}`, {
59
52
  method: "POST",
60
53
  headers: { "Content-Type": "application/json" },
61
54
  body: JSON.stringify(body || {}),
62
- // Mostly increased for stress test
63
55
  signal: AbortSignal.timeout(timeout)
64
56
  });
65
57
  if (!response.ok) {
@@ -205,11 +197,8 @@ async function handleAuthLogout() {
205
197
  console.log(chalk.gray("Stopped daemon"));
206
198
  } catch {
207
199
  }
208
- const paths = machine.getStatePaths();
209
- if (existsSync(paths.rootDir)) {
210
- rmSync(paths.rootDir, { recursive: true, force: true });
211
- console.log(chalk.gray(`Removed agentrix home directory`));
212
- }
200
+ await machine.clearCredentials();
201
+ console.log(chalk.gray(`Removed credentials`));
213
202
  console.log(chalk.green("\u2713 Successfully logged out"));
214
203
  } catch (error) {
215
204
  throw new Error(`Failed to logout: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -11865,7 +11854,7 @@ async function findAllAgentrixProcesses() {
11865
11854
  async function findRunawayAgentrixProcesses() {
11866
11855
  const allProcesses = await findAllAgentrixProcesses();
11867
11856
  return allProcesses.filter(
11868
- (p) => p.pid !== process.pid && (p.type === "daemon" || p.type === "worker")
11857
+ (p) => p.pid !== process.pid && (p.type === "daemon" || p.type === "worker" || p.type === "upgrade-daemon")
11869
11858
  ).map((p) => ({ pid: p.pid, command: p.command }));
11870
11859
  }
11871
11860
  async function killRunawayAgentrixProcesses() {
@@ -11967,6 +11956,27 @@ async function runDoctorCommand(filter) {
11967
11956
  } catch (error) {
11968
11957
  console.log(chalk.red("\u274C Error reading credentials"));
11969
11958
  }
11959
+ console.log(chalk.bold("\n\u{1F512} Sandbox Dependencies"));
11960
+ const platform = getPlatform();
11961
+ if (isSupportedPlatform(platform)) {
11962
+ console.log(`Platform: ${chalk.green(platform)} (supported)`);
11963
+ const depsOk = checkSandboxDependencies();
11964
+ if (depsOk) {
11965
+ console.log(chalk.green("\u2713 All sandbox dependencies available"));
11966
+ } else {
11967
+ console.log(chalk.yellow("\u26A0\uFE0F Some sandbox dependencies missing"));
11968
+ if (platform === "linux") {
11969
+ console.log(chalk.gray(" Required: bubblewrap, socat"));
11970
+ console.log(chalk.gray(" Install: sudo apt install bubblewrap socat"));
11971
+ } else if (platform === "macos") {
11972
+ console.log(chalk.gray(" Required: ripgrep"));
11973
+ console.log(chalk.gray(" Install: brew install ripgrep"));
11974
+ }
11975
+ }
11976
+ } else {
11977
+ console.log(`Platform: ${chalk.yellow(platform)} (not supported)`);
11978
+ console.log(chalk.gray(" \u26A0\uFE0F Sandbox will be disabled"));
11979
+ }
11970
11980
  }
11971
11981
  console.log(chalk.bold("\n\u{1F916} Daemon Status"));
11972
11982
  try {
@@ -12191,9 +12201,11 @@ function createPromiseWithTimeout(options) {
12191
12201
  class TaskWorkerManager {
12192
12202
  pidToTrackedSession;
12193
12203
  pidToAwaiter;
12194
- constructor() {
12204
+ sandboxPool;
12205
+ constructor(sandboxPool) {
12195
12206
  this.pidToTrackedSession = /* @__PURE__ */ new Map();
12196
12207
  this.pidToAwaiter = /* @__PURE__ */ new Map();
12208
+ this.sandboxPool = sandboxPool || null;
12197
12209
  }
12198
12210
  getCurrentSessions() {
12199
12211
  return Array.from(this.pidToTrackedSession.values());
@@ -12234,9 +12246,15 @@ class TaskWorkerManager {
12234
12246
  this.pidToTrackedSession.set(workerProcess.pid, tracked);
12235
12247
  workerProcess.on("exit", (code, signal) => {
12236
12248
  this.pidToTrackedSession.delete(workerProcess.pid);
12249
+ if (this.sandboxPool) {
12250
+ this.sandboxPool.disposeWorkerSandbox(data.taskId);
12251
+ }
12237
12252
  });
12238
12253
  workerProcess.on("error", (error) => {
12239
12254
  this.pidToTrackedSession.delete(workerProcess.pid);
12255
+ if (this.sandboxPool) {
12256
+ this.sandboxPool.disposeWorkerSandbox(data.taskId);
12257
+ }
12240
12258
  });
12241
12259
  }
12242
12260
  async startWorker(options) {
@@ -12261,14 +12279,53 @@ class TaskWorkerManager {
12261
12279
  "--idle-timeout",
12262
12280
  "120"
12263
12281
  ];
12264
- const workerProcess = spawnAgentrixCLI(args, {
12265
- cwd,
12266
- detached: true,
12267
- stdio: ["ignore", "pipe", "pipe"],
12268
- env: {
12269
- ...process.env
12282
+ let workerProcess;
12283
+ if (this.sandboxPool?.isEnabled()) {
12284
+ try {
12285
+ const sandbox = await this.sandboxPool.createWorkerSandbox(
12286
+ options.taskId,
12287
+ options.userId,
12288
+ cwd
12289
+ );
12290
+ if (!sandbox) {
12291
+ throw new Error("Failed to create sandbox instance");
12292
+ }
12293
+ const { projectPath } = await import('./logger-BzpMLIL-.mjs').then(function (n) { return n.d; });
12294
+ const { join } = await import('path');
12295
+ const entrypoint = join(projectPath(), "dist", "index.mjs");
12296
+ const nodeArgs = ["--no-warnings", "--no-deprecation", entrypoint, ...args];
12297
+ const originalCommand = `"${process.execPath}" ${nodeArgs.map((a) => `"${a}"`).join(" ")}`;
12298
+ const sandboxedCommand = await this.sandboxPool.wrapWorkerCommand(
12299
+ options.taskId,
12300
+ originalCommand
12301
+ );
12302
+ logger.debug(`[SESSION] Sandboxed command for task ${options.taskId}: ${sandboxedCommand}`);
12303
+ workerProcess = spawn(sandboxedCommand, {
12304
+ shell: true,
12305
+ cwd,
12306
+ detached: true,
12307
+ stdio: ["ignore", "pipe", "pipe"],
12308
+ env: {
12309
+ ...process.env
12310
+ // Environment variables controlled by SandboxManager
12311
+ }
12312
+ });
12313
+ logger.info(`[SESSION] Worker started with sandbox, PID: ${workerProcess.pid}`);
12314
+ } catch (error) {
12315
+ logger.error(`[SESSION] Failed to setup sandbox for task ${options.taskId}:`, error);
12316
+ ack.status = "failed";
12317
+ ack.message = `Sandbox setup failed: ${error instanceof Error ? error.message : String(error)}`;
12318
+ return ack;
12270
12319
  }
12271
- });
12320
+ } else {
12321
+ workerProcess = spawnAgentrixCLI(args, {
12322
+ cwd,
12323
+ detached: true,
12324
+ stdio: ["ignore", "pipe", "pipe"],
12325
+ env: { ...process.env }
12326
+ });
12327
+ logger.info(`[SESSION] Worker started without sandbox, PID: ${workerProcess.pid}`);
12328
+ }
12272
12329
  if (process.env.DEBUG) {
12273
12330
  workerProcess.stdout?.on("data", (data) => {
12274
12331
  logger.debug(`[Daemon] worker stdout: ${data.toString()}`);
@@ -12387,6 +12444,114 @@ function setupGracefulShutdown(options) {
12387
12444
  };
12388
12445
  }
12389
12446
 
12447
+ class SandboxPool {
12448
+ networkManager = null;
12449
+ workerSandboxes = /* @__PURE__ */ new Map();
12450
+ settings = null;
12451
+ platform;
12452
+ constructor() {
12453
+ this.platform = getPlatform();
12454
+ }
12455
+ async initialize(settings) {
12456
+ this.settings = settings;
12457
+ if (!settings.enabled) {
12458
+ logger.info("[SANDBOX] Sandbox disabled via settings");
12459
+ return false;
12460
+ }
12461
+ if (!isSupportedPlatform(this.platform)) {
12462
+ logger.warn("[SANDBOX] Platform not supported, sandbox disabled");
12463
+ return false;
12464
+ }
12465
+ try {
12466
+ const apiHost = new URL(machine.serverUrl).hostname;
12467
+ const networkConfig = {
12468
+ allowedDomains: [
12469
+ apiHost,
12470
+ ...settings.network.allowedDomains
12471
+ ],
12472
+ deniedDomains: settings.network.deniedDomains,
12473
+ allowLocalBinding: false
12474
+ };
12475
+ this.networkManager = new NetworkManager();
12476
+ await this.networkManager.initialize(networkConfig);
12477
+ logger.info("[SANDBOX] Sandbox pool initialized successfully");
12478
+ return true;
12479
+ } catch (error) {
12480
+ logger.error("[SANDBOX] Failed to initialize:", error);
12481
+ throw error;
12482
+ }
12483
+ }
12484
+ async createWorkerSandbox(taskId, userId, workingDirectory) {
12485
+ if (!this.networkManager || !this.settings?.enabled) {
12486
+ return null;
12487
+ }
12488
+ try {
12489
+ const taskDir = machine.resolveTaskDir(userId, taskId);
12490
+ const logsDir = machine.getStatePaths().logsDir;
12491
+ const baseFilesystem = this.settings.filesystem || {};
12492
+ const baseEnv = this.settings.env || {};
12493
+ const filesystemConfig = {
12494
+ ...baseFilesystem,
12495
+ allowWrite: [
12496
+ ...baseFilesystem.allowWrite || [],
12497
+ taskDir,
12498
+ workingDirectory,
12499
+ logsDir
12500
+ ]
12501
+ };
12502
+ if (this.platform === "linux" && baseFilesystem.allowRead) {
12503
+ filesystemConfig.allowRead = [
12504
+ ...baseFilesystem.allowRead
12505
+ ];
12506
+ }
12507
+ const instanceConfig = {
12508
+ filesystem: filesystemConfig,
12509
+ env: baseEnv
12510
+ };
12511
+ const sandbox = new SandboxManager(this.networkManager, instanceConfig);
12512
+ this.workerSandboxes.set(taskId, sandbox);
12513
+ logger.info(`[SANDBOX] Created sandbox for task ${taskId}`);
12514
+ return sandbox;
12515
+ } catch (error) {
12516
+ logger.error(`[SANDBOX] Failed to create sandbox for task ${taskId}:`, error);
12517
+ return null;
12518
+ }
12519
+ }
12520
+ async wrapWorkerCommand(taskId, command) {
12521
+ const sandbox = this.workerSandboxes.get(taskId);
12522
+ if (!sandbox) {
12523
+ throw new Error(`No sandbox found for task ${taskId}`);
12524
+ }
12525
+ const wrapped = await sandbox.wrapWithSandbox(command);
12526
+ logger.debug(`[SANDBOX] Wrapped command for task ${taskId}`);
12527
+ return wrapped;
12528
+ }
12529
+ disposeWorkerSandbox(taskId) {
12530
+ const sandbox = this.workerSandboxes.get(taskId);
12531
+ if (sandbox) {
12532
+ sandbox.dispose();
12533
+ this.workerSandboxes.delete(taskId);
12534
+ logger.debug(`[SANDBOX] Disposed sandbox for task ${taskId}`);
12535
+ }
12536
+ }
12537
+ async shutdown() {
12538
+ logger.info("[SANDBOX] Shutting down sandbox pool");
12539
+ for (const [taskId, sandbox] of this.workerSandboxes.entries()) {
12540
+ sandbox.dispose();
12541
+ logger.debug(`[SANDBOX] Disposed sandbox for task ${taskId}`);
12542
+ }
12543
+ this.workerSandboxes.clear();
12544
+ if (this.networkManager) {
12545
+ await this.networkManager.shutdown();
12546
+ this.networkManager = null;
12547
+ logger.info("[SANDBOX] Network manager shutdown complete");
12548
+ }
12549
+ }
12550
+ isEnabled() {
12551
+ return this.settings?.enabled === true;
12552
+ }
12553
+ }
12554
+
12390
12555
  async function startDaemon() {
12391
12556
  Object.assign(logger, createLogger({ type: "daemon" }));
12392
12557
  const { requestShutdown, shutdownPromise } = setupGracefulShutdown({
@@ -12415,7 +12580,9 @@ async function startDaemon() {
12415
12580
  }
12416
12581
  const credentials = await authAndSetupMachineIfNeeded();
12417
12582
  logger.debug("[DAEMON RUN] Auth and machine setup complete");
12418
- const sessionManager = new TaskWorkerManager();
12583
+ const sandboxPool = new SandboxPool();
12584
+ await sandboxPool.initialize(machine.getSandboxSettings());
12585
+ const sessionManager = new TaskWorkerManager(sandboxPool);
12419
12586
  const { port: controlPort, stop: stopControlServer } = await startDaemonControlServer({
12420
12587
  getChildren: () => sessionManager.getCurrentSessions(),
12421
12588
  stopSession: (id) => sessionManager.stopSession(id),
@@ -12463,6 +12630,7 @@ async function startDaemon() {
12463
12630
  const cleanupAndShutdown = async (source, errorMessage) => {
12464
12631
  await machineClient.disconnect();
12465
12632
  await stopControlServer();
12633
+ await sandboxPool.shutdown();
12466
12634
  await cleanupDaemonState();
12467
12635
  await stopCaffeinate();
12468
12636
  await machine.releaseDaemonLock(daemonLockHandle);
@@ -12579,14 +12747,6 @@ function createWorkerEventHandlers(context) {
12579
12747
  }
12580
12748
  }
12581
12749
  }
12582
- },
12583
- "require-permission-response": async (data) => {
12584
- if (data.taskId !== context.taskId) {
12585
- return;
12586
- }
12587
- if (context.onPermissionResponse) {
12588
- await context.onPermissionResponse(data);
12589
- }
12590
12750
  }
12591
12751
  };
12592
12752
  }
@@ -12627,7 +12787,6 @@ class WorkerClient {
12627
12787
  cwd: normalizedCwd,
12628
12788
  stopTask: options.stopTask,
12629
12789
  onTaskMessage: options.onTaskMessage,
12630
- onPermissionResponse: options.onPermissionResponse,
12631
12790
  onGitPush: options.onGitPush,
12632
12791
  dataEncryptionKey: config.dataEncryptionKey
12633
12792
  };
@@ -12656,26 +12815,32 @@ class WorkerClient {
12656
12815
  }
12657
12816
  this.client.disconnect();
12658
12817
  }
12659
- sendTaskMessage(message) {
12660
- const cwdWithSlash = this.context.cwd;
12661
- const cwdWithoutSlash = cwdWithSlash.slice(0, -1);
12662
- let content = JSON.stringify(message);
12663
- content = content.replaceAll(cwdWithSlash, "");
12664
- content = content.replaceAll(cwdWithoutSlash, ".");
12665
- let assistantMessage = JSON.parse(content);
12818
+ sendTaskMessage(message, options) {
12819
+ const { replaceCwd = true } = options || {};
12820
+ let processedMessage = message;
12821
+ if (replaceCwd) {
12822
+ const cwdWithSlash = this.context.cwd;
12823
+ const cwdWithoutSlash = cwdWithSlash.slice(0, -1);
12824
+ let content = JSON.stringify(message);
12825
+ content = content.replaceAll(cwdWithSlash, "");
12826
+ content = content.replaceAll(cwdWithoutSlash, ".");
12827
+ processedMessage = JSON.parse(content);
12828
+ }
12666
12829
  let encryptedMessage;
12667
12830
  if (this.context.dataEncryptionKey) {
12668
- encryptedMessage = encryptSdkMessage(assistantMessage, this.context.dataEncryptionKey);
12669
- assistantMessage = void 0;
12831
+ encryptedMessage = encryptSdkMessage(processedMessage, this.context.dataEncryptionKey);
12832
+ processedMessage = void 0;
12670
12833
  }
12834
+ const eventId = createEventId();
12671
12835
  const payload = {
12672
- eventId: createEventId(),
12836
+ eventId,
12673
12837
  taskId: this.context.taskId,
12674
12838
  from: "worker",
12675
- message: assistantMessage,
12839
+ message: processedMessage,
12676
12840
  encryptedMessage
12677
12841
  };
12678
12842
  this.client.send("task-message", payload);
12843
+ return eventId;
12679
12844
  }
12680
12845
  sendWorkerInitializing() {
12681
12846
  const workerInitializingEvent = {
@@ -12686,12 +12851,13 @@ class WorkerClient {
12686
12851
  };
12687
12852
  this.client.send("worker-initializing", workerInitializingEvent);
12688
12853
  }
12689
- sendWorkerReady() {
12854
+ sendWorkerReady(duration) {
12690
12855
  const workerReadyEvent = {
12691
12856
  eventId: createEventId(),
12692
12857
  taskId: this.context.taskId,
12693
12858
  machineId: this.context.machineId,
12694
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
12859
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12860
+ ...duration !== void 0 && { duration }
12695
12861
  };
12696
12862
  this.client.send("worker-ready", workerReadyEvent);
12697
12863
  }
@@ -12749,17 +12915,13 @@ ${errorMessage}`,
12749
12915
  };
12750
12916
  this.sendTaskMessage(systemMessage);
12751
12917
  }
12752
- sendRequirePermission(toolName, toolInput) {
12753
- const eventId = createEventId();
12754
- const permissionRequest = {
12755
- eventId,
12756
- taskId: this.context.taskId,
12757
- toolName,
12758
- toolInput
12759
- };
12760
- this.client.send("require-permission", permissionRequest);
12761
- logger.info(`[AGENT] Permission requested for tool: ${toolName}`);
12762
- return eventId;
12918
+ /**
12919
+ * Send ask-user message to request user input
12920
+ * @param questions - Array of questions (1-4)
12921
+ * @returns eventId for tracking the request
12922
+ */
12923
+ sendAskUser(questions) {
12924
+ return this.sendTaskMessage({ type: "ask_user", questions }, { replaceCwd: false });
12763
12925
  }
12764
12926
  sendUpdateTaskAgentSessionId(agentSessionId) {
12765
12927
  const updateSessionEvent = {
@@ -12814,7 +12976,108 @@ ${errorMessage}`,
12814
12976
  this.client.onEvent("cancel-task", handlers["cancel-task"]);
12815
12977
  this.client.onEvent("stop-task", handlers["stop-task"]);
12816
12978
  this.client.onEvent("task-message", handlers["task-message"]);
12817
- this.client.onEvent("require-permission-response", handlers["require-permission-response"]);
12979
+ }
12980
+ }
12981
+
12982
+ const MIME_TYPE_MAP = {
12983
+ // Images
12984
+ ".jpg": "image/jpeg",
12985
+ ".jpeg": "image/jpeg",
12986
+ ".png": "image/png",
12987
+ ".gif": "image/gif",
12988
+ ".webp": "image/webp",
12989
+ ".bmp": "image/bmp",
12990
+ ".svg": "image/svg+xml",
12991
+ ".ico": "image/x-icon",
12992
+ // Documents
12993
+ ".pdf": "application/pdf",
12994
+ ".doc": "application/msword",
12995
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
12996
+ ".xls": "application/vnd.ms-excel",
12997
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
12998
+ ".ppt": "application/vnd.ms-powerpoint",
12999
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
13000
+ // Text
13001
+ ".txt": "text/plain",
13002
+ ".md": "text/markdown",
13003
+ ".csv": "text/csv",
13004
+ ".json": "application/json",
13005
+ ".xml": "application/xml",
13006
+ ".html": "text/html",
13007
+ ".css": "text/css",
13008
+ ".js": "application/javascript",
13009
+ ".ts": "application/typescript",
13010
+ // Archives
13011
+ ".zip": "application/zip",
13012
+ ".tar": "application/x-tar",
13013
+ ".gz": "application/gzip",
13014
+ ".rar": "application/vnd.rar",
13015
+ // Other
13016
+ ".mp3": "audio/mpeg",
13017
+ ".mp4": "video/mp4",
13018
+ ".wav": "audio/wav",
13019
+ ".avi": "video/x-msvideo"
13020
+ };
13021
+ function detectMimeType(extension) {
13022
+ const normalized = extension.toLowerCase();
13023
+ return MIME_TYPE_MAP[normalized] || "application/octet-stream";
13024
+ }
13025
+ function extractExtension(url) {
13026
+ try {
13027
+ const urlObj = new URL(url);
13028
+ const pathExt = extname(urlObj.pathname);
13029
+ if (pathExt) {
13030
+ return pathExt;
13031
+ }
13032
+ const filenameParam = urlObj.searchParams.get("filename") || urlObj.searchParams.get("name") || urlObj.searchParams.get("file");
13033
+ if (filenameParam) {
13034
+ const paramExt = extname(filenameParam);
13035
+ if (paramExt) {
13036
+ return paramExt;
13037
+ }
13038
+ }
13039
+ return "";
13040
+ } catch {
13041
+ return "";
13042
+ }
13043
+ }
13044
+ async function downloadFile(url, targetDir, preserveFilename = false) {
13045
+ try {
13046
+ const extension = extractExtension(url) || "";
13047
+ let filename;
13048
+ if (preserveFilename) {
13049
+ try {
13050
+ const urlPath = new URL(url).pathname;
13051
+ const originalFilename = basename(urlPath);
13052
+ filename = originalFilename && extname(originalFilename) ? originalFilename : `${randomUUID()}${extension || ".dat"}`;
13053
+ } catch {
13054
+ filename = `${randomUUID()}${extension || ".dat"}`;
13055
+ }
13056
+ } else {
13057
+ filename = `${randomUUID()}${extension || ".dat"}`;
13058
+ }
13059
+ const filePath = join$1(targetDir, filename);
13060
+ const response = await fetch(url);
13061
+ if (!response.ok) {
13062
+ throw new Error(`Failed to download file: ${response.status} ${response.statusText}`);
13063
+ }
13064
+ if (!response.body) {
13065
+ throw new Error("Response body is null");
13066
+ }
13067
+ const contentType = response.headers.get("content-type");
13068
+ const mimeType = contentType?.split(";")[0].trim() || detectMimeType(extension);
13069
+ const nodeStream = response.body;
13070
+ const fileStream = createWriteStream(filePath);
13071
+ await pipeline(nodeStream, fileStream);
13072
+ return {
13073
+ filePath,
13074
+ mimeType,
13075
+ filename
13076
+ };
13077
+ } catch (error) {
13078
+ throw new Error(
13079
+ `Failed to download file from ${url}: ${error instanceof Error ? error.message : String(error)}`
13080
+ );
12818
13081
  }
12819
13082
  }
12820
13083
 
@@ -12915,6 +13178,12 @@ async function hasUncommittedChanges(dir) {
12915
13178
  const status = await git.status();
12916
13179
  return !status.isClean();
12917
13180
  }
13181
+ async function gitStash(dir, message) {
13182
+ const git = simpleGit(dir);
13183
+ {
13184
+ await git.stash(["push"]);
13185
+ }
13186
+ }
12918
13187
  async function getCurrentCommitHash(dir) {
12919
13188
  const git = simpleGit(dir);
12920
13189
  const log = await git.log({ maxCount: 1 });
@@ -13004,6 +13273,30 @@ async function executeHook(hooks, hookName, input, logger) {
13004
13273
  }
13005
13274
  }
13006
13275
 
13276
+ async function checkUncommittedChanges(workingDirectory) {
13277
+ const isRepo = await isGitRepository(workingDirectory);
13278
+ if (!isRepo) {
13279
+ return false;
13280
+ }
13281
+ return await hasUncommittedChanges(workingDirectory);
13282
+ }
13283
+ async function handleUncommittedChanges(workingDirectory, action) {
13284
+ switch (action) {
13285
+ case "Ignore":
13286
+ console.log("[GIT] User chose to ignore uncommitted changes");
13287
+ break;
13288
+ case "Commit":
13289
+ console.log("[GIT] Auto-committing uncommitted changes");
13290
+ await autoCommit(workingDirectory, "WIP: Auto-commit before task");
13291
+ break;
13292
+ case "Stash":
13293
+ console.log("[GIT] Stashing uncommitted changes");
13294
+ await gitStash(workingDirectory);
13295
+ break;
13296
+ case "Abort":
13297
+ throw new Error("Task aborted by user due to uncommitted changes");
13298
+ }
13299
+ }
13007
13300
  function createTaskBranchName(taskId) {
13008
13301
  return `agentrix/${taskId}`;
13009
13302
  }
@@ -13043,17 +13336,6 @@ async function setupLocalWorkspace(workingDirectory, taskId, hooks) {
13043
13336
  const isRepo = await isGitRepository(workingDirectory);
13044
13337
  const isEmpty = isDirectoryEmpty(workingDirectory);
13045
13338
  if (isRepo) {
13046
- const hasChanges = await hasUncommittedChanges(workingDirectory);
13047
- if (hasChanges) {
13048
- throw new Error(
13049
- `Directory ${workingDirectory} has uncommitted changes.
13050
-
13051
- Please commit or stash your changes before starting:
13052
- git add . && git commit -m "WIP"
13053
- or:
13054
- git stash`
13055
- );
13056
- }
13057
13339
  const hasCommits = await hasAnyCommits(workingDirectory);
13058
13340
  if (!hasCommits) {
13059
13341
  console.log("[GIT] Repository has no commits, creating initial commit");
@@ -13181,9 +13463,32 @@ async function markCommitAsSent(userId, taskId, commitHash) {
13181
13463
  await machine.writeLastSentCommitHash(userId, taskId, commitHash);
13182
13464
  }
13183
13465
 
13466
+ function getDefaultPRPrompt(params) {
13467
+ return `All changes have been pushed to branch "${params.branchName}".
13468
+
13469
+ Commit range: ${params.initialCommitHash}..${params.currentCommitHash}
13470
+
13471
+ Based on our conversation context, create a Pull Request:
13472
+ - Title: conventional commits format (feat/fix/docs/refactor/test/chore: description)
13473
+ - Description: what changed, why, and any important decisions
13474
+
13475
+ Use mcp__agentrix__create_pr tool to create the PR.`;
13476
+ }
13477
+ function applyTemplateVariables(template, params) {
13478
+ return template.replace(/\{\{initialCommitHash\}\}/g, params.initialCommitHash).replace(/\{\{currentCommitHash\}\}/g, params.currentCommitHash).replace(/\{\{branchName\}\}/g, params.branchName);
13479
+ }
13480
+ function buildPRPrompt(params, config) {
13481
+ const defaultPrompt = getDefaultPRPrompt(params);
13482
+ if (!config?.customTemplate) {
13483
+ return defaultPrompt;
13484
+ }
13485
+ const customPrompt = applyTemplateVariables(config.customTemplate, params);
13486
+ return config.mode === "replace" ? customPrompt : defaultPrompt + "\n\n" + customPrompt;
13487
+ }
13488
+
13184
13489
  function executeCommandStreaming(command, cwd, callbacks, timeoutMs = 6e4) {
13185
13490
  return new Promise((resolve) => {
13186
- const toolUseId = `shell_${randomUUID().replace(/-/g, "")}`;
13491
+ const toolUseId = `shell_${randomUUID$1().replace(/-/g, "")}`;
13187
13492
  callbacks.onOutput({
13188
13493
  type: "assistant",
13189
13494
  message: {
@@ -13298,6 +13603,7 @@ class MessageCoordinator {
13298
13603
  currentMessageId = null;
13299
13604
  messageIdCounter = 0;
13300
13605
  isStopped = false;
13606
+ runStartTime = null;
13301
13607
  constructor(config) {
13302
13608
  this.config = config;
13303
13609
  }
@@ -13417,11 +13723,11 @@ class MessageCoordinator {
13417
13723
  async processBashCommand(envelope) {
13418
13724
  this.log("info", "COORDINATOR", `Processing bash command: ${envelope.content}`);
13419
13725
  await this.waitForState("idle");
13420
- this.workerState = "executing-command";
13726
+ this.setWorkerState("running");
13421
13727
  try {
13422
13728
  await this.config.handlers.onBashCommand(envelope.content, envelope.originalMessage);
13423
13729
  } finally {
13424
- this.workerState = "idle";
13730
+ this.setWorkerState("idle");
13425
13731
  }
13426
13732
  }
13427
13733
  /**
@@ -13430,11 +13736,11 @@ class MessageCoordinator {
13430
13736
  async processMergeRequest(envelope) {
13431
13737
  this.log("info", "COORDINATOR", "Processing merge-request command");
13432
13738
  await this.waitForState("idle");
13433
- this.workerState = "executing-command";
13739
+ this.setWorkerState("running");
13434
13740
  try {
13435
13741
  await this.config.handlers.onMergeRequest(envelope.originalMessage);
13436
13742
  } finally {
13437
- this.workerState = "idle";
13743
+ this.setWorkerState("idle");
13438
13744
  }
13439
13745
  }
13440
13746
  async waitForState(targetState) {
@@ -13454,11 +13760,24 @@ class MessageCoordinator {
13454
13760
  }
13455
13761
  /**
13456
13762
  * Set the worker state (called by worker when state changes)
13763
+ * Automatically sends WebSocket events and tracks execution duration
13457
13764
  */
13458
13765
  setWorkerState(state) {
13459
- if (this.workerState !== state) {
13460
- this.log("info", "COORDINATOR", `Worker state: ${this.workerState} \u2192 ${state}`);
13461
- this.workerState = state;
13766
+ if (this.workerState === state) return;
13767
+ const prevState = this.workerState;
13768
+ this.log("info", "COORDINATOR", `Worker state: ${prevState} \u2192 ${state}`);
13769
+ this.workerState = state;
13770
+ if (state === "running" && prevState === "idle") {
13771
+ this.runStartTime = Date.now();
13772
+ this.config.workClient.sendWorkRunning();
13773
+ }
13774
+ if (state === "idle" && prevState === "running") {
13775
+ let duration;
13776
+ if (this.runStartTime) {
13777
+ duration = Date.now() - this.runStartTime;
13778
+ this.runStartTime = null;
13779
+ }
13780
+ this.config.workClient.sendWorkerReady(duration);
13462
13781
  }
13463
13782
  }
13464
13783
  /**
@@ -13498,7 +13817,7 @@ class ClaudeWorker {
13498
13817
  messageQueue = [];
13499
13818
  messageResolverRef = { current: null };
13500
13819
  abortController = new AbortController();
13501
- permissionAwaiter = /* @__PURE__ */ new Map();
13820
+ askUserAwaiter = /* @__PURE__ */ new Map();
13502
13821
  filteredToolUseIds = /* @__PURE__ */ new Set();
13503
13822
  timerManager;
13504
13823
  context;
@@ -13508,6 +13827,11 @@ class ClaudeWorker {
13508
13827
  dataEncryptionKey = null;
13509
13828
  coordinator;
13510
13829
  loadedHooks;
13830
+ loadedAgentConfig;
13831
+ // Pending permission requests: toolName -> Promise<'allow' | 'deny'> (to dedupe concurrent requests)
13832
+ pendingPermissions = /* @__PURE__ */ new Map();
13833
+ // Granted permissions cache: toolName -> true (to avoid repeated asks for same tool)
13834
+ grantedPermissions = /* @__PURE__ */ new Set();
13511
13835
  async start() {
13512
13836
  try {
13513
13837
  await this.initialize();
@@ -13528,18 +13852,69 @@ class ClaudeWorker {
13528
13852
  if (this.timerManager) {
13529
13853
  this.timerManager.clearIdleTimer();
13530
13854
  }
13855
+ if (this.logger) {
13856
+ await new Promise((resolve) => {
13857
+ this.logger.on("finish", resolve);
13858
+ this.logger.end();
13859
+ });
13860
+ }
13531
13861
  process.exit(0);
13532
13862
  }
13533
13863
  }
13534
13864
  async initialize() {
13535
13865
  const taskId = this.options.input.taskId;
13536
13866
  const userId = this.options.input.userId;
13537
- let workingDirectory = process.cwd();
13538
- let initialCommitHash;
13539
- this.logger = await this.createLogger({ type: "worker", taskId });
13867
+ this.logger = this.createWorkerLogger({ type: "worker", taskId });
13540
13868
  if (this.options.input.dataEncryptionKey && this.options.secretKey) {
13541
13869
  this.dataEncryptionKey = decryptWithEphemeralKey(decodeBase64(this.options.input.dataEncryptionKey), this.options.secretKey);
13542
13870
  }
13871
+ if (this.options.input.encryptedMessage && this.dataEncryptionKey) {
13872
+ this.options.input.message = decryptSdkMessage(this.options.input.encryptedMessage, this.dataEncryptionKey) || void 0;
13873
+ }
13874
+ let workingDirectory = this.options.input.cwd ? this.options.input.cwd.replace(/^~/, homedir()) : process.cwd();
13875
+ const idleTimeoutMs = Math.max(0, this.options.idleTimeoutSecond ?? 0) * 1e3;
13876
+ this.timerManager = this.createIdleTimerManager(idleTimeoutMs, taskId);
13877
+ const workerConfig = this.createWorkerClientConfig(userId, taskId, workingDirectory);
13878
+ const workClient = new WorkerClient(workerConfig.config, workerConfig.handlers);
13879
+ await workClient.connect();
13880
+ workClient.sendWorkerInitializing();
13881
+ this.context = {
13882
+ credentials: this.credentials,
13883
+ options: this.options,
13884
+ workClient,
13885
+ workingDirectory,
13886
+ initialCommitHash: "",
13887
+ // Will be set after setupWorkspace
13888
+ logger: this.logger
13889
+ };
13890
+ this.coordinator = new MessageCoordinator({
13891
+ workerType: "claude",
13892
+ workClient,
13893
+ handlers: {
13894
+ onNormalMessage: async (message) => {
13895
+ await this.enqueueMessage(message);
13896
+ },
13897
+ onBashCommand: async (command, _originalMessage) => {
13898
+ await this.executeBashCommand(command);
13899
+ },
13900
+ onMergeRequest: async (_originalMessage) => {
13901
+ await this.executeMergeRequest();
13902
+ }
13903
+ },
13904
+ logger: (level, category, message) => {
13905
+ const validLevel = level;
13906
+ this.log(validLevel, category, message);
13907
+ }
13908
+ });
13909
+ if (!this.options.input.gitUrl) {
13910
+ const hasChanges = await checkUncommittedChanges(workingDirectory);
13911
+ if (hasChanges) {
13912
+ this.log("info", "GIT", "Detected uncommitted changes, asking user for action");
13913
+ const action = await this.askUncommittedChangesAction();
13914
+ await handleUncommittedChanges(workingDirectory, action);
13915
+ }
13916
+ }
13917
+ let initialCommitHash;
13543
13918
  try {
13544
13919
  const hooks = await this.loadAgentHooks();
13545
13920
  const workspaceResult = await setupWorkspace({
@@ -13551,6 +13926,8 @@ class ClaudeWorker {
13551
13926
  }, hooks);
13552
13927
  workingDirectory = workspaceResult.workingDirectory;
13553
13928
  initialCommitHash = workspaceResult.initialCommitHash;
13929
+ this.context.workingDirectory = workingDirectory;
13930
+ this.context.initialCommitHash = initialCommitHash;
13554
13931
  await machine.writeInitialCommitHash(userId, taskId, initialCommitHash);
13555
13932
  this.log("info", "GIT", `Initial commit: ${initialCommitHash}`);
13556
13933
  this.initialCommitHashForPR = initialCommitHash;
@@ -13558,20 +13935,10 @@ class ClaudeWorker {
13558
13935
  this.logGitStateResult(gitStateResult, "start");
13559
13936
  } catch (error) {
13560
13937
  this.log("error", "GIT", "Failed to setup workspace:", error);
13561
- const basicConfig = this.createBasicWorkerConfig(userId, taskId, workingDirectory);
13562
13938
  const errorMessage = error instanceof Error ? error.message : String(error);
13563
- await WorkerClient.sendErrorAndExit(
13564
- basicConfig,
13565
- `Failed to setup workspace: ${errorMessage}`
13566
- );
13939
+ await workClient.sendErrorMessageAndExit(`Failed to setup workspace: ${errorMessage}`);
13567
13940
  process.exit(1);
13568
13941
  }
13569
- const idleTimeoutMs = Math.max(0, this.options.idleTimeoutSecond ?? 0) * 1e3;
13570
- this.timerManager = this.createIdleTimerManager(idleTimeoutMs, taskId);
13571
- const workerConfig = this.createWorkerClientConfig(userId, taskId, workingDirectory);
13572
- const workClient = new WorkerClient(workerConfig.config, workerConfig.handlers);
13573
- await workClient.connect();
13574
- workClient.sendWorkerInitializing();
13575
13942
  try {
13576
13943
  const metadata = {
13577
13944
  cwd: workingDirectory,
@@ -13588,35 +13955,6 @@ class ClaudeWorker {
13588
13955
  } catch (error) {
13589
13956
  this.log("warn", "DAEMON", "Failed to report session:", error);
13590
13957
  }
13591
- if (this.options.input.encryptedMessage && this.dataEncryptionKey) {
13592
- this.options.input.message = decryptSdkMessage(this.options.input.encryptedMessage, this.dataEncryptionKey) || void 0;
13593
- }
13594
- this.context = {
13595
- credentials: this.credentials,
13596
- options: this.options,
13597
- workClient,
13598
- workingDirectory,
13599
- initialCommitHash,
13600
- logger: this.logger
13601
- };
13602
- this.coordinator = new MessageCoordinator({
13603
- workerType: "claude",
13604
- handlers: {
13605
- onNormalMessage: async (message) => {
13606
- await this.enqueueMessage(message);
13607
- },
13608
- onBashCommand: async (command, _originalMessage) => {
13609
- await this.executeBashCommand(command);
13610
- },
13611
- onMergeRequest: async (_originalMessage) => {
13612
- await this.executeMergeRequest();
13613
- }
13614
- },
13615
- logger: (level, category, message) => {
13616
- const validLevel = level;
13617
- this.log(validLevel, category, message);
13618
- }
13619
- });
13620
13958
  }
13621
13959
  async handleEvent() {
13622
13960
  if (this.options.input.message) {
@@ -13634,15 +13972,30 @@ class ClaudeWorker {
13634
13972
  }
13635
13973
  const hasChanges = await hasUncommittedChanges(this.context.workingDirectory);
13636
13974
  if (hasChanges) {
13637
- await autoCommit(this.context.workingDirectory, "Update task changes");
13975
+ await autoCommit(this.context.workingDirectory, "Checkpoint for PR generation");
13638
13976
  this.log("info", "MERGE", "Auto-committed changes");
13639
13977
  }
13978
+ const currentHash = await getCurrentCommitHash(this.context.workingDirectory);
13979
+ const diffStats = await getDiffStats(this.context.workingDirectory, this.initialCommitHashForPR, currentHash);
13980
+ if (diffStats.files.length === 0) {
13981
+ const errorMessage = "No changes to create PR: no files changed since task started";
13982
+ this.log("error", "MERGE", errorMessage);
13983
+ this.context.workClient.sendSystemErrorMessage(errorMessage);
13984
+ return;
13985
+ }
13986
+ this.log("info", "MERGE", `Found ${diffStats.files.length} files changed`);
13640
13987
  const branchName = await getCurrentBranch(this.context.workingDirectory);
13641
13988
  this.log("info", "MERGE", `Pushing branch ${branchName} to remote`);
13642
13989
  await gitPush(this.context.workingDirectory, branchName, false);
13643
13990
  this.log("info", "MERGE", "Successfully pushed branch to remote");
13991
+ const prPrompt = buildPRPrompt(
13992
+ { initialCommitHash: this.initialCommitHashForPR, currentCommitHash: currentHash, branchName },
13993
+ this.loadedAgentConfig?.customPRPromptTemplate ? {
13994
+ customTemplate: this.loadedAgentConfig.customPRPromptTemplate,
13995
+ mode: this.loadedAgentConfig.prPromptMode
13996
+ } : void 0
13997
+ );
13644
13998
  this.inMergeRequest = true;
13645
- const prPrompt = await this.buildCreatePRPrompt();
13646
13999
  await this.enqueueMessage({
13647
14000
  type: "user",
13648
14001
  message: {
@@ -13671,53 +14024,8 @@ class ClaudeWorker {
13671
14024
  }
13672
14025
  );
13673
14026
  this.timerManager.startIdleTimer();
13674
- this.context.workClient.sendWorkerReady();
13675
14027
  this.log("info", "BASH", `Worker ready after command execution (exit code: ${exitCode})`);
13676
14028
  }
13677
- async buildCreatePRPrompt() {
13678
- if (!this.initialCommitHashForPR) {
13679
- return 'Forbidden create PR by yourself. Must use the mcp__agentrix__create_pr tool. All the changed has been pushed. Please analyze the changes and use the mcp__agentrix__create_pr tool to create a pull request with a title and description. Use conventional commits format for the title (e.g., "feat: add new feature").';
13680
- }
13681
- try {
13682
- const currentHash = await autoCommit(this.context.workingDirectory, "Checkpoint for PR generation");
13683
- const stats = await getDiffStats(
13684
- this.context.workingDirectory,
13685
- this.initialCommitHashForPR,
13686
- currentHash
13687
- );
13688
- const diff = await generateDiffPatch(
13689
- this.context.workingDirectory,
13690
- this.initialCommitHashForPR,
13691
- currentHash
13692
- );
13693
- const statsText = `Files changed: ${stats.files.length}, +${stats.totalInsertions}/-${stats.totalDeletions}
13694
-
13695
- Detailed changes:
13696
- ${stats.files.map((f) => ` ${f.path}: +${f.insertions}/-${f.deletions}`).join("\n")}`;
13697
- return `All the changed has been successfully pushed to the git branch. Please create a Pull Request by analyzing the changes below.
13698
-
13699
- Changes made:
13700
- ${statsText}
13701
-
13702
- Diff (first 5000 chars):
13703
- \`\`\`
13704
- ${diff.substring(0, 5e3)}
13705
- \`\`\`
13706
-
13707
- Requirements:
13708
- - Title: Use conventional commits format (feat/fix/docs/refactor/test/chore: description), maximum 50 characters
13709
- - Description: Provide a clear, detailed explanation of:
13710
- * What changed (the actual modifications made)
13711
- * Why these changes were necessary (the problem being solved)
13712
- * Any important technical decisions or trade-offs
13713
- * Impact on existing functionality
13714
-
13715
- Please must use the mcp__agentrix__create_pr tool to create the pull request with the generated title and description.Forbidden create PR by yourself.`;
13716
- } catch (error) {
13717
- this.log("warn", "GIT", "Failed to generate diff for PR prompt:", error);
13718
- return 'The code has been committed. Please use the mcp__agentrix__create_pr tool to create a pull request with a title and description. Use conventional commits format for the title (e.g., "feat: add new feature").';
13719
- }
13720
- }
13721
14029
  async runClaude() {
13722
14030
  this.log("info", "AGENT", `Starting Claude agent for task ${this.taskId}`);
13723
14031
  const agentSessionId = "agentSessionId" in this.options.input ? this.options.input.agentSessionId : void 0;
@@ -13728,12 +14036,12 @@ Please must use the mcp__agentrix__create_pr tool to create the pull request wit
13728
14036
  );
13729
14037
  const sdkMcpServer = this.createAgentrixMcpServer();
13730
14038
  const mcpServers = {
13731
- agentrix: sdkMcpServer,
13732
- ...agentConfig.customMcpServers
14039
+ agentrix: sdkMcpServer
13733
14040
  };
13734
14041
  const allowedTools = [
13735
14042
  "mcp__agentrix__change_task_title",
13736
14043
  "mcp__agentrix__create_pr",
14044
+ "mcp__agentrix__ask_user",
13737
14045
  ...agentConfig.customAllowedTools
13738
14046
  ];
13739
14047
  const messageConsumer = this.createMessageConsumer();
@@ -13753,6 +14061,7 @@ Please must use the mcp__agentrix__create_pr tool to create the pull request wit
13753
14061
  systemPrompt: finalSystemPrompt,
13754
14062
  mcpServers,
13755
14063
  allowedTools,
14064
+ plugins: agentConfig.customPlugins,
13756
14065
  abortController: this.abortController,
13757
14066
  env: this.buildEnvironmentOverrides(),
13758
14067
  maxTurns: agentConfig.customMaxTurns ?? this.options.input.maxTurns ?? 50,
@@ -13762,18 +14071,16 @@ Please must use the mcp__agentrix__create_pr tool to create the pull request wit
13762
14071
  }
13763
14072
  });
13764
14073
  if (this.messageQueue.length > 0) {
13765
- this.context.workClient.sendWorkRunning();
13766
- this.coordinator.setWorkerState("processing-sdk");
14074
+ this.coordinator.setWorkerState("running");
13767
14075
  } else {
13768
14076
  this.timerManager.startIdleTimer();
13769
- this.context.workClient.sendWorkerReady();
13770
14077
  }
13771
14078
  for await (const message of response) {
13772
14079
  this.timerManager.clearIdleTimer();
13773
14080
  this.context.logger.debug(`sdk message: ${JSON.stringify(message)}`);
13774
14081
  if (message.type === "system" && message.subtype === "init") {
13775
14082
  this.context.workClient.sendUpdateTaskAgentSessionId(message.session_id);
13776
- this.context.workClient.sendWorkRunning();
14083
+ this.coordinator.setWorkerState("running");
13777
14084
  continue;
13778
14085
  }
13779
14086
  const filteredMessage = this.filterMessages(message);
@@ -13783,15 +14090,14 @@ Please must use the mcp__agentrix__create_pr tool to create the pull request wit
13783
14090
  if (message.type === "result") {
13784
14091
  this.coordinator.setWorkerState("idle");
13785
14092
  this.timerManager.startIdleTimer();
13786
- this.context.workClient.sendWorkerReady();
13787
14093
  } else {
13788
- this.coordinator.setWorkerState("processing-sdk");
14094
+ this.coordinator.setWorkerState("running");
13789
14095
  }
13790
14096
  }
13791
14097
  this.log("info", "AGENT", `Claude agent finished for task ${this.taskId}`);
13792
14098
  }
13793
14099
  async enqueueMessage(message) {
13794
- const processedMessage = await this.processImageUrls(message);
14100
+ const processedMessage = await this.processAttachments(message);
13795
14101
  if (this.messageResolverRef.current) {
13796
14102
  const resolver = this.messageResolverRef.current;
13797
14103
  this.messageResolverRef.current = null;
@@ -13800,10 +14106,14 @@ Please must use the mcp__agentrix__create_pr tool to create the pull request wit
13800
14106
  this.messageQueue.push(processedMessage);
13801
14107
  }
13802
14108
  }
13803
- async processImageUrls(message) {
14109
+ async processAttachments(message) {
13804
14110
  if (!Array.isArray(message.message.content)) {
13805
14111
  return message;
13806
14112
  }
14113
+ const attachmentsDir = machine.resolveAttachmentsDir(
14114
+ this.options.input.userId,
14115
+ this.taskId
14116
+ );
13807
14117
  const processedContent = await Promise.all(
13808
14118
  message.message.content.map(async (block) => {
13809
14119
  if (block.type === "image" && block.source?.type === "url" && block.source?.url) {
@@ -13839,6 +14149,24 @@ Please must use the mcp__agentrix__create_pr tool to create the pull request wit
13839
14149
  return block;
13840
14150
  }
13841
14151
  }
14152
+ if (block.type === "document" && block.source?.type === "url" && block.source?.url) {
14153
+ try {
14154
+ const url = block.source.url;
14155
+ this.log("info", "DOCUMENT", `Downloading document from: ${url}`);
14156
+ const { filePath, mimeType, filename } = await downloadFile(url, attachmentsDir, true);
14157
+ this.log("info", "DOCUMENT", `Document downloaded to: ${filePath}`);
14158
+ const title = block.title || filename;
14159
+ return {
14160
+ type: "text",
14161
+ text: `Document: ${filePath}
14162
+ Title: ${title}
14163
+ Type: ${mimeType}`
14164
+ };
14165
+ } catch (error) {
14166
+ this.log("error", "DOCUMENT", `Error processing document: ${error}`);
14167
+ return block;
14168
+ }
14169
+ }
13842
14170
  return block;
13843
14171
  })
13844
14172
  );
@@ -13923,33 +14251,117 @@ Please must use the mcp__agentrix__create_pr tool to create the pull request wit
13923
14251
  }
13924
14252
  createPermissionHandler() {
13925
14253
  return async (toolName, input) => {
13926
- const eventId = this.context.workClient.sendRequirePermission(toolName, input);
13927
- const timeoutMs = 3e4;
13928
- const permissionResponse = await new Promise(
13929
- (resolve, reject) => {
13930
- const timeout = setTimeout(() => {
13931
- this.permissionAwaiter.delete(eventId);
13932
- reject(new Error(`Permission request timeout for tool ${toolName}`));
13933
- }, timeoutMs);
13934
- this.permissionAwaiter.set(eventId, (response) => {
13935
- clearTimeout(timeout);
13936
- resolve(response);
13937
- });
14254
+ if (this.grantedPermissions.has(toolName)) {
14255
+ this.log("info", "PERMISSION", `Tool "${toolName}" already granted, skipping`);
14256
+ return { behavior: "allow", updatedInput: input };
14257
+ }
14258
+ const pending = this.pendingPermissions.get(toolName);
14259
+ if (pending) {
14260
+ this.log("info", "PERMISSION", `Tool "${toolName}" has pending request, waiting...`);
14261
+ const decision = await pending;
14262
+ if (decision === "allow") {
14263
+ return { behavior: "allow", updatedInput: input };
14264
+ } else {
14265
+ return { behavior: "deny", message: "Permission denied by user" };
13938
14266
  }
13939
- );
13940
- if (permissionResponse.behavior === "allow") {
13941
- return {
13942
- behavior: "allow",
13943
- updatedInput: input
13944
- };
13945
- } else {
13946
- return {
13947
- behavior: "deny",
13948
- message: permissionResponse.message || "Permission denied"
13949
- };
14267
+ }
14268
+ this.log("info", "PERMISSION", `Requesting permission for "${toolName}"`);
14269
+ let resolveDecision;
14270
+ const permissionPromise = new Promise((resolve) => {
14271
+ resolveDecision = resolve;
14272
+ });
14273
+ this.pendingPermissions.set(toolName, permissionPromise);
14274
+ try {
14275
+ const decision = await this.requestToolPermission(toolName);
14276
+ resolveDecision(decision);
14277
+ if (decision === "allow") {
14278
+ this.grantedPermissions.add(toolName);
14279
+ return { behavior: "allow", updatedInput: input };
14280
+ } else {
14281
+ return { behavior: "deny", message: "Permission denied by user" };
14282
+ }
14283
+ } catch (error) {
14284
+ resolveDecision("deny");
14285
+ return { behavior: "deny", message: "Permission request failed" };
14286
+ } finally {
14287
+ this.pendingPermissions.delete(toolName);
13950
14288
  }
13951
14289
  };
13952
14290
  }
14291
+ async requestToolPermission(toolName) {
14292
+ const questions = [{
14293
+ question: `Tool "${toolName}" is requesting permission to execute. Allow this operation?`,
14294
+ header: "Permission",
14295
+ multiSelect: false,
14296
+ options: [
14297
+ { label: "Allow", description: "Allow this tool to execute" },
14298
+ { label: "Deny", description: "Deny this tool execution" }
14299
+ ]
14300
+ }];
14301
+ try {
14302
+ const response = await this.askUser(questions);
14303
+ const answer = response.answers[0];
14304
+ return answer === "Allow" ? "allow" : "deny";
14305
+ } catch (error) {
14306
+ this.log("warn", "PERMISSION", `Permission request failed: ${error}`);
14307
+ return "deny";
14308
+ }
14309
+ }
14310
+ /**
14311
+ * Ask user questions and wait for response
14312
+ * Sends ask_user message via task-message and waits for ask_user_response
14313
+ * @param questions - Array of questions (1-4)
14314
+ * @returns Promise resolving to user's response
14315
+ */
14316
+ async askUser(questions) {
14317
+ const eventId = this.context.workClient.sendAskUser(questions);
14318
+ const timeoutMs = 3e5;
14319
+ return new Promise((resolve, reject) => {
14320
+ const timeout = setTimeout(() => {
14321
+ this.askUserAwaiter.delete(eventId);
14322
+ reject(new Error("Ask user request timed out"));
14323
+ }, timeoutMs);
14324
+ this.askUserAwaiter.set(eventId, (response) => {
14325
+ clearTimeout(timeout);
14326
+ resolve(response);
14327
+ });
14328
+ });
14329
+ }
14330
+ /**
14331
+ * Ask user how to handle uncommitted changes
14332
+ * @returns The action to take: ignore, commit, stash, or abort
14333
+ */
14334
+ async askUncommittedChangesAction() {
14335
+ const questions = [{
14336
+ question: "Uncommitted changes detected in the working directory. How would you like to proceed?",
14337
+ header: "Git Status",
14338
+ multiSelect: false,
14339
+ options: [
14340
+ { label: "Ignore", description: "Keep changes and continue with the task" },
14341
+ { label: "Commit", description: "Commit current changes before starting" },
14342
+ { label: "Stash", description: "Stash changes (git stash) before starting" },
14343
+ { label: "Abort", description: "Cancel the task, do nothing" }
14344
+ ]
14345
+ }];
14346
+ try {
14347
+ const response = await this.askUser(questions);
14348
+ const answer = response.answers[0];
14349
+ if (answer.startsWith("other:")) {
14350
+ this.log("info", "GIT", `User provided custom input: ${answer}, defaulting to Abort`);
14351
+ return "Abort";
14352
+ }
14353
+ const labelToAction = {
14354
+ "Ignore": "Ignore",
14355
+ "Commit": "Commit",
14356
+ "Stash": "Stash",
14357
+ "Abort": "Abort"
14358
+ };
14359
+ return labelToAction[answer] || "Abort";
14360
+ } catch (error) {
14361
+ this.log("warn", "GIT", `Failed to get user response for uncommitted changes: ${error}`);
14362
+ return "Abort";
14363
+ }
14364
+ }
13953
14365
  createAgentrixMcpServer() {
13954
14366
  return createSdkMcpServer({
13955
14367
  name: "agentrix",
@@ -14010,6 +14422,50 @@ URL: ${result.pullRequestUrl}`
14010
14422
  };
14011
14423
  }
14012
14424
  }
14425
+ ),
14426
+ tool(
14427
+ "ask_user",
14428
+ 'Ask the user questions when you need clarification or user input. Supports 1-4 questions with 2-4 options each. Use this when you need user decisions or additional information. An "Other" option with free text input is automatically added.',
14429
+ {
14430
+ questions: z.array(z.object({
14431
+ question: z.string().describe("The complete question to ask the user"),
14432
+ header: z.string().max(12).describe("Short label displayed as a chip/tag (max 12 chars)"),
14433
+ multiSelect: z.boolean().describe("Set to true to allow multiple option selections"),
14434
+ options: z.array(z.object({
14435
+ label: z.string().describe("Option label (1-5 words)"),
14436
+ description: z.string().describe("Explanation of what this option means")
14437
+ })).min(2).max(4).describe("Available choices (2-4 options)")
14438
+ })).min(1).max(4).describe("Questions to ask (1-4 questions)")
14439
+ },
14440
+ async (args) => {
14441
+ try {
14442
+ const questionsWithOther = args.questions.map((q) => ({
14443
+ ...q,
14444
+ options: [...q.options, { label: "Other", description: "" }]
14445
+ }));
14446
+ const result = await this.askUser(questionsWithOther);
14447
+ const answerText = result.answers.map((answer) => {
14448
+ if (answer.startsWith("other:")) {
14449
+ return `Other: "${answer.slice(6)}"`;
14450
+ }
14451
+ return answer;
14452
+ }).join("\n");
14453
+ return {
14454
+ content: [{ type: "text", text: `User answers:
14455
+ ${answerText}` }]
14456
+ };
14457
+ } catch (error) {
14458
+ this.log("error", "ASK_USER", "Failed to get user response:", error);
14459
+ return {
14460
+ content: [
14461
+ {
14462
+ type: "text",
14463
+ text: `Failed to get user response: ${error instanceof Error ? error.message : "Unknown error"}`
14464
+ }
14465
+ ]
14466
+ };
14467
+ }
14468
+ }
14013
14469
  )
14014
14470
  ]
14015
14471
  });
@@ -14025,7 +14481,7 @@ URL: ${result.pullRequestUrl}`
14025
14481
  } catch (error) {
14026
14482
  this.log("warn", "GIT", "Failed to handle git state on worker stop:", error);
14027
14483
  }
14028
- this.context.workClient.sendWorkerReady();
14484
+ this.coordinator.setWorkerState("idle");
14029
14485
  }
14030
14486
  filterMessages(message) {
14031
14487
  const msg = message;
@@ -14066,38 +14522,45 @@ URL: ${result.pullRequestUrl}`
14066
14522
  return this.createDefaultAgentConfig();
14067
14523
  }
14068
14524
  const claudeConfig = agentConfig.claude;
14069
- const customMcpServers = Object.fromEntries(
14070
- Object.entries(claudeConfig.mcpServers).map(([name, server]) => [name, server.instance])
14071
- );
14072
- this.log("info", "AGENT", `Agent ${this.options.input.agentId} loaded successfully`);
14073
- return {
14525
+ const customPlugins = claudeConfig.plugins.map((path) => ({
14526
+ type: "local",
14527
+ path
14528
+ }));
14529
+ this.log("info", "AGENT", `Agent ${this.options.input.agentId} loaded successfully (${customPlugins.length} plugins)`);
14530
+ const config = {
14074
14531
  customSystemPrompt: claudeConfig.systemPrompt,
14075
- // Loaded string content
14076
14532
  customModel: claudeConfig.config.model,
14077
14533
  customFallbackModel: claudeConfig.config.fallbackModel,
14078
14534
  customMaxTurns: claudeConfig.config.maxTurns,
14079
14535
  customExtraArgs: claudeConfig.config.extraArgs,
14080
14536
  customPermissionMode: claudeConfig.config.settings?.permissionMode,
14081
- customMcpServers,
14082
14537
  customAllowedTools: claudeConfig.config.settings?.allowedTools || [],
14538
+ customPlugins,
14083
14539
  systemPromptMode: claudeConfig.config.systemPrompt?.mode ?? "append",
14084
- hooks: this.loadedHooks
14085
- // Use cached hooks
14540
+ hooks: this.loadedHooks,
14541
+ customPRPromptTemplate: claudeConfig.prPromptTemplate,
14542
+ prPromptMode: claudeConfig.config.pullRequestPrompt?.mode ?? "append"
14086
14543
  };
14544
+ this.loadedAgentConfig = config;
14545
+ return config;
14087
14546
  } catch (error) {
14088
14547
  this.log("error", "AGENT", `Failed to load agent: ${error instanceof Error ? error.message : String(error)}`);
14089
14548
  return this.createDefaultAgentConfig();
14090
14549
  }
14091
14550
  }
14092
14551
  createDefaultAgentConfig() {
14093
- return {
14552
+ const config = {
14094
14553
  customSystemPrompt: void 0,
14095
14554
  customModel: void 0,
14096
14555
  customMaxTurns: void 0,
14097
- customMcpServers: {},
14098
14556
  customAllowedTools: [],
14099
- systemPromptMode: "append"
14557
+ customPlugins: [],
14558
+ systemPromptMode: "append",
14559
+ customPRPromptTemplate: void 0,
14560
+ prPromptMode: "append"
14100
14561
  };
14562
+ this.loadedAgentConfig = config;
14563
+ return config;
14101
14564
  }
14102
14565
  buildEnvironmentOverrides() {
14103
14566
  return {
@@ -14205,18 +14668,20 @@ URL: ${result.pullRequestUrl}`
14205
14668
  this.timerManager.stopTask("event");
14206
14669
  },
14207
14670
  onTaskMessage: async (message) => {
14208
- if (message.type === "user") {
14671
+ if (isAskUserResponseMessage(message)) {
14672
+ const [eventId, awaiter] = this.askUserAwaiter.entries().next().value || [];
14673
+ if (eventId && awaiter) {
14674
+ this.askUserAwaiter.delete(eventId);
14675
+ awaiter(message);
14676
+ }
14677
+ this.timerManager.clearIdleTimer();
14678
+ return;
14679
+ }
14680
+ if ("type" in message && message.type === "user") {
14209
14681
  const userMessage = message;
14210
14682
  await this.coordinator.enqueue(userMessage);
14211
14683
  this.timerManager.clearIdleTimer();
14212
14684
  }
14213
- },
14214
- onPermissionResponse: async (response) => {
14215
- const awaiter = this.permissionAwaiter.get(response.opCode);
14216
- if (awaiter) {
14217
- this.permissionAwaiter.delete(response.opCode);
14218
- awaiter(response);
14219
- }
14220
14685
  }
14221
14686
  }
14222
14687
  };
@@ -14255,8 +14720,7 @@ URL: ${result.pullRequestUrl}`
14255
14720
  this.log("info", "GIT", `Patch: ${gitStateResult.patchPath}`);
14256
14721
  }
14257
14722
  }
14258
- async createLogger(options) {
14259
- const { createLogger } = await import('./logger-7E71dnBD.mjs').then(function (n) { return n.b; });
14723
+ createWorkerLogger(options) {
14260
14724
  return createLogger(options);
14261
14725
  }
14262
14726
  log(level, category, message, ...args) {
@@ -14277,7 +14741,7 @@ async function runClaude(credentials, options) {
14277
14741
  var require$$0 = /*@__PURE__*/getAugmentedNamespace(logger$1);
14278
14742
 
14279
14743
  function generateAndMapToolId(itemId, idMapper) {
14280
- const uniqueId = randomUUID$1();
14744
+ const uniqueId = randomUUID();
14281
14745
  idMapper.set(itemId, uniqueId);
14282
14746
  return uniqueId;
14283
14747
  }
@@ -14360,7 +14824,7 @@ function convertAgentMessage(item) {
14360
14824
  },
14361
14825
  parent_tool_use_id: null,
14362
14826
  session_id: "",
14363
- uuid: randomUUID$1().toString()
14827
+ uuid: randomUUID().toString()
14364
14828
  };
14365
14829
  }
14366
14830
  function convertCommandExecutionStarted(item, idMapper) {
@@ -15880,28 +16344,6 @@ Example response format:
15880
16344
  CRITICAL: Respond with ONLY the JSON object, no additional text before or after. Now analyze the changes and provide your response:`;
15881
16345
  }
15882
16346
 
15883
- async function downloadImage(url, targetDir) {
15884
- try {
15885
- const urlPath = new URL(url).pathname;
15886
- const extension = extname(urlPath) || ".jpg";
15887
- const filename = `${randomUUID$1()}${extension}`;
15888
- const filePath = join$1(targetDir, filename);
15889
- const response = await fetch(url);
15890
- if (!response.ok) {
15891
- throw new Error(`Failed to download image: ${response.status} ${response.statusText}`);
15892
- }
15893
- if (!response.body) {
15894
- throw new Error("Response body is null");
15895
- }
15896
- const nodeStream = response.body;
15897
- const fileStream = createWriteStream(filePath);
15898
- await pipeline(nodeStream, fileStream);
15899
- return filePath;
15900
- } catch (error) {
15901
- throw new Error(`Failed to download image from ${url}: ${error instanceof Error ? error.message : String(error)}`);
15902
- }
15903
- }
15904
-
15905
16347
  class CodexWorker {
15906
16348
  constructor(credentials, options) {
15907
16349
  this.credentials = credentials;
@@ -16017,6 +16459,7 @@ class CodexWorker {
16017
16459
  };
16018
16460
  this.coordinator = new MessageCoordinator({
16019
16461
  workerType: "codex",
16462
+ workClient,
16020
16463
  handlers: {
16021
16464
  onNormalMessage: async (message) => {
16022
16465
  const input = await this.convertSDKMessageToCodexInput(message);
@@ -16089,7 +16532,6 @@ class CodexWorker {
16089
16532
  }
16090
16533
  );
16091
16534
  this.timerManager.startIdleTimer();
16092
- this.context.workClient.sendWorkerReady();
16093
16535
  this.log("info", "BASH", `Worker ready after command execution (exit code: ${exitCode})`);
16094
16536
  }
16095
16537
  async buildCreatePRPrompt() {
@@ -16146,8 +16588,7 @@ ${stats.files.map((f) => ` ${f.path}: +${f.insertions}/-${f.deletions}`).join("
16146
16588
  skipGitRepoCheck: true
16147
16589
  });
16148
16590
  }
16149
- this.context.workClient.sendWorkRunning();
16150
- this.coordinator.setWorkerState("processing-sdk");
16591
+ this.coordinator.setWorkerState("running");
16151
16592
  while (!this.isStopping) {
16152
16593
  this.log("debug", "AGENT", `Loop iteration: turnCount=${this.turnCount}, queueLength=${this.messageQueue.length}`);
16153
16594
  let userInput = null;
@@ -16159,7 +16600,6 @@ ${stats.files.map((f) => ` ${f.path}: +${f.insertions}/-${f.deletions}`).join("
16159
16600
  await this.reportGitState("idle");
16160
16601
  this.coordinator.setWorkerState("idle");
16161
16602
  this.timerManager.startIdleTimer();
16162
- this.context.workClient.sendWorkerReady();
16163
16603
  this.log("info", "AGENT", "Sent worker-ready, waiting for next message");
16164
16604
  userInput = await this.waitForNextMessage();
16165
16605
  }
@@ -16169,7 +16609,7 @@ ${stats.files.map((f) => ` ${f.path}: +${f.insertions}/-${f.deletions}`).join("
16169
16609
  }
16170
16610
  try {
16171
16611
  this.timerManager.clearIdleTimer();
16172
- this.coordinator.setWorkerState("processing-sdk");
16612
+ this.coordinator.setWorkerState("running");
16173
16613
  await this.processUserInput(userInput);
16174
16614
  this.turnCount++;
16175
16615
  this.log("debug", "AGENT", `Message processed, turnCount now ${this.turnCount}`);
@@ -16205,7 +16645,7 @@ ${stats.files.map((f) => ` ${f.path}: +${f.insertions}/-${f.deletions}`).join("
16205
16645
  const turnOptions = this.inMergeRequest ? { outputSchema: getPROutputSchema() } : {};
16206
16646
  this.log("debug", "AGENT", "Calling thread.runStreamed");
16207
16647
  const { events } = await this.thread.runStreamed(input, turnOptions);
16208
- this.context.workClient.sendWorkRunning();
16648
+ this.coordinator.setWorkerState("running");
16209
16649
  this.log("debug", "AGENT", "Starting to process events");
16210
16650
  for await (const event of events) {
16211
16651
  if (this.isStopping) {
@@ -16309,7 +16749,7 @@ Please try again or create the PR manually.`
16309
16749
  const message = {
16310
16750
  type: "assistant",
16311
16751
  message: {
16312
- id: randomUUID$1().toString(),
16752
+ id: randomUUID().toString(),
16313
16753
  type: "message",
16314
16754
  container: null,
16315
16755
  role: "assistant",
@@ -16326,7 +16766,7 @@ Please try again or create the PR manually.`
16326
16766
  },
16327
16767
  parent_tool_use_id: null,
16328
16768
  session_id: "",
16329
- uuid: randomUUID$1().toString()
16769
+ uuid: randomUUID().toString()
16330
16770
  };
16331
16771
  this.context.workClient.sendTaskMessage(message);
16332
16772
  }
@@ -16380,8 +16820,8 @@ URL: ${result.pullRequestUrl}`);
16380
16820
  }
16381
16821
  /**
16382
16822
  * Convert SDKUserMessage to Codex Input format
16383
- * Handles both text-only messages and messages with images
16384
- * Downloads images from URLs to local attachments directory
16823
+ * Handles both text-only messages and messages with images and documents
16824
+ * Downloads images and documents from URLs to local attachments directory
16385
16825
  */
16386
16826
  async convertSDKMessageToCodexInput(message) {
16387
16827
  const content = message.message.content;
@@ -16390,6 +16830,10 @@ URL: ${result.pullRequestUrl}`);
16390
16830
  }
16391
16831
  if (Array.isArray(content)) {
16392
16832
  const userInputs = [];
16833
+ const attachmentsDir = machine.resolveAttachmentsDir(
16834
+ this.options.input.userId,
16835
+ this.taskId
16836
+ );
16393
16837
  for (const block of content) {
16394
16838
  if (block.type === "text" && block.text) {
16395
16839
  userInputs.push({
@@ -16399,19 +16843,30 @@ URL: ${result.pullRequestUrl}`);
16399
16843
  } else if (block.type === "image" && block.source && block.source.url) {
16400
16844
  const url = block.source.url;
16401
16845
  try {
16402
- const attachmentsDir = machine.resolveAttachmentsDir(
16403
- this.options.input.userId,
16404
- this.taskId
16405
- );
16406
- const localPath = await downloadImage(url, attachmentsDir);
16407
- this.log("info", "IMAGE", `Downloaded image from ${url} to ${localPath}`);
16846
+ const { filePath } = await downloadFile(url, attachmentsDir, false);
16847
+ this.log("info", "IMAGE", `Downloaded image from ${url} to ${filePath}`);
16408
16848
  userInputs.push({
16409
16849
  type: "local_image",
16410
- path: localPath
16850
+ path: filePath
16411
16851
  });
16412
16852
  } catch (error) {
16413
16853
  this.log("error", "IMAGE", `Failed to download image from ${url}:`, error);
16414
16854
  }
16855
+ } else if (block.type === "document" && block.source && block.source.url) {
16856
+ const url = block.source.url;
16857
+ try {
16858
+ const { filePath, mimeType, filename } = await downloadFile(url, attachmentsDir, true);
16859
+ this.log("info", "DOCUMENT", `Downloaded document from ${url} to ${filePath}`);
16860
+ const title = block.title || filename;
16861
+ userInputs.push({
16862
+ type: "text",
16863
+ text: `Document: ${filePath}
16864
+ Title: ${title}
16865
+ Type: ${mimeType}`
16866
+ });
16867
+ } catch (error) {
16868
+ this.log("error", "DOCUMENT", `Failed to download document from ${url}:`, error);
16869
+ }
16415
16870
  }
16416
16871
  }
16417
16872
  if (userInputs.length === 1 && userInputs[0].type === "text") {
@@ -16481,7 +16936,7 @@ URL: ${result.pullRequestUrl}`);
16481
16936
  }
16482
16937
  async handleStopHook() {
16483
16938
  await this.reportGitState("stop");
16484
- this.context.workClient.sendWorkerReady();
16939
+ this.coordinator.setWorkerState("idle");
16485
16940
  }
16486
16941
  async loadAgentConfiguration() {
16487
16942
  if (!this.options.input.agentId || this.options.input.agentId === "default") {
@@ -16573,14 +17028,15 @@ URL: ${result.pullRequestUrl}`);
16573
17028
  this.timerManager.stopTask("event");
16574
17029
  },
16575
17030
  onTaskMessage: async (message) => {
16576
- if (message.type === "user") {
17031
+ if (isAskUserResponseMessage(message)) {
17032
+ this.log("debug", "AGENT", "Received ask_user_response (not used by Codex)");
17033
+ return;
17034
+ }
17035
+ if ("type" in message && message.type === "user") {
16577
17036
  const userMessage = message;
16578
17037
  await this.coordinator.enqueue(userMessage);
16579
17038
  this.timerManager.clearIdleTimer();
16580
17039
  }
16581
- },
16582
- onPermissionResponse: async (response) => {
16583
- this.log("debug", "AGENT", `Permission response received: ${response.behavior}`);
16584
17040
  }
16585
17041
  }
16586
17042
  };
@@ -16935,7 +17391,7 @@ cli.command("upgrade", "Upgrade CLI to the latest version", {}, async (argv) =>
16935
17391
  }
16936
17392
  }
16937
17393
  try {
16938
- const { version } = await import('./logger-7E71dnBD.mjs').then(function (n) { return n._; });
17394
+ const { version } = await import('./logger-BzpMLIL-.mjs').then(function (n) { return n._; });
16939
17395
  console.log(chalk.green(`
16940
17396
  \u2713 Now running version: ${version}`));
16941
17397
  } catch {
@@ -17117,7 +17573,7 @@ cli.command(
17117
17573
  },
17118
17574
  async (argv) => {
17119
17575
  try {
17120
- const { testCommand } = await import('./index-zM9f-RKY.mjs');
17576
+ const { testCommand } = await import('./index-DNDJMHW8.mjs');
17121
17577
  await testCommand(argv);
17122
17578
  } catch (error) {
17123
17579
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Test mode failed");