opencodekit 0.11.1 → 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 +12 -2
- package/dist/template/.opencode/agent/build.md +20 -20
- package/dist/template/.opencode/agent/explore.md +13 -13
- package/dist/template/.opencode/agent/planner.md +20 -20
- package/dist/template/.opencode/agent/review.md +18 -18
- package/dist/template/.opencode/agent/rush.md +20 -20
- package/dist/template/.opencode/agent/scout.md +16 -16
- package/dist/template/.opencode/agent/vision.md +19 -19
- package/dist/template/.opencode/command/create.md +1 -1
- package/dist/template/.opencode/command/import-plan.md +1 -1
- package/dist/template/.opencode/command/issue.md +5 -2
- package/dist/template/.opencode/command/plan.md +1 -1
- package/dist/template/.opencode/command/research.md +4 -0
- package/dist/template/.opencode/command/resume.md +11 -2
- package/dist/template/.opencode/command/revert-feature.md +1 -1
- package/dist/template/.opencode/command/status.md +3 -0
- package/dist/template/.opencode/command/triage.md +15 -12
- package/dist/template/.opencode/opencode.json +18 -10
- package/dist/template/.opencode/package.json +1 -1
- package/dist/template/.opencode/plugin/beads.ts +857 -270
- package/dist/template/.opencode/plugin/sessions.ts +295 -38
- package/dist/template/.opencode/skill/beads/SKILL.md +6 -18
- package/dist/template/.opencode/skill/beads/references/DEPENDENCIES.md +3 -3
- package/dist/template/.opencode/skill/beads/references/WORKFLOWS.md +1 -1
- 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
|
}
|
|
@@ -129,10 +129,6 @@ bd_ls({ status: "all", limit: 20 });
|
|
|
129
129
|
// See ready work (unblocked tasks)
|
|
130
130
|
bd_ready();
|
|
131
131
|
// Returns tasks where all dependencies are closed
|
|
132
|
-
|
|
133
|
-
// See execution plan with parallel tracks
|
|
134
|
-
bd_plan();
|
|
135
|
-
// Groups ready tasks by priority
|
|
136
132
|
```
|
|
137
133
|
|
|
138
134
|
## Session Start Protocol
|
|
@@ -354,29 +350,21 @@ bd_status({ include_agents: true });
|
|
|
354
350
|
|
|
355
351
|
Shows ready tasks, in-progress count, active locks, agent info.
|
|
356
352
|
|
|
357
|
-
### Find
|
|
358
|
-
|
|
359
|
-
```typescript
|
|
360
|
-
bd_insights();
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
Returns blocked tasks, high-priority keystones, dependency analysis.
|
|
364
|
-
|
|
365
|
-
### Priority Recommendations
|
|
353
|
+
### Find Blocked Work
|
|
366
354
|
|
|
367
355
|
```typescript
|
|
368
|
-
|
|
356
|
+
bd_blocked();
|
|
369
357
|
```
|
|
370
358
|
|
|
371
|
-
|
|
359
|
+
Returns tasks that have unresolved dependencies.
|
|
372
360
|
|
|
373
|
-
###
|
|
361
|
+
### View Dependencies
|
|
374
362
|
|
|
375
363
|
```typescript
|
|
376
|
-
|
|
364
|
+
bd_dep({ action: "tree", child: "<task-id>", type: "blocks" });
|
|
377
365
|
```
|
|
378
366
|
|
|
379
|
-
|
|
367
|
+
Shows dependency tree for a specific task.
|
|
380
368
|
|
|
381
369
|
## Git Sync
|
|
382
370
|
|
|
@@ -4,7 +4,7 @@ Beads supports task dependencies for ordering work.
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
Dependencies affect what work is "ready" - tasks with unmet dependencies won't appear in `bd_claim()`
|
|
7
|
+
Dependencies affect what work is "ready" - tasks with unmet dependencies won't appear in `bd_claim()` results.
|
|
8
8
|
|
|
9
9
|
## Creating Dependencies
|
|
10
10
|
|
|
@@ -125,6 +125,6 @@ bd_add({ title: "API", deps: ["task:tests"] }) // API depends on tests?
|
|
|
125
125
|
bd_show({ id: "task-abc" });
|
|
126
126
|
// Shows what blocks this task and what this task blocks
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
// Shows
|
|
128
|
+
bd_blocked();
|
|
129
|
+
// Shows all blocked tasks across project
|
|
130
130
|
```
|
|
@@ -138,7 +138,7 @@ Session Start with in_progress:
|
|
|
138
138
|
```
|
|
139
139
|
Unblocking:
|
|
140
140
|
- [ ] bd_ls({ status: "open" }) to see all tasks
|
|
141
|
-
- [ ]
|
|
141
|
+
- [ ] bd_blocked() to find blocked tasks
|
|
142
142
|
- [ ] Identify blocker tasks
|
|
143
143
|
- [ ] Work on blockers first
|
|
144
144
|
- [ ] Closing blocker unblocks dependent work
|