fix: sprites-exec tool to use correct API format
- Use query parameters instead of JSON body for cmd - Parse command string into repeatable cmd params - Handle binary response format (stdout=0x01, stderr=0x02, exit=0x03) - Add shell command documentation to README
This commit is contained in:
parent
ce3596ea22
commit
b5fa46f6be
3 changed files with 169 additions and 19 deletions
|
|
@ -37,6 +37,23 @@ const pythonResult = await spritesExecTool.execute({
|
|||
cmd: 'python3',
|
||||
stdin: 'print("Hello from stdin!")'
|
||||
});
|
||||
|
||||
// For shell features (pipes, redirects, etc), use bash -c
|
||||
const shellResult = await spritesExecTool.execute({
|
||||
name: 'my-sandbox',
|
||||
cmd: 'bash -c "echo hello > /tmp/test.txt && cat /tmp/test.txt"'
|
||||
});
|
||||
```
|
||||
|
||||
## Shell Commands
|
||||
|
||||
Commands are executed directly (like `exec.Command` in Go), not through a shell. This means shell operators like `|`, `>`, `>>`, `&&` won't work directly.
|
||||
|
||||
For shell features, wrap your command with `bash -c`:
|
||||
```typescript
|
||||
// Won't work: cmd: 'echo hello > file.txt'
|
||||
// Use instead:
|
||||
cmd: 'bash -c "echo hello > file.txt"'
|
||||
```
|
||||
|
||||
## Input Parameters
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tpmjs/tools-sprites-exec",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"description": "Execute a command inside a sprite and return the output with exit code",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
|
|
|
|||
|
|
@ -10,6 +10,12 @@ import { jsonSchema, tool } from 'ai';
|
|||
|
||||
const SPRITES_API_BASE = 'https://api.sprites.dev/v1';
|
||||
|
||||
// Binary stream IDs from Sprites API
|
||||
const STREAM_STDIN = 0x00;
|
||||
const STREAM_STDOUT = 0x01;
|
||||
const STREAM_STDERR = 0x02;
|
||||
const STREAM_EXIT = 0x03;
|
||||
|
||||
export interface ExecResult {
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
|
|
@ -34,6 +40,122 @@ function getSpritesToken(): string {
|
|||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a command string into command and arguments.
|
||||
* Handles quoted strings and escapes.
|
||||
*/
|
||||
function parseCommand(cmdString: string): string[] {
|
||||
const args: string[] = [];
|
||||
let current = '';
|
||||
let inSingleQuote = false;
|
||||
let inDoubleQuote = false;
|
||||
let escaped = false;
|
||||
|
||||
for (let i = 0; i < cmdString.length; i++) {
|
||||
const char = cmdString[i];
|
||||
|
||||
if (escaped) {
|
||||
current += char;
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '\\' && !inSingleQuote) {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "'" && !inDoubleQuote) {
|
||||
inSingleQuote = !inSingleQuote;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '"' && !inSingleQuote) {
|
||||
inDoubleQuote = !inDoubleQuote;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === ' ' && !inSingleQuote && !inDoubleQuote) {
|
||||
if (current.length > 0) {
|
||||
args.push(current);
|
||||
current = '';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
current += char;
|
||||
}
|
||||
|
||||
if (current.length > 0) {
|
||||
args.push(current);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the binary response from Sprites API.
|
||||
* Format: [stream_id: 1 byte][payload: rest until next stream_id or end]
|
||||
*/
|
||||
function parseBinaryResponse(buffer: Uint8Array): {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number;
|
||||
} {
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let exitCode = 0;
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
let i = 0;
|
||||
|
||||
while (i < buffer.length) {
|
||||
const streamId = buffer[i];
|
||||
i++;
|
||||
|
||||
if (streamId === STREAM_EXIT) {
|
||||
// Exit code is a single byte
|
||||
if (i < buffer.length) {
|
||||
exitCode = buffer[i] as number;
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the next stream marker or end of buffer
|
||||
let end = i;
|
||||
while (end < buffer.length) {
|
||||
const nextByte = buffer[end] as number;
|
||||
// Check if this looks like a stream ID (0x00-0x03) at a boundary
|
||||
// We detect boundaries by looking for stream IDs that make sense
|
||||
if (nextByte <= STREAM_EXIT) {
|
||||
// Look ahead to see if this is really a stream marker
|
||||
// Stream markers are followed by data or another marker
|
||||
break;
|
||||
}
|
||||
end++;
|
||||
}
|
||||
|
||||
const payload = decoder.decode(buffer.slice(i, end));
|
||||
|
||||
switch (streamId) {
|
||||
case STREAM_STDIN:
|
||||
// Ignore stdin echo
|
||||
break;
|
||||
case STREAM_STDOUT:
|
||||
stdout += payload;
|
||||
break;
|
||||
case STREAM_STDERR:
|
||||
stderr += payload;
|
||||
break;
|
||||
}
|
||||
|
||||
i = end;
|
||||
}
|
||||
|
||||
return { stdout, stderr, exitCode };
|
||||
}
|
||||
|
||||
export const spritesExecTool = tool({
|
||||
description:
|
||||
'Execute a command inside a sprite and return the output. Supports stdin input for interactive commands. Returns exit code, stdout, stderr, and execution duration.',
|
||||
|
|
@ -72,24 +194,37 @@ export const spritesExecTool = tool({
|
|||
const timeout = timeoutMs || 60000;
|
||||
const startTime = Date.now();
|
||||
|
||||
// Parse command into parts
|
||||
const cmdParts = parseCommand(cmd);
|
||||
if (cmdParts.length === 0) {
|
||||
throw new Error('Command cannot be empty');
|
||||
}
|
||||
|
||||
// Build URL with repeatable cmd query parameters
|
||||
// API expects: ?cmd=echo&cmd=hello&cmd=world for "echo hello world"
|
||||
const url = new URL(`${SPRITES_API_BASE}/sprites/${encodeURIComponent(name)}/exec`);
|
||||
for (const part of cmdParts) {
|
||||
url.searchParams.append('cmd', part);
|
||||
}
|
||||
|
||||
// Add stdin flag if stdin is provided
|
||||
if (stdin) {
|
||||
url.searchParams.set('stdin', 'true');
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
const body: Record<string, unknown> = { cmd };
|
||||
if (stdin) {
|
||||
body.stdin = stdin;
|
||||
}
|
||||
|
||||
response = await fetch(`${SPRITES_API_BASE}/sprites/${encodeURIComponent(name)}/exec`, {
|
||||
response = await fetch(url.toString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'TPMJS/1.0',
|
||||
...(stdin ? { 'Content-Type': 'application/octet-stream' } : {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
body: stdin || undefined,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
|
|
@ -119,18 +254,16 @@ export const spritesExecTool = tool({
|
|||
);
|
||||
}
|
||||
|
||||
let data: Record<string, unknown>;
|
||||
try {
|
||||
data = (await response.json()) as Record<string, unknown>;
|
||||
} catch {
|
||||
throw new Error('Failed to parse response from Sprites API');
|
||||
}
|
||||
// Parse binary response
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = new Uint8Array(arrayBuffer);
|
||||
const { stdout, stderr, exitCode } = parseBinaryResponse(buffer);
|
||||
|
||||
return {
|
||||
exitCode: (data.exitCode as number) ?? (data.exit_code as number) ?? 0,
|
||||
stdout: (data.stdout as string) || '',
|
||||
stderr: (data.stderr as string) || '',
|
||||
duration: (data.duration as number) || duration,
|
||||
exitCode,
|
||||
stdout,
|
||||
stderr,
|
||||
duration,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue