@lumoai/cli 1.3.0 → 1.5.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 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 初始化", "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.'
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", "artifact update", "update artifact", "edit artifact", "change artifact kind", "change artifact source", "remove artifact", "delete artifact", "spec artifact", "record spec", "attach spec", "attach plan", "记录 spec", "挂 spec", "查看 artifact", "编辑 artifact", "修改 artifact", "删除 artifact", figma, attach figma, figma link, 关联 figma, 设计稿, figma design, "memory", "记忆", "remember", "record a memory", "记一条", "promote memory", "promote to project", "沉淀", "task memory", "project memory", "lumo memory".'
4
4
  ---
5
5
 
6
6
  ## Prerequisites
@@ -299,20 +299,35 @@ The CLI does not support @-mention chip syntax. If the user wants to ping someon
299
299
 
300
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
301
 
302
- #### `lumo task artifact add <task> --kind <kind> --title <title> --file <path> [--source <source>]`
302
+ #### `lumo task artifact add <task> --kind <kind> --title <title> --file <path> --source <source> --agent <agent>`
303
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.
304
+ Attaches an artifact to a task. `--kind`, `--title`, `--source` are stored verbatim — **`kind` is opaque** (no enumeration; `spec` / `plan` / `requirements` / anything is accepted). `--source` is **required** and names the spec-generation framework that produced the artifact, written as its **formal name** (`Superpowers` / `Spec Kit` / `BMad` / `OpenSpec` / `GSD` / …) — it is also opaque (no enumeration), but there is **no default**, so you must pass it. Quote names that contain spaces (`--source "Spec Kit"`). `--file` supplies the body (file contents). 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
+ `--agent` is **required** and names the coding tool that produced the artifact (enum). Valid values: `claude-code | codex | cursor | gemini-cli | github-copilot | windsurf` (case-insensitive; `gemini` and `copilot` are accepted aliases).
305
307
 
306
308
  ```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
+ lumo task artifact add LUM-42 --kind spec --title "Spec" --file docs/spec.md --source Superpowers --agent claude-code
310
+ lumo task artifact add LUM-42 --kind plan --title "Implementation plan" --file docs/plan.md --source "Spec Kit" --agent claude-code
309
311
  ```
310
312
 
311
313
  Output: `Added [spec] "Spec" to LUM-42`
312
314
 
315
+ #### `lumo task artifact update <task> <artifact-id> [--kind <kind>] [--title <title>] [--source <source>] [--agent <agent>]`
316
+
317
+ Patches an existing artifact's metadata in place. Editable fields are **`kind`, `title`, `source`, and `agent`** — **content is immutable** (to change the body, `rm` the artifact and `add` it again). At least one flag is required; passing none errors before any network call. Empty values (`--kind ""`) are rejected. The `<artifact-id>` is the cuid in column 1 of `artifact list`. `--agent` accepts the same valid values and aliases as `artifact add`: `claude-code | codex | cursor | gemini-cli | github-copilot | windsurf` (case-insensitive; `gemini` and `copilot` are accepted aliases).
318
+
319
+ ```bash
320
+ lumo task artifact update LUM-42 cma_xxx --kind plan # re-classify spec → plan
321
+ lumo task artifact update LUM-42 cma_xxx --source "Spec Kit" # fix the framework label
322
+ lumo task artifact update LUM-42 cma_xxx --title "Final spec" --source OpenSpec
323
+ lumo task artifact update LUM-42 cma_xxx --agent cursor # fix the agent label
324
+ ```
325
+
326
+ Output: `Updated [plan] "Final spec" (OpenSpec) on LUM-42`. A 404 (task or artifact missing in this workspace) prints the server message and exits 1.
327
+
313
328
  #### `lumo task artifact list <task>`
314
329
 
315
- Lists artifacts on the task in order: `<id> <kind> <source> <title>`. Prints `No artifacts on <task>` when there are none.
330
+ Lists artifacts on the task in order: `<id> <kind> <agent> <source> <title>`. Prints `No artifacts on <task>` when there are none.
316
331
 
317
332
  ```bash
318
333
  lumo task artifact list LUM-42
@@ -320,7 +335,7 @@ lumo task artifact list LUM-42
320
335
 
321
336
  #### `lumo task artifact show <task> <artifact-id>`
322
337
 
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`.
338
+ Prints one artifact's key:value header (id, kind, title, agent, source, order, task) followed by the full content body. The `<artifact-id>` is the cuid in column 1 of `artifact list`.
324
339
 
325
340
  ```bash
326
341
  lumo task artifact show LUM-42 cma_xxx
@@ -334,7 +349,7 @@ Deletes an artifact from a task. Irreversible — `--yes` is required and there
334
349
  lumo task artifact rm LUM-42 cma_xxx --yes
335
350
  ```
336
351
 
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.
352
+ 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) — always pass `--source` with the framework you used. Use `task artifact list` to see what's already recorded, `task artifact show` to inspect a single artifact's content, `task artifact update` to fix a wrong kind/title/source without re-uploading, and `task artifact rm` to drop one that's wrong or stale.
338
353
 
339
354
  ### Task ↔ Figma Designs
340
355
 
@@ -874,6 +889,70 @@ lumo sprint remove 3 LUM-48
874
889
 
875
890
  When to suggest: user says "remove LUM-48 from the sprint", "take this task out of sprint 3", "move task to backlog".
876
891
 
892
+ ## Memory management
893
+
894
+ Record and curate the long-term Memory that Claude reads on future sessions.
895
+ Memories are scoped **TASK** (useful only for one task) or **PROJECT** (useful
896
+ across the whole project). Automated extraction (layer1) and promotion (layer2,
897
+ on task→done) already run; these commands are the manual override.
898
+
899
+ ### Commands
900
+
901
+ ```bash
902
+ # List
903
+ lumo task memory list [LUM-N] [--category trap|decision|convention|procedural] [-n N]
904
+ lumo project memory list [<project>] [--category ...] [-n N]
905
+
906
+ # Add (per-category fields; <task>/<project> default to the session-bound task)
907
+ lumo task memory add [LUM-N] --category trap --trigger "..." --outcome "..." [--workaround "..."]
908
+ lumo task memory add [LUM-N] --category decision --what "..." --why "..." [--alternatives "..."] [--implications "..."]
909
+ lumo task memory add [LUM-N] --category convention --rule "..." --applies "..."
910
+ lumo task memory add [LUM-N] --category procedural --workflow "..." --trigger "..." [--step "..." --step "..."]
911
+ lumo project memory add [<project>] --category ... # same flags; records at PROJECT scope
912
+
913
+ # Single-memory ops (memoryId from `... memory list` column 1)
914
+ lumo memory promote <memoryId> # TASK → PROJECT
915
+ lumo memory rm <memoryId> --yes # hard delete
916
+ ```
917
+
918
+ When the session is bound (`lumo session attach <LUM-N>`), omit the identifier:
919
+ `lumo task memory add --category trap --trigger ... --outcome ...` records onto
920
+ the bound task; `lumo project memory add ...` records onto its project.
921
+
922
+ ### When to record a memory (worthiness)
923
+
924
+ Record only knowledge that is **invisible in the codebase** — the _why_ behind a
925
+ choice, a gotcha that only surfaces at runtime, a rule that lives in people's
926
+ heads, a non-obvious failure cause, a non-trivial workflow. **Skip** routine work
927
+ (reading files, plain edits, normal git, successful builds) and anything a
928
+ developer could learn from the source, git log, or docs. When unsure, don't.
929
+
930
+ ### Which category
931
+
932
+ - `trap` — a pitfall. Describe the PROBLEM ONLY (`--trigger`, `--outcome`); put any fix in a separate `procedural`.
933
+ - `decision` — an engineering decision (`--what` + `--why`, optional `--alternatives`/`--implications`).
934
+ - `convention` — a team rule (`--rule` + `--applies` = where it applies).
935
+ - `procedural` — a reusable workflow (`--workflow` + `--trigger` + `--step`…).
936
+
937
+ ### TASK vs PROJECT (at add time)
938
+
939
+ Default to **TASK** (`lumo task memory add`). Record directly to **PROJECT**
940
+ (`lumo project memory add`) only when it helps _any_ task in the project: a
941
+ toolchain/environment trap, a team-wide convention, a cross-task decision. When
942
+ unsure → TASK.
943
+
944
+ ### When to promote (TASK → PROJECT)
945
+
946
+ `lumo memory promote <id>` only when the lesson **recurs across 2+ different
947
+ tasks**, would help a _different_ task, and no equivalent PROJECT memory exists.
948
+ A wrong promotion is costly (every agent sees it forever) — prefer leaving it at TASK.
949
+
950
+ ### When to reach for this
951
+
952
+ After a non-trivial debugging session, a pitfall you hit, or establishing a
953
+ convention → consider `lumo task memory add`. When you notice a lesson recurring
954
+ across multiple tasks → consider `lumo memory promote`.
955
+
877
956
  ## Session Management
878
957
 
879
958
  ### `lumo session attach <identifier>` — bind the current session to a task
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.memoryProjectAdd = memoryProjectAdd;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const memory_content_1 = require("../lib/memory-content");
7
+ const resolve_1 = require("../lib/resolve");
8
+ const resolve_bound_task_1 = require("../lib/resolve-bound-task");
9
+ const resolve_project_1 = require("../lib/resolve-project");
10
+ async function memoryProjectAdd(refArg, options) {
11
+ if (!options.category) {
12
+ console.error('Error: --category <trap|decision|convention|procedural> is required.');
13
+ return 1;
14
+ }
15
+ const built = (0, memory_content_1.buildMemoryContent)(options.category, options);
16
+ if (!built.ok) {
17
+ console.error(`Error: ${built.error}`);
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 envUrl = process.env.LUMO_API_URL?.trim();
26
+ const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
27
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
28
+ let projectId;
29
+ let echoSuffix = '';
30
+ if (refArg) {
31
+ projectId = await (0, resolve_1.resolveProjectId)(base, creds.token, refArg);
32
+ }
33
+ else {
34
+ const bound = await (0, resolve_bound_task_1.resolveBoundTaskIdentifier)(apiUrl, creds.token);
35
+ if (!bound) {
36
+ console.error('Error: no <project-ref> given and no task bound to this session.\nPass a project, or run `lumo session attach <LUM-N>`.');
37
+ return 1;
38
+ }
39
+ const r = await (0, resolve_project_1.resolveBoundProjectId)(apiUrl, creds.token, bound);
40
+ if (!r.ok) {
41
+ console.error(`Error: ${r.error}`);
42
+ return 1;
43
+ }
44
+ projectId = r.id;
45
+ echoSuffix = ` (project of bound task ${bound})`;
46
+ }
47
+ let res;
48
+ try {
49
+ res = await fetch(`${base}/api/projects/${encodeURIComponent(projectId)}/memories`, {
50
+ method: 'POST',
51
+ headers: { Authorization: `Bearer ${creds.token}`, 'Content-Type': 'application/json' },
52
+ body: JSON.stringify({ category: built.category, content: built.content }),
53
+ });
54
+ }
55
+ catch (err) {
56
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${err instanceof Error ? err.message : String(err)})`);
57
+ return 1;
58
+ }
59
+ if (res.status !== 201) {
60
+ let m = null;
61
+ try {
62
+ const b = (await res.json());
63
+ if (typeof b.error === 'string')
64
+ m = b.error;
65
+ }
66
+ catch { /* */ }
67
+ console.error(m ? `Error: ${m}` : `Error: memory add failed (HTTP ${res.status})`);
68
+ return 1;
69
+ }
70
+ process.stdout.write(`Added ${built.category} PROJECT memory${echoSuffix}\n`);
71
+ }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.memoryProjectList = memoryProjectList;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const memory_content_1 = require("../lib/memory-content");
7
+ const resolve_1 = require("../lib/resolve");
8
+ const resolve_bound_task_1 = require("../lib/resolve-bound-task");
9
+ const resolve_project_1 = require("../lib/resolve-project");
10
+ async function memoryProjectList(refArg, options) {
11
+ const limit = options.limit !== undefined ? parseInt(options.limit, 10) : undefined;
12
+ if (limit !== undefined && (Number.isNaN(limit) || limit < 1)) {
13
+ console.error(`Error: invalid --limit "${options.limit}" (expected a positive integer)`);
14
+ return 1;
15
+ }
16
+ const creds = (0, config_1.readCredentials)();
17
+ if (!creds) {
18
+ console.error('Error: not logged in. Run `lumo auth login` first.');
19
+ return 1;
20
+ }
21
+ const envUrl = process.env.LUMO_API_URL?.trim();
22
+ const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
23
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
24
+ let projectId;
25
+ if (refArg) {
26
+ projectId = await (0, resolve_1.resolveProjectId)(base, creds.token, refArg);
27
+ }
28
+ else {
29
+ const bound = await (0, resolve_bound_task_1.resolveBoundTaskIdentifier)(apiUrl, creds.token);
30
+ if (!bound) {
31
+ console.error('Error: no <project-ref> given and no task bound to this session.\nPass a project, or run `lumo session attach <LUM-N>`.');
32
+ return 1;
33
+ }
34
+ const r = await (0, resolve_project_1.resolveBoundProjectId)(apiUrl, creds.token, bound);
35
+ if (!r.ok) {
36
+ console.error(`Error: ${r.error}`);
37
+ return 1;
38
+ }
39
+ projectId = r.id;
40
+ }
41
+ let res;
42
+ try {
43
+ res = await fetch(`${base}/api/projects/${encodeURIComponent(projectId)}/memories`, { headers: { Authorization: `Bearer ${creds.token}` } });
44
+ }
45
+ catch (err) {
46
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${err instanceof Error ? err.message : String(err)})`);
47
+ return 1;
48
+ }
49
+ if (!res.ok) {
50
+ console.error(`Error: memory list failed (HTTP ${res.status})`);
51
+ return 1;
52
+ }
53
+ let rows = (await res.json()).memories;
54
+ if (options.category)
55
+ rows = rows.filter(m => m.category.toLowerCase() === options.category.toLowerCase());
56
+ if (limit !== undefined)
57
+ rows = rows.slice(0, limit);
58
+ process.stdout.write((0, memory_content_1.formatMemoryList)(rows) + '\n');
59
+ }
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.memoryPromote = memoryPromote;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ async function memoryPromote(memoryId) {
7
+ if (!memoryId) {
8
+ console.error('Error: missing <memoryId>. Usage: lumo memory promote <memoryId>');
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/memories/${encodeURIComponent(memoryId)}`, {
22
+ method: 'PATCH',
23
+ headers: { Authorization: `Bearer ${creds.token}`, 'Content-Type': 'application/json' },
24
+ body: JSON.stringify({ scope: 'PROJECT' }),
25
+ });
26
+ }
27
+ catch (err) {
28
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${err instanceof Error ? err.message : String(err)})`);
29
+ return 1;
30
+ }
31
+ if (res.status === 404) {
32
+ console.error(`Error: memory ${memoryId} not found`);
33
+ return 1;
34
+ }
35
+ if (res.status === 409) {
36
+ console.error('Error: this memory is already project-scoped.');
37
+ return 1;
38
+ }
39
+ if (res.status !== 200) {
40
+ let m = null;
41
+ try {
42
+ const b = (await res.json());
43
+ if (typeof b.error === 'string')
44
+ m = b.error;
45
+ }
46
+ catch { /* */ }
47
+ console.error(m ? `Error: ${m}` : `Error: promote failed (HTTP ${res.status})`);
48
+ return 1;
49
+ }
50
+ process.stdout.write(`Promoted ${memoryId} to PROJECT — every agent on this project now sees it.\n` +
51
+ 'Promote only lessons that recur across 2+ tasks.\n');
52
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.memoryRm = memoryRm;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ async function memoryRm(memoryId, options) {
7
+ if (!memoryId) {
8
+ console.error('Error: missing <memoryId>. Usage: lumo memory rm <memoryId> --yes');
9
+ return 1;
10
+ }
11
+ if (!options.yes) {
12
+ console.error('Error: refusing to delete without --yes. Re-run with --yes to confirm.');
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/memories/${encodeURIComponent(memoryId)}`, {
26
+ method: 'DELETE',
27
+ headers: { Authorization: `Bearer ${creds.token}` },
28
+ });
29
+ }
30
+ catch (err) {
31
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${err instanceof Error ? err.message : String(err)})`);
32
+ return 1;
33
+ }
34
+ if (res.status === 404) {
35
+ console.error(`Error: memory ${memoryId} not found`);
36
+ return 1;
37
+ }
38
+ if (res.status !== 204) {
39
+ console.error(`Error: delete failed (HTTP ${res.status})`);
40
+ return 1;
41
+ }
42
+ process.stdout.write(`Deleted memory ${memoryId}\n`);
43
+ }
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.memoryTaskAdd = memoryTaskAdd;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const memory_content_1 = require("../lib/memory-content");
7
+ const resolve_bound_task_1 = require("../lib/resolve-bound-task");
8
+ async function memoryTaskAdd(identifierArg, options) {
9
+ if (!options.category) {
10
+ console.error('Error: --category <trap|decision|convention|procedural> is required.');
11
+ return 1;
12
+ }
13
+ const built = (0, memory_content_1.buildMemoryContent)(options.category, options);
14
+ if (!built.ok) {
15
+ console.error(`Error: ${built.error}`);
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 envUrl = process.env.LUMO_API_URL?.trim();
24
+ const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
25
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
26
+ // Resolve the target: explicit arg > session-bound task.
27
+ const identifier = identifierArg ?? (await (0, resolve_bound_task_1.resolveBoundTaskIdentifier)(apiUrl, creds.token));
28
+ if (!identifier) {
29
+ console.error('Error: no <LUM-N> given and no task bound to this session.\n' +
30
+ 'Pass it explicitly or run `lumo session attach <LUM-N>`.');
31
+ return 1;
32
+ }
33
+ // LUM-N → task id.
34
+ let taskId;
35
+ try {
36
+ const r = await fetch(`${base}/api/tasks/resolve/${encodeURIComponent(identifier)}`, {
37
+ headers: { Authorization: `Bearer ${creds.token}` },
38
+ });
39
+ if (r.status === 401) {
40
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
41
+ return 1;
42
+ }
43
+ if (r.status === 404) {
44
+ console.error(`Error: task ${identifier} not found in workspace ${creds.workspaceSlug}`);
45
+ return 1;
46
+ }
47
+ if (!r.ok) {
48
+ console.error(`Error: resolve failed (HTTP ${r.status})`);
49
+ return 1;
50
+ }
51
+ taskId = (await r.json()).id;
52
+ }
53
+ catch (err) {
54
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${err instanceof Error ? err.message : String(err)})`);
55
+ return 1;
56
+ }
57
+ // POST the memory.
58
+ let res;
59
+ try {
60
+ res = await fetch(`${base}/api/tasks/${encodeURIComponent(taskId)}/memories`, {
61
+ method: 'POST',
62
+ headers: { Authorization: `Bearer ${creds.token}`, 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({ category: built.category, content: built.content }),
64
+ });
65
+ }
66
+ catch (err) {
67
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${err instanceof Error ? err.message : String(err)})`);
68
+ return 1;
69
+ }
70
+ if (res.status !== 201) {
71
+ let m = null;
72
+ try {
73
+ const b = (await res.json());
74
+ if (typeof b.error === 'string')
75
+ m = b.error;
76
+ }
77
+ catch { /* */ }
78
+ console.error(m ? `Error: ${m}` : `Error: memory add failed (HTTP ${res.status})`);
79
+ return 1;
80
+ }
81
+ process.stdout.write(`Added ${built.category} memory to ${identifier}` +
82
+ `${identifierArg ? '' : ' (from bound session)'}\n` +
83
+ 'Tip: only useful for this task? If it generalizes, use `lumo project memory add` or `lumo memory promote` later.\n');
84
+ }
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.memoryTaskList = memoryTaskList;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const memory_content_1 = require("../lib/memory-content");
7
+ const resolve_bound_task_1 = require("../lib/resolve-bound-task");
8
+ async function memoryTaskList(identifierArg, options) {
9
+ const limit = options.limit !== undefined ? parseInt(options.limit, 10) : undefined;
10
+ if (limit !== undefined && (Number.isNaN(limit) || limit < 1)) {
11
+ console.error(`Error: invalid --limit "${options.limit}" (expected a positive integer)`);
12
+ return 1;
13
+ }
14
+ const creds = (0, config_1.readCredentials)();
15
+ if (!creds) {
16
+ console.error('Error: not logged in. Run `lumo auth login` first.');
17
+ return 1;
18
+ }
19
+ const envUrl = process.env.LUMO_API_URL?.trim();
20
+ const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
21
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
22
+ const identifier = identifierArg ?? (await (0, resolve_bound_task_1.resolveBoundTaskIdentifier)(apiUrl, creds.token));
23
+ if (!identifier) {
24
+ console.error('Error: no <LUM-N> given and no task bound to this session.\nPass it explicitly or run `lumo session attach <LUM-N>`.');
25
+ return 1;
26
+ }
27
+ let taskId;
28
+ try {
29
+ const r = await fetch(`${base}/api/tasks/resolve/${encodeURIComponent(identifier)}`, { headers: { Authorization: `Bearer ${creds.token}` } });
30
+ if (r.status === 404) {
31
+ console.error(`Error: task ${identifier} not found in workspace ${creds.workspaceSlug}`);
32
+ return 1;
33
+ }
34
+ if (!r.ok) {
35
+ console.error(`Error: resolve failed (HTTP ${r.status})`);
36
+ return 1;
37
+ }
38
+ taskId = (await r.json()).id;
39
+ }
40
+ catch (err) {
41
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${err instanceof Error ? err.message : String(err)})`);
42
+ return 1;
43
+ }
44
+ let res;
45
+ try {
46
+ res = await fetch(`${base}/api/tasks/${encodeURIComponent(taskId)}/memories`, { headers: { Authorization: `Bearer ${creds.token}` } });
47
+ }
48
+ catch (err) {
49
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${err instanceof Error ? err.message : String(err)})`);
50
+ return 1;
51
+ }
52
+ if (!res.ok) {
53
+ console.error(`Error: memory list failed (HTTP ${res.status})`);
54
+ return 1;
55
+ }
56
+ let rows = (await res.json()).memories;
57
+ if (options.category)
58
+ rows = rows.filter(m => m.category.toLowerCase() === options.category.toLowerCase());
59
+ if (limit !== undefined)
60
+ rows = rows.slice(0, limit);
61
+ process.stdout.write((0, memory_content_1.formatMemoryList)(rows) + '\n');
62
+ }
@@ -4,13 +4,15 @@ exports.taskArtifactAdd = taskArtifactAdd;
4
4
  const config_1 = require("../lib/config");
5
5
  const api_1 = require("../lib/api");
6
6
  const doc_input_1 = require("../lib/doc-input");
7
+ const agent_1 = require("../lib/agent");
7
8
  async function taskArtifactAdd(identifier, options) {
8
9
  if (!identifier) {
9
- console.error('Error: missing <task>. Usage: lumo task artifact add <LUM-42> --kind spec --title "Spec" --file spec.md');
10
+ console.error('Error: missing <task>. Usage: lumo task artifact add <LUM-42> --kind spec --title "Spec" --file spec.md --source Superpowers');
10
11
  return 1;
11
12
  }
12
13
  const kind = options.kind?.trim();
13
14
  const title = options.title?.trim();
15
+ const source = options.source?.trim();
14
16
  if (!kind) {
15
17
  console.error('Error: --kind is required and cannot be empty.');
16
18
  return 1;
@@ -19,6 +21,20 @@ async function taskArtifactAdd(identifier, options) {
19
21
  console.error('Error: --title is required and cannot be empty.');
20
22
  return 1;
21
23
  }
24
+ if (!source) {
25
+ console.error('Error: --source is required (the spec-gen framework, formal name e.g. Superpowers, "Spec Kit", BMad, OpenSpec, GSD).');
26
+ return 1;
27
+ }
28
+ const agentRaw = options.agent?.trim();
29
+ if (!agentRaw) {
30
+ console.error(`Error: --agent is required. Valid values: ${agent_1.VALID_AGENT_TOKENS.join(', ')}.`);
31
+ return 1;
32
+ }
33
+ const agent = (0, agent_1.normalizeAgent)(agentRaw);
34
+ if (!agent) {
35
+ console.error(`Error: invalid --agent "${agentRaw}". Valid values: ${agent_1.VALID_AGENT_TOKENS.join(', ')}.`);
36
+ return 1;
37
+ }
22
38
  if (!options.file) {
23
39
  console.error('Error: --file <path> is required.');
24
40
  return 1;
@@ -43,9 +59,13 @@ async function taskArtifactAdd(identifier, options) {
43
59
  const envUrl = process.env.LUMO_API_URL?.trim();
44
60
  const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
45
61
  const base = (0, api_1.trimTrailingSlash)(apiUrl);
46
- const payload = { kind, title, content };
47
- if (options.source?.trim())
48
- payload.source = options.source.trim();
62
+ const payload = {
63
+ kind,
64
+ title,
65
+ content,
66
+ source,
67
+ agent,
68
+ };
49
69
  let res;
50
70
  try {
51
71
  res = await fetch(`${base}/api/tasks/${encodeURIComponent(identifier)}/artifacts`, {
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.taskArtifactList = taskArtifactList;
4
4
  const config_1 = require("../lib/config");
5
5
  const api_1 = require("../lib/api");
6
+ const agent_1 = require("../lib/agent");
6
7
  async function taskArtifactList(identifier) {
7
8
  if (!identifier) {
8
9
  console.error('Error: missing <task>. Usage: lumo task artifact list <LUM-42>');
@@ -43,6 +44,7 @@ async function taskArtifactList(identifier) {
43
44
  return;
44
45
  }
45
46
  for (const a of artifacts) {
46
- process.stdout.write(`${a.id} ${a.kind.padEnd(10)} ${a.source.padEnd(12)} ${a.title}\n`);
47
+ const agentLabel = agent_1.AGENT_LABELS[a.agent] ?? a.agent;
48
+ process.stdout.write(`${a.id} ${a.kind.padEnd(10)} ${agentLabel.padEnd(15)} ${a.source.padEnd(12)} ${a.title}\n`);
47
49
  }
48
50
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.taskArtifactShow = taskArtifactShow;
4
4
  const config_1 = require("../lib/config");
5
5
  const api_1 = require("../lib/api");
6
+ const agent_1 = require("../lib/agent");
6
7
  async function taskArtifactShow(identifier, artifactId) {
7
8
  if (!identifier) {
8
9
  console.error('Error: missing <task>. Usage: lumo task artifact show <LUM-42> <artifact-id>');
@@ -55,6 +56,7 @@ async function taskArtifactShow(identifier, artifactId) {
55
56
  process.stdout.write(`kind: ${artifact.kind}\n`);
56
57
  process.stdout.write(`title: ${artifact.title}\n`);
57
58
  process.stdout.write(`source: ${artifact.source}\n`);
59
+ process.stdout.write(`agent: ${agent_1.AGENT_LABELS[artifact.agent] ?? artifact.agent}\n`);
58
60
  process.stdout.write(`order: ${artifact.order}\n`);
59
61
  process.stdout.write(`task: ${identifier}\n`);
60
62
  process.stdout.write(`\n${artifact.content}\n`);
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskArtifactUpdate = taskArtifactUpdate;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const agent_1 = require("../lib/agent");
7
+ async function taskArtifactUpdate(identifier, artifactId, options) {
8
+ if (!identifier) {
9
+ console.error('Error: missing <task>. Usage: lumo task artifact update <LUM-42> <artifact-id> --kind plan');
10
+ return 1;
11
+ }
12
+ if (!artifactId) {
13
+ console.error('Error: missing <artifact-id>. Usage: lumo task artifact update <LUM-42> <artifact-id> --kind plan');
14
+ return 1;
15
+ }
16
+ const payload = {};
17
+ const kind = options.kind?.trim();
18
+ const title = options.title?.trim();
19
+ const source = options.source?.trim();
20
+ if (options.kind !== undefined) {
21
+ if (!kind) {
22
+ console.error('Error: --kind cannot be empty.');
23
+ return 1;
24
+ }
25
+ payload.kind = kind;
26
+ }
27
+ if (options.title !== undefined) {
28
+ if (!title) {
29
+ console.error('Error: --title cannot be empty.');
30
+ return 1;
31
+ }
32
+ payload.title = title;
33
+ }
34
+ if (options.source !== undefined) {
35
+ if (!source) {
36
+ console.error('Error: --source cannot be empty.');
37
+ return 1;
38
+ }
39
+ payload.source = source;
40
+ }
41
+ if (options.agent !== undefined) {
42
+ const agent = (0, agent_1.normalizeAgent)(options.agent);
43
+ if (!agent) {
44
+ console.error(`Error: invalid --agent "${options.agent}". Valid values: ${agent_1.VALID_AGENT_TOKENS.join(', ')}.`);
45
+ return 1;
46
+ }
47
+ payload.agent = agent;
48
+ }
49
+ if (Object.keys(payload).length === 0) {
50
+ console.error('Error: provide at least one of --kind, --title, --source, or --agent to update.');
51
+ return 1;
52
+ }
53
+ const creds = (0, config_1.readCredentials)();
54
+ if (!creds) {
55
+ console.error('Error: not logged in. Run `lumo auth login` first.');
56
+ return 1;
57
+ }
58
+ const envUrl = process.env.LUMO_API_URL?.trim();
59
+ const apiUrl = envUrl && envUrl.length > 0 ? envUrl : creds.apiUrl;
60
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
61
+ let res;
62
+ try {
63
+ res = await fetch(`${base}/api/tasks/${encodeURIComponent(identifier)}/artifacts/${encodeURIComponent(artifactId)}`, {
64
+ method: 'PATCH',
65
+ headers: {
66
+ Authorization: `Bearer ${creds.token}`,
67
+ 'Content-Type': 'application/json',
68
+ },
69
+ body: JSON.stringify(payload),
70
+ });
71
+ }
72
+ catch (err) {
73
+ const msg = err instanceof Error ? err.message : String(err);
74
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
75
+ return 1;
76
+ }
77
+ if (res.status === 401) {
78
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
79
+ return 1;
80
+ }
81
+ if (res.status === 404) {
82
+ let serverMsg = `task ${identifier} or artifact ${artifactId} not found in workspace ${creds.workspaceSlug}`;
83
+ try {
84
+ const errBody = (await res.json());
85
+ if (typeof errBody.error === 'string')
86
+ serverMsg = errBody.error;
87
+ }
88
+ catch {
89
+ /* not JSON */
90
+ }
91
+ console.error(`Error: ${serverMsg}`);
92
+ return 1;
93
+ }
94
+ if (res.status !== 200) {
95
+ let serverMsg = null;
96
+ try {
97
+ const errBody = (await res.json());
98
+ if (typeof errBody.error === 'string')
99
+ serverMsg = errBody.error;
100
+ }
101
+ catch {
102
+ /* not JSON */
103
+ }
104
+ console.error(serverMsg
105
+ ? `Error: ${serverMsg}`
106
+ : `Error: artifact update failed (HTTP ${res.status})`);
107
+ return 1;
108
+ }
109
+ const data = (await res.json());
110
+ process.stdout.write(`Updated [${data.artifact.kind}] "${data.artifact.title}" (${data.artifact.source}) on ${identifier}\n`);
111
+ }
@@ -55,10 +55,17 @@ const task_figma_add_1 = require("./commands/task-figma-add");
55
55
  const task_figma_list_1 = require("./commands/task-figma-list");
56
56
  const task_figma_rm_1 = require("./commands/task-figma-rm");
57
57
  const task_figma_refresh_1 = require("./commands/task-figma-refresh");
58
+ const memory_task_list_1 = require("./commands/memory-task-list");
59
+ const memory_task_add_1 = require("./commands/memory-task-add");
60
+ const memory_project_list_1 = require("./commands/memory-project-list");
61
+ const memory_project_add_1 = require("./commands/memory-project-add");
62
+ const memory_promote_1 = require("./commands/memory-promote");
63
+ const memory_rm_1 = require("./commands/memory-rm");
58
64
  const task_artifact_add_1 = require("./commands/task-artifact-add");
59
65
  const task_artifact_list_1 = require("./commands/task-artifact-list");
60
66
  const task_artifact_show_1 = require("./commands/task-artifact-show");
61
67
  const task_artifact_rm_1 = require("./commands/task-artifact-rm");
68
+ const task_artifact_update_1 = require("./commands/task-artifact-update");
62
69
  const project_list_1 = require("./commands/project-list");
63
70
  const milestone_list_1 = require("./commands/milestone-list");
64
71
  const milestone_create_1 = require("./commands/milestone-create");
@@ -236,6 +243,33 @@ taskFigma
236
243
  .command('refresh <task>')
237
244
  .description('Re-fetch Figma metadata + thumbnail for every link on this task. Per-link failures isolated.')
238
245
  .action(wrap((taskId) => (0, task_figma_refresh_1.taskFigmaRefresh)({ identifier: taskId })));
246
+ const taskMemory = task
247
+ .command('memory')
248
+ .description('View and record memories scoped to a task');
249
+ taskMemory
250
+ .command('list [task]')
251
+ .description("List a task's memories. <task> defaults to the session-bound task.")
252
+ .option('--category <cat>', 'Filter by trap|decision|convention|procedural')
253
+ .option('-n, --limit <count>', 'Cap output to the first N rows')
254
+ .action(wrap((taskArg, opts) => (0, memory_task_list_1.memoryTaskList)(taskArg, opts)));
255
+ taskMemory
256
+ .command('add [task]')
257
+ .description('Record a memory on a task (<task> defaults to the bound session). ' +
258
+ 'Record only knowledge invisible in the codebase (the why, a runtime gotcha, a non-obvious failure, a non-trivial workflow); skip routine work. ' +
259
+ 'Pick --category then its fields.')
260
+ .requiredOption('--category <cat>', 'trap | decision | convention | procedural')
261
+ .option('--trigger <text>', 'trap/procedural: the situation that triggers it')
262
+ .option('--outcome <text>', 'trap: what goes wrong')
263
+ .option('--workaround <text>', 'trap: optional fix')
264
+ .option('--what <text>', 'decision: the decision')
265
+ .option('--why <text>', 'decision: rationale')
266
+ .option('--alternatives <text>', 'decision: optional alternatives considered')
267
+ .option('--implications <text>', 'decision: optional implications')
268
+ .option('--rule <text>', 'convention: the rule')
269
+ .option('--applies <text>', 'convention: where the rule applies')
270
+ .option('--workflow <text>', 'procedural: the workflow name')
271
+ .option('--step <text>', 'procedural: a step (repeatable)', collect, [])
272
+ .action(wrap((taskArg, opts) => (0, memory_task_add_1.memoryTaskAdd)(taskArg, opts)));
239
273
  const taskArtifact = task
240
274
  .command('artifact')
241
275
  .description('Record spec-engineering artifacts (spec / plan / design …) on a task');
@@ -245,8 +279,17 @@ taskArtifact
245
279
  .requiredOption('--kind <kind>', 'Artifact kind, e.g. spec | plan | design (opaque)')
246
280
  .requiredOption('--title <title>', 'Artifact title')
247
281
  .requiredOption('--file <path>', 'File whose contents become the artifact body')
248
- .option('--source <source>', 'Producer label (default: claude-code)')
282
+ .requiredOption('--source <source>', 'Spec-gen framework, formal name e.g. Superpowers | "Spec Kit" | BMad | OpenSpec | GSD (opaque; quote names with spaces)')
283
+ .requiredOption('--agent <agent>', 'Coding tool that produced the artifact: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf')
249
284
  .action(wrap((taskId, options) => (0, task_artifact_add_1.taskArtifactAdd)(taskId, options)));
285
+ taskArtifact
286
+ .command('update <task> <artifact-id>')
287
+ .description('Update an artifact’s kind / title / source (not content). Provide at least one flag.')
288
+ .option('--kind <kind>', 'New artifact kind (opaque)')
289
+ .option('--title <title>', 'New artifact title')
290
+ .option('--source <source>', 'New spec-gen framework, formal name e.g. Superpowers | "Spec Kit" | BMad | OpenSpec | GSD (quote names with spaces)')
291
+ .option('--agent <agent>', 'New coding tool: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf')
292
+ .action(wrap((taskId, artifactId, options) => (0, task_artifact_update_1.taskArtifactUpdate)(taskId, artifactId, options)));
250
293
  taskArtifact
251
294
  .command('list <task>')
252
295
  .description('List artifacts attached to a task, in order')
@@ -267,6 +310,43 @@ projectCmd
267
310
  .command('list')
268
311
  .description('List projects in the current workspace. Slug column matches `task create --project <ref>`.')
269
312
  .action(wrap(() => (0, project_list_1.projectList)()));
313
+ const projectMemory = projectCmd
314
+ .command('memory')
315
+ .description('View and record PROJECT-scope memories');
316
+ projectMemory
317
+ .command('list [project]')
318
+ .description("List a project's PROJECT-scope memories. <project> defaults to the bound task's project.")
319
+ .option('--category <cat>', 'Filter by trap|decision|convention|procedural')
320
+ .option('-n, --limit <count>', 'Cap output to the first N rows')
321
+ .action(wrap((p, opts) => (0, memory_project_list_1.memoryProjectList)(p, opts)));
322
+ projectMemory
323
+ .command('add [project]')
324
+ .description("Record a PROJECT-scope memory. Use PROJECT scope only when the lesson helps any task in the project. <project> defaults to the bound task's project.")
325
+ .requiredOption('--category <cat>', 'trap | decision | convention | procedural')
326
+ .option('--trigger <text>', 'trap/procedural trigger')
327
+ .option('--outcome <text>', 'trap outcome')
328
+ .option('--workaround <text>', 'trap optional workaround')
329
+ .option('--what <text>', 'decision')
330
+ .option('--why <text>', 'decision rationale')
331
+ .option('--alternatives <text>', 'decision optional alternatives')
332
+ .option('--implications <text>', 'decision optional implications')
333
+ .option('--rule <text>', 'convention rule')
334
+ .option('--applies <text>', 'convention: where it applies')
335
+ .option('--workflow <text>', 'procedural workflow')
336
+ .option('--step <text>', 'procedural step (repeatable)', collect, [])
337
+ .action(wrap((p, opts) => (0, memory_project_add_1.memoryProjectAdd)(p, opts)));
338
+ const memoryCmd = program
339
+ .command('memory')
340
+ .description('Operate on a single memory by id (see `lumo task memory` / `lumo project memory` to list/add)');
341
+ memoryCmd
342
+ .command('promote <memoryId>')
343
+ .description('Promote a TASK memory to PROJECT scope. Only when the lesson recurs across 2+ tasks.')
344
+ .action(wrap((id) => (0, memory_promote_1.memoryPromote)(id)));
345
+ memoryCmd
346
+ .command('rm <memoryId>')
347
+ .description('Delete a memory (hard delete). Requires --yes.')
348
+ .option('--yes', 'Confirm deletion')
349
+ .action(wrap((id, opts) => (0, memory_rm_1.memoryRm)(id, opts)));
270
350
  const milestoneCmd = program
271
351
  .command('milestone')
272
352
  .description('Inspect milestones from the terminal');
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ /**
3
+ * CLI-local agent enum support. The CLI talks to the API over HTTP only and
4
+ * does not depend on @prisma/client, so the enum values + accepted tokens are
5
+ * mirrored here. Keep in sync with prisma `enum TaskArtifactAgent`.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.VALID_AGENT_TOKENS = exports.AGENT_LABELS = void 0;
9
+ exports.normalizeAgent = normalizeAgent;
10
+ /** enum value → display label (matches lib/i18n en.json taskArtifact.agent.*) */
11
+ exports.AGENT_LABELS = {
12
+ CLAUDE_CODE: 'Claude Code',
13
+ CODEX: 'Codex',
14
+ CURSOR: 'Cursor',
15
+ GEMINI_CLI: 'Gemini CLI',
16
+ GITHUB_COPILOT: 'GitHub Copilot',
17
+ WINDSURF: 'Windsurf',
18
+ };
19
+ /** accepted CLI token (normalized) → enum value */
20
+ const TOKEN_TO_ENUM = {
21
+ 'claude-code': 'CLAUDE_CODE',
22
+ codex: 'CODEX',
23
+ cursor: 'CURSOR',
24
+ 'gemini-cli': 'GEMINI_CLI',
25
+ gemini: 'GEMINI_CLI',
26
+ 'github-copilot': 'GITHUB_COPILOT',
27
+ copilot: 'GITHUB_COPILOT',
28
+ windsurf: 'WINDSURF',
29
+ };
30
+ /** Canonical tokens shown in error/usage hints (aliases omitted). */
31
+ exports.VALID_AGENT_TOKENS = [
32
+ 'claude-code',
33
+ 'codex',
34
+ 'cursor',
35
+ 'gemini-cli',
36
+ 'github-copilot',
37
+ 'windsurf',
38
+ ];
39
+ /**
40
+ * Normalize a user-supplied agent string to its enum value, or null if
41
+ * unknown. Case-insensitive; spaces and underscores are treated as hyphens.
42
+ */
43
+ function normalizeAgent(input) {
44
+ const key = input
45
+ .trim()
46
+ .toLowerCase()
47
+ .replace(/[\s_]+/g, '-');
48
+ return TOKEN_TO_ENUM[key] ?? null;
49
+ }
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ // Category/field metadata + builders for the `lumo memory` commands.
3
+ // Mirrors the four content shapes validated server-side by parseMemoryContent.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.buildMemoryContent = buildMemoryContent;
6
+ exports.formatMemoryList = formatMemoryList;
7
+ const trimmed = (v) => (v ?? '').trim();
8
+ /**
9
+ * Validate the per-category flags and assemble { category, content }. Required
10
+ * fields must be non-empty; optional string fields are dropped when blank;
11
+ * `--applies` maps to convention `content.scope`; repeated `--step` becomes a
12
+ * trimmed, empty-filtered `steps` array (omitted when empty).
13
+ */
14
+ function buildMemoryContent(category, flags) {
15
+ const cat = category?.toLowerCase();
16
+ switch (cat) {
17
+ case 'trap': {
18
+ const trigger = trimmed(flags.trigger);
19
+ const outcome = trimmed(flags.outcome);
20
+ const workaround = trimmed(flags.workaround);
21
+ if (!trigger)
22
+ return { ok: false, error: '--trigger is required for category trap.' };
23
+ if (!outcome)
24
+ return { ok: false, error: '--outcome is required for category trap.' };
25
+ return { ok: true, category: 'TRAP', content: { trigger, outcome, ...(workaround ? { workaround } : {}) } };
26
+ }
27
+ case 'decision': {
28
+ const what = trimmed(flags.what);
29
+ const why = trimmed(flags.why);
30
+ const alternatives = trimmed(flags.alternatives);
31
+ const implications = trimmed(flags.implications);
32
+ if (!what)
33
+ return { ok: false, error: '--what is required for category decision.' };
34
+ if (!why)
35
+ return { ok: false, error: '--why is required for category decision.' };
36
+ return {
37
+ ok: true,
38
+ category: 'DECISION',
39
+ content: { what, why, ...(alternatives ? { alternatives } : {}), ...(implications ? { implications } : {}) },
40
+ };
41
+ }
42
+ case 'convention': {
43
+ const rule = trimmed(flags.rule);
44
+ const applies = trimmed(flags.applies);
45
+ if (!rule)
46
+ return { ok: false, error: '--rule is required for category convention.' };
47
+ if (!applies)
48
+ return { ok: false, error: '--applies is required for category convention (where the rule applies).' };
49
+ return { ok: true, category: 'CONVENTION', content: { rule, scope: applies } };
50
+ }
51
+ case 'procedural': {
52
+ const workflow = trimmed(flags.workflow);
53
+ const trigger = trimmed(flags.trigger);
54
+ const steps = (flags.step ?? []).map(s => s.trim()).filter(Boolean);
55
+ if (!workflow)
56
+ return { ok: false, error: '--workflow is required for category procedural.' };
57
+ if (!trigger)
58
+ return { ok: false, error: '--trigger is required for category procedural.' };
59
+ return { ok: true, category: 'PROCEDURAL', content: { workflow, trigger, ...(steps.length ? { steps } : {}) } };
60
+ }
61
+ default:
62
+ return { ok: false, error: `Unknown --category "${category}". Use trap | decision | convention | procedural.` };
63
+ }
64
+ }
65
+ function headline(category, content) {
66
+ const c = (content ?? {});
67
+ const key = category === 'TRAP' ? 'trigger'
68
+ : category === 'DECISION' ? 'what'
69
+ : category === 'CONVENTION' ? 'rule'
70
+ : 'workflow';
71
+ const v = c[key];
72
+ return typeof v === 'string' && v.length > 0 ? v : '(unparseable)';
73
+ }
74
+ /** Fixed-width rows: id SCOPE CATEGORY headline source(auto|manual). */
75
+ function formatMemoryList(rows) {
76
+ if (rows.length === 0)
77
+ return 'No memories.';
78
+ const idW = Math.max(...rows.map(r => r.id.length));
79
+ const scopeW = Math.max(...rows.map(r => r.scope.length));
80
+ const catW = Math.max(...rows.map(r => r.category.length));
81
+ return rows
82
+ .map(r => {
83
+ const src = r.createdByMemberId ? 'manual' : 'auto';
84
+ return `${r.id.padEnd(idW)} ${r.scope.padEnd(scopeW)} ${r.category.padEnd(catW)} ${headline(r.category, r.content)} ${src}`;
85
+ })
86
+ .join('\n');
87
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveBoundTaskIdentifier = resolveBoundTaskIdentifier;
4
+ const api_1 = require("./api");
5
+ /**
6
+ * The task identifier (LUM-N) the current Claude Code session is bound to, or
7
+ * null when there is no session env, no server-side session row, or no binding.
8
+ * Used by the memory commands to default their target to the bound task.
9
+ */
10
+ async function resolveBoundTaskIdentifier(apiUrl, token) {
11
+ const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
12
+ if (!sessionId)
13
+ return null;
14
+ const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/sessions/${encodeURIComponent(sessionId)}`;
15
+ let res;
16
+ try {
17
+ res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ if (!res.ok)
23
+ return null;
24
+ try {
25
+ const data = (await res.json());
26
+ return data.taskIdentifier ?? null;
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveBoundProjectId = resolveBoundProjectId;
4
+ const api_1 = require("./api");
5
+ /** Resolve the project id of the task bound to this session (via its identifier). */
6
+ async function resolveBoundProjectId(apiUrl, token, boundIdentifier) {
7
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
8
+ let res;
9
+ try {
10
+ res = await fetch(`${base}/api/tasks/by-identifier/${encodeURIComponent(boundIdentifier)}`, {
11
+ headers: { Authorization: `Bearer ${token}` },
12
+ });
13
+ }
14
+ catch (err) {
15
+ return { ok: false, error: `could not reach Lumo API (${err instanceof Error ? err.message : String(err)})` };
16
+ }
17
+ if (!res.ok)
18
+ return { ok: false, error: `could not resolve bound task ${boundIdentifier} (HTTP ${res.status})` };
19
+ const { task } = (await res.json());
20
+ const id = task.project?.id;
21
+ if (!id)
22
+ return { ok: false, error: `bound task ${boundIdentifier} has no resolvable project` };
23
+ return { ok: true, id };
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumoai/cli",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",