@skrr-ai/cli 0.1.37 → 0.1.38

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 (174) hide show
  1. package/bin/dev-fallback.js +167 -4
  2. package/dist/base-command.js +46 -2
  3. package/dist/commands/agents/actions/create.js +18 -7
  4. package/dist/commands/agents/actions/delete.d.ts +1 -0
  5. package/dist/commands/agents/actions/delete.js +7 -0
  6. package/dist/commands/agents/actions/update.js +5 -3
  7. package/dist/commands/agents/api-actions/add.js +5 -2
  8. package/dist/commands/agents/api-actions/index.d.ts +11 -0
  9. package/dist/commands/agents/api-actions/index.js +27 -1
  10. package/dist/commands/agents/avatar/default/index.d.ts +11 -0
  11. package/dist/commands/agents/avatar/default/index.js +17 -0
  12. package/dist/commands/agents/avatar/default/set.d.ts +2 -1
  13. package/dist/commands/agents/avatar/default/set.js +32 -7
  14. package/dist/commands/agents/avatar/upload.js +7 -1
  15. package/dist/commands/agents/categories/delete.js +8 -0
  16. package/dist/commands/agents/categories/index.d.ts +21 -5
  17. package/dist/commands/agents/categories/index.js +68 -15
  18. package/dist/commands/agents/chat.d.ts +21 -0
  19. package/dist/commands/agents/chat.js +94 -7
  20. package/dist/commands/agents/create.js +11 -4
  21. package/dist/commands/agents/list.d.ts +15 -0
  22. package/dist/commands/agents/list.js +31 -1
  23. package/dist/commands/agents/scene-background/upload.d.ts +30 -0
  24. package/dist/commands/agents/scene-background/upload.js +97 -0
  25. package/dist/commands/agents/show.js +13 -0
  26. package/dist/commands/agents/tools/calls.d.ts +23 -5
  27. package/dist/commands/agents/tools/calls.js +81 -18
  28. package/dist/commands/agents/update.js +15 -5
  29. package/dist/commands/code/{oversky-agent.d.ts → skrr-agent.d.ts} +3 -3
  30. package/dist/commands/code/{oversky-agent.js → skrr-agent.js} +11 -8
  31. package/dist/commands/convos/messages.d.ts +13 -0
  32. package/dist/commands/convos/messages.js +40 -16
  33. package/dist/commands/daemon/index.js +8 -1
  34. package/dist/commands/daemon/logs.d.ts +26 -0
  35. package/dist/commands/daemon/logs.js +56 -0
  36. package/dist/commands/daemon/uninstall.js +1 -1
  37. package/dist/commands/followups/act.js +3 -2
  38. package/dist/commands/followups/cancel.d.ts +6 -3
  39. package/dist/commands/followups/cancel.js +16 -6
  40. package/dist/commands/followups/observability.d.ts +23 -0
  41. package/dist/commands/followups/observability.js +114 -0
  42. package/dist/commands/followups/reschedule.js +1 -1
  43. package/dist/commands/followups/resolve.js +3 -3
  44. package/dist/commands/followups/show.js +2 -1
  45. package/dist/commands/followups/watch.js +9 -5
  46. package/dist/commands/harnesses/forget.js +28 -0
  47. package/dist/commands/harnesses/install.d.ts +41 -0
  48. package/dist/commands/harnesses/install.js +165 -0
  49. package/dist/commands/harnesses/installers.d.ts +45 -0
  50. package/dist/commands/harnesses/installers.js +155 -0
  51. package/dist/commands/harnesses/list.d.ts +82 -1
  52. package/dist/commands/harnesses/list.js +104 -12
  53. package/dist/commands/harnesses/models.js +10 -1
  54. package/dist/commands/harnesses/show.d.ts +15 -0
  55. package/dist/commands/harnesses/show.js +44 -3
  56. package/dist/commands/harnesses/update.js +1 -1
  57. package/dist/commands/harnesses/usage.d.ts +25 -0
  58. package/dist/commands/harnesses/usage.js +24 -2
  59. package/dist/commands/instructions/status.js +13 -1
  60. package/dist/commands/machines/dedicated/catalog.js +4 -15
  61. package/dist/commands/machines/dedicated/create.d.ts +8 -0
  62. package/dist/commands/machines/dedicated/create.js +58 -1
  63. package/dist/commands/machines/dedicated/destroy.js +18 -4
  64. package/dist/commands/machines/dedicated/list.js +9 -6
  65. package/dist/commands/machines/hosted/list.js +12 -0
  66. package/dist/commands/machines/list.js +11 -0
  67. package/dist/commands/machines/show.d.ts +1 -0
  68. package/dist/commands/machines/show.js +40 -11
  69. package/dist/commands/profiles/list.d.ts +23 -0
  70. package/dist/commands/profiles/list.js +96 -0
  71. package/dist/commands/search/skills.d.ts +11 -1
  72. package/dist/commands/search/skills.js +26 -8
  73. package/dist/commands/skills/details.d.ts +2 -0
  74. package/dist/commands/skills/details.js +24 -1
  75. package/dist/commands/skills/import-as-actions.d.ts +32 -0
  76. package/dist/commands/skills/import-as-actions.js +175 -0
  77. package/dist/commands/skills/install.d.ts +2 -0
  78. package/dist/commands/skills/install.js +24 -1
  79. package/dist/commands/skills/invocable.js +6 -2
  80. package/dist/commands/skills/local.d.ts +40 -0
  81. package/dist/commands/skills/local.js +160 -0
  82. package/dist/commands/skills/search.d.ts +11 -1
  83. package/dist/commands/skills/search.js +29 -8
  84. package/dist/commands/spaces/actions/add.js +23 -5
  85. package/dist/commands/spaces/actions/available.js +3 -0
  86. package/dist/commands/spaces/actions/list.d.ts +5 -0
  87. package/dist/commands/spaces/actions/list.js +49 -3
  88. package/dist/commands/spaces/actions/run.d.ts +28 -0
  89. package/dist/commands/spaces/actions/run.js +146 -0
  90. package/dist/commands/spaces/actions/update.js +27 -5
  91. package/dist/commands/spaces/label-summaries.d.ts +27 -0
  92. package/dist/commands/spaces/label-summaries.js +64 -0
  93. package/dist/commands/spaces/task-rollups.d.ts +5 -0
  94. package/dist/commands/spaces/task-rollups.js +15 -4
  95. package/dist/commands/spaces/workflows/lifecycle-diagnostics.js +63 -3
  96. package/dist/commands/tasks/actions/attach.js +32 -7
  97. package/dist/commands/tasks/complete.js +13 -0
  98. package/dist/commands/tasks/create.js +1 -1
  99. package/dist/commands/tasks/events/append.js +29 -6
  100. package/dist/commands/tasks/expectations/assess.d.ts +1 -0
  101. package/dist/commands/tasks/expectations/assess.js +43 -5
  102. package/dist/commands/tasks/expectations.js +77 -16
  103. package/dist/commands/tasks/follow-up.js +2 -2
  104. package/dist/commands/tasks/lifecycle/definitions.js +21 -2
  105. package/dist/commands/tasks/lifecycle/detach.d.ts +7 -0
  106. package/dist/commands/tasks/lifecycle/detach.js +43 -3
  107. package/dist/commands/tasks/lifecycle/migrate.d.ts +2 -0
  108. package/dist/commands/tasks/lifecycle/migrate.js +42 -3
  109. package/dist/commands/tasks/lifecycle/reopen.d.ts +2 -0
  110. package/dist/commands/tasks/lifecycle/reopen.js +40 -3
  111. package/dist/commands/tasks/lifecycle/transition.js +33 -2
  112. package/dist/commands/tasks/list.js +16 -9
  113. package/dist/commands/tasks/move.js +11 -13
  114. package/dist/commands/tasks/review-queue.d.ts +15 -0
  115. package/dist/commands/tasks/review-queue.js +180 -31
  116. package/dist/commands/tasks/show.d.ts +11 -0
  117. package/dist/commands/tasks/show.js +105 -12
  118. package/dist/commands/tasks/trust.js +7 -2
  119. package/dist/commands/tasks/update.js +46 -10
  120. package/dist/commands/tasks/updates/add.js +1 -1
  121. package/dist/commands/token/list.d.ts +22 -0
  122. package/dist/commands/token/list.js +68 -0
  123. package/dist/commands/token/revoke.d.ts +23 -0
  124. package/dist/commands/token/revoke.js +53 -0
  125. package/dist/lib/agent-actions.d.ts +57 -2
  126. package/dist/lib/agent-actions.js +113 -21
  127. package/dist/lib/agent-pin-mute.d.ts +10 -5
  128. package/dist/lib/agent-pin-mute.js +17 -4
  129. package/dist/lib/agent-visual-refs.d.ts +27 -0
  130. package/dist/lib/agent-visual-refs.js +127 -2
  131. package/dist/lib/agentic-stream.d.ts +6 -0
  132. package/dist/lib/agentic-stream.js +34 -3
  133. package/dist/lib/assignee-resolver.d.ts +1 -1
  134. package/dist/lib/assignee-resolver.js +8 -0
  135. package/dist/lib/bulk-task-targets.d.ts +12 -0
  136. package/dist/lib/bulk-task-targets.js +34 -1
  137. package/dist/lib/cli-installers.d.ts +130 -0
  138. package/dist/lib/cli-installers.js +145 -0
  139. package/dist/lib/command-miss.js +55 -7
  140. package/dist/lib/dedicated-lease-command.d.ts +13 -0
  141. package/dist/lib/dedicated-lease-command.js +22 -1
  142. package/dist/lib/dedicated-machines.d.ts +95 -1
  143. package/dist/lib/dedicated-machines.js +236 -2
  144. package/dist/lib/failed-lookup.d.ts +3 -0
  145. package/dist/lib/failed-lookup.js +40 -0
  146. package/dist/lib/first-party-harness-agent.js +9 -2
  147. package/dist/lib/first-party-harness-doctor.d.ts +15 -0
  148. package/dist/lib/first-party-harness-doctor.js +126 -27
  149. package/dist/lib/followups.d.ts +14 -6
  150. package/dist/lib/followups.js +31 -6
  151. package/dist/lib/harnesses.d.ts +8 -0
  152. package/dist/lib/instruction-input.d.ts +19 -1
  153. package/dist/lib/instruction-input.js +52 -1
  154. package/dist/lib/label-scope.d.ts +15 -0
  155. package/dist/lib/label-scope.js +25 -5
  156. package/dist/lib/local-skills.d.ts +98 -0
  157. package/dist/lib/local-skills.js +195 -0
  158. package/dist/lib/machines.d.ts +10 -10
  159. package/dist/lib/machines.js +23 -1
  160. package/dist/lib/search-query.d.ts +19 -0
  161. package/dist/lib/search-query.js +35 -0
  162. package/dist/lib/space-custom-actions.d.ts +72 -0
  163. package/dist/lib/space-custom-actions.js +117 -0
  164. package/dist/lib/space-resolver.d.ts +1 -21
  165. package/dist/lib/space-resolver.js +19 -10
  166. package/dist/lib/task-closure.d.ts +3 -2
  167. package/dist/lib/task-closure.js +18 -20
  168. package/dist/lib/task-lifecycle-output.js +117 -9
  169. package/dist/lib/task-ref-resolver.d.ts +1 -17
  170. package/dist/lib/task-ref-resolver.js +18 -8
  171. package/dist/lib/tasks.js +9 -3
  172. package/dist/node_modules/@skrr-ai/data-provider/index.js +5076 -4947
  173. package/oclif.manifest.json +26635 -25649
  174. package/package.json +9 -3
@@ -48,6 +48,116 @@ function sourceTreeDigest(fileSystem, sourceRoot) {
48
48
  return hash.digest('hex');
49
49
  }
50
50
 
51
+ /**
52
+ * Every relative specifier a source imports, re-exports, or dynamically loads.
53
+ *
54
+ * Deliberately a scan rather than a parse: this runs before the CLI starts, so
55
+ * it may not cost a TypeScript program, and over-collecting is harmless — an
56
+ * extra file in the closure can only make the guard more conservative, never
57
+ * less.
58
+ */
59
+ function relativeSpecifiers(text) {
60
+ const found = [];
61
+ const patterns = [
62
+ /(?:^|[\s;}])(?:import|export)[\s\S]{0,400}?from\s*['"](\.[^'"]*)['"]/g,
63
+ /\bimport\s*\(\s*['"](\.[^'"]*)['"]\s*\)/g,
64
+ /\brequire\s*\(\s*['"](\.[^'"]*)['"]\s*\)/g,
65
+ /(?:^|[\s;}])import\s+['"](\.[^'"]*)['"]/g,
66
+ ];
67
+ for (const pattern of patterns) {
68
+ let match;
69
+ while ((match = pattern.exec(text)) !== null) found.push(match[1]);
70
+ }
71
+ return found;
72
+ }
73
+
74
+ /** Resolve a relative specifier the way tsc does, extensionless first. */
75
+ function resolveSourceSpecifier(fileSystem, fromDirectory, specifier) {
76
+ const base = path.resolve(fromDirectory, specifier);
77
+ const candidates = [
78
+ base,
79
+ `${base}.ts`,
80
+ `${base}.tsx`,
81
+ `${base}.json`,
82
+ path.join(base, 'index.ts'),
83
+ path.join(base, 'index.tsx'),
84
+ ];
85
+ for (const candidate of candidates) {
86
+ try {
87
+ if (fileSystem.statSync(candidate).isFile()) return candidate;
88
+ } catch {
89
+ // try the next shape
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+
95
+ /**
96
+ * The digest of everything a single command is compiled FROM: its own source
97
+ * plus the transitive closure of the relative imports it reaches.
98
+ *
99
+ * This replaces a digest over the whole of `src` as the freshness test for one
100
+ * command, and the difference is not an optimisation — it is the difference
101
+ * between a true statement and a false one. The whole-tree digest answers "has
102
+ * anything in the CLI changed", and that was used to answer "is THIS command's
103
+ * build current". In a checkout where another session is editing a different
104
+ * command — routine here, see root CLAUDE.md on ~35 worktrees sharing one
105
+ * checkout — those answers disagree constantly, and the launcher then refuses a
106
+ * command whose own build it can prove is byte-current, or silently delegates it
107
+ * to whatever older CLI is on PATH. A dogfood run spent its first twenty minutes
108
+ * concluding that a shipped feature had not shipped, because the commands under
109
+ * test were being answered by a build that predated them.
110
+ *
111
+ * Commands do import other commands (`tasks update` imports `tasks create`), so
112
+ * the closure is walked rather than assuming command sources are independent —
113
+ * that assumption is false here, and a guard built on it would call a genuinely
114
+ * stale command fresh, which is the failure worth avoiding.
115
+ *
116
+ * An unresolvable specifier is hashed as its own text. The build script and this
117
+ * launcher run this same function, so they agree on that value; and a specifier
118
+ * that starts resolving later changes the digest, which is the correct verdict.
119
+ */
120
+ function commandClosureDigest(fileSystem, sourceRoot, entrySource) {
121
+ const seen = new Set();
122
+ const unresolved = [];
123
+ const pending = [path.resolve(entrySource)];
124
+ while (pending.length) {
125
+ const current = pending.pop();
126
+ if (seen.has(current)) continue;
127
+ seen.add(current);
128
+ let text;
129
+ try {
130
+ text = fileSystem.readFileSync(current, 'utf8');
131
+ } catch {
132
+ continue;
133
+ }
134
+ for (const specifier of relativeSpecifiers(text)) {
135
+ const resolved = resolveSourceSpecifier(fileSystem, path.dirname(current), specifier);
136
+ if (resolved) {
137
+ if (!seen.has(resolved)) pending.push(resolved);
138
+ } else {
139
+ unresolved.push(`${path.relative(sourceRoot, current)}:${specifier}`);
140
+ }
141
+ }
142
+ }
143
+ const hash = crypto.createHash('sha256');
144
+ for (const file of [...seen].sort()) {
145
+ hash.update(path.relative(sourceRoot, file).split(path.sep).join('/'));
146
+ hash.update('\0');
147
+ try {
148
+ hash.update(fileSystem.readFileSync(file));
149
+ } catch {
150
+ hash.update('<unreadable>');
151
+ }
152
+ hash.update('\0');
153
+ }
154
+ for (const miss of unresolved.sort()) {
155
+ hash.update(miss.split(path.sep).join('/'));
156
+ hash.update('\0');
157
+ }
158
+ return hash.digest('hex');
159
+ }
160
+
51
161
  function findAlternateCli({
52
162
  commandName,
53
163
  currentEntry,
@@ -143,9 +253,17 @@ function missingLocalCommandBuild({ cliRoot, argv, fileSystem = fs, verifySource
143
253
  } catch {
144
254
  commandBuildManifest = null;
145
255
  }
146
- const currentSourceTreeDigest = commandBuildManifest
147
- ? sourceTreeDigest(fileSystem, path.join(cliRoot, 'src'))
148
- : null;
256
+ // Lazily, because the closure test below almost always settles the question
257
+ // first and this one reads every `.ts` under `src` — 1,300 files here, on a
258
+ // path that runs before any command starts.
259
+ let sourceTreeDigestCache;
260
+ const currentSourceTreeDigest = () => {
261
+ if (!commandBuildManifest) return null;
262
+ if (sourceTreeDigestCache === undefined) {
263
+ sourceTreeDigestCache = sourceTreeDigest(fileSystem, path.join(cliRoot, 'src'));
264
+ }
265
+ return sourceTreeDigestCache;
266
+ };
149
267
  const commandParts = [];
150
268
  for (const value of argv) {
151
269
  if (String(value).startsWith('-')) break;
@@ -165,11 +283,26 @@ function missingLocalCommandBuild({ cliRoot, argv, fileSystem = fs, verifySource
165
283
  if (!verifySourceIdentity) return null;
166
284
  try {
167
285
  const relative = path.relative(sourceCommandsDirectory, source).split(path.sep).join('/');
286
+ // What this command is actually compiled from: its own source plus the
287
+ // transitive closure of its relative imports. Scoped to the command
288
+ // because a change to an unrelated one cannot reach it, and walked rather
289
+ // than assumed because commands here do import each other.
290
+ const builtClosure = commandBuildManifest?.closures?.[relative];
291
+ if (typeof builtClosure === 'string') {
292
+ return builtClosure === commandClosureDigest(fileSystem, path.join(cliRoot, 'src'), source)
293
+ ? null
294
+ : compiled;
295
+ }
168
296
  const builtDigest = commandBuildManifest?.commands?.[relative];
297
+ // A manifest written before closures existed. Fall back to the old, wider
298
+ // test rather than trusting a per-file digest that cannot see an edited
299
+ // import — an over-cautious verdict is recoverable, a wrong "fresh" is
300
+ // what this launcher exists to prevent.
301
+ //
169
302
  // Git checkouts and partial builds can make old JS newer than its source.
170
303
  // Content identity is authoritative; no digest means the development
171
304
  // build is unproved and the installed CLI is safer.
172
- return commandBuildManifest?.sourceTreeDigest === currentSourceTreeDigest &&
305
+ return commandBuildManifest?.sourceTreeDigest === currentSourceTreeDigest() &&
173
306
  typeof builtDigest === 'string' &&
174
307
  builtDigest === sourceDigest(fileSystem, source)
175
308
  ? null
@@ -188,6 +321,35 @@ function missingLocalCommandBuild({ cliRoot, argv, fileSystem = fs, verifySource
188
321
  if (incomplete) return incomplete;
189
322
  break;
190
323
  }
324
+ /*
325
+ * `--version` must report the binary that will actually RUN.
326
+ *
327
+ * It names no command, so the loop above never ran and this returned null —
328
+ * meaning `skrr --version` answered from the local checkout while every real
329
+ * command silently delegated to a different build. Someone verifying a change
330
+ * saw the version they expected and tested code they had not written, which is
331
+ * the precise failure this launcher exists to prevent, wearing a reassuring
332
+ * number.
333
+ *
334
+ * The whole-tree digest is the right test here: a stale tree means SOME
335
+ * command would fall back, so the version the user is about to rely on is not
336
+ * this one.
337
+ */
338
+ if (argv.some((value) => value === '--version' || value === '-v')) {
339
+ // A STALE tree and a MISSING build are the same answer to the user's
340
+ // question. The digest comparison covers the first and skipped the second,
341
+ // because a wiped or half-written `dist` has no manifest to compare — so
342
+ // `--version` still answered from the checkout while every command fell
343
+ // back. Observed twice in one dogfood session, both times right after a
344
+ // rebuild left `dist` incomplete.
345
+ if (verifySourceIdentity && !commandBuildManifest) return commandsDirectory;
346
+ if (
347
+ verifySourceIdentity &&
348
+ commandBuildManifest.sourceTreeDigest !== currentSourceTreeDigest()
349
+ ) {
350
+ return commandsDirectory;
351
+ }
352
+ }
191
353
  if (argv.some((value) => value === '--help' || value === '-h')) {
192
354
  const sourceDirectory = path.join(sourceCommandsDirectory, ...commandParts);
193
355
  const pending = [sourceDirectory];
@@ -215,6 +377,7 @@ function missingLocalCommandBuild({ cliRoot, argv, fileSystem = fs, verifySource
215
377
 
216
378
  module.exports = {
217
379
  COMMAND_BUILD_MANIFEST,
380
+ commandClosureDigest,
218
381
  findAlternateCli,
219
382
  missingLocalCommandBuild,
220
383
  sourceTreeDigest,
@@ -16,6 +16,7 @@ exports.isTypedServerRefusal = isTypedServerRefusal;
16
16
  exports.isRetryableFailure = isRetryableFailure;
17
17
  const core_1 = require("@oclif/core");
18
18
  Object.defineProperty(exports, "Flags", { enumerable: true, get: function () { return core_1.Flags; } });
19
+ const failed_lookup_1 = require("./lib/failed-lookup");
19
20
  // Re-exported from its home in `lib/keychain`, beside the other profile
20
21
  // functions. It lived here while `init()` was its only caller; a lib module
21
22
  // needing it would otherwise have to import the command base class, and with
@@ -112,7 +113,8 @@ class BaseCommand extends core_1.Command {
112
113
  }),
113
114
  profile: core_1.Flags.string({
114
115
  description: 'Which stored session to use. Each profile keeps its own credential and deployment, ' +
115
- 'so signing into one does not sign you out of another. Also OVERSKY_PROFILE.',
116
+ 'so signing into one does not sign you out of another. Also OVERSKY_PROFILE. ' +
117
+ 'List them with `profiles list`.',
116
118
  }),
117
119
  };
118
120
  cliConfig;
@@ -391,6 +393,14 @@ class BaseCommand extends core_1.Command {
391
393
  // UUID, so refs go through the same exact-match resolution as titles.
392
394
  if (!query || (0, space_resolver_1.looksLikeCanonicalSpaceId)(query))
393
395
  return query;
396
+ if ((0, failed_lookup_1.isFailedLookupValue)(query)) {
397
+ this.failWithCliError({
398
+ message: (0, failed_lookup_1.describeFailedLookupValue)(query, 'Space'),
399
+ code: space_resolver_1.SPACE_CLI_ERROR_CODE.NOT_A_REFERENCE,
400
+ exit: 2,
401
+ retryable: false,
402
+ });
403
+ }
394
404
  const normalizedQuery = (0, space_resolver_1.normalizeSpaceRef)(query);
395
405
  const load = async (includeArchived) => {
396
406
  let response;
@@ -489,6 +499,16 @@ class BaseCommand extends core_1.Command {
489
499
  const query = value.trim();
490
500
  if (!query)
491
501
  return query;
502
+ // Before the search, not after it: `null` from a failed `jq -r` must never
503
+ // reach the title tiers, and it should not cost a request to say so.
504
+ if ((0, failed_lookup_1.isFailedLookupValue)(query)) {
505
+ this.failWithCliError({
506
+ message: (0, failed_lookup_1.describeFailedLookupValue)(query, 'task'),
507
+ code: task_ref_resolver_1.TASK_REF_CLI_ERROR_CODE.NOT_A_REFERENCE,
508
+ exit: 2,
509
+ retryable: false,
510
+ });
511
+ }
492
512
  let response;
493
513
  try {
494
514
  response = await data_provider_1.dataService.listTasks({ q: query, limit: 50 });
@@ -690,6 +710,7 @@ class BaseCommand extends core_1.Command {
690
710
  AMBIGUOUS: 'ASSIGNEE_AMBIGUOUS',
691
711
  NO_SPACE: 'ASSIGNEE_NO_SPACE_CONTEXT',
692
712
  NOT_FOUND: 'ASSIGNEE_NOT_FOUND',
713
+ NOT_A_REFERENCE: 'ASSIGNEE_NOT_A_REFERENCE',
693
714
  };
694
715
  this.failWithCliError({
695
716
  message: outcome.failures
@@ -1231,7 +1252,21 @@ class BaseCommand extends core_1.Command {
1231
1252
  //
1232
1253
  // Field-level validation is appended for the same reason: the server names
1233
1254
  // the offending field and only `--json` callers could read it.
1234
- this.error(`${message}${formatValidationIssues(details)}`, { exit, code });
1255
+ //
1256
+ // And the REMEDY, when the message does not already carry one. This CLI's
1257
+ // convention is a `→` remedy line inside the message (103 sites), with
1258
+ // `details.suggestion` as its machine-readable twin for `--json`. Where a
1259
+ // call site wrote the remedy ONLY into `details.suggestion`, it reached
1260
+ // `--json` alone, and a person at a terminal was told what was wrong and
1261
+ // never what to do — `followups list` without an agent, OSK-9670. oclif
1262
+ // renders `suggestions` as its own "Try this:" block. Skipped when the
1263
+ // message opens a `→` line, so those do not print the remedy twice.
1264
+ const suggestion = hasRemedyLine(message) ? undefined : authoredSuggestion(details);
1265
+ this.error(`${message}${formatValidationIssues(details)}`, {
1266
+ exit,
1267
+ code,
1268
+ ...(suggestion ? { suggestions: [suggestion] } : {}),
1269
+ });
1235
1270
  }
1236
1271
  writeJsonCliError({ message, code, status, exit, retryable, details, }) {
1237
1272
  this.log(JSON.stringify({
@@ -1612,6 +1647,15 @@ function isRetryableFailure(status, body) {
1612
1647
  return false;
1613
1648
  return status === 408 || status === 425 || status === 429 || !!(status && status >= 500);
1614
1649
  }
1650
+ /** True when a message already carries this CLI's `→` remedy line. */
1651
+ function hasRemedyLine(message) {
1652
+ return /^\s*→/m.test(message);
1653
+ }
1654
+ /** The remedy a call site wrote into `details.suggestion`, if it wrote one. */
1655
+ function authoredSuggestion(details) {
1656
+ const suggestion = details?.suggestion;
1657
+ return typeof suggestion === 'string' && suggestion.trim() ? suggestion.trim() : undefined;
1658
+ }
1615
1659
  /**
1616
1660
  * Render server-supplied field validation into the human error.
1617
1661
  *
@@ -46,7 +46,7 @@ class AgentsActionsCreate extends base_command_1.BaseCommand {
46
46
  space: core_1.Flags.string({ description: 'Offer only in this space (implies --scope space)' }),
47
47
  'space-title': core_1.Flags.string({ description: 'Denormalized space title for the scope chip' }),
48
48
  label: core_1.Flags.string({
49
- description: 'Offer in every space carrying this label (repeatable, any-of; implies --scope label)',
49
+ description: 'Offer in every space carrying this Label — a Label id or name from `labels list` (repeatable, any-of; implies --scope label). Not button text: that is `spaces actions add --label`.',
50
50
  multiple: true,
51
51
  }),
52
52
  outcome: core_1.Flags.string({
@@ -70,7 +70,7 @@ class AgentsActionsCreate extends base_command_1.BaseCommand {
70
70
  enabled: flags.enabled,
71
71
  // Flags win over --from-json, and a JSON body may still carry these
72
72
  // directly. Both paths land on the same two fields below.
73
- scope: (0, agent_actions_1.resolveAgentActionScopeFlags)(flags),
73
+ scope: await (0, agent_actions_1.resolveAgentActionScopeFlags)(flags),
74
74
  outcome: flags.outcome,
75
75
  });
76
76
  }
@@ -78,14 +78,17 @@ class AgentsActionsCreate extends base_command_1.BaseCommand {
78
78
  this.error(err.message, { exit: 1 });
79
79
  }
80
80
  let created;
81
+ let replayed = false;
81
82
  try {
82
- const agent = await (0, agent_actions_1.fetchAgentForActionEdit)(args.agentId);
83
- const actions = (0, agent_actions_1.getAgentActions)(agent);
84
83
  const title = typeof body.title === 'string' ? body.title.trim() : '';
85
84
  if (!title)
86
85
  this.error('Action title is required. Pass --title or include title.', { exit: 1 });
87
86
  const kind = body.kind === 'builtin' ? 'builtin' : 'prompt';
88
- const order = typeof body.order === 'number' ? body.order : (0, agent_actions_1.nextActionOrder)(actions);
87
+ // No read of the library: position is assigned by the server at the
88
+ // moment of the append, so it cannot be computed from a stale copy. An
89
+ // explicit --order is still honoured.
90
+ const explicitOrder = typeof body.order === 'number' ? body.order : undefined;
91
+ const order = explicitOrder ?? 0;
89
92
  const enabled = typeof body.enabled === 'boolean' ? body.enabled : true;
90
93
  const scope = body.scope;
91
94
  const outcome = body.outcome === 'durable' || body.outcome === 'ephemeral'
@@ -140,7 +143,13 @@ class AgentsActionsCreate extends base_command_1.BaseCommand {
140
143
  sourceListingId: typeof body.sourceListingId === 'string' ? body.sourceListingId : undefined,
141
144
  });
142
145
  }
143
- await (0, agent_actions_1.updateAgentActions)(args.agentId, [...actions, created]);
146
+ if (explicitOrder === undefined) {
147
+ delete created.order;
148
+ }
149
+ const appended = await (0, agent_actions_1.appendAgentAction)(args.agentId, created);
150
+ // Report what the server stored, not what we sent.
151
+ created = appended.action;
152
+ replayed = !appended.created;
144
153
  }
145
154
  catch (err) {
146
155
  this.handleApiError(err);
@@ -149,7 +158,9 @@ class AgentsActionsCreate extends base_command_1.BaseCommand {
149
158
  this.log(JSON.stringify(created, null, 2));
150
159
  return;
151
160
  }
152
- this.log(`Created action on agent ${args.agentId}:`);
161
+ this.log(replayed
162
+ ? `Action ${created.id} was already on agent ${args.agentId} (nothing added):`
163
+ : `Created action on agent ${args.agentId}:`);
153
164
  (0, agent_actions_1.renderAction)(created, (line) => this.log(line));
154
165
  }
155
166
  }
@@ -8,6 +8,7 @@ export default class AgentsActionsDelete extends BaseCommand {
8
8
  };
9
9
  static flags: {
10
10
  yes: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
11
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
11
12
  };
12
13
  run(): Promise<void>;
13
14
  }
@@ -16,6 +16,9 @@ class AgentsActionsDelete extends base_command_1.BaseCommand {
16
16
  };
17
17
  static flags = {
18
18
  yes: core_1.Flags.boolean({ char: 'y', description: 'Skip confirmation prompt' }),
19
+ // The only sibling (create, update, list, show) without --json, so a script
20
+ // driving the five broke on the fifth.
21
+ json: core_1.Flags.boolean({ description: 'Output the result as JSON' }),
19
22
  };
20
23
  async run() {
21
24
  this.requireAuth();
@@ -37,6 +40,10 @@ class AgentsActionsDelete extends base_command_1.BaseCommand {
37
40
  catch (err) {
38
41
  this.handleApiError(err);
39
42
  }
43
+ if (flags.json) {
44
+ this.log(JSON.stringify({ deleted: true, agentId: args.agentId, actionId: args.actionId }, null, 2));
45
+ return;
46
+ }
40
47
  this.log(`Deleted action ${args.actionId} from agent ${args.agentId}.`);
41
48
  }
42
49
  }
@@ -42,7 +42,7 @@ class AgentsActionsUpdate extends base_command_1.BaseCommand {
42
42
  space: core_1.Flags.string({ description: 'Offer only in this space (implies --scope space)' }),
43
43
  'space-title': core_1.Flags.string({ description: 'Denormalized space title for the scope chip' }),
44
44
  label: core_1.Flags.string({
45
- description: 'Offer in every space carrying this label (repeatable, any-of; implies --scope label)',
45
+ description: 'Offer in every space carrying this Label — a Label id or name from `labels list` (repeatable, any-of; implies --scope label). Not button text: that is `spaces actions add --label`.',
46
46
  multiple: true,
47
47
  }),
48
48
  outcome: core_1.Flags.string({
@@ -63,7 +63,7 @@ class AgentsActionsUpdate extends base_command_1.BaseCommand {
63
63
  config: (0, agent_actions_1.parseJsonObjectFlag)(flags.config, '--config'),
64
64
  order: flags.order,
65
65
  enabled: flags.enabled,
66
- scope: (0, agent_actions_1.resolveAgentActionScopeFlags)(flags),
66
+ scope: await (0, agent_actions_1.resolveAgentActionScopeFlags)(flags),
67
67
  outcome: flags.outcome,
68
68
  });
69
69
  }
@@ -125,7 +125,9 @@ class AgentsActionsUpdate extends base_command_1.BaseCommand {
125
125
  }
126
126
  actions[index] = next;
127
127
  updated = next;
128
- await (0, agent_actions_1.updateAgentActions)(args.agentId, actions);
128
+ const savedAgent = await (0, agent_actions_1.updateAgentActions)(args.agentId, actions);
129
+ // Report what the server stored, not what we sent — see storedActionOrLocal.
130
+ updated = (0, agent_actions_1.storedActionOrLocal)(savedAgent, updated);
129
131
  }
130
132
  catch (err) {
131
133
  this.handleApiError(err);
@@ -165,9 +165,12 @@ class AgentsApiActionsAdd extends base_command_1.BaseCommand {
165
165
  path: props.path,
166
166
  })),
167
167
  };
168
+ // Say it was a dry run: the output used to be indistinguishable from what
169
+ // a real add would have printed, so a reader could not tell whether
170
+ // anything had been written.
168
171
  this.log(flags.json
169
- ? JSON.stringify(preview, null, 2)
170
- : `${preview.functions.length} operation(s) on ${domain}:\n` +
172
+ ? JSON.stringify({ dryRun: true, written: false, ...preview }, null, 2)
173
+ : `Dry run — nothing was written. ${preview.functions.length} operation(s) on ${domain}:\n` +
171
174
  preview.functions
172
175
  .map((f) => ` ${f.name} ${f.method.toUpperCase()} ${f.path}`)
173
176
  .join('\n'));
@@ -17,6 +17,17 @@ import { BaseCommand } from '../../../base-command';
17
17
  * the other two would break muscle memory for a surface that works, so this one
18
18
  * takes an unambiguous name and all three now say which concept they are.
19
19
  */
20
+ /**
21
+ * The operation names an action exposes, parsed from its stored spec by the same
22
+ * parser `add` and the web builder use. The listing used to show id, agent,
23
+ * domain and auth — never what the action can DO — so answering "which tools does
24
+ * this give the agent" meant reading the raw spec.
25
+ */
26
+ export declare function operationNames(action: {
27
+ metadata?: {
28
+ raw_spec?: string;
29
+ };
30
+ }): string[];
20
31
  export default class AgentsApiActionsList extends BaseCommand {
21
32
  static description: string;
22
33
  static examples: string[];
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.operationNames = operationNames;
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");
@@ -22,6 +23,21 @@ const format_1 = require("../../../lib/format");
22
23
  * the other two would break muscle memory for a surface that works, so this one
23
24
  * takes an unambiguous name and all three now say which concept they are.
24
25
  */
26
+ /**
27
+ * The operation names an action exposes, parsed from its stored spec by the same
28
+ * parser `add` and the web builder use. The listing used to show id, agent,
29
+ * domain and auth — never what the action can DO — so answering "which tools does
30
+ * this give the agent" meant reading the raw spec.
31
+ */
32
+ function operationNames(action) {
33
+ const raw = action.metadata?.raw_spec;
34
+ if (!raw)
35
+ return [];
36
+ const parsed = (0, data_provider_1.validateAndParseOpenAPISpec)(raw);
37
+ if (!parsed.status || !parsed.spec)
38
+ return [];
39
+ return Object.keys((0, data_provider_1.openapiToFunction)(parsed.spec).requestBuilders);
40
+ }
25
41
  class AgentsApiActionsList extends base_command_1.BaseCommand {
26
42
  static description = 'List OpenAPI-backed Actions on agents you can edit (NOT `agents actions`, which is embedded buttons)';
27
43
  static examples = [
@@ -51,7 +67,15 @@ class AgentsApiActionsList extends base_command_1.BaseCommand {
51
67
  ? actions.filter((action) => action.agent_id === flags.agent)
52
68
  : actions;
53
69
  if (flags.json) {
54
- this.log(JSON.stringify(rows, null, 2));
70
+ // Database internals (`_id`, `__v`) are not part of an action; a script
71
+ // reading this should not learn to depend on them.
72
+ const clean = rows.map((action) => {
73
+ const { _id, __v, ...rest } = action;
74
+ void _id;
75
+ void __v;
76
+ return { ...rest, operations: operationNames(action) };
77
+ });
78
+ this.log(JSON.stringify(clean, null, 2));
55
79
  return;
56
80
  }
57
81
  if (rows.length === 0) {
@@ -66,11 +90,13 @@ class AgentsApiActionsList extends base_command_1.BaseCommand {
66
90
  agentId: action.agent_id ?? '-',
67
91
  domain: action.metadata?.domain ?? '-',
68
92
  auth: action.metadata?.auth?.type ?? 'none',
93
+ operations: operationNames(action).join(', ') || '-',
69
94
  })), [
70
95
  { key: 'actionId', header: 'ACTION ID', verbatim: true },
71
96
  { key: 'agentId', header: 'AGENT ID', verbatim: true },
72
97
  { key: 'domain', header: 'DOMAIN', maxWidth: 40 },
73
98
  { key: 'auth', header: 'AUTH' },
99
+ { key: 'operations', header: 'OPERATIONS', maxWidth: 48 },
74
100
  ], (line) => this.log(line));
75
101
  }
76
102
  }
@@ -1,4 +1,15 @@
1
1
  import { BaseCommand } from '../../../../base-command';
2
+ /**
3
+ * Which of the two states produced the policy line above it.
4
+ *
5
+ * The route sends `explicit` precisely because the policy alone cannot carry
6
+ * it: `resolveAgentAvatarDefaultPolicy` maps an absent field and a deliberate
7
+ * choice of the same shape onto one value. So "I picked random-from-all" and "I
8
+ * have never picked anything" printed identically, and they are not the same
9
+ * fact — only the second follows the shipped default if it ever changes, which
10
+ * is the whole reason clearing exists as a separate operation from choosing.
11
+ */
12
+ export declare function describePolicyProvenance(explicit: boolean): string;
2
13
  /**
3
14
  * What face a NEW agent gets when its creator does not pick one.
4
15
  *
@@ -1,8 +1,24 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.describePolicyProvenance = describePolicyProvenance;
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");
7
+ /**
8
+ * Which of the two states produced the policy line above it.
9
+ *
10
+ * The route sends `explicit` precisely because the policy alone cannot carry
11
+ * it: `resolveAgentAvatarDefaultPolicy` maps an absent field and a deliberate
12
+ * choice of the same shape onto one value. So "I picked random-from-all" and "I
13
+ * have never picked anything" printed identically, and they are not the same
14
+ * fact — only the second follows the shipped default if it ever changes, which
15
+ * is the whole reason clearing exists as a separate operation from choosing.
16
+ */
17
+ function describePolicyProvenance(explicit) {
18
+ return explicit
19
+ ? ' You chose this. It stays until you change or clear it.'
20
+ : ' You have not chosen one, so this follows the shipped default — including if it changes.';
21
+ }
6
22
  /**
7
23
  * What face a NEW agent gets when its creator does not pick one.
8
24
  *
@@ -36,6 +52,7 @@ class AgentsAvatarDefaultShow extends base_command_1.BaseCommand {
36
52
  return;
37
53
  }
38
54
  this.log((0, data_provider_1.describeAgentAvatarPolicy)(response.policy));
55
+ this.log(describePolicyProvenance(Boolean(response.explicit)));
39
56
  this.log('');
40
57
  this.log('Applies to agents created from now on. Existing agents keep the face they have —');
41
58
  this.log('`skrr agents avatar default apply` is the one path that changes them.');
@@ -12,7 +12,8 @@ export default class AgentsAvatarDefaultSet extends BaseCommand {
12
12
  static description: string;
13
13
  static examples: string[];
14
14
  static flags: {
15
- mode: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
15
+ mode: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
16
+ clear: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
16
17
  ref: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
17
18
  scope: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
18
19
  category: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
@@ -20,12 +20,18 @@ class AgentsAvatarDefaultSet extends base_command_1.BaseCommand {
20
20
  '<%= config.bin %> agents avatar default set --mode fixed --ref preset:kosmo',
21
21
  '<%= config.bin %> agents avatar default set --mode random --scope category --category illustrations',
22
22
  '<%= config.bin %> agents avatar default set --mode random --scope mine',
23
+ '<%= config.bin %> agents avatar default set --clear',
23
24
  ];
24
25
  static flags = {
25
26
  mode: core_1.Flags.string({
26
27
  description: 'platform = the shipped logo; fixed = one avatar; random = a different one each time',
27
28
  options: ['platform', 'fixed', 'random'],
28
- required: true,
29
+ exclusive: ['clear'],
30
+ }),
31
+ clear: core_1.Flags.boolean({
32
+ description: 'Remove your policy entirely, so new agents follow whatever the workspace or the ' +
33
+ 'platform ships. NOT the same as --mode platform, which pins the logo.',
34
+ exclusive: ['mode', 'ref', 'scope', 'category'],
29
35
  }),
30
36
  ref: core_1.Flags.string({
31
37
  description: '--mode fixed: which avatar. default, preset:<id>, asset:<id>, or image:<filepath>@<source>',
@@ -70,12 +76,22 @@ class AgentsAvatarDefaultSet extends base_command_1.BaseCommand {
70
76
  async run() {
71
77
  this.requireAuth();
72
78
  const { flags } = await this.parse(AgentsAvatarDefaultSet);
73
- let policy;
74
- try {
75
- policy = this.buildPolicy(flags);
79
+ // `--mode` was `required`, which made the cleared state — the one the write
80
+ // route goes out of its way to support — unreachable from here: every value
81
+ // of `--mode` WRITES a policy, so once this command had been run there was
82
+ // no way back to "no opinion" short of calling the API by hand.
83
+ if (!flags.clear && !flags.mode) {
84
+ this.error('Choose --mode platform|fixed|random to set a policy, or --clear to remove yours so ' +
85
+ 'new agents follow whatever the workspace or the platform ships.', { exit: 1 });
76
86
  }
77
- catch (err) {
78
- this.error(err.message, { exit: 1 });
87
+ let policy = null;
88
+ if (!flags.clear) {
89
+ try {
90
+ policy = this.buildPolicy(flags);
91
+ }
92
+ catch (err) {
93
+ this.error(err.message, { exit: 1 });
94
+ }
79
95
  }
80
96
  let response;
81
97
  try {
@@ -88,7 +104,16 @@ class AgentsAvatarDefaultSet extends base_command_1.BaseCommand {
88
104
  this.log(JSON.stringify(response, null, 2));
89
105
  return;
90
106
  }
91
- this.log(`Default avatar policy: ${(0, data_provider_1.describeAgentAvatarPolicy)(response.policy)}`);
107
+ if (flags.clear) {
108
+ // Say what it FOLLOWS now, not just that something was removed — the
109
+ // value is the same one an explicit `--mode random --scope all` would
110
+ // print, and the difference between them is the whole point of clearing.
111
+ this.log(`Cleared. New agents follow the current default: ${(0, data_provider_1.describeAgentAvatarPolicy)(response.policy)}.`);
112
+ this.log('If that default changes later, this account follows it.');
113
+ }
114
+ else {
115
+ this.log(`Default avatar policy: ${(0, data_provider_1.describeAgentAvatarPolicy)(response.policy)}`);
116
+ }
92
117
  this.log('Existing agents are unchanged. `skrr agents avatar default apply` changes those.');
93
118
  }
94
119
  }