@lumoai/cli 1.54.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.
Files changed (30) hide show
  1. package/assets/skill/SKILL.md +78 -1
  2. package/assets/skill/references/priority.md +6 -5
  3. package/assets/skill/references/tasks.md +15 -3
  4. package/dist/cli/src/commands/idea-comment.js +127 -0
  5. package/dist/cli/src/commands/idea-figma-add.js +30 -0
  6. package/dist/cli/src/commands/idea-figma-context.js +65 -0
  7. package/dist/cli/src/commands/idea-figma-list.js +32 -0
  8. package/dist/cli/src/commands/idea-figma-refresh.js +40 -0
  9. package/dist/cli/src/commands/idea-figma-rm.js +19 -0
  10. package/dist/cli/src/commands/idea-slack-add.js +66 -0
  11. package/dist/cli/src/commands/idea-slack-rm.js +63 -0
  12. package/dist/cli/src/commands/idea-slack-show.js +65 -0
  13. package/dist/cli/src/commands/idea-web-add.js +66 -0
  14. package/dist/cli/src/commands/idea-web-rm.js +63 -0
  15. package/dist/cli/src/commands/idea-web-show.js +71 -0
  16. package/dist/cli/src/commands/idea.js +140 -0
  17. package/dist/cli/src/commands/initiative.js +132 -0
  18. package/dist/cli/src/commands/next.js +9 -1
  19. package/dist/cli/src/commands/plan.js +157 -0
  20. package/dist/cli/src/commands/session-attach.js +4 -0
  21. package/dist/cli/src/commands/setup.js +17 -8
  22. package/dist/cli/src/commands/task-comment-list.js +2 -1
  23. package/dist/cli/src/commands/task-context.js +5 -1
  24. package/dist/cli/src/commands/task-create.js +18 -1
  25. package/dist/cli/src/commands/task-update.js +16 -0
  26. package/dist/cli/src/commands/worktree-add.js +6 -2
  27. package/dist/cli/src/index.js +128 -3
  28. package/dist/cli/src/lib/idea-figma-api.js +65 -0
  29. package/dist/cli/src/lib/next-steps.js +55 -0
  30. package/package.json +1 -1
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatPlanDeepLink = formatPlanDeepLink;
4
+ exports.stageLabel = stageLabel;
5
+ exports.plan = plan;
6
+ exports.planStatus = planStatus;
7
+ const config_1 = require("../lib/config");
8
+ const api_1 = require("../lib/api");
9
+ const sanitize_1 = require("../lib/sanitize");
10
+ const browser_1 = require("../lib/browser");
11
+ /**
12
+ * Web deep-link to a run's converter page: `/workspace/<slug>/plan/<runId>`.
13
+ * The page routes to whichever gate is open on the run — so the same link is
14
+ * "gate A" at start (run in CLUSTERING) and "the next gate" mid-run. Editing
15
+ * and gate confirmation happen in the web UI; the CLI only carries the human
16
+ * there.
17
+ */
18
+ function formatPlanDeepLink(base, workspaceSlug, runId) {
19
+ return `${(0, api_1.trimTrailingSlash)(base)}/workspace/${workspaceSlug}/plan/${runId}`;
20
+ }
21
+ /**
22
+ * Human label for a run stage. The gates (`*_READY`) name the decision waiting
23
+ * on the human; the working stages name the LLM segment in flight. Unknown /
24
+ * future stages fall back to the raw enum so nothing is silently swallowed.
25
+ */
26
+ function stageLabel(stage) {
27
+ const LABELS = {
28
+ CLUSTERING: '聚类中 (clustering)',
29
+ CLUSTERS_READY: '闸门 A · 聚类待确认 (gate A — confirm clusters)',
30
+ ALIGNING: '对齐中 (aligning)',
31
+ ALIGNMENT_READY: '闸门 B · 对齐待确认 (gate B — confirm alignment)',
32
+ GENERATING: '成计划中 (generating)',
33
+ DRAFT_READY: '闸门 C · 草案待确认 (gate C — confirm draft)',
34
+ FAILED: '失败 · 可重试 (failed — retryable)',
35
+ };
36
+ return LABELS[stage] ?? stage;
37
+ }
38
+ /**
39
+ * `lumo plan [--abandon-active]` — start a design-thinking 转换器 run.
40
+ *
41
+ * Starts a run and prints (and opens) the gate-A web deep-link. When a run is
42
+ * already active the server refuses (409) — we point the user at
43
+ * `lumo plan status` or `--abandon-active`. `--abandon-active` first abandons
44
+ * the prior run, then starts a fresh one. All editing/confirmation is in web;
45
+ * the CLI only triggers and hands off the deep-link.
46
+ */
47
+ async function plan(opts = {}) {
48
+ const creds = (0, config_1.readCredentials)();
49
+ if (!creds) {
50
+ console.error('Error: not logged in. Run `lumo auth login` first.');
51
+ return 1;
52
+ }
53
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
54
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
55
+ const body = {};
56
+ if (opts.abandonActive)
57
+ body.abandonActive = true;
58
+ let res;
59
+ try {
60
+ res = await fetch(`${base}/api/plan-runs`, {
61
+ method: 'POST',
62
+ headers: {
63
+ Authorization: `Bearer ${creds.token}`,
64
+ 'Content-Type': 'application/json',
65
+ },
66
+ body: JSON.stringify(body),
67
+ });
68
+ }
69
+ catch (err) {
70
+ const msg = err instanceof Error ? err.message : String(err);
71
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
72
+ return 1;
73
+ }
74
+ if (res.status === 401) {
75
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
76
+ return 1;
77
+ }
78
+ if (res.status === 409) {
79
+ console.error('Error: a plan run is already active for your team.\n' +
80
+ ' Check it with `lumo plan status`, or start over with ' +
81
+ '`lumo plan --abandon-active`.');
82
+ return 1;
83
+ }
84
+ if (res.status === 201) {
85
+ const { run } = (await res.json());
86
+ const deepLink = formatPlanDeepLink(base, creds.workspaceSlug, run.id);
87
+ process.stdout.write(`✓ 计划转换 run 已启动 (${run.id}) — 聚类进行中。\n` +
88
+ '到闸门 A 在 web 端确认聚类结果:\n' +
89
+ ` ${(0, sanitize_1.sanitizeField)(deepLink)}\n`);
90
+ (0, browser_1.openBrowser)(deepLink);
91
+ return;
92
+ }
93
+ return reportServerError(res);
94
+ }
95
+ /**
96
+ * `lumo plan status` — where the active run stands.
97
+ *
98
+ * Prints the active run's current stage and the deep-link to act on its next
99
+ * gate. With no active run, prompts the user to run `lumo plan`.
100
+ */
101
+ async function planStatus() {
102
+ const creds = (0, config_1.readCredentials)();
103
+ if (!creds) {
104
+ console.error('Error: not logged in. Run `lumo auth login` first.');
105
+ return 1;
106
+ }
107
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
108
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
109
+ let res;
110
+ try {
111
+ res = await fetch(`${base}/api/plan-runs/active`, {
112
+ headers: { Authorization: `Bearer ${creds.token}` },
113
+ });
114
+ }
115
+ catch (err) {
116
+ const msg = err instanceof Error ? err.message : String(err);
117
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
118
+ return 1;
119
+ }
120
+ if (res.status === 401) {
121
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
122
+ return 1;
123
+ }
124
+ if (!res.ok) {
125
+ return reportServerError(res);
126
+ }
127
+ const { run } = (await res.json());
128
+ if (!run) {
129
+ process.stdout.write('没有进行中的计划转换 run。运行 `lumo plan` 启动一个。\n');
130
+ return;
131
+ }
132
+ const deepLink = formatPlanDeepLink(base, creds.workspaceSlug, run.id);
133
+ process.stdout.write(`计划转换 run ${run.id}\n` +
134
+ ` 当前 stage: ${stageLabel(run.stage)}\n` +
135
+ ' 下一个闸门在 web 端处理:\n' +
136
+ ` ${(0, sanitize_1.sanitizeField)(deepLink)}\n`);
137
+ return;
138
+ }
139
+ /** Print a server error body (or status-only fallback) to stderr; returns 1. */
140
+ async function reportServerError(res) {
141
+ let serverMsg = null;
142
+ try {
143
+ const errBody = (await res.json());
144
+ if (typeof errBody.error === 'string')
145
+ serverMsg = errBody.error;
146
+ }
147
+ catch {
148
+ // Body wasn't JSON; fall through to a status-only message.
149
+ }
150
+ if (serverMsg) {
151
+ console.error(`Error: ${(0, sanitize_1.sanitizeField)(serverMsg)}`);
152
+ }
153
+ else {
154
+ console.error(`Error: plan command failed (HTTP ${res.status})`);
155
+ }
156
+ return 1;
157
+ }
@@ -4,6 +4,7 @@ exports.sessionAttach = sessionAttach;
4
4
  const config_1 = require("../lib/config");
5
5
  const api_1 = require("../lib/api");
6
6
  const sanitize_1 = require("../lib/sanitize");
7
+ const next_steps_1 = require("../lib/next-steps");
7
8
  const resolve_project_1 = require("../lib/resolve-project");
8
9
  const memory_auto_1 = require("../lib/memory-auto");
9
10
  /**
@@ -166,6 +167,9 @@ async function sessionAttach(identifier, options) {
166
167
  catch {
167
168
  // best-effort — the bind already succeeded; never surface a sync error here
168
169
  }
170
+ // Last, after the contract/memory/sync sections — the block reads as the
171
+ // closing "so do this now" line rather than interrupting them (LUM-686).
172
+ (0, next_steps_1.emitNextSteps)(body.nextSteps ?? [], options ?? {});
169
173
  }
170
174
  /**
171
175
  * LUM-640: the `--steward` form — bind this session to a MILESTONE as a
@@ -41,6 +41,7 @@ const os = __importStar(require("os"));
41
41
  const path = __importStar(require("path"));
42
42
  const child_process_1 = require("child_process");
43
43
  const hooks_template_1 = require("../lib/hooks-template");
44
+ const next_steps_1 = require("../lib/next-steps");
44
45
  const git_hook_template_1 = require("../lib/git-hook-template");
45
46
  const line_prompt_1 = require("../lib/line-prompt");
46
47
  const agent_1 = require("../lib/agent");
@@ -246,20 +247,28 @@ function printPostInstall() {
246
247
  const onPath = isLumoOnPath();
247
248
  const credsPath = path.join((0, config_1.configDir)(), 'credentials.json');
248
249
  const authed = fs.existsSync(credsPath);
249
- process.stdout.write('\nNext steps:\n');
250
+ // LUM-686: same conditional advice, now assembled as NextStep[] and handed
251
+ // to the shared renderer. Locally static — setup has no task state to judge.
252
+ const steps = [];
250
253
  if (!onPath || isRunningUnderNpx()) {
251
- process.stdout.write(' • Install the CLI globally so Claude Code hooks can find it:\n' +
252
- ' npm install -g @lumoai/cli\n');
254
+ steps.push({
255
+ command: 'npm install -g @lumoai/cli',
256
+ why: 'install the CLI globally so Claude Code hooks can find it',
257
+ });
253
258
  }
254
259
  if (!authed) {
255
- process.stdout.write(' • Authenticate so the CLI can sync with the Lumo server:\n' +
256
- ' lumo auth login\n');
260
+ steps.push({
261
+ command: 'lumo auth login',
262
+ why: 'authenticate so the CLI can sync with the Lumo server',
263
+ });
257
264
  }
258
265
  if (onPath && authed && !isRunningUnderNpx()) {
259
- process.stdout.write(' • All set. Open Claude Code in this directory — the SKILL.md and\n' +
260
- ' hooks are wired in.\n');
266
+ steps.push({
267
+ command: null,
268
+ why: 'All set. Open Claude Code in this directory — the SKILL.md and hooks are wired in.',
269
+ });
261
270
  }
262
- process.stdout.write('\n');
271
+ process.stdout.write(`\n${(0, next_steps_1.formatNextSteps)(steps)}\n\n`);
263
272
  }
264
273
  function isLumoOnPath() {
265
274
  try {
@@ -54,11 +54,12 @@ function formatCommentThread(comments, opts = {}) {
54
54
  if (opts.full)
55
55
  return blocks.join('\n\n');
56
56
  const id = opts.identifier ?? '<LUM-N>';
57
+ const fetchCommand = opts.fetchCommand ?? 'task comments list';
57
58
  return (0, output_budget_1.truncateUnitsToBudget)({
58
59
  units: blocks,
59
60
  maxTokens: opts.maxTokens,
60
61
  unitNoun: 'comments',
61
- fetchHint: `read the whole thread with: lumo task comments list ${id} --full`,
62
+ fetchHint: `read the whole thread with: lumo ${fetchCommand} ${id} --full`,
62
63
  separator: '\n\n',
63
64
  }).text;
64
65
  }
@@ -5,9 +5,10 @@ exports.formatTaskContextMarkdown = formatTaskContextMarkdown;
5
5
  const config_1 = require("../lib/config");
6
6
  const api_1 = require("../lib/api");
7
7
  const sanitize_1 = require("../lib/sanitize");
8
+ const next_steps_1 = require("../lib/next-steps");
8
9
  const format_1 = require("../lib/format");
9
10
  const output_budget_1 = require("../../../shared/src/output-budget");
10
- async function taskContext(identifier) {
11
+ async function taskContext(identifier, options) {
11
12
  if (!identifier) {
12
13
  console.error('Error: missing <identifier>. Usage: lumo task context <LUM-42>');
13
14
  return 1;
@@ -45,6 +46,9 @@ async function taskContext(identifier) {
45
46
  const data = (await res.json());
46
47
  const now = new Date();
47
48
  process.stdout.write(formatTaskContextMarkdown(data, now));
49
+ // Emitted outside the rendered markdown so the output budget can never drop
50
+ // it — the closing "so do this now" line is the cheapest section here.
51
+ (0, next_steps_1.emitNextSteps)(data.nextSteps ?? [], options ?? {});
48
52
  }
49
53
  /**
50
54
  * Render a TaskContextResponse as the agent-facing markdown handoff. Pure
@@ -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,20 @@ 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");
63
+ const plan_1 = require("./commands/plan");
50
64
  const cost_1 = require("./commands/cost");
51
65
  const priority_1 = require("./commands/priority");
52
66
  const criteria_audit_1 = require("./commands/criteria-audit");
@@ -202,8 +216,18 @@ const program = new commander_1.Command()
202
216
  // point at --help instead of dead-ending on "unknown option". Subcommands
203
217
  // created via .command() inherit these settings.
204
218
  .showSuggestionAfterError(true)
205
- .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');
206
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
+ });
207
231
  const auth = program.command('auth').description('Manage Lumo authentication');
208
232
  auth
209
233
  .command('login')
@@ -278,11 +302,112 @@ program
278
302
  .option('-n, --count <N>', 'Number of tasks to recommend (default 3)')
279
303
  .option('--claimable', 'Only recommend agent-claimable tasks that are unblocked (F3) and within milestone budget (F2)')
280
304
  .action(wrap(options => (0, next_1.nextCommand)(options)));
281
- program
282
- .command('idea <statement>')
305
+ const ideaCmd = program
306
+ .command('idea [statement]')
283
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.')
284
308
  .option('-c, --context <text>', 'Free-text origin context for the idea')
285
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)()));
402
+ const planCmd = program
403
+ .command('plan')
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.')
405
+ .option('--abandon-active', 'Abandon the team’s existing active run before starting a fresh one')
406
+ .action(wrap(options => (0, plan_1.plan)(options)));
407
+ planCmd
408
+ .command('status')
409
+ .description('Print the active plan run’s current stage and the deep-link to its next gate; with no active run, prompts to run `lumo plan`.')
410
+ .action(wrap(() => (0, plan_1.planStatus)()));
286
411
  program
287
412
  .command('cost')
288
413
  .description('Show per-operation (per-tool) token cost. Defaults to a workspace 30-day window; scope with --task / --session')
@@ -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.54.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",