@stage5/lumine 0.2.40 → 0.2.42
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 +78 -13
- package/lib/admin-news.js +236 -0
- package/lib/admin-workflows.js +434 -0
- package/lib/admin.js +644 -45
- package/lib/agent/mcp-server.js +138 -0
- package/lib/agent/providers/claude-code.js +214 -0
- package/lib/agent/providers/codex.js +522 -0
- package/lib/agent/providers/environment.js +40 -0
- package/lib/agent/providers/index.js +38 -0
- package/lib/agent/tool-session.js +301 -0
- package/lib/agent/trace.js +138 -0
- package/lib/agent.js +453 -0
- package/lib/api.js +81 -1
- package/lib/build-review.js +493 -0
- package/lib/commands.js +86 -9
- package/lib/constants.js +2 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +24 -3
- package/sdk/LUMINE_ADMIN.md +271 -32
package/lib/admin.js
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { assertAuthScope, resolveAuth } from "./auth.js";
|
|
4
4
|
import { requestJson } from "./http.js";
|
|
5
|
+
import {
|
|
6
|
+
extractNewsClaim,
|
|
7
|
+
readAdminJsonFile,
|
|
8
|
+
validateNewsEditorial,
|
|
9
|
+
writeNewsClaimArtifacts,
|
|
10
|
+
} from "./admin-news.js";
|
|
11
|
+
import {
|
|
12
|
+
runAutomaticPagination,
|
|
13
|
+
runBatchSkips,
|
|
14
|
+
} from "./admin-workflows.js";
|
|
15
|
+
import {
|
|
16
|
+
parseBuildReviewReceipt,
|
|
17
|
+
runManagedBuildReview,
|
|
18
|
+
} from "./build-review.js";
|
|
5
19
|
|
|
6
20
|
const MAX_EDITORIAL_FILE_BYTES = 256 * 1024;
|
|
7
21
|
const MAX_COMPOSED_TEXT_FILE_BYTES = 64 * 1024;
|
|
8
22
|
const MAX_COMPOSED_TEXT_LENGTH = 10_000;
|
|
9
23
|
const MAX_NOTABLE_NOTE_LENGTH = 2_000;
|
|
24
|
+
const MAX_IDENTITY_INSPECTION_REASON_LENGTH = 500;
|
|
25
|
+
const MAX_ESCALATION_DECISION_NOTE_LENGTH = 2_000;
|
|
10
26
|
|
|
11
27
|
// Operator-composed persona text (plain UTF-8, not JSON). The agent writes
|
|
12
28
|
// the content in the bot's persona itself; the server never invokes
|
|
@@ -64,6 +80,19 @@ function readEditorialFile(filePath) {
|
|
|
64
80
|
|
|
65
81
|
export async function adminCommand(options) {
|
|
66
82
|
const operation = parseAdminOperation(options);
|
|
83
|
+
if (operation.name === "news.validate") {
|
|
84
|
+
const validation = validateNewsEditorial({
|
|
85
|
+
claim: operation.claim,
|
|
86
|
+
editorial: operation.editorial,
|
|
87
|
+
});
|
|
88
|
+
const result = {
|
|
89
|
+
ok: true,
|
|
90
|
+
status: "success",
|
|
91
|
+
changed: false,
|
|
92
|
+
data: { validation },
|
|
93
|
+
};
|
|
94
|
+
return finishAdminOutput({ options, operation, result });
|
|
95
|
+
}
|
|
67
96
|
const viewFilter = resolveOperatorViewFilter({
|
|
68
97
|
operation,
|
|
69
98
|
unviewed: options.adminUnviewed,
|
|
@@ -112,34 +141,76 @@ export async function adminCommand(options) {
|
|
|
112
141
|
options.idempotencyKey || (operation.mutates ? `cli:${randomUUID()}` : "");
|
|
113
142
|
let result;
|
|
114
143
|
try {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (operation.name === "builds.candidates") {
|
|
127
|
-
result = normalizeAdminBuildCandidatesResult({
|
|
128
|
-
result,
|
|
129
|
-
siteUrl: options.siteUrl,
|
|
144
|
+
const fetchOperation = async (requestPath = operation.path) =>
|
|
145
|
+
requestJson({
|
|
146
|
+
method: operation.method,
|
|
147
|
+
url: `${options.apiUrl}${requestPath}`,
|
|
148
|
+
authToken: auth.token,
|
|
149
|
+
body: operation.body,
|
|
150
|
+
headers: {
|
|
151
|
+
...(runId ? { "x-lumine-admin-run-id": String(runId) } : {}),
|
|
152
|
+
...(requestId ? { "x-lumine-idempotency-key": requestId } : {}),
|
|
153
|
+
},
|
|
154
|
+
timeoutMs: options.timeoutMs,
|
|
130
155
|
});
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
result,
|
|
135
|
-
|
|
156
|
+
const transformResult = (rawResult) =>
|
|
157
|
+
transformAdminResult({
|
|
158
|
+
operation,
|
|
159
|
+
result: rawResult,
|
|
160
|
+
options,
|
|
161
|
+
recommendationContentTypes,
|
|
162
|
+
viewFilter,
|
|
136
163
|
});
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
164
|
+
if (operation.name === "build.review") {
|
|
165
|
+
result = await runManagedBuildReview({
|
|
166
|
+
options,
|
|
167
|
+
authToken: auth.token,
|
|
168
|
+
buildId: operation.buildId,
|
|
169
|
+
});
|
|
170
|
+
} else if (operation.name === "post.skip-batch") {
|
|
171
|
+
result = await runBatchSkips({
|
|
172
|
+
options,
|
|
173
|
+
authToken: auth.token,
|
|
174
|
+
runId,
|
|
175
|
+
parseTarget: parseRecommendationTarget,
|
|
176
|
+
});
|
|
177
|
+
} else if (options.adminAll) {
|
|
178
|
+
if (options.adminCursor) {
|
|
179
|
+
throw cliValidationError(
|
|
180
|
+
"Use --resume with the scan checkpoint instead of combining --all with --cursor.",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
result = await runAutomaticPagination({
|
|
184
|
+
options,
|
|
185
|
+
operation,
|
|
186
|
+
runId,
|
|
187
|
+
fetchPage: fetchOperation,
|
|
188
|
+
transformPage: transformResult,
|
|
189
|
+
recordCoverage: async (coverage) =>
|
|
190
|
+
requestJson({
|
|
191
|
+
method: "POST",
|
|
192
|
+
url: `${options.apiUrl}/cli/admin/daily-runs/coverage`,
|
|
193
|
+
authToken: auth.token,
|
|
194
|
+
body: coverage,
|
|
195
|
+
headers: {
|
|
196
|
+
"x-lumine-admin-run-id": String(runId),
|
|
197
|
+
"x-lumine-idempotency-key": `cli:queue-coverage:${runId}:${adminValueFingerprint(coverage).slice(0, 32)}`,
|
|
198
|
+
},
|
|
199
|
+
timeoutMs: options.timeoutMs,
|
|
200
|
+
}),
|
|
201
|
+
});
|
|
202
|
+
} else {
|
|
203
|
+
if (options.adminResume) {
|
|
204
|
+
throw cliValidationError("--resume requires --all or post skip-batch.");
|
|
205
|
+
}
|
|
206
|
+
result = transformResult(await fetchOperation());
|
|
140
207
|
}
|
|
141
208
|
} catch (error) {
|
|
142
|
-
if (
|
|
209
|
+
if (
|
|
210
|
+
operation.mutates &&
|
|
211
|
+
requestId &&
|
|
212
|
+
operation.name !== "post.skip-batch"
|
|
213
|
+
) {
|
|
143
214
|
const retryInstruction = `Retry with --idempotency-key ${requestId}.`;
|
|
144
215
|
error.data = error.data || {
|
|
145
216
|
ok: false,
|
|
@@ -175,6 +246,81 @@ export async function adminCommand(options) {
|
|
|
175
246
|
expectedContent: operation.body.content,
|
|
176
247
|
});
|
|
177
248
|
}
|
|
249
|
+
if (operation.name === "news.claim") {
|
|
250
|
+
const artifacts = writeNewsClaimArtifacts({
|
|
251
|
+
result,
|
|
252
|
+
outputPath: options.adminOutput,
|
|
253
|
+
scaffoldPath: options.adminScaffoldFile,
|
|
254
|
+
});
|
|
255
|
+
result = {
|
|
256
|
+
...result,
|
|
257
|
+
data: { ...(result.data || {}), artifacts },
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return finishAdminOutput({ options, operation, result });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function transformAdminResult({
|
|
264
|
+
operation,
|
|
265
|
+
result,
|
|
266
|
+
options,
|
|
267
|
+
recommendationContentTypes,
|
|
268
|
+
viewFilter,
|
|
269
|
+
}) {
|
|
270
|
+
let transformed = result;
|
|
271
|
+
if (operation.name === "recommendations.list") {
|
|
272
|
+
assertRecommendationWindowResult({ operation, result: transformed });
|
|
273
|
+
}
|
|
274
|
+
if (operation.name === "builds.candidates") {
|
|
275
|
+
transformed = normalizeAdminBuildCandidatesResult({
|
|
276
|
+
result: transformed,
|
|
277
|
+
siteUrl: options.siteUrl,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (recommendationContentTypes) {
|
|
281
|
+
transformed = filterRecommendationQueueResult({
|
|
282
|
+
result: transformed,
|
|
283
|
+
contentTypes: recommendationContentTypes,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
if (viewFilter) {
|
|
287
|
+
transformed = filterListResultByOperatorView({
|
|
288
|
+
result: transformed,
|
|
289
|
+
viewFilter,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return transformed;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function assertRecommendationWindowResult({ operation, result }) {
|
|
296
|
+
const mode = operation?.pagination?.coverageMode;
|
|
297
|
+
if (!mode || mode === "legacy") return;
|
|
298
|
+
const after = result?.data?.pagination?.after;
|
|
299
|
+
const snapshotTimeStamp = result?.data?.pagination?.snapshotTimeStamp;
|
|
300
|
+
if (
|
|
301
|
+
!Number.isSafeInteger(after) ||
|
|
302
|
+
after < 0 ||
|
|
303
|
+
!Number.isSafeInteger(snapshotTimeStamp) ||
|
|
304
|
+
snapshotTimeStamp < 0
|
|
305
|
+
) {
|
|
306
|
+
const error = new Error(
|
|
307
|
+
"The deployed API did not confirm the bounded recommendation window and snapshot. Deploy the matching API before using this CLI; use --include-legacy only for an intentional historical scan.",
|
|
308
|
+
);
|
|
309
|
+
error.code = "LUMINE_ADMIN_RECOMMENDATION_WINDOW_UNSUPPORTED";
|
|
310
|
+
error.data = {
|
|
311
|
+
ok: false,
|
|
312
|
+
status: "validation_error",
|
|
313
|
+
error: {
|
|
314
|
+
code: error.code,
|
|
315
|
+
message: error.message,
|
|
316
|
+
details: { requestedMode: mode },
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function finishAdminOutput({ options, operation, result }) {
|
|
178
324
|
if (options.json) {
|
|
179
325
|
console.log(JSON.stringify(result));
|
|
180
326
|
return result;
|
|
@@ -354,8 +500,12 @@ function adminOperationRequiresRun(operation) {
|
|
|
354
500
|
"identity.list",
|
|
355
501
|
"identity.status",
|
|
356
502
|
"identity.use",
|
|
503
|
+
"identity.inspect",
|
|
357
504
|
"daily-run.start",
|
|
358
505
|
"daily-run.status",
|
|
506
|
+
"escalation.list",
|
|
507
|
+
"escalation.set",
|
|
508
|
+
"notable.add",
|
|
359
509
|
].includes(operation.name) && !operation.name.startsWith("ai-bucket.")
|
|
360
510
|
);
|
|
361
511
|
}
|
|
@@ -396,6 +546,35 @@ export function parseAdminOperation(options) {
|
|
|
396
546
|
{ identity: parseIdentity(target) },
|
|
397
547
|
);
|
|
398
548
|
}
|
|
549
|
+
if (action === "inspect") {
|
|
550
|
+
const inspectionTarget = String(target || "").trim();
|
|
551
|
+
const reason = String(options.adminReason || "").trim();
|
|
552
|
+
if (!inspectionTarget) {
|
|
553
|
+
throw cliValidationError(
|
|
554
|
+
"Usage: lumine admin identity inspect <userId|username> --reason <management reason> [--include-private-evidence].",
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
if (!reason) {
|
|
558
|
+
throw cliValidationError(
|
|
559
|
+
"Explain why private identity evidence is needed with --reason <management reason>.",
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
if (reason.length > MAX_IDENTITY_INSPECTION_REASON_LENGTH) {
|
|
563
|
+
throw cliValidationError(
|
|
564
|
+
`An identity-inspection reason must be at most ${MAX_IDENTITY_INSPECTION_REASON_LENGTH} characters.`,
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
return writeOperation(
|
|
568
|
+
"identity.inspect",
|
|
569
|
+
"POST",
|
|
570
|
+
"/cli/admin/identity/inspect",
|
|
571
|
+
{
|
|
572
|
+
target: inspectionTarget,
|
|
573
|
+
reason,
|
|
574
|
+
includePrivateEvidence: options.adminIncludePrivateEvidence === true,
|
|
575
|
+
},
|
|
576
|
+
);
|
|
577
|
+
}
|
|
399
578
|
}
|
|
400
579
|
|
|
401
580
|
if (namespace === "ai-bucket" || namespace === "ai-buckets") {
|
|
@@ -460,6 +639,31 @@ export function parseAdminOperation(options) {
|
|
|
460
639
|
if (action === "status") {
|
|
461
640
|
return readOperation("daily-run.status", "/cli/admin/daily-runs/status");
|
|
462
641
|
}
|
|
642
|
+
if (action === "report") {
|
|
643
|
+
return readOperation("daily-run.report", "/cli/admin/daily-runs/report");
|
|
644
|
+
}
|
|
645
|
+
if (action === "escalation" && target === "add") {
|
|
646
|
+
const rawTarget = String(options.adminTarget || "").trim();
|
|
647
|
+
const summary = String(options.note || "").trim();
|
|
648
|
+
if (!rawTarget || !summary) {
|
|
649
|
+
throw cliValidationError(
|
|
650
|
+
"Pass a concrete --target and --note when recording a run escalation.",
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
const parsedTarget = parseAdminEscalationTarget(rawTarget);
|
|
654
|
+
return writeOperation(
|
|
655
|
+
"daily-run.escalation.add",
|
|
656
|
+
"POST",
|
|
657
|
+
"/cli/admin/daily-runs/escalations",
|
|
658
|
+
{
|
|
659
|
+
targetType: parsedTarget.targetType || undefined,
|
|
660
|
+
targetId: parsedTarget.targetId || undefined,
|
|
661
|
+
url: parsedTarget.url || undefined,
|
|
662
|
+
summary,
|
|
663
|
+
severity: options.adminSeverity || "attention",
|
|
664
|
+
},
|
|
665
|
+
);
|
|
666
|
+
}
|
|
463
667
|
if (action === "complete" || action === "fail") {
|
|
464
668
|
return writeOperation(
|
|
465
669
|
`daily-run.${action}`,
|
|
@@ -470,6 +674,47 @@ export function parseAdminOperation(options) {
|
|
|
470
674
|
}
|
|
471
675
|
}
|
|
472
676
|
|
|
677
|
+
if (namespace === "escalation" || namespace === "escalations") {
|
|
678
|
+
if (!action || action === "list") {
|
|
679
|
+
const status = parseEscalationListStatus(options.adminStatus || "open");
|
|
680
|
+
return readOperation(
|
|
681
|
+
"escalation.list",
|
|
682
|
+
withQuery("/cli/admin/escalations", {
|
|
683
|
+
status,
|
|
684
|
+
limit: options.limit,
|
|
685
|
+
}),
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
if (action === "set") {
|
|
689
|
+
const escalationAuditId = parseRequiredInteger(
|
|
690
|
+
target,
|
|
691
|
+
"Escalation audit ID",
|
|
692
|
+
1,
|
|
693
|
+
);
|
|
694
|
+
const status = parseEscalationStatus(options.adminStatus);
|
|
695
|
+
const note = String(options.note || "").trim();
|
|
696
|
+
if (!note) {
|
|
697
|
+
throw cliValidationError(
|
|
698
|
+
"Record the decision or next step with --note <text>.",
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
if (note.length > MAX_ESCALATION_DECISION_NOTE_LENGTH) {
|
|
702
|
+
throw cliValidationError(
|
|
703
|
+
`An escalation decision note must be at most ${MAX_ESCALATION_DECISION_NOTE_LENGTH} characters.`,
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
return writeOperation(
|
|
707
|
+
"escalation.set",
|
|
708
|
+
"PUT",
|
|
709
|
+
`/cli/admin/escalations/${escalationAuditId}`,
|
|
710
|
+
{ status, note },
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
throw cliValidationError(
|
|
714
|
+
"Usage: lumine admin escalation list [--status open|acknowledged|resolved|all] | escalation set <auditId> --status <status> --note <decision>.",
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
473
718
|
if (
|
|
474
719
|
(namespace === "recommend-queue" && (!action || action === "list")) ||
|
|
475
720
|
(namespace === "recommendations" && action === "list")
|
|
@@ -478,6 +723,7 @@ export function parseAdminOperation(options) {
|
|
|
478
723
|
if (kind !== "recommend") {
|
|
479
724
|
throw cliValidationError("--kind currently supports only recommend.");
|
|
480
725
|
}
|
|
726
|
+
const recommendationWindow = parseRecommendationWindow(options);
|
|
481
727
|
return readOperation(
|
|
482
728
|
"recommendations.list",
|
|
483
729
|
withQuery("/cli/admin/recommendations", {
|
|
@@ -485,7 +731,33 @@ export function parseAdminOperation(options) {
|
|
|
485
731
|
contentTypes: options.adminContentTypes,
|
|
486
732
|
cursor: options.adminCursor,
|
|
487
733
|
limit: options.limit,
|
|
734
|
+
sinceRun: recommendationWindow.mode === "since-run" ? "true" : "",
|
|
735
|
+
after:
|
|
736
|
+
recommendationWindow.mode === "after"
|
|
737
|
+
? recommendationWindow.after
|
|
738
|
+
: "",
|
|
739
|
+
includeLegacy:
|
|
740
|
+
recommendationWindow.mode === "legacy" ? "true" : "",
|
|
488
741
|
}),
|
|
742
|
+
{
|
|
743
|
+
pagination: {
|
|
744
|
+
collectionKey: "items",
|
|
745
|
+
coverageQueue: "recommendations",
|
|
746
|
+
coverageMode: recommendationWindow.mode,
|
|
747
|
+
after:
|
|
748
|
+
recommendationWindow.mode === "after"
|
|
749
|
+
? parseAfterForCoverage(recommendationWindow.after)
|
|
750
|
+
: null,
|
|
751
|
+
filters: {
|
|
752
|
+
contentTypes: options.adminContentTypes || null,
|
|
753
|
+
operatorView: options.adminUnviewed
|
|
754
|
+
? "unviewed"
|
|
755
|
+
: options.adminViewed
|
|
756
|
+
? "viewed"
|
|
757
|
+
: null,
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
},
|
|
489
761
|
);
|
|
490
762
|
}
|
|
491
763
|
|
|
@@ -498,8 +770,33 @@ export function parseAdminOperation(options) {
|
|
|
498
770
|
cursor: options.adminCursor,
|
|
499
771
|
limit: options.limit,
|
|
500
772
|
}),
|
|
773
|
+
{
|
|
774
|
+
pagination: {
|
|
775
|
+
collectionKey: "builds",
|
|
776
|
+
coverageQueue: "builds",
|
|
777
|
+
coverageMode: "all",
|
|
778
|
+
after: null,
|
|
779
|
+
filters: { sort: "recent", scope: "all" },
|
|
780
|
+
},
|
|
781
|
+
},
|
|
501
782
|
);
|
|
502
783
|
}
|
|
784
|
+
if (namespace === "builds" && action === "review") {
|
|
785
|
+
const parsed = parseAdminCommentTarget({ target, explicitType: "build" });
|
|
786
|
+
if (parsed.type !== "build") {
|
|
787
|
+
throw cliValidationError(
|
|
788
|
+
"Build review targets a Build URL or build:<id>.",
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
name: "build.review",
|
|
793
|
+
method: "GET",
|
|
794
|
+
path: "",
|
|
795
|
+
body: undefined,
|
|
796
|
+
mutates: false,
|
|
797
|
+
buildId: parsed.id,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
503
800
|
|
|
504
801
|
if (
|
|
505
802
|
namespace === "subjects" &&
|
|
@@ -524,6 +821,7 @@ export function parseAdminOperation(options) {
|
|
|
524
821
|
cursor: options.adminCursor,
|
|
525
822
|
limit: options.limit,
|
|
526
823
|
}),
|
|
824
|
+
{ pagination: { collectionKey: "comments" } },
|
|
527
825
|
);
|
|
528
826
|
}
|
|
529
827
|
if (action === "reveal") {
|
|
@@ -591,6 +889,7 @@ export function parseAdminOperation(options) {
|
|
|
591
889
|
cursor: options.adminCursor,
|
|
592
890
|
limit: options.limit,
|
|
593
891
|
}),
|
|
892
|
+
{ pagination: { collectionKey: "comments" } },
|
|
594
893
|
);
|
|
595
894
|
}
|
|
596
895
|
if (action === "recommend") {
|
|
@@ -613,6 +912,20 @@ export function parseAdminOperation(options) {
|
|
|
613
912
|
{ reason: options.adminReason || undefined },
|
|
614
913
|
);
|
|
615
914
|
}
|
|
915
|
+
if (action === "skip-batch") {
|
|
916
|
+
if (!options.adminTargetFile) {
|
|
917
|
+
throw cliValidationError(
|
|
918
|
+
"Pass batch skip targets with --target-file <file>.",
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
name: "post.skip-batch",
|
|
923
|
+
method: "POST",
|
|
924
|
+
path: "",
|
|
925
|
+
body: undefined,
|
|
926
|
+
mutates: true,
|
|
927
|
+
};
|
|
928
|
+
}
|
|
616
929
|
if (action === "reward") {
|
|
617
930
|
const parsedTarget = parseRecommendationTarget({
|
|
618
931
|
target,
|
|
@@ -653,22 +966,41 @@ export function parseAdminOperation(options) {
|
|
|
653
966
|
...(repairDate ? { date: repairDate } : {}),
|
|
654
967
|
});
|
|
655
968
|
}
|
|
656
|
-
if (action === "
|
|
657
|
-
const
|
|
658
|
-
options.
|
|
659
|
-
"--edition-id",
|
|
660
|
-
1,
|
|
969
|
+
if (action === "validate") {
|
|
970
|
+
const claim = extractNewsClaim(
|
|
971
|
+
readAdminJsonFile(options.adminClaimFile, "--claim <claim.json>"),
|
|
661
972
|
);
|
|
662
|
-
|
|
973
|
+
return {
|
|
974
|
+
name: "news.validate",
|
|
975
|
+
local: true,
|
|
976
|
+
mutates: false,
|
|
977
|
+
claim,
|
|
978
|
+
editorial: readEditorialFile(options.adminFile),
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
if (action === "submit") {
|
|
982
|
+
const claim = options.adminClaimFile
|
|
983
|
+
? extractNewsClaim(
|
|
984
|
+
readAdminJsonFile(options.adminClaimFile, "--claim <claim.json>"),
|
|
985
|
+
)
|
|
986
|
+
: null;
|
|
987
|
+
const editionId = claim
|
|
988
|
+
? claim.editionId
|
|
989
|
+
: parseRequiredInteger(options.adminEditionId, "--edition-id", 1);
|
|
990
|
+
const leaseToken = claim
|
|
991
|
+
? claim.leaseToken
|
|
992
|
+
: String(options.adminLeaseToken || "").trim();
|
|
663
993
|
if (!leaseToken) {
|
|
664
994
|
throw cliValidationError(
|
|
665
995
|
"Pass the claim's lease token with --lease-token <token>.",
|
|
666
996
|
);
|
|
667
997
|
}
|
|
998
|
+
const editorial = readEditorialFile(options.adminFile);
|
|
999
|
+
if (claim) validateNewsEditorial({ claim, editorial });
|
|
668
1000
|
return writeOperation("news.submit", "POST", "/cli/admin/news/submit", {
|
|
669
1001
|
editionId,
|
|
670
1002
|
leaseToken,
|
|
671
|
-
editorial
|
|
1003
|
+
editorial,
|
|
672
1004
|
model: options.model || undefined,
|
|
673
1005
|
});
|
|
674
1006
|
}
|
|
@@ -776,6 +1108,7 @@ export function parseAdminOperation(options) {
|
|
|
776
1108
|
limit: options.limit,
|
|
777
1109
|
full: options.adminFull ? "true" : "",
|
|
778
1110
|
}),
|
|
1111
|
+
{ pagination: { collectionKey: "events" } },
|
|
779
1112
|
);
|
|
780
1113
|
}
|
|
781
1114
|
|
|
@@ -800,20 +1133,46 @@ export function parseAdminOperation(options) {
|
|
|
800
1133
|
const buildReviewMethod = options.adminBuildReviewMethod
|
|
801
1134
|
? parseAdminBuildReviewMethod(options.adminBuildReviewMethod)
|
|
802
1135
|
: undefined;
|
|
1136
|
+
const reviewReceipt = options.adminReviewReceipt
|
|
1137
|
+
? parseBuildReviewReceipt(options.adminReviewReceipt)
|
|
1138
|
+
: null;
|
|
1139
|
+
if (
|
|
1140
|
+
reviewReceipt &&
|
|
1141
|
+
(reviewedBuildVersionId || buildReviewMethod)
|
|
1142
|
+
) {
|
|
1143
|
+
throw cliValidationError(
|
|
1144
|
+
"Pass either --review-receipt or manual --reviewed-version/--reviewed-via evidence, not both.",
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
const confirmedBuildVersionId = reviewReceipt
|
|
1148
|
+
? Number(reviewReceipt.publishedArtifactVersionId)
|
|
1149
|
+
: reviewedBuildVersionId;
|
|
1150
|
+
const confirmedBuildReviewMethod = reviewReceipt
|
|
1151
|
+
? "runtime"
|
|
1152
|
+
: buildReviewMethod;
|
|
1153
|
+
if (
|
|
1154
|
+
reviewReceipt &&
|
|
1155
|
+
parsedTarget.type === "build" &&
|
|
1156
|
+
Number(reviewReceipt.buildId) !== parsedTarget.id
|
|
1157
|
+
) {
|
|
1158
|
+
throw cliValidationError(
|
|
1159
|
+
"The review receipt belongs to a different Build.",
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
803
1162
|
if (parsedTarget.type === "build") {
|
|
804
1163
|
if (!options.adminFile) {
|
|
805
1164
|
throw cliValidationError(
|
|
806
1165
|
"Build comments are management-agent composed only; pass --file <comment.md> after reviewing the project.",
|
|
807
1166
|
);
|
|
808
1167
|
}
|
|
809
|
-
if (!
|
|
1168
|
+
if (!confirmedBuildVersionId || !confirmedBuildReviewMethod) {
|
|
810
1169
|
throw cliValidationError(
|
|
811
1170
|
"After reviewing the project, pass both --reviewed-version <published-artifact-id> and --reviewed-via runtime|code.",
|
|
812
1171
|
);
|
|
813
1172
|
}
|
|
814
1173
|
} else if (
|
|
815
1174
|
parsedTarget.type !== "comment" &&
|
|
816
|
-
(
|
|
1175
|
+
(confirmedBuildVersionId || confirmedBuildReviewMethod)
|
|
817
1176
|
) {
|
|
818
1177
|
throw cliValidationError(
|
|
819
1178
|
"Build review evidence applies only to build:<id> or a comment:<id> inside a Build.",
|
|
@@ -832,8 +1191,12 @@ export function parseAdminOperation(options) {
|
|
|
832
1191
|
...(options.adminFile
|
|
833
1192
|
? { content: readComposedTextFile(options.adminFile) }
|
|
834
1193
|
: {}),
|
|
835
|
-
...(
|
|
836
|
-
|
|
1194
|
+
...(confirmedBuildVersionId
|
|
1195
|
+
? { reviewedBuildVersionId: confirmedBuildVersionId }
|
|
1196
|
+
: {}),
|
|
1197
|
+
...(confirmedBuildReviewMethod
|
|
1198
|
+
? { buildReviewMethod: confirmedBuildReviewMethod }
|
|
1199
|
+
: {}),
|
|
837
1200
|
},
|
|
838
1201
|
);
|
|
839
1202
|
}
|
|
@@ -877,7 +1240,7 @@ export function parseAdminOperation(options) {
|
|
|
877
1240
|
}
|
|
878
1241
|
|
|
879
1242
|
throw cliValidationError(
|
|
880
|
-
"Usage: lumine admin identity|daily-run|recommendations|builds|post|subjects|subject|featured|comment|announcement|chat|news|audit|brief|bot-output|notable ...",
|
|
1243
|
+
"Usage: lumine admin identity|daily-run|escalation|recommendations|builds|post|subjects|subject|featured|comment|announcement|chat|news|audit|brief|bot-output|notable ...",
|
|
881
1244
|
);
|
|
882
1245
|
}
|
|
883
1246
|
|
|
@@ -890,6 +1253,17 @@ function subjectsListOperation(options) {
|
|
|
890
1253
|
effort: options.adminEffort,
|
|
891
1254
|
limit: options.limit,
|
|
892
1255
|
}),
|
|
1256
|
+
{
|
|
1257
|
+
pagination: {
|
|
1258
|
+
collectionKey: "subjects",
|
|
1259
|
+
coverageQueue: "subjects",
|
|
1260
|
+
coverageMode: options.adminAfter ? "after" : "all",
|
|
1261
|
+
after: options.adminAfter
|
|
1262
|
+
? parseAfterForCoverage(options.adminAfter)
|
|
1263
|
+
: null,
|
|
1264
|
+
filters: { effort: options.adminEffort || "all" },
|
|
1265
|
+
},
|
|
1266
|
+
},
|
|
893
1267
|
);
|
|
894
1268
|
}
|
|
895
1269
|
|
|
@@ -997,6 +1371,35 @@ function recommendOperation(target, options) {
|
|
|
997
1371
|
);
|
|
998
1372
|
}
|
|
999
1373
|
|
|
1374
|
+
export function parseRecommendationWindow(options) {
|
|
1375
|
+
const selected = [
|
|
1376
|
+
options.adminSinceRun ? "since-run" : "",
|
|
1377
|
+
options.adminAfter ? "after" : "",
|
|
1378
|
+
options.adminIncludeLegacy ? "legacy" : "",
|
|
1379
|
+
].filter(Boolean);
|
|
1380
|
+
if (selected.length > 1) {
|
|
1381
|
+
throw cliValidationError(
|
|
1382
|
+
"Choose one recommendation window: --since-run, --after, or --include-legacy.",
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
const mode = selected[0] || "since-run";
|
|
1386
|
+
if (mode === "after") parseAfterForCoverage(options.adminAfter);
|
|
1387
|
+
return { mode, after: mode === "after" ? options.adminAfter : "" };
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function parseAfterForCoverage(value) {
|
|
1391
|
+
const raw = String(value || "").trim();
|
|
1392
|
+
if (/^\d+$/.test(raw)) {
|
|
1393
|
+
const parsed = Number(raw);
|
|
1394
|
+
if (Number.isSafeInteger(parsed) && parsed >= 0) return parsed;
|
|
1395
|
+
}
|
|
1396
|
+
const parsedMs = Date.parse(raw);
|
|
1397
|
+
if (Number.isFinite(parsedMs)) return Math.floor(parsedMs / 1000);
|
|
1398
|
+
throw cliValidationError(
|
|
1399
|
+
"--after must be a Unix timestamp or ISO-8601 date.",
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1000
1403
|
export function parseSubjectId(value) {
|
|
1001
1404
|
const normalized = String(value || "").trim();
|
|
1002
1405
|
const match = normalized.match(/(?:^subject:|\/subjects\/)(\d+)(?:[/?#]|$)/i);
|
|
@@ -1090,6 +1493,42 @@ export function parseAdminCommentTarget({ target, explicitType = "" }) {
|
|
|
1090
1493
|
return baseTarget;
|
|
1091
1494
|
}
|
|
1092
1495
|
|
|
1496
|
+
export function parseAdminEscalationTarget(value) {
|
|
1497
|
+
const normalized = String(value || "").trim();
|
|
1498
|
+
const genericTarget = normalized.match(
|
|
1499
|
+
/^([a-zA-Z][a-zA-Z0-9_-]{0,39}):(\d+)$/,
|
|
1500
|
+
);
|
|
1501
|
+
if (genericTarget) {
|
|
1502
|
+
return {
|
|
1503
|
+
targetType: genericTarget[1],
|
|
1504
|
+
targetId: parseRequiredInteger(genericTarget[2], "escalation target ID", 1),
|
|
1505
|
+
url: null,
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
if (/^https:\/\//i.test(normalized)) {
|
|
1509
|
+
let parsedUrl;
|
|
1510
|
+
try {
|
|
1511
|
+
parsedUrl = new URL(normalized);
|
|
1512
|
+
} catch {
|
|
1513
|
+
throw cliValidationError("The escalation target URL is invalid.");
|
|
1514
|
+
}
|
|
1515
|
+
if (parsedUrl.protocol !== "https:") {
|
|
1516
|
+
throw cliValidationError("The escalation target URL must use HTTPS.");
|
|
1517
|
+
}
|
|
1518
|
+
if (
|
|
1519
|
+
/\/(?:subjects|comments|ai-stories|daily-reflections|app|build)\/\d+(?:[/?#]|$)/i.test(
|
|
1520
|
+
normalized,
|
|
1521
|
+
)
|
|
1522
|
+
) {
|
|
1523
|
+
const parsed = parseAdminCommentTarget({ target: normalized });
|
|
1524
|
+
return { targetType: parsed.type, targetId: parsed.id, url: normalized };
|
|
1525
|
+
}
|
|
1526
|
+
return { targetType: null, targetId: null, url: normalized };
|
|
1527
|
+
}
|
|
1528
|
+
const parsed = parseAdminCommentTarget({ target: normalized });
|
|
1529
|
+
return { targetType: parsed.type, targetId: parsed.id, url: null };
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1093
1532
|
function parseAdminBuildReviewMethod(value) {
|
|
1094
1533
|
const normalized = String(value || "")
|
|
1095
1534
|
.trim()
|
|
@@ -1134,8 +1573,15 @@ export function formatAdminJsonError(error) {
|
|
|
1134
1573
|
};
|
|
1135
1574
|
}
|
|
1136
1575
|
|
|
1137
|
-
function readOperation(name, path) {
|
|
1138
|
-
return {
|
|
1576
|
+
function readOperation(name, path, extra = {}) {
|
|
1577
|
+
return {
|
|
1578
|
+
name,
|
|
1579
|
+
method: "GET",
|
|
1580
|
+
path,
|
|
1581
|
+
body: undefined,
|
|
1582
|
+
mutates: false,
|
|
1583
|
+
...extra,
|
|
1584
|
+
};
|
|
1139
1585
|
}
|
|
1140
1586
|
|
|
1141
1587
|
function writeOperation(name, method, path, body) {
|
|
@@ -1173,6 +1619,30 @@ function parseCommentMode(value) {
|
|
|
1173
1619
|
return mode;
|
|
1174
1620
|
}
|
|
1175
1621
|
|
|
1622
|
+
function parseEscalationStatus(value) {
|
|
1623
|
+
const status = String(value || "")
|
|
1624
|
+
.trim()
|
|
1625
|
+
.toLowerCase();
|
|
1626
|
+
if (!["open", "acknowledged", "resolved"].includes(status)) {
|
|
1627
|
+
throw cliValidationError(
|
|
1628
|
+
"--status must be open, acknowledged, or resolved.",
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
return status;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
function parseEscalationListStatus(value) {
|
|
1635
|
+
const status = String(value || "open")
|
|
1636
|
+
.trim()
|
|
1637
|
+
.toLowerCase();
|
|
1638
|
+
if (!["open", "acknowledged", "resolved", "all"].includes(status)) {
|
|
1639
|
+
throw cliValidationError(
|
|
1640
|
+
"--status must be open, acknowledged, resolved, or all.",
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
return status;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1176
1646
|
function parseOrderedIds(value) {
|
|
1177
1647
|
const ids = String(value || "")
|
|
1178
1648
|
.split(",")
|
|
@@ -1276,8 +1746,121 @@ function cliValidationError(message) {
|
|
|
1276
1746
|
return error;
|
|
1277
1747
|
}
|
|
1278
1748
|
|
|
1749
|
+
function adminValueFingerprint(value) {
|
|
1750
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1279
1753
|
function printAdminResult({ operation, result }) {
|
|
1280
1754
|
const data = result?.data || {};
|
|
1755
|
+
if (data.review) {
|
|
1756
|
+
console.log(
|
|
1757
|
+
`Confirmed Build #${data.review.buildId} runtime at published artifact #${data.review.publishedArtifactVersionId}.`,
|
|
1758
|
+
);
|
|
1759
|
+
console.log(`Screenshot: ${data.screenshotPath}`);
|
|
1760
|
+
console.log(`Review receipt: ${data.receiptPath}`);
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
if (data.validation) {
|
|
1764
|
+
console.log(
|
|
1765
|
+
`Editorial valid for edition #${data.validation.editionId}: ${data.validation.citedEventCount} cited and ${data.validation.coveredEventCount} covered event(s).`,
|
|
1766
|
+
);
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
if (data.batch) {
|
|
1770
|
+
console.log(
|
|
1771
|
+
`Skipped ${data.batch.completedCount} audited target(s); ${data.batch.changedCount} changed canonical state.`,
|
|
1772
|
+
);
|
|
1773
|
+
console.log(`Checkpoint: ${data.batch.checkpointPath}`);
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
if (data.report) {
|
|
1777
|
+
const report = data.report;
|
|
1778
|
+
console.log(
|
|
1779
|
+
`Run #${report.run.id}: ${report.mutations.successfulMutationCount} successful mutation(s), ${report.queueCoverage.length} queue coverage record(s), ${report.escalations.length} escalation(s).`,
|
|
1780
|
+
);
|
|
1781
|
+
for (const coverage of report.queueCoverage) {
|
|
1782
|
+
console.log(
|
|
1783
|
+
` ${coverage.queue}: ${coverage.candidateCount} candidate(s), ${coverage.scannedCount} row(s) scanned across ${coverage.pages} page(s).`,
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
for (const escalation of report.escalations) {
|
|
1787
|
+
const target =
|
|
1788
|
+
escalation.url ||
|
|
1789
|
+
`${escalation.targetType || "target"}:${escalation.targetId || "?"}`;
|
|
1790
|
+
console.log(
|
|
1791
|
+
` ${String(escalation.severity || "attention").toUpperCase()} ${target} — ${escalation.summary}`,
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1794
|
+
const surfaces = report.brief?.engagementPulse?.surfaces;
|
|
1795
|
+
if (surfaces && typeof surfaces === "object") {
|
|
1796
|
+
const deltas = Object.entries(surfaces)
|
|
1797
|
+
.filter(([, value]) => Number(value?.delta || 0) !== 0)
|
|
1798
|
+
.sort(
|
|
1799
|
+
([, left], [, right]) =>
|
|
1800
|
+
Math.abs(Number(right?.delta || 0)) -
|
|
1801
|
+
Math.abs(Number(left?.delta || 0)),
|
|
1802
|
+
)
|
|
1803
|
+
.slice(0, 5)
|
|
1804
|
+
.map(
|
|
1805
|
+
([name, value]) =>
|
|
1806
|
+
`${name} ${Number(value.delta) > 0 ? "+" : ""}${Number(value.delta)}`,
|
|
1807
|
+
);
|
|
1808
|
+
if (deltas.length) console.log(`Engagement deltas: ${deltas.join(", ")}.`);
|
|
1809
|
+
}
|
|
1810
|
+
const notableCount = Array.isArray(report.brief?.notableCandidates)
|
|
1811
|
+
? report.brief.notableCandidates.length
|
|
1812
|
+
: 0;
|
|
1813
|
+
console.log(`Notable-user candidates in this brief: ${notableCount}.`);
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
if (data.inspection) {
|
|
1817
|
+
const inspection = data.inspection;
|
|
1818
|
+
console.log(
|
|
1819
|
+
`Identity inspection for user #${inspection.targetUserId}: ${inspection.accounts?.length || 0} candidate account(s); oldest #${inspection.oldestAccount?.userId || "unknown"}.`,
|
|
1820
|
+
);
|
|
1821
|
+
if (inspection.manualBucket) {
|
|
1822
|
+
console.log(
|
|
1823
|
+
`AI bucket #${inspection.manualBucket.id} (${inspection.manualBucket.label}); ${inspection.manualBucket.memberCount} canonical member(s).`,
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
for (const account of inspection.accounts || []) {
|
|
1827
|
+
console.log(
|
|
1828
|
+
` #${account.userId} ${account.username || "(no username)"} — joined ${account.joinedAt || "unknown"}; ${account.relationBasis.join(", ") || "no relation evidence"}${account.hasDateOfBirth ? "; DOB on file" : "; no DOB on file"}.`,
|
|
1829
|
+
);
|
|
1830
|
+
if (account.privateEvidence) {
|
|
1831
|
+
console.log(
|
|
1832
|
+
` Private evidence: DOB ${account.privateEvidence.dateOfBirth || "none"}; verified email(s) ${account.privateEvidence.verifiedEmails.join(", ") || "none"}.`,
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
if (Array.isArray(data.escalations)) {
|
|
1839
|
+
console.log(`${data.escalations.length} escalation(s):`);
|
|
1840
|
+
for (const escalation of data.escalations) {
|
|
1841
|
+
const target =
|
|
1842
|
+
escalation.url ||
|
|
1843
|
+
`${escalation.targetType || "target"}:${escalation.targetId || "?"}`;
|
|
1844
|
+
console.log(
|
|
1845
|
+
` #${escalation.auditId} ${String(escalation.status || "open").toUpperCase()} ${target} — ${escalation.summary}`,
|
|
1846
|
+
);
|
|
1847
|
+
if (escalation.decisionNote) {
|
|
1848
|
+
console.log(` Decision: ${escalation.decisionNote}`);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
if (data.truncated) {
|
|
1852
|
+
console.log(
|
|
1853
|
+
"More matching escalations exist than the requested limit; raise --limit or narrow --status.",
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
if (data.escalation?.decisionNote) {
|
|
1859
|
+
console.log(
|
|
1860
|
+
`Escalation #${data.escalation.auditId}: ${String(data.escalation.status).toUpperCase()} — ${data.escalation.decisionNote}`,
|
|
1861
|
+
);
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1281
1864
|
if (data.bucket && Array.isArray(data.memberUserIds)) {
|
|
1282
1865
|
const added = Array.isArray(data.accounts)
|
|
1283
1866
|
? `; added ${data.accounts.length} explicit account(s)`
|
|
@@ -1347,12 +1930,28 @@ function printAdminResult({ operation, result }) {
|
|
|
1347
1930
|
return;
|
|
1348
1931
|
}
|
|
1349
1932
|
if (data.claim) {
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
`
|
|
1355
|
-
|
|
1933
|
+
if (data.artifacts?.claimFile) {
|
|
1934
|
+
console.log(
|
|
1935
|
+
`Claimed edition #${data.claim.editionId} (${data.claim.dateKey}): ${data.claim.events.length} event(s).`,
|
|
1936
|
+
);
|
|
1937
|
+
console.log(`Claim file: ${data.artifacts.claimFile}`);
|
|
1938
|
+
if (data.artifacts?.scaffoldFile) {
|
|
1939
|
+
console.log(`Editorial scaffold: ${data.artifacts.scaffoldFile}`);
|
|
1940
|
+
}
|
|
1941
|
+
console.log(
|
|
1942
|
+
`Validate before submission: lumine admin news validate --claim ${data.artifacts.claimFile} --file ${data.artifacts.scaffoldFile || "editorial.json"}`,
|
|
1943
|
+
);
|
|
1944
|
+
console.log(
|
|
1945
|
+
`Submit the confirmed pair: lumine admin news submit --claim ${data.artifacts.claimFile} --file ${data.artifacts.scaffoldFile || "editorial.json"}`,
|
|
1946
|
+
);
|
|
1947
|
+
} else {
|
|
1948
|
+
console.log(
|
|
1949
|
+
`Claimed edition #${data.claim.editionId} (${data.claim.dateKey}): ${data.claim.events.length} event(s); lease token ${data.claim.leaseToken}.`,
|
|
1950
|
+
);
|
|
1951
|
+
console.log(
|
|
1952
|
+
`Write the editorial JSON, then run: lumine admin news submit --edition-id ${data.claim.editionId} --lease-token ${data.claim.leaseToken} --file editorial.json`,
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1356
1955
|
return;
|
|
1357
1956
|
}
|
|
1358
1957
|
if (data.newspaper) {
|