@skrr-ai/cli 0.1.40 → 0.1.42

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 +9 -0
  2. package/dist/base-command.js +18 -3
  3. package/dist/commands/agent-shares/list-for.js +4 -11
  4. package/dist/commands/agent-shares/received.js +4 -11
  5. package/dist/commands/agent-shares/sent.js +4 -11
  6. package/dist/commands/agent-shares/with.js +3 -10
  7. package/dist/commands/agents/activity.js +1 -1
  8. package/dist/commands/agents/api-actions/add.js +6 -0
  9. package/dist/commands/agents/chat.js +6 -1
  10. package/dist/commands/agents/create.js +6 -1
  11. package/dist/commands/agents/manager/ensure.d.ts +18 -0
  12. package/dist/commands/agents/manager/ensure.js +56 -0
  13. package/dist/commands/agents/show.js +4 -0
  14. package/dist/commands/agents/tools/auth.d.ts +6 -0
  15. package/dist/commands/agents/tools/auth.js +27 -1
  16. package/dist/commands/agents/tools/index.js +11 -1
  17. package/dist/commands/agents/update.js +6 -1
  18. package/dist/commands/code/install.d.ts +4 -2
  19. package/dist/commands/code/install.js +15 -14
  20. package/dist/commands/convos/list.js +1 -3
  21. package/dist/commands/convos/show.js +4 -11
  22. package/dist/commands/daemon/index.js +3 -3
  23. package/dist/commands/daemon/install.js +4 -4
  24. package/dist/commands/daemon/login.js +1 -1
  25. package/dist/commands/daemon/logs.d.ts +3 -1
  26. package/dist/commands/daemon/logs.js +16 -1
  27. package/dist/commands/daemon/start.js +3 -3
  28. package/dist/commands/daemon/status.d.ts +9 -0
  29. package/dist/commands/daemon/status.js +22 -0
  30. package/dist/commands/dm/messages.js +1 -11
  31. package/dist/commands/followups/cancel.js +1 -1
  32. package/dist/commands/followups/remind.js +4 -0
  33. package/dist/commands/followups/resolve.js +1 -1
  34. package/dist/commands/followups/show.js +16 -0
  35. package/dist/commands/harnesses/forget.js +20 -3
  36. package/dist/commands/harnesses/install.d.ts +18 -0
  37. package/dist/commands/harnesses/install.js +146 -25
  38. package/dist/commands/harnesses/installers.d.ts +1 -1
  39. package/dist/commands/harnesses/installers.js +36 -12
  40. package/dist/commands/harnesses/list.d.ts +74 -2
  41. package/dist/commands/harnesses/list.js +231 -15
  42. package/dist/commands/harnesses/ping.d.ts +2 -0
  43. package/dist/commands/harnesses/ping.js +21 -0
  44. package/dist/commands/harnesses/show.d.ts +22 -0
  45. package/dist/commands/harnesses/show.js +116 -17
  46. package/dist/commands/harnesses/update.d.ts +8 -0
  47. package/dist/commands/harnesses/update.js +25 -1
  48. package/dist/commands/harnesses/usage.d.ts +2 -0
  49. package/dist/commands/harnesses/usage.js +7 -2
  50. package/dist/commands/instructions/status.js +13 -0
  51. package/dist/commands/instructions/update-target.d.ts +1 -0
  52. package/dist/commands/instructions/update-target.js +11 -3
  53. package/dist/commands/login.js +5 -5
  54. package/dist/commands/machines/list.js +8 -9
  55. package/dist/commands/machines/show.js +46 -17
  56. package/dist/commands/messages/show.js +3 -10
  57. package/dist/commands/shares/get.js +2 -9
  58. package/dist/commands/shares/list.js +1 -9
  59. package/dist/commands/shortcuts/show.js +4 -11
  60. package/dist/commands/skills/import-as-actions.js +6 -15
  61. package/dist/commands/skills/local.js +6 -0
  62. package/dist/commands/spaces/actions/run.js +3 -4
  63. package/dist/commands/spaces/briefs/get.js +2 -9
  64. package/dist/commands/spaces/briefs/list.js +1 -9
  65. package/dist/commands/spaces/conversations.js +1 -9
  66. package/dist/commands/spaces/heartbeats/list.js +1 -9
  67. package/dist/commands/spaces/heartbeats/status.js +2 -9
  68. package/dist/commands/tasks/activity.js +1 -10
  69. package/dist/commands/tasks/approvals/list.js +1 -9
  70. package/dist/commands/tasks/attachments/list.js +1 -9
  71. package/dist/commands/tasks/comments/list.js +1 -9
  72. package/dist/commands/tasks/complete.js +13 -0
  73. package/dist/commands/tasks/dri-status.js +2 -9
  74. package/dist/commands/tasks/events/append.js +9 -7
  75. package/dist/commands/tasks/execution-timeline.js +1 -9
  76. package/dist/commands/tasks/monitor.js +1 -9
  77. package/dist/commands/tasks/result/submit.js +14 -0
  78. package/dist/commands/tasks/runs.js +1 -9
  79. package/dist/commands/tasks/show.js +5 -12
  80. package/dist/commands/token/list.d.ts +8 -0
  81. package/dist/commands/token/list.js +51 -6
  82. package/dist/help.js +8 -12
  83. package/dist/lib/agent-home-workspace.d.ts +19 -0
  84. package/dist/lib/agent-home-workspace.js +39 -0
  85. package/dist/lib/agent-resolver.js +2 -2
  86. package/dist/lib/agentic-stream.d.ts +22 -1
  87. package/dist/lib/agentic-stream.js +48 -7
  88. package/dist/lib/cli-installers.d.ts +124 -2
  89. package/dist/lib/cli-installers.js +196 -13
  90. package/dist/lib/command-miss.d.ts +22 -0
  91. package/dist/lib/command-miss.js +78 -0
  92. package/dist/lib/daemon-setup.js +2 -2
  93. package/dist/lib/daemon-target.d.ts +14 -0
  94. package/dist/lib/daemon-target.js +32 -0
  95. package/dist/lib/daemonBroker.js +1 -1
  96. package/dist/lib/daemonHandoff.js +1 -1
  97. package/dist/lib/exec-runtime-binary.js +2 -2
  98. package/dist/lib/followups.d.ts +12 -0
  99. package/dist/lib/followups.js +14 -1
  100. package/dist/lib/format.d.ts +11 -0
  101. package/dist/lib/format.js +21 -0
  102. package/dist/lib/harness-labels.d.ts +39 -0
  103. package/dist/lib/harness-labels.js +60 -0
  104. package/dist/lib/harnesses.d.ts +6 -0
  105. package/dist/lib/instruction-input.d.ts +13 -0
  106. package/dist/lib/instruction-input.js +20 -1
  107. package/dist/lib/local-skills.d.ts +44 -1
  108. package/dist/lib/local-skills.js +58 -2
  109. package/dist/lib/login.js +5 -5
  110. package/dist/lib/machines.d.ts +6 -0
  111. package/dist/lib/machines.js +68 -2
  112. package/dist/lib/runtime-tool-support.d.ts +34 -0
  113. package/dist/lib/runtime-tool-support.js +77 -0
  114. package/dist/lib/share-rows.d.ts +17 -0
  115. package/dist/lib/share-rows.js +35 -0
  116. package/dist/lib/task-closure.d.ts +13 -0
  117. package/dist/lib/task-closure.js +17 -0
  118. package/dist/node_modules/@skrr-ai/data-provider/index.js +5066 -5033
  119. package/oclif.manifest.json +33472 -33360
  120. package/package.json +6 -3
@@ -11,7 +11,7 @@ import { BaseCommand } from '../../base-command';
11
11
  *
12
12
  * That gap is not cosmetic, because `skrr harnesses list` answers a DIFFERENT
13
13
  * question and looks like it answers this one. Its `status` is the daemon's
14
- * socket, so on this account `skrr-code` read `online` with no binary installed
14
+ * socket, so on this account the first-party harness read `online` with no binary installed
15
15
  * at all, and `harnesses ping` answered it in 78ms — the daemon was there, the
16
16
  * harness was not. A user could not discover that from inside the product; it
17
17
  * took `command -v` in a shell.
@@ -4,7 +4,6 @@ const core_1 = require("@oclif/core");
4
4
  const base_command_1 = require("../../base-command");
5
5
  const data_provider_1 = require("@skrr-ai/data-provider");
6
6
  const cli_installers_1 = require("../../lib/cli-installers");
7
- const daemon_ref_1 = require("../../lib/daemon-ref");
8
7
  const daemon_target_1 = require("../../lib/daemon-target");
9
8
  const format_1 = require("../../lib/format");
10
9
  /**
@@ -19,7 +18,7 @@ const format_1 = require("../../lib/format");
19
18
  *
20
19
  * That gap is not cosmetic, because `skrr harnesses list` answers a DIFFERENT
21
20
  * question and looks like it answers this one. Its `status` is the daemon's
22
- * socket, so on this account `skrr-code` read `online` with no binary installed
21
+ * socket, so on this account the first-party harness read `online` with no binary installed
23
22
  * at all, and `harnesses ping` answered it in 78ms — the daemon was there, the
24
23
  * harness was not. A user could not discover that from inside the product; it
25
24
  * took `command -v` in a shell.
@@ -44,7 +43,7 @@ class HarnessesInstallers extends base_command_1.BaseCommand {
44
43
  static flags = {
45
44
  json: core_1.Flags.boolean({ description: 'Output as JSON' }),
46
45
  daemon: core_1.Flags.string({
47
- description: 'Which machine to ask. Defaults to your only active daemon; required when several are active.',
46
+ description: 'Which machine to ask. Defaults to your only connected daemon; required when several are connected.',
48
47
  }),
49
48
  };
50
49
  async run() {
@@ -53,7 +52,7 @@ class HarnessesInstallers extends base_command_1.BaseCommand {
53
52
  const target = await (0, daemon_target_1.resolveDaemonTarget)({
54
53
  explicit: flags.daemon,
55
54
  local: (0, daemon_target_1.localMachine)(),
56
- listActiveDaemons: () => (0, daemon_target_1.listActiveDaemons)(this.resolvedCredential),
55
+ listActiveDaemons: () => (0, daemon_target_1.listConnectedDaemons)(this.resolvedCredential),
57
56
  });
58
57
  const daemonId = this.daemonIdFor(target);
59
58
  let res;
@@ -65,8 +64,21 @@ class HarnessesInstallers extends base_command_1.BaseCommand {
65
64
  return;
66
65
  }
67
66
  const providers = res.providers ?? [];
67
+ // Computed before the output mode splits: a JSON reader needs the same
68
+ // "this daemon is older than that provider" signal the table prints, and
69
+ // the daemon version the header shows (OSK-9537 verifier).
70
+ const notOffered = providers.length > 0 ? (0, cli_installers_1.providersNotOffered)(providers, data_provider_1.MANAGED_CLI_PROVIDER_IDS) : [];
68
71
  if (flags.json) {
69
- this.log(JSON.stringify({ daemonId, platform: res.platform, providers }, null, 2));
72
+ this.log(JSON.stringify({
73
+ daemonId,
74
+ platform: res.platform,
75
+ daemonVersion: res.daemonVersion ?? null,
76
+ providers,
77
+ notOffered: notOffered.map((provider) => ({
78
+ provider,
79
+ addedInDaemon: cli_installers_1.MANAGED_CLI_PROVIDER_SINCE[(0, cli_installers_1.readinessKey)(provider)] ?? null,
80
+ })),
81
+ }, null, 2));
70
82
  return;
71
83
  }
72
84
  if (providers.length === 0) {
@@ -116,12 +128,12 @@ class HarnessesInstallers extends base_command_1.BaseCommand {
116
128
  }
117
129
  // A provider the platform knows and this daemon did not mention. Without
118
130
  // this it simply is not on the list, which reads as dropped support rather
119
- // than as an older daemon — `devin` arrived in daemon 0.8.43, so 0.8.35
131
+ // than as an older daemon — `devin` arrived in the 0.8.45 daemon release, so 0.8.35
120
132
  // answers with six providers and says nothing about the seventh.
121
- const missing = (0, cli_installers_1.providersNotOffered)(providers, data_provider_1.MANAGED_CLI_PROVIDER_IDS);
133
+ const missing = notOffered;
122
134
  if (missing.length > 0) {
123
135
  this.log('');
124
- this.log(` Not offered by this daemon: ${missing.join(', ')}. ` +
136
+ this.log(` Not offered by this daemon: ${(0, cli_installers_1.describeProvidersNotOffered)(missing)}. ` +
125
137
  'That usually means the daemon is older than the provider — ' +
126
138
  'check `skrr daemon status` for its version and how to update it.');
127
139
  }
@@ -142,14 +154,26 @@ class HarnessesInstallers extends base_command_1.BaseCommand {
142
154
  this.error('This machine is a Dedicated Runtime whose daemon has not published an id this process can read. Name a machine with --daemon.', { exit: 1 });
143
155
  }
144
156
  if (target.kind === 'none') {
145
- this.error(`No active daemon is registered for this account, so nothing can report on it. Start the local runtime with \`${this.config.bin} daemon start\`.`, { exit: 1 });
157
+ this.failWithCliError({
158
+ message: `No daemon is connected for this account, so nothing can report on it. Start the background service on this computer with \`${this.config.bin} daemon start\`.`,
159
+ code: 'NO_DAEMON',
160
+ exit: 1,
161
+ retryable: false,
162
+ });
146
163
  }
147
164
  const choices = target.daemons
148
165
  .slice(0, 10)
149
- .map((d) => ` ${(0, daemon_ref_1.formatDaemonRef)(d.id)} ${d.name || '—'}`)
166
+ // Full ids: they are what `--daemon` takes (OSK-9993).
167
+ .map((d) => ` ${d.id} ${d.name || '—'}`)
150
168
  .join('\n');
151
- this.error(`${target.daemons.length} active daemons — say which machine to report on:\n${choices}\n` +
152
- `Pass one with --daemon, or see them all with \`${this.config.bin} list-daemons\`.`, { exit: 1 });
169
+ // The same refusal, and the same code, as \`code install\` (OSK-9993 verifier).
170
+ this.failWithCliError({
171
+ message: `${target.daemons.length} connected daemons — say which machine to report on:\n${choices}\n` +
172
+ `Pass one with --daemon, or see each daemon id in the DAEMON ID column of \`${this.config.bin} harnesses list\`.`,
173
+ code: 'AMBIGUOUS_DAEMON',
174
+ exit: 1,
175
+ retryable: false,
176
+ });
153
177
  }
154
178
  }
155
179
  exports.default = HarnessesInstallers;
@@ -1,4 +1,5 @@
1
1
  import { BaseCommand } from '../../base-command';
2
+ import { type ProviderReadiness } from '../../lib/cli-installers';
2
3
  /**
3
4
  * Minimal shape of a provider Harness row as returned by
4
5
  * `GET /api/harnesses` (`{ harnesses: WireHarness[] }`). Only the
@@ -71,6 +72,25 @@ export declare function shortMachineLabel(machineUuid?: string): string;
71
72
  export declare function emptyListMessage(input: {
72
73
  provider: string | null;
73
74
  local: boolean;
75
+ /** `--all` was passed, so suggesting it is noise (OSK-9285). */
76
+ all?: boolean;
77
+ /**
78
+ * Every provider this account has a row for, whatever its status, when the
79
+ * caller could read it. Absent or null: unknown, and nothing is claimed.
80
+ */
81
+ accountProviders?: readonly string[] | null;
82
+ }): string;
83
+ /**
84
+ * The NAME cell. `show` and `--json` said when YOUR alias covers the shared
85
+ * name; this table printed the alias as if it were the name everyone sees
86
+ * (OSK-10004). The marker leads because the column truncates from the right,
87
+ * and a trailing one is the first thing a long name would cut.
88
+ */
89
+ export declare const ALIAS_MARKER = "*";
90
+ export declare function harnessNameCell(row: {
91
+ name?: string;
92
+ sharedName?: string;
93
+ aliased?: boolean;
74
94
  }): string;
75
95
  /**
76
96
  * What the DEFAULT column says for one row.
@@ -89,9 +109,33 @@ export type HarnessSelection = {
89
109
  hiddenOffline: number;
90
110
  /** Rows dropped by `--limit` alone. */
91
111
  hiddenByLimit: number;
112
+ /**
113
+ * Superseded rows a status filter dropped — the ones `--status offline`
114
+ * leaves to `--status superseded`. Only superseded rows are counted: online
115
+ * rows the same filter drops were never offline, so they are not "hidden".
116
+ */
117
+ hiddenSuperseded: number;
92
118
  /** Rows that survived every filter, before the online-only default and `--limit`. */
93
119
  matched: number;
94
120
  };
121
+ /**
122
+ * What `--status` accepts: exactly what the STATUS column prints (OSK-10005).
123
+ *
124
+ * The column says `superseded` for a row a later registration on the same
125
+ * machine replaced; the filter accepted only online|offline, so the one value
126
+ * that says "never coming back" could be read and never selected.
127
+ */
128
+ export declare const HARNESS_STATUS_FILTERS: readonly ["online", "offline", "superseded"];
129
+ /**
130
+ * The status to ask the SERVER for. Only `online` narrows there.
131
+ *
132
+ * Supersession is judged among the rows in one response — a row is superseded
133
+ * when a newer row for the same provider on the same machine exists. Asked for
134
+ * `offline` only, the server cannot see the live row that replaced an offline
135
+ * one, so it labelled that row `offline` while `--all` called it `superseded`.
136
+ * Online rows are never superseded, so that one filter is safe to push down.
137
+ */
138
+ export declare function serverStatusParam(status: string | undefined): string | undefined;
95
139
  /**
96
140
  * Harness rows created before `harnessLocation` was introduced do not carry that
97
141
  * field. A daemon id is the durable discriminator for those legacy rows:
@@ -145,6 +189,8 @@ export declare function selectHarnessRows(rows: HarnessRow[], options: {
145
189
  local: boolean;
146
190
  daemon?: string;
147
191
  limit?: number;
192
+ /** Keep only rows whose STATUS column reads this (`harnessStatusLabel`). */
193
+ status?: string;
148
194
  /** Hide offline rows (the human-readable default). */
149
195
  onlineOnly: boolean;
150
196
  }): HarnessSelection;
@@ -177,7 +223,9 @@ export type HarnessDefaults = {
177
223
  workspaceDefaultHarnessId?: string | null;
178
224
  };
179
225
  export type HarnessesJson = {
180
- harnesses: HarnessRow[];
226
+ harnesses: Array<HarnessRow & {
227
+ readiness?: ProviderReadiness | null;
228
+ }>;
181
229
  filters: HarnessesFilters;
182
230
  defaults: HarnessDefaults;
183
231
  counts: {
@@ -189,6 +237,8 @@ export type HarnessesJson = {
189
237
  hiddenByLocal: number;
190
238
  /** Rows `--limit` dropped — raise `--limit` to get them back. */
191
239
  hiddenByLimit: number;
240
+ /** Superseded rows `--status` dropped — `--status superseded` lists them. */
241
+ hiddenSuperseded: number;
192
242
  /**
193
243
  * OSK-3385 — how many of the returned rows are `status: 'online'`.
194
244
  *
@@ -213,7 +263,22 @@ export type HarnessesJson = {
213
263
  * (`compareHarnessRows`), not in server order. Round 1 introduced that sort;
214
264
  * it is deliberate, so `.harnesses[0]` is the row you almost always want.
215
265
  */
216
- export declare function buildHarnessesJson(selection: HarnessSelection, filters: HarnessesFilters, defaults?: HarnessDefaults): HarnessesJson;
266
+ export declare function buildHarnessesJson(selection: HarnessSelection, filters: HarnessesFilters, defaults?: HarnessDefaults, readiness?: (row: HarnessRow) => ProviderReadiness | undefined): HarnessesJson;
267
+ /**
268
+ * What the READINESS column's non-answers mean, counted from the rows shown.
269
+ *
270
+ * Each footnote names a different next step, so they are not merged: a `-` on
271
+ * an offline row wants `--all`/a started daemon, a `-` on a provider the daemon
272
+ * does not manage (hermes, gemini) wants nothing, `unknown` is installed but
273
+ * unverifiable, and only a definite not-ready label (interactive-required,
274
+ * not-installed, …) has something to fix. Counting `unknown` as not ready
275
+ * would send the reader to fix eleven rows that are fine as far as anyone can
276
+ * tell.
277
+ */
278
+ export declare function readinessFootnotes(rows: readonly HarnessRow[], readiness: {
279
+ lookup: (row: HarnessRow) => ProviderReadiness | undefined;
280
+ daemonAnswered: (row: HarnessRow) => boolean;
281
+ }): string[];
217
282
  /**
218
283
  * Rows with the provider in its CANONICAL spelling.
219
284
  *
@@ -246,6 +311,13 @@ export default class HarnessesList extends BaseCommand {
246
311
  daemon: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
247
312
  limit: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
248
313
  local: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
314
+ readiness: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
249
315
  };
250
316
  run(): Promise<void>;
317
+ /**
318
+ * The canonical providers this account has any row for, or null when that
319
+ * cannot be read. Asked only when a --provider filter came back empty, so
320
+ * the ordinary path pays nothing for it.
321
+ */
322
+ private accountProviders;
251
323
  }
@@ -1,18 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HARNESS_STATUS_FILTERS = exports.ALIAS_MARKER = void 0;
3
4
  exports.shortMachineLabel = shortMachineLabel;
4
5
  exports.emptyListMessage = emptyListMessage;
6
+ exports.harnessNameCell = harnessNameCell;
5
7
  exports.defaultMarker = defaultMarker;
8
+ exports.serverStatusParam = serverStatusParam;
6
9
  exports.isLocalHarnessRow = isLocalHarnessRow;
7
10
  exports.selectHarnessRows = selectHarnessRows;
8
11
  exports.buildHarnessesJson = buildHarnessesJson;
12
+ exports.readinessFootnotes = readinessFootnotes;
9
13
  exports.canonicalHarnessRows = canonicalHarnessRows;
10
14
  const core_1 = require("@oclif/core");
11
15
  const auth_core_1 = require("@skrr-ai/auth-core");
12
16
  const base_command_1 = require("../../base-command");
13
17
  const api_fetch_1 = require("../../lib/api-fetch");
18
+ const cli_installers_1 = require("../../lib/cli-installers");
14
19
  const format_1 = require("../../lib/format");
15
20
  const harness_provider_input_1 = require("../../lib/harness-provider-input");
21
+ const data_provider_1 = require("@skrr-ai/data-provider");
22
+ const harness_labels_1 = require("../../lib/harness-labels");
16
23
  /**
17
24
  * Products skrr provisions and owns. A row that says it is one of these is
18
25
  * cloud, whatever `harnessLocation` reads.
@@ -50,17 +57,48 @@ function shortMachineLabel(machineUuid) {
50
57
  * pointing at the one command that shows which providers actually exist.
51
58
  */
52
59
  function emptyListMessage(input) {
60
+ if (input.provider) {
61
+ const known = input.accountProviders;
62
+ // A provider that appears on no row of the account — a typo, or a harness
63
+ // never registered — is a different fact from one whose rows are merely
64
+ // hidden by a filter. Both used to print the same sentence (OSK-9285).
65
+ if (known && !known.includes(input.provider)) {
66
+ return known.length > 0
67
+ ? `No harness on this account has provider "${input.provider}". Providers here: ${known.join(', ')}.`
68
+ : `No harness on this account has provider "${input.provider}", and the account has no harnesses yet.`;
69
+ }
70
+ const lifts = [];
71
+ if (!input.all)
72
+ lifts.push('offline rows are hidden by default (--all shows them)');
73
+ if (input.local)
74
+ lifts.push('hosted rows are excluded (--no-local includes them)');
75
+ const liftText = lifts.join('; ');
76
+ return (`No harnesses with provider "${input.provider}" match these filters.` +
77
+ (liftText ? ` ${liftText.charAt(0).toUpperCase()}${liftText.slice(1)}.` : '') +
78
+ (known
79
+ ? ''
80
+ : ' Run `skrr harnesses list --all` without --provider to see which providers this account has registered.'));
81
+ }
53
82
  const hiddenByDefault = input.local
54
83
  ? ' Offline rows are hidden by default (--all shows them) and hosted rows are excluded (--no-local includes them).'
55
84
  : '';
56
- if (input.provider) {
57
- return (`No harnesses with provider "${input.provider}".${hiddenByDefault}` +
58
- ' Run `skrr harnesses list --all` without --provider to see which providers this account has registered.');
59
- }
60
85
  return input.local
61
86
  ? `No harnesses found. Is your daemon connected?${hiddenByDefault}`
62
87
  : 'No harnesses found.';
63
88
  }
89
+ /**
90
+ * The NAME cell. `show` and `--json` said when YOUR alias covers the shared
91
+ * name; this table printed the alias as if it were the name everyone sees
92
+ * (OSK-10004). The marker leads because the column truncates from the right,
93
+ * and a trailing one is the first thing a long name would cut.
94
+ */
95
+ exports.ALIAS_MARKER = '*';
96
+ function harnessNameCell(row) {
97
+ // The same rule the web sidebar renders (OSK-9290): a name stored under an
98
+ // older spelling or `oversky …` name is not a second product.
99
+ const name = row.name ? (0, data_provider_1.canonicalizeHarnessLabel)(row.name) : (0, format_1.formatCell)(row.name);
100
+ return (0, harness_labels_1.isAliasCoveringSharedName)(row) ? `${exports.ALIAS_MARKER} ${name}` : name;
101
+ }
64
102
  /**
65
103
  * What the DEFAULT column says for one row.
66
104
  *
@@ -84,6 +122,26 @@ const MANAGED_REGISTRATION_SOURCES = new Set([
84
122
  'hosted-job-machine',
85
123
  'cloud-coding',
86
124
  ]);
125
+ /**
126
+ * What `--status` accepts: exactly what the STATUS column prints (OSK-10005).
127
+ *
128
+ * The column says `superseded` for a row a later registration on the same
129
+ * machine replaced; the filter accepted only online|offline, so the one value
130
+ * that says "never coming back" could be read and never selected.
131
+ */
132
+ exports.HARNESS_STATUS_FILTERS = ['online', 'offline', 'superseded'];
133
+ /**
134
+ * The status to ask the SERVER for. Only `online` narrows there.
135
+ *
136
+ * Supersession is judged among the rows in one response — a row is superseded
137
+ * when a newer row for the same provider on the same machine exists. Asked for
138
+ * `offline` only, the server cannot see the live row that replaced an offline
139
+ * one, so it labelled that row `offline` while `--all` called it `superseded`.
140
+ * Online rows are never superseded, so that one filter is safe to push down.
141
+ */
142
+ function serverStatusParam(status) {
143
+ return status === 'online' ? 'online' : undefined;
144
+ }
87
145
  /**
88
146
  * Harness rows created before `harnessLocation` was introduced do not carry that
89
147
  * field. A daemon id is the durable discriminator for those legacy rows:
@@ -155,6 +213,16 @@ function selectHarnessRows(rows, options) {
155
213
  const needle = options.daemon.toLowerCase();
156
214
  filtered = filtered.filter((r) => (r.daemonId ?? '').toLowerCase().includes(needle));
157
215
  }
216
+ let hiddenSuperseded = 0;
217
+ if (options.status) {
218
+ const wanted = options.status;
219
+ filtered = filtered.filter((r) => {
220
+ const label = (0, harness_labels_1.harnessStatusLabel)(r);
221
+ if (label !== wanted && label === 'superseded')
222
+ hiddenSuperseded += 1;
223
+ return label === wanted;
224
+ });
225
+ }
158
226
  const sorted = [...filtered].sort(compareHarnessRows);
159
227
  const shown = options.onlineOnly ? sorted.filter((r) => r.status === 'online') : sorted;
160
228
  const limited = options.limit != null && options.limit > 0 ? shown.slice(0, options.limit) : shown;
@@ -163,6 +231,7 @@ function selectHarnessRows(rows, options) {
163
231
  hiddenByLocal,
164
232
  hiddenOffline: sorted.length - shown.length,
165
233
  hiddenByLimit: shown.length - limited.length,
234
+ hiddenSuperseded,
166
235
  matched: sorted.length,
167
236
  };
168
237
  }
@@ -178,9 +247,13 @@ function selectHarnessRows(rows, options) {
178
247
  * (`compareHarnessRows`), not in server order. Round 1 introduced that sort;
179
248
  * it is deliberate, so `.harnesses[0]` is the row you almost always want.
180
249
  */
181
- function buildHarnessesJson(selection, filters, defaults = {}) {
250
+ function buildHarnessesJson(selection, filters, defaults = {}, readiness) {
182
251
  return {
183
- harnesses: selection.visible,
252
+ // `readiness` is the per-provider answer the READINESS column prints —
253
+ // `null` where no daemon answered for that row, never a guess (OSK-10100).
254
+ harnesses: readiness
255
+ ? selection.visible.map((row) => ({ ...row, readiness: readiness(row) ?? null }))
256
+ : selection.visible,
184
257
  filters,
185
258
  defaults: {
186
259
  effectiveDefaultHarnessId: defaults.effectiveDefaultHarnessId ?? null,
@@ -193,10 +266,61 @@ function buildHarnessesJson(selection, filters, defaults = {}) {
193
266
  matched: selection.matched,
194
267
  hiddenByLocal: selection.hiddenByLocal,
195
268
  hiddenByLimit: selection.hiddenByLimit,
269
+ hiddenSuperseded: selection.hiddenSuperseded,
196
270
  online: selection.visible.filter((r) => r.status === 'online').length,
197
271
  },
198
272
  };
199
273
  }
274
+ /**
275
+ * What the READINESS column's non-answers mean, counted from the rows shown.
276
+ *
277
+ * Each footnote names a different next step, so they are not merged: a `-` on
278
+ * an offline row wants `--all`/a started daemon, a `-` on a provider the daemon
279
+ * does not manage (hermes, gemini) wants nothing, `unknown` is installed but
280
+ * unverifiable, and only a definite not-ready label (interactive-required,
281
+ * not-installed, …) has something to fix. Counting `unknown` as not ready
282
+ * would send the reader to fix eleven rows that are fine as far as anyone can
283
+ * tell.
284
+ */
285
+ function readinessFootnotes(rows, readiness) {
286
+ const lines = [];
287
+ let notLive = 0;
288
+ let unmanaged = 0;
289
+ let noAnswer = 0;
290
+ let unknown = 0;
291
+ let notReady = 0;
292
+ for (const row of rows) {
293
+ const verdict = readiness.lookup(row);
294
+ if (verdict) {
295
+ if (verdict.label === 'unknown')
296
+ unknown += 1;
297
+ else if (!verdict.ok)
298
+ notReady += 1;
299
+ }
300
+ else if (row.status !== 'online' || row.superseded)
301
+ notLive += 1;
302
+ else if (readiness.daemonAnswered(row))
303
+ unmanaged += 1;
304
+ else
305
+ noAnswer += 1;
306
+ }
307
+ const dash = [];
308
+ if (notLive > 0)
309
+ dash.push(`${notLive} offline or superseded`);
310
+ if (unmanaged > 0)
311
+ dash.push(`${unmanaged} a provider its daemon does not report on`);
312
+ if (noAnswer > 0)
313
+ dash.push(`${noAnswer} whose daemon did not answer in time`);
314
+ if (dash.length > 0)
315
+ lines.push(`(READINESS -: ${dash.join('; ')}.)`);
316
+ if (unknown > 0) {
317
+ lines.push(`(READINESS unknown: ${unknown} installed, but the daemon cannot read that provider's sign-in state.)`);
318
+ }
319
+ if (notReady > 0) {
320
+ lines.push(`(${notReady} not ready — \`skrr harnesses show <id>\` says what each needs.)`);
321
+ }
322
+ return lines;
323
+ }
200
324
  /**
201
325
  * Rows with the provider in its CANONICAL spelling.
202
326
  *
@@ -253,8 +377,8 @@ class HarnessesList extends base_command_1.BaseCommand {
253
377
  static flags = {
254
378
  json: core_1.Flags.boolean({ description: 'Output as JSON' }),
255
379
  status: core_1.Flags.string({
256
- description: 'Filter by harness status.',
257
- options: ['online', 'offline'],
380
+ description: 'Filter by the STATUS column. `offline` leaves out superseded rows — a later registration on the same machine replaced them — and `superseded` lists only those.',
381
+ options: [...exports.HARNESS_STATUS_FILTERS],
258
382
  exclusive: ['online', 'all'],
259
383
  }),
260
384
  online: core_1.Flags.boolean({
@@ -284,6 +408,11 @@ class HarnessesList extends base_command_1.BaseCommand {
284
408
  default: true,
285
409
  allowNo: true,
286
410
  }),
411
+ readiness: core_1.Flags.boolean({
412
+ description: 'Ask each connected daemon whether its coding-agent CLIs are installed and signed in (the READINESS column; `readiness` in --json). One request per daemon. On by default — pass --no-readiness to skip it.',
413
+ default: true,
414
+ allowNo: true,
415
+ }),
287
416
  };
288
417
  async run() {
289
418
  this.requireAuth();
@@ -296,8 +425,9 @@ class HarnessesList extends base_command_1.BaseCommand {
296
425
  // asked for the canonical one, which it matches against every stored spelling.
297
426
  const provider = flags.provider ? (0, harness_provider_input_1.canonicalHarnessInput)(flags.provider) : undefined;
298
427
  const params = new URLSearchParams();
299
- if (status)
300
- params.set('status', status);
428
+ const serverStatus = serverStatusParam(status);
429
+ if (serverStatus)
430
+ params.set('status', serverStatus);
301
431
  if (provider)
302
432
  params.set('provider', provider);
303
433
  const query = params.toString() ? `?${params.toString()}` : '';
@@ -312,16 +442,36 @@ class HarnessesList extends base_command_1.BaseCommand {
312
442
  local: flags.local,
313
443
  daemon: flags.daemon,
314
444
  limit: flags.limit,
445
+ status,
315
446
  onlineOnly: false,
316
447
  });
448
+ // STATUS is the daemon's socket, not the coding agent: a Devin that needs
449
+ // sign-in read `online` exactly like a ready one, and `list` is where a
450
+ // user picks a harness (OSK-10100). One read per connected daemon, bounded;
451
+ // a daemon that cannot answer leaves its rows unknown rather than failing
452
+ // the list.
453
+ const readiness = flags.readiness
454
+ ? await (0, cli_installers_1.readinessForRows)(selection.visible, this.resolvedCredential)
455
+ : null;
317
456
  if (flags.json) {
457
+ // A provider no row on the account has is a typo (or a harness never
458
+ // registered), and a script that got an empty list and exit 0 could not
459
+ // tell it from a filter that hid real rows — the silent failure OSK-9285
460
+ // describes. Refused exactly as the table refuses it; a real provider
461
+ // whose rows a filter hid still gets its empty list.
462
+ if (provider && selection.visible.length === 0) {
463
+ const accountProviders = await this.accountProviders();
464
+ if (accountProviders && !accountProviders.includes(provider)) {
465
+ this.error(emptyListMessage({ provider, local: flags.local, all: flags.all, accountProviders }), { exit: 1 });
466
+ }
467
+ }
318
468
  const payload = buildHarnessesJson(selection, {
319
469
  local: flags.local,
320
470
  status: status ?? null,
321
471
  provider: provider ?? null,
322
472
  daemon: flags.daemon ?? null,
323
473
  limit: flags.limit ?? null,
324
- }, res);
474
+ }, res, readiness ? readiness.lookup : () => undefined);
325
475
  this.log(JSON.stringify(payload, null, 2));
326
476
  return;
327
477
  }
@@ -338,18 +488,39 @@ class HarnessesList extends base_command_1.BaseCommand {
338
488
  if (selection.hiddenByLocal > 0) {
339
489
  hidden.push(`${selection.hiddenByLocal} hosted (--no-local)`);
340
490
  }
491
+ if (selection.hiddenSuperseded > 0) {
492
+ hidden.push(`${selection.hiddenSuperseded} superseded (--status superseded)`);
493
+ }
341
494
  if (hidden.length > 0) {
342
495
  this.log(`No harnesses match the default filters. Hidden: ${hidden.join(', ')}.`);
343
496
  return;
344
497
  }
345
- this.log(emptyListMessage({ provider: provider ?? null, local: flags.local }));
498
+ if (!provider) {
499
+ this.log(emptyListMessage({ provider: null, local: flags.local, all: flags.all }));
500
+ return;
501
+ }
502
+ const accountProviders = await this.accountProviders();
503
+ const message = emptyListMessage({
504
+ provider,
505
+ local: flags.local,
506
+ all: flags.all,
507
+ accountProviders,
508
+ });
509
+ if (accountProviders && !accountProviders.includes(provider)) {
510
+ // Nothing on the account answers to this name: exit non-zero so a script
511
+ // cannot mistake a typo for an empty-but-valid filter. `--json` refuses it
512
+ // the same way (above).
513
+ this.error(message, { exit: 1 });
514
+ }
515
+ this.log(message);
346
516
  return;
347
517
  }
348
518
  const tableRows = selection.visible.map((r) => ({
349
519
  id: (0, format_1.formatCell)(r._id),
350
- name: (0, format_1.formatCell)(r.name),
351
- provider: (0, format_1.formatCell)(r.provider),
352
- status: r.superseded ? 'superseded' : (0, format_1.formatCell)(r.status),
520
+ name: harnessNameCell(r),
521
+ provider: (0, harness_labels_1.harnessProviderLabel)(r.provider),
522
+ status: (0, harness_labels_1.harnessStatusLabel)(r),
523
+ readiness: readiness?.lookup(r)?.label ?? '-',
353
524
  default: defaultMarker(r._id, res),
354
525
  daemon: (0, format_1.formatCell)(r.daemonId),
355
526
  model: (0, format_1.formatCell)(r.model),
@@ -363,6 +534,7 @@ class HarnessesList extends base_command_1.BaseCommand {
363
534
  // only inside a third command (`machines show`). Now that the server labels
364
535
  // supersession here too, say it where the rows are.
365
536
  const supersededShown = selection.visible.filter((r) => r.superseded).length;
537
+ const aliasedShown = selection.visible.filter((r) => (0, harness_labels_1.isAliasCoveringSharedName)(r)).length;
366
538
  (0, format_1.renderTable)(tableRows, [
367
539
  { key: 'id', header: 'HARNESS ID' },
368
540
  // `harnesses alias` exists to make a row recognisable in a LIST. Without
@@ -371,6 +543,11 @@ class HarnessesList extends base_command_1.BaseCommand {
371
543
  { key: 'name', header: 'NAME', maxWidth: 22 },
372
544
  { key: 'provider', header: 'PROVIDER' },
373
545
  { key: 'status', header: 'STATUS' },
546
+ // Whether the coding agent itself can run — installed, signed in — as
547
+ // opposed to STATUS, which is only whether its daemon is connected.
548
+ ...(readiness && readiness.answered > 0
549
+ ? [{ key: 'readiness', header: 'READINESS' }]
550
+ : []),
374
551
  // `set-default` had no reader anywhere in the CLI. This is it.
375
552
  { key: 'default', header: 'DEFAULT' },
376
553
  { key: 'daemon', header: 'DAEMON ID', verbatim: true },
@@ -378,11 +555,28 @@ class HarnessesList extends base_command_1.BaseCommand {
378
555
  { key: 'machine', header: 'MACHINE' },
379
556
  { key: 'seen', header: `LAST SEEN (${(0, format_1.localZone)()})` },
380
557
  ], (line) => this.log(line));
558
+ if (readiness && readiness.answered > 0) {
559
+ for (const line of readinessFootnotes(selection.visible, readiness))
560
+ this.log(line);
561
+ }
562
+ else if (readiness && readiness.asked > 0) {
563
+ this.log('(Readiness not reported — no connected daemon answered. `skrr harnesses installers --daemon <DAEMON ID>` asks one directly.)');
564
+ }
565
+ const noCodingAgentShown = selection.visible.filter((r) => (0, harness_labels_1.isNoCodingAgentProvider)(r.provider)).length;
566
+ if (noCodingAgentShown > 0) {
567
+ this.log(`(provider ${harness_labels_1.NO_CODING_AGENT_PROVIDER_LABEL}: ${noCodingAgentShown} shown are machines with no coding agent installed — the row is how the machine is listed, not a harness named for a provider.)`);
568
+ }
569
+ if (aliasedShown > 0) {
570
+ this.log(`(${exports.ALIAS_MARKER} your alias — everyone else this harness is shared with sees its shared name. \`skrr harnesses show <id>\` prints both.)`);
571
+ }
381
572
  if (supersededShown > 0) {
382
573
  // This is the sentence that makes `machines list`'s fraction derivable
383
574
  // from here: live = printed − superseded.
384
575
  this.log(`(${supersededShown} of ${selection.visible.length} shown are superseded — a later registration on the same machine replaced them, so they will not come back. ${selection.visible.length - supersededShown} live.)`);
385
576
  }
577
+ if (selection.hiddenSuperseded > 0) {
578
+ this.log(`(${selection.hiddenSuperseded} superseded hidden — \`--status superseded\` lists them.)`);
579
+ }
386
580
  if (selection.hiddenByLocal > 0) {
387
581
  // Same reason as the JSON `counts` block: the `--local` default is the
388
582
  // single biggest source of "the list is shorter than I expected".
@@ -397,5 +591,27 @@ class HarnessesList extends base_command_1.BaseCommand {
397
591
  this.log(`(showing ${selection.visible.length} of ${selection.visible.length + selection.hiddenByLimit} — raise --limit for the rest)`);
398
592
  }
399
593
  }
594
+ /**
595
+ * The canonical providers this account has any row for, or null when that
596
+ * cannot be read. Asked only when a --provider filter came back empty, so
597
+ * the ordinary path pays nothing for it.
598
+ */
599
+ async accountProviders() {
600
+ try {
601
+ const all = await (0, api_fetch_1.apiFetch)('/api/harnesses', {
602
+ credential: this.resolvedCredential,
603
+ });
604
+ const providers = new Set();
605
+ for (const row of canonicalHarnessRows(all.harnesses ?? [])) {
606
+ if (typeof row.provider === 'string' && !(0, harness_labels_1.isNoCodingAgentProvider)(row.provider)) {
607
+ providers.add(row.provider);
608
+ }
609
+ }
610
+ return [...providers].sort();
611
+ }
612
+ catch {
613
+ return null;
614
+ }
615
+ }
400
616
  }
401
617
  exports.default = HarnessesList;
@@ -19,4 +19,6 @@ export default class HarnessesPing extends BaseCommand {
19
19
  timeout: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
20
20
  };
21
21
  run(): Promise<void>;
22
+ /** Best-effort: never turns a successful ping into a failure. */
23
+ private logReadiness;
22
24
  }