@spatius/cli 0.1.0-beta.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 Spatialwalk
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 ADDED
@@ -0,0 +1,67 @@
1
+ # Spatius CLI
2
+
3
+ Create avatars and render videos from a coding agent or terminal. The CLI handles
4
+ Studio login, app credentials, temporary uploads, polling, and MP4 downloads.
5
+
6
+ Requires Node.js 22+, a Spatius Studio account, and separately approved Avatar
7
+ and Video API access. Avatar creation uses your existing Avatar Creations balance.
8
+ The CLI is experimental.
9
+
10
+ ## Get started
11
+
12
+ After the npm release:
13
+
14
+ ```sh
15
+ npm install -g @spatius/cli@beta
16
+ spatius auth login
17
+ spatius setup
18
+ ```
19
+
20
+ Approve login at `https://app.spatius.ai` in your local browser. Studio API
21
+ requests use `https://api.studio.spatius.ai`; development origin settings are
22
+ documented in the [workflow guide](docs/workflows.md). Credentials stay in private user configuration
23
+ storage; commands never print API keys. Setup reuses a dedicated `Spatius CLI` app.
24
+ Use `spatius setup --app-id <id>` to select another app you own.
25
+
26
+ ```sh
27
+ spatius avatars create --image ./portrait.png --name Presenter
28
+ spatius avatars jobs wait <avatar-job-id>
29
+ spatius videos create --avatar-id <avatar-id> --audio ./speech.wav --background ./background.png
30
+ spatius videos wait <video-job-id>
31
+ spatius videos download <video-job-id> --output ./video.mp4
32
+ ```
33
+
34
+ Inputs accept local files or public HTTP(S) URLs. Local files are uploaded to
35
+ temporary storage and remain available for 24 hours after upload completion.
36
+ Video output follows the service's retention deadline, currently seven days
37
+ after render submission. Save your MP4 before it expires.
38
+
39
+ Commands return JSON on stdout; progress and errors go to stderr. Save the
40
+ `operationId` and `jobId` returned by creation. Resume an interrupted creation with
41
+ `spatius videos create --resume <operation-id>` or
42
+ `spatius avatars create --resume <operation-id>`; do not start a fresh creation
43
+ just because a command timed out.
44
+
45
+ ## Use with coding agents
46
+
47
+ ```sh
48
+ npx skills add spatius-ai/spatius-cli --skill spatius-shared spatius-avatar spatius-video
49
+ spatius schema
50
+ spatius schema videos create
51
+ ```
52
+
53
+ The three skills cover setup, avatar creation, and video generation. The npm
54
+ package includes the matching skills under `skills/`; use those files when an
55
+ exact version match is needed. The GitHub installer above follows the repository
56
+ default branch. Skills and CLI behavior ship together. `--dry-run` previews a
57
+ creation without uploading or submitting it. `--help` describes the installed CLI's options.
58
+
59
+ See the [workflow guide](docs/workflows.md) for input requirements and recovery,
60
+ and the [Spatius API documentation](https://docs.spatius.ai/api-reference/video-generation)
61
+ for service behavior. Repository development instructions are in
62
+ [CONTRIBUTING.md](CONTRIBUTING.md).
63
+
64
+ Prereleases use the npm `beta` channel. After a stable release, install it with
65
+ `npm install -g @spatius/cli`. The executable is always `spatius`. Maintainers can
66
+ find the release workflow and first-time setup in the
67
+ [deployment guide](docs/deployment.md).
@@ -0,0 +1,26 @@
1
+ # Third-party notices
2
+
3
+ The Studio loopback authentication implementation adapts the protocol and code
4
+ from [create-spatius-app](https://github.com/spatius-ai/create-spatius-app).
5
+
6
+ MIT License
7
+
8
+ Copyright (c) 2026 spatialwalk
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
@@ -0,0 +1,479 @@
1
+ // src/commands.ts
2
+ import { Command, Option } from "commander";
3
+
4
+ // src/core/errors.ts
5
+ var CliError = class extends Error {
6
+ constructor(code, message, options = {}) {
7
+ super(message);
8
+ this.code = code;
9
+ this.options = options;
10
+ this.name = "CliError";
11
+ }
12
+ code;
13
+ options;
14
+ };
15
+ function asCliError(error) {
16
+ if (error instanceof CliError) return error;
17
+ if (error instanceof Error && error.name === "AbortError")
18
+ return new CliError("INTERRUPTED", "Operation interrupted.", {
19
+ exitCode: 130
20
+ });
21
+ return new CliError(
22
+ "INTERNAL_ERROR",
23
+ "The operation could not be completed.",
24
+ {
25
+ recovery: "Retry a read operation, or inspect the saved operation before retrying a creation."
26
+ }
27
+ );
28
+ }
29
+
30
+ // src/commands.ts
31
+ var timeout = {
32
+ flags: "--timeout <seconds>",
33
+ description: "Wait deadline in seconds (default: 600).",
34
+ type: "number"
35
+ };
36
+ var resume = {
37
+ flags: "--resume <operation-id>",
38
+ description: "Resume a saved operation without supplying new input."
39
+ };
40
+ var wait = {
41
+ flags: "--wait",
42
+ description: "Wait for the admitted job to finish."
43
+ };
44
+ var dryRun = {
45
+ flags: "--dry-run",
46
+ description: "Validate and preview without network requests or local state changes."
47
+ };
48
+ var pages = [
49
+ {
50
+ flags: "--page-size <number>",
51
+ description: "Results per page (1\u2013100).",
52
+ type: "number"
53
+ },
54
+ {
55
+ flags: "--page-token <token>",
56
+ description: "Next page token returned by the preceding list; keep page size unchanged."
57
+ }
58
+ ];
59
+ var status = {
60
+ flags: "--status <statuses>",
61
+ description: "Comma-separated job status filters."
62
+ };
63
+ var str = (v, key) => v[key];
64
+ var num = (v, key) => v[key];
65
+ var bool = (v, key) => v[key] === true;
66
+ function pageOptions(v) {
67
+ return {
68
+ pageSize: num(v, "pageSize"),
69
+ pageToken: str(v, "pageToken"),
70
+ statuses: str(v, "status")?.split(",")
71
+ };
72
+ }
73
+ function settings(v) {
74
+ return Object.fromEntries(
75
+ [
76
+ "width",
77
+ "height",
78
+ "fit",
79
+ "backgroundColor",
80
+ "backgroundFit",
81
+ "leadInSeconds",
82
+ "leadOutSeconds"
83
+ ].filter((key) => v[key] !== void 0).map((key) => [key, v[key]])
84
+ );
85
+ }
86
+ var definitions = [
87
+ {
88
+ path: "auth login",
89
+ description: "Authorize this local CLI through Spatius Studio.",
90
+ output: "Authenticated user and selected app metadata; no credentials.",
91
+ example: "spatius auth login",
92
+ flags: [
93
+ {
94
+ flags: "--no-browser",
95
+ description: "Print the approval URL without opening a browser."
96
+ },
97
+ {
98
+ ...timeout,
99
+ description: "Browser approval timeout in seconds (default: 300)."
100
+ }
101
+ ],
102
+ run: async (_, v, c) => c.auth.login({
103
+ noBrowser: v.browser === false,
104
+ timeoutMs: (num(v, "timeout") ?? 300) * 1e3,
105
+ onAuthorize: (url) => c.progress({
106
+ event: "authorization_required",
107
+ url,
108
+ requiresHuman: true
109
+ }),
110
+ signal: c.signal
111
+ })
112
+ },
113
+ {
114
+ path: "auth status",
115
+ description: "Inspect the active Studio login.",
116
+ output: "Authentication and account metadata.",
117
+ example: "spatius auth status",
118
+ run: async (_, __, c) => c.auth.status()
119
+ },
120
+ {
121
+ path: "auth logout",
122
+ description: "Revoke refresh access and remove local credentials.",
123
+ output: "Logged-out state.",
124
+ example: "spatius auth logout",
125
+ run: async (_, __, c) => c.auth.logout()
126
+ },
127
+ {
128
+ path: "setup",
129
+ description: "Create or reuse a Studio app and API key for this account.",
130
+ output: "appId, userId, and reuse information; no API key.",
131
+ example: "spatius setup",
132
+ flags: [
133
+ {
134
+ flags: "--app-id <id>",
135
+ description: "Select an existing app owned by the authenticated user."
136
+ },
137
+ {
138
+ flags: "--retry-uncertain",
139
+ description: "After reconciliation, allow one new app/key creation; an earlier request may already have created it."
140
+ }
141
+ ],
142
+ run: async (_, v, c) => c.auth.setup({
143
+ appId: str(v, "appId"),
144
+ retryUncertain: bool(v, "retryUncertain")
145
+ })
146
+ },
147
+ {
148
+ path: "apps list",
149
+ description: "List owned Studio apps without exposing their API keys.",
150
+ output: "Array of appId, name, and createdAt.",
151
+ example: "spatius apps list",
152
+ run: async (_, __, c) => c.auth.listApps()
153
+ },
154
+ {
155
+ path: "assets upload",
156
+ description: "Upload a local input to temporary storage.",
157
+ output: "Upload ID, accepted parts, status, and completed URL/expiration.",
158
+ example: "spatius assets upload ./speech.wav --kind audio",
159
+ args: [{ name: "file", required: true }],
160
+ flags: [
161
+ {
162
+ flags: "--kind <kind>",
163
+ description: "Input purpose.",
164
+ choices: ["avatar-image", "audio", "background"]
165
+ },
166
+ {
167
+ flags: "--resume <upload-id>",
168
+ description: "Resume an existing upload of this same local file."
169
+ }
170
+ ],
171
+ run: async (a, v, c) => {
172
+ const kind = str(v, "kind");
173
+ if (!kind)
174
+ throw new CliError("INVALID_ARGUMENT", "--kind is required.", {
175
+ exitCode: 2
176
+ });
177
+ return c.workflows.upload(a[0], {
178
+ kind,
179
+ resume: str(v, "resume")
180
+ });
181
+ }
182
+ },
183
+ {
184
+ path: "assets get",
185
+ description: "Inspect an owned temporary upload.",
186
+ output: "Upload status and accepted parts.",
187
+ example: "spatius assets get 00000000-0000-4000-8000-000000000001",
188
+ args: [{ name: "id", required: true }],
189
+ run: async (a, _, c) => c.workflows.getUpload(a[0])
190
+ },
191
+ {
192
+ path: "assets abort",
193
+ description: "Abort an unfinished upload and release storage after cleanup.",
194
+ output: "Aborted upload state.",
195
+ example: "spatius assets abort 00000000-0000-4000-8000-000000000001",
196
+ args: [{ name: "id", required: true }],
197
+ run: async (a, _, c) => c.workflows.abortUpload(a[0])
198
+ },
199
+ {
200
+ path: "avatars create",
201
+ description: "Create an avatar from a local JPEG/PNG or public URL.",
202
+ output: "operationId, jobId, status, createdAt; optional completed job.",
203
+ example: "spatius avatars create --image ./portrait.png --name Presenter",
204
+ flags: [
205
+ {
206
+ flags: "--image <file-or-url>",
207
+ description: "Opaque JPEG/PNG, at most 5 MiB, shorter side at least 340 pixels."
208
+ },
209
+ { flags: "--name <name>", description: "Avatar display name." },
210
+ resume,
211
+ wait,
212
+ timeout,
213
+ dryRun
214
+ ],
215
+ run: async (_, v, c) => c.workflows.createAvatar({
216
+ image: str(v, "image"),
217
+ name: str(v, "name"),
218
+ resume: str(v, "resume"),
219
+ wait: bool(v, "wait"),
220
+ timeout: num(v, "timeout"),
221
+ dryRun: bool(v, "dryRun")
222
+ })
223
+ },
224
+ {
225
+ path: "avatars get",
226
+ description: "Read an account avatar.",
227
+ output: "Public Avatar API detail.",
228
+ example: "spatius avatars get 00000000-0000-4000-8000-000000000001",
229
+ args: [{ name: "id", required: true }],
230
+ run: async (a, _, c) => c.workflows.getAvatar(a[0])
231
+ },
232
+ {
233
+ path: "avatars list",
234
+ description: "List account avatars.",
235
+ output: "Avatars and pagination.nextPageToken.",
236
+ example: "spatius avatars list --page-size 20",
237
+ flags: pages,
238
+ run: async (_, v, c) => c.workflows.listAvatars(pageOptions(v))
239
+ },
240
+ {
241
+ path: "videos create",
242
+ description: "Render an avatar with audio and an optional background.",
243
+ output: "operationId, jobId, status, createdAt; optional completed job.",
244
+ example: "spatius videos create --avatar-id 00000000-0000-4000-8000-000000000001 --audio ./speech.wav",
245
+ flags: [
246
+ {
247
+ flags: "--avatar-id <id>",
248
+ description: "Public, assigned, or explicitly permitted avatar UUID."
249
+ },
250
+ {
251
+ flags: "--audio <file-or-url>",
252
+ description: "Supported audio input, at most 500 MiB."
253
+ },
254
+ {
255
+ flags: "--background <file-or-url>",
256
+ description: "Optional JPEG/PNG/WebP background, at most 50 MiB."
257
+ },
258
+ { flags: "--name <name>", description: "Video job name." },
259
+ {
260
+ flags: "--request-id <uuid>",
261
+ description: "Retry identity; generated and saved when omitted."
262
+ },
263
+ {
264
+ flags: "--width <pixels>",
265
+ description: "Even width, 64\u20131920 (default 1024).",
266
+ type: "number"
267
+ },
268
+ {
269
+ flags: "--height <pixels>",
270
+ description: "Even height, 64\u20131920 (default 1024).",
271
+ type: "number"
272
+ },
273
+ {
274
+ flags: "--fit <fit>",
275
+ description: "Avatar fit (default crop).",
276
+ choices: ["crop", "contain"]
277
+ },
278
+ {
279
+ flags: "--background-color <hex>",
280
+ description: "Six-digit RGB color (default #000000)."
281
+ },
282
+ {
283
+ flags: "--background-fit <fit>",
284
+ description: "Background fit (default cover).",
285
+ choices: ["cover", "contain", "stretch"]
286
+ },
287
+ {
288
+ flags: "--lead-in-seconds <seconds>",
289
+ description: "Additional initial idle time, 0\u201360.",
290
+ type: "number"
291
+ },
292
+ {
293
+ flags: "--lead-out-seconds <seconds>",
294
+ description: "Additional final idle time, 0\u201360.",
295
+ type: "number"
296
+ },
297
+ resume,
298
+ wait,
299
+ timeout,
300
+ dryRun
301
+ ],
302
+ run: async (_, v, c) => c.workflows.createVideo({
303
+ avatarId: str(v, "avatarId"),
304
+ audio: str(v, "audio"),
305
+ background: str(v, "background"),
306
+ name: str(v, "name"),
307
+ requestId: str(v, "requestId"),
308
+ video: settings(v),
309
+ resume: str(v, "resume"),
310
+ wait: bool(v, "wait"),
311
+ timeout: num(v, "timeout"),
312
+ dryRun: bool(v, "dryRun")
313
+ })
314
+ },
315
+ ...["avatar", "video"].flatMap((kind) => {
316
+ const prefix = kind === "avatar" ? "avatars jobs" : "videos";
317
+ return [
318
+ {
319
+ path: `${prefix} get`,
320
+ description: `Read a ${kind} job.`,
321
+ output: "Job detail; a successful video includes a fresh download URL.",
322
+ example: `spatius ${prefix} get 00000000-0000-4000-8000-000000000001`,
323
+ args: [{ name: "id", required: true }],
324
+ run: async (a, _, c) => c.workflows.getJob(kind, a[0])
325
+ },
326
+ {
327
+ path: `${prefix} list`,
328
+ description: `List ${kind} jobs.`,
329
+ output: "Jobs and pagination.nextPageToken; no download URLs.",
330
+ example: `spatius ${prefix} list --status processing`,
331
+ flags: [...pages, status],
332
+ run: async (_, v, c) => c.workflows.listJobs(kind, pageOptions(v))
333
+ },
334
+ {
335
+ path: `${prefix} wait`,
336
+ description: `Wait for an existing ${kind} job without submitting another.`,
337
+ output: "Terminal job detail. Deadline reached exits 3 and preserves the job.",
338
+ example: `spatius ${prefix} wait 00000000-0000-4000-8000-000000000001 --timeout 600`,
339
+ args: [{ name: "id", required: true }],
340
+ flags: [timeout],
341
+ run: async (a, v, c) => c.workflows.waitJob(kind, a[0], { timeout: num(v, "timeout") })
342
+ }
343
+ ];
344
+ }),
345
+ {
346
+ path: "videos download",
347
+ description: "Fetch a fresh MP4 link and save the output atomically.",
348
+ output: "Saved file path and video job identity.",
349
+ example: "spatius videos download 00000000-0000-4000-8000-000000000001 --output ./video.mp4",
350
+ args: [{ name: "id", required: true }],
351
+ flags: [
352
+ {
353
+ flags: "--output <path>",
354
+ description: "Destination MP4 file (required)."
355
+ },
356
+ { flags: "--force", description: "Replace an existing destination." }
357
+ ],
358
+ run: async (a, v, c) => {
359
+ const output = str(v, "output");
360
+ if (!output)
361
+ throw new CliError("INVALID_ARGUMENT", "--output is required.", {
362
+ exitCode: 2
363
+ });
364
+ return c.workflows.download(a[0], { output, force: bool(v, "force") });
365
+ }
366
+ }
367
+ ];
368
+ function commandSchema(path) {
369
+ const found = path ? definitions.filter((d) => d.path === path) : definitions;
370
+ if (!found.length)
371
+ throw new CliError(
372
+ "UNKNOWN_COMMAND",
373
+ "No command matches that schema path.",
374
+ {
375
+ exitCode: 2,
376
+ recovery: "Run spatius schema to discover available commands."
377
+ }
378
+ );
379
+ return {
380
+ schemaVersion: 1,
381
+ executable: "spatius",
382
+ outputEnvelope: {
383
+ schemaVersion: 1,
384
+ ok: true,
385
+ data: "command-specific result"
386
+ },
387
+ errorEnvelope: {
388
+ schemaVersion: 1,
389
+ ok: false,
390
+ error: {
391
+ code: "string",
392
+ message: "string",
393
+ retryable: "boolean",
394
+ recovery: "string?",
395
+ details: "object?"
396
+ }
397
+ },
398
+ streams: { result: "stdout", error: "stderr", progress: "stderr" },
399
+ exitCodes: {
400
+ 0: "success",
401
+ 1: "operation failure",
402
+ 2: "invalid arguments",
403
+ 3: "wait deadline",
404
+ 130: "interrupted"
405
+ },
406
+ commands: found.map(
407
+ ({ path: path2, description, args, flags, output, example }) => ({
408
+ path: path2,
409
+ description,
410
+ arguments: args ?? [],
411
+ options: flags ?? [],
412
+ output,
413
+ examples: [example]
414
+ })
415
+ )
416
+ };
417
+ }
418
+ function buildProgram(getContext, emit, version) {
419
+ const root = new Command().name("spatius").description("Avatar and video workflows for coding agents.").version(version).option("--json", "Use structured JSON output (the default).").exitOverride();
420
+ const groups = /* @__PURE__ */ new Map([["", root]]);
421
+ for (const def of definitions) {
422
+ const parts = def.path.split(" ");
423
+ let parent = root;
424
+ for (let i = 0; i < parts.length - 1; i++) {
425
+ const key = parts.slice(0, i + 1).join(" ");
426
+ let group = groups.get(key);
427
+ if (!group) {
428
+ group = parent.command(parts[i]);
429
+ groups.set(key, group);
430
+ }
431
+ parent = group;
432
+ }
433
+ const cmd = parent.command(parts.at(-1)).description(def.description);
434
+ for (const arg of def.args ?? [])
435
+ cmd.argument(arg.required ? `<${arg.name}>` : `[${arg.name}]`);
436
+ for (const flag of def.flags ?? []) {
437
+ const option = new Option(flag.flags, flag.description);
438
+ if (flag.choices) option.choices(flag.choices);
439
+ if (flag.type === "number")
440
+ option.argParser((value) => {
441
+ const parsed = Number(value);
442
+ if (!value.trim() || !Number.isFinite(parsed))
443
+ throw new CliError(
444
+ "INVALID_ARGUMENT",
445
+ "Expected a finite number.",
446
+ { exitCode: 2 }
447
+ );
448
+ return parsed;
449
+ });
450
+ if (flag.default !== void 0) option.default(flag.default);
451
+ cmd.addOption(option);
452
+ }
453
+ cmd.action(async (...args) => {
454
+ const count = def.args?.length ?? 0;
455
+ const values = args[count];
456
+ if (typeof values.timeout === "number" && values.timeout <= 0)
457
+ throw new CliError("INVALID_ARGUMENT", "--timeout must be positive.", {
458
+ exitCode: 2
459
+ });
460
+ emit(
461
+ await def.run(args.slice(0, count), values, getContext())
462
+ );
463
+ });
464
+ }
465
+ root.command("schema").description(
466
+ "Describe commands and structured output for this CLI version."
467
+ ).argument("[command...]").action(
468
+ (parts) => emit(commandSchema(parts.length ? parts.join(" ") : void 0))
469
+ );
470
+ return root;
471
+ }
472
+
473
+ export {
474
+ CliError,
475
+ asCliError,
476
+ definitions,
477
+ commandSchema,
478
+ buildProgram
479
+ };