@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 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? `skills/mediagen/SKILL.md` describes what this CLI does, which tool
257
- to prefer when the project already generates media another way, and the rules that keep a
258
- paid request from being wasted.
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 里用?`skills/mediagen/SKILL.md` 说明这个 CLI 能做什么、项目已有别的生成工具时该用哪个,
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 fs6 from "fs";
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 = fs6.readFileSync(new URL("../package.json", import.meta.url), "utf8");
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 = fs6.existsSync(file) ? fs6.readFileSync(file, "utf8") : "";
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 = fs6.existsSync(file);
3023
- const declared = exists ? parseToml(fs6.readFileSync(file, "utf8")) : {};
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 path6 = block.path ?? (effectiveDialect === void 0 ? void 0 : adapterPath(effectiveDialect, task));
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 (path6 === void 0) {
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}${path6}`;
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(fs6.realpathSync(modulePath)).href;
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(fileURLToPath(moduleUrl));
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.2.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",
@@ -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