@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.
- package/assets/skill/SKILL.md +71 -0
- package/assets/skill/references/priority.md +6 -5
- package/assets/skill/references/tasks.md +15 -3
- package/dist/cli/src/commands/idea-comment.js +127 -0
- package/dist/cli/src/commands/idea-figma-add.js +30 -0
- package/dist/cli/src/commands/idea-figma-context.js +65 -0
- package/dist/cli/src/commands/idea-figma-list.js +32 -0
- package/dist/cli/src/commands/idea-figma-refresh.js +40 -0
- package/dist/cli/src/commands/idea-figma-rm.js +19 -0
- package/dist/cli/src/commands/idea-slack-add.js +66 -0
- package/dist/cli/src/commands/idea-slack-rm.js +63 -0
- package/dist/cli/src/commands/idea-slack-show.js +65 -0
- package/dist/cli/src/commands/idea-web-add.js +66 -0
- package/dist/cli/src/commands/idea-web-rm.js +63 -0
- package/dist/cli/src/commands/idea-web-show.js +71 -0
- package/dist/cli/src/commands/idea.js +140 -0
- package/dist/cli/src/commands/initiative.js +132 -0
- package/dist/cli/src/commands/next.js +9 -1
- package/dist/cli/src/commands/session-attach.js +4 -0
- package/dist/cli/src/commands/setup.js +17 -8
- package/dist/cli/src/commands/task-comment-list.js +2 -1
- package/dist/cli/src/commands/task-context.js +5 -1
- package/dist/cli/src/commands/task-create.js +18 -1
- package/dist/cli/src/commands/task-update.js +16 -0
- package/dist/cli/src/commands/worktree-add.js +6 -2
- package/dist/cli/src/index.js +118 -3
- package/dist/cli/src/lib/idea-figma-api.js +65 -0
- package/dist/cli/src/lib/next-steps.js +55 -0
- package/package.json +1 -1
package/assets/skill/SKILL.md
CHANGED
|
@@ -74,12 +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
|
+
```
|
|
77
119
|
|
|
78
120
|
**Plan runs (design-thinking 转换器)**
|
|
79
121
|
|
|
80
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**.
|
|
81
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`.
|
|
82
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
|
+
```
|
|
83
144
|
|
|
84
145
|
**Task dependencies** — see [task-deps.md](references/task-deps.md)
|
|
85
146
|
|
|
@@ -207,6 +268,16 @@ lumo criteria audit
|
|
|
207
268
|
|
|
208
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.
|
|
209
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
|
+
|
|
210
281
|
## Commands & flags that do NOT exist (common mistakes)
|
|
211
282
|
|
|
212
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** —
|
|
21
|
-
|
|
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:
|
|
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
|
+
}
|