episoda 0.2.104 → 0.2.106

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.
@@ -1571,15 +1571,15 @@ var require_git_executor = __commonJS({
1571
1571
  try {
1572
1572
  const { stdout: gitDir } = await execAsync3("git rev-parse --git-dir", { cwd, timeout: 5e3 });
1573
1573
  const gitDirPath = gitDir.trim();
1574
- const fs22 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1574
+ const fs23 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1575
1575
  const rebaseMergePath = `${gitDirPath}/rebase-merge`;
1576
1576
  const rebaseApplyPath = `${gitDirPath}/rebase-apply`;
1577
1577
  try {
1578
- await fs22.access(rebaseMergePath);
1578
+ await fs23.access(rebaseMergePath);
1579
1579
  inRebase = true;
1580
1580
  } catch {
1581
1581
  try {
1582
- await fs22.access(rebaseApplyPath);
1582
+ await fs23.access(rebaseApplyPath);
1583
1583
  inRebase = true;
1584
1584
  } catch {
1585
1585
  inRebase = false;
@@ -1633,9 +1633,9 @@ var require_git_executor = __commonJS({
1633
1633
  error: validation.error || "UNKNOWN_ERROR"
1634
1634
  };
1635
1635
  }
1636
- const fs22 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1636
+ const fs23 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1637
1637
  try {
1638
- await fs22.access(command.path);
1638
+ await fs23.access(command.path);
1639
1639
  return {
1640
1640
  success: false,
1641
1641
  error: "WORKTREE_EXISTS",
@@ -1689,9 +1689,9 @@ var require_git_executor = __commonJS({
1689
1689
  */
1690
1690
  async executeWorktreeRemove(command, cwd, options) {
1691
1691
  try {
1692
- const fs22 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1692
+ const fs23 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1693
1693
  try {
1694
- await fs22.access(command.path);
1694
+ await fs23.access(command.path);
1695
1695
  } catch {
1696
1696
  return {
1697
1697
  success: false,
@@ -1726,7 +1726,7 @@ var require_git_executor = __commonJS({
1726
1726
  const result = await this.runGitCommand(args, cwd, options);
1727
1727
  if (result.success) {
1728
1728
  try {
1729
- await fs22.rm(command.path, { recursive: true, force: true });
1729
+ await fs23.rm(command.path, { recursive: true, force: true });
1730
1730
  } catch {
1731
1731
  }
1732
1732
  return {
@@ -1860,10 +1860,10 @@ var require_git_executor = __commonJS({
1860
1860
  */
1861
1861
  async executeCloneBare(command, options) {
1862
1862
  try {
1863
- const fs22 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1864
- const path23 = await Promise.resolve().then(() => __importStar(require("path")));
1863
+ const fs23 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1864
+ const path24 = await Promise.resolve().then(() => __importStar(require("path")));
1865
1865
  try {
1866
- await fs22.access(command.path);
1866
+ await fs23.access(command.path);
1867
1867
  return {
1868
1868
  success: false,
1869
1869
  error: "BRANCH_ALREADY_EXISTS",
@@ -1872,9 +1872,9 @@ var require_git_executor = __commonJS({
1872
1872
  };
1873
1873
  } catch {
1874
1874
  }
1875
- const parentDir = path23.dirname(command.path);
1875
+ const parentDir = path24.dirname(command.path);
1876
1876
  try {
1877
- await fs22.mkdir(parentDir, { recursive: true });
1877
+ await fs23.mkdir(parentDir, { recursive: true });
1878
1878
  } catch {
1879
1879
  }
1880
1880
  const { stdout, stderr } = await execAsync3(
@@ -1922,22 +1922,22 @@ var require_git_executor = __commonJS({
1922
1922
  */
1923
1923
  async executeProjectInfo(cwd, options) {
1924
1924
  try {
1925
- const fs22 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1926
- const path23 = await Promise.resolve().then(() => __importStar(require("path")));
1925
+ const fs23 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1926
+ const path24 = await Promise.resolve().then(() => __importStar(require("path")));
1927
1927
  let currentPath = cwd;
1928
1928
  let projectPath = cwd;
1929
1929
  let bareRepoPath;
1930
1930
  for (let i = 0; i < 10; i++) {
1931
- const bareDir = path23.join(currentPath, ".bare");
1932
- const episodaDir = path23.join(currentPath, ".episoda");
1931
+ const bareDir = path24.join(currentPath, ".bare");
1932
+ const episodaDir = path24.join(currentPath, ".episoda");
1933
1933
  try {
1934
- await fs22.access(bareDir);
1935
- await fs22.access(episodaDir);
1934
+ await fs23.access(bareDir);
1935
+ await fs23.access(episodaDir);
1936
1936
  projectPath = currentPath;
1937
1937
  bareRepoPath = bareDir;
1938
1938
  break;
1939
1939
  } catch {
1940
- const parentPath = path23.dirname(currentPath);
1940
+ const parentPath = path24.dirname(currentPath);
1941
1941
  if (parentPath === currentPath) {
1942
1942
  break;
1943
1943
  }
@@ -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
  });
@@ -2572,31 +2590,31 @@ var require_auth = __commonJS({
2572
2590
  exports2.loadConfig = loadConfig8;
2573
2591
  exports2.saveConfig = saveConfig2;
2574
2592
  exports2.validateToken = validateToken;
2575
- var fs22 = __importStar(require("fs"));
2576
- var path23 = __importStar(require("path"));
2593
+ var fs23 = __importStar(require("fs"));
2594
+ var path24 = __importStar(require("path"));
2577
2595
  var os9 = __importStar(require("os"));
2578
2596
  var child_process_1 = require("child_process");
2579
2597
  var DEFAULT_CONFIG_FILE = "config.json";
2580
2598
  function getConfigDir8() {
2581
- return process.env.EPISODA_CONFIG_DIR || path23.join(os9.homedir(), ".episoda");
2599
+ return process.env.EPISODA_CONFIG_DIR || path24.join(os9.homedir(), ".episoda");
2582
2600
  }
2583
2601
  function getConfigPath(configPath) {
2584
2602
  if (configPath) {
2585
2603
  return configPath;
2586
2604
  }
2587
- return path23.join(getConfigDir8(), DEFAULT_CONFIG_FILE);
2605
+ return path24.join(getConfigDir8(), DEFAULT_CONFIG_FILE);
2588
2606
  }
2589
2607
  function ensureConfigDir(configPath) {
2590
- const dir = path23.dirname(configPath);
2591
- const isNew = !fs22.existsSync(dir);
2608
+ const dir = path24.dirname(configPath);
2609
+ const isNew = !fs23.existsSync(dir);
2592
2610
  if (isNew) {
2593
- fs22.mkdirSync(dir, { recursive: true, mode: 448 });
2611
+ fs23.mkdirSync(dir, { recursive: true, mode: 448 });
2594
2612
  }
2595
2613
  if (process.platform === "darwin") {
2596
- const nosyncPath = path23.join(dir, ".nosync");
2597
- if (isNew || !fs22.existsSync(nosyncPath)) {
2614
+ const nosyncPath = path24.join(dir, ".nosync");
2615
+ if (isNew || !fs23.existsSync(nosyncPath)) {
2598
2616
  try {
2599
- fs22.writeFileSync(nosyncPath, "", { mode: 384 });
2617
+ fs23.writeFileSync(nosyncPath, "", { mode: 384 });
2600
2618
  (0, child_process_1.execSync)(`xattr -w com.apple.fileprovider.ignore 1 "${dir}"`, {
2601
2619
  stdio: "ignore",
2602
2620
  timeout: 5e3
@@ -2608,9 +2626,9 @@ var require_auth = __commonJS({
2608
2626
  }
2609
2627
  async function loadConfig8(configPath) {
2610
2628
  const fullPath = getConfigPath(configPath);
2611
- if (fs22.existsSync(fullPath)) {
2629
+ if (fs23.existsSync(fullPath)) {
2612
2630
  try {
2613
- const content = fs22.readFileSync(fullPath, "utf8");
2631
+ const content = fs23.readFileSync(fullPath, "utf8");
2614
2632
  const config = JSON.parse(content);
2615
2633
  return config;
2616
2634
  } catch (error) {
@@ -2620,9 +2638,9 @@ var require_auth = __commonJS({
2620
2638
  if (process.env.EPISODA_MODE === "cloud" && process.env.EPISODA_WORKSPACE) {
2621
2639
  const homeDir = process.env.HOME || require("os").homedir();
2622
2640
  const workspaceConfigPath = require("path").join(homeDir, "episoda", process.env.EPISODA_WORKSPACE, ".episoda", "config.json");
2623
- if (fs22.existsSync(workspaceConfigPath)) {
2641
+ if (fs23.existsSync(workspaceConfigPath)) {
2624
2642
  try {
2625
- const content = fs22.readFileSync(workspaceConfigPath, "utf8");
2643
+ const content = fs23.readFileSync(workspaceConfigPath, "utf8");
2626
2644
  const workspaceConfig = JSON.parse(content);
2627
2645
  return {
2628
2646
  access_token: process.env.EPISODA_ACCESS_TOKEN || workspaceConfig.accessToken,
@@ -2669,7 +2687,7 @@ var require_auth = __commonJS({
2669
2687
  ensureConfigDir(fullPath);
2670
2688
  try {
2671
2689
  const content = JSON.stringify(config, null, 2);
2672
- fs22.writeFileSync(fullPath, content, { mode: 384 });
2690
+ fs23.writeFileSync(fullPath, content, { mode: 384 });
2673
2691
  } catch (error) {
2674
2692
  throw new Error(`Failed to save config: ${error instanceof Error ? error.message : String(error)}`);
2675
2693
  }
@@ -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.104",
2807
+ version: "0.2.106",
2790
2808
  description: "CLI tool for Episoda local development workflow orchestration",
2791
2809
  main: "dist/index.js",
2792
2810
  types: "dist/index.d.ts",
@@ -4763,7 +4781,13 @@ var WorktreeManager = class _WorktreeManager {
4763
4781
  }
4764
4782
  };
4765
4783
  function getEpisodaRoot() {
4766
- return process.env.EPISODA_ROOT || path7.join(require("os").homedir(), "episoda");
4784
+ if (process.env.EPISODA_ROOT) {
4785
+ return process.env.EPISODA_ROOT;
4786
+ }
4787
+ if (process.env.EPISODA_MODE === "cloud") {
4788
+ return process.env.HOME || "/home/episoda";
4789
+ }
4790
+ return path7.join(require("os").homedir(), "episoda");
4767
4791
  }
4768
4792
  function getProjectPath(workspaceSlug, projectSlug) {
4769
4793
  return path7.join(getEpisodaRoot(), workspaceSlug, projectSlug);
@@ -7712,6 +7736,85 @@ async function persistArtifactsBeforeEject(artifactsPath, projectId, projectSlug
7712
7736
  }
7713
7737
  }
7714
7738
 
7739
+ // src/daemon/handlers/project-handlers.ts
7740
+ var path16 = __toESM(require("path"));
7741
+ var fs15 = __toESM(require("fs"));
7742
+ function validateSlug(slug, fieldName) {
7743
+ if (!slug || typeof slug !== "string") {
7744
+ return `${fieldName} is required`;
7745
+ }
7746
+ const trimmed = slug.trim();
7747
+ if (!trimmed) {
7748
+ return `${fieldName} cannot be empty`;
7749
+ }
7750
+ if (trimmed.includes("..") || trimmed.includes("/") || trimmed.includes("\\")) {
7751
+ return `${fieldName} contains invalid characters`;
7752
+ }
7753
+ if (trimmed.includes("\0")) {
7754
+ return `${fieldName} contains invalid characters`;
7755
+ }
7756
+ if (trimmed.startsWith(".")) {
7757
+ return `${fieldName} cannot start with a dot`;
7758
+ }
7759
+ return null;
7760
+ }
7761
+ var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
7762
+ async function handleProjectSetup(params) {
7763
+ const { workspaceSlug, projectSlug, projectId } = params;
7764
+ const workspaceError = validateSlug(workspaceSlug, "workspaceSlug");
7765
+ if (workspaceError) {
7766
+ console.error(`[ProjectSetup] EP1199: Validation failed: ${workspaceError}`);
7767
+ return { success: false, error: workspaceError };
7768
+ }
7769
+ const projectError = validateSlug(projectSlug, "projectSlug");
7770
+ if (projectError) {
7771
+ console.error(`[ProjectSetup] EP1199: Validation failed: ${projectError}`);
7772
+ return { success: false, error: projectError };
7773
+ }
7774
+ if (!UUID_REGEX.test(projectId)) {
7775
+ console.error(`[ProjectSetup] EP1199: Invalid projectId format: ${projectId}`);
7776
+ return { success: false, error: "Invalid projectId format" };
7777
+ }
7778
+ console.log(`[ProjectSetup] EP1199: Setting up project ${workspaceSlug}/${projectSlug}`);
7779
+ try {
7780
+ const projectPath = getProjectPath(workspaceSlug, projectSlug);
7781
+ const artifactsPath = path16.join(projectPath, "artifacts");
7782
+ const configDir = path16.join(projectPath, ".episoda");
7783
+ const configPath = path16.join(configDir, "config.json");
7784
+ await fs15.promises.mkdir(artifactsPath, { recursive: true });
7785
+ await fs15.promises.mkdir(configDir, { recursive: true });
7786
+ let existingConfig = {};
7787
+ try {
7788
+ const existing = await fs15.promises.readFile(configPath, "utf-8");
7789
+ existingConfig = JSON.parse(existing);
7790
+ } catch {
7791
+ }
7792
+ const config = {
7793
+ ...existingConfig,
7794
+ project_id: projectId,
7795
+ workspace_slug: workspaceSlug,
7796
+ project_slug: projectSlug,
7797
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
7798
+ // Only set created_at if not already present
7799
+ created_at: existingConfig.created_at || (/* @__PURE__ */ new Date()).toISOString()
7800
+ };
7801
+ await fs15.promises.writeFile(configPath, JSON.stringify(config, null, 2));
7802
+ console.log(`[ProjectSetup] EP1199: Project setup complete at ${projectPath}`);
7803
+ return {
7804
+ success: true,
7805
+ projectPath,
7806
+ artifactsPath
7807
+ };
7808
+ } catch (error) {
7809
+ const errorMessage = error instanceof Error ? error.message : String(error);
7810
+ console.error(`[ProjectSetup] EP1199: Setup failed:`, errorMessage);
7811
+ return {
7812
+ success: false,
7813
+ error: errorMessage
7814
+ };
7815
+ }
7816
+ }
7817
+
7715
7818
  // src/daemon/handlers/stale-commit-cleanup.ts
7716
7819
  var import_child_process9 = require("child_process");
7717
7820
  var import_util2 = require("util");
@@ -7809,12 +7912,12 @@ async function cleanupStaleCommits(projectPath) {
7809
7912
 
7810
7913
  // src/agent/claude-binary.ts
7811
7914
  var import_child_process10 = require("child_process");
7812
- var path16 = __toESM(require("path"));
7813
- var fs15 = __toESM(require("fs"));
7915
+ var path17 = __toESM(require("path"));
7916
+ var fs16 = __toESM(require("fs"));
7814
7917
  var cachedBinaryPath = null;
7815
7918
  function isValidClaudeBinary(binaryPath) {
7816
7919
  try {
7817
- fs15.accessSync(binaryPath, fs15.constants.X_OK);
7920
+ fs16.accessSync(binaryPath, fs16.constants.X_OK);
7818
7921
  const version = (0, import_child_process10.execSync)(`"${binaryPath}" --version`, {
7819
7922
  encoding: "utf-8",
7820
7923
  timeout: 5e3,
@@ -7847,14 +7950,14 @@ async function ensureClaudeBinary() {
7847
7950
  }
7848
7951
  const bundledPaths = [
7849
7952
  // In production: node_modules/.bin/claude
7850
- path16.join(__dirname, "..", "..", "node_modules", ".bin", "claude"),
7953
+ path17.join(__dirname, "..", "..", "node_modules", ".bin", "claude"),
7851
7954
  // In monorepo development: packages/episoda/node_modules/.bin/claude
7852
- path16.join(__dirname, "..", "..", "..", "..", "node_modules", ".bin", "claude"),
7955
+ path17.join(__dirname, "..", "..", "..", "..", "node_modules", ".bin", "claude"),
7853
7956
  // Root monorepo node_modules
7854
- path16.join(__dirname, "..", "..", "..", "..", "..", "node_modules", ".bin", "claude")
7957
+ path17.join(__dirname, "..", "..", "..", "..", "..", "node_modules", ".bin", "claude")
7855
7958
  ];
7856
7959
  for (const bundledPath of bundledPaths) {
7857
- if (fs15.existsSync(bundledPath) && isValidClaudeBinary(bundledPath)) {
7960
+ if (fs16.existsSync(bundledPath) && isValidClaudeBinary(bundledPath)) {
7858
7961
  cachedBinaryPath = bundledPath;
7859
7962
  return cachedBinaryPath;
7860
7963
  }
@@ -7880,12 +7983,12 @@ async function ensureClaudeBinary() {
7880
7983
 
7881
7984
  // src/agent/codex-binary.ts
7882
7985
  var import_child_process11 = require("child_process");
7883
- var path17 = __toESM(require("path"));
7884
- var fs16 = __toESM(require("fs"));
7986
+ var path18 = __toESM(require("path"));
7987
+ var fs17 = __toESM(require("fs"));
7885
7988
  var cachedBinaryPath2 = null;
7886
7989
  function isValidCodexBinary(binaryPath) {
7887
7990
  try {
7888
- fs16.accessSync(binaryPath, fs16.constants.X_OK);
7991
+ fs17.accessSync(binaryPath, fs17.constants.X_OK);
7889
7992
  const version = (0, import_child_process11.execSync)(`"${binaryPath}" --version`, {
7890
7993
  encoding: "utf-8",
7891
7994
  timeout: 5e3,
@@ -7918,14 +8021,14 @@ async function ensureCodexBinary() {
7918
8021
  }
7919
8022
  const bundledPaths = [
7920
8023
  // In production: node_modules/.bin/codex
7921
- path17.join(__dirname, "..", "..", "node_modules", ".bin", "codex"),
8024
+ path18.join(__dirname, "..", "..", "node_modules", ".bin", "codex"),
7922
8025
  // In monorepo development: packages/episoda/node_modules/.bin/codex
7923
- path17.join(__dirname, "..", "..", "..", "..", "node_modules", ".bin", "codex"),
8026
+ path18.join(__dirname, "..", "..", "..", "..", "node_modules", ".bin", "codex"),
7924
8027
  // Root monorepo node_modules
7925
- path17.join(__dirname, "..", "..", "..", "..", "..", "node_modules", ".bin", "codex")
8028
+ path18.join(__dirname, "..", "..", "..", "..", "..", "node_modules", ".bin", "codex")
7926
8029
  ];
7927
8030
  for (const bundledPath of bundledPaths) {
7928
- if (fs16.existsSync(bundledPath) && isValidCodexBinary(bundledPath)) {
8031
+ if (fs17.existsSync(bundledPath) && isValidCodexBinary(bundledPath)) {
7929
8032
  cachedBinaryPath2 = bundledPath;
7930
8033
  return cachedBinaryPath2;
7931
8034
  }
@@ -7992,8 +8095,8 @@ function generateCodexConfig(credentials, projectPath) {
7992
8095
 
7993
8096
  // src/agent/agent-manager.ts
7994
8097
  var import_child_process12 = require("child_process");
7995
- var path18 = __toESM(require("path"));
7996
- var fs17 = __toESM(require("fs"));
8098
+ var path19 = __toESM(require("path"));
8099
+ var fs18 = __toESM(require("fs"));
7997
8100
  var os6 = __toESM(require("os"));
7998
8101
 
7999
8102
  // src/agent/claude-config.ts
@@ -8316,7 +8419,7 @@ var AgentManager = class {
8316
8419
  this.initialized = false;
8317
8420
  // EP1133: Lock for config file writes to prevent race conditions
8318
8421
  this.configWriteLock = Promise.resolve();
8319
- this.pidDir = path18.join(os6.homedir(), ".episoda", "agent-pids");
8422
+ this.pidDir = path19.join(os6.homedir(), ".episoda", "agent-pids");
8320
8423
  }
8321
8424
  /**
8322
8425
  * EP1133: Acquire lock for config file writes
@@ -8345,8 +8448,8 @@ var AgentManager = class {
8345
8448
  return;
8346
8449
  }
8347
8450
  console.log("[AgentManager] Initializing...");
8348
- if (!fs17.existsSync(this.pidDir)) {
8349
- fs17.mkdirSync(this.pidDir, { recursive: true });
8451
+ if (!fs18.existsSync(this.pidDir)) {
8452
+ fs18.mkdirSync(this.pidDir, { recursive: true });
8350
8453
  }
8351
8454
  await this.cleanupOrphanedProcesses();
8352
8455
  try {
@@ -8593,9 +8696,9 @@ If changes are needed, explain what needs to be done.`;
8593
8696
  const useApiKey = !useOAuth && !!session.credentials.apiKey;
8594
8697
  if (provider === "codex") {
8595
8698
  await this.withConfigLock(async () => {
8596
- const codexDir = path18.join(os6.homedir(), ".codex");
8597
- if (!fs17.existsSync(codexDir)) {
8598
- fs17.mkdirSync(codexDir, { recursive: true });
8699
+ const codexDir = path19.join(os6.homedir(), ".codex");
8700
+ if (!fs18.existsSync(codexDir)) {
8701
+ fs18.mkdirSync(codexDir, { recursive: true });
8599
8702
  }
8600
8703
  if (useOAuth) {
8601
8704
  const codexConfig = generateCodexConfig({
@@ -8605,21 +8708,21 @@ If changes are needed, explain what needs to be done.`;
8605
8708
  accountId: session.credentials.accountId,
8606
8709
  expiresAt: session.credentials.expiresAt
8607
8710
  }, session.projectPath);
8608
- const authJsonPath = path18.join(codexDir, "auth.json");
8609
- fs17.writeFileSync(authJsonPath, codexConfig["auth.json"], { mode: 384 });
8711
+ const authJsonPath = path19.join(codexDir, "auth.json");
8712
+ fs18.writeFileSync(authJsonPath, codexConfig["auth.json"], { mode: 384 });
8610
8713
  console.log("[AgentManager] EP1133: Wrote Codex auth.json to ~/.codex/auth.json");
8611
8714
  if (codexConfig["config.toml"]) {
8612
- const configTomlPath = path18.join(codexDir, "config.toml");
8715
+ const configTomlPath = path19.join(codexDir, "config.toml");
8613
8716
  let existingConfig = "";
8614
8717
  try {
8615
- existingConfig = fs17.readFileSync(configTomlPath, "utf-8");
8718
+ existingConfig = fs18.readFileSync(configTomlPath, "utf-8");
8616
8719
  } catch {
8617
8720
  }
8618
8721
  const escapedPathForRegex = session.projectPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8619
8722
  const projectKeyPattern = new RegExp(`\\[projects\\."${escapedPathForRegex}"\\]`);
8620
8723
  const projectAlreadyTrusted = projectKeyPattern.test(existingConfig);
8621
8724
  if (!projectAlreadyTrusted) {
8622
- fs17.writeFileSync(configTomlPath, existingConfig + "\n" + codexConfig["config.toml"], { mode: 420 });
8725
+ fs18.writeFileSync(configTomlPath, existingConfig + "\n" + codexConfig["config.toml"], { mode: 420 });
8623
8726
  console.log("[AgentManager] EP1133: Updated Codex config.toml with project trust");
8624
8727
  }
8625
8728
  }
@@ -8629,14 +8732,14 @@ If changes are needed, explain what needs to be done.`;
8629
8732
  });
8630
8733
  } else {
8631
8734
  await this.withConfigLock(async () => {
8632
- const claudeDir = path18.join(os6.homedir(), ".claude");
8633
- const credentialsPath = path18.join(claudeDir, ".credentials.json");
8634
- const statsigDir = path18.join(claudeDir, "statsig");
8635
- if (!fs17.existsSync(claudeDir)) {
8636
- fs17.mkdirSync(claudeDir, { recursive: true });
8735
+ const claudeDir = path19.join(os6.homedir(), ".claude");
8736
+ const credentialsPath = path19.join(claudeDir, ".credentials.json");
8737
+ const statsigDir = path19.join(claudeDir, "statsig");
8738
+ if (!fs18.existsSync(claudeDir)) {
8739
+ fs18.mkdirSync(claudeDir, { recursive: true });
8637
8740
  }
8638
- if (!fs17.existsSync(statsigDir)) {
8639
- fs17.mkdirSync(statsigDir, { recursive: true });
8741
+ if (!fs18.existsSync(statsigDir)) {
8742
+ fs18.mkdirSync(statsigDir, { recursive: true });
8640
8743
  }
8641
8744
  if (useOAuth) {
8642
8745
  const oauthCredentials = {
@@ -8654,7 +8757,7 @@ If changes are needed, explain what needs to be done.`;
8654
8757
  const credentialsContent = JSON.stringify({
8655
8758
  claudeAiOauth: oauthCredentials
8656
8759
  }, null, 2);
8657
- fs17.writeFileSync(credentialsPath, credentialsContent, { mode: 384 });
8760
+ fs18.writeFileSync(credentialsPath, credentialsContent, { mode: 384 });
8658
8761
  console.log("[AgentManager] Wrote OAuth credentials to ~/.claude/.credentials.json");
8659
8762
  try {
8660
8763
  const claudeConfig = generateClaudeConfig({
@@ -8666,11 +8769,11 @@ If changes are needed, explain what needs to be done.`;
8666
8769
  if (!hasEvaluations || !hasStableId) {
8667
8770
  throw new Error(`Invalid statsig config: missing required files`);
8668
8771
  }
8669
- const settingsPath = path18.join(claudeDir, "settings.json");
8670
- fs17.writeFileSync(settingsPath, claudeConfig["settings.json"], { mode: 384 });
8772
+ const settingsPath = path19.join(claudeDir, "settings.json");
8773
+ fs18.writeFileSync(settingsPath, claudeConfig["settings.json"], { mode: 384 });
8671
8774
  for (const [filename, content] of Object.entries(claudeConfig.statsig)) {
8672
- const filePath = path18.join(statsigDir, filename);
8673
- fs17.writeFileSync(filePath, content, { mode: 420 });
8775
+ const filePath = path19.join(statsigDir, filename);
8776
+ fs18.writeFileSync(filePath, content, { mode: 420 });
8674
8777
  }
8675
8778
  if (session.credentials.githubToken) {
8676
8779
  console.log("[AgentManager] EP1146: GitHub MCP server enabled with installation token");
@@ -8940,14 +9043,14 @@ If changes are needed, explain what needs to be done.`;
8940
9043
  */
8941
9044
  async cleanupOrphanedProcesses() {
8942
9045
  let cleaned = 0;
8943
- if (!fs17.existsSync(this.pidDir)) {
9046
+ if (!fs18.existsSync(this.pidDir)) {
8944
9047
  return { cleaned };
8945
9048
  }
8946
- const pidFiles = fs17.readdirSync(this.pidDir).filter((f) => f.endsWith(".pid"));
9049
+ const pidFiles = fs18.readdirSync(this.pidDir).filter((f) => f.endsWith(".pid"));
8947
9050
  for (const pidFile of pidFiles) {
8948
- const pidPath = path18.join(this.pidDir, pidFile);
9051
+ const pidPath = path19.join(this.pidDir, pidFile);
8949
9052
  try {
8950
- const pidStr = fs17.readFileSync(pidPath, "utf-8").trim();
9053
+ const pidStr = fs18.readFileSync(pidPath, "utf-8").trim();
8951
9054
  const pid = parseInt(pidStr, 10);
8952
9055
  if (!isNaN(pid)) {
8953
9056
  try {
@@ -8958,7 +9061,7 @@ If changes are needed, explain what needs to be done.`;
8958
9061
  } catch {
8959
9062
  }
8960
9063
  }
8961
- fs17.unlinkSync(pidPath);
9064
+ fs18.unlinkSync(pidPath);
8962
9065
  } catch (error) {
8963
9066
  console.warn(`[AgentManager] Error cleaning PID file ${pidFile}:`, error);
8964
9067
  }
@@ -8972,17 +9075,17 @@ If changes are needed, explain what needs to be done.`;
8972
9075
  * Write PID file for session tracking
8973
9076
  */
8974
9077
  writePidFile(sessionId, pid) {
8975
- const pidPath = path18.join(this.pidDir, `${sessionId}.pid`);
8976
- fs17.writeFileSync(pidPath, pid.toString());
9078
+ const pidPath = path19.join(this.pidDir, `${sessionId}.pid`);
9079
+ fs18.writeFileSync(pidPath, pid.toString());
8977
9080
  }
8978
9081
  /**
8979
9082
  * Remove PID file for session
8980
9083
  */
8981
9084
  removePidFile(sessionId) {
8982
- const pidPath = path18.join(this.pidDir, `${sessionId}.pid`);
9085
+ const pidPath = path19.join(this.pidDir, `${sessionId}.pid`);
8983
9086
  try {
8984
- if (fs17.existsSync(pidPath)) {
8985
- fs17.unlinkSync(pidPath);
9087
+ if (fs18.existsSync(pidPath)) {
9088
+ fs18.unlinkSync(pidPath);
8986
9089
  }
8987
9090
  } catch {
8988
9091
  }
@@ -8992,8 +9095,8 @@ If changes are needed, explain what needs to be done.`;
8992
9095
  // src/utils/dev-server.ts
8993
9096
  var import_child_process13 = require("child_process");
8994
9097
  var import_core11 = __toESM(require_dist());
8995
- var fs18 = __toESM(require("fs"));
8996
- var path19 = __toESM(require("path"));
9098
+ var fs19 = __toESM(require("fs"));
9099
+ var path20 = __toESM(require("path"));
8997
9100
  var MAX_RESTART_ATTEMPTS = 5;
8998
9101
  var INITIAL_RESTART_DELAY_MS = 2e3;
8999
9102
  var MAX_RESTART_DELAY_MS = 3e4;
@@ -9001,26 +9104,26 @@ var MAX_LOG_SIZE_BYTES = 5 * 1024 * 1024;
9001
9104
  var NODE_MEMORY_LIMIT_MB = 2048;
9002
9105
  var activeServers = /* @__PURE__ */ new Map();
9003
9106
  function getLogsDir() {
9004
- const logsDir = path19.join((0, import_core11.getConfigDir)(), "logs");
9005
- if (!fs18.existsSync(logsDir)) {
9006
- fs18.mkdirSync(logsDir, { recursive: true });
9107
+ const logsDir = path20.join((0, import_core11.getConfigDir)(), "logs");
9108
+ if (!fs19.existsSync(logsDir)) {
9109
+ fs19.mkdirSync(logsDir, { recursive: true });
9007
9110
  }
9008
9111
  return logsDir;
9009
9112
  }
9010
9113
  function getLogFilePath(moduleUid) {
9011
- return path19.join(getLogsDir(), `dev-${moduleUid}.log`);
9114
+ return path20.join(getLogsDir(), `dev-${moduleUid}.log`);
9012
9115
  }
9013
9116
  function rotateLogIfNeeded(logPath) {
9014
9117
  try {
9015
- if (fs18.existsSync(logPath)) {
9016
- const stats = fs18.statSync(logPath);
9118
+ if (fs19.existsSync(logPath)) {
9119
+ const stats = fs19.statSync(logPath);
9017
9120
  if (stats.size > MAX_LOG_SIZE_BYTES) {
9018
9121
  const backupPath = `${logPath}.1`;
9019
- if (fs18.existsSync(backupPath)) {
9020
- fs18.unlinkSync(backupPath);
9122
+ if (fs19.existsSync(backupPath)) {
9123
+ fs19.unlinkSync(backupPath);
9021
9124
  }
9022
- fs18.renameSync(logPath, backupPath);
9023
- console.log(`[DevServer] EP932: Rotated log file for ${path19.basename(logPath)}`);
9125
+ fs19.renameSync(logPath, backupPath);
9126
+ console.log(`[DevServer] EP932: Rotated log file for ${path20.basename(logPath)}`);
9024
9127
  }
9025
9128
  }
9026
9129
  } catch (error) {
@@ -9033,7 +9136,7 @@ function writeToLog(logPath, line, isError = false) {
9033
9136
  const prefix = isError ? "ERR" : "OUT";
9034
9137
  const logLine = `[${timestamp}] [${prefix}] ${line}
9035
9138
  `;
9036
- fs18.appendFileSync(logPath, logLine);
9139
+ fs19.appendFileSync(logPath, logLine);
9037
9140
  } catch {
9038
9141
  }
9039
9142
  }
@@ -9212,8 +9315,8 @@ async function startDevServer(projectPath, port = 3e3, moduleUid = "default", op
9212
9315
  });
9213
9316
  injectedEnvVars = result.envVars;
9214
9317
  console.log(`[DevServer] EP998: Loaded ${Object.keys(injectedEnvVars).length} env vars (from ${result.fromCache ? "cache" : "server"})`);
9215
- const envFilePath = path19.join(projectPath, ".env");
9216
- if (!fs18.existsSync(envFilePath) && Object.keys(injectedEnvVars).length > 0) {
9318
+ const envFilePath = path20.join(projectPath, ".env");
9319
+ if (!fs19.existsSync(envFilePath) && Object.keys(injectedEnvVars).length > 0) {
9217
9320
  console.log(`[DevServer] EP1004: .env file missing, writing ${Object.keys(injectedEnvVars).length} vars to ${envFilePath}`);
9218
9321
  writeEnvFile(projectPath, injectedEnvVars);
9219
9322
  }
@@ -9319,19 +9422,19 @@ function getDevServerStatus() {
9319
9422
  }
9320
9423
 
9321
9424
  // src/utils/worktree.ts
9322
- var path20 = __toESM(require("path"));
9323
- var fs19 = __toESM(require("fs"));
9425
+ var path21 = __toESM(require("path"));
9426
+ var fs20 = __toESM(require("fs"));
9324
9427
  var os7 = __toESM(require("os"));
9325
9428
  var import_core12 = __toESM(require_dist());
9326
9429
  function getEpisodaRoot2() {
9327
- return process.env.EPISODA_ROOT || path20.join(os7.homedir(), "episoda");
9430
+ return process.env.EPISODA_ROOT || path21.join(os7.homedir(), "episoda");
9328
9431
  }
9329
9432
  function getWorktreeInfo(moduleUid, workspaceSlug, projectSlug) {
9330
9433
  const root = getEpisodaRoot2();
9331
- const worktreePath = path20.join(root, workspaceSlug, projectSlug, moduleUid);
9434
+ const worktreePath = path21.join(root, workspaceSlug, projectSlug, moduleUid);
9332
9435
  return {
9333
9436
  path: worktreePath,
9334
- exists: fs19.existsSync(worktreePath),
9437
+ exists: fs20.existsSync(worktreePath),
9335
9438
  moduleUid
9336
9439
  };
9337
9440
  }
@@ -9345,15 +9448,15 @@ async function getWorktreeInfoForModule(moduleUid) {
9345
9448
  return null;
9346
9449
  }
9347
9450
  const root = getEpisodaRoot2();
9348
- const workspaceRoot = path20.join(root, config.workspace_slug);
9451
+ const workspaceRoot = path21.join(root, config.workspace_slug);
9349
9452
  try {
9350
- const entries = fs19.readdirSync(workspaceRoot, { withFileTypes: true });
9453
+ const entries = fs20.readdirSync(workspaceRoot, { withFileTypes: true });
9351
9454
  for (const entry of entries) {
9352
9455
  if (!entry.isDirectory()) {
9353
9456
  continue;
9354
9457
  }
9355
- const worktreePath = path20.join(workspaceRoot, entry.name, moduleUid);
9356
- if (fs19.existsSync(worktreePath)) {
9458
+ const worktreePath = path21.join(workspaceRoot, entry.name, moduleUid);
9459
+ if (fs20.existsSync(worktreePath)) {
9357
9460
  return {
9358
9461
  path: worktreePath,
9359
9462
  exists: true,
@@ -9370,61 +9473,61 @@ async function getWorktreeInfoForModule(moduleUid) {
9370
9473
  }
9371
9474
 
9372
9475
  // src/framework-detector.ts
9373
- var fs20 = __toESM(require("fs"));
9374
- var path21 = __toESM(require("path"));
9476
+ var fs21 = __toESM(require("fs"));
9477
+ var path22 = __toESM(require("path"));
9375
9478
  function getInstallCommand(cwd) {
9376
- if (fs20.existsSync(path21.join(cwd, "bun.lockb"))) {
9479
+ if (fs21.existsSync(path22.join(cwd, "bun.lockb"))) {
9377
9480
  return {
9378
9481
  command: ["bun", "install"],
9379
9482
  description: "Installing dependencies with bun",
9380
9483
  detectedFrom: "bun.lockb"
9381
9484
  };
9382
9485
  }
9383
- if (fs20.existsSync(path21.join(cwd, "pnpm-lock.yaml"))) {
9486
+ if (fs21.existsSync(path22.join(cwd, "pnpm-lock.yaml"))) {
9384
9487
  return {
9385
9488
  command: ["pnpm", "install"],
9386
9489
  description: "Installing dependencies with pnpm",
9387
9490
  detectedFrom: "pnpm-lock.yaml"
9388
9491
  };
9389
9492
  }
9390
- if (fs20.existsSync(path21.join(cwd, "yarn.lock"))) {
9493
+ if (fs21.existsSync(path22.join(cwd, "yarn.lock"))) {
9391
9494
  return {
9392
9495
  command: ["yarn", "install"],
9393
9496
  description: "Installing dependencies with yarn",
9394
9497
  detectedFrom: "yarn.lock"
9395
9498
  };
9396
9499
  }
9397
- if (fs20.existsSync(path21.join(cwd, "package-lock.json"))) {
9500
+ if (fs21.existsSync(path22.join(cwd, "package-lock.json"))) {
9398
9501
  return {
9399
9502
  command: ["npm", "ci"],
9400
9503
  description: "Installing dependencies with npm ci",
9401
9504
  detectedFrom: "package-lock.json"
9402
9505
  };
9403
9506
  }
9404
- if (fs20.existsSync(path21.join(cwd, "package.json"))) {
9507
+ if (fs21.existsSync(path22.join(cwd, "package.json"))) {
9405
9508
  return {
9406
9509
  command: ["npm", "install"],
9407
9510
  description: "Installing dependencies with npm",
9408
9511
  detectedFrom: "package.json"
9409
9512
  };
9410
9513
  }
9411
- if (fs20.existsSync(path21.join(cwd, "Pipfile.lock")) || fs20.existsSync(path21.join(cwd, "Pipfile"))) {
9514
+ if (fs21.existsSync(path22.join(cwd, "Pipfile.lock")) || fs21.existsSync(path22.join(cwd, "Pipfile"))) {
9412
9515
  return {
9413
9516
  command: ["pipenv", "install"],
9414
9517
  description: "Installing dependencies with pipenv",
9415
- detectedFrom: fs20.existsSync(path21.join(cwd, "Pipfile.lock")) ? "Pipfile.lock" : "Pipfile"
9518
+ detectedFrom: fs21.existsSync(path22.join(cwd, "Pipfile.lock")) ? "Pipfile.lock" : "Pipfile"
9416
9519
  };
9417
9520
  }
9418
- if (fs20.existsSync(path21.join(cwd, "poetry.lock"))) {
9521
+ if (fs21.existsSync(path22.join(cwd, "poetry.lock"))) {
9419
9522
  return {
9420
9523
  command: ["poetry", "install"],
9421
9524
  description: "Installing dependencies with poetry",
9422
9525
  detectedFrom: "poetry.lock"
9423
9526
  };
9424
9527
  }
9425
- if (fs20.existsSync(path21.join(cwd, "pyproject.toml"))) {
9426
- const pyprojectPath = path21.join(cwd, "pyproject.toml");
9427
- const content = fs20.readFileSync(pyprojectPath, "utf-8");
9528
+ if (fs21.existsSync(path22.join(cwd, "pyproject.toml"))) {
9529
+ const pyprojectPath = path22.join(cwd, "pyproject.toml");
9530
+ const content = fs21.readFileSync(pyprojectPath, "utf-8");
9428
9531
  if (content.includes("[tool.poetry]")) {
9429
9532
  return {
9430
9533
  command: ["poetry", "install"],
@@ -9433,41 +9536,42 @@ function getInstallCommand(cwd) {
9433
9536
  };
9434
9537
  }
9435
9538
  }
9436
- if (fs20.existsSync(path21.join(cwd, "requirements.txt"))) {
9539
+ if (fs21.existsSync(path22.join(cwd, "requirements.txt"))) {
9437
9540
  return {
9438
9541
  command: ["pip", "install", "-r", "requirements.txt"],
9439
9542
  description: "Installing dependencies with pip",
9440
9543
  detectedFrom: "requirements.txt"
9441
9544
  };
9442
9545
  }
9443
- if (fs20.existsSync(path21.join(cwd, "Gemfile.lock")) || fs20.existsSync(path21.join(cwd, "Gemfile"))) {
9546
+ if (fs21.existsSync(path22.join(cwd, "Gemfile.lock")) || fs21.existsSync(path22.join(cwd, "Gemfile"))) {
9444
9547
  return {
9445
9548
  command: ["bundle", "install"],
9446
9549
  description: "Installing dependencies with bundler",
9447
- detectedFrom: fs20.existsSync(path21.join(cwd, "Gemfile.lock")) ? "Gemfile.lock" : "Gemfile"
9550
+ detectedFrom: fs21.existsSync(path22.join(cwd, "Gemfile.lock")) ? "Gemfile.lock" : "Gemfile"
9448
9551
  };
9449
9552
  }
9450
- if (fs20.existsSync(path21.join(cwd, "go.sum")) || fs20.existsSync(path21.join(cwd, "go.mod"))) {
9553
+ if (fs21.existsSync(path22.join(cwd, "go.sum")) || fs21.existsSync(path22.join(cwd, "go.mod"))) {
9451
9554
  return {
9452
9555
  command: ["go", "mod", "download"],
9453
9556
  description: "Downloading Go modules",
9454
- detectedFrom: fs20.existsSync(path21.join(cwd, "go.sum")) ? "go.sum" : "go.mod"
9557
+ detectedFrom: fs21.existsSync(path22.join(cwd, "go.sum")) ? "go.sum" : "go.mod"
9455
9558
  };
9456
9559
  }
9457
- if (fs20.existsSync(path21.join(cwd, "Cargo.lock")) || fs20.existsSync(path21.join(cwd, "Cargo.toml"))) {
9560
+ if (fs21.existsSync(path22.join(cwd, "Cargo.lock")) || fs21.existsSync(path22.join(cwd, "Cargo.toml"))) {
9458
9561
  return {
9459
9562
  command: ["cargo", "build"],
9460
9563
  description: "Building Rust project (downloads dependencies)",
9461
- detectedFrom: fs20.existsSync(path21.join(cwd, "Cargo.lock")) ? "Cargo.lock" : "Cargo.toml"
9564
+ detectedFrom: fs21.existsSync(path22.join(cwd, "Cargo.lock")) ? "Cargo.lock" : "Cargo.toml"
9462
9565
  };
9463
9566
  }
9464
9567
  return null;
9465
9568
  }
9466
9569
 
9467
9570
  // src/daemon/daemon-process.ts
9468
- var fs21 = __toESM(require("fs"));
9571
+ var fs22 = __toESM(require("fs"));
9572
+ var http2 = __toESM(require("http"));
9469
9573
  var os8 = __toESM(require("os"));
9470
- var path22 = __toESM(require("path"));
9574
+ var path23 = __toESM(require("path"));
9471
9575
  var packageJson = require_package();
9472
9576
  async function ensureValidToken(config, bufferMs = 5 * 60 * 1e3) {
9473
9577
  const now = Date.now();
@@ -9600,6 +9704,8 @@ var Daemon = class _Daemon {
9600
9704
  // 60 seconds
9601
9705
  // EP1190: Worktree cleanup runs every N health checks (5 * 60s = 5 minutes)
9602
9706
  this.healthCheckCounter = 0;
9707
+ // EP1210-7: Health HTTP endpoint for external monitoring
9708
+ this.healthServer = null;
9603
9709
  this.ipcServer = new IPCServer();
9604
9710
  }
9605
9711
  static {
@@ -9619,6 +9725,9 @@ var Daemon = class _Daemon {
9619
9725
  static {
9620
9726
  this.WORKTREE_CLEANUP_EVERY_N_CHECKS = 5;
9621
9727
  }
9728
+ static {
9729
+ this.HEALTH_PORT = 9999;
9730
+ }
9622
9731
  /**
9623
9732
  * Start the daemon
9624
9733
  */
@@ -9640,6 +9749,7 @@ var Daemon = class _Daemon {
9640
9749
  await this.auditWorktreesOnStartup();
9641
9750
  this.startHealthCheckPolling();
9642
9751
  this.setupShutdownHandlers();
9752
+ this.startHealthEndpoint();
9643
9753
  console.log("[Daemon] Daemon started successfully");
9644
9754
  const modeConfig = getDaemonModeConfig();
9645
9755
  console.log("[Daemon] EP1115: Mode config:", {
@@ -9669,6 +9779,55 @@ var Daemon = class _Daemon {
9669
9779
  }
9670
9780
  }
9671
9781
  // EP738: Removed startHttpServer - device info now flows through WebSocket broadcast + database
9782
+ /**
9783
+ * EP1210-7: Start health HTTP endpoint for external monitoring
9784
+ *
9785
+ * Provides a simple HTTP endpoint that external systems can use to check daemon status.
9786
+ * Returns 200 when daemon has at least one live connection, 503 when disconnected.
9787
+ *
9788
+ * Only binds to localhost (127.0.0.1) for security.
9789
+ */
9790
+ startHealthEndpoint() {
9791
+ try {
9792
+ this.healthServer = http2.createServer((req, res) => {
9793
+ if (req.url === "/health" || req.url === "/") {
9794
+ const isConnected = this.liveConnections.size > 0;
9795
+ const projects = Array.from(this.connections.entries()).map(([path24, conn]) => ({
9796
+ path: path24,
9797
+ connected: this.liveConnections.has(path24)
9798
+ }));
9799
+ const status = {
9800
+ status: isConnected ? "healthy" : "degraded",
9801
+ connected: isConnected,
9802
+ machineId: this.machineId,
9803
+ uptime: process.uptime(),
9804
+ liveConnections: this.liveConnections.size,
9805
+ totalConnections: this.connections.size,
9806
+ projects
9807
+ };
9808
+ res.writeHead(isConnected ? 200 : 503, { "Content-Type": "application/json" });
9809
+ res.end(JSON.stringify(status));
9810
+ } else {
9811
+ res.writeHead(404);
9812
+ res.end("Not found");
9813
+ }
9814
+ });
9815
+ this.healthServer.listen(_Daemon.HEALTH_PORT, "127.0.0.1", () => {
9816
+ console.log(`[Daemon] EP1210-7: Health endpoint listening on http://127.0.0.1:${_Daemon.HEALTH_PORT}/health`);
9817
+ });
9818
+ this.healthServer.on("error", (err) => {
9819
+ if (err.code === "EADDRINUSE") {
9820
+ console.warn(`[Daemon] EP1210-7: Health port ${_Daemon.HEALTH_PORT} already in use, skipping health endpoint`);
9821
+ } else {
9822
+ console.warn("[Daemon] EP1210-7: Health endpoint failed to start:", err.message);
9823
+ }
9824
+ this.healthServer = null;
9825
+ });
9826
+ } catch (err) {
9827
+ console.warn("[Daemon] EP1210-7: Failed to create health server:", err.message);
9828
+ this.healthServer = null;
9829
+ }
9830
+ }
9672
9831
  /**
9673
9832
  * Register IPC command handlers
9674
9833
  */
@@ -9865,9 +10024,12 @@ var Daemon = class _Daemon {
9865
10024
  this.ipcServer.on("worktree-list", async (params) => {
9866
10025
  return handleWorktreeList(params.workspaceSlug, params.projectSlug);
9867
10026
  });
9868
- this.ipcServer.on("project-eject", async (params) => {
10027
+ this.ipcServer.on("project:eject", async (params) => {
9869
10028
  return handleProjectEject(params);
9870
10029
  });
10030
+ this.ipcServer.on("project:setup", async (params) => {
10031
+ return handleProjectSetup(params);
10032
+ });
9871
10033
  }
9872
10034
  /**
9873
10035
  * Restore WebSocket connections for tracked projects
@@ -10000,7 +10162,7 @@ var Daemon = class _Daemon {
10000
10162
  client.updateActivity();
10001
10163
  try {
10002
10164
  const gitCmd = message.command;
10003
- const bareRepoPath = path22.join(projectPath, ".bare");
10165
+ const bareRepoPath = path23.join(projectPath, ".bare");
10004
10166
  const cwd = gitCmd.worktreePath || bareRepoPath;
10005
10167
  if (gitCmd.worktreePath) {
10006
10168
  console.log(`[Daemon] Routing command to worktree: ${gitCmd.worktreePath}`);
@@ -10114,9 +10276,14 @@ var Daemon = class _Daemon {
10114
10276
  );
10115
10277
  break;
10116
10278
  // EP1144: Project ejection for Tier 1 cleanup
10117
- case "eject_project":
10279
+ // EP1199: Renamed from eject_project to project:eject for naming consistency
10280
+ case "project:eject":
10118
10281
  result = await handleProjectEject(cmd);
10119
10282
  break;
10283
+ // EP1199: Project setup for cloud containers
10284
+ case "project:setup":
10285
+ result = await handleProjectSetup(cmd);
10286
+ break;
10120
10287
  default:
10121
10288
  result = {
10122
10289
  success: false,
@@ -10580,8 +10747,8 @@ var Daemon = class _Daemon {
10580
10747
  let daemonPid;
10581
10748
  try {
10582
10749
  const pidPath = getPidFilePath();
10583
- if (fs21.existsSync(pidPath)) {
10584
- const pidStr = fs21.readFileSync(pidPath, "utf-8").trim();
10750
+ if (fs22.existsSync(pidPath)) {
10751
+ const pidStr = fs22.readFileSync(pidPath, "utf-8").trim();
10585
10752
  daemonPid = parseInt(pidStr, 10);
10586
10753
  }
10587
10754
  } catch (pidError) {
@@ -10662,28 +10829,28 @@ var Daemon = class _Daemon {
10662
10829
  * - workDir: The directory to run git commands in (cwd)
10663
10830
  */
10664
10831
  getGitDirs(projectPath) {
10665
- const bareDir = path22.join(projectPath, ".bare");
10666
- const gitPath = path22.join(projectPath, ".git");
10667
- if (fs21.existsSync(bareDir) && fs21.statSync(bareDir).isDirectory()) {
10832
+ const bareDir = path23.join(projectPath, ".bare");
10833
+ const gitPath = path23.join(projectPath, ".git");
10834
+ if (fs22.existsSync(bareDir) && fs22.statSync(bareDir).isDirectory()) {
10668
10835
  return { gitDir: bareDir, workDir: projectPath };
10669
10836
  }
10670
- if (fs21.existsSync(gitPath) && fs21.statSync(gitPath).isDirectory()) {
10837
+ if (fs22.existsSync(gitPath) && fs22.statSync(gitPath).isDirectory()) {
10671
10838
  return { gitDir: null, workDir: projectPath };
10672
10839
  }
10673
- if (fs21.existsSync(gitPath) && fs21.statSync(gitPath).isFile()) {
10840
+ if (fs22.existsSync(gitPath) && fs22.statSync(gitPath).isFile()) {
10674
10841
  return { gitDir: null, workDir: projectPath };
10675
10842
  }
10676
- const entries = fs21.readdirSync(projectPath, { withFileTypes: true });
10843
+ const entries = fs22.readdirSync(projectPath, { withFileTypes: true });
10677
10844
  for (const entry of entries) {
10678
10845
  if (entry.isDirectory() && entry.name.startsWith("EP")) {
10679
- const worktreePath = path22.join(projectPath, entry.name);
10680
- const worktreeGit = path22.join(worktreePath, ".git");
10681
- if (fs21.existsSync(worktreeGit)) {
10846
+ const worktreePath = path23.join(projectPath, entry.name);
10847
+ const worktreeGit = path23.join(worktreePath, ".git");
10848
+ if (fs22.existsSync(worktreeGit)) {
10682
10849
  return { gitDir: null, workDir: worktreePath };
10683
10850
  }
10684
10851
  }
10685
10852
  }
10686
- if (fs21.existsSync(bareDir)) {
10853
+ if (fs22.existsSync(bareDir)) {
10687
10854
  return { gitDir: bareDir, workDir: projectPath };
10688
10855
  }
10689
10856
  return { gitDir: null, workDir: projectPath };
@@ -10747,24 +10914,24 @@ var Daemon = class _Daemon {
10747
10914
  async installGitHooks(projectPath) {
10748
10915
  const hooks = ["post-checkout", "pre-commit", "post-commit"];
10749
10916
  let hooksDir;
10750
- const bareHooksDir = path22.join(projectPath, ".bare", "hooks");
10751
- const gitHooksDir = path22.join(projectPath, ".git", "hooks");
10752
- if (fs21.existsSync(bareHooksDir)) {
10917
+ const bareHooksDir = path23.join(projectPath, ".bare", "hooks");
10918
+ const gitHooksDir = path23.join(projectPath, ".git", "hooks");
10919
+ if (fs22.existsSync(bareHooksDir)) {
10753
10920
  hooksDir = bareHooksDir;
10754
- } else if (fs21.existsSync(gitHooksDir) && fs21.statSync(path22.join(projectPath, ".git")).isDirectory()) {
10921
+ } else if (fs22.existsSync(gitHooksDir) && fs22.statSync(path23.join(projectPath, ".git")).isDirectory()) {
10755
10922
  hooksDir = gitHooksDir;
10756
10923
  } else {
10757
- const parentBareHooks = path22.join(projectPath, "..", ".bare", "hooks");
10758
- if (fs21.existsSync(parentBareHooks)) {
10924
+ const parentBareHooks = path23.join(projectPath, "..", ".bare", "hooks");
10925
+ if (fs22.existsSync(parentBareHooks)) {
10759
10926
  hooksDir = parentBareHooks;
10760
10927
  } else {
10761
10928
  console.warn(`[Daemon] Hooks directory not found for: ${projectPath}`);
10762
10929
  return;
10763
10930
  }
10764
10931
  }
10765
- if (!fs21.existsSync(hooksDir)) {
10932
+ if (!fs22.existsSync(hooksDir)) {
10766
10933
  try {
10767
- fs21.mkdirSync(hooksDir, { recursive: true });
10934
+ fs22.mkdirSync(hooksDir, { recursive: true });
10768
10935
  } catch (error) {
10769
10936
  console.warn(`[Daemon] Hooks directory not found and could not create: ${hooksDir}`);
10770
10937
  return;
@@ -10772,20 +10939,20 @@ var Daemon = class _Daemon {
10772
10939
  }
10773
10940
  for (const hookName of hooks) {
10774
10941
  try {
10775
- const hookPath = path22.join(hooksDir, hookName);
10776
- const bundledHookPath = path22.join(__dirname, "..", "hooks", hookName);
10777
- if (!fs21.existsSync(bundledHookPath)) {
10942
+ const hookPath = path23.join(hooksDir, hookName);
10943
+ const bundledHookPath = path23.join(__dirname, "..", "hooks", hookName);
10944
+ if (!fs22.existsSync(bundledHookPath)) {
10778
10945
  console.warn(`[Daemon] Bundled hook not found: ${bundledHookPath}`);
10779
10946
  continue;
10780
10947
  }
10781
- const hookContent = fs21.readFileSync(bundledHookPath, "utf-8");
10782
- if (fs21.existsSync(hookPath)) {
10783
- const existingContent = fs21.readFileSync(hookPath, "utf-8");
10948
+ const hookContent = fs22.readFileSync(bundledHookPath, "utf-8");
10949
+ if (fs22.existsSync(hookPath)) {
10950
+ const existingContent = fs22.readFileSync(hookPath, "utf-8");
10784
10951
  if (existingContent === hookContent) {
10785
10952
  continue;
10786
10953
  }
10787
10954
  }
10788
- fs21.writeFileSync(hookPath, hookContent, { mode: 493 });
10955
+ fs22.writeFileSync(hookPath, hookContent, { mode: 493 });
10789
10956
  console.log(`[Daemon] Installed git hook: ${hookName}`);
10790
10957
  } catch (error) {
10791
10958
  console.warn(`[Daemon] Failed to install ${hookName} hook:`, error instanceof Error ? error.message : error);
@@ -11750,6 +11917,10 @@ var Daemon = class _Daemon {
11750
11917
  if (this.shuttingDown) return;
11751
11918
  this.shuttingDown = true;
11752
11919
  console.log("[Daemon] Shutting down...");
11920
+ if (this.healthServer) {
11921
+ this.healthServer.close();
11922
+ this.healthServer = null;
11923
+ }
11753
11924
  this.stopTunnelPolling();
11754
11925
  this.stopHealthCheckPolling();
11755
11926
  for (const [projectPath, connection] of this.connections) {
@@ -11785,8 +11956,8 @@ var Daemon = class _Daemon {
11785
11956
  await this.shutdown();
11786
11957
  try {
11787
11958
  const pidPath = getPidFilePath();
11788
- if (fs21.existsSync(pidPath)) {
11789
- fs21.unlinkSync(pidPath);
11959
+ if (fs22.existsSync(pidPath)) {
11960
+ fs22.unlinkSync(pidPath);
11790
11961
  console.log("[Daemon] PID file cleaned up");
11791
11962
  }
11792
11963
  } catch (error) {