@skrr-ai/cli 0.1.57 → 0.1.59

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 (49) hide show
  1. package/dist/base-command.d.ts +0 -16
  2. package/dist/base-command.js +65 -11
  3. package/dist/commands/agents/show.js +13 -2
  4. package/dist/commands/harnesses/ping.d.ts +2 -0
  5. package/dist/commands/harnesses/ping.js +10 -1
  6. package/dist/commands/login.js +6 -1
  7. package/dist/commands/logout.d.ts +0 -46
  8. package/dist/commands/logout.js +28 -2
  9. package/dist/commands/pair.js +1 -1
  10. package/dist/lib/agentic-stream.d.ts +54 -0
  11. package/dist/lib/agentic-stream.js +153 -15
  12. package/dist/lib/api-fetch.d.ts +11 -0
  13. package/dist/lib/api-fetch.js +98 -1
  14. package/dist/lib/auth-storage.d.ts +27 -0
  15. package/dist/lib/auth-storage.js +42 -0
  16. package/dist/lib/commitments.js +1 -0
  17. package/dist/lib/daemonBroker.d.ts +5 -1
  18. package/dist/lib/daemonBroker.js +13 -2
  19. package/dist/lib/daemonBrokerRefusal.d.ts +21 -0
  20. package/dist/lib/daemonBrokerRefusal.js +87 -2
  21. package/dist/lib/daemonHandoff.d.ts +1 -1
  22. package/dist/lib/daemonHandoff.js +18 -0
  23. package/dist/lib/dedicated-guest.d.ts +13 -0
  24. package/dist/lib/dedicated-guest.js +22 -0
  25. package/dist/lib/dedicated-machines.js +17 -1
  26. package/dist/lib/dedicated-service.d.ts +1 -13
  27. package/dist/lib/dedicated-service.js +7 -19
  28. package/dist/lib/login.js +12 -3
  29. package/dist/lib/oauthLogin.js +3 -1
  30. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +41 -0
  31. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +147 -3
  32. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +17 -0
  33. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +38 -0
  34. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +2 -2
  35. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +5 -3
  36. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.d.ts +21 -1
  37. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.js +40 -7
  38. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +41 -0
  39. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +147 -4
  40. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +17 -0
  41. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +38 -1
  42. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +2 -2
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -2
  44. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.d.ts +21 -1
  45. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.js +39 -6
  46. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  47. package/dist/node_modules/@skrr-ai/data-provider/index.js +4388 -4349
  48. package/oclif.manifest.json +26425 -26425
  49. package/package.json +1 -1
@@ -20,22 +20,6 @@ type OclifFlagOutput = {
20
20
  type OclifArgOutput = {
21
21
  [name: string]: unknown;
22
22
  };
23
- /**
24
- * Base class for every CLI command.
25
- *
26
- * Responsibilities:
27
- * - Run the one-shot auth-out-of-config migration (Phase H).
28
- * - Parse global flags (`--bare`, `--token`) from argv once and thread
29
- * them through the credential resolver and the HTTP adapter.
30
- * - Resolve the active credential via the CredentialResolver chain —
31
- * flag → env → helper → keychain → file (Phase H.1). Returns the
32
- * source name for display in `skrr whoami` / `skrr doctor`.
33
- * - Wire the Node HttpAdapter into @skrr-ai/data-provider so
34
- * `dataService.*` calls use our fetch-based transport with bearer
35
- * auth that matches what the resolver surfaced.
36
- * - Provide `requireAuth()` for commands that need a signed-in user,
37
- * and `handleApiError()` for uniform error translation.
38
- */
39
23
  export declare abstract class BaseCommand extends Command {
40
24
  /** Add the owning agent's web detail link without overwriting resource URLs. */
41
25
  protected withAgentContext<T>(agentId: string, value: T): T & {
@@ -16,6 +16,7 @@ exports.isDaemonScopedCredential = isDaemonScopedCredential;
16
16
  exports.describeDaemonScopedCredential = describeDaemonScopedCredential;
17
17
  exports.isTypedServerRefusal = isTypedServerRefusal;
18
18
  exports.isRetryableFailure = isRetryableFailure;
19
+ const node_fs_1 = require("node:fs");
19
20
  const core_1 = require("@oclif/core");
20
21
  Object.defineProperty(exports, "Flags", { enumerable: true, get: function () { return core_1.Flags; } });
21
22
  const option_hint_1 = require("./lib/option-hint");
@@ -51,6 +52,7 @@ const update_check_1 = require("./lib/update-check");
51
52
  const web_url_1 = require("./lib/web-url");
52
53
  const edge_error_page_1 = require("./lib/edge-error-page");
53
54
  const delegated_cli_1 = require("./lib/delegated-cli");
55
+ const dedicated_guest_1 = require("./lib/dedicated-guest");
54
56
  const tasks_1 = require("./lib/tasks");
55
57
  function assigneePayloadNeedsResolution(payload) {
56
58
  const fields = [
@@ -82,6 +84,25 @@ function assigneePayloadNeedsResolution(payload) {
82
84
  * - Provide `requireAuth()` for commands that need a signed-in user,
83
85
  * and `handleApiError()` for uniform error translation.
84
86
  */
87
+ /**
88
+ * On a Dedicated Runtime guest, the description of a daemon that did not
89
+ * answer the hand-off at all — revoked and exited, cycling under systemd, or
90
+ * gone behind a stale descriptor (OSK-12473). Null anywhere else, where "Not
91
+ * signed in. Run `skrr login` first." is the right sentence.
92
+ */
93
+ function describeAbsentGuestDaemon(outcome, bin) {
94
+ const onGuest = outcome.dedicatedGuest === true || (0, dedicated_guest_1.runningOnDedicatedRuntimeGuest)();
95
+ if (!onGuest)
96
+ return null;
97
+ let descriptorPresent = false;
98
+ try {
99
+ descriptorPresent = (0, node_fs_1.existsSync)(daemonBroker_1.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR);
100
+ }
101
+ catch {
102
+ /* unreadable reads as absent */
103
+ }
104
+ return (0, daemonBrokerRefusal_1.describeGuestDaemonUnavailable)(outcome, { bin, onGuest, descriptorPresent });
105
+ }
85
106
  class BaseCommand extends core_1.Command {
86
107
  /** Add the owning agent's web detail link without overwriting resource URLs. */
87
108
  withAgentContext(agentId, value) {
@@ -375,7 +396,9 @@ class BaseCommand extends core_1.Command {
375
396
  }
376
397
  : null;
377
398
  }
378
- else if ((this.brokerRefusal = (0, daemonBrokerRefusal_1.describeBrokerRefusal)(autoResult.outcome, this.config.bin))) {
399
+ else if ((this.brokerRefusal =
400
+ (0, daemonBrokerRefusal_1.describeBrokerRefusal)(autoResult.outcome, this.config.bin) ??
401
+ describeAbsentGuestDaemon(autoResult.outcome, this.config.bin))) {
379
402
  // Reported by requireAuth(); nothing to print here.
380
403
  }
381
404
  else if (autoResult.outcome.reason === 'rate_limited') {
@@ -1140,19 +1163,45 @@ class BaseCommand extends core_1.Command {
1140
1163
  bin: this.config.bin,
1141
1164
  cliId: this.cliConfig ? (0, cli_id_1.resolveCliId)(this.cliConfig) : undefined,
1142
1165
  });
1143
- this.failWithCliError({
1144
- message: say(presentedCiToken
1145
- ? `Authentication failed: this is an \`osk_ci_*\` token, which only bootstraps a ` +
1166
+ // OSK-12474 — `apiFetch` asked the local daemon for a hand-off after the
1167
+ // refused refresh, and the daemon refused too. Its reason decides the
1168
+ // repair (OSK-12149, OSK-12151), and on a Dedicated guest there is no
1169
+ // login to run, so its remedy replaces the generic "Run login".
1170
+ const brokerRefusal = presentedCiToken || !e.brokerFailure
1171
+ ? null
1172
+ : (0, daemonBrokerRefusal_1.describeBrokerRefusal)(e.brokerFailure, this.config.bin);
1173
+ const renewal = refreshRefusal
1174
+ ? refreshRefusal.message.replace(/\s*Run `[^`]+` to sign in again\.$/, '')
1175
+ : "This CLI's own sign-in could not be renewed.";
1176
+ const details = {
1177
+ ...(refreshRefusal && bodyCode ? { accessTokenCode: bodyCode } : {}),
1178
+ ...(brokerRefusal
1179
+ ? { broker: brokerRefusal.broker, suggestion: brokerRefusal.remedy }
1180
+ : {}),
1181
+ };
1182
+ let authMessage;
1183
+ if (presentedCiToken) {
1184
+ authMessage =
1185
+ `Authentication failed: this is an \`osk_ci_*\` token, which only bootstraps a ` +
1146
1186
  `daemon (\`OVERSKY_TOKEN=<token> skrrd start\`). ${this.config.bin} commands do ` +
1147
- `not accept it — sign in with \`${this.config.bin} login\` instead.`
1148
- : refreshRefusal
1149
- ? `Authentication failed. ${refreshRefusal.message}`
1150
- : `Authentication failed. Run \`${this.config.bin} login\` to sign in again.`),
1187
+ `not accept it — sign in with \`${this.config.bin} login\` instead.`;
1188
+ }
1189
+ else if (brokerRefusal) {
1190
+ authMessage = `Authentication failed. ${renewal} ${brokerRefusal.headline}\n→ ${brokerRefusal.remedy}`;
1191
+ }
1192
+ else if (refreshRefusal) {
1193
+ authMessage = `Authentication failed. ${refreshRefusal.message}`;
1194
+ }
1195
+ else {
1196
+ authMessage = `Authentication failed. Run \`${this.config.bin} login\` to sign in again.`;
1197
+ }
1198
+ this.failWithCliError({
1199
+ message: say(authMessage),
1151
1200
  code: refreshRefusal?.code || bodyCode || 'AUTHENTICATION_FAILED',
1152
1201
  status: e.status,
1153
1202
  exit: exitCode,
1154
1203
  retryable: true,
1155
- ...(refreshRefusal && bodyCode ? { details: { accessTokenCode: bodyCode } } : {}),
1204
+ ...(Object.keys(details).length > 0 ? { details } : {}),
1156
1205
  });
1157
1206
  }
1158
1207
  else if ((e.status === 403 || e.status === 504) &&
@@ -1438,9 +1487,14 @@ class BaseCommand extends core_1.Command {
1438
1487
  bin: this.config.bin,
1439
1488
  cliId: this.cliConfig ? (0, cli_id_1.resolveCliId)(this.cliConfig) : undefined,
1440
1489
  });
1441
- const why = refusal
1490
+ const brokerRefusal = e.brokerFailure
1491
+ ? (0, daemonBrokerRefusal_1.describeBrokerRefusal)(e.brokerFailure, this.config.bin)
1492
+ : null;
1493
+ const why = (refusal
1442
1494
  ? refusal.message.replace(/\s*Run `[^`]+` to sign in again\.$/, '')
1443
- : 'The server did not accept this sign-in.';
1495
+ : 'The server did not accept this sign-in.') +
1496
+ // OSK-12474 — the daemon was asked for a hand-off and refused; say why.
1497
+ (brokerRefusal ? ` ${brokerRefusal.headline}` : '');
1444
1498
  this.failWithCliError({
1445
1499
  message: `Authentication failed${statusSuffix}. ${why} Your ${write.summary} was not applied; it ` +
1446
1500
  `was saved locally as ${queuedId} so it is not lost. Sign in with ` +
@@ -54,8 +54,19 @@ function agentPermissionLines(agent, harness) {
54
54
  return [`Permission: ${stored} — enforced by the ${harness} harness`];
55
55
  }
56
56
  const lines = [];
57
- if (typeof effective.mode === 'string' && effective.mode) {
58
- const from = PERMISSION_SOURCE_LABELS[effective.source] ?? effective.source;
57
+ const from = PERMISSION_SOURCE_LABELS[effective.source] ?? effective.source;
58
+ if (typeof effective.refusedMode === 'string' && effective.refusedMode) {
59
+ // OSK-12316: a mode the harness could only honour by WIDENING it is refused
60
+ // at dispatch (OSK-12154); it does not "run as" anything.
61
+ const required = typeof effective.requiredMode === 'string' && effective.requiredMode
62
+ ? effective.requiredMode
63
+ : null;
64
+ lines.push(`Permission: ${effective.refusedMode} (from ${from}) — turns are REFUSED: the ${harness} harness cannot run it without allowing more`);
65
+ if (required) {
66
+ lines.push(`${pad}Set ${required} to run (skrr agents update <agent> --default-permission-mode ${required}), or pass --permission-mode for a turn.`);
67
+ }
68
+ }
69
+ else if (typeof effective.mode === 'string' && effective.mode) {
59
70
  lines.push(`Permission: ${effective.mode} (from ${from}) — enforced by the ${harness} harness`);
60
71
  }
61
72
  else {
@@ -8,6 +8,8 @@ import { BaseCommand } from '../../base-command';
8
8
  * sends, and testing for them alone is what made the success branch dead code.
9
9
  */
10
10
  export declare function isPingSuccess(status: string | undefined): boolean;
11
+ /** The server declared a completed probe failure, not a still-pending poll. */
12
+ export declare function describePingFailure(status: string, error?: string | null): string;
11
13
  export default class HarnessesPing extends BaseCommand {
12
14
  static description: string;
13
15
  static examples: string[];
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isPingSuccess = isPingSuccess;
4
+ exports.describePingFailure = describePingFailure;
4
5
  const core_1 = require("@oclif/core");
5
6
  const base_command_1 = require("../../base-command");
6
7
  const harnesses_1 = require("../../lib/harnesses");
@@ -31,6 +32,14 @@ const POLL_INTERVAL_MS = 300;
31
32
  function isPingSuccess(status) {
32
33
  return status === 'completed' || status === 'ok' || status === 'success';
33
34
  }
35
+ /** The server declared a completed probe failure, not a still-pending poll. */
36
+ function describePingFailure(status, error) {
37
+ const detail = typeof error === 'string' && error.trim() ? `: ${error.trim()}` : '.';
38
+ if (status === 'timeout') {
39
+ return `Ping timed out${detail} The daemon is registered but may be offline or not responding.`;
40
+ }
41
+ return `Ping ${status}${detail} The daemon is registered but may be offline or not responding.`;
42
+ }
34
43
  class HarnessesPing extends base_command_1.BaseCommand {
35
44
  static description = 'Ping a harness and wait for its daemon to answer (local only)';
36
45
  static examples = [
@@ -90,7 +99,7 @@ class HarnessesPing extends base_command_1.BaseCommand {
90
99
  await this.logReadiness(args.harness);
91
100
  }
92
101
  else {
93
- this.log(`Ping ${result.status}${result.error ? `: ${result.error}` : ''}.`);
102
+ this.error(describePingFailure(result.status, result.error), { exit: 1 });
94
103
  }
95
104
  return;
96
105
  }
@@ -234,6 +234,11 @@ class Login extends base_command_1.BaseCommand {
234
234
  if (handoff.status === 'delivered') {
235
235
  this.log(`The local daemon was signed in too — ${handoff.detail}.`);
236
236
  }
237
+ else if (handoff.reason === 'platform-managed') {
238
+ // A Dedicated guest: its daemon is the platform's, so there is
239
+ // nothing to install and nothing to hand over (OSK-12296).
240
+ this.log("This machine's daemon is managed by the platform; there is nothing to set up here.");
241
+ }
237
242
  else if (handoff.reason === 'foreign-root') {
238
243
  // Not a failure and not a hint to re-login anything: the daemon is
239
244
  // the default root's, and this is a second root (OSK-12189).
@@ -391,7 +396,7 @@ class Login extends base_command_1.BaseCommand {
391
396
  refreshExpiresAt: result.refreshExpiresAt
392
397
  ? new Date(result.refreshExpiresAt).getTime()
393
398
  : undefined,
394
- }, { serverOrigin: this.cliConfig.baseURL, clearReauth: true });
399
+ }, { serverOrigin: this.cliConfig.baseURL, clearReauth: true, provenance: 'user_login' });
395
400
  this.log('');
396
401
  this.log(`Signed in as ${result.user.email} via recovery code on ` +
397
402
  `${(0, config_1.describeDeployment)(this.cliConfig.baseURL)}.`);
@@ -5,52 +5,6 @@ export declare function shouldSkipLocalLogout(input: {
5
5
  hasUnreadableEncryptedToken: boolean;
6
6
  allDevices: boolean;
7
7
  }): boolean;
8
- /**
9
- * `skrr logout` / `skrr logout --all-devices`
10
- *
11
- * Default mode (no flags): revokes the current CLI refresh session on the
12
- * server (best-effort) and clears every local backend (Keychain on macOS,
13
- * cli-auth.json fallback, and any legacy token fields in cli-config.json).
14
- * `baseURL` and `cliId` are preserved so the next `skrr login` defaults to
15
- * the same host and reuses the per-machine identity (which becomes a new
16
- * refresh family — server-side revocation invalidated the prior family).
17
- *
18
- * `--all-devices` (L7 fleet-grade logout): in addition to the local
19
- * cleanup, iterate the user's registered daemons and revoke each one
20
- * server-side. This is the threat-model-correct response for a stolen-
21
- * laptop scenario where the user wants every persistent daemon credential
22
- * gone immediately, regardless of which machine they're calling from.
23
- *
24
- * Architectural note: the L7 server-side has a bulk endpoint
25
- * (`POST /api/auth/sessions/revoke-all`) but it requires a user-scope JWT.
26
- * CLI sessions are minted with `scope: 'cli'` (interactive single-machine
27
- * use), so they cannot directly hit the bulk endpoint — that's by design,
28
- * preventing a leaked CLI credential from kicking the user out of every
29
- * other device. We instead iterate `DELETE /api/daemon-resources/:id`
30
- * which CLI scope CAN hit (per-resource ownership check, not scope check).
31
- * N round trips for N daemons; for typical fleet sizes (< 20) this is
32
- * sub-second. Each per-daemon DELETE produces its own audit row keyed by
33
- * `'user_initiated_per_device'`, which preserves forensic granularity.
34
- *
35
- * The CLI's own refresh session is NOT a Daemon resource (it's a
36
- * `scope='cli'` refresh-session row with no Daemon FK), so it's invisible
37
- * to `/api/daemon-resources`. We revoke it separately via
38
- * `revokeCurrentRefreshToken()`.
39
- *
40
- * Ordering rationale for `--all-devices`:
41
- * 1. `runAllDevices` is attempted first (remote sweep).
42
- * 2. If it throws (network error, API unreachable), the error is captured
43
- * and a warning is emitted — but execution CONTINUES to step 3.
44
- * Rationale: a user who runs `logout --all-devices` after a security
45
- * incident must not be left with live tokens on disk just because the
46
- * API was momentarily unreachable. Local cleanup is always the safer
47
- * default; re-running --all-devices later from a connected machine is
48
- * idempotent and cleans up whatever the first attempt missed.
49
- * 3. If the user explicitly declines the confirmation prompt inside
50
- * `runAllDevices`, that path calls `this.exit(1)` which invokes
51
- * `process.exit` — the try/catch is never reached, so local cleanup
52
- * is correctly skipped (user said "abort").
53
- */
54
8
  export default class Logout extends BaseCommand {
55
9
  static description: string;
56
10
  static flags: {
@@ -40,6 +40,7 @@ const config_1 = require("../lib/config");
40
40
  const first_party_harness_managed_1 = require("../lib/first-party-harness-managed");
41
41
  const auth_storage_1 = require("../lib/auth-storage");
42
42
  const daemonBroker_1 = require("../lib/daemonBroker");
43
+ const dedicated_service_1 = require("../lib/dedicated-service");
43
44
  const refresh_1 = require("../lib/refresh");
44
45
  const api_fetch_1 = require("../lib/api-fetch");
45
46
  const cred_envelope_1 = require("../lib/cred-envelope");
@@ -97,6 +98,18 @@ function shouldSkipLocalLogout(input) {
97
98
  * `process.exit` — the try/catch is never reached, so local cleanup
98
99
  * is correctly skipped (user said "abort").
99
100
  */
101
+ /**
102
+ * True only on a Dedicated Runtime guest whose platform hand-off can still
103
+ * serve this CLI. A laptop daemon's island file ALSO advertises
104
+ * `access_token`, so the descriptor alone is not evidence of a platform-managed
105
+ * machine: on a laptop, gating on it alone told a person who had just signed
106
+ * out that "skrr keeps working here through the platform's managed hand-off"
107
+ * (OSK-12467). The guest markers are what make that sentence true.
108
+ */
109
+ function platformManagedHandoff() {
110
+ return (0, dedicated_service_1.runningOnDedicatedRuntimeGuest)() && Boolean((0, daemonBroker_1.findBrokeredHandoffDescriptor)());
111
+ }
112
+ const SIGN_IN_AGAIN = 'To sign in again, run `skrr login`.';
100
113
  class Logout extends base_command_1.BaseCommand {
101
114
  static description = 'Sign out and clear locally-stored tokens. ' +
102
115
  '--all-devices also revokes every registered daemon for this account.';
@@ -134,12 +147,12 @@ class Logout extends base_command_1.BaseCommand {
134
147
  // `access_token`) there IS no local credential to clear — the CLI holds
135
148
  // a per-process token redeemed over loopback and nothing is persisted.
136
149
  // Say that honestly rather than reporting a logout that did not happen.
137
- if ((0, daemonBroker_1.findBrokeredHandoffDescriptor)()) {
150
+ if (platformManagedHandoff()) {
138
151
  this.log("This machine is signed in through the platform's managed hand-off; " +
139
152
  'there is no local credential to clear.');
140
153
  }
141
154
  else {
142
- this.log('Not signed in. Nothing to do.');
155
+ this.log(`Not signed in. Nothing to do. ${SIGN_IN_AGAIN}`);
143
156
  }
144
157
  return;
145
158
  }
@@ -257,6 +270,19 @@ class Logout extends base_command_1.BaseCommand {
257
270
  : 'Signed out from all devices.'
258
271
  : 'Signed out.';
259
272
  this.log(finalMessage);
273
+ // On a brokered Dedicated guest the sign-in just removed was the person's
274
+ // OWN (an explicit `skrr login`); the machine's platform-managed access is
275
+ // untouched and skrr keeps working through it. Say both, so "Signed out."
276
+ // is not read as "this machine can no longer reach skrr" (OSK-12043).
277
+ // Anywhere else there is no such hand-off, so say how to sign back in
278
+ // instead (OSK-12467).
279
+ if (platformManagedHandoff()) {
280
+ this.log('Removed your own sign-in from this machine. ' +
281
+ "skrr keeps working here through the platform's managed hand-off.");
282
+ }
283
+ else {
284
+ this.log(SIGN_IN_AGAIN);
285
+ }
260
286
  if (sweptLegacy.length > 0) {
261
287
  this.log(`Also removed ${sweptLegacy.length} credential${sweptLegacy.length === 1 ? '' : 's'} left by the previous product identity:`);
262
288
  for (const service of sweptLegacy)
@@ -273,7 +273,7 @@ function persistPairCredentials(plaintext, serverOrigin) {
273
273
  (0, auth_storage_1.writeToBackend)({
274
274
  token: plaintext.token,
275
275
  ...(plaintext.refreshToken ? { refreshToken: plaintext.refreshToken } : {}),
276
- }, { serverOrigin, clearReauth: true });
276
+ }, { serverOrigin, clearReauth: true, provenance: 'user_login' });
277
277
  if (plaintext.dataEncryptionKey) {
278
278
  // The unified `writeToBackend` API only carries token+refresh
279
279
  // today. For now, persist the DEK as a sibling field on the next
@@ -93,7 +93,35 @@ export type AgenticStreamOptions = {
93
93
  idleTerminalProbeMs?: number;
94
94
  /** How long to keep trying to attach before reporting a detach. Default 30s. */
95
95
  attachTimeoutMs?: number;
96
+ /**
97
+ * How long a prompt listens to (and throws away) input before it is shown.
98
+ * Default {@link TYPE_AHEAD_DISCARD_MS}. See {@link discardTypeAhead}.
99
+ */
100
+ typeAheadDiscardMs?: number;
96
101
  };
102
+ /**
103
+ * How long input arriving just before a prompt is shown is thrown away.
104
+ *
105
+ * Long enough for bytes already typed into the terminal to be read out of the
106
+ * kernel once stdin starts flowing (one event-loop turn in practice), short
107
+ * enough that nobody waits for it.
108
+ */
109
+ export declare const TYPE_AHEAD_DISCARD_MS = 60;
110
+ /**
111
+ * Throw away whatever was typed BEFORE a prompt is shown (OSK-12468).
112
+ *
113
+ * A key pressed while the turn was still streaming sits in the stream's buffer
114
+ * or the terminal's, and the first `readline.question` read it as the answer:
115
+ * an owner typed `y` before any `[y/N]` was visible and approved a Bash command
116
+ * they had never seen. An approval is only consent to what was on the screen,
117
+ * so every prompt now starts from an empty input: bytes the stream already
118
+ * holds are read and dropped, then stdin flows into a discarding listener for
119
+ * `windowMs` so the terminal's own buffer is drained too. Raw mode is switched
120
+ * on for that window because a half-typed line (`y`, no Enter) is invisible to
121
+ * a read in canonical mode and would otherwise surface as soon as readline
122
+ * enters raw mode itself. A Ctrl-C in the discarded input is still honoured.
123
+ */
124
+ export declare function discardTypeAhead(input: NodeJS.ReadableStream, windowMs?: number): Promise<void>;
97
125
  /**
98
126
  * What a permission request would DO, in one line a person can judge.
99
127
  *
@@ -371,6 +399,12 @@ export declare class AgenticStreamClient {
371
399
  * (OSK-12197) instead of leaving `[y/N]` open after the run stopped asking.
372
400
  */
373
401
  private readonly openPermissionPrompts;
402
+ /**
403
+ * Question prompts on the terminal still waiting for an answer, by
404
+ * questionId, so a server `question:cancel` can close the one it names
405
+ * (OSK-12208) the same way `permission:cancel` closes a permission prompt.
406
+ */
407
+ private readonly openQuestionPrompts;
374
408
  /** Text already written to the terminal, for suffix-only rendering. */
375
409
  private renderedText;
376
410
  private idleTimer;
@@ -455,7 +489,27 @@ export declare class AgenticStreamClient {
455
489
  * and close the terminal prompt if this client is still showing it.
456
490
  */
457
491
  private handlePermissionCancel;
492
+ /**
493
+ * The server closed a question (OSK-12208): nobody answered it in time, or
494
+ * the turn ended first. Say so, and close the terminal prompt if this
495
+ * client is still showing it — the question twin of handlePermissionCancel.
496
+ */
497
+ private handleQuestionCancel;
498
+ /** Prompts run one at a time: two readlines on one stdin would both read a line. */
499
+ private askQueue;
458
500
  private ask;
501
+ /**
502
+ * One prompt, on a readline that exists only while the prompt is on screen.
503
+ *
504
+ * The interface used to be created at the first prompt and kept until the
505
+ * run closed, and it was created AFTER stdin had been accumulating whatever
506
+ * the person typed while the turn streamed — so the first question answered
507
+ * itself from that type-ahead (OSK-12468). Now the input is emptied first
508
+ * ({@link discardTypeAhead}), only then is the question shown, and the
509
+ * interface is closed with the answer, so nothing typed between two prompts
510
+ * can answer the second one either.
511
+ */
512
+ private askNow;
459
513
  /**
460
514
  * True the first time this (kind, identity) pair is seen; false afterwards.
461
515
  * The identity is the event's OWN — a tool-use id, a message id — never a