opencodekit 0.12.0 → 0.12.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/index.js +43 -15
- package/dist/template/.opencode/AGENTS.md +10 -0
- package/dist/template/.opencode/command/research.md +4 -0
- package/dist/template/.opencode/command/resume.md +11 -2
- package/dist/template/.opencode/command/status.md +3 -0
- package/dist/template/.opencode/command/triage.md +4 -0
- package/dist/template/.opencode/opencode.json +523 -480
- package/dist/template/.opencode/plugin/sessions.ts +295 -38
- package/package.json +1 -1
|
@@ -12,9 +12,35 @@ import { readFile } from "fs/promises";
|
|
|
12
12
|
* OpenCode Storage Structure:
|
|
13
13
|
* - Sessions: ~/.local/share/opencode/storage/session/<project_hash>/<session_id>.json
|
|
14
14
|
* - Messages: ~/.local/share/opencode/storage/message/<session_id>/msg_*.json
|
|
15
|
+
* - Parts: ~/.local/share/opencode/storage/part/<message_id>/*.json
|
|
15
16
|
* - Diffs: ~/.local/share/opencode/storage/session_diff/<session_id>.json
|
|
16
17
|
*/
|
|
17
18
|
|
|
19
|
+
// Constants
|
|
20
|
+
const MAX_SESSIONS_TO_SCAN = 50;
|
|
21
|
+
const SEARCH_TIMEOUT_MS = 60000;
|
|
22
|
+
const LIST_TIMEOUT_MS = 30000;
|
|
23
|
+
const READ_TIMEOUT_MS = 30000;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Wrap a promise with a timeout
|
|
27
|
+
*/
|
|
28
|
+
function withTimeout<T>(
|
|
29
|
+
promise: Promise<T>,
|
|
30
|
+
ms: number,
|
|
31
|
+
operation: string,
|
|
32
|
+
): Promise<T> {
|
|
33
|
+
return Promise.race([
|
|
34
|
+
promise,
|
|
35
|
+
new Promise<T>((_, reject) =>
|
|
36
|
+
setTimeout(
|
|
37
|
+
() => reject(new Error(`${operation} timed out after ${ms}ms`)),
|
|
38
|
+
ms,
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
|
|
18
44
|
interface SessionMetadata {
|
|
19
45
|
id: string;
|
|
20
46
|
title?: string;
|
|
@@ -32,6 +58,25 @@ interface SessionInfo extends SessionMetadata {
|
|
|
32
58
|
fileCount?: number;
|
|
33
59
|
}
|
|
34
60
|
|
|
61
|
+
interface MessagePart {
|
|
62
|
+
id: string;
|
|
63
|
+
type: string;
|
|
64
|
+
text?: string;
|
|
65
|
+
thinking?: string;
|
|
66
|
+
tool?: string;
|
|
67
|
+
input?: unknown;
|
|
68
|
+
output?: unknown;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface SearchResult {
|
|
72
|
+
session_id: string;
|
|
73
|
+
message_id: string;
|
|
74
|
+
role: string;
|
|
75
|
+
excerpt: string;
|
|
76
|
+
match_count: number;
|
|
77
|
+
timestamp?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
35
80
|
function parseDate(dateStr: string): Date | null {
|
|
36
81
|
const today = new Date();
|
|
37
82
|
today.setHours(0, 0, 0, 0);
|
|
@@ -156,6 +201,109 @@ export const SessionsPlugin: Plugin = async ({ client, directory }) => {
|
|
|
156
201
|
return sessions.slice(0, limit);
|
|
157
202
|
}
|
|
158
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Read message parts from part storage
|
|
206
|
+
*/
|
|
207
|
+
async function readMessageParts(messageId: string): Promise<MessagePart[]> {
|
|
208
|
+
const partDir = join(storageDir, "part", messageId);
|
|
209
|
+
if (!existsSync(partDir)) return [];
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const partFiles = readdirSync(partDir).filter((f) => f.endsWith(".json"));
|
|
213
|
+
const parts = await Promise.all(
|
|
214
|
+
partFiles.map(async (file) => {
|
|
215
|
+
try {
|
|
216
|
+
const content = await readFile(join(partDir, file), "utf-8");
|
|
217
|
+
return JSON.parse(content) as MessagePart;
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
return parts.filter((p): p is MessagePart => p !== null);
|
|
224
|
+
} catch {
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Extract text content from message parts
|
|
231
|
+
*/
|
|
232
|
+
function extractTextFromParts(parts: MessagePart[]): string {
|
|
233
|
+
return parts
|
|
234
|
+
.filter((p) => p.type === "text" && p.text)
|
|
235
|
+
.map((p) => p.text!)
|
|
236
|
+
.join("\n");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Search within a single session
|
|
241
|
+
*/
|
|
242
|
+
async function searchInSession(
|
|
243
|
+
sessionId: string,
|
|
244
|
+
query: string,
|
|
245
|
+
caseSensitive: boolean,
|
|
246
|
+
maxResults: number,
|
|
247
|
+
): Promise<SearchResult[]> {
|
|
248
|
+
const messageDir = join(storageDir, "message", sessionId);
|
|
249
|
+
if (!existsSync(messageDir)) return [];
|
|
250
|
+
|
|
251
|
+
const results: SearchResult[] = [];
|
|
252
|
+
const searchQuery = caseSensitive ? query : query.toLowerCase();
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const messageFiles = readdirSync(messageDir).filter((f) =>
|
|
256
|
+
f.endsWith(".json"),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
for (const file of messageFiles) {
|
|
260
|
+
if (results.length >= maxResults) break;
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const content = await readFile(join(messageDir, file), "utf-8");
|
|
264
|
+
const message = JSON.parse(content);
|
|
265
|
+
const messageId = message.id || file.replace(".json", "");
|
|
266
|
+
|
|
267
|
+
// Read parts for this message
|
|
268
|
+
const parts = await readMessageParts(messageId);
|
|
269
|
+
const textContent = extractTextFromParts(parts);
|
|
270
|
+
const searchText = caseSensitive
|
|
271
|
+
? textContent
|
|
272
|
+
: textContent.toLowerCase();
|
|
273
|
+
|
|
274
|
+
if (searchText.includes(searchQuery)) {
|
|
275
|
+
// Count matches
|
|
276
|
+
const matchCount = searchText.split(searchQuery).length - 1;
|
|
277
|
+
|
|
278
|
+
// Extract excerpt (50 chars around first match)
|
|
279
|
+
const matchIndex = searchText.indexOf(searchQuery);
|
|
280
|
+
const start = Math.max(0, matchIndex - 25);
|
|
281
|
+
const end = Math.min(
|
|
282
|
+
textContent.length,
|
|
283
|
+
matchIndex + query.length + 25,
|
|
284
|
+
);
|
|
285
|
+
const excerpt = textContent.substring(start, end);
|
|
286
|
+
|
|
287
|
+
results.push({
|
|
288
|
+
session_id: sessionId,
|
|
289
|
+
message_id: messageId,
|
|
290
|
+
role: message.role || "unknown",
|
|
291
|
+
excerpt: `...${excerpt}...`,
|
|
292
|
+
match_count: matchCount,
|
|
293
|
+
timestamp: message.time?.created,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
} catch {
|
|
297
|
+
// Skip corrupted message files
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
// Skip inaccessible session directories
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return results;
|
|
305
|
+
}
|
|
306
|
+
|
|
159
307
|
return {
|
|
160
308
|
tool: {
|
|
161
309
|
summarize_session: tool({
|
|
@@ -212,48 +360,146 @@ export const SessionsPlugin: Plugin = async ({ client, directory }) => {
|
|
|
212
360
|
? undefined
|
|
213
361
|
: args.project;
|
|
214
362
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
363
|
+
try {
|
|
364
|
+
const sessions = await withTimeout(
|
|
365
|
+
listSessions(projectFilter, args.since, args.limit || 20),
|
|
366
|
+
LIST_TIMEOUT_MS,
|
|
367
|
+
"List sessions",
|
|
368
|
+
);
|
|
220
369
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
370
|
+
if (sessions.length === 0) {
|
|
371
|
+
return "No sessions found matching filters.";
|
|
372
|
+
}
|
|
224
373
|
|
|
225
|
-
|
|
374
|
+
let output = `# Sessions (${sessions.length})\n\n`;
|
|
226
375
|
|
|
227
|
-
|
|
228
|
-
|
|
376
|
+
if (projectFilter && projectFilter !== "all") {
|
|
377
|
+
output += `**Project:** ${projectFilter}\n`;
|
|
378
|
+
}
|
|
379
|
+
if (args.since) {
|
|
380
|
+
output += `**Since:** ${args.since}\n`;
|
|
381
|
+
}
|
|
382
|
+
output += "\n";
|
|
383
|
+
|
|
384
|
+
sessions.forEach((s, i) => {
|
|
385
|
+
const date = s.time?.created
|
|
386
|
+
? new Date(s.time.created).toLocaleString()
|
|
387
|
+
: "Unknown";
|
|
388
|
+
const title = s.title || s.id;
|
|
389
|
+
output += `${i + 1}. **${s.id}**\n`;
|
|
390
|
+
output += ` Title: ${title}\n`;
|
|
391
|
+
output += ` Date: ${date}\n`;
|
|
392
|
+
output += ` Messages: ${s.messageCount || 0}, Files: ${s.fileCount || 0}\n`;
|
|
393
|
+
if (s.summary && typeof s.summary === "object") {
|
|
394
|
+
const sumObj = s.summary as any;
|
|
395
|
+
if (
|
|
396
|
+
sumObj.additions !== undefined ||
|
|
397
|
+
sumObj.deletions !== undefined
|
|
398
|
+
) {
|
|
399
|
+
output += ` Changes: +${sumObj.additions || 0}/-${sumObj.deletions || 0}\n`;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
output += "\n";
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
return output;
|
|
406
|
+
} catch (error) {
|
|
407
|
+
return `Error listing sessions: ${error instanceof Error ? error.message : String(error)}`;
|
|
229
408
|
}
|
|
230
|
-
|
|
231
|
-
|
|
409
|
+
},
|
|
410
|
+
}),
|
|
411
|
+
|
|
412
|
+
search_session: tool({
|
|
413
|
+
description:
|
|
414
|
+
"Full-text search across session messages. Returns matching messages with excerpts.",
|
|
415
|
+
args: {
|
|
416
|
+
query: tool.schema.string().describe("Search query"),
|
|
417
|
+
session_id: tool.schema
|
|
418
|
+
.string()
|
|
419
|
+
.optional()
|
|
420
|
+
.describe("Limit search to specific session ID"),
|
|
421
|
+
case_sensitive: tool.schema
|
|
422
|
+
.boolean()
|
|
423
|
+
.optional()
|
|
424
|
+
.describe("Case sensitive search (default: false)"),
|
|
425
|
+
limit: tool.schema
|
|
426
|
+
.number()
|
|
427
|
+
.optional()
|
|
428
|
+
.describe("Max results to return (default: 20)"),
|
|
429
|
+
},
|
|
430
|
+
async execute(args) {
|
|
431
|
+
if (!args.query || args.query.trim().length === 0) {
|
|
432
|
+
return "Error: Search query cannot be empty.";
|
|
232
433
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
434
|
+
|
|
435
|
+
const caseSensitive = args.case_sensitive ?? false;
|
|
436
|
+
const limit = args.limit ?? 20;
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
const searchOperation = async () => {
|
|
440
|
+
const allResults: SearchResult[] = [];
|
|
441
|
+
|
|
442
|
+
if (args.session_id) {
|
|
443
|
+
// Search specific session
|
|
444
|
+
const results = await searchInSession(
|
|
445
|
+
args.session_id,
|
|
446
|
+
args.query,
|
|
447
|
+
caseSensitive,
|
|
448
|
+
limit,
|
|
449
|
+
);
|
|
450
|
+
allResults.push(...results);
|
|
451
|
+
} else {
|
|
452
|
+
// Search across sessions (limit to MAX_SESSIONS_TO_SCAN)
|
|
453
|
+
const sessions = await listSessions(
|
|
454
|
+
directory,
|
|
455
|
+
undefined,
|
|
456
|
+
MAX_SESSIONS_TO_SCAN,
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
for (const session of sessions) {
|
|
460
|
+
if (allResults.length >= limit) break;
|
|
461
|
+
|
|
462
|
+
const results = await searchInSession(
|
|
463
|
+
session.id,
|
|
464
|
+
args.query,
|
|
465
|
+
caseSensitive,
|
|
466
|
+
limit - allResults.length,
|
|
467
|
+
);
|
|
468
|
+
allResults.push(...results);
|
|
469
|
+
}
|
|
251
470
|
}
|
|
471
|
+
|
|
472
|
+
return allResults;
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const results = await withTimeout(
|
|
476
|
+
searchOperation(),
|
|
477
|
+
SEARCH_TIMEOUT_MS,
|
|
478
|
+
"Session search",
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
if (results.length === 0) {
|
|
482
|
+
return `No matches found for "${args.query}"${args.session_id ? ` in session ${args.session_id}` : ""}.`;
|
|
252
483
|
}
|
|
253
|
-
output += "\n";
|
|
254
|
-
});
|
|
255
484
|
|
|
256
|
-
|
|
485
|
+
let output = `# Search Results: "${args.query}"\n\n`;
|
|
486
|
+
output += `Found ${results.length} match${results.length === 1 ? "" : "es"}`;
|
|
487
|
+
if (args.session_id) {
|
|
488
|
+
output += ` in session ${args.session_id}`;
|
|
489
|
+
}
|
|
490
|
+
output += "\n\n";
|
|
491
|
+
|
|
492
|
+
results.forEach((r, i) => {
|
|
493
|
+
output += `${i + 1}. **${r.session_id}** (${r.role})\n`;
|
|
494
|
+
output += ` Message: ${r.message_id}\n`;
|
|
495
|
+
output += ` Matches: ${r.match_count}\n`;
|
|
496
|
+
output += ` Excerpt: ${r.excerpt}\n\n`;
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
return output;
|
|
500
|
+
} catch (error) {
|
|
501
|
+
return `Error searching sessions: ${error instanceof Error ? error.message : String(error)}`;
|
|
502
|
+
}
|
|
257
503
|
},
|
|
258
504
|
}),
|
|
259
505
|
|
|
@@ -430,10 +676,15 @@ export const SessionsPlugin: Plugin = async ({ client, directory }) => {
|
|
|
430
676
|
} else {
|
|
431
677
|
// Full summary
|
|
432
678
|
summary += `## User Messages\n\n`;
|
|
433
|
-
userMessages.
|
|
434
|
-
const
|
|
679
|
+
for (let i = 0; i < Math.min(userMessages.length, 5); i++) {
|
|
680
|
+
const m = userMessages[i];
|
|
681
|
+
const messageId = m.id || `msg_${i}`;
|
|
682
|
+
const parts = await readMessageParts(messageId);
|
|
683
|
+
const partText = extractTextFromParts(parts);
|
|
684
|
+
const content =
|
|
685
|
+
partText || m.summary?.title || m.content || "[No content]";
|
|
435
686
|
summary += `${i + 1}. ${content.substring(0, 200)}\n`;
|
|
436
|
-
}
|
|
687
|
+
}
|
|
437
688
|
|
|
438
689
|
if (diffs.length > 0) {
|
|
439
690
|
summary += `\n## Files Modified\n\n`;
|
|
@@ -445,8 +696,14 @@ export const SessionsPlugin: Plugin = async ({ client, directory }) => {
|
|
|
445
696
|
// Last assistant message
|
|
446
697
|
if (assistantMessages.length > 0) {
|
|
447
698
|
const last = assistantMessages[assistantMessages.length - 1];
|
|
699
|
+
const lastMessageId = last.id || `msg_last`;
|
|
700
|
+
const lastParts = await readMessageParts(lastMessageId);
|
|
701
|
+
const lastPartText = extractTextFromParts(lastParts);
|
|
448
702
|
const lastContent =
|
|
449
|
-
|
|
703
|
+
lastPartText ||
|
|
704
|
+
last.summary?.body ||
|
|
705
|
+
last.content ||
|
|
706
|
+
"[No content]";
|
|
450
707
|
summary += `\n## Last Assistant Response\n\n${lastContent.substring(0, 500)}\n`;
|
|
451
708
|
}
|
|
452
709
|
}
|