@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.
- package/README.md +127 -341
- package/dist/channel/channel.d.ts +33 -0
- package/dist/channel/channel.d.ts.map +1 -0
- package/dist/channel/channel.js +90 -0
- package/dist/channel/channel.js.map +1 -0
- package/dist/channel/index.d.ts +13 -0
- package/dist/channel/index.d.ts.map +1 -0
- package/dist/channel/index.js +23 -0
- package/dist/channel/index.js.map +1 -0
- package/dist/channel/message-formatter.d.ts +14 -0
- package/dist/channel/message-formatter.d.ts.map +1 -0
- package/dist/channel/message-formatter.js +71 -0
- package/dist/channel/message-formatter.js.map +1 -0
- package/dist/channel/oauth-client.d.ts +15 -0
- package/dist/channel/oauth-client.d.ts.map +1 -0
- package/dist/channel/oauth-client.js +45 -0
- package/dist/channel/oauth-client.js.map +1 -0
- package/dist/channel/rest-client.d.ts +16 -0
- package/dist/channel/rest-client.d.ts.map +1 -0
- package/dist/channel/rest-client.js +66 -0
- package/dist/channel/rest-client.js.map +1 -0
- package/dist/channel/session-mapper.d.ts +14 -0
- package/dist/channel/session-mapper.d.ts.map +1 -0
- package/dist/channel/session-mapper.js +37 -0
- package/dist/channel/session-mapper.js.map +1 -0
- package/dist/channel/sse-client.d.ts +31 -0
- package/dist/channel/sse-client.d.ts.map +1 -0
- package/dist/channel/sse-client.js +171 -0
- package/dist/channel/sse-client.js.map +1 -0
- package/dist/channel/types.d.ts +65 -0
- package/dist/channel/types.d.ts.map +1 -0
- package/dist/channel/types.js +3 -0
- package/dist/channel/types.js.map +1 -0
- package/dist/config/agent-workflow.js +7 -7
- package/dist/index.d.ts +1 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -30
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/agent-config.d.ts.map +1 -1
- package/dist/utils/agent-config.js +14 -0
- package/dist/utils/agent-config.js.map +1 -1
- package/openclaw.plugin.json +25 -0
- package/package.json +66 -77
- package/templates/.astrid.config.json +60 -60
- package/templates/ASTRID.template.md +74 -74
- package/dist/bin/cli.d.ts +0 -14
- package/dist/bin/cli.d.ts.map +0 -1
- package/dist/bin/cli.js +0 -1610
- package/dist/bin/cli.js.map +0 -1
- package/dist/executors/claude.d.ts +0 -65
- package/dist/executors/claude.d.ts.map +0 -1
- package/dist/executors/claude.js +0 -838
- package/dist/executors/claude.js.map +0 -1
- package/dist/executors/gemini.d.ts +0 -23
- package/dist/executors/gemini.d.ts.map +0 -1
- package/dist/executors/gemini.js +0 -558
- package/dist/executors/gemini.js.map +0 -1
- package/dist/executors/openai.d.ts +0 -17
- package/dist/executors/openai.d.ts.map +0 -1
- package/dist/executors/openai.js +0 -614
- package/dist/executors/openai.js.map +0 -1
- package/dist/executors/shared/index.d.ts +0 -9
- package/dist/executors/shared/index.d.ts.map +0 -1
- package/dist/executors/shared/index.js +0 -21
- package/dist/executors/shared/index.js.map +0 -1
- package/dist/executors/shared/tool-executor.d.ts +0 -52
- package/dist/executors/shared/tool-executor.d.ts.map +0 -1
- package/dist/executors/shared/tool-executor.js +0 -262
- package/dist/executors/shared/tool-executor.js.map +0 -1
- package/dist/executors/shared/tool-schemas.d.ts +0 -61
- package/dist/executors/shared/tool-schemas.d.ts.map +0 -1
- package/dist/executors/shared/tool-schemas.js +0 -135
- package/dist/executors/shared/tool-schemas.js.map +0 -1
- package/dist/executors/terminal-base.d.ts +0 -207
- package/dist/executors/terminal-base.d.ts.map +0 -1
- package/dist/executors/terminal-base.js +0 -552
- package/dist/executors/terminal-base.js.map +0 -1
- package/dist/executors/terminal-claude.d.ts +0 -116
- package/dist/executors/terminal-claude.d.ts.map +0 -1
- package/dist/executors/terminal-claude.js +0 -700
- package/dist/executors/terminal-claude.js.map +0 -1
- package/dist/executors/terminal-executors.test.d.ts +0 -8
- package/dist/executors/terminal-executors.test.d.ts.map +0 -1
- package/dist/executors/terminal-executors.test.js +0 -469
- package/dist/executors/terminal-executors.test.js.map +0 -1
- package/dist/executors/terminal-gemini.d.ts +0 -50
- package/dist/executors/terminal-gemini.d.ts.map +0 -1
- package/dist/executors/terminal-gemini.js +0 -401
- package/dist/executors/terminal-gemini.js.map +0 -1
- package/dist/executors/terminal-openai.d.ts +0 -50
- package/dist/executors/terminal-openai.d.ts.map +0 -1
- package/dist/executors/terminal-openai.js +0 -405
- package/dist/executors/terminal-openai.js.map +0 -1
- package/dist/server/astrid-client.d.ts +0 -77
- package/dist/server/astrid-client.d.ts.map +0 -1
- package/dist/server/astrid-client.js +0 -125
- package/dist/server/astrid-client.js.map +0 -1
- package/dist/server/index.d.ts +0 -38
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/index.js +0 -408
- package/dist/server/index.js.map +0 -1
- package/dist/server/repo-manager.d.ts +0 -41
- package/dist/server/repo-manager.d.ts.map +0 -1
- package/dist/server/repo-manager.js +0 -177
- package/dist/server/repo-manager.js.map +0 -1
- package/dist/server/session-manager.d.ts +0 -93
- package/dist/server/session-manager.d.ts.map +0 -1
- package/dist/server/session-manager.js +0 -217
- package/dist/server/session-manager.js.map +0 -1
- package/dist/server/webhook-signature.d.ts +0 -23
- package/dist/server/webhook-signature.d.ts.map +0 -1
- package/dist/server/webhook-signature.js +0 -74
- 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
|