greprag 0.3.0 → 0.5.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/dist/commands/init.js +6 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/hook.d.ts +5 -7
- package/dist/hook.js +621 -662
- package/dist/hook.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -75
- package/dist/index.js.map +1 -1
- package/dist/project-anchor.d.ts +27 -0
- package/dist/project-anchor.js +142 -0
- package/dist/project-anchor.js.map +1 -0
- package/package.json +1 -1
package/dist/hook.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
/** GrepRAG Hook —
|
|
3
|
+
/** GrepRAG Hook — Stop-hook turn capture for episodic memory.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
* summary(PreCompact) → compact → recover(SessionStart:compact)
|
|
10
|
-
* PostToolUse(ExitPlanMode | AskUserQuestion) → signal */
|
|
5
|
+
* On Stop, build the per-turn envelope from the transcript and POST to
|
|
6
|
+
* /v1/memory/turn. No LLM, no enrichment — the API just inserts the row.
|
|
7
|
+
* See C:/greprag/docs/episodic-memory.md.
|
|
8
|
+
*/
|
|
11
9
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
10
|
if (k2 === undefined) k2 = k;
|
|
13
11
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -44,55 +42,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
44
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
43
|
const path = __importStar(require("path"));
|
|
46
44
|
const fs = __importStar(require("fs"));
|
|
47
|
-
const
|
|
45
|
+
const crypto = __importStar(require("crypto"));
|
|
46
|
+
const child_process_1 = require("child_process");
|
|
47
|
+
const project_anchor_1 = require("./project-anchor");
|
|
48
48
|
const API_URL_DEFAULT = 'https://api.greprag.com';
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
if (!fs.existsSync(GREPRAG_DIR))
|
|
52
|
-
fs.mkdirSync(GREPRAG_DIR, { recursive: true });
|
|
53
|
-
}
|
|
54
|
-
function getConfig(cwd) {
|
|
55
|
-
ensureEnv(cwd);
|
|
56
|
-
return {
|
|
57
|
-
apiUrl: process.env.GREPRAG_API_URL || API_URL_DEFAULT,
|
|
58
|
-
apiKey: process.env.GREPRAG_API_KEY || '',
|
|
59
|
-
enabled: process.env.MEMORY_HOOK_ENABLED === 'true',
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
function flagPath(name) {
|
|
63
|
-
ensureDir();
|
|
64
|
-
return path.join(GREPRAG_DIR, `${name}.flag`);
|
|
65
|
-
}
|
|
66
|
-
function setFlag(name) {
|
|
67
|
-
fs.writeFileSync(flagPath(name), Date.now().toString());
|
|
68
|
-
}
|
|
69
|
-
function consumeFlag(name) {
|
|
70
|
-
const p = flagPath(name);
|
|
71
|
-
try {
|
|
72
|
-
fs.unlinkSync(p);
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
function getCachePath() {
|
|
80
|
-
ensureDir();
|
|
81
|
-
return path.join(GREPRAG_DIR, 'pending-prompt.json');
|
|
82
|
-
}
|
|
83
|
-
function cachePrompt(data) {
|
|
84
|
-
fs.writeFileSync(getCachePath(), JSON.stringify(data));
|
|
85
|
-
}
|
|
86
|
-
function readCachedPrompt() {
|
|
87
|
-
try {
|
|
88
|
-
const data = JSON.parse(fs.readFileSync(getCachePath(), 'utf-8'));
|
|
89
|
-
fs.unlinkSync(getCachePath()); // consume once
|
|
90
|
-
return data;
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
49
|
+
const MAX_FIELD_CHARS = 500_000; // safety cap per text field
|
|
50
|
+
// ---------- Env + config ---------------------------------------------------
|
|
96
51
|
function loadEnvFile(filePath) {
|
|
97
52
|
try {
|
|
98
53
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
@@ -109,14 +64,11 @@ function loadEnvFile(filePath) {
|
|
|
109
64
|
(value.startsWith("'") && value.endsWith("'"))) {
|
|
110
65
|
value = value.slice(1, -1);
|
|
111
66
|
}
|
|
112
|
-
if (!process.env[key])
|
|
67
|
+
if (!process.env[key])
|
|
113
68
|
process.env[key] = value;
|
|
114
|
-
}
|
|
115
69
|
}
|
|
116
70
|
}
|
|
117
|
-
catch {
|
|
118
|
-
// File doesn't exist — fine
|
|
119
|
-
}
|
|
71
|
+
catch { /* file missing — fine */ }
|
|
120
72
|
}
|
|
121
73
|
function ensureEnv(cwd) {
|
|
122
74
|
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
@@ -125,6 +77,14 @@ function ensureEnv(cwd) {
|
|
|
125
77
|
if (!process.env.GREPRAG_API_KEY)
|
|
126
78
|
loadEnvFile(path.join(cwd, '.env'));
|
|
127
79
|
}
|
|
80
|
+
function getConfig(cwd) {
|
|
81
|
+
ensureEnv(cwd);
|
|
82
|
+
return {
|
|
83
|
+
apiUrl: process.env.GREPRAG_API_URL || API_URL_DEFAULT,
|
|
84
|
+
apiKey: process.env.GREPRAG_API_KEY || '',
|
|
85
|
+
enabled: process.env.MEMORY_HOOK_ENABLED === 'true',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
128
88
|
async function apiCall(url, apiKey, body) {
|
|
129
89
|
const res = await fetch(url, {
|
|
130
90
|
method: 'POST',
|
|
@@ -140,659 +100,679 @@ async function apiCall(url, apiKey, body) {
|
|
|
140
100
|
}
|
|
141
101
|
return res.json();
|
|
142
102
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
103
|
+
function extractText(content) {
|
|
104
|
+
if (typeof content === 'string')
|
|
105
|
+
return content.trim();
|
|
106
|
+
if (!Array.isArray(content))
|
|
107
|
+
return '';
|
|
108
|
+
return content
|
|
109
|
+
.filter((b) => b?.type === 'text')
|
|
110
|
+
.map((b) => b.text || '')
|
|
111
|
+
.join('\n').trim();
|
|
112
|
+
}
|
|
113
|
+
/** Extract text from a tool_result's content field. Can be a string or
|
|
114
|
+
* an array of text blocks. */
|
|
115
|
+
function extractToolResultText(content) {
|
|
116
|
+
if (typeof content === 'string')
|
|
117
|
+
return content;
|
|
118
|
+
if (!Array.isArray(content))
|
|
119
|
+
return '';
|
|
120
|
+
return content
|
|
121
|
+
.filter((b) => b?.type === 'text' || typeof b === 'string')
|
|
122
|
+
.map((b) => typeof b === 'string' ? b : (b.text || ''))
|
|
123
|
+
.join('\n');
|
|
124
|
+
}
|
|
125
|
+
/** Build a tool_call entry from a tool_use block. */
|
|
126
|
+
function summarizeToolUse(block) {
|
|
127
|
+
const name = block.name || 'unknown';
|
|
128
|
+
const input = (block.input || {});
|
|
129
|
+
const tc = { name };
|
|
130
|
+
switch (name) {
|
|
131
|
+
case 'Bash': {
|
|
132
|
+
const cmd = input.command || '';
|
|
133
|
+
tc.target = input.description || cmd.split(/\s+/)[0] || '';
|
|
134
|
+
tc.brief = cmd.length > 800 ? cmd.slice(0, 800) + '…' : cmd;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case 'Edit':
|
|
138
|
+
case 'Write':
|
|
139
|
+
case 'MultiEdit':
|
|
140
|
+
case 'NotebookEdit': {
|
|
141
|
+
tc.target = input.file_path || input.notebook_path || '';
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case 'Read': {
|
|
145
|
+
tc.target = input.file_path || '';
|
|
146
|
+
if (input.offset || input.limit) {
|
|
147
|
+
tc.brief = `offset=${input.offset ?? 0} limit=${input.limit ?? '?'}`;
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case 'Grep': {
|
|
152
|
+
tc.target = input.pattern || '';
|
|
153
|
+
const ctx = [input.path, input.glob, input.type].filter(Boolean).join(' ');
|
|
154
|
+
if (ctx)
|
|
155
|
+
tc.brief = ctx;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case 'Glob': {
|
|
159
|
+
tc.target = input.pattern || '';
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case 'WebFetch':
|
|
163
|
+
case 'WebSearch': {
|
|
164
|
+
tc.target = input.url || input.query || '';
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case 'Agent':
|
|
168
|
+
case 'Task': {
|
|
169
|
+
tc.target = input.description || input.subagent_type || '';
|
|
170
|
+
tc.brief = (input.prompt || '').slice(0, 200);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
default: {
|
|
174
|
+
// Unknown tool — best-effort summary
|
|
175
|
+
const json = JSON.stringify(input);
|
|
176
|
+
tc.brief = json.length > 200 ? json.slice(0, 200) + '…' : json;
|
|
155
177
|
}
|
|
156
178
|
}
|
|
179
|
+
return tc;
|
|
157
180
|
}
|
|
158
|
-
/**
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const profileId = path.basename(cwd);
|
|
171
|
-
// After compaction, the first prompt is system-generated — skip caching it
|
|
172
|
-
const skipStore = consumeFlag('skip-next');
|
|
173
|
-
// Compute stress streak directly from transcript — zero timing lag.
|
|
174
|
-
// Count consecutive assistant turns without tool_use from the tail.
|
|
175
|
-
const stressStreak = computeTranscriptStress(input.transcript_path);
|
|
176
|
-
// Fetch signals (entities + novelty) + inbox count in parallel.
|
|
177
|
-
// No content injection — `greprag recall` handles that on demand.
|
|
178
|
-
const [result, countResult] = await Promise.all([
|
|
179
|
-
apiCall(`${cfg.apiUrl}/v1/memory/retrieve`, cfg.apiKey, {
|
|
180
|
-
query: prompt,
|
|
181
|
-
profileId,
|
|
182
|
-
sessionId,
|
|
183
|
-
stressStreak,
|
|
184
|
-
}),
|
|
185
|
-
apiCall(`${cfg.apiUrl}/v1/memory/messages/count`, cfg.apiKey, {
|
|
186
|
-
profileId,
|
|
187
|
-
sessionId,
|
|
188
|
-
}),
|
|
189
|
-
]);
|
|
190
|
-
let context = '';
|
|
191
|
-
// Prepend message notification if any pending
|
|
192
|
-
const pending = countResult?.ok ? countResult.count ?? 0 : 0;
|
|
193
|
-
if (pending > 0) {
|
|
194
|
-
const s = pending === 1 ? 'message' : 'messages';
|
|
195
|
-
context += `[${pending} pending ${s} — run: greprag inbox --profile ${profileId}]\n`;
|
|
181
|
+
/** Parse the transcript, extracting the latest turn (since the last real user message). */
|
|
182
|
+
function parseLatestTurn(transcriptPath) {
|
|
183
|
+
const empty = {
|
|
184
|
+
userPrompt: '', agentResponse: '', toolCalls: [],
|
|
185
|
+
filesTouched: [], model: null, status: 'completed',
|
|
186
|
+
userStartTime: null,
|
|
187
|
+
};
|
|
188
|
+
if (!transcriptPath)
|
|
189
|
+
return empty;
|
|
190
|
+
let lines;
|
|
191
|
+
try {
|
|
192
|
+
lines = fs.readFileSync(transcriptPath, 'utf-8').split('\n');
|
|
196
193
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
194
|
+
catch {
|
|
195
|
+
return empty;
|
|
196
|
+
}
|
|
197
|
+
// Walk backwards to find the last real user message (not a tool_result).
|
|
198
|
+
let userStartIdx = -1;
|
|
199
|
+
let userPrompt = '';
|
|
200
|
+
let userStartTime = null;
|
|
201
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
202
|
+
const line = lines[i].trim();
|
|
203
|
+
if (!line)
|
|
204
|
+
continue;
|
|
205
|
+
let entry;
|
|
206
|
+
try {
|
|
207
|
+
entry = JSON.parse(line);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const msg = (entry.message || entry);
|
|
213
|
+
const role = (msg.role || entry.type || '');
|
|
214
|
+
if (role !== 'user')
|
|
215
|
+
continue;
|
|
216
|
+
if (Array.isArray(msg.content)) {
|
|
217
|
+
const isToolResult = msg.content
|
|
218
|
+
.some(b => b?.type === 'tool_result');
|
|
219
|
+
if (isToolResult)
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const text = extractText(msg.content);
|
|
223
|
+
if (text && !text.startsWith('<system') && !text.startsWith('{')) {
|
|
224
|
+
userPrompt = text;
|
|
225
|
+
userStartIdx = i;
|
|
226
|
+
if (typeof entry.timestamp === 'string')
|
|
227
|
+
userStartTime = entry.timestamp;
|
|
228
|
+
break;
|
|
211
229
|
}
|
|
212
|
-
chainMeta = r.chainMeta;
|
|
213
|
-
}
|
|
214
|
-
// GrepRAG signal line — novelty hints at how much matched in the corpus,
|
|
215
|
-
// stress hints at the agent's flow state. Both inform whether `greprag recall` is worth running.
|
|
216
|
-
const parts = [];
|
|
217
|
-
if (chainMeta) {
|
|
218
|
-
const nov = typeof chainMeta.noveltyRatio === 'number' ? Math.round(chainMeta.noveltyRatio * 100) : null;
|
|
219
|
-
const streak = typeof chainMeta.stressStreak === 'number' ? chainMeta.stressStreak : 0;
|
|
220
|
-
const state = streak >= 2 ? `stress: ${streak} (expanded)` : streak === 1 ? 'stress: 1' : 'action';
|
|
221
|
-
parts.push(`novelty: ${nov !== null ? nov + '%' : '?'} | ${state}`);
|
|
222
|
-
}
|
|
223
|
-
const incomplete = entityContext.filter(e => e.status !== 'complete');
|
|
224
|
-
if (incomplete.length > 0) {
|
|
225
|
-
parts.push(`entities: <entity-summary name="Name">what it is</entity-summary> for: ${incomplete.map(e => e.name).join(', ')}`);
|
|
226
|
-
}
|
|
227
|
-
if (parts.length > 0) {
|
|
228
|
-
context += `\n[greprag: ${parts.join(' | ')}]`;
|
|
229
|
-
}
|
|
230
|
-
// Assertive-recall directive — tells the agent to pull memory themselves when the
|
|
231
|
-
// question touches prior context. Glued to the user turn, so it reads as instruction.
|
|
232
|
-
context += `\n[greprag-recall: memory may hold relevant prior context (decisions, specs, past bugs, how something was implemented). When the user's question touches anything not obvious from the current session, run Bash: \`greprag recall "precise query" --profile ${profileId}\`. It's cheap (~$0.000007/call) — use it freely, chain multiple queries if needed. Rate what comes back via <mfb> before ending your turn.]`;
|
|
233
|
-
// Cache prompt for store pairing. No retrieval metadata — nothing was auto-injected;
|
|
234
|
-
// feedback (if any) will come from agent-initiated `greprag recall` calls.
|
|
235
|
-
if (prompt && !skipStore) {
|
|
236
|
-
cachePrompt({ prompt, sessionId, profileId });
|
|
237
230
|
}
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
231
|
+
if (userStartIdx === -1)
|
|
232
|
+
return empty;
|
|
233
|
+
// Walk forward from the user message, collecting assistant text + tool_use.
|
|
234
|
+
const agentTextParts = [];
|
|
235
|
+
const toolCalls = [];
|
|
236
|
+
const filesTouched = new Set();
|
|
237
|
+
let model = null;
|
|
238
|
+
let status = 'completed';
|
|
239
|
+
for (let i = userStartIdx + 1; i < lines.length; i++) {
|
|
240
|
+
const line = lines[i].trim();
|
|
241
|
+
if (!line)
|
|
242
|
+
continue;
|
|
243
|
+
let entry;
|
|
244
|
+
try {
|
|
245
|
+
entry = JSON.parse(line);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const msg = (entry.message || entry);
|
|
251
|
+
const role = (msg.role || entry.type || '');
|
|
252
|
+
if (role === 'assistant') {
|
|
253
|
+
if (!model && typeof msg.model === 'string')
|
|
254
|
+
model = msg.model;
|
|
255
|
+
if (Array.isArray(msg.content)) {
|
|
256
|
+
for (const b of msg.content) {
|
|
257
|
+
if (b?.type === 'text' && typeof b.text === 'string') {
|
|
258
|
+
agentTextParts.push(b.text);
|
|
259
|
+
}
|
|
260
|
+
else if (b?.type === 'tool_use') {
|
|
261
|
+
const tc = summarizeToolUse(b);
|
|
262
|
+
tc._useId = b.id || undefined;
|
|
263
|
+
toolCalls.push(tc);
|
|
264
|
+
// Files touched derivation
|
|
265
|
+
const input = (b.input || {});
|
|
266
|
+
if (['Edit', 'Write', 'MultiEdit'].includes(tc.name) && input.file_path) {
|
|
267
|
+
filesTouched.add(input.file_path);
|
|
268
|
+
}
|
|
269
|
+
else if (tc.name === 'NotebookEdit' && input.notebook_path) {
|
|
270
|
+
filesTouched.add(input.notebook_path);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else if (role === 'user') {
|
|
277
|
+
// tool_result blocks — pair to tool_use by tool_use_id, capture stdout
|
|
278
|
+
if (Array.isArray(msg.content)) {
|
|
279
|
+
for (const b of msg.content) {
|
|
280
|
+
if (b?.type === 'tool_result') {
|
|
281
|
+
const useId = b.tool_use_id || '';
|
|
282
|
+
const out = extractToolResultText(b.content);
|
|
283
|
+
if (useId && out) {
|
|
284
|
+
const tc = toolCalls.find(c => c._useId === useId);
|
|
285
|
+
if (tc)
|
|
286
|
+
tc._output = out;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const text = extractText(msg.content);
|
|
292
|
+
if (text.includes('[Request interrupted by user]'))
|
|
293
|
+
status = 'interrupted';
|
|
294
|
+
}
|
|
245
295
|
}
|
|
296
|
+
return {
|
|
297
|
+
userPrompt,
|
|
298
|
+
agentResponse: agentTextParts.join('\n\n').trim(),
|
|
299
|
+
toolCalls,
|
|
300
|
+
filesTouched: Array.from(filesTouched).sort(),
|
|
301
|
+
model,
|
|
302
|
+
status,
|
|
303
|
+
userStartTime,
|
|
304
|
+
};
|
|
246
305
|
}
|
|
247
|
-
/**
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
306
|
+
/** Walk subagent transcripts under <session>/subagents/ and pull tool_use
|
|
307
|
+
* blocks that fired during the current turn. Returns flattened ToolCall[]
|
|
308
|
+
* augmented with _output where a matching tool_result exists in the same
|
|
309
|
+
* subagent transcript. Also returns files_touched derived from subagent
|
|
310
|
+
* Edit/Write/MultiEdit/NotebookEdit calls.
|
|
311
|
+
*
|
|
312
|
+
* Subagents have their own transcript files because Claude Code scopes them
|
|
313
|
+
* to their own session. The main transcript only sees the Agent tool_use
|
|
314
|
+
* itself — not the work the subagent did. Without this flattening, any
|
|
315
|
+
* commit/deploy/PR a subagent fires would be invisible to artifact detection.
|
|
316
|
+
*/
|
|
317
|
+
function parseSubagentCalls(transcriptPath, sinceTime) {
|
|
318
|
+
const empty = { toolCalls: [], filesTouched: [] };
|
|
319
|
+
if (!sinceTime)
|
|
320
|
+
return empty;
|
|
321
|
+
// Subagent transcripts live at <transcriptPath without .jsonl>/subagents/*.jsonl
|
|
322
|
+
const sessionDir = transcriptPath.replace(/\.jsonl$/, '');
|
|
323
|
+
const subagentsDir = path.join(sessionDir, 'subagents');
|
|
324
|
+
if (!fs.existsSync(subagentsDir))
|
|
325
|
+
return empty;
|
|
326
|
+
let files = [];
|
|
257
327
|
try {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
328
|
+
files = fs.readdirSync(subagentsDir).filter(f => f.endsWith('.jsonl'));
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
return empty;
|
|
332
|
+
}
|
|
333
|
+
const sinceMs = new Date(sinceTime).getTime();
|
|
334
|
+
const merged = [];
|
|
335
|
+
const filesTouched = new Set();
|
|
336
|
+
for (const fname of files) {
|
|
337
|
+
const fpath = path.join(subagentsDir, fname);
|
|
338
|
+
let raw;
|
|
339
|
+
try {
|
|
340
|
+
raw = fs.readFileSync(fpath, 'utf-8');
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
// Two passes: collect tool_use blocks (filtered by time), then pair
|
|
346
|
+
// tool_results to populate _output.
|
|
347
|
+
const localCalls = [];
|
|
348
|
+
for (const line of raw.split('\n')) {
|
|
349
|
+
const trimmed = line.trim();
|
|
350
|
+
if (!trimmed)
|
|
263
351
|
continue;
|
|
264
352
|
let entry;
|
|
265
353
|
try {
|
|
266
|
-
entry = JSON.parse(
|
|
354
|
+
entry = JSON.parse(trimmed);
|
|
267
355
|
}
|
|
268
356
|
catch {
|
|
269
357
|
continue;
|
|
270
358
|
}
|
|
359
|
+
const ts = typeof entry.timestamp === 'string' ? new Date(entry.timestamp).getTime() : NaN;
|
|
360
|
+
if (!Number.isFinite(ts) || ts < sinceMs)
|
|
361
|
+
continue;
|
|
271
362
|
const msg = (entry.message || entry);
|
|
272
363
|
const role = (msg.role || entry.type || '');
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
364
|
+
if (role !== 'assistant' || !Array.isArray(msg.content))
|
|
365
|
+
continue;
|
|
366
|
+
for (const b of msg.content) {
|
|
367
|
+
if (b?.type !== 'tool_use')
|
|
368
|
+
continue;
|
|
369
|
+
const tc = summarizeToolUse(b);
|
|
370
|
+
tc._useId = b.id || undefined;
|
|
371
|
+
localCalls.push(tc);
|
|
372
|
+
const input = (b.input || {});
|
|
373
|
+
if (['Edit', 'Write', 'MultiEdit'].includes(tc.name) && input.file_path) {
|
|
374
|
+
filesTouched.add(input.file_path);
|
|
280
375
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
// Assistant entry: check for tool_use
|
|
284
|
-
if (role === 'assistant' && Array.isArray(msg.content)) {
|
|
285
|
-
for (const b of msg.content) {
|
|
286
|
-
if (b?.type === 'tool_use')
|
|
287
|
-
return true;
|
|
376
|
+
else if (tc.name === 'NotebookEdit' && input.notebook_path) {
|
|
377
|
+
filesTouched.add(input.notebook_path);
|
|
288
378
|
}
|
|
289
379
|
}
|
|
290
|
-
// Skip system, file-history-snapshot, etc.
|
|
291
380
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
/** Count consecutive substantial prose turns (no tool_use, 50+ words) from the
|
|
297
|
-
* transcript tail. Short responses (< 50 words) are neutral — don't increment
|
|
298
|
-
* the streak but don't reset it either. Action always resets. */
|
|
299
|
-
const STRESS_MIN_WORDS = 50;
|
|
300
|
-
function computeTranscriptStress(transcriptPath) {
|
|
301
|
-
if (!transcriptPath)
|
|
302
|
-
return 0;
|
|
303
|
-
try {
|
|
304
|
-
const raw = fs.readFileSync(transcriptPath, 'utf-8');
|
|
305
|
-
const lines = raw.split('\n');
|
|
306
|
-
let streak = 0;
|
|
307
|
-
let turnHasAction = false;
|
|
308
|
-
let turnTextLength = 0;
|
|
309
|
-
let inTurn = false;
|
|
310
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
311
|
-
const line = lines[i].trim();
|
|
312
|
-
if (!line)
|
|
381
|
+
// Pair tool_results to outputs (same subagent transcript)
|
|
382
|
+
for (const line of raw.split('\n')) {
|
|
383
|
+
const trimmed = line.trim();
|
|
384
|
+
if (!trimmed)
|
|
313
385
|
continue;
|
|
314
386
|
let entry;
|
|
315
387
|
try {
|
|
316
|
-
entry = JSON.parse(
|
|
388
|
+
entry = JSON.parse(trimmed);
|
|
317
389
|
}
|
|
318
390
|
catch {
|
|
319
391
|
continue;
|
|
320
392
|
}
|
|
321
393
|
const msg = (entry.message || entry);
|
|
322
394
|
const role = (msg.role || entry.type || '');
|
|
323
|
-
if (role
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
else if (role === 'user') {
|
|
336
|
-
const isToolResult = Array.isArray(msg.content) &&
|
|
337
|
-
msg.content.some(b => b?.type === 'tool_result');
|
|
338
|
-
if (isToolResult)
|
|
339
|
-
continue; // still same turn
|
|
340
|
-
// Real user message = turn boundary
|
|
341
|
-
if (inTurn) {
|
|
342
|
-
if (turnHasAction)
|
|
343
|
-
break; // action resets — stop counting
|
|
344
|
-
if (turnTextLength >= STRESS_MIN_WORDS)
|
|
345
|
-
streak++; // substantial prose
|
|
346
|
-
// short prose: neutral — don't increment, don't reset
|
|
395
|
+
if (role !== 'user' || !Array.isArray(msg.content))
|
|
396
|
+
continue;
|
|
397
|
+
for (const b of msg.content) {
|
|
398
|
+
if (b?.type !== 'tool_result')
|
|
399
|
+
continue;
|
|
400
|
+
const useId = b.tool_use_id || '';
|
|
401
|
+
const out = extractToolResultText(b.content);
|
|
402
|
+
if (useId && out) {
|
|
403
|
+
const tc = localCalls.find(c => c._useId === useId);
|
|
404
|
+
if (tc)
|
|
405
|
+
tc._output = out;
|
|
347
406
|
}
|
|
348
|
-
turnHasAction = false;
|
|
349
|
-
turnTextLength = 0;
|
|
350
|
-
inTurn = false;
|
|
351
407
|
}
|
|
352
408
|
}
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
catch {
|
|
356
|
-
return 0;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
/** Parse <entity-summary> tags from agent response. */
|
|
360
|
-
function parseEntitySummaries(text) {
|
|
361
|
-
const results = [];
|
|
362
|
-
const regex = /<entity-summary\s+name="([^"]+)">([\s\S]*?)<\/entity-summary>/g;
|
|
363
|
-
let match;
|
|
364
|
-
while ((match = regex.exec(text)) !== null) {
|
|
365
|
-
const name = match[1].trim();
|
|
366
|
-
const summary = match[2].trim();
|
|
367
|
-
if (name && summary)
|
|
368
|
-
results.push({ name, summary });
|
|
409
|
+
merged.push(...localCalls);
|
|
369
410
|
}
|
|
370
|
-
return
|
|
371
|
-
}
|
|
372
|
-
/** Parse <profile-summary> tag from agent response. Returns null if not found. */
|
|
373
|
-
function parseProfileSummary(text) {
|
|
374
|
-
const match = text.match(/<profile-summary>([\s\S]*?)<\/profile-summary>/);
|
|
375
|
-
return match && match[1].trim() ? match[1].trim() : null;
|
|
411
|
+
return { toolCalls: merged, filesTouched: Array.from(filesTouched).sort() };
|
|
376
412
|
}
|
|
377
|
-
|
|
378
|
-
function
|
|
379
|
-
const results = [];
|
|
380
|
-
const regex = /<profile-detail>([\s\S]*?)<\/profile-detail>/g;
|
|
381
|
-
let match;
|
|
382
|
-
while ((match = regex.exec(text)) !== null) {
|
|
383
|
-
const detail = match[1].trim();
|
|
384
|
-
if (detail)
|
|
385
|
-
results.push(detail);
|
|
386
|
-
}
|
|
387
|
-
return results;
|
|
388
|
-
}
|
|
389
|
-
/** Parse <mfb>[...]</mfb> from agent response. Returns null if not found. */
|
|
390
|
-
function parseFeedbackTag(text) {
|
|
391
|
-
const match = text.match(/<mfb>([\s\S]*?)<\/mfb>/);
|
|
392
|
-
if (!match)
|
|
393
|
-
return null;
|
|
413
|
+
// ---------- Git branch + artifact detection -------------------------------
|
|
414
|
+
function getBranch(cwd) {
|
|
394
415
|
try {
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
return null;
|
|
398
|
-
// Filter out template entries (s=0) and invalid entries
|
|
399
|
-
return parsed.filter((e) => typeof e.id === 'string' && typeof e.s === 'number' && e.s >= 1 && e.s <= 5);
|
|
416
|
+
const out = (0, child_process_1.execSync)('git branch --show-current', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
417
|
+
return out.trim() || null;
|
|
400
418
|
}
|
|
401
419
|
catch {
|
|
402
420
|
return null;
|
|
403
421
|
}
|
|
404
422
|
}
|
|
405
|
-
/**
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
if (!cfg.enabled || !cfg.apiKey)
|
|
410
|
-
return;
|
|
411
|
-
const agentProse = input.last_assistant_message || '';
|
|
412
|
-
if (!agentProse)
|
|
413
|
-
return;
|
|
414
|
-
// Read the cached prompt from the retrieve hook
|
|
415
|
-
const cached = readCachedPrompt();
|
|
416
|
-
const userMessage = cached?.prompt || '';
|
|
417
|
-
const sessionId = cached?.sessionId || input.session_id || `hook-${Date.now()}`;
|
|
418
|
-
const profileId = cached?.profileId || path.basename(cwd);
|
|
419
|
-
// Detect action: check transcript for tool_use in last assistant turn, fall back to code blocks
|
|
420
|
-
const hasAction = detectAction(input.transcript_path, agentProse);
|
|
421
|
-
// Store the turn memory
|
|
422
|
-
const storePromise = apiCall(`${cfg.apiUrl}/v1/memory/store`, cfg.apiKey, {
|
|
423
|
-
profileId,
|
|
424
|
-
sessionId,
|
|
425
|
-
userMessage,
|
|
426
|
-
agentProse,
|
|
427
|
-
hasAction,
|
|
428
|
-
crystallization: 'turn',
|
|
429
|
-
});
|
|
430
|
-
// Parse and ship feedback if present. With auto-injection disabled, `<mfb>` tags
|
|
431
|
-
// come from agent-initiated `greprag recall` calls — parse unconditionally.
|
|
432
|
-
// Server resolves short→full memory IDs via LIKE; retrievalScore/retrievalMeta are
|
|
433
|
-
// unavailable here (the recall ran out-of-band) so we ship what we have.
|
|
434
|
-
let feedbackPromise = null;
|
|
435
|
-
const ratings = parseFeedbackTag(agentProse);
|
|
436
|
-
if (ratings && ratings.length > 0) {
|
|
437
|
-
feedbackPromise = apiCall(`${cfg.apiUrl}/v1/memory/feedback`, cfg.apiKey, {
|
|
438
|
-
ratings: ratings.map(r => ({
|
|
439
|
-
memoryId: r.id,
|
|
440
|
-
relevanceScore: r.s,
|
|
441
|
-
reason: r.r || undefined,
|
|
442
|
-
})),
|
|
443
|
-
profileId,
|
|
444
|
-
sessionId,
|
|
445
|
-
triggerQuery: cached?.prompt,
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
// Parse and ship entity summaries if present
|
|
449
|
-
let entityPromise = null;
|
|
450
|
-
const entitySummaries = parseEntitySummaries(agentProse);
|
|
451
|
-
if (entitySummaries.length > 0) {
|
|
452
|
-
entityPromise = apiCall(`${cfg.apiUrl}/v1/memory/entities/summarize`, cfg.apiKey, {
|
|
453
|
-
summaries: entitySummaries,
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
// Parse and ship profile summary/details if present
|
|
457
|
-
let profilePromise = null;
|
|
458
|
-
const profSummary = parseProfileSummary(agentProse);
|
|
459
|
-
const profDetails = parseProfileDetails(agentProse);
|
|
460
|
-
if (profSummary || profDetails.length > 0) {
|
|
461
|
-
profilePromise = apiCall(`${cfg.apiUrl}/v1/memory/profile/summary`, cfg.apiKey, {
|
|
462
|
-
profileId,
|
|
463
|
-
summary: profSummary || undefined,
|
|
464
|
-
details: profDetails.length > 0 && !profSummary ? profDetails : undefined,
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
// Run store + feedback + entity summaries + profile summary in parallel
|
|
468
|
-
await Promise.all([storePromise, feedbackPromise, entityPromise, profilePromise].filter(Boolean));
|
|
469
|
-
}
|
|
470
|
-
/** Find the most recently modified plan file in ~/.claude/plans/ */
|
|
471
|
-
function readNewestPlanFile() {
|
|
472
|
-
const plansDir = path.join(os.homedir(), '.claude', 'plans');
|
|
473
|
-
try {
|
|
474
|
-
const files = fs.readdirSync(plansDir)
|
|
475
|
-
.filter(f => f.endsWith('.md'))
|
|
476
|
-
.map(f => {
|
|
477
|
-
const full = path.join(plansDir, f);
|
|
478
|
-
return { path: full, mtime: fs.statSync(full).mtimeMs };
|
|
479
|
-
})
|
|
480
|
-
.sort((a, b) => b.mtime - a.mtime);
|
|
481
|
-
if (!files.length)
|
|
482
|
-
return null;
|
|
483
|
-
// Only consider plans modified in the last 60 seconds (freshly written)
|
|
484
|
-
if (Date.now() - files[0].mtime > 60_000)
|
|
485
|
-
return null;
|
|
486
|
-
return fs.readFileSync(files[0].path, 'utf-8');
|
|
487
|
-
}
|
|
488
|
-
catch {
|
|
423
|
+
/** Parse a PR URL (number + url) out of `gh pr create` stdout. */
|
|
424
|
+
function parsePrFromOutput(out, cmd) {
|
|
425
|
+
const m = out.match(/https:\/\/github\.com\/[^\/\s]+\/[^\/\s]+\/pull\/(\d+)/);
|
|
426
|
+
if (!m)
|
|
489
427
|
return null;
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const answer = answers?.[question] || '';
|
|
502
|
-
if (question) {
|
|
503
|
-
lines.push(`Q: ${question}`);
|
|
504
|
-
lines.push(`A: ${answer || '(no answer)'}`);
|
|
505
|
-
lines.push('');
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
return lines.join('\n').trim();
|
|
509
|
-
}
|
|
510
|
-
/** PostToolUse — capture high-signal tool events (plan approvals, user answers). */
|
|
511
|
-
async function signal(input) {
|
|
512
|
-
const cwd = input.cwd || process.cwd();
|
|
513
|
-
const cfg = getConfig(cwd);
|
|
514
|
-
if (!cfg.enabled || !cfg.apiKey)
|
|
515
|
-
return;
|
|
516
|
-
const toolName = input.tool_name || '';
|
|
517
|
-
const sessionId = input.session_id || `hook-${Date.now()}`;
|
|
518
|
-
const profileId = path.basename(cwd);
|
|
519
|
-
let userMessage = '';
|
|
520
|
-
let agentProse = '';
|
|
521
|
-
let crystallization = '';
|
|
522
|
-
if (toolName === 'ExitPlanMode') {
|
|
523
|
-
const planContent = readNewestPlanFile();
|
|
524
|
-
if (!planContent)
|
|
525
|
-
return; // no plan file found
|
|
526
|
-
userMessage = '[Plan Approved]';
|
|
527
|
-
// Cap at ~2000 words to avoid oversized payloads
|
|
528
|
-
const words = planContent.split(/\s+/);
|
|
529
|
-
agentProse = words.length > 2000
|
|
530
|
-
? words.slice(0, 2000).join(' ') + '\n\n[truncated]'
|
|
531
|
-
: planContent;
|
|
532
|
-
crystallization = 'plan';
|
|
533
|
-
}
|
|
534
|
-
else if (toolName === 'AskUserQuestion') {
|
|
535
|
-
const toolInput = input.tool_input || {};
|
|
536
|
-
const toolResponse = input.tool_response || {};
|
|
537
|
-
const formatted = formatQuestionAnswers(toolInput, toolResponse);
|
|
538
|
-
if (!formatted)
|
|
539
|
-
return;
|
|
540
|
-
userMessage = formatted;
|
|
541
|
-
agentProse = '';
|
|
542
|
-
crystallization = 'decision';
|
|
543
|
-
}
|
|
544
|
-
else {
|
|
545
|
-
return; // unknown tool, ignore
|
|
546
|
-
}
|
|
547
|
-
await apiCall(`${cfg.apiUrl}/v1/memory/store`, cfg.apiKey, {
|
|
548
|
-
profileId,
|
|
549
|
-
sessionId,
|
|
550
|
-
userMessage,
|
|
551
|
-
agentProse,
|
|
552
|
-
crystallization,
|
|
553
|
-
});
|
|
428
|
+
// Try to pull --title "..." from the command for a clean title
|
|
429
|
+
const titleM = cmd.match(/--title\s+["']([^"']+)["']/);
|
|
430
|
+
return { id: '#' + m[1], url: m[0], title: titleM ? titleM[1] : undefined };
|
|
431
|
+
}
|
|
432
|
+
/** Parse a release URL out of `gh release create` stdout. */
|
|
433
|
+
function parseReleaseFromOutput(out, cmd) {
|
|
434
|
+
const m = out.match(/https:\/\/github\.com\/[^\/\s]+\/[^\/\s]+\/releases\/tag\/(\S+)/);
|
|
435
|
+
if (!m)
|
|
436
|
+
return null;
|
|
437
|
+
const titleM = cmd.match(/--title\s+["']([^"']+)["']/);
|
|
438
|
+
return { id: m[1], url: m[0], title: titleM ? titleM[1] : m[1] };
|
|
554
439
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
if (!
|
|
559
|
-
return
|
|
560
|
-
return
|
|
561
|
-
.filter((b) => b?.type === 'text')
|
|
562
|
-
.map((b) => b.text || '')
|
|
563
|
-
.join('\n').trim();
|
|
440
|
+
/** Parse `npm publish` stdout. Format: `+ pkg@1.2.3`. */
|
|
441
|
+
function parseNpmPublishFromOutput(out) {
|
|
442
|
+
const m = out.match(/^\+\s+(\S+@\S+)/m);
|
|
443
|
+
if (!m)
|
|
444
|
+
return null;
|
|
445
|
+
return { id: m[1], url: null, title: m[1] };
|
|
564
446
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
let raw;
|
|
571
|
-
try {
|
|
572
|
-
raw = fs.readFileSync(transcriptPath, 'utf-8');
|
|
573
|
-
}
|
|
574
|
-
catch {
|
|
447
|
+
/** Parse `wrangler deploy` stdout. Picks the "Current Version ID" line plus
|
|
448
|
+
* worker name and URL. */
|
|
449
|
+
function parseWranglerDeployFromOutput(out) {
|
|
450
|
+
const m = out.match(/Current Version ID:\s+([a-f0-9-]+)/i);
|
|
451
|
+
if (!m)
|
|
575
452
|
return null;
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
453
|
+
const urlM = out.match(/https:\/\/\S+\.workers\.dev/);
|
|
454
|
+
const workerM = out.match(/Uploaded (\S+)/);
|
|
455
|
+
return { id: m[1], url: urlM ? urlM[0] : null, title: workerM ? workerM[1] : undefined };
|
|
456
|
+
}
|
|
457
|
+
/** Detect artifacts from tool_calls. Real artifact IDs are parsed from tool
|
|
458
|
+
* outputs where possible; falls back to "pending" if output is missing. */
|
|
459
|
+
function detectArtifacts(toolCalls, cwd) {
|
|
460
|
+
const events = [];
|
|
461
|
+
let commitCount = 0;
|
|
462
|
+
let pushDetected = false;
|
|
463
|
+
for (const tc of toolCalls) {
|
|
464
|
+
if (tc.name !== 'Bash')
|
|
579
465
|
continue;
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
466
|
+
const cmd = (tc.brief || '').trim();
|
|
467
|
+
const out = tc._output || '';
|
|
468
|
+
if (/^git commit\b/.test(cmd) || /&&\s*git commit\b/.test(cmd))
|
|
469
|
+
commitCount++;
|
|
470
|
+
if (/^git push\b/.test(cmd) || /&&\s*git push\b/.test(cmd))
|
|
471
|
+
pushDetected = true;
|
|
472
|
+
if (/^gh pr create\b/.test(cmd) || /&&\s*gh pr create\b/.test(cmd)) {
|
|
473
|
+
const parsed = parsePrFromOutput(out, cmd);
|
|
474
|
+
events.push({
|
|
475
|
+
artifactType: 'pr',
|
|
476
|
+
artifactId: parsed?.id || 'pending',
|
|
477
|
+
artifactUrl: parsed?.url || null,
|
|
478
|
+
title: parsed?.title || cmd.slice(0, 120),
|
|
479
|
+
});
|
|
583
480
|
}
|
|
584
|
-
|
|
585
|
-
|
|
481
|
+
if (/^gh release create\b/.test(cmd) || /&&\s*gh release create\b/.test(cmd)) {
|
|
482
|
+
const parsed = parseReleaseFromOutput(out, cmd);
|
|
483
|
+
events.push({
|
|
484
|
+
artifactType: 'release',
|
|
485
|
+
artifactId: parsed?.id || 'pending',
|
|
486
|
+
artifactUrl: parsed?.url || null,
|
|
487
|
+
title: parsed?.title || cmd.slice(0, 120),
|
|
488
|
+
});
|
|
586
489
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
490
|
+
if (/\bnpm publish\b/.test(cmd)) {
|
|
491
|
+
const parsed = parseNpmPublishFromOutput(out);
|
|
492
|
+
events.push({
|
|
493
|
+
artifactType: 'release',
|
|
494
|
+
artifactId: parsed?.id || 'pending',
|
|
495
|
+
artifactUrl: parsed?.url || null,
|
|
496
|
+
title: parsed?.title || cmd.slice(0, 120),
|
|
497
|
+
});
|
|
595
498
|
}
|
|
596
|
-
if (
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
499
|
+
if (/wrangler deploy\b/.test(cmd) || /npm run deploy\b/.test(cmd)) {
|
|
500
|
+
const parsed = parseWranglerDeployFromOutput(out);
|
|
501
|
+
events.push({
|
|
502
|
+
artifactType: 'deploy',
|
|
503
|
+
artifactId: parsed?.id || 'pending',
|
|
504
|
+
artifactUrl: parsed?.url || null,
|
|
505
|
+
title: parsed?.title || cmd.slice(0, 120),
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
if (/^git merge\b/.test(cmd)) {
|
|
509
|
+
events.push({
|
|
510
|
+
artifactType: 'merge', artifactId: 'pending',
|
|
511
|
+
artifactUrl: null, title: cmd.slice(0, 200),
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// For commits: lookback in git log to recover SHA + subject for each commit.
|
|
516
|
+
if (commitCount > 0) {
|
|
517
|
+
try {
|
|
518
|
+
const log = (0, child_process_1.execSync)(`git log -${commitCount} --format=%H|%s`, {
|
|
519
|
+
cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
520
|
+
});
|
|
521
|
+
for (const line of log.trim().split('\n')) {
|
|
522
|
+
const idx = line.indexOf('|');
|
|
523
|
+
if (idx < 0)
|
|
524
|
+
continue;
|
|
525
|
+
const sha = line.slice(0, idx);
|
|
526
|
+
const subject = line.slice(idx + 1);
|
|
527
|
+
if (sha)
|
|
528
|
+
events.push({
|
|
529
|
+
artifactType: 'commit', artifactId: sha,
|
|
530
|
+
artifactUrl: null, title: subject,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
catch { /* git log failed */ }
|
|
535
|
+
}
|
|
536
|
+
// For pushes: capture HEAD SHA at hook time as the push tip.
|
|
537
|
+
if (pushDetected) {
|
|
538
|
+
try {
|
|
539
|
+
const sha = (0, child_process_1.execSync)('git rev-parse HEAD', {
|
|
540
|
+
cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
541
|
+
}).trim();
|
|
542
|
+
events.push({
|
|
543
|
+
artifactType: 'push', artifactId: sha,
|
|
544
|
+
artifactUrl: null, title: `push to ${getBranch(cwd) || 'unknown'}`,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
catch { /* git command failed */ }
|
|
548
|
+
}
|
|
549
|
+
return events;
|
|
550
|
+
}
|
|
551
|
+
// ---------- Env-var scrubbing ---------------------------------------------
|
|
552
|
+
/** Build a redaction map of value → [REDACTED:VAR_NAME] from .env files and process.env. */
|
|
553
|
+
function buildRedactionMap(cwd) {
|
|
554
|
+
const entries = [];
|
|
555
|
+
const seenValues = new Set();
|
|
556
|
+
const addEntry = (key, value) => {
|
|
557
|
+
if (!value || value.length < 16)
|
|
558
|
+
return;
|
|
559
|
+
if (seenValues.has(value))
|
|
560
|
+
return;
|
|
561
|
+
seenValues.add(value);
|
|
562
|
+
entries.push([value, `[REDACTED:${key}]`]);
|
|
563
|
+
};
|
|
564
|
+
// Walk up looking for .env files
|
|
565
|
+
let dir = path.resolve(cwd);
|
|
566
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
567
|
+
const envPath = path.join(dir, '.env');
|
|
568
|
+
if (fs.existsSync(envPath)) {
|
|
569
|
+
try {
|
|
570
|
+
const raw = fs.readFileSync(envPath, 'utf-8');
|
|
571
|
+
for (const line of raw.split('\n')) {
|
|
572
|
+
const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.+?)\s*$/);
|
|
573
|
+
if (!m)
|
|
607
574
|
continue;
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
575
|
+
let value = m[2].trim();
|
|
576
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
577
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
578
|
+
value = value.slice(1, -1);
|
|
579
|
+
}
|
|
580
|
+
addEntry(m[1], value);
|
|
612
581
|
}
|
|
613
582
|
}
|
|
583
|
+
catch { /* skip */ }
|
|
614
584
|
}
|
|
585
|
+
const parent = path.dirname(dir);
|
|
586
|
+
if (parent === dir)
|
|
587
|
+
break;
|
|
588
|
+
dir = parent;
|
|
589
|
+
}
|
|
590
|
+
// Process env
|
|
591
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
592
|
+
if (typeof value === 'string')
|
|
593
|
+
addEntry(key, value);
|
|
594
|
+
}
|
|
595
|
+
// Sort by value length desc so longer values are replaced first
|
|
596
|
+
entries.sort((a, b) => b[0].length - a[0].length);
|
|
597
|
+
return entries;
|
|
598
|
+
}
|
|
599
|
+
function scrubString(text, map) {
|
|
600
|
+
if (!text)
|
|
601
|
+
return text;
|
|
602
|
+
let out = text;
|
|
603
|
+
for (const [value, replacement] of map) {
|
|
604
|
+
if (out.includes(value))
|
|
605
|
+
out = out.split(value).join(replacement);
|
|
606
|
+
}
|
|
607
|
+
return out;
|
|
608
|
+
}
|
|
609
|
+
function scrubObject(obj, map) {
|
|
610
|
+
if (typeof obj === 'string')
|
|
611
|
+
return scrubString(obj, map);
|
|
612
|
+
if (Array.isArray(obj))
|
|
613
|
+
return obj.map(o => scrubObject(o, map));
|
|
614
|
+
if (obj && typeof obj === 'object') {
|
|
615
|
+
const out = {};
|
|
616
|
+
for (const k of Object.keys(obj)) {
|
|
617
|
+
out[k] = scrubObject(obj[k], map);
|
|
618
|
+
}
|
|
619
|
+
return out;
|
|
615
620
|
}
|
|
616
|
-
|
|
617
|
-
const seen = new Set();
|
|
618
|
-
const uniqueReqs = userReqs.map(r => r.split('\n')[0].slice(0, 150))
|
|
619
|
-
.filter(r => { if (seen.has(r))
|
|
620
|
-
return false; seen.add(r); return true; }).slice(-10);
|
|
621
|
-
return {
|
|
622
|
-
userReqs: uniqueReqs,
|
|
623
|
-
files: Array.from(files).sort(),
|
|
624
|
-
cmds: cmds.slice(-5),
|
|
625
|
-
responses: Array.from(responses.values()).slice(-3).map(r => {
|
|
626
|
-
const t = r.slice(0, 500);
|
|
627
|
-
const dot = t.lastIndexOf('.');
|
|
628
|
-
return dot > 200 ? t.slice(0, dot + 1) : t;
|
|
629
|
-
}),
|
|
630
|
-
};
|
|
621
|
+
return obj;
|
|
631
622
|
}
|
|
632
|
-
|
|
633
|
-
function
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
return null;
|
|
640
|
-
const cwd = input.cwd || process.cwd();
|
|
641
|
-
const parts = [];
|
|
642
|
-
if (parsed.userReqs.length)
|
|
643
|
-
parts.push('User requests: ' + parsed.userReqs.join('; '));
|
|
644
|
-
if (parsed.files.length)
|
|
645
|
-
parts.push('Files modified: ' + parsed.files.slice(0, 20).join(', '));
|
|
646
|
-
if (parsed.cmds.length)
|
|
647
|
-
parts.push('Commands: ' + parsed.cmds.join('; '));
|
|
648
|
-
if (parsed.responses.length)
|
|
649
|
-
parts.push('Recent context:\n' + parsed.responses.join('\n'));
|
|
650
|
-
const rawText = parts.join('\n\n');
|
|
651
|
-
if (!rawText.trim())
|
|
652
|
-
return null;
|
|
653
|
-
return { rawText, cwd, project: path.basename(cwd) };
|
|
623
|
+
// ---------- Field-size cap ------------------------------------------------
|
|
624
|
+
function capField(text) {
|
|
625
|
+
if (!text || text.length <= MAX_FIELD_CHARS)
|
|
626
|
+
return text;
|
|
627
|
+
const truncated = text.slice(0, MAX_FIELD_CHARS);
|
|
628
|
+
const originalKb = (text.length / 1000).toFixed(1);
|
|
629
|
+
return `${truncated}\n\n[truncated: original ${originalKb}KB]`;
|
|
654
630
|
}
|
|
655
|
-
|
|
656
|
-
async function
|
|
631
|
+
// ---------- Store ---------------------------------------------------------
|
|
632
|
+
async function store(input) {
|
|
633
|
+
const cwd = input.cwd || process.cwd();
|
|
657
634
|
const cfg = getConfig(cwd);
|
|
658
|
-
if (cfg.enabled && cfg.apiKey) {
|
|
659
|
-
await apiCall(`${cfg.apiUrl}/v1/memory/store`, cfg.apiKey, {
|
|
660
|
-
profileId: project,
|
|
661
|
-
sessionId: input.session_id || `hook-${Date.now()}`,
|
|
662
|
-
userMessage: label,
|
|
663
|
-
agentProse: rawText,
|
|
664
|
-
crystallization: 'compaction',
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
/** PreCompact — extract raw session text, send to API for LLM summarization + storage. */
|
|
669
|
-
async function summary(input) {
|
|
670
|
-
const extracted = extractSessionText(input);
|
|
671
|
-
if (!extracted)
|
|
672
|
-
return;
|
|
673
|
-
await storeSessionSummary(input, extracted.rawText, extracted.cwd, extracted.project, '[Session Summary]');
|
|
674
|
-
}
|
|
675
|
-
/** SessionEnd — store final session summary (fire-and-forget to survive teardown). */
|
|
676
|
-
async function farewell(input) {
|
|
677
|
-
const extracted = extractSessionText(input);
|
|
678
|
-
if (!extracted)
|
|
679
|
-
return;
|
|
680
|
-
// Only store to API if content is substantial (skip tiny post-compaction tails)
|
|
681
|
-
if (extracted.rawText.split(/\s+/).length < 30)
|
|
682
|
-
return;
|
|
683
|
-
const cfg = getConfig(extracted.cwd);
|
|
684
635
|
if (!cfg.enabled || !cfg.apiKey)
|
|
685
636
|
return;
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
637
|
+
const anchor = (0, project_anchor_1.readAnchor)(cwd);
|
|
638
|
+
const turn = parseLatestTurn(input.transcript_path);
|
|
639
|
+
// Flatten subagent tool_use blocks into the main turn so artifact detection
|
|
640
|
+
// catches commits/deploys/PRs a subagent fired. See adr/raw-turn-capture.md.
|
|
641
|
+
if (input.transcript_path && turn.userStartTime) {
|
|
642
|
+
const sub = parseSubagentCalls(input.transcript_path, turn.userStartTime);
|
|
643
|
+
if (sub.toolCalls.length > 0) {
|
|
644
|
+
turn.toolCalls.push(...sub.toolCalls);
|
|
645
|
+
const filesSet = new Set([...turn.filesTouched, ...sub.filesTouched]);
|
|
646
|
+
turn.filesTouched = Array.from(filesSet).sort();
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
// Empty turn — nothing to capture.
|
|
650
|
+
if (!turn.userPrompt && !turn.agentResponse && turn.toolCalls.length === 0)
|
|
651
|
+
return;
|
|
652
|
+
const branch = getBranch(cwd);
|
|
653
|
+
const shipEvents = detectArtifacts(turn.toolCalls, cwd);
|
|
654
|
+
// Env-var scrub
|
|
655
|
+
const redaction = buildRedactionMap(cwd);
|
|
656
|
+
const userPrompt = capField(scrubString(turn.userPrompt, redaction));
|
|
657
|
+
const agentResponse = capField(scrubString(turn.agentResponse, redaction));
|
|
658
|
+
// Strip internal-only fields (_output, _useId) before sending to API,
|
|
659
|
+
// then scrub. The internal fields fed detectArtifacts above.
|
|
660
|
+
const toolCallsForApi = turn.toolCalls.map(tc => ({
|
|
661
|
+
name: tc.name, target: tc.target, brief: tc.brief,
|
|
662
|
+
}));
|
|
663
|
+
const toolCalls = scrubObject(toolCallsForApi, redaction);
|
|
664
|
+
const artifactRefs = shipEvents.map(ev => `${ev.artifactType}:${ev.artifactId.slice(0, 12)}`);
|
|
665
|
+
await apiCall(`${cfg.apiUrl}/v1/memory/turn`, cfg.apiKey, {
|
|
666
|
+
projectId: anchor.projectId,
|
|
667
|
+
projectName: anchor.projectName,
|
|
668
|
+
branch,
|
|
669
|
+
workingDir: cwd,
|
|
670
|
+
sessionIdExternal: input.session_id || `hook-${Date.now()}`,
|
|
671
|
+
turnId: crypto.randomUUID(),
|
|
672
|
+
model: turn.model,
|
|
673
|
+
status: turn.status,
|
|
674
|
+
userPrompt,
|
|
675
|
+
agentResponse,
|
|
676
|
+
toolCalls,
|
|
677
|
+
filesTouched: turn.filesTouched,
|
|
678
|
+
artifacts: artifactRefs,
|
|
679
|
+
shipEvents,
|
|
680
|
+
});
|
|
706
681
|
}
|
|
707
|
-
/**
|
|
708
|
-
|
|
682
|
+
/** SessionStart — fetch recent episodic activity for this project and print
|
|
683
|
+
* to stdout. Claude Code injects the printed text as session context. Fires
|
|
684
|
+
* once per session. No LLM call. */
|
|
685
|
+
async function recap(input) {
|
|
709
686
|
const cwd = input.cwd || process.cwd();
|
|
710
687
|
const cfg = getConfig(cwd);
|
|
711
688
|
if (!cfg.enabled || !cfg.apiKey)
|
|
712
689
|
return;
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
if (!result?.ok)
|
|
690
|
+
const anchor = (0, project_anchor_1.readAnchor)(cwd);
|
|
691
|
+
if (!anchor.projectId)
|
|
716
692
|
return;
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
signals.push('<profile-detail>new fact about this project</profile-detail>');
|
|
736
|
-
parts.push(`[greprag-profile: ${signals.join(' + ')}]`);
|
|
737
|
-
// Session recap block
|
|
738
|
-
if (result.found && result.enrichedSummary) {
|
|
739
|
-
parts.push(header);
|
|
740
|
-
parts.push(result.enrichedSummary);
|
|
741
|
-
parts.push(footer);
|
|
742
|
-
}
|
|
743
|
-
if (parts.length > 0) {
|
|
744
|
-
process.stdout.write(parts.join('\n') + '\n');
|
|
693
|
+
const now = new Date();
|
|
694
|
+
const todayStart = new Date(now);
|
|
695
|
+
todayStart.setUTCHours(0, 0, 0, 0);
|
|
696
|
+
const fromIso = new Date(now.getTime() - 7 * 86400_000).toISOString();
|
|
697
|
+
const toIso = now.toISOString();
|
|
698
|
+
const url = `${cfg.apiUrl}/v1/memory/by-period`
|
|
699
|
+
+ `?projectId=${encodeURIComponent(anchor.projectId)}`
|
|
700
|
+
+ `&from=${encodeURIComponent(fromIso)}`
|
|
701
|
+
+ `&to=${encodeURIComponent(toIso)}`
|
|
702
|
+
+ `&limit=200`;
|
|
703
|
+
let data = null;
|
|
704
|
+
try {
|
|
705
|
+
const res = await fetch(url, {
|
|
706
|
+
headers: { 'Authorization': `Bearer ${cfg.apiKey}` },
|
|
707
|
+
});
|
|
708
|
+
if (!res.ok)
|
|
709
|
+
return;
|
|
710
|
+
data = await res.json();
|
|
745
711
|
}
|
|
746
|
-
|
|
747
|
-
/** SessionStart:startup — inject last session's summary for continuity. */
|
|
748
|
-
async function recap(input) {
|
|
749
|
-
await injectRecap(input, '--- LAST SESSION (auto-generated) ---', '--- END LAST SESSION ---');
|
|
750
|
-
}
|
|
751
|
-
/** SessionStart:compact recovery — inject cached session summary into context. */
|
|
752
|
-
async function recover(input) {
|
|
753
|
-
await injectRecap(input, '--- SESSION RECOVERY (auto-generated) ---', '--- END SESSION RECOVERY ---');
|
|
754
|
-
}
|
|
755
|
-
/** Manual handoff — store user-provided recap for next session (before /clear).
|
|
756
|
-
* Fire-and-forget: sends the API request, prints confirmation immediately,
|
|
757
|
-
* then exits after a short delay to let the TCP socket flush. */
|
|
758
|
-
async function handoff(input) {
|
|
759
|
-
const cwd = input.cwd || process.cwd();
|
|
760
|
-
const cfg = getConfig(cwd);
|
|
761
|
-
if (!cfg.enabled || !cfg.apiKey) {
|
|
762
|
-
process.stderr.write('[greprag-hook] Memory hook not enabled or no API key\n');
|
|
712
|
+
catch {
|
|
763
713
|
return;
|
|
764
714
|
}
|
|
765
|
-
const
|
|
766
|
-
if (
|
|
767
|
-
process.stderr.write('[greprag-hook] No handoff message provided\n');
|
|
715
|
+
const memories = data?.memories || [];
|
|
716
|
+
if (memories.length === 0)
|
|
768
717
|
return;
|
|
718
|
+
// Bucket by crystallization
|
|
719
|
+
const dailies = memories.filter(m => m.crystallization === 'episodic-daily')
|
|
720
|
+
.sort((a, b) => (a.windowStart || '').localeCompare(b.windowStart || ''));
|
|
721
|
+
const hourlies = memories.filter(m => m.crystallization === 'episodic-hourly')
|
|
722
|
+
.sort((a, b) => (a.windowStart || '').localeCompare(b.windowStart || ''));
|
|
723
|
+
const shipEvents = memories.filter(m => m.crystallization === 'ship-event'
|
|
724
|
+
&& m.artifactId
|
|
725
|
+
&& m.artifactId !== 'pending').sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
726
|
+
// Most recent daily
|
|
727
|
+
const latestDaily = dailies[dailies.length - 1];
|
|
728
|
+
// Today's hourlies (window_start >= UTC midnight today)
|
|
729
|
+
const todaysHourlies = hourlies.filter(h => h.windowStart && new Date(h.windowStart).getTime() >= todayStart.getTime());
|
|
730
|
+
// Last 5 ship-events with real IDs
|
|
731
|
+
const recentShipEvents = shipEvents.slice(-5);
|
|
732
|
+
// Nothing useful to surface
|
|
733
|
+
if (!latestDaily && todaysHourlies.length === 0 && recentShipEvents.length === 0)
|
|
734
|
+
return;
|
|
735
|
+
const parts = [];
|
|
736
|
+
parts.push(`[Recent activity in ${anchor.projectName}]`);
|
|
737
|
+
if (latestDaily && latestDaily.windowStart) {
|
|
738
|
+
const dailyDate = latestDaily.windowStart.slice(0, 10);
|
|
739
|
+
parts.push('');
|
|
740
|
+
parts.push(`Last full day (${dailyDate}):`);
|
|
741
|
+
parts.push(latestDaily.content);
|
|
742
|
+
}
|
|
743
|
+
if (todaysHourlies.length > 0) {
|
|
744
|
+
parts.push('');
|
|
745
|
+
parts.push(`Today's activity:`);
|
|
746
|
+
for (const h of todaysHourlies) {
|
|
747
|
+
if (!h.windowStart || !h.windowEnd)
|
|
748
|
+
continue;
|
|
749
|
+
const startHHMM = h.windowStart.slice(11, 16);
|
|
750
|
+
const endHHMM = h.windowEnd.slice(11, 16);
|
|
751
|
+
// Pull the first numbered item from the hourly content as the gist
|
|
752
|
+
const m = h.content.match(/^\s*[0-9]+[.)]\s+([\s\S]+?)(?=\n\s*[0-9]+[.)]|\nOpen:|\nShipped:|$)/);
|
|
753
|
+
let gist = m ? m[1].trim() : h.content.trim();
|
|
754
|
+
gist = gist.replace(/\s+/g, ' ');
|
|
755
|
+
if (gist.length > 220)
|
|
756
|
+
gist = gist.slice(0, 217) + '…';
|
|
757
|
+
parts.push(`- ${startHHMM}–${endHHMM} UTC: ${gist}`);
|
|
758
|
+
}
|
|
769
759
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
'
|
|
777
|
-
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
sessionId,
|
|
782
|
-
userMessage: '[Session End]',
|
|
783
|
-
agentProse: message,
|
|
784
|
-
crystallization: 'compaction',
|
|
785
|
-
}),
|
|
786
|
-
}).catch(() => { }); // swallow errors — already printed confirmation
|
|
787
|
-
process.stdout.write('Handoff stored. Next session will pick it up via recap.\n');
|
|
788
|
-
// Brief delay to let the HTTP request body flush to the socket
|
|
789
|
-
await new Promise(resolve => setTimeout(resolve, 300));
|
|
760
|
+
if (recentShipEvents.length > 0) {
|
|
761
|
+
parts.push('');
|
|
762
|
+
parts.push(`Recently shipped:`);
|
|
763
|
+
for (const s of recentShipEvents) {
|
|
764
|
+
const id = (s.artifactId || '').slice(0, 12);
|
|
765
|
+
// s.content is "<artifact_type>: <title>" — strip the prefix
|
|
766
|
+
const title = s.content.replace(/^[^:]+:\s*/, '').slice(0, 120);
|
|
767
|
+
parts.push(`- ${s.artifactType} ${id}: ${title}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
process.stdout.write(parts.join('\n') + '\n');
|
|
790
771
|
}
|
|
791
|
-
const SUBCOMMANDS = ['retrieve', 'store', 'compact', 'signal', 'summary', 'recover', 'farewell', 'recap', 'handoff'];
|
|
792
772
|
async function main() {
|
|
793
773
|
const subcommand = process.argv[2];
|
|
794
|
-
if (
|
|
795
|
-
process.stderr.write(`Usage: greprag-hook
|
|
774
|
+
if (subcommand !== 'store' && subcommand !== 'recap') {
|
|
775
|
+
process.stderr.write(`Usage: greprag-hook <store|recap>\n`);
|
|
796
776
|
process.exit(1);
|
|
797
777
|
}
|
|
798
778
|
let input = {};
|
|
@@ -808,36 +788,15 @@ async function main() {
|
|
|
808
788
|
catch {
|
|
809
789
|
process.exit(0);
|
|
810
790
|
}
|
|
811
|
-
if (subcommand === '
|
|
812
|
-
await compact(input);
|
|
813
|
-
}
|
|
814
|
-
else if (subcommand === 'retrieve') {
|
|
815
|
-
await retrieve(input);
|
|
816
|
-
}
|
|
817
|
-
else if (subcommand === 'signal') {
|
|
818
|
-
await signal(input);
|
|
819
|
-
}
|
|
820
|
-
else if (subcommand === 'summary') {
|
|
821
|
-
await summary(input);
|
|
822
|
-
}
|
|
823
|
-
else if (subcommand === 'farewell') {
|
|
824
|
-
await farewell(input);
|
|
825
|
-
}
|
|
826
|
-
else if (subcommand === 'recover') {
|
|
827
|
-
await recover(input);
|
|
828
|
-
}
|
|
829
|
-
else if (subcommand === 'recap') {
|
|
791
|
+
if (subcommand === 'recap') {
|
|
830
792
|
await recap(input);
|
|
831
793
|
}
|
|
832
|
-
else if (subcommand === 'handoff') {
|
|
833
|
-
await handoff(input);
|
|
834
|
-
}
|
|
835
794
|
else {
|
|
836
795
|
await store(input);
|
|
837
796
|
}
|
|
838
797
|
}
|
|
839
798
|
main().catch(err => {
|
|
840
799
|
process.stderr.write(`[greprag-hook] Fatal: ${err.message}\n`);
|
|
841
|
-
process.exit(0);
|
|
800
|
+
process.exit(0);
|
|
842
801
|
});
|
|
843
802
|
//# sourceMappingURL=hook.js.map
|