opencode-knowledge 0.1.0
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/LICENSE +21 -0
- package/README.md +88 -0
- package/dist/command/context-test.md +5 -0
- package/dist/command/hello.md +5 -0
- package/dist/command/hook-test.md +5 -0
- package/dist/command/personality.md +9 -0
- package/dist/index.js +218 -0
- package/dist/templates/abbreviated.template.md +8 -0
- package/dist/templates/first-prompt.template.md +18 -0
- package/dist/templates/personalities/cthulhu.txt +1 -0
- package/dist/templates/personalities/staff_engineer.txt +1 -0
- package/package.json +44 -0
- package/src/version.ts +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 zenobi.us
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# opencode-knowledge
|
|
2
|
+
|
|
3
|
+
An OpenCode plugin
|
|
4
|
+
|
|
5
|
+
> An OpenCode plugin created from the [opencode-plugin-template](https://github.com/zenobi-us/opencode-plugin-template)
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- ๐๏ธ TypeScript-based plugin architecture
|
|
10
|
+
- ๐ง Mise task runner integration
|
|
11
|
+
- ๐ฆ Bun/npm build tooling
|
|
12
|
+
- โจ ESLint + Prettier formatting
|
|
13
|
+
- ๐งช Vitest testing setup
|
|
14
|
+
- ๐ GitHub Actions CI/CD
|
|
15
|
+
- ๐ Release automation with release-please
|
|
16
|
+
|
|
17
|
+
## Getting Started
|
|
18
|
+
|
|
19
|
+
1. **Clone this template:**
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cp -r opencode-plugin-template your-plugin-name
|
|
23
|
+
cd your-plugin-name
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
2. **Update package.json:**
|
|
27
|
+
- Change `name` to your plugin name
|
|
28
|
+
- Update `description`
|
|
29
|
+
- Update `repository.url`
|
|
30
|
+
|
|
31
|
+
3. **Install dependencies:**
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bun install
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
4. **Implement your plugin in `src/index.ts`:**
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import type { Plugin } from '@opencode-ai/plugin';
|
|
41
|
+
|
|
42
|
+
export const YourPlugin: Plugin = async (ctx) => {
|
|
43
|
+
return {
|
|
44
|
+
tool: {
|
|
45
|
+
// Your plugin tools here
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
5. **Test your plugin:**
|
|
52
|
+
```bash
|
|
53
|
+
mise run test
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
- `mise run build` - Build the plugin
|
|
59
|
+
- `mise run test` - Run tests
|
|
60
|
+
- `mise run lint` - Lint code
|
|
61
|
+
- `mise run lint:fix` - Fix linting issues
|
|
62
|
+
- `mise run format` - Format code with Prettier
|
|
63
|
+
|
|
64
|
+
## Installation in OpenCode
|
|
65
|
+
|
|
66
|
+
Create or edit `~/.config/opencode/config.json`:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"plugins": ["opencode-knowledge"]
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Author
|
|
75
|
+
|
|
76
|
+
msegoviadev <marcos@msegovia.dev>
|
|
77
|
+
|
|
78
|
+
## Repository
|
|
79
|
+
|
|
80
|
+
https://github.com/msegoviadev/opencode-knowledge
|
|
81
|
+
|
|
82
|
+
## Contributing
|
|
83
|
+
|
|
84
|
+
Contributions are welcome! Please file issues or submit pull requests on the GitHub repository.
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT License. See the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Display current configured personality and debug plugin state
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## description: Display the current configured personality
|
|
6
|
+
|
|
7
|
+
This command displays your current personality configuration and asks the AI to introduce itself according to that role.
|
|
8
|
+
|
|
9
|
+
The personality context should be automatically injected above this message if the plugin is working correctly.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/index.ts
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
6
|
+
import { existsSync as existsSync2 } from "fs";
|
|
7
|
+
|
|
8
|
+
// src/lib/session-state.ts
|
|
9
|
+
import { readFile } from "fs/promises";
|
|
10
|
+
import { existsSync } from "fs";
|
|
11
|
+
var sessionStates = new Map;
|
|
12
|
+
async function createSessionState(sessionId) {
|
|
13
|
+
if (sessionStates.has(sessionId)) {
|
|
14
|
+
throw new Error(`Session state already exists for session: ${sessionId}`);
|
|
15
|
+
}
|
|
16
|
+
const settingsPath = ".opencode/knowledge/settings.json";
|
|
17
|
+
if (!existsSync(settingsPath)) {
|
|
18
|
+
throw new Error(`CONFIGURATION ERROR: Cannot create session state - settings file not found at ${settingsPath}`);
|
|
19
|
+
}
|
|
20
|
+
let role;
|
|
21
|
+
try {
|
|
22
|
+
const settingsContent = await readFile(settingsPath, "utf-8");
|
|
23
|
+
const settings = JSON.parse(settingsContent);
|
|
24
|
+
if (!settings.role) {
|
|
25
|
+
throw new Error(`CONFIGURATION ERROR: Cannot create session state - missing 'role' field in ${settingsPath}`);
|
|
26
|
+
}
|
|
27
|
+
role = settings.role;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error instanceof Error && error.message.includes("CONFIGURATION ERROR")) {
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`Error reading settings.json: ${error}`);
|
|
33
|
+
}
|
|
34
|
+
const state = {
|
|
35
|
+
role,
|
|
36
|
+
isFirstPrompt: true,
|
|
37
|
+
loadedPackages: new Set,
|
|
38
|
+
createdAt: new Date
|
|
39
|
+
};
|
|
40
|
+
sessionStates.set(sessionId, state);
|
|
41
|
+
}
|
|
42
|
+
function getSessionState(sessionId) {
|
|
43
|
+
const state = sessionStates.get(sessionId);
|
|
44
|
+
if (!state) {
|
|
45
|
+
throw new Error(`Session state not found for session: ${sessionId}`);
|
|
46
|
+
}
|
|
47
|
+
return state;
|
|
48
|
+
}
|
|
49
|
+
function updateSessionState(sessionId, updates) {
|
|
50
|
+
const state = getSessionState(sessionId);
|
|
51
|
+
Object.assign(state, updates);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/index.ts
|
|
55
|
+
var __dirname = "/Users/marcos/workspace/ai/opencode-knowledge-2/src";
|
|
56
|
+
var logToFile = async (message) => {
|
|
57
|
+
try {
|
|
58
|
+
const logEntry = `${new Date().toISOString()}: ${message}
|
|
59
|
+
`;
|
|
60
|
+
let existing = "";
|
|
61
|
+
try {
|
|
62
|
+
existing = await Bun.file("/tmp/opencode-knowledge-debug.log").text();
|
|
63
|
+
} catch {}
|
|
64
|
+
await Bun.write("/tmp/opencode-knowledge-debug.log", existing + logEntry);
|
|
65
|
+
} catch (e) {}
|
|
66
|
+
};
|
|
67
|
+
function parseFrontmatter(content) {
|
|
68
|
+
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
|
69
|
+
const match = content.match(frontmatterRegex);
|
|
70
|
+
if (!match) {
|
|
71
|
+
return { frontmatter: {}, body: content.trim() };
|
|
72
|
+
}
|
|
73
|
+
const [, yamlContent, body] = match;
|
|
74
|
+
const frontmatter = {};
|
|
75
|
+
for (const line of yamlContent.split(`
|
|
76
|
+
`)) {
|
|
77
|
+
const colonIndex = line.indexOf(":");
|
|
78
|
+
if (colonIndex === -1)
|
|
79
|
+
continue;
|
|
80
|
+
const key = line.slice(0, colonIndex).trim();
|
|
81
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
82
|
+
if (key === "description")
|
|
83
|
+
frontmatter.description = value;
|
|
84
|
+
if (key === "agent")
|
|
85
|
+
frontmatter.agent = value;
|
|
86
|
+
if (key === "model")
|
|
87
|
+
frontmatter.model = value;
|
|
88
|
+
if (key === "subtask")
|
|
89
|
+
frontmatter.subtask = value === "true";
|
|
90
|
+
}
|
|
91
|
+
return { frontmatter, body: body.trim() };
|
|
92
|
+
}
|
|
93
|
+
async function loadCommands() {
|
|
94
|
+
const commands = [];
|
|
95
|
+
const __dirname2 = path.dirname(fileURLToPath(import.meta.url));
|
|
96
|
+
let commandDir = path.join(__dirname2, "command");
|
|
97
|
+
if (!existsSync2(commandDir)) {
|
|
98
|
+
commandDir = path.join(__dirname2, "..", "command");
|
|
99
|
+
}
|
|
100
|
+
const glob = new Bun.Glob("**/*.md");
|
|
101
|
+
for await (const file of glob.scan({ cwd: commandDir, absolute: true })) {
|
|
102
|
+
const content = await Bun.file(file).text();
|
|
103
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
104
|
+
const relativePath = path.relative(commandDir, file);
|
|
105
|
+
const name = relativePath.replace(/\.md$/, "").replace(/\//g, "-");
|
|
106
|
+
commands.push({
|
|
107
|
+
name,
|
|
108
|
+
frontmatter,
|
|
109
|
+
template: body
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return commands;
|
|
113
|
+
}
|
|
114
|
+
async function loadPersonality() {
|
|
115
|
+
const settingsPath = ".opencode/knowledge/settings.json";
|
|
116
|
+
if (!existsSync2(settingsPath)) {
|
|
117
|
+
const errorMsg = `\u274C CONFIGURATION ERROR: Settings file not found at ${settingsPath}. Please create this file with {"role": "your_role"}`;
|
|
118
|
+
await logToFile(errorMsg);
|
|
119
|
+
throw new Error(errorMsg);
|
|
120
|
+
}
|
|
121
|
+
let role;
|
|
122
|
+
try {
|
|
123
|
+
const settingsContent = await readFile2(settingsPath, "utf-8");
|
|
124
|
+
const settings = JSON.parse(settingsContent);
|
|
125
|
+
if (!settings.role) {
|
|
126
|
+
const errorMsg = `\u274C CONFIGURATION ERROR: Settings file exists but missing 'role' field. Please add {"role": "your_role"} to ${settingsPath}`;
|
|
127
|
+
await logToFile(errorMsg);
|
|
128
|
+
throw new Error(errorMsg);
|
|
129
|
+
}
|
|
130
|
+
role = settings.role;
|
|
131
|
+
await logToFile(`\u2705 Settings loaded: role="${role}" from ${settingsPath}`);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error instanceof Error && error.message.includes("CONFIGURATION ERROR")) {
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
const errorMsg = `\u274C Error parsing settings.json: ${error}`;
|
|
137
|
+
await logToFile(errorMsg);
|
|
138
|
+
throw new Error(errorMsg);
|
|
139
|
+
}
|
|
140
|
+
const personalityPath = path.join(__dirname, "..", "templates", "personalities", `${role}.txt`);
|
|
141
|
+
if (!existsSync2(personalityPath)) {
|
|
142
|
+
const errorMsg = `\u274C CONFIGURATION ERROR: Personality file not found at ${personalityPath}. Available roles should have a corresponding .txt file in templates/personalities/`;
|
|
143
|
+
await logToFile(errorMsg);
|
|
144
|
+
throw new Error(errorMsg);
|
|
145
|
+
}
|
|
146
|
+
const content = await readFile2(personalityPath, "utf-8");
|
|
147
|
+
await logToFile(`\u2705 Personality loaded from: ${personalityPath}`);
|
|
148
|
+
return content.trim();
|
|
149
|
+
}
|
|
150
|
+
var opencodeKnowledge = async (ctx) => {
|
|
151
|
+
await logToFile("Plugin initialized");
|
|
152
|
+
const commands = await loadCommands();
|
|
153
|
+
return {
|
|
154
|
+
async config(config) {
|
|
155
|
+
config.command = config.command ?? {};
|
|
156
|
+
for (const cmd of commands) {
|
|
157
|
+
let template = cmd.template;
|
|
158
|
+
template = template.replace("{{CURRENT_TIME}}", new Date().toISOString());
|
|
159
|
+
config.command[cmd.name] = {
|
|
160
|
+
template,
|
|
161
|
+
description: cmd.frontmatter.description,
|
|
162
|
+
agent: cmd.frontmatter.agent,
|
|
163
|
+
model: cmd.frontmatter.model,
|
|
164
|
+
subtask: cmd.frontmatter.subtask
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"chat.message": async (input, output) => {
|
|
169
|
+
try {
|
|
170
|
+
const state = getSessionState(input.sessionID);
|
|
171
|
+
if (state.isFirstPrompt) {
|
|
172
|
+
await logToFile(`\uD83C\uDFAF First message in session ${input.sessionID} - injecting personality`);
|
|
173
|
+
const personality = await loadPersonality();
|
|
174
|
+
const personalityPart = {
|
|
175
|
+
type: "text",
|
|
176
|
+
text: `## Role Context
|
|
177
|
+
|
|
178
|
+
${personality}`,
|
|
179
|
+
id: `personality-${Date.now()}`,
|
|
180
|
+
sessionID: input.sessionID,
|
|
181
|
+
messageID: input.messageID || ""
|
|
182
|
+
};
|
|
183
|
+
output.parts.push(personalityPart);
|
|
184
|
+
updateSessionState(input.sessionID, { isFirstPrompt: false });
|
|
185
|
+
await logToFile("\u2705 Personality injected on first message!");
|
|
186
|
+
} else {
|
|
187
|
+
await logToFile(`\u23ED\uFE0F Subsequent message - skipping personality injection`);
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
await logToFile(`\u274C Error in chat.message: ${error}`);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
event: async ({ event }) => {
|
|
194
|
+
try {
|
|
195
|
+
if (event.type === "session.created") {
|
|
196
|
+
await logToFile("\uD83D\uDE80 session.created event");
|
|
197
|
+
const eventData = event;
|
|
198
|
+
const sessionId = eventData.properties?.info?.id;
|
|
199
|
+
if (!sessionId) {
|
|
200
|
+
const errorMsg = `\u274C Could not extract session ID from session.created event`;
|
|
201
|
+
await logToFile(errorMsg);
|
|
202
|
+
await logToFile(`Event structure: ${JSON.stringify(eventData)}`);
|
|
203
|
+
throw new Error(errorMsg);
|
|
204
|
+
}
|
|
205
|
+
await logToFile(`\u2705 Extracted session ID: ${sessionId}`);
|
|
206
|
+
await createSessionState(sessionId);
|
|
207
|
+
const state = getSessionState(sessionId);
|
|
208
|
+
await logToFile(`\u2705 Session state created for session ${sessionId} with role: ${state.role}`);
|
|
209
|
+
}
|
|
210
|
+
} catch (error) {
|
|
211
|
+
await logToFile(`\u274C Error in event: ${error}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
export {
|
|
217
|
+
opencodeKnowledge
|
|
218
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
## Knowledge Context
|
|
2
|
+
|
|
3
|
+
You have access to an engineering knowledge base. Core areas include **{{CORE_TAGS}}** and key packages: **{{CORE_PACKAGES}}**.
|
|
4
|
+
|
|
5
|
+
### Your Task
|
|
6
|
+
{{USER_PROMPT}}
|
|
7
|
+
|
|
8
|
+
Use knowledge_load and knowledge_search tools to access relevant information as needed.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
## Knowledge Context
|
|
2
|
+
|
|
3
|
+
You have access to a comprehensive knowledge base containing {{CATEGORIES_COUNT}} main categories of engineering knowledge:
|
|
4
|
+
|
|
5
|
+
{{FORMATTED_MAP}}
|
|
6
|
+
|
|
7
|
+
### Core Knowledge Areas
|
|
8
|
+
**Tags:** {{CORE_TAGS}}
|
|
9
|
+
**Packages:**
|
|
10
|
+
{{CORE_PACKAGES_LIST}}
|
|
11
|
+
|
|
12
|
+
### Your Task
|
|
13
|
+
{{USER_PROMPT}}
|
|
14
|
+
|
|
15
|
+
### Knowledge Loading
|
|
16
|
+
You can load specific knowledge packages using the knowledge_load tool when relevant to the task at hand. Focus on packages that match the technical domain or problem you're solving.
|
|
17
|
+
|
|
18
|
+
Available knowledge packages can be discovered by searching for specific tags, technologies, or domains using the knowledge_search tool.
|
|
@@ -0,0 +1 @@
|
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
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.
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-knowledge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "An OpenCode plugin",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "msegoviadev",
|
|
7
|
+
"email": "marcos@msegovia.dev"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/msegoviadev/opencode-knowledge"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"provenance": true
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src/version.ts"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@opencode-ai/plugin": "1.0.85"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@eslint/js": "^9.39.1",
|
|
33
|
+
"@types/node": "^20.11.5",
|
|
34
|
+
"@typescript-eslint/eslint-plugin": "8.47.0",
|
|
35
|
+
"@typescript-eslint/parser": "8.47.0",
|
|
36
|
+
"bun-types": "latest",
|
|
37
|
+
"eslint": "^9.39.1",
|
|
38
|
+
"eslint-config-prettier": "10.1.8",
|
|
39
|
+
"eslint-plugin-prettier": "^5.1.3",
|
|
40
|
+
"prettier": "^3.2.4",
|
|
41
|
+
"typescript-eslint": "^8.47.0",
|
|
42
|
+
"vitest": "^3.2.4"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/version.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const VERSION = '0.1.0';
|