opencodekit 0.16.4 → 0.16.6
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 +1 -1
- package/dist/template/.opencode/AGENTS.md +106 -384
- package/dist/template/.opencode/README.md +170 -104
- package/dist/template/.opencode/agent/build.md +39 -32
- package/dist/template/.opencode/agent/explore.md +2 -0
- package/dist/template/.opencode/agent/review.md +3 -0
- package/dist/template/.opencode/agent/scout.md +22 -11
- package/dist/template/.opencode/command/create.md +164 -106
- package/dist/template/.opencode/command/design.md +5 -1
- package/dist/template/.opencode/command/handoff.md +6 -4
- package/dist/template/.opencode/command/init.md +1 -1
- package/dist/template/.opencode/command/plan.md +26 -23
- package/dist/template/.opencode/command/research.md +13 -6
- package/dist/template/.opencode/command/resume.md +8 -6
- package/dist/template/.opencode/command/ship.md +1 -1
- package/dist/template/.opencode/command/start.md +30 -25
- package/dist/template/.opencode/command/status.md +9 -42
- package/dist/template/.opencode/command/verify.md +11 -11
- package/dist/template/.opencode/memory/README.md +67 -37
- package/dist/template/.opencode/memory/_templates/prd.md +102 -18
- package/dist/template/.opencode/memory/project/gotchas.md +31 -0
- package/dist/template/.opencode/memory.db +0 -0
- package/dist/template/.opencode/memory.db-shm +0 -0
- package/dist/template/.opencode/memory.db-wal +0 -0
- package/dist/template/.opencode/opencode.json +0 -10
- package/dist/template/.opencode/package.json +1 -1
- package/dist/template/.opencode/skill/beads/SKILL.md +164 -380
- package/dist/template/.opencode/skill/beads/references/BOUNDARIES.md +23 -22
- package/dist/template/.opencode/skill/beads/references/DEPENDENCIES.md +23 -29
- package/dist/template/.opencode/skill/beads/references/RESUMABILITY.md +5 -8
- package/dist/template/.opencode/skill/beads/references/WORKFLOWS.md +43 -39
- package/dist/template/.opencode/skill/beads-bridge/SKILL.md +80 -53
- package/dist/template/.opencode/skill/brainstorming/SKILL.md +19 -5
- package/dist/template/.opencode/skill/context-engineering/SKILL.md +30 -63
- package/dist/template/.opencode/skill/context-management/SKILL.md +115 -0
- package/dist/template/.opencode/skill/deep-research/SKILL.md +4 -4
- package/dist/template/.opencode/skill/development-lifecycle/SKILL.md +305 -0
- package/dist/template/.opencode/skill/memory-system/SKILL.md +3 -3
- package/dist/template/.opencode/skill/prd/SKILL.md +47 -122
- package/dist/template/.opencode/skill/prd-task/SKILL.md +48 -4
- package/dist/template/.opencode/skill/prd-task/references/prd-schema.json +120 -24
- package/dist/template/.opencode/skill/swarm-coordination/SKILL.md +79 -61
- package/dist/template/.opencode/skill/tool-priority/SKILL.md +31 -22
- package/dist/template/.opencode/tool/context7.ts +183 -0
- package/dist/template/.opencode/tool/memory-admin.ts +445 -0
- package/dist/template/.opencode/tool/swarm.ts +572 -0
- package/package.json +1 -1
- package/dist/template/.opencode/memory/_templates/spec.md +0 -66
- package/dist/template/.opencode/tool/beads-sync.ts +0 -657
- package/dist/template/.opencode/tool/context7-query-docs.ts +0 -89
- package/dist/template/.opencode/tool/context7-resolve-library-id.ts +0 -113
- package/dist/template/.opencode/tool/memory-maintain.ts +0 -167
- package/dist/template/.opencode/tool/memory-migrate.ts +0 -319
- package/dist/template/.opencode/tool/swarm-delegate.ts +0 -180
- package/dist/template/.opencode/tool/swarm-monitor.ts +0 -388
- package/dist/template/.opencode/tool/swarm-plan.ts +0 -697
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
|
|
3
|
+
// Context7 API v2 - https://context7.com/docs/api-guide
|
|
4
|
+
const CONTEXT7_API = "https://context7.com/api/v2";
|
|
5
|
+
|
|
6
|
+
interface LibraryInfo {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
totalSnippets?: number;
|
|
11
|
+
trustScore?: number;
|
|
12
|
+
benchmarkScore?: number;
|
|
13
|
+
versions?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface SearchResponse {
|
|
17
|
+
results: LibraryInfo[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Consolidated Context7 documentation lookup tool.
|
|
22
|
+
* Operations: resolve (find library ID), query (get docs)
|
|
23
|
+
*/
|
|
24
|
+
export default tool({
|
|
25
|
+
description: `Context7 documentation lookup: resolve library IDs and query docs.
|
|
26
|
+
|
|
27
|
+
Operations:
|
|
28
|
+
- "resolve": Find library ID from name (e.g., "react" → "/reactjs/react.dev")
|
|
29
|
+
- "query": Get documentation for a library topic
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
context7({ operation: "resolve", libraryName: "react" })
|
|
33
|
+
context7({ operation: "query", libraryId: "/reactjs/react.dev", topic: "hooks" })`,
|
|
34
|
+
args: {
|
|
35
|
+
operation: tool.schema
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.default("resolve")
|
|
39
|
+
.describe("Operation: resolve or query"),
|
|
40
|
+
libraryName: tool.schema
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("Library name to resolve (for resolve operation)"),
|
|
44
|
+
libraryId: tool.schema
|
|
45
|
+
.string()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe("Library ID from resolve (for query operation)"),
|
|
48
|
+
topic: tool.schema
|
|
49
|
+
.string()
|
|
50
|
+
.optional()
|
|
51
|
+
.describe("Documentation topic (for query operation)"),
|
|
52
|
+
},
|
|
53
|
+
execute: async (args: {
|
|
54
|
+
operation?: string;
|
|
55
|
+
libraryName?: string;
|
|
56
|
+
libraryId?: string;
|
|
57
|
+
topic?: string;
|
|
58
|
+
}) => {
|
|
59
|
+
const operation = args.operation || "resolve";
|
|
60
|
+
|
|
61
|
+
// Add API key if available
|
|
62
|
+
const apiKey = process.env.CONTEXT7_API_KEY;
|
|
63
|
+
const headers: HeadersInit = {
|
|
64
|
+
Accept: "application/json",
|
|
65
|
+
"User-Agent": "OpenCode/1.0",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (apiKey) {
|
|
69
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ===== RESOLVE =====
|
|
73
|
+
if (operation === "resolve") {
|
|
74
|
+
const { libraryName } = args;
|
|
75
|
+
|
|
76
|
+
if (!libraryName || libraryName.trim() === "") {
|
|
77
|
+
return "Error: libraryName is required for resolve operation";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const url = new URL(`${CONTEXT7_API}/libs/search`);
|
|
82
|
+
url.searchParams.set("libraryName", libraryName);
|
|
83
|
+
url.searchParams.set("query", "documentation");
|
|
84
|
+
|
|
85
|
+
const response = await fetch(url.toString(), { headers });
|
|
86
|
+
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
if (response.status === 401) {
|
|
89
|
+
return `Error: Invalid CONTEXT7_API_KEY. Get a free key at https://context7.com/dashboard`;
|
|
90
|
+
}
|
|
91
|
+
if (response.status === 429) {
|
|
92
|
+
return `Error: Rate limit exceeded. Get a free API key at https://context7.com/dashboard for higher limits.`;
|
|
93
|
+
}
|
|
94
|
+
return `Error: Context7 API returned ${response.status}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const data = (await response.json()) as SearchResponse;
|
|
98
|
+
const libraries = data.results || [];
|
|
99
|
+
|
|
100
|
+
if (!libraries || libraries.length === 0) {
|
|
101
|
+
return `No libraries found matching: ${libraryName}\n\nTry:\n- Different library name\n- Check spelling\n- Use official package name`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const formatted = libraries
|
|
105
|
+
.slice(0, 5)
|
|
106
|
+
.map((lib, i) => {
|
|
107
|
+
const desc = lib.description
|
|
108
|
+
? `\n ${lib.description.slice(0, 100)}...`
|
|
109
|
+
: "";
|
|
110
|
+
const snippets = lib.totalSnippets
|
|
111
|
+
? ` (${lib.totalSnippets} snippets)`
|
|
112
|
+
: "";
|
|
113
|
+
const score = lib.benchmarkScore
|
|
114
|
+
? ` [score: ${lib.benchmarkScore}]`
|
|
115
|
+
: "";
|
|
116
|
+
return `${i + 1}. **${lib.title}** → \`${lib.id}\`${snippets}${score}${desc}`;
|
|
117
|
+
})
|
|
118
|
+
.join("\n\n");
|
|
119
|
+
|
|
120
|
+
return `Found ${libraries.length} libraries matching "${libraryName}":
|
|
121
|
+
|
|
122
|
+
${formatted}
|
|
123
|
+
|
|
124
|
+
**Next step**: Use \`context7({ operation: "query", libraryId: "${libraries[0].id}", topic: "your topic" })\` to fetch documentation.`;
|
|
125
|
+
} catch (error: unknown) {
|
|
126
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
127
|
+
return `Error resolving library: ${message}`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ===== QUERY =====
|
|
132
|
+
if (operation === "query") {
|
|
133
|
+
const { libraryId, topic } = args;
|
|
134
|
+
|
|
135
|
+
if (!libraryId || libraryId.trim() === "") {
|
|
136
|
+
return 'Error: libraryId is required (use operation: "resolve" first)';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!topic || topic.trim() === "") {
|
|
140
|
+
return "Error: topic is required (e.g., 'hooks', 'setup', 'API reference')";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const url = new URL(`${CONTEXT7_API}/context`);
|
|
145
|
+
url.searchParams.set("libraryId", libraryId);
|
|
146
|
+
url.searchParams.set("query", topic);
|
|
147
|
+
|
|
148
|
+
const queryHeaders = { ...headers, Accept: "text/plain" };
|
|
149
|
+
const response = await fetch(url.toString(), {
|
|
150
|
+
headers: queryHeaders,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
if (response.status === 401) {
|
|
155
|
+
return `Error: Invalid CONTEXT7_API_KEY. Get a free key at https://context7.com/dashboard`;
|
|
156
|
+
}
|
|
157
|
+
if (response.status === 404) {
|
|
158
|
+
return `Error: Library not found: ${libraryId}\n\nUse operation: "resolve" first to find the correct ID.`;
|
|
159
|
+
}
|
|
160
|
+
if (response.status === 429) {
|
|
161
|
+
return `Error: Rate limit exceeded. Get a free API key at https://context7.com/dashboard for higher limits.`;
|
|
162
|
+
}
|
|
163
|
+
return `Error: Context7 API returned ${response.status}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const content = await response.text();
|
|
167
|
+
|
|
168
|
+
if (!content || content.trim() === "") {
|
|
169
|
+
return `No documentation found for "${topic}" in ${libraryId}.\n\nTry:\n- Simpler terms (e.g., "useState" instead of "state management")\n- Different topic spelling\n- Broader topics like "API reference" or "getting started"`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return `# Documentation: ${topic} (${libraryId})
|
|
173
|
+
|
|
174
|
+
${content}`;
|
|
175
|
+
} catch (error: unknown) {
|
|
176
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
177
|
+
return `Error querying documentation: ${message}`;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return `Unknown operation: ${operation}. Use: resolve, query`;
|
|
182
|
+
},
|
|
183
|
+
});
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { tool } from "@opencode-ai/plugin";
|
|
4
|
+
import {
|
|
5
|
+
type ConfidenceLevel,
|
|
6
|
+
type ObservationInput,
|
|
7
|
+
type ObservationType,
|
|
8
|
+
archiveOldObservations,
|
|
9
|
+
checkpointWAL,
|
|
10
|
+
getDatabaseSizes,
|
|
11
|
+
getMemoryDB,
|
|
12
|
+
getObservationStats,
|
|
13
|
+
rebuildFTS5,
|
|
14
|
+
runFullMaintenance,
|
|
15
|
+
storeObservation,
|
|
16
|
+
vacuumDatabase,
|
|
17
|
+
} from "../plugin/lib/memory-db.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Consolidated memory administration tool.
|
|
21
|
+
* Operations: status, full, archive, checkpoint, vacuum, migrate
|
|
22
|
+
*/
|
|
23
|
+
export default tool({
|
|
24
|
+
description: `Memory system administration: maintenance and migration.
|
|
25
|
+
|
|
26
|
+
Operations:
|
|
27
|
+
- "status": Storage stats and recommendations
|
|
28
|
+
- "full": Full maintenance cycle (archive + checkpoint + vacuum)
|
|
29
|
+
- "archive": Archive old observations (>90 days default)
|
|
30
|
+
- "checkpoint": Checkpoint WAL file
|
|
31
|
+
- "vacuum": Vacuum database
|
|
32
|
+
- "migrate": Import .opencode/memory/observations/*.md into SQLite
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
memory-admin({ operation: "status" })
|
|
36
|
+
memory-admin({ operation: "migrate", dry_run: true })`,
|
|
37
|
+
args: {
|
|
38
|
+
operation: tool.schema
|
|
39
|
+
.string()
|
|
40
|
+
.optional()
|
|
41
|
+
.default("status")
|
|
42
|
+
.describe(
|
|
43
|
+
"Operation: status, full, archive, checkpoint, vacuum, migrate",
|
|
44
|
+
),
|
|
45
|
+
older_than_days: tool.schema
|
|
46
|
+
.number()
|
|
47
|
+
.optional()
|
|
48
|
+
.default(90)
|
|
49
|
+
.describe("Archive threshold in days (default: 90)"),
|
|
50
|
+
dry_run: tool.schema
|
|
51
|
+
.boolean()
|
|
52
|
+
.optional()
|
|
53
|
+
.default(false)
|
|
54
|
+
.describe("Preview changes without executing"),
|
|
55
|
+
force: tool.schema
|
|
56
|
+
.boolean()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe("Force re-migration of all files"),
|
|
59
|
+
},
|
|
60
|
+
execute: async (args: {
|
|
61
|
+
operation?: string;
|
|
62
|
+
older_than_days?: number;
|
|
63
|
+
dry_run?: boolean;
|
|
64
|
+
force?: boolean;
|
|
65
|
+
}) => {
|
|
66
|
+
const operation = args.operation || "status";
|
|
67
|
+
const olderThanDays = args.older_than_days ?? 90;
|
|
68
|
+
const dryRun = args.dry_run ?? false;
|
|
69
|
+
|
|
70
|
+
// Helper to format bytes
|
|
71
|
+
const formatBytes = (bytes: number): string => {
|
|
72
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
73
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
74
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// ===== MIGRATE =====
|
|
78
|
+
if (operation === "migrate") {
|
|
79
|
+
return await runMigration(args.dry_run, args.force);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const results: string[] = [];
|
|
83
|
+
|
|
84
|
+
// ===== STATUS =====
|
|
85
|
+
if (operation === "status") {
|
|
86
|
+
const sizes = getDatabaseSizes();
|
|
87
|
+
const stats = getObservationStats();
|
|
88
|
+
|
|
89
|
+
results.push("## Memory System Status\n");
|
|
90
|
+
results.push("### Database Size");
|
|
91
|
+
results.push(`- Main DB: ${formatBytes(sizes.mainDb)}`);
|
|
92
|
+
results.push(`- WAL file: ${formatBytes(sizes.wal)}`);
|
|
93
|
+
results.push(`- **Total: ${formatBytes(sizes.total)}**\n`);
|
|
94
|
+
|
|
95
|
+
results.push("### Observations");
|
|
96
|
+
results.push(`- Total: ${stats.total}`);
|
|
97
|
+
for (const [type, count] of Object.entries(stats)) {
|
|
98
|
+
if (type !== "total") {
|
|
99
|
+
results.push(`- ${type}: ${count}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const archiveCandidates = archiveOldObservations({
|
|
104
|
+
olderThanDays,
|
|
105
|
+
includeSuperseded: true,
|
|
106
|
+
dryRun: true,
|
|
107
|
+
});
|
|
108
|
+
results.push(`\n### Maintenance Recommendations`);
|
|
109
|
+
results.push(
|
|
110
|
+
`- Archive candidates (>${olderThanDays} days): ${archiveCandidates}`,
|
|
111
|
+
);
|
|
112
|
+
if (sizes.wal > 1024 * 1024) {
|
|
113
|
+
results.push(`- WAL checkpoint recommended (WAL > 1MB)`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return results.join("\n");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ===== FULL MAINTENANCE =====
|
|
120
|
+
if (operation === "full") {
|
|
121
|
+
results.push(
|
|
122
|
+
dryRun ? "## Full Maintenance (DRY RUN)\n" : "## Full Maintenance\n",
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const stats = runFullMaintenance({
|
|
126
|
+
olderThanDays,
|
|
127
|
+
includeSuperseded: true,
|
|
128
|
+
dryRun,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
results.push(`### Results`);
|
|
132
|
+
results.push(`- Archived observations: ${stats.archived}`);
|
|
133
|
+
results.push(`- WAL checkpointed: ${stats.checkpointed ? "Yes" : "No"}`);
|
|
134
|
+
results.push(`- Database vacuumed: ${stats.vacuumed ? "Yes" : "No"}`);
|
|
135
|
+
results.push(`- Space before: ${formatBytes(stats.dbSizeBefore)}`);
|
|
136
|
+
results.push(`- Space after: ${formatBytes(stats.dbSizeAfter)}`);
|
|
137
|
+
results.push(`- **Freed: ${formatBytes(stats.freedBytes)}**`);
|
|
138
|
+
|
|
139
|
+
return results.join("\n");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ===== ARCHIVE ONLY =====
|
|
143
|
+
if (operation === "archive") {
|
|
144
|
+
const archived = archiveOldObservations({
|
|
145
|
+
olderThanDays,
|
|
146
|
+
includeSuperseded: true,
|
|
147
|
+
dryRun,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (dryRun) {
|
|
151
|
+
return `## Archive Preview\n\nWould archive ${archived} observations older than ${olderThanDays} days.\n\nRun without dry_run to execute.`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return `## Archive Complete\n\nArchived ${archived} observations to observations_archive table.`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ===== CHECKPOINT ONLY =====
|
|
158
|
+
if (operation === "checkpoint") {
|
|
159
|
+
if (dryRun) {
|
|
160
|
+
const sizes = getDatabaseSizes();
|
|
161
|
+
return `## Checkpoint Preview\n\nWAL size: ${formatBytes(sizes.wal)}\n\nRun without dry_run to checkpoint.`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const result = checkpointWAL();
|
|
165
|
+
return `## Checkpoint Complete\n\nCheckpointed: ${result.checkpointed ? "Yes" : "No"}\nWAL pages processed: ${result.walSize}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ===== VACUUM ONLY =====
|
|
169
|
+
if (operation === "vacuum") {
|
|
170
|
+
if (dryRun) {
|
|
171
|
+
const sizes = getDatabaseSizes();
|
|
172
|
+
return `## Vacuum Preview\n\nCurrent size: ${formatBytes(sizes.total)}\n\nRun without dry_run to vacuum.`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const before = getDatabaseSizes();
|
|
176
|
+
const success = vacuumDatabase();
|
|
177
|
+
const after = getDatabaseSizes();
|
|
178
|
+
|
|
179
|
+
return `## Vacuum Complete\n\nSuccess: ${success ? "Yes" : "No"}\nBefore: ${formatBytes(before.total)}\nAfter: ${formatBytes(after.total)}\nFreed: ${formatBytes(before.total - after.total)}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return `Unknown operation: ${operation}. Use: status, full, archive, checkpoint, vacuum, migrate`;
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ===== MIGRATION HELPERS =====
|
|
187
|
+
|
|
188
|
+
interface ParsedObservation {
|
|
189
|
+
type: ObservationType;
|
|
190
|
+
title: string;
|
|
191
|
+
subtitle?: string;
|
|
192
|
+
facts: string[];
|
|
193
|
+
narrative: string;
|
|
194
|
+
concepts: string[];
|
|
195
|
+
files_read: string[];
|
|
196
|
+
files_modified: string[];
|
|
197
|
+
confidence: ConfidenceLevel;
|
|
198
|
+
bead_id?: string;
|
|
199
|
+
supersedes?: string;
|
|
200
|
+
markdown_file: string;
|
|
201
|
+
created_at: string;
|
|
202
|
+
created_at_epoch: number;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function parseYAML(yamlContent: string): Record<string, unknown> {
|
|
206
|
+
const result: Record<string, unknown> = {};
|
|
207
|
+
|
|
208
|
+
for (const line of yamlContent.split("\n")) {
|
|
209
|
+
const match = line.match(/^(\w+):\s*(.*)$/);
|
|
210
|
+
if (match) {
|
|
211
|
+
const [, key, value] = match;
|
|
212
|
+
if (value.startsWith("[")) {
|
|
213
|
+
try {
|
|
214
|
+
result[key] = JSON.parse(value);
|
|
215
|
+
} catch {
|
|
216
|
+
result[key] = value;
|
|
217
|
+
}
|
|
218
|
+
} else if (value === "null" || value === "") {
|
|
219
|
+
result[key] = null;
|
|
220
|
+
} else if (value.startsWith('"') && value.endsWith('"')) {
|
|
221
|
+
result[key] = value.slice(1, -1);
|
|
222
|
+
} else {
|
|
223
|
+
result[key] = value;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function extractFacts(narrative: string): string[] {
|
|
232
|
+
const facts: string[] = [];
|
|
233
|
+
const lines = narrative.split("\n");
|
|
234
|
+
|
|
235
|
+
for (const line of lines) {
|
|
236
|
+
const bulletMatch = line.match(/^[-*]\s+(.+)$/);
|
|
237
|
+
if (bulletMatch) {
|
|
238
|
+
facts.push(bulletMatch[1].trim());
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return facts;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function parseMarkdownObservation(
|
|
246
|
+
content: string,
|
|
247
|
+
filename: string,
|
|
248
|
+
): ParsedObservation {
|
|
249
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
250
|
+
if (!frontmatterMatch) {
|
|
251
|
+
throw new Error(`Invalid format: ${filename} - no YAML frontmatter`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const yaml = parseYAML(frontmatterMatch[1]);
|
|
255
|
+
const narrative = frontmatterMatch[2].trim();
|
|
256
|
+
|
|
257
|
+
const titleMatch = narrative.match(/^#\s+.+?\s+(.+)$/m);
|
|
258
|
+
const title = titleMatch
|
|
259
|
+
? titleMatch[1]
|
|
260
|
+
: (yaml.title as string) || "Untitled";
|
|
261
|
+
|
|
262
|
+
const validTypes: ObservationType[] = [
|
|
263
|
+
"decision",
|
|
264
|
+
"bugfix",
|
|
265
|
+
"feature",
|
|
266
|
+
"pattern",
|
|
267
|
+
"discovery",
|
|
268
|
+
"learning",
|
|
269
|
+
"warning",
|
|
270
|
+
];
|
|
271
|
+
const type = (yaml.type as string)?.toLowerCase() as ObservationType;
|
|
272
|
+
if (!validTypes.includes(type)) {
|
|
273
|
+
throw new Error(`Invalid type '${yaml.type}' in ${filename}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const validConfidence: ConfidenceLevel[] = ["high", "medium", "low"];
|
|
277
|
+
const confidence = ((yaml.confidence as string)?.toLowerCase() ||
|
|
278
|
+
"high") as ConfidenceLevel;
|
|
279
|
+
if (!validConfidence.includes(confidence)) {
|
|
280
|
+
throw new Error(`Invalid confidence '${yaml.confidence}' in ${filename}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const createdStr = yaml.created as string;
|
|
284
|
+
if (!createdStr) {
|
|
285
|
+
throw new Error(`Missing created date in ${filename}`);
|
|
286
|
+
}
|
|
287
|
+
const createdAt = new Date(createdStr);
|
|
288
|
+
if (Number.isNaN(createdAt.getTime())) {
|
|
289
|
+
throw new Error(`Invalid created date '${createdStr}' in ${filename}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const facts = extractFacts(narrative);
|
|
293
|
+
const files = (yaml.files as string[]) || [];
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
type,
|
|
297
|
+
title,
|
|
298
|
+
subtitle: yaml.subtitle as string | undefined,
|
|
299
|
+
facts,
|
|
300
|
+
narrative,
|
|
301
|
+
concepts: (yaml.concepts as string[]) || [],
|
|
302
|
+
files_read: files,
|
|
303
|
+
files_modified: files,
|
|
304
|
+
confidence,
|
|
305
|
+
bead_id: yaml.bead_id as string | undefined,
|
|
306
|
+
supersedes: yaml.supersedes as string | undefined,
|
|
307
|
+
markdown_file: filename,
|
|
308
|
+
created_at: createdAt.toISOString(),
|
|
309
|
+
created_at_epoch: createdAt.getTime(),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function runMigration(
|
|
314
|
+
dryRun?: boolean,
|
|
315
|
+
force?: boolean,
|
|
316
|
+
): Promise<string> {
|
|
317
|
+
const obsDir = path.join(process.cwd(), ".opencode/memory/observations");
|
|
318
|
+
const migrationMarker = path.join(obsDir, ".migrated");
|
|
319
|
+
|
|
320
|
+
if (!force) {
|
|
321
|
+
try {
|
|
322
|
+
await fs.access(migrationMarker);
|
|
323
|
+
const markerContent = await fs.readFile(migrationMarker, "utf-8");
|
|
324
|
+
return `Migration already complete.\n\n${markerContent}\n\nUse force: true to re-migrate.`;
|
|
325
|
+
} catch {
|
|
326
|
+
// No marker, proceed with migration
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let files: string[];
|
|
331
|
+
try {
|
|
332
|
+
const entries = await fs.readdir(obsDir);
|
|
333
|
+
files = entries.filter((f) => f.endsWith(".md") && !f.startsWith("."));
|
|
334
|
+
} catch {
|
|
335
|
+
return "No observations directory found at .opencode/memory/observations/";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (files.length === 0) {
|
|
339
|
+
return "No markdown files found to migrate.";
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const db = getMemoryDB();
|
|
343
|
+
const results: {
|
|
344
|
+
migrated: string[];
|
|
345
|
+
skipped: string[];
|
|
346
|
+
errors: { file: string; error: string }[];
|
|
347
|
+
} = {
|
|
348
|
+
migrated: [],
|
|
349
|
+
skipped: [],
|
|
350
|
+
errors: [],
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
files.sort();
|
|
354
|
+
|
|
355
|
+
for (const file of files) {
|
|
356
|
+
try {
|
|
357
|
+
const content = await fs.readFile(path.join(obsDir, file), "utf-8");
|
|
358
|
+
const parsed = parseMarkdownObservation(content, file);
|
|
359
|
+
|
|
360
|
+
if (dryRun) {
|
|
361
|
+
results.migrated.push(
|
|
362
|
+
`${file} → ${parsed.type}: ${parsed.title.substring(0, 50)}`,
|
|
363
|
+
);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const existing = db
|
|
368
|
+
.query("SELECT id FROM observations WHERE markdown_file = ?")
|
|
369
|
+
.get(file);
|
|
370
|
+
|
|
371
|
+
if (existing && !force) {
|
|
372
|
+
results.skipped.push(file);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const input: ObservationInput = {
|
|
377
|
+
type: parsed.type,
|
|
378
|
+
title: parsed.title,
|
|
379
|
+
subtitle: parsed.subtitle,
|
|
380
|
+
facts: parsed.facts,
|
|
381
|
+
narrative: parsed.narrative,
|
|
382
|
+
concepts: parsed.concepts,
|
|
383
|
+
files_read: parsed.files_read,
|
|
384
|
+
files_modified: parsed.files_modified,
|
|
385
|
+
confidence: parsed.confidence,
|
|
386
|
+
bead_id: parsed.bead_id,
|
|
387
|
+
markdown_file: file,
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
storeObservation(input);
|
|
391
|
+
results.migrated.push(file);
|
|
392
|
+
} catch (e) {
|
|
393
|
+
results.errors.push({
|
|
394
|
+
file,
|
|
395
|
+
error: e instanceof Error ? e.message : String(e),
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!dryRun && results.migrated.length > 0) {
|
|
401
|
+
try {
|
|
402
|
+
rebuildFTS5();
|
|
403
|
+
} catch {
|
|
404
|
+
// FTS5 rebuild failed, continue
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (!dryRun) {
|
|
409
|
+
const markerContent = [
|
|
410
|
+
`Migrated ${results.migrated.length} observations on ${new Date().toISOString()}`,
|
|
411
|
+
`Skipped: ${results.skipped.length}`,
|
|
412
|
+
`Errors: ${results.errors.length}`,
|
|
413
|
+
].join("\n");
|
|
414
|
+
await fs.writeFile(migrationMarker, markerContent, "utf-8");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
let output = dryRun
|
|
418
|
+
? "# Migration Preview (Dry Run)\n\n"
|
|
419
|
+
: "# Migration Complete\n\n";
|
|
420
|
+
|
|
421
|
+
output += `**Total files**: ${files.length}\n`;
|
|
422
|
+
output += `**Migrated**: ${results.migrated.length}\n`;
|
|
423
|
+
output += `**Skipped**: ${results.skipped.length}\n`;
|
|
424
|
+
output += `**Errors**: ${results.errors.length}\n\n`;
|
|
425
|
+
|
|
426
|
+
if (results.errors.length > 0) {
|
|
427
|
+
output += "## Errors\n\n";
|
|
428
|
+
for (const { file, error } of results.errors) {
|
|
429
|
+
output += `- **${file}**: ${error}\n`;
|
|
430
|
+
}
|
|
431
|
+
output += "\n";
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (dryRun && results.migrated.length > 0) {
|
|
435
|
+
output += "## Files to migrate\n\n";
|
|
436
|
+
for (const item of results.migrated.slice(0, 20)) {
|
|
437
|
+
output += `- ${item}\n`;
|
|
438
|
+
}
|
|
439
|
+
if (results.migrated.length > 20) {
|
|
440
|
+
output += `- ... and ${results.migrated.length - 20} more\n`;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return output;
|
|
445
|
+
}
|