@lumoai/cli 1.7.0 → 1.8.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.
|
@@ -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,9 +105,12 @@ 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 payload at session start.
|
|
108
|
+
// Recovery card + memory + PR-review todos share one additionalContext block
|
|
109
|
+
// so Claude Code injects a single coherent context payload at session start.
|
|
110
|
+
// The card slots in first so it's the first thing the model reads.
|
|
111
|
+
const card = renderRecoveryCard(body.previousSession, tb?.taskIdentifier ?? '', now);
|
|
109
112
|
const envelope = sessionContextEnvelope([
|
|
113
|
+
card,
|
|
110
114
|
body.memorySection,
|
|
111
115
|
body.reviewTodosSection,
|
|
112
116
|
]);
|
|
@@ -121,6 +125,37 @@ function formatHookStdoutLines(path, responseBody) {
|
|
|
121
125
|
function unboundPromptLine(sessionId) {
|
|
122
126
|
return `[Lumo] session_id=${sessionId} | 当前未绑定任务。请告诉我你要处理的任务编号(如 LUM-42),或说"跳过"。`;
|
|
123
127
|
}
|
|
128
|
+
const MAX_UNRESOLVED = 5;
|
|
129
|
+
/**
|
|
130
|
+
* Render the "续上次进度" recovery card from the structured previousSession
|
|
131
|
+
* payload. Returns undefined when there's nothing to show (null payload or an
|
|
132
|
+
* empty headline). Free text (headline / unresolved) is sanitized here — it's
|
|
133
|
+
* LLM-generated and routed to Claude Code stdout. unresolved is capped at
|
|
134
|
+
* MAX_UNRESOLVED with a "+M more" pointer to `lumo task context`.
|
|
135
|
+
*/
|
|
136
|
+
function renderRecoveryCard(prev, taskIdentifier, now) {
|
|
137
|
+
if (!prev || typeof prev.headline !== 'string' || prev.headline === '') {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
const ago = (0, format_1.relativeTime)(new Date(prev.lastActivityAt), now);
|
|
141
|
+
const dur = (0, format_1.formatDuration)(prev.durationMs);
|
|
142
|
+
const lines = [
|
|
143
|
+
`## 续上次进度 (上一段 session · ${ago} · ${dur})`,
|
|
144
|
+
`上次干到:${(0, sanitize_1.sanitizeField)(prev.headline)}`,
|
|
145
|
+
];
|
|
146
|
+
const unresolved = Array.isArray(prev.unresolved) ? prev.unresolved : [];
|
|
147
|
+
if (unresolved.length > 0) {
|
|
148
|
+
lines.push('未完成:');
|
|
149
|
+
const shown = unresolved.slice(0, MAX_UNRESOLVED);
|
|
150
|
+
for (const u of shown)
|
|
151
|
+
lines.push(`- ${(0, sanitize_1.sanitizeField)(u)}`);
|
|
152
|
+
const extra = unresolved.length - shown.length;
|
|
153
|
+
if (extra > 0) {
|
|
154
|
+
lines.push(`- …(+${extra} more, 跑 lumo task context ${taskIdentifier} 查看全部)`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return lines.join('\n');
|
|
158
|
+
}
|
|
124
159
|
/**
|
|
125
160
|
* Wrap any non-empty context parts into a single SessionStart
|
|
126
161
|
* hookSpecificOutput envelope so Claude Code injects one coherent
|
|
@@ -153,7 +188,7 @@ function formatAutoBindLine(sessionId, match, result) {
|
|
|
153
188
|
* bound task's memory). Falls back to the unbound prompt when nothing matches
|
|
154
189
|
* or the bind fails. Bound sessions reuse the existing formatting.
|
|
155
190
|
*/
|
|
156
|
-
async function resolveSessionStartStdout(responseBody, deps) {
|
|
191
|
+
async function resolveSessionStartStdout(responseBody, deps, now = new Date()) {
|
|
157
192
|
if (responseBody == null || typeof responseBody !== 'object')
|
|
158
193
|
return [];
|
|
159
194
|
const body = responseBody;
|
|
@@ -162,7 +197,7 @@ async function resolveSessionStartStdout(responseBody, deps) {
|
|
|
162
197
|
const sessionId = body.sessionId;
|
|
163
198
|
const tb = body.taskBinding;
|
|
164
199
|
if (tb && tb.bound === true) {
|
|
165
|
-
return formatHookStdoutLines('session-start', responseBody);
|
|
200
|
+
return formatHookStdoutLines('session-start', responseBody, now);
|
|
166
201
|
}
|
|
167
202
|
if (!tb || tb.bound !== false)
|
|
168
203
|
return [];
|
|
@@ -173,7 +208,11 @@ async function resolveSessionStartStdout(responseBody, deps) {
|
|
|
173
208
|
if (!result.ok)
|
|
174
209
|
return [unboundPromptLine(sessionId)];
|
|
175
210
|
const lines = [formatAutoBindLine(sessionId, match, result)];
|
|
176
|
-
const
|
|
211
|
+
const card = renderRecoveryCard(result.previousSession, result.taskIdentifier ?? match.identifier, now);
|
|
212
|
+
// bind-task only returns memory + previousSession (not reviewTodosSection) —
|
|
213
|
+
// the auto-bind endpoint never built PR-review todos. Keep that scope here;
|
|
214
|
+
// surfacing review todos on the auto-bind path is a separate change.
|
|
215
|
+
const envelope = sessionContextEnvelope([card, result.memorySection]);
|
|
177
216
|
if (envelope)
|
|
178
217
|
lines.push(envelope);
|
|
179
218
|
return lines;
|
|
@@ -207,6 +246,7 @@ async function postBindTask(sessionId, identifier, token, apiUrl) {
|
|
|
207
246
|
taskIdentifier: body.taskIdentifier,
|
|
208
247
|
taskTitle: body.taskTitle,
|
|
209
248
|
memorySection: body.memorySection,
|
|
249
|
+
previousSession: body.previousSession ?? undefined,
|
|
210
250
|
};
|
|
211
251
|
}
|
|
212
252
|
catch (err) {
|