@skrr-ai/cli 0.1.8 → 0.1.9

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 (169) hide show
  1. package/dist/base-command.d.ts +27 -0
  2. package/dist/base-command.js +83 -11
  3. package/dist/commands/agent/message/send.js +1 -1
  4. package/dist/commands/agents/chat.d.ts +1 -1
  5. package/dist/commands/agents/chat.js +1 -1
  6. package/dist/commands/browser/install.d.ts +1 -1
  7. package/dist/commands/browser/install.js +10 -2
  8. package/dist/commands/browser/mcp-config.js +3 -1
  9. package/dist/commands/browser/skill/show.js +1 -1
  10. package/dist/commands/browser/status.js +4 -1
  11. package/dist/commands/browser/uninstall.d.ts +1 -1
  12. package/dist/commands/browser/uninstall.js +10 -2
  13. package/dist/commands/browser.d.ts +1 -1
  14. package/dist/commands/browser.js +3 -3
  15. package/dist/commands/code/index.js +6 -3
  16. package/dist/commands/code/install.d.ts +61 -0
  17. package/dist/commands/code/install.js +247 -0
  18. package/dist/commands/code/oversky-agent.d.ts +2 -1
  19. package/dist/commands/code/oversky-agent.js +12 -8
  20. package/dist/commands/commands.js +1 -1
  21. package/dist/commands/context.js +1 -1
  22. package/dist/commands/create-token.js +1 -1
  23. package/dist/commands/daemon/byok.d.ts +1 -0
  24. package/dist/commands/daemon/byok.js +14 -1
  25. package/dist/commands/daemon/index.js +18 -7
  26. package/dist/commands/daemon/install.d.ts +1 -0
  27. package/dist/commands/daemon/install.js +22 -3
  28. package/dist/commands/daemon/logs.d.ts +1 -0
  29. package/dist/commands/daemon/logs.js +4 -0
  30. package/dist/commands/daemon/start.d.ts +1 -0
  31. package/dist/commands/daemon/start.js +15 -2
  32. package/dist/commands/daemon/status.d.ts +1 -0
  33. package/dist/commands/daemon/status.js +14 -1
  34. package/dist/commands/daemon/stop.d.ts +1 -0
  35. package/dist/commands/daemon/stop.js +14 -1
  36. package/dist/commands/daemon/uninstall.d.ts +1 -0
  37. package/dist/commands/daemon/uninstall.js +15 -2
  38. package/dist/commands/daemon/usage.d.ts +1 -0
  39. package/dist/commands/daemon/usage.js +5 -1
  40. package/dist/commands/doctor.js +2 -2
  41. package/dist/commands/github/allow-tools.d.ts +1 -0
  42. package/dist/commands/github/app/install.d.ts +1 -0
  43. package/dist/commands/github/app/status.d.ts +1 -0
  44. package/dist/commands/github/connect.d.ts +1 -0
  45. package/dist/commands/github/connections.d.ts +1 -0
  46. package/dist/commands/github/disconnect.d.ts +1 -0
  47. package/dist/commands/github/finalize.d.ts +1 -0
  48. package/dist/commands/github/reconnect.d.ts +1 -0
  49. package/dist/commands/github/run.d.ts +1 -0
  50. package/dist/commands/github/tools.d.ts +1 -0
  51. package/dist/commands/goals/create.js +2 -2
  52. package/dist/commands/goals/show.js +5 -0
  53. package/dist/commands/harnesses/list.js +14 -3
  54. package/dist/commands/harnesses/show.js +14 -0
  55. package/dist/commands/instructions/activate.js +1 -1
  56. package/dist/commands/instructions/archive-version.js +1 -1
  57. package/dist/commands/instructions/delete.js +1 -1
  58. package/dist/commands/instructions/diff.js +1 -1
  59. package/dist/commands/instructions/install.js +7 -1
  60. package/dist/commands/instructions/restore.js +1 -1
  61. package/dist/commands/instructions/show.js +17 -1
  62. package/dist/commands/instructions/update.js +1 -1
  63. package/dist/commands/integrations/connections/connect.d.ts +1 -0
  64. package/dist/commands/integrations/connections/disconnect.d.ts +1 -0
  65. package/dist/commands/integrations/connections/finalize.d.ts +1 -0
  66. package/dist/commands/integrations/connections/list.d.ts +1 -0
  67. package/dist/commands/integrations/connections/reconnect.d.ts +1 -0
  68. package/dist/commands/integrations/execute.d.ts +1 -0
  69. package/dist/commands/integrations/search.d.ts +1 -0
  70. package/dist/commands/integrations/toolkits.d.ts +1 -0
  71. package/dist/commands/integrations/tools/allow.d.ts +1 -0
  72. package/dist/commands/integrations/tools/list.d.ts +1 -0
  73. package/dist/commands/list-daemons.d.ts +2 -7
  74. package/dist/commands/list-daemons.js +13 -13
  75. package/dist/commands/login.js +73 -31
  76. package/dist/commands/logout.js +52 -1
  77. package/dist/commands/machines/hosted/start.js +1 -1
  78. package/dist/commands/pair.js +2 -2
  79. package/dist/commands/projects/create.js +6 -1
  80. package/dist/commands/projects/tasks.js +6 -1
  81. package/dist/commands/revoke-daemon.js +6 -2
  82. package/dist/commands/set-capabilities.js +6 -2
  83. package/dist/commands/skills/install.js +1 -1
  84. package/dist/commands/skills/library.js +2 -2
  85. package/dist/commands/spaces/create.js +6 -2
  86. package/dist/commands/tasks/complete.d.ts +1 -1
  87. package/dist/commands/tasks/complete.js +21 -8
  88. package/dist/commands/tasks/create.js +4 -1
  89. package/dist/commands/tasks/current.js +1 -1
  90. package/dist/commands/tasks/delete.d.ts +1 -0
  91. package/dist/commands/tasks/delete.js +9 -0
  92. package/dist/commands/tasks/export.js +5 -1
  93. package/dist/commands/tasks/follow-up.js +2 -1
  94. package/dist/commands/tasks/list.js +5 -1
  95. package/dist/commands/tasks/pr.js +1 -1
  96. package/dist/commands/tasks/show.js +10 -1
  97. package/dist/commands/tasks/triage.js +2 -1
  98. package/dist/commands/tasks/update.js +4 -1
  99. package/dist/commands/tasks/upsert.js +4 -1
  100. package/dist/commands/usage.js +1 -1
  101. package/dist/lib/agentic-stream.js +21 -1
  102. package/dist/lib/api-fetch.d.ts +1 -1
  103. package/dist/lib/api-fetch.js +1 -1
  104. package/dist/lib/auth-core-init.js +23 -3
  105. package/dist/lib/auth-storage.d.ts +0 -12
  106. package/dist/lib/auth-storage.js +37 -2
  107. package/dist/lib/config.d.ts +3 -3
  108. package/dist/lib/config.js +327 -26
  109. package/dist/lib/created-line.d.ts +22 -0
  110. package/dist/lib/created-line.js +25 -0
  111. package/dist/lib/daemon-binding.d.ts +29 -0
  112. package/dist/lib/daemon-binding.js +63 -0
  113. package/dist/lib/daemon-ref.d.ts +31 -0
  114. package/dist/lib/daemon-ref.js +47 -0
  115. package/dist/lib/daemonBroker.js +1 -1
  116. package/dist/lib/delegated-cli.js +3 -3
  117. package/dist/lib/exec-oversky.d.ts +18 -1
  118. package/dist/lib/exec-oversky.js +21 -4
  119. package/dist/lib/html-text.d.ts +15 -0
  120. package/dist/lib/html-text.js +40 -0
  121. package/dist/lib/keychain.d.ts +43 -0
  122. package/dist/lib/keychain.js +145 -4
  123. package/dist/lib/login.js +3 -3
  124. package/dist/lib/node-adapter.js +1 -1
  125. package/dist/lib/oauthLogin.js +17 -6
  126. package/dist/lib/sky-code-agent.d.ts +1 -1
  127. package/dist/lib/sky-code-agent.js +8 -8
  128. package/dist/lib/sky-code-broker.js +18 -2
  129. package/dist/lib/sky-code-doctor.js +120 -4
  130. package/dist/lib/sky-code-managed.d.ts +12 -0
  131. package/dist/lib/sky-code-managed.js +21 -7
  132. package/dist/lib/sky-code.d.ts +16 -1
  133. package/dist/lib/sky-code.js +102 -4
  134. package/dist/lib/task-execution.js +4 -11
  135. package/dist/lib/tasks.d.ts +1 -0
  136. package/dist/lib/tasks.js +9 -6
  137. package/dist/lib/update-check.js +1 -1
  138. package/dist/lib/usage-discovery.js +1 -1
  139. package/dist/lib/web-url.js +12 -1
  140. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialResolver.d.ts +1 -1
  141. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialResolver.js +2 -2
  142. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
  143. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +13 -1
  144. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.js +16 -3
  145. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/legacyStatePreflight.d.ts +0 -9
  146. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/legacyStatePreflight.js +18 -1
  147. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loginLocalhost.js +3 -3
  148. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +3 -3
  149. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/runtime.d.ts +7 -3
  150. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/skyCodeChannels.d.ts +96 -0
  151. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/skyCodeChannels.js +115 -0
  152. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialResolver.d.ts +1 -1
  153. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialResolver.js +2 -2
  154. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
  155. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -0
  156. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.js +16 -3
  157. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/legacyStatePreflight.d.ts +0 -9
  158. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/legacyStatePreflight.js +18 -1
  159. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loginLocalhost.js +3 -3
  160. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +3 -3
  161. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/runtime.d.ts +7 -3
  162. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/skyCodeChannels.d.ts +96 -0
  163. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/skyCodeChannels.js +107 -0
  164. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  165. package/dist/node_modules/@skrr-ai/data-provider/index.js +4206 -4191
  166. package/dist/node_modules/@skrr-ai/inference-broker/dist/cjs/managed-inference-broker.js +2 -2
  167. package/dist/node_modules/@skrr-ai/inference-broker/dist/esm/managed-inference-broker.js +2 -2
  168. package/oclif.manifest.json +9924 -3352
  169. package/package.json +22 -19
@@ -4,6 +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 task_resolver_1 = require("../../lib/task-resolver");
7
+ const tasks_1 = require("../../lib/tasks");
7
8
  /**
8
9
  * A deliberately thin decision façade. Triage has no state machine of its own:
9
10
  * accepting and declining are ordinary TYPE-resolved moves, duplicate is the
@@ -29,7 +30,7 @@ class TasksTriage extends base_command_1.BaseCommand {
29
30
  static flags = {
30
31
  reason: core_1.Flags.string({ description: 'Reason returned to the task source when declining' }),
31
32
  priority: core_1.Flags.string({
32
- options: ['low', 'medium', 'high', 'critical'],
33
+ options: [...tasks_1.TASK_PRIORITIES],
33
34
  description: 'Priority to set before accepting',
34
35
  }),
35
36
  of: core_1.Flags.string({ description: 'Canonical task for the duplicate decision' }),
@@ -45,7 +45,10 @@ class TasksUpdate extends base_command_1.BaseCommand {
45
45
  description: 'Input format for --description or from-json description',
46
46
  options: ['html', 'markdown', 'plain'],
47
47
  }),
48
- priority: core_1.Flags.string({ description: 'New priority' }),
48
+ priority: core_1.Flags.string({
49
+ description: 'New priority',
50
+ options: [...tasks_1.TASK_PRIORITIES],
51
+ }),
49
52
  'start-date': core_1.Flags.string({ description: 'New planned start date (ISO string)' }),
50
53
  'due-date': core_1.Flags.string({ description: 'New due date (ISO string)' }),
51
54
  estimate: core_1.Flags.integer({ description: 'New estimate value from the Space estimate scale' }),
@@ -31,7 +31,10 @@ class TasksUpsert extends base_command_1.BaseCommand {
31
31
  description: 'Input format for --description or from-json description',
32
32
  options: ['html', 'markdown', 'plain'],
33
33
  }),
34
- priority: core_1.Flags.string({ description: 'Task priority' }),
34
+ priority: core_1.Flags.string({
35
+ description: 'Task priority',
36
+ options: [...tasks_1.TASK_PRIORITIES],
37
+ }),
35
38
  parent: core_1.Flags.string({ description: 'Parent task ID' }),
36
39
  space: core_1.Flags.string({ description: 'Space ID' }),
37
40
  goal: core_1.Flags.string({ description: 'Goal ID' }),
@@ -5,7 +5,7 @@ const base_command_1 = require("../base-command");
5
5
  const command_manifest_1 = require("../lib/command-manifest");
6
6
  const usage_discovery_1 = require("../lib/usage-discovery");
7
7
  class Usage extends base_command_1.BaseCommand {
8
- static description = 'Progressively discover Sky commands from the manifest, one token-budgeted domain at a time.';
8
+ static description = 'Progressively discover skrr commands from the manifest, one token-budgeted domain at a time.';
9
9
  static examples = [
10
10
  '<%= config.bin %> usage',
11
11
  '<%= config.bin %> usage tasks',
@@ -8,6 +8,8 @@ exports.toWebSocketOrigin = toWebSocketOrigin;
8
8
  const node_readline_1 = require("node:readline");
9
9
  const socket_io_client_1 = require("socket.io-client");
10
10
  const data_provider_1 = require("@skrr-ai/data-provider");
11
+ const config_1 = require("./config");
12
+ const publicEndpoints_generated_1 = require("./publicEndpoints.generated");
11
13
  /**
12
14
  * Small Node-side adapter for the existing Agentic Socket.IO contract.
13
15
  * It deliberately normalizes only lifecycle/text/tool events; the server's
@@ -458,9 +460,27 @@ class AgenticStreamClient {
458
460
  }
459
461
  }
460
462
  exports.AgenticStreamClient = AgenticStreamClient;
463
+ /**
464
+ * Web apexes whose realtime traffic lives on their `api.` subdomain.
465
+ *
466
+ * Derived from the endpoint contract rather than written out, because the two
467
+ * hostnames used to be literals here and the domain cutover moved one of them:
468
+ * a CLI logged into the new prod apex fell through to `wss://<apex>`, which
469
+ * CloudFront cannot upgrade. Nothing errors at the call site — the socket just
470
+ * never connects, which reads as a network problem rather than a stale list.
471
+ *
472
+ * The retired prod apex stays, for the reverse reason: a config file written
473
+ * before the cutover still names it, and dropping it would strand exactly the
474
+ * sessions the entry exists to carry.
475
+ */
476
+ const REALTIME_APEXES = new Set([
477
+ new URL(publicEndpoints_generated_1.OVERSKY_ENVIRONMENT_ENDPOINTS.prod.appUrl).hostname,
478
+ new URL(publicEndpoints_generated_1.OVERSKY_ENVIRONMENT_ENDPOINTS.dev.appUrl).hostname,
479
+ new URL(config_1.LEGACY_PROD_BASE_URL).hostname,
480
+ ]);
461
481
  function toWebSocketOrigin(baseURL) {
462
482
  const url = new URL(baseURL);
463
- if (url.hostname === 'oversky.dev' || url.hostname === 'oversky.ai') {
483
+ if (REALTIME_APEXES.has(url.hostname)) {
464
484
  url.hostname = `api.${url.hostname}`;
465
485
  }
466
486
  url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
@@ -32,7 +32,7 @@ export declare class ApiFetchError extends Error {
32
32
  constructor(message: string, status: number, body?: string, code?: string);
33
33
  }
34
34
  /**
35
- * Call an skrr API endpoint with the current credential. Throws
35
+ * Call a skrr API endpoint with the current credential. Throws
36
36
  * `ApiFetchError` on any non-2xx status so callers can branch on
37
37
  * `err.status` / `err.code`.
38
38
  */
@@ -56,7 +56,7 @@ class ApiFetchError extends Error {
56
56
  }
57
57
  exports.ApiFetchError = ApiFetchError;
58
58
  /**
59
- * Call an skrr API endpoint with the current credential. Throws
59
+ * Call a skrr API endpoint with the current credential. Throws
60
60
  * `ApiFetchError` on any non-2xx status so callers can branch on
61
61
  * `err.status` / `err.code`.
62
62
  */
@@ -85,6 +85,26 @@ function ensureAuthCoreConfigured() {
85
85
  // Phase D: share the daemon's config root so auth.lock and the
86
86
  // needs-reauth flag are coordinated across binaries.
87
87
  configDir: () => node_path_1.default.join(node_os_1.default.homedir(), '.skrr'),
88
+ /**
89
+ * The machine root, PINNED — never the active profile's directory.
90
+ *
91
+ * `machineConfigDir` defaults to `configDir()`, and while the CLI's
92
+ * `configDir` was unconditionally `~/.skrr` that default was correct and
93
+ * this line would have been noise. It stops being correct the moment
94
+ * `configDir` becomes profile-aware: machine identity would start drifting
95
+ * per profile, and the server binds `cliId` into the JWT `did` claim and
96
+ * REFUSES a rotation presented against a different one. A second profile
97
+ * would not degrade — it would fail to stay logged in.
98
+ *
99
+ * The daemon already learned this the expensive way. OSK-1478: pinning the
100
+ * machine UUID to a profile-scoped `configDir` silently killed the
101
+ * `legacyDaemonUuids()` fold safety net, because it scanned
102
+ * `<profileDir>/profiles/*`, which never exists.
103
+ *
104
+ * Wired ahead of the profile work rather than with it, so the landmine is
105
+ * gone before anything can step on it.
106
+ */
107
+ machineConfigDir: () => node_path_1.default.join(node_os_1.default.homedir(), '.skrr'),
88
108
  logger: cliLogger,
89
109
  killSwitches: {
90
110
  // CLI shares the same kill-switch surface as the daemon for ops parity.
@@ -139,13 +159,13 @@ async function settlesWithin(promise, timeoutMs, keepTimerRef = false) {
139
159
  function encryptedCredentialInitError(timedOut, cause) {
140
160
  const state = (0, cred_envelope_1.describeCredEnvelopeState)();
141
161
  if (timedOut) {
142
- return new CliCredentialInitializationError('Stored Sky credentials are encrypted, but the local credential store did not ' +
162
+ return new CliCredentialInitializationError('Stored skrr credentials are encrypted, but the local credential store did not ' +
143
163
  `initialize within ${ENCRYPTED_CREDENTIAL_INIT_TIMEOUT_MS / 1000} seconds. ` +
144
- 'The credential remains protected; wait for other Sky processes to finish and retry.');
164
+ 'The credential remains protected; wait for other skrr processes to finish and retry.');
145
165
  }
146
166
  const detail = state.kind === 'disabled' && state.reason ? `${state.kind} (${state.reason})` : state.kind;
147
167
  const causeMessage = cause instanceof Error ? cause.message.trim().slice(0, 200) : '';
148
- return new CliCredentialInitializationError(`Stored Sky credentials are encrypted, but the local credential store is ${detail}. ` +
168
+ return new CliCredentialInitializationError(`Stored skrr credentials are encrypted, but the local credential store is ${detail}. ` +
149
169
  (causeMessage ? `Initialization failed: ${causeMessage}. ` : '') +
150
170
  'The credential remains protected; unlock or repair the OS credential store and retry.');
151
171
  }
@@ -33,18 +33,6 @@ export interface WriteOptions {
33
33
  * function returns whatever is persisted.
34
34
  */
35
35
  export declare function readFromBackend(expectedServerOrigin?: string): ReadResult;
36
- /**
37
- * Persist a token bundle to the preferred backend.
38
- *
39
- * Writes to keychain when available, file otherwise. If keychain writes
40
- * fail mid-flight (e.g., user canceled the unlock prompt), we fall back
41
- * to the file backend so the CLI remains functional.
42
- *
43
- * Returns the backend that actually accepted the write. Callers wanting
44
- * a specific backend (bare mode, store-mode overrides) should call the
45
- * lower-level write helpers directly — not exported here to keep the
46
- * default path honest.
47
- */
48
36
  export declare function writeToBackend(bundle: AuthBundle, options?: WriteOptions): AuthBackend;
49
37
  /**
50
38
  * Clear both backends. Called on `skrr logout` (and internally during
@@ -357,6 +357,37 @@ function readFromBackend(expectedServerOrigin) {
357
357
  * lower-level write helpers directly — not exported here to keep the
358
358
  * default path honest.
359
359
  */
360
+ /**
361
+ * Clear the DAEMON's needs-reauth latch as well as the CLI's own.
362
+ *
363
+ * `clearReauthState()` resolves `getAuthConfigDir()`, and the two binaries wire
364
+ * that differently: the CLI points auth-core at the machine root (`~/.skrr`),
365
+ * the daemon at its PROFILE dir (`~/.skrr/profiles/<profile>`). So the daemon
366
+ * latched at one path while `skrr login` cleared another — a file that on most
367
+ * machines does not exist at all, which made the clear a silent no-op.
368
+ *
369
+ * The visible cost was a loop with no exit: `skrr daemon status` reported
370
+ * "Cause: TOKEN_INVALID · Remedy: run `skrr login`", the login succeeded, and
371
+ * the flag never moved, so the daemon's realtime channel stayed down and it
372
+ * could not execute tasks. `auth-core-init.ts` states the intent — "share the
373
+ * daemon's config root so auth.lock and the needs-reauth flag are coordinated
374
+ * across binaries" — which stopped being true when the daemon became
375
+ * profile-scoped.
376
+ *
377
+ * The ACTIVE profile only, deliberately: a per-profile latch is correct because
378
+ * different profiles can hold different credentials, and a login re-authorises
379
+ * exactly the one it just wrote.
380
+ */
381
+ function clearDaemonProfileReauthState() {
382
+ try {
383
+ const target = path.join(os.homedir(), '.skrr', 'profiles', keychain.getActiveProfile(), 'needs-reauth.json');
384
+ fs.rmSync(target, { force: true });
385
+ }
386
+ catch {
387
+ // Best effort: the CLI's own latch is already cleared, and failing to
388
+ // remove the daemon's must not fail a login that otherwise succeeded.
389
+ }
390
+ }
360
391
  function writeToBackend(bundle, options = {}) {
361
392
  const requestedOrigin = options.serverOrigin ?? bundle.serverOrigin;
362
393
  let persistedBundle = bundle;
@@ -373,8 +404,10 @@ function writeToBackend(bundle, options = {}) {
373
404
  // Remove any stale file copy so we don't have two sources of truth.
374
405
  deleteFileBackend();
375
406
  backend = 'keychain';
376
- if (options.clearReauth)
407
+ if (options.clearReauth) {
377
408
  (0, auth_core_1.clearReauthState)();
409
+ clearDaemonProfileReauthState();
410
+ }
378
411
  return backend;
379
412
  }
380
413
  // Keychain failed — fall through to the file backend rather than
@@ -386,8 +419,10 @@ function writeToBackend(bundle, options = {}) {
386
419
  }
387
420
  writeToFile(persistedBundle);
388
421
  backend = 'file';
389
- if (options.clearReauth)
422
+ if (options.clearReauth) {
390
423
  (0, auth_core_1.clearReauthState)();
424
+ clearDaemonProfileReauthState();
425
+ }
391
426
  return backend;
392
427
  }
393
428
  /**
@@ -128,9 +128,9 @@ export declare function canonicalizeKnownBaseURL(value: string): string;
128
128
  */
129
129
  export declare function isEnvAuthOverride(): boolean;
130
130
  /**
131
- * Read the config file. Returns defaults if missing. Throws on corrupt
132
- * JSON. Performs a one-shot legacy → new copy when only the legacy file
133
- * exists, so existing users don't have to re-login.
131
+ * Read the config file. Returns defaults if missing. Throws on corrupt JSON.
132
+ * Performs a one-shot legacy → new copy when only the legacy file exists, so
133
+ * existing users don't have to re-login.
134
134
  */
135
135
  export declare function loadConfig(): CliConfig;
136
136
  /** Write the config file with mode 0600 inside a 0700 directory. */
@@ -48,6 +48,7 @@ exports.saveRawConfig = saveRawConfig;
48
48
  const fs = __importStar(require("node:fs"));
49
49
  const os = __importStar(require("node:os"));
50
50
  const path = __importStar(require("node:path"));
51
+ const keychain_1 = require("./keychain");
51
52
  const publicEndpoints_generated_1 = require("./publicEndpoints.generated");
52
53
  // Both from config/public-endpoints.json, the one domain contract, rather than a
53
54
  // fourth hand-copied pair. The CLI previously knew only about dev — there was no
@@ -138,18 +139,46 @@ const ENV_VAR = {
138
139
  refreshToken: 'OVERSKY_REFRESH_TOKEN',
139
140
  cliId: 'OVERSKY_CLI_ID',
140
141
  };
142
+ /**
143
+ * Read one overlay variable under either prefix.
144
+ *
145
+ * The ~921 `OVERSKY_*` variables are RETAINED by decision
146
+ * (docs/product/skrr-env-inventory.md), so this migrates nothing and changes no
147
+ * existing environment: the legacy spelling still wins when both are set. What
148
+ * it stops is the renamed product SILENTLY IGNORING the prefix its own
149
+ * contributor docs tell new code to use. `SKRR_BASE_URL=https://oversky.dev`
150
+ * previously did nothing at all — the command ran happily against production
151
+ * and reported success, which is the worst available outcome for a variable
152
+ * whose entire job is choosing the deployment.
153
+ *
154
+ * A disagreement is reported rather than resolved quietly, because two
155
+ * different values for one setting is a mistake either way.
156
+ */
157
+ let warnedEnvPrefixConflict = false;
158
+ function envOverlayValue(key) {
159
+ const legacyName = ENV_VAR[key];
160
+ const renamedName = legacyName.replace(/^OVERSKY_/, 'SKRR_');
161
+ const legacy = process.env[legacyName]?.trim();
162
+ const renamed = process.env[renamedName]?.trim();
163
+ if (legacy && renamed && legacy !== renamed && !warnedEnvPrefixConflict) {
164
+ warnedEnvPrefixConflict = true;
165
+ process.stderr.write(`warning: ${legacyName} and ${renamedName} are both set and disagree. ` +
166
+ `Using ${legacyName}=${legacy}.\n`);
167
+ }
168
+ return legacy && legacy.length > 0 ? legacy : renamed && renamed.length > 0 ? renamed : undefined;
169
+ }
141
170
  function overlayEnv(cfg) {
142
171
  const out = { ...cfg };
143
- const bu = process.env[ENV_VAR.baseURL] || process.env[ENV_VAR.serverUrl];
172
+ const bu = envOverlayValue('baseURL') || envOverlayValue('serverUrl');
144
173
  if (bu && bu.length > 0)
145
174
  out.baseURL = canonicalizeKnownBaseURL(bu);
146
- const tok = process.env[ENV_VAR.token];
175
+ const tok = envOverlayValue('token');
147
176
  if (tok && tok.length > 0)
148
177
  out.token = tok;
149
- const rt = process.env[ENV_VAR.refreshToken];
178
+ const rt = envOverlayValue('refreshToken');
150
179
  if (rt && rt.length > 0)
151
180
  out.refreshToken = rt;
152
- const cid = process.env[ENV_VAR.cliId];
181
+ const cid = envOverlayValue('cliId');
153
182
  if (cid && cid.length > 0)
154
183
  out.cliId = cid;
155
184
  return out;
@@ -161,8 +190,9 @@ function overlayEnv(cfg) {
161
190
  * us persist a rotated pair that the next `loadConfig()` would shadow.
162
191
  */
163
192
  function isEnvAuthOverride() {
164
- return !!((process.env[ENV_VAR.token] && process.env[ENV_VAR.token].length > 0) ||
165
- (process.env[ENV_VAR.refreshToken] && process.env[ENV_VAR.refreshToken].length > 0));
193
+ // Both prefixes, or a credential injected as SKRR_TOKEN would be used for the
194
+ // request while the process still believed it could refresh and persist.
195
+ return !!(envOverlayValue('token') || envOverlayValue('refreshToken'));
166
196
  }
167
197
  /**
168
198
  * Storage layout (Phase H+).
@@ -178,9 +208,100 @@ function isEnvAuthOverride() {
178
208
  function newConfigDir() {
179
209
  return path.join(os.homedir(), '.skrr');
180
210
  }
211
+ /**
212
+ * Fields that belong to the MACHINE, not to a profile.
213
+ *
214
+ * `cliId` is the sharp one. Its own doc says it is presented at login and at
215
+ * every refresh rotation, and that the server binds it into the JWT `did` claim
216
+ * and rejects a rotation against a different one. Split it per profile and a
217
+ * second profile does not degrade — it fails to stay logged in.
218
+ *
219
+ * `credentials.store` is a machine posture (keychain / file / env-only), and
220
+ * `migrated` is a breadcrumb about this machine's DISK LAYOUT. Neither is a
221
+ * property of which deployment you are addressing.
222
+ *
223
+ * Everything else follows the profile, because everything else IS the
224
+ * deployment: `baseURL`, who you are there (`userId`, `email`), and what you
225
+ * chose there (`workspaceId`, `skyCodeAgentId`).
226
+ */
227
+ const MACHINE_SCOPED_KEYS = ['cliId', 'credentials', 'migrated'];
228
+ /**
229
+ * Where this profile's config lives.
230
+ *
231
+ * The DEFAULT profile keeps `~/.skrr/cli-config.json` — the exact path it has
232
+ * always used. That is the whole reason this change needs no migration and
233
+ * carries almost no risk: profiles are ADDITIVE. An existing install is the
234
+ * default profile, its file does not move, and nothing about it is rewritten.
235
+ *
236
+ * A named profile gets `~/.skrr/profiles/<name>/cli-config.json`, sharing the
237
+ * tree the daemon already uses for the same idea. Sharing rather than inventing
238
+ * `cli-profiles/` is deliberate: today's worst failure is the CLI and the daemon
239
+ * pointing at different deployments (OSK-279), and one profile namespace makes
240
+ * that harder to produce, not easier.
241
+ */
242
+ function profileConfigDir() {
243
+ const profile = (0, keychain_1.getActiveProfile)();
244
+ if (!profile || profile === keychain_1.DEFAULT_PROFILE)
245
+ return newConfigDir();
246
+ return path.join(newConfigDir(), 'profiles', profile);
247
+ }
181
248
  function newConfigFile() {
249
+ return path.join(profileConfigDir(), 'cli-config.json');
250
+ }
251
+ /** The machine-scoped file, which never moves with the profile. */
252
+ function machineConfigFile() {
182
253
  return path.join(newConfigDir(), 'cli-config.json');
183
254
  }
255
+ /** Split a config into the half that follows the profile and the half that does not. */
256
+ function splitByScope(config) {
257
+ const machine = {};
258
+ const profile = {};
259
+ for (const [key, value] of Object.entries(config)) {
260
+ if (MACHINE_SCOPED_KEYS.includes(key))
261
+ machine[key] = value;
262
+ else
263
+ profile[key] = value;
264
+ }
265
+ return { machine, profile };
266
+ }
267
+ /**
268
+ * Machine-scoped fields, read from the machine file regardless of profile.
269
+ *
270
+ * Returns an empty object rather than throwing: a named profile on a machine
271
+ * that has never run the default one has no machine file yet, and that is a
272
+ * first run, not a fault.
273
+ */
274
+ function readMachineScoped() {
275
+ const p = machineConfigFile();
276
+ if (!fs.existsSync(p))
277
+ return {};
278
+ try {
279
+ return splitByScope(readJsonOrThrow(p)).machine;
280
+ }
281
+ catch {
282
+ return {};
283
+ }
284
+ }
285
+ /**
286
+ * The PRE-RENAME config root — the generation between `.sky` and `.skrr`.
287
+ *
288
+ * This was missing from the fallback chain, which went straight from `.skrr` to
289
+ * `.sky` and skipped the directory that every current install actually has.
290
+ * The effect is not a missing preference: with no config found, `loadConfig`
291
+ * returns DEFAULT_CONFIG, whose baseURL is the production apex — so a CLI that
292
+ * had been pointed at the dev deployment silently starts addressing PRODUCTION,
293
+ * and its `workspaceId` is gone. Nothing errors; commands just answer about a
294
+ * different backend.
295
+ *
296
+ * Note the filename differs by generation: `.sky` used `config.json`, `.oversky`
297
+ * and `.skrr` use `cli-config.json`.
298
+ */
299
+ function priorConfigDir() {
300
+ return path.join(os.homedir(), '.oversky');
301
+ }
302
+ function priorConfigFile() {
303
+ return path.join(priorConfigDir(), 'cli-config.json');
304
+ }
184
305
  function legacyConfigDir() {
185
306
  return path.join(os.homedir(), '.sky');
186
307
  }
@@ -188,41 +309,221 @@ function legacyConfigFile() {
188
309
  return path.join(legacyConfigDir(), 'config.json');
189
310
  }
190
311
  /**
191
- * Read the config file. Returns defaults if missing. Throws on corrupt
192
- * JSON. Performs a one-shot legacy → new copy when only the legacy file
193
- * exists, so existing users don't have to re-login.
312
+ * Breadcrumb proving the carry-forward already ran on this machine.
313
+ *
314
+ * Deliberately OUTSIDE the file it produces. The migration's own record used to
315
+ * be the config it wrote, which made it one-shot only for as long as that file
316
+ * survived: any moment where `cli-config.json` was absent looked exactly like a
317
+ * first run, and the pre-rename config was adopted again — `baseURL` included.
318
+ *
319
+ * Follows the `.migrated-*` convention already used by the daemon's
320
+ * profile-migration and keychain-hygiene passes.
321
+ */
322
+ const CARRY_FORWARD_BREADCRUMB = '.migrated-2026-09-02-cli-config-carry-forward';
323
+ function carryForwardBreadcrumbPath() {
324
+ return path.join(newConfigDir(), CARRY_FORWARD_BREADCRUMB);
325
+ }
326
+ function hasCarriedForward() {
327
+ try {
328
+ return fs.existsSync(carryForwardBreadcrumbPath());
329
+ }
330
+ catch {
331
+ return false;
332
+ }
333
+ }
334
+ function markCarriedForward() {
335
+ try {
336
+ const dir = newConfigDir();
337
+ if (!fs.existsSync(dir))
338
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
339
+ fs.writeFileSync(carryForwardBreadcrumbPath(), `${new Date().toISOString()}\n`, {
340
+ mode: 0o600,
341
+ });
342
+ }
343
+ catch {
344
+ // Best effort. Failing to record it means the migration may run again,
345
+ // which is the old behaviour — not worse.
346
+ }
347
+ }
348
+ let announcedAdoptedDeployment = false;
349
+ /**
350
+ * Say when the CLI adopts a deployment the user did not name in this command.
351
+ *
352
+ * Whatever the source, changing which DEPLOYMENT a session addresses is not a
353
+ * detail: the failure it produces is an empty list, not an error, because a
354
+ * successful `200 {"data": []}` from the wrong deployment is indistinguishable
355
+ * from having no rows.
356
+ */
357
+ function announceAdoptedDeployment(baseURL, source) {
358
+ if (announcedAdoptedDeployment)
359
+ return;
360
+ announcedAdoptedDeployment = true;
361
+ process.stderr.write(`note: no CLI config found; using ${baseURL} (from ${source}).\n` +
362
+ ` If that is the wrong deployment: skrr login --base-url <url>\n`);
363
+ }
364
+ /**
365
+ * Read the config file. Returns defaults if missing. Throws on corrupt JSON.
366
+ * Performs a one-shot legacy → new copy when only the legacy file exists, so
367
+ * existing users don't have to re-login.
194
368
  */
195
369
  function loadConfig() {
196
370
  const newPath = newConfigFile();
371
+ const named = newPath !== machineConfigFile();
197
372
  if (fs.existsSync(newPath)) {
198
- return overlayEnv(migrateLegacyBaseURL(readJsonOrThrow(newPath)));
373
+ const current = migrateLegacyBaseURL(readJsonOrThrow(newPath));
374
+ warnIfStrandedPriorConfig(current);
375
+ // A named profile's file holds only its own half; the machine half is read
376
+ // from the machine file so `cliId` is one value for this machine no matter
377
+ // how many profiles exist.
378
+ return overlayEnv(named ? { ...current, ...readMachineScoped() } : current);
199
379
  }
200
- const legacyPath = legacyConfigFile();
201
- if (fs.existsSync(legacyPath)) {
202
- const legacy = migrateLegacyBaseURL(readJsonOrThrow(legacyPath));
203
- try {
204
- saveConfig(legacy);
205
- }
206
- catch {
207
- /* swallow caller still has a usable config in memory */
380
+ // A named profile that has never been used is a FIRST RUN for that profile,
381
+ // not a machine without config. It must not adopt the default profile's
382
+ // deployment that is how "switch to dev" would silently keep talking to
383
+ // prod — so it starts from the documented default, carrying only the machine
384
+ // half forward. The generation carry-forward below is deliberately skipped:
385
+ // there is no `.sky`/`.oversky` ancestor of a profile that did not exist then.
386
+ if (named) {
387
+ return overlayEnv({ ...DEFAULT_CONFIG, ...readMachineScoped() });
388
+ }
389
+ // Newest first, and ONCE per machine. Each generation is copied forward so a
390
+ // user never has to re-login because a directory was renamed — but only on a
391
+ // genuine first run. After the breadcrumb exists, a missing config means the
392
+ // file went away, not that this machine is pre-rename, and re-adopting the
393
+ // old file would silently move the session to the old DEPLOYMENT.
394
+ if (!hasCarriedForward()) {
395
+ for (const carriedPath of [priorConfigFile(), legacyConfigFile()]) {
396
+ if (!fs.existsSync(carriedPath))
397
+ continue;
398
+ const carried = migrateLegacyBaseURL(readJsonOrThrow(carriedPath));
399
+ try {
400
+ saveConfig(carried);
401
+ }
402
+ catch {
403
+ /* swallow — caller still has a usable config in memory */
404
+ }
405
+ markCarriedForward();
406
+ announceAdoptedDeployment(carried.baseURL, carriedPath);
407
+ return overlayEnv(carried);
208
408
  }
209
- return overlayEnv(legacy);
210
409
  }
410
+ // Nothing to carry: fall through to the documented default. Deliberately NOT
411
+ // derived from the stored credential's origin — making config resolution read
412
+ // the keychain would let ambient machine state override a documented default,
413
+ // and a fresh install defaulting to dev is intended behaviour, not a bug.
211
414
  return overlayEnv({ ...DEFAULT_CONFIG });
212
415
  }
416
+ /**
417
+ * Surface the one case this fix cannot repair on its own.
418
+ *
419
+ * A CLI that ran after the directory rename but before this migration existed
420
+ * already wrote a `.skrr` config from DEFAULT_CONFIG. The copy-forward above
421
+ * will not fire for it, and it must not: once a config exists, "the user chose
422
+ * this backend" and "the default landed here" are indistinguishable, and
423
+ * silently rewriting a deliberate choice is worse than the bug.
424
+ *
425
+ * So it says so instead of guessing. Warned once per process, to stderr, and
426
+ * only when the two disagree — agreement needs no warning.
427
+ */
428
+ let warnedStrandedPriorConfig = false;
429
+ function warnIfStrandedPriorConfig(current) {
430
+ if (warnedStrandedPriorConfig)
431
+ return;
432
+ // On EVERY command, forever, was the complaint — and it is fair: once you have
433
+ // read it and meant the deployment you are on, it is pure noise burying real
434
+ // stderr. Deleting the old file is one exit; this is the other, for a machine
435
+ // that wants to keep it.
436
+ if ((process.env[QUIET_ENV] ?? '').trim().length > 0)
437
+ return;
438
+ const priorPath = priorConfigFile();
439
+ if (!fs.existsSync(priorPath))
440
+ return;
441
+ let prior;
442
+ try {
443
+ prior = readJsonOrThrow(priorPath);
444
+ }
445
+ catch {
446
+ return; // an unreadable prior config is not evidence of anything
447
+ }
448
+ const priorBase = canonicalizeKnownBaseURL(prior.baseURL);
449
+ if (!priorBase || priorBase === canonicalizeKnownBaseURL(current.baseURL))
450
+ return;
451
+ warnedStrandedPriorConfig = true;
452
+ const stranded = strandedSettings(prior);
453
+ const alsoHolds = stranded.length > 0 ? ` (it also holds ${stranded.join(', ')})` : '';
454
+ process.stderr.write(`warning: ${priorPath} points at ${priorBase}; this CLI is using ${current.baseURL}.\n` +
455
+ ` Use it instead: skrr login --base-url ${priorBase}\n` +
456
+ ` Or silence this: set ${QUIET_ENV}=1, or delete that file${alsoHolds}\n`);
457
+ }
458
+ /** Opt out once you have read the warning and meant the deployment you are on. */
459
+ const QUIET_ENV = 'SKRR_QUIET_CONFIG_WARNING';
460
+ /**
461
+ * The settings that deleting the prior config would discard.
462
+ *
463
+ * The migration did not carry these forward, so the old file is their only
464
+ * copy — naming them is the difference between an instruction and a trap.
465
+ */
466
+ function strandedSettings(prior) {
467
+ const carried = new Set(['baseURL', 'migrated', 'cliId']);
468
+ return Object.keys(prior)
469
+ .filter((k) => !carried.has(k) && prior[k] !== undefined)
470
+ .sort();
471
+ }
472
+ function newPath() {
473
+ return newConfigFile();
474
+ }
213
475
  /** Write the config file with mode 0600 inside a 0700 directory. */
214
476
  function saveConfig(config) {
215
- const dir = newConfigDir();
216
- if (!fs.existsSync(dir)) {
217
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
218
- }
219
- const p = newConfigFile();
220
477
  const canonical = {
221
478
  ...config,
222
479
  baseURL: canonicalizeKnownBaseURL(config.baseURL),
223
480
  };
224
- fs.writeFileSync(p, JSON.stringify(canonical, null, 2), { mode: 0o600 });
225
- fs.chmodSync(p, 0o600);
481
+ const target = newConfigFile();
482
+ const machineFile = machineConfigFile();
483
+ // Default profile: one file, exactly as before. Nothing is split, nothing
484
+ // moves, and an existing install cannot notice this change.
485
+ if (target === machineFile) {
486
+ writeConfigFile(target, canonical);
487
+ return;
488
+ }
489
+ const { machine, profile } = splitByScope(canonical);
490
+ writeConfigFile(target, profile);
491
+ // Machine-scoped fields are MERGED into the machine file rather than
492
+ // replacing it: the default profile's deployment, workspace and account live
493
+ // in that same file, and a named profile writing its own half over the top
494
+ // would sign the default profile out.
495
+ const existingMachine = fs.existsSync(machineFile) ? readJsonOrThrow(machineFile) : {};
496
+ const merged = { ...existingMachine, ...machine };
497
+ if (JSON.stringify(merged) !== JSON.stringify(existingMachine)) {
498
+ writeConfigFile(machineFile, merged);
499
+ }
500
+ }
501
+ /**
502
+ * Warn once when a write happens before the profile was chosen.
503
+ *
504
+ * Such a write lands in the default profile's file whatever `--profile` said,
505
+ * because the choice had not been made yet. It is silent, it is permanent, and
506
+ * the only way anyone finds it is by wondering why a named profile keeps
507
+ * forgetting things. The daemon carries the same guard for the same reason.
508
+ */
509
+ let warnedUnresolvedProfile = false;
510
+ function warnIfProfileUnresolved(target) {
511
+ if ((0, keychain_1.isProfileResolved)() || warnedUnresolvedProfile)
512
+ return;
513
+ warnedUnresolvedProfile = true;
514
+ process.stderr.write(`[profile] config written to ${target} before the profile was resolved; ` +
515
+ 'it landed in the default profile. If this was not the crash path, it is a ' +
516
+ 'regression — a writer is running before BaseCommand.init().\n');
517
+ }
518
+ /** Write one config file with the directory created and 0600 enforced. */
519
+ function writeConfigFile(target, contents) {
520
+ warnIfProfileUnresolved(target);
521
+ const dir = path.dirname(target);
522
+ if (!fs.existsSync(dir)) {
523
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
524
+ }
525
+ fs.writeFileSync(target, JSON.stringify(contents, null, 2), { mode: 0o600 });
526
+ fs.chmodSync(target, 0o600);
226
527
  }
227
528
  /**
228
529
  * One-shot migration for clients installed before oversky.dev became the dev