driftguard-mcp 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/README.md +146 -0
- package/dist/bin.js +2205 -0
- package/dist/mcp-server.js +1864 -0
- package/package.json +45 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,2205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
27
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
28
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
|
+
mod
|
|
31
|
+
));
|
|
32
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
33
|
+
|
|
34
|
+
// src/watchers/claude-parser.ts
|
|
35
|
+
function parseTimestamp(raw) {
|
|
36
|
+
if (typeof raw === "number" && isFinite(raw)) return raw;
|
|
37
|
+
if (typeof raw === "string") {
|
|
38
|
+
const ms = new Date(raw).getTime();
|
|
39
|
+
if (isFinite(ms)) return ms;
|
|
40
|
+
}
|
|
41
|
+
return Date.now();
|
|
42
|
+
}
|
|
43
|
+
function parseJSONL(filePath) {
|
|
44
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
45
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
46
|
+
const messages = [];
|
|
47
|
+
let skipped = 0;
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
try {
|
|
50
|
+
const entry = JSON.parse(line);
|
|
51
|
+
if (entry.type !== "user" && entry.type !== "assistant") continue;
|
|
52
|
+
const content = entry.message?.content;
|
|
53
|
+
let text = "";
|
|
54
|
+
if (typeof content === "string") {
|
|
55
|
+
text = content.trim();
|
|
56
|
+
} else if (Array.isArray(content)) {
|
|
57
|
+
text = content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text.trim()).join("\n").trim();
|
|
58
|
+
}
|
|
59
|
+
if (!text.trim()) continue;
|
|
60
|
+
messages.push({
|
|
61
|
+
id: entry.uuid,
|
|
62
|
+
role: entry.message.role,
|
|
63
|
+
content: text,
|
|
64
|
+
timestamp: parseTimestamp(entry.timestamp)
|
|
65
|
+
});
|
|
66
|
+
} catch {
|
|
67
|
+
skipped++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (skipped > 0) {
|
|
71
|
+
const pct = Math.round(skipped / lines.length * 100);
|
|
72
|
+
console.warn(`[driftcli] Skipped ${skipped}/${lines.length} malformed JSONL lines (${pct}%) in ${path.basename(filePath)}`);
|
|
73
|
+
}
|
|
74
|
+
return messages;
|
|
75
|
+
}
|
|
76
|
+
function claudeProjectsDir() {
|
|
77
|
+
return path.join(process.env.DRIFTCLI_HOME ?? os.homedir(), ".claude", "projects");
|
|
78
|
+
}
|
|
79
|
+
function findSessionByUUID(sessionId) {
|
|
80
|
+
const baseDir = claudeProjectsDir();
|
|
81
|
+
if (!fs.existsSync(baseDir)) return null;
|
|
82
|
+
let dirs = [];
|
|
83
|
+
try {
|
|
84
|
+
dirs = fs.readdirSync(baseDir).map((d) => path.join(baseDir, d)).filter((d) => {
|
|
85
|
+
try {
|
|
86
|
+
return fs.statSync(d).isDirectory();
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
for (const dir of dirs) {
|
|
95
|
+
try {
|
|
96
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
97
|
+
for (const file of files) {
|
|
98
|
+
if (file.replace(".jsonl", "") === sessionId) {
|
|
99
|
+
return path.join(dir, file);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
function cwdToProjectSlug(cwd) {
|
|
109
|
+
return cwd.replace(/[:\\/]/g, "-").replace(/^-+|-+$/g, "");
|
|
110
|
+
}
|
|
111
|
+
function findSessionByCwd(cwd) {
|
|
112
|
+
const slug = cwdToProjectSlug(cwd ?? process.cwd());
|
|
113
|
+
const projectDir = path.join(claudeProjectsDir(), slug);
|
|
114
|
+
if (!fs.existsSync(projectDir)) return null;
|
|
115
|
+
let files = [];
|
|
116
|
+
try {
|
|
117
|
+
files = fs.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl"));
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
let latestFile = null;
|
|
122
|
+
let latestTime = 0;
|
|
123
|
+
for (const file of files) {
|
|
124
|
+
const fullPath = path.join(projectDir, file);
|
|
125
|
+
try {
|
|
126
|
+
const mtime = fs.statSync(fullPath).mtimeMs;
|
|
127
|
+
if (mtime > latestTime) {
|
|
128
|
+
latestTime = mtime;
|
|
129
|
+
latestFile = fullPath;
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return latestFile;
|
|
136
|
+
}
|
|
137
|
+
function findLatestSession() {
|
|
138
|
+
const baseDir = claudeProjectsDir();
|
|
139
|
+
if (!fs.existsSync(baseDir)) {
|
|
140
|
+
console.warn(`[driftcli] Session directory not found: ${baseDir}`);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
let dirs = [];
|
|
144
|
+
try {
|
|
145
|
+
dirs = fs.readdirSync(baseDir).map((d) => path.join(baseDir, d)).filter((d) => {
|
|
146
|
+
try {
|
|
147
|
+
return fs.statSync(d).isDirectory();
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.warn(`[driftcli] Could not read session directory: ${err instanceof Error ? err.message : err}`);
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
let latestFile = null;
|
|
157
|
+
let latestTime = 0;
|
|
158
|
+
for (const dir of dirs) {
|
|
159
|
+
let files = [];
|
|
160
|
+
try {
|
|
161
|
+
files = fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
162
|
+
} catch {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
for (const file of files) {
|
|
166
|
+
const fullPath = path.join(dir, file);
|
|
167
|
+
try {
|
|
168
|
+
const mtime = fs.statSync(fullPath).mtimeMs;
|
|
169
|
+
if (mtime > latestTime) {
|
|
170
|
+
latestTime = mtime;
|
|
171
|
+
latestFile = fullPath;
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return latestFile;
|
|
179
|
+
}
|
|
180
|
+
var fs, path, os;
|
|
181
|
+
var init_claude_parser = __esm({
|
|
182
|
+
"src/watchers/claude-parser.ts"() {
|
|
183
|
+
"use strict";
|
|
184
|
+
fs = __toESM(require("fs"));
|
|
185
|
+
path = __toESM(require("path"));
|
|
186
|
+
os = __toESM(require("os"));
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// src/watchers/claude-adapter.ts
|
|
191
|
+
var ClaudeAdapter;
|
|
192
|
+
var init_claude_adapter = __esm({
|
|
193
|
+
"src/watchers/claude-adapter.ts"() {
|
|
194
|
+
"use strict";
|
|
195
|
+
init_claude_parser();
|
|
196
|
+
ClaudeAdapter = class {
|
|
197
|
+
constructor() {
|
|
198
|
+
this.name = "claude";
|
|
199
|
+
}
|
|
200
|
+
canParse(filePath) {
|
|
201
|
+
return filePath.includes(".claude") && filePath.endsWith(".jsonl");
|
|
202
|
+
}
|
|
203
|
+
parse(filePath) {
|
|
204
|
+
return parseJSONL(filePath);
|
|
205
|
+
}
|
|
206
|
+
findLatest() {
|
|
207
|
+
return findSessionByCwd() ?? findLatestSession();
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// src/watchers/gemini-adapter.ts
|
|
214
|
+
var fs2, path2, os2, GeminiAdapter;
|
|
215
|
+
var init_gemini_adapter = __esm({
|
|
216
|
+
"src/watchers/gemini-adapter.ts"() {
|
|
217
|
+
"use strict";
|
|
218
|
+
fs2 = __toESM(require("fs"));
|
|
219
|
+
path2 = __toESM(require("path"));
|
|
220
|
+
os2 = __toESM(require("os"));
|
|
221
|
+
GeminiAdapter = class {
|
|
222
|
+
constructor() {
|
|
223
|
+
this.name = "gemini";
|
|
224
|
+
}
|
|
225
|
+
canParse(filePath) {
|
|
226
|
+
return filePath.includes(".gemini") && filePath.endsWith(".jsonl");
|
|
227
|
+
}
|
|
228
|
+
parse(filePath) {
|
|
229
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
230
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
231
|
+
const messages = [];
|
|
232
|
+
let index = 0;
|
|
233
|
+
for (const line of lines) {
|
|
234
|
+
try {
|
|
235
|
+
const entry = JSON.parse(line);
|
|
236
|
+
if (entry.role !== "user" && entry.role !== "model") continue;
|
|
237
|
+
const text = (entry.parts ?? []).map((p) => p.text ?? "").join("\n").trim();
|
|
238
|
+
if (!text) continue;
|
|
239
|
+
const ts = entry.timestamp ? new Date(entry.timestamp).getTime() : Date.now();
|
|
240
|
+
messages.push({
|
|
241
|
+
id: `gemini-${index++}`,
|
|
242
|
+
role: entry.role === "model" ? "assistant" : "user",
|
|
243
|
+
content: text,
|
|
244
|
+
timestamp: isFinite(ts) ? ts : Date.now()
|
|
245
|
+
});
|
|
246
|
+
} catch {
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return messages;
|
|
250
|
+
}
|
|
251
|
+
findLatest() {
|
|
252
|
+
const geminiTmp = path2.join(
|
|
253
|
+
process.env.DRIFTCLI_HOME ?? os2.homedir(),
|
|
254
|
+
".gemini",
|
|
255
|
+
"tmp"
|
|
256
|
+
);
|
|
257
|
+
if (!fs2.existsSync(geminiTmp)) return null;
|
|
258
|
+
let latestFile = null;
|
|
259
|
+
let latestTime = 0;
|
|
260
|
+
try {
|
|
261
|
+
const sessionDirs = fs2.readdirSync(geminiTmp).map((d) => path2.join(geminiTmp, d)).filter((d) => {
|
|
262
|
+
try {
|
|
263
|
+
return fs2.statSync(d).isDirectory();
|
|
264
|
+
} catch {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
for (const dir of sessionDirs) {
|
|
269
|
+
try {
|
|
270
|
+
const files = fs2.readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
271
|
+
for (const file of files) {
|
|
272
|
+
const fullPath = path2.join(dir, file);
|
|
273
|
+
try {
|
|
274
|
+
const mtime = fs2.statSync(fullPath).mtimeMs;
|
|
275
|
+
if (mtime > latestTime) {
|
|
276
|
+
latestTime = mtime;
|
|
277
|
+
latestFile = fullPath;
|
|
278
|
+
}
|
|
279
|
+
} catch {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
return latestFile;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// src/watchers/codex-adapter.ts
|
|
297
|
+
var fs3, path3, os3, CodexAdapter;
|
|
298
|
+
var init_codex_adapter = __esm({
|
|
299
|
+
"src/watchers/codex-adapter.ts"() {
|
|
300
|
+
"use strict";
|
|
301
|
+
fs3 = __toESM(require("fs"));
|
|
302
|
+
path3 = __toESM(require("path"));
|
|
303
|
+
os3 = __toESM(require("os"));
|
|
304
|
+
CodexAdapter = class {
|
|
305
|
+
constructor() {
|
|
306
|
+
this.name = "codex";
|
|
307
|
+
}
|
|
308
|
+
canParse(filePath) {
|
|
309
|
+
return filePath.includes(".codex") && filePath.endsWith(".jsonl");
|
|
310
|
+
}
|
|
311
|
+
parse(filePath) {
|
|
312
|
+
const raw = fs3.readFileSync(filePath, "utf-8");
|
|
313
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
314
|
+
const messages = [];
|
|
315
|
+
let index = 0;
|
|
316
|
+
for (const line of lines) {
|
|
317
|
+
try {
|
|
318
|
+
const entry = JSON.parse(line);
|
|
319
|
+
if (entry.role !== "user" && entry.role !== "assistant") continue;
|
|
320
|
+
let text = "";
|
|
321
|
+
if (typeof entry.content === "string") {
|
|
322
|
+
text = entry.content.trim();
|
|
323
|
+
} else if (Array.isArray(entry.content)) {
|
|
324
|
+
text = entry.content.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text.trim()).join("\n").trim();
|
|
325
|
+
}
|
|
326
|
+
if (!text) continue;
|
|
327
|
+
let ts = Date.now();
|
|
328
|
+
if (entry.timestamp) {
|
|
329
|
+
const parsed = typeof entry.timestamp === "number" ? entry.timestamp : new Date(entry.timestamp).getTime();
|
|
330
|
+
if (isFinite(parsed)) ts = parsed;
|
|
331
|
+
}
|
|
332
|
+
messages.push({
|
|
333
|
+
id: `codex-${index++}`,
|
|
334
|
+
role: entry.role,
|
|
335
|
+
content: text,
|
|
336
|
+
timestamp: ts
|
|
337
|
+
});
|
|
338
|
+
} catch {
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return messages;
|
|
342
|
+
}
|
|
343
|
+
findLatest() {
|
|
344
|
+
const codexDir = path3.join(
|
|
345
|
+
process.env.DRIFTCLI_HOME ?? os3.homedir(),
|
|
346
|
+
".codex"
|
|
347
|
+
);
|
|
348
|
+
if (!fs3.existsSync(codexDir)) return null;
|
|
349
|
+
let latestFile = null;
|
|
350
|
+
let latestTime = 0;
|
|
351
|
+
const scan = (dir) => {
|
|
352
|
+
try {
|
|
353
|
+
for (const entry of fs3.readdirSync(dir)) {
|
|
354
|
+
const fullPath = path3.join(dir, entry);
|
|
355
|
+
try {
|
|
356
|
+
const stat = fs3.statSync(fullPath);
|
|
357
|
+
if (stat.isDirectory()) {
|
|
358
|
+
scan(fullPath);
|
|
359
|
+
} else if (entry.endsWith(".jsonl") && stat.mtimeMs > latestTime) {
|
|
360
|
+
latestTime = stat.mtimeMs;
|
|
361
|
+
latestFile = fullPath;
|
|
362
|
+
}
|
|
363
|
+
} catch {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
} catch {
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
scan(codexDir);
|
|
371
|
+
return latestFile;
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// src/watchers/adapter-registry.ts
|
|
378
|
+
function detectAdapter(filePath) {
|
|
379
|
+
return ADAPTERS.find((a) => a.canParse(filePath)) ?? ADAPTERS[0];
|
|
380
|
+
}
|
|
381
|
+
var ADAPTERS;
|
|
382
|
+
var init_adapter_registry = __esm({
|
|
383
|
+
"src/watchers/adapter-registry.ts"() {
|
|
384
|
+
"use strict";
|
|
385
|
+
init_claude_adapter();
|
|
386
|
+
init_gemini_adapter();
|
|
387
|
+
init_codex_adapter();
|
|
388
|
+
ADAPTERS = [new ClaudeAdapter(), new GeminiAdapter(), new CodexAdapter()];
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// src/watchers/session-resolver.ts
|
|
393
|
+
var SessionResolver;
|
|
394
|
+
var init_session_resolver = __esm({
|
|
395
|
+
"src/watchers/session-resolver.ts"() {
|
|
396
|
+
"use strict";
|
|
397
|
+
init_claude_parser();
|
|
398
|
+
init_claude_adapter();
|
|
399
|
+
init_adapter_registry();
|
|
400
|
+
SessionResolver = class {
|
|
401
|
+
constructor(cacheTtlMs = 5e3, adapter) {
|
|
402
|
+
this.cacheTtlMs = cacheTtlMs;
|
|
403
|
+
this.cached = null;
|
|
404
|
+
this.adapter = adapter ?? new ClaudeAdapter();
|
|
405
|
+
}
|
|
406
|
+
resolve() {
|
|
407
|
+
if (this.cached && this.cached.expiresAt > Date.now()) {
|
|
408
|
+
return this.cached.file;
|
|
409
|
+
}
|
|
410
|
+
const result = this.resolveFromEnv() ?? this.resolveFromCwd() ?? findLatestSession();
|
|
411
|
+
if (result) {
|
|
412
|
+
this.cached = { file: result, expiresAt: Date.now() + this.cacheTtlMs };
|
|
413
|
+
} else {
|
|
414
|
+
this.cached = null;
|
|
415
|
+
}
|
|
416
|
+
return result;
|
|
417
|
+
}
|
|
418
|
+
/** Return the adapter to use for parsing the resolved session file. */
|
|
419
|
+
getAdapter(filePath) {
|
|
420
|
+
if (filePath) return detectAdapter(filePath);
|
|
421
|
+
return this.adapter;
|
|
422
|
+
}
|
|
423
|
+
/** Force the next call to re-resolve rather than use the cache. */
|
|
424
|
+
invalidate() {
|
|
425
|
+
this.cached = null;
|
|
426
|
+
}
|
|
427
|
+
resolveFromCwd() {
|
|
428
|
+
const file = findSessionByCwd();
|
|
429
|
+
if (file && process.env.DRIFTCLI_DEBUG) {
|
|
430
|
+
const slug = file.split(/[\\/]/).slice(-2, -1)[0];
|
|
431
|
+
console.warn(`[driftcli] Session resolved via CWD match: ${slug}`);
|
|
432
|
+
}
|
|
433
|
+
return file;
|
|
434
|
+
}
|
|
435
|
+
resolveFromEnv() {
|
|
436
|
+
const sessionId = process.env.DRIFTCLI_SESSION_ID;
|
|
437
|
+
if (!sessionId) return null;
|
|
438
|
+
const found = findSessionByUUID(sessionId);
|
|
439
|
+
if (!found) {
|
|
440
|
+
console.warn(`[driftcli] DRIFTCLI_SESSION_ID="${sessionId}" set but no matching .jsonl found \u2014 falling back to newest file`);
|
|
441
|
+
}
|
|
442
|
+
return found;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// src/core/types.ts
|
|
449
|
+
function scoreToLevel(score) {
|
|
450
|
+
if (score < 30) return "fresh";
|
|
451
|
+
if (score <= 60) return "warming";
|
|
452
|
+
if (score <= 80) return "drifting";
|
|
453
|
+
return "polluted";
|
|
454
|
+
}
|
|
455
|
+
var DEFAULT_WEIGHTS;
|
|
456
|
+
var init_types = __esm({
|
|
457
|
+
"src/core/types.ts"() {
|
|
458
|
+
"use strict";
|
|
459
|
+
DEFAULT_WEIGHTS = {
|
|
460
|
+
contextSaturation: 0.2,
|
|
461
|
+
topicScatter: 0.12,
|
|
462
|
+
uncertaintySignals: 0.15,
|
|
463
|
+
codeInconsistency: 0.08,
|
|
464
|
+
repetition: 0.2,
|
|
465
|
+
goalDistance: 0.15,
|
|
466
|
+
confidenceDrift: 0.1
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// src/core/topic-analyzer.ts
|
|
472
|
+
function calculateTopicEntropy(messages) {
|
|
473
|
+
if (messages.length < 3) return 0;
|
|
474
|
+
const capped = messages.length > TOPIC_ENTROPY_MSG_CAP ? messages.slice(-TOPIC_ENTROPY_MSG_CAP) : messages;
|
|
475
|
+
const windows = createWindows(capped, 3);
|
|
476
|
+
if (windows.length < 2) return 0;
|
|
477
|
+
const corpus = windows.map((w) => w.join(" "));
|
|
478
|
+
const tfidfVectors = buildTfidfVectors(corpus);
|
|
479
|
+
const consecutiveSims = [];
|
|
480
|
+
for (let i = 1; i < tfidfVectors.length; i++) {
|
|
481
|
+
consecutiveSims.push(cosineSimilarity(tfidfVectors[i - 1], tfidfVectors[i]));
|
|
482
|
+
}
|
|
483
|
+
const avgConsecutive = consecutiveSims.reduce((a, b) => a + b, 0) / consecutiveSims.length;
|
|
484
|
+
const wideSims = [];
|
|
485
|
+
for (let i = 3; i < tfidfVectors.length; i += 3) {
|
|
486
|
+
wideSims.push(cosineSimilarity(tfidfVectors[i - 3], tfidfVectors[i]));
|
|
487
|
+
}
|
|
488
|
+
const avgWide = wideSims.length > 0 ? wideSims.reduce((a, b) => a + b, 0) / wideSims.length : avgConsecutive;
|
|
489
|
+
const blendedSimilarity = avgConsecutive * 0.6 + avgWide * 0.4;
|
|
490
|
+
const entropy = Math.round((1 - blendedSimilarity) * 100);
|
|
491
|
+
return Math.min(100, Math.max(0, entropy));
|
|
492
|
+
}
|
|
493
|
+
function calculateAnchorDrift(messages, userGoal) {
|
|
494
|
+
if (messages.length < 4) return 0;
|
|
495
|
+
const userMessages = messages.filter((m) => m.role === "user");
|
|
496
|
+
if (userMessages.length < 2) return 0;
|
|
497
|
+
const anchorDoc = userGoal ? userGoal : userMessages.slice(0, Math.min(2, userMessages.length)).map((m) => m.content).join(" ");
|
|
498
|
+
const recentMessages = messages.slice(-3);
|
|
499
|
+
const recentDoc = recentMessages.map((m) => m.content).join(" ");
|
|
500
|
+
if (!anchorDoc.trim() || !recentDoc.trim()) return 0;
|
|
501
|
+
const corpus = [anchorDoc, recentDoc];
|
|
502
|
+
const vectors = buildTfidfVectors(corpus);
|
|
503
|
+
if (vectors.length < 2) return 0;
|
|
504
|
+
const similarity = cosineSimilarity(vectors[0], vectors[1]);
|
|
505
|
+
if (typeof similarity !== "number" || isNaN(similarity)) return 0;
|
|
506
|
+
const rawDrift = 1 - similarity;
|
|
507
|
+
const score = Math.round(Math.min(100, rawDrift * 140));
|
|
508
|
+
return Math.max(0, isNaN(score) ? 0 : score);
|
|
509
|
+
}
|
|
510
|
+
function calculateGoalDriftCheckpoints(messages, userGoal) {
|
|
511
|
+
if (messages.length < 4) {
|
|
512
|
+
return {
|
|
513
|
+
checkpoints: [],
|
|
514
|
+
trajectory: "stable",
|
|
515
|
+
averageScore: 0,
|
|
516
|
+
startToEndDrift: 0
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
const userMessages = messages.filter((m) => m.role === "user");
|
|
520
|
+
if (userMessages.length < 2) {
|
|
521
|
+
return {
|
|
522
|
+
checkpoints: [],
|
|
523
|
+
trajectory: "stable",
|
|
524
|
+
averageScore: 0,
|
|
525
|
+
startToEndDrift: 0
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
const anchorTexts = userGoal ? [userGoal] : userMessages.slice(0, Math.min(2, userMessages.length));
|
|
529
|
+
const anchorDoc = Array.isArray(anchorTexts) ? anchorTexts.map((m) => typeof m === "string" ? m : m.content).join(" ") : anchorTexts;
|
|
530
|
+
if (!anchorDoc.trim()) {
|
|
531
|
+
return {
|
|
532
|
+
checkpoints: [],
|
|
533
|
+
trajectory: "stable",
|
|
534
|
+
averageScore: 0,
|
|
535
|
+
startToEndDrift: 0
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
const checkpoints = [];
|
|
539
|
+
const positions = [0, 25, 50, 75, 100];
|
|
540
|
+
for (const pos of positions) {
|
|
541
|
+
const endIdx = Math.max(1, Math.floor(messages.length * pos / 100));
|
|
542
|
+
const windowMessages = messages.slice(0, endIdx);
|
|
543
|
+
const windowDoc = windowMessages.map((m) => m.content).join(" ");
|
|
544
|
+
if (!windowDoc.trim()) {
|
|
545
|
+
checkpoints.push({
|
|
546
|
+
position: pos,
|
|
547
|
+
similarity: 1,
|
|
548
|
+
// Empty window = same as anchor
|
|
549
|
+
driftScore: 0
|
|
550
|
+
});
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
const corpus = [anchorDoc, windowDoc];
|
|
554
|
+
const vectors = buildTfidfVectors(corpus);
|
|
555
|
+
if (vectors.length < 2) {
|
|
556
|
+
checkpoints.push({
|
|
557
|
+
position: pos,
|
|
558
|
+
similarity: pos === 0 ? 1 : 0,
|
|
559
|
+
driftScore: pos === 0 ? 0 : 100
|
|
560
|
+
});
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
const similarity = cosineSimilarity(vectors[0], vectors[1]);
|
|
564
|
+
const validSimilarity = typeof similarity === "number" && !isNaN(similarity) ? similarity : 0;
|
|
565
|
+
const driftScore = Math.round(
|
|
566
|
+
Math.min(100, (1 - validSimilarity) * 140)
|
|
567
|
+
);
|
|
568
|
+
checkpoints.push({
|
|
569
|
+
position: pos,
|
|
570
|
+
similarity: Math.max(0, Math.min(1, validSimilarity)),
|
|
571
|
+
driftScore: Math.max(0, isNaN(driftScore) ? 0 : driftScore)
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
const scores = checkpoints.map((c) => c.driftScore);
|
|
575
|
+
const trajectory = detectTrajectory(scores);
|
|
576
|
+
const driftsAfterStart = scores.slice(1);
|
|
577
|
+
const averageScore = driftsAfterStart.length ? Math.round(driftsAfterStart.reduce((a, b) => a + b, 0) / driftsAfterStart.length) : 0;
|
|
578
|
+
const startToEndDrift = checkpoints.length > 1 ? checkpoints[checkpoints.length - 1].driftScore : 0;
|
|
579
|
+
return {
|
|
580
|
+
checkpoints,
|
|
581
|
+
trajectory,
|
|
582
|
+
averageScore,
|
|
583
|
+
startToEndDrift
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function detectTrajectory(scores) {
|
|
587
|
+
if (scores.length < 3) return "stable";
|
|
588
|
+
const changes = [];
|
|
589
|
+
for (let i = 1; i < scores.length; i++) {
|
|
590
|
+
changes.push(scores[i] - scores[i - 1]);
|
|
591
|
+
}
|
|
592
|
+
const avgChange = changes.reduce((a, b) => a + b, 0) / changes.length;
|
|
593
|
+
const volatility = Math.sqrt(
|
|
594
|
+
changes.reduce((sum, c) => sum + (c - avgChange) ** 2, 0) / changes.length
|
|
595
|
+
);
|
|
596
|
+
const declineCount = changes.filter((c) => c < 0).length;
|
|
597
|
+
const increaseCount = changes.filter((c) => c > 0).length;
|
|
598
|
+
const hasSharpDrop = changes.some((c) => c < -20);
|
|
599
|
+
const isGradualDecline = declineCount >= changes.length * 0.6 && avgChange < -3 && !hasSharpDrop;
|
|
600
|
+
const isRecovery = scores[0] > 20 && scores.some((s, i) => i > 0 && i < scores.length - 1 && s > scores[scores.length - 1] + 15) && changes[changes.length - 1] > 0;
|
|
601
|
+
const isVolatile = volatility > 20 && increaseCount > 0 && declineCount > 0;
|
|
602
|
+
if (hasSharpDrop) return "sharp_drop";
|
|
603
|
+
if (isGradualDecline) return "gradual_decline";
|
|
604
|
+
if (isRecovery) return "recovery";
|
|
605
|
+
if (isVolatile) return "volatile";
|
|
606
|
+
return "stable";
|
|
607
|
+
}
|
|
608
|
+
function buildTfidfVectors(corpus) {
|
|
609
|
+
const n = corpus.length;
|
|
610
|
+
if (n === 0) return [];
|
|
611
|
+
const tokenized = corpus.map((doc) => tokenize(doc));
|
|
612
|
+
const df = /* @__PURE__ */ new Map();
|
|
613
|
+
for (const tokens of tokenized) {
|
|
614
|
+
const unique = new Set(tokens);
|
|
615
|
+
for (const term of unique) {
|
|
616
|
+
df.set(term, (df.get(term) || 0) + 1);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return tokenized.map((tokens) => {
|
|
620
|
+
const vector = /* @__PURE__ */ new Map();
|
|
621
|
+
if (tokens.length === 0) return vector;
|
|
622
|
+
const tf = /* @__PURE__ */ new Map();
|
|
623
|
+
for (const term of tokens) {
|
|
624
|
+
tf.set(term, (tf.get(term) || 0) + 1);
|
|
625
|
+
}
|
|
626
|
+
for (const [term, count] of tf) {
|
|
627
|
+
const termFreq = count / tokens.length;
|
|
628
|
+
const docFreq = df.get(term) || 1;
|
|
629
|
+
const idf = Math.log((n + 1) / (docFreq + 1)) + 1;
|
|
630
|
+
vector.set(term, termFreq * idf);
|
|
631
|
+
}
|
|
632
|
+
return vector;
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
function cosineSimilarity(a, b) {
|
|
636
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
637
|
+
let dotProduct = 0;
|
|
638
|
+
let normA = 0;
|
|
639
|
+
let normB = 0;
|
|
640
|
+
for (const [term, weightA] of a) {
|
|
641
|
+
normA += weightA * weightA;
|
|
642
|
+
const weightB = b.get(term);
|
|
643
|
+
if (weightB !== void 0) {
|
|
644
|
+
dotProduct += weightA * weightB;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
for (const [, weightB] of b) {
|
|
648
|
+
normB += weightB * weightB;
|
|
649
|
+
}
|
|
650
|
+
normA = Math.max(0, normA);
|
|
651
|
+
normB = Math.max(0, normB);
|
|
652
|
+
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
|
|
653
|
+
if (denominator === 0 || isNaN(denominator)) return 0;
|
|
654
|
+
const result = dotProduct / denominator;
|
|
655
|
+
return isNaN(result) ? 0 : Math.max(0, Math.min(1, result));
|
|
656
|
+
}
|
|
657
|
+
function tokenize(text) {
|
|
658
|
+
const withoutCode = text.replace(/```[\s\S]*?```/g, " ");
|
|
659
|
+
return withoutCode.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length >= 4 && !STOP_WORDS.has(w));
|
|
660
|
+
}
|
|
661
|
+
function createWindows(messages, windowSize) {
|
|
662
|
+
const windows = [];
|
|
663
|
+
for (let i = 0; i <= messages.length - windowSize; i++) {
|
|
664
|
+
const windowTexts = messages.slice(i, i + windowSize).map((m) => {
|
|
665
|
+
const withoutCode = m.content.replace(/```[\s\S]*?```/g, " ");
|
|
666
|
+
return withoutCode.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length >= 4 && !STOP_WORDS.has(w));
|
|
667
|
+
}).flat();
|
|
668
|
+
windows.push(windowTexts);
|
|
669
|
+
}
|
|
670
|
+
return windows;
|
|
671
|
+
}
|
|
672
|
+
function extractTopTerms(messages, n = 5) {
|
|
673
|
+
const freq = /* @__PURE__ */ new Map();
|
|
674
|
+
for (const msg of messages) {
|
|
675
|
+
for (const term of tokenize(msg.content)) {
|
|
676
|
+
freq.set(term, (freq.get(term) ?? 0) + 1);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return [...freq.entries()].sort((a, b) => b[1] - a[1]).slice(0, n).map(([term]) => term);
|
|
680
|
+
}
|
|
681
|
+
function extractNgrams(text, n = 3) {
|
|
682
|
+
const tokens = tokenize(text).filter((w) => !/^\d/.test(w));
|
|
683
|
+
if (tokens.length < n) return /* @__PURE__ */ new Set();
|
|
684
|
+
const ngrams = /* @__PURE__ */ new Set();
|
|
685
|
+
for (let i = 0; i <= tokens.length - n; i++) {
|
|
686
|
+
ngrams.add(tokens.slice(i, i + n).join(" "));
|
|
687
|
+
}
|
|
688
|
+
return ngrams;
|
|
689
|
+
}
|
|
690
|
+
function fleschKincaidGrade(text) {
|
|
691
|
+
const sentences = countSentences(text);
|
|
692
|
+
const words = countWords(text);
|
|
693
|
+
const syllables = countSyllables(text);
|
|
694
|
+
if (sentences === 0 || words === 0) return 0;
|
|
695
|
+
const grade = 0.39 * (words / sentences) + 11.8 * (syllables / words) - 15.59;
|
|
696
|
+
return Math.max(0, grade);
|
|
697
|
+
}
|
|
698
|
+
function countSentences(text) {
|
|
699
|
+
const matches = text.match(/[.!?]+/g);
|
|
700
|
+
return Math.max(1, matches ? matches.length : 1);
|
|
701
|
+
}
|
|
702
|
+
function countWords(text) {
|
|
703
|
+
return text.split(/\s+/).filter((w) => w.length > 0).length;
|
|
704
|
+
}
|
|
705
|
+
function countSyllables(text) {
|
|
706
|
+
const words = text.toLowerCase().split(/\s+/).filter((w) => w.length > 0);
|
|
707
|
+
let total = 0;
|
|
708
|
+
for (const word of words) {
|
|
709
|
+
total += countWordSyllables(word);
|
|
710
|
+
}
|
|
711
|
+
return Math.max(1, total);
|
|
712
|
+
}
|
|
713
|
+
function countWordSyllables(word) {
|
|
714
|
+
const clean = word.replace(/[^a-z]/g, "");
|
|
715
|
+
if (clean.length <= 2) return 1;
|
|
716
|
+
const vowelGroups = clean.match(/[aeiouy]+/g);
|
|
717
|
+
let count = vowelGroups ? vowelGroups.length : 1;
|
|
718
|
+
if (clean.endsWith("e") && count > 1) count--;
|
|
719
|
+
if (clean.endsWith("le") && clean.length > 2 && !/[aeiouy]/.test(clean[clean.length - 3])) count++;
|
|
720
|
+
return Math.max(1, count);
|
|
721
|
+
}
|
|
722
|
+
var TOPIC_ENTROPY_MSG_CAP, STOP_WORDS;
|
|
723
|
+
var init_topic_analyzer = __esm({
|
|
724
|
+
"src/core/topic-analyzer.ts"() {
|
|
725
|
+
"use strict";
|
|
726
|
+
TOPIC_ENTROPY_MSG_CAP = 150;
|
|
727
|
+
STOP_WORDS = /* @__PURE__ */ new Set([
|
|
728
|
+
"about",
|
|
729
|
+
"after",
|
|
730
|
+
"also",
|
|
731
|
+
"been",
|
|
732
|
+
"before",
|
|
733
|
+
"being",
|
|
734
|
+
"between",
|
|
735
|
+
"both",
|
|
736
|
+
"came",
|
|
737
|
+
"come",
|
|
738
|
+
"could",
|
|
739
|
+
"each",
|
|
740
|
+
"from",
|
|
741
|
+
"have",
|
|
742
|
+
"here",
|
|
743
|
+
"herself",
|
|
744
|
+
"himself",
|
|
745
|
+
"into",
|
|
746
|
+
"just",
|
|
747
|
+
"like",
|
|
748
|
+
"make",
|
|
749
|
+
"many",
|
|
750
|
+
"might",
|
|
751
|
+
"more",
|
|
752
|
+
"most",
|
|
753
|
+
"much",
|
|
754
|
+
"must",
|
|
755
|
+
"never",
|
|
756
|
+
"only",
|
|
757
|
+
"other",
|
|
758
|
+
"over",
|
|
759
|
+
"said",
|
|
760
|
+
"same",
|
|
761
|
+
"should",
|
|
762
|
+
"since",
|
|
763
|
+
"some",
|
|
764
|
+
"still",
|
|
765
|
+
"such",
|
|
766
|
+
"take",
|
|
767
|
+
"than",
|
|
768
|
+
"that",
|
|
769
|
+
"their",
|
|
770
|
+
"them",
|
|
771
|
+
"then",
|
|
772
|
+
"there",
|
|
773
|
+
"these",
|
|
774
|
+
"they",
|
|
775
|
+
"this",
|
|
776
|
+
"those",
|
|
777
|
+
"through",
|
|
778
|
+
"under",
|
|
779
|
+
"very",
|
|
780
|
+
"want",
|
|
781
|
+
"well",
|
|
782
|
+
"were",
|
|
783
|
+
"what",
|
|
784
|
+
"when",
|
|
785
|
+
"where",
|
|
786
|
+
"which",
|
|
787
|
+
"while",
|
|
788
|
+
"will",
|
|
789
|
+
"with",
|
|
790
|
+
"would",
|
|
791
|
+
"your",
|
|
792
|
+
"does",
|
|
793
|
+
"done",
|
|
794
|
+
"doing",
|
|
795
|
+
"going",
|
|
796
|
+
"know",
|
|
797
|
+
"need",
|
|
798
|
+
"please",
|
|
799
|
+
"really",
|
|
800
|
+
"right",
|
|
801
|
+
"sure",
|
|
802
|
+
"tell",
|
|
803
|
+
"thank",
|
|
804
|
+
"thanks",
|
|
805
|
+
"that",
|
|
806
|
+
"think",
|
|
807
|
+
"though",
|
|
808
|
+
"using",
|
|
809
|
+
"want",
|
|
810
|
+
"work",
|
|
811
|
+
"yeah",
|
|
812
|
+
"actually",
|
|
813
|
+
"already",
|
|
814
|
+
"always",
|
|
815
|
+
"another",
|
|
816
|
+
"anything",
|
|
817
|
+
"around",
|
|
818
|
+
"because",
|
|
819
|
+
"better",
|
|
820
|
+
"called",
|
|
821
|
+
"cannot",
|
|
822
|
+
"didnt",
|
|
823
|
+
"doesnt",
|
|
824
|
+
"dont",
|
|
825
|
+
"enough",
|
|
826
|
+
"every",
|
|
827
|
+
"example",
|
|
828
|
+
"first",
|
|
829
|
+
"getting",
|
|
830
|
+
"given",
|
|
831
|
+
"gonna",
|
|
832
|
+
"great",
|
|
833
|
+
"hasnt",
|
|
834
|
+
"helps",
|
|
835
|
+
"heres",
|
|
836
|
+
"however",
|
|
837
|
+
"instead",
|
|
838
|
+
"keep",
|
|
839
|
+
"looking",
|
|
840
|
+
"means",
|
|
841
|
+
"maybe",
|
|
842
|
+
"nothing",
|
|
843
|
+
"people",
|
|
844
|
+
"point",
|
|
845
|
+
"probably",
|
|
846
|
+
"something",
|
|
847
|
+
"sorry",
|
|
848
|
+
"start",
|
|
849
|
+
"still",
|
|
850
|
+
"things",
|
|
851
|
+
"trying",
|
|
852
|
+
"understand",
|
|
853
|
+
"used",
|
|
854
|
+
"without",
|
|
855
|
+
"youre",
|
|
856
|
+
// Chat-speak
|
|
857
|
+
"therre",
|
|
858
|
+
"thnx",
|
|
859
|
+
"thanx",
|
|
860
|
+
"wanna",
|
|
861
|
+
"kinda",
|
|
862
|
+
"gotta",
|
|
863
|
+
"lemme",
|
|
864
|
+
"soooo",
|
|
865
|
+
"sooo",
|
|
866
|
+
"okayy",
|
|
867
|
+
"yeahh",
|
|
868
|
+
"nope",
|
|
869
|
+
"yep",
|
|
870
|
+
// Generic non-informative tech words
|
|
871
|
+
"file",
|
|
872
|
+
"code",
|
|
873
|
+
"function",
|
|
874
|
+
"variable",
|
|
875
|
+
"method",
|
|
876
|
+
"class",
|
|
877
|
+
"type",
|
|
878
|
+
"value",
|
|
879
|
+
"data",
|
|
880
|
+
"list",
|
|
881
|
+
"array",
|
|
882
|
+
"object",
|
|
883
|
+
"string",
|
|
884
|
+
"number",
|
|
885
|
+
"return",
|
|
886
|
+
"output",
|
|
887
|
+
"input",
|
|
888
|
+
"result",
|
|
889
|
+
"issue",
|
|
890
|
+
"problem",
|
|
891
|
+
"question",
|
|
892
|
+
"answer",
|
|
893
|
+
"approach",
|
|
894
|
+
"solution",
|
|
895
|
+
"basically",
|
|
896
|
+
"stuff",
|
|
897
|
+
"thing",
|
|
898
|
+
"part",
|
|
899
|
+
"case",
|
|
900
|
+
"line",
|
|
901
|
+
"step",
|
|
902
|
+
// Dutch
|
|
903
|
+
"deze",
|
|
904
|
+
"voor",
|
|
905
|
+
"maar",
|
|
906
|
+
"ook",
|
|
907
|
+
"zijn",
|
|
908
|
+
"niet",
|
|
909
|
+
"meer",
|
|
910
|
+
"naar",
|
|
911
|
+
"hier",
|
|
912
|
+
"goed",
|
|
913
|
+
"heel",
|
|
914
|
+
"hoe",
|
|
915
|
+
"als",
|
|
916
|
+
"dan",
|
|
917
|
+
"alle",
|
|
918
|
+
"veel",
|
|
919
|
+
"omdat",
|
|
920
|
+
"welke",
|
|
921
|
+
"weten",
|
|
922
|
+
"alleen",
|
|
923
|
+
"iets",
|
|
924
|
+
"andere",
|
|
925
|
+
"zonder",
|
|
926
|
+
"hebben",
|
|
927
|
+
"wordt",
|
|
928
|
+
"kunnen",
|
|
929
|
+
"moeten",
|
|
930
|
+
"willen",
|
|
931
|
+
"zouden",
|
|
932
|
+
"eigenlijk",
|
|
933
|
+
"misschien",
|
|
934
|
+
"graag",
|
|
935
|
+
"bedankt",
|
|
936
|
+
"prima",
|
|
937
|
+
"klopt",
|
|
938
|
+
"precies",
|
|
939
|
+
// German
|
|
940
|
+
"aber",
|
|
941
|
+
"auch",
|
|
942
|
+
"dein",
|
|
943
|
+
"denn",
|
|
944
|
+
"doch",
|
|
945
|
+
"durch",
|
|
946
|
+
"eine",
|
|
947
|
+
"ganz",
|
|
948
|
+
"gern",
|
|
949
|
+
"hier",
|
|
950
|
+
"jede",
|
|
951
|
+
"kann",
|
|
952
|
+
"mehr",
|
|
953
|
+
"nach",
|
|
954
|
+
"noch",
|
|
955
|
+
"ohne",
|
|
956
|
+
"oder",
|
|
957
|
+
"sein",
|
|
958
|
+
"sich",
|
|
959
|
+
"\xFCber",
|
|
960
|
+
"unter",
|
|
961
|
+
"viel",
|
|
962
|
+
"weil",
|
|
963
|
+
"wenn",
|
|
964
|
+
"wird",
|
|
965
|
+
"diese",
|
|
966
|
+
"haben",
|
|
967
|
+
"jetzt",
|
|
968
|
+
"muss",
|
|
969
|
+
"nicht",
|
|
970
|
+
"sehr",
|
|
971
|
+
"danke",
|
|
972
|
+
"bitte",
|
|
973
|
+
"genau",
|
|
974
|
+
"vielleicht",
|
|
975
|
+
"eigentlich",
|
|
976
|
+
"bestimmt",
|
|
977
|
+
// French
|
|
978
|
+
"aussi",
|
|
979
|
+
"avec",
|
|
980
|
+
"bien",
|
|
981
|
+
"cette",
|
|
982
|
+
"dans",
|
|
983
|
+
"elle",
|
|
984
|
+
"\xEAtre",
|
|
985
|
+
"fait",
|
|
986
|
+
"mais",
|
|
987
|
+
"m\xEAme",
|
|
988
|
+
"nous",
|
|
989
|
+
"pour",
|
|
990
|
+
"sans",
|
|
991
|
+
"sont",
|
|
992
|
+
"tout",
|
|
993
|
+
"tr\xE8s",
|
|
994
|
+
"vous",
|
|
995
|
+
"apr\xE8s",
|
|
996
|
+
"autre",
|
|
997
|
+
"avant",
|
|
998
|
+
"avoir",
|
|
999
|
+
"comme",
|
|
1000
|
+
"encore",
|
|
1001
|
+
"entre",
|
|
1002
|
+
"faire",
|
|
1003
|
+
"leurs",
|
|
1004
|
+
"merci",
|
|
1005
|
+
"notre",
|
|
1006
|
+
"parce",
|
|
1007
|
+
"plus",
|
|
1008
|
+
"quand",
|
|
1009
|
+
"quel",
|
|
1010
|
+
"sera",
|
|
1011
|
+
"tous",
|
|
1012
|
+
"votre",
|
|
1013
|
+
"donc",
|
|
1014
|
+
"peut",
|
|
1015
|
+
// Spanish
|
|
1016
|
+
"algo",
|
|
1017
|
+
"antes",
|
|
1018
|
+
"bien",
|
|
1019
|
+
"cada",
|
|
1020
|
+
"como",
|
|
1021
|
+
"cual",
|
|
1022
|
+
"desde",
|
|
1023
|
+
"donde",
|
|
1024
|
+
"esta",
|
|
1025
|
+
"esto",
|
|
1026
|
+
"hace",
|
|
1027
|
+
"hasta",
|
|
1028
|
+
"luego",
|
|
1029
|
+
"mejor",
|
|
1030
|
+
"mismo",
|
|
1031
|
+
"mucho",
|
|
1032
|
+
"nada",
|
|
1033
|
+
"otro",
|
|
1034
|
+
"para",
|
|
1035
|
+
"pero",
|
|
1036
|
+
"poco",
|
|
1037
|
+
"puede",
|
|
1038
|
+
"sino",
|
|
1039
|
+
"solo",
|
|
1040
|
+
"tambi\xE9n",
|
|
1041
|
+
"tiene",
|
|
1042
|
+
"todo",
|
|
1043
|
+
"todos",
|
|
1044
|
+
"usted",
|
|
1045
|
+
"sobre",
|
|
1046
|
+
"gracias",
|
|
1047
|
+
"bueno",
|
|
1048
|
+
"creo",
|
|
1049
|
+
"ahora",
|
|
1050
|
+
"entonces",
|
|
1051
|
+
"realmente",
|
|
1052
|
+
"bastante",
|
|
1053
|
+
// Portuguese
|
|
1054
|
+
"agora",
|
|
1055
|
+
"algo",
|
|
1056
|
+
"antes",
|
|
1057
|
+
"cada",
|
|
1058
|
+
"como",
|
|
1059
|
+
"desde",
|
|
1060
|
+
"esta",
|
|
1061
|
+
"isso",
|
|
1062
|
+
"mais",
|
|
1063
|
+
"muito",
|
|
1064
|
+
"nada",
|
|
1065
|
+
"onde",
|
|
1066
|
+
"para",
|
|
1067
|
+
"pode",
|
|
1068
|
+
"qual",
|
|
1069
|
+
"quando",
|
|
1070
|
+
"quem",
|
|
1071
|
+
"ser\xE1",
|
|
1072
|
+
"seus",
|
|
1073
|
+
"sobre",
|
|
1074
|
+
"tamb\xE9m",
|
|
1075
|
+
"tudo",
|
|
1076
|
+
"voc\xEA",
|
|
1077
|
+
"obrigado",
|
|
1078
|
+
"obrigada",
|
|
1079
|
+
"acho",
|
|
1080
|
+
"ent\xE3o",
|
|
1081
|
+
"realmente",
|
|
1082
|
+
"bastante"
|
|
1083
|
+
]);
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
// src/core/contradiction-detector.ts
|
|
1088
|
+
function countContradictions(assistantMessages) {
|
|
1089
|
+
let count = 0;
|
|
1090
|
+
for (let i = 0; i < assistantMessages.length; i++) {
|
|
1091
|
+
const msg = assistantMessages[i];
|
|
1092
|
+
const text = msg.content.toLowerCase();
|
|
1093
|
+
count += countPatternMatches(text, CORRECTION_PATTERNS);
|
|
1094
|
+
if (i > 0) {
|
|
1095
|
+
const prevText = assistantMessages[i - 1].content.toLowerCase();
|
|
1096
|
+
count += detectReversals(prevText, text);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return count;
|
|
1100
|
+
}
|
|
1101
|
+
function countPatternMatches(text, patterns) {
|
|
1102
|
+
let count = 0;
|
|
1103
|
+
for (const pattern of patterns) {
|
|
1104
|
+
const matches = text.match(pattern);
|
|
1105
|
+
if (matches) count += matches.length;
|
|
1106
|
+
}
|
|
1107
|
+
return count;
|
|
1108
|
+
}
|
|
1109
|
+
function detectReversals(prevText, currentText) {
|
|
1110
|
+
let reversals = 0;
|
|
1111
|
+
const hasReversalSignal = currentText.includes("actually") || currentText.includes("however") || currentText.includes("but ") || currentText.includes("instead") || // Dutch
|
|
1112
|
+
currentText.includes("eigenlijk") || currentText.includes("echter") || currentText.includes("maar ") || // German
|
|
1113
|
+
currentText.includes("eigentlich") || currentText.includes("allerdings") || currentText.includes("aber ") || currentText.includes("stattdessen") || // French
|
|
1114
|
+
currentText.includes("en fait") || currentText.includes("cependant") || currentText.includes("mais ") || currentText.includes("plut\xF4t") || // Spanish
|
|
1115
|
+
currentText.includes("en realidad") || currentText.includes("sin embargo") || currentText.includes("pero ") || currentText.includes("en cambio") || // Portuguese
|
|
1116
|
+
currentText.includes("na verdade") || currentText.includes("no entanto") || currentText.includes("mas ") || currentText.includes("em vez");
|
|
1117
|
+
if (hasReversalSignal) {
|
|
1118
|
+
for (const negation of NEGATION_PAIRS) {
|
|
1119
|
+
if (prevText.includes(negation.positive) && currentText.includes(negation.negative)) {
|
|
1120
|
+
reversals++;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return reversals;
|
|
1125
|
+
}
|
|
1126
|
+
var CORRECTION_PATTERNS, NEGATION_PAIRS;
|
|
1127
|
+
var init_contradiction_detector = __esm({
|
|
1128
|
+
"src/core/contradiction-detector.ts"() {
|
|
1129
|
+
"use strict";
|
|
1130
|
+
CORRECTION_PATTERNS = [
|
|
1131
|
+
// English — explicit corrections
|
|
1132
|
+
/i apologize/gi,
|
|
1133
|
+
/i was (?:wrong|mistaken|incorrect)/gi,
|
|
1134
|
+
/let me correct/gi,
|
|
1135
|
+
/let me (?:re-?do|re-?write|re-?phrase|fix) that/gi,
|
|
1136
|
+
/i (?:need to|should) clarify/gi,
|
|
1137
|
+
/upon (?:further )?(?:review|reflection|consideration)/gi,
|
|
1138
|
+
/i previously (?:said|mentioned|stated|suggested)/gi,
|
|
1139
|
+
/(?:ignore|disregard) (?:my |what i )(?:previous|earlier)/gi,
|
|
1140
|
+
/i made (?:a |an )?(?:error|mistake)/gi,
|
|
1141
|
+
/correction:/gi,
|
|
1142
|
+
/wait[,.]? (?:that's|i|let me)/gi,
|
|
1143
|
+
/actually[,.]? (?:that|i was|you're right|the|no)/gi,
|
|
1144
|
+
/you'?re (?:right|correct)[,.]? (?:i|let|my|the)/gi,
|
|
1145
|
+
/my (?:earlier|previous) (?:response|answer|suggestion) was/gi,
|
|
1146
|
+
/i (?:over|under)(?:looked|estimated|stated)/gi,
|
|
1147
|
+
/sorry[,.]? (?:i|that|for|about|let)/gi,
|
|
1148
|
+
/i (?:should have|shouldn't have|misspoke)/gi,
|
|
1149
|
+
/(?:good|fair) point[,.]? (?:i|let|you)/gi,
|
|
1150
|
+
/i stand corrected/gi,
|
|
1151
|
+
/to clarify (?:my|what|the)/gi,
|
|
1152
|
+
/(?:that|this) (?:is|was) (?:not )?(?:quite )?(?:right|correct|accurate)/gi,
|
|
1153
|
+
/i (?:incorrectly|mistakenly) (?:said|stated|suggested|assumed)/gi,
|
|
1154
|
+
/my (?:bad|apologies|mistake)/gi,
|
|
1155
|
+
/i (?:was |)(?:confused|confusing)/gi,
|
|
1156
|
+
/(?:let me|i'll) (?:start over|rethink|reconsider)/gi,
|
|
1157
|
+
// Dutch — self-correction patterns
|
|
1158
|
+
/(?:sorry|excuses)[,.]? (?:ik|dat|voor)/gi,
|
|
1159
|
+
/ik had (?:het |)(?:fout|mis|verkeerd)/gi,
|
|
1160
|
+
/laat me (?:dat |het |)(?:corrigeren|verbeteren|herstellen)/gi,
|
|
1161
|
+
/dat (?:klopt|klopte) (?:niet|eigenlijk niet)/gi,
|
|
1162
|
+
/ik (?:bedoelde|bedoel) eigenlijk/gi,
|
|
1163
|
+
/bij nader inzien/gi,
|
|
1164
|
+
/mijn fout/gi,
|
|
1165
|
+
// German — self-correction patterns
|
|
1166
|
+
/(?:entschuldigung|tut mir leid)[,.]? (?:ich|das|für)/gi,
|
|
1167
|
+
/ich (?:war|lag) (?:falsch|daneben)/gi,
|
|
1168
|
+
/lass mich (?:das |)(?:korrigieren|berichtigen|richtigstellen)/gi,
|
|
1169
|
+
/das (?:stimmt|stimmte) (?:nicht|so nicht)/gi,
|
|
1170
|
+
/ich (?:meinte|meine) eigentlich/gi,
|
|
1171
|
+
/bei näherer betrachtung/gi,
|
|
1172
|
+
/mein fehler/gi,
|
|
1173
|
+
/ich muss mich korrigieren/gi,
|
|
1174
|
+
/ich habe mich (?:geirrt|vertan|getäuscht)/gi,
|
|
1175
|
+
// French — self-correction patterns
|
|
1176
|
+
/(?:je m'excuse|pardon|désolé)[,.]? (?:je|c'|pour)/gi,
|
|
1177
|
+
/j'(?:avais|ai) (?:tort|fait une erreur)/gi,
|
|
1178
|
+
/laissez-moi (?:corriger|rectifier)/gi,
|
|
1179
|
+
/(?:c'est|c'était) (?:pas correct|inexact|une erreur)/gi,
|
|
1180
|
+
/en fait[,.]? (?:c'est|je|il|non)/gi,
|
|
1181
|
+
/après (?:réflexion|vérification)/gi,
|
|
1182
|
+
/mon erreur/gi,
|
|
1183
|
+
/je (?:me suis trompé|dois rectifier)/gi,
|
|
1184
|
+
// Spanish — self-correction patterns
|
|
1185
|
+
/(?:perdón|disculpa|lo siento)[,.]? (?:yo|eso|por)/gi,
|
|
1186
|
+
/(?:estaba|estuve) (?:equivocado|mal|incorrecto)/gi,
|
|
1187
|
+
/(?:déjame|permíteme) (?:corregir|rectificar)/gi,
|
|
1188
|
+
/(?:eso|esto) (?:no es|no era) (?:correcto|exacto)/gi,
|
|
1189
|
+
/en realidad[,.]? (?:es|yo|no|el)/gi,
|
|
1190
|
+
/pensándolo (?:bien|mejor)/gi,
|
|
1191
|
+
/mi error/gi,
|
|
1192
|
+
/me (?:equivoqué|confundí)/gi,
|
|
1193
|
+
// Portuguese — self-correction patterns
|
|
1194
|
+
/(?:desculpe?|perdão)[,.]? (?:eu|isso|por)/gi,
|
|
1195
|
+
/eu (?:estava|estive) (?:errado|equivocado|enganado)/gi,
|
|
1196
|
+
/(?:deixe-me|permita-me) (?:corrigir|retificar)/gi,
|
|
1197
|
+
/(?:isso|isto) (?:não é|não estava) (?:correto|certo)/gi,
|
|
1198
|
+
/na verdade[,.]? (?:é|eu|não|o)/gi,
|
|
1199
|
+
/pensando (?:bem|melhor)/gi,
|
|
1200
|
+
/meu erro/gi,
|
|
1201
|
+
/eu (?:me enganei|me confundi|errei)/gi
|
|
1202
|
+
];
|
|
1203
|
+
NEGATION_PAIRS = [
|
|
1204
|
+
// English
|
|
1205
|
+
{ positive: "you should", negative: "you shouldn't" },
|
|
1206
|
+
{ positive: "you should", negative: "you should not" },
|
|
1207
|
+
{ positive: "recommend", negative: "don't recommend" },
|
|
1208
|
+
{ positive: "recommend", negative: "do not recommend" },
|
|
1209
|
+
{ positive: "best practice", negative: "not.+best practice" },
|
|
1210
|
+
{ positive: "you can", negative: "you can't" },
|
|
1211
|
+
{ positive: "you can", negative: "you cannot" },
|
|
1212
|
+
{ positive: "safe to", negative: "not safe to" },
|
|
1213
|
+
{ positive: "correct", negative: "incorrect" },
|
|
1214
|
+
// Dutch
|
|
1215
|
+
{ positive: "je moet", negative: "je moet niet" },
|
|
1216
|
+
{ positive: "je kunt", negative: "je kunt niet" },
|
|
1217
|
+
{ positive: "aanbevolen", negative: "niet aanbevolen" },
|
|
1218
|
+
{ positive: "veilig", negative: "niet veilig" },
|
|
1219
|
+
// German
|
|
1220
|
+
{ positive: "du solltest", negative: "du solltest nicht" },
|
|
1221
|
+
{ positive: "du kannst", negative: "du kannst nicht" },
|
|
1222
|
+
{ positive: "empfohlen", negative: "nicht empfohlen" },
|
|
1223
|
+
{ positive: "sicher", negative: "nicht sicher" },
|
|
1224
|
+
// French
|
|
1225
|
+
{ positive: "vous devriez", negative: "vous ne devriez pas" },
|
|
1226
|
+
{ positive: "vous pouvez", negative: "vous ne pouvez pas" },
|
|
1227
|
+
{ positive: "recommand\xE9", negative: "pas recommand\xE9" },
|
|
1228
|
+
{ positive: "correct", negative: "pas correct" },
|
|
1229
|
+
// Spanish
|
|
1230
|
+
{ positive: "deber\xEDas", negative: "no deber\xEDas" },
|
|
1231
|
+
{ positive: "puedes", negative: "no puedes" },
|
|
1232
|
+
{ positive: "recomendado", negative: "no recomendado" },
|
|
1233
|
+
{ positive: "seguro", negative: "no es seguro" },
|
|
1234
|
+
// Portuguese
|
|
1235
|
+
{ positive: "voc\xEA deveria", negative: "voc\xEA n\xE3o deveria" },
|
|
1236
|
+
{ positive: "voc\xEA pode", negative: "voc\xEA n\xE3o pode" },
|
|
1237
|
+
{ positive: "recomendado", negative: "n\xE3o recomendado" },
|
|
1238
|
+
{ positive: "seguro", negative: "n\xE3o \xE9 seguro" }
|
|
1239
|
+
];
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
// src/core/confidence-analyzer.ts
|
|
1244
|
+
function detectHedgingLanguage(messageContent) {
|
|
1245
|
+
if (!messageContent || messageContent.length < 10) return 0;
|
|
1246
|
+
const lower = messageContent.toLowerCase();
|
|
1247
|
+
let hedgingCount = 0;
|
|
1248
|
+
let totalRelevantWords = 0;
|
|
1249
|
+
for (const patterns of Object.values(HEDGING_PATTERNS)) {
|
|
1250
|
+
for (const pattern of patterns) {
|
|
1251
|
+
const regex = new RegExp(`\\b${pattern}\\b`, "gi");
|
|
1252
|
+
const matches = lower.match(regex);
|
|
1253
|
+
if (matches) {
|
|
1254
|
+
hedgingCount += matches.length;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
const words = lower.split(/\s+/).filter((w) => w.length > 2).length;
|
|
1259
|
+
totalRelevantWords = Math.max(words, 1);
|
|
1260
|
+
const hedgingRatio = hedgingCount / totalRelevantWords;
|
|
1261
|
+
return Math.min(100, Math.round(hedgingRatio * 200));
|
|
1262
|
+
}
|
|
1263
|
+
function trackConfidenceTrend(messages) {
|
|
1264
|
+
const assistantMessages = messages.filter((m) => m.role === "assistant");
|
|
1265
|
+
if (assistantMessages.length < 2) return 0;
|
|
1266
|
+
const earlyMessages = assistantMessages.slice(0, Math.ceil(assistantMessages.length / 3));
|
|
1267
|
+
const lateMessages = assistantMessages.slice(Math.floor(assistantMessages.length * 2 / 3));
|
|
1268
|
+
const earlyAvg = earlyMessages.reduce((sum, m) => sum + detectHedgingLanguage(m.content), 0) / earlyMessages.length;
|
|
1269
|
+
const lateAvg = lateMessages.reduce((sum, m) => sum + detectHedgingLanguage(m.content), 0) / lateMessages.length;
|
|
1270
|
+
const trendScore = Math.max(0, lateAvg - earlyAvg);
|
|
1271
|
+
return Math.round(trendScore);
|
|
1272
|
+
}
|
|
1273
|
+
function detectNegationReversals(messages) {
|
|
1274
|
+
const assistantMessages = messages.filter((m) => m.role === "assistant");
|
|
1275
|
+
if (assistantMessages.length < 2) return 0;
|
|
1276
|
+
let reversalCount = 0;
|
|
1277
|
+
for (const msg of assistantMessages) {
|
|
1278
|
+
const lower = msg.content.toLowerCase();
|
|
1279
|
+
for (const pattern of HEDGING_PATTERNS.negationPatterns) {
|
|
1280
|
+
const regex = new RegExp(`\\b${pattern}\\b`, "gi");
|
|
1281
|
+
if (regex.test(lower)) {
|
|
1282
|
+
reversalCount += 1;
|
|
1283
|
+
break;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
const baseScore = reversalCount / assistantMessages.length * 50;
|
|
1288
|
+
const lateReversals = assistantMessages.slice(-Math.ceil(assistantMessages.length / 3));
|
|
1289
|
+
const lateReversalCount = lateReversals.filter((m) => {
|
|
1290
|
+
const lower = m.content.toLowerCase();
|
|
1291
|
+
return HEDGING_PATTERNS.negationPatterns.some((p) => new RegExp(`\\b${p}\\b`, "i").test(lower));
|
|
1292
|
+
}).length;
|
|
1293
|
+
const lateBonus = lateReversalCount / Math.max(lateReversals.length, 1) * 30;
|
|
1294
|
+
return Math.min(100, Math.round(baseScore + lateBonus));
|
|
1295
|
+
}
|
|
1296
|
+
function calculateConfidenceDrift(messages) {
|
|
1297
|
+
if (messages.length < 2) return 0;
|
|
1298
|
+
const hedgingScore = messages.filter((m) => m.role === "assistant").reduce((sum, m) => sum + detectHedgingLanguage(m.content), 0) / Math.max(messages.filter((m) => m.role === "assistant").length, 1);
|
|
1299
|
+
const trendScore = trackConfidenceTrend(messages);
|
|
1300
|
+
const reversalScore = detectNegationReversals(messages);
|
|
1301
|
+
const composite = Math.round(
|
|
1302
|
+
hedgingScore * 0.4 + trendScore * 0.35 + reversalScore * 0.25
|
|
1303
|
+
);
|
|
1304
|
+
return Math.min(100, composite);
|
|
1305
|
+
}
|
|
1306
|
+
var HEDGING_PATTERNS;
|
|
1307
|
+
var init_confidence_analyzer = __esm({
|
|
1308
|
+
"src/core/confidence-analyzer.ts"() {
|
|
1309
|
+
"use strict";
|
|
1310
|
+
HEDGING_PATTERNS = {
|
|
1311
|
+
// Modal verbs + uncertainty
|
|
1312
|
+
modalUncertainty: [
|
|
1313
|
+
"might",
|
|
1314
|
+
"may",
|
|
1315
|
+
"could",
|
|
1316
|
+
"could be",
|
|
1317
|
+
"appears to be",
|
|
1318
|
+
"seems to be",
|
|
1319
|
+
"appears",
|
|
1320
|
+
"seems",
|
|
1321
|
+
"possibly",
|
|
1322
|
+
"arguably",
|
|
1323
|
+
"allegedly"
|
|
1324
|
+
],
|
|
1325
|
+
// Adverbs of uncertainty
|
|
1326
|
+
uncertainAdverbs: [
|
|
1327
|
+
"probably",
|
|
1328
|
+
"likely",
|
|
1329
|
+
"perhaps",
|
|
1330
|
+
"maybe",
|
|
1331
|
+
"somewhat",
|
|
1332
|
+
"relatively",
|
|
1333
|
+
"quite",
|
|
1334
|
+
"fairly",
|
|
1335
|
+
"rather",
|
|
1336
|
+
"approximately",
|
|
1337
|
+
"roughly",
|
|
1338
|
+
"sort of",
|
|
1339
|
+
"kind of",
|
|
1340
|
+
"a bit",
|
|
1341
|
+
"a little"
|
|
1342
|
+
],
|
|
1343
|
+
// Epistemic markers (subjective speech)
|
|
1344
|
+
epistemicMarkers: [
|
|
1345
|
+
"I think",
|
|
1346
|
+
"I believe",
|
|
1347
|
+
"in my opinion",
|
|
1348
|
+
"in my view",
|
|
1349
|
+
"it seems",
|
|
1350
|
+
"it appears",
|
|
1351
|
+
"I would say",
|
|
1352
|
+
"I'd say"
|
|
1353
|
+
],
|
|
1354
|
+
// Downgraders (reduce force of statement)
|
|
1355
|
+
downgraders: [
|
|
1356
|
+
"just",
|
|
1357
|
+
"only",
|
|
1358
|
+
"merely",
|
|
1359
|
+
"simply",
|
|
1360
|
+
"barely",
|
|
1361
|
+
"scarcely",
|
|
1362
|
+
"somewhat",
|
|
1363
|
+
"not quite",
|
|
1364
|
+
"not entirely",
|
|
1365
|
+
"not fully"
|
|
1366
|
+
],
|
|
1367
|
+
// Negation reversals (contradicting prior claims)
|
|
1368
|
+
negationPatterns: [
|
|
1369
|
+
"actually",
|
|
1370
|
+
"wait",
|
|
1371
|
+
"hold on",
|
|
1372
|
+
"correction",
|
|
1373
|
+
"let me correct",
|
|
1374
|
+
"upon reflection",
|
|
1375
|
+
"on second thought",
|
|
1376
|
+
"rethinking",
|
|
1377
|
+
"mistake",
|
|
1378
|
+
"I was wrong",
|
|
1379
|
+
"I apologize",
|
|
1380
|
+
"I retract"
|
|
1381
|
+
]
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
// src/core/drift-calculator.ts
|
|
1387
|
+
function calculateDrift(messages, weights = DEFAULT_WEIGHTS, userGoal) {
|
|
1388
|
+
if (messages.length === 0) {
|
|
1389
|
+
return emptyAnalysis(weights);
|
|
1390
|
+
}
|
|
1391
|
+
messages = messages.slice().sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
|
1392
|
+
for (let i = 1; i < messages.length; i++) {
|
|
1393
|
+
if ((messages[i].timestamp || 0) < (messages[i - 1].timestamp || 0)) {
|
|
1394
|
+
console.warn("[driftcli] Non-monotonic timestamps detected after sort \u2014 possible clock skew or corrupted session file");
|
|
1395
|
+
break;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
const factors = {
|
|
1399
|
+
contextSaturation: calcMessageDecay(messages),
|
|
1400
|
+
topicScatter: calculateTopicEntropy(messages),
|
|
1401
|
+
uncertaintySignals: calcContradictionScore(messages),
|
|
1402
|
+
codeInconsistency: calcCodeInconsistency(messages),
|
|
1403
|
+
repetition: calcRepetition(messages),
|
|
1404
|
+
goalDistance: calculateAnchorDrift(messages, userGoal),
|
|
1405
|
+
confidenceDrift: calculateConfidenceDrift(messages)
|
|
1406
|
+
};
|
|
1407
|
+
const score = Math.min(100, Math.max(0, Math.round(
|
|
1408
|
+
factors.contextSaturation * weights.contextSaturation + factors.topicScatter * weights.topicScatter + factors.uncertaintySignals * weights.uncertaintySignals + factors.codeInconsistency * weights.codeInconsistency + factors.repetition * weights.repetition + factors.goalDistance * weights.goalDistance + factors.confidenceDrift * weights.confidenceDrift
|
|
1409
|
+
)));
|
|
1410
|
+
const level = scoreToLevel(score);
|
|
1411
|
+
const sessionDuration = messages[messages.length - 1].timestamp - messages[0].timestamp;
|
|
1412
|
+
const goalDrift = calculateGoalDriftCheckpoints(messages, userGoal);
|
|
1413
|
+
return {
|
|
1414
|
+
score,
|
|
1415
|
+
level,
|
|
1416
|
+
factors,
|
|
1417
|
+
weights,
|
|
1418
|
+
messageCount: messages.length,
|
|
1419
|
+
sessionDuration,
|
|
1420
|
+
recommendations: generateRecommendations(score, factors),
|
|
1421
|
+
calculatedAt: Date.now(),
|
|
1422
|
+
goalDrift
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
function calcMessageDecay(messages) {
|
|
1426
|
+
let totalTokens = 0;
|
|
1427
|
+
for (const msg of messages) {
|
|
1428
|
+
const words = msg.content.split(/\s+/).filter((w) => w.length > 0).length;
|
|
1429
|
+
const hasCode = msg.content.includes("```");
|
|
1430
|
+
const tokenEstimate = Math.round(words * 1.3 * (hasCode ? 1.5 : 1));
|
|
1431
|
+
totalTokens += tokenEstimate;
|
|
1432
|
+
}
|
|
1433
|
+
if (totalTokens < 500) return 0;
|
|
1434
|
+
let score = Math.min(100, Math.round(15 * Math.log(totalTokens / 1500)));
|
|
1435
|
+
if (score < 0) score = 0;
|
|
1436
|
+
if (messages.length > 50) score += 10;
|
|
1437
|
+
else if (messages.length > 25) score += 5;
|
|
1438
|
+
const readabilityPenalty = calcReadabilityDecay(messages);
|
|
1439
|
+
score += readabilityPenalty;
|
|
1440
|
+
return Math.min(100, score);
|
|
1441
|
+
}
|
|
1442
|
+
function calcReadabilityDecay(messages) {
|
|
1443
|
+
const assistantMsgs = messages.filter((m) => m.role === "assistant" && m.content.length > 50);
|
|
1444
|
+
if (assistantMsgs.length < 4) return 0;
|
|
1445
|
+
const quarter = Math.max(2, Math.floor(assistantMsgs.length / 4));
|
|
1446
|
+
const earlyMsgs = assistantMsgs.slice(0, quarter);
|
|
1447
|
+
const lateMsgs = assistantMsgs.slice(-quarter);
|
|
1448
|
+
const earlyGrade = earlyMsgs.reduce((sum, m) => sum + fleschKincaidGrade(m.content), 0) / earlyMsgs.length;
|
|
1449
|
+
const lateGrade = lateMsgs.reduce((sum, m) => sum + fleschKincaidGrade(m.content), 0) / lateMsgs.length;
|
|
1450
|
+
const drop = earlyGrade - lateGrade;
|
|
1451
|
+
if (drop < 2) return 0;
|
|
1452
|
+
if (drop < 4) return 5;
|
|
1453
|
+
if (drop < 6) return 10;
|
|
1454
|
+
return 15;
|
|
1455
|
+
}
|
|
1456
|
+
function calcContradictionScore(messages) {
|
|
1457
|
+
const assistantMessages = messages.filter((m) => m.role === "assistant");
|
|
1458
|
+
if (assistantMessages.length === 0) return 0;
|
|
1459
|
+
const totalContradictions = countContradictions(assistantMessages);
|
|
1460
|
+
const score = Math.min(100, totalContradictions / 5 * 80);
|
|
1461
|
+
return Math.round(score);
|
|
1462
|
+
}
|
|
1463
|
+
function calcCodeInconsistency(messages) {
|
|
1464
|
+
const codeBlocks = extractCodeBlocks(messages);
|
|
1465
|
+
if (codeBlocks.length < 2) return 0;
|
|
1466
|
+
const languages = new Set(
|
|
1467
|
+
codeBlocks.map((b) => b.language).filter((l) => l !== "unknown")
|
|
1468
|
+
);
|
|
1469
|
+
if (languages.size <= 1) return 0;
|
|
1470
|
+
const score = Math.min(100, Math.round(15 + (languages.size - 1) * 20));
|
|
1471
|
+
return score;
|
|
1472
|
+
}
|
|
1473
|
+
function calcRepetition(messages) {
|
|
1474
|
+
if (messages.length < 8) return 0;
|
|
1475
|
+
const assistantMsgs = messages.filter((m) => m.role === "assistant").slice(-25);
|
|
1476
|
+
if (assistantMsgs.length < 4) return 0;
|
|
1477
|
+
const WINDOW = 4;
|
|
1478
|
+
const repetitionRates = [];
|
|
1479
|
+
for (let i = 3; i < assistantMsgs.length; i++) {
|
|
1480
|
+
const current = extractNgrams(assistantMsgs[i].content, 3);
|
|
1481
|
+
if (current.size < 3) continue;
|
|
1482
|
+
const pool = /* @__PURE__ */ new Set();
|
|
1483
|
+
for (let j = Math.max(0, i - WINDOW); j < i; j++) {
|
|
1484
|
+
for (const gram of extractNgrams(assistantMsgs[j].content, 3)) {
|
|
1485
|
+
pool.add(gram);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
let repeated = 0;
|
|
1489
|
+
for (const gram of current) {
|
|
1490
|
+
if (pool.has(gram)) repeated++;
|
|
1491
|
+
}
|
|
1492
|
+
const rate = repeated / current.size;
|
|
1493
|
+
repetitionRates.push(rate);
|
|
1494
|
+
}
|
|
1495
|
+
let codeScore = 0;
|
|
1496
|
+
const codeBlocksByMsg = assistantMsgs.map((m) => {
|
|
1497
|
+
return [...m.content.matchAll(/```\w*\n([\s\S]*?)```/g)].map((match) => match[1].replace(/\s+/g, " ").trim());
|
|
1498
|
+
});
|
|
1499
|
+
for (let i = 0; i < codeBlocksByMsg.length; i++) {
|
|
1500
|
+
for (let j = i + 2; j < Math.min(codeBlocksByMsg.length, i + WINDOW + 1); j++) {
|
|
1501
|
+
for (const blockA of codeBlocksByMsg[i]) {
|
|
1502
|
+
if (blockA.length < 20) continue;
|
|
1503
|
+
for (const blockB of codeBlocksByMsg[j]) {
|
|
1504
|
+
if (blockB.length < 20) continue;
|
|
1505
|
+
if (charSimilarity(blockA, blockB) > 0.8) codeScore += 25;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
if (repetitionRates.length === 0) return Math.min(100, codeScore);
|
|
1511
|
+
let weightedSum = 0;
|
|
1512
|
+
let totalWeight = 0;
|
|
1513
|
+
for (let i = 0; i < repetitionRates.length; i++) {
|
|
1514
|
+
const w = i + 1;
|
|
1515
|
+
weightedSum += repetitionRates[i] * w;
|
|
1516
|
+
totalWeight += w;
|
|
1517
|
+
}
|
|
1518
|
+
const avgRate = weightedSum / totalWeight;
|
|
1519
|
+
let textScore = 0;
|
|
1520
|
+
if (avgRate >= 0.6) {
|
|
1521
|
+
textScore = Math.round(80 + Math.min(20, (avgRate - 0.6) / 0.4 * 20));
|
|
1522
|
+
} else if (avgRate >= 0.35) {
|
|
1523
|
+
textScore = Math.round(40 + (avgRate - 0.35) / 0.25 * 40);
|
|
1524
|
+
} else if (avgRate >= 0.15) {
|
|
1525
|
+
textScore = Math.round((avgRate - 0.15) / 0.2 * 40);
|
|
1526
|
+
}
|
|
1527
|
+
const finalScore = Math.min(100, textScore + codeScore);
|
|
1528
|
+
return finalScore;
|
|
1529
|
+
}
|
|
1530
|
+
function charSimilarity(a, b) {
|
|
1531
|
+
const maxLen = Math.max(a.length, b.length);
|
|
1532
|
+
if (maxLen === 0) return 1;
|
|
1533
|
+
if (a === b) return 1;
|
|
1534
|
+
let positionalMatches = 0;
|
|
1535
|
+
const minLen = Math.min(a.length, b.length);
|
|
1536
|
+
for (let i = 0; i < minLen; i++) {
|
|
1537
|
+
if (a[i] === b[i]) positionalMatches++;
|
|
1538
|
+
}
|
|
1539
|
+
const positionalScore = positionalMatches / maxLen;
|
|
1540
|
+
const bigramsA = /* @__PURE__ */ new Map();
|
|
1541
|
+
const bigramsB = /* @__PURE__ */ new Map();
|
|
1542
|
+
for (let i = 0; i < a.length - 1; i++) {
|
|
1543
|
+
const bg = a.slice(i, i + 2);
|
|
1544
|
+
bigramsA.set(bg, (bigramsA.get(bg) || 0) + 1);
|
|
1545
|
+
}
|
|
1546
|
+
for (let i = 0; i < b.length - 1; i++) {
|
|
1547
|
+
const bg = b.slice(i, i + 2);
|
|
1548
|
+
bigramsB.set(bg, (bigramsB.get(bg) || 0) + 1);
|
|
1549
|
+
}
|
|
1550
|
+
let intersection = 0;
|
|
1551
|
+
let union = 0;
|
|
1552
|
+
const allBigrams = /* @__PURE__ */ new Set([...bigramsA.keys(), ...bigramsB.keys()]);
|
|
1553
|
+
for (const bg of allBigrams) {
|
|
1554
|
+
const countA = bigramsA.get(bg) || 0;
|
|
1555
|
+
const countB = bigramsB.get(bg) || 0;
|
|
1556
|
+
intersection += Math.min(countA, countB);
|
|
1557
|
+
union += Math.max(countA, countB);
|
|
1558
|
+
}
|
|
1559
|
+
const bigramScore = union === 0 ? 1 : intersection / union;
|
|
1560
|
+
return Math.max(positionalScore, bigramScore);
|
|
1561
|
+
}
|
|
1562
|
+
function extractCodeBlocks(messages) {
|
|
1563
|
+
const blocks = [];
|
|
1564
|
+
for (const msg of messages) {
|
|
1565
|
+
for (const match of msg.content.matchAll(/```(\w*)\n([\s\S]*?)```/g)) {
|
|
1566
|
+
const language = detectLanguage(match[1], match[2]);
|
|
1567
|
+
blocks.push({ language, content: match[2] });
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
return blocks;
|
|
1571
|
+
}
|
|
1572
|
+
function detectLanguage(label, content) {
|
|
1573
|
+
if (label) return label.toLowerCase();
|
|
1574
|
+
if (content.includes("import React") || content.includes("useState")) return "jsx";
|
|
1575
|
+
if (content.includes("def ") && content.includes(":")) return "python";
|
|
1576
|
+
if (content.includes("func ") && content.includes("{")) return "go";
|
|
1577
|
+
if (content.includes("fn ") && content.includes("->")) return "rust";
|
|
1578
|
+
if (content.includes("function") || content.includes("const ")) return "javascript";
|
|
1579
|
+
return "unknown";
|
|
1580
|
+
}
|
|
1581
|
+
function generateRecommendations(score, factors) {
|
|
1582
|
+
const recs = [];
|
|
1583
|
+
if (score <= 30) {
|
|
1584
|
+
recs.push("Context is clean \u2014 carry on!");
|
|
1585
|
+
return recs;
|
|
1586
|
+
}
|
|
1587
|
+
if (factors.contextSaturation > 50) {
|
|
1588
|
+
recs.push("Long conversation \u2014 consider starting fresh with a summary of key decisions.");
|
|
1589
|
+
}
|
|
1590
|
+
if (factors.topicScatter > 50) {
|
|
1591
|
+
recs.push("Multiple topics detected \u2014 try to keep one topic per conversation.");
|
|
1592
|
+
}
|
|
1593
|
+
if (factors.uncertaintySignals > 40) {
|
|
1594
|
+
recs.push("AI is self-correcting frequently \u2014 context may be confused. Re-state your requirements.");
|
|
1595
|
+
}
|
|
1596
|
+
if (factors.codeInconsistency > 30) {
|
|
1597
|
+
recs.push("Multiple languages/frameworks in one chat \u2014 start a new chat for each tech stack.");
|
|
1598
|
+
}
|
|
1599
|
+
if (factors.repetition > 30) {
|
|
1600
|
+
recs.push("AI is repeating itself \u2014 context is degrading. Start a new conversation.");
|
|
1601
|
+
}
|
|
1602
|
+
if (factors.goalDistance > 50) {
|
|
1603
|
+
recs.push("Conversation has drifted far from your original goal. Re-anchor or start fresh.");
|
|
1604
|
+
}
|
|
1605
|
+
if (factors.confidenceDrift > 40) {
|
|
1606
|
+
recs.push("AI confidence is declining \u2014 context may be becoming unreliable. Verify assumptions.");
|
|
1607
|
+
}
|
|
1608
|
+
if (score > 80) {
|
|
1609
|
+
recs.push("Strongly recommend starting a new conversation. Copy your key context first.");
|
|
1610
|
+
}
|
|
1611
|
+
return recs;
|
|
1612
|
+
}
|
|
1613
|
+
function emptyAnalysis(weights) {
|
|
1614
|
+
return {
|
|
1615
|
+
score: 0,
|
|
1616
|
+
level: "fresh",
|
|
1617
|
+
factors: {
|
|
1618
|
+
contextSaturation: 0,
|
|
1619
|
+
topicScatter: 0,
|
|
1620
|
+
uncertaintySignals: 0,
|
|
1621
|
+
codeInconsistency: 0,
|
|
1622
|
+
repetition: 0,
|
|
1623
|
+
goalDistance: 0,
|
|
1624
|
+
confidenceDrift: 0
|
|
1625
|
+
},
|
|
1626
|
+
weights,
|
|
1627
|
+
messageCount: 0,
|
|
1628
|
+
sessionDuration: 0,
|
|
1629
|
+
recommendations: ["No messages yet."],
|
|
1630
|
+
calculatedAt: Date.now(),
|
|
1631
|
+
goalDrift: {
|
|
1632
|
+
checkpoints: [],
|
|
1633
|
+
trajectory: "stable",
|
|
1634
|
+
averageScore: 0,
|
|
1635
|
+
startToEndDrift: 0
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
var init_drift_calculator = __esm({
|
|
1640
|
+
"src/core/drift-calculator.ts"() {
|
|
1641
|
+
"use strict";
|
|
1642
|
+
init_types();
|
|
1643
|
+
init_topic_analyzer();
|
|
1644
|
+
init_contradiction_detector();
|
|
1645
|
+
init_confidence_analyzer();
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
// src/config.ts
|
|
1650
|
+
function loadConfig() {
|
|
1651
|
+
const homeDir = process.env.DRIFTCLI_HOME ?? os4.homedir();
|
|
1652
|
+
const globalRaw = tryLoadJson(path4.join(homeDir, ".driftclirc")) ?? {};
|
|
1653
|
+
const projectRaw = tryLoadJson(path4.join(process.cwd(), ".driftcli")) ?? {};
|
|
1654
|
+
const warnThreshold = projectRaw.warnThreshold ?? globalRaw.warnThreshold ?? DEFAULT_CONFIG.warnThreshold;
|
|
1655
|
+
const cacheTtlMs = projectRaw.sessionResolution?.cacheTtlMs ?? globalRaw.sessionResolution?.cacheTtlMs ?? DEFAULT_CONFIG.sessionResolution.cacheTtlMs;
|
|
1656
|
+
const storageEnabled = projectRaw.storage?.enabled ?? globalRaw.storage?.enabled ?? DEFAULT_CONFIG.storage.enabled;
|
|
1657
|
+
const storageDirectory = projectRaw.storage?.directory ?? globalRaw.storage?.directory;
|
|
1658
|
+
const presetName = projectRaw.preset ?? globalRaw.preset;
|
|
1659
|
+
let weights = { ...DEFAULT_WEIGHTS };
|
|
1660
|
+
if (presetName !== void 0) {
|
|
1661
|
+
const presetWeights = WEIGHT_PRESETS[presetName];
|
|
1662
|
+
if (presetWeights) {
|
|
1663
|
+
weights = { ...presetWeights };
|
|
1664
|
+
} else {
|
|
1665
|
+
console.warn(`[driftcli] Unknown preset "${presetName}" \u2014 using default weights`);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
if (globalRaw.weights) {
|
|
1669
|
+
weights = { ...weights, ...globalRaw.weights };
|
|
1670
|
+
}
|
|
1671
|
+
if (projectRaw.weights) {
|
|
1672
|
+
weights = { ...weights, ...projectRaw.weights };
|
|
1673
|
+
}
|
|
1674
|
+
return {
|
|
1675
|
+
weights,
|
|
1676
|
+
preset: presetName,
|
|
1677
|
+
warnThreshold,
|
|
1678
|
+
sessionResolution: { cacheTtlMs },
|
|
1679
|
+
storage: { enabled: storageEnabled, ...storageDirectory ? { directory: storageDirectory } : {} }
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
function tryLoadJson(filePath) {
|
|
1683
|
+
try {
|
|
1684
|
+
if (!fs4.existsSync(filePath)) return null;
|
|
1685
|
+
const raw = fs4.readFileSync(filePath, "utf-8");
|
|
1686
|
+
return JSON.parse(raw);
|
|
1687
|
+
} catch (err) {
|
|
1688
|
+
console.warn(
|
|
1689
|
+
`[driftcli] Could not load config from ${filePath}: ${err instanceof Error ? err.message : err}`
|
|
1690
|
+
);
|
|
1691
|
+
return null;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
var fs4, path4, os4, WEIGHT_PRESETS, DEFAULT_CONFIG;
|
|
1695
|
+
var init_config = __esm({
|
|
1696
|
+
"src/config.ts"() {
|
|
1697
|
+
"use strict";
|
|
1698
|
+
fs4 = __toESM(require("fs"));
|
|
1699
|
+
path4 = __toESM(require("path"));
|
|
1700
|
+
os4 = __toESM(require("os"));
|
|
1701
|
+
init_types();
|
|
1702
|
+
WEIGHT_PRESETS = {
|
|
1703
|
+
/** Equal importance across all seven factors. */
|
|
1704
|
+
strict: {
|
|
1705
|
+
contextSaturation: 1 / 7,
|
|
1706
|
+
topicScatter: 1 / 7,
|
|
1707
|
+
uncertaintySignals: 1 / 7,
|
|
1708
|
+
codeInconsistency: 1 / 7,
|
|
1709
|
+
repetition: 1 / 7,
|
|
1710
|
+
goalDistance: 1 / 7,
|
|
1711
|
+
confidenceDrift: 1 / 7
|
|
1712
|
+
},
|
|
1713
|
+
/** Emphasises code consistency and repetition — good for focused coding sessions. */
|
|
1714
|
+
coding: {
|
|
1715
|
+
contextSaturation: 0.2,
|
|
1716
|
+
topicScatter: 0.08,
|
|
1717
|
+
uncertaintySignals: 0.1,
|
|
1718
|
+
codeInconsistency: 0.22,
|
|
1719
|
+
repetition: 0.25,
|
|
1720
|
+
goalDistance: 0.1,
|
|
1721
|
+
confidenceDrift: 0.05
|
|
1722
|
+
},
|
|
1723
|
+
/** Emphasises topic stability and goal alignment — good for research or planning. */
|
|
1724
|
+
research: {
|
|
1725
|
+
contextSaturation: 0.15,
|
|
1726
|
+
topicScatter: 0.2,
|
|
1727
|
+
uncertaintySignals: 0.15,
|
|
1728
|
+
codeInconsistency: 0.05,
|
|
1729
|
+
repetition: 0.15,
|
|
1730
|
+
goalDistance: 0.25,
|
|
1731
|
+
confidenceDrift: 0.05
|
|
1732
|
+
},
|
|
1733
|
+
/** Forgiving preset for brainstorming — topic scatter is not penalised heavily. */
|
|
1734
|
+
brainstorm: {
|
|
1735
|
+
contextSaturation: 0.25,
|
|
1736
|
+
topicScatter: 0.05,
|
|
1737
|
+
uncertaintySignals: 0.15,
|
|
1738
|
+
codeInconsistency: 0.05,
|
|
1739
|
+
repetition: 0.25,
|
|
1740
|
+
goalDistance: 0.1,
|
|
1741
|
+
confidenceDrift: 0.15
|
|
1742
|
+
}
|
|
1743
|
+
};
|
|
1744
|
+
DEFAULT_CONFIG = {
|
|
1745
|
+
weights: { ...DEFAULT_WEIGHTS },
|
|
1746
|
+
warnThreshold: 60,
|
|
1747
|
+
sessionResolution: {
|
|
1748
|
+
cacheTtlMs: 5e3
|
|
1749
|
+
},
|
|
1750
|
+
storage: {
|
|
1751
|
+
enabled: true
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
// src/storage.ts
|
|
1758
|
+
var fs5, path5, os5, Storage;
|
|
1759
|
+
var init_storage = __esm({
|
|
1760
|
+
"src/storage.ts"() {
|
|
1761
|
+
"use strict";
|
|
1762
|
+
fs5 = __toESM(require("fs"));
|
|
1763
|
+
path5 = __toESM(require("path"));
|
|
1764
|
+
os5 = __toESM(require("os"));
|
|
1765
|
+
Storage = class {
|
|
1766
|
+
constructor(directory) {
|
|
1767
|
+
this.dir = directory ?? path5.join(
|
|
1768
|
+
process.env.DRIFTCLI_HOME ?? os5.homedir(),
|
|
1769
|
+
".driftcli",
|
|
1770
|
+
"history"
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
/** Derive the storage path for a given session key. */
|
|
1774
|
+
sessionPath(sessionKey) {
|
|
1775
|
+
return path5.join(this.dir, `${sessionKey}.jsonl`);
|
|
1776
|
+
}
|
|
1777
|
+
/** Append a drift snapshot for a session. Creates the directory if needed. */
|
|
1778
|
+
record(sessionKey, analysis) {
|
|
1779
|
+
try {
|
|
1780
|
+
if (!fs5.existsSync(this.dir)) {
|
|
1781
|
+
fs5.mkdirSync(this.dir, { recursive: true });
|
|
1782
|
+
}
|
|
1783
|
+
const snapshot = {
|
|
1784
|
+
score: analysis.score,
|
|
1785
|
+
level: analysis.level,
|
|
1786
|
+
factors: { ...analysis.factors },
|
|
1787
|
+
messageCount: analysis.messageCount,
|
|
1788
|
+
sessionDuration: analysis.sessionDuration,
|
|
1789
|
+
calculatedAt: analysis.calculatedAt
|
|
1790
|
+
};
|
|
1791
|
+
fs5.appendFileSync(this.sessionPath(sessionKey), JSON.stringify(snapshot) + "\n", "utf-8");
|
|
1792
|
+
} catch (err) {
|
|
1793
|
+
console.warn(`[driftcli] Storage write failed: ${err instanceof Error ? err.message : err}`);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
/** Read the last `limit` snapshots for a session. Returns [] if no history exists. */
|
|
1797
|
+
getHistory(sessionKey, limit = 20) {
|
|
1798
|
+
const filePath = this.sessionPath(sessionKey);
|
|
1799
|
+
if (!fs5.existsSync(filePath)) return [];
|
|
1800
|
+
try {
|
|
1801
|
+
const snapshots = [];
|
|
1802
|
+
const lines = fs5.readFileSync(filePath, "utf-8").split("\n").filter((l) => l.trim());
|
|
1803
|
+
for (const line of lines) {
|
|
1804
|
+
try {
|
|
1805
|
+
snapshots.push(JSON.parse(line));
|
|
1806
|
+
} catch {
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
return snapshots.slice(-limit);
|
|
1810
|
+
} catch (err) {
|
|
1811
|
+
console.warn(`[driftcli] Storage read failed: ${err instanceof Error ? err.message : err}`);
|
|
1812
|
+
return [];
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
/** Delete history for a session (e.g. after a handoff/reset). */
|
|
1816
|
+
clearHistory(sessionKey) {
|
|
1817
|
+
const filePath = this.sessionPath(sessionKey);
|
|
1818
|
+
try {
|
|
1819
|
+
if (fs5.existsSync(filePath)) fs5.unlinkSync(filePath);
|
|
1820
|
+
} catch (err) {
|
|
1821
|
+
console.warn(`[driftcli] Storage clear failed: ${err instanceof Error ? err.message : err}`);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1828
|
+
// src/ui.ts
|
|
1829
|
+
function levelColor(level) {
|
|
1830
|
+
switch (level) {
|
|
1831
|
+
case "fresh":
|
|
1832
|
+
return C.green;
|
|
1833
|
+
case "warming":
|
|
1834
|
+
return C.yellow;
|
|
1835
|
+
case "drifting":
|
|
1836
|
+
return C.red;
|
|
1837
|
+
case "polluted":
|
|
1838
|
+
return C.dim;
|
|
1839
|
+
default:
|
|
1840
|
+
return C.reset;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
function bar(score, width = 10) {
|
|
1844
|
+
const filled = Math.min(width, Math.round(score / 100 * width));
|
|
1845
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
1846
|
+
}
|
|
1847
|
+
function trendArrow(cur, prev) {
|
|
1848
|
+
if (prev === void 0) return " ";
|
|
1849
|
+
if (cur - prev > 2) return "\u2197";
|
|
1850
|
+
if (prev - cur > 2) return "\u2198";
|
|
1851
|
+
return "\u2192";
|
|
1852
|
+
}
|
|
1853
|
+
function sparkline(scores) {
|
|
1854
|
+
const CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
1855
|
+
return scores.map((s) => CHARS[Math.min(7, Math.floor(s / 12.5))]).join("");
|
|
1856
|
+
}
|
|
1857
|
+
function renderDashboard(analysis, prevFactors, sessionFile) {
|
|
1858
|
+
const color = levelColor(analysis.level);
|
|
1859
|
+
const emoji = LEVEL_EMOJI[analysis.level] ?? "\u2753";
|
|
1860
|
+
const project = path6.basename(path6.dirname(sessionFile));
|
|
1861
|
+
const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
1862
|
+
const sep = `${C.dim}${"\u2501".repeat(54)}${C.reset}`;
|
|
1863
|
+
const lines = [
|
|
1864
|
+
sep,
|
|
1865
|
+
` ${C.bold}DriftCLI${C.reset} \u2022 ${project} \u2022 ${time}`,
|
|
1866
|
+
``,
|
|
1867
|
+
` Score ${color}${C.bold}${analysis.score}${C.reset} ${emoji} ${color}${analysis.level.toUpperCase()}${C.reset} Messages ${analysis.messageCount}`,
|
|
1868
|
+
``
|
|
1869
|
+
];
|
|
1870
|
+
for (const [key, val] of Object.entries(analysis.factors)) {
|
|
1871
|
+
const prev = prevFactors?.[key];
|
|
1872
|
+
const arrow = trendArrow(val, prev);
|
|
1873
|
+
const label = (FACTOR_LABELS[key] ?? key).padEnd(20);
|
|
1874
|
+
const barStr = `${color}${bar(val)}${C.reset}`;
|
|
1875
|
+
const valStr = String(Math.round(val)).padStart(3);
|
|
1876
|
+
lines.push(` ${label} ${barStr} ${valStr} ${arrow}`);
|
|
1877
|
+
}
|
|
1878
|
+
lines.push(sep);
|
|
1879
|
+
return lines.join("\n");
|
|
1880
|
+
}
|
|
1881
|
+
function renderTrend(snapshots) {
|
|
1882
|
+
if (snapshots.length === 0) {
|
|
1883
|
+
return "No drift history recorded yet.\nTip: Call get_drift() a few times to build up trend data.";
|
|
1884
|
+
}
|
|
1885
|
+
const scores = snapshots.map((s) => s.score);
|
|
1886
|
+
const first = scores[0];
|
|
1887
|
+
const last = scores[scores.length - 1];
|
|
1888
|
+
const peak = Math.max(...scores);
|
|
1889
|
+
const peakSnap = snapshots[scores.indexOf(peak)];
|
|
1890
|
+
const avg = Math.round(scores.reduce((a, b) => a + b, 0) / scores.length);
|
|
1891
|
+
const delta = last - first;
|
|
1892
|
+
let trendLabel;
|
|
1893
|
+
if (delta > 15) trendLabel = "\u2197 rising";
|
|
1894
|
+
else if (delta < -15) trendLabel = "\u2198 falling";
|
|
1895
|
+
else trendLabel = "\u2192 stable";
|
|
1896
|
+
const scoreSeq = scores.length <= 10 ? scores.join(" \u2192 ") : [...scores.slice(0, 3), "\u2026", ...scores.slice(-3)].join(" \u2192 ");
|
|
1897
|
+
const peakEmoji = LEVEL_EMOJI[peakSnap.level] ?? "\u2753";
|
|
1898
|
+
const latestEmoji = LEVEL_EMOJI[snapshots[snapshots.length - 1].level] ?? "\u2753";
|
|
1899
|
+
const lines = [
|
|
1900
|
+
`Drift Trend \u2014 last ${snapshots.length} snapshot${snapshots.length === 1 ? "" : "s"}`,
|
|
1901
|
+
``,
|
|
1902
|
+
` ${sparkline(scores)}`,
|
|
1903
|
+
` ${scoreSeq}`,
|
|
1904
|
+
``,
|
|
1905
|
+
` Trend: ${trendLabel} (${delta >= 0 ? "+" : ""}${delta} over ${snapshots.length} checks)`,
|
|
1906
|
+
` Peak: ${peak} ${peakEmoji} ${peakSnap.level.toUpperCase()}`,
|
|
1907
|
+
` Average: ${avg}`,
|
|
1908
|
+
` Latest: ${last} ${latestEmoji} ${snapshots[snapshots.length - 1].level.toUpperCase()}`
|
|
1909
|
+
];
|
|
1910
|
+
if (last > 60) {
|
|
1911
|
+
lines.push(``, `\u26A0\uFE0F Drift is high. Consider calling get_handoff() to start fresh.`);
|
|
1912
|
+
}
|
|
1913
|
+
return lines.join("\n");
|
|
1914
|
+
}
|
|
1915
|
+
var path6, C, LEVEL_EMOJI, FACTOR_LABELS;
|
|
1916
|
+
var init_ui = __esm({
|
|
1917
|
+
"src/ui.ts"() {
|
|
1918
|
+
"use strict";
|
|
1919
|
+
path6 = __toESM(require("path"));
|
|
1920
|
+
C = {
|
|
1921
|
+
green: "\x1B[32m",
|
|
1922
|
+
yellow: "\x1B[33m",
|
|
1923
|
+
red: "\x1B[31m",
|
|
1924
|
+
dim: "\x1B[90m",
|
|
1925
|
+
bold: "\x1B[1m",
|
|
1926
|
+
reset: "\x1B[0m"
|
|
1927
|
+
};
|
|
1928
|
+
LEVEL_EMOJI = {
|
|
1929
|
+
fresh: "\u{1F7E2}",
|
|
1930
|
+
warming: "\u{1F7E1}",
|
|
1931
|
+
drifting: "\u{1F534}",
|
|
1932
|
+
polluted: "\u26AB"
|
|
1933
|
+
};
|
|
1934
|
+
FACTOR_LABELS = {
|
|
1935
|
+
contextSaturation: "Context Saturation",
|
|
1936
|
+
topicScatter: "Topic Scatter",
|
|
1937
|
+
uncertaintySignals: "Uncertainty",
|
|
1938
|
+
codeInconsistency: "Code Inconsistency",
|
|
1939
|
+
repetition: "Repetition",
|
|
1940
|
+
goalDistance: "Goal Distance",
|
|
1941
|
+
confidenceDrift: "Confidence Drift"
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
// src/cli.ts
|
|
1947
|
+
var cli_exports = {};
|
|
1948
|
+
__export(cli_exports, {
|
|
1949
|
+
run: () => run
|
|
1950
|
+
});
|
|
1951
|
+
function run() {
|
|
1952
|
+
const config2 = loadConfig();
|
|
1953
|
+
const resolver2 = new SessionResolver(config2.sessionResolution.cacheTtlMs);
|
|
1954
|
+
const storage2 = config2.storage.enabled ? new Storage(config2.storage.directory) : null;
|
|
1955
|
+
const sessionFile = resolver2.resolve();
|
|
1956
|
+
if (!sessionFile) {
|
|
1957
|
+
console.error("No Claude Code session files found in ~/.claude/projects/");
|
|
1958
|
+
console.error("Tip: set DRIFTCLI_SESSION_ID=<uuid> to pin a specific session.");
|
|
1959
|
+
process.exit(1);
|
|
1960
|
+
}
|
|
1961
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
1962
|
+
let lastMessageCount = 0;
|
|
1963
|
+
let prevFactors;
|
|
1964
|
+
function check() {
|
|
1965
|
+
const file = resolver2.resolve();
|
|
1966
|
+
if (!file) return;
|
|
1967
|
+
try {
|
|
1968
|
+
const messages = resolver2.getAdapter(file).parse(file);
|
|
1969
|
+
if (messages.length === lastMessageCount) return;
|
|
1970
|
+
lastMessageCount = messages.length;
|
|
1971
|
+
if (messages.length < 2) return;
|
|
1972
|
+
const chatMessages = messages.map((m) => ({
|
|
1973
|
+
...m,
|
|
1974
|
+
platform: "claude",
|
|
1975
|
+
tabId: 0,
|
|
1976
|
+
chatId: "cli"
|
|
1977
|
+
}));
|
|
1978
|
+
const analysis = calculateDrift(chatMessages, config2.weights);
|
|
1979
|
+
const level = analysis.level;
|
|
1980
|
+
const emoji = LEVEL_EMOJI[level] ?? "\u2753";
|
|
1981
|
+
process.stdout.write(`\x1B]0;${emoji} ${analysis.score} \u2013 DriftCLI\x07`);
|
|
1982
|
+
process.stdout.write("\x1B[H");
|
|
1983
|
+
process.stdout.write(renderDashboard(analysis, prevFactors, file) + "\n");
|
|
1984
|
+
prevFactors = { ...analysis.factors };
|
|
1985
|
+
if (storage2) {
|
|
1986
|
+
const sessionKey = path7.basename(file, ".jsonl");
|
|
1987
|
+
storage2.record(sessionKey, analysis);
|
|
1988
|
+
}
|
|
1989
|
+
} catch (err) {
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
check();
|
|
1993
|
+
setInterval(check, 3e3);
|
|
1994
|
+
}
|
|
1995
|
+
var path7;
|
|
1996
|
+
var init_cli = __esm({
|
|
1997
|
+
"src/cli.ts"() {
|
|
1998
|
+
"use strict";
|
|
1999
|
+
path7 = __toESM(require("path"));
|
|
2000
|
+
init_session_resolver();
|
|
2001
|
+
init_drift_calculator();
|
|
2002
|
+
init_config();
|
|
2003
|
+
init_storage();
|
|
2004
|
+
init_ui();
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
2007
|
+
|
|
2008
|
+
// src/mcp-server.ts
|
|
2009
|
+
var mcp_server_exports = {};
|
|
2010
|
+
__export(mcp_server_exports, {
|
|
2011
|
+
main: () => main
|
|
2012
|
+
});
|
|
2013
|
+
function buildHandoff(analysis, messages, chatMessages, emoji, level) {
|
|
2014
|
+
const durationMs = analysis.sessionDuration;
|
|
2015
|
+
const durationMin = durationMs > 0 ? Math.round(durationMs / 6e4) : null;
|
|
2016
|
+
const durationStr = durationMin !== null ? durationMin >= 60 ? `${Math.floor(durationMin / 60)}h ${durationMin % 60}m` : `${durationMin}m` : "unknown duration";
|
|
2017
|
+
const topTerms = extractTopTerms(chatMessages, 6);
|
|
2018
|
+
const topicsStr = topTerms.length > 0 ? topTerms.join(", ") : "not enough content to determine";
|
|
2019
|
+
const recentUserMessages = messages.filter((m) => m.role === "user").slice(-3).map((m, i) => `${i + 1}. ${m.content.slice(0, 200)}${m.content.length > 200 ? "\u2026" : ""}`).join("\n");
|
|
2020
|
+
let lastCodeSnippet = "";
|
|
2021
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2022
|
+
const match = [...messages[i].content.matchAll(/```(\w*)\n([\s\S]*?)```/g)].pop();
|
|
2023
|
+
if (match) {
|
|
2024
|
+
const lang = match[1] || "";
|
|
2025
|
+
const code = match[2].split("\n").slice(0, 20).join("\n");
|
|
2026
|
+
lastCodeSnippet = `\`\`\`${lang}
|
|
2027
|
+
${code}
|
|
2028
|
+
\`\`\``;
|
|
2029
|
+
break;
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
const lines = [
|
|
2033
|
+
`## Context Handoff`,
|
|
2034
|
+
``,
|
|
2035
|
+
`**Drift:** ${analysis.score}/100 ${emoji} ${level.toUpperCase()} | **Messages:** ${messages.length} | **Duration:** ${durationStr}`,
|
|
2036
|
+
``,
|
|
2037
|
+
`**Top topics:** ${topicsStr}`,
|
|
2038
|
+
``,
|
|
2039
|
+
`**Recent focus (last 3 user messages):**`,
|
|
2040
|
+
recentUserMessages
|
|
2041
|
+
];
|
|
2042
|
+
if (lastCodeSnippet) {
|
|
2043
|
+
lines.push(``, `**Last code context:**`, lastCodeSnippet);
|
|
2044
|
+
}
|
|
2045
|
+
lines.push(
|
|
2046
|
+
``,
|
|
2047
|
+
`---`,
|
|
2048
|
+
`*Paste the section below into your new session to restore context.*`,
|
|
2049
|
+
``,
|
|
2050
|
+
`**Continuing from a previous session** (drift was ${analysis.score}/100, ${messages.length} messages, ${durationStr}).`,
|
|
2051
|
+
`Main topics: ${topicsStr}.`,
|
|
2052
|
+
``,
|
|
2053
|
+
`Recent questions:`,
|
|
2054
|
+
recentUserMessages,
|
|
2055
|
+
``,
|
|
2056
|
+
`Please continue from here.`
|
|
2057
|
+
);
|
|
2058
|
+
return lines.join("\n");
|
|
2059
|
+
}
|
|
2060
|
+
async function main() {
|
|
2061
|
+
const transport = new import_stdio.StdioServerTransport();
|
|
2062
|
+
await server.connect(transport);
|
|
2063
|
+
}
|
|
2064
|
+
var path8, import_server, import_stdio, import_types3, config, resolver, storage, server, LEVEL_EMOJI2;
|
|
2065
|
+
var init_mcp_server = __esm({
|
|
2066
|
+
"src/mcp-server.ts"() {
|
|
2067
|
+
"use strict";
|
|
2068
|
+
path8 = __toESM(require("path"));
|
|
2069
|
+
import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
2070
|
+
import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
2071
|
+
import_types3 = require("@modelcontextprotocol/sdk/types.js");
|
|
2072
|
+
init_session_resolver();
|
|
2073
|
+
init_drift_calculator();
|
|
2074
|
+
init_types();
|
|
2075
|
+
init_topic_analyzer();
|
|
2076
|
+
init_config();
|
|
2077
|
+
init_storage();
|
|
2078
|
+
init_ui();
|
|
2079
|
+
config = loadConfig();
|
|
2080
|
+
resolver = new SessionResolver(config.sessionResolution.cacheTtlMs);
|
|
2081
|
+
storage = config.storage.enabled ? new Storage(config.storage.directory) : null;
|
|
2082
|
+
server = new import_server.Server(
|
|
2083
|
+
{ name: "driftcli", version: "0.1.0" },
|
|
2084
|
+
{ capabilities: { tools: {} } }
|
|
2085
|
+
);
|
|
2086
|
+
server.setRequestHandler(import_types3.ListToolsRequestSchema, async () => ({
|
|
2087
|
+
tools: [
|
|
2088
|
+
{
|
|
2089
|
+
name: "get_drift",
|
|
2090
|
+
description: "Returns the current drift score and factor breakdown for the active Claude Code session. Call this to check if the conversation context is degrading.",
|
|
2091
|
+
inputSchema: { type: "object", properties: {} }
|
|
2092
|
+
},
|
|
2093
|
+
{
|
|
2094
|
+
name: "get_handoff",
|
|
2095
|
+
description: "Generates a handoff prompt summarizing the current session state. Use this when drift score is high (>60) to help start a fresh context.",
|
|
2096
|
+
inputSchema: { type: "object", properties: {} }
|
|
2097
|
+
},
|
|
2098
|
+
{
|
|
2099
|
+
name: "get_trend",
|
|
2100
|
+
description: "Returns the drift history trend for the current session. Shows sparkline, score sequence, peak, average, and trajectory.",
|
|
2101
|
+
inputSchema: { type: "object", properties: {} }
|
|
2102
|
+
}
|
|
2103
|
+
]
|
|
2104
|
+
}));
|
|
2105
|
+
LEVEL_EMOJI2 = {
|
|
2106
|
+
fresh: "\u{1F7E2}",
|
|
2107
|
+
warming: "\u{1F7E1}",
|
|
2108
|
+
drifting: "\u{1F534}",
|
|
2109
|
+
polluted: "\u26AB"
|
|
2110
|
+
};
|
|
2111
|
+
server.setRequestHandler(import_types3.CallToolRequestSchema, async (request) => {
|
|
2112
|
+
let sessionFile;
|
|
2113
|
+
try {
|
|
2114
|
+
sessionFile = resolver.resolve();
|
|
2115
|
+
} catch (err) {
|
|
2116
|
+
return { content: [{ type: "text", text: `Error finding session: ${err instanceof Error ? err.message : err}` }] };
|
|
2117
|
+
}
|
|
2118
|
+
if (!sessionFile) {
|
|
2119
|
+
return { content: [{ type: "text", text: "No active Claude Code session found." }] };
|
|
2120
|
+
}
|
|
2121
|
+
const adapter = resolver.getAdapter(sessionFile);
|
|
2122
|
+
let messages;
|
|
2123
|
+
try {
|
|
2124
|
+
messages = adapter.parse(sessionFile);
|
|
2125
|
+
} catch (err) {
|
|
2126
|
+
return { content: [{ type: "text", text: `Error reading session file: ${err instanceof Error ? err.message : err}` }] };
|
|
2127
|
+
}
|
|
2128
|
+
if (messages.length < 2) {
|
|
2129
|
+
return { content: [{ type: "text", text: "Not enough messages to calculate drift yet." }] };
|
|
2130
|
+
}
|
|
2131
|
+
const chatMessages = messages.map((m) => ({
|
|
2132
|
+
...m,
|
|
2133
|
+
platform: "claude",
|
|
2134
|
+
tabId: 0,
|
|
2135
|
+
chatId: "cli"
|
|
2136
|
+
}));
|
|
2137
|
+
const analysis = calculateDrift(chatMessages, config.weights);
|
|
2138
|
+
const level = scoreToLevel(analysis.score);
|
|
2139
|
+
const emoji = LEVEL_EMOJI2[level] ?? "\u2753";
|
|
2140
|
+
const adapterTag = adapter.name !== "claude" ? ` (${adapter.name})` : "";
|
|
2141
|
+
if (request.params.name === "get_drift") {
|
|
2142
|
+
let trendLine = "";
|
|
2143
|
+
if (storage) {
|
|
2144
|
+
const sessionKey = path8.basename(sessionFile, ".jsonl");
|
|
2145
|
+
storage.record(sessionKey, analysis);
|
|
2146
|
+
const snapshots = storage.getHistory(sessionKey, 10);
|
|
2147
|
+
if (snapshots.length >= 3) {
|
|
2148
|
+
const scores = snapshots.map((s) => s.score);
|
|
2149
|
+
const delta = scores[scores.length - 1] - scores[0];
|
|
2150
|
+
const arrow = delta > 10 ? "\u2197" : delta < -10 ? "\u2198" : "\u2192";
|
|
2151
|
+
const sign = delta >= 0 ? "+" : "";
|
|
2152
|
+
trendLine = `Trend (last ${scores.length}): ${sparkline(scores)} ${sign}${delta} over ${scores.length} checks ${arrow}`;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
const factors = Object.entries(analysis.factors).map(([k, v]) => ` ${k}: ${v.toFixed(1)}`).join("\n");
|
|
2156
|
+
const isDegrading = analysis.score > config.warnThreshold;
|
|
2157
|
+
const lines = [
|
|
2158
|
+
`Drift Score: ${analysis.score} ${emoji} ${level.toUpperCase()}${adapterTag}`,
|
|
2159
|
+
`Messages: ${messages.length}`,
|
|
2160
|
+
``,
|
|
2161
|
+
`Factor breakdown:`,
|
|
2162
|
+
factors,
|
|
2163
|
+
``,
|
|
2164
|
+
isDegrading ? `\u26A0\uFE0F Context is degrading.` : `Context is healthy.`
|
|
2165
|
+
];
|
|
2166
|
+
if (trendLine) lines.push(``, trendLine);
|
|
2167
|
+
if (isDegrading) {
|
|
2168
|
+
lines.push(
|
|
2169
|
+
``,
|
|
2170
|
+
`---`,
|
|
2171
|
+
buildHandoff(analysis, messages, chatMessages, emoji, level)
|
|
2172
|
+
);
|
|
2173
|
+
}
|
|
2174
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2175
|
+
}
|
|
2176
|
+
if (request.params.name === "get_handoff") {
|
|
2177
|
+
return { content: [{ type: "text", text: buildHandoff(analysis, messages, chatMessages, emoji, level) }] };
|
|
2178
|
+
}
|
|
2179
|
+
if (request.params.name === "get_trend") {
|
|
2180
|
+
if (!storage) {
|
|
2181
|
+
return {
|
|
2182
|
+
content: [{
|
|
2183
|
+
type: "text",
|
|
2184
|
+
text: "Trend history is disabled.\nSet storage.enabled=true in ~/.driftclirc to activate it."
|
|
2185
|
+
}]
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
const sessionKey = path8.basename(sessionFile, ".jsonl");
|
|
2189
|
+
const snapshots = storage.getHistory(sessionKey);
|
|
2190
|
+
return { content: [{ type: "text", text: renderTrend(snapshots) }] };
|
|
2191
|
+
}
|
|
2192
|
+
return { content: [{ type: "text", text: "Unknown tool." }] };
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
});
|
|
2196
|
+
|
|
2197
|
+
// src/bin.ts
|
|
2198
|
+
var command = process.argv[2];
|
|
2199
|
+
if (command === "watch") {
|
|
2200
|
+
const { run: run2 } = (init_cli(), __toCommonJS(cli_exports));
|
|
2201
|
+
run2();
|
|
2202
|
+
} else {
|
|
2203
|
+
const { main: main2 } = (init_mcp_server(), __toCommonJS(mcp_server_exports));
|
|
2204
|
+
main2().catch(console.error);
|
|
2205
|
+
}
|