@fpr1m3/opencode-pai-plugin 1.1.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/dist/index.js +79 -31
- package/dist/lib/logger.d.ts +5 -9
- package/dist/lib/logger.js +104 -120
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -91,8 +91,12 @@ 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();
|
|
96
100
|
// Auto-initialize PAI infrastructure if needed
|
|
97
101
|
ensurePAIStructure();
|
|
98
102
|
// Load CORE skill content from $PAI_DIR/skill/core/SKILL.md
|
|
@@ -131,17 +135,33 @@ export const PAIPlugin = async ({ worktree }) => {
|
|
|
131
135
|
const hooks = {
|
|
132
136
|
event: async ({ event }) => {
|
|
133
137
|
const anyEvent = event;
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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));
|
|
138
148
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
!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') {
|
|
143
152
|
logger.logOpenCodeEvent(event);
|
|
144
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
|
+
}
|
|
145
165
|
// Handle real-time tab title updates (Pre-Tool Use)
|
|
146
166
|
if (anyEvent.type === 'tool.call') {
|
|
147
167
|
const props = anyEvent.properties;
|
|
@@ -158,39 +178,67 @@ export const PAIPlugin = async ({ worktree }) => {
|
|
|
158
178
|
process.stderr.write(`\x1b]0;Agent: ${type}...\x07`);
|
|
159
179
|
}
|
|
160
180
|
}
|
|
161
|
-
// Handle assistant completion (Tab Titles &
|
|
181
|
+
// Handle assistant message completion (Tab Titles & Artifact Archival)
|
|
162
182
|
if (event.type === 'message.updated') {
|
|
163
183
|
const info = anyEvent.properties?.info;
|
|
164
184
|
const role = info?.role || info?.author;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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('');
|
|
179
207
|
}
|
|
180
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);
|
|
223
|
+
}
|
|
181
224
|
}
|
|
182
225
|
}
|
|
183
226
|
// Handle session deletion / end or idle (for one-shot commands)
|
|
184
227
|
if (event.type === 'session.deleted' || event.type === 'session.idle') {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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)
|
|
189
233
|
}
|
|
190
234
|
},
|
|
191
235
|
"tool.execute.after": async (input, output) => {
|
|
192
|
-
|
|
193
|
-
|
|
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);
|
|
194
242
|
}
|
|
195
243
|
},
|
|
196
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,7 +26,6 @@ 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;
|
|
35
31
|
private determineArtifactType;
|
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,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,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,11 +236,9 @@ 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++;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
242
|
return count >= 2;
|
|
249
243
|
}
|
|
250
244
|
determineArtifactType(agentRole, isLearning, sections) {
|
|
@@ -263,43 +257,37 @@ This session summary was automatically generated by the PAI OpenCode Plugin.
|
|
|
263
257
|
return isLearning ? 'LEARNING' : 'WORK';
|
|
264
258
|
}
|
|
265
259
|
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 = `---
|
|
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 = `---
|
|
303
291
|
capture_type: ${type}
|
|
304
292
|
timestamp: ${new Date().toISOString()}
|
|
305
293
|
session_id: ${this.sessionId}
|
|
@@ -309,7 +297,11 @@ ${sections['SUMMARY'] ? `summary: ${sections['SUMMARY'].replace(/"/g, '\\"')}` :
|
|
|
309
297
|
|
|
310
298
|
${content}
|
|
311
299
|
`;
|
|
312
|
-
|
|
300
|
+
writeFileSync(filePath, redactString(frontmatter), 'utf-8');
|
|
301
|
+
}
|
|
302
|
+
catch (e) {
|
|
303
|
+
this.logError('CreateArtifact', e);
|
|
304
|
+
}
|
|
313
305
|
}
|
|
314
306
|
logError(context, error) {
|
|
315
307
|
try {
|
|
@@ -319,26 +311,22 @@ ${content}
|
|
|
319
311
|
const month = String(pstDate.getMonth() + 1).padStart(2, '0');
|
|
320
312
|
const day = String(pstDate.getDate()).padStart(2, '0');
|
|
321
313
|
const filename = `${year}-${month}-${day}_errors.log`;
|
|
322
|
-
const
|
|
314
|
+
const historyDir = this.getHistoryDir();
|
|
315
|
+
const filePath = join(historyDir, 'system-logs', filename);
|
|
323
316
|
const dir = dirname(filePath);
|
|
324
|
-
if (!existsSync(dir))
|
|
317
|
+
if (!existsSync(dir))
|
|
325
318
|
mkdirSync(dir, { recursive: true });
|
|
326
|
-
}
|
|
327
319
|
const timestamp = this.getPSTTimestamp();
|
|
328
320
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
329
321
|
const stack = error instanceof Error ? error.stack : '';
|
|
330
322
|
const logEntry = `[${timestamp}] [${context}] ${errorMessage}\n${stack}\n-------------------\n`;
|
|
331
323
|
appendFileSync(filePath, logEntry, 'utf-8');
|
|
332
324
|
}
|
|
333
|
-
catch (e) {
|
|
334
|
-
// Intentionally silent - TUI protection
|
|
335
|
-
}
|
|
325
|
+
catch (e) { }
|
|
336
326
|
}
|
|
337
|
-
// Core write method
|
|
338
327
|
writeEvent(eventType, payload, toolName, toolInput) {
|
|
339
328
|
const sessionId = this.sessionId;
|
|
340
329
|
let agentName = this.getAgentForSession(sessionId);
|
|
341
|
-
// Create base event object
|
|
342
330
|
let hookEvent = {
|
|
343
331
|
source_app: agentName,
|
|
344
332
|
session_id: sessionId,
|
|
@@ -347,10 +335,8 @@ ${content}
|
|
|
347
335
|
timestamp: Date.now(),
|
|
348
336
|
timestamp_pst: this.getPSTTimestamp()
|
|
349
337
|
};
|
|
350
|
-
// Enrich with agent instance metadata if this is a Task tool call
|
|
351
338
|
if (toolName && toolInput && isAgentSpawningCall(toolName, toolInput)) {
|
|
352
|
-
hookEvent = enrichEventWithAgentMetadata(hookEvent, toolInput, payload.description
|
|
353
|
-
);
|
|
339
|
+
hookEvent = enrichEventWithAgentMetadata(hookEvent, toolInput, payload.description);
|
|
354
340
|
}
|
|
355
341
|
try {
|
|
356
342
|
const eventsFile = this.getEventsFilePath();
|
|
@@ -361,7 +347,5 @@ ${content}
|
|
|
361
347
|
this.logError('EventCapture', error);
|
|
362
348
|
}
|
|
363
349
|
}
|
|
364
|
-
flush() {
|
|
365
|
-
// No-op for now as we append synchronously
|
|
366
|
-
}
|
|
350
|
+
flush() { }
|
|
367
351
|
}
|