@lumoai/cli 1.5.1 → 1.7.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
@@ -1099,6 +1102,27 @@ across multiple tasks → consider `lumo memory promote`.
1099
1102
 
1100
1103
  ## Session Management
1101
1104
 
1105
+ ### Auto-bind at session start (from local git)
1106
+
1107
+ When a session starts **without** a bound task, the `session-start` hook tries to
1108
+ infer the task from local git before falling back to the "请告诉我任务编号" prompt:
1109
+
1110
+ - It reads the **current branch name** first (e.g. `lumo/LUM-145-...`), then the
1111
+ **most recent commit subjects** (e.g. `... [LUM-145]`), extracting the first
1112
+ `LUM-<n>`.
1113
+ - On a hit it binds the session to that task automatically (same `bind-task`
1114
+ endpoint as `session attach`) and prints a single line:
1115
+ `已自动绑定 LUM-145 - <title>(依据分支名/最近 commit)。如果不对,回复"不是"我就帮你解绑。`
1116
+ The freshly-bound task's memory is injected too.
1117
+ - No match (detached HEAD, a non-lumo branch with no tagged commits, not a git
1118
+ repo) or a failed bind (unknown task) → it degrades silently to the normal
1119
+ unbound prompt.
1120
+
1121
+ **Agent guidance:** if the user responds "不是" / "不对" / "wrong task" to an
1122
+ auto-bind line, run `lumo session detach` to clear the binding (then `session
1123
+ attach <LUM-N>` if they name the right one). No detach is needed when the
1124
+ auto-bound task is correct.
1125
+
1102
1126
  ### `lumo session attach <identifier>` — bind the current session to a task
1103
1127
 
1104
1128
  Use this whenever the user mentions a task ID. The command is the only way to bind a session to a task.
@@ -1139,6 +1163,32 @@ lumo session detach
1139
1163
 
1140
1164
  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
1165
 
1166
+ ### `lumo session wrap [--yes] [--dry-run]` — draft + post a progress comment at wrap-up
1167
+
1168
+ Session-end wrap-up panel. Reads back the current Claude Code session's per-turn
1169
+ `turnSummary` rows (the one-line Chinese summaries written each STOP), aggregates
1170
+ every turn **since the last progress comment** into one bulleted body, and — after
1171
+ a `[y] 发送 / [e] 编辑 / [s] 跳过` confirmation — posts it as a comment on the
1172
+ session's bound task. A server-side watermark (`Session.lastProgressCommentAt`)
1173
+ means re-running never re-posts the same turns.
1174
+
1175
+ ```bash
1176
+ lumo session wrap # interactive: preview draft, choose y / e / s
1177
+ lumo session wrap --yes # post the drafted body without prompting (agent-friendly)
1178
+ lumo session wrap --dry-run # print the draft only; never posts, never advances watermark
1179
+ ```
1180
+
1181
+ - Requires `$CLAUDE_CODE_SESSION_ID` (must run inside Claude Code) and a bound
1182
+ task (`lumo session attach <LUM-N>` first). With no bound task or no new turn
1183
+ summaries, the panel prints "(无内容)" and posts nothing.
1184
+ - `[e] 编辑` opens `$EDITOR` (fallback vi/nano) on the drafted body; the edited
1185
+ text is posted and the watermark still advances to the turns the draft covered.
1186
+ - Non-TTY without `--yes`: prints the draft and does **not** post (safe default).
1187
+
1188
+ When to suggest: at the end of a working session on a bound task, to record what
1189
+ was done as a progress comment — offer `lumo session wrap` rather than composing
1190
+ a `task comment` by hand.
1191
+
1142
1192
  ### When to suggest session binding
1143
1193
 
1144
1194
  - 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
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.matchTaskIdentifier = matchTaskIdentifier;
4
+ exports.extractTaskFromGit = extractTaskFromGit;
5
+ const child_process_1 = require("child_process");
6
+ /**
7
+ * How many recent commit subjects to scan when the branch name carries no
8
+ * task id (e.g. on a generic feature branch or detached HEAD).
9
+ */
10
+ const DEFAULT_COMMIT_DEPTH = 20;
11
+ const TASK_RE = /LUM-(\d+)/i;
12
+ /**
13
+ * Pull the first `LUM-<n>` token out of arbitrary text (a branch name or a
14
+ * commit subject) and normalize it to upper case. Returns null when no task
15
+ * id is present. The leading `-` in the pattern means the bare word `lumo`
16
+ * never matches.
17
+ */
18
+ function matchTaskIdentifier(text) {
19
+ const m = text.match(TASK_RE);
20
+ return m ? `LUM-${m[1]}` : null;
21
+ }
22
+ /**
23
+ * Run a git subcommand and return trimmed stdout, or '' on any failure
24
+ * (non-zero exit, spawn error, not a repository, timeout). Never throws.
25
+ */
26
+ function gitOutput(args, cwd) {
27
+ try {
28
+ const r = (0, child_process_1.spawnSync)('git', args, {
29
+ cwd,
30
+ encoding: 'utf8',
31
+ timeout: 2000,
32
+ });
33
+ if (r.error || r.status !== 0)
34
+ return '';
35
+ return (r.stdout ?? '').trim();
36
+ }
37
+ catch {
38
+ return '';
39
+ }
40
+ }
41
+ /**
42
+ * Infer the task to work on from local git: prefer the current branch name
43
+ * (e.g. `lumo/LUM-145-...`), then fall back to the most recent commit
44
+ * subjects (e.g. `... [LUM-145]`). Returns null when nothing matches —
45
+ * detached HEAD (branch reads `HEAD`), a non-lumo branch with no tagged
46
+ * commits, or a directory that is not a git repository all degrade to null.
47
+ */
48
+ function extractTaskFromGit(cwd, commitDepth = DEFAULT_COMMIT_DEPTH) {
49
+ const branch = gitOutput(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
50
+ const fromBranch = matchTaskIdentifier(branch);
51
+ if (fromBranch)
52
+ return { identifier: fromBranch, source: 'branch' };
53
+ const log = gitOutput(['log', '-n', String(commitDepth), '--format=%s'], cwd);
54
+ const fromLog = matchTaskIdentifier(log);
55
+ if (fromLog)
56
+ return { identifier: fromLog, source: 'commit' };
57
+ return null;
58
+ }
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formatHookStdoutLines = formatHookStdoutLines;
4
+ exports.formatAutoBindLine = formatAutoBindLine;
5
+ exports.resolveSessionStartStdout = resolveSessionStartStdout;
4
6
  exports.runHook = runHook;
5
7
  exports.runHookWithBody = runHookWithBody;
6
8
  const config_1 = require("./config");
@@ -8,6 +10,7 @@ const api_1 = require("./api");
8
10
  const hook_log_1 = require("./hook-log");
9
11
  const sanitize_1 = require("./sanitize");
10
12
  const agent_1 = require("./agent");
13
+ const git_task_1 = require("./git-task");
11
14
  /**
12
15
  * Hard timeout for the hook POST. On timeout the request is aborted,
13
16
  * logged, and `runHook` exits 0 — Claude Code is never blocked beyond
@@ -51,16 +54,40 @@ function readStdin() {
51
54
  }
52
55
  /**
53
56
  * 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'.
57
+ * body. Returns an empty array for any path that emits nothing.
55
58
  *
56
59
  * For 'session-start' the array contains:
57
60
  * [0] plain-text bind/unbound status line (always present when taskBinding exists)
58
- * [1] (optional) hookSpecificOutput JSON when memorySection is a non-empty string
61
+ * [1] (optional) hookSpecificOutput JSON when there is any additionalContext
62
+ * the memory section and the PR-review-todos section, concatenated.
59
63
  *
60
- * The JSON on line [1] conforms to Claude Code's hookSpecificOutput envelope so
61
- * the runtime injects additionalContext into the conversation automatically.
64
+ * For 'pre-tool-use' the array contains:
65
+ * [0] (optional) a PreToolUse hookSpecificOutput JSON carrying the parallel-
66
+ * edit collision warning as additionalContext, when the server returned a
67
+ * `collisionWarning` (LUM-150 step ③). Empty otherwise.
68
+ *
69
+ * The JSON lines conform to Claude Code's hookSpecificOutput envelope so the
70
+ * runtime injects additionalContext into the conversation automatically.
62
71
  */
63
72
  function formatHookStdoutLines(path, responseBody) {
73
+ if (path === 'pre-tool-use') {
74
+ if (responseBody == null || typeof responseBody !== 'object')
75
+ return [];
76
+ const warning = responseBody
77
+ .collisionWarning;
78
+ if (typeof warning !== 'string' || warning === '')
79
+ return [];
80
+ return [
81
+ JSON.stringify({
82
+ hookSpecificOutput: {
83
+ hookEventName: 'PreToolUse',
84
+ // Server-built text routed back to stdout — sanitize untrusted free
85
+ // text before Claude Code consumes it (ANSI/control-char injection).
86
+ additionalContext: (0, sanitize_1.sanitizeField)(warning),
87
+ },
88
+ }),
89
+ ];
90
+ }
64
91
  if (path !== 'session-start')
65
92
  return [];
66
93
  if (responseBody == null || typeof responseBody !== 'object')
@@ -75,18 +102,121 @@ function formatHookStdoutLines(path, responseBody) {
75
102
  lines.push(`[Lumo] session_id=${sessionId} | 当前任务: ${tb.taskIdentifier} - ${(0, sanitize_1.sanitizeField)(tb.taskTitle ?? '')}`);
76
103
  }
77
104
  else if (tb && tb.bound === false) {
78
- lines.push(`[Lumo] session_id=${sessionId} | 当前未绑定任务。请告诉我你要处理的任务编号(如 LUM-42),或说"跳过"。`);
105
+ lines.push(unboundPromptLine(sessionId));
79
106
  }
80
- if (typeof body.memorySection === 'string' && body.memorySection !== '') {
81
- lines.push(JSON.stringify({
82
- hookSpecificOutput: {
83
- hookEventName: 'SessionStart',
84
- additionalContext: body.memorySection,
85
- },
86
- }));
107
+ // Memory + PR-review todos share one additionalContext block so Claude Code
108
+ // injects a single coherent context payload at session start.
109
+ const envelope = sessionContextEnvelope([
110
+ body.memorySection,
111
+ body.reviewTodosSection,
112
+ ]);
113
+ if (envelope)
114
+ lines.push(envelope);
115
+ return lines;
116
+ }
117
+ /**
118
+ * The plain-text line shown when a session starts without a bound task and
119
+ * no task could be inferred from local git — the user is asked to name one.
120
+ */
121
+ function unboundPromptLine(sessionId) {
122
+ return `[Lumo] session_id=${sessionId} | 当前未绑定任务。请告诉我你要处理的任务编号(如 LUM-42),或说"跳过"。`;
123
+ }
124
+ /**
125
+ * Wrap any non-empty context parts into a single SessionStart
126
+ * hookSpecificOutput envelope so Claude Code injects one coherent
127
+ * additionalContext payload. Returns null when there is nothing to inject.
128
+ */
129
+ function sessionContextEnvelope(parts) {
130
+ const filled = parts.filter((s) => typeof s === 'string' && s !== '');
131
+ if (filled.length === 0)
132
+ return null;
133
+ return JSON.stringify({
134
+ hookSpecificOutput: {
135
+ hookEventName: 'SessionStart',
136
+ additionalContext: filled.join('\n\n'),
137
+ },
138
+ });
139
+ }
140
+ /**
141
+ * The single status line printed after a successful auto-bind. Explains which
142
+ * git signal (branch name vs recent commit) it relied on and how to undo it.
143
+ */
144
+ function formatAutoBindLine(sessionId, match, result) {
145
+ const basis = match.source === 'branch' ? '分支名' : '最近 commit';
146
+ const identifier = result.taskIdentifier ?? match.identifier;
147
+ return `[Lumo] session_id=${sessionId} | 已自动绑定 ${identifier} - ${(0, sanitize_1.sanitizeField)(result.taskTitle ?? '')}(依据${basis})。如果不对,回复"不是"我就帮你解绑。`;
148
+ }
149
+ /**
150
+ * Build the stdout lines for a session-start response, including the LUM-145
151
+ * auto-bind behavior: when the server reports no binding, infer the task from
152
+ * local git and bind it; on a hit, print the auto-bind line (plus the freshly
153
+ * bound task's memory). Falls back to the unbound prompt when nothing matches
154
+ * or the bind fails. Bound sessions reuse the existing formatting.
155
+ */
156
+ async function resolveSessionStartStdout(responseBody, deps) {
157
+ if (responseBody == null || typeof responseBody !== 'object')
158
+ return [];
159
+ const body = responseBody;
160
+ if (!body.sessionId)
161
+ return [];
162
+ const sessionId = body.sessionId;
163
+ const tb = body.taskBinding;
164
+ if (tb && tb.bound === true) {
165
+ return formatHookStdoutLines('session-start', responseBody);
87
166
  }
167
+ if (!tb || tb.bound !== false)
168
+ return [];
169
+ const match = deps.extractTask();
170
+ if (!match)
171
+ return [unboundPromptLine(sessionId)];
172
+ const result = await deps.bindTask(sessionId, match.identifier);
173
+ if (!result.ok)
174
+ return [unboundPromptLine(sessionId)];
175
+ const lines = [formatAutoBindLine(sessionId, match, result)];
176
+ const envelope = sessionContextEnvelope([result.memorySection]);
177
+ if (envelope)
178
+ lines.push(envelope);
88
179
  return lines;
89
180
  }
181
+ /**
182
+ * POST the inferred task binding to the same `bind-task` endpoint that
183
+ * `lumo session attach` uses. Guarded by the same short timeout as the hook
184
+ * POST so the extra round trip can never make session-start hang. Any
185
+ * failure (404 unknown task, network, timeout, non-2xx) returns
186
+ * `{ ok: false }`, which routes the caller back to the unbound prompt.
187
+ */
188
+ async function postBindTask(sessionId, identifier, token, apiUrl) {
189
+ const controller = new AbortController();
190
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
191
+ try {
192
+ const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/sessions/${encodeURIComponent(sessionId)}/bind-task`;
193
+ const res = await fetch(url, {
194
+ method: 'POST',
195
+ headers: {
196
+ 'Content-Type': 'application/json',
197
+ Authorization: `Bearer ${token}`,
198
+ },
199
+ body: JSON.stringify({ taskIdentifier: identifier }),
200
+ signal: controller.signal,
201
+ });
202
+ if (!res.ok)
203
+ return { ok: false };
204
+ const body = (await res.json());
205
+ return {
206
+ ok: true,
207
+ taskIdentifier: body.taskIdentifier,
208
+ taskTitle: body.taskTitle,
209
+ memorySection: body.memorySection,
210
+ };
211
+ }
212
+ catch (err) {
213
+ (0, hook_log_1.logHookError)('[session-start] auto-bind', err);
214
+ return { ok: false };
215
+ }
216
+ finally {
217
+ clearTimeout(timer);
218
+ }
219
+ }
90
220
  /**
91
221
  * POST the hook body to /api/hooks/<path> with a short timeout. All errors
92
222
  * — credential missing, network failure, timeout, non-2xx — are routed to
@@ -145,12 +275,21 @@ async function runHookWithBody(path, body, agentToken) {
145
275
  if (!res.ok) {
146
276
  (0, hook_log_1.logHookError)(`[${path}]`, `HTTP ${res.status} from ${url}`);
147
277
  }
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.
278
+ else if (path === 'session-start' || path === 'pre-tool-use') {
279
+ // Paths that turn the response body into stdout for Claude Code:
280
+ // session-start bind status + injected context (incl. LUM-145
281
+ // auto-bind from local git when unbound)
282
+ // pre-tool-use → parallel-edit collision warning (LUM-150 ③)
283
+ // Only after a 2xx so a transient server failure emits nothing.
151
284
  try {
152
285
  const responseBody = await res.json();
153
- for (const line of formatHookStdoutLines(path, responseBody)) {
286
+ const lines = path === 'session-start'
287
+ ? await resolveSessionStartStdout(responseBody, {
288
+ extractTask: () => (0, git_task_1.extractTaskFromGit)(),
289
+ bindTask: (sessionId, identifier) => postBindTask(sessionId, identifier, creds.token, apiUrl),
290
+ })
291
+ : formatHookStdoutLines(path, responseBody);
292
+ for (const line of lines) {
154
293
  process.stdout.write(line + '\n');
155
294
  }
156
295
  }
@@ -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.7.0",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",