@yandy0725/mediagen-cli 0.2.0 → 0.3.1
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/README.md +20 -3
- package/README.zh-CN.md +16 -2
- package/dist/cli.js +158 -11
- package/package.json +7 -1
- package/skills/mediagen/SKILL.md +9 -0
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ Node >= 18, two runtime dependencies, a DashScope and/or Ark API key.
|
|
|
39
39
|
| `mediagen login` / `logout` / `auth status` | store, remove, inspect API keys |
|
|
40
40
|
| `mediagen config show` / `config path` | resolved config, and where it lives |
|
|
41
41
|
| `mediagen config add <name>` | create a provider in config.toml; everything else is a manual edit |
|
|
42
|
+
| `mediagen skill install --dir <path>` | copy the packaged agent skill into `<path>/mediagen/`; re-run to replace it |
|
|
42
43
|
|
|
43
44
|
`mediagen image generate --help` and `mediagen video generate --help` are the
|
|
44
45
|
reference: every flag, the model table, the size reference, examples. Everything below
|
|
@@ -253,9 +254,21 @@ download and 600 s per video download. For video that budget is the *whole* poll
|
|
|
253
254
|
window, not one request: each create or poll gets 60 s, so a hung request cannot eat
|
|
254
255
|
the budget, and the download has its own 600 s and is not counted against it.
|
|
255
256
|
|
|
256
|
-
Working from an agent?
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
Working from an agent? Inside pi, install the skill as a pi package:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
pi install npm:@yandy0725/mediagen-cli
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
That delivers `skills/mediagen/SKILL.md` only: pi never puts the `mediagen` command on
|
|
264
|
+
`PATH`, so the CLI still comes from `npm install -g @yandy0725/mediagen-cli`. For any other
|
|
265
|
+
agent, `mediagen skill install --dir <skills dir>` puts the same file where that agent
|
|
266
|
+
already looks — e.g. `--dir ~/.claude/skills` — and re-running it after an upgrade replaces
|
|
267
|
+
the old copy. Use one route, not both: pi loads skills by name, so the two copies collide — the
|
|
268
|
+
directory you copied wins and pi only reports a `name "mediagen" collision` warning, which
|
|
269
|
+
means a stale copy silently shadows the upgraded package. The skill describes what this CLI
|
|
270
|
+
does, which tool to prefer when the project already generates media another way, and the
|
|
271
|
+
rules that keep a paid request from being wasted.
|
|
259
272
|
|
|
260
273
|
---
|
|
261
274
|
|
|
@@ -268,6 +281,10 @@ npm test # vitest, fully offline: fetch is stubbed everywhere
|
|
|
268
281
|
npm run dev -- image generate --help
|
|
269
282
|
```
|
|
270
283
|
|
|
284
|
+
Tests live in `tests/` — one file per module, imported from `../src/` — and never
|
|
285
|
+
beside the sources; `vitest.config.ts` picks up `tests/**/*.test.ts` and nothing else.
|
|
286
|
+
`tsc --noEmit` typechecks them too.
|
|
287
|
+
|
|
271
288
|
`src/cli.ts` wires commander and maps errors to exit codes; `src/generate.ts` runs
|
|
272
289
|
the pipeline (validate → resolve → call → download → report); `src/api/{qwen,ark,dashscope}.ts`
|
|
273
290
|
are the dialect adapters behind the `DialectAdapter` seam in `src/types.ts`, with
|
package/README.zh-CN.md
CHANGED
|
@@ -38,6 +38,7 @@ Node >= 18,两个运行时依赖,需要 DashScope 和/或方舟的 API key
|
|
|
38
38
|
| `mediagen login` / `logout` / `auth status` | 保存、删除、查看 API key |
|
|
39
39
|
| `mediagen config show` / `config path` | 解析后的配置,以及它存在哪里 |
|
|
40
40
|
| `mediagen config add <name>` | 在 config.toml 里新建一条 provider(其余修改手写文件) |
|
|
41
|
+
| `mediagen skill install --dir <path>` | 把自带的 agent 技能装到 `<path>/mediagen/`;重跑即覆盖 |
|
|
41
42
|
|
|
42
43
|
`mediagen image generate --help` 与 `mediagen video generate --help` 才是参考手册:
|
|
43
44
|
全部参数、模型表、尺寸表、示例。下面只是简化版。
|
|
@@ -228,8 +229,18 @@ OpenAI 兼容端点(`compatible-mode`)只在业务空间专属域名上提
|
|
|
228
229
|
`timeout_seconds` 是**整个轮询窗口**,不是单次请求:建任务与每次轮询各自 60 秒,这样一个挂住
|
|
229
230
|
的请求吃不掉预算;下载另有 600 秒,不计入预算。
|
|
230
231
|
|
|
231
|
-
在 Agent
|
|
232
|
-
|
|
232
|
+
在 Agent 里用?在 pi 里,把这份技能当成 pi 包安装:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
pi install npm:@yandy0725/mediagen-cli
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
它只送 `skills/mediagen/SKILL.md`:pi 不会把 `mediagen` 命令放进 `PATH`,命令仍来自
|
|
239
|
+
`npm install -g @yandy0725/mediagen-cli`。其他 agent 用 `mediagen skill install --dir <技能目录>`
|
|
240
|
+
把同一个文件装到它已经在读的位置(例如 `--dir ~/.claude/skills`),升级后重跑一次即可覆盖。
|
|
241
|
+
两条路线只选一条:pi 按技能名加载,两份同名技能会冲突——复制出来的那份胜出,pi 只给一条
|
|
242
|
+
name "mediagen" collision 警告,于是旧副本会静默盖住升级后的包。这份技能说明这个 CLI
|
|
243
|
+
能做什么、项目已有别的生成工具时该用哪个,以及哪些规则能避免白花钱。
|
|
233
244
|
|
|
234
245
|
---
|
|
235
246
|
|
|
@@ -242,6 +253,9 @@ npm test # vitest,完全离线:所有地方的 fetch 都被打桩
|
|
|
242
253
|
npm run dev -- image generate --help
|
|
243
254
|
```
|
|
244
255
|
|
|
256
|
+
测试统一放在 `tests/`:一个模块一个文件,从 `../src/` 引入,不再与源码同目录;
|
|
257
|
+
`vitest.config.ts` 只收集 `tests/**/*.test.ts`,`tsc --noEmit` 也一并检查它们。
|
|
258
|
+
|
|
245
259
|
`src/cli.ts` 负责 commander 接线和错误到退出码的映射;`src/generate.ts` 是流水线
|
|
246
260
|
(校验 → 解析 → 调用 → 下载 → 汇报);`src/api/{qwen,ark,dashscope}.ts` 是
|
|
247
261
|
`src/types.ts` 中 `DialectAdapter` 接缝背后的方言适配器,周围是 `config.ts`、
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import fs7 from "fs";
|
|
5
|
+
import path7 from "path";
|
|
5
6
|
import readline from "readline";
|
|
6
7
|
import { Writable } from "stream";
|
|
7
|
-
import { fileURLToPath, pathToFileURL } from "url";
|
|
8
|
+
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
|
|
8
9
|
import { Command, CommanderError } from "commander";
|
|
9
10
|
import { parse as parseToml } from "smol-toml";
|
|
10
11
|
|
|
@@ -2353,6 +2354,130 @@ function describeError(err) {
|
|
|
2353
2354
|
return "unknown error";
|
|
2354
2355
|
}
|
|
2355
2356
|
|
|
2357
|
+
// src/skill.ts
|
|
2358
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2359
|
+
import fs6 from "fs";
|
|
2360
|
+
import os3 from "os";
|
|
2361
|
+
import path6 from "path";
|
|
2362
|
+
import { fileURLToPath } from "url";
|
|
2363
|
+
function installSkill(sourceDir, targetDir, deps = {}) {
|
|
2364
|
+
const root = path6.dirname(targetDir);
|
|
2365
|
+
fs6.mkdirSync(root, { recursive: true });
|
|
2366
|
+
const staging = stashPath(root, "install");
|
|
2367
|
+
const retired = stashPath(root, "retired");
|
|
2368
|
+
const cp = deps.cp ?? ((from, to) => fs6.cpSync(from, to, { recursive: true }));
|
|
2369
|
+
const rename = deps.rename ?? ((from, to) => fs6.renameSync(from, to));
|
|
2370
|
+
const existing = lstatOrUndefined(targetDir);
|
|
2371
|
+
if (existing !== void 0 && !existing.isDirectory()) {
|
|
2372
|
+
throw new UsageError(
|
|
2373
|
+
`${targetDir} exists and is not a directory: move it aside, then run the command again`
|
|
2374
|
+
);
|
|
2375
|
+
}
|
|
2376
|
+
let retiredPath;
|
|
2377
|
+
let replaced = false;
|
|
2378
|
+
try {
|
|
2379
|
+
cp(sourceDir, staging);
|
|
2380
|
+
if (existing !== void 0) {
|
|
2381
|
+
rename(targetDir, retired);
|
|
2382
|
+
retiredPath = retired;
|
|
2383
|
+
replaced = true;
|
|
2384
|
+
}
|
|
2385
|
+
rename(staging, targetDir);
|
|
2386
|
+
if (retiredPath !== void 0) {
|
|
2387
|
+
fs6.rmSync(retiredPath, { recursive: true, force: true });
|
|
2388
|
+
retiredPath = void 0;
|
|
2389
|
+
}
|
|
2390
|
+
} catch (error) {
|
|
2391
|
+
if (retiredPath !== void 0) {
|
|
2392
|
+
try {
|
|
2393
|
+
rename(retiredPath, targetDir);
|
|
2394
|
+
retiredPath = void 0;
|
|
2395
|
+
} catch {
|
|
2396
|
+
throw new Error(
|
|
2397
|
+
`${messageOf(error)}; the previous copy could not be put back and is at ${retiredPath}`
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
throw error;
|
|
2402
|
+
} finally {
|
|
2403
|
+
try {
|
|
2404
|
+
fs6.rmSync(staging, { recursive: true, force: true });
|
|
2405
|
+
} catch {
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
return replaced;
|
|
2409
|
+
}
|
|
2410
|
+
function lstatOrUndefined(target) {
|
|
2411
|
+
try {
|
|
2412
|
+
return fs6.lstatSync(target);
|
|
2413
|
+
} catch {
|
|
2414
|
+
return void 0;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
function stashPath(root, kind) {
|
|
2418
|
+
return path6.join(root, `.mediagen-${kind}-${process.pid}-${randomUUID3().slice(0, 8)}`);
|
|
2419
|
+
}
|
|
2420
|
+
function messageOf(error) {
|
|
2421
|
+
return error instanceof Error ? error.message : String(error);
|
|
2422
|
+
}
|
|
2423
|
+
function bundledSkillsDir() {
|
|
2424
|
+
return fileURLToPath(new URL("../skills/", import.meta.url));
|
|
2425
|
+
}
|
|
2426
|
+
function bundledSkillNames(root = bundledSkillsDir()) {
|
|
2427
|
+
try {
|
|
2428
|
+
return fs6.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
2429
|
+
} catch (error) {
|
|
2430
|
+
throw new Error(
|
|
2431
|
+
`the skills this package ships were not found at ${root} (${messageOf(error)}); run mediagen from a normal npm install, where the skills/ directory ships beside dist/cli.js`
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
function installSkills(targetDir, deps = {}) {
|
|
2436
|
+
const sourceRoot = deps.sourceRoot ?? bundledSkillsDir();
|
|
2437
|
+
const names = bundledSkillNames(sourceRoot);
|
|
2438
|
+
if (names.length === 0) {
|
|
2439
|
+
throw new Error(
|
|
2440
|
+
`no skills were found in ${sourceRoot}: the package is incomplete, reinstall mediagen`
|
|
2441
|
+
);
|
|
2442
|
+
}
|
|
2443
|
+
const root = prepareTargetDir(targetDir);
|
|
2444
|
+
return names.map((name) => ({
|
|
2445
|
+
name,
|
|
2446
|
+
path: path6.join(root, name),
|
|
2447
|
+
replaced: installSkill(path6.join(sourceRoot, name), path6.join(root, name), deps)
|
|
2448
|
+
}));
|
|
2449
|
+
}
|
|
2450
|
+
function prepareTargetDir(value) {
|
|
2451
|
+
const trimmed = value.trim();
|
|
2452
|
+
if (trimmed === "") {
|
|
2453
|
+
throw new UsageError(
|
|
2454
|
+
'a destination is required: pass --dir <path>, e.g. --dir "~/.claude/skills"'
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
const expanded = trimmed === "~" || trimmed.startsWith("~/") ? path6.join(os3.homedir(), trimmed.slice(1)) : trimmed;
|
|
2458
|
+
const dir = path6.resolve(expanded);
|
|
2459
|
+
if (fs6.existsSync(dir) && !fs6.statSync(dir).isDirectory()) {
|
|
2460
|
+
throw new UsageError(
|
|
2461
|
+
`--dir ${dir} is a file, not a directory: name the directory that holds skills`
|
|
2462
|
+
);
|
|
2463
|
+
}
|
|
2464
|
+
try {
|
|
2465
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
2466
|
+
} catch (error) {
|
|
2467
|
+
throw new UsageError(
|
|
2468
|
+
`cannot create the skills directory ${dir}: ${messageOf(error)}; check --dir`
|
|
2469
|
+
);
|
|
2470
|
+
}
|
|
2471
|
+
try {
|
|
2472
|
+
fs6.accessSync(dir, fs6.constants.W_OK);
|
|
2473
|
+
} catch (error) {
|
|
2474
|
+
throw new UsageError(
|
|
2475
|
+
`cannot write to the skills directory ${dir}: ${messageOf(error)}; check --dir`
|
|
2476
|
+
);
|
|
2477
|
+
}
|
|
2478
|
+
return dir;
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2356
2481
|
// src/cli.ts
|
|
2357
2482
|
var ROOT_HELP = `
|
|
2358
2483
|
Built for AI agents: explicit flags, no interactive prompts, machine-readable output.
|
|
@@ -2375,6 +2500,7 @@ Subcommands:
|
|
|
2375
2500
|
config show the resolved configuration, with a source note for each value
|
|
2376
2501
|
config path where config.toml lives
|
|
2377
2502
|
config add create a provider in config.toml (edit the file for anything else)
|
|
2503
|
+
skill install copy the agent skill this package ships into a directory you name
|
|
2378
2504
|
video generate create and pay for a video task, then save the result
|
|
2379
2505
|
video status look at one task once, and save its video if it finished
|
|
2380
2506
|
|
|
@@ -2580,7 +2706,7 @@ function addVideoOptions(command) {
|
|
|
2580
2706
|
}
|
|
2581
2707
|
function packageVersion() {
|
|
2582
2708
|
try {
|
|
2583
|
-
const raw =
|
|
2709
|
+
const raw = fs7.readFileSync(new URL("../package.json", import.meta.url), "utf8");
|
|
2584
2710
|
const parsed = JSON.parse(raw);
|
|
2585
2711
|
return typeof parsed.version === "string" ? parsed.version : void 0;
|
|
2586
2712
|
} catch {
|
|
@@ -2620,6 +2746,10 @@ function createProgram(options = {}) {
|
|
|
2620
2746
|
config.command("add <name>").description("Create a [providers.<name>] table in config.toml").option("--api-type <dialect>", "protocol dialect: openai, dashscope, ark or minimax").option("--base-url <url>", "the provider's origin, e.g. https://ark.cn-beijing.volces.com").option("--task <image|video>", "which task table to create (default: image)").option("--model <id>", "the model id this task defaults to").option("--api-key-env <VAR>", "environment variable holding this provider's key").option("--json", "print a JSON result").action(async (name, flags) => {
|
|
2621
2747
|
runConfigAdd(name, flags);
|
|
2622
2748
|
});
|
|
2749
|
+
const skill = program.command("skill").description("Install the agent skill this package ships");
|
|
2750
|
+
skill.command("install").description("Copy the skill into a directory an agent reads skills from, replacing an older copy").option("--dir <path>", "the skills directory, e.g. ~/.claude/skills (the skill lands in <path>/mediagen)").option("--json", "print a JSON result").action(async (flags) => {
|
|
2751
|
+
runSkillInstall(flags);
|
|
2752
|
+
});
|
|
2623
2753
|
const image = program.command("image").description("Generate and edit images");
|
|
2624
2754
|
const generate = image.command("generate").description("Generate an image from a prompt; add --image to edit").argument("<prompt>", "what to draw; quote it and describe the subject, style and text");
|
|
2625
2755
|
addImageOptions(generate).addHelpText("after", imageHelpText("generate"));
|
|
@@ -2942,6 +3072,23 @@ function runConfigPath(flags) {
|
|
|
2942
3072
|
}
|
|
2943
3073
|
writeLine(configPath());
|
|
2944
3074
|
}
|
|
3075
|
+
function runSkillInstall(flags) {
|
|
3076
|
+
const installed = installSkills(flags.dir ?? "");
|
|
3077
|
+
const container = path7.dirname(installed[0].path);
|
|
3078
|
+
if (fs7.existsSync(path7.join(container, "SKILL.md"))) {
|
|
3079
|
+
writeErrLine(
|
|
3080
|
+
`note: ${container} already holds a SKILL.md; --dir names the directory that contains skills, so the skill landed in ${installed[0].path}`
|
|
3081
|
+
);
|
|
3082
|
+
}
|
|
3083
|
+
if (flags.json === true) {
|
|
3084
|
+
writeLine(JSON.stringify({ skills: installed }));
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
3087
|
+
for (const entry of installed) {
|
|
3088
|
+
if (entry.replaced) writeErrLine(`replaced the existing copy at ${entry.path}`);
|
|
3089
|
+
}
|
|
3090
|
+
for (const entry of installed) writeLine(entry.path);
|
|
3091
|
+
}
|
|
2945
3092
|
function runConfigAdd(name, flags) {
|
|
2946
3093
|
const provider = name.trim();
|
|
2947
3094
|
if (provider === "") throw new UsageError("a provider name is required");
|
|
@@ -2975,7 +3122,7 @@ function runConfigAdd(name, flags) {
|
|
|
2975
3122
|
base_url: originBaseUrl(flags.baseUrl)
|
|
2976
3123
|
};
|
|
2977
3124
|
if (apiKeyEnv !== void 0) fields.api_key_env = apiKeyEnv;
|
|
2978
|
-
const before =
|
|
3125
|
+
const before = fs7.existsSync(file) ? fs7.readFileSync(file, "utf8") : "";
|
|
2979
3126
|
const beforeTable = before.trim() === "" ? void 0 : parseToml(before);
|
|
2980
3127
|
if (apiType === "qwen") {
|
|
2981
3128
|
writeErrLine('note: api_type "qwen" is a deprecated alias; writing api_type = "openai"');
|
|
@@ -3019,8 +3166,8 @@ function runConfigAdd(name, flags) {
|
|
|
3019
3166
|
function runConfigShow(flags) {
|
|
3020
3167
|
const config = loadConfig();
|
|
3021
3168
|
const file = configPath();
|
|
3022
|
-
const exists =
|
|
3023
|
-
const declared = exists ? parseToml(
|
|
3169
|
+
const exists = fs7.existsSync(file);
|
|
3170
|
+
const declared = exists ? parseToml(fs7.readFileSync(file, "utf8")) : {};
|
|
3024
3171
|
const defaultProvider = configuredValue(
|
|
3025
3172
|
config.default_provider,
|
|
3026
3173
|
declared.default_provider
|
|
@@ -3157,17 +3304,17 @@ function buildProviderView(name, entry) {
|
|
|
3157
3304
|
if (block.path !== void 0) {
|
|
3158
3305
|
taskView.path = { value: block.path, source: "config.toml" };
|
|
3159
3306
|
}
|
|
3160
|
-
const
|
|
3307
|
+
const path8 = block.path ?? (effectiveDialect === void 0 ? void 0 : adapterPath(effectiveDialect, task));
|
|
3161
3308
|
if (effectiveDialect === void 0) {
|
|
3162
3309
|
taskView.endpoint_note = "api_type is not set";
|
|
3163
|
-
} else if (
|
|
3310
|
+
} else if (path8 === void 0) {
|
|
3164
3311
|
taskView.endpoint_note = `the ${effectiveDialect} dialect has no ${task} task`;
|
|
3165
3312
|
} else if (base === void 0) {
|
|
3166
3313
|
taskView.endpoint_note = "base_url is not a URL yet";
|
|
3167
3314
|
} else if (block.path === void 0 && vendorPathIssue(base) !== void 0) {
|
|
3168
3315
|
taskView.endpoint_note = "base_url carries a path; declare path = ... or use the origin";
|
|
3169
3316
|
} else {
|
|
3170
|
-
taskView.endpoint = `${base}${
|
|
3317
|
+
taskView.endpoint = `${base}${path8}`;
|
|
3171
3318
|
}
|
|
3172
3319
|
return taskView;
|
|
3173
3320
|
}
|
|
@@ -3283,14 +3430,14 @@ function sameInstant(before, after) {
|
|
|
3283
3430
|
function realHref(modulePath) {
|
|
3284
3431
|
if (modulePath === void 0) return void 0;
|
|
3285
3432
|
try {
|
|
3286
|
-
return pathToFileURL(
|
|
3433
|
+
return pathToFileURL(fs7.realpathSync(modulePath)).href;
|
|
3287
3434
|
} catch {
|
|
3288
3435
|
return void 0;
|
|
3289
3436
|
}
|
|
3290
3437
|
}
|
|
3291
3438
|
function selfHref(moduleUrl) {
|
|
3292
3439
|
try {
|
|
3293
|
-
return realHref(
|
|
3440
|
+
return realHref(fileURLToPath2(moduleUrl));
|
|
3294
3441
|
} catch {
|
|
3295
3442
|
return void 0;
|
|
3296
3443
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yandy0725/mediagen-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Non-interactive image generation and editing, plus short video generation, for agents: qwen-image, doubao/seedream, doubao/seedance, MiniMax H3, files on disk, JSON output.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,7 +11,13 @@
|
|
|
11
11
|
"dist",
|
|
12
12
|
"skills"
|
|
13
13
|
],
|
|
14
|
+
"pi": {
|
|
15
|
+
"skills": [
|
|
16
|
+
"./skills"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
14
19
|
"keywords": [
|
|
20
|
+
"pi-package",
|
|
15
21
|
"cli",
|
|
16
22
|
"image",
|
|
17
23
|
"image-generation",
|
package/skills/mediagen/SKILL.md
CHANGED
|
@@ -42,6 +42,15 @@ project has, or when the user asks for it by name.
|
|
|
42
42
|
|
|
43
43
|
## Setup, once
|
|
44
44
|
|
|
45
|
+
This skill carries no binary: `mediagen` must already be on `PATH`, and the pi package that
|
|
46
|
+
delivered this skill does not put it there. If `command -v mediagen` prints nothing:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install -g @yandy0725/mediagen-cli
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then one pass per machine:
|
|
53
|
+
|
|
45
54
|
```bash
|
|
46
55
|
mediagen config show # any command writes and validates config.toml on first use
|
|
47
56
|
mediagen login --api-key sk-... # the key for the default provider; --provider <name> for another
|