@skrr-ai/cli 0.1.41 → 0.1.43

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 (79) hide show
  1. package/dist/commands/balance/overage.js +4 -3
  2. package/dist/commands/balance/plan.js +10 -6
  3. package/dist/commands/balance/usage/events.js +3 -2
  4. package/dist/commands/balance/usage.js +2 -1
  5. package/dist/commands/browser/install.js +3 -2
  6. package/dist/commands/browser/uninstall.js +3 -2
  7. package/dist/commands/code/install.js +6 -6
  8. package/dist/commands/followups/remind.js +4 -0
  9. package/dist/commands/harnesses/install.d.ts +29 -0
  10. package/dist/commands/harnesses/install.js +153 -25
  11. package/dist/commands/harnesses/installers.d.ts +10 -0
  12. package/dist/commands/harnesses/installers.js +31 -0
  13. package/dist/commands/harnesses/list.js +3 -2
  14. package/dist/commands/skills/import-as-actions.js +6 -15
  15. package/dist/commands/tasks/attachments/set-role.d.ts +24 -0
  16. package/dist/commands/tasks/attachments/set-role.js +60 -0
  17. package/dist/commands/tasks/attachments/upload.d.ts +1 -0
  18. package/dist/commands/tasks/attachments/upload.js +18 -52
  19. package/dist/commands/tasks/comments/add.d.ts +1 -0
  20. package/dist/commands/tasks/comments/add.js +67 -2
  21. package/dist/commands/tasks/complete.d.ts +1 -0
  22. package/dist/commands/tasks/complete.js +69 -1
  23. package/dist/commands/tasks/result/submit.d.ts +3 -0
  24. package/dist/commands/tasks/result/submit.js +78 -5
  25. package/dist/commands/tasks/update.js +4 -2
  26. package/dist/commands/tasks/updates/add.d.ts +1 -0
  27. package/dist/commands/tasks/updates/add.js +63 -0
  28. package/dist/lib/api-fetch.js +7 -2
  29. package/dist/lib/cli-installers.d.ts +63 -4
  30. package/dist/lib/cli-installers.js +98 -8
  31. package/dist/lib/dedicated-machines.js +4 -2
  32. package/dist/lib/file-mime.js +1 -1
  33. package/dist/lib/first-party-harness-agent.js +6 -5
  34. package/dist/lib/first-party-harness-doctor.js +15 -32
  35. package/dist/lib/first-party-harness-managed.d.ts +9 -4
  36. package/dist/lib/first-party-harness-managed.js +13 -10
  37. package/dist/lib/first-party-harness.d.ts +25 -21
  38. package/dist/lib/first-party-harness.js +40 -27
  39. package/dist/lib/followups.d.ts +2 -0
  40. package/dist/lib/harness-provider-input.d.ts +18 -12
  41. package/dist/lib/harness-provider-input.js +18 -12
  42. package/dist/lib/harness-tiers.d.ts +4 -3
  43. package/dist/lib/harness-tiers.js +4 -3
  44. package/dist/lib/html-text.js +25 -0
  45. package/dist/lib/local-skills.d.ts +27 -0
  46. package/dist/lib/local-skills.js +38 -0
  47. package/dist/lib/session-task-endpoints.d.ts +1 -1
  48. package/dist/lib/session-task-endpoints.js +3 -0
  49. package/dist/lib/task-asset-upload.d.ts +84 -0
  50. package/dist/lib/task-asset-upload.js +374 -0
  51. package/dist/lib/task-closure.d.ts +28 -0
  52. package/dist/lib/task-closure.js +37 -0
  53. package/dist/lib/tasks.d.ts +8 -0
  54. package/dist/lib/tasks.js +16 -0
  55. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +113 -39
  56. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +148 -71
  57. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.d.ts +21 -33
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.js +24 -50
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.d.ts +19 -7
  60. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.js +26 -10
  61. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessTrust.d.ts +9 -5
  62. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessTrust.js +9 -5
  63. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
  64. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +1 -13
  65. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +113 -39
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +146 -70
  67. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.d.ts +21 -33
  68. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.js +24 -49
  69. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.d.ts +19 -7
  70. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.js +27 -11
  71. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessTrust.d.ts +9 -5
  72. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessTrust.js +9 -5
  73. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
  74. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -4
  75. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  76. package/dist/node_modules/@skrr-ai/data-provider/index.js +4025 -3938
  77. package/dist/node_modules/@skrr-ai/data-provider/package.json +1 -1
  78. package/oclif.manifest.json +12853 -12721
  79. package/package.json +2 -2
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const core_1 = require("@oclif/core");
4
+ const auth_core_1 = require("@skrr-ai/auth-core");
4
5
  const base_command_1 = require("../../base-command");
5
6
  const balance_1 = require("../../lib/balance");
6
7
  class BalanceOverage extends base_command_1.BaseCommand {
7
- static description = 'Enable or disable personal Sky Code overage after included allowance is exhausted';
8
+ static description = `Enable or disable personal ${auth_core_1.FIRST_PARTY_HARNESS.displayName} overage after included allowance is exhausted`;
8
9
  static examples = [
9
10
  '<%= config.bin %> balance overage enabled',
10
11
  '<%= config.bin %> balance overage disabled',
@@ -12,7 +13,7 @@ class BalanceOverage extends base_command_1.BaseCommand {
12
13
  ];
13
14
  static args = {
14
15
  state: core_1.Args.string({
15
- description: 'Whether personal stored balance may pay for Sky Code overage',
16
+ description: `Whether personal stored balance may pay for ${auth_core_1.FIRST_PARTY_HARNESS.displayName} overage`,
16
17
  required: true,
17
18
  options: ['enabled', 'disabled'],
18
19
  ignoreStdin: true,
@@ -34,7 +35,7 @@ class BalanceOverage extends base_command_1.BaseCommand {
34
35
  this.log(JSON.stringify({ ownerType: 'user', overageEnabled: result?.enabled === true }, null, 2));
35
36
  return;
36
37
  }
37
- this.log(`Sky Code overage ${result?.enabled ? 'enabled' : 'disabled'} for your personal billing account.`);
38
+ this.log(`${auth_core_1.FIRST_PARTY_HARNESS.displayName} overage ${result?.enabled ? 'enabled' : 'disabled'} for your personal billing account.`);
38
39
  this.log(result?.enabled
39
40
  ? 'New admissions may use stored balance after included allowance is exhausted.'
40
41
  : 'New admissions will stop when included allowance is exhausted.');
@@ -2,12 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderSkyCodePlan = renderSkyCodePlan;
4
4
  const core_1 = require("@oclif/core");
5
+ const auth_core_1 = require("@skrr-ai/auth-core");
5
6
  const data_provider_1 = require("@skrr-ai/data-provider");
6
7
  const base_command_1 = require("../../base-command");
7
8
  const balance_1 = require("../../lib/balance");
8
9
  const web_url_1 = require("../../lib/web-url");
9
10
  class BalancePlan extends base_command_1.BaseCommand {
10
- static description = 'Show your Sky Code plan, renewal state, available tiers, and the matching lifecycle page';
11
+ static description = `Show your ${auth_core_1.FIRST_PARTY_HARNESS.displayName} plan, renewal state, available tiers, and the matching lifecycle page`;
11
12
  static examples = [
12
13
  '<%= config.bin %> balance plan',
13
14
  '<%= config.bin %> balance plan --json',
@@ -33,7 +34,7 @@ class BalancePlan extends base_command_1.BaseCommand {
33
34
  }
34
35
  exports.default = BalancePlan;
35
36
  function renderSkyCodePlan(subscription, lifecycleUrl, log) {
36
- log(`Sky Code plan — ${subscription.tierName}`);
37
+ log(`${auth_core_1.FIRST_PARTY_HARNESS.displayName} plan — ${subscription.tierName}`);
37
38
  log(` Status ${subscription.status}`);
38
39
  log(` Price $${subscription.monthlyPriceUsd}/month`);
39
40
  log(` Current period ${subscription.currentPeriodStart ?? 'not started'}`);
@@ -43,10 +44,13 @@ function renderSkyCodePlan(subscription, lifecycleUrl, log) {
43
44
  log(` Concurrent sessions ${subscription.concurrentSessions ?? 'not reported'}`);
44
45
  log(` Requests per minute ${subscription.rateLimitPerMin}`);
45
46
  log('');
46
- log('Available Sky Code plans:');
47
- for (const key of data_provider_1.SKY_CODE_PLAN_KEYS.filter((plan) => plan !== 'free')) {
48
- const plan = data_provider_1.SKY_CODE_PLAN_CATALOG[key];
49
- log(` ${plan.displayName.padEnd(18)} $${plan.monthlyPriceUsd}/month · ` +
47
+ log(`Available ${auth_core_1.FIRST_PARTY_HARNESS.displayName} plans:`);
48
+ const paidPlans = data_provider_1.SKY_CODE_PLAN_KEYS.filter((plan) => plan !== 'free').map((key) => data_provider_1.SKY_CODE_PLAN_CATALOG[key]);
49
+ // Padded to the longest name rather than a fixed width: plan names embed the
50
+ // product's display name, whose length is not this renderer's to assume.
51
+ const nameWidth = Math.max(...paidPlans.map((plan) => plan.displayName.length)) + 1;
52
+ for (const plan of paidPlans) {
53
+ log(` ${plan.displayName.padEnd(nameWidth)} $${plan.monthlyPriceUsd}/month · ` +
50
54
  `${(0, data_provider_1.usageMicrosToDisplayCredits)(plan.includedUsageMicros).toLocaleString()} monthly credits · ` +
51
55
  `${(0, data_provider_1.usageMicrosToDisplayCredits)(plan.fiveHourUsageMicros).toLocaleString()} per 5 hours · ` +
52
56
  `${plan.concurrentSessions} sessions · ${plan.requestsPerMinute} RPM`);
@@ -2,10 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderSkyCodeUsageEvents = renderSkyCodeUsageEvents;
4
4
  const core_1 = require("@oclif/core");
5
+ const auth_core_1 = require("@skrr-ai/auth-core");
5
6
  const base_command_1 = require("../../../base-command");
6
7
  const balance_1 = require("../../../lib/balance");
7
8
  class BalanceUsageEvents extends base_command_1.BaseCommand {
8
- static description = 'List the Sky Code charges and run/session attribution behind aggregate allowance usage';
9
+ static description = `List the ${auth_core_1.FIRST_PARTY_HARNESS.displayName} charges and run/session attribution behind aggregate allowance usage`;
9
10
  static examples = [
10
11
  '<%= config.bin %> balance usage events',
11
12
  '<%= config.bin %> balance usage events --limit 20 --json',
@@ -44,7 +45,7 @@ class BalanceUsageEvents extends base_command_1.BaseCommand {
44
45
  }
45
46
  exports.default = BalanceUsageEvents;
46
47
  function renderSkyCodeUsageEvents(events, log) {
47
- log(`Sky Code usage events · ${events.ownerType} ${events.ownerId}`);
48
+ log(`${auth_core_1.FIRST_PARTY_HARNESS.displayName} usage events · ${events.ownerType} ${events.ownerId}`);
48
49
  log(` Window ${events.from} to ${events.to}`);
49
50
  if (events.items.length === 0) {
50
51
  log(' No usage events in this window.');
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderSkyCodeUsage = renderSkyCodeUsage;
4
4
  const core_1 = require("@oclif/core");
5
+ const auth_core_1 = require("@skrr-ai/auth-core");
5
6
  const base_command_1 = require("../../base-command");
6
7
  const balance_1 = require("../../lib/balance");
7
8
  /**
@@ -40,7 +41,7 @@ class BalanceUsage extends base_command_1.BaseCommand {
40
41
  }
41
42
  exports.default = BalanceUsage;
42
43
  function renderSkyCodeUsage(usage, log) {
43
- log(`Sky Code ${usage.plan ?? 'current'} plan · ${usage.ownerType} ${usage.ownerId}`);
44
+ log(`${auth_core_1.FIRST_PARTY_HARNESS.displayName} ${usage.plan ?? 'current'} plan · ${usage.ownerType} ${usage.ownerId}`);
44
45
  log(` Monthly used ${(0, balance_1.formatCreditsWithUsd)(usage.monthly.usedUsageMicros)}`);
45
46
  log(` Monthly reserved ${(0, balance_1.formatCreditsWithUsd)(usage.monthly.reservedUsageMicros)}`);
46
47
  log(` Monthly remaining ${(0, balance_1.formatCreditsWithUsd)(usage.monthly.remainingUsageMicros)}`);
@@ -23,8 +23,9 @@ class BrowserInstall extends core_1.Command {
23
23
  async run() {
24
24
  const { flags } = await this.parse(BrowserInstall);
25
25
  const args = ['install'];
26
- // Any spelling of the first-party harness is accepted here and handed to the
27
- // daemon in the spelling every installed daemon understands.
26
+ // Any ACCEPTED spelling of the first-party harness is handed to the daemon in
27
+ // the wire spelling; anything else — a retired spelling included — is forwarded
28
+ // unchanged for the daemon's own parser to accept or refuse.
28
29
  if (flags.harness)
29
30
  args.push('--harness', (0, harness_provider_input_1.daemonHarnessArgument)(flags.harness));
30
31
  if (flags.config)
@@ -23,8 +23,9 @@ class BrowserUninstall extends core_1.Command {
23
23
  async run() {
24
24
  const { flags } = await this.parse(BrowserUninstall);
25
25
  const args = ['uninstall'];
26
- // Any spelling of the first-party harness is accepted here and handed to the
27
- // daemon in the spelling every installed daemon understands.
26
+ // Any ACCEPTED spelling of the first-party harness is handed to the daemon in
27
+ // the wire spelling; anything else — a retired spelling included — is forwarded
28
+ // unchanged for the daemon's own parser to accept or refuse.
28
29
  if (flags.harness)
29
30
  args.push('--harness', (0, harness_provider_input_1.daemonHarnessArgument)(flags.harness));
30
31
  if (flags.config)
@@ -4,7 +4,6 @@ const core_1 = require("@oclif/core");
4
4
  const auth_core_1 = require("@skrr-ai/auth-core");
5
5
  const base_command_1 = require("../../base-command");
6
6
  const api_fetch_1 = require("../../lib/api-fetch");
7
- const daemon_ref_1 = require("../../lib/daemon-ref");
8
7
  const daemon_target_1 = require("../../lib/daemon-target");
9
8
  const first_party_harness_1 = require("../../lib/first-party-harness");
10
9
  /**
@@ -84,11 +83,11 @@ class CodeInstall extends base_command_1.BaseCommand {
84
83
  // survive in mine.
85
84
  const reason = await this.installerNote(daemonId);
86
85
  if (reason) {
87
- this.log(`Nothing to do on ${(0, daemon_ref_1.formatDaemonRef)(daemonId)} — ${reason}`);
86
+ this.log(`Nothing to do on ${daemonId} — ${reason}`);
88
87
  return;
89
88
  }
90
89
  this.failWithCliError({
91
- message: `The daemon on ${(0, daemon_ref_1.formatDaemonRef)(daemonId)} accepted the request and returned no ` +
90
+ message: `The daemon on ${daemonId} accepted the request and returned no ` +
92
91
  'install job, and does not say why. Nothing has been shown to start. ' +
93
92
  `Check what it offers with \`${this.config.bin} harnesses installers --daemon ${daemonId}\`, and ` +
94
93
  `\`${this.config.bin} code doctor\` here once you expect it to have landed.`,
@@ -104,7 +103,7 @@ class CodeInstall extends base_command_1.BaseCommand {
104
103
  this.log(JSON.stringify({ daemonId, ...started }, null, 2));
105
104
  return;
106
105
  }
107
- this.log(`Install started on ${(0, daemon_ref_1.formatDaemonRef)(daemonId)} — job ${jobId}.`);
106
+ this.log(`Install started on ${daemonId} — job ${jobId}.`);
108
107
  return;
109
108
  }
110
109
  const job = await this.followJob(daemonId, jobId, flags.json === true);
@@ -202,8 +201,9 @@ class CodeInstall extends base_command_1.BaseCommand {
202
201
  async installerNote(daemonId) {
203
202
  try {
204
203
  const res = await (0, api_fetch_1.apiFetch)(`/api/daemons/${encodeURIComponent(daemonId)}/cli-installers`, { credential: this.resolvedCredential });
205
- // Any spelling: a daemon lists the provider the way it registered it, and
206
- // one that predates or postdates a rename spells it differently.
204
+ // Any accepted spelling: a daemon lists the provider the way it registered it,
205
+ // and during a rename's alias window one that predates or postdates the flip
206
+ // spells it differently.
207
207
  const row = (res.providers ?? []).find((p) => (0, auth_core_1.isFirstPartyHarnessProvider)(p.provider));
208
208
  const reason = row?.installDisabledReason?.trim();
209
209
  if (!reason)
@@ -95,6 +95,10 @@ class FollowupsRemind extends base_command_1.BaseCommand {
95
95
  return;
96
96
  }
97
97
  const row = response.followup;
98
+ if (response.reused && response.rescheduled) {
99
+ this.log(`Moved: ${row?.id} now fires at ${row?.nextRunAt ?? at} — the same reminder already existed, so it was rescheduled rather than duplicated.`);
100
+ return;
101
+ }
98
102
  if (response.reused) {
99
103
  this.log(`Already armed: ${row?.id} — the same reminder exists and was not duplicated.`);
100
104
  return;
@@ -1,4 +1,5 @@
1
1
  import { BaseCommand } from '../../base-command';
2
+ import { type ManagedCliProviderState } from '../../lib/cli-installers';
2
3
  /**
3
4
  * Install one managed coding-agent CLI on a machine.
4
5
  *
@@ -33,9 +34,37 @@ export default class HarnessesInstall extends BaseCommand {
33
34
  static flags: {
34
35
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
35
36
  daemon: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
37
+ update: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
36
38
  };
37
39
  run(): Promise<void>;
40
+ /**
41
+ * Update an installed CLI in place (OSK-10096).
42
+ *
43
+ * Asks the inventory first rather than posting blind: a daemon older than
44
+ * managed updates has no handler for the request and would leave it to time
45
+ * out, and a CLI that updates some other way (the copy inside Devin.app) has a
46
+ * reason the daemon already words. Both are answered before anything runs.
47
+ */
48
+ private update;
49
+ /**
50
+ * Start an install or update, and survive a gateway that cut the request off.
51
+ *
52
+ * The relay holds the request while the daemon answers, and a gateway can end
53
+ * it first: an update returned HTTP 504 after 21.9s while the job it started
54
+ * ran to success, and the caller was told the outcome was unknown (OSK-10096
55
+ * verifier). The daemon is the only party that knows whether the request
56
+ * arrived, so on a server-side failure we ask it, and follow the job it
57
+ * started rather than reporting a result we do not have.
58
+ */
59
+ private start;
60
+ /** Follow a job to its end, or report why it could not be followed (and return nothing). */
61
+ private follow;
38
62
  /** What the daemon says about this provider right now, best effort. */
39
63
  private installerNote;
40
64
  private daemonIdFor;
41
65
  }
66
+ /**
67
+ * The next step after "Already installed.": how to update it, or where its
68
+ * update comes from — or nothing from a daemon that cannot say (OSK-10096).
69
+ */
70
+ export declare function updateHint(provider: string, state: ManagedCliProviderState | undefined, bin: string): string | null;
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.updateHint = updateHint;
3
4
  const core_1 = require("@oclif/core");
4
5
  const data_provider_1 = require("@skrr-ai/data-provider");
5
6
  const base_command_1 = require("../../base-command");
6
7
  const cli_installers_1 = require("../../lib/cli-installers");
7
- const daemon_ref_1 = require("../../lib/daemon-ref");
8
8
  const daemon_target_1 = require("../../lib/daemon-target");
9
9
  /**
10
10
  * Install one managed coding-agent CLI on a machine.
@@ -32,10 +32,11 @@ const daemon_target_1 = require("../../lib/daemon-target");
32
32
  * come back as an opaque refusal.
33
33
  */
34
34
  class HarnessesInstall extends base_command_1.BaseCommand {
35
- static description = 'Install a managed coding-agent CLI (claude, codex, devin, …) on one of your machines';
35
+ static description = 'Install a managed coding-agent CLI (claude, codex, devin, …) on one of your machines, or update one already installed (--update)';
36
36
  static examples = [
37
37
  '<%= config.bin %> harnesses install devin',
38
38
  '<%= config.bin %> harnesses install codex --daemon daemon_07c086e3c249_cli_default',
39
+ '<%= config.bin %> harnesses install claude --update',
39
40
  ];
40
41
  static args = {
41
42
  provider: core_1.Args.string({
@@ -49,6 +50,9 @@ class HarnessesInstall extends base_command_1.BaseCommand {
49
50
  daemon: core_1.Flags.string({
50
51
  description: 'Which machine to install on. Defaults to your only connected daemon; required when several are connected.',
51
52
  }),
53
+ update: core_1.Flags.boolean({
54
+ description: "Update the CLI already installed, with that CLI's own updater. When a CLI updates some other way (e.g. inside its desktop app), the daemon says so instead.",
55
+ }),
52
56
  };
53
57
  async run() {
54
58
  this.requireAuth();
@@ -66,14 +70,13 @@ class HarnessesInstall extends base_command_1.BaseCommand {
66
70
  listActiveDaemons: () => (0, daemon_target_1.listConnectedDaemons)(this.resolvedCredential),
67
71
  });
68
72
  const daemonId = this.daemonIdFor(target);
69
- let start;
70
- try {
71
- start = await (0, cli_installers_1.startManagedCliInstall)(daemonId, provider, this.resolvedCredential);
72
- }
73
- catch (err) {
74
- this.handleApiError(err);
73
+ if (flags.update) {
74
+ await this.update(daemonId, provider, Boolean(flags.json));
75
75
  return;
76
76
  }
77
+ const start = await this.start(daemonId, provider, () => (0, cli_installers_1.startManagedCliInstall)(daemonId, provider, this.resolvedCredential), Boolean(flags.json));
78
+ if (!start)
79
+ return;
77
80
  const jobId = start.job?.id;
78
81
  if (!start.started || !jobId) {
79
82
  // The daemon declined and already knows why. Read its own reason off the
@@ -83,27 +86,22 @@ class HarnessesInstall extends base_command_1.BaseCommand {
83
86
  const reason = start.provider?.installDisabledReason?.trim() ||
84
87
  (await this.installerNote(daemonId, provider));
85
88
  this.log(reason ? `${provider}: ${reason}` : `${provider}: the daemon started no install job.`);
89
+ // "Already installed." used to be the end of the road: the product could
90
+ // install a CLI and then had no word about updating it (OSK-10096).
91
+ if (start.alreadyInstalled) {
92
+ const next = updateHint(provider, start.provider, this.config.bin);
93
+ if (next)
94
+ this.log(next);
95
+ }
86
96
  return;
87
97
  }
98
+ // The raw id: it is what `--daemon` takes, and a formatDaemonRef prefix of a
99
+ // `daemon_<hash>_…` id reads `d_daemon_4...` for every local daemon (OSK-10182).
88
100
  if (!flags.json)
89
- this.logToStderr(`Installing ${provider} on ${(0, daemon_ref_1.formatDaemonRef)(daemonId)}…`);
90
- const outcome = await (0, cli_installers_1.followManagedCliInstallJob)({
91
- daemonId,
92
- jobId,
93
- credential: this.resolvedCredential,
94
- onLogLine: flags.json ? undefined : (line) => this.logToStderr(` ${line}`),
95
- });
96
- if (outcome.kind === 'missing') {
97
- this.error(`The daemon no longer has install job ${jobId}.`, { exit: 1 });
98
- return;
99
- }
100
- if (outcome.kind === 'timeout') {
101
- // Not a failure: the daemon is still working, and calling this failed
102
- // would send someone to debug a download that is going fine.
103
- this.error(`Still installing after ${Math.round(cli_installers_1.INSTALL_MAX_WAIT_MS / 60000)} minutes — the daemon is still working on job ${jobId}. Check \`${this.config.bin} harnesses installers\` in a while.`, { exit: 1 });
101
+ this.logToStderr(`Installing ${provider} on ${daemonId}…`);
102
+ const job = await this.follow(daemonId, jobId, 'install', Boolean(flags.json));
103
+ if (!job)
104
104
  return;
105
- }
106
- const job = outcome.job;
107
105
  if (flags.json) {
108
106
  this.log(JSON.stringify({ daemonId, provider, job }, null, 2));
109
107
  if (job.status !== 'succeeded')
@@ -123,6 +121,123 @@ class HarnessesInstall extends base_command_1.BaseCommand {
123
121
  const state = await this.installerNote(daemonId, provider, true);
124
122
  this.log(`${provider} installed.${state ? ` ${state}` : ''}`);
125
123
  }
124
+ /**
125
+ * Update an installed CLI in place (OSK-10096).
126
+ *
127
+ * Asks the inventory first rather than posting blind: a daemon older than
128
+ * managed updates has no handler for the request and would leave it to time
129
+ * out, and a CLI that updates some other way (the copy inside Devin.app) has a
130
+ * reason the daemon already words. Both are answered before anything runs.
131
+ */
132
+ async update(daemonId, provider, json) {
133
+ let inventory;
134
+ try {
135
+ inventory = await (0, cli_installers_1.listCliInstallers)(daemonId, this.resolvedCredential);
136
+ }
137
+ catch (err) {
138
+ this.handleApiError(err);
139
+ return;
140
+ }
141
+ const row = (inventory.providers ?? []).find((p) => (0, data_provider_1.normalizeManagedCliProvider)(p.provider) === provider);
142
+ if (!row || row.updatable === undefined) {
143
+ this.failWithCliError({
144
+ message: `The daemon ${daemonId} (${inventory.daemonVersion ?? 'version not reported'}) cannot update ${provider}: it predates managed updates. ` +
145
+ `Update the daemon, then run this again — \`${this.config.bin} daemon status\` says how.`,
146
+ code: 'DAEMON_UPDATE_UNSUPPORTED',
147
+ exit: 1,
148
+ retryable: false,
149
+ });
150
+ return;
151
+ }
152
+ if (!row.updatable) {
153
+ this.failWithCliError({
154
+ message: `${provider}: ${row.updateDisabledReason?.trim() || 'the daemon cannot update it here.'}`,
155
+ code: 'CLI_NOT_UPDATABLE',
156
+ exit: 1,
157
+ retryable: false,
158
+ });
159
+ return;
160
+ }
161
+ const start = await this.start(daemonId, provider, () => (0, cli_installers_1.startManagedCliUpdate)(daemonId, provider, this.resolvedCredential), json);
162
+ if (!start)
163
+ return;
164
+ const jobId = start.job?.id;
165
+ if (!start.started || !jobId) {
166
+ this.log(start.alreadyRunning
167
+ ? `${provider}: an install or update is already running (job ${start.job?.id ?? 'unknown'}).`
168
+ : `${provider}: ${start.provider?.updateDisabledReason?.trim() || 'the daemon started no update job.'}`);
169
+ return;
170
+ }
171
+ if (!json) {
172
+ this.logToStderr(`Updating ${provider} ${row.version ? `${row.version} ` : ''}on ${daemonId}…`);
173
+ }
174
+ const job = await this.follow(daemonId, jobId, 'update', json);
175
+ if (!job)
176
+ return;
177
+ if (json) {
178
+ this.log(JSON.stringify({ daemonId, provider, job }, null, 2));
179
+ if (job.status !== 'succeeded')
180
+ process.exitCode = 1;
181
+ return;
182
+ }
183
+ if (job.status === 'failed') {
184
+ this.error(`Updating ${provider} failed: ${job.error || 'the daemon gave no reason.'}`, {
185
+ exit: 1,
186
+ });
187
+ return;
188
+ }
189
+ this.log((0, cli_installers_1.updateOutcomeLine)(job) ??
190
+ `${provider} updated${job.installedVersion ? ` to ${job.installedVersion}` : ''}.`);
191
+ }
192
+ /**
193
+ * Start an install or update, and survive a gateway that cut the request off.
194
+ *
195
+ * The relay holds the request while the daemon answers, and a gateway can end
196
+ * it first: an update returned HTTP 504 after 21.9s while the job it started
197
+ * ran to success, and the caller was told the outcome was unknown (OSK-10096
198
+ * verifier). The daemon is the only party that knows whether the request
199
+ * arrived, so on a server-side failure we ask it, and follow the job it
200
+ * started rather than reporting a result we do not have.
201
+ */
202
+ async start(daemonId, provider, begin, json) {
203
+ try {
204
+ return await begin();
205
+ }
206
+ catch (err) {
207
+ const status = err.status ?? 0;
208
+ if (status >= 500 || status === 408) {
209
+ const job = await (0, cli_installers_1.jobStartedDespiteTimeout)(daemonId, provider, this.resolvedCredential);
210
+ if (job?.id) {
211
+ if (!json) {
212
+ this.logToStderr(`The request to ${daemonId} did not come back (HTTP ${status}), but the daemon started job ${job.id}. Following it.`);
213
+ }
214
+ return { started: true, job };
215
+ }
216
+ }
217
+ this.handleApiError(err);
218
+ return undefined;
219
+ }
220
+ }
221
+ /** Follow a job to its end, or report why it could not be followed (and return nothing). */
222
+ async follow(daemonId, jobId, verb, json) {
223
+ const outcome = await (0, cli_installers_1.followManagedCliInstallJob)({
224
+ daemonId,
225
+ jobId,
226
+ credential: this.resolvedCredential,
227
+ onLogLine: json ? undefined : (line) => this.logToStderr(` ${line}`),
228
+ });
229
+ if (outcome.kind === 'missing') {
230
+ this.error(`The daemon no longer has ${verb} job ${jobId}.`, { exit: 1 });
231
+ return undefined;
232
+ }
233
+ if (outcome.kind === 'timeout') {
234
+ // Not a failure: the daemon is still working, and calling this failed
235
+ // would send someone to debug a download that is going fine.
236
+ this.error(`Still ${verb === 'install' ? 'installing' : 'updating'} after ${Math.round(cli_installers_1.INSTALL_MAX_WAIT_MS / 60000)} minutes — the daemon is still working on job ${jobId}. Check \`${this.config.bin} harnesses installers\` in a while.`, { exit: 1 });
237
+ return undefined;
238
+ }
239
+ return outcome.job;
240
+ }
126
241
  /** What the daemon says about this provider right now, best effort. */
127
242
  async installerNote(daemonId, provider, readiness = false) {
128
243
  try {
@@ -175,3 +290,16 @@ class HarnessesInstall extends base_command_1.BaseCommand {
175
290
  }
176
291
  }
177
292
  exports.default = HarnessesInstall;
293
+ /**
294
+ * The next step after "Already installed.": how to update it, or where its
295
+ * update comes from — or nothing from a daemon that cannot say (OSK-10096).
296
+ */
297
+ function updateHint(provider, state, bin) {
298
+ if (!state || state.updatable === undefined)
299
+ return null;
300
+ if (state.updatable) {
301
+ return `To update it: ${bin} harnesses install ${provider} --update`;
302
+ }
303
+ const reason = state.updateDisabledReason?.trim();
304
+ return reason ? `Updating: ${reason}` : null;
305
+ }
@@ -1,4 +1,5 @@
1
1
  import { BaseCommand } from '../../base-command';
2
+ import { type ManagedCliProviderState } from '../../lib/cli-installers';
2
3
  /**
3
4
  * What each managed provider's state is on one machine.
4
5
  *
@@ -43,3 +44,12 @@ export default class HarnessesInstallers extends BaseCommand {
43
44
  */
44
45
  private daemonIdFor;
45
46
  }
47
+ /**
48
+ * What each installed CLI's update story is, as sentences under the table.
49
+ *
50
+ * Sentences, not a column: an update reason is "the copy inside Devin.app
51
+ * updates with that app", which no column fits. Silent for a daemon that
52
+ * reports no update fields at all — it cannot say, and inventing "no" for it
53
+ * would be worse than nothing.
54
+ */
55
+ export declare function updateNotes(providers: readonly ManagedCliProviderState[], bin: string): string[];
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.updateNotes = updateNotes;
3
4
  const core_1 = require("@oclif/core");
4
5
  const base_command_1 = require("../../base-command");
5
6
  const data_provider_1 = require("@skrr-ai/data-provider");
@@ -126,6 +127,14 @@ class HarnessesInstallers extends base_command_1.BaseCommand {
126
127
  for (const note of notes)
127
128
  this.log(` ${note.provider}: ${note.detail}`);
128
129
  }
130
+ // The VERSION column is where a reader asks "can I get a newer one", and
131
+ // the answer lived only in `--json` (OSK-10096 verifier).
132
+ const updateLines = updateNotes(providers, this.config.bin);
133
+ if (updateLines.length > 0) {
134
+ this.log('');
135
+ for (const line of updateLines)
136
+ this.log(` ${line}`);
137
+ }
129
138
  // A provider the platform knows and this daemon did not mention. Without
130
139
  // this it simply is not on the list, which reads as dropped support rather
131
140
  // than as an older daemon — `devin` arrived in the 0.8.45 daemon release, so 0.8.35
@@ -177,3 +186,25 @@ class HarnessesInstallers extends base_command_1.BaseCommand {
177
186
  }
178
187
  }
179
188
  exports.default = HarnessesInstallers;
189
+ /**
190
+ * What each installed CLI's update story is, as sentences under the table.
191
+ *
192
+ * Sentences, not a column: an update reason is "the copy inside Devin.app
193
+ * updates with that app", which no column fits. Silent for a daemon that
194
+ * reports no update fields at all — it cannot say, and inventing "no" for it
195
+ * would be worse than nothing.
196
+ */
197
+ function updateNotes(providers, bin) {
198
+ const lines = [];
199
+ for (const provider of providers) {
200
+ if (provider.updatable === true) {
201
+ lines.push(`${provider.provider}: update it with \`${bin} harnesses install ${provider.provider} --update\``);
202
+ }
203
+ }
204
+ for (const provider of providers) {
205
+ if (provider.updatable === false && provider.installed && provider.updateDisabledReason) {
206
+ lines.push(`${provider.provider}: ${provider.updateDisabledReason}`);
207
+ }
208
+ }
209
+ return lines;
210
+ }
@@ -421,8 +421,9 @@ class HarnessesList extends base_command_1.BaseCommand {
421
421
  // mode. Materialize that default before the request so JSON cannot quietly
422
422
  // return every historical offline row while the table shows seven live ones.
423
423
  const status = flags.online ? 'online' : (flags.status ?? (flags.all ? undefined : 'online'));
424
- // Any spelling of the first-party harness filters the same rows; the server is
425
- // asked for the canonical one, which it matches against every stored spelling.
424
+ // Any accepted spelling of the first-party harness filters the same rows; the
425
+ // server is asked for the canonical one, which it matches against every
426
+ // accepted stored spelling.
426
427
  const provider = flags.provider ? (0, harness_provider_input_1.canonicalHarnessInput)(flags.provider) : undefined;
427
428
  const params = new URLSearchParams();
428
429
  const serverStatus = serverStatusParam(status);
@@ -140,35 +140,26 @@ class SkillsImportAsActions extends base_command_1.BaseCommand {
140
140
  this.handleApiError(err);
141
141
  return;
142
142
  }
143
- const failed = result.results.filter((r) => r.status === 'failed');
143
+ const { rows, summary, failed } = (0, local_skills_1.summarizeSkillActionImport)(result);
144
144
  if (flags.json) {
145
145
  this.log(JSON.stringify(result, null, 2));
146
146
  }
147
147
  else {
148
- (0, format_1.renderTable)(result.results.map((r) => ({
149
- skill: r.name,
150
- import: r.status,
151
- action: r.actionCreated
152
- ? 'created'
153
- : (r.actionSkippedReason ??
154
- r.actionError ??
155
- (r.status === 'failed' ? '-' : 'not added')),
156
- detail: r.error ?? '',
157
- })), [
148
+ (0, format_1.renderTable)(rows, [
158
149
  { key: 'skill', header: 'SKILL' },
159
150
  { key: 'import', header: 'IMPORT' },
160
151
  { key: 'action', header: 'ACTION' },
161
152
  { key: 'detail', header: 'DETAIL' },
162
153
  ], (line) => this.log(line));
163
- this.log(`${result.actionsCreated} action(s) created on ${result.targetAgentId}; ` +
164
- `${failed.length} skill(s) failed.`);
154
+ this.log(summary);
165
155
  if (result.actionsError) {
166
156
  this.log(`The skills imported, but their actions could not be added: ${result.actionsError.message}`);
167
157
  }
168
158
  }
169
159
  // Partial success is normal and reported above; exit non-zero only when
170
- // something the caller asked for did not happen.
171
- if (failed.length > 0 || result.actionsError)
160
+ // something the caller asked for did not happen — including an action the
161
+ // full library could not take.
162
+ if (failed)
172
163
  this.exit(1);
173
164
  }
174
165
  }
@@ -0,0 +1,24 @@
1
+ import { BaseCommand } from '../../../base-command';
2
+ /**
3
+ * `skrr tasks attachments set-role <task> <attachment> --role …`
4
+ *
5
+ * The one way to change what an existing asset IS on a Task. Its real use is
6
+ * promotion: an agent may only upload `evidence` or `deliverable`, because an
7
+ * `input` is forwarded into every future Run of the Task — so when an agent's
8
+ * screenshot should become an input, a person promotes it here. The server
9
+ * refuses the call from an agent, whatever credential it holds, and moves the
10
+ * asset's ledger link with the role.
11
+ */
12
+ export default class TasksAttachmentsSetRole extends BaseCommand {
13
+ static description: string;
14
+ static examples: string[];
15
+ static args: {
16
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
17
+ attachmentId: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
18
+ };
19
+ static flags: {
20
+ role: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
21
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
22
+ };
23
+ run(): Promise<void>;
24
+ }