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',
|
cmd: 'python3',
|
||||||
stdin: 'print("Hello from stdin!")'
|
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
|
## Input Parameters
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@tpmjs/tools-sprites-exec",
|
"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",
|
"description": "Execute a command inside a sprite and return the output with exit code",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,12 @@ import { jsonSchema, tool } from 'ai';
|
||||||
|
|
||||||
const SPRITES_API_BASE = 'https://api.sprites.dev/v1';
|
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 {
|
export interface ExecResult {
|
||||||
exitCode: number;
|
exitCode: number;
|
||||||
stdout: string;
|
stdout: string;
|
||||||
|
|
@ -34,6 +40,122 @@ function getSpritesToken(): string {
|
||||||
return token;
|
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({
|
export const spritesExecTool = tool({
|
||||||
description:
|
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.',
|
'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 timeout = timeoutMs || 60000;
|
||||||
const startTime = Date.now();
|
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;
|
let response: Response;
|
||||||
try {
|
try {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||||
|
|
||||||
const body: Record<string, unknown> = { cmd };
|
response = await fetch(url.toString(), {
|
||||||
if (stdin) {
|
|
||||||
body.stdin = stdin;
|
|
||||||
}
|
|
||||||
|
|
||||||
response = await fetch(`${SPRITES_API_BASE}/sprites/${encodeURIComponent(name)}/exec`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'User-Agent': 'TPMJS/1.0',
|
'User-Agent': 'TPMJS/1.0',
|
||||||
|
...(stdin ? { 'Content-Type': 'application/octet-stream' } : {}),
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body),
|
body: stdin || undefined,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -119,18 +254,16 @@ export const spritesExecTool = tool({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let data: Record<string, unknown>;
|
// Parse binary response
|
||||||
try {
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
data = (await response.json()) as Record<string, unknown>;
|
const buffer = new Uint8Array(arrayBuffer);
|
||||||
} catch {
|
const { stdout, stderr, exitCode } = parseBinaryResponse(buffer);
|
||||||
throw new Error('Failed to parse response from Sprites API');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
exitCode: (data.exitCode as number) ?? (data.exit_code as number) ?? 0,
|
exitCode,
|
||||||
stdout: (data.stdout as string) || '',
|
stdout,
|
||||||
stderr: (data.stderr as string) || '',
|
stderr,
|
||||||
duration: (data.duration as number) || duration,
|
duration,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue