@residue/cli 0.0.3 → 0.0.4
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/CHANGELOG.md +8 -0
- package/README.md +25 -1
- package/dist/index.js +442 -59
- package/package.json +1 -1
- package/src/commands/clear.ts +42 -0
- package/src/commands/search.ts +266 -0
- package/src/commands/sync.ts +85 -1
- package/src/index.ts +19 -0
- package/src/lib/search-text.ts +257 -0
- package/test/lib/search-text.test.ts +366 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight text extractors for search indexing.
|
|
3
|
+
*
|
|
4
|
+
* These are NOT full conversation mappers. They produce a simple text
|
|
5
|
+
* representation optimized for embedding/search -- stripping thinking
|
|
6
|
+
* blocks, token metadata, tool output, signatures, cache data, etc.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
type SearchTextMetadata = {
|
|
10
|
+
sessionId: string;
|
|
11
|
+
agent: string;
|
|
12
|
+
commits: string[];
|
|
13
|
+
branch: string;
|
|
14
|
+
repo: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type SearchLine = {
|
|
18
|
+
role: "human" | "assistant" | "tool";
|
|
19
|
+
text: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build the final search document from metadata + extracted lines.
|
|
24
|
+
*/
|
|
25
|
+
function buildSearchText(opts: {
|
|
26
|
+
metadata: SearchTextMetadata;
|
|
27
|
+
lines: SearchLine[];
|
|
28
|
+
}): string {
|
|
29
|
+
const header = [
|
|
30
|
+
`Session: ${opts.metadata.sessionId}`,
|
|
31
|
+
`Agent: ${opts.metadata.agent}`,
|
|
32
|
+
opts.metadata.commits.length > 0
|
|
33
|
+
? `Commits: ${opts.metadata.commits.join(", ")}`
|
|
34
|
+
: null,
|
|
35
|
+
opts.metadata.branch ? `Branch: ${opts.metadata.branch}` : null,
|
|
36
|
+
opts.metadata.repo ? `Repo: ${opts.metadata.repo}` : null,
|
|
37
|
+
]
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.join("\n");
|
|
40
|
+
|
|
41
|
+
const body = opts.lines
|
|
42
|
+
.map((line) => `[${line.role}] ${line.text}`)
|
|
43
|
+
.join("\n");
|
|
44
|
+
|
|
45
|
+
return `${header}\n\n${body}\n`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// -- Claude Code extractor --------------------------------------------------
|
|
49
|
+
|
|
50
|
+
type ClaudeContentBlock = {
|
|
51
|
+
type: string;
|
|
52
|
+
text?: string;
|
|
53
|
+
thinking?: string;
|
|
54
|
+
name?: string;
|
|
55
|
+
input?: Record<string, unknown>;
|
|
56
|
+
tool_use_id?: string;
|
|
57
|
+
content?: string | ClaudeContentBlock[];
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type ClaudeEntry = {
|
|
61
|
+
type: string;
|
|
62
|
+
isMeta?: boolean;
|
|
63
|
+
isSidechain?: boolean;
|
|
64
|
+
message?: {
|
|
65
|
+
role?: string;
|
|
66
|
+
content?: string | ClaudeContentBlock[];
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function extractClaudeCode(raw: string): SearchLine[] {
|
|
71
|
+
const lines: SearchLine[] = [];
|
|
72
|
+
if (!raw.trim()) return lines;
|
|
73
|
+
|
|
74
|
+
const entries: ClaudeEntry[] = [];
|
|
75
|
+
for (const line of raw.split("\n")) {
|
|
76
|
+
if (!line.trim()) continue;
|
|
77
|
+
try {
|
|
78
|
+
entries.push(JSON.parse(line) as ClaudeEntry);
|
|
79
|
+
} catch {
|
|
80
|
+
// skip malformed
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
if (entry.isMeta || entry.isSidechain) continue;
|
|
86
|
+
|
|
87
|
+
if (entry.type === "user") {
|
|
88
|
+
const content = entry.message?.content;
|
|
89
|
+
if (!content) continue;
|
|
90
|
+
|
|
91
|
+
if (typeof content === "string") {
|
|
92
|
+
const trimmed = content.trim();
|
|
93
|
+
if (trimmed) lines.push({ role: "human", text: trimmed });
|
|
94
|
+
} else if (Array.isArray(content)) {
|
|
95
|
+
// Extract text blocks only, skip tool_result blocks
|
|
96
|
+
const hasToolResult = content.some((b) => b.type === "tool_result");
|
|
97
|
+
if (hasToolResult) continue;
|
|
98
|
+
|
|
99
|
+
const text = content
|
|
100
|
+
.filter((b) => b.type === "text" && b.text)
|
|
101
|
+
.map((b) => b.text!)
|
|
102
|
+
.join("\n")
|
|
103
|
+
.trim();
|
|
104
|
+
if (text) lines.push({ role: "human", text });
|
|
105
|
+
}
|
|
106
|
+
} else if (entry.type === "assistant") {
|
|
107
|
+
const content = entry.message?.content;
|
|
108
|
+
if (!Array.isArray(content)) continue;
|
|
109
|
+
|
|
110
|
+
for (const block of content) {
|
|
111
|
+
if (block.type === "text" && block.text) {
|
|
112
|
+
const trimmed = block.text.trim();
|
|
113
|
+
if (trimmed) lines.push({ role: "assistant", text: trimmed });
|
|
114
|
+
} else if (block.type === "tool_use" && block.name) {
|
|
115
|
+
// Extract tool name + short descriptor from input
|
|
116
|
+
const desc = summarizeToolInput(block.name, block.input);
|
|
117
|
+
lines.push({ role: "tool", text: desc });
|
|
118
|
+
}
|
|
119
|
+
// Skip thinking blocks, signatures, etc.
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Skip system, summary, progress, etc.
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return lines;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// -- Pi extractor ------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
type PiContentBlock = {
|
|
131
|
+
type: string;
|
|
132
|
+
text?: string;
|
|
133
|
+
thinking?: string;
|
|
134
|
+
name?: string;
|
|
135
|
+
arguments?: Record<string, unknown>;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
type PiEntry = {
|
|
139
|
+
type: string;
|
|
140
|
+
message?: {
|
|
141
|
+
role?: string;
|
|
142
|
+
content?: string | PiContentBlock[];
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
function extractPi(raw: string): SearchLine[] {
|
|
147
|
+
const lines: SearchLine[] = [];
|
|
148
|
+
if (!raw.trim()) return lines;
|
|
149
|
+
|
|
150
|
+
const entries: PiEntry[] = [];
|
|
151
|
+
for (const line of raw.split("\n")) {
|
|
152
|
+
if (!line.trim()) continue;
|
|
153
|
+
try {
|
|
154
|
+
entries.push(JSON.parse(line) as PiEntry);
|
|
155
|
+
} catch {
|
|
156
|
+
// skip malformed
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
if (entry.type !== "message") continue;
|
|
162
|
+
const msg = entry.message;
|
|
163
|
+
if (!msg) continue;
|
|
164
|
+
|
|
165
|
+
if (msg.role === "user") {
|
|
166
|
+
const content = msg.content;
|
|
167
|
+
if (!content) continue;
|
|
168
|
+
|
|
169
|
+
if (typeof content === "string") {
|
|
170
|
+
const trimmed = content.trim();
|
|
171
|
+
if (trimmed) lines.push({ role: "human", text: trimmed });
|
|
172
|
+
} else if (Array.isArray(content)) {
|
|
173
|
+
const text = content
|
|
174
|
+
.filter((b) => b.type === "text" && b.text)
|
|
175
|
+
.map((b) => b.text!)
|
|
176
|
+
.join("\n")
|
|
177
|
+
.trim();
|
|
178
|
+
if (text) lines.push({ role: "human", text });
|
|
179
|
+
}
|
|
180
|
+
} else if (msg.role === "assistant") {
|
|
181
|
+
const content = msg.content;
|
|
182
|
+
if (!Array.isArray(content)) {
|
|
183
|
+
if (typeof content === "string" && content.trim()) {
|
|
184
|
+
lines.push({ role: "assistant", text: content.trim() });
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const block of content) {
|
|
190
|
+
if (block.type === "text" && block.text) {
|
|
191
|
+
const trimmed = block.text.trim();
|
|
192
|
+
if (trimmed) lines.push({ role: "assistant", text: trimmed });
|
|
193
|
+
} else if (block.type === "toolCall" && block.name) {
|
|
194
|
+
const desc = summarizeToolInput(block.name, block.arguments);
|
|
195
|
+
lines.push({ role: "tool", text: desc });
|
|
196
|
+
}
|
|
197
|
+
// Skip thinking blocks
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Skip toolResult, session, compaction, etc.
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return lines;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// -- Shared helpers ----------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Produce a short one-line summary of a tool invocation.
|
|
210
|
+
* Extracts file paths and commands when recognizable.
|
|
211
|
+
*/
|
|
212
|
+
function summarizeToolInput(
|
|
213
|
+
name: string,
|
|
214
|
+
input: Record<string, unknown> | undefined,
|
|
215
|
+
): string {
|
|
216
|
+
if (!input) return name;
|
|
217
|
+
|
|
218
|
+
// Common tool patterns
|
|
219
|
+
const path =
|
|
220
|
+
input.path ?? input.file_path ?? input.filePath ?? input.filename;
|
|
221
|
+
if (typeof path === "string") return `${name} ${path}`;
|
|
222
|
+
|
|
223
|
+
const command = input.command ?? input.cmd;
|
|
224
|
+
if (typeof command === "string") {
|
|
225
|
+
// Truncate long commands
|
|
226
|
+
const short =
|
|
227
|
+
command.length > 120 ? command.slice(0, 120) + "..." : command;
|
|
228
|
+
return `${name} ${short}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const query = input.query ?? input.search ?? input.pattern;
|
|
232
|
+
if (typeof query === "string") return `${name} ${query}`;
|
|
233
|
+
|
|
234
|
+
return name;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// -- Public API --------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
type ExtractorName = "claude-code" | "pi";
|
|
240
|
+
|
|
241
|
+
const extractors: Record<ExtractorName, (raw: string) => SearchLine[]> = {
|
|
242
|
+
"claude-code": extractClaudeCode,
|
|
243
|
+
pi: extractPi,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
function getExtractor(agent: string): ((raw: string) => SearchLine[]) | null {
|
|
247
|
+
return extractors[agent as ExtractorName] ?? null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export {
|
|
251
|
+
buildSearchText,
|
|
252
|
+
extractClaudeCode,
|
|
253
|
+
extractPi,
|
|
254
|
+
getExtractor,
|
|
255
|
+
summarizeToolInput,
|
|
256
|
+
};
|
|
257
|
+
export type { SearchLine, SearchTextMetadata, ExtractorName };
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
buildSearchText,
|
|
4
|
+
extractClaudeCode,
|
|
5
|
+
extractPi,
|
|
6
|
+
getExtractor,
|
|
7
|
+
summarizeToolInput,
|
|
8
|
+
} from "@/lib/search-text";
|
|
9
|
+
|
|
10
|
+
describe("search text extractors", () => {
|
|
11
|
+
describe("extractClaudeCode", () => {
|
|
12
|
+
test("extracts human and assistant messages from JSONL", () => {
|
|
13
|
+
const raw = [
|
|
14
|
+
JSON.stringify({
|
|
15
|
+
type: "user",
|
|
16
|
+
message: { role: "user", content: "fix the auth redirect" },
|
|
17
|
+
}),
|
|
18
|
+
JSON.stringify({
|
|
19
|
+
type: "assistant",
|
|
20
|
+
message: {
|
|
21
|
+
role: "assistant",
|
|
22
|
+
content: [{ type: "text", text: "I will update the middleware." }],
|
|
23
|
+
},
|
|
24
|
+
}),
|
|
25
|
+
].join("\n");
|
|
26
|
+
|
|
27
|
+
const lines = extractClaudeCode(raw);
|
|
28
|
+
expect(lines).toHaveLength(2);
|
|
29
|
+
expect(lines[0]).toEqual({
|
|
30
|
+
role: "human",
|
|
31
|
+
text: "fix the auth redirect",
|
|
32
|
+
});
|
|
33
|
+
expect(lines[1]).toEqual({
|
|
34
|
+
role: "assistant",
|
|
35
|
+
text: "I will update the middleware.",
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("extracts tool_use as tool lines with file path", () => {
|
|
40
|
+
const raw = JSON.stringify({
|
|
41
|
+
type: "assistant",
|
|
42
|
+
message: {
|
|
43
|
+
role: "assistant",
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: "tool_use",
|
|
47
|
+
name: "edit",
|
|
48
|
+
input: { path: "src/auth.ts" },
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const lines = extractClaudeCode(raw);
|
|
55
|
+
expect(lines).toHaveLength(1);
|
|
56
|
+
expect(lines[0]).toEqual({ role: "tool", text: "edit src/auth.ts" });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("skips thinking blocks", () => {
|
|
60
|
+
const raw = JSON.stringify({
|
|
61
|
+
type: "assistant",
|
|
62
|
+
message: {
|
|
63
|
+
role: "assistant",
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: "thinking",
|
|
67
|
+
thinking: "Let me think about this...",
|
|
68
|
+
signature: "abc123",
|
|
69
|
+
},
|
|
70
|
+
{ type: "text", text: "Here is my answer." },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const lines = extractClaudeCode(raw);
|
|
76
|
+
expect(lines).toHaveLength(1);
|
|
77
|
+
expect(lines[0].role).toBe("assistant");
|
|
78
|
+
expect(lines[0].text).toBe("Here is my answer.");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("skips meta entries", () => {
|
|
82
|
+
const raw = [
|
|
83
|
+
JSON.stringify({
|
|
84
|
+
type: "user",
|
|
85
|
+
isMeta: true,
|
|
86
|
+
message: { role: "user", content: "auto-injected system stuff" },
|
|
87
|
+
}),
|
|
88
|
+
JSON.stringify({
|
|
89
|
+
type: "user",
|
|
90
|
+
message: { role: "user", content: "real user message" },
|
|
91
|
+
}),
|
|
92
|
+
].join("\n");
|
|
93
|
+
|
|
94
|
+
const lines = extractClaudeCode(raw);
|
|
95
|
+
expect(lines).toHaveLength(1);
|
|
96
|
+
expect(lines[0].text).toBe("real user message");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("skips sidechain entries", () => {
|
|
100
|
+
const raw = JSON.stringify({
|
|
101
|
+
type: "assistant",
|
|
102
|
+
isSidechain: true,
|
|
103
|
+
message: {
|
|
104
|
+
role: "assistant",
|
|
105
|
+
content: [{ type: "text", text: "sidechain response" }],
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const lines = extractClaudeCode(raw);
|
|
110
|
+
expect(lines).toHaveLength(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("skips tool_result user entries", () => {
|
|
114
|
+
const raw = JSON.stringify({
|
|
115
|
+
type: "user",
|
|
116
|
+
message: {
|
|
117
|
+
role: "user",
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: "tool_result",
|
|
121
|
+
tool_use_id: "abc",
|
|
122
|
+
content: "some output",
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const lines = extractClaudeCode(raw);
|
|
129
|
+
expect(lines).toHaveLength(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("skips system and summary entries", () => {
|
|
133
|
+
const raw = [
|
|
134
|
+
JSON.stringify({ type: "system", message: { content: "init" } }),
|
|
135
|
+
JSON.stringify({ type: "summary", summary: "something" }),
|
|
136
|
+
JSON.stringify({ type: "progress" }),
|
|
137
|
+
].join("\n");
|
|
138
|
+
|
|
139
|
+
const lines = extractClaudeCode(raw);
|
|
140
|
+
expect(lines).toHaveLength(0);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("handles empty input", () => {
|
|
144
|
+
expect(extractClaudeCode("")).toHaveLength(0);
|
|
145
|
+
expect(extractClaudeCode(" \n ")).toHaveLength(0);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("handles malformed JSON lines gracefully", () => {
|
|
149
|
+
const raw = [
|
|
150
|
+
"not json at all",
|
|
151
|
+
JSON.stringify({
|
|
152
|
+
type: "user",
|
|
153
|
+
message: { content: "valid line" },
|
|
154
|
+
}),
|
|
155
|
+
"{ broken json",
|
|
156
|
+
].join("\n");
|
|
157
|
+
|
|
158
|
+
const lines = extractClaudeCode(raw);
|
|
159
|
+
expect(lines).toHaveLength(1);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("extractPi", () => {
|
|
164
|
+
test("extracts human and assistant messages", () => {
|
|
165
|
+
const raw = [
|
|
166
|
+
JSON.stringify({
|
|
167
|
+
type: "message",
|
|
168
|
+
message: { role: "user", content: "add a search feature" },
|
|
169
|
+
}),
|
|
170
|
+
JSON.stringify({
|
|
171
|
+
type: "message",
|
|
172
|
+
message: {
|
|
173
|
+
role: "assistant",
|
|
174
|
+
content: [
|
|
175
|
+
{ type: "text", text: "I will add the search endpoint." },
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
].join("\n");
|
|
180
|
+
|
|
181
|
+
const lines = extractPi(raw);
|
|
182
|
+
expect(lines).toHaveLength(2);
|
|
183
|
+
expect(lines[0]).toEqual({
|
|
184
|
+
role: "human",
|
|
185
|
+
text: "add a search feature",
|
|
186
|
+
});
|
|
187
|
+
expect(lines[1]).toEqual({
|
|
188
|
+
role: "assistant",
|
|
189
|
+
text: "I will add the search endpoint.",
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("extracts toolCall blocks", () => {
|
|
194
|
+
const raw = JSON.stringify({
|
|
195
|
+
type: "message",
|
|
196
|
+
message: {
|
|
197
|
+
role: "assistant",
|
|
198
|
+
content: [
|
|
199
|
+
{
|
|
200
|
+
type: "toolCall",
|
|
201
|
+
name: "bash",
|
|
202
|
+
arguments: { command: "git diff --staged" },
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const lines = extractPi(raw);
|
|
209
|
+
expect(lines).toHaveLength(1);
|
|
210
|
+
expect(lines[0]).toEqual({
|
|
211
|
+
role: "tool",
|
|
212
|
+
text: "bash git diff --staged",
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("skips non-message entry types", () => {
|
|
217
|
+
const raw = [
|
|
218
|
+
JSON.stringify({ type: "session", message: {} }),
|
|
219
|
+
JSON.stringify({ type: "compaction" }),
|
|
220
|
+
JSON.stringify({
|
|
221
|
+
type: "message",
|
|
222
|
+
message: { role: "user", content: "hello" },
|
|
223
|
+
}),
|
|
224
|
+
].join("\n");
|
|
225
|
+
|
|
226
|
+
const lines = extractPi(raw);
|
|
227
|
+
expect(lines).toHaveLength(1);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("skips toolResult entries", () => {
|
|
231
|
+
const raw = JSON.stringify({
|
|
232
|
+
type: "message",
|
|
233
|
+
message: {
|
|
234
|
+
role: "toolResult",
|
|
235
|
+
content: "some output text",
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const lines = extractPi(raw);
|
|
240
|
+
expect(lines).toHaveLength(0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("handles assistant string content", () => {
|
|
244
|
+
const raw = JSON.stringify({
|
|
245
|
+
type: "message",
|
|
246
|
+
message: {
|
|
247
|
+
role: "assistant",
|
|
248
|
+
content: "simple string response",
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const lines = extractPi(raw);
|
|
253
|
+
expect(lines).toHaveLength(1);
|
|
254
|
+
expect(lines[0]).toEqual({
|
|
255
|
+
role: "assistant",
|
|
256
|
+
text: "simple string response",
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("handles empty input", () => {
|
|
261
|
+
expect(extractPi("")).toHaveLength(0);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe("summarizeToolInput", () => {
|
|
266
|
+
test("returns tool name when no input", () => {
|
|
267
|
+
expect(summarizeToolInput("read", undefined)).toBe("read");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("extracts path from input", () => {
|
|
271
|
+
expect(summarizeToolInput("edit", { path: "src/index.ts" })).toBe(
|
|
272
|
+
"edit src/index.ts",
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("extracts file_path variant", () => {
|
|
277
|
+
expect(summarizeToolInput("read", { file_path: "README.md" })).toBe(
|
|
278
|
+
"read README.md",
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("extracts command from input", () => {
|
|
283
|
+
expect(summarizeToolInput("bash", { command: "ls -la" })).toBe(
|
|
284
|
+
"bash ls -la",
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("truncates long commands", () => {
|
|
289
|
+
const longCmd = "a".repeat(200);
|
|
290
|
+
const result = summarizeToolInput("bash", { command: longCmd });
|
|
291
|
+
expect(result.length).toBeLessThan(200);
|
|
292
|
+
expect(result).toContain("...");
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("extracts query from input", () => {
|
|
296
|
+
expect(summarizeToolInput("search", { query: "auth middleware" })).toBe(
|
|
297
|
+
"search auth middleware",
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("falls back to tool name for unknown input shape", () => {
|
|
302
|
+
expect(summarizeToolInput("custom_tool", { foo: 42, bar: true })).toBe(
|
|
303
|
+
"custom_tool",
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe("getExtractor", () => {
|
|
309
|
+
test("returns extractor for claude-code", () => {
|
|
310
|
+
expect(getExtractor("claude-code")).not.toBeNull();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("returns extractor for pi", () => {
|
|
314
|
+
expect(getExtractor("pi")).not.toBeNull();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("returns null for unknown agent", () => {
|
|
318
|
+
expect(getExtractor("unknown-agent")).toBeNull();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe("buildSearchText", () => {
|
|
323
|
+
test("builds header + body from metadata and lines", () => {
|
|
324
|
+
const result = buildSearchText({
|
|
325
|
+
metadata: {
|
|
326
|
+
sessionId: "abc-123",
|
|
327
|
+
agent: "claude-code",
|
|
328
|
+
commits: ["abc1234", "def5678"],
|
|
329
|
+
branch: "feature-auth",
|
|
330
|
+
repo: "my-team/my-app",
|
|
331
|
+
},
|
|
332
|
+
lines: [
|
|
333
|
+
{ role: "human", text: "fix the bug" },
|
|
334
|
+
{ role: "assistant", text: "I will fix it." },
|
|
335
|
+
{ role: "tool", text: "edit src/fix.ts" },
|
|
336
|
+
],
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
expect(result).toContain("Session: abc-123");
|
|
340
|
+
expect(result).toContain("Agent: claude-code");
|
|
341
|
+
expect(result).toContain("Commits: abc1234, def5678");
|
|
342
|
+
expect(result).toContain("Branch: feature-auth");
|
|
343
|
+
expect(result).toContain("Repo: my-team/my-app");
|
|
344
|
+
expect(result).toContain("[human] fix the bug");
|
|
345
|
+
expect(result).toContain("[assistant] I will fix it.");
|
|
346
|
+
expect(result).toContain("[tool] edit src/fix.ts");
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("omits empty metadata fields", () => {
|
|
350
|
+
const result = buildSearchText({
|
|
351
|
+
metadata: {
|
|
352
|
+
sessionId: "abc",
|
|
353
|
+
agent: "pi",
|
|
354
|
+
commits: [],
|
|
355
|
+
branch: "",
|
|
356
|
+
repo: "",
|
|
357
|
+
},
|
|
358
|
+
lines: [{ role: "human", text: "hello" }],
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
expect(result).not.toContain("Commits:");
|
|
362
|
+
expect(result).not.toContain("Branch:");
|
|
363
|
+
expect(result).not.toContain("Repo:");
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
});
|