@noimosai/platform-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/USAGE.md +103 -0
- package/bin/noimos-platform.mjs +11 -0
- package/dist/cli/cli.js +177 -0
- package/dist/cli/command-help.js +69 -0
- package/dist/cli/download.js +72 -0
- package/dist/cli/index.js +3 -0
- package/dist/client/index.js +225 -0
- package/dist/client/local-input.js +37 -0
- package/dist/schema/platformPublicExecution.js +64 -0
- package/package.json +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AGOS Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/USAGE.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# NoimosAI Platform CLI
|
|
2
|
+
|
|
3
|
+
Command-line access to the NoimosAI Developer Platform.
|
|
4
|
+
Requires Node.js 22 or later.
|
|
5
|
+
|
|
6
|
+
~~~bash
|
|
7
|
+
npm install -g @noimosai/platform-cli
|
|
8
|
+
noimos-platform --help
|
|
9
|
+
~~~
|
|
10
|
+
|
|
11
|
+
Without a global install: npx -y @noimosai/platform-cli --help.
|
|
12
|
+
|
|
13
|
+
Set NOIMOS_PLATFORM_API_URL to the tool API URL shown in the developer console
|
|
14
|
+
(ending in /noimosToolBridge) and NOIMOS_PLATFORM_API_KEY to your dedicated
|
|
15
|
+
developer key. Never commit the key to a repository.
|
|
16
|
+
|
|
17
|
+
~~~bash
|
|
18
|
+
noimos-platform tools --summary
|
|
19
|
+
noimos-platform tool web_search
|
|
20
|
+
noimos-platform quote web_search '{"query":"content marketing"}'
|
|
21
|
+
noimos-platform help execute
|
|
22
|
+
~~~
|
|
23
|
+
|
|
24
|
+
Execution requires the quote's price version and maximum charge, plus a retained
|
|
25
|
+
idempotency key. Retry an uncertain submission with the same arguments and key.
|
|
26
|
+
Use execution, executions, wait, and download to recover results.
|
|
27
|
+
Discovery and quotes do not start paid work.
|
|
28
|
+
|
|
29
|
+
## Media understanding and transcription
|
|
30
|
+
|
|
31
|
+
Use analyze_video for agentic video understanding, analyze_image for image/OCR
|
|
32
|
+
questions, analyze_audio for sound/music/speech questions, and transcribe_audio
|
|
33
|
+
for verbatim text, speaker labels and word timestamps with SRT/VTT strings.
|
|
34
|
+
Inspect each tool's current schema and availability with `tool <name>`.
|
|
35
|
+
|
|
36
|
+
~~~bash
|
|
37
|
+
noimos-platform upload ./interview.wav
|
|
38
|
+
noimos-platform tool transcribe_audio
|
|
39
|
+
noimos-platform quote analyze_video '{"source":{"url":"https://www.youtube.com/watch?v=LuIL5JATZsc"},"question":"Describe the key scenes with timestamps."}'
|
|
40
|
+
~~~
|
|
41
|
+
|
|
42
|
+
Use the upload response's `data.id` as `source.fileId`. Inputs may alternatively
|
|
43
|
+
reference a project artifact with `source.executionId` and `source.artifactId`.
|
|
44
|
+
Uploads accept explicitly selected regular JPG/PNG/WAV/MP3/MP4 files up to 12 MiB,
|
|
45
|
+
are private to the developer project and expire after 30 days. Public HTTPS media
|
|
46
|
+
URLs use the same size bound; public YouTube video URLs are passed to the analysis
|
|
47
|
+
service. Uploads do not start inference. Quotes reserve input/output token limits;
|
|
48
|
+
adaptive video processing can exceed the reservation and require review, without
|
|
49
|
+
automatically charging above the accepted maximum. Use local tools such as FFmpeg
|
|
50
|
+
for conversion, trimming, merging and subtitle rendering.
|
|
51
|
+
|
|
52
|
+
## Reference generation and voice isolation
|
|
53
|
+
|
|
54
|
+
Use generate_image with referenceImages and describe the desired edit in prompt.
|
|
55
|
+
Each image is a {fileId}, {executionId, artifactId}, or {url} object. Upload a local
|
|
56
|
+
image first, then use its returned ID. Gemini and both GPT Image models support
|
|
57
|
+
the same input contract; inspect the catalog for current models and prices.
|
|
58
|
+
|
|
59
|
+
~~~bash
|
|
60
|
+
noimos-platform upload ./product.png
|
|
61
|
+
noimos-platform quote generate_image '{"prompt":"Keep the product; change the background to a bright kitchen.","referenceImages":[{"fileId":"f_<SHA-256>"}]}'
|
|
62
|
+
noimos-platform quote generate_video '{"prompt":"Slow camera move around the product.","firstFrame":{"fileId":"f_<SHA-256>"},"duration":5}'
|
|
63
|
+
noimos-platform quote isolate_voice '{"source":{"fileId":"f_<SHA-256>"},"maxDurationSeconds":60}'
|
|
64
|
+
~~~
|
|
65
|
+
|
|
66
|
+
Replace the placeholder IDs with upload results. Image/video references accept
|
|
67
|
+
up to four ordered JPG/PNG images, 12 MiB combined. Video accepts referenceImages
|
|
68
|
+
or firstFrame with optional lastFrame; omit aspectRatio in frame mode. Reuse a
|
|
69
|
+
generated artifact as input for further edits without downloading/uploading it.
|
|
70
|
+
|
|
71
|
+
isolate_voice removes background noise from speech. It accepts PCM WAV or MPEG-1
|
|
72
|
+
Layer III MP3 at 44.1 kHz/128 kbps, up to 12 MiB and 600 seconds. It bills the full
|
|
73
|
+
input duration including silence; unused duration reservation is returned.
|
|
74
|
+
Availability and the configured per-minute price are returned by the catalog.
|
|
75
|
+
|
|
76
|
+
## MCP
|
|
77
|
+
|
|
78
|
+
The MCP server is a separate package: @noimosai/platform-mcp.
|
|
79
|
+
Configure your host to launch npx with arguments
|
|
80
|
+
["-y", "@noimosai/platform-mcp"], passing the two environment variables above.
|
|
81
|
+
Pin a tested package version in managed installations.
|
|
82
|
+
|
|
83
|
+
~~~json
|
|
84
|
+
{
|
|
85
|
+
"mcpServers": {
|
|
86
|
+
"noimos-platform": {
|
|
87
|
+
"command": "npx",
|
|
88
|
+
"args": ["-y", "@noimosai/platform-mcp"],
|
|
89
|
+
"env": {
|
|
90
|
+
"NOIMOS_PLATFORM_API_URL": "https://YOUR_API_HOST/noimosToolBridge",
|
|
91
|
+
"NOIMOS_PLATFORM_API_KEY": "YOUR_DEVELOPER_API_KEY"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
~~~
|
|
97
|
+
|
|
98
|
+
This package uses developer projects and USD balances. It does not use the
|
|
99
|
+
workspace credentials, social connections or credits of the NoimosAI app.
|
|
100
|
+
For workspace operations, use the separate @noimosai/cli or @noimosai/mcp.
|
|
101
|
+
|
|
102
|
+
Local emulators only: set NOIMOS_PLATFORM_ENV=local to allow loopback HTTP.
|
|
103
|
+
The MCP transport is stdio; this package does not start a hosted HTTP endpoint.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
const args = process.argv.slice(2);
|
|
5
|
+
if (args.length === 1 && args[0] === "--version") {
|
|
6
|
+
const manifest = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
7
|
+
console.log(manifest.version);
|
|
8
|
+
} else {
|
|
9
|
+
const { runCli } = await import("../dist/cli/cli.js");
|
|
10
|
+
process.exitCode = await runCli(args, process.env, console.log, console.error);
|
|
11
|
+
}
|
package/dist/cli/cli.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { configFromEnvironment, PlatformClient, platformToolInputSchema, platformCreateExecutionSchema, platformArtifactIdSchema, platformExecutionListQuerySchema, platformToolDiscoverySchema } from "../client/index.js";
|
|
4
|
+
import { downloadArtifact } from "./download.js";
|
|
5
|
+
import { commandHelp } from "./command-help.js";
|
|
6
|
+
import { uploadLocalInput } from "../client/local-input.js";
|
|
7
|
+
export const help = `NoimosAI Developer Platform CLI
|
|
8
|
+
|
|
9
|
+
Usage: noimos-platform <command>
|
|
10
|
+
noimos-platform <command> --help | help <command>
|
|
11
|
+
|
|
12
|
+
project Show the developer organization and project bound to the API key
|
|
13
|
+
usage Show the current project's monthly request count and spend
|
|
14
|
+
tools [--query <text>] [--category <research|seo|social|generation>]
|
|
15
|
+
[--available-only] [--summary]
|
|
16
|
+
List tools and USD prices; summary omits schemas and examples
|
|
17
|
+
tool <name>
|
|
18
|
+
Inspect one tool's arguments, example and price
|
|
19
|
+
executions [--limit <1-100>] [--before <id>] [--status <state>] [--tool <name>]
|
|
20
|
+
Recover the API key's project execution history
|
|
21
|
+
quote <tool> '<arguments-json>'
|
|
22
|
+
Get a free quote without running the tool
|
|
23
|
+
execute <tool> '<arguments-json>' --maximum-charge-micros <integer>
|
|
24
|
+
--price-version <version> --idempotency-key <uuid>
|
|
25
|
+
Reserve up to the quoted amount and enqueue paid work
|
|
26
|
+
execution <id>
|
|
27
|
+
Read status, output and the final USD charge
|
|
28
|
+
wait <id> [--timeout <seconds>]
|
|
29
|
+
Wait for completion (default 480 seconds, maximum 900)
|
|
30
|
+
cancel <id>
|
|
31
|
+
Request cancellation of work that has not started
|
|
32
|
+
artifact-url <execution-id> <artifact-id>
|
|
33
|
+
Get a private output's download URL, valid for up to 5 minutes
|
|
34
|
+
download <execution-id> <artifact-id> --out <file>
|
|
35
|
+
Save an artifact after size/SHA-256 checks; never overwrite a file
|
|
36
|
+
upload <file>
|
|
37
|
+
Upload one explicit JPG/PNG/WAV/MP3/MP4 input, at most 12 MiB
|
|
38
|
+
|
|
39
|
+
Get a quote first. Use its price version and maximum charge (1 USD = 1000000
|
|
40
|
+
micros). Keep the same body and idempotency key when retrying a submission.
|
|
41
|
+
Execution is asynchronous. Poll at intervals of at least 5 seconds.
|
|
42
|
+
Use - in place of arguments-json to read JSON from stdin (at most 64 KiB).
|
|
43
|
+
Use --input-file <path> in place of arguments-json to read a JSON file.
|
|
44
|
+
wait exits 0 on success, 1 on failed/cancelled/needs_review, 3 on timeout.
|
|
45
|
+
A timeout leaves accepted work running; read or wait on the same execution ID.
|
|
46
|
+
|
|
47
|
+
Standalone tools only: no workspace, brand, knowledge base, dataset, connected
|
|
48
|
+
provider account, post, workspace skill or chat-session commands. Scope comes
|
|
49
|
+
from the developer API key; supply prompts, public URLs, project files or artifacts.
|
|
50
|
+
|
|
51
|
+
Environment: NOIMOS_PLATFORM_API_URL, NOIMOS_PLATFORM_API_KEY
|
|
52
|
+
Local emulators only: NOIMOS_PLATFORM_ENV=local
|
|
53
|
+
|
|
54
|
+
Manage organizations, members, keys and billing at platform.noimosai.com.
|
|
55
|
+
`;
|
|
56
|
+
export async function runCli(args, env, output, error, transport = fetch, stdin = process.stdin) {
|
|
57
|
+
if (!args.length || args[0] === "--help" || args[0] === "-h" || args.length === 1 && args[0] === "help") {
|
|
58
|
+
output(help);
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
if (args[0] === "help" || args.slice(1).some((arg) => arg === "--help" || arg === "-h")) {
|
|
62
|
+
const detail = commandHelp(args[0] === "help" ? args[1] : args[0]);
|
|
63
|
+
if (!detail || args[0] === "help" && args.length !== 2) {
|
|
64
|
+
error("Unknown help command. Run noimos-platform --help.");
|
|
65
|
+
return 2;
|
|
66
|
+
}
|
|
67
|
+
output(detail);
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
if (args.length === 1 && args[0] === "--version") {
|
|
71
|
+
output("0.1.0");
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
let command;
|
|
75
|
+
let exitCode = 0;
|
|
76
|
+
try {
|
|
77
|
+
if (["quote", "execute"].includes(args[0]) && ["-", "--input-file"].includes(args[2])) {
|
|
78
|
+
const file = args[2] === "--input-file" ? args[3] : "-";
|
|
79
|
+
if (!file || file.startsWith("--"))
|
|
80
|
+
throw new Error("Provide an input file");
|
|
81
|
+
const source = file === "-" ? stdin : createReadStream(file);
|
|
82
|
+
const chunks = [];
|
|
83
|
+
let bytes = 0;
|
|
84
|
+
for await (const chunk of source) {
|
|
85
|
+
const buffer = Buffer.from(chunk);
|
|
86
|
+
bytes += buffer.byteLength;
|
|
87
|
+
if (bytes > 65536)
|
|
88
|
+
throw new Error("Input exceeds 64 KiB");
|
|
89
|
+
chunks.push(buffer);
|
|
90
|
+
}
|
|
91
|
+
args = [args[0], args[1], Buffer.concat(chunks).toString("utf8"), ...args.slice(args[2] === "--input-file" ? 4 : 3)];
|
|
92
|
+
}
|
|
93
|
+
if (args.length === 2 && args[0] === "upload" && args[1].trim())
|
|
94
|
+
command = (client) => uploadLocalInput(client, args[1]);
|
|
95
|
+
else if (args.length === 1 && args[0] === "project")
|
|
96
|
+
command = (client) => client.project();
|
|
97
|
+
else if (args.length === 1 && args[0] === "usage")
|
|
98
|
+
command = (client) => client.usage();
|
|
99
|
+
else if (args[0] === "tools") {
|
|
100
|
+
const { values, tokens } = parseArgs({ args: args.slice(1), tokens: true, options: {
|
|
101
|
+
query: { type: "string" }, category: { type: "string" }, "available-only": { type: "boolean" }, summary: { type: "boolean" },
|
|
102
|
+
} });
|
|
103
|
+
const names = tokens.filter((token) => token.kind === "option").map((token) => token.name);
|
|
104
|
+
if (new Set(names).size !== names.length)
|
|
105
|
+
throw new Error("Duplicate discovery option");
|
|
106
|
+
const input = platformToolDiscoverySchema.parse({ query: values.query, category: values.category, availableOnly: values["available-only"], summary: values.summary });
|
|
107
|
+
command = (client) => client.discoverTools(input);
|
|
108
|
+
}
|
|
109
|
+
else if (args.length === 2 && args[0] === "tool" && /^[a-z][a-z0-9_]{1,63}$/.test(args[1]))
|
|
110
|
+
command = (client) => client.tool(args[1]);
|
|
111
|
+
else if (args[0] === "executions" && args.length % 2 === 1) {
|
|
112
|
+
const flags = {};
|
|
113
|
+
for (let index = 1; index < args.length; index += 2) {
|
|
114
|
+
const name = args[index].slice(2);
|
|
115
|
+
if (!["--limit", "--before", "--status", "--tool"].includes(args[index]) || Object.hasOwn(flags, name))
|
|
116
|
+
throw new Error("Invalid history flag");
|
|
117
|
+
flags[name] = name === "limit" ? Number(args[index + 1]) : args[index + 1];
|
|
118
|
+
}
|
|
119
|
+
const input = platformExecutionListQuerySchema.parse(flags);
|
|
120
|
+
command = (client) => client.executions(input);
|
|
121
|
+
}
|
|
122
|
+
else if (args[0] === "wait" && (args.length === 2 || args.length === 4 && args[2] === "--timeout") && /^[A-Za-z0-9_-]{1,128}$/.test(args[1])) {
|
|
123
|
+
const seconds = args.length === 4 ? Number(args[3]) : 480;
|
|
124
|
+
if (!Number.isInteger(seconds) || seconds < 1 || seconds > 900)
|
|
125
|
+
throw new Error("Invalid wait timeout");
|
|
126
|
+
command = async (client) => {
|
|
127
|
+
const result = await client.waitExecution(args[1], seconds * 1000);
|
|
128
|
+
exitCode = result.timedOut ? 3 : result.data?.status === "succeeded" ? 0 : 1;
|
|
129
|
+
return result;
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
else if (args.length === 2 && ["execution", "cancel"].includes(args[0]) && /^[A-Za-z0-9_-]{1,128}$/.test(args[1])) {
|
|
133
|
+
command = (client) => args[0] === "execution" ? client.execution(args[1]) : client.cancelExecution(args[1]);
|
|
134
|
+
}
|
|
135
|
+
else if (args.length === 5 && args[0] === "download" && args[3] === "--out" && args[4].trim() && /^[A-Za-z0-9_-]{1,128}$/.test(args[1])) {
|
|
136
|
+
const artifactId = platformArtifactIdSchema.parse(args[2]);
|
|
137
|
+
command = (client) => downloadArtifact(client, args[1], artifactId, args[4], transport);
|
|
138
|
+
}
|
|
139
|
+
else if (args.length === 3 && args[0] === "artifact-url" && /^[A-Za-z0-9_-]{1,128}$/.test(args[1])) {
|
|
140
|
+
const artifactId = platformArtifactIdSchema.parse(args[2]);
|
|
141
|
+
command = (client) => client.artifactDownload(args[1], artifactId);
|
|
142
|
+
}
|
|
143
|
+
else if (args.length === 3 && args[0] === "quote") {
|
|
144
|
+
const input = platformToolInputSchema.parse({ tool: args[1], arguments: JSON.parse(args[2]) });
|
|
145
|
+
command = (client) => client.quote(input);
|
|
146
|
+
}
|
|
147
|
+
else if (args.length === 9 && args[0] === "execute") {
|
|
148
|
+
const flags = new Map();
|
|
149
|
+
for (let index = 3; index < args.length; index += 2) {
|
|
150
|
+
if (!["--maximum-charge-micros", "--price-version", "--idempotency-key"].includes(args[index]) || flags.has(args[index]))
|
|
151
|
+
throw new Error("Invalid flag");
|
|
152
|
+
flags.set(args[index], args[index + 1]);
|
|
153
|
+
}
|
|
154
|
+
const maximum = flags.get("--maximum-charge-micros"), idempotencyKey = flags.get("--idempotency-key");
|
|
155
|
+
if (!maximum || !/^\d+$/.test(maximum) || !idempotencyKey || !/^[A-Za-z0-9._:-]{8,128}$/.test(idempotencyKey))
|
|
156
|
+
throw new Error("Invalid execution confirmation");
|
|
157
|
+
const input = platformCreateExecutionSchema.parse({ tool: args[1], arguments: JSON.parse(args[2]), maximumChargeMicros: Number(maximum), priceVersion: flags.get("--price-version") });
|
|
158
|
+
command = (client) => client.createExecution(input, idempotencyKey);
|
|
159
|
+
}
|
|
160
|
+
else
|
|
161
|
+
throw new Error("Invalid command");
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
error(`Invalid command, JSON arguments or input file.\n\n${commandHelp(args[0]) ?? help}`);
|
|
165
|
+
return 2;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const client = new PlatformClient(configFromEnvironment(env, "cli"), transport);
|
|
169
|
+
const result = await command(client);
|
|
170
|
+
output(JSON.stringify(result, null, 2));
|
|
171
|
+
return exitCode;
|
|
172
|
+
}
|
|
173
|
+
catch (failure) {
|
|
174
|
+
error(failure instanceof Error ? failure.message : "Platform request failed.");
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const inputHelp = `Supply one JSON object inline, use - for stdin, or use --input-file <path>.
|
|
2
|
+
File/stdin input is limited to 64 KiB. --input-file - also reads stdin.`;
|
|
3
|
+
const commands = {
|
|
4
|
+
upload: `upload <file>
|
|
5
|
+
Upload one explicitly chosen local JPG/PNG/WAV/MP3/MP4, at most 12 MiB, to this project.
|
|
6
|
+
Requires tools:execute. No generation charge. Returns a file ID valid for up to 30 days.
|
|
7
|
+
Use {"source":{"fileId":"..."}} with an understanding or transcription tool.
|
|
8
|
+
Repeating identical bytes reuses the same project file; it does not extend its expiry.
|
|
9
|
+
File bytes and local paths are not included in command output.`,
|
|
10
|
+
project: `project
|
|
11
|
+
Read the organization and project bound to the developer API key. Free.`,
|
|
12
|
+
usage: `usage
|
|
13
|
+
Read this project's monthly requests and spend. amountCents is USD cents. Free.`,
|
|
14
|
+
tools: `tools [--query <text>] [--category <research|seo|social|generation>]
|
|
15
|
+
[--available-only] [--summary]
|
|
16
|
+
Search names/descriptions, filter by category or configured provider, and show USD prices.
|
|
17
|
+
Use --summary to omit schemas/examples, then tool <name> for full details. Free.
|
|
18
|
+
available=true does not override executionEnabled=false or key/budget limits.
|
|
19
|
+
|
|
20
|
+
Example: noimos-platform tools --query search --available-only --summary`,
|
|
21
|
+
tool: `tool <name>
|
|
22
|
+
Inspect one tool's full input schema, example, availability and USD price. Free.
|
|
23
|
+
|
|
24
|
+
Example: noimos-platform tool web_search`,
|
|
25
|
+
executions: `executions [--limit <1-100>] [--before <id>] [--status <state>] [--tool <name>]
|
|
26
|
+
Read project history, newest first. Use nextBefore as --before for the next page.
|
|
27
|
+
States: queued, running, waiting, succeeded, failed, cancelled, needs_review. Free.
|
|
28
|
+
|
|
29
|
+
Example: noimos-platform executions --status failed --limit 20`,
|
|
30
|
+
quote: `quote <tool> '<arguments-json>'
|
|
31
|
+
Get a free quote without starting paid work. Returns priceVersion and maximumChargeMicros.
|
|
32
|
+
${inputHelp}
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
noimos-platform quote web_search '{"query":"content marketing trends"}'
|
|
36
|
+
noimos-platform quote web_search --input-file brief.json`,
|
|
37
|
+
execute: `execute <tool> '<arguments-json>' --maximum-charge-micros <integer>
|
|
38
|
+
--price-version <version> --idempotency-key <key>
|
|
39
|
+
Enqueue paid work using an accepted quote. All three confirmation flags are required.
|
|
40
|
+
1 USD = 1000000 micros. Retain the same body and key when retrying a submission.
|
|
41
|
+
Keys: 8-128 letters, digits, periods, underscores, colons or hyphens.
|
|
42
|
+
${inputHelp}
|
|
43
|
+
Returns an execution ID. Read it with execution <id> or wait <id>; acceptance is not completion.
|
|
44
|
+
|
|
45
|
+
Example (use the values from your quote and a saved key):
|
|
46
|
+
noimos-platform execute web_search --input-file brief.json \\
|
|
47
|
+
--maximum-charge-micros "$PLATFORM_QUOTED_MAXIMUM_MICROS" \\
|
|
48
|
+
--price-version "$PLATFORM_QUOTED_PRICE_VERSION" --idempotency-key "$PLATFORM_REQUEST_UUID"`,
|
|
49
|
+
execution: `execution <id>
|
|
50
|
+
Read an execution's status, output and chargedMicros (1 USD = 1000000 micros). Free.
|
|
51
|
+
A pending zero charge is not a free completion. For needs_review, contact support;
|
|
52
|
+
do not automatically submit a replacement.`,
|
|
53
|
+
wait: `wait <id> [--timeout <seconds>]
|
|
54
|
+
Wait for completion, polling every 5 seconds. Default timeout: 480 seconds; range: 1-900.
|
|
55
|
+
Exits 0 on success, 1 on failed/cancelled/needs_review or request error, 3 on timeout.
|
|
56
|
+
A timeout leaves accepted work running. Continue with the same ID. Free.`,
|
|
57
|
+
cancel: `cancel <id>
|
|
58
|
+
Request cancellation of unstarted work. Already incurred provider costs may be charged.
|
|
59
|
+
Requires execution ownership or project ADMIN access.`,
|
|
60
|
+
"artifact-url": `artifact-url <execution-id> <artifact-id>
|
|
61
|
+
Get a private output's temporary download URL, valid for up to 5 minutes.
|
|
62
|
+
Treat it as private access; never send the developer API key to the storage host.`,
|
|
63
|
+
download: `download <execution-id> <artifact-id> --out <file>
|
|
64
|
+
Save a private artifact after verifying size and SHA-256. Never overwrite an existing file.
|
|
65
|
+
The developer API key is sent only to the Platform API, not the storage host.`,
|
|
66
|
+
};
|
|
67
|
+
export function commandHelp(name) {
|
|
68
|
+
return Object.hasOwn(commands, name) ? `Usage: noimos-platform ${commands[name]}\n` : undefined;
|
|
69
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { link, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { platformArtifactSchema } from "../client/index.js";
|
|
5
|
+
function artifacts(output) {
|
|
6
|
+
if (!output)
|
|
7
|
+
return [];
|
|
8
|
+
const candidates = Array.isArray(output.artifacts) ? output.artifacts : [];
|
|
9
|
+
const found = [];
|
|
10
|
+
for (const candidate of candidates) {
|
|
11
|
+
const parsed = platformArtifactSchema.safeParse(candidate);
|
|
12
|
+
if (parsed.success)
|
|
13
|
+
found.push(parsed.data);
|
|
14
|
+
}
|
|
15
|
+
if (Array.isArray(output.results))
|
|
16
|
+
for (const result of output.results) {
|
|
17
|
+
if (result && typeof result === "object" && !Array.isArray(result) && result.data && typeof result.data === "object" && !Array.isArray(result.data)) {
|
|
18
|
+
found.push(...artifacts(result.data));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return found;
|
|
22
|
+
}
|
|
23
|
+
export async function downloadArtifact(client, id, artifactId, filename, transport = fetch) {
|
|
24
|
+
const execution = (await client.execution(id)).data;
|
|
25
|
+
const artifact = artifacts(execution.output).find((entry) => entry.id === artifactId);
|
|
26
|
+
if (!artifact)
|
|
27
|
+
throw new Error("Artifact is not present in this execution. Inspect execution output for available artifact IDs.");
|
|
28
|
+
const { data } = await client.artifactDownload(id, artifactId);
|
|
29
|
+
const url = new URL(data.url);
|
|
30
|
+
if (url.hostname !== "storage.googleapis.com" && !url.hostname.endsWith(".storage.googleapis.com"))
|
|
31
|
+
throw new Error("Unsupported artifact download host.");
|
|
32
|
+
const response = await transport(url, { method: "GET", redirect: "error", signal: AbortSignal.timeout(60000) }).catch(() => {
|
|
33
|
+
throw new Error("Artifact download failed. Retrieve a fresh URL and try again.");
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok || !response.body)
|
|
36
|
+
throw new Error("Artifact download failed. Retrieve a fresh URL and try again.");
|
|
37
|
+
const reader = response.body.getReader(), chunks = [];
|
|
38
|
+
let bytes = 0;
|
|
39
|
+
try {
|
|
40
|
+
for (;;) {
|
|
41
|
+
const part = await reader.read();
|
|
42
|
+
if (part.done)
|
|
43
|
+
break;
|
|
44
|
+
bytes += part.value.byteLength;
|
|
45
|
+
if (bytes > artifact.sizeBytes) {
|
|
46
|
+
await reader.cancel();
|
|
47
|
+
throw new Error("Artifact exceeded its recorded size.");
|
|
48
|
+
}
|
|
49
|
+
chunks.push(part.value);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw new Error("Artifact download failed. Retrieve a fresh URL and try again.");
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
reader.releaseLock();
|
|
57
|
+
}
|
|
58
|
+
const buffer = Buffer.concat(chunks);
|
|
59
|
+
if (bytes !== artifact.sizeBytes || createHash("sha256").update(buffer).digest("hex") !== artifact.sha256)
|
|
60
|
+
throw new Error("Artifact integrity check failed; no file was saved.");
|
|
61
|
+
const path = resolve(filename);
|
|
62
|
+
const temporary = await mkdtemp(join(dirname(path), ".noimos-platform-"));
|
|
63
|
+
try {
|
|
64
|
+
const staged = join(temporary, "artifact");
|
|
65
|
+
await writeFile(staged, buffer, { flag: "wx", mode: 0o600 });
|
|
66
|
+
await link(staged, path);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await rm(temporary, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
return { id, artifactId, path, sizeBytes: bytes, sha256: artifact.sha256 };
|
|
72
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { platformInputFileSchema, platformInputUploadSchema, PLATFORM_UPLOAD_MAX_BYTES } from "../schema/platformPublicExecution.js";
|
|
3
|
+
export { platformInputFileSchema, platformInputMimeSchema, PLATFORM_INPUT_MAX_BYTES } from "../schema/platformPublicExecution.js";
|
|
4
|
+
import { PLATFORM_EXECUTION_RESPONSE_MAX_BYTES, platformCatalogSchema, platformQuoteSchema, platformExecutionSchema, platformCreateExecutionSchema, platformToolInputSchema, platformToolNameSchema, platformArtifactIdSchema, platformArtifactDownloadSchema, platformExecutionListQuerySchema, platformExecutionListSchema } from "../schema/platformPublicExecution.js";
|
|
5
|
+
export { platformCreateExecutionSchema, platformToolInputSchema, platformExecutionSchema, platformArtifactIdSchema, platformArtifactSchema, platformExecutionListQuerySchema } from "../schema/platformPublicExecution.js";
|
|
6
|
+
export const projectResultSchema = z.object({ data: z.object({ developerOrganizationId: z.string(), developerProjectId: z.string(), name: z.string() }), requestId: z.string() });
|
|
7
|
+
export const usageResultSchema = z.object({ data: z.object({ month: z.string(), requests: z.number().int().nonnegative(), amountCents: z.number().nonnegative(), currency: z.literal("usd") }), requestId: z.string() });
|
|
8
|
+
export const executionResultSchema = z.object({ data: platformExecutionSchema });
|
|
9
|
+
const catalogResultSchema = z.object({ data: platformCatalogSchema });
|
|
10
|
+
export const platformToolDiscoverySchema = z.object({
|
|
11
|
+
query: z.string().trim().min(1).max(200).optional().describe("Case-insensitive substring of a tool's name or description."),
|
|
12
|
+
category: platformCatalogSchema.shape.tools.element.shape.category.optional(),
|
|
13
|
+
availableOnly: z.boolean().optional().describe("Only tools with a configured provider; check executionEnabled separately."),
|
|
14
|
+
summary: z.boolean().optional().describe("Omit input schemas and examples; inspect a selected tool for those details."),
|
|
15
|
+
}).strict();
|
|
16
|
+
const quoteResultSchema = z.object({ data: platformQuoteSchema });
|
|
17
|
+
const executionIdSchema = z.string().regex(/^[A-Za-z0-9_-]{1,128}$/);
|
|
18
|
+
const idempotencySchema = z.string().regex(/^[A-Za-z0-9._:-]{8,128}$/);
|
|
19
|
+
const publicErrors = {
|
|
20
|
+
platform_quote_changed: "The quote changed. Request a new quote before submitting another execution.",
|
|
21
|
+
platform_idempotency_key_used_for_another_request: "This idempotency key was already used for another request. Keep the original body to retrieve its result.",
|
|
22
|
+
platform_executions_disabled: "Paid executions are paused.",
|
|
23
|
+
platform_provider_not_configured: "This tool's provider is unavailable.",
|
|
24
|
+
platform_insufficient_balance_or_spending_limit_reached: "The prepaid balance or monthly spending limit is insufficient.",
|
|
25
|
+
platform_request_rate_exceeded: "The API key's request limit was reached. Wait before retrying.",
|
|
26
|
+
platform_api_key_scope_required: "The API key is missing the required scope.",
|
|
27
|
+
platform_artifact_unavailable: "This output file is unavailable or its retention period has ended.",
|
|
28
|
+
};
|
|
29
|
+
export class PlatformApiError extends Error {
|
|
30
|
+
status;
|
|
31
|
+
code;
|
|
32
|
+
constructor(status, code) {
|
|
33
|
+
super(`Platform API request failed (${status}). ${code && Object.hasOwn(publicErrors, code) ? publicErrors[code] : "Check the developer key, project access and limits in the console."}`);
|
|
34
|
+
this.status = status;
|
|
35
|
+
this.code = code;
|
|
36
|
+
this.name = "PlatformApiError";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function responseJson(response) {
|
|
40
|
+
const reader = response.body?.getReader();
|
|
41
|
+
if (!reader)
|
|
42
|
+
throw new Error("The Platform API returned an empty response.");
|
|
43
|
+
const chunks = [];
|
|
44
|
+
let size = 0;
|
|
45
|
+
try {
|
|
46
|
+
for (;;) {
|
|
47
|
+
const { value, done } = await reader.read();
|
|
48
|
+
if (done)
|
|
49
|
+
break;
|
|
50
|
+
size += value.byteLength;
|
|
51
|
+
if (size > PLATFORM_EXECUTION_RESPONSE_MAX_BYTES) {
|
|
52
|
+
await reader.cancel();
|
|
53
|
+
throw new Error("The Platform API response is too large.");
|
|
54
|
+
}
|
|
55
|
+
chunks.push(value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
reader.releaseLock();
|
|
60
|
+
}
|
|
61
|
+
const bytes = new Uint8Array(size);
|
|
62
|
+
let offset = 0;
|
|
63
|
+
for (const chunk of chunks) {
|
|
64
|
+
bytes.set(chunk, offset);
|
|
65
|
+
offset += chunk.length;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new Error("The Platform API returned an invalid response.");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function configFromEnvironment(env, source) {
|
|
75
|
+
if (!env.NOIMOS_PLATFORM_API_URL || !env.NOIMOS_PLATFORM_API_KEY)
|
|
76
|
+
throw new Error("Set NOIMOS_PLATFORM_API_URL and NOIMOS_PLATFORM_API_KEY from the developer console.");
|
|
77
|
+
return { apiUrl: env.NOIMOS_PLATFORM_API_URL, apiKey: env.NOIMOS_PLATFORM_API_KEY, source, local: env.NOIMOS_PLATFORM_ENV === "local" };
|
|
78
|
+
}
|
|
79
|
+
export class PlatformClient {
|
|
80
|
+
config;
|
|
81
|
+
transport;
|
|
82
|
+
baseUrl;
|
|
83
|
+
constructor(config, transport = fetch) {
|
|
84
|
+
this.config = config;
|
|
85
|
+
this.transport = transport;
|
|
86
|
+
const url = new URL(config.apiUrl);
|
|
87
|
+
if (url.username || url.password || url.search || url.hash || (url.protocol !== "https:" && !(config.local && url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname))))
|
|
88
|
+
throw new Error("The Platform API URL must use HTTPS. Explicit local mode permits loopback HTTP.");
|
|
89
|
+
if (!/^nmp_[A-Za-z0-9_-]{43}$/.test(config.apiKey))
|
|
90
|
+
throw new Error("A dedicated nmp_ developer API key is required.");
|
|
91
|
+
this.baseUrl = url.href.replace(/\/$/, "");
|
|
92
|
+
}
|
|
93
|
+
async request(path, schema, options) {
|
|
94
|
+
const body = options?.body === undefined ? undefined : JSON.stringify(options.body);
|
|
95
|
+
if (body && new TextEncoder().encode(body).byteLength > (path === "/tools/upload" ? PLATFORM_UPLOAD_MAX_BYTES : 65536))
|
|
96
|
+
throw new Error(path === "/tools/upload" ? "The upload exceeds 12 MiB." : "The tool request exceeds 64 KiB.");
|
|
97
|
+
let response;
|
|
98
|
+
try {
|
|
99
|
+
options?.signal?.throwIfAborted();
|
|
100
|
+
const timeout = AbortSignal.timeout(20000);
|
|
101
|
+
response = await this.transport(`${this.baseUrl}${path}`, { method: options?.method ?? "GET", redirect: "error", signal: options?.signal ? AbortSignal.any([timeout, options.signal]) : timeout,
|
|
102
|
+
headers: { authorization: `Bearer ${this.config.apiKey}`, "x-noimos-client": this.config.source, "idempotency-key": options?.idempotencyKey ?? crypto.randomUUID(),
|
|
103
|
+
...(body === undefined ? {} : { "content-type": "application/json" }) }, ...(body === undefined ? {} : { body }) });
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
options?.signal?.throwIfAborted();
|
|
107
|
+
throw new Error("The Platform API could not be reached. If submitting an execution, retry with the same body and idempotency key.");
|
|
108
|
+
}
|
|
109
|
+
const payload = await responseJson(response).catch(() => {
|
|
110
|
+
options?.signal?.throwIfAborted();
|
|
111
|
+
if (!response.ok)
|
|
112
|
+
throw new PlatformApiError(response.status, null);
|
|
113
|
+
throw new Error("The Platform API returned an invalid response. If submitting an execution, retain the same body and idempotency key.");
|
|
114
|
+
});
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
const parsed = z.object({ error: z.object({ message: z.string() }) }).safeParse(payload);
|
|
117
|
+
const code = parsed.success && Object.hasOwn(publicErrors, parsed.data.error.message) ? parsed.data.error.message : null;
|
|
118
|
+
throw new PlatformApiError(response.status, code);
|
|
119
|
+
}
|
|
120
|
+
const parsed = schema.safeParse(payload);
|
|
121
|
+
if (!parsed.success)
|
|
122
|
+
throw new Error("The Platform API returned an unexpected response. If submitting an execution, retain the same body and idempotency key.");
|
|
123
|
+
return parsed.data;
|
|
124
|
+
}
|
|
125
|
+
project() { return this.request("/tools/project", projectResultSchema); }
|
|
126
|
+
usage() { return this.request("/tools/usage", usageResultSchema); }
|
|
127
|
+
tools() { return this.request("/tools", catalogResultSchema); }
|
|
128
|
+
uploadInput(input) {
|
|
129
|
+
return this.request("/tools/upload", z.object({ data: platformInputFileSchema }), { method: "POST", body: platformInputUploadSchema.parse(input) });
|
|
130
|
+
}
|
|
131
|
+
async discoverTools(options = {}) {
|
|
132
|
+
const { query, category, availableOnly, summary } = platformToolDiscoverySchema.parse(options);
|
|
133
|
+
const { data } = await this.tools();
|
|
134
|
+
const search = query?.toLowerCase();
|
|
135
|
+
const tools = data.tools.filter((tool) => (!search || tool.name.toLowerCase().includes(search) || tool.description.toLowerCase().includes(search)) &&
|
|
136
|
+
(!category || tool.category === category) && (!availableOnly || tool.available));
|
|
137
|
+
return { data: { ...data, tools: summary ? tools.map(({ inputSchema, example, ...tool }) => tool) : tools } };
|
|
138
|
+
}
|
|
139
|
+
async tool(name) {
|
|
140
|
+
platformToolNameSchema.parse(name);
|
|
141
|
+
const { data } = await this.tools();
|
|
142
|
+
const tool = data.tools.find((entry) => entry.name === name);
|
|
143
|
+
if (!tool)
|
|
144
|
+
throw new Error("Tool is unavailable in the standalone Platform catalog. Run tools to discover supported operations.");
|
|
145
|
+
return { data: { ...tool, priceVersion: data.priceVersion, currency: data.currency, executionEnabled: data.executionEnabled } };
|
|
146
|
+
}
|
|
147
|
+
executions(input = {}) {
|
|
148
|
+
const parsed = platformExecutionListQuerySchema.parse(input);
|
|
149
|
+
const query = new URLSearchParams({ limit: String(parsed.limit) });
|
|
150
|
+
if (parsed.before)
|
|
151
|
+
query.set("before", parsed.before);
|
|
152
|
+
if (parsed.status)
|
|
153
|
+
query.set("status", parsed.status);
|
|
154
|
+
if (parsed.tool)
|
|
155
|
+
query.set("tool", parsed.tool);
|
|
156
|
+
return this.request(`/tools/executions?${query}`, z.object({ data: platformExecutionListSchema }));
|
|
157
|
+
}
|
|
158
|
+
quote(input) {
|
|
159
|
+
const parsed = platformToolInputSchema.safeParse(input);
|
|
160
|
+
if (!parsed.success)
|
|
161
|
+
throw new Error("Provide a tool name and a JSON object of arguments.");
|
|
162
|
+
return this.request("/tools/quote", quoteResultSchema, { method: "POST", body: parsed.data });
|
|
163
|
+
}
|
|
164
|
+
createExecution(input, idempotencyKey) {
|
|
165
|
+
const parsed = platformCreateExecutionSchema.safeParse(input);
|
|
166
|
+
if (!parsed.success)
|
|
167
|
+
throw new Error("Provide a tool, arguments, the quote's priceVersion and maximumChargeMicros.");
|
|
168
|
+
if (!idempotencySchema.safeParse(idempotencyKey).success)
|
|
169
|
+
throw new Error("Keep an idempotency key of 8–128 letters, digits, periods, underscores, colons or hyphens for this execution.");
|
|
170
|
+
return this.request(`/tools/${parsed.data.tool}`, executionResultSchema, { method: "POST", body: parsed.data, idempotencyKey });
|
|
171
|
+
}
|
|
172
|
+
execution(id, signal) {
|
|
173
|
+
if (!executionIdSchema.safeParse(id).success)
|
|
174
|
+
throw new Error("Provide a valid execution ID.");
|
|
175
|
+
return this.request(`/tools/executions/${id}`, executionResultSchema, { signal });
|
|
176
|
+
}
|
|
177
|
+
async waitExecution(id, timeoutMs = 20000, signal) {
|
|
178
|
+
executionIdSchema.parse(id);
|
|
179
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1000 || timeoutMs > 900000)
|
|
180
|
+
throw new Error("Wait must be between 1 and 900 seconds.");
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
const timeout = new Error("Execution wait timed out; use the same execution ID to continue reading.");
|
|
183
|
+
const timer = setTimeout(() => controller.abort(timeout), timeoutMs);
|
|
184
|
+
const abort = () => controller.abort(signal?.reason);
|
|
185
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
186
|
+
if (signal?.aborted)
|
|
187
|
+
abort();
|
|
188
|
+
let latest = null;
|
|
189
|
+
try {
|
|
190
|
+
for (;;) {
|
|
191
|
+
controller.signal.throwIfAborted();
|
|
192
|
+
latest = (await this.execution(id, controller.signal)).data;
|
|
193
|
+
if (!["queued", "running", "waiting"].includes(latest.status))
|
|
194
|
+
return { data: latest, id, timedOut: false };
|
|
195
|
+
await new Promise((resolve, reject) => {
|
|
196
|
+
const done = () => { controller.signal.removeEventListener("abort", cancelled); resolve(); };
|
|
197
|
+
const pause = setTimeout(done, 5000);
|
|
198
|
+
const cancelled = () => { clearTimeout(pause); controller.signal.removeEventListener("abort", cancelled); reject(controller.signal.reason); };
|
|
199
|
+
controller.signal.addEventListener("abort", cancelled, { once: true });
|
|
200
|
+
if (controller.signal.aborted)
|
|
201
|
+
cancelled();
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (error !== timeout)
|
|
207
|
+
throw error;
|
|
208
|
+
return { data: latest, id, timedOut: true };
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
clearTimeout(timer);
|
|
212
|
+
signal?.removeEventListener("abort", abort);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
cancelExecution(id) {
|
|
216
|
+
if (!executionIdSchema.safeParse(id).success)
|
|
217
|
+
throw new Error("Provide a valid execution ID.");
|
|
218
|
+
return this.request(`/tools/executions/${id}/cancel`, executionResultSchema, { method: "POST" });
|
|
219
|
+
}
|
|
220
|
+
artifactDownload(id, artifactId) {
|
|
221
|
+
if (!executionIdSchema.safeParse(id).success || !platformArtifactIdSchema.safeParse(artifactId).success)
|
|
222
|
+
throw new Error("Provide valid execution and artifact IDs.");
|
|
223
|
+
return this.request(`/tools/executions/${id}/artifacts/${artifactId}/download`, z.object({ data: platformArtifactDownloadSchema }));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { open } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { extname } from "node:path";
|
|
4
|
+
import { platformInputMimeSchema, PLATFORM_INPUT_MAX_BYTES } from "./index.js";
|
|
5
|
+
const extensions = { ".mp3": "audio/mpeg", ".wav": "audio/wav", ".mp4": "video/mp4", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png" };
|
|
6
|
+
export async function uploadLocalInput(client, path) {
|
|
7
|
+
const mimeType = platformInputMimeSchema.safeParse(extensions[extname(path).toLowerCase()]);
|
|
8
|
+
if (!mimeType.success)
|
|
9
|
+
throw new Error("Supported input files: JPG, PNG, WAV, MP3, MP4.");
|
|
10
|
+
let data;
|
|
11
|
+
try {
|
|
12
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
13
|
+
try {
|
|
14
|
+
const stat = await file.stat();
|
|
15
|
+
if (!stat.isFile() || stat.size < 1 || stat.size > PLATFORM_INPUT_MAX_BYTES)
|
|
16
|
+
throw new Error("Invalid file");
|
|
17
|
+
const buffer = Buffer.alloc(stat.size + 1);
|
|
18
|
+
let offset = 0;
|
|
19
|
+
while (offset < buffer.length) {
|
|
20
|
+
const { bytesRead } = await file.read(buffer, offset, buffer.length - offset, null);
|
|
21
|
+
if (!bytesRead)
|
|
22
|
+
break;
|
|
23
|
+
offset += bytesRead;
|
|
24
|
+
}
|
|
25
|
+
if (offset !== stat.size)
|
|
26
|
+
throw new Error("File changed");
|
|
27
|
+
data = buffer.subarray(0, offset).toString("base64");
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
await file.close();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new Error("Input must be an existing regular file, at most 12 MiB. Symlinks are not accepted.");
|
|
35
|
+
}
|
|
36
|
+
return client.uploadInput({ mimeType: mimeType.data, data });
|
|
37
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const PLATFORM_EXECUTION_MAX_STEPS = 20;
|
|
3
|
+
export const PLATFORM_STEP_OUTPUT_MAX_BYTES = 1_048_576;
|
|
4
|
+
export const PLATFORM_EXECUTION_RESPONSE_MAX_BYTES = PLATFORM_EXECUTION_MAX_STEPS * PLATFORM_STEP_OUTPUT_MAX_BYTES + 65_536;
|
|
5
|
+
export const platformJsonObjectSchema = z.record(z.string(), z.json());
|
|
6
|
+
export const PLATFORM_INPUT_MAX_BYTES = 12 * 1024 * 1024;
|
|
7
|
+
export const PLATFORM_UPLOAD_MAX_BYTES = 16 * 1024 * 1024 + 4096;
|
|
8
|
+
export const platformInputFileIdSchema = z.string().regex(/^f_[a-f0-9]{64}$/);
|
|
9
|
+
export const platformInputMimeSchema = z.enum(["image/jpeg", "image/png", "audio/wav", "audio/mpeg", "video/mp4"]);
|
|
10
|
+
export const platformInputUploadSchema = z.object({ mimeType: platformInputMimeSchema, data: z.string().min(4).max(16 * 1024 * 1024) }).strict();
|
|
11
|
+
export const platformInputFileSchema = z.object({ id: platformInputFileIdSchema, mimeType: platformInputMimeSchema,
|
|
12
|
+
sizeBytes: z.number().int().positive().max(PLATFORM_INPUT_MAX_BYTES), sha256: z.string().regex(/^[a-f0-9]{64}$/), expiresAt: z.iso.datetime() });
|
|
13
|
+
export const platformMicrosSchema = z.number().int().min(0).max(1_000_000_000_000);
|
|
14
|
+
export const platformToolNameSchema = z.string().regex(/^[a-z][a-z0-9_]{1,63}$/);
|
|
15
|
+
export const platformExecutionStatusSchema = z.enum(["queued", "running", "waiting", "succeeded", "failed", "cancelled", "needs_review"]);
|
|
16
|
+
const workspaceArgumentNames = new Set(["workspaceid", "workspaceids", "teamid", "provideraccountid", "provideraccountids", "datasetid", "datasetids", "knowledgebaseid", "sessionid", "skillid"]);
|
|
17
|
+
const standaloneArgumentsSchema = platformJsonObjectSchema.superRefine((value, context) => {
|
|
18
|
+
for (const key of Object.keys(value))
|
|
19
|
+
if (workspaceArgumentNames.has(key.replaceAll("_", "").toLowerCase())) {
|
|
20
|
+
context.addIssue({ code: "custom", path: [key], message: "Workspace-owned data is unavailable on the standalone Platform." });
|
|
21
|
+
}
|
|
22
|
+
}).describe("Self-contained tool arguments: prompts, public URLs or public account identifiers. Web workspace identifiers are not supported.");
|
|
23
|
+
export const platformToolInputSchema = z.object({ tool: platformToolNameSchema.describe("Tool name from the standalone Platform catalog."), arguments: standaloneArgumentsSchema }).strict();
|
|
24
|
+
export const platformCreateExecutionSchema = platformToolInputSchema.extend({
|
|
25
|
+
maximumChargeMicros: platformMicrosSchema,
|
|
26
|
+
priceVersion: z.string().min(1).max(100),
|
|
27
|
+
}).strict();
|
|
28
|
+
export const platformQuoteSchema = z.object({
|
|
29
|
+
tool: platformToolNameSchema, priceVersion: z.string(), currency: z.literal("usd"),
|
|
30
|
+
minimumChargeMicros: platformMicrosSchema, maximumChargeMicros: platformMicrosSchema,
|
|
31
|
+
expiresAt: z.string(),
|
|
32
|
+
});
|
|
33
|
+
export const platformExecutionSchema = z.object({
|
|
34
|
+
id: z.string(), tool: platformToolNameSchema, status: platformExecutionStatusSchema, cancelRequested: z.boolean(),
|
|
35
|
+
priceVersion: z.string(), currency: z.literal("usd"), reservedMicros: platformMicrosSchema, chargedMicros: platformMicrosSchema,
|
|
36
|
+
output: platformJsonObjectSchema.nullable(), errorCode: z.string().nullable(),
|
|
37
|
+
createdAt: z.string(), updatedAt: z.string(), completedAt: z.string().nullable(),
|
|
38
|
+
});
|
|
39
|
+
export const platformExecutionListQuerySchema = z.object({
|
|
40
|
+
limit: z.number().int().min(1).max(100).default(20).describe("Page size, 1–100."),
|
|
41
|
+
before: z.string().regex(/^[A-Za-z0-9_-]{1,128}$/).optional().describe("nextBefore from the previous page."),
|
|
42
|
+
status: platformExecutionStatusSchema.optional().describe("Only executions with this status."),
|
|
43
|
+
tool: platformToolNameSchema.optional().describe("Only executions of this catalog tool."),
|
|
44
|
+
}).strict();
|
|
45
|
+
export const platformExecutionSummarySchema = platformExecutionSchema.omit({ output: true });
|
|
46
|
+
export const platformExecutionListSchema = z.object({
|
|
47
|
+
executions: z.array(platformExecutionSummarySchema).max(100), nextBefore: z.string().nullable(),
|
|
48
|
+
});
|
|
49
|
+
export const platformArtifactIdSchema = z.string().regex(/^a_[A-Za-z0-9_-]{1,100}$/);
|
|
50
|
+
export const platformArtifactSchema = z.object({
|
|
51
|
+
id: platformArtifactIdSchema, mimeType: z.enum(["image/jpeg", "image/png", "audio/wav", "audio/mpeg", "video/mp4"]),
|
|
52
|
+
sizeBytes: z.number().int().positive().max(64 * 1024 * 1024), sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
53
|
+
generation: z.string().regex(/^\d{1,30}$/), expiresAt: z.iso.datetime(),
|
|
54
|
+
});
|
|
55
|
+
export const platformArtifactDownloadSchema = z.object({ url: z.url().refine((value) => { const url = new URL(value); return url.protocol === "https:" && !url.username && !url.password; }), expiresAt: z.iso.datetime() });
|
|
56
|
+
export const platformCatalogSchema = z.object({
|
|
57
|
+
priceVersion: z.string(), currency: z.literal("usd"), executionEnabled: z.boolean(),
|
|
58
|
+
tools: z.array(z.object({
|
|
59
|
+
name: platformToolNameSchema, description: z.string(), category: z.enum(["research", "seo", "social", "generation"]),
|
|
60
|
+
available: z.boolean(), inputSchema: platformJsonObjectSchema, example: platformJsonObjectSchema,
|
|
61
|
+
models: z.array(z.object({ id: z.string(), available: z.boolean(), example: platformJsonObjectSchema, priceDescription: z.string() })).optional(),
|
|
62
|
+
price: z.object({ minimumChargeMicros: platformMicrosSchema, unit: z.string(), unitPriceMicros: platformMicrosSchema.nullable(), description: z.string() }),
|
|
63
|
+
})),
|
|
64
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@noimosai/platform-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "NoimosAI Developer Platform CLI for research, SEO, social data and media generation.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"noimos-platform": "./bin/noimos-platform.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"dist",
|
|
12
|
+
"USAGE.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"registry": "https://registry.npmjs.org/",
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://platform.noimosai.com",
|
|
23
|
+
"author": "AGOS Labs",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"keywords": [
|
|
26
|
+
"noimosai",
|
|
27
|
+
"cli",
|
|
28
|
+
"mcp",
|
|
29
|
+
"marketing",
|
|
30
|
+
"seo",
|
|
31
|
+
"ai-agents"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"zod": "^4.5.4"
|
|
35
|
+
}
|
|
36
|
+
}
|