@sting8k/pi-vcc 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -0
- package/index.ts +10 -0
- package/package.json +36 -0
- package/scripts/audit-sessions.ts +88 -0
- package/scripts/benchmark-real-sessions.ts +25 -0
- package/scripts/compare-before-after.ts +36 -0
- package/src/commands/pi-vcc.ts +11 -0
- package/src/core/build-sections.ts +119 -0
- package/src/core/content.ts +20 -0
- package/src/core/filter-noise.ts +42 -0
- package/src/core/format-recall.ts +23 -0
- package/src/core/format.ts +34 -0
- package/src/core/normalize.ts +62 -0
- package/src/core/redact.ts +8 -0
- package/src/core/render-entries.ts +48 -0
- package/src/core/report.ts +225 -0
- package/src/core/sanitize.ts +5 -0
- package/src/core/search-entries.ts +14 -0
- package/src/core/summarize.ts +81 -0
- package/src/core/tool-args.ts +14 -0
- package/src/details.ts +7 -0
- package/src/extract/decisions.ts +32 -0
- package/src/extract/files.ts +46 -0
- package/src/extract/findings.ts +27 -0
- package/src/extract/goals.ts +41 -0
- package/src/extract/preferences.ts +30 -0
- package/src/hooks/before-compact.ts +141 -0
- package/src/sections.ts +11 -0
- package/src/tools/recall.ts +85 -0
- package/src/types.ts +14 -0
- package/tests/build-sections.test.ts +56 -0
- package/tests/compile.test.ts +50 -0
- package/tests/extract-decisions.test.ts +30 -0
- package/tests/extract-files.test.ts +62 -0
- package/tests/extract-findings.test.ts +39 -0
- package/tests/extract-goals.test.ts +86 -0
- package/tests/extract-preferences.test.ts +30 -0
- package/tests/filter-noise.test.ts +61 -0
- package/tests/fixtures.ts +61 -0
- package/tests/format-recall.test.ts +30 -0
- package/tests/format.test.ts +47 -0
- package/tests/normalize.test.ts +97 -0
- package/tests/real-sessions.test.ts +38 -0
- package/tests/render-entries.test.ts +40 -0
- package/tests/report.test.ts +54 -0
- package/tests/sanitize.test.ts +24 -0
- package/tests/search-entries.test.ts +33 -0
- package/tests/support/load-session.ts +23 -0
- package/tests/support/real-sessions.ts +51 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Message } from "@mariozechner/pi-ai";
|
|
2
|
+
|
|
3
|
+
const ts = Date.now();
|
|
4
|
+
const assistBase = {
|
|
5
|
+
api: "messages" as any,
|
|
6
|
+
provider: "anthropic" as any,
|
|
7
|
+
model: "test",
|
|
8
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
9
|
+
timestamp: ts,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const userMsg = (text: string): Message => ({
|
|
13
|
+
role: "user",
|
|
14
|
+
content: text,
|
|
15
|
+
timestamp: ts,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const assistantText = (text: string): Message => ({
|
|
19
|
+
role: "assistant",
|
|
20
|
+
content: [{ type: "text", text }],
|
|
21
|
+
...assistBase,
|
|
22
|
+
stopReason: "stop",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const assistantWithThinking = (
|
|
26
|
+
text: string,
|
|
27
|
+
thinking: string,
|
|
28
|
+
): Message => ({
|
|
29
|
+
role: "assistant",
|
|
30
|
+
content: [
|
|
31
|
+
{ type: "thinking", thinking },
|
|
32
|
+
{ type: "text", text },
|
|
33
|
+
],
|
|
34
|
+
...assistBase,
|
|
35
|
+
stopReason: "stop",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const assistantWithToolCall = (
|
|
39
|
+
name: string,
|
|
40
|
+
args: Record<string, unknown>,
|
|
41
|
+
): Message => ({
|
|
42
|
+
role: "assistant",
|
|
43
|
+
content: [{ type: "toolCall", id: "tc_1", name, arguments: args }],
|
|
44
|
+
...assistBase,
|
|
45
|
+
stopReason: "toolUse",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const toolResult = (
|
|
49
|
+
name: string,
|
|
50
|
+
text: string,
|
|
51
|
+
isError = false,
|
|
52
|
+
): Message => ({
|
|
53
|
+
role: "toolResult",
|
|
54
|
+
toolCallId: "tc_1",
|
|
55
|
+
toolName: name,
|
|
56
|
+
content: [{ type: "text", text }],
|
|
57
|
+
isError,
|
|
58
|
+
timestamp: ts,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { formatRecallOutput } from "../src/core/format-recall";
|
|
3
|
+
import type { RenderedEntry } from "../src/core/render-entries";
|
|
4
|
+
|
|
5
|
+
describe("formatRecallOutput", () => {
|
|
6
|
+
it("shows no-match message with query", () => {
|
|
7
|
+
const r = formatRecallOutput([], "xyz");
|
|
8
|
+
expect(r).toContain('No matches for "xyz"');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("shows no-entries message without query", () => {
|
|
12
|
+
expect(formatRecallOutput([])).toContain("No entries");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("formats entries with index and role", () => {
|
|
16
|
+
const entries: RenderedEntry[] = [
|
|
17
|
+
{ index: 0, role: "user", summary: "hello" },
|
|
18
|
+
];
|
|
19
|
+
const r = formatRecallOutput(entries);
|
|
20
|
+
expect(r).toContain("#0 [user] hello");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("shows match count with query", () => {
|
|
24
|
+
const entries: RenderedEntry[] = [
|
|
25
|
+
{ index: 2, role: "assistant", summary: "done" },
|
|
26
|
+
];
|
|
27
|
+
const r = formatRecallOutput(entries, "done");
|
|
28
|
+
expect(r).toContain('Found 1 matches for "done"');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { formatSummary } from "../src/core/format";
|
|
3
|
+
import type { SectionData } from "../src/sections";
|
|
4
|
+
|
|
5
|
+
const empty: SectionData = {
|
|
6
|
+
sessionGoal: [], currentState: [], whatWasDone: [],
|
|
7
|
+
importantFindings: [], filesRead: [], filesModified: [],
|
|
8
|
+
filesCreated: [], openProblems: [], decisions: [],
|
|
9
|
+
userPreferences: [], nextSteps: [],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe("formatSummary", () => {
|
|
13
|
+
it("returns empty string for all-empty sections", () => {
|
|
14
|
+
expect(formatSummary(empty)).toBe("");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("formats a single section", () => {
|
|
18
|
+
const data = { ...empty, sessionGoal: ["Fix login"] };
|
|
19
|
+
expect(formatSummary(data)).toBe("[Session Goal]\n- Fix login");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("formats files section with subcategories", () => {
|
|
23
|
+
const data = {
|
|
24
|
+
...empty,
|
|
25
|
+
filesRead: ["a.ts"],
|
|
26
|
+
filesModified: ["b.ts"],
|
|
27
|
+
};
|
|
28
|
+
const r = formatSummary(data);
|
|
29
|
+
expect(r).toContain("[Files And Changes]");
|
|
30
|
+
expect(r).toContain("Read:");
|
|
31
|
+
expect(r).toContain(" - a.ts");
|
|
32
|
+
expect(r).toContain("Modified:");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("joins multiple sections with blank line", () => {
|
|
36
|
+
const data = {
|
|
37
|
+
...empty,
|
|
38
|
+
sessionGoal: ["goal"],
|
|
39
|
+
nextSteps: ["step 1"],
|
|
40
|
+
};
|
|
41
|
+
const r = formatSummary(data);
|
|
42
|
+
expect(r).toContain("\n\n");
|
|
43
|
+
expect(r).toContain("[Session Goal]");
|
|
44
|
+
expect(r).toContain("[Next Best Steps]");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { normalize } from "../src/core/normalize";
|
|
3
|
+
import {
|
|
4
|
+
userMsg,
|
|
5
|
+
assistantText,
|
|
6
|
+
assistantWithThinking,
|
|
7
|
+
assistantWithToolCall,
|
|
8
|
+
toolResult,
|
|
9
|
+
} from "./fixtures";
|
|
10
|
+
|
|
11
|
+
describe("normalize", () => {
|
|
12
|
+
it("returns empty for empty input", () => {
|
|
13
|
+
expect(normalize([])).toEqual([]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("normalizes user message (string content)", () => {
|
|
17
|
+
const blocks = normalize([userMsg("fix the bug")]);
|
|
18
|
+
expect(blocks).toEqual([{ kind: "user", text: "fix the bug" }]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("normalizes assistant text message", () => {
|
|
22
|
+
const blocks = normalize([assistantText("done")]);
|
|
23
|
+
expect(blocks).toEqual([{ kind: "assistant", text: "done" }]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("normalizes assistant string content", () => {
|
|
27
|
+
const msg = { ...assistantText("done"), content: "plain text" } as any;
|
|
28
|
+
expect(normalize([msg])).toEqual([{ kind: "assistant", text: "plain text" }]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("splits assistant thinking + text", () => {
|
|
32
|
+
const blocks = normalize([assistantWithThinking("result", "hmm")]);
|
|
33
|
+
expect(blocks).toHaveLength(2);
|
|
34
|
+
expect(blocks[0]).toEqual({
|
|
35
|
+
kind: "thinking", text: "hmm", redacted: false,
|
|
36
|
+
});
|
|
37
|
+
expect(blocks[1]).toEqual({ kind: "assistant", text: "result" });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("normalizes tool call", () => {
|
|
41
|
+
const blocks = normalize([assistantWithToolCall("Read", { path: "a.ts" })]);
|
|
42
|
+
expect(blocks).toEqual([{
|
|
43
|
+
kind: "tool_call", name: "Read", args: { path: "a.ts" },
|
|
44
|
+
}]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("normalizes tool result", () => {
|
|
48
|
+
const blocks = normalize([toolResult("Read", "file contents")]);
|
|
49
|
+
expect(blocks).toEqual([{
|
|
50
|
+
kind: "tool_result", name: "Read",
|
|
51
|
+
text: "file contents", isError: false,
|
|
52
|
+
}]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("normalizes error tool result", () => {
|
|
56
|
+
const blocks = normalize([toolResult("Edit", "not found", true)]);
|
|
57
|
+
expect(blocks[0]).toMatchObject({
|
|
58
|
+
kind: "tool_result", isError: true,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("handles mixed message sequence", () => {
|
|
63
|
+
const blocks = normalize([
|
|
64
|
+
userMsg("fix it"),
|
|
65
|
+
assistantWithToolCall("Read", { path: "x.ts" }),
|
|
66
|
+
toolResult("Read", "code"),
|
|
67
|
+
assistantText("done"),
|
|
68
|
+
]);
|
|
69
|
+
expect(blocks).toHaveLength(4);
|
|
70
|
+
expect(blocks.map((b) => b.kind)).toEqual([
|
|
71
|
+
"user", "tool_call", "tool_result", "assistant",
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("produces image placeholder for user image content", () => {
|
|
76
|
+
const msg = {
|
|
77
|
+
role: "user" as const,
|
|
78
|
+
content: [
|
|
79
|
+
{ type: "text" as const, text: "look at this" },
|
|
80
|
+
{ type: "image" as const, data: "abc", mimeType: "image/png" },
|
|
81
|
+
],
|
|
82
|
+
timestamp: Date.now(),
|
|
83
|
+
};
|
|
84
|
+
const blocks = normalize([msg]);
|
|
85
|
+
expect(blocks).toHaveLength(2);
|
|
86
|
+
expect(blocks[0]).toEqual({ kind: "user", text: "look at this" });
|
|
87
|
+
expect(blocks[1]).toEqual({ kind: "user", text: "[image: image/png]" });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("skips unknown message roles gracefully", () => {
|
|
91
|
+
const weird = { role: "bashExecution", command: "ls", output: "files", exitCode: 0 } as any;
|
|
92
|
+
const blocks = normalize([weird]);
|
|
93
|
+
expect(blocks).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from "bun:test";
|
|
2
|
+
import { buildCompactReport } from "../src/core/report";
|
|
3
|
+
import { prepareSessionSamples, readSourceStat, type SessionSample } from "./support/real-sessions";
|
|
4
|
+
import { loadSessionMessages } from "./support/load-session";
|
|
5
|
+
|
|
6
|
+
let samples: SessionSample[] = [];
|
|
7
|
+
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
samples = await prepareSessionSamples(2);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("real session integration", () => {
|
|
13
|
+
it("compiles copied large sessions without mutating originals", async () => {
|
|
14
|
+
for (const sample of samples) {
|
|
15
|
+
const before = await readSourceStat(sample);
|
|
16
|
+
const loaded = loadSessionMessages(sample.copy);
|
|
17
|
+
const report = buildCompactReport({ messages: loaded.messages });
|
|
18
|
+
const after = await readSourceStat(sample);
|
|
19
|
+
|
|
20
|
+
expect(loaded.messageCount).toBeGreaterThan(0);
|
|
21
|
+
expect(loaded.skippedCount).toBeGreaterThanOrEqual(0);
|
|
22
|
+
expect(report.summary.length).toBeGreaterThan(0);
|
|
23
|
+
expect(report.summary).toContain("[");
|
|
24
|
+
expect(report.before.preview.length).toBeGreaterThan(0);
|
|
25
|
+
expect(report.after.summaryPreview.length).toBeGreaterThan(0);
|
|
26
|
+
expect(report.compression.charsBefore).toBeGreaterThan(0);
|
|
27
|
+
expect(report.recall.probes.length).toBeGreaterThan(0);
|
|
28
|
+
expect(after).toEqual(before);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("uses read-only copied fixtures", () => {
|
|
33
|
+
for (const sample of samples) {
|
|
34
|
+
expect(sample.copy).not.toBe(sample.source);
|
|
35
|
+
expect(sample.copy.includes("pi-vcc-sessions-")).toBe(true);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { renderMessage } from "../src/core/render-entries";
|
|
3
|
+
import type { Message } from "@mariozechner/pi-ai";
|
|
4
|
+
import { userMsg, assistantText, assistantWithToolCall, toolResult } from "./fixtures";
|
|
5
|
+
|
|
6
|
+
describe("renderMessage", () => {
|
|
7
|
+
it("renders user message", () => {
|
|
8
|
+
const r = renderMessage(userMsg("hello"), 0);
|
|
9
|
+
expect(r).toEqual({ index: 0, role: "user", summary: "hello" });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("renders assistant text", () => {
|
|
13
|
+
const r = renderMessage(assistantText("done"), 1);
|
|
14
|
+
expect(r.role).toBe("assistant");
|
|
15
|
+
expect(r.summary).toBe("done");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("renders tool result", () => {
|
|
19
|
+
const r = renderMessage(toolResult("Read", "file contents"), 2);
|
|
20
|
+
expect(r.role).toBe("tool_result");
|
|
21
|
+
expect(r.summary).toContain("[Read]");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("renders tool call arguments with values", () => {
|
|
25
|
+
const r = renderMessage(assistantWithToolCall("Read", { path: "a.ts" }), 2);
|
|
26
|
+
expect(r.summary).toContain("Read(path=a.ts)");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("renders error tool result with prefix", () => {
|
|
30
|
+
const r = renderMessage(toolResult("bash", "not found", true), 3);
|
|
31
|
+
expect(r.summary).toStartWith("ERROR");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("truncates long user text", () => {
|
|
35
|
+
const long = "x".repeat(500);
|
|
36
|
+
const r = renderMessage(userMsg(long), 0);
|
|
37
|
+
expect(r.summary.length).toBeLessThanOrEqual(300);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { buildCompactReport } from "../src/core/report";
|
|
3
|
+
import {
|
|
4
|
+
userMsg,
|
|
5
|
+
assistantText,
|
|
6
|
+
assistantWithToolCall,
|
|
7
|
+
toolResult,
|
|
8
|
+
} from "./fixtures";
|
|
9
|
+
|
|
10
|
+
describe("buildCompactReport", () => {
|
|
11
|
+
it("includes before and after compact metrics", () => {
|
|
12
|
+
const report = buildCompactReport({
|
|
13
|
+
messages: [
|
|
14
|
+
userMsg("Fix login bug in auth.ts"),
|
|
15
|
+
assistantWithToolCall("Read", { path: "auth.ts" }),
|
|
16
|
+
toolResult("Read", "function login() {}"),
|
|
17
|
+
assistantText("Found the root cause in auth.ts.\n1. Fix validation\n2. Run tests"),
|
|
18
|
+
],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(report.summary).toContain("[Session Goal]");
|
|
22
|
+
expect(report.before.messageCount).toBe(4);
|
|
23
|
+
expect(report.before.roleCounts.user).toBe(1);
|
|
24
|
+
expect(report.before.topFiles).toContain("auth.ts");
|
|
25
|
+
expect(report.before.preview).toContain("Fix login bug in auth.ts");
|
|
26
|
+
expect(report.after.sectionCount).toBeGreaterThan(0);
|
|
27
|
+
expect(report.after.summaryPreview).toContain("[Files And Changes]");
|
|
28
|
+
expect(report.compression.charsBefore).toBeGreaterThan(0);
|
|
29
|
+
expect(report.recall.probes.length).toBeGreaterThan(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("marks recall probe coverage for goal and file queries", () => {
|
|
33
|
+
const report = buildCompactReport({
|
|
34
|
+
messages: [
|
|
35
|
+
userMsg("Investigate timeout in api/server.ts"),
|
|
36
|
+
assistantWithToolCall("Read", { path: "api/server.ts" }),
|
|
37
|
+
toolResult("Read", "timeout error in api/server.ts"),
|
|
38
|
+
assistantText("Confirmed timeout error in api/server.ts.\n- Patch retry handling"),
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const goalProbe = report.recall.probes.find((probe) => probe.label === "goal");
|
|
43
|
+
const fileProbe = report.recall.probes.find((probe) => probe.label === "file");
|
|
44
|
+
|
|
45
|
+
expect(goalProbe).toBeDefined();
|
|
46
|
+
expect(goalProbe?.sourceText).toContain("Investigate timeout");
|
|
47
|
+
expect(goalProbe?.summaryMentioned).toBe(true);
|
|
48
|
+
expect(goalProbe?.recallHits).toBeGreaterThan(0);
|
|
49
|
+
expect(fileProbe).toBeDefined();
|
|
50
|
+
expect(fileProbe?.sourceText).toBe("api/server.ts");
|
|
51
|
+
expect(fileProbe?.summaryMentioned).toBe(true);
|
|
52
|
+
expect(fileProbe?.recallHits).toBeGreaterThan(0);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { sanitize } from "../src/core/sanitize";
|
|
3
|
+
|
|
4
|
+
describe("sanitize", () => {
|
|
5
|
+
it("strips ANSI escape codes", () => {
|
|
6
|
+
expect(sanitize("\x1b[31mred\x1b[0m")).toBe("red");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("normalizes CRLF to LF", () => {
|
|
10
|
+
expect(sanitize("a\r\nb\r\n")).toBe("a\nb\n");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("strips bare CR", () => {
|
|
14
|
+
expect(sanitize("a\rb")).toBe("a\nb");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("strips control characters but preserves newlines and tabs", () => {
|
|
18
|
+
expect(sanitize("a\x00b\tc\nd")).toBe("ab\tc\nd");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("passes clean text unchanged", () => {
|
|
22
|
+
expect(sanitize("hello world")).toBe("hello world");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { searchEntries } from "../src/core/search-entries";
|
|
3
|
+
import type { RenderedEntry } from "../src/core/render-entries";
|
|
4
|
+
|
|
5
|
+
const entries: RenderedEntry[] = [
|
|
6
|
+
{ index: 0, role: "user", summary: "Fix login bug" },
|
|
7
|
+
{ index: 1, role: "assistant", summary: "Reading auth.ts" },
|
|
8
|
+
{ index: 2, role: "tool_result", summary: "[Read] code here" },
|
|
9
|
+
{ index: 3, role: "assistant", summary: "Found the root cause" },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
describe("searchEntries", () => {
|
|
13
|
+
it("returns all for empty query", () => {
|
|
14
|
+
expect(searchEntries(entries)).toEqual(entries);
|
|
15
|
+
expect(searchEntries(entries, "")).toEqual(entries);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("filters by single term", () => {
|
|
19
|
+
const r = searchEntries(entries, "login");
|
|
20
|
+
expect(r).toHaveLength(1);
|
|
21
|
+
expect(r[0].index).toBe(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("filters by multiple terms (AND)", () => {
|
|
25
|
+
const r = searchEntries(entries, "root cause");
|
|
26
|
+
expect(r).toHaveLength(1);
|
|
27
|
+
expect(r[0].index).toBe(3);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("returns empty for no match", () => {
|
|
31
|
+
expect(searchEntries(entries, "xyz123")).toEqual([]);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { buildSessionContext, loadEntriesFromFile } from "../../node_modules/@mariozechner/pi-coding-agent/dist/core/session-manager.js";
|
|
2
|
+
import type { Message } from "@mariozechner/pi-ai";
|
|
3
|
+
|
|
4
|
+
export interface LoadedSession {
|
|
5
|
+
messageCount: number;
|
|
6
|
+
skippedCount: number;
|
|
7
|
+
messages: Message[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const loadSessionMessages = (file: string): LoadedSession => {
|
|
11
|
+
const entries = loadEntriesFromFile(file);
|
|
12
|
+
const sessionEntries = entries.filter((entry) => entry.type !== "header");
|
|
13
|
+
const context = buildSessionContext(sessionEntries as any);
|
|
14
|
+
const messages = (context.messages as any[]).filter(
|
|
15
|
+
(msg): msg is Message =>
|
|
16
|
+
msg && typeof msg.role === "string" && "content" in msg,
|
|
17
|
+
);
|
|
18
|
+
return {
|
|
19
|
+
messageCount: messages.length,
|
|
20
|
+
skippedCount: context.messages.length - messages.length,
|
|
21
|
+
messages,
|
|
22
|
+
};
|
|
23
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, copyFile, chmod, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join, basename } from "node:path";
|
|
4
|
+
|
|
5
|
+
const SESSION_ROOT = join(process.env.HOME ?? "", ".pi/agent/sessions");
|
|
6
|
+
|
|
7
|
+
export interface SessionSample {
|
|
8
|
+
source: string;
|
|
9
|
+
copy: string;
|
|
10
|
+
size: number;
|
|
11
|
+
mtimeMs: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const walk = async (dir: string): Promise<string[]> => {
|
|
15
|
+
const names = await readdir(dir, { withFileTypes: true });
|
|
16
|
+
const out: string[] = [];
|
|
17
|
+
for (const name of names) {
|
|
18
|
+
const path = join(dir, name.name);
|
|
19
|
+
if (name.isDirectory()) out.push(...await walk(path));
|
|
20
|
+
else if (name.isFile() && path.endsWith(".jsonl")) out.push(path);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const pickLargest = async (limit: number): Promise<string[]> => {
|
|
26
|
+
const files = await walk(SESSION_ROOT);
|
|
27
|
+
const sized = await Promise.all(
|
|
28
|
+
files.map(async (file) => ({ file, size: (await stat(file)).size })),
|
|
29
|
+
);
|
|
30
|
+
return sized.sort((a, b) => b.size - a.size).slice(0, limit).map((x) => x.file);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const prepareSessionSamples = async (limit = 2): Promise<SessionSample[]> => {
|
|
34
|
+
const selected = await pickLargest(limit);
|
|
35
|
+
const dir = await mkdtemp(join(tmpdir(), "pi-vcc-sessions-"));
|
|
36
|
+
await mkdir(dir, { recursive: true });
|
|
37
|
+
const samples: SessionSample[] = [];
|
|
38
|
+
for (const source of selected) {
|
|
39
|
+
const srcStat = await stat(source);
|
|
40
|
+
const copy = join(dir, basename(source));
|
|
41
|
+
await copyFile(source, copy);
|
|
42
|
+
await chmod(copy, 0o444);
|
|
43
|
+
samples.push({ source, copy, size: srcStat.size, mtimeMs: srcStat.mtimeMs });
|
|
44
|
+
}
|
|
45
|
+
return samples;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const readSourceStat = async (sample: SessionSample) => {
|
|
49
|
+
const s = await stat(sample.source);
|
|
50
|
+
return { size: s.size, mtimeMs: s.mtimeMs };
|
|
51
|
+
};
|