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 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
- npm install -g clyops-api
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: import("express-serve-static-core").Express;
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.2.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.2.0",
47
- "clyops-jobs": "0.2.0",
48
- "clyops-mcp": "0.2.0",
49
- "clyops-tools": "0.2.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
  }