@skrr-ai/cli 0.1.39 → 0.1.41

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 (134) hide show
  1. package/README.md +1 -1
  2. package/dist/base-command.d.ts +9 -0
  3. package/dist/base-command.js +26 -3
  4. package/dist/commands/agent-shares/list-for.js +4 -11
  5. package/dist/commands/agent-shares/received.js +4 -11
  6. package/dist/commands/agent-shares/sent.js +4 -11
  7. package/dist/commands/agent-shares/with.js +3 -10
  8. package/dist/commands/agents/activity.js +1 -1
  9. package/dist/commands/agents/api-actions/add.js +6 -0
  10. package/dist/commands/agents/chat.js +6 -1
  11. package/dist/commands/agents/create.d.ts +7 -0
  12. package/dist/commands/agents/create.js +19 -2
  13. package/dist/commands/agents/manager/ensure.d.ts +18 -0
  14. package/dist/commands/agents/manager/ensure.js +56 -0
  15. package/dist/commands/agents/show.js +4 -0
  16. package/dist/commands/agents/tools/auth.d.ts +6 -0
  17. package/dist/commands/agents/tools/auth.js +27 -1
  18. package/dist/commands/agents/tools/index.js +11 -1
  19. package/dist/commands/agents/triggers/policy.d.ts +1 -0
  20. package/dist/commands/agents/triggers/policy.js +32 -0
  21. package/dist/commands/agents/update.js +10 -4
  22. package/dist/commands/code/install.d.ts +4 -2
  23. package/dist/commands/code/install.js +12 -10
  24. package/dist/commands/convos/list.js +1 -3
  25. package/dist/commands/convos/show.js +4 -11
  26. package/dist/commands/daemon/index.js +3 -3
  27. package/dist/commands/daemon/install.js +4 -4
  28. package/dist/commands/daemon/login.js +1 -1
  29. package/dist/commands/daemon/logs.d.ts +3 -1
  30. package/dist/commands/daemon/logs.js +16 -1
  31. package/dist/commands/daemon/start.js +3 -3
  32. package/dist/commands/daemon/status.d.ts +9 -0
  33. package/dist/commands/daemon/status.js +22 -0
  34. package/dist/commands/dm/messages.js +1 -11
  35. package/dist/commands/followups/cancel.js +1 -1
  36. package/dist/commands/followups/observability.js +6 -2
  37. package/dist/commands/followups/remind.js +5 -2
  38. package/dist/commands/followups/reschedule.js +2 -1
  39. package/dist/commands/followups/resolve.js +1 -1
  40. package/dist/commands/followups/show.js +20 -0
  41. package/dist/commands/followups/watch.d.ts +1 -0
  42. package/dist/commands/followups/watch.js +9 -3
  43. package/dist/commands/harnesses/forget.js +20 -3
  44. package/dist/commands/harnesses/install.js +18 -6
  45. package/dist/commands/harnesses/installers.d.ts +1 -1
  46. package/dist/commands/harnesses/installers.js +36 -12
  47. package/dist/commands/harnesses/list.d.ts +74 -2
  48. package/dist/commands/harnesses/list.js +231 -15
  49. package/dist/commands/harnesses/ping.d.ts +2 -0
  50. package/dist/commands/harnesses/ping.js +21 -0
  51. package/dist/commands/harnesses/show.d.ts +22 -0
  52. package/dist/commands/harnesses/show.js +116 -17
  53. package/dist/commands/harnesses/update.d.ts +8 -0
  54. package/dist/commands/harnesses/update.js +25 -1
  55. package/dist/commands/harnesses/usage.d.ts +2 -0
  56. package/dist/commands/harnesses/usage.js +7 -2
  57. package/dist/commands/instructions/status.js +13 -0
  58. package/dist/commands/instructions/update-target.d.ts +1 -0
  59. package/dist/commands/instructions/update-target.js +11 -3
  60. package/dist/commands/login.js +5 -5
  61. package/dist/commands/machines/list.js +8 -9
  62. package/dist/commands/machines/show.js +46 -17
  63. package/dist/commands/messages/show.js +3 -10
  64. package/dist/commands/shares/get.js +2 -9
  65. package/dist/commands/shares/list.js +1 -9
  66. package/dist/commands/shortcuts/show.js +4 -11
  67. package/dist/commands/skills/local.js +6 -0
  68. package/dist/commands/spaces/actions/run.js +3 -4
  69. package/dist/commands/spaces/briefs/get.js +2 -9
  70. package/dist/commands/spaces/briefs/list.js +1 -9
  71. package/dist/commands/spaces/conversations.js +1 -9
  72. package/dist/commands/spaces/heartbeats/list.js +1 -9
  73. package/dist/commands/spaces/heartbeats/status.js +2 -9
  74. package/dist/commands/tasks/activity.js +1 -10
  75. package/dist/commands/tasks/approvals/list.js +1 -9
  76. package/dist/commands/tasks/attachments/list.js +1 -9
  77. package/dist/commands/tasks/comments/list.js +1 -9
  78. package/dist/commands/tasks/dri-status.js +2 -9
  79. package/dist/commands/tasks/events/append.js +9 -7
  80. package/dist/commands/tasks/execution-timeline.js +1 -9
  81. package/dist/commands/tasks/lifecycle/detach.js +26 -5
  82. package/dist/commands/tasks/monitor.js +1 -9
  83. package/dist/commands/tasks/result/submit.js +9 -0
  84. package/dist/commands/tasks/runs.js +1 -9
  85. package/dist/commands/tasks/show.js +5 -12
  86. package/dist/commands/token/list.d.ts +8 -0
  87. package/dist/commands/token/list.js +51 -6
  88. package/dist/help.js +8 -12
  89. package/dist/lib/agent-home-workspace.d.ts +19 -0
  90. package/dist/lib/agent-home-workspace.js +39 -0
  91. package/dist/lib/agent-resolver.js +2 -2
  92. package/dist/lib/agentic-stream.d.ts +22 -1
  93. package/dist/lib/agentic-stream.js +48 -7
  94. package/dist/lib/cli-installers.d.ts +84 -2
  95. package/dist/lib/cli-installers.js +146 -13
  96. package/dist/lib/command-miss.d.ts +22 -0
  97. package/dist/lib/command-miss.js +78 -0
  98. package/dist/lib/daemon-setup.js +2 -2
  99. package/dist/lib/daemon-target.d.ts +14 -0
  100. package/dist/lib/daemon-target.js +32 -0
  101. package/dist/lib/daemonBroker.js +1 -1
  102. package/dist/lib/daemonHandoff.js +1 -1
  103. package/dist/lib/dedicated-lease-command.js +4 -1
  104. package/dist/lib/dedicated-machines.d.ts +8 -1
  105. package/dist/lib/dedicated-machines.js +22 -6
  106. package/dist/lib/dedicated-wait.js +13 -0
  107. package/dist/lib/exec-runtime-binary.js +2 -2
  108. package/dist/lib/followups.d.ts +18 -0
  109. package/dist/lib/followups.js +14 -1
  110. package/dist/lib/format.d.ts +11 -0
  111. package/dist/lib/format.js +21 -0
  112. package/dist/lib/harness-labels.d.ts +39 -0
  113. package/dist/lib/harness-labels.js +60 -0
  114. package/dist/lib/harnesses.d.ts +6 -0
  115. package/dist/lib/instruction-input.d.ts +13 -0
  116. package/dist/lib/instruction-input.js +20 -1
  117. package/dist/lib/label-ref.d.ts +8 -1
  118. package/dist/lib/label-ref.js +12 -2
  119. package/dist/lib/local-skills.d.ts +17 -1
  120. package/dist/lib/local-skills.js +20 -2
  121. package/dist/lib/login.js +5 -5
  122. package/dist/lib/machines.d.ts +6 -0
  123. package/dist/lib/machines.js +68 -2
  124. package/dist/lib/option-hint.d.ts +15 -0
  125. package/dist/lib/option-hint.js +75 -0
  126. package/dist/lib/runtime-tool-support.d.ts +34 -0
  127. package/dist/lib/runtime-tool-support.js +77 -0
  128. package/dist/lib/share-rows.d.ts +17 -0
  129. package/dist/lib/share-rows.js +35 -0
  130. package/dist/lib/workspaces.d.ts +3 -0
  131. package/dist/lib/workspaces.js +38 -0
  132. package/dist/node_modules/@skrr-ai/data-provider/index.js +5029 -5007
  133. package/oclif.manifest.json +34255 -34142
  134. package/package.json +10 -5
@@ -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
  }
@@ -4,6 +4,7 @@ exports.isPingSuccess = isPingSuccess;
4
4
  const core_1 = require("@oclif/core");
5
5
  const base_command_1 = require("../../base-command");
6
6
  const harnesses_1 = require("../../lib/harnesses");
7
+ const cli_installers_1 = require("../../lib/cli-installers");
7
8
  /**
8
9
  * Is this machine actually reachable right now?
9
10
  *
@@ -84,6 +85,9 @@ class HarnessesPing extends base_command_1.BaseCommand {
84
85
  this.log(result.latencyMs != null
85
86
  ? `Answered in ${result.latencyMs}ms.`
86
87
  : 'Answered (no latency reported).');
88
+ // Answering proves the daemon is reachable, not that this harness can
89
+ // run a turn — a signed-out Devin answers too (OSK-9263).
90
+ await this.logReadiness(args.harness);
87
91
  }
88
92
  else {
89
93
  this.log(`Ping ${result.status}${result.error ? `: ${result.error}` : ''}.`);
@@ -104,5 +108,22 @@ class HarnessesPing extends base_command_1.BaseCommand {
104
108
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
105
109
  }
106
110
  }
111
+ /** Best-effort: never turns a successful ping into a failure. */
112
+ async logReadiness(harnessId) {
113
+ try {
114
+ const response = await (0, harnesses_1.getHarness)(harnessId);
115
+ const harness = response?.harness ??
116
+ response;
117
+ if (!harness?.daemonId)
118
+ return;
119
+ const readiness = await (0, cli_installers_1.readinessByProvider)(harness.daemonId, this.resolvedCredential);
120
+ const verdict = readiness?.get((0, cli_installers_1.readinessKey)(harness.provider));
121
+ if (verdict)
122
+ this.log(`Readiness: ${verdict.label} — ${verdict.detail}`);
123
+ }
124
+ catch {
125
+ // A readiness read is extra information; the ping already answered.
126
+ }
127
+ }
107
128
  }
108
129
  exports.default = HarnessesPing;
@@ -25,3 +25,25 @@ export default class HarnessesShow extends BaseCommand {
25
25
  * still falls through to the general answer rather than being guessed at.
26
26
  */
27
27
  export declare function describeWrongHarnessRef(ref: string): string;
28
+ type HarnessRefResolution = {
29
+ kind: 'harness';
30
+ id: string;
31
+ } | {
32
+ kind: 'message';
33
+ message: string;
34
+ } | {
35
+ kind: 'none';
36
+ };
37
+ /**
38
+ * Resolve a reference that is not a full harness id against this account's
39
+ * rows: a unique id prefix or a unique name selects the harness; a machine uuid
40
+ * prefix or an ambiguous match explains itself. `none` falls through to the
41
+ * shape-based message.
42
+ */
43
+ export declare function resolveHarnessRef(ref: string): Promise<HarnessRefResolution>;
44
+ export declare function pickHarnessRef(rows: Array<{
45
+ _id: string;
46
+ name?: string;
47
+ machineUuid?: string;
48
+ }>, ref: string): HarnessRefResolution;
49
+ export {};
@@ -1,11 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.describeWrongHarnessRef = describeWrongHarnessRef;
4
+ exports.resolveHarnessRef = resolveHarnessRef;
5
+ exports.pickHarnessRef = pickHarnessRef;
4
6
  const core_1 = require("@oclif/core");
5
7
  const base_command_1 = require("../../base-command");
6
8
  const harness_provider_input_1 = require("../../lib/harness-provider-input");
7
9
  const harnesses_1 = require("../../lib/harnesses");
10
+ const harness_labels_1 = require("../../lib/harness-labels");
11
+ const cli_installers_1 = require("../../lib/cli-installers");
8
12
  const api_fetch_1 = require("../../lib/api-fetch");
13
+ const data_provider_1 = require("@skrr-ai/data-provider");
9
14
  class HarnessesShow extends base_command_1.BaseCommand {
10
15
  static description = 'Show one harness — one provider on one machine';
11
16
  static examples = [
@@ -29,20 +34,30 @@ class HarnessesShow extends base_command_1.BaseCommand {
29
34
  }
30
35
  catch (err) {
31
36
  // The server answers a malformed id with "Invalid harness ID", which says
32
- // nothing about what a valid one IS or where to get one. The sibling
33
- // `machines show` already sets the expectation by accepting what its own
34
- // list prints, so failing here with no pointer is the odd one out.
35
- // The readable text lives in `body`, not `message` — `message` is
36
- // "HTTP 400 Bad Request — GET /api/harnesses/<id>". Any 400 on this
37
- // lookup is the id being wrong, so branch on the status.
38
- if (err instanceof api_fetch_1.ApiFetchError && err.status === 400) {
39
- this.error(`Not a harness id: ${args.harness}\n${describeWrongHarnessRef(args.harness)}`, {
40
- exit: 1,
41
- });
37
+ // nothing about what a valid one IS or where to get one. The readable
38
+ // text lives in `body`, not `message`, and any 400 on this lookup is the
39
+ // id being wrong, so branch on the status.
40
+ if (!(err instanceof api_fetch_1.ApiFetchError && err.status === 400)) {
41
+ this.handleApiError(err);
42
+ return;
43
+ }
44
+ // Before refusing, try what other commands PRINT: the head of a harness
45
+ // id, a harness name, or a machine uuid. `machines show` accepts prefixes
46
+ // and names; this sibling answered the same input with a generic "24
47
+ // hexadecimal characters" (OSK-9313).
48
+ const resolved = await resolveHarnessRef(args.harness);
49
+ if (resolved.kind !== 'harness') {
50
+ const why = resolved.kind === 'message' ? resolved.message : describeWrongHarnessRef(args.harness);
51
+ this.error(`Not a harness id: ${args.harness}\n${why}`, { exit: 1 });
52
+ return;
53
+ }
54
+ try {
55
+ response = await (0, harnesses_1.getHarness)(resolved.id);
56
+ }
57
+ catch (retryErr) {
58
+ this.handleApiError(retryErr);
42
59
  return;
43
60
  }
44
- this.handleApiError(err);
45
- return;
46
61
  }
47
62
  const stored = response?.harness;
48
63
  // Canonical spelling in every output, whichever spelling the row was stored under.
@@ -53,13 +68,30 @@ class HarnessesShow extends base_command_1.BaseCommand {
53
68
  this.error(`No harness ${args.harness}.`, { exit: 1 });
54
69
  return;
55
70
  }
71
+ // `status online` is a heartbeat, not "this harness can run a turn": a
72
+ // Devin that is installed and not signed in read as fully usable (OSK-9263).
73
+ // Asked of the daemon, so only for a live local row; best-effort.
74
+ const readiness = harness.daemonId &&
75
+ harness.harnessLocation === 'local' &&
76
+ harness.status === 'online' &&
77
+ !harness.superseded
78
+ ? await (0, cli_installers_1.readinessByProvider)(harness.daemonId, this.resolvedCredential)
79
+ : null;
80
+ const verdict = readiness?.get((0, cli_installers_1.readinessKey)(harness.provider));
56
81
  if (flags.json) {
57
- this.log(JSON.stringify(harness, null, 2));
82
+ // `authStatus` is the DAEMON's sign-in to skrr, the same on every row of a
83
+ // daemon, so a script read a signed-out Devin as "ok" (OSK-9263/OSK-9265
84
+ // verifiers). `readiness` is the per-provider answer the text prints;
85
+ // null when it was not asked (not a live local row) or not reported.
86
+ this.log(JSON.stringify({ ...harness, readiness: verdict ?? null }, null, 2));
58
87
  return;
59
88
  }
60
- this.log(`${harness.name || harness._id}`);
89
+ // The same rule the list applies (OSK-9649): a no-engine placeholder's
90
+ // stored name is "oversky — <machine>", which headed this view with a
91
+ // provider name that is not one.
92
+ this.log(`${harness.name ? (0, data_provider_1.canonicalizeHarnessLabel)(harness.name) : harness._id}`);
61
93
  this.log(` id ${harness._id}`);
62
- this.log(` provider ${harness.provider}`);
94
+ this.log(` provider ${(0, harness_labels_1.harnessProviderDescription)(harness.provider)}`);
63
95
  // `harnessLocation` is the difference between "your laptop" and "a cloud row",
64
96
  // and it decides which verbs apply: ping and claudeEngine are local-only.
65
97
  // When YOUR alias is covering the shared name, say so and show both. The
@@ -67,11 +99,11 @@ class HarnessesShow extends base_command_1.BaseCommand {
67
99
  // value `harnesses update --name` just wrote appears on no surface at all,
68
100
  // and you cannot tell you are reading your own label rather than the one
69
101
  // everyone else sees.
70
- if (harness.aliased && harness.sharedName && harness.sharedName !== harness.name) {
102
+ if ((0, harness_labels_1.isAliasCoveringSharedName)(harness)) {
71
103
  this.log(` shared name ${harness.sharedName} (your alias is showing instead)`);
72
104
  }
73
105
  this.log(` mode ${harness.harnessLocation ?? 'unknown'}`);
74
- this.log(` status ${harness.status ?? 'unknown'}`);
106
+ this.log(` status ${(0, harness_labels_1.harnessStatusLabel)(harness)}`);
75
107
  // Only for the claude provider. The daemon stamps `claudeEngine` on every
76
108
  // harness it registers, so printing it whenever it is set labels a
77
109
  // first-party harness or `codex` row with how CLAUDE Code would run — a fact about a
@@ -81,6 +113,12 @@ class HarnessesShow extends base_command_1.BaseCommand {
81
113
  }
82
114
  if (harness.daemonId)
83
115
  this.log(` daemon ${harness.daemonId}`);
116
+ if (verdict) {
117
+ this.log(` readiness ${verdict.label} — ${verdict.detail}`);
118
+ }
119
+ else if (readiness) {
120
+ this.log(' readiness not reported — this daemon does not track credentials for this provider');
121
+ }
84
122
  if (harness.lastSeenAt)
85
123
  this.log(` last seen ${harness.lastSeenAt}`);
86
124
  }
@@ -105,6 +143,14 @@ function describeWrongHarnessRef(ref) {
105
143
  if (/^daemon_/.test(ref)) {
106
144
  return `That is a daemon id. \`skrr machines show ${ref}\` takes those. ${seeIds}`;
107
145
  }
146
+ if (/^(dedicated-runtime-|hosted-machine-|hosted-runtime-)/.test(ref)) {
147
+ // Printed in the DAEMON ID column for a rented machine, so it is the same
148
+ // redirect as `daemon_…`, not a malformed harness id (OSK-9313).
149
+ return `That is a daemon id. \`skrr machines show ${ref}\` takes those. ${seeIds}`;
150
+ }
151
+ if (/^sbs_/.test(ref)) {
152
+ return `That is a hosted machine id. \`skrr machines show ${ref}\` takes those. ${seeIds}`;
153
+ }
108
154
  if (/^machine:/.test(ref)) {
109
155
  return `That is a machine id. \`skrr machines show ${ref}\` takes those. ${seeIds}`;
110
156
  }
@@ -118,3 +164,56 @@ function describeWrongHarnessRef(ref) {
118
164
  }
119
165
  return `A harness id is 24 hexadecimal characters. ${seeIds}`;
120
166
  }
167
+ /**
168
+ * Resolve a reference that is not a full harness id against this account's
169
+ * rows: a unique id prefix or a unique name selects the harness; a machine uuid
170
+ * prefix or an ambiguous match explains itself. `none` falls through to the
171
+ * shape-based message.
172
+ */
173
+ async function resolveHarnessRef(ref) {
174
+ if (!ref.trim())
175
+ return { kind: 'none' };
176
+ let rows;
177
+ try {
178
+ const res = await (0, api_fetch_1.apiFetch)('/api/harnesses');
179
+ rows = res.harnesses ?? [];
180
+ }
181
+ catch {
182
+ return { kind: 'none' };
183
+ }
184
+ return pickHarnessRef(rows, ref);
185
+ }
186
+ function pickHarnessRef(rows, ref) {
187
+ const typed = ref.trim();
188
+ const needle = typed.toLowerCase();
189
+ const byId = rows.filter((r) => r._id.toLowerCase().startsWith(needle));
190
+ if (byId.length === 1)
191
+ return { kind: 'harness', id: byId[0]._id };
192
+ const byName = rows.filter((r) => typeof r.name === 'string' &&
193
+ (r.name.toLowerCase() === needle ||
194
+ (0, data_provider_1.canonicalizeHarnessLabel)(r.name).toLowerCase() === needle));
195
+ if (byName.length === 1)
196
+ return { kind: 'harness', id: byName[0]._id };
197
+ const ambiguous = byId.length > 1 ? byId : byName;
198
+ if (ambiguous.length > 1) {
199
+ const shown = ambiguous
200
+ .slice(0, 5)
201
+ .map((r) => `${r._id}${r.name ? ` (${(0, data_provider_1.canonicalizeHarnessLabel)(r.name)})` : ''}`);
202
+ return {
203
+ kind: 'message',
204
+ message: `"${typed}" matches ${ambiguous.length} harnesses: ${shown.join(', ')}${ambiguous.length > shown.length ? `, and ${ambiguous.length - shown.length} more` : ''}. Use a longer prefix or the full id.`,
205
+ };
206
+ }
207
+ const machine = rows.find((r) => {
208
+ const uuid = (r.machineUuid ?? '').toLowerCase();
209
+ const tail = uuid.includes(':') ? uuid.slice(uuid.indexOf(':') + 1) : uuid;
210
+ return uuid.startsWith(needle) || tail.startsWith(needle);
211
+ });
212
+ if (machine) {
213
+ return {
214
+ kind: 'message',
215
+ message: `That is a machine. \`skrr machines show ${typed}\` takes those; its harnesses are listed there.`,
216
+ };
217
+ }
218
+ return { kind: 'none' };
219
+ }