@stage5/lumine 0.2.52 → 0.2.53
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 +11 -0
- package/lib/admin.js +231 -7
- package/lib/commands.js +2 -0
- package/lib/sdk.js +39 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +122 -7
- package/sdk/LUMINE_ADMIN.md +230 -6
package/README.md
CHANGED
|
@@ -139,6 +139,10 @@ methods. The JSON args are sent as the request body (shapes follow
|
|
|
139
139
|
```bash
|
|
140
140
|
lumine sdk call aiStories.chapters '{"limit": 5}'
|
|
141
141
|
lumine sdk call aiStories.list '{"difficulty": 1}' --repeat 5 --build 1374
|
|
142
|
+
lumine sdk call live.list '{}'
|
|
143
|
+
lumine sdk call live.get '{"sessionId": "..."}'
|
|
144
|
+
lumine sdk call live.listReplays '{"limit": 20}'
|
|
145
|
+
lumine sdk call live.getReplay '{"replayId": "..."}'
|
|
142
146
|
```
|
|
143
147
|
|
|
144
148
|
It targets the build in the current workspace, or pass `--build <id>`. Add
|
|
@@ -146,6 +150,13 @@ It targets the build in the current workspace, or pass `--build <id>`. Add
|
|
|
146
150
|
which can differ from a method's `Twinkle.*` SDK return shape — check
|
|
147
151
|
`TWINKLE_BUILD_SDK.md` for SDK return shapes. Methods that change data require
|
|
148
152
|
`--allow-write`.
|
|
153
|
+
Stopping a hosted stream is available as
|
|
154
|
+
`lumine sdk call live.stop '{"sessionId":"..."}' --allow-write`; the command
|
|
155
|
+
prints canonical server state and refuses to mint `live:write` without the
|
|
156
|
+
explicit write flag.
|
|
157
|
+
Replay listing and status are available without exposing private playback
|
|
158
|
+
grants. A creator or app owner can remove one with
|
|
159
|
+
`lumine sdk call live.deleteReplay '{"replayId":"..."}' --allow-write`.
|
|
149
160
|
|
|
150
161
|
## Assets and AI image generation
|
|
151
162
|
|
package/lib/admin.js
CHANGED
|
@@ -96,9 +96,7 @@ function readBuildReviewContextFile(filePath) {
|
|
|
96
96
|
);
|
|
97
97
|
}
|
|
98
98
|
const understanding =
|
|
99
|
-
typeof parsed.understanding === "string"
|
|
100
|
-
? parsed.understanding.trim()
|
|
101
|
-
: "";
|
|
99
|
+
typeof parsed.understanding === "string" ? parsed.understanding.trim() : "";
|
|
102
100
|
if (!understanding) {
|
|
103
101
|
throw cliValidationError(
|
|
104
102
|
"The Build review context understanding must be a non-empty string.",
|
|
@@ -1235,6 +1233,35 @@ export function parseAdminOperation(options) {
|
|
|
1235
1233
|
);
|
|
1236
1234
|
}
|
|
1237
1235
|
|
|
1236
|
+
if (namespace === "ai-costs") {
|
|
1237
|
+
if (action === "monthly" && !target && !extra) {
|
|
1238
|
+
if (options.adminDays) {
|
|
1239
|
+
throw cliValidationError(
|
|
1240
|
+
"ai-costs monthly uses UTC calendar months and does not accept --days.",
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
return readOperation("ai-costs.monthly", "/cli/admin/ai-costs/monthly");
|
|
1244
|
+
}
|
|
1245
|
+
throw cliValidationError("Usage: lumine admin ai-costs monthly [--json].");
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
if (namespace === "media-costs") {
|
|
1249
|
+
if (action === "monthly" && !target && !extra) {
|
|
1250
|
+
if (options.adminDays) {
|
|
1251
|
+
throw cliValidationError(
|
|
1252
|
+
"media-costs monthly uses the canonical UTC ledger and does not accept --days.",
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
return readOperation(
|
|
1256
|
+
"media-costs.monthly",
|
|
1257
|
+
"/cli/admin/media-costs/monthly",
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
throw cliValidationError(
|
|
1261
|
+
"Usage: lumine admin media-costs monthly [--json].",
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1238
1265
|
if (namespace === "brief" && !action) {
|
|
1239
1266
|
if (options.adminDays) {
|
|
1240
1267
|
const days = Number(options.adminDays);
|
|
@@ -1419,9 +1446,7 @@ export function parseAdminOperation(options) {
|
|
|
1419
1446
|
...(confirmedBuildReviewMethod
|
|
1420
1447
|
? { buildReviewMethod: confirmedBuildReviewMethod }
|
|
1421
1448
|
: {}),
|
|
1422
|
-
...(buildReviewUnderstanding
|
|
1423
|
-
? { buildReviewUnderstanding }
|
|
1424
|
-
: {}),
|
|
1449
|
+
...(buildReviewUnderstanding ? { buildReviewUnderstanding } : {}),
|
|
1425
1450
|
},
|
|
1426
1451
|
);
|
|
1427
1452
|
}
|
|
@@ -1465,7 +1490,7 @@ export function parseAdminOperation(options) {
|
|
|
1465
1490
|
}
|
|
1466
1491
|
|
|
1467
1492
|
throw cliValidationError(
|
|
1468
|
-
"Usage: lumine admin identity|economy|rescue|daily-run|escalation|todo|recommendations|builds|post|subjects|subject|featured|comment|announcement|chat|news|audit|brief|bot-output|notable ...",
|
|
1493
|
+
"Usage: lumine admin identity|economy|rescue|daily-run|escalation|todo|recommendations|builds|post|subjects|subject|featured|comment|announcement|chat|news|audit|brief|ai-costs|media-costs|bot-output|notable ...",
|
|
1469
1494
|
);
|
|
1470
1495
|
}
|
|
1471
1496
|
|
|
@@ -2047,6 +2072,197 @@ function adminValueFingerprint(value) {
|
|
|
2047
2072
|
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
2048
2073
|
}
|
|
2049
2074
|
|
|
2075
|
+
function formatAdminUsd(value) {
|
|
2076
|
+
const amount = Number(value);
|
|
2077
|
+
if (!Number.isFinite(amount)) return "unavailable";
|
|
2078
|
+
return `$${amount.toLocaleString("en-US", {
|
|
2079
|
+
minimumFractionDigits: 2,
|
|
2080
|
+
maximumFractionDigits: 2,
|
|
2081
|
+
})}`;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
function formatAdminMonthlyCostComparison(projection, previousMonthKey) {
|
|
2085
|
+
const rawPercent = projection?.comparisonToPreviousMonth?.percentChange;
|
|
2086
|
+
const percent = rawPercent === null ? NaN : Number(rawPercent);
|
|
2087
|
+
if (!Number.isFinite(percent)) {
|
|
2088
|
+
return `comparison with ${previousMonthKey} unavailable`;
|
|
2089
|
+
}
|
|
2090
|
+
if (percent === 0) return `even with ${previousMonthKey}`;
|
|
2091
|
+
return `${Math.abs(percent).toFixed(2)}% ${percent < 0 ? "below" : "above"} ${previousMonthKey}`;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
function printAdminMonthlyAiCosts(monthlyAiCosts) {
|
|
2095
|
+
const previous = monthlyAiCosts.previousMonth;
|
|
2096
|
+
const current = monthlyAiCosts.currentMonth;
|
|
2097
|
+
const generatedAt = new Date(
|
|
2098
|
+
Number(monthlyAiCosts.generatedAt) * 1000,
|
|
2099
|
+
).toISOString();
|
|
2100
|
+
console.log(`Application AI-cost ledger (UTC; generated ${generatedAt}).`);
|
|
2101
|
+
console.log(
|
|
2102
|
+
`${previous.monthKey} closed month: ${formatAdminUsd(previous.estimatedCostUsd)} estimated cost.`,
|
|
2103
|
+
);
|
|
2104
|
+
if (current.completed.dayCount > 0) {
|
|
2105
|
+
console.log(
|
|
2106
|
+
`${current.monthKey} completed-day MTD through ${current.completed.throughDayKey}: ${formatAdminUsd(current.completed.estimatedCostUsd)} across ${current.completed.dayCount} completed UTC day(s).`,
|
|
2107
|
+
);
|
|
2108
|
+
} else {
|
|
2109
|
+
console.log(
|
|
2110
|
+
`${current.monthKey} completed-day MTD: ${formatAdminUsd(current.completed.estimatedCostUsd)}; no UTC day has completed yet.`,
|
|
2111
|
+
);
|
|
2112
|
+
}
|
|
2113
|
+
console.log(
|
|
2114
|
+
`${current.inProgressDay.dayKey} in progress: ${formatAdminUsd(current.inProgressDay.estimatedCostUsd)} so far (excluded from completed-day MTD and both projections).`,
|
|
2115
|
+
);
|
|
2116
|
+
|
|
2117
|
+
const allPace = current.projections.allCompletedDaysPace;
|
|
2118
|
+
if (allPace) {
|
|
2119
|
+
console.log(
|
|
2120
|
+
`All-completed-days pace full-month projection: ${formatAdminUsd(allPace.estimatedMonthTotalUsd)} (${formatAdminUsd(allPace.dailyAverageUsd)}/day across ${allPace.basisDayCount} completed day(s); ${formatAdminMonthlyCostComparison(allPace, previous.monthKey)}).`,
|
|
2121
|
+
);
|
|
2122
|
+
} else {
|
|
2123
|
+
console.log(
|
|
2124
|
+
"All-completed-days pace full-month projection: unavailable until one UTC day has completed.",
|
|
2125
|
+
);
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
const recentPace = current.projections.recentSevenCompletedDaysPace;
|
|
2129
|
+
if (recentPace) {
|
|
2130
|
+
console.log(
|
|
2131
|
+
`Recent-seven-completed-day pace full-month projection: ${formatAdminUsd(recentPace.estimatedMonthTotalUsd)} (${formatAdminUsd(recentPace.dailyAverageUsd)}/day from ${recentPace.basisStartDayKey} through ${recentPace.basisEndDayKey}; ${formatAdminMonthlyCostComparison(recentPace, previous.monthKey)}).`,
|
|
2132
|
+
);
|
|
2133
|
+
} else {
|
|
2134
|
+
console.log(
|
|
2135
|
+
"Recent-seven-completed-day pace full-month projection: unavailable until seven UTC days have completed.",
|
|
2136
|
+
);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
function formatAdminMediaUsd(value) {
|
|
2141
|
+
const amount = Number(value);
|
|
2142
|
+
if (!Number.isFinite(amount)) return "unavailable";
|
|
2143
|
+
return `$${amount.toLocaleString("en-US", {
|
|
2144
|
+
minimumFractionDigits: 2,
|
|
2145
|
+
maximumFractionDigits: 6,
|
|
2146
|
+
})}`;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
function formatAdminMediaBytes(value) {
|
|
2150
|
+
const bytes = Number(value);
|
|
2151
|
+
if (!Number.isFinite(bytes) || bytes < 0) return "unavailable";
|
|
2152
|
+
if (bytes < 1024) return `${Math.floor(bytes)} B`;
|
|
2153
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
2154
|
+
let amount = bytes / 1024;
|
|
2155
|
+
let unit = units[0];
|
|
2156
|
+
for (let index = 1; index < units.length && amount >= 1024; index += 1) {
|
|
2157
|
+
amount /= 1024;
|
|
2158
|
+
unit = units[index];
|
|
2159
|
+
}
|
|
2160
|
+
return `${amount.toFixed(amount >= 100 ? 0 : amount >= 10 ? 1 : 2)} ${unit}`;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
function printAdminMonthlyMediaCosts(monthlyMediaCosts) {
|
|
2164
|
+
const generatedAt = new Date(
|
|
2165
|
+
Number(monthlyMediaCosts.generatedAt) * 1000,
|
|
2166
|
+
).toISOString();
|
|
2167
|
+
const current = monthlyMediaCosts.currentMonth;
|
|
2168
|
+
const today = monthlyMediaCosts.currentUtcDay;
|
|
2169
|
+
const operations = monthlyMediaCosts.operations;
|
|
2170
|
+
const streamAttempts = today.streamAttempts || {
|
|
2171
|
+
attemptedCount: 0,
|
|
2172
|
+
reachedLiveCount: 0,
|
|
2173
|
+
endedCount: 0,
|
|
2174
|
+
failedCount: 0,
|
|
2175
|
+
cancelledCount: 0,
|
|
2176
|
+
inProgressCount: 0,
|
|
2177
|
+
failureCodeCounts: [],
|
|
2178
|
+
};
|
|
2179
|
+
console.log(
|
|
2180
|
+
`Lumine media-cost monitor (UTC; ${monthlyMediaCosts.status}; generated ${generatedAt}).`,
|
|
2181
|
+
);
|
|
2182
|
+
console.log(
|
|
2183
|
+
`${current.monthKey}: ${formatAdminMediaUsd(current.estimatedSpentUsd)} settled estimate + ${formatAdminMediaUsd(current.activeReservedUsd)} active reservations + ${formatAdminMediaUsd(current.carryoverUsd)} carryover = ${formatAdminMediaUsd(current.guardedTotalUsd)} guarded of ${formatAdminMediaUsd(current.limitUsd)} (${Number(current.percentUsed).toFixed(2)}% used; ${formatAdminMediaUsd(current.remainingUsd)} remaining).`,
|
|
2184
|
+
);
|
|
2185
|
+
console.log(
|
|
2186
|
+
`${today.dayKey} so far: ${today.reservationsCreated} action(s) reserved, ${today.commitmentsSettled} committed, ${today.cancellationsSettled} cancelled, ${formatAdminMediaUsd(today.estimatedCostSettledUsd)} settled estimate.`,
|
|
2187
|
+
);
|
|
2188
|
+
console.log(
|
|
2189
|
+
`Stream attempts created ${today.dayKey} UTC: ${streamAttempts.attemptedCount} attempted / ${streamAttempts.reachedLiveCount} reached live / ${streamAttempts.endedCount} ended after live / ${streamAttempts.failedCount} failed / ${streamAttempts.cancelledCount} cancelled before live / ${streamAttempts.inProgressCount} still in progress.`,
|
|
2190
|
+
);
|
|
2191
|
+
const streamFailureCodeCounts = Array.isArray(
|
|
2192
|
+
streamAttempts.failureCodeCounts,
|
|
2193
|
+
)
|
|
2194
|
+
? streamAttempts.failureCodeCounts
|
|
2195
|
+
: [];
|
|
2196
|
+
console.log(
|
|
2197
|
+
streamFailureCodeCounts.length > 0
|
|
2198
|
+
? `Stream failure codes: ${streamFailureCodeCounts
|
|
2199
|
+
.map((entry) => `${entry.code}=${entry.count}`)
|
|
2200
|
+
.join(", ")}.`
|
|
2201
|
+
: "Stream failure codes: none.",
|
|
2202
|
+
);
|
|
2203
|
+
const stillActiveOrCleanupPendingCount =
|
|
2204
|
+
operations.live.stillActiveOrCleanupPendingCount ??
|
|
2205
|
+
Number(operations.live.provisioningCount || 0) +
|
|
2206
|
+
Number(operations.live.readyCount || 0) +
|
|
2207
|
+
Number(operations.live.liveCount || 0) +
|
|
2208
|
+
Number(operations.live.endingCount || 0) +
|
|
2209
|
+
Number(operations.live.cleanupFailedCount || 0);
|
|
2210
|
+
const replayOperations = operations.replays || {
|
|
2211
|
+
pendingCount: 0,
|
|
2212
|
+
processingCount: 0,
|
|
2213
|
+
readyCount: 0,
|
|
2214
|
+
failedCount: 0,
|
|
2215
|
+
deletePendingCount: 0,
|
|
2216
|
+
deleteFailedCount: 0,
|
|
2217
|
+
expiredReadyCount: 0,
|
|
2218
|
+
finalizationOverdueCount: 0,
|
|
2219
|
+
deletionOverdueCount: 0,
|
|
2220
|
+
storedBytes: 0,
|
|
2221
|
+
storedObjectCount: 0,
|
|
2222
|
+
};
|
|
2223
|
+
const replayViewers = operations.replayViewers || {
|
|
2224
|
+
activeGrantCount: 0,
|
|
2225
|
+
expiredActiveGrantCount: 0,
|
|
2226
|
+
};
|
|
2227
|
+
const kindRows = [
|
|
2228
|
+
["Short clips", current.byKind.clip],
|
|
2229
|
+
["Live inputs", current.byKind.liveInput],
|
|
2230
|
+
["Live viewers", current.byKind.liveViewer],
|
|
2231
|
+
];
|
|
2232
|
+
if (current.byKind.replayViewer) {
|
|
2233
|
+
kindRows.push(["Replay viewers", current.byKind.replayViewer]);
|
|
2234
|
+
}
|
|
2235
|
+
for (const [label, cost] of kindRows) {
|
|
2236
|
+
console.log(
|
|
2237
|
+
`${label}: ${cost.actionCount} action(s), ${cost.committedCount} committed for ${formatAdminMediaUsd(cost.estimatedSpentUsd)}, ${cost.activeReservedCount} active reservation(s) holding ${formatAdminMediaUsd(cost.activeReservedUsd)}, ${cost.cancelledCount} cancelled.`,
|
|
2238
|
+
);
|
|
2239
|
+
}
|
|
2240
|
+
console.log(
|
|
2241
|
+
`Ledger reconciliation: ${current.reconciliation.consistent ? "consistent" : "MISMATCH"}; spent delta ${formatAdminMediaUsd(current.reconciliation.spentDeltaUsd)}, reserved delta ${formatAdminMediaUsd(current.reconciliation.reservedDeltaUsd)}.`,
|
|
2242
|
+
);
|
|
2243
|
+
console.log(
|
|
2244
|
+
`Operations: clips ${operations.clips.completingCount} completing / ${operations.clips.processingCount} processing / ${operations.clips.staleCount} stale; live ${operations.live.liveCount} broadcasting / ${stillActiveOrCleanupPendingCount} active-or-cleanup-pending / ${operations.live.costBearingChannelCount} cost-bearing channel(s) / ${operations.live.possibleOrphanedCount ?? operations.live.cleanupOverdueCount} possible orphan(s); viewers ${operations.viewers.activeGrantCount} active / ${operations.viewers.expiredActiveGrantCount} expired-active.`,
|
|
2245
|
+
);
|
|
2246
|
+
console.log(
|
|
2247
|
+
`Replays: ${replayOperations.pendingCount} pending / ${replayOperations.processingCount} processing / ${replayOperations.readyCount} ready / ${replayOperations.failedCount} failed / ${replayOperations.deletePendingCount} deleting / ${replayOperations.deleteFailedCount} delete-failed; ${replayOperations.finalizationOverdueCount} finalization-overdue / ${replayOperations.deletionOverdueCount} deletion-overdue / ${replayOperations.expiredReadyCount} expired-ready; ${formatAdminMediaBytes(replayOperations.storedBytes)} across ${replayOperations.storedObjectCount} canonical object(s); viewers ${replayViewers.activeGrantCount} active / ${replayViewers.expiredActiveGrantCount} expired-active.`,
|
|
2248
|
+
);
|
|
2249
|
+
console.log(
|
|
2250
|
+
`Shared runtime storage context: ${operations.runtimeStorage.readyImages.assetCount} ready image(s), ${formatAdminMediaBytes(operations.runtimeStorage.readyImages.totalBytes)}; ${operations.runtimeStorage.readyClips.assetCount} ready clip(s), ${formatAdminMediaBytes(operations.runtimeStorage.readyClips.totalBytes)}. Images include all Build runtime image uploads, not only camera captures.`,
|
|
2251
|
+
);
|
|
2252
|
+
if (monthlyMediaCosts.alerts.length === 0) {
|
|
2253
|
+
console.log("Media-cost alerts: none.");
|
|
2254
|
+
} else {
|
|
2255
|
+
for (const alert of monthlyMediaCosts.alerts) {
|
|
2256
|
+
console.log(
|
|
2257
|
+
`Media-cost alert [${String(alert.severity).toUpperCase()}] ${alert.code}: ${alert.message}`,
|
|
2258
|
+
);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
console.log(
|
|
2262
|
+
"These are conservative provider-cost ledger estimates, not an AWS invoice; reconcile IVS, MediaConvert, replay S3, and shared runtime S3 Cost Explorer data separately after billing lag.",
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2050
2266
|
function printAdminResult({ operation, result }) {
|
|
2051
2267
|
const data = result?.data || {};
|
|
2052
2268
|
if (data.review) {
|
|
@@ -2057,6 +2273,14 @@ function printAdminResult({ operation, result }) {
|
|
|
2057
2273
|
console.log(`Review receipt: ${data.receiptPath}`);
|
|
2058
2274
|
return;
|
|
2059
2275
|
}
|
|
2276
|
+
if (data.monthlyAiCosts) {
|
|
2277
|
+
printAdminMonthlyAiCosts(data.monthlyAiCosts);
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
if (data.monthlyMediaCosts) {
|
|
2281
|
+
printAdminMonthlyMediaCosts(data.monthlyMediaCosts);
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2060
2284
|
if (data.validation) {
|
|
2061
2285
|
console.log(
|
|
2062
2286
|
`Editorial valid for edition #${data.validation.editionId}: ${data.validation.citedEventCount} cited and ${data.validation.coveredEventCount} covered event(s).`,
|
package/lib/commands.js
CHANGED
|
@@ -2772,6 +2772,8 @@ export function printHelp() {
|
|
|
2772
2772
|
lumine admin comment post --draft-id <id> [--json]
|
|
2773
2773
|
lumine admin comment edit <comment-id> --file <comment.md> [--json]
|
|
2774
2774
|
lumine admin brief [--days <1..30>] [--json]
|
|
2775
|
+
lumine admin ai-costs monthly [--json]
|
|
2776
|
+
lumine admin media-costs monthly [--json]
|
|
2775
2777
|
lumine admin bot-output [--days <1..30>|--cursor <cursor>] [--json]
|
|
2776
2778
|
lumine admin announcement post --file <announcement.md> [--json]
|
|
2777
2779
|
lumine admin chat send <user-id|username> --file <message.md> [--json]
|
package/lib/sdk.js
CHANGED
|
@@ -100,6 +100,43 @@ export const SDK_CLI_METHODS = {
|
|
|
100
100
|
write: true,
|
|
101
101
|
sdkReshape: "the SDK returns { success, usage }",
|
|
102
102
|
},
|
|
103
|
+
"media.getUsage": {
|
|
104
|
+
path: "api/media/usage",
|
|
105
|
+
scopes: ["media:read"],
|
|
106
|
+
sdkReshape: "the SDK returns the mediaEnergy object directly",
|
|
107
|
+
},
|
|
108
|
+
"media.getClip": { path: "api/media/clips/status", scopes: ["media:read"] },
|
|
109
|
+
"media.listClips": { path: "api/media/clips/list", scopes: ["media:read"] },
|
|
110
|
+
"live.list": {
|
|
111
|
+
path: "api/live/list",
|
|
112
|
+
scopes: ["live:read"],
|
|
113
|
+
sdkReshape: "the SDK returns the sessions array directly",
|
|
114
|
+
},
|
|
115
|
+
"live.get": {
|
|
116
|
+
path: "api/live/status",
|
|
117
|
+
scopes: ["live:read"],
|
|
118
|
+
sdkReshape: "the SDK returns the session object directly",
|
|
119
|
+
},
|
|
120
|
+
"live.stop": {
|
|
121
|
+
path: "api/live/stop",
|
|
122
|
+
scopes: ["live:write"],
|
|
123
|
+
write: true,
|
|
124
|
+
},
|
|
125
|
+
"live.listReplays": {
|
|
126
|
+
path: "api/live/replays/list",
|
|
127
|
+
scopes: ["live:read"],
|
|
128
|
+
sdkReshape: "the SDK returns the replays array directly",
|
|
129
|
+
},
|
|
130
|
+
"live.getReplay": {
|
|
131
|
+
path: "api/live/replays/status",
|
|
132
|
+
scopes: ["live:read"],
|
|
133
|
+
sdkReshape: "the SDK returns the replay object directly",
|
|
134
|
+
},
|
|
135
|
+
"live.deleteReplay": {
|
|
136
|
+
path: "api/live/replays/delete",
|
|
137
|
+
scopes: ["live:write"],
|
|
138
|
+
write: true,
|
|
139
|
+
},
|
|
103
140
|
"notifications.getSubscription": { path: "api/notifications/subscription", scopes: ["notifications:read"] },
|
|
104
141
|
"notifications.subscribe": { path: "api/notifications/subscription/subscribe", scopes: ["notifications:write"], write: true },
|
|
105
142
|
"notifications.unsubscribe": { path: "api/notifications/subscription/unsubscribe", scopes: ["notifications:write"], write: true },
|
|
@@ -164,6 +201,8 @@ export const SDK_CLI_METHOD_NAMES_BY_PATH = (() => {
|
|
|
164
201
|
// without --allow-write.
|
|
165
202
|
export const SDK_CLI_READ_SCOPES = [
|
|
166
203
|
"files:read",
|
|
204
|
+
"media:read",
|
|
205
|
+
"live:read",
|
|
167
206
|
"user:read",
|
|
168
207
|
"users:read",
|
|
169
208
|
"dailyReflections:read",
|
package/package.json
CHANGED
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Build SDK Index
|
|
2
2
|
|
|
3
|
-
Version: 1.
|
|
4
|
-
Updated: 2026-08-
|
|
5
|
-
Generated: 2026-08-
|
|
3
|
+
Version: 1.38.2
|
|
4
|
+
Updated: 2026-08-27
|
|
5
|
+
Generated: 2026-08-27T12:11:09.595Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -26,9 +26,13 @@ Generated: 2026-08-26T15:17:11.027Z
|
|
|
26
26
|
- Live web search is enabled by default for Twinkle.ai.chat and for Medium/High Twinkle.ai.generateObject and Twinkle.characters.chat requests. App authors can pass webSearch: false to disable it for their app. Search uses the provider's live web-search tool and is included in AI Energy usage; structured and character Lite Mode remains tool-free.
|
|
27
27
|
- Interface text must not be selectable on touch devices: apply user-select: none plus -webkit-user-select: none and -webkit-touch-callout: none to interface text (HUD, buttons, labels, menus, scores, game controls) so mobile long-press does not highlight UI. Keep text inputs and genuinely user-copyable content selectable.
|
|
28
28
|
- Build app tab mute is enforced by the host runtime automatically for standard media elements and Web Audio connections to AudioContext.destination. Apps with custom audio engines can also observe Twinkle.onAudioMuteChange and check Twinkle.isAudioMuted.
|
|
29
|
+
- Use Twinkle.media for camera photos and camera-only two-second clips. Twinkle confirms each capture or paid processing action. Clips are processed to canonical 480p MP4 assets before they become visible; use sharedDb or privateDb to publish/store the returned asset metadata.
|
|
30
|
+
- Static media published through sharedDb is app-owned feed data. A public user-generated feed must provide a visible report flow and owner removal, and must not claim that Twinkle globally moderates those posts.
|
|
31
|
+
- Use Twinkle.live for one-way app livestreams and Twinkle.chat for the accompanying thread. Free livestreams require a verified host, end after at most 15 minutes, and issue at most 10 private viewer grants. Twinkle keeps platform-owned live-status/end controls above active hosts, so app code cannot hide or replace the broadcaster's Stop path.
|
|
32
|
+
- Media Energy is separate from AI Energy. Replace Media Energy UI only from canonical mediaEnergy/getUsage responses; never decrement, reserve, or synthesize it in app code.
|
|
29
33
|
|
|
30
34
|
## Token Scopes
|
|
31
|
-
files:read, user:read, users:read, dailyReflections:read, content:read, content:write, sharedDb:read, sharedDb:write, privateDb:read, privateDb:write, files:write, chat:read, chat:write, notifications:read, notifications:write, notifications:emit, reminders:read, reminders:write
|
|
35
|
+
files:read, media:read, media:write, live:read, live:write, user:read, users:read, dailyReflections:read, content:read, content:write, sharedDb:read, sharedDb:write, privateDb:read, privateDb:write, files:write, chat:read, chat:write, notifications:read, notifications:write, notifications:emit, reminders:read, reminders:write
|
|
32
36
|
|
|
33
37
|
## Namespaces
|
|
34
38
|
|
|
@@ -282,7 +286,7 @@ console.log(analysis.bestMove, analysis.evaluation, analysis.mate);
|
|
|
282
286
|
- Simple visible same-origin images can still use normal browser anchors with href and download.
|
|
283
287
|
- Example: await Twinkle.files.saveAs({ fileName: 'fashion-guide.png', dataUrl: imageUrl, mimeType: 'image/png' });
|
|
284
288
|
- async uploadGenerated({ fileName, url, dataUrl, data, text, json, bytes, blob, file, mimeType } = {}) | scopes: files:write
|
|
285
|
-
- Returns: { assets: [{ id, buildId, fileName, originalFileName, mimeType, sizeBytes, filePath, url, thumbUrl, fileType, uploadedByUserId, createdAt }], failed?: [{ fileName, message }], canceled }
|
|
289
|
+
- Returns: { assets: [{ id, buildId, fileName, originalFileName, mimeType, sizeBytes, filePath, url, thumbUrl, fileType, mediaKind, durationMs, uploadedByUserId, createdAt }], failed?: [{ fileName, message }], canceled }
|
|
286
290
|
- Upload an app-generated file to Twinkle-hosted cloud storage without opening a picker, then store the returned asset refs in sharedDb/privateDb/userDb.
|
|
287
291
|
- Signed-in viewers only.
|
|
288
292
|
- Uploads generated blobs, files, bytes, data URLs, or fetchable URLs to Twinkle-hosted cloud storage.
|
|
@@ -290,7 +294,7 @@ console.log(analysis.bestMove, analysis.evaluation, analysis.mate);
|
|
|
290
294
|
- Store the returned asset metadata in sharedDb/privateDb/userDb instead of storing raw file bytes in a DB record.
|
|
291
295
|
- Example: const { assets } = await Twinkle.files.uploadGenerated({ fileName: 'fashion-guide.png', dataUrl: generatedImageUrl, mimeType: 'image/png' });
|
|
292
296
|
- async pickAndUpload({ accept, multiple } = {}) | scopes: files:write
|
|
293
|
-
- Returns: { assets: [{ id, buildId, fileName, originalFileName, mimeType, sizeBytes, filePath, url, thumbUrl, fileType, uploadedByUserId, createdAt }], failed?: [{ fileName, message }], canceled }
|
|
297
|
+
- Returns: { assets: [{ id, buildId, fileName, originalFileName, mimeType, sizeBytes, filePath, url, thumbUrl, fileType, mediaKind, durationMs, uploadedByUserId, createdAt }], failed?: [{ fileName, message }], canceled }
|
|
294
298
|
- Pick supported local files and upload them to Twinkle-hosted cloud storage, then store the returned asset refs in sharedDb/privateDb/userDb.
|
|
295
299
|
- Signed-in viewers only.
|
|
296
300
|
- Uploads to Twinkle-hosted cloud storage and returns asset references.
|
|
@@ -299,7 +303,7 @@ console.log(analysis.bestMove, analysis.evaluation, analysis.mate);
|
|
|
299
303
|
- Store the returned asset metadata in sharedDb/privateDb/userDb instead of storing raw file bytes in a DB record.
|
|
300
304
|
- Example: const { assets, canceled } = await Twinkle.files.pickAndUpload({ accept: 'image/*,.pdf', multiple: true });
|
|
301
305
|
- async list({ cursor, limit } = {}) | scopes: files:read
|
|
302
|
-
- Returns: { assets: [{ id, buildId, fileName, originalFileName, mimeType, sizeBytes, filePath, url, thumbUrl, fileType, uploadedByUserId, createdAt }], nextCursor, usage: { totalBytes, fileCount, maxRuntimeFileStorageBytes, remainingBytes } | null }
|
|
306
|
+
- Returns: { assets: [{ id, buildId, fileName, originalFileName, mimeType, sizeBytes, filePath, url, thumbUrl, fileType, mediaKind, durationMs, uploadedByUserId, createdAt }], nextCursor, usage: { totalBytes, fileCount, maxRuntimeFileStorageBytes, remainingBytes } | null }
|
|
303
307
|
- List the current viewer's uploaded runtime files for this build.
|
|
304
308
|
- Signed-in viewers only.
|
|
305
309
|
- Lists the current viewer's ready uploads for this build only.
|
|
@@ -311,6 +315,117 @@ console.log(analysis.bestMove, analysis.evaluation, analysis.mate);
|
|
|
311
315
|
- Deletes one of the current viewer's uploaded runtime files and updates quota usage.
|
|
312
316
|
- Example: await Twinkle.files.delete(assetId);
|
|
313
317
|
|
|
318
|
+
### Twinkle.media
|
|
319
|
+
- async capturePhoto({ facingMode?, maxWidth?, quality?, settleMs?, fileName? } = {}) | scopes: files:write
|
|
320
|
+
- Returns: { asset, assets, failed }
|
|
321
|
+
- Ask for camera permission, capture one JPEG photo, and upload it to the current viewer's Twinkle file storage.
|
|
322
|
+
- Signed-in viewers only. Call from an explicit viewer action; Twinkle shows its own one-action confirmation before the browser may show camera permission.
|
|
323
|
+
- The photo is saved in the viewer's Twinkle file storage. The returned asset is canonical server state and can be stored in sharedDb/privateDb/userDb.
|
|
324
|
+
- Example: const { asset } = await Twinkle.media.capturePhoto({ facingMode: 'user' });
|
|
325
|
+
if (asset) await Twinkle.sharedDb.addEntry('photos', asset);
|
|
326
|
+
- async recordClip({ previewElement?, facingMode?, fileName?, waitForReady?, timeoutMs? } = {}) | scopes: media:write
|
|
327
|
+
- Returns: { clip: { id, status, durationMs, failureCode, asset }, mediaEnergy }
|
|
328
|
+
- Record a camera-only short video, upload it, and by default wait for the canonical two-second 480p MP4 asset.
|
|
329
|
+
- Call from an explicit viewer action. Twinkle confirms each recording before requesting camera permission.
|
|
330
|
+
- The recording is camera-only; use Twinkle.live when audio is part of the experience.
|
|
331
|
+
- The server targets a two-second maximum input window and processes it to 480p MP4. Encoder frame boundaries can differ by one frame; app code cannot raise the limit.
|
|
332
|
+
- By default this method polls confirmed server state until ready. Pass waitForReady: false to receive the processing ID immediately, then call getClip().
|
|
333
|
+
- Example: const { clip } = await Twinkle.media.recordClip({ previewElement: '#cameraPreview' });
|
|
334
|
+
await Twinkle.sharedDb.addEntry('clips', clip.asset);
|
|
335
|
+
- async uploadClip({ file?, blob?, fileName?, mimeType?, requestId?, waitForReady?, timeoutMs? }) | scopes: media:write
|
|
336
|
+
- Returns: { clip: { id, status, durationMs, failureCode, asset }, mediaEnergy }
|
|
337
|
+
- Upload a generated or selected video through the same server-bounded two-second clip pipeline.
|
|
338
|
+
- Call from an explicit viewer action. Twinkle confirms each selected or generated video before upload and paid processing.
|
|
339
|
+
- Input is limited to 8 MB. The canonical output is a server-produced 480p MP4 targeting a two-second maximum, with at most a frame of encoder-boundary variance.
|
|
340
|
+
- Use a stable requestId when retrying the same user action.
|
|
341
|
+
- Example: const result = await Twinkle.media.uploadClip({ file: recordedFile });
|
|
342
|
+
- async getClip(assetId) | scopes: media:read
|
|
343
|
+
- Returns: { clip: { id, status, durationMs, failureCode, asset }, mediaEnergy }
|
|
344
|
+
- Load and reconcile canonical processing state for one of the current viewer's clips.
|
|
345
|
+
- Example: const { clip } = await Twinkle.media.getClip(assetId);
|
|
346
|
+
- async listClips({ cursor?, limit? } = {}) | scopes: media:read
|
|
347
|
+
- Returns: { assets, nextCursor, mediaEnergy }
|
|
348
|
+
- List the current viewer's ready short clips for this Build app.
|
|
349
|
+
- Example: const { assets } = await Twinkle.media.listClips({ limit: 20 });
|
|
350
|
+
- async getUsage() | scopes: media:read
|
|
351
|
+
- Returns: { monthKey, resetsAt, global, user, build, energyPercent, energySegments, energySegmentsRemaining }
|
|
352
|
+
- Load canonical current Media Energy for this viewer and app.
|
|
353
|
+
- Replace displayed state only from this response or a newer mediaEnergy response. Never decrement or synthesize the battery locally.
|
|
354
|
+
- global.carryoverMicroUsd accounts for reservations that crossed the UTC month boundary so the global reset cannot double the budget.
|
|
355
|
+
- Example: const mediaEnergy = await Twinkle.media.getUsage();
|
|
356
|
+
renderBattery(mediaEnergy.energyPercent);
|
|
357
|
+
|
|
358
|
+
### Twinkle.live
|
|
359
|
+
- async start({ previewElement?, facingMode?, audio?, durationSeconds?, maxViewers?, saveReplay?, requestId? } = {}) | scopes: live:write
|
|
360
|
+
- Returns: { session: { id, replayId, buildId, hostUserId, status, maxViewers, viewersGranted, durationSeconds, saveReplay, updatedAt, hardEndsAt }, mediaEnergy }
|
|
361
|
+
- Create an IVS channel, attach the camera/microphone, begin broadcasting, and optionally save a seven-day replay.
|
|
362
|
+
- Call from an explicit viewer action. Twinkle confirms each new broadcast before camera/microphone permission or paid channel creation.
|
|
363
|
+
- saveReplay defaults to false. When true, the same action confirmation says the stream will be saved, and Twinkle records it to private storage for seven days after it becomes ready.
|
|
364
|
+
- Replay storage is included in the live Media Energy reservation. Replay viewing has its own Media Energy reservation.
|
|
365
|
+
- previewElement must be a canvas element or selector because the IVS Broadcast SDK draws a composited preview.
|
|
366
|
+
- Free sessions broadcast at 854x480 and are capped server-side at 15 minutes and 10 private viewer grants. Lower durationSeconds/maxViewers values are allowed.
|
|
367
|
+
- Twinkle confirms that a platform-owned live indicator and End stream action are present before returning broadcast credentials to the app, and keeps the control until server cleanup is canonically terminal. Fullscreen and Picture-in-Picture are unavailable while hosting so that Stop control stays visible.
|
|
368
|
+
- The SDK does not include broadcast credentials in its returned value. Credentials are ephemeral, and the SDK stops local broadcasting at hardEndsAt while the API independently stops and deletes the IVS channel.
|
|
369
|
+
- Example: const { session } = await Twinkle.live.start({ previewElement: '#broadcastPreview', audio: true });
|
|
370
|
+
await Twinkle.sharedDb.setKvItems('live', [{ key: 'current', value: session }]);
|
|
371
|
+
- async list() | scopes: live:read
|
|
372
|
+
- Returns: Array<LiveSession>
|
|
373
|
+
- List currently available livestream sessions for this Build app.
|
|
374
|
+
- Only sessions canonically acknowledged as live are listed; channels still being prepared are never advertised to viewers.
|
|
375
|
+
- Example: const sessions = await Twinkle.live.list();
|
|
376
|
+
- async get(sessionId) | scopes: live:read
|
|
377
|
+
- Returns: LiveSession | null
|
|
378
|
+
- Load canonical server status for a livestream in this Build app.
|
|
379
|
+
- Example: const session = await Twinkle.live.get(sessionId);
|
|
380
|
+
- async watch(sessionId, { videoElement, requestId? }) | scopes: live:write
|
|
381
|
+
- Returns: { session, viewerGrantId, mediaEnergy, playbackStarted }
|
|
382
|
+
- Use a private single-use playback grant to attach a livestream to an HTML video element.
|
|
383
|
+
- Call from an explicit viewer action. Twinkle confirms admission before allocating the private viewer grant or using Media Energy.
|
|
384
|
+
- The first watch action consumes one of at most 10 grants for the session. Repeated watch() calls in the same page reuse the local grant until leave().
|
|
385
|
+
- Playback authorization is single-use and capped at SD. The watch() result omits the signed playback URL; bridge traffic is still app-visible and must be treated as ephemeral.
|
|
386
|
+
- playbackStarted becomes true only after IVS or the HTML video element confirms a playing state. If browser autoplay is blocked or no playing state is confirmed, it is false and the video controls remain available so the viewer can start playback explicitly.
|
|
387
|
+
- Viewers may use the video element's ordinary fullscreen and Picture-in-Picture controls.
|
|
388
|
+
- Example: await Twinkle.live.watch(session.id, { videoElement: '#liveVideo' });
|
|
389
|
+
- async leave(sessionId) | scopes: live:write
|
|
390
|
+
- Returns: { success }
|
|
391
|
+
- Destroy the local player and revoke/settle its private viewer session.
|
|
392
|
+
- Example: await Twinkle.live.leave(sessionId);
|
|
393
|
+
- async stop(sessionId) | scopes: live:write
|
|
394
|
+
- Returns: { session, cleanupInProgress, mediaEnergy }
|
|
395
|
+
- Stop local broadcasting and ask the server to stop and delete the host's ephemeral IVS channel.
|
|
396
|
+
- Use the returned canonical session state. Do not locally synthesize an ended status.
|
|
397
|
+
- Example: await Twinkle.live.stop(sessionId);
|
|
398
|
+
- async listReplays({ limit? } = {}) | scopes: live:read
|
|
399
|
+
- Returns: Array<LiveReplay>
|
|
400
|
+
- List canonical saved replays for this Build app.
|
|
401
|
+
- Ready, unexpired replays are visible to viewers in the app. A creator can also see processing or failed state for their own opted-in stream.
|
|
402
|
+
- Replays expire seven days after becoming ready. Replace displayed state from this canonical response; do not synthesize processing or ready state locally.
|
|
403
|
+
- Example: const replays = await Twinkle.live.listReplays({ limit: 20 });
|
|
404
|
+
- async getReplay(replayId) | scopes: live:read
|
|
405
|
+
- Returns: LiveReplay | null
|
|
406
|
+
- Load canonical processing, ready, or failed state for a visible replay.
|
|
407
|
+
- Only the creator can see a processing or failed replay; ready replays are visible to signed-in viewers in this app.
|
|
408
|
+
- Example: const replay = await Twinkle.live.getReplay(replayId);
|
|
409
|
+
- async watchReplay(replayId, { videoElement, requestId? }) | scopes: live:write
|
|
410
|
+
- Returns: { replay, viewerGrantId, mediaEnergy, playbackStarted }
|
|
411
|
+
- Open a short-lived private playback grant and attach a saved replay to an HTML video element.
|
|
412
|
+
- Call from an explicit viewer action. Twinkle confirms each playback grant before using Media Energy.
|
|
413
|
+
- Admission reserves the replay's maximum delivery estimate; leave, page close, or playback end settles the canonical elapsed viewing window instead of charging unused playback time.
|
|
414
|
+
- The grant lasts at most 20 minutes and is settled automatically when playback ends, on leaveReplay(), when the page closes, or at server expiry.
|
|
415
|
+
- playbackStarted becomes true only after IVS or the HTML video element confirms a playing state. If browser autoplay is blocked or no playing state is confirmed, it is false and the video controls remain available so the viewer can start playback explicitly.
|
|
416
|
+
- Viewers may use the video element's ordinary fullscreen and Picture-in-Picture controls.
|
|
417
|
+
- Example: await Twinkle.live.watchReplay(replay.id, { videoElement: '#replayVideo' });
|
|
418
|
+
- async leaveReplay(replayId) | scopes: live:write
|
|
419
|
+
- Returns: { success }
|
|
420
|
+
- Destroy the local replay player and canonically settle its private viewer grant.
|
|
421
|
+
- The returned canonical settlement charges the conservative elapsed playback estimate, capped by the replay duration.
|
|
422
|
+
- Example: await Twinkle.live.leaveReplay(replayId);
|
|
423
|
+
- async deleteReplay(replayId, { requestId? } = {}) | scopes: live:write
|
|
424
|
+
- Returns: { replay, cleanupInProgress, mediaEnergy }
|
|
425
|
+
- Permanently remove an opted-in replay through the canonical private-storage cleanup path.
|
|
426
|
+
- Twinkle asks for an action-specific confirmation. Use the canonical returned state; cleanupInProgress means provider recording finalization or deletion is still being confirmed.
|
|
427
|
+
- Example: await Twinkle.live.deleteReplay(replayId);
|
|
428
|
+
|
|
314
429
|
### Twinkle.ai
|
|
315
430
|
- async getUsagePolicy() | scopes: none
|
|
316
431
|
- Returns: BuildAiUsagePolicy | null
|
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -740,10 +740,7 @@ type AdminTodo = {
|
|
|
740
740
|
|
|
741
741
|
type AdminTodoList = Success<{
|
|
742
742
|
todos: AdminTodo[];
|
|
743
|
-
statusFilter:
|
|
744
|
-
| "pending"
|
|
745
|
-
| "all"
|
|
746
|
-
| AdminTodo["status"];
|
|
743
|
+
statusFilter: "pending" | "all" | AdminTodo["status"];
|
|
747
744
|
truncated: boolean;
|
|
748
745
|
}>;
|
|
749
746
|
|
|
@@ -1700,6 +1697,8 @@ A run report that skipped the conduct review is incomplete.
|
|
|
1700
1697
|
```bash
|
|
1701
1698
|
lumine admin brief --json
|
|
1702
1699
|
lumine admin brief --days 3 --json
|
|
1700
|
+
lumine admin ai-costs monthly --json
|
|
1701
|
+
lumine admin media-costs monthly --json
|
|
1703
1702
|
lumine admin notable add 12647 --note "Top authored-activity kid of the window: 11 subjects, 61 comments." --json
|
|
1704
1703
|
lumine admin notable add Minecrarft_guy --note "Helped three new builders debug their projects and gave detailed feedback on five posts." --json
|
|
1705
1704
|
```
|
|
@@ -1711,6 +1710,231 @@ end every run report with an **"Insights for Mikey"** section carrying only
|
|
|
1711
1710
|
the deltas and anomalies worth his time, next to the escalation list. Never
|
|
1712
1711
|
dump raw sections at him.
|
|
1713
1712
|
|
|
1713
|
+
### Application AI calendar-month cost (standing duty, every run)
|
|
1714
|
+
|
|
1715
|
+
Run `lumine admin ai-costs monthly --json` during every website-management
|
|
1716
|
+
run. This read-only command requires the active delegated run and returns one
|
|
1717
|
+
server-owned calendar summary from the canonical deduplicated application AI-
|
|
1718
|
+
cost ledger. It deliberately takes no `--days`: all boundaries are UTC calendar
|
|
1719
|
+
months, so the result is directly comparable from one run to the next.
|
|
1720
|
+
|
|
1721
|
+
The previous month is a closed-calendar-month estimated total. Current-month
|
|
1722
|
+
MTD contains only completed UTC days and names its inclusive `throughDayKey`.
|
|
1723
|
+
The current UTC day's still-filling bucket is returned separately as
|
|
1724
|
+
`inProgressDay`; **never add it to MTD or either projection**. A missing daily
|
|
1725
|
+
aggregate inside the covered calendar is a recorded zero-cost day, not a
|
|
1726
|
+
reason to shrink the denominator.
|
|
1727
|
+
|
|
1728
|
+
Two clearly different full-month projections are returned:
|
|
1729
|
+
|
|
1730
|
+
- `allCompletedDaysPace` retains completed-day spend, then applies the average
|
|
1731
|
+
across every completed calendar day (including recorded zero days) to the
|
|
1732
|
+
current and remaining UTC days;
|
|
1733
|
+
- `recentSevenCompletedDaysPace` retains completed-day spend, then applies the
|
|
1734
|
+
average of the latest seven completed UTC days to the current and remaining
|
|
1735
|
+
days. It is `null` until seven days have completed.
|
|
1736
|
+
|
|
1737
|
+
Both projections exclude the partial day's actual cost, replace every not-yet-
|
|
1738
|
+
completed day with their stated daily pace, and compare their projected total
|
|
1739
|
+
with the previous closed month. They are run-rate scenarios, not forecasts
|
|
1740
|
+
from a billing provider. All ledger values are pricing-based estimates rather
|
|
1741
|
+
than invoices and can change if canonical usage attribution or pricing is
|
|
1742
|
+
corrected.
|
|
1743
|
+
|
|
1744
|
+
The stable JSON payload is `data.monthlyAiCosts`:
|
|
1745
|
+
|
|
1746
|
+
```ts
|
|
1747
|
+
type MonthlyAiCosts = {
|
|
1748
|
+
schemaVersion: 1;
|
|
1749
|
+
generatedAt: number; // Unix seconds
|
|
1750
|
+
timezone: "UTC";
|
|
1751
|
+
currency: "USD";
|
|
1752
|
+
source: {
|
|
1753
|
+
basis: "canonical_deduplicated_ai_cost_report";
|
|
1754
|
+
reportDays: number;
|
|
1755
|
+
reportStartDayIndex: number;
|
|
1756
|
+
reportEndDayIndex: number;
|
|
1757
|
+
mtdIncludesInProgressDay: false;
|
|
1758
|
+
projectionsIncludeInProgressDayActual: false;
|
|
1759
|
+
};
|
|
1760
|
+
previousMonth: {
|
|
1761
|
+
status: "closed";
|
|
1762
|
+
monthKey: string; // YYYY-MM
|
|
1763
|
+
startDayKey: string;
|
|
1764
|
+
endDayKeyExclusive: string;
|
|
1765
|
+
calendarDayCount: number;
|
|
1766
|
+
estimatedCostUsd: number;
|
|
1767
|
+
};
|
|
1768
|
+
currentMonth: {
|
|
1769
|
+
status: "in_progress";
|
|
1770
|
+
monthKey: string;
|
|
1771
|
+
startDayKey: string;
|
|
1772
|
+
endDayKeyExclusive: string;
|
|
1773
|
+
calendarDayCount: number;
|
|
1774
|
+
completed: {
|
|
1775
|
+
dayCount: number;
|
|
1776
|
+
throughDayKey: string | null;
|
|
1777
|
+
estimatedCostUsd: number;
|
|
1778
|
+
dailyAverageUsd: number | null;
|
|
1779
|
+
};
|
|
1780
|
+
inProgressDay: {
|
|
1781
|
+
dayIndex: number;
|
|
1782
|
+
dayKey: string;
|
|
1783
|
+
estimatedCostUsd: number;
|
|
1784
|
+
eventCount: number;
|
|
1785
|
+
requestCount: number;
|
|
1786
|
+
};
|
|
1787
|
+
daysToEstimate: number;
|
|
1788
|
+
projections: {
|
|
1789
|
+
allCompletedDaysPace: MonthlyAiCostProjection | null;
|
|
1790
|
+
recentSevenCompletedDaysPace: MonthlyAiCostProjection | null;
|
|
1791
|
+
};
|
|
1792
|
+
};
|
|
1793
|
+
};
|
|
1794
|
+
|
|
1795
|
+
type MonthlyAiCostProjection = {
|
|
1796
|
+
basis: "all_completed_days" | "recent_7_completed_days";
|
|
1797
|
+
basisStartDayKey: string;
|
|
1798
|
+
basisEndDayKey: string;
|
|
1799
|
+
basisDayCount: number;
|
|
1800
|
+
dailyAverageUsd: number;
|
|
1801
|
+
remainingDayCount: number;
|
|
1802
|
+
estimatedMonthTotalUsd: number;
|
|
1803
|
+
comparisonToPreviousMonth: {
|
|
1804
|
+
estimatedCostDeltaUsd: number;
|
|
1805
|
+
percentChange: number | null; // null when the prior total is zero
|
|
1806
|
+
};
|
|
1807
|
+
};
|
|
1808
|
+
```
|
|
1809
|
+
|
|
1810
|
+
Release boundary: `/cli/admin/ai-costs/monthly` and all calendar math are API-
|
|
1811
|
+
owned. Deploy and verify the compatible `twinkle-api` route before publishing
|
|
1812
|
+
or installing the Lumine CLI release that invokes it; an older API will reject
|
|
1813
|
+
the new command instead of synthesizing figures locally.
|
|
1814
|
+
|
|
1815
|
+
### Lumine media feature cost and cleanup watch (standing duty, every run)
|
|
1816
|
+
|
|
1817
|
+
Run `lumine admin media-costs monthly --json` during every website-management
|
|
1818
|
+
run. This read-only, delegated-run-gated command reports the canonical Media
|
|
1819
|
+
Energy ledger for short clips, livestream input/viewer usage, and replay
|
|
1820
|
+
storage/viewing. Include in
|
|
1821
|
+
**"Insights for Mikey"** on every run:
|
|
1822
|
+
|
|
1823
|
+
- current-month settled estimated cost, active reservations, cross-month
|
|
1824
|
+
carryover, guarded total, global limit, remaining headroom, and percent used;
|
|
1825
|
+
- the current UTC day's reservations, settlements, cancellations, and settled
|
|
1826
|
+
estimated cost;
|
|
1827
|
+
- the current UTC day's privacy-safe stream-attempt cohort: attempted,
|
|
1828
|
+
reached-live, ended-after-live, failed, cancelled-before-live, still in
|
|
1829
|
+
progress, and grouped server failure-code counts;
|
|
1830
|
+
- clip, live-input, live-viewer, and replay-viewer action/cost breakdowns;
|
|
1831
|
+
- whether the global usage row reconciles exactly with reservation rows;
|
|
1832
|
+
- every returned alert, plus incomplete clip jobs, cost-bearing IVS channels,
|
|
1833
|
+
active-or-cleanup-pending sessions, possible orphaned sessions, overdue
|
|
1834
|
+
cleanup, replay finalization/deletion state, retained replay bytes/objects,
|
|
1835
|
+
and expired-active live or replay viewer grants;
|
|
1836
|
+
- ready image/clip storage counts and bytes as scale context.
|
|
1837
|
+
|
|
1838
|
+
Treat `status: "critical"`, any reconciliation mismatch, overdue IVS cleanup,
|
|
1839
|
+
or overdue replay finalization/deletion as an operational incident to
|
|
1840
|
+
investigate in the same run. Treat
|
|
1841
|
+
`status: "attention"` as a required finding, not a decorative warning. The
|
|
1842
|
+
server's request-time global Media Energy guardrail defaults to **$40/month**;
|
|
1843
|
+
the separate AWS Budget is **$50/month**, leaving provider-billing and shared-
|
|
1844
|
+
infrastructure headroom. Never infer or locally decrement either value.
|
|
1845
|
+
|
|
1846
|
+
The ledger is the immediate application source of truth and deliberately uses
|
|
1847
|
+
conservative provider-cost estimates. It is not an AWS invoice. Photo capture
|
|
1848
|
+
uses existing Build runtime file storage rather than the paid Media Energy
|
|
1849
|
+
ledger; `operations.runtimeStorage.readyImages` therefore reports all ready
|
|
1850
|
+
Build runtime images, not camera captures alone. Reconcile delayed AWS
|
|
1851
|
+
MediaConvert and IVS service charges every run as described below. S3 is shared
|
|
1852
|
+
with other Twinkle uploads, so report its service-level cost as shared context,
|
|
1853
|
+
not as photo-only spend.
|
|
1854
|
+
|
|
1855
|
+
Release boundary: `/cli/admin/media-costs/monthly` owns the canonical ledger,
|
|
1856
|
+
reconciliation, resource-state checks, and alerts. Deploy and verify that API
|
|
1857
|
+
route before publishing or installing the Lumine CLI release that invokes it.
|
|
1858
|
+
|
|
1859
|
+
`currentUtcDay.streamAttempts` is a `createdAt` cohort for that UTC day. Its
|
|
1860
|
+
outcomes partition every attempt into `endedCount` (reached live, then ended),
|
|
1861
|
+
`failedCount` (failed or cleanup-failed), `cancelledCount` (ended before ever
|
|
1862
|
+
reaching live), or `inProgressCount`; `reachedLiveCount` is the overlapping
|
|
1863
|
+
milestone count. `failureCodeCounts` contains only server-defined codes and
|
|
1864
|
+
counts—never usernames, Build ids, titles, viewer identities, or report
|
|
1865
|
+
identities. `operations.live.stillActiveOrCleanupPendingCount` and
|
|
1866
|
+
`possibleOrphanedCount` are current global counts, not members of the daily
|
|
1867
|
+
cohort.
|
|
1868
|
+
|
|
1869
|
+
Replay storage and write cost is conservatively embedded in an opted-in
|
|
1870
|
+
`live-input` reservation; `replay-viewer` is a separate kind. Report
|
|
1871
|
+
`operations.replays` (pending, processing, ready, failed, deleting,
|
|
1872
|
+
delete-failed, overdue finalization/deletion, expired-ready, bytes, and object
|
|
1873
|
+
count) and `operations.replayViewers` on every run. A replay finalization or
|
|
1874
|
+
deletion alert is an operational incident because private recording cleanup is
|
|
1875
|
+
part of the feature contract.
|
|
1876
|
+
|
|
1877
|
+
### AWS monthly bill expectation (standing duty, every run)
|
|
1878
|
+
|
|
1879
|
+
Starting 2026-08-27, every website-management run must also check AWS Cost
|
|
1880
|
+
Explorer and include the current calendar month's expected AWS bill in
|
|
1881
|
+
**"Insights for Mikey"**. This is an account-level infrastructure cost check,
|
|
1882
|
+
not the `aiSpending` application-cost section above; never substitute one for
|
|
1883
|
+
the other or combine their totals.
|
|
1884
|
+
|
|
1885
|
+
First verify the Twinkle AWS principal exactly as required by the repository
|
|
1886
|
+
agent guide. Use profile `mikey-iam`, pass an explicit region on every command,
|
|
1887
|
+
and stop rather than reading another account if the ARN is not
|
|
1888
|
+
`arn:aws:iam::019490893667:user/twinkle-admin`:
|
|
1889
|
+
|
|
1890
|
+
```bash
|
|
1891
|
+
aws sts get-caller-identity --profile mikey-iam --region us-east-1
|
|
1892
|
+
```
|
|
1893
|
+
|
|
1894
|
+
Then use UTC calendar boundaries and Cost Explorer's unblended-cost metric.
|
|
1895
|
+
`End` is exclusive: the month-to-date query below covers completed dates before
|
|
1896
|
+
`<today-UTC>`. On the first UTC day of a month, report that no completed-day MTD
|
|
1897
|
+
period exists instead of sending an empty interval.
|
|
1898
|
+
|
|
1899
|
+
```bash
|
|
1900
|
+
aws ce get-cost-and-usage --profile mikey-iam --region us-east-1 \
|
|
1901
|
+
--time-period Start=<month-start-YYYY-MM-01>,End=<today-UTC> \
|
|
1902
|
+
--granularity MONTHLY --metrics UnblendedCost
|
|
1903
|
+
|
|
1904
|
+
aws ce get-cost-and-usage --profile mikey-iam --region us-east-1 \
|
|
1905
|
+
--time-period Start=<month-start-YYYY-MM-01>,End=<today-UTC> \
|
|
1906
|
+
--granularity MONTHLY --metrics UnblendedCost \
|
|
1907
|
+
--group-by Type=DIMENSION,Key=SERVICE
|
|
1908
|
+
|
|
1909
|
+
aws ce get-cost-forecast --profile mikey-iam --region us-east-1 \
|
|
1910
|
+
--time-period Start=<today-UTC>,End=<next-month-YYYY-MM-01> \
|
|
1911
|
+
--metric UNBLENDED_COST --granularity DAILY \
|
|
1912
|
+
--prediction-interval-level 80
|
|
1913
|
+
```
|
|
1914
|
+
|
|
1915
|
+
Report the Cost Explorer snapshot date, currency, estimated MTD amount and its
|
|
1916
|
+
through-date, plus the returned **remaining-period** forecast mean and 80%
|
|
1917
|
+
lower/upper bounds. Verify that the first and last returned daily periods cover
|
|
1918
|
+
exactly the requested `Start`-inclusive, `End`-exclusive interval before doing
|
|
1919
|
+
any arithmetic; reject or separately explain a response with expanded or
|
|
1920
|
+
missing dates. Calculate the expected full-calendar-month mean and bounds by
|
|
1921
|
+
adding completed-day MTD to the sums of the returned daily mean, lower, and
|
|
1922
|
+
upper values. Do not use monthly granularity for this mid-month remainder:
|
|
1923
|
+
Cost Explorer can return the whole calendar month even when the requested
|
|
1924
|
+
start is mid-month, and adding MTD to that result would double-count. This
|
|
1925
|
+
split deliberately forecasts the current UTC day instead of mixing its
|
|
1926
|
+
incomplete actual into MTD. Label current-month actuals
|
|
1927
|
+
when `Estimated` is true, and describe the result as a Cost Explorer expectation
|
|
1928
|
+
rather than a final invoice because reporting lags and later credits, refunds,
|
|
1929
|
+
taxes, or adjustments can change the bill. If either the forecast or MTD query
|
|
1930
|
+
is unavailable, report the available component and say why a complete
|
|
1931
|
+
full-month expectation is unavailable instead of extrapolating it locally. A
|
|
1932
|
+
previous closed month may be quoted for context when the difference is
|
|
1933
|
+
material, but it is not a replacement for the current-month expectation.
|
|
1934
|
+
For the media watch, separately identify AWS Elemental MediaConvert and Amazon
|
|
1935
|
+
Interactive Video Service rows when present. Also report Amazon S3 as shared
|
|
1936
|
+
storage context, without attributing the whole S3 row to Lumine media.
|
|
1937
|
+
|
|
1714
1938
|
Ten sections (Mikey's chosen cut 2026-08-10; behavioral-insight and
|
|
1715
1939
|
farm-signal sections added that day; AI Card summon watch added 2026-08-24):
|
|
1716
1940
|
|
|
@@ -1762,8 +1986,8 @@ farm-signal sections added that day; AI Card summon watch added 2026-08-24):
|
|
|
1762
1986
|
every individual summon. Each group reports its maximum same-day account and
|
|
1763
1987
|
charged-summon totals, the multi-account days, and days above the shared
|
|
1764
1988
|
three-card limit. Review every `requiresIdentityInspection` group,
|
|
1765
|
-
prioritizing `daysAboveSharedLimit > 0`, with reason-required
|
|
1766
|
-
inspect`. If
|
|
1989
|
+
prioritizing `daysAboveSharedLimit > 0`, with reason-required
|
|
1990
|
+
`identity inspect`. If
|
|
1767
1991
|
`riskGroupsTruncated` is true, report that the bounded watch has more groups
|
|
1768
1992
|
than it returned rather than calling the review exhaustive. Add only
|
|
1769
1993
|
operator-confirmed exact accounts to an unbanned quota bucket with
|