@fpr1m3/opencode-pai-plugin 1.0.1 → 1.1.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 +6 -5
- package/dist/index.js +25 -12
- package/dist/lib/logger.d.ts +5 -0
- package/dist/lib/logger.js +103 -0
- package/dist/lib/paths.js +1 -1
- package/package.json +1 -1
- package/dist/lib/context-loader.d.ts +0 -1
- package/dist/lib/context-loader.js +0 -21
- package/dist/lib/notifier.d.ts +0 -1
- package/dist/lib/notifier.js +0 -12
package/README.md
CHANGED
|
@@ -13,12 +13,13 @@ This project is an OpenCode-compatible clone of the hook system from **Dan Miess
|
|
|
13
13
|
## Features
|
|
14
14
|
|
|
15
15
|
### 1. Identity & Context Injection
|
|
16
|
-
* **Core Skill Loading**: Automatically injects your `
|
|
16
|
+
* **Core Skill Loading**: Automatically injects your `skill/core/SKILL.md` (from `PAI_DIR`) into the system prompt.
|
|
17
17
|
* **Dynamic Substitution**: Supports placeholders like `{{DA}}`, `{{DA_COLOR}}`, and `{{ENGINEER_NAME}}` for personalized interactions.
|
|
18
18
|
* **Project Requirements**: Automatically detects and loads `.opencode/dynamic-requirements.md` from your current project, allowing for task-specific instructions.
|
|
19
19
|
|
|
20
|
-
### 2. Intelligent History & Logging
|
|
20
|
+
### 2. Intelligent History & Logging (UOCS)
|
|
21
21
|
* **Real-time Event Capture**: Logs all tool calls and SDK events to `PAI_DIR/history/raw-outputs` in an analytics-ready JSONL format.
|
|
22
|
+
* **Universal Output Capture System (UOCS)**: Automatically parses assistant responses for structured sections (SUMMARY, ANALYSIS, etc.) and generates artifacts in `decisions/`, `learnings/`, `research/`, or `execution/` based on context.
|
|
22
23
|
* **Session Summaries**: Generates human-readable Markdown summaries in `PAI_DIR/history/sessions` at the end of every session, tracking files modified, tools used, and commands executed.
|
|
23
24
|
* **Agent Mapping**: Tracks session-to-agent relationships (e.g., mapping a subagent session to its specialized type).
|
|
24
25
|
|
|
@@ -36,7 +37,7 @@ The plugin centers around the `PAI_DIR` environment variable.
|
|
|
36
37
|
|
|
37
38
|
| Variable | Description | Default |
|
|
38
39
|
| :--- | :--- | :--- |
|
|
39
|
-
| `PAI_DIR` | Root directory for PAI
|
|
40
|
+
| `PAI_DIR` | Root directory for PAI skill and history | `$XDG_CONFIG_HOME/opencode` |
|
|
40
41
|
| `DA` | Name of your Digital Assistant | `PAI` |
|
|
41
42
|
| `ENGINEER_NAME` | Your name/identity | `Operator` |
|
|
42
43
|
| `DA_COLOR` | UI color theme for your DA | `blue` |
|
|
@@ -55,7 +56,7 @@ Add the plugin to your global `opencode.json` configuration file (typically loca
|
|
|
55
56
|
|
|
56
57
|
Upon first run, the plugin will automatically:
|
|
57
58
|
1. Detect or create your `PAI_DIR` (default: `$XDG_CONFIG_HOME/opencode`).
|
|
58
|
-
2. Initialize the required directory structure for
|
|
59
|
+
2. Initialize the required directory structure for skill and history.
|
|
59
60
|
3. Create a default `SKILL.md` core identity if one does not exist.
|
|
60
61
|
|
|
61
62
|
## Development & Testing
|
|
@@ -72,7 +73,7 @@ We provide scripts to verify the plugin in a pristine environment:
|
|
|
72
73
|
|
|
73
74
|
---
|
|
74
75
|
|
|
75
|
-
**Note**: This plugin is designed to work with the PAI ecosystem. While it auto-initializes a basic structure, you can customize your identity by editing `$PAI_DIR/
|
|
76
|
+
**Note**: This plugin is designed to work with the PAI ecosystem. While it auto-initializes a basic structure, you can customize your identity by editing `$PAI_DIR/skill/core/SKILL.md`.
|
|
76
77
|
|
|
77
78
|
---
|
|
78
79
|
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Logger } from './lib/logger';
|
|
2
|
-
import { PAI_DIR } from './lib/paths';
|
|
2
|
+
import { PAI_DIR, HISTORY_DIR } from './lib/paths';
|
|
3
3
|
import { validateCommand } from './lib/security';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
@@ -8,10 +8,16 @@ import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
|
8
8
|
*/
|
|
9
9
|
function ensurePAIStructure() {
|
|
10
10
|
const dirs = [
|
|
11
|
-
join(PAI_DIR, '
|
|
12
|
-
join(
|
|
13
|
-
join(
|
|
14
|
-
join(
|
|
11
|
+
join(PAI_DIR, 'skill', 'core'),
|
|
12
|
+
join(HISTORY_DIR, 'raw-outputs'),
|
|
13
|
+
join(HISTORY_DIR, 'sessions'),
|
|
14
|
+
join(HISTORY_DIR, 'learnings'),
|
|
15
|
+
join(HISTORY_DIR, 'decisions'),
|
|
16
|
+
join(HISTORY_DIR, 'research'),
|
|
17
|
+
join(HISTORY_DIR, 'execution', 'features'),
|
|
18
|
+
join(HISTORY_DIR, 'execution', 'bugs'),
|
|
19
|
+
join(HISTORY_DIR, 'execution', 'refactors'),
|
|
20
|
+
join(HISTORY_DIR, 'system-logs'),
|
|
15
21
|
];
|
|
16
22
|
for (const dir of dirs) {
|
|
17
23
|
if (!existsSync(dir)) {
|
|
@@ -24,7 +30,7 @@ function ensurePAIStructure() {
|
|
|
24
30
|
}
|
|
25
31
|
}
|
|
26
32
|
}
|
|
27
|
-
const coreSkillPath = join(PAI_DIR, '
|
|
33
|
+
const coreSkillPath = join(PAI_DIR, 'skill', 'core', 'SKILL.md');
|
|
28
34
|
if (!existsSync(coreSkillPath)) {
|
|
29
35
|
const defaultSkill = `# PAI Core Identity
|
|
30
36
|
You are {{DA}}, a Personal AI Infrastructure.
|
|
@@ -89,9 +95,9 @@ export const PAIPlugin = async ({ worktree }) => {
|
|
|
89
95
|
let currentSessionId = null;
|
|
90
96
|
// Auto-initialize PAI infrastructure if needed
|
|
91
97
|
ensurePAIStructure();
|
|
92
|
-
// Load CORE skill content from $PAI_DIR/
|
|
98
|
+
// Load CORE skill content from $PAI_DIR/skill/core/SKILL.md
|
|
93
99
|
let coreSkillContent = '';
|
|
94
|
-
const coreSkillPath = join(PAI_DIR, '
|
|
100
|
+
const coreSkillPath = join(PAI_DIR, 'skill', 'core', 'SKILL.md');
|
|
95
101
|
if (existsSync(coreSkillPath)) {
|
|
96
102
|
try {
|
|
97
103
|
coreSkillContent = readFileSync(coreSkillPath, 'utf-8');
|
|
@@ -152,18 +158,25 @@ export const PAIPlugin = async ({ worktree }) => {
|
|
|
152
158
|
process.stderr.write(`\x1b]0;Agent: ${type}...\x07`);
|
|
153
159
|
}
|
|
154
160
|
}
|
|
155
|
-
// Handle assistant completion (Tab Titles)
|
|
161
|
+
// Handle assistant completion (Tab Titles & UOCS)
|
|
156
162
|
if (event.type === 'message.updated') {
|
|
157
163
|
const info = anyEvent.properties?.info;
|
|
158
|
-
|
|
159
|
-
|
|
164
|
+
const role = info?.role || info?.author;
|
|
165
|
+
if (role === 'assistant') {
|
|
166
|
+
// Robust content extraction
|
|
167
|
+
const content = info?.content || info?.text || '';
|
|
168
|
+
const contentStr = typeof content === 'string' ? content : '';
|
|
160
169
|
// Look for COMPLETED: line (can be prefaced by 🎯 or just text)
|
|
161
|
-
const completedMatch =
|
|
170
|
+
const completedMatch = contentStr.match(/(?:🎯\s*)?COMPLETED:\s*(.+?)(?:\n|$)/i);
|
|
162
171
|
if (completedMatch) {
|
|
163
172
|
const completedLine = completedMatch[1].trim();
|
|
164
173
|
// Set Tab Title
|
|
165
174
|
const tabTitle = generateTabTitle(completedLine);
|
|
166
175
|
process.stderr.write(`\x1b]0;${tabTitle}\x07`);
|
|
176
|
+
// UOCS: Process response for artifact generation
|
|
177
|
+
if (logger && contentStr) {
|
|
178
|
+
await logger.processAssistantMessage(contentStr);
|
|
179
|
+
}
|
|
167
180
|
}
|
|
168
181
|
}
|
|
169
182
|
}
|
package/dist/lib/logger.d.ts
CHANGED
|
@@ -29,6 +29,11 @@ export declare class Logger {
|
|
|
29
29
|
metadata: any;
|
|
30
30
|
}): void;
|
|
31
31
|
generateSessionSummary(): Promise<string | null>;
|
|
32
|
+
processAssistantMessage(content: string): Promise<void>;
|
|
33
|
+
private parseStructuredResponse;
|
|
34
|
+
private isLearningCapture;
|
|
35
|
+
private determineArtifactType;
|
|
36
|
+
private createArtifact;
|
|
32
37
|
logError(context: string, error: any): void;
|
|
33
38
|
private writeEvent;
|
|
34
39
|
flush(): void;
|
package/dist/lib/logger.js
CHANGED
|
@@ -208,6 +208,109 @@ This session summary was automatically generated by the PAI OpenCode Plugin.
|
|
|
208
208
|
return null;
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
|
+
async processAssistantMessage(content) {
|
|
212
|
+
try {
|
|
213
|
+
const sections = this.parseStructuredResponse(content);
|
|
214
|
+
if (Object.keys(sections).length === 0)
|
|
215
|
+
return;
|
|
216
|
+
const agentRole = this.getAgentForSession(this.sessionId);
|
|
217
|
+
const isLearning = this.isLearningCapture(sections);
|
|
218
|
+
const type = this.determineArtifactType(agentRole, isLearning, sections);
|
|
219
|
+
await this.createArtifact(type, content, sections);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
this.logError('ProcessAssistantMessage', error);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
parseStructuredResponse(content) {
|
|
226
|
+
const sections = {};
|
|
227
|
+
const sectionHeaders = [
|
|
228
|
+
'SUMMARY', 'ANALYSIS', 'ACTIONS', 'RESULTS', 'STATUS', 'CAPTURE', 'NEXT', 'STORY EXPLANATION', 'COMPLETED'
|
|
229
|
+
];
|
|
230
|
+
for (const header of sectionHeaders) {
|
|
231
|
+
const regex = new RegExp(`${header}:\\s*([\\s\\S]*?)(?=\\n(?:${sectionHeaders.join('|')}):|$)`, 'i');
|
|
232
|
+
const match = content.match(regex);
|
|
233
|
+
if (match && match[1]) {
|
|
234
|
+
sections[header] = match[1].trim();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return sections;
|
|
238
|
+
}
|
|
239
|
+
isLearningCapture(sections) {
|
|
240
|
+
const indicators = ['fixed', 'solved', 'discovered', 'lesson', 'troubleshoot', 'debug', 'root cause', 'learning', 'bug', 'issue', 'resolved'];
|
|
241
|
+
const textToSearch = ((sections['ANALYSIS'] || '') + ' ' + (sections['RESULTS'] || '')).toLowerCase();
|
|
242
|
+
let count = 0;
|
|
243
|
+
for (const indicator of indicators) {
|
|
244
|
+
if (textToSearch.includes(indicator)) {
|
|
245
|
+
count++;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return count >= 2;
|
|
249
|
+
}
|
|
250
|
+
determineArtifactType(agentRole, isLearning, sections) {
|
|
251
|
+
const summary = (sections['SUMMARY'] || '').toLowerCase();
|
|
252
|
+
if (agentRole === 'architect')
|
|
253
|
+
return 'DECISION';
|
|
254
|
+
if (agentRole === 'researcher' || agentRole === 'pentester')
|
|
255
|
+
return 'RESEARCH';
|
|
256
|
+
if (agentRole === 'engineer' || agentRole === 'designer') {
|
|
257
|
+
if (summary.includes('fix') || summary.includes('bug') || summary.includes('issue'))
|
|
258
|
+
return 'BUG';
|
|
259
|
+
if (summary.includes('refactor') || summary.includes('improve') || summary.includes('cleanup'))
|
|
260
|
+
return 'REFACTOR';
|
|
261
|
+
return 'FEATURE';
|
|
262
|
+
}
|
|
263
|
+
return isLearning ? 'LEARNING' : 'WORK';
|
|
264
|
+
}
|
|
265
|
+
async createArtifact(type, content, sections) {
|
|
266
|
+
const now = new Date();
|
|
267
|
+
const timestamp = now.toISOString()
|
|
268
|
+
.replace(/:/g, '')
|
|
269
|
+
.replace(/\..+/, '')
|
|
270
|
+
.replace('T', '-');
|
|
271
|
+
const yearMonth = timestamp.substring(0, 7);
|
|
272
|
+
const summary = sections['SUMMARY'] || 'no-summary';
|
|
273
|
+
const slug = summary.toLowerCase()
|
|
274
|
+
.replace(/[^\w\s-]/g, '')
|
|
275
|
+
.replace(/\s+/g, '-')
|
|
276
|
+
.substring(0, 50);
|
|
277
|
+
const filename = `${timestamp}_${type}_${slug}.md`;
|
|
278
|
+
let subdir = 'execution';
|
|
279
|
+
if (type === 'LEARNING')
|
|
280
|
+
subdir = 'learnings';
|
|
281
|
+
else if (type === 'DECISION')
|
|
282
|
+
subdir = 'decisions';
|
|
283
|
+
else if (type === 'RESEARCH')
|
|
284
|
+
subdir = 'research';
|
|
285
|
+
else if (type === 'WORK')
|
|
286
|
+
subdir = 'sessions';
|
|
287
|
+
else {
|
|
288
|
+
// For BUG, REFACTOR, FEATURE
|
|
289
|
+
if (type === 'BUG')
|
|
290
|
+
subdir = join('execution', 'bugs');
|
|
291
|
+
else if (type === 'REFACTOR')
|
|
292
|
+
subdir = join('execution', 'refactors');
|
|
293
|
+
else
|
|
294
|
+
subdir = join('execution', 'features');
|
|
295
|
+
}
|
|
296
|
+
const targetDir = join(HISTORY_DIR, subdir, yearMonth);
|
|
297
|
+
if (!existsSync(targetDir)) {
|
|
298
|
+
mkdirSync(targetDir, { recursive: true });
|
|
299
|
+
}
|
|
300
|
+
const filePath = join(targetDir, filename);
|
|
301
|
+
const agentRole = this.getAgentForSession(this.sessionId);
|
|
302
|
+
const frontmatter = `---
|
|
303
|
+
capture_type: ${type}
|
|
304
|
+
timestamp: ${new Date().toISOString()}
|
|
305
|
+
session_id: ${this.sessionId}
|
|
306
|
+
executor: ${agentRole}
|
|
307
|
+
${sections['SUMMARY'] ? `summary: ${sections['SUMMARY'].replace(/"/g, '\\"')}` : ''}
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
${content}
|
|
311
|
+
`;
|
|
312
|
+
writeFileSync(filePath, redactString(frontmatter), 'utf-8');
|
|
313
|
+
}
|
|
211
314
|
logError(context, error) {
|
|
212
315
|
try {
|
|
213
316
|
const now = new Date();
|
package/dist/lib/paths.js
CHANGED
|
@@ -25,7 +25,7 @@ export const PAI_DIR = process.env.PAI_DIR
|
|
|
25
25
|
* Common PAI directories
|
|
26
26
|
*/
|
|
27
27
|
export const HOOKS_DIR = join(PAI_DIR, 'hooks');
|
|
28
|
-
export const SKILLS_DIR = join(PAI_DIR, '
|
|
28
|
+
export const SKILLS_DIR = join(PAI_DIR, 'skill');
|
|
29
29
|
export const AGENTS_DIR = join(PAI_DIR, 'agents');
|
|
30
30
|
export const HISTORY_DIR = join(PAI_DIR, 'history');
|
|
31
31
|
export const COMMANDS_DIR = join(PAI_DIR, 'commands');
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function getCoreContext(baseDir: string, env: NodeJS.ProcessEnv): string;
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import { getSkillsDir } from './paths';
|
|
4
|
-
export function getCoreContext(baseDir, env) {
|
|
5
|
-
const skillsDir = getSkillsDir(baseDir);
|
|
6
|
-
const coreSkillPath = join(skillsDir, 'core', 'SKILL.md');
|
|
7
|
-
if (!existsSync(coreSkillPath)) {
|
|
8
|
-
console.warn(`Core skill file not found at ${coreSkillPath}`);
|
|
9
|
-
return '';
|
|
10
|
-
}
|
|
11
|
-
let content = readFileSync(coreSkillPath, 'utf-8');
|
|
12
|
-
// Variable replacement
|
|
13
|
-
const replacements = {
|
|
14
|
-
'{{DA}}': env.DA_NAME || 'PAI',
|
|
15
|
-
'{{ENGINEER_NAME}}': env.USER_NAME || env.USER || 'Engineer',
|
|
16
|
-
};
|
|
17
|
-
for (const [key, value] of Object.entries(replacements)) {
|
|
18
|
-
content = content.replaceAll(key, value);
|
|
19
|
-
}
|
|
20
|
-
return content;
|
|
21
|
-
}
|
package/dist/lib/notifier.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function notifyVoiceServer(message: string): Promise<void>;
|
package/dist/lib/notifier.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export async function notifyVoiceServer(message) {
|
|
2
|
-
try {
|
|
3
|
-
await fetch('http://localhost:8888/notify', {
|
|
4
|
-
method: 'POST',
|
|
5
|
-
headers: { 'Content-Type': 'application/json' },
|
|
6
|
-
body: JSON.stringify({ message }),
|
|
7
|
-
});
|
|
8
|
-
}
|
|
9
|
-
catch (error) {
|
|
10
|
-
// Ignore errors if server is not running
|
|
11
|
-
}
|
|
12
|
-
}
|