@lumoai/cli 1.6.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.
package/assets/skill.md
CHANGED
|
@@ -1102,6 +1102,27 @@ across multiple tasks → consider `lumo memory promote`.
|
|
|
1102
1102
|
|
|
1103
1103
|
## Session Management
|
|
1104
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
|
+
|
|
1105
1126
|
### `lumo session attach <identifier>` — bind the current session to a task
|
|
1106
1127
|
|
|
1107
1128
|
Use this whenever the user mentions a task ID. The command is the only way to bind a session to a task.
|
|
@@ -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
|
|
@@ -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,8 @@ 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");
|
|
14
|
+
const format_1 = require("./format");
|
|
11
15
|
/**
|
|
12
16
|
* Hard timeout for the hook POST. On timeout the request is aborted,
|
|
13
17
|
* logged, and `runHook` exits 0 — Claude Code is never blocked beyond
|
|
@@ -66,7 +70,7 @@ function readStdin() {
|
|
|
66
70
|
* The JSON lines conform to Claude Code's hookSpecificOutput envelope so the
|
|
67
71
|
* runtime injects additionalContext into the conversation automatically.
|
|
68
72
|
*/
|
|
69
|
-
function formatHookStdoutLines(path, responseBody) {
|
|
73
|
+
function formatHookStdoutLines(path, responseBody, now = new Date()) {
|
|
70
74
|
if (path === 'pre-tool-use') {
|
|
71
75
|
if (responseBody == null || typeof responseBody !== 'object')
|
|
72
76
|
return [];
|
|
@@ -99,21 +103,160 @@ function formatHookStdoutLines(path, responseBody) {
|
|
|
99
103
|
lines.push(`[Lumo] session_id=${sessionId} | 当前任务: ${tb.taskIdentifier} - ${(0, sanitize_1.sanitizeField)(tb.taskTitle ?? '')}`);
|
|
100
104
|
}
|
|
101
105
|
else if (tb && tb.bound === false) {
|
|
102
|
-
lines.push(
|
|
106
|
+
lines.push(unboundPromptLine(sessionId));
|
|
103
107
|
}
|
|
104
|
-
//
|
|
105
|
-
// injects a single coherent context payload at session start.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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);
|
|
112
|
+
const envelope = sessionContextEnvelope([
|
|
113
|
+
card,
|
|
114
|
+
body.memorySection,
|
|
115
|
+
body.reviewTodosSection,
|
|
116
|
+
]);
|
|
117
|
+
if (envelope)
|
|
118
|
+
lines.push(envelope);
|
|
119
|
+
return lines;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The plain-text line shown when a session starts without a bound task and
|
|
123
|
+
* no task could be inferred from local git — the user is asked to name one.
|
|
124
|
+
*/
|
|
125
|
+
function unboundPromptLine(sessionId) {
|
|
126
|
+
return `[Lumo] session_id=${sessionId} | 当前未绑定任务。请告诉我你要处理的任务编号(如 LUM-42),或说"跳过"。`;
|
|
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;
|
|
114
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
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Wrap any non-empty context parts into a single SessionStart
|
|
161
|
+
* hookSpecificOutput envelope so Claude Code injects one coherent
|
|
162
|
+
* additionalContext payload. Returns null when there is nothing to inject.
|
|
163
|
+
*/
|
|
164
|
+
function sessionContextEnvelope(parts) {
|
|
165
|
+
const filled = parts.filter((s) => typeof s === 'string' && s !== '');
|
|
166
|
+
if (filled.length === 0)
|
|
167
|
+
return null;
|
|
168
|
+
return JSON.stringify({
|
|
169
|
+
hookSpecificOutput: {
|
|
170
|
+
hookEventName: 'SessionStart',
|
|
171
|
+
additionalContext: filled.join('\n\n'),
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The single status line printed after a successful auto-bind. Explains which
|
|
177
|
+
* git signal (branch name vs recent commit) it relied on and how to undo it.
|
|
178
|
+
*/
|
|
179
|
+
function formatAutoBindLine(sessionId, match, result) {
|
|
180
|
+
const basis = match.source === 'branch' ? '分支名' : '最近 commit';
|
|
181
|
+
const identifier = result.taskIdentifier ?? match.identifier;
|
|
182
|
+
return `[Lumo] session_id=${sessionId} | 已自动绑定 ${identifier} - ${(0, sanitize_1.sanitizeField)(result.taskTitle ?? '')}(依据${basis})。如果不对,回复"不是"我就帮你解绑。`;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Build the stdout lines for a session-start response, including the LUM-145
|
|
186
|
+
* auto-bind behavior: when the server reports no binding, infer the task from
|
|
187
|
+
* local git and bind it; on a hit, print the auto-bind line (plus the freshly
|
|
188
|
+
* bound task's memory). Falls back to the unbound prompt when nothing matches
|
|
189
|
+
* or the bind fails. Bound sessions reuse the existing formatting.
|
|
190
|
+
*/
|
|
191
|
+
async function resolveSessionStartStdout(responseBody, deps, now = new Date()) {
|
|
192
|
+
if (responseBody == null || typeof responseBody !== 'object')
|
|
193
|
+
return [];
|
|
194
|
+
const body = responseBody;
|
|
195
|
+
if (!body.sessionId)
|
|
196
|
+
return [];
|
|
197
|
+
const sessionId = body.sessionId;
|
|
198
|
+
const tb = body.taskBinding;
|
|
199
|
+
if (tb && tb.bound === true) {
|
|
200
|
+
return formatHookStdoutLines('session-start', responseBody, now);
|
|
201
|
+
}
|
|
202
|
+
if (!tb || tb.bound !== false)
|
|
203
|
+
return [];
|
|
204
|
+
const match = deps.extractTask();
|
|
205
|
+
if (!match)
|
|
206
|
+
return [unboundPromptLine(sessionId)];
|
|
207
|
+
const result = await deps.bindTask(sessionId, match.identifier);
|
|
208
|
+
if (!result.ok)
|
|
209
|
+
return [unboundPromptLine(sessionId)];
|
|
210
|
+
const lines = [formatAutoBindLine(sessionId, match, result)];
|
|
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]);
|
|
216
|
+
if (envelope)
|
|
217
|
+
lines.push(envelope);
|
|
115
218
|
return lines;
|
|
116
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* POST the inferred task binding to the same `bind-task` endpoint that
|
|
222
|
+
* `lumo session attach` uses. Guarded by the same short timeout as the hook
|
|
223
|
+
* POST so the extra round trip can never make session-start hang. Any
|
|
224
|
+
* failure (404 unknown task, network, timeout, non-2xx) returns
|
|
225
|
+
* `{ ok: false }`, which routes the caller back to the unbound prompt.
|
|
226
|
+
*/
|
|
227
|
+
async function postBindTask(sessionId, identifier, token, apiUrl) {
|
|
228
|
+
const controller = new AbortController();
|
|
229
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
230
|
+
try {
|
|
231
|
+
const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/sessions/${encodeURIComponent(sessionId)}/bind-task`;
|
|
232
|
+
const res = await fetch(url, {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
headers: {
|
|
235
|
+
'Content-Type': 'application/json',
|
|
236
|
+
Authorization: `Bearer ${token}`,
|
|
237
|
+
},
|
|
238
|
+
body: JSON.stringify({ taskIdentifier: identifier }),
|
|
239
|
+
signal: controller.signal,
|
|
240
|
+
});
|
|
241
|
+
if (!res.ok)
|
|
242
|
+
return { ok: false };
|
|
243
|
+
const body = (await res.json());
|
|
244
|
+
return {
|
|
245
|
+
ok: true,
|
|
246
|
+
taskIdentifier: body.taskIdentifier,
|
|
247
|
+
taskTitle: body.taskTitle,
|
|
248
|
+
memorySection: body.memorySection,
|
|
249
|
+
previousSession: body.previousSession ?? undefined,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
(0, hook_log_1.logHookError)('[session-start] auto-bind', err);
|
|
254
|
+
return { ok: false };
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
clearTimeout(timer);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
117
260
|
/**
|
|
118
261
|
* POST the hook body to /api/hooks/<path> with a short timeout. All errors
|
|
119
262
|
* — credential missing, network failure, timeout, non-2xx — are routed to
|
|
@@ -174,12 +317,19 @@ async function runHookWithBody(path, body, agentToken) {
|
|
|
174
317
|
}
|
|
175
318
|
else if (path === 'session-start' || path === 'pre-tool-use') {
|
|
176
319
|
// Paths that turn the response body into stdout for Claude Code:
|
|
177
|
-
// session-start → bind status + injected context
|
|
320
|
+
// session-start → bind status + injected context (incl. LUM-145
|
|
321
|
+
// auto-bind from local git when unbound)
|
|
178
322
|
// pre-tool-use → parallel-edit collision warning (LUM-150 ③)
|
|
179
323
|
// Only after a 2xx so a transient server failure emits nothing.
|
|
180
324
|
try {
|
|
181
325
|
const responseBody = await res.json();
|
|
182
|
-
|
|
326
|
+
const lines = path === 'session-start'
|
|
327
|
+
? await resolveSessionStartStdout(responseBody, {
|
|
328
|
+
extractTask: () => (0, git_task_1.extractTaskFromGit)(),
|
|
329
|
+
bindTask: (sessionId, identifier) => postBindTask(sessionId, identifier, creds.token, apiUrl),
|
|
330
|
+
})
|
|
331
|
+
: formatHookStdoutLines(path, responseBody);
|
|
332
|
+
for (const line of lines) {
|
|
183
333
|
process.stdout.write(line + '\n');
|
|
184
334
|
}
|
|
185
335
|
}
|