@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
|
-
//
|
|
108
|
-
// injects a single coherent context
|
|
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
|
|
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) {
|