@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
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: lumo
3
- description: 'Use when the user mentions a Lumo task id (LUM-N, or any team prefix like SPEC-12) or the `lumo` CLI, in any language; is starting, resuming, or about to claim completion of a task; asks what to work on next; or works with any Lumo resource — task context, sessions, acceptance criteria, machine verification, tasks, ideas, projects, milestones, sprints, docs, artifacts, Figma links, dependencies, team memory, priorities, or worktrees. Key triggers: "LUM-", "lumo", "task context", "session attach", "verify", "task status", "acceptance criteria", "what should I work on", "resume task".'
3
+ description: 'Use when the user mentions a Lumo task id (LUM-N, or any team prefix like SPEC-12) or the `lumo` CLI, in any language; is starting, resuming, or about to claim completion of a task; asks what to work on next; or works with any Lumo resource — task context, sessions, acceptance criteria, machine verification, tasks, ideas, plan runs (the idea→plan converter), projects, milestones, sprints, docs, artifacts, Figma links, dependencies, team memory, priorities, or worktrees. Key triggers: "LUM-", "lumo", "task context", "session attach", "verify", "task status", "acceptance criteria", "what should I work on", "resume task".'
4
4
  ---
5
5
 
6
6
  ## Prerequisites
@@ -74,6 +74,73 @@ The command catalog below is a **map**: it lists every command grouped by domain
74
74
 
75
75
  - `lumo idea "<statement>" [--context <text>]` — capture a team-level idea into the pool in <10s. Provenance is grabbed opportunistically and never blocks the capture: the current `CLAUDE_CODE_SESSION_ID` (→ sourceSessionId) and the session's bound task (→ sourceTaskId, null when nothing is bound). Prints an **I-prefixed** id (`✓ 想法 LUM-I42 已入池`) — the `I` keeps ideas from colliding with task ids (`LUM-42`) in the same team namespace. Ideas are team-scoped (no project); the workspace's default team owns them. The unprocessed (CAPTURED) pool is what the Phase-2 transformer consumes.
76
76
  - **When to suggest**: the user has a stray idea/thought/improvement they want to park without derailing the current task ("记一下这个想法", "capture this idea", "add to the backlog of ideas"). Prefer this over `task create` for un-triaged sparks — an idea is above projects and isn't yet actionable work.
77
+ - **Example**:
78
+ ```bash
79
+ lumo idea "Add a quick-capture keyboard shortcut" --context "raised in standup"
80
+ ```
81
+ - `lumo idea list` — print the team idea pool **newest-first**, one line per idea: the `LUM-I<n>` id, status (`CAPTURED` | `DEVELOPING` | `PLANNED` | `DROPPED`) and statement. Reads `GET /api/ideas`, which returns the whole pool (every status), not just the unprocessed slice. Empty pool prints `No ideas.`. The same pool is browsable/searchable in web at `/workspace/<slug>/ideas` (sidebar → Ideas), where a PLANNED idea links to the plan run that materialised it and the status filter hides DROPPED by default.
82
+ - **When to suggest**: the user wants to see what's parked in the idea pool / "看看想法池" / "list the ideas" / triage before running `lumo plan`.
83
+ - **Example**:
84
+ ```bash
85
+ lumo idea list
86
+ ```
87
+ - `lumo idea update <id> --status <captured|developing|planned|dropped>` — move an idea through its lifecycle (LUM-679). `<id>` is the `LUM-I<n>` id (a bare number also resolves); `--status` is case-insensitive. The four states are `CAPTURED` (pooled) → `DEVELOPING` (in triage / pre-work) → `PLANNED` (consumed by an initiative/plan), with `DROPPED` the terminal discard. Allowed transitions: `CAPTURED↔DEVELOPING`, `DEVELOPING→PLANNED`, and **any non-terminal → DROPPED**; nothing transitions out of `DROPPED`, and `CAPTURED→PLANNED` is not a manual move (only the converter's materialisation lands an idea in PLANNED). An illegal move is rejected with a clear error (400); an unknown id → 404.
88
+ - **When to suggest**: the user wants to triage the pool by hand — start pre-work on an idea ("开始预研这个想法" → `developing`), or discard one ("废弃/丢掉这个想法" → `dropped`). The status control also lives on the Idea detail page in web.
89
+ - **Example**:
90
+ ```bash
91
+ lumo idea update LUM-I42 --status developing
92
+ lumo idea update LUM-I42 --status dropped
93
+ ```
94
+ - `lumo idea slack add/show/rm <idea> ...`, `lumo idea web add/show/rm <idea> ...`, `lumo idea figma add/list/rm/refresh/context <idea> ...` (LUM-681) — attach & inspect evidence (Slack threads, web links, Figma design links) on an idea, mirroring the `task slack/web/figma` source-card commands but hitting `/api/ideas/:id/...`. `<idea>` is the `LUM-I<n>` id. `show`/`context` are Tier-2 retrieval (stored snapshot / fetched body / cached design metadata, no live re-fetch) and stamp the disclosure funnel like their task-side counterparts; `figma refresh` re-fetches metadata for every link on the idea.
95
+ - **When to suggest**: the user wants to attach supporting evidence (a Slack discussion, a spec doc link, a Figma mock) to an idea before it goes through `lumo plan` — the converter carries this evidence forward into the resulting task/initiative. Prefer attaching to the idea over waiting until after conversion.
96
+ - **Example**:
97
+ ```bash
98
+ lumo idea web add LUM-I42 https://example.com/spec
99
+ lumo idea web show LUM-I42 wl_1
100
+ lumo idea web rm LUM-I42 wl_1
101
+ lumo idea slack add LUM-I42 https://myteam.slack.com/archives/C1/p123
102
+ lumo idea slack show LUM-I42 ctx_1
103
+ lumo idea slack rm LUM-I42 ctx_1
104
+ lumo idea figma add LUM-I42 https://www.figma.com/file/abc/Mock
105
+ lumo idea figma list LUM-I42
106
+ lumo idea figma context LUM-I42 cfl_1
107
+ lumo idea figma refresh LUM-I42
108
+ lumo idea figma rm LUM-I42 cfl_1
109
+ ```
110
+
111
+ - `lumo idea comment <id> <body>` — post a comment on an idea (LUM-680). `<id>` is the `LUM-I<n>` id (a bare number also resolves) or a cuid; the body is plain text — quote it to pass spaces/newlines. Ideas reuse the task Comment model via a polymorphic `ideaId`; the thread is flat (no replies). Posting also records a `COMMENT_ADDED` entry on the idea's activity timeline. Comments/activity also render on the Idea detail slide-over in web.
112
+ - `lumo idea comments list <id> [--full]` — print an idea's comment thread (oldest first), capped to the output budget like `task comments list`; `--full` prints every comment. Empty thread → `(no comments)`.
113
+ - **When to suggest**: the user wants to discuss or annotate a parked idea ("给这个想法加条评论", "comment on the idea"), or review an idea's discussion/history ("看看这个想法的评论/活动"). For the activity timeline (status changes, statement edits, materialisation) there is no CLI reader — it's shown on the Idea detail slide-over in web.
114
+ - **Example**:
115
+ ```bash
116
+ lumo idea comment LUM-I42 "Let's scope this for next sprint"
117
+ lumo idea comments list LUM-I42
118
+ ```
119
+
120
+ **Plan runs (design-thinking 转换器)**
121
+
122
+ - `lumo plan [--abandon-active]` — start a converter run that turns the team's captured idea pool + current priority into an executable plan through a 3-gate recoverable state machine (聚类→对齐→成计划). Freezes the input snapshot at start and enforces **one active run per team**: if a run is already active it refuses and points to `lumo plan status`; `--abandon-active` abandons the prior run first, then starts fresh. On success it prints (and opens) the **gate-A** web deep-link (`/workspace/<slug>/plan/<runId>`). The CLI only triggers — **all editing and gate confirmation happen in web**.
123
+ - `lumo plan status` — print the active run's current stage (working segment or the open `*_READY` gate) and the deep-link to act on its next gate; with no active run, prompts to run `lumo plan`.
124
+ - **When to suggest**: the user wants to turn the parked idea pool into a plan / "跑一下转换器" / "start a planning run" / "把想法变成计划"; or asks where an in-flight plan run stands ("plan 到哪一步了", "plan status"). The gate work itself is web-only — the CLI hands off the deep-link.
125
+ - **Example** — check where the active run stands:
126
+ ```bash
127
+ lumo plan status
128
+ ```
129
+
130
+ **Initiatives (team-level, LUM-INIT-<n>)**
131
+
132
+ - `lumo initiative create "<goal>" [--assumption <text>]` — create a team-level Initiative **directly, no plan run required** (`planRunId=null`, status `ACTIVE`). Prints the `LUM-INIT-<n>` id; `--assumption` records the bet (赌注旁注) behind it. A blank goal is rejected (400). Converter-materialised initiatives (from `lumo plan`) live in the same list.
133
+ - **When to suggest**: the user wants to stand up a strategic initiative directly — "建一个 initiative" / "create an initiative" / "开一个举措" — without running the converter.
134
+ - **Example**:
135
+ ```bash
136
+ lumo initiative create "Ship the initiatives browse surface" --assumption "teams curate initiatives directly"
137
+ ```
138
+ - `lumo initiative list` — print the team's initiatives newest-first, one line per initiative: the `LUM-INIT-<n>` id, status (`ACTIVE` | `DONE` | `DROPPED`) and goal. Browsable in web at `/workspace/<slug>/initiatives` (sidebar → Initiatives), where each links to its lineage (consumed ideas + attached tasks).
139
+ - **When to suggest**: the user wants to see the team's initiatives / "看看 initiatives" / "list the initiatives".
140
+ - **Example**:
141
+ ```bash
142
+ lumo initiative list
143
+ ```
77
144
 
78
145
  **Task dependencies** — see [task-deps.md](references/task-deps.md)
79
146
 
@@ -201,6 +268,16 @@ lumo criteria audit
201
268
 
202
269
  **When to suggest**: when the user wants to audit how much of the workspace's "green" criteria are self-confirming vs externally anchored; when diagnosing whether acceptance criteria have real external hooks or are circular.
203
270
 
271
+ ## Next-step hints (the trailing `Next:` block)
272
+
273
+ `task create`, `task update`, `session attach`, `task context` and `next` end with a short `Next:` block naming what to do now — attach an unbound session, draft a missing contract, run `lumo verify`, read `task status` after a rejected round, explain an open boundary crossing, or stop and wait when the task is IN_REVIEW.
274
+
275
+ The rules are computed **server-side** from task status × contract × session binding × verify round, so the advice tracks the real state machine rather than a hard-coded string. Capped at **3 suggestions**; a rule whose condition is already satisfied never appears. The block goes to **stdout**, after the command's own result line — the result line stays first and unchanged, so reading line 1 still works.
276
+
277
+ Silence it with `--no-hints` on any command, or `LUMO_NO_HINTS=1` for a whole shell.
278
+
279
+ The block never replaces the acceptance-contract section: on `session attach` / `task context` the criteria draft reminder comes from `criteriaSection` and is deliberately **not** repeated as a step.
280
+
204
281
  ## Commands & flags that do NOT exist (common mistakes)
205
282
 
206
283
  Don't guess these:
@@ -17,11 +17,8 @@ Declare the current team priority.
17
17
  ```bash
18
18
  lumo priority "Ship the idea-capture MVP; defer the converter to Spec 2."
19
19
  ```
20
- - **With no argument** — prompts interactively for a 1-3 sentence statement:
21
- ```bash
22
- lumo priority
23
- # Priority (1-3 sentences): _
24
- ```
20
+ - **With no argument** — running `lumo priority` on its own prompts
21
+ interactively for a 1-3 sentence statement (`Priority (1-3 sentences): _`).
25
22
  An empty prompt records nothing and exits non-zero.
26
23
 
27
24
  On success it prints `Recorded priority: <statement>`. A blank statement is
@@ -32,6 +29,10 @@ rejected (the API's Zod layer returns 400).
32
29
  Print the append history, newest-first. The current declaration (the row with
33
30
  the latest `effectiveFrom`) is marked with a leading `*`:
34
31
 
32
+ ```bash
33
+ lumo priority list
34
+ ```
35
+
35
36
  ```
36
37
  * 2026-02-01 Now focus on the converter state machine
37
38
  2026-01-01 Ship the idea-capture MVP
@@ -34,9 +34,15 @@ On success the command prints one or two lines to stdout:
34
34
  ```
35
35
  Created LUM-55 "Title" https://www.uselumo.ai/workspace/<slug>/my-tasks/LUM-55
36
36
  Tags: bug, urgent
37
+
38
+ Next:
39
+ lumo session attach LUM-55
40
+ Bind this session to the task so its work is recorded against it
41
+ lumo task criteria set LUM-55 --file <criteria.json>
42
+ Draft the acceptance contract before writing the first line of code
37
43
  ```
38
44
 
39
- The `Tags:` line is omitted when no tags were attached.
45
+ The `Tags:` line is omitted when no tags were attached. The trailing `Next:` block is server-computed and conditional — the attach line is skipped when this session is already bound (a second attach is a 409), and the whole block is suppressed by `--no-hints` / `LUMO_NO_HINTS=1`. The `Created …` line stays first and unchanged. See SKILL.md "Next-step hints".
40
46
 
41
47
  If the workspace has multiple projects and `--project` is omitted, the server will return an error naming available projects — re-run with `--project <slug>`.
42
48
 
@@ -93,9 +99,13 @@ On success the command prints one or two lines to stdout:
93
99
  ```
94
100
  Updated LUM-48 "Title" https://www.uselumo.ai/workspace/<slug>/my-tasks/LUM-48
95
101
  Tags: urgent
102
+
103
+ Next:
104
+ lumo verify --note "<self-report>"
105
+ Run the machine acceptance loop before claiming the task is done
96
106
  ```
97
107
 
98
- The `Tags:` line is omitted when the resulting tag set is empty.
108
+ The `Tags:` line is omitted when the resulting tag set is empty. The trailing `Next:` block reflects the **post-update** status — `--status in_review` prints a stop-and-wait note instead of a command, and a DONE task prints nothing. Suppressed by `--no-hints` / `LUMO_NO_HINTS=1`.
99
109
 
100
110
  ### Status transitions — direct moves are legal
101
111
 
@@ -213,7 +223,9 @@ Top 3 recommended tasks (of 12 open):
213
223
  3. LUM-12 TODO MEDIUM Add rate limiting
214
224
  ↳ MEDIUM · blocked by 1 open dep · due 2026-06-10
215
225
 
216
- Next: lumo session attach LUM-42 && lumo task context LUM-42
226
+ Next:
227
+ lumo session attach LUM-42 && lumo task context LUM-42
228
+ start on the top-ranked task
217
229
  ```
218
230
 
219
231
  When to suggest: the user asks "what should I work on", "what's next", "recommend my next task" (in any language), "pick my next task", or starts a session without a task in mind. After they choose, run `session attach` + `task context` for the picked task.
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ideaComment = ideaComment;
4
+ exports.ideaCommentList = ideaCommentList;
5
+ const config_1 = require("../lib/config");
6
+ const api_1 = require("../lib/api");
7
+ const sanitize_1 = require("../lib/sanitize");
8
+ const task_comment_list_1 = require("./task-comment-list");
9
+ /**
10
+ * `lumo idea comment <id> <body>` — post a comment on a team-level idea.
11
+ *
12
+ * `<id>` is the `<TEAM>-I<n>` identifier (a bare `<n>` also resolves) or a cuid.
13
+ * Unlike task comments, the idea comments endpoint is keyed by the idea ref
14
+ * directly, so there's no separate resolve step. Body is plain text — quote it
15
+ * to pass spaces/newlines.
16
+ */
17
+ async function ideaComment(idRef, body) {
18
+ if (!idRef || idRef.trim().length === 0) {
19
+ console.error('Error: usage: lumo idea comment <LUM-I42> "<body>"');
20
+ return 1;
21
+ }
22
+ if (!body || body.trim().length === 0) {
23
+ console.error('Error: comment body cannot be empty.');
24
+ return 1;
25
+ }
26
+ const creds = (0, config_1.readCredentials)();
27
+ if (!creds) {
28
+ console.error('Error: not logged in. Run `lumo auth login` first.');
29
+ return 1;
30
+ }
31
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
32
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
33
+ const url = `${base}/api/ideas/${encodeURIComponent(idRef.trim())}/comments`;
34
+ let res;
35
+ try {
36
+ res = await fetch(url, {
37
+ method: 'POST',
38
+ headers: {
39
+ Authorization: `Bearer ${creds.token}`,
40
+ 'Content-Type': 'application/json',
41
+ },
42
+ body: JSON.stringify({ body: body.trim() }),
43
+ });
44
+ }
45
+ catch (err) {
46
+ const msg = err instanceof Error ? err.message : String(err);
47
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
48
+ return 1;
49
+ }
50
+ if (res.status === 401) {
51
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
52
+ return 1;
53
+ }
54
+ if (res.status === 201) {
55
+ process.stdout.write(`✓ commented on ${(0, sanitize_1.sanitizeField)(idRef.trim())}\n`);
56
+ return;
57
+ }
58
+ let serverMsg = null;
59
+ try {
60
+ const errBody = (await res.json());
61
+ if (typeof errBody.error === 'string')
62
+ serverMsg = errBody.error;
63
+ }
64
+ catch {
65
+ // Body wasn't JSON; fall through to status-only message
66
+ }
67
+ if (serverMsg) {
68
+ console.error(`Error: ${(0, sanitize_1.sanitizeField)(serverMsg)}`);
69
+ }
70
+ else {
71
+ console.error(`Error: idea comment failed (HTTP ${res.status})`);
72
+ }
73
+ return 1;
74
+ }
75
+ /**
76
+ * `lumo idea comments list <id>` — print an idea's comment thread. Reuses the
77
+ * shared `formatCommentThread` renderer (same output-budget cap + `--full`
78
+ * escape hatch as `lumo task comments list`). Idea threads are flat, so each
79
+ * block is a single top-level comment.
80
+ */
81
+ async function ideaCommentList(idRef, opts = {}) {
82
+ if (!idRef || idRef.trim().length === 0) {
83
+ console.error('Error: usage: lumo idea comments list <LUM-I42>');
84
+ return 1;
85
+ }
86
+ const creds = (0, config_1.readCredentials)();
87
+ if (!creds) {
88
+ console.error('Error: not logged in. Run `lumo auth login` first.');
89
+ return 1;
90
+ }
91
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
92
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
93
+ const url = `${base}/api/ideas/${encodeURIComponent(idRef.trim())}/comments`;
94
+ let res;
95
+ try {
96
+ res = await fetch(url, {
97
+ headers: { Authorization: `Bearer ${creds.token}` },
98
+ });
99
+ }
100
+ catch (err) {
101
+ const msg = err instanceof Error ? err.message : String(err);
102
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
103
+ return 1;
104
+ }
105
+ if (res.status === 401) {
106
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
107
+ return 1;
108
+ }
109
+ if (res.status === 404) {
110
+ console.error(`Error: idea ${(0, sanitize_1.sanitizeField)(idRef.trim())} not found`);
111
+ return 1;
112
+ }
113
+ if (!res.ok) {
114
+ console.error(`Error: idea comments list failed (HTTP ${res.status})`);
115
+ return 1;
116
+ }
117
+ const { comments } = (await res.json());
118
+ if (!comments || comments.length === 0) {
119
+ console.log('(no comments)');
120
+ return;
121
+ }
122
+ console.log((0, task_comment_list_1.formatCommentThread)(comments, {
123
+ full: opts.full,
124
+ identifier: idRef.trim(),
125
+ fetchCommand: 'idea comments list',
126
+ }));
127
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ideaFigmaAdd = ideaFigmaAdd;
4
+ const idea_figma_api_1 = require("../lib/idea-figma-api");
5
+ const sanitize_1 = require("../lib/sanitize");
6
+ async function ideaFigmaAdd(args) {
7
+ try {
8
+ const { link } = await (0, idea_figma_api_1.addIdeaFigmaLink)(args.identifier, args.url);
9
+ const label = (0, sanitize_1.sanitizeField)(link.frameName
10
+ ? `${link.fileName} · ${link.frameName}`
11
+ : `${link.fileName}${link.nodeId === '' ? ' (file-level)' : ''}`);
12
+ console.log(`Linked ${args.identifier} ↔ Figma "${label}"`);
13
+ console.log(` ${link.url}`);
14
+ return 0;
15
+ }
16
+ catch (e) {
17
+ const err = e;
18
+ if (err.code === 'figma_not_connected') {
19
+ console.error(`✗ You haven't connected Figma yet.`);
20
+ console.error(` Run: open https://www.uselumo.ai/settings/integrations`);
21
+ return 1;
22
+ }
23
+ if (err.code === 'figma_needs_reauth') {
24
+ console.error(`✗ Your Figma token is no longer valid. Please reconnect.`);
25
+ console.error(` Run: open https://www.uselumo.ai/settings/integrations`);
26
+ return 1;
27
+ }
28
+ throw e; // let wrap() in cli/src/index.ts log and exit 1
29
+ }
30
+ }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ideaFigmaContext = ideaFigmaContext;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const sanitize_1 = require("../lib/sanitize");
7
+ const report_pull_1 = require("../lib/report-pull");
8
+ /**
9
+ * `lumo idea figma context <LUM-I1> <link-id>`
10
+ *
11
+ * Tier-2 retrieval for the cheap inline Figma card on an idea. Reads the
12
+ * cached design metadata (file/frame name, url, thumbnail, last sync) from
13
+ * `/api/ideas/:id/figma-links/:linkId/context`. Mirrors `taskFigmaContext`
14
+ * (LUM-681).
15
+ */
16
+ async function ideaFigmaContext(identifier, linkId) {
17
+ if (!identifier || !linkId) {
18
+ console.error('Error: usage: lumo idea figma context <LUM-I1> <link-id>');
19
+ return 1;
20
+ }
21
+ const creds = (0, config_1.readCredentials)();
22
+ if (!creds) {
23
+ console.error('Error: not logged in. Run `lumo auth login` first.');
24
+ return 1;
25
+ }
26
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
27
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
28
+ let res;
29
+ try {
30
+ res = await fetch(`${base}/api/ideas/${encodeURIComponent(identifier)}/figma-links/${encodeURIComponent(linkId)}/context`, { headers: { Authorization: `Bearer ${creds.token}` } });
31
+ }
32
+ catch (err) {
33
+ const msg = err instanceof Error ? err.message : String(err);
34
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
35
+ return 1;
36
+ }
37
+ if (res.status === 401) {
38
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
39
+ return 1;
40
+ }
41
+ if (res.status === 404) {
42
+ console.error(`Error: idea ${identifier} or figma link ${linkId} not found in workspace ${creds.workspaceSlug}`);
43
+ return 1;
44
+ }
45
+ if (!res.ok) {
46
+ console.error(`Error: figma context failed (HTTP ${res.status})`);
47
+ return 1;
48
+ }
49
+ const { metadata, note } = (await res.json());
50
+ if (metadata.fileName)
51
+ console.log(`file: ${(0, sanitize_1.sanitizeField)(metadata.fileName)}`);
52
+ if (metadata.frameName)
53
+ console.log(`frame: ${(0, sanitize_1.sanitizeField)(metadata.frameName)}`);
54
+ console.log(`url: ${metadata.url}`);
55
+ if (metadata.lastSyncedAt)
56
+ console.log(`synced: ${metadata.lastSyncedAt}`);
57
+ if (metadata.lastSyncError)
58
+ console.log(`syncError: ${(0, sanitize_1.sanitizeField)(metadata.lastSyncError)}`);
59
+ if (note)
60
+ console.log(`\nnote: ${(0, sanitize_1.sanitizeField)(note)}`);
61
+ // LUM-681: mirrors LUM-500 disclosure-funnel stamping for idea fragments.
62
+ // The linkId arg == lineage FIGMA fragmentId. Fire-and-forget — never
63
+ // blocks output, swallows failures.
64
+ await (0, report_pull_1.reportPull)({ fragmentType: 'FIGMA', fragmentId: linkId });
65
+ }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ideaFigmaList = ideaFigmaList;
4
+ const idea_figma_api_1 = require("../lib/idea-figma-api");
5
+ const sanitize_1 = require("../lib/sanitize");
6
+ const STALE_MS = 25 * 24 * 3600 * 1000;
7
+ function formatRow(id, fileName, frame, synced, stale) {
8
+ return [
9
+ id.padEnd(10),
10
+ (0, sanitize_1.sanitizeField)(fileName).padEnd(20),
11
+ (0, sanitize_1.sanitizeField)(frame).padEnd(24),
12
+ synced.padEnd(12),
13
+ stale ? '⚠ thumbnail stale' : '',
14
+ ]
15
+ .join(' ')
16
+ .trimEnd();
17
+ }
18
+ async function ideaFigmaList(args) {
19
+ const { links } = await (0, idea_figma_api_1.listIdeaFigmaLinks)(args.identifier);
20
+ if (links.length === 0) {
21
+ console.log('No Figma links attached.');
22
+ return 0;
23
+ }
24
+ for (const link of links) {
25
+ const frame = link.frameName ?? '(file-level)';
26
+ const synced = link.lastSyncedAt.slice(0, 10);
27
+ const stale = link.thumbnailFetchedAt === null ||
28
+ Date.now() - new Date(link.thumbnailFetchedAt).getTime() > STALE_MS;
29
+ console.log(formatRow(link.id, link.fileName, frame, synced, stale && !!link.thumbnailUrl));
30
+ }
31
+ return 0;
32
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ideaFigmaRefresh = ideaFigmaRefresh;
4
+ const idea_figma_api_1 = require("../lib/idea-figma-api");
5
+ const sanitize_1 = require("../lib/sanitize");
6
+ async function ideaFigmaRefresh(args) {
7
+ try {
8
+ const { links } = await (0, idea_figma_api_1.listIdeaFigmaLinks)(args.identifier);
9
+ const byId = new Map(links.map(l => [l.id, l]));
10
+ const { results } = await (0, idea_figma_api_1.refreshIdeaFigmaLinks)(args.identifier);
11
+ if (results.length === 0) {
12
+ console.log(`No Figma links attached to ${args.identifier}.`);
13
+ return 0;
14
+ }
15
+ console.log(`Refreshed ${results.length} Figma links on ${args.identifier}`);
16
+ for (const r of results) {
17
+ const link = byId.get(r.id);
18
+ const label = link
19
+ ? (0, sanitize_1.sanitizeField)(link.frameName
20
+ ? `${link.fileName} · ${link.frameName}`
21
+ : `${link.fileName} (file-level)`)
22
+ : r.id;
23
+ if (r.ok)
24
+ console.log(` ✓ ${label}`);
25
+ else
26
+ console.log(` ✗ ${label} (${(0, sanitize_1.sanitizeField)(r.error ?? '')})`);
27
+ }
28
+ return 0;
29
+ }
30
+ catch (e) {
31
+ const err = e;
32
+ if (err.code === 'figma_not_connected' ||
33
+ err.code === 'figma_needs_reauth') {
34
+ console.error(`✗ Figma is not connected (or token expired). Reconnect:`);
35
+ console.error(` open https://www.uselumo.ai/settings/integrations`);
36
+ return 1;
37
+ }
38
+ throw e;
39
+ }
40
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ideaFigmaRm = ideaFigmaRm;
4
+ const idea_figma_api_1 = require("../lib/idea-figma-api");
5
+ const sanitize_1 = require("../lib/sanitize");
6
+ async function ideaFigmaRm(args) {
7
+ const { links } = await (0, idea_figma_api_1.listIdeaFigmaLinks)(args.identifier);
8
+ const match = links.find(l => l.id === args.linkIdOrUrl || l.url === args.linkIdOrUrl);
9
+ if (!match) {
10
+ console.log(`Not linked: ${args.linkIdOrUrl}`);
11
+ return 0;
12
+ }
13
+ await (0, idea_figma_api_1.removeIdeaFigmaLink)(args.identifier, match.id);
14
+ const label = (0, sanitize_1.sanitizeField)(match.frameName
15
+ ? `${match.fileName} · ${match.frameName}`
16
+ : `${match.fileName} (file-level)`);
17
+ console.log(`Removed Figma link from ${args.identifier}: "${label}"`);
18
+ return 0;
19
+ }
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ideaSlackAdd = ideaSlackAdd;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const sanitize_1 = require("../lib/sanitize");
7
+ /**
8
+ * `lumo idea slack add <LUM-I1> <permalink>`
9
+ *
10
+ * Attaches a Slack thread to an idea by POSTing the permalink to
11
+ * `/api/ideas/:id/slack-contexts`. Mirrors the task-side flow (LUM-681).
12
+ */
13
+ async function ideaSlackAdd(identifier, permalink) {
14
+ if (!identifier || !permalink) {
15
+ console.error('Error: usage: lumo idea slack add <LUM-I1> <permalink>');
16
+ return 1;
17
+ }
18
+ const creds = (0, config_1.readCredentials)();
19
+ if (!creds) {
20
+ console.error('Error: not logged in. Run `lumo auth login` first.');
21
+ return 1;
22
+ }
23
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
24
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
25
+ let res;
26
+ try {
27
+ res = await fetch(`${base}/api/ideas/${encodeURIComponent(identifier)}/slack-contexts`, {
28
+ method: 'POST',
29
+ headers: {
30
+ Authorization: `Bearer ${creds.token}`,
31
+ 'Content-Type': 'application/json',
32
+ },
33
+ body: JSON.stringify({ permalink }),
34
+ });
35
+ }
36
+ catch (err) {
37
+ const msg = err instanceof Error ? err.message : String(err);
38
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
39
+ return 1;
40
+ }
41
+ if (res.status === 401) {
42
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
43
+ return 1;
44
+ }
45
+ if (res.status === 404) {
46
+ console.error(`Error: idea ${identifier} not found in workspace ${creds.workspaceSlug}`);
47
+ return 1;
48
+ }
49
+ if (!res.ok) {
50
+ let serverMsg = null;
51
+ try {
52
+ const errBody = (await res.json());
53
+ if (typeof errBody.error === 'string')
54
+ serverMsg = errBody.error;
55
+ }
56
+ catch {
57
+ /* not JSON */
58
+ }
59
+ console.error(serverMsg
60
+ ? `Error: ${(0, sanitize_1.sanitizeField)(serverMsg)}`
61
+ : `Error: slack add failed (HTTP ${res.status})`);
62
+ return 1;
63
+ }
64
+ const { context } = (await res.json());
65
+ console.log(`Linked Slack thread to ${identifier} (context ${context.id})`);
66
+ }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ideaSlackRm = ideaSlackRm;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const sanitize_1 = require("../lib/sanitize");
7
+ /**
8
+ * `lumo idea slack rm <LUM-I1> <context-id>`
9
+ *
10
+ * Removes a Slack context from an idea via
11
+ * `DELETE /api/ideas/:id/slack-contexts/:contextId`. Mirrors the task-side
12
+ * shape (LUM-681); no task-side `rm` equivalent exists yet, so this follows
13
+ * the DELETE route contract directly.
14
+ */
15
+ async function ideaSlackRm(identifier, contextId) {
16
+ if (!identifier || !contextId) {
17
+ console.error('Error: usage: lumo idea slack rm <LUM-I1> <context-id>');
18
+ return 1;
19
+ }
20
+ const creds = (0, config_1.readCredentials)();
21
+ if (!creds) {
22
+ console.error('Error: not logged in. Run `lumo auth login` first.');
23
+ return 1;
24
+ }
25
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
26
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
27
+ let res;
28
+ try {
29
+ res = await fetch(`${base}/api/ideas/${encodeURIComponent(identifier)}/slack-contexts/${encodeURIComponent(contextId)}`, {
30
+ method: 'DELETE',
31
+ headers: { Authorization: `Bearer ${creds.token}` },
32
+ });
33
+ }
34
+ catch (err) {
35
+ const msg = err instanceof Error ? err.message : String(err);
36
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
37
+ return 1;
38
+ }
39
+ if (res.status === 401) {
40
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
41
+ return 1;
42
+ }
43
+ if (res.status === 404) {
44
+ console.error(`Error: idea ${identifier} or slack context ${contextId} not found in workspace ${creds.workspaceSlug}`);
45
+ return 1;
46
+ }
47
+ if (!res.ok && res.status !== 204) {
48
+ let serverMsg = null;
49
+ try {
50
+ const errBody = (await res.json());
51
+ if (typeof errBody.error === 'string')
52
+ serverMsg = errBody.error;
53
+ }
54
+ catch {
55
+ /* not JSON */
56
+ }
57
+ console.error(serverMsg
58
+ ? `Error: ${(0, sanitize_1.sanitizeField)(serverMsg)}`
59
+ : `Error: slack rm failed (HTTP ${res.status})`);
60
+ return 1;
61
+ }
62
+ console.log(`Removed Slack context ${contextId} from ${identifier}`);
63
+ }