@skrr-ai/cli 0.1.42 → 0.1.44

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 (120) hide show
  1. package/dist/base-command.d.ts +2 -0
  2. package/dist/base-command.js +1 -0
  3. package/dist/commands/balance/index.js +1 -1
  4. package/dist/commands/balance/overage.js +4 -3
  5. package/dist/commands/balance/plan.js +10 -6
  6. package/dist/commands/balance/show.d.ts +32 -0
  7. package/dist/commands/balance/show.js +74 -3
  8. package/dist/commands/balance/usage/events.js +3 -2
  9. package/dist/commands/balance/usage.js +2 -1
  10. package/dist/commands/browser/install.js +3 -2
  11. package/dist/commands/browser/uninstall.js +3 -2
  12. package/dist/commands/code/handover.d.ts +1 -0
  13. package/dist/commands/code/handover.js +4 -0
  14. package/dist/commands/code/install.js +3 -2
  15. package/dist/commands/code/jobs/run.d.ts +1 -0
  16. package/dist/commands/code/jobs/run.js +4 -0
  17. package/dist/commands/daemon/install.js +7 -0
  18. package/dist/commands/harnesses/install.d.ts +11 -0
  19. package/dist/commands/harnesses/install.js +33 -14
  20. package/dist/commands/harnesses/installers.d.ts +10 -0
  21. package/dist/commands/harnesses/installers.js +31 -0
  22. package/dist/commands/harnesses/leases/show.js +14 -0
  23. package/dist/commands/harnesses/list.js +3 -2
  24. package/dist/commands/instructions/install.d.ts +22 -0
  25. package/dist/commands/instructions/install.js +83 -7
  26. package/dist/commands/instructions/list.js +5 -0
  27. package/dist/commands/instructions/show.d.ts +6 -0
  28. package/dist/commands/instructions/show.js +34 -1
  29. package/dist/commands/instructions/status.js +17 -1
  30. package/dist/commands/payments/wallet.js +2 -2
  31. package/dist/commands/tasks/attachments/set-role.d.ts +24 -0
  32. package/dist/commands/tasks/attachments/set-role.js +60 -0
  33. package/dist/commands/tasks/attachments/upload.d.ts +1 -0
  34. package/dist/commands/tasks/attachments/upload.js +18 -52
  35. package/dist/commands/tasks/comments/add.d.ts +1 -0
  36. package/dist/commands/tasks/comments/add.js +67 -2
  37. package/dist/commands/tasks/complete.d.ts +1 -0
  38. package/dist/commands/tasks/complete.js +56 -1
  39. package/dist/commands/tasks/create.d.ts +6 -0
  40. package/dist/commands/tasks/create.js +26 -3
  41. package/dist/commands/tasks/labels/attach.js +4 -0
  42. package/dist/commands/tasks/list.d.ts +1 -0
  43. package/dist/commands/tasks/list.js +9 -0
  44. package/dist/commands/tasks/result/submit.d.ts +3 -0
  45. package/dist/commands/tasks/result/submit.js +76 -8
  46. package/dist/commands/tasks/show.d.ts +23 -0
  47. package/dist/commands/tasks/show.js +61 -1
  48. package/dist/commands/tasks/update.js +16 -2
  49. package/dist/commands/tasks/updates/add.d.ts +1 -0
  50. package/dist/commands/tasks/updates/add.js +63 -0
  51. package/dist/commands/views/create.js +12 -2
  52. package/dist/commands/views/list.js +3 -2
  53. package/dist/commands/views/show.js +2 -0
  54. package/dist/lib/agentic-stream.d.ts +10 -4
  55. package/dist/lib/agentic-stream.js +25 -11
  56. package/dist/lib/api-fetch.js +7 -2
  57. package/dist/lib/cli-installers.d.ts +23 -4
  58. package/dist/lib/cli-installers.js +56 -8
  59. package/dist/lib/daemon-setup.d.ts +18 -1
  60. package/dist/lib/daemon-setup.js +33 -1
  61. package/dist/lib/dedicated-machines.js +4 -2
  62. package/dist/lib/file-mime.js +1 -1
  63. package/dist/lib/first-party-harness-agent.d.ts +16 -1
  64. package/dist/lib/first-party-harness-agent.js +47 -17
  65. package/dist/lib/first-party-harness-doctor.js +49 -48
  66. package/dist/lib/first-party-harness-managed.d.ts +9 -4
  67. package/dist/lib/first-party-harness-managed.js +13 -10
  68. package/dist/lib/first-party-harness.d.ts +32 -21
  69. package/dist/lib/first-party-harness.js +49 -31
  70. package/dist/lib/harness-provider-input.d.ts +18 -12
  71. package/dist/lib/harness-provider-input.js +18 -12
  72. package/dist/lib/harness-tiers.d.ts +4 -3
  73. package/dist/lib/harness-tiers.js +4 -3
  74. package/dist/lib/harnesses.d.ts +6 -0
  75. package/dist/lib/html-text.js +25 -0
  76. package/dist/lib/instruction-input.d.ts +21 -0
  77. package/dist/lib/instruction-input.js +30 -0
  78. package/dist/lib/instruction-provenance.d.ts +54 -0
  79. package/dist/lib/instruction-provenance.js +84 -0
  80. package/dist/lib/session-task-endpoints.d.ts +1 -1
  81. package/dist/lib/session-task-endpoints.js +3 -0
  82. package/dist/lib/task-asset-upload.d.ts +84 -0
  83. package/dist/lib/task-asset-upload.js +374 -0
  84. package/dist/lib/task-closure.d.ts +15 -0
  85. package/dist/lib/task-closure.js +20 -0
  86. package/dist/lib/task-view-render.d.ts +14 -0
  87. package/dist/lib/task-view-render.js +55 -0
  88. package/dist/lib/tasks.d.ts +26 -0
  89. package/dist/lib/tasks.js +38 -1
  90. package/dist/lib/views/vocabulary.d.ts +1 -1
  91. package/dist/lib/views/vocabulary.js +3 -1
  92. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +183 -58
  93. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +286 -100
  94. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.d.ts +21 -33
  95. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.js +24 -50
  96. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.d.ts +19 -7
  97. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.js +26 -10
  98. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessTrust.d.ts +9 -5
  99. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessTrust.js +9 -5
  100. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +1 -13
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/legacyStatePreflight.d.ts +21 -1
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/legacyStatePreflight.js +75 -19
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +183 -58
  105. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +279 -99
  106. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.d.ts +21 -33
  107. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.js +24 -49
  108. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.d.ts +19 -7
  109. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.js +27 -11
  110. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessTrust.d.ts +9 -5
  111. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessTrust.js +9 -5
  112. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
  113. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -4
  114. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/legacyStatePreflight.d.ts +21 -1
  115. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/legacyStatePreflight.js +74 -19
  116. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  117. package/dist/node_modules/@skrr-ai/data-provider/index.js +4588 -4509
  118. package/dist/node_modules/@skrr-ai/data-provider/package.json +1 -1
  119. package/oclif.manifest.json +33980 -33832
  120. package/package.json +3 -3
@@ -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 format_1 = require("../../lib/format");
7
+ const task_view_render_1 = require("../../lib/task-view-render");
7
8
  const web_url_1 = require("../../lib/web-url");
8
9
  class ViewsList extends base_command_1.BaseCommand {
9
10
  static description = 'List personalized Task Views';
@@ -30,13 +31,13 @@ class ViewsList extends base_command_1.BaseCommand {
30
31
  name: view.name,
31
32
  type: view.viewType,
32
33
  group: view.groupBy,
33
- tasks: view.taskIds.length ? String(view.taskIds.length) : 'criteria',
34
+ selection: (0, task_view_render_1.summarizeTaskViewSelection)(view),
34
35
  id: view.id,
35
36
  })), [
36
37
  { key: 'name', header: 'NAME', maxWidth: 36 },
37
38
  { key: 'type', header: 'TYPE' },
38
39
  { key: 'group', header: 'GROUP BY' },
39
- { key: 'tasks', header: 'SELECTION' },
40
+ { key: 'selection', header: 'SELECTION', maxWidth: 48 },
40
41
  { key: 'id', header: 'ID' },
41
42
  ], (line) => this.log(line));
42
43
  }
@@ -33,6 +33,8 @@ class ViewsShow extends base_command_1.BaseCommand {
33
33
  this.log(line);
34
34
  }
35
35
  this.log(result.url);
36
+ // The criteria are half the answer; the tasks they select are the other.
37
+ this.log(`Tasks: ${this.config.bin} tasks list --view ${view.id}`);
36
38
  }
37
39
  }
38
40
  exports.default = ViewsShow;
@@ -1,9 +1,15 @@
1
1
  /**
2
- * The answer a question gets when no terminal exists to ask it on. Written to the
3
- * agent, so it says what happened and what to do rather than pretending a person
4
- * replied.
2
+ * Why a question was dismissed when no terminal exists to ask it on. It is the
3
+ * REASON for a dismissal, never an answer: it travels as the value of the
4
+ * reserved `_dismissed` key, which every consumer already reads as "nobody
5
+ * answered" — the web card sends the same key when a person closes it.
6
+ *
7
+ * It used to be written into every question's answer slot (OSK-9767). That was
8
+ * harmless text for Claude's AskUserQuestion, but a Devin form elicitation read
9
+ * it as the field's value and was answered `accept` with this sentence filled in
10
+ * — consent nobody gave, on a form nobody saw (OSK-10274).
5
11
  */
6
- export declare const NON_INTERACTIVE_QUESTION_ANSWER: string;
12
+ export declare const NON_INTERACTIVE_QUESTION_DISMISSAL: string;
7
13
  export type AgenticStreamEvent = {
8
14
  type: string;
9
15
  sessionId?: string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AgenticStreamClient = exports.USER_CONNECTION_CAP_CODE = exports.NON_INTERACTIVE_QUESTION_ANSWER = void 0;
3
+ exports.AgenticStreamClient = exports.USER_CONNECTION_CAP_CODE = exports.NON_INTERACTIVE_QUESTION_DISMISSAL = void 0;
4
4
  exports.writeJsonLine = writeJsonLine;
5
5
  exports.envelopesText = envelopesText;
6
6
  exports.envelopeKey = envelopeKey;
@@ -20,11 +20,17 @@ const data_provider_1 = require("@skrr-ai/data-provider");
20
20
  const config_1 = require("./config");
21
21
  const publicEndpoints_generated_1 = require("./publicEndpoints.generated");
22
22
  /**
23
- * The answer a question gets when no terminal exists to ask it on. Written to the
24
- * agent, so it says what happened and what to do rather than pretending a person
25
- * replied.
23
+ * Why a question was dismissed when no terminal exists to ask it on. It is the
24
+ * REASON for a dismissal, never an answer: it travels as the value of the
25
+ * reserved `_dismissed` key, which every consumer already reads as "nobody
26
+ * answered" — the web card sends the same key when a person closes it.
27
+ *
28
+ * It used to be written into every question's answer slot (OSK-9767). That was
29
+ * harmless text for Claude's AskUserQuestion, but a Devin form elicitation read
30
+ * it as the field's value and was answered `accept` with this sentence filled in
31
+ * — consent nobody gave, on a form nobody saw (OSK-10274).
26
32
  */
27
- exports.NON_INTERACTIVE_QUESTION_ANSWER = 'No answer: this turn was started non-interactively (no terminal), so nobody can reply to ' +
33
+ exports.NON_INTERACTIVE_QUESTION_DISMISSAL = 'No answer: this turn was started non-interactively (no terminal), so nobody can reply to ' +
28
34
  'questions. Proceed with your best judgement, or finish by stating exactly what you need.';
29
35
  /**
30
36
  * Write exactly one complete JSON value on one line.
@@ -710,13 +716,21 @@ class AgenticStreamClient {
710
716
  // "Not answered … timed out", at the cost of the whole wait. Answer at once
711
717
  // instead, saying why, so the agent proceeds or states what it needs. An
712
718
  // explicit `input` (tests, an embedding caller) is treated as interactive.
719
+ //
720
+ // "At once" is a DISMISSAL, never an answer. Nothing goes in any question's
721
+ // answer slot: a harness that turns answers into structured consent (an ACP
722
+ // form elicitation) would otherwise accept whatever text sits there
723
+ // (OSK-10274). The reason rides on `_dismissed`, where the server maps it to
724
+ // a dismissal for daemon sessions and a cloud session still reads why.
713
725
  const interactive = this.options.input !== undefined || Boolean(process.stdin.isTTY);
714
726
  const answers = {};
715
- for (const question of event.questions ?? []) {
716
- const answer = interactive
717
- ? await this.ask(`${question.header ?? 'Question'}: ${question.question}\n> `)
718
- : exports.NON_INTERACTIVE_QUESTION_ANSWER;
719
- answers[question.header ?? question.question] = answer;
727
+ if (interactive) {
728
+ for (const question of event.questions ?? []) {
729
+ answers[question.header ?? question.question] = await this.ask(`${question.header ?? 'Question'}: ${question.question}\n> `);
730
+ }
731
+ }
732
+ else {
733
+ answers._dismissed = exports.NON_INTERACTIVE_QUESTION_DISMISSAL;
720
734
  }
721
735
  if (event.questionId && this.socket) {
722
736
  this.socket.emit('question:answer', {
@@ -824,7 +838,7 @@ class AgenticStreamClient {
824
838
  this.renderedText = this.accumulated;
825
839
  }
826
840
  else if (event.type === 'question.answered' && event.nonInteractive) {
827
- output.write('\n[question] no terminal to answer on, so the agent was told to proceed without an answer\n');
841
+ output.write('\n[question] no terminal to answer on, so the question was dismissed unanswered\n');
828
842
  this.renderedText = this.accumulated;
829
843
  }
830
844
  }
@@ -93,9 +93,14 @@ async function apiFetch(pathOrUrl, opts = {}) {
93
93
  ? assertTrustedAutonomousUrl(pathOrUrl, baseURL)
94
94
  : pathOrUrl
95
95
  : `${baseURL.replace(/\/+$/, '')}${pathOrUrl.startsWith('/') ? '' : '/'}${pathOrUrl}`;
96
+ // A FormData body passes through untouched — fetch sets the multipart
97
+ // boundary itself, and stringifying would destroy it. Upload commands
98
+ // (tasks attachments) need this on the session-scoped path, which is not
99
+ // in dataService.
100
+ const isFormData = typeof FormData !== 'undefined' && opts.body instanceof FormData;
96
101
  let body;
97
102
  if (opts.body !== undefined && method !== 'GET') {
98
- body = JSON.stringify(opts.body);
103
+ body = isFormData ? opts.body : JSON.stringify(opts.body);
99
104
  }
100
105
  // Same rule as every `dataService` write (`@skrr-ai/data-provider`
101
106
  // idempotency), chosen once so the 401 retries below resend the same key.
@@ -145,7 +150,7 @@ async function apiFetch(pathOrUrl, opts = {}) {
145
150
  Accept: 'application/json',
146
151
  'X-Oversky-Origin': (0, refresh_1.getCliOrigin)(),
147
152
  };
148
- if (opts.body !== undefined && method !== 'GET') {
153
+ if (opts.body !== undefined && method !== 'GET' && !isFormData) {
149
154
  headers['Content-Type'] = 'application/json';
150
155
  }
151
156
  if (credential.token) {
@@ -34,10 +34,7 @@ export interface ManagedCliProviderState {
34
34
  updateDisabledReason?: string | null;
35
35
  platformSupported?: boolean;
36
36
  authentication?: ManagedCliAuthentication | null;
37
- job?: {
38
- id?: string;
39
- status?: string;
40
- } | null;
37
+ job?: ManagedCliInstallJob | null;
41
38
  }
42
39
  export interface CliInstallersResponse {
43
40
  daemonId?: string;
@@ -155,6 +152,7 @@ export interface ManagedCliInstallJob {
155
152
  status: 'queued' | 'running' | 'succeeded' | 'failed';
156
153
  /** `update` for an in-place update; absent from an older daemon, meaning install. */
157
154
  kind?: 'install' | 'update';
155
+ startedAt?: string;
158
156
  previousVersion?: string | null;
159
157
  installedVersion?: string | null;
160
158
  error?: string;
@@ -184,6 +182,17 @@ export interface StartInstallResponse {
184
182
  };
185
183
  }
186
184
  export declare function startManagedCliInstall(daemonId: string, provider: string, credential?: unknown): Promise<StartInstallResponse>;
185
+ /**
186
+ * How long after a start request times out a job for the same provider is taken
187
+ * to be the one that request began.
188
+ *
189
+ * The relay holds the request while the daemon answers, and a gateway can cut
190
+ * that off at its own deadline: an update returned HTTP 504 after 21.9s while
191
+ * the job it started ran to success, and the caller was told the outcome was
192
+ * unknown (OSK-10096 verifier). A job that started within this window of the
193
+ * request is that request's job.
194
+ */
195
+ export declare const START_TIMEOUT_JOB_WINDOW_MS = 120000;
187
196
  /** Ask the daemon to update an installed CLI in place (OSK-10096). */
188
197
  export declare function startManagedCliUpdate(daemonId: string, provider: string, credential?: unknown): Promise<StartInstallResponse>;
189
198
  /**
@@ -191,6 +200,16 @@ export declare function startManagedCliUpdate(daemonId: string, provider: string
191
200
  * "Updated Claude Code 2.1.270 → 2.1.280." or "Claude Code is still 2.1.270:
192
201
  * its updater found nothing newer." Null when the job does not carry both.
193
202
  */
203
+ /**
204
+ * The job a timed-out start request began, or null when none can be claimed.
205
+ *
206
+ * Read from the daemon's own inventory rather than assumed: it is the only
207
+ * party that knows whether the request reached it.
208
+ */
209
+ export declare function jobStartedDespiteTimeout(daemonId: string, provider: string, credential?: unknown, opts?: {
210
+ now?: number;
211
+ windowMs?: number;
212
+ }): Promise<ManagedCliInstallJob | null>;
194
213
  export declare function updateOutcomeLine(job: ManagedCliInstallJob): string | null;
195
214
  export declare function readManagedCliInstallJob(daemonId: string, jobId: string, credential?: unknown): Promise<{
196
215
  job?: ManagedCliInstallJob;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.INSTALL_POLL_INTERVAL_MS = exports.INSTALL_MAX_WAIT_MS = exports.ROW_READINESS_TIMEOUT_MS = exports.MANAGED_CLI_PROVIDER_SINCE = void 0;
3
+ exports.INSTALL_POLL_INTERVAL_MS = exports.INSTALL_MAX_WAIT_MS = exports.START_TIMEOUT_JOB_WINDOW_MS = exports.ROW_READINESS_TIMEOUT_MS = exports.MANAGED_CLI_PROVIDER_SINCE = void 0;
4
4
  exports.describeProvidersNotOffered = describeProvidersNotOffered;
5
5
  exports.providersNotOffered = providersNotOffered;
6
6
  exports.listCliInstallers = listCliInstallers;
@@ -10,6 +10,7 @@ exports.readinessKey = readinessKey;
10
10
  exports.readinessForRows = readinessForRows;
11
11
  exports.startManagedCliInstall = startManagedCliInstall;
12
12
  exports.startManagedCliUpdate = startManagedCliUpdate;
13
+ exports.jobStartedDespiteTimeout = jobStartedDespiteTimeout;
13
14
  exports.updateOutcomeLine = updateOutcomeLine;
14
15
  exports.readManagedCliInstallJob = readManagedCliInstallJob;
15
16
  exports.unseenLogLines = unseenLogLines;
@@ -88,12 +89,14 @@ function describeProvidersNotOffered(missing) {
88
89
  .join(', ');
89
90
  }
90
91
  function providersNotOffered(offered, known) {
91
- // Canonicalize BOTH sides. An older daemon answers under the spelling it was
92
- // built with — the 0.8.35 build reports the first-party harness under its
93
- // older spelling, so a raw comparison against the current one reported it missing on
94
- // a machine where it is installed and working. Caught by running this against
95
- // the very daemon the finding was about: the first version of this helper
96
- // said "Not offered: devin, <first-party harness>" when only `devin` was true.
92
+ // Canonicalize BOTH sides. A daemon answers under the spelling it was built
93
+ // with — during the rename window the 0.8.35 build reported the first-party
94
+ // harness under its older spelling, so a raw comparison against the current
95
+ // one reported it missing on a machine where it was installed and working (the
96
+ // first version of this helper said "Not offered: devin, <first-party
97
+ // harness>" when only `devin` was true). This is INGRESS, so it canonicalizes
98
+ // accepted spellings only: since contract a retired spelling is not the
99
+ // harness, and a daemon still answering under one predates the wire release.
97
100
  const seen = new Set((offered ?? []).map((provider) => canonical(provider.provider)).filter(Boolean));
98
101
  return known.filter((provider) => !seen.has(canonical(provider)));
99
102
  }
@@ -126,9 +129,17 @@ function readinessOf(provider) {
126
129
  ok: false,
127
130
  };
128
131
  }
132
+ // Every other not-ready state tells the reader what to do next, so this one
133
+ // does too. Name the install command only when the daemon says it can
134
+ // install the provider. Otherwise the daemon's own reason, or the bare
135
+ // fact, is the whole answer.
136
+ const key = canonical(provider.provider);
129
137
  return {
130
138
  label: 'not-installed',
131
- detail: provider.installDisabledReason || 'No binary for this provider on this machine.',
139
+ detail: provider.installDisabledReason ||
140
+ (provider.installable === true && key
141
+ ? `No binary for this provider on this machine. Install it with \`skrr harnesses install ${key}\`.`
142
+ : 'No binary for this provider on this machine.'),
132
143
  ok: false,
133
144
  };
134
145
  }
@@ -214,6 +225,17 @@ async function readinessForRows(rows, credential, opts = {}) {
214
225
  async function startManagedCliInstall(daemonId, provider, credential) {
215
226
  return (0, api_fetch_1.apiFetch)(`/api/daemons/${encodeURIComponent(daemonId)}/cli-installers/${encodeURIComponent(provider)}/install`, { method: 'POST', body: {}, credential: credential });
216
227
  }
228
+ /**
229
+ * How long after a start request times out a job for the same provider is taken
230
+ * to be the one that request began.
231
+ *
232
+ * The relay holds the request while the daemon answers, and a gateway can cut
233
+ * that off at its own deadline: an update returned HTTP 504 after 21.9s while
234
+ * the job it started ran to success, and the caller was told the outcome was
235
+ * unknown (OSK-10096 verifier). A job that started within this window of the
236
+ * request is that request's job.
237
+ */
238
+ exports.START_TIMEOUT_JOB_WINDOW_MS = 120_000;
217
239
  /** Ask the daemon to update an installed CLI in place (OSK-10096). */
218
240
  async function startManagedCliUpdate(daemonId, provider, credential) {
219
241
  return (0, api_fetch_1.apiFetch)(`/api/daemons/${encodeURIComponent(daemonId)}/cli-installers/${encodeURIComponent(provider)}/update`, { method: 'POST', body: {}, credential: credential });
@@ -223,6 +245,32 @@ async function startManagedCliUpdate(daemonId, provider, credential) {
223
245
  * "Updated Claude Code 2.1.270 → 2.1.280." or "Claude Code is still 2.1.270:
224
246
  * its updater found nothing newer." Null when the job does not carry both.
225
247
  */
248
+ /**
249
+ * The job a timed-out start request began, or null when none can be claimed.
250
+ *
251
+ * Read from the daemon's own inventory rather than assumed: it is the only
252
+ * party that knows whether the request reached it.
253
+ */
254
+ async function jobStartedDespiteTimeout(daemonId, provider, credential, opts = {}) {
255
+ let response;
256
+ try {
257
+ response = await listCliInstallers(daemonId, credential);
258
+ }
259
+ catch {
260
+ return null;
261
+ }
262
+ const row = (response.providers ?? []).find((entry) => canonical(entry.provider) === canonical(provider));
263
+ const job = row?.job;
264
+ if (!job?.id)
265
+ return null;
266
+ if (job.status === 'queued' || job.status === 'running')
267
+ return job;
268
+ const startedAt = job.startedAt ? Date.parse(job.startedAt) : NaN;
269
+ if (!Number.isFinite(startedAt))
270
+ return null;
271
+ const now = opts.now ?? Date.now();
272
+ return now - startedAt <= (opts.windowMs ?? exports.START_TIMEOUT_JOB_WINDOW_MS) ? job : null;
273
+ }
226
274
  function updateOutcomeLine(job) {
227
275
  const before = job.previousVersion;
228
276
  const after = job.installedVersion;
@@ -28,6 +28,7 @@
28
28
  * whole area has been removing. So: asked when there is someone to ask, skipped
29
29
  * with an instruction when there is not, and never on the opt-out.
30
30
  */
31
+ import { type LegacyStateReport } from '@skrr-ai/auth-core';
31
32
  import { confirm } from './prompt';
32
33
  export interface DaemonSetupOutcome {
33
34
  status: 'installed' | 'declined' | 'skipped' | 'failed';
@@ -41,9 +42,25 @@ export interface DaemonSetupOptions {
41
42
  confirmImpl?: typeof confirm;
42
43
  interactive?: boolean;
43
44
  log?: (line: string) => void;
45
+ /** Injected by tests. */
46
+ findLegacyStateImpl?: () => LegacyStateReport;
44
47
  }
48
+ /**
49
+ * Refuse, before anything is fetched or asked, on a machine still carrying state
50
+ * from the previous product identity.
51
+ *
52
+ * The runtime refuses the same machine in `skrrd install`, and it is right to —
53
+ * but that check can only run once there is a runtime to run it, so onboarding
54
+ * downloaded a release, asked "Set it up now?", and only then stopped (OSK-6895).
55
+ * The detection is a read of two directory names, so the CLI does it first and
56
+ * the refusal names `skrr daemon install`, the command the person actually uses.
57
+ * The runtime's own check stays: `skrrd install` can be run directly.
58
+ *
59
+ * Returns true when it refused, having printed why and what to run.
60
+ */
61
+ export declare function refuseOnLegacyLocalState(log: (line: string) => void, findImpl?: () => LegacyStateReport): boolean;
45
62
  /**
46
63
  * Ask, then install. Returns without touching anything when there is nobody to
47
- * ask or the operator has opted out.
64
+ * ask, the operator has opted out, or the machine still needs its cutover.
48
65
  */
49
66
  export declare function offerDaemonSetup(opts?: DaemonSetupOptions): Promise<DaemonSetupOutcome>;
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.refuseOnLegacyLocalState = refuseOnLegacyLocalState;
3
4
  exports.offerDaemonSetup = offerDaemonSetup;
4
5
  /**
5
6
  * daemon-setup.ts — offer to set this machine up, at the moment we can.
@@ -31,15 +32,40 @@ exports.offerDaemonSetup = offerDaemonSetup;
31
32
  * whole area has been removing. So: asked when there is someone to ask, skipped
32
33
  * with an instruction when there is not, and never on the opt-out.
33
34
  */
35
+ const auth_core_1 = require("@skrr-ai/auth-core");
34
36
  const prompt_1 = require("./prompt");
35
37
  const daemon_binding_1 = require("./daemon-binding");
36
38
  const daemon_handoff_opt_out_1 = require("./daemon-handoff-opt-out");
37
39
  const exec_runtime_binary_1 = require("./exec-runtime-binary");
38
40
  const daemon_installer_1 = require("./daemon-installer");
39
41
  const ssh_detect_1 = require("./ssh-detect");
42
+ /**
43
+ * Refuse, before anything is fetched or asked, on a machine still carrying state
44
+ * from the previous product identity.
45
+ *
46
+ * The runtime refuses the same machine in `skrrd install`, and it is right to —
47
+ * but that check can only run once there is a runtime to run it, so onboarding
48
+ * downloaded a release, asked "Set it up now?", and only then stopped (OSK-6895).
49
+ * The detection is a read of two directory names, so the CLI does it first and
50
+ * the refusal names `skrr daemon install`, the command the person actually uses.
51
+ * The runtime's own check stays: `skrrd install` can be run directly.
52
+ *
53
+ * Returns true when it refused, having printed why and what to run.
54
+ */
55
+ function refuseOnLegacyLocalState(log, findImpl = auth_core_1.findLegacyLocalState) {
56
+ const report = findImpl();
57
+ if (!report.hasLegacyState)
58
+ return false;
59
+ log('');
60
+ for (const line of (0, auth_core_1.describeLegacyState)(report, 'skrr daemon').split('\n')) {
61
+ log(line ? ` ${line}` : '');
62
+ }
63
+ log('');
64
+ return true;
65
+ }
40
66
  /**
41
67
  * Ask, then install. Returns without touching anything when there is nobody to
42
- * ask or the operator has opted out.
68
+ * ask, the operator has opted out, or the machine still needs its cutover.
43
69
  */
44
70
  async function offerDaemonSetup(opts = {}) {
45
71
  const env = opts.env ?? process.env;
@@ -51,6 +77,12 @@ async function offerDaemonSetup(opts = {}) {
51
77
  if (optOut) {
52
78
  return { status: 'skipped', detail: `skipped by ${optOut}` };
53
79
  }
80
+ if (refuseOnLegacyLocalState(log, opts.findLegacyStateImpl)) {
81
+ return {
82
+ status: 'skipped',
83
+ detail: 'state from the previous product identity is still here — finish the cutover above, then run `skrr daemon install`',
84
+ };
85
+ }
54
86
  const interactive = opts.interactive ?? !(0, ssh_detect_1.isNonInteractive)();
55
87
  if (!interactive) {
56
88
  // A prompt with no terminal hangs forever, which is worse than the extra
@@ -845,8 +845,10 @@ function selectDedicatedAttachHarness(lease, harnesses, engine) {
845
845
  }
846
846
  const providers = [...new Set(rows.map((row) => row.provider))];
847
847
  if (engine) {
848
- // Any accepted spelling of an engine names it: a user who typed the first-party
849
- // harness's older name means the row stored under its canonical one.
848
+ // Any ACCEPTED spelling of an engine names it — during a rename's alias window,
849
+ // a user who typed the first-party harness's older name means the row stored
850
+ // under its canonical one. A retired spelling is accepted nowhere, so it names
851
+ // no engine here either (`isSameHarnessProvider`).
850
852
  const match = rows.find((row) => (0, auth_core_1.isSameHarnessProvider)(row.provider, engine));
851
853
  if (!match) {
852
854
  return {
@@ -68,7 +68,7 @@ const MIME_TYPES = {
68
68
  '.md': 'text/markdown',
69
69
  '.mjs': 'text/javascript',
70
70
  '.mkv': 'video/mkv',
71
- '.mov': 'video/mov',
71
+ '.mov': 'video/quicktime',
72
72
  '.mp3': 'audio/mpeg',
73
73
  '.mp4': 'video/mp4',
74
74
  '.ogv': 'video/ogv',
@@ -83,7 +83,7 @@ interface AgentRow {
83
83
  defaultForHarness?: string;
84
84
  }
85
85
  export interface FirstPartyHarnessAgentDeps {
86
- /** Agents this user may run. Bounded; see `ROSTER_LIMIT`. */
86
+ /** Agents this user may run. Paged and bounded; see `ROSTER_MAX`. */
87
87
  listAgents?: (signal?: AbortSignal) => Promise<AgentRow[]>;
88
88
  /** Direct read, for an id outside the bounded roster. `null` when unreadable. */
89
89
  getAgent?: (id: string, signal?: AbortSignal) => Promise<AgentRow | null>;
@@ -158,4 +158,19 @@ export declare function readConfiguredAgentId(): string | undefined;
158
158
  export declare function setConfiguredAgentId(ref: string, deps?: FirstPartyHarnessAgentDeps, options?: FirstPartyHarnessAgentOptions): Promise<FirstPartyHarnessAgent>;
159
159
  /** Remove the setting. Returns what was there, so the caller can say so. */
160
160
  export declare function clearConfiguredAgentId(): string | undefined;
161
+ /** One `/api/agents` response: a list page, or a bare array from an older server. */
162
+ export type AgentRosterPage = {
163
+ data?: AgentRow[];
164
+ agents?: AgentRow[];
165
+ has_more?: boolean;
166
+ after?: string | null;
167
+ } | AgentRow[];
168
+ /**
169
+ * Read roster pages until the server reports no more, up to `max` rows.
170
+ *
171
+ * A page that does not say `has_more` with a usable `after` cursor ends the walk,
172
+ * so an older server that ignores `cursor` (or returns a bare array) costs exactly
173
+ * the one request it always did.
174
+ */
175
+ export declare function collectRosterPages(fetchPage: (cursor?: string) => Promise<AgentRosterPage>, max?: number): Promise<AgentRow[]>;
161
176
  export {};
@@ -55,18 +55,26 @@ exports.resolveFirstPartyHarnessAgent = resolveFirstPartyHarnessAgent;
55
55
  exports.readConfiguredAgentId = readConfiguredAgentId;
56
56
  exports.setConfiguredAgentId = setConfiguredAgentId;
57
57
  exports.clearConfiguredAgentId = clearConfiguredAgentId;
58
+ exports.collectRosterPages = collectRosterPages;
58
59
  const auth_core_1 = require("@skrr-ai/auth-core");
59
60
  const data_provider_1 = require("@skrr-ai/data-provider");
60
61
  const api_fetch_1 = require("./api-fetch");
61
62
  const config_1 = require("./config");
62
63
  const first_party_harness_managed_1 = require("./first-party-harness-managed");
64
+ /** Page size for one `/api/agents` roster request. */
65
+ const ROSTER_PAGE_SIZE = 100;
63
66
  /**
64
- * How many agents the refusal is allowed to look at.
67
+ * The most agents the roster will read, across pages.
65
68
  *
66
- * A bound rather than pagination: this list exists to answer "did you mean one of
67
- * these?", and a user with more agents than this is a user who will pass the flag.
69
+ * The roster is not only the refusal's "did you mean" list. Resolution also finds
70
+ * the user's EXISTING default agent in it (`createDefault: false`) and matches a
71
+ * configured agent by name, so one fixed page answered "you have no default agent"
72
+ * for an account whose default was agent 198 of 210 (found by the end-to-end run
73
+ * on 2026-09-17). Pages are followed until the server says there are no more, up
74
+ * to this bound, which keeps a pathological account from turning one session
75
+ * start into an unbounded crawl.
68
76
  */
69
- const ROSTER_LIMIT = 100;
77
+ const ROSTER_MAX = 1000;
70
78
  /** How many candidates a refusal prints before it summarizes the rest. */
71
79
  const CANDIDATES_SHOWN = 12;
72
80
  class FirstPartyHarnessAgentError extends Error {
@@ -220,7 +228,7 @@ async function resolveFirstPartyHarnessAgent(explicit, deps = {}, options = {})
220
228
  const created = options.createDefault === false
221
229
  ? null
222
230
  : await (deps.ensureDefaultAgent ?? ensureDefaultAgent)(
223
- // Canonical: what the server stores. It accepts every spelling.
231
+ // Canonical: what the server stores, and the one spelling every server accepts.
224
232
  auth_core_1.FIRST_PARTY_HARNESS.provider, options.signal);
225
233
  if (created?.id) {
226
234
  return {
@@ -330,11 +338,12 @@ function readRawConfigKey() {
330
338
  * first real session read. Returns whether the file now holds it under the
331
339
  * neutral key.
332
340
  *
333
- * Reading alone used to leave it where it was, which is safe only while that key
334
- * is still an accepted spelling. The contract phase empties the identity's
335
- * aliases, `legacyAgentConfigKeys()` then stops naming the old key, and a user
341
+ * Reading alone used to leave it where it was. `legacyAgentConfigKeys()` keeps
342
+ * naming an old key after its spelling is retired (it reads the identity's
343
+ * ARTIFACT spellings, since the file predates the rename), but a key that is only
344
+ * ever read is one the next pruning of `formerSpellings` would strand, and a user
336
345
  * who never ran `--set`, `--clear` or logout would silently lose their setting.
337
- * Moving it here retires the old key long before that date.
346
+ * Moving it on the first session read retires the old key long before that.
338
347
  *
339
348
  * Only the session path calls this — `skrr code doctor` reports and never
340
349
  * writes. Best-effort: an unwritable config is left alone and still read.
@@ -368,14 +377,35 @@ function writeConfigKey(id) {
368
377
  * `fields=summary` drops the prompt-sized fields; this needs an id and a name.
369
378
  */
370
379
  async function listAgents(signal) {
371
- const response = await (0, api_fetch_1.apiFetch)(`/api/agents?limit=${ROSTER_LIMIT}&fields=summary&requiredPermission=${data_provider_1.PermissionBits.EDIT}`, signal ? { signal } : {});
372
- if (Array.isArray(response))
373
- return response;
374
- if (Array.isArray(response?.data))
375
- return response.data;
376
- if (Array.isArray(response?.agents))
377
- return response.agents;
378
- return [];
380
+ return collectRosterPages((cursor) => (0, api_fetch_1.apiFetch)(`/api/agents?limit=${ROSTER_PAGE_SIZE}&fields=summary&requiredPermission=${data_provider_1.PermissionBits.EDIT}` +
381
+ (cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''), signal ? { signal } : {}));
382
+ }
383
+ /**
384
+ * Read roster pages until the server reports no more, up to `max` rows.
385
+ *
386
+ * A page that does not say `has_more` with a usable `after` cursor ends the walk,
387
+ * so an older server that ignores `cursor` (or returns a bare array) costs exactly
388
+ * the one request it always did.
389
+ */
390
+ async function collectRosterPages(fetchPage, max = ROSTER_MAX) {
391
+ const rows = [];
392
+ let cursor;
393
+ do {
394
+ const response = await fetchPage(cursor);
395
+ const page = Array.isArray(response)
396
+ ? response
397
+ : Array.isArray(response?.data)
398
+ ? response.data
399
+ : Array.isArray(response?.agents)
400
+ ? response.agents
401
+ : [];
402
+ rows.push(...page);
403
+ const next = !Array.isArray(response) && response?.has_more === true && typeof response.after === 'string'
404
+ ? response.after
405
+ : undefined;
406
+ cursor = next && next !== cursor && page.length > 0 ? next : undefined;
407
+ } while (cursor && rows.length < max);
408
+ return rows.slice(0, max);
379
409
  }
380
410
  /**
381
411
  * The caller's own user id, memoized FOR THIS PROCESS ONLY.