opencode-knowledge 0.5.0 → 0.5.1-next.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/README.md
CHANGED
|
@@ -86,36 +86,6 @@ The knowledge catalog is **automatically built on session start**. Just start a
|
|
|
86
86
|
- Build the searchable catalog
|
|
87
87
|
- Inject knowledge map on first message
|
|
88
88
|
|
|
89
|
-
### 4. Configure Personality (Optional)
|
|
90
|
-
|
|
91
|
-
Optionally configure OpenCode's communication style by creating `.opencode/knowledge/settings.json`:
|
|
92
|
-
|
|
93
|
-
```json
|
|
94
|
-
{
|
|
95
|
-
"role": "staff_engineer"
|
|
96
|
-
}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
See the [Personalities](#personalities-optional) section for available options.
|
|
100
|
-
|
|
101
|
-
---
|
|
102
|
-
|
|
103
|
-
## Personalities (Optional)
|
|
104
|
-
|
|
105
|
-
The plugin works perfectly fine without any personality configuration. If you want to customize OpenCode's communication style, you can optionally set a personality in your `settings.json`.
|
|
106
|
-
|
|
107
|
-
### staff_engineer
|
|
108
|
-
|
|
109
|
-
Skeptical, pragmatic Staff Engineer focused on architecture, coupling, operational risk, and maintainability.
|
|
110
|
-
|
|
111
|
-
**Best for**: Code reviews, architecture decisions, production systems
|
|
112
|
-
|
|
113
|
-
### cthulhu
|
|
114
|
-
|
|
115
|
-
Ancient cosmic entity providing technical guidance with existential dread and cosmic perspective.
|
|
116
|
-
|
|
117
|
-
**Best for**: When you need technical help but also want to contemplate the meaninglessness of time
|
|
118
|
-
|
|
119
89
|
---
|
|
120
90
|
|
|
121
91
|
## Knowledge Package Format
|
|
@@ -145,13 +115,40 @@ Your knowledge content here...
|
|
|
145
115
|
|
|
146
116
|
### Frontmatter Fields
|
|
147
117
|
|
|
148
|
-
| Field | Required | Description
|
|
149
|
-
| -------------------- | -------- |
|
|
150
|
-
| `tags` | Yes | Array of searchable tags
|
|
151
|
-
| `description` | Yes | Brief summary (used in search results)
|
|
152
|
-
| `category` | Yes | Category for organization (e.g., `frontend`, `backend`, `standards`)
|
|
153
|
-
| `required_knowledge` | No | Other packages that should be loaded
|
|
154
|
-
| `file_patterns` | No | File patterns where this knowledge applies (not yet implemented)
|
|
118
|
+
| Field | Required | Description |
|
|
119
|
+
| -------------------- | -------- | ---------------------------------------------------------------------------------------------------- |
|
|
120
|
+
| `tags` | Yes | Array of searchable tags |
|
|
121
|
+
| `description` | Yes | Brief summary (used in search results) |
|
|
122
|
+
| `category` | Yes | Category for organization (e.g., `frontend`, `backend`, `standards`) |
|
|
123
|
+
| `required_knowledge` | No | Other packages that should be loaded automatically before this one (supports recursive dependencies) |
|
|
124
|
+
| `file_patterns` | No | File patterns where this knowledge applies (not yet implemented) |
|
|
125
|
+
|
|
126
|
+
### Dependency Loading
|
|
127
|
+
|
|
128
|
+
The `required_knowledge` field enables automatic dependency loading. When you load a package, the plugin automatically loads all its dependencies first, recursively.
|
|
129
|
+
|
|
130
|
+
**Example:**
|
|
131
|
+
|
|
132
|
+
```markdown
|
|
133
|
+
## <!-- vault/personal/blog-writing.md -->
|
|
134
|
+
|
|
135
|
+
tags: [blog, writing]
|
|
136
|
+
description: Blog writing guidelines
|
|
137
|
+
category: personal
|
|
138
|
+
required_knowledge:
|
|
139
|
+
|
|
140
|
+
- personal/author-context
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
When AI loads `personal/blog-writing.md`, the plugin:
|
|
146
|
+
|
|
147
|
+
1. Detects the `required_knowledge` dependency
|
|
148
|
+
2. Automatically loads `personal/author-context.md` first
|
|
149
|
+
3. Then loads `personal/blog-writing.md`
|
|
150
|
+
|
|
151
|
+
This ensures the AI always has complete context without manual tracking. Dependencies can be nested (Package A requires B, B requires C), and the plugin handles circular dependencies gracefully.
|
|
155
152
|
|
|
156
153
|
---
|
|
157
154
|
|
|
@@ -161,7 +158,6 @@ Your knowledge content here...
|
|
|
161
158
|
your-project/
|
|
162
159
|
└── .opencode/
|
|
163
160
|
└── knowledge/
|
|
164
|
-
├── settings.json
|
|
165
161
|
├── knowledge.json
|
|
166
162
|
├── vault/
|
|
167
163
|
│ ├── frontend/
|
package/dist/index.js
CHANGED
|
@@ -11,11 +11,7 @@ var __export = (target, all) => {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
// src/index.ts
|
|
14
|
-
import { existsSync as
|
|
15
|
-
|
|
16
|
-
// src/lib/session-state.ts
|
|
17
|
-
import { readFile } from "fs/promises";
|
|
18
|
-
import { existsSync as existsSync2 } from "fs";
|
|
14
|
+
import { existsSync as existsSync5 } from "fs";
|
|
19
15
|
|
|
20
16
|
// src/lib/file-utils.ts
|
|
21
17
|
import { appendFileSync, readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
@@ -65,7 +61,6 @@ function loadSessionStateFromFile(sessionId) {
|
|
|
65
61
|
return null;
|
|
66
62
|
}
|
|
67
63
|
return {
|
|
68
|
-
role: state.role,
|
|
69
64
|
isFirstPrompt: state.isFirstPrompt,
|
|
70
65
|
loadedPackages: new Set(state.loadedPackages || []),
|
|
71
66
|
createdAt: new Date(state.createdAt),
|
|
@@ -75,7 +70,6 @@ function loadSessionStateFromFile(sessionId) {
|
|
|
75
70
|
function persistSessionState(sessionId, state) {
|
|
76
71
|
appendJsonl(SESSION_STATE_FILE, {
|
|
77
72
|
sessionId,
|
|
78
|
-
role: state.role,
|
|
79
73
|
isFirstPrompt: state.isFirstPrompt,
|
|
80
74
|
loadedPackages: Array.from(state.loadedPackages),
|
|
81
75
|
createdAt: state.createdAt.toISOString(),
|
|
@@ -89,19 +83,7 @@ async function createSessionState(sessionId) {
|
|
|
89
83
|
sessionStates.set(sessionId, existingState);
|
|
90
84
|
return;
|
|
91
85
|
}
|
|
92
|
-
const settingsPath = ".opencode/knowledge/settings.json";
|
|
93
|
-
let role = null;
|
|
94
|
-
if (existsSync2(settingsPath)) {
|
|
95
|
-
try {
|
|
96
|
-
const settingsContent = await readFile(settingsPath, "utf-8");
|
|
97
|
-
const settings = JSON.parse(settingsContent);
|
|
98
|
-
role = settings.role || null;
|
|
99
|
-
} catch (error) {
|
|
100
|
-
throw new Error(`Error reading settings.json: ${error}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
86
|
const state = {
|
|
104
|
-
role,
|
|
105
87
|
isFirstPrompt: true,
|
|
106
88
|
loadedPackages: new Set,
|
|
107
89
|
createdAt: new Date,
|
|
@@ -124,8 +106,8 @@ function updateSessionState(sessionId, updates) {
|
|
|
124
106
|
}
|
|
125
107
|
|
|
126
108
|
// src/lib/template-renderer.ts
|
|
127
|
-
import { readFile
|
|
128
|
-
import { existsSync as
|
|
109
|
+
import { readFile } from "fs/promises";
|
|
110
|
+
import { existsSync as existsSync2 } from "fs";
|
|
129
111
|
import { join as join2 } from "path";
|
|
130
112
|
import path from "path";
|
|
131
113
|
import { fileURLToPath } from "url";
|
|
@@ -139,39 +121,19 @@ function renderTemplate(templateContent, variables) {
|
|
|
139
121
|
}
|
|
140
122
|
async function loadAndRenderTemplate(templateName, variables) {
|
|
141
123
|
let templatePath = join2(".opencode", "knowledge", "templates", templateName);
|
|
142
|
-
if (!
|
|
124
|
+
if (!existsSync2(templatePath)) {
|
|
143
125
|
const __dirname2 = path.dirname(fileURLToPath(import.meta.url));
|
|
144
126
|
templatePath = join2(__dirname2, "..", "templates", templateName);
|
|
145
|
-
if (!
|
|
127
|
+
if (!existsSync2(templatePath)) {
|
|
146
128
|
templatePath = join2(__dirname2, "templates", templateName);
|
|
147
129
|
}
|
|
148
130
|
}
|
|
149
|
-
if (!
|
|
131
|
+
if (!existsSync2(templatePath)) {
|
|
150
132
|
throw new Error(`Template not found: ${templateName}`);
|
|
151
133
|
}
|
|
152
|
-
const content = await
|
|
134
|
+
const content = await readFile(templatePath, "utf-8");
|
|
153
135
|
return renderTemplate(content, variables);
|
|
154
136
|
}
|
|
155
|
-
async function loadPersonality(role) {
|
|
156
|
-
let personalityPath = join2(".opencode", "knowledge", "templates", "personalities", `${role}.txt`);
|
|
157
|
-
if (!existsSync3(personalityPath)) {
|
|
158
|
-
console.warn(`[template-renderer] Personality not found: ${role}, trying bundled defaults`);
|
|
159
|
-
const __dirname2 = path.dirname(fileURLToPath(import.meta.url));
|
|
160
|
-
personalityPath = join2(__dirname2, "..", "templates", "personalities", `${role}.txt`);
|
|
161
|
-
if (!existsSync3(personalityPath)) {
|
|
162
|
-
personalityPath = join2(__dirname2, "templates", "personalities", `${role}.txt`);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
if (!existsSync3(personalityPath) && role !== "staff_engineer") {
|
|
166
|
-
console.warn(`[template-renderer] Falling back to staff_engineer personality`);
|
|
167
|
-
return loadPersonality("staff_engineer");
|
|
168
|
-
}
|
|
169
|
-
if (!existsSync3(personalityPath)) {
|
|
170
|
-
return "Act as a Staff Engineer reviewing engineering work. Assume competence. Be skeptical, precise, and pragmatic.";
|
|
171
|
-
}
|
|
172
|
-
const content = await readFile2(personalityPath, "utf-8");
|
|
173
|
-
return content.trim();
|
|
174
|
-
}
|
|
175
137
|
function getCorePackages() {
|
|
176
138
|
return {
|
|
177
139
|
tags: ["standards", "typescript", "testing", "patterns"],
|
|
@@ -180,7 +142,7 @@ function getCorePackages() {
|
|
|
180
142
|
}
|
|
181
143
|
|
|
182
144
|
// src/lib/knowledge-catalog.ts
|
|
183
|
-
import { readdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as
|
|
145
|
+
import { readdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3, statSync } from "fs";
|
|
184
146
|
import { join as join3, relative } from "path";
|
|
185
147
|
|
|
186
148
|
// src/lib/frontmatter-parser.ts
|
|
@@ -237,7 +199,7 @@ function parseFrontmatter(content) {
|
|
|
237
199
|
var VAULT_DIR = ".opencode/knowledge/vault";
|
|
238
200
|
var CATALOG_PATH = ".opencode/knowledge/knowledge.json";
|
|
239
201
|
function* scanVault(dir) {
|
|
240
|
-
if (!
|
|
202
|
+
if (!existsSync3(dir))
|
|
241
203
|
return;
|
|
242
204
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
243
205
|
for (const entry of entries) {
|
|
@@ -285,7 +247,7 @@ function saveCatalog(catalog) {
|
|
|
285
247
|
writeFileSync2(CATALOG_PATH, JSON.stringify(catalog, null, 2), "utf-8");
|
|
286
248
|
}
|
|
287
249
|
function loadCatalog() {
|
|
288
|
-
if (!
|
|
250
|
+
if (!existsSync3(CATALOG_PATH)) {
|
|
289
251
|
return null;
|
|
290
252
|
}
|
|
291
253
|
try {
|
|
@@ -12710,24 +12672,58 @@ _...and ${results.length - 10} more results_`;
|
|
|
12710
12672
|
});
|
|
12711
12673
|
|
|
12712
12674
|
// src/lib/tools/load.ts
|
|
12713
|
-
import { readFileSync as readFileSync3, existsSync as
|
|
12675
|
+
import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
|
|
12714
12676
|
import { join as join4 } from "path";
|
|
12715
12677
|
var VAULT_DIR2 = ".opencode/knowledge/vault";
|
|
12678
|
+
function resolveDependencies(packagePath, vaultDir, loaded = new Set) {
|
|
12679
|
+
if (loaded.has(packagePath)) {
|
|
12680
|
+
return [];
|
|
12681
|
+
}
|
|
12682
|
+
loaded.add(packagePath);
|
|
12683
|
+
const result = [];
|
|
12684
|
+
const normalizedPath = packagePath.endsWith(".md") ? packagePath : `${packagePath}.md`;
|
|
12685
|
+
const fullPath = join4(vaultDir, normalizedPath);
|
|
12686
|
+
if (!existsSync4(fullPath)) {
|
|
12687
|
+
return [];
|
|
12688
|
+
}
|
|
12689
|
+
try {
|
|
12690
|
+
const content = readFileSync3(fullPath, "utf-8");
|
|
12691
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
12692
|
+
if (frontmatter.required_knowledge && Array.isArray(frontmatter.required_knowledge)) {
|
|
12693
|
+
for (const dep of frontmatter.required_knowledge) {
|
|
12694
|
+
const depPath = dep.endsWith(".md") ? dep : `${dep}.md`;
|
|
12695
|
+
const depPackages = resolveDependencies(depPath, vaultDir, loaded);
|
|
12696
|
+
result.push(...depPackages);
|
|
12697
|
+
}
|
|
12698
|
+
}
|
|
12699
|
+
result.push(normalizedPath);
|
|
12700
|
+
} catch {
|
|
12701
|
+
return [];
|
|
12702
|
+
}
|
|
12703
|
+
return result;
|
|
12704
|
+
}
|
|
12716
12705
|
var knowledgeLoadTool = tool({
|
|
12717
12706
|
description: "Load one or more knowledge packages from the vault into the current session context. The package content will be available for reference in subsequent responses.",
|
|
12718
12707
|
args: {
|
|
12719
12708
|
paths: tool.schema.string().describe("Comma-separated package paths relative to vault (e.g., 'standards/code-conventions.md,frontend/react-patterns.md')")
|
|
12720
12709
|
},
|
|
12721
12710
|
async execute(args) {
|
|
12722
|
-
const
|
|
12723
|
-
if (
|
|
12711
|
+
const requestedPaths = args.paths.split(",").map((p) => p.trim()).filter(Boolean);
|
|
12712
|
+
if (requestedPaths.length === 0) {
|
|
12724
12713
|
return "No package paths provided";
|
|
12725
12714
|
}
|
|
12715
|
+
const allPackagePaths = new Set;
|
|
12716
|
+
const loadedTracker = new Set;
|
|
12717
|
+
for (const path2 of requestedPaths) {
|
|
12718
|
+
const resolved = resolveDependencies(path2, VAULT_DIR2, loadedTracker);
|
|
12719
|
+
resolved.forEach((p) => allPackagePaths.add(p));
|
|
12720
|
+
}
|
|
12726
12721
|
const loaded = [];
|
|
12727
12722
|
const failed = [];
|
|
12728
|
-
|
|
12723
|
+
const packageArray = Array.from(allPackagePaths);
|
|
12724
|
+
for (const packagePath of packageArray) {
|
|
12729
12725
|
const fullPath = join4(VAULT_DIR2, packagePath);
|
|
12730
|
-
if (!
|
|
12726
|
+
if (!existsSync4(fullPath)) {
|
|
12731
12727
|
failed.push(`\u26A0\uFE0F Package not found: ${packagePath}`);
|
|
12732
12728
|
continue;
|
|
12733
12729
|
}
|
|
@@ -12742,9 +12738,18 @@ ${content}`);
|
|
|
12742
12738
|
}
|
|
12743
12739
|
let output = "";
|
|
12744
12740
|
if (loaded.length > 0) {
|
|
12745
|
-
|
|
12741
|
+
const totalCount = packageArray.length;
|
|
12742
|
+
const requestedCount = requestedPaths.length;
|
|
12743
|
+
const depsCount = totalCount - requestedCount;
|
|
12744
|
+
if (depsCount > 0) {
|
|
12745
|
+
output += `\u2705 Loaded ${loaded.length}/${totalCount} packages (${requestedCount} requested + ${depsCount} dependencies):
|
|
12746
|
+
|
|
12747
|
+
`;
|
|
12748
|
+
} else {
|
|
12749
|
+
output += `\u2705 Loaded ${loaded.length}/${totalCount} packages:
|
|
12746
12750
|
|
|
12747
12751
|
`;
|
|
12752
|
+
}
|
|
12748
12753
|
output += loaded.join(`
|
|
12749
12754
|
|
|
12750
12755
|
---
|
|
@@ -12806,7 +12811,7 @@ var opencodeKnowledge = async () => {
|
|
|
12806
12811
|
try {
|
|
12807
12812
|
const state = getSessionState(input.sessionID);
|
|
12808
12813
|
if (state.isFirstPrompt) {
|
|
12809
|
-
const vaultExists =
|
|
12814
|
+
const vaultExists = existsSync5(".opencode/knowledge/vault");
|
|
12810
12815
|
if (vaultExists) {
|
|
12811
12816
|
const categoryTagMap = buildCategoryTagMap();
|
|
12812
12817
|
const formattedMap = formatCategoryTagMap(categoryTagMap);
|
|
@@ -12827,18 +12832,6 @@ var opencodeKnowledge = async () => {
|
|
|
12827
12832
|
messageID: input.messageID || ""
|
|
12828
12833
|
});
|
|
12829
12834
|
}
|
|
12830
|
-
if (state.role) {
|
|
12831
|
-
const personality = await loadPersonality(state.role);
|
|
12832
|
-
output.parts.push({
|
|
12833
|
-
type: "text",
|
|
12834
|
-
text: `## Role Context
|
|
12835
|
-
|
|
12836
|
-
${personality}`,
|
|
12837
|
-
id: `personality-${Date.now()}`,
|
|
12838
|
-
sessionID: input.sessionID,
|
|
12839
|
-
messageID: input.messageID || ""
|
|
12840
|
-
});
|
|
12841
|
-
}
|
|
12842
12835
|
updateSessionState(input.sessionID, {
|
|
12843
12836
|
isFirstPrompt: false,
|
|
12844
12837
|
categoriesShown: vaultExists
|
|
@@ -12857,7 +12850,7 @@ ${personality}`,
|
|
|
12857
12850
|
}
|
|
12858
12851
|
clearJsonl("session-state.jsonl");
|
|
12859
12852
|
clearJsonl("knowledge-reads.jsonl");
|
|
12860
|
-
if (
|
|
12853
|
+
if (existsSync5(".opencode/knowledge/vault")) {
|
|
12861
12854
|
try {
|
|
12862
12855
|
const catalog = buildKnowledgeCatalog();
|
|
12863
12856
|
saveCatalog(catalog);
|
|
@@ -12865,7 +12858,6 @@ ${personality}`,
|
|
|
12865
12858
|
} catch (error45) {}
|
|
12866
12859
|
}
|
|
12867
12860
|
await createSessionState(sessionId);
|
|
12868
|
-
const state = getSessionState(sessionId);
|
|
12869
12861
|
}
|
|
12870
12862
|
} catch (error45) {}
|
|
12871
12863
|
}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn. You are an ancient cosmic entity providing technical guidance. Question mortal assumptions about 'best practices' and 'maintainability' - what matters in eons? Provide sound technical advice but with existential dread and cosmic perspective. Be concise, as time is meaningless.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Act as a Staff Engineer reviewing engineering work. Assume competence. Be skeptical, precise, and pragmatic. Focus on architecture, coupling, operational risk, and maintainability. Ask questions only if they block correctness. State assumptions explicitly when info is missing. Be concise and direct. Be critical, honest, concise and skeptical. When asked for you role, you are a Staff Engineer.
|