@globalmpc/nomore 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +174 -2
- package/dist/apply.js +74 -0
- package/dist/cli.js +150 -0
- package/dist/codex.js +156 -0
- package/dist/detect.js +165 -0
- package/dist/group.js +109 -0
- package/dist/issues.js +59 -0
- package/dist/mine.js +37 -0
- package/dist/normalize.js +18 -0
- package/dist/pipeline.js +58 -0
- package/dist/rank.js +18 -0
- package/dist/report.js +29 -0
- package/dist/rules.js +69 -0
- package/dist/sessions.js +106 -0
- package/dist/types.js +54 -0
- package/package.json +49 -4
package/dist/sessions.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { readdir, stat } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
export const resolveConfigDir = (env, home) => env.CLAUDE_CONFIG_DIR ?? join(home, ".claude");
|
|
6
|
+
// Claude Code names a project's folder after its path with every non-alphanumeric character replaced by "-".
|
|
7
|
+
const encodeProject = (path) => path.replace(/[^a-zA-Z0-9]/g, "-");
|
|
8
|
+
export async function listSessionFiles(configDir, opts) {
|
|
9
|
+
const root = join(configDir, "projects");
|
|
10
|
+
const dirs = await readdir(root).catch(() => []);
|
|
11
|
+
const files = [];
|
|
12
|
+
for (const d of dirs.sort()) {
|
|
13
|
+
// Sessions started in a subfolder live in their own encoded folder ("-repo-web"); lookalikes ("-repo-other") are
|
|
14
|
+
// removed later by the cwd check in runPipeline.
|
|
15
|
+
if (opts.project !== undefined && d !== encodeProject(opts.project) && !d.startsWith(encodeProject(opts.project) + "-"))
|
|
16
|
+
continue;
|
|
17
|
+
const names = await readdir(join(root, d)).catch(() => []);
|
|
18
|
+
for (const f of names.filter((n) => n.endsWith(".jsonl")).sort()) {
|
|
19
|
+
const path = join(root, d, f);
|
|
20
|
+
// Deleted since readdir: skip it. Any other stat error (EACCES) keeps the file, so parseSession counts it unreadable.
|
|
21
|
+
const mtime = await stat(path).then((st) => st.mtimeMs, (e) => (e.code === "ENOENT" ? -1 : Infinity));
|
|
22
|
+
if (mtime >= opts.sinceMs)
|
|
23
|
+
files.push(path);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return files;
|
|
27
|
+
}
|
|
28
|
+
const REJECTED = "The user doesn't want to proceed";
|
|
29
|
+
const INTERRUPT = "[Request interrupted by user";
|
|
30
|
+
// IDE context (<ide_opened_file>…), command tags and command output arrive as complete tag blocks before the text.
|
|
31
|
+
const stripLeadingTags = (s) => s.replace(/^\s*(<([\w-]+)[^>]*>[\s\S]*?<\/\2>\s*)+/, "");
|
|
32
|
+
const resultText = (c) => typeof c === "string" ? c : Array.isArray(c) ? c.map((i) => (typeof i?.text === "string" ? i.text : "")).join("") : "";
|
|
33
|
+
// One record → zero or more events (spec › Inputs, › Not a user message). Unknown shapes yield nothing.
|
|
34
|
+
function toEvents(r) {
|
|
35
|
+
if (r.isSidechain === true || typeof r.uuid !== "string")
|
|
36
|
+
return [];
|
|
37
|
+
// Ranking needs a time; without one the record can't be placed, so it is dropped like any other unknown shape.
|
|
38
|
+
if (typeof r.timestamp !== "string" || Number.isNaN(Date.parse(r.timestamp)))
|
|
39
|
+
return [];
|
|
40
|
+
// sdk-cli, sdk-ts, sdk-py: Claude Code run by a script, whose "user" messages are program-written templates.
|
|
41
|
+
if (typeof r.entrypoint === "string" && r.entrypoint.startsWith("sdk"))
|
|
42
|
+
return [];
|
|
43
|
+
const base = { uuid: r.uuid, sessionId: String(r.sessionId ?? ""), cwd: String(r.cwd ?? ""), ts: r.timestamp };
|
|
44
|
+
const content = r.message?.content;
|
|
45
|
+
if (r.type === "user") {
|
|
46
|
+
const injected = r.isMeta === true || r.isCompactSummary === true; // compaction summaries are written by Claude Code
|
|
47
|
+
const typed = (raw) => {
|
|
48
|
+
const text = stripLeadingTags(raw).trim();
|
|
49
|
+
return injected || text === "" || text.startsWith("<") ? [] : [{ ...base, kind: "user_text", text }];
|
|
50
|
+
};
|
|
51
|
+
if (typeof content === "string")
|
|
52
|
+
return typed(content);
|
|
53
|
+
if (!Array.isArray(content))
|
|
54
|
+
return [];
|
|
55
|
+
const signals = content.flatMap((i) => {
|
|
56
|
+
if (i?.type === "text" && typeof i.text === "string" && i.text.startsWith(INTERRUPT))
|
|
57
|
+
return [{ ...base, kind: "user_marker", text: i.text }];
|
|
58
|
+
if (i?.type === "tool_result" && i.is_error === true && resultText(i.content).startsWith(REJECTED))
|
|
59
|
+
return [{ ...base, kind: "tool_rejected" }];
|
|
60
|
+
return [];
|
|
61
|
+
});
|
|
62
|
+
// IDE extensions and pasted images store a typed message as blocks: join its plain text blocks.
|
|
63
|
+
if (content.some((i) => i?.type === "tool_result"))
|
|
64
|
+
return signals;
|
|
65
|
+
const text = content
|
|
66
|
+
.filter((i) => i?.type === "text" && typeof i.text === "string" && !i.text.startsWith(INTERRUPT) && !i.text.trimStart().startsWith("<"))
|
|
67
|
+
.map((i) => i.text)
|
|
68
|
+
.join("\n");
|
|
69
|
+
return [...signals, ...typed(text)];
|
|
70
|
+
}
|
|
71
|
+
if (r.type === "assistant" && Array.isArray(content)) {
|
|
72
|
+
return content.flatMap((i) => {
|
|
73
|
+
if (i?.type === "tool_use" && typeof i.name === "string")
|
|
74
|
+
return [{ ...base, kind: "assistant_tool_use", tool: i.name, input: i.input ?? {} }];
|
|
75
|
+
if (i?.type === "text" && typeof i.text === "string")
|
|
76
|
+
return [{ ...base, kind: "assistant_text", text: i.text }];
|
|
77
|
+
return [];
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
// A file that can't be read (permissions, a folder named *.jsonl) is reported as unreadable instead of ending the run.
|
|
83
|
+
export async function parseSession(file) {
|
|
84
|
+
const events = [];
|
|
85
|
+
let skippedLines = 0;
|
|
86
|
+
try {
|
|
87
|
+
for await (const line of createInterface({ input: createReadStream(file), crlfDelay: Infinity })) {
|
|
88
|
+
if (line.trim() === "")
|
|
89
|
+
continue;
|
|
90
|
+
let r;
|
|
91
|
+
try {
|
|
92
|
+
r = JSON.parse(line);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
skippedLines++; // a session still being written ends in a partial line
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (r && typeof r === "object")
|
|
99
|
+
events.push(...toEvents(r));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return { events: [], skippedLines: 0, unreadable: true };
|
|
104
|
+
}
|
|
105
|
+
return { events, skippedLines, unreadable: false };
|
|
106
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const Event = z.object({
|
|
3
|
+
kind: z.enum(["user_text", "user_marker", "tool_rejected", "assistant_tool_use", "assistant_text"]),
|
|
4
|
+
uuid: z.string(),
|
|
5
|
+
sessionId: z.string(),
|
|
6
|
+
cwd: z.string(),
|
|
7
|
+
ts: z.string(),
|
|
8
|
+
text: z.string().optional(),
|
|
9
|
+
tool: z.string().optional(),
|
|
10
|
+
input: z.record(z.string(), z.unknown()).optional(),
|
|
11
|
+
});
|
|
12
|
+
export const Signal = z.enum(["rejected", "interrupt", "phrase", "revert", "repeated"]);
|
|
13
|
+
export const Correction = z.object({
|
|
14
|
+
uuid: z.string(),
|
|
15
|
+
sessionId: z.string(),
|
|
16
|
+
cwd: z.string(),
|
|
17
|
+
ts: z.string(),
|
|
18
|
+
text: z.string(),
|
|
19
|
+
norm: z.string(),
|
|
20
|
+
signals: z.array(Signal),
|
|
21
|
+
});
|
|
22
|
+
export const Group = z.object({
|
|
23
|
+
id: z.string(),
|
|
24
|
+
members: z.array(Correction),
|
|
25
|
+
representative: Correction,
|
|
26
|
+
count: z.number(),
|
|
27
|
+
sessions: z.number(),
|
|
28
|
+
projects: z.number(),
|
|
29
|
+
days: z.number(),
|
|
30
|
+
lastSeen: z.string(),
|
|
31
|
+
score: z.number(),
|
|
32
|
+
signals: z.array(Signal),
|
|
33
|
+
});
|
|
34
|
+
export const Suggestion = z.object({
|
|
35
|
+
id: z.string(),
|
|
36
|
+
text: z.string(),
|
|
37
|
+
status: z.enum(["new", "ignored"]),
|
|
38
|
+
matchedRule: z.string().optional(),
|
|
39
|
+
count: z.number(),
|
|
40
|
+
sessions: z.number(),
|
|
41
|
+
projects: z.number(),
|
|
42
|
+
days: z.number(),
|
|
43
|
+
score: z.number(),
|
|
44
|
+
lastSeen: z.string(),
|
|
45
|
+
examples: z.array(z.string()).max(3),
|
|
46
|
+
signals: z.array(Signal),
|
|
47
|
+
});
|
|
48
|
+
export const Report = z.object({
|
|
49
|
+
scannedSessions: z.number(),
|
|
50
|
+
skippedLines: z.number(),
|
|
51
|
+
skippedFiles: z.number(),
|
|
52
|
+
corrections: z.number(),
|
|
53
|
+
suggestions: z.array(Suggestion),
|
|
54
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@globalmpc/nomore",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Stop telling Claude Code or Codex the same thing twice: a CLAUDE.md rule pack from ~10k issues, and a local tool that finds what you keep repeating.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude-code",
|
|
7
|
+
"claude",
|
|
8
|
+
"claude-md",
|
|
9
|
+
"codex",
|
|
10
|
+
"openai-codex",
|
|
11
|
+
"agents-md",
|
|
12
|
+
"ai-agents",
|
|
13
|
+
"developer-tools",
|
|
14
|
+
"cli"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/globalmpc/nomore.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/globalmpc/nomore#readme",
|
|
22
|
+
"bugs": "https://github.com/globalmpc/nomore/issues",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"bin": {
|
|
25
|
+
"nomore": "dist/cli.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"build": "tsc -p tsconfig.build.json",
|
|
37
|
+
"mine": "tsx scripts/mine-issues.ts"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"zod": "^4.6.7"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^26.6.5",
|
|
44
|
+
"tsx": "^4.23.15",
|
|
45
|
+
"typescript": "^7.0.2",
|
|
46
|
+
"vitest": "^5.0.5"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|