@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
@@ -3,10 +3,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.LIVE_LOG_RESOLUTION_RUNTIME_VERSION = void 0;
6
7
  exports.describeLogAge = describeLogAge;
7
8
  exports.resolveLegacyDaemonLogPath = resolveLegacyDaemonLogPath;
9
+ exports.resolveDaemonLogsProfile = resolveDaemonLogsProfile;
10
+ exports.installedServiceLogDestinations = installedServiceLogDestinations;
11
+ exports.installedServiceLogCandidates = installedServiceLogCandidates;
12
+ exports.chooseLegacyFallbackLog = chooseLegacyFallbackLog;
13
+ exports.resolveForwardedLogPath = resolveForwardedLogPath;
14
+ exports.parseRuntimeVersion = parseRuntimeVersion;
15
+ exports.runtimeVersionAtLeast = runtimeVersionAtLeast;
16
+ exports.probeRuntimeVersion = probeRuntimeVersion;
17
+ exports.probeStatusLogPath = probeStatusLogPath;
18
+ exports.planDaemonLogs = planDaemonLogs;
8
19
  exports.readDaemonLogTail = readDaemonLogTail;
9
- exports.buildOverskyLogsArgs = buildOverskyLogsArgs;
20
+ exports.buildDaemonLogsArgs = buildDaemonLogsArgs;
10
21
  const core_1 = require("@oclif/core");
11
22
  const node_fs_1 = require("node:fs");
12
23
  const node_os_1 = require("node:os");
@@ -62,6 +73,11 @@ function describeLogAge(mtimeMs, now = Date.now()) {
62
73
  };
63
74
  }
64
75
  function resolveLegacyDaemonLogPath(flags, options = {}) {
76
+ const home = options.home ?? (0, node_os_1.homedir)();
77
+ return node_path_1.default.join(home, '.skrr', 'profiles', resolveDaemonLogsProfile(flags, options), 'logs', flags.stderr ? 'daemon.err.log' : 'daemon.log');
78
+ }
79
+ /** The profile `logs` addresses: --profile, then the profile environment variable, then the pin. */
80
+ function resolveDaemonLogsProfile(flags, options = {}) {
65
81
  const home = options.home ?? (0, node_os_1.homedir)();
66
82
  const env = options.env ?? process.env;
67
83
  // An explicit --profile outranks the environment, as it does everywhere else.
@@ -81,7 +97,259 @@ function resolveLegacyDaemonLogPath(flags, options = {}) {
81
97
  // An absent/corrupt sticky selector must match the daemon's default.
82
98
  }
83
99
  }
84
- return node_path_1.default.join(home, '.skrr', 'profiles', profile ?? 'default', 'logs', flags.stderr ? 'daemon.err.log' : 'daemon.log');
100
+ return profile ?? 'default';
101
+ }
102
+ /**
103
+ * Read the installed launchd definition's log destinations (OSK-15993). The
104
+ * desktop supervisor starts the daemon with
105
+ * `--log-file ~/Library/Logs/Skrr/daemon.app.log` and redirects stderr to
106
+ * `daemon.stderr.log`, so the profile's `logs/` directory can hold a file
107
+ * nobody has written for a week. Read-only: the plist is parsed, never loaded.
108
+ */
109
+ function installedServiceLogDestinations(profile, options = {}) {
110
+ if ((options.platform ?? process.platform) !== 'darwin')
111
+ return null;
112
+ const plist = node_path_1.default.join(options.home ?? (0, node_os_1.homedir)(), 'Library', 'LaunchAgents', `ai.skrr.daemon.${profile}.plist`);
113
+ let xml;
114
+ try {
115
+ xml = (0, node_fs_1.readFileSync)(plist, 'utf8');
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ const unescape = (v) => v
121
+ .replace(/&lt;/g, '<')
122
+ .replace(/&gt;/g, '>')
123
+ .replace(/&quot;/g, '"')
124
+ .replace(/&apos;/g, "'")
125
+ .replace(/&amp;/g, '&');
126
+ const stringFor = (key) => {
127
+ const m = new RegExp(`<key>\\s*${key}\\s*</key>\\s*<string>([\\s\\S]*?)</string>`).exec(xml);
128
+ return m?.[1]?.trim() ? unescape(m[1].trim()) : null;
129
+ };
130
+ let logFile = null;
131
+ const array = /<key>\s*ProgramArguments\s*<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(xml);
132
+ if (array?.[1]) {
133
+ const args = [...array[1].matchAll(/<string>([\s\S]*?)<\/string>/g)].map((m) => unescape(m[1].trim()));
134
+ const at = args.lastIndexOf('--log-file');
135
+ if (at >= 0 && args[at + 1] && !args[at + 1].startsWith('-'))
136
+ logFile = args[at + 1];
137
+ }
138
+ return {
139
+ logFile,
140
+ stdoutPath: stringFor('StandardOutPath'),
141
+ stderrPath: stringFor('StandardErrorPath'),
142
+ };
143
+ }
144
+ /** The installed service's log files for the main log, or for `--stderr`. */
145
+ function installedServiceLogCandidates(profile, stderr, options = {}) {
146
+ const dest = installedServiceLogDestinations(profile, options);
147
+ if (!dest)
148
+ return [];
149
+ if (stderr)
150
+ return dest.stderrPath ? [dest.stderrPath] : [];
151
+ return [dest.logFile, dest.stdoutPath].filter((p) => Boolean(p));
152
+ }
153
+ /**
154
+ * Pick the file the legacy fallback reads. An old daemon cannot say which file
155
+ * it writes, so the most recently written of the profile log and the installed
156
+ * service's logs is the best evidence of the live one — and whenever that is
157
+ * not the profile log, the choice is said out loud rather than made silently.
158
+ */
159
+ function chooseLegacyFallbackLog(args) {
160
+ const profileMtime = args.mtimeOf(args.profilePath);
161
+ let best = null;
162
+ for (const file of args.serviceCandidates) {
163
+ if (node_path_1.default.resolve(file) === node_path_1.default.resolve(args.profilePath))
164
+ continue;
165
+ const mtime = args.mtimeOf(file);
166
+ if (mtime === null)
167
+ continue;
168
+ if (!best || mtime > best.mtime)
169
+ best = { file, mtime };
170
+ }
171
+ if (!best || (profileMtime !== null && profileMtime >= best.mtime)) {
172
+ return { path: args.profilePath, note: null };
173
+ }
174
+ return {
175
+ path: best.file,
176
+ note: `Showing ${best.file}, the installed service's log: it was written more recently than ` +
177
+ (profileMtime === null
178
+ ? `the profile log ${args.profilePath}, which does not exist.`
179
+ : `the profile log ${args.profilePath}, which has not been written since.`) +
180
+ ` ${args.reason ?? 'This daemon predates `logs`, so it cannot name its live log itself.'}`,
181
+ };
182
+ }
183
+ /**
184
+ * Ask the installed runtime which file `logs` will read — the same live-aware
185
+ * answer it uses itself — so the age banner describes the file actually shown.
186
+ * Null when the runtime cannot answer in time; the banner is then skipped
187
+ * rather than printed about a file that may not be the one shown.
188
+ */
189
+ async function resolveForwardedLogPath(flags, capture = exec_runtime_binary_1.execRuntimeBinaryCapture) {
190
+ try {
191
+ const out = await capture(buildDaemonLogsArgs({ ...flags, path: true }), {
192
+ timeoutMs: 5_000,
193
+ });
194
+ if (out.code !== 0)
195
+ return null;
196
+ const lines = out.stdout
197
+ .split('\n')
198
+ .map((l) => l.trim())
199
+ .filter(Boolean);
200
+ const last = lines.at(-1);
201
+ return last && node_path_1.default.isAbsolute(last) ? last : null;
202
+ }
203
+ catch {
204
+ return null;
205
+ }
206
+ }
207
+ /**
208
+ * The first runtime whose own `logs` reads the LIVE daemon's `--log-file`
209
+ * (OSK-15993, `daemon/src/daemon-log-target.ts`). 0.8.112 through 0.8.118 have
210
+ * a `logs` subcommand, so the subcommand probe says "supported", but it reads
211
+ * the profile's `logs/daemon.log` unconditionally — with a desktop-managed
212
+ * daemon, a file nobody has written for a week. Forwarding to one of those is
213
+ * forwarding to the bug, so the CLI forwards only at or above this version.
214
+ */
215
+ exports.LIVE_LOG_RESOLUTION_RUNTIME_VERSION = '0.8.119';
216
+ /** `x.y.z` from a version string, or null. A prerelease suffix is ignored. */
217
+ function parseRuntimeVersion(text) {
218
+ const m = /(\d+)\.(\d+)\.(\d+)/.exec(text);
219
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
220
+ }
221
+ /** Whether `version` is at least `floor`; null when either is not a version. */
222
+ function runtimeVersionAtLeast(version, floor) {
223
+ const a = parseRuntimeVersion(version);
224
+ const b = parseRuntimeVersion(floor);
225
+ if (!a || !b)
226
+ return null;
227
+ for (let i = 0; i < 3; i += 1) {
228
+ if (a[i] !== b[i])
229
+ return a[i] > b[i];
230
+ }
231
+ return true;
232
+ }
233
+ /** The installed runtime's `--version`, or null when it cannot say in time. */
234
+ async function probeRuntimeVersion(capture = exec_runtime_binary_1.execRuntimeBinaryCapture) {
235
+ try {
236
+ const out = await capture(['--version'], { timeoutMs: 5_000 });
237
+ if (out.code !== 0)
238
+ return null;
239
+ const parsed = parseRuntimeVersion(out.stdout);
240
+ return parsed ? parsed.join('.') : null;
241
+ }
242
+ catch {
243
+ return null;
244
+ }
245
+ }
246
+ /**
247
+ * The live daemon's log as the runtime's `status --json` reports it
248
+ * (`service.logs`). Status has named the live `--log-file` since before `logs`
249
+ * did, so on 0.8.117 status is right while logs is wrong. Null when status
250
+ * cannot answer, or answers with anything but an absolute path.
251
+ */
252
+ async function probeStatusLogPath(flags, capture = exec_runtime_binary_1.execRuntimeBinaryCapture) {
253
+ try {
254
+ const out = await capture([...(flags.profile ? ['--profile', flags.profile] : []), 'status', '--json'], { timeoutMs: 15_000 });
255
+ // `status` exits non-zero when it reports problems; the JSON is still the answer.
256
+ const start = out.stdout.indexOf('{');
257
+ if (start < 0)
258
+ return null;
259
+ const parsed = JSON.parse(out.stdout.slice(start));
260
+ const logs = parsed?.service?.logs;
261
+ return typeof logs === 'string' && node_path_1.default.isAbsolute(logs) ? logs : null;
262
+ }
263
+ catch {
264
+ return null;
265
+ }
266
+ }
267
+ function sameLogFile(a, b) {
268
+ return Boolean(a && b && node_path_1.default.resolve(a) === node_path_1.default.resolve(b));
269
+ }
270
+ /**
271
+ * Decide which runtime, if any, `skrr daemon logs` may trust (OSK-15993).
272
+ *
273
+ * - No `logs` subcommand: the legacy fallback, as before.
274
+ * - A runtime at or above {@link LIVE_LOG_RESOLUTION_RUNTIME_VERSION}: forward.
275
+ * - An older runtime, or one whose version cannot be read: ask its `status`
276
+ * which file the live daemon writes and read that file here. When status
277
+ * cannot answer, an older runtime gets the legacy fallback (the newest of the
278
+ * profile log and the installed service's logs); an unknown one is forwarded.
279
+ */
280
+ async function planDaemonLogs(flags, deps) {
281
+ const legacy = (reason) => {
282
+ const chosen = chooseLegacyFallbackLog({
283
+ profilePath: deps.profilePath,
284
+ serviceCandidates: (flags.stderr
285
+ ? [deps.service?.stderrPath]
286
+ : [deps.service?.logFile, deps.service?.stdoutPath]).filter((p) => Boolean(p)),
287
+ mtimeOf: deps.mtimeOf,
288
+ reason,
289
+ });
290
+ return {
291
+ kind: 'read',
292
+ path: chosen.path,
293
+ note: chosen.note,
294
+ missingHint: 'The installed daemon predates `oversky logs`; start it or upgrade it, then retry.',
295
+ };
296
+ };
297
+ const supports = await deps.supportsLogs();
298
+ if (supports === false) {
299
+ return legacy('This daemon predates `logs`, so it cannot name its live log itself.');
300
+ }
301
+ // Help could not be read: nothing to go on, keep the old behaviour.
302
+ if (supports === null)
303
+ return { kind: 'forward' };
304
+ const version = await deps.runtimeVersion();
305
+ const current = version
306
+ ? runtimeVersionAtLeast(version, exports.LIVE_LOG_RESOLUTION_RUNTIME_VERSION)
307
+ : null;
308
+ if (current === true)
309
+ return { kind: 'forward' };
310
+ const live = await deps.statusLogPath();
311
+ const predates = version !== null
312
+ ? `The installed runtime (${version}) predates \`logs\` following the live daemon's ` +
313
+ `--log-file (${exports.LIVE_LOG_RESOLUTION_RUNTIME_VERSION}), so it would read ${deps.profilePath}.`
314
+ : `The installed runtime did not report its version, so its \`logs\` may read ` +
315
+ `${deps.profilePath} instead of the live log.`;
316
+ if (!live) {
317
+ if (current === null)
318
+ return { kind: 'forward' };
319
+ return legacy(`${predates} Its \`status\` did not name the live log either.`);
320
+ }
321
+ // The live daemon writes the profile log itself: the runtime's answer is right.
322
+ if (sameLogFile(live, node_path_1.default.join(node_path_1.default.dirname(deps.profilePath), 'daemon.log'))) {
323
+ return { kind: 'forward' };
324
+ }
325
+ const missingHint = 'The live daemon has not written it yet; `skrr daemon status` shows whether one is running.';
326
+ if (!flags.stderr) {
327
+ return {
328
+ kind: 'read',
329
+ path: live,
330
+ note: `Showing ${live}, the live daemon's log as \`skrrd status\` reports it. ${predates}`,
331
+ missingHint,
332
+ };
333
+ }
334
+ // --stderr: trust the installed definition's redirect only when it is the
335
+ // one that launched this daemon (its --log-file is the live one) — the same
336
+ // rule the runtime's own resolution applies.
337
+ if (deps.service?.stderrPath && sameLogFile(deps.service.logFile, live)) {
338
+ return {
339
+ kind: 'read',
340
+ path: deps.service.stderrPath,
341
+ note: `Showing ${deps.service.stderrPath}, the live daemon's stderr as its service definition ` +
342
+ `redirects it; its full log, WARN and ERROR included, is ${live}. ${predates}`,
343
+ missingHint,
344
+ };
345
+ }
346
+ return {
347
+ kind: 'read',
348
+ path: live,
349
+ note: `Showing ${live}, the live daemon's log as \`skrrd status\` reports it; it carries every ` +
350
+ `WARN and ERROR line. ${predates}`,
351
+ missingHint,
352
+ };
85
353
  }
86
354
  /** Read a bounded trailing window, so an unrotated legacy err log is safe. */
87
355
  function readDaemonLogTail(filePath, maxLines) {
@@ -114,7 +382,7 @@ function readDaemonLogTail(filePath, maxLines) {
114
382
  * `completeTaskDurably`. Every flag declared on the command must appear here,
115
383
  * or it parses successfully and is then silently dropped.
116
384
  */
117
- function buildOverskyLogsArgs(flags) {
385
+ function buildDaemonLogsArgs(flags) {
118
386
  return [
119
387
  // The runtime's own global option, placed before the subcommand.
120
388
  ...(flags.profile ? ['--profile', flags.profile] : []),
@@ -174,33 +442,57 @@ class DaemonLogs extends core_1.Command {
174
442
  ];
175
443
  async run() {
176
444
  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) {
445
+ const args = buildDaemonLogsArgs(flags);
446
+ const showAge = (filePath) => {
447
+ // Before ANY log content, on stderr so it never pollutes a pipe. `--path`
448
+ // is exempt: its whole contract is one path on stdout.
449
+ if (flags.path || !filePath)
450
+ return;
181
451
  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);
452
+ if ((0, node_fs_1.existsSync)(filePath)) {
453
+ this.logToStderr(`${filePath}: ${describeLogAge((0, node_fs_1.statSync)(filePath).mtimeMs).line}`);
186
454
  }
187
455
  }
188
456
  catch {
189
457
  // Never let the banner stop the logs from being shown.
190
458
  }
191
- }
459
+ };
192
460
  try {
193
461
  // `oversky 0.2.x` has profiles and writes the same files, but has no
194
462
  // `logs` subcommand. Probe top-level help first so forwarding does not
195
463
  // print an unrelated "unknown option" error before the useful output.
196
- if ((await (0, exec_runtime_binary_1.runtimeBinarySupportsSubcommand)('logs')) === false) {
197
- const logPath = resolveLegacyDaemonLogPath(flags);
464
+ //
465
+ // OSK-15993: a runtime that HAS `logs` is not necessarily one to trust
466
+ // with it — 0.8.112–0.8.118 read the profile log even when the live
467
+ // daemon writes elsewhere. `planDaemonLogs` forwards only to a runtime
468
+ // that resolves the live log itself.
469
+ const plan = await planDaemonLogs(flags, {
470
+ supportsLogs: () => (0, exec_runtime_binary_1.runtimeBinarySupportsSubcommand)('logs'),
471
+ runtimeVersion: () => probeRuntimeVersion(),
472
+ statusLogPath: () => probeStatusLogPath(flags),
473
+ profilePath: resolveLegacyDaemonLogPath(flags),
474
+ service: installedServiceLogDestinations(resolveDaemonLogsProfile(flags)),
475
+ mtimeOf: (filePath) => {
476
+ try {
477
+ return (0, node_fs_1.statSync)(filePath).mtimeMs;
478
+ }
479
+ catch {
480
+ return null;
481
+ }
482
+ },
483
+ });
484
+ if (plan.kind === 'read') {
485
+ const logPath = plan.path;
486
+ // `--path` prints the one path and nothing else, on either stream.
198
487
  if (flags.path) {
199
488
  this.log(logPath);
200
489
  return;
201
490
  }
491
+ if (plan.note)
492
+ this.logToStderr(plan.note);
493
+ showAge(logPath);
202
494
  if (!(0, node_fs_1.existsSync)(logPath)) {
203
- this.error(`No daemon log at ${logPath}. The installed daemon predates \`oversky logs\`; start it or upgrade it, then retry.`, { exit: 1 });
495
+ this.error(`No daemon log at ${logPath}. ${plan.missingHint}`, { exit: 1 });
204
496
  }
205
497
  const lines = readDaemonLogTail(logPath, flags.lines ?? 200);
206
498
  if (lines.length > 0)
@@ -234,6 +526,10 @@ class DaemonLogs extends core_1.Command {
234
526
  }
235
527
  return;
236
528
  }
529
+ // The banner must describe the file the runtime will actually print —
530
+ // the live daemon's log (OSK-15993) — not the profile default.
531
+ if (!flags.path)
532
+ showAge(await resolveForwardedLogPath(flags));
237
533
  const code = await (0, exec_runtime_binary_1.execRuntimeBinary)(args);
238
534
  this.exit(code);
239
535
  }
@@ -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;