Skip to content

computer: add putFile command #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: ty/fix-build-and-url
Choose a base branch
from
Open
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: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
HDR_API_KEY=YOUR_API_KEY_HERE
# optional
ANTHROPIC_API_KEY=
ANTHROPIC_API_KEY=
# set if using a local hudson instance, eg: http://localhost:8080 - this should not be considered a production-ready override and is slightly hacky
HOSTNAME_OVERRIDE=
47 changes: 30 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,32 +113,46 @@ Execute arbitrary bash commands on the remote system.
The main class for establishing connections and controlling remote computers.

```typescript
class Computer extends EventEmitter implements IComputer {
constructor(
baseUrl: string,
apiKey: string,
options?: Partial<ComputerOptions>
);
class Computer {
// Main factory method - use this to create instances
static create(options?: Partial<ComputerOptions>): Promise<Computer>;

// Core methods
connect(): Promise<void>;
execute(command: Action): Promise<ComputerMessage>;
isConnected(): boolean;
close(): Promise<void>;
do(objective: string, provider?: 'anthropic' | 'custom'): Promise<void>;
screenshot(): Promise<string>;

// Tool lists
listComputerUseTools(): BetaToolUnion[];
listMcpTools(): Promise<BetaTool[]>;
listAllTools(): Promise<BetaToolUnion[]>;

// MCP related operations
startMcpServer(name: string, command: string): Promise<StartServerResponse>;
callMcpTool(
name: string,
args?: Record<string, unknown>,
resultSchema?:
| typeof CallToolResultSchema
| typeof CompatibilityCallToolResultSchema,
options?: RequestOptions
);
getMcpServerCapabilities(): Promise<ServerCapabilities | undefined>;
getMcpServerVersion(): Promise<Implementation | undefined>;
mcpPing(): Promise<void>;

// File operations
putFile(path: string): Promise<Response>; // Upload the file located at the given local path
}
```

### Configuration Options

```typescript
interface ComputerOptions {
onMessage?: (message: ComputerMessage) => void | Promise<void>;
parseMessage?: (message: MessageEvent) => void | ComputerMessage;
onOpen?: () => void | Promise<void>;
onError?: (error: Error) => void;
onClose?: (code: number, reason: string) => void;
beforeSend?: (data: unknown) => unknown;
baseUrl: string; // HDR API base URL
apiKey: string | null; // HDR API authentication key
tools: Set<BetaToolUnion>; // Available tools for computer control (you'll likely want to leave this default)
logOutput: boolean; // Enable/disable logging
}
```

Expand Down Expand Up @@ -169,7 +183,6 @@ try {

- `HDR_API_KEY`: Your API key for authentication
- `ANTHROPIC_API_KEY`: optional Anthropic key for high-level objective-oriented computer use
- `HDR_WS_URL`: optional URL for computer use socket (defaults to 'wss://api.hdr.is/compute/ephemeral')

## Development

Expand Down
Binary file modified bun.lockb
Binary file not shown.
125 changes: 53 additions & 72 deletions lib/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import {
MachineMetadata,
type DefaultSamplingOptions,
} from '../lib/types';
import { logger } from '../lib/utils/logger';
import { makeToolResult } from './tools';
import { logger, cleanMessage } from '../lib/utils/logger';
import { convertToolResult } from './tools';
import { ToolResult } from '../lib/types';
import { UnknownAction } from './schemas/unknownAction';
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
Expand Down Expand Up @@ -57,6 +57,7 @@ export async function useComputer(
// Merge provided options with defaults
const samplingOptions = { ...defaultSamplingOptions, ...options };
const client = new Anthropic();
const tools = await computer.listAllTools();

// Initialize conversation history
const messages: BetaMessageParam[] = [];
Expand All @@ -70,18 +71,9 @@ export async function useComputer(
// Create system prompt that tells Claude about the computer's capabilities
const systemPrompt: BetaTextBlockParam = {
type: 'text',
text: await systemCapability(await computer.getMetadata()),
text: systemCapability(computer.machineMetadata),
};

// Verify computer connection before proceeding
if (!computer.isConnected()) {
throw new Error('Failed to connect to computer');
}

// Log available tools for debugging
const tools = await computer.listAllTools();
logger.info({ tools }, 'Tools enabled: ');

// Main interaction loop
while (true) {
// Get Claude's response
Expand All @@ -107,7 +99,7 @@ export async function useComputer(
} else if (content.type === 'tool_use') {
// Execute and log tool usage
logger.info({ command: content }, 'Executing: ');
toolResults.push(...(await handleToolRequest(content, computer)));
toolResults.push(await handleToolRequest(content, computer));
}
}

Expand All @@ -131,76 +123,65 @@ export async function useComputer(

// Clean up and log completion
logger.info({ task }, 'Completed task: ');
const cleanedMessages = messages.map(cleanMessage);

return cleanedMessages;
}

/**
* Handles execution of a single tool use request from Claude
*
* @param block - The tool use request from Claude
* @param computer - The computer instance
* @returns {Promise<BetaToolResultBlockParam[]>} - The tool results
* @returns {Promise<BetaToolResultBlockParam>} - The tool result
*/
async function handleToolRequest(block: BetaToolUseBlock, computer: Computer) {
const toolResults: BetaToolResultBlockParam[] = [];

// Select an executor function based on the shape of the 'block'
const execute = (() => {
const parseAction = Action.safeParse({
tool: block.name,
params: block.input,
});

if (parseAction.success) {
logger.debug(parseAction.data, 'Parsed action:');
return async () =>
makeToolResult(
(await computer.execute(parseAction.data)).tool_result,
block.id
);
}
async function handleToolRequest(
block: BetaToolUseBlock,
computer: Computer
): Promise<BetaToolResultBlockParam> {
// Try to parse as computer use action
const parseAction = Action.safeParse({
tool: block.name,
params: block.input,
});

const parseUnknownAction = UnknownAction.safeParse({
tool: block.name,
params: block.input,
});
if (parseAction.success) {
logger.debug(parseAction.data, 'Parsed action:');
return convertToolResult(
(await computer.execute(parseAction.data)).tool_result,
block.id
);
}

if (parseUnknownAction.success) {
logger.debug(parseUnknownAction.data, 'Parsed unknown action:');
return async () => {
const toolResult = await computer.callMcpTool(
parseUnknownAction.data.tool,
parseUnknownAction.data.params
);
const result: ToolResultBlockParam = {
tool_use_id: block.id,
type: 'tool_result',
content: JSON.stringify(toolResult.content),
is_error: toolResult.isError
? Boolean(toolResult.isError)
: undefined,
};
return result;
};
}
// Try to parse as MCP (unknown) action
const parseUnknownAction = UnknownAction.safeParse({
tool: block.name,
params: block.input,
});

const result = ToolResult.parse({
error: `Tool ${block.name} failed is invalid`,
output: null,
base64_image: null,
system: null,
});
const errorResult = makeToolResult(result, block.id);
logger.debug({ tool_use_error: errorResult }, 'Could not parse tool use');
toolResults.push(errorResult);
return null;
})();

if (execute) {
// Execute the tool request and store result
const toolResult = await execute();
logger.info(toolResult, 'Tool Result:');
toolResults.push(toolResult);
if (parseUnknownAction.success) {
logger.debug(parseUnknownAction.data, 'Parsed unknown action:');
const result = await computer.callMcpTool(
parseUnknownAction.data.tool,
parseUnknownAction.data.params
);
const toolResult: ToolResultBlockParam = {
tool_use_id: block.id,
type: 'tool_result',
content: JSON.stringify(result.content),
is_error: result.isError ? Boolean(result.isError) : undefined,
};
return toolResult;
}

return toolResults;
// Return error result
const result: ToolResult = {
error: `Tool ${block.name} failed is invalid`,
output: null,
base64_image: null,
system: null,
};
const errorResult = convertToolResult(result, block.id);
logger.debug({ tool_use_error: errorResult }, 'Could not parse tool use');
return errorResult;
}
112 changes: 112 additions & 0 deletions lib/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { Action } from './schemas/action';
import {
ComputerMessage,
StartServerResponseSchema,
type StartServerRequest,
type StartServerResponse,
} from './types';
import { fetchAndValidate } from './utils/fetchAndValidate';
import path from 'path';

type HdrApiPaths = {
computerUse: string;
mcp: {
base: string;
startServer: string;
};
file: {
upload: string;
download: string;
};
system: string;
};

enum HeaderSet {
Get,
PostJson,
PostFormData,
}

export class HdrApi {
apiKey: string | null;
paths: HdrApiPaths;

constructor(hostname: string, apiKey: string | null) {
this.apiKey = apiKey;
this.paths = {
computerUse: path.join(hostname, 'computer_use'),
mcp: {
base: path.join(hostname, 'mcp'),
startServer: path.join(hostname, 'mcp', 'register_server'),
},
file: {
// TODO: Remove redundant 'file' in paths
upload: path.join(hostname, 'file', 'file', 'upload'),
download: path.join(hostname, 'file', 'file', 'download'),
},
system: path.join(hostname, 'system'),
};
}

async useComputer(action: Action): Promise<ComputerMessage> {
return fetchAndValidate(this.paths.computerUse, ComputerMessage, {
method: 'POST',
headers: this.getHeaders(HeaderSet.PostJson),
body: JSON.stringify(action),
});
}

async startMcpServer(
name: string,
command: string
): Promise<StartServerResponse> {
const request: StartServerRequest = {
name,
command,
};

return fetchAndValidate(
this.paths.mcp.startServer,
StartServerResponseSchema,
{
method: 'POST',
headers: this.getHeaders(HeaderSet.PostJson),
body: JSON.stringify(request),
}
);
}

async uploadFile(filename: string, blob: Blob) {
const formData = new FormData();
formData.append('file', blob, filename);

return fetch(this.paths.file.upload, {
method: 'POST',
headers: this.getHeaders(HeaderSet.PostFormData),
body: formData,
});
}

private getBearerToken(): string {
return `Bearer ${this.apiKey}`;
}

private getHeaders(method: HeaderSet): HeadersInit {
switch (method) {
case HeaderSet.Get:
return {
Authorization: this.getBearerToken(),
};
case HeaderSet.PostJson:
return {
Authorization: this.getBearerToken(),
'Content-Type': 'application/json',
};
case HeaderSet.PostFormData:
return {
Authorization: this.getBearerToken(),
'Content-Type': 'multipart/form-data',
};
}
}
}
Loading