@fpr1m3/opencode-pai-plugin 1.0.1 → 1.2.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/README.md +6 -5
- package/dist/index.js +95 -34
- package/dist/lib/logger.d.ts +9 -8
- package/dist/lib/logger.js +145 -58
- 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.
|
|
@@ -85,13 +91,17 @@ function generateTabTitle(completedLine) {
|
|
|
85
91
|
return 'PAI Task Done';
|
|
86
92
|
}
|
|
87
93
|
export const PAIPlugin = async ({ worktree }) => {
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
const loggers = new Map();
|
|
95
|
+
// Track the latest text content for each message (from streaming parts)
|
|
96
|
+
// Key: messageID, Value: latest full text from part.text
|
|
97
|
+
const messageTextCache = new Map();
|
|
98
|
+
// Track which messages we've already processed for archival (deduplication)
|
|
99
|
+
const processedMessageIds = new Set();
|
|
90
100
|
// Auto-initialize PAI infrastructure if needed
|
|
91
101
|
ensurePAIStructure();
|
|
92
|
-
// Load CORE skill content from $PAI_DIR/
|
|
102
|
+
// Load CORE skill content from $PAI_DIR/skill/core/SKILL.md
|
|
93
103
|
let coreSkillContent = '';
|
|
94
|
-
const coreSkillPath = join(PAI_DIR, '
|
|
104
|
+
const coreSkillPath = join(PAI_DIR, 'skill', 'core', 'SKILL.md');
|
|
95
105
|
if (existsSync(coreSkillPath)) {
|
|
96
106
|
try {
|
|
97
107
|
coreSkillContent = readFileSync(coreSkillPath, 'utf-8');
|
|
@@ -125,17 +135,33 @@ export const PAIPlugin = async ({ worktree }) => {
|
|
|
125
135
|
const hooks = {
|
|
126
136
|
event: async ({ event }) => {
|
|
127
137
|
const anyEvent = event;
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
138
|
+
// Get Session ID from event (try multiple locations)
|
|
139
|
+
const sessionId = anyEvent.properties?.part?.sessionID ||
|
|
140
|
+
anyEvent.properties?.info?.sessionID ||
|
|
141
|
+
anyEvent.properties?.sessionID ||
|
|
142
|
+
anyEvent.sessionID;
|
|
143
|
+
if (!sessionId)
|
|
144
|
+
return;
|
|
145
|
+
// Initialize Logger if needed
|
|
146
|
+
if (!loggers.has(sessionId)) {
|
|
147
|
+
loggers.set(sessionId, new Logger(sessionId, worktree));
|
|
132
148
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
!shouldSkipEvent(event, currentSessionId)) {
|
|
149
|
+
const logger = loggers.get(sessionId);
|
|
150
|
+
// Handle generic event logging (skip streaming parts to reduce noise)
|
|
151
|
+
if (!shouldSkipEvent(event, sessionId) && event.type !== 'message.part.updated') {
|
|
137
152
|
logger.logOpenCodeEvent(event);
|
|
138
153
|
}
|
|
154
|
+
// STREAMING CAPTURE: Cache the latest text from message.part.updated
|
|
155
|
+
// The part.text field contains the FULL accumulated text, not a delta
|
|
156
|
+
if (event.type === 'message.part.updated') {
|
|
157
|
+
const part = anyEvent.properties?.part;
|
|
158
|
+
const messageId = part?.messageID;
|
|
159
|
+
const partType = part?.type;
|
|
160
|
+
// Only cache text parts (not tool parts)
|
|
161
|
+
if (messageId && partType === 'text' && part?.text) {
|
|
162
|
+
messageTextCache.set(messageId, part.text);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
139
165
|
// Handle real-time tab title updates (Pre-Tool Use)
|
|
140
166
|
if (anyEvent.type === 'tool.call') {
|
|
141
167
|
const props = anyEvent.properties;
|
|
@@ -152,32 +178,67 @@ export const PAIPlugin = async ({ worktree }) => {
|
|
|
152
178
|
process.stderr.write(`\x1b]0;Agent: ${type}...\x07`);
|
|
153
179
|
}
|
|
154
180
|
}
|
|
155
|
-
// Handle assistant completion (Tab Titles)
|
|
181
|
+
// Handle assistant message completion (Tab Titles & Artifact Archival)
|
|
156
182
|
if (event.type === 'message.updated') {
|
|
157
183
|
const info = anyEvent.properties?.info;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
184
|
+
const role = info?.role || info?.author;
|
|
185
|
+
const messageId = info?.id;
|
|
186
|
+
if (role === 'assistant' && messageId) {
|
|
187
|
+
// Get content from our streaming cache first, fallback to info.content
|
|
188
|
+
let contentStr = messageTextCache.get(messageId) || '';
|
|
189
|
+
// Fallback: try to get content from the event itself
|
|
190
|
+
if (!contentStr) {
|
|
191
|
+
const content = info?.content || info?.text || '';
|
|
192
|
+
if (typeof content === 'string') {
|
|
193
|
+
contentStr = content;
|
|
194
|
+
}
|
|
195
|
+
else if (Array.isArray(content)) {
|
|
196
|
+
contentStr = content
|
|
197
|
+
.map((p) => {
|
|
198
|
+
if (typeof p === 'string')
|
|
199
|
+
return p;
|
|
200
|
+
if (p?.text)
|
|
201
|
+
return p.text;
|
|
202
|
+
if (p?.content)
|
|
203
|
+
return p.content;
|
|
204
|
+
return '';
|
|
205
|
+
})
|
|
206
|
+
.join('');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Process if we have content and haven't processed this message yet
|
|
210
|
+
if (contentStr && !processedMessageIds.has(messageId)) {
|
|
211
|
+
processedMessageIds.add(messageId);
|
|
212
|
+
// Look for COMPLETED: line for tab title
|
|
213
|
+
const completedMatch = contentStr.match(/(?:🎯\s*)?COMPLETED:\s*(.+?)(?:\n|$)/i);
|
|
214
|
+
if (completedMatch) {
|
|
215
|
+
const completedLine = completedMatch[1].trim();
|
|
216
|
+
const tabTitle = generateTabTitle(completedLine);
|
|
217
|
+
process.stderr.write(`\x1b]0;${tabTitle}\x07`);
|
|
218
|
+
}
|
|
219
|
+
// Archive structured response
|
|
220
|
+
await logger.processAssistantMessage(contentStr, messageId);
|
|
221
|
+
// Clean up cache for this message
|
|
222
|
+
messageTextCache.delete(messageId);
|
|
167
223
|
}
|
|
168
224
|
}
|
|
169
225
|
}
|
|
170
226
|
// Handle session deletion / end or idle (for one-shot commands)
|
|
171
227
|
if (event.type === 'session.deleted' || event.type === 'session.idle') {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
228
|
+
await logger.generateSessionSummary();
|
|
229
|
+
logger.flush();
|
|
230
|
+
loggers.delete(sessionId);
|
|
231
|
+
// Clean up any stale cache entries for this session
|
|
232
|
+
// (In practice, messages are cleaned up after processing)
|
|
176
233
|
}
|
|
177
234
|
},
|
|
178
235
|
"tool.execute.after": async (input, output) => {
|
|
179
|
-
|
|
180
|
-
|
|
236
|
+
const sessionId = input.sessionID;
|
|
237
|
+
if (sessionId) {
|
|
238
|
+
if (!loggers.has(sessionId)) {
|
|
239
|
+
loggers.set(sessionId, new Logger(sessionId, worktree));
|
|
240
|
+
}
|
|
241
|
+
loggers.get(sessionId).logToolExecution(input, output);
|
|
181
242
|
}
|
|
182
243
|
},
|
|
183
244
|
"permission.ask": async (permission) => {
|
package/dist/lib/logger.d.ts
CHANGED
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
import type { Event } from '@opencode-ai/sdk';
|
|
2
2
|
export declare class Logger {
|
|
3
3
|
private sessionId;
|
|
4
|
+
private worktree;
|
|
4
5
|
private toolsUsed;
|
|
5
6
|
private filesChanged;
|
|
6
7
|
private commandsExecuted;
|
|
8
|
+
private processedMessageIds;
|
|
7
9
|
private startTime;
|
|
8
|
-
constructor(sessionId: string);
|
|
10
|
+
constructor(sessionId: string, worktree?: string);
|
|
11
|
+
private getHistoryDir;
|
|
12
|
+
processAssistantMessage(content: string, messageId?: string): Promise<void>;
|
|
9
13
|
private getPSTTimestamp;
|
|
10
14
|
private getEventsFilePath;
|
|
11
15
|
private getSessionMappingFile;
|
|
12
16
|
private getAgentForSession;
|
|
13
17
|
private setAgentForSession;
|
|
14
|
-
logEvent(event: Event): void;
|
|
15
18
|
logOpenCodeEvent(event: Event): void;
|
|
16
|
-
/**
|
|
17
|
-
* Log tool execution from tool.execute.after hook
|
|
18
|
-
*
|
|
19
|
-
* Input structure: { tool: string; sessionID: string; callID: string }
|
|
20
|
-
* Output structure: { title: string; output: string; metadata: any }
|
|
21
|
-
*/
|
|
22
19
|
logToolExecution(input: {
|
|
23
20
|
tool: string;
|
|
24
21
|
sessionID: string;
|
|
@@ -29,6 +26,10 @@ export declare class Logger {
|
|
|
29
26
|
metadata: any;
|
|
30
27
|
}): void;
|
|
31
28
|
generateSessionSummary(): Promise<string | null>;
|
|
29
|
+
private parseStructuredResponse;
|
|
30
|
+
private isLearningCapture;
|
|
31
|
+
private determineArtifactType;
|
|
32
|
+
private createArtifact;
|
|
32
33
|
logError(context: string, error: any): void;
|
|
33
34
|
private writeEvent;
|
|
34
35
|
flush(): void;
|
package/dist/lib/logger.js
CHANGED
|
@@ -1,18 +1,52 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync } from 'fs';
|
|
2
2
|
import { dirname, join } from 'path';
|
|
3
|
-
import { PAI_DIR,
|
|
4
|
-
import {
|
|
3
|
+
import { PAI_DIR, HISTORY_DIR } from './paths';
|
|
4
|
+
import { isAgentSpawningCall, enrichEventWithAgentMetadata } from './metadata-extraction';
|
|
5
5
|
import { redactString, redactObject } from './redaction';
|
|
6
6
|
export class Logger {
|
|
7
7
|
sessionId;
|
|
8
|
+
worktree;
|
|
8
9
|
toolsUsed = new Set();
|
|
9
10
|
filesChanged = new Set();
|
|
10
11
|
commandsExecuted = [];
|
|
12
|
+
processedMessageIds = new Set();
|
|
11
13
|
startTime = Date.now();
|
|
12
|
-
constructor(sessionId) {
|
|
14
|
+
constructor(sessionId, worktree = '/') {
|
|
13
15
|
this.sessionId = sessionId;
|
|
16
|
+
this.worktree = worktree;
|
|
17
|
+
}
|
|
18
|
+
getHistoryDir() {
|
|
19
|
+
if (process.env.HISTORY_DIR)
|
|
20
|
+
return process.env.HISTORY_DIR;
|
|
21
|
+
if (existsSync(PAI_DIR))
|
|
22
|
+
return HISTORY_DIR;
|
|
23
|
+
return join(this.worktree, '.opencode', 'history');
|
|
24
|
+
}
|
|
25
|
+
async processAssistantMessage(content, messageId) {
|
|
26
|
+
try {
|
|
27
|
+
// Deduplication: skip if we've already processed this message
|
|
28
|
+
if (messageId) {
|
|
29
|
+
if (this.processedMessageIds.has(messageId))
|
|
30
|
+
return;
|
|
31
|
+
this.processedMessageIds.add(messageId);
|
|
32
|
+
}
|
|
33
|
+
// Parse structured response sections
|
|
34
|
+
const sections = this.parseStructuredResponse(content);
|
|
35
|
+
// Require at least SUMMARY or COMPLETED to be a valid structured response
|
|
36
|
+
// This prevents archiving every random message
|
|
37
|
+
const hasRequiredSection = sections['SUMMARY'] || sections['COMPLETED'];
|
|
38
|
+
if (!hasRequiredSection || Object.keys(sections).length < 2) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const agentRole = this.getAgentForSession(this.sessionId);
|
|
42
|
+
const isLearning = this.isLearningCapture(sections);
|
|
43
|
+
const type = this.determineArtifactType(agentRole, isLearning, sections);
|
|
44
|
+
await this.createArtifact(type, content, sections);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
this.logError('ProcessAssistantMessage', error);
|
|
48
|
+
}
|
|
14
49
|
}
|
|
15
|
-
// Get PST timestamp
|
|
16
50
|
getPSTTimestamp() {
|
|
17
51
|
const date = new Date();
|
|
18
52
|
const pstDate = new Date(date.toLocaleString('en-US', { timeZone: process.env.TIME_ZONE || 'America/Los_Angeles' }));
|
|
@@ -31,11 +65,11 @@ export class Logger {
|
|
|
31
65
|
const month = String(pstDate.getMonth() + 1).padStart(2, '0');
|
|
32
66
|
const day = String(pstDate.getDate()).padStart(2, '0');
|
|
33
67
|
const filename = `${year}-${month}-${day}_all-events.jsonl`;
|
|
34
|
-
const
|
|
68
|
+
const historyDir = this.getHistoryDir();
|
|
69
|
+
const filePath = join(historyDir, 'raw-outputs', filename);
|
|
35
70
|
const dir = dirname(filePath);
|
|
36
|
-
if (!existsSync(dir))
|
|
71
|
+
if (!existsSync(dir))
|
|
37
72
|
mkdirSync(dir, { recursive: true });
|
|
38
|
-
}
|
|
39
73
|
return filePath;
|
|
40
74
|
}
|
|
41
75
|
getSessionMappingFile() {
|
|
@@ -49,39 +83,24 @@ export class Logger {
|
|
|
49
83
|
return mappings[sessionId] || 'pai';
|
|
50
84
|
}
|
|
51
85
|
}
|
|
52
|
-
catch (error) {
|
|
53
|
-
// Ignore errors, default to pai
|
|
54
|
-
}
|
|
86
|
+
catch (error) { }
|
|
55
87
|
return 'pai';
|
|
56
88
|
}
|
|
57
89
|
setAgentForSession(sessionId, agentName) {
|
|
58
90
|
try {
|
|
59
91
|
const mappingFile = this.getSessionMappingFile();
|
|
60
92
|
let mappings = {};
|
|
61
|
-
if (existsSync(mappingFile))
|
|
93
|
+
if (existsSync(mappingFile))
|
|
62
94
|
mappings = JSON.parse(readFileSync(mappingFile, 'utf-8'));
|
|
63
|
-
}
|
|
64
95
|
mappings[sessionId] = agentName;
|
|
65
96
|
writeFileSync(mappingFile, JSON.stringify(mappings, null, 2), 'utf-8');
|
|
66
97
|
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
// Silently fail - don't block
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
logEvent(event) {
|
|
72
|
-
// Legacy method, not used much as we use logOpenCodeEvent
|
|
73
|
-
// But might be called from index.ts if I didn't update all calls
|
|
74
|
-
this.logOpenCodeEvent(event);
|
|
98
|
+
catch (error) { }
|
|
75
99
|
}
|
|
76
|
-
// Method to log generic OpenCode event
|
|
77
100
|
logOpenCodeEvent(event) {
|
|
78
101
|
const anyEvent = event;
|
|
79
102
|
const timestamp = anyEvent.timestamp || Date.now();
|
|
80
|
-
const payload = {
|
|
81
|
-
...anyEvent.properties,
|
|
82
|
-
timestamp: timestamp
|
|
83
|
-
};
|
|
84
|
-
// Track stats for summary
|
|
103
|
+
const payload = { ...anyEvent.properties, timestamp };
|
|
85
104
|
if (anyEvent.type === 'tool.call' || anyEvent.type === 'tool.execute.before') {
|
|
86
105
|
const props = anyEvent.properties;
|
|
87
106
|
const tool = props?.tool || props?.tool_name;
|
|
@@ -93,8 +112,8 @@ export class Logger {
|
|
|
93
112
|
this.commandsExecuted.push(redactString(command));
|
|
94
113
|
}
|
|
95
114
|
if (['Edit', 'Write', 'edit', 'write'].includes(tool)) {
|
|
96
|
-
const path = props?.input?.file_path || props?.input?.path ||
|
|
97
|
-
props?.tool_input?.file_path || props?.tool_input?.path;
|
|
115
|
+
const path = props?.input?.file_path || props?.input?.path || props?.input?.filePath ||
|
|
116
|
+
props?.tool_input?.file_path || props?.tool_input?.path || props?.tool_input?.filePath;
|
|
98
117
|
if (path)
|
|
99
118
|
this.filesChanged.add(path);
|
|
100
119
|
}
|
|
@@ -102,19 +121,11 @@ export class Logger {
|
|
|
102
121
|
}
|
|
103
122
|
this.writeEvent(anyEvent.type, redactObject(payload));
|
|
104
123
|
}
|
|
105
|
-
/**
|
|
106
|
-
* Log tool execution from tool.execute.after hook
|
|
107
|
-
*
|
|
108
|
-
* Input structure: { tool: string; sessionID: string; callID: string }
|
|
109
|
-
* Output structure: { title: string; output: string; metadata: any }
|
|
110
|
-
*/
|
|
111
124
|
logToolExecution(input, output) {
|
|
112
125
|
const toolName = input.tool;
|
|
113
126
|
const sessionId = this.sessionId;
|
|
114
127
|
this.toolsUsed.add(toolName);
|
|
115
|
-
// Extract metadata - may contain additional tool info
|
|
116
128
|
const metadata = output.metadata || {};
|
|
117
|
-
// Logic to update agent mapping based on Task tool spawning subagents
|
|
118
129
|
if (toolName === 'Task' && metadata?.subagent_type) {
|
|
119
130
|
this.setAgentForSession(sessionId, metadata.subagent_type);
|
|
120
131
|
}
|
|
@@ -129,22 +140,23 @@ export class Logger {
|
|
|
129
140
|
call_id: input.callID,
|
|
130
141
|
};
|
|
131
142
|
this.writeEvent('ToolUse', redactObject(payload), toolName, metadata);
|
|
143
|
+
if (toolName === 'task' || toolName === 'Task') {
|
|
144
|
+
if (output.output)
|
|
145
|
+
this.processAssistantMessage(output.output);
|
|
146
|
+
}
|
|
132
147
|
}
|
|
133
148
|
async generateSessionSummary() {
|
|
134
149
|
try {
|
|
135
150
|
const now = new Date();
|
|
136
|
-
const timestamp = now.toISOString()
|
|
137
|
-
.replace(/:/g, '')
|
|
138
|
-
.replace(/\..+/, '')
|
|
139
|
-
.replace('T', '-'); // YYYY-MM-DD-HHMMSS
|
|
151
|
+
const timestamp = now.toISOString().replace(/:/g, '').replace(/\..+/, '').replace('T', '-');
|
|
140
152
|
const yearMonth = timestamp.substring(0, 7);
|
|
141
153
|
const date = timestamp.substring(0, 10);
|
|
142
154
|
const time = timestamp.substring(11).replace(/-/g, ':');
|
|
143
155
|
const duration = Math.round((Date.now() - this.startTime) / 60000);
|
|
144
|
-
const
|
|
145
|
-
|
|
156
|
+
const historyDir = this.getHistoryDir();
|
|
157
|
+
const sessionDir = join(historyDir, 'sessions', yearMonth);
|
|
158
|
+
if (!existsSync(sessionDir))
|
|
146
159
|
mkdirSync(sessionDir, { recursive: true });
|
|
147
|
-
}
|
|
148
160
|
const focus = this.filesChanged.size > 0 ? 'development' : 'research';
|
|
149
161
|
const filename = `${timestamp}_SESSION_${focus}.md`;
|
|
150
162
|
const filePath = join(sessionDir, filename);
|
|
@@ -204,10 +216,93 @@ This session summary was automatically generated by the PAI OpenCode Plugin.
|
|
|
204
216
|
return filePath;
|
|
205
217
|
}
|
|
206
218
|
catch (error) {
|
|
207
|
-
this.logError('SessionSummary', error);
|
|
208
219
|
return null;
|
|
209
220
|
}
|
|
210
221
|
}
|
|
222
|
+
parseStructuredResponse(content) {
|
|
223
|
+
const sections = {};
|
|
224
|
+
const sectionHeaders = ['SUMMARY', 'ANALYSIS', 'ACTIONS', 'RESULTS', 'STATUS', 'CAPTURE', 'NEXT', 'STORY EXPLANATION', 'COMPLETED'];
|
|
225
|
+
for (const header of sectionHeaders) {
|
|
226
|
+
// Match header with optional markdown bold (**) or other formatting
|
|
227
|
+
// Handles: "SUMMARY:", "**SUMMARY:**", "**SUMMARY**:", "* SUMMARY:", etc.
|
|
228
|
+
const regex = new RegExp(`(?:^|\\n)\\*{0,2}\\s*${header}\\s*\\*{0,2}:\\s*([\\s\\S]*?)(?=\\n\\*{0,2}\\s*(?:${sectionHeaders.join('|')})\\s*\\*{0,2}:|$)`, 'i');
|
|
229
|
+
const match = content.match(regex);
|
|
230
|
+
if (match && match[1])
|
|
231
|
+
sections[header] = match[1].trim();
|
|
232
|
+
}
|
|
233
|
+
return sections;
|
|
234
|
+
}
|
|
235
|
+
isLearningCapture(sections) {
|
|
236
|
+
const indicators = ['fixed', 'solved', 'discovered', 'lesson', 'troubleshoot', 'debug', 'root cause', 'learning', 'bug', 'issue', 'resolved'];
|
|
237
|
+
const textToSearch = ((sections['ANALYSIS'] || '') + ' ' + (sections['RESULTS'] || '')).toLowerCase();
|
|
238
|
+
let count = 0;
|
|
239
|
+
for (const indicator of indicators)
|
|
240
|
+
if (textToSearch.includes(indicator))
|
|
241
|
+
count++;
|
|
242
|
+
return count >= 2;
|
|
243
|
+
}
|
|
244
|
+
determineArtifactType(agentRole, isLearning, sections) {
|
|
245
|
+
const summary = (sections['SUMMARY'] || '').toLowerCase();
|
|
246
|
+
if (agentRole === 'architect')
|
|
247
|
+
return 'DECISION';
|
|
248
|
+
if (agentRole === 'researcher' || agentRole === 'pentester')
|
|
249
|
+
return 'RESEARCH';
|
|
250
|
+
if (agentRole === 'engineer' || agentRole === 'designer') {
|
|
251
|
+
if (summary.includes('fix') || summary.includes('bug') || summary.includes('issue'))
|
|
252
|
+
return 'BUG';
|
|
253
|
+
if (summary.includes('refactor') || summary.includes('improve') || summary.includes('cleanup'))
|
|
254
|
+
return 'REFACTOR';
|
|
255
|
+
return 'FEATURE';
|
|
256
|
+
}
|
|
257
|
+
return isLearning ? 'LEARNING' : 'WORK';
|
|
258
|
+
}
|
|
259
|
+
async createArtifact(type, content, sections) {
|
|
260
|
+
try {
|
|
261
|
+
const now = new Date();
|
|
262
|
+
const timestamp = now.toISOString().replace(/:/g, '').replace(/\..+/, '').replace('T', '-');
|
|
263
|
+
const yearMonth = timestamp.substring(0, 7);
|
|
264
|
+
const summary = sections['SUMMARY'] || 'no-summary';
|
|
265
|
+
const slug = summary.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-').substring(0, 50);
|
|
266
|
+
const filename = `${timestamp}_${type}_${slug}.md`;
|
|
267
|
+
let subdir = 'execution';
|
|
268
|
+
if (type === 'LEARNING')
|
|
269
|
+
subdir = 'learnings';
|
|
270
|
+
else if (type === 'DECISION')
|
|
271
|
+
subdir = 'decisions';
|
|
272
|
+
else if (type === 'RESEARCH')
|
|
273
|
+
subdir = 'research';
|
|
274
|
+
else if (type === 'WORK')
|
|
275
|
+
subdir = 'sessions';
|
|
276
|
+
else {
|
|
277
|
+
if (type === 'BUG')
|
|
278
|
+
subdir = join('execution', 'bugs');
|
|
279
|
+
else if (type === 'REFACTOR')
|
|
280
|
+
subdir = join('execution', 'refactors');
|
|
281
|
+
else
|
|
282
|
+
subdir = join('execution', 'features');
|
|
283
|
+
}
|
|
284
|
+
const historyDir = this.getHistoryDir();
|
|
285
|
+
const targetDir = join(historyDir, subdir, yearMonth);
|
|
286
|
+
if (!existsSync(targetDir))
|
|
287
|
+
mkdirSync(targetDir, { recursive: true });
|
|
288
|
+
const filePath = join(targetDir, filename);
|
|
289
|
+
const agentRole = this.getAgentForSession(this.sessionId);
|
|
290
|
+
const frontmatter = `---
|
|
291
|
+
capture_type: ${type}
|
|
292
|
+
timestamp: ${new Date().toISOString()}
|
|
293
|
+
session_id: ${this.sessionId}
|
|
294
|
+
executor: ${agentRole}
|
|
295
|
+
${sections['SUMMARY'] ? `summary: ${sections['SUMMARY'].replace(/"/g, '\\"')}` : ''}
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
${content}
|
|
299
|
+
`;
|
|
300
|
+
writeFileSync(filePath, redactString(frontmatter), 'utf-8');
|
|
301
|
+
}
|
|
302
|
+
catch (e) {
|
|
303
|
+
this.logError('CreateArtifact', e);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
211
306
|
logError(context, error) {
|
|
212
307
|
try {
|
|
213
308
|
const now = new Date();
|
|
@@ -216,26 +311,22 @@ This session summary was automatically generated by the PAI OpenCode Plugin.
|
|
|
216
311
|
const month = String(pstDate.getMonth() + 1).padStart(2, '0');
|
|
217
312
|
const day = String(pstDate.getDate()).padStart(2, '0');
|
|
218
313
|
const filename = `${year}-${month}-${day}_errors.log`;
|
|
219
|
-
const
|
|
314
|
+
const historyDir = this.getHistoryDir();
|
|
315
|
+
const filePath = join(historyDir, 'system-logs', filename);
|
|
220
316
|
const dir = dirname(filePath);
|
|
221
|
-
if (!existsSync(dir))
|
|
317
|
+
if (!existsSync(dir))
|
|
222
318
|
mkdirSync(dir, { recursive: true });
|
|
223
|
-
}
|
|
224
319
|
const timestamp = this.getPSTTimestamp();
|
|
225
320
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
226
321
|
const stack = error instanceof Error ? error.stack : '';
|
|
227
322
|
const logEntry = `[${timestamp}] [${context}] ${errorMessage}\n${stack}\n-------------------\n`;
|
|
228
323
|
appendFileSync(filePath, logEntry, 'utf-8');
|
|
229
324
|
}
|
|
230
|
-
catch (e) {
|
|
231
|
-
// Intentionally silent - TUI protection
|
|
232
|
-
}
|
|
325
|
+
catch (e) { }
|
|
233
326
|
}
|
|
234
|
-
// Core write method
|
|
235
327
|
writeEvent(eventType, payload, toolName, toolInput) {
|
|
236
328
|
const sessionId = this.sessionId;
|
|
237
329
|
let agentName = this.getAgentForSession(sessionId);
|
|
238
|
-
// Create base event object
|
|
239
330
|
let hookEvent = {
|
|
240
331
|
source_app: agentName,
|
|
241
332
|
session_id: sessionId,
|
|
@@ -244,10 +335,8 @@ This session summary was automatically generated by the PAI OpenCode Plugin.
|
|
|
244
335
|
timestamp: Date.now(),
|
|
245
336
|
timestamp_pst: this.getPSTTimestamp()
|
|
246
337
|
};
|
|
247
|
-
// Enrich with agent instance metadata if this is a Task tool call
|
|
248
338
|
if (toolName && toolInput && isAgentSpawningCall(toolName, toolInput)) {
|
|
249
|
-
hookEvent = enrichEventWithAgentMetadata(hookEvent, toolInput, payload.description
|
|
250
|
-
);
|
|
339
|
+
hookEvent = enrichEventWithAgentMetadata(hookEvent, toolInput, payload.description);
|
|
251
340
|
}
|
|
252
341
|
try {
|
|
253
342
|
const eventsFile = this.getEventsFilePath();
|
|
@@ -258,7 +347,5 @@ This session summary was automatically generated by the PAI OpenCode Plugin.
|
|
|
258
347
|
this.logError('EventCapture', error);
|
|
259
348
|
}
|
|
260
349
|
}
|
|
261
|
-
flush() {
|
|
262
|
-
// No-op for now as we append synchronously
|
|
263
|
-
}
|
|
350
|
+
flush() { }
|
|
264
351
|
}
|
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
|
-
}
|