@lh8ppl/claude-memory-kit 0.1.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/bin/cmk-compress-lazy.mjs +59 -0
- package/bin/cmk-daily-distill.mjs +67 -0
- package/bin/cmk-weekly-curate.mjs +56 -0
- package/bin/cmk.mjs +12 -0
- package/package.json +50 -0
- package/src/audit-log.mjs +103 -0
- package/src/auto-extract.mjs +742 -0
- package/src/capture-prompt.mjs +61 -0
- package/src/capture-turn.mjs +273 -0
- package/src/claude-md.mjs +212 -0
- package/src/compress-session.mjs +349 -0
- package/src/compressor.mjs +376 -0
- package/src/conflict-queue.mjs +796 -0
- package/src/cooldown.mjs +61 -0
- package/src/daily-distill.mjs +252 -0
- package/src/doctor.mjs +528 -0
- package/src/forget.mjs +335 -0
- package/src/frontmatter.mjs +73 -0
- package/src/import-anthropic-memory.mjs +266 -0
- package/src/index-db.mjs +154 -0
- package/src/index-rebuild.mjs +597 -0
- package/src/index.mjs +90 -0
- package/src/inject-context.mjs +484 -0
- package/src/install.mjs +327 -0
- package/src/lazy-compress.mjs +326 -0
- package/src/lock-discipline.mjs +166 -0
- package/src/mcp-server.mjs +498 -0
- package/src/memory-write.mjs +565 -0
- package/src/merge-facts.mjs +213 -0
- package/src/observe-edit.mjs +87 -0
- package/src/platform-commands.mjs +138 -0
- package/src/poison-guard.mjs +245 -0
- package/src/privacy.mjs +21 -0
- package/src/provenance.mjs +217 -0
- package/src/register-crons.mjs +354 -0
- package/src/reindex.mjs +134 -0
- package/src/repair.mjs +316 -0
- package/src/result-shapes.mjs +155 -0
- package/src/review-queue.mjs +345 -0
- package/src/roll.mjs +115 -0
- package/src/scratchpad.mjs +335 -0
- package/src/search.mjs +311 -0
- package/src/subcommands.mjs +1252 -0
- package/src/tier-paths.mjs +74 -0
- package/src/transcripts.mjs +234 -0
- package/src/trust.mjs +226 -0
- package/src/weekly-curate.mjs +454 -0
- package/src/write-fact.mjs +205 -0
- package/template/.claude/hooks/pre-tool-memory.js +78 -0
- package/template/.claude/hooks/transcript-capture.js +69 -0
- package/template/.claude/settings.json +27 -0
- package/template/.claude/skills/memory-write/SKILL.md +117 -0
- package/template/.gitignore.fragment +12 -0
- package/template/CLAUDE.md.template +49 -0
- package/template/docs/journey/journey-log.md.template +292 -0
- package/template/local/machine-paths.md.template +37 -0
- package/template/local/overrides.md.template +36 -0
- package/template/project/.index/.gitkeep +0 -0
- package/template/project/MEMORY.md.template +47 -0
- package/template/project/SOUL.md.template +35 -0
- package/template/project/memory/INDEX.md.template +47 -0
- package/template/project/memory/archive/superseded/.gitkeep +0 -0
- package/template/project/memory/archive/tombstones/.gitkeep +0 -0
- package/template/project/queues/.gitkeep +0 -0
- package/template/project/sessions/.gitkeep +0 -0
- package/template/project/transcripts/.gitkeep +0 -0
- package/template/support/cron-jobs/daily-memory-distill.md +15 -0
- package/template/support/cron-jobs/nightly-memsearch-index.md +17 -0
- package/template/support/cron-jobs/weekly-memory-curator.md +15 -0
- package/template/support/milvus-deploy/README.md +57 -0
- package/template/support/milvus-deploy/docker-compose.yml +66 -0
- package/template/support/scripts/auto-extract-memory.sh +102 -0
- package/template/support/scripts/memsearch-index-with-flush.sh +59 -0
- package/template/support/scripts/refresh-distill-timestamp.py +35 -0
- package/template/support/scripts/register-crons.py +242 -0
- package/template/support/scripts/run-daily-distill.sh +67 -0
- package/template/support/scripts/run-weekly-curate.sh +58 -0
- package/template/user/HABITS.md.template +18 -0
- package/template/user/LESSONS.md.template +18 -0
- package/template/user/USER.md.template +18 -0
- package/template/user/fragments/INDEX.md.template +23 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// SessionEnd compression (Task 22, T-019).
|
|
2
|
+
//
|
|
3
|
+
// Public boundary: compressSession({projectRoot, backend, now,
|
|
4
|
+
// cooldownMs, maxOutputBytes}) — invoked by the SessionEnd hook
|
|
5
|
+
// (plugin/bin/cmk-compress-session.mjs). Reads
|
|
6
|
+
// context/sessions/now.md, compresses via the injected
|
|
7
|
+
// CompressorBackend using the design §8.4 prompt, appends the result
|
|
8
|
+
// to context/sessions/today-{YYYY-MM-DD}.md, then truncates now.md
|
|
9
|
+
// so the next session starts fresh.
|
|
10
|
+
//
|
|
11
|
+
// The CompressorBackend interface (see compressor.mjs) lets tests
|
|
12
|
+
// inject a MockHaikuBackend without spawning the real `claude`
|
|
13
|
+
// binary; the real-binary spawn smoke for the SessionEnd code path
|
|
14
|
+
// lives in tests/spawn-smoke-compress-session.test.js per design §17.
|
|
15
|
+
//
|
|
16
|
+
// Cooldown (design §8.2): if `<projectRoot>/context/.locks/
|
|
17
|
+
// last-haiku-call.ts` mtime is within `cooldownMs` of `now`, skip
|
|
18
|
+
// the compression (the auto-extract subagent may have just spent the
|
|
19
|
+
// budget on a Stop-hook fire). Default cooldownMs = 120_000.
|
|
20
|
+
//
|
|
21
|
+
// Error semantics (tasks.md 22.5): backend.compress() throw → action
|
|
22
|
+
// 'error', now.md UNTOUCHED, today-{date}.md NOT written, log entry
|
|
23
|
+
// with success:false + error_category:'compress_failed'. The bin
|
|
24
|
+
// wrapper exits 0 either way — a crashed SessionEnd hook would block
|
|
25
|
+
// the user from closing their terminal.
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
existsSync,
|
|
29
|
+
mkdirSync,
|
|
30
|
+
readFileSync,
|
|
31
|
+
writeFileSync,
|
|
32
|
+
appendFileSync,
|
|
33
|
+
truncateSync,
|
|
34
|
+
} from 'node:fs';
|
|
35
|
+
import { join, dirname } from 'node:path';
|
|
36
|
+
import { nowIso } from './audit-log.mjs';
|
|
37
|
+
import { ERROR_CATEGORIES } from './result-shapes.mjs';
|
|
38
|
+
import { HaikuTimeoutError } from './compressor.mjs';
|
|
39
|
+
import {
|
|
40
|
+
DEFAULT_COOLDOWN_MS,
|
|
41
|
+
isCooldownActive,
|
|
42
|
+
touchCooldownMarker,
|
|
43
|
+
} from './cooldown.mjs';
|
|
44
|
+
|
|
45
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 4096;
|
|
46
|
+
|
|
47
|
+
const NOW_MD_RELATIVE = ['context', 'sessions', 'now.md'];
|
|
48
|
+
const SESSIONS_DIR_RELATIVE = ['context', 'sessions'];
|
|
49
|
+
|
|
50
|
+
// Compression prompt (design §8.4). Written from scratch per the
|
|
51
|
+
// licensing posture in SOURCES.md (claude-remember's prompts are not
|
|
52
|
+
// copied verbatim — only the structural pattern of "instructions go
|
|
53
|
+
// in --append-system-prompt / our `instructions` field; the live
|
|
54
|
+
// buffer goes in the user message / our `input` field" is absorbed).
|
|
55
|
+
//
|
|
56
|
+
// The four-section structure (Decisions / Open Questions / Files
|
|
57
|
+
// Touched / Active Threads) is the §8.4 contract. The citation-ID
|
|
58
|
+
// preservation rule (`/#[ULP]-[A-Z0-9]{6,8}/`) is the §3.1 contract
|
|
59
|
+
// — Haiku must NEVER invent IDs and must preserve any it sees.
|
|
60
|
+
//
|
|
61
|
+
// Prompt-engineering note: compressor.mjs concatenates
|
|
62
|
+
// `${instructions}\n\n${input}` into a single user-side message. The
|
|
63
|
+
// earlier prompt phrasing ("You receive a live session buffer...")
|
|
64
|
+
// invited Haiku to read the whole thing as a meta-configuration
|
|
65
|
+
// conversation and respond with "OK, ready to compress — send me the
|
|
66
|
+
// buffer." Surfaced by tests/spawn-smoke-compress-session.test.js in
|
|
67
|
+
// the full-suite run on 2026-05-25 (the isolated run got lucky on
|
|
68
|
+
// stochastic Haiku output; under cache-cold/full-suite conditions it
|
|
69
|
+
// took the meta-conversation path). Fix: (a) imperative voice with
|
|
70
|
+
// an explicit forward-reference to the buffer that follows, (b)
|
|
71
|
+
// SESSION_BUFFER_DELIMITER markers around the buffer so the model
|
|
72
|
+
// has an unambiguous boundary between directive and input, and
|
|
73
|
+
// (c) explicit ban on preamble / clarifying questions / "I
|
|
74
|
+
// understand" acknowledgments.
|
|
75
|
+
const SESSION_BUFFER_DELIMITER = '=== BEGIN SESSION BUFFER (compress this) ===';
|
|
76
|
+
const SESSION_BUFFER_END_DELIMITER = '=== END SESSION BUFFER ===';
|
|
77
|
+
|
|
78
|
+
function buildCompressionInstructions(maxOutputBytes) {
|
|
79
|
+
return [
|
|
80
|
+
'You are a memory compressor for claude-memory-kit. Your task is to compress the session buffer that appears below into a four-section Markdown summary.',
|
|
81
|
+
'',
|
|
82
|
+
'Output ONLY the compressed Markdown. Do not write preamble. Do not acknowledge the task. Do not ask clarifying questions. Do not include any meta-commentary. Begin your response with the first applicable section heading.',
|
|
83
|
+
'',
|
|
84
|
+
'REQUIRED FORMAT (emit these section headings exactly, in this order; omit any heading whose section would have no entries):',
|
|
85
|
+
'',
|
|
86
|
+
'## Decisions',
|
|
87
|
+
'- <one bullet per concrete decision the session reached, ≤80 chars>',
|
|
88
|
+
'',
|
|
89
|
+
'## Open Questions',
|
|
90
|
+
'- <one bullet per unresolved question raised during the session, ≤80 chars>',
|
|
91
|
+
'',
|
|
92
|
+
'## Files Touched',
|
|
93
|
+
'- path: <relative path> — <verb summary> (cites: [#P-XXXXXXXX])',
|
|
94
|
+
'',
|
|
95
|
+
'## Active Threads',
|
|
96
|
+
'- <one bullet per work-in-progress thread the next session should resume, ≤80 chars>',
|
|
97
|
+
'',
|
|
98
|
+
'HARD RULES:',
|
|
99
|
+
' 1. Preserve every citation ID matching /#[ULP]-[A-Z0-9]{6,8}/ verbatim. Never invent new IDs.',
|
|
100
|
+
` 2. Total output ≤ ${maxOutputBytes} bytes.`,
|
|
101
|
+
' 3. If a section has no entries, omit the heading entirely (do not emit an empty heading).',
|
|
102
|
+
' 4. No prose around the headings — only the bulleted list per section.',
|
|
103
|
+
' 5. Your output goes directly into the next session\'s memory. Do not address the user, do not refer to yourself, do not narrate.',
|
|
104
|
+
'',
|
|
105
|
+
`The session buffer to compress appears below between the ${SESSION_BUFFER_DELIMITER} and ${SESSION_BUFFER_END_DELIMITER} markers.`,
|
|
106
|
+
].join('\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function wrapBufferForPrompt(buffer) {
|
|
110
|
+
return `${SESSION_BUFFER_DELIMITER}\n${buffer}\n${SESSION_BUFFER_END_DELIMITER}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function readNowMdPath(projectRoot) {
|
|
114
|
+
return join(projectRoot, ...NOW_MD_RELATIVE);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function todayMdPath(projectRoot, date) {
|
|
118
|
+
return join(projectRoot, ...SESSIONS_DIR_RELATIVE, `today-${date}.md`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function compressLogPath(projectRoot, date) {
|
|
122
|
+
return join(projectRoot, ...SESSIONS_DIR_RELATIVE, `${date}.compress.log`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function dateFromIso(ts) {
|
|
126
|
+
// ISO 8601 first 10 chars are YYYY-MM-DD; safe for both
|
|
127
|
+
// '2026-05-26T10:00:00Z' and the nowIso() shape.
|
|
128
|
+
return ts.slice(0, 10);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function readNowBuffer(projectRoot) {
|
|
132
|
+
const p = readNowMdPath(projectRoot);
|
|
133
|
+
if (!existsSync(p)) return '';
|
|
134
|
+
try {
|
|
135
|
+
return readFileSync(p, 'utf8');
|
|
136
|
+
} catch {
|
|
137
|
+
return '';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function appendToTodayMd({ projectRoot, date, body }) {
|
|
142
|
+
const path = todayMdPath(projectRoot, date);
|
|
143
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
144
|
+
// Append with a trailing newline so successive same-day appends
|
|
145
|
+
// don't collide on a missing terminator.
|
|
146
|
+
const suffix = body.endsWith('\n') ? '' : '\n';
|
|
147
|
+
appendFileSync(path, body + suffix, 'utf8');
|
|
148
|
+
return path;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function truncateNowMd(projectRoot) {
|
|
152
|
+
const p = readNowMdPath(projectRoot);
|
|
153
|
+
if (!existsSync(p)) return;
|
|
154
|
+
try {
|
|
155
|
+
truncateSync(p, 0);
|
|
156
|
+
} catch {
|
|
157
|
+
// Best-effort. If truncate fails (perm error etc.), the next
|
|
158
|
+
// session compresses a slightly-larger buffer — not a data-loss
|
|
159
|
+
// event.
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function writeCompressLogEntry({ projectRoot, date, entry }) {
|
|
164
|
+
const path = compressLogPath(projectRoot, date);
|
|
165
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
166
|
+
appendFileSync(path, JSON.stringify(entry) + '\n', 'utf8');
|
|
167
|
+
return path;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function compressSession({
|
|
171
|
+
projectRoot,
|
|
172
|
+
backend,
|
|
173
|
+
now,
|
|
174
|
+
cooldownMs = DEFAULT_COOLDOWN_MS,
|
|
175
|
+
maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES,
|
|
176
|
+
} = {}) {
|
|
177
|
+
const ts = now ?? nowIso();
|
|
178
|
+
const date = dateFromIso(ts);
|
|
179
|
+
const t0 = Date.now();
|
|
180
|
+
|
|
181
|
+
if (!projectRoot) {
|
|
182
|
+
return {
|
|
183
|
+
action: 'error',
|
|
184
|
+
error_category: ERROR_CATEGORIES.MISSING_PROJECT_ROOT,
|
|
185
|
+
duration_ms: Date.now() - t0,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (!backend || typeof backend.compress !== 'function') {
|
|
189
|
+
return {
|
|
190
|
+
action: 'error',
|
|
191
|
+
error_category: ERROR_CATEGORIES.MISSING_BACKEND,
|
|
192
|
+
duration_ms: Date.now() - t0,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Project must have been `cmk install`-ed (context/sessions/
|
|
197
|
+
// exists). If not, this is a no-op — we don't create directories
|
|
198
|
+
// in projects that haven't opted in. Crucially, this skip does
|
|
199
|
+
// NOT write a log entry; that would create the very directory we
|
|
200
|
+
// just decided not to create. The scaffold test relies on this
|
|
201
|
+
// (it invokes the bin handler from the repo root without a
|
|
202
|
+
// context/ tree and expects no side effects).
|
|
203
|
+
const sessionsDir = join(projectRoot, ...SESSIONS_DIR_RELATIVE);
|
|
204
|
+
if (!existsSync(sessionsDir)) {
|
|
205
|
+
return {
|
|
206
|
+
action: 'skipped',
|
|
207
|
+
reason: 'no-context-dir',
|
|
208
|
+
duration_ms: Date.now() - t0,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 1. Cooldown gate (design §8.2). Checked BEFORE reading now.md so
|
|
213
|
+
// a stale buffer doesn't get retried within the 120s window —
|
|
214
|
+
// the next SessionEnd will re-trigger naturally.
|
|
215
|
+
if (isCooldownActive({ projectRoot, now: ts, cooldownMs })) {
|
|
216
|
+
const duration_ms = Date.now() - t0;
|
|
217
|
+
const entry = {
|
|
218
|
+
ts,
|
|
219
|
+
scope: 'session-end',
|
|
220
|
+
input_bytes: 0,
|
|
221
|
+
output_bytes: 0,
|
|
222
|
+
model_id: typeof backend.modelId === 'function' ? backend.modelId() : null,
|
|
223
|
+
cost_usd: 0,
|
|
224
|
+
duration_ms,
|
|
225
|
+
success: true,
|
|
226
|
+
skipped_reason: 'cooldown',
|
|
227
|
+
};
|
|
228
|
+
writeCompressLogEntry({ projectRoot, date, entry });
|
|
229
|
+
return {
|
|
230
|
+
action: 'skipped',
|
|
231
|
+
reason: 'cooldown',
|
|
232
|
+
duration_ms,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 2. Read live buffer; no-op if empty (tasks.md 22.1).
|
|
237
|
+
const buffer = readNowBuffer(projectRoot);
|
|
238
|
+
if (buffer.trim() === '') {
|
|
239
|
+
const duration_ms = Date.now() - t0;
|
|
240
|
+
const entry = {
|
|
241
|
+
ts,
|
|
242
|
+
scope: 'session-end',
|
|
243
|
+
input_bytes: 0,
|
|
244
|
+
output_bytes: 0,
|
|
245
|
+
model_id: typeof backend.modelId === 'function' ? backend.modelId() : null,
|
|
246
|
+
cost_usd: 0,
|
|
247
|
+
duration_ms,
|
|
248
|
+
success: true,
|
|
249
|
+
skipped_reason: 'empty',
|
|
250
|
+
};
|
|
251
|
+
writeCompressLogEntry({ projectRoot, date, entry });
|
|
252
|
+
return {
|
|
253
|
+
action: 'skipped',
|
|
254
|
+
reason: 'empty',
|
|
255
|
+
duration_ms,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const input_bytes = Buffer.byteLength(buffer, 'utf8');
|
|
260
|
+
const instructions = buildCompressionInstructions(maxOutputBytes);
|
|
261
|
+
|
|
262
|
+
// 3. Invoke backend. On throw: leave now.md intact (22.5).
|
|
263
|
+
//
|
|
264
|
+
// Subprocess timeout: 50_000 ms. Sits under the 60s SessionEnd
|
|
265
|
+
// hook ceiling (design §5.1) so on timeout the catch + log write
|
|
266
|
+
// complete BEFORE Claude Code kills the parent. now.md is left
|
|
267
|
+
// intact in the timeout case (the truncate step is reached only
|
|
268
|
+
// on the success path), so the next session-end retries naturally.
|
|
269
|
+
// See design §8.5 for the composition rationale.
|
|
270
|
+
let result;
|
|
271
|
+
try {
|
|
272
|
+
result = await backend.compress({
|
|
273
|
+
input: wrapBufferForPrompt(buffer),
|
|
274
|
+
instructions,
|
|
275
|
+
preserveCitationIds: true,
|
|
276
|
+
maxOutputBytes,
|
|
277
|
+
timeoutMs: 50_000,
|
|
278
|
+
});
|
|
279
|
+
} catch (err) {
|
|
280
|
+
// Distinguish HAIKU_TIMEOUT (slow Anthropic) from COMPRESS_FAILED
|
|
281
|
+
// (non-zero subprocess exit / spawn ENOENT / etc). Analytics
|
|
282
|
+
// treat them differently — timeouts retry naturally on the
|
|
283
|
+
// next SessionEnd; failed exits often need investigation.
|
|
284
|
+
// `instanceof HaikuTimeoutError` (not string match on
|
|
285
|
+
// err.category) so the routing contract is type-anchored —
|
|
286
|
+
// see compressor.mjs HaikuTimeoutError docstring for rationale.
|
|
287
|
+
const errorCategory = err instanceof HaikuTimeoutError
|
|
288
|
+
? ERROR_CATEGORIES.HAIKU_TIMEOUT
|
|
289
|
+
: ERROR_CATEGORIES.COMPRESS_FAILED;
|
|
290
|
+
const duration_ms = Date.now() - t0;
|
|
291
|
+
const entry = {
|
|
292
|
+
ts,
|
|
293
|
+
scope: 'session-end',
|
|
294
|
+
input_bytes,
|
|
295
|
+
output_bytes: 0,
|
|
296
|
+
model_id: typeof backend.modelId === 'function' ? backend.modelId() : null,
|
|
297
|
+
cost_usd: 0,
|
|
298
|
+
duration_ms,
|
|
299
|
+
success: false,
|
|
300
|
+
error_category: errorCategory,
|
|
301
|
+
};
|
|
302
|
+
writeCompressLogEntry({ projectRoot, date, entry });
|
|
303
|
+
return {
|
|
304
|
+
action: 'error',
|
|
305
|
+
error_category: errorCategory,
|
|
306
|
+
duration_ms,
|
|
307
|
+
errorMessage: err?.message ?? String(err),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const output = result?.outputText ?? '';
|
|
312
|
+
const output_bytes = Buffer.byteLength(output, 'utf8');
|
|
313
|
+
|
|
314
|
+
// 4. Write compressed output to today-{date}.md (append for same-day).
|
|
315
|
+
const outputPath = appendToTodayMd({
|
|
316
|
+
projectRoot,
|
|
317
|
+
date,
|
|
318
|
+
body: output,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// 5. Truncate now.md (22.3).
|
|
322
|
+
truncateNowMd(projectRoot);
|
|
323
|
+
|
|
324
|
+
// 6. Touch cooldown marker so the next caller within 120s skips.
|
|
325
|
+
touchCooldownMarker({ projectRoot, now: ts });
|
|
326
|
+
|
|
327
|
+
const duration_ms = Date.now() - t0;
|
|
328
|
+
const entry = {
|
|
329
|
+
ts,
|
|
330
|
+
scope: 'session-end',
|
|
331
|
+
input_bytes,
|
|
332
|
+
output_bytes,
|
|
333
|
+
model_id:
|
|
334
|
+
result?.modelId ??
|
|
335
|
+
(typeof backend.modelId === 'function' ? backend.modelId() : null),
|
|
336
|
+
cost_usd: result?.costUSD ?? 0,
|
|
337
|
+
duration_ms,
|
|
338
|
+
success: true,
|
|
339
|
+
};
|
|
340
|
+
writeCompressLogEntry({ projectRoot, date, entry });
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
action: 'compressed',
|
|
344
|
+
outputPath,
|
|
345
|
+
bytesIn: input_bytes,
|
|
346
|
+
bytesOut: output_bytes,
|
|
347
|
+
duration_ms,
|
|
348
|
+
};
|
|
349
|
+
}
|