@skrr-ai/cli 0.1.33 → 0.1.34

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.
@@ -203,6 +203,11 @@ class BaseCommand extends core_1.Command {
203
203
  (0, data_provider_1.setHttpAdapter)((0, node_adapter_1.createNodeAdapter)());
204
204
  return;
205
205
  }
206
+ // A replacement guest keeps its workspace but receives a fresh Linux
207
+ // machine-id. Retire the prior instance's machine-bound CLI envelopes
208
+ // before migration/prefetch reads them; the current daemon handoff below
209
+ // supplies fresh authority without a user login.
210
+ await (0, auth_core_init_1.prepareDedicatedRuntimeAuthGeneration)();
206
211
  // One-shot migration: legacy token/refreshToken in cli-config.json
207
212
  // → keychain (macOS) or cli-auth.json (elsewhere). Idempotent.
208
213
  try {
@@ -14,7 +14,8 @@ class LearningDelete extends base_command_1.BaseCommand {
14
14
  const { flags } = await this.parse(LearningDelete);
15
15
  if (!flags.yes &&
16
16
  !(await this.confirm('Delete all adaptive-learning state, episodes, and outcomes? [y/N] '))) {
17
- return;
17
+ this.log('Cancelled.');
18
+ this.exit(1);
18
19
  }
19
20
  let result;
20
21
  try {
@@ -1,5 +1,9 @@
1
1
  import { BaseCommand } from '../../base-command';
2
2
  export default class LearningExport extends BaseCommand {
3
3
  static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ output: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
7
+ };
4
8
  run(): Promise<void>;
5
9
  }
@@ -1,12 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const promises_1 = require("node:fs/promises");
4
+ const core_1 = require("@oclif/core");
3
5
  const base_command_1 = require("../../base-command");
4
6
  const api_fetch_1 = require("../../lib/api-fetch");
5
7
  class LearningExport extends base_command_1.BaseCommand {
6
8
  static description = 'Export your adaptive-learning consent, state, episodes, and outcomes as JSON';
9
+ static examples = [
10
+ '<%= config.bin %> learning export --output learning-data.json',
11
+ '<%= config.bin %> learning export --output - | jq .consent',
12
+ ];
13
+ static flags = {
14
+ output: core_1.Flags.string({
15
+ char: 'o',
16
+ description: 'Required destination path; use "-" only for deliberate stdout output',
17
+ required: true,
18
+ }),
19
+ };
7
20
  async run() {
8
21
  this.requireAuth();
9
- await this.parse(LearningExport);
22
+ const { flags } = await this.parse(LearningExport);
10
23
  let result;
11
24
  try {
12
25
  result = await (0, api_fetch_1.apiFetch)('/api/user/adaptive-learning/export', {
@@ -16,7 +29,28 @@ class LearningExport extends base_command_1.BaseCommand {
16
29
  catch (err) {
17
30
  this.handleApiError(err);
18
31
  }
19
- this.log(JSON.stringify(result, null, 2));
32
+ const serialized = `${JSON.stringify(result, null, 2)}\n`;
33
+ if (flags.output === '-') {
34
+ this.log(serialized.trimEnd());
35
+ return;
36
+ }
37
+ try {
38
+ await (0, promises_1.writeFile)(flags.output, serialized, {
39
+ encoding: 'utf8',
40
+ flag: 'wx',
41
+ mode: 0o600,
42
+ });
43
+ }
44
+ catch (err) {
45
+ this.failWithCliError({
46
+ message: `Could not write ${flags.output}: ${err.message}`,
47
+ code: 'ADAPTIVE_LEARNING_EXPORT_WRITE_FAILED',
48
+ exit: 1,
49
+ retryable: false,
50
+ details: { path: flags.output },
51
+ });
52
+ }
53
+ this.log(`Wrote adaptive-learning export to ${flags.output}.`);
20
54
  }
21
55
  }
22
56
  exports.default = LearningExport;
@@ -12,8 +12,10 @@ class LearningReset extends base_command_1.BaseCommand {
12
12
  async run() {
13
13
  this.requireAuth();
14
14
  const { flags } = await this.parse(LearningReset);
15
- if (!flags.yes && !(await this.confirm('Reset all learned serving state? [y/N] ')))
16
- return;
15
+ if (!flags.yes && !(await this.confirm('Reset all learned serving state? [y/N] '))) {
16
+ this.log('Cancelled.');
17
+ this.exit(1);
18
+ }
17
19
  let result;
18
20
  try {
19
21
  result = await (0, api_fetch_1.apiFetch)('/api/user/adaptive-learning/reset', {
@@ -1,4 +1,26 @@
1
1
  import { type AuthBundle } from './auth-storage';
2
+ type DedicatedGenerationPreparationOptions = {
3
+ machineUuid?: string;
4
+ configDir?: string;
5
+ clearStoredAuth?: () => void;
6
+ clearCredentialEnvelope?: () => Promise<void>;
7
+ clearDeviceIdentity?: () => Promise<void>;
8
+ };
9
+ /**
10
+ * A Dedicated Runtime's workspace survives replacement but `/etc/machine-id`
11
+ * intentionally does not. Linux's fallback KEK is derived from that id, so a
12
+ * CLI credential persisted by the prior instance is unreadable on the next
13
+ * one. The guest never needs that stale authority: its root-owned loopback
14
+ * handoff descriptor mints a fresh CLI credential from the current daemon.
15
+ *
16
+ * Clear only the workload CLI's machine-bound auth artifacts once per runtime
17
+ * generation, before auth-core tries to decrypt them. A cross-process lock is
18
+ * required because terminals and agent tools can launch several `skrr`
19
+ * processes as soon as the guest becomes ready; without it they can clear a
20
+ * sibling's just-enrolled credential. The marker contains identity, not a
21
+ * secret, and stays in the persistent workspace beside the auth files.
22
+ */
23
+ export declare function prepareDedicatedRuntimeAuthGeneration(options?: DedicatedGenerationPreparationOptions): Promise<boolean>;
2
24
  export declare function ensureAuthCoreConfigured(): void;
3
25
  export declare class CliCredentialInitializationError extends Error {
4
26
  readonly code = "CREDENTIAL_INITIALIZATION_UNAVAILABLE";
@@ -42,3 +64,4 @@ export declare function waitForCliCredentialEnvelope(): Promise<void>;
42
64
  /** @internal Test seam — reset the module-level one-shot initialization. */
43
65
  export declare function __resetPrefetchCliAuthForTest(): void;
44
66
  export declare function shouldUseAccessTokenForEnrollment(bundle: Pick<AuthBundle, 'token' | 'expiresAt'> | null | undefined, nowMs?: number, minTtlMs?: number): boolean;
67
+ export {};
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.CliCredentialInitializationError = void 0;
7
+ exports.prepareDedicatedRuntimeAuthGeneration = prepareDedicatedRuntimeAuthGeneration;
7
8
  exports.ensureAuthCoreConfigured = ensureAuthCoreConfigured;
8
9
  exports.prefetchCliAuth = prefetchCliAuth;
9
10
  exports.waitForCliCredentialEnvelope = waitForCliCredentialEnvelope;
@@ -33,8 +34,10 @@ exports.shouldUseAccessTokenForEnrollment = shouldUseAccessTokenForEnrollment;
33
34
  * - Fire-and-forget enrollment when an access token is already
34
35
  * present — short-circuits silently on a fresh install before login.
35
36
  */
37
+ const node_fs_1 = __importDefault(require("node:fs"));
36
38
  const node_os_1 = __importDefault(require("node:os"));
37
39
  const node_path_1 = __importDefault(require("node:path"));
40
+ const proper_lockfile_1 = __importDefault(require("proper-lockfile"));
38
41
  const auth_core_1 = require("@skrr-ai/auth-core");
39
42
  const cred_envelope_1 = require("./cred-envelope");
40
43
  const device_identity_1 = require("./device-identity");
@@ -64,6 +67,64 @@ const cliLogger = {
64
67
  * so cross-binary file paths align byte-for-byte.
65
68
  */
66
69
  const DEFAULT_PROFILE = 'default';
70
+ const DEDICATED_AUTH_GENERATION_MARKER = 'dedicated-runtime-auth-generation';
71
+ /**
72
+ * A Dedicated Runtime's workspace survives replacement but `/etc/machine-id`
73
+ * intentionally does not. Linux's fallback KEK is derived from that id, so a
74
+ * CLI credential persisted by the prior instance is unreadable on the next
75
+ * one. The guest never needs that stale authority: its root-owned loopback
76
+ * handoff descriptor mints a fresh CLI credential from the current daemon.
77
+ *
78
+ * Clear only the workload CLI's machine-bound auth artifacts once per runtime
79
+ * generation, before auth-core tries to decrypt them. A cross-process lock is
80
+ * required because terminals and agent tools can launch several `skrr`
81
+ * processes as soon as the guest becomes ready; without it they can clear a
82
+ * sibling's just-enrolled credential. The marker contains identity, not a
83
+ * secret, and stays in the persistent workspace beside the auth files.
84
+ */
85
+ async function prepareDedicatedRuntimeAuthGeneration(options = {}) {
86
+ const machineUuid = String(options.machineUuid ?? process.env.SKRR_MACHINE_UUID ?? '').trim();
87
+ if (!machineUuid.startsWith('dedicated-runtime:'))
88
+ return false;
89
+ const configDir = options.configDir ?? node_path_1.default.join(node_os_1.default.homedir(), '.skrr');
90
+ node_fs_1.default.mkdirSync(configDir, { recursive: true, mode: 0o700 });
91
+ const markerPath = node_path_1.default.join(configDir, DEDICATED_AUTH_GENERATION_MARKER);
92
+ const release = await proper_lockfile_1.default.lock(configDir, {
93
+ lockfilePath: `${markerPath}.lock`,
94
+ realpath: false,
95
+ retries: { retries: 20, minTimeout: 25, maxTimeout: 250 },
96
+ });
97
+ try {
98
+ let recorded = '';
99
+ try {
100
+ recorded = node_fs_1.default.readFileSync(markerPath, 'utf8').trim();
101
+ }
102
+ catch (error) {
103
+ if (error.code !== 'ENOENT')
104
+ throw error;
105
+ }
106
+ if (recorded === machineUuid)
107
+ return false;
108
+ (options.clearStoredAuth ?? auth_storage_1.clearAllBackends)();
109
+ await (options.clearDeviceIdentity ??
110
+ (() => (0, device_identity_1.resetDeviceIdentity)({
111
+ profile: DEFAULT_PROFILE,
112
+ clearOnDisk: true,
113
+ })))();
114
+ await (options.clearCredentialEnvelope ??
115
+ (() => (0, cred_envelope_1.resetCredEnvelope)({
116
+ profile: DEFAULT_PROFILE,
117
+ clearOnDisk: true,
118
+ })))();
119
+ const temporary = `${markerPath}.tmp.${process.pid}.${Date.now()}`;
120
+ node_fs_1.default.writeFileSync(temporary, `${machineUuid}\n`, { mode: 0o600 });
121
+ node_fs_1.default.renameSync(temporary, markerPath);
122
+ return true;
123
+ }
124
+ finally {
125
+ await release();
126
+ }
127
+ }
67
128
  /**
68
129
  * Soft cap so a stuck KEK probe (corrupt keychain, hung native binding)
69
130
  * does not wedge commands on a fresh/plaintext install. Persisted encrypted
@@ -893,7 +893,11 @@ function formatDedicatedRuntimeApiError(err) {
893
893
  }
894
894
  try {
895
895
  const parsed = JSON.parse(err.body);
896
- const sentence = parsed.message || parsed.error || parsed.code;
896
+ // This formatter is reached only by `machines dedicated *`. Older API
897
+ // replicas and product-neutral action errors may still carry the Hosted
898
+ // Machine noun even though the caller named a Dedicated Runtime. Do not
899
+ // repeat that contradictory product name while a mixed deployment rolls.
900
+ const sentence = (parsed.message || parsed.error || parsed.code)?.replace(/(?:skrr )?Hosted Machine/g, 'Dedicated Runtime');
897
901
  return sentence ? `${sentence}${describeRefusalReasons(err)}` : null;
898
902
  }
899
903
  catch {