@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
package/dist/bin/cli.js
DELETED
|
@@ -1,1610 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
/**
|
|
4
|
-
* @gracefultools/astrid-sdk CLI
|
|
5
|
-
*
|
|
6
|
-
* Command-line interface for running the Astrid AI agent worker
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* npx astrid-agent # Start polling for tasks (API mode)
|
|
10
|
-
* npx astrid-agent --terminal # Start polling using local Claude Code CLI
|
|
11
|
-
* npx astrid-agent <taskId> # Process a specific task
|
|
12
|
-
* npx astrid-agent --help # Show help
|
|
13
|
-
*/
|
|
14
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
-
if (k2 === undefined) k2 = k;
|
|
16
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
-
}
|
|
20
|
-
Object.defineProperty(o, k2, desc);
|
|
21
|
-
}) : (function(o, m, k, k2) {
|
|
22
|
-
if (k2 === undefined) k2 = k;
|
|
23
|
-
o[k2] = m[k];
|
|
24
|
-
}));
|
|
25
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
-
}) : function(o, v) {
|
|
28
|
-
o["default"] = v;
|
|
29
|
-
});
|
|
30
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
31
|
-
var ownKeys = function(o) {
|
|
32
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
33
|
-
var ar = [];
|
|
34
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
35
|
-
return ar;
|
|
36
|
-
};
|
|
37
|
-
return ownKeys(o);
|
|
38
|
-
};
|
|
39
|
-
return function (mod) {
|
|
40
|
-
if (mod && mod.__esModule) return mod;
|
|
41
|
-
var result = {};
|
|
42
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
43
|
-
__setModuleDefault(result, mod);
|
|
44
|
-
return result;
|
|
45
|
-
};
|
|
46
|
-
})();
|
|
47
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
-
const dotenv = __importStar(require("dotenv"));
|
|
49
|
-
const path = __importStar(require("path"));
|
|
50
|
-
// Load environment variables
|
|
51
|
-
dotenv.config({ path: path.resolve(process.cwd(), '.env.local') });
|
|
52
|
-
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
|
|
53
|
-
const claude_js_1 = require("../executors/claude.js");
|
|
54
|
-
const terminal_claude_js_1 = require("../executors/terminal-claude.js");
|
|
55
|
-
const terminal_openai_js_1 = require("../executors/terminal-openai.js");
|
|
56
|
-
const terminal_gemini_js_1 = require("../executors/terminal-gemini.js");
|
|
57
|
-
const openai_js_1 = require("../executors/openai.js");
|
|
58
|
-
const gemini_js_1 = require("../executors/gemini.js");
|
|
59
|
-
const agent_config_js_1 = require("../utils/agent-config.js");
|
|
60
|
-
const astrid_oauth_js_1 = require("../adapters/astrid-oauth.js");
|
|
61
|
-
// ============================================================================
|
|
62
|
-
// CONFIGURATION
|
|
63
|
-
// ============================================================================
|
|
64
|
-
const CONFIG = {
|
|
65
|
-
// AI Provider API Keys
|
|
66
|
-
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
|
|
67
|
-
openaiApiKey: process.env.OPENAI_API_KEY,
|
|
68
|
-
geminiApiKey: process.env.GEMINI_API_KEY,
|
|
69
|
-
// GitHub
|
|
70
|
-
githubToken: process.env.GITHUB_TOKEN,
|
|
71
|
-
// Astrid OAuth
|
|
72
|
-
astridListId: process.env.ASTRID_OAUTH_LIST_ID,
|
|
73
|
-
// Worker settings
|
|
74
|
-
pollIntervalMs: parseInt(process.env.POLL_INTERVAL_MS || '30000'),
|
|
75
|
-
maxBudgetUsd: parseFloat(process.env.MAX_BUDGET_USD || '10.0'),
|
|
76
|
-
// Vercel deployment (prefer VERCEL_API_TOKEN over VERCEL_TOKEN)
|
|
77
|
-
vercelToken: process.env.VERCEL_API_TOKEN || process.env.VERCEL_TOKEN,
|
|
78
|
-
// iOS TestFlight
|
|
79
|
-
testflightPublicLink: process.env.TESTFLIGHT_PUBLIC_LINK,
|
|
80
|
-
// Terminal mode settings
|
|
81
|
-
terminalMode: process.env.ASTRID_TERMINAL_MODE === 'true',
|
|
82
|
-
defaultProjectPath: process.env.DEFAULT_PROJECT_PATH || process.cwd(),
|
|
83
|
-
// Claude terminal settings
|
|
84
|
-
claudeModel: process.env.CLAUDE_MODEL || 'opus',
|
|
85
|
-
claudeMaxTurns: parseInt(process.env.CLAUDE_MAX_TURNS || '50', 10),
|
|
86
|
-
// OpenAI terminal settings
|
|
87
|
-
openaiModel: process.env.OPENAI_MODEL || 'o4-mini',
|
|
88
|
-
openaiMaxTurns: parseInt(process.env.OPENAI_MAX_TURNS || '50', 10),
|
|
89
|
-
// Gemini terminal settings
|
|
90
|
-
geminiModel: process.env.GEMINI_MODEL || 'gemini-2.5-flash',
|
|
91
|
-
geminiMaxTurns: parseInt(process.env.GEMINI_MAX_TURNS || '50', 10),
|
|
92
|
-
};
|
|
93
|
-
function getApiKeyForService(service) {
|
|
94
|
-
switch (service) {
|
|
95
|
-
case 'claude': return CONFIG.anthropicApiKey;
|
|
96
|
-
case 'openai': return CONFIG.openaiApiKey;
|
|
97
|
-
case 'gemini': return CONFIG.geminiApiKey;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Create a terminal executor for the specified AI service.
|
|
102
|
-
* Terminal executors process tasks using local tool execution.
|
|
103
|
-
*/
|
|
104
|
-
function createTerminalExecutor(service) {
|
|
105
|
-
switch (service) {
|
|
106
|
-
case 'claude':
|
|
107
|
-
return new terminal_claude_js_1.TerminalClaudeExecutor({
|
|
108
|
-
model: CONFIG.claudeModel,
|
|
109
|
-
maxTurns: CONFIG.claudeMaxTurns,
|
|
110
|
-
});
|
|
111
|
-
case 'openai':
|
|
112
|
-
return new terminal_openai_js_1.TerminalOpenAIExecutor({
|
|
113
|
-
apiKey: CONFIG.openaiApiKey,
|
|
114
|
-
model: CONFIG.openaiModel,
|
|
115
|
-
maxTurns: CONFIG.openaiMaxTurns,
|
|
116
|
-
});
|
|
117
|
-
case 'gemini':
|
|
118
|
-
return new terminal_gemini_js_1.TerminalGeminiExecutor({
|
|
119
|
-
apiKey: CONFIG.geminiApiKey,
|
|
120
|
-
model: CONFIG.geminiModel,
|
|
121
|
-
maxTurns: CONFIG.geminiMaxTurns,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Check which terminal mode providers are available
|
|
127
|
-
*/
|
|
128
|
-
async function getAvailableTerminalProviders() {
|
|
129
|
-
const providers = [];
|
|
130
|
-
// Check Claude Code CLI
|
|
131
|
-
const claudeExecutor = new terminal_claude_js_1.TerminalClaudeExecutor();
|
|
132
|
-
providers.push({ service: 'claude', available: await claudeExecutor.checkAvailable() });
|
|
133
|
-
// Check OpenAI API key
|
|
134
|
-
const openaiExecutor = new terminal_openai_js_1.TerminalOpenAIExecutor();
|
|
135
|
-
providers.push({ service: 'openai', available: await openaiExecutor.checkAvailable() });
|
|
136
|
-
// Check Gemini API key
|
|
137
|
-
const geminiExecutor = new terminal_gemini_js_1.TerminalGeminiExecutor();
|
|
138
|
-
providers.push({ service: 'gemini', available: await geminiExecutor.checkAvailable() });
|
|
139
|
-
return providers;
|
|
140
|
-
}
|
|
141
|
-
// ============================================================================
|
|
142
|
-
// LOGGING
|
|
143
|
-
// ============================================================================
|
|
144
|
-
const logger = (level, message, meta) => {
|
|
145
|
-
const timestamp = new Date().toISOString();
|
|
146
|
-
const prefix = level === 'error' ? 'ā' : level === 'warn' ? 'ā ļø' : 'š';
|
|
147
|
-
console.log(`${timestamp} ${prefix} ${message}`, meta ? JSON.stringify(meta, null, 2) : '');
|
|
148
|
-
};
|
|
149
|
-
async function routePlanningToService(service, taskTitle, taskDescription, config) {
|
|
150
|
-
switch (service) {
|
|
151
|
-
case 'claude':
|
|
152
|
-
return (0, claude_js_1.planWithClaude)(taskTitle, taskDescription, config);
|
|
153
|
-
case 'openai':
|
|
154
|
-
return (0, openai_js_1.planWithOpenAI)(taskTitle, taskDescription, config);
|
|
155
|
-
case 'gemini':
|
|
156
|
-
return (0, gemini_js_1.planWithGemini)(taskTitle, taskDescription, config);
|
|
157
|
-
default:
|
|
158
|
-
throw new Error(`Unknown service: ${service}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
async function routeExecutionToService(service, plan, taskTitle, taskDescription, config) {
|
|
162
|
-
switch (service) {
|
|
163
|
-
case 'claude':
|
|
164
|
-
return (0, claude_js_1.executeWithClaude)(plan, taskTitle, taskDescription, config);
|
|
165
|
-
case 'openai':
|
|
166
|
-
return (0, openai_js_1.executeWithOpenAI)(plan, taskTitle, taskDescription, config);
|
|
167
|
-
case 'gemini':
|
|
168
|
-
return (0, gemini_js_1.executeWithGemini)(plan, taskTitle, taskDescription, config);
|
|
169
|
-
default:
|
|
170
|
-
throw new Error(`Unknown service: ${service}`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
// ============================================================================
|
|
174
|
-
// HELPER FUNCTIONS
|
|
175
|
-
// ============================================================================
|
|
176
|
-
/**
|
|
177
|
-
* Get assignee email from task, handling both flat and nested formats
|
|
178
|
-
* API v1 returns assignee as nested object: { assignee: { email } }
|
|
179
|
-
* Some APIs may return flat assigneeEmail field
|
|
180
|
-
*/
|
|
181
|
-
function getAssigneeEmail(task) {
|
|
182
|
-
return task.assigneeEmail || task.assignee?.email;
|
|
183
|
-
}
|
|
184
|
-
async function processTask(task, repoPath, options) {
|
|
185
|
-
const assigneeEmail = task.assigneeEmail || 'claude@astrid.cc';
|
|
186
|
-
const service = (0, agent_config_js_1.getAgentService)(assigneeEmail);
|
|
187
|
-
const apiKey = getApiKeyForService(service);
|
|
188
|
-
const onComment = options?.onComment || (async () => { });
|
|
189
|
-
if (!apiKey) {
|
|
190
|
-
throw new Error(`No API key configured for ${service}. Set ${service.toUpperCase()}_API_KEY`);
|
|
191
|
-
}
|
|
192
|
-
let iterationCount = 0;
|
|
193
|
-
const config = {
|
|
194
|
-
repoPath,
|
|
195
|
-
apiKey,
|
|
196
|
-
model: agent_config_js_1.DEFAULT_MODELS[service],
|
|
197
|
-
maxBudgetUsd: CONFIG.maxBudgetUsd,
|
|
198
|
-
maxTurns: 50,
|
|
199
|
-
maxIterations: 50,
|
|
200
|
-
logger,
|
|
201
|
-
onProgress: async (msg) => {
|
|
202
|
-
console.log(` ā ${msg}`);
|
|
203
|
-
// Post progress update every 10 iterations during execution
|
|
204
|
-
iterationCount++;
|
|
205
|
-
if (iterationCount % 10 === 0) {
|
|
206
|
-
await onComment(`š **Progress Update**\n\nIteration ${iterationCount}: ${msg}`);
|
|
207
|
-
}
|
|
208
|
-
},
|
|
209
|
-
};
|
|
210
|
-
console.log(`\nš Processing task: ${task.title}`);
|
|
211
|
-
console.log(` Service: ${service}`);
|
|
212
|
-
console.log(` Repository: ${repoPath}`);
|
|
213
|
-
// Phase 1: Planning
|
|
214
|
-
console.log('\nš Phase 1: Planning...');
|
|
215
|
-
const planResult = await routePlanningToService(service, task.title, task.description, config);
|
|
216
|
-
if (!planResult.success || !planResult.plan) {
|
|
217
|
-
throw new Error(`Planning failed: ${planResult.error}`);
|
|
218
|
-
}
|
|
219
|
-
console.log('\nā
Plan created:');
|
|
220
|
-
console.log(` Summary: ${planResult.plan.summary}`);
|
|
221
|
-
console.log(` Files: ${planResult.plan.files.length}`);
|
|
222
|
-
console.log(` Complexity: ${planResult.plan.estimatedComplexity}`);
|
|
223
|
-
if (planResult.usage) {
|
|
224
|
-
console.log(` Cost: $${planResult.usage.costUSD.toFixed(4)}`);
|
|
225
|
-
}
|
|
226
|
-
// Post planning complete comment
|
|
227
|
-
const filesList = planResult.plan.files.slice(0, 5).map(f => `- \`${f.path}\`: ${f.purpose}`).join('\n');
|
|
228
|
-
const moreFiles = planResult.plan.files.length > 5 ? `\n- ... and ${planResult.plan.files.length - 5} more files` : '';
|
|
229
|
-
await onComment(`š **Planning Complete**\n\n` +
|
|
230
|
-
`**Summary:** ${planResult.plan.summary}\n\n` +
|
|
231
|
-
`**Complexity:** ${planResult.plan.estimatedComplexity}\n\n` +
|
|
232
|
-
`**Files to modify (${planResult.plan.files.length}):**\n${filesList}${moreFiles}\n\n` +
|
|
233
|
-
`ā³ Starting implementation...`);
|
|
234
|
-
// Phase 2: Execution
|
|
235
|
-
console.log('\nšØ Phase 2: Execution...');
|
|
236
|
-
iterationCount = 0; // Reset for execution phase
|
|
237
|
-
const execResult = await routeExecutionToService(service, planResult.plan, task.title, task.description, config);
|
|
238
|
-
if (!execResult.success) {
|
|
239
|
-
throw new Error(`Execution failed: ${execResult.error}`);
|
|
240
|
-
}
|
|
241
|
-
console.log('\nā
Execution complete:');
|
|
242
|
-
console.log(` Files modified: ${execResult.files.length}`);
|
|
243
|
-
console.log(` Commit message: ${execResult.commitMessage}`);
|
|
244
|
-
if (execResult.usage) {
|
|
245
|
-
console.log(` Cost: $${execResult.usage.costUSD.toFixed(4)}`);
|
|
246
|
-
}
|
|
247
|
-
// Show modified files
|
|
248
|
-
if (execResult.files.length > 0) {
|
|
249
|
-
console.log('\nš Modified files:');
|
|
250
|
-
for (const file of execResult.files) {
|
|
251
|
-
console.log(` ${file.action === 'create' ? '+' : file.action === 'delete' ? '-' : '~'} ${file.path}`);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
return { plan: planResult.plan, execResult };
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* Process a task using terminal mode (local tool execution)
|
|
258
|
-
* Routes to the appropriate executor based on assignee email.
|
|
259
|
-
*
|
|
260
|
-
* @param task - Task details including id, title, description, and assignee
|
|
261
|
-
* @param projectPath - Path to the project directory
|
|
262
|
-
* @param comments - Previous comments on the task for context
|
|
263
|
-
* @param isFollowUp - Whether this is a follow-up to a previous session
|
|
264
|
-
* @param onComment - Optional callback to post comments during execution
|
|
265
|
-
*/
|
|
266
|
-
async function processTaskTerminal(task, projectPath, comments, isFollowUp, onComment) {
|
|
267
|
-
// Determine which service to use based on assignee
|
|
268
|
-
const assigneeEmail = task.assigneeEmail || 'claude@astrid.cc';
|
|
269
|
-
const service = (0, agent_config_js_1.getAgentService)(assigneeEmail);
|
|
270
|
-
const executor = createTerminalExecutor(service);
|
|
271
|
-
// Check if the executor is available
|
|
272
|
-
const isAvailable = await executor.checkAvailable();
|
|
273
|
-
if (!isAvailable) {
|
|
274
|
-
const errorMessages = {
|
|
275
|
-
claude: 'Claude Code CLI not found. Install it with: npm install -g @anthropic-ai/claude-code',
|
|
276
|
-
openai: 'OpenAI API key not configured. Set OPENAI_API_KEY environment variable.',
|
|
277
|
-
gemini: 'Gemini API key not configured. Set GEMINI_API_KEY environment variable.',
|
|
278
|
-
};
|
|
279
|
-
return {
|
|
280
|
-
success: false,
|
|
281
|
-
error: errorMessages[service]
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
// Get model and max turns for the selected service
|
|
285
|
-
const modelConfig = {
|
|
286
|
-
claude: { model: CONFIG.claudeModel, maxTurns: CONFIG.claudeMaxTurns },
|
|
287
|
-
openai: { model: CONFIG.openaiModel, maxTurns: CONFIG.openaiMaxTurns },
|
|
288
|
-
gemini: { model: CONFIG.geminiModel, maxTurns: CONFIG.geminiMaxTurns },
|
|
289
|
-
};
|
|
290
|
-
const { model, maxTurns } = modelConfig[service];
|
|
291
|
-
console.log(`\nš„ļø Terminal Mode: Processing task with ${service.toUpperCase()}`);
|
|
292
|
-
console.log(` Task: ${task.title}`);
|
|
293
|
-
console.log(` Project: ${projectPath}`);
|
|
294
|
-
console.log(` Service: ${service}`);
|
|
295
|
-
console.log(` Model: ${model}`);
|
|
296
|
-
console.log(` Max turns: ${maxTurns}`);
|
|
297
|
-
// Create session object
|
|
298
|
-
const session = {
|
|
299
|
-
id: task.id,
|
|
300
|
-
taskId: task.id,
|
|
301
|
-
title: task.title,
|
|
302
|
-
description: task.description || '',
|
|
303
|
-
projectPath,
|
|
304
|
-
provider: service,
|
|
305
|
-
claudeSessionId: undefined, // Only used by Claude executor
|
|
306
|
-
status: 'pending',
|
|
307
|
-
createdAt: new Date().toISOString(),
|
|
308
|
-
updatedAt: new Date().toISOString(),
|
|
309
|
-
messageCount: 0,
|
|
310
|
-
};
|
|
311
|
-
const context = { comments };
|
|
312
|
-
let result;
|
|
313
|
-
// Check if we should resume an existing session (only Claude supports native session resumption)
|
|
314
|
-
const existingSessionId = service === 'claude'
|
|
315
|
-
? await terminal_claude_js_1.terminalSessionStore.getClaudeSessionId(task.id)
|
|
316
|
-
: undefined;
|
|
317
|
-
// Helper to transform retry-trigger comments into meaningful prompts
|
|
318
|
-
// If user just says "retry!" we need to provide full task context, not pass "retry!" as the prompt
|
|
319
|
-
const getEffectiveInput = (userComment) => {
|
|
320
|
-
const defaultPrompt = `Please continue with this task: ${task.title}${task.description ? `. ${task.description}` : ''}`;
|
|
321
|
-
if (!userComment) {
|
|
322
|
-
return defaultPrompt;
|
|
323
|
-
}
|
|
324
|
-
// Check if this is a retry-trigger phrase (like "retry!", "try again", etc.)
|
|
325
|
-
// These shouldn't be passed as-is to Claude - they need to be expanded with full context
|
|
326
|
-
if (isRetryComment(userComment)) {
|
|
327
|
-
console.log(`š Expanding retry trigger "${userComment}" to full task context`);
|
|
328
|
-
return defaultPrompt;
|
|
329
|
-
}
|
|
330
|
-
// User provided meaningful instructions, use them as-is
|
|
331
|
-
return userComment;
|
|
332
|
-
};
|
|
333
|
-
// Build callbacks object
|
|
334
|
-
const callbacks = {
|
|
335
|
-
onProgress: (msg) => {
|
|
336
|
-
console.log(` ā ${msg}`);
|
|
337
|
-
},
|
|
338
|
-
onComment,
|
|
339
|
-
};
|
|
340
|
-
if (isFollowUp && existingSessionId) {
|
|
341
|
-
// Get the last user comment as the follow-up input
|
|
342
|
-
const lastUserComment = comments?.filter(c => !c.authorName.includes('Agent'))?.pop();
|
|
343
|
-
const input = getEffectiveInput(lastUserComment?.content);
|
|
344
|
-
console.log(`\nš Resuming existing session: ${existingSessionId}`);
|
|
345
|
-
result = await executor.resumeSession(session, input, context, callbacks);
|
|
346
|
-
}
|
|
347
|
-
else if (isFollowUp) {
|
|
348
|
-
// Non-Claude services rebuild context for follow-ups
|
|
349
|
-
const lastUserComment = comments?.filter(c => !c.authorName.includes('Agent'))?.pop();
|
|
350
|
-
const input = getEffectiveInput(lastUserComment?.content);
|
|
351
|
-
console.log(`\nš Processing follow-up with ${service.toUpperCase()}...`);
|
|
352
|
-
result = await executor.resumeSession(session, input, context, callbacks);
|
|
353
|
-
}
|
|
354
|
-
else {
|
|
355
|
-
console.log(`\nš Starting new ${service.toUpperCase()} session...`);
|
|
356
|
-
result = await executor.startSession(session, undefined, context, callbacks);
|
|
357
|
-
}
|
|
358
|
-
// Parse the output
|
|
359
|
-
const parsed = executor.parseOutput(result.stdout);
|
|
360
|
-
if (result.exitCode !== 0 && result.exitCode !== null) {
|
|
361
|
-
return {
|
|
362
|
-
success: false,
|
|
363
|
-
error: parsed.error || `${service.toUpperCase()} executor exited with code ${result.exitCode}`,
|
|
364
|
-
files: result.modifiedFiles,
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
return {
|
|
368
|
-
success: true,
|
|
369
|
-
prUrl: result.prUrl || parsed.prUrl,
|
|
370
|
-
files: result.modifiedFiles || parsed.files,
|
|
371
|
-
summary: parsed.summary,
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
function isStartingMarker(content) {
|
|
375
|
-
const lower = content.toLowerCase();
|
|
376
|
-
return content.includes('**Starting work**') ||
|
|
377
|
-
content.includes('**Gemini AI Agent Starting**') ||
|
|
378
|
-
content.includes('**Claude AI Agent Starting**') ||
|
|
379
|
-
content.includes('**OpenAI Agent Starting**') ||
|
|
380
|
-
content.includes('Gemini AI Agent starting') ||
|
|
381
|
-
content.includes('Claude AI Agent starting') ||
|
|
382
|
-
(lower.includes('starting') && lower.includes('agent'));
|
|
383
|
-
}
|
|
384
|
-
function isStartingMarkerStale(content, createdAt) {
|
|
385
|
-
// A "Starting" marker is stale if it's older than 5 minutes
|
|
386
|
-
// If the task hasn't progressed past "Starting" in 5 min, something went wrong
|
|
387
|
-
if (!isStartingMarker(content))
|
|
388
|
-
return false;
|
|
389
|
-
const markerTime = new Date(createdAt).getTime();
|
|
390
|
-
const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);
|
|
391
|
-
return markerTime < fiveMinutesAgo;
|
|
392
|
-
}
|
|
393
|
-
function isFailureMarker(content) {
|
|
394
|
-
return content.includes('Workflow Failed') ||
|
|
395
|
-
content.includes('ā **Error**') ||
|
|
396
|
-
content.includes('ā **Processing Failed**') ||
|
|
397
|
-
content.includes('Planning produced no files') ||
|
|
398
|
-
content.includes('**Implementation Failed**');
|
|
399
|
-
}
|
|
400
|
-
function isCompletionMarker(content) {
|
|
401
|
-
return content.includes('**Pull Request Created**') ||
|
|
402
|
-
content.includes('š **Pull Request Created!**') ||
|
|
403
|
-
content.includes('**Implementation Complete**') ||
|
|
404
|
-
content.includes('**Implementation Complete!**') || // Terminal mode posts with !
|
|
405
|
-
content.includes('š **Shipped!**') ||
|
|
406
|
-
content.includes('**Shipped!**') ||
|
|
407
|
-
content.includes('has been merged to main') ||
|
|
408
|
-
content.includes('ā **Failed**') ||
|
|
409
|
-
content.includes('ā **Processing Failed**') ||
|
|
410
|
-
content.includes('**Processing Failed**') ||
|
|
411
|
-
content.includes('š **Ready for Review!**'); // Task is done when ready for review
|
|
412
|
-
}
|
|
413
|
-
function isShippedMarker(content) {
|
|
414
|
-
return content.includes('š **Shipped!**') ||
|
|
415
|
-
content.includes('**Shipped!**') ||
|
|
416
|
-
content.includes('has been merged to main');
|
|
417
|
-
}
|
|
418
|
-
/**
|
|
419
|
-
* Check if task is in a terminal state (done, no more processing needed)
|
|
420
|
-
* This catches tasks that have a completion marker AND no actionable feedback
|
|
421
|
-
*/
|
|
422
|
-
function isTerminalState(content) {
|
|
423
|
-
return isShippedMarker(content) ||
|
|
424
|
-
content.includes('š **Ready for Review!**') ||
|
|
425
|
-
content.includes('**Implementation Complete**') ||
|
|
426
|
-
content.includes('**Implementation Complete!**') ||
|
|
427
|
-
content.includes('ā **Processing Failed**') ||
|
|
428
|
-
content.includes('**Processing Failed**');
|
|
429
|
-
}
|
|
430
|
-
function isRetryComment(content) {
|
|
431
|
-
const lower = content.toLowerCase().trim();
|
|
432
|
-
// Match various ways users might ask for retry
|
|
433
|
-
const retryPhrases = [
|
|
434
|
-
'retry',
|
|
435
|
-
'try again',
|
|
436
|
-
'tryagain',
|
|
437
|
-
'please retry',
|
|
438
|
-
'retry please',
|
|
439
|
-
'reprocess',
|
|
440
|
-
'please reprocess',
|
|
441
|
-
'redo',
|
|
442
|
-
'do again',
|
|
443
|
-
'run again',
|
|
444
|
-
'try it again',
|
|
445
|
-
'give it another try',
|
|
446
|
-
'one more time',
|
|
447
|
-
'again please',
|
|
448
|
-
'again',
|
|
449
|
-
];
|
|
450
|
-
for (const phrase of retryPhrases) {
|
|
451
|
-
if (lower === phrase || lower.startsWith(phrase + ' ') || lower.endsWith(' ' + phrase)) {
|
|
452
|
-
return true;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
// Also match if "retry" or "try again" appears anywhere in a short message
|
|
456
|
-
if (lower.length < 50 && (lower.includes('retry') || lower.includes('try again'))) {
|
|
457
|
-
return true;
|
|
458
|
-
}
|
|
459
|
-
return false;
|
|
460
|
-
}
|
|
461
|
-
function isShipItComment(content) {
|
|
462
|
-
const lower = content.toLowerCase().trim();
|
|
463
|
-
return lower === 'ship it' ||
|
|
464
|
-
lower === 'shipit' ||
|
|
465
|
-
lower === 'ship' ||
|
|
466
|
-
lower === 'merge' ||
|
|
467
|
-
lower === 'lgtm' ||
|
|
468
|
-
lower.startsWith('ship it');
|
|
469
|
-
}
|
|
470
|
-
function isApprovalOrAckComment(content) {
|
|
471
|
-
// Comments that acknowledge completion but don't request changes
|
|
472
|
-
const lower = content.toLowerCase().trim();
|
|
473
|
-
const approvalPhrases = [
|
|
474
|
-
'thanks', 'thank you', 'thx', 'ty',
|
|
475
|
-
'great', 'awesome', 'perfect', 'nice', 'good',
|
|
476
|
-
'looks good', 'looking good',
|
|
477
|
-
'approved', 'approve',
|
|
478
|
-
'š', 'š', 'ā
', 'šÆ',
|
|
479
|
-
'ok', 'okay', 'k',
|
|
480
|
-
'done', 'complete', 'finished',
|
|
481
|
-
'ship it', 'shipit', 'ship', 'merge', 'lgtm' // Also ignore ship it here - handled separately
|
|
482
|
-
];
|
|
483
|
-
// Short comments that are likely acknowledgments
|
|
484
|
-
if (lower.length < 30) {
|
|
485
|
-
for (const phrase of approvalPhrases) {
|
|
486
|
-
if (lower === phrase || lower.startsWith(phrase + ' ') || lower.startsWith(phrase + '!')) {
|
|
487
|
-
return true;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
return false;
|
|
492
|
-
}
|
|
493
|
-
function extractPrUrl(comments) {
|
|
494
|
-
for (const comment of comments) {
|
|
495
|
-
// Look for PR URLs in comments
|
|
496
|
-
const prMatch = comment.content.match(/https:\/\/github\.com\/[^\/]+\/[^\/]+\/pull\/\d+/);
|
|
497
|
-
if (prMatch) {
|
|
498
|
-
return prMatch[0];
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
return null;
|
|
502
|
-
}
|
|
503
|
-
function isAIAgentComment(comment) {
|
|
504
|
-
const email = comment.author?.email || '';
|
|
505
|
-
return email.endsWith('@astrid.cc') && email !== 'system@astrid.cc';
|
|
506
|
-
}
|
|
507
|
-
/**
|
|
508
|
-
* Determine if a task should be processed based on its comments.
|
|
509
|
-
* NOTE: Task completion status (task.completed) should be checked BEFORE calling this.
|
|
510
|
-
* This function only handles comment-based state detection for incomplete tasks.
|
|
511
|
-
*/
|
|
512
|
-
function shouldProcessTask(comments) {
|
|
513
|
-
// No comments = new task, process it
|
|
514
|
-
if (comments.length === 0) {
|
|
515
|
-
return { shouldProcess: true, reason: 'New task - no comments' };
|
|
516
|
-
}
|
|
517
|
-
// Sort by date descending (most recent first)
|
|
518
|
-
const sorted = [...comments].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
519
|
-
// Find most recent AI agent comment and most recent user comment
|
|
520
|
-
const mostRecentAgentComment = sorted.find(c => isAIAgentComment(c));
|
|
521
|
-
const mostRecentUserComment = sorted.find(c => !isAIAgentComment(c));
|
|
522
|
-
// If no agent has worked on this yet, process it
|
|
523
|
-
if (!mostRecentAgentComment) {
|
|
524
|
-
return { shouldProcess: true, reason: 'No agent activity yet' };
|
|
525
|
-
}
|
|
526
|
-
// Check if task has a PR ready for shipping
|
|
527
|
-
if (mostRecentUserComment && isShipItComment(mostRecentUserComment.content)) {
|
|
528
|
-
// Only ship if there's a PR created comment before this
|
|
529
|
-
const prUrl = extractPrUrl(sorted);
|
|
530
|
-
if (prUrl) {
|
|
531
|
-
return { shouldProcess: true, action: 'ship_it', reason: 'User requested ship it', prUrl };
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
// Check if agent is currently working (starting marker without completion)
|
|
535
|
-
if (isStartingMarker(mostRecentAgentComment.content)) {
|
|
536
|
-
if (isStartingMarkerStale(mostRecentAgentComment.content, mostRecentAgentComment.createdAt)) {
|
|
537
|
-
return { shouldProcess: true, reason: 'Starting marker is stale (>5 min) - task likely stuck' };
|
|
538
|
-
}
|
|
539
|
-
return { shouldProcess: false, reason: 'Task is currently being processed' };
|
|
540
|
-
}
|
|
541
|
-
// Check if agent finished (any completion or terminal state)
|
|
542
|
-
if (isCompletionMarker(mostRecentAgentComment.content) || isTerminalState(mostRecentAgentComment.content)) {
|
|
543
|
-
// Shipped = done forever
|
|
544
|
-
if (isShippedMarker(mostRecentAgentComment.content)) {
|
|
545
|
-
return { shouldProcess: false, reason: 'Task already shipped' };
|
|
546
|
-
}
|
|
547
|
-
// PR created or ready for review = waiting for user action, don't reprocess
|
|
548
|
-
if (mostRecentAgentComment.content.includes('**Pull Request Created**') ||
|
|
549
|
-
mostRecentAgentComment.content.includes('**Ready for Review!**')) {
|
|
550
|
-
return { shouldProcess: false, reason: 'PR created - waiting for user review' };
|
|
551
|
-
}
|
|
552
|
-
// Implementation complete = waiting for "ship it" approval, don't reprocess
|
|
553
|
-
if (mostRecentAgentComment.content.includes('**Implementation Complete**') ||
|
|
554
|
-
mostRecentAgentComment.content.includes('**Implementation Complete!**')) {
|
|
555
|
-
// Check if user said "ship it" after completion
|
|
556
|
-
if (mostRecentUserComment && isShipItComment(mostRecentUserComment.content)) {
|
|
557
|
-
// Check if there's a PR to ship
|
|
558
|
-
const prUrl = extractPrUrl(sorted);
|
|
559
|
-
if (prUrl) {
|
|
560
|
-
return { shouldProcess: true, action: 'ship_it', reason: 'User requested ship it after completion', prUrl };
|
|
561
|
-
}
|
|
562
|
-
// No PR, but user wants to ship - allow processing to push/deploy
|
|
563
|
-
return { shouldProcess: true, action: 'ship_it', reason: 'User requested ship it - will push and deploy' };
|
|
564
|
-
}
|
|
565
|
-
return { shouldProcess: false, reason: 'Implementation complete - comment "ship it" to deploy' };
|
|
566
|
-
}
|
|
567
|
-
// Failed = only reprocess if user explicitly requests retry
|
|
568
|
-
if (mostRecentAgentComment.content.includes('**Processing Failed**') ||
|
|
569
|
-
mostRecentAgentComment.content.includes('**Failed**')) {
|
|
570
|
-
if (mostRecentUserComment && isRetryComment(mostRecentUserComment.content)) {
|
|
571
|
-
return { shouldProcess: true, reason: 'User requested retry after failure' };
|
|
572
|
-
}
|
|
573
|
-
return { shouldProcess: false, reason: 'Task failed - comment "retry" or "try again" to retry' };
|
|
574
|
-
}
|
|
575
|
-
// Other completion - don't reprocess unless explicit retry
|
|
576
|
-
if (mostRecentUserComment && isRetryComment(mostRecentUserComment.content)) {
|
|
577
|
-
return { shouldProcess: true, reason: 'User requested retry' };
|
|
578
|
-
}
|
|
579
|
-
return { shouldProcess: false, reason: 'Task already completed' };
|
|
580
|
-
}
|
|
581
|
-
// Default: process if we can't determine state
|
|
582
|
-
return { shouldProcess: true, reason: 'Unknown state - processing' };
|
|
583
|
-
}
|
|
584
|
-
// ============================================================================
|
|
585
|
-
// GITHUB PR CREATION
|
|
586
|
-
// ============================================================================
|
|
587
|
-
async function createPullRequest(repoPath, owner, repo, taskTitle, commitMessage, prDescription, agentName) {
|
|
588
|
-
const { execSync } = await import('child_process');
|
|
589
|
-
try {
|
|
590
|
-
// Create branch name
|
|
591
|
-
const timestamp = Date.now();
|
|
592
|
-
const safeName = taskTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 30);
|
|
593
|
-
const branchName = `astrid-ai/${timestamp}-${safeName}`;
|
|
594
|
-
console.log(`š¦ Creating branch: ${branchName}`);
|
|
595
|
-
execSync(`git checkout -b ${branchName}`, { cwd: repoPath, stdio: 'pipe' });
|
|
596
|
-
// Stage and commit
|
|
597
|
-
execSync('git add -A', { cwd: repoPath, stdio: 'pipe' });
|
|
598
|
-
const status = execSync('git status --porcelain', { cwd: repoPath, encoding: 'utf-8' });
|
|
599
|
-
if (!status.trim()) {
|
|
600
|
-
return { success: false, error: 'No changes to commit' };
|
|
601
|
-
}
|
|
602
|
-
const fullCommit = `${commitMessage}\n\nš¤ Generated with ${agentName} via Astrid`;
|
|
603
|
-
execSync(`git commit -m "${fullCommit.replace(/"/g, '\\"')}"`, { cwd: repoPath, stdio: 'pipe' });
|
|
604
|
-
console.log(`š¤ Pushing to origin/${branchName}`);
|
|
605
|
-
execSync(`git push -u origin ${branchName}`, { cwd: repoPath, stdio: 'pipe' });
|
|
606
|
-
// Create PR via GitHub API
|
|
607
|
-
console.log('š Creating pull request');
|
|
608
|
-
const prBody = `${prDescription}\n\n---\nš¤ Generated with ${agentName} via Astrid`;
|
|
609
|
-
const prResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls`, {
|
|
610
|
-
method: 'POST',
|
|
611
|
-
headers: {
|
|
612
|
-
'Authorization': `token ${CONFIG.githubToken}`,
|
|
613
|
-
'Accept': 'application/vnd.github.v3+json',
|
|
614
|
-
'Content-Type': 'application/json',
|
|
615
|
-
},
|
|
616
|
-
body: JSON.stringify({
|
|
617
|
-
title: commitMessage,
|
|
618
|
-
body: prBody,
|
|
619
|
-
head: branchName,
|
|
620
|
-
base: 'main',
|
|
621
|
-
}),
|
|
622
|
-
});
|
|
623
|
-
if (!prResponse.ok) {
|
|
624
|
-
const error = await prResponse.text();
|
|
625
|
-
return { success: false, error: `GitHub API error: ${error}` };
|
|
626
|
-
}
|
|
627
|
-
const prData = await prResponse.json();
|
|
628
|
-
console.log(`ā
PR created: ${prData.html_url}`);
|
|
629
|
-
return { success: true, prUrl: prData.html_url, branchName };
|
|
630
|
-
}
|
|
631
|
-
catch (error) {
|
|
632
|
-
return { success: false, error: String(error) };
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
// ============================================================================
|
|
636
|
-
// VERCEL PREVIEW DEPLOYMENT
|
|
637
|
-
// ============================================================================
|
|
638
|
-
function branchToSubdomain(branchName) {
|
|
639
|
-
// Convert branch name to valid subdomain
|
|
640
|
-
return branchName
|
|
641
|
-
.replace(/[^a-zA-Z0-9-]/g, '-')
|
|
642
|
-
.replace(/-+/g, '-')
|
|
643
|
-
.replace(/^-|-$/g, '')
|
|
644
|
-
.toLowerCase()
|
|
645
|
-
.slice(0, 63);
|
|
646
|
-
}
|
|
647
|
-
async function deployVercelPreview(repoPath, branchName) {
|
|
648
|
-
if (!CONFIG.vercelToken) {
|
|
649
|
-
return { success: false, error: 'VERCEL_TOKEN not configured' };
|
|
650
|
-
}
|
|
651
|
-
const { execSync } = await import('child_process');
|
|
652
|
-
const subdomain = branchToSubdomain(branchName);
|
|
653
|
-
console.log(`š Deploying Vercel preview for branch: ${branchName}`);
|
|
654
|
-
try {
|
|
655
|
-
// Deploy to Vercel preview
|
|
656
|
-
const deployOutput = execSync(`vercel deploy --yes --force --token=${CONFIG.vercelToken}`, { cwd: repoPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
657
|
-
// Extract deployment URL from output
|
|
658
|
-
const urlMatch = deployOutput.match(/https:\/\/[^\s]+\.vercel\.app/);
|
|
659
|
-
if (!urlMatch) {
|
|
660
|
-
return { success: false, error: 'Could not extract deployment URL' };
|
|
661
|
-
}
|
|
662
|
-
const deploymentUrl = urlMatch[0];
|
|
663
|
-
console.log(`ā
Vercel preview deployed: ${deploymentUrl}`);
|
|
664
|
-
return { success: true, previewUrl: deploymentUrl };
|
|
665
|
-
}
|
|
666
|
-
catch (error) {
|
|
667
|
-
return { success: false, error: String(error) };
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
// ============================================================================
|
|
671
|
-
// iOS DETECTION
|
|
672
|
-
// ============================================================================
|
|
673
|
-
const IOS_FILE_PATTERNS = [
|
|
674
|
-
/^ios-app\//,
|
|
675
|
-
/^ios\//,
|
|
676
|
-
/\.swift$/,
|
|
677
|
-
/\.xcodeproj/,
|
|
678
|
-
/\.xcworkspace/,
|
|
679
|
-
/Info\.plist$/,
|
|
680
|
-
/\.entitlements$/,
|
|
681
|
-
/Podfile$/,
|
|
682
|
-
];
|
|
683
|
-
function detectIOSChanges(repoPath) {
|
|
684
|
-
try {
|
|
685
|
-
const { execSync } = require('child_process');
|
|
686
|
-
// Get list of changed files in the branch
|
|
687
|
-
const changedFiles = execSync('git diff --name-only HEAD~1 HEAD 2>/dev/null || git diff --name-only HEAD', { cwd: repoPath, encoding: 'utf-8' }).split('\n').filter(Boolean);
|
|
688
|
-
return changedFiles.some((file) => IOS_FILE_PATTERNS.some(pattern => pattern.test(file)));
|
|
689
|
-
}
|
|
690
|
-
catch {
|
|
691
|
-
return false;
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
// ============================================================================
|
|
695
|
-
// WORKER LOOP
|
|
696
|
-
// ============================================================================
|
|
697
|
-
// Track tasks currently being processed to avoid duplicates
|
|
698
|
-
const processingTasks = new Set();
|
|
699
|
-
// Track recently completed tasks to prevent rapid reprocessing (cooldown)
|
|
700
|
-
const recentlyCompletedTasks = new Map(); // taskId -> completedAt timestamp
|
|
701
|
-
const COMPLETION_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes cooldown after completion
|
|
702
|
-
function isInCooldown(taskId) {
|
|
703
|
-
const completedAt = recentlyCompletedTasks.get(taskId);
|
|
704
|
-
if (!completedAt)
|
|
705
|
-
return false;
|
|
706
|
-
const elapsed = Date.now() - completedAt;
|
|
707
|
-
if (elapsed > COMPLETION_COOLDOWN_MS) {
|
|
708
|
-
recentlyCompletedTasks.delete(taskId); // Clean up expired entry
|
|
709
|
-
return false;
|
|
710
|
-
}
|
|
711
|
-
return true;
|
|
712
|
-
}
|
|
713
|
-
function markTaskCompleted(taskId) {
|
|
714
|
-
recentlyCompletedTasks.set(taskId, Date.now());
|
|
715
|
-
}
|
|
716
|
-
async function runWorker() {
|
|
717
|
-
const client = new astrid_oauth_js_1.AstridOAuthClient();
|
|
718
|
-
if (!client.isConfigured()) {
|
|
719
|
-
console.error('ā OAuth credentials not configured');
|
|
720
|
-
console.error(' Set ASTRID_OAUTH_CLIENT_ID and ASTRID_OAUTH_CLIENT_SECRET');
|
|
721
|
-
process.exit(1);
|
|
722
|
-
}
|
|
723
|
-
const listId = CONFIG.astridListId;
|
|
724
|
-
if (!listId) {
|
|
725
|
-
console.error('ā ASTRID_OAUTH_LIST_ID not configured');
|
|
726
|
-
process.exit(1);
|
|
727
|
-
}
|
|
728
|
-
if (!CONFIG.githubToken) {
|
|
729
|
-
console.error('ā GITHUB_TOKEN not configured');
|
|
730
|
-
process.exit(1);
|
|
731
|
-
}
|
|
732
|
-
console.log('š¤ Astrid Agent Worker');
|
|
733
|
-
console.log(` List ID: ${listId}`);
|
|
734
|
-
console.log(` Poll interval: ${CONFIG.pollIntervalMs}ms`);
|
|
735
|
-
console.log('');
|
|
736
|
-
// Test connection
|
|
737
|
-
const testResult = await client.testConnection();
|
|
738
|
-
if (!testResult.success) {
|
|
739
|
-
console.error(`ā Connection failed: ${testResult.error}`);
|
|
740
|
-
process.exit(1);
|
|
741
|
-
}
|
|
742
|
-
console.log('ā
Connected to Astrid API\n');
|
|
743
|
-
// Get list info to find repository
|
|
744
|
-
const listResult = await client.getList(listId);
|
|
745
|
-
let defaultRepo = { owner: '', repo: '' };
|
|
746
|
-
// API returns githubRepositoryId (e.g., "owner/repo"), not repository
|
|
747
|
-
const repoString = listResult.data?.githubRepositoryId || listResult.data?.repository;
|
|
748
|
-
if (listResult.success && repoString) {
|
|
749
|
-
const parts = repoString.split('/');
|
|
750
|
-
if (parts.length === 2) {
|
|
751
|
-
defaultRepo = { owner: parts[0], repo: parts[1] };
|
|
752
|
-
console.log(`š¦ Default repository: ${repoString}\n`);
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
if (!defaultRepo.owner || !defaultRepo.repo) {
|
|
756
|
-
console.error('ā No repository configured for this list.');
|
|
757
|
-
console.error(' Set the GitHub repository in list settings on astrid.cc');
|
|
758
|
-
process.exit(1);
|
|
759
|
-
}
|
|
760
|
-
// Polling loop
|
|
761
|
-
while (true) {
|
|
762
|
-
try {
|
|
763
|
-
const tasksResult = await client.getTasks(listId, false);
|
|
764
|
-
if (tasksResult.success && tasksResult.data) {
|
|
765
|
-
// STEP 1: Filter out completed and unassigned tasks FIRST
|
|
766
|
-
// This is the primary filter - don't waste any tokens on these
|
|
767
|
-
const eligibleTasks = tasksResult.data.filter(task => {
|
|
768
|
-
// RULE 1: Completed tasks are NEVER processed
|
|
769
|
-
if (task.completed) {
|
|
770
|
-
return false;
|
|
771
|
-
}
|
|
772
|
-
// RULE 2: Must be assigned to a registered AI agent
|
|
773
|
-
const email = getAssigneeEmail(task);
|
|
774
|
-
if (!email || !(0, agent_config_js_1.isRegisteredAgent)(email)) {
|
|
775
|
-
return false;
|
|
776
|
-
}
|
|
777
|
-
return true;
|
|
778
|
-
});
|
|
779
|
-
if (eligibleTasks.length > 0) {
|
|
780
|
-
console.log(`\nš Found ${eligibleTasks.length} eligible task(s) (not completed, assigned to AI)`);
|
|
781
|
-
for (const task of eligibleTasks) {
|
|
782
|
-
// Skip if already being processed by this worker instance
|
|
783
|
-
if (processingTasks.has(task.id)) {
|
|
784
|
-
console.log(` ā³ ${task.id.slice(0, 8)}... already in progress`);
|
|
785
|
-
continue;
|
|
786
|
-
}
|
|
787
|
-
// Skip if in cooldown period (recently completed)
|
|
788
|
-
if (isInCooldown(task.id)) {
|
|
789
|
-
console.log(` āøļø ${task.id.slice(0, 8)}... in cooldown (recently completed)`);
|
|
790
|
-
continue;
|
|
791
|
-
}
|
|
792
|
-
// Declare agentUserId outside try block so it's accessible in catch
|
|
793
|
-
let agentUserId = null;
|
|
794
|
-
try {
|
|
795
|
-
// STEP 2: Check comments for processing state
|
|
796
|
-
// (only after we've confirmed task is not completed and is assigned to AI)
|
|
797
|
-
const commentsResult = await client.getComments(task.id);
|
|
798
|
-
const comments = commentsResult.success ? commentsResult.data || [] : [];
|
|
799
|
-
const status = shouldProcessTask(comments);
|
|
800
|
-
const assigneeEmail = getAssigneeEmail(task) || 'claude@astrid.cc';
|
|
801
|
-
console.log(`\nš Task: ${task.title.slice(0, 50)}...`);
|
|
802
|
-
console.log(` ID: ${task.id}`);
|
|
803
|
-
console.log(` Agent: ${assigneeEmail}`);
|
|
804
|
-
console.log(` Status: ${status.reason}`);
|
|
805
|
-
if (!status.shouldProcess) {
|
|
806
|
-
continue;
|
|
807
|
-
}
|
|
808
|
-
// Get agent info for posting comments
|
|
809
|
-
const service = (0, agent_config_js_1.getAgentService)(assigneeEmail);
|
|
810
|
-
const agentName = `${service.charAt(0).toUpperCase() + service.slice(1)} AI Agent`;
|
|
811
|
-
// Look up agent user ID to post comments as the agent
|
|
812
|
-
agentUserId = await client.getAgentIdByEmail(assigneeEmail);
|
|
813
|
-
if (agentUserId) {
|
|
814
|
-
console.log(` Posting as: ${assigneeEmail} (${agentUserId})`);
|
|
815
|
-
}
|
|
816
|
-
else {
|
|
817
|
-
console.log(` ā ļø Could not find agent ID for ${assigneeEmail} - comments will be posted as OAuth user`);
|
|
818
|
-
}
|
|
819
|
-
// Handle "ship it" action - merge existing PR instead of reprocessing
|
|
820
|
-
if (status.action === 'ship_it' && status.prUrl) {
|
|
821
|
-
console.log(`\nš Ship It! Merging PR: ${status.prUrl}`);
|
|
822
|
-
processingTasks.add(task.id);
|
|
823
|
-
try {
|
|
824
|
-
// Extract PR number from URL
|
|
825
|
-
const prMatch = status.prUrl.match(/\/pull\/(\d+)/);
|
|
826
|
-
if (!prMatch) {
|
|
827
|
-
throw new Error('Could not extract PR number from URL');
|
|
828
|
-
}
|
|
829
|
-
const prNumber = prMatch[1];
|
|
830
|
-
// Merge PR using GitHub CLI or API
|
|
831
|
-
const { execSync } = await import('child_process');
|
|
832
|
-
execSync(`gh pr merge ${prNumber} --merge --repo ${defaultRepo.owner}/${defaultRepo.repo}`, { stdio: 'inherit' });
|
|
833
|
-
console.log(`ā
PR #${prNumber} merged successfully!`);
|
|
834
|
-
// Deploy to production if Vercel token is configured
|
|
835
|
-
let productionUrl;
|
|
836
|
-
if (process.env.VERCEL_TOKEN) {
|
|
837
|
-
console.log(`\nš Deploying to production...`);
|
|
838
|
-
try {
|
|
839
|
-
const deployOutput = execSync('vercel --prod --yes', {
|
|
840
|
-
encoding: 'utf-8',
|
|
841
|
-
timeout: 300000, // 5 minute timeout
|
|
842
|
-
env: { ...process.env },
|
|
843
|
-
});
|
|
844
|
-
// Extract production URL from output
|
|
845
|
-
const urlMatch = deployOutput.match(/https:\/\/[^\s]+\.vercel\.app/);
|
|
846
|
-
if (urlMatch) {
|
|
847
|
-
productionUrl = urlMatch[0];
|
|
848
|
-
}
|
|
849
|
-
console.log(`ā
Production deployment complete!`);
|
|
850
|
-
}
|
|
851
|
-
catch (deployError) {
|
|
852
|
-
console.error(`ā ļø Production deployment failed:`, deployError);
|
|
853
|
-
// Continue - merge was successful, just deployment failed
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
// Post success comment
|
|
857
|
-
let successMessage = `š **Shipped!**\n\n` +
|
|
858
|
-
`PR #${prNumber} has been merged to main.\n\n`;
|
|
859
|
-
if (productionUrl) {
|
|
860
|
-
successMessage += `š **Production:** [${productionUrl}](${productionUrl})\n\n`;
|
|
861
|
-
}
|
|
862
|
-
else if (process.env.VERCEL_TOKEN) {
|
|
863
|
-
successMessage += `ā ļø Production deployment may have failed - check Vercel dashboard.\n\n`;
|
|
864
|
-
}
|
|
865
|
-
else {
|
|
866
|
-
successMessage += `The changes will be live after deployment completes.\n\n`;
|
|
867
|
-
}
|
|
868
|
-
successMessage += `---\n*Merged by ${agentName}*`;
|
|
869
|
-
await client.addComment(task.id, successMessage, agentUserId || undefined);
|
|
870
|
-
}
|
|
871
|
-
catch (mergeError) {
|
|
872
|
-
console.error('ā Failed to merge PR:', mergeError);
|
|
873
|
-
await client.addComment(task.id, `ā **Merge Failed**\n\n` +
|
|
874
|
-
`Could not merge PR: ${mergeError instanceof Error ? mergeError.message : String(mergeError)}\n\n` +
|
|
875
|
-
`Please merge manually: ${status.prUrl}`, agentUserId || undefined);
|
|
876
|
-
}
|
|
877
|
-
finally {
|
|
878
|
-
processingTasks.delete(task.id);
|
|
879
|
-
}
|
|
880
|
-
continue;
|
|
881
|
-
}
|
|
882
|
-
// Mark as processing for normal workflow
|
|
883
|
-
processingTasks.add(task.id);
|
|
884
|
-
// Post starting comment
|
|
885
|
-
await client.addComment(task.id, `š¤ **${agentName} Starting**\n\n` +
|
|
886
|
-
`**Task:** ${task.title}\n` +
|
|
887
|
-
`**Repository:** \`${defaultRepo.owner}/${defaultRepo.repo}\`\n\n` +
|
|
888
|
-
`**Workflow:**\n` +
|
|
889
|
-
`1. ā³ Clone repository\n` +
|
|
890
|
-
`2. ā³ **Planning** - Analyze codebase\n` +
|
|
891
|
-
`3. ā³ **Implementation** - Make changes\n` +
|
|
892
|
-
`4. ā³ Create pull request\n\n` +
|
|
893
|
-
`---\n*Using ${service} API*`, agentUserId || undefined);
|
|
894
|
-
// Clone repository
|
|
895
|
-
console.log(`\nš¦ Cloning ${defaultRepo.owner}/${defaultRepo.repo}...`);
|
|
896
|
-
const { repoPath, cleanup } = await (0, claude_js_1.prepareRepository)(defaultRepo.owner, defaultRepo.repo, 'main', CONFIG.githubToken);
|
|
897
|
-
try {
|
|
898
|
-
// Process task with progress comments
|
|
899
|
-
const { plan, execResult } = await processTask({
|
|
900
|
-
id: task.id,
|
|
901
|
-
title: task.title,
|
|
902
|
-
description: task.description,
|
|
903
|
-
assigneeEmail,
|
|
904
|
-
}, repoPath, {
|
|
905
|
-
onComment: async (message) => {
|
|
906
|
-
await client.addComment(task.id, message, agentUserId || undefined);
|
|
907
|
-
}
|
|
908
|
-
});
|
|
909
|
-
// Verify changes before creating PR
|
|
910
|
-
console.log(`\nš Verifying changes...`);
|
|
911
|
-
await client.addComment(task.id, `š **Verifying Changes**\n\n` +
|
|
912
|
-
`Running build/type checks to ensure code quality...`, agentUserId || undefined);
|
|
913
|
-
const verificationResult = await (0, claude_js_1.verifyChanges)(repoPath, logger);
|
|
914
|
-
if (!verificationResult.success) {
|
|
915
|
-
console.log(`ā ļø Verification failed, attempting to fix...`);
|
|
916
|
-
// Post verification failure - agent should have already tried to fix
|
|
917
|
-
await client.addComment(task.id, `ā ļø **Verification Warning**\n\n` +
|
|
918
|
-
`Initial verification had issues. The agent attempted to fix them.\n\n` +
|
|
919
|
-
`\`\`\`\n${verificationResult.output.slice(0, 1500)}\n\`\`\`\n\n` +
|
|
920
|
-
`Proceeding with PR creation - please review carefully.`, agentUserId || undefined);
|
|
921
|
-
}
|
|
922
|
-
else {
|
|
923
|
-
console.log(`ā
Verification passed!`);
|
|
924
|
-
}
|
|
925
|
-
// Get the execution result for PR creation
|
|
926
|
-
// For now, use a generic commit message
|
|
927
|
-
const commitMessage = `feat: ${task.title}`;
|
|
928
|
-
const prDescription = task.description || task.title;
|
|
929
|
-
// Create PR
|
|
930
|
-
const prResult = await createPullRequest(repoPath, defaultRepo.owner, defaultRepo.repo, task.title, commitMessage, prDescription, agentName);
|
|
931
|
-
if (prResult.success && prResult.prUrl) {
|
|
932
|
-
// Post initial PR created message
|
|
933
|
-
await client.addComment(task.id, `š **Pull Request Created!**\n\n` +
|
|
934
|
-
`š **[${prResult.prUrl}](${prResult.prUrl})**\n\n` +
|
|
935
|
-
`ā³ Deploying preview...\n\n` +
|
|
936
|
-
`---\n*Generated by ${agentName} via Astrid*`, agentUserId || undefined);
|
|
937
|
-
// Deploy Vercel preview if configured
|
|
938
|
-
let previewMessage = '';
|
|
939
|
-
if (prResult.branchName && CONFIG.vercelToken) {
|
|
940
|
-
const vercelResult = await deployVercelPreview(repoPath, prResult.branchName);
|
|
941
|
-
if (vercelResult.success && vercelResult.previewUrl) {
|
|
942
|
-
previewMessage = `š **Preview:** [${vercelResult.previewUrl}](${vercelResult.previewUrl})\n\n`;
|
|
943
|
-
console.log(` ā
Preview deployed: ${vercelResult.previewUrl}`);
|
|
944
|
-
}
|
|
945
|
-
else {
|
|
946
|
-
console.log(` ā ļø Preview deployment failed: ${vercelResult.error}`);
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
// Check for iOS changes and add TestFlight link
|
|
950
|
-
let iosMessage = '';
|
|
951
|
-
const hasIOSChanges = detectIOSChanges(repoPath);
|
|
952
|
-
if (hasIOSChanges && CONFIG.testflightPublicLink) {
|
|
953
|
-
iosMessage = `š± **iOS TestFlight:** [${CONFIG.testflightPublicLink}](${CONFIG.testflightPublicLink})\n` +
|
|
954
|
-
`*(Build will be available after Xcode Cloud completes)*\n\n`;
|
|
955
|
-
console.log(` š± iOS changes detected - TestFlight link added`);
|
|
956
|
-
}
|
|
957
|
-
// Post staging ready message with preview URL and TestFlight if applicable
|
|
958
|
-
if (previewMessage || iosMessage) {
|
|
959
|
-
await client.addComment(task.id, `š **Ready for Review!**\n\n` +
|
|
960
|
-
previewMessage +
|
|
961
|
-
iosMessage +
|
|
962
|
-
`**What's next:**\n` +
|
|
963
|
-
`1. ā
Test the changes\n` +
|
|
964
|
-
`2. Review the code in the PR\n` +
|
|
965
|
-
`3. Comment "ship it" to merge!\n\n` +
|
|
966
|
-
`---\n*Preview deployment complete*`, agentUserId || undefined);
|
|
967
|
-
}
|
|
968
|
-
// Unassign task so it goes back to user for review
|
|
969
|
-
await client.reassignTask(task.id, null).catch(err => {
|
|
970
|
-
console.log(` ā ļø Could not unassign task: ${err}`);
|
|
971
|
-
});
|
|
972
|
-
console.log(` ā
Task unassigned - ready for review`);
|
|
973
|
-
// Mark as completed with cooldown to prevent rapid reprocessing
|
|
974
|
-
markTaskCompleted(task.id);
|
|
975
|
-
}
|
|
976
|
-
else {
|
|
977
|
-
await client.addComment(task.id, `ā ļø **PR Creation Failed**\n\n` +
|
|
978
|
-
`Error: ${prResult.error}\n\n` +
|
|
979
|
-
`The changes were made but could not be pushed. Check the logs for details.`, agentUserId || undefined);
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
finally {
|
|
983
|
-
await cleanup();
|
|
984
|
-
processingTasks.delete(task.id);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
catch (error) {
|
|
988
|
-
console.error(`ā Failed to process task ${task.id}:`, error);
|
|
989
|
-
processingTasks.delete(task.id);
|
|
990
|
-
// Post error comment (agentUserId may be undefined if error occurred before lookup)
|
|
991
|
-
await client.addComment(task.id, `ā **Processing Failed**\n\n` +
|
|
992
|
-
`Error: ${error instanceof Error ? error.message : String(error)}\n\n` +
|
|
993
|
-
`---\n` +
|
|
994
|
-
`š” **To try again:** Comment "retry" or "try again"\n` +
|
|
995
|
-
`š” **To ship existing PR:** Comment "ship it"`, agentUserId || undefined).catch(() => { });
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
catch (error) {
|
|
1002
|
-
console.error('ā Poll error:', error);
|
|
1003
|
-
}
|
|
1004
|
-
// Wait before next poll
|
|
1005
|
-
await new Promise(resolve => setTimeout(resolve, CONFIG.pollIntervalMs));
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
// ============================================================================
|
|
1009
|
-
// TERMINAL MODE WORKER LOOP
|
|
1010
|
-
// ============================================================================
|
|
1011
|
-
/**
|
|
1012
|
-
* Terminal mode worker loop - uses local tool execution instead of API
|
|
1013
|
-
* Supports Claude (CLI), OpenAI (API), and Gemini (API) providers.
|
|
1014
|
-
*/
|
|
1015
|
-
async function runWorkerTerminal() {
|
|
1016
|
-
const client = new astrid_oauth_js_1.AstridOAuthClient();
|
|
1017
|
-
if (!client.isConfigured()) {
|
|
1018
|
-
console.error('ā OAuth credentials not configured');
|
|
1019
|
-
console.error(' Set ASTRID_OAUTH_CLIENT_ID and ASTRID_OAUTH_CLIENT_SECRET');
|
|
1020
|
-
process.exit(1);
|
|
1021
|
-
}
|
|
1022
|
-
const listId = CONFIG.astridListId;
|
|
1023
|
-
if (!listId) {
|
|
1024
|
-
console.error('ā ASTRID_OAUTH_LIST_ID not configured');
|
|
1025
|
-
process.exit(1);
|
|
1026
|
-
}
|
|
1027
|
-
// Check which terminal mode providers are available
|
|
1028
|
-
const providers = await getAvailableTerminalProviders();
|
|
1029
|
-
const availableProviders = providers.filter(p => p.available);
|
|
1030
|
-
if (availableProviders.length === 0) {
|
|
1031
|
-
console.error('ā No terminal mode providers available');
|
|
1032
|
-
console.error('');
|
|
1033
|
-
console.error(' Configure at least one of the following:');
|
|
1034
|
-
console.error(' ⢠Claude: Install Claude Code CLI (npm install -g @anthropic-ai/claude-code)');
|
|
1035
|
-
console.error(' ⢠OpenAI: Set OPENAI_API_KEY environment variable');
|
|
1036
|
-
console.error(' ⢠Gemini: Set GEMINI_API_KEY environment variable');
|
|
1037
|
-
console.error('');
|
|
1038
|
-
console.error(' Or use API mode: npx astrid-agent (without --terminal)');
|
|
1039
|
-
process.exit(1);
|
|
1040
|
-
}
|
|
1041
|
-
console.log('š¤ Astrid Agent Worker (Terminal Mode)');
|
|
1042
|
-
console.log(` List ID: ${listId}`);
|
|
1043
|
-
console.log(` Poll interval: ${CONFIG.pollIntervalMs}ms`);
|
|
1044
|
-
console.log(` Project path: ${CONFIG.defaultProjectPath}`);
|
|
1045
|
-
console.log('');
|
|
1046
|
-
console.log(' Available providers:');
|
|
1047
|
-
for (const provider of providers) {
|
|
1048
|
-
const status = provider.available ? 'ā
' : 'ā';
|
|
1049
|
-
const config = provider.service === 'claude'
|
|
1050
|
-
? `(model: ${CONFIG.claudeModel})`
|
|
1051
|
-
: provider.service === 'openai'
|
|
1052
|
-
? `(model: ${CONFIG.openaiModel})`
|
|
1053
|
-
: `(model: ${CONFIG.geminiModel})`;
|
|
1054
|
-
console.log(` ${status} ${provider.service.toUpperCase()} ${provider.available ? config : '(not configured)'}`);
|
|
1055
|
-
}
|
|
1056
|
-
console.log('');
|
|
1057
|
-
// Test connection
|
|
1058
|
-
const testResult = await client.testConnection();
|
|
1059
|
-
if (!testResult.success) {
|
|
1060
|
-
console.error(`ā Connection failed: ${testResult.error}`);
|
|
1061
|
-
process.exit(1);
|
|
1062
|
-
}
|
|
1063
|
-
console.log('ā
Connected to Astrid API');
|
|
1064
|
-
console.log(`ā
${availableProviders.length} terminal provider(s) available\n`);
|
|
1065
|
-
// Polling loop
|
|
1066
|
-
while (true) {
|
|
1067
|
-
try {
|
|
1068
|
-
const tasksResult = await client.getTasks(listId, false);
|
|
1069
|
-
if (tasksResult.success && tasksResult.data) {
|
|
1070
|
-
// Filter for incomplete tasks assigned to AI agents
|
|
1071
|
-
const eligibleTasks = tasksResult.data.filter(task => {
|
|
1072
|
-
if (task.completed)
|
|
1073
|
-
return false;
|
|
1074
|
-
const email = getAssigneeEmail(task);
|
|
1075
|
-
if (!email || !(0, agent_config_js_1.isRegisteredAgent)(email))
|
|
1076
|
-
return false;
|
|
1077
|
-
return true;
|
|
1078
|
-
});
|
|
1079
|
-
if (eligibleTasks.length > 0) {
|
|
1080
|
-
console.log(`\nš Found ${eligibleTasks.length} eligible task(s)`);
|
|
1081
|
-
for (const task of eligibleTasks) {
|
|
1082
|
-
// Skip if already being processed
|
|
1083
|
-
if (processingTasks.has(task.id)) {
|
|
1084
|
-
console.log(` ā³ ${task.id.slice(0, 8)}... already in progress`);
|
|
1085
|
-
continue;
|
|
1086
|
-
}
|
|
1087
|
-
// Skip if in cooldown period (recently completed)
|
|
1088
|
-
if (isInCooldown(task.id)) {
|
|
1089
|
-
console.log(` āøļø ${task.id.slice(0, 8)}... in cooldown (recently completed)`);
|
|
1090
|
-
continue;
|
|
1091
|
-
}
|
|
1092
|
-
let agentUserId = null;
|
|
1093
|
-
try {
|
|
1094
|
-
// Check comments for processing state
|
|
1095
|
-
const commentsResult = await client.getComments(task.id);
|
|
1096
|
-
const comments = commentsResult.success ? commentsResult.data || [] : [];
|
|
1097
|
-
const status = shouldProcessTask(comments);
|
|
1098
|
-
const assigneeEmail = getAssigneeEmail(task) || 'claude@astrid.cc';
|
|
1099
|
-
console.log(`\nš Task: ${task.title.slice(0, 50)}...`);
|
|
1100
|
-
console.log(` ID: ${task.id}`);
|
|
1101
|
-
console.log(` Agent: ${assigneeEmail}`);
|
|
1102
|
-
console.log(` Status: ${status.reason}`);
|
|
1103
|
-
if (!status.shouldProcess) {
|
|
1104
|
-
continue;
|
|
1105
|
-
}
|
|
1106
|
-
const service = (0, agent_config_js_1.getAgentService)(assigneeEmail);
|
|
1107
|
-
const agentName = `${service.charAt(0).toUpperCase() + service.slice(1)} AI Agent (Terminal)`;
|
|
1108
|
-
// Get agent user ID
|
|
1109
|
-
agentUserId = await client.getAgentIdByEmail(assigneeEmail);
|
|
1110
|
-
if (agentUserId) {
|
|
1111
|
-
console.log(` Posting as: ${assigneeEmail} (${agentUserId})`);
|
|
1112
|
-
}
|
|
1113
|
-
// Handle "ship it" action
|
|
1114
|
-
if (status.action === 'ship_it' && status.prUrl) {
|
|
1115
|
-
console.log(`\nš Ship It! Merging PR: ${status.prUrl}`);
|
|
1116
|
-
processingTasks.add(task.id);
|
|
1117
|
-
try {
|
|
1118
|
-
const prMatch = status.prUrl.match(/\/pull\/(\d+)/);
|
|
1119
|
-
if (!prMatch) {
|
|
1120
|
-
throw new Error('Could not extract PR number from URL');
|
|
1121
|
-
}
|
|
1122
|
-
const prNumber = prMatch[1];
|
|
1123
|
-
// Get repo from list
|
|
1124
|
-
const listResult = await client.getList(listId);
|
|
1125
|
-
const repoString = listResult.data?.githubRepositoryId || listResult.data?.repository;
|
|
1126
|
-
if (!repoString) {
|
|
1127
|
-
throw new Error('No repository configured for this list');
|
|
1128
|
-
}
|
|
1129
|
-
const { execSync } = await import('child_process');
|
|
1130
|
-
execSync(`gh pr merge ${prNumber} --merge --repo ${repoString}`, { stdio: 'inherit' });
|
|
1131
|
-
console.log(`ā
PR #${prNumber} merged successfully!`);
|
|
1132
|
-
// Deploy to production if Vercel token is configured
|
|
1133
|
-
let productionUrl;
|
|
1134
|
-
if (process.env.VERCEL_TOKEN) {
|
|
1135
|
-
console.log(`\nš Deploying to production...`);
|
|
1136
|
-
try {
|
|
1137
|
-
const deployOutput = execSync('vercel --prod --yes', {
|
|
1138
|
-
cwd: CONFIG.defaultProjectPath,
|
|
1139
|
-
encoding: 'utf-8',
|
|
1140
|
-
timeout: 300000, // 5 minute timeout
|
|
1141
|
-
env: { ...process.env },
|
|
1142
|
-
});
|
|
1143
|
-
// Extract production URL from output
|
|
1144
|
-
const urlMatch = deployOutput.match(/https:\/\/[^\s]+\.vercel\.app/);
|
|
1145
|
-
if (urlMatch) {
|
|
1146
|
-
productionUrl = urlMatch[0];
|
|
1147
|
-
}
|
|
1148
|
-
console.log(`ā
Production deployment complete!`);
|
|
1149
|
-
}
|
|
1150
|
-
catch (deployError) {
|
|
1151
|
-
console.error(`ā ļø Production deployment failed:`, deployError);
|
|
1152
|
-
// Continue - merge was successful, just deployment failed
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
// Post success comment
|
|
1156
|
-
let successMessage = `š **Shipped!**\n\n` +
|
|
1157
|
-
`PR #${prNumber} has been merged to main.\n\n`;
|
|
1158
|
-
if (productionUrl) {
|
|
1159
|
-
successMessage += `š **Production:** [${productionUrl}](${productionUrl})\n\n`;
|
|
1160
|
-
}
|
|
1161
|
-
else if (process.env.VERCEL_TOKEN) {
|
|
1162
|
-
successMessage += `ā ļø Production deployment may have failed - check Vercel dashboard.\n\n`;
|
|
1163
|
-
}
|
|
1164
|
-
else {
|
|
1165
|
-
successMessage += `The changes will be live after deployment completes.\n\n`;
|
|
1166
|
-
}
|
|
1167
|
-
successMessage += `---\n*Merged by ${agentName}*`;
|
|
1168
|
-
await client.addComment(task.id, successMessage, agentUserId || undefined);
|
|
1169
|
-
}
|
|
1170
|
-
catch (mergeError) {
|
|
1171
|
-
console.error('ā Failed to merge PR:', mergeError);
|
|
1172
|
-
await client.addComment(task.id, `ā **Merge Failed**\n\n` +
|
|
1173
|
-
`Could not merge PR: ${mergeError instanceof Error ? mergeError.message : String(mergeError)}\n\n` +
|
|
1174
|
-
`Please merge manually: ${status.prUrl}`, agentUserId || undefined);
|
|
1175
|
-
}
|
|
1176
|
-
finally {
|
|
1177
|
-
processingTasks.delete(task.id);
|
|
1178
|
-
}
|
|
1179
|
-
continue;
|
|
1180
|
-
}
|
|
1181
|
-
// Mark as processing
|
|
1182
|
-
processingTasks.add(task.id);
|
|
1183
|
-
// Post starting comment
|
|
1184
|
-
await client.addComment(task.id, `š„ļø **${agentName} Starting**\n\n` +
|
|
1185
|
-
`**Task:** ${task.title}\n` +
|
|
1186
|
-
`**Mode:** Terminal (local Claude Code CLI)\n` +
|
|
1187
|
-
`**Model:** ${CONFIG.claudeModel}\n\n` +
|
|
1188
|
-
`Working on this task locally...\n\n` +
|
|
1189
|
-
`---\n*Using local Claude Code*`, agentUserId || undefined);
|
|
1190
|
-
// Format comments for terminal executor
|
|
1191
|
-
const formattedComments = comments.map(c => ({
|
|
1192
|
-
authorName: c.author?.name || 'Unknown',
|
|
1193
|
-
content: c.content,
|
|
1194
|
-
createdAt: c.createdAt,
|
|
1195
|
-
}));
|
|
1196
|
-
// Check if this is a follow-up (has previous agent activity)
|
|
1197
|
-
const hasAgentActivity = comments.some(c => isAIAgentComment(c));
|
|
1198
|
-
const isFollowUp = hasAgentActivity && comments.length > 1;
|
|
1199
|
-
// Process task using local Claude Code with comment posting
|
|
1200
|
-
const result = await processTaskTerminal({
|
|
1201
|
-
id: task.id,
|
|
1202
|
-
title: task.title,
|
|
1203
|
-
description: task.description,
|
|
1204
|
-
assigneeEmail,
|
|
1205
|
-
}, CONFIG.defaultProjectPath, formattedComments, isFollowUp,
|
|
1206
|
-
// Post intermediate comments (plans, questions, progress) to the task
|
|
1207
|
-
async (content) => {
|
|
1208
|
-
await client.addComment(task.id, content, agentUserId || undefined);
|
|
1209
|
-
});
|
|
1210
|
-
if (result.success) {
|
|
1211
|
-
// Post success comment
|
|
1212
|
-
let successMessage = `š **Implementation Complete!**\n\n`;
|
|
1213
|
-
if (result.summary) {
|
|
1214
|
-
successMessage += `${result.summary}\n\n`;
|
|
1215
|
-
}
|
|
1216
|
-
if (result.files && result.files.length > 0) {
|
|
1217
|
-
successMessage += `**Files modified:**\n${result.files.map(f => `- \`${f}\``).join('\n')}\n\n`;
|
|
1218
|
-
}
|
|
1219
|
-
if (result.prUrl) {
|
|
1220
|
-
successMessage += `š **Pull Request:** [${result.prUrl}](${result.prUrl})\n\n`;
|
|
1221
|
-
// Deploy Vercel preview if configured
|
|
1222
|
-
let previewUrl = '';
|
|
1223
|
-
if (CONFIG.vercelToken && result.prUrl) {
|
|
1224
|
-
// Extract branch name from PR URL or use result
|
|
1225
|
-
const branchMatch = result.prUrl.match(/\/pull\/(\d+)/);
|
|
1226
|
-
if (branchMatch) {
|
|
1227
|
-
// Try to get branch name - for now use task ID prefix
|
|
1228
|
-
const branchName = `task/${task.id.slice(0, 8)}`;
|
|
1229
|
-
const vercelResult = await deployVercelPreview(CONFIG.defaultProjectPath, branchName);
|
|
1230
|
-
if (vercelResult.success && vercelResult.previewUrl) {
|
|
1231
|
-
previewUrl = vercelResult.previewUrl;
|
|
1232
|
-
successMessage += `š **Preview:** [${previewUrl}](${previewUrl})\n\n`;
|
|
1233
|
-
console.log(` ā
Preview deployed: ${previewUrl}`);
|
|
1234
|
-
}
|
|
1235
|
-
else {
|
|
1236
|
-
console.log(` ā ļø Preview deployment skipped: ${vercelResult.error}`);
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
successMessage += `**What's next:**\n` +
|
|
1241
|
-
`1. Review the changes in the PR\n` +
|
|
1242
|
-
(previewUrl ? `2. Test the preview: ${previewUrl}\n` : `2. Test the preview deployment\n`) +
|
|
1243
|
-
`3. Comment "ship it" to merge!\n\n`;
|
|
1244
|
-
}
|
|
1245
|
-
successMessage += `---\n*Generated by ${agentName}*`;
|
|
1246
|
-
await client.addComment(task.id, successMessage, agentUserId || undefined);
|
|
1247
|
-
// Unassign task so it goes back to user
|
|
1248
|
-
await client.reassignTask(task.id, null).catch(err => {
|
|
1249
|
-
console.log(` ā ļø Could not unassign task: ${err}`);
|
|
1250
|
-
});
|
|
1251
|
-
// Mark as completed with cooldown to prevent rapid reprocessing
|
|
1252
|
-
markTaskCompleted(task.id);
|
|
1253
|
-
}
|
|
1254
|
-
else {
|
|
1255
|
-
// Post failure comment
|
|
1256
|
-
await client.addComment(task.id, `ā **Processing Failed**\n\n` +
|
|
1257
|
-
`Error: ${result.error}\n\n` +
|
|
1258
|
-
`---\n` +
|
|
1259
|
-
`š” **To try again:** Comment "retry" or "try again"\n` +
|
|
1260
|
-
`š” **To ship existing PR:** Comment "ship it"`, agentUserId || undefined);
|
|
1261
|
-
}
|
|
1262
|
-
processingTasks.delete(task.id);
|
|
1263
|
-
}
|
|
1264
|
-
catch (error) {
|
|
1265
|
-
console.error(`ā Failed to process task ${task.id}:`, error);
|
|
1266
|
-
processingTasks.delete(task.id);
|
|
1267
|
-
await client.addComment(task.id, `ā **Processing Failed**\n\n` +
|
|
1268
|
-
`Error: ${error instanceof Error ? error.message : String(error)}\n\n` +
|
|
1269
|
-
`---\n` +
|
|
1270
|
-
`š” **To try again:** Comment "retry" or "try again"`, agentUserId || undefined).catch(() => { });
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
catch (error) {
|
|
1277
|
-
console.error('ā Poll error:', error);
|
|
1278
|
-
}
|
|
1279
|
-
// Wait before next poll
|
|
1280
|
-
await new Promise(resolve => setTimeout(resolve, CONFIG.pollIntervalMs));
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
// ============================================================================
|
|
1284
|
-
// MAIN
|
|
1285
|
-
// ============================================================================
|
|
1286
|
-
function showHelp() {
|
|
1287
|
-
console.log(`
|
|
1288
|
-
@gracefultools/astrid-sdk - AI Agent Worker
|
|
1289
|
-
|
|
1290
|
-
Usage:
|
|
1291
|
-
npx astrid-agent Start polling for tasks (API mode)
|
|
1292
|
-
npx astrid-agent --terminal Start polling using local Claude Code CLI
|
|
1293
|
-
npx astrid-agent serve Start webhook server (for always-on servers)
|
|
1294
|
-
npx astrid-agent <taskId> Process a specific task
|
|
1295
|
-
npx astrid-agent --help Show this help
|
|
1296
|
-
|
|
1297
|
-
Modes:
|
|
1298
|
-
|
|
1299
|
-
API MODE (Default)
|
|
1300
|
-
------------------
|
|
1301
|
-
Best for: Cloud servers, CI/CD, when you don't have Claude Code CLI installed
|
|
1302
|
-
|
|
1303
|
-
Uses Claude Agent SDK API to process tasks remotely.
|
|
1304
|
-
|
|
1305
|
-
Environment:
|
|
1306
|
-
ASTRID_OAUTH_CLIENT_ID OAuth client ID
|
|
1307
|
-
ASTRID_OAUTH_CLIENT_SECRET OAuth client secret
|
|
1308
|
-
ASTRID_OAUTH_LIST_ID List ID to monitor
|
|
1309
|
-
ANTHROPIC_API_KEY For Claude tasks
|
|
1310
|
-
|
|
1311
|
-
Example:
|
|
1312
|
-
npx astrid-agent # Starts polling (API mode)
|
|
1313
|
-
|
|
1314
|
-
TERMINAL MODE (--terminal)
|
|
1315
|
-
--------------------------
|
|
1316
|
-
Best for: Local development, when you have Claude Code CLI installed
|
|
1317
|
-
|
|
1318
|
-
Uses your local Claude Code CLI (spawn) to process tasks.
|
|
1319
|
-
Enables remote control of your local Claude Code from Astrid.
|
|
1320
|
-
|
|
1321
|
-
Environment:
|
|
1322
|
-
ASTRID_TERMINAL_MODE=true Enable terminal mode (or use --terminal flag)
|
|
1323
|
-
CLAUDE_MODEL Model to use (default: opus)
|
|
1324
|
-
CLAUDE_MAX_TURNS Max turns per execution (default: 50)
|
|
1325
|
-
DEFAULT_PROJECT_PATH Project directory (default: current dir)
|
|
1326
|
-
|
|
1327
|
-
Example:
|
|
1328
|
-
npx astrid-agent --terminal
|
|
1329
|
-
# Or with environment variable:
|
|
1330
|
-
ASTRID_TERMINAL_MODE=true npx astrid-agent
|
|
1331
|
-
# With custom options:
|
|
1332
|
-
npx astrid-agent --terminal --model=sonnet --cwd=/path/to/project
|
|
1333
|
-
|
|
1334
|
-
WEBHOOK MODE (serve)
|
|
1335
|
-
--------------------
|
|
1336
|
-
Best for: Always-on servers with permanent IP (VPS, fly.io, etc.)
|
|
1337
|
-
|
|
1338
|
-
Astrid sends tasks directly to your server via webhook.
|
|
1339
|
-
|
|
1340
|
-
Environment:
|
|
1341
|
-
ASTRID_WEBHOOK_SECRET Secret from Astrid settings
|
|
1342
|
-
ASTRID_CALLBACK_URL Callback URL (optional)
|
|
1343
|
-
|
|
1344
|
-
Example:
|
|
1345
|
-
npx astrid-agent serve --port=3001
|
|
1346
|
-
|
|
1347
|
-
Common Environment Variables:
|
|
1348
|
-
# AI Provider Keys
|
|
1349
|
-
ANTHROPIC_API_KEY For Claude tasks (required)
|
|
1350
|
-
OPENAI_API_KEY For OpenAI tasks (optional)
|
|
1351
|
-
GEMINI_API_KEY For Gemini tasks (optional)
|
|
1352
|
-
|
|
1353
|
-
# GitHub (for repository access)
|
|
1354
|
-
GITHUB_TOKEN For cloning private repositories
|
|
1355
|
-
|
|
1356
|
-
Quick Start (Terminal Mode):
|
|
1357
|
-
# 1. Install Claude Code CLI and astrid-sdk
|
|
1358
|
-
npm install -g @anthropic-ai/claude-code @gracefultools/astrid-sdk
|
|
1359
|
-
|
|
1360
|
-
# 2. Set up environment
|
|
1361
|
-
export ANTHROPIC_API_KEY=sk-ant-...
|
|
1362
|
-
export ASTRID_OAUTH_CLIENT_ID=...
|
|
1363
|
-
export ASTRID_OAUTH_CLIENT_SECRET=...
|
|
1364
|
-
export ASTRID_OAUTH_LIST_ID=...
|
|
1365
|
-
|
|
1366
|
-
# 3. Start in terminal mode
|
|
1367
|
-
cd /your/project
|
|
1368
|
-
npx astrid-agent --terminal
|
|
1369
|
-
|
|
1370
|
-
# Now create a task in Astrid and assign to claude@astrid.cc
|
|
1371
|
-
# Your local Claude Code will process it!
|
|
1372
|
-
|
|
1373
|
-
Quick Start (API Mode):
|
|
1374
|
-
# 1. Install globally
|
|
1375
|
-
npm install -g @gracefultools/astrid-sdk
|
|
1376
|
-
|
|
1377
|
-
# 2. Set up environment (same as above)
|
|
1378
|
-
# 3. Start polling
|
|
1379
|
-
npx astrid-agent
|
|
1380
|
-
`);
|
|
1381
|
-
}
|
|
1382
|
-
async function startWebhookServer(port) {
|
|
1383
|
-
// Check for required environment variables
|
|
1384
|
-
const webhookSecret = process.env.ASTRID_WEBHOOK_SECRET;
|
|
1385
|
-
if (!webhookSecret) {
|
|
1386
|
-
console.error(`ā ASTRID_WEBHOOK_SECRET not configured
|
|
1387
|
-
|
|
1388
|
-
To set up:
|
|
1389
|
-
1. Go to Settings -> AI Agent Settings in Astrid
|
|
1390
|
-
2. Configure your webhook URL: http://your-server:${port}/webhook
|
|
1391
|
-
3. Copy the webhook secret to your .env file:
|
|
1392
|
-
|
|
1393
|
-
ASTRID_WEBHOOK_SECRET=<your-secret-here>
|
|
1394
|
-
`);
|
|
1395
|
-
process.exit(1);
|
|
1396
|
-
}
|
|
1397
|
-
// Check for at least one AI provider
|
|
1398
|
-
const hasProvider = CONFIG.anthropicApiKey || CONFIG.openaiApiKey || CONFIG.geminiApiKey;
|
|
1399
|
-
if (!hasProvider) {
|
|
1400
|
-
console.warn(`ā ļø No AI provider keys configured. Add at least one:
|
|
1401
|
-
ANTHROPIC_API_KEY=sk-ant-... (for Claude)
|
|
1402
|
-
OPENAI_API_KEY=sk-... (for OpenAI)
|
|
1403
|
-
GEMINI_API_KEY=AIza... (for Gemini)
|
|
1404
|
-
`);
|
|
1405
|
-
}
|
|
1406
|
-
// Import and start the server from the server module
|
|
1407
|
-
try {
|
|
1408
|
-
const { startServer } = await import('../server/index.js');
|
|
1409
|
-
await startServer({
|
|
1410
|
-
port,
|
|
1411
|
-
webhookSecret,
|
|
1412
|
-
callbackUrl: process.env.ASTRID_CALLBACK_URL,
|
|
1413
|
-
});
|
|
1414
|
-
}
|
|
1415
|
-
catch (error) {
|
|
1416
|
-
if (error.message?.includes('express')) {
|
|
1417
|
-
console.error(`ā Express not installed. Install it with:
|
|
1418
|
-
npm install express
|
|
1419
|
-
`);
|
|
1420
|
-
}
|
|
1421
|
-
else {
|
|
1422
|
-
console.error(`ā Failed to start server:`, error);
|
|
1423
|
-
}
|
|
1424
|
-
process.exit(1);
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
async function main() {
|
|
1428
|
-
const args = process.argv.slice(2);
|
|
1429
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
1430
|
-
showHelp();
|
|
1431
|
-
process.exit(0);
|
|
1432
|
-
}
|
|
1433
|
-
// Parse --terminal flag
|
|
1434
|
-
if (args.includes('--terminal')) {
|
|
1435
|
-
CONFIG.terminalMode = true;
|
|
1436
|
-
}
|
|
1437
|
-
// Parse --model flag
|
|
1438
|
-
const modelArg = args.find(a => a.startsWith('--model='));
|
|
1439
|
-
if (modelArg) {
|
|
1440
|
-
CONFIG.claudeModel = modelArg.split('=')[1];
|
|
1441
|
-
}
|
|
1442
|
-
// Parse --cwd flag
|
|
1443
|
-
const cwdArg = args.find(a => a.startsWith('--cwd='));
|
|
1444
|
-
if (cwdArg) {
|
|
1445
|
-
CONFIG.defaultProjectPath = cwdArg.split('=')[1];
|
|
1446
|
-
}
|
|
1447
|
-
// Parse --max-turns flag
|
|
1448
|
-
const maxTurnsArg = args.find(a => a.startsWith('--max-turns='));
|
|
1449
|
-
if (maxTurnsArg) {
|
|
1450
|
-
CONFIG.claudeMaxTurns = parseInt(maxTurnsArg.split('=')[1], 10) || 50;
|
|
1451
|
-
}
|
|
1452
|
-
// Handle 'serve' command
|
|
1453
|
-
if (args[0] === 'serve') {
|
|
1454
|
-
let port = 3001;
|
|
1455
|
-
const portArg = args.find(a => a.startsWith('--port='));
|
|
1456
|
-
if (portArg) {
|
|
1457
|
-
port = parseInt(portArg.split('=')[1], 10) || 3001;
|
|
1458
|
-
}
|
|
1459
|
-
await startWebhookServer(port);
|
|
1460
|
-
return;
|
|
1461
|
-
}
|
|
1462
|
-
// Handle specific task ID (first non-flag argument)
|
|
1463
|
-
const taskIdArg = args.find(a => !a.startsWith('-') && a !== 'serve');
|
|
1464
|
-
if (taskIdArg) {
|
|
1465
|
-
// Process a specific task
|
|
1466
|
-
const taskId = taskIdArg;
|
|
1467
|
-
console.log(`\nšÆ Processing task: ${taskId}`);
|
|
1468
|
-
const client = new astrid_oauth_js_1.AstridOAuthClient();
|
|
1469
|
-
const taskResult = await client.getTask(taskId);
|
|
1470
|
-
if (!taskResult.success || !taskResult.data) {
|
|
1471
|
-
console.error(`ā Failed to get task: ${taskResult.error}`);
|
|
1472
|
-
process.exit(1);
|
|
1473
|
-
}
|
|
1474
|
-
const projectPath = CONFIG.defaultProjectPath;
|
|
1475
|
-
if (CONFIG.terminalMode) {
|
|
1476
|
-
console.log(`\nš„ļø Terminal mode enabled`);
|
|
1477
|
-
// Initialize OAuth client for posting comments
|
|
1478
|
-
const taskData = taskResult.data;
|
|
1479
|
-
const assigneeEmail = getAssigneeEmail(taskData);
|
|
1480
|
-
const agentUserId = await client.getAgentIdByEmail(assigneeEmail || 'claude@astrid.cc');
|
|
1481
|
-
const agentName = assigneeEmail?.includes('gemini') ? 'Gemini AI Agent (Terminal)' :
|
|
1482
|
-
assigneeEmail?.includes('openai') ? 'OpenAI Agent (Terminal)' :
|
|
1483
|
-
'Claude AI Agent (Terminal)';
|
|
1484
|
-
// Post starting comment
|
|
1485
|
-
await client.addComment(taskData.id, `š„ļø **${agentName.replace(' (Terminal)', '')} (Terminal) Starting**\n\n` +
|
|
1486
|
-
`**Task:** ${taskData.title}\n` +
|
|
1487
|
-
`**Mode:** Terminal (local execution)\n\n` +
|
|
1488
|
-
`Working on this task locally...\n\n` +
|
|
1489
|
-
`---\n*Using local Claude Code*`, agentUserId || undefined).catch(() => { });
|
|
1490
|
-
const result = await processTaskTerminal({
|
|
1491
|
-
id: taskResult.data.id,
|
|
1492
|
-
title: taskResult.data.title,
|
|
1493
|
-
description: taskResult.data.description,
|
|
1494
|
-
assigneeEmail,
|
|
1495
|
-
}, projectPath, undefined, // No previous comments for specific task processing
|
|
1496
|
-
false, // Not a follow-up
|
|
1497
|
-
// Post intermediate comments (plans, questions, progress) to the task
|
|
1498
|
-
async (content) => {
|
|
1499
|
-
await client.addComment(taskData.id, content, agentUserId || undefined);
|
|
1500
|
-
});
|
|
1501
|
-
if (!result.success) {
|
|
1502
|
-
// Post failure comment
|
|
1503
|
-
await client.addComment(taskData.id, `ā **Processing Failed**\n\n` +
|
|
1504
|
-
`Error: ${result.error}\n\n` +
|
|
1505
|
-
`---\n` +
|
|
1506
|
-
`š” **To try again:** Comment "retry" or "try again"`, agentUserId || undefined).catch(() => { });
|
|
1507
|
-
console.error(`ā Task failed: ${result.error}`);
|
|
1508
|
-
process.exit(1);
|
|
1509
|
-
}
|
|
1510
|
-
// Build success message
|
|
1511
|
-
let successMessage = `š **Implementation Complete!**\n\n`;
|
|
1512
|
-
if (result.files && result.files.length > 0) {
|
|
1513
|
-
successMessage += `**Files modified:**\n${result.files.map(f => `- \`${f}\``).join('\n')}\n\n`;
|
|
1514
|
-
}
|
|
1515
|
-
if (result.prUrl) {
|
|
1516
|
-
successMessage += `š **Pull Request:** [${result.prUrl}](${result.prUrl})\n\n`;
|
|
1517
|
-
// Deploy Vercel preview if configured
|
|
1518
|
-
let previewUrl = '';
|
|
1519
|
-
if (CONFIG.vercelToken) {
|
|
1520
|
-
const branchName = `task/${taskData.id.slice(0, 8)}`;
|
|
1521
|
-
console.log(`\nš Deploying Vercel preview...`);
|
|
1522
|
-
const vercelResult = await deployVercelPreview(projectPath, branchName);
|
|
1523
|
-
if (vercelResult.success && vercelResult.previewUrl) {
|
|
1524
|
-
previewUrl = vercelResult.previewUrl;
|
|
1525
|
-
successMessage += `š **Preview:** [${previewUrl}](${previewUrl})\n\n`;
|
|
1526
|
-
console.log(` ā
Preview deployed: ${previewUrl}`);
|
|
1527
|
-
}
|
|
1528
|
-
else {
|
|
1529
|
-
console.log(` ā ļø Preview deployment skipped: ${vercelResult.error}`);
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
successMessage += `**What's next:**\n` +
|
|
1533
|
-
`1. Review the changes in the PR\n` +
|
|
1534
|
-
(previewUrl ? `2. Test the preview: ${previewUrl}\n` : `2. Test the preview deployment\n`) +
|
|
1535
|
-
`3. Comment "ship it" to merge!\n\n`;
|
|
1536
|
-
}
|
|
1537
|
-
successMessage += `---\n*Generated by ${agentName}*`;
|
|
1538
|
-
// Post success comment
|
|
1539
|
-
await client.addComment(taskData.id, successMessage, agentUserId || undefined).catch(() => { });
|
|
1540
|
-
console.log(`\nā
Task completed successfully`);
|
|
1541
|
-
if (result.prUrl) {
|
|
1542
|
-
console.log(` PR: ${result.prUrl}`);
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
else {
|
|
1546
|
-
// Initialize OAuth client for posting comments
|
|
1547
|
-
const client = new astrid_oauth_js_1.AstridOAuthClient();
|
|
1548
|
-
const taskData = taskResult.data;
|
|
1549
|
-
const assigneeEmail = getAssigneeEmail(taskData);
|
|
1550
|
-
const agentUserId = await client.getAgentIdByEmail(assigneeEmail || 'claude@astrid.cc');
|
|
1551
|
-
await processTask({
|
|
1552
|
-
id: taskData.id,
|
|
1553
|
-
title: taskData.title,
|
|
1554
|
-
description: taskData.description,
|
|
1555
|
-
assigneeEmail,
|
|
1556
|
-
}, projectPath, {
|
|
1557
|
-
onComment: async (message) => {
|
|
1558
|
-
try {
|
|
1559
|
-
await client.addComment(taskData.id, message, agentUserId || undefined);
|
|
1560
|
-
}
|
|
1561
|
-
catch (err) {
|
|
1562
|
-
console.log(` (Failed to post comment: ${err instanceof Error ? err.message : 'unknown error'})`);
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
});
|
|
1566
|
-
}
|
|
1567
|
-
return;
|
|
1568
|
-
}
|
|
1569
|
-
// Default: Run polling worker
|
|
1570
|
-
if (CONFIG.terminalMode) {
|
|
1571
|
-
console.log(`
|
|
1572
|
-
š„ļø Starting terminal mode...
|
|
1573
|
-
|
|
1574
|
-
Terminal mode uses your local Claude Code CLI to process tasks.
|
|
1575
|
-
This enables remote control of your local Claude Code from Astrid.
|
|
1576
|
-
|
|
1577
|
-
Settings:
|
|
1578
|
-
- Model: ${CONFIG.claudeModel}
|
|
1579
|
-
- Max turns: ${CONFIG.claudeMaxTurns}
|
|
1580
|
-
- Project path: ${CONFIG.defaultProjectPath}
|
|
1581
|
-
|
|
1582
|
-
Polling for tasks every ${CONFIG.pollIntervalMs / 1000}s...
|
|
1583
|
-
|
|
1584
|
-
`);
|
|
1585
|
-
await runWorkerTerminal();
|
|
1586
|
-
}
|
|
1587
|
-
else {
|
|
1588
|
-
console.log(`
|
|
1589
|
-
š Starting polling mode (API)...
|
|
1590
|
-
|
|
1591
|
-
Polling is ideal for:
|
|
1592
|
-
- Local devices behind NAT/firewalls
|
|
1593
|
-
- Laptops and home servers
|
|
1594
|
-
- Intermittent connectivity
|
|
1595
|
-
|
|
1596
|
-
For terminal mode (uses local Claude Code CLI):
|
|
1597
|
-
npx astrid-agent --terminal
|
|
1598
|
-
|
|
1599
|
-
For always-on servers with permanent IPs, consider:
|
|
1600
|
-
npx astrid-agent serve --port=3001
|
|
1601
|
-
|
|
1602
|
-
`);
|
|
1603
|
-
await runWorker();
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
main().catch(error => {
|
|
1607
|
-
console.error('ā Fatal error:', error);
|
|
1608
|
-
process.exit(1);
|
|
1609
|
-
});
|
|
1610
|
-
//# sourceMappingURL=cli.js.map
|