commandmate 0.2.12 → 0.3.0
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/.env.example +21 -2
- package/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +30 -22
- package/.next/app-path-routes-manifest.json +1 -1
- package/.next/build-manifest.json +7 -7
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/config.json +3 -3
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/1.pack +0 -0
- package/.next/cache/webpack/client-production/2.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +1 -1
- package/.next/react-loadable-manifest.json +2 -2
- package/.next/required-server-files.json +1 -1
- package/.next/routes-manifest.json +1 -1
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/api/app/update-check/route.js +1 -1
- package/.next/server/app/api/auth/login/route.js +1 -0
- package/.next/server/app/api/auth/login/route.js.nft.json +1 -0
- package/.next/server/app/api/auth/logout/route.js +1 -0
- package/.next/server/app/api/auth/logout/route.js.nft.json +1 -0
- package/.next/server/app/api/auth/status/route.js +1 -0
- package/.next/server/app/api/auth/status/route.js.nft.json +1 -0
- package/.next/server/app/api/hooks/claude-done/route.js +1 -1
- package/.next/server/app/api/hooks/claude-done/route.js.nft.json +1 -1
- package/.next/server/app/api/repositories/route.js +2 -2
- package/.next/server/app/api/repositories/route.js.nft.json +1 -1
- package/.next/server/app/api/slash-commands/route.js +1 -1
- package/.next/server/app/api/slash-commands/route.js.nft.json +1 -1
- package/.next/server/app/api/slash-commands.body +1 -1
- package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/auto-yes/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/current-output/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/interrupt/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/interrupt/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/kill-session/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/kill-session/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/logs/[filename]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/prompt-response/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/respond/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/respond/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/send/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/send/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/slash-commands/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/slash-commands/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/start-polling/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/start-polling/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/route.js +1 -1
- package/.next/server/app/api/worktrees/route.js.nft.json +1 -1
- package/.next/server/app/login/page.js +1 -0
- package/.next/server/app/login/page.js.nft.json +1 -0
- package/.next/server/app/login/page_client-reference-manifest.js +1 -0
- package/.next/server/app/page.js +2 -2
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page.js +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page.js.nft.json +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/page.js +3 -3
- package/.next/server/app/worktrees/[id]/page.js.nft.json +1 -1
- package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/terminal/page.js +1 -1
- package/.next/server/app/worktrees/[id]/terminal/page.js.nft.json +1 -1
- package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +13 -9
- package/.next/server/chunks/3013.js +1 -0
- package/.next/server/chunks/3074.js +1 -0
- package/.next/server/chunks/{1287.js → 3294.js} +2 -2
- package/.next/server/chunks/3860.js +1 -1
- package/.next/server/chunks/4893.js +2 -2
- package/.next/server/chunks/539.js +35 -0
- package/.next/server/chunks/5795.js +1 -0
- package/.next/server/chunks/7536.js +1 -1
- package/.next/server/chunks/7566.js +19 -0
- package/.next/server/chunks/8693.js +1 -1
- package/.next/server/edge-runtime-webpack.js +2 -0
- package/.next/server/edge-runtime-webpack.js.map +1 -0
- package/.next/server/functions-config-manifest.json +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-manifest.json +28 -2
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/server/src/middleware.js +14 -0
- package/.next/server/src/middleware.js.map +1 -0
- package/.next/static/chunks/{2626.2125083a1ff3b80a.js → 6163.f672451d4575decf.js} +1 -1
- package/.next/static/chunks/{656.d72f25ce819bd77e.js → 656.5e2de0173f5a06bd.js} +1 -1
- package/.next/static/chunks/8091-925542bdfc843dce.js +1 -0
- package/.next/static/chunks/8528-4d554d3b94d4cf9b.js +1 -0
- package/.next/static/chunks/app/{layout-07755491d5d57242.js → layout-9110f9a5e41c6bf4.js} +1 -1
- package/.next/static/chunks/app/login/page-2d42204ba87cd136.js +1 -0
- package/.next/static/chunks/app/page-238b5a70d8c101e9.js +1 -0
- package/.next/static/chunks/app/worktrees/[id]/page-9418e49bdc1de02c.js +1 -0
- package/.next/static/chunks/main-db79434ee4a6c931.js +1 -0
- package/.next/static/chunks/webpack-3c0ee3ce5b546818.js +1 -0
- package/.next/static/css/b9ea6a4fad17dc32.css +3 -0
- package/.next/trace +5 -5
- package/.next/types/app/api/auth/login/route.ts +343 -0
- package/.next/types/app/api/auth/logout/route.ts +343 -0
- package/.next/types/app/api/auth/status/route.ts +343 -0
- package/.next/types/app/login/page.ts +79 -0
- package/README.md +6 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +2 -0
- package/dist/cli/commands/start.d.ts +2 -0
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +159 -14
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +4 -0
- package/dist/cli/config/security-messages.d.ts +3 -1
- package/dist/cli/config/security-messages.d.ts.map +1 -1
- package/dist/cli/config/security-messages.js +6 -2
- package/dist/cli/index.js +17 -0
- package/dist/cli/types/index.d.ts +17 -0
- package/dist/cli/types/index.d.ts.map +1 -1
- package/dist/cli/utils/daemon.d.ts.map +1 -1
- package/dist/cli/utils/daemon.js +16 -3
- package/dist/config/auth-config.d.ts +43 -0
- package/dist/config/auth-config.d.ts.map +1 -0
- package/dist/config/auth-config.js +112 -0
- package/dist/lib/auth.d.ts +104 -0
- package/dist/lib/auth.d.ts.map +1 -0
- package/dist/lib/auth.js +250 -0
- package/dist/server/server.js +123 -12
- package/dist/server/src/config/auth-config.js +112 -0
- package/dist/server/src/lib/auth.js +250 -0
- package/dist/server/src/lib/auto-yes-manager.js +180 -96
- package/dist/server/src/lib/ip-restriction.js +241 -0
- package/dist/server/src/lib/response-poller.js +92 -39
- package/dist/server/src/lib/ws-server.js +63 -33
- package/dist/server/src/types/slash-commands.js +1 -0
- package/package.json +2 -2
- package/.next/server/chunks/9238.js +0 -35
- package/.next/server/chunks/9367.js +0 -19
- package/.next/static/chunks/5970-2e18108d0cabd8af.js +0 -1
- package/.next/static/chunks/816-af44cb865b0c980e.js +0 -1
- package/.next/static/chunks/app/page-a6593b9640df66a6.js +0 -1
- package/.next/static/chunks/app/worktrees/[id]/page-c99258f57461962c.js +0 -1
- package/.next/static/chunks/main-f00f82f1cf18dd99.js +0 -1
- package/.next/static/chunks/webpack-af8567a485ade35a.js +0 -1
- package/.next/static/css/897ffb669f47c97b.css +0 -3
- /package/.next/static/{ym6mA6Dl9wX62h3AoYO45 → clTo9tuAoPMLcGRuVENfO}/_buildManifest.js +0 -0
- /package/.next/static/{ym6mA6Dl9wX62h3AoYO45 → clTo9tuAoPMLcGRuVENfO}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Token Authentication Core Module
|
|
4
|
+
* Issue #331: Token authentication and HTTPS support
|
|
5
|
+
*
|
|
6
|
+
* CONSTRAINTS:
|
|
7
|
+
* - C001: No Next.js module dependencies (next/headers, next/server, etc.)
|
|
8
|
+
* This module must be compatible with CLI build (tsconfig.cli.json)
|
|
9
|
+
* - S001: Token verification uses crypto.timingSafeEqual() (timing-safe comparison)
|
|
10
|
+
* - S002: AUTH_EXCLUDED_PATHS matching uses === (exact match, no startsWith)
|
|
11
|
+
*/
|
|
12
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.DEFAULT_COOKIE_MAX_AGE_SECONDS = exports.RATE_LIMIT_CONFIG = exports.isValidTokenHash = exports.DEFAULT_EXPIRE_DURATION_MS = exports.computeExpireAt = exports.parseDuration = exports.AUTH_EXCLUDED_PATHS = exports.AUTH_COOKIE_NAME = void 0;
|
|
17
|
+
exports.generateToken = generateToken;
|
|
18
|
+
exports.hashToken = hashToken;
|
|
19
|
+
exports.verifyToken = verifyToken;
|
|
20
|
+
exports.parseCookies = parseCookies;
|
|
21
|
+
exports.isAuthEnabled = isAuthEnabled;
|
|
22
|
+
exports.getTokenMaxAge = getTokenMaxAge;
|
|
23
|
+
exports.isHttpsEnabled = isHttpsEnabled;
|
|
24
|
+
exports.buildAuthCookieOptions = buildAuthCookieOptions;
|
|
25
|
+
exports.createRateLimiter = createRateLimiter;
|
|
26
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
27
|
+
// Import and re-export shared constants and functions from Edge Runtime-compatible config
|
|
28
|
+
const auth_config_1 = require("../config/auth-config");
|
|
29
|
+
Object.defineProperty(exports, "AUTH_COOKIE_NAME", { enumerable: true, get: function () { return auth_config_1.AUTH_COOKIE_NAME; } });
|
|
30
|
+
Object.defineProperty(exports, "AUTH_EXCLUDED_PATHS", { enumerable: true, get: function () { return auth_config_1.AUTH_EXCLUDED_PATHS; } });
|
|
31
|
+
Object.defineProperty(exports, "parseDuration", { enumerable: true, get: function () { return auth_config_1.parseDuration; } });
|
|
32
|
+
Object.defineProperty(exports, "computeExpireAt", { enumerable: true, get: function () { return auth_config_1.computeExpireAt; } });
|
|
33
|
+
Object.defineProperty(exports, "DEFAULT_EXPIRE_DURATION_MS", { enumerable: true, get: function () { return auth_config_1.DEFAULT_EXPIRE_DURATION_MS; } });
|
|
34
|
+
Object.defineProperty(exports, "isValidTokenHash", { enumerable: true, get: function () { return auth_config_1.isValidTokenHash; } });
|
|
35
|
+
/** Rate limiting configuration for brute-force protection */
|
|
36
|
+
exports.RATE_LIMIT_CONFIG = {
|
|
37
|
+
/** Maximum failed attempts before lockout */
|
|
38
|
+
maxAttempts: 5,
|
|
39
|
+
/** Lockout duration in ms (15 minutes) */
|
|
40
|
+
lockoutDuration: 15 * 60 * 1000,
|
|
41
|
+
/** Cleanup interval in ms (1 hour) */
|
|
42
|
+
cleanupInterval: 60 * 60 * 1000,
|
|
43
|
+
};
|
|
44
|
+
/** Fallback cookie maxAge in seconds when no explicit expiry is set (24 hours) */
|
|
45
|
+
exports.DEFAULT_COOKIE_MAX_AGE_SECONDS = 24 * 60 * 60;
|
|
46
|
+
// ============================================================
|
|
47
|
+
// Module-level state (initialized from env at import time)
|
|
48
|
+
// ============================================================
|
|
49
|
+
/** The stored hash of the authentication token */
|
|
50
|
+
const storedTokenHash = (() => {
|
|
51
|
+
const hash = process.env.CM_AUTH_TOKEN_HASH || undefined;
|
|
52
|
+
if (!hash)
|
|
53
|
+
return undefined;
|
|
54
|
+
// Store validation result to avoid type predicate narrowing to 'never' in else branch
|
|
55
|
+
const valid = (0, auth_config_1.isValidTokenHash)(hash);
|
|
56
|
+
if (!valid) {
|
|
57
|
+
console.error(`[Security] CM_AUTH_TOKEN_HASH is not a valid 64-character hex string (got ${hash.length} chars). Authentication will be disabled.`);
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return hash;
|
|
61
|
+
})();
|
|
62
|
+
/** Token expiration timestamp (ms since epoch) */
|
|
63
|
+
const expireAt = (0, auth_config_1.computeExpireAt)();
|
|
64
|
+
// ============================================================
|
|
65
|
+
// Token Functions
|
|
66
|
+
// ============================================================
|
|
67
|
+
/**
|
|
68
|
+
* Generate a cryptographically secure random token
|
|
69
|
+
* @returns 64-character hex string (32 bytes of entropy)
|
|
70
|
+
*/
|
|
71
|
+
function generateToken() {
|
|
72
|
+
return crypto_1.default.randomBytes(32).toString('hex');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Hash a token using SHA-256
|
|
76
|
+
* @param token - The plain text token to hash
|
|
77
|
+
* @returns 64-character hex string (SHA-256 hash)
|
|
78
|
+
*/
|
|
79
|
+
function hashToken(token) {
|
|
80
|
+
return crypto_1.default.createHash('sha256').update(token).digest('hex');
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Verify a token against the stored hash
|
|
84
|
+
* S001: Uses crypto.timingSafeEqual() for timing-safe comparison
|
|
85
|
+
*
|
|
86
|
+
* @param token - The plain text token to verify
|
|
87
|
+
* @returns true if the token is valid and not expired
|
|
88
|
+
*/
|
|
89
|
+
function verifyToken(token) {
|
|
90
|
+
if (!storedTokenHash) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
// Guard against undefined/null/empty token
|
|
94
|
+
if (!token || typeof token !== 'string') {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
// Check expiration
|
|
98
|
+
if (expireAt !== null && Date.now() > expireAt) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
const tokenHash = hashToken(token);
|
|
102
|
+
// S001: timing-safe comparison to prevent timing attacks
|
|
103
|
+
const hashBuffer = Buffer.from(tokenHash, 'hex');
|
|
104
|
+
const storedBuffer = Buffer.from(storedTokenHash, 'hex');
|
|
105
|
+
if (hashBuffer.length !== storedBuffer.length) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return crypto_1.default.timingSafeEqual(hashBuffer, storedBuffer);
|
|
109
|
+
}
|
|
110
|
+
// ============================================================
|
|
111
|
+
// Cookie Parsing (for WebSocket authentication)
|
|
112
|
+
// ============================================================
|
|
113
|
+
/**
|
|
114
|
+
* Parse a Cookie header string into key-value pairs
|
|
115
|
+
* Used by WebSocket upgrade handler where next/headers is not available
|
|
116
|
+
*
|
|
117
|
+
* @param cookieHeader - Raw Cookie header string
|
|
118
|
+
* @returns Parsed cookies as Record<string, string>
|
|
119
|
+
*/
|
|
120
|
+
function parseCookies(cookieHeader) {
|
|
121
|
+
const cookies = {};
|
|
122
|
+
if (!cookieHeader) {
|
|
123
|
+
return cookies;
|
|
124
|
+
}
|
|
125
|
+
const pairs = cookieHeader.split(';');
|
|
126
|
+
for (const pair of pairs) {
|
|
127
|
+
const eqIndex = pair.indexOf('=');
|
|
128
|
+
if (eqIndex === -1) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const name = pair.substring(0, eqIndex).trim();
|
|
132
|
+
const value = pair.substring(eqIndex + 1).trim();
|
|
133
|
+
if (name) {
|
|
134
|
+
cookies[name] = value;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return cookies;
|
|
138
|
+
}
|
|
139
|
+
// ============================================================
|
|
140
|
+
// Auth State Functions
|
|
141
|
+
// ============================================================
|
|
142
|
+
/**
|
|
143
|
+
* Check if authentication is enabled.
|
|
144
|
+
* Returns true only when CM_AUTH_TOKEN_HASH is set AND passes format validation.
|
|
145
|
+
* This prevents the state where auth appears enabled but login is impossible
|
|
146
|
+
* (e.g., when the hash value is malformed).
|
|
147
|
+
*/
|
|
148
|
+
function isAuthEnabled() {
|
|
149
|
+
return !!storedTokenHash;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Calculate the Cookie maxAge in seconds (remaining token lifetime)
|
|
153
|
+
* @returns maxAge in seconds, or 0 if expired/no expiry
|
|
154
|
+
*/
|
|
155
|
+
function getTokenMaxAge() {
|
|
156
|
+
if (expireAt === null) {
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
const remaining = expireAt - Date.now();
|
|
160
|
+
if (remaining <= 0) {
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
return Math.floor(remaining / 1000);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Check if HTTPS is enabled based on certificate environment variable
|
|
167
|
+
* @returns true if CM_HTTPS_CERT is set (indicating TLS certificates are configured)
|
|
168
|
+
*/
|
|
169
|
+
function isHttpsEnabled() {
|
|
170
|
+
return !!process.env.CM_HTTPS_CERT;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Build authentication cookie options with consistent security settings.
|
|
174
|
+
* Centralizes cookie configuration to enforce HttpOnly, SameSite, and Secure flags.
|
|
175
|
+
*
|
|
176
|
+
* @param maxAge - Cookie max age in seconds. Use 0 to clear the cookie.
|
|
177
|
+
* @returns Cookie options object compatible with Next.js response.cookies.set()
|
|
178
|
+
*/
|
|
179
|
+
function buildAuthCookieOptions(maxAge) {
|
|
180
|
+
return {
|
|
181
|
+
httpOnly: true,
|
|
182
|
+
sameSite: 'strict',
|
|
183
|
+
secure: isHttpsEnabled(),
|
|
184
|
+
maxAge,
|
|
185
|
+
path: '/',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Create a rate limiter for brute-force protection
|
|
190
|
+
* Uses in-memory Map with periodic cleanup
|
|
191
|
+
*
|
|
192
|
+
* @returns RateLimiter instance with checkLimit, recordFailure, recordSuccess, destroy
|
|
193
|
+
*/
|
|
194
|
+
function createRateLimiter() {
|
|
195
|
+
const entries = new Map();
|
|
196
|
+
// Periodic cleanup of expired entries
|
|
197
|
+
const cleanupTimer = setInterval(() => {
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
for (const [ip, entry] of entries) {
|
|
200
|
+
// Remove entries whose lockout has expired (or was never set)
|
|
201
|
+
// and whose last attempt is older than the lockout duration
|
|
202
|
+
const isLockoutExpired = entry.lockedUntil === null || now > entry.lockedUntil;
|
|
203
|
+
const isStale = now - entry.lastAttempt > exports.RATE_LIMIT_CONFIG.lockoutDuration;
|
|
204
|
+
if (isLockoutExpired && isStale) {
|
|
205
|
+
entries.delete(ip);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}, exports.RATE_LIMIT_CONFIG.cleanupInterval);
|
|
209
|
+
// Ensure timer doesn't prevent process exit
|
|
210
|
+
if (cleanupTimer.unref) {
|
|
211
|
+
cleanupTimer.unref();
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
checkLimit(ip) {
|
|
215
|
+
const entry = entries.get(ip);
|
|
216
|
+
if (!entry) {
|
|
217
|
+
return { allowed: true };
|
|
218
|
+
}
|
|
219
|
+
// Check if lockout has expired
|
|
220
|
+
if (entry.lockedUntil !== null) {
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
if (now < entry.lockedUntil) {
|
|
223
|
+
const retryAfter = Math.ceil((entry.lockedUntil - now) / 1000);
|
|
224
|
+
return { allowed: false, retryAfter };
|
|
225
|
+
}
|
|
226
|
+
// Lockout expired - reset entry
|
|
227
|
+
entry.attempts = 0;
|
|
228
|
+
entry.lockedUntil = null;
|
|
229
|
+
}
|
|
230
|
+
return { allowed: true };
|
|
231
|
+
},
|
|
232
|
+
recordFailure(ip) {
|
|
233
|
+
const now = Date.now();
|
|
234
|
+
const entry = entries.get(ip) || { attempts: 0, lockedUntil: null, lastAttempt: now };
|
|
235
|
+
entry.attempts++;
|
|
236
|
+
entry.lastAttempt = now;
|
|
237
|
+
if (entry.attempts >= exports.RATE_LIMIT_CONFIG.maxAttempts) {
|
|
238
|
+
entry.lockedUntil = now + exports.RATE_LIMIT_CONFIG.lockoutDuration;
|
|
239
|
+
}
|
|
240
|
+
entries.set(ip, entry);
|
|
241
|
+
},
|
|
242
|
+
recordSuccess(ip) {
|
|
243
|
+
entries.delete(ip);
|
|
244
|
+
},
|
|
245
|
+
destroy() {
|
|
246
|
+
clearInterval(cleanupTimer);
|
|
247
|
+
entries.clear();
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
@@ -22,6 +22,10 @@ exports.clearAllPollerStates = clearAllPollerStates;
|
|
|
22
22
|
exports.getLastServerResponseTimestamp = getLastServerResponseTimestamp;
|
|
23
23
|
exports.executeRegexWithTimeout = executeRegexWithTimeout;
|
|
24
24
|
exports.checkStopCondition = checkStopCondition;
|
|
25
|
+
exports.validatePollingContext = validatePollingContext;
|
|
26
|
+
exports.captureAndCleanOutput = captureAndCleanOutput;
|
|
27
|
+
exports.processStopConditionDelta = processStopConditionDelta;
|
|
28
|
+
exports.detectAndRespondToPrompt = detectAndRespondToPrompt;
|
|
25
29
|
exports.startAutoYesPolling = startAutoYesPolling;
|
|
26
30
|
exports.stopAutoYesPolling = stopAutoYesPolling;
|
|
27
31
|
exports.stopAllAutoYesPolling = stopAllAutoYesPolling;
|
|
@@ -196,6 +200,18 @@ function clearAllAutoYesStates() {
|
|
|
196
200
|
// =============================================================================
|
|
197
201
|
// Server-side Polling (Issue #138)
|
|
198
202
|
// =============================================================================
|
|
203
|
+
/**
|
|
204
|
+
* Get poller state for a worktree.
|
|
205
|
+
* Returns undefined if no poller state exists.
|
|
206
|
+
*
|
|
207
|
+
* Issue #323: Introduced for consistency with getAutoYesState() accessor pattern (DR003).
|
|
208
|
+
*
|
|
209
|
+
* @param worktreeId - Worktree identifier
|
|
210
|
+
* @returns Poller state or undefined
|
|
211
|
+
*/
|
|
212
|
+
function getPollerState(worktreeId) {
|
|
213
|
+
return autoYesPollerStates.get(worktreeId);
|
|
214
|
+
}
|
|
199
215
|
/**
|
|
200
216
|
* Get the number of active pollers.
|
|
201
217
|
*
|
|
@@ -221,7 +237,7 @@ function clearAllPollerStates() {
|
|
|
221
237
|
* @returns Timestamp (Date.now()) of the last server response, or null if none
|
|
222
238
|
*/
|
|
223
239
|
function getLastServerResponseTimestamp(worktreeId) {
|
|
224
|
-
const pollerState =
|
|
240
|
+
const pollerState = getPollerState(worktreeId);
|
|
225
241
|
return pollerState?.lastServerResponseTimestamp ?? null;
|
|
226
242
|
}
|
|
227
243
|
/**
|
|
@@ -231,7 +247,7 @@ function getLastServerResponseTimestamp(worktreeId) {
|
|
|
231
247
|
* @param timestamp - Timestamp value (Date.now())
|
|
232
248
|
*/
|
|
233
249
|
function updateLastServerResponseTimestamp(worktreeId, timestamp) {
|
|
234
|
-
const pollerState =
|
|
250
|
+
const pollerState = getPollerState(worktreeId);
|
|
235
251
|
if (pollerState) {
|
|
236
252
|
pollerState.lastServerResponseTimestamp = timestamp;
|
|
237
253
|
}
|
|
@@ -242,7 +258,7 @@ function updateLastServerResponseTimestamp(worktreeId, timestamp) {
|
|
|
242
258
|
* @param worktreeId - Worktree identifier
|
|
243
259
|
*/
|
|
244
260
|
function resetErrorCount(worktreeId) {
|
|
245
|
-
const pollerState =
|
|
261
|
+
const pollerState = getPollerState(worktreeId);
|
|
246
262
|
if (pollerState) {
|
|
247
263
|
pollerState.consecutiveErrors = 0;
|
|
248
264
|
pollerState.currentInterval = exports.POLLING_INTERVAL_MS;
|
|
@@ -254,7 +270,7 @@ function resetErrorCount(worktreeId) {
|
|
|
254
270
|
* @param worktreeId - Worktree identifier
|
|
255
271
|
*/
|
|
256
272
|
function incrementErrorCount(worktreeId) {
|
|
257
|
-
const pollerState =
|
|
273
|
+
const pollerState = getPollerState(worktreeId);
|
|
258
274
|
if (pollerState) {
|
|
259
275
|
pollerState.consecutiveErrors++;
|
|
260
276
|
pollerState.currentInterval = calculateBackoffInterval(pollerState.consecutiveErrors);
|
|
@@ -342,137 +358,205 @@ function checkStopCondition(worktreeId, cleanOutput) {
|
|
|
342
358
|
}
|
|
343
359
|
return false;
|
|
344
360
|
}
|
|
361
|
+
// =============================================================================
|
|
362
|
+
// Extracted Functions for pollAutoYes (Issue #323: SRP decomposition)
|
|
363
|
+
// =============================================================================
|
|
345
364
|
/**
|
|
346
|
-
*
|
|
347
|
-
*
|
|
365
|
+
* Validate that polling context is still valid.
|
|
366
|
+
* Checks pollerState existence and auto-yes enabled state.
|
|
348
367
|
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
368
|
+
* @sideeffect When returning 'expired', calls stopAutoYesPolling() to clean up
|
|
369
|
+
* the poller state. This side-effect is intentional - separating the check from
|
|
370
|
+
* the cleanup would risk callers forgetting to call stopAutoYesPolling() (DR002).
|
|
352
371
|
*
|
|
372
|
+
* @internal Exported for testing purposes only.
|
|
373
|
+
* @precondition worktreeId is validated by isValidWorktreeId() in startAutoYesPolling()
|
|
374
|
+
* gateway before being registered in globalThis Map. This function assumes worktreeId
|
|
375
|
+
* has already passed that validation.
|
|
353
376
|
* @param worktreeId - Worktree identifier
|
|
354
|
-
* @param
|
|
377
|
+
* @param pollerState - Current poller state (or undefined if not found)
|
|
378
|
+
* @returns 'valid' | 'stopped' | 'expired'
|
|
355
379
|
*/
|
|
356
|
-
|
|
357
|
-
// Check if poller was stopped
|
|
358
|
-
const pollerState = autoYesPollerStates.get(worktreeId);
|
|
380
|
+
function validatePollingContext(worktreeId, pollerState) {
|
|
359
381
|
if (!pollerState)
|
|
360
|
-
return;
|
|
361
|
-
// Check if auto-yes is still enabled
|
|
382
|
+
return 'stopped';
|
|
362
383
|
const autoYesState = getAutoYesState(worktreeId);
|
|
363
384
|
if (!autoYesState?.enabled || isAutoYesExpired(autoYesState)) {
|
|
364
385
|
stopAutoYesPolling(worktreeId);
|
|
365
|
-
return;
|
|
386
|
+
return 'expired';
|
|
366
387
|
}
|
|
388
|
+
return 'valid';
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Capture tmux session output and strip ANSI escape codes.
|
|
392
|
+
*
|
|
393
|
+
* @internal Exported for testing purposes only.
|
|
394
|
+
* @precondition worktreeId is validated by isValidWorktreeId() in startAutoYesPolling()
|
|
395
|
+
* gateway before being registered in globalThis Map. This function assumes worktreeId
|
|
396
|
+
* has already passed that validation.
|
|
397
|
+
* @param worktreeId - Worktree identifier
|
|
398
|
+
* @param cliToolId - CLI tool type being polled
|
|
399
|
+
* @returns Cleaned output string (ANSI stripped)
|
|
400
|
+
*/
|
|
401
|
+
async function captureAndCleanOutput(worktreeId, cliToolId) {
|
|
402
|
+
// 5000 lines: matches the existing pollAutoYes() line limit (IC002).
|
|
403
|
+
// captureSessionOutput() default is 1000 lines, but tmux buffer capture
|
|
404
|
+
// requires 5000 to avoid truncating long outputs.
|
|
405
|
+
const output = await (0, cli_session_1.captureSessionOutput)(worktreeId, cliToolId, 5000);
|
|
406
|
+
return (0, cli_patterns_1.stripAnsi)(output);
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Process stop condition check using delta-based approach.
|
|
410
|
+
* Manages baseline length, extracts new content, and delegates
|
|
411
|
+
* pattern matching to existing checkStopCondition().
|
|
412
|
+
*
|
|
413
|
+
* Note: This is a higher-level function that internally calls
|
|
414
|
+
* checkStopCondition() (L409, Issue #314) for regex pattern matching.
|
|
415
|
+
*
|
|
416
|
+
* @internal Exported for testing purposes only.
|
|
417
|
+
* @precondition worktreeId is validated by isValidWorktreeId() in startAutoYesPolling()
|
|
418
|
+
* gateway before being registered in globalThis Map. This function assumes worktreeId
|
|
419
|
+
* has already passed that validation.
|
|
420
|
+
* @param worktreeId - Worktree identifier
|
|
421
|
+
* @param pollerState - Current poller state (mutated: stopCheckBaselineLength updated)
|
|
422
|
+
* @param cleanOutput - ANSI-stripped terminal output
|
|
423
|
+
* @returns true if stop condition matched and auto-yes was disabled
|
|
424
|
+
*/
|
|
425
|
+
function processStopConditionDelta(worktreeId, pollerState, cleanOutput) {
|
|
426
|
+
if (pollerState.stopCheckBaselineLength < 0) {
|
|
427
|
+
// First poll: set baseline, skip stop condition check
|
|
428
|
+
pollerState.stopCheckBaselineLength = cleanOutput.length;
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
const baseline = pollerState.stopCheckBaselineLength;
|
|
432
|
+
if (cleanOutput.length > baseline) {
|
|
433
|
+
// Output grew: check only new content (delta)
|
|
434
|
+
const newContent = cleanOutput.substring(baseline);
|
|
435
|
+
pollerState.stopCheckBaselineLength = cleanOutput.length;
|
|
436
|
+
return checkStopCondition(worktreeId, newContent);
|
|
437
|
+
}
|
|
438
|
+
else if (cleanOutput.length < baseline) {
|
|
439
|
+
// Buffer shrank (old lines dropped from scrollback): reset baseline
|
|
440
|
+
pollerState.stopCheckBaselineLength = cleanOutput.length;
|
|
441
|
+
}
|
|
442
|
+
// If length unchanged: no new content, skip check
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Detect prompt in terminal output, resolve auto-answer, and send response.
|
|
447
|
+
* Handles the complete flow: detection -> duplicate check -> answer resolution ->
|
|
448
|
+
* tmux send -> timestamp/error-count update -> promptKey recording.
|
|
449
|
+
*
|
|
450
|
+
* Note: Cooldown scheduling is NOT this function's responsibility.
|
|
451
|
+
* The caller (pollAutoYes() orchestrator) determines scheduling interval
|
|
452
|
+
* based on the return value ('responded' -> cooldown, others -> normal interval).
|
|
453
|
+
*
|
|
454
|
+
* @internal Exported for testing purposes only.
|
|
455
|
+
* @precondition worktreeId is validated by isValidWorktreeId() in startAutoYesPolling()
|
|
456
|
+
* gateway before being registered in globalThis Map. This function assumes worktreeId
|
|
457
|
+
* has already passed that validation.
|
|
458
|
+
* @param worktreeId - Worktree identifier
|
|
459
|
+
* @param pollerState - Current poller state (mutated: lastAnsweredPromptKey updated)
|
|
460
|
+
* @param cliToolId - CLI tool type
|
|
461
|
+
* @param cleanOutput - ANSI-stripped terminal output
|
|
462
|
+
* @returns 'responded' | 'no_prompt' | 'duplicate' | 'no_answer' | 'error'
|
|
463
|
+
*/
|
|
464
|
+
async function detectAndRespondToPrompt(worktreeId, pollerState, cliToolId, cleanOutput) {
|
|
367
465
|
try {
|
|
368
|
-
// 1.
|
|
369
|
-
const output = await (0, cli_session_1.captureSessionOutput)(worktreeId, cliToolId, 5000);
|
|
370
|
-
// 2. Strip ANSI codes
|
|
371
|
-
const cleanOutput = (0, cli_patterns_1.stripAnsi)(output);
|
|
372
|
-
// 2.5. Skip prompt detection during thinking state (Issue #161, Layer 1)
|
|
373
|
-
// This prevents false positive detection of numbered lists in CLI output
|
|
374
|
-
// while Claude is actively processing (thinking/planning).
|
|
375
|
-
//
|
|
376
|
-
// Issue #191: Apply windowing to detectThinking() to prevent stale thinking
|
|
377
|
-
// summary lines (e.g., "· Simmering...") from blocking prompt detection.
|
|
378
|
-
// Window size matches detectPrompt()'s multiple_choice scan range (50 lines).
|
|
379
|
-
//
|
|
380
|
-
// Safety: Claude CLI does not emit prompts during thinking, so narrowing
|
|
381
|
-
// the window cannot cause false auto-responses (see IA-003 in design doc).
|
|
382
|
-
//
|
|
383
|
-
// Processing order: stripAnsi -> split -> slice -> join
|
|
384
|
-
// stripAnsi is applied BEFORE split to ensure ANSI escape sequences spanning
|
|
385
|
-
// line boundaries do not affect line counting (IA-002).
|
|
386
|
-
//
|
|
387
|
-
// Boundary case: if buffer has fewer than 50 lines, slice(-50) returns the
|
|
388
|
-
// entire array (Array.prototype.slice specification), which is safe degradation
|
|
389
|
-
// equivalent to pre-fix behavior (IA-001).
|
|
390
|
-
const recentLines = cleanOutput.split('\n').slice(-exports.THINKING_CHECK_LINE_COUNT).join('\n');
|
|
391
|
-
if ((0, cli_patterns_1.detectThinking)(cliToolId, recentLines)) {
|
|
392
|
-
scheduleNextPoll(worktreeId, cliToolId);
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
// 2.7. Check stop condition (Issue #314)
|
|
396
|
-
// After thinking check, before prompt detection: if terminal output matches
|
|
397
|
-
// the stop pattern, disable auto-yes and stop polling immediately.
|
|
398
|
-
//
|
|
399
|
-
// Delta-based check: Only check NEW output since Auto-Yes was enabled.
|
|
400
|
-
// On the first poll, establish the baseline output length (skip check to
|
|
401
|
-
// avoid matching pre-existing terminal content like shell prompts or paths).
|
|
402
|
-
// On subsequent polls, check only the output delta (new content appended).
|
|
403
|
-
// If the buffer shrank (tmux scrollback shifted), reset baseline and skip
|
|
404
|
-
// that cycle to avoid false positives from old content.
|
|
405
|
-
if (pollerState.stopCheckBaselineLength < 0) {
|
|
406
|
-
// First poll: set baseline, skip stop condition check
|
|
407
|
-
pollerState.stopCheckBaselineLength = cleanOutput.length;
|
|
408
|
-
}
|
|
409
|
-
else {
|
|
410
|
-
const baseline = pollerState.stopCheckBaselineLength;
|
|
411
|
-
if (cleanOutput.length > baseline) {
|
|
412
|
-
// Output grew: check only new content (delta)
|
|
413
|
-
const newContent = cleanOutput.substring(baseline);
|
|
414
|
-
pollerState.stopCheckBaselineLength = cleanOutput.length;
|
|
415
|
-
if (checkStopCondition(worktreeId, newContent)) {
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
else if (cleanOutput.length < baseline) {
|
|
420
|
-
// Buffer shrank (old lines dropped from scrollback): reset baseline
|
|
421
|
-
pollerState.stopCheckBaselineLength = cleanOutput.length;
|
|
422
|
-
}
|
|
423
|
-
// If length unchanged: no new content, skip check
|
|
424
|
-
}
|
|
425
|
-
// 3. Detect prompt
|
|
466
|
+
// 1. Detect prompt
|
|
426
467
|
const promptOptions = (0, cli_patterns_1.buildDetectPromptOptions)(cliToolId);
|
|
427
468
|
const promptDetection = (0, prompt_detector_1.detectPrompt)(cleanOutput, promptOptions);
|
|
428
469
|
if (!promptDetection.isPrompt || !promptDetection.promptData) {
|
|
429
470
|
// No prompt detected - reset lastAnsweredPromptKey (Issue #306)
|
|
430
471
|
pollerState.lastAnsweredPromptKey = null;
|
|
431
|
-
|
|
432
|
-
return;
|
|
472
|
+
return 'no_prompt';
|
|
433
473
|
}
|
|
434
|
-
//
|
|
474
|
+
// 2. Check for duplicate prompt (Issue #306)
|
|
435
475
|
const promptKey = (0, prompt_key_1.generatePromptKey)(promptDetection.promptData);
|
|
436
476
|
if (isDuplicatePrompt(pollerState, promptKey)) {
|
|
437
|
-
|
|
438
|
-
return;
|
|
477
|
+
return 'duplicate';
|
|
439
478
|
}
|
|
440
|
-
//
|
|
479
|
+
// 3. Resolve auto answer
|
|
441
480
|
const answer = (0, auto_yes_resolver_1.resolveAutoAnswer)(promptDetection.promptData);
|
|
442
481
|
if (answer === null) {
|
|
443
|
-
|
|
444
|
-
scheduleNextPoll(worktreeId, cliToolId);
|
|
445
|
-
return;
|
|
482
|
+
return 'no_answer';
|
|
446
483
|
}
|
|
447
|
-
//
|
|
484
|
+
// 4. Send answer to tmux
|
|
448
485
|
const manager = manager_1.CLIToolManager.getInstance();
|
|
449
486
|
const cliTool = manager.getTool(cliToolId);
|
|
450
487
|
const sessionName = cliTool.getSessionName(worktreeId);
|
|
451
|
-
// Issue #287 Bug2: Uses shared sendPromptAnswer() to unify logic
|
|
452
|
-
// with route.ts, including cursor-key navigation for Claude Code
|
|
453
|
-
// multiple-choice prompts and fallback handling.
|
|
454
488
|
await (0, prompt_answer_sender_1.sendPromptAnswer)({
|
|
455
489
|
sessionName,
|
|
456
490
|
answer,
|
|
457
491
|
cliToolId,
|
|
458
492
|
promptData: promptDetection.promptData,
|
|
459
493
|
});
|
|
460
|
-
//
|
|
494
|
+
// 5. Update timestamp and reset error count
|
|
461
495
|
updateLastServerResponseTimestamp(worktreeId, Date.now());
|
|
462
|
-
// 7. Reset error count on success
|
|
463
496
|
resetErrorCount(worktreeId);
|
|
464
|
-
//
|
|
497
|
+
// 6. Record answered prompt key
|
|
465
498
|
pollerState.lastAnsweredPromptKey = promptKey;
|
|
466
499
|
// Log success (without sensitive content)
|
|
467
500
|
console.info(`[Auto-Yes Poller] Sent response for worktree: ${worktreeId}`);
|
|
468
|
-
|
|
469
|
-
|
|
501
|
+
return 'responded';
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
// IC003: This catch handles errors from prompt detection/sending only.
|
|
505
|
+
// incrementErrorCount is called here, and this function never throws,
|
|
506
|
+
// preventing double incrementErrorCount in the outer pollAutoYes() catch.
|
|
507
|
+
incrementErrorCount(worktreeId);
|
|
508
|
+
console.warn(`[Auto-Yes Poller] Error in detectAndRespondToPrompt for worktree ${worktreeId}: ${getErrorMessage(error)}`);
|
|
509
|
+
return 'error';
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Internal polling function that recursively schedules itself via setTimeout.
|
|
514
|
+
* Orchestrates the polling flow by delegating to extracted functions.
|
|
515
|
+
*
|
|
516
|
+
* Issue #323: Refactored from ~139 lines to ~30 lines as an orchestrator.
|
|
517
|
+
* Each responsibility is delegated to a focused function:
|
|
518
|
+
* - validatePollingContext(): Pre-condition checks
|
|
519
|
+
* - captureAndCleanOutput(): tmux output capture + ANSI cleanup
|
|
520
|
+
* - processStopConditionDelta(): Stop condition delta-based check
|
|
521
|
+
* - detectAndRespondToPrompt(): Prompt detection + auto-response
|
|
522
|
+
*
|
|
523
|
+
* @param worktreeId - Worktree identifier
|
|
524
|
+
* @param cliToolId - CLI tool type being polled
|
|
525
|
+
*/
|
|
526
|
+
async function pollAutoYes(worktreeId, cliToolId) {
|
|
527
|
+
// 1. Validate context
|
|
528
|
+
const pollerState = getPollerState(worktreeId);
|
|
529
|
+
const contextResult = validatePollingContext(worktreeId, pollerState);
|
|
530
|
+
if (contextResult !== 'valid')
|
|
470
531
|
return;
|
|
532
|
+
// pollerState is guaranteed non-null after 'valid' check
|
|
533
|
+
try {
|
|
534
|
+
// 2. Capture and clean output
|
|
535
|
+
const cleanOutput = await captureAndCleanOutput(worktreeId, cliToolId);
|
|
536
|
+
// 3. Thinking check (inline - policy decision about window size)
|
|
537
|
+
// Issue #161 Layer 1 / Issue #191: windowing applied to detectThinking()
|
|
538
|
+
const recentLines = cleanOutput.split('\n').slice(-exports.THINKING_CHECK_LINE_COUNT).join('\n');
|
|
539
|
+
if ((0, cli_patterns_1.detectThinking)(cliToolId, recentLines)) {
|
|
540
|
+
scheduleNextPoll(worktreeId, cliToolId);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
// 4. Stop condition delta check (Issue #314)
|
|
544
|
+
if (processStopConditionDelta(worktreeId, pollerState, cleanOutput)) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
// 5. Detect and respond to prompt
|
|
548
|
+
const result = await detectAndRespondToPrompt(worktreeId, pollerState, cliToolId, cleanOutput);
|
|
549
|
+
if (result === 'responded') {
|
|
550
|
+
// Issue #306: Apply cooldown interval after successful response
|
|
551
|
+
scheduleNextPoll(worktreeId, cliToolId, exports.COOLDOWN_INTERVAL_MS);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
471
554
|
}
|
|
472
555
|
catch (error) {
|
|
473
|
-
//
|
|
556
|
+
// IC003: This catch handles captureAndCleanOutput() or processStopConditionDelta()
|
|
557
|
+
// errors only. detectAndRespondToPrompt() catches its own errors and returns
|
|
558
|
+
// 'error' instead of throwing (preventing double incrementErrorCount).
|
|
474
559
|
incrementErrorCount(worktreeId);
|
|
475
|
-
// Log error (without sensitive details)
|
|
476
560
|
console.warn(`[Auto-Yes Poller] Error for worktree ${worktreeId}: ${getErrorMessage(error)}`);
|
|
477
561
|
}
|
|
478
562
|
// Schedule next poll (catch block fallthrough or other paths)
|
|
@@ -485,7 +569,7 @@ async function pollAutoYes(worktreeId, cliToolId) {
|
|
|
485
569
|
* Type: number | undefined (optional parameter).
|
|
486
570
|
*/
|
|
487
571
|
function scheduleNextPoll(worktreeId, cliToolId, overrideInterval) {
|
|
488
|
-
const pollerState =
|
|
572
|
+
const pollerState = getPollerState(worktreeId);
|
|
489
573
|
if (!pollerState)
|
|
490
574
|
return;
|
|
491
575
|
// S4-F003: Floor guard - polling interval must not be below POLLING_INTERVAL_MS
|
|
@@ -548,7 +632,7 @@ function startAutoYesPolling(worktreeId, cliToolId) {
|
|
|
548
632
|
* @param worktreeId - Worktree identifier
|
|
549
633
|
*/
|
|
550
634
|
function stopAutoYesPolling(worktreeId) {
|
|
551
|
-
const pollerState =
|
|
635
|
+
const pollerState = getPollerState(worktreeId);
|
|
552
636
|
if (!pollerState)
|
|
553
637
|
return;
|
|
554
638
|
// Clear timer
|