@stage5/lumine 0.2.99 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/admin.js CHANGED
@@ -640,6 +640,8 @@ export async function adminCommand(options) {
640
640
  let runScope = null;
641
641
  let correctionSessionId = 0;
642
642
  let correctionSession = null;
643
+ let commentSessionId = 0;
644
+ let commentSession = null;
643
645
  if (adminOperationRequiresRun(operation)) {
644
646
  if (operation.correctionEligible) {
645
647
  const correctionStatus = await requestJson({
@@ -657,6 +659,15 @@ export async function adminCommand(options) {
657
659
  correctionSession = correction;
658
660
  }
659
661
  }
662
+ if (!correctionSessionId && adminOperationUsesCommentSession(operation)) {
663
+ commentSession = await loadActiveCommentSession({
664
+ options,
665
+ authToken: auth.token,
666
+ });
667
+ commentSessionId = Number(commentSession?.id || 0);
668
+ }
669
+ // Look up the daily run too: when both are active the caller must choose
670
+ // one (--via), and exactly one authority is sent.
660
671
  const runStatus = correctionSessionId
661
672
  ? null
662
673
  : await requestJson({
@@ -671,9 +682,39 @@ export async function adminCommand(options) {
671
682
  ].includes(operation.name)
672
683
  ? runStatus?.data?.lastRun || null
673
684
  : null;
674
- const selectedRun = activeRun || retryableFinishedRun;
685
+ let selectedRun = activeRun || retryableFinishedRun;
686
+ const via = parseAdminVia(options.adminVia, operation);
687
+ if (via && correctionSessionId) {
688
+ throw cliValidationError(
689
+ `Correction session #${correctionSessionId} is active for this comment and handles it; drop --via or complete the correction first.`,
690
+ );
691
+ }
692
+ if (commentSessionId && selectedRun) {
693
+ let choice = via;
694
+ if (!choice) {
695
+ if (COMMENT_SESSION_WRITE_OPERATIONS.has(operation.name)) {
696
+ throw cliValidationError(
697
+ `Both comment session #${commentSessionId} (${commentSession.identity?.key || "unknown"}) and daily run #${selectedRun.id} (${selectedRun.identity?.key || "unknown"}) are active. Choose which one this ${operation.name} uses with --via session or --via run.`,
698
+ );
699
+ }
700
+ // Reads create nothing; without --via they keep using the daily run.
701
+ choice = "run";
702
+ }
703
+ if (choice === "session") {
704
+ selectedRun = null;
705
+ } else {
706
+ commentSessionId = 0;
707
+ commentSession = null;
708
+ }
709
+ } else if (via === "session" && !commentSessionId) {
710
+ throw cliValidationError(
711
+ "--via session: no comment session is active. Start one with lumine admin comment session start.",
712
+ );
713
+ } else if (via === "run" && !selectedRun && !correctionSessionId) {
714
+ throw noActiveRunError();
715
+ }
675
716
  runId = Number(selectedRun?.id || 0);
676
- if (!runId && !correctionSessionId) {
717
+ if (!runId && !correctionSessionId && !commentSessionId) {
677
718
  if (operation.correctionEligible) {
678
719
  const error = new Error(
679
720
  `Start a correction session first: lumine admin correction start ${operation.correctionCommentId}.`,
@@ -687,7 +728,17 @@ export async function adminCommand(options) {
687
728
  runScope = canonicalAdminRunScope(selectedRun);
688
729
  assertAdminOperationAllowedForRunScope({ operation, runScope });
689
730
  }
690
- if (options.adminIdentity && (selectedRun || correctionSession)) {
731
+ if (options.adminIdentity && commentSession) {
732
+ const requestedIdentity = parseIdentity(options.adminIdentity);
733
+ if (
734
+ requestedIdentity !== "auto" &&
735
+ requestedIdentity !== commentSession.identity?.key
736
+ ) {
737
+ throw cliValidationError(
738
+ `--identity ${requestedIdentity} does not match comment session #${commentSessionId} (${commentSession.identity?.key || "unknown"}).`,
739
+ );
740
+ }
741
+ } else if (options.adminIdentity && (selectedRun || correctionSession)) {
691
742
  const requestedIdentity = parseIdentity(options.adminIdentity);
692
743
  const canonicalIdentity = correctionSession
693
744
  ? correctionSession.identity?.key
@@ -722,6 +773,11 @@ export async function adminCommand(options) {
722
773
  String(correctionSessionId),
723
774
  }
724
775
  : {}),
776
+ ...(commentSessionId
777
+ ? {
778
+ "x-lumine-admin-comment-session-id": String(commentSessionId),
779
+ }
780
+ : {}),
725
781
  ...(requestId ? { "x-lumine-idempotency-key": requestId } : {}),
726
782
  },
727
783
  // Long operations (reward-bank seed) carry their own timeout.
@@ -1529,6 +1585,81 @@ export function assertAdminOperationAllowedForRunScope({
1529
1585
  );
1530
1586
  }
1531
1587
 
1588
+ // Commands a standalone comment session can serve: drafting, replying,
1589
+ // publishing and editing its own comments, plus the reads needed to compose
1590
+ // on its targets. The API still enforces the session's target lock on each.
1591
+ const COMMENT_SESSION_WRITE_OPERATIONS = new Set([
1592
+ "comment.draft",
1593
+ "comment.post",
1594
+ "comment.edit",
1595
+ ]);
1596
+ const COMMENT_SESSION_OPERATIONS = new Set([
1597
+ ...COMMENT_SESSION_WRITE_OPERATIONS,
1598
+ "subject.get",
1599
+ "subject.comments",
1600
+ "post.get",
1601
+ "post.comments",
1602
+ ]);
1603
+
1604
+ export function adminOperationUsesCommentSession(operation) {
1605
+ return COMMENT_SESSION_OPERATIONS.has(operation?.name);
1606
+ }
1607
+
1608
+ export function parseAdminVia(value, operation) {
1609
+ const via = String(value || "").trim().toLowerCase();
1610
+ if (!via) return "";
1611
+ if (via !== "session" && via !== "run") {
1612
+ throw cliValidationError("--via must be session or run.");
1613
+ }
1614
+ if (!adminOperationUsesCommentSession(operation)) {
1615
+ throw cliValidationError(
1616
+ "--via applies only to comment draft/reply/post/edit and the subject/post/comment reads.",
1617
+ );
1618
+ }
1619
+ return via;
1620
+ }
1621
+
1622
+ async function loadActiveCommentSession({ options, authToken }) {
1623
+ let status;
1624
+ try {
1625
+ status = await requestJson({
1626
+ url: `${options.apiUrl}/cli/admin/comment-sessions/status`,
1627
+ authToken,
1628
+ timeoutMs: options.timeoutMs,
1629
+ });
1630
+ } catch (error) {
1631
+ // An API without comment sessions yet: keep daily runs working.
1632
+ if (error?.status === 404) return null;
1633
+ throw error;
1634
+ }
1635
+ const session = status?.data?.commentSession || null;
1636
+ return session?.status === "active" && Number(session.id) > 0
1637
+ ? session
1638
+ : null;
1639
+ }
1640
+
1641
+ export function parseCommentSessionTargets(values) {
1642
+ const raw = values
1643
+ .flatMap((value) => String(value || "").split(","))
1644
+ .map((value) => value.trim())
1645
+ .filter(Boolean);
1646
+ if (!raw.length) {
1647
+ throw cliValidationError(
1648
+ "Name the targets with --target <subject:ID|comment:ID|build:ID|aiStory:ID|dailyReflection:ID>[,...].",
1649
+ );
1650
+ }
1651
+ const seen = new Set();
1652
+ const targets = [];
1653
+ for (const value of raw) {
1654
+ const target = parseAdminCommentTarget({ target: value });
1655
+ const key = `${target.type}:${target.id}`;
1656
+ if (seen.has(key)) continue;
1657
+ seen.add(key);
1658
+ targets.push(target);
1659
+ }
1660
+ return targets;
1661
+ }
1662
+
1532
1663
  function adminOperationRequiresRun(operation) {
1533
1664
  if (typeof operation.requiresRun === "boolean") {
1534
1665
  return operation.requiresRun;
@@ -1547,6 +1678,9 @@ function adminOperationRequiresRun(operation) {
1547
1678
  "correction.start",
1548
1679
  "correction.status",
1549
1680
  "correction.complete",
1681
+ "comment-session.start",
1682
+ "comment-session.status",
1683
+ "comment-session.close",
1550
1684
  "escalation.list",
1551
1685
  "escalation.set",
1552
1686
  "notable.add",
@@ -3361,6 +3495,51 @@ export function parseAdminOperation(options) {
3361
3495
  }
3362
3496
 
3363
3497
  if (namespace === "comment") {
3498
+ if (action === "session") {
3499
+ const sessionAction = target || "status";
3500
+ if (sessionAction === "status") {
3501
+ return readOperation(
3502
+ "comment-session.status",
3503
+ "/cli/admin/comment-sessions/status",
3504
+ );
3505
+ }
3506
+ if (sessionAction === "start") {
3507
+ const identity = String(options.adminIdentity || "")
3508
+ .trim()
3509
+ .toLowerCase();
3510
+ if (identity !== "zero" && identity !== "ciel") {
3511
+ throw cliValidationError(
3512
+ "comment session start needs --identity zero|ciel.",
3513
+ );
3514
+ }
3515
+ const targets = parseCommentSessionTargets([
3516
+ options.adminTarget,
3517
+ ...(options.positional || []).slice(3),
3518
+ ]);
3519
+ const reason = String(options.adminReason || "").trim();
3520
+ return writeOperation(
3521
+ "comment-session.start",
3522
+ "POST",
3523
+ "/cli/admin/comment-sessions",
3524
+ {
3525
+ identity,
3526
+ targets,
3527
+ ...(reason ? { reason } : {}),
3528
+ },
3529
+ );
3530
+ }
3531
+ if (sessionAction === "close") {
3532
+ return writeOperation(
3533
+ "comment-session.close",
3534
+ "POST",
3535
+ "/cli/admin/comment-sessions/close",
3536
+ {},
3537
+ );
3538
+ }
3539
+ throw cliValidationError(
3540
+ "Usage: lumine admin comment session start --identity zero|ciel --target <type:id>[,<type:id>...] [--reason <text>] | comment session status | comment session close.",
3541
+ );
3542
+ }
3364
3543
  if (action === "draft" || action === "reply") {
3365
3544
  const parsedTarget = parseAdminCommentTarget({
3366
3545
  target,
package/lib/commands.js CHANGED
@@ -2597,6 +2597,7 @@ export function parseArgs(args) {
2597
2597
  adminUnviewed: Boolean(raw.unviewed),
2598
2598
  adminViewed: Boolean(raw.viewed),
2599
2599
  adminIdentity: raw.identity ? String(raw.identity) : "",
2600
+ adminVia: raw.via ? String(raw.via) : "",
2600
2601
  adminScope: raw.scope ? String(raw.scope) : "",
2601
2602
  commentMode: raw.commentMode ? String(raw.commentMode) : "",
2602
2603
  runKey: raw.runKey ? String(raw.runKey) : "",
@@ -3062,6 +3063,9 @@ export function printHelp() {
3062
3063
  lumine admin comment draft <target> [--type subject|comment|build|aiStory|dailyReflection] [--file <comment.md>] [--review-receipt <review.json>|--reviewed-version <id> --reviewed-via runtime|code] [--review-context <context.json>] [--identity zero|ciel|auto] [--json]
3063
3064
  lumine admin comment reply comment:<id> [--file <reply.md>] [--reviewed-version <id> --reviewed-via runtime|code] [--review-context <context.json>] [--identity zero|ciel|auto] [--json]
3064
3065
  lumine admin comment post --draft-id <id> [--json]
3066
+ lumine admin comment session start --identity zero|ciel --target <subject:ID|comment:ID|build:ID|aiStory:ID>[,...] [--reason <text>] [--json]
3067
+ lumine admin comment session status|close [--json]
3068
+ (with both a comment session and a daily run active, comment draft/reply/post/edit need --via session|run)
3065
3069
  lumine admin comment edit <comment-id> --file <comment.md> [--review-receipt <review.json> | --reviewed-version <id> --reviewed-via runtime|code] [--review-context <context.json>] [--json]
3066
3070
  lumine admin brief [--days <1..30>] [--json]
3067
3071
  lumine admin ai-costs monthly [--output <file.json>] [--json] (no active run required)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.99",
3
+ "version": "0.3.0",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4171,6 +4171,74 @@ canonical review record ID before claiming success. Older APIs that still block
4171
4171
  Build edits must be deployed first; do not silently substitute a duplicate reply
4172
4172
  when Mikey requested an edit.
4173
4173
 
4174
+ ### Standalone comment sessions
4175
+
4176
+ Posting a few new comments or replies as Zero or Ciel does not require a full
4177
+ daily run. An approved operator names the identity and the exact targets:
4178
+
4179
+ ```bash
4180
+ lumine admin comment session start --identity ciel \
4181
+ --target subject:123,comment:456 --reason "Mikey asked for a reply" --json
4182
+ lumine admin subject get 123 --include-comments --json
4183
+ lumine admin comment draft subject:123 --file comment.md --json
4184
+ lumine admin comment reply comment:456 --file reply.md --json
4185
+ lumine admin comment post --draft-id <id> --json
4186
+ lumine admin comment edit <commentId> --file corrected.md --json
4187
+ lumine admin comment session status --json
4188
+ lumine admin comment session close --json
4189
+ ```
4190
+
4191
+ `--target` takes a comma-separated list (extra space-separated targets after
4192
+ it also count); each is `subject:ID`, `comment:ID`, `build:ID`, `aiStory:ID` or
4193
+ `dailyReflection:ID`, at most ten. Pass them in one `--target`: a repeated
4194
+ `--target` flag keeps only its last value. The server checks every target when
4195
+ the session starts and refuses one the draft path would refuse later.
4196
+
4197
+ What the session authorizes:
4198
+
4199
+ - **Scopes:** only `content:read`, `comment:draft` and `comment:post`. No chat,
4200
+ Featured, reward, recommendation, subject, newspaper or run-completion
4201
+ command works through it; those still need a daily run.
4202
+ - **Target lock:** drafts, replies and the reads above work only on the named
4203
+ targets. A reply target counts when it is a named `comment:ID`, or a comment
4204
+ sitting directly in a named subject, Build, AI Story or Daily Reflection.
4205
+ Anything else fails with `CLI_ADMIN_COMMENT_SESSION_TARGET_FORBIDDEN` and
4206
+ the allowed list. `comment post` accepts only drafts created in the session.
4207
+ - **Edits:** `comment edit <commentId>` works under the session only for a
4208
+ comment this session itself posted, as the same identity
4209
+ (`CLI_ADMIN_COMMENT_SESSION_EDIT_FORBIDDEN` otherwise). The usual edit rules
4210
+ apply unchanged, including fresh Build review evidence for a Build comment.
4211
+ Edits do not count toward the post cap.
4212
+ - **Limits:** 30 minutes, at most five published comments
4213
+ (`CLI_ADMIN_COMMENT_SESSION_POST_CAP`), one active session per operator.
4214
+ `close` or the TTL ends it; start a new session for more.
4215
+ - **Same bar as a daily run:** drafting and publishing use the exact daily-run
4216
+ draft/publish path. Build comments still require composed text, a review
4217
+ receipt or `--reviewed-version/--reviewed-via` evidence of the current
4218
+ published version, and `--review-context`; context and persona revisions
4219
+ are checked at publish time as usual.
4220
+ - **Identity:** the operator must be an approved delegated administrator, and
4221
+ the session posts only as the identity named at start (`--identity auto` is
4222
+ not accepted). It does not change the Bangkok calendar assignment or the
4223
+ identity preference.
4224
+
4225
+ A comment session and a daily run can be active together, but every request
4226
+ carries exactly one authority. The session never claims the daily-run slot.
4227
+
4228
+ - When only one of them is active, the CLI uses it.
4229
+ - When both are active, `comment draft`, `comment reply`, `comment post` and
4230
+ `comment edit` refuse to run until you choose with `--via session` or
4231
+ `--via run`. Target reads (`subject get`, `post get`, `comments get`, their
4232
+ comment lists) default to the daily run and take `--via session` to read
4233
+ through the session instead.
4234
+ - The API refuses a request that names a comment session together with a
4235
+ daily run or correction session (`CLI_ADMIN_AMBIGUOUS_AUTHORITY`); a session
4236
+ request outside the session is refused, never handed to the run.
4237
+ - A draft records the authority that created it, and only that authority can
4238
+ publish it (`CLI_ADMIN_DRAFT_AUTHORITY_MISMATCH`).
4239
+ - A correction session still takes precedence for its one comment; `--via` is
4240
+ refused while it is active for that comment.
4241
+
4174
4242
  **Editing the bot's own comments.** `comment edit <commentId> --file
4175
4243
  <comment.md>` replaces the text of a comment the acting bot itself authored —
4176
4244
  for correcting a factual error, an unfulfillable claim, or outdated guidance