@skrr-ai/cli 0.1.126 → 0.1.127

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.
@@ -8,8 +8,8 @@ import { BaseCommand } from '../../../base-command';
8
8
  * Wraps `GET /api/agents/:agent_id/goals` (data-provider `listAgentGoals`),
9
9
  * which returns goals where the agent is owner, DRI, or a contributor, each
10
10
  * enriched with `_agentGoalRole` (which of the three) and `_completionPercent`.
11
- * The endpoint sorts by most-recently-updated and caps at 100 goals; there is
12
- * no pagination cursor, so neither is there a `--cursor` flag here.
11
+ * The endpoint sorts by most-recently-updated and supports cursor pagination.
12
+ * This command currently shows its first page of up to 100 goals.
13
13
  */
14
14
  export default class AgentsGoalsList extends BaseCommand {
15
15
  static description: string;
@@ -14,8 +14,8 @@ const format_1 = require("../../../lib/format");
14
14
  * Wraps `GET /api/agents/:agent_id/goals` (data-provider `listAgentGoals`),
15
15
  * which returns goals where the agent is owner, DRI, or a contributor, each
16
16
  * enriched with `_agentGoalRole` (which of the three) and `_completionPercent`.
17
- * The endpoint sorts by most-recently-updated and caps at 100 goals; there is
18
- * no pagination cursor, so neither is there a `--cursor` flag here.
17
+ * The endpoint sorts by most-recently-updated and supports cursor pagination.
18
+ * This command currently shows its first page of up to 100 goals.
19
19
  */
20
20
  class AgentsGoalsList extends base_command_1.BaseCommand {
21
21
  static description = 'List the goals an agent owns, is DRI for, or contributes to';
@@ -6,6 +6,7 @@ export default class GoalsCreate extends BaseCommand {
6
6
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
7
  'from-json': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
8
  title: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'idempotency-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
10
  description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  level: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
12
  'target-date': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -24,6 +24,9 @@ class GoalsCreate extends base_command_1.BaseCommand {
24
24
  description: 'Path to a JSON file (or "-" for stdin) to use as the request body',
25
25
  }),
26
26
  title: core_1.Flags.string({ description: 'Goal title (required unless set via --from-json)' }),
27
+ 'idempotency-key': core_1.Flags.string({
28
+ description: 'Stable key for retries of this exact Goal creation request',
29
+ }),
27
30
  description: core_1.Flags.string({ description: 'Goal description' }),
28
31
  level: core_1.Flags.string({
29
32
  description: 'Goal level',
@@ -126,7 +129,7 @@ class GoalsCreate extends base_command_1.BaseCommand {
126
129
  payload.labelIds = resolvedLabelIds;
127
130
  let created;
128
131
  try {
129
- created = await data_provider_1.dataService.createGoal(payload);
132
+ created = await data_provider_1.dataService.createGoal(payload, flags['idempotency-key'] ? { idempotencyKey: flags['idempotency-key'] } : undefined);
130
133
  if (flags.space) {
131
134
  const goalId = created.id;
132
135
  if (!goalId || typeof goalId !== 'string') {
@@ -4,20 +4,7 @@ const core_1 = require("@oclif/core");
4
4
  const data_provider_1 = require("@skrr-ai/data-provider");
5
5
  const base_command_1 = require("../../base-command");
6
6
  const format_1 = require("../../lib/format");
7
- /**
8
- * The one class of goal change this log does not contain.
9
- *
10
- * `PATCH /api/goals/:goalId/planner-config` writes the goal through
11
- * `Goal.findOneAndUpdate` directly rather than `GoalModel.updateGoal`, which is
12
- * where `RevisionService` is called from — so arming, re-timing or disarming the
13
- * auto-planner (and any `maxActiveTasks` change made through that route) leaves
14
- * no revision. "Who turned the planner on for this goal, and when" is the audit
15
- * question a shared goal actually raises, and the table answers it with silence.
16
- *
17
- * Naming the gap is the honest thing the CLI can do; closing it is a server
18
- * change. DELETE THIS FOOTER once the planner-config route records a revision.
19
- */
20
- const PLANNER_GAP_NOTE = 'Note: auto-planner changes made with `goals planner-config` are not recorded as revisions.';
7
+ const revision_actor_1 = require("../../lib/revision-actor");
21
8
  class GoalsRevisions extends base_command_1.BaseCommand {
22
9
  static description = 'List revisions for a goal';
23
10
  static examples = [
@@ -47,25 +34,22 @@ class GoalsRevisions extends base_command_1.BaseCommand {
47
34
  const items = result?.data ?? [];
48
35
  if (items.length === 0) {
49
36
  this.log('No revisions.');
50
- this.log(PLANNER_GAP_NOTE);
51
37
  return;
52
38
  }
53
39
  const rows = items.map((rev) => ({
54
40
  version: String(rev.revisionNumber ?? '-'),
55
41
  changeType: String(rev.changeType ?? '-'),
56
- user: String(rev.userId ?? '-'),
42
+ user: (0, revision_actor_1.revisionActorLabel)(rev),
57
43
  changes: String(Array.isArray(rev.changes) ? rev.changes.length : 0),
58
44
  when: rev.createdAt ? String(rev.createdAt).slice(0, 19).replace('T', ' ') : '-',
59
45
  }));
60
46
  (0, format_1.renderTable)(rows, [
61
47
  { key: 'version', header: 'REV' },
62
48
  { key: 'changeType', header: 'CHANGE' },
63
- { key: 'user', header: 'USER' },
49
+ { key: 'user', header: 'ACTOR' },
64
50
  { key: 'changes', header: 'FIELDS' },
65
51
  { key: 'when', header: 'AT' },
66
52
  ], (line) => this.log(line));
67
- this.log('');
68
- this.log(PLANNER_GAP_NOTE);
69
53
  }
70
54
  }
71
55
  exports.default = GoalsRevisions;
@@ -5,6 +5,7 @@ const data_provider_1 = require("@skrr-ai/data-provider");
5
5
  const base_command_1 = require("../../base-command");
6
6
  const format_1 = require("../../lib/format");
7
7
  const space_output_1 = require("../../lib/space-output");
8
+ const revision_actor_1 = require("../../lib/revision-actor");
8
9
  class SpacesRevisions extends base_command_1.BaseCommand {
9
10
  static description = 'List revisions for a space';
10
11
  static examples = [
@@ -40,14 +41,14 @@ class SpacesRevisions extends base_command_1.BaseCommand {
40
41
  const rows = items.map((rev) => ({
41
42
  version: String(rev.revisionNumber ?? '-'),
42
43
  changeType: String(rev.changeType ?? '-'),
43
- user: String(rev.userId ?? '-'),
44
+ user: (0, revision_actor_1.revisionActorLabel)(rev),
44
45
  changes: String(Array.isArray(rev.changes) ? rev.changes.length : 0),
45
46
  when: rev.createdAt ? String(rev.createdAt).slice(0, 19).replace('T', ' ') : '-',
46
47
  }));
47
48
  (0, format_1.renderTable)(rows, [
48
49
  { key: 'version', header: 'REV' },
49
50
  { key: 'changeType', header: 'CHANGE' },
50
- { key: 'user', header: 'USER' },
51
+ { key: 'user', header: 'ACTOR' },
51
52
  { key: 'changes', header: 'FIELDS' },
52
53
  { key: 'when', header: 'AT' },
53
54
  ], (line) => this.log(line));
@@ -5,6 +5,7 @@ const core_1 = require("@oclif/core");
5
5
  const data_provider_1 = require("@skrr-ai/data-provider");
6
6
  const base_command_1 = require("../../base-command");
7
7
  const format_1 = require("../../lib/format");
8
+ const revision_actor_1 = require("../../lib/revision-actor");
8
9
  /** " (via CLI credential)" when the revision recorded its credential channel. */
9
10
  function revisionChannelSuffix(rev) {
10
11
  const via = rev.metadata?.via;
@@ -46,14 +47,14 @@ class TasksRevisions extends base_command_1.BaseCommand {
46
47
  version: String(rev.revisionNumber ?? '-'),
47
48
  changeType: String(rev.changeType ?? '-'),
48
49
  // The channel the person's credential came through, when recorded (OSK-16621).
49
- user: `${String(rev.userId ?? '-')}${revisionChannelSuffix(rev)}`,
50
+ user: `${(0, revision_actor_1.revisionActorLabel)(rev)}${rev.userId != null ? revisionChannelSuffix(rev) : ''}`,
50
51
  changes: String(Array.isArray(rev.changes) ? rev.changes.length : 0),
51
52
  when: rev.createdAt ? String(rev.createdAt).slice(0, 19).replace('T', ' ') : '-',
52
53
  }));
53
54
  (0, format_1.renderTable)(rows, [
54
55
  { key: 'version', header: 'REV' },
55
56
  { key: 'changeType', header: 'CHANGE' },
56
- { key: 'user', header: 'USER' },
57
+ { key: 'user', header: 'ACTOR' },
57
58
  { key: 'changes', header: 'FIELDS' },
58
59
  { key: 'when', header: 'AT' },
59
60
  ], (line) => this.log(line));
@@ -29,6 +29,8 @@ function describeFireAuditDetail(detail, fireId, triggerId) {
29
29
  lines.push(`Duration: ${String(detail.durationMs)}ms`);
30
30
  if (detail.conversationId)
31
31
  lines.push(`Conversation: ${String(detail.conversationId)}`);
32
+ // Tool calls refused during a turn that was delivered: `triggers show` points here for the facts.
33
+ lines.push(...(0, triggers_1.fireRefusalLines)(detail.metadata));
32
34
  return lines;
33
35
  }
34
36
  class TriggersFireDetail extends base_command_1.BaseCommand {
@@ -0,0 +1,2 @@
1
+ /** Render the actual actor without presenting a system or Agent as a user. */
2
+ export declare function revisionActorLabel(revision: Record<string, unknown>): string;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.revisionActorLabel = revisionActorLabel;
4
+ /** Render the actual actor without presenting a system or Agent as a user. */
5
+ function revisionActorLabel(revision) {
6
+ if (revision.userId != null)
7
+ return String(revision.userId);
8
+ const actor = revision.actor;
9
+ if (actor && typeof actor === 'object' && !Array.isArray(actor)) {
10
+ const { kind, id } = actor;
11
+ if ((kind === 'system' || kind === 'agent') && typeof id === 'string' && id) {
12
+ return `${kind}: ${id}`;
13
+ }
14
+ }
15
+ return '-';
16
+ }
@@ -158,6 +158,15 @@ export interface TriggerFireRecord {
158
158
  replayBlockedReason?: string;
159
159
  replayRequirements?: string[];
160
160
  replayContext?: unknown;
161
+ /** Tool calls refused during a turn that was delivered (OSK-20776). */
162
+ permissionRefusals?: {
163
+ count?: number;
164
+ tools?: string[];
165
+ unattended?: boolean;
166
+ layer?: string;
167
+ };
168
+ /** The refusal record could not be read for this fire, which is not "none were refused". */
169
+ permissionRefusalsUnread?: boolean;
161
170
  };
162
171
  }
163
172
  export interface TriggerListResponse {
@@ -516,6 +525,29 @@ export declare function writeModeLabel(writeMode: string): string;
516
525
  * the configured base URL rather than guessed.
517
526
  */
518
527
  export declare function webhookIngestUrl(baseURL: string, triggerId: string): string;
528
+ /**
529
+ * What `triggers show` adds when the last fire DELIVERED but had tool calls
530
+ * refused (OSK-20776). The outcome of such a fire is `succeeded`, so the
531
+ * `Fires:` line reads as success; this says what the string after `last` means
532
+ * and what a person can do about it.
533
+ *
534
+ * It states the mechanism and not this fire's cause, because `lastResult` names
535
+ * the refused kinds only. Whether a fire was unattended, and which layer
536
+ * refused, is on the fire record (`fire-detail`), and this points there.
537
+ *
538
+ * It does not recommend a mode. `bypassPermissions` is the only mode under which
539
+ * an unattended shell call runs, and it runs EVERY call without asking, which is
540
+ * a decision about what this agent may do on its own (a release job included),
541
+ * not a hint to paste.
542
+ */
543
+ export declare function toolsRefusedHintLines(trigger: TriggerDefinition, kinds: readonly string[], bin?: string): string[];
544
+ /**
545
+ * The refusal facts a fire record holds, as `fire-detail` lines. Nothing when
546
+ * the fire recorded none. A fire whose record could not be read says so: that
547
+ * is not the same as no refusal, and a reader must not be able to mistake it
548
+ * for one.
549
+ */
550
+ export declare function fireRefusalLines(metadata: unknown): string[];
519
551
  export declare function renderTriggerSummary(trigger: TriggerDefinition, log: (line: string) => void, options?: {
520
552
  baseURL?: string;
521
553
  }): void;
@@ -48,6 +48,8 @@ exports.formatDuplicateSubscriptionWarning = formatDuplicateSubscriptionWarning;
48
48
  exports.armedStateLabel = armedStateLabel;
49
49
  exports.writeModeLabel = writeModeLabel;
50
50
  exports.webhookIngestUrl = webhookIngestUrl;
51
+ exports.toolsRefusedHintLines = toolsRefusedHintLines;
52
+ exports.fireRefusalLines = fireRefusalLines;
51
53
  exports.renderTriggerSummary = renderTriggerSummary;
52
54
  exports.renderLegacyRuns = renderLegacyRuns;
53
55
  exports.renderFireAudit = renderFireAudit;
@@ -1276,6 +1278,65 @@ const WEBHOOK_INGEST_KINDS = new Set(['on_demand', 'form', 'chat_message']);
1276
1278
  function webhookIngestUrl(baseURL, triggerId) {
1277
1279
  return `${String(baseURL).replace(/\/+$/, '')}/api/triggers/webhook/${encodeURIComponent(triggerId)}`;
1278
1280
  }
1281
+ /** An id or token that is short and plain, so it is safe in a command a person will paste. */
1282
+ const PLAIN_TOKEN = /^[A-Za-z0-9_.-]{1,64}$/;
1283
+ /**
1284
+ * What `triggers show` adds when the last fire DELIVERED but had tool calls
1285
+ * refused (OSK-20776). The outcome of such a fire is `succeeded`, so the
1286
+ * `Fires:` line reads as success; this says what the string after `last` means
1287
+ * and what a person can do about it.
1288
+ *
1289
+ * It states the mechanism and not this fire's cause, because `lastResult` names
1290
+ * the refused kinds only. Whether a fire was unattended, and which layer
1291
+ * refused, is on the fire record (`fire-detail`), and this points there.
1292
+ *
1293
+ * It does not recommend a mode. `bypassPermissions` is the only mode under which
1294
+ * an unattended shell call runs, and it runs EVERY call without asking, which is
1295
+ * a decision about what this agent may do on its own (a release job included),
1296
+ * not a hint to paste.
1297
+ */
1298
+ function toolsRefusedHintLines(trigger, kinds, bin = 'skrr') {
1299
+ const scopedAgent = trigger.scope?.kind === 'agent' ? trigger.scope.refs?.agentId : undefined;
1300
+ const agent = typeof scopedAgent === 'string' && PLAIN_TOKEN.test(scopedAgent) ? scopedAgent : '<agent>';
1301
+ const triggerId = typeof trigger.id === 'string' && PLAIN_TOKEN.test(trigger.id) ? trigger.id : '<trigger-id>';
1302
+ const what = kinds.length > 0 ? kinds.join(', ') : 'some tool calls';
1303
+ return [
1304
+ `Refused: ${what} - the last fire delivered, but these tool calls were refused`,
1305
+ ' A scheduled fire runs unattended: a call that would need approval is refused, because no one is there to give it.',
1306
+ " To allow it, change the agent's permission mode (bypassPermissions runs every tool call without asking, so decide that on purpose):",
1307
+ ` ${bin} agents update ${agent} --default-permission-mode <mode> (modes: ${bin} agents update --help)`,
1308
+ ` What was refused, per fire: ${bin} triggers audit ${triggerId}, then ${bin} triggers fire-detail ${triggerId} <fire-id>`,
1309
+ ];
1310
+ }
1311
+ /**
1312
+ * The refusal facts a fire record holds, as `fire-detail` lines. Nothing when
1313
+ * the fire recorded none. A fire whose record could not be read says so: that
1314
+ * is not the same as no refusal, and a reader must not be able to mistake it
1315
+ * for one.
1316
+ */
1317
+ function fireRefusalLines(metadata) {
1318
+ if (!metadata || typeof metadata !== 'object')
1319
+ return [];
1320
+ const { permissionRefusals, permissionRefusalsUnread } = metadata;
1321
+ const lines = [];
1322
+ if (permissionRefusals && typeof permissionRefusals === 'object') {
1323
+ const { count, tools, unattended, layer } = permissionRefusals;
1324
+ const kinds = (Array.isArray(tools) ? tools : []).filter((tool) => typeof tool === 'string' && PLAIN_TOKEN.test(tool));
1325
+ const calls = typeof count === 'number' && Number.isFinite(count) && count > 0
1326
+ ? `${count} tool call${count === 1 ? '' : 's'}`
1327
+ : 'tool calls';
1328
+ const parts = [`${calls} refused${kinds.length > 0 ? ` (${kinds.join(', ')})` : ''}`];
1329
+ if (unattended === true)
1330
+ parts.push('unattended, so no one could grant them');
1331
+ if (typeof layer === 'string' && PLAIN_TOKEN.test(layer))
1332
+ parts.push(`refused by ${layer}`);
1333
+ lines.push(`Refused: ${parts.join('; ')}`);
1334
+ }
1335
+ if (permissionRefusalsUnread === true) {
1336
+ lines.push('Refused: unknown - the refusal record could not be read for this fire; that is not the same as none');
1337
+ }
1338
+ return lines;
1339
+ }
1279
1340
  function renderTriggerSummary(trigger, log, options = {}) {
1280
1341
  log(`ID: ${trigger.id ?? '-'}`);
1281
1342
  log(`Kind: ${trigger.kind ?? '-'}`);
@@ -1389,6 +1450,11 @@ function renderTriggerSummary(trigger, log, options = {}) {
1389
1450
  parts.push(`last ${audit.lastResult}`);
1390
1451
  log(`Fires: ${parts.join(', ')}`);
1391
1452
  }
1453
+ // `last tools_refused:...` above is a success that held a refusal. Say what that means.
1454
+ const refused = (0, data_provider_1.parseToolsRefusedResult)(audit.lastResult);
1455
+ if (refused)
1456
+ for (const line of toolsRefusedHintLines(trigger, refused))
1457
+ log(line);
1392
1458
  // The reason, at the surface a reader reaches for — not only in `--json`.
1393
1459
  if (audit.lastError)
1394
1460
  log(`Last error: ${audit.lastError}`);