@gracefultools/astrid-sdk 0.7.16 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +127 -341
  2. package/dist/channel/channel.d.ts +33 -0
  3. package/dist/channel/channel.d.ts.map +1 -0
  4. package/dist/channel/channel.js +90 -0
  5. package/dist/channel/channel.js.map +1 -0
  6. package/dist/channel/index.d.ts +13 -0
  7. package/dist/channel/index.d.ts.map +1 -0
  8. package/dist/channel/index.js +23 -0
  9. package/dist/channel/index.js.map +1 -0
  10. package/dist/channel/message-formatter.d.ts +14 -0
  11. package/dist/channel/message-formatter.d.ts.map +1 -0
  12. package/dist/channel/message-formatter.js +71 -0
  13. package/dist/channel/message-formatter.js.map +1 -0
  14. package/dist/channel/oauth-client.d.ts +15 -0
  15. package/dist/channel/oauth-client.d.ts.map +1 -0
  16. package/dist/channel/oauth-client.js +45 -0
  17. package/dist/channel/oauth-client.js.map +1 -0
  18. package/dist/channel/rest-client.d.ts +16 -0
  19. package/dist/channel/rest-client.d.ts.map +1 -0
  20. package/dist/channel/rest-client.js +66 -0
  21. package/dist/channel/rest-client.js.map +1 -0
  22. package/dist/channel/session-mapper.d.ts +14 -0
  23. package/dist/channel/session-mapper.d.ts.map +1 -0
  24. package/dist/channel/session-mapper.js +37 -0
  25. package/dist/channel/session-mapper.js.map +1 -0
  26. package/dist/channel/sse-client.d.ts +31 -0
  27. package/dist/channel/sse-client.d.ts.map +1 -0
  28. package/dist/channel/sse-client.js +171 -0
  29. package/dist/channel/sse-client.js.map +1 -0
  30. package/dist/channel/types.d.ts +65 -0
  31. package/dist/channel/types.d.ts.map +1 -0
  32. package/dist/channel/types.js +3 -0
  33. package/dist/channel/types.js.map +1 -0
  34. package/dist/config/agent-workflow.js +7 -7
  35. package/dist/index.d.ts +1 -8
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +16 -30
  38. package/dist/index.js.map +1 -1
  39. package/dist/types/index.d.ts +1 -1
  40. package/dist/types/index.d.ts.map +1 -1
  41. package/dist/utils/agent-config.d.ts.map +1 -1
  42. package/dist/utils/agent-config.js +14 -0
  43. package/dist/utils/agent-config.js.map +1 -1
  44. package/openclaw.plugin.json +25 -0
  45. package/package.json +66 -77
  46. package/templates/.astrid.config.json +60 -60
  47. package/templates/ASTRID.template.md +74 -74
  48. package/dist/bin/cli.d.ts +0 -14
  49. package/dist/bin/cli.d.ts.map +0 -1
  50. package/dist/bin/cli.js +0 -1610
  51. package/dist/bin/cli.js.map +0 -1
  52. package/dist/executors/claude.d.ts +0 -65
  53. package/dist/executors/claude.d.ts.map +0 -1
  54. package/dist/executors/claude.js +0 -838
  55. package/dist/executors/claude.js.map +0 -1
  56. package/dist/executors/gemini.d.ts +0 -23
  57. package/dist/executors/gemini.d.ts.map +0 -1
  58. package/dist/executors/gemini.js +0 -558
  59. package/dist/executors/gemini.js.map +0 -1
  60. package/dist/executors/openai.d.ts +0 -17
  61. package/dist/executors/openai.d.ts.map +0 -1
  62. package/dist/executors/openai.js +0 -614
  63. package/dist/executors/openai.js.map +0 -1
  64. package/dist/executors/shared/index.d.ts +0 -9
  65. package/dist/executors/shared/index.d.ts.map +0 -1
  66. package/dist/executors/shared/index.js +0 -21
  67. package/dist/executors/shared/index.js.map +0 -1
  68. package/dist/executors/shared/tool-executor.d.ts +0 -52
  69. package/dist/executors/shared/tool-executor.d.ts.map +0 -1
  70. package/dist/executors/shared/tool-executor.js +0 -262
  71. package/dist/executors/shared/tool-executor.js.map +0 -1
  72. package/dist/executors/shared/tool-schemas.d.ts +0 -61
  73. package/dist/executors/shared/tool-schemas.d.ts.map +0 -1
  74. package/dist/executors/shared/tool-schemas.js +0 -135
  75. package/dist/executors/shared/tool-schemas.js.map +0 -1
  76. package/dist/executors/terminal-base.d.ts +0 -207
  77. package/dist/executors/terminal-base.d.ts.map +0 -1
  78. package/dist/executors/terminal-base.js +0 -552
  79. package/dist/executors/terminal-base.js.map +0 -1
  80. package/dist/executors/terminal-claude.d.ts +0 -116
  81. package/dist/executors/terminal-claude.d.ts.map +0 -1
  82. package/dist/executors/terminal-claude.js +0 -700
  83. package/dist/executors/terminal-claude.js.map +0 -1
  84. package/dist/executors/terminal-executors.test.d.ts +0 -8
  85. package/dist/executors/terminal-executors.test.d.ts.map +0 -1
  86. package/dist/executors/terminal-executors.test.js +0 -469
  87. package/dist/executors/terminal-executors.test.js.map +0 -1
  88. package/dist/executors/terminal-gemini.d.ts +0 -50
  89. package/dist/executors/terminal-gemini.d.ts.map +0 -1
  90. package/dist/executors/terminal-gemini.js +0 -401
  91. package/dist/executors/terminal-gemini.js.map +0 -1
  92. package/dist/executors/terminal-openai.d.ts +0 -50
  93. package/dist/executors/terminal-openai.d.ts.map +0 -1
  94. package/dist/executors/terminal-openai.js +0 -405
  95. package/dist/executors/terminal-openai.js.map +0 -1
  96. package/dist/server/astrid-client.d.ts +0 -77
  97. package/dist/server/astrid-client.d.ts.map +0 -1
  98. package/dist/server/astrid-client.js +0 -125
  99. package/dist/server/astrid-client.js.map +0 -1
  100. package/dist/server/index.d.ts +0 -38
  101. package/dist/server/index.d.ts.map +0 -1
  102. package/dist/server/index.js +0 -408
  103. package/dist/server/index.js.map +0 -1
  104. package/dist/server/repo-manager.d.ts +0 -41
  105. package/dist/server/repo-manager.d.ts.map +0 -1
  106. package/dist/server/repo-manager.js +0 -177
  107. package/dist/server/repo-manager.js.map +0 -1
  108. package/dist/server/session-manager.d.ts +0 -93
  109. package/dist/server/session-manager.d.ts.map +0 -1
  110. package/dist/server/session-manager.js +0 -217
  111. package/dist/server/session-manager.js.map +0 -1
  112. package/dist/server/webhook-signature.d.ts +0 -23
  113. package/dist/server/webhook-signature.d.ts.map +0 -1
  114. package/dist/server/webhook-signature.js +0 -74
  115. package/dist/server/webhook-signature.js.map +0 -1
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