@lumoai/cli 1.7.0 → 1.9.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
@@ -124,6 +124,7 @@ The command prints a markdown document to stdout containing:
124
124
  2. **Memory section** — cross-session learnings accumulated over prior sessions; treat as trusted background context
125
125
  3. **Inline source cards** — Slack / web / Figma / artifacts / documents / comments / Pull Requests (see "Context Retrieval" below)
126
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
+ - The **inline source cards** (Slack / web / Figma / PR) are likewise **auto-injected at session start** when the session is bound, under a single global token budget shared with the memory section (priority: memory > PR > Slack > Figma > web). Cards that don't fit the budget are degraded to a one-line manifest carrying just the title and its `lumo task … show` retrieval command — so you still know they exist and can pull the full content on demand.
127
128
  5. **Previous sessions** — ordered newest-first, each with:
128
129
  - A headline summary of what was done
129
130
  - Unresolved items (carry-over TODOs from that session)
@@ -5,6 +5,7 @@ exports.formatTaskContextMarkdown = formatTaskContextMarkdown;
5
5
  const config_1 = require("../lib/config");
6
6
  const api_1 = require("../lib/api");
7
7
  const sanitize_1 = require("../lib/sanitize");
8
+ const format_1 = require("../lib/format");
8
9
  async function taskContext(identifier) {
9
10
  if (!identifier) {
10
11
  console.error('Error: missing <identifier>. Usage: lumo task context <LUM-42>');
@@ -115,8 +116,8 @@ function formatTaskContextMarkdown(data, now) {
115
116
  lines.push('');
116
117
  for (const s of data.sessions) {
117
118
  const shortId = s.id.slice(0, 8);
118
- const ago = relativeTime(new Date(s.lastActivityAt), now);
119
- const dur = formatDuration(s.durationMs);
119
+ const ago = (0, format_1.relativeTime)(new Date(s.lastActivityAt), now);
120
+ const dur = (0, format_1.formatDuration)(s.durationMs);
120
121
  lines.push(`### Session ${shortId} · ${ago} · ${dur}`);
121
122
  lines.push(`**Summary**: ${(0, sanitize_1.sanitizeField)(s.headline)}`);
122
123
  if (s.unresolved.length > 0) {
@@ -128,36 +129,3 @@ function formatTaskContextMarkdown(data, now) {
128
129
  }
129
130
  return lines.join('\n');
130
131
  }
131
- function formatDuration(ms) {
132
- if (ms < 60_000) {
133
- return `${Math.max(1, Math.round(ms / 1000))}s`;
134
- }
135
- const totalMinutes = Math.round(ms / 60_000);
136
- if (totalMinutes < 60)
137
- return `${totalMinutes}min`;
138
- const hours = Math.floor(totalMinutes / 60);
139
- const minutes = totalMinutes % 60;
140
- return minutes === 0 ? `${hours}h` : `${hours}h ${minutes}min`;
141
- }
142
- function relativeTime(then, now) {
143
- const diffMs = now.getTime() - then.getTime();
144
- const seconds = Math.floor(diffMs / 1000);
145
- if (seconds < 60)
146
- return 'just now';
147
- const minutes = Math.floor(seconds / 60);
148
- if (minutes < 60)
149
- return `${minutes}min ago`;
150
- const hours = Math.floor(minutes / 60);
151
- if (hours < 24)
152
- return `${hours}h ago`;
153
- const days = Math.floor(hours / 24);
154
- if (days === 1)
155
- return 'yesterday';
156
- if (days < 7)
157
- return `${days} days ago`;
158
- const weeks = Math.floor(days / 7);
159
- if (weeks < 5)
160
- return `${weeks}w ago`;
161
- const months = Math.floor(days / 30);
162
- return `${months}mo ago`;
163
- }
@@ -1,7 +1,46 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatDuration = formatDuration;
4
+ exports.relativeTime = relativeTime;
3
5
  exports.formatTaskListTable = formatTaskListTable;
4
6
  const sanitize_1 = require("./sanitize");
7
+ /**
8
+ * Shared CLI time/duration formatters. Extracted from task-context.ts so the
9
+ * session-start recovery card (hook-runner.ts) can reuse the exact same shape.
10
+ */
11
+ function formatDuration(ms) {
12
+ if (ms < 60_000) {
13
+ return `${Math.max(1, Math.round(ms / 1000))}s`;
14
+ }
15
+ const totalMinutes = Math.round(ms / 60_000);
16
+ if (totalMinutes < 60)
17
+ return `${totalMinutes}min`;
18
+ const hours = Math.floor(totalMinutes / 60);
19
+ const minutes = totalMinutes % 60;
20
+ return minutes === 0 ? `${hours}h` : `${hours}h ${minutes}min`;
21
+ }
22
+ function relativeTime(then, now) {
23
+ const diffMs = now.getTime() - then.getTime();
24
+ const seconds = Math.floor(diffMs / 1000);
25
+ if (seconds < 60)
26
+ return 'just now';
27
+ const minutes = Math.floor(seconds / 60);
28
+ if (minutes < 60)
29
+ return `${minutes}min ago`;
30
+ const hours = Math.floor(minutes / 60);
31
+ if (hours < 24)
32
+ return `${hours}h ago`;
33
+ const days = Math.floor(hours / 24);
34
+ if (days === 1)
35
+ return 'yesterday';
36
+ if (days < 7)
37
+ return `${days} days ago`;
38
+ const weeks = Math.floor(days / 7);
39
+ if (weeks < 5)
40
+ return `${weeks}w ago`;
41
+ const months = Math.floor(days / 30);
42
+ return `${months}mo ago`;
43
+ }
5
44
  /**
6
45
  * Render a task list as a fixed-width table. Each row:
7
46
  * LUM-42 IN_PROGRESS HIGH project-name Title here
@@ -11,6 +11,7 @@ const hook_log_1 = require("./hook-log");
11
11
  const sanitize_1 = require("./sanitize");
12
12
  const agent_1 = require("./agent");
13
13
  const git_task_1 = require("./git-task");
14
+ const format_1 = require("./format");
14
15
  /**
15
16
  * Hard timeout for the hook POST. On timeout the request is aborted,
16
17
  * logged, and `runHook` exits 0 — Claude Code is never blocked beyond
@@ -69,7 +70,7 @@ function readStdin() {
69
70
  * The JSON lines conform to Claude Code's hookSpecificOutput envelope so the
70
71
  * runtime injects additionalContext into the conversation automatically.
71
72
  */
72
- function formatHookStdoutLines(path, responseBody) {
73
+ function formatHookStdoutLines(path, responseBody, now = new Date()) {
73
74
  if (path === 'pre-tool-use') {
74
75
  if (responseBody == null || typeof responseBody !== 'object')
75
76
  return [];
@@ -104,10 +105,15 @@ function formatHookStdoutLines(path, responseBody) {
104
105
  else if (tb && tb.bound === false) {
105
106
  lines.push(unboundPromptLine(sessionId));
106
107
  }
107
- // Memory + PR-review todos share one additionalContext block so Claude Code
108
- // injects a single coherent context payload at session start.
108
+ // Recovery card + memory + linked resources + PR-review todos share one
109
+ // additionalContext block so Claude Code injects a single coherent context
110
+ // payload at session start. The card slots in first so it's the first thing
111
+ // the model reads.
112
+ const card = renderRecoveryCard(body.previousSession, tb?.taskIdentifier ?? '', now);
109
113
  const envelope = sessionContextEnvelope([
114
+ card,
110
115
  body.memorySection,
116
+ body.linkedResourcesSection,
111
117
  body.reviewTodosSection,
112
118
  ]);
113
119
  if (envelope)
@@ -121,6 +127,37 @@ function formatHookStdoutLines(path, responseBody) {
121
127
  function unboundPromptLine(sessionId) {
122
128
  return `[Lumo] session_id=${sessionId} | 当前未绑定任务。请告诉我你要处理的任务编号(如 LUM-42),或说"跳过"。`;
123
129
  }
130
+ const MAX_UNRESOLVED = 5;
131
+ /**
132
+ * Render the "续上次进度" recovery card from the structured previousSession
133
+ * payload. Returns undefined when there's nothing to show (null payload or an
134
+ * empty headline). Free text (headline / unresolved) is sanitized here — it's
135
+ * LLM-generated and routed to Claude Code stdout. unresolved is capped at
136
+ * MAX_UNRESOLVED with a "+M more" pointer to `lumo task context`.
137
+ */
138
+ function renderRecoveryCard(prev, taskIdentifier, now) {
139
+ if (!prev || typeof prev.headline !== 'string' || prev.headline === '') {
140
+ return undefined;
141
+ }
142
+ const ago = (0, format_1.relativeTime)(new Date(prev.lastActivityAt), now);
143
+ const dur = (0, format_1.formatDuration)(prev.durationMs);
144
+ const lines = [
145
+ `## 续上次进度 (上一段 session · ${ago} · ${dur})`,
146
+ `上次干到:${(0, sanitize_1.sanitizeField)(prev.headline)}`,
147
+ ];
148
+ const unresolved = Array.isArray(prev.unresolved) ? prev.unresolved : [];
149
+ if (unresolved.length > 0) {
150
+ lines.push('未完成:');
151
+ const shown = unresolved.slice(0, MAX_UNRESOLVED);
152
+ for (const u of shown)
153
+ lines.push(`- ${(0, sanitize_1.sanitizeField)(u)}`);
154
+ const extra = unresolved.length - shown.length;
155
+ if (extra > 0) {
156
+ lines.push(`- …(+${extra} more, 跑 lumo task context ${taskIdentifier} 查看全部)`);
157
+ }
158
+ }
159
+ return lines.join('\n');
160
+ }
124
161
  /**
125
162
  * Wrap any non-empty context parts into a single SessionStart
126
163
  * hookSpecificOutput envelope so Claude Code injects one coherent
@@ -153,7 +190,7 @@ function formatAutoBindLine(sessionId, match, result) {
153
190
  * bound task's memory). Falls back to the unbound prompt when nothing matches
154
191
  * or the bind fails. Bound sessions reuse the existing formatting.
155
192
  */
156
- async function resolveSessionStartStdout(responseBody, deps) {
193
+ async function resolveSessionStartStdout(responseBody, deps, now = new Date()) {
157
194
  if (responseBody == null || typeof responseBody !== 'object')
158
195
  return [];
159
196
  const body = responseBody;
@@ -162,7 +199,7 @@ async function resolveSessionStartStdout(responseBody, deps) {
162
199
  const sessionId = body.sessionId;
163
200
  const tb = body.taskBinding;
164
201
  if (tb && tb.bound === true) {
165
- return formatHookStdoutLines('session-start', responseBody);
202
+ return formatHookStdoutLines('session-start', responseBody, now);
166
203
  }
167
204
  if (!tb || tb.bound !== false)
168
205
  return [];
@@ -173,7 +210,11 @@ async function resolveSessionStartStdout(responseBody, deps) {
173
210
  if (!result.ok)
174
211
  return [unboundPromptLine(sessionId)];
175
212
  const lines = [formatAutoBindLine(sessionId, match, result)];
176
- const envelope = sessionContextEnvelope([result.memorySection]);
213
+ const card = renderRecoveryCard(result.previousSession, result.taskIdentifier ?? match.identifier, now);
214
+ // bind-task only returns memory + previousSession (not reviewTodosSection) —
215
+ // the auto-bind endpoint never built PR-review todos. Keep that scope here;
216
+ // surfacing review todos on the auto-bind path is a separate change.
217
+ const envelope = sessionContextEnvelope([card, result.memorySection]);
177
218
  if (envelope)
178
219
  lines.push(envelope);
179
220
  return lines;
@@ -207,6 +248,7 @@ async function postBindTask(sessionId, identifier, token, apiUrl) {
207
248
  taskIdentifier: body.taskIdentifier,
208
249
  taskTitle: body.taskTitle,
209
250
  memorySection: body.memorySection,
251
+ previousSession: body.previousSession ?? undefined,
210
252
  };
211
253
  }
212
254
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumoai/cli",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",