@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/README.md +177 -72
- package/dist/bin/cli.d.ts +2 -1
- package/dist/bin/cli.d.ts.map +1 -1
- package/dist/bin/cli.js +530 -29
- package/dist/bin/cli.js.map +1 -1
- package/dist/executors/terminal-claude.d.ts +109 -0
- package/dist/executors/terminal-claude.d.ts.map +1 -0
- package/dist/executors/terminal-claude.js +493 -0
- package/dist/executors/terminal-claude.js.map +1 -0
- package/dist/server/astrid-client.d.ts +77 -0
- package/dist/server/astrid-client.d.ts.map +1 -0
- package/dist/server/astrid-client.js +125 -0
- package/dist/server/astrid-client.js.map +1 -0
- package/dist/server/index.d.ts +38 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +408 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/repo-manager.d.ts +41 -0
- package/dist/server/repo-manager.d.ts.map +1 -0
- package/dist/server/repo-manager.js +177 -0
- package/dist/server/repo-manager.js.map +1 -0
- package/dist/server/session-manager.d.ts +93 -0
- package/dist/server/session-manager.d.ts.map +1 -0
- package/dist/server/session-manager.js +217 -0
- package/dist/server/session-manager.js.map +1 -0
- package/dist/server/webhook-signature.d.ts +23 -0
- package/dist/server/webhook-signature.d.ts.map +1 -0
- package/dist/server/webhook-signature.js +74 -0
- package/dist/server/webhook-signature.js.map +1 -0
- package/package.json +7 -2
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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"
|
|
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
|
-
`
|
|
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
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
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
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
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
|
}
|