@skrr-ai/cli 0.1.102 → 0.1.104

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 (52) hide show
  1. package/dist/base-command.js +24 -0
  2. package/dist/commands/agent/message/send.js +6 -2
  3. package/dist/commands/agents/triggers/create.js +7 -17
  4. package/dist/commands/agents/triggers/update.js +41 -28
  5. package/dist/commands/commitments/create.js +2 -0
  6. package/dist/commands/commitments/preflight.js +18 -1
  7. package/dist/commands/commitments/roster.d.ts +15 -0
  8. package/dist/commands/commitments/roster.js +39 -0
  9. package/dist/commands/commitments/show.js +1 -1
  10. package/dist/commands/commitments/status.d.ts +17 -0
  11. package/dist/commands/commitments/status.js +41 -0
  12. package/dist/commands/commitments/update.js +12 -0
  13. package/dist/commands/daemon/logs.d.ts +106 -1
  14. package/dist/commands/daemon/logs.js +311 -15
  15. package/dist/commands/followups/checkin.js +46 -3
  16. package/dist/commands/tasks/approvals/resolve.d.ts +12 -0
  17. package/dist/commands/tasks/approvals/resolve.js +21 -5
  18. package/dist/commands/triggers/create.js +8 -17
  19. package/dist/commands/triggers/fire.js +3 -0
  20. package/dist/commands/triggers/update.js +41 -27
  21. package/dist/commands/widgets/check.d.ts +39 -0
  22. package/dist/commands/widgets/check.js +179 -0
  23. package/dist/lib/agent-mailbox.d.ts +0 -1
  24. package/dist/lib/agent-mailbox.js +0 -1
  25. package/dist/lib/commitment-loop-status.d.ts +16 -0
  26. package/dist/lib/commitment-loop-status.js +164 -0
  27. package/dist/lib/commitments.d.ts +28 -1
  28. package/dist/lib/commitments.js +184 -5
  29. package/dist/lib/delegated-cli.d.ts +23 -1
  30. package/dist/lib/delegated-cli.js +61 -4
  31. package/dist/lib/first-party-harness.d.ts +1 -1
  32. package/dist/lib/first-party-harness.js +1 -1
  33. package/dist/lib/session-task-write.d.ts +13 -0
  34. package/dist/lib/session-task-write.js +63 -3
  35. package/dist/lib/triggers.d.ts +26 -0
  36. package/dist/lib/triggers.js +134 -1
  37. package/dist/lib/widgetCheck.d.ts +123 -0
  38. package/dist/lib/widgetCheck.js +998 -0
  39. package/dist/lib/widgetStaticScan.d.ts +111 -0
  40. package/dist/lib/widgetStaticScan.js +833 -0
  41. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +9 -1
  42. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +12 -0
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.d.ts +61 -0
  44. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.js +177 -0
  45. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +9 -1
  46. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +12 -0
  47. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.d.ts +61 -0
  48. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.js +171 -0
  49. package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
  50. package/dist/node_modules/@skrr-ai/data-provider/index.js +11908 -8693
  51. package/oclif.manifest.json +219 -6
  52. package/package.json +2 -2
@@ -261,6 +261,30 @@ class BaseCommand extends core_1.Command {
261
261
  this.credentialProvenance = { kind: 'daemon-delegated' };
262
262
  }
263
263
  catch (err) {
264
+ // OSK-15990 — a refusal only a new daemon sign-in repairs (e.g.
265
+ // DEVICE_PROOF_PIN_MISMATCH) is NOT retryable: exit 75 told every
266
+ // caller to loop on it. Say so, and say what to run.
267
+ // Matched by name, not instanceof: specs mock `./lib/delegated-cli`
268
+ // down to one or two functions, and a missing class would throw here.
269
+ if (err?.name === 'DelegatedCliUnavailableError') {
270
+ const e = err;
271
+ const remedy = e.remedy && !e.message.includes(e.remedy) ? ` Fix: ${e.remedy}` : '';
272
+ this.failWithCliError({
273
+ message: `Daemon delegated skrr credential is unavailable: ${e.message}${remedy}`,
274
+ code: 'DELEGATED_CLI_CREDENTIAL_UNAVAILABLE',
275
+ // 75 (EX_TEMPFAIL) only when waiting can help; 77 (EX_NOPERM) when
276
+ // the daemon must be signed in again first.
277
+ exit: e.retryable ? 75 : 77,
278
+ retryable: e.retryable,
279
+ details: {
280
+ ...(e.upstreamStatus !== undefined ? { upstreamStatus: e.upstreamStatus } : {}),
281
+ ...(e.upstreamCode ? { upstreamCode: e.upstreamCode } : {}),
282
+ ...(e.upstreamReason ? { upstreamReason: e.upstreamReason } : {}),
283
+ ...(e.retryAt ? { retryAt: e.retryAt } : {}),
284
+ ...(e.remedy ? { remedy: e.remedy } : {}),
285
+ },
286
+ });
287
+ }
264
288
  this.failWithCliError({
265
289
  message: `Daemon delegated skrr credential is unavailable: ${err.message}`,
266
290
  code: 'DELEGATED_CLI_CREDENTIAL_UNAVAILABLE',
@@ -535,9 +535,13 @@ function messageSegmentationAdvisory({ destination, text, followingParts, }) {
535
535
  return undefined;
536
536
  if (destination.kind !== 'app_owner_dm' && destination.kind !== 'slack_owner_dm')
537
537
  return undefined;
538
- if (text.length <= exports.OWNER_DM_SEGMENTATION_SOFT_LIMIT)
538
+ // Measure what is READ. A widget block is not a thought to be split: its css, html and
539
+ // script are a program, and an advisory that counted them would invite an agent to cut
540
+ // one block across `--part`s, which the app could not read as a widget.
541
+ const prose = (0, data_provider_1.stripUiWidgetFencesForDisplay)(text, { final: true });
542
+ if (prose.length <= exports.OWNER_DM_SEGMENTATION_SOFT_LIMIT)
539
543
  return undefined;
540
- return (`[skrr] Owner-DM message is ${text.length} characters. It may read better as 2–4 short thoughts: ` +
544
+ return (`[skrr] Owner-DM message is ${prose.length} characters. It may read better as 2–4 short thoughts: ` +
541
545
  'use --part with one explicit --idempotency-key. This is only an advisory; ' +
542
546
  'use --no-message-lint to suppress it.');
543
547
  }
@@ -66,7 +66,9 @@ class AgentsTriggersCreate extends base_command_1.BaseCommand {
66
66
  multiple: true,
67
67
  }),
68
68
  timezone: core_1.Flags.string({ description: 'IANA timezone for schedule/once triggers' }),
69
- 'max-fires-per-day': core_1.Flags.integer({ description: 'Daily fire cap (schedule/stochastic)' }),
69
+ 'max-fires-per-day': core_1.Flags.integer({
70
+ description: 'Daily fire cap (schedule, stochastic and self_scheduled triggers only)',
71
+ }),
70
72
  'max-runs': core_1.Flags.integer({
71
73
  description: 'Lifetime fire cap for a schedule — the trigger disables itself after N total runs',
72
74
  }),
@@ -172,6 +174,8 @@ class AgentsTriggersCreate extends base_command_1.BaseCommand {
172
174
  let payload;
173
175
  try {
174
176
  payload = await (0, prompt_1.readJsonBody)(flags['from-json']);
177
+ // A flag for another kind is refused, not dropped (OSK-15491).
178
+ (0, triggers_1.assertKindFlagsApply)(flags.kind ?? payload.kind, flags);
175
179
  // Agent scope is injected unconditionally — even when --from-json
176
180
  // supplies a scope, the agent identity from the positional arg wins so
177
181
  // the trigger can never be silently mis-scoped to a different agent.
@@ -213,22 +217,8 @@ class AgentsTriggersCreate extends base_command_1.BaseCommand {
213
217
  filterField: flags['filter-field'],
214
218
  filterValues,
215
219
  });
216
- (0, triggers_1.setDefined)(event, {
217
- entityType: flags.entity,
218
- eventType: flags.event,
219
- filterMode: flags['filter-mode'],
220
- filterField: flags['filter-field'],
221
- filterValue: filterValues && filterValues.length > 1 ? filterValues : filterValues?.[0],
222
- debounceMs: flags['debounce-ms'],
223
- maxPerHour: flags['max-per-hour'],
224
- });
225
- const stochastic = {};
226
- (0, triggers_1.setDefined)(stochastic, {
227
- probability: flags.probability === undefined ? undefined : Number(flags.probability),
228
- jitterMs: flags['jitter-ms'],
229
- maxFiresPerDay: flags['max-fires-per-day'],
230
- evalIntervalMinutes: flags['eval-interval-minutes'],
231
- });
220
+ Object.assign(event, (0, triggers_1.buildEventConfigFromFlags)(flags, filterValues));
221
+ const stochastic = (0, triggers_1.buildStochasticConfigFromFlags)(flags);
232
222
  const selfScheduled = (0, triggers_1.buildSelfScheduledConfigFromFlags)(flags);
233
223
  const form = (0, triggers_1.buildFormConfigFromFlags)(flags);
234
224
  const chatMessage = (0, triggers_1.buildChatMessageConfigFromFlags)(flags);
@@ -55,7 +55,9 @@ class AgentsTriggersUpdate extends base_command_1.BaseCommand {
55
55
  multiple: true,
56
56
  }),
57
57
  timezone: core_1.Flags.string({ description: 'IANA timezone for schedule triggers' }),
58
- 'max-fires-per-day': core_1.Flags.integer({ description: 'Daily fire cap' }),
58
+ 'max-fires-per-day': core_1.Flags.integer({
59
+ description: 'Daily fire cap (schedule, stochastic and self_scheduled triggers only)',
60
+ }),
59
61
  'max-runs': core_1.Flags.integer({ description: 'Lifetime fire cap for a schedule' }),
60
62
  'end-at': core_1.Flags.string({
61
63
  description: 'ISO-8601 datetime after which a schedule stops firing',
@@ -156,6 +158,29 @@ class AgentsTriggersUpdate extends base_command_1.BaseCommand {
156
158
  // Scope is immutable from this surface — an agent trigger stays bound to
157
159
  // its agent. Strip any scope in the JSON so the patch can't re-home it.
158
160
  delete payload.scope;
161
+ }
162
+ catch (err) {
163
+ this.error(err.message, { exit: 1 });
164
+ }
165
+ // The trigger's kind says which config block is its own, and only that block is sent. Every
166
+ // block used to go, so `--max-fires-per-day` wrote `stochastic` and `selfScheduled` onto a
167
+ // schedule trigger and a cap onto an on_demand one that nothing reads (OSK-15491). The scope
168
+ // check that used to follow the build now comes first because it is what reads the trigger.
169
+ let scoped;
170
+ try {
171
+ scoped = await (0, triggers_1.fetchAgentScopedTrigger)(args.agentId, args.triggerId);
172
+ }
173
+ catch (err) {
174
+ if (err instanceof triggers_1.AgentTriggerScopeError)
175
+ this.error(err.message, { exit: 4 });
176
+ this.handleApiError(err);
177
+ }
178
+ const scopedKind = scoped.kind;
179
+ if (typeof scopedKind !== 'string') {
180
+ this.error(`Trigger ${args.triggerId} did not report its kind, so its settings cannot be checked.`, { exit: 1 });
181
+ }
182
+ try {
183
+ (0, triggers_1.assertKindFlagsApply)(scopedKind, flags, { kindIsFixed: true });
159
184
  const execution = {};
160
185
  const builtinAction = (0, triggers_1.buildBuiltinActionPayload)({
161
186
  builtinAction: flags['builtin-action'],
@@ -175,7 +200,10 @@ class AgentsTriggersUpdate extends base_command_1.BaseCommand {
175
200
  });
176
201
  const schedule = (0, triggers_1.buildScheduleConfigFromFlags)(flags);
177
202
  (0, triggers_1.assertScheduleTimingShape)(schedule);
178
- const once = (0, triggers_1.buildOnceConfigFromFlags)(flags);
203
+ const once = (0, triggers_1.buildOnceConfigFromFlags)(flags, {
204
+ kind: scopedKind,
205
+ allowNextRunAtAlias: true,
206
+ });
179
207
  const filterValues = (0, triggers_1.splitRepeated)(flags['filter-value']);
180
208
  // Advisory: a catalog this CLI has not caught up with must not make a
181
209
  // server-supported event unreachable. Warn and send; the server validates.
@@ -190,22 +218,8 @@ class AgentsTriggersUpdate extends base_command_1.BaseCommand {
190
218
  filterValues,
191
219
  });
192
220
  const event = {};
193
- (0, triggers_1.setDefined)(event, {
194
- entityType: flags.entity,
195
- eventType: flags.event,
196
- filterMode: flags['filter-mode'],
197
- filterField: flags['filter-field'],
198
- filterValue: filterValues && filterValues.length > 1 ? filterValues : filterValues?.[0],
199
- debounceMs: flags['debounce-ms'],
200
- maxPerHour: flags['max-per-hour'],
201
- });
202
- const stochastic = {};
203
- (0, triggers_1.setDefined)(stochastic, {
204
- probability: flags.probability === undefined ? undefined : Number(flags.probability),
205
- jitterMs: flags['jitter-ms'],
206
- maxFiresPerDay: flags['max-fires-per-day'],
207
- evalIntervalMinutes: flags['eval-interval-minutes'],
208
- });
221
+ Object.assign(event, (0, triggers_1.buildEventConfigFromFlags)(flags, filterValues));
222
+ const stochastic = (0, triggers_1.buildStochasticConfigFromFlags)(flags);
209
223
  const selfScheduled = (0, triggers_1.buildSelfScheduledConfigFromFlags)(flags);
210
224
  const form = (0, triggers_1.buildFormConfigFromFlags)(flags);
211
225
  const chatMessage = (0, triggers_1.buildChatMessageConfigFromFlags)(flags);
@@ -217,15 +231,15 @@ class AgentsTriggersUpdate extends base_command_1.BaseCommand {
217
231
  idempotencyWindow: flags['idempotency-window'],
218
232
  });
219
233
  (0, triggers_1.mergeObjectField)(payload, 'execution', execution);
220
- (0, triggers_1.mergeObjectField)(payload, 'schedule', schedule);
221
- (0, triggers_1.mergeObjectField)(payload, 'once', once);
222
- (0, triggers_1.mergeObjectField)(payload, 'event', event);
223
- // `kind` is immutable on update (rejected above), so there is no kind to
224
- // gate on here; stochastic is only non-empty when stochastic flags were passed.
225
- (0, triggers_1.mergeObjectField)(payload, 'stochastic', stochastic);
226
- (0, triggers_1.mergeObjectField)(payload, 'selfScheduled', selfScheduled);
227
- (0, triggers_1.mergeObjectField)(payload, 'form', form);
228
- (0, triggers_1.mergeObjectField)(payload, 'chatMessage', chatMessage);
234
+ (0, triggers_1.mergeTriggerKindConfig)(payload, scopedKind, {
235
+ schedule,
236
+ once,
237
+ event,
238
+ stochastic,
239
+ selfScheduled,
240
+ form,
241
+ chatMessage,
242
+ });
229
243
  }
230
244
  catch (err) {
231
245
  this.error(err.message, { exit: 1 });
@@ -235,7 +249,6 @@ class AgentsTriggersUpdate extends base_command_1.BaseCommand {
235
249
  }
236
250
  let trigger;
237
251
  try {
238
- await (0, triggers_1.fetchAgentScopedTrigger)(args.agentId, args.triggerId);
239
252
  trigger = await triggers_1.triggerApi.update(args.triggerId, payload);
240
253
  }
241
254
  catch (err) {
@@ -164,6 +164,8 @@ class CommitmentsCreate extends base_command_1.BaseCommand {
164
164
  '<%= config.bin %> commitments create --agent <agent-id> --title "AWS cost watch" --outcome "Cost regressions are investigated" --agent-directive-file ./aws-cost-policy.md',
165
165
  '<%= config.bin %> commitments create --from-json commitment.json --json',
166
166
  `# Replace <agent-id> with your Agent ID; this creates a draft, not a running loop.
167
+ # In "policy", 0 is a ZERO budget (nothing may run or be spent), not "no limit".
168
+ # To remove a limit set it to null (e.g. "maxCostUsdPerDay": null) or leave the key out.
167
169
  cat > commitment.json <<'JSON'
168
170
  {
169
171
  "agentId": "<agent-id>",
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.preflightGateLines = preflightGateLines;
4
4
  const core_1 = require("@oclif/core");
5
+ const data_provider_1 = require("@skrr-ai/data-provider");
5
6
  const base_command_1 = require("../../base-command");
6
7
  const commitments_1 = require("../../lib/commitments");
7
8
  const prompt_1 = require("../../lib/prompt");
@@ -23,6 +24,22 @@ const prompt_1 = require("../../lib/prompt");
23
24
  * dash as a short code puts two different kinds of thing in one slot.
24
25
  */
25
26
  function preflightGateLines(gate, verbose = false) {
27
+ // "Could it contact you now?" is not a Run gate. It answers whether a message
28
+ // would reach the owner, and a limit the owner set themselves (zero nudges a day,
29
+ // quiet hours) is a chosen quiet, not a defect. It carried the same BLOCK tag, a
30
+ // bare code and no setting as the real blockers beside it ("paused", "Always-On
31
+ // off"), while the report's own `admission` field said it blocks nothing
32
+ // (OSK-15420). Say what it is in words, and which setting moves it.
33
+ if (gate.id === 'attention' && gate.verdict === 'block') {
34
+ const copy = (0, data_provider_1.describeSuppression)(gate.reason);
35
+ const lines = [
36
+ ` ${'info'.padEnd(5)} ${gate.label} — ${copy.title.toLowerCase()}${verbose && gate.reason ? ` (${gate.reason})` : ''}`,
37
+ ` ${copy.detail}`,
38
+ ];
39
+ if (copy.settingKey)
40
+ lines.push(` Setting: ${copy.settingKey}`);
41
+ return lines;
42
+ }
26
43
  const mark = gate.verdict === 'pass' ? 'ok' : gate.verdict === 'block' ? 'BLOCK' : 'warn';
27
44
  // A BLOCKING gate always states its reason. `ok` gates got a helpful `note`
28
45
  // and `warn` gates got a reason token, and the BLOCK gates — the only ones
@@ -176,7 +193,7 @@ class CommitmentsPreflight extends base_command_1.BaseCommand {
176
193
  (endpoint.reason ? ` (${endpoint.reason})` : '') +
177
194
  (!endpoint.available && endpoint.detail ? ` — ${endpoint.detail}` : ''));
178
195
  }
179
- this.log(`\nAttention: ${report.attention?.allowed ? 'delivery allowed now' : `suppressed by ${report.attention?.suppressedBy || 'policy'}`}`);
196
+ this.log(`\nAttention: ${report.attention?.allowed ? 'delivery allowed now' : `held back — ${(0, data_provider_1.describeSuppression)(report.attention?.suppressedBy).title.toLowerCase()}${report.attention?.suppressedBy ? ` (${report.attention.suppressedBy})` : ''}`}`);
180
197
  if (report.verdict === 'blocked')
181
198
  process.exitCode = 1;
182
199
  }
@@ -0,0 +1,15 @@
1
+ import { BaseCommand } from '../../base-command';
2
+ /**
3
+ * Every Commitment one agent holds — system roles included — with whether its
4
+ * loop is actually installed and working: what it watches, when it next wakes,
5
+ * whether the last check was stopped, and what it ran today.
6
+ */
7
+ export default class CommitmentsRoster extends BaseCommand {
8
+ static description: string;
9
+ static examples: string[];
10
+ static flags: {
11
+ agent: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
12
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
13
+ };
14
+ run(): Promise<void>;
15
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_command_1 = require("../../base-command");
5
+ const commitments_1 = require("../../lib/commitments");
6
+ const commitment_loop_status_1 = require("../../lib/commitment-loop-status");
7
+ /**
8
+ * Every Commitment one agent holds — system roles included — with whether its
9
+ * loop is actually installed and working: what it watches, when it next wakes,
10
+ * whether the last check was stopped, and what it ran today.
11
+ */
12
+ class CommitmentsRoster extends base_command_1.BaseCommand {
13
+ static description = "List an agent's commitments with each one's loop state (healthy, blocked, paused, unarmed, degraded, inactive, unknown)";
14
+ static examples = [
15
+ '<%= config.bin %> commitments roster --agent agent_abc123',
16
+ '<%= config.bin %> commitments roster --agent agent_abc123 --json',
17
+ ];
18
+ static flags = {
19
+ agent: core_1.Flags.string({ description: 'Agent ID', required: true }),
20
+ json: core_1.Flags.boolean({ description: 'Output as JSON' }),
21
+ };
22
+ async run() {
23
+ this.requireAuth();
24
+ const { flags } = await this.parse(CommitmentsRoster);
25
+ try {
26
+ const roster = await commitments_1.commitmentApi.roster(flags.agent);
27
+ if (flags.json) {
28
+ this.log(JSON.stringify(roster, null, 2));
29
+ return;
30
+ }
31
+ for (const line of (0, commitment_loop_status_1.formatCommitmentRoster)(roster))
32
+ this.log(line);
33
+ }
34
+ catch (err) {
35
+ this.handleApiError(err);
36
+ }
37
+ }
38
+ }
39
+ exports.default = CommitmentsRoster;
@@ -49,7 +49,7 @@ class CommitmentsShow extends base_command_1.BaseCommand {
49
49
  this.log(`Commitment ${args.id} not found.`);
50
50
  return;
51
51
  }
52
- (0, commitments_1.renderCommitmentSummary)(response.commitment, (line) => this.log(line), response.latestCheck);
52
+ (0, commitments_1.renderCommitmentSummary)((0, commitments_1.withLiveAssessment)(response.commitment, response.operatingState), (line) => this.log(line), response.latestCheck);
53
53
  // Whose permissions a run uses (OSK-11975) — read from the operating state,
54
54
  // which carries the same projection preflight reports (OSK-12065).
55
55
  const autonomy = response.operatingState?.execution;
@@ -0,0 +1,17 @@
1
+ import { BaseCommand } from '../../base-command';
2
+ /**
3
+ * Is this commitment's loop installed and working? Names the loop state and
4
+ * why, the next wakes, each watched source's access, the last stop and its
5
+ * fix, and today's runs and spend.
6
+ */
7
+ export default class CommitmentsStatus extends BaseCommand {
8
+ static description: string;
9
+ static examples: string[];
10
+ static args: {
11
+ commitmentId: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
12
+ };
13
+ static flags: {
14
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
15
+ };
16
+ run(): Promise<void>;
17
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_command_1 = require("../../base-command");
5
+ const commitments_1 = require("../../lib/commitments");
6
+ const commitment_loop_status_1 = require("../../lib/commitment-loop-status");
7
+ /**
8
+ * Is this commitment's loop installed and working? Names the loop state and
9
+ * why, the next wakes, each watched source's access, the last stop and its
10
+ * fix, and today's runs and spend.
11
+ */
12
+ class CommitmentsStatus extends base_command_1.BaseCommand {
13
+ static description = "Show whether a commitment's loop is installed and working: state, next wakes, source access, last stop, cost";
14
+ static examples = [
15
+ '<%= config.bin %> commitments status obj_abc123',
16
+ '<%= config.bin %> commitments status obj_abc123 --json',
17
+ ];
18
+ static args = {
19
+ commitmentId: core_1.Args.string({ ignoreStdin: true, description: 'Commitment ID', required: true }),
20
+ };
21
+ static flags = {
22
+ json: core_1.Flags.boolean({ description: 'Output as JSON' }),
23
+ };
24
+ async run() {
25
+ this.requireAuth();
26
+ const { args, flags } = await this.parse(CommitmentsStatus);
27
+ try {
28
+ const status = await commitments_1.commitmentApi.loopStatus(args.commitmentId);
29
+ if (flags.json) {
30
+ this.log(JSON.stringify(status, null, 2));
31
+ return;
32
+ }
33
+ for (const line of (0, commitment_loop_status_1.formatCommitmentLoopStatus)(status))
34
+ this.log(line);
35
+ }
36
+ catch (err) {
37
+ this.handleApiError(err);
38
+ }
39
+ }
40
+ }
41
+ exports.default = CommitmentsStatus;
@@ -320,6 +320,18 @@ JSON
320
320
  cadence.mode = flags['cadence-mode'];
321
321
  if (flags.timezone)
322
322
  cadence.timezone = flags.timezone;
323
+ // A daily cadence runs at set times and has no interval, so `--cadence 6h`
324
+ // on one was stored as an `intervalMs` nothing reads: the command succeeded,
325
+ // the version moved, and the cadence still read "daily 08:00". Refuse it and
326
+ // say what would do what was meant (OSK-15418).
327
+ if (flags.cadence && cadence.mode === 'daily') {
328
+ const at = Array.isArray(cadence.times) && cadence.times.length
329
+ ? ` at ${cadence.times.join(', ')}`
330
+ : '';
331
+ this.error(`This commitment's cadence is daily${at}, which has no interval, so --cadence ${flags.cadence} would change nothing. ` +
332
+ `To check every ${flags.cadence} instead, add --cadence-mode interval. ` +
333
+ 'To change the daily times, use --from-json with cadence.times.', { exit: 1 });
334
+ }
323
335
  // A mode change must not carry the previous mode's fields into the
324
336
  // replacement. Keep explicitly authored fields so invalid combinations
325
337
  // are refused rather than silently discarding what the user requested.
@@ -1,4 +1,5 @@
1
1
  import { Command } from '@oclif/core';
2
+ import { execRuntimeBinaryCapture } from '../../lib/exec-runtime-binary';
2
3
  /**
3
4
  * `skrr daemon logs` — forwards to `oversky logs`, which resolves the
4
5
  * per-profile log directory (the daemon owns that mapping, and it varies by
@@ -55,6 +56,110 @@ export declare function resolveLegacyDaemonLogPath(flags: Pick<DaemonLogsFlags,
55
56
  home?: string;
56
57
  env?: NodeJS.ProcessEnv;
57
58
  }): string;
59
+ /** The profile `logs` addresses: --profile, then the profile environment variable, then the pin. */
60
+ export declare function resolveDaemonLogsProfile(flags: Pick<DaemonLogsFlags, 'profile'>, options?: {
61
+ home?: string;
62
+ env?: NodeJS.ProcessEnv;
63
+ }): string;
64
+ /** Output destinations the installed launchd definition for a profile declares. */
65
+ export interface InstalledServiceLogDestinations {
66
+ /** The `--log-file` in ProgramArguments, when present. */
67
+ logFile: string | null;
68
+ stdoutPath: string | null;
69
+ stderrPath: string | null;
70
+ }
71
+ /**
72
+ * Read the installed launchd definition's log destinations (OSK-15993). The
73
+ * desktop supervisor starts the daemon with
74
+ * `--log-file ~/Library/Logs/Skrr/daemon.app.log` and redirects stderr to
75
+ * `daemon.stderr.log`, so the profile's `logs/` directory can hold a file
76
+ * nobody has written for a week. Read-only: the plist is parsed, never loaded.
77
+ */
78
+ export declare function installedServiceLogDestinations(profile: string, options?: {
79
+ home?: string;
80
+ platform?: NodeJS.Platform;
81
+ }): InstalledServiceLogDestinations | null;
82
+ /** The installed service's log files for the main log, or for `--stderr`. */
83
+ export declare function installedServiceLogCandidates(profile: string, stderr: boolean, options?: {
84
+ home?: string;
85
+ platform?: NodeJS.Platform;
86
+ }): string[];
87
+ /**
88
+ * Pick the file the legacy fallback reads. An old daemon cannot say which file
89
+ * it writes, so the most recently written of the profile log and the installed
90
+ * service's logs is the best evidence of the live one — and whenever that is
91
+ * not the profile log, the choice is said out loud rather than made silently.
92
+ */
93
+ export declare function chooseLegacyFallbackLog(args: {
94
+ profilePath: string;
95
+ serviceCandidates: readonly string[];
96
+ mtimeOf: (filePath: string) => number | null;
97
+ /** Why the runtime's own answer is not used; ends the note. */
98
+ reason?: string;
99
+ }): {
100
+ path: string;
101
+ note: string | null;
102
+ };
103
+ /**
104
+ * Ask the installed runtime which file `logs` will read — the same live-aware
105
+ * answer it uses itself — so the age banner describes the file actually shown.
106
+ * Null when the runtime cannot answer in time; the banner is then skipped
107
+ * rather than printed about a file that may not be the one shown.
108
+ */
109
+ export declare function resolveForwardedLogPath(flags: Pick<DaemonLogsFlags, 'stderr' | 'profile'>, capture?: typeof execRuntimeBinaryCapture): Promise<string | null>;
110
+ /**
111
+ * The first runtime whose own `logs` reads the LIVE daemon's `--log-file`
112
+ * (OSK-15993, `daemon/src/daemon-log-target.ts`). 0.8.112 through 0.8.118 have
113
+ * a `logs` subcommand, so the subcommand probe says "supported", but it reads
114
+ * the profile's `logs/daemon.log` unconditionally — with a desktop-managed
115
+ * daemon, a file nobody has written for a week. Forwarding to one of those is
116
+ * forwarding to the bug, so the CLI forwards only at or above this version.
117
+ */
118
+ export declare const LIVE_LOG_RESOLUTION_RUNTIME_VERSION = "0.8.119";
119
+ /** `x.y.z` from a version string, or null. A prerelease suffix is ignored. */
120
+ export declare function parseRuntimeVersion(text: string): [number, number, number] | null;
121
+ /** Whether `version` is at least `floor`; null when either is not a version. */
122
+ export declare function runtimeVersionAtLeast(version: string, floor: string): boolean | null;
123
+ /** The installed runtime's `--version`, or null when it cannot say in time. */
124
+ export declare function probeRuntimeVersion(capture?: typeof execRuntimeBinaryCapture): Promise<string | null>;
125
+ /**
126
+ * The live daemon's log as the runtime's `status --json` reports it
127
+ * (`service.logs`). Status has named the live `--log-file` since before `logs`
128
+ * did, so on 0.8.117 status is right while logs is wrong. Null when status
129
+ * cannot answer, or answers with anything but an absolute path.
130
+ */
131
+ export declare function probeStatusLogPath(flags: Pick<DaemonLogsFlags, 'profile'>, capture?: typeof execRuntimeBinaryCapture): Promise<string | null>;
132
+ /** What `skrr daemon logs` does: forward to the runtime, or read a file itself. */
133
+ export type DaemonLogsPlan = {
134
+ kind: 'forward';
135
+ }
136
+ /** `note` goes to stderr before any log content; never printed for `--path`. */
137
+ | {
138
+ kind: 'read';
139
+ path: string;
140
+ note: string | null;
141
+ missingHint: string;
142
+ };
143
+ export interface DaemonLogsPlanDeps {
144
+ supportsLogs(): Promise<boolean | null>;
145
+ runtimeVersion(): Promise<string | null>;
146
+ statusLogPath(): Promise<string | null>;
147
+ /** The profile log for this invocation (`daemon.err.log` under `--stderr`). */
148
+ profilePath: string;
149
+ service: InstalledServiceLogDestinations | null;
150
+ mtimeOf(filePath: string): number | null;
151
+ }
152
+ /**
153
+ * Decide which runtime, if any, `skrr daemon logs` may trust (OSK-15993).
154
+ *
155
+ * - No `logs` subcommand: the legacy fallback, as before.
156
+ * - A runtime at or above {@link LIVE_LOG_RESOLUTION_RUNTIME_VERSION}: forward.
157
+ * - An older runtime, or one whose version cannot be read: ask its `status`
158
+ * which file the live daemon writes and read that file here. When status
159
+ * cannot answer, an older runtime gets the legacy fallback (the newest of the
160
+ * profile log and the installed service's logs); an unknown one is forwarded.
161
+ */
162
+ export declare function planDaemonLogs(flags: Pick<DaemonLogsFlags, 'stderr'>, deps: DaemonLogsPlanDeps): Promise<DaemonLogsPlan>;
58
163
  /** Read a bounded trailing window, so an unrotated legacy err log is safe. */
59
164
  export declare function readDaemonLogTail(filePath: string, maxLines: number): string[];
60
165
  /**
@@ -65,7 +170,7 @@ export declare function readDaemonLogTail(filePath: string, maxLines: number): s
65
170
  * `completeTaskDurably`. Every flag declared on the command must appear here,
66
171
  * or it parses successfully and is then silently dropped.
67
172
  */
68
- export declare function buildOverskyLogsArgs(flags: DaemonLogsFlags): string[];
173
+ export declare function buildDaemonLogsArgs(flags: DaemonLogsFlags): string[];
69
174
  export default class DaemonLogs extends Command {
70
175
  static aliases: string[];
71
176
  static description: string;