@lumoai/cli 1.10.0 → 1.11.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", "新建里程碑", "更新里程碑", "删除里程碑", "查看里程碑", "milestone summary", "milestone retro", "summarize milestone", "里程碑总结", "里程碑复盘", "milestone add", "milestone remove", "add tasks to milestone", "remove tasks from milestone", "batch milestone", "bulk milestone", "挂任务到里程碑", "批量挂里程碑", "从里程碑移除任务", "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", "retrieval", "取全文", "load full content", "拉全文", "task slack show", "看 thread", "看 slack thread", "show slack thread", "slack 全文", "task web show", "show web link body", "web 正文", "抓网页正文", "task figma context", "figma metadata", "figma 元数据", "figma 设计上下文", "task comments list", "list comments", "看评论", "评论流", "task pr show", "查看 PR", "show pr", "PR 详情", "pr metadata", "import google doc", "sync google doc", "google drive", "doc import-gdoc", "doc sync", "导入 google 文档", "同步 google 文档", "session wrap", "wrap up session", "收尾", "post progress", "把进度发出去", "progress comment", "进度评论".'
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", "新建里程碑", "更新里程碑", "删除里程碑", "查看里程碑", "milestone summary", "milestone retro", "summarize milestone", "里程碑总结", "里程碑复盘", "milestone add", "milestone remove", "add tasks to milestone", "remove tasks from milestone", "batch milestone", "bulk milestone", "挂任务到里程碑", "批量挂里程碑", "从里程碑移除任务", "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", "retrieval", "取全文", "load full content", "拉全文", "task slack show", "看 thread", "看 slack thread", "show slack thread", "slack 全文", "task web show", "show web link body", "web 正文", "抓网页正文", "task figma context", "figma metadata", "figma 元数据", "figma 设计上下文", "task comments list", "list comments", "看评论", "评论流", "task pr show", "查看 PR", "show pr", "PR 详情", "pr metadata", "import google doc", "sync google doc", "google drive", "doc import-gdoc", "doc sync", "导入 google 文档", "同步 google 文档", "session wrap", "wrap up session", "收尾", "post progress", "把进度发出去", "progress comment", "进度评论", "lumo next", "next task", "what''s next", "what should I work on", "recommend a task", "推荐下一个任务", "pick my next task".'
4
4
  ---
5
5
 
6
6
  ## Prerequisites
@@ -370,6 +370,44 @@ Filtering is currently client-side — the server returns the full "my tasks" se
370
370
  - The user asks "what am I working on", "what tasks do I have", "list my tasks", "show me my queue".
371
371
  - Before suggesting a status change ("mark something as done"), if no task ID is in context — run `task list` first to surface candidates.
372
372
 
373
+ ### `lumo next [--count <N>]` — recommend the next task to work on
374
+
375
+ Ranks the tasks assigned to you and prints the top N (default 3), each with a
376
+ one-line reason. Read-only — it does **not** bind or load context. Pick one from
377
+ the list, then run `lumo session attach <LUM-N>` + `lumo task context <LUM-N>`.
378
+
379
+ Ranking is lexicographic: **priority** (URGENT→LOW) first, then **active-sprint
380
+ membership**, then **due date** (earlier first), then in-flight status
381
+ (IN_PROGRESS / IN_REVIEW ahead of TODO). DONE tasks are excluded. The active
382
+ sprint lookup is best-effort — if it fails the command still recommends, just
383
+ without the sprint boost.
384
+
385
+ | Flag | Type | Notes |
386
+ | ---- | ---- | ----- |
387
+ | `-n, --count <N>` | integer | How many tasks to recommend. Defaults to 3. Must be a positive integer. |
388
+
389
+ ```bash
390
+ lumo next
391
+ lumo next --count 1
392
+ ```
393
+
394
+ Output:
395
+
396
+ ```
397
+ Top 3 recommended tasks (of 12 open):
398
+
399
+ 1. LUM-42 IN_PROGRESS URGENT Fix Slack OAuth redirect
400
+ ↳ URGENT · active sprint · due 2026-06-03 (overdue) · in progress
401
+ 2. LUM-48 TODO HIGH Investigate slow query
402
+ ↳ HIGH · active sprint
403
+ 3. LUM-12 TODO MEDIUM Add rate limiting
404
+ ↳ MEDIUM · due 2026-06-10
405
+
406
+ Next: lumo session attach LUM-42 && lumo task context LUM-42
407
+ ```
408
+
409
+ When to suggest: the user asks "what should I work on", "what's next", "推荐下一个任务", "pick my next task", or starts a session without a task in mind. After they choose, run `session attach` + `task context` for the picked task.
410
+
373
411
  ### `lumo task show <identifier>` — print one task's detail
374
412
 
375
413
  Returns a key:value block for a single task — title, status, priority, project, assignee (with display name from Clerk), URL, and the full description below. Lighter than `task context` because it does not load prior session summaries or memory.
@@ -1235,27 +1273,46 @@ lumo session detach
1235
1273
 
1236
1274
  When to suggest: the user wants to stop tagging the current session with the active task (e.g., switching to unrelated exploratory work without binding to a different task).
1237
1275
 
1238
- ### `lumo session wrap [--yes] [--dry-run]` — draft + post a progress comment at wrap-up
1276
+ ### `lumo session wrap [--yes] [--dry-run]` — wrap-up panel: progress comment + memory review
1239
1277
 
1240
- Session-end wrap-up panel. Reads back the current Claude Code session's per-turn
1278
+ Session-end wrap-up panel with **two sections, run in order**:
1279
+
1280
+ **1. 进度评论** — reads back the current Claude Code session's per-turn
1241
1281
  `turnSummary` rows (the one-line Chinese summaries written each STOP), aggregates
1242
1282
  every turn **since the last progress comment** into one bulleted body, and — after
1243
1283
  a `[y] 发送 / [e] 编辑 / [s] 跳过` confirmation — posts it as a comment on the
1244
1284
  session's bound task. A server-side watermark (`Session.lastProgressCommentAt`)
1245
1285
  means re-running never re-posts the same turns.
1246
1286
 
1287
+ **2. 记忆审阅** — lists the Layer1 memories this session sedimented since the
1288
+ last review (deduped by a per-session watermark `Session.lastMemoryReviewAt`).
1289
+ Each new memory is shown as `[SCOPE] CATEGORY headline`, numbered from 1. You
1290
+ curate with a single line: `d 1,3` deletes rows 1 and 3, `p 2` promotes row 2 to
1291
+ project scope, and they combine (`d 1,3 p 2`). **回车 (empty) keeps all**; `s`
1292
+ skips the section. Keeping all (回车 or `--yes`) still **advances the watermark**
1293
+ so the next wrap won't re-list reviewed memories; `s` leaves them for next time.
1294
+ Out-of-range indices are ignored. Deletes/promotes run server-side, scoped to
1295
+ memories this session created (you can't touch other sessions' memories through
1296
+ this panel). With no new memories the section prints "(无内容)" and does nothing.
1297
+
1247
1298
  ```bash
1248
- lumo session wrap # interactive: preview draft, choose y / e / s
1249
- lumo session wrap --yes # post the drafted body without prompting (agent-friendly)
1250
- lumo session wrap --dry-run # print the draft only; never posts, never advances watermark
1299
+ lumo session wrap # interactive: preview each section, choose per-section
1300
+ lumo session wrap --yes # progress comment posted + memories all kept, no prompting (agent-friendly)
1301
+ lumo session wrap --dry-run # print both drafts only; never posts, never mutates, never advances watermarks
1251
1302
  ```
1252
1303
 
1253
1304
  - Requires `$CLAUDE_CODE_SESSION_ID` (must run inside Claude Code) and a bound
1254
1305
  task (`lumo session attach <LUM-N>` first). With no bound task or no new turn
1255
- summaries, the panel prints "(无内容)" and posts nothing.
1256
- - `[e] 编辑` opens `$EDITOR` (fallback vi/nano) on the drafted body; the edited
1257
- text is posted and the watermark still advances to the turns the draft covered.
1258
- - Non-TTY without `--yes`: prints the draft and does **not** post (safe default).
1306
+ summaries, the 进度评论 section prints "(无内容)" and posts nothing.
1307
+ - `[e] 编辑` (进度评论) opens `$EDITOR` (fallback vi/nano) on the drafted body;
1308
+ the edited text is posted and the watermark still advances to the turns the
1309
+ draft covered.
1310
+ - `--yes` applies to both sections: posts the progress comment AND keeps all
1311
+ memories (no deletes/promotes) while advancing the memory-review watermark.
1312
+ - `--dry-run` prints both drafts; never posts, never mutates memories, never
1313
+ advances either watermark.
1314
+ - Non-TTY without `--yes`: prints the drafts and does **not** post or mutate
1315
+ (safe default).
1259
1316
 
1260
1317
  When to suggest: at the end of a working session on a bound task, to record what
1261
1318
  was done as a progress comment — offer `lumo session wrap` rather than composing
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.nextCommand = nextCommand;
4
+ exports.formatNextOutput = formatNextOutput;
5
+ const config_1 = require("../lib/config");
6
+ const api_1 = require("../lib/api");
7
+ const sanitize_1 = require("../lib/sanitize");
8
+ const rank_tasks_1 = require("../lib/rank-tasks");
9
+ /**
10
+ * `lumo next [-n, --count <N>]` — recommend the next task(s) to work on.
11
+ *
12
+ * Chains two read-only endpoints: active sprint ids (for a ranking boost) and
13
+ * "my tasks". The active-sprints call is non-fatal — if it fails we warn and
14
+ * rank without the sprint boost rather than aborting.
15
+ */
16
+ async function nextCommand(opts) {
17
+ const count = opts.count !== undefined ? parseInt(opts.count, 10) : 3;
18
+ if (Number.isNaN(count) || count < 1) {
19
+ console.error(`Error: invalid --count "${opts.count}" (expected a positive integer)`);
20
+ return 1;
21
+ }
22
+ const creds = (0, config_1.readCredentials)();
23
+ if (!creds) {
24
+ console.error('Error: not logged in. Run `lumo auth login` first.');
25
+ return 1;
26
+ }
27
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
28
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
29
+ const authHeaders = { Authorization: `Bearer ${creds.token}` };
30
+ // 1. Active sprint ids — non-fatal. Failure just drops the sprint boost.
31
+ let activeSprintIds = new Set();
32
+ try {
33
+ const res = await fetch(`${base}/api/me/active-sprints`, {
34
+ headers: authHeaders,
35
+ });
36
+ if (res.ok) {
37
+ const data = (await res.json());
38
+ activeSprintIds = new Set(data.sprintIds);
39
+ }
40
+ else {
41
+ console.error(`Warning: could not load active sprints (HTTP ${res.status}); ranking without sprint boost.`);
42
+ }
43
+ }
44
+ catch (err) {
45
+ const msg = err instanceof Error ? err.message : String(err);
46
+ console.error(`Warning: could not load active sprints (${msg}); ranking without sprint boost.`);
47
+ }
48
+ // 2. My tasks — fatal on failure.
49
+ let res;
50
+ try {
51
+ res = await fetch(`${base}/api/tasks/me`, { headers: authHeaders });
52
+ }
53
+ catch (err) {
54
+ const msg = err instanceof Error ? err.message : String(err);
55
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
56
+ return 1;
57
+ }
58
+ if (res.status === 401) {
59
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
60
+ return 1;
61
+ }
62
+ if (!res.ok) {
63
+ console.error(`Error: task fetch failed (HTTP ${res.status})`);
64
+ return 1;
65
+ }
66
+ const data = (await res.json());
67
+ const open = data.tasks.filter(t => t.status !== 'DONE');
68
+ const ranked = (0, rank_tasks_1.rankTasks)(open, activeSprintIds, new Date()).slice(0, count);
69
+ process.stdout.write(formatNextOutput(ranked, open.length) + '\n');
70
+ }
71
+ /**
72
+ * Render the ranked recommendation block. `ranked` is already sliced to the
73
+ * requested count; `totalOpen` is the count of all non-DONE tasks (for the
74
+ * "(of N open)" header). Pure — no I/O.
75
+ */
76
+ function formatNextOutput(ranked, totalOpen) {
77
+ const first = ranked[0];
78
+ if (!first)
79
+ return 'No open tasks assigned to you. 🎉';
80
+ const widths = {
81
+ identifier: Math.max(...ranked.map(r => r.identifier.length)),
82
+ status: Math.max(...ranked.map(r => r.task.status.length)),
83
+ priority: Math.max(...ranked.map(r => r.task.priority.length)),
84
+ };
85
+ const plural = ranked.length === 1 ? '' : 's';
86
+ const lines = [
87
+ `Top ${ranked.length} recommended task${plural} (of ${totalOpen} open):`,
88
+ '',
89
+ ];
90
+ ranked.forEach((r, i) => {
91
+ lines.push(`${i + 1}. ${r.identifier.padEnd(widths.identifier)} ` +
92
+ `${r.task.status.padEnd(widths.status)} ` +
93
+ `${r.task.priority.padEnd(widths.priority)} ` +
94
+ (0, sanitize_1.sanitizeField)(r.task.title));
95
+ lines.push(` ↳ ${r.reasons.join(' · ')}`);
96
+ });
97
+ lines.push('');
98
+ lines.push(`Next: lumo session attach ${first.identifier} && lumo task context ${first.identifier}`);
99
+ if (ranked.length > 1) {
100
+ lines.push('(也可换成列表里任意一个 LUM-N)');
101
+ }
102
+ return lines.join('\n');
103
+ }
@@ -4,13 +4,15 @@ exports.sessionWrap = sessionWrap;
4
4
  const config_1 = require("../lib/config");
5
5
  const wrap_panel_1 = require("../lib/wrap-panel");
6
6
  const progress_comment_section_1 = require("./wrap/progress-comment-section");
7
+ const memory_review_section_1 = require("./wrap/memory-review-section");
7
8
  /**
8
9
  * `lumo session wrap [--yes] [--dry-run]`
9
10
  *
10
- * Session-end wrap-up panel. v1 has a single section: draft a progress comment
11
- * from this session's unposted turnSummaries and post it (after y/e/s
12
- * confirmation) to the bound task. Designed as a multi-section panel so
13
- * LUM-152's memory-review section slots in without touching this command.
11
+ * Session-end wrap-up panel with two sections, run in order: (1) draft a
12
+ * progress comment from this session's unposted turnSummaries and post it
13
+ * (after y/e/s confirmation) to the bound task; (2) review the Layer1 memories
14
+ * this session sedimented keep/delete/promote, deduped by a per-session
15
+ * watermark.
14
16
  */
15
17
  async function sessionWrap(options) {
16
18
  const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
@@ -24,7 +26,10 @@ async function sessionWrap(options) {
24
26
  console.error('Error: not logged in. Run `lumo auth login` first.');
25
27
  return 1;
26
28
  }
27
- const sections = [new progress_comment_section_1.ProgressCommentSection({ creds, sessionId })];
29
+ const sections = [
30
+ new progress_comment_section_1.ProgressCommentSection({ creds, sessionId }),
31
+ new memory_review_section_1.MemoryReviewSection({ creds, sessionId }),
32
+ ];
28
33
  await (0, wrap_panel_1.runWrapPanel)(sections, {
29
34
  yes: options.yes === true,
30
35
  dryRun: options.dryRun === true,
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MemoryReviewSection = void 0;
4
+ exports.parseReviewInstruction = parseReviewInstruction;
5
+ const sanitize_1 = require("../../lib/sanitize");
6
+ const line_prompt_1 = require("../../lib/line-prompt");
7
+ const memory_content_1 = require("../../lib/memory-content");
8
+ const session_memory_api_1 = require("../../lib/session-memory-api");
9
+ /** Parse a one-line review instruction into 0-based row indices. */
10
+ function parseReviewInstruction(line) {
11
+ const result = { deleteIdx: [], promoteIdx: [] };
12
+ const re = /([dp])\s*([\d,\s]+)/gi;
13
+ let m;
14
+ while ((m = re.exec(line)) !== null) {
15
+ const nums = m[2]
16
+ .split(/[\s,]+/)
17
+ .map(s => s.trim())
18
+ .filter(Boolean)
19
+ .map(s => parseInt(s, 10) - 1)
20
+ .filter(n => Number.isInteger(n) && n >= 0);
21
+ if (m[1].toLowerCase() === 'd')
22
+ result.deleteIdx.push(...nums);
23
+ else
24
+ result.promoteIdx.push(...nums);
25
+ }
26
+ return result;
27
+ }
28
+ /**
29
+ * Wrap-panel section that lists the Layer1 memories this session sedimented and
30
+ * lets the user delete noise / promote keepers to project scope. Keeps its own
31
+ * draft state between prepare() and run(). Dedup is server-side via watermark.
32
+ */
33
+ class MemoryReviewSection {
34
+ deps;
35
+ title = '记忆审阅';
36
+ draft = null;
37
+ constructor(deps) {
38
+ this.deps = deps;
39
+ }
40
+ async prepare() {
41
+ this.draft = await (0, session_memory_api_1.fetchMemoryDraft)(this.deps.creds, this.deps.sessionId);
42
+ return this.draft.memories.length > 0;
43
+ }
44
+ async run(opts) {
45
+ const draft = this.draft;
46
+ if (!draft || !draft.watermark || draft.memories.length === 0)
47
+ return;
48
+ process.stdout.write(`本次会话新增了这 ${draft.memories.length} 条 memory:\n`);
49
+ process.stdout.write(`${(0, sanitize_1.sanitizeField)((0, memory_content_1.formatMemoryReviewList)(draft.memories))}\n`);
50
+ if (opts.dryRun) {
51
+ process.stdout.write('(dry-run,未改动)\n');
52
+ return;
53
+ }
54
+ if (opts.yes) {
55
+ await this.apply(draft.watermark, [], []);
56
+ return;
57
+ }
58
+ const line = (await (0, line_prompt_1.promptLine)('[回车] 全部保留 [d 1,3] 删除 [p 2] 提升到项目级 [s] 跳过 > ')).trim();
59
+ if (line.toLowerCase() === 's') {
60
+ process.stdout.write('已跳过本节。\n');
61
+ return;
62
+ }
63
+ if (line === '') {
64
+ await this.apply(draft.watermark, [], []);
65
+ return;
66
+ }
67
+ const { deleteIdx, promoteIdx } = parseReviewInstruction(line);
68
+ const inRange = (n) => n >= 0 && n < draft.memories.length;
69
+ const deleteIds = deleteIdx.filter(inRange).map(i => draft.memories[i].id);
70
+ const promoteIds = promoteIdx
71
+ .filter(inRange)
72
+ .map(i => draft.memories[i].id)
73
+ .filter(id => !deleteIds.includes(id));
74
+ await this.apply(draft.watermark, deleteIds, promoteIds);
75
+ }
76
+ async apply(watermark, deleteIds, promoteIds) {
77
+ const { deleted, promoted } = await (0, session_memory_api_1.applyMemoryReview)(this.deps.creds, this.deps.sessionId, { watermark, deleteIds, promoteIds });
78
+ process.stdout.write(`已删除 ${deleted} 条,提升 ${promoted} 条到项目级。\n`);
79
+ }
80
+ }
81
+ exports.MemoryReviewSection = MemoryReviewSection;
@@ -46,6 +46,7 @@ const session_attach_1 = require("./commands/session-attach");
46
46
  const session_detach_1 = require("./commands/session-detach");
47
47
  const session_status_1 = require("./commands/session-status");
48
48
  const session_wrap_1 = require("./commands/session-wrap");
49
+ const next_1 = require("./commands/next");
49
50
  const task_context_1 = require("./commands/task-context");
50
51
  const task_create_1 = require("./commands/task-create");
51
52
  const task_update_1 = require("./commands/task-update");
@@ -184,6 +185,11 @@ program
184
185
  .option('--force', 'Overwrite an existing SKILL.md when its contents differ from the bundled version')
185
186
  .option('--agent <token>', 'Coding agent these hooks run under (claude-code, codex, cursor, gemini-cli, github-copilot, windsurf). Baked into every hook command. Defaults to claude-code.')
186
187
  .action(wrap(options => (0, setup_1.setup)(options)));
188
+ program
189
+ .command('next')
190
+ .description('Recommend the next task(s) to work on, ranked by priority, active sprint, and due date. Prints top N (default 3); pick one and run `session attach` + `task context`.')
191
+ .option('-n, --count <N>', 'Number of tasks to recommend (default 3)')
192
+ .action(wrap(options => (0, next_1.nextCommand)(options)));
187
193
  const session = program
188
194
  .command('session')
189
195
  .description('Manage per-terminal coding-session context');
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildMemoryContent = buildMemoryContent;
4
4
  exports.formatMemoryList = formatMemoryList;
5
+ exports.formatMemoryReviewList = formatMemoryReviewList;
5
6
  // Category/field metadata + builders for the `lumo memory` commands.
6
7
  // Mirrors the four content shapes validated server-side by parseMemoryContent.
7
8
  const sanitize_1 = require("./sanitize");
@@ -86,3 +87,9 @@ function formatMemoryList(rows) {
86
87
  })
87
88
  .join('\n');
88
89
  }
90
+ /** Numbered review list: ` N. [SCOPE] CATEGORY headline`. 1-indexed. */
91
+ function formatMemoryReviewList(rows) {
92
+ return rows
93
+ .map((r, i) => ` ${i + 1}. [${r.scope}] ${r.category} ${headline(r.category, r.content)}`)
94
+ .join('\n');
95
+ }
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ /**
3
+ * Pure ranking logic for `lumo next`. Given the caller's open tasks plus the
4
+ * set of ACTIVE sprint ids, produce a recommendation order and a per-task list
5
+ * of human-readable reason factors.
6
+ *
7
+ * Ordering is lexicographic (explainable over a tunable magic score), honoring
8
+ * "priority first": priority → active-sprint membership → dueDate → in-flight
9
+ * status → updatedAt desc tiebreak.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.rankTasks = rankTasks;
13
+ const PRIORITY_WEIGHT = {
14
+ URGENT: 0,
15
+ HIGH: 1,
16
+ MEDIUM: 2,
17
+ LOW: 3,
18
+ };
19
+ function priorityWeight(priority) {
20
+ return PRIORITY_WEIGHT[priority] ?? 99;
21
+ }
22
+ /** Earlier due first; missing/invalid dueDate sorts last. */
23
+ function dueValue(dueDate) {
24
+ if (!dueDate)
25
+ return Number.POSITIVE_INFINITY;
26
+ const t = new Date(dueDate).getTime();
27
+ return Number.isNaN(t) ? Number.POSITIVE_INFINITY : t;
28
+ }
29
+ const IN_FLIGHT = new Set(['IN_PROGRESS', 'IN_REVIEW']);
30
+ /** in-flight (0) before TODO (1) before anything else (2). */
31
+ function statusRank(status) {
32
+ if (IN_FLIGHT.has(status))
33
+ return 0;
34
+ if (status === 'TODO')
35
+ return 1;
36
+ return 2;
37
+ }
38
+ function inActiveSprint(task, activeSprintIds) {
39
+ return task.sprintId !== null && activeSprintIds.has(task.sprintId);
40
+ }
41
+ function deriveReasons(task, activeSprintIds, now) {
42
+ const reasons = [task.priority];
43
+ if (inActiveSprint(task, activeSprintIds)) {
44
+ reasons.push('active sprint');
45
+ }
46
+ if (task.dueDate) {
47
+ const day = task.dueDate.slice(0, 10);
48
+ const due = new Date(task.dueDate).getTime();
49
+ const overdue = !Number.isNaN(due) && due < now.getTime();
50
+ reasons.push(overdue ? `due ${day} (overdue)` : `due ${day}`);
51
+ }
52
+ if (task.status === 'IN_PROGRESS')
53
+ reasons.push('in progress');
54
+ else if (task.status === 'IN_REVIEW')
55
+ reasons.push('in review');
56
+ return reasons;
57
+ }
58
+ function rankTasks(tasks, activeSprintIds, now) {
59
+ const sorted = [...tasks].sort((a, b) => {
60
+ const pw = priorityWeight(a.priority) - priorityWeight(b.priority);
61
+ if (pw !== 0)
62
+ return pw;
63
+ const sa = inActiveSprint(a, activeSprintIds) ? 0 : 1;
64
+ const sb = inActiveSprint(b, activeSprintIds) ? 0 : 1;
65
+ if (sa !== sb)
66
+ return sa - sb;
67
+ const dv = dueValue(a.dueDate) - dueValue(b.dueDate);
68
+ if (dv !== 0 && !Number.isNaN(dv))
69
+ return dv;
70
+ const st = statusRank(a.status) - statusRank(b.status);
71
+ if (st !== 0)
72
+ return st;
73
+ return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
74
+ });
75
+ return sorted.map(task => ({
76
+ task,
77
+ identifier: `${task.teamIdentifier}-${task.number}`,
78
+ reasons: deriveReasons(task, activeSprintIds, now),
79
+ }));
80
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchMemoryDraft = fetchMemoryDraft;
4
+ exports.applyMemoryReview = applyMemoryReview;
5
+ const api_1 = require("./api");
6
+ function base(creds) {
7
+ return (0, api_1.trimTrailingSlash)((0, api_1.resolveAuthedApiUrl)(creds.apiUrl));
8
+ }
9
+ /** GET the memory-review draft for the session. Throws on transport / non-200. */
10
+ async function fetchMemoryDraft(creds, sessionId) {
11
+ const url = `${base(creds)}/api/sessions/${encodeURIComponent(sessionId)}/session-memories`;
12
+ const res = await fetch(url, {
13
+ headers: { Authorization: `Bearer ${creds.token}` },
14
+ });
15
+ if (res.status === 401)
16
+ throw new Error('API key invalid or revoked. Run `lumo auth login`.');
17
+ if (!res.ok)
18
+ throw new Error(`memory draft fetch failed (HTTP ${res.status})`);
19
+ return (await res.json());
20
+ }
21
+ /** POST the review (deletes + promotes + watermark). Throws server message on non-2xx. */
22
+ async function applyMemoryReview(creds, sessionId, payload) {
23
+ const url = `${base(creds)}/api/sessions/${encodeURIComponent(sessionId)}/memory-review`;
24
+ const res = await fetch(url, {
25
+ method: 'POST',
26
+ headers: {
27
+ Authorization: `Bearer ${creds.token}`,
28
+ 'Content-Type': 'application/json',
29
+ },
30
+ body: JSON.stringify(payload),
31
+ });
32
+ if (res.status === 401)
33
+ throw new Error('API key invalid or revoked. Run `lumo auth login`.');
34
+ if (!res.ok) {
35
+ let serverMsg = null;
36
+ try {
37
+ const errBody = (await res.json());
38
+ if (typeof errBody.error === 'string')
39
+ serverMsg = errBody.error;
40
+ }
41
+ catch {
42
+ // body wasn't JSON
43
+ }
44
+ throw new Error(serverMsg ?? `memory review failed (HTTP ${res.status})`);
45
+ }
46
+ return (await res.json());
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumoai/cli",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",