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
|
@@ -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
|
-
});
|