@lumoai/cli 1.2.1 → 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/assets/skill.md +109 -1
- package/dist/cli/src/commands/task-artifact-add.js +90 -0
- package/dist/cli/src/commands/task-artifact-list.js +48 -0
- package/dist/cli/src/commands/task-artifact-rm.js +72 -0
- package/dist/cli/src/commands/task-artifact-show.js +61 -0
- package/dist/cli/src/commands/task-context.js +11 -2
- package/dist/cli/src/commands/task-figma-add.js +29 -0
- package/dist/cli/src/commands/task-figma-list.js +31 -0
- package/dist/cli/src/commands/task-figma-refresh.js +39 -0
- package/dist/cli/src/commands/task-figma-rm.js +18 -0
- package/dist/cli/src/commands/task-show.js +3 -2
- package/dist/cli/src/index.js +54 -0
- package/dist/cli/src/lib/figma-api.js +60 -0
- package/package.json +1 -1
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
|
|
@@ -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.
|
|
@@ -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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
|
31
|
+
for (const line of body.split('\n')) {
|
|
31
32
|
lines.push(` ${line}`);
|
|
32
33
|
}
|
|
33
34
|
}
|
package/dist/cli/src/index.js
CHANGED
|
@@ -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
|
+
}
|