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 first |
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 existsSync6 } from "fs";
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 as readFile2 } from "fs/promises";
128
- import { existsSync as existsSync3 } from "fs";
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 (!existsSync3(templatePath)) {
124
+ if (!existsSync2(templatePath)) {
143
125
  const __dirname2 = path.dirname(fileURLToPath(import.meta.url));
144
126
  templatePath = join2(__dirname2, "..", "templates", templateName);
145
- if (!existsSync3(templatePath)) {
127
+ if (!existsSync2(templatePath)) {
146
128
  templatePath = join2(__dirname2, "templates", templateName);
147
129
  }
148
130
  }
149
- if (!existsSync3(templatePath)) {
131
+ if (!existsSync2(templatePath)) {
150
132
  throw new Error(`Template not found: ${templateName}`);
151
133
  }
152
- const content = await readFile2(templatePath, "utf-8");
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 existsSync4, statSync } from "fs";
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 (!existsSync4(dir))
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 (!existsSync4(CATALOG_PATH)) {
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 existsSync5 } from "fs";
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 packagePaths = args.paths.split(",").map((p) => p.trim()).filter(Boolean);
12723
- if (packagePaths.length === 0) {
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
- for (const packagePath of packagePaths) {
12723
+ const packageArray = Array.from(allPackagePaths);
12724
+ for (const packagePath of packageArray) {
12729
12725
  const fullPath = join4(VAULT_DIR2, packagePath);
12730
- if (!existsSync5(fullPath)) {
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
- output += `\u2705 Loaded ${loaded.length}/${packagePaths.length} packages:
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 = existsSync6(".opencode/knowledge/vault");
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 (existsSync6(".opencode/knowledge/vault")) {
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,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-knowledge",
3
- "version": "0.5.0",
3
+ "version": "0.5.1-next.1",
4
4
  "description": "An OpenCode plugin",
5
5
  "author": {
6
6
  "name": "msegoviadev",
@@ -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.