cc-resilient 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SaravananJaichandar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # cc-resilient
2
+
3
+ Network-resilient wrapper for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI.
4
+
5
+ Built as a working prototype for [anthropics/claude-code#26729](https://github.com/anthropics/claude-code/issues/26729) -- a proposal for native streaming resilience in Claude Code.
6
+
7
+ ## Problem
8
+
9
+ When using Claude Code over unstable connections (Wi-Fi drops, power cuts, VPN disconnects, mobile hotspot switching), active sessions hang silently with no timeout, no recovery, and no graceful handling. The only option is to kill the process and manually restart.
10
+
11
+ ## What cc-resilient does
12
+
13
+ `cc-resilient` wraps the `claude` CLI and adds three capabilities:
14
+
15
+ 1. **Network monitoring** -- pings `api.anthropic.com` every 5 seconds to detect connectivity loss
16
+ 2. **Hang detection** -- identifies stalled processes with no output for a configurable timeout
17
+ 3. **Automatic recovery** -- kills hung processes, saves session metadata, and resumes with context when connectivity returns
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install -g cc-resilient
23
+ ```
24
+
25
+ **Prerequisite**: Claude Code must be installed and authenticated (`claude` command available in PATH).
26
+
27
+ ## Quick start
28
+
29
+ ```bash
30
+ # Use exactly like claude, but with network resilience
31
+ cc-resilient -- "explain this project"
32
+
33
+ # Print mode
34
+ cc-resilient -- -p "refactor the auth module"
35
+
36
+ # Continue a session with resilience
37
+ cc-resilient -- --continue
38
+
39
+ # Interactive mode (stdin passed through)
40
+ cc-resilient
41
+
42
+ # Disable auto-resume (ask before resuming)
43
+ cc-resilient --no-auto-resume -- -p "build feature X"
44
+
45
+ # Verbose mode (see network status on stderr)
46
+ cc-resilient --verbose -- -p "hello"
47
+
48
+ # Check last recovery state
49
+ cc-resilient --status
50
+ ```
51
+
52
+ ## How it works
53
+
54
+ ```
55
+ cc-resilient
56
+ |
57
+ +-------------+-------------+
58
+ | | |
59
+ Network Monitor Process Monitor Session Tracker
60
+ (ping every 5s) (track stdout) (find session ID)
61
+ | | |
62
+ +------+------+ |
63
+ | |
64
+ Recovery Manager <--------+
65
+ (orchestrate disconnect/reconnect/resume)
66
+ ```
67
+
68
+ 1. `cc-resilient` spawns `claude` as a child process
69
+ 2. In parallel, it pings `api.anthropic.com` every 5 seconds (HTTPS HEAD)
70
+ 3. If 3 consecutive pings fail, it declares the connection offline
71
+ 4. It gracefully kills the hung claude process (SIGTERM, then SIGKILL after 5s)
72
+ 5. It saves recovery metadata to `~/.cc-resilient/recovery.json`
73
+ 6. When 2 consecutive pings succeed, it declares the connection restored
74
+ 7. It resumes the session: `claude --continue -p "You were interrupted by a network disconnection..."`
75
+
76
+ ## Configuration
77
+
78
+ ### CLI flags
79
+
80
+ | Flag | Default | Description |
81
+ |------|---------|-------------|
82
+ | `--health-interval <ms>` | 5000 | How often to check connectivity |
83
+ | `--health-timeout <ms>` | 3000 | Timeout per health check |
84
+ | `--hang-timeout <ms>` | 300000 | No-output duration before declaring a hang (5 min) |
85
+ | `--no-auto-resume` | false | Ask before resuming instead of auto-resume |
86
+ | `--max-resumes <n>` | 3 | Give up after N consecutive resume failures |
87
+ | `--resume-prompt <text>` | (see below) | Custom text for the resume context message |
88
+ | `--log-file <path>` | null | Write structured logs to a file |
89
+ | `--verbose` | false | Show detailed status on stderr |
90
+ | `--config <path>` | `~/.cc-resilient.json` | Path to config file |
91
+ | `--status` | - | Print last recovery state and exit |
92
+
93
+ ### Config file
94
+
95
+ Create `~/.cc-resilient.json` with any of these options:
96
+
97
+ ```json
98
+ {
99
+ "healthCheckIntervalMs": 5000,
100
+ "healthCheckTimeoutMs": 3000,
101
+ "healthCheckEndpoint": "https://api.anthropic.com",
102
+ "offlineThreshold": 3,
103
+ "reconnectStabilityCount": 2,
104
+ "processHangTimeoutMs": 300000,
105
+ "gracefulShutdownTimeoutMs": 5000,
106
+ "autoResume": true,
107
+ "autoResumeDelayMs": 2000,
108
+ "maxResumeAttempts": 3,
109
+ "verbose": false
110
+ }
111
+ ```
112
+
113
+ All fields are optional. Missing fields use defaults.
114
+
115
+ ## Limitations
116
+
117
+ This is an external wrapper, not a native feature. It can solve some problems but not all:
118
+
119
+ | What it can do | What it cannot do |
120
+ |----------------|-------------------|
121
+ | Detect network loss (via periodic pings) | See the SSE stream directly (instant detection) |
122
+ | Kill hung processes | Save partial streaming responses |
123
+ | Auto-resume sessions with context | Know exactly which tool was mid-execution |
124
+ | Save recovery metadata to disk | Repair corrupted conversation state (orphaned tool_use blocks) |
125
+ | Work with both interactive and print modes | Selectively retry safe vs unsafe tools |
126
+
127
+ The remaining ~60% of the proposed functionality requires native support inside Claude Code. See the [full feature request](https://github.com/anthropics/claude-code/issues/26729) for the complete design.
128
+
129
+ ## Recovery metadata
130
+
131
+ On disconnect, `cc-resilient` saves state to `~/.cc-resilient/recovery.json`:
132
+
133
+ ```json
134
+ {
135
+ "sessionId": "fdbee89f-40d1-483c-b33b-b4305d924d84",
136
+ "workingDirectory": "/home/user/project",
137
+ "timestamp": "2026-02-23T13:45:00.000Z",
138
+ "lastActivityTimestamp": "2026-02-23T13:44:55.000Z",
139
+ "disconnectReason": "network",
140
+ "claudeArgs": ["-p", "build the auth module"],
141
+ "pid": 12345,
142
+ "resumeCount": 0
143
+ }
144
+ ```
145
+
146
+ This persists even if `cc-resilient` itself crashes, so you can inspect what happened and manually resume.
147
+
148
+ ## Development
149
+
150
+ ```bash
151
+ git clone https://github.com/SaravananJaichandar/cc-resilient.git
152
+ cd cc-resilient
153
+ npm install
154
+ npm run build
155
+ npm test
156
+ ```
157
+
158
+ ## License
159
+
160
+ MIT
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function createCli(): Command;
@@ -0,0 +1,74 @@
1
+ import { Command } from 'commander';
2
+ import { loadConfig } from './config.js';
3
+ import { CcResilientWrapper } from './wrapper.js';
4
+ import { RecoveryManager } from './recovery-manager.js';
5
+ export function createCli() {
6
+ const program = new Command();
7
+ program
8
+ .name('cc-resilient')
9
+ .description('Network-resilient wrapper for Claude Code CLI')
10
+ .version('0.1.0')
11
+ .option('--health-interval <ms>', 'Health check interval in ms', '5000')
12
+ .option('--health-timeout <ms>', 'Health check timeout in ms', '3000')
13
+ .option('--hang-timeout <ms>', 'Process hang timeout in ms', '300000')
14
+ .option('--no-auto-resume', 'Disable automatic resume on reconnect')
15
+ .option('--max-resumes <n>', 'Maximum resume attempts', '3')
16
+ .option('--resume-prompt <text>', 'Custom prompt for resume context')
17
+ .option('--log-file <path>', 'Write logs to file')
18
+ .option('--verbose', 'Enable verbose output', false)
19
+ .option('--config <path>', 'Path to config file')
20
+ .option('--status', 'Show last recovery state and exit')
21
+ .allowUnknownOption(true)
22
+ .argument('[claude-args...]', 'Arguments to pass through to claude')
23
+ .action(async (claudeArgs, options) => {
24
+ if (options.status) {
25
+ showStatus();
26
+ return;
27
+ }
28
+ const cliOverrides = {};
29
+ if (options.healthInterval !== '5000') {
30
+ cliOverrides.healthCheckIntervalMs = parseInt(options.healthInterval, 10);
31
+ }
32
+ if (options.healthTimeout !== '3000') {
33
+ cliOverrides.healthCheckTimeoutMs = parseInt(options.healthTimeout, 10);
34
+ }
35
+ if (options.hangTimeout !== '300000') {
36
+ cliOverrides.processHangTimeoutMs = parseInt(options.hangTimeout, 10);
37
+ }
38
+ if (options.autoResume === false) {
39
+ cliOverrides.autoResume = false;
40
+ }
41
+ if (options.maxResumes !== '3') {
42
+ cliOverrides.maxResumeAttempts = parseInt(options.maxResumes, 10);
43
+ }
44
+ if (options.resumePrompt) {
45
+ cliOverrides.resumePrompt = options.resumePrompt;
46
+ }
47
+ if (options.logFile) {
48
+ cliOverrides.logFile = options.logFile;
49
+ }
50
+ if (options.verbose) {
51
+ cliOverrides.verbose = true;
52
+ }
53
+ const config = loadConfig(cliOverrides, options.config);
54
+ const wrapper = new CcResilientWrapper(claudeArgs, config);
55
+ const exitCode = await wrapper.run();
56
+ process.exit(exitCode);
57
+ });
58
+ return program;
59
+ }
60
+ function showStatus() {
61
+ const metadata = RecoveryManager.loadRecoveryMetadata();
62
+ if (!metadata) {
63
+ process.stderr.write('[cc-resilient] No recovery data found.\n');
64
+ return;
65
+ }
66
+ process.stderr.write('[cc-resilient] Last recovery state:\n');
67
+ process.stderr.write(` Session ID: ${metadata.sessionId ?? 'unknown'}\n`);
68
+ process.stderr.write(` Directory: ${metadata.workingDirectory}\n`);
69
+ process.stderr.write(` Disconnected: ${metadata.timestamp}\n`);
70
+ process.stderr.write(` Reason: ${metadata.disconnectReason}\n`);
71
+ process.stderr.write(` Resumes: ${metadata.resumeCount}\n`);
72
+ process.stderr.write(` Args: ${metadata.claudeArgs.join(' ')}\n`);
73
+ }
74
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { CcResilientConfig } from './types.js';
2
+ declare const DEFAULT_CONFIG: CcResilientConfig;
3
+ export declare function loadConfig(cliOverrides?: Partial<CcResilientConfig>, configPath?: string): CcResilientConfig;
4
+ export { DEFAULT_CONFIG };
@@ -0,0 +1,62 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ const DEFAULT_CONFIG = {
5
+ healthCheckIntervalMs: 5000,
6
+ healthCheckTimeoutMs: 3000,
7
+ healthCheckEndpoint: 'https://api.anthropic.com',
8
+ offlineThreshold: 3,
9
+ reconnectStabilityCount: 2,
10
+ processHangTimeoutMs: 300000,
11
+ gracefulShutdownTimeoutMs: 5000,
12
+ autoResume: true,
13
+ autoResumeDelayMs: 2000,
14
+ maxResumeAttempts: 3,
15
+ resumePrompt: 'You were interrupted by a network disconnection at {timestamp}. Your session has been automatically resumed. Please continue from where you left off. If you were in the middle of a multi-step task, summarize what you have completed so far and continue.',
16
+ logFile: null,
17
+ verbose: false,
18
+ };
19
+ function loadConfigFile(configPath) {
20
+ const filePath = configPath ?? join(homedir(), '.cc-resilient.json');
21
+ try {
22
+ const raw = readFileSync(filePath, 'utf-8');
23
+ return JSON.parse(raw);
24
+ }
25
+ catch {
26
+ return {};
27
+ }
28
+ }
29
+ function validateConfig(config) {
30
+ if (config.healthCheckIntervalMs <= 0)
31
+ throw new Error('healthCheckIntervalMs must be positive');
32
+ if (config.healthCheckTimeoutMs <= 0)
33
+ throw new Error('healthCheckTimeoutMs must be positive');
34
+ if (config.offlineThreshold < 1)
35
+ throw new Error('offlineThreshold must be at least 1');
36
+ if (config.reconnectStabilityCount < 1)
37
+ throw new Error('reconnectStabilityCount must be at least 1');
38
+ if (config.processHangTimeoutMs <= 0)
39
+ throw new Error('processHangTimeoutMs must be positive');
40
+ if (config.gracefulShutdownTimeoutMs <= 0)
41
+ throw new Error('gracefulShutdownTimeoutMs must be positive');
42
+ if (config.maxResumeAttempts < 1)
43
+ throw new Error('maxResumeAttempts must be at least 1');
44
+ try {
45
+ new URL(config.healthCheckEndpoint);
46
+ }
47
+ catch {
48
+ throw new Error('healthCheckEndpoint must be a valid URL');
49
+ }
50
+ }
51
+ export function loadConfig(cliOverrides = {}, configPath) {
52
+ const fileConfig = loadConfigFile(configPath);
53
+ const merged = {
54
+ ...DEFAULT_CONFIG,
55
+ ...fileConfig,
56
+ ...cliOverrides,
57
+ };
58
+ validateConfig(merged);
59
+ return merged;
60
+ }
61
+ export { DEFAULT_CONFIG };
62
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAGlC,MAAM,cAAc,GAAsB;IACxC,qBAAqB,EAAE,IAAI;IAC3B,oBAAoB,EAAE,IAAI;IAC1B,mBAAmB,EAAE,2BAA2B;IAChD,gBAAgB,EAAE,CAAC;IACnB,uBAAuB,EAAE,CAAC;IAC1B,oBAAoB,EAAE,MAAM;IAC5B,yBAAyB,EAAE,IAAI;IAC/B,UAAU,EAAE,IAAI;IAChB,iBAAiB,EAAE,IAAI;IACvB,iBAAiB,EAAE,CAAC;IACpB,YAAY,EACV,8PAA8P;IAChQ,OAAO,EAAE,IAAI;IACb,OAAO,EAAE,KAAK;CACf,CAAC;AAEF,SAAS,cAAc,CAAC,UAAmB;IACzC,MAAM,QAAQ,GAAG,UAAU,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,oBAAoB,CAAC,CAAC;IACrE,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC5C,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAA+B,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,MAAyB;IAC/C,IAAI,MAAM,CAAC,qBAAqB,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;IACjG,IAAI,MAAM,CAAC,oBAAoB,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC/F,IAAI,MAAM,CAAC,gBAAgB,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;IACxF,IAAI,MAAM,CAAC,uBAAuB,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;IACtG,IAAI,MAAM,CAAC,oBAAoB,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC/F,IAAI,MAAM,CAAC,yBAAyB,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;IACzG,IAAI,MAAM,CAAC,iBAAiB,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1F,IAAI,CAAC;QACH,IAAI,GAAG,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;AACH,CAAC;AAED,MAAM,UAAU,UAAU,CACxB,eAA2C,EAAE,EAC7C,UAAmB;IAEnB,MAAM,UAAU,GAAG,cAAc,CAAC,UAAU,CAAC,CAAC;IAC9C,MAAM,MAAM,GAAsB;QAChC,GAAG,cAAc;QACjB,GAAG,UAAU;QACb,GAAG,YAAY;KAChB,CAAC;IACF,cAAc,CAAC,MAAM,CAAC,CAAC;IACvB,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,OAAO,EAAE,cAAc,EAAE,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { createCli } from './cli.js';
3
+ const program = createCli();
4
+ program.parseAsync(process.argv).catch((err) => {
5
+ process.stderr.write(`[cc-resilient] Fatal error: ${err.message}\n`);
6
+ process.exit(1);
7
+ });
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAErC,MAAM,OAAO,GAAG,SAAS,EAAE,CAAC;AAC5B,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,GAAU,EAAE,EAAE;IACpD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,+BAA+B,GAAG,CAAC,OAAO,IAAI,CAAC,CAAC;IACrE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,10 @@
1
+ export declare class Logger {
2
+ private logFile;
3
+ private verbose;
4
+ constructor(logFile: string | null, verbose: boolean);
5
+ info(message: string, data?: Record<string, unknown>): void;
6
+ warn(message: string, data?: Record<string, unknown>): void;
7
+ error(message: string, data?: Record<string, unknown>): void;
8
+ debug(message: string, data?: Record<string, unknown>): void;
9
+ private write;
10
+ }
@@ -0,0 +1,48 @@
1
+ import { appendFileSync, mkdirSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ export class Logger {
4
+ logFile;
5
+ verbose;
6
+ constructor(logFile, verbose) {
7
+ this.logFile = logFile;
8
+ this.verbose = verbose;
9
+ if (this.logFile) {
10
+ mkdirSync(dirname(this.logFile), { recursive: true });
11
+ }
12
+ }
13
+ info(message, data) {
14
+ this.write('INFO', message, data);
15
+ }
16
+ warn(message, data) {
17
+ this.write('WARN', message, data);
18
+ }
19
+ error(message, data) {
20
+ this.write('ERROR', message, data);
21
+ }
22
+ debug(message, data) {
23
+ if (this.verbose) {
24
+ this.write('DEBUG', message, data);
25
+ }
26
+ }
27
+ write(level, message, data) {
28
+ const entry = {
29
+ timestamp: new Date().toISOString(),
30
+ level,
31
+ message,
32
+ ...data,
33
+ };
34
+ if (this.logFile) {
35
+ appendFileSync(this.logFile, JSON.stringify(entry) + '\n');
36
+ }
37
+ if (this.verbose || level === 'ERROR' || level === 'WARN') {
38
+ const prefix = level === 'ERROR' ? '\x1b[31m' : level === 'WARN' ? '\x1b[33m' : '\x1b[90m';
39
+ const reset = '\x1b[0m';
40
+ const isTTY = process.stderr.isTTY;
41
+ const line = isTTY
42
+ ? `${prefix}[cc-resilient] ${level}: ${message}${reset}\n`
43
+ : `[cc-resilient] ${level}: ${message}\n`;
44
+ process.stderr.write(line);
45
+ }
46
+ }
47
+ }
48
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,MAAM,OAAO,MAAM;IACT,OAAO,CAAgB;IACvB,OAAO,CAAU;IAEzB,YAAY,OAAsB,EAAE,OAAgB;QAClD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,IAA8B;QAClD,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IACpC,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,IAA8B;QAClD,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IACpC,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,IAA8B;QACnD,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IACrC,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,IAA8B;QACnD,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,KAAa,EAAE,OAAe,EAAE,IAA8B;QAC1E,MAAM,KAAK,GAAG;YACZ,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,KAAK;YACL,OAAO;YACP,GAAG,IAAI;SACR,CAAC;QAEF,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,cAAc,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;QAC7D,CAAC;QAED,IAAI,IAAI,CAAC,OAAO,IAAI,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;YAC1D,MAAM,MAAM,GAAG,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC;YAC3F,MAAM,KAAK,GAAG,SAAS,CAAC;YACxB,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;YACnC,MAAM,IAAI,GAAG,KAAK;gBAChB,CAAC,CAAC,GAAG,MAAM,kBAAkB,KAAK,KAAK,OAAO,GAAG,KAAK,IAAI;gBAC1D,CAAC,CAAC,kBAAkB,KAAK,KAAK,OAAO,IAAI,CAAC;YAC5C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,18 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { ConnectionState } from './types.js';
3
+ import type { CcResilientConfig, NetworkCheckResult } from './types.js';
4
+ import type { Logger } from './logger.js';
5
+ export declare class NetworkMonitor extends EventEmitter {
6
+ private config;
7
+ private logger;
8
+ private intervalHandle;
9
+ private consecutiveFailures;
10
+ private consecutiveSuccesses;
11
+ private state;
12
+ constructor(config: Pick<CcResilientConfig, 'healthCheckIntervalMs' | 'healthCheckTimeoutMs' | 'healthCheckEndpoint' | 'offlineThreshold' | 'reconnectStabilityCount'>, logger: Logger);
13
+ start(): void;
14
+ stop(): void;
15
+ getState(): ConnectionState;
16
+ checkOnce(): Promise<NetworkCheckResult>;
17
+ private updateState;
18
+ }
@@ -0,0 +1,110 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import https from 'node:https';
3
+ import { ConnectionState } from './types.js';
4
+ export class NetworkMonitor extends EventEmitter {
5
+ config;
6
+ logger;
7
+ intervalHandle = null;
8
+ consecutiveFailures = 0;
9
+ consecutiveSuccesses = 0;
10
+ state = ConnectionState.ONLINE;
11
+ constructor(config, logger) {
12
+ super();
13
+ this.config = config;
14
+ this.logger = logger;
15
+ }
16
+ start() {
17
+ if (this.intervalHandle)
18
+ return;
19
+ this.logger.debug('Network monitor started', {
20
+ interval: this.config.healthCheckIntervalMs,
21
+ endpoint: this.config.healthCheckEndpoint,
22
+ });
23
+ this.intervalHandle = setInterval(() => {
24
+ this.checkOnce().catch((err) => {
25
+ this.logger.error('Health check error', { error: String(err) });
26
+ });
27
+ }, this.config.healthCheckIntervalMs);
28
+ }
29
+ stop() {
30
+ if (this.intervalHandle) {
31
+ clearInterval(this.intervalHandle);
32
+ this.intervalHandle = null;
33
+ this.logger.debug('Network monitor stopped');
34
+ }
35
+ }
36
+ getState() {
37
+ return this.state;
38
+ }
39
+ async checkOnce() {
40
+ const start = Date.now();
41
+ const url = new URL(this.config.healthCheckEndpoint);
42
+ const result = await new Promise((resolve) => {
43
+ const controller = new AbortController();
44
+ const timeout = setTimeout(() => controller.abort(), this.config.healthCheckTimeoutMs);
45
+ const req = https.request({
46
+ hostname: url.hostname,
47
+ port: 443,
48
+ path: '/',
49
+ method: 'HEAD',
50
+ signal: controller.signal,
51
+ }, () => {
52
+ clearTimeout(timeout);
53
+ resolve({
54
+ ok: true,
55
+ latencyMs: Date.now() - start,
56
+ timestamp: new Date().toISOString(),
57
+ });
58
+ });
59
+ req.on('error', (err) => {
60
+ clearTimeout(timeout);
61
+ resolve({
62
+ ok: false,
63
+ latencyMs: Date.now() - start,
64
+ error: err.message,
65
+ timestamp: new Date().toISOString(),
66
+ });
67
+ });
68
+ req.end();
69
+ });
70
+ this.emit('check', result);
71
+ this.updateState(result);
72
+ return result;
73
+ }
74
+ updateState(result) {
75
+ const oldState = this.state;
76
+ if (result.ok) {
77
+ this.consecutiveFailures = 0;
78
+ this.consecutiveSuccesses++;
79
+ if (this.state === ConnectionState.OFFLINE || this.state === ConnectionState.RECONNECTING) {
80
+ if (this.consecutiveSuccesses >= this.config.reconnectStabilityCount) {
81
+ this.state = ConnectionState.ONLINE;
82
+ }
83
+ else {
84
+ this.state = ConnectionState.RECONNECTING;
85
+ }
86
+ }
87
+ else {
88
+ this.state = ConnectionState.ONLINE;
89
+ }
90
+ }
91
+ else {
92
+ this.consecutiveSuccesses = 0;
93
+ this.consecutiveFailures++;
94
+ if (this.consecutiveFailures >= this.config.offlineThreshold) {
95
+ this.state = ConnectionState.OFFLINE;
96
+ }
97
+ else if (this.consecutiveFailures > 0) {
98
+ this.state = ConnectionState.DEGRADED;
99
+ }
100
+ }
101
+ if (oldState !== this.state) {
102
+ this.logger.info(`Network state: ${oldState} -> ${this.state}`, {
103
+ consecutiveFailures: this.consecutiveFailures,
104
+ consecutiveSuccesses: this.consecutiveSuccesses,
105
+ });
106
+ this.emit('stateChange', oldState, this.state);
107
+ }
108
+ }
109
+ }
110
+ //# sourceMappingURL=network-monitor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network-monitor.js","sourceRoot":"","sources":["../../src/network-monitor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { type ChildProcess } from 'node:child_process';
3
+ import type { CcResilientConfig } from './types.js';
4
+ import type { Logger } from './logger.js';
5
+ export declare class ProcessMonitor extends EventEmitter {
6
+ private config;
7
+ private logger;
8
+ private childProcess;
9
+ private lastActivityTimestamp;
10
+ private hangCheckInterval;
11
+ private isInteractive;
12
+ constructor(config: Pick<CcResilientConfig, 'processHangTimeoutMs' | 'gracefulShutdownTimeoutMs'>, logger: Logger);
13
+ spawnClaude(args: string[]): ChildProcess;
14
+ gracefulKill(): Promise<void>;
15
+ isAlive(): boolean;
16
+ getLastActivity(): number;
17
+ getPid(): number | undefined;
18
+ getIsInteractive(): boolean;
19
+ private startHangDetection;
20
+ private stopHangDetection;
21
+ }