forge-openclaw-plugin 0.2.108 → 0.2.109
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.
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
|
-
import
|
|
4
|
+
import bonjourService from "bonjour-service";
|
|
5
5
|
import { logForgeDebug } from "./debug.js";
|
|
6
6
|
import { companionIrohApiBaseUrlFromNodeId, companionIrohUiBaseUrlFromNodeId, getCompanionIrohStatus } from "./services/companion-iroh.js";
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
|
+
const BonjourConstructor = bonjourService.Bonjour ??
|
|
9
|
+
bonjourService.default ??
|
|
10
|
+
bonjourService;
|
|
8
11
|
export async function startForgeDiscoveryAdvertiser(options) {
|
|
9
12
|
if (options.enabled === false ||
|
|
10
13
|
process.env.FORGE_DISABLE_DISCOVERY_ADVERTISEMENT === "1") {
|
|
@@ -18,7 +21,7 @@ export async function startForgeDiscoveryAdvertiser(options) {
|
|
|
18
21
|
});
|
|
19
22
|
const irohTransport = getCompanionIrohStatus();
|
|
20
23
|
const irohNodeId = irohTransport.pairPayload?.node_id;
|
|
21
|
-
const bonjour = new
|
|
24
|
+
const bonjour = new BonjourConstructor({}, (error) => {
|
|
22
25
|
logForgeDebug(`[forge-discovery] ignored mDNS advertisement error: ${formatDiscoveryError(error)}`);
|
|
23
26
|
});
|
|
24
27
|
let service;
|
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
import { createReadStream, existsSync, statSync } from "node:fs";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const tokenPattern = /[a-z][a-z0-9'*_-]*/gi;
|
|
9
|
+
const defaultSwearLexicon = [
|
|
10
|
+
{ root: "fuck", variants: ["fuck", "fucked", "fucker", "fuckers", "fuckin", "fucking", "fucks", "motherfuck", "motherfucked", "motherfucker", "motherfuckers", "motherfucking"] },
|
|
11
|
+
{ root: "wtf", variants: ["wtf"] },
|
|
12
|
+
{ root: "shit", variants: ["shit", "shitshow", "shits", "shitty", "bullshit", "bullshitting", "dipshit", "dipshits"] },
|
|
13
|
+
{ root: "dick", variants: ["dick", "dicks", "dickhead", "dickheads"] },
|
|
14
|
+
{ root: "ass", variants: ["ass", "asses", "asshole", "assholes", "ashole", "asholes", "dumbass", "dumbasses", "dumb ass", "dumb asses", "dumb-ass", "dumb-asses", "jackass", "jackasses", "jack ass", "jack asses", "jack-ass", "jack-asses"] },
|
|
15
|
+
{ root: "damn", variants: ["damn", "damned", "dammit", "goddamn", "goddamned"] },
|
|
16
|
+
{ root: "bitch", variants: ["bitch", "bitches", "bitching"] },
|
|
17
|
+
{ root: "hell", variants: ["hell"] },
|
|
18
|
+
{ root: "crap", variants: ["crap", "crappy", "piece of crap", "piece-of-crap"] },
|
|
19
|
+
{ root: "moron", variants: ["moron", "morons", "morno", "mornos"] },
|
|
20
|
+
{ root: "idiot", variants: ["idiot", "idiots"] },
|
|
21
|
+
{ root: "stupid", variants: ["stupid"] },
|
|
22
|
+
{ root: "dumb", variants: ["dumb"] },
|
|
23
|
+
{ root: "garbage", variants: ["garbage"] },
|
|
24
|
+
{ root: "trash", variants: ["trash"] },
|
|
25
|
+
{ root: "suck", variants: ["suck", "sucks", "sucked", "sucking"] }
|
|
26
|
+
];
|
|
27
|
+
const ADAPTER_FACTORIES = {
|
|
28
|
+
amp: () => jsonThreadAdapter("amp", [join(dataHome(), "amp", "threads", "*.json")]),
|
|
29
|
+
claude: claudeAdapter,
|
|
30
|
+
cline: clineAdapter,
|
|
31
|
+
codex: codexAdapter,
|
|
32
|
+
hermes: () => genericLocalLogAdapter("hermes", [
|
|
33
|
+
join(homedir(), ".hermes", "**/*.{json,jsonl}"),
|
|
34
|
+
join(homedir(), ".config", "hermes", "**/*.{json,jsonl}")
|
|
35
|
+
]),
|
|
36
|
+
openclaw: () => genericLocalLogAdapter("openclaw", [
|
|
37
|
+
join(homedir(), ".openclaw", "**/*.{json,jsonl}"),
|
|
38
|
+
join(homedir(), "Library", "Application Support", "OpenClaw", "**/*.{json,jsonl}")
|
|
39
|
+
]),
|
|
40
|
+
opencode: opencodeAdapter,
|
|
41
|
+
zed: zedAdapter
|
|
42
|
+
};
|
|
43
|
+
export async function scanConversations(options) {
|
|
44
|
+
const adapters = options.sources?.size
|
|
45
|
+
? [...options.sources].map((source) => createAdapter(source))
|
|
46
|
+
: allAdapters();
|
|
47
|
+
const conversations = [];
|
|
48
|
+
const warnings = [];
|
|
49
|
+
for (const adapter of adapters) {
|
|
50
|
+
const result = await adapter.read();
|
|
51
|
+
conversations.push(...result.conversations);
|
|
52
|
+
warnings.push(...result.warnings);
|
|
53
|
+
}
|
|
54
|
+
const report = analyzeConversations(conversations, options);
|
|
55
|
+
report.warnings = warnings;
|
|
56
|
+
report.sourceFilter = adapters.map((adapter) => adapter.source).sort();
|
|
57
|
+
return report;
|
|
58
|
+
}
|
|
59
|
+
export function availableSources() {
|
|
60
|
+
return Object.keys(ADAPTER_FACTORIES).sort();
|
|
61
|
+
}
|
|
62
|
+
function createAdapter(source) {
|
|
63
|
+
const factory = ADAPTER_FACTORIES[source.toLowerCase()];
|
|
64
|
+
if (!factory) {
|
|
65
|
+
throw new Error(`unknown source: ${source} (available: ${availableSources().join(", ")})`);
|
|
66
|
+
}
|
|
67
|
+
return factory();
|
|
68
|
+
}
|
|
69
|
+
function allAdapters() {
|
|
70
|
+
return availableSources().map((source) => createAdapter(source));
|
|
71
|
+
}
|
|
72
|
+
function analyzeConversations(conversations, options, generatedAt = new Date().toISOString()) {
|
|
73
|
+
const { tokenIndex, phraseVariants } = buildLexiconIndexes();
|
|
74
|
+
const agentStats = new Map();
|
|
75
|
+
const sourceStats = new Map();
|
|
76
|
+
const wordStats = new Map();
|
|
77
|
+
const actualWordStats = new Map();
|
|
78
|
+
const filesScanned = new Set();
|
|
79
|
+
const conversationStats = [];
|
|
80
|
+
let messagesScanned = 0;
|
|
81
|
+
let messagesWithSwears = 0;
|
|
82
|
+
let totalSwears = 0;
|
|
83
|
+
for (const conversation of conversations) {
|
|
84
|
+
if (!isConversationInDateRange(conversation, options)) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
filesScanned.add(conversation.sourceFile);
|
|
88
|
+
const dateKey = dayKey(conversation.updatedAt, options.timeZone);
|
|
89
|
+
let conversationMessages = 0;
|
|
90
|
+
let conversationMessagesWithSwears = 0;
|
|
91
|
+
let conversationSwears = 0;
|
|
92
|
+
const currentSource = sourceStats.get(conversation.source) ?? {
|
|
93
|
+
source: conversation.source,
|
|
94
|
+
conversations: 0,
|
|
95
|
+
messages: 0,
|
|
96
|
+
messagesWithSwears: 0,
|
|
97
|
+
swears: 0
|
|
98
|
+
};
|
|
99
|
+
currentSource.conversations += 1;
|
|
100
|
+
for (const message of conversation.messages) {
|
|
101
|
+
if (!options.roles.has(message.role)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
messagesScanned += 1;
|
|
105
|
+
conversationMessages += 1;
|
|
106
|
+
currentSource.messages += 1;
|
|
107
|
+
const agent = normalizeAgent(message.agent);
|
|
108
|
+
const currentAgent = agentStats.get(agent) ?? {
|
|
109
|
+
messages: 0,
|
|
110
|
+
messagesWithSwears: 0,
|
|
111
|
+
swears: 0
|
|
112
|
+
};
|
|
113
|
+
currentAgent.messages += 1;
|
|
114
|
+
let swearsInMessage = 0;
|
|
115
|
+
for (const occurrence of findOccurrences(message.text, tokenIndex, phraseVariants)) {
|
|
116
|
+
swearsInMessage += 1;
|
|
117
|
+
totalSwears += 1;
|
|
118
|
+
conversationSwears += 1;
|
|
119
|
+
currentSource.swears += 1;
|
|
120
|
+
addOccurrence(wordStats, actualWordStats, occurrence);
|
|
121
|
+
}
|
|
122
|
+
if (swearsInMessage > 0) {
|
|
123
|
+
messagesWithSwears += 1;
|
|
124
|
+
conversationMessagesWithSwears += 1;
|
|
125
|
+
currentSource.messagesWithSwears += 1;
|
|
126
|
+
currentAgent.messagesWithSwears += 1;
|
|
127
|
+
currentAgent.swears += swearsInMessage;
|
|
128
|
+
}
|
|
129
|
+
agentStats.set(agent, currentAgent);
|
|
130
|
+
}
|
|
131
|
+
sourceStats.set(conversation.source, currentSource);
|
|
132
|
+
conversationStats.push({
|
|
133
|
+
source: conversation.source,
|
|
134
|
+
conversationId: conversation.conversationId,
|
|
135
|
+
project: conversation.project,
|
|
136
|
+
sourceFile: conversation.sourceFile,
|
|
137
|
+
updatedAt: conversation.updatedAt,
|
|
138
|
+
dateKey,
|
|
139
|
+
messages: conversationMessages,
|
|
140
|
+
messagesWithSwears: conversationMessagesWithSwears,
|
|
141
|
+
swears: conversationSwears
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
generatedAt,
|
|
146
|
+
filesScanned: [...filesScanned].sort(),
|
|
147
|
+
conversationsScanned: conversationStats.length,
|
|
148
|
+
messagesScanned,
|
|
149
|
+
messagesWithSwears,
|
|
150
|
+
totalSwears,
|
|
151
|
+
byAgent: [...agentStats.entries()]
|
|
152
|
+
.map(([agent, stats]) => ({ agent, ...stats }))
|
|
153
|
+
.sort((left, right) => right.swears - left.swears ||
|
|
154
|
+
right.messages - left.messages ||
|
|
155
|
+
left.agent.localeCompare(right.agent)),
|
|
156
|
+
bySource: [...sourceStats.values()].sort((left, right) => right.swears - left.swears ||
|
|
157
|
+
right.messages - left.messages ||
|
|
158
|
+
left.source.localeCompare(right.source)),
|
|
159
|
+
conversations: conversationStats.sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt) ||
|
|
160
|
+
left.source.localeCompare(right.source) ||
|
|
161
|
+
left.conversationId.localeCompare(right.conversationId)),
|
|
162
|
+
daily: buildDailyStats(conversationStats),
|
|
163
|
+
topWords: [...wordStats.values()].sort((left, right) => right.count - left.count || left.root.localeCompare(right.root)),
|
|
164
|
+
actualWords: [...actualWordStats.values()].sort((left, right) => right.count - left.count ||
|
|
165
|
+
left.word.localeCompare(right.word) ||
|
|
166
|
+
left.root.localeCompare(right.root)),
|
|
167
|
+
warnings: [],
|
|
168
|
+
roleFilter: [...options.roles].sort(),
|
|
169
|
+
sourceFilter: [],
|
|
170
|
+
dateFilter: {
|
|
171
|
+
date: options.date,
|
|
172
|
+
since: options.since?.toISOString(),
|
|
173
|
+
until: options.until?.toISOString()
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function codexAdapter() {
|
|
178
|
+
return {
|
|
179
|
+
source: "codex",
|
|
180
|
+
async read() {
|
|
181
|
+
return readJsonlTree("codex", [join(homedir(), ".codex", "sessions"), join(homedir(), ".codex", "archived_sessions")], parseCodexLine);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function claudeAdapter() {
|
|
186
|
+
return {
|
|
187
|
+
source: "claude",
|
|
188
|
+
async read() {
|
|
189
|
+
return readJsonlTree("claude", [join(homedir(), ".claude", "projects")], parseClaudeLine);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function clineAdapter() {
|
|
194
|
+
return {
|
|
195
|
+
source: "cline",
|
|
196
|
+
async read() {
|
|
197
|
+
const roots = getVSCodeGlobalStoragePaths()
|
|
198
|
+
.flatMap((basePath) => [
|
|
199
|
+
join(basePath, "saoudrizwan.claude-dev", "tasks"),
|
|
200
|
+
join(basePath, "rooveterinaryinc.roo-cline", "tasks")
|
|
201
|
+
])
|
|
202
|
+
.concat(join(homedir(), ".cline", "data", "tasks"));
|
|
203
|
+
const conversations = [];
|
|
204
|
+
const warnings = [];
|
|
205
|
+
for (const root of roots) {
|
|
206
|
+
if (!existsSync(root)) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const files = await globFiles(`${root.replace(/\/+$/, "")}/**/api_conversation_history.json`);
|
|
210
|
+
for (const filePath of files) {
|
|
211
|
+
try {
|
|
212
|
+
const raw = await readFile(filePath, "utf8");
|
|
213
|
+
const parsed = JSON.parse(raw);
|
|
214
|
+
if (!Array.isArray(parsed)) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const messages = parsed.flatMap((entry, index) => compactMessage(parseGenericMessage(entry, {
|
|
218
|
+
source: "cline",
|
|
219
|
+
conversationId: basename(filePath.replace(/\/api_conversation_history\.json$/, "")),
|
|
220
|
+
sourceFile: filePath,
|
|
221
|
+
fallbackTimestamp: fileTimestamp(filePath),
|
|
222
|
+
index
|
|
223
|
+
})));
|
|
224
|
+
pushConversation(conversations, "cline", filePath, messages);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
warnings.push({ file: filePath, line: 0, reason: "Malformed Cline conversation skipped." });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return { conversations, warnings };
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function opencodeAdapter() {
|
|
236
|
+
return {
|
|
237
|
+
source: "opencode",
|
|
238
|
+
async read() {
|
|
239
|
+
const dbPath = findFirstExisting([
|
|
240
|
+
join(dataHome(), "opencode", "opencode.db"),
|
|
241
|
+
join(homedir(), "Library", "Application Support", "opencode", "opencode.db")
|
|
242
|
+
]);
|
|
243
|
+
if (!dbPath) {
|
|
244
|
+
return { conversations: [], warnings: [] };
|
|
245
|
+
}
|
|
246
|
+
const Database = loadBetterSqlite();
|
|
247
|
+
if (!Database) {
|
|
248
|
+
return {
|
|
249
|
+
conversations: [],
|
|
250
|
+
warnings: [{ file: dbPath, line: 0, reason: "better-sqlite3 unavailable; OpenCode database skipped." }]
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const bySession = new Map();
|
|
254
|
+
const db = new Database(dbPath, { readonly: true });
|
|
255
|
+
try {
|
|
256
|
+
const rows = db
|
|
257
|
+
.prepare(`SELECT m.session_id AS sessionId,
|
|
258
|
+
m.time_created AS timeCreated,
|
|
259
|
+
json_extract(m.data, '$.role') AS role,
|
|
260
|
+
json_extract(p.data, '$.text') AS text
|
|
261
|
+
FROM message m
|
|
262
|
+
JOIN part p ON p.message_id = m.id
|
|
263
|
+
WHERE json_extract(p.data, '$.type') = 'text'
|
|
264
|
+
ORDER BY m.time_created ASC`)
|
|
265
|
+
.all();
|
|
266
|
+
for (const row of rows) {
|
|
267
|
+
const text = typeof row.text === "string" ? row.text.trim() : "";
|
|
268
|
+
const role = normalizeRole(row.role);
|
|
269
|
+
if (!text || isContextInjection(role, text)) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const conversationId = String(row.sessionId || "unknown");
|
|
273
|
+
const message = {
|
|
274
|
+
agent: "opencode",
|
|
275
|
+
source: "opencode",
|
|
276
|
+
conversationId,
|
|
277
|
+
role,
|
|
278
|
+
text,
|
|
279
|
+
timestamp: new Date(row.timeCreated).toISOString(),
|
|
280
|
+
sourceFile: dbPath
|
|
281
|
+
};
|
|
282
|
+
const messages = bySession.get(conversationId) ?? [];
|
|
283
|
+
messages.push(message);
|
|
284
|
+
bySession.set(conversationId, messages);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
db.close();
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
conversations: [...bySession.entries()].map(([conversationId, messages]) => ({
|
|
292
|
+
source: "opencode",
|
|
293
|
+
conversationId,
|
|
294
|
+
sourceFile: dbPath,
|
|
295
|
+
updatedAt: maxTimestamp(messages) ?? fileTimestamp(dbPath),
|
|
296
|
+
messages
|
|
297
|
+
})),
|
|
298
|
+
warnings: []
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function zedAdapter() {
|
|
304
|
+
return {
|
|
305
|
+
source: "zed",
|
|
306
|
+
async read() {
|
|
307
|
+
const base = process.platform === "darwin"
|
|
308
|
+
? join(homedir(), "Library", "Application Support", "Zed")
|
|
309
|
+
: join(dataHome(), "zed");
|
|
310
|
+
const conversations = [];
|
|
311
|
+
const warnings = [];
|
|
312
|
+
const jsonFiles = await globFiles(join(base, "conversations", "*.json"));
|
|
313
|
+
for (const filePath of jsonFiles) {
|
|
314
|
+
try {
|
|
315
|
+
const raw = await readFile(filePath, "utf8");
|
|
316
|
+
const parsed = JSON.parse(raw);
|
|
317
|
+
const entries = Array.isArray(parsed.messages) ? parsed.messages : [];
|
|
318
|
+
const messages = entries.flatMap((entry, index) => compactMessage(parseGenericMessage(entry, {
|
|
319
|
+
source: "zed",
|
|
320
|
+
conversationId: basename(filePath, ".json"),
|
|
321
|
+
sourceFile: filePath,
|
|
322
|
+
fallbackTimestamp: fileTimestamp(filePath),
|
|
323
|
+
index
|
|
324
|
+
})));
|
|
325
|
+
pushConversation(conversations, "zed", filePath, messages);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
warnings.push({ file: filePath, line: 0, reason: "Malformed Zed conversation skipped." });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return { conversations, warnings };
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function jsonThreadAdapter(source, patterns) {
|
|
336
|
+
return {
|
|
337
|
+
source,
|
|
338
|
+
async read() {
|
|
339
|
+
const files = await globFiles(patterns);
|
|
340
|
+
const conversations = [];
|
|
341
|
+
const warnings = [];
|
|
342
|
+
for (const filePath of files) {
|
|
343
|
+
try {
|
|
344
|
+
const raw = await readFile(filePath, "utf8");
|
|
345
|
+
const parsed = JSON.parse(raw);
|
|
346
|
+
const entries = Array.isArray(parsed.messages) ? parsed.messages : [];
|
|
347
|
+
const messages = entries.flatMap((entry, index) => compactMessage(parseGenericMessage(entry, {
|
|
348
|
+
source,
|
|
349
|
+
conversationId: basename(filePath, ".json"),
|
|
350
|
+
sourceFile: filePath,
|
|
351
|
+
fallbackTimestamp: fileTimestamp(filePath),
|
|
352
|
+
index
|
|
353
|
+
})));
|
|
354
|
+
pushConversation(conversations, source, filePath, messages);
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
warnings.push({ file: filePath, line: 0, reason: `Malformed ${source} conversation skipped.` });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return { conversations, warnings };
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function genericLocalLogAdapter(source, patterns) {
|
|
365
|
+
return {
|
|
366
|
+
source,
|
|
367
|
+
async read() {
|
|
368
|
+
const candidateFiles = await globFiles(patterns, {
|
|
369
|
+
ignore: ["node_modules", ".git", "venv", "__pycache__"]
|
|
370
|
+
});
|
|
371
|
+
const files = candidateFiles.filter((file) => /conversation|history|session|thread|transcript/i.test(file));
|
|
372
|
+
const conversations = [];
|
|
373
|
+
const warnings = [];
|
|
374
|
+
for (const filePath of files) {
|
|
375
|
+
if (filePath.endsWith(".jsonl")) {
|
|
376
|
+
const result = await readJsonlFile(source, filePath, parseGenericJsonLine);
|
|
377
|
+
conversations.push(...result.conversations);
|
|
378
|
+
warnings.push(...result.warnings);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
const raw = await readFile(filePath, "utf8");
|
|
383
|
+
const parsed = JSON.parse(raw);
|
|
384
|
+
const entries = extractMessageArray(parsed);
|
|
385
|
+
const messages = entries.flatMap((entry, index) => compactMessage(parseGenericMessage(entry, {
|
|
386
|
+
source,
|
|
387
|
+
conversationId: basename(filePath).replace(/\.[^.]+$/, ""),
|
|
388
|
+
sourceFile: filePath,
|
|
389
|
+
fallbackTimestamp: fileTimestamp(filePath),
|
|
390
|
+
index
|
|
391
|
+
})));
|
|
392
|
+
pushConversation(conversations, source, filePath, messages);
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
warnings.push({ file: filePath, line: 0, reason: `Malformed ${source} log skipped.` });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return { conversations, warnings };
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
async function readJsonlTree(source, roots, parser) {
|
|
403
|
+
const files = (await Promise.all(roots.map((root) => globFiles(`${root.replace(/\/+$/, "")}/**/*.jsonl`))))
|
|
404
|
+
.flat()
|
|
405
|
+
.sort();
|
|
406
|
+
const conversations = [];
|
|
407
|
+
const warnings = [];
|
|
408
|
+
for (const filePath of files) {
|
|
409
|
+
const result = await readJsonlFile(source, filePath, parser);
|
|
410
|
+
conversations.push(...result.conversations);
|
|
411
|
+
warnings.push(...result.warnings);
|
|
412
|
+
}
|
|
413
|
+
return { conversations, warnings };
|
|
414
|
+
}
|
|
415
|
+
async function readJsonlFile(source, filePath, parser) {
|
|
416
|
+
const messages = [];
|
|
417
|
+
const warnings = [];
|
|
418
|
+
const fallbackTimestamp = fileTimestamp(filePath);
|
|
419
|
+
const conversationId = basename(filePath, ".jsonl");
|
|
420
|
+
const lines = createInterface({
|
|
421
|
+
input: createReadStream(filePath, { encoding: "utf8" }),
|
|
422
|
+
crlfDelay: Infinity
|
|
423
|
+
});
|
|
424
|
+
let lineNumber = 0;
|
|
425
|
+
for await (const line of lines) {
|
|
426
|
+
lineNumber += 1;
|
|
427
|
+
if (!line.trim()) {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
const parsed = JSON.parse(line);
|
|
432
|
+
const message = parser(parsed, {
|
|
433
|
+
source,
|
|
434
|
+
conversationId,
|
|
435
|
+
sourceFile: filePath,
|
|
436
|
+
fallbackTimestamp,
|
|
437
|
+
line: lineNumber
|
|
438
|
+
});
|
|
439
|
+
if (message) {
|
|
440
|
+
messages.push(message);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
warnings.push({ file: filePath, line: lineNumber, reason: "Invalid JSONL record skipped." });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
conversations: messages.length === 0
|
|
449
|
+
? []
|
|
450
|
+
: [
|
|
451
|
+
{
|
|
452
|
+
source,
|
|
453
|
+
conversationId,
|
|
454
|
+
sourceFile: filePath,
|
|
455
|
+
updatedAt: maxTimestamp(messages) ?? fallbackTimestamp,
|
|
456
|
+
messages
|
|
457
|
+
}
|
|
458
|
+
],
|
|
459
|
+
warnings
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function parseCodexLine(record, context) {
|
|
463
|
+
if (!isObject(record) || record.type !== "response_item" || !isObject(record.payload)) {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
const payload = record.payload;
|
|
467
|
+
if (payload.type !== "message") {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
const role = normalizeRole(payload.role);
|
|
471
|
+
const text = extractText(payload.content).join("\n").trim();
|
|
472
|
+
if (!text || isContextInjection(role, text)) {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
agent: "codex",
|
|
477
|
+
source: "codex",
|
|
478
|
+
conversationId: context.conversationId,
|
|
479
|
+
role,
|
|
480
|
+
text,
|
|
481
|
+
timestamp: typeof record.timestamp === "string" ? record.timestamp : undefined,
|
|
482
|
+
sourceFile: context.sourceFile
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
function parseClaudeLine(record, context) {
|
|
486
|
+
if (!isObject(record)) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
const role = normalizeRole(record.role ?? record.type);
|
|
490
|
+
const message = isObject(record.message) ? record.message : record;
|
|
491
|
+
const text = extractText(message.content ?? record.content).join("\n").trim();
|
|
492
|
+
if (!text || isContextInjection(role, text)) {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
agent: "claude",
|
|
497
|
+
source: "claude",
|
|
498
|
+
conversationId: context.conversationId,
|
|
499
|
+
role,
|
|
500
|
+
text,
|
|
501
|
+
timestamp: typeof record.timestamp === "string"
|
|
502
|
+
? record.timestamp
|
|
503
|
+
: typeof record.createdAt === "string"
|
|
504
|
+
? record.createdAt
|
|
505
|
+
: undefined,
|
|
506
|
+
sourceFile: context.sourceFile
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function parseGenericJsonLine(record, context) {
|
|
510
|
+
return parseGenericMessage(record, {
|
|
511
|
+
source: context.source,
|
|
512
|
+
conversationId: context.conversationId,
|
|
513
|
+
sourceFile: context.sourceFile,
|
|
514
|
+
fallbackTimestamp: context.fallbackTimestamp,
|
|
515
|
+
index: context.line
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
function parseGenericMessage(entry, context) {
|
|
519
|
+
if (!isObject(entry)) {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
const message = isObject(entry.message) ? entry.message : entry;
|
|
523
|
+
const role = normalizeRole(message.role ?? entry.role ?? entry.type);
|
|
524
|
+
const text = extractText(message.content ?? entry.content ?? message.text ?? entry.text).join("\n").trim();
|
|
525
|
+
if (!text || isContextInjection(role, text)) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
const timestamp = stringTimestamp(entry.timestamp) ??
|
|
529
|
+
stringTimestamp(entry.createdAt) ??
|
|
530
|
+
stringTimestamp(message.timestamp) ??
|
|
531
|
+
numberTimestamp(entry.ts) ??
|
|
532
|
+
numberTimestamp(message.ts) ??
|
|
533
|
+
context.fallbackTimestamp;
|
|
534
|
+
return {
|
|
535
|
+
agent: context.source,
|
|
536
|
+
source: context.source,
|
|
537
|
+
conversationId: context.conversationId,
|
|
538
|
+
role,
|
|
539
|
+
text,
|
|
540
|
+
timestamp,
|
|
541
|
+
sourceFile: context.sourceFile
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function normalizeRole(role) {
|
|
545
|
+
if (role === "assistant" || role === "user" || role === "developer" || role === "system") {
|
|
546
|
+
return role;
|
|
547
|
+
}
|
|
548
|
+
return "unknown";
|
|
549
|
+
}
|
|
550
|
+
function isContextInjection(role, text) {
|
|
551
|
+
if (role !== "user") {
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
const trimmed = text.trimStart();
|
|
555
|
+
return (trimmed.startsWith("<environment_context>") ||
|
|
556
|
+
trimmed.startsWith("<permissions instructions>") ||
|
|
557
|
+
trimmed.startsWith("# AGENTS.md instructions for ") ||
|
|
558
|
+
(trimmed.includes("<environment_context>") && trimmed.includes("<INSTRUCTIONS>")));
|
|
559
|
+
}
|
|
560
|
+
function extractText(content) {
|
|
561
|
+
if (typeof content === "string") {
|
|
562
|
+
return [content];
|
|
563
|
+
}
|
|
564
|
+
if (Array.isArray(content)) {
|
|
565
|
+
return content.flatMap((item) => extractText(item));
|
|
566
|
+
}
|
|
567
|
+
if (!isObject(content)) {
|
|
568
|
+
return [];
|
|
569
|
+
}
|
|
570
|
+
const direct = content.text ?? content.value;
|
|
571
|
+
if (typeof direct === "string") {
|
|
572
|
+
return [direct];
|
|
573
|
+
}
|
|
574
|
+
if (content.content !== undefined) {
|
|
575
|
+
return extractText(content.content);
|
|
576
|
+
}
|
|
577
|
+
return [];
|
|
578
|
+
}
|
|
579
|
+
function buildLexiconIndexes(lexicon = defaultSwearLexicon) {
|
|
580
|
+
const tokenIndex = new Map();
|
|
581
|
+
const phraseVariants = [];
|
|
582
|
+
for (const entry of lexicon) {
|
|
583
|
+
for (const variant of entry.variants) {
|
|
584
|
+
const normalizedVariant = variant.toLowerCase();
|
|
585
|
+
if (/[\s-]/.test(normalizedVariant)) {
|
|
586
|
+
phraseVariants.push({
|
|
587
|
+
root: entry.root,
|
|
588
|
+
variant: normalizedVariant,
|
|
589
|
+
pattern: buildPhrasePattern(normalizedVariant)
|
|
590
|
+
});
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
tokenIndex.set(normalizedVariant, entry.root);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
phraseVariants.sort((left, right) => right.variant.length - left.variant.length || left.variant.localeCompare(right.variant));
|
|
597
|
+
return { tokenIndex, phraseVariants };
|
|
598
|
+
}
|
|
599
|
+
function buildPhrasePattern(variant) {
|
|
600
|
+
const words = variant.split(/[\s-]+/).map(escapeRegExp);
|
|
601
|
+
return new RegExp(`\\b${words.join("[\\s-]*")}\\b`, "gi");
|
|
602
|
+
}
|
|
603
|
+
function findOccurrences(text, tokenIndex, phraseVariants) {
|
|
604
|
+
const occurrences = [];
|
|
605
|
+
const phraseRanges = [];
|
|
606
|
+
for (const phrase of phraseVariants) {
|
|
607
|
+
phrase.pattern.lastIndex = 0;
|
|
608
|
+
for (const match of text.matchAll(phrase.pattern)) {
|
|
609
|
+
const matchedText = match[0];
|
|
610
|
+
const start = match.index ?? 0;
|
|
611
|
+
const end = start + matchedText.length;
|
|
612
|
+
if (!/[\s-]/.test(matchedText) && tokenIndex.has(normalizeToken(matchedText))) {
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
if (overlapsAny({ start, end }, phraseRanges)) {
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
phraseRanges.push({ start, end });
|
|
619
|
+
occurrences.push({
|
|
620
|
+
root: phrase.root,
|
|
621
|
+
variant: phrase.variant,
|
|
622
|
+
actual: matchedText.toLowerCase().replace(/\s+/g, " ").trim()
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
for (const match of tokenizeWithSpans(text)) {
|
|
627
|
+
if (overlapsAny(match, phraseRanges)) {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const root = tokenIndex.get(match.token);
|
|
631
|
+
if (root) {
|
|
632
|
+
occurrences.push({ root, variant: match.token, actual: match.token });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return occurrences;
|
|
636
|
+
}
|
|
637
|
+
function tokenizeWithSpans(text) {
|
|
638
|
+
const matches = [];
|
|
639
|
+
tokenPattern.lastIndex = 0;
|
|
640
|
+
for (const match of text.matchAll(tokenPattern)) {
|
|
641
|
+
const rawToken = match[0];
|
|
642
|
+
const token = normalizeToken(rawToken);
|
|
643
|
+
if (!token) {
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
const start = match.index ?? 0;
|
|
647
|
+
matches.push({ token, start, end: start + rawToken.length });
|
|
648
|
+
}
|
|
649
|
+
return matches;
|
|
650
|
+
}
|
|
651
|
+
function addOccurrence(wordStats, actualWordStats, occurrence) {
|
|
652
|
+
const currentWord = wordStats.get(occurrence.root) ?? {
|
|
653
|
+
root: occurrence.root,
|
|
654
|
+
count: 0,
|
|
655
|
+
variants: {}
|
|
656
|
+
};
|
|
657
|
+
currentWord.count += 1;
|
|
658
|
+
currentWord.variants[occurrence.variant] = (currentWord.variants[occurrence.variant] ?? 0) + 1;
|
|
659
|
+
wordStats.set(occurrence.root, currentWord);
|
|
660
|
+
const actualKey = `${occurrence.root}\u0000${occurrence.actual}`;
|
|
661
|
+
const currentActual = actualWordStats.get(actualKey) ?? {
|
|
662
|
+
word: occurrence.actual,
|
|
663
|
+
root: occurrence.root,
|
|
664
|
+
count: 0
|
|
665
|
+
};
|
|
666
|
+
currentActual.count += 1;
|
|
667
|
+
actualWordStats.set(actualKey, currentActual);
|
|
668
|
+
}
|
|
669
|
+
function buildDailyStats(conversations) {
|
|
670
|
+
const byDay = new Map();
|
|
671
|
+
for (const conversation of conversations) {
|
|
672
|
+
const current = byDay.get(conversation.dateKey) ??
|
|
673
|
+
{
|
|
674
|
+
dateKey: conversation.dateKey,
|
|
675
|
+
conversations: 0,
|
|
676
|
+
messages: 0,
|
|
677
|
+
messagesWithSwears: 0,
|
|
678
|
+
swears: 0,
|
|
679
|
+
swearingMessagePercent: 0
|
|
680
|
+
};
|
|
681
|
+
current.conversations += 1;
|
|
682
|
+
current.messages += conversation.messages;
|
|
683
|
+
current.messagesWithSwears += conversation.messagesWithSwears;
|
|
684
|
+
current.swears += conversation.swears;
|
|
685
|
+
current.swearingMessagePercent =
|
|
686
|
+
current.messages === 0 ? 0 : (current.messagesWithSwears / current.messages) * 100;
|
|
687
|
+
byDay.set(conversation.dateKey, current);
|
|
688
|
+
}
|
|
689
|
+
return [...byDay.values()].sort((left, right) => right.dateKey.localeCompare(left.dateKey));
|
|
690
|
+
}
|
|
691
|
+
function compactMessage(message) {
|
|
692
|
+
return message ? [message] : [];
|
|
693
|
+
}
|
|
694
|
+
function extractMessageArray(value) {
|
|
695
|
+
if (Array.isArray(value)) {
|
|
696
|
+
return value;
|
|
697
|
+
}
|
|
698
|
+
if (!isObject(value)) {
|
|
699
|
+
return [];
|
|
700
|
+
}
|
|
701
|
+
for (const key of ["messages", "conversation", "turns", "items", "history"]) {
|
|
702
|
+
const candidate = value[key];
|
|
703
|
+
if (Array.isArray(candidate)) {
|
|
704
|
+
return candidate;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return [];
|
|
708
|
+
}
|
|
709
|
+
function pushConversation(conversations, source, filePath, messages) {
|
|
710
|
+
if (messages.length === 0) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
conversations.push({
|
|
714
|
+
source,
|
|
715
|
+
conversationId: messages[0]?.conversationId ?? basename(filePath).replace(/\.[^.]+$/, ""),
|
|
716
|
+
sourceFile: filePath,
|
|
717
|
+
updatedAt: maxTimestamp(messages) ?? fileTimestamp(filePath),
|
|
718
|
+
messages
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
function dayKey(value, timeZone) {
|
|
722
|
+
const date = new Date(value);
|
|
723
|
+
if (Number.isNaN(date.getTime())) {
|
|
724
|
+
return value.slice(0, 10);
|
|
725
|
+
}
|
|
726
|
+
if (!timeZone) {
|
|
727
|
+
return date.toISOString().slice(0, 10);
|
|
728
|
+
}
|
|
729
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
730
|
+
timeZone,
|
|
731
|
+
year: "numeric",
|
|
732
|
+
month: "2-digit",
|
|
733
|
+
day: "2-digit"
|
|
734
|
+
}).formatToParts(date);
|
|
735
|
+
const year = parts.find((part) => part.type === "year")?.value;
|
|
736
|
+
const month = parts.find((part) => part.type === "month")?.value;
|
|
737
|
+
const day = parts.find((part) => part.type === "day")?.value;
|
|
738
|
+
return year && month && day ? `${year}-${month}-${day}` : date.toISOString().slice(0, 10);
|
|
739
|
+
}
|
|
740
|
+
function isConversationInDateRange(conversation, options) {
|
|
741
|
+
const updatedAtMs = Date.parse(conversation.updatedAt);
|
|
742
|
+
if (!Number.isFinite(updatedAtMs)) {
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
if (options.date && dayKey(conversation.updatedAt, options.timeZone) !== options.date) {
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
if (options.since && updatedAtMs < options.since.getTime()) {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
if (options.until && updatedAtMs >= options.until.getTime()) {
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
function maxTimestamp(messages) {
|
|
757
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
758
|
+
let value = null;
|
|
759
|
+
for (const message of messages) {
|
|
760
|
+
if (!message.timestamp) {
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
const timestamp = Date.parse(message.timestamp);
|
|
764
|
+
if (Number.isFinite(timestamp) && timestamp > max) {
|
|
765
|
+
max = timestamp;
|
|
766
|
+
value = new Date(timestamp).toISOString();
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return value;
|
|
770
|
+
}
|
|
771
|
+
function fileTimestamp(filePath) {
|
|
772
|
+
try {
|
|
773
|
+
return statSync(filePath).mtime.toISOString();
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
return new Date(0).toISOString();
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function dataHome() {
|
|
780
|
+
return process.env["XDG_DATA_HOME"] ?? join(homedir(), ".local", "share");
|
|
781
|
+
}
|
|
782
|
+
function getVSCodeGlobalStoragePaths() {
|
|
783
|
+
if (process.platform === "darwin") {
|
|
784
|
+
return [
|
|
785
|
+
join(homedir(), "Library", "Application Support", "Code", "User", "globalStorage"),
|
|
786
|
+
join(homedir(), "Library", "Application Support", "Code - Insiders", "User", "globalStorage"),
|
|
787
|
+
join(homedir(), "Library", "Application Support", "Cursor", "User", "globalStorage")
|
|
788
|
+
];
|
|
789
|
+
}
|
|
790
|
+
if (process.platform === "linux") {
|
|
791
|
+
const configBase = process.env["XDG_CONFIG_HOME"] ?? join(homedir(), ".config");
|
|
792
|
+
return [
|
|
793
|
+
join(configBase, "Code", "User", "globalStorage"),
|
|
794
|
+
join(configBase, "Code - Insiders", "User", "globalStorage"),
|
|
795
|
+
join(configBase, "Cursor", "User", "globalStorage")
|
|
796
|
+
];
|
|
797
|
+
}
|
|
798
|
+
const appData = process.env["APPDATA"] ?? join(homedir(), "AppData", "Roaming");
|
|
799
|
+
return [
|
|
800
|
+
join(appData, "Code", "User", "globalStorage"),
|
|
801
|
+
join(appData, "Code - Insiders", "User", "globalStorage")
|
|
802
|
+
];
|
|
803
|
+
}
|
|
804
|
+
function findFirstExisting(paths) {
|
|
805
|
+
return paths.find((path) => existsSync(path)) ?? null;
|
|
806
|
+
}
|
|
807
|
+
async function globFiles(patterns, options = {}) {
|
|
808
|
+
const results = new Set();
|
|
809
|
+
for (const pattern of Array.isArray(patterns) ? patterns : [patterns]) {
|
|
810
|
+
const normalizedPattern = normalizePath(pattern);
|
|
811
|
+
const root = globRoot(pattern);
|
|
812
|
+
if (!root || !existsSync(root)) {
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
const matcher = globPatternToRegExp(normalizedPattern);
|
|
816
|
+
for await (const filePath of walkFiles(root, options.ignore ?? [])) {
|
|
817
|
+
if (matcher.test(normalizePath(filePath))) {
|
|
818
|
+
results.add(filePath);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return [...results].sort();
|
|
823
|
+
}
|
|
824
|
+
async function* walkFiles(root, ignoredNames) {
|
|
825
|
+
let entries;
|
|
826
|
+
try {
|
|
827
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
for (const entry of entries) {
|
|
833
|
+
if (ignoredNames.includes(entry.name)) {
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
const fullPath = join(root, entry.name);
|
|
837
|
+
if (entry.isDirectory()) {
|
|
838
|
+
yield* walkFiles(fullPath, ignoredNames);
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
if (entry.isFile()) {
|
|
842
|
+
yield fullPath;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
function globRoot(pattern) {
|
|
847
|
+
const firstGlob = pattern.search(/[*{]/);
|
|
848
|
+
if (firstGlob < 0) {
|
|
849
|
+
return statIsFile(pattern) ? pattern : pathDirectory(pattern);
|
|
850
|
+
}
|
|
851
|
+
const prefix = pattern.slice(0, firstGlob).replace(/[\\/]+$/, "");
|
|
852
|
+
return prefix ? resolve(prefix) : resolve(sep);
|
|
853
|
+
}
|
|
854
|
+
function pathDirectory(value) {
|
|
855
|
+
const trimmed = value.replace(/[\\/]+$/, "");
|
|
856
|
+
return resolve(dirname(trimmed || "."));
|
|
857
|
+
}
|
|
858
|
+
function globPatternToRegExp(pattern) {
|
|
859
|
+
let expression = "";
|
|
860
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
861
|
+
const char = pattern[index];
|
|
862
|
+
const next = pattern[index + 1];
|
|
863
|
+
if (char === "*" && next === "*") {
|
|
864
|
+
const after = pattern[index + 2];
|
|
865
|
+
if (after === "/") {
|
|
866
|
+
expression += "(?:.*/)?";
|
|
867
|
+
index += 2;
|
|
868
|
+
}
|
|
869
|
+
else {
|
|
870
|
+
expression += ".*";
|
|
871
|
+
index += 1;
|
|
872
|
+
}
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
if (char === "*") {
|
|
876
|
+
expression += "[^/]*";
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (char === "{") {
|
|
880
|
+
const close = pattern.indexOf("}", index + 1);
|
|
881
|
+
if (close > index) {
|
|
882
|
+
const alternatives = pattern
|
|
883
|
+
.slice(index + 1, close)
|
|
884
|
+
.split(",")
|
|
885
|
+
.map(escapeRegExp)
|
|
886
|
+
.join("|");
|
|
887
|
+
expression += `(?:${alternatives})`;
|
|
888
|
+
index = close;
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
expression += escapeRegExp(char);
|
|
893
|
+
}
|
|
894
|
+
return new RegExp(`^${expression}$`);
|
|
895
|
+
}
|
|
896
|
+
function normalizePath(value) {
|
|
897
|
+
return resolve(value).split(sep).join("/");
|
|
898
|
+
}
|
|
899
|
+
function statIsFile(value) {
|
|
900
|
+
try {
|
|
901
|
+
return statSync(value).isFile();
|
|
902
|
+
}
|
|
903
|
+
catch {
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
function loadBetterSqlite() {
|
|
908
|
+
try {
|
|
909
|
+
return require("better-sqlite3");
|
|
910
|
+
}
|
|
911
|
+
catch {
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
function stringTimestamp(value) {
|
|
916
|
+
if (typeof value !== "string") {
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
const parsed = Date.parse(value);
|
|
920
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
|
|
921
|
+
}
|
|
922
|
+
function numberTimestamp(value) {
|
|
923
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
const ms = value > 10_000_000_000 ? value : value * 1000;
|
|
927
|
+
return new Date(ms).toISOString();
|
|
928
|
+
}
|
|
929
|
+
function normalizeToken(token) {
|
|
930
|
+
return token
|
|
931
|
+
.toLowerCase()
|
|
932
|
+
.replaceAll("*", "")
|
|
933
|
+
.replaceAll("_", "")
|
|
934
|
+
.replaceAll("-", "")
|
|
935
|
+
.replace(/^'+|'+$/g, "");
|
|
936
|
+
}
|
|
937
|
+
function normalizeAgent(agent) {
|
|
938
|
+
return agent.trim().toLowerCase() || "unknown";
|
|
939
|
+
}
|
|
940
|
+
function overlapsAny(range, ranges) {
|
|
941
|
+
return ranges.some((other) => range.start < other.end && range.end > other.start);
|
|
942
|
+
}
|
|
943
|
+
function escapeRegExp(value) {
|
|
944
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
945
|
+
}
|
|
946
|
+
function isObject(value) {
|
|
947
|
+
return typeof value === "object" && value !== null;
|
|
948
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { scanConversations } from "
|
|
2
|
+
import { scanConversations } from "./devrage-scanner.js";
|
|
3
3
|
import { getDatabase, runInTransaction } from "../db.js";
|
|
4
4
|
import { psycheMetricsViewDataSchema } from "../psyche-types.js";
|
|
5
5
|
const SWEAR_COUNT_KEY = "swear_count";
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "forge-openclaw-plugin",
|
|
3
3
|
"name": "Forge",
|
|
4
4
|
"description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.109",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true,
|
|
8
8
|
"onCapabilities": [
|
package/package.json
CHANGED