episoda 0.2.103 → 0.2.105

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.
@@ -2117,6 +2117,7 @@ var require_websocket_client = __commonJS({
2117
2117
  this.lastCommandTime = Date.now();
2118
2118
  this.isIntentionalDisconnect = false;
2119
2119
  this.lastConnectAttemptTime = 0;
2120
+ this.consecutiveAuthFailures = 0;
2120
2121
  }
2121
2122
  /**
2122
2123
  * Connect to episoda.dev WebSocket gateway
@@ -2384,6 +2385,10 @@ var require_websocket_client = __commonJS({
2384
2385
  this.rateLimitBackoffUntil = Date.now() + retryAfterMs;
2385
2386
  console.log(`[EpisodaClient] ${errorMessage.code}: will retry after ${retryAfterMs / 1e3}s`);
2386
2387
  }
2388
+ if (errorMessage.code === "AUTH_FAILED" || errorMessage.code === "UNAUTHORIZED" || errorMessage.code === "INVALID_TOKEN") {
2389
+ this.consecutiveAuthFailures++;
2390
+ console.warn(`[EpisodaClient] Auth failure (${this.consecutiveAuthFailures}): ${errorMessage.code}`);
2391
+ }
2387
2392
  }
2388
2393
  const handlers = this.eventHandlers.get(message.type) || [];
2389
2394
  handlers.forEach((handler) => {
@@ -2442,7 +2447,19 @@ var require_websocket_client = __commonJS({
2442
2447
  }
2443
2448
  let delay;
2444
2449
  let shouldRetry = true;
2445
- if (this.isGracefulShutdown) {
2450
+ const isCloudMode = this.environment === "cloud";
2451
+ const MAX_CLOUD_AUTH_FAILURES = 3;
2452
+ const MAX_CLOUD_RECONNECT_DELAY = 3e5;
2453
+ if (isCloudMode) {
2454
+ if (this.consecutiveAuthFailures >= MAX_CLOUD_AUTH_FAILURES) {
2455
+ console.error(`[EpisodaClient] Cloud mode: ${MAX_CLOUD_AUTH_FAILURES} consecutive auth failures - token may be invalid. Giving up.`);
2456
+ shouldRetry = false;
2457
+ } else {
2458
+ delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), MAX_CLOUD_RECONNECT_DELAY);
2459
+ const delayStr = delay >= 6e4 ? `${Math.round(delay / 6e4)}m` : `${Math.round(delay / 1e3)}s`;
2460
+ console.log(`[EpisodaClient] Cloud mode: reconnecting in ${delayStr}... (attempt ${this.reconnectAttempts + 1}, never giving up)`);
2461
+ }
2462
+ } else if (this.isGracefulShutdown) {
2446
2463
  if (this.reconnectAttempts >= 7) {
2447
2464
  console.error('[EpisodaClient] Server restart reconnection failed after 7 attempts. Run "episoda dev" to reconnect.');
2448
2465
  shouldRetry = false;
@@ -2485,6 +2502,7 @@ var require_websocket_client = __commonJS({
2485
2502
  this.isGracefulShutdown = false;
2486
2503
  this.firstDisconnectTime = void 0;
2487
2504
  this.rateLimitBackoffUntil = void 0;
2505
+ this.consecutiveAuthFailures = 0;
2488
2506
  }).catch((error) => {
2489
2507
  console.error("[EpisodaClient] Reconnection failed:", error.message);
2490
2508
  });
@@ -2786,7 +2804,7 @@ var require_package = __commonJS({
2786
2804
  "package.json"(exports2, module2) {
2787
2805
  module2.exports = {
2788
2806
  name: "episoda",
2789
- version: "0.2.103",
2807
+ version: "0.2.105",
2790
2808
  description: "CLI tool for Episoda local development workflow orchestration",
2791
2809
  main: "dist/index.js",
2792
2810
  types: "dist/index.d.ts",
@@ -8374,7 +8392,7 @@ var AgentManager = class {
8374
8392
  * EP1173: Added autonomousMode parameter for permission-free execution
8375
8393
  */
8376
8394
  async startSession(options) {
8377
- const { sessionId, moduleId, moduleUid, projectPath, provider = "claude", autonomousMode = true, message, credentials, systemPrompt, onChunk, onComplete, onError } = options;
8395
+ const { sessionId, moduleId, moduleUid, projectPath, provider = "claude", autonomousMode = true, canWrite = true, readOnlyReason, message, credentials, systemPrompt, onChunk, onComplete, onError } = options;
8378
8396
  if (this.sessions.has(sessionId)) {
8379
8397
  return { success: false, error: "Session already exists" };
8380
8398
  }
@@ -8413,6 +8431,10 @@ var AgentManager = class {
8413
8431
  // EP1133: Store provider in session
8414
8432
  autonomousMode,
8415
8433
  // EP1173: Store autonomous mode setting
8434
+ canWrite,
8435
+ // EP1205: Store write permission
8436
+ readOnlyReason,
8437
+ // EP1205: Store reason for read-only mode
8416
8438
  credentials,
8417
8439
  systemPrompt,
8418
8440
  status: "starting",
@@ -8437,16 +8459,50 @@ var AgentManager = class {
8437
8459
  * EP1133: Supports both Claude Code and Codex CLI with provider-specific handling.
8438
8460
  */
8439
8461
  async sendMessage(options) {
8440
- const { sessionId, message, isFirstMessage, agentSessionId, claudeSessionId, onChunk, onComplete, onError } = options;
8462
+ const { sessionId, message, isFirstMessage, canWrite, readOnlyReason, agentSessionId, claudeSessionId, onChunk, onComplete, onError } = options;
8441
8463
  const session = this.sessions.get(sessionId);
8442
8464
  if (!session) {
8443
8465
  return { success: false, error: "Session not found" };
8444
8466
  }
8445
8467
  session.lastActivityAt = /* @__PURE__ */ new Date();
8446
8468
  session.status = "running";
8469
+ if (canWrite !== void 0) {
8470
+ session.canWrite = canWrite;
8471
+ session.readOnlyReason = readOnlyReason;
8472
+ }
8447
8473
  const resumeSessionId = agentSessionId || claudeSessionId;
8448
8474
  try {
8449
8475
  const provider = session.provider || "claude";
8476
+ let effectiveSystemPrompt = session.systemPrompt || "";
8477
+ if (session.canWrite === false) {
8478
+ const readOnlyNotice = `
8479
+ \u26A0\uFE0F READ-ONLY SESSION - STRICT ENFORCEMENT
8480
+
8481
+ You are operating in READ-ONLY mode. You MUST NOT perform ANY write operations:
8482
+
8483
+ FORBIDDEN ACTIONS:
8484
+ - Create, modify, or delete any files (Write/Edit tools are blocked)
8485
+ - Run Bash commands that modify files (rm, mv, cp, touch, echo >, sed -i, etc.)
8486
+ - Run Bash commands that modify git state (git commit, git push, git checkout --, etc.)
8487
+ - Make any changes to the filesystem whatsoever
8488
+
8489
+ Reason: ${session.readOnlyReason || "You do not have write access to this module."}
8490
+
8491
+ ALLOWED ACTIONS:
8492
+ - Read files (using Read tool)
8493
+ - Run read-only Bash commands (ls, cat, git status, git log, git diff, grep, find, etc.)
8494
+ - Analyze code and provide recommendations
8495
+ - Explain architecture and suggest changes (but not implement them)
8496
+
8497
+ If the user requests changes, explain what would need to be done but DO NOT execute any write operations.
8498
+ Violations will result in errors and may affect your ability to assist.
8499
+
8500
+ ---
8501
+
8502
+ `;
8503
+ effectiveSystemPrompt = readOnlyNotice + effectiveSystemPrompt;
8504
+ console.log(`[AgentManager] EP1205: Read-only mode enabled for session ${sessionId}: ${session.readOnlyReason}`);
8505
+ }
8450
8506
  let binaryPath;
8451
8507
  let args;
8452
8508
  if (provider === "codex") {
@@ -8461,21 +8517,39 @@ var AgentManager = class {
8461
8517
  session.projectPath
8462
8518
  // Working directory
8463
8519
  ];
8464
- if (session.autonomousMode) {
8520
+ if (session.autonomousMode && session.canWrite !== false) {
8465
8521
  args.push("--full-auto");
8466
8522
  console.log(`[AgentManager] EP1173: Codex autonomous mode enabled - using --full-auto`);
8467
8523
  }
8524
+ if (session.canWrite === false) {
8525
+ args.push("--sandbox", "read-only");
8526
+ console.log(`[AgentManager] EP1205: Codex read-only mode - using --sandbox read-only`);
8527
+ }
8468
8528
  if (resumeSessionId) {
8469
8529
  args.push("resume", resumeSessionId);
8470
8530
  }
8471
8531
  let fullMessage = message;
8472
- if (isFirstMessage && session.systemPrompt) {
8473
- fullMessage = `${session.systemPrompt}
8532
+ if (isFirstMessage && effectiveSystemPrompt) {
8533
+ fullMessage = `${effectiveSystemPrompt}
8474
8534
 
8475
8535
  ---
8476
8536
 
8477
8537
  ${message}`;
8478
8538
  }
8539
+ if (session.canWrite === false && !isFirstMessage) {
8540
+ const readOnlyReminder = `
8541
+ [SYSTEM REMINDER - READ-ONLY MODE]
8542
+ You are in READ-ONLY mode. Do NOT:
8543
+ - Create, modify, or delete any files
8544
+ - Run commands that write to the filesystem
8545
+ - Make git commits or changes
8546
+ Reason: ${session.readOnlyReason || "No write access to this module."}
8547
+ If changes are needed, explain what needs to be done but do not execute.
8548
+ [END SYSTEM REMINDER]
8549
+
8550
+ `;
8551
+ fullMessage = readOnlyReminder + fullMessage;
8552
+ }
8479
8553
  args.push(fullMessage);
8480
8554
  } else {
8481
8555
  binaryPath = await ensureClaudeBinary();
@@ -8494,12 +8568,26 @@ ${message}`;
8494
8568
  args.push("--model", session.credentials.preferredModel);
8495
8569
  console.log(`[AgentManager] EP1152: Using user preferred model: ${session.credentials.preferredModel}`);
8496
8570
  }
8497
- if (session.autonomousMode) {
8571
+ if (session.autonomousMode && session.canWrite !== false) {
8498
8572
  args.push("--dangerously-skip-permissions");
8499
8573
  console.log(`[AgentManager] EP1173: Autonomous mode enabled - skipping permission prompts`);
8574
+ } else if (session.autonomousMode && session.canWrite === false) {
8575
+ console.log(`[AgentManager] EP1205: Autonomous mode with read-only - NOT skipping permissions (safety net)`);
8500
8576
  }
8501
- if (isFirstMessage && session.systemPrompt) {
8502
- args.push("--system-prompt", session.systemPrompt);
8577
+ if (isFirstMessage && effectiveSystemPrompt) {
8578
+ args.push("--system-prompt", effectiveSystemPrompt);
8579
+ }
8580
+ if (session.canWrite === false) {
8581
+ args.push("--disallowed-tools", "Write,Edit,MultiEdit,NotebookEdit");
8582
+ console.log("[AgentManager] EP1205: Read-only enforcement - disallowed Write/Edit tools");
8583
+ if (!isFirstMessage) {
8584
+ const readOnlyReminder = `
8585
+ REMINDER: You are in READ-ONLY mode. You cannot create, modify, or delete files.
8586
+ Reason: ${session.readOnlyReason || "No write access to this module."}
8587
+ If changes are needed, explain what needs to be done.`;
8588
+ args.push("--append-system-prompt", readOnlyReminder);
8589
+ console.log("[AgentManager] EP1205: Appended read-only reminder to subsequent message");
8590
+ }
8503
8591
  }
8504
8592
  if (resumeSessionId) {
8505
8593
  args.push("--resume", resumeSessionId);
@@ -9396,6 +9484,7 @@ function getInstallCommand(cwd) {
9396
9484
 
9397
9485
  // src/daemon/daemon-process.ts
9398
9486
  var fs21 = __toESM(require("fs"));
9487
+ var http2 = __toESM(require("http"));
9399
9488
  var os8 = __toESM(require("os"));
9400
9489
  var path22 = __toESM(require("path"));
9401
9490
  var packageJson = require_package();
@@ -9530,6 +9619,8 @@ var Daemon = class _Daemon {
9530
9619
  // 60 seconds
9531
9620
  // EP1190: Worktree cleanup runs every N health checks (5 * 60s = 5 minutes)
9532
9621
  this.healthCheckCounter = 0;
9622
+ // EP1210-7: Health HTTP endpoint for external monitoring
9623
+ this.healthServer = null;
9533
9624
  this.ipcServer = new IPCServer();
9534
9625
  }
9535
9626
  static {
@@ -9549,6 +9640,9 @@ var Daemon = class _Daemon {
9549
9640
  static {
9550
9641
  this.WORKTREE_CLEANUP_EVERY_N_CHECKS = 5;
9551
9642
  }
9643
+ static {
9644
+ this.HEALTH_PORT = 9999;
9645
+ }
9552
9646
  /**
9553
9647
  * Start the daemon
9554
9648
  */
@@ -9570,6 +9664,7 @@ var Daemon = class _Daemon {
9570
9664
  await this.auditWorktreesOnStartup();
9571
9665
  this.startHealthCheckPolling();
9572
9666
  this.setupShutdownHandlers();
9667
+ this.startHealthEndpoint();
9573
9668
  console.log("[Daemon] Daemon started successfully");
9574
9669
  const modeConfig = getDaemonModeConfig();
9575
9670
  console.log("[Daemon] EP1115: Mode config:", {
@@ -9599,6 +9694,55 @@ var Daemon = class _Daemon {
9599
9694
  }
9600
9695
  }
9601
9696
  // EP738: Removed startHttpServer - device info now flows through WebSocket broadcast + database
9697
+ /**
9698
+ * EP1210-7: Start health HTTP endpoint for external monitoring
9699
+ *
9700
+ * Provides a simple HTTP endpoint that external systems can use to check daemon status.
9701
+ * Returns 200 when daemon has at least one live connection, 503 when disconnected.
9702
+ *
9703
+ * Only binds to localhost (127.0.0.1) for security.
9704
+ */
9705
+ startHealthEndpoint() {
9706
+ try {
9707
+ this.healthServer = http2.createServer((req, res) => {
9708
+ if (req.url === "/health" || req.url === "/") {
9709
+ const isConnected = this.liveConnections.size > 0;
9710
+ const projects = Array.from(this.connections.entries()).map(([path23, conn]) => ({
9711
+ path: path23,
9712
+ connected: this.liveConnections.has(path23)
9713
+ }));
9714
+ const status = {
9715
+ status: isConnected ? "healthy" : "degraded",
9716
+ connected: isConnected,
9717
+ machineId: this.machineId,
9718
+ uptime: process.uptime(),
9719
+ liveConnections: this.liveConnections.size,
9720
+ totalConnections: this.connections.size,
9721
+ projects
9722
+ };
9723
+ res.writeHead(isConnected ? 200 : 503, { "Content-Type": "application/json" });
9724
+ res.end(JSON.stringify(status));
9725
+ } else {
9726
+ res.writeHead(404);
9727
+ res.end("Not found");
9728
+ }
9729
+ });
9730
+ this.healthServer.listen(_Daemon.HEALTH_PORT, "127.0.0.1", () => {
9731
+ console.log(`[Daemon] EP1210-7: Health endpoint listening on http://127.0.0.1:${_Daemon.HEALTH_PORT}/health`);
9732
+ });
9733
+ this.healthServer.on("error", (err) => {
9734
+ if (err.code === "EADDRINUSE") {
9735
+ console.warn(`[Daemon] EP1210-7: Health port ${_Daemon.HEALTH_PORT} already in use, skipping health endpoint`);
9736
+ } else {
9737
+ console.warn("[Daemon] EP1210-7: Health endpoint failed to start:", err.message);
9738
+ }
9739
+ this.healthServer = null;
9740
+ });
9741
+ } catch (err) {
9742
+ console.warn("[Daemon] EP1210-7: Failed to create health server:", err.message);
9743
+ this.healthServer = null;
9744
+ }
9745
+ }
9602
9746
  /**
9603
9747
  * Register IPC command handlers
9604
9748
  */
@@ -10217,11 +10361,23 @@ var Daemon = class _Daemon {
10217
10361
  if (cmd.action === "start") {
10218
10362
  const callbacks = createStreamingCallbacks(cmd.sessionId, message.id);
10219
10363
  let agentWorkingDir = projectPath;
10220
- if (cmd.moduleUid) {
10364
+ if (cmd.moduleUid && cmd.sessionContext === "worktree") {
10221
10365
  const worktreeInfo = await getWorktreeInfoForModule(cmd.moduleUid);
10222
10366
  if (worktreeInfo?.exists) {
10223
10367
  agentWorkingDir = worktreeInfo.path;
10224
- console.log(`[Daemon] EP959: Agent for ${cmd.moduleUid} in worktree: ${agentWorkingDir}`);
10368
+ console.log(`[Daemon] EP1205: Agent for ${cmd.moduleUid} in worktree: ${agentWorkingDir}`);
10369
+ } else {
10370
+ console.log(`[Daemon] EP1205: Worktree requested but not found for ${cmd.moduleUid}, using project root`);
10371
+ }
10372
+ } else if (cmd.sessionContext === "project_root") {
10373
+ console.log(`[Daemon] EP1205: Agent for ${cmd.moduleUid || "project"} in project root (sessionContext=project_root)`);
10374
+ } else {
10375
+ if (cmd.moduleUid) {
10376
+ const worktreeInfo = await getWorktreeInfoForModule(cmd.moduleUid);
10377
+ if (worktreeInfo?.exists) {
10378
+ agentWorkingDir = worktreeInfo.path;
10379
+ console.log(`[Daemon] EP959: Agent for ${cmd.moduleUid} in worktree (legacy, no sessionContext): ${agentWorkingDir}`);
10380
+ }
10225
10381
  }
10226
10382
  }
10227
10383
  const startResult = await agentManager.startSession({
@@ -10233,6 +10389,10 @@ var Daemon = class _Daemon {
10233
10389
  // EP1133: Multi-provider support
10234
10390
  autonomousMode: cmd.autonomousMode ?? true,
10235
10391
  // EP1173: Default to autonomous
10392
+ canWrite: cmd.canWrite ?? true,
10393
+ // EP1205: Default to writable
10394
+ readOnlyReason: cmd.readOnlyReason,
10395
+ // EP1205: Pass reason for UI
10236
10396
  message: cmd.message,
10237
10397
  credentials: cmd.credentials,
10238
10398
  systemPrompt: cmd.systemPrompt,
@@ -10250,6 +10410,10 @@ var Daemon = class _Daemon {
10250
10410
  sessionId: cmd.sessionId,
10251
10411
  message: cmd.message,
10252
10412
  isFirstMessage: false,
10413
+ canWrite: cmd.canWrite,
10414
+ // EP1205: Update write permission if changed
10415
+ readOnlyReason: cmd.readOnlyReason,
10416
+ // EP1205: Update reason if changed
10253
10417
  agentSessionId: cmd.agentSessionId || cmd.claudeSessionId,
10254
10418
  claudeSessionId: cmd.claudeSessionId,
10255
10419
  // Backward compat
@@ -11660,6 +11824,10 @@ var Daemon = class _Daemon {
11660
11824
  if (this.shuttingDown) return;
11661
11825
  this.shuttingDown = true;
11662
11826
  console.log("[Daemon] Shutting down...");
11827
+ if (this.healthServer) {
11828
+ this.healthServer.close();
11829
+ this.healthServer = null;
11830
+ }
11663
11831
  this.stopTunnelPolling();
11664
11832
  this.stopHealthCheckPolling();
11665
11833
  for (const [projectPath, connection] of this.connections) {