@skrr-ai/cli 0.1.102 → 0.1.103

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 (47) 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/preflight.js +18 -1
  6. package/dist/commands/commitments/roster.d.ts +15 -0
  7. package/dist/commands/commitments/roster.js +39 -0
  8. package/dist/commands/commitments/show.js +1 -1
  9. package/dist/commands/commitments/status.d.ts +17 -0
  10. package/dist/commands/commitments/status.js +41 -0
  11. package/dist/commands/commitments/update.js +12 -0
  12. package/dist/commands/daemon/logs.d.ts +39 -1
  13. package/dist/commands/daemon/logs.js +140 -13
  14. package/dist/commands/followups/checkin.js +46 -3
  15. package/dist/commands/tasks/approvals/resolve.d.ts +12 -0
  16. package/dist/commands/tasks/approvals/resolve.js +21 -5
  17. package/dist/commands/triggers/create.js +8 -17
  18. package/dist/commands/triggers/fire.js +3 -0
  19. package/dist/commands/triggers/update.js +41 -27
  20. package/dist/commands/widgets/check.d.ts +39 -0
  21. package/dist/commands/widgets/check.js +179 -0
  22. package/dist/lib/agent-mailbox.d.ts +0 -1
  23. package/dist/lib/agent-mailbox.js +0 -1
  24. package/dist/lib/commitment-loop-status.d.ts +16 -0
  25. package/dist/lib/commitment-loop-status.js +164 -0
  26. package/dist/lib/commitments.d.ts +28 -1
  27. package/dist/lib/commitments.js +184 -5
  28. package/dist/lib/delegated-cli.d.ts +23 -1
  29. package/dist/lib/delegated-cli.js +61 -4
  30. package/dist/lib/first-party-harness.d.ts +1 -1
  31. package/dist/lib/first-party-harness.js +1 -1
  32. package/dist/lib/session-task-write.d.ts +13 -0
  33. package/dist/lib/session-task-write.js +63 -3
  34. package/dist/lib/triggers.d.ts +26 -0
  35. package/dist/lib/triggers.js +134 -1
  36. package/dist/lib/widgetCheck.d.ts +123 -0
  37. package/dist/lib/widgetCheck.js +998 -0
  38. package/dist/lib/widgetStaticScan.d.ts +111 -0
  39. package/dist/lib/widgetStaticScan.js +833 -0
  40. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +9 -1
  41. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +12 -0
  42. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +9 -1
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +12 -0
  44. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  45. package/dist/node_modules/@skrr-ai/data-provider/index.js +11902 -8696
  46. package/oclif.manifest.json +218 -5
  47. package/package.json +2 -2
@@ -5,8 +5,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.describeLogAge = describeLogAge;
7
7
  exports.resolveLegacyDaemonLogPath = resolveLegacyDaemonLogPath;
8
+ exports.resolveDaemonLogsProfile = resolveDaemonLogsProfile;
9
+ exports.installedServiceLogCandidates = installedServiceLogCandidates;
10
+ exports.chooseLegacyFallbackLog = chooseLegacyFallbackLog;
11
+ exports.resolveForwardedLogPath = resolveForwardedLogPath;
8
12
  exports.readDaemonLogTail = readDaemonLogTail;
9
- exports.buildOverskyLogsArgs = buildOverskyLogsArgs;
13
+ exports.buildDaemonLogsArgs = buildDaemonLogsArgs;
10
14
  const core_1 = require("@oclif/core");
11
15
  const node_fs_1 = require("node:fs");
12
16
  const node_os_1 = require("node:os");
@@ -62,6 +66,11 @@ function describeLogAge(mtimeMs, now = Date.now()) {
62
66
  };
63
67
  }
64
68
  function resolveLegacyDaemonLogPath(flags, options = {}) {
69
+ const home = options.home ?? (0, node_os_1.homedir)();
70
+ return node_path_1.default.join(home, '.skrr', 'profiles', resolveDaemonLogsProfile(flags, options), 'logs', flags.stderr ? 'daemon.err.log' : 'daemon.log');
71
+ }
72
+ /** The profile `logs` addresses: --profile, then the profile environment variable, then the pin. */
73
+ function resolveDaemonLogsProfile(flags, options = {}) {
65
74
  const home = options.home ?? (0, node_os_1.homedir)();
66
75
  const env = options.env ?? process.env;
67
76
  // An explicit --profile outranks the environment, as it does everywhere else.
@@ -81,7 +90,106 @@ function resolveLegacyDaemonLogPath(flags, options = {}) {
81
90
  // An absent/corrupt sticky selector must match the daemon's default.
82
91
  }
83
92
  }
84
- return node_path_1.default.join(home, '.skrr', 'profiles', profile ?? 'default', 'logs', flags.stderr ? 'daemon.err.log' : 'daemon.log');
93
+ return profile ?? 'default';
94
+ }
95
+ /**
96
+ * Log files the installed launchd definition for this profile sends output to
97
+ * (OSK-15993). The desktop supervisor starts the daemon with
98
+ * `--log-file ~/Library/Logs/Skrr/daemon.app.log` and redirects stderr to
99
+ * `daemon.stderr.log`, so the profile's `logs/` directory can hold a file
100
+ * nobody has written for a week. Read-only: the plist is parsed, never loaded.
101
+ */
102
+ function installedServiceLogCandidates(profile, stderr, options = {}) {
103
+ if ((options.platform ?? process.platform) !== 'darwin')
104
+ return [];
105
+ const plist = node_path_1.default.join(options.home ?? (0, node_os_1.homedir)(), 'Library', 'LaunchAgents', `ai.skrr.daemon.${profile}.plist`);
106
+ let xml;
107
+ try {
108
+ xml = (0, node_fs_1.readFileSync)(plist, 'utf8');
109
+ }
110
+ catch {
111
+ return [];
112
+ }
113
+ const unescape = (v) => v
114
+ .replace(/&lt;/g, '<')
115
+ .replace(/&gt;/g, '>')
116
+ .replace(/&quot;/g, '"')
117
+ .replace(/&apos;/g, "'")
118
+ .replace(/&amp;/g, '&');
119
+ const stringFor = (key) => {
120
+ const m = new RegExp(`<key>\\s*${key}\\s*</key>\\s*<string>([\\s\\S]*?)</string>`).exec(xml);
121
+ return m?.[1]?.trim() ? unescape(m[1].trim()) : null;
122
+ };
123
+ if (stderr) {
124
+ const errPath = stringFor('StandardErrorPath');
125
+ return errPath ? [errPath] : [];
126
+ }
127
+ const out = [];
128
+ const array = /<key>\s*ProgramArguments\s*<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(xml);
129
+ if (array?.[1]) {
130
+ const args = [...array[1].matchAll(/<string>([\s\S]*?)<\/string>/g)].map((m) => unescape(m[1].trim()));
131
+ const at = args.lastIndexOf('--log-file');
132
+ if (at >= 0 && args[at + 1] && !args[at + 1].startsWith('-'))
133
+ out.push(args[at + 1]);
134
+ }
135
+ const stdoutPath = stringFor('StandardOutPath');
136
+ if (stdoutPath)
137
+ out.push(stdoutPath);
138
+ return out;
139
+ }
140
+ /**
141
+ * Pick the file the legacy fallback reads. An old daemon cannot say which file
142
+ * it writes, so the most recently written of the profile log and the installed
143
+ * service's logs is the best evidence of the live one — and whenever that is
144
+ * not the profile log, the choice is said out loud rather than made silently.
145
+ */
146
+ function chooseLegacyFallbackLog(args) {
147
+ const profileMtime = args.mtimeOf(args.profilePath);
148
+ let best = null;
149
+ for (const file of args.serviceCandidates) {
150
+ if (node_path_1.default.resolve(file) === node_path_1.default.resolve(args.profilePath))
151
+ continue;
152
+ const mtime = args.mtimeOf(file);
153
+ if (mtime === null)
154
+ continue;
155
+ if (!best || mtime > best.mtime)
156
+ best = { file, mtime };
157
+ }
158
+ if (!best || (profileMtime !== null && profileMtime >= best.mtime)) {
159
+ return { path: args.profilePath, note: null };
160
+ }
161
+ return {
162
+ path: best.file,
163
+ note: `Showing ${best.file}, the installed service's log: it was written more recently than ` +
164
+ (profileMtime === null
165
+ ? `the profile log ${args.profilePath}, which does not exist.`
166
+ : `the profile log ${args.profilePath}, which has not been written since.`) +
167
+ ' This daemon predates `logs`, so it cannot name its live log itself.',
168
+ };
169
+ }
170
+ /**
171
+ * Ask the installed runtime which file `logs` will read — the same live-aware
172
+ * answer it uses itself — so the age banner describes the file actually shown.
173
+ * Null when the runtime cannot answer in time; the banner is then skipped
174
+ * rather than printed about a file that may not be the one shown.
175
+ */
176
+ async function resolveForwardedLogPath(flags, capture = exec_runtime_binary_1.execRuntimeBinaryCapture) {
177
+ try {
178
+ const out = await capture(buildDaemonLogsArgs({ ...flags, path: true }), {
179
+ timeoutMs: 5_000,
180
+ });
181
+ if (out.code !== 0)
182
+ return null;
183
+ const lines = out.stdout
184
+ .split('\n')
185
+ .map((l) => l.trim())
186
+ .filter(Boolean);
187
+ const last = lines.at(-1);
188
+ return last && node_path_1.default.isAbsolute(last) ? last : null;
189
+ }
190
+ catch {
191
+ return null;
192
+ }
85
193
  }
86
194
  /** Read a bounded trailing window, so an unrotated legacy err log is safe. */
87
195
  function readDaemonLogTail(filePath, maxLines) {
@@ -114,7 +222,7 @@ function readDaemonLogTail(filePath, maxLines) {
114
222
  * `completeTaskDurably`. Every flag declared on the command must appear here,
115
223
  * or it parses successfully and is then silently dropped.
116
224
  */
117
- function buildOverskyLogsArgs(flags) {
225
+ function buildDaemonLogsArgs(flags) {
118
226
  return [
119
227
  // The runtime's own global option, placed before the subcommand.
120
228
  ...(flags.profile ? ['--profile', flags.profile] : []),
@@ -174,31 +282,46 @@ class DaemonLogs extends core_1.Command {
174
282
  ];
175
283
  async run() {
176
284
  const { flags } = await this.parse(DaemonLogs);
177
- const args = buildOverskyLogsArgs(flags);
178
- // Before ANY log content, on stderr so it never pollutes a pipe. `--path`
179
- // is exempt: its whole contract is one path on stdout.
180
- if (!flags.path) {
285
+ const args = buildDaemonLogsArgs(flags);
286
+ const showAge = (filePath) => {
287
+ // Before ANY log content, on stderr so it never pollutes a pipe. `--path`
288
+ // is exempt: its whole contract is one path on stdout.
289
+ if (flags.path || !filePath)
290
+ return;
181
291
  try {
182
- const candidate = resolveLegacyDaemonLogPath(flags);
183
- if ((0, node_fs_1.existsSync)(candidate)) {
184
- const { line } = describeLogAge((0, node_fs_1.statSync)(candidate).mtimeMs);
185
- this.logToStderr(line);
292
+ if ((0, node_fs_1.existsSync)(filePath)) {
293
+ this.logToStderr(`${filePath}: ${describeLogAge((0, node_fs_1.statSync)(filePath).mtimeMs).line}`);
186
294
  }
187
295
  }
188
296
  catch {
189
297
  // Never let the banner stop the logs from being shown.
190
298
  }
191
- }
299
+ };
192
300
  try {
193
301
  // `oversky 0.2.x` has profiles and writes the same files, but has no
194
302
  // `logs` subcommand. Probe top-level help first so forwarding does not
195
303
  // print an unrelated "unknown option" error before the useful output.
196
304
  if ((await (0, exec_runtime_binary_1.runtimeBinarySupportsSubcommand)('logs')) === false) {
197
- const logPath = resolveLegacyDaemonLogPath(flags);
305
+ const chosen = chooseLegacyFallbackLog({
306
+ profilePath: resolveLegacyDaemonLogPath(flags),
307
+ serviceCandidates: installedServiceLogCandidates(resolveDaemonLogsProfile(flags), flags.stderr === true),
308
+ mtimeOf: (filePath) => {
309
+ try {
310
+ return (0, node_fs_1.statSync)(filePath).mtimeMs;
311
+ }
312
+ catch {
313
+ return null;
314
+ }
315
+ },
316
+ });
317
+ const logPath = chosen.path;
318
+ if (chosen.note)
319
+ this.logToStderr(chosen.note);
198
320
  if (flags.path) {
199
321
  this.log(logPath);
200
322
  return;
201
323
  }
324
+ showAge(logPath);
202
325
  if (!(0, node_fs_1.existsSync)(logPath)) {
203
326
  this.error(`No daemon log at ${logPath}. The installed daemon predates \`oversky logs\`; start it or upgrade it, then retry.`, { exit: 1 });
204
327
  }
@@ -234,6 +357,10 @@ class DaemonLogs extends core_1.Command {
234
357
  }
235
358
  return;
236
359
  }
360
+ // The banner must describe the file the runtime will actually print —
361
+ // the live daemon's log (OSK-15993) — not the profile default.
362
+ if (!flags.path)
363
+ showAge(await resolveForwardedLogPath(flags));
237
364
  const code = await (0, exec_runtime_binary_1.execRuntimeBinary)(args);
238
365
  this.exit(code);
239
366
  }
@@ -99,18 +99,32 @@ class FollowupsCheckin extends base_command_1.BaseCommand {
99
99
  this.handleApiError(err);
100
100
  return;
101
101
  }
102
+ const producer = agent.boundToSession ? 'agent_in_turn' : 'user_explicit';
103
+ // An explicit request from a person that arms nothing is a refusal, and a script reading the
104
+ // exit code must see it as one (OSK-15413). A check-in the AGENT chose to make is different:
105
+ // there, staying quiet when the budget is spent is the designed outcome.
106
+ if (response.dropped && producer === 'user_explicit') {
107
+ this.failWithCliError({
108
+ message: budgetRefusalMessage(agent.id, response.budget),
109
+ code: 'FOLLOWUP_BUDGET_EXHAUSTED',
110
+ exit: 1,
111
+ retryable: false,
112
+ details: { budget: response.budget ?? null, response },
113
+ });
114
+ return;
115
+ }
102
116
  if (flags.json) {
103
117
  this.log(JSON.stringify(response, null, 2));
104
118
  return;
105
119
  }
106
120
  const row = response.followup;
107
121
  if (response.reused) {
108
- this.log(`Already armed: ${row?.id} — one pending check-in per conversation, and this is it.`);
122
+ this.log(reusedMessage(row, subject, flags.why));
109
123
  return;
110
124
  }
111
125
  if (response.dropped) {
112
- // silent_drop is the check-in budget's contract: the answer to "one
113
- // greeting too many" is quiet, not an error the caller can retry.
126
+ // silent_drop is the check-in budget's contract for an agent's own check-in: the answer to
127
+ // "one greeting too many" is quiet, not an error the caller can retry.
114
128
  this.log('Dropped by the daily check-in budget — staying quiet is the correct outcome.');
115
129
  return;
116
130
  }
@@ -118,3 +132,32 @@ class FollowupsCheckin extends base_command_1.BaseCommand {
118
132
  }
119
133
  }
120
134
  exports.default = FollowupsCheckin;
135
+ /**
136
+ * What a reused answer means for the person who asked: a second check-in on a subject that already
137
+ * has one arms nothing and changes nothing, so the note and time they typed were NOT applied. Say
138
+ * so, show what is armed, and name the two ways forward (OSK-15411). Exit stays 0: the follow-up
139
+ * they wanted to exist does exist.
140
+ */
141
+ function reusedMessage(row, subject, requestedWhy) {
142
+ const lines = [
143
+ `Already armed: ${row?.id} — one pending check-in per subject (${subject.type}:${subject.externalKey}), and this is it.`,
144
+ 'Nothing in this request was applied: it armed no new check-in and changed neither the note nor the time.',
145
+ ];
146
+ if (row) {
147
+ const sameNote = row.why === requestedWhy;
148
+ lines.push(`Armed: ${row.why ? `"${row.why}"` : '(no note)'}, next ${row.nextRunAt ?? 'when its schedule says'}${sameNote ? ' (the same note you gave)' : ''}.`);
149
+ }
150
+ lines.push(`To change the time: skrr followups reschedule ${row?.id ?? '<id>'} --at <instant> or --in <offset>.`, "To change the note: cancel it and create it again (a cancelled check-in still counts against today's check-in budget).");
151
+ return lines.join('\n');
152
+ }
153
+ /** The refusal for an explicit check-in the daily budget will not take, with the number and the way to raise it. */
154
+ function budgetRefusalMessage(agentId, budget) {
155
+ const { used, limit } = (budget && typeof budget === 'object' ? budget : {});
156
+ const spent = typeof used === 'number' && typeof limit === 'number'
157
+ ? `${agentId} has used ${used} of its ${limit} check-ins today`
158
+ : `${agentId} has used its daily check-in budget`;
159
+ return [
160
+ `No check-in was armed: ${spent}. The day resets at 00:00 UTC, and a cancelled check-in still counts.`,
161
+ `Raise the limit with: skrr agents triggers policy ${agentId} --followup-budget checkin=<count>`,
162
+ ].join(' ');
163
+ }
@@ -1,4 +1,16 @@
1
+ import { type TaskApprovalResolveParams } from '@skrr-ai/data-provider';
1
2
  import { BaseCommand } from '../../../base-command';
3
+ /**
4
+ * The request body for resolving an approval. The server's field is `comment`
5
+ * (`TaskApprovalResolveParams`); `--reason` is only the flag's name. It was sent
6
+ * as `reason` through an `as unknown as` cast, so the type checker could not see
7
+ * the mismatch and the server's non-strict schema dropped the key: the command
8
+ * succeeded and the reason was stored nowhere (OSK-15362).
9
+ */
10
+ export declare function approvalResolutionBody(flags: {
11
+ status: 'approved' | 'rejected';
12
+ reason?: string;
13
+ }): TaskApprovalResolveParams;
2
14
  export default class TasksApprovalsResolve extends BaseCommand {
3
15
  static description: string;
4
16
  static examples: string[];
@@ -1,8 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.approvalResolutionBody = approvalResolutionBody;
3
4
  const core_1 = require("@oclif/core");
4
5
  const data_provider_1 = require("@skrr-ai/data-provider");
5
6
  const base_command_1 = require("../../../base-command");
7
+ /**
8
+ * The request body for resolving an approval. The server's field is `comment`
9
+ * (`TaskApprovalResolveParams`); `--reason` is only the flag's name. It was sent
10
+ * as `reason` through an `as unknown as` cast, so the type checker could not see
11
+ * the mismatch and the server's non-strict schema dropped the key: the command
12
+ * succeeded and the reason was stored nowhere (OSK-15362).
13
+ */
14
+ function approvalResolutionBody(flags) {
15
+ const body = { status: flags.status };
16
+ if (flags.reason)
17
+ body.comment = flags.reason;
18
+ return body;
19
+ }
6
20
  class TasksApprovalsResolve extends base_command_1.BaseCommand {
7
21
  static description = 'Resolve (approve or reject) a task approval';
8
22
  static examples = [
@@ -27,15 +41,15 @@ class TasksApprovalsResolve extends base_command_1.BaseCommand {
27
41
  this.requireAuth();
28
42
  const { args, flags } = await this.parse(TasksApprovalsResolve);
29
43
  const taskRef = await this.resolveEntityRef('task', args.id);
30
- const data = { status: flags.status };
31
- if (flags.reason)
32
- data.reason = flags.reason;
33
44
  let result;
34
45
  try {
35
46
  result = await data_provider_1.dataService.resolveTaskApproval({
36
47
  taskId: taskRef,
37
48
  approvalId: args.approvalId,
38
- data: data,
49
+ data: approvalResolutionBody({
50
+ status: flags.status,
51
+ reason: flags.reason,
52
+ }),
39
53
  });
40
54
  }
41
55
  catch (err) {
@@ -46,7 +60,9 @@ class TasksApprovalsResolve extends base_command_1.BaseCommand {
46
60
  return;
47
61
  }
48
62
  const a = result;
49
- this.log(`Approval ${a.id ?? a._id ?? args.approvalId} → ${flags.status}`);
63
+ // Say what was recorded, so a reason that was not stored cannot pass unseen.
64
+ const recorded = typeof a.comment === 'string' && a.comment ? ` (reason: ${a.comment})` : '';
65
+ this.log(`Approval ${a.id ?? a._id ?? args.approvalId} → ${flags.status}${recorded}`);
50
66
  }
51
67
  }
52
68
  exports.default = TasksApprovalsResolve;
@@ -91,7 +91,9 @@ class TriggersCreate extends base_command_1.BaseCommand {
91
91
  multiple: true,
92
92
  }),
93
93
  timezone: core_1.Flags.string({ description: 'IANA timezone for schedule triggers' }),
94
- 'max-fires-per-day': core_1.Flags.integer({ description: 'Daily fire cap' }),
94
+ 'max-fires-per-day': core_1.Flags.integer({
95
+ description: 'Daily fire cap (schedule, stochastic and self_scheduled triggers only)',
96
+ }),
95
97
  'max-runs': core_1.Flags.integer({ description: 'Lifetime fire cap for schedule triggers' }),
96
98
  'end-at': core_1.Flags.string({
97
99
  description: 'ISO-8601 datetime after which a schedule stops firing',
@@ -192,6 +194,9 @@ class TriggersCreate extends base_command_1.BaseCommand {
192
194
  let payload;
193
195
  try {
194
196
  payload = await (0, prompt_1.readJsonBody)(flags['from-json']);
197
+ // A flag for a kind other than --kind is refused, not dropped: the block it fills is not
198
+ // sent, so the caller would believe a limit was set that nothing holds (OSK-15491).
199
+ (0, triggers_1.assertKindFlagsApply)(flags.kind ?? payload.kind, flags);
195
200
  const refs = (0, triggers_1.parseKeyValueFlags)(flags.ref, '--ref', flags.scope ?? payload.scope?.kind);
196
201
  if (flags.scope || Object.keys(refs).length > 0) {
197
202
  payload.scope = {
@@ -267,22 +272,8 @@ class TriggersCreate extends base_command_1.BaseCommand {
267
272
  filterField: flags['filter-field'],
268
273
  filterValues,
269
274
  });
270
- (0, triggers_1.setDefined)(event, {
271
- entityType: flags.entity,
272
- eventType: flags.event,
273
- filterMode: flags['filter-mode'],
274
- filterField: flags['filter-field'],
275
- filterValue: filterValues && filterValues.length > 1 ? filterValues : filterValues?.[0],
276
- debounceMs: flags['debounce-ms'],
277
- maxPerHour: flags['max-per-hour'],
278
- });
279
- const stochastic = {};
280
- (0, triggers_1.setDefined)(stochastic, {
281
- probability: flags.probability === undefined ? undefined : Number(flags.probability),
282
- jitterMs: flags['jitter-ms'],
283
- maxFiresPerDay: flags['max-fires-per-day'],
284
- evalIntervalMinutes: flags['eval-interval-minutes'],
285
- });
275
+ Object.assign(event, (0, triggers_1.buildEventConfigFromFlags)(flags, filterValues));
276
+ const stochastic = (0, triggers_1.buildStochasticConfigFromFlags)(flags);
286
277
  const selfScheduled = (0, triggers_1.buildSelfScheduledConfigFromFlags)(flags);
287
278
  const form = (0, triggers_1.buildFormConfigFromFlags)(flags);
288
279
  const chatMessage = (0, triggers_1.buildChatMessageConfigFromFlags)(flags);
@@ -41,6 +41,9 @@ class TriggersFire extends base_command_1.BaseCommand {
41
41
  return;
42
42
  }
43
43
  this.log(`Fire requested: outcome=${String(result.outcome ?? '-')} reason=${String(result.reason ?? '-')}`);
44
+ const note = (0, triggers_1.describeScheduledFire)(result);
45
+ if (note)
46
+ this.log(note);
44
47
  }
45
48
  }
46
49
  exports.default = TriggersFire;
@@ -48,7 +48,9 @@ class TriggersUpdate extends base_command_1.BaseCommand {
48
48
  multiple: true,
49
49
  }),
50
50
  timezone: core_1.Flags.string({ description: 'IANA timezone for schedule triggers' }),
51
- 'max-fires-per-day': core_1.Flags.integer({ description: 'Daily fire cap' }),
51
+ 'max-fires-per-day': core_1.Flags.integer({
52
+ description: 'Daily fire cap (schedule, stochastic and self_scheduled triggers only)',
53
+ }),
52
54
  'max-runs': core_1.Flags.integer({ description: 'Lifetime fire cap for schedule triggers' }),
53
55
  'end-at': core_1.Flags.string({
54
56
  description: 'ISO-8601 datetime after which a schedule stops firing',
@@ -150,6 +152,29 @@ class TriggersUpdate extends base_command_1.BaseCommand {
150
152
  throw new Error('Trigger kind cannot be changed after creation. ' +
151
153
  'Remove `kind` from --from-json, or delete and recreate the trigger.');
152
154
  }
155
+ }
156
+ catch (err) {
157
+ this.error(err.message, { exit: 1 });
158
+ }
159
+ // The kind is the server's to say and cannot change, so the flags are checked against it and
160
+ // only the config block it owns is sent. Every block used to go: `--max-fires-per-day` wrote
161
+ // `stochastic` and `selfScheduled` onto a schedule trigger, and onto an on_demand one a cap
162
+ // nothing reads (OSK-15491).
163
+ let existing;
164
+ try {
165
+ existing = await triggers_1.triggerApi.get(args.id);
166
+ }
167
+ catch (err) {
168
+ this.handleApiError(err);
169
+ }
170
+ const existingKind = existing?.kind;
171
+ if (typeof existingKind !== 'string') {
172
+ this.error(`Trigger ${args.id} did not report its kind, so its settings cannot be checked.`, {
173
+ exit: 1,
174
+ });
175
+ }
176
+ try {
177
+ (0, triggers_1.assertKindFlagsApply)(existingKind, flags, { kindIsFixed: true });
153
178
  const execution = {};
154
179
  const builtinAction = (0, triggers_1.buildBuiltinActionPayload)({
155
180
  builtinAction: flags['builtin-action'],
@@ -169,7 +194,10 @@ class TriggersUpdate extends base_command_1.BaseCommand {
169
194
  });
170
195
  const schedule = (0, triggers_1.buildScheduleConfigFromFlags)(flags);
171
196
  (0, triggers_1.assertScheduleTimingShape)(schedule);
172
- const once = (0, triggers_1.buildOnceConfigFromFlags)(flags);
197
+ const once = (0, triggers_1.buildOnceConfigFromFlags)(flags, {
198
+ kind: existingKind,
199
+ allowNextRunAtAlias: true,
200
+ });
173
201
  const filterValues = (0, triggers_1.splitRepeated)(flags['filter-value']);
174
202
  // Consult the authoritative server catalog before warning about a local
175
203
  // snapshot miss; server validation remains final when it is unavailable.
@@ -188,22 +216,8 @@ class TriggersUpdate extends base_command_1.BaseCommand {
188
216
  filterValues,
189
217
  });
190
218
  const event = {};
191
- (0, triggers_1.setDefined)(event, {
192
- entityType: flags.entity,
193
- eventType: flags.event,
194
- filterMode: flags['filter-mode'],
195
- filterField: flags['filter-field'],
196
- filterValue: filterValues && filterValues.length > 1 ? filterValues : filterValues?.[0],
197
- debounceMs: flags['debounce-ms'],
198
- maxPerHour: flags['max-per-hour'],
199
- });
200
- const stochastic = {};
201
- (0, triggers_1.setDefined)(stochastic, {
202
- probability: flags.probability === undefined ? undefined : Number(flags.probability),
203
- jitterMs: flags['jitter-ms'],
204
- maxFiresPerDay: flags['max-fires-per-day'],
205
- evalIntervalMinutes: flags['eval-interval-minutes'],
206
- });
219
+ Object.assign(event, (0, triggers_1.buildEventConfigFromFlags)(flags, filterValues));
220
+ const stochastic = (0, triggers_1.buildStochasticConfigFromFlags)(flags);
207
221
  const selfScheduled = (0, triggers_1.buildSelfScheduledConfigFromFlags)(flags);
208
222
  const form = (0, triggers_1.buildFormConfigFromFlags)(flags);
209
223
  const chatMessage = (0, triggers_1.buildChatMessageConfigFromFlags)(flags);
@@ -215,15 +229,15 @@ class TriggersUpdate extends base_command_1.BaseCommand {
215
229
  idempotencyWindow: flags['idempotency-window'],
216
230
  });
217
231
  (0, triggers_1.mergeObjectField)(payload, 'execution', execution);
218
- (0, triggers_1.mergeObjectField)(payload, 'schedule', schedule);
219
- (0, triggers_1.mergeObjectField)(payload, 'once', once);
220
- (0, triggers_1.mergeObjectField)(payload, 'event', event);
221
- // `kind` is immutable on update (rejected above), so there is no kind to
222
- // gate on here; stochastic is only non-empty when stochastic flags were passed.
223
- (0, triggers_1.mergeObjectField)(payload, 'stochastic', stochastic);
224
- (0, triggers_1.mergeObjectField)(payload, 'selfScheduled', selfScheduled);
225
- (0, triggers_1.mergeObjectField)(payload, 'form', form);
226
- (0, triggers_1.mergeObjectField)(payload, 'chatMessage', chatMessage);
232
+ (0, triggers_1.mergeTriggerKindConfig)(payload, existingKind, {
233
+ schedule,
234
+ once,
235
+ event,
236
+ stochastic,
237
+ selfScheduled,
238
+ form,
239
+ chatMessage,
240
+ });
227
241
  }
228
242
  catch (err) {
229
243
  this.error(err.message, { exit: 1 });
@@ -0,0 +1,39 @@
1
+ import { Command } from '@oclif/core';
2
+ /**
3
+ * `skrr widgets check` — the offline self-check a harness runs in Bash BEFORE it
4
+ * replies, because a rejected widget is only discovered after the message is sent.
5
+ *
6
+ * It extends `Command`, not `BaseCommand`, on purpose: it reads a file or stdin and
7
+ * prints, so it must work with no sign-in, no network and no daemon credential
8
+ * (a sandboxed harness has none, and `BaseCommand.init()` resolves one before any
9
+ * command body runs). The same reason `instructions guide` does it.
10
+ *
11
+ * It reaches NO API. There is no route behind it, so there is nothing for the
12
+ * CLI/API parity spec (`domain-api-parity.spec.ts`) to hold it to: it adds nothing
13
+ * to `api-endpoints.ts` or `dataService`, and it is safe to run anywhere.
14
+ *
15
+ * Exit codes: 0 every widget would be accepted and has no error finding; 1 at
16
+ * least one would be refused or has an error finding (or none was found); 2 the
17
+ * input could not be read.
18
+ */
19
+ export declare const WIDGETS_CHECK_EXIT: {
20
+ readonly ok: 0;
21
+ readonly notOk: 1;
22
+ readonly unreadable: 2;
23
+ };
24
+ export default class WidgetsCheck extends Command {
25
+ static description: string;
26
+ static examples: string[];
27
+ static args: {
28
+ file: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
29
+ };
30
+ static flags: {
31
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
32
+ format: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
33
+ strict: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
34
+ 'render-fence': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
35
+ };
36
+ run(): Promise<void>;
37
+ private readInput;
38
+ private emitInputError;
39
+ }