devrage 0.5.8 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +468 -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 +20 -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 +271 -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
|
}
|
|
@@ -1036,7 +1056,7 @@ async function* parseCursorStore(store, options) {
|
|
|
1036
1056
|
if (parsed === void 0) {
|
|
1037
1057
|
continue;
|
|
1038
1058
|
}
|
|
1039
|
-
for (const message of extractCursorMessages(parsed, row.key)) {
|
|
1059
|
+
for (const message of extractCursorMessages(parsed, row.key, options?.role ?? "user")) {
|
|
1040
1060
|
const text = message.text.trim();
|
|
1041
1061
|
if (!isLikelyMessageText(text)) {
|
|
1042
1062
|
continue;
|
|
@@ -1177,21 +1197,22 @@ function decodeStateValue(value) {
|
|
|
1177
1197
|
}
|
|
1178
1198
|
return null;
|
|
1179
1199
|
}
|
|
1180
|
-
function extractCursorMessages(root, rowKey) {
|
|
1200
|
+
function extractCursorMessages(root, rowKey, role) {
|
|
1181
1201
|
if (rowKey.startsWith("bubbleId:")) {
|
|
1182
|
-
const message = extractCursorBubbleMessage(root, rowKey);
|
|
1202
|
+
const message = extractCursorBubbleMessage(root, rowKey, role);
|
|
1183
1203
|
return message ? [message] : [];
|
|
1184
1204
|
}
|
|
1185
1205
|
const messages = [];
|
|
1186
|
-
collectRoleMessages(root, messages);
|
|
1187
|
-
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"))) {
|
|
1188
1208
|
collectPromptMessages(root, messages);
|
|
1189
1209
|
}
|
|
1190
1210
|
return uniqueMessages(messages);
|
|
1191
1211
|
}
|
|
1192
|
-
function extractCursorBubbleMessage(root, rowKey) {
|
|
1212
|
+
function extractCursorBubbleMessage(root, rowKey, role) {
|
|
1193
1213
|
const record = asRecord4(root);
|
|
1194
|
-
|
|
1214
|
+
const expectedType = role === "user" ? 1 : 2;
|
|
1215
|
+
if (!record || numberValue3(record["type"]) !== expectedType) {
|
|
1195
1216
|
return null;
|
|
1196
1217
|
}
|
|
1197
1218
|
const text = firstTextField(record, ["text", "richText"]);
|
|
@@ -1287,13 +1308,13 @@ function cursorBubbleSession(rowKey) {
|
|
|
1287
1308
|
const [, composerId] = rowKey.split(":");
|
|
1288
1309
|
return composerId?.trim() || void 0;
|
|
1289
1310
|
}
|
|
1290
|
-
function collectRoleMessages(value, messages, inheritedSession, depth = 0) {
|
|
1311
|
+
function collectRoleMessages(value, messages, role, inheritedSession, depth = 0) {
|
|
1291
1312
|
if (depth > 12) {
|
|
1292
1313
|
return;
|
|
1293
1314
|
}
|
|
1294
1315
|
if (Array.isArray(value)) {
|
|
1295
1316
|
for (const item of value) {
|
|
1296
|
-
collectRoleMessages(item, messages, inheritedSession, depth + 1);
|
|
1317
|
+
collectRoleMessages(item, messages, role, inheritedSession, depth + 1);
|
|
1297
1318
|
}
|
|
1298
1319
|
return;
|
|
1299
1320
|
}
|
|
@@ -1302,15 +1323,15 @@ function collectRoleMessages(value, messages, inheritedSession, depth = 0) {
|
|
|
1302
1323
|
return;
|
|
1303
1324
|
}
|
|
1304
1325
|
const session = extractSession(record) ?? inheritedSession;
|
|
1305
|
-
if (
|
|
1306
|
-
const text =
|
|
1326
|
+
if (isAuthoredBy(record, role)) {
|
|
1327
|
+
const text = extractMessageText2(record);
|
|
1307
1328
|
if (text) {
|
|
1308
1329
|
messages.push({ text, timestamp: extractTimestamp2(record), session });
|
|
1309
1330
|
}
|
|
1310
1331
|
}
|
|
1311
1332
|
for (const child of Object.values(record)) {
|
|
1312
1333
|
if (typeof child === "object" && child !== null) {
|
|
1313
|
-
collectRoleMessages(child, messages, session, depth + 1);
|
|
1334
|
+
collectRoleMessages(child, messages, role, session, depth + 1);
|
|
1314
1335
|
}
|
|
1315
1336
|
}
|
|
1316
1337
|
}
|
|
@@ -1350,6 +1371,9 @@ function collectPromptMessages(value, messages, inheritedSession, depth = 0) {
|
|
|
1350
1371
|
}
|
|
1351
1372
|
}
|
|
1352
1373
|
}
|
|
1374
|
+
function isAuthoredBy(record, role) {
|
|
1375
|
+
return role === "user" ? isUserAuthored(record) : isAssistantAuthored(record);
|
|
1376
|
+
}
|
|
1353
1377
|
function isUserAuthored(record) {
|
|
1354
1378
|
return ["role", "speaker", "sender", "author", "source", "from", "type", "kind"].some(
|
|
1355
1379
|
(field) => actorIsUser(record[field])
|
|
@@ -1383,7 +1407,7 @@ function actorString(value) {
|
|
|
1383
1407
|
}
|
|
1384
1408
|
return null;
|
|
1385
1409
|
}
|
|
1386
|
-
function
|
|
1410
|
+
function extractMessageText2(record) {
|
|
1387
1411
|
return firstTextField(record, ["text", "content", "message", "prompt", "query", "input"]);
|
|
1388
1412
|
}
|
|
1389
1413
|
function firstTextField(record, fields) {
|
|
@@ -1514,7 +1538,7 @@ function opencodeAdapter() {
|
|
|
1514
1538
|
return;
|
|
1515
1539
|
}
|
|
1516
1540
|
try {
|
|
1517
|
-
yield*
|
|
1541
|
+
yield* queryMessages(db, options);
|
|
1518
1542
|
} finally {
|
|
1519
1543
|
db.close();
|
|
1520
1544
|
}
|
|
@@ -1543,7 +1567,7 @@ async function openOpencodeDb() {
|
|
|
1543
1567
|
}
|
|
1544
1568
|
return db;
|
|
1545
1569
|
}
|
|
1546
|
-
function*
|
|
1570
|
+
function* queryMessages(db, options) {
|
|
1547
1571
|
let query = `
|
|
1548
1572
|
SELECT
|
|
1549
1573
|
m.session_id,
|
|
@@ -1551,10 +1575,10 @@ function* queryUserMessages(db, options) {
|
|
|
1551
1575
|
json_extract(p.data, '$.text') as text
|
|
1552
1576
|
FROM message m
|
|
1553
1577
|
JOIN part p ON p.message_id = m.id
|
|
1554
|
-
WHERE json_extract(m.data, '$.role') =
|
|
1578
|
+
WHERE json_extract(m.data, '$.role') = ?
|
|
1555
1579
|
AND json_extract(p.data, '$.type') = 'text'
|
|
1556
1580
|
`;
|
|
1557
|
-
const params = [];
|
|
1581
|
+
const params = [options?.role ?? "user"];
|
|
1558
1582
|
if (options?.since) {
|
|
1559
1583
|
query += ` AND m.time_created >= ?`;
|
|
1560
1584
|
params.push(options.since.getTime());
|
|
@@ -1662,7 +1686,12 @@ async function* walkPiSessions(dir, options, project) {
|
|
|
1662
1686
|
yield* walkPiSessions(fullPath, options, project ?? entry);
|
|
1663
1687
|
} else if (entry.endsWith(".jsonl")) {
|
|
1664
1688
|
const session = entry.replace(".jsonl", "");
|
|
1665
|
-
yield* parsePiJsonl(fullPath, {
|
|
1689
|
+
yield* parsePiJsonl(fullPath, {
|
|
1690
|
+
session,
|
|
1691
|
+
project,
|
|
1692
|
+
role: options?.role ?? "user",
|
|
1693
|
+
since: options?.since
|
|
1694
|
+
});
|
|
1666
1695
|
}
|
|
1667
1696
|
}
|
|
1668
1697
|
}
|
|
@@ -1707,7 +1736,7 @@ async function* parsePiJsonl(filePath, context) {
|
|
|
1707
1736
|
continue;
|
|
1708
1737
|
}
|
|
1709
1738
|
const message = entry.message;
|
|
1710
|
-
if (!message || message.role !==
|
|
1739
|
+
if (!message || message.role !== context.role) {
|
|
1711
1740
|
continue;
|
|
1712
1741
|
}
|
|
1713
1742
|
const text = contentToString2(message.content);
|
|
@@ -1824,7 +1853,7 @@ function t3codeAdapter() {
|
|
|
1824
1853
|
continue;
|
|
1825
1854
|
}
|
|
1826
1855
|
try {
|
|
1827
|
-
yield*
|
|
1856
|
+
yield* queryMessages2(db, location, options);
|
|
1828
1857
|
} finally {
|
|
1829
1858
|
db.close();
|
|
1830
1859
|
}
|
|
@@ -1886,7 +1915,7 @@ function resolveHomePath(value) {
|
|
|
1886
1915
|
async function openT3Db(dbPath) {
|
|
1887
1916
|
return openReadonlySqliteDatabase(dbPath);
|
|
1888
1917
|
}
|
|
1889
|
-
function*
|
|
1918
|
+
function* queryMessages2(db, location, options) {
|
|
1890
1919
|
if (!hasColumns(db, "projection_thread_messages", ["thread_id", "role", "text", "created_at"])) {
|
|
1891
1920
|
return;
|
|
1892
1921
|
}
|
|
@@ -1894,9 +1923,9 @@ function* queryUserMessages2(db, location, options) {
|
|
|
1894
1923
|
let query = `
|
|
1895
1924
|
SELECT thread_id, created_at, text
|
|
1896
1925
|
FROM projection_thread_messages
|
|
1897
|
-
WHERE role =
|
|
1926
|
+
WHERE role = ?
|
|
1898
1927
|
`;
|
|
1899
|
-
const params = [];
|
|
1928
|
+
const params = [options?.role ?? "user"];
|
|
1900
1929
|
if (options?.since) {
|
|
1901
1930
|
query += ` AND created_at >= ?`;
|
|
1902
1931
|
params.push(options.since.toISOString());
|
|
@@ -2251,7 +2280,7 @@ function zedAdapter() {
|
|
|
2251
2280
|
}
|
|
2252
2281
|
};
|
|
2253
2282
|
}
|
|
2254
|
-
async function* parseTextThreads(dir,
|
|
2283
|
+
async function* parseTextThreads(dir, options) {
|
|
2255
2284
|
if (!existsSync5(dir)) {
|
|
2256
2285
|
return;
|
|
2257
2286
|
}
|
|
@@ -2271,8 +2300,9 @@ async function* parseTextThreads(dir, _options) {
|
|
|
2271
2300
|
if (!conversation.messages || !Array.isArray(conversation.messages)) {
|
|
2272
2301
|
continue;
|
|
2273
2302
|
}
|
|
2303
|
+
const role = options?.role ?? "user";
|
|
2274
2304
|
for (const msg of conversation.messages) {
|
|
2275
|
-
if (msg.role !==
|
|
2305
|
+
if (msg.role !== role) {
|
|
2276
2306
|
continue;
|
|
2277
2307
|
}
|
|
2278
2308
|
const text = typeof msg.content === "string" ? msg.content : null;
|
|
@@ -2288,7 +2318,7 @@ async function* parseTextThreads(dir, _options) {
|
|
|
2288
2318
|
}
|
|
2289
2319
|
}
|
|
2290
2320
|
}
|
|
2291
|
-
async function* parseAgentThreads(dbDir,
|
|
2321
|
+
async function* parseAgentThreads(dbDir, options) {
|
|
2292
2322
|
if (!existsSync5(dbDir)) {
|
|
2293
2323
|
return;
|
|
2294
2324
|
}
|
|
@@ -2324,8 +2354,8 @@ async function* parseAgentThreads(dbDir, _options) {
|
|
|
2324
2354
|
continue;
|
|
2325
2355
|
}
|
|
2326
2356
|
const contentCol = colNames.includes("content") ? "content" : colNames.includes("body") ? "body" : "text";
|
|
2327
|
-
|
|
2328
|
-
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");
|
|
2329
2359
|
for (const row of rows) {
|
|
2330
2360
|
if (!row.text?.trim()) {
|
|
2331
2361
|
continue;
|
|
@@ -2960,6 +2990,261 @@ function asRecord7(value) {
|
|
|
2960
2990
|
return value;
|
|
2961
2991
|
}
|
|
2962
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: "real gap",
|
|
3064
|
+
category: "sycophancy",
|
|
3065
|
+
pattern: /\b(?:a\s+)?real\s+gap\b/giu
|
|
3066
|
+
},
|
|
3067
|
+
{
|
|
3068
|
+
tell: "error in my framing",
|
|
3069
|
+
category: "sycophancy",
|
|
3070
|
+
pattern: /\b(?:a\s+)?real\s+error\s+in\s+(?:my|the)\s+framing\b/giu
|
|
3071
|
+
},
|
|
3072
|
+
{
|
|
3073
|
+
tell: "I was wrong",
|
|
3074
|
+
category: "sycophancy",
|
|
3075
|
+
pattern: /\bi\s+was\s+wrong\b/giu
|
|
3076
|
+
},
|
|
3077
|
+
{
|
|
3078
|
+
tell: "I overcomplicated it",
|
|
3079
|
+
category: "sycophancy",
|
|
3080
|
+
pattern: /\bi\s+(?:overcomplicated|over-engineered|overthought)\s+(?:this|that|it)\b/giu
|
|
3081
|
+
},
|
|
3082
|
+
// Cross-model corporate/chatbot prose, constrained where a raw word is technical.
|
|
3083
|
+
{ tell: "delve", category: "stock prose", pattern: /\b(?:delve(?:d|s)?|delving)\b/giu },
|
|
3084
|
+
{ tell: "crucial", category: "stock prose", pattern: /\bcrucial\b/giu },
|
|
3085
|
+
{ tell: "pivotal", category: "stock prose", pattern: /\bpivotal\b/giu },
|
|
3086
|
+
{ tell: "tapestry", category: "stock prose", pattern: /\btapestr(?:y|ies)\b/giu },
|
|
3087
|
+
{
|
|
3088
|
+
tell: "here's the thing",
|
|
3089
|
+
category: "stock prose",
|
|
3090
|
+
pattern: /\bhere(?:['’]s|\s+is)\s+the\s+thing\b/giu
|
|
3091
|
+
},
|
|
3092
|
+
{
|
|
3093
|
+
tell: "hope this helps",
|
|
3094
|
+
category: "stock prose",
|
|
3095
|
+
pattern: /\bhope\s+(?:this|that)\s+helps\b/giu
|
|
3096
|
+
},
|
|
3097
|
+
{
|
|
3098
|
+
tell: "after careful consideration",
|
|
3099
|
+
category: "stock prose",
|
|
3100
|
+
pattern: /\bafter\s+careful\s+consideration\b/giu
|
|
3101
|
+
},
|
|
3102
|
+
{
|
|
3103
|
+
tell: "quick update",
|
|
3104
|
+
category: "stock prose",
|
|
3105
|
+
pattern: /\bto\s+provide\s+(?:you\s+with\s+)?a\s+quick\s+update\b/giu
|
|
3106
|
+
},
|
|
3107
|
+
{
|
|
3108
|
+
tell: "robust and reliable",
|
|
3109
|
+
category: "stock prose",
|
|
3110
|
+
pattern: /\brobust(?:\s+and|,)\s+reliable\b/giu
|
|
3111
|
+
},
|
|
3112
|
+
{ tell: "seamless", category: "stock prose", pattern: /\bseamless(?:ly)?\b/giu },
|
|
3113
|
+
{
|
|
3114
|
+
tell: "in the realm of",
|
|
3115
|
+
category: "stock prose",
|
|
3116
|
+
pattern: /\b(?:in|within)\s+the\s+realm\s+of\b/giu
|
|
3117
|
+
},
|
|
3118
|
+
{
|
|
3119
|
+
tell: "important to note",
|
|
3120
|
+
category: "stock prose",
|
|
3121
|
+
pattern: /\b(?:(?:it\s+is|it(?:['’]s))\s+)?important\s+to\s+note\b/giu
|
|
3122
|
+
},
|
|
3123
|
+
{
|
|
3124
|
+
tell: "let me break this down",
|
|
3125
|
+
category: "stock prose",
|
|
3126
|
+
pattern: /\blet\s+(?:me|us)\s+break\s+(?:this|it)\s+down\b/giu
|
|
3127
|
+
},
|
|
3128
|
+
{
|
|
3129
|
+
tell: "let's dive in",
|
|
3130
|
+
category: "stock prose",
|
|
3131
|
+
pattern: /\blet(?:['’]s|\s+us)\s+dive\s+(?:in|into)\b/giu
|
|
3132
|
+
},
|
|
3133
|
+
{
|
|
3134
|
+
tell: "let's unpack this",
|
|
3135
|
+
category: "stock prose",
|
|
3136
|
+
pattern: /\blet(?:['’]s|\s+us)\s+unpack\s+(?:this|that|it)\b/giu
|
|
3137
|
+
},
|
|
3138
|
+
{ tell: "at its core", category: "stock prose", pattern: /\bat\s+its\s+core\b/giu },
|
|
3139
|
+
{
|
|
3140
|
+
tell: "successfully implemented",
|
|
3141
|
+
category: "stock prose",
|
|
3142
|
+
pattern: /\bsuccessfully\s+(?:added|completed|created|fixed|implemented|resolved|updated)\b/giu
|
|
3143
|
+
},
|
|
3144
|
+
{
|
|
3145
|
+
tell: "Certainly.",
|
|
3146
|
+
category: "stock prose",
|
|
3147
|
+
pattern: /(?:^|\n)[\t ]*certainly\b/gimu
|
|
3148
|
+
},
|
|
3149
|
+
{
|
|
3150
|
+
tell: "I'd be happy to",
|
|
3151
|
+
category: "stock prose",
|
|
3152
|
+
pattern: /\bi(?:['’]d|\s+would)\s+be\s+happy\s+to\b/giu
|
|
3153
|
+
},
|
|
3154
|
+
// Corrective juxtaposition and fake-deep contrast templates.
|
|
3155
|
+
{
|
|
3156
|
+
tell: "it's not X, it's Y",
|
|
3157
|
+
category: "rhetorical template",
|
|
3158
|
+
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
|
|
3159
|
+
},
|
|
3160
|
+
{
|
|
3161
|
+
tell: "it's not X, it's Y",
|
|
3162
|
+
category: "rhetorical template",
|
|
3163
|
+
pattern: /\b(?:it|this|that)\s+isn(?:['’]t)\s+[^.!?\n]{1,100}[.!;—]\s*(?:it|this|that)(?:['’]s|\s+is)\s+[^.!?\n]{1,100}/giu
|
|
3164
|
+
},
|
|
3165
|
+
{
|
|
3166
|
+
tell: "not just X, but Y",
|
|
3167
|
+
category: "rhetorical template",
|
|
3168
|
+
pattern: /\bnot\s+just\s+[^.!?\n]{1,100}?,?\s+(?:but(?:\s+also)?|(?:it|this|that)(?:['’]s|\s+is))\s+[^.!?\n]{1,100}/giu
|
|
3169
|
+
}
|
|
3170
|
+
];
|
|
3171
|
+
function detectSlop(text) {
|
|
3172
|
+
const prose = maskMarkdownCode(text);
|
|
3173
|
+
const candidates = SLOP_SIGNALS.flatMap((signal) => findSignalMatches(prose, signal));
|
|
3174
|
+
addCheckmarkWall(prose, candidates);
|
|
3175
|
+
const matches = longestNonOverlapping(candidates).map(({ end: _end, ...match }) => match);
|
|
3176
|
+
return { count: matches.length, matches };
|
|
3177
|
+
}
|
|
3178
|
+
function findSignalMatches(text, signal) {
|
|
3179
|
+
const matches = [];
|
|
3180
|
+
signal.pattern.lastIndex = 0;
|
|
3181
|
+
let match;
|
|
3182
|
+
while ((match = signal.pattern.exec(text)) !== null) {
|
|
3183
|
+
const bounds = trimmedBounds(match[0]);
|
|
3184
|
+
if (bounds.text) {
|
|
3185
|
+
const index = match.index + bounds.start;
|
|
3186
|
+
matches.push({
|
|
3187
|
+
tell: signal.tell,
|
|
3188
|
+
text: bounds.text,
|
|
3189
|
+
index,
|
|
3190
|
+
end: index + bounds.text.length,
|
|
3191
|
+
category: signal.category
|
|
3192
|
+
});
|
|
3193
|
+
}
|
|
3194
|
+
if (match[0].length === 0) {
|
|
3195
|
+
signal.pattern.lastIndex++;
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
return matches;
|
|
3199
|
+
}
|
|
3200
|
+
function trimmedBounds(value) {
|
|
3201
|
+
const start = value.search(/\S/u);
|
|
3202
|
+
if (start === -1) {
|
|
3203
|
+
return { text: "", start: 0 };
|
|
3204
|
+
}
|
|
3205
|
+
return { text: value.trim(), start };
|
|
3206
|
+
}
|
|
3207
|
+
function maskMarkdownCode(text) {
|
|
3208
|
+
return text.replace(/(?:```|~~~)[\s\S]*?(?:(?:```|~~~)|$)/gu, preserveNewlines).replace(/`[^`\n]+`/gu, preserveNewlines);
|
|
3209
|
+
}
|
|
3210
|
+
function preserveNewlines(value) {
|
|
3211
|
+
return value.replace(/[^\n]/gu, " ");
|
|
3212
|
+
}
|
|
3213
|
+
function addCheckmarkWall(text, candidates) {
|
|
3214
|
+
const checkmarks = Array.from(text.matchAll(/^[\t ]*(?:[-*]\s*)?[✅✓]\s+/gmu));
|
|
3215
|
+
if (checkmarks.length < 3) {
|
|
3216
|
+
return;
|
|
3217
|
+
}
|
|
3218
|
+
const first = checkmarks[0];
|
|
3219
|
+
if (!first) {
|
|
3220
|
+
return;
|
|
3221
|
+
}
|
|
3222
|
+
const offset = first[0].search(/[✅✓]/u);
|
|
3223
|
+
const index = first.index + Math.max(offset, 0);
|
|
3224
|
+
candidates.push({
|
|
3225
|
+
tell: "checkmark wall",
|
|
3226
|
+
text: `\u2705 \xD7${checkmarks.length}`,
|
|
3227
|
+
index,
|
|
3228
|
+
end: index + 2,
|
|
3229
|
+
category: "formatting"
|
|
3230
|
+
});
|
|
3231
|
+
}
|
|
3232
|
+
function longestNonOverlapping(candidates) {
|
|
3233
|
+
const longestFirst = [...candidates].sort(
|
|
3234
|
+
(left, right) => right.end - right.index - (left.end - left.index) || left.index - right.index || left.tell.localeCompare(right.tell)
|
|
3235
|
+
);
|
|
3236
|
+
const accepted = [];
|
|
3237
|
+
for (const candidate of longestFirst) {
|
|
3238
|
+
const overlaps = accepted.some(
|
|
3239
|
+
(match) => candidate.index < match.end && candidate.end > match.index
|
|
3240
|
+
);
|
|
3241
|
+
if (!overlaps) {
|
|
3242
|
+
accepted.push(candidate);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
return accepted.sort((left, right) => left.index - right.index);
|
|
3246
|
+
}
|
|
3247
|
+
|
|
2963
3248
|
// src/commands/scan.ts
|
|
2964
3249
|
var c = {
|
|
2965
3250
|
reset: "\x1B[0m",
|
|
@@ -2994,6 +3279,14 @@ var COST_SPINNER_MESSAGES = [
|
|
|
2994
3279
|
"Crunching token counts",
|
|
2995
3280
|
"Still working through local history"
|
|
2996
3281
|
];
|
|
3282
|
+
var SLOP_SPINNER_MESSAGES = [
|
|
3283
|
+
"Auditing agent prose",
|
|
3284
|
+
"Counting load-bearing insights",
|
|
3285
|
+
"Inspecting suspiciously honest takes",
|
|
3286
|
+
"Measuring corrective juxtaposition",
|
|
3287
|
+
"Checking what earned its keep",
|
|
3288
|
+
"Cataloging chatbot tics"
|
|
3289
|
+
];
|
|
2997
3290
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2998
3291
|
function createSpinner(messages = SPINNER_MESSAGES) {
|
|
2999
3292
|
let messageIdx = 0;
|
|
@@ -3033,7 +3326,7 @@ function createSpinner(messages = SPINNER_MESSAGES) {
|
|
|
3033
3326
|
}
|
|
3034
3327
|
};
|
|
3035
3328
|
}
|
|
3036
|
-
function parseArgs(args) {
|
|
3329
|
+
function parseArgs(args, command = "scan") {
|
|
3037
3330
|
const options = {};
|
|
3038
3331
|
for (let i = 0; i < args.length; i++) {
|
|
3039
3332
|
const arg = args[i];
|
|
@@ -3055,7 +3348,18 @@ function parseArgs(args) {
|
|
|
3055
3348
|
} else if (arg === "--month") {
|
|
3056
3349
|
setRelativeRange(options, 30);
|
|
3057
3350
|
} else if (arg === "--help" || arg === "-h") {
|
|
3058
|
-
|
|
3351
|
+
if (command === "slop") {
|
|
3352
|
+
console.log(`devrage slop \u2014 scan coding-agent responses for AI-isms
|
|
3353
|
+
|
|
3354
|
+
Options:
|
|
3355
|
+
--agent, -a <name> Scan only a specific agent (claude, codex, cursor, opencode, amp, cline, pi, t3code, zed)
|
|
3356
|
+
--since, -s <date> Only scan responses after this date (ISO 8601)
|
|
3357
|
+
--day, --days [n] Only scan the last n days (default: 1)
|
|
3358
|
+
--week Only scan the last 7 days
|
|
3359
|
+
--month Only scan the last 30 days
|
|
3360
|
+
--help, -h Show this help`);
|
|
3361
|
+
} else {
|
|
3362
|
+
console.log(`devrage scan \u2014 scan sessions for profanity
|
|
3059
3363
|
|
|
3060
3364
|
Options:
|
|
3061
3365
|
--agent, -a <name> Scan only a specific agent (claude, codex, cursor, opencode, amp, cline, pi, t3code, zed)
|
|
@@ -3064,6 +3368,7 @@ Options:
|
|
|
3064
3368
|
--week Only scan the last 7 days
|
|
3065
3369
|
--month Only scan the last 30 days
|
|
3066
3370
|
--help, -h Show this help`);
|
|
3371
|
+
}
|
|
3067
3372
|
process.exit(0);
|
|
3068
3373
|
}
|
|
3069
3374
|
}
|
|
@@ -3208,6 +3513,98 @@ async function scan(args) {
|
|
|
3208
3513
|
console.log("");
|
|
3209
3514
|
}
|
|
3210
3515
|
}
|
|
3516
|
+
async function slop(args) {
|
|
3517
|
+
const options = parseArgs(args, "slop");
|
|
3518
|
+
const adapters = options.agent ? [createAdapter(options.agent)] : allAdapters();
|
|
3519
|
+
const spinner = createSpinner(SLOP_SPINNER_MESSAGES);
|
|
3520
|
+
const tellTally = {};
|
|
3521
|
+
const categoryTally = {};
|
|
3522
|
+
const perAgent = {};
|
|
3523
|
+
let totalMessages = 0;
|
|
3524
|
+
let totalHits = 0;
|
|
3525
|
+
let taintedMessages = 0;
|
|
3526
|
+
spinner.start();
|
|
3527
|
+
try {
|
|
3528
|
+
for (const adapter of adapters) {
|
|
3529
|
+
let agentMessages = 0;
|
|
3530
|
+
let agentHits = 0;
|
|
3531
|
+
let agentTainted = 0;
|
|
3532
|
+
spinner.update();
|
|
3533
|
+
for await (const message of adapter.messages({
|
|
3534
|
+
role: "assistant",
|
|
3535
|
+
since: options.since
|
|
3536
|
+
})) {
|
|
3537
|
+
totalMessages++;
|
|
3538
|
+
agentMessages++;
|
|
3539
|
+
const result = detectSlop(message.text);
|
|
3540
|
+
if (result.count === 0) {
|
|
3541
|
+
continue;
|
|
3542
|
+
}
|
|
3543
|
+
totalHits += result.count;
|
|
3544
|
+
agentHits += result.count;
|
|
3545
|
+
taintedMessages++;
|
|
3546
|
+
agentTainted++;
|
|
3547
|
+
for (const match of result.matches) {
|
|
3548
|
+
tellTally[match.tell] = (tellTally[match.tell] ?? 0) + 1;
|
|
3549
|
+
categoryTally[match.category] = (categoryTally[match.category] ?? 0) + 1;
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
if (agentMessages > 0) {
|
|
3553
|
+
perAgent[adapter.name] = {
|
|
3554
|
+
messages: agentMessages,
|
|
3555
|
+
hits: agentHits,
|
|
3556
|
+
tainted: agentTainted
|
|
3557
|
+
};
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
} finally {
|
|
3561
|
+
spinner.stop();
|
|
3562
|
+
}
|
|
3563
|
+
const activeAgents = Object.entries(perAgent);
|
|
3564
|
+
console.log("");
|
|
3565
|
+
printReportHeader(options, "slop");
|
|
3566
|
+
printSlopOverview(totalMessages, totalHits, taintedMessages);
|
|
3567
|
+
if (activeAgents.length > 1) {
|
|
3568
|
+
console.log("");
|
|
3569
|
+
console.log(` ${sectionTitle("agent slop")}`);
|
|
3570
|
+
for (const [name, stats] of activeAgents) {
|
|
3571
|
+
const rate = stats.messages > 0 ? stats.tainted / stats.messages : 0;
|
|
3572
|
+
console.log(
|
|
3573
|
+
` ${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}`
|
|
3574
|
+
);
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
if (totalHits > 0) {
|
|
3578
|
+
const tells = Object.entries(tellTally).sort(
|
|
3579
|
+
([left, leftCount], [right, rightCount]) => rightCount - leftCount || left.localeCompare(right)
|
|
3580
|
+
);
|
|
3581
|
+
console.log("");
|
|
3582
|
+
console.log(` ${sectionTitle("top tells")}`);
|
|
3583
|
+
for (const [tell, count] of tells.slice(0, 15)) {
|
|
3584
|
+
console.log(
|
|
3585
|
+
` ${c.yellow}${tell.padEnd(28)}${c.reset} ${c.bold}${String(count).padStart(4)}${c.reset}`
|
|
3586
|
+
);
|
|
3587
|
+
}
|
|
3588
|
+
const categories = Object.entries(categoryTally).sort(
|
|
3589
|
+
([left, leftCount], [right, rightCount]) => (rightCount ?? 0) - (leftCount ?? 0) || left.localeCompare(right)
|
|
3590
|
+
);
|
|
3591
|
+
console.log("");
|
|
3592
|
+
console.log(` ${sectionTitle("slop flavors")}`);
|
|
3593
|
+
for (const [category, count] of categories) {
|
|
3594
|
+
console.log(
|
|
3595
|
+
` ${c.cyan}${category.padEnd(28)}${c.reset} ${c.bold}${String(count).padStart(4)}${c.reset}`
|
|
3596
|
+
);
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
console.log("");
|
|
3600
|
+
if (totalMessages === 0) {
|
|
3601
|
+
console.log(` ${c.gray}no assistant messages found.${c.reset}`);
|
|
3602
|
+
console.log("");
|
|
3603
|
+
} else if (totalHits === 0) {
|
|
3604
|
+
console.log(` ${c.green}suspiciously human. not a single slop tell found.${c.reset}`);
|
|
3605
|
+
console.log("");
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3211
3608
|
async function cost(args) {
|
|
3212
3609
|
const options = parseCostArgs(args);
|
|
3213
3610
|
const adapters = options.agent ? [createAdapter(options.agent)] : allAdapters();
|
|
@@ -3601,13 +3998,25 @@ function renderCostHtmlReport(data) {
|
|
|
3601
3998
|
function jsonForScript(value) {
|
|
3602
3999
|
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
3603
4000
|
}
|
|
3604
|
-
function printReportHeader(options) {
|
|
4001
|
+
function printReportHeader(options, label = "report") {
|
|
3605
4002
|
const scope = options.rangeLabel ?? (options.since ? `since ${formatDate(options.since)}` : "all local history");
|
|
3606
4003
|
const agent = options.agent ? ` \xB7 ${options.agent}` : "";
|
|
3607
|
-
console.log(` ${c.bold}${c.red}devrage${c.reset} ${c.dim}
|
|
4004
|
+
console.log(` ${c.bold}${c.red}devrage${c.reset} ${c.dim}${label}${c.reset}`);
|
|
3608
4005
|
console.log(` ${c.dim}${scope}${agent}${c.reset}`);
|
|
3609
4006
|
console.log(` ${c.dim}${"\u2500".repeat(54)}${c.reset}`);
|
|
3610
4007
|
}
|
|
4008
|
+
function printSlopOverview(totalMessages, totalHits, taintedMessages) {
|
|
4009
|
+
const rate = totalMessages > 0 ? taintedMessages / totalMessages : 0;
|
|
4010
|
+
console.log(
|
|
4011
|
+
` ${c.dim}assistant messages${c.reset} ${c.bold}${formatNumber(totalMessages)}${c.reset}`
|
|
4012
|
+
);
|
|
4013
|
+
console.log(
|
|
4014
|
+
` ${c.dim}slop hits${c.reset} ${c.bold}${c.yellow}${formatNumber(totalHits)}${c.reset}`
|
|
4015
|
+
);
|
|
4016
|
+
console.log(
|
|
4017
|
+
` ${c.dim}messages with slop${c.reset} ${c.bold}${formatNumber(taintedMessages)}${c.reset} ${c.dim}(${formatPercent(rate)})${c.reset}`
|
|
4018
|
+
);
|
|
4019
|
+
}
|
|
3611
4020
|
function printBasicOverview(totalMessages, totalSwears) {
|
|
3612
4021
|
console.log(
|
|
3613
4022
|
` ${c.dim}messages scanned${c.reset} ${c.bold}${formatNumber(totalMessages)}${c.reset}`
|
|
@@ -3735,11 +4144,12 @@ function formatDate(value) {
|
|
|
3735
4144
|
// src/cli.ts
|
|
3736
4145
|
var COMMANDS = {
|
|
3737
4146
|
cost,
|
|
3738
|
-
scan
|
|
4147
|
+
scan,
|
|
4148
|
+
slop
|
|
3739
4149
|
};
|
|
3740
4150
|
var OPTIONS_WITH_VALUES = /* @__PURE__ */ new Set(["--agent", "-a", "--since", "-s"]);
|
|
3741
4151
|
function usage() {
|
|
3742
|
-
console.log(`devrage \u2014
|
|
4152
|
+
console.log(`devrage \u2014 measure rage, cost, and AI slop in coding-agent sessions
|
|
3743
4153
|
|
|
3744
4154
|
Usage:
|
|
3745
4155
|
devrage <command> [options]
|
|
@@ -3747,6 +4157,7 @@ Usage:
|
|
|
3747
4157
|
Commands:
|
|
3748
4158
|
cost Show API-equivalent coding agent cost
|
|
3749
4159
|
scan Scan sessions for profanity
|
|
4160
|
+
slop Scan agent responses for common AI-isms
|
|
3750
4161
|
|
|
3751
4162
|
Options:
|
|
3752
4163
|
--help, -h Show this help message
|
|
@@ -3755,7 +4166,9 @@ Options:
|
|
|
3755
4166
|
Examples:
|
|
3756
4167
|
devrage cost
|
|
3757
4168
|
devrage scan
|
|
4169
|
+
devrage slop
|
|
3758
4170
|
devrage scan --agent claude
|
|
4171
|
+
devrage slop --agent codex
|
|
3759
4172
|
devrage scan --since 2025-01-01`);
|
|
3760
4173
|
}
|
|
3761
4174
|
async function main() {
|
|
@@ -3766,7 +4179,7 @@ async function main() {
|
|
|
3766
4179
|
process.exit(0);
|
|
3767
4180
|
}
|
|
3768
4181
|
if (command === "--version") {
|
|
3769
|
-
console.log("0.
|
|
4182
|
+
console.log("0.6.1");
|
|
3770
4183
|
process.exit(0);
|
|
3771
4184
|
}
|
|
3772
4185
|
const parsed = parseCommand(args);
|