opencode-knowledge 0.5.1 → 0.6.0-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,13 @@ 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 automatically before this one (supports recursive dependencies) |
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) |
155
125
 
156
126
  ### Dependency Loading
157
127
 
@@ -160,17 +130,20 @@ The `required_knowledge` field enables automatic dependency loading. When you lo
160
130
  **Example:**
161
131
 
162
132
  ```markdown
163
- <!-- vault/personal/blog-writing.md -->
164
- ---
133
+ ## <!-- vault/personal/blog-writing.md -->
134
+
165
135
  tags: [blog, writing]
166
136
  description: Blog writing guidelines
167
137
  category: personal
168
138
  required_knowledge:
169
- - personal/author-context
139
+
140
+ - personal/author-context
141
+
170
142
  ---
171
143
  ```
172
144
 
173
145
  When AI loads `personal/blog-writing.md`, the plugin:
146
+
174
147
  1. Detects the `required_knowledge` dependency
175
148
  2. Automatically loads `personal/author-context.md` first
176
149
  3. Then loads `personal/blog-writing.md`
@@ -185,7 +158,6 @@ This ensures the AI always has complete context without manual tracking. Depende
185
158
  your-project/
186
159
  └── .opencode/
187
160
  └── knowledge/
188
- ├── settings.json
189
161
  ├── knowledge.json
190
162
  ├── vault/
191
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 {
@@ -12669,7 +12631,53 @@ function tool(input) {
12669
12631
  return input;
12670
12632
  }
12671
12633
  tool.schema = exports_external;
12634
+ // src/lib/logger.ts
12635
+ var ENV_CONSOLE_LOG = "OPENCODE_KNOWLEDGE_CONSOLE_LOG";
12636
+ var _client = null;
12637
+ function isConsoleLogEnabled() {
12638
+ const val = process.env[ENV_CONSOLE_LOG];
12639
+ return val === "1" || val?.toLowerCase() === "true";
12640
+ }
12641
+ function initLogger(client) {
12642
+ _client = client;
12643
+ }
12644
+ function createLogger(module) {
12645
+ const service = `opencode-knowledge.${module}`;
12646
+ const log = (level, message, extra) => {
12647
+ const app = _client?.app;
12648
+ if (app && typeof app.log === "function") {
12649
+ app.log({
12650
+ body: { service, level, message, extra }
12651
+ }).catch(() => {});
12652
+ } else if (isConsoleLogEnabled()) {
12653
+ const prefix = `[${service}]`;
12654
+ const args = extra ? [prefix, message, extra] : [prefix, message];
12655
+ switch (level) {
12656
+ case "debug":
12657
+ console.debug(...args);
12658
+ break;
12659
+ case "info":
12660
+ console.info(...args);
12661
+ break;
12662
+ case "warn":
12663
+ console.warn(...args);
12664
+ break;
12665
+ case "error":
12666
+ console.error(...args);
12667
+ break;
12668
+ }
12669
+ }
12670
+ };
12671
+ return {
12672
+ debug: (message, extra) => log("debug", message, extra),
12673
+ info: (message, extra) => log("info", message, extra),
12674
+ warn: (message, extra) => log("warn", message, extra),
12675
+ error: (message, extra) => log("error", message, extra)
12676
+ };
12677
+ }
12678
+
12672
12679
  // src/lib/tools/search.ts
12680
+ var log = createLogger("tools.search");
12673
12681
  var knowledgeSearchTool = tool({
12674
12682
  description: "Search the knowledge vault for packages matching specific tags. Returns a ranked list of relevant knowledge packages with their metadata.",
12675
12683
  args: {
@@ -12677,12 +12685,16 @@ var knowledgeSearchTool = tool({
12677
12685
  },
12678
12686
  async execute(args) {
12679
12687
  const tagArray = args.tags.split(",").map((t) => t.trim()).filter(Boolean);
12688
+ log.debug("knowledge_search called", { tags: args.tags, parsedTags: tagArray });
12680
12689
  if (tagArray.length === 0) {
12690
+ log.debug("knowledge_search - no tags provided");
12681
12691
  return "No tags provided. Please specify at least one tag.";
12682
12692
  }
12683
12693
  try {
12694
+ log.debug("knowledge_search - calling searchKnowledge", { tagCount: tagArray.length });
12684
12695
  const results = await searchKnowledge(tagArray);
12685
12696
  loadCatalog();
12697
+ log.debug("searchKnowledge completed", { resultsCount: results.length });
12686
12698
  if (results.length === 0) {
12687
12699
  return `No knowledge packages found matching [${tagArray.join(", ")}]`;
12688
12700
  }
@@ -12704,13 +12716,14 @@ _...and ${results.length - 10} more results_`;
12704
12716
  }
12705
12717
  return output;
12706
12718
  } catch (error45) {
12719
+ log.error("knowledge_search error", { error: String(error45) });
12707
12720
  return "Failed to search knowledge vault. Please try again.";
12708
12721
  }
12709
12722
  }
12710
12723
  });
12711
12724
 
12712
12725
  // src/lib/tools/load.ts
12713
- import { readFileSync as readFileSync3, existsSync as existsSync5 } from "fs";
12726
+ import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
12714
12727
  import { join as join4 } from "path";
12715
12728
  var VAULT_DIR2 = ".opencode/knowledge/vault";
12716
12729
  function resolveDependencies(packagePath, vaultDir, loaded = new Set) {
@@ -12721,7 +12734,7 @@ function resolveDependencies(packagePath, vaultDir, loaded = new Set) {
12721
12734
  const result = [];
12722
12735
  const normalizedPath = packagePath.endsWith(".md") ? packagePath : `${packagePath}.md`;
12723
12736
  const fullPath = join4(vaultDir, normalizedPath);
12724
- if (!existsSync5(fullPath)) {
12737
+ if (!existsSync4(fullPath)) {
12725
12738
  return [];
12726
12739
  }
12727
12740
  try {
@@ -12761,7 +12774,7 @@ var knowledgeLoadTool = tool({
12761
12774
  const packageArray = Array.from(allPackagePaths);
12762
12775
  for (const packagePath of packageArray) {
12763
12776
  const fullPath = join4(VAULT_DIR2, packagePath);
12764
- if (!existsSync5(fullPath)) {
12777
+ if (!existsSync4(fullPath)) {
12765
12778
  failed.push(`\u26A0\uFE0F Package not found: ${packagePath}`);
12766
12779
  continue;
12767
12780
  }
@@ -12838,18 +12851,22 @@ var knowledgeIndexTool = tool({
12838
12851
  });
12839
12852
 
12840
12853
  // src/index.ts
12841
- var opencodeKnowledge = async () => {
12854
+ var log2 = createLogger("plugin");
12855
+ var opencodeKnowledge = async (input) => {
12856
+ initLogger(input.client);
12857
+ log2.info("Plugin initialized");
12842
12858
  return {
12843
12859
  tool: {
12844
12860
  knowledge_search: knowledgeSearchTool,
12845
12861
  knowledge_load: knowledgeLoadTool,
12846
12862
  knowledge_index: knowledgeIndexTool
12847
12863
  },
12848
- "chat.message": async (input, output) => {
12864
+ "chat.message": async (input2, output) => {
12849
12865
  try {
12850
- const state = getSessionState(input.sessionID);
12866
+ const state = getSessionState(input2.sessionID);
12851
12867
  if (state.isFirstPrompt) {
12852
- const vaultExists = existsSync6(".opencode/knowledge/vault");
12868
+ log2.debug("First message in session", { sessionID: input2.sessionID });
12869
+ const vaultExists = existsSync5(".opencode/knowledge/vault");
12853
12870
  if (vaultExists) {
12854
12871
  const categoryTagMap = buildCategoryTagMap();
12855
12872
  const formattedMap = formatCategoryTagMap(categoryTagMap);
@@ -12866,51 +12883,52 @@ var opencodeKnowledge = async () => {
12866
12883
  type: "text",
12867
12884
  text: knowledgePrompt,
12868
12885
  id: `knowledge-${Date.now()}`,
12869
- sessionID: input.sessionID,
12870
- messageID: input.messageID || ""
12871
- });
12872
- }
12873
- if (state.role) {
12874
- const personality = await loadPersonality(state.role);
12875
- output.parts.push({
12876
- type: "text",
12877
- text: `## Role Context
12878
-
12879
- ${personality}`,
12880
- id: `personality-${Date.now()}`,
12881
- sessionID: input.sessionID,
12882
- messageID: input.messageID || ""
12886
+ sessionID: input2.sessionID,
12887
+ messageID: input2.messageID || ""
12883
12888
  });
12889
+ log2.debug("Knowledge map injected");
12884
12890
  }
12885
- updateSessionState(input.sessionID, {
12891
+ updateSessionState(input2.sessionID, {
12886
12892
  isFirstPrompt: false,
12887
12893
  categoriesShown: vaultExists
12888
12894
  });
12895
+ log2.debug("First message processed");
12889
12896
  }
12890
- } catch (error45) {}
12897
+ } catch (error45) {
12898
+ log2.error("Error in chat.message", { error: String(error45) });
12899
+ }
12891
12900
  },
12892
12901
  event: async ({ event }) => {
12893
12902
  try {
12894
12903
  if (event.type === "session.created") {
12904
+ log2.debug("session.created event");
12895
12905
  const eventData = event;
12896
12906
  const sessionId = eventData.properties?.info?.id;
12897
12907
  if (!sessionId) {
12898
- const errorMsg = `\u274C Could not extract session ID from session.created event`;
12908
+ const errorMsg = "Could not extract session ID from session.created event";
12909
+ log2.error(errorMsg);
12899
12910
  throw new Error(errorMsg);
12900
12911
  }
12912
+ log2.debug("Extracted session ID", { sessionId });
12901
12913
  clearJsonl("session-state.jsonl");
12902
12914
  clearJsonl("knowledge-reads.jsonl");
12903
- if (existsSync6(".opencode/knowledge/vault")) {
12915
+ if (existsSync5(".opencode/knowledge/vault")) {
12916
+ log2.debug("Building knowledge index...");
12904
12917
  try {
12905
12918
  const catalog = buildKnowledgeCatalog();
12906
12919
  saveCatalog(catalog);
12907
12920
  const packagesCount = Object.values(catalog.knowledge).reduce((sum, packages) => sum + Object.keys(packages).length, 0);
12908
- } catch (error45) {}
12921
+ log2.info("Knowledge catalog built", { packagesCount });
12922
+ } catch (error45) {
12923
+ log2.warn("Failed to build knowledge catalog", { error: String(error45) });
12924
+ }
12909
12925
  }
12910
12926
  await createSessionState(sessionId);
12911
- const state = getSessionState(sessionId);
12927
+ log2.debug("Session state created");
12912
12928
  }
12913
- } catch (error45) {}
12929
+ } catch (error45) {
12930
+ log2.error("Error in event", { error: String(error45) });
12931
+ }
12914
12932
  }
12915
12933
  };
12916
12934
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-knowledge",
3
- "version": "0.5.1",
3
+ "version": "0.6.0-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.