rulereceipt 0.1.80 → 0.1.81
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/NOTICE.md +13 -0
- package/dist/adapters/copilot.d.ts +7 -0
- package/dist/adapters/copilot.js +192 -0
- package/dist/adapters/index.d.ts +15 -1
- package/dist/adapters/index.js +22 -2
- package/package.json +1 -1
package/NOTICE.md
CHANGED
|
@@ -17,3 +17,16 @@ reader working from a single file rather than the whole repository.
|
|
|
17
17
|
|
|
18
18
|
Reading, studying, auditing, and using this code for personal or
|
|
19
19
|
internal purposes remains fully permitted — see LICENSE clauses 2-3.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Third-party format references (credits)
|
|
24
|
+
|
|
25
|
+
RuleReceipt's agent adapters are our own code. Where an on-disk session FORMAT
|
|
26
|
+
was documented by another MIT-licensed project, we credit it (we read the format,
|
|
27
|
+
we did not copy the code):
|
|
28
|
+
|
|
29
|
+
- **GitHub Copilot CLI** session format (`~/.copilot/session-state/*/events.jsonl`):
|
|
30
|
+
documented by **yigitkonur/cli-continues** (MIT), pinned commit
|
|
31
|
+
`e486cd22a592d89d890cff056624647fbe9cbe80`, which itself reverse-engineered it
|
|
32
|
+
from **Pilan-AI/mnemo** (MIT). Our reader is `src/adapters/copilot.ts`.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TranscriptEvent } from "../types.js";
|
|
2
|
+
/** Copilot CLI session files (events.jsonl) for this cwd, newest first. */
|
|
3
|
+
export declare function listCopilotSessions(cwd: string): string[];
|
|
4
|
+
/** True when the file looks like a Copilot CLI log at all (≥1 known event type). */
|
|
5
|
+
export declare function copilotFormatIsKnown(sessionFile: string): boolean;
|
|
6
|
+
/** Parse a Copilot CLI events.jsonl into neutral events. Tolerant: unknown → skipped. */
|
|
7
|
+
export declare function parseCopilotTranscript(sessionFile: string): TranscriptEvent[];
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync, realpathSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* GitHub Copilot CLI adapter — EXPERIMENTAL (no real-session fixtures yet).
|
|
6
|
+
*
|
|
7
|
+
* Reader is OUR OWN code. The on-disk FORMAT is documented by
|
|
8
|
+
* yigitkonur/cli-continues (MIT, src/parsers/copilot.ts, pinned commit
|
|
9
|
+
* e486cd22a592d89d890cff056624647fbe9cbe80), which reverse-engineered it; we map
|
|
10
|
+
* that format to our neutral events ourselves. Credit: see NOTICE.md and REUSE.md.
|
|
11
|
+
*
|
|
12
|
+
* Layout: ~/.copilot/session-state/<id>/ with `workspace.yaml` (holds `cwd`) and
|
|
13
|
+
* `events.jsonl`. `COPILOT_HOME` overrides the root. Event `type`s we map:
|
|
14
|
+
* user.message -> text (role user) data.content / transformedContent
|
|
15
|
+
* assistant.message -> text (role assistant) data.content; data.toolRequests[] -> tool_use
|
|
16
|
+
* tool.execution_start -> tool_use data.toolName / toolCallId / arguments
|
|
17
|
+
* tool.execution_complete -> tool_result data.toolCallId / success / result
|
|
18
|
+
*
|
|
19
|
+
* RESILIENCE (a format change must make us quieter, never wrong):
|
|
20
|
+
* - depend only on the fields above; ignore every unknown field;
|
|
21
|
+
* - a line that doesn't parse, or whose type we don't know, is skipped, never guessed;
|
|
22
|
+
* - `copilotFormatIsKnown` reports whether the file looked like a Copilot log at
|
|
23
|
+
* all, so a caller can say "format newer than tested" instead of fabricating.
|
|
24
|
+
*/
|
|
25
|
+
const KNOWN_TYPES = new Set([
|
|
26
|
+
"user.message", "assistant.message", "tool.execution_start", "tool.execution_complete",
|
|
27
|
+
"session.start", "session.shutdown",
|
|
28
|
+
]);
|
|
29
|
+
function copilotRoot() {
|
|
30
|
+
const env = process.env.COPILOT_HOME?.trim();
|
|
31
|
+
return env && env.length > 0 ? env : join(homedir(), ".copilot");
|
|
32
|
+
}
|
|
33
|
+
function realpathOr(p) {
|
|
34
|
+
try {
|
|
35
|
+
return realpathSync(p);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return p;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** `cwd:` out of a workspace.yaml, read by a line scan (no YAML dependency). */
|
|
42
|
+
function workspaceCwd(dir) {
|
|
43
|
+
try {
|
|
44
|
+
for (const line of readFileSync(join(dir, "workspace.yaml"), "utf-8").split("\n")) {
|
|
45
|
+
const m = line.match(/^\s*cwd\s*:\s*(.+?)\s*$/);
|
|
46
|
+
if (m)
|
|
47
|
+
return m[1].replace(/^["']|["']$/g, "");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch { /* unreadable */ }
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
/** Copilot CLI session files (events.jsonl) for this cwd, newest first. */
|
|
54
|
+
export function listCopilotSessions(cwd) {
|
|
55
|
+
const base = join(copilotRoot(), "session-state");
|
|
56
|
+
if (!existsSync(base))
|
|
57
|
+
return [];
|
|
58
|
+
const target = realpathOr(cwd);
|
|
59
|
+
const hits = [];
|
|
60
|
+
let entries = [];
|
|
61
|
+
try {
|
|
62
|
+
entries = readdirSync(base);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
for (const name of entries) {
|
|
68
|
+
const dir = join(base, name);
|
|
69
|
+
const events = join(dir, "events.jsonl");
|
|
70
|
+
if (!existsSync(events))
|
|
71
|
+
continue;
|
|
72
|
+
const wc = workspaceCwd(dir);
|
|
73
|
+
if (wc && realpathOr(wc) !== target)
|
|
74
|
+
continue; // belongs to another project
|
|
75
|
+
try {
|
|
76
|
+
hits.push({ file: events, mtimeMs: statSync(events).mtimeMs });
|
|
77
|
+
}
|
|
78
|
+
catch { /* skip */ }
|
|
79
|
+
}
|
|
80
|
+
return hits.sort((a, b) => b.mtimeMs - a.mtimeMs).map((h) => h.file);
|
|
81
|
+
}
|
|
82
|
+
function asRecord(v) {
|
|
83
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
84
|
+
}
|
|
85
|
+
function str(v) {
|
|
86
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
87
|
+
}
|
|
88
|
+
/** Map one tool invocation (name + args) to our canonical tool_use input. */
|
|
89
|
+
function toToolUse(name, args, id, ts) {
|
|
90
|
+
const command = str(args.command) ?? str(args.cmd);
|
|
91
|
+
const filePath = str(args.path) ?? str(args.file_path) ?? str(args.filePath);
|
|
92
|
+
const lower = name.toLowerCase();
|
|
93
|
+
// A shell command → canonical Bash shape, so the command-scanning checks fire
|
|
94
|
+
// on Copilot exactly as on Claude Code.
|
|
95
|
+
if (command && (/(shell|bash|terminal|exec|run|command)/.test(lower) || !filePath)) {
|
|
96
|
+
return { role: "assistant", kind: "tool_use", toolName: "Bash", toolUseId: id, input: { command }, timestamp: ts };
|
|
97
|
+
}
|
|
98
|
+
if (filePath && /(write|create|edit|replace|apply|patch|insert|modif)/.test(lower)) {
|
|
99
|
+
return { role: "assistant", kind: "tool_use", toolName: lower.includes("edit") || lower.includes("replace") || lower.includes("patch") ? "Edit" : "Write", toolUseId: id, input: { file_path: filePath, ...args }, timestamp: ts };
|
|
100
|
+
}
|
|
101
|
+
if (filePath && /(read|view|open|cat|show)/.test(lower)) {
|
|
102
|
+
return { role: "assistant", kind: "tool_use", toolName: "Read", toolUseId: id, input: { file_path: filePath }, timestamp: ts };
|
|
103
|
+
}
|
|
104
|
+
return { role: "assistant", kind: "tool_use", toolName: name, toolUseId: id, input: args, timestamp: ts };
|
|
105
|
+
}
|
|
106
|
+
/** True when the file looks like a Copilot CLI log at all (≥1 known event type). */
|
|
107
|
+
export function copilotFormatIsKnown(sessionFile) {
|
|
108
|
+
let raw;
|
|
109
|
+
try {
|
|
110
|
+
raw = readFileSync(sessionFile, "utf-8");
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
for (const line of raw.split("\n")) {
|
|
116
|
+
if (!line.trim())
|
|
117
|
+
continue;
|
|
118
|
+
try {
|
|
119
|
+
const t = JSON.parse(line).type;
|
|
120
|
+
if (typeof t === "string" && KNOWN_TYPES.has(t))
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
catch { /* skip */ }
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
/** Parse a Copilot CLI events.jsonl into neutral events. Tolerant: unknown → skipped. */
|
|
128
|
+
export function parseCopilotTranscript(sessionFile) {
|
|
129
|
+
let raw;
|
|
130
|
+
try {
|
|
131
|
+
raw = readFileSync(sessionFile, "utf-8");
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
const out = [];
|
|
137
|
+
const startNames = new Map(); // toolCallId -> toolName
|
|
138
|
+
for (const line of raw.split("\n")) {
|
|
139
|
+
if (!line.trim())
|
|
140
|
+
continue;
|
|
141
|
+
let e;
|
|
142
|
+
try {
|
|
143
|
+
e = JSON.parse(line);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const ts = str(e.timestamp) ?? "";
|
|
149
|
+
const d = e.data ?? {};
|
|
150
|
+
switch (e.type) {
|
|
151
|
+
case "user.message": {
|
|
152
|
+
const text = str(d.content) ?? str(d.transformedContent);
|
|
153
|
+
if (text)
|
|
154
|
+
out.push({ role: "user", kind: "text", text, timestamp: ts });
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case "assistant.message": {
|
|
158
|
+
const text = typeof d.content === "string" ? d.content : d.content ? JSON.stringify(d.content) : undefined;
|
|
159
|
+
if (text)
|
|
160
|
+
out.push({ role: "assistant", kind: "text", text, timestamp: ts });
|
|
161
|
+
for (const [i, tr] of (d.toolRequests ?? []).entries()) {
|
|
162
|
+
const name = str(tr?.name);
|
|
163
|
+
if (!name)
|
|
164
|
+
continue;
|
|
165
|
+
out.push(toToolUse(name, { ...asRecord(tr?.args), ...asRecord(tr?.arguments) }, `tr:${i}`, ts));
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case "tool.execution_start": {
|
|
170
|
+
const name = str(d.toolName);
|
|
171
|
+
if (!name)
|
|
172
|
+
break;
|
|
173
|
+
const id = str(d.toolCallId);
|
|
174
|
+
if (id)
|
|
175
|
+
startNames.set(id, name);
|
|
176
|
+
out.push(toToolUse(name, asRecord(d.arguments), id, ts));
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case "tool.execution_complete": {
|
|
180
|
+
const id = str(d.toolCallId);
|
|
181
|
+
const name = id ? startNames.get(id) : undefined;
|
|
182
|
+
const r = d.result;
|
|
183
|
+
const content = typeof r === "string" ? r : str(asRecord(r).content) ?? str(asRecord(r).detailedContent) ?? (r ? JSON.stringify(r).slice(0, 2000) : "");
|
|
184
|
+
out.push({ role: "user", kind: "tool_result", toolUseId: id, content, isError: d.success === false, timestamp: ts, ...(name ? { toolName: name } : {}) });
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
default:
|
|
188
|
+
break; // unknown type: ignore, never fabricate
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -18,6 +18,12 @@ export interface SessionAdapter {
|
|
|
18
18
|
listSessions(cwd: string): string[];
|
|
19
19
|
/** Parse one session file into neutral events. */
|
|
20
20
|
parse(sessionFile: string): TranscriptEvent[];
|
|
21
|
+
/**
|
|
22
|
+
* EXPERIMENTAL = reader exists but has no real-session + planted + clean
|
|
23
|
+
* fixtures yet, so it is NOT auto-detected (it would risk an unverified parse
|
|
24
|
+
* winning "newest session"). It is used only when the user points at a file.
|
|
25
|
+
*/
|
|
26
|
+
experimental?: boolean;
|
|
21
27
|
}
|
|
22
28
|
/**
|
|
23
29
|
* Claude Code — the original and reference adapter. Delegates to the existing
|
|
@@ -28,8 +34,16 @@ export interface SessionAdapter {
|
|
|
28
34
|
export declare const claudeCodeAdapter: SessionAdapter;
|
|
29
35
|
/** OpenAI Codex CLI — format verified, parsing tolerant. See adapters/codex.ts. */
|
|
30
36
|
export declare const codexAdapter: SessionAdapter;
|
|
31
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* GitHub Copilot CLI — EXPERIMENTAL. Reader is ours; the format is per
|
|
39
|
+
* cli-continues (MIT, pinned SHA — see REUSE.md/NOTICE). No real-session
|
|
40
|
+
* fixtures yet, so not auto-detected; reachable via `--transcript <events.jsonl>`.
|
|
41
|
+
*/
|
|
42
|
+
export declare const copilotCliAdapter: SessionAdapter;
|
|
43
|
+
/** Adapters with a verified, tested parser — these auto-detect the newest session. */
|
|
32
44
|
export declare const ADAPTERS: SessionAdapter[];
|
|
45
|
+
/** Experimental adapters: reader exists, awaiting real+planted+clean fixtures. */
|
|
46
|
+
export declare const EXPERIMENTAL_ADAPTERS: SessionAdapter[];
|
|
33
47
|
/**
|
|
34
48
|
* Tools deliberately NOT read yet, with the honest reason. Kept as data (not
|
|
35
49
|
* silence) so the tool — and its docs — can state exactly where the line is
|
package/dist/adapters/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { statSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { listAllSessionFiles, readTranscriptFromFile, findSubagentFiles } from "../parsers/transcriptParser.js";
|
|
4
4
|
import { listCodexSessions, parseCodexTranscript } from "./codex.js";
|
|
5
|
+
import { listCopilotSessions, parseCopilotTranscript, copilotFormatIsKnown } from "./copilot.js";
|
|
5
6
|
/**
|
|
6
7
|
* Claude Code — the original and reference adapter. Delegates to the existing
|
|
7
8
|
* transcriptParser so its behaviour (including subagent transcripts) is
|
|
@@ -24,8 +25,21 @@ export const codexAdapter = {
|
|
|
24
25
|
listSessions: (cwd) => listCodexSessions(cwd),
|
|
25
26
|
parse: (sessionFile) => parseCodexTranscript(sessionFile),
|
|
26
27
|
};
|
|
27
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* GitHub Copilot CLI — EXPERIMENTAL. Reader is ours; the format is per
|
|
30
|
+
* cli-continues (MIT, pinned SHA — see REUSE.md/NOTICE). No real-session
|
|
31
|
+
* fixtures yet, so not auto-detected; reachable via `--transcript <events.jsonl>`.
|
|
32
|
+
*/
|
|
33
|
+
export const copilotCliAdapter = {
|
|
34
|
+
tool: "copilot-cli",
|
|
35
|
+
listSessions: (cwd) => listCopilotSessions(cwd),
|
|
36
|
+
parse: (sessionFile) => parseCopilotTranscript(sessionFile),
|
|
37
|
+
experimental: true,
|
|
38
|
+
};
|
|
39
|
+
/** Adapters with a verified, tested parser — these auto-detect the newest session. */
|
|
28
40
|
export const ADAPTERS = [claudeCodeAdapter, codexAdapter];
|
|
41
|
+
/** Experimental adapters: reader exists, awaiting real+planted+clean fixtures. */
|
|
42
|
+
export const EXPERIMENTAL_ADAPTERS = [copilotCliAdapter];
|
|
29
43
|
/**
|
|
30
44
|
* Tools deliberately NOT read yet, with the honest reason. Kept as data (not
|
|
31
45
|
* silence) so the tool — and its docs — can state exactly where the line is
|
|
@@ -36,7 +50,6 @@ export const UNSUPPORTED_TOOLS = [
|
|
|
36
50
|
{ tool: "aider", reason: "history is a Markdown transcript (.aider.chat.history.md), not structured events — needs a prose parser, not a field mapping" },
|
|
37
51
|
{ tool: "opencode", reason: "stores sessions in a SQLite DB (opencode.db) since v1.2.0 (per-record JSON before) — needs a SQLite reader, version-dependent" },
|
|
38
52
|
{ tool: "cursor", reason: "IDE-embedded; chat history lives in undocumented internal state that changes across Cursor versions — real ongoing maintenance, out of scope for this pass" },
|
|
39
|
-
{ tool: "github-copilot", reason: "IDE-embedded; no accessible, stable local session log a third-party CLI can read" },
|
|
40
53
|
{ tool: "windsurf", reason: "IDE-embedded; history in undocumented internal state, same as Cursor" },
|
|
41
54
|
];
|
|
42
55
|
/**
|
|
@@ -113,6 +126,13 @@ export function parseSessionFile(file) {
|
|
|
113
126
|
if (obj && typeof obj === "object" && (obj.type === "session_meta" || obj.type === "response_item")) {
|
|
114
127
|
return parseCodexTranscript(file);
|
|
115
128
|
}
|
|
129
|
+
// Copilot CLI events.jsonl opens with a dotted event type (session.start,
|
|
130
|
+
// user.message, …). EXPERIMENTAL adapter; confirm it really is a Copilot log
|
|
131
|
+
// (a known event type somewhere) before using its reader, never on the
|
|
132
|
+
// dotted-type guess alone.
|
|
133
|
+
if (obj && typeof obj === "object" && typeof obj.type === "string" && obj.type.includes(".") && copilotFormatIsKnown(file)) {
|
|
134
|
+
return parseCopilotTranscript(file);
|
|
135
|
+
}
|
|
116
136
|
}
|
|
117
137
|
catch {
|
|
118
138
|
/* first line is not JSON: treat as a Claude transcript below */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rulereceipt",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.81",
|
|
4
4
|
"description": "Checks whether your AI coding agent followed your rules, with evidence. Works with Claude Code (Codex in testing); reads CLAUDE.md, AGENTS.md, Cursor, Copilot and Windsurf rules.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|