@skrr-ai/cli 0.1.113 → 0.1.115

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 (63) hide show
  1. package/bin/update-npm-runner.cjs +4 -1
  2. package/bin/update-worker.cjs +6 -3
  3. package/dist/commands/agents/chat.js +10 -1
  4. package/dist/commands/computer-use/audio.d.ts +6 -0
  5. package/dist/commands/computer-use/audio.js +9 -0
  6. package/dist/commands/computer-use/browser.d.ts +7 -0
  7. package/dist/commands/computer-use/browser.js +16 -0
  8. package/dist/commands/computer-use/disable.d.ts +6 -0
  9. package/dist/commands/computer-use/disable.js +9 -0
  10. package/dist/commands/computer-use/enable.d.ts +6 -0
  11. package/dist/commands/computer-use/enable.js +9 -0
  12. package/dist/commands/computer-use/index.d.ts +6 -0
  13. package/dist/commands/computer-use/index.js +9 -0
  14. package/dist/commands/computer-use/install.d.ts +6 -0
  15. package/dist/commands/computer-use/install.js +9 -0
  16. package/dist/commands/computer-use/recover.d.ts +6 -0
  17. package/dist/commands/computer-use/recover.js +9 -0
  18. package/dist/commands/computer-use/status.d.ts +6 -0
  19. package/dist/commands/computer-use/status.js +9 -0
  20. package/dist/commands/computer-use/stop.d.ts +6 -0
  21. package/dist/commands/computer-use/stop.js +9 -0
  22. package/dist/commands/daemon/login.js +1 -1
  23. package/dist/commands/followups/plans/cancel.js +10 -3
  24. package/dist/commands/followups/plans/capacity.d.ts +9 -0
  25. package/dist/commands/followups/plans/capacity.js +42 -0
  26. package/dist/commands/followups/plans/create.js +3 -0
  27. package/dist/commands/followups/plans/revise.js +13 -6
  28. package/dist/commands/followups/remind.js +13 -5
  29. package/dist/commands/followups/watch.js +2 -2
  30. package/dist/lib/agentic-stream.d.ts +5 -1
  31. package/dist/lib/agentic-stream.js +25 -9
  32. package/dist/lib/computer-use.d.ts +9 -0
  33. package/dist/lib/computer-use.js +41 -0
  34. package/dist/lib/lookbackPlans.d.ts +20 -6
  35. package/dist/lib/lookbackPlans.js +19 -5
  36. package/dist/lib/update-check.d.ts +11 -10
  37. package/dist/lib/update-check.js +41 -23
  38. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerUse.d.ts +228 -0
  39. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerUse.js +375 -0
  40. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonScopes.d.ts +4 -1
  41. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonScopes.js +6 -2
  42. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonWire.js +40 -0
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/desktopWire.d.ts +140 -0
  44. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/desktopWire.js +46 -0
  45. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +4 -4
  46. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +2 -2
  47. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
  48. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +8 -5
  49. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerUse.d.ts +228 -0
  50. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerUse.js +358 -0
  51. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonScopes.d.ts +4 -1
  52. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonScopes.js +5 -1
  53. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonWire.js +40 -0
  54. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/desktopWire.d.ts +140 -0
  55. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/desktopWire.js +42 -0
  56. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +4 -4
  57. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +2 -2
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +3 -1
  60. package/dist/node_modules/@skrr-ai/auth-core/package.json +21 -1
  61. package/dist/node_modules/@skrr-ai/data-provider/index.js +23687 -23456
  62. package/oclif.manifest.json +306 -26
  63. package/package.json +2 -2
@@ -740,6 +740,7 @@ class AgenticStreamClient {
740
740
  textJoiner = (0, data_provider_1.createAssistantTextJoiner)();
741
741
  /** Highest envelope `__seq` folded into `accumulated`, for gap detection. */
742
742
  highestSeq = 0;
743
+ publicProjection;
743
744
  pendingEnvelopes = new Map();
744
745
  replayTimer = null;
745
746
  replayFlight = null;
@@ -972,6 +973,7 @@ class AgenticStreamClient {
972
973
  reason,
973
974
  ...(terminal.error ? { message: terminal.error, error: terminal.error } : {}),
974
975
  ...(typeof terminal.text === 'string' ? { result: terminal.text } : {}),
976
+ ...(terminal.publicProjection ? { publicProjection: terminal.publicProjection } : {}),
975
977
  });
976
978
  return true;
977
979
  }
@@ -1129,6 +1131,7 @@ class AgenticStreamClient {
1129
1131
  type: 'execution:complete',
1130
1132
  sessionId: this.options.sessionId,
1131
1133
  result: reading.text,
1134
+ ...(reading.publicProjection ? { publicProjection: reading.publicProjection } : {}),
1132
1135
  repaired: true,
1133
1136
  repairedFrom: 'durable_record',
1134
1137
  overturned: { code: exports.WORKER_REPLACED_CODE, reason: exports.DAEMON_HANDOVER_REASON },
@@ -1146,6 +1149,7 @@ class AgenticStreamClient {
1146
1149
  message: reading.error,
1147
1150
  errors: [reading.error],
1148
1151
  ...(typeof reading.text === 'string' && reading.text ? { result: reading.text } : {}),
1152
+ ...(reading.publicProjection ? { publicProjection: reading.publicProjection } : {}),
1149
1153
  handover: { outcome: 'machine_failed', waitedMs },
1150
1154
  });
1151
1155
  return true;
@@ -1454,6 +1458,9 @@ class AgenticStreamClient {
1454
1458
  if (fresh.length === 0)
1455
1459
  return; // a pure re-delivery — drop it whole
1456
1460
  const scoped = { ...event, envelopes: fresh };
1461
+ if (fresh.some((env) => env?.ev?.t === 'text' &&
1462
+ env.ev?.publicProjection === 'lookback_answer_v1'))
1463
+ this.publicProjection = 'lookback_answer_v1';
1457
1464
  // Note what never arrived before folding this batch in (OSK-7631,
1458
1465
  // OSK-13518). Checked against the FRESH set: a re-delivery carries old
1459
1466
  // seqs and would read as a gap that never happened.
@@ -1539,6 +1546,8 @@ class AgenticStreamClient {
1539
1546
  socket.on('message:stream', (event) => {
1540
1547
  if (!match(event))
1541
1548
  return;
1549
+ if (event.publicProjection === 'lookback_answer_v1')
1550
+ this.publicProjection = event.publicProjection;
1542
1551
  const text = typeof event.text === 'string' ? event.text : '';
1543
1552
  const delta = typeof event.delta === 'string' ? event.delta : '';
1544
1553
  if (text && text.length >= this.accumulated.length)
@@ -1574,6 +1583,7 @@ class AgenticStreamClient {
1574
1583
  if (!match(event))
1575
1584
  return;
1576
1585
  this.accumulated = '';
1586
+ this.publicProjection = undefined;
1577
1587
  this.textJoiner.reset();
1578
1588
  this.renderedText = '';
1579
1589
  // Seqs restart with the stream, so a fresh low seq is not a gap.
@@ -2099,7 +2109,7 @@ class AgenticStreamClient {
2099
2109
  const streamedText = (0, widgetTerminalText_1.widgetsAsText)(this.accumulated, { final: true });
2100
2110
  void this.readRecordedText().then((recorded) => {
2101
2111
  this.settleGapVerdict(streamedText, recorded);
2102
- this.settle(status, event, recorded?.text || undefined);
2112
+ this.settle(status, event, recorded);
2103
2113
  });
2104
2114
  return;
2105
2115
  }
@@ -2124,10 +2134,10 @@ class AgenticStreamClient {
2124
2134
  try {
2125
2135
  const terminal = await reconcile();
2126
2136
  if (terminal && typeof terminal.text === 'string' && terminal.text) {
2127
- return { text: terminal.text };
2137
+ return { text: terminal.text, ...(terminal.publicProjection ? { publicProjection: terminal.publicProjection } : {}) };
2128
2138
  }
2129
2139
  if (terminal)
2130
- empty = { text: '' };
2140
+ empty = { text: '', ...(terminal.publicProjection ? { publicProjection: terminal.publicProjection } : {}) };
2131
2141
  }
2132
2142
  catch {
2133
2143
  // No evidence either way; try again, then settle on the stream.
@@ -2135,7 +2145,7 @@ class AgenticStreamClient {
2135
2145
  }
2136
2146
  return empty;
2137
2147
  }
2138
- settle(status, event, recordedText) {
2148
+ settle(status, event, recorded) {
2139
2149
  // `execution:complete` carries the final answer as `result`, and it is
2140
2150
  // AUTHORITATIVE — preferred over whatever the deltas added up to, not just
2141
2151
  // used when they added up to nothing.
@@ -2146,6 +2156,8 @@ class AgenticStreamClient {
2146
2156
  // `result` whenever the server sends one makes the returned text the
2147
2157
  // agent's actual final message rather than however much of it arrived in
2148
2158
  // time.
2159
+ if (event.publicProjection === 'lookback_answer_v1')
2160
+ this.publicProjection = event.publicProjection;
2149
2161
  const finalText = typeof event.result === 'string' ? event.result : '';
2150
2162
  // …EXCEPT when it is a strict suffix of what we accumulated.
2151
2163
  //
@@ -2168,7 +2180,10 @@ class AgenticStreamClient {
2168
2180
  this.accumulated.length > finalText.length &&
2169
2181
  (this.accumulated.endsWith(finalText) ||
2170
2182
  endsWithIgnoringWhitespace(this.accumulated, finalText));
2171
- if (finalText && finalText !== this.accumulated && !accumulationIsSuperset) {
2183
+ if (event.publicProjection === 'lookback_answer_v1' && typeof event.result === 'string') {
2184
+ this.accumulated = event.result;
2185
+ }
2186
+ else if (finalText && finalText !== this.accumulated && !accumulationIsSuperset) {
2172
2187
  this.accumulated = finalText;
2173
2188
  }
2174
2189
  // The record wins over a stream known to have lost chunks, unless what
@@ -2176,9 +2191,10 @@ class AgenticStreamClient {
2176
2191
  // other way round).
2177
2192
  // (Compared as the reader sees it: the record holds a widget as its fallback sentence,
2178
2193
  // which is shorter than the block the stream carried, and the raw length would never lose.)
2179
- if (recordedText &&
2180
- recordedText.length > (0, widgetTerminalText_1.widgetsAsText)(this.accumulated, { final: true }).length) {
2181
- this.accumulated = recordedText;
2194
+ if (recorded && (recorded.publicProjection === 'lookback_answer_v1' ||
2195
+ recorded.text.length > (0, widgetTerminalText_1.widgetsAsText)(this.accumulated, { final: true }).length)) {
2196
+ this.accumulated = recorded.text;
2197
+ this.publicProjection = recorded.publicProjection;
2182
2198
  }
2183
2199
  // The finished answer, as every other surface shows it: the web applies
2184
2200
  // this same sanitizer, so a scheduling JSON block or leaked function-call
@@ -2186,7 +2202,7 @@ class AgenticStreamClient {
2186
2202
  // printed as part of the agent's answer.
2187
2203
  // A widget is its fallback sentence in a terminal, first: the sanitizer must read
2188
2204
  // prose and never a widget's script (the server projects before it sanitizes, too).
2189
- this.accumulated = (0, data_provider_1.sanitizeFinalText)((0, widgetTerminalText_1.widgetsAsText)(this.accumulated, { final: true }));
2205
+ this.accumulated = (0, data_provider_1.sanitizeFinalText)((0, widgetTerminalText_1.widgetsAsText)(this.accumulated, { final: true }), this.publicProjection);
2190
2206
  if (!this.options.quiet && !this.options.jsonl && this.accumulated) {
2191
2207
  const output = this.options.output ?? process.stdout;
2192
2208
  const { append, shown } = renderableDelta(this.renderedText, this.accumulated);
@@ -0,0 +1,9 @@
1
+ import { Command } from '@oclif/core';
2
+ export declare function buildComputerUseArgs(action: string | undefined, argv: readonly string[]): string[];
3
+ /** The runtime owns local consent, installation and lifecycle; the REST CLI only dispatches. */
4
+ export declare function forwardComputerUseCommand(command: Command, action: string | undefined, argv: readonly string[]): Promise<void>;
5
+ export declare abstract class ComputerUseCommand extends Command {
6
+ static strict: boolean;
7
+ protected abstract action: string | undefined;
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ComputerUseCommand = void 0;
4
+ exports.buildComputerUseArgs = buildComputerUseArgs;
5
+ exports.forwardComputerUseCommand = forwardComputerUseCommand;
6
+ const core_1 = require("@oclif/core");
7
+ const exec_runtime_binary_1 = require("./exec-runtime-binary");
8
+ function buildComputerUseArgs(action, argv) {
9
+ return ['computer-use', ...(action ? [action] : []), ...argv];
10
+ }
11
+ /** The runtime owns local consent, installation and lifecycle; the REST CLI only dispatches. */
12
+ async function forwardComputerUseCommand(command, action, argv) {
13
+ const fail = (message, code, exit) => {
14
+ if (argv.includes('--json')) {
15
+ command.log(JSON.stringify({ object: 'computer_use_control', schemaVersion: 1, action: action ?? argv[0] ?? 'help', ok: false, error: { code, message } }));
16
+ return command.exit(exit);
17
+ }
18
+ command.log(message);
19
+ return command.exit(exit);
20
+ };
21
+ try {
22
+ if ((await (0, exec_runtime_binary_1.runtimeBinarySupportsSubcommand)('computer-use')) === false) {
23
+ return fail('This local daemon does not support Computer Use. Update skrrd, then retry.', 'runtime_unsupported', 2);
24
+ }
25
+ command.exit(await (0, exec_runtime_binary_1.execRuntimeBinary)(buildComputerUseArgs(action, argv)));
26
+ }
27
+ catch (error) {
28
+ if (error.message === 'runtime-binary-not-installed') {
29
+ return fail((0, exec_runtime_binary_1.notInstalledMessage)(), 'runtime_not_installed', 127);
30
+ }
31
+ throw error;
32
+ }
33
+ }
34
+ class ComputerUseCommand extends core_1.Command {
35
+ static strict = false;
36
+ async run() {
37
+ // Never call parse(): unknown runtime flags must reach the authoritative parser unchanged.
38
+ await forwardComputerUseCommand(this, this.action, this.argv);
39
+ }
40
+ }
41
+ exports.ComputerUseCommand = ComputerUseCommand;
@@ -1,13 +1,27 @@
1
- import { lookbackPlansApi, type CreateLookbackPlanRequest, type LookbackPlan, type LookbackPlanInput } from '@skrr-ai/data-provider';
2
- export { lookbackPlansApi };
1
+ import { lookbackPlansApi, isLookbackMutationOutcomeUnknown, type CreateLookbackPlanRequest, type LookbackPlan, type LookbackPlanInput } from '@skrr-ai/data-provider';
2
+ export { lookbackPlansApi, isLookbackMutationOutcomeUnknown };
3
3
  export declare function lookbackInputFailure(error: unknown): {
4
+ code: "LOOKBACK_INVALID_FIELD" | "LOOKBACK_INVALID_TIME" | "LOOKBACK_INVALID_TIMEZONE" | "LOOKBACK_AMBIGUOUS_TIME" | "LOOKBACK_NONEXISTENT_TIME" | "LOOKBACK_LIMIT_EXCEEDED";
5
+ message: string;
6
+ details: {
7
+ field?: string | undefined;
8
+ stage: string;
9
+ retryCondition: string;
10
+ remedy: string;
11
+ };
4
12
  exit: number;
5
13
  retryable: boolean;
6
- details?: {
7
- field: string;
8
- } | undefined;
9
- code: "LOOKBACK_INVALID_FIELD" | "LOOKBACK_INVALID_TIME" | "LOOKBACK_INVALID_TIMEZONE" | "LOOKBACK_AMBIGUOUS_TIME" | "LOOKBACK_NONEXISTENT_TIME" | "LOOKBACK_LIMIT_EXCEEDED";
14
+ };
15
+ export declare function lookbackUnconfirmedFailure(details: Record<string, unknown>): {
16
+ code: string;
10
17
  message: string;
18
+ exit: number;
19
+ retryable: boolean;
20
+ details: {
21
+ stage: string;
22
+ retryCondition: string;
23
+ remedy: string;
24
+ };
11
25
  };
12
26
  export declare const lookbackPlanIdArg: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
13
27
  /** JSON can be the plan itself or the canonical create envelope. Never copy identity fields. */
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.lookbackPlanIdArg = exports.lookbackPlansApi = void 0;
3
+ exports.lookbackPlanIdArg = exports.isLookbackMutationOutcomeUnknown = exports.lookbackPlansApi = void 0;
4
4
  exports.lookbackInputFailure = lookbackInputFailure;
5
+ exports.lookbackUnconfirmedFailure = lookbackUnconfirmedFailure;
5
6
  exports.lookbackCreateBody = lookbackCreateBody;
6
7
  exports.lookbackInput = lookbackInput;
7
8
  exports.lookbackPlanSummary = lookbackPlanSummary;
@@ -9,6 +10,7 @@ exports.lookbackArmingLine = lookbackArmingLine;
9
10
  const core_1 = require("@oclif/core");
10
11
  const data_provider_1 = require("@skrr-ai/data-provider");
11
12
  Object.defineProperty(exports, "lookbackPlansApi", { enumerable: true, get: function () { return data_provider_1.lookbackPlansApi; } });
13
+ Object.defineProperty(exports, "isLookbackMutationOutcomeUnknown", { enumerable: true, get: function () { return data_provider_1.isLookbackMutationOutcomeUnknown; } });
12
14
  const followups_1 = require("./followups");
13
15
  class LookbackValidationError extends Error {
14
16
  issue;
@@ -18,12 +20,23 @@ class LookbackValidationError extends Error {
18
20
  }
19
21
  }
20
22
  function lookbackInputFailure(error) {
23
+ const repair = { stage: 'validation', retryCondition: 'correct_input',
24
+ remedy: 'Correct the indicated input field before submitting again.',
25
+ ...(error instanceof LookbackValidationError ? { field: error.issue.field } : {}),
26
+ };
21
27
  return {
22
28
  code: error instanceof LookbackValidationError ? error.issue.code : 'LOOKBACK_INVALID_FIELD',
23
29
  message: error instanceof Error ? error.message : String(error),
24
- ...(error instanceof LookbackValidationError ? { details: { field: error.issue.field } } : {}),
25
- exit: 2,
26
- retryable: false,
30
+ // BaseCommand's machine envelope carries additive domain facts in details.
31
+ details: repair, exit: 2, retryable: false,
32
+ };
33
+ }
34
+ function lookbackUnconfirmedFailure(details) {
35
+ return {
36
+ code: 'LOOKBACK_OUTCOME_UNCONFIRMED', message: 'The plan update outcome could not be confirmed. Read the original source or known plan before submitting another change.',
37
+ exit: 5, retryable: false,
38
+ details: { stage: 'lifecycle', retryCondition: 'read_original_source_or_known_plan',
39
+ remedy: 'Read the known plan or original source. Do not submit a replacement source or blindly repeat this mutation.', ...details },
27
40
  };
28
41
  }
29
42
  exports.lookbackPlanIdArg = core_1.Args.string({
@@ -75,7 +88,8 @@ function lookbackInput(raw, allowPast = false) {
75
88
  return raw;
76
89
  }
77
90
  function lookbackPlanSummary(plan) {
78
- const next = plan.steps.find((step) => step.state === 'pending');
91
+ const next = plan.steps.filter((step) => step.state === 'pending')
92
+ .reduce((earliest, step) => !earliest || Date.parse(step.runAt) < Date.parse(earliest.runAt) ? step : earliest, undefined);
79
93
  return `${plan.id} [${plan.state}] r${plan.revision} ${plan.title || plan.objective}` +
80
94
  ` ${plan.steps.length} lookbacks${next ? ` next:${next.runAt}` : ''}`;
81
95
  }
@@ -1,4 +1,9 @@
1
1
  export type CliInstallKind = 'npm-global' | 'npm-local' | 'dev' | 'bundled' | 'image' | 'homebrew' | 'other';
2
+ export interface CliInstallDescriptor {
3
+ kind: CliInstallKind;
4
+ writable?: boolean;
5
+ npmPrefix?: string;
6
+ }
2
7
  export interface CliUpdateAttempt {
3
8
  status: 'queued' | 'running' | 'succeeded' | 'failed';
4
9
  version: string;
@@ -10,6 +15,7 @@ export interface CliUpdateAttempt {
10
15
  message?: string;
11
16
  packageRoot?: string;
12
17
  npmPath?: string;
18
+ npmPrefix?: string;
13
19
  coordinationVersion?: number;
14
20
  workerPid?: number;
15
21
  interrupted?: boolean;
@@ -19,10 +25,7 @@ export interface CheckCache {
19
25
  latest: string | null;
20
26
  npmPackage: string | null;
21
27
  baseURL: string;
22
- install?: {
23
- kind: CliInstallKind;
24
- writable?: boolean;
25
- };
28
+ install?: CliInstallDescriptor;
26
29
  packageRoot?: string;
27
30
  npmGlobalRoot?: string | null;
28
31
  autoUpdate?: {
@@ -45,6 +48,7 @@ export interface CliUpdateStatus {
45
48
  installedVersion: string | null;
46
49
  offeredVersion: string | null;
47
50
  owner: CliInstallKind;
51
+ npmPrefix?: string;
48
52
  state: 'up-to-date' | 'update-available' | 'pending' | 'restart-pending' | 'failed' | 'managed' | 'unknown' | 'check-failed';
49
53
  autoUpdateAllowed: boolean;
50
54
  autoUpdateReason: string | null;
@@ -76,12 +80,9 @@ export declare function resolveCliPackageDir(argv?: readonly string[]): string |
76
80
  * dir beside `dist` exists only in a checkout — published packages ship
77
81
  * `dist` and no `src`.
78
82
  */
79
- export declare function classifyCliInstall(env: NodeJS.ProcessEnv, argv: readonly string[], npmGlobalRoot: string | null, probeWritable?: boolean): {
80
- kind: CliInstallKind;
81
- writable?: boolean;
82
- };
83
- export declare function cliUpdateCommand(owner: CliInstallKind): string;
84
- export declare function buildNudgeLine(currentVersion: string, latest: string | null, _npmPackage: string | null, installKind?: CliInstallKind): string | null;
83
+ export declare function classifyCliInstall(env: NodeJS.ProcessEnv, argv: readonly string[], npmGlobalRoot: string | null, probeWritable?: boolean): CliInstallDescriptor;
84
+ export declare function cliUpdateCommand(owner: CliInstallKind, npmPrefix?: string): string;
85
+ export declare function buildNudgeLine(currentVersion: string, latest: string | null, _npmPackage: string | null, installKind?: CliInstallKind, npmPrefix?: string): string | null;
85
86
  export type CliUpdateAction = {
86
87
  type: 'nudge';
87
88
  line: string;
@@ -207,6 +207,20 @@ function classifyCliInstall(env, argv, npmGlobalRoot, probeWritable = true) {
207
207
  return { kind: 'other' };
208
208
  }
209
209
  const nodeModulesDir = path.dirname(path.dirname(packageDir)); // …/node_modules
210
+ const npmPrefix = process.platform !== 'win32' && path.basename(path.dirname(nodeModulesDir)) === 'lib'
211
+ ? path.dirname(path.dirname(nodeModulesDir)) : undefined;
212
+ // npm's current default may differ from the prefix that installed this
213
+ // executable. A project directory named `lib` is insufficient evidence:
214
+ // the prefix's public bin must resolve to this exact installed entry.
215
+ let linkedGlobalPrefix = false;
216
+ if (npmPrefix) {
217
+ try {
218
+ linkedGlobalPrefix = !fs.existsSync(path.join(npmPrefix, 'package.json')) &&
219
+ fs.realpathSync(path.join(npmPrefix, 'bin', 'skrr')) ===
220
+ fs.realpathSync(path.join(packageDir, 'bin', 'run.js'));
221
+ }
222
+ catch { /* no executable ownership proof */ }
223
+ }
210
224
  if (npmGlobalRoot) {
211
225
  let resolvedGlobal = npmGlobalRoot;
212
226
  try {
@@ -222,15 +236,13 @@ function classifyCliInstall(env, argv, npmGlobalRoot, probeWritable = true) {
222
236
  catch {
223
237
  /* compare the raw form */
224
238
  }
225
- if (resolvedNodeModules !== resolvedGlobal)
239
+ if (resolvedNodeModules !== resolvedGlobal && !linkedGlobalPrefix)
226
240
  return { kind: 'npm-local' };
227
241
  }
228
- else if (!/[/\\](lib|npm)[/\\]node_modules$/.test(nodeModulesDir)) {
242
+ else if (!linkedGlobalPrefix) {
229
243
  // `npm root -g` gave no answer, so prove the global shape from the path
230
- // instead: npm's unix global root is always `<prefix>/lib/node_modules`
231
- // (incl. nvm/volta) and on Windows `%APPDATA%\npm\node_modules`. A
232
- // project-local `<project>/node_modules` never matches either — without
233
- // proof we must not treat it as ours to auto-update.
244
+ // and matching public executable instead. Without ownership proof we
245
+ // must not treat a project's dependency as ours to auto-update.
234
246
  return { kind: 'npm-local' };
235
247
  }
236
248
  let writable = false;
@@ -245,7 +257,7 @@ function classifyCliInstall(env, argv, npmGlobalRoot, probeWritable = true) {
245
257
  /* read-only */
246
258
  }
247
259
  }
248
- return { kind: 'npm-global', writable };
260
+ return { kind: 'npm-global', writable, ...(npmPrefix ? { npmPrefix } : {}) };
249
261
  }
250
262
  /**
251
263
  * Resolve `npm root -g` for install classification. Runs ONLY inside the
@@ -269,12 +281,14 @@ async function resolveNpmGlobalRoot(env = process.env) {
269
281
  return null;
270
282
  }
271
283
  }
272
- function cliUpdateCommand(owner) {
284
+ function cliUpdateCommand(owner, npmPrefix) {
273
285
  switch (owner) {
274
286
  case 'npm-global':
275
- return `npm install -g ${CLI_PACKAGE}@latest`;
287
+ return npmPrefix
288
+ ? `npm install --global --prefix '${npmPrefix.replace(/'/g, "'\\''")}' ${CLI_PACKAGE}@latest`
289
+ : `npm install -g ${CLI_PACKAGE}@latest`;
276
290
  case 'npm-local':
277
- return `npm update ${CLI_PACKAGE}`;
291
+ return `Use npm in the project that owns ${CLI_PACKAGE}`;
278
292
  case 'dev':
279
293
  return 'npm --prefix cli run build';
280
294
  case 'bundled':
@@ -287,16 +301,18 @@ function cliUpdateCommand(owner) {
287
301
  return 'Update via your install method — see https://skrr.ai';
288
302
  }
289
303
  }
290
- function buildNudgeLine(currentVersion, latest, _npmPackage, installKind = 'npm-global') {
304
+ function buildNudgeLine(currentVersion, latest, _npmPackage, installKind = 'npm-global', npmPrefix) {
291
305
  if (!latest || !isOlderVersion(currentVersion, latest))
292
306
  return null;
293
- const command = installKind === 'npm-global' ? `npm update -g ${CLI_PACKAGE}` : cliUpdateCommand(installKind);
294
- const context = installKind === 'npm-local'
295
- ? ' in the owning project'
296
- : installKind === 'dev'
297
- ? ' to rebuild your checkout'
298
- : '';
299
- return `[skrr] Update available: ${currentVersion} → ${latest}. Run \`${command}\`${context}. Silence with OVERSKY_NO_UPDATE_NUDGE=1.\n`;
307
+ const command = installKind === 'npm-global' && !npmPrefix
308
+ ? `npm update -g ${CLI_PACKAGE}` : cliUpdateCommand(installKind, npmPrefix);
309
+ const context = installKind === 'dev'
310
+ ? ' to rebuild your checkout'
311
+ : '';
312
+ const advice = installKind === 'npm-local'
313
+ ? `Update ${CLI_PACKAGE} using the project that owns ${CLI_PACKAGE}.`
314
+ : `Run \`${command}\`${context}.`;
315
+ return `[skrr] Update available: ${currentVersion} → ${latest}. ${advice} Silence with OVERSKY_NO_UPDATE_NUDGE=1.\n`;
300
316
  }
301
317
  function decideCliUpdateAction(args) {
302
318
  const { currentVersion, cache, env, isTTY } = args;
@@ -327,10 +343,10 @@ function decideCliUpdateAction(args) {
327
343
  if (attempt?.status === 'failed')
328
344
  return {
329
345
  type: 'nudge',
330
- line: `[skrr] CLI update to ${cache.latest} failed. Inspect \`skrr update --status\` or run \`${cliUpdateCommand(install.kind)}\`.\n`,
346
+ line: `[skrr] CLI update to ${cache.latest} failed. Inspect \`skrr update --status\` or run \`${cliUpdateCommand(install.kind, install.npmPrefix)}\`.\n`,
331
347
  };
332
348
  }
333
- const line = buildNudgeLine(currentVersion, cache.latest, CLI_PACKAGE, install.kind);
349
+ const line = buildNudgeLine(currentVersion, cache.latest, CLI_PACKAGE, install.kind, install.npmPrefix);
334
350
  return line ? { type: 'nudge', line } : null;
335
351
  }
336
352
  function readCache(env = process.env) {
@@ -462,10 +478,11 @@ function readCliUpdateStatus(opts) {
462
478
  installedVersion,
463
479
  offeredVersion,
464
480
  owner: install.kind,
481
+ ...(install.npmPrefix ? { npmPrefix: install.npmPrefix } : {}),
465
482
  state,
466
483
  autoUpdateAllowed: autoUpdateReason === null,
467
484
  autoUpdateReason,
468
- command: cliUpdateCommand(install.kind),
485
+ command: cliUpdateCommand(install.kind, install.npmPrefix),
469
486
  lastCheckAt: matchingCache?.lastCheckAt ?? null,
470
487
  lastAttempt,
471
488
  logPath: path.join(directory, 'update.log'),
@@ -591,7 +608,7 @@ async function performCliUpdate(opts) {
591
608
  env.SKRR_CLI_UPDATE_SESSION_UNAVAILABLE === '1') {
592
609
  return {
593
610
  ...status,
594
- message: `This CLI is owned by ${install.kind}${install.kind === 'npm-global' ? ' in a shared or external prefix' : ''}. ${cliUpdateCommand(install.kind)}.`,
611
+ message: `This CLI is owned by ${install.kind}${install.kind === 'npm-global' ? ' in a shared or external prefix' : ''}. ${cliUpdateCommand(install.kind, install.npmPrefix)}.`,
595
612
  };
596
613
  }
597
614
  const recoverableAttempt = status.lastAttempt && ['queued', 'running'].includes(status.lastAttempt.status);
@@ -620,6 +637,7 @@ async function performCliUpdate(opts) {
620
637
  attemptedAt: Date.now(),
621
638
  packageRoot: packageRoot(argv),
622
639
  npmPath,
640
+ ...(install.npmPrefix ? { npmPrefix: install.npmPrefix } : {}),
623
641
  coordinationVersion: api.COORDINATION_VERSION,
624
642
  message: 'Waiting for active CLI sessions to exit; the next invocation uses the new version.',
625
643
  });
@@ -649,7 +667,7 @@ async function maybeWarnOutdatedCli(opts) {
649
667
  currentVersion: opts.currentVersion,
650
668
  cache: {
651
669
  ...cache,
652
- install: { kind: status.owner, writable: status.autoUpdateAllowed },
670
+ install: { kind: status.owner, writable: status.autoUpdateAllowed, npmPrefix: status.npmPrefix },
653
671
  autoUpdate: status.lastAttempt
654
672
  ? {
655
673
  attemptedAt: status.lastAttempt.attemptedAt,