@fpr1m3/opencode-pai-plugin 1.1.1 → 1.3.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/dist/index.js +105 -31
- package/dist/lib/logger.d.ts +14 -9
- package/dist/lib/logger.js +150 -123
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -91,8 +91,15 @@ function generateTabTitle(completedLine) {
|
|
|
91
91
|
return 'PAI Task Done';
|
|
92
92
|
}
|
|
93
93
|
export const PAIPlugin = async ({ worktree }) => {
|
|
94
|
-
|
|
95
|
-
|
|
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();
|
|
100
|
+
// Track pending Task tool calls to capture subagent_type
|
|
101
|
+
// Key: callID, Value: subagent_type
|
|
102
|
+
const pendingTaskCalls = new Map();
|
|
96
103
|
// Auto-initialize PAI infrastructure if needed
|
|
97
104
|
ensurePAIStructure();
|
|
98
105
|
// Load CORE skill content from $PAI_DIR/skill/core/SKILL.md
|
|
@@ -131,17 +138,33 @@ export const PAIPlugin = async ({ worktree }) => {
|
|
|
131
138
|
const hooks = {
|
|
132
139
|
event: async ({ event }) => {
|
|
133
140
|
const anyEvent = event;
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
141
|
+
// Get Session ID from event (try multiple locations)
|
|
142
|
+
const sessionId = anyEvent.properties?.part?.sessionID ||
|
|
143
|
+
anyEvent.properties?.info?.sessionID ||
|
|
144
|
+
anyEvent.properties?.sessionID ||
|
|
145
|
+
anyEvent.sessionID;
|
|
146
|
+
if (!sessionId)
|
|
147
|
+
return;
|
|
148
|
+
// Initialize Logger if needed
|
|
149
|
+
if (!loggers.has(sessionId)) {
|
|
150
|
+
loggers.set(sessionId, new Logger(sessionId, worktree));
|
|
138
151
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
!shouldSkipEvent(event, currentSessionId)) {
|
|
152
|
+
const logger = loggers.get(sessionId);
|
|
153
|
+
// Handle generic event logging (skip streaming parts to reduce noise)
|
|
154
|
+
if (!shouldSkipEvent(event, sessionId) && event.type !== 'message.part.updated') {
|
|
143
155
|
logger.logOpenCodeEvent(event);
|
|
144
156
|
}
|
|
157
|
+
// STREAMING CAPTURE: Cache the latest text from message.part.updated
|
|
158
|
+
// The part.text field contains the FULL accumulated text, not a delta
|
|
159
|
+
if (event.type === 'message.part.updated') {
|
|
160
|
+
const part = anyEvent.properties?.part;
|
|
161
|
+
const messageId = part?.messageID;
|
|
162
|
+
const partType = part?.type;
|
|
163
|
+
// Only cache text parts (not tool parts)
|
|
164
|
+
if (messageId && partType === 'text' && part?.text) {
|
|
165
|
+
messageTextCache.set(messageId, part.text);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
145
168
|
// Handle real-time tab title updates (Pre-Tool Use)
|
|
146
169
|
if (anyEvent.type === 'tool.call') {
|
|
147
170
|
const props = anyEvent.properties;
|
|
@@ -153,44 +176,95 @@ export const PAIPlugin = async ({ worktree }) => {
|
|
|
153
176
|
const file = props?.input?.file_path?.split('/').pop() || 'file';
|
|
154
177
|
process.stderr.write(`\x1b]0;Editing ${file}...\x07`);
|
|
155
178
|
}
|
|
156
|
-
else if (props?.tool === 'Task') {
|
|
179
|
+
else if (props?.tool === 'Task' || props?.tool === 'task') {
|
|
157
180
|
const type = props?.input?.subagent_type || 'agent';
|
|
158
181
|
process.stderr.write(`\x1b]0;Agent: ${type}...\x07`);
|
|
182
|
+
// Cache the subagent_type for when tool.execute.after fires
|
|
183
|
+
const callId = props?.id || props?.callId || props?.call_id;
|
|
184
|
+
if (callId && props?.input?.subagent_type) {
|
|
185
|
+
pendingTaskCalls.set(callId, props.input.subagent_type);
|
|
186
|
+
}
|
|
159
187
|
}
|
|
160
188
|
}
|
|
161
|
-
// Handle assistant completion (Tab Titles &
|
|
189
|
+
// Handle assistant message completion (Tab Titles & Artifact Archival)
|
|
162
190
|
if (event.type === 'message.updated') {
|
|
163
191
|
const info = anyEvent.properties?.info;
|
|
164
192
|
const role = info?.role || info?.author;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
193
|
+
const messageId = info?.id;
|
|
194
|
+
if (role === 'assistant' && messageId) {
|
|
195
|
+
// Get content from our streaming cache first, fallback to info.content
|
|
196
|
+
let contentStr = messageTextCache.get(messageId) || '';
|
|
197
|
+
// Fallback: try to get content from the event itself
|
|
198
|
+
if (!contentStr) {
|
|
199
|
+
const content = info?.content || info?.text || '';
|
|
200
|
+
if (typeof content === 'string') {
|
|
201
|
+
contentStr = content;
|
|
202
|
+
}
|
|
203
|
+
else if (Array.isArray(content)) {
|
|
204
|
+
contentStr = content
|
|
205
|
+
.map((p) => {
|
|
206
|
+
if (typeof p === 'string')
|
|
207
|
+
return p;
|
|
208
|
+
if (p?.text)
|
|
209
|
+
return p.text;
|
|
210
|
+
if (p?.content)
|
|
211
|
+
return p.content;
|
|
212
|
+
return '';
|
|
213
|
+
})
|
|
214
|
+
.join('');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Process if we have content and haven't processed this message yet
|
|
218
|
+
if (contentStr && !processedMessageIds.has(messageId)) {
|
|
219
|
+
processedMessageIds.add(messageId);
|
|
220
|
+
// Look for COMPLETED: line for tab title
|
|
221
|
+
const completedMatch = contentStr.match(/(?:🎯\s*)?COMPLETED:\s*(.+?)(?:\n|$)/i);
|
|
222
|
+
if (completedMatch) {
|
|
223
|
+
const completedLine = completedMatch[1].trim();
|
|
224
|
+
const tabTitle = generateTabTitle(completedLine);
|
|
225
|
+
process.stderr.write(`\x1b]0;${tabTitle}\x07`);
|
|
179
226
|
}
|
|
227
|
+
// Archive structured response
|
|
228
|
+
await logger.processAssistantMessage(contentStr, messageId);
|
|
229
|
+
// Clean up cache for this message
|
|
230
|
+
messageTextCache.delete(messageId);
|
|
180
231
|
}
|
|
181
232
|
}
|
|
182
233
|
}
|
|
183
234
|
// Handle session deletion / end or idle (for one-shot commands)
|
|
184
235
|
if (event.type === 'session.deleted' || event.type === 'session.idle') {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
236
|
+
await logger.generateSessionSummary();
|
|
237
|
+
logger.flush();
|
|
238
|
+
loggers.delete(sessionId);
|
|
239
|
+
// Clean up any stale cache entries for this session
|
|
240
|
+
// (In practice, messages are cleaned up after processing)
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
"tool.execute.before": async (input, output) => {
|
|
244
|
+
// Cache subagent_type from Task tool args for later use in tool.execute.after
|
|
245
|
+
if ((input.tool === 'Task' || input.tool === 'task') && input.callID) {
|
|
246
|
+
const args = output.args;
|
|
247
|
+
if (args?.subagent_type) {
|
|
248
|
+
pendingTaskCalls.set(input.callID, args.subagent_type);
|
|
188
249
|
}
|
|
189
250
|
}
|
|
190
251
|
},
|
|
191
252
|
"tool.execute.after": async (input, output) => {
|
|
192
|
-
|
|
193
|
-
|
|
253
|
+
const sessionId = input.sessionID;
|
|
254
|
+
if (sessionId) {
|
|
255
|
+
if (!loggers.has(sessionId)) {
|
|
256
|
+
loggers.set(sessionId, new Logger(sessionId, worktree));
|
|
257
|
+
}
|
|
258
|
+
// For Task tools, inject the cached subagent_type into metadata
|
|
259
|
+
if ((input.tool === 'Task' || input.tool === 'task') && input.callID) {
|
|
260
|
+
const cachedAgentType = pendingTaskCalls.get(input.callID);
|
|
261
|
+
if (cachedAgentType) {
|
|
262
|
+
output.metadata = output.metadata || {};
|
|
263
|
+
output.metadata.subagent_type = cachedAgentType;
|
|
264
|
+
pendingTaskCalls.delete(input.callID);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
loggers.get(sessionId).logToolExecution(input, output);
|
|
194
268
|
}
|
|
195
269
|
},
|
|
196
270
|
"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,9 +26,17 @@ export declare class Logger {
|
|
|
29
26
|
metadata: any;
|
|
30
27
|
}): void;
|
|
31
28
|
generateSessionSummary(): Promise<string | null>;
|
|
32
|
-
processAssistantMessage(content: string): Promise<void>;
|
|
33
29
|
private parseStructuredResponse;
|
|
34
30
|
private isLearningCapture;
|
|
31
|
+
/**
|
|
32
|
+
* Normalize agent role from subagent_type patterns to base role.
|
|
33
|
+
* Handles patterns like:
|
|
34
|
+
* - "subagents/researcher-claude" → "researcher"
|
|
35
|
+
* - "subagents/sparc-architect" → "architect"
|
|
36
|
+
* - "subagents/sparc-dev" → "engineer"
|
|
37
|
+
* - "researcher" → "researcher" (passthrough)
|
|
38
|
+
*/
|
|
39
|
+
private normalizeAgentRole;
|
|
35
40
|
private determineArtifactType;
|
|
36
41
|
private createArtifact;
|
|
37
42
|
logError(context: string, error: any): 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
|
-
}
|
|
98
|
+
catch (error) { }
|
|
70
99
|
}
|
|
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);
|
|
75
|
-
}
|
|
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,20 +121,12 @@ 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
|
-
|
|
118
|
-
if (toolName === 'Task' && metadata?.subagent_type) {
|
|
129
|
+
if ((toolName === 'Task' || toolName === 'task') && metadata?.subagent_type) {
|
|
119
130
|
this.setAgentForSession(sessionId, metadata.subagent_type);
|
|
120
131
|
}
|
|
121
132
|
else if (toolName === 'subagent_stop' || toolName === 'stop') {
|
|
@@ -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,35 +216,19 @@ 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
|
}
|
|
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
222
|
parseStructuredResponse(content) {
|
|
226
223
|
const sections = {};
|
|
227
|
-
const sectionHeaders = [
|
|
228
|
-
'SUMMARY', 'ANALYSIS', 'ACTIONS', 'RESULTS', 'STATUS', 'CAPTURE', 'NEXT', 'STORY EXPLANATION', 'COMPLETED'
|
|
229
|
-
];
|
|
224
|
+
const sectionHeaders = ['SUMMARY', 'ANALYSIS', 'ACTIONS', 'RESULTS', 'STATUS', 'CAPTURE', 'NEXT', 'STORY EXPLANATION', 'COMPLETED'];
|
|
230
225
|
for (const header of sectionHeaders) {
|
|
231
|
-
|
|
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');
|
|
232
229
|
const match = content.match(regex);
|
|
233
|
-
if (match && match[1])
|
|
230
|
+
if (match && match[1])
|
|
234
231
|
sections[header] = match[1].trim();
|
|
235
|
-
}
|
|
236
232
|
}
|
|
237
233
|
return sections;
|
|
238
234
|
}
|
|
@@ -240,20 +236,61 @@ This session summary was automatically generated by the PAI OpenCode Plugin.
|
|
|
240
236
|
const indicators = ['fixed', 'solved', 'discovered', 'lesson', 'troubleshoot', 'debug', 'root cause', 'learning', 'bug', 'issue', 'resolved'];
|
|
241
237
|
const textToSearch = ((sections['ANALYSIS'] || '') + ' ' + (sections['RESULTS'] || '')).toLowerCase();
|
|
242
238
|
let count = 0;
|
|
243
|
-
for (const indicator of indicators)
|
|
244
|
-
if (textToSearch.includes(indicator))
|
|
239
|
+
for (const indicator of indicators)
|
|
240
|
+
if (textToSearch.includes(indicator))
|
|
245
241
|
count++;
|
|
242
|
+
return count >= 2;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Normalize agent role from subagent_type patterns to base role.
|
|
246
|
+
* Handles patterns like:
|
|
247
|
+
* - "subagents/researcher-claude" → "researcher"
|
|
248
|
+
* - "subagents/sparc-architect" → "architect"
|
|
249
|
+
* - "subagents/sparc-dev" → "engineer"
|
|
250
|
+
* - "researcher" → "researcher" (passthrough)
|
|
251
|
+
*/
|
|
252
|
+
normalizeAgentRole(agentRole) {
|
|
253
|
+
if (!agentRole)
|
|
254
|
+
return 'pai';
|
|
255
|
+
// Remove "subagents/" prefix if present (case-insensitive)
|
|
256
|
+
let role = agentRole.replace(/^subagents\//i, '').toLowerCase();
|
|
257
|
+
// Role keywords to look for anywhere in the string
|
|
258
|
+
// Order matters: more specific patterns first
|
|
259
|
+
const roleKeywords = [
|
|
260
|
+
// Researcher patterns (prefix or contains)
|
|
261
|
+
[/researcher/i, 'researcher'],
|
|
262
|
+
[/research/i, 'researcher'],
|
|
263
|
+
// Architect patterns
|
|
264
|
+
[/architect/i, 'architect'],
|
|
265
|
+
// Engineer patterns (includes "dev" for sparc-dev)
|
|
266
|
+
[/engineer/i, 'engineer'],
|
|
267
|
+
[/\bdev\b/i, 'engineer'], // "sparc-dev" → engineer
|
|
268
|
+
// Designer patterns
|
|
269
|
+
[/designer/i, 'designer'],
|
|
270
|
+
// Security patterns
|
|
271
|
+
[/pentester/i, 'pentester'],
|
|
272
|
+
// These map to researcher
|
|
273
|
+
[/analyst/i, 'researcher'],
|
|
274
|
+
[/explorer/i, 'researcher'],
|
|
275
|
+
[/^explore$/i, 'researcher'],
|
|
276
|
+
[/^intern$/i, 'researcher'],
|
|
277
|
+
];
|
|
278
|
+
for (const [pattern, normalized] of roleKeywords) {
|
|
279
|
+
if (pattern.test(role)) {
|
|
280
|
+
return normalized;
|
|
246
281
|
}
|
|
247
282
|
}
|
|
248
|
-
|
|
283
|
+
// Return lowercase role if no pattern matched
|
|
284
|
+
return role;
|
|
249
285
|
}
|
|
250
286
|
determineArtifactType(agentRole, isLearning, sections) {
|
|
251
287
|
const summary = (sections['SUMMARY'] || '').toLowerCase();
|
|
252
|
-
|
|
288
|
+
const normalizedRole = this.normalizeAgentRole(agentRole);
|
|
289
|
+
if (normalizedRole === 'architect')
|
|
253
290
|
return 'DECISION';
|
|
254
|
-
if (
|
|
291
|
+
if (normalizedRole === 'researcher' || normalizedRole === 'pentester')
|
|
255
292
|
return 'RESEARCH';
|
|
256
|
-
if (
|
|
293
|
+
if (normalizedRole === 'engineer' || normalizedRole === 'designer') {
|
|
257
294
|
if (summary.includes('fix') || summary.includes('bug') || summary.includes('issue'))
|
|
258
295
|
return 'BUG';
|
|
259
296
|
if (summary.includes('refactor') || summary.includes('improve') || summary.includes('cleanup'))
|
|
@@ -263,43 +300,37 @@ This session summary was automatically generated by the PAI OpenCode Plugin.
|
|
|
263
300
|
return isLearning ? 'LEARNING' : 'WORK';
|
|
264
301
|
}
|
|
265
302
|
async createArtifact(type, content, sections) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
.replace(/:/g, '')
|
|
269
|
-
.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
+
try {
|
|
304
|
+
const now = new Date();
|
|
305
|
+
const timestamp = now.toISOString().replace(/:/g, '').replace(/\..+/, '').replace('T', '-');
|
|
306
|
+
const yearMonth = timestamp.substring(0, 7);
|
|
307
|
+
const summary = sections['SUMMARY'] || 'no-summary';
|
|
308
|
+
const slug = summary.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-').substring(0, 50);
|
|
309
|
+
const filename = `${timestamp}_${type}_${slug}.md`;
|
|
310
|
+
let subdir = 'execution';
|
|
311
|
+
if (type === 'LEARNING')
|
|
312
|
+
subdir = 'learnings';
|
|
313
|
+
else if (type === 'DECISION')
|
|
314
|
+
subdir = 'decisions';
|
|
315
|
+
else if (type === 'RESEARCH')
|
|
316
|
+
subdir = 'research';
|
|
317
|
+
else if (type === 'WORK')
|
|
318
|
+
subdir = 'sessions';
|
|
319
|
+
else {
|
|
320
|
+
if (type === 'BUG')
|
|
321
|
+
subdir = join('execution', 'bugs');
|
|
322
|
+
else if (type === 'REFACTOR')
|
|
323
|
+
subdir = join('execution', 'refactors');
|
|
324
|
+
else
|
|
325
|
+
subdir = join('execution', 'features');
|
|
326
|
+
}
|
|
327
|
+
const historyDir = this.getHistoryDir();
|
|
328
|
+
const targetDir = join(historyDir, subdir, yearMonth);
|
|
329
|
+
if (!existsSync(targetDir))
|
|
330
|
+
mkdirSync(targetDir, { recursive: true });
|
|
331
|
+
const filePath = join(targetDir, filename);
|
|
332
|
+
const agentRole = this.getAgentForSession(this.sessionId);
|
|
333
|
+
const frontmatter = `---
|
|
303
334
|
capture_type: ${type}
|
|
304
335
|
timestamp: ${new Date().toISOString()}
|
|
305
336
|
session_id: ${this.sessionId}
|
|
@@ -309,7 +340,11 @@ ${sections['SUMMARY'] ? `summary: ${sections['SUMMARY'].replace(/"/g, '\\"')}` :
|
|
|
309
340
|
|
|
310
341
|
${content}
|
|
311
342
|
`;
|
|
312
|
-
|
|
343
|
+
writeFileSync(filePath, redactString(frontmatter), 'utf-8');
|
|
344
|
+
}
|
|
345
|
+
catch (e) {
|
|
346
|
+
this.logError('CreateArtifact', e);
|
|
347
|
+
}
|
|
313
348
|
}
|
|
314
349
|
logError(context, error) {
|
|
315
350
|
try {
|
|
@@ -319,26 +354,22 @@ ${content}
|
|
|
319
354
|
const month = String(pstDate.getMonth() + 1).padStart(2, '0');
|
|
320
355
|
const day = String(pstDate.getDate()).padStart(2, '0');
|
|
321
356
|
const filename = `${year}-${month}-${day}_errors.log`;
|
|
322
|
-
const
|
|
357
|
+
const historyDir = this.getHistoryDir();
|
|
358
|
+
const filePath = join(historyDir, 'system-logs', filename);
|
|
323
359
|
const dir = dirname(filePath);
|
|
324
|
-
if (!existsSync(dir))
|
|
360
|
+
if (!existsSync(dir))
|
|
325
361
|
mkdirSync(dir, { recursive: true });
|
|
326
|
-
}
|
|
327
362
|
const timestamp = this.getPSTTimestamp();
|
|
328
363
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
329
364
|
const stack = error instanceof Error ? error.stack : '';
|
|
330
365
|
const logEntry = `[${timestamp}] [${context}] ${errorMessage}\n${stack}\n-------------------\n`;
|
|
331
366
|
appendFileSync(filePath, logEntry, 'utf-8');
|
|
332
367
|
}
|
|
333
|
-
catch (e) {
|
|
334
|
-
// Intentionally silent - TUI protection
|
|
335
|
-
}
|
|
368
|
+
catch (e) { }
|
|
336
369
|
}
|
|
337
|
-
// Core write method
|
|
338
370
|
writeEvent(eventType, payload, toolName, toolInput) {
|
|
339
371
|
const sessionId = this.sessionId;
|
|
340
372
|
let agentName = this.getAgentForSession(sessionId);
|
|
341
|
-
// Create base event object
|
|
342
373
|
let hookEvent = {
|
|
343
374
|
source_app: agentName,
|
|
344
375
|
session_id: sessionId,
|
|
@@ -347,10 +378,8 @@ ${content}
|
|
|
347
378
|
timestamp: Date.now(),
|
|
348
379
|
timestamp_pst: this.getPSTTimestamp()
|
|
349
380
|
};
|
|
350
|
-
// Enrich with agent instance metadata if this is a Task tool call
|
|
351
381
|
if (toolName && toolInput && isAgentSpawningCall(toolName, toolInput)) {
|
|
352
|
-
hookEvent = enrichEventWithAgentMetadata(hookEvent, toolInput, payload.description
|
|
353
|
-
);
|
|
382
|
+
hookEvent = enrichEventWithAgentMetadata(hookEvent, toolInput, payload.description);
|
|
354
383
|
}
|
|
355
384
|
try {
|
|
356
385
|
const eventsFile = this.getEventsFilePath();
|
|
@@ -361,7 +390,5 @@ ${content}
|
|
|
361
390
|
this.logError('EventCapture', error);
|
|
362
391
|
}
|
|
363
392
|
}
|
|
364
|
-
flush() {
|
|
365
|
-
// No-op for now as we append synchronously
|
|
366
|
-
}
|
|
393
|
+
flush() { }
|
|
367
394
|
}
|