@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.
- package/LICENSE +76 -0
- package/README.md +178 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +401 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/index.d.ts +9 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +9 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/mcp.d.ts +16 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +19 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/setup.d.ts +20 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +585 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/start.d.ts +16 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +638 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +63 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/stop.d.ts +5 -0
- package/dist/commands/stop.d.ts.map +1 -0
- package/dist/commands/stop.js +85 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/execution/direct-strategy.d.ts +18 -0
- package/dist/execution/direct-strategy.d.ts.map +1 -0
- package/dist/execution/direct-strategy.js +156 -0
- package/dist/execution/direct-strategy.js.map +1 -0
- package/dist/execution/docker-strategy.d.ts +26 -0
- package/dist/execution/docker-strategy.d.ts.map +1 -0
- package/dist/execution/docker-strategy.js +222 -0
- package/dist/execution/docker-strategy.js.map +1 -0
- package/dist/execution/index.d.ts +14 -0
- package/dist/execution/index.d.ts.map +1 -0
- package/dist/execution/index.js +13 -0
- package/dist/execution/index.js.map +1 -0
- package/dist/execution/kubernetes-exec-strategy.d.ts +23 -0
- package/dist/execution/kubernetes-exec-strategy.d.ts.map +1 -0
- package/dist/execution/kubernetes-exec-strategy.js +232 -0
- package/dist/execution/kubernetes-exec-strategy.js.map +1 -0
- package/dist/execution/registry.d.ts +41 -0
- package/dist/execution/registry.d.ts.map +1 -0
- package/dist/execution/registry.js +84 -0
- package/dist/execution/registry.js.map +1 -0
- package/dist/execution/slurm-strategy.d.ts +22 -0
- package/dist/execution/slurm-strategy.d.ts.map +1 -0
- package/dist/execution/slurm-strategy.js +219 -0
- package/dist/execution/slurm-strategy.js.map +1 -0
- package/dist/execution/types.d.ts +72 -0
- package/dist/execution/types.d.ts.map +1 -0
- package/dist/execution/types.js +10 -0
- package/dist/execution/types.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api-client.d.ts +35 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +126 -0
- package/dist/lib/api-client.js.map +1 -0
- package/dist/lib/config.d.ts +174 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +399 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/copy-worktree.d.ts +73 -0
- package/dist/lib/copy-worktree.d.ts.map +1 -0
- package/dist/lib/copy-worktree.js +374 -0
- package/dist/lib/copy-worktree.js.map +1 -0
- package/dist/lib/git-pr.d.ts +63 -0
- package/dist/lib/git-pr.d.ts.map +1 -0
- package/dist/lib/git-pr.js +224 -0
- package/dist/lib/git-pr.js.map +1 -0
- package/dist/lib/hardware-id.d.ts +25 -0
- package/dist/lib/hardware-id.d.ts.map +1 -0
- package/dist/lib/hardware-id.js +186 -0
- package/dist/lib/hardware-id.js.map +1 -0
- package/dist/lib/hpc-context.d.ts +35 -0
- package/dist/lib/hpc-context.d.ts.map +1 -0
- package/dist/lib/hpc-context.js +167 -0
- package/dist/lib/hpc-context.js.map +1 -0
- package/dist/lib/prompt-templates.d.ts +195 -0
- package/dist/lib/prompt-templates.d.ts.map +1 -0
- package/dist/lib/prompt-templates.js +353 -0
- package/dist/lib/prompt-templates.js.map +1 -0
- package/dist/lib/providers.d.ts +27 -0
- package/dist/lib/providers.d.ts.map +1 -0
- package/dist/lib/providers.js +372 -0
- package/dist/lib/providers.js.map +1 -0
- package/dist/lib/repo-context.d.ts +18 -0
- package/dist/lib/repo-context.d.ts.map +1 -0
- package/dist/lib/repo-context.js +61 -0
- package/dist/lib/repo-context.js.map +1 -0
- package/dist/lib/repo-utils.d.ts +35 -0
- package/dist/lib/repo-utils.d.ts.map +1 -0
- package/dist/lib/repo-utils.js +222 -0
- package/dist/lib/repo-utils.js.map +1 -0
- package/dist/lib/resources.d.ts +17 -0
- package/dist/lib/resources.d.ts.map +1 -0
- package/dist/lib/resources.js +227 -0
- package/dist/lib/resources.js.map +1 -0
- package/dist/lib/slurm-detect.d.ts +15 -0
- package/dist/lib/slurm-detect.d.ts.map +1 -0
- package/dist/lib/slurm-detect.js +148 -0
- package/dist/lib/slurm-detect.js.map +1 -0
- package/dist/lib/slurm-executor.d.ts +70 -0
- package/dist/lib/slurm-executor.d.ts.map +1 -0
- package/dist/lib/slurm-executor.js +402 -0
- package/dist/lib/slurm-executor.js.map +1 -0
- package/dist/lib/slurm-job-monitor.d.ts +52 -0
- package/dist/lib/slurm-job-monitor.d.ts.map +1 -0
- package/dist/lib/slurm-job-monitor.js +212 -0
- package/dist/lib/slurm-job-monitor.js.map +1 -0
- package/dist/lib/ssh-discovery.d.ts +17 -0
- package/dist/lib/ssh-discovery.d.ts.map +1 -0
- package/dist/lib/ssh-discovery.js +287 -0
- package/dist/lib/ssh-discovery.js.map +1 -0
- package/dist/lib/ssh-installer.d.ts +69 -0
- package/dist/lib/ssh-installer.d.ts.map +1 -0
- package/dist/lib/ssh-installer.js +230 -0
- package/dist/lib/ssh-installer.js.map +1 -0
- package/dist/lib/streaming-prompt.d.ts +48 -0
- package/dist/lib/streaming-prompt.d.ts.map +1 -0
- package/dist/lib/streaming-prompt.js +91 -0
- package/dist/lib/streaming-prompt.js.map +1 -0
- package/dist/lib/task-executor.d.ts +114 -0
- package/dist/lib/task-executor.d.ts.map +1 -0
- package/dist/lib/task-executor.js +753 -0
- package/dist/lib/task-executor.js.map +1 -0
- package/dist/lib/websocket-client.d.ts +200 -0
- package/dist/lib/websocket-client.d.ts.map +1 -0
- package/dist/lib/websocket-client.js +781 -0
- package/dist/lib/websocket-client.js.map +1 -0
- package/dist/lib/workdir-safety.d.ts +63 -0
- package/dist/lib/workdir-safety.d.ts.map +1 -0
- package/dist/lib/workdir-safety.js +247 -0
- package/dist/lib/workdir-safety.js.map +1 -0
- package/dist/lib/worktree-include.d.ts +14 -0
- package/dist/lib/worktree-include.d.ts.map +1 -0
- package/dist/lib/worktree-include.js +68 -0
- package/dist/lib/worktree-include.js.map +1 -0
- package/dist/lib/worktree-setup.d.ts +18 -0
- package/dist/lib/worktree-setup.d.ts.map +1 -0
- package/dist/lib/worktree-setup.js +60 -0
- package/dist/lib/worktree-setup.js.map +1 -0
- package/dist/lib/worktree.d.ts +37 -0
- package/dist/lib/worktree.d.ts.map +1 -0
- package/dist/lib/worktree.js +411 -0
- package/dist/lib/worktree.js.map +1 -0
- package/dist/mcp/index.d.ts +8 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +8 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +45 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +153 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/session-bridge.d.ts +87 -0
- package/dist/mcp/session-bridge.d.ts.map +1 -0
- package/dist/mcp/session-bridge.js +317 -0
- package/dist/mcp/session-bridge.js.map +1 -0
- package/dist/mcp/tools.d.ts +70 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +234 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/mcp/types.d.ts +197 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +16 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/providers/base-adapter.d.ts +56 -0
- package/dist/providers/base-adapter.d.ts.map +1 -0
- package/dist/providers/base-adapter.js +5 -0
- package/dist/providers/base-adapter.js.map +1 -0
- package/dist/providers/claude-code-adapter.d.ts +27 -0
- package/dist/providers/claude-code-adapter.d.ts.map +1 -0
- package/dist/providers/claude-code-adapter.js +298 -0
- package/dist/providers/claude-code-adapter.js.map +1 -0
- package/dist/providers/claude-sdk-adapter.d.ts +60 -0
- package/dist/providers/claude-sdk-adapter.d.ts.map +1 -0
- package/dist/providers/claude-sdk-adapter.js +632 -0
- package/dist/providers/claude-sdk-adapter.js.map +1 -0
- package/dist/providers/codex-adapter.d.ts +21 -0
- package/dist/providers/codex-adapter.d.ts.map +1 -0
- package/dist/providers/codex-adapter.js +197 -0
- package/dist/providers/codex-adapter.js.map +1 -0
- package/dist/providers/index.d.ts +26 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +58 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/slurm-adapter.d.ts +26 -0
- package/dist/providers/slurm-adapter.d.ts.map +1 -0
- package/dist/providers/slurm-adapter.js +146 -0
- package/dist/providers/slurm-adapter.js.map +1 -0
- package/dist/types.d.ts +592 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- 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
|