@stage5/lumine 0.2.69 → 0.2.70
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 +23 -6
- package/lib/admin-featured.js +33 -0
- package/lib/admin-runtime-logs.js +22 -3
- package/lib/admin.js +22 -0
- package/lib/assets.js +11 -8
- package/lib/build-review.js +609 -8
- package/lib/commands.js +12 -6
- package/lib/constants.js +5 -1
- package/lib/sdk.js +108 -4
- package/lib/thumbnail.js +2 -2
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +7 -3
- package/sdk/LUMINE_ADMIN.md +129 -9
package/README.md
CHANGED
|
@@ -158,6 +158,20 @@ Replay listing and status are available without exposing private playback
|
|
|
158
158
|
grants. A creator or app owner can remove one with
|
|
159
159
|
`lumine sdk call live.deleteReplay '{"replayId":"..."}' --allow-write`.
|
|
160
160
|
|
|
161
|
+
`Twinkle.rewards` is callable too, on the same server-verified endpoints the
|
|
162
|
+
published app uses — the CLI holds no award logic. `lumine sdk call
|
|
163
|
+
rewards.getStatus '{}' --build <id>` is read-only and works for the build owner
|
|
164
|
+
without `--allow-write` (the endpoint accepts only the `rewards:claim` scope,
|
|
165
|
+
which is minted for it, but only the status operation is sent).
|
|
166
|
+
`rewards.start '{"ruleId":"..."}'` and
|
|
167
|
+
`rewards.claim '{"challengeId":"...","answers":[1,2]}'` mutate real XP/Coins
|
|
168
|
+
state and require `--allow-write`. Every rewards call first reads the
|
|
169
|
+
published-runtime reward grant from the canonical `GET /build/:id/runtime`
|
|
170
|
+
payload for the signed-in account; when the server issues none (draft,
|
|
171
|
+
private, unapproved or superseded release — the cases where the app itself
|
|
172
|
+
sees preview mode) the call stops with an explanatory error and nothing is
|
|
173
|
+
sent. `--path api/rewards/...` is refused like every other curated endpoint.
|
|
174
|
+
|
|
161
175
|
## Assets and AI image generation
|
|
162
176
|
|
|
163
177
|
Binary media never lives in the workspace — assets are uploaded to Twinkle and
|
|
@@ -166,19 +180,22 @@ audio, and MIDI data (`.mid`/`.midi`; playback still needs an app-side parser
|
|
|
166
180
|
or synth); `lumine assets list` prints your uploads and refreshes
|
|
167
181
|
`.twinkle/assets.json`.
|
|
168
182
|
|
|
169
|
-
`lumine assets generate "<prompt>" --model <gpt-image-2|nano-banana>` creates
|
|
183
|
+
`lumine assets generate "<prompt>" --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana>` creates
|
|
170
184
|
an AI-generated image asset instead of uploading one. `--model` is required
|
|
171
|
-
(
|
|
172
|
-
|
|
173
|
-
estimated cost and asks for confirmation first — non-interactive
|
|
174
|
-
|
|
185
|
+
(Flare = fast generation; Sunburst = precise editing; gpt-image-2 = original
|
|
186
|
+
model; nano-banana = Gemini). Generation spends your Twinkle AI Battery, so the
|
|
187
|
+
CLI shows the estimated cost and asks for confirmation first — non-interactive
|
|
188
|
+
runs must pass
|
|
189
|
+
`--yes` to consent. `--quality low|medium|high|xhigh|max` applies to GPT Image
|
|
190
|
+
models; `xhigh` and `max` require a 2.5 model. Estimates describe image output;
|
|
191
|
+
actual battery usage also includes prompt and reference input.
|
|
175
192
|
|
|
176
193
|
## Thumbnails
|
|
177
194
|
|
|
178
195
|
`lumine thumbnail set <file>` uploads a jpg/png/webp (max 8MB) as the build's
|
|
179
196
|
thumbnail. `lumine thumbnail capture` screenshots the running app server-side
|
|
180
197
|
and sets the result (add `--out <file>` to keep a local copy).
|
|
181
|
-
`lumine thumbnail generate ["<prompt>"] --model <gpt-image-2|nano-banana>`
|
|
198
|
+
`lumine thumbnail generate ["<prompt>"] --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana>`
|
|
182
199
|
generates an AI image and sets it as the thumbnail (the image is also kept as a
|
|
183
200
|
normal reusable asset); without a prompt the server composes one from the build
|
|
184
201
|
title and description. Replacing an existing thumbnail asks for confirmation;
|
package/lib/admin-featured.js
CHANGED
|
@@ -88,6 +88,14 @@ export function readFeaturedSelections(file) {
|
|
|
88
88
|
});
|
|
89
89
|
positive(raw?.reviewId, "Review ID");
|
|
90
90
|
positive(raw?.coverageId, "Reviewed coverage ID");
|
|
91
|
+
// Review and coverage receipts are distinct audit rows, so equal IDs can
|
|
92
|
+
// only mean the review ID was copied into coverageId. Catch that locally
|
|
93
|
+
// instead of letting the server reject an unrelated-looking receipt.
|
|
94
|
+
if (raw.coverageId === raw.reviewId) {
|
|
95
|
+
throw invalid(
|
|
96
|
+
`coverageId ${raw.coverageId} is the review ID, not the acknowledged coverage receipt. Use data.reviewedCoverage.id from \`featured comments acknowledge --reviewed\` (or its data.decisionsTemplate / --decisions-template file).`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
91
99
|
if (!Array.isArray(raw.selections) || raw.selections.length > 20_000) {
|
|
92
100
|
throw invalid(
|
|
93
101
|
"selections must be an array of at most 20000 agent-selected comments.",
|
|
@@ -530,6 +538,29 @@ export async function runFeaturedWorkflow({
|
|
|
530
538
|
return call(`/reviews/${state.review.id}/report`);
|
|
531
539
|
}
|
|
532
540
|
const subjects = Object.values(state.subjects);
|
|
541
|
+
// The recommend step needs the review ID plus the acknowledged coverage
|
|
542
|
+
// receipt ID (never the review ID); hand back a ready-to-fill file so the
|
|
543
|
+
// agent does not assemble those identifiers by hand.
|
|
544
|
+
const decisionsTemplate = state.coverage
|
|
545
|
+
? {
|
|
546
|
+
reviewId: state.review.id,
|
|
547
|
+
coverageId: state.coverage.id,
|
|
548
|
+
selections: [],
|
|
549
|
+
}
|
|
550
|
+
: null;
|
|
551
|
+
let decisionsTemplatePath = null;
|
|
552
|
+
if (options.adminDecisionsTemplate) {
|
|
553
|
+
if (workflow !== "acknowledge")
|
|
554
|
+
throw invalid(
|
|
555
|
+
"--decisions-template is written by `featured comments acknowledge --reviewed`.",
|
|
556
|
+
);
|
|
557
|
+
positive(decisionsTemplate?.coverageId, "Acknowledged coverage ID");
|
|
558
|
+
decisionsTemplatePath = path.resolve(options.adminDecisionsTemplate);
|
|
559
|
+
writeAdminJsonFile(decisionsTemplatePath, decisionsTemplate, {
|
|
560
|
+
privateFile: true,
|
|
561
|
+
maxBytes: MAX_BYTES,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
533
564
|
return {
|
|
534
565
|
ok: true,
|
|
535
566
|
status: "success",
|
|
@@ -537,6 +568,8 @@ export async function runFeaturedWorkflow({
|
|
|
537
568
|
data: {
|
|
538
569
|
review: state.review,
|
|
539
570
|
checkpointPath: checkpoint,
|
|
571
|
+
decisionsTemplate,
|
|
572
|
+
decisionsTemplatePath,
|
|
540
573
|
fetched: {
|
|
541
574
|
complete: subjects.every((item) => item.complete),
|
|
542
575
|
subjectsCompleted: subjects.filter((item) => item.complete).length,
|
|
@@ -562,6 +562,21 @@ async function recoverPendingSnapshot({ options, authToken, session }) {
|
|
|
562
562
|
return { status, recoveredArtifact };
|
|
563
563
|
}
|
|
564
564
|
|
|
565
|
+
// Only `completed` closes a review. `needs_review` and
|
|
566
|
+
// `post_clear_review_required` both hand back another snapshot that must be
|
|
567
|
+
// read before rerunning finish, so the top-level status says so instead of
|
|
568
|
+
// reporting `success` for an unfinished review.
|
|
569
|
+
export function completionStatusEnvelope(result) {
|
|
570
|
+
const completionStatus = String(result?.data?.completionStatus || "");
|
|
571
|
+
if (
|
|
572
|
+
completionStatus === "needs_review" ||
|
|
573
|
+
completionStatus === "post_clear_review_required"
|
|
574
|
+
) {
|
|
575
|
+
return { ...result, status: "needs_review" };
|
|
576
|
+
}
|
|
577
|
+
return result;
|
|
578
|
+
}
|
|
579
|
+
|
|
565
580
|
function scrubLeaseToken(result, session, artifact) {
|
|
566
581
|
const review = result?.data?.review || {};
|
|
567
582
|
const { leaseToken: _leaseToken, ...safeReview } = review;
|
|
@@ -742,13 +757,13 @@ export async function runAdminRuntimeLogWorkflow({
|
|
|
742
757
|
});
|
|
743
758
|
if (recovered.recoveredArtifact) {
|
|
744
759
|
return scrubLeaseToken(
|
|
745
|
-
{
|
|
760
|
+
completionStatusEnvelope({
|
|
746
761
|
...recovered.status,
|
|
747
762
|
data: {
|
|
748
763
|
...(recovered.status.data || {}),
|
|
749
764
|
completionStatus: "needs_review",
|
|
750
765
|
},
|
|
751
|
-
},
|
|
766
|
+
}),
|
|
752
767
|
session,
|
|
753
768
|
recovered.recoveredArtifact,
|
|
754
769
|
);
|
|
@@ -802,5 +817,9 @@ export async function runAdminRuntimeLogWorkflow({
|
|
|
802
817
|
}
|
|
803
818
|
session.status = String(result?.data?.review?.status || session.status);
|
|
804
819
|
writeSession(session);
|
|
805
|
-
return scrubLeaseToken(
|
|
820
|
+
return scrubLeaseToken(
|
|
821
|
+
action === "complete" ? completionStatusEnvelope(result) : result,
|
|
822
|
+
session,
|
|
823
|
+
artifact,
|
|
824
|
+
);
|
|
806
825
|
}
|
package/lib/admin.js
CHANGED
|
@@ -3520,6 +3520,14 @@ function printAdminResult({ operation, result }) {
|
|
|
3520
3520
|
`Confirmed Build #${data.review.buildId} runtime at published artifact #${data.review.publishedArtifactVersionId}.`,
|
|
3521
3521
|
);
|
|
3522
3522
|
console.log(`Screenshot: ${data.screenshotPath}`);
|
|
3523
|
+
if (data.review.interaction) {
|
|
3524
|
+
console.log(
|
|
3525
|
+
`Interaction script: ${data.review.interaction.stepsCompleted}/${data.review.interaction.stepsPlanned} step(s) ${data.review.interaction.status}.`,
|
|
3526
|
+
);
|
|
3527
|
+
}
|
|
3528
|
+
for (const shot of data.review.screenshots || []) {
|
|
3529
|
+
console.log(` Screenshot [${shot.label}]: ${shot.path}`);
|
|
3530
|
+
}
|
|
3523
3531
|
console.log(`Review receipt: ${data.receiptPath}`);
|
|
3524
3532
|
return;
|
|
3525
3533
|
}
|
|
@@ -3668,6 +3676,20 @@ function printAdminResult({ operation, result }) {
|
|
|
3668
3676
|
);
|
|
3669
3677
|
return;
|
|
3670
3678
|
}
|
|
3679
|
+
if (data.escalation?.summary) {
|
|
3680
|
+
const target =
|
|
3681
|
+
data.escalation.url ||
|
|
3682
|
+
`${data.escalation.targetType || "target"}:${data.escalation.targetId || "?"}`;
|
|
3683
|
+
console.log(
|
|
3684
|
+
`Escalation${data.escalation.auditId ? ` #${data.escalation.auditId}` : ""} recorded: ${String(data.escalation.severity || "attention").toUpperCase()} ${target} — ${data.escalation.summary}`,
|
|
3685
|
+
);
|
|
3686
|
+
if (data.escalation.auditId) {
|
|
3687
|
+
console.log(
|
|
3688
|
+
`Set its disposition later with: lumine admin escalation set ${data.escalation.auditId} --status <status> --note <decision>`,
|
|
3689
|
+
);
|
|
3690
|
+
}
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3671
3693
|
if (Array.isArray(data.todos)) {
|
|
3672
3694
|
printTodoItems(data.todos, "Private carry-over work");
|
|
3673
3695
|
if (data.truncated) {
|
package/lib/assets.js
CHANGED
|
@@ -56,7 +56,7 @@ export async function assetsCommand(options) {
|
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
58
|
throw new Error(
|
|
59
|
-
'Usage: lumine assets [list] | lumine assets upload <file...> | lumine assets generate "<prompt>" --model <gpt-image-2|nano-banana> | lumine assets delete <assetId> | lumine assets prune [--yes]',
|
|
59
|
+
'Usage: lumine assets [list] | lumine assets upload <file...> | lumine assets generate "<prompt>" --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana> | lumine assets delete <assetId> | lumine assets prune [--yes]',
|
|
60
60
|
);
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -65,18 +65,21 @@ export function resolveGenerateModel(options) {
|
|
|
65
65
|
const model = GENERATE_MODEL_ALIASES[rawModel];
|
|
66
66
|
if (!model) {
|
|
67
67
|
throw new Error(
|
|
68
|
-
"--model is required: gpt-image-2 (
|
|
68
|
+
"--model is required: gpt-image-2.5-flare (fast generation), gpt-image-2.5-sunburst (precise editing), gpt-image-2, or nano-banana. No default model is applied.",
|
|
69
69
|
);
|
|
70
70
|
}
|
|
71
71
|
const rawQuality = String(options.quality || "").trim();
|
|
72
72
|
if (rawQuality && !GENERATE_QUALITIES.has(rawQuality)) {
|
|
73
|
-
throw new Error("--quality must be low, medium, or
|
|
73
|
+
throw new Error("--quality must be low, medium, high, xhigh, or max.");
|
|
74
74
|
}
|
|
75
|
-
if (rawQuality && model
|
|
75
|
+
if (rawQuality && !model.startsWith("gpt-image-")) {
|
|
76
76
|
throw new Error(
|
|
77
|
-
"--quality only applies to
|
|
77
|
+
"--quality only applies to GPT Image models; nano-banana has a single quality tier.",
|
|
78
78
|
);
|
|
79
79
|
}
|
|
80
|
+
if (model === "gpt-image-2" && ["xhigh", "max"].includes(rawQuality)) {
|
|
81
|
+
throw new Error("xhigh and max require a GPT Image 2.5 model.");
|
|
82
|
+
}
|
|
80
83
|
return { model, quality: rawQuality || null };
|
|
81
84
|
}
|
|
82
85
|
|
|
@@ -118,7 +121,7 @@ export async function assetsGenerate(options) {
|
|
|
118
121
|
const prompt = String(options.positional[1] || "").trim();
|
|
119
122
|
if (!prompt) {
|
|
120
123
|
throw new Error(
|
|
121
|
-
'Usage: lumine assets generate "<prompt>" --model <gpt-image-2|nano-banana> [--quality low|medium|high] [--name <fileName>] [--yes]',
|
|
124
|
+
'Usage: lumine assets generate "<prompt>" --model <gpt-image-2.5-flare|gpt-image-2.5-sunburst|gpt-image-2|nano-banana> [--quality low|medium|high|xhigh|max] [--name <fileName>] [--yes]',
|
|
122
125
|
);
|
|
123
126
|
}
|
|
124
127
|
const { model, quality } = resolveGenerateModel(options);
|
|
@@ -161,8 +164,8 @@ export async function assetsGenerate(options) {
|
|
|
161
164
|
(option) => option.model === model,
|
|
162
165
|
);
|
|
163
166
|
const costLine = selectedOption
|
|
164
|
-
? ` Estimated
|
|
165
|
-
: " Estimated
|
|
167
|
+
? ` Estimated image output: ${formatBatteryPercent(selectedOption.energyUnits, estimate?.fullBatteryUnits)} of a full AI battery (~$${Number(selectedOption.estimatedUsd || 0).toFixed(2)}). Prompt input uses additional energy.`
|
|
168
|
+
: " Estimated image output: unavailable";
|
|
166
169
|
const remainingLine = estimate
|
|
167
170
|
? ` Battery remaining now: ${formatBatteryPercent(estimate.energyRemaining, estimate.fullBatteryUnits)}`
|
|
168
171
|
: "";
|