@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 CHANGED
@@ -91,8 +91,15 @@ function generateTabTitle(completedLine) {
91
91
  return 'PAI Task Done';
92
92
  }
93
93
  export const PAIPlugin = async ({ worktree }) => {
94
- let logger = null;
95
- let currentSessionId = null;
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
- // Initialize Logger on session creation
135
- if (event.type === 'session.created') {
136
- currentSessionId = anyEvent.properties.info.id;
137
- logger = new Logger(currentSessionId);
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
- // Handle generic event logging
140
- if (logger &&
141
- event.type !== 'message.part.updated' &&
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 & UOCS)
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
- if (role === 'assistant') {
166
- // Robust content extraction
167
- const content = info?.content || info?.text || '';
168
- const contentStr = typeof content === 'string' ? content : '';
169
- // Look for COMPLETED: line (can be prefaced by 🎯 or just text)
170
- const completedMatch = contentStr.match(/(?:🎯\s*)?COMPLETED:\s*(.+?)(?:\n|$)/i);
171
- if (completedMatch) {
172
- const completedLine = completedMatch[1].trim();
173
- // Set Tab Title
174
- const tabTitle = generateTabTitle(completedLine);
175
- process.stderr.write(`\x1b]0;${tabTitle}\x07`);
176
- // UOCS: Process response for artifact generation
177
- if (logger && contentStr) {
178
- await logger.processAssistantMessage(contentStr);
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
- if (logger) {
186
- await logger.generateSessionSummary();
187
- logger.flush();
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
- if (logger) {
193
- logger.logToolExecution(input, output);
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) => {
@@ -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;
@@ -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, getHistoryFilePath, HISTORY_DIR } from './paths';
4
- import { enrichEventWithAgentMetadata, isAgentSpawningCall } from './metadata-extraction';
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 filePath = getHistoryFilePath('raw-outputs', filename);
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
- // Logic to update agent mapping based on Task tool spawning subagents
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 sessionDir = join(HISTORY_DIR, 'sessions', yearMonth);
145
- if (!existsSync(sessionDir)) {
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
- const regex = new RegExp(`${header}:\\s*([\\s\\S]*?)(?=\\n(?:${sectionHeaders.join('|')}):|$)`, 'i');
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
- return count >= 2;
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
- if (agentRole === 'architect')
288
+ const normalizedRole = this.normalizeAgentRole(agentRole);
289
+ if (normalizedRole === 'architect')
253
290
  return 'DECISION';
254
- if (agentRole === 'researcher' || agentRole === 'pentester')
291
+ if (normalizedRole === 'researcher' || normalizedRole === 'pentester')
255
292
  return 'RESEARCH';
256
- if (agentRole === 'engineer' || agentRole === 'designer') {
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
- 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
+ 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
- writeFileSync(filePath, redactString(frontmatter), 'utf-8');
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 filePath = getHistoryFilePath('system-logs', filename);
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 // Assuming description is available in payload if passed
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fpr1m3/opencode-pai-plugin",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "description": "Personal AI Infrastructure (PAI) plugin for OpenCode",
6
6
  "main": "dist/index.js",