From 67f05af7a55bdd15e5ca39987bb6752608d66be7 Mon Sep 17 00:00:00 2001 From: Space-Banane Date: Fri, 27 Mar 2026 16:55:28 +0100 Subject: [PATCH] feat: add execute command for function execution with streaming options --- OpenclawSkill.md | 2 + package.json | 2 +- src/commands/function/execute.ts | 152 +++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/commands/function/execute.ts diff --git a/OpenclawSkill.md b/OpenclawSkill.md index dc8ea11..46ae77a 100644 --- a/OpenclawSkill.md +++ b/OpenclawSkill.md @@ -33,6 +33,8 @@ this will check the health, and if not setup, it will prompt you to set up the C - `shsf get namespace `: Get details of a specific namespace by its ID - `shsf get trigger `: Get details of a specific trigger from a function. +- `shsf execute --id [--payload ] [--no-stream]`: Execute a function and stream the output (debug/internal). + - `shsf storage create --name --purpose `: Create a new storage. - `shsf storage delete --name `: Delete a storage. - `shsf storage list`: List all storages. diff --git a/package.json b/package.json index 664a860..a8f14b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shsf-cli", - "version": "2.2.1", + "version": "2.2.2", "description": "", "type": "module", "files": [ diff --git a/src/commands/function/execute.ts b/src/commands/function/execute.ts new file mode 100644 index 0000000..f49ddb6 --- /dev/null +++ b/src/commands/function/execute.ts @@ -0,0 +1,152 @@ +import chalk from "chalk"; +import { getApiClient } from "../../api.js"; +import { AxiosResponse } from "axios"; + +export const functionExecuteDefinition = { + name: "execute", + description: "Execute a function and stream the output (debug/internal).", + options: [ + { name: "--id ", description: "Function ID", required: true }, + { name: "--payload ", description: "JSON payload for execution", required: false }, + { name: "--no-stream", description: "Disable streaming and get final result only", required: false, default: false }, + ], + action: async (options: any) => { + const client = await getApiClient(); + + // Explicitly check for noStream from commander + // Commander uses camelsCase for options, so --no-stream becomes stream: false + // but we use --no-stream which commander natively handles as a boolean toggle + const useStream = options.stream !== false; + + let payload = {}; + if (options.payload) { + try { + payload = JSON.parse(options.payload); + } catch (error: any) { + console.error(`${chalk.red("✗")} Invalid JSON payload: ${chalk.yellow(error.message)}`); + return; + } + } + + const handleJsonChunk = (chunk: string) => { + const trimmed = chunk.trim(); + if (!trimmed) return; + + try { + const obj = JSON.parse(trimmed); + if (obj.type === "output" && obj.content) { + process.stdout.write(obj.content); + } else if (obj.type === "end") { + if (obj.exitCode !== 0 && obj.exitCode !== undefined) { + console.error(`\n${chalk.red("✗")} Execution failed with exit code ${obj.exitCode}`); + } else { + console.log(`\n${chalk.green("✓")} Execution completed.`); + } + if (obj.result !== undefined) { + console.log(chalk.blue("Result:"), typeof obj.result === 'object' ? JSON.stringify(obj.result, null, 2) : obj.result); + } + if (obj.took) { + const total = obj.took.find((t: any) => t.description === "Total"); + if (total) { + console.log(chalk.gray(`Total time: ${total.value}s`)); + } + } + } + } catch (e) { + // If it's not JSON, it might be raw output from a non-stream response + // that got into the stream handler mistakenly or just raw noise + if (trimmed.length > 0) { + process.stdout.write(chunk); + } + // Not a full JSON object yet or invalid JSON + } + }; + + console.log(`${chalk.blue("ℹ")} Executing function ${chalk.cyan(options.id)} (stream: ${useStream})...`); + + try { + const response = await client.post(`/api/function/${options.id}/execute`, + { run: payload }, + { + params: { stream: useStream ? "true" : "false" }, + responseType: useStream ? "stream" : "json" + } + ); + + if (useStream) { + const stream = (response as AxiosResponse).data; + let buffer = ""; + + return new Promise((resolve, reject) => { + stream.on("data", (chunk: any) => { + const str = chunk.toString(); + buffer += str; + + // Try to split by potential JSON boundaries + // Matches cases like ...}{... + const pattern = /\}\s*\{/g; + let lastIndex = 0; + let match; + + while ((match = pattern.exec(buffer)) !== null) { + const part = buffer.slice(lastIndex, match.index + 1); + handleJsonChunk(part); + lastIndex = match.index + match[0].indexOf('{'); + } + + buffer = buffer.slice(lastIndex); + + // Attempt to parse the remaining buffer if it looks like a complete object + try { + if (buffer.trim().startsWith('{') && buffer.trim().endsWith('}')) { + handleJsonChunk(buffer); + buffer = ""; + } + } catch (e) { + // Wait for more data + } + }); + + stream.on("end", () => { + if (buffer.trim()) { + handleJsonChunk(buffer); + } + resolve(); + }); + + stream.on("error", (err: Error) => { + console.error(`\n${chalk.red("✗")} Stream error: ${err.message}`); + reject(err); + }); + }); + } else { + // Non-stream returns just the output content or the body + const result = response.data; + if (typeof result === 'string') { + process.stdout.write(result); + } else if (result && result.data && typeof result.data === 'string') { + process.stdout.write(result.data); + } else if (result && typeof result.output === 'string') { + process.stdout.write(result.output); + } else if (result && typeof result.result === 'string') { + process.stdout.write(result.result); + } else if (result && result.data && result.data.output) { + process.stdout.write(result.data.output); + } else { + process.stdout.write(JSON.stringify(result, null, 2)); + } + console.log(`\n${chalk.green("✓")} Execution completed.`); + } + } catch (error: any) { + if (error.response) { + console.error( + `${chalk.red("✗")} Execution failed: ${chalk.yellow(error.response.data?.message || error.response.statusText || "Unknown error")}`, + ); + } else if (error.request) { + console.error(`${chalk.red("✗")} No response received from server.`); + } else { + console.error(`${chalk.red("✗")} Error: ${error.message}`); + } + } + }, +};