@lumoai/cli 1.55.0 → 1.56.0

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.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.assertSameTeam = assertSameTeam;
4
4
  exports.normalizePriority = normalizePriority;
5
+ exports.formatCreateOutput = formatCreateOutput;
5
6
  exports.formatCreatedTaskLine = formatCreatedTaskLine;
6
7
  exports.taskCreate = taskCreate;
7
8
  const config_1 = require("../lib/config");
@@ -9,6 +10,7 @@ const api_1 = require("../lib/api");
9
10
  const tag_resolver_1 = require("../lib/tag-resolver");
10
11
  const resolve_1 = require("../lib/resolve");
11
12
  const sanitize_1 = require("../lib/sanitize");
13
+ const next_steps_1 = require("../lib/next-steps");
12
14
  const ALLOWED_PRIORITIES = ['LOW', 'MEDIUM', 'HIGH', 'URGENT'];
13
15
  /**
14
16
  * Assert that the sprint belongs to the same team as the newly created task.
@@ -80,6 +82,21 @@ function normalizePriority(value) {
80
82
  ? upper
81
83
  : null;
82
84
  }
85
+ /**
86
+ * Compose the full `task create` output: the result line first, then the
87
+ * server-computed next-step block (LUM-686). Exported so the render test
88
+ * exercises the real composition rather than re-assembling the parts itself.
89
+ *
90
+ * The result line stays byte-identical and first — a reader taking line 1 is
91
+ * unaffected by the block below it.
92
+ */
93
+ function formatCreateOutput(task, steps = [], opts = {}, env = process.env) {
94
+ const head = formatCreatedTaskLine(task);
95
+ if ((0, next_steps_1.nextStepsSilenced)(env, opts))
96
+ return head;
97
+ const block = (0, next_steps_1.formatNextSteps)(steps);
98
+ return block === '' ? head : `${head}\n\n${block}`;
99
+ }
83
100
  /**
84
101
  * Format the single-line success output. Title is double-quoted; embedded
85
102
  * double-quotes are backslash-escaped so the output stays parseable as one
@@ -173,7 +190,7 @@ async function taskCreate(title, opts) {
173
190
  if (res.status === 201) {
174
191
  const data = (await res.json());
175
192
  const responseTagNames = (data.task.taskTags ?? []).map(t => t.name);
176
- process.stdout.write(formatCreatedTaskLine({ ...data.task, tags: responseTagNames }) + '\n');
193
+ process.stdout.write(formatCreateOutput({ ...data.task, tags: responseTagNames }, data.nextSteps ?? [], opts) + '\n');
177
194
  // Sprint binding (optional): if --sprint was passed, bind the new task.
178
195
  if (opts.sprint) {
179
196
  const workspaceSlug = creds.workspaceSlug ?? '';
@@ -11,6 +11,7 @@ const task_create_1 = require("./task-create");
11
11
  const tag_resolver_1 = require("../lib/tag-resolver");
12
12
  const resolve_1 = require("../lib/resolve");
13
13
  const sanitize_1 = require("../lib/sanitize");
14
+ const next_steps_1 = require("../lib/next-steps");
14
15
  const ALLOWED_STATUSES = ['TODO', 'IN_PROGRESS', 'IN_REVIEW', 'DONE'];
15
16
  /**
16
17
  * Pure function: given the task's current sprint binding and the resolved
@@ -101,7 +102,21 @@ function formatUpdatedTaskLine(task) {
101
102
  }
102
103
  return head;
103
104
  }
105
+ /**
106
+ * Thin wrapper so the next-step block always lands last (LUM-686). The update
107
+ * flow has several success exits — a plain PATCH, plus the sprint bind/unbind
108
+ * branches — and hints belong after whatever the command printed, not wedged
109
+ * before the Sprint line. Only the wrapper emits; the inner run just collects.
110
+ */
104
111
  async function taskUpdate(identifier, opts) {
112
+ const collected = { steps: [] };
113
+ const code = await runTaskUpdate(identifier, opts, collected);
114
+ // Success only — a failed update's advice would be noise on top of an error.
115
+ if (code === undefined)
116
+ (0, next_steps_1.emitNextSteps)(collected.steps, opts);
117
+ return code;
118
+ }
119
+ async function runTaskUpdate(identifier, opts, collected) {
105
120
  if (!identifier || identifier.trim().length === 0) {
106
121
  console.error('Error: missing <identifier>. Usage: lumo task update <LUM-42> [options]');
107
122
  return 1;
@@ -249,6 +264,7 @@ async function taskUpdate(identifier, opts) {
249
264
  if (responseTagNames !== undefined)
250
265
  taskLine.tags = responseTagNames;
251
266
  process.stdout.write(formatUpdatedTaskLine(taskLine) + '\n');
267
+ collected.steps = data.nextSteps ?? [];
252
268
  }
253
269
  else {
254
270
  let serverMsg = null;
@@ -37,6 +37,7 @@ exports.worktreeAdd = worktreeAdd;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const child_process_1 = require("child_process");
40
+ const next_steps_1 = require("../lib/next-steps");
40
41
  const worktree_1 = require("../lib/worktree");
41
42
  function printGuidance(dir, branch) {
42
43
  console.log('');
@@ -44,8 +45,11 @@ function printGuidance(dir, branch) {
44
45
  console.log(` branch: ${branch}`);
45
46
  console.log(` node_modules: symlinked to main checkout`);
46
47
  console.log('');
47
- console.log('Next:');
48
- console.log(` cd ${dir}`);
48
+ // Locally-static source: no server round-trip to carry steps, but the same
49
+ // renderer so the block looks identical everywhere (LUM-686).
50
+ console.log((0, next_steps_1.formatNextSteps)([
51
+ { command: `cd ${dir}`, why: 'work from the isolated checkout' },
52
+ ]));
49
53
  console.log('');
50
54
  console.log('Gotchas baked into this worktree:');
51
55
  console.log(' • prisma: the generated client is SHARED via the node_modules symlink.');
@@ -47,6 +47,19 @@ const session_attach_1 = require("./commands/session-attach");
47
47
  const session_status_1 = require("./commands/session-status");
48
48
  const next_1 = require("./commands/next");
49
49
  const idea_1 = require("./commands/idea");
50
+ const idea_slack_add_1 = require("./commands/idea-slack-add");
51
+ const idea_slack_show_1 = require("./commands/idea-slack-show");
52
+ const idea_slack_rm_1 = require("./commands/idea-slack-rm");
53
+ const idea_web_add_1 = require("./commands/idea-web-add");
54
+ const idea_web_show_1 = require("./commands/idea-web-show");
55
+ const idea_web_rm_1 = require("./commands/idea-web-rm");
56
+ const idea_figma_add_1 = require("./commands/idea-figma-add");
57
+ const idea_figma_list_1 = require("./commands/idea-figma-list");
58
+ const idea_figma_rm_1 = require("./commands/idea-figma-rm");
59
+ const idea_figma_refresh_1 = require("./commands/idea-figma-refresh");
60
+ const idea_figma_context_1 = require("./commands/idea-figma-context");
61
+ const idea_comment_1 = require("./commands/idea-comment");
62
+ const initiative_1 = require("./commands/initiative");
50
63
  const plan_1 = require("./commands/plan");
51
64
  const cost_1 = require("./commands/cost");
52
65
  const priority_1 = require("./commands/priority");
@@ -203,8 +216,18 @@ const program = new commander_1.Command()
203
216
  // point at --help instead of dead-ending on "unknown option". Subcommands
204
217
  // created via .command() inherit these settings.
205
218
  .showSuggestionAfterError(true)
206
- .showHelpAfterError('(run the command with --help to list its valid flags and arguments)');
219
+ .showHelpAfterError('(run the command with --help to list its valid flags and arguments)')
220
+ // LUM-686: global mute for the trailing next-step block. `LUMO_NO_HINTS=1`
221
+ // does the same for a whole shell.
222
+ .option('--no-hints', 'suppress the trailing next-step suggestions');
207
223
  exports.program = program;
224
+ // Commander does not merge program-level options into a subcommand's own
225
+ // opts(), so the global `--no-hints` is folded into the env channel before any
226
+ // action runs. One silencing path (`nextStepsSilenced`) instead of two.
227
+ program.hook('preAction', thisCommand => {
228
+ if (thisCommand.opts().hints === false)
229
+ process.env.LUMO_NO_HINTS = '1';
230
+ });
208
231
  const auth = program.command('auth').description('Manage Lumo authentication');
209
232
  auth
210
233
  .command('login')
@@ -279,11 +302,103 @@ program
279
302
  .option('-n, --count <N>', 'Number of tasks to recommend (default 3)')
280
303
  .option('--claimable', 'Only recommend agent-claimable tasks that are unblocked (F3) and within milestone budget (F2)')
281
304
  .action(wrap(options => (0, next_1.nextCommand)(options)));
282
- program
283
- .command('idea <statement>')
305
+ const ideaCmd = program
306
+ .command('idea [statement]')
284
307
  .description('Capture a team-level idea into the pool (<10s, no friction). Grabs the current Claude Code session id and its bound task as provenance; prints an I-prefixed id (e.g. LUM-I42). The transformer (Spec 2) consumes the pool.')
285
308
  .option('-c, --context <text>', 'Free-text origin context for the idea')
286
309
  .action(wrap((statement, options) => (0, idea_1.ideaCapture)(statement, options)));
310
+ ideaCmd
311
+ .command('list')
312
+ .description('List the team idea pool newest-first — each line shows the LUM-I<n> id, status (CAPTURED|DEVELOPING|PLANNED|DROPPED) and statement.')
313
+ .action(wrap(() => (0, idea_1.ideaList)()));
314
+ ideaCmd
315
+ .command('update <id>')
316
+ .description('Move an idea through its lifecycle (LUM-679). Allowed: CAPTURED↔DEVELOPING→PLANNED and any state→DROPPED (DROPPED is terminal). <id> is the LUM-I<n> id (a bare number also resolves).')
317
+ .option('-s, --status <value>', 'New status: captured | developing | planned | dropped (case-insensitive)')
318
+ .action(wrap((id, options) => (0, idea_1.ideaUpdate)(id, options)));
319
+ // Idea evidence commands (LUM-681): mirrors `task slack/web/figma`, hitting
320
+ // `/api/ideas/:id/...` instead of `/api/tasks/:id/...`. Lets an agent attach
321
+ // Slack threads, web links, and Figma design links to an idea before it goes
322
+ // through the plan-run converter.
323
+ const ideaSlack = ideaCmd
324
+ .command('slack')
325
+ .description('Attach & inspect Slack context on an idea');
326
+ ideaSlack
327
+ .command('add <idea> <permalink>')
328
+ .description('Attach a Slack thread to an idea')
329
+ .action(wrap((id, p) => (0, idea_slack_add_1.ideaSlackAdd)(id, p)));
330
+ ideaSlack
331
+ .command('show <idea> <contextId>')
332
+ .description('Show the full stored Slack thread snapshot')
333
+ .action(wrap((id, ctx) => (0, idea_slack_show_1.ideaSlackShow)(id, ctx)));
334
+ ideaSlack
335
+ .command('rm <idea> <contextId>')
336
+ .description('Remove a Slack context from an idea')
337
+ .action(wrap((id, ctx) => (0, idea_slack_rm_1.ideaSlackRm)(id, ctx)));
338
+ const ideaWeb = ideaCmd
339
+ .command('web')
340
+ .description('Attach & inspect web links on an idea');
341
+ ideaWeb
342
+ .command('add <idea> <url>')
343
+ .description('Attach a web link to an idea')
344
+ .action(wrap((id, u) => (0, idea_web_add_1.ideaWebAdd)(id, u)));
345
+ ideaWeb
346
+ .command('show <idea> <linkId>')
347
+ .description('Show the fetched web link body as plain text')
348
+ .action(wrap((id, l) => (0, idea_web_show_1.ideaWebShow)(id, l)));
349
+ ideaWeb
350
+ .command('rm <idea> <linkId>')
351
+ .description('Remove a web link from an idea')
352
+ .action(wrap((id, l) => (0, idea_web_rm_1.ideaWebRm)(id, l)));
353
+ const ideaFigma = ideaCmd
354
+ .command('figma')
355
+ .description('Attach Figma links to an idea');
356
+ ideaFigma
357
+ .command('add <idea> <url>')
358
+ .description('Attach a Figma file/frame URL to an idea')
359
+ .action(wrap((id, u) => (0, idea_figma_add_1.ideaFigmaAdd)({ identifier: id, url: u })));
360
+ ideaFigma
361
+ .command('list <idea>')
362
+ .description('List Figma links on an idea')
363
+ .action(wrap(id => (0, idea_figma_list_1.ideaFigmaList)({ identifier: id })));
364
+ ideaFigma
365
+ .command('rm <idea> <link-id-or-url>')
366
+ .description('Remove a Figma link from an idea (idempotent)')
367
+ .action(wrap((id, l) => (0, idea_figma_rm_1.ideaFigmaRm)({ identifier: id, linkIdOrUrl: l })));
368
+ ideaFigma
369
+ .command('refresh <idea>')
370
+ .description('Re-fetch Figma metadata for every link on an idea')
371
+ .action(wrap(id => (0, idea_figma_refresh_1.ideaFigmaRefresh)({ identifier: id })));
372
+ ideaFigma
373
+ .command('context <idea> <linkId>')
374
+ .description('Show cached Figma design context')
375
+ .action(wrap((id, l) => (0, idea_figma_context_1.ideaFigmaContext)(id, l)));
376
+ ideaCmd
377
+ .command('comment <id> <body>')
378
+ .description('Comment on an idea (LUM-680). <id> is the LUM-I<n> id (a bare number also resolves). Body is plain text — quote it to pass spaces or newlines.')
379
+ .action(wrap((id, body) => (0, idea_comment_1.ideaComment)(id, body)));
380
+ // Plural `comments` parent (mirrors `task comments`): `idea comment <id> <body>`
381
+ // already exists for posting, and commander disallows a duplicate name.
382
+ const ideaComments = ideaCmd
383
+ .command('comments')
384
+ .description('Inspect an idea comment thread');
385
+ ideaComments
386
+ .command('list <id>')
387
+ .description('List an idea comment thread (capped to the output budget; --full prints every comment).')
388
+ .option('--full', 'Print every comment, bypassing the output-token cap')
389
+ .action(wrap((id, opts) => (0, idea_comment_1.ideaCommentList)(id, opts)));
390
+ const initiativeCmd = program
391
+ .command('initiative')
392
+ .description('Team-level Initiatives (LUM-INIT-<n>): create one directly (no plan run required) or list them. Converter-materialised initiatives show here too.');
393
+ initiativeCmd
394
+ .command('create <goal>')
395
+ .description('Create a team-level initiative directly from a goal statement — no plan run required. Prints the LUM-INIT-<n> id; --assumption records the bet behind it.')
396
+ .option('--assumption <text>', 'The bet/assumption behind the initiative (赌注旁注)')
397
+ .action(wrap((goal, options) => (0, initiative_1.initiativeCreate)(goal, options)));
398
+ initiativeCmd
399
+ .command('list')
400
+ .description('List the team initiatives newest-first — each line shows the LUM-INIT-<n> id, status (ACTIVE|DONE|DROPPED) and goal.')
401
+ .action(wrap(() => (0, initiative_1.initiativeList)()));
287
402
  const planCmd = program
288
403
  .command('plan')
289
404
  .description('Start a design-thinking 转换器 run (聚类→对齐→成计划) and print the gate-A web deep-link. Refuses when a run is already active (see `lumo plan status`); --abandon-active abandons the prior run first. Editing/confirmation happen in web.')
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.addIdeaFigmaLink = addIdeaFigmaLink;
4
+ exports.listIdeaFigmaLinks = listIdeaFigmaLinks;
5
+ exports.removeIdeaFigmaLink = removeIdeaFigmaLink;
6
+ exports.refreshIdeaFigmaLinks = refreshIdeaFigmaLinks;
7
+ const api_1 = require("./api");
8
+ const config_1 = require("./config");
9
+ function buildErr(status, body) {
10
+ const err = Object.assign(new Error(body.error ?? `HTTP ${status}`), {
11
+ status,
12
+ });
13
+ if (body.code !== undefined)
14
+ err.code = body.code;
15
+ return err;
16
+ }
17
+ async function call(path, init) {
18
+ const creds = (0, config_1.readCredentials)();
19
+ if (!creds)
20
+ throw new Error('Not logged in. Run: lumo auth login');
21
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
22
+ const res = await fetch(`${(0, api_1.trimTrailingSlash)(apiUrl)}${path}`, {
23
+ ...init,
24
+ headers: {
25
+ Authorization: `Bearer ${creds.token}`,
26
+ 'Content-Type': 'application/json',
27
+ ...(init.headers ?? {}),
28
+ },
29
+ });
30
+ if (!res.ok) {
31
+ let parsed = {};
32
+ try {
33
+ parsed = (await res.json());
34
+ }
35
+ catch {
36
+ /* non-JSON body */
37
+ }
38
+ throw buildErr(res.status, parsed);
39
+ }
40
+ return (await res.json());
41
+ }
42
+ /**
43
+ * Idea-side counterpart to `cli/src/lib/figma-api.ts` (LUM-681). Same shape,
44
+ * same error codes (`figma_not_connected` / `figma_needs_reauth`), only the
45
+ * URL prefix differs: `/api/ideas/:id/...` instead of `/api/tasks/:id/...`.
46
+ */
47
+ async function addIdeaFigmaLink(identifier, url) {
48
+ return call(`/api/ideas/${encodeURIComponent(identifier)}/figma`, {
49
+ method: 'POST',
50
+ body: JSON.stringify({ url }),
51
+ });
52
+ }
53
+ async function listIdeaFigmaLinks(identifier) {
54
+ return call(`/api/ideas/${encodeURIComponent(identifier)}/figma`, {
55
+ method: 'GET',
56
+ });
57
+ }
58
+ async function removeIdeaFigmaLink(identifier, linkId) {
59
+ return call(`/api/ideas/${encodeURIComponent(identifier)}/figma/${encodeURIComponent(linkId)}`, { method: 'DELETE' });
60
+ }
61
+ async function refreshIdeaFigmaLinks(identifier) {
62
+ return call(`/api/ideas/${encodeURIComponent(identifier)}/figma/refresh`, {
63
+ method: 'POST',
64
+ });
65
+ }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NEXT_STEPS_MAX = void 0;
4
+ exports.nextStepsSilenced = nextStepsSilenced;
5
+ exports.formatNextSteps = formatNextSteps;
6
+ exports.emitNextSteps = emitNextSteps;
7
+ const sanitize_1 = require("./sanitize");
8
+ /** Mirrors NEXT_STEPS_MAX server-side; a defence in depth, not the only cap. */
9
+ exports.NEXT_STEPS_MAX = 3;
10
+ const HEADING = 'Next:';
11
+ /**
12
+ * True when the user has muted suggestions. Hints cost tokens on every command;
13
+ * anyone who finds them noise must be able to turn them off for good.
14
+ */
15
+ function nextStepsSilenced(env = process.env, opts = {}) {
16
+ // Commander maps `--no-hints` to `opts.hints === false`.
17
+ if (opts.hints === false)
18
+ return true;
19
+ const flag = env.LUMO_NO_HINTS;
20
+ return flag !== undefined && flag !== '' && flag !== '0';
21
+ }
22
+ /**
23
+ * Render the trailing suggestion block. Returns "" when there is nothing to
24
+ * say, so callers can append unconditionally without emitting a bare heading.
25
+ */
26
+ function formatNextSteps(steps) {
27
+ const capped = steps.slice(0, exports.NEXT_STEPS_MAX);
28
+ if (capped.length === 0)
29
+ return '';
30
+ const lines = [HEADING];
31
+ for (const step of capped) {
32
+ const why = (0, sanitize_1.sanitizeField)(step.why);
33
+ lines.push(step.command === null
34
+ ? ` ${why}`
35
+ : ` ${(0, sanitize_1.sanitizeField)(step.command)}\n ${why}`);
36
+ }
37
+ return lines.join('\n');
38
+ }
39
+ /**
40
+ * Write the block to **stdout**, after the command's own result line.
41
+ *
42
+ * Deliberately not stderr: suggestions are not errors, `2>/dev/null` would
43
+ * swallow them, some harnesses read any stderr output as failure, and with
44
+ * both streams redirected to one file the block can surface *above* the result
45
+ * it follows. Machine consumers are served by `--json` instead, where the
46
+ * steps ride as a field rather than as text.
47
+ */
48
+ function emitNextSteps(steps, opts = {}, out = process.stdout, env = process.env) {
49
+ if (nextStepsSilenced(env, opts))
50
+ return;
51
+ const block = formatNextSteps(steps);
52
+ if (block === '')
53
+ return;
54
+ out.write(`\n${block}\n`);
55
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumoai/cli",
3
- "version": "1.55.0",
3
+ "version": "1.56.0",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",