supernaturalist 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 +85 -0
- package/dist/cli.js +1097 -0
- package/dist/cli.js.map +7 -0
- package/dist/lib/adapters/amp.d.ts +3 -0
- package/dist/lib/adapters/amp.d.ts.map +1 -0
- package/dist/lib/adapters/amp.js +79 -0
- package/dist/lib/adapters/amp.js.map +1 -0
- package/dist/lib/adapters/claude.d.ts +3 -0
- package/dist/lib/adapters/claude.d.ts.map +1 -0
- package/dist/lib/adapters/claude.js +147 -0
- package/dist/lib/adapters/claude.js.map +1 -0
- package/dist/lib/adapters/cline.d.ts +3 -0
- package/dist/lib/adapters/cline.d.ts.map +1 -0
- package/dist/lib/adapters/cline.js +125 -0
- package/dist/lib/adapters/cline.js.map +1 -0
- package/dist/lib/adapters/codex.d.ts +3 -0
- package/dist/lib/adapters/codex.d.ts.map +1 -0
- package/dist/lib/adapters/codex.js +96 -0
- package/dist/lib/adapters/codex.js.map +1 -0
- package/dist/lib/adapters/index.d.ts +17 -0
- package/dist/lib/adapters/index.d.ts.map +1 -0
- package/dist/lib/adapters/index.js +27 -0
- package/dist/lib/adapters/index.js.map +1 -0
- package/dist/lib/adapters/opencode.d.ts +3 -0
- package/dist/lib/adapters/opencode.d.ts.map +1 -0
- package/dist/lib/adapters/opencode.js +86 -0
- package/dist/lib/adapters/opencode.js.map +1 -0
- package/dist/lib/adapters/pi.d.ts +3 -0
- package/dist/lib/adapters/pi.d.ts.map +1 -0
- package/dist/lib/adapters/pi.js +118 -0
- package/dist/lib/adapters/pi.js.map +1 -0
- package/dist/lib/adapters/zed.d.ts +3 -0
- package/dist/lib/adapters/zed.d.ts.map +1 -0
- package/dist/lib/adapters/zed.js +156 -0
- package/dist/lib/adapters/zed.js.map +1 -0
- package/dist/lib/detector/index.d.ts +32 -0
- package/dist/lib/detector/index.d.ts.map +1 -0
- package/dist/lib/detector/index.js +234 -0
- package/dist/lib/detector/index.js.map +1 -0
- package/dist/lib/index.d.ts +3 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +3 -0
- package/dist/lib/index.js.map +1 -0
- package/package.json +39 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1097 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/adapters/amp.ts
|
|
4
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
function getAmpThreadsDir() {
|
|
8
|
+
return join(
|
|
9
|
+
process.env["XDG_DATA_HOME"] ?? join(homedir(), ".local", "share"),
|
|
10
|
+
"amp",
|
|
11
|
+
"threads"
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
function ampAdapter() {
|
|
15
|
+
return {
|
|
16
|
+
name: "amp",
|
|
17
|
+
async *messages(options) {
|
|
18
|
+
const threadsDir = getAmpThreadsDir();
|
|
19
|
+
let files;
|
|
20
|
+
try {
|
|
21
|
+
files = await readdir(threadsDir);
|
|
22
|
+
} catch {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
26
|
+
for (const file of jsonFiles) {
|
|
27
|
+
const filePath = join(threadsDir, file);
|
|
28
|
+
const threadId = file.replace(".json", "");
|
|
29
|
+
try {
|
|
30
|
+
const raw = await readFile(filePath, "utf-8");
|
|
31
|
+
const thread = JSON.parse(raw);
|
|
32
|
+
if (!thread.messages || !Array.isArray(thread.messages)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
for (const msg of thread.messages) {
|
|
36
|
+
if (msg.role !== "assistant") {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const text = extractText(msg.content);
|
|
40
|
+
if (!text) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const timestamp = msg.timestamp ?? msg.createdAt ?? void 0;
|
|
44
|
+
if (options?.since && timestamp) {
|
|
45
|
+
const ts = new Date(timestamp);
|
|
46
|
+
if (ts < options.since) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
yield {
|
|
51
|
+
text,
|
|
52
|
+
timestamp,
|
|
53
|
+
session: threadId
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function extractText(content) {
|
|
63
|
+
if (typeof content === "string") {
|
|
64
|
+
return content;
|
|
65
|
+
}
|
|
66
|
+
if (Array.isArray(content)) {
|
|
67
|
+
const parts = content.filter(
|
|
68
|
+
(p) => typeof p === "object" && p !== null && typeof p.text === "string"
|
|
69
|
+
).map((p) => p.text);
|
|
70
|
+
return parts.length > 0 ? parts.join(" ") : null;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/adapters/claude.ts
|
|
76
|
+
import { createReadStream } from "node:fs";
|
|
77
|
+
import { readdir as readdir2, stat } from "node:fs/promises";
|
|
78
|
+
import { createInterface } from "node:readline";
|
|
79
|
+
import { homedir as homedir2 } from "node:os";
|
|
80
|
+
import { join as join2 } from "node:path";
|
|
81
|
+
var CLAUDE_DIR = join2(homedir2(), ".claude", "projects");
|
|
82
|
+
function claudeAdapter() {
|
|
83
|
+
return {
|
|
84
|
+
name: "claude",
|
|
85
|
+
async *messages(options) {
|
|
86
|
+
const projectsDir = CLAUDE_DIR;
|
|
87
|
+
let projectDirs;
|
|
88
|
+
try {
|
|
89
|
+
projectDirs = await readdir2(projectsDir);
|
|
90
|
+
} catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (const projectDir of projectDirs) {
|
|
94
|
+
const projectPath = join2(projectsDir, projectDir);
|
|
95
|
+
const projectStat = await stat(projectPath);
|
|
96
|
+
if (!projectStat.isDirectory()) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const entries = await readdir2(projectPath);
|
|
100
|
+
const jsonlFiles = entries.filter((f) => f.endsWith(".jsonl"));
|
|
101
|
+
for (const file of jsonlFiles) {
|
|
102
|
+
const filePath = join2(projectPath, file);
|
|
103
|
+
const session = file.replace(".jsonl", "");
|
|
104
|
+
const context = options?.since ? { session, project: projectDir, since: options.since } : { session, project: projectDir };
|
|
105
|
+
yield* parseClaudeJsonl(filePath, context);
|
|
106
|
+
}
|
|
107
|
+
const subdirs = entries.filter((f) => !f.includes("."));
|
|
108
|
+
for (const subdir of subdirs) {
|
|
109
|
+
const subagentsDir = join2(projectPath, subdir, "subagents");
|
|
110
|
+
try {
|
|
111
|
+
const subFiles = await readdir2(subagentsDir);
|
|
112
|
+
const subJsonl = subFiles.filter((f) => f.endsWith(".jsonl"));
|
|
113
|
+
for (const file of subJsonl) {
|
|
114
|
+
const context = options?.since ? {
|
|
115
|
+
session: `${subdir}/${file.replace(".jsonl", "")}`,
|
|
116
|
+
project: projectDir,
|
|
117
|
+
since: options.since
|
|
118
|
+
} : {
|
|
119
|
+
session: `${subdir}/${file.replace(".jsonl", "")}`,
|
|
120
|
+
project: projectDir
|
|
121
|
+
};
|
|
122
|
+
yield* parseClaudeJsonl(join2(subagentsDir, file), context);
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
async function* parseClaudeJsonl(filePath, context) {
|
|
132
|
+
const rl = createInterface({
|
|
133
|
+
input: createReadStream(filePath, { encoding: "utf-8" }),
|
|
134
|
+
crlfDelay: Infinity
|
|
135
|
+
});
|
|
136
|
+
for await (const line of rl) {
|
|
137
|
+
if (!line.trim()) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const entry = JSON.parse(line);
|
|
142
|
+
const text = extractAssistantText(entry);
|
|
143
|
+
if (!text) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const timestamp = extractTimestamp(entry);
|
|
147
|
+
if (context.since && timestamp) {
|
|
148
|
+
const ts = new Date(timestamp);
|
|
149
|
+
if (ts < context.since) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const message = timestamp ? {
|
|
154
|
+
text,
|
|
155
|
+
timestamp,
|
|
156
|
+
session: context.session,
|
|
157
|
+
project: context.project
|
|
158
|
+
} : {
|
|
159
|
+
text,
|
|
160
|
+
session: context.session,
|
|
161
|
+
project: context.project
|
|
162
|
+
};
|
|
163
|
+
yield message;
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function extractAssistantText(entry) {
|
|
169
|
+
if (entry["type"] === "assistant") {
|
|
170
|
+
const message = entry["message"];
|
|
171
|
+
if (!message) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
return contentToString(message["content"]);
|
|
175
|
+
}
|
|
176
|
+
if (entry["role"] === "assistant") {
|
|
177
|
+
return contentToString(entry["content"]);
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
function contentToString(content) {
|
|
182
|
+
if (typeof content === "string") {
|
|
183
|
+
return content;
|
|
184
|
+
}
|
|
185
|
+
if (Array.isArray(content)) {
|
|
186
|
+
const parts = content.filter(
|
|
187
|
+
(p) => typeof p === "object" && p !== null && p.type === "text"
|
|
188
|
+
).map((p) => p.text);
|
|
189
|
+
return parts.length > 0 ? parts.join(" ") : null;
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
function extractTimestamp(entry) {
|
|
194
|
+
if (typeof entry["timestamp"] === "string") {
|
|
195
|
+
return entry["timestamp"];
|
|
196
|
+
}
|
|
197
|
+
if (typeof entry["createdAt"] === "string") {
|
|
198
|
+
return entry["createdAt"];
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/adapters/cline.ts
|
|
204
|
+
import { readdir as readdir3, readFile as readFile2, stat as stat2 } from "node:fs/promises";
|
|
205
|
+
import { existsSync } from "node:fs";
|
|
206
|
+
import { homedir as homedir3 } from "node:os";
|
|
207
|
+
import { join as join3 } from "node:path";
|
|
208
|
+
function getClineTaskDirs() {
|
|
209
|
+
const dirs = [];
|
|
210
|
+
const vscodePaths = getVSCodeGlobalStoragePaths();
|
|
211
|
+
const extensionIds = ["saoudrizwan.claude-dev", "rooveterinaryinc.roo-cline"];
|
|
212
|
+
for (const basePath of vscodePaths) {
|
|
213
|
+
for (const extId of extensionIds) {
|
|
214
|
+
const tasksDir = join3(basePath, extId, "tasks");
|
|
215
|
+
if (existsSync(tasksDir)) {
|
|
216
|
+
dirs.push(tasksDir);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const clineStandalone = join3(homedir3(), ".cline", "data", "tasks");
|
|
221
|
+
if (existsSync(clineStandalone)) {
|
|
222
|
+
dirs.push(clineStandalone);
|
|
223
|
+
}
|
|
224
|
+
return dirs;
|
|
225
|
+
}
|
|
226
|
+
function getVSCodeGlobalStoragePaths() {
|
|
227
|
+
const paths = [];
|
|
228
|
+
if (process.platform === "darwin") {
|
|
229
|
+
paths.push(
|
|
230
|
+
join3(homedir3(), "Library", "Application Support", "Code", "User", "globalStorage"),
|
|
231
|
+
join3(homedir3(), "Library", "Application Support", "Code - Insiders", "User", "globalStorage"),
|
|
232
|
+
join3(homedir3(), "Library", "Application Support", "Cursor", "User", "globalStorage")
|
|
233
|
+
);
|
|
234
|
+
} else if (process.platform === "linux") {
|
|
235
|
+
const configBase = process.env["XDG_CONFIG_HOME"] ?? join3(homedir3(), ".config");
|
|
236
|
+
paths.push(
|
|
237
|
+
join3(configBase, "Code", "User", "globalStorage"),
|
|
238
|
+
join3(configBase, "Code - Insiders", "User", "globalStorage"),
|
|
239
|
+
join3(configBase, "Cursor", "User", "globalStorage")
|
|
240
|
+
);
|
|
241
|
+
} else {
|
|
242
|
+
const appData = process.env["APPDATA"] ?? join3(homedir3(), "AppData", "Roaming");
|
|
243
|
+
paths.push(
|
|
244
|
+
join3(appData, "Code", "User", "globalStorage"),
|
|
245
|
+
join3(appData, "Code - Insiders", "User", "globalStorage"),
|
|
246
|
+
join3(appData, "Cursor", "User", "globalStorage")
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return paths;
|
|
250
|
+
}
|
|
251
|
+
function clineAdapter() {
|
|
252
|
+
return {
|
|
253
|
+
name: "cline",
|
|
254
|
+
async *messages(options) {
|
|
255
|
+
const taskDirs = getClineTaskDirs();
|
|
256
|
+
for (const tasksDir of taskDirs) {
|
|
257
|
+
let taskIds;
|
|
258
|
+
try {
|
|
259
|
+
taskIds = await readdir3(tasksDir);
|
|
260
|
+
} catch {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
for (const taskId of taskIds) {
|
|
264
|
+
const taskDir = join3(tasksDir, taskId);
|
|
265
|
+
const taskStat = await stat2(taskDir).catch(() => null);
|
|
266
|
+
if (!taskStat?.isDirectory()) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const historyFile = join3(taskDir, "api_conversation_history.json");
|
|
270
|
+
try {
|
|
271
|
+
const raw = await readFile2(historyFile, "utf-8");
|
|
272
|
+
const messages = JSON.parse(raw);
|
|
273
|
+
if (!Array.isArray(messages)) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
for (const msg of messages) {
|
|
277
|
+
if (msg.role !== "assistant") {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const text = extractText2(msg.content);
|
|
281
|
+
if (!text) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const timestamp = msg.ts ?? void 0;
|
|
285
|
+
if (options?.since && timestamp) {
|
|
286
|
+
const ts = new Date(timestamp);
|
|
287
|
+
if (ts < options.since) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
yield {
|
|
292
|
+
text,
|
|
293
|
+
session: taskId
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function extractText2(content) {
|
|
304
|
+
if (typeof content === "string") {
|
|
305
|
+
return content;
|
|
306
|
+
}
|
|
307
|
+
if (Array.isArray(content)) {
|
|
308
|
+
const parts = content.filter(
|
|
309
|
+
(p) => typeof p === "object" && p !== null && p.type === "text" && typeof p.text === "string"
|
|
310
|
+
).map((p) => p.text);
|
|
311
|
+
return parts.length > 0 ? parts.join(" ") : null;
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/adapters/codex.ts
|
|
317
|
+
import { createReadStream as createReadStream2 } from "node:fs";
|
|
318
|
+
import { readdir as readdir4, stat as stat3 } from "node:fs/promises";
|
|
319
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
320
|
+
import { homedir as homedir4 } from "node:os";
|
|
321
|
+
import { join as join4 } from "node:path";
|
|
322
|
+
var CODEX_SESSIONS_DIR = join4(homedir4(), ".codex", "sessions");
|
|
323
|
+
function codexAdapter() {
|
|
324
|
+
return {
|
|
325
|
+
name: "codex",
|
|
326
|
+
async *messages(options) {
|
|
327
|
+
yield* walkCodexSessions(CODEX_SESSIONS_DIR, options);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
async function* walkCodexSessions(dir, options) {
|
|
332
|
+
let entries;
|
|
333
|
+
try {
|
|
334
|
+
entries = await readdir4(dir);
|
|
335
|
+
} catch {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
for (const entry of entries) {
|
|
339
|
+
const fullPath = join4(dir, entry);
|
|
340
|
+
const entryStat = await stat3(fullPath);
|
|
341
|
+
if (entryStat.isDirectory()) {
|
|
342
|
+
yield* walkCodexSessions(fullPath, options);
|
|
343
|
+
} else if (entry.endsWith(".jsonl")) {
|
|
344
|
+
const session = entry.replace(".jsonl", "");
|
|
345
|
+
const context = options?.since ? { session, since: options.since } : { session };
|
|
346
|
+
yield* parseCodexJsonl(fullPath, context);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async function* parseCodexJsonl(filePath, context) {
|
|
351
|
+
const rl = createInterface2({
|
|
352
|
+
input: createReadStream2(filePath, { encoding: "utf-8" }),
|
|
353
|
+
crlfDelay: Infinity
|
|
354
|
+
});
|
|
355
|
+
for await (const line of rl) {
|
|
356
|
+
if (!line.trim()) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
const entry = JSON.parse(line);
|
|
361
|
+
if (entry.type !== "response_item") {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const payload = entry.payload;
|
|
365
|
+
if (!payload || payload.role !== "assistant") {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
const text = extractText3(payload.content);
|
|
369
|
+
if (!text) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (context.since && entry.timestamp) {
|
|
373
|
+
const ts = new Date(entry.timestamp);
|
|
374
|
+
if (ts < context.since) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const message = entry.timestamp ? { text, timestamp: entry.timestamp, session: context.session } : { text, session: context.session };
|
|
379
|
+
yield message;
|
|
380
|
+
} catch {
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function extractText3(content) {
|
|
385
|
+
if (!Array.isArray(content)) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
const parts = content.filter(
|
|
389
|
+
(p) => typeof p === "object" && p !== null && (p.type === "output_text" || p.type === "text") && typeof p.text === "string"
|
|
390
|
+
).map((p) => p.text);
|
|
391
|
+
return parts.length > 0 ? parts.join(" ") : null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/adapters/opencode.ts
|
|
395
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
396
|
+
import { homedir as homedir5 } from "node:os";
|
|
397
|
+
import { join as join5 } from "node:path";
|
|
398
|
+
function getOpencodeDatabasePath() {
|
|
399
|
+
const xdgPath = join5(
|
|
400
|
+
process.env["XDG_DATA_HOME"] ?? join5(homedir5(), ".local", "share"),
|
|
401
|
+
"opencode",
|
|
402
|
+
"opencode.db"
|
|
403
|
+
);
|
|
404
|
+
if (existsSync2(xdgPath)) {
|
|
405
|
+
return xdgPath;
|
|
406
|
+
}
|
|
407
|
+
if (process.platform === "darwin") {
|
|
408
|
+
const macPath = join5(
|
|
409
|
+
homedir5(),
|
|
410
|
+
"Library",
|
|
411
|
+
"Application Support",
|
|
412
|
+
"opencode",
|
|
413
|
+
"opencode.db"
|
|
414
|
+
);
|
|
415
|
+
if (existsSync2(macPath)) {
|
|
416
|
+
return macPath;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
function opencodeAdapter() {
|
|
422
|
+
return {
|
|
423
|
+
name: "opencode",
|
|
424
|
+
async *messages(options) {
|
|
425
|
+
const dbPath = getOpencodeDatabasePath();
|
|
426
|
+
if (!dbPath) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
let db;
|
|
430
|
+
try {
|
|
431
|
+
const BetterSqlite3 = await import("better-sqlite3");
|
|
432
|
+
const Ctor = BetterSqlite3.default ?? BetterSqlite3;
|
|
433
|
+
db = new Ctor(dbPath, { readonly: true });
|
|
434
|
+
} catch {
|
|
435
|
+
console.warn(
|
|
436
|
+
"supernaturalist: better-sqlite3 not available, skipping OpenCode sessions"
|
|
437
|
+
);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
yield* queryAssistantMessages(db, options);
|
|
442
|
+
} finally {
|
|
443
|
+
db.close();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function* queryAssistantMessages(db, options) {
|
|
449
|
+
let query = `
|
|
450
|
+
SELECT
|
|
451
|
+
m.session_id,
|
|
452
|
+
m.time_created,
|
|
453
|
+
json_extract(p.data, '$.text') as text
|
|
454
|
+
FROM message m
|
|
455
|
+
JOIN part p ON p.message_id = m.id
|
|
456
|
+
WHERE json_extract(m.data, '$.role') = 'assistant'
|
|
457
|
+
AND json_extract(p.data, '$.type') = 'text'
|
|
458
|
+
`;
|
|
459
|
+
if (options?.since) {
|
|
460
|
+
const sinceMs = options.since.getTime();
|
|
461
|
+
query += ` AND m.time_created >= ${sinceMs}`;
|
|
462
|
+
}
|
|
463
|
+
query += ` ORDER BY m.time_created ASC`;
|
|
464
|
+
const rows = db.prepare(query).all();
|
|
465
|
+
for (const row of rows) {
|
|
466
|
+
if (!row.text || !row.text.trim()) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
yield {
|
|
470
|
+
text: row.text,
|
|
471
|
+
timestamp: new Date(row.time_created).toISOString(),
|
|
472
|
+
session: row.session_id
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/adapters/pi.ts
|
|
478
|
+
import { createReadStream as createReadStream3 } from "node:fs";
|
|
479
|
+
import { readdir as readdir5, stat as stat4 } from "node:fs/promises";
|
|
480
|
+
import { createInterface as createInterface3 } from "node:readline";
|
|
481
|
+
import { homedir as homedir6 } from "node:os";
|
|
482
|
+
import { join as join6 } from "node:path";
|
|
483
|
+
var PI_SESSIONS_DIR = join6(homedir6(), ".pi", "agent", "sessions");
|
|
484
|
+
function piAdapter() {
|
|
485
|
+
return {
|
|
486
|
+
name: "pi",
|
|
487
|
+
async *messages(options) {
|
|
488
|
+
yield* walkPiSessions(PI_SESSIONS_DIR, options);
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
async function* walkPiSessions(dir, options, project) {
|
|
493
|
+
let entries;
|
|
494
|
+
try {
|
|
495
|
+
entries = await readdir5(dir);
|
|
496
|
+
} catch {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
for (const entry of entries) {
|
|
500
|
+
const fullPath = join6(dir, entry);
|
|
501
|
+
const entryStat = await stat4(fullPath).catch(() => null);
|
|
502
|
+
if (!entryStat) {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (entryStat.isDirectory()) {
|
|
506
|
+
yield* walkPiSessions(fullPath, options, project ?? entry);
|
|
507
|
+
} else if (entry.endsWith(".jsonl")) {
|
|
508
|
+
const session = entry.replace(".jsonl", "");
|
|
509
|
+
const context = options?.since ? { session, project, since: options.since } : { session, project };
|
|
510
|
+
yield* parsePiJsonl(fullPath, context);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async function* parsePiJsonl(filePath, context) {
|
|
515
|
+
const rl = createInterface3({
|
|
516
|
+
input: createReadStream3(filePath, { encoding: "utf-8" }),
|
|
517
|
+
crlfDelay: Infinity
|
|
518
|
+
});
|
|
519
|
+
let project = context.project;
|
|
520
|
+
for await (const line of rl) {
|
|
521
|
+
if (!line.trim()) {
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
try {
|
|
525
|
+
const entry = JSON.parse(line);
|
|
526
|
+
if (entry.type === "session") {
|
|
527
|
+
project = entry.cwd ?? project;
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
if (entry.type !== "message") {
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
const message = entry.message;
|
|
534
|
+
if (!message || message.role !== "assistant") {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const text = contentToString2(message.content);
|
|
538
|
+
if (!text) {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const timestamp = typeof entry.timestamp === "string" ? entry.timestamp : typeof message.timestamp === "number" ? new Date(message.timestamp).toISOString() : void 0;
|
|
542
|
+
if (context.since && timestamp) {
|
|
543
|
+
const ts = new Date(timestamp);
|
|
544
|
+
if (ts < context.since) {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const record = timestamp ? project ? { text, timestamp, session: context.session, project } : { text, timestamp, session: context.session } : project ? { text, session: context.session, project } : { text, session: context.session };
|
|
549
|
+
yield record;
|
|
550
|
+
} catch {
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function contentToString2(content) {
|
|
555
|
+
if (typeof content === "string") {
|
|
556
|
+
return content;
|
|
557
|
+
}
|
|
558
|
+
if (Array.isArray(content)) {
|
|
559
|
+
const parts = content.filter(
|
|
560
|
+
(p) => typeof p === "object" && p !== null && p.type === "text" && typeof p.text === "string"
|
|
561
|
+
).map((p) => p.text);
|
|
562
|
+
return parts.length > 0 ? parts.join(" ") : null;
|
|
563
|
+
}
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/adapters/zed.ts
|
|
568
|
+
import { readdir as readdir6, readFile as readFile3 } from "node:fs/promises";
|
|
569
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
570
|
+
import { homedir as homedir7 } from "node:os";
|
|
571
|
+
import { join as join7 } from "node:path";
|
|
572
|
+
function getZedPaths() {
|
|
573
|
+
if (process.platform === "darwin") {
|
|
574
|
+
const base2 = join7(homedir7(), "Library", "Application Support", "Zed");
|
|
575
|
+
return {
|
|
576
|
+
conversations: join7(base2, "conversations"),
|
|
577
|
+
db: join7(base2, "db")
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
const base = join7(
|
|
581
|
+
process.env["XDG_DATA_HOME"] ?? join7(homedir7(), ".local", "share"),
|
|
582
|
+
"zed"
|
|
583
|
+
);
|
|
584
|
+
return {
|
|
585
|
+
conversations: join7(base, "conversations"),
|
|
586
|
+
db: join7(base, "db")
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function zedAdapter() {
|
|
590
|
+
return {
|
|
591
|
+
name: "zed",
|
|
592
|
+
async *messages(options) {
|
|
593
|
+
const paths = getZedPaths();
|
|
594
|
+
yield* parseTextThreads(paths.conversations, options);
|
|
595
|
+
yield* parseAgentThreads(paths.db, options);
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
async function* parseTextThreads(dir, _options) {
|
|
600
|
+
if (!existsSync3(dir)) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
let files;
|
|
604
|
+
try {
|
|
605
|
+
files = await readdir6(dir);
|
|
606
|
+
} catch {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
610
|
+
for (const file of jsonFiles) {
|
|
611
|
+
const filePath = join7(dir, file);
|
|
612
|
+
const session = file.replace(".json", "");
|
|
613
|
+
try {
|
|
614
|
+
const raw = await readFile3(filePath, "utf-8");
|
|
615
|
+
const conversation = JSON.parse(raw);
|
|
616
|
+
if (!conversation.messages || !Array.isArray(conversation.messages)) {
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
for (const msg of conversation.messages) {
|
|
620
|
+
if (msg.role !== "assistant") {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
const text = typeof msg.content === "string" ? msg.content : null;
|
|
624
|
+
if (!text) {
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
yield {
|
|
628
|
+
text,
|
|
629
|
+
session
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
} catch {
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async function* parseAgentThreads(dbDir, _options) {
|
|
637
|
+
if (!existsSync3(dbDir)) {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
let dbFiles;
|
|
641
|
+
try {
|
|
642
|
+
const entries = await readdir6(dbDir);
|
|
643
|
+
dbFiles = entries.filter((f) => f.endsWith(".db"));
|
|
644
|
+
} catch {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (dbFiles.length === 0) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
let Database;
|
|
651
|
+
try {
|
|
652
|
+
const mod = await import("better-sqlite3");
|
|
653
|
+
Database = mod.default ?? mod;
|
|
654
|
+
} catch {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
for (const dbFile of dbFiles) {
|
|
658
|
+
const dbPath = join7(dbDir, dbFile);
|
|
659
|
+
let db;
|
|
660
|
+
try {
|
|
661
|
+
db = new Database(dbPath, { readonly: true });
|
|
662
|
+
} catch {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
|
|
667
|
+
const tableNames = tables.map((t) => t.name);
|
|
668
|
+
const msgTable = tableNames.find(
|
|
669
|
+
(t) => t === "messages" || t === "thread_messages" || t.includes("message")
|
|
670
|
+
);
|
|
671
|
+
if (!msgTable) {
|
|
672
|
+
db.close();
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
const columns = db.prepare(`PRAGMA table_info("${msgTable}")`).all();
|
|
676
|
+
const colNames = columns.map((c2) => c2.name);
|
|
677
|
+
const hasRole = colNames.includes("role");
|
|
678
|
+
if (!hasRole) {
|
|
679
|
+
db.close();
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
const contentCol = colNames.includes("content") ? "content" : colNames.includes("body") ? "body" : "text";
|
|
683
|
+
const query = `SELECT "${contentCol}" as text FROM "${msgTable}" WHERE role = 'assistant'`;
|
|
684
|
+
const rows = db.prepare(query).all();
|
|
685
|
+
for (const row of rows) {
|
|
686
|
+
if (!row.text?.trim()) {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
yield { text: row.text };
|
|
690
|
+
}
|
|
691
|
+
} catch {
|
|
692
|
+
} finally {
|
|
693
|
+
db.close();
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/adapters/index.ts
|
|
699
|
+
var ADAPTERS = {
|
|
700
|
+
claude: claudeAdapter,
|
|
701
|
+
codex: codexAdapter,
|
|
702
|
+
opencode: opencodeAdapter,
|
|
703
|
+
amp: ampAdapter,
|
|
704
|
+
cline: clineAdapter,
|
|
705
|
+
pi: piAdapter,
|
|
706
|
+
zed: zedAdapter
|
|
707
|
+
};
|
|
708
|
+
function createAdapter(name) {
|
|
709
|
+
const factory = ADAPTERS[name];
|
|
710
|
+
if (!factory) {
|
|
711
|
+
throw new Error(
|
|
712
|
+
`unknown adapter: ${name} (available: ${Object.keys(ADAPTERS).join(", ")})`
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
return factory();
|
|
716
|
+
}
|
|
717
|
+
function allAdapters() {
|
|
718
|
+
return Object.values(ADAPTERS).map((f) => f());
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/detector/index.ts
|
|
722
|
+
var WORDLIST = [
|
|
723
|
+
// === GHOST family (vivid) ===
|
|
724
|
+
{ word: "ghost", intensity: "vivid", group: "ghost" },
|
|
725
|
+
{ word: "ghosts", intensity: "vivid", group: "ghost" },
|
|
726
|
+
{ word: "ghostly", intensity: "vivid", group: "ghost" },
|
|
727
|
+
{ word: "ghosted", intensity: "vivid", group: "ghost" },
|
|
728
|
+
{ word: "ghosting", intensity: "vivid", group: "ghost" },
|
|
729
|
+
{ word: "ghostwriter", intensity: "subtle", group: "ghost" },
|
|
730
|
+
// === HAUNT family (ominous) ===
|
|
731
|
+
{ word: "haunt", intensity: "ominous", group: "haunt" },
|
|
732
|
+
{ word: "haunted", intensity: "ominous", group: "haunt" },
|
|
733
|
+
{ word: "haunting", intensity: "ominous", group: "haunt" },
|
|
734
|
+
{ word: "haunts", intensity: "ominous", group: "haunt" },
|
|
735
|
+
{ word: "hauntings", intensity: "ominous", group: "haunt" },
|
|
736
|
+
// === CURSE family (ominous) ===
|
|
737
|
+
{ word: "curse", intensity: "ominous", group: "curse" },
|
|
738
|
+
{ word: "cursed", intensity: "ominous", group: "curse" },
|
|
739
|
+
{ word: "curses", intensity: "ominous", group: "curse" },
|
|
740
|
+
{ word: "cursing", intensity: "ominous", group: "curse" },
|
|
741
|
+
{ word: "cursedness", intensity: "ominous", group: "curse" },
|
|
742
|
+
// === GHOUL family (vivid) ===
|
|
743
|
+
{ word: "ghoul", intensity: "vivid", group: "ghoul" },
|
|
744
|
+
{ word: "ghouls", intensity: "vivid", group: "ghoul" },
|
|
745
|
+
{ word: "ghoulish", intensity: "vivid", group: "ghoul" },
|
|
746
|
+
// === GOBLIN family (vivid) ===
|
|
747
|
+
{ word: "goblin", intensity: "vivid", group: "goblin" },
|
|
748
|
+
{ word: "goblins", intensity: "vivid", group: "goblin" },
|
|
749
|
+
{ word: "gobliny", intensity: "subtle", group: "goblin" },
|
|
750
|
+
// === SPECTER family (vivid) ===
|
|
751
|
+
{ word: "specter", intensity: "vivid", group: "specter" },
|
|
752
|
+
{ word: "specters", intensity: "vivid", group: "specter" },
|
|
753
|
+
{ word: "spectral", intensity: "vivid", group: "specter" },
|
|
754
|
+
// === PHANTOM family (vivid) ===
|
|
755
|
+
{ word: "phantom", intensity: "vivid", group: "phantom" },
|
|
756
|
+
{ word: "phantoms", intensity: "vivid", group: "phantom" },
|
|
757
|
+
// === WRAITH family (ominous) ===
|
|
758
|
+
{ word: "wraith", intensity: "ominous", group: "wraith" },
|
|
759
|
+
{ word: "wraiths", intensity: "ominous", group: "wraith" },
|
|
760
|
+
{ word: "wraithlike", intensity: "ominous", group: "wraith" },
|
|
761
|
+
// === APPARITION family (vivid) ===
|
|
762
|
+
{ word: "apparition", intensity: "vivid", group: "apparition" },
|
|
763
|
+
{ word: "apparitions", intensity: "vivid", group: "apparition" },
|
|
764
|
+
{ word: "apparitional", intensity: "vivid", group: "apparition" },
|
|
765
|
+
// === POLTERGEIST family (ominous) ===
|
|
766
|
+
{ word: "poltergeist", intensity: "ominous", group: "poltergeist" },
|
|
767
|
+
{ word: "poltergeists", intensity: "ominous", group: "poltergeist" },
|
|
768
|
+
// === DEMON family (ominous) ===
|
|
769
|
+
{ word: "demon", intensity: "ominous", group: "demon" },
|
|
770
|
+
{ word: "demons", intensity: "ominous", group: "demon" },
|
|
771
|
+
{ word: "demonic", intensity: "ominous", group: "demon" },
|
|
772
|
+
// === UNDEAD family (vivid) ===
|
|
773
|
+
{ word: "undead", intensity: "vivid", group: "undead" },
|
|
774
|
+
{ word: "lich", intensity: "ominous", group: "undead" },
|
|
775
|
+
{ word: "liches", intensity: "ominous", group: "undead" },
|
|
776
|
+
{ word: "revenant", intensity: "ominous", group: "undead" },
|
|
777
|
+
{ word: "revenants", intensity: "ominous", group: "undead" },
|
|
778
|
+
// === ZOMBIE family (vivid) ===
|
|
779
|
+
{ word: "zombie", intensity: "vivid", group: "zombie" },
|
|
780
|
+
{ word: "zombies", intensity: "vivid", group: "zombie" },
|
|
781
|
+
{ word: "zombified", intensity: "vivid", group: "zombie" },
|
|
782
|
+
{ word: "zombifying", intensity: "vivid", group: "zombie" },
|
|
783
|
+
// === VAMPIRE family (ominous) ===
|
|
784
|
+
{ word: "vampire", intensity: "ominous", group: "vampire" },
|
|
785
|
+
{ word: "vampires", intensity: "ominous", group: "vampire" },
|
|
786
|
+
{ word: "vampiric", intensity: "ominous", group: "vampire" },
|
|
787
|
+
{ word: "vampirism", intensity: "ominous", group: "vampire" },
|
|
788
|
+
// === WEREWOLF family (ominous) ===
|
|
789
|
+
{ word: "werewolf", intensity: "ominous", group: "werewolf" },
|
|
790
|
+
{ word: "werewolves", intensity: "ominous", group: "werewolf" },
|
|
791
|
+
{ word: "lycan", intensity: "ominous", group: "werewolf" },
|
|
792
|
+
{ word: "lycans", intensity: "ominous", group: "werewolf" },
|
|
793
|
+
{ word: "lycanthrope", intensity: "ominous", group: "werewolf" },
|
|
794
|
+
{ word: "lycanthropy", intensity: "ominous", group: "werewolf" },
|
|
795
|
+
// === WITCH family (vivid) ===
|
|
796
|
+
{ word: "witch", intensity: "vivid", group: "witch" },
|
|
797
|
+
{ word: "witches", intensity: "vivid", group: "witch" },
|
|
798
|
+
{ word: "witchcraft", intensity: "vivid", group: "witch" },
|
|
799
|
+
{ word: "witchy", intensity: "subtle", group: "witch" },
|
|
800
|
+
// === WARLOCK family (vivid) ===
|
|
801
|
+
{ word: "warlock", intensity: "vivid", group: "warlock" },
|
|
802
|
+
{ word: "warlocks", intensity: "vivid", group: "warlock" },
|
|
803
|
+
// === SORCERY family (vivid) ===
|
|
804
|
+
{ word: "sorcery", intensity: "vivid", group: "sorcery" },
|
|
805
|
+
{ word: "sorcerer", intensity: "vivid", group: "sorcery" },
|
|
806
|
+
{ word: "sorcerers", intensity: "vivid", group: "sorcery" },
|
|
807
|
+
{ word: "sorcerous", intensity: "vivid", group: "sorcery" },
|
|
808
|
+
// === SPELL family (subtle) ===
|
|
809
|
+
{ word: "spell", intensity: "subtle", group: "spell" },
|
|
810
|
+
{ word: "spells", intensity: "subtle", group: "spell" },
|
|
811
|
+
{ word: "spellbound", intensity: "vivid", group: "spell" },
|
|
812
|
+
{ word: "spellbinding", intensity: "vivid", group: "spell" },
|
|
813
|
+
{ word: "spellbook", intensity: "vivid", group: "spell" },
|
|
814
|
+
{ word: "spellcasting", intensity: "vivid", group: "spell" },
|
|
815
|
+
// === HEX family (ominous) ===
|
|
816
|
+
{ word: "hex", intensity: "ominous", group: "hex" },
|
|
817
|
+
{ word: "hexed", intensity: "ominous", group: "hex" },
|
|
818
|
+
{ word: "hexes", intensity: "ominous", group: "hex" },
|
|
819
|
+
{ word: "hexing", intensity: "ominous", group: "hex" },
|
|
820
|
+
// === GRIMOIRE family (vivid) ===
|
|
821
|
+
{ word: "grimoire", intensity: "vivid", group: "grimoire" },
|
|
822
|
+
{ word: "grimoires", intensity: "vivid", group: "grimoire" },
|
|
823
|
+
// === ELDRITCH family (ominous) ===
|
|
824
|
+
{ word: "eldritch", intensity: "ominous", group: "eldritch" },
|
|
825
|
+
{ word: "eldritchy", intensity: "ominous", group: "eldritch" },
|
|
826
|
+
// === CURSED OBJECTS ===
|
|
827
|
+
{ word: "hauntedhouse", intensity: "ominous", group: "haunt" },
|
|
828
|
+
{ word: "hauntingly", intensity: "ominous", group: "haunt" },
|
|
829
|
+
{ word: "cursedobject", intensity: "ominous", group: "curse" },
|
|
830
|
+
// === OTHER SUPERNATURAL ===
|
|
831
|
+
{ word: "possessed", intensity: "ominous", group: "possession" },
|
|
832
|
+
{ word: "possession", intensity: "ominous", group: "possession" },
|
|
833
|
+
{ word: "possess", intensity: "ominous", group: "possession" },
|
|
834
|
+
{ word: "exorcism", intensity: "ominous", group: "exorcism" },
|
|
835
|
+
{ word: "exorcist", intensity: "ominous", group: "exorcism" },
|
|
836
|
+
{ word: "seance", intensity: "vivid", group: "seance" },
|
|
837
|
+
{ word: "s\xE9ance", intensity: "vivid", group: "seance" },
|
|
838
|
+
{ word: "medium", intensity: "subtle", group: "medium" },
|
|
839
|
+
{ word: "mediums", intensity: "subtle", group: "medium" },
|
|
840
|
+
{ word: "ouija", intensity: "vivid", group: "ouija" },
|
|
841
|
+
{ word: "hauntology", intensity: "subtle", group: "haunt" },
|
|
842
|
+
{ word: "banshee", intensity: "vivid", group: "banshee" },
|
|
843
|
+
{ word: "banshees", intensity: "vivid", group: "banshee" },
|
|
844
|
+
{ word: "wight", intensity: "ominous", group: "undead" },
|
|
845
|
+
{ word: "wights", intensity: "ominous", group: "undead" },
|
|
846
|
+
{ word: "ghast", intensity: "ominous", group: "ghoul" },
|
|
847
|
+
{ word: "ghasts", intensity: "ominous", group: "ghoul" },
|
|
848
|
+
{ word: "hauntedness", intensity: "ominous", group: "haunt" },
|
|
849
|
+
{ word: "spectrality", intensity: "vivid", group: "specter" },
|
|
850
|
+
{ word: "spirit", intensity: "subtle", group: "spirit" },
|
|
851
|
+
{ word: "spirits", intensity: "subtle", group: "spirit" },
|
|
852
|
+
{ word: "spiritual", intensity: "subtle", group: "spirit" },
|
|
853
|
+
{ word: "phantasm", intensity: "vivid", group: "phantom" },
|
|
854
|
+
{ word: "phantasms", intensity: "vivid", group: "phantom" }
|
|
855
|
+
];
|
|
856
|
+
function collapseRepeats(text) {
|
|
857
|
+
return text.replace(/(.)\1+/g, "$1");
|
|
858
|
+
}
|
|
859
|
+
function buildPattern(words) {
|
|
860
|
+
const sorted = [...words].sort((a, b) => b.word.length - a.word.length);
|
|
861
|
+
const pattern = sorted.map((w) => escapeRegExp(w.word)).join("|");
|
|
862
|
+
return new RegExp(`\\b(${pattern})\\b`, "gi");
|
|
863
|
+
}
|
|
864
|
+
function escapeRegExp(word) {
|
|
865
|
+
return word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
866
|
+
}
|
|
867
|
+
var DEFAULT_PATTERN = buildPattern(WORDLIST);
|
|
868
|
+
var WORD_MAP = new Map(WORDLIST.map((w) => [w.word.toLowerCase(), w]));
|
|
869
|
+
function detect(text) {
|
|
870
|
+
const matches = [];
|
|
871
|
+
const seen = /* @__PURE__ */ new Set();
|
|
872
|
+
runPattern(text, text.toLowerCase(), matches, seen);
|
|
873
|
+
const collapsed = collapseRepeats(text.toLowerCase());
|
|
874
|
+
if (collapsed !== text.toLowerCase()) {
|
|
875
|
+
runPattern(text, collapsed, matches, seen);
|
|
876
|
+
}
|
|
877
|
+
return { count: matches.length, matches };
|
|
878
|
+
}
|
|
879
|
+
function runPattern(_originalText, searchText, matches, seen) {
|
|
880
|
+
DEFAULT_PATTERN.lastIndex = 0;
|
|
881
|
+
let match;
|
|
882
|
+
while ((match = DEFAULT_PATTERN.exec(searchText)) !== null) {
|
|
883
|
+
if (seen.has(match.index)) {
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
const word = match[0].toLowerCase();
|
|
887
|
+
const entry = WORD_MAP.get(word);
|
|
888
|
+
if (!entry) {
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
seen.add(match.index);
|
|
892
|
+
matches.push({
|
|
893
|
+
word,
|
|
894
|
+
index: match.index,
|
|
895
|
+
intensity: entry.intensity,
|
|
896
|
+
group: entry.group
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// src/commands/scan.ts
|
|
902
|
+
var c = {
|
|
903
|
+
reset: "\x1B[0m",
|
|
904
|
+
bold: "\x1B[1m",
|
|
905
|
+
dim: "\x1B[2m",
|
|
906
|
+
red: "\x1B[31m",
|
|
907
|
+
green: "\x1B[32m",
|
|
908
|
+
yellow: "\x1B[33m",
|
|
909
|
+
blue: "\x1B[34m",
|
|
910
|
+
magenta: "\x1B[35m",
|
|
911
|
+
cyan: "\x1B[36m",
|
|
912
|
+
white: "\x1B[37m",
|
|
913
|
+
gray: "\x1B[90m"
|
|
914
|
+
};
|
|
915
|
+
var SPINNER_MESSAGES = [
|
|
916
|
+
"Summoning the archives",
|
|
917
|
+
"Scanning for hauntings",
|
|
918
|
+
"Listening for whispers",
|
|
919
|
+
"Consulting the grimoire",
|
|
920
|
+
"Cataloging the uncanny",
|
|
921
|
+
"Tracking spectral echoes",
|
|
922
|
+
"Measuring the eerie",
|
|
923
|
+
"Indexing the supernatural",
|
|
924
|
+
"Checking for curses",
|
|
925
|
+
"Counting the eldritch"
|
|
926
|
+
];
|
|
927
|
+
function createSpinner() {
|
|
928
|
+
let messageIdx = 0;
|
|
929
|
+
let dotCount = 0;
|
|
930
|
+
let timer = null;
|
|
931
|
+
return {
|
|
932
|
+
start() {
|
|
933
|
+
messageIdx = Math.floor(Math.random() * SPINNER_MESSAGES.length);
|
|
934
|
+
timer = setInterval(() => {
|
|
935
|
+
dotCount = (dotCount + 1) % 4;
|
|
936
|
+
const msg = SPINNER_MESSAGES[messageIdx % SPINNER_MESSAGES.length];
|
|
937
|
+
const dots = ".".repeat(dotCount || 1);
|
|
938
|
+
process.stdout.write(`\r ${c.dim}${msg}${dots}${c.reset} `);
|
|
939
|
+
}, 300);
|
|
940
|
+
},
|
|
941
|
+
update() {
|
|
942
|
+
messageIdx++;
|
|
943
|
+
},
|
|
944
|
+
stop() {
|
|
945
|
+
if (timer) {
|
|
946
|
+
clearInterval(timer);
|
|
947
|
+
timer = null;
|
|
948
|
+
}
|
|
949
|
+
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
function parseArgs(args) {
|
|
954
|
+
const options = {};
|
|
955
|
+
for (let i = 0; i < args.length; i++) {
|
|
956
|
+
const arg = args[i];
|
|
957
|
+
if (arg === "--agent" || arg === "-a") {
|
|
958
|
+
options.agent = args[++i];
|
|
959
|
+
} else if (arg === "--since" || arg === "-s") {
|
|
960
|
+
const val = args[++i];
|
|
961
|
+
if (val) {
|
|
962
|
+
options.since = new Date(val);
|
|
963
|
+
if (isNaN(options.since.getTime())) {
|
|
964
|
+
console.error(`invalid date: ${val}`);
|
|
965
|
+
process.exit(1);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
969
|
+
console.log(`supernaturalist scan \u2014 scan sessions for supernatural terms
|
|
970
|
+
|
|
971
|
+
Options:
|
|
972
|
+
--agent, -a <name> Scan only a specific agent (claude, codex, opencode, amp, cline, pi, zed)
|
|
973
|
+
--since, -s <date> Only scan messages after this date (ISO 8601)
|
|
974
|
+
--help, -h Show this help`);
|
|
975
|
+
process.exit(0);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return options;
|
|
979
|
+
}
|
|
980
|
+
async function scan(args) {
|
|
981
|
+
const options = parseArgs(args);
|
|
982
|
+
const adapters = options.agent ? [createAdapter(options.agent)] : allAdapters();
|
|
983
|
+
const spinner = createSpinner();
|
|
984
|
+
spinner.start();
|
|
985
|
+
const groupTally = {};
|
|
986
|
+
const variantTally = {};
|
|
987
|
+
let totalMessages = 0;
|
|
988
|
+
let totalFindings = 0;
|
|
989
|
+
const perAgent = {};
|
|
990
|
+
for (const adapter of adapters) {
|
|
991
|
+
let agentMessages = 0;
|
|
992
|
+
let agentFindings = 0;
|
|
993
|
+
spinner.update();
|
|
994
|
+
const adapterOptions = options.since ? { since: options.since } : void 0;
|
|
995
|
+
for await (const message of adapter.messages(adapterOptions)) {
|
|
996
|
+
totalMessages++;
|
|
997
|
+
agentMessages++;
|
|
998
|
+
const result = detect(message.text);
|
|
999
|
+
if (result.count > 0) {
|
|
1000
|
+
totalFindings += result.count;
|
|
1001
|
+
agentFindings += result.count;
|
|
1002
|
+
for (const match of result.matches) {
|
|
1003
|
+
groupTally[match.group] = (groupTally[match.group] ?? 0) + 1;
|
|
1004
|
+
const variants = variantTally[match.group] ??= {};
|
|
1005
|
+
variants[match.word] = (variants[match.word] ?? 0) + 1;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (agentMessages > 0) {
|
|
1010
|
+
perAgent[adapter.name] = { messages: agentMessages, findings: agentFindings };
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
spinner.stop();
|
|
1014
|
+
console.log("");
|
|
1015
|
+
console.log(` ${c.bold}${c.magenta}supernaturalist${c.reset} ${c.dim}report${c.reset}`);
|
|
1016
|
+
console.log(` ${c.dim}${"\u2500".repeat(30)}${c.reset}`);
|
|
1017
|
+
console.log("");
|
|
1018
|
+
console.log(` ${c.dim}agent messages scanned${c.reset} ${c.bold}${totalMessages}${c.reset}`);
|
|
1019
|
+
console.log(
|
|
1020
|
+
` ${c.dim}supernatural hits${c.reset} ${c.bold}${c.magenta}${totalFindings}${c.reset}`
|
|
1021
|
+
);
|
|
1022
|
+
const activeAgents = Object.entries(perAgent);
|
|
1023
|
+
if (activeAgents.length > 1) {
|
|
1024
|
+
console.log("");
|
|
1025
|
+
console.log(` ${c.bold}by agent${c.reset}`);
|
|
1026
|
+
for (const [name, stats] of activeAgents) {
|
|
1027
|
+
const rate = (stats.findings / stats.messages * 100).toFixed(1);
|
|
1028
|
+
console.log(
|
|
1029
|
+
` ${c.cyan}${name.padEnd(10)}${c.reset} ${c.bold}${String(stats.findings).padStart(4)}${c.reset} ${c.dim}in ${stats.messages} messages (${rate}%)${c.reset}`
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (totalFindings > 0) {
|
|
1034
|
+
const sorted = Object.entries(groupTally).sort(([, a], [, b]) => b - a);
|
|
1035
|
+
console.log("");
|
|
1036
|
+
console.log(` ${c.bold}top terms${c.reset}`);
|
|
1037
|
+
for (const [group, count] of sorted.slice(0, 10)) {
|
|
1038
|
+
const variants = variantTally[group] ?? {};
|
|
1039
|
+
const variantList = Object.entries(variants).sort(([, a], [, b]) => b - a).filter(([v]) => v !== group).slice(0, 15).map(([v, cnt]) => `${c.dim}${v}${c.reset} ${cnt}`).join(`${c.dim},${c.reset} `);
|
|
1040
|
+
const suffix = variantList ? ` ${c.dim}(${c.reset}${variantList}${c.dim})${c.reset}` : "";
|
|
1041
|
+
console.log(
|
|
1042
|
+
` ${c.yellow}${group.padEnd(12)}${c.reset} ${c.bold}${String(count).padStart(4)}${c.reset}${suffix}`
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
console.log("");
|
|
1047
|
+
if (totalFindings === 0) {
|
|
1048
|
+
console.log(` ${c.green}all quiet. no supernatural traces found.${c.reset}`);
|
|
1049
|
+
console.log("");
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// src/cli.ts
|
|
1054
|
+
var COMMANDS = {
|
|
1055
|
+
scan
|
|
1056
|
+
};
|
|
1057
|
+
function usage() {
|
|
1058
|
+
console.log(`supernaturalist \u2014 analyze supernatural language in agent responses
|
|
1059
|
+
|
|
1060
|
+
Usage:
|
|
1061
|
+
supernaturalist <command> [options]
|
|
1062
|
+
|
|
1063
|
+
Commands:
|
|
1064
|
+
scan Scan agent responses for supernatural terms
|
|
1065
|
+
|
|
1066
|
+
Options:
|
|
1067
|
+
--help, -h Show this help message
|
|
1068
|
+
--version Show version
|
|
1069
|
+
|
|
1070
|
+
Examples:
|
|
1071
|
+
supernaturalist scan
|
|
1072
|
+
supernaturalist scan --agent claude
|
|
1073
|
+
supernaturalist scan --since 2025-01-01`);
|
|
1074
|
+
}
|
|
1075
|
+
async function main() {
|
|
1076
|
+
const args = process.argv.slice(2);
|
|
1077
|
+
const command = args[0];
|
|
1078
|
+
if (command === "--help" || command === "-h") {
|
|
1079
|
+
usage();
|
|
1080
|
+
process.exit(0);
|
|
1081
|
+
}
|
|
1082
|
+
if (command === "--version") {
|
|
1083
|
+
console.log("0.1.0");
|
|
1084
|
+
process.exit(0);
|
|
1085
|
+
}
|
|
1086
|
+
const handler = command ? COMMANDS[command] : void 0;
|
|
1087
|
+
if (handler) {
|
|
1088
|
+
await handler(args.slice(1));
|
|
1089
|
+
} else {
|
|
1090
|
+
await scan(args);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
main().catch((err) => {
|
|
1094
|
+
console.error(err);
|
|
1095
|
+
process.exit(1);
|
|
1096
|
+
});
|
|
1097
|
+
//# sourceMappingURL=cli.js.map
|