@yu_robotics/remote-cli 1.0.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 +21 -0
- package/README.md +94 -0
- package/bin/remote-cli.js +2 -0
- package/dist/client/MessageHandler.d.ts +92 -0
- package/dist/client/MessageHandler.d.ts.map +1 -0
- package/dist/client/MessageHandler.js +496 -0
- package/dist/client/MessageHandler.js.map +1 -0
- package/dist/client/WebSocketClient.d.ts +109 -0
- package/dist/client/WebSocketClient.d.ts.map +1 -0
- package/dist/client/WebSocketClient.js +234 -0
- package/dist/client/WebSocketClient.js.map +1 -0
- package/dist/commands/config.d.ts +35 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +195 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/init.d.ts +25 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +112 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/start.d.ts +20 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +108 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/status.d.ts +37 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +71 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/stop.d.ts +23 -0
- package/dist/commands/stop.d.ts.map +1 -0
- package/dist/commands/stop.js +52 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/config/ConfigManager.d.ts +109 -0
- package/dist/config/ConfigManager.d.ts.map +1 -0
- package/dist/config/ConfigManager.js +262 -0
- package/dist/config/ConfigManager.js.map +1 -0
- package/dist/executor/ClaudeExecutor.d.ts +89 -0
- package/dist/executor/ClaudeExecutor.d.ts.map +1 -0
- package/dist/executor/ClaudeExecutor.js +365 -0
- package/dist/executor/ClaudeExecutor.js.map +1 -0
- package/dist/executor/ClaudePersistentExecutor.d.ts +175 -0
- package/dist/executor/ClaudePersistentExecutor.d.ts.map +1 -0
- package/dist/executor/ClaudePersistentExecutor.js +958 -0
- package/dist/executor/ClaudePersistentExecutor.js.map +1 -0
- package/dist/executor/index.d.ts +20 -0
- package/dist/executor/index.d.ts.map +1 -0
- package/dist/executor/index.js +48 -0
- package/dist/executor/index.js.map +1 -0
- package/dist/hooks/ClaudeCodeHooks.d.ts +281 -0
- package/dist/hooks/ClaudeCodeHooks.d.ts.map +1 -0
- package/dist/hooks/ClaudeCodeHooks.js +350 -0
- package/dist/hooks/ClaudeCodeHooks.js.map +1 -0
- package/dist/hooks/FeishuNotificationAdapter.d.ts +87 -0
- package/dist/hooks/FeishuNotificationAdapter.d.ts.map +1 -0
- package/dist/hooks/FeishuNotificationAdapter.js +280 -0
- package/dist/hooks/FeishuNotificationAdapter.js.map +1 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +10 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +333 -0
- package/dist/index.js.map +1 -0
- package/dist/security/DirectoryGuard.d.ts +54 -0
- package/dist/security/DirectoryGuard.d.ts.map +1 -0
- package/dist/security/DirectoryGuard.js +143 -0
- package/dist/security/DirectoryGuard.js.map +1 -0
- package/dist/types/config.d.ts +46 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +22 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/index.d.ts +110 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/FeishuMessageFormatter.d.ts +84 -0
- package/dist/utils/FeishuMessageFormatter.d.ts.map +1 -0
- package/dist/utils/FeishuMessageFormatter.js +395 -0
- package/dist/utils/FeishuMessageFormatter.js.map +1 -0
- package/dist/utils/stripAnsi.d.ts +21 -0
- package/dist/utils/stripAnsi.d.ts.map +1 -0
- package/dist/utils/stripAnsi.js +30 -0
- package/dist/utils/stripAnsi.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ClaudePersistentExecutor = void 0;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const ClaudeCodeHooks_1 = require("../hooks/ClaudeCodeHooks");
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const events_1 = require("events");
|
|
13
|
+
/**
|
|
14
|
+
* Get Claude's global sessions directory
|
|
15
|
+
*/
|
|
16
|
+
function getClaudeSessionsDir() {
|
|
17
|
+
const homeDir = os_1.default.homedir();
|
|
18
|
+
const possiblePaths = [
|
|
19
|
+
path_1.default.join(homeDir, '.claude', 'sessions'),
|
|
20
|
+
path_1.default.join(homeDir, '.config', 'claude', 'sessions'),
|
|
21
|
+
path_1.default.join(homeDir, 'Library', 'Application Support', 'Claude', 'sessions'),
|
|
22
|
+
];
|
|
23
|
+
for (const dir of possiblePaths) {
|
|
24
|
+
if (fs_1.default.existsSync(dir)) {
|
|
25
|
+
return dir;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Claude Persistent Executor
|
|
32
|
+
*
|
|
33
|
+
* Maintains a long-running Claude process and communicates via stdin/stdout
|
|
34
|
+
* using stream-json format for real-time bidirectional communication.
|
|
35
|
+
*/
|
|
36
|
+
class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
37
|
+
directoryGuard;
|
|
38
|
+
currentWorkingDirectory;
|
|
39
|
+
isDestroyed = false;
|
|
40
|
+
defaultTimeout = 600000; // 10 minutes default, but will extend on activity
|
|
41
|
+
sessionId = null;
|
|
42
|
+
sessionFilePath;
|
|
43
|
+
// Persistent process
|
|
44
|
+
claudeProcess = null;
|
|
45
|
+
isStarting = false;
|
|
46
|
+
isStopping = false; // Flag to indicate intentional process stop
|
|
47
|
+
commandQueue = [];
|
|
48
|
+
isProcessing = false;
|
|
49
|
+
// Output handling
|
|
50
|
+
currentOutputBuffer = [];
|
|
51
|
+
currentStreamCallback;
|
|
52
|
+
currentToolUseCallback;
|
|
53
|
+
currentToolResultCallback;
|
|
54
|
+
currentCommandResolve;
|
|
55
|
+
currentCommandReject;
|
|
56
|
+
currentTimeoutTimer;
|
|
57
|
+
// Structured content collection for rich formatting
|
|
58
|
+
structuredContentBlocks = [];
|
|
59
|
+
currentStructuredCallback;
|
|
60
|
+
// Current task context for hooks
|
|
61
|
+
currentTaskId = null;
|
|
62
|
+
currentTaskStartTime = 0;
|
|
63
|
+
// Interactive input handling
|
|
64
|
+
isWaitingForInput = false;
|
|
65
|
+
inputRequestCallbacks = [];
|
|
66
|
+
inputDetectionTimer;
|
|
67
|
+
lastOutputTime = 0;
|
|
68
|
+
// Activity tracking for timeout extension (optional, can be disabled)
|
|
69
|
+
activityTrackingEnabled = false;
|
|
70
|
+
constructor(directoryGuard) {
|
|
71
|
+
super();
|
|
72
|
+
this.directoryGuard = directoryGuard;
|
|
73
|
+
this.currentWorkingDirectory = process.cwd();
|
|
74
|
+
this.sessionFilePath = path_1.default.join(this.currentWorkingDirectory, '.claude-session');
|
|
75
|
+
this.loadSessionId();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Load session ID from file if exists
|
|
79
|
+
*/
|
|
80
|
+
loadSessionId() {
|
|
81
|
+
try {
|
|
82
|
+
if (fs_1.default.existsSync(this.sessionFilePath)) {
|
|
83
|
+
const data = fs_1.default.readFileSync(this.sessionFilePath, 'utf-8');
|
|
84
|
+
const session = JSON.parse(data);
|
|
85
|
+
if (session.id) {
|
|
86
|
+
this.sessionId = session.id;
|
|
87
|
+
console.log(`[ClaudePersistent] Loaded session ID: ${this.sessionId}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.error('[ClaudePersistent] Failed to load session ID:', error);
|
|
93
|
+
this.sessionId = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Save session ID to file
|
|
98
|
+
*/
|
|
99
|
+
saveSessionId(sessionId) {
|
|
100
|
+
try {
|
|
101
|
+
const data = JSON.stringify({
|
|
102
|
+
id: sessionId,
|
|
103
|
+
savedAt: new Date().toISOString(),
|
|
104
|
+
});
|
|
105
|
+
fs_1.default.writeFileSync(this.sessionFilePath, data, 'utf-8');
|
|
106
|
+
console.log(`[ClaudePersistent] Saved session ID: ${sessionId}`);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
console.error('[ClaudePersistent] Failed to save session ID:', error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get recent session ID from Claude's sessions directory
|
|
114
|
+
*/
|
|
115
|
+
async getRecentSessionId() {
|
|
116
|
+
try {
|
|
117
|
+
const sessionsDir = getClaudeSessionsDir();
|
|
118
|
+
if (!sessionsDir) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const entries = fs_1.default.readdirSync(sessionsDir, { withFileTypes: true });
|
|
122
|
+
const sessionDirs = entries
|
|
123
|
+
.filter(entry => entry.isDirectory())
|
|
124
|
+
.map(entry => {
|
|
125
|
+
const sessionPath = path_1.default.join(sessionsDir, entry.name);
|
|
126
|
+
const stats = fs_1.default.statSync(sessionPath);
|
|
127
|
+
return {
|
|
128
|
+
id: entry.name,
|
|
129
|
+
mtime: stats.mtime,
|
|
130
|
+
};
|
|
131
|
+
})
|
|
132
|
+
.filter(session => {
|
|
133
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
134
|
+
return uuidPattern.test(session.id);
|
|
135
|
+
})
|
|
136
|
+
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
137
|
+
if (sessionDirs.length === 0) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return sessionDirs[0].id;
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
console.error('[ClaudePersistent] Failed to get recent session ID:', error);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Get current working directory
|
|
149
|
+
*/
|
|
150
|
+
getCurrentWorkingDirectory() {
|
|
151
|
+
return this.currentWorkingDirectory;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Set working directory
|
|
155
|
+
*/
|
|
156
|
+
setWorkingDirectory(targetPath) {
|
|
157
|
+
const resolvedPath = this.directoryGuard.resolveWorkingDirectory(targetPath, this.currentWorkingDirectory);
|
|
158
|
+
// If directory changes, we need to restart the process
|
|
159
|
+
const needsRestart = this.currentWorkingDirectory !== resolvedPath && this.claudeProcess !== null;
|
|
160
|
+
this.currentWorkingDirectory = resolvedPath;
|
|
161
|
+
this.sessionFilePath = path_1.default.join(this.currentWorkingDirectory, '.claude-session');
|
|
162
|
+
this.loadSessionId();
|
|
163
|
+
if (needsRestart) {
|
|
164
|
+
console.log('[ClaudePersistent] Working directory changed, restarting process...');
|
|
165
|
+
this.stopProcess().then(() => this.startProcess());
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Start the persistent Claude process
|
|
170
|
+
*/
|
|
171
|
+
async startProcess() {
|
|
172
|
+
if (this.claudeProcess || this.isStarting) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.isStarting = true;
|
|
176
|
+
try {
|
|
177
|
+
// If no session ID, try to get recent one
|
|
178
|
+
if (!this.sessionId) {
|
|
179
|
+
console.log('[ClaudePersistent] No session ID, checking for recent sessions...');
|
|
180
|
+
this.sessionId = await this.getRecentSessionId();
|
|
181
|
+
if (this.sessionId) {
|
|
182
|
+
this.saveSessionId(this.sessionId);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Build arguments
|
|
186
|
+
// Note: --output-format=stream-json requires --verbose
|
|
187
|
+
const args = [
|
|
188
|
+
'--input-format=stream-json',
|
|
189
|
+
'--output-format=stream-json',
|
|
190
|
+
'--include-partial-messages',
|
|
191
|
+
'--verbose',
|
|
192
|
+
'--dangerously-skip-permissions'
|
|
193
|
+
];
|
|
194
|
+
if (this.sessionId) {
|
|
195
|
+
args.push('--resume', this.sessionId);
|
|
196
|
+
console.log(`[ClaudePersistent] Resuming session: ${this.sessionId}`);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
console.log('[ClaudePersistent] Starting new session');
|
|
200
|
+
}
|
|
201
|
+
console.log(`[ClaudePersistent] Starting: claude ${args.join(' ')}`);
|
|
202
|
+
console.log(`[ClaudePersistent] Working directory: ${this.currentWorkingDirectory}`);
|
|
203
|
+
// Spawn the process
|
|
204
|
+
const child = (0, child_process_1.spawn)('claude', args, {
|
|
205
|
+
cwd: this.currentWorkingDirectory,
|
|
206
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
207
|
+
env: {
|
|
208
|
+
...process.env,
|
|
209
|
+
FORCE_COLOR: '0',
|
|
210
|
+
// Prevent nested session error
|
|
211
|
+
CLAUDECODE: '',
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
this.claudeProcess = child;
|
|
215
|
+
// Buffer for collecting stderr on startup (for error reporting)
|
|
216
|
+
let stderrBuffer = [];
|
|
217
|
+
let isStartupPhase = true;
|
|
218
|
+
// Handle stdout (JSON stream)
|
|
219
|
+
let buffer = '';
|
|
220
|
+
child.stdout?.on('data', (data) => {
|
|
221
|
+
buffer += data.toString();
|
|
222
|
+
// Process complete JSON lines
|
|
223
|
+
const lines = buffer.split('\n');
|
|
224
|
+
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
225
|
+
for (const line of lines) {
|
|
226
|
+
if (line.trim()) {
|
|
227
|
+
this.handleOutputLine(line.trim());
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
// Handle stderr
|
|
232
|
+
child.stderr?.on('data', (data) => {
|
|
233
|
+
const text = data.toString();
|
|
234
|
+
// Collect stderr during startup for error reporting
|
|
235
|
+
if (isStartupPhase) {
|
|
236
|
+
stderrBuffer.push(text);
|
|
237
|
+
// Keep only last 20 lines to avoid memory issues
|
|
238
|
+
if (stderrBuffer.length > 20) {
|
|
239
|
+
stderrBuffer.shift();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
console.error('[ClaudePersistent stderr]', text);
|
|
243
|
+
// Forward to current stream callback if available
|
|
244
|
+
if (this.currentStreamCallback) {
|
|
245
|
+
this.currentStreamCallback(text);
|
|
246
|
+
}
|
|
247
|
+
this.currentOutputBuffer.push(text);
|
|
248
|
+
});
|
|
249
|
+
// Handle process exit
|
|
250
|
+
child.on('exit', (code, signal) => {
|
|
251
|
+
isStartupPhase = false;
|
|
252
|
+
const stderrOutput = stderrBuffer.join('');
|
|
253
|
+
stderrBuffer = []; // Clear buffer
|
|
254
|
+
if (code !== 0 && code !== null) {
|
|
255
|
+
console.error(`[ClaudePersistent] Process exited with code ${code}, signal ${signal}`);
|
|
256
|
+
if (stderrOutput) {
|
|
257
|
+
console.error('[ClaudePersistent] stderr output:\n', stderrOutput);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
console.log(`[ClaudePersistent] Process exited with code ${code}, signal ${signal}`);
|
|
262
|
+
}
|
|
263
|
+
this.claudeProcess = null;
|
|
264
|
+
// Check if this was an intentional stop (abort/reset)
|
|
265
|
+
const wasIntentionalStop = this.isStopping;
|
|
266
|
+
this.isStopping = false; // Reset the flag
|
|
267
|
+
// Reject current command if any (only for unexpected exits)
|
|
268
|
+
if (this.currentCommandReject && !wasIntentionalStop) {
|
|
269
|
+
let errorMsg = `Claude process exited unexpectedly (code: ${code})`;
|
|
270
|
+
if (stderrOutput) {
|
|
271
|
+
errorMsg += `\nstderr: ${stderrOutput.substring(0, 500)}`;
|
|
272
|
+
}
|
|
273
|
+
this.currentCommandReject(new Error(errorMsg));
|
|
274
|
+
this.resetCurrentCommand();
|
|
275
|
+
}
|
|
276
|
+
// Emit event for external handling
|
|
277
|
+
this.emit('processExit', { code, signal, intentional: wasIntentionalStop });
|
|
278
|
+
// Auto-restart if not destroyed and there are pending commands
|
|
279
|
+
// Don't auto-restart if this was an intentional stop (let processQueue handle it)
|
|
280
|
+
if (!this.isDestroyed && !wasIntentionalStop && this.commandQueue.length > 0) {
|
|
281
|
+
console.log('[ClaudePersistent] Auto-restarting process...');
|
|
282
|
+
setTimeout(() => this.startProcess(), 1000);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
// Handle process error
|
|
286
|
+
child.on('error', (error) => {
|
|
287
|
+
console.error('[ClaudePersistent] Process error:', error);
|
|
288
|
+
this.claudeProcess = null;
|
|
289
|
+
this.isStarting = false;
|
|
290
|
+
if (this.currentCommandReject) {
|
|
291
|
+
if (error.message.includes('ENOENT')) {
|
|
292
|
+
this.currentCommandReject(new Error('Claude Code CLI not found. Please ensure Claude Code is installed and available in PATH.'));
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
this.currentCommandReject(error);
|
|
296
|
+
}
|
|
297
|
+
this.resetCurrentCommand();
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
// Wait a bit for process to be ready
|
|
301
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
302
|
+
// Clear startup phase after a few seconds (stderr won't be buffered anymore)
|
|
303
|
+
setTimeout(() => {
|
|
304
|
+
isStartupPhase = false;
|
|
305
|
+
stderrBuffer = [];
|
|
306
|
+
}, 5000);
|
|
307
|
+
console.log(`[ClaudePersistent] Process started with PID: ${child.pid}`);
|
|
308
|
+
this.isStarting = false;
|
|
309
|
+
// Process any pending commands
|
|
310
|
+
this.processQueue();
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
this.isStarting = false;
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Stop the persistent Claude process
|
|
319
|
+
*/
|
|
320
|
+
async stopProcess() {
|
|
321
|
+
if (!this.claudeProcess) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
console.log('[ClaudePersistent] Stopping process...');
|
|
325
|
+
this.isStopping = true;
|
|
326
|
+
// Send EOF to stdin to gracefully close
|
|
327
|
+
this.claudeProcess.stdin?.end();
|
|
328
|
+
// Kill after timeout
|
|
329
|
+
const killTimeout = setTimeout(() => {
|
|
330
|
+
if (this.claudeProcess) {
|
|
331
|
+
console.log('[ClaudePersistent] Force killing process...');
|
|
332
|
+
this.claudeProcess.kill('SIGTERM');
|
|
333
|
+
}
|
|
334
|
+
}, 5000);
|
|
335
|
+
// Wait for process to exit
|
|
336
|
+
await new Promise(resolve => {
|
|
337
|
+
if (!this.claudeProcess) {
|
|
338
|
+
resolve();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
this.claudeProcess.on('exit', () => {
|
|
342
|
+
clearTimeout(killTimeout);
|
|
343
|
+
resolve();
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
this.claudeProcess = null;
|
|
347
|
+
console.log('[ClaudePersistent] Process stopped');
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Reset the command timeout timer when activity is detected
|
|
351
|
+
* This prevents timeout during long-running tasks with continuous output
|
|
352
|
+
*
|
|
353
|
+
* NOTE: This method is currently disabled because inactivity timeout can cause
|
|
354
|
+
* state inconsistency - the executor may timeout while Claude process is still
|
|
355
|
+
* running, leading to confusing behavior where user sees timeout error but
|
|
356
|
+
* Claude is still working. Users can use /abort to cancel long-running tasks.
|
|
357
|
+
*/
|
|
358
|
+
resetActivityTimeout() {
|
|
359
|
+
// Activity-based timeout is disabled to prevent state inconsistency.
|
|
360
|
+
// If a command hangs, users can manually abort with /abort.
|
|
361
|
+
if (!this.activityTrackingEnabled) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (!this.isProcessing || !this.currentTimeoutTimer) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
// Clear existing timer and set a new one
|
|
368
|
+
clearTimeout(this.currentTimeoutTimer);
|
|
369
|
+
this.currentTimeoutTimer = setTimeout(() => {
|
|
370
|
+
console.error('[ClaudePersistent] Command timeout due to inactivity');
|
|
371
|
+
this.completeCurrentCommand(false, 'Execution timeout: No response from Claude for 10 minutes');
|
|
372
|
+
}, this.defaultTimeout);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Handle a line of JSON output from Claude
|
|
376
|
+
*/
|
|
377
|
+
handleOutputLine(line) {
|
|
378
|
+
try {
|
|
379
|
+
// Parse message first to check type
|
|
380
|
+
const parsedMessage = JSON.parse(line);
|
|
381
|
+
// Skip logging for stream_event messages to avoid console spam
|
|
382
|
+
// These are internal protocol messages (content_block_start, content_block_delta, etc.)
|
|
383
|
+
if (parsedMessage.type !== 'stream_event') {
|
|
384
|
+
const timestamp = new Date().toISOString();
|
|
385
|
+
console.log(`[ClaudePersistent RAW ${timestamp}] ${line}`);
|
|
386
|
+
}
|
|
387
|
+
const message = parsedMessage;
|
|
388
|
+
// Reset timeout on any activity to prevent timeout during long tasks
|
|
389
|
+
this.resetActivityTimeout();
|
|
390
|
+
switch (message.type) {
|
|
391
|
+
case 'message':
|
|
392
|
+
case 'thinking':
|
|
393
|
+
const contentLength = typeof message.content === 'string' ? message.content.length : JSON.stringify(message.content).length;
|
|
394
|
+
console.log(`[ClaudePersistent] Received ${message.type} message, partial=${message.partial}, content length=${contentLength}`);
|
|
395
|
+
if (message.content) {
|
|
396
|
+
const contentStr = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
|
|
397
|
+
this.currentOutputBuffer.push(contentStr);
|
|
398
|
+
if (this.currentStreamCallback) {
|
|
399
|
+
this.currentStreamCallback(contentStr);
|
|
400
|
+
}
|
|
401
|
+
// Note: Don't complete on partial=false here
|
|
402
|
+
// The command completion should be handled by the 'result' message
|
|
403
|
+
// which is the definitive end-of-response signal
|
|
404
|
+
}
|
|
405
|
+
break;
|
|
406
|
+
case 'error':
|
|
407
|
+
console.error('[ClaudePersistent] Error from Claude:', message.content);
|
|
408
|
+
this.completeCurrentCommand(false, typeof message.content === 'string' ? message.content : 'Unknown error from Claude');
|
|
409
|
+
break;
|
|
410
|
+
case 'usage':
|
|
411
|
+
// Usage info, can be logged or ignored
|
|
412
|
+
if (message.usage) {
|
|
413
|
+
console.log(`[ClaudePersistent] Usage: ${message.usage.input_tokens} in / ${message.usage.output_tokens} out`);
|
|
414
|
+
}
|
|
415
|
+
break;
|
|
416
|
+
case 'system':
|
|
417
|
+
// System messages (e.g., init)
|
|
418
|
+
if (message.subtype === 'init' && message.session_id) {
|
|
419
|
+
console.log(`[ClaudePersistent] Session initialized: ${message.session_id}`);
|
|
420
|
+
this.sessionId = message.session_id;
|
|
421
|
+
this.saveSessionId(message.session_id);
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
case 'stream_event':
|
|
425
|
+
// Stream events are internal protocol messages, silently ignore
|
|
426
|
+
// These include content_block_start, content_block_delta, etc.
|
|
427
|
+
break;
|
|
428
|
+
case 'result':
|
|
429
|
+
// Result messages contain the final response and completion status
|
|
430
|
+
console.log(`[ClaudePersistent] Result received, subtype=${message.subtype}, has result=${!!message.result}, has error=${message.is_error}`);
|
|
431
|
+
console.log(`[ClaudePersistent] Result message FULL: ${JSON.stringify(message)}`);
|
|
432
|
+
// NOTE: Do NOT send message.result to stream callback here!
|
|
433
|
+
// The result content has already been sent via 'assistant' messages (type: 'text' blocks)
|
|
434
|
+
// Sending it again would cause duplicate display in the UI
|
|
435
|
+
// Only complete if we're not waiting for user input
|
|
436
|
+
// The command completion should happen after all content is processed
|
|
437
|
+
if (!this.isWaitingForInput) {
|
|
438
|
+
// Complete the command with success or error based on is_error flag
|
|
439
|
+
if (message.is_error) {
|
|
440
|
+
this.completeCurrentCommand(false, message.result || 'Command failed');
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
this.completeCurrentCommand(true);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
console.log('[ClaudePersistent] Result received but waiting for input, not completing yet');
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
case 'assistant':
|
|
451
|
+
// Assistant messages contain the actual response content
|
|
452
|
+
// They can be partial (streaming) or complete
|
|
453
|
+
// partial=true: streaming chunk, partial=false or undefined: complete message
|
|
454
|
+
const isPartial = message.partial === true;
|
|
455
|
+
console.log(`[ClaudePersistent] Assistant message, partial=${isPartial}`);
|
|
456
|
+
// Check for tool_use in the nested message.content array
|
|
457
|
+
const contentBlocks = message.message?.content || (Array.isArray(message.content) ? message.content : null);
|
|
458
|
+
if (contentBlocks && contentBlocks.length > 0) {
|
|
459
|
+
for (const block of contentBlocks) {
|
|
460
|
+
if (block.type === 'tool_use') {
|
|
461
|
+
console.log(`[ClaudePersistent] Tool use detected: ${block.name}, id=${block.id}`);
|
|
462
|
+
// Send structured tool use event if callback is available
|
|
463
|
+
if (this.currentToolUseCallback) {
|
|
464
|
+
this.currentToolUseCallback({
|
|
465
|
+
name: block.name || 'unknown',
|
|
466
|
+
id: block.id || 'unknown',
|
|
467
|
+
input: block.input || {}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
// Note: We no longer send text-based tool use separators.
|
|
471
|
+
// Tool use is now communicated via structured events (onToolUse callback)
|
|
472
|
+
// which are converted to Feishu Card 2.0 elements by the router.
|
|
473
|
+
// NOTE: Do NOT emit tool:afterExecution hook here!
|
|
474
|
+
// tool_use is just a REQUEST to execute the tool, not the actual execution result.
|
|
475
|
+
// The tool will be executed by Claude CLI, and we'll receive the result in a 'user' message
|
|
476
|
+
// with tool_result content. We should emit the hook when we receive tool_result.
|
|
477
|
+
}
|
|
478
|
+
else if (block.type === 'text' && block.text) {
|
|
479
|
+
// Stream text content to callback for real-time display
|
|
480
|
+
this.currentOutputBuffer.push(block.text);
|
|
481
|
+
if (this.currentStreamCallback) {
|
|
482
|
+
this.currentStreamCallback(block.text);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// Also handle simple string content (fallback)
|
|
488
|
+
if (typeof message.content === 'string' && message.content) {
|
|
489
|
+
this.currentOutputBuffer.push(message.content);
|
|
490
|
+
if (this.currentStreamCallback) {
|
|
491
|
+
this.currentStreamCallback(message.content);
|
|
492
|
+
}
|
|
493
|
+
this.startInputDetectionTimer(message.content);
|
|
494
|
+
}
|
|
495
|
+
// Check if this is the final message (stop_reason indicates completion)
|
|
496
|
+
const isComplete = message.message?.stop_reason !== null && message.message?.stop_reason !== undefined;
|
|
497
|
+
if (isComplete) {
|
|
498
|
+
console.log(`[ClaudePersistent] Assistant message complete, stop_reason=${message.message?.stop_reason}`);
|
|
499
|
+
}
|
|
500
|
+
break;
|
|
501
|
+
case 'user':
|
|
502
|
+
// User messages can contain tool_result blocks - need to process these
|
|
503
|
+
const userTimestamp = new Date().toISOString();
|
|
504
|
+
console.log(`[ClaudePersistent] User message at ${userTimestamp}, checking for tool_result...`);
|
|
505
|
+
// Check if this message contains tool results
|
|
506
|
+
const userContentBlocks = message.message?.content || (Array.isArray(message.content) ? message.content : null);
|
|
507
|
+
if (userContentBlocks && Array.isArray(userContentBlocks)) {
|
|
508
|
+
for (const block of userContentBlocks) {
|
|
509
|
+
if (block.type === 'tool_result') {
|
|
510
|
+
const isError = block.is_error === true;
|
|
511
|
+
console.log(`[ClaudePersistent] Tool result received: tool_use_id=${block.id}, is_error=${isError}`);
|
|
512
|
+
console.log(`[ClaudePersistent] Tool result full: ${JSON.stringify(message).substring(0, 500)}`);
|
|
513
|
+
// Send structured tool result event if callback is available
|
|
514
|
+
if (this.currentToolResultCallback) {
|
|
515
|
+
this.currentToolResultCallback({
|
|
516
|
+
tool_use_id: block.id || 'unknown',
|
|
517
|
+
content: block.content || '(no content)',
|
|
518
|
+
is_error: isError
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
// Note: We no longer send text-based tool result messages.
|
|
522
|
+
// Tool results are now communicated via structured events (onToolResult callback)
|
|
523
|
+
// which are converted to Feishu Card 2.0 elements by the router.
|
|
524
|
+
// Emit hook for tool execution completion (this is the ACTUAL execution result)
|
|
525
|
+
// Note: We don't have the original tool name here, but we have the tool_use_id
|
|
526
|
+
ClaudeCodeHooks_1.claudeCodeHooks.notifyToolExecuted({
|
|
527
|
+
toolName: block.id || 'unknown', // Use tool_use_id as identifier
|
|
528
|
+
params: {}, // Original params not available in tool_result
|
|
529
|
+
timestamp: Date.now(),
|
|
530
|
+
taskId: this.currentTaskId || undefined,
|
|
531
|
+
}, {
|
|
532
|
+
success: !isError,
|
|
533
|
+
result: block.content || '',
|
|
534
|
+
error: isError ? (block.content || 'Tool execution failed') : undefined,
|
|
535
|
+
duration: 0, // Duration not available
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
break;
|
|
541
|
+
default:
|
|
542
|
+
// Log unknown message types for debugging
|
|
543
|
+
console.log('[ClaudePersistent] Unknown message type:', message.type, 'Full message:', JSON.stringify(message).substring(0, 200));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
// Not valid JSON, treat as plain text output
|
|
548
|
+
this.currentOutputBuffer.push(line);
|
|
549
|
+
if (this.currentStreamCallback) {
|
|
550
|
+
this.currentStreamCallback(line + '\n');
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Start a timer to detect if user input is requested
|
|
556
|
+
* If no new output arrives within the timeout, check if the last output contains input request patterns
|
|
557
|
+
*/
|
|
558
|
+
startInputDetectionTimer(content) {
|
|
559
|
+
// Clear any existing timer
|
|
560
|
+
if (this.inputDetectionTimer) {
|
|
561
|
+
clearTimeout(this.inputDetectionTimer);
|
|
562
|
+
}
|
|
563
|
+
this.lastOutputTime = Date.now();
|
|
564
|
+
// Set a timer to check for input request after a short delay
|
|
565
|
+
this.inputDetectionTimer = setTimeout(() => {
|
|
566
|
+
if (this.isWaitingForInput || !this.isProcessing) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const output = this.currentOutputBuffer.join('');
|
|
570
|
+
if (this.isInputRequest(output)) {
|
|
571
|
+
console.log('[ClaudePersistent] Detected input request in output');
|
|
572
|
+
this.handleInputRequest(output);
|
|
573
|
+
}
|
|
574
|
+
}, 2000); // 2 second delay to wait for more output
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Handle input request by notifying user and waiting for response
|
|
578
|
+
*/
|
|
579
|
+
async handleInputRequest(output) {
|
|
580
|
+
// Extract the last line or prompt from output
|
|
581
|
+
const lines = output.trim().split('\n');
|
|
582
|
+
const lastLine = lines[lines.length - 1] || 'Please provide your response';
|
|
583
|
+
// Pause processing state
|
|
584
|
+
this.isWaitingForInput = true;
|
|
585
|
+
// Notify through hooks
|
|
586
|
+
const userInput = await this.requestInteractiveInput(lastLine);
|
|
587
|
+
if (userInput !== null) {
|
|
588
|
+
this.sendInput(userInput);
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
// User cancelled or timed out
|
|
592
|
+
console.log('[ClaudePersistent] Input request cancelled or timed out');
|
|
593
|
+
this.isWaitingForInput = false;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Check if the output indicates a request for user input
|
|
598
|
+
* This detects patterns like "Press Enter to continue", "(y/n)", etc.
|
|
599
|
+
*/
|
|
600
|
+
isInputRequest(output) {
|
|
601
|
+
const inputPatterns = [
|
|
602
|
+
/\(y\/n\?*\)/i, // (y/n) or (y/n?)
|
|
603
|
+
/\[y\/n\]/i, // [y/n]
|
|
604
|
+
/press enter to continue/i, // Press Enter to continue
|
|
605
|
+
/type 'yes' to continue/i, // Type 'yes' to continue
|
|
606
|
+
/waiting for your (response|input)/i, // Waiting for response/input
|
|
607
|
+
/please confirm/i, // Please confirm
|
|
608
|
+
/do you want to/i, // Do you want to...
|
|
609
|
+
/would you like to/i, // Would you like to...
|
|
610
|
+
/>\s*$/, // Prompt ending with >
|
|
611
|
+
/:\s*$/, // Prompt ending with :
|
|
612
|
+
];
|
|
613
|
+
return inputPatterns.some(pattern => pattern.test(output));
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Request user input through hooks
|
|
617
|
+
* Returns a promise that resolves when user provides input via sendInput()
|
|
618
|
+
*/
|
|
619
|
+
async requestInteractiveInput(prompt) {
|
|
620
|
+
console.log('[ClaudePersistent] Requesting interactive input:', prompt);
|
|
621
|
+
this.isWaitingForInput = true;
|
|
622
|
+
// Emit hook to notify user (fire and forget)
|
|
623
|
+
ClaudeCodeHooks_1.claudeCodeHooks.requestUserInput({
|
|
624
|
+
prompt,
|
|
625
|
+
type: 'text',
|
|
626
|
+
timeout: 300000, // 5 minutes timeout for input
|
|
627
|
+
}).catch(() => {
|
|
628
|
+
// Ignore errors from notification
|
|
629
|
+
});
|
|
630
|
+
// Wait for input via sendInput() or timeout
|
|
631
|
+
return new Promise((resolve) => {
|
|
632
|
+
const timeoutId = setTimeout(() => {
|
|
633
|
+
console.log('[ClaudePersistent] Input request timed out');
|
|
634
|
+
this.isWaitingForInput = false;
|
|
635
|
+
resolve(null);
|
|
636
|
+
}, 300000); // 5 minutes timeout
|
|
637
|
+
// Store callback to be called when sendInput is invoked
|
|
638
|
+
this.inputRequestCallbacks.push((input) => {
|
|
639
|
+
clearTimeout(timeoutId);
|
|
640
|
+
resolve(input);
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Send user input to the Claude process
|
|
646
|
+
* This is called by MessageHandler when user sends a message while waiting for input
|
|
647
|
+
*/
|
|
648
|
+
sendInput(input) {
|
|
649
|
+
if (!this.claudeProcess || !this.isWaitingForInput) {
|
|
650
|
+
console.log('[ClaudePersistent] Cannot send input - process not running or not waiting for input');
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
const inputMessage = {
|
|
654
|
+
type: 'user',
|
|
655
|
+
message: {
|
|
656
|
+
role: 'user',
|
|
657
|
+
content: input,
|
|
658
|
+
},
|
|
659
|
+
};
|
|
660
|
+
const inputLine = JSON.stringify(inputMessage);
|
|
661
|
+
console.log(`[ClaudePersistent] Sending user input: ${input}`);
|
|
662
|
+
this.claudeProcess.stdin?.write(inputLine + '\n');
|
|
663
|
+
this.isWaitingForInput = false;
|
|
664
|
+
// Notify any waiting callbacks (for requestInteractiveInput)
|
|
665
|
+
for (const callback of this.inputRequestCallbacks) {
|
|
666
|
+
callback(input);
|
|
667
|
+
}
|
|
668
|
+
this.inputRequestCallbacks = [];
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Check if currently waiting for user input
|
|
673
|
+
*/
|
|
674
|
+
isWaitingInput() {
|
|
675
|
+
return this.isWaitingForInput;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Complete the current command
|
|
679
|
+
*/
|
|
680
|
+
completeCurrentCommand(success, errorMessage) {
|
|
681
|
+
// Guard against double completion
|
|
682
|
+
if (!this.currentCommandResolve && !this.currentCommandReject) {
|
|
683
|
+
console.log('[ClaudePersistent] Command already completed, ignoring duplicate completion');
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
if (this.currentTimeoutTimer) {
|
|
687
|
+
clearTimeout(this.currentTimeoutTimer);
|
|
688
|
+
this.currentTimeoutTimer = undefined;
|
|
689
|
+
}
|
|
690
|
+
const output = this.currentOutputBuffer.join('');
|
|
691
|
+
console.log(`[ClaudePersistent] Completing command, success=${success}, output length=${output.length}, output preview: ${output.substring(0, 100)}...`);
|
|
692
|
+
// Emit task completion hooks
|
|
693
|
+
if (this.currentTaskId) {
|
|
694
|
+
const endTime = Date.now();
|
|
695
|
+
const duration = endTime - this.currentTaskStartTime;
|
|
696
|
+
if (success) {
|
|
697
|
+
ClaudeCodeHooks_1.claudeCodeHooks.notifyTaskCompleted({
|
|
698
|
+
taskId: this.currentTaskId,
|
|
699
|
+
description: '', // Will be populated from queue if needed
|
|
700
|
+
workingDirectory: this.currentWorkingDirectory,
|
|
701
|
+
sessionId: this.sessionId || undefined,
|
|
702
|
+
startTime: this.currentTaskStartTime,
|
|
703
|
+
}, {
|
|
704
|
+
success: true,
|
|
705
|
+
output: output.trim(),
|
|
706
|
+
endTime,
|
|
707
|
+
duration,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
ClaudeCodeHooks_1.claudeCodeHooks.notifyTaskFailed({
|
|
712
|
+
taskId: this.currentTaskId,
|
|
713
|
+
description: '',
|
|
714
|
+
workingDirectory: this.currentWorkingDirectory,
|
|
715
|
+
sessionId: this.sessionId || undefined,
|
|
716
|
+
startTime: this.currentTaskStartTime,
|
|
717
|
+
}, new Error(errorMessage || 'Command failed'));
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (success && this.currentCommandResolve) {
|
|
721
|
+
// Get session abbreviation (last 8 characters of session ID)
|
|
722
|
+
const sessionAbbr = this.sessionId ? this.sessionId.slice(-8) : undefined;
|
|
723
|
+
this.currentCommandResolve({
|
|
724
|
+
success: true,
|
|
725
|
+
output: output.trim(),
|
|
726
|
+
sessionAbbr,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
else if (this.currentCommandReject) {
|
|
730
|
+
this.currentCommandReject(new Error(errorMessage || 'Command failed'));
|
|
731
|
+
}
|
|
732
|
+
this.resetCurrentCommand();
|
|
733
|
+
// Process next command in queue
|
|
734
|
+
this.isProcessing = false;
|
|
735
|
+
this.processQueue();
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Reset current command state
|
|
739
|
+
*/
|
|
740
|
+
resetCurrentCommand() {
|
|
741
|
+
this.currentOutputBuffer = [];
|
|
742
|
+
this.currentStreamCallback = undefined;
|
|
743
|
+
this.currentToolUseCallback = undefined;
|
|
744
|
+
this.currentToolResultCallback = undefined;
|
|
745
|
+
this.currentCommandResolve = undefined;
|
|
746
|
+
this.currentCommandReject = undefined;
|
|
747
|
+
if (this.currentTimeoutTimer) {
|
|
748
|
+
clearTimeout(this.currentTimeoutTimer);
|
|
749
|
+
this.currentTimeoutTimer = undefined;
|
|
750
|
+
}
|
|
751
|
+
if (this.inputDetectionTimer) {
|
|
752
|
+
clearTimeout(this.inputDetectionTimer);
|
|
753
|
+
this.inputDetectionTimer = undefined;
|
|
754
|
+
}
|
|
755
|
+
this.isWaitingForInput = false;
|
|
756
|
+
// Note: currentTaskId and currentTaskStartTime are cleared separately after hooks
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Process the command queue
|
|
760
|
+
*/
|
|
761
|
+
processQueue() {
|
|
762
|
+
if (this.isProcessing || this.commandQueue.length === 0) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
// Ensure process is running
|
|
766
|
+
if (!this.claudeProcess) {
|
|
767
|
+
this.startProcess();
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const command = this.commandQueue.shift();
|
|
771
|
+
if (!command) {
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
this.isProcessing = true;
|
|
775
|
+
this.currentStreamCallback = command.options.onStream;
|
|
776
|
+
this.currentStructuredCallback = command.options.onStructuredContent;
|
|
777
|
+
this.currentToolUseCallback = command.options.onToolUse;
|
|
778
|
+
this.currentToolResultCallback = command.options.onToolResult;
|
|
779
|
+
this.currentCommandResolve = command.resolve;
|
|
780
|
+
this.currentCommandReject = command.reject;
|
|
781
|
+
// Reset structured content collection
|
|
782
|
+
this.structuredContentBlocks = [];
|
|
783
|
+
// Generate task ID and track start time for hooks
|
|
784
|
+
this.currentTaskId = `task_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
785
|
+
this.currentTaskStartTime = Date.now();
|
|
786
|
+
// Emit task started hook
|
|
787
|
+
ClaudeCodeHooks_1.claudeCodeHooks.notifyTaskStarted({
|
|
788
|
+
taskId: this.currentTaskId,
|
|
789
|
+
description: command.prompt,
|
|
790
|
+
workingDirectory: this.currentWorkingDirectory,
|
|
791
|
+
sessionId: this.sessionId || undefined,
|
|
792
|
+
startTime: this.currentTaskStartTime,
|
|
793
|
+
});
|
|
794
|
+
// Set timeout (only if explicitly requested via options.timeout)
|
|
795
|
+
// Note: We don't set a default timeout because it can cause state inconsistency
|
|
796
|
+
// - the executor times out but Claude process keeps running
|
|
797
|
+
// Users can cancel long-running tasks with /abort
|
|
798
|
+
if (command.options.timeout) {
|
|
799
|
+
this.currentTimeoutTimer = setTimeout(() => {
|
|
800
|
+
console.error('[ClaudePersistent] Command timeout');
|
|
801
|
+
this.completeCurrentCommand(false, 'Execution timeout exceeded');
|
|
802
|
+
}, command.options.timeout);
|
|
803
|
+
}
|
|
804
|
+
// Send the command
|
|
805
|
+
const inputMessage = {
|
|
806
|
+
type: 'user',
|
|
807
|
+
message: {
|
|
808
|
+
role: 'user',
|
|
809
|
+
content: command.prompt,
|
|
810
|
+
},
|
|
811
|
+
};
|
|
812
|
+
const inputLine = JSON.stringify(inputMessage);
|
|
813
|
+
console.log(`[ClaudePersistent] Sending command: ${command.prompt.substring(0, 100)}...`);
|
|
814
|
+
this.claudeProcess.stdin?.write(inputLine + '\n');
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Execute a command through the persistent Claude process
|
|
818
|
+
*/
|
|
819
|
+
async execute(prompt, options = {}) {
|
|
820
|
+
if (this.isDestroyed) {
|
|
821
|
+
return {
|
|
822
|
+
success: false,
|
|
823
|
+
error: 'Executor has been destroyed',
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
return new Promise((resolve, reject) => {
|
|
827
|
+
// Add to queue
|
|
828
|
+
this.commandQueue.push({
|
|
829
|
+
prompt,
|
|
830
|
+
options,
|
|
831
|
+
resolve: (result) => {
|
|
832
|
+
// Extract and save session ID if it's a new session
|
|
833
|
+
if (!this.sessionId && result.success) {
|
|
834
|
+
const uuidPattern = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
|
|
835
|
+
const matches = result.output?.match(uuidPattern);
|
|
836
|
+
if (matches && matches.length > 0) {
|
|
837
|
+
this.sessionId = matches[0];
|
|
838
|
+
this.saveSessionId(this.sessionId);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
resolve(result);
|
|
842
|
+
},
|
|
843
|
+
reject,
|
|
844
|
+
});
|
|
845
|
+
// Try to process queue
|
|
846
|
+
this.processQueue();
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Abort current command execution
|
|
851
|
+
* Stops the current process and rejects the pending command
|
|
852
|
+
*/
|
|
853
|
+
async abort() {
|
|
854
|
+
if (!this.isProcessing && !this.isWaitingForInput) {
|
|
855
|
+
console.log('[ClaudePersistent] No command is currently executing');
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
// If waiting for input, cancel the input request
|
|
859
|
+
if (this.isWaitingForInput) {
|
|
860
|
+
console.log('[ClaudePersistent] Cancelling input request...');
|
|
861
|
+
this.isWaitingForInput = false;
|
|
862
|
+
if (this.inputDetectionTimer) {
|
|
863
|
+
clearTimeout(this.inputDetectionTimer);
|
|
864
|
+
this.inputDetectionTimer = undefined;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
console.log('[ClaudePersistent] Aborting current command...');
|
|
868
|
+
// Emit task aborted hook before stopping
|
|
869
|
+
if (this.currentTaskId) {
|
|
870
|
+
ClaudeCodeHooks_1.claudeCodeHooks.notifyTaskAborted({
|
|
871
|
+
taskId: this.currentTaskId,
|
|
872
|
+
description: '',
|
|
873
|
+
workingDirectory: this.currentWorkingDirectory,
|
|
874
|
+
sessionId: this.sessionId || undefined,
|
|
875
|
+
startTime: this.currentTaskStartTime,
|
|
876
|
+
}, 'Command aborted by user via /abort');
|
|
877
|
+
}
|
|
878
|
+
// Mark as intentional stop before rejecting and stopping
|
|
879
|
+
this.isStopping = true;
|
|
880
|
+
// Reject current command if any
|
|
881
|
+
if (this.currentCommandReject) {
|
|
882
|
+
this.currentCommandReject(new Error('Command aborted by user'));
|
|
883
|
+
}
|
|
884
|
+
// Reset command state
|
|
885
|
+
this.resetCurrentCommand();
|
|
886
|
+
this.isProcessing = false;
|
|
887
|
+
this.currentTaskId = null;
|
|
888
|
+
this.currentTaskStartTime = 0;
|
|
889
|
+
// Stop the process to ensure clean state
|
|
890
|
+
await this.stopProcess();
|
|
891
|
+
console.log('[ClaudePersistent] Process stopped after abort');
|
|
892
|
+
// Clear any pending commands in queue
|
|
893
|
+
if (this.commandQueue.length > 0) {
|
|
894
|
+
console.log(`[ClaudePersistent] Clearing ${this.commandQueue.length} pending commands from queue`);
|
|
895
|
+
for (const command of this.commandQueue) {
|
|
896
|
+
command.reject(new Error('Command aborted by user'));
|
|
897
|
+
}
|
|
898
|
+
this.commandQueue = [];
|
|
899
|
+
}
|
|
900
|
+
return true;
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Reset execution context
|
|
904
|
+
*/
|
|
905
|
+
resetContext() {
|
|
906
|
+
console.log('[ClaudePersistent] Resetting session context');
|
|
907
|
+
this.sessionId = null;
|
|
908
|
+
// Stop current process
|
|
909
|
+
this.stopProcess();
|
|
910
|
+
// Clear queue
|
|
911
|
+
this.commandQueue = [];
|
|
912
|
+
this.resetCurrentCommand();
|
|
913
|
+
this.isProcessing = false;
|
|
914
|
+
// Remove session file
|
|
915
|
+
try {
|
|
916
|
+
if (fs_1.default.existsSync(this.sessionFilePath)) {
|
|
917
|
+
fs_1.default.unlinkSync(this.sessionFilePath);
|
|
918
|
+
console.log('[ClaudePersistent] Session file removed');
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
catch (error) {
|
|
922
|
+
console.error('[ClaudePersistent] Failed to remove session file:', error);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Destroy executor and cleanup
|
|
927
|
+
*/
|
|
928
|
+
async destroy() {
|
|
929
|
+
this.isDestroyed = true;
|
|
930
|
+
// Clear queue and reject pending commands
|
|
931
|
+
for (const command of this.commandQueue) {
|
|
932
|
+
command.reject(new Error('Executor has been destroyed'));
|
|
933
|
+
}
|
|
934
|
+
this.commandQueue = [];
|
|
935
|
+
if (this.currentCommandReject) {
|
|
936
|
+
this.currentCommandReject(new Error('Executor has been destroyed'));
|
|
937
|
+
this.resetCurrentCommand();
|
|
938
|
+
}
|
|
939
|
+
// Stop process
|
|
940
|
+
await this.stopProcess();
|
|
941
|
+
// Remove all listeners
|
|
942
|
+
this.removeAllListeners();
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Check if process is running
|
|
946
|
+
*/
|
|
947
|
+
isProcessRunning() {
|
|
948
|
+
return this.claudeProcess !== null && !this.claudeProcess.killed;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Get current session ID
|
|
952
|
+
*/
|
|
953
|
+
getSessionId() {
|
|
954
|
+
return this.sessionId;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
exports.ClaudePersistentExecutor = ClaudePersistentExecutor;
|
|
958
|
+
//# sourceMappingURL=ClaudePersistentExecutor.js.map
|