@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 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 `skills/core/SKILL.md` (from `PAI_DIR`) into the system prompt.
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 skills and history | `$XDG_CONFIG_HOME/opencode` |
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 skills and history.
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/skills/core/SKILL.md`.
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, 'skills', 'core'),
12
- join(PAI_DIR, 'history', 'raw-outputs'),
13
- join(PAI_DIR, 'history', 'sessions'),
14
- join(PAI_DIR, 'history', 'system-logs'),
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, 'skills', 'core', 'SKILL.md');
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/skills/core/SKILL.md
98
+ // Load CORE skill content from $PAI_DIR/skill/core/SKILL.md
93
99
  let coreSkillContent = '';
94
- const coreSkillPath = join(PAI_DIR, 'skills', 'core', 'SKILL.md');
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
- if (info?.author === 'assistant' && info?.content) {
159
- const content = typeof info.content === 'string' ? info.content : '';
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 = content.match(/(?:🎯\s*)?COMPLETED:\s*(.+?)(?:\n|$)/i);
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
  }
@@ -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;
@@ -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, 'skills');
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@fpr1m3/opencode-pai-plugin",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
5
  "description": "Personal AI Infrastructure (PAI) plugin for OpenCode",
6
6
  "main": "dist/index.js",
@@ -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
- }
@@ -1 +0,0 @@
1
- export declare function notifyVoiceServer(message: string): Promise<void>;
@@ -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
- }