@lumoai/cli 1.5.1 → 1.6.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", "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 文档".'
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", "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", "进度评论".'
4
4
  ---
5
5
 
6
6
  ## Prerequisites
@@ -122,13 +122,16 @@ The command prints a markdown document to stdout containing:
122
122
 
123
123
  1. **Task header** — identifier, title, status, description
124
124
  2. **Memory section** — cross-session learnings accumulated over prior sessions; treat as trusted background context
125
- 3. **Previous sessions** — ordered newest-first, each with:
125
+ 3. **Inline source cards** — Slack / web / Figma / artifacts / documents / comments / Pull Requests (see "Context Retrieval" below)
126
+ 4. **PR Review 待办** — mirrored PR review comments as a checkbox todo list: each line-level reviewer comment (shown as `` `file:line` `` + the reviewer's ask + a link to the GitHub comment) and each `changes_requested` review summary (shown as "🛑 整体要求改动"). Present only when the task's PR(s) have review comments. This same block is **auto-injected at session start** (alongside the memory section) when the session is bound to a task — so reviewer asks surface without re-running `task context`.
127
+ 5. **Previous sessions** — ordered newest-first, each with:
126
128
  - A headline summary of what was done
127
129
  - Unresolved items (carry-over TODOs from that session)
128
130
 
129
131
  ### How to use the context
130
132
 
131
133
  - **Unresolved items** from the most recent session are the highest-priority carry-overs — address them before starting new work unless the user says otherwise
134
+ - **PR Review 待办** items are reviewer-requested changes — treat each unchecked box as a TODO to resolve, then reply on the PR (a Lumo comment mirrors back to GitHub)
132
135
  - **Memory section** provides validated context that persists across sessions — use it to avoid re-learning decisions or constraints
133
136
  - Focus on the **most recent 1–2 sessions** for relevant state; older sessions are for historical reference only
134
137
  - If there are **no prior sessions**, this is a fresh start — read the task description carefully and ask clarifying questions if needed
@@ -1139,6 +1142,32 @@ lumo session detach
1139
1142
 
1140
1143
  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).
1141
1144
 
1145
+ ### `lumo session wrap [--yes] [--dry-run]` — draft + post a progress comment at wrap-up
1146
+
1147
+ Session-end wrap-up panel. Reads back the current Claude Code session's per-turn
1148
+ `turnSummary` rows (the one-line Chinese summaries written each STOP), aggregates
1149
+ every turn **since the last progress comment** into one bulleted body, and — after
1150
+ a `[y] 发送 / [e] 编辑 / [s] 跳过` confirmation — posts it as a comment on the
1151
+ session's bound task. A server-side watermark (`Session.lastProgressCommentAt`)
1152
+ means re-running never re-posts the same turns.
1153
+
1154
+ ```bash
1155
+ lumo session wrap # interactive: preview draft, choose y / e / s
1156
+ lumo session wrap --yes # post the drafted body without prompting (agent-friendly)
1157
+ lumo session wrap --dry-run # print the draft only; never posts, never advances watermark
1158
+ ```
1159
+
1160
+ - Requires `$CLAUDE_CODE_SESSION_ID` (must run inside Claude Code) and a bound
1161
+ task (`lumo session attach <LUM-N>` first). With no bound task or no new turn
1162
+ summaries, the panel prints "(无内容)" and posts nothing.
1163
+ - `[e] 编辑` opens `$EDITOR` (fallback vi/nano) on the drafted body; the edited
1164
+ text is posted and the watermark still advances to the turns the draft covered.
1165
+ - Non-TTY without `--yes`: prints the draft and does **not** post (safe default).
1166
+
1167
+ When to suggest: at the end of a working session on a bound task, to record what
1168
+ was done as a progress comment — offer `lumo session wrap` rather than composing
1169
+ a `task comment` by hand.
1170
+
1142
1171
  ### When to suggest session binding
1143
1172
 
1144
1173
  - If the user mentions a task ID (e.g., "let's work on LUM-42") and no session is currently bound, **suggest running `lumo session attach`**.
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sessionWrap = sessionWrap;
4
+ const config_1 = require("../lib/config");
5
+ const wrap_panel_1 = require("../lib/wrap-panel");
6
+ const progress_comment_section_1 = require("./wrap/progress-comment-section");
7
+ /**
8
+ * `lumo session wrap [--yes] [--dry-run]`
9
+ *
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.
14
+ */
15
+ async function sessionWrap(options) {
16
+ const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
17
+ if (!sessionId) {
18
+ console.error('Error: $CLAUDE_CODE_SESSION_ID is not set.\n' +
19
+ '`lumo session wrap` must be run inside a Claude Code session.');
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 sections = [new progress_comment_section_1.ProgressCommentSection({ creds, sessionId })];
28
+ await (0, wrap_panel_1.runWrapPanel)(sections, {
29
+ yes: options.yes === true,
30
+ dryRun: options.dryRun === true,
31
+ });
32
+ }
@@ -99,6 +99,11 @@ function formatTaskContextMarkdown(data, now) {
99
99
  lines.push((0, sanitize_1.sanitizeField)(data.prSection.trimEnd()));
100
100
  lines.push('');
101
101
  }
102
+ if (data.prReviewTodosSection &&
103
+ data.prReviewTodosSection.trim().length > 0) {
104
+ lines.push((0, sanitize_1.sanitizeField)(data.prReviewTodosSection.trimEnd()));
105
+ lines.push('');
106
+ }
102
107
  if (data.sessions.length === 0) {
103
108
  lines.push('## Previous Sessions (0)');
104
109
  lines.push('');
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProgressCommentSection = void 0;
4
+ exports.formatProgressBody = formatProgressBody;
5
+ const sanitize_1 = require("../../lib/sanitize");
6
+ const line_prompt_1 = require("../../lib/line-prompt");
7
+ const editor_1 = require("../../lib/editor");
8
+ const progress_comment_api_1 = require("../../lib/progress-comment-api");
9
+ const HEADER = '本次会话进度';
10
+ /** Join turn summaries into a bulleted progress comment body under a header. */
11
+ function formatProgressBody(summaries) {
12
+ return [HEADER, ...summaries.map(s => `- ${s}`)].join('\n');
13
+ }
14
+ /**
15
+ * Wrap-panel section that drafts a progress comment from the session's
16
+ * unposted turnSummaries and posts it after y/e/s confirmation. Holds its own
17
+ * draft + body state between prepare() and run().
18
+ */
19
+ class ProgressCommentSection {
20
+ deps;
21
+ title = '进度评论';
22
+ draft = null;
23
+ body = '';
24
+ constructor(deps) {
25
+ this.deps = deps;
26
+ }
27
+ async prepare() {
28
+ this.draft = await (0, progress_comment_api_1.fetchProgressDraft)(this.deps.creds, this.deps.sessionId);
29
+ if (!this.draft.taskIdentifier || this.draft.summaries.length === 0) {
30
+ return false;
31
+ }
32
+ this.body = formatProgressBody(this.draft.summaries.map(s => s.turnSummary));
33
+ return true;
34
+ }
35
+ async run(opts) {
36
+ const draft = this.draft;
37
+ if (!draft || !draft.watermark)
38
+ return;
39
+ // Preview: sanitize the server free-text before it hits the terminal.
40
+ process.stdout.write(`将发到 ${draft.taskIdentifier} "${(0, sanitize_1.sanitizeField)(draft.taskTitle ?? '')}":\n`);
41
+ process.stdout.write(`${(0, sanitize_1.sanitizeField)(this.body)}\n`);
42
+ if (opts.dryRun) {
43
+ process.stdout.write('(dry-run,未发送)\n');
44
+ return;
45
+ }
46
+ if (opts.yes) {
47
+ await this.post(draft.watermark, this.body);
48
+ return;
49
+ }
50
+ const choice = (await (0, line_prompt_1.promptLine)('[y] 发送 [e] 编辑 [s] 跳过 > ')).toLowerCase();
51
+ if (choice === 's' || choice === '') {
52
+ process.stdout.write('已跳过。\n');
53
+ return;
54
+ }
55
+ if (choice === 'e') {
56
+ const edited = (await (0, editor_1.editInEditor)(this.body)).trim();
57
+ if (edited.length === 0) {
58
+ process.stdout.write('正文为空,已跳过。\n');
59
+ return;
60
+ }
61
+ process.stdout.write(`${(0, sanitize_1.sanitizeField)(edited)}\n`);
62
+ const confirm = (await (0, line_prompt_1.promptLine)('[y] 发送 [s] 跳过 > ')).toLowerCase();
63
+ if (confirm !== 'y') {
64
+ process.stdout.write('已跳过。\n');
65
+ return;
66
+ }
67
+ await this.post(draft.watermark, edited);
68
+ return;
69
+ }
70
+ if (choice === 'y') {
71
+ await this.post(draft.watermark, this.body);
72
+ return;
73
+ }
74
+ process.stdout.write('无法识别的选择,已跳过。\n');
75
+ }
76
+ async post(watermark, body) {
77
+ const { commentId } = await (0, progress_comment_api_1.postProgressComment)(this.deps.creds, this.deps.sessionId, { body, watermark });
78
+ process.stdout.write(`已发送进度评论 (comment ${commentId})\n`);
79
+ }
80
+ }
81
+ exports.ProgressCommentSection = ProgressCommentSection;
@@ -45,6 +45,7 @@ const hook_1 = require("./commands/hook");
45
45
  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
+ const session_wrap_1 = require("./commands/session-wrap");
48
49
  const task_context_1 = require("./commands/task-context");
49
50
  const task_create_1 = require("./commands/task-create");
50
51
  const task_update_1 = require("./commands/task-update");
@@ -195,6 +196,12 @@ session
195
196
  .command('detach')
196
197
  .description('Clear the task binding on the current Claude Code session. Past hook events keep their taskId; only future events become untagged.')
197
198
  .action(wrap(() => (0, session_detach_1.sessionDetach)()));
199
+ session
200
+ .command('wrap')
201
+ .description("Session-end wrap-up: draft a progress comment from this session's turn summaries and post it to the bound task after confirmation.")
202
+ .option('-y, --yes', 'Post the drafted comment without prompting (agent-friendly)')
203
+ .option('--dry-run', 'Print the draft but do not post or advance the watermark')
204
+ .action(wrap(options => (0, session_wrap_1.sessionWrap)(options)));
198
205
  const task = program
199
206
  .command('task')
200
207
  .description('Inspect tasks from the terminal');
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.editInEditor = editInEditor;
37
+ const child_process_1 = require("child_process");
38
+ const fs = __importStar(require("fs"));
39
+ const os = __importStar(require("os"));
40
+ const path = __importStar(require("path"));
41
+ /**
42
+ * Open `initial` in the user's $EDITOR (fallback vi → nano), return the edited
43
+ * text. Degrades gracefully: when stdin is not a TTY (piped / agent) or the
44
+ * editor exits non-zero, the original text is returned unchanged so the caller
45
+ * can proceed without an interactive editor.
46
+ */
47
+ async function editInEditor(initial) {
48
+ if (!process.stdin.isTTY)
49
+ return initial;
50
+ const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
51
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumo-wrap-'));
52
+ const file = path.join(dir, 'progress.md');
53
+ try {
54
+ fs.writeFileSync(file, initial, 'utf8');
55
+ const result = (0, child_process_1.spawnSync)(editor, [file], { stdio: 'inherit' });
56
+ if (result.status !== 0)
57
+ return initial;
58
+ return fs.readFileSync(file, 'utf8');
59
+ }
60
+ catch {
61
+ return initial;
62
+ }
63
+ finally {
64
+ fs.rmSync(dir, { recursive: true, force: true });
65
+ }
66
+ }
@@ -51,16 +51,40 @@ function readStdin() {
51
51
  }
52
52
  /**
53
53
  * Build the array of stdout lines to emit for a given hook path + response
54
- * body. Returns an empty array for any path other than 'session-start'.
54
+ * body. Returns an empty array for any path that emits nothing.
55
55
  *
56
56
  * For 'session-start' the array contains:
57
57
  * [0] plain-text bind/unbound status line (always present when taskBinding exists)
58
- * [1] (optional) hookSpecificOutput JSON when memorySection is a non-empty string
58
+ * [1] (optional) hookSpecificOutput JSON when there is any additionalContext
59
+ * the memory section and the PR-review-todos section, concatenated.
59
60
  *
60
- * The JSON on line [1] conforms to Claude Code's hookSpecificOutput envelope so
61
- * the runtime injects additionalContext into the conversation automatically.
61
+ * For 'pre-tool-use' the array contains:
62
+ * [0] (optional) a PreToolUse hookSpecificOutput JSON carrying the parallel-
63
+ * edit collision warning as additionalContext, when the server returned a
64
+ * `collisionWarning` (LUM-150 step ③). Empty otherwise.
65
+ *
66
+ * The JSON lines conform to Claude Code's hookSpecificOutput envelope so the
67
+ * runtime injects additionalContext into the conversation automatically.
62
68
  */
63
69
  function formatHookStdoutLines(path, responseBody) {
70
+ if (path === 'pre-tool-use') {
71
+ if (responseBody == null || typeof responseBody !== 'object')
72
+ return [];
73
+ const warning = responseBody
74
+ .collisionWarning;
75
+ if (typeof warning !== 'string' || warning === '')
76
+ return [];
77
+ return [
78
+ JSON.stringify({
79
+ hookSpecificOutput: {
80
+ hookEventName: 'PreToolUse',
81
+ // Server-built text routed back to stdout — sanitize untrusted free
82
+ // text before Claude Code consumes it (ANSI/control-char injection).
83
+ additionalContext: (0, sanitize_1.sanitizeField)(warning),
84
+ },
85
+ }),
86
+ ];
87
+ }
64
88
  if (path !== 'session-start')
65
89
  return [];
66
90
  if (responseBody == null || typeof responseBody !== 'object')
@@ -77,11 +101,14 @@ function formatHookStdoutLines(path, responseBody) {
77
101
  else if (tb && tb.bound === false) {
78
102
  lines.push(`[Lumo] session_id=${sessionId} | 当前未绑定任务。请告诉我你要处理的任务编号(如 LUM-42),或说"跳过"。`);
79
103
  }
80
- if (typeof body.memorySection === 'string' && body.memorySection !== '') {
104
+ // Memory + PR-review todos share one additionalContext block so Claude Code
105
+ // injects a single coherent context payload at session start.
106
+ const contextParts = [body.memorySection, body.reviewTodosSection].filter((s) => typeof s === 'string' && s !== '');
107
+ if (contextParts.length > 0) {
81
108
  lines.push(JSON.stringify({
82
109
  hookSpecificOutput: {
83
110
  hookEventName: 'SessionStart',
84
- additionalContext: body.memorySection,
111
+ additionalContext: contextParts.join('\n\n'),
85
112
  },
86
113
  }));
87
114
  }
@@ -145,9 +172,11 @@ async function runHookWithBody(path, body, agentToken) {
145
172
  if (!res.ok) {
146
173
  (0, hook_log_1.logHookError)(`[${path}]`, `HTTP ${res.status} from ${url}`);
147
174
  }
148
- else if (path === 'session-start') {
149
- // Per-hook side effects fire only after a 2xx response so a transient
150
- // server failure doesn't desync local state from server state.
175
+ else if (path === 'session-start' || path === 'pre-tool-use') {
176
+ // Paths that turn the response body into stdout for Claude Code:
177
+ // session-start bind status + injected context
178
+ // pre-tool-use → parallel-edit collision warning (LUM-150 ③)
179
+ // Only after a 2xx so a transient server failure emits nothing.
151
180
  try {
152
181
  const responseBody = await res.json();
153
182
  for (const line of formatHookStdoutLines(path, responseBody)) {
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchProgressDraft = fetchProgressDraft;
4
+ exports.postProgressComment = postProgressComment;
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 unposted progress draft for the session. Throws on transport / non-200. */
10
+ async function fetchProgressDraft(creds, sessionId) {
11
+ const url = `${base(creds)}/api/sessions/${encodeURIComponent(sessionId)}/turn-summaries`;
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(`progress draft fetch failed (HTTP ${res.status})`);
19
+ return (await res.json());
20
+ }
21
+ /** POST the (possibly edited) body + watermark. Throws the server message on non-201. */
22
+ async function postProgressComment(creds, sessionId, payload) {
23
+ const url = `${base(creds)}/api/sessions/${encodeURIComponent(sessionId)}/progress-comment`;
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.status !== 201) {
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 ?? `progress comment failed (HTTP ${res.status})`);
45
+ }
46
+ return (await res.json());
47
+ }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runWrapPanel = runWrapPanel;
4
+ /** Run each section in order; print its title, prepare, then run if it has content. */
5
+ async function runWrapPanel(sections, opts) {
6
+ for (const section of sections) {
7
+ process.stdout.write(`\n━━ ${section.title} ━━\n`);
8
+ const hasContent = await section.prepare();
9
+ if (!hasContent) {
10
+ process.stdout.write('(无内容)\n');
11
+ continue;
12
+ }
13
+ await section.run(opts);
14
+ }
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumoai/cli",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",