devrage 0.5.7 → 0.6.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/dist/cli.js +527 -55
- package/dist/cli.js.map +4 -4
- package/dist/lib/adapters/amp.d.ts.map +1 -1
- package/dist/lib/adapters/amp.js +2 -1
- package/dist/lib/adapters/amp.js.map +1 -1
- package/dist/lib/adapters/claude.d.ts.map +1 -1
- package/dist/lib/adapters/claude.js +25 -10
- package/dist/lib/adapters/claude.js.map +1 -1
- package/dist/lib/adapters/cline.d.ts.map +1 -1
- package/dist/lib/adapters/cline.js +2 -1
- package/dist/lib/adapters/cline.js.map +1 -1
- package/dist/lib/adapters/codex.d.ts.map +1 -1
- package/dist/lib/adapters/codex.js +62 -19
- package/dist/lib/adapters/codex.js.map +1 -1
- package/dist/lib/adapters/cursor.js +17 -12
- package/dist/lib/adapters/cursor.js.map +1 -1
- package/dist/lib/adapters/index.d.ts +3 -1
- package/dist/lib/adapters/index.d.ts.map +1 -1
- package/dist/lib/adapters/index.js.map +1 -1
- package/dist/lib/adapters/opencode.js +7 -7
- package/dist/lib/adapters/opencode.js.map +1 -1
- package/dist/lib/adapters/pi.js +7 -2
- package/dist/lib/adapters/pi.js.map +1 -1
- package/dist/lib/adapters/t3code.js +4 -4
- package/dist/lib/adapters/t3code.js.map +1 -1
- package/dist/lib/adapters/zed.js +6 -5
- package/dist/lib/adapters/zed.js.map +1 -1
- package/dist/lib/index.d.ts +2 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +1 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/slop/index.d.ts +19 -0
- package/dist/lib/slop/index.d.ts.map +1 -0
- package/dist/lib/slop/index.js +291 -0
- package/dist/lib/slop/index.js.map +1 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,7 @@ function ampAdapter() {
|
|
|
17
17
|
name: "amp",
|
|
18
18
|
async *messages(options) {
|
|
19
19
|
const threadsDir = getAmpThreadsDir();
|
|
20
|
+
const role = options?.role ?? "user";
|
|
20
21
|
let files;
|
|
21
22
|
try {
|
|
22
23
|
files = await readdir(threadsDir);
|
|
@@ -36,7 +37,7 @@ function ampAdapter() {
|
|
|
36
37
|
continue;
|
|
37
38
|
}
|
|
38
39
|
for (const msg of thread.messages) {
|
|
39
|
-
if (msg.role !==
|
|
40
|
+
if (msg.role !== role) {
|
|
40
41
|
continue;
|
|
41
42
|
}
|
|
42
43
|
const text = extractText(msg.content);
|
|
@@ -240,7 +241,11 @@ function claudeAdapter() {
|
|
|
240
241
|
name: "claude",
|
|
241
242
|
async *messages(options) {
|
|
242
243
|
for await (const file of discoverClaudeJsonlFiles()) {
|
|
243
|
-
yield* parseClaudeJsonl(file.filePath, {
|
|
244
|
+
yield* parseClaudeJsonl(file.filePath, {
|
|
245
|
+
...file,
|
|
246
|
+
role: options?.role ?? "user",
|
|
247
|
+
since: options?.since
|
|
248
|
+
});
|
|
244
249
|
}
|
|
245
250
|
},
|
|
246
251
|
async *usage(options) {
|
|
@@ -295,13 +300,14 @@ async function* parseClaudeJsonl(filePath, context) {
|
|
|
295
300
|
input: createReadStream(filePath, { encoding: "utf-8" }),
|
|
296
301
|
crlfDelay: Infinity
|
|
297
302
|
});
|
|
303
|
+
const seenAssistantRows = /* @__PURE__ */ new Set();
|
|
298
304
|
for await (const line of rl) {
|
|
299
305
|
if (!line.trim()) {
|
|
300
306
|
continue;
|
|
301
307
|
}
|
|
302
308
|
try {
|
|
303
309
|
const entry = JSON.parse(line);
|
|
304
|
-
const text =
|
|
310
|
+
const text = extractMessageText(entry, context.role);
|
|
305
311
|
if (!text) {
|
|
306
312
|
continue;
|
|
307
313
|
}
|
|
@@ -312,6 +318,15 @@ async function* parseClaudeJsonl(filePath, context) {
|
|
|
312
318
|
continue;
|
|
313
319
|
}
|
|
314
320
|
}
|
|
321
|
+
if (context.role === "assistant") {
|
|
322
|
+
const message = asRecord2(entry["message"]);
|
|
323
|
+
const messageId = stringValue(message?.["id"]) ?? stringValue(entry["uuid"]);
|
|
324
|
+
const dedupeKey = `${messageId ?? timestamp ?? ""}\0${text}`;
|
|
325
|
+
if (seenAssistantRows.has(dedupeKey)) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
seenAssistantRows.add(dedupeKey);
|
|
329
|
+
}
|
|
315
330
|
yield {
|
|
316
331
|
text,
|
|
317
332
|
timestamp: timestamp ?? void 0,
|
|
@@ -322,22 +337,22 @@ async function* parseClaudeJsonl(filePath, context) {
|
|
|
322
337
|
}
|
|
323
338
|
}
|
|
324
339
|
}
|
|
325
|
-
function
|
|
326
|
-
if (entry["type"] ===
|
|
327
|
-
const message = entry["message"];
|
|
340
|
+
function extractMessageText(entry, role) {
|
|
341
|
+
if (entry["type"] === role) {
|
|
342
|
+
const message = asRecord2(entry["message"]);
|
|
328
343
|
if (!message) {
|
|
329
344
|
return null;
|
|
330
345
|
}
|
|
331
346
|
return contentToString(message["content"]);
|
|
332
347
|
}
|
|
333
|
-
if (entry["type"] === "human") {
|
|
334
|
-
const message = entry["message"];
|
|
348
|
+
if (role === "user" && entry["type"] === "human") {
|
|
349
|
+
const message = asRecord2(entry["message"]);
|
|
335
350
|
if (!message) {
|
|
336
351
|
return null;
|
|
337
352
|
}
|
|
338
353
|
return contentToString(message["content"]);
|
|
339
354
|
}
|
|
340
|
-
if (entry["role"] ===
|
|
355
|
+
if (entry["role"] === role) {
|
|
341
356
|
return contentToString(entry["content"]);
|
|
342
357
|
}
|
|
343
358
|
return null;
|
|
@@ -493,6 +508,7 @@ function clineAdapter() {
|
|
|
493
508
|
name: "cline",
|
|
494
509
|
async *messages(options) {
|
|
495
510
|
const taskDirs = getClineTaskDirs();
|
|
511
|
+
const role = options?.role ?? "user";
|
|
496
512
|
for (const tasksDir of taskDirs) {
|
|
497
513
|
let taskIds;
|
|
498
514
|
try {
|
|
@@ -514,7 +530,7 @@ function clineAdapter() {
|
|
|
514
530
|
continue;
|
|
515
531
|
}
|
|
516
532
|
for (const msg of messages) {
|
|
517
|
-
if (msg.role !==
|
|
533
|
+
if (msg.role !== role) {
|
|
518
534
|
continue;
|
|
519
535
|
}
|
|
520
536
|
const text = extractText2(msg.content);
|
|
@@ -565,7 +581,11 @@ function codexAdapter() {
|
|
|
565
581
|
name: "codex",
|
|
566
582
|
async *messages(options) {
|
|
567
583
|
for await (const file of discoverCodexSessionFiles(CODEX_SESSIONS_DIR)) {
|
|
568
|
-
yield* parseCodexJsonl(file.filePath, {
|
|
584
|
+
yield* parseCodexJsonl(file.filePath, {
|
|
585
|
+
session: file.session,
|
|
586
|
+
role: options?.role ?? "user",
|
|
587
|
+
since: options?.since
|
|
588
|
+
});
|
|
569
589
|
}
|
|
570
590
|
},
|
|
571
591
|
async *usage(options) {
|
|
@@ -623,18 +643,17 @@ async function* parseCodexJsonl(filePath, context) {
|
|
|
623
643
|
continue;
|
|
624
644
|
}
|
|
625
645
|
const payload = entry.payload;
|
|
626
|
-
if (!payload || payload.role !==
|
|
646
|
+
if (!payload || payload.role !== context.role) {
|
|
627
647
|
continue;
|
|
628
648
|
}
|
|
629
|
-
const text = extractText3(payload.content);
|
|
649
|
+
const text = extractText3(payload.content, context.role);
|
|
630
650
|
if (!text) {
|
|
631
651
|
continue;
|
|
632
652
|
}
|
|
633
|
-
if (
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
continue;
|
|
653
|
+
if (context.role === "user") {
|
|
654
|
+
if (text.startsWith("<environment_context>") || text.startsWith("<permissions instructions>")) {
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
638
657
|
}
|
|
639
658
|
if (context.since && entry.timestamp) {
|
|
640
659
|
const ts = new Date(entry.timestamp);
|
|
@@ -651,12 +670,13 @@ async function* parseCodexJsonl(filePath, context) {
|
|
|
651
670
|
}
|
|
652
671
|
}
|
|
653
672
|
}
|
|
654
|
-
function extractText3(content) {
|
|
673
|
+
function extractText3(content, role) {
|
|
655
674
|
if (!Array.isArray(content)) {
|
|
656
675
|
return null;
|
|
657
676
|
}
|
|
677
|
+
const textTypes = role === "assistant" ? /* @__PURE__ */ new Set(["output_text", "text"]) : /* @__PURE__ */ new Set(["input_text"]);
|
|
658
678
|
const parts = content.filter(
|
|
659
|
-
(p) => typeof p === "object" && p !== null && p.type
|
|
679
|
+
(p) => typeof p === "object" && p !== null && textTypes.has(p.type) && typeof p.text === "string"
|
|
660
680
|
).map((p) => p.text);
|
|
661
681
|
return parts.length > 0 ? parts.join(" ") : null;
|
|
662
682
|
}
|
|
@@ -670,6 +690,7 @@ async function* parseCodexUsageJsonl(filePath, context) {
|
|
|
670
690
|
let previousUsageSignature = null;
|
|
671
691
|
let session = context.session;
|
|
672
692
|
let sawSessionMeta = false;
|
|
693
|
+
let forkReplayStartedAt = null;
|
|
673
694
|
for await (const line of rl) {
|
|
674
695
|
if (!line.trim()) {
|
|
675
696
|
continue;
|
|
@@ -682,6 +703,15 @@ async function* parseCodexUsageJsonl(filePath, context) {
|
|
|
682
703
|
if (metaSession && !sawSessionMeta) {
|
|
683
704
|
session = metaSession;
|
|
684
705
|
sawSessionMeta = true;
|
|
706
|
+
if (payload?.["thread_source"] === "subagent") {
|
|
707
|
+
forkReplayStartedAt = uuidV7Timestamp(metaSession) ?? timestampMilliseconds(entry["timestamp"]) ?? timestampMilliseconds(payload["timestamp"]);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
if (forkReplayStartedAt !== null) {
|
|
713
|
+
if (isLiveForkTaskStart(entry, payload, forkReplayStartedAt)) {
|
|
714
|
+
forkReplayStartedAt = null;
|
|
685
715
|
}
|
|
686
716
|
continue;
|
|
687
717
|
}
|
|
@@ -813,6 +843,35 @@ function numberValue2(value) {
|
|
|
813
843
|
function stringValue2(value) {
|
|
814
844
|
return typeof value === "string" && value.trim() ? value : void 0;
|
|
815
845
|
}
|
|
846
|
+
function isLiveForkTaskStart(entry, payload, forkStartedAt) {
|
|
847
|
+
if (entry["type"] !== "event_msg" || payload?.["type"] !== "task_started") {
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
const taskIdStartedAt = uuidV7Timestamp(stringValue2(payload["turn_id"]));
|
|
851
|
+
if (taskIdStartedAt !== null) {
|
|
852
|
+
return taskIdStartedAt >= forkStartedAt;
|
|
853
|
+
}
|
|
854
|
+
const taskStartedAt = timestampMilliseconds(payload["started_at"]);
|
|
855
|
+
return taskStartedAt !== null && taskStartedAt >= Math.floor(forkStartedAt / 1e3) * 1e3;
|
|
856
|
+
}
|
|
857
|
+
function uuidV7Timestamp(value) {
|
|
858
|
+
const normalized = value?.replaceAll("-", "");
|
|
859
|
+
if (!normalized || !/^[0-9a-f]{12}7/i.test(normalized)) {
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
const timestamp = Number.parseInt(normalized.slice(0, 12), 16);
|
|
863
|
+
return Number.isSafeInteger(timestamp) ? timestamp : null;
|
|
864
|
+
}
|
|
865
|
+
function timestampMilliseconds(value) {
|
|
866
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
867
|
+
return value >= 1e12 ? value : value * 1e3;
|
|
868
|
+
}
|
|
869
|
+
if (typeof value === "string") {
|
|
870
|
+
const timestamp = Date.parse(value);
|
|
871
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
872
|
+
}
|
|
873
|
+
return null;
|
|
874
|
+
}
|
|
816
875
|
function asRecord3(value) {
|
|
817
876
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
818
877
|
return null;
|
|
@@ -997,7 +1056,7 @@ async function* parseCursorStore(store, options) {
|
|
|
997
1056
|
if (parsed === void 0) {
|
|
998
1057
|
continue;
|
|
999
1058
|
}
|
|
1000
|
-
for (const message of extractCursorMessages(parsed, row.key)) {
|
|
1059
|
+
for (const message of extractCursorMessages(parsed, row.key, options?.role ?? "user")) {
|
|
1001
1060
|
const text = message.text.trim();
|
|
1002
1061
|
if (!isLikelyMessageText(text)) {
|
|
1003
1062
|
continue;
|
|
@@ -1138,21 +1197,22 @@ function decodeStateValue(value) {
|
|
|
1138
1197
|
}
|
|
1139
1198
|
return null;
|
|
1140
1199
|
}
|
|
1141
|
-
function extractCursorMessages(root, rowKey) {
|
|
1200
|
+
function extractCursorMessages(root, rowKey, role) {
|
|
1142
1201
|
if (rowKey.startsWith("bubbleId:")) {
|
|
1143
|
-
const message = extractCursorBubbleMessage(root, rowKey);
|
|
1202
|
+
const message = extractCursorBubbleMessage(root, rowKey, role);
|
|
1144
1203
|
return message ? [message] : [];
|
|
1145
1204
|
}
|
|
1146
1205
|
const messages = [];
|
|
1147
|
-
collectRoleMessages(root, messages);
|
|
1148
|
-
if (rowKey.startsWith("aiService.prompts") || rowKey.startsWith("aiService.generations")) {
|
|
1206
|
+
collectRoleMessages(root, messages, role);
|
|
1207
|
+
if (role === "user" && (rowKey.startsWith("aiService.prompts") || rowKey.startsWith("aiService.generations"))) {
|
|
1149
1208
|
collectPromptMessages(root, messages);
|
|
1150
1209
|
}
|
|
1151
1210
|
return uniqueMessages(messages);
|
|
1152
1211
|
}
|
|
1153
|
-
function extractCursorBubbleMessage(root, rowKey) {
|
|
1212
|
+
function extractCursorBubbleMessage(root, rowKey, role) {
|
|
1154
1213
|
const record = asRecord4(root);
|
|
1155
|
-
|
|
1214
|
+
const expectedType = role === "user" ? 1 : 2;
|
|
1215
|
+
if (!record || numberValue3(record["type"]) !== expectedType) {
|
|
1156
1216
|
return null;
|
|
1157
1217
|
}
|
|
1158
1218
|
const text = firstTextField(record, ["text", "richText"]);
|
|
@@ -1248,13 +1308,13 @@ function cursorBubbleSession(rowKey) {
|
|
|
1248
1308
|
const [, composerId] = rowKey.split(":");
|
|
1249
1309
|
return composerId?.trim() || void 0;
|
|
1250
1310
|
}
|
|
1251
|
-
function collectRoleMessages(value, messages, inheritedSession, depth = 0) {
|
|
1311
|
+
function collectRoleMessages(value, messages, role, inheritedSession, depth = 0) {
|
|
1252
1312
|
if (depth > 12) {
|
|
1253
1313
|
return;
|
|
1254
1314
|
}
|
|
1255
1315
|
if (Array.isArray(value)) {
|
|
1256
1316
|
for (const item of value) {
|
|
1257
|
-
collectRoleMessages(item, messages, inheritedSession, depth + 1);
|
|
1317
|
+
collectRoleMessages(item, messages, role, inheritedSession, depth + 1);
|
|
1258
1318
|
}
|
|
1259
1319
|
return;
|
|
1260
1320
|
}
|
|
@@ -1263,15 +1323,15 @@ function collectRoleMessages(value, messages, inheritedSession, depth = 0) {
|
|
|
1263
1323
|
return;
|
|
1264
1324
|
}
|
|
1265
1325
|
const session = extractSession(record) ?? inheritedSession;
|
|
1266
|
-
if (
|
|
1267
|
-
const text =
|
|
1326
|
+
if (isAuthoredBy(record, role)) {
|
|
1327
|
+
const text = extractMessageText2(record);
|
|
1268
1328
|
if (text) {
|
|
1269
1329
|
messages.push({ text, timestamp: extractTimestamp2(record), session });
|
|
1270
1330
|
}
|
|
1271
1331
|
}
|
|
1272
1332
|
for (const child of Object.values(record)) {
|
|
1273
1333
|
if (typeof child === "object" && child !== null) {
|
|
1274
|
-
collectRoleMessages(child, messages, session, depth + 1);
|
|
1334
|
+
collectRoleMessages(child, messages, role, session, depth + 1);
|
|
1275
1335
|
}
|
|
1276
1336
|
}
|
|
1277
1337
|
}
|
|
@@ -1311,6 +1371,9 @@ function collectPromptMessages(value, messages, inheritedSession, depth = 0) {
|
|
|
1311
1371
|
}
|
|
1312
1372
|
}
|
|
1313
1373
|
}
|
|
1374
|
+
function isAuthoredBy(record, role) {
|
|
1375
|
+
return role === "user" ? isUserAuthored(record) : isAssistantAuthored(record);
|
|
1376
|
+
}
|
|
1314
1377
|
function isUserAuthored(record) {
|
|
1315
1378
|
return ["role", "speaker", "sender", "author", "source", "from", "type", "kind"].some(
|
|
1316
1379
|
(field) => actorIsUser(record[field])
|
|
@@ -1344,7 +1407,7 @@ function actorString(value) {
|
|
|
1344
1407
|
}
|
|
1345
1408
|
return null;
|
|
1346
1409
|
}
|
|
1347
|
-
function
|
|
1410
|
+
function extractMessageText2(record) {
|
|
1348
1411
|
return firstTextField(record, ["text", "content", "message", "prompt", "query", "input"]);
|
|
1349
1412
|
}
|
|
1350
1413
|
function firstTextField(record, fields) {
|
|
@@ -1475,7 +1538,7 @@ function opencodeAdapter() {
|
|
|
1475
1538
|
return;
|
|
1476
1539
|
}
|
|
1477
1540
|
try {
|
|
1478
|
-
yield*
|
|
1541
|
+
yield* queryMessages(db, options);
|
|
1479
1542
|
} finally {
|
|
1480
1543
|
db.close();
|
|
1481
1544
|
}
|
|
@@ -1504,7 +1567,7 @@ async function openOpencodeDb() {
|
|
|
1504
1567
|
}
|
|
1505
1568
|
return db;
|
|
1506
1569
|
}
|
|
1507
|
-
function*
|
|
1570
|
+
function* queryMessages(db, options) {
|
|
1508
1571
|
let query = `
|
|
1509
1572
|
SELECT
|
|
1510
1573
|
m.session_id,
|
|
@@ -1512,10 +1575,10 @@ function* queryUserMessages(db, options) {
|
|
|
1512
1575
|
json_extract(p.data, '$.text') as text
|
|
1513
1576
|
FROM message m
|
|
1514
1577
|
JOIN part p ON p.message_id = m.id
|
|
1515
|
-
WHERE json_extract(m.data, '$.role') =
|
|
1578
|
+
WHERE json_extract(m.data, '$.role') = ?
|
|
1516
1579
|
AND json_extract(p.data, '$.type') = 'text'
|
|
1517
1580
|
`;
|
|
1518
|
-
const params = [];
|
|
1581
|
+
const params = [options?.role ?? "user"];
|
|
1519
1582
|
if (options?.since) {
|
|
1520
1583
|
query += ` AND m.time_created >= ?`;
|
|
1521
1584
|
params.push(options.since.getTime());
|
|
@@ -1623,7 +1686,12 @@ async function* walkPiSessions(dir, options, project) {
|
|
|
1623
1686
|
yield* walkPiSessions(fullPath, options, project ?? entry);
|
|
1624
1687
|
} else if (entry.endsWith(".jsonl")) {
|
|
1625
1688
|
const session = entry.replace(".jsonl", "");
|
|
1626
|
-
yield* parsePiJsonl(fullPath, {
|
|
1689
|
+
yield* parsePiJsonl(fullPath, {
|
|
1690
|
+
session,
|
|
1691
|
+
project,
|
|
1692
|
+
role: options?.role ?? "user",
|
|
1693
|
+
since: options?.since
|
|
1694
|
+
});
|
|
1627
1695
|
}
|
|
1628
1696
|
}
|
|
1629
1697
|
}
|
|
@@ -1668,7 +1736,7 @@ async function* parsePiJsonl(filePath, context) {
|
|
|
1668
1736
|
continue;
|
|
1669
1737
|
}
|
|
1670
1738
|
const message = entry.message;
|
|
1671
|
-
if (!message || message.role !==
|
|
1739
|
+
if (!message || message.role !== context.role) {
|
|
1672
1740
|
continue;
|
|
1673
1741
|
}
|
|
1674
1742
|
const text = contentToString2(message.content);
|
|
@@ -1785,7 +1853,7 @@ function t3codeAdapter() {
|
|
|
1785
1853
|
continue;
|
|
1786
1854
|
}
|
|
1787
1855
|
try {
|
|
1788
|
-
yield*
|
|
1856
|
+
yield* queryMessages2(db, location, options);
|
|
1789
1857
|
} finally {
|
|
1790
1858
|
db.close();
|
|
1791
1859
|
}
|
|
@@ -1847,7 +1915,7 @@ function resolveHomePath(value) {
|
|
|
1847
1915
|
async function openT3Db(dbPath) {
|
|
1848
1916
|
return openReadonlySqliteDatabase(dbPath);
|
|
1849
1917
|
}
|
|
1850
|
-
function*
|
|
1918
|
+
function* queryMessages2(db, location, options) {
|
|
1851
1919
|
if (!hasColumns(db, "projection_thread_messages", ["thread_id", "role", "text", "created_at"])) {
|
|
1852
1920
|
return;
|
|
1853
1921
|
}
|
|
@@ -1855,9 +1923,9 @@ function* queryUserMessages2(db, location, options) {
|
|
|
1855
1923
|
let query = `
|
|
1856
1924
|
SELECT thread_id, created_at, text
|
|
1857
1925
|
FROM projection_thread_messages
|
|
1858
|
-
WHERE role =
|
|
1926
|
+
WHERE role = ?
|
|
1859
1927
|
`;
|
|
1860
|
-
const params = [];
|
|
1928
|
+
const params = [options?.role ?? "user"];
|
|
1861
1929
|
if (options?.since) {
|
|
1862
1930
|
query += ` AND created_at >= ?`;
|
|
1863
1931
|
params.push(options.since.toISOString());
|
|
@@ -2212,7 +2280,7 @@ function zedAdapter() {
|
|
|
2212
2280
|
}
|
|
2213
2281
|
};
|
|
2214
2282
|
}
|
|
2215
|
-
async function* parseTextThreads(dir,
|
|
2283
|
+
async function* parseTextThreads(dir, options) {
|
|
2216
2284
|
if (!existsSync5(dir)) {
|
|
2217
2285
|
return;
|
|
2218
2286
|
}
|
|
@@ -2232,8 +2300,9 @@ async function* parseTextThreads(dir, _options) {
|
|
|
2232
2300
|
if (!conversation.messages || !Array.isArray(conversation.messages)) {
|
|
2233
2301
|
continue;
|
|
2234
2302
|
}
|
|
2303
|
+
const role = options?.role ?? "user";
|
|
2235
2304
|
for (const msg of conversation.messages) {
|
|
2236
|
-
if (msg.role !==
|
|
2305
|
+
if (msg.role !== role) {
|
|
2237
2306
|
continue;
|
|
2238
2307
|
}
|
|
2239
2308
|
const text = typeof msg.content === "string" ? msg.content : null;
|
|
@@ -2249,7 +2318,7 @@ async function* parseTextThreads(dir, _options) {
|
|
|
2249
2318
|
}
|
|
2250
2319
|
}
|
|
2251
2320
|
}
|
|
2252
|
-
async function* parseAgentThreads(dbDir,
|
|
2321
|
+
async function* parseAgentThreads(dbDir, options) {
|
|
2253
2322
|
if (!existsSync5(dbDir)) {
|
|
2254
2323
|
return;
|
|
2255
2324
|
}
|
|
@@ -2285,8 +2354,8 @@ async function* parseAgentThreads(dbDir, _options) {
|
|
|
2285
2354
|
continue;
|
|
2286
2355
|
}
|
|
2287
2356
|
const contentCol = colNames.includes("content") ? "content" : colNames.includes("body") ? "body" : "text";
|
|
2288
|
-
|
|
2289
|
-
const rows = db.prepare(query).all();
|
|
2357
|
+
const query = `SELECT "${contentCol}" as text FROM "${msgTable}" WHERE role = ?`;
|
|
2358
|
+
const rows = db.prepare(query).all(options?.role ?? "user");
|
|
2290
2359
|
for (const row of rows) {
|
|
2291
2360
|
if (!row.text?.trim()) {
|
|
2292
2361
|
continue;
|
|
@@ -2921,6 +2990,281 @@ function asRecord7(value) {
|
|
|
2921
2990
|
return value;
|
|
2922
2991
|
}
|
|
2923
2992
|
|
|
2993
|
+
// src/slop/index.ts
|
|
2994
|
+
var SLOP_SIGNALS = [
|
|
2995
|
+
// Claude's especially recognizable coding-agent voice.
|
|
2996
|
+
{
|
|
2997
|
+
tell: "load-bearing",
|
|
2998
|
+
category: "claude-ism",
|
|
2999
|
+
pattern: /\bload(?:[-‐‑‒–—\s]+)bearing\b/giu
|
|
3000
|
+
},
|
|
3001
|
+
{
|
|
3002
|
+
tell: "honest take",
|
|
3003
|
+
category: "claude-ism",
|
|
3004
|
+
pattern: /\bhonest\s+(?:take|evaluation|assessment)\b/giu
|
|
3005
|
+
},
|
|
3006
|
+
{
|
|
3007
|
+
tell: "precise mechanism",
|
|
3008
|
+
category: "claude-ism",
|
|
3009
|
+
pattern: /\b(?:the\s+)?precise\s+mechanism\b/giu
|
|
3010
|
+
},
|
|
3011
|
+
{
|
|
3012
|
+
tell: "and that matters",
|
|
3013
|
+
category: "claude-ism",
|
|
3014
|
+
pattern: /\band\s+that\s+matters\b/giu
|
|
3015
|
+
},
|
|
3016
|
+
{
|
|
3017
|
+
tell: "key insight",
|
|
3018
|
+
category: "claude-ism",
|
|
3019
|
+
pattern: /\b(?:the\s+)?key\s+insight\b/giu
|
|
3020
|
+
},
|
|
3021
|
+
{
|
|
3022
|
+
tell: "without ceremony",
|
|
3023
|
+
category: "claude-ism",
|
|
3024
|
+
pattern: /\b(?:without|with\s+no|needs?\s+no|requires?\s+no|no)\s+(?:additional\s+)?ceremony\b/giu
|
|
3025
|
+
},
|
|
3026
|
+
{
|
|
3027
|
+
tell: "earns its keep",
|
|
3028
|
+
category: "claude-ism",
|
|
3029
|
+
pattern: /\bearn(?:s|ed|ing)?\s+(?:its|their|the)\s+keep\b/giu
|
|
3030
|
+
},
|
|
3031
|
+
{
|
|
3032
|
+
tell: "keeps X honest",
|
|
3033
|
+
category: "claude-ism",
|
|
3034
|
+
pattern: /\b(?:keep|keeps|keeping|kept)\s+(?:(?:this|that|us|me|things?)|(?:the|our|your|my)\s+[\w-]+(?:\s+[\w-]+){0,2})\s+honest\b/giu
|
|
3035
|
+
},
|
|
3036
|
+
{
|
|
3037
|
+
tell: "belt-and-suspenders",
|
|
3038
|
+
category: "claude-ism",
|
|
3039
|
+
pattern: /\bbelt(?:[-‐‑‒–—\s]+)and(?:[-‐‑‒–—\s]+)suspenders\b/giu
|
|
3040
|
+
},
|
|
3041
|
+
{
|
|
3042
|
+
tell: "soup to nuts",
|
|
3043
|
+
category: "claude-ism",
|
|
3044
|
+
pattern: /\b(?:from\s+)?soup(?:[-‐‑‒–—\s]+)to(?:[-‐‑‒–—\s]+)nuts\b/giu
|
|
3045
|
+
},
|
|
3046
|
+
{
|
|
3047
|
+
tell: "architectural seam",
|
|
3048
|
+
category: "claude-ism",
|
|
3049
|
+
pattern: /\b(?:(?:clean|natural|architectural|implementation|integration)\s+seam|(?:at|across)\s+the\s+seam\s+between)\b/giu
|
|
3050
|
+
},
|
|
3051
|
+
// The apology/validation loop developers see after correcting an agent.
|
|
3052
|
+
{
|
|
3053
|
+
tell: "you're absolutely right",
|
|
3054
|
+
category: "sycophancy",
|
|
3055
|
+
pattern: /\byou(?:['’]re|\s+are)\s+absolutely\s+right\b/giu
|
|
3056
|
+
},
|
|
3057
|
+
{
|
|
3058
|
+
tell: "you're right to call that out",
|
|
3059
|
+
category: "sycophancy",
|
|
3060
|
+
pattern: /\byou(?:['’]re|\s+are)\s+right\s+to\s+(?:call|point|flag)\s+(?:me|that|this)\s+out\b/giu
|
|
3061
|
+
},
|
|
3062
|
+
{
|
|
3063
|
+
tell: "good question",
|
|
3064
|
+
category: "sycophancy",
|
|
3065
|
+
pattern: /\b(?:(?:that(?:['’]s|\s+is)|this\s+is)\s+)?(?:a\s+)?(?:good|great|excellent)\s+question\b/giu
|
|
3066
|
+
},
|
|
3067
|
+
{
|
|
3068
|
+
tell: "good catch",
|
|
3069
|
+
category: "sycophancy",
|
|
3070
|
+
pattern: /\b(?:(?:that(?:['’]s|\s+is)|this\s+is)\s+)?(?:a\s+)?(?:good|great|excellent)\s+catch\b/giu
|
|
3071
|
+
},
|
|
3072
|
+
{
|
|
3073
|
+
tell: "real gap",
|
|
3074
|
+
category: "sycophancy",
|
|
3075
|
+
pattern: /\b(?:a\s+)?real\s+gap\b/giu
|
|
3076
|
+
},
|
|
3077
|
+
{
|
|
3078
|
+
tell: "error in my framing",
|
|
3079
|
+
category: "sycophancy",
|
|
3080
|
+
pattern: /\b(?:a\s+)?real\s+error\s+in\s+(?:my|the)\s+framing\b/giu
|
|
3081
|
+
},
|
|
3082
|
+
{
|
|
3083
|
+
tell: "I was wrong",
|
|
3084
|
+
category: "sycophancy",
|
|
3085
|
+
pattern: /\bi\s+was\s+wrong\b/giu
|
|
3086
|
+
},
|
|
3087
|
+
{
|
|
3088
|
+
tell: "I overcomplicated it",
|
|
3089
|
+
category: "sycophancy",
|
|
3090
|
+
pattern: /\bi\s+(?:overcomplicated|over-engineered|overthought)\s+(?:this|that|it)\b/giu
|
|
3091
|
+
},
|
|
3092
|
+
// Cross-model corporate/chatbot prose, constrained where a raw word is technical.
|
|
3093
|
+
{ tell: "delve", category: "stock prose", pattern: /\b(?:delve(?:d|s)?|delving)\b/giu },
|
|
3094
|
+
{ tell: "crucial", category: "stock prose", pattern: /\bcrucial\b/giu },
|
|
3095
|
+
{ tell: "pivotal", category: "stock prose", pattern: /\bpivotal\b/giu },
|
|
3096
|
+
{ tell: "tapestry", category: "stock prose", pattern: /\btapestr(?:y|ies)\b/giu },
|
|
3097
|
+
{
|
|
3098
|
+
tell: "here's the thing",
|
|
3099
|
+
category: "stock prose",
|
|
3100
|
+
pattern: /\bhere(?:['’]s|\s+is)\s+the\s+thing\b/giu
|
|
3101
|
+
},
|
|
3102
|
+
{
|
|
3103
|
+
tell: "hope this helps",
|
|
3104
|
+
category: "stock prose",
|
|
3105
|
+
pattern: /\bhope\s+(?:this|that)\s+helps\b/giu
|
|
3106
|
+
},
|
|
3107
|
+
{
|
|
3108
|
+
tell: "after careful consideration",
|
|
3109
|
+
category: "stock prose",
|
|
3110
|
+
pattern: /\bafter\s+careful\s+consideration\b/giu
|
|
3111
|
+
},
|
|
3112
|
+
{
|
|
3113
|
+
tell: "quick update",
|
|
3114
|
+
category: "stock prose",
|
|
3115
|
+
pattern: /\bto\s+provide\s+(?:you\s+with\s+)?a\s+quick\s+update\b/giu
|
|
3116
|
+
},
|
|
3117
|
+
{
|
|
3118
|
+
tell: "robust and reliable",
|
|
3119
|
+
category: "stock prose",
|
|
3120
|
+
pattern: /\brobust(?:\s+and|,)\s+reliable\b/giu
|
|
3121
|
+
},
|
|
3122
|
+
{ tell: "seamless", category: "stock prose", pattern: /\bseamless(?:ly)?\b/giu },
|
|
3123
|
+
{
|
|
3124
|
+
tell: "in the realm of",
|
|
3125
|
+
category: "stock prose",
|
|
3126
|
+
pattern: /\b(?:in|within)\s+the\s+realm\s+of\b/giu
|
|
3127
|
+
},
|
|
3128
|
+
{
|
|
3129
|
+
tell: "important to note",
|
|
3130
|
+
category: "stock prose",
|
|
3131
|
+
pattern: /\b(?:(?:it\s+is|it(?:['’]s))\s+)?important\s+to\s+note\b/giu
|
|
3132
|
+
},
|
|
3133
|
+
{
|
|
3134
|
+
tell: "worth noting",
|
|
3135
|
+
category: "stock prose",
|
|
3136
|
+
pattern: /\b(?:(?:it\s+is|it(?:['’]s))\s+)?worth\s+noting\b/giu
|
|
3137
|
+
},
|
|
3138
|
+
{
|
|
3139
|
+
tell: "let me break this down",
|
|
3140
|
+
category: "stock prose",
|
|
3141
|
+
pattern: /\blet\s+(?:me|us)\s+break\s+(?:this|it)\s+down\b/giu
|
|
3142
|
+
},
|
|
3143
|
+
{
|
|
3144
|
+
tell: "let's dive in",
|
|
3145
|
+
category: "stock prose",
|
|
3146
|
+
pattern: /\blet(?:['’]s|\s+us)\s+dive\s+(?:in|into)\b/giu
|
|
3147
|
+
},
|
|
3148
|
+
{
|
|
3149
|
+
tell: "let's unpack this",
|
|
3150
|
+
category: "stock prose",
|
|
3151
|
+
pattern: /\blet(?:['’]s|\s+us)\s+unpack\s+(?:this|that|it)\b/giu
|
|
3152
|
+
},
|
|
3153
|
+
{ tell: "at its core", category: "stock prose", pattern: /\bat\s+its\s+core\b/giu },
|
|
3154
|
+
{
|
|
3155
|
+
tell: "comprehensive",
|
|
3156
|
+
category: "stock prose",
|
|
3157
|
+
pattern: /\bcomprehensive\s+(?:analysis|approach|coverage|error\s+handling|guide|implementation|overview|solution|suite)\b/giu
|
|
3158
|
+
},
|
|
3159
|
+
{
|
|
3160
|
+
tell: "successfully implemented",
|
|
3161
|
+
category: "stock prose",
|
|
3162
|
+
pattern: /\bsuccessfully\s+(?:added|completed|created|fixed|implemented|resolved|updated)\b/giu
|
|
3163
|
+
},
|
|
3164
|
+
{
|
|
3165
|
+
tell: "Certainly.",
|
|
3166
|
+
category: "stock prose",
|
|
3167
|
+
pattern: /(?:^|\n)[\t ]*certainly\b/gimu
|
|
3168
|
+
},
|
|
3169
|
+
{
|
|
3170
|
+
tell: "I'd be happy to",
|
|
3171
|
+
category: "stock prose",
|
|
3172
|
+
pattern: /\bi(?:['’]d|\s+would)\s+be\s+happy\s+to\b/giu
|
|
3173
|
+
},
|
|
3174
|
+
// Corrective juxtaposition and fake-deep contrast templates.
|
|
3175
|
+
{
|
|
3176
|
+
tell: "it's not X, it's Y",
|
|
3177
|
+
category: "rhetorical template",
|
|
3178
|
+
pattern: /\b(?:it|this|that)(?:['’]s|\s+is)\s+not\s+[^.!?\n]{1,100}?(?:,|;|—)\s*(?:it|this|that)(?:['’]s|\s+is)\s+[^.!?\n]{1,100}/giu
|
|
3179
|
+
},
|
|
3180
|
+
{
|
|
3181
|
+
tell: "it's not X, it's Y",
|
|
3182
|
+
category: "rhetorical template",
|
|
3183
|
+
pattern: /\b(?:it|this|that)\s+isn(?:['’]t)\s+[^.!?\n]{1,100}[.!;—]\s*(?:it|this|that)(?:['’]s|\s+is)\s+[^.!?\n]{1,100}/giu
|
|
3184
|
+
},
|
|
3185
|
+
{
|
|
3186
|
+
tell: "not just X, but Y",
|
|
3187
|
+
category: "rhetorical template",
|
|
3188
|
+
pattern: /\bnot\s+just\s+[^.!?\n]{1,100}?,?\s+(?:but(?:\s+also)?|(?:it|this|that)(?:['’]s|\s+is))\s+[^.!?\n]{1,100}/giu
|
|
3189
|
+
}
|
|
3190
|
+
];
|
|
3191
|
+
function detectSlop(text) {
|
|
3192
|
+
const prose = maskMarkdownCode(text);
|
|
3193
|
+
const candidates = SLOP_SIGNALS.flatMap((signal) => findSignalMatches(prose, signal));
|
|
3194
|
+
addCheckmarkWall(prose, candidates);
|
|
3195
|
+
const matches = longestNonOverlapping(candidates).map(({ end: _end, ...match }) => match);
|
|
3196
|
+
return { count: matches.length, matches };
|
|
3197
|
+
}
|
|
3198
|
+
function findSignalMatches(text, signal) {
|
|
3199
|
+
const matches = [];
|
|
3200
|
+
signal.pattern.lastIndex = 0;
|
|
3201
|
+
let match;
|
|
3202
|
+
while ((match = signal.pattern.exec(text)) !== null) {
|
|
3203
|
+
const bounds = trimmedBounds(match[0]);
|
|
3204
|
+
if (bounds.text) {
|
|
3205
|
+
const index = match.index + bounds.start;
|
|
3206
|
+
matches.push({
|
|
3207
|
+
tell: signal.tell,
|
|
3208
|
+
text: bounds.text,
|
|
3209
|
+
index,
|
|
3210
|
+
end: index + bounds.text.length,
|
|
3211
|
+
category: signal.category
|
|
3212
|
+
});
|
|
3213
|
+
}
|
|
3214
|
+
if (match[0].length === 0) {
|
|
3215
|
+
signal.pattern.lastIndex++;
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
return matches;
|
|
3219
|
+
}
|
|
3220
|
+
function trimmedBounds(value) {
|
|
3221
|
+
const start = value.search(/\S/u);
|
|
3222
|
+
if (start === -1) {
|
|
3223
|
+
return { text: "", start: 0 };
|
|
3224
|
+
}
|
|
3225
|
+
return { text: value.trim(), start };
|
|
3226
|
+
}
|
|
3227
|
+
function maskMarkdownCode(text) {
|
|
3228
|
+
return text.replace(/(?:```|~~~)[\s\S]*?(?:(?:```|~~~)|$)/gu, preserveNewlines).replace(/`[^`\n]+`/gu, preserveNewlines);
|
|
3229
|
+
}
|
|
3230
|
+
function preserveNewlines(value) {
|
|
3231
|
+
return value.replace(/[^\n]/gu, " ");
|
|
3232
|
+
}
|
|
3233
|
+
function addCheckmarkWall(text, candidates) {
|
|
3234
|
+
const checkmarks = Array.from(text.matchAll(/^[\t ]*(?:[-*]\s*)?[✅✓]\s+/gmu));
|
|
3235
|
+
if (checkmarks.length < 3) {
|
|
3236
|
+
return;
|
|
3237
|
+
}
|
|
3238
|
+
const first = checkmarks[0];
|
|
3239
|
+
if (!first) {
|
|
3240
|
+
return;
|
|
3241
|
+
}
|
|
3242
|
+
const offset = first[0].search(/[✅✓]/u);
|
|
3243
|
+
const index = first.index + Math.max(offset, 0);
|
|
3244
|
+
candidates.push({
|
|
3245
|
+
tell: "checkmark wall",
|
|
3246
|
+
text: `\u2705 \xD7${checkmarks.length}`,
|
|
3247
|
+
index,
|
|
3248
|
+
end: index + 2,
|
|
3249
|
+
category: "formatting"
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
function longestNonOverlapping(candidates) {
|
|
3253
|
+
const longestFirst = [...candidates].sort(
|
|
3254
|
+
(left, right) => right.end - right.index - (left.end - left.index) || left.index - right.index || left.tell.localeCompare(right.tell)
|
|
3255
|
+
);
|
|
3256
|
+
const accepted = [];
|
|
3257
|
+
for (const candidate of longestFirst) {
|
|
3258
|
+
const overlaps = accepted.some(
|
|
3259
|
+
(match) => candidate.index < match.end && candidate.end > match.index
|
|
3260
|
+
);
|
|
3261
|
+
if (!overlaps) {
|
|
3262
|
+
accepted.push(candidate);
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
return accepted.sort((left, right) => left.index - right.index);
|
|
3266
|
+
}
|
|
3267
|
+
|
|
2924
3268
|
// src/commands/scan.ts
|
|
2925
3269
|
var c = {
|
|
2926
3270
|
reset: "\x1B[0m",
|
|
@@ -2955,6 +3299,14 @@ var COST_SPINNER_MESSAGES = [
|
|
|
2955
3299
|
"Crunching token counts",
|
|
2956
3300
|
"Still working through local history"
|
|
2957
3301
|
];
|
|
3302
|
+
var SLOP_SPINNER_MESSAGES = [
|
|
3303
|
+
"Auditing agent prose",
|
|
3304
|
+
"Counting load-bearing insights",
|
|
3305
|
+
"Inspecting suspiciously honest takes",
|
|
3306
|
+
"Measuring corrective juxtaposition",
|
|
3307
|
+
"Checking what earned its keep",
|
|
3308
|
+
"Cataloging chatbot tics"
|
|
3309
|
+
];
|
|
2958
3310
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2959
3311
|
function createSpinner(messages = SPINNER_MESSAGES) {
|
|
2960
3312
|
let messageIdx = 0;
|
|
@@ -2994,7 +3346,7 @@ function createSpinner(messages = SPINNER_MESSAGES) {
|
|
|
2994
3346
|
}
|
|
2995
3347
|
};
|
|
2996
3348
|
}
|
|
2997
|
-
function parseArgs(args) {
|
|
3349
|
+
function parseArgs(args, command = "scan") {
|
|
2998
3350
|
const options = {};
|
|
2999
3351
|
for (let i = 0; i < args.length; i++) {
|
|
3000
3352
|
const arg = args[i];
|
|
@@ -3016,7 +3368,18 @@ function parseArgs(args) {
|
|
|
3016
3368
|
} else if (arg === "--month") {
|
|
3017
3369
|
setRelativeRange(options, 30);
|
|
3018
3370
|
} else if (arg === "--help" || arg === "-h") {
|
|
3019
|
-
|
|
3371
|
+
if (command === "slop") {
|
|
3372
|
+
console.log(`devrage slop \u2014 scan coding-agent responses for AI-isms
|
|
3373
|
+
|
|
3374
|
+
Options:
|
|
3375
|
+
--agent, -a <name> Scan only a specific agent (claude, codex, cursor, opencode, amp, cline, pi, t3code, zed)
|
|
3376
|
+
--since, -s <date> Only scan responses after this date (ISO 8601)
|
|
3377
|
+
--day, --days [n] Only scan the last n days (default: 1)
|
|
3378
|
+
--week Only scan the last 7 days
|
|
3379
|
+
--month Only scan the last 30 days
|
|
3380
|
+
--help, -h Show this help`);
|
|
3381
|
+
} else {
|
|
3382
|
+
console.log(`devrage scan \u2014 scan sessions for profanity
|
|
3020
3383
|
|
|
3021
3384
|
Options:
|
|
3022
3385
|
--agent, -a <name> Scan only a specific agent (claude, codex, cursor, opencode, amp, cline, pi, t3code, zed)
|
|
@@ -3025,6 +3388,7 @@ Options:
|
|
|
3025
3388
|
--week Only scan the last 7 days
|
|
3026
3389
|
--month Only scan the last 30 days
|
|
3027
3390
|
--help, -h Show this help`);
|
|
3391
|
+
}
|
|
3028
3392
|
process.exit(0);
|
|
3029
3393
|
}
|
|
3030
3394
|
}
|
|
@@ -3169,6 +3533,98 @@ async function scan(args) {
|
|
|
3169
3533
|
console.log("");
|
|
3170
3534
|
}
|
|
3171
3535
|
}
|
|
3536
|
+
async function slop(args) {
|
|
3537
|
+
const options = parseArgs(args, "slop");
|
|
3538
|
+
const adapters = options.agent ? [createAdapter(options.agent)] : allAdapters();
|
|
3539
|
+
const spinner = createSpinner(SLOP_SPINNER_MESSAGES);
|
|
3540
|
+
const tellTally = {};
|
|
3541
|
+
const categoryTally = {};
|
|
3542
|
+
const perAgent = {};
|
|
3543
|
+
let totalMessages = 0;
|
|
3544
|
+
let totalHits = 0;
|
|
3545
|
+
let taintedMessages = 0;
|
|
3546
|
+
spinner.start();
|
|
3547
|
+
try {
|
|
3548
|
+
for (const adapter of adapters) {
|
|
3549
|
+
let agentMessages = 0;
|
|
3550
|
+
let agentHits = 0;
|
|
3551
|
+
let agentTainted = 0;
|
|
3552
|
+
spinner.update();
|
|
3553
|
+
for await (const message of adapter.messages({
|
|
3554
|
+
role: "assistant",
|
|
3555
|
+
since: options.since
|
|
3556
|
+
})) {
|
|
3557
|
+
totalMessages++;
|
|
3558
|
+
agentMessages++;
|
|
3559
|
+
const result = detectSlop(message.text);
|
|
3560
|
+
if (result.count === 0) {
|
|
3561
|
+
continue;
|
|
3562
|
+
}
|
|
3563
|
+
totalHits += result.count;
|
|
3564
|
+
agentHits += result.count;
|
|
3565
|
+
taintedMessages++;
|
|
3566
|
+
agentTainted++;
|
|
3567
|
+
for (const match of result.matches) {
|
|
3568
|
+
tellTally[match.tell] = (tellTally[match.tell] ?? 0) + 1;
|
|
3569
|
+
categoryTally[match.category] = (categoryTally[match.category] ?? 0) + 1;
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
if (agentMessages > 0) {
|
|
3573
|
+
perAgent[adapter.name] = {
|
|
3574
|
+
messages: agentMessages,
|
|
3575
|
+
hits: agentHits,
|
|
3576
|
+
tainted: agentTainted
|
|
3577
|
+
};
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
} finally {
|
|
3581
|
+
spinner.stop();
|
|
3582
|
+
}
|
|
3583
|
+
const activeAgents = Object.entries(perAgent);
|
|
3584
|
+
console.log("");
|
|
3585
|
+
printReportHeader(options, "slop");
|
|
3586
|
+
printSlopOverview(totalMessages, totalHits, taintedMessages);
|
|
3587
|
+
if (activeAgents.length > 1) {
|
|
3588
|
+
console.log("");
|
|
3589
|
+
console.log(` ${sectionTitle("agent slop")}`);
|
|
3590
|
+
for (const [name, stats] of activeAgents) {
|
|
3591
|
+
const rate = stats.messages > 0 ? stats.tainted / stats.messages : 0;
|
|
3592
|
+
console.log(
|
|
3593
|
+
` ${colorText(name.padEnd(10), agentColor(name))} ${c.bold}${String(stats.hits).padStart(4)}${c.reset} ${c.dim}hits \xB7 ${stats.tainted}/${stats.messages} messages (${formatPercent(rate)})${c.reset}`
|
|
3594
|
+
);
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
if (totalHits > 0) {
|
|
3598
|
+
const tells = Object.entries(tellTally).sort(
|
|
3599
|
+
([left, leftCount], [right, rightCount]) => rightCount - leftCount || left.localeCompare(right)
|
|
3600
|
+
);
|
|
3601
|
+
console.log("");
|
|
3602
|
+
console.log(` ${sectionTitle("top tells")}`);
|
|
3603
|
+
for (const [tell, count] of tells.slice(0, 15)) {
|
|
3604
|
+
console.log(
|
|
3605
|
+
` ${c.yellow}${tell.padEnd(28)}${c.reset} ${c.bold}${String(count).padStart(4)}${c.reset}`
|
|
3606
|
+
);
|
|
3607
|
+
}
|
|
3608
|
+
const categories = Object.entries(categoryTally).sort(
|
|
3609
|
+
([left, leftCount], [right, rightCount]) => (rightCount ?? 0) - (leftCount ?? 0) || left.localeCompare(right)
|
|
3610
|
+
);
|
|
3611
|
+
console.log("");
|
|
3612
|
+
console.log(` ${sectionTitle("slop flavors")}`);
|
|
3613
|
+
for (const [category, count] of categories) {
|
|
3614
|
+
console.log(
|
|
3615
|
+
` ${c.cyan}${category.padEnd(28)}${c.reset} ${c.bold}${String(count).padStart(4)}${c.reset}`
|
|
3616
|
+
);
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
console.log("");
|
|
3620
|
+
if (totalMessages === 0) {
|
|
3621
|
+
console.log(` ${c.gray}no assistant messages found.${c.reset}`);
|
|
3622
|
+
console.log("");
|
|
3623
|
+
} else if (totalHits === 0) {
|
|
3624
|
+
console.log(` ${c.green}suspiciously human. not a single slop tell found.${c.reset}`);
|
|
3625
|
+
console.log("");
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3172
3628
|
async function cost(args) {
|
|
3173
3629
|
const options = parseCostArgs(args);
|
|
3174
3630
|
const adapters = options.agent ? [createAdapter(options.agent)] : allAdapters();
|
|
@@ -3562,13 +4018,25 @@ function renderCostHtmlReport(data) {
|
|
|
3562
4018
|
function jsonForScript(value) {
|
|
3563
4019
|
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
3564
4020
|
}
|
|
3565
|
-
function printReportHeader(options) {
|
|
4021
|
+
function printReportHeader(options, label = "report") {
|
|
3566
4022
|
const scope = options.rangeLabel ?? (options.since ? `since ${formatDate(options.since)}` : "all local history");
|
|
3567
4023
|
const agent = options.agent ? ` \xB7 ${options.agent}` : "";
|
|
3568
|
-
console.log(` ${c.bold}${c.red}devrage${c.reset} ${c.dim}
|
|
4024
|
+
console.log(` ${c.bold}${c.red}devrage${c.reset} ${c.dim}${label}${c.reset}`);
|
|
3569
4025
|
console.log(` ${c.dim}${scope}${agent}${c.reset}`);
|
|
3570
4026
|
console.log(` ${c.dim}${"\u2500".repeat(54)}${c.reset}`);
|
|
3571
4027
|
}
|
|
4028
|
+
function printSlopOverview(totalMessages, totalHits, taintedMessages) {
|
|
4029
|
+
const rate = totalMessages > 0 ? taintedMessages / totalMessages : 0;
|
|
4030
|
+
console.log(
|
|
4031
|
+
` ${c.dim}assistant messages${c.reset} ${c.bold}${formatNumber(totalMessages)}${c.reset}`
|
|
4032
|
+
);
|
|
4033
|
+
console.log(
|
|
4034
|
+
` ${c.dim}slop hits${c.reset} ${c.bold}${c.yellow}${formatNumber(totalHits)}${c.reset}`
|
|
4035
|
+
);
|
|
4036
|
+
console.log(
|
|
4037
|
+
` ${c.dim}messages with slop${c.reset} ${c.bold}${formatNumber(taintedMessages)}${c.reset} ${c.dim}(${formatPercent(rate)})${c.reset}`
|
|
4038
|
+
);
|
|
4039
|
+
}
|
|
3572
4040
|
function printBasicOverview(totalMessages, totalSwears) {
|
|
3573
4041
|
console.log(
|
|
3574
4042
|
` ${c.dim}messages scanned${c.reset} ${c.bold}${formatNumber(totalMessages)}${c.reset}`
|
|
@@ -3696,11 +4164,12 @@ function formatDate(value) {
|
|
|
3696
4164
|
// src/cli.ts
|
|
3697
4165
|
var COMMANDS = {
|
|
3698
4166
|
cost,
|
|
3699
|
-
scan
|
|
4167
|
+
scan,
|
|
4168
|
+
slop
|
|
3700
4169
|
};
|
|
3701
4170
|
var OPTIONS_WITH_VALUES = /* @__PURE__ */ new Set(["--agent", "-a", "--since", "-s"]);
|
|
3702
4171
|
function usage() {
|
|
3703
|
-
console.log(`devrage \u2014
|
|
4172
|
+
console.log(`devrage \u2014 measure rage, cost, and AI slop in coding-agent sessions
|
|
3704
4173
|
|
|
3705
4174
|
Usage:
|
|
3706
4175
|
devrage <command> [options]
|
|
@@ -3708,6 +4177,7 @@ Usage:
|
|
|
3708
4177
|
Commands:
|
|
3709
4178
|
cost Show API-equivalent coding agent cost
|
|
3710
4179
|
scan Scan sessions for profanity
|
|
4180
|
+
slop Scan agent responses for common AI-isms
|
|
3711
4181
|
|
|
3712
4182
|
Options:
|
|
3713
4183
|
--help, -h Show this help message
|
|
@@ -3716,7 +4186,9 @@ Options:
|
|
|
3716
4186
|
Examples:
|
|
3717
4187
|
devrage cost
|
|
3718
4188
|
devrage scan
|
|
4189
|
+
devrage slop
|
|
3719
4190
|
devrage scan --agent claude
|
|
4191
|
+
devrage slop --agent codex
|
|
3720
4192
|
devrage scan --since 2025-01-01`);
|
|
3721
4193
|
}
|
|
3722
4194
|
async function main() {
|
|
@@ -3727,7 +4199,7 @@ async function main() {
|
|
|
3727
4199
|
process.exit(0);
|
|
3728
4200
|
}
|
|
3729
4201
|
if (command === "--version") {
|
|
3730
|
-
console.log("0.
|
|
4202
|
+
console.log("0.6.0");
|
|
3731
4203
|
process.exit(0);
|
|
3732
4204
|
}
|
|
3733
4205
|
const parsed = parseCommand(args);
|