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.
- package/dist/daemon/daemon-process.js +180 -12
- package/dist/daemon/daemon-process.js.map +1 -1
- package/dist/index.js +19 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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.
|
|
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 &&
|
|
8473
|
-
fullMessage = `${
|
|
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 &&
|
|
8502
|
-
args.push("--system-prompt",
|
|
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]
|
|
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) {
|