@lumoai/cli 1.2.0 → 1.3.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/README.md CHANGED
@@ -91,18 +91,18 @@ lumo task update LUM-42 --status done
91
91
 
92
92
  ## Commands
93
93
 
94
- | Group | Highlights |
95
- | --------------- | ------------------------------------------------------------- |
96
- | `setup` | Install SKILL.md + hooks into `~/.claude/` or `./.claude/` |
97
- | `auth` | `login`, `logout` |
98
- | `whoami` | Show current identity + workspace |
99
- | `update` | Self-update to the latest npm release |
100
- | `task` | `create`, `update`, `list`, `show`, `comment`, `context` |
101
- | `session` | `attach`, `status`, `detach` (binds Claude Code sessions) |
102
- | `project` | `list` |
103
- | `milestone` | `create`, `update`, `list`, `show`, `delete` |
104
- | `sprint` | `create`, `start`, `close`, `list`, `show`, `add`, `remove`, `summary` |
105
- | `doc` | `create`, `update`, `list`, `show`, `move`, `bind`, `share` |
94
+ | Group | Highlights |
95
+ | ----------- | ---------------------------------------------------------------------- |
96
+ | `setup` | Install SKILL.md + hooks into `~/.claude/` or `./.claude/` |
97
+ | `auth` | `login`, `logout` |
98
+ | `whoami` | Show current identity + workspace |
99
+ | `update` | Self-update to the latest npm release |
100
+ | `task` | `create`, `update`, `list`, `show`, `comment`, `context` |
101
+ | `session` | `attach`, `status`, `detach` (binds Claude Code sessions) |
102
+ | `project` | `list` |
103
+ | `milestone` | `create`, `update`, `list`, `show`, `delete` |
104
+ | `sprint` | `create`, `start`, `close`, `list`, `show`, `add`, `remove`, `summary` |
105
+ | `doc` | `create`, `update`, `list`, `show`, `move`, `bind`, `share` |
106
106
 
107
107
  Every command accepts `--help` for full flags and examples:
108
108
 
package/assets/skill.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: lumo
3
- description: 'Use the Lumo CLI to load task context, manage session bindings, inspect projects and milestones, and create/update/list/show/comment on tasks from the terminal. Activate when: user mentions a Lumo task identifier (LUM-42, LUM-12, etc.), asks to load task background or context, wants to bind, check, or detach a Claude Code session''s task binding, is about to start development work on a specific task, wants to create a new task, list their tasks, view a task, comment on a task, list projects, list milestones, attach a task to a milestone, or update a task''s status/title/description/priority/assignee/milestone. Triggers on: "LUM-", "task context", "load context", "session start", "session attach", "session status", "session detach", "bind session", "unbind session", "which task", "what task am I on", "work on LUM", "create task", "new task", "add task", "file a task", "log a task", "list tasks", "my tasks", "show task", "view task", "comment on task", "leave a comment", "list projects", "what projects", "update task", "change task status", "rename task", "reassign task", "mark task as done", "milestone", "里程碑", "list milestones", "set milestone", "挂到 milestone", "attach milestone", "unbind milestone", "create milestone", "new milestone", "update milestone", "change milestone status", "delete milestone", "show milestone", "view milestone", "tasks in milestone", "milestone tasks", "新建里程碑", "更新里程碑", "删除里程碑", "查看里程碑", "auth login", "log in", "login", "auth logout", "log out", "logout", "sign out", "switch account", "switch identity", "whoami", "who am I", "current identity", "current user", "current workspace", "登录", "登出", "切换账号", "当前身份", "create doc", "new doc", "new document", "write doc", "写文档", "新建文档", "update doc", "edit doc", "修改文档", "更新文档", "list docs", "my docs", "我的文档", "show doc", "view doc", "查看文档", "delete doc", "删除文档", "bind doc", "attach doc to task", "把文档关联到任务", "文档绑定到任务", "unbind doc", "解绑文档", "personal doc", "workspace doc", "个人文档", "workspace 文档", "doc scope", "tag", "add tag", "remove tag", "标签", "添加标签", "移除标签", "doc tag", "task tag", "share doc", "doc share", "share document", "分享文档", "文档分享", "unshare doc", "remove share", "取消分享", "share list", "list doc shares", "who has access", "viewer", "editor", "manager", "shared with", "doc tree", "doc move", "move doc", "reparent doc", "文档树", "移动文档", "sprint", "start sprint", "close sprint", "add to sprint", "active sprints", "冲刺", "迭代", "开始冲刺", "关闭冲刺", "create sprint", "new sprint", "list sprints", "show sprint", "update sprint", "delete sprint", "sprint summary", "sprint retro", "把任务挂到冲刺", "冲刺里有什么", "lumo update", "update cli", "upgrade lumo", "update lumo", "upgrade cli", "升级 lumo", "更新 cli", "new lumo version", "是否有新版本", "lumo setup", "install lumo skill", "install lumo hooks", "wire up lumo", "set up lumo", "onboard lumo", "npx @lumoai/cli", "安装 lumo", "配置 lumo", "lumo 初始化".'
3
+ description: 'Use the Lumo CLI to load task context, manage session bindings, inspect projects and milestones, and create/update/list/show/comment on tasks from the terminal. Activate when: user mentions a Lumo task identifier (LUM-42, LUM-12, etc.), asks to load task background or context, wants to bind, check, or detach a Claude Code session''s task binding, is about to start development work on a specific task, wants to create a new task, list their tasks, view a task, comment on a task, list projects, list milestones, attach a task to a milestone, or update a task''s status/title/description/priority/assignee/milestone. Triggers on: "LUM-", "task context", "load context", "session start", "session attach", "session status", "session detach", "bind session", "unbind session", "which task", "what task am I on", "work on LUM", "create task", "new task", "add task", "file a task", "log a task", "list tasks", "my tasks", "show task", "view task", "comment on task", "leave a comment", "list projects", "what projects", "update task", "change task status", "rename task", "reassign task", "mark task as done", "milestone", "里程碑", "list milestones", "set milestone", "挂到 milestone", "attach milestone", "unbind milestone", "create milestone", "new milestone", "update milestone", "change milestone status", "delete milestone", "show milestone", "view milestone", "tasks in milestone", "milestone tasks", "新建里程碑", "更新里程碑", "删除里程碑", "查看里程碑", "auth login", "log in", "login", "auth logout", "log out", "logout", "sign out", "switch account", "switch identity", "whoami", "who am I", "current identity", "current user", "current workspace", "登录", "登出", "切换账号", "当前身份", "create doc", "new doc", "new document", "write doc", "写文档", "新建文档", "update doc", "edit doc", "修改文档", "更新文档", "list docs", "my docs", "我的文档", "show doc", "view doc", "查看文档", "delete doc", "删除文档", "bind doc", "attach doc to task", "把文档关联到任务", "文档绑定到任务", "unbind doc", "解绑文档", "personal doc", "workspace doc", "个人文档", "workspace 文档", "doc scope", "tag", "add tag", "remove tag", "标签", "添加标签", "移除标签", "doc tag", "task tag", "share doc", "doc share", "share document", "分享文档", "文档分享", "unshare doc", "remove share", "取消分享", "share list", "list doc shares", "who has access", "viewer", "editor", "manager", "shared with", "doc tree", "doc move", "move doc", "reparent doc", "文档树", "移动文档", "sprint", "start sprint", "close sprint", "add to sprint", "active sprints", "冲刺", "迭代", "开始冲刺", "关闭冲刺", "create sprint", "new sprint", "list sprints", "show sprint", "update sprint", "delete sprint", "sprint summary", "sprint retro", "把任务挂到冲刺", "冲刺里有什么", "lumo update", "update cli", "upgrade lumo", "update lumo", "upgrade cli", "升级 lumo", "更新 cli", "new lumo version", "是否有新版本", "lumo setup", "install lumo skill", "install lumo hooks", "wire up lumo", "set up lumo", "onboard lumo", "npx @lumoai/cli", "安装 lumo", "配置 lumo", "lumo 初始化", "task artifact", "artifact add", "artifact list", "artifact show", "artifact rm", "artifact delete", "remove artifact", "delete artifact", "spec artifact", "record spec", "attach spec", "attach plan", "记录 spec", "挂 spec", "查看 artifact", "删除 artifact", figma, attach figma, figma link, 关联 figma, 设计稿, figma design.'
4
4
  ---
5
5
 
6
6
  ## Prerequisites
@@ -187,7 +187,7 @@ Pure flag-driven update. Provide at least one of:
187
187
  | `-p, --priority <lvl>` | enum | `low \| medium \| high \| urgent`. |
188
188
  | `-a, --assignee <ref>` | string | `me`, an email, or a member name. `--assignee ""` clears the field. |
189
189
  | `--milestone <ref>` | string | Milestone name (case-insensitive) within the task's project. `--milestone ""` unbinds. |
190
- | `--sprint <ref>` | string | Sprint number or UUID to bind the task to. `--sprint ""` clears the current sprint binding (idempotent when already unbound). |
190
+ | `--sprint <ref>` | string | Sprint number or UUID to bind the task to. `--sprint ""` clears the current sprint binding (idempotent when already unbound). |
191
191
  | `--tag <name>` | string (repeatable) | **Bulk replace** the tag set by name. Creates tag if missing. Max 20. Mutually exclusive with `--add-tag*` / `--remove-tag*`. |
192
192
  | `--tag-id <cuid>` | string (repeatable) | **Bulk replace** the tag set by id. Max 20. Mutually exclusive with `--add-tag*` / `--remove-tag*`. |
193
193
  | `--add-tag <name>` | string (repeatable) | Attach tag by name (find-or-create). Max 20. |
@@ -295,6 +295,114 @@ lumo task comment LUM-42 "Reproduced the redirect bug on staging — Safari only
295
295
 
296
296
  The CLI does not support @-mention chip syntax. If the user wants to ping someone, they should comment from the web UI.
297
297
 
298
+ ### Task ↔ Spec Artifacts
299
+
300
+ Record Claude Code spec-engineering products (spec / plan / design …) on a task. The artifacts show up in the task detail "规格" (definition) layer and are injected into `lumo task context`.
301
+
302
+ #### `lumo task artifact add <task> --kind <kind> --title <title> --file <path> [--source <source>]`
303
+
304
+ Attaches an artifact to a task. `--kind`, `--title`, `--source` are stored verbatim — **`kind` is opaque** (no enumeration; `spec` / `plan` / `requirements` / anything is accepted). `--file` supplies the body (file contents). `--source` defaults to `claude-code`. Each call appends to the end of the task's artifact list — call once per artifact (e.g. Superpowers: one `spec`, one `plan`). The `<task>` (e.g. `LUM-42`) is resolved server-side.
305
+
306
+ ```bash
307
+ lumo task artifact add LUM-42 --kind spec --title "Spec" --file docs/spec.md --source superpowers
308
+ lumo task artifact add LUM-42 --kind plan --title "Implementation plan" --file docs/plan.md --source superpowers
309
+ ```
310
+
311
+ Output: `Added [spec] "Spec" to LUM-42`
312
+
313
+ #### `lumo task artifact list <task>`
314
+
315
+ Lists artifacts on the task in order: `<id> <kind> <source> <title>`. Prints `No artifacts on <task>` when there are none.
316
+
317
+ ```bash
318
+ lumo task artifact list LUM-42
319
+ ```
320
+
321
+ #### `lumo task artifact show <task> <artifact-id>`
322
+
323
+ Prints one artifact's key:value header (id, kind, title, source, order, task) followed by the full content body. The `<artifact-id>` is the cuid in column 1 of `artifact list`.
324
+
325
+ ```bash
326
+ lumo task artifact show LUM-42 cma_xxx
327
+ ```
328
+
329
+ #### `lumo task artifact rm <task> <artifact-id> --yes`
330
+
331
+ Deletes an artifact from a task. Irreversible — `--yes` is required and there is no interactive prompt (agent-friendly). On success prints `Removed <artifact-id> from <task>`. A 404 (task or artifact missing in this workspace) prints the server message and exits 1.
332
+
333
+ ```bash
334
+ lumo task artifact rm LUM-42 cma_xxx --yes
335
+ ```
336
+
337
+ When to suggest: after running a spec/plan workflow in Claude Code, offer to record the product(s) with `task artifact add` (one call per artifact). Use `task artifact list` to see what's already recorded, `task artifact show` to inspect a single artifact's content, and `task artifact rm` to drop one that's wrong or stale.
338
+
339
+ ### Task ↔ Figma Designs
340
+
341
+ #### `lumo task figma add <task> <url>` — attach a Figma file/frame
342
+
343
+ Fetches file name, frame name, and thumbnail via Figma OAuth and stores them
344
+ on the task.
345
+
346
+ ```bash
347
+ lumo task figma add LUM-42 "https://www.figma.com/design/abc123/Onboarding?node-id=1-234"
348
+ ```
349
+
350
+ If the URL omits `node-id`, the link is stored as file-level; the CLI prints
351
+ a `(file-level, thumbnail from "...")` note showing the auto-picked
352
+ representative frame.
353
+
354
+ Idempotent — re-adding the same URL within 7 days returns the existing row
355
+ without re-calling Figma.
356
+
357
+ **Not connected?** Errors with:
358
+
359
+ ```
360
+ ✗ You haven't connected Figma yet.
361
+ Run: open https://www.uselumo.ai/settings/integrations
362
+ ```
363
+
364
+ #### `lumo task figma list <task>` — list attachments
365
+
366
+ ```
367
+ $ lumo task figma list LUM-42
368
+ cfl_xxx1 Onboarding Welcome screen 2026-05-28
369
+ cfl_xxx2 Design System Button / Primary 2026-05-27
370
+ cfl_xxx3 Onboarding (file-level) 2026-05-20 ⚠ thumbnail stale
371
+ ```
372
+
373
+ `⚠ thumbnail stale` appears when `thumbnailFetchedAt > 25 days`.
374
+
375
+ #### `lumo task figma rm <task> <link-id-or-url>` — remove an attachment
376
+
377
+ Accepts a `cfl_*` cuid or the original URL. Idempotent (`Not linked: ...` + exit 0 when no match).
378
+
379
+ #### `lumo task figma refresh <task>` — manual refresh
380
+
381
+ Re-fetches metadata + thumbnail for every Figma link on the task. Per-link
382
+ failures are isolated.
383
+
384
+ ```
385
+ $ lumo task figma refresh LUM-42
386
+ Refreshed 3 Figma links on LUM-42
387
+ ✓ Onboarding · Welcome screen
388
+ ✓ Onboarding · Sign-up form
389
+ ✗ Design System · Button (file not accessible)
390
+ ```
391
+
392
+ ### When to suggest the `task figma` commands
393
+
394
+ - User pastes a Figma URL or mentions a design ("here's the mock", "the
395
+ Figma is at...").
396
+ - User asks "what designs are linked to this task" or "show me the Figma
397
+ for LUM-42".
398
+ - After implementing a UI change, suggest `lumo task figma refresh <task>`
399
+ if the user mentioned updating the Figma source.
400
+
401
+ OAuth connection lives in the Web UI at
402
+ `/settings/integrations`. The CLI does **not** have a `figma auth`
403
+ command; if the user tries `task figma add` without connecting, the error
404
+ message directs them to the Web UI.
405
+
298
406
  ### `lumo project list` — list projects in the workspace
299
407
 
300
408
  Prints `<slug> <Display Name>` lines. The slug column matches the `--project <ref>` argument accepted by `task create`, so users (and you) can copy a slug straight from this output into a create command.
@@ -629,11 +737,11 @@ If user creates a doc with `--task LUM-N` and the current Claude Code session is
629
737
 
630
738
  Prints fixed-width rows: `<NUMBER> <STATUS> <start> <end> <name>`, sorted newest-first (server sort).
631
739
 
632
- | Flag | Type | Notes |
633
- | ---------------------- | ------- | -------------------------------------------------- |
740
+ | Flag | Type | Notes |
741
+ | ---------------------- | ------- | ----------------------------------------------------- |
634
742
  | `--team <ref>` | string | Team name or slug. Required in multi-team workspaces. |
635
- | `-s, --status <value>` | enum | `draft \| active \| closed`. |
636
- | `-n, --limit <count>` | integer | Cap output to the first N rows. |
743
+ | `-s, --status <value>` | enum | `draft \| active \| closed`. |
744
+ | `-n, --limit <count>` | integer | Cap output to the first N rows. |
637
745
 
638
746
  ```bash
639
747
  lumo sprint list
@@ -645,12 +753,12 @@ When to suggest: user asks "what sprints do we have", "which sprint is active",
645
753
 
646
754
  ### `lumo sprint create [flags]` — create a sprint
647
755
 
648
- | Flag | Type | Notes |
649
- | ----------------- | ------ | ------------------------------------------------------- |
650
- | `--team <ref>` | string | Team name or slug. Required in multi-team workspaces. |
651
- | `--start <date>` | string | **Required.** YYYY-MM-DD. |
652
- | `--end <date>` | string | **Required.** YYYY-MM-DD. |
653
- | `-n, --name <>` | string | Optional. Server fills a default name when omitted. |
756
+ | Flag | Type | Notes |
757
+ | ---------------- | ------ | ----------------------------------------------------- |
758
+ | `--team <ref>` | string | Team name or slug. Required in multi-team workspaces. |
759
+ | `--start <date>` | string | **Required.** YYYY-MM-DD. |
760
+ | `--end <date>` | string | **Required.** YYYY-MM-DD. |
761
+ | `-n, --name <>` | string | Optional. Server fills a default name when omitted. |
654
762
 
655
763
  ```bash
656
764
  lumo sprint create --start 2026-06-01 --end 2026-06-14
@@ -679,12 +787,12 @@ When to suggest: user asks "what's in sprint 3", "show me the current sprint", "
679
787
 
680
788
  Updates sprint metadata. At least one flag required. **No `--status` flag** — use `lumo sprint start` / `lumo sprint close` to transition status.
681
789
 
682
- | Flag | Type | Notes |
683
- | ----------------- | ------ | ------------------------------------------------------------- |
684
- | `--team <ref>` | string | Required when identifier is a number in a multi-team workspace. |
685
- | `-n, --name <>` | string | New name. Cannot be empty. |
686
- | `--start <date>` | string | YYYY-MM-DD. `--start ""` clears. |
687
- | `--end <date>` | string | YYYY-MM-DD. `--end ""` clears. |
790
+ | Flag | Type | Notes |
791
+ | ---------------- | ------ | --------------------------------------------------------------- |
792
+ | `--team <ref>` | string | Required when identifier is a number in a multi-team workspace. |
793
+ | `-n, --name <>` | string | New name. Cannot be empty. |
794
+ | `--start <date>` | string | YYYY-MM-DD. `--start ""` clears. |
795
+ | `--end <date>` | string | YYYY-MM-DD. `--end ""` clears. |
688
796
 
689
797
  ```bash
690
798
  lumo sprint update 3 --name "Sprint 4 (extended)"
@@ -717,11 +825,11 @@ When to suggest: user says "start the sprint", "开始冲刺", "kick off sprint
717
825
 
718
826
  Handles unfinished tasks based on flags. Without flags: closes only if all tasks are done; otherwise prints a list of unfinished tasks and refuses.
719
827
 
720
- | Flag | Type | Notes |
721
- | -------------- | ------- | ------------------------------------------------------------------------- |
722
- | `--move-all` | boolean | Move all unfinished tasks to the next sprint. Requires `--yes`. |
723
- | `--backlog-all`| boolean | Remove all unfinished tasks from the sprint (send to backlog). Requires `--yes`. |
724
- | `--yes` | boolean | Required when `--move-all` or `--backlog-all` is given. |
828
+ | Flag | Type | Notes |
829
+ | --------------- | ------- | -------------------------------------------------------------------------------- |
830
+ | `--move-all` | boolean | Move all unfinished tasks to the next sprint. Requires `--yes`. |
831
+ | `--backlog-all` | boolean | Remove all unfinished tasks from the sprint (send to backlog). Requires `--yes`. |
832
+ | `--yes` | boolean | Required when `--move-all` or `--backlog-all` is given. |
725
833
 
726
834
  ```bash
727
835
  lumo sprint close 3 # fails if unfinished tasks exist
@@ -735,9 +843,9 @@ When to suggest: user says "close the sprint", "关闭冲刺", "end sprint 3", "
735
843
 
736
844
  Prints the AI-generated retrospective summary for the sprint. A 404 response means no summary has been generated yet ("no summary yet").
737
845
 
738
- | Flag | Type | Notes |
739
- | --------- | ------- | ---------------------------------------------------- |
740
- | `--retry` | boolean | Queue a regeneration (async, server returns 202). |
846
+ | Flag | Type | Notes |
847
+ | --------- | ------- | ------------------------------------------------- |
848
+ | `--retry` | boolean | Queue a regeneration (async, server returns 202). |
741
849
 
742
850
  ```bash
743
851
  lumo sprint summary 3
@@ -148,7 +148,9 @@ function isLumoOnPath() {
148
148
  try {
149
149
  // `command -v` is a POSIX shell builtin (`execSync` defaults to /bin/sh
150
150
  // on Unix). `where` is a cmd.exe builtin on Windows.
151
- (0, child_process_1.execSync)(process.platform === 'win32' ? 'where lumo' : 'command -v lumo', { stdio: 'pipe' });
151
+ (0, child_process_1.execSync)(process.platform === 'win32' ? 'where lumo' : 'command -v lumo', {
152
+ stdio: 'pipe',
153
+ });
152
154
  return true;
153
155
  }
154
156
  catch {
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskArtifactAdd = taskArtifactAdd;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const doc_input_1 = require("../lib/doc-input");
7
+ async function taskArtifactAdd(identifier, options) {
8
+ if (!identifier) {
9
+ console.error('Error: missing <task>. Usage: lumo task artifact add <LUM-42> --kind spec --title "Spec" --file spec.md');
10
+ return 1;
11
+ }
12
+ const kind = options.kind?.trim();
13
+ const title = options.title?.trim();
14
+ if (!kind) {
15
+ console.error('Error: --kind is required and cannot be empty.');
16
+ return 1;
17
+ }
18
+ if (!title) {
19
+ console.error('Error: --title is required and cannot be empty.');
20
+ return 1;
21
+ }
22
+ if (!options.file) {
23
+ console.error('Error: --file <path> is required.');
24
+ return 1;
25
+ }
26
+ let content;
27
+ try {
28
+ content = await (0, doc_input_1.readFileUtf8)(options.file);
29
+ }
30
+ catch {
31
+ console.error(`Error: could not read file ${options.file}`);
32
+ return 1;
33
+ }
34
+ if (content.trim().length === 0) {
35
+ console.error(`Error: file ${options.file} is empty.`);
36
+ return 1;
37
+ }
38
+ const creds = (0, config_1.readCredentials)();
39
+ if (!creds) {
40
+ console.error('Error: not logged in. Run `lumo auth login` first.');
41
+ return 1;
42
+ }
43
+ const envUrl = process.env.LUMO_API_URL?.trim();
44
+ const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
45
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
46
+ const payload = { kind, title, content };
47
+ if (options.source?.trim())
48
+ payload.source = options.source.trim();
49
+ let res;
50
+ try {
51
+ res = await fetch(`${base}/api/tasks/${encodeURIComponent(identifier)}/artifacts`, {
52
+ method: 'POST',
53
+ headers: {
54
+ Authorization: `Bearer ${creds.token}`,
55
+ 'Content-Type': 'application/json',
56
+ },
57
+ body: JSON.stringify(payload),
58
+ });
59
+ }
60
+ catch (err) {
61
+ const msg = err instanceof Error ? err.message : String(err);
62
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
63
+ return 1;
64
+ }
65
+ if (res.status === 401) {
66
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
67
+ return 1;
68
+ }
69
+ if (res.status === 404) {
70
+ console.error(`Error: task ${identifier} not found in workspace ${creds.workspaceSlug}`);
71
+ return 1;
72
+ }
73
+ if (res.status !== 201) {
74
+ let serverMsg = null;
75
+ try {
76
+ const errBody = (await res.json());
77
+ if (typeof errBody.error === 'string')
78
+ serverMsg = errBody.error;
79
+ }
80
+ catch {
81
+ /* not JSON */
82
+ }
83
+ console.error(serverMsg
84
+ ? `Error: ${serverMsg}`
85
+ : `Error: artifact add failed (HTTP ${res.status})`);
86
+ return 1;
87
+ }
88
+ const data = (await res.json());
89
+ process.stdout.write(`Added [${data.artifact.kind}] "${data.artifact.title}" to ${identifier}\n`);
90
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskArtifactList = taskArtifactList;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ async function taskArtifactList(identifier) {
7
+ if (!identifier) {
8
+ console.error('Error: missing <task>. Usage: lumo task artifact list <LUM-42>');
9
+ return 1;
10
+ }
11
+ const creds = (0, config_1.readCredentials)();
12
+ if (!creds) {
13
+ console.error('Error: not logged in. Run `lumo auth login` first.');
14
+ return 1;
15
+ }
16
+ const envUrl = process.env.LUMO_API_URL?.trim();
17
+ const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
18
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
19
+ let res;
20
+ try {
21
+ res = await fetch(`${base}/api/tasks/${encodeURIComponent(identifier)}/artifacts`, { headers: { Authorization: `Bearer ${creds.token}` } });
22
+ }
23
+ catch (err) {
24
+ const msg = err instanceof Error ? err.message : String(err);
25
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
26
+ return 1;
27
+ }
28
+ if (res.status === 401) {
29
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
30
+ return 1;
31
+ }
32
+ if (res.status === 404) {
33
+ console.error(`Error: task ${identifier} not found in workspace ${creds.workspaceSlug}`);
34
+ return 1;
35
+ }
36
+ if (!res.ok) {
37
+ console.error(`Error: artifact list failed (HTTP ${res.status})`);
38
+ return 1;
39
+ }
40
+ const { artifacts } = (await res.json());
41
+ if (artifacts.length === 0) {
42
+ process.stdout.write(`No artifacts on ${identifier}\n`);
43
+ return;
44
+ }
45
+ for (const a of artifacts) {
46
+ process.stdout.write(`${a.id} ${a.kind.padEnd(10)} ${a.source.padEnd(12)} ${a.title}\n`);
47
+ }
48
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskArtifactRm = taskArtifactRm;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ async function taskArtifactRm(identifier, artifactId, options) {
7
+ if (!identifier) {
8
+ console.error('Error: missing <task>. Usage: lumo task artifact rm <LUM-42> <artifact-id> --yes');
9
+ return 1;
10
+ }
11
+ if (!artifactId) {
12
+ console.error('Error: missing <artifact-id>. Usage: lumo task artifact rm <LUM-42> <artifact-id> --yes');
13
+ return 1;
14
+ }
15
+ if (!options.yes) {
16
+ console.error('Error: --yes is required (artifact deletion is irreversible).');
17
+ return 1;
18
+ }
19
+ const creds = (0, config_1.readCredentials)();
20
+ if (!creds) {
21
+ console.error('Error: not logged in. Run `lumo auth login` first.');
22
+ return 1;
23
+ }
24
+ const envUrl = process.env.LUMO_API_URL?.trim();
25
+ const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
26
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
27
+ let res;
28
+ try {
29
+ res = await fetch(`${base}/api/tasks/${encodeURIComponent(identifier)}/artifacts/${encodeURIComponent(artifactId)}`, {
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
+ let serverMsg = `task ${identifier} or artifact ${artifactId} not found in workspace ${creds.workspaceSlug}`;
45
+ try {
46
+ const errBody = (await res.json());
47
+ if (typeof errBody.error === 'string')
48
+ serverMsg = errBody.error;
49
+ }
50
+ catch {
51
+ /* not JSON */
52
+ }
53
+ console.error(`Error: ${serverMsg}`);
54
+ return 1;
55
+ }
56
+ if (res.status !== 204) {
57
+ let serverMsg = null;
58
+ try {
59
+ const errBody = (await res.json());
60
+ if (typeof errBody.error === 'string')
61
+ serverMsg = errBody.error;
62
+ }
63
+ catch {
64
+ /* not JSON */
65
+ }
66
+ console.error(serverMsg
67
+ ? `Error: ${serverMsg}`
68
+ : `Error: artifact rm failed (HTTP ${res.status})`);
69
+ return 1;
70
+ }
71
+ process.stdout.write(`Removed ${artifactId} from ${identifier}\n`);
72
+ }
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskArtifactShow = taskArtifactShow;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ async function taskArtifactShow(identifier, artifactId) {
7
+ if (!identifier) {
8
+ console.error('Error: missing <task>. Usage: lumo task artifact show <LUM-42> <artifact-id>');
9
+ return 1;
10
+ }
11
+ if (!artifactId) {
12
+ console.error('Error: missing <artifact-id>. Usage: lumo task artifact show <LUM-42> <artifact-id>');
13
+ return 1;
14
+ }
15
+ const creds = (0, config_1.readCredentials)();
16
+ if (!creds) {
17
+ console.error('Error: not logged in. Run `lumo auth login` first.');
18
+ return 1;
19
+ }
20
+ const envUrl = process.env.LUMO_API_URL?.trim();
21
+ const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
22
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
23
+ let res;
24
+ try {
25
+ res = await fetch(`${base}/api/tasks/${encodeURIComponent(identifier)}/artifacts/${encodeURIComponent(artifactId)}`, { headers: { Authorization: `Bearer ${creds.token}` } });
26
+ }
27
+ catch (err) {
28
+ const msg = err instanceof Error ? err.message : String(err);
29
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
30
+ return 1;
31
+ }
32
+ if (res.status === 401) {
33
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
34
+ return 1;
35
+ }
36
+ if (res.status === 404) {
37
+ let serverMsg = `task ${identifier} or artifact ${artifactId} not found in workspace ${creds.workspaceSlug}`;
38
+ try {
39
+ const errBody = (await res.json());
40
+ if (typeof errBody.error === 'string')
41
+ serverMsg = errBody.error;
42
+ }
43
+ catch {
44
+ /* not JSON */
45
+ }
46
+ console.error(`Error: ${serverMsg}`);
47
+ return 1;
48
+ }
49
+ if (!res.ok) {
50
+ console.error(`Error: artifact show failed (HTTP ${res.status})`);
51
+ return 1;
52
+ }
53
+ const { artifact } = (await res.json());
54
+ process.stdout.write(`id: ${artifact.id}\n`);
55
+ process.stdout.write(`kind: ${artifact.kind}\n`);
56
+ process.stdout.write(`title: ${artifact.title}\n`);
57
+ process.stdout.write(`source: ${artifact.source}\n`);
58
+ process.stdout.write(`order: ${artifact.order}\n`);
59
+ process.stdout.write(`task: ${identifier}\n`);
60
+ process.stdout.write(`\n${artifact.content}\n`);
61
+ }
@@ -60,8 +60,9 @@ function formatTaskContextMarkdown(data, now) {
60
60
  : '';
61
61
  lines.push(`**Milestone**: ${data.task.milestone.name} (${data.task.milestone.status}${target})`);
62
62
  }
63
- if (data.task.description && data.task.description.trim().length > 0) {
64
- lines.push(`**Description**: ${data.task.description}`);
63
+ const body = data.task.descriptionMarkdown ?? data.task.description;
64
+ if (body && body.trim().length > 0) {
65
+ lines.push(`**Description**: ${body}`);
65
66
  }
66
67
  lines.push('');
67
68
  // Frontload memory before sessions: it's cold context the agent should see
@@ -78,6 +79,14 @@ function formatTaskContextMarkdown(data, now) {
78
79
  lines.push(data.webLinkSection.trimEnd());
79
80
  lines.push('');
80
81
  }
82
+ if (data.figmaSection && data.figmaSection.trim().length > 0) {
83
+ lines.push(data.figmaSection.trimEnd());
84
+ lines.push('');
85
+ }
86
+ if (data.artifactSection && data.artifactSection.trim().length > 0) {
87
+ lines.push(data.artifactSection.trimEnd());
88
+ lines.push('');
89
+ }
81
90
  if (data.sessions.length === 0) {
82
91
  lines.push('## Previous Sessions (0)');
83
92
  lines.push('');
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskFigmaAdd = taskFigmaAdd;
4
+ const figma_api_1 = require("../lib/figma-api");
5
+ async function taskFigmaAdd(args) {
6
+ try {
7
+ const { link } = await (0, figma_api_1.addFigmaLink)(args.identifier, args.url);
8
+ const label = link.frameName
9
+ ? `${link.fileName} · ${link.frameName}`
10
+ : `${link.fileName}${link.nodeId === '' ? ' (file-level)' : ''}`;
11
+ console.log(`Linked ${args.identifier} ↔ Figma "${label}"`);
12
+ console.log(` ${link.url}`);
13
+ return 0;
14
+ }
15
+ catch (e) {
16
+ const err = e;
17
+ if (err.code === 'figma_not_connected') {
18
+ console.error(`✗ You haven't connected Figma yet.`);
19
+ console.error(` Run: open https://www.uselumo.ai/settings/integrations`);
20
+ return 1;
21
+ }
22
+ if (err.code === 'figma_needs_reauth') {
23
+ console.error(`✗ Your Figma token is no longer valid. Please reconnect.`);
24
+ console.error(` Run: open https://www.uselumo.ai/settings/integrations`);
25
+ return 1;
26
+ }
27
+ throw e; // let wrap() in cli/src/index.ts log and exit 1
28
+ }
29
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskFigmaList = taskFigmaList;
4
+ const figma_api_1 = require("../lib/figma-api");
5
+ const STALE_MS = 25 * 24 * 3600 * 1000;
6
+ function formatRow(id, fileName, frame, synced, stale) {
7
+ return [
8
+ id.padEnd(10),
9
+ fileName.padEnd(20),
10
+ frame.padEnd(24),
11
+ synced.padEnd(12),
12
+ stale ? '⚠ thumbnail stale' : '',
13
+ ]
14
+ .join(' ')
15
+ .trimEnd();
16
+ }
17
+ async function taskFigmaList(args) {
18
+ const { links } = await (0, figma_api_1.listFigmaLinks)(args.identifier);
19
+ if (links.length === 0) {
20
+ console.log('No Figma links attached.');
21
+ return 0;
22
+ }
23
+ for (const link of links) {
24
+ const frame = link.frameName ?? '(file-level)';
25
+ const synced = link.lastSyncedAt.slice(0, 10);
26
+ const stale = link.thumbnailFetchedAt === null ||
27
+ Date.now() - new Date(link.thumbnailFetchedAt).getTime() > STALE_MS;
28
+ console.log(formatRow(link.id, link.fileName, frame, synced, stale && !!link.thumbnailUrl));
29
+ }
30
+ return 0;
31
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskFigmaRefresh = taskFigmaRefresh;
4
+ const figma_api_1 = require("../lib/figma-api");
5
+ async function taskFigmaRefresh(args) {
6
+ try {
7
+ const { links } = await (0, figma_api_1.listFigmaLinks)(args.identifier);
8
+ const byId = new Map(links.map(l => [l.id, l]));
9
+ const { results } = await (0, figma_api_1.refreshFigmaLinks)(args.identifier);
10
+ if (results.length === 0) {
11
+ console.log(`No Figma links attached to ${args.identifier}.`);
12
+ return 0;
13
+ }
14
+ console.log(`Refreshed ${results.length} Figma links on ${args.identifier}`);
15
+ for (const r of results) {
16
+ const link = byId.get(r.id);
17
+ const label = link
18
+ ? link.frameName
19
+ ? `${link.fileName} · ${link.frameName}`
20
+ : `${link.fileName} (file-level)`
21
+ : r.id;
22
+ if (r.ok)
23
+ console.log(` ✓ ${label}`);
24
+ else
25
+ console.log(` ✗ ${label} (${r.error})`);
26
+ }
27
+ return 0;
28
+ }
29
+ catch (e) {
30
+ const err = e;
31
+ if (err.code === 'figma_not_connected' ||
32
+ err.code === 'figma_needs_reauth') {
33
+ console.error(`✗ Figma is not connected (or token expired). Reconnect:`);
34
+ console.error(` open https://www.uselumo.ai/settings/integrations`);
35
+ return 1;
36
+ }
37
+ throw e;
38
+ }
39
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskFigmaRm = taskFigmaRm;
4
+ const figma_api_1 = require("../lib/figma-api");
5
+ async function taskFigmaRm(args) {
6
+ const { links } = await (0, figma_api_1.listFigmaLinks)(args.identifier);
7
+ const match = links.find(l => l.id === args.linkIdOrUrl || l.url === args.linkIdOrUrl);
8
+ if (!match) {
9
+ console.log(`Not linked: ${args.linkIdOrUrl}`);
10
+ return 0;
11
+ }
12
+ await (0, figma_api_1.removeFigmaLink)(args.identifier, match.id);
13
+ const label = match.frameName
14
+ ? `${match.fileName} · ${match.frameName}`
15
+ : `${match.fileName} (file-level)`;
16
+ console.log(`Removed Figma link from ${args.identifier}: "${label}"`);
17
+ return 0;
18
+ }
@@ -24,10 +24,11 @@ function formatTaskShow(task) {
24
24
  lines.push('Assignee: (unassigned)');
25
25
  }
26
26
  lines.push(`URL: ${task.url}`);
27
- if (task.description && task.description.trim().length > 0) {
27
+ const body = task.descriptionMarkdown ?? task.description;
28
+ if (body && body.trim().length > 0) {
28
29
  lines.push('');
29
30
  lines.push('Description:');
30
- for (const line of task.description.split('\n')) {
31
+ for (const line of body.split('\n')) {
31
32
  lines.push(` ${line}`);
32
33
  }
33
34
  }
@@ -51,6 +51,14 @@ const task_update_1 = require("./commands/task-update");
51
51
  const task_list_1 = require("./commands/task-list");
52
52
  const task_show_1 = require("./commands/task-show");
53
53
  const task_comment_1 = require("./commands/task-comment");
54
+ const task_figma_add_1 = require("./commands/task-figma-add");
55
+ const task_figma_list_1 = require("./commands/task-figma-list");
56
+ const task_figma_rm_1 = require("./commands/task-figma-rm");
57
+ const task_figma_refresh_1 = require("./commands/task-figma-refresh");
58
+ const task_artifact_add_1 = require("./commands/task-artifact-add");
59
+ const task_artifact_list_1 = require("./commands/task-artifact-list");
60
+ const task_artifact_show_1 = require("./commands/task-artifact-show");
61
+ const task_artifact_rm_1 = require("./commands/task-artifact-rm");
54
62
  const project_list_1 = require("./commands/project-list");
55
63
  const milestone_list_1 = require("./commands/milestone-list");
56
64
  const milestone_create_1 = require("./commands/milestone-create");
@@ -206,6 +214,52 @@ task
206
214
  .option('--tag-id <cuid>', 'Attach tag by id (repeatable)', collect, [])
207
215
  .option('--sprint <ref>', 'Sprint number or UUID to add the task to after creation')
208
216
  .action(wrap((title, options) => (0, task_create_1.taskCreate)(title, options)));
217
+ const taskFigma = task
218
+ .command('figma')
219
+ .description('Attach Figma file/frame links to a task');
220
+ taskFigma
221
+ .command('add <task> <url>')
222
+ .description('Attach a Figma file or frame URL to the given task. Fetches file/frame name + thumbnail via Figma OAuth.')
223
+ .action(wrap((taskId, url) => (0, task_figma_add_1.taskFigmaAdd)({ identifier: taskId, url: url })));
224
+ taskFigma
225
+ .command('list <task>')
226
+ .description('List Figma links attached to a task')
227
+ .action(wrap((taskId) => (0, task_figma_list_1.taskFigmaList)({ identifier: taskId })));
228
+ taskFigma
229
+ .command('rm <task> <link-id-or-url>')
230
+ .description('Remove a Figma link from a task (idempotent)')
231
+ .action(wrap((taskId, linkIdOrUrl) => (0, task_figma_rm_1.taskFigmaRm)({
232
+ identifier: taskId,
233
+ linkIdOrUrl: linkIdOrUrl,
234
+ })));
235
+ taskFigma
236
+ .command('refresh <task>')
237
+ .description('Re-fetch Figma metadata + thumbnail for every link on this task. Per-link failures isolated.')
238
+ .action(wrap((taskId) => (0, task_figma_refresh_1.taskFigmaRefresh)({ identifier: taskId })));
239
+ const taskArtifact = task
240
+ .command('artifact')
241
+ .description('Record spec-engineering artifacts (spec / plan / design …) on a task');
242
+ taskArtifact
243
+ .command('add <task>')
244
+ .description('Attach an artifact to a task. --kind/--title/--source are stored verbatim; --file supplies the content.')
245
+ .requiredOption('--kind <kind>', 'Artifact kind, e.g. spec | plan | design (opaque)')
246
+ .requiredOption('--title <title>', 'Artifact title')
247
+ .requiredOption('--file <path>', 'File whose contents become the artifact body')
248
+ .option('--source <source>', 'Producer label (default: claude-code)')
249
+ .action(wrap((taskId, options) => (0, task_artifact_add_1.taskArtifactAdd)(taskId, options)));
250
+ taskArtifact
251
+ .command('list <task>')
252
+ .description('List artifacts attached to a task, in order')
253
+ .action(wrap((taskId) => (0, task_artifact_list_1.taskArtifactList)(taskId)));
254
+ taskArtifact
255
+ .command('show <task> <artifact-id>')
256
+ .description('Show one artifact (key:value header + content body). Find <artifact-id> in `artifact list` column 1.')
257
+ .action(wrap((taskId, artifactId) => (0, task_artifact_show_1.taskArtifactShow)(taskId, artifactId)));
258
+ taskArtifact
259
+ .command('rm <task> <artifact-id>')
260
+ .description('Delete an artifact from a task. Irreversible — requires --yes.')
261
+ .option('--yes', 'Confirm deletion (required, no interactive prompt)')
262
+ .action(wrap((taskId, artifactId, options) => (0, task_artifact_rm_1.taskArtifactRm)(taskId, artifactId, options)));
209
263
  const projectCmd = program
210
264
  .command('project')
211
265
  .description('Inspect projects from the terminal');
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.addFigmaLink = addFigmaLink;
4
+ exports.listFigmaLinks = listFigmaLinks;
5
+ exports.removeFigmaLink = removeFigmaLink;
6
+ exports.refreshFigmaLinks = refreshFigmaLinks;
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.resolveApiUrl)();
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
+ async function addFigmaLink(identifier, url) {
43
+ return call(`/api/tasks/${encodeURIComponent(identifier)}/figma`, {
44
+ method: 'POST',
45
+ body: JSON.stringify({ url }),
46
+ });
47
+ }
48
+ async function listFigmaLinks(identifier) {
49
+ return call(`/api/tasks/${encodeURIComponent(identifier)}/figma`, {
50
+ method: 'GET',
51
+ });
52
+ }
53
+ async function removeFigmaLink(identifier, linkId) {
54
+ return call(`/api/tasks/${encodeURIComponent(identifier)}/figma/${encodeURIComponent(linkId)}`, { method: 'DELETE' });
55
+ }
56
+ async function refreshFigmaLinks(identifier) {
57
+ return call(`/api/tasks/${encodeURIComponent(identifier)}/figma/refresh`, {
58
+ method: 'POST',
59
+ });
60
+ }
@@ -49,7 +49,9 @@ function buildLumoHookFragment() {
49
49
  return { hooks };
50
50
  }
51
51
  function mergeLumoHooks(existing) {
52
- const base = existing ? JSON.parse(JSON.stringify(existing)) : {};
52
+ const base = existing
53
+ ? JSON.parse(JSON.stringify(existing))
54
+ : {};
53
55
  if (!base.hooks)
54
56
  base.hooks = {};
55
57
  const stats = { addedEvents: [], alreadyPresent: [] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumoai/cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",
@@ -40,9 +40,11 @@
40
40
  "prepublishOnly": "npm run clean && npm run build"
41
41
  },
42
42
  "dependencies": {
43
- "commander": "^13.1.0"
43
+ "commander": "^13.1.0",
44
+ "markdown-it": "^14.1.1"
44
45
  },
45
46
  "devDependencies": {
47
+ "@types/markdown-it": "^14.1.2",
46
48
  "@types/node": "^25",
47
49
  "typescript": "^5"
48
50
  }