@episoda/cli 0.2.182 → 0.2.187

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.
@@ -3046,7 +3046,7 @@ var require_package = __commonJS({
3046
3046
  "package.json"(exports2, module2) {
3047
3047
  module2.exports = {
3048
3048
  name: "@episoda/cli",
3049
- version: "0.2.182",
3049
+ version: "0.2.187",
3050
3050
  description: "CLI tool for Episoda local development workflow orchestration",
3051
3051
  main: "dist/index.js",
3052
3052
  types: "dist/index.d.ts",
@@ -12875,7 +12875,7 @@ var pty = __toESM(require("@lydell/node-pty"));
12875
12875
  var INACTIVITY_TIMEOUT_MS3 = 30 * 60 * 1e3;
12876
12876
  var sessions = /* @__PURE__ */ new Map();
12877
12877
  async function handlePtySpawn(payload, client) {
12878
- const { moduleUid, agent_run_id, command, args, env, cwd, cols = 220, rows = 50 } = payload;
12878
+ const { moduleUid, agent_run_id, command, args, stdin, env, cwd, cols = 220, rows = 50 } = payload;
12879
12879
  if (sessions.has(agent_run_id)) {
12880
12880
  console.warn(`[PTY] Session already exists for agent_run_id ${agent_run_id}, ignoring spawn`);
12881
12881
  return;
@@ -12933,6 +12933,9 @@ async function handlePtySpawn(payload, client) {
12933
12933
  console.error(`[PTY] Failed to send pty_data for ${agent_run_id}:`, err.message);
12934
12934
  });
12935
12935
  });
12936
+ if (typeof stdin === "string" && stdin.trim().length > 0) {
12937
+ proc.write(`${stdin.replace(/\n/g, "\r")}\r`);
12938
+ }
12936
12939
  proc.onExit(({ exitCode }) => {
12937
12940
  const durationMs = Date.now() - session.startedAt;
12938
12941
  if (session.watchdogTimer) clearTimeout(session.watchdogTimer);
@@ -15012,9 +15015,19 @@ var ProjectMessageRouter = class {
15012
15015
  client.on("pty_spawn", async (message) => {
15013
15016
  if (message.type === "pty_spawn") {
15014
15017
  const payload = message.payload;
15018
+ let resolvedPayload = payload;
15019
+ if (!payload.cwd && payload.moduleUid) {
15020
+ const worktreeInfo = await getWorktreeInfoForModule(payload.moduleUid);
15021
+ if (worktreeInfo?.path) {
15022
+ resolvedPayload = {
15023
+ ...payload,
15024
+ cwd: worktreeInfo.path
15025
+ };
15026
+ }
15027
+ }
15015
15028
  console.log(`[Daemon] EP1441: Received pty_spawn for ${projectId}: ${payload.agent_run_id}`);
15016
15029
  client.updateActivity();
15017
- await handlePtySpawn(payload, client);
15030
+ await handlePtySpawn(resolvedPayload, client);
15018
15031
  }
15019
15032
  });
15020
15033
  client.on("pty_resize", (message) => {
@@ -15311,10 +15324,7 @@ var Daemon = class _Daemon {
15311
15324
  });
15312
15325
  }
15313
15326
  static {
15314
- this.AGENT_HEARTBEAT_INTERVAL_MS = 15e3;
15315
- }
15316
- static {
15317
- this.AGENT_HEARTBEAT_REQUEST_TIMEOUT_MS = 12e3;
15327
+ this.AGENT_HEARTBEAT_INTERVAL_MS = 6e4;
15318
15328
  }
15319
15329
  static {
15320
15330
  this.HEALTH_PORT = 9999;
@@ -15341,59 +15351,58 @@ var Daemon = class _Daemon {
15341
15351
  loop.stop();
15342
15352
  this.agentHeartbeatLoops.delete(sessionId);
15343
15353
  }
15344
- async handleServerRequestedAgentStop(sessionId) {
15354
+ async shouldContinueAgentSession(sessionId) {
15355
+ try {
15356
+ const config = await (0, import_core22.loadConfig)();
15357
+ const apiUrl = config?.api_url || "https://episoda.dev";
15358
+ const response = await fetchWithAuth(`${apiUrl}/api/agent-sessions/${encodeURIComponent(sessionId)}/heartbeat`, {
15359
+ method: "POST"
15360
+ });
15361
+ if (!response.ok) {
15362
+ console.warn(`[Daemon] EP1429: Heartbeat API returned ${response.status} for session ${sessionId}; continuing session`);
15363
+ return true;
15364
+ }
15365
+ const payload = await response.json().catch(() => null);
15366
+ return payload?.continue !== false;
15367
+ } catch (error) {
15368
+ console.warn(`[Daemon] EP1429: Heartbeat API check failed for session ${sessionId}; continuing session`, error);
15369
+ return true;
15370
+ }
15371
+ }
15372
+ async stopAgentSessionFromServerRequest(sessionId) {
15345
15373
  try {
15346
15374
  const agentManager = getAgentControlPlane();
15375
+ await agentManager.initialize();
15347
15376
  await agentManager.stopSession(sessionId);
15348
- console.log(`[Daemon] EP1430: Stopped session ${sessionId} due to server continue=false`);
15377
+ this.agentEventSeq.delete(sessionId);
15378
+ console.log(`[Daemon] EP1429: Stopped agent session ${sessionId} on server heartbeat continue=false`);
15349
15379
  } catch (error) {
15350
- console.warn(
15351
- `[Daemon] EP1430: Failed to stop session ${sessionId} after server continue=false:`,
15352
- error
15353
- );
15380
+ console.warn(`[Daemon] EP1429: Failed to stop session ${sessionId} after continue=false`, error);
15354
15381
  }
15355
15382
  }
15356
- startAgentHeartbeatLoop(sessionId) {
15383
+ startAgentHeartbeatLoop(sessionId, client) {
15357
15384
  if (this.agentHeartbeatLoops.has(sessionId)) return;
15358
15385
  let stopped = false;
15359
- let inFlight = false;
15360
15386
  let timer = null;
15361
15387
  const runHeartbeat = async () => {
15362
- if (stopped || inFlight) return;
15363
- inFlight = true;
15364
- const abortController = new AbortController();
15365
- const requestTimeout = setTimeout(() => {
15366
- abortController.abort();
15367
- }, _Daemon.AGENT_HEARTBEAT_REQUEST_TIMEOUT_MS);
15388
+ if (stopped) return;
15368
15389
  try {
15369
- const config = await (0, import_core22.loadConfig)();
15370
- if (!config?.access_token) return;
15371
- const apiUrl = config.api_url || "https://episoda.dev";
15372
- const response = await fetchWithAuth(
15373
- `${apiUrl}/api/agent-sessions/${sessionId}/heartbeat`,
15374
- {
15375
- method: "POST",
15376
- signal: abortController.signal
15377
- }
15378
- );
15379
- if (!response.ok && response.status !== 409) {
15380
- return;
15381
- }
15382
- const payload = await response.json().catch(() => null);
15383
- const shouldContinue = response.status === 409 ? false : Boolean(payload?.data?.continue ?? payload?.continue ?? true);
15390
+ await client.send({
15391
+ type: "agent_heartbeat",
15392
+ sessionId,
15393
+ pid: process.pid,
15394
+ status: "running"
15395
+ });
15396
+ const shouldContinue = await this.shouldContinueAgentSession(sessionId);
15384
15397
  if (!shouldContinue) {
15385
- await this.handleServerRequestedAgentStop(sessionId);
15386
15398
  this.stopAgentHeartbeatLoop(sessionId);
15399
+ await this.stopAgentSessionFromServerRequest(sessionId);
15387
15400
  }
15388
15401
  } catch (error) {
15389
- if (error instanceof Error && error.name === "AbortError") {
15390
- console.warn(
15391
- `[Daemon] EP1430: Heartbeat request timed out for session ${sessionId} after ${_Daemon.AGENT_HEARTBEAT_REQUEST_TIMEOUT_MS}ms`
15392
- );
15393
- }
15394
- } finally {
15395
- clearTimeout(requestTimeout);
15396
- inFlight = false;
15402
+ console.warn(
15403
+ `[Daemon] EP1429: Failed to send agent_heartbeat for session ${sessionId}:`,
15404
+ error
15405
+ );
15397
15406
  }
15398
15407
  };
15399
15408
  timer = setInterval(() => {
@@ -15848,7 +15857,7 @@ var Daemon = class _Daemon {
15848
15857
  await agentManager.initialize();
15849
15858
  let result;
15850
15859
  if (cmd.action === "start") {
15851
- this.startAgentHeartbeatLoop(cmd.sessionId);
15860
+ this.startAgentHeartbeatLoop(cmd.sessionId, client);
15852
15861
  const callbacks = createStreamingCallbacks(cmd.sessionId, message.id);
15853
15862
  const agentWorkingDir = await resolveAgentWorkingDirectory({
15854
15863
  command: cmd,
@@ -15889,7 +15898,7 @@ var Daemon = class _Daemon {
15889
15898
  error: startResult.error
15890
15899
  };
15891
15900
  } else if (cmd.action === "message") {
15892
- this.startAgentHeartbeatLoop(cmd.sessionId);
15901
+ this.startAgentHeartbeatLoop(cmd.sessionId, client);
15893
15902
  const callbacks = createStreamingCallbacks(cmd.sessionId, message.id);
15894
15903
  const sendResult = await agentManager.sendMessage({
15895
15904
  sessionId: cmd.sessionId,