@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
@@ -5,6 +5,7 @@ const core_1 = require("@oclif/core");
5
5
  const data_provider_1 = require("@skrr-ai/data-provider");
6
6
  const base_command_1 = require("../../base-command");
7
7
  const web_url_1 = require("../../lib/web-url");
8
+ const format_1 = require("../../lib/format");
8
9
  class ConvosShow extends base_command_1.BaseCommand {
9
10
  static description = 'Show details for a single conversation';
10
11
  static examples = [
@@ -38,10 +39,10 @@ class ConvosShow extends base_command_1.BaseCommand {
38
39
  this.log(`Model: ${c.model ?? '-'}`);
39
40
  this.log(`User: ${c.user ?? '-'}`);
40
41
  if (c.createdAt) {
41
- this.log(`Created: ${formatTime(c.createdAt)}`);
42
+ this.log(`Created: ${(0, format_1.localTime)(c.createdAt)}`);
42
43
  }
43
44
  if (c.updatedAt) {
44
- this.log(`Updated: ${formatTime(c.updatedAt)}`);
45
+ this.log(`Updated: ${(0, format_1.localTime)(c.updatedAt)}`);
45
46
  }
46
47
  if (c.agent_id || c.agentId) {
47
48
  this.log(`Agent: ${typeof c.agentName === 'string' && c.agentName ? `${c.agentName} (${c.agent_id ?? c.agentId})` : (c.agent_id ?? c.agentId)}`);
@@ -102,17 +103,9 @@ function formatHandoverParty(entry, baseURL) {
102
103
  if (entry.by)
103
104
  lines.push(` By: ${entry.by}`);
104
105
  if (entry.at)
105
- lines.push(` At: ${formatTime(entry.at)}`);
106
+ lines.push(` At: ${(0, format_1.localTime)(entry.at)}`);
106
107
  // The reason last, because it is the sentence rather than a field.
107
108
  if (entry.reason)
108
109
  lines.push(` Reason: ${entry.reason}`);
109
110
  return lines;
110
111
  }
111
- function formatTime(value) {
112
- try {
113
- return new Date(String(value)).toISOString().slice(0, 19).replace('T', ' ');
114
- }
115
- catch {
116
- return String(value);
117
- }
118
- }
@@ -37,10 +37,10 @@ class Daemon extends core_1.Command {
37
37
  this.log((0, exec_runtime_binary_1.notInstalledMessage)());
38
38
  this.exit(127);
39
39
  }
40
- this.log(`runtime binary: ${bin}`);
40
+ this.log(`background service binary: ${bin}`);
41
41
  this.log('');
42
- this.log('Local runtime:');
43
- this.log(' skrr daemon install — install the runtime as a background service');
42
+ this.log('Background service on this computer:');
43
+ this.log(' skrr daemon install — install it as a background service');
44
44
  this.log(' skrr daemon uninstall — remove that service (local state is left alone)');
45
45
  this.log(' skrr daemon start — start the daemon');
46
46
  this.log(' skrr daemon stop — stop the daemon');
@@ -35,7 +35,7 @@ class DaemonInstall extends core_1.Command {
35
35
  // whole topic, and without a per-command alias only the bare topic
36
36
  // resolved: every subcommand under it failed.
37
37
  static aliases = ['daemons:install'];
38
- static summary = 'Install the local skrr runtime as a background service';
38
+ static summary = 'Install the skrr background service on this computer';
39
39
  static description = (0, exec_runtime_binary_1.forwardedFlagsNote)('install', {
40
40
  binds: true,
41
41
  bootstrapsRuntime: true,
@@ -57,11 +57,11 @@ class DaemonInstall extends core_1.Command {
57
57
  // is a floor under the missing case, not a second update channel competing
58
58
  // with the daemon's own.
59
59
  if (!(0, exec_runtime_binary_1.isCurrentRuntimeBinary)((0, exec_runtime_binary_1.findRuntimeBinary)())) {
60
- this.log('No local skrr runtime found. Fetching the signed release...');
60
+ this.log('No skrr background service installed on this computer. Fetching the signed release...');
61
61
  const outcome = await (0, daemon_installer_1.installDaemon)();
62
62
  if (!outcome.ok) {
63
63
  this.log('');
64
- this.log(` Could not install the runtime: ${outcome.error}`);
64
+ this.log(` Could not install the background service: ${outcome.error}`);
65
65
  this.log('');
66
66
  this.log(' You can install it directly instead:');
67
67
  this.log(' macOS: brew install dush1023/oversky/oversky');
@@ -84,7 +84,7 @@ class DaemonInstall extends core_1.Command {
84
84
  // Reachable only if the runtime vanished between the bootstrap above
85
85
  // and this line. Saying so beats repeating install instructions for
86
86
  // something that was just installed.
87
- this.log('The skrr runtime disappeared between installing it and running it.');
87
+ this.log('The skrr background service disappeared between installing it and running it.');
88
88
  this.exit(127);
89
89
  }
90
90
  throw err;
@@ -28,7 +28,7 @@ const exec_runtime_binary_1 = require("../../lib/exec-runtime-binary");
28
28
  */
29
29
  class DaemonLogin extends core_1.Command {
30
30
  static aliases = ['daemons:login'];
31
- static summary = 'Authenticate the local skrr runtime (separate from `skrr login`)';
31
+ static summary = 'Authenticate the skrr background service on this computer (separate from `skrr login`)';
32
32
  static description = (0, exec_runtime_binary_1.forwardedFlagsNote)('login', { binds: true });
33
33
  static strict = false; // forward all args verbatim — see run()
34
34
  async run() {
@@ -17,6 +17,7 @@ export interface DaemonLogsFlags {
17
17
  lines?: number;
18
18
  stderr?: boolean;
19
19
  path?: boolean;
20
+ profile?: string;
20
21
  }
21
22
  /**
22
23
  * Older daemons already kept logs in this profile-scoped location, even
@@ -50,7 +51,7 @@ export declare function describeLogAge(mtimeMs: number, now?: number): {
50
51
  line: string;
51
52
  stale: boolean;
52
53
  };
53
- export declare function resolveLegacyDaemonLogPath(flags: Pick<DaemonLogsFlags, 'stderr'>, options?: {
54
+ export declare function resolveLegacyDaemonLogPath(flags: Pick<DaemonLogsFlags, 'stderr' | 'profile'>, options?: {
54
55
  home?: string;
55
56
  env?: NodeJS.ProcessEnv;
56
57
  }): string;
@@ -81,6 +82,7 @@ export default class DaemonLogs extends Command {
81
82
  lines: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
82
83
  stderr: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
83
84
  path: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
85
+ profile: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
84
86
  };
85
87
  static examples: string[];
86
88
  run(): Promise<void>;
@@ -64,8 +64,14 @@ function describeLogAge(mtimeMs, now = Date.now()) {
64
64
  function resolveLegacyDaemonLogPath(flags, options = {}) {
65
65
  const home = options.home ?? (0, node_os_1.homedir)();
66
66
  const env = options.env ?? process.env;
67
+ // An explicit --profile outranks the environment, as it does everywhere else.
68
+ const fromFlag = flags.profile?.trim();
67
69
  const fromEnv = env.OVERSKY_PROFILE?.trim();
68
- let profile = fromEnv && PROFILE_NAME.test(fromEnv) ? fromEnv : undefined;
70
+ let profile = fromFlag && PROFILE_NAME.test(fromFlag)
71
+ ? fromFlag
72
+ : fromEnv && PROFILE_NAME.test(fromEnv)
73
+ ? fromEnv
74
+ : undefined;
69
75
  if (!profile) {
70
76
  try {
71
77
  const pinned = (0, node_fs_1.readFileSync)(node_path_1.default.join(home, '.skrr', 'profile.lock'), 'utf8').trim();
@@ -110,6 +116,8 @@ function readDaemonLogTail(filePath, maxLines) {
110
116
  */
111
117
  function buildOverskyLogsArgs(flags) {
112
118
  return [
119
+ // The runtime's own global option, placed before the subcommand.
120
+ ...(flags.profile ? ['--profile', flags.profile] : []),
113
121
  'logs',
114
122
  ...(flags.follow ? ['--follow'] : []),
115
123
  ...(flags.lines !== undefined ? ['--lines', String(flags.lines)] : []),
@@ -149,6 +157,12 @@ class DaemonLogs extends core_1.Command {
149
157
  path: core_1.Flags.boolean({
150
158
  description: 'Print the resolved log file path and exit',
151
159
  }),
160
+ // Every other daemon verb reaches a non-default profile with --profile; this
161
+ // one could only be steered by OVERSKY_PROFILE (OSK-10001).
162
+ profile: core_1.Flags.string({
163
+ char: 'p',
164
+ description: 'Read the logs of this daemon profile instead of the active one',
165
+ }),
152
166
  };
153
167
  static examples = [
154
168
  '<%= config.bin %> daemon logs',
@@ -156,6 +170,7 @@ class DaemonLogs extends core_1.Command {
156
170
  '<%= config.bin %> daemon logs --lines 500',
157
171
  '<%= config.bin %> daemon logs --stderr',
158
172
  '<%= config.bin %> daemon logs --path',
173
+ '<%= config.bin %> daemon logs --profile dogfood-laptop --lines 50',
159
174
  ];
160
175
  async run() {
161
176
  const { flags } = await this.parse(DaemonLogs);
@@ -34,19 +34,19 @@ class DaemonStart extends core_1.Command {
34
34
  const status = await (0, exec_runtime_binary_1.execRuntimeBinaryCapture)((0, runtime_start_verification_1.statusArgsForStart)(argv));
35
35
  const verification = (0, runtime_start_verification_1.verifyRuntimeStart)(status.stdout, status.code, argv);
36
36
  if (verification.status === 'reauth_required') {
37
- this.logToStderr(`Local runtime did not start: profile '${verification.profile}' requires ` +
37
+ this.logToStderr(`The background service did not start: profile '${verification.profile}' requires ` +
38
38
  `re-authentication (reason: ${verification.reason ?? 'UNKNOWN'}). Run ` +
39
39
  `\`${verification.recoveryCommand}\`, then retry \`skrr daemon start\`.`);
40
40
  this.exit(runtime_start_verification_1.DAEMON_START_REAUTH_EXIT);
41
41
  }
42
42
  if (verification.status === 'not_running') {
43
- this.logToStderr(`Local runtime did not remain running after skrrd exited successfully ` +
43
+ this.logToStderr(`The background service did not keep running after skrrd exited successfully ` +
44
44
  `(profile '${verification.profile}', service status: ` +
45
45
  `${verification.runtimeStatus ?? 'unknown'}). Run \`skrr daemon logs\` for the reason.`);
46
46
  this.exit(runtime_start_verification_1.DAEMON_START_UNVERIFIED_EXIT);
47
47
  }
48
48
  if (verification.status === 'unreadable_status') {
49
- this.logToStderr(`skrrd start exited successfully, but skrr could not verify a running local runtime ` +
49
+ this.logToStderr(`skrrd start exited successfully, but skrr could not verify a running background service ` +
50
50
  `for profile '${verification.profile}'; refusing to report success. ` +
51
51
  'Run `skrr daemon status` and `skrr daemon logs` for the reason.');
52
52
  this.exit(runtime_start_verification_1.DAEMON_START_UNVERIFIED_EXIT);
@@ -1,4 +1,13 @@
1
1
  import { Command } from '@oclif/core';
2
+ /**
3
+ * The status report is the runtime's, and it describes the daemon: running,
4
+ * connected, which credential. Whether the coding-agent CLIs on the machine can
5
+ * actually run a turn — installed, signed in — is a different command, and
6
+ * nothing on this surface named it (OSK-10002). One line after a successful
7
+ * human report; never inside `--json`, whose contract is one document, and
8
+ * never after `--help` or a failure, where it would be noise beside the point.
9
+ */
10
+ export declare function readinessPointer(argv: readonly string[], exitCode: number): string | null;
2
11
  export default class DaemonStatus extends Command {
3
12
  static aliases: string[];
4
13
  static summary: string;
@@ -1,7 +1,26 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readinessPointer = readinessPointer;
3
4
  const core_1 = require("@oclif/core");
4
5
  const exec_runtime_binary_1 = require("../../lib/exec-runtime-binary");
6
+ /**
7
+ * The status report is the runtime's, and it describes the daemon: running,
8
+ * connected, which credential. Whether the coding-agent CLIs on the machine can
9
+ * actually run a turn — installed, signed in — is a different command, and
10
+ * nothing on this surface named it (OSK-10002). One line after a successful
11
+ * human report; never inside `--json`, whose contract is one document, and
12
+ * never after `--help` or a failure, where it would be noise beside the point.
13
+ */
14
+ function readinessPointer(argv, exitCode) {
15
+ if (exitCode !== 0)
16
+ return null;
17
+ if (argv.some((arg) => arg === '--json' || arg === '--help' || arg === '-h'))
18
+ return null;
19
+ // Runnable as printed: `installers` picks the only connected daemon itself,
20
+ // and lists the ids to choose from when there are several (OSK-10002 verifier).
21
+ return (' Coding agents: which CLIs are installed on this machine, and signed in where the daemon can tell — ' +
22
+ '`skrr harnesses installers` (add `--daemon <Daemon ID above>` if several daemons are connected)');
23
+ }
5
24
  class DaemonStatus extends core_1.Command {
6
25
  // `skrr daemons <sub>` — the plural is advertised as an alias for the
7
26
  // whole topic, and without a per-command alias only the bare topic
@@ -23,6 +42,9 @@ class DaemonStatus extends core_1.Command {
23
42
  const argv = this.argv;
24
43
  try {
25
44
  const code = await (0, exec_runtime_binary_1.execRuntimeBinary)(['status', ...argv]);
45
+ const pointer = readinessPointer(argv, code);
46
+ if (pointer)
47
+ this.log(pointer);
26
48
  this.exit(code);
27
49
  }
28
50
  catch (err) {
@@ -87,7 +87,7 @@ class DmMessages extends base_command_1.BaseCommand {
87
87
  return;
88
88
  }
89
89
  (0, format_1.renderTable)(selected.map((message) => ({
90
- when: formatTime(message.createdAt),
90
+ when: (0, format_1.localTime)(message.createdAt),
91
91
  role: String(message.sender ?? (message.isCreatedByUser ? 'user' : 'assistant')),
92
92
  text: textFromMessage(message).replace(/\s+/g, ' ').trim(),
93
93
  id: String(message.messageId ?? message._id ?? message.id ?? '-'),
@@ -100,16 +100,6 @@ class DmMessages extends base_command_1.BaseCommand {
100
100
  }
101
101
  }
102
102
  exports.default = DmMessages;
103
- function formatTime(value) {
104
- if (!value)
105
- return '-';
106
- try {
107
- return new Date(String(value)).toISOString().slice(0, 19);
108
- }
109
- catch {
110
- return String(value);
111
- }
112
- }
113
103
  function textFromMessage(message) {
114
104
  if (typeof message.text === 'string')
115
105
  return message.text;
@@ -44,7 +44,7 @@ class FollowupsCancel extends base_command_1.BaseCommand {
44
44
  this.log(JSON.stringify(result, null, 2));
45
45
  return;
46
46
  }
47
- this.log(`Follow-up ${args.id} cancelled.`);
47
+ this.log((0, followups_1.settledLine)(args.id, result, 'cancelled'));
48
48
  }
49
49
  }
50
50
  exports.default = FollowupsCancel;
@@ -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;
@@ -48,7 +48,7 @@ class FollowupsResolve extends base_command_1.BaseCommand {
48
48
  this.log(JSON.stringify(result, null, 2));
49
49
  return;
50
50
  }
51
- this.log(`Follow-up ${args.id} settled: ${flags.outcome}.`);
51
+ this.log((0, followups_1.settledLine)(args.id, result, `settled: ${flags.outcome}`));
52
52
  }
53
53
  }
54
54
  exports.default = FollowupsResolve;
@@ -3,6 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const core_1 = require("@oclif/core");
4
4
  const base_command_1 = require("../../base-command");
5
5
  const followups_1 = require("../../lib/followups");
6
+ /** What each gate posture means for whoever reads `show`. */
7
+ const GATE_LABEL = {
8
+ exempt_silent: 'fires without approval (silent)',
9
+ exempt_user_directed: 'fires without approval (you asked for it)',
10
+ exempt_system: 'fires without approval (system)',
11
+ user_directed_guarded: 'fires without approval (you asked for it); still held by quiet hours, approval-only action classes and the spend cap',
12
+ autonomy_ladder: "follows the agent's autonomy mode",
13
+ approval_ladder: 'needs approval unless the autonomy mode allows it',
14
+ };
6
15
  /** `skrr followups show <id>` — one follow-up, the same shape as the list rows. */
7
16
  class FollowupsShow extends base_command_1.BaseCommand {
8
17
  static description = 'Show one follow-up';
@@ -34,12 +43,19 @@ class FollowupsShow extends base_command_1.BaseCommand {
34
43
  ['why', row.why],
35
44
  ['intent', row.intentKey],
36
45
  ['producer', row.producer],
46
+ ['gate', row.gate ? (GATE_LABEL[row.gate] ?? row.gate) : undefined],
37
47
  ['terminal', row.terminal ? `${row.terminal} (${row.terminalReason ?? '—'})` : undefined],
38
48
  [
39
49
  'settled',
40
50
  row.terminal ? `${row.terminalAt ?? '—'} by ${row.terminalBy ?? 'unknown'}` : undefined,
41
51
  ],
42
52
  ['deadline', row.deadlineAt],
53
+ [
54
+ 'waiting',
55
+ row.parkedAt && !row.terminal
56
+ ? `on an offline machine since ${row.parkedAt} (escalates after 60 minutes)`
57
+ : undefined,
58
+ ],
43
59
  ['escalated to', row.escalatedInitiativeId],
44
60
  ]) {
45
61
  if (value)
@@ -54,12 +54,29 @@ class HarnessesForget extends base_command_1.BaseCommand {
54
54
  const current = await (0, harnesses_1.getHarness)(args.harness);
55
55
  label = current?.harness?.name || args.harness;
56
56
  }
57
- catch {
58
- /* fall back to the id — the confirmation still has to happen */
57
+ catch (err) {
58
+ // Existence before consent (OSK-9995): asking "Forget harness X?" about
59
+ // a row that does not exist, then reporting NOT_FOUND only after --yes,
60
+ // made the prompt the first thing to fail. Any other read failure still
61
+ // falls back to the id — the confirmation has to happen regardless.
62
+ if (err instanceof api_fetch_1.ApiFetchError && err.status === 404) {
63
+ // NOT_FOUND, the code `harnesses show` gives the same missing id, so a
64
+ // script branching on the code sees one answer (OSK-9995 verifier).
65
+ this.failWithCliError({
66
+ message: `No harness on this account has id ${args.harness}. Run \`skrr harnesses list --all\` to see them.`,
67
+ code: 'NOT_FOUND',
68
+ status: 404,
69
+ exit: 4,
70
+ retryable: false,
71
+ });
72
+ return;
73
+ }
59
74
  }
60
75
  const ok = await this.confirm(`Forget harness "${label}"? Agents pinned to it lose their pin.`);
61
76
  if (!ok) {
62
- this.log('Cancelled.');
77
+ // Non-zero because nothing was forgotten — the same rule as `tasks
78
+ // update`: a script can only read the exit code, and 0 read as success.
79
+ this.error('Cancelled — nothing was forgotten.', { exit: 1 });
63
80
  return;
64
81
  }
65
82
  }
@@ -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,26 @@ 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
+ /** Follow a job to its end, or report why it could not be followed (and return nothing). */
50
+ private follow;
38
51
  /** What the daemon says about this provider right now, best effort. */
39
52
  private installerNote;
40
53
  private daemonIdFor;
41
54
  }
55
+ /**
56
+ * The next step after "Already installed.": how to update it, or where its
57
+ * update comes from — or nothing from a daemon that cannot say (OSK-10096).
58
+ */
59
+ 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({
@@ -47,7 +48,10 @@ class HarnessesInstall extends base_command_1.BaseCommand {
47
48
  static flags = {
48
49
  json: core_1.Flags.boolean({ description: 'Output as JSON' }),
49
50
  daemon: core_1.Flags.string({
50
- description: 'Which machine to install on. Defaults to your only active daemon; required when several are active.',
51
+ description: 'Which machine to install on. Defaults to your only connected daemon; required when several are connected.',
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.",
51
55
  }),
52
56
  };
53
57
  async run() {
@@ -63,9 +67,13 @@ class HarnessesInstall extends base_command_1.BaseCommand {
63
67
  const target = await (0, daemon_target_1.resolveDaemonTarget)({
64
68
  explicit: flags.daemon,
65
69
  local: (0, daemon_target_1.localMachine)(),
66
- listActiveDaemons: () => (0, daemon_target_1.listActiveDaemons)(this.resolvedCredential),
70
+ listActiveDaemons: () => (0, daemon_target_1.listConnectedDaemons)(this.resolvedCredential),
67
71
  });
68
72
  const daemonId = this.daemonIdFor(target);
73
+ if (flags.update) {
74
+ await this.update(daemonId, provider, Boolean(flags.json));
75
+ return;
76
+ }
69
77
  let start;
70
78
  try {
71
79
  start = await (0, cli_installers_1.startManagedCliInstall)(daemonId, provider, this.resolvedCredential);
@@ -83,27 +91,22 @@ class HarnessesInstall extends base_command_1.BaseCommand {
83
91
  const reason = start.provider?.installDisabledReason?.trim() ||
84
92
  (await this.installerNote(daemonId, provider));
85
93
  this.log(reason ? `${provider}: ${reason}` : `${provider}: the daemon started no install job.`);
94
+ // "Already installed." used to be the end of the road: the product could
95
+ // install a CLI and then had no word about updating it (OSK-10096).
96
+ if (start.alreadyInstalled) {
97
+ const next = updateHint(provider, start.provider, this.config.bin);
98
+ if (next)
99
+ this.log(next);
100
+ }
86
101
  return;
87
102
  }
103
+ // The raw id: it is what `--daemon` takes, and a formatDaemonRef prefix of a
104
+ // `daemon_<hash>_…` id reads `d_daemon_4...` for every local daemon (OSK-10182).
88
105
  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 });
106
+ this.logToStderr(`Installing ${provider} on ${daemonId}…`);
107
+ const job = await this.follow(daemonId, jobId, 'install', Boolean(flags.json));
108
+ if (!job)
98
109
  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 });
104
- return;
105
- }
106
- const job = outcome.job;
107
110
  if (flags.json) {
108
111
  this.log(JSON.stringify({ daemonId, provider, job }, null, 2));
109
112
  if (job.status !== 'succeeded')
@@ -123,6 +126,99 @@ class HarnessesInstall extends base_command_1.BaseCommand {
123
126
  const state = await this.installerNote(daemonId, provider, true);
124
127
  this.log(`${provider} installed.${state ? ` ${state}` : ''}`);
125
128
  }
129
+ /**
130
+ * Update an installed CLI in place (OSK-10096).
131
+ *
132
+ * Asks the inventory first rather than posting blind: a daemon older than
133
+ * managed updates has no handler for the request and would leave it to time
134
+ * out, and a CLI that updates some other way (the copy inside Devin.app) has a
135
+ * reason the daemon already words. Both are answered before anything runs.
136
+ */
137
+ async update(daemonId, provider, json) {
138
+ let inventory;
139
+ try {
140
+ inventory = await (0, cli_installers_1.listCliInstallers)(daemonId, this.resolvedCredential);
141
+ }
142
+ catch (err) {
143
+ this.handleApiError(err);
144
+ return;
145
+ }
146
+ const row = (inventory.providers ?? []).find((p) => (0, data_provider_1.normalizeManagedCliProvider)(p.provider) === provider);
147
+ if (!row || row.updatable === undefined) {
148
+ this.failWithCliError({
149
+ message: `The daemon ${daemonId} (${inventory.daemonVersion ?? 'version not reported'}) cannot update ${provider}: it predates managed updates. ` +
150
+ `Update the daemon, then run this again — \`${this.config.bin} daemon status\` says how.`,
151
+ code: 'DAEMON_UPDATE_UNSUPPORTED',
152
+ exit: 1,
153
+ retryable: false,
154
+ });
155
+ return;
156
+ }
157
+ if (!row.updatable) {
158
+ this.failWithCliError({
159
+ message: `${provider}: ${row.updateDisabledReason?.trim() || 'the daemon cannot update it here.'}`,
160
+ code: 'CLI_NOT_UPDATABLE',
161
+ exit: 1,
162
+ retryable: false,
163
+ });
164
+ return;
165
+ }
166
+ let start;
167
+ try {
168
+ start = await (0, cli_installers_1.startManagedCliUpdate)(daemonId, provider, this.resolvedCredential);
169
+ }
170
+ catch (err) {
171
+ this.handleApiError(err);
172
+ return;
173
+ }
174
+ const jobId = start.job?.id;
175
+ if (!start.started || !jobId) {
176
+ this.log(start.alreadyRunning
177
+ ? `${provider}: an install or update is already running (job ${start.job?.id ?? 'unknown'}).`
178
+ : `${provider}: ${start.provider?.updateDisabledReason?.trim() || 'the daemon started no update job.'}`);
179
+ return;
180
+ }
181
+ if (!json) {
182
+ this.logToStderr(`Updating ${provider} ${row.version ? `${row.version} ` : ''}on ${daemonId}…`);
183
+ }
184
+ const job = await this.follow(daemonId, jobId, 'update', json);
185
+ if (!job)
186
+ return;
187
+ if (json) {
188
+ this.log(JSON.stringify({ daemonId, provider, job }, null, 2));
189
+ if (job.status !== 'succeeded')
190
+ process.exitCode = 1;
191
+ return;
192
+ }
193
+ if (job.status === 'failed') {
194
+ this.error(`Updating ${provider} failed: ${job.error || 'the daemon gave no reason.'}`, {
195
+ exit: 1,
196
+ });
197
+ return;
198
+ }
199
+ this.log((0, cli_installers_1.updateOutcomeLine)(job) ??
200
+ `${provider} updated${job.installedVersion ? ` to ${job.installedVersion}` : ''}.`);
201
+ }
202
+ /** Follow a job to its end, or report why it could not be followed (and return nothing). */
203
+ async follow(daemonId, jobId, verb, json) {
204
+ const outcome = await (0, cli_installers_1.followManagedCliInstallJob)({
205
+ daemonId,
206
+ jobId,
207
+ credential: this.resolvedCredential,
208
+ onLogLine: json ? undefined : (line) => this.logToStderr(` ${line}`),
209
+ });
210
+ if (outcome.kind === 'missing') {
211
+ this.error(`The daemon no longer has ${verb} job ${jobId}.`, { exit: 1 });
212
+ return undefined;
213
+ }
214
+ if (outcome.kind === 'timeout') {
215
+ // Not a failure: the daemon is still working, and calling this failed
216
+ // would send someone to debug a download that is going fine.
217
+ 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 });
218
+ return undefined;
219
+ }
220
+ return outcome.job;
221
+ }
126
222
  /** What the daemon says about this provider right now, best effort. */
127
223
  async installerNote(daemonId, provider, readiness = false) {
128
224
  try {
@@ -152,14 +248,39 @@ class HarnessesInstall extends base_command_1.BaseCommand {
152
248
  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 });
153
249
  }
154
250
  if (target.kind === 'none') {
155
- this.error(`No active daemon is registered for this account, so nothing can perform the install. Start the local runtime with \`${this.config.bin} daemon start\`.`, { exit: 1 });
251
+ this.failWithCliError({
252
+ message: `No daemon is connected for this account, so nothing can perform the install. Start the background service on this computer with \`${this.config.bin} daemon start\`.`,
253
+ code: 'NO_DAEMON',
254
+ exit: 1,
255
+ retryable: false,
256
+ });
156
257
  }
157
258
  const choices = target.daemons
158
259
  .slice(0, 10)
159
- .map((d) => ` ${(0, daemon_ref_1.formatDaemonRef)(d.id)} ${d.name || '—'}`)
260
+ // Full ids: they are what `--daemon` takes (OSK-9993).
261
+ .map((d) => ` ${d.id} ${d.name || '—'}`)
160
262
  .join('\n');
161
- this.error(`${target.daemons.length} active daemons — say which machine to install on:\n${choices}\n` +
162
- `Pass one with --daemon, or see them all with \`${this.config.bin} list-daemons\`.`, { exit: 1 });
263
+ // The same refusal, and the same code, as \`code install\` (OSK-9993 verifier).
264
+ this.failWithCliError({
265
+ message: `${target.daemons.length} connected daemons — say which machine to install on:\n${choices}\n` +
266
+ `Pass one with --daemon, or see each daemon id in the DAEMON ID column of \`${this.config.bin} harnesses list\`.`,
267
+ code: 'AMBIGUOUS_DAEMON',
268
+ exit: 1,
269
+ retryable: false,
270
+ });
163
271
  }
164
272
  }
165
273
  exports.default = HarnessesInstall;
274
+ /**
275
+ * The next step after "Already installed.": how to update it, or where its
276
+ * update comes from — or nothing from a daemon that cannot say (OSK-10096).
277
+ */
278
+ function updateHint(provider, state, bin) {
279
+ if (!state || state.updatable === undefined)
280
+ return null;
281
+ if (state.updatable) {
282
+ return `To update it: ${bin} harnesses install ${provider} --update`;
283
+ }
284
+ const reason = state.updateDisabledReason?.trim();
285
+ return reason ? `Updating: ${reason}` : null;
286
+ }