@skrr-ai/cli 0.1.88 → 0.1.90

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 (59) hide show
  1. package/dist/commands/machines/computer-adoption.d.ts +3 -2
  2. package/dist/commands/machines/computer-adoption.js +4 -3
  3. package/dist/commands/machines/dedicated/agent-browser.d.ts +25 -0
  4. package/dist/commands/machines/dedicated/agent-browser.js +65 -0
  5. package/dist/commands/machines/dedicated/exec.js +60 -14
  6. package/dist/commands/machines/dedicated/index.js +2 -0
  7. package/dist/commands/machines/dedicated/power/explain.d.ts +13 -0
  8. package/dist/commands/machines/dedicated/power/explain.js +36 -0
  9. package/dist/commands/machines/dedicated/power/index.d.ts +10 -0
  10. package/dist/commands/machines/dedicated/power/index.js +24 -0
  11. package/dist/commands/machines/dedicated/power/keep-awake.d.ts +20 -0
  12. package/dist/commands/machines/dedicated/power/keep-awake.js +69 -0
  13. package/dist/commands/machines/dedicated/power/set.d.ts +23 -0
  14. package/dist/commands/machines/dedicated/power/set.js +106 -0
  15. package/dist/commands/machines/dedicated/power/show.d.ts +13 -0
  16. package/dist/commands/machines/dedicated/power/show.js +36 -0
  17. package/dist/commands/machines/dedicated/show.js +17 -2
  18. package/dist/lib/commitments.d.ts +1 -1
  19. package/dist/lib/commitments.js +4 -2
  20. package/dist/lib/computer-adoption.d.ts +5 -0
  21. package/dist/lib/computer-adoption.js +6 -1
  22. package/dist/lib/computer-consent.d.ts +43 -1
  23. package/dist/lib/computer-consent.js +125 -4
  24. package/dist/lib/dedicated-keep-awake.d.ts +19 -0
  25. package/dist/lib/dedicated-keep-awake.js +64 -0
  26. package/dist/lib/dedicated-machines.d.ts +31 -0
  27. package/dist/lib/dedicated-machines.js +40 -0
  28. package/dist/lib/dedicated-power.d.ts +61 -0
  29. package/dist/lib/dedicated-power.js +243 -0
  30. package/dist/lib/exec-runtime-binary.d.ts +3 -1
  31. package/dist/lib/exec-runtime-binary.js +16 -2
  32. package/dist/lib/format.d.ts +13 -0
  33. package/dist/lib/format.js +51 -0
  34. package/dist/lib/keychain.js +3 -0
  35. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.d.ts +135 -8
  36. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.js +166 -16
  37. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dedicatedRuntimeBusyWire.d.ts +107 -0
  38. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dedicatedRuntimeBusyWire.js +151 -0
  39. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dedicatedRuntimeWakeWire.d.ts +115 -0
  40. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dedicatedRuntimeWakeWire.js +117 -0
  41. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
  42. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +8 -3
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +31 -7
  44. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +149 -9
  45. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.d.ts +135 -8
  46. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.js +161 -13
  47. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dedicatedRuntimeBusyWire.d.ts +107 -0
  48. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dedicatedRuntimeBusyWire.js +147 -0
  49. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dedicatedRuntimeWakeWire.d.ts +115 -0
  50. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dedicatedRuntimeWakeWire.js +113 -0
  51. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
  52. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
  53. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +31 -7
  54. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +144 -8
  55. package/dist/node_modules/@skrr-ai/auth-core/package.json +21 -1
  56. package/dist/node_modules/@skrr-ai/data-provider/index.js +6730 -6366
  57. package/dist/node_modules/@skrr-ai/data-provider/package.json +1 -1
  58. package/oclif.manifest.json +6303 -5852
  59. package/package.json +2 -2
@@ -744,7 +744,7 @@ export declare function describeAutonomyLine(commitment: CommitmentView, latestC
744
744
  } | null): string;
745
745
  /**
746
746
  * One-line rendering of `commitment.executorBlocked` — the B4/A7 signal that
747
- * the executor's targets are all unusable: `{reason:'quota'|'offline', until?,
747
+ * the executor's targets are all unusable: `{reason:'quota'|'offline'|'waiting_for_machine', until?,
748
748
  * provider?}`.
749
749
  *
750
750
  * Defensive by contract: the field is being added server-side, so its absence
@@ -874,7 +874,7 @@ function describeAutonomyLine(commitment, latestCheck) {
874
874
  }
875
875
  /**
876
876
  * One-line rendering of `commitment.executorBlocked` — the B4/A7 signal that
877
- * the executor's targets are all unusable: `{reason:'quota'|'offline', until?,
877
+ * the executor's targets are all unusable: `{reason:'quota'|'offline'|'waiting_for_machine', until?,
878
878
  * provider?}`.
879
879
  *
880
880
  * Defensive by contract: the field is being added server-side, so its absence
@@ -893,7 +893,9 @@ function describeExecutorBlocked(commitment) {
893
893
  ? `quota exhausted${provider ? ` on ${provider}` : ''}`
894
894
  : reason === 'offline'
895
895
  ? 'executor runtime offline'
896
- : reason || 'blocked';
896
+ : reason === 'waiting_for_machine'
897
+ ? 'executor machine is stopped and will be started for the work'
898
+ : reason || 'blocked';
897
899
  return `${label}${until ? ` until ${until}` : ''}`;
898
900
  }
899
901
  function renderCommitmentSummary(commitment, log,
@@ -45,6 +45,11 @@ export interface ComputerAdoption {
45
45
  oldFamilies: string[];
46
46
  daemonHalfMet: boolean | null;
47
47
  mobileHalfMet?: boolean | null;
48
+ /** Set once the families were retired; absent from a server before OSK-13509. */
49
+ retired?: {
50
+ on: string;
51
+ reason: string;
52
+ };
48
53
  };
49
54
  }
50
55
  /**
@@ -61,7 +61,12 @@ function computerAdoptionLines(read) {
61
61
  verdict = 'not measurable: no active daemons';
62
62
  else if (criterion.daemonHalfMet)
63
63
  verdict = 'MET';
64
- lines.push('', `Retirement of ${criterion.oldFamilies.join(', ')}`, ` daemon half (< ${(criterion.maxLegacyOnlyShare * 100).toFixed(0)}% of daemons active in ${criterion.windowDays} days on the old families only): ${verdict}`, mobileHalfLine(read));
64
+ lines.push('', `Retirement of ${criterion.oldFamilies.join(', ')}`);
65
+ if (criterion.retired) {
66
+ // The families are gone: the counts above are who it stranded, not who is still served.
67
+ lines.push(` RETIRED ${criterion.retired.on}: ${criterion.retired.reason}`, ' old-families-only daemons above can reach no terminal or browser; each is told to update when it registers');
68
+ }
69
+ lines.push(` daemon half (< ${(criterion.maxLegacyOnlyShare * 100).toFixed(0)}% of daemons active in ${criterion.windowDays} days on the old families only): ${verdict}`, mobileHalfLine(read));
65
70
  lines.push(...clientLines(read));
66
71
  return lines;
67
72
  }
@@ -1,4 +1,5 @@
1
1
  import { Command } from '@oclif/core';
2
+ import { type RuntimeBinaryOutput } from './exec-runtime-binary';
2
3
  /**
3
4
  * The bridge for `skrr computer` — the machine-local consent for the
4
5
  * computer's new surfaces on a personal machine (design §18, D11;
@@ -10,4 +11,45 @@ import { Command } from '@oclif/core';
10
11
  * Keeping the CLI a thin forwarder preserves that boundary: `skrr` carries
11
12
  * the verb to the same binary `skrrd` runs it on.
12
13
  */
13
- export declare function forwardComputerConsent(command: Command, passthrough: readonly string[]): Promise<void>;
14
+ /** Bound on the `skrrd status --json` probe: it is a read, and must never stall the command. */
15
+ export declare const DAEMON_STATUS_PROBE_TIMEOUT_MS = 8000;
16
+ /** What `skrrd status --json` says about the daemon that would answer. */
17
+ export interface AnsweringDaemon {
18
+ /** The binary `skrr` forwards to. */
19
+ binary: string | null;
20
+ /** The on-disk binary's version (`statusCommandVersion`), when it said. */
21
+ binaryVersion: string | null;
22
+ /** The running daemon's version (`liveVersion`/`version`), when it said. */
23
+ runningVersion: string | null;
24
+ daemonId: string | null;
25
+ profile: string | null;
26
+ /** `update.state` — `source-build`, `update-available`, `restart-pending`, … */
27
+ updateState: string | null;
28
+ /** `update.commands`: the install-method-correct way to update THIS daemon. */
29
+ updateCommands: string[];
30
+ restartPending: boolean;
31
+ }
32
+ export interface ComputerConsentDeps {
33
+ supportsSubcommand: (subcommand: string) => Promise<boolean | null>;
34
+ exec: (args: string[]) => Promise<number>;
35
+ capture: (args: string[], opts?: {
36
+ timeoutMs?: number;
37
+ }) => Promise<RuntimeBinaryOutput>;
38
+ findBinary: () => string | null;
39
+ }
40
+ /**
41
+ * Read `skrrd status --json` for the facts a person needs to know WHICH daemon
42
+ * answered and how it updates. Null when the status cannot be read — the
43
+ * caller then says what it can without inventing the rest.
44
+ */
45
+ export declare function readAnsweringDaemon(passthrough?: readonly string[], deps?: ComputerConsentDeps): Promise<AnsweringDaemon | null>;
46
+ /** "skrrd 0.8.103 at /path (daemon_…, running 0.8.90)" — whatever of that is known. */
47
+ export declare function describeAnsweringDaemon(daemon: AnsweringDaemon | null, binary: string | null): string;
48
+ /**
49
+ * What to tell a person whose local daemon has no `computer` verb: which
50
+ * daemon answered, and the update path for THAT install (OSK-13822). A source
51
+ * build updates by rebuilding its checkout — `skrrd status --json` already
52
+ * carries that recipe under `update.commands` — not by reinstalling.
53
+ */
54
+ export declare function computerUnsupportedMessage(daemon: AnsweringDaemon | null, binary: string | null): string;
55
+ export declare function forwardComputerConsent(command: Command, passthrough: readonly string[], deps?: ComputerConsentDeps): Promise<void>;
@@ -1,5 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DAEMON_STATUS_PROBE_TIMEOUT_MS = void 0;
4
+ exports.readAnsweringDaemon = readAnsweringDaemon;
5
+ exports.describeAnsweringDaemon = describeAnsweringDaemon;
6
+ exports.computerUnsupportedMessage = computerUnsupportedMessage;
3
7
  exports.forwardComputerConsent = forwardComputerConsent;
4
8
  const exec_runtime_binary_1 = require("./exec-runtime-binary");
5
9
  /**
@@ -13,15 +17,132 @@ const exec_runtime_binary_1 = require("./exec-runtime-binary");
13
17
  * Keeping the CLI a thin forwarder preserves that boundary: `skrr` carries
14
18
  * the verb to the same binary `skrrd` runs it on.
15
19
  */
16
- async function forwardComputerConsent(command, passthrough) {
20
+ /** Bound on the `skrrd status --json` probe: it is a read, and must never stall the command. */
21
+ exports.DAEMON_STATUS_PROBE_TIMEOUT_MS = 8_000;
22
+ const defaultDeps = {
23
+ supportsSubcommand: exec_runtime_binary_1.runtimeBinarySupportsSubcommand,
24
+ exec: (args) => (0, exec_runtime_binary_1.execRuntimeBinary)(args),
25
+ capture: (args, opts) => (0, exec_runtime_binary_1.execRuntimeBinaryCapture)(args, opts),
26
+ findBinary: exec_runtime_binary_1.findRuntimeBinary,
27
+ };
28
+ function text(value) {
29
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
30
+ }
31
+ /**
32
+ * Read `skrrd status --json` for the facts a person needs to know WHICH daemon
33
+ * answered and how it updates. Null when the status cannot be read — the
34
+ * caller then says what it can without inventing the rest.
35
+ */
36
+ async function readAnsweringDaemon(passthrough = [], deps = defaultDeps) {
37
+ let output;
38
+ try {
39
+ output = await deps.capture([...profileArgs(passthrough), 'status', '--json'], {
40
+ timeoutMs: exports.DAEMON_STATUS_PROBE_TIMEOUT_MS,
41
+ });
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ let status;
47
+ try {
48
+ status = JSON.parse(output.stdout);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ if (!status || typeof status !== 'object')
54
+ return null;
55
+ const update = status.update && typeof status.update === 'object'
56
+ ? status.update
57
+ : {};
58
+ return {
59
+ binary: deps.findBinary(),
60
+ binaryVersion: text(status.statusCommandVersion),
61
+ runningVersion: text(status.liveVersion) ?? text(status.version),
62
+ daemonId: text(status.daemonId),
63
+ profile: text(status.profile),
64
+ updateState: text(update.state),
65
+ updateCommands: Array.isArray(update.commands)
66
+ ? update.commands.filter((c) => typeof c === 'string' && c.trim() !== '')
67
+ : [],
68
+ restartPending: update.restartPending === true,
69
+ };
70
+ }
71
+ /** `--profile <name>` from the pass-through, so the status probe asks the same daemon. */
72
+ function profileArgs(passthrough) {
73
+ for (let i = 0; i < passthrough.length; i += 1) {
74
+ const arg = passthrough[i];
75
+ if (arg === '--profile' && passthrough[i + 1])
76
+ return ['--profile', passthrough[i + 1]];
77
+ if (arg.startsWith('--profile='))
78
+ return ['--profile', arg.slice('--profile='.length)];
79
+ }
80
+ return [];
81
+ }
82
+ /** "skrrd 0.8.103 at /path (daemon_…, running 0.8.90)" — whatever of that is known. */
83
+ function describeAnsweringDaemon(daemon, binary) {
84
+ const where = daemon?.binary ?? binary;
85
+ const parts = [];
86
+ parts.push(`${where ?? 'skrrd'}${daemon?.binaryVersion ? ` (v${daemon.binaryVersion} on disk)` : ''}`);
87
+ if (daemon?.runningVersion && daemon.runningVersion !== daemon.binaryVersion) {
88
+ parts.push(`running daemon v${daemon.runningVersion}`);
89
+ }
90
+ else if (daemon?.runningVersion && !daemon.binaryVersion) {
91
+ parts.push(`v${daemon.runningVersion}`);
92
+ }
93
+ if (daemon?.daemonId)
94
+ parts.push(daemon.daemonId);
95
+ return parts.join(', ');
96
+ }
97
+ /**
98
+ * What to tell a person whose local daemon has no `computer` verb: which
99
+ * daemon answered, and the update path for THAT install (OSK-13822). A source
100
+ * build updates by rebuilding its checkout — `skrrd status --json` already
101
+ * carries that recipe under `update.commands` — not by reinstalling.
102
+ */
103
+ function computerUnsupportedMessage(daemon, binary) {
104
+ const lines = [
105
+ `The local daemon does not support \`skrr computer\` yet: ${describeAnsweringDaemon(daemon, binary)}.`,
106
+ ];
107
+ if (daemon?.updateState === 'source-build') {
108
+ const recipe = daemon.updateCommands.length
109
+ ? daemon.updateCommands
110
+ : ['cd <daemon-dir> && git pull && npm run build'];
111
+ lines.push('It runs from a source checkout, which updates by rebuilding:');
112
+ for (const command of recipe)
113
+ lines.push(` ${command}`);
114
+ lines.push('Then restart it:', ' skrr daemon restart');
115
+ }
116
+ else if (daemon?.restartPending) {
117
+ lines.push('A newer daemon is already on disk; the running one predates it. Restart it:', ' skrr daemon restart');
118
+ }
119
+ else if (daemon && daemon.updateCommands.length > 0) {
120
+ lines.push('Update it:');
121
+ for (const command of daemon.updateCommands)
122
+ lines.push(` ${command}`);
123
+ lines.push('Then restart it:', ' skrr daemon restart');
124
+ }
125
+ else {
126
+ lines.push('Update it (`skrr daemon update`, or reinstall it), restart it (`skrr daemon restart`), then retry.');
127
+ }
128
+ return lines.join('\n');
129
+ }
130
+ async function forwardComputerConsent(command, passthrough, deps = defaultDeps) {
17
131
  try {
18
132
  // Older daemons treated unknown subcommands as a daemon startup in some
19
133
  // releases. Probe the command list before forwarding so a consent verb
20
134
  // cannot turn into a surprising service mutation.
21
- if ((await (0, exec_runtime_binary_1.runtimeBinarySupportsSubcommand)('computer')) === false) {
22
- command.error('The local `skrrd` daemon does not support `skrr computer` yet. Update or reinstall the daemon, then retry.', { exit: 2 });
135
+ if ((await deps.supportsSubcommand('computer')) === false) {
136
+ const daemon = await readAnsweringDaemon(passthrough, deps);
137
+ command.error(computerUnsupportedMessage(daemon, deps.findBinary()), { exit: 2 });
138
+ }
139
+ if (passthrough.includes('status')) {
140
+ // Which daemon answered, before its answer — on stderr, so `--json`
141
+ // output stays exactly what the daemon printed.
142
+ const daemon = await readAnsweringDaemon(passthrough, deps);
143
+ command.logToStderr(`Answered by: ${describeAnsweringDaemon(daemon, deps.findBinary())}`);
23
144
  }
24
- command.exit(await (0, exec_runtime_binary_1.execRuntimeBinary)(['computer', ...passthrough]));
145
+ command.exit(await deps.exec(['computer', ...passthrough]));
25
146
  }
26
147
  catch (error) {
27
148
  if (error.message === 'runtime-binary-not-installed') {
@@ -0,0 +1,19 @@
1
+ import { type DedicatedRuntimeKeepAwakeResponse, type DedicatedRuntimeKeepAwakeState, type SetDedicatedRuntimeKeepAwakeRequest } from '@skrr-ai/data-provider';
2
+ import { type WhenOptions } from './format';
3
+ import { type DedicatedRuntimeScope } from './dedicated-machines';
4
+ /**
5
+ * Domain lib for `skrr machines dedicated power keep-awake` (K1). Keep-awake is a
6
+ * temporary override that forbids AUTOMATIC stop; it never starts the machine.
7
+ * The web calls the same routes through `dataService.setDedicatedRuntimeKeepAwake`.
8
+ */
9
+ export declare function setDedicatedKeepAwake(leaseId: string, request: SetDedicatedRuntimeKeepAwakeRequest, scope?: DedicatedRuntimeScope): Promise<DedicatedRuntimeKeepAwakeResponse>;
10
+ export declare function clearDedicatedKeepAwake(leaseId: string, scope?: DedicatedRuntimeScope): Promise<DedicatedRuntimeKeepAwakeResponse>;
11
+ /**
12
+ * `--for` takes a whole number with a unit: `90m`, `2h`, `3d`. A bare number is
13
+ * minutes (the unit the API speaks). Returns minutes, or throws a plain message.
14
+ */
15
+ export declare function parseKeepAwakeDuration(value: string): number;
16
+ export declare function assertKeepAwakeMinutes(minutes: number): void;
17
+ /** `--until` takes any ISO 8601 date. The server re-checks the bounds. */
18
+ export declare function parseKeepAwakeUntil(value: string): string;
19
+ export declare function describeKeepAwake(state: DedicatedRuntimeKeepAwakeState, when?: WhenOptions): string;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setDedicatedKeepAwake = setDedicatedKeepAwake;
4
+ exports.clearDedicatedKeepAwake = clearDedicatedKeepAwake;
5
+ exports.parseKeepAwakeDuration = parseKeepAwakeDuration;
6
+ exports.assertKeepAwakeMinutes = assertKeepAwakeMinutes;
7
+ exports.parseKeepAwakeUntil = parseKeepAwakeUntil;
8
+ exports.describeKeepAwake = describeKeepAwake;
9
+ const data_provider_1 = require("@skrr-ai/data-provider");
10
+ const api_fetch_1 = require("./api-fetch");
11
+ const format_1 = require("./format");
12
+ const dedicated_machines_1 = require("./dedicated-machines");
13
+ /**
14
+ * Domain lib for `skrr machines dedicated power keep-awake` (K1). Keep-awake is a
15
+ * temporary override that forbids AUTOMATIC stop; it never starts the machine.
16
+ * The web calls the same routes through `dataService.setDedicatedRuntimeKeepAwake`.
17
+ */
18
+ function setDedicatedKeepAwake(leaseId, request, scope = {}) {
19
+ return (0, api_fetch_1.apiFetch)((0, dedicated_machines_1.withDedicatedScope)(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/power/keep-awake`, scope), {
20
+ method: 'PUT',
21
+ body: request,
22
+ });
23
+ }
24
+ function clearDedicatedKeepAwake(leaseId, scope = {}) {
25
+ return (0, api_fetch_1.apiFetch)((0, dedicated_machines_1.withDedicatedScope)(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/power/keep-awake`, scope), {
26
+ method: 'DELETE',
27
+ });
28
+ }
29
+ /**
30
+ * `--for` takes a whole number with a unit: `90m`, `2h`, `3d`. A bare number is
31
+ * minutes (the unit the API speaks). Returns minutes, or throws a plain message.
32
+ */
33
+ function parseKeepAwakeDuration(value) {
34
+ const match = value.trim().match(/^(\d+)\s*(m|h|d)?$/i);
35
+ if (!match) {
36
+ throw new Error(`Invalid --for value "${value}". Use a number with m, h or d, such as 90m, 2h or 3d.`);
37
+ }
38
+ const amount = Number(match[1]);
39
+ const unit = (match[2] || 'm').toLowerCase();
40
+ const minutes = unit === 'd' ? amount * 1440 : unit === 'h' ? amount * 60 : amount;
41
+ assertKeepAwakeMinutes(minutes);
42
+ return minutes;
43
+ }
44
+ function assertKeepAwakeMinutes(minutes) {
45
+ if (minutes < data_provider_1.DEDICATED_KEEP_AWAKE_MINUTES_MIN) {
46
+ throw new Error(`Keep-awake must last at least ${data_provider_1.DEDICATED_KEEP_AWAKE_MINUTES_MIN} minutes.`);
47
+ }
48
+ if (minutes > data_provider_1.DEDICATED_KEEP_AWAKE_MINUTES_MAX) {
49
+ throw new Error(`Keep-awake can last at most ${data_provider_1.DEDICATED_KEEP_AWAKE_MINUTES_MAX / 1440} days. For longer, use \`power set --policy always_on\`.`);
50
+ }
51
+ }
52
+ /** `--until` takes any ISO 8601 date. The server re-checks the bounds. */
53
+ function parseKeepAwakeUntil(value) {
54
+ const at = new Date(value);
55
+ if (Number.isNaN(at.getTime())) {
56
+ throw new Error(`Invalid --until value "${value}". Use an ISO date such as 2026-10-10T18:00:00Z.`);
57
+ }
58
+ return at.toISOString();
59
+ }
60
+ function describeKeepAwake(state, when = {}) {
61
+ return state.active && state.until
62
+ ? `is kept awake until ${(0, format_1.formatWhen)(state.until, when)}`
63
+ : 'has no keep-awake; it follows its power policy';
64
+ }
@@ -358,6 +358,34 @@ export declare function restoreDeletedDedicatedRuntime(leaseId: string, options?
358
358
  * on the next reconciler pass.
359
359
  */
360
360
  export declare function setDedicatedRuntimeSpendingLimit(leaseId: string, spendingLimitCents: number | null, scope?: DedicatedRuntimeScope): Promise<DedicatedRuntimeResponse>;
361
+ /**
362
+ * Whether the agents working on a Dedicated Runtime get a browser (OSK-13809).
363
+ *
364
+ * `state` is the machine's setting; `offered` is whether its CONNECTED daemon
365
+ * offers the browser right now (null: nothing connected, or unreadable). The
366
+ * two differ after a change until the daemon reconnects, and on an image
367
+ * without Chrome, which never offers it.
368
+ */
369
+ export type DedicatedAgentBrowserState = {
370
+ leaseId: string;
371
+ state: 'on' | 'off' | 'pending_first_start';
372
+ enabled: boolean;
373
+ offered: boolean | null;
374
+ capability: string;
375
+ changed?: boolean;
376
+ effectiveOnNextReconnect?: boolean;
377
+ };
378
+ export declare function getDedicatedRuntimeAgentBrowser(leaseId: string, scope?: DedicatedRuntimeScope): Promise<DedicatedAgentBrowserState>;
379
+ /** Turn the agents' browser on or off. An actual change is in the machine's audit record. */
380
+ export declare function setDedicatedRuntimeAgentBrowser(leaseId: string, enabled: boolean, scope?: DedicatedRuntimeScope): Promise<DedicatedAgentBrowserState>;
381
+ /**
382
+ * One line for a person. Says the bad cases out loud: a setting that is on
383
+ * while the connected daemon does not offer it yet, and a setting we could
384
+ * not read at all.
385
+ */
386
+ export declare function describeDedicatedAgentBrowser(value: DedicatedAgentBrowserState | {
387
+ error: string;
388
+ } | null | undefined, leaseId: string): string;
361
389
  export declare function closeDedicatedRuntimeIncidentHold(leaseId: string, options?: {
362
390
  requestId?: string;
363
391
  } & DedicatedRuntimeScope): Promise<DedicatedRuntimeResponse>;
@@ -627,6 +655,9 @@ export declare function dedicatedSpendSummary(lease: DedicatedLease): string;
627
655
  export declare function describeDedicatedDeletion(deletion: DedicatedLeaseDeletion, leaseId?: string): string;
628
656
  export declare function describeDedicatedLease(lease: DedicatedLease, harnesses?: readonly Harness[], opts?: {
629
657
  latestDaemonRelease?: LatestDaemonRelease | null;
658
+ agentBrowser?: DedicatedAgentBrowserState | {
659
+ error: string;
660
+ } | null;
630
661
  }): string[];
631
662
  /** "2026-09-30 14:00 UTC (in 17 days)" — a deadline readable without converting it. */
632
663
  export declare function formatDedicatedDeadline(value: string | null | undefined, now?: Date): string;
@@ -17,6 +17,9 @@ exports.performDedicatedLeaseAction = performDedicatedLeaseAction;
17
17
  exports.growDedicatedRuntime = growDedicatedRuntime;
18
18
  exports.restoreDeletedDedicatedRuntime = restoreDeletedDedicatedRuntime;
19
19
  exports.setDedicatedRuntimeSpendingLimit = setDedicatedRuntimeSpendingLimit;
20
+ exports.getDedicatedRuntimeAgentBrowser = getDedicatedRuntimeAgentBrowser;
21
+ exports.setDedicatedRuntimeAgentBrowser = setDedicatedRuntimeAgentBrowser;
22
+ exports.describeDedicatedAgentBrowser = describeDedicatedAgentBrowser;
20
23
  exports.closeDedicatedRuntimeIncidentHold = closeDedicatedRuntimeIncidentHold;
21
24
  exports.destroyDedicatedRuntime = destroyDedicatedRuntime;
22
25
  exports.execDedicatedRuntime = execDedicatedRuntime;
@@ -368,6 +371,40 @@ async function restoreDeletedDedicatedRuntime(leaseId, options = {}) {
368
371
  async function setDedicatedRuntimeSpendingLimit(leaseId, spendingLimitCents, scope = {}) {
369
372
  return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/leases/${encodeURIComponent(leaseId)}/billing`, scope), { method: 'PATCH', body: { spendingLimitCents } });
370
373
  }
374
+ async function getDedicatedRuntimeAgentBrowser(leaseId, scope = {}) {
375
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/agent-browser`, scope));
376
+ }
377
+ /** Turn the agents' browser on or off. An actual change is in the machine's audit record. */
378
+ async function setDedicatedRuntimeAgentBrowser(leaseId, enabled, scope = {}) {
379
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/agent-browser`, scope), { method: 'PUT', body: { enabled } });
380
+ }
381
+ /**
382
+ * One line for a person. Says the bad cases out loud: a setting that is on
383
+ * while the connected daemon does not offer it yet, and a setting we could
384
+ * not read at all.
385
+ */
386
+ function describeDedicatedAgentBrowser(value, leaseId) {
387
+ if (!value)
388
+ return 'unknown';
389
+ if ('error' in value)
390
+ return `unknown (could not read: ${value.error})`;
391
+ if (value.state === 'pending_first_start') {
392
+ return value.enabled
393
+ ? 'on when the machine first starts (the default for new machines)'
394
+ : 'off when the machine first starts';
395
+ }
396
+ if (value.state === 'off') {
397
+ return value.offered
398
+ ? `off — sessions already running keep theirs until the daemon reconnects (\`skrr machines dedicated restart ${leaseId}\`)`
399
+ : `off — turn on with \`skrr machines dedicated agent-browser ${leaseId} --on\``;
400
+ }
401
+ if (value.offered === true)
402
+ return 'on — every agent session can open one; you can watch and take it over';
403
+ if (value.offered === false) {
404
+ return `on, but the connected daemon does not offer it yet: it applies when the daemon reconnects (\`skrr machines dedicated restart ${leaseId}\`), and an image without Chrome never offers it`;
405
+ }
406
+ return 'on (not connected now, so whether the machine offers it is unknown)';
407
+ }
371
408
  async function closeDedicatedRuntimeIncidentHold(leaseId, options = {}) {
372
409
  return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/incident-hold/close`, options), {
373
410
  method: 'POST',
@@ -813,6 +850,9 @@ function describeDedicatedLease(lease, harnesses = [], opts = {}) {
813
850
  if (checkpoint)
814
851
  lines.push(` ${''.padEnd(13)}${checkpoint.sentence}`);
815
852
  }
853
+ if (opts.agentBrowser !== undefined) {
854
+ row('agent browser', describeDedicatedAgentBrowser(opts.agentBrowser, dedicatedLeaseId(lease)));
855
+ }
816
856
  const engines = dedicatedLeaseHarnesses(lease, harnesses);
817
857
  if (engines.length > 0) {
818
858
  row('engines', engines.map((harness) => `${harness.provider} (${harness.status || 'unknown'})`).join(', '));
@@ -0,0 +1,61 @@
1
+ import { type DedicatedRuntimePowerSummary, type DedicatedPowerPolicy, type DedicatedRuntimePowerPolicyState, type OnDemandIneligibleError, type SetDedicatedRuntimePowerPolicyResponse } from '@skrr-ai/data-provider';
2
+ import { type WhenOptions } from './format';
3
+ import { type DedicatedRuntimeScope } from './dedicated-machines';
4
+ /**
5
+ * Domain lib for `skrr machines dedicated power …` (W6). Like its siblings in
6
+ * `dedicated-machines.ts` it speaks `apiFetch` so every refusal arrives as an
7
+ * `ApiFetchError` the Dedicated commands already know how to word; the route is
8
+ * covered by the machines parity spec through this file. The web calls the same
9
+ * route through `dataService.setDedicatedRuntimePowerPolicy`.
10
+ */
11
+ export type SetPowerOutcome = {
12
+ ok: true;
13
+ response: SetDedicatedRuntimePowerPolicyResponse;
14
+ } | {
15
+ ok: false;
16
+ refusal: OnDemandIneligibleError;
17
+ };
18
+ /**
19
+ * Set the policy. An ineligible `on_demand` is a RESULT, not an exception: the
20
+ * refusal is the answer the caller must explain (reason + fix). Every other
21
+ * failure is rethrown for the command's usual error handling.
22
+ */
23
+ export declare function setDedicatedPowerPolicy(leaseId: string, request: {
24
+ policy?: DedicatedPowerPolicy;
25
+ idleAfterMinutes?: number;
26
+ budgetHoursPerMonth?: number | null;
27
+ }, scope?: DedicatedRuntimeScope): Promise<SetPowerOutcome>;
28
+ /**
29
+ * The policy in words. `on_demand` promises a stop only when the platform stops idle machines at all
30
+ * (`autoStopMode`); an unknown mode is never read as on, so the sentence never promises one.
31
+ */
32
+ export declare function describePolicyWords(policy: DedicatedPowerPolicy, idleAfterMinutes: number, autoStopMode?: 'off' | 'canary' | 'on'): string;
33
+ export declare function describePowerPolicy(power: DedicatedRuntimePowerPolicyState, autoStopMode?: 'off' | 'canary' | 'on'): string;
34
+ /** `GET /api/machines/dedicated/:leaseId/power` — the read-only power card. */
35
+ export declare function getDedicatedPowerSummary(leaseId: string, scope?: DedicatedRuntimeScope): Promise<DedicatedRuntimePowerSummary>;
36
+ type PowerDecisionView = NonNullable<DedicatedRuntimePowerSummary['decision']>;
37
+ /** The stay/stop/wake decision in words: the shared reason copy backs the server's detail. */
38
+ export declare function describePowerDecisionLines(decision: PowerDecisionView): {
39
+ headline: string;
40
+ detail: string;
41
+ fix: string | null;
42
+ };
43
+ /**
44
+ * The last automatic stop and how it was judged (A3). Every word comes from the shared copy
45
+ * (`describeAutoStopVerdict`, `describeMachineCheckpointOutcome`); nothing is composed here.
46
+ * `verdict: null` is "not judged yet", and is said so: it is never rendered as clean. A sentence
47
+ * the decision text already carries (the server appends it while the machine is stopped) is not
48
+ * repeated.
49
+ */
50
+ export declare function describeLastAutoStopLines(last: DedicatedRuntimePowerSummary['lastAutoStop'], decisionDetail?: string, when?: WhenOptions): {
51
+ lines: string[];
52
+ fix: string | null;
53
+ };
54
+ /** The latest wake decision and who asked, as lines; empty when there is none. */
55
+ export declare function describeLastWakeDecisionLines(wake: DedicatedRuntimePowerSummary['lastWakeDecision'], when?: WhenOptions): string[];
56
+ /** The lines of `power show`. */
57
+ export declare function describePowerSummary(summary: DedicatedRuntimePowerSummary, when?: WhenOptions): string[];
58
+ /** The lines of `power explain`; the fix is always last. */
59
+ export declare function describePowerExplanation(summary: DedicatedRuntimePowerSummary, when?: WhenOptions): string[];
60
+ export declare function describeBudget(power: DedicatedRuntimePowerPolicyState): string;
61
+ export {};