@gracefultools/astrid-sdk 0.4.1 → 0.6.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/dist/bin/cli.js CHANGED
@@ -6,7 +6,8 @@
6
6
  * Command-line interface for running the Astrid AI agent worker
7
7
  *
8
8
  * Usage:
9
- * npx astrid-agent # Start polling for tasks
9
+ * npx astrid-agent # Start polling for tasks (API mode)
10
+ * npx astrid-agent --terminal # Start polling using local Claude Code CLI
10
11
  * npx astrid-agent <taskId> # Process a specific task
11
12
  * npx astrid-agent --help # Show help
12
13
  */
@@ -50,6 +51,7 @@ const path = __importStar(require("path"));
50
51
  dotenv.config({ path: path.resolve(process.cwd(), '.env.local') });
51
52
  dotenv.config({ path: path.resolve(process.cwd(), '.env') });
52
53
  const claude_js_1 = require("../executors/claude.js");
54
+ const terminal_claude_js_1 = require("../executors/terminal-claude.js");
53
55
  const openai_js_1 = require("../executors/openai.js");
54
56
  const gemini_js_1 = require("../executors/gemini.js");
55
57
  const agent_config_js_1 = require("../utils/agent-config.js");
@@ -73,6 +75,11 @@ const CONFIG = {
73
75
  vercelToken: process.env.VERCEL_TOKEN,
74
76
  // iOS TestFlight
75
77
  testflightPublicLink: process.env.TESTFLIGHT_PUBLIC_LINK,
78
+ // Terminal mode settings
79
+ terminalMode: process.env.ASTRID_TERMINAL_MODE === 'true',
80
+ claudeModel: process.env.CLAUDE_MODEL || 'opus',
81
+ claudeMaxTurns: parseInt(process.env.CLAUDE_MAX_TURNS || '50', 10),
82
+ defaultProjectPath: process.env.DEFAULT_PROJECT_PATH || process.cwd(),
76
83
  };
77
84
  function getApiKeyForService(service) {
78
85
  switch (service) {
@@ -180,6 +187,76 @@ async function processTask(task, repoPath) {
180
187
  }
181
188
  }
182
189
  }
190
+ /**
191
+ * Process a task using the local Claude Code CLI (terminal mode)
192
+ */
193
+ async function processTaskTerminal(task, projectPath, comments, isFollowUp) {
194
+ const executor = new terminal_claude_js_1.TerminalClaudeExecutor({
195
+ model: CONFIG.claudeModel,
196
+ maxTurns: CONFIG.claudeMaxTurns,
197
+ });
198
+ // Check if Claude Code CLI is available
199
+ const isAvailable = await executor.checkAvailable();
200
+ if (!isAvailable) {
201
+ return {
202
+ success: false,
203
+ error: 'Claude Code CLI not found. Install it with: npm install -g @anthropic-ai/claude-code'
204
+ };
205
+ }
206
+ console.log(`\nšŸ–„ļø Terminal Mode: Processing task with local Claude Code CLI`);
207
+ console.log(` Task: ${task.title}`);
208
+ console.log(` Project: ${projectPath}`);
209
+ console.log(` Model: ${CONFIG.claudeModel}`);
210
+ console.log(` Max turns: ${CONFIG.claudeMaxTurns}`);
211
+ // Create session object
212
+ const session = {
213
+ id: task.id,
214
+ taskId: task.id,
215
+ title: task.title,
216
+ description: task.description || '',
217
+ projectPath,
218
+ provider: 'claude',
219
+ claudeSessionId: undefined,
220
+ status: 'pending',
221
+ createdAt: new Date().toISOString(),
222
+ updatedAt: new Date().toISOString(),
223
+ messageCount: 0,
224
+ };
225
+ const context = { comments };
226
+ let result;
227
+ // Check if we should resume an existing session
228
+ const existingSessionId = await terminal_claude_js_1.terminalSessionStore.getClaudeSessionId(task.id);
229
+ if (isFollowUp && existingSessionId) {
230
+ // Get the last user comment as the follow-up input
231
+ const lastUserComment = comments?.filter(c => !c.authorName.includes('Agent'))?.pop();
232
+ const input = lastUserComment?.content || 'Please continue with the task.';
233
+ console.log(`\nšŸ”„ Resuming existing session: ${existingSessionId}`);
234
+ result = await executor.resumeSession(session, input, context, (msg) => {
235
+ console.log(` → ${msg}`);
236
+ });
237
+ }
238
+ else {
239
+ console.log(`\nšŸš€ Starting new Claude Code session...`);
240
+ result = await executor.startSession(session, undefined, context, (msg) => {
241
+ console.log(` → ${msg}`);
242
+ });
243
+ }
244
+ // Parse the output
245
+ const parsed = executor.parseOutput(result.stdout);
246
+ if (result.exitCode !== 0 && result.exitCode !== null) {
247
+ return {
248
+ success: false,
249
+ error: parsed.error || `Claude Code exited with code ${result.exitCode}`,
250
+ files: result.modifiedFiles,
251
+ };
252
+ }
253
+ return {
254
+ success: true,
255
+ prUrl: result.prUrl || parsed.prUrl,
256
+ files: result.modifiedFiles || parsed.files,
257
+ summary: parsed.summary,
258
+ };
259
+ }
183
260
  function isStartingMarker(content) {
184
261
  const lower = content.toLowerCase();
185
262
  return content.includes('**Starting work**') ||
@@ -234,9 +311,34 @@ function isTerminalState(content) {
234
311
  }
235
312
  function isRetryComment(content) {
236
313
  const lower = content.toLowerCase().trim();
237
- return lower === 'retry' ||
238
- lower.startsWith('retry ') ||
239
- lower.includes('please reprocess');
314
+ // Match various ways users might ask for retry
315
+ const retryPhrases = [
316
+ 'retry',
317
+ 'try again',
318
+ 'tryagain',
319
+ 'please retry',
320
+ 'retry please',
321
+ 'reprocess',
322
+ 'please reprocess',
323
+ 'redo',
324
+ 'do again',
325
+ 'run again',
326
+ 'try it again',
327
+ 'give it another try',
328
+ 'one more time',
329
+ 'again please',
330
+ 'again',
331
+ ];
332
+ for (const phrase of retryPhrases) {
333
+ if (lower === phrase || lower.startsWith(phrase + ' ') || lower.endsWith(' ' + phrase)) {
334
+ return true;
335
+ }
336
+ }
337
+ // Also match if "retry" or "try again" appears anywhere in a short message
338
+ if (lower.length < 50 && (lower.includes('retry') || lower.includes('try again'))) {
339
+ return true;
340
+ }
341
+ return false;
240
342
  }
241
343
  function isShipItComment(content) {
242
344
  const lower = content.toLowerCase().trim();
@@ -335,7 +437,7 @@ function shouldProcessTask(comments) {
335
437
  if (mostRecentUserComment && isRetryComment(mostRecentUserComment.content)) {
336
438
  return { shouldProcess: true, reason: 'User requested retry after failure' };
337
439
  }
338
- return { shouldProcess: false, reason: 'Task failed - comment "retry" to try again' };
440
+ return { shouldProcess: false, reason: 'Task failed - comment "retry" or "try again" to retry' };
339
441
  }
340
442
  // Other completion - don't reprocess unless explicit retry
341
443
  if (mostRecentUserComment && isRetryComment(mostRecentUserComment.content)) {
@@ -696,7 +798,207 @@ async function runWorker() {
696
798
  // Post error comment (agentUserId may be undefined if error occurred before lookup)
697
799
  await client.addComment(task.id, `āŒ **Processing Failed**\n\n` +
698
800
  `Error: ${error instanceof Error ? error.message : String(error)}\n\n` +
699
- `Please check the configuration and try again.`, agentUserId || undefined).catch(() => { });
801
+ `---\n` +
802
+ `šŸ’” **To try again:** Comment "retry" or "try again"\n` +
803
+ `šŸ’” **To ship existing PR:** Comment "ship it"`, agentUserId || undefined).catch(() => { });
804
+ }
805
+ }
806
+ }
807
+ }
808
+ }
809
+ catch (error) {
810
+ console.error('āŒ Poll error:', error);
811
+ }
812
+ // Wait before next poll
813
+ await new Promise(resolve => setTimeout(resolve, CONFIG.pollIntervalMs));
814
+ }
815
+ }
816
+ // ============================================================================
817
+ // TERMINAL MODE WORKER LOOP
818
+ // ============================================================================
819
+ /**
820
+ * Terminal mode worker loop - uses local Claude Code CLI instead of API
821
+ */
822
+ async function runWorkerTerminal() {
823
+ const client = new astrid_oauth_js_1.AstridOAuthClient();
824
+ if (!client.isConfigured()) {
825
+ console.error('āŒ OAuth credentials not configured');
826
+ console.error(' Set ASTRID_OAUTH_CLIENT_ID and ASTRID_OAUTH_CLIENT_SECRET');
827
+ process.exit(1);
828
+ }
829
+ const listId = CONFIG.astridListId;
830
+ if (!listId) {
831
+ console.error('āŒ ASTRID_OAUTH_LIST_ID not configured');
832
+ process.exit(1);
833
+ }
834
+ // Check Claude Code CLI availability
835
+ const executor = new terminal_claude_js_1.TerminalClaudeExecutor({
836
+ model: CONFIG.claudeModel,
837
+ maxTurns: CONFIG.claudeMaxTurns,
838
+ });
839
+ const isAvailable = await executor.checkAvailable();
840
+ if (!isAvailable) {
841
+ console.error('āŒ Claude Code CLI not found');
842
+ console.error(' Install it with: npm install -g @anthropic-ai/claude-code');
843
+ console.error(' Or use API mode: npx astrid-agent (without --terminal)');
844
+ process.exit(1);
845
+ }
846
+ console.log('šŸ¤– Astrid Agent Worker (Terminal Mode)');
847
+ console.log(` List ID: ${listId}`);
848
+ console.log(` Poll interval: ${CONFIG.pollIntervalMs}ms`);
849
+ console.log(` Project path: ${CONFIG.defaultProjectPath}`);
850
+ console.log(` Model: ${CONFIG.claudeModel}`);
851
+ console.log('');
852
+ // Test connection
853
+ const testResult = await client.testConnection();
854
+ if (!testResult.success) {
855
+ console.error(`āŒ Connection failed: ${testResult.error}`);
856
+ process.exit(1);
857
+ }
858
+ console.log('āœ… Connected to Astrid API');
859
+ console.log('āœ… Claude Code CLI available\n');
860
+ // Polling loop
861
+ while (true) {
862
+ try {
863
+ const tasksResult = await client.getTasks(listId, false);
864
+ if (tasksResult.success && tasksResult.data) {
865
+ // Filter for incomplete tasks assigned to AI agents
866
+ const eligibleTasks = tasksResult.data.filter(task => {
867
+ if (task.completed)
868
+ return false;
869
+ const email = getAssigneeEmail(task);
870
+ if (!email || !(0, agent_config_js_1.isRegisteredAgent)(email))
871
+ return false;
872
+ return true;
873
+ });
874
+ if (eligibleTasks.length > 0) {
875
+ console.log(`\nšŸ“‹ Found ${eligibleTasks.length} eligible task(s)`);
876
+ for (const task of eligibleTasks) {
877
+ // Skip if already being processed
878
+ if (processingTasks.has(task.id)) {
879
+ console.log(` ā³ ${task.id.slice(0, 8)}... already in progress`);
880
+ continue;
881
+ }
882
+ let agentUserId = null;
883
+ try {
884
+ // Check comments for processing state
885
+ const commentsResult = await client.getComments(task.id);
886
+ const comments = commentsResult.success ? commentsResult.data || [] : [];
887
+ const status = shouldProcessTask(comments);
888
+ const assigneeEmail = getAssigneeEmail(task) || 'claude@astrid.cc';
889
+ console.log(`\nšŸ” Task: ${task.title.slice(0, 50)}...`);
890
+ console.log(` ID: ${task.id}`);
891
+ console.log(` Agent: ${assigneeEmail}`);
892
+ console.log(` Status: ${status.reason}`);
893
+ if (!status.shouldProcess) {
894
+ continue;
895
+ }
896
+ const service = (0, agent_config_js_1.getAgentService)(assigneeEmail);
897
+ const agentName = `${service.charAt(0).toUpperCase() + service.slice(1)} AI Agent (Terminal)`;
898
+ // Get agent user ID
899
+ agentUserId = await client.getAgentIdByEmail(assigneeEmail);
900
+ if (agentUserId) {
901
+ console.log(` Posting as: ${assigneeEmail} (${agentUserId})`);
902
+ }
903
+ // Handle "ship it" action
904
+ if (status.action === 'ship_it' && status.prUrl) {
905
+ console.log(`\nšŸš€ Ship It! Merging PR: ${status.prUrl}`);
906
+ processingTasks.add(task.id);
907
+ try {
908
+ const prMatch = status.prUrl.match(/\/pull\/(\d+)/);
909
+ if (!prMatch) {
910
+ throw new Error('Could not extract PR number from URL');
911
+ }
912
+ const prNumber = prMatch[1];
913
+ // Get repo from list
914
+ const listResult = await client.getList(listId);
915
+ const repoString = listResult.data?.githubRepositoryId || listResult.data?.repository;
916
+ if (!repoString) {
917
+ throw new Error('No repository configured for this list');
918
+ }
919
+ const { execSync } = await import('child_process');
920
+ execSync(`gh pr merge ${prNumber} --merge --repo ${repoString}`, { stdio: 'inherit' });
921
+ await client.addComment(task.id, `šŸŽ‰ **Shipped!**\n\n` +
922
+ `PR #${prNumber} has been merged to main.\n\n` +
923
+ `---\n*Merged by ${agentName}*`, agentUserId || undefined);
924
+ console.log(`āœ… PR #${prNumber} merged successfully!`);
925
+ }
926
+ catch (mergeError) {
927
+ console.error('āŒ Failed to merge PR:', mergeError);
928
+ await client.addComment(task.id, `āŒ **Merge Failed**\n\n` +
929
+ `Could not merge PR: ${mergeError instanceof Error ? mergeError.message : String(mergeError)}\n\n` +
930
+ `Please merge manually: ${status.prUrl}`, agentUserId || undefined);
931
+ }
932
+ finally {
933
+ processingTasks.delete(task.id);
934
+ }
935
+ continue;
936
+ }
937
+ // Mark as processing
938
+ processingTasks.add(task.id);
939
+ // Post starting comment
940
+ await client.addComment(task.id, `šŸ–„ļø **${agentName} Starting**\n\n` +
941
+ `**Task:** ${task.title}\n` +
942
+ `**Mode:** Terminal (local Claude Code CLI)\n` +
943
+ `**Model:** ${CONFIG.claudeModel}\n\n` +
944
+ `Working on this task locally...\n\n` +
945
+ `---\n*Using local Claude Code*`, agentUserId || undefined);
946
+ // Format comments for terminal executor
947
+ const formattedComments = comments.map(c => ({
948
+ authorName: c.author?.name || 'Unknown',
949
+ content: c.content,
950
+ createdAt: c.createdAt,
951
+ }));
952
+ // Check if this is a follow-up (has previous agent activity)
953
+ const hasAgentActivity = comments.some(c => isAIAgentComment(c));
954
+ const isFollowUp = hasAgentActivity && comments.length > 1;
955
+ // Process task using local Claude Code
956
+ const result = await processTaskTerminal({
957
+ id: task.id,
958
+ title: task.title,
959
+ description: task.description,
960
+ assigneeEmail,
961
+ }, CONFIG.defaultProjectPath, formattedComments, isFollowUp);
962
+ if (result.success) {
963
+ // Post success comment
964
+ let successMessage = `šŸŽ‰ **Implementation Complete!**\n\n`;
965
+ if (result.summary) {
966
+ successMessage += `${result.summary}\n\n`;
967
+ }
968
+ if (result.files && result.files.length > 0) {
969
+ successMessage += `**Files modified:**\n${result.files.map(f => `- \`${f}\``).join('\n')}\n\n`;
970
+ }
971
+ if (result.prUrl) {
972
+ successMessage += `šŸ”— **Pull Request:** [${result.prUrl}](${result.prUrl})\n\n`;
973
+ successMessage += `**What's next:**\n` +
974
+ `1. Review the changes in the PR\n` +
975
+ `2. Test the preview deployment\n` +
976
+ `3. Comment "ship it" to merge!\n\n`;
977
+ }
978
+ successMessage += `---\n*Generated by ${agentName}*`;
979
+ await client.addComment(task.id, successMessage, agentUserId || undefined);
980
+ // Unassign task so it goes back to user
981
+ await client.reassignTask(task.id, null).catch(err => {
982
+ console.log(` āš ļø Could not unassign task: ${err}`);
983
+ });
984
+ }
985
+ else {
986
+ // Post failure comment
987
+ await client.addComment(task.id, `āŒ **Processing Failed**\n\n` +
988
+ `Error: ${result.error}\n\n` +
989
+ `---\n` +
990
+ `šŸ’” **To try again:** Comment "retry" or "try again"\n` +
991
+ `šŸ’” **To ship existing PR:** Comment "ship it"`, agentUserId || undefined);
992
+ }
993
+ processingTasks.delete(task.id);
994
+ }
995
+ catch (error) {
996
+ console.error(`āŒ Failed to process task ${task.id}:`, error);
997
+ processingTasks.delete(task.id);
998
+ await client.addComment(task.id, `āŒ **Processing Failed**\n\n` +
999
+ `Error: ${error instanceof Error ? error.message : String(error)}\n\n` +
1000
+ `---\n` +
1001
+ `šŸ’” **To try again:** Comment "retry" or "try again"`, agentUserId || undefined).catch(() => { });
700
1002
  }
701
1003
  }
702
1004
  }
@@ -717,33 +1019,182 @@ function showHelp() {
717
1019
  @gracefultools/astrid-sdk - AI Agent Worker
718
1020
 
719
1021
  Usage:
720
- npx astrid-agent Start polling for tasks
1022
+ npx astrid-agent Start polling for tasks (API mode)
1023
+ npx astrid-agent --terminal Start polling using local Claude Code CLI
1024
+ npx astrid-agent serve Start webhook server (for always-on servers)
721
1025
  npx astrid-agent <taskId> Process a specific task
722
1026
  npx astrid-agent --help Show this help
723
1027
 
724
- Environment Variables:
725
- ASTRID_OAUTH_CLIENT_ID OAuth client ID
726
- ASTRID_OAUTH_CLIENT_SECRET OAuth client secret
727
- ASTRID_OAUTH_LIST_ID List ID to monitor
1028
+ Modes:
1029
+
1030
+ API MODE (Default)
1031
+ ------------------
1032
+ Best for: Cloud servers, CI/CD, when you don't have Claude Code CLI installed
1033
+
1034
+ Uses Claude Agent SDK API to process tasks remotely.
1035
+
1036
+ Environment:
1037
+ ASTRID_OAUTH_CLIENT_ID OAuth client ID
1038
+ ASTRID_OAUTH_CLIENT_SECRET OAuth client secret
1039
+ ASTRID_OAUTH_LIST_ID List ID to monitor
1040
+ ANTHROPIC_API_KEY For Claude tasks
1041
+
1042
+ Example:
1043
+ npx astrid-agent # Starts polling (API mode)
1044
+
1045
+ TERMINAL MODE (--terminal)
1046
+ --------------------------
1047
+ Best for: Local development, when you have Claude Code CLI installed
1048
+
1049
+ Uses your local Claude Code CLI (spawn) to process tasks.
1050
+ Enables remote control of your local Claude Code from Astrid.
1051
+
1052
+ Environment:
1053
+ ASTRID_TERMINAL_MODE=true Enable terminal mode (or use --terminal flag)
1054
+ CLAUDE_MODEL Model to use (default: opus)
1055
+ CLAUDE_MAX_TURNS Max turns per execution (default: 50)
1056
+ DEFAULT_PROJECT_PATH Project directory (default: current dir)
1057
+
1058
+ Example:
1059
+ npx astrid-agent --terminal
1060
+ # Or with environment variable:
1061
+ ASTRID_TERMINAL_MODE=true npx astrid-agent
1062
+ # With custom options:
1063
+ npx astrid-agent --terminal --model=sonnet --cwd=/path/to/project
1064
+
1065
+ WEBHOOK MODE (serve)
1066
+ --------------------
1067
+ Best for: Always-on servers with permanent IP (VPS, fly.io, etc.)
728
1068
 
729
- ANTHROPIC_API_KEY For Claude tasks
730
- OPENAI_API_KEY For OpenAI tasks
731
- GEMINI_API_KEY For Gemini tasks
1069
+ Astrid sends tasks directly to your server via webhook.
732
1070
 
733
- GITHUB_TOKEN For cloning repositories
734
- POLL_INTERVAL_MS Polling interval (default: 30000)
735
- MAX_BUDGET_USD Max budget per task (default: 10.0)
1071
+ Environment:
1072
+ ASTRID_WEBHOOK_SECRET Secret from Astrid settings
1073
+ ASTRID_CALLBACK_URL Callback URL (optional)
1074
+
1075
+ Example:
1076
+ npx astrid-agent serve --port=3001
1077
+
1078
+ Common Environment Variables:
1079
+ # AI Provider Keys
1080
+ ANTHROPIC_API_KEY For Claude tasks (required)
1081
+ OPENAI_API_KEY For OpenAI tasks (optional)
1082
+ GEMINI_API_KEY For Gemini tasks (optional)
1083
+
1084
+ # GitHub (for repository access)
1085
+ GITHUB_TOKEN For cloning private repositories
1086
+
1087
+ Quick Start (Terminal Mode):
1088
+ # 1. Install Claude Code CLI and astrid-sdk
1089
+ npm install -g @anthropic-ai/claude-code @gracefultools/astrid-sdk
1090
+
1091
+ # 2. Set up environment
1092
+ export ANTHROPIC_API_KEY=sk-ant-...
1093
+ export ASTRID_OAUTH_CLIENT_ID=...
1094
+ export ASTRID_OAUTH_CLIENT_SECRET=...
1095
+ export ASTRID_OAUTH_LIST_ID=...
1096
+
1097
+ # 3. Start in terminal mode
1098
+ cd /your/project
1099
+ npx astrid-agent --terminal
1100
+
1101
+ # Now create a task in Astrid and assign to claude@astrid.cc
1102
+ # Your local Claude Code will process it!
1103
+
1104
+ Quick Start (API Mode):
1105
+ # 1. Install globally
1106
+ npm install -g @gracefultools/astrid-sdk
1107
+
1108
+ # 2. Set up environment (same as above)
1109
+ # 3. Start polling
1110
+ npx astrid-agent
736
1111
  `);
737
1112
  }
1113
+ async function startWebhookServer(port) {
1114
+ // Check for required environment variables
1115
+ const webhookSecret = process.env.ASTRID_WEBHOOK_SECRET;
1116
+ if (!webhookSecret) {
1117
+ console.error(`āŒ ASTRID_WEBHOOK_SECRET not configured
1118
+
1119
+ To set up:
1120
+ 1. Go to Settings -> AI Agent Settings in Astrid
1121
+ 2. Configure your webhook URL: http://your-server:${port}/webhook
1122
+ 3. Copy the webhook secret to your .env file:
1123
+
1124
+ ASTRID_WEBHOOK_SECRET=<your-secret-here>
1125
+ `);
1126
+ process.exit(1);
1127
+ }
1128
+ // Check for at least one AI provider
1129
+ const hasProvider = CONFIG.anthropicApiKey || CONFIG.openaiApiKey || CONFIG.geminiApiKey;
1130
+ if (!hasProvider) {
1131
+ console.warn(`āš ļø No AI provider keys configured. Add at least one:
1132
+ ANTHROPIC_API_KEY=sk-ant-... (for Claude)
1133
+ OPENAI_API_KEY=sk-... (for OpenAI)
1134
+ GEMINI_API_KEY=AIza... (for Gemini)
1135
+ `);
1136
+ }
1137
+ // Import and start the server from the server module
1138
+ try {
1139
+ const { startServer } = await import('../server/index.js');
1140
+ await startServer({
1141
+ port,
1142
+ webhookSecret,
1143
+ callbackUrl: process.env.ASTRID_CALLBACK_URL,
1144
+ });
1145
+ }
1146
+ catch (error) {
1147
+ if (error.message?.includes('express')) {
1148
+ console.error(`āŒ Express not installed. Install it with:
1149
+ npm install express
1150
+ `);
1151
+ }
1152
+ else {
1153
+ console.error(`āŒ Failed to start server:`, error);
1154
+ }
1155
+ process.exit(1);
1156
+ }
1157
+ }
738
1158
  async function main() {
739
1159
  const args = process.argv.slice(2);
740
1160
  if (args.includes('--help') || args.includes('-h')) {
741
1161
  showHelp();
742
1162
  process.exit(0);
743
1163
  }
744
- if (args.length > 0 && !args[0].startsWith('-')) {
1164
+ // Parse --terminal flag
1165
+ if (args.includes('--terminal')) {
1166
+ CONFIG.terminalMode = true;
1167
+ }
1168
+ // Parse --model flag
1169
+ const modelArg = args.find(a => a.startsWith('--model='));
1170
+ if (modelArg) {
1171
+ CONFIG.claudeModel = modelArg.split('=')[1];
1172
+ }
1173
+ // Parse --cwd flag
1174
+ const cwdArg = args.find(a => a.startsWith('--cwd='));
1175
+ if (cwdArg) {
1176
+ CONFIG.defaultProjectPath = cwdArg.split('=')[1];
1177
+ }
1178
+ // Parse --max-turns flag
1179
+ const maxTurnsArg = args.find(a => a.startsWith('--max-turns='));
1180
+ if (maxTurnsArg) {
1181
+ CONFIG.claudeMaxTurns = parseInt(maxTurnsArg.split('=')[1], 10) || 50;
1182
+ }
1183
+ // Handle 'serve' command
1184
+ if (args[0] === 'serve') {
1185
+ let port = 3001;
1186
+ const portArg = args.find(a => a.startsWith('--port='));
1187
+ if (portArg) {
1188
+ port = parseInt(portArg.split('=')[1], 10) || 3001;
1189
+ }
1190
+ await startWebhookServer(port);
1191
+ return;
1192
+ }
1193
+ // Handle specific task ID (first non-flag argument)
1194
+ const taskIdArg = args.find(a => !a.startsWith('-') && a !== 'serve');
1195
+ if (taskIdArg) {
745
1196
  // Process a specific task
746
- const taskId = args[0];
1197
+ const taskId = taskIdArg;
747
1198
  console.log(`\nšŸŽÆ Processing task: ${taskId}`);
748
1199
  const client = new astrid_oauth_js_1.AstridOAuthClient();
749
1200
  const taskResult = await client.getTask(taskId);
@@ -751,18 +1202,68 @@ async function main() {
751
1202
  console.error(`āŒ Failed to get task: ${taskResult.error}`);
752
1203
  process.exit(1);
753
1204
  }
754
- // TODO: Clone repository based on task's list configuration
755
- // For now, use current directory
756
- const repoPath = process.cwd();
757
- await processTask({
758
- id: taskResult.data.id,
759
- title: taskResult.data.title,
760
- description: taskResult.data.description,
761
- assigneeEmail: taskResult.data.assigneeEmail,
762
- }, repoPath);
1205
+ const projectPath = CONFIG.defaultProjectPath;
1206
+ if (CONFIG.terminalMode) {
1207
+ console.log(`\nšŸ–„ļø Terminal mode enabled`);
1208
+ const result = await processTaskTerminal({
1209
+ id: taskResult.data.id,
1210
+ title: taskResult.data.title,
1211
+ description: taskResult.data.description,
1212
+ assigneeEmail: getAssigneeEmail(taskResult.data),
1213
+ }, projectPath);
1214
+ if (!result.success) {
1215
+ console.error(`āŒ Task failed: ${result.error}`);
1216
+ process.exit(1);
1217
+ }
1218
+ console.log(`\nāœ… Task completed successfully`);
1219
+ if (result.prUrl) {
1220
+ console.log(` PR: ${result.prUrl}`);
1221
+ }
1222
+ }
1223
+ else {
1224
+ await processTask({
1225
+ id: taskResult.data.id,
1226
+ title: taskResult.data.title,
1227
+ description: taskResult.data.description,
1228
+ assigneeEmail: getAssigneeEmail(taskResult.data),
1229
+ }, projectPath);
1230
+ }
1231
+ return;
1232
+ }
1233
+ // Default: Run polling worker
1234
+ if (CONFIG.terminalMode) {
1235
+ console.log(`
1236
+ šŸ–„ļø Starting terminal mode...
1237
+
1238
+ Terminal mode uses your local Claude Code CLI to process tasks.
1239
+ This enables remote control of your local Claude Code from Astrid.
1240
+
1241
+ Settings:
1242
+ - Model: ${CONFIG.claudeModel}
1243
+ - Max turns: ${CONFIG.claudeMaxTurns}
1244
+ - Project path: ${CONFIG.defaultProjectPath}
1245
+
1246
+ Polling for tasks every ${CONFIG.pollIntervalMs / 1000}s...
1247
+
1248
+ `);
1249
+ await runWorkerTerminal();
763
1250
  }
764
1251
  else {
765
- // Run worker loop
1252
+ console.log(`
1253
+ šŸ”„ Starting polling mode (API)...
1254
+
1255
+ Polling is ideal for:
1256
+ - Local devices behind NAT/firewalls
1257
+ - Laptops and home servers
1258
+ - Intermittent connectivity
1259
+
1260
+ For terminal mode (uses local Claude Code CLI):
1261
+ npx astrid-agent --terminal
1262
+
1263
+ For always-on servers with permanent IPs, consider:
1264
+ npx astrid-agent serve --port=3001
1265
+
1266
+ `);
766
1267
  await runWorker();
767
1268
  }
768
1269
  }