@skrr-ai/cli 0.1.9 → 0.1.11

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 (109) hide show
  1. package/dist/base-command.d.ts +1 -13
  2. package/dist/base-command.js +78 -1
  3. package/dist/commands/browser/skill/show.js +7 -1
  4. package/dist/commands/code/index.d.ts +1 -0
  5. package/dist/commands/code/index.js +9 -1
  6. package/dist/commands/commitments/analytics/index.js +2 -0
  7. package/dist/commands/daemon/byok.d.ts +1 -0
  8. package/dist/commands/daemon/byok.js +2 -1
  9. package/dist/commands/daemon/install.d.ts +1 -0
  10. package/dist/commands/daemon/install.js +2 -1
  11. package/dist/commands/daemon/login.d.ts +31 -0
  12. package/dist/commands/daemon/login.js +56 -0
  13. package/dist/commands/daemon/restart.d.ts +9 -0
  14. package/dist/commands/daemon/restart.js +36 -0
  15. package/dist/commands/daemon/start.d.ts +1 -0
  16. package/dist/commands/daemon/start.js +2 -1
  17. package/dist/commands/daemon/status.d.ts +1 -0
  18. package/dist/commands/daemon/status.js +2 -1
  19. package/dist/commands/daemon/stop.d.ts +1 -0
  20. package/dist/commands/daemon/stop.js +2 -1
  21. package/dist/commands/daemon/uninstall.d.ts +1 -0
  22. package/dist/commands/daemon/uninstall.js +2 -1
  23. package/dist/commands/daemon/unlock.d.ts +9 -0
  24. package/dist/commands/daemon/unlock.js +33 -0
  25. package/dist/commands/goals/key-results/create.js +32 -1
  26. package/dist/commands/goals/key-results/update.d.ts +11 -0
  27. package/dist/commands/goals/key-results/update.js +80 -2
  28. package/dist/commands/goals/plan-now.d.ts +54 -2
  29. package/dist/commands/goals/plan-now.js +175 -18
  30. package/dist/commands/goals/planner-config.d.ts +60 -9
  31. package/dist/commands/goals/planner-config.js +82 -34
  32. package/dist/commands/goals/revisions.js +17 -0
  33. package/dist/commands/goals/show.d.ts +17 -0
  34. package/dist/commands/goals/show.js +90 -3
  35. package/dist/commands/login.js +36 -3
  36. package/dist/commands/spaces/create.js +2 -1
  37. package/dist/commands/spaces/index.js +9 -1
  38. package/dist/commands/spaces/list.d.ts +18 -0
  39. package/dist/commands/spaces/list.js +57 -7
  40. package/dist/commands/spaces/show.js +4 -1
  41. package/dist/commands/spaces/summary.d.ts +4 -0
  42. package/dist/commands/spaces/summary.js +77 -1
  43. package/dist/commands/spaces/update.d.ts +4 -0
  44. package/dist/commands/spaces/update.js +38 -1
  45. package/dist/commands/tasks/actionability.js +40 -1
  46. package/dist/commands/tasks/activity.d.ts +29 -0
  47. package/dist/commands/tasks/activity.js +47 -0
  48. package/dist/commands/tasks/complete.d.ts +47 -0
  49. package/dist/commands/tasks/complete.js +159 -12
  50. package/dist/commands/tasks/create.d.ts +26 -0
  51. package/dist/commands/tasks/create.js +60 -1
  52. package/dist/commands/tasks/events/append.d.ts +2 -0
  53. package/dist/commands/tasks/events/append.js +41 -10
  54. package/dist/commands/tasks/events/list.js +60 -9
  55. package/dist/commands/tasks/output.js +22 -2
  56. package/dist/commands/tasks/ready.d.ts +38 -0
  57. package/dist/commands/tasks/ready.js +37 -0
  58. package/dist/commands/tasks/runs.d.ts +22 -0
  59. package/dist/commands/tasks/runs.js +110 -2
  60. package/dist/commands/tasks/show.d.ts +28 -0
  61. package/dist/commands/tasks/show.js +61 -0
  62. package/dist/commands/tasks/timeline.d.ts +7 -0
  63. package/dist/commands/tasks/timeline.js +34 -3
  64. package/dist/commands/tasks/update.d.ts +20 -0
  65. package/dist/commands/tasks/update.js +38 -1
  66. package/dist/commands/whoami.d.ts +40 -0
  67. package/dist/commands/whoami.js +61 -10
  68. package/dist/commands/wiki/ls.d.ts +23 -0
  69. package/dist/commands/wiki/ls.js +63 -10
  70. package/dist/commands/wiki/mv.d.ts +70 -0
  71. package/dist/commands/wiki/mv.js +198 -4
  72. package/dist/commands/wiki/rm.js +12 -1
  73. package/dist/commands/wiki/write.js +35 -3
  74. package/dist/help.d.ts +27 -0
  75. package/dist/help.js +48 -0
  76. package/dist/hooks/command-not-found.d.ts +31 -0
  77. package/dist/hooks/command-not-found.js +12 -80
  78. package/dist/lib/command-miss.d.ts +60 -0
  79. package/dist/lib/command-miss.js +128 -0
  80. package/dist/lib/commitment-analytics.d.ts +10 -0
  81. package/dist/lib/commitment-analytics.js +10 -0
  82. package/dist/lib/config.d.ts +40 -0
  83. package/dist/lib/config.js +72 -9
  84. package/dist/lib/daemonBroker.d.ts +7 -1
  85. package/dist/lib/daemonBroker.js +71 -0
  86. package/dist/lib/daemonHandoff.d.ts +38 -0
  87. package/dist/lib/daemonHandoff.js +297 -0
  88. package/dist/lib/exec-oversky.d.ts +30 -0
  89. package/dist/lib/exec-oversky.js +41 -0
  90. package/dist/lib/format.d.ts +7 -0
  91. package/dist/lib/format.js +27 -5
  92. package/dist/lib/login.js +22 -2
  93. package/dist/lib/sky-code.js +9 -4
  94. package/dist/lib/task-transcript.d.ts +9 -0
  95. package/dist/lib/task-transcript.js +13 -2
  96. package/dist/lib/tasks.d.ts +34 -0
  97. package/dist/lib/tasks.js +70 -1
  98. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.d.ts +50 -0
  99. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.js +67 -0
  100. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +7 -1
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.d.ts +50 -0
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.js +59 -0
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
  105. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +3 -0
  106. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  107. package/dist/node_modules/@skrr-ai/data-provider/index.js +15 -4
  108. package/oclif.manifest.json +15713 -15480
  109. package/package.json +5 -3
@@ -4,6 +4,20 @@ const core_1 = require("@oclif/core");
4
4
  const data_provider_1 = require("@skrr-ai/data-provider");
5
5
  const base_command_1 = require("../../base-command");
6
6
  const format_1 = require("../../lib/format");
7
+ /**
8
+ * The one class of goal change this log does not contain.
9
+ *
10
+ * `PATCH /api/goals/:goalId/planner-config` writes the goal through
11
+ * `Goal.findOneAndUpdate` directly rather than `GoalModel.updateGoal`, which is
12
+ * where `RevisionService` is called from — so arming, re-timing or disarming the
13
+ * auto-planner (and any `maxActiveTasks` change made through that route) leaves
14
+ * no revision. "Who turned the planner on for this goal, and when" is the audit
15
+ * question a shared goal actually raises, and the table answers it with silence.
16
+ *
17
+ * Naming the gap is the honest thing the CLI can do; closing it is a server
18
+ * change. DELETE THIS FOOTER once the planner-config route records a revision.
19
+ */
20
+ const PLANNER_GAP_NOTE = 'Note: auto-planner changes made with `goals planner-config` are not recorded as revisions.';
7
21
  class GoalsRevisions extends base_command_1.BaseCommand {
8
22
  static description = 'List revisions for a goal';
9
23
  static examples = [
@@ -33,6 +47,7 @@ class GoalsRevisions extends base_command_1.BaseCommand {
33
47
  const items = result?.data ?? [];
34
48
  if (items.length === 0) {
35
49
  this.log('No revisions.');
50
+ this.log(PLANNER_GAP_NOTE);
36
51
  return;
37
52
  }
38
53
  const rows = items.map((rev) => ({
@@ -49,6 +64,8 @@ class GoalsRevisions extends base_command_1.BaseCommand {
49
64
  { key: 'changes', header: 'FIELDS' },
50
65
  { key: 'when', header: 'AT' },
51
66
  ], (line) => this.log(line));
67
+ this.log('');
68
+ this.log(PLANNER_GAP_NOTE);
52
69
  }
53
70
  }
54
71
  exports.default = GoalsRevisions;
@@ -1,4 +1,12 @@
1
1
  import { BaseCommand } from '../../base-command';
2
+ /**
3
+ * The completion row, labelled by what it counts.
4
+ *
5
+ * `null`/absent is rendered `-` rather than `0%`: the server returns null when a
6
+ * goal has NO linked tasks, and printing 0% there says "nothing is done" about a
7
+ * denominator that does not exist.
8
+ */
9
+ export declare function taskCompletionLine(completionPercent: unknown): string;
2
10
  export default class GoalsShow extends BaseCommand {
3
11
  static description: string;
4
12
  static examples: string[];
@@ -9,4 +17,13 @@ export default class GoalsShow extends BaseCommand {
9
17
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
18
  };
11
19
  run(): Promise<void>;
20
+ /**
21
+ * The goal's key results, best-effort.
22
+ *
23
+ * A second request, and a failure here must not take the goal down with it —
24
+ * `goals show` answering "here is your goal, minus its key results" is far
25
+ * better than answering nothing. `null` distinguishes "could not read" from
26
+ * "there are none", so neither view claims an empty OKR.
27
+ */
28
+ private readKeyResults;
12
29
  }
@@ -1,9 +1,37 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskCompletionLine = taskCompletionLine;
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
+ const format_1 = require("../../lib/format");
6
8
  const web_url_1 = require("../../lib/web-url");
9
+ const planner_config_1 = require("./planner-config");
10
+ /**
11
+ * What `_completionPercent` actually measures.
12
+ *
13
+ * `GoalEnrichment.enrichGoalCompletion` computes it as done ÷ total over the
14
+ * goal's LINKED TASKS. Key-result progress is a parallel axis that never enters
15
+ * it. Rendering that number under the bare label `Progress:` on an OKR goal made
16
+ * it read as "the goal is half achieved" when it meant "1 of the 2 tasks I
17
+ * linked is closed" — dogfooded on a goal whose two key results were at 25% and
18
+ * 40% while the goal reported 0%.
19
+ *
20
+ * So the label names its own denominator, and the key results are printed
21
+ * beside it rather than left to a second command nobody knows to run.
22
+ */
23
+ const TASK_COMPLETION_NOTE = 'share of linked tasks closed — key results are a separate axis';
24
+ /**
25
+ * The completion row, labelled by what it counts.
26
+ *
27
+ * `null`/absent is rendered `-` rather than `0%`: the server returns null when a
28
+ * goal has NO linked tasks, and printing 0% there says "nothing is done" about a
29
+ * denominator that does not exist.
30
+ */
31
+ function taskCompletionLine(completionPercent) {
32
+ const value = typeof completionPercent === 'number' ? `${Math.round(completionPercent)}%` : '-';
33
+ return `Tasks done: ${value} (${TASK_COMPLETION_NOTE})`;
34
+ }
7
35
  class GoalsShow extends base_command_1.BaseCommand {
8
36
  static description = 'Show details for a single goal';
9
37
  static examples = [
@@ -28,11 +56,14 @@ class GoalsShow extends base_command_1.BaseCommand {
28
56
  }
29
57
  const g = goal;
30
58
  const url = (0, web_url_1.goalUrlFrom)(this.cliConfig.baseURL, g);
59
+ const planner = (0, planner_config_1.goalPlannerView)(g);
60
+ const keyResults = await this.readKeyResults(args.id);
31
61
  if (flags.json) {
32
- this.log(JSON.stringify({ ...g, url }, null, 2));
62
+ // Additive: the server's own fields are passed through untouched, with
63
+ // the derived planner reading and the goal's key results beside them.
64
+ this.log(JSON.stringify({ ...g, url, planner, keyResults }, null, 2));
33
65
  return;
34
66
  }
35
- const progress = g._completionPercent;
36
67
  this.log(`ID: ${g.id ?? g._id ?? '-'}`);
37
68
  this.log(`Title: ${g.title ?? '-'}`);
38
69
  if (url)
@@ -47,12 +78,68 @@ class GoalsShow extends base_command_1.BaseCommand {
47
78
  this.log(`Target date: ${g.targetDate ? String(g.targetDate).slice(0, 10) : '-'}`);
48
79
  this.log(`DRI: ${g._driName ?? g.driUserId ?? g.driAgentId ?? '-'}`);
49
80
  this.log(`Owner Agent: ${g.ownerAgentId ?? '-'}`);
50
- this.log(`Progress: ${typeof progress === 'number' ? `${Math.round(progress)}%` : '-'}`);
81
+ this.log(taskCompletionLine(g._completionPercent));
82
+ // The single most consequential piece of state on a goal — a recurring
83
+ // automation that creates tasks, and optionally starts agents on them —
84
+ // was readable only through `--json` field names you had to know in
85
+ // advance, or by WRITING to `planner-config`.
86
+ this.log('');
87
+ this.log('Planner:');
88
+ for (const line of (0, planner_config_1.renderGoalPlannerLines)(planner)) {
89
+ this.log(line);
90
+ }
91
+ this.log('');
92
+ if (keyResults === null) {
93
+ this.log('Key results: (not readable — see the warning above)');
94
+ }
95
+ else if (keyResults.length === 0) {
96
+ this.log('Key results: none');
97
+ this.log(` Add one with \`${this.config.bin} goals key-results create ${args.id} --title ...\``);
98
+ }
99
+ else {
100
+ this.log('Key results:');
101
+ (0, format_1.renderTable)(keyResults
102
+ .slice()
103
+ .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
104
+ .map((kr) => {
105
+ const value = (0, data_provider_1.resolveKeyResultProgress)(kr);
106
+ return {
107
+ progress: typeof value === 'number' ? `${Math.round(value)}%` : '-',
108
+ status: String(kr.status ?? '-'),
109
+ mode: String(kr.progressMode ?? '-'),
110
+ title: String(kr.title ?? '-'),
111
+ };
112
+ }), [
113
+ { key: 'progress', header: 'PROGRESS' },
114
+ { key: 'status', header: 'STATUS' },
115
+ { key: 'mode', header: 'MODE' },
116
+ { key: 'title', header: 'TITLE', maxWidth: 48 },
117
+ ], (line) => this.log(` ${line}`));
118
+ }
51
119
  if (g.description) {
52
120
  this.log('');
53
121
  this.log('Description:');
54
122
  this.log(` ${g.description}`);
55
123
  }
56
124
  }
125
+ /**
126
+ * The goal's key results, best-effort.
127
+ *
128
+ * A second request, and a failure here must not take the goal down with it —
129
+ * `goals show` answering "here is your goal, minus its key results" is far
130
+ * better than answering nothing. `null` distinguishes "could not read" from
131
+ * "there are none", so neither view claims an empty OKR.
132
+ */
133
+ async readKeyResults(goalId) {
134
+ try {
135
+ const response = await data_provider_1.dataService.listKeyResults(goalId);
136
+ return response?.data ?? [];
137
+ }
138
+ catch (err) {
139
+ this.warn(`Could not read this goal's key results: ${err.message}. ` +
140
+ `Try \`${this.config.bin} goals key-results list ${goalId}\`.`);
141
+ return null;
142
+ }
143
+ }
57
144
  }
58
145
  exports.default = GoalsShow;
@@ -4,7 +4,9 @@ const core_1 = require("@oclif/core");
4
4
  const data_provider_1 = require("@skrr-ai/data-provider");
5
5
  const auth_core_1 = require("@skrr-ai/auth-core");
6
6
  const base_command_1 = require("../base-command");
7
+ const daemonHandoff_1 = require("../lib/daemonHandoff");
7
8
  const config_1 = require("../lib/config");
9
+ const keychain_1 = require("../lib/keychain");
8
10
  const login_1 = require("../lib/login");
9
11
  const api_fetch_1 = require("../lib/api-fetch");
10
12
  const auth_storage_1 = require("../lib/auth-storage");
@@ -116,6 +118,21 @@ class Login extends base_command_1.BaseCommand {
116
118
  const previousBaseURL = existing.baseURL;
117
119
  const nextBaseURL = (0, config_1.canonicalizeKnownBaseURL)(requestedBaseUrl);
118
120
  if (previousBaseURL && previousBaseURL !== nextBaseURL) {
121
+ // Say what is about to be lost. `--base-url` on the default profile
122
+ // silently REPOINTS it, so a session on the other deployment — and
123
+ // every space, task and board reachable through it — becomes
124
+ // unreachable until another interactive login. The `--profile` help
125
+ // advertises profiles as exactly this isolation ("signing into one does
126
+ // not sign you out of another"), so a reader has good reason to expect
127
+ // the flag to be additive rather than a move.
128
+ //
129
+ // A warning rather than a prompt: `cliLogin` completes without a
130
+ // terminal through the daemon broker, which is the case that matters
131
+ // most on a headless box, and a confirmation would break it.
132
+ const profile = (0, keychain_1.getActiveProfile)();
133
+ this.warn(`Pointing profile "${profile}" at ${(0, config_1.describeDeployment)(nextBaseURL)}. ` +
134
+ `Its ${(0, config_1.describeDeployment)(previousBaseURL)} session will be replaced — ` +
135
+ `sign in with \`--profile <name>\` instead to keep both.`);
119
136
  undoBaseURL = () => {
120
137
  // Re-read rather than reusing `existing`: the flow may have persisted
121
138
  // other fields (cliId, migration markers) before failing, and this
@@ -155,7 +172,21 @@ class Login extends base_command_1.BaseCommand {
155
172
  // Only now is the new pointer earned.
156
173
  undoBaseURL = null;
157
174
  this.log('');
158
- this.log(`Signed in via ${result.flow}. Run \`${this.config.bin} whoami\` to confirm your identity.`);
175
+ this.log(`Signed in via ${result.flow} on ${(0, config_1.describeDeployment)(this.cliConfig.baseURL)}. ` +
176
+ `Run \`${this.config.bin} whoami\` to confirm your identity.`);
177
+ // One login covers the machine: hand the local daemon a credential of its
178
+ // own. Best-effort by construction — the login above has already SUCCEEDED,
179
+ // and a failure here must not fail it or discard what the user just earned.
180
+ // It degrades to exactly the old behaviour, with a hint naming the command
181
+ // that finishes the job.
182
+ const handoff = await (0, daemonHandoff_1.handOffToLocalDaemon)(process.env, { loginFlow: result.flow });
183
+ if (handoff.status === 'delivered') {
184
+ this.log(`The local daemon was signed in too — ${handoff.detail}.`);
185
+ }
186
+ else if (handoff.status === 'failed') {
187
+ this.log(`The local daemon was not signed in (${handoff.detail}). ` +
188
+ `Run \`${this.config.bin} daemon login\` to finish.`);
189
+ }
159
190
  }
160
191
  catch (err) {
161
192
  // Everything structured in login.ts throws Error; surface the
@@ -201,7 +232,8 @@ class Login extends base_command_1.BaseCommand {
201
232
  cliId,
202
233
  });
203
234
  this.log('');
204
- this.log(`Signed in via ${result.flow} (OVERSKY_REFRESH_TOKEN). ` +
235
+ this.log(`Signed in via ${result.flow} (OVERSKY_REFRESH_TOKEN) on ` +
236
+ `${(0, config_1.describeDeployment)(this.cliConfig.baseURL)}. ` +
205
237
  `Run \`${this.config.bin} whoami\` to confirm your identity.`);
206
238
  }
207
239
  catch (err) {
@@ -278,7 +310,8 @@ class Login extends base_command_1.BaseCommand {
278
310
  : undefined,
279
311
  }, { serverOrigin: this.cliConfig.baseURL, clearReauth: true });
280
312
  this.log('');
281
- this.log(`Signed in as ${result.user.email} via recovery code.`);
313
+ this.log(`Signed in as ${result.user.email} via recovery code on ` +
314
+ `${(0, config_1.describeDeployment)(this.cliConfig.baseURL)}.`);
282
315
  this.log(`Run \`${this.config.bin} whoami\` to confirm your identity.`);
283
316
  }
284
317
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const core_1 = require("@oclif/core");
4
+ const data_provider_1 = require("@skrr-ai/data-provider");
4
5
  const outbox_1 = require("../../lib/outbox");
5
6
  const api_fetch_1 = require("../../lib/api-fetch");
6
7
  const base_command_1 = require("../../base-command");
@@ -24,7 +25,7 @@ class SpacesCreate extends base_command_1.BaseCommand {
24
25
  }),
25
26
  title: core_1.Flags.string({ description: 'Space title (required unless set via --from-json)' }),
26
27
  description: core_1.Flags.string({ description: 'Space description' }),
27
- status: core_1.Flags.string({ description: 'Space status' }),
28
+ status: core_1.Flags.string({ description: 'Space status', options: [...data_provider_1.SPACE_STATUSES] }),
28
29
  color: core_1.Flags.string({ description: 'Space color (hex, e.g. #4f46e5)' }),
29
30
  'lead-agent': core_1.Flags.string({ description: 'Space Leader agent ID' }),
30
31
  'target-date': core_1.Flags.string({ description: 'Target date (ISO string)' }),
@@ -117,7 +117,15 @@ class Spaces extends base_command_1.BaseCommand {
117
117
  this.log(JSON.stringify({ object: 'space_guide', data: PERSPECTIVES }, null, 2));
118
118
  return;
119
119
  }
120
- this.log('Space perspectives:');
120
+ // Every perspective below needs a `<space-id>`, and this screen used to
121
+ // start at the first of them — so `skrr spaces`, the natural way to orient
122
+ // yourself in the domain, printed ~14 commands a reader had no way to run
123
+ // and never named the two that unblock them. The entry point to a domain
124
+ // has to answer "how do I get one of these" before "what can I do with
125
+ // one".
126
+ this.log('Get a space id: skrr spaces list Make one: skrr spaces create --title "..."');
127
+ this.log('');
128
+ this.log('Space perspectives (each takes a <space-id>):');
121
129
  this.log('');
122
130
  for (const item of PERSPECTIVES) {
123
131
  this.log(`${item.perspective.padEnd(12)} ${item.purpose}`);
@@ -1,4 +1,22 @@
1
1
  import { BaseCommand } from '../../base-command';
2
+ /**
3
+ * What an empty `--q` result is allowed to claim.
4
+ *
5
+ * `No spaces matching "space scratch".` is a confident assertion that nothing
6
+ * matched, and for the sense a person means it is FALSE: both words are in the
7
+ * title of a space they own and created a minute earlier. The server ORs one
8
+ * escaped regex built from the whole query across title/description/identifier,
9
+ * so the query is a substring and word order is load-bearing — a fact the
10
+ * message hid, leaving "it does not exist" as the only available reading.
11
+ *
12
+ * So the message reports what was actually run and hands back the two moves
13
+ * that recover: shorten to one distinctive word, or list everything. It says
14
+ * "matched" (what this query did) rather than "matching" (what exists).
15
+ *
16
+ * Pure and exported so the wording is testable without an oclif runtime — the
17
+ * convention `describeResourceNotFound` follows in `base-command`.
18
+ */
19
+ export declare function noSpacesMatchedMessage(q: string, bin?: string): string;
2
20
  export default class SpacesList extends BaseCommand {
3
21
  static description: string;
4
22
  static examples: string[];
@@ -1,10 +1,34 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noSpacesMatchedMessage = noSpacesMatchedMessage;
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 web_url_1 = require("../../lib/web-url");
7
8
  const format_1 = require("../../lib/format");
9
+ /**
10
+ * What an empty `--q` result is allowed to claim.
11
+ *
12
+ * `No spaces matching "space scratch".` is a confident assertion that nothing
13
+ * matched, and for the sense a person means it is FALSE: both words are in the
14
+ * title of a space they own and created a minute earlier. The server ORs one
15
+ * escaped regex built from the whole query across title/description/identifier,
16
+ * so the query is a substring and word order is load-bearing — a fact the
17
+ * message hid, leaving "it does not exist" as the only available reading.
18
+ *
19
+ * So the message reports what was actually run and hands back the two moves
20
+ * that recover: shorten to one distinctive word, or list everything. It says
21
+ * "matched" (what this query did) rather than "matching" (what exists).
22
+ *
23
+ * Pure and exported so the wording is testable without an oclif runtime — the
24
+ * convention `describeResourceNotFound` follows in `base-command`.
25
+ */
26
+ function noSpacesMatchedMessage(q, bin = 'skrr') {
27
+ return (`No spaces matched ${JSON.stringify(q)}.\n` +
28
+ ' → `--q` is a case-insensitive SUBSTRING match over title, description and identifier, ' +
29
+ 'not a tokenized search, so word order matters.\n' +
30
+ ` → retry with one distinctive word, or list them all with \`${bin} spaces list\`.`);
31
+ }
8
32
  class SpacesList extends base_command_1.BaseCommand {
9
33
  static description = 'List spaces';
10
34
  static examples = [
@@ -17,14 +41,32 @@ class SpacesList extends base_command_1.BaseCommand {
17
41
  ];
18
42
  static flags = {
19
43
  json: core_1.Flags.boolean({ description: 'Output as JSON' }),
20
- status: core_1.Flags.string({ description: 'Filter by space status' }),
44
+ // Enumerated so a typo is refused by the flag parser naming the choices.
45
+ // Undeclared, `--status bogus_status` exited 0 with zero results — which is
46
+ // exactly what a real status with no spaces looks like, so a typo and an
47
+ // empty set were indistinguishable and nothing named a valid value.
48
+ status: core_1.Flags.string({
49
+ description: 'Filter by space status',
50
+ options: [...data_provider_1.SPACE_STATUSES],
51
+ }),
21
52
  label: core_1.Flags.string({
22
53
  description: 'Filter by label ID (repeatable). Multiple labels AND together — a space must carry all of them.',
23
54
  multiple: true,
24
55
  }),
25
56
  'include-archived': core_1.Flags.boolean({ description: 'Include archived spaces' }),
26
57
  q: core_1.Flags.string({
27
- description: 'Free-text search over title, description, or space ref (case-insensitive)',
58
+ // NOT "free-text search". The server ORs ONE escaped regex built from the
59
+ // whole string across title/description/identifier
60
+ // (`api/models/Space.js` → `listSpaces`), so `--q` is a `LIKE %q%`.
61
+ // "Free-text search" made a promise the floor does not keep: it reads as
62
+ // "type the words you remember, in any order", and `--q "space scratch"`
63
+ // then answers "no spaces" for a space titled "… scratch space".
64
+ //
65
+ // A Meilisearch-backed deployment narrows through the index instead and
66
+ // IS tokenized and typo-tolerant — but the CLI cannot tell which path ran,
67
+ // so the help describes the guaranteed floor rather than the better case.
68
+ description: 'Match title, description, or identifier (case-insensitive). Substring, not tokenized — ' +
69
+ 'word order matters, so prefer one distinctive word.',
28
70
  }),
29
71
  limit: core_1.Flags.integer({ description: 'Max results', default: 50 }),
30
72
  cursor: core_1.Flags.string({ description: 'Pagination cursor from a previous page' }),
@@ -53,12 +95,12 @@ class SpacesList extends base_command_1.BaseCommand {
53
95
  const r = response;
54
96
  const items = r?.data ?? [];
55
97
  if (items.length === 0) {
56
- this.log(flags.q ? `No spaces matching ${JSON.stringify(flags.q)}.` : 'No spaces.');
98
+ this.log(flags.q ? noSpacesMatchedMessage(flags.q, this.config.bin) : 'No spaces.');
57
99
  return;
58
100
  }
59
101
  const rows = items.map((s) => ({
60
102
  id: String(s.id ?? s._id ?? '-'),
61
- ref: String(s.identifier ?? '-'),
103
+ identifier: String(s.identifier ?? '-'),
62
104
  title: String(s.title ?? '-'),
63
105
  status: String(s.status ?? '-'),
64
106
  goals: String(Array.isArray(s.goalIds) ? s.goalIds.length : 0),
@@ -66,9 +108,17 @@ class SpacesList extends base_command_1.BaseCommand {
66
108
  lead: String(s.leadAgentId ?? '-'),
67
109
  }));
68
110
  (0, format_1.renderTable)(rows, [
69
- // REF leads for the same reason it does on tasks: it is the short
70
- // thing a person types back into another command.
71
- { key: 'ref', header: 'REF' },
111
+ // Leads for the same reason it does on tasks: it is the short thing a
112
+ // person types back into another command.
113
+ //
114
+ // Headed `IDENTIFIER`, not `REF`, because `identifier` is the ONLY name
115
+ // this value has anywhere else — it is the wire field, the storage
116
+ // field, and what `@skrr-ai/data-provider` calls it. The column used to
117
+ // say `REF` and the detail view `Ref:`, so all three human surfaces
118
+ // named a key that does not exist in `--json`; a script author read
119
+ // `ref` off every row, got nothing, and concluded `--json` was dropping
120
+ // a field the table plainly had. (OSK S4 dogfood finding)
121
+ { key: 'identifier', header: 'IDENTIFIER' },
72
122
  { key: 'title', header: 'TITLE', maxWidth: 50 },
73
123
  { key: 'status', header: 'STATUS' },
74
124
  { key: 'goals', header: 'GOALS' },
@@ -43,7 +43,10 @@ class SpacesShow extends base_command_1.BaseCommand {
43
43
  }
44
44
  const ancestors = Array.isArray(s.ancestorSpaceIds) ? s.ancestorSpaceIds : [];
45
45
  this.log(`ID: ${s.id ?? s._id ?? '-'}`);
46
- this.log(`Ref: ${s.identifier ?? '-'}`);
46
+ // `Identifier`, not `Ref` the label has to be the key a reader can then
47
+ // find in `--json`, where this value is `identifier` and no `ref` exists.
48
+ // Matches the `IDENTIFIER` column in `spaces list`.
49
+ this.log(`Identifier: ${s.identifier ?? '-'}`);
47
50
  this.log(`Title: ${s.title ?? '-'}`);
48
51
  if (url)
49
52
  this.log(`URL: ${url}`);
@@ -1,4 +1,8 @@
1
1
  import { BaseCommand } from '../../base-command';
2
+ /** Exported for `summary.spec.ts`. `0` is a fact worth printing here — "blocked 0" is the answer, not a blank. */
3
+ export declare function num(value: unknown): string;
4
+ /** `label n · label n`, dropping pairs the payload did not carry at all. */
5
+ export declare function pairs(source: Record<string, unknown> | undefined, keys: [string, string][]): string;
2
6
  export default class SpacesSummary extends BaseCommand {
3
7
  static description: string;
4
8
  static args: {
@@ -1,9 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.num = num;
4
+ exports.pairs = pairs;
3
5
  const core_1 = require("@oclif/core");
4
6
  const data_provider_1 = require("@skrr-ai/data-provider");
5
7
  const base_command_1 = require("../../base-command");
6
8
  const space_output_1 = require("../../lib/space-output");
9
+ /** Exported for `summary.spec.ts`. `0` is a fact worth printing here — "blocked 0" is the answer, not a blank. */
10
+ function num(value) {
11
+ return typeof value === 'number' ? String(value) : '-';
12
+ }
13
+ /** `label n · label n`, dropping pairs the payload did not carry at all. */
14
+ function pairs(source, keys) {
15
+ return keys
16
+ .filter(([key]) => source?.[key] !== undefined)
17
+ .map(([key, label]) => `${label} ${num(source?.[key])}`)
18
+ .join(' · ');
19
+ }
7
20
  class SpacesSummary extends base_command_1.BaseCommand {
8
21
  static description = 'Show the aggregated summary for a space';
9
22
  static args = {
@@ -23,7 +36,70 @@ class SpacesSummary extends base_command_1.BaseCommand {
23
36
  const output = result && typeof result === 'object'
24
37
  ? (0, space_output_1.withSpacePerspectiveUrl)(this.cliConfig.baseURL, args.id, 'dashboard', result)
25
38
  : result;
26
- this.log(JSON.stringify(output, null, 2));
39
+ // This printed the raw payload regardless of `--json`, which differed only
40
+ // by its own `generatedAt` timestamp — so the flag advertised a mode the
41
+ // command did not have, and the FIRST perspective `skrr spaces` points a
42
+ // human at (the dashboard row) was a wall of JSON.
43
+ if (flags.json) {
44
+ this.log(JSON.stringify(output, null, 2));
45
+ return;
46
+ }
47
+ const o = (output ?? {});
48
+ const tasks = o.tasks;
49
+ const byPriority = tasks?.openByPriority;
50
+ this.log(`Space: ${o.spaceId ?? args.id}`);
51
+ if (o.url)
52
+ this.log(`Web: ${o.url}`);
53
+ this.log('');
54
+ if (tasks) {
55
+ const pct = typeof tasks.percentage === 'number' ? ` (${tasks.percentage}%)` : '';
56
+ this.log(`Tasks ${num(tasks.total)} total · ${num(tasks.done)} done${pct} · ${num(tasks.open)} open`);
57
+ const flow = pairs(tasks, [
58
+ ['inProgress', 'in progress'],
59
+ ['running', 'running'],
60
+ ['blocked', 'blocked'],
61
+ ['failed', 'failed'],
62
+ ]);
63
+ if (flow)
64
+ this.log(` ${flow}`);
65
+ const timing = pairs(tasks, [
66
+ ['dueToday', 'due today'],
67
+ ['dueSoon', 'due soon'],
68
+ ['stale', 'stale'],
69
+ ['unassigned', 'unassigned'],
70
+ ]);
71
+ if (timing)
72
+ this.log(` ${timing}`);
73
+ const priority = pairs(byPriority, [
74
+ ['critical', 'critical'],
75
+ ['high', 'high'],
76
+ ['medium', 'medium'],
77
+ ['low', 'low'],
78
+ ]);
79
+ if (priority)
80
+ this.log(` open by priority: ${priority}`);
81
+ }
82
+ for (const [key, label] of [
83
+ ['goals', 'Goals '],
84
+ ['conversations', 'Chats '],
85
+ ]) {
86
+ const section = o[key];
87
+ if (section?.total !== undefined)
88
+ this.log(`${label} ${num(section.total)}`);
89
+ }
90
+ // Agents carries `activeIds`, not a total — counting the array is the
91
+ // answer to "how many agents are working here", and an empty one is a real
92
+ // answer rather than a missing section.
93
+ const agents = o.agents;
94
+ if (Array.isArray(agents?.activeIds))
95
+ this.log(`Agents ${agents.activeIds.length} active`);
96
+ // The per-status breakdown is in the payload and deliberately not a
97
+ // section: it restates `byStatus` counts the lines above already summarise,
98
+ // and nine near-zero rows would bury the four numbers that matter.
99
+ this.log('');
100
+ this.log('Full breakdown (byStatus, per-section detail): --json');
101
+ if (o.next_command)
102
+ this.log(`Next: ${o.next_command}`);
27
103
  }
28
104
  }
29
105
  exports.default = SpacesSummary;
@@ -16,6 +16,10 @@ export default class SpacesUpdate extends BaseCommand {
16
16
  'target-date': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
17
17
  goal: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
18
18
  'inherit-access': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
19
+ 'estimate-scale': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
20
+ 'estimate-migration': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
21
+ 'default-estimate': import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
22
+ 'use-default-estimate': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
19
23
  'worktree-isolation': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
20
24
  };
21
25
  run(): Promise<void>;
@@ -13,6 +13,7 @@ class SpacesUpdate extends base_command_1.BaseCommand {
13
13
  '<%= config.bin %> spaces update <space-id> --lead-agent <agent-id>',
14
14
  '<%= config.bin %> spaces update <space-id> --no-inherit-access',
15
15
  '<%= config.bin %> spaces update <space-id> --from-json patch.json',
16
+ '<%= config.bin %> spaces update <space-id> --estimate-scale fibonacci --estimate-migration migrate',
16
17
  ];
17
18
  static args = {
18
19
  id: core_1.Args.string({ description: 'Space ID', required: true, ignoreStdin: true }),
@@ -24,7 +25,7 @@ class SpacesUpdate extends base_command_1.BaseCommand {
24
25
  }),
25
26
  title: core_1.Flags.string({ description: 'New title' }),
26
27
  description: core_1.Flags.string({ description: 'New description' }),
27
- status: core_1.Flags.string({ description: 'New status' }),
28
+ status: core_1.Flags.string({ description: 'New status', options: [...data_provider_1.SPACE_STATUSES] }),
28
29
  color: core_1.Flags.string({ description: 'New color (hex)' }),
29
30
  'lead-agent': core_1.Flags.string({ description: 'New Space Leader agent ID' }),
30
31
  'target-date': core_1.Flags.string({ description: 'New target date (ISO string)' }),
@@ -37,6 +38,38 @@ class SpacesUpdate extends base_command_1.BaseCommand {
37
38
  allowNo: true,
38
39
  description: 'Whether this subspace inherits members/permissions from its parent chain. Use --no-inherit-access to make it private. (To change the parent itself, use `spaces move`.)',
39
40
  }),
41
+ // `tasks create --estimate` resolves its scale up the ancestor chain, and
42
+ // with nothing set it resolves to `none`, which admits no value — so the
43
+ // estimate is dropped. That was reachable only from the web UI, which made
44
+ // the CLI's own estimate flag conditionally inert with no way to satisfy
45
+ // its prerequisite. `PATCH /api/spaces/:id` has accepted these three fields
46
+ // all along.
47
+ 'estimate-scale': core_1.Flags.string({
48
+ description: 'Estimate scale for this space and its descendants. Allowed values per scale: ' +
49
+ Object.entries(data_provider_1.ESTIMATE_SCALE_VALUES)
50
+ .map(([scale, values]) => `${scale} ${values.length ? values.join('/') : '(none)'}`)
51
+ .join(', '),
52
+ options: [...data_provider_1.ESTIMATE_SCALES],
53
+ }),
54
+ // The server makes this MANDATORY on a scale change (deliberately — so no
55
+ // destructive interpretation is inferred) and answers a missing one with
56
+ // ESTIMATE_SCALE_MIGRATION_REQUIRED. Surfacing it as its own flag is the
57
+ // difference between a documented choice and a 400 the caller has to
58
+ // reverse-engineer.
59
+ 'estimate-migration': core_1.Flags.string({
60
+ description: 'What happens to estimates that already exist when the scale changes. Required whenever ' +
61
+ '--estimate-scale changes the EFFECTIVE scale. `migrate` maps each estimate onto the ' +
62
+ 'nearest value in the new scale; `clear` DELETES every estimate in this space and its ' +
63
+ 'descendants. Changing to `none` accepts only `clear`, since there is nothing to map into.',
64
+ options: [...data_provider_1.ESTIMATE_SCALE_MIGRATION_MODES],
65
+ }),
66
+ 'default-estimate': core_1.Flags.integer({
67
+ description: 'Estimate applied to unestimated tasks when --use-default-estimate is on. Must be a value the scale allows',
68
+ }),
69
+ 'use-default-estimate': core_1.Flags.boolean({
70
+ allowNo: true,
71
+ description: 'Whether rollups treat an unestimated task as --default-estimate',
72
+ }),
40
73
  'worktree-isolation': core_1.Flags.string({
41
74
  options: ['shared', 'ephemeral'],
42
75
  description: "Space default for how this space's autonomous task runs use git worktrees. " +
@@ -63,6 +96,10 @@ class SpacesUpdate extends base_command_1.BaseCommand {
63
96
  leadAgentId: flags['lead-agent'],
64
97
  targetDate: flags['target-date'],
65
98
  inheritAccess: flags['inherit-access'],
99
+ estimateScale: flags['estimate-scale'],
100
+ estimateScaleMigration: flags['estimate-migration'],
101
+ defaultEstimate: flags['default-estimate'],
102
+ useDefaultEstimateForUnestimated: flags['use-default-estimate'],
66
103
  });
67
104
  // Worktree-isolation default lives on the space's deliveryPolicy subdoc,
68
105
  // which the API replaces wholesale on update. Merge into the policy the user