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.
Files changed (56) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/template/.opencode/AGENTS.md +106 -384
  3. package/dist/template/.opencode/README.md +170 -104
  4. package/dist/template/.opencode/agent/build.md +39 -32
  5. package/dist/template/.opencode/agent/explore.md +2 -0
  6. package/dist/template/.opencode/agent/review.md +3 -0
  7. package/dist/template/.opencode/agent/scout.md +22 -11
  8. package/dist/template/.opencode/command/create.md +164 -106
  9. package/dist/template/.opencode/command/design.md +5 -1
  10. package/dist/template/.opencode/command/handoff.md +6 -4
  11. package/dist/template/.opencode/command/init.md +1 -1
  12. package/dist/template/.opencode/command/plan.md +26 -23
  13. package/dist/template/.opencode/command/research.md +13 -6
  14. package/dist/template/.opencode/command/resume.md +8 -6
  15. package/dist/template/.opencode/command/ship.md +1 -1
  16. package/dist/template/.opencode/command/start.md +30 -25
  17. package/dist/template/.opencode/command/status.md +9 -42
  18. package/dist/template/.opencode/command/verify.md +11 -11
  19. package/dist/template/.opencode/memory/README.md +67 -37
  20. package/dist/template/.opencode/memory/_templates/prd.md +102 -18
  21. package/dist/template/.opencode/memory/project/gotchas.md +31 -0
  22. package/dist/template/.opencode/memory.db +0 -0
  23. package/dist/template/.opencode/memory.db-shm +0 -0
  24. package/dist/template/.opencode/memory.db-wal +0 -0
  25. package/dist/template/.opencode/opencode.json +0 -10
  26. package/dist/template/.opencode/package.json +1 -1
  27. package/dist/template/.opencode/skill/beads/SKILL.md +164 -380
  28. package/dist/template/.opencode/skill/beads/references/BOUNDARIES.md +23 -22
  29. package/dist/template/.opencode/skill/beads/references/DEPENDENCIES.md +23 -29
  30. package/dist/template/.opencode/skill/beads/references/RESUMABILITY.md +5 -8
  31. package/dist/template/.opencode/skill/beads/references/WORKFLOWS.md +43 -39
  32. package/dist/template/.opencode/skill/beads-bridge/SKILL.md +80 -53
  33. package/dist/template/.opencode/skill/brainstorming/SKILL.md +19 -5
  34. package/dist/template/.opencode/skill/context-engineering/SKILL.md +30 -63
  35. package/dist/template/.opencode/skill/context-management/SKILL.md +115 -0
  36. package/dist/template/.opencode/skill/deep-research/SKILL.md +4 -4
  37. package/dist/template/.opencode/skill/development-lifecycle/SKILL.md +305 -0
  38. package/dist/template/.opencode/skill/memory-system/SKILL.md +3 -3
  39. package/dist/template/.opencode/skill/prd/SKILL.md +47 -122
  40. package/dist/template/.opencode/skill/prd-task/SKILL.md +48 -4
  41. package/dist/template/.opencode/skill/prd-task/references/prd-schema.json +120 -24
  42. package/dist/template/.opencode/skill/swarm-coordination/SKILL.md +79 -61
  43. package/dist/template/.opencode/skill/tool-priority/SKILL.md +31 -22
  44. package/dist/template/.opencode/tool/context7.ts +183 -0
  45. package/dist/template/.opencode/tool/memory-admin.ts +445 -0
  46. package/dist/template/.opencode/tool/swarm.ts +572 -0
  47. package/package.json +1 -1
  48. package/dist/template/.opencode/memory/_templates/spec.md +0 -66
  49. package/dist/template/.opencode/tool/beads-sync.ts +0 -657
  50. package/dist/template/.opencode/tool/context7-query-docs.ts +0 -89
  51. package/dist/template/.opencode/tool/context7-resolve-library-id.ts +0 -113
  52. package/dist/template/.opencode/tool/memory-maintain.ts +0 -167
  53. package/dist/template/.opencode/tool/memory-migrate.ts +0 -319
  54. package/dist/template/.opencode/tool/swarm-delegate.ts +0 -180
  55. package/dist/template/.opencode/tool/swarm-monitor.ts +0 -388
  56. package/dist/template/.opencode/tool/swarm-plan.ts +0 -697
@@ -1,89 +0,0 @@
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
- export default tool({
7
- description: `Query library documentation from Context7 using a library ID.
8
-
9
- Use when:
10
- - You have a library ID (from context7_resolve_library_id)
11
- - Need specific documentation about a library feature
12
- - Looking for API reference, examples, or setup instructions
13
-
14
- Always resolve library name to ID first with context7_resolve_library_id!
15
-
16
- Examples:
17
- context7_query_docs({ libraryId: "/reactjs/react.dev", topic: "hooks" })
18
- context7_query_docs({ libraryId: "/vercel/next.js", topic: "middleware" })
19
- context7_query_docs({ libraryId: "/microsoft/TypeScript", topic: "generics" })
20
- `,
21
- args: {
22
- libraryId: tool.schema
23
- .string()
24
- .describe(
25
- "Library ID from context7_resolve_library_id (e.g., '/reactjs/react.dev')",
26
- ),
27
- topic: tool.schema
28
- .string()
29
- .describe("Documentation topic or feature to search for"),
30
- },
31
- execute: async (args) => {
32
- const { libraryId, topic } = args;
33
-
34
- if (!libraryId || libraryId.trim() === "") {
35
- return "Error: libraryId is required (use context7-resolve-library-id first)";
36
- }
37
-
38
- if (!topic || topic.trim() === "") {
39
- return "Error: topic is required (e.g., 'hooks', 'setup', 'API reference')";
40
- }
41
-
42
- try {
43
- // Query Context7 documentation - GET /api/v2/context
44
- // Returns text format by default which is better for LLM consumption
45
- const url = new URL(`${CONTEXT7_API}/context`);
46
- url.searchParams.set("libraryId", libraryId);
47
- url.searchParams.set("query", topic);
48
-
49
- // Add API key if available (recommended for higher rate limits)
50
- const apiKey = process.env.CONTEXT7_API_KEY;
51
- const headers: HeadersInit = {
52
- Accept: "text/plain",
53
- "User-Agent": "OpenCode/1.0",
54
- };
55
-
56
- if (apiKey) {
57
- headers.Authorization = `Bearer ${apiKey}`;
58
- }
59
-
60
- const response = await fetch(url.toString(), { headers });
61
-
62
- if (!response.ok) {
63
- if (response.status === 401) {
64
- return `Error: Invalid CONTEXT7_API_KEY. Get a free key at https://context7.com/dashboard`;
65
- }
66
- if (response.status === 404) {
67
- return `Error: Library not found: ${libraryId}\n\nUse context7-resolve-library-id first to find the correct ID.`;
68
- }
69
- if (response.status === 429) {
70
- return `Error: Rate limit exceeded. Get a free API key at https://context7.com/dashboard for higher limits.`;
71
- }
72
- return `Error: Context7 API returned ${response.status}`;
73
- }
74
-
75
- const content = await response.text();
76
-
77
- if (!content || content.trim() === "") {
78
- 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"`;
79
- }
80
-
81
- return `# Documentation: ${topic} (${libraryId})
82
-
83
- ${content}`;
84
- } catch (error: unknown) {
85
- const message = error instanceof Error ? error.message : String(error);
86
- return `Error querying documentation: ${message}`;
87
- }
88
- },
89
- });
@@ -1,113 +0,0 @@
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
- export default tool({
21
- description: `Resolve a library name to its Context7 ID for documentation lookup.
22
-
23
- Use when:
24
- - You need to find the exact library ID format for a package
25
- - Starting a documentation search (get the ID first, then query docs)
26
- - Normalizing library names (e.g., "react" → "/reactjs/react.dev")
27
-
28
- Examples:
29
- context7_resolve_library_id({ libraryName: "react" })
30
- context7_resolve_library_id({ libraryName: "vue", query: "composition API" })
31
- context7_resolve_library_id({ libraryName: "nextjs" })
32
- `,
33
- args: {
34
- libraryName: tool.schema
35
- .string()
36
- .describe(
37
- "Library name to resolve (e.g., 'react', 'lodash', 'typescript')",
38
- ),
39
- query: tool.schema
40
- .string()
41
- .optional()
42
- .describe("Optional context for the search (improves relevance ranking)"),
43
- },
44
- execute: async (args) => {
45
- const { libraryName, query = "documentation" } = args;
46
-
47
- if (!libraryName || libraryName.trim() === "") {
48
- return "Error: libraryName is required";
49
- }
50
-
51
- try {
52
- // Query Context7 library search - GET /api/v2/libs/search
53
- const url = new URL(`${CONTEXT7_API}/libs/search`);
54
- url.searchParams.set("libraryName", libraryName);
55
- url.searchParams.set("query", query);
56
-
57
- // Add API key if available (recommended for higher rate limits)
58
- const apiKey = process.env.CONTEXT7_API_KEY;
59
- const headers: HeadersInit = {
60
- Accept: "application/json",
61
- "User-Agent": "OpenCode/1.0",
62
- };
63
-
64
- if (apiKey) {
65
- headers.Authorization = `Bearer ${apiKey}`;
66
- }
67
-
68
- const response = await fetch(url.toString(), { headers });
69
-
70
- if (!response.ok) {
71
- if (response.status === 401) {
72
- return `Error: Invalid CONTEXT7_API_KEY. Get a free key at https://context7.com/dashboard`;
73
- }
74
- if (response.status === 429) {
75
- return `Error: Rate limit exceeded. Get a free API key at https://context7.com/dashboard for higher limits.`;
76
- }
77
- return `Error: Context7 API returned ${response.status}`;
78
- }
79
-
80
- const data = (await response.json()) as SearchResponse;
81
- const libraries = data.results || [];
82
-
83
- if (!libraries || libraries.length === 0) {
84
- return `No libraries found matching: ${libraryName}\n\nTry:\n- Different library name\n- Check spelling\n- Use official package name`;
85
- }
86
-
87
- const formatted = libraries
88
- .slice(0, 5)
89
- .map((lib, i) => {
90
- const desc = lib.description
91
- ? `\n ${lib.description.slice(0, 100)}...`
92
- : "";
93
- const snippets = lib.totalSnippets
94
- ? ` (${lib.totalSnippets} snippets)`
95
- : "";
96
- const score = lib.benchmarkScore
97
- ? ` [score: ${lib.benchmarkScore}]`
98
- : "";
99
- return `${i + 1}. **${lib.title}** → \`${lib.id}\`${snippets}${score}${desc}`;
100
- })
101
- .join("\n\n");
102
-
103
- return `Found ${libraries.length} libraries matching "${libraryName}":
104
-
105
- ${formatted}
106
-
107
- **Next step**: Use \`context7-query-docs({ libraryId: "${libraries[0].id}", topic: "your topic" })\` to fetch documentation.`;
108
- } catch (error: unknown) {
109
- const message = error instanceof Error ? error.message : String(error);
110
- return `Error resolving library: ${message}`;
111
- }
112
- },
113
- });
@@ -1,167 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin";
2
- import {
3
- archiveOldObservations,
4
- checkpointWAL,
5
- getDatabaseSizes,
6
- getObservationStats,
7
- runFullMaintenance,
8
- vacuumDatabase,
9
- } from "../plugin/lib/memory-db.js";
10
-
11
- export default tool({
12
- description: `Maintain and cleanup the memory system for long-term storage health.
13
-
14
- Purpose:
15
- - Archive old/superseded observations (default: >90 days)
16
- - Checkpoint WAL file back to main database
17
- - Vacuum database to reclaim space and defragment
18
- - Optimize FTS5 search index
19
-
20
- Operations:
21
- - "status": Show current storage stats (default)
22
- - "full": Run full maintenance cycle
23
- - "archive": Only archive old observations
24
- - "checkpoint": Only checkpoint WAL
25
- - "vacuum": Only vacuum database
26
-
27
- Example:
28
- memory-maintain({ operation: "status" })
29
- memory-maintain({ operation: "full", older_than_days: 60 })
30
- memory-maintain({ operation: "archive", dry_run: true })`,
31
- args: {
32
- operation: tool.schema
33
- .string()
34
- .optional()
35
- .default("status")
36
- .describe("Operation: status, full, archive, checkpoint, vacuum"),
37
- older_than_days: tool.schema
38
- .number()
39
- .optional()
40
- .default(90)
41
- .describe("Archive observations older than this many days (default: 90)"),
42
- dry_run: tool.schema
43
- .boolean()
44
- .optional()
45
- .default(false)
46
- .describe("Preview changes without executing"),
47
- },
48
- execute: async (args: {
49
- operation?: string;
50
- older_than_days?: number;
51
- dry_run?: boolean;
52
- }) => {
53
- const operation = args.operation || "status";
54
- const olderThanDays = args.older_than_days ?? 90;
55
- const dryRun = args.dry_run ?? false;
56
-
57
- const results: string[] = [];
58
-
59
- // Helper to format bytes
60
- const formatBytes = (bytes: number): string => {
61
- if (bytes < 1024) return `${bytes} B`;
62
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
63
- return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
64
- };
65
-
66
- // ===== STATUS =====
67
- if (operation === "status") {
68
- const sizes = getDatabaseSizes();
69
- const stats = getObservationStats();
70
-
71
- results.push("## Memory System Status\n");
72
- results.push("### Database Size");
73
- results.push(`- Main DB: ${formatBytes(sizes.mainDb)}`);
74
- results.push(`- WAL file: ${formatBytes(sizes.wal)}`);
75
- results.push(`- **Total: ${formatBytes(sizes.total)}**\n`);
76
-
77
- results.push("### Observations");
78
- results.push(`- Total: ${stats.total}`);
79
- for (const [type, count] of Object.entries(stats)) {
80
- if (type !== "total") {
81
- results.push(`- ${type}: ${count}`);
82
- }
83
- }
84
-
85
- // Archive candidates
86
- const archiveCandidates = archiveOldObservations({
87
- olderThanDays,
88
- includeSuperseded: true,
89
- dryRun: true,
90
- });
91
- results.push(`\n### Maintenance Recommendations`);
92
- results.push(
93
- `- Archive candidates (>${olderThanDays} days): ${archiveCandidates}`,
94
- );
95
- if (sizes.wal > 1024 * 1024) {
96
- results.push(`- WAL checkpoint recommended (WAL > 1MB)`);
97
- }
98
-
99
- return results.join("\n");
100
- }
101
-
102
- // ===== FULL MAINTENANCE =====
103
- if (operation === "full") {
104
- results.push(
105
- dryRun ? "## Full Maintenance (DRY RUN)\n" : "## Full Maintenance\n",
106
- );
107
-
108
- const stats = runFullMaintenance({
109
- olderThanDays,
110
- includeSuperseded: true,
111
- dryRun,
112
- });
113
-
114
- results.push(`### Results`);
115
- results.push(`- Archived observations: ${stats.archived}`);
116
- results.push(`- WAL checkpointed: ${stats.checkpointed ? "Yes" : "No"}`);
117
- results.push(`- Database vacuumed: ${stats.vacuumed ? "Yes" : "No"}`);
118
- results.push(`- Space before: ${formatBytes(stats.dbSizeBefore)}`);
119
- results.push(`- Space after: ${formatBytes(stats.dbSizeAfter)}`);
120
- results.push(`- **Freed: ${formatBytes(stats.freedBytes)}**`);
121
-
122
- return results.join("\n");
123
- }
124
-
125
- // ===== ARCHIVE ONLY =====
126
- if (operation === "archive") {
127
- const archived = archiveOldObservations({
128
- olderThanDays,
129
- includeSuperseded: true,
130
- dryRun,
131
- });
132
-
133
- if (dryRun) {
134
- return `## Archive Preview\n\nWould archive ${archived} observations older than ${olderThanDays} days.\n\nRun without dry_run to execute.`;
135
- }
136
-
137
- return `## Archive Complete\n\nArchived ${archived} observations to observations_archive table.`;
138
- }
139
-
140
- // ===== CHECKPOINT ONLY =====
141
- if (operation === "checkpoint") {
142
- if (dryRun) {
143
- const sizes = getDatabaseSizes();
144
- return `## Checkpoint Preview\n\nWAL size: ${formatBytes(sizes.wal)}\n\nRun without dry_run to checkpoint.`;
145
- }
146
-
147
- const result = checkpointWAL();
148
- return `## Checkpoint Complete\n\nCheckpointed: ${result.checkpointed ? "Yes" : "No"}\nWAL pages processed: ${result.walSize}`;
149
- }
150
-
151
- // ===== VACUUM ONLY =====
152
- if (operation === "vacuum") {
153
- if (dryRun) {
154
- const sizes = getDatabaseSizes();
155
- return `## Vacuum Preview\n\nCurrent size: ${formatBytes(sizes.total)}\n\nRun without dry_run to vacuum.`;
156
- }
157
-
158
- const before = getDatabaseSizes();
159
- const success = vacuumDatabase();
160
- const after = getDatabaseSizes();
161
-
162
- return `## Vacuum Complete\n\nSuccess: ${success ? "Yes" : "No"}\nBefore: ${formatBytes(before.total)}\nAfter: ${formatBytes(after.total)}\nFreed: ${formatBytes(before.total - after.total)}`;
163
- }
164
-
165
- return `Unknown operation: ${operation}. Use: status, full, archive, checkpoint, vacuum`;
166
- },
167
- });
@@ -1,319 +0,0 @@
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
- getMemoryDB,
9
- rebuildFTS5,
10
- storeObservation,
11
- } from "../plugin/lib/memory-db";
12
-
13
- interface ParsedObservation {
14
- type: ObservationType;
15
- title: string;
16
- subtitle?: string;
17
- facts: string[];
18
- narrative: string;
19
- concepts: string[];
20
- files_read: string[];
21
- files_modified: string[];
22
- confidence: ConfidenceLevel;
23
- bead_id?: string;
24
- supersedes?: string;
25
- markdown_file: string;
26
- created_at: string;
27
- created_at_epoch: number;
28
- }
29
-
30
- /**
31
- * Parse YAML frontmatter from markdown content.
32
- */
33
- function parseYAML(yamlContent: string): Record<string, unknown> {
34
- const result: Record<string, unknown> = {};
35
-
36
- for (const line of yamlContent.split("\n")) {
37
- const match = line.match(/^(\w+):\s*(.*)$/);
38
- if (match) {
39
- const [, key, value] = match;
40
- // Handle JSON arrays
41
- if (value.startsWith("[")) {
42
- try {
43
- result[key] = JSON.parse(value);
44
- } catch {
45
- result[key] = value;
46
- }
47
- } else if (value === "null" || value === "") {
48
- result[key] = null;
49
- } else if (value.startsWith('"') && value.endsWith('"')) {
50
- result[key] = value.slice(1, -1);
51
- } else {
52
- result[key] = value;
53
- }
54
- }
55
- }
56
-
57
- return result;
58
- }
59
-
60
- /**
61
- * Extract structured facts from narrative (bullet points).
62
- */
63
- function extractFacts(narrative: string): string[] {
64
- const facts: string[] = [];
65
- const lines = narrative.split("\n");
66
-
67
- for (const line of lines) {
68
- const bulletMatch = line.match(/^[-*]\s+(.+)$/);
69
- if (bulletMatch) {
70
- facts.push(bulletMatch[1].trim());
71
- }
72
- }
73
-
74
- return facts;
75
- }
76
-
77
- /**
78
- * Parse a markdown observation file into structured data.
79
- */
80
- function parseMarkdownObservation(
81
- content: string,
82
- filename: string,
83
- ): ParsedObservation {
84
- // Parse YAML frontmatter
85
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
86
- if (!frontmatterMatch) {
87
- throw new Error(`Invalid format: ${filename} - no YAML frontmatter`);
88
- }
89
-
90
- const yaml = parseYAML(frontmatterMatch[1]);
91
- const narrative = frontmatterMatch[2].trim();
92
-
93
- // Extract title from markdown heading
94
- const titleMatch = narrative.match(/^#\s+.+?\s+(.+)$/m);
95
- const title = titleMatch
96
- ? titleMatch[1]
97
- : (yaml.title as string) || "Untitled";
98
-
99
- // Validate type
100
- const validTypes: ObservationType[] = [
101
- "decision",
102
- "bugfix",
103
- "feature",
104
- "pattern",
105
- "discovery",
106
- "learning",
107
- "warning",
108
- ];
109
- const type = (yaml.type as string)?.toLowerCase() as ObservationType;
110
- if (!validTypes.includes(type)) {
111
- throw new Error(`Invalid type '${yaml.type}' in ${filename}`);
112
- }
113
-
114
- // Validate confidence
115
- const validConfidence: ConfidenceLevel[] = ["high", "medium", "low"];
116
- const confidence = ((yaml.confidence as string)?.toLowerCase() ||
117
- "high") as ConfidenceLevel;
118
- if (!validConfidence.includes(confidence)) {
119
- throw new Error(`Invalid confidence '${yaml.confidence}' in ${filename}`);
120
- }
121
-
122
- // Parse created date
123
- const createdStr = yaml.created as string;
124
- if (!createdStr) {
125
- throw new Error(`Missing created date in ${filename}`);
126
- }
127
- const createdAt = new Date(createdStr);
128
- if (Number.isNaN(createdAt.getTime())) {
129
- throw new Error(`Invalid created date '${createdStr}' in ${filename}`);
130
- }
131
-
132
- // Extract facts from narrative
133
- const facts = extractFacts(narrative);
134
-
135
- // Parse files array
136
- const files = (yaml.files as string[]) || [];
137
-
138
- return {
139
- type,
140
- title,
141
- subtitle: yaml.subtitle as string | undefined,
142
- facts,
143
- narrative,
144
- concepts: (yaml.concepts as string[]) || [],
145
- files_read: files, // Assume all files were read for migration
146
- files_modified: files, // Assume all files were modified for migration
147
- confidence,
148
- bead_id: yaml.bead_id as string | undefined,
149
- supersedes: yaml.supersedes as string | undefined,
150
- markdown_file: filename,
151
- created_at: createdAt.toISOString(),
152
- created_at_epoch: createdAt.getTime(),
153
- };
154
- }
155
-
156
- export default tool({
157
- description: `Migrate existing markdown observations to SQLite database.
158
-
159
- Purpose:
160
- - Import existing .opencode/memory/observations/*.md files into SQLite
161
- - Preserves all metadata and content
162
- - Creates FTS5 index for fast search
163
- - Safe to run multiple times (skips already migrated files)
164
-
165
- Example:
166
- memory-migrate({ dry_run: true }) // Preview migration
167
- memory-migrate({}) // Run migration`,
168
- args: {
169
- dry_run: tool.schema
170
- .boolean()
171
- .optional()
172
- .describe("Preview migration without writing to database"),
173
- force: tool.schema
174
- .boolean()
175
- .optional()
176
- .describe("Force re-migration of all files"),
177
- },
178
- execute: async (args: { dry_run?: boolean; force?: boolean }) => {
179
- const obsDir = path.join(process.cwd(), ".opencode/memory/observations");
180
- const migrationMarker = path.join(obsDir, ".migrated");
181
-
182
- // Check for existing migration
183
- if (!args.force) {
184
- try {
185
- await fs.access(migrationMarker);
186
- const markerContent = await fs.readFile(migrationMarker, "utf-8");
187
- return `Migration already complete.\n\n${markerContent}\n\nUse force: true to re-migrate.`;
188
- } catch {
189
- // No marker, proceed with migration
190
- }
191
- }
192
-
193
- // Get all markdown files
194
- let files: string[];
195
- try {
196
- const entries = await fs.readdir(obsDir);
197
- files = entries.filter((f) => f.endsWith(".md") && !f.startsWith("."));
198
- } catch {
199
- return "No observations directory found at .opencode/memory/observations/";
200
- }
201
-
202
- if (files.length === 0) {
203
- return "No markdown files found to migrate.";
204
- }
205
-
206
- // Initialize database
207
- const db = getMemoryDB();
208
-
209
- // Parse and migrate
210
- const results: {
211
- migrated: string[];
212
- skipped: string[];
213
- errors: { file: string; error: string }[];
214
- } = {
215
- migrated: [],
216
- skipped: [],
217
- errors: [],
218
- };
219
-
220
- // Sort by filename (which includes date) for chronological order
221
- files.sort();
222
-
223
- for (const file of files) {
224
- try {
225
- const content = await fs.readFile(path.join(obsDir, file), "utf-8");
226
- const parsed = parseMarkdownObservation(content, file);
227
-
228
- if (args.dry_run) {
229
- results.migrated.push(
230
- `${file} → ${parsed.type}: ${parsed.title.substring(0, 50)}`,
231
- );
232
- continue;
233
- }
234
-
235
- // Check if already migrated (by markdown_file)
236
- const existing = db
237
- .query("SELECT id FROM observations WHERE markdown_file = ?")
238
- .get(file);
239
-
240
- if (existing && !args.force) {
241
- results.skipped.push(file);
242
- continue;
243
- }
244
-
245
- // Store observation
246
- const input: ObservationInput = {
247
- type: parsed.type,
248
- title: parsed.title,
249
- subtitle: parsed.subtitle,
250
- facts: parsed.facts,
251
- narrative: parsed.narrative,
252
- concepts: parsed.concepts,
253
- files_read: parsed.files_read,
254
- files_modified: parsed.files_modified,
255
- confidence: parsed.confidence,
256
- bead_id: parsed.bead_id,
257
- markdown_file: file,
258
- };
259
-
260
- storeObservation(input);
261
- results.migrated.push(file);
262
- } catch (e) {
263
- results.errors.push({
264
- file,
265
- error: e instanceof Error ? e.message : String(e),
266
- });
267
- }
268
- }
269
-
270
- // Rebuild FTS5 index after migration
271
- if (!args.dry_run && results.migrated.length > 0) {
272
- try {
273
- rebuildFTS5();
274
- } catch {
275
- // FTS5 rebuild failed, continue
276
- }
277
- }
278
-
279
- // Write migration marker
280
- if (!args.dry_run) {
281
- const markerContent = [
282
- `Migrated ${results.migrated.length} observations on ${new Date().toISOString()}`,
283
- `Skipped: ${results.skipped.length}`,
284
- `Errors: ${results.errors.length}`,
285
- ].join("\n");
286
- await fs.writeFile(migrationMarker, markerContent, "utf-8");
287
- }
288
-
289
- // Format output
290
- let output = args.dry_run
291
- ? "# Migration Preview (Dry Run)\n\n"
292
- : "# Migration Complete\n\n";
293
-
294
- output += `**Total files**: ${files.length}\n`;
295
- output += `**Migrated**: ${results.migrated.length}\n`;
296
- output += `**Skipped**: ${results.skipped.length}\n`;
297
- output += `**Errors**: ${results.errors.length}\n\n`;
298
-
299
- if (results.errors.length > 0) {
300
- output += "## Errors\n\n";
301
- for (const { file, error } of results.errors) {
302
- output += `- **${file}**: ${error}\n`;
303
- }
304
- output += "\n";
305
- }
306
-
307
- if (args.dry_run && results.migrated.length > 0) {
308
- output += "## Files to migrate\n\n";
309
- for (const item of results.migrated.slice(0, 20)) {
310
- output += `- ${item}\n`;
311
- }
312
- if (results.migrated.length > 20) {
313
- output += `- ... and ${results.migrated.length - 20} more\n`;
314
- }
315
- }
316
-
317
- return output;
318
- },
319
- });