@gracefultools/astrid-sdk 0.7.16 → 0.8.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.
Files changed (115) hide show
  1. package/README.md +127 -341
  2. package/dist/channel/channel.d.ts +33 -0
  3. package/dist/channel/channel.d.ts.map +1 -0
  4. package/dist/channel/channel.js +90 -0
  5. package/dist/channel/channel.js.map +1 -0
  6. package/dist/channel/index.d.ts +13 -0
  7. package/dist/channel/index.d.ts.map +1 -0
  8. package/dist/channel/index.js +23 -0
  9. package/dist/channel/index.js.map +1 -0
  10. package/dist/channel/message-formatter.d.ts +14 -0
  11. package/dist/channel/message-formatter.d.ts.map +1 -0
  12. package/dist/channel/message-formatter.js +71 -0
  13. package/dist/channel/message-formatter.js.map +1 -0
  14. package/dist/channel/oauth-client.d.ts +15 -0
  15. package/dist/channel/oauth-client.d.ts.map +1 -0
  16. package/dist/channel/oauth-client.js +45 -0
  17. package/dist/channel/oauth-client.js.map +1 -0
  18. package/dist/channel/rest-client.d.ts +16 -0
  19. package/dist/channel/rest-client.d.ts.map +1 -0
  20. package/dist/channel/rest-client.js +66 -0
  21. package/dist/channel/rest-client.js.map +1 -0
  22. package/dist/channel/session-mapper.d.ts +14 -0
  23. package/dist/channel/session-mapper.d.ts.map +1 -0
  24. package/dist/channel/session-mapper.js +37 -0
  25. package/dist/channel/session-mapper.js.map +1 -0
  26. package/dist/channel/sse-client.d.ts +31 -0
  27. package/dist/channel/sse-client.d.ts.map +1 -0
  28. package/dist/channel/sse-client.js +171 -0
  29. package/dist/channel/sse-client.js.map +1 -0
  30. package/dist/channel/types.d.ts +65 -0
  31. package/dist/channel/types.d.ts.map +1 -0
  32. package/dist/channel/types.js +3 -0
  33. package/dist/channel/types.js.map +1 -0
  34. package/dist/config/agent-workflow.js +7 -7
  35. package/dist/index.d.ts +1 -8
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +16 -30
  38. package/dist/index.js.map +1 -1
  39. package/dist/types/index.d.ts +1 -1
  40. package/dist/types/index.d.ts.map +1 -1
  41. package/dist/utils/agent-config.d.ts.map +1 -1
  42. package/dist/utils/agent-config.js +14 -0
  43. package/dist/utils/agent-config.js.map +1 -1
  44. package/openclaw.plugin.json +25 -0
  45. package/package.json +66 -77
  46. package/templates/.astrid.config.json +60 -60
  47. package/templates/ASTRID.template.md +74 -74
  48. package/dist/bin/cli.d.ts +0 -14
  49. package/dist/bin/cli.d.ts.map +0 -1
  50. package/dist/bin/cli.js +0 -1610
  51. package/dist/bin/cli.js.map +0 -1
  52. package/dist/executors/claude.d.ts +0 -65
  53. package/dist/executors/claude.d.ts.map +0 -1
  54. package/dist/executors/claude.js +0 -838
  55. package/dist/executors/claude.js.map +0 -1
  56. package/dist/executors/gemini.d.ts +0 -23
  57. package/dist/executors/gemini.d.ts.map +0 -1
  58. package/dist/executors/gemini.js +0 -558
  59. package/dist/executors/gemini.js.map +0 -1
  60. package/dist/executors/openai.d.ts +0 -17
  61. package/dist/executors/openai.d.ts.map +0 -1
  62. package/dist/executors/openai.js +0 -614
  63. package/dist/executors/openai.js.map +0 -1
  64. package/dist/executors/shared/index.d.ts +0 -9
  65. package/dist/executors/shared/index.d.ts.map +0 -1
  66. package/dist/executors/shared/index.js +0 -21
  67. package/dist/executors/shared/index.js.map +0 -1
  68. package/dist/executors/shared/tool-executor.d.ts +0 -52
  69. package/dist/executors/shared/tool-executor.d.ts.map +0 -1
  70. package/dist/executors/shared/tool-executor.js +0 -262
  71. package/dist/executors/shared/tool-executor.js.map +0 -1
  72. package/dist/executors/shared/tool-schemas.d.ts +0 -61
  73. package/dist/executors/shared/tool-schemas.d.ts.map +0 -1
  74. package/dist/executors/shared/tool-schemas.js +0 -135
  75. package/dist/executors/shared/tool-schemas.js.map +0 -1
  76. package/dist/executors/terminal-base.d.ts +0 -207
  77. package/dist/executors/terminal-base.d.ts.map +0 -1
  78. package/dist/executors/terminal-base.js +0 -552
  79. package/dist/executors/terminal-base.js.map +0 -1
  80. package/dist/executors/terminal-claude.d.ts +0 -116
  81. package/dist/executors/terminal-claude.d.ts.map +0 -1
  82. package/dist/executors/terminal-claude.js +0 -700
  83. package/dist/executors/terminal-claude.js.map +0 -1
  84. package/dist/executors/terminal-executors.test.d.ts +0 -8
  85. package/dist/executors/terminal-executors.test.d.ts.map +0 -1
  86. package/dist/executors/terminal-executors.test.js +0 -469
  87. package/dist/executors/terminal-executors.test.js.map +0 -1
  88. package/dist/executors/terminal-gemini.d.ts +0 -50
  89. package/dist/executors/terminal-gemini.d.ts.map +0 -1
  90. package/dist/executors/terminal-gemini.js +0 -401
  91. package/dist/executors/terminal-gemini.js.map +0 -1
  92. package/dist/executors/terminal-openai.d.ts +0 -50
  93. package/dist/executors/terminal-openai.d.ts.map +0 -1
  94. package/dist/executors/terminal-openai.js +0 -405
  95. package/dist/executors/terminal-openai.js.map +0 -1
  96. package/dist/server/astrid-client.d.ts +0 -77
  97. package/dist/server/astrid-client.d.ts.map +0 -1
  98. package/dist/server/astrid-client.js +0 -125
  99. package/dist/server/astrid-client.js.map +0 -1
  100. package/dist/server/index.d.ts +0 -38
  101. package/dist/server/index.d.ts.map +0 -1
  102. package/dist/server/index.js +0 -408
  103. package/dist/server/index.js.map +0 -1
  104. package/dist/server/repo-manager.d.ts +0 -41
  105. package/dist/server/repo-manager.d.ts.map +0 -1
  106. package/dist/server/repo-manager.js +0 -177
  107. package/dist/server/repo-manager.js.map +0 -1
  108. package/dist/server/session-manager.d.ts +0 -93
  109. package/dist/server/session-manager.d.ts.map +0 -1
  110. package/dist/server/session-manager.js +0 -217
  111. package/dist/server/session-manager.js.map +0 -1
  112. package/dist/server/webhook-signature.d.ts +0 -23
  113. package/dist/server/webhook-signature.d.ts.map +0 -1
  114. package/dist/server/webhook-signature.js +0 -74
  115. package/dist/server/webhook-signature.js.map +0 -1
@@ -1,700 +0,0 @@
1
- "use strict";
2
- /**
3
- * Terminal Claude Executor
4
- *
5
- * Executes tasks using the local Claude Code CLI via spawn.
6
- * This enables running the astrid-agent in "terminal mode" where tasks
7
- * are processed by the local Claude Code installation instead of the API.
8
- *
9
- * Key features:
10
- * - Uses `claude --print` for non-interactive execution
11
- * - Supports session resumption via `--resume` flag
12
- * - Extracts PR URLs and git changes from output
13
- */
14
- var __importDefault = (this && this.__importDefault) || function (mod) {
15
- return (mod && mod.__esModule) ? mod : { "default": mod };
16
- };
17
- Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.terminalClaudeExecutor = exports.TerminalClaudeExecutor = exports.terminalSessionStore = void 0;
19
- const child_process_1 = require("child_process");
20
- const promises_1 = __importDefault(require("fs/promises"));
21
- const path_1 = __importDefault(require("path"));
22
- const os_1 = __importDefault(require("os"));
23
- const terminal_base_js_1 = require("./terminal-base.js");
24
- const agent_workflow_js_1 = require("../config/agent-workflow.js");
25
- /**
26
- * Simple file-based session store for terminal mode.
27
- * Stores Claude session IDs and worktree paths for resumption support.
28
- */
29
- class TerminalSessionStore {
30
- storagePath;
31
- sessions = new Map();
32
- loaded = false;
33
- constructor() {
34
- // Store in ~/.astrid-agent/sessions.json
35
- const homeDir = os_1.default.homedir();
36
- const dataDir = process.env.ASTRID_AGENT_DATA_DIR || path_1.default.join(homeDir, '.astrid-agent');
37
- this.storagePath = path_1.default.join(dataDir, 'terminal-sessions.json');
38
- }
39
- async load() {
40
- if (this.loaded)
41
- return;
42
- try {
43
- const dir = path_1.default.dirname(this.storagePath);
44
- await promises_1.default.mkdir(dir, { recursive: true });
45
- const data = await promises_1.default.readFile(this.storagePath, 'utf-8');
46
- const parsed = JSON.parse(data);
47
- // Handle migration from old format (string) to new format (StoredSession)
48
- this.sessions = new Map();
49
- for (const [key, value] of Object.entries(parsed)) {
50
- if (typeof value === 'string') {
51
- // Old format: just claudeSessionId
52
- this.sessions.set(key, { claudeSessionId: value });
53
- }
54
- else {
55
- // New format: StoredSession object
56
- this.sessions.set(key, value);
57
- }
58
- }
59
- this.loaded = true;
60
- console.log(`📂 Loaded ${this.sessions.size} terminal sessions from ${this.storagePath}`);
61
- }
62
- catch (error) {
63
- if (error.code === 'ENOENT') {
64
- this.sessions = new Map();
65
- this.loaded = true;
66
- }
67
- else {
68
- console.warn(`⚠️ Could not load terminal sessions: ${error.message}`);
69
- this.sessions = new Map();
70
- this.loaded = true;
71
- }
72
- }
73
- }
74
- async save() {
75
- try {
76
- const data = Object.fromEntries(this.sessions);
77
- const dir = path_1.default.dirname(this.storagePath);
78
- await promises_1.default.mkdir(dir, { recursive: true });
79
- await promises_1.default.writeFile(this.storagePath, JSON.stringify(data, null, 2));
80
- }
81
- catch (error) {
82
- console.warn(`⚠️ Could not save terminal sessions: ${error.message}`);
83
- }
84
- }
85
- async getSession(taskId) {
86
- await this.load();
87
- return this.sessions.get(taskId);
88
- }
89
- async getClaudeSessionId(taskId) {
90
- const session = await this.getSession(taskId);
91
- return session?.claudeSessionId;
92
- }
93
- async setClaudeSessionId(taskId, claudeSessionId) {
94
- await this.load();
95
- const existing = this.sessions.get(taskId) || {};
96
- this.sessions.set(taskId, { ...existing, claudeSessionId });
97
- await this.save();
98
- console.log(`🔗 Stored Claude session ${claudeSessionId} for task ${taskId}`);
99
- }
100
- async setWorktree(taskId, worktreePath, branchName) {
101
- await this.load();
102
- const existing = this.sessions.get(taskId) || {};
103
- this.sessions.set(taskId, { ...existing, worktreePath, branchName });
104
- await this.save();
105
- console.log(`🌳 Stored worktree path for task ${taskId}: ${worktreePath}`);
106
- }
107
- async getWorktree(taskId) {
108
- const session = await this.getSession(taskId);
109
- if (session?.worktreePath && session?.branchName) {
110
- return { path: session.worktreePath, branch: session.branchName };
111
- }
112
- return undefined;
113
- }
114
- async deleteSession(taskId) {
115
- await this.load();
116
- if (this.sessions.has(taskId)) {
117
- this.sessions.delete(taskId);
118
- await this.save();
119
- }
120
- }
121
- }
122
- // Singleton instance
123
- exports.terminalSessionStore = new TerminalSessionStore();
124
- // ============================================================================
125
- // TERMINAL CLAUDE EXECUTOR
126
- // ============================================================================
127
- class TerminalClaudeExecutor {
128
- model;
129
- maxTurns;
130
- timeout;
131
- constructor(options = {}) {
132
- // Use 'opus' alias which points to Claude Opus 4.5
133
- this.model = options.model || process.env.CLAUDE_MODEL || 'opus';
134
- this.maxTurns = options.maxTurns || parseInt(process.env.CLAUDE_MAX_TURNS || '50', 10);
135
- this.timeout = options.timeout || parseInt(process.env.CLAUDE_TIMEOUT || '900000', 10); // 15 min default
136
- }
137
- /**
138
- * Capture git diff and modified files after execution
139
- * If baseline is provided, filters out pre-existing uncommitted files
140
- */
141
- async captureGitChanges(projectPath, baseline) {
142
- const { execSync } = await import('child_process');
143
- try {
144
- // Get modified files (staged and unstaged)
145
- const statusOutput = execSync('git status --porcelain', {
146
- cwd: projectPath,
147
- encoding: 'utf-8',
148
- timeout: 10000
149
- });
150
- let files = statusOutput
151
- .split('\n')
152
- .filter(line => line.trim())
153
- .map(line => line.slice(3).trim()); // Remove status prefix
154
- // Filter out pre-existing files if baseline provided
155
- if (baseline && baseline.size > 0) {
156
- const originalCount = files.length;
157
- files = files.filter(f => !baseline.has(f));
158
- if (originalCount !== files.length) {
159
- console.log(`📊 Filtered out ${originalCount - files.length} pre-existing uncommitted files`);
160
- }
161
- }
162
- // Get diff (staged and unstaged, limited to 5000 chars)
163
- let diff = '';
164
- try {
165
- diff = execSync('git diff HEAD --no-color', {
166
- cwd: projectPath,
167
- encoding: 'utf-8',
168
- timeout: 10000,
169
- maxBuffer: 1024 * 1024 // 1MB max
170
- });
171
- // Truncate if too long
172
- if (diff.length > 5000) {
173
- diff = diff.slice(0, 5000) + '\n\n[... diff truncated ...]';
174
- }
175
- }
176
- catch {
177
- // No diff or not a git repo
178
- }
179
- console.log(`📊 Git changes: ${files.length} files modified by task`);
180
- return { diff, files };
181
- }
182
- catch {
183
- return { diff: '', files: [] };
184
- }
185
- }
186
- /**
187
- * Extract PR URL from Claude output
188
- */
189
- extractPrUrl(output) {
190
- // Match GitHub PR URLs
191
- const prUrlPatterns = [
192
- /https:\/\/github\.com\/[^\/]+\/[^\/]+\/pull\/\d+/g,
193
- /PR URL:\s*(https:\/\/[^\s]+)/i,
194
- /Pull Request:\s*(https:\/\/[^\s]+)/i
195
- ];
196
- for (const pattern of prUrlPatterns) {
197
- const match = output.match(pattern);
198
- if (match) {
199
- return match[0].replace(/PR URL:\s*/i, '').replace(/Pull Request:\s*/i, '');
200
- }
201
- }
202
- return undefined;
203
- }
204
- /**
205
- * Read project context files (CLAUDE.md, ASTRID.md)
206
- */
207
- async readProjectContext(projectPath) {
208
- const MAX_CONTEXT_CHARS = 4000;
209
- const contextFiles = ['CLAUDE.md', 'ASTRID.md', 'CODEX.md'];
210
- let context = '';
211
- for (const file of contextFiles) {
212
- try {
213
- const filePath = path_1.default.join(projectPath, file);
214
- let content = await promises_1.default.readFile(filePath, 'utf-8');
215
- if (content.length > MAX_CONTEXT_CHARS) {
216
- content = content.slice(0, MAX_CONTEXT_CHARS) + '\n\n[... truncated ...]';
217
- }
218
- context += `\n\n## Project Instructions (from ${file})\n\n${content}`;
219
- break; // Only use the first found file
220
- }
221
- catch {
222
- // File doesn't exist, try next
223
- }
224
- }
225
- return context;
226
- }
227
- /**
228
- * Format comment history for context
229
- */
230
- formatCommentHistory(comments) {
231
- if (!comments || comments.length === 0)
232
- return '';
233
- const formatted = comments
234
- .slice(-10)
235
- .map(c => `**${c.authorName}** (${new Date(c.createdAt).toLocaleString()}):\n${c.content}`)
236
- .join('\n\n---\n\n');
237
- return `\n\n## Previous Discussion\n\n${formatted}`;
238
- }
239
- /**
240
- * Build prompt from task details
241
- *
242
- * IMPORTANT: Keep prompts relatively concise. Very long prompts with complex
243
- * markdown can cause issues. Focus on essential instructions.
244
- *
245
- * NOTE: Workflow instructions are built from environment-based configuration.
246
- * See config/agent-workflow.ts for available environment variables.
247
- */
248
- async buildPrompt(session, userMessage, context) {
249
- if (userMessage) {
250
- // For follow-up messages, just return the message directly
251
- return userMessage;
252
- }
253
- const description = session.description?.trim() || '';
254
- const config = (0, agent_workflow_js_1.getAgentWorkflowConfig)();
255
- // Build workflow instructions based on configuration
256
- const workflowInstructions = (0, agent_workflow_js_1.buildWorkflowInstructions)(session.taskId, session.title, config);
257
- const prompt = `# Task: ${session.title}
258
-
259
- ${description ? `## Description\n${description}\n\n` : ''}${workflowInstructions}`;
260
- return prompt;
261
- }
262
- /**
263
- * Start a new Claude Code session with retry logic
264
- * Uses git worktree isolation when enabled (default) to protect the main working directory
265
- */
266
- async startSession(session, prompt, context, callbacks) {
267
- const taskPrompt = prompt || await this.buildPrompt(session, undefined, context);
268
- const { onProgress, onComment } = callbacks || {};
269
- // Truncate prompt if too long (avoid ARG_MAX issues)
270
- const MAX_PROMPT_LENGTH = 50000;
271
- let finalPrompt = taskPrompt;
272
- if (taskPrompt.length > MAX_PROMPT_LENGTH) {
273
- finalPrompt = taskPrompt.slice(0, MAX_PROMPT_LENGTH) + '\n\n[... prompt truncated ...]';
274
- }
275
- console.log(`🚀 Starting new Claude Code session for task: ${session.title}`);
276
- // Determine working directory - use worktree if enabled
277
- const useWorktree = (0, terminal_base_js_1.shouldUseWorktree)() && session.projectPath;
278
- let workingPath = session.projectPath || process.cwd();
279
- let worktreeResult;
280
- if (useWorktree) {
281
- try {
282
- console.log(`🌳 Creating isolated worktree for task...`);
283
- worktreeResult = await (0, terminal_base_js_1.createWorktree)(session.projectPath, session.taskId);
284
- workingPath = worktreeResult.worktreePath;
285
- // Store worktree info for resumption
286
- await exports.terminalSessionStore.setWorktree(session.taskId, worktreeResult.worktreePath, worktreeResult.branchName);
287
- console.log(`📁 Working directory (worktree): ${workingPath}`);
288
- }
289
- catch (error) {
290
- console.error(`⚠️ Failed to create worktree, falling back to main directory:`, error);
291
- workingPath = session.projectPath || process.cwd();
292
- console.log(`📁 Working directory (fallback): ${workingPath}`);
293
- }
294
- }
295
- else {
296
- console.log(`📁 Working directory: ${workingPath}`);
297
- }
298
- // Create a modified session with the working path
299
- const workingSession = {
300
- ...session,
301
- projectPath: workingPath,
302
- };
303
- // Capture git baseline BEFORE execution to filter out pre-existing files
304
- let gitBaseline;
305
- const { captureGitBaseline } = await import('./terminal-base.js');
306
- gitBaseline = await captureGitBaseline(workingPath);
307
- // Retry logic for intermittent Claude Code hangs
308
- const MAX_RETRIES = 3;
309
- let lastError;
310
- let finalResult;
311
- try {
312
- for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
313
- if (attempt > 1) {
314
- const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
315
- console.log(`🔄 Retry attempt ${attempt}/${MAX_RETRIES} after ${delay}ms delay...`);
316
- await new Promise(resolve => setTimeout(resolve, delay));
317
- }
318
- // Use stdin for prompt - more reliable than command-line args for long prompts
319
- const args = [
320
- '--print',
321
- '--model', this.model,
322
- '--output-format', 'text',
323
- '--dangerously-skip-permissions',
324
- ];
325
- const result = await this.runClaude(args, workingSession, { onProgress, onComment }, finalPrompt);
326
- // Check if we got actual output (not a timeout/hang)
327
- if (result.stdout.length > 0 || result.exitCode === 0) {
328
- // Store session ID for resumption
329
- if (result.sessionId) {
330
- await exports.terminalSessionStore.setClaudeSessionId(session.taskId, result.sessionId);
331
- }
332
- // Capture git changes from working directory
333
- const changes = await this.captureGitChanges(workingPath);
334
- result.gitDiff = changes.diff;
335
- result.modifiedFiles = changes.files;
336
- // If using worktree, push changes and create PR
337
- if (worktreeResult && changes.files.length > 0) {
338
- console.log(`\n📤 Pushing changes from worktree...`);
339
- const prUrl = await (0, terminal_base_js_1.pushWorktreeChanges)(worktreeResult.worktreePath, worktreeResult.branchName, session.title);
340
- if (prUrl) {
341
- result.prUrl = prUrl;
342
- }
343
- }
344
- // Extract PR URL from output if not already set
345
- if (!result.prUrl) {
346
- result.prUrl = this.extractPrUrl(result.stdout);
347
- }
348
- finalResult = result;
349
- break;
350
- }
351
- console.log(`⚠️ Attempt ${attempt} failed (no output received)`);
352
- lastError = new Error(`No output received from Claude Code (attempt ${attempt})`);
353
- }
354
- }
355
- finally {
356
- // Cleanup worktree if it was created
357
- if (worktreeResult) {
358
- try {
359
- await worktreeResult.cleanup();
360
- }
361
- catch (cleanupError) {
362
- console.error(`⚠️ Worktree cleanup failed:`, cleanupError);
363
- }
364
- }
365
- }
366
- if (finalResult) {
367
- return finalResult;
368
- }
369
- // All retries failed, return empty result
370
- console.error(`❌ All ${MAX_RETRIES} attempts failed`);
371
- return {
372
- exitCode: -1,
373
- stdout: '',
374
- stderr: lastError?.message || 'All retry attempts failed',
375
- };
376
- }
377
- /**
378
- * Resume an existing Claude Code session with retry logic
379
- * Uses stored worktree path if available
380
- */
381
- async resumeSession(session, input, context, callbacks) {
382
- const { onProgress, onComment } = callbacks || {};
383
- // Get stored Claude session ID and worktree info
384
- const claudeSessionId = await exports.terminalSessionStore.getClaudeSessionId(session.taskId);
385
- const storedWorktree = await exports.terminalSessionStore.getWorktree(session.taskId);
386
- if (!claudeSessionId) {
387
- console.log(`⚠️ No stored session for task ${session.taskId}, starting new session`);
388
- return this.startSession(session, input, context, callbacks);
389
- }
390
- const promptWithContext = await this.buildPrompt(session, input, context);
391
- // Determine working path - use stored worktree if available and still exists
392
- let workingPath = session.projectPath || process.cwd();
393
- let useStoredWorktree = false;
394
- if (storedWorktree) {
395
- try {
396
- const fsModule = await import('fs/promises');
397
- await fsModule.access(storedWorktree.path);
398
- workingPath = storedWorktree.path;
399
- useStoredWorktree = true;
400
- console.log(`🔄 Resuming Claude Code session ${claudeSessionId}`);
401
- console.log(`🌳 Using stored worktree: ${workingPath}`);
402
- }
403
- catch {
404
- console.log(`⚠️ Stored worktree no longer exists, using main directory`);
405
- }
406
- }
407
- else {
408
- console.log(`🔄 Resuming Claude Code session ${claudeSessionId}`);
409
- console.log(`📁 Working directory: ${workingPath}`);
410
- }
411
- // Create a modified session with the working path
412
- const workingSession = {
413
- ...session,
414
- projectPath: workingPath,
415
- };
416
- // Capture git baseline BEFORE execution to filter out pre-existing files
417
- const { captureGitBaseline } = await import('./terminal-base.js');
418
- const gitBaseline = await captureGitBaseline(workingPath);
419
- // Retry logic for intermittent Claude Code hangs
420
- const MAX_RETRIES = 3;
421
- let lastError;
422
- for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
423
- if (attempt > 1) {
424
- const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
425
- console.log(`🔄 Retry attempt ${attempt}/${MAX_RETRIES} after ${delay}ms delay...`);
426
- await new Promise(resolve => setTimeout(resolve, delay));
427
- }
428
- // Use stdin for prompt - more reliable than command-line args
429
- const args = [
430
- '--print',
431
- '--resume', claudeSessionId,
432
- '--output-format', 'text',
433
- '--dangerously-skip-permissions',
434
- ];
435
- const result = await this.runClaude(args, workingSession, { onProgress, onComment }, promptWithContext);
436
- // Check if we got actual output
437
- if (result.stdout.length > 0 || result.exitCode === 0) {
438
- // Update session ID if we got a new one
439
- if (result.sessionId) {
440
- await exports.terminalSessionStore.setClaudeSessionId(session.taskId, result.sessionId);
441
- }
442
- // Capture git changes (filtering out pre-existing files using baseline)
443
- const changes = await this.captureGitChanges(workingPath, gitBaseline);
444
- result.gitDiff = changes.diff;
445
- result.modifiedFiles = changes.files;
446
- // If using worktree and have changes, push them
447
- if (useStoredWorktree && storedWorktree && changes.files.length > 0) {
448
- console.log(`\n📤 Pushing changes from worktree...`);
449
- const prUrl = await (0, terminal_base_js_1.pushWorktreeChanges)(storedWorktree.path, storedWorktree.branch, session.title);
450
- if (prUrl) {
451
- result.prUrl = prUrl;
452
- }
453
- }
454
- // Extract PR URL from output if not already set
455
- if (!result.prUrl) {
456
- result.prUrl = this.extractPrUrl(result.stdout);
457
- }
458
- return result;
459
- }
460
- console.log(`⚠️ Attempt ${attempt} failed (no output received)`);
461
- lastError = new Error(`No output received from Claude Code (attempt ${attempt})`);
462
- }
463
- // All retries failed
464
- console.error(`❌ All ${MAX_RETRIES} attempts failed`);
465
- return {
466
- exitCode: -1,
467
- stdout: '',
468
- stderr: lastError?.message || 'All retry attempts failed',
469
- };
470
- }
471
- /**
472
- * Execute Claude Code CLI
473
- *
474
- * Uses stdin for prompt input instead of command-line args to avoid
475
- * issues with long prompts, special characters, and newlines.
476
- *
477
- * Parses output in real-time to detect plans, questions, and progress,
478
- * posting them as comments to the task via the onComment callback.
479
- */
480
- runClaude(args, session, callbacks, stdinInput) {
481
- const { onProgress, onComment } = callbacks || {};
482
- return new Promise((resolve, reject) => {
483
- let stdout = '';
484
- let stderr = '';
485
- let extractedSessionId;
486
- let lastProgressTime = Date.now();
487
- let lastOutputTime = Date.now();
488
- let heartbeatInterval = null;
489
- let initialTimeoutHandle = null;
490
- // Create parser state for detecting plans/questions
491
- const parserState = (0, terminal_base_js_1.createParserState)();
492
- // Initial timeout: Claude CLI needs time to load context
493
- // Complex tasks may take 60-90s before producing output
494
- const INITIAL_TIMEOUT = 180000; // 3 minutes for first output
495
- const STALL_TIMEOUT = 300000; // 5 minutes of no output = stalled
496
- // Log command (without prompt for readability)
497
- console.log(`🤖 Running: claude ${args.join(' ')}`);
498
- if (stdinInput) {
499
- console.log(`📝 Prompt via stdin: ${stdinInput.length} chars (first 100: ${stdinInput.slice(0, 100).replace(/\n/g, '\\n')}...)`);
500
- }
501
- console.log(`⏱️ Timeouts: initial=${INITIAL_TIMEOUT / 1000}s, stall=${STALL_TIMEOUT / 1000}s, max=${this.timeout / 1000}s`);
502
- // Prepare environment with required tokens
503
- const env = {
504
- ...process.env,
505
- CLAUDE_CODE_ENTRYPOINT: 'cli',
506
- };
507
- // Ensure GH_TOKEN is set for gh CLI (it prefers GH_TOKEN over GITHUB_TOKEN)
508
- if (process.env.GITHUB_TOKEN && !process.env.GH_TOKEN) {
509
- env.GH_TOKEN = process.env.GITHUB_TOKEN;
510
- }
511
- const proc = (0, child_process_1.spawn)('claude', args, {
512
- cwd: session.projectPath || process.cwd(),
513
- env,
514
- stdio: [stdinInput ? 'pipe' : 'ignore', 'pipe', 'pipe']
515
- });
516
- console.log(`🚀 Claude process spawned with PID: ${proc.pid}`);
517
- if (!proc.pid) {
518
- reject(new Error('Failed to spawn Claude process'));
519
- return;
520
- }
521
- // Write prompt to stdin if provided
522
- if (stdinInput && proc.stdin) {
523
- console.log(`📤 Writing prompt to stdin...`);
524
- proc.stdin.write(stdinInput);
525
- proc.stdin.end();
526
- console.log(`✅ Stdin closed`);
527
- }
528
- // Heartbeat: log status every 30 seconds
529
- heartbeatInterval = setInterval(() => {
530
- const elapsed = Math.round((Date.now() - lastOutputTime) / 1000);
531
- const totalElapsed = Math.round((Date.now() - lastProgressTime) / 1000);
532
- console.log(`💓 Heartbeat: ${totalElapsed}s elapsed, last output ${elapsed}s ago, stdout=${stdout.length} chars`);
533
- // Check for stall
534
- if (Date.now() - lastOutputTime > STALL_TIMEOUT) {
535
- console.error(`❌ Process stalled - no output for ${STALL_TIMEOUT / 1000}s, killing`);
536
- proc.kill('SIGTERM');
537
- }
538
- }, 30000);
539
- // Initial timeout: if no output within timeout, something is wrong
540
- initialTimeoutHandle = setTimeout(() => {
541
- if (stdout.length === 0 && stderr.length === 0) {
542
- console.error(`❌ No output received within ${INITIAL_TIMEOUT / 1000}s, killing process`);
543
- proc.kill('SIGTERM');
544
- }
545
- }, INITIAL_TIMEOUT);
546
- const cleanup = () => {
547
- if (heartbeatInterval)
548
- clearInterval(heartbeatInterval);
549
- if (initialTimeoutHandle)
550
- clearTimeout(initialTimeoutHandle);
551
- };
552
- if (!proc.stdout || !proc.stderr) {
553
- cleanup();
554
- reject(new Error('Failed to get stdout/stderr pipes from Claude process'));
555
- return;
556
- }
557
- proc.stdout.on('data', (data) => {
558
- const chunk = data.toString();
559
- stdout += chunk;
560
- lastOutputTime = Date.now();
561
- process.stdout.write(chunk); // Echo to console
562
- // Clear initial timeout once we get output
563
- if (initialTimeoutHandle) {
564
- clearTimeout(initialTimeoutHandle);
565
- initialTimeoutHandle = null;
566
- }
567
- // Parse for session ID
568
- const lines = chunk.split('\n').filter(l => l.trim());
569
- for (const line of lines) {
570
- try {
571
- const json = JSON.parse(line);
572
- if (json.session_id) {
573
- extractedSessionId = json.session_id;
574
- console.log(`🔑 Extracted session ID: ${extractedSessionId}`);
575
- }
576
- if (json.type === 'system' && json.session_id) {
577
- extractedSessionId = json.session_id;
578
- }
579
- // Progress updates from JSON
580
- if (onProgress && Date.now() - lastProgressTime > 30000) {
581
- if (json.type === 'assistant' && json.message) {
582
- onProgress(`Working... ${json.message.slice(0, 200)}`);
583
- lastProgressTime = Date.now();
584
- }
585
- }
586
- }
587
- catch {
588
- // Not JSON, try regex patterns
589
- const sessionMatch = line.match(/Session ID:\s*([a-f0-9-]+)/i);
590
- if (sessionMatch) {
591
- extractedSessionId = sessionMatch[1];
592
- }
593
- const contextMatch = line.match(/"session_id":\s*"([a-f0-9-]+)"/i);
594
- if (contextMatch) {
595
- extractedSessionId = contextMatch[1];
596
- }
597
- }
598
- }
599
- // Parse chunk for plans, questions, and progress to post as comments
600
- if (onComment) {
601
- const detected = (0, terminal_base_js_1.parseOutputChunk)(chunk, parserState);
602
- for (const item of detected) {
603
- const comment = (0, terminal_base_js_1.formatContentAsComment)(item, 'Claude');
604
- console.log(`📤 Posting ${item.type} comment to task...`);
605
- onComment(comment).catch(err => {
606
- console.error(`⚠️ Failed to post ${item.type} comment:`, err);
607
- });
608
- // Also call onProgress for detected items
609
- if (onProgress && item.type === 'progress') {
610
- onProgress(item.content);
611
- }
612
- }
613
- }
614
- });
615
- proc.stderr.on('data', (data) => {
616
- const chunk = data.toString();
617
- stderr += chunk;
618
- lastOutputTime = Date.now();
619
- console.error(`⚠️ stderr: ${chunk}`);
620
- });
621
- proc.on('close', (code) => {
622
- cleanup();
623
- console.log(`✅ Claude Code exited with code ${code}`);
624
- console.log(`📊 Final stats: stdout=${stdout.length} chars, stderr=${stderr.length} chars`);
625
- resolve({
626
- exitCode: code,
627
- stdout,
628
- stderr,
629
- sessionId: extractedSessionId
630
- });
631
- });
632
- proc.on('error', (error) => {
633
- cleanup();
634
- console.error(`❌ Claude Code execution error:`, error);
635
- reject(error);
636
- });
637
- // Max timeout
638
- setTimeout(() => {
639
- if (!proc.killed) {
640
- cleanup();
641
- console.log(`⏰ Max timeout (${this.timeout / 1000}s) reached, killing Claude Code process`);
642
- proc.kill('SIGTERM');
643
- }
644
- }, this.timeout);
645
- });
646
- }
647
- /**
648
- * Parse Claude Code output to extract key information
649
- */
650
- parseOutput(output) {
651
- const result = {};
652
- const lines = output.split('\n');
653
- const files = [];
654
- for (const line of lines) {
655
- // Extract modified/created files
656
- const fileMatch = line.match(/(?:modified|created|edited|wrote):\s*[`'"]*([^`'"]+)[`'"]*/i);
657
- if (fileMatch) {
658
- files.push(fileMatch[1].trim());
659
- }
660
- // Extract PR URL
661
- const prMatch = line.match(/(https:\/\/github\.com\/[\w-]+\/[\w-]+\/pull\/\d+)/i);
662
- if (prMatch) {
663
- result.prUrl = prMatch[1];
664
- }
665
- // Extract error messages
666
- if (line.toLowerCase().includes('error:') || line.toLowerCase().includes('failed:')) {
667
- result.error = line.trim();
668
- }
669
- }
670
- if (files.length > 0) {
671
- result.files = [...new Set(files)];
672
- }
673
- // Try to extract summary (last substantial paragraph)
674
- const paragraphs = output.split(/\n\n+/).filter(p => p.trim().length > 50);
675
- if (paragraphs.length > 0) {
676
- result.summary = paragraphs[paragraphs.length - 1].trim().slice(0, 500);
677
- }
678
- return result;
679
- }
680
- /**
681
- * Check if Claude Code CLI is available
682
- */
683
- async checkAvailable() {
684
- return new Promise((resolve) => {
685
- const proc = (0, child_process_1.spawn)('claude', ['--version'], {
686
- timeout: 5000
687
- });
688
- proc.on('close', (code) => {
689
- resolve(code === 0);
690
- });
691
- proc.on('error', () => {
692
- resolve(false);
693
- });
694
- });
695
- }
696
- }
697
- exports.TerminalClaudeExecutor = TerminalClaudeExecutor;
698
- // Export singleton instance
699
- exports.terminalClaudeExecutor = new TerminalClaudeExecutor();
700
- //# sourceMappingURL=terminal-claude.js.map