@skrr-ai/cli 0.1.38 → 0.1.40

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 (34) hide show
  1. package/README.md +1 -1
  2. package/bin/dev-fallback.js +8 -0
  3. package/dist/base-command.js +8 -0
  4. package/dist/commands/agents/create.d.ts +7 -0
  5. package/dist/commands/agents/create.js +13 -1
  6. package/dist/commands/agents/triggers/policy.d.ts +1 -0
  7. package/dist/commands/agents/triggers/policy.js +32 -0
  8. package/dist/commands/agents/update.js +4 -3
  9. package/dist/commands/followups/observability.js +6 -2
  10. package/dist/commands/followups/remind.js +5 -2
  11. package/dist/commands/followups/reschedule.js +2 -1
  12. package/dist/commands/followups/show.js +4 -0
  13. package/dist/commands/followups/watch.d.ts +1 -0
  14. package/dist/commands/followups/watch.js +9 -3
  15. package/dist/commands/machines/dedicated/create.js +6 -2
  16. package/dist/commands/machines/dedicated/destroy.js +10 -10
  17. package/dist/commands/machines/dedicated/exec.js +3 -1
  18. package/dist/commands/machines/dedicated/price-book.d.ts +17 -0
  19. package/dist/commands/machines/dedicated/price-book.js +35 -0
  20. package/dist/commands/tasks/lifecycle/detach.js +26 -5
  21. package/dist/lib/dedicated-lease-command.d.ts +3 -1
  22. package/dist/lib/dedicated-lease-command.js +8 -3
  23. package/dist/lib/dedicated-machines.d.ts +48 -1
  24. package/dist/lib/dedicated-machines.js +93 -7
  25. package/dist/lib/dedicated-wait.js +13 -0
  26. package/dist/lib/followups.d.ts +8 -0
  27. package/dist/lib/label-ref.d.ts +8 -1
  28. package/dist/lib/label-ref.js +12 -2
  29. package/dist/lib/option-hint.d.ts +15 -0
  30. package/dist/lib/option-hint.js +75 -0
  31. package/dist/lib/workspaces.d.ts +3 -0
  32. package/dist/lib/workspaces.js +38 -0
  33. package/oclif.manifest.json +4952 -4885
  34. package/package.json +5 -3
package/README.md CHANGED
@@ -38,7 +38,7 @@ observability), and shared links.
38
38
  | **spaces widgets** | `list`, `add`, `remove` |
39
39
  | **spaces skills** | `list`, `add`, `remove`, `pin` |
40
40
  | **spaces labels** | `list`, `add`, `remove` (attach/detach; filter with `spaces list --label`) |
41
- | **labels** | `list`, `palette`, `create`, `update`, `archive`, `restore`, `delete`, `reorder` (unified labels across `user`, `space`, and `workspace` scopes) |
41
+ | **labels** | `list`, `palette`, `create`, `update`, `archive`, `restore`, `delete`, `reorder` (unified labels across `user` and `workspace` scopes) |
42
42
  | **chats triggers** | `show`, `set` |
43
43
  | **dm** | `list`, `show`, `messages`, `threads`, `thread show/messages/follow/mute/share`, `send` (agent 1:1 main chat and Slack-style Thread controls) |
44
44
  | **diagnostics** | `context`, `doctor`, `usage`, `commands` (agent-friendly runtime identity, readiness, progressive token-budgeted discovery, and complete machine-readable schemas) |
@@ -245,6 +245,14 @@ function findAlternateCli({
245
245
  function missingLocalCommandBuild({ cliRoot, argv, fileSystem = fs, verifySourceIdentity = true }) {
246
246
  const commandsDirectory = path.join(cliRoot, 'dist', 'commands');
247
247
  const sourceCommandsDirectory = path.join(cliRoot, 'src', 'commands');
248
+ // Staleness is a relation between a build and its SOURCE, so a CLI without
249
+ // source — every installed npm package, which ships `dist` and no `src` — has
250
+ // nothing to be stale against. Every check below is a development-checkout
251
+ // guard. Only the per-command one happened to notice: it looks a source file
252
+ // up first. The `--version` branch did not, read the missing source-digest
253
+ // manifest as a missing build, and made an installed 0.1.38 report whatever
254
+ // other `skrr` was on the PATH, or fail (OSK-9883).
255
+ if (!fileSystem.existsSync(sourceCommandsDirectory)) return null;
248
256
  let commandBuildManifest;
249
257
  try {
250
258
  commandBuildManifest = JSON.parse(
@@ -16,6 +16,7 @@ exports.isTypedServerRefusal = isTypedServerRefusal;
16
16
  exports.isRetryableFailure = isRetryableFailure;
17
17
  const core_1 = require("@oclif/core");
18
18
  Object.defineProperty(exports, "Flags", { enumerable: true, get: function () { return core_1.Flags; } });
19
+ const option_hint_1 = require("./lib/option-hint");
19
20
  const failed_lookup_1 = require("./lib/failed-lookup");
20
21
  // Re-exported from its home in `lib/keychain`, beside the other profile
21
22
  // functions. It lived here while `init()` was its only caller; a lib module
@@ -788,6 +789,13 @@ class BaseCommand extends core_1.Command {
788
789
  if (retiredHint) {
789
790
  err.message = `${err.message}\n\n${retiredHint}`;
790
791
  }
792
+ // Sibling domains spell one lifecycle differently (`in_progress` for a
793
+ // space, `active` for a goal), and oclif's enum refusal named neither the
794
+ // difference nor the value to use (OSK-9735).
795
+ const optionHint = (0, option_hint_1.invalidOptionHint)(err.message || '');
796
+ if (optionHint) {
797
+ err.message = `${err.message}\n\n${optionHint}`;
798
+ }
791
799
  if (this.wantsJsonOutput()) {
792
800
  const exit = err.exitCode ?? err.oclif?.exit ?? 1;
793
801
  process.exitCode = exit;
@@ -64,6 +64,13 @@ export declare function agentCreateMissingFields(payload: Record<string, unknown
64
64
  * a failed create would be worse than the silence.
65
65
  */
66
66
  export declare function unappliedPayloadKeys(sent: Record<string, unknown>, created: unknown, ignore?: readonly string[]): string[];
67
+ /**
68
+ * Why a key can come back missing. Both readings are live: a typo of a real
69
+ * field (`max_turns` for `maxTurns`), and a real field only the server sets
70
+ * (`nativeRole`, `author`). Naming only the first told a caller who had tried
71
+ * to claim a platform role that they had misspelled something.
72
+ */
73
+ export declare function unappliedPayloadKeysWarning(keys: readonly string[]): string;
67
74
  export declare function backgroundLoopLines(agentId: string, relationshipLoopsSkipped: boolean): string[];
68
75
  export default class AgentsCreate extends BaseCommand {
69
76
  static description: string;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.agentCreateMissingFieldsMessage = agentCreateMissingFieldsMessage;
4
4
  exports.agentCreateMissingFields = agentCreateMissingFields;
5
5
  exports.unappliedPayloadKeys = unappliedPayloadKeys;
6
+ exports.unappliedPayloadKeysWarning = unappliedPayloadKeysWarning;
6
7
  exports.backgroundLoopLines = backgroundLoopLines;
7
8
  const core_1 = require("@oclif/core");
8
9
  const data_provider_1 = require("@skrr-ai/data-provider");
@@ -105,6 +106,17 @@ function unappliedPayloadKeys(sent, created, ignore = []) {
105
106
  .filter((key) => !Object.prototype.hasOwnProperty.call(back, key))
106
107
  .sort();
107
108
  }
109
+ /**
110
+ * Why a key can come back missing. Both readings are live: a typo of a real
111
+ * field (`max_turns` for `maxTurns`), and a real field only the server sets
112
+ * (`nativeRole`, `author`). Naming only the first told a caller who had tried
113
+ * to claim a platform role that they had misspelled something.
114
+ */
115
+ function unappliedPayloadKeysWarning(keys) {
116
+ const one = keys.length === 1;
117
+ return (`The server did not keep ${keys.join(', ')}: ${one ? 'it is not an agent field' : 'they are not agent fields'}, ` +
118
+ `or only the server sets ${one ? 'it' : 'them'}. \`skrr agents show <id> --json\` shows what the agent stores.`);
119
+ }
108
120
  function backgroundLoopLines(agentId, relationshipLoopsSkipped) {
109
121
  if (relationshipLoopsSkipped) {
110
122
  return [
@@ -368,7 +380,7 @@ class AgentsCreate extends base_command_1.BaseCommand {
368
380
  // Agent document field and is intentionally absent from the response.
369
381
  const unapplied = unappliedPayloadKeys(payload, created, ['relationshipLoops']);
370
382
  if (unapplied.length > 0) {
371
- this.warn(`The server did not keep ${unapplied.join(', ')} — ${unapplied.length === 1 ? 'that field is' : 'those fields are'} not part of the agent it created. Check \`skrr agents show <id> --json\`.`);
383
+ this.warn(unappliedPayloadKeysWarning(unapplied));
372
384
  }
373
385
  if (flags.json) {
374
386
  this.log(JSON.stringify((0, web_url_1.withAgentUrl)(this.cliConfig.baseURL, created), null, 2));
@@ -15,6 +15,7 @@ export default class AgentsTriggersPolicy extends BaseCommand {
15
15
  'max-per-hour': import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
16
16
  'working-hours': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
17
17
  'quiet-hours': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
18
+ 'followup-budget': import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
18
19
  };
19
20
  run(): Promise<void>;
20
21
  }
@@ -8,6 +8,28 @@ const triggers_1 = require("../../../lib/triggers");
8
8
  // used to carry its own copy that stopped one rung short, so `auto_run` was
9
9
  // accepted by the API and unreachable from the CLI.
10
10
  const data_provider_1 = require("@skrr-ai/data-provider");
11
+ /** `watch=10 checkin=default` → `{ watch: 10, checkin: null }`; the server validates the pools. */
12
+ function parseFollowUpBudget(values) {
13
+ if (!values || values.length === 0)
14
+ return undefined;
15
+ const budget = {};
16
+ for (const entry of (0, triggers_1.splitRepeated)(values) ?? []) {
17
+ const [pool, raw] = entry.split('=');
18
+ if (!pool || raw === undefined || raw === '') {
19
+ throw new Error(`--followup-budget expects pool=count, got "${entry}"`);
20
+ }
21
+ if (raw === 'default') {
22
+ budget[pool] = null;
23
+ continue;
24
+ }
25
+ const count = Number(raw);
26
+ if (!Number.isInteger(count) || count < 0) {
27
+ throw new Error(`--followup-budget ${pool} must be a non-negative integer or "default"`);
28
+ }
29
+ budget[pool] = count;
30
+ }
31
+ return budget;
32
+ }
11
33
  class AgentsTriggersPolicy extends base_command_1.BaseCommand {
12
34
  static description = "Show or update an agent's initiative policy for agent-scoped triggers";
13
35
  static examples = [
@@ -38,6 +60,10 @@ class AgentsTriggersPolicy extends base_command_1.BaseCommand {
38
60
  'max-per-hour': core_1.Flags.integer({ description: 'Agent initiative hourly fire cap' }),
39
61
  'working-hours': core_1.Flags.string({ description: 'Policy workingHours JSON object' }),
40
62
  'quiet-hours': core_1.Flags.string({ description: 'Policy quietHours JSON object' }),
63
+ 'followup-budget': core_1.Flags.string({
64
+ description: 'Daily follow-up creation cap for this agent, as pool=count (pools: watch, checkin, act; count "default" clears it). Repeatable',
65
+ multiple: true,
66
+ }),
41
67
  };
42
68
  async run() {
43
69
  this.requireAuth();
@@ -52,6 +78,7 @@ class AgentsTriggersPolicy extends base_command_1.BaseCommand {
52
78
  maxPerHour: flags['max-per-hour'],
53
79
  workingHours: (0, triggers_1.parseJsonObject)(flags['working-hours'], '--working-hours'),
54
80
  quietHours: (0, triggers_1.parseJsonObject)(flags['quiet-hours'], '--quiet-hours'),
81
+ followUpBudget: parseFollowUpBudget(flags['followup-budget']),
55
82
  });
56
83
  }
57
84
  catch (err) {
@@ -77,6 +104,11 @@ class AgentsTriggersPolicy extends base_command_1.BaseCommand {
77
104
  this.log(`Allowed kinds: ${Array.isArray(policy.allowedKinds) ? policy.allowedKinds.join(', ') : '-'}`);
78
105
  this.log(`Max fires/day: ${String(policy.maxFiresPerDay ?? '-')}`);
79
106
  this.log(`Max/hour: ${String(policy.maxPerHour ?? '-')}`);
107
+ const followUpBudget = policy.followUpBudget;
108
+ const budgetEntries = Object.entries(followUpBudget ?? {}).filter(([, v]) => v != null);
109
+ this.log(`Follow-up caps: ${budgetEntries.length > 0
110
+ ? budgetEntries.map(([pool, cap]) => `${pool} ${String(cap)}/day`).join(', ')
111
+ : 'defaults'}`);
80
112
  if (Object.keys(counts).length > 0) {
81
113
  this.log(`Triggers: ${String(counts.total ?? 0)} total, ${String(counts.enabled ?? 0)} enabled`);
82
114
  }
@@ -186,13 +186,14 @@ class AgentsUpdate extends base_command_1.BaseCommand {
186
186
  this.handleApiError(err);
187
187
  }
188
188
  // The same check `agents create` makes. Without it, a key the server does
189
- // not model — or a typo of one that it does, `max_turns` for `maxTurns` —
190
- // was discarded with the same "Updated agent" line as a real change.
189
+ // not model, a typo of one that it does (`max_turns` for `maxTurns`), or a
190
+ // field only the server sets was discarded with the same "Updated agent"
191
+ // line as a real change.
191
192
  // `removeProjectIds` is an instruction, not a stored field, so it never
192
193
  // comes back and is not reported.
193
194
  const unapplied = (0, create_1.unappliedPayloadKeys)(payload, updated, ['removeProjectIds']);
194
195
  if (unapplied.length > 0) {
195
- this.warn(`The server did not keep ${unapplied.join(', ')} — ${unapplied.length === 1 ? 'that field is' : 'those fields are'} not part of the agent. Check \`skrr agents show <id> --json\` for the field names it stores.`);
196
+ this.warn((0, create_1.unappliedPayloadKeysWarning)(unapplied));
196
197
  }
197
198
  if (flags.json) {
198
199
  this.log(JSON.stringify((0, web_url_1.withAgentUrl)(this.cliConfig.baseURL, updated), null, 2));
@@ -24,7 +24,7 @@ const outcomes = (terminal) => Object.entries(terminal)
24
24
  * every agent only with `--all-agents`.
25
25
  */
26
26
  class FollowupsObservability extends base_command_1.BaseCommand {
27
- static description = 'Per-agent, per-kind rollup — created, outcomes, escalation, budget, check-in silence';
27
+ static description = 'Per-agent, per-kind rollup — created, outcomes, escalation, spend, budget, check-in silence';
28
28
  static examples = [
29
29
  '<%= config.bin %> followups observability',
30
30
  '<%= config.bin %> followups observability --agent agent_1',
@@ -93,6 +93,9 @@ class FollowupsObservability extends base_command_1.BaseCommand {
93
93
  outcomes: outcomes(kind.terminal),
94
94
  escalated: rate(kind.escalationRate),
95
95
  fires: String(kind.fires),
96
+ spend: kind.spend
97
+ ? `$${kind.spend.today.toFixed(2)} / $${kind.spend.total.toFixed(2)}`
98
+ : '—',
96
99
  budget: kind.budget ? `${kind.budget.used}/${kind.budget.limit ?? '∞'}` : '—',
97
100
  silence: rate(kind.silenceRate),
98
101
  })), [
@@ -104,11 +107,12 @@ class FollowupsObservability extends base_command_1.BaseCommand {
104
107
  { key: 'outcomes', header: 'OUTCOMES' },
105
108
  { key: 'escalated', header: 'ESCALATED' },
106
109
  { key: 'fires', header: 'FIRES' },
110
+ { key: 'spend', header: 'SPENT TODAY/ALL' },
107
111
  { key: 'budget', header: 'TODAY/LIMIT' },
108
112
  { key: 'silence', header: 'SILENT' },
109
113
  ], (line) => this.log(line));
110
114
  });
111
- this.log(`\nAs of ${report.generatedAt ?? 'now'}. TODAY/LIMIT is the daily budget (reminders are not metered); SILENT is how often a check-in chose to stay quiet.`);
115
+ this.log(`\nAs of ${report.generatedAt ?? 'now'}. SPENT is the agent turns these follow-ups woke (USD, today / all time); TODAY/LIMIT is the daily creation budget (reminders are not metered); SILENT is how often a check-in chose to stay quiet.`);
112
116
  }
113
117
  }
114
118
  exports.default = FollowupsObservability;
@@ -15,11 +15,14 @@ class FollowupsRemind extends base_command_1.BaseCommand {
15
15
  static description = 'Remind the owner at a time — the reminder follow-up';
16
16
  static examples = [
17
17
  '<%= config.bin %> followups remind --subject task:OSK-42 --content "pick the deploy up" --in 2h',
18
- '<%= config.bin %> followups remind --subject conversation:conv_9 --content "ask how the move went" --at 2026-09-20T09:00:00Z',
18
+ '<%= config.bin %> followups remind --subject conversation:conv_9 --content "ask how the move went" --at 2026-09-20T09:00:00Z --active --agent agent_1',
19
19
  '<%= config.bin %> followups remind --subject task:OSK-42 --content "ping me" --at 2026-09-20T09:00:00Z --active --agent agent_1',
20
20
  ];
21
21
  static flags = {
22
- subject: core_1.Flags.string({ description: 'Subject ref: type:externalKey', required: true }),
22
+ subject: core_1.Flags.string({
23
+ description: 'Subject ref: type:externalKey. A passive reminder attaches to a task, project, goal or wiki_page; use --active for a conversation',
24
+ required: true,
25
+ }),
23
26
  content: core_1.Flags.string({ description: "The reminder's own words", required: true }),
24
27
  at: core_1.Flags.string({ description: 'ISO-8601 instant to remind at', exclusive: ['in'] }),
25
28
  in: core_1.Flags.string({
@@ -42,7 +42,8 @@ class FollowupsReschedule extends base_command_1.BaseCommand {
42
42
  this.log(JSON.stringify(result, null, 2));
43
43
  return;
44
44
  }
45
- this.log(`Follow-up ${args.id} rescheduled to ${at}.`);
45
+ const next = result.followup?.nextRunAt;
46
+ this.log(`Follow-up ${args.id} rescheduled to ${typeof next === 'string' ? next : at}.`);
46
47
  }
47
48
  }
48
49
  exports.default = FollowupsReschedule;
@@ -35,6 +35,10 @@ class FollowupsShow extends base_command_1.BaseCommand {
35
35
  ['intent', row.intentKey],
36
36
  ['producer', row.producer],
37
37
  ['terminal', row.terminal ? `${row.terminal} (${row.terminalReason ?? '—'})` : undefined],
38
+ [
39
+ 'settled',
40
+ row.terminal ? `${row.terminalAt ?? '—'} by ${row.terminalBy ?? 'unknown'}` : undefined,
41
+ ],
38
42
  ['deadline', row.deadlineAt],
39
43
  ['escalated to', row.escalatedInitiativeId],
40
44
  ]) {
@@ -15,6 +15,7 @@ import { BaseCommand } from '../../base-command';
15
15
  * cannot stop is a leak.
16
16
  */
17
17
  export default class FollowupsWatch extends BaseCommand {
18
+ static summary: string;
18
19
  static description: string;
19
20
  static examples: string[];
20
21
  static flags: {
@@ -21,7 +21,11 @@ const triggers_1 = require("../../lib/triggers");
21
21
  * cannot stop is a leak.
22
22
  */
23
23
  class FollowupsWatch extends base_command_1.BaseCommand {
24
- static description = 'Watch an asynchronous tail until it settles — the watch follow-up';
24
+ static summary = 'Watch an asynchronous tail until it settles — the watch follow-up';
25
+ static description = [
26
+ 'Two forms, and they cost different amounts. Prefer the predicate form whenever the state is readable over HTTPS: --url + --until is evaluated by the server, wakes the agent once when the condition settles, and always ends at --deadline.',
27
+ "The recipe form (--check / --success / --abort) wakes the agent for one turn per check, and every turn counts against the agent's watch budget. Use it only for a check a predicate cannot express.",
28
+ ].join('\n\n');
25
29
  static examples = [
26
30
  '<%= config.bin %> followups watch --subject task:OSK-42 --why "deploy still running" --url https://ci.example.com/api/build/9 --until \'{"path":"status","op":"equals","value":"done"}\' --deadline 6h',
27
31
  '<%= config.bin %> followups watch --subject task:OSK-42 --why "pr review" --check "gh pr view 42 --json state" --success "state == MERGED" --abort "state == CLOSED" --wait event --wait-entity github --wait-event pull_request_merged --wait-fallback 2h',
@@ -34,7 +38,7 @@ class FollowupsWatch extends base_command_1.BaseCommand {
34
38
  // predicate form
35
39
  url: core_1.Flags.string({ description: 'HTTPS endpoint to observe (predicate form)' }),
36
40
  until: core_1.Flags.string({
37
- description: 'Predicate tree as JSON (predicate form): a leaf {"path","op","value"} or {"all"|"any": [...]}. op is equals, not_equals, contains, starts_with, in, gt, lt, exists, … — an unknown op is refused with the full list',
41
+ description: 'Predicate tree as JSON (predicate form — the server evaluates it; no agent turn per check): a leaf {"path","op","value"} or {"all"|"any": [...]}. op is equals, not_equals, contains, starts_with, in, gt, lt, exists, … — an unknown op is refused with the full list',
38
42
  }),
39
43
  grant: core_1.Flags.string({ description: 'Grant id supplying the watch credentials' }),
40
44
  deadline: core_1.Flags.string({ description: 'When the watch must stop (predicate form, required)' }),
@@ -43,7 +47,9 @@ class FollowupsWatch extends base_command_1.BaseCommand {
43
47
  'root-path': core_1.Flags.string({ description: 'JSON root path inside the response' }),
44
48
  'resume-prompt': core_1.Flags.string({ description: 'What to tell the settling turn' }),
45
49
  // recipe form
46
- check: core_1.Flags.string({ description: 'The deterministic check to run (recipe form)' }),
50
+ check: core_1.Flags.string({
51
+ description: 'The check the agent runs (recipe form) — every check wakes the agent for one turn, counted against the watch budget',
52
+ }),
47
53
  success: core_1.Flags.string({ description: 'What state means the wait resolved (recipe form)' }),
48
54
  abort: core_1.Flags.string({ description: 'When to stop and escalate (recipe form)' }),
49
55
  wait: core_1.Flags.string({
@@ -72,11 +72,15 @@ class DedicatedMachinesCreate extends dedicated_lease_command_1.DedicatedLeaseCo
72
72
  if (flags.repository !== undefined && !(0, dedicated_machines_1.isDedicatedRepositoryRef)(flags.repository)) {
73
73
  this.error(`--repository must look like owner/repo, got "${flags.repository}".`, { exit: 2 });
74
74
  }
75
+ // The server trims the preset, so ' standard' buys a standard machine; the
76
+ // preview must judge the same value, or it calls a size it is about to buy
77
+ // "not in the catalog" and then buys it anyway (OSK-9592).
78
+ const sizePreset = flags['size-preset'].trim();
75
79
  const requestId = flags['request-id'] || newRequestId();
76
80
  const body = (0, dedicated_machines_1.dedicatedRuntimeCreateInput)({
77
81
  requestId,
78
82
  displayName: flags.name,
79
- sizePreset: flags['size-preset'],
83
+ sizePreset,
80
84
  monthlySpendingLimitCents: flags['spending-limit-cents'],
81
85
  regionClass: flags['region-class'],
82
86
  storageGb: flags['storage-gb'],
@@ -93,7 +97,7 @@ class DedicatedMachinesCreate extends dedicated_lease_command_1.DedicatedLeaseCo
93
97
  const catalog = await this.readCatalogForSummary();
94
98
  const summary = (0, dedicated_machines_1.dedicatedCreateSummary)(catalog, {
95
99
  name: flags.name,
96
- sizePreset: flags['size-preset'],
100
+ sizePreset,
97
101
  storageGb: flags['storage-gb'],
98
102
  retentionClass: flags['retention-class'],
99
103
  spendingLimitCents: flags['spending-limit-cents'],
@@ -73,16 +73,16 @@ class DedicatedMachinesDestroy extends dedicated_lease_command_1.DedicatedLeaseC
73
73
  else if (flags['retain-storage'])
74
74
  retentionIntent = 'retain';
75
75
  const response = await this.callDedicated(() => (0, dedicated_machines_1.destroyDedicatedRuntime)(leaseId, { ...this.scopeFrom(flags), retentionIntent }));
76
- await this.reportLeaseAction(response, flags, { kind: 'destroyed' }, 'Destroying');
77
- if (!flags.json) {
78
- const note = (0, dedicated_machines_1.dedicatedDestroyRetentionNote)(prior, {
79
- purge: Boolean(flags['purge-storage']),
80
- retain: Boolean(flags['retain-storage']),
81
- bin: this.config.bin,
82
- });
83
- if (note)
84
- this.log(note);
85
- }
76
+ const note = (0, dedicated_machines_1.dedicatedDestroyRetentionNote)(prior, {
77
+ purge: Boolean(flags['purge-storage']),
78
+ retain: Boolean(flags['retain-storage']),
79
+ bin: this.config.bin,
80
+ });
81
+ // A script reading --json is exactly the caller that never sees a printed
82
+ // line, so the kept-and-billing fact rides in the document too (OSK-9593).
83
+ await this.reportLeaseAction(response, flags, { kind: 'destroyed' }, 'Destroying', note ? { storageNote: note } : {});
84
+ if (!flags.json && note)
85
+ this.log(note);
86
86
  }
87
87
  }
88
88
  exports.default = DedicatedMachinesDestroy;
@@ -55,6 +55,8 @@ class DedicatedMachinesExec extends base_command_1.BaseCommand {
55
55
  // What a script depends on, in the help a script author reads (OSK-8703).
56
56
  static description = [
57
57
  'Runs the command once, as the workload user, in /workspace or --cwd, through a shell with no TTY. ' +
58
+ 'The workload user can read /workspace but writes only under user-data/ (its HOME) and repos/, so a ' +
59
+ 'command that writes to its starting directory needs --cwd user-data or a path under one of those. ' +
58
60
  'Give it after -- as words (each one quoted for you), or as one shell string with --command; not both. ' +
59
61
  'The agent shell policy does not apply. A start whose answer is lost is followed up under the same ' +
60
62
  'request id, never run a second time.',
@@ -95,7 +97,7 @@ class DedicatedMachinesExec extends base_command_1.BaseCommand {
95
97
  description: 'Command to run as one shell string, instead of the words after --',
96
98
  }),
97
99
  cwd: core_1.Flags.string({
98
- description: 'Working directory, relative to /workspace (for example repos/app)',
100
+ description: 'Working directory, relative to /workspace (for example repos/app or user-data). /workspace itself is not writable by the workload user.',
99
101
  }),
100
102
  timeout: core_1.Flags.string({
101
103
  description: `How long to let it run, such as 30s or 5m (default 2m, max ${dedicated_machines_1.DEDICATED_EXEC_MAX_TIMEOUT_MS / 60000}m). The process group is killed at the deadline.`,
@@ -0,0 +1,17 @@
1
+ import { DedicatedLeaseCommand } from '../../../lib/dedicated-lease-command';
2
+ /**
3
+ * `skrr machines dedicated price-book` — the operator's answer to "under what
4
+ * policy, and at what margin, is each tier sold".
5
+ *
6
+ * `catalog` is the customer view: prices and the book version, never a cost or
7
+ * a policy. This is admin-only on the server, because costs and margins are not
8
+ * customer facts.
9
+ */
10
+ export default class DedicatedMachinesPriceBook extends DedicatedLeaseCommand {
11
+ static description: string;
12
+ static examples: string[];
13
+ static flags: {
14
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
15
+ };
16
+ run(): Promise<void>;
17
+ }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const dedicated_lease_command_1 = require("../../../lib/dedicated-lease-command");
5
+ const dedicated_machines_1 = require("../../../lib/dedicated-machines");
6
+ /**
7
+ * `skrr machines dedicated price-book` — the operator's answer to "under what
8
+ * policy, and at what margin, is each tier sold".
9
+ *
10
+ * `catalog` is the customer view: prices and the book version, never a cost or
11
+ * a policy. This is admin-only on the server, because costs and margins are not
12
+ * customer facts.
13
+ */
14
+ class DedicatedMachinesPriceBook extends dedicated_lease_command_1.DedicatedLeaseCommand {
15
+ static description = 'Show the Dedicated Runtime price book an operator audits: version, pricing policy, and each tier rate, cost and margin (admin only)';
16
+ static examples = [
17
+ '<%= config.bin %> machines dedicated price-book',
18
+ '<%= config.bin %> machines dedicated price-book --json',
19
+ ];
20
+ static flags = {
21
+ json: core_1.Flags.boolean({ description: 'Output as JSON' }),
22
+ };
23
+ async run() {
24
+ this.requireAuth();
25
+ const { flags } = await this.parse(DedicatedMachinesPriceBook);
26
+ const read = await this.callDedicated(() => (0, dedicated_machines_1.getDedicatedOperatorPriceBook)());
27
+ if (flags.json) {
28
+ this.log(JSON.stringify(read, null, 2));
29
+ return;
30
+ }
31
+ for (const line of (0, dedicated_machines_1.dedicatedOperatorPriceBookLines)(read))
32
+ this.log(line);
33
+ }
34
+ }
35
+ exports.default = DedicatedMachinesPriceBook;
@@ -43,11 +43,26 @@ class TasksLifecycleDetach extends base_command_1.BaseCommand {
43
43
  return this.log('No lifecycle is attached to this Task — nothing to detach.');
44
44
  this.log(`Would detach ${lifecycle.packId ? `pack ${lifecycle.packId}` : `definition ${lifecycle.definitionId}`}` +
45
45
  ` at generation ${lifecycle.generation}, stage ${lifecycle.currentStageKey ?? '—'}.`);
46
- this.log(' The run is cancelled and kept as history; nothing already recorded is removed.');
47
- this.log(' The generation is bumped, so no in-flight writer from the old run can land.');
46
+ if (lifecycle.runId) {
47
+ this.log(' The run is cancelled and kept as history; nothing already recorded is removed.');
48
+ this.log(' The generation is bumped, so no in-flight writer from the old run can land.');
49
+ }
50
+ else {
51
+ this.log(' It has not started, so there is no run to cancel.');
52
+ }
48
53
  this.log(' Re-attaching afterwards starts a NEW generation — it does not resume this one.');
49
54
  return;
50
55
  }
56
+ // The server clears runId on detach, so whether there WAS a run to cancel
57
+ // is only knowable before the write. Best-effort: a failed read must not
58
+ // block the detach, it only costs the sentence its precision.
59
+ let hadRun;
60
+ try {
61
+ hadRun = Boolean((await data_provider_1.dataService.getTaskLifecycle(taskId)).lifecycle?.runId);
62
+ }
63
+ catch {
64
+ hadRun = undefined;
65
+ }
51
66
  let response;
52
67
  try {
53
68
  response = await data_provider_1.dataService.detachTaskLifecycle({
@@ -62,9 +77,15 @@ class TasksLifecycleDetach extends base_command_1.BaseCommand {
62
77
  return this.log(JSON.stringify(response, null, 2));
63
78
  const lifecycle = response.lifecycle;
64
79
  // Name what changed. "Detached lifecycle from the Task." left the reader to
65
- // diff the state afterwards to learn what had happened.
66
- this.log(`Detached${lifecycle?.packId ? ` pack ${lifecycle.packId}` : ''} — the run is cancelled and` +
67
- ` kept as history, and the binding is now generation ${lifecycle?.generation ?? '?'}` +
80
+ // diff the state afterwards to learn what had happened — and saying a run
81
+ // was cancelled when none had started is the same problem the other way.
82
+ const runSentence = hadRun === false
83
+ ? 'it had not started, so there was no run to cancel'
84
+ : hadRun
85
+ ? 'the run is cancelled and kept as history'
86
+ : 'any run is cancelled and kept as history';
87
+ this.log(`Detached${lifecycle?.packId ? ` pack ${lifecycle.packId}` : ''} — ${runSentence},` +
88
+ ` and the binding is now generation ${lifecycle?.generation ?? '?'}` +
68
89
  ` (${lifecycle?.status ?? 'detached'}).`);
69
90
  this.log('Re-attach with: skrr tasks lifecycle attach <task-id> --pack <pack>');
70
91
  }
@@ -61,7 +61,9 @@ export declare abstract class DedicatedLeaseCommand extends BaseCommand {
61
61
  timeout?: string;
62
62
  interval?: number;
63
63
  workspace?: string;
64
- }, goal: DedicatedWaitGoal | null, verb: string): Promise<void>;
64
+ }, goal: DedicatedWaitGoal | null, verb: string,
65
+ /** Fields a `--json` caller needs that the server response does not carry. */
66
+ jsonExtra?: Record<string, unknown>): Promise<void>;
65
67
  }
66
68
  /** The same transient verdict BaseCommand gives an ordinary API call. */
67
69
  export declare function dedicatedRuntimeApiErrorRetryable(err: unknown): boolean;
@@ -127,7 +127,10 @@ class DedicatedLeaseCommand extends base_command_1.BaseCommand {
127
127
  return await fn();
128
128
  }
129
129
  catch (err) {
130
- const message = (0, dedicated_machines_1.formatDedicatedRuntimeApiError)(err);
130
+ const message = (0, dedicated_machines_1.formatDedicatedRuntimeApiError)(err, {
131
+ command: this.id,
132
+ bin: this.config.bin,
133
+ });
131
134
  if (message) {
132
135
  this.failWithCliError({
133
136
  message,
@@ -145,7 +148,9 @@ class DedicatedLeaseCommand extends base_command_1.BaseCommand {
145
148
  * Print the lease an action returned, or wait for its operation to finish
146
149
  * first when `--wait` was passed.
147
150
  */
148
- async reportLeaseAction(response, flags, goal, verb) {
151
+ async reportLeaseAction(response, flags, goal, verb,
152
+ /** Fields a `--json` caller needs that the server response does not carry. */
153
+ jsonExtra = {}) {
149
154
  let lease = response?.lease;
150
155
  const id = (0, dedicated_machines_1.dedicatedLeaseId)(lease);
151
156
  if (flags.wait && goal && id) {
@@ -201,7 +206,7 @@ class DedicatedLeaseCommand extends base_command_1.BaseCommand {
201
206
  }
202
207
  }
203
208
  if (flags.json) {
204
- this.log(JSON.stringify({ ...(response || {}), lease }, null, 2));
209
+ this.log(JSON.stringify({ ...(response || {}), lease, ...jsonExtra }, null, 2));
205
210
  return;
206
211
  }
207
212
  this.log(`Dedicated Runtime ${id || 'unknown'} ${(0, dedicated_machines_1.dedicatedLeaseState)(lease)}.`);
@@ -221,6 +221,46 @@ export declare function dedicatedRuntimeCreateInput(input: {
221
221
  */
222
222
  export declare function isDedicatedRepositoryRef(value: string): boolean;
223
223
  export declare function createDedicatedRuntime(input: CreateDedicatedRuntimeInput): Promise<DedicatedRuntimeResponse>;
224
+ /** One offered tier as the operator price book checks it. */
225
+ export interface DedicatedPriceBookPresetCheck {
226
+ sizePreset: string;
227
+ customerRateCentsPerHour: number | null;
228
+ providerCostCentsPerHour: number | null;
229
+ grossMarginBps: number | null;
230
+ priced: boolean;
231
+ providerCostRecorded: boolean;
232
+ marginTargetMet: boolean;
233
+ }
234
+ /** `GET /api/machines/dedicated/price-book` — admin only. */
235
+ export interface DedicatedOperatorPriceBook {
236
+ timestamp?: string;
237
+ availability: 'available' | 'disabled';
238
+ disabledReason: string | null;
239
+ configError: string | null;
240
+ priceBook: {
241
+ required: boolean;
242
+ approved: boolean;
243
+ version: string | null;
244
+ pricingPolicyRef: string | null;
245
+ targetGrossMarginBps: number | null;
246
+ missingReason: string | null;
247
+ presetChecks: DedicatedPriceBookPresetCheck[];
248
+ };
249
+ sizeAvailability: Array<{
250
+ id: string;
251
+ offered: boolean;
252
+ withheldReason: string | null;
253
+ }>;
254
+ }
255
+ /**
256
+ * The Dedicated Runtime price book as an operator reads it: which version and
257
+ * which pricing policy, and each tier's rate, cost and margin. The customer
258
+ * catalog carries none of the policy or cost facts, and the admin plane that
259
+ * also serves this is not deployed in production (OSK-9562).
260
+ */
261
+ export declare function getDedicatedOperatorPriceBook(): Promise<DedicatedOperatorPriceBook>;
262
+ /** Human lines for `machines dedicated price-book`. Pure, so it can be pinned. */
263
+ export declare function dedicatedOperatorPriceBookLines(read: DedicatedOperatorPriceBook): string[];
224
264
  export declare function listDedicatedRuntimes(options?: {
225
265
  limit?: number;
226
266
  state?: string;
@@ -603,5 +643,12 @@ export declare function dedicatedRuntimeApiErrorCode(err: unknown): string | und
603
643
  * second list would only drift from the first.
604
644
  */
605
645
  export declare function dedicatedRuntimeApiErrorDetails(err: unknown): Record<string, unknown> | undefined;
606
- export declare function formatDedicatedRuntimeApiError(err: unknown): string | null;
646
+ /** What a refused command knows about itself, so advice can be one it can follow. */
647
+ export interface DedicatedApiErrorContext {
648
+ /** oclif command id, e.g. `machines:dedicated:create`. */
649
+ command?: string;
650
+ bin?: string;
651
+ leaseId?: string;
652
+ }
653
+ export declare function formatDedicatedRuntimeApiError(err: unknown, context?: DedicatedApiErrorContext): string | null;
607
654
  export {};