@lumoai/cli 1.25.1 → 1.27.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.
@@ -102,7 +102,7 @@ function formatHookStdoutLines(path, responseBody, now = new Date()) {
102
102
  const sessionId = body.sessionId;
103
103
  const tb = body.taskBinding;
104
104
  if (tb && tb.bound === true) {
105
- lines.push(`[Lumo] session_id=${sessionId} | 当前任务: ${tb.taskIdentifier} - ${(0, sanitize_1.sanitizeField)(tb.taskTitle ?? '')}`);
105
+ lines.push(`[Lumo] session_id=${sessionId} | Current task: ${tb.taskIdentifier} - ${(0, sanitize_1.sanitizeField)(tb.taskTitle ?? '')}`);
106
106
  }
107
107
  else if (tb && tb.bound === false) {
108
108
  lines.push(unboundPromptLine(sessionId));
@@ -135,11 +135,11 @@ function formatHookStdoutLines(path, responseBody, now = new Date()) {
135
135
  * no task could be inferred from local git — the user is asked to name one.
136
136
  */
137
137
  function unboundPromptLine(sessionId) {
138
- return `[Lumo] session_id=${sessionId} | 当前未绑定任务。请告诉我你要处理的任务编号(如 LUM-42),或说"跳过"。`;
138
+ return `[Lumo] session_id=${sessionId} | No task bound. Tell me the task you want to work on (e.g. LUM-42), or say "skip".`;
139
139
  }
140
140
  const MAX_UNRESOLVED = 5;
141
141
  /**
142
- * Render the "续上次进度" recovery card from the structured previousSession
142
+ * Render the "resuming previous session" recovery card from the structured previousSession
143
143
  * payload. Returns undefined when there's nothing to show (null payload or an
144
144
  * empty headline). Free text (headline / unresolved) is sanitized here — it's
145
145
  * LLM-generated and routed to Claude Code stdout. unresolved is capped at
@@ -152,18 +152,18 @@ function renderRecoveryCard(prev, taskIdentifier, now) {
152
152
  const ago = (0, format_1.relativeTime)(new Date(prev.lastActivityAt), now);
153
153
  const dur = (0, format_1.formatDuration)(prev.durationMs);
154
154
  const lines = [
155
- `## 续上次进度 (上一段 session · ${ago} · ${dur})`,
156
- `上次干到:${(0, sanitize_1.sanitizeField)(prev.headline)}`,
155
+ `## Resuming previous session (${ago} · ${dur})`,
156
+ `Last stopped at: ${(0, sanitize_1.sanitizeField)(prev.headline)}`,
157
157
  ];
158
158
  const unresolved = Array.isArray(prev.unresolved) ? prev.unresolved : [];
159
159
  if (unresolved.length > 0) {
160
- lines.push('未完成:');
160
+ lines.push('Unfinished:');
161
161
  const shown = unresolved.slice(0, MAX_UNRESOLVED);
162
162
  for (const u of shown)
163
163
  lines.push(`- ${(0, sanitize_1.sanitizeField)(u)}`);
164
164
  const extra = unresolved.length - shown.length;
165
165
  if (extra > 0) {
166
- lines.push(`- …(+${extra} more, lumo task context ${taskIdentifier} 查看全部)`);
166
+ lines.push(`- … (+${extra} more run \`lumo task context ${taskIdentifier}\` for the full list)`);
167
167
  }
168
168
  }
169
169
  return lines.join('\n');
@@ -193,8 +193,8 @@ function sessionContextEnvelope(parts) {
193
193
  * context surface only once the user actually attaches.
194
194
  */
195
195
  function formatSuggestLine(sessionId, match) {
196
- const basis = match.source === 'branch' ? '分支名' : '最近 commit';
197
- return `[Lumo] session_id=${sessionId} | 检测到 ${match.identifier}(依据${basis})。运行 lumo session attach ${match.identifier} 绑定。`;
196
+ const basis = match.source === 'branch' ? 'branch name' : 'recent commits';
197
+ return `[Lumo] session_id=${sessionId} | Detected ${match.identifier} (from ${basis}). Run \`lumo session attach ${match.identifier}\` to bind.`;
198
198
  }
199
199
  /**
200
200
  * Build the stdout lines for a session-start response. When the server reports
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ /**
3
+ * Minimal line-based unified diff (LUM-408, `lumo doc diff`).
4
+ *
5
+ * Pure and dependency-free: the CLI ships only commander + markdown-it, and
6
+ * shelling out to system `diff` would not be portable. Equality (the exit
7
+ * code) is decided on raw bytes by the caller — this renderer only has to
8
+ * make the divergence readable, so a plain LCS with a size guard is enough.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.formatUnifiedDiff = formatUnifiedDiff;
12
+ const CONTEXT_LINES = 3;
13
+ /** Above this old×new line product the LCS table is too big — degrade to a single whole-block hunk. */
14
+ const MAX_LCS_CELLS = 4_000_000;
15
+ function splitLines(text) {
16
+ return text.split('\n');
17
+ }
18
+ /** LCS-based op list for the (already prefix/suffix-trimmed) middle section. */
19
+ function diffOps(oldLines, newLines) {
20
+ const n = oldLines.length;
21
+ const m = newLines.length;
22
+ if (n * m > MAX_LCS_CELLS) {
23
+ return [
24
+ ...oldLines.map(text => ({ type: 'del', text })),
25
+ ...newLines.map(text => ({ type: 'add', text })),
26
+ ];
27
+ }
28
+ // lcs[i][j] = LCS length of oldLines[i:] vs newLines[j:]
29
+ const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
30
+ for (let i = n - 1; i >= 0; i--) {
31
+ for (let j = m - 1; j >= 0; j--) {
32
+ lcs[i][j] =
33
+ oldLines[i] === newLines[j]
34
+ ? lcs[i + 1][j + 1] + 1
35
+ : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
36
+ }
37
+ }
38
+ const ops = [];
39
+ let i = 0;
40
+ let j = 0;
41
+ while (i < n && j < m) {
42
+ if (oldLines[i] === newLines[j]) {
43
+ ops.push({ type: 'ctx', text: oldLines[i] });
44
+ i++;
45
+ j++;
46
+ }
47
+ else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
48
+ ops.push({ type: 'del', text: oldLines[i] });
49
+ i++;
50
+ }
51
+ else {
52
+ ops.push({ type: 'add', text: newLines[j] });
53
+ j++;
54
+ }
55
+ }
56
+ while (i < n)
57
+ ops.push({ type: 'del', text: oldLines[i++] });
58
+ while (j < m)
59
+ ops.push({ type: 'add', text: newLines[j++] });
60
+ return ops;
61
+ }
62
+ /**
63
+ * Render a unified diff between two texts. Returns '' when the line
64
+ * sequences are identical (callers decide byte-equality separately —
65
+ * e.g. a trailing-newline-only difference still flips the exit code).
66
+ */
67
+ function formatUnifiedDiff(oldText, newText, oldLabel, newLabel) {
68
+ const oldAll = splitLines(oldText);
69
+ const newAll = splitLines(newText);
70
+ // Trim the common prefix/suffix so the LCS only sees the changed middle.
71
+ let prefix = 0;
72
+ while (prefix < oldAll.length &&
73
+ prefix < newAll.length &&
74
+ oldAll[prefix] === newAll[prefix]) {
75
+ prefix++;
76
+ }
77
+ let suffix = 0;
78
+ while (suffix < oldAll.length - prefix &&
79
+ suffix < newAll.length - prefix &&
80
+ oldAll[oldAll.length - 1 - suffix] === newAll[newAll.length - 1 - suffix]) {
81
+ suffix++;
82
+ }
83
+ const middleOps = diffOps(oldAll.slice(prefix, oldAll.length - suffix), newAll.slice(prefix, newAll.length - suffix));
84
+ if (!middleOps.some(op => op.type !== 'ctx'))
85
+ return '';
86
+ // Re-attach trimmed context so hunks can carry CONTEXT_LINES around edits.
87
+ const ops = [
88
+ ...oldAll.slice(0, prefix).map(text => ({ type: 'ctx', text })),
89
+ ...middleOps,
90
+ ...oldAll
91
+ .slice(oldAll.length - suffix)
92
+ .map(text => ({ type: 'ctx', text })),
93
+ ];
94
+ // Group ops into hunks: a change plus up to CONTEXT_LINES of context on
95
+ // each side; nearby changes merge into one hunk.
96
+ const lines = [`--- ${oldLabel}`, `+++ ${newLabel}`];
97
+ let oldLineNo = 1;
98
+ let newLineNo = 1;
99
+ let idx = 0;
100
+ while (idx < ops.length) {
101
+ if (ops[idx].type === 'ctx') {
102
+ oldLineNo++;
103
+ newLineNo++;
104
+ idx++;
105
+ continue;
106
+ }
107
+ // Found a change — open a hunk starting CONTEXT_LINES back.
108
+ let hunkStart = idx;
109
+ let ctxBack = 0;
110
+ while (hunkStart > 0 && ops[hunkStart - 1].type === 'ctx' && ctxBack < CONTEXT_LINES) {
111
+ hunkStart--;
112
+ ctxBack++;
113
+ }
114
+ // Extend forward: include changes separated by ≤ 2×CONTEXT_LINES context.
115
+ let hunkEnd = idx;
116
+ let scan = idx;
117
+ while (scan < ops.length) {
118
+ if (ops[scan].type !== 'ctx') {
119
+ hunkEnd = scan;
120
+ scan++;
121
+ continue;
122
+ }
123
+ let run = 0;
124
+ while (scan + run < ops.length && ops[scan + run].type === 'ctx')
125
+ run++;
126
+ if (scan + run < ops.length && run <= CONTEXT_LINES * 2) {
127
+ scan += run;
128
+ continue;
129
+ }
130
+ break;
131
+ }
132
+ const tail = Math.min(hunkEnd + CONTEXT_LINES, ops.length - 1);
133
+ const hunkOps = ops.slice(hunkStart, tail + 1);
134
+ const oldStart = oldLineNo - ctxBack;
135
+ const newStart = newLineNo - ctxBack;
136
+ const oldCount = hunkOps.filter(o => o.type !== 'add').length;
137
+ const newCount = hunkOps.filter(o => o.type !== 'del').length;
138
+ lines.push(`@@ -${oldStart}${oldCount === 1 ? '' : `,${oldCount}`} +${newStart}${newCount === 1 ? '' : `,${newCount}`} @@`);
139
+ for (const op of hunkOps) {
140
+ const marker = op.type === 'ctx' ? ' ' : op.type === 'del' ? '-' : '+';
141
+ lines.push(`${marker}${op.text}`);
142
+ }
143
+ // Advance the line counters across everything the hunk consumed.
144
+ for (let k = idx; k <= tail; k++) {
145
+ const t = ops[k].type;
146
+ if (t !== 'add')
147
+ oldLineNo++;
148
+ if (t !== 'del')
149
+ newLineNo++;
150
+ }
151
+ idx = tail + 1;
152
+ }
153
+ return lines.join('\n');
154
+ }
@@ -7,7 +7,7 @@ async function runWrapPanel(sections, opts) {
7
7
  process.stdout.write(`\n━━ ${section.title} ━━\n`);
8
8
  const hasContent = await section.prepare();
9
9
  if (!hasContent) {
10
- process.stdout.write('(无内容)\n');
10
+ process.stdout.write('(no content)\n');
11
11
  continue;
12
12
  }
13
13
  await section.run(opts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumoai/cli",
3
- "version": "1.25.1",
3
+ "version": "1.27.0",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",