@xiaolei.shawn/mcp-server 0.2.0 → 0.2.1
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/README.md +49 -0
- package/dist/__tests__/ingest.test.d.ts +1 -0
- package/dist/__tests__/ingest.test.js +105 -0
- package/dist/adapters/codex.d.ts +2 -0
- package/dist/adapters/codex.js +322 -0
- package/dist/adapters/cursor.d.ts +2 -0
- package/dist/adapters/cursor.js +279 -0
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.js +20 -0
- package/dist/adapters/types.d.ts +34 -0
- package/dist/adapters/types.js +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +20 -0
- package/dist/dashboard.js +333 -2
- package/dist/event-envelope.d.ts +35 -3
- package/dist/index.js +71 -2
- package/dist/ingest.d.ts +17 -0
- package/dist/ingest.js +419 -0
- package/dist/store.d.ts +2 -2
- package/dist/store.js +6 -4
- package/dist/tools.d.ts +1066 -22
- package/dist/tools.js +563 -0
- package/package.json +15 -9
package/dist/ingest.js
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { getIngestFingerprintMaxWindowHours, getIngestFingerprintMinConfidence, getSessionsDir } from "./config.js";
|
|
5
|
+
import { adaptRawContent } from "./adapters/index.js";
|
|
6
|
+
import { readSessionEvents } from "./store.js";
|
|
7
|
+
function safeFilename(input) {
|
|
8
|
+
return input.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
9
|
+
}
|
|
10
|
+
/** Exact key: same kind, ts, actor, scope, payload (for strict dedupe within same source). */
|
|
11
|
+
function dedupeKey(event) {
|
|
12
|
+
return JSON.stringify([
|
|
13
|
+
event.kind,
|
|
14
|
+
event.ts,
|
|
15
|
+
event.actor.type,
|
|
16
|
+
event.actor.id ?? "",
|
|
17
|
+
event.scope ?? {},
|
|
18
|
+
event.payload ?? {},
|
|
19
|
+
]);
|
|
20
|
+
}
|
|
21
|
+
/** Ts rounded to 2-minute bucket for semantic matching (MCP vs adapter may differ by seconds). */
|
|
22
|
+
function tsBucket(ts) {
|
|
23
|
+
const ms = new Date(ts).getTime();
|
|
24
|
+
if (Number.isNaN(ms))
|
|
25
|
+
return ts;
|
|
26
|
+
const bucket = Math.floor(ms / (2 * 60 * 1000)) * (2 * 60 * 1000);
|
|
27
|
+
return new Date(bucket).toISOString();
|
|
28
|
+
}
|
|
29
|
+
/** Normalize text for semantic key: lowercase, collapse spaces, truncate. */
|
|
30
|
+
function semanticNorm(value, max = 400) {
|
|
31
|
+
if (!value || typeof value !== "string")
|
|
32
|
+
return "";
|
|
33
|
+
return value
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/\s+/g, " ")
|
|
36
|
+
.trim()
|
|
37
|
+
.slice(0, max);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Semantic key for merge dedupe: same logical event from MCP vs adapter should match
|
|
41
|
+
* so we keep one (prefer existing) and don't double-count for recommendations/risk.
|
|
42
|
+
*/
|
|
43
|
+
function semanticDedupeKey(event) {
|
|
44
|
+
const kind = event.kind;
|
|
45
|
+
const ts = tsBucket(event.ts);
|
|
46
|
+
const payload = (event.payload ?? {});
|
|
47
|
+
const scope = event.scope ?? {};
|
|
48
|
+
if (kind === "session_start" || kind === "session_end") {
|
|
49
|
+
return `${kind}`;
|
|
50
|
+
}
|
|
51
|
+
if (kind === "intent") {
|
|
52
|
+
const desc = semanticNorm(String(payload.description ?? payload.title ?? ""));
|
|
53
|
+
return `intent:${desc}`;
|
|
54
|
+
}
|
|
55
|
+
if (kind === "tool_call") {
|
|
56
|
+
const action = semanticNorm(String(payload.action ?? ""), 80);
|
|
57
|
+
const target = semanticNorm(String(payload.target ?? payload.details?.raw ?? ""), 200);
|
|
58
|
+
return `tool_call:${event.actor.type}:${action}:${target}`;
|
|
59
|
+
}
|
|
60
|
+
if (kind === "artifact_created") {
|
|
61
|
+
const artifactType = String(payload.artifact_type ?? "");
|
|
62
|
+
const text = semanticNorm(String(payload.text ?? payload.summary ?? ""), 300);
|
|
63
|
+
const intentId = scope.intent_id ?? "";
|
|
64
|
+
return `artifact:${artifactType}:${intentId}:${text}`;
|
|
65
|
+
}
|
|
66
|
+
if (kind === "token_usage_checkpoint") {
|
|
67
|
+
const intentId = scope.intent_id ?? "";
|
|
68
|
+
return `token_usage:${ts}:${intentId}`;
|
|
69
|
+
}
|
|
70
|
+
// Other kinds: fall back to exact-like key but with ts bucket to allow minor ts drift
|
|
71
|
+
return JSON.stringify([
|
|
72
|
+
kind,
|
|
73
|
+
ts,
|
|
74
|
+
event.actor.type,
|
|
75
|
+
event.actor.id ?? "",
|
|
76
|
+
scope,
|
|
77
|
+
payload,
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
function toIso(raw, fallback) {
|
|
81
|
+
if (!raw)
|
|
82
|
+
return fallback;
|
|
83
|
+
const date = new Date(raw);
|
|
84
|
+
return Number.isNaN(date.getTime()) ? fallback : date.toISOString();
|
|
85
|
+
}
|
|
86
|
+
function toMs(raw) {
|
|
87
|
+
if (!raw)
|
|
88
|
+
return undefined;
|
|
89
|
+
const ms = new Date(raw).getTime();
|
|
90
|
+
return Number.isNaN(ms) ? undefined : ms;
|
|
91
|
+
}
|
|
92
|
+
function normalizeFingerprint(value) {
|
|
93
|
+
if (!value)
|
|
94
|
+
return "";
|
|
95
|
+
return value
|
|
96
|
+
.toLowerCase()
|
|
97
|
+
.replace(/[^a-z0-9\s]/g, " ")
|
|
98
|
+
.replace(/\s+/g, " ")
|
|
99
|
+
.trim()
|
|
100
|
+
.slice(0, 320);
|
|
101
|
+
}
|
|
102
|
+
function tokenSet(value) {
|
|
103
|
+
const words = value.split(" ").filter((w) => w.length > 2);
|
|
104
|
+
return new Set(words);
|
|
105
|
+
}
|
|
106
|
+
function jaccard(a, b) {
|
|
107
|
+
if (a.size === 0 || b.size === 0)
|
|
108
|
+
return 0;
|
|
109
|
+
let overlap = 0;
|
|
110
|
+
for (const item of a) {
|
|
111
|
+
if (b.has(item))
|
|
112
|
+
overlap += 1;
|
|
113
|
+
}
|
|
114
|
+
const union = a.size + b.size - overlap;
|
|
115
|
+
return union <= 0 ? 0 : overlap / union;
|
|
116
|
+
}
|
|
117
|
+
function promptSimilarity(aRaw, bRaw) {
|
|
118
|
+
const a = normalizeFingerprint(aRaw);
|
|
119
|
+
const b = normalizeFingerprint(bRaw);
|
|
120
|
+
if (!a || !b)
|
|
121
|
+
return 0;
|
|
122
|
+
if (a === b)
|
|
123
|
+
return 1;
|
|
124
|
+
if (a.length >= 18 && b.length >= 18 && (a.includes(b) || b.includes(a)))
|
|
125
|
+
return 0.9;
|
|
126
|
+
return jaccard(tokenSet(a), tokenSet(b));
|
|
127
|
+
}
|
|
128
|
+
function timeProximityScore(sourceMs, targetMs) {
|
|
129
|
+
if (!sourceMs || !targetMs)
|
|
130
|
+
return 0;
|
|
131
|
+
const hours = Math.abs(sourceMs - targetMs) / (1000 * 60 * 60);
|
|
132
|
+
if (hours <= 0.5)
|
|
133
|
+
return 1;
|
|
134
|
+
if (hours <= 6)
|
|
135
|
+
return 0.8;
|
|
136
|
+
if (hours <= 24)
|
|
137
|
+
return 0.5;
|
|
138
|
+
if (hours <= 72)
|
|
139
|
+
return 0.25;
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
function extractPromptFromAdapted(adapted) {
|
|
143
|
+
if (adapted.user_prompt && adapted.user_prompt.trim())
|
|
144
|
+
return adapted.user_prompt;
|
|
145
|
+
const intent = adapted.events.find((event) => event.kind === "intent");
|
|
146
|
+
if (!intent)
|
|
147
|
+
return adapted.goal;
|
|
148
|
+
const payload = (intent.payload ?? {});
|
|
149
|
+
const description = typeof payload.description === "string" ? payload.description : undefined;
|
|
150
|
+
const title = typeof payload.title === "string" ? payload.title : undefined;
|
|
151
|
+
return description ?? title ?? adapted.goal;
|
|
152
|
+
}
|
|
153
|
+
function parseExistingSessionFingerprint(path, sessionId) {
|
|
154
|
+
const raw = readFileSync(path, "utf-8").trim();
|
|
155
|
+
if (!raw)
|
|
156
|
+
return undefined;
|
|
157
|
+
const lines = raw.split("\n").filter((line) => line.trim() !== "");
|
|
158
|
+
let prompt;
|
|
159
|
+
let sessionStartGoal;
|
|
160
|
+
let startedAt;
|
|
161
|
+
let endedAt;
|
|
162
|
+
for (const line of lines) {
|
|
163
|
+
let parsed;
|
|
164
|
+
try {
|
|
165
|
+
parsed = JSON.parse(line);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (!parsed || typeof parsed !== "object")
|
|
171
|
+
continue;
|
|
172
|
+
const event = parsed;
|
|
173
|
+
if (!startedAt && typeof event.ts === "string")
|
|
174
|
+
startedAt = event.ts;
|
|
175
|
+
if (event.kind === "session_start") {
|
|
176
|
+
const payload = (event.payload ?? {});
|
|
177
|
+
const userPrompt = typeof payload.user_prompt === "string" ? payload.user_prompt : undefined;
|
|
178
|
+
const goal = typeof payload.goal === "string" ? payload.goal : undefined;
|
|
179
|
+
if (userPrompt && userPrompt.trim() !== "") {
|
|
180
|
+
prompt = userPrompt;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
sessionStartGoal = goal;
|
|
184
|
+
prompt = prompt ?? goal;
|
|
185
|
+
}
|
|
186
|
+
startedAt = typeof event.ts === "string" ? event.ts : startedAt;
|
|
187
|
+
}
|
|
188
|
+
else if (event.kind === "intent") {
|
|
189
|
+
const payload = (event.payload ?? {});
|
|
190
|
+
const description = typeof payload.description === "string" ? payload.description : undefined;
|
|
191
|
+
const title = typeof payload.title === "string" ? payload.title : undefined;
|
|
192
|
+
const intentText = description ?? title;
|
|
193
|
+
if (intentText && (!prompt || prompt === sessionStartGoal)) {
|
|
194
|
+
prompt = intentText;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else if (event.kind === "session_end") {
|
|
198
|
+
endedAt = typeof event.ts === "string" ? event.ts : endedAt;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const stats = statSync(path);
|
|
202
|
+
return {
|
|
203
|
+
session_id: sessionId,
|
|
204
|
+
prompt,
|
|
205
|
+
started_at: startedAt,
|
|
206
|
+
ended_at: endedAt,
|
|
207
|
+
updated_at: stats.mtime.toISOString(),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function loadSessionFingerprints() {
|
|
211
|
+
const dir = getSessionsDir();
|
|
212
|
+
if (!existsSync(dir))
|
|
213
|
+
return [];
|
|
214
|
+
const files = readdirSync(dir).filter((name) => name.endsWith(".jsonl") && !name.includes(".raw."));
|
|
215
|
+
const out = [];
|
|
216
|
+
for (const file of files) {
|
|
217
|
+
const sessionId = file.replace(/\.jsonl$/, "");
|
|
218
|
+
const meta = parseExistingSessionFingerprint(join(dir, file), sessionId);
|
|
219
|
+
if (meta)
|
|
220
|
+
out.push(meta);
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
function findFingerprintSessionMatch(adapted) {
|
|
225
|
+
const maxWindowHours = getIngestFingerprintMaxWindowHours();
|
|
226
|
+
const prompt = extractPromptFromAdapted(adapted);
|
|
227
|
+
const normalized = normalizeFingerprint(prompt);
|
|
228
|
+
if (!normalized)
|
|
229
|
+
return undefined;
|
|
230
|
+
const sourceTs = toMs(adapted.started_at) ??
|
|
231
|
+
toMs(adapted.events[0]?.ts) ??
|
|
232
|
+
toMs(adapted.ended_at) ??
|
|
233
|
+
Date.now();
|
|
234
|
+
const candidates = loadSessionFingerprints();
|
|
235
|
+
let best;
|
|
236
|
+
for (const candidate of candidates) {
|
|
237
|
+
const candidatePrompt = normalizeFingerprint(candidate.prompt);
|
|
238
|
+
if (!candidatePrompt)
|
|
239
|
+
continue;
|
|
240
|
+
const promptScore = promptSimilarity(normalized, candidatePrompt);
|
|
241
|
+
if (promptScore < 0.52)
|
|
242
|
+
continue;
|
|
243
|
+
const candidateTs = toMs(candidate.ended_at) ?? toMs(candidate.started_at) ?? toMs(candidate.updated_at) ?? undefined;
|
|
244
|
+
const timeScore = timeProximityScore(sourceTs, candidateTs);
|
|
245
|
+
const distanceHours = sourceTs && candidateTs ? Math.abs(sourceTs - candidateTs) / (1000 * 60 * 60) : Number.POSITIVE_INFINITY;
|
|
246
|
+
if (distanceHours > maxWindowHours)
|
|
247
|
+
continue;
|
|
248
|
+
const score = promptScore * 0.78 + timeScore * 0.22;
|
|
249
|
+
if (!best || score > best.confidence) {
|
|
250
|
+
best = { session_id: candidate.session_id, confidence: Number(score.toFixed(3)) };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (!best)
|
|
254
|
+
return undefined;
|
|
255
|
+
if (best.confidence < getIngestFingerprintMinConfidence())
|
|
256
|
+
return undefined;
|
|
257
|
+
return best;
|
|
258
|
+
}
|
|
259
|
+
function buildCanonicalEvent(sessionId, seq, event, fallbackTs) {
|
|
260
|
+
const ts = toIso(event.ts, fallbackTs);
|
|
261
|
+
return {
|
|
262
|
+
id: `${sessionId}:${seq}:${randomUUID().slice(0, 8)}`,
|
|
263
|
+
session_id: sessionId,
|
|
264
|
+
seq,
|
|
265
|
+
ts,
|
|
266
|
+
kind: event.kind,
|
|
267
|
+
actor: event.actor,
|
|
268
|
+
scope: event.scope,
|
|
269
|
+
payload: event.payload,
|
|
270
|
+
derived: event.derived,
|
|
271
|
+
confidence: event.confidence,
|
|
272
|
+
visibility: event.visibility,
|
|
273
|
+
schema_version: 1,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function writeEventsAppend(sessionId, events) {
|
|
277
|
+
const outDir = getSessionsDir();
|
|
278
|
+
if (!existsSync(outDir))
|
|
279
|
+
mkdirSync(outDir, { recursive: true });
|
|
280
|
+
const path = join(outDir, `${safeFilename(sessionId)}.jsonl`);
|
|
281
|
+
const existing = existsSync(path) ? readFileSync(path, "utf-8").trim() : "";
|
|
282
|
+
const append = events.map((e) => JSON.stringify(e)).join("\n");
|
|
283
|
+
const body = [existing, append].filter(Boolean).join("\n") + "\n";
|
|
284
|
+
writeFileSync(path, body, "utf-8");
|
|
285
|
+
return path;
|
|
286
|
+
}
|
|
287
|
+
/** Write full session log with events sorted by ts then seq and seq reassigned 1..N. */
|
|
288
|
+
function writeSessionFull(sessionId, events) {
|
|
289
|
+
const outDir = getSessionsDir();
|
|
290
|
+
if (!existsSync(outDir))
|
|
291
|
+
mkdirSync(outDir, { recursive: true });
|
|
292
|
+
const path = join(outDir, `${safeFilename(sessionId)}.jsonl`);
|
|
293
|
+
const sorted = [...events].sort((a, b) => {
|
|
294
|
+
const t = a.ts.localeCompare(b.ts);
|
|
295
|
+
if (t !== 0)
|
|
296
|
+
return t;
|
|
297
|
+
return (a.seq ?? 0) - (b.seq ?? 0);
|
|
298
|
+
});
|
|
299
|
+
let seq = 0;
|
|
300
|
+
const normalized = sorted.map((e) => {
|
|
301
|
+
seq += 1;
|
|
302
|
+
return {
|
|
303
|
+
...e,
|
|
304
|
+
session_id: sessionId,
|
|
305
|
+
seq,
|
|
306
|
+
id: `${sessionId}:${seq}:${randomUUID().slice(0, 8)}`,
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
const body = normalized.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
310
|
+
writeFileSync(path, body, "utf-8");
|
|
311
|
+
return path;
|
|
312
|
+
}
|
|
313
|
+
function writeRawSidecar(sessionId, source, raw) {
|
|
314
|
+
const outDir = getSessionsDir();
|
|
315
|
+
if (!existsSync(outDir))
|
|
316
|
+
mkdirSync(outDir, { recursive: true });
|
|
317
|
+
const path = join(outDir, `${safeFilename(sessionId)}.${safeFilename(source)}.raw.jsonl`);
|
|
318
|
+
writeFileSync(path, raw, "utf-8");
|
|
319
|
+
return path;
|
|
320
|
+
}
|
|
321
|
+
function chooseSessionId(adapted, mergeSessionId) {
|
|
322
|
+
if (mergeSessionId) {
|
|
323
|
+
return {
|
|
324
|
+
session_id: mergeSessionId,
|
|
325
|
+
strategy: "explicit_merge",
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
const adaptedSessionId = adapted.session_id?.trim();
|
|
329
|
+
if (adaptedSessionId) {
|
|
330
|
+
const existingPath = join(getSessionsDir(), `${safeFilename(adaptedSessionId)}.jsonl`);
|
|
331
|
+
if (existsSync(existingPath)) {
|
|
332
|
+
return {
|
|
333
|
+
session_id: adaptedSessionId,
|
|
334
|
+
strategy: "adapted_session_id",
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const fingerprintMatch = findFingerprintSessionMatch(adapted);
|
|
339
|
+
if (fingerprintMatch) {
|
|
340
|
+
return {
|
|
341
|
+
session_id: fingerprintMatch.session_id,
|
|
342
|
+
strategy: "fingerprint_match",
|
|
343
|
+
fingerprint_confidence: fingerprintMatch.confidence,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (adaptedSessionId) {
|
|
347
|
+
return {
|
|
348
|
+
session_id: adaptedSessionId,
|
|
349
|
+
strategy: "new_session",
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
session_id: `sess_${Date.now()}_${randomUUID().slice(0, 8)}`,
|
|
354
|
+
strategy: "new_session",
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
export function ingestRawContent(raw, options = {}) {
|
|
358
|
+
const adapted = adaptRawContent(raw, options.adapter ?? "auto");
|
|
359
|
+
const sessionSelection = chooseSessionId(adapted, options.merge_session_id);
|
|
360
|
+
const sessionId = sessionSelection.session_id;
|
|
361
|
+
const existing = readSessionEvents(sessionId);
|
|
362
|
+
const doDedupe = options.dedupe ?? true;
|
|
363
|
+
const fallbackTs = new Date().toISOString();
|
|
364
|
+
const isMerge = existing.length > 0;
|
|
365
|
+
const existingSemanticKeys = new Set(existing.map((e) => semanticDedupeKey(e)));
|
|
366
|
+
const existingExactKeys = new Set(existing.map((e) => dedupeKey(e)));
|
|
367
|
+
let seq = existing.length > 0 ? Math.max(...existing.map((e) => e.seq)) : 0;
|
|
368
|
+
let inserted = 0;
|
|
369
|
+
let skipped = 0;
|
|
370
|
+
const toInsert = [];
|
|
371
|
+
for (const event of adapted.events) {
|
|
372
|
+
const candidate = buildCanonicalEvent(sessionId, seq + 1, event, fallbackTs);
|
|
373
|
+
if (doDedupe) {
|
|
374
|
+
if (isMerge && existingSemanticKeys.has(semanticDedupeKey(candidate))) {
|
|
375
|
+
skipped += 1;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (!isMerge && existingExactKeys.has(dedupeKey(candidate))) {
|
|
379
|
+
skipped += 1;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
seq += 1;
|
|
384
|
+
candidate.seq = seq;
|
|
385
|
+
candidate.id = `${sessionId}:${seq}:${randomUUID().slice(0, 8)}`;
|
|
386
|
+
toInsert.push(candidate);
|
|
387
|
+
if (isMerge)
|
|
388
|
+
existingSemanticKeys.add(semanticDedupeKey(candidate));
|
|
389
|
+
else
|
|
390
|
+
existingExactKeys.add(dedupeKey(candidate));
|
|
391
|
+
inserted += 1;
|
|
392
|
+
}
|
|
393
|
+
let sessionPath;
|
|
394
|
+
if (isMerge && toInsert.length > 0) {
|
|
395
|
+
const combined = [...existing, ...toInsert];
|
|
396
|
+
sessionPath = writeSessionFull(sessionId, combined);
|
|
397
|
+
}
|
|
398
|
+
else if (isMerge && toInsert.length === 0) {
|
|
399
|
+
sessionPath = join(getSessionsDir(), `${safeFilename(sessionId)}.jsonl`);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
sessionPath = writeEventsAppend(sessionId, toInsert);
|
|
403
|
+
}
|
|
404
|
+
const rawPath = writeRawSidecar(sessionId, adapted.source, raw);
|
|
405
|
+
return {
|
|
406
|
+
session_id: sessionId,
|
|
407
|
+
adapter: adapted.source,
|
|
408
|
+
inserted,
|
|
409
|
+
skipped_duplicates: skipped,
|
|
410
|
+
session_path: sessionPath,
|
|
411
|
+
raw_path: rawPath,
|
|
412
|
+
merge_strategy: sessionSelection.strategy,
|
|
413
|
+
merge_confidence: sessionSelection.fingerprint_confidence,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
export function ingestRawFile(filePath, options = {}) {
|
|
417
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
418
|
+
return ingestRawContent(raw, options);
|
|
419
|
+
}
|
package/dist/store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ActorType, type CanonicalEvent, type SessionLogFile } from "./event-envelope.js";
|
|
1
|
+
import { type ActorType, type CanonicalEvent, type EventKind, type SessionLogFile } from "./event-envelope.js";
|
|
2
2
|
export interface SessionState {
|
|
3
3
|
session_id: string;
|
|
4
4
|
goal: string;
|
|
@@ -43,7 +43,7 @@ export declare function ensureActiveSession(): SessionState;
|
|
|
43
43
|
export declare function setActiveIntent(intentId: string): void;
|
|
44
44
|
export interface CreateEventInput {
|
|
45
45
|
session_id: string;
|
|
46
|
-
kind:
|
|
46
|
+
kind: EventKind;
|
|
47
47
|
actor: {
|
|
48
48
|
type: ActorType;
|
|
49
49
|
id?: string;
|
package/dist/store.js
CHANGED
|
@@ -136,18 +136,19 @@ function deriveSnapshot(session, events) {
|
|
|
136
136
|
const files = new Set();
|
|
137
137
|
let intentCount = 0;
|
|
138
138
|
for (const event of events) {
|
|
139
|
+
const payload = (event.payload ?? {});
|
|
139
140
|
kinds[event.kind] = (kinds[event.kind] ?? 0) + 1;
|
|
140
141
|
if (event.kind === "intent")
|
|
141
142
|
intentCount += 1;
|
|
142
143
|
if (event.kind === "file_op") {
|
|
143
|
-
const target = typeof
|
|
144
|
-
?
|
|
144
|
+
const target = typeof payload.target === "string"
|
|
145
|
+
? payload.target
|
|
145
146
|
: event.scope?.file;
|
|
146
147
|
if (target && target.trim() !== "")
|
|
147
148
|
files.add(target);
|
|
148
149
|
}
|
|
149
150
|
if (event.kind === "verification") {
|
|
150
|
-
const result =
|
|
151
|
+
const result = payload.result;
|
|
151
152
|
if (result === "pass")
|
|
152
153
|
pass += 1;
|
|
153
154
|
else if (result === "fail")
|
|
@@ -157,7 +158,8 @@ function deriveSnapshot(session, events) {
|
|
|
157
158
|
}
|
|
158
159
|
}
|
|
159
160
|
const end = [...events].reverse().find((event) => event.kind === "session_end");
|
|
160
|
-
const
|
|
161
|
+
const endPayload = (end?.payload ?? {});
|
|
162
|
+
const outcomeRaw = endPayload.outcome;
|
|
161
163
|
const outcome = outcomeRaw === "completed" || outcomeRaw === "partial" || outcomeRaw === "failed" || outcomeRaw === "aborted"
|
|
162
164
|
? outcomeRaw
|
|
163
165
|
: "unknown";
|