@lumenkey/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +134 -0
  2. package/bin/lumenkey-mcp.js +7 -0
  3. package/bin/lumenkey.js +7 -0
  4. package/dist/cli/copied.d.ts +27 -0
  5. package/dist/cli/copied.d.ts.map +1 -0
  6. package/dist/cli/copied.js +489 -0
  7. package/dist/cli/copied.js.map +1 -0
  8. package/dist/cli/index.d.ts +2 -0
  9. package/dist/cli/index.d.ts.map +1 -0
  10. package/dist/cli/index.js +227 -0
  11. package/dist/cli/index.js.map +1 -0
  12. package/dist/core/client.d.ts +60 -0
  13. package/dist/core/client.d.ts.map +1 -0
  14. package/dist/core/client.js +227 -0
  15. package/dist/core/client.js.map +1 -0
  16. package/dist/core/config.d.ts +12 -0
  17. package/dist/core/config.d.ts.map +1 -0
  18. package/dist/core/config.js +97 -0
  19. package/dist/core/config.js.map +1 -0
  20. package/dist/core/explain.d.ts +23 -0
  21. package/dist/core/explain.d.ts.map +1 -0
  22. package/dist/core/explain.js +54 -0
  23. package/dist/core/explain.js.map +1 -0
  24. package/dist/core/poller.d.ts +19 -0
  25. package/dist/core/poller.d.ts.map +1 -0
  26. package/dist/core/poller.js +108 -0
  27. package/dist/core/poller.js.map +1 -0
  28. package/dist/core/profiles.d.ts +25 -0
  29. package/dist/core/profiles.d.ts.map +1 -0
  30. package/dist/core/profiles.js +96 -0
  31. package/dist/core/profiles.js.map +1 -0
  32. package/dist/core/set-values.d.ts +22 -0
  33. package/dist/core/set-values.d.ts.map +1 -0
  34. package/dist/core/set-values.js +141 -0
  35. package/dist/core/set-values.js.map +1 -0
  36. package/dist/core/types.d.ts +80 -0
  37. package/dist/core/types.d.ts.map +1 -0
  38. package/dist/core/types.js +6 -0
  39. package/dist/core/types.js.map +1 -0
  40. package/dist/core/wait.d.ts +7 -0
  41. package/dist/core/wait.d.ts.map +1 -0
  42. package/dist/core/wait.js +19 -0
  43. package/dist/core/wait.js.map +1 -0
  44. package/dist/index.d.ts +7 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +7 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/mcp/server.d.ts +4 -0
  49. package/dist/mcp/server.d.ts.map +1 -0
  50. package/dist/mcp/server.js +346 -0
  51. package/dist/mcp/server.js.map +1 -0
  52. package/package.json +49 -0
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # @lumenkey/cli
2
+
3
+ > **光钥 (LumenKey) 官方 CLI 命令行工具 & 智能体 MCP (Model Context Protocol) 统一服务包**
4
+ > 一套代码同构支持:**人类终端命令行 (CLI)** 与 **AI Agent (Cursor / Claude Code / Codex / Antigravity) 原生工具挂载 (MCP Server)**。
5
+
6
+ ---
7
+
8
+ ## 🌟 核心特性
9
+
10
+ - ⚡ **双模一体 (Dual-Mode)**:既能作为人类敲命令、脚本批处理的 CLI,又能作为 MCP Server 一键挂载进智能体。
11
+ - 🎬 **全模态视频直接出片**:现网直通 **Seedance 2.0 (`doubao-seedance-2.0`)**、**通义万相 Wan 3.0 (`wan3.0-video`)** 等顶流模型,带实时状态轮询与自动下载。
12
+ - 🔍 **动态模型目录 (Dynamic Catalog)**:实时拉取最新模型池与价格,新模型上线与旧模型兼容零延迟。
13
+ - 🔒 **工业级安全与规范**:MCP 标准 `stdout` 仅输出协议,调试日志纯走 `stderr`,完美适配 Cursor 与 Claude。
14
+
15
+ ---
16
+
17
+ ## 📦 极速上手 (Quick Start)
18
+
19
+ ### 1. 认证配置
20
+
21
+ 可通过环境变量或配置命令设置您的 API Key:
22
+
23
+ ```bash
24
+ # 方式一:设置环境变量(推荐)
25
+ export LUMENKEY_API_KEY="lk_live_xxxxxxxx"
26
+
27
+ # 方式二:保存至本地 ~/.lumenkey/config.json
28
+ npx @lumenkey/cli login --key lk_live_xxxxxxxx
29
+ ```
30
+
31
+ ---
32
+
33
+ ## 💻 命令行模式 (CLI Usage)
34
+
35
+ ### 1. 浏览全站可用模型
36
+ ```bash
37
+ # 查看全站模型(文本/视频/生图)
38
+ npx @lumenkey/cli models
39
+
40
+ # 只看视频模型
41
+ npx @lumenkey/cli models --type video
42
+ ```
43
+
44
+ ### 2. 一键生成视频(自动轮询并下载)
45
+ ```bash
46
+ npx @lumenkey/cli video \
47
+ --model doubao-seedance-2.0 \
48
+ --prompt "赛博朋克风格的雨夜街道,霓虹灯倒影在积水中,电影级运镜" \
49
+ --duration 5 \
50
+ --resolution 720p \
51
+ --output ./cyberpunk.mp4
52
+ ```
53
+
54
+ ### 3. 查询异步任务状态
55
+ ```bash
56
+ npx @lumenkey/cli status <jobId>
57
+ ```
58
+
59
+ ### 4. 查看账户算力余额
60
+ ```bash
61
+ npx @lumenkey/cli balance
62
+ ```
63
+
64
+ ---
65
+
66
+ ## 🤖 智能体挂载 (MCP Server for AI Agents)
67
+
68
+ 任何支持 **Model Context Protocol (MCP)** 的工具或编辑器(Cursor、Claude Code、Claude Desktop、Codex、Antigravity),只需配置 3 行代码:
69
+
70
+ ### 1. 在 Cursor 中配置 (`.cursor/mcp.json`)
71
+
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "lumenkey": {
76
+ "command": "npx",
77
+ "args": ["-y", "@lumenkey/cli", "mcp"],
78
+ "env": {
79
+ "LUMENKEY_API_KEY": "lk_live_xxxxxxxx",
80
+ "LUMENKEY_BASE_URL": "https://123outlets.com"
81
+ }
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ ### 2. 在 Claude Code 中配置
88
+
89
+ ```bash
90
+ claude mcp add lumenkey npx -y @lumenkey/cli mcp
91
+ ```
92
+
93
+ ### 3. 在 Claude Desktop 中配置 (`claude_desktop_config.json`)
94
+
95
+ ```json
96
+ {
97
+ "mcpServers": {
98
+ "lumenkey": {
99
+ "command": "npx",
100
+ "args": ["-y", "@lumenkey/cli", "mcp"],
101
+ "env": {
102
+ "LUMENKEY_API_KEY": "lk_live_xxxxxxxx"
103
+ }
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ ---
110
+
111
+ ## 🛠️ MCP 暴露给 Agent 的工具集
112
+
113
+ | 工具名 (Tool) | 描述 |
114
+ | :--- | :--- |
115
+ | `lumenkey_list_models` | 动态获取站内所有模型目录、模态分类与价格 |
116
+ | `lumenkey_generate_video` | 调度 Seedance 2.0 / Wan 3.0 生成视频并自动保存 |
117
+ | `lumenkey_generate_image` | 调度商业级图像模型生成图片 |
118
+ | `lumenkey_query_job` | 根据任务 ID 异步查询进度与生成产物 URL |
119
+ | `lumenkey_check_balance` | 实时查询账户算力点数与充值状态 |
120
+
121
+ ---
122
+
123
+ ## 🏗️ 架构与底层协议
124
+
125
+ 遵循光钥华语站/国际站 OpenAPI 标准:
126
+ 1. `POST /v1/generation-quotes`:获取单次生成报价与算力预扣凭据;
127
+ 2. `POST /v1/generation-jobs`:附带 `Idempotency-Key` 幂等创建异步作业;
128
+ 3. `GET /v1/generation-jobs/{jobId}`:指数退避轮询任务状态;
129
+ 4. `POST /v1/generation-jobs/{jobId}/cancel`:超时或主动中断任务。
130
+
131
+ ---
132
+
133
+ ## 📄 License
134
+ MIT © 2026 LumenKey Team.
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runMcpStdio } from '../dist/mcp/server.js';
3
+
4
+ runMcpStdio().catch((err) => {
5
+ process.stderr.write(`[LumenKey MCP Fatal] ${err?.message || err}\n`);
6
+ process.exit(1);
7
+ });
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from '../dist/cli/index.js';
3
+
4
+ runCli().catch((err) => {
5
+ console.error(err);
6
+ process.exit(1);
7
+ });
@@ -0,0 +1,27 @@
1
+ import type { CAC } from 'cac';
2
+ import { LumenKeyClient } from '../core/client.js';
3
+ import type { GenerationJobDetail, ModelItem } from '../core/types.js';
4
+ export declare function wantsJson(argv?: string[]): boolean;
5
+ export declare function rawOption(names: string[], argv?: string[]): string | undefined;
6
+ export declare function applyRaw(options: Record<string, unknown>, key: string, names: string[]): void;
7
+ export declare function emit(data: unknown, human: () => void, argv?: string[]): void;
8
+ export declare function submitQuote(modelId: string, input: Record<string, unknown>, options: Record<string, unknown>): Promise<{
9
+ jobId: string;
10
+ idempotencyKey: string;
11
+ } | undefined>;
12
+ export declare function buildGenerationInput(modelId: string, fields: Record<string, unknown>, pairs: string[], known?: ModelItem): Promise<{
13
+ input: Record<string, unknown>;
14
+ properties: Record<string, unknown>;
15
+ }>;
16
+ export declare function registerCopied(cli: CAC): void;
17
+ export declare function outputUrls(detail: {
18
+ outputs?: Array<{
19
+ url?: string;
20
+ }>;
21
+ }): string[];
22
+ export declare function pollConfirmed(api: LumenKeyClient, jobId: string, options: {
23
+ interval?: unknown;
24
+ timeout?: unknown;
25
+ }): Promise<GenerationJobDetail>;
26
+ export declare function promptForKey(): Promise<string | undefined>;
27
+ //# sourceMappingURL=copied.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copied.d.ts","sourceRoot":"","sources":["../../src/cli/copied.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAE/B,OAAO,EAAE,cAAc,EAAiB,MAAM,mBAAmB,CAAC;AAMlE,OAAO,KAAK,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AASvE,wBAAgB,SAAS,CAAC,IAAI,WAAe,GAAG,OAAO,CAEtD;AAOD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,WAAe,GAAG,MAAM,GAAG,SAAS,CAYlF;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAI7F;AAED,wBAAgB,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,IAAI,EAAE,IAAI,WAAe,GAAG,IAAI,CAMhF;AAYD,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAC,CAyBnL;AAED,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,CAOjN;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAsS7C;AAqBD,wBAAgB,UAAU,CAAC,MAAM,EAAE;IAAE,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,GAAG,MAAM,EAAE,CAElF;AAED,wBAAsB,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAmBxJ;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAIhE"}
@@ -0,0 +1,489 @@
1
+ import crypto from 'node:crypto';
2
+ import os from 'node:os';
3
+ import { describeKey, loadConfig } from '../core/config.js';
4
+ import { LumenKeyClient, LumenKeyError } from '../core/client.js';
5
+ import { explainModel, findModels } from '../core/explain.js';
6
+ import { applyKeyValues, asPairs, validateKnownEnums } from '../core/set-values.js';
7
+ import { createProfile, currentProfile, deleteProfile, listProfiles, publicProfile, setNamedProfileFields, setProfile, setProfileFields, unsetProfile } from '../core/profiles.js';
8
+ import { waitForJob } from '../core/wait.js';
9
+ import { assertJobResult, downloadFile, pollJob } from '../core/poller.js';
10
+ const COMPLETIONS = {
11
+ bash: `_lumenkey() {\n local cur prev\n cur="\${COMP_WORDS[COMP_CWORD]}"\n prev="\${COMP_WORDS[COMP_CWORD-1]}"\n case "$prev" in\n model) COMPREPLY=( $(compgen -W "search get explain" -- "$cur") ) ;;\n image) COMPREPLY=( $(compgen -W "generate" -- "$cur") ) ;;\n tasks) COMPREPLY=( $(compgen -W "progress wait cancel" -- "$cur") ) ;;\n profile) COMPREPLY=( $(compgen -W "list create set unset key host delete" -- "$cur") ) ;;\n auth) COMPREPLY=( $(compgen -W "profile" -- "$cur") ) ;;\n *) COMPREPLY=( $(compgen -W "login models video status balance chat mcp model image tasks profile auth doctor completion" -- "$cur") ) ;;\n esac\n}\ncomplete -F _lumenkey lumenkey\n`,
12
+ zsh: `#compdef lumenkey\n_lumenkey() {\n if (( CURRENT == 2 )); then\n compadd login models video status balance chat mcp model image tasks profile auth doctor completion\n elif [[ \${words[2]} == model ]]; then\n compadd search get explain\n elif [[ \${words[2]} == image ]]; then\n compadd generate\n elif [[ \${words[2]} == tasks ]]; then\n compadd progress wait cancel\n fi\n}\ncompdef _lumenkey lumenkey\n`,
13
+ tcsh: `complete lumenkey 'p/1/(login models video status balance chat mcp model image tasks profile auth doctor completion)/' 'n/model/(search get explain)/' 'n/image/(generate)/' 'n/tasks/(progress wait cancel)/'\n`,
14
+ fish: `complete -c lumenkey -n "__fish_use_subcommand" -a "login models video status balance chat mcp model image tasks profile auth doctor completion"\ncomplete -c lumenkey -n "__fish_seen_subcommand_from model" -a "search get explain"\ncomplete -c lumenkey -n "__fish_seen_subcommand_from image" -a "generate"\ncomplete -c lumenkey -n "__fish_seen_subcommand_from tasks" -a "progress wait cancel"\n`,
15
+ };
16
+ export function wantsJson(argv = process.argv) {
17
+ return argv.includes('--json');
18
+ }
19
+ function spellings(name) {
20
+ const camel = name.replace(/([a-z])-([a-z])/g, (_, a, b) => a + b.toUpperCase());
21
+ return camel === name ? [name] : [name, camel];
22
+ }
23
+ export function rawOption(names, argv = process.argv) {
24
+ const all = names.flatMap(spellings);
25
+ let found;
26
+ for (let i = 2; i < argv.length; i += 1) {
27
+ const arg = argv[i];
28
+ if (arg === '--')
29
+ break;
30
+ for (const name of all) {
31
+ if (arg === name && i + 1 < argv.length)
32
+ found = argv[i + 1];
33
+ else if (arg.startsWith(`${name}=`))
34
+ found = arg.slice(name.length + 1);
35
+ }
36
+ }
37
+ return found;
38
+ }
39
+ export function applyRaw(options, key, names) {
40
+ const raw = rawOption(names);
41
+ if (raw !== undefined)
42
+ options[key] = raw;
43
+ else if (options[key] !== undefined && typeof options[key] !== 'string')
44
+ throw new Error(`${names[0]} could not be read as text. Use ${names[0]} <value>.`);
45
+ }
46
+ export function emit(data, human, argv = process.argv) {
47
+ if (wantsJson(argv)) {
48
+ console.log(JSON.stringify(data));
49
+ return;
50
+ }
51
+ human();
52
+ }
53
+ function client() {
54
+ return new LumenKeyClient(loadConfig());
55
+ }
56
+ function propertiesOf(model) {
57
+ const properties = model.inputSchema?.properties;
58
+ if (!properties || typeof properties !== 'object' || Array.isArray(properties))
59
+ throw new LumenKeyError('MODEL_INPUT_SCHEMA_INVALID');
60
+ return properties;
61
+ }
62
+ export async function submitQuote(modelId, input, options) {
63
+ const api = client();
64
+ const rawKey = rawOption(['--idempotency-key']);
65
+ if (rawKey !== undefined) {
66
+ if (!rawKey.trim())
67
+ throw new Error('--idempotency-key must not be empty.');
68
+ options.idempotencyKey = rawKey;
69
+ }
70
+ const schemaProperties = options.schemaProperties;
71
+ if (schemaProperties && typeof schemaProperties === 'object' && !Array.isArray(schemaProperties))
72
+ validateKnownEnums(schemaProperties, input);
73
+ const quote = await api.getQuote({ modelId, input });
74
+ if (!options.confirm) {
75
+ emit({ ...quote, submitted: false }, () => {
76
+ console.log(`Quote ID: ${quote.quoteId} (${quote.credits} credits)`);
77
+ console.log('Quote only. No job submitted.');
78
+ });
79
+ return;
80
+ }
81
+ if (options.maxCredits !== undefined && (!Number.isSafeInteger(Number(options.maxCredits)) || Number(options.maxCredits) < 0 || quote.credits > Number(options.maxCredits)))
82
+ throw new Error('QUOTE_CREDIT_LIMIT');
83
+ const provided = options.idempotencyKey == null ? '' : String(options.idempotencyKey).trim();
84
+ const key = provided || crypto.randomUUID();
85
+ console.error('Submitting generation job...');
86
+ const projectId = options.projectId == null || options.projectId === '' ? undefined : String(options.projectId);
87
+ const job = await api.createJob({ quoteId: quote.quoteId, projectId, input }, key);
88
+ if (!options.suppressEmit)
89
+ emit({ ...job, idempotencyKey: key, submitted: true }, () => console.log(`Job created: ${job.jobId}`));
90
+ return { jobId: job.jobId, idempotencyKey: key };
91
+ }
92
+ export async function buildGenerationInput(modelId, fields, pairs, known) {
93
+ const model = known?.id === modelId ? known : (await client().listModels()).find((item) => item.id === modelId);
94
+ if (!model)
95
+ throw new LumenKeyError('MODEL_NOT_IN_CATALOG');
96
+ const properties = propertiesOf(model);
97
+ const declared = Object.fromEntries(Object.entries(fields).filter(([key, value]) => value !== undefined && Object.prototype.hasOwnProperty.call(properties, key)));
98
+ const extra = applyKeyValues({}, pairs);
99
+ return { input: { ...declared, ...extra }, properties };
100
+ }
101
+ export function registerCopied(cli) {
102
+ cli.option('--json', 'Emit machine-readable JSON');
103
+ cli.command('model <action> [name]', 'model search | get | explain')
104
+ .example('lumenkey model search wan --type video')
105
+ .example('lumenkey model get minimax-h3')
106
+ .example('lumenkey --json model explain minimax-h3')
107
+ .option('--type <type>', 'Filter or disambiguate by catalog type')
108
+ .action(async (action, name, options) => {
109
+ applyRaw(options, 'type', ['--type']);
110
+ if (action === 'search') {
111
+ const models = findModels(await client().listModels(), name || '', options.type);
112
+ emit({ query: name || '', count: models.length, models }, () => {
113
+ for (const model of models)
114
+ console.log(model.id);
115
+ if (models.length === 0)
116
+ console.log('No models available for this request.');
117
+ });
118
+ return;
119
+ }
120
+ if (!name)
121
+ throw new Error('Model name is required. Try: model search');
122
+ if (action === 'get') {
123
+ const exact = findModels(await client().listModels(), '').filter((model) => model.id === name || (model.displayName || model.name || '').toLowerCase() === name.toLowerCase());
124
+ if (exact.length !== 1)
125
+ throw new Error(`Expected exactly one model match for ${name}, got ${exact.length}. Try: model search ${name}`);
126
+ emit(exact[0], () => console.log(JSON.stringify(exact[0], null, 2)));
127
+ return;
128
+ }
129
+ if (action === 'explain') {
130
+ const all = await client().listModels();
131
+ const exact = all.filter((model) => model.id === name || (model.displayName || model.name || '').toLowerCase() === name.toLowerCase());
132
+ const chosen = exact.length === 1 ? exact[0] : findModels(all, name, options.type).length === 1 ? findModels(all, name, options.type)[0] : undefined;
133
+ if (!chosen)
134
+ throw new Error(`Expected exactly one model match for ${name}, got ${exact.length}. Try: model search ${name}`);
135
+ const explained = explainModel(chosen);
136
+ emit(explained, () => console.log(JSON.stringify(explained, null, 2)));
137
+ return;
138
+ }
139
+ throw new Error(`Unknown model command: ${action}`);
140
+ });
141
+ cli.command('image <action>', 'image generate')
142
+ .example('lumenkey image generate -m wan2.7-image -p "red circle"')
143
+ .example('lumenkey image generate -m wan2.7-image -p "red circle" --set seed=2 --dry-run')
144
+ .option('-m, --model <modelId>', 'Model id')
145
+ .option('-p, --prompt <prompt>', 'Prompt')
146
+ .option('--resolution <res>', 'Resolution, only if the catalog declares it')
147
+ .option('--aspect-ratio <ratio>', 'Aspect ratio, only if the catalog declares it')
148
+ .option('--count <count>', 'Count, only if the catalog declares it')
149
+ .option('--image <image>', 'Reference image, only if the catalog declares it')
150
+ .option('-s, --set <pair>', 'Extra key=value. Repeatable')
151
+ .option('--dry-run', 'Print the projected input and do not quote')
152
+ .option('--confirm', 'Submit this quote')
153
+ .option('--idempotency-key <key>', 'Stable operation key')
154
+ .option('--max-credits <credits>', 'Reject quotes above this limit')
155
+ .option('-o, --output <outputPath>', 'Save the first result to this path. Omit it to print the URLs only')
156
+ .option('--interval <seconds>', 'Poll interval seconds after confirm', { default: 5 })
157
+ .option('--timeout <seconds>', 'Poll timeout seconds after confirm', { default: 1800 })
158
+ .action(async (action, options) => {
159
+ if (action !== 'generate')
160
+ throw new Error(`Unknown image command: ${action}`);
161
+ applyRaw(options, 'model', ['--model', '-m']);
162
+ applyRaw(options, 'prompt', ['--prompt', '-p']);
163
+ applyRaw(options, 'resolution', ['--resolution']);
164
+ applyRaw(options, 'aspectRatio', ['--aspect-ratio']);
165
+ applyRaw(options, 'count', ['--count']);
166
+ applyRaw(options, 'image', ['--image']);
167
+ applyRaw(options, 'idempotencyKey', ['--idempotency-key']);
168
+ applyRaw(options, 'output', ['--output', '-o']);
169
+ if (!options.model)
170
+ throw new LumenKeyError('MODEL_REQUIRED');
171
+ if (!options.prompt)
172
+ throw new Error('--prompt is required.');
173
+ const model = (await client().listModels()).find((item) => item.id === options.model);
174
+ if (!model)
175
+ throw new LumenKeyError('MODEL_NOT_IN_CATALOG');
176
+ const props = propertiesOf(model);
177
+ const fields = { prompt: String(options.prompt) };
178
+ const put = (name, value) => {
179
+ if (value !== undefined && Object.prototype.hasOwnProperty.call(props, name))
180
+ fields[name] = coerceDeclared(props[name], value);
181
+ };
182
+ put('resolution', options.resolution);
183
+ put('aspect_ratio', options.aspectRatio);
184
+ put('ratio', options.aspectRatio);
185
+ put('count', options.count);
186
+ put('image', options.image);
187
+ const built = await buildGenerationInput(String(options.model), fields, asPairs(options.set), model);
188
+ if (options.dryRun) {
189
+ emit({ dryRun: true, modelId: options.model, input: built.input }, () => console.log(JSON.stringify({ dryRun: true, modelId: options.model, input: built.input })));
190
+ return;
191
+ }
192
+ const created = await submitQuote(String(options.model), built.input, { ...options, schemaProperties: built.properties });
193
+ if (!created || wantsJson())
194
+ return;
195
+ const detail = await pollConfirmed(client(), created.jobId, options);
196
+ assertJobResult(detail, true);
197
+ const urls = outputUrls(detail);
198
+ for (const url of urls)
199
+ console.log(url);
200
+ if (urls[0] && typeof options.output === 'string' && options.output) {
201
+ const savedPath = await downloadFile(urls[0], options.output);
202
+ console.log(`Saved to ${savedPath}`);
203
+ }
204
+ });
205
+ cli.command('tasks <action> <jobId>', 'tasks progress | wait | cancel')
206
+ .example('lumenkey tasks progress <jobId>')
207
+ .example('lumenkey tasks wait <jobId> --interval 5 --timeout 1800')
208
+ .example('lumenkey tasks cancel <jobId> --yes')
209
+ .option('--interval <seconds>', 'Poll interval seconds', { default: 5 })
210
+ .option('--timeout <seconds>', 'Timeout seconds', { default: 1800 })
211
+ .option('--yes', 'Confirm cancellation')
212
+ .action(async (action, jobId, options) => {
213
+ if (action === 'progress') {
214
+ const detail = await client().getJob(jobId);
215
+ assertJobResult(detail, false);
216
+ emit(detail, () => console.log(JSON.stringify(detail, null, 2)));
217
+ return;
218
+ }
219
+ if (action === 'wait') {
220
+ const interval = Number(options.interval);
221
+ const timeout = Number(options.timeout);
222
+ if (!Number.isFinite(interval) || interval < 0 || !Number.isFinite(timeout) || timeout < 0)
223
+ throw new Error('--interval and --timeout must be non-negative numbers.');
224
+ if (interval * 1000 > 2_147_483_647)
225
+ throw new Error('--interval must be between 0 and 2147483 seconds.');
226
+ try {
227
+ const detail = await waitForJob(client(), jobId, { intervalMs: interval * 1000, timeoutMs: timeout * 1000 });
228
+ const failed = detail.error != null || ['failed', 'rejected', 'cancelled', 'expired'].includes(detail.status);
229
+ const body = { ...detail, ok: detail.status === 'succeeded' && !failed, timedOut: false };
230
+ emit(body, () => console.log(JSON.stringify(body, null, 2)));
231
+ if (!body.ok)
232
+ process.exitCode = 1;
233
+ return;
234
+ }
235
+ catch (err) {
236
+ if (err instanceof LumenKeyError && err.code === 'WAIT_TIMEOUT') {
237
+ const body = { status: 'TIMEOUT', ok: false, timedOut: true, jobId };
238
+ emit(body, () => console.log(JSON.stringify(body)));
239
+ process.exitCode = 1;
240
+ return;
241
+ }
242
+ throw err;
243
+ }
244
+ }
245
+ if (action === 'cancel') {
246
+ if (!options.yes)
247
+ throw new Error('Refusing to cancel without --yes.');
248
+ const result = await client().cancelJob(jobId);
249
+ emit(result, () => console.log(JSON.stringify(result)));
250
+ return;
251
+ }
252
+ throw new Error(`Unknown tasks command: ${action}`);
253
+ });
254
+ cli.command('profile <action> [value]', 'profile list | create | set | unset | key | host | delete')
255
+ .example('lumenkey profile create work')
256
+ .example('lumenkey profile key')
257
+ .example('lumenkey profile host https://123outlets.com')
258
+ .example('lumenkey --json profile list')
259
+ .action(async (action, value) => {
260
+ if (action === 'list') {
261
+ const listed = listProfiles();
262
+ emit(listed, () => {
263
+ for (const row of listed.profiles)
264
+ console.log(`${row.isSet ? '* ' : ' '}${row.profile} ${row.settings.join(', ')}`);
265
+ });
266
+ return;
267
+ }
268
+ if (action === 'unset') {
269
+ unsetProfile();
270
+ emit({ current: null }, () => console.log('Default profile unset.'));
271
+ return;
272
+ }
273
+ if (action === 'key') {
274
+ const key = await readProfileKey();
275
+ const name = setProfileFields({ key });
276
+ emit({ profile: name, hasStoredToken: true }, () => console.log(`Key set for profile ${name}.`));
277
+ return;
278
+ }
279
+ if (!value)
280
+ throw new Error(`Profile ${action} needs a name.`);
281
+ if (action === 'create') {
282
+ const result = createProfile(value);
283
+ const text = result === 'exists' ? `Profile ${value} already exists.` : `Profile ${value} created.`;
284
+ emit({ name: value, created: result === 'created' }, () => console.log(text));
285
+ return;
286
+ }
287
+ if (action === 'set') {
288
+ try {
289
+ setProfile(value);
290
+ }
291
+ catch (err) {
292
+ if (!(err instanceof Error) || !err.message.includes('does not exist'))
293
+ throw err;
294
+ console.error(`Profile ${value} does not exist.`);
295
+ const answer = (await ask('Would you like to create it? (y/N): ')).trim().toLowerCase();
296
+ if (answer !== 'y' && answer !== 'yes') {
297
+ emit({ current: null, created: false }, () => console.log('Profile creation cancelled.'));
298
+ return;
299
+ }
300
+ createProfile(value);
301
+ }
302
+ emit({ current: value }, () => console.log(`Default profile set to ${value}.`));
303
+ return;
304
+ }
305
+ if (action === 'host') {
306
+ try {
307
+ new LumenKeyClient({ baseUrl: value, timeoutMs: 1000 });
308
+ }
309
+ catch {
310
+ throw new LumenKeyError('BASE_URL_INVALID');
311
+ }
312
+ const name = setProfileFields({ host: value });
313
+ emit({ profile: name, host: value }, () => console.log(`Host set to ${value}.`));
314
+ return;
315
+ }
316
+ if (action === 'delete') {
317
+ const removed = deleteProfile(value);
318
+ const text = removed ? `Profile ${value} deleted.` : `Profile ${value} does not exist.`;
319
+ emit({ deleted: removed, name: value }, () => console.log(text));
320
+ return;
321
+ }
322
+ throw new Error(`Unknown profile command: ${action}`);
323
+ });
324
+ cli.command('auth <group> [action] [name]', 'auth status | profile list | use | save | delete')
325
+ .example('lumenkey auth profile list')
326
+ .example('lumenkey auth profile save work')
327
+ .example('lumenkey auth profile use work')
328
+ .example('lumenkey auth profile delete work --yes')
329
+ .option('--yes', 'Confirm deletion')
330
+ .option('--store-token', 'Store the environment key after a balance check')
331
+ .option('--no-activate', 'Do not make the saved profile active')
332
+ .action(async (group, action, name, options) => {
333
+ if (group === 'status') {
334
+ const body = describeKey();
335
+ emit(body, () => console.log(`key: ${body.keyConfigured ? body.keySource : 'not configured'}`));
336
+ return;
337
+ }
338
+ if (group !== 'profile')
339
+ throw new Error(`Unknown auth command: ${group}`);
340
+ if (action === 'list') {
341
+ const data = listProfiles();
342
+ const body = { active: data.current ?? null, profiles: data.profiles.map((row) => ({ name: row.profile, settings: row.settings })) };
343
+ emit(body, () => console.log(JSON.stringify(body, null, 2)));
344
+ return;
345
+ }
346
+ if (!name)
347
+ throw new Error(`Auth profile ${action} needs a name.`);
348
+ if (action === 'use') {
349
+ setProfile(name);
350
+ const record = currentProfile().record;
351
+ emit({ name, hasStoredToken: Boolean(record?.key), account: publicProfile(record) }, () => console.log(`Profile ${name} activated.`));
352
+ return;
353
+ }
354
+ if (action === 'save') {
355
+ const activate = options.activate !== false;
356
+ if (createProfile(name, undefined, activate) === 'exists' && activate)
357
+ setProfile(name);
358
+ if (options.storeToken) {
359
+ const key = process.env.LUMENKEY_API_KEY;
360
+ if (!key)
361
+ throw new Error('LUMENKEY_API_KEY is not configured.');
362
+ await client().getBalance();
363
+ setNamedProfileFields(name, { key, host: process.env.LUMENKEY_BASE_URL });
364
+ }
365
+ const record = listProfiles().profiles.find((row) => row.profile === name);
366
+ emit({ name, active: activate, storedToken: Boolean(options.storeToken), settings: record?.settings ?? [] }, () => console.log(`Auth profile ${name} saved.`));
367
+ return;
368
+ }
369
+ if (action === 'delete') {
370
+ if (!options.yes)
371
+ throw new Error('Refusing to delete auth profile without --yes.');
372
+ const removed = deleteProfile(name);
373
+ const text = removed ? `Auth profile ${name} deleted.` : `Profile ${name} does not exist.`;
374
+ emit({ deleted: removed, name }, () => console.log(text));
375
+ return;
376
+ }
377
+ throw new Error(`Unknown auth profile command: ${action}`);
378
+ });
379
+ cli.command('doctor', 'Local environment summary. --online checks the key by reading balance')
380
+ .option('--online', 'Verify the configured key')
381
+ .action(async (options) => {
382
+ const config = loadConfig();
383
+ const checks = [
384
+ { name: 'cli', status: 'pass', version: '0.1.0' },
385
+ { name: 'runtime', status: 'pass', value: process.version },
386
+ { name: 'platform', status: 'pass', value: `${process.platform} ${os.release()}` },
387
+ { name: 'host', status: hostStatus(config), value: config.baseUrl },
388
+ { name: 'key', status: config.apiKey ? 'pass' : 'warn', configured: Boolean(config.apiKey) },
389
+ ];
390
+ if (options.online) {
391
+ try {
392
+ const balance = await new LumenKeyClient(config).getBalance();
393
+ checks.push({ name: 'onlineAuth', status: 'pass', availableCredits: balance.availableCredits });
394
+ }
395
+ catch (err) {
396
+ checks.push({ name: 'onlineAuth', status: 'fail', message: err instanceof Error ? err.message : 'online auth failed' });
397
+ }
398
+ }
399
+ const ok = checks.every((check) => check.status !== 'fail');
400
+ emit({ ok, checks }, () => {
401
+ console.log(`cli: 0.1.0`);
402
+ console.log(`runtime: ${process.version}`);
403
+ console.log(`platform: ${process.platform} ${os.release()}`);
404
+ console.log(`host: ${config.baseUrl}`);
405
+ console.log(`key: ${config.apiKey ? 'configured' : 'not configured'}`);
406
+ if (options.online)
407
+ console.log(`onlineAuth: ${checks.find((check) => check.name === 'onlineAuth')?.status}`);
408
+ });
409
+ if (!ok)
410
+ process.exitCode = 1;
411
+ });
412
+ cli.command('completion <shell>', 'Print a shell completion script')
413
+ .action((shell) => {
414
+ const script = COMPLETIONS[shell];
415
+ if (!script)
416
+ throw new Error('Shell must be bash, zsh, tcsh, or fish.');
417
+ console.log(script);
418
+ });
419
+ }
420
+ function hostStatus(config) {
421
+ try {
422
+ new LumenKeyClient({ baseUrl: config.baseUrl, timeoutMs: config.timeoutMs, apiKey: config.apiKey });
423
+ return 'pass';
424
+ }
425
+ catch {
426
+ return 'fail';
427
+ }
428
+ }
429
+ function coerceDeclared(spec, value) {
430
+ const type = spec && typeof spec === 'object' && !Array.isArray(spec) ? spec.type : undefined;
431
+ if (type === 'integer' || type === 'number') {
432
+ const parsed = Number(value);
433
+ if (!Number.isFinite(parsed))
434
+ throw new Error(`Invalid number: ${String(value)}`);
435
+ return type === 'integer' ? Math.trunc(parsed) : parsed;
436
+ }
437
+ return String(value);
438
+ }
439
+ export function outputUrls(detail) {
440
+ return (detail.outputs ?? []).map((item) => item.url).filter((url) => typeof url === 'string' && url.length > 0);
441
+ }
442
+ export async function pollConfirmed(api, jobId, options) {
443
+ const interval = Number(options.interval ?? 5);
444
+ const timeout = Number(options.timeout ?? 1800);
445
+ if (!Number.isFinite(interval) || interval < 0 || interval * 1000 > 2_147_483_647 || !Number.isFinite(timeout) || timeout < 0) {
446
+ throw new Error('--interval and --timeout must be non-negative numbers.');
447
+ }
448
+ const onSigint = () => {
449
+ api.cancelJob(jobId).catch(() => undefined).finally(() => process.exit(130));
450
+ };
451
+ process.once('SIGINT', onSigint);
452
+ try {
453
+ return await pollJob(api, jobId, {
454
+ initialIntervalMs: interval * 1000,
455
+ timeoutMs: timeout * 1000,
456
+ onProgress: (row) => process.stderr.write(` Status: ${row.status}\n`),
457
+ });
458
+ }
459
+ finally {
460
+ process.off('SIGINT', onSigint);
461
+ }
462
+ }
463
+ export async function promptForKey() {
464
+ if (!process.stdin.isTTY || process.stdin.readableEnded || process.stdin.destroyed)
465
+ return undefined;
466
+ const key = (await ask('Enter the key: ')).trim();
467
+ return key || undefined;
468
+ }
469
+ async function ask(prompt) {
470
+ if (process.stdin.readableEnded || process.stdin.destroyed)
471
+ throw new Error('stdin closed before an answer was given.');
472
+ const readline = await import('node:readline');
473
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
474
+ return await new Promise((resolve, reject) => {
475
+ let answered = false;
476
+ rl.once('line', (line) => { answered = true; rl.close(); resolve(line); });
477
+ rl.once('close', () => { if (!answered)
478
+ reject(new Error('stdin closed before an answer was given.')); });
479
+ rl.setPrompt(prompt);
480
+ rl.prompt();
481
+ });
482
+ }
483
+ async function readProfileKey() {
484
+ const key = (await ask('Enter the key: ')).trim();
485
+ if (!key)
486
+ throw new Error('Enter the key: empty.');
487
+ return key;
488
+ }
489
+ //# sourceMappingURL=copied.js.map