@astroanywhere/agent 0.1.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 (203) hide show
  1. package/LICENSE +76 -0
  2. package/README.md +178 -0
  3. package/dist/cli.d.ts +15 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +401 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/index.d.ts +9 -0
  8. package/dist/commands/index.d.ts.map +1 -0
  9. package/dist/commands/index.js +9 -0
  10. package/dist/commands/index.js.map +1 -0
  11. package/dist/commands/mcp.d.ts +16 -0
  12. package/dist/commands/mcp.d.ts.map +1 -0
  13. package/dist/commands/mcp.js +19 -0
  14. package/dist/commands/mcp.js.map +1 -0
  15. package/dist/commands/setup.d.ts +20 -0
  16. package/dist/commands/setup.d.ts.map +1 -0
  17. package/dist/commands/setup.js +585 -0
  18. package/dist/commands/setup.js.map +1 -0
  19. package/dist/commands/start.d.ts +16 -0
  20. package/dist/commands/start.d.ts.map +1 -0
  21. package/dist/commands/start.js +638 -0
  22. package/dist/commands/start.js.map +1 -0
  23. package/dist/commands/status.d.ts +5 -0
  24. package/dist/commands/status.d.ts.map +1 -0
  25. package/dist/commands/status.js +63 -0
  26. package/dist/commands/status.js.map +1 -0
  27. package/dist/commands/stop.d.ts +5 -0
  28. package/dist/commands/stop.d.ts.map +1 -0
  29. package/dist/commands/stop.js +85 -0
  30. package/dist/commands/stop.js.map +1 -0
  31. package/dist/execution/direct-strategy.d.ts +18 -0
  32. package/dist/execution/direct-strategy.d.ts.map +1 -0
  33. package/dist/execution/direct-strategy.js +156 -0
  34. package/dist/execution/direct-strategy.js.map +1 -0
  35. package/dist/execution/docker-strategy.d.ts +26 -0
  36. package/dist/execution/docker-strategy.d.ts.map +1 -0
  37. package/dist/execution/docker-strategy.js +222 -0
  38. package/dist/execution/docker-strategy.js.map +1 -0
  39. package/dist/execution/index.d.ts +14 -0
  40. package/dist/execution/index.d.ts.map +1 -0
  41. package/dist/execution/index.js +13 -0
  42. package/dist/execution/index.js.map +1 -0
  43. package/dist/execution/kubernetes-exec-strategy.d.ts +23 -0
  44. package/dist/execution/kubernetes-exec-strategy.d.ts.map +1 -0
  45. package/dist/execution/kubernetes-exec-strategy.js +232 -0
  46. package/dist/execution/kubernetes-exec-strategy.js.map +1 -0
  47. package/dist/execution/registry.d.ts +41 -0
  48. package/dist/execution/registry.d.ts.map +1 -0
  49. package/dist/execution/registry.js +84 -0
  50. package/dist/execution/registry.js.map +1 -0
  51. package/dist/execution/slurm-strategy.d.ts +22 -0
  52. package/dist/execution/slurm-strategy.d.ts.map +1 -0
  53. package/dist/execution/slurm-strategy.js +219 -0
  54. package/dist/execution/slurm-strategy.js.map +1 -0
  55. package/dist/execution/types.d.ts +72 -0
  56. package/dist/execution/types.d.ts.map +1 -0
  57. package/dist/execution/types.js +10 -0
  58. package/dist/execution/types.js.map +1 -0
  59. package/dist/index.d.ts +22 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +22 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/lib/api-client.d.ts +35 -0
  64. package/dist/lib/api-client.d.ts.map +1 -0
  65. package/dist/lib/api-client.js +126 -0
  66. package/dist/lib/api-client.js.map +1 -0
  67. package/dist/lib/config.d.ts +174 -0
  68. package/dist/lib/config.d.ts.map +1 -0
  69. package/dist/lib/config.js +399 -0
  70. package/dist/lib/config.js.map +1 -0
  71. package/dist/lib/copy-worktree.d.ts +73 -0
  72. package/dist/lib/copy-worktree.d.ts.map +1 -0
  73. package/dist/lib/copy-worktree.js +374 -0
  74. package/dist/lib/copy-worktree.js.map +1 -0
  75. package/dist/lib/git-pr.d.ts +63 -0
  76. package/dist/lib/git-pr.d.ts.map +1 -0
  77. package/dist/lib/git-pr.js +224 -0
  78. package/dist/lib/git-pr.js.map +1 -0
  79. package/dist/lib/hardware-id.d.ts +25 -0
  80. package/dist/lib/hardware-id.d.ts.map +1 -0
  81. package/dist/lib/hardware-id.js +186 -0
  82. package/dist/lib/hardware-id.js.map +1 -0
  83. package/dist/lib/hpc-context.d.ts +35 -0
  84. package/dist/lib/hpc-context.d.ts.map +1 -0
  85. package/dist/lib/hpc-context.js +167 -0
  86. package/dist/lib/hpc-context.js.map +1 -0
  87. package/dist/lib/prompt-templates.d.ts +195 -0
  88. package/dist/lib/prompt-templates.d.ts.map +1 -0
  89. package/dist/lib/prompt-templates.js +353 -0
  90. package/dist/lib/prompt-templates.js.map +1 -0
  91. package/dist/lib/providers.d.ts +27 -0
  92. package/dist/lib/providers.d.ts.map +1 -0
  93. package/dist/lib/providers.js +372 -0
  94. package/dist/lib/providers.js.map +1 -0
  95. package/dist/lib/repo-context.d.ts +18 -0
  96. package/dist/lib/repo-context.d.ts.map +1 -0
  97. package/dist/lib/repo-context.js +61 -0
  98. package/dist/lib/repo-context.js.map +1 -0
  99. package/dist/lib/repo-utils.d.ts +35 -0
  100. package/dist/lib/repo-utils.d.ts.map +1 -0
  101. package/dist/lib/repo-utils.js +222 -0
  102. package/dist/lib/repo-utils.js.map +1 -0
  103. package/dist/lib/resources.d.ts +17 -0
  104. package/dist/lib/resources.d.ts.map +1 -0
  105. package/dist/lib/resources.js +227 -0
  106. package/dist/lib/resources.js.map +1 -0
  107. package/dist/lib/slurm-detect.d.ts +15 -0
  108. package/dist/lib/slurm-detect.d.ts.map +1 -0
  109. package/dist/lib/slurm-detect.js +148 -0
  110. package/dist/lib/slurm-detect.js.map +1 -0
  111. package/dist/lib/slurm-executor.d.ts +70 -0
  112. package/dist/lib/slurm-executor.d.ts.map +1 -0
  113. package/dist/lib/slurm-executor.js +402 -0
  114. package/dist/lib/slurm-executor.js.map +1 -0
  115. package/dist/lib/slurm-job-monitor.d.ts +52 -0
  116. package/dist/lib/slurm-job-monitor.d.ts.map +1 -0
  117. package/dist/lib/slurm-job-monitor.js +212 -0
  118. package/dist/lib/slurm-job-monitor.js.map +1 -0
  119. package/dist/lib/ssh-discovery.d.ts +17 -0
  120. package/dist/lib/ssh-discovery.d.ts.map +1 -0
  121. package/dist/lib/ssh-discovery.js +287 -0
  122. package/dist/lib/ssh-discovery.js.map +1 -0
  123. package/dist/lib/ssh-installer.d.ts +69 -0
  124. package/dist/lib/ssh-installer.d.ts.map +1 -0
  125. package/dist/lib/ssh-installer.js +230 -0
  126. package/dist/lib/ssh-installer.js.map +1 -0
  127. package/dist/lib/streaming-prompt.d.ts +48 -0
  128. package/dist/lib/streaming-prompt.d.ts.map +1 -0
  129. package/dist/lib/streaming-prompt.js +91 -0
  130. package/dist/lib/streaming-prompt.js.map +1 -0
  131. package/dist/lib/task-executor.d.ts +114 -0
  132. package/dist/lib/task-executor.d.ts.map +1 -0
  133. package/dist/lib/task-executor.js +753 -0
  134. package/dist/lib/task-executor.js.map +1 -0
  135. package/dist/lib/websocket-client.d.ts +200 -0
  136. package/dist/lib/websocket-client.d.ts.map +1 -0
  137. package/dist/lib/websocket-client.js +781 -0
  138. package/dist/lib/websocket-client.js.map +1 -0
  139. package/dist/lib/workdir-safety.d.ts +63 -0
  140. package/dist/lib/workdir-safety.d.ts.map +1 -0
  141. package/dist/lib/workdir-safety.js +247 -0
  142. package/dist/lib/workdir-safety.js.map +1 -0
  143. package/dist/lib/worktree-include.d.ts +14 -0
  144. package/dist/lib/worktree-include.d.ts.map +1 -0
  145. package/dist/lib/worktree-include.js +68 -0
  146. package/dist/lib/worktree-include.js.map +1 -0
  147. package/dist/lib/worktree-setup.d.ts +18 -0
  148. package/dist/lib/worktree-setup.d.ts.map +1 -0
  149. package/dist/lib/worktree-setup.js +60 -0
  150. package/dist/lib/worktree-setup.js.map +1 -0
  151. package/dist/lib/worktree.d.ts +37 -0
  152. package/dist/lib/worktree.d.ts.map +1 -0
  153. package/dist/lib/worktree.js +411 -0
  154. package/dist/lib/worktree.js.map +1 -0
  155. package/dist/mcp/index.d.ts +8 -0
  156. package/dist/mcp/index.d.ts.map +1 -0
  157. package/dist/mcp/index.js +8 -0
  158. package/dist/mcp/index.js.map +1 -0
  159. package/dist/mcp/server.d.ts +45 -0
  160. package/dist/mcp/server.d.ts.map +1 -0
  161. package/dist/mcp/server.js +153 -0
  162. package/dist/mcp/server.js.map +1 -0
  163. package/dist/mcp/session-bridge.d.ts +87 -0
  164. package/dist/mcp/session-bridge.d.ts.map +1 -0
  165. package/dist/mcp/session-bridge.js +317 -0
  166. package/dist/mcp/session-bridge.js.map +1 -0
  167. package/dist/mcp/tools.d.ts +70 -0
  168. package/dist/mcp/tools.d.ts.map +1 -0
  169. package/dist/mcp/tools.js +234 -0
  170. package/dist/mcp/tools.js.map +1 -0
  171. package/dist/mcp/types.d.ts +197 -0
  172. package/dist/mcp/types.d.ts.map +1 -0
  173. package/dist/mcp/types.js +16 -0
  174. package/dist/mcp/types.js.map +1 -0
  175. package/dist/providers/base-adapter.d.ts +56 -0
  176. package/dist/providers/base-adapter.d.ts.map +1 -0
  177. package/dist/providers/base-adapter.js +5 -0
  178. package/dist/providers/base-adapter.js.map +1 -0
  179. package/dist/providers/claude-code-adapter.d.ts +27 -0
  180. package/dist/providers/claude-code-adapter.d.ts.map +1 -0
  181. package/dist/providers/claude-code-adapter.js +298 -0
  182. package/dist/providers/claude-code-adapter.js.map +1 -0
  183. package/dist/providers/claude-sdk-adapter.d.ts +60 -0
  184. package/dist/providers/claude-sdk-adapter.d.ts.map +1 -0
  185. package/dist/providers/claude-sdk-adapter.js +632 -0
  186. package/dist/providers/claude-sdk-adapter.js.map +1 -0
  187. package/dist/providers/codex-adapter.d.ts +21 -0
  188. package/dist/providers/codex-adapter.d.ts.map +1 -0
  189. package/dist/providers/codex-adapter.js +197 -0
  190. package/dist/providers/codex-adapter.js.map +1 -0
  191. package/dist/providers/index.d.ts +26 -0
  192. package/dist/providers/index.d.ts.map +1 -0
  193. package/dist/providers/index.js +58 -0
  194. package/dist/providers/index.js.map +1 -0
  195. package/dist/providers/slurm-adapter.d.ts +26 -0
  196. package/dist/providers/slurm-adapter.d.ts.map +1 -0
  197. package/dist/providers/slurm-adapter.js +146 -0
  198. package/dist/providers/slurm-adapter.js.map +1 -0
  199. package/dist/types.d.ts +592 -0
  200. package/dist/types.d.ts.map +1 -0
  201. package/dist/types.js +5 -0
  202. package/dist/types.js.map +1 -0
  203. package/package.json +77 -0
@@ -0,0 +1,638 @@
1
+ /**
2
+ * Start command - starts the agent runner
3
+ */
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import { spawn, execFileSync } from 'node:child_process';
7
+ import { readFileSync, readdirSync, existsSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { dirname, join } from 'node:path';
10
+ import { homedir } from 'node:os';
11
+ import { config } from '../lib/config.js';
12
+ import { detectProviders } from '../lib/providers.js';
13
+ import { getMachineResources, formatResourceSummary } from '../lib/resources.js';
14
+ import { WebSocketClient } from '../lib/websocket-client.js';
15
+ import { TaskExecutor } from '../lib/task-executor.js';
16
+ import { localRepoSetup } from '../lib/repo-utils.js';
17
+ import { executionStrategyRegistry } from '../execution/index.js';
18
+ // Get package version from package.json
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = dirname(__filename);
21
+ async function getVersion() {
22
+ try {
23
+ const packageJson = await import(join(__dirname, '../../package.json'), {
24
+ with: { type: 'json' },
25
+ });
26
+ return packageJson.default.version ?? '0.1.0';
27
+ }
28
+ catch {
29
+ return '0.1.0';
30
+ }
31
+ }
32
+ /**
33
+ * Scan ~/.claude/skills/ and {workdir}/.claude/skills/ for SKILL.md files.
34
+ * Parse YAML frontmatter to extract name and description.
35
+ */
36
+ function scanSlashCommands(workingDirectory) {
37
+ const commands = [];
38
+ const seen = new Set();
39
+ const skillDirs = [
40
+ join(homedir(), '.claude', 'skills'),
41
+ ];
42
+ if (workingDirectory) {
43
+ skillDirs.push(join(workingDirectory, '.claude', 'skills'));
44
+ }
45
+ for (const skillsDir of skillDirs) {
46
+ if (!existsSync(skillsDir))
47
+ continue;
48
+ try {
49
+ const entries = readdirSync(skillsDir, { withFileTypes: true });
50
+ for (const entry of entries) {
51
+ if (!entry.isDirectory())
52
+ continue;
53
+ const skillFile = join(skillsDir, entry.name, 'SKILL.md');
54
+ if (!existsSync(skillFile))
55
+ continue;
56
+ try {
57
+ const content = readFileSync(skillFile, 'utf-8');
58
+ // Parse YAML frontmatter (between --- markers)
59
+ const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
60
+ if (!fmMatch)
61
+ continue;
62
+ const fm = fmMatch[1];
63
+ const nameMatch = fm.match(/^name:\s*(.+)$/m);
64
+ const descMatch = fm.match(/^description:\s*(.+)$/m);
65
+ const name = nameMatch?.[1]?.trim().replace(/^["']|["']$/g, '') || entry.name;
66
+ const description = descMatch?.[1]?.trim().replace(/^["']|["']$/g, '') || '';
67
+ const cmdName = `/${name}`;
68
+ if (!seen.has(cmdName)) {
69
+ seen.add(cmdName);
70
+ commands.push({ name: cmdName, description });
71
+ }
72
+ }
73
+ catch {
74
+ // Skip unreadable skill files
75
+ }
76
+ }
77
+ }
78
+ catch {
79
+ // Skip unreadable skill directories
80
+ }
81
+ }
82
+ return commands;
83
+ }
84
+ export async function startCommand(options = {}) {
85
+ // Auto-run setup if not completed
86
+ if (!config.isSetupComplete()) {
87
+ console.log(chalk.yellow('Setup has not been completed. Running setup automatically...\n'));
88
+ const { setupCommand } = await import('./setup.js');
89
+ await setupCommand({
90
+ relay: options.relay,
91
+ skipAuth: true,
92
+ nonInteractive: true,
93
+ withSshConfig: true,
94
+ });
95
+ if (!config.isSetupComplete()) {
96
+ console.log(chalk.red('Setup failed. Run manually: npx @astro/agent setup'));
97
+ process.exit(1);
98
+ }
99
+ }
100
+ // Initialize hardware-based machine ID if not set
101
+ await config.initializeMachineId();
102
+ // Get configuration
103
+ const runnerId = config.getRunnerId();
104
+ const machineId = config.getMachineId();
105
+ const relayUrl = options.relay ?? config.getRelayUrl();
106
+ const maxTasks = options.maxTasks ?? 4;
107
+ const logLevel = options.logLevel ?? config.getLogLevel();
108
+ // Background mode: spawn detached process
109
+ if (!options.foreground) {
110
+ console.log(chalk.bold('Starting Astro Agent Runner in background...\n'));
111
+ const scriptPath = process.argv[1];
112
+ const args = ['start', '--foreground'];
113
+ if (options.relay)
114
+ args.push('--relay', options.relay);
115
+ if (options.maxTasks)
116
+ args.push('--max-tasks', String(options.maxTasks));
117
+ if (options.logLevel)
118
+ args.push('--log-level', options.logLevel);
119
+ if (options.preserveWorktrees)
120
+ args.push('--preserve-worktrees');
121
+ const child = spawn(process.execPath, [scriptPath, ...args], {
122
+ detached: true,
123
+ stdio: 'ignore',
124
+ });
125
+ child.unref();
126
+ // Write PID file for stop command
127
+ const pidDir = join(homedir(), '.astro');
128
+ mkdirSync(pidDir, { recursive: true });
129
+ const pidFile = join(pidDir, 'agent-runner.pid');
130
+ if (child.pid) {
131
+ writeFileSync(pidFile, String(child.pid));
132
+ }
133
+ console.log(chalk.green('✓ Agent runner started in background'));
134
+ console.log(chalk.dim(` PID: ${child.pid}`));
135
+ console.log(chalk.dim(` Runner ID: ${runnerId}`));
136
+ console.log();
137
+ console.log('To view logs:');
138
+ console.log(chalk.cyan(' npx @astro/agent logs'));
139
+ console.log();
140
+ console.log('To stop:');
141
+ console.log(chalk.cyan(' npx @astro/agent stop'));
142
+ return;
143
+ }
144
+ // Foreground mode: run directly
145
+ console.log(chalk.bold('\n🤖 Astro Agent Runner\n'));
146
+ const version = await getVersion();
147
+ console.log(chalk.dim(`Version: ${version}`));
148
+ console.log(chalk.dim(`Runner ID: ${runnerId}`));
149
+ console.log(chalk.dim(`Machine ID: ${machineId}`));
150
+ console.log(chalk.dim(`Relay: ${relayUrl}`));
151
+ console.log(chalk.dim(`Max concurrent tasks: ${maxTasks}`));
152
+ console.log(chalk.dim(`Log level: ${logLevel}`));
153
+ console.log();
154
+ // Set Claude OAuth token if configured (from `claude setup-token`)
155
+ const claudeOauthToken = config.getClaudeOauthToken();
156
+ if (claudeOauthToken && !process.env.CLAUDE_CODE_OAUTH_TOKEN) {
157
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = claudeOauthToken;
158
+ console.log(chalk.dim('Using stored Claude OAuth token'));
159
+ }
160
+ console.log();
161
+ // Detect resources
162
+ const resourceSpinner = ora('Detecting machine resources...').start();
163
+ let resources;
164
+ try {
165
+ resources = await getMachineResources();
166
+ resourceSpinner.succeed('Machine resources detected');
167
+ if (logLevel === 'debug') {
168
+ console.log(chalk.dim(formatResourceSummary(resources)));
169
+ }
170
+ }
171
+ catch (error) {
172
+ resourceSpinner.fail('Failed to detect resources');
173
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
174
+ }
175
+ // Detect providers
176
+ const providerSpinner = ora('Detecting agent providers...').start();
177
+ let providers = [];
178
+ try {
179
+ providers = await detectProviders();
180
+ if (providers.length > 0) {
181
+ providerSpinner.succeed(`Found ${providers.length} provider(s): ${providers.map((p) => p.name).join(', ')}`);
182
+ }
183
+ else {
184
+ providerSpinner.warn('No providers available');
185
+ }
186
+ }
187
+ catch {
188
+ providerSpinner.fail('Failed to detect providers');
189
+ providers = [];
190
+ }
191
+ // Detect execution strategies
192
+ const strategySpinner = ora('Detecting execution strategies...').start();
193
+ let executionStrategies = [];
194
+ try {
195
+ executionStrategies = await executionStrategyRegistry.detectAll();
196
+ const available = executionStrategies.filter((s) => s.available);
197
+ if (available.length > 0) {
198
+ strategySpinner.succeed(`Found ${available.length} execution strategy(s): ${available.map((s) => s.name).join(', ')}`);
199
+ }
200
+ else {
201
+ strategySpinner.warn('No execution strategies detected (direct always available)');
202
+ }
203
+ }
204
+ catch {
205
+ strategySpinner.fail('Failed to detect execution strategies');
206
+ executionStrategies = [];
207
+ }
208
+ console.log();
209
+ // Create WebSocket client
210
+ const connectSpinner = ora('Connecting to relay server...').start();
211
+ // Create task executor first (needed for callback closures)
212
+ // wsClient will be assigned after construction
213
+ let taskExecutor;
214
+ const wsClient = new WebSocketClient({
215
+ runnerId,
216
+ machineId,
217
+ providers,
218
+ executionStrategies: executionStrategies.filter((s) => s.available),
219
+ version,
220
+ wsToken: config.getWsToken(),
221
+ config: {
222
+ relayUrl,
223
+ maxConcurrentTasks: maxTasks,
224
+ logLevel,
225
+ },
226
+ onEvent: (event) => handleEvent(event, logLevel),
227
+ onTaskDispatch: (task) => {
228
+ taskExecutor.submitTask(task).catch((error) => {
229
+ log('error', `Failed to submit task ${task.id}: ${error.message}`, logLevel);
230
+ });
231
+ },
232
+ onTaskCancel: (taskId) => {
233
+ taskExecutor.cancelTask(taskId);
234
+ },
235
+ onTaskSafetyDecision: (taskId, decision) => {
236
+ log('info', `Safety decision for task ${taskId}: ${decision}`, logLevel);
237
+ taskExecutor.handleSafetyDecision(taskId, decision).catch((error) => {
238
+ log('error', `Failed to handle safety decision for task ${taskId}: ${error.message}`, logLevel);
239
+ });
240
+ },
241
+ onTaskSteer: (taskId, message, action, interrupt) => {
242
+ log('info', `Received steer for task ${taskId}: "${message.slice(0, 100)}"${action ? ` (action: ${action})` : ''}${interrupt ? ' (interrupt)' : ''}`, logLevel);
243
+ taskExecutor.steerTask(taskId, message, interrupt ?? false).then((result) => {
244
+ wsClient.sendSteerAck(taskId, result.accepted, result.reason, interrupt);
245
+ log('info', `Steer ack for task ${taskId}: accepted=${result.accepted}${result.reason ? ` reason=${result.reason}` : ''}${interrupt ? ' (interrupt)' : ''}`, logLevel);
246
+ }).catch((err) => {
247
+ log('error', `Steer failed for task ${taskId}: ${err instanceof Error ? err.message : String(err)}`, logLevel);
248
+ wsClient.sendSteerAck(taskId, false, 'Internal error');
249
+ });
250
+ },
251
+ onFileList: (path, correlationId) => {
252
+ log('debug', `File list request for path: ${path || '(cwd)'}`, logLevel);
253
+ try {
254
+ const cwd = path || process.cwd();
255
+ if (!existsSync(cwd)) {
256
+ log('debug', `Directory does not exist: ${cwd}`, logLevel);
257
+ wsClient.sendFileListResponse(correlationId, []);
258
+ return;
259
+ }
260
+ // Check if directory is a git repo before trying git ls-files
261
+ try {
262
+ execFileSync('git', ['rev-parse', '--is-inside-work-tree'], {
263
+ cwd,
264
+ encoding: 'utf-8',
265
+ timeout: 5000,
266
+ stdio: 'pipe',
267
+ });
268
+ }
269
+ catch {
270
+ log('debug', `Not a git repo, returning empty file list: ${cwd}`, logLevel);
271
+ wsClient.sendFileListResponse(correlationId, []);
272
+ return;
273
+ }
274
+ const output = execFileSync('git', ['ls-files'], {
275
+ cwd,
276
+ encoding: 'utf-8',
277
+ timeout: 5000,
278
+ maxBuffer: 1024 * 1024,
279
+ });
280
+ const files = output.trim().split('\n').filter(Boolean);
281
+ wsClient.sendFileListResponse(correlationId, files);
282
+ log('debug', `Sent ${files.length} files for path: ${cwd}`, logLevel);
283
+ }
284
+ catch (error) {
285
+ log('warn', `Failed to list files in ${path}: ${error instanceof Error ? error.message : String(error)}`, logLevel);
286
+ wsClient.sendFileListResponse(correlationId, []);
287
+ }
288
+ },
289
+ onSlashCommands: (correlationId, workingDirectory) => {
290
+ log('debug', `Slash commands request for dir: ${workingDirectory || '(global)'}`, logLevel);
291
+ try {
292
+ const commands = scanSlashCommands(workingDirectory);
293
+ wsClient.sendSlashCommandsResponse(correlationId, commands);
294
+ log('debug', `Sent ${commands.length} slash commands`, logLevel);
295
+ }
296
+ catch (error) {
297
+ log('warn', `Failed to scan slash commands: ${error instanceof Error ? error.message : String(error)}`, logLevel);
298
+ wsClient.sendSlashCommandsResponse(correlationId, []);
299
+ }
300
+ },
301
+ onRepoSetup: (payload) => {
302
+ const { correlationId, projectId, workingDirectory, repository } = payload;
303
+ log('info', `Repo setup request: dir=${workingDirectory || '(none)'} repo=${repository || '(none)'}`, logLevel);
304
+ try {
305
+ const result = localRepoSetup({ workingDirectory, repository, projectId });
306
+ wsClient.sendRepoSetupResponse(correlationId, result);
307
+ log('info', `Repo setup result: success=${result.success} files=${result.fileTree?.length ?? 0}`, logLevel);
308
+ }
309
+ catch (error) {
310
+ log('error', `Repo setup failed: ${error instanceof Error ? error.message : String(error)}`, logLevel);
311
+ wsClient.sendRepoSetupResponse(correlationId, {
312
+ success: false,
313
+ error: error instanceof Error ? error.message : String(error),
314
+ });
315
+ }
316
+ },
317
+ onRepoDetect: (payload) => {
318
+ const { correlationId, path: dirPath } = payload;
319
+ log('info', `Repo detect request: path=${dirPath}`, logLevel);
320
+ try {
321
+ if (!existsSync(dirPath)) {
322
+ wsClient.sendRepoDetectResponse(correlationId, {
323
+ exists: false,
324
+ isGit: false,
325
+ remoteType: 'none',
326
+ suggestedDeliveryMode: 'branch',
327
+ });
328
+ return;
329
+ }
330
+ // Check if it's a git repo
331
+ let isGit = false;
332
+ try {
333
+ execFileSync('git', ['-C', dirPath, 'rev-parse', '--is-inside-work-tree'], {
334
+ encoding: 'utf-8',
335
+ timeout: 5_000,
336
+ stdio: 'pipe',
337
+ });
338
+ isGit = true;
339
+ }
340
+ catch {
341
+ // Not a git repo
342
+ }
343
+ if (!isGit) {
344
+ // Compute directory size for non-git directories
345
+ let dirSizeMB = null;
346
+ try {
347
+ const duOutput = execFileSync('du', ['-sk', dirPath], {
348
+ encoding: 'utf-8',
349
+ timeout: 10_000,
350
+ stdio: 'pipe',
351
+ });
352
+ const kb = parseInt(duOutput.trim().split(/\s+/)[0], 10);
353
+ if (!isNaN(kb)) {
354
+ dirSizeMB = Math.round((kb / 1024) * 100) / 100;
355
+ }
356
+ }
357
+ catch {
358
+ // Non-fatal
359
+ }
360
+ wsClient.sendRepoDetectResponse(correlationId, {
361
+ exists: true,
362
+ isGit: false,
363
+ remoteType: 'none',
364
+ suggestedDeliveryMode: 'direct',
365
+ dirSizeMB,
366
+ });
367
+ return;
368
+ }
369
+ // Get remote URL
370
+ let remoteUrl;
371
+ try {
372
+ const url = execFileSync('git', ['-C', dirPath, 'remote', 'get-url', 'origin'], {
373
+ encoding: 'utf-8',
374
+ timeout: 5_000,
375
+ stdio: 'pipe',
376
+ }).trim();
377
+ if (url)
378
+ remoteUrl = url;
379
+ }
380
+ catch {
381
+ // No remote
382
+ }
383
+ // Get default branch
384
+ let baseBranch = 'main';
385
+ try {
386
+ const branch = execFileSync('git', ['-C', dirPath, 'symbolic-ref', '--short', 'HEAD'], {
387
+ encoding: 'utf-8',
388
+ timeout: 5_000,
389
+ stdio: 'pipe',
390
+ }).trim();
391
+ if (branch)
392
+ baseBranch = branch;
393
+ }
394
+ catch {
395
+ // Default to 'main'
396
+ }
397
+ // Detect remote type
398
+ const detectRemoteType = (url) => {
399
+ if (!url)
400
+ return 'none';
401
+ const lower = url.toLowerCase();
402
+ if (lower.includes('github.com'))
403
+ return 'github';
404
+ if (lower.includes('gitlab.com') || lower.includes('gitlab.'))
405
+ return 'gitlab';
406
+ if (lower.includes('bitbucket.org') || lower.includes('bitbucket.'))
407
+ return 'bitbucket';
408
+ if (lower.startsWith('git@') || lower.startsWith('http') || lower.startsWith('ssh://'))
409
+ return 'generic';
410
+ return 'none';
411
+ };
412
+ const remoteType = detectRemoteType(remoteUrl);
413
+ let suggestedDeliveryMode = 'branch';
414
+ switch (remoteType) {
415
+ case 'github':
416
+ case 'gitlab':
417
+ case 'bitbucket':
418
+ suggestedDeliveryMode = 'pr';
419
+ break;
420
+ case 'generic':
421
+ suggestedDeliveryMode = 'push';
422
+ break;
423
+ case 'none':
424
+ default:
425
+ suggestedDeliveryMode = 'branch';
426
+ break;
427
+ }
428
+ wsClient.sendRepoDetectResponse(correlationId, {
429
+ exists: true,
430
+ isGit: true,
431
+ remoteUrl,
432
+ remoteType,
433
+ baseBranch,
434
+ suggestedDeliveryMode,
435
+ });
436
+ log('info', `Repo detect result: isGit=true remote=${remoteType} branch=${baseBranch}`, logLevel);
437
+ }
438
+ catch (error) {
439
+ log('error', `Repo detect failed: ${error instanceof Error ? error.message : String(error)}`, logLevel);
440
+ wsClient.sendRepoDetectResponse(correlationId, {
441
+ exists: false,
442
+ isGit: false,
443
+ remoteType: 'none',
444
+ suggestedDeliveryMode: 'branch',
445
+ error: error instanceof Error ? error.message : String(error),
446
+ });
447
+ }
448
+ },
449
+ onGitInit: (payload) => {
450
+ const { correlationId, workingDirectory, projectName } = payload;
451
+ log('info', `Git init request: dir=${workingDirectory}`, logLevel);
452
+ try {
453
+ // Initialize git, create .gitignore, initial commit
454
+ execFileSync('git', ['init'], { cwd: workingDirectory, stdio: 'pipe', timeout: 10_000 });
455
+ execFileSync('git', ['config', 'user.name', 'Astro Agent'], { cwd: workingDirectory, stdio: 'pipe', timeout: 5_000 });
456
+ execFileSync('git', ['config', 'user.email', 'agent@astro.local'], { cwd: workingDirectory, stdio: 'pipe', timeout: 5_000 });
457
+ // Generate basic .gitignore if missing
458
+ const gitignorePath = join(workingDirectory, '.gitignore');
459
+ if (!existsSync(gitignorePath)) {
460
+ writeFileSync(gitignorePath, 'node_modules/\n.env\n.DS_Store\n*.log\n');
461
+ }
462
+ // Initial commit
463
+ try {
464
+ execFileSync('git', ['add', '-A'], { cwd: workingDirectory, stdio: 'pipe', timeout: 10_000 });
465
+ execFileSync('git', ['commit', '-m', `Initial commit for ${projectName}`, '--allow-empty'], {
466
+ cwd: workingDirectory,
467
+ stdio: 'pipe',
468
+ timeout: 10_000,
469
+ });
470
+ }
471
+ catch {
472
+ // Non-fatal: might be empty directory
473
+ }
474
+ // Get file tree after init
475
+ let fileTree = [];
476
+ try {
477
+ const output = execFileSync('git', ['ls-files'], {
478
+ cwd: workingDirectory,
479
+ encoding: 'utf-8',
480
+ timeout: 10_000,
481
+ maxBuffer: 5 * 1024 * 1024,
482
+ });
483
+ fileTree = output.trim().split('\n').filter(Boolean);
484
+ }
485
+ catch {
486
+ // Non-fatal
487
+ }
488
+ wsClient.sendGitInitResponse(correlationId, {
489
+ success: true,
490
+ workingDirectory,
491
+ fileTree,
492
+ });
493
+ log('info', `Git init result: success=true files=${fileTree.length}`, logLevel);
494
+ }
495
+ catch (error) {
496
+ log('error', `Git init failed: ${error instanceof Error ? error.message : String(error)}`, logLevel);
497
+ wsClient.sendGitInitResponse(correlationId, {
498
+ success: false,
499
+ error: error instanceof Error ? error.message : String(error),
500
+ });
501
+ }
502
+ },
503
+ });
504
+ // Create task executor
505
+ taskExecutor = new TaskExecutor({
506
+ wsClient,
507
+ maxConcurrentTasks: maxTasks,
508
+ preserveWorktrees: options.preserveWorktrees,
509
+ allowNonGit: options.allowNonGit,
510
+ useSandbox: options.useSandbox,
511
+ maxSandboxSize: options.maxSandboxSize,
512
+ });
513
+ // Handle graceful shutdown
514
+ let isShuttingDown = false;
515
+ const shutdown = (signal) => {
516
+ if (isShuttingDown) {
517
+ // Second signal: force exit immediately
518
+ console.log('\nForce exit.');
519
+ process.exit(1);
520
+ }
521
+ isShuttingDown = true;
522
+ console.log();
523
+ log('info', `Received ${signal}, shutting down...`, logLevel);
524
+ // Force exit after 3 seconds if graceful shutdown hangs
525
+ setTimeout(() => {
526
+ log('warn', 'Graceful shutdown timed out, forcing exit', logLevel);
527
+ process.exit(1);
528
+ }, 3000).unref();
529
+ try {
530
+ // Cancel all running tasks
531
+ const counts = taskExecutor.getTaskCounts();
532
+ if (counts.running > 0 || counts.queued > 0) {
533
+ log('info', `Cancelling ${counts.running} running and ${counts.queued} queued tasks`, logLevel);
534
+ taskExecutor.cancelAll();
535
+ }
536
+ // Disconnect WebSocket
537
+ wsClient.disconnect();
538
+ // Remove PID file
539
+ const pidFile = join(homedir(), '.astro', 'agent-runner.pid');
540
+ try {
541
+ unlinkSync(pidFile);
542
+ }
543
+ catch { /* ignore */ }
544
+ }
545
+ catch {
546
+ // Ignore errors during shutdown
547
+ }
548
+ log('info', 'Shutdown complete', logLevel);
549
+ process.exit(0);
550
+ };
551
+ process.on('SIGINT', () => shutdown('SIGINT'));
552
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
553
+ // Connect to relay
554
+ try {
555
+ await wsClient.connect();
556
+ connectSpinner.succeed('Connected to relay server');
557
+ config.updateLastConnected();
558
+ }
559
+ catch (error) {
560
+ connectSpinner.fail('Failed to connect to relay server');
561
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
562
+ console.log();
563
+ console.log(chalk.yellow('The agent will continue trying to reconnect...'));
564
+ console.log(chalk.dim('Press Ctrl+C to stop'));
565
+ console.log();
566
+ // Still try to reconnect
567
+ wsClient.connect().catch(() => {
568
+ // Reconnection handled internally
569
+ });
570
+ }
571
+ console.log();
572
+ console.log(chalk.green('Agent runner is active'));
573
+ console.log(chalk.dim('Waiting for tasks...'));
574
+ console.log(chalk.dim('Press Ctrl+C to stop'));
575
+ console.log();
576
+ // Keep process alive
577
+ await new Promise(() => {
578
+ // Never resolves - keeps the process running
579
+ });
580
+ }
581
+ function handleEvent(event, logLevel) {
582
+ switch (event.type) {
583
+ case 'connected':
584
+ log('info', 'Connected to relay server', logLevel);
585
+ break;
586
+ case 'disconnected':
587
+ log('warn', `Disconnected from relay: ${event.reason}`, logLevel);
588
+ break;
589
+ case 'reconnecting':
590
+ log('info', `Reconnecting (attempt ${event.attempt})...`, logLevel);
591
+ break;
592
+ case 'task_received':
593
+ log('info', `Received task: ${event.task.id}`, logLevel);
594
+ log('debug', ` Provider: ${event.task.provider}`, logLevel);
595
+ log('debug', ` Prompt: ${event.task.prompt.slice(0, 100)}...`, logLevel);
596
+ break;
597
+ case 'task_started':
598
+ log('info', `Started task: ${event.taskId}`, logLevel);
599
+ break;
600
+ case 'task_completed':
601
+ log('info', `Completed task: ${event.result.taskId} (${event.result.status})`, logLevel);
602
+ if (event.result.error) {
603
+ log('info', ` Error: ${event.result.error}`, logLevel);
604
+ }
605
+ break;
606
+ case 'task_cancelled':
607
+ log('info', `Cancelled task: ${event.taskId}`, logLevel);
608
+ break;
609
+ case 'error':
610
+ log('error', `Error: ${event.error.message}`, logLevel);
611
+ break;
612
+ }
613
+ }
614
+ function log(level, message, configLevel) {
615
+ const levels = ['debug', 'info', 'warn', 'error'];
616
+ const levelIndex = levels.indexOf(level);
617
+ const configLevelIndex = levels.indexOf(configLevel);
618
+ if (levelIndex < configLevelIndex) {
619
+ return;
620
+ }
621
+ const timestamp = new Date().toISOString();
622
+ const prefix = `[${timestamp}]`;
623
+ switch (level) {
624
+ case 'debug':
625
+ console.log(chalk.dim(`${prefix} ${message}`));
626
+ break;
627
+ case 'info':
628
+ console.log(`${prefix} ${message}`);
629
+ break;
630
+ case 'warn':
631
+ console.log(chalk.yellow(`${prefix} ${message}`));
632
+ break;
633
+ case 'error':
634
+ console.error(chalk.red(`${prefix} ${message}`));
635
+ break;
636
+ }
637
+ }
638
+ //# sourceMappingURL=start.js.map