clyops-api 0.2.0 → 0.3.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/README.md +6 -1
- package/dist/cli.js +2 -0
- package/dist/server.d.ts +4 -2
- package/dist/server.js +3 -3
- package/package.json +11 -12
- package/src/cli.ts +2 -0
- package/src/server.ts +7 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dan VerWeire
|
|
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/README.md
CHANGED
|
@@ -7,7 +7,7 @@ becomes a `POST` endpoint whose JSON body is validated against the tool's own
|
|
|
7
7
|
return a job to poll.
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
|
|
10
|
+
pnpm add --global clyops-api
|
|
11
11
|
clyops-api --root ~/mytool/scripts # http://127.0.0.1:8080
|
|
12
12
|
```
|
|
13
13
|
|
|
@@ -161,3 +161,8 @@ Watching is off by default in the library (`watch: true` turns it on, `onReload`
|
|
|
161
161
|
new set) and on by default in the `clyops-api` command.
|
|
162
162
|
|
|
163
163
|
`loadTools(root)` and `runTool(tool, input)` are exported for other servers.
|
|
164
|
+
|
|
165
|
+
`--positionals-order first|last` controls generated argument order (default:
|
|
166
|
+
`first`). An option terminator is only emitted for positional values starting
|
|
167
|
+
with `-`, which are always placed after the options. The Node API exposes this
|
|
168
|
+
as `positionalsOrder`.
|
package/dist/cli.js
CHANGED
|
@@ -26,6 +26,7 @@ cli.optArray('CLYOPS_API_PATHS_WITHIN', 'paths-within', '', 'Path inputs must re
|
|
|
26
26
|
cli.opt('CLYOPS_API_MAX_BODY', 'max-body', '', '10485760', 'Largest request body in bytes: JSON, multipart or spooled for an async job', 'Security', 'int:1-');
|
|
27
27
|
cli.opt('CLYOPS_API_MAX_OUTPUT', 'max-output', '', '16777216', 'Keep at most this many bytes of a tool\'s stdout and stderr (0: all)', 'Security', 'int:0-');
|
|
28
28
|
cli.opt('CLYOPS_API_AUDIT', 'audit', '', 'optional', 'Append a JSON line per run to this file (-: stderr)', 'Security', 'path');
|
|
29
|
+
cli.opt('CLYOPS_API_POSITIONALS_ORDER', 'positionals-order', '', 'first', 'Positionals before or after options', 'Tools', 'choice:first,last');
|
|
29
30
|
const args = cli.run();
|
|
30
31
|
let keys;
|
|
31
32
|
if (args.CLYOPS_API_KEYS) {
|
|
@@ -41,6 +42,7 @@ const { app, current } = await createApi({
|
|
|
41
42
|
root: args.CLYOPS_API_ROOT,
|
|
42
43
|
name: args.CLYOPS_API_NAME ?? undefined,
|
|
43
44
|
cwd: args.CLYOPS_API_CWD ?? undefined,
|
|
45
|
+
positionalsOrder: args.CLYOPS_API_POSITIONALS_ORDER,
|
|
44
46
|
timeoutMs: args.CLYOPS_API_TIMEOUT * 1000,
|
|
45
47
|
concurrency: args.CLYOPS_API_CONCURRENCY ?? undefined,
|
|
46
48
|
apiKey: args.CLYOPS_API_API_KEY ?? undefined,
|
package/dist/server.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AuditEntry, type Group, type Tool, type ToolFilter, type ToolResult } from 'clyops-tools';
|
|
2
2
|
import { JobQueue } from 'clyops-jobs';
|
|
3
|
-
import { type NextFunction, type Request, type Response } from 'express';
|
|
3
|
+
import express, { type NextFunction, type Request, type Response } from 'express';
|
|
4
4
|
/** A named API key and what it may run: globs over tool words (see clyops-tools' ToolFilter). */
|
|
5
5
|
export interface ApiKey {
|
|
6
6
|
key: string;
|
|
@@ -8,6 +8,8 @@ export interface ApiKey {
|
|
|
8
8
|
deny?: string[];
|
|
9
9
|
}
|
|
10
10
|
export interface ApiOptions {
|
|
11
|
+
/** Positionals before or after options (default: first). */
|
|
12
|
+
positionalsOrder?: 'first' | 'last';
|
|
11
13
|
/** Tools directory or dispatcher definition file. */
|
|
12
14
|
root: string;
|
|
13
15
|
/** API title (default: the root's name). */
|
|
@@ -57,7 +59,7 @@ type ApiResult = ToolResult & {
|
|
|
57
59
|
* are fixed when the app is built.
|
|
58
60
|
*/
|
|
59
61
|
export declare function createApi(opts: ApiOptions): Promise<{
|
|
60
|
-
app:
|
|
62
|
+
app: express.Express;
|
|
61
63
|
queue: JobQueue<ApiResult>;
|
|
62
64
|
/** The tree and tools being served now. */
|
|
63
65
|
current: () => {
|
package/dist/server.js
CHANGED
|
@@ -236,7 +236,7 @@ export async function createApi(opts) {
|
|
|
236
236
|
instructions: api.tree.description || undefined,
|
|
237
237
|
tools: caller?.filter ? api.tools.filter((t) => allowed(t, caller.filter)) : api.tools,
|
|
238
238
|
cwd: opts.cwd,
|
|
239
|
-
timeoutMs: opts.timeoutMs,
|
|
239
|
+
timeoutMs: opts.timeoutMs, positionalsOrder: opts.positionalsOrder,
|
|
240
240
|
within: opts.within,
|
|
241
241
|
maxOutput: opts.maxOutput,
|
|
242
242
|
audit: opts.audit && ((entry) => opts.audit?.({ ...entry, key: caller?.name })),
|
|
@@ -293,7 +293,7 @@ async function runBody(opts, ctx, tool, req, res) {
|
|
|
293
293
|
*/
|
|
294
294
|
async function respond(opts, ctx, tool, input, req, res, stdin, dir) {
|
|
295
295
|
const caller = res.locals.caller;
|
|
296
|
-
const base = { cwd: opts.cwd, timeoutMs: opts.timeoutMs, within: opts.within, maxOutput: opts.maxOutput };
|
|
296
|
+
const base = { cwd: opts.cwd, timeoutMs: opts.timeoutMs, positionalsOrder: opts.positionalsOrder, within: opts.within, maxOutput: opts.maxOutput };
|
|
297
297
|
const open = () => (typeof stdin === 'string' ? createReadStream(stdin) : stdin);
|
|
298
298
|
const audit = (result) => opts.audit?.({
|
|
299
299
|
key: caller?.name, tool: tool.words.join(' '), command: result.command, exitCode: result.exitCode, signal: result.signal,
|
|
@@ -307,7 +307,7 @@ async function respond(opts, ctx, tool, input, req, res, stdin, dir) {
|
|
|
307
307
|
if (toFile) {
|
|
308
308
|
const started = startTool(tool, input, { ...base, signal, stdin: open(), stdout: 'stream' });
|
|
309
309
|
const [r] = await Promise.all([started.result, pipeline(started.stdout, createWriteStream(join(jobDir, 'stdout')))]);
|
|
310
|
-
result = { ...r, ok: r.exitCode === 0, stdoutUrl: `/jobs/${job.record.job_id}/stdout` };
|
|
310
|
+
result = { ...r, ok: r.exitCode === 0 && !r.timedOut && !r.signal, stdoutUrl: `/jobs/${job.record.job_id}/stdout` };
|
|
311
311
|
}
|
|
312
312
|
else {
|
|
313
313
|
result = envelope(await runTool(tool, input, { ...base, signal, stdin: open(), stdout: binaryStdout(tool) ? 'buffer' : 'text' }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clyops-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Serve a directory of clyops tools as an HTTP API: an endpoint per tool, validated and documented in OpenAPI from its schema, with sync and async (job) runs.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,18 +35,12 @@
|
|
|
35
35
|
"dist",
|
|
36
36
|
"src"
|
|
37
37
|
],
|
|
38
|
-
"scripts": {
|
|
39
|
-
"build": "tsc -p tsconfig.json && chmod +x dist/cli.js",
|
|
40
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
41
|
-
"test": "node --test test/*.test.mjs",
|
|
42
|
-
"prepublishOnly": "npm run build && npm test"
|
|
43
|
-
},
|
|
44
38
|
"dependencies": {
|
|
45
39
|
"busboy": "^1.6.0",
|
|
46
|
-
"clyops": "0.
|
|
47
|
-
"clyops-jobs": "0.
|
|
48
|
-
"clyops-mcp": "0.
|
|
49
|
-
"clyops-tools": "0.
|
|
40
|
+
"clyops": "0.3.0",
|
|
41
|
+
"clyops-jobs": "0.3.0",
|
|
42
|
+
"clyops-mcp": "0.3.0",
|
|
43
|
+
"clyops-tools": "0.3.0",
|
|
50
44
|
"express": "^5.2.1",
|
|
51
45
|
"plus-express": "^2.1.1"
|
|
52
46
|
},
|
|
@@ -56,5 +50,10 @@
|
|
|
56
50
|
"@types/express": "^5.0.0",
|
|
57
51
|
"@types/node": "^22.0.0",
|
|
58
52
|
"typescript": "^5.6.0"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsc -p tsconfig.json && chmod +x dist/cli.js",
|
|
56
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
57
|
+
"test": "node --test test/*.test.mjs"
|
|
59
58
|
}
|
|
60
|
-
}
|
|
59
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -28,6 +28,7 @@ cli.optArray('CLYOPS_API_PATHS_WITHIN', 'paths-within', '', 'Path
|
|
|
28
28
|
cli.opt('CLYOPS_API_MAX_BODY', 'max-body', '', '10485760', 'Largest request body in bytes: JSON, multipart or spooled for an async job', 'Security', 'int:1-');
|
|
29
29
|
cli.opt('CLYOPS_API_MAX_OUTPUT', 'max-output', '', '16777216', 'Keep at most this many bytes of a tool\'s stdout and stderr (0: all)', 'Security', 'int:0-');
|
|
30
30
|
cli.opt('CLYOPS_API_AUDIT', 'audit', '', 'optional', 'Append a JSON line per run to this file (-: stderr)', 'Security', 'path');
|
|
31
|
+
cli.opt('CLYOPS_API_POSITIONALS_ORDER', 'positionals-order', '', 'first', 'Positionals before or after options', 'Tools', 'choice:first,last');
|
|
31
32
|
const args = cli.run();
|
|
32
33
|
|
|
33
34
|
let keys: Record<string, ApiKey> | undefined;
|
|
@@ -44,6 +45,7 @@ const { app, current } = await createApi({
|
|
|
44
45
|
root: args.CLYOPS_API_ROOT as string,
|
|
45
46
|
name: (args.CLYOPS_API_NAME as string | null) ?? undefined,
|
|
46
47
|
cwd: (args.CLYOPS_API_CWD as string | null) ?? undefined,
|
|
48
|
+
positionalsOrder: args.CLYOPS_API_POSITIONALS_ORDER as 'first' | 'last',
|
|
47
49
|
timeoutMs: (args.CLYOPS_API_TIMEOUT as number) * 1000,
|
|
48
50
|
concurrency: (args.CLYOPS_API_CONCURRENCY as number | null) ?? undefined,
|
|
49
51
|
apiKey: (args.CLYOPS_API_API_KEY as string | null) ?? undefined,
|
package/src/server.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import { JobQueue, type JobRecord } from 'clyops-jobs';
|
|
8
8
|
import { mcpHttpHandler } from 'clyops-mcp';
|
|
9
9
|
import busboy from 'busboy';
|
|
10
|
-
import express, { type NextFunction, type Request, type Response } from 'express';
|
|
10
|
+
import express, { type Express, type NextFunction, type Request, type Response } from 'express';
|
|
11
11
|
import { createReadStream, createWriteStream, existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
12
12
|
import { tmpdir } from 'node:os';
|
|
13
13
|
import { basename, join } from 'node:path';
|
|
@@ -24,6 +24,8 @@ export interface ApiKey {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export interface ApiOptions {
|
|
27
|
+
/** Positionals before or after options (default: first). */
|
|
28
|
+
positionalsOrder?: 'first' | 'last';
|
|
27
29
|
/** Tools directory or dispatcher definition file. */
|
|
28
30
|
root: string;
|
|
29
31
|
/** API title (default: the root's name). */
|
|
@@ -242,7 +244,7 @@ export async function createApi(opts: ApiOptions) {
|
|
|
242
244
|
: undefined;
|
|
243
245
|
swap(watcher ? watcher.current() : await loadTools(opts.root, { name: opts.name, filter: opts.filter, onError }));
|
|
244
246
|
|
|
245
|
-
const app = express();
|
|
247
|
+
const app: Express = express();
|
|
246
248
|
app.use(express.json({ limit: maxBody }));
|
|
247
249
|
const keys = Object.entries(opts.keys ?? {});
|
|
248
250
|
if (opts.apiKey || keys.length) {
|
|
@@ -285,7 +287,7 @@ export async function createApi(opts: ApiOptions) {
|
|
|
285
287
|
instructions: api.tree.description || undefined,
|
|
286
288
|
tools: caller?.filter ? api.tools.filter((t) => allowed(t, caller.filter as ToolFilter)) : api.tools,
|
|
287
289
|
cwd: opts.cwd,
|
|
288
|
-
timeoutMs: opts.timeoutMs,
|
|
290
|
+
timeoutMs: opts.timeoutMs, positionalsOrder: opts.positionalsOrder,
|
|
289
291
|
within: opts.within,
|
|
290
292
|
maxOutput: opts.maxOutput,
|
|
291
293
|
audit: opts.audit && ((entry) => opts.audit?.({ ...entry, key: caller?.name })),
|
|
@@ -344,7 +346,7 @@ async function runBody(opts: ApiOptions, ctx: Ctx, tool: ApiTool, req: Request,
|
|
|
344
346
|
async function respond(opts: ApiOptions, ctx: Ctx, tool: ApiTool, input: Record<string, unknown>, req: Request, res: Response,
|
|
345
347
|
stdin?: string | Readable, dir?: string): Promise<void> {
|
|
346
348
|
const caller = res.locals.caller as Caller | undefined;
|
|
347
|
-
const base: ToolRunOptions = { cwd: opts.cwd, timeoutMs: opts.timeoutMs, within: opts.within, maxOutput: opts.maxOutput };
|
|
349
|
+
const base: ToolRunOptions = { cwd: opts.cwd, timeoutMs: opts.timeoutMs, positionalsOrder: opts.positionalsOrder, within: opts.within, maxOutput: opts.maxOutput };
|
|
348
350
|
const open = () => (typeof stdin === 'string' ? createReadStream(stdin) : stdin);
|
|
349
351
|
const audit = (result: RunResult) => opts.audit?.({
|
|
350
352
|
key: caller?.name, tool: tool.words.join(' '), command: result.command, exitCode: result.exitCode, signal: result.signal,
|
|
@@ -359,7 +361,7 @@ async function respond(opts: ApiOptions, ctx: Ctx, tool: ApiTool, input: Record<
|
|
|
359
361
|
if (toFile) {
|
|
360
362
|
const started = startTool(tool, input, { ...base, signal, stdin: open(), stdout: 'stream' });
|
|
361
363
|
const [r] = await Promise.all([started.result, pipeline(started.stdout, createWriteStream(join(jobDir, 'stdout')))]);
|
|
362
|
-
result = { ...r, ok: r.exitCode === 0, stdoutUrl: `/jobs/${job.record.job_id}/stdout` };
|
|
364
|
+
result = { ...r, ok: r.exitCode === 0 && !r.timedOut && !r.signal, stdoutUrl: `/jobs/${job.record.job_id}/stdout` };
|
|
363
365
|
} else {
|
|
364
366
|
result = envelope(await runTool(tool, input, { ...base, signal, stdin: open(), stdout: binaryStdout(tool) ? 'buffer' : 'text' }));
|
|
365
367
|
}
|