@artillect/cli 0.1.0 → 0.1.2

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.
Files changed (3) hide show
  1. package/README.md +37 -40
  2. package/dist/index.js +752 -142
  3. package/package.json +13 -17
package/README.md CHANGED
@@ -1,58 +1,55 @@
1
1
  # `@artillect/cli`
2
2
 
3
- Fourth Artillect client (Studio / Public API / MCP / CLI). Thin Commander binary over Public API v1. The SDK is **bundled** into this package and is not published separately. **No local model list** — `artillect models` is `GET /api/v1/models`.
3
+ Artillect CLI генерация картинок и видео из терминала.
4
4
 
5
- ```bash
6
- npm i -g @artillect/cli
7
- # or, without a global install:
8
- npx @artillect/cli@latest --help
9
- ```
10
-
11
- Requires Node.js **≥ 20**. First publish of a scoped package uses `npm publish --access public` (already set in `publishConfig`).
5
+ ## Установка
12
6
 
13
7
  ```bash
14
- artillect auth login # device flow → ~/.config/artillect/config.json
15
- # or: export ARTILLECT_API_KEY=art_…
16
- # export ARTILLECT_BASE_URL=https://dev.artillect.pro # optional
17
-
18
- artillect balance
19
- artillect models list
20
- artillect generate image --prompt "a cat" --wait
21
- artillect generate video kling-3 --prompt "a cat walks" --start-image ./a.png --wait
8
+ npm install -g @artillect/cli
22
9
  ```
23
10
 
24
- `--json` prints the API envelope. `--wait` polls until `status=completed` and downloads media into `--out` (default cwd).
25
-
26
- ## Updates and hotfixes
11
+ Нужен Node.js 20+. Без глобальной установки: `npx @artillect/cli@latest --help`.
27
12
 
28
- npm **does not push** new versions to machines that already ran `npm i -g`. After install, the user keeps that exact tarball until they upgrade.
13
+ ## Быстрый старт
29
14
 
30
- | Who | What happens |
31
- | --------------------- | --------------------------------------------------------------------------------------------------------------- |
32
- | You ship a CLI hotfix | Bump **patch** (`0.1.0` → `0.1.1`), `pnpm --filter @artillect/cli publish`. |
33
- | You ship a feature | Bump **minor**. Breaking CLI flags → **major**. |
34
- | API/server hotfix | Users get it on the next request. No CLI reinstall. Catalog, billing, and generation bugs live on the server. |
35
- | SDK change | SDK is private and inlined in the CLI bundle. Republish `@artillect/cli` — there is no `@artillect/sdk` on npm. |
15
+ ```bash
16
+ artillect auth login
17
+ artillect generate image --prompt "кот в космосе" --wait --open
18
+ ```
36
19
 
37
- How a user actually receives a new CLI:
20
+ ## Примеры
38
21
 
39
22
  ```bash
40
- npm i -g @artillect/cli@latest
41
- # or one-shot without touching the global:
42
- npx @artillect/cli@latest models list
23
+ artillect generate image --model seedream-5-pro --prompt "кот в космосе" --wait --open
24
+ artillect generate video grok-imagine-video --prompt "кот идёт" --wait --open
25
+ artillect generate video kling-3 --prompt "кот идёт" --start-image ./a.png --wait
26
+ artillect estimate image -m seedream-5-pro -p "кот"
27
+ artillect models list
28
+ artillect generate get <task_id>
29
+ artillect generate wait <task_id> --open
30
+ artillect projects list
31
+ artillect assets list --project 12
43
32
  ```
44
33
 
45
- There is no App Store review, no push notification, and no silent auto-update of globals. The CLI prints a **stderr hint at most once a day** when `registry.npmjs.org/@artillect/cli/latest` is newer than the running binary. Disable with `ARTILLECT_NO_UPDATE_CHECK=1`. Announce breaking changes in release notes / Discord / the product changelog — npm will not email end users.
34
+ Ключ также можно задать через `ARTILLECT_API_KEY`. `--wait` скачивает файл, `--open` открывает его.
46
35
 
47
- `npx @artillect/cli` without `@latest` may use a cached copy. Prefer `@latest` in docs.
36
+ ## Команды
48
37
 
49
- ## Repo scripts
38
+ | Команда | Зачем |
39
+ | -------------------- | ------------------------------------------ |
40
+ | `artillect auth` | login / logout / status |
41
+ | `artillect balance` | баланс |
42
+ | `artillect models` | list / get |
43
+ | `artillect generate` | image / video / get / wait / list / cancel |
44
+ | `artillect estimate` | цена без списания |
45
+ | `artillect upload` | загрузить файл, получить ast_… |
46
+ | `artillect assets` | list / get / copy |
47
+ | `artillect projects` | list |
48
+ | `artillect elements` | list / create (`@Name`) |
50
49
 
51
- ```bash
52
- pnpm --filter @artillect/sdk run build
53
- pnpm --filter @artillect/cli run build
54
- pnpm --filter @artillect/cli run pack:check # tarball + --help/--version
55
- pnpm --filter @artillect/cli run smoke # live health + models + device/code
56
- ```
50
+ Обновление: `npm i -g @artillect/cli@latest`.
51
+
52
+ ## Ссылки
57
53
 
58
- Not in v1: Homebrew, a second catalog, `@artillect/mcp` on npm (MCP is the hosted URL).
54
+ - Документация: https://app.artillect.pro/docs/cli
55
+ - Артиллект: https://app.artillect.pro
package/dist/index.js CHANGED
@@ -195,6 +195,8 @@ async function publicApiUploadFile(config, file) {
195
195
  const form = new FormData();
196
196
  const blob = new Blob([Buffer.from(file.bytes)], { type: contentType });
197
197
  form.append("file", blob, file.filename);
198
+ if (file.projectId != null)
199
+ form.append("project_id", String(file.projectId));
198
200
  const res = await fetch(url, {
199
201
  method: "POST",
200
202
  headers: {
@@ -231,6 +233,10 @@ function listQueryPath(basePath, params) {
231
233
  search.set("before_created_at", params.before_created_at);
232
234
  if (params?.q)
233
235
  search.set("q", params.q);
236
+ if (params?.scope)
237
+ search.set("scope", params.scope);
238
+ if (params?.project_id != null)
239
+ search.set("project_id", String(params.project_id));
234
240
  if (params?.mine_only)
235
241
  search.set("mine_only", "1");
236
242
  if (params?.actor_user_id != null)
@@ -318,7 +324,13 @@ function createClient(opts) {
318
324
  deleteWebhook: () => publicApiFetch(config, "DELETE", "/api/v1/webhooks"),
319
325
  rotateWebhookSecret: () => publicApiFetch(config, "POST", "/api/v1/webhooks/rotate-secret"),
320
326
  listWebhookDeliveries: (params) => publicApiFetch(config, "GET", listQueryPath("/api/v1/webhooks/deliveries", params)),
321
- getWebhookDelivery: (eventId) => publicApiFetch(config, "GET", `/api/v1/webhooks/deliveries/${encodeURIComponent(eventId)}`)
327
+ getWebhookDelivery: (eventId) => publicApiFetch(config, "GET", `/api/v1/webhooks/deliveries/${encodeURIComponent(eventId)}`),
328
+ listAssets: (params) => publicApiFetch(config, "GET", listQueryPath("/api/v1/assets", params)),
329
+ getAsset: (assetId) => publicApiFetch(config, "GET", `/api/v1/assets/${encodeURIComponent(assetId)}`),
330
+ copyAsset: (assetId, body) => publicApiFetch(config, "POST", `/api/v1/assets/${encodeURIComponent(assetId)}`, body),
331
+ listProjects: (params) => publicApiFetch(config, "GET", listQueryPath("/api/v1/projects", params)),
332
+ listProjectElements: (projectId, params) => publicApiFetch(config, "GET", listQueryPath(`/api/v1/projects/${encodeURIComponent(String(projectId))}/elements`, params)),
333
+ createProjectElement: (projectId, body) => publicApiFetch(config, "POST", `/api/v1/projects/${encodeURIComponent(String(projectId))}/elements`, body)
322
334
  };
323
335
  }
324
336
 
@@ -355,7 +367,7 @@ function loadCliConfig() {
355
367
  function requireApiKey() {
356
368
  const cfg = loadCliConfig();
357
369
  if (!cfg.apiKey) {
358
- throw new Error("Not logged in. Run `artillect auth login` or set ARTILLECT_API_KEY.");
370
+ throw new Error("\u041D\u0435\u0442 \u0432\u0445\u043E\u0434\u0430. \u0412\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u0435 `artillect auth login` \u0438\u043B\u0438 \u0437\u0430\u0434\u0430\u0439\u0442\u0435 ARTILLECT_API_KEY.");
359
371
  }
360
372
  return cfg;
361
373
  }
@@ -411,72 +423,9 @@ function sleep(ms) {
411
423
  return new Promise((r) => setTimeout(r, ms));
412
424
  }
413
425
 
414
- // src/auth.ts
415
- async function authLogin() {
416
- const { baseUrl } = loadCliConfig();
417
- const started = await postJson(baseUrl, "/api/v1/device/code", {});
418
- if (started.status >= 400) {
419
- throw new Error(String(started.json.error || `device code failed (${started.status})`));
420
- }
421
- const deviceCode = String(started.json.device_code || "");
422
- const userCode = String(started.json.user_code || "");
423
- const verificationUrl = String(started.json.verification_url || "");
424
- const intervalSec = Math.max(1, Number(started.json.interval) || 5);
425
- const expiresIn = Number(started.json.expires_in) || 600;
426
- if (!deviceCode || !verificationUrl) {
427
- throw new Error("Device authorization response missing device_code/verification_url");
428
- }
429
- console.log("Open this URL in a browser where you are logged into Artillect:");
430
- console.log(` ${verificationUrl}`);
431
- if (userCode) console.log(`Confirm code: ${userCode}`);
432
- console.log("Waiting for approval\u2026");
433
- const deadline = Date.now() + expiresIn * 1e3;
434
- while (Date.now() < deadline) {
435
- await sleep(intervalSec * 1e3);
436
- const polled = await postJson(baseUrl, "/api/v1/device/token", { device_code: deviceCode });
437
- const status = String(polled.json.status || "");
438
- if (status === "pending") continue;
439
- if (status === "expired" || polled.status === 410) {
440
- throw new Error("Device code expired. Run `artillect auth login` again.");
441
- }
442
- const apiKey = String(polled.json.api_key || "");
443
- if (status === "completed" && apiKey) {
444
- const path = saveCliConfig({ apiKey, baseUrl });
445
- console.log(`Logged in. Key saved to ${path}`);
446
- return;
447
- }
448
- throw new Error(String(polled.json.error || `login failed (${polled.status})`));
449
- }
450
- throw new Error("Timed out waiting for browser approval.");
451
- }
452
- async function authLogout() {
453
- clearCliConfig();
454
- console.log("Logged out.");
455
- }
456
- async function authStatus() {
457
- const envKey = Boolean(String(process.env.ARTILLECT_API_KEY || "").trim());
458
- try {
459
- const cfg = requireApiKey();
460
- const client = createClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
461
- const me = await client.getMe();
462
- if (!me.ok) {
463
- console.log(
464
- `Configured (${envKey ? "ARTILLECT_API_KEY" : configPath()}), but /me failed: ${me.error}`
465
- );
466
- process.exitCode = 1;
467
- return;
468
- }
469
- const body = me.body;
470
- console.log(`Logged in as ${body.email ?? body.user_id ?? "ok"}`);
471
- console.log(`Base URL: ${cfg.baseUrl}`);
472
- } catch (err) {
473
- console.log(err instanceof Error ? err.message : String(err));
474
- process.exitCode = 1;
475
- }
476
- }
477
-
478
- // src/generate.ts
479
- import { createWriteStream } from "node:fs";
426
+ // src/runtime.ts
427
+ import { spawn } from "node:child_process";
428
+ import { createWriteStream, mkdirSync as mkdirSync2 } from "node:fs";
480
429
  import { basename as basename2 } from "node:path";
481
430
  import { Readable } from "node:stream";
482
431
  import { pipeline } from "node:stream/promises";
@@ -487,12 +436,12 @@ function asRecord(value) {
487
436
  }
488
437
  function formatModelsList(body, kind) {
489
438
  const groups = [
490
- ["images", "Images"],
491
- ["video", "Video"],
492
- ["chat", "Chat"],
493
- ["audio", "Audio"],
494
- ["upscale", "Upscale"],
495
- ["mesh", "Mesh"],
439
+ ["images", "\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438"],
440
+ ["video", "\u0412\u0438\u0434\u0435\u043E"],
441
+ ["chat", "\u0427\u0430\u0442"],
442
+ ["audio", "\u0410\u0443\u0434\u0438\u043E"],
443
+ ["upscale", "\u0410\u043F\u0441\u043A\u0435\u0439\u043B"],
444
+ ["mesh", "3D"],
496
445
  ["switchx", "SwitchX"]
497
446
  ];
498
447
  const lines = [];
@@ -512,20 +461,126 @@ function formatModelsList(body, kind) {
512
461
  }
513
462
  return lines.join("\n");
514
463
  }
464
+ function findModel(body, slug) {
465
+ const want = String(slug || "").trim().toLowerCase();
466
+ if (!want) return null;
467
+ const groups = ["images", "video", "chat", "audio", "upscale", "mesh", "switchx"];
468
+ for (const kind of groups) {
469
+ const models2 = asRecord(body[kind]).models;
470
+ if (!Array.isArray(models2)) continue;
471
+ for (const raw of models2) {
472
+ const m = asRecord(raw);
473
+ if (String(m.slug || "").toLowerCase() === want) return { kind, model: m };
474
+ const aliases = Array.isArray(m.aliases) ? m.aliases : [];
475
+ if (aliases.some((a) => String(a || "").toLowerCase() === want)) return { kind, model: m };
476
+ }
477
+ }
478
+ return null;
479
+ }
515
480
 
516
- // src/generate.ts
481
+ // src/runtime.ts
517
482
  function fail(result) {
518
483
  if (result.ok) throw new Error("expected error result");
484
+ const code = result.code ? ` [${result.code}]` : "";
519
485
  const extra = result.requestId ? ` (request_id ${result.requestId})` : "";
520
- throw new Error(`${result.error}${extra}`);
486
+ throw new Error(`${result.error}${code}${extra}`);
487
+ }
488
+ function printOrJson(jsonMode, payload, lines) {
489
+ if (jsonMode) {
490
+ console.log(JSON.stringify(payload, null, 2));
491
+ return;
492
+ }
493
+ for (const line of lines) console.log(line);
494
+ }
495
+ var ASSET_ID_RE = /^ast_[0-9A-HJKMNPQRSTVWXYZ]{26}$/;
496
+ function isAssetId(value) {
497
+ return ASSET_ID_RE.test(value.trim());
498
+ }
499
+ function parseProjectId(raw) {
500
+ const n = Number(raw);
501
+ if (!Number.isInteger(n) || n <= 0) {
502
+ throw new Error("project_id \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043F\u043E\u043B\u043E\u0436\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u043C \u0446\u0435\u043B\u044B\u043C");
503
+ }
504
+ return n;
521
505
  }
522
- async function uploadLocal(client, filePath) {
506
+ function parseLimit(raw, fallback) {
507
+ if (raw == null || raw === "") return fallback;
508
+ const n = Number(raw);
509
+ if (!Number.isInteger(n) || n <= 0) {
510
+ throw new Error(`--limit \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043F\u043E\u043B\u043E\u0436\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u043C \u0446\u0435\u043B\u044B\u043C (\u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E ${raw})`);
511
+ }
512
+ return n;
513
+ }
514
+ function extensionFromDownload(contentType, url) {
515
+ const type = contentType.split(";")[0]?.trim().toLowerCase() || "";
516
+ switch (type) {
517
+ case "video/mp4":
518
+ return "mp4";
519
+ case "video/webm":
520
+ return "webm";
521
+ case "video/quicktime":
522
+ return "mov";
523
+ case "image/jpeg":
524
+ return "jpg";
525
+ case "image/webp":
526
+ return "webp";
527
+ case "image/gif":
528
+ return "gif";
529
+ case "image/png":
530
+ return "png";
531
+ case "model/gltf-binary":
532
+ return "glb";
533
+ case "model/gltf+json":
534
+ return "gltf";
535
+ case "audio/mpeg":
536
+ return "mp3";
537
+ case "audio/wav":
538
+ case "audio/x-wav":
539
+ return "wav";
540
+ default:
541
+ break;
542
+ }
543
+ if (url.includes(".mp4")) return "mp4";
544
+ if (url.includes(".webm")) return "webm";
545
+ if (url.includes(".glb")) return "glb";
546
+ if (url.includes(".webp")) return "webp";
547
+ if (url.includes(".jpg") || url.includes(".jpeg")) return "jpg";
548
+ if (url.includes(".png")) return "png";
549
+ return "bin";
550
+ }
551
+ function nextPageLines(body) {
552
+ if (body.next_cursor == null || body.next_cursor === "") return [];
553
+ return [`\u0434\u0430\u043B\u044C\u0448\u0435 --cursor ${body.next_cursor}`];
554
+ }
555
+ async function uploadLocal(client, filePath, opts) {
523
556
  const file = await readLocalUploadFile(filePath);
524
- const up = await client.uploadFile({ bytes: file.bytes, filename: file.filename });
557
+ const up = await client.uploadFile({
558
+ bytes: file.bytes,
559
+ filename: file.filename,
560
+ ...opts?.projectId != null ? { projectId: opts.projectId } : {}
561
+ });
525
562
  if (!up.ok) fail(up);
526
- const url = String(asRecord(up.body).url || "").trim();
527
- if (!url) throw new Error(`Upload of ${basename2(filePath)} returned no url`);
528
- return url;
563
+ const body = asRecord(up.body);
564
+ const url = String(body.url || "").trim();
565
+ if (!url) throw new Error(`\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 ${basename2(filePath)} \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B\u0430 url`);
566
+ return {
567
+ url,
568
+ assetId: String(body.asset_id || "").trim(),
569
+ kind: String(body.kind || "").trim()
570
+ };
571
+ }
572
+ async function resolveInputRef(client, ref, opts) {
573
+ const value = ref.trim();
574
+ if (/^ast_/i.test(value)) {
575
+ if (!isAssetId(value)) throw new Error(`\u043D\u0435 \u043F\u043E\u0445\u043E\u0436\u0435 \u043D\u0430 asset id: ${value} (\u043E\u0436\u0438\u0434\u0430\u044E ast_\u2026)`);
576
+ const res = await client.getAsset(value);
577
+ if (!res.ok) fail(res);
578
+ const url = String(asRecord(res.body).url || "").trim();
579
+ if (!url) throw new Error(`asset ${value} \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B url`);
580
+ return url;
581
+ }
582
+ if (/^https?:\/\//i.test(value)) return value;
583
+ return (await uploadLocal(client, value, opts)).url;
529
584
  }
530
585
  function mediaUrls(body) {
531
586
  const images = Array.isArray(body.images) ? body.images : [];
@@ -543,55 +598,180 @@ function terminalStatus(body) {
543
598
  if (body.done === true) return String(body.error || "") ? "failed" : "completed";
544
599
  return null;
545
600
  }
546
- async function waitForTask(client, taskId, jsonMode) {
601
+ var STATUS_RU = {
602
+ queued: "\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438",
603
+ pending: "\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438",
604
+ running: "\u0438\u0434\u0451\u0442",
605
+ processing: "\u0438\u0434\u0451\u0442",
606
+ generating: "\u0438\u0434\u0451\u0442",
607
+ in_progress: "\u0438\u0434\u0451\u0442",
608
+ finalizing: "\u0441\u043E\u0445\u0440\u0430\u043D\u044F\u0435\u043C"
609
+ };
610
+ function formatStatus(raw) {
611
+ const key = String(raw || "queued").toLowerCase();
612
+ return STATUS_RU[key] || key;
613
+ }
614
+ function parseDurationMs(raw, fallbackMs) {
615
+ const text = String(raw || "").trim();
616
+ if (!text) return fallbackMs;
617
+ const m = text.match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h)?$/i);
618
+ if (!m) throw new Error(`\u043D\u0435 \u043F\u043E\u043D\u044F\u043B \u0434\u043B\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0441\u0442\u044C: ${text} (\u043F\u0440\u0438\u043C\u0435\u0440\u044B: 90s, 10m)`);
619
+ const n = Number(m[1]);
620
+ const unit = (m[2] || "s").toLowerCase();
621
+ if (unit === "ms") return Math.round(n);
622
+ if (unit === "s") return Math.round(n * 1e3);
623
+ if (unit === "m") return Math.round(n * 60 * 1e3);
624
+ return Math.round(n * 60 * 60 * 1e3);
625
+ }
626
+ async function waitForTask(client, taskId, opts) {
627
+ const timeoutMs = opts.timeoutMs ?? 20 * 60 * 1e3;
547
628
  const started = Date.now();
548
- const timeoutMs = 20 * 60 * 1e3;
629
+ const showProgress = !opts.jsonMode && Boolean(process.stderr.isTTY);
549
630
  while (Date.now() - started < timeoutMs) {
550
631
  const poll = await client.getTask(taskId);
551
632
  if (!poll.ok) fail(poll);
552
633
  const body = asRecord(poll.body);
553
634
  const term = terminalStatus(body);
554
635
  if (term) {
636
+ if (showProgress) process.stderr.write("\n");
555
637
  if (term !== "completed") {
556
- throw new Error(String(body.error || `generation ${term}`));
638
+ throw new Error(
639
+ String(
640
+ body.error || (term === "cancelled" ? "\u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u044F \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u0430" : "\u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C \u0441 \u043E\u0448\u0438\u0431\u043A\u043E\u0439")
641
+ )
642
+ );
557
643
  }
558
644
  return body;
559
645
  }
560
646
  const interval = Math.max(2, Number(body.recommended_poll_interval_sec) || 3);
561
- if (!jsonMode) {
647
+ if (showProgress) {
562
648
  const progress = body.progress != null ? ` ${body.progress}%` : "";
563
- process.stderr.write(`\r${String(body.status || "queued")}${progress} `);
649
+ process.stderr.write(`\r${formatStatus(body.status)}${progress} `);
564
650
  }
565
651
  await sleep(interval * 1e3);
566
652
  }
567
- throw new Error("Timed out waiting for generation");
653
+ throw new Error("\u0412\u0440\u0435\u043C\u044F \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438 \u0438\u0441\u0442\u0435\u043A\u043B\u043E.");
568
654
  }
569
655
  async function downloadAll(urls, outDir) {
656
+ const destDir = outDir.replace(/\/+$/, "") || ".";
657
+ mkdirSync2(destDir, { recursive: true });
570
658
  const saved = [];
571
659
  for (const [i, url] of urls.entries()) {
572
660
  const res = await fetch(url);
573
- if (!res.ok || !res.body) throw new Error(`download failed HTTP ${res.status}`);
574
- const extGuess = url.includes(".mp4") ? "mp4" : url.includes(".webm") ? "webm" : "png";
575
- const dest = `${outDir.replace(/\/+$/, "") || "."}/artillect-${Date.now()}-${i + 1}.${extGuess}`;
661
+ if (!res.ok || !res.body) throw new Error(`\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043A\u0430\u0447\u0430\u0442\u044C \u0444\u0430\u0439\u043B (HTTP ${res.status})`);
662
+ const ext = extensionFromDownload(res.headers.get("content-type") || "", url);
663
+ const dest = `${destDir}/artillect-${Date.now()}-${i + 1}.${ext}`;
576
664
  await pipeline(Readable.fromWeb(res.body), createWriteStream(dest));
577
665
  saved.push(dest);
578
666
  }
579
667
  return saved;
580
668
  }
581
- function printOrJson(jsonMode, payload, lines) {
582
- if (jsonMode) {
583
- console.log(JSON.stringify(payload, null, 2));
584
- return;
669
+ function openExternal(target) {
670
+ const bin = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
671
+ const args = process.platform === "win32" ? ["/c", "start", "", target] : [target];
672
+ try {
673
+ spawn(bin, args, { detached: true, stdio: "ignore" }).unref();
674
+ } catch {
585
675
  }
586
- for (const line of lines) console.log(line);
587
676
  }
677
+ function openSaved(paths) {
678
+ for (const filePath of paths) openExternal(filePath);
679
+ }
680
+ async function finishAndSave(client, taskId, opts) {
681
+ const done = await waitForTask(client, taskId, {
682
+ jsonMode: opts.json,
683
+ timeoutMs: opts.timeoutMs
684
+ });
685
+ const urls = mediaUrls(done);
686
+ const saved = urls.length ? await downloadAll(urls, opts.out) : [];
687
+ if (opts.open && saved.length) openSaved(saved);
688
+ printOrJson(opts.json, { ...done, saved }, [
689
+ `\u0413\u043E\u0442\u043E\u0432\u043E ${taskId}`,
690
+ ...saved.map((p) => `\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u043E ${p}`),
691
+ ...!saved.length ? urls.map((u) => u) : [],
692
+ ...saved.length && !opts.open ? [`\u041E\u0442\u043A\u0440\u044B\u0442\u044C: ${process.platform === "darwin" ? "open" : "xdg-open"} ${saved[0]}`] : []
693
+ ]);
694
+ }
695
+
696
+ // src/auth.ts
697
+ async function authLogin() {
698
+ const { baseUrl } = loadCliConfig();
699
+ const started = await postJson(baseUrl, "/api/v1/device/code", {});
700
+ if (started.status >= 400) {
701
+ throw new Error(
702
+ String(started.json.error || `\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u043A\u043E\u0434 \u0432\u0445\u043E\u0434\u0430 (${started.status})`)
703
+ );
704
+ }
705
+ const deviceCode = String(started.json.device_code || "");
706
+ const userCode = String(started.json.user_code || "");
707
+ const verificationUrl = String(started.json.verification_url || "");
708
+ const intervalSec = Math.max(1, Number(started.json.interval) || 5);
709
+ const expiresIn = Number(started.json.expires_in) || 600;
710
+ if (!deviceCode || !verificationUrl) {
711
+ throw new Error("\u0412 \u043E\u0442\u0432\u0435\u0442\u0435 \u043D\u0435\u0442 device_code \u0438\u043B\u0438 verification_url");
712
+ }
713
+ console.log("\u041E\u0442\u043A\u0440\u043E\u0439\u0442\u0435 \u0441\u0441\u044B\u043B\u043A\u0443 \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435, \u0433\u0434\u0435 \u0432\u044B \u0432\u043E\u0448\u043B\u0438 \u0432 \u0410\u0440\u0442\u0438\u043B\u043B\u0435\u043A\u0442:");
714
+ console.log(` ${verificationUrl}`);
715
+ if (userCode) console.log(`\u041A\u043E\u0434 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F: ${userCode}`);
716
+ if (process.env.ARTILLECT_NO_BROWSER !== "1") {
717
+ openExternal(verificationUrl);
718
+ console.log("\u0416\u0434\u0451\u043C \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435\u2026");
719
+ } else {
720
+ console.log("\u0416\u0434\u0451\u043C \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435\u2026");
721
+ }
722
+ const deadline = Date.now() + expiresIn * 1e3;
723
+ while (Date.now() < deadline) {
724
+ await sleep(intervalSec * 1e3);
725
+ const polled = await postJson(baseUrl, "/api/v1/device/token", { device_code: deviceCode });
726
+ const status = String(polled.json.status || "");
727
+ if (status === "pending") continue;
728
+ if (status === "expired" || polled.status === 410) {
729
+ throw new Error("\u041A\u043E\u0434 \u0438\u0441\u0442\u0435\u043A. \u0421\u043D\u043E\u0432\u0430 \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u0435 `artillect auth login`.");
730
+ }
731
+ const apiKey = String(polled.json.api_key || "");
732
+ if (status === "completed" && apiKey) {
733
+ const path = saveCliConfig({ apiKey, baseUrl });
734
+ console.log(`\u0412\u0445\u043E\u0434 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D. \u041A\u043B\u044E\u0447 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D \u0432 ${path}`);
735
+ return;
736
+ }
737
+ throw new Error(String(polled.json.error || `\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0432\u043E\u0439\u0442\u0438 (${polled.status})`));
738
+ }
739
+ throw new Error("\u0412\u0440\u0435\u043C\u044F \u043D\u0430 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435 \u0438\u0441\u0442\u0435\u043A\u043B\u043E.");
740
+ }
741
+ async function authLogout() {
742
+ clearCliConfig();
743
+ console.log("\u0412\u044B \u0432\u044B\u0448\u043B\u0438.");
744
+ }
745
+ async function authStatus() {
746
+ const envKey = Boolean(String(process.env.ARTILLECT_API_KEY || "").trim());
747
+ try {
748
+ const cfg = requireApiKey();
749
+ const client = createClient({ apiKey: cfg.apiKey, baseUrl: cfg.baseUrl });
750
+ const me = await client.getMe();
751
+ if (!me.ok) {
752
+ console.log(
753
+ `\u041A\u043B\u044E\u0447 \u0437\u0430\u0434\u0430\u043D (${envKey ? "ARTILLECT_API_KEY" : configPath()}), \u043D\u043E /me \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B: ${me.error}`
754
+ );
755
+ process.exitCode = 1;
756
+ return;
757
+ }
758
+ const body = me.body;
759
+ console.log(`\u0412\u044B \u0432\u043E\u0448\u043B\u0438 \u043A\u0430\u043A ${body.email ?? body.user_id ?? "ok"}`);
760
+ console.log(`\u0421\u0435\u0440\u0432\u0435\u0440: ${cfg.baseUrl}`);
761
+ } catch (err) {
762
+ console.log(err instanceof Error ? err.message : String(err));
763
+ process.exitCode = 1;
764
+ }
765
+ }
766
+
767
+ // src/generate.ts
588
768
  async function cmdBalance(jsonMode) {
589
769
  const cfg = requireApiKey();
590
770
  const client = createClient(cfg);
591
771
  const res = await client.getBalance();
592
772
  if (!res.ok) fail(res);
593
773
  const body = asRecord(res.body);
594
- printOrJson(jsonMode, body, [`Balance: ${body.token_balance ?? 0} tokens`]);
774
+ printOrJson(jsonMode, body, [`\u0411\u0430\u043B\u0430\u043D\u0441: ${body.token_balance ?? 0} \u0442\u043E\u043A\u0435\u043D\u043E\u0432`]);
595
775
  }
596
776
  async function cmdModels(kind, jsonMode) {
597
777
  const cfg = loadCliConfig();
@@ -599,7 +779,9 @@ async function cmdModels(kind, jsonMode) {
599
779
  if (cfg.apiKey) headers.Authorization = `Bearer ${cfg.apiKey}`;
600
780
  const res = await getJson(cfg.baseUrl, "/api/v1/models", headers);
601
781
  if (res.status >= 400) {
602
- throw new Error(String(res.json.error || `GET /api/v1/models failed (${res.status})`));
782
+ throw new Error(
783
+ String(res.json.error || `\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u043C\u043E\u0434\u0435\u043B\u0435\u0439 (${res.status})`)
784
+ );
603
785
  }
604
786
  if (jsonMode) {
605
787
  console.log(JSON.stringify(kind ? res.json[kind] ?? res.json : res.json, null, 2));
@@ -609,62 +791,363 @@ async function cmdModels(kind, jsonMode) {
609
791
  if (text) process.stdout.write(text.endsWith("\n") ? text : `${text}
610
792
  `);
611
793
  }
794
+ async function cmdModelsGet(slug, jsonMode) {
795
+ const cfg = loadCliConfig();
796
+ const headers = {};
797
+ if (cfg.apiKey) headers.Authorization = `Bearer ${cfg.apiKey}`;
798
+ const res = await getJson(cfg.baseUrl, "/api/v1/models", headers);
799
+ if (res.status >= 400) {
800
+ throw new Error(
801
+ String(res.json.error || `\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u043C\u043E\u0434\u0435\u043B\u0435\u0439 (${res.status})`)
802
+ );
803
+ }
804
+ const found = findModel(res.json, slug);
805
+ if (!found) throw new Error(`\u043C\u043E\u0434\u0435\u043B\u044C \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430: ${slug}`);
806
+ if (jsonMode) {
807
+ console.log(JSON.stringify(found.model, null, 2));
808
+ return;
809
+ }
810
+ const m = found.model;
811
+ const lines = [
812
+ `${found.kind}: ${String(m.slug || slug)}`,
813
+ m.display_name ? String(m.display_name) : "",
814
+ Array.isArray(m.tasks) ? `\u0437\u0430\u0434\u0430\u0447\u0438: ${m.tasks.join(", ")}` : "",
815
+ m.max_prompt_chars != null ? `\u043B\u0438\u043C\u0438\u0442 \u043F\u0440\u043E\u043C\u043F\u0442\u0430: ${m.max_prompt_chars}` : ""
816
+ ].filter(Boolean);
817
+ if (m.parameters && typeof m.parameters === "object") {
818
+ lines.push("\u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B:");
819
+ lines.push(JSON.stringify(m.parameters, null, 2));
820
+ }
821
+ printOrJson(false, found.model, lines);
822
+ }
823
+ async function cmdUpload(opts) {
824
+ const cfg = requireApiKey();
825
+ const client = createClient(cfg);
826
+ const projectId = opts.projectId ? parseProjectId(opts.projectId) : void 0;
827
+ const uploaded = await uploadLocal(
828
+ client,
829
+ opts.filePath,
830
+ projectId != null ? { projectId } : void 0
831
+ );
832
+ printOrJson(opts.json, { asset_id: uploaded.assetId, url: uploaded.url, kind: uploaded.kind }, [
833
+ uploaded.assetId || "(\u043D\u0435\u0442 asset_id)",
834
+ uploaded.url
835
+ ]);
836
+ }
837
+ async function cmdEstimate(opts) {
838
+ const cfg = requireApiKey();
839
+ const client = createClient(cfg);
840
+ const body = {
841
+ type: opts.type,
842
+ prompt: opts.prompt
843
+ };
844
+ if (opts.model) body.model = opts.model;
845
+ const res = await client.estimate(body);
846
+ if (!res.ok) fail(res);
847
+ const payload = asRecord(res.body);
848
+ printOrJson(opts.json, payload, [`\u041E\u0446\u0435\u043D\u043A\u0430: ${payload.cost_tokens ?? "?"} \u0442\u043E\u043A\u0435\u043D\u043E\u0432`]);
849
+ }
612
850
  async function cmdGenerateImage(opts) {
613
851
  const cfg = requireApiKey();
614
852
  const client = createClient(cfg);
853
+ const projectId = opts.projectId ? parseProjectId(opts.projectId) : void 0;
615
854
  const inputUrls = [];
616
- for (const path of opts.input ?? []) {
617
- inputUrls.push(await uploadLocal(client, path));
855
+ for (const ref of opts.input ?? []) {
856
+ inputUrls.push(
857
+ await resolveInputRef(client, ref, projectId != null ? { projectId } : void 0)
858
+ );
618
859
  }
619
860
  const res = await client.generateImage({
620
861
  prompt: opts.prompt,
621
862
  ...opts.model ? { model: opts.model } : {},
622
- ...inputUrls.length ? { input_urls: inputUrls } : {}
863
+ ...inputUrls.length ? { input_urls: inputUrls } : {},
864
+ ...projectId != null ? { project_id: projectId } : {}
623
865
  });
624
866
  if (!res.ok) fail(res);
625
867
  const body = asRecord(res.body);
626
868
  const taskId = String(body.task_id || body.id || "");
627
869
  if (!opts.wait) {
628
- printOrJson(opts.json, body, [`Submitted ${taskId || "(no task_id)"}`]);
870
+ printOrJson(opts.json, body, [`\u041E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043E ${taskId || "(\u043D\u0435\u0442 task_id)"}`]);
629
871
  return;
630
872
  }
631
- if (!taskId) throw new Error("submit returned no task_id");
632
- const done = await waitForTask(client, taskId, opts.json);
633
- const urls = mediaUrls(done);
634
- const saved = urls.length ? await downloadAll(urls, opts.out) : [];
635
- printOrJson(opts.json, { ...done, saved }, [
636
- `Completed ${taskId}`,
637
- ...saved.map((p) => `Saved ${p}`),
638
- ...!saved.length ? urls.map((u) => u) : []
639
- ]);
873
+ if (!taskId) throw new Error("\u0441\u0435\u0440\u0432\u0435\u0440 \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B task_id");
874
+ await finishAndSave(client, taskId, {
875
+ json: opts.json,
876
+ out: opts.out,
877
+ open: opts.open,
878
+ timeoutMs: parseDurationMs(opts.waitTimeout, 20 * 60 * 1e3)
879
+ });
640
880
  }
641
881
  async function cmdGenerateVideo(opts) {
642
882
  const cfg = requireApiKey();
643
883
  const client = createClient(cfg);
884
+ const projectId = opts.projectId ? parseProjectId(opts.projectId) : void 0;
644
885
  const body = { model: opts.model, prompt: opts.prompt };
645
- if (opts.startImage) body.start_image_url = await uploadLocal(client, opts.startImage);
646
- if (opts.endImage) body.end_image_url = await uploadLocal(client, opts.endImage);
886
+ if (projectId != null) body.project_id = projectId;
887
+ if (opts.startImage) {
888
+ body.start_image_url = await resolveInputRef(
889
+ client,
890
+ opts.startImage,
891
+ projectId != null ? { projectId } : void 0
892
+ );
893
+ }
894
+ if (opts.endImage) {
895
+ body.end_image_url = await resolveInputRef(
896
+ client,
897
+ opts.endImage,
898
+ projectId != null ? { projectId } : void 0
899
+ );
900
+ }
647
901
  const res = await client.generateVideo(body);
648
902
  if (!res.ok) fail(res);
649
903
  const submitted = asRecord(res.body);
650
904
  const taskId = String(submitted.task_id || submitted.id || "");
651
905
  if (!opts.wait) {
652
- printOrJson(opts.json, submitted, [`Submitted ${taskId || "(no task_id)"}`]);
906
+ printOrJson(opts.json, submitted, [`\u041E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043E ${taskId || "(\u043D\u0435\u0442 task_id)"}`]);
653
907
  return;
654
908
  }
655
- if (!taskId) throw new Error("submit returned no task_id");
656
- const done = await waitForTask(client, taskId, opts.json);
657
- const urls = mediaUrls(done);
658
- const saved = urls.length ? await downloadAll(urls, opts.out) : [];
659
- printOrJson(opts.json, { ...done, saved }, [
660
- `Completed ${taskId}`,
661
- ...saved.map((p) => `Saved ${p}`),
662
- ...!saved.length ? urls.map((u) => u) : []
663
- ]);
909
+ if (!taskId) throw new Error("\u0441\u0435\u0440\u0432\u0435\u0440 \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B task_id");
910
+ await finishAndSave(client, taskId, {
911
+ json: opts.json,
912
+ out: opts.out,
913
+ open: opts.open,
914
+ timeoutMs: parseDurationMs(opts.waitTimeout, 20 * 60 * 1e3)
915
+ });
916
+ }
917
+ async function cmdGenerateGet(taskId, jsonMode) {
918
+ const cfg = requireApiKey();
919
+ const client = createClient(cfg);
920
+ const res = await client.getTask(taskId);
921
+ if (!res.ok) fail(res);
922
+ const body = asRecord(res.body);
923
+ printOrJson(jsonMode, body, [`${taskId} ${formatListStatus(body)}`, ...mediaHint(body)]);
924
+ }
925
+ async function cmdGenerateWait(opts) {
926
+ const cfg = requireApiKey();
927
+ const client = createClient(cfg);
928
+ await finishAndSave(client, opts.taskId, {
929
+ json: opts.json,
930
+ out: opts.out,
931
+ open: opts.open,
932
+ timeoutMs: parseDurationMs(opts.waitTimeout, 20 * 60 * 1e3)
933
+ });
934
+ }
935
+ async function cmdGenerateCancel(taskId, jsonMode) {
936
+ const cfg = requireApiKey();
937
+ const client = createClient(cfg);
938
+ const kind = inferJobKind(taskId);
939
+ const res = kind === "image" ? await client.cancelImageGeneration(taskId) : kind === "video" ? await client.cancelVideoGeneration(taskId) : kind === "upscale" ? await client.cancelUpscaleGeneration(taskId) : kind === "mesh" ? await client.cancelMeshGeneration(taskId) : kind === "switchx" ? await client.cancelSwitchxGeneration(taskId) : null;
940
+ if (!res) {
941
+ throw new Error(`\u043D\u0435 \u0443\u043C\u0435\u044E \u043E\u0442\u043C\u0435\u043D\u044F\u0442\u044C \u044D\u0442\u043E\u0442 \u0442\u0438\u043F \u0437\u0430\u0434\u0430\u0447\u0438 (${kind || "unknown"})`);
942
+ }
943
+ if (!res.ok) fail(res);
944
+ printOrJson(jsonMode, res.body, [`\u041E\u0442\u043C\u0435\u043D\u0435\u043D\u043E ${taskId}`]);
945
+ }
946
+ async function cmdGenerateList(opts) {
947
+ const cfg = requireApiKey();
948
+ const client = createClient(cfg);
949
+ const params = {
950
+ limit: opts.limit,
951
+ ...opts.cursor ? { cursor: opts.cursor } : {}
952
+ };
953
+ const res = opts.kind === "video" ? await client.listVideoGenerations(params) : await client.listImageGenerations(params);
954
+ if (!res.ok) fail(res);
955
+ const body = asRecord(res.body);
956
+ const items = Array.isArray(body.items) ? body.items : [];
957
+ const lines = items.length ? items.map((raw) => {
958
+ const item = asRecord(raw);
959
+ return `${String(item.id || "")} ${formatStatus(item.status)} ${String(item.model || "")}`;
960
+ }) : ["\u043F\u0443\u0441\u0442\u043E"];
961
+ printOrJson(opts.json, body, [...lines, ...nextPageLines(body)]);
962
+ }
963
+ function formatListStatus(body) {
964
+ const status = formatStatus(body.status || (body.done ? "completed" : "queued"));
965
+ const progress = body.progress != null ? ` ${body.progress}%` : "";
966
+ return `${status}${progress}`;
967
+ }
968
+ function mediaHint(body) {
969
+ const images = Array.isArray(body.images) ? body.images : [];
970
+ const videos = Array.isArray(body.videos) ? body.videos : [];
971
+ const urls = [...images, ...videos].map((item) => item && typeof item === "object" ? String(asRecord(item).url || "") : "").filter(Boolean);
972
+ const media = Array.isArray(body.media_urls) ? body.media_urls.map((u) => String(u || "")) : [];
973
+ return [...urls, ...media].filter(Boolean);
974
+ }
975
+
976
+ // src/library.ts
977
+ function itemsOf(body) {
978
+ return Array.isArray(body.items) ? body.items.map((raw) => asRecord(raw)) : [];
979
+ }
980
+ async function cmdAssetsList(opts) {
981
+ const cfg = requireApiKey();
982
+ const client = createClient(cfg);
983
+ const res = await client.listAssets({
984
+ limit: opts.limit,
985
+ ...opts.kind ? { kind: opts.kind } : {},
986
+ ...opts.scope ? { scope: opts.scope } : {},
987
+ ...opts.projectId ? { project_id: parseProjectId(opts.projectId) } : {},
988
+ ...opts.query ? { q: opts.query } : {},
989
+ ...opts.cursor ? { cursor: opts.cursor } : {}
990
+ });
991
+ if (!res.ok) fail(res);
992
+ const body = asRecord(res.body);
993
+ const items = itemsOf(body);
994
+ const lines = items.length ? items.map((item) => {
995
+ const id = String(item.asset_id || "");
996
+ const kind = String(item.kind || "");
997
+ const scope = String(item.scope || "");
998
+ const project = item.project_id != null ? ` project:${item.project_id}` : "";
999
+ const name = String(item.filename || item.title || "");
1000
+ return `${id} ${kind} ${scope}${project} ${name}`.trim();
1001
+ }) : ["\u043F\u0443\u0441\u0442\u043E"];
1002
+ printOrJson(opts.json, body, [...lines, ...nextPageLines(body)]);
1003
+ }
1004
+ async function cmdAssetsGet(assetId, json) {
1005
+ const id = assetId.trim();
1006
+ if (!isAssetId(id)) throw new Error(`\u043D\u0435 \u043F\u043E\u0445\u043E\u0436\u0435 \u043D\u0430 asset id: ${assetId} (\u043E\u0436\u0438\u0434\u0430\u044E ast_\u2026)`);
1007
+ const cfg = requireApiKey();
1008
+ const client = createClient(cfg);
1009
+ const res = await client.getAsset(id);
1010
+ if (!res.ok) fail(res);
1011
+ const body = asRecord(res.body);
1012
+ printOrJson(
1013
+ json,
1014
+ body,
1015
+ [
1016
+ String(body.asset_id || id),
1017
+ [body.kind, body.scope, body.filename].filter(Boolean).join(" "),
1018
+ String(body.url || "")
1019
+ ].filter(Boolean)
1020
+ );
1021
+ }
1022
+ async function cmdAssetsCopy(opts) {
1023
+ const id = opts.assetId.trim();
1024
+ if (!isAssetId(id)) throw new Error(`\u043D\u0435 \u043F\u043E\u0445\u043E\u0436\u0435 \u043D\u0430 asset id: ${opts.assetId} (\u043E\u0436\u0438\u0434\u0430\u044E ast_\u2026)`);
1025
+ if (opts.personal === Boolean(opts.projectId)) {
1026
+ throw new Error("\u0443\u043A\u0430\u0436\u0438\u0442\u0435 --project <id> \u0438\u043B\u0438 --personal");
1027
+ }
1028
+ const cfg = requireApiKey();
1029
+ const client = createClient(cfg);
1030
+ const res = await client.copyAsset(
1031
+ id,
1032
+ opts.personal ? { target: "personal" } : { target: "project", project_id: parseProjectId(String(opts.projectId)) }
1033
+ );
1034
+ if (!res.ok) fail(res);
1035
+ const body = asRecord(res.body);
1036
+ const copied = String(body.asset_id || "");
1037
+ printOrJson(
1038
+ opts.json,
1039
+ body,
1040
+ [
1041
+ copied ? `\u0421\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D\u043E ${copied}` : "\u0421\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D\u043E",
1042
+ body.scope ? String(body.scope) : "",
1043
+ body.project_id != null ? `project:${body.project_id}` : ""
1044
+ ].filter(Boolean)
1045
+ );
1046
+ }
1047
+ async function cmdProjectsList(opts) {
1048
+ const cfg = requireApiKey();
1049
+ const client = createClient(cfg);
1050
+ const res = await client.listProjects({
1051
+ limit: opts.limit,
1052
+ ...opts.cursor ? { cursor: opts.cursor } : {}
1053
+ });
1054
+ if (!res.ok) fail(res);
1055
+ const body = asRecord(res.body);
1056
+ const items = itemsOf(body);
1057
+ const lines = items.length ? items.map((item) => {
1058
+ const hub = item.is_personal_generations_hub ? " \u043B\u0438\u0447\u043D\u044B\u0435" : "";
1059
+ return `${item.id} ${item.name || ""} ${item.role || ""}${hub}`.trim();
1060
+ }) : ["\u043F\u0443\u0441\u0442\u043E"];
1061
+ printOrJson(opts.json, body, [...lines, ...nextPageLines(body)]);
1062
+ }
1063
+ async function cmdElementsList(opts) {
1064
+ const projectId = parseProjectId(opts.projectId);
1065
+ const cfg = requireApiKey();
1066
+ const client = createClient(cfg);
1067
+ const res = await client.listProjectElements(projectId, {
1068
+ limit: opts.limit,
1069
+ ...opts.kinds ? { kinds: opts.kinds } : {},
1070
+ ...opts.cursor ? { cursor: opts.cursor } : {}
1071
+ });
1072
+ if (!res.ok) fail(res);
1073
+ const body = asRecord(res.body);
1074
+ const items = itemsOf(body);
1075
+ const lines = items.length ? items.map((item) => {
1076
+ const mention = String(item.mention || (item.name ? `@${item.name}` : ""));
1077
+ return `${mention} ${item.kind || ""} ${item.id || ""}`.trim();
1078
+ }) : ["\u043F\u0443\u0441\u0442\u043E"];
1079
+ printOrJson(opts.json, body, [...lines, ...nextPageLines(body)]);
1080
+ }
1081
+ async function ensureProjectAsset(client, assetId, projectId) {
1082
+ const res = await client.getAsset(assetId);
1083
+ if (!res.ok) fail(res);
1084
+ const body = asRecord(res.body);
1085
+ if (String(body.scope) === "project" && Number(body.project_id) === projectId) {
1086
+ return assetId;
1087
+ }
1088
+ const copied = await client.copyAsset(assetId, { target: "project", project_id: projectId });
1089
+ if (!copied.ok) fail(copied);
1090
+ const newId = String(asRecord(copied.body).asset_id || "").trim();
1091
+ if (!newId) throw new Error("copy \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B asset_id");
1092
+ return newId;
1093
+ }
1094
+ async function cmdElementsCreate(opts) {
1095
+ const sources = [opts.from, opts.file, opts.url].filter(Boolean);
1096
+ if (sources.length !== 1) {
1097
+ throw new Error("\u0443\u043A\u0430\u0436\u0438\u0442\u0435 \u0440\u043E\u0432\u043D\u043E \u043E\u0434\u0438\u043D \u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A: --from ast_\u2026, --file \u0438\u043B\u0438 --url");
1098
+ }
1099
+ const name = opts.name.replace(/^@/, "").trim();
1100
+ if (!name) throw new Error("\u0443\u043A\u0430\u0436\u0438\u0442\u0435 --name \u0431\u0435\u0437 \u043F\u0443\u0441\u0442\u043E\u0433\u043E \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F");
1101
+ const projectId = parseProjectId(opts.projectId);
1102
+ const cfg = requireApiKey();
1103
+ const client = createClient(cfg);
1104
+ let assetId = "";
1105
+ let imageUrl = "";
1106
+ let kind = String(opts.kind || "").toLowerCase();
1107
+ if (opts.file) {
1108
+ const uploaded = await uploadLocal(client, opts.file, { projectId });
1109
+ assetId = uploaded.assetId;
1110
+ if (!assetId) throw new Error("\u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B\u0430 asset_id");
1111
+ if (!kind) kind = uploaded.kind || "image";
1112
+ } else if (opts.from) {
1113
+ const from = opts.from.trim();
1114
+ if (!isAssetId(from)) throw new Error(`\u043D\u0435 \u043F\u043E\u0445\u043E\u0436\u0435 \u043D\u0430 asset id: ${opts.from} (\u043E\u0436\u0438\u0434\u0430\u044E ast_\u2026)`);
1115
+ assetId = await ensureProjectAsset(client, from, projectId);
1116
+ if (!kind) kind = "image";
1117
+ } else {
1118
+ imageUrl = String(opts.url || "").trim();
1119
+ if (!/^https?:\/\//i.test(imageUrl)) throw new Error("--url \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C http(s)");
1120
+ if (!kind) kind = "image";
1121
+ }
1122
+ if (kind !== "image" && kind !== "video") {
1123
+ throw new Error("kind: image \u0438\u043B\u0438 video");
1124
+ }
1125
+ const body = { name, kind };
1126
+ if (assetId) body.asset_id = assetId;
1127
+ else if (kind === "video") {
1128
+ body.media_json = { video_url: imageUrl };
1129
+ body.preview_url = imageUrl;
1130
+ } else {
1131
+ body.media_json = { images: [imageUrl] };
1132
+ body.preview_url = imageUrl;
1133
+ }
1134
+ const res = await client.createProjectElement(projectId, body);
1135
+ if (!res.ok) fail(res);
1136
+ const created = asRecord(asRecord(res.body).item ?? res.body);
1137
+ const mention = String(created.mention || (created.name ? `@${created.name}` : `@${name}`));
1138
+ printOrJson(
1139
+ opts.json,
1140
+ res.body,
1141
+ [
1142
+ `\u042D\u043B\u0435\u043C\u0435\u043D\u0442 ${mention}`,
1143
+ created.id ? String(created.id) : "",
1144
+ assetId ? assetId : imageUrl
1145
+ ].filter(Boolean)
1146
+ );
664
1147
  }
665
1148
 
666
1149
  // src/updateCheck.ts
667
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
1150
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
668
1151
  import { dirname as dirname2, join as join2 } from "node:path";
669
1152
 
670
1153
  // src/version.ts
@@ -684,11 +1167,11 @@ function parse(raw) {
684
1167
  return [Number(m[1]), Number(m[2]), Number(m[3])];
685
1168
  }
686
1169
  function cliVersion() {
687
- return String("0.1.0");
1170
+ return String("0.1.2");
688
1171
  }
689
1172
  function updateAvailableMessage(local, latest) {
690
- return `A new version of @artillect/cli is available (${latest}; you have ${local}).
691
- Update: npm i -g @artillect/cli@latest`;
1173
+ return `\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u0430 \u043D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F @artillect/cli (${latest}, \u0443 \u0432\u0430\u0441 ${local}).
1174
+ \u041E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435: npm i -g @artillect/cli@latest`;
692
1175
  }
693
1176
 
694
1177
  // src/updateCheck.ts
@@ -711,7 +1194,7 @@ function readLastCheck() {
711
1194
  }
712
1195
  function writeLastCheck() {
713
1196
  try {
714
- mkdirSync2(dirname2(cachePath()), { recursive: true });
1197
+ mkdirSync3(dirname2(cachePath()), { recursive: true });
715
1198
  writeFileSync2(cachePath(), `${JSON.stringify({ checkedAt: Date.now() })}
716
1199
  `);
717
1200
  } catch {
@@ -739,37 +1222,75 @@ async function maybeNotifyUpdate() {
739
1222
 
740
1223
  // src/index.ts
741
1224
  var program = new Command();
742
- program.name("artillect").description(
743
- "Artillect CLI \u2014 same Public API v1 as Studio, REST, and MCP. Models come from GET /api/v1/models."
744
- ).version(cliVersion());
1225
+ program.name("artillect").description("Artillect CLI \u2014 \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u044F \u043A\u0430\u0440\u0442\u0438\u043D\u043E\u043A \u0438 \u0432\u0438\u0434\u0435\u043E \u0438\u0437 \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0430.").version(cliVersion());
1226
+ program.showHelpAfterError();
1227
+ program.showSuggestionAfterError();
1228
+ program.addHelpText("after", "\n\u0414\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\u0446\u0438\u044F: https://app.artillect.pro/docs/cli\n");
745
1229
  program.hook("preAction", async () => {
746
1230
  await maybeNotifyUpdate();
747
1231
  });
748
- var auth = program.command("auth").description("Device-flow login (or ARTILLECT_API_KEY)");
749
- auth.command("login").description("Open /connect, poll until an API key is issued").action(authLogin);
750
- auth.command("logout").description("Remove the saved API key").action(authLogout);
751
- auth.command("status").description("Show who the current key belongs to").action(authStatus);
752
- program.command("balance").description("Show token balance").option("--json", "print raw JSON", false).action(async (opts) => {
1232
+ var auth = program.command("auth").description("\u0412\u0445\u043E\u0434 (\u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u0438\u043B\u0438 ARTILLECT_API_KEY)");
1233
+ auth.command("login").description("\u0412\u043E\u0439\u0442\u0438 \u0447\u0435\u0440\u0435\u0437 \u0431\u0440\u0430\u0443\u0437\u0435\u0440").action(authLogin);
1234
+ auth.command("logout").description("\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u0439 \u043A\u043B\u044E\u0447").action(authLogout);
1235
+ auth.command("status").description("\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C, \u043F\u043E\u0434 \u043A\u0435\u043C \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D \u0432\u0445\u043E\u0434").action(authStatus);
1236
+ program.command("balance").description("\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0431\u0430\u043B\u0430\u043D\u0441 \u0442\u043E\u043A\u0435\u043D\u043E\u0432").option("--json", "\u0432\u044B\u0432\u0435\u0441\u0442\u0438 \u0441\u044B\u0440\u043E\u0439 JSON", false).action(async (opts) => {
753
1237
  await cmdBalance(Boolean(opts.json));
754
1238
  });
755
- var models = program.command("models").description("Discovery from GET /api/v1/models (no local catalog)");
756
- models.command("list").description("List public model slugs").option("-k, --kind <kind>", "images | video | chat | audio | upscale | mesh").option("--json", "print raw JSON", false).action(async (opts) => {
1239
+ var models = program.command("models").description("\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u043C\u043E\u0434\u0435\u043B\u0435\u0439 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430");
1240
+ models.command("list").description("\u0421\u043F\u0438\u0441\u043E\u043A \u043C\u043E\u0434\u0435\u043B\u0435\u0439").option("-k, --kind <kind>", "images | video | chat | audio | upscale | mesh").option("--json", "\u0432\u044B\u0432\u0435\u0441\u0442\u0438 \u0441\u044B\u0440\u043E\u0439 JSON", false).action(async (opts) => {
757
1241
  await cmdModels(opts.kind, Boolean(opts.json));
758
1242
  });
759
- var generate = program.command("generate").description("Submit a generation");
760
- generate.command("image").description("Text-to-image or image edit").requiredOption("-p, --prompt <text>", "prompt").option("-m, --model <slug>", "public image slug (default: gpt-image-2)").option("-i, --input <file...>", "local image files to upload as input_urls").option("-w, --wait", "poll until complete and download results", false).option("-o, --out <dir>", "directory for downloads", ".").option("--json", "print raw JSON", false).action(
1243
+ models.command("get").description("\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B \u043E\u0434\u043D\u043E\u0439 \u043C\u043E\u0434\u0435\u043B\u0438").argument("<slug>", "\u0441\u043B\u0430\u0433, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 gpt-image-2").option("--json", "\u0432\u044B\u0432\u0435\u0441\u0442\u0438 \u0441\u044B\u0440\u043E\u0439 JSON", false).action(async (slug, opts) => {
1244
+ await cmdModelsGet(slug, Boolean(opts.json));
1245
+ });
1246
+ program.command("upload").description("\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u0432 storage \u0438 \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C ast_\u2026").argument("<file>", "\u043F\u0443\u0442\u044C \u043A \u0444\u0430\u0439\u043B\u0443").option("--project <id>", "\u043F\u043E\u043B\u043E\u0436\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u0432 storage \u043F\u0440\u043E\u0435\u043A\u0442\u0430").option("--json", "\u0432\u044B\u0432\u0435\u0441\u0442\u0438 \u0441\u044B\u0440\u043E\u0439 JSON", false).action(async (file, opts) => {
1247
+ await cmdUpload({
1248
+ filePath: file,
1249
+ projectId: opts.project,
1250
+ json: Boolean(opts.json)
1251
+ });
1252
+ });
1253
+ program.command("estimate").description("\u041E\u0446\u0435\u043D\u0438\u0442\u044C \u0446\u0435\u043D\u0443 \u0431\u0435\u0437 \u0441\u043F\u0438\u0441\u0430\u043D\u0438\u044F").argument("<type>", "image | video").requiredOption("-p, --prompt <text>", "\u043F\u0440\u043E\u043C\u043F\u0442").option("-m, --model <slug>", "\u0441\u043B\u0430\u0433 \u043C\u043E\u0434\u0435\u043B\u0438").option("--json", "\u0432\u044B\u0432\u0435\u0441\u0442\u0438 \u0441\u044B\u0440\u043E\u0439 JSON", false).action(async (type, opts) => {
1254
+ const kind = type === "video" ? "video" : type === "image" ? "image" : null;
1255
+ if (!kind) throw new Error("type: image \u0438\u043B\u0438 video");
1256
+ await cmdEstimate({
1257
+ type: kind,
1258
+ model: opts.model,
1259
+ prompt: opts.prompt,
1260
+ json: Boolean(opts.json)
1261
+ });
1262
+ });
1263
+ var generate = program.command("generate").description("\u0417\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0438\u043B\u0438 \u0441\u043C\u043E\u0442\u0440\u0435\u0442\u044C \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u044E");
1264
+ generate.command("image").description("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043F\u043E \u0442\u0435\u043A\u0441\u0442\u0443 \u0438\u043B\u0438 \u043F\u0440\u0430\u0432\u043A\u0430 \u043F\u043E \u0440\u0435\u0444\u0435\u0440\u0435\u043D\u0441\u0430\u043C").requiredOption("-p, --prompt <text>", "\u043F\u0440\u043E\u043C\u043F\u0442").option("-m, --model <slug>", "\u0441\u043B\u0430\u0433 \u043C\u043E\u0434\u0435\u043B\u0438 (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E gpt-image-2)").option("-i, --input <file...>", "\u0440\u0435\u0444\u0435\u0440\u0435\u043D\u0441\u044B: \u0444\u0430\u0439\u043B, url \u0438\u043B\u0438 ast_\u2026").option("--project <id>", "\u043F\u0440\u043E\u0435\u043A\u0442 (\u0434\u043B\u044F @\u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432 \u0438 \u043B\u0435\u043D\u0442\u044B)").option("-w, --wait", "\u0434\u043E\u0436\u0434\u0430\u0442\u044C\u0441\u044F \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438 \u0438 \u0441\u043A\u0430\u0447\u0430\u0442\u044C \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442", false).option("--wait-timeout <duration>", "\u043B\u0438\u043C\u0438\u0442 \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 10m", "20m").option("-o, --out <dir>", "\u043F\u0430\u043F\u043A\u0430 \u0434\u043B\u044F \u0444\u0430\u0439\u043B\u043E\u0432", ".").option("--open", "\u043E\u0442\u043A\u0440\u044B\u0442\u044C \u0444\u0430\u0439\u043B \u043F\u043E\u0441\u043B\u0435 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F", false).option("--json", "\u0432\u044B\u0432\u0435\u0441\u0442\u0438 \u0441\u044B\u0440\u043E\u0439 JSON", false).addHelpText(
1265
+ "after",
1266
+ `
1267
+ \u041F\u0440\u0438\u043C\u0435\u0440\u044B:
1268
+ $ artillect generate image -p "\u043A\u043E\u0442 \u0432 \u043A\u043E\u0441\u043C\u043E\u0441\u0435" --wait --open
1269
+ $ artillect generate image -m seedream-5-pro -p "\u043A\u043E\u0442" -i ./ref.png --wait
1270
+ `
1271
+ ).action(
761
1272
  async (opts) => {
762
1273
  await cmdGenerateImage({
763
1274
  prompt: opts.prompt,
764
1275
  model: opts.model,
765
1276
  input: opts.input,
1277
+ projectId: opts.project,
766
1278
  wait: Boolean(opts.wait),
1279
+ waitTimeout: opts.waitTimeout,
767
1280
  json: Boolean(opts.json),
768
- out: opts.out
1281
+ out: opts.out,
1282
+ open: Boolean(opts.open)
769
1283
  });
770
1284
  }
771
1285
  );
772
- generate.command("video").description("Text/image-to-video").argument("[model]", "public video slug (default: kling-3-turbo)").requiredOption("-p, --prompt <text>", "prompt").option("-m, --model <slug>", "public video slug (overrides the positional model)").option("--start-image <file>", "local start-frame image").option("--end-image <file>", "local end-frame image").option("-w, --wait", "poll until complete and download results", false).option("-o, --out <dir>", "directory for downloads", ".").option("--json", "print raw JSON", false).action(
1286
+ generate.command("video").description("\u0412\u0438\u0434\u0435\u043E \u043F\u043E \u0442\u0435\u043A\u0441\u0442\u0443 \u0438\u043B\u0438 \u043A\u0430\u0434\u0440\u0430\u043C").argument("[model]", "\u0441\u043B\u0430\u0433 \u0432\u0438\u0434\u0435\u043E\u043C\u043E\u0434\u0435\u043B\u0438 (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E kling-3-turbo)").requiredOption("-p, --prompt <text>", "\u043F\u0440\u043E\u043C\u043F\u0442").option("-m, --model <slug>", "\u0441\u043B\u0430\u0433 \u043C\u043E\u0434\u0435\u043B\u0438 (\u0435\u0441\u043B\u0438 \u043D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D \u0430\u0440\u0433\u0443\u043C\u0435\u043D\u0442\u043E\u043C)").option("--start-image <ref>", "\u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0439 \u043A\u0430\u0434\u0440: \u0444\u0430\u0439\u043B, url \u0438\u043B\u0438 ast_\u2026").option("--end-image <ref>", "\u0444\u0438\u043D\u0430\u043B\u044C\u043D\u044B\u0439 \u043A\u0430\u0434\u0440: \u0444\u0430\u0439\u043B, url \u0438\u043B\u0438 ast_\u2026").option("--project <id>", "\u043F\u0440\u043E\u0435\u043A\u0442 (\u043D\u0443\u0436\u0435\u043D \u0434\u043B\u044F @\u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432)").option("-w, --wait", "\u0434\u043E\u0436\u0434\u0430\u0442\u044C\u0441\u044F \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438 \u0438 \u0441\u043A\u0430\u0447\u0430\u0442\u044C \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442", false).option("--wait-timeout <duration>", "\u043B\u0438\u043C\u0438\u0442 \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 10m", "20m").option("-o, --out <dir>", "\u043F\u0430\u043F\u043A\u0430 \u0434\u043B\u044F \u0444\u0430\u0439\u043B\u043E\u0432", ".").option("--open", "\u043E\u0442\u043A\u0440\u044B\u0442\u044C \u0444\u0430\u0439\u043B \u043F\u043E\u0441\u043B\u0435 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F", false).option("--json", "\u0432\u044B\u0432\u0435\u0441\u0442\u0438 \u0441\u044B\u0440\u043E\u0439 JSON", false).addHelpText(
1287
+ "after",
1288
+ `
1289
+ \u041F\u0440\u0438\u043C\u0435\u0440\u044B:
1290
+ $ artillect generate video grok-imagine-video -p "\u043A\u043E\u0442 \u0438\u0434\u0451\u0442" --wait --open
1291
+ $ artillect generate video kling-3 -p "@Hero \u0438\u0434\u0451\u0442" --project 12 --start-image ast_\u2026 --wait
1292
+ `
1293
+ ).action(
773
1294
  async (positionalModel, opts) => {
774
1295
  const model = String(opts.model || positionalModel || "kling-3-turbo").trim();
775
1296
  await cmdGenerateVideo({
@@ -777,9 +1298,98 @@ generate.command("video").description("Text/image-to-video").argument("[model]",
777
1298
  prompt: opts.prompt,
778
1299
  startImage: opts.startImage,
779
1300
  endImage: opts.endImage,
1301
+ projectId: opts.project,
780
1302
  wait: Boolean(opts.wait),
1303
+ waitTimeout: opts.waitTimeout,
781
1304
  json: Boolean(opts.json),
782
- out: opts.out
1305
+ out: opts.out,
1306
+ open: Boolean(opts.open)
1307
+ });
1308
+ }
1309
+ );
1310
+ generate.command("get").description("\u0421\u0442\u0430\u0442\u0443\u0441 \u0437\u0430\u0434\u0430\u0447\u0438").argument("<task_id>", "id \u0438\u0437 generate").option("--json", "\u0432\u044B\u0432\u0435\u0441\u0442\u0438 \u0441\u044B\u0440\u043E\u0439 JSON", false).action(async (taskId, opts) => {
1311
+ await cmdGenerateGet(taskId, Boolean(opts.json));
1312
+ });
1313
+ generate.command("wait").description("\u0414\u043E\u0436\u0434\u0430\u0442\u044C\u0441\u044F \u0437\u0430\u0434\u0430\u0447\u0438 \u0438 \u0441\u043A\u0430\u0447\u0430\u0442\u044C \u0444\u0430\u0439\u043B").argument("<task_id>", "id \u0438\u0437 generate").option("--wait-timeout <duration>", "\u043B\u0438\u043C\u0438\u0442 \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 10m", "20m").option("-o, --out <dir>", "\u043F\u0430\u043F\u043A\u0430 \u0434\u043B\u044F \u0444\u0430\u0439\u043B\u043E\u0432", ".").option("--open", "\u043E\u0442\u043A\u0440\u044B\u0442\u044C \u0444\u0430\u0439\u043B \u043F\u043E\u0441\u043B\u0435 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F", false).option("--json", "\u0432\u044B\u0432\u0435\u0441\u0442\u0438 \u0441\u044B\u0440\u043E\u0439 JSON", false).action(
1314
+ async (taskId, opts) => {
1315
+ await cmdGenerateWait({
1316
+ taskId,
1317
+ waitTimeout: opts.waitTimeout,
1318
+ out: opts.out,
1319
+ open: Boolean(opts.open),
1320
+ json: Boolean(opts.json)
1321
+ });
1322
+ }
1323
+ );
1324
+ generate.command("cancel").description("\u041E\u0442\u043C\u0435\u043D\u0438\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0443").argument("<task_id>", "id \u0438\u0437 generate").option("--json", "\u0432\u044B\u0432\u0435\u0441\u0442\u0438 \u0441\u044B\u0440\u043E\u0439 JSON", false).action(async (taskId, opts) => {
1325
+ await cmdGenerateCancel(taskId, Boolean(opts.json));
1326
+ });
1327
+ generate.command("list").description("\u041F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438").option("-k, --kind <kind>", "image | video", "image").option("--limit <n>", "\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0441\u0442\u0440\u043E\u043A", "20").option("--cursor <id>", "\u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430 \u043F\u043E\u0441\u043B\u0435 \u044D\u0442\u043E\u0433\u043E task_id").option("--json", "\u0432\u044B\u0432\u0435\u0441\u0442\u0438 \u0441\u044B\u0440\u043E\u0439 JSON", false).action(async (opts) => {
1328
+ const kind = opts.kind === "video" ? "video" : "image";
1329
+ await cmdGenerateList({
1330
+ kind,
1331
+ json: Boolean(opts.json),
1332
+ limit: parseLimit(opts.limit, 20),
1333
+ cursor: opts.cursor
1334
+ });
1335
+ });
1336
+ var assets = program.command("assets").description("Storage: ast_\u2026 \u0444\u0430\u0439\u043B\u044B \u0430\u043A\u043A\u0430\u0443\u043D\u0442\u0430 \u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u043E\u0432");
1337
+ assets.command("list").description("\u0421\u043F\u0438\u0441\u043E\u043A \u0444\u0430\u0439\u043B\u043E\u0432 \u0432 storage").option("-k, --kind <kind>", "image | video | audio | mesh").option("--scope <scope>", "all | personal | project").option("--project <id>", "\u0442\u043E\u043B\u044C\u043A\u043E storage \u043F\u0440\u043E\u0435\u043A\u0442\u0430").option("-q, --query <text>", "\u043F\u043E\u0438\u0441\u043A \u043F\u043E \u0438\u043C\u0435\u043D\u0438").option("--limit <n>", "\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0441\u0442\u0440\u043E\u043A", "20").option("--cursor <id>", "\u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430 \u043F\u043E\u0441\u043B\u0435 \u044D\u0442\u043E\u0433\u043E ast_\u2026").option("--json", "\u0432\u044B\u0432\u0435\u0441\u0442\u0438 \u0441\u044B\u0440\u043E\u0439 JSON", false).action(
1338
+ async (opts) => {
1339
+ await cmdAssetsList({
1340
+ kind: opts.kind,
1341
+ scope: opts.scope,
1342
+ projectId: opts.project,
1343
+ query: opts.query,
1344
+ cursor: opts.cursor,
1345
+ limit: parseLimit(opts.limit, 20),
1346
+ json: Boolean(opts.json)
1347
+ });
1348
+ }
1349
+ );
1350
+ assets.command("get").description("\u041C\u0435\u0442\u0430\u0434\u0430\u043D\u043D\u044B\u0435 \u0438 \u0441\u0432\u0435\u0436\u0438\u0439 url \u043F\u043E ast_\u2026").argument("<asset_id>", "ast_\u2026").option("--json", "\u0432\u044B\u0432\u0435\u0441\u0442\u0438 \u0441\u044B\u0440\u043E\u0439 JSON", false).action(async (assetId, opts) => {
1351
+ await cmdAssetsGet(assetId, Boolean(opts.json));
1352
+ });
1353
+ assets.command("copy").description("\u0421\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0444\u0430\u0439\u043B \u0432 \u043F\u0440\u043E\u0435\u043A\u0442 \u0438\u043B\u0438 \u0432 \u043B\u0438\u0447\u043D\u044B\u0439 storage").argument("<asset_id>", "ast_\u2026").option("--project <id>", "\u0446\u0435\u043B\u044C \u2014 storage \u043F\u0440\u043E\u0435\u043A\u0442\u0430").option("--personal", "\u0446\u0435\u043B\u044C \u2014 \u043B\u0438\u0447\u043D\u044B\u0439 storage", false).option("--json", "\u0432\u044B\u0432\u0435\u0441\u0442\u0438 \u0441\u044B\u0440\u043E\u0439 JSON", false).action(
1354
+ async (assetId, opts) => {
1355
+ await cmdAssetsCopy({
1356
+ assetId,
1357
+ projectId: opts.project,
1358
+ personal: Boolean(opts.personal),
1359
+ json: Boolean(opts.json)
1360
+ });
1361
+ }
1362
+ );
1363
+ var projects = program.command("projects").description("\u041F\u0440\u043E\u0435\u043A\u0442\u044B \u0430\u043A\u043A\u0430\u0443\u043D\u0442\u0430");
1364
+ projects.command("list").description("\u0421\u043F\u0438\u0441\u043E\u043A \u043F\u0440\u043E\u0435\u043A\u0442\u043E\u0432").option("--limit <n>", "\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0441\u0442\u0440\u043E\u043A", "50").option("--cursor <id>", "\u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430 \u043F\u043E\u0441\u043B\u0435 \u044D\u0442\u043E\u0433\u043E id").option("--json", "\u0432\u044B\u0432\u0435\u0441\u0442\u0438 \u0441\u044B\u0440\u043E\u0439 JSON", false).action(async (opts) => {
1365
+ await cmdProjectsList({
1366
+ limit: parseLimit(opts.limit, 50),
1367
+ cursor: opts.cursor,
1368
+ json: Boolean(opts.json)
1369
+ });
1370
+ });
1371
+ var elements = program.command("elements").description("Element library \u043F\u0440\u043E\u0435\u043A\u0442\u0430 (@Name)");
1372
+ elements.command("list").description("\u042D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043F\u0440\u043E\u0435\u043A\u0442\u0430").requiredOption("--project <id>", "id \u043F\u0440\u043E\u0435\u043A\u0442\u0430").option("--kinds <kinds>", "image,video").option("--limit <n>", "\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0441\u0442\u0440\u043E\u043A", "50").option("--cursor <id>", "\u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430 \u043F\u043E\u0441\u043B\u0435 \u044D\u0442\u043E\u0433\u043E id").option("--json", "\u0432\u044B\u0432\u0435\u0441\u0442\u0438 \u0441\u044B\u0440\u043E\u0439 JSON", false).action(
1373
+ async (opts) => {
1374
+ await cmdElementsList({
1375
+ projectId: opts.project,
1376
+ kinds: opts.kinds,
1377
+ cursor: opts.cursor,
1378
+ limit: parseLimit(opts.limit, 50),
1379
+ json: Boolean(opts.json)
1380
+ });
1381
+ }
1382
+ );
1383
+ elements.command("create").description("\u0421\u043E\u0437\u0434\u0430\u0442\u044C \u044D\u043B\u0435\u043C\u0435\u043D\u0442 @Name \u0438\u0437 ast_\u2026, \u0444\u0430\u0439\u043B\u0430 \u0438\u043B\u0438 url").requiredOption("--project <id>", "id \u043F\u0440\u043E\u0435\u043A\u0442\u0430").requiredOption("--name <name>", "\u0438\u043C\u044F \u0431\u0435\u0437 @").option("--from <asset_id>", "ast_\u2026 (\u043B\u0438\u0447\u043D\u044B\u0439 \u0441\u043A\u043E\u043F\u0438\u0440\u0443\u0435\u0442\u0441\u044F \u0432 \u043F\u0440\u043E\u0435\u043A\u0442)").option("--file <path>", "\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u0444\u0430\u0439\u043B").option("--url <https>", "\u043F\u0443\u0431\u043B\u0438\u0447\u043D\u044B\u0439 HTTPS").option("--kind <kind>", "image | video").option("--json", "\u0432\u044B\u0432\u0435\u0441\u0442\u0438 \u0441\u044B\u0440\u043E\u0439 JSON", false).action(
1384
+ async (opts) => {
1385
+ await cmdElementsCreate({
1386
+ projectId: opts.project,
1387
+ name: opts.name,
1388
+ from: opts.from,
1389
+ file: opts.file,
1390
+ url: opts.url,
1391
+ kind: opts.kind,
1392
+ json: Boolean(opts.json)
783
1393
  });
784
1394
  }
785
1395
  );
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@artillect/cli",
3
- "version": "0.1.0",
4
- "description": "Artillect CLI — generate images and video via Public API v1.",
3
+ "version": "0.1.2",
4
+ "description": "Artillect CLI — генерация картинок и видео из терминала.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
7
7
  "bin": {
@@ -14,12 +14,7 @@
14
14
  "publishConfig": {
15
15
  "access": "public"
16
16
  },
17
- "repository": {
18
- "type": "git",
19
- "url": "https://git.krem.digital/diffusionbooth/artillect.git",
20
- "directory": "packages/artillect-cli"
21
- },
22
- "homepage": "https://app.artillect.pro/docs",
17
+ "homepage": "https://app.artillect.pro/docs/cli",
23
18
  "bugs": {
24
19
  "url": "https://app.artillect.pro/feedback"
25
20
  },
@@ -33,19 +28,20 @@
33
28
  "engines": {
34
29
  "node": ">=20"
35
30
  },
31
+ "scripts": {
32
+ "build": "node build.mjs",
33
+ "prepack": "node build.mjs",
34
+ "typecheck": "tsc -p tsconfig.json --noEmit",
35
+ "pack:check": "node scripts/pack-check.mjs",
36
+ "smoke": "node scripts/smoke.mjs"
37
+ },
36
38
  "dependencies": {
37
39
  "commander": "^14.0.0"
38
40
  },
39
41
  "devDependencies": {
42
+ "@artillect/sdk": "workspace:*",
40
43
  "@types/node": "^22.15.0",
41
44
  "esbuild": "^0.25.0",
42
- "typescript": "^5.8.0",
43
- "@artillect/sdk": "0.1.0"
44
- },
45
- "scripts": {
46
- "build": "node build.mjs",
47
- "typecheck": "tsc -p tsconfig.json --noEmit",
48
- "pack:check": "node scripts/pack-check.mjs",
49
- "smoke": "node scripts/smoke.mjs"
45
+ "typescript": "^5.8.0"
50
46
  }
51
- }
47
+ }