@lumenkey/cli 0.3.1 → 0.4.0-rc.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 +36 -2
- package/dist/cli/copied.js +86 -85
- package/dist/cli/index.js +56 -44
- package/dist/cli/render.js +3 -2
- package/dist/core/asset-stream.d.ts +16 -0
- package/dist/core/asset-stream.js +64 -0
- package/dist/core/atomic-config-file.js +8 -7
- package/dist/core/chinese-messages.d.ts +4 -0
- package/dist/core/chinese-messages.js +907 -0
- package/dist/core/chinese-script.d.ts +7 -0
- package/dist/core/chinese-script.js +28 -0
- package/dist/core/client.js +2 -1
- package/dist/core/config-dir.js +2 -1
- package/dist/core/config.js +7 -6
- package/dist/core/download-root.d.ts +5 -0
- package/dist/core/download-root.js +64 -0
- package/dist/core/download.js +15 -12
- package/dist/core/generation-input.js +3 -2
- package/dist/core/generation.d.ts +8 -1
- package/dist/core/generation.js +13 -10
- package/dist/core/job-display.js +7 -6
- package/dist/core/locale.js +163 -148
- package/dist/core/outcome.js +6 -4
- package/dist/core/output-path.d.ts +3 -1
- package/dist/core/output-path.js +5 -2
- package/dist/core/params.js +26 -25
- package/dist/core/profiles.js +5 -4
- package/dist/core/set-values.d.ts +0 -1
- package/dist/core/set-values.js +7 -7
- package/dist/core/submission-journal.js +31 -16
- package/dist/core/version.js +2 -3
- package/dist/mcp/server.js +30 -29
- package/dist/studio/asset-integrity-hash.d.ts +28 -0
- package/dist/studio/asset-integrity-hash.js +93 -0
- package/dist/studio/asset-integrity-queue.d.ts +24 -0
- package/dist/studio/asset-integrity-queue.js +94 -0
- package/dist/studio/asset-store.d.ts +52 -0
- package/dist/studio/asset-store.js +352 -0
- package/dist/studio/canvas/LICENSES.txt +1590 -0
- package/dist/studio/canvas/dependencies.json +862 -0
- package/dist/studio/canvas/director-source-manifest.json +47 -0
- package/dist/studio/canvas/editor.css +239 -0
- package/dist/studio/canvas/editor.js +4613 -0
- package/dist/studio/canvas/integration-source-manifest.json +384 -0
- package/dist/studio/canvas/source-manifest.json +240 -0
- package/dist/studio/canvas-store.d.ts +1254 -0
- package/dist/studio/canvas-store.js +205 -0
- package/dist/studio/capture-placement.d.ts +11 -0
- package/dist/studio/capture-placement.js +85 -0
- package/dist/studio/cloud-contract.d.ts +128 -0
- package/dist/studio/cloud-contract.js +229 -0
- package/dist/studio/cloud-service.d.ts +178 -0
- package/dist/studio/cloud-service.js +202 -0
- package/dist/studio/director-captures.d.ts +59 -0
- package/dist/studio/director-captures.js +141 -0
- package/dist/studio/director-contract.d.ts +1489 -0
- package/dist/studio/director-contract.js +288 -0
- package/dist/studio/director-png.d.ts +6 -0
- package/dist/studio/director-png.js +41 -0
- package/dist/studio/director-relations.d.ts +40 -0
- package/dist/studio/director-relations.js +103 -0
- package/dist/studio/document-authority.d.ts +7 -0
- package/dist/studio/document-authority.js +50 -0
- package/dist/studio/draft-store.d.ts +95 -0
- package/dist/studio/draft-store.js +100 -0
- package/dist/studio/errors.d.ts +85 -0
- package/dist/studio/errors.js +94 -0
- package/dist/studio/launch.d.ts +5 -0
- package/dist/studio/launch.js +31 -0
- package/dist/studio/media.d.ts +9 -0
- package/dist/studio/media.js +50 -0
- package/dist/studio/output-save-contract.d.ts +1321 -0
- package/dist/studio/output-save-contract.js +26 -0
- package/dist/studio/output-save-journal.d.ts +971 -0
- package/dist/studio/output-save-journal.js +287 -0
- package/dist/studio/output-save-recovery.d.ts +18 -0
- package/dist/studio/output-save-recovery.js +161 -0
- package/dist/studio/output-save-service.d.ts +436 -0
- package/dist/studio/output-save-service.js +251 -0
- package/dist/studio/output-save-worker.d.ts +40 -0
- package/dist/studio/output-save-worker.js +360 -0
- package/dist/studio/paths.d.ts +6 -0
- package/dist/studio/paths.js +68 -0
- package/dist/studio/run-journal.d.ts +993 -0
- package/dist/studio/run-journal.js +125 -0
- package/dist/studio/server.d.ts +17 -0
- package/dist/studio/server.js +440 -0
- package/dist/studio/session.d.ts +6 -0
- package/dist/studio/session.js +26 -0
- package/dist/studio/shell.d.ts +3 -0
- package/dist/studio/shell.js +23 -0
- package/dist/studio/style.d.ts +2 -0
- package/dist/studio/style.js +29 -0
- package/dist/studio/types.d.ts +26 -0
- package/dist/studio/types.js +2 -0
- package/dist/studio/web/app.d.ts +2 -0
- package/dist/studio/web/app.js +662 -0
- package/dist/studio/web/asset-controller.d.ts +37 -0
- package/dist/studio/web/asset-controller.js +191 -0
- package/dist/studio/web/asset-model.d.ts +29 -0
- package/dist/studio/web/asset-model.js +51 -0
- package/dist/studio/web/controller.d.ts +113 -0
- package/dist/studio/web/controller.js +401 -0
- package/dist/studio/web/messages.d.ts +2 -0
- package/dist/studio/web/messages.js +74 -0
- package/dist/studio/web/model.d.ts +75 -0
- package/dist/studio/web/model.js +182 -0
- package/dist/studio/web/save-controller.d.ts +85 -0
- package/dist/studio/web/save-controller.js +495 -0
- package/dist/studio/web/save-model.d.ts +63 -0
- package/dist/studio/web/save-model.js +64 -0
- package/dist/studio/web/save-queue.d.ts +7 -0
- package/dist/studio/web/save-queue.js +62 -0
- package/dist/studio/web/save-view.d.ts +9 -0
- package/dist/studio/web/save-view.js +19 -0
- package/dist/studio/writer-lock.d.ts +5 -0
- package/dist/studio/writer-lock.js +88 -0
- package/package.json +11 -5
- package/dist/cli/copied.d.ts.map +0 -1
- package/dist/cli/copied.js.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/render.d.ts.map +0 -1
- package/dist/cli/render.js.map +0 -1
- package/dist/core/atomic-config-file.d.ts.map +0 -1
- package/dist/core/atomic-config-file.js.map +0 -1
- package/dist/core/chat.d.ts.map +0 -1
- package/dist/core/chat.js.map +0 -1
- package/dist/core/client.d.ts.map +0 -1
- package/dist/core/client.js.map +0 -1
- package/dist/core/config-dir.d.ts.map +0 -1
- package/dist/core/config-dir.js.map +0 -1
- package/dist/core/config-error.d.ts.map +0 -1
- package/dist/core/config-error.js.map +0 -1
- package/dist/core/config.d.ts.map +0 -1
- package/dist/core/config.js.map +0 -1
- package/dist/core/credit-cap.d.ts.map +0 -1
- package/dist/core/credit-cap.js.map +0 -1
- package/dist/core/download.d.ts.map +0 -1
- package/dist/core/download.js.map +0 -1
- package/dist/core/error-result.d.ts.map +0 -1
- package/dist/core/error-result.js.map +0 -1
- package/dist/core/explain.d.ts.map +0 -1
- package/dist/core/explain.js.map +0 -1
- package/dist/core/generation-input.d.ts.map +0 -1
- package/dist/core/generation-input.js.map +0 -1
- package/dist/core/generation-result.d.ts.map +0 -1
- package/dist/core/generation-result.js.map +0 -1
- package/dist/core/generation.d.ts.map +0 -1
- package/dist/core/generation.js.map +0 -1
- package/dist/core/job-display.d.ts.map +0 -1
- package/dist/core/job-display.js.map +0 -1
- package/dist/core/locale.d.ts.map +0 -1
- package/dist/core/locale.js.map +0 -1
- package/dist/core/origin.d.ts.map +0 -1
- package/dist/core/origin.js.map +0 -1
- package/dist/core/outcome.d.ts.map +0 -1
- package/dist/core/outcome.js.map +0 -1
- package/dist/core/output-path.d.ts.map +0 -1
- package/dist/core/output-path.js.map +0 -1
- package/dist/core/params.d.ts.map +0 -1
- package/dist/core/params.js.map +0 -1
- package/dist/core/poller.d.ts.map +0 -1
- package/dist/core/poller.js.map +0 -1
- package/dist/core/profiles.d.ts.map +0 -1
- package/dist/core/profiles.js.map +0 -1
- package/dist/core/set-values.d.ts.map +0 -1
- package/dist/core/set-values.js.map +0 -1
- package/dist/core/submission-journal.d.ts.map +0 -1
- package/dist/core/submission-journal.js.map +0 -1
- package/dist/core/types.d.ts.map +0 -1
- package/dist/core/types.js.map +0 -1
- package/dist/core/version.d.ts.map +0 -1
- package/dist/core/version.js.map +0 -1
- package/dist/core/wait.d.ts.map +0 -1
- package/dist/core/wait.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/mcp/render.d.ts.map +0 -1
- package/dist/mcp/render.js.map +0 -1
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/mcp/server.js.map +0 -1
package/dist/core/params.js
CHANGED
|
@@ -1,48 +1,49 @@
|
|
|
1
|
+
import { zhText } from '../core/chinese-messages.js';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
import { LumenKeyError } from './client.js';
|
|
3
4
|
import { parseCreditCap } from './credit-cap.js';
|
|
4
5
|
import { asPairs, modelParameterError } from './set-values.js';
|
|
5
6
|
import { isBlankPrompt, promptRequiredSentence, ui } from './locale.js';
|
|
6
7
|
const text = (en, zh) => ({ en, zh });
|
|
7
|
-
const modelId = { name: 'modelId', kind: 'string', required: true, flag: '--model', aliases: ['-m'], placeholder: '<modelId>', describe: text('Model ID',
|
|
8
|
-
const prompt = { name: 'prompt', kind: 'string', required: true, flag: '--prompt', aliases: ['-p'], placeholder: '<prompt>', describe: text('Prompt for the model',
|
|
9
|
-
const confirm = { name: 'confirm', kind: 'boolean', flag: '--confirm', describe: text('Explicitly submit this billable generation; otherwise quote only',
|
|
10
|
-
const dryRun = { name: 'dryRun', kind: 'boolean', flag: '--dry-run', only: 'cli', describe: text('Print the projected input and do not quote',
|
|
11
|
-
const idempotencyKey = { name: 'idempotencyKey', kind: 'string', flag: '--idempotency-key', placeholder: '<key>', describe: text('Optional. Generated when omitted. Reuse it only to recover a submission whose outcome was unknown; the recorded body is re-sent unchanged',
|
|
12
|
-
const maxCredits = { name: 'maxCredits', kind: 'number', constraint: 'integer-nonnegative', flag: '--max-credits', placeholder: '<credits>', describe: text('Reject quotes above this spending cap (non-negative integer)',
|
|
13
|
-
const projectId = { name: 'projectId', kind: 'string', only: 'mcp', describe: text('Leave empty. The client fills this.',
|
|
14
|
-
const downloadPath = { name: 'downloadPath', kind: 'string', flag: '--output', aliases: ['-o'], placeholder: '<path>', describe: text('Save the first result to this file path (absolute or ~/...). Omit it to receive the URLs only',
|
|
15
|
-
const set = { name: 'set', kind: 'string[]', flag: '--set', aliases: ['-s'], placeholder: '<pair>', describe: text('Extra key=value declared by the model schema. Repeatable',
|
|
16
|
-
const intervalMs = { name: 'intervalMs', kind: 'number', constraint: 'positive', flag: '--interval', placeholder: '<seconds>', default: 5000, describe: text('Polling interval; defaults to 5 seconds',
|
|
17
|
-
const timeoutMs = { name: 'timeoutMs', kind: 'number', constraint: 'nonnegative', flag: '--timeout', placeholder: '<seconds>', default: 1_800_000, describe: text('Wait timeout; defaults to 1800 seconds. Does not cancel the task',
|
|
18
|
-
const resolution = { name: 'resolution', kind: 'string', flag: '--resolution', aliases: ['-r'], placeholder: '<res>', describe: text('Resolution, only if the catalog declares it',
|
|
19
|
-
const ratio = { name: 'ratio', kind: 'string', flag: '--aspect-ratio', aliases: ['--ratio'], placeholder: '<ratio>', describe: text('Aspect ratio, only if the catalog declares aspect_ratio or ratio',
|
|
20
|
-
const count = { name: 'count', kind: 'numeric', flag: '--count', placeholder: '<count>', describe: text('Count, only if the catalog declares it; validated against the catalog type',
|
|
8
|
+
const modelId = { name: 'modelId', kind: 'string', required: true, flag: '--model', aliases: ['-m'], placeholder: '<modelId>', describe: text('Model ID', zhText("m238")) };
|
|
9
|
+
const prompt = { name: 'prompt', kind: 'string', required: true, flag: '--prompt', aliases: ['-p'], placeholder: '<prompt>', describe: text('Prompt for the model', zhText("m237")) };
|
|
10
|
+
const confirm = { name: 'confirm', kind: 'boolean', flag: '--confirm', describe: text('Explicitly submit this billable generation; otherwise quote only', zhText("m236")) };
|
|
11
|
+
const dryRun = { name: 'dryRun', kind: 'boolean', flag: '--dry-run', only: 'cli', describe: text('Print the projected input and do not quote', zhText("m235")) };
|
|
12
|
+
const idempotencyKey = { name: 'idempotencyKey', kind: 'string', flag: '--idempotency-key', placeholder: '<key>', describe: text('Optional. Generated when omitted. Reuse it only to recover a submission whose outcome was unknown; the recorded body is re-sent unchanged', zhText("m234")) };
|
|
13
|
+
const maxCredits = { name: 'maxCredits', kind: 'number', constraint: 'integer-nonnegative', flag: '--max-credits', placeholder: '<credits>', describe: text('Reject quotes above this spending cap (non-negative integer)', zhText("m233")) };
|
|
14
|
+
const projectId = { name: 'projectId', kind: 'string', only: 'mcp', describe: text('Leave empty. The client fills this.', zhText("m232")) };
|
|
15
|
+
const downloadPath = { name: 'downloadPath', kind: 'string', flag: '--output', aliases: ['-o'], placeholder: '<path>', describe: text('Save the first result to this file path (absolute or ~/...). Omit it to receive the URLs only', zhText("m231")) };
|
|
16
|
+
const set = { name: 'set', kind: 'string[]', flag: '--set', aliases: ['-s'], placeholder: '<pair>', describe: text('Extra key=value declared by the model schema. Repeatable', zhText("m230")) };
|
|
17
|
+
const intervalMs = { name: 'intervalMs', kind: 'number', constraint: 'positive', flag: '--interval', placeholder: '<seconds>', default: 5000, describe: text('Polling interval; defaults to 5 seconds', zhText("m229")) };
|
|
18
|
+
const timeoutMs = { name: 'timeoutMs', kind: 'number', constraint: 'nonnegative', flag: '--timeout', placeholder: '<seconds>', default: 1_800_000, describe: text('Wait timeout; defaults to 1800 seconds. Does not cancel the task', zhText("m228")) };
|
|
19
|
+
const resolution = { name: 'resolution', kind: 'string', flag: '--resolution', aliases: ['-r'], placeholder: '<res>', describe: text('Resolution, only if the catalog declares it', zhText("m227")) };
|
|
20
|
+
const ratio = { name: 'ratio', kind: 'string', flag: '--aspect-ratio', aliases: ['--ratio'], placeholder: '<ratio>', describe: text('Aspect ratio, only if the catalog declares aspect_ratio or ratio', zhText("m226")) };
|
|
21
|
+
const count = { name: 'count', kind: 'numeric', flag: '--count', placeholder: '<count>', describe: text('Count, only if the catalog declares it; validated against the catalog type', zhText("m225")) };
|
|
21
22
|
export const videoParams = [
|
|
22
23
|
modelId, prompt,
|
|
23
|
-
{ name: 'imageUrl', kind: 'string', flag: '--image', aliases: ['-i'], placeholder: '<imageUrl>', describe: text('Source image URL for image-to-video, only if the catalog declares it',
|
|
24
|
+
{ name: 'imageUrl', kind: 'string', flag: '--image', aliases: ['-i'], placeholder: '<imageUrl>', describe: text('Source image URL for image-to-video, only if the catalog declares it', zhText("m224")) },
|
|
24
25
|
// No numeric constraint on duration: the catalog decides what a valid duration is (an explicit 0 is passed through, as in 0.2.3).
|
|
25
|
-
{ name: 'duration', kind: 'number', flag: '--duration', aliases: ['-d'], placeholder: '<seconds>', describe: text('Video duration in seconds; omitted scalar catalog defaults are explicitly sent',
|
|
26
|
+
{ name: 'duration', kind: 'number', flag: '--duration', aliases: ['-d'], placeholder: '<seconds>', describe: text('Video duration in seconds; omitted scalar catalog defaults are explicitly sent', zhText("m223")) },
|
|
26
27
|
resolution, ratio, count, set, dryRun, confirm, idempotencyKey, maxCredits, projectId, downloadPath, intervalMs, timeoutMs,
|
|
27
28
|
];
|
|
28
29
|
export const imageParams = [
|
|
29
30
|
modelId, prompt,
|
|
30
|
-
{ name: 'imageUrl', kind: 'string', flag: '--image', placeholder: '<imageUrl>', describe: text('Reference image URL, only if the catalog declares it',
|
|
31
|
+
{ name: 'imageUrl', kind: 'string', flag: '--image', placeholder: '<imageUrl>', describe: text('Reference image URL, only if the catalog declares it', zhText("m222")) },
|
|
31
32
|
{ ...resolution, aliases: undefined }, ratio,
|
|
32
|
-
{ name: 'size', kind: 'string', flag: '--size', placeholder: '<size>', describe: text('Legacy size, only for models whose catalog schema declares size',
|
|
33
|
+
{ name: 'size', kind: 'string', flag: '--size', placeholder: '<size>', describe: text('Legacy size, only for models whose catalog schema declares size', zhText("m221")) },
|
|
33
34
|
count, set, dryRun, confirm, idempotencyKey, maxCredits, projectId, downloadPath, intervalMs, timeoutMs,
|
|
34
35
|
];
|
|
35
36
|
export const chatParams = [
|
|
36
|
-
{ ...modelId, required: false, default: 'qwen3.7-flash', describe: text('Model ID (e.g. qwen3.7-flash, qwen3.8-max, grok-4.6)',
|
|
37
|
-
{ ...prompt, describe: text('User message to send to the model',
|
|
38
|
-
{ name: 'systemPrompt', kind: 'string', flag: '--system-prompt', placeholder: '<prompt>', describe: text('Optional system instruction; a blank one is not sent',
|
|
39
|
-
{ ...maxCredits, describe: text('Reject before inference if the required reservation exceeds this spending cap (non-negative integer)',
|
|
40
|
-
{ ...idempotencyKey, describe: text('Reuse only for an identical request; after a timeout, retry the identical request with the key that was reported',
|
|
37
|
+
{ ...modelId, required: false, default: 'qwen3.7-flash', describe: text('Model ID (e.g. qwen3.7-flash, qwen3.8-max, grok-4.6)', zhText("m220")) },
|
|
38
|
+
{ ...prompt, describe: text('User message to send to the model', zhText("m219")) },
|
|
39
|
+
{ name: 'systemPrompt', kind: 'string', flag: '--system-prompt', placeholder: '<prompt>', describe: text('Optional system instruction; a blank one is not sent', zhText("m218")) },
|
|
40
|
+
{ ...maxCredits, describe: text('Reject before inference if the required reservation exceeds this spending cap (non-negative integer)', zhText("m217")) },
|
|
41
|
+
{ ...idempotencyKey, describe: text('Reuse only for an identical request; after a timeout, retry the identical request with the key that was reported', zhText("m216")) },
|
|
41
42
|
];
|
|
42
43
|
export const waitParams = [intervalMs, timeoutMs];
|
|
43
44
|
/** `tasks resume <key>` / `lumenkey_resume_submission`: the key plus wait and output options; no generation argument. */
|
|
44
45
|
export const resumeParams = [
|
|
45
|
-
{ ...idempotencyKey, required: true, only: 'mcp', describe: text('The idempotency key of the recorded submission to re-send verbatim',
|
|
46
|
+
{ ...idempotencyKey, required: true, only: 'mcp', describe: text('The idempotency key of the recorded submission to re-send verbatim', zhText("m215")) },
|
|
46
47
|
downloadPath, intervalMs, timeoutMs,
|
|
47
48
|
];
|
|
48
49
|
export function forSurface(spec, surface) {
|
|
@@ -126,7 +127,7 @@ export function waitMs(value, surface, fallbackMs) {
|
|
|
126
127
|
}
|
|
127
128
|
function shortIntervalWarning(intervalMs, surface) {
|
|
128
129
|
if (surface === 'cli' && intervalMs > 0 && intervalMs < 1000) {
|
|
129
|
-
process.stderr.write(ui('Warning [POLL_INTERVAL_SHORT]: --interval uses seconds. Values below 1 second may cause excessive polling; consider --interval 5.\n',
|
|
130
|
+
process.stderr.write(ui('Warning [POLL_INTERVAL_SHORT]: --interval uses seconds. Values below 1 second may cause excessive polling; consider --interval 5.\n', zhText("m214")));
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
133
|
function countValue(value) {
|
package/dist/core/profiles.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { zhText } from '../core/chinese-messages.js';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import os from 'node:os';
|
|
@@ -25,7 +26,7 @@ export class ProfileError extends LumenKeyError {
|
|
|
25
26
|
}
|
|
26
27
|
export class ProfileMissingError extends ProfileError {
|
|
27
28
|
constructor(name) {
|
|
28
|
-
super('PROFILE_MISSING', ui(`Profile ${name} does not exist.`,
|
|
29
|
+
super('PROFILE_MISSING', ui(`Profile ${name} does not exist.`, (zhText("m242") + (name) + zhText("m243"))));
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
function filePath(home = os.homedir()) {
|
|
@@ -52,18 +53,18 @@ function object(value) {
|
|
|
52
53
|
}
|
|
53
54
|
function validName(name) {
|
|
54
55
|
if (!name.trim() || Object.prototype.hasOwnProperty.call(Object.prototype, name)) {
|
|
55
|
-
throw new ProfileError('PROFILE_INVALID', ui('Invalid profile name.',
|
|
56
|
+
throw new ProfileError('PROFILE_INVALID', ui('Invalid profile name.', zhText("m241")));
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
function validate(value) {
|
|
59
60
|
if (!object(value) || !object(value.profiles) || (value.current !== undefined && typeof value.current !== 'string')) {
|
|
60
|
-
throw new ProfileError('PROFILE_INVALID', ui('profiles.json must contain a profiles object.',
|
|
61
|
+
throw new ProfileError('PROFILE_INVALID', ui('profiles.json must contain a profiles object.', zhText("m240")));
|
|
61
62
|
}
|
|
62
63
|
const profiles = Object.create(null);
|
|
63
64
|
for (const [name, record] of Object.entries(value.profiles)) {
|
|
64
65
|
validName(name);
|
|
65
66
|
if (!object(record) || ['key', 'host', 'projectId'].some(key => record[key] !== undefined && typeof record[key] !== 'string')) {
|
|
66
|
-
throw new ProfileError('PROFILE_INVALID', ui('Profile records must be objects with string fields.',
|
|
67
|
+
throw new ProfileError('PROFILE_INVALID', ui('Profile records must be objects with string fields.', zhText("m239")));
|
|
67
68
|
}
|
|
68
69
|
profiles[name] = Object.fromEntries(['key', 'host', 'projectId'].filter(key => record[key] !== undefined).map(key => [key, record[key]]));
|
|
69
70
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Pixmax `_parse_value` plus fal's first `=` / `:=` split and `[index]` / `[]` paths. */
|
|
2
1
|
import { LumenKeyError } from './client.js';
|
|
3
2
|
/** Identify a parameter without echoing its value, a whole pair, or a payload. */
|
|
4
3
|
export declare function modelParameterError(code: 'MODEL_PARAM_INVALID' | 'MODEL_PARAM_UNKNOWN', key: string): LumenKeyError;
|
package/dist/core/set-values.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
import { zhText } from '../core/chinese-messages.js';
|
|
2
2
|
import { LumenKeyError } from './client.js';
|
|
3
3
|
import { ui } from './locale.js';
|
|
4
4
|
/** Identify a parameter without echoing its value, a whole pair, or a payload. */
|
|
5
5
|
export function modelParameterError(code, key) {
|
|
6
6
|
if (!/^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(key)) {
|
|
7
7
|
return new LumenKeyError(code, undefined, undefined, code === 'MODEL_PARAM_UNKNOWN'
|
|
8
|
-
? ui('This model does not support the supplied parameter.',
|
|
9
|
-
: ui('The supplied parameter is invalid. Check lumenkey model explain.',
|
|
8
|
+
? ui('This model does not support the supplied parameter.', zhText("m250"))
|
|
9
|
+
: ui('The supplied parameter is invalid. Check lumenkey model explain.', zhText("m249")));
|
|
10
10
|
}
|
|
11
11
|
const label = key === 'aspect_ratio' || key === 'ratio'
|
|
12
|
-
? ui('aspect ratio',
|
|
12
|
+
? ui('aspect ratio', zhText("m248"))
|
|
13
13
|
: key;
|
|
14
14
|
const detail = code === 'MODEL_PARAM_UNKNOWN'
|
|
15
|
-
? ui(`This model has no ${label} parameter.`,
|
|
16
|
-
: ui(`Invalid value for ${label}. Check this model's allowed parameters with lumenkey model explain.`,
|
|
15
|
+
? ui(`This model has no ${label} parameter.`, (zhText("m246") + (label) + zhText("m247")))
|
|
16
|
+
: ui(`Invalid value for ${label}. Check this model's allowed parameters with lumenkey model explain.`, ("" + (label) + zhText("m245")));
|
|
17
17
|
return new LumenKeyError(code, undefined, undefined, detail);
|
|
18
18
|
}
|
|
19
19
|
export function parseValue(value) {
|
|
@@ -49,7 +49,7 @@ export function parsePath(key) {
|
|
|
49
49
|
if (!name)
|
|
50
50
|
throw new LumenKeyError('MODEL_PARAM_SYNTAX_INVALID');
|
|
51
51
|
if (name === '__proto__' || name === 'constructor' || name === 'prototype')
|
|
52
|
-
throw new LumenKeyError('MODEL_PARAM_SYNTAX_INVALID', undefined, undefined, ui('Unsafe key in key=value option.',
|
|
52
|
+
throw new LumenKeyError('MODEL_PARAM_SYNTAX_INVALID', undefined, undefined, ui('Unsafe key in key=value option.', zhText("m244")));
|
|
53
53
|
steps.push({ kind: 'key', name });
|
|
54
54
|
};
|
|
55
55
|
while (i < key.length) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { zhText } from '../core/chinese-messages.js';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -68,13 +69,27 @@ function pastRetention(entry, now = Date.now()) {
|
|
|
68
69
|
}
|
|
69
70
|
export function ageText(entry) {
|
|
70
71
|
const hours = Math.floor(ageMs(entry) / 3600000);
|
|
71
|
-
return hours >= 1 ? ui(`${hours} hours ago`,
|
|
72
|
+
return hours >= 1 ? ui(`${hours} hours ago`, ("" + (hours) + zhText("m270"))) : ui(`${Math.max(1, Math.floor(ageMs(entry) / 60000))} minutes ago`, ("" + (Math.max(1, Math.floor(ageMs(entry) / 60000))) + zhText("m269")));
|
|
72
73
|
}
|
|
73
74
|
function journalError(code, file, error, detail) {
|
|
74
|
-
return Object.assign(new LumenKeyError(code, undefined, undefined, detail), {
|
|
75
|
+
return Object.assign(new LumenKeyError(code, undefined, undefined, detail), { recordId: recordIdentifier(file), cause: error });
|
|
76
|
+
}
|
|
77
|
+
function recordIdentifier(file) {
|
|
78
|
+
return `record:${crypto.createHash('sha256').update(path.basename(file)).digest('hex').slice(0, 12)}`;
|
|
75
79
|
}
|
|
76
80
|
function reasonOf(error) {
|
|
77
|
-
|
|
81
|
+
const code = error?.code;
|
|
82
|
+
return typeof code === 'string' && /^[A-Z][A-Z0-9_]{0,79}$/.test(code) ? code : 'FILESYSTEM_ERROR';
|
|
83
|
+
}
|
|
84
|
+
function assertJournalReadable(file) {
|
|
85
|
+
try {
|
|
86
|
+
assertConfigDirReadable(file);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error instanceof LumenKeyError)
|
|
90
|
+
throw Object.assign(new LumenKeyError(error.code, error.status, error.retryAfterMs, error.publicMessage), { recordId: recordIdentifier(file) });
|
|
91
|
+
throw journalError('SUBMISSION_JOURNAL_INVALID', file, error, errorSentence('SUBMISSION_JOURNAL_INVALID'));
|
|
92
|
+
}
|
|
78
93
|
}
|
|
79
94
|
/**
|
|
80
95
|
* Prove the journal can be written before anything billable happens, so a refused submission
|
|
@@ -91,7 +106,7 @@ export function assertJournalWritable(home) {
|
|
|
91
106
|
fs.unlinkSync(probe);
|
|
92
107
|
}
|
|
93
108
|
catch (error) {
|
|
94
|
-
throw journalError('SUBMISSION_JOURNAL_UNWRITABLE', dir, error, `${errorSentence('SUBMISSION_JOURNAL_UNWRITABLE')} ${ui(`No job was submitted.
|
|
109
|
+
throw journalError('SUBMISSION_JOURNAL_UNWRITABLE', dir, error, `${errorSentence('SUBMISSION_JOURNAL_UNWRITABLE')} ${ui(`No job was submitted. The pending-submissions journal directory is unavailable (${reasonOf(error)}). Set LUMENKEY_JOURNAL_DIR to a writable directory to keep pending records elsewhere.`, (zhText("m267") + (reasonOf(error)) + zhText("m268")))}`);
|
|
95
110
|
}
|
|
96
111
|
}
|
|
97
112
|
function write(entry, home) {
|
|
@@ -101,7 +116,7 @@ function write(entry, home) {
|
|
|
101
116
|
writeAtomicConfigFile(file, entry);
|
|
102
117
|
}
|
|
103
118
|
catch (error) {
|
|
104
|
-
throw journalError('SUBMISSION_JOURNAL_UNWRITABLE', file, error, `${errorSentence('SUBMISSION_JOURNAL_UNWRITABLE')} ${ui(`
|
|
119
|
+
throw journalError('SUBMISSION_JOURNAL_UNWRITABLE', file, error, `${errorSentence('SUBMISSION_JOURNAL_UNWRITABLE')} ${ui(`Record ${recordIdentifier(file)} (${reasonOf(error)}).`, (zhText("m266") + (recordIdentifier(file)) + "(" + (reasonOf(error)) + ")。"))}`);
|
|
105
120
|
}
|
|
106
121
|
}
|
|
107
122
|
/** Write before the POST. Any failure here must stop the submission: a job without a record cannot be recovered. */
|
|
@@ -111,7 +126,7 @@ export function rememberPendingSubmission(entry, home) {
|
|
|
111
126
|
}
|
|
112
127
|
catch (error) {
|
|
113
128
|
if (error instanceof LumenKeyError)
|
|
114
|
-
error.publicMessage = `${error.publicMessage} ${ui('No job was submitted.',
|
|
129
|
+
error.publicMessage = `${error.publicMessage} ${ui('No job was submitted.', zhText("m265"))}`;
|
|
115
130
|
throw error;
|
|
116
131
|
}
|
|
117
132
|
sweep(home);
|
|
@@ -119,9 +134,9 @@ export function rememberPendingSubmission(entry, home) {
|
|
|
119
134
|
/** Read a record by key without any expiry side effect. */
|
|
120
135
|
function readRaw(idempotencyKey, home) {
|
|
121
136
|
const file = entryPath(idempotencyKey, home);
|
|
122
|
-
assertConfigDirReadable(file);
|
|
123
137
|
let entry;
|
|
124
138
|
try {
|
|
139
|
+
assertConfigDirReadable(file);
|
|
125
140
|
assertSafeConfigFile(file);
|
|
126
141
|
if (!fs.existsSync(file))
|
|
127
142
|
return undefined;
|
|
@@ -129,7 +144,7 @@ function readRaw(idempotencyKey, home) {
|
|
|
129
144
|
}
|
|
130
145
|
catch (error) {
|
|
131
146
|
if (error instanceof LumenKeyError)
|
|
132
|
-
throw Object.assign(error, {
|
|
147
|
+
throw Object.assign(new LumenKeyError(error.code, error.status, error.retryAfterMs, error.publicMessage), { recordId: recordIdentifier(file) });
|
|
133
148
|
throw journalError('SUBMISSION_JOURNAL_INVALID', file, error, errorSentence('SUBMISSION_JOURNAL_INVALID'));
|
|
134
149
|
}
|
|
135
150
|
if (entry.idempotencyKey !== idempotencyKey)
|
|
@@ -165,7 +180,7 @@ export function forgetPendingSubmission(idempotencyKey, quoteId, home) {
|
|
|
165
180
|
catch (error) {
|
|
166
181
|
if (error.code === 'ENOENT')
|
|
167
182
|
return false;
|
|
168
|
-
throw journalError('SUBMISSION_JOURNAL_UNWRITABLE', file, error, ui(`The pending record ${file} could not be removed (${reasonOf(error)}); it will be swept after 48 hours.`,
|
|
183
|
+
throw journalError('SUBMISSION_JOURNAL_UNWRITABLE', file, error, ui(`The pending record ${recordIdentifier(file)} could not be removed (${reasonOf(error)}); it will be swept after 48 hours.`, (zhText("m262") + (recordIdentifier(file)) + zhText("m263") + (reasonOf(error)) + zhText("m264"))));
|
|
169
184
|
}
|
|
170
185
|
}
|
|
171
186
|
/**
|
|
@@ -240,7 +255,7 @@ export function isInputPolicyConflict(entry, input, callerInput, defaults, legac
|
|
|
240
255
|
}
|
|
241
256
|
/** Before lookup: retain the replay guard for 23h, then report expiry; sweep after 48h. */
|
|
242
257
|
export function findInputPolicyConflict(identity, modelId, input, callerInput, defaults, home, legacyCallerInput) {
|
|
243
|
-
|
|
258
|
+
assertJournalReadable(path.join(journalDir(home), 'probe'));
|
|
244
259
|
sweep(home);
|
|
245
260
|
return readAll(home).entries
|
|
246
261
|
.filter(entry => sameSender(entry, identity) && entry.modelId === modelId && !isExpired(entry) && isInputPolicyConflict(entry, input, callerInput, defaults, legacyCallerInput))
|
|
@@ -265,21 +280,21 @@ export function processAlive(pid) {
|
|
|
265
280
|
* are invisible here.
|
|
266
281
|
*/
|
|
267
282
|
export function findResumablePendingSubmission(identity, modelId, canonicalInput, home) {
|
|
268
|
-
|
|
283
|
+
assertJournalReadable(path.join(journalDir(home), 'probe'));
|
|
269
284
|
const { entries, damaged } = readAll(home);
|
|
270
|
-
const notices = damaged.map(file => ui(`${errorSentence('SUBMISSION_JOURNAL_INVALID')} (SUBMISSION_JOURNAL_INVALID) ${file}`, `${errorSentence('SUBMISSION_JOURNAL_INVALID')}(SUBMISSION_JOURNAL_INVALID)${file}`));
|
|
285
|
+
const notices = damaged.map(file => ui(`${errorSentence('SUBMISSION_JOURNAL_INVALID')} (SUBMISSION_JOURNAL_INVALID) ${recordIdentifier(file)}`, `${errorSentence('SUBMISSION_JOURNAL_INVALID')}(SUBMISSION_JOURNAL_INVALID)${recordIdentifier(file)}`));
|
|
271
286
|
const candidates = [];
|
|
272
287
|
for (const entry of entries) {
|
|
273
288
|
if (!sameSender(entry, identity) || entry.modelId !== modelId || pastRetention(entry))
|
|
274
289
|
continue;
|
|
275
290
|
if (isExpired(entry)) {
|
|
276
|
-
notices.push(ui(`An unconfirmed submission recorded ${ageText(entry)} (idempotency key ${entry.idempotencyKey}, quote ${entry.quoteId}) has expired past its replay window; check job history before continuing. It was not re-sent.`,
|
|
291
|
+
notices.push(ui(`An unconfirmed submission recorded ${ageText(entry)} (idempotency key ${entry.idempotencyKey}, quote ${entry.quoteId}) has expired past its replay window; check job history before continuing. It was not re-sent.`, (zhText("m260") + (entry.idempotencyKey) + zhText("m34") + (entry.quoteId) + zhText("m29") + (ageText(entry)) + zhText("m261"))));
|
|
277
292
|
continue;
|
|
278
293
|
}
|
|
279
294
|
const sameInput = canonical(entry.input) === canonicalInput;
|
|
280
295
|
if (!sameInput) {
|
|
281
296
|
if (entry.state === 'unknown' || (entry.hostname === os.hostname() && !processAlive(entry.pid)))
|
|
282
|
-
notices.push(ui(`A submission for this model with a different input was recorded ${ageText(entry)} (idempotency key ${entry.idempotencyKey}, quote ${entry.quoteId}) and is still unconfirmed; it was not resumed because the input differs. If it is the one you meant (for example after a catalog change), run lumenkey tasks resume ${entry.idempotencyKey} (MCP: lumenkey_resume_submission).`,
|
|
297
|
+
notices.push(ui(`A submission for this model with a different input was recorded ${ageText(entry)} (idempotency key ${entry.idempotencyKey}, quote ${entry.quoteId}) and is still unconfirmed; it was not resumed because the input differs. If it is the one you meant (for example after a catalog change), run lumenkey tasks resume ${entry.idempotencyKey} (MCP: lumenkey_resume_submission).`, (zhText("m257") + (ageText(entry)) + zhText("m258") + (entry.idempotencyKey) + zhText("m34") + (entry.quoteId) + zhText("m259") + (entry.idempotencyKey) + "(MCP:lumenkey_resume_submission)。")));
|
|
283
298
|
continue;
|
|
284
299
|
}
|
|
285
300
|
if (entry.state === 'unknown') {
|
|
@@ -287,11 +302,11 @@ export function findResumablePendingSubmission(identity, modelId, canonicalInput
|
|
|
287
302
|
continue;
|
|
288
303
|
}
|
|
289
304
|
if (entry.hostname !== os.hostname()) {
|
|
290
|
-
notices.push(ui(`An identical submission is still in flight from host ${entry.hostname} (idempotency key ${entry.idempotencyKey}); it was not resumed from here.`,
|
|
305
|
+
notices.push(ui(`An identical submission is still in flight from host ${entry.hostname} (idempotency key ${entry.idempotencyKey}); it was not resumed from here.`, (zhText("m254") + (entry.hostname) + zhText("m255") + (entry.idempotencyKey) + zhText("m256"))));
|
|
291
306
|
continue;
|
|
292
307
|
}
|
|
293
308
|
if (processAlive(entry.pid)) {
|
|
294
|
-
notices.push(ui(`An identical submission is still in flight in process ${entry.pid} on this host (idempotency key ${entry.idempotencyKey}); it was not resumed. If that process is gone, run lumenkey tasks resume ${entry.idempotencyKey} (MCP: lumenkey_resume_submission).`,
|
|
309
|
+
notices.push(ui(`An identical submission is still in flight in process ${entry.pid} on this host (idempotency key ${entry.idempotencyKey}); it was not resumed. If that process is gone, run lumenkey tasks resume ${entry.idempotencyKey} (MCP: lumenkey_resume_submission).`, (zhText("m251") + (entry.pid) + zhText("m252") + (entry.idempotencyKey) + zhText("m253") + (entry.idempotencyKey) + "(MCP:lumenkey_resume_submission)。")));
|
|
295
310
|
continue;
|
|
296
311
|
}
|
|
297
312
|
candidates.push(entry);
|
package/dist/core/version.js
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export const packageVersion =
|
|
3
|
-
//# sourceMappingURL=version.js.map
|
|
1
|
+
// Generated from package.json by stamp-build-version.mjs.
|
|
2
|
+
export const packageVersion = "0.4.0-rc.1";
|
package/dist/mcp/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { zhText } from '../core/chinese-messages.js';
|
|
1
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
4
|
import { z } from 'zod';
|
|
@@ -16,18 +17,18 @@ function log(line) {
|
|
|
16
17
|
}
|
|
17
18
|
/** MCP view of generation progress; the same core events the CLI renders, on stderr. */
|
|
18
19
|
function mcpGenerationEvents() {
|
|
19
|
-
const nounOf = (kind) => kind === 'video' ? ui('video',
|
|
20
|
+
const nounOf = (kind) => kind === 'video' ? ui('video', zhText("m442")) : ui('image', zhText("m441"));
|
|
20
21
|
return event => {
|
|
21
22
|
switch (event.type) {
|
|
22
|
-
case 'quoting': return log(ui(`Quoting ${nounOf(event.kind)} generation for ${event.modelId}...`,
|
|
23
|
+
case 'quoting': return log(ui(`Quoting ${nounOf(event.kind)} generation for ${event.modelId}...`, (zhText("m438") + (event.modelId) + zhText("m439") + (nounOf(event.kind)) + zhText("m440"))));
|
|
23
24
|
case 'submitting': return log(event.resubmit
|
|
24
|
-
? ui(`Re-sending an earlier unconfirmed submission (idempotency key ${event.idempotencyKey}, quote ${event.quoteId}, ${event.credits} credits) instead of creating a new job...`,
|
|
25
|
-
: ui(`Submitting job with quote ${event.quoteId} (${event.credits} credits)...`,
|
|
26
|
-
case 'job_created': return log(ui(`Job created: ${event.jobId} (idempotency key ${event.idempotencyKey}, quote ${event.quoteId})`,
|
|
27
|
-
case 'polling': return log(ui(`Polling job ${event.jobId}...`,
|
|
28
|
-
case 'progress': return log(ui(`Job ${event.detail.jobId} status: ${event.detail.status}`,
|
|
29
|
-
case 'downloading': return log(ui(`Downloading ${nounOf(event.kind)}...`,
|
|
30
|
-
case 'notice': return log(ui(`Note: ${event.text}`,
|
|
25
|
+
? ui(`Re-sending an earlier unconfirmed submission (idempotency key ${event.idempotencyKey}, quote ${event.quoteId}, ${event.credits} credits) instead of creating a new job...`, (zhText("m354") + (event.idempotencyKey) + zhText("m434") + (event.quoteId) + "," + (event.credits) + zhText("m437")))
|
|
26
|
+
: ui(`Submitting job with quote ${event.quoteId} (${event.credits} credits)...`, (zhText("m435") + (event.quoteId) + "(" + (event.credits) + zhText("m436"))));
|
|
27
|
+
case 'job_created': return log(ui(`Job created: ${event.jobId} (idempotency key ${event.idempotencyKey}, quote ${event.quoteId})`, (zhText("m351") + (event.jobId) + zhText("m258") + (event.idempotencyKey) + zhText("m434") + (event.quoteId) + ")")));
|
|
28
|
+
case 'polling': return log(ui(`Polling job ${event.jobId}...`, (zhText("m432") + (event.jobId) + zhText("m433"))));
|
|
29
|
+
case 'progress': return log(ui(`Job ${event.detail.jobId} status: ${event.detail.status}`, (zhText("m430") + (event.detail.jobId) + zhText("m431") + (jobStatusText(event.detail.status)) + "")));
|
|
30
|
+
case 'downloading': return log(ui(`Downloading ${nounOf(event.kind)}...`, (zhText("m429") + (nounOf(event.kind)) + "...")));
|
|
31
|
+
case 'notice': return log(ui(`Note: ${event.text}`, (zhText("m350") + (event.text) + "")));
|
|
31
32
|
default: return;
|
|
32
33
|
}
|
|
33
34
|
};
|
|
@@ -54,7 +55,7 @@ function inputError(toolName, issues, args) {
|
|
|
54
55
|
if (field === 'duration')
|
|
55
56
|
return new LumenKeyError('VIDEO_DURATION_INVALID');
|
|
56
57
|
if (issue.code === 'unrecognized_keys') {
|
|
57
|
-
return new LumenKeyError('INPUT_INVALID', undefined, undefined, ui(`Unknown argument: ${issue.keys.join(', ')}.`,
|
|
58
|
+
return new LumenKeyError('INPUT_INVALID', undefined, undefined, ui(`Unknown argument: ${issue.keys.join(', ')}.`, (zhText("m428") + (issue.keys.join('、')) + "。")));
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
return new LumenKeyError('INPUT_INVALID', undefined, undefined, issues.map(issue => `${issue.path.join('.') || 'arguments'}: ${issue.message}`).join(' '));
|
|
@@ -91,42 +92,42 @@ export function createMcpServer() {
|
|
|
91
92
|
}
|
|
92
93
|
});
|
|
93
94
|
}
|
|
94
|
-
tool('lumenkey_list_models', ui('List all available AI models (video, image, text, audio) from LumenKey catalog with schemas and pricing',
|
|
95
|
-
type: z.enum(['video', 'image', 'text', 'audio']).optional().describe(ui('Filter models by type',
|
|
95
|
+
tool('lumenkey_list_models', ui('List all available AI models (video, image, text, audio) from LumenKey catalog with schemas and pricing', zhText("m427")), {
|
|
96
|
+
type: z.enum(['video', 'image', 'text', 'audio']).optional().describe(ui('Filter models by type', zhText("m426"))),
|
|
96
97
|
}, async ({ type }) => {
|
|
97
98
|
const models = await api().listModels(type);
|
|
98
99
|
return dataResult({ count: models.length, models });
|
|
99
100
|
});
|
|
100
|
-
tool('lumenkey_generate_video', ui('Quote a LumenKey video. Omitted scalar catalog defaults are explicitly sent in input. Nothing is submitted until confirm is true. Conflicting parameter aliases are rejected locally.',
|
|
101
|
-
tool('lumenkey_generate_image', ui('Quote a LumenKey image. Omitted scalar catalog defaults are explicitly sent in input. Nothing is submitted until confirm is true. Conflicting parameter aliases are rejected locally.',
|
|
102
|
-
tool('lumenkey_query_job', ui('Query the status of an asynchronous generation task by its jobId',
|
|
103
|
-
tool('lumenkey_check_balance', ui('Query current LumenKey account credits balance (available credits, reserved credits, and updatedAt timestamp)',
|
|
104
|
-
tool('lumenkey_chat', ui('Run billable LLM inference. Every successful call may incur credits; reuse idempotencyKey only for an identical request.',
|
|
101
|
+
tool('lumenkey_generate_video', ui('Quote a LumenKey video. Omitted scalar catalog defaults are explicitly sent in input. Nothing is submitted until confirm is true. Conflicting parameter aliases are rejected locally.', zhText("m425")), zodShape(videoParams), async (args) => toolResult(await runGeneration(api(), { ...generationArgs('video', args, 'mcp'), onEvent: mcpGenerationEvents() })));
|
|
102
|
+
tool('lumenkey_generate_image', ui('Quote a LumenKey image. Omitted scalar catalog defaults are explicitly sent in input. Nothing is submitted until confirm is true. Conflicting parameter aliases are rejected locally.', zhText("m424")), zodShape(imageParams), async (args) => toolResult(await runGeneration(api(), { ...generationArgs('image', args, 'mcp'), onEvent: mcpGenerationEvents() })));
|
|
103
|
+
tool('lumenkey_query_job', ui('Query the status of an asynchronous generation task by its jobId', zhText("m423")), { jobId: z.string().describe(ui('The task ID to query', zhText("m417"))) }, async ({ jobId }) => toolResult(await readJob(api(), jobId)));
|
|
104
|
+
tool('lumenkey_check_balance', ui('Query current LumenKey account credits balance (available credits, reserved credits, and updatedAt timestamp)', zhText("m422")), {}, async () => dataResult(await api().getBalance()));
|
|
105
|
+
tool('lumenkey_chat', ui('Run billable LLM inference. Every successful call may incur credits; reuse idempotencyKey only for an identical request.', zhText("m421")), zodShape(chatParams), async (args) => {
|
|
105
106
|
const outcome = await runChat(api(), chatArgs(args, 'mcp'));
|
|
106
107
|
return toolResult(outcome, outcome.ok ? outcome.text : undefined);
|
|
107
108
|
});
|
|
108
|
-
tool('lumenkey_explain_model', ui('Explain one model: its parameters, which are required, and which values are allowed.',
|
|
109
|
-
modelId: z.string().describe(ui('The model ID to explain',
|
|
109
|
+
tool('lumenkey_explain_model', ui('Explain one model: its parameters, which are required, and which values are allowed.', zhText("m420")), {
|
|
110
|
+
modelId: z.string().describe(ui('The model ID to explain', zhText("m419"))),
|
|
110
111
|
}, async ({ modelId }) => {
|
|
111
112
|
const model = (await api().listModels()).find((item) => item.id === modelId);
|
|
112
113
|
if (!model)
|
|
113
114
|
throw new LumenKeyError('MODEL_NOT_IN_CATALOG', undefined, undefined, missingModelSentence(modelId));
|
|
114
115
|
return dataResult(explainModel(model));
|
|
115
116
|
});
|
|
116
|
-
tool('lumenkey_tasks_progress', ui('Read one existing generation job',
|
|
117
|
-
jobId: z.string().describe(ui('The task ID to query',
|
|
117
|
+
tool('lumenkey_tasks_progress', ui('Read one existing generation job', zhText("m418")), {
|
|
118
|
+
jobId: z.string().describe(ui('The task ID to query', zhText("m417"))),
|
|
118
119
|
}, async ({ jobId }) => toolResult(await readJob(api(), jobId)));
|
|
119
|
-
tool('lumenkey_tasks_wait', ui('Wait for an existing job without creating one',
|
|
120
|
-
jobId: z.string().describe(ui('The existing task ID to wait for',
|
|
120
|
+
tool('lumenkey_tasks_wait', ui('Wait for an existing job without creating one', zhText("m416")), {
|
|
121
|
+
jobId: z.string().describe(ui('The existing task ID to wait for', zhText("m415"))),
|
|
121
122
|
...zodShape(waitParams),
|
|
122
123
|
}, async ({ jobId, ...rest }) => toolResult(await waitJob(api(), jobId, waitArgs(rest, 'mcp'))));
|
|
123
|
-
tool('lumenkey_resume_submission', ui('Re-send a recorded submission verbatim by its idempotency key (recovery after an unknown outcome). The gateway replays the earlier job or creates it once; nothing is quoted anew and no generation argument is accepted.',
|
|
124
|
-
tool('lumenkey_tasks_cancel', ui('Request cancellation with yes: true. Cancellation is not a refund; held credits are released or charged only when gateway settlement completes.',
|
|
125
|
-
jobId: z.string().describe(ui('The existing task ID to cancel',
|
|
126
|
-
yes: z.boolean({ invalid_type_error: ui('Cancellation requires yes: true.',
|
|
124
|
+
tool('lumenkey_resume_submission', ui('Re-send a recorded submission verbatim by its idempotency key (recovery after an unknown outcome). The gateway replays the earlier job or creates it once; nothing is quoted anew and no generation argument is accepted.', zhText("m414")), zodShape(resumeParams), async (args) => toolResult(await resumeSubmission(api(), { ...resumeArgs(args, 'mcp'), onEvent: mcpGenerationEvents() })));
|
|
125
|
+
tool('lumenkey_tasks_cancel', ui('Request cancellation with yes: true. Cancellation is not a refund; held credits are released or charged only when gateway settlement completes.', zhText("m413")), {
|
|
126
|
+
jobId: z.string().describe(ui('The existing task ID to cancel', zhText("m412"))),
|
|
127
|
+
yes: z.boolean({ invalid_type_error: ui('Cancellation requires yes: true.', zhText("m410")) }).optional().describe(ui('Must be true to authorize cancellation; cancellation is not a refund', zhText("m411"))),
|
|
127
128
|
}, async ({ jobId, yes }) => {
|
|
128
129
|
if (yes !== true)
|
|
129
|
-
throw new LumenKeyError('CANCEL_CONFIRMATION_REQUIRED', undefined, undefined, ui('Cancellation requires yes: true.',
|
|
130
|
+
throw new LumenKeyError('CANCEL_CONFIRMATION_REQUIRED', undefined, undefined, ui('Cancellation requires yes: true.', zhText("m410")));
|
|
130
131
|
return dataResult(await api().cancelJob(jobId));
|
|
131
132
|
});
|
|
132
133
|
return server;
|
|
@@ -135,6 +136,6 @@ export async function runMcpStdio() {
|
|
|
135
136
|
const server = createMcpServer();
|
|
136
137
|
const transport = new StdioServerTransport();
|
|
137
138
|
await server.connect(transport);
|
|
138
|
-
log(ui('Server running on stdio transport',
|
|
139
|
+
log(ui('Server running on stdio transport', zhText("m409")));
|
|
139
140
|
}
|
|
140
141
|
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { StudioRoot, StudioAsset } from './types.js';
|
|
2
|
+
export type IntegrityFileFact = 'NOT_CHECKED' | 'ABSENT' | 'PARTIAL' | 'VERIFIED' | 'MISSING' | 'MODIFIED' | 'UNSAFE' | 'UNKNOWN';
|
|
3
|
+
export interface AssetIntegrity {
|
|
4
|
+
file: IntegrityFileFact;
|
|
5
|
+
checkedAt: string | null;
|
|
6
|
+
integrityCheck: 'IDLE' | 'PENDING';
|
|
7
|
+
}
|
|
8
|
+
export interface IntegrityResult {
|
|
9
|
+
file: 'VERIFIED' | 'MISSING' | 'MODIFIED' | 'UNSAFE' | 'UNKNOWN';
|
|
10
|
+
checkedAt: string | null;
|
|
11
|
+
signature?: string;
|
|
12
|
+
errorCode?: 'SAVE_FILE_MISSING' | 'SAVE_FILE_MODIFIED' | 'SAVE_FILE_UNSAFE' | 'SAVE_FILE_READ_FAILED';
|
|
13
|
+
}
|
|
14
|
+
/** Not a serialized flag: only a completed real hash can mint this process-local proof. */
|
|
15
|
+
export declare function isVerifiedHashResult(assetId: string, value: unknown): value is IntegrityResult;
|
|
16
|
+
/** Bounded stat path: no hash, no user file mutation. The identity comes from an opened handle. */
|
|
17
|
+
export declare function inspectStudioAsset(root: StudioRoot, asset: StudioAsset): Promise<IntegrityResult>;
|
|
18
|
+
/** The only producer of VERIFIED. Full chunked bytes and identity are checked on
|
|
19
|
+
* the SAME open regular-file handle before and after reading. No network/repair. */
|
|
20
|
+
export declare function hashStudioAsset(root: StudioRoot, asset: StudioAsset, options: {
|
|
21
|
+
maxBytes: number;
|
|
22
|
+
expectedSignature?: string;
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
now?: () => number;
|
|
25
|
+
beforeRead?(): void | Promise<void>;
|
|
26
|
+
afterChunk?(): void | Promise<void>;
|
|
27
|
+
}): Promise<IntegrityResult>;
|
|
28
|
+
//# sourceMappingURL=asset-integrity-hash.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, open } from 'node:fs/promises';
|
|
3
|
+
import { assertStudioRoot, resolveStudioAssetPath } from './paths.js';
|
|
4
|
+
import { hasFsCode, StudioError } from './errors.js';
|
|
5
|
+
const proofs = new WeakMap();
|
|
6
|
+
/** Not a serialized flag: only a completed real hash can mint this process-local proof. */
|
|
7
|
+
export function isVerifiedHashResult(assetId, value) {
|
|
8
|
+
return typeof value === 'object' && value !== null && proofs.get(value) === `${assetId}:${JSON.stringify(value)}`;
|
|
9
|
+
}
|
|
10
|
+
function signature(s) {
|
|
11
|
+
return JSON.stringify([String(s.dev), String(s.ino), s.size, s.mtimeMs, s.ctimeMs, s.birthtimeMs, s.nlink]);
|
|
12
|
+
}
|
|
13
|
+
function regular(s) { return s.isFile() && !s.isSymbolicLink() && s.nlink === 1 && s.ino !== 0; }
|
|
14
|
+
function failed(file) {
|
|
15
|
+
return { file, checkedAt: null, errorCode: { MISSING: 'SAVE_FILE_MISSING', MODIFIED: 'SAVE_FILE_MODIFIED', UNSAFE: 'SAVE_FILE_UNSAFE', UNKNOWN: 'SAVE_FILE_READ_FAILED' }[file] };
|
|
16
|
+
}
|
|
17
|
+
function failure(error) {
|
|
18
|
+
return failed(hasFsCode(error, 'ENOENT') ? 'MISSING' : error instanceof StudioError && error.code === 'STUDIO_PATH_UNSAFE' ? 'UNSAFE' : 'UNKNOWN');
|
|
19
|
+
}
|
|
20
|
+
/** Bounded stat path: no hash, no user file mutation. The identity comes from an opened handle. */
|
|
21
|
+
export async function inspectStudioAsset(root, asset) {
|
|
22
|
+
try {
|
|
23
|
+
const file = await resolveStudioAssetPath(root, asset);
|
|
24
|
+
const handle = await open(file, 'r');
|
|
25
|
+
try {
|
|
26
|
+
const current = await handle.stat();
|
|
27
|
+
const named = await lstat(file);
|
|
28
|
+
await assertStudioRoot(root);
|
|
29
|
+
if (!regular(current) || !regular(named) || signature(current) !== signature(named))
|
|
30
|
+
return failed('UNSAFE');
|
|
31
|
+
if (current.size !== asset.byteLength)
|
|
32
|
+
return failed('MODIFIED');
|
|
33
|
+
return { file: 'UNKNOWN', checkedAt: null, signature: signature(current) };
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await handle.close();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return failure(error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** The only producer of VERIFIED. Full chunked bytes and identity are checked on
|
|
44
|
+
* the SAME open regular-file handle before and after reading. No network/repair. */
|
|
45
|
+
export async function hashStudioAsset(root, asset, options) {
|
|
46
|
+
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 1)
|
|
47
|
+
throw new StudioError('STUDIO_INPUT_INVALID');
|
|
48
|
+
try {
|
|
49
|
+
const file = await resolveStudioAssetPath(root, asset);
|
|
50
|
+
const handle = await open(file, 'r');
|
|
51
|
+
try {
|
|
52
|
+
const before = await handle.stat();
|
|
53
|
+
const named = await lstat(file);
|
|
54
|
+
if (!regular(before) || !regular(named) || signature(before) !== signature(named))
|
|
55
|
+
return failed('UNSAFE');
|
|
56
|
+
const beforeSignature = signature(before);
|
|
57
|
+
if (before.size !== asset.byteLength || before.size > options.maxBytes || (options.expectedSignature !== undefined && options.expectedSignature !== beforeSignature))
|
|
58
|
+
return failed('MODIFIED');
|
|
59
|
+
const hash = createHash('sha256');
|
|
60
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
61
|
+
let position = 0;
|
|
62
|
+
for (;;) {
|
|
63
|
+
options.signal?.throwIfAborted();
|
|
64
|
+
await options.beforeRead?.();
|
|
65
|
+
const { bytesRead } = await handle.read(buffer, 0, Math.min(buffer.length, asset.byteLength + 1 - position), position);
|
|
66
|
+
if (!bytesRead)
|
|
67
|
+
break;
|
|
68
|
+
position += bytesRead;
|
|
69
|
+
if (position > options.maxBytes || position > asset.byteLength)
|
|
70
|
+
return failed('MODIFIED');
|
|
71
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
72
|
+
await options.afterChunk?.();
|
|
73
|
+
}
|
|
74
|
+
const after = await handle.stat();
|
|
75
|
+
const afterNamed = await lstat(file);
|
|
76
|
+
await assertStudioRoot(root);
|
|
77
|
+
if (!regular(after) || !regular(afterNamed))
|
|
78
|
+
return failed('UNSAFE');
|
|
79
|
+
if (signature(after) !== beforeSignature || signature(afterNamed) !== beforeSignature || position !== asset.byteLength || hash.digest('hex') !== asset.sha256)
|
|
80
|
+
return failed('MODIFIED');
|
|
81
|
+
const result = Object.freeze({ file: 'VERIFIED', checkedAt: new Date((options.now ?? Date.now)()).toISOString(), signature: beforeSignature });
|
|
82
|
+
proofs.set(result, `${asset.assetId}:${JSON.stringify(result)}`);
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await handle.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return failure(error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=asset-integrity-hash.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type AssetIntegrity } from './asset-integrity-hash.js';
|
|
2
|
+
import type { StudioSaveAuthority } from './output-save-journal.js';
|
|
3
|
+
import type { StudioAsset, StudioRoot } from './types.js';
|
|
4
|
+
export interface AssetIntegrityQueue {
|
|
5
|
+
cached(assetIds: readonly string[]): Promise<Map<string, AssetIntegrity>>;
|
|
6
|
+
observe(asset: StudioAsset): Promise<AssetIntegrity>;
|
|
7
|
+
idle(): Promise<void>;
|
|
8
|
+
close(): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/** Root-owned local observer. No network, repair, HTTP hooks or file mutation.
|
|
11
|
+
* Hashing runs outside the authority's short commit queue. A failed completion
|
|
12
|
+
* publication makes later reads fail closed, rather than advertising stale success. */
|
|
13
|
+
export declare function createAssetIntegrityQueue(root: StudioRoot, authority: StudioSaveAuthority, options: {
|
|
14
|
+
maxBytes: number;
|
|
15
|
+
now?: () => number;
|
|
16
|
+
beforeHash?(asset: StudioAsset): void | Promise<void>;
|
|
17
|
+
afterHash?(asset: StudioAsset): void | Promise<void>;
|
|
18
|
+
onInspect?(): void;
|
|
19
|
+
hashOptions?: {
|
|
20
|
+
beforeRead?(): void | Promise<void>;
|
|
21
|
+
afterChunk?(): void | Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
}): AssetIntegrityQueue;
|
|
24
|
+
//# sourceMappingURL=asset-integrity-queue.d.ts.map
|