@skrr-ai/cli 0.1.104 → 0.1.106

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.
Files changed (111) hide show
  1. package/dist/base-command.js +6 -2
  2. package/dist/commands/agenda/trigger/list.js +2 -0
  3. package/dist/commands/agents/chat.js +4 -1
  4. package/dist/commands/agents/operating-plan.js +5 -2
  5. package/dist/commands/agents/triggers/create.js +3 -1
  6. package/dist/commands/agents/triggers/fire.js +2 -0
  7. package/dist/commands/agents/triggers/replay.js +2 -0
  8. package/dist/commands/agents/triggers/update.js +12 -4
  9. package/dist/commands/commitments/action-history.js +7 -2
  10. package/dist/commands/commitments/cadence-decisions.js +6 -1
  11. package/dist/commands/commitments/cadence-requests/decide.d.ts +1 -1
  12. package/dist/commands/commitments/cadence-requests/decide.js +1 -1
  13. package/dist/commands/commitments/doctor.js +7 -1
  14. package/dist/commands/commitments/observations.js +7 -1
  15. package/dist/commands/commitments/operating-review.d.ts +15 -0
  16. package/dist/commands/commitments/operating-review.js +23 -2
  17. package/dist/commands/commitments/preflight.d.ts +12 -0
  18. package/dist/commands/commitments/preflight.js +23 -9
  19. package/dist/commands/commitments/reflection.js +6 -1
  20. package/dist/commands/commitments/show.js +3 -1
  21. package/dist/commands/commitments/trigger-status.js +2 -22
  22. package/dist/commands/commitments/wake-capabilities.d.ts +13 -0
  23. package/dist/commands/commitments/wake-capabilities.js +39 -3
  24. package/dist/commands/daemon/logs.d.ts +73 -3
  25. package/dist/commands/daemon/logs.js +304 -17
  26. package/dist/commands/followups/act.js +13 -1
  27. package/dist/commands/followups/checkin.js +11 -2
  28. package/dist/commands/followups/reschedule.js +13 -1
  29. package/dist/commands/proposals/discard.d.ts +1 -1
  30. package/dist/commands/proposals/keep.d.ts +1 -1
  31. package/dist/commands/proposals/list.d.ts +7 -0
  32. package/dist/commands/proposals/list.js +31 -5
  33. package/dist/commands/proposals/show.d.ts +2 -1
  34. package/dist/commands/proposals/show.js +31 -4
  35. package/dist/commands/proposals/snooze.d.ts +1 -1
  36. package/dist/commands/requests/create.js +8 -3
  37. package/dist/commands/spaces/workflows/create.js +3 -3
  38. package/dist/commands/tasks/complete.js +4 -0
  39. package/dist/commands/tasks/create.d.ts +2 -0
  40. package/dist/commands/tasks/create.js +17 -3
  41. package/dist/commands/tasks/deliverable/add.d.ts +2 -0
  42. package/dist/commands/tasks/deliverable/add.js +22 -7
  43. package/dist/commands/tasks/deliverable/list.d.ts +13 -0
  44. package/dist/commands/tasks/deliverable/list.js +34 -1
  45. package/dist/commands/tasks/economics-compare.js +7 -1
  46. package/dist/commands/tasks/expectations.d.ts +10 -0
  47. package/dist/commands/tasks/expectations.js +31 -3
  48. package/dist/commands/tasks/list.d.ts +9 -0
  49. package/dist/commands/tasks/list.js +40 -5
  50. package/dist/commands/tasks/report.js +1 -1
  51. package/dist/commands/tasks/result/show.d.ts +7 -0
  52. package/dist/commands/tasks/result/show.js +32 -1
  53. package/dist/commands/tasks/result/submit.js +1 -1
  54. package/dist/commands/tasks/retry-schedule.js +11 -1
  55. package/dist/commands/tasks/show.d.ts +6 -0
  56. package/dist/commands/tasks/show.js +23 -0
  57. package/dist/commands/tasks/update.d.ts +3 -0
  58. package/dist/commands/tasks/update.js +23 -3
  59. package/dist/commands/triggers/audit.js +6 -2
  60. package/dist/commands/triggers/create.js +3 -1
  61. package/dist/commands/triggers/fire.d.ts +1 -0
  62. package/dist/commands/triggers/fire.js +16 -1
  63. package/dist/commands/triggers/preflight.js +6 -5
  64. package/dist/commands/triggers/replay.js +2 -0
  65. package/dist/commands/triggers/update.js +12 -4
  66. package/dist/commands/workspaces/stewardship.js +5 -1
  67. package/dist/lib/agentic-stream.d.ts +9 -0
  68. package/dist/lib/agentic-stream.js +38 -18
  69. package/dist/lib/commitment-event-wakes.d.ts +8 -0
  70. package/dist/lib/commitment-event-wakes.js +39 -0
  71. package/dist/lib/commitment-ledgers.d.ts +9 -0
  72. package/dist/lib/commitment-ledgers.js +126 -0
  73. package/dist/lib/commitments.d.ts +102 -2
  74. package/dist/lib/commitments.js +208 -8
  75. package/dist/lib/delegated-cli.js +13 -2
  76. package/dist/lib/followups.d.ts +6 -0
  77. package/dist/lib/followups.js +12 -0
  78. package/dist/lib/message-content.d.ts +6 -0
  79. package/dist/lib/message-content.js +24 -0
  80. package/dist/lib/operating-plan-loops.d.ts +8 -0
  81. package/dist/lib/operating-plan-loops.js +32 -0
  82. package/dist/lib/preflight-copy.d.ts +27 -0
  83. package/dist/lib/preflight-copy.js +89 -0
  84. package/dist/lib/proposal-decision-command.d.ts +1 -1
  85. package/dist/lib/proposal-decision-command.js +27 -3
  86. package/dist/lib/proposals.d.ts +17 -0
  87. package/dist/lib/proposals.js +41 -0
  88. package/dist/lib/stewardship-output.js +57 -12
  89. package/dist/lib/task-economics-line.d.ts +30 -0
  90. package/dist/lib/task-economics-line.js +49 -0
  91. package/dist/lib/task-economics.d.ts +15 -0
  92. package/dist/lib/task-economics.js +73 -8
  93. package/dist/lib/task-recurrence-outcome.d.ts +19 -0
  94. package/dist/lib/task-recurrence-outcome.js +30 -0
  95. package/dist/lib/triggers.d.ts +42 -0
  96. package/dist/lib/triggers.js +136 -2
  97. package/dist/lib/widgetTerminalText.d.ts +8 -0
  98. package/dist/lib/widgetTerminalText.js +116 -0
  99. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +33 -0
  100. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +27 -0
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.d.ts +28 -5
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.js +50 -5
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +33 -0
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +26 -0
  105. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.d.ts +28 -5
  106. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.js +50 -5
  107. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  108. package/dist/node_modules/@skrr-ai/data-provider/index.js +7261 -7056
  109. package/docs/v1/operations.md +1 -1
  110. package/oclif.manifest.json +116 -72
  111. package/package.json +1 -1
@@ -25,12 +25,18 @@ exports.mergeObjectField = mergeObjectField;
25
25
  exports.mergeTriggerKindConfig = mergeTriggerKindConfig;
26
26
  exports.assertKindFlagsApply = assertKindFlagsApply;
27
27
  exports.describeScheduledFire = describeScheduledFire;
28
+ exports.fireWasAccepted = fireWasAccepted;
29
+ exports.describeFireDetail = describeFireDetail;
30
+ exports.formatFireInZone = formatFireInZone;
31
+ exports.nextFireLines = nextFireLines;
32
+ exports.destinationLine = destinationLine;
28
33
  exports.withQuery = withQuery;
29
34
  exports.agentScope = agentScope;
30
35
  exports.spaceSubscriberScope = spaceSubscriberScope;
31
36
  exports.agentRefEntity = agentRefEntity;
32
37
  exports.fetchAgentScopedTrigger = fetchAgentScopedTrigger;
33
38
  exports.scopeLabel = scopeLabel;
39
+ exports.onceDueAt = onceDueAt;
34
40
  exports.nextRunLabel = nextRunLabel;
35
41
  exports.armedLabel = armedLabel;
36
42
  exports.degradedMarker = degradedMarker;
@@ -766,6 +772,116 @@ function describeScheduledFire(result) {
766
772
  }
767
773
  return undefined;
768
774
  }
775
+ /**
776
+ * Was the fire admitted? `in_progress` and `awaiting_runtime` are admissions whose terminal arrives
777
+ * later (every agent-turn fire answers `in_progress`), and a repeat of a fire that was admitted is
778
+ * `duplicate` with the original beside it; none of them is a refusal (OSK-3867 made the same mistake
779
+ * about `in_progress` once). Only these are success for `triggers fire --strict`.
780
+ */
781
+ function fireWasAccepted(result) {
782
+ const accepted = data_provider_1.ACCEPTED_AGENT_TRIGGER_FIRE_OUTCOMES;
783
+ if (typeof result.outcome === 'string' && accepted.includes(result.outcome))
784
+ return true;
785
+ return (result.outcome === 'duplicate' &&
786
+ typeof result.originalOutcome === 'string' &&
787
+ accepted.includes(result.originalOutcome));
788
+ }
789
+ /**
790
+ * The lines a fire or replay owes beyond `outcome=… reason=…` (OSK-15494): the sentence the
791
+ * server wrote ("Replayed fire … resolved a step that was not waiting when that fire ran:
792
+ * run …, step …"), or, for a debounced fire, when a retry will be accepted. Empty when the server
793
+ * reported nothing more.
794
+ */
795
+ function describeFireDetail(result) {
796
+ const lines = [];
797
+ if (typeof result.detail === 'string' && result.detail.trim())
798
+ lines.push(result.detail.trim());
799
+ else if (typeof result.retryAt === 'string' && result.retryAt) {
800
+ lines.push(`Try again after ${result.retryAt}.`);
801
+ }
802
+ // A fire option the server does not read is named, because dropping it without a word left a caller
803
+ // unable to tell whether `--from-json` was honoured (OSK-15498).
804
+ if (Array.isArray(result.ignoredOptions) && result.ignoredOptions.length > 0) {
805
+ lines.push(`Ignored fire options (a fire does not read them): ${result.ignoredOptions.map(String).join(', ')}`);
806
+ }
807
+ // Only a fire that resolved a step: an executionRef is a job id, a message id or a
808
+ // notification id for every other executor, and "Resolved:" would be wrong for them.
809
+ if (typeof result.executionRef === 'string' &&
810
+ result.executionRef &&
811
+ result.outcome === 'succeeded' &&
812
+ typeof result.reason === 'string' &&
813
+ result.reason.startsWith('resolved:')) {
814
+ lines.push(`Resolved: ${result.executionRef}`);
815
+ }
816
+ return lines;
817
+ }
818
+ const FIRE_DENIAL_WORDS = {
819
+ outside_working_hours: 'outside working hours',
820
+ inside_quiet_hours: 'inside quiet hours',
821
+ };
822
+ /**
823
+ * A projected fire as the trigger's own clock reads it. The header said `America/New_York` over
824
+ * values that were UTC instants, so nothing on the line was in the zone it named (OSK-15407). The
825
+ * instant itself stays beside it, exact.
826
+ */
827
+ function formatFireInZone(iso, timeZone) {
828
+ const date = new Date(iso);
829
+ if (!timeZone || Number.isNaN(date.getTime()))
830
+ return iso;
831
+ try {
832
+ const parts = new Intl.DateTimeFormat('en-CA', {
833
+ timeZone,
834
+ year: 'numeric',
835
+ month: '2-digit',
836
+ day: '2-digit',
837
+ hour: '2-digit',
838
+ minute: '2-digit',
839
+ hourCycle: 'h23',
840
+ timeZoneName: 'short',
841
+ }).formatToParts(date);
842
+ const part = (type) => parts.find((entry) => entry.type === type)?.value ?? '';
843
+ return `${part('year')}-${part('month')}-${part('day')} ${part('hour')}:${part('minute')} ${part('timeZoneName')} (${iso})`;
844
+ }
845
+ catch {
846
+ return iso;
847
+ }
848
+ }
849
+ function nextFireLines(schedule) {
850
+ if (!schedule?.nextFires?.length)
851
+ return [];
852
+ const windows = new Map((schedule.nextFireWindows ?? []).map((entry) => [entry.at, entry]));
853
+ const lines = [`Next fires (${schedule.timezone}):`];
854
+ for (const at of schedule.nextFires) {
855
+ const window = windows.get(at);
856
+ const mark = window && !window.runs
857
+ ? ` ✗ would be skipped: ${FIRE_DENIAL_WORDS[window.reason ?? ''] ?? window.reason ?? 'window'}`
858
+ : '';
859
+ lines.push(` ${formatFireInZone(at, schedule.timezone)}${mark}`);
860
+ }
861
+ return lines;
862
+ }
863
+ /**
864
+ * A destination as `key=value` pairs, without Mongo's storage fields.
865
+ *
866
+ * A nested object that is longer than a short locator (`agentChat` is the Agent's whole chat row,
867
+ * with the preview of its last message) is named by its id, not dumped: the line printed ~700
868
+ * characters of unrelated chat state into a preflight (OSK-16200). `--json` carries all of it.
869
+ */
870
+ function destinationLine(destination) {
871
+ const fields = Object.entries(destination).filter(([key]) => key !== '_id' && key !== '__v');
872
+ return fields.map(([key, value]) => `${key}=${destinationValue(value)}`).join(' ');
873
+ }
874
+ const DESTINATION_INLINE_MAX = 80;
875
+ function destinationValue(value) {
876
+ if (typeof value !== 'object' || value === null)
877
+ return String(value);
878
+ const json = JSON.stringify(value);
879
+ if (json.length <= DESTINATION_INLINE_MAX)
880
+ return json;
881
+ const record = value;
882
+ const id = [record.agentChatId, record.id, record.conversationId].find((candidate) => typeof candidate === 'string' && candidate);
883
+ return id ? String(id) : `{${Object.keys(record).length} fields; see --json}`;
884
+ }
769
885
  function withQuery(path, params) {
770
886
  const search = new URLSearchParams();
771
887
  for (const [key, value] of Object.entries(params)) {
@@ -849,9 +965,24 @@ function scopeLabel(trigger) {
849
965
  .join(',');
850
966
  return refText ? `${kind}:${refText}` : kind;
851
967
  }
968
+ /**
969
+ * When an armed `once` trigger is due: its `once.runAt` (or `once.at`), and only while it has not
970
+ * fired. A follow-up check-in due next month had no `schedule.nextRunAt`, so every table said `-` and
971
+ * `triggers show` said `Armed: yes` with no time (OSK-15416).
972
+ */
973
+ function onceDueAt(trigger) {
974
+ if (trigger.kind !== 'once' || !trigger.once)
975
+ return null;
976
+ const once = trigger.once;
977
+ if (once.firedAt || trigger.enabled === false)
978
+ return null;
979
+ const due = once.runAt ?? once.at;
980
+ return due ? String(due) : null;
981
+ }
852
982
  function nextRunLabel(trigger) {
853
983
  const next = trigger.schedule?.nextRunAt ??
854
984
  trigger.selfScheduled?.nextRunAt ??
985
+ onceDueAt(trigger) ??
855
986
  trigger.audit?.lastFiredAt ??
856
987
  '';
857
988
  return next ? String(next) : '-';
@@ -1073,8 +1204,11 @@ function renderTriggerSummary(trigger, log, options = {}) {
1073
1204
  if (effect?.writeMode)
1074
1205
  log(`Writes: ${writeModeLabel(String(effect.writeMode))}`);
1075
1206
  const next = nextRunLabel(trigger);
1076
- if (next !== '-')
1077
- log(`Next/last:${next.length > 0 ? ` ${next}` : ''}`);
1207
+ if (next !== '-') {
1208
+ // `nextRunLabel` falls back to the last fire when nothing is scheduled; say which it is.
1209
+ const upcoming = Boolean(trigger.schedule?.nextRunAt ?? trigger.selfScheduled?.nextRunAt ?? onceDueAt(trigger));
1210
+ log(`${upcoming ? 'Next run:' : 'Last run:'}${next.length > 0 ? ` ${next}` : ''}`);
1211
+ }
1078
1212
  /*
1079
1213
  * What actually happened when it fired.
1080
1214
  *
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @param text the answer so far, widget blocks included
3
+ * @param final the answer will not grow any more (release held-back text, and report
4
+ * a block still open as cut off)
5
+ */
6
+ export declare function widgetsAsText(text: string, { final }: {
7
+ final: boolean;
8
+ }): string;
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.widgetsAsText = widgetsAsText;
4
+ /**
5
+ * An agent's answer as a TERMINAL can show it: each widget block replaced by its
6
+ * fallback sentence.
7
+ *
8
+ * An agent may answer with a widget: a fenced block of CSS, HTML and JavaScript
9
+ * that the web and mobile apps draw in a sandboxed frame. A terminal cannot draw
10
+ * one, and printing the block is not a faithful rendering of it: it is a program,
11
+ * dumped between two paragraphs of prose, that can run to hundreds of kilobytes.
12
+ * The design record is explicit that the CLI is one of the consumers that "gets the
13
+ * sentence" (docs/architecture/generative-ui-2026-10-13.md section 15.4), the same
14
+ * sentence the server stores as `Message.text` and sends to a channel. The block
15
+ * itself is in the persisted message for the app to draw.
16
+ *
17
+ * WHICH LANES THIS COVERS. The streaming lane (`text.delta`), the final answer
18
+ * (`run.result`, `--no-stream`, `--json`'s `response`) and the comparison against the
19
+ * persisted record all read the ANSWER, and now all read this. The raw lane does
20
+ * not: a `session.output` envelope is the wire, and `--jsonl` keeps mirroring it
21
+ * unchanged for a consumer that wants the block itself (a coding agent that asked a
22
+ * cloud agent for a widget, say). That is where the source is.
23
+ *
24
+ * ONE PARSER. The grammar is read only through the shared incremental parser
25
+ * (`createUiWidgetFenceParser`), the one the web client and the server use, so a
26
+ * block is recognised, bounded and refused identically on every surface; this file
27
+ * never spells the fence. What it adds is the JOIN: how a substituted sentence sits
28
+ * among the prose. That rule is the server's (`GenerativeUi/ingest.js`
29
+ * `createTextDeriver`, which produces `Message.text`) and is mirrored here, because
30
+ * a terminal-side answer that spaced the sentence differently would be a second,
31
+ * quietly different text, and the whitespace-insensitive comparison against the
32
+ * record (`streamShowsRecordedText`) would no longer be the only thing keeping the
33
+ * two honest. The join: a fallback is its own paragraph, preceded by a blank line
34
+ * unless the text so far already ends in one, and followed by a blank line before
35
+ * whatever comes next; the break is written only when something follows, so the
36
+ * result only ever grows by appending while the input grows, which is what a
37
+ * terminal that prints "what is new" needs (`renderableDelta`).
38
+ *
39
+ * `final: false` holds back a trailing line that could still turn into a block
40
+ * opener, so text a terminal has already printed is never retracted.
41
+ */
42
+ const data_provider_1 = require("@skrr-ai/data-provider");
43
+ const PARAGRAPH_BREAK = '\n\n';
44
+ /** The last line of `text` starts with a colon: the shape a held-back opener has. */
45
+ function endsInColonLine(text) {
46
+ const start = Math.max(text.lastIndexOf('\n'), text.lastIndexOf('\r')) + 1;
47
+ return text.charCodeAt(start) === 58;
48
+ }
49
+ /** What must precede a paragraph so a blank line separates it from `tail`. */
50
+ function leadForBlankLine(tail) {
51
+ if (tail.endsWith('\n\n'))
52
+ return '';
53
+ if (tail.endsWith('\n'))
54
+ return '\n';
55
+ return PARAGRAPH_BREAK;
56
+ }
57
+ /** Fence-looking lines in a substituted sentence are pushed off column 0, so nothing re-reads them as a block. */
58
+ const OPENER_AT_LINE_START = new RegExp(`^(:{${data_provider_1.UI_WIDGET_FENCE.minColons},})`, 'gm');
59
+ function fallbackFor(event) {
60
+ if (event.type === 'widget-complete')
61
+ return (0, data_provider_1.uiWidgetFallbackText)(event.payload);
62
+ // A cut-off block contributes nothing: its sentence is not known yet, and adding the
63
+ // title now would make a later, shorter sentence shrink the text.
64
+ if (event.type === 'widget-error' && event.partial && event.reason !== 'truncated') {
65
+ return (0, data_provider_1.uiWidgetFallbackText)(event.partial);
66
+ }
67
+ return '';
68
+ }
69
+ /**
70
+ * @param text the answer so far, widget blocks included
71
+ * @param final the answer will not grow any more (release held-back text, and report
72
+ * a block still open as cut off)
73
+ */
74
+ function widgetsAsText(text, { final }) {
75
+ // Cheap necessary condition for any work at all: a block needs the fence name, and
76
+ // while text is still streaming a trailing colon line may be an opener not yet decided.
77
+ if (text.indexOf(data_provider_1.UI_WIDGET_FENCE.name) === -1 && !(!final && endsInColonLine(text))) {
78
+ return text;
79
+ }
80
+ const parser = (0, data_provider_1.createUiWidgetFenceParser)();
81
+ const events = parser.push(text).slice();
82
+ if (final)
83
+ events.push(...parser.end());
84
+ let out = '';
85
+ let tail = '';
86
+ let started = false;
87
+ let pendingBreak = false;
88
+ const emit = (chunk) => {
89
+ if (chunk === '')
90
+ return;
91
+ started = true;
92
+ tail = (tail + chunk).slice(-2);
93
+ out += chunk;
94
+ };
95
+ for (const event of events) {
96
+ if (event.type === 'text') {
97
+ if (event.text === '')
98
+ continue;
99
+ const lead = pendingBreak ? PARAGRAPH_BREAK : '';
100
+ pendingBreak = false;
101
+ emit(lead + event.text);
102
+ continue;
103
+ }
104
+ const fallback = fallbackFor(event);
105
+ if (!fallback)
106
+ continue;
107
+ let lead = '';
108
+ if (pendingBreak)
109
+ lead = PARAGRAPH_BREAK;
110
+ else if (started)
111
+ lead = leadForBlankLine(tail);
112
+ pendingBreak = true;
113
+ emit(lead + fallback.replace(OPENER_AT_LINE_START, ' $1'));
114
+ }
115
+ return out;
116
+ }
@@ -133,3 +133,36 @@ export declare function parseCliHandoffDpopProofResponse(value: unknown): CliHan
133
133
  export declare const DELEGATED_AGENT_CLI_ID_PREFIX = "oversky-daemon-agent-cli-";
134
134
  export declare const WORKLOAD_CLI_ID_PREFIX = "oversky-daemon-workload-cli-";
135
135
  export declare function classifyCliFamilyId(id: unknown): 'agent' | 'workload' | 'person';
136
+ /**
137
+ * Who keeps a daemon running, which decides how it is signed in again.
138
+ * - `desktop`: the Skrr desktop app hosts it (Electron-as-Node under the
139
+ * app's LaunchAgent). The app mints the daemon's credential itself at
140
+ * `POST /api/daemons/token`, with a device proof, on every start of its
141
+ * daemon client (OSK-15990), so reopening the app IS the sign-in. Neither
142
+ * login command repairs it: `skrr daemon login` refuses an Electron-hosted
143
+ * daemon (its program is the app, not a `skrrd`), and a `skrrd` on PATH is
144
+ * usually not installed, or is a different build from the one running.
145
+ * - `service`: `skrrd install` (launchd/systemd/schtasks) or a hand start;
146
+ * `skrrd login` for that profile, server and working directory is the fix.
147
+ */
148
+ export type DaemonSupervisor = 'desktop' | 'service';
149
+ /**
150
+ * The ONE sentence for "only a new daemon sign-in repairs this hand-off
151
+ * refusal" (OSK-16216). The daemon fills it in with what it knows about
152
+ * itself and sends it as the loopback 503's `remedy`; the CLI uses it, with
153
+ * nothing filled in, when an older daemon sent none. Before this the two were
154
+ * written separately and disagreed — the CLI said `skrr daemon login`, the
155
+ * daemon said `skrrd --profile … login` — and neither was right for a daemon
156
+ * the desktop app runs.
157
+ *
158
+ * Returned without a leading capital or trailing period so callers can put it
159
+ * after "Fix: " or "— ".
160
+ */
161
+ export declare function daemonSignInRemedy(opts?: {
162
+ /** How the daemon is run; absent when the caller cannot tell. */
163
+ supervisor?: DaemonSupervisor;
164
+ /** The exact `skrrd … login` for this profile, when the daemon knows it. */
165
+ loginCommand?: string;
166
+ /** The refusal was DEVICE_PROOF_PIN_MISMATCH: say that a sign-in re-pins. */
167
+ repinsDeviceKey?: boolean;
168
+ }): string;
@@ -27,6 +27,7 @@ exports.descriptorOffersDpop = descriptorOffersDpop;
27
27
  exports.parseCliHandoffTokenResponse = parseCliHandoffTokenResponse;
28
28
  exports.parseCliHandoffDpopProofResponse = parseCliHandoffDpopProofResponse;
29
29
  exports.classifyCliFamilyId = classifyCliFamilyId;
30
+ exports.daemonSignInRemedy = daemonSignInRemedy;
30
31
  /**
31
32
  * What a caller may redeem the descriptor secret for.
32
33
  * - `refresh_family`: the legacy durable mint (90-day cli-scope family).
@@ -156,3 +157,29 @@ function classifyCliFamilyId(id) {
156
157
  return 'workload';
157
158
  return 'person';
158
159
  }
160
+ /**
161
+ * The ONE sentence for "only a new daemon sign-in repairs this hand-off
162
+ * refusal" (OSK-16216). The daemon fills it in with what it knows about
163
+ * itself and sends it as the loopback 503's `remedy`; the CLI uses it, with
164
+ * nothing filled in, when an older daemon sent none. Before this the two were
165
+ * written separately and disagreed — the CLI said `skrr daemon login`, the
166
+ * daemon said `skrrd --profile … login` — and neither was right for a daemon
167
+ * the desktop app runs.
168
+ *
169
+ * Returned without a leading capital or trailing period so callers can put it
170
+ * after "Fix: " or "— ".
171
+ */
172
+ function daemonSignInRemedy(opts = {}) {
173
+ const repin = opts.repinsDeviceKey === true;
174
+ if (opts.supervisor === 'desktop') {
175
+ return ('quit and reopen the Skrr desktop app on the machine running the daemon (install its update ' +
176
+ `first if one is offered): the app signs its daemon in again on start${repin ? ' and re-pins the device key' : ''}`);
177
+ }
178
+ if (opts.supervisor === 'service') {
179
+ const login = opts.loginCommand ? `\`${opts.loginCommand}\`` : '`skrr daemon login`';
180
+ return `run ${login}${repin ? ' — an interactive login re-pins the device key' : ''}`;
181
+ }
182
+ return ('sign the daemon in again on the machine running it: if the Skrr desktop app runs it, quit ' +
183
+ 'and reopen the app; otherwise run `skrr daemon login`' +
184
+ (repin ? ' (either one re-pins the device key)' : ''));
185
+ }
@@ -16,10 +16,30 @@
16
16
  * and `api/server/services/HostedPreview/__tests__/telemetryRedaction.spec.js` against tokens the issuer really
17
17
  * mints, so a change to either side fails there rather than in a trace nobody reads.
18
18
  *
19
+ * THE OTHER PATH CREDENTIALS (OSK-16174). Four more bearer credentials travel in a URL, and until they were added here
20
+ * only the request logger's path rewrite (`requestLogger.js`) knew them, so the same address that was scrubbed from a
21
+ * log line reached a trace and an error report in full: a workspace invitation (`wsi_`), a platform invitation
22
+ * (`pinv_`), a member's invitation link (`usr_`) and an operator's (`adm2_`, and the older `adm_`). Unlike `pvw_`, whose
23
+ * prefix is rare enough to match on its own, `usr_` and `adm_` begin ordinary words and identifiers (`/usr_local`,
24
+ * `adm_users`), so each of these is recognised by the SHAPE its minting code gives it, and a lookalike is left alone:
25
+ *
26
+ * wsi_ / pinv_ / usr_ / adm2_ `<prefix>_<32 hex>.<version>.<key id>.<43 base64url>`, from `buildCredential` in
27
+ * `api/models/WorkspaceInvite.js` and `PlatformInvitation.js` and `buildUserCode` and
28
+ * `buildAdminCode` in `InvitationCode.js`. The older member link, `usr_<32 hex>.<24
29
+ * base64url>`, has the same first two fields. Matched from the prefix to the end of the
30
+ * token's characters once the 32-hex id and its dot are there, so a credential cut off
31
+ * later in the log line is still redacted whole.
32
+ * adm_ `adm_<43 base64url>`, the older operator code (`generateCode`: 32 random bytes).
33
+ * Exactly 43 characters, because that is the only length it has.
34
+ *
35
+ * `api/server/services/HostedPreview/__tests__/telemetryRedaction.spec.js` mints credentials with the real minting
36
+ * code and requires every one to come out as the marker, so a change to either side fails there.
37
+ *
19
38
  * WHAT IT DOES NOT. A prefix that is itself percent-encoded (`%70vw_`) is not recognised: nothing in this
20
- * repository writes one, and a scan that decoded every string first would cost every sink on every record. The
21
- * request log's own `/[credential]` rewrite (`requestLogger.js`) covers the other credential prefixes and
22
- * stays as it is.
39
+ * repository writes one, and a scan that decoded every string first would cost every sink on every record. A
40
+ * credential cut off before its id is whole (`pinv_0123abc`) is an identifier, not yet the secret, and is left as it
41
+ * is. The request log's own `/[credential]` rewrite stays as it is, and is looser (any `wsi_`, `pinv_`, `usr_`,
42
+ * `adm_`, `adm2_` or `pvw_` path segment), because a path segment is a place a credential is known to be.
23
43
  *
24
44
  * COST. Linear in the input. The only nested repetition is `%(?:25)*XX`, and `XX` is never `25`, so the engine
25
45
  * cannot read one run of `25`s two ways; the tests feed it a megabyte of the worst shapes.
@@ -31,8 +51,11 @@
31
51
  /** What replaces a token. Fixed, so a trace can say a preview was involved but not which one. */
32
52
  export declare const PREVIEW_TOKEN_REDACTION: "pvw_[redacted]";
33
53
  /**
34
- * Replace every preview token in a string with {@link PREVIEW_TOKEN_REDACTION}. The rest of the string is
35
- * untouched, so a trace is still readable. Anything that is not a string comes back as it came.
54
+ * Replace every credential that travels in a URL with a marker: a Hosted Machine preview token with
55
+ * {@link PREVIEW_TOKEN_REDACTION}, and a workspace invitation, platform invitation, member link or operator code with
56
+ * its own prefix and `_[redacted]`. The rest of the string is untouched, so a trace is still readable. Anything
57
+ * that is not a string comes back as it came. (The name is the preview token's, which this began as; the sinks
58
+ * and the boundary specs import it by that name.)
36
59
  */
37
60
  export declare function redactPreviewTokens(value: string): string;
38
61
  export declare function redactPreviewTokens<T>(value: T): T;
@@ -17,10 +17,30 @@
17
17
  * and `api/server/services/HostedPreview/__tests__/telemetryRedaction.spec.js` against tokens the issuer really
18
18
  * mints, so a change to either side fails there rather than in a trace nobody reads.
19
19
  *
20
+ * THE OTHER PATH CREDENTIALS (OSK-16174). Four more bearer credentials travel in a URL, and until they were added here
21
+ * only the request logger's path rewrite (`requestLogger.js`) knew them, so the same address that was scrubbed from a
22
+ * log line reached a trace and an error report in full: a workspace invitation (`wsi_`), a platform invitation
23
+ * (`pinv_`), a member's invitation link (`usr_`) and an operator's (`adm2_`, and the older `adm_`). Unlike `pvw_`, whose
24
+ * prefix is rare enough to match on its own, `usr_` and `adm_` begin ordinary words and identifiers (`/usr_local`,
25
+ * `adm_users`), so each of these is recognised by the SHAPE its minting code gives it, and a lookalike is left alone:
26
+ *
27
+ * wsi_ / pinv_ / usr_ / adm2_ `<prefix>_<32 hex>.<version>.<key id>.<43 base64url>`, from `buildCredential` in
28
+ * `api/models/WorkspaceInvite.js` and `PlatformInvitation.js` and `buildUserCode` and
29
+ * `buildAdminCode` in `InvitationCode.js`. The older member link, `usr_<32 hex>.<24
30
+ * base64url>`, has the same first two fields. Matched from the prefix to the end of the
31
+ * token's characters once the 32-hex id and its dot are there, so a credential cut off
32
+ * later in the log line is still redacted whole.
33
+ * adm_ `adm_<43 base64url>`, the older operator code (`generateCode`: 32 random bytes).
34
+ * Exactly 43 characters, because that is the only length it has.
35
+ *
36
+ * `api/server/services/HostedPreview/__tests__/telemetryRedaction.spec.js` mints credentials with the real minting
37
+ * code and requires every one to come out as the marker, so a change to either side fails there.
38
+ *
20
39
  * WHAT IT DOES NOT. A prefix that is itself percent-encoded (`%70vw_`) is not recognised: nothing in this
21
- * repository writes one, and a scan that decoded every string first would cost every sink on every record. The
22
- * request log's own `/[credential]` rewrite (`requestLogger.js`) covers the other credential prefixes and
23
- * stays as it is.
40
+ * repository writes one, and a scan that decoded every string first would cost every sink on every record. A
41
+ * credential cut off before its id is whole (`pinv_0123abc`) is an identifier, not yet the secret, and is left as it
42
+ * is. The request log's own `/[credential]` rewrite stays as it is, and is looser (any `wsi_`, `pinv_`, `usr_`,
43
+ * `adm_`, `adm2_` or `pvw_` path segment), because a path segment is a place a credential is known to be.
24
44
  *
25
45
  * COST. Linear in the input. The only nested repetition is `%(?:25)*XX`, and `XX` is never `25`, so the engine
26
46
  * cannot read one run of `25`s two ways; the tests feed it a megabyte of the worst shapes.
@@ -49,11 +69,36 @@ const TOKEN_CHAR = '(?:[A-Za-z0-9._-]|%(?:25)*(?:2[DdEe]|3[0-9]|4[1-9A-Fa-f]|5[0
49
69
  * nothing and redacting twice changes nothing.
50
70
  */
51
71
  const PREVIEW_TOKEN = new RegExp(`pvw(?:_|%(?:25)*5[Ff])${TOKEN_CHAR}+`, 'g');
72
+ /**
73
+ * A prefix's underscore, which (like the token's own characters) may be written as a percent-escape, once or several
74
+ * times; and the dot that ends an id.
75
+ */
76
+ const UNDERSCORE = '(?:_|%(?:25)*5[Ff])';
77
+ const DOT = '(?:\\.|%(?:25)*2[Ee])';
78
+ /**
79
+ * `wsi`, `pinv`, `usr` or `adm2`, an underscore, and then a lookahead that insists on what the minting code writes
80
+ * next: 32 lowercase hex characters and the dot that ends them. Everything the credential is made of follows, and the
81
+ * prefix is captured so the marker can say which kind it was. `usr_<40 hex>`, `usr_00000001` and `/usr_profile` have no
82
+ * dot after exactly 32 hex characters, so they match nothing.
83
+ */
84
+ const SIGNED_CREDENTIAL = new RegExp(`(wsi|pinv|usr|adm2)${UNDERSCORE}(?=[0-9a-f]{32}${DOT})${TOKEN_CHAR}+`, 'g');
85
+ /**
86
+ * The older operator code: `adm_` and exactly 43 base64url characters (32 random bytes), not followed by another one.
87
+ * A shorter or longer run of letters is a name, not this credential. The characters are the plain alphabet and not
88
+ * `TOKEN_CHAR`, whose dot would swallow the full stop at the end of a sentence.
89
+ */
90
+ const LEGACY_OPERATOR_CODE = new RegExp(`(adm)${UNDERSCORE}(?=[A-Za-z0-9_-]{43}(?![A-Za-z0-9_-]))[A-Za-z0-9_-]{43}`, 'g');
91
+ /** Cheap test before any of the expressions: a string with none of these cannot hold a credential. */
92
+ const CREDENTIAL_HINTS = ['pvw', 'wsi', 'pinv', 'usr', 'adm'];
93
+ const credentialMarker = (_match, prefix) => `${prefix}_[redacted]`;
52
94
  function redactPreviewTokens(value) {
53
- if (typeof value !== 'string' || value.indexOf('pvw') === -1) {
95
+ if (typeof value !== 'string' || !CREDENTIAL_HINTS.some((hint) => value.indexOf(hint) !== -1)) {
54
96
  return value;
55
97
  }
56
- return value.replace(PREVIEW_TOKEN, exports.PREVIEW_TOKEN_REDACTION);
98
+ return value
99
+ .replace(PREVIEW_TOKEN, exports.PREVIEW_TOKEN_REDACTION)
100
+ .replace(SIGNED_CREDENTIAL, credentialMarker)
101
+ .replace(LEGACY_OPERATOR_CODE, credentialMarker);
57
102
  }
58
103
  const DEFAULT_MAX_NODES = 50_000;
59
104
  const DEFAULT_MAX_DEPTH = 32;
@@ -133,3 +133,36 @@ export declare function parseCliHandoffDpopProofResponse(value: unknown): CliHan
133
133
  export declare const DELEGATED_AGENT_CLI_ID_PREFIX = "oversky-daemon-agent-cli-";
134
134
  export declare const WORKLOAD_CLI_ID_PREFIX = "oversky-daemon-workload-cli-";
135
135
  export declare function classifyCliFamilyId(id: unknown): 'agent' | 'workload' | 'person';
136
+ /**
137
+ * Who keeps a daemon running, which decides how it is signed in again.
138
+ * - `desktop`: the Skrr desktop app hosts it (Electron-as-Node under the
139
+ * app's LaunchAgent). The app mints the daemon's credential itself at
140
+ * `POST /api/daemons/token`, with a device proof, on every start of its
141
+ * daemon client (OSK-15990), so reopening the app IS the sign-in. Neither
142
+ * login command repairs it: `skrr daemon login` refuses an Electron-hosted
143
+ * daemon (its program is the app, not a `skrrd`), and a `skrrd` on PATH is
144
+ * usually not installed, or is a different build from the one running.
145
+ * - `service`: `skrrd install` (launchd/systemd/schtasks) or a hand start;
146
+ * `skrrd login` for that profile, server and working directory is the fix.
147
+ */
148
+ export type DaemonSupervisor = 'desktop' | 'service';
149
+ /**
150
+ * The ONE sentence for "only a new daemon sign-in repairs this hand-off
151
+ * refusal" (OSK-16216). The daemon fills it in with what it knows about
152
+ * itself and sends it as the loopback 503's `remedy`; the CLI uses it, with
153
+ * nothing filled in, when an older daemon sent none. Before this the two were
154
+ * written separately and disagreed — the CLI said `skrr daemon login`, the
155
+ * daemon said `skrrd --profile … login` — and neither was right for a daemon
156
+ * the desktop app runs.
157
+ *
158
+ * Returned without a leading capital or trailing period so callers can put it
159
+ * after "Fix: " or "— ".
160
+ */
161
+ export declare function daemonSignInRemedy(opts?: {
162
+ /** How the daemon is run; absent when the caller cannot tell. */
163
+ supervisor?: DaemonSupervisor;
164
+ /** The exact `skrrd … login` for this profile, when the daemon knows it. */
165
+ loginCommand?: string;
166
+ /** The refusal was DEVICE_PROOF_PIN_MISMATCH: say that a sign-in re-pins. */
167
+ repinsDeviceKey?: boolean;
168
+ }): string;
@@ -145,3 +145,29 @@ export function classifyCliFamilyId(id) {
145
145
  return 'workload';
146
146
  return 'person';
147
147
  }
148
+ /**
149
+ * The ONE sentence for "only a new daemon sign-in repairs this hand-off
150
+ * refusal" (OSK-16216). The daemon fills it in with what it knows about
151
+ * itself and sends it as the loopback 503's `remedy`; the CLI uses it, with
152
+ * nothing filled in, when an older daemon sent none. Before this the two were
153
+ * written separately and disagreed — the CLI said `skrr daemon login`, the
154
+ * daemon said `skrrd --profile … login` — and neither was right for a daemon
155
+ * the desktop app runs.
156
+ *
157
+ * Returned without a leading capital or trailing period so callers can put it
158
+ * after "Fix: " or "— ".
159
+ */
160
+ export function daemonSignInRemedy(opts = {}) {
161
+ const repin = opts.repinsDeviceKey === true;
162
+ if (opts.supervisor === 'desktop') {
163
+ return ('quit and reopen the Skrr desktop app on the machine running the daemon (install its update ' +
164
+ `first if one is offered): the app signs its daemon in again on start${repin ? ' and re-pins the device key' : ''}`);
165
+ }
166
+ if (opts.supervisor === 'service') {
167
+ const login = opts.loginCommand ? `\`${opts.loginCommand}\`` : '`skrr daemon login`';
168
+ return `run ${login}${repin ? ' — an interactive login re-pins the device key' : ''}`;
169
+ }
170
+ return ('sign the daemon in again on the machine running it: if the Skrr desktop app runs it, quit ' +
171
+ 'and reopen the app; otherwise run `skrr daemon login`' +
172
+ (repin ? ' (either one re-pins the device key)' : ''));
173
+ }
@@ -16,10 +16,30 @@
16
16
  * and `api/server/services/HostedPreview/__tests__/telemetryRedaction.spec.js` against tokens the issuer really
17
17
  * mints, so a change to either side fails there rather than in a trace nobody reads.
18
18
  *
19
+ * THE OTHER PATH CREDENTIALS (OSK-16174). Four more bearer credentials travel in a URL, and until they were added here
20
+ * only the request logger's path rewrite (`requestLogger.js`) knew them, so the same address that was scrubbed from a
21
+ * log line reached a trace and an error report in full: a workspace invitation (`wsi_`), a platform invitation
22
+ * (`pinv_`), a member's invitation link (`usr_`) and an operator's (`adm2_`, and the older `adm_`). Unlike `pvw_`, whose
23
+ * prefix is rare enough to match on its own, `usr_` and `adm_` begin ordinary words and identifiers (`/usr_local`,
24
+ * `adm_users`), so each of these is recognised by the SHAPE its minting code gives it, and a lookalike is left alone:
25
+ *
26
+ * wsi_ / pinv_ / usr_ / adm2_ `<prefix>_<32 hex>.<version>.<key id>.<43 base64url>`, from `buildCredential` in
27
+ * `api/models/WorkspaceInvite.js` and `PlatformInvitation.js` and `buildUserCode` and
28
+ * `buildAdminCode` in `InvitationCode.js`. The older member link, `usr_<32 hex>.<24
29
+ * base64url>`, has the same first two fields. Matched from the prefix to the end of the
30
+ * token's characters once the 32-hex id and its dot are there, so a credential cut off
31
+ * later in the log line is still redacted whole.
32
+ * adm_ `adm_<43 base64url>`, the older operator code (`generateCode`: 32 random bytes).
33
+ * Exactly 43 characters, because that is the only length it has.
34
+ *
35
+ * `api/server/services/HostedPreview/__tests__/telemetryRedaction.spec.js` mints credentials with the real minting
36
+ * code and requires every one to come out as the marker, so a change to either side fails there.
37
+ *
19
38
  * WHAT IT DOES NOT. A prefix that is itself percent-encoded (`%70vw_`) is not recognised: nothing in this
20
- * repository writes one, and a scan that decoded every string first would cost every sink on every record. The
21
- * request log's own `/[credential]` rewrite (`requestLogger.js`) covers the other credential prefixes and
22
- * stays as it is.
39
+ * repository writes one, and a scan that decoded every string first would cost every sink on every record. A
40
+ * credential cut off before its id is whole (`pinv_0123abc`) is an identifier, not yet the secret, and is left as it
41
+ * is. The request log's own `/[credential]` rewrite stays as it is, and is looser (any `wsi_`, `pinv_`, `usr_`,
42
+ * `adm_`, `adm2_` or `pvw_` path segment), because a path segment is a place a credential is known to be.
23
43
  *
24
44
  * COST. Linear in the input. The only nested repetition is `%(?:25)*XX`, and `XX` is never `25`, so the engine
25
45
  * cannot read one run of `25`s two ways; the tests feed it a megabyte of the worst shapes.
@@ -31,8 +51,11 @@
31
51
  /** What replaces a token. Fixed, so a trace can say a preview was involved but not which one. */
32
52
  export declare const PREVIEW_TOKEN_REDACTION: "pvw_[redacted]";
33
53
  /**
34
- * Replace every preview token in a string with {@link PREVIEW_TOKEN_REDACTION}. The rest of the string is
35
- * untouched, so a trace is still readable. Anything that is not a string comes back as it came.
54
+ * Replace every credential that travels in a URL with a marker: a Hosted Machine preview token with
55
+ * {@link PREVIEW_TOKEN_REDACTION}, and a workspace invitation, platform invitation, member link or operator code with
56
+ * its own prefix and `_[redacted]`. The rest of the string is untouched, so a trace is still readable. Anything
57
+ * that is not a string comes back as it came. (The name is the preview token's, which this began as; the sinks
58
+ * and the boundary specs import it by that name.)
36
59
  */
37
60
  export declare function redactPreviewTokens(value: string): string;
38
61
  export declare function redactPreviewTokens<T>(value: T): T;