+
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Skip node_modules and rover for development image
.rover
.claude
node_modules
2 changes: 1 addition & 1 deletion .github/workflows/build-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
- name: Build and push
uses: docker/build-push-action@v5
with:
context: ./images/node
context: .
file: ./images/node/Dockerfile
platforms: ${{ inputs.platforms }}
push: true
Expand Down
17 changes: 15 additions & 2 deletions images/node/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
FROM node:24-alpine AS agent-builder
COPY . /app
WORKDIR /app
# Build the agent
RUN npm install && npm run build:agent
RUN cd ./packages/agent && npm pack && mv *.tgz /rover-agent.tgz

FROM node:24-alpine
ARG TARGETARCH
ARG PACKAGE_MANAGER_MCP_SERVER_VERSION="v0.1.3"
Expand All @@ -11,5 +18,11 @@ RUN apk update && \
wget -O /usr/local/bin/package-manager-mcp-server \
https://github.com/endorhq/package-manager-mcp/releases/download/${PACKAGE_MANAGER_MCP_SERVER_VERSION}/package-manager-mcp-${ARCH_SUFFIX} && \
chmod +s+x /usr/local/bin/package-manager-mcp-server
COPY ./assets/1-sudoers-setup /etc/sudoers.d/1-agent-setup
COPY ./assets/2-sudoers-cleanup /etc/sudoers.d/2-agent-cleanup

# Install Node dependencies
COPY --from=agent-builder /rover-agent.tgz /rover-agent.tgz
RUN npm install -g mcp-remote@0.1.29
RUN npm install -g /rover-agent.tgz

COPY ./images/node/assets/1-sudoers-setup /etc/sudoers.d/1-agent-setup
COPY ./images/node/assets/2-sudoers-cleanup /etc/sudoers.d/2-agent-cleanup
28 changes: 28 additions & 0 deletions images/node/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
FROM node:24-alpine AS agent-builder
COPY . /app
WORKDIR /app
# Build the agent
RUN npm install && npm run build:agent
RUN cd ./packages/agent && npm pack && mv *.tgz /rover-agent.tgz

FROM node:24-alpine
ARG TARGETARCH
ARG PACKAGE_MANAGER_MCP_SERVER_VERSION="v0.1.3"
RUN apk update && \
apk add bash file jq sudo && \
case ${TARGETARCH} in \
amd64) ARCH_SUFFIX="x86_64-unknown-linux-musl" ;; \
arm64) ARCH_SUFFIX="aarch64-unknown-linux-musl" ;; \
*) echo "Unsupported architecture: ${TARGETARCH}" && exit 1 ;; \
esac && \
wget -O /usr/local/bin/package-manager-mcp-server \
https://github.com/endorhq/package-manager-mcp/releases/download/${PACKAGE_MANAGER_MCP_SERVER_VERSION}/package-manager-mcp-${ARCH_SUFFIX} && \
chmod +s+x /usr/local/bin/package-manager-mcp-server

# Install Node dependencies
COPY --from=agent-builder /rover-agent.tgz /rover-agent.tgz
RUN npm install -g mcp-remote@0.1.29
RUN npm install -g /rover-agent.tgz

COPY ./images/node/assets/1-sudoers-setup /etc/sudoers.d/1-agent-setup
COPY ./images/node/assets/2-sudoers-cleanup /etc/sudoers.d/2-agent-cleanup
28 changes: 28 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion packages/agent/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ program
[]
)
.option('--inputs-json <jsonPath>', 'Load the input values from a JSON file')
.option('--inputs-yaml <yamlPath>', 'Load the input values from a YAML file')
.option(
'--agent-tool <agent>',
'Agent tool to use. It overrides defaults, but prioritize step tools if available.'
Expand All @@ -65,6 +64,10 @@ program
'--status-file <path>',
'Path to status.json file for tracking workflow progress'
)
.option(
'--output <directory>',
'Directory to move the output files and values from the workflow. If none, it will save them in the current folder.'
)
.action(runCommand);

// Install workflow dependencies
Expand Down
2 changes: 1 addition & 1 deletion packages/agent/src/commands/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface InstallCommandOutput extends CommandOutput {}

// Default agent version to install
export const DEFAULT_INSTALL_VERSION = 'latest';
export const DEFAULT_INSTALL_DIRECTORY = '/home/agent';
export const DEFAULT_INSTALL_DIRECTORY = process.env.HOME || '/home/agent';

/**
* Install an AI Coding Tool and configure the required credentials to run it
Expand Down
65 changes: 58 additions & 7 deletions packages/agent/src/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ import { AgentWorkflow } from '../workflow.js';
import { parseCollectOptions } from '../lib/options.js';
import { Runner } from '../lib/runner.js';
import { IterationStatus } from 'rover-common';
import { existsSync, readFileSync } from 'node:fs';

interface RunCommandOptions {
// Inputs. Take precedence over files
input: string[];
// Load the inputs from a YAML file
inputYaml?: string;
// Load the inputs from a JSON file
inputJson?: string;
inputsJson?: string;
// Tool to use instead of workflow defaults
agentTool?: string;
// Model to use instead of workflow defaults
Expand All @@ -20,6 +19,8 @@ interface RunCommandOptions {
taskId?: string;
// Path to status.json file
statusFile?: string;
// Optional output directory
output?: string;
}

interface RunCommandOutput extends CommandOutput {}
Expand All @@ -38,14 +39,24 @@ export const runCommand = async (

// Declare status manager outside try block so it's accessible in catch
let statusManager: IterationStatus | undefined;
let totalDuration = 0;

try {
// Validate status tracking options
if (options.statusFile && !options.taskId) {
console.log(
colors.red('\n✗ --task-id is required when --status-file is provided')
);
output.error = '--task-id is required when --status-file is provided';
return;
}

// Check if the output folder exists.
if (options.output && !existsSync(options.output)) {
console.log(
colors.red(
`\n✗ The "${options.output}" directory does not exist or current user does not have permissions.`
)
);
return;
}

Expand All @@ -70,7 +81,37 @@ export const runCommand = async (

// Load the agent workflow
const agentWorkflow = AgentWorkflow.load(workflowPath);
const providedInputs = parseCollectOptions(options.input);
let providedInputs = new Map();

if (options.inputsJson != null) {
console.log(colors.gray(`Loading inputs from ${options.inputsJson}\n`));
if (!existsSync(options.inputsJson)) {
console.log(
colors.yellow(
`The provided JSON input file (${options.inputsJson}) does not exist. Skipping it.`
)
);
} else {
try {
const jsonData = readFileSync(options.inputsJson, 'utf-8');
const data = JSON.parse(jsonData);

for (const key in data) {
providedInputs.set(key, data[key]);
}
} catch (err) {
console.log(
colors.yellow(
`The provided JSON input file (${options.inputsJson}) is not a valid JSON. Skipping it.`
)
);
}
}
}

// Users might override the --inputs-json values with --input.
// The --input always have preference
providedInputs = parseCollectOptions(options.input, providedInputs);

// Merge provided inputs with defaults
const inputs = new Map(providedInputs);
Expand Down Expand Up @@ -151,7 +192,7 @@ export const runCommand = async (
runSteps++;

// Run it
const result = await runner.run();
const result = await runner.run(options.output);

// Display step results
console.log(colors.bold(`\n📊 Step Results: ${step.name}`));
Expand All @@ -166,6 +207,7 @@ export const runCommand = async (
colors.gray('├── Duration: ') +
colors.yellow(`${result.duration.toFixed(2)}s`)
);
totalDuration += result.duration;

if (result.tokens) {
console.log(
Expand Down Expand Up @@ -196,8 +238,13 @@ export const runCommand = async (
const prefix =
idx === outputEntries.length - 1 ? ' └──' : ' ├──';
// Truncate long values for display
const displayValue =
let displayValue =
value.length > 100 ? value.substring(0, 100) + '...' : value;

if (displayValue.includes('\n')) {
displayValue = displayValue.split('\n')[0] + '...';
}

console.log(
colors.gray(`${prefix} ${key}: `) + colors.cyan(displayValue)
);
Expand Down Expand Up @@ -236,6 +283,10 @@ export const runCommand = async (

// Display workflow completion summary
console.log(colors.bold('\n🎉 Workflow Execution Summary'));
console.log(
colors.gray('├── Duration: ') +
colors.cyan(totalDuration.toFixed(2) + 's')
);
console.log(
colors.gray('├── Total Steps: ') +
colors.cyan(agentWorkflow.steps.length.toString())
Expand Down
49 changes: 44 additions & 5 deletions packages/agent/src/lib/agents/claude.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { existsSync, copyFileSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import {
existsSync,
copyFileSync,
readFileSync,
writeFileSync,
lstatSync,
cpSync,
} from 'node:fs';
import path, { basename, join } from 'node:path';
import colors from 'ansi-colors';
import { AgentCredentialFile } from './types.js';
import { BaseAgent } from './base.js';
Expand Down Expand Up @@ -27,39 +34,71 @@ export class ClaudeAgent extends BaseAgent {
required: true,
},
];

if (requiredBedrockCredentials()) {
// TODO: mount bedrock credentials
}

if (requiredClaudeCredentials()) {
requiredCredentials.push({
path: '/.credentials.json',
description: 'Claude credentials',
required: true,
});
}

if (requiredVertexAiCredentials()) {
requiredCredentials.push({
path: '/.config/gcloud',
description: 'Google Cloud credentials',
required: true,
});
}

return requiredCredentials;
}

async copyCredentials(targetDir: string): Promise<void> {
console.log(colors.bold(`\nCopying ${this.name} credentials`));

const targetClaudeDir = join(targetDir, '.claude');
console.log(colors.gray(`├── Target directory: ${targetClaudeDir}`));
// Ensure .claude directory exists
this.ensureDirectory(targetClaudeDir);

const credentials = this.getRequiredCredentials();

for (const cred of credentials) {
if (existsSync(cred.path)) {
const filename = cred.path.split('/').pop()!;
copyFileSync(cred.path, join(targetClaudeDir, filename));
console.log(colors.gray('├── Copied: ') + colors.cyan(cred.path));
const filename = basename(cred.path);

// For .claude.json, we need to edit the projects section
if (cred.path.includes('.claude.json')) {
console.log(colors.gray('├── Processing .claude.json'));

// Read the config and clear the projects object
const config = JSON.parse(readFileSync(cred.path, 'utf-8'));
config.projects = {};

// Write to targetDir instead of targetClaudeDir.
// The .claude.json file is located at $HOME
writeFileSync(
join(targetDir, filename),
JSON.stringify(config, null, 2)
);
console.log(
colors.gray('├── Copied: ') +
colors.cyan('.claude.json (projects cleared)')
);
} else if (cred.path.includes('gcloud')) {
// Copy the entire folder
cpSync(cred.path, join(targetDir, '.config'), { recursive: true });
console.log(colors.gray('├── Copied: ') + colors.cyan(cred.path));
} else {
// Copy file right away
copyFileSync(cred.path, join(targetClaudeDir, filename));
console.log(colors.gray('├── Copied: ') + colors.cyan(cred.path));
}
}
}

Expand Down
9 changes: 7 additions & 2 deletions packages/agent/src/lib/agents/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@ export class CodexAgent extends BaseAgent {
},
{
path: '/.codex/config.json',
description: 'Codex configuration',
required: true,
description: 'Codex configuration (old)',
required: false,
},
{
path: '/.codex/config.toml',
description: 'Codex configuration (new)',
required: false,
},
];
}
Expand Down
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载