kiku-agent 1.0.9 → 1.0.11

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.
@@ -1,244 +1,410 @@
1
- import { promises as fs } from 'node:fs';
2
- import path from 'node:path';
3
- import { EXPORT_DIR, SESSION_DIR as DATA_SESSION_DIR } from './appPaths.js';
4
-
5
- const SESSION_DIR = DATA_SESSION_DIR;
6
- const SESSION_FILE = path.join(SESSION_DIR, 'current_session.json');
7
-
8
- export class AgentSession {
9
- constructor(options = {}) {
10
- this.id = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
11
- this.createdAt = new Date();
12
- this.thinkingMode = options.thinkingMode || 'hide';
13
- this.messages = [];
14
- this.stats = {
15
- totalQueries: 0,
16
- totalToolCalls: 0,
17
- totalDurationMs: 0
18
- };
19
- this._storageWarningShown = false;
20
- }
21
-
22
- setThinkingMode(mode) {
23
- if (mode === 'show' || mode === 'hide') {
24
- this.thinkingMode = mode;
25
- return true;
26
- }
27
- return false;
28
- }
29
-
30
- toggleThinkingMode() {
31
- this.thinkingMode = this.thinkingMode === 'show' ? 'hide' : 'show';
32
- return this.thinkingMode;
33
- }
34
-
35
- createUserMessage(text) {
36
- const msg = {
37
- id: `msg_${Date.now()}_u`,
38
- role: 'user',
39
- createdAt: new Date(),
40
- parts: [
41
- { type: 'text', content: text }
42
- ]
43
- };
44
- this.messages.push(msg);
45
- this.stats.totalQueries++;
46
-
47
- this.saveToDisk().catch(err => console.error('Session save failed:', err));
48
- return msg;
49
- }
50
-
51
- createAssistantMessage() {
52
- const msg = {
53
- id: `msg_${Date.now()}_a`,
54
- role: 'assistant',
55
- createdAt: new Date(),
56
- parts: []
57
- };
58
- this.messages.push(msg);
59
- return msg;
60
- }
61
-
62
- addReasoningPart(message, reasoningText) {
63
- let part = message.parts.find(p => p.type === 'reasoning');
64
- if (!part) {
65
- part = {
66
- type: 'reasoning',
67
- id: `rsn_${Date.now()}`,
68
- content: '',
69
- startedAt: Date.now(),
70
- endedAt: null
71
- };
72
- message.parts.push(part);
73
- }
74
- part.content += reasoningText;
75
- return part;
76
- }
77
-
78
- finalizeReasoningPart(message) {
79
- const part = message.parts.find(p => p.type === 'reasoning');
80
- if (part && !part.endedAt) {
81
- part.endedAt = Date.now();
82
- part.durationMs = part.endedAt - part.startedAt;
83
- }
84
- }
85
-
86
- addToolPart(message, toolName, args) {
87
- const part = {
88
- type: 'tool',
89
- id: `tool_${Date.now()}_${message.parts.length}`,
90
- name: toolName,
91
- args,
92
- result: null,
93
- startedAt: Date.now(),
94
- endedAt: null,
95
- durationMs: 0
96
- };
97
- message.parts.push(part);
98
- this.stats.totalToolCalls++;
99
- return part;
100
- }
101
-
102
- finalizeToolPart(message, toolId, result) {
103
- const part = message.parts.find(p => p.id === toolId || (p.type === 'tool' && !p.endedAt));
104
- if (part) {
105
- part.result = result;
106
- part.endedAt = Date.now();
107
- part.durationMs = part.endedAt - part.startedAt;
108
- }
109
- return part;
110
- }
111
-
112
- addTextPart(message, textChunk) {
113
- let part = message.parts.find(p => p.type === 'text');
114
- if (!part) {
115
- part = {
116
- type: 'text',
117
- content: ''
118
- };
119
- message.parts.push(part);
120
- }
121
- part.content += textChunk;
122
- return part;
123
- }
124
-
125
- async saveToDisk() {
126
- try {
127
- await fs.mkdir(SESSION_DIR, { recursive: true });
128
- const sessionData = {
129
- id: this.id,
130
- createdAt: this.createdAt,
131
- thinkingMode: this.thinkingMode,
132
- messages: this.messages,
133
- stats: this.stats
134
- };
135
- await fs.writeFile(SESSION_FILE, JSON.stringify(sessionData, null, 2), 'utf-8');
136
- return true;
137
- } catch (error) {
138
- if (!this._storageWarningShown) {
139
- this._storageWarningShown = true;
140
- console.error(`Kiku session is running without persistence (${error.code || 'unknown'}) in ${SESSION_DIR}. Set KIKU_DATA_DIR to a writable user folder; Administrator privileges are not required.`);
141
- }
142
- return false;
143
- }
144
- }
145
-
146
- clear() {
147
- this.messages = [];
148
- this.stats.totalQueries = 0;
149
- this.stats.totalToolCalls = 0;
150
- this.stats.totalDurationMs = 0;
151
- }
152
-
153
- toSnapshot() {
154
- return {
155
- id: this.id,
156
- createdAt: this.createdAt,
157
- thinkingMode: this.thinkingMode,
158
- messages: this.messages,
159
- stats: { ...this.stats }
160
- };
161
- }
162
-
163
- reset() {
164
- this.id = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
165
- this.createdAt = new Date();
166
- this.messages = [];
167
- this.stats = {
168
- totalQueries: 0,
169
- totalToolCalls: 0,
170
- totalDurationMs: 0
171
- };
172
- }
173
-
174
- async loadFromDisk() {
175
- try {
176
- const data = await fs.readFile(SESSION_FILE, 'utf-8');
177
- const sessionData = JSON.parse(data);
178
-
179
- this.id = sessionData.id;
180
- this.createdAt = new Date(sessionData.createdAt);
181
- this.thinkingMode = sessionData.thinkingMode || 'hide';
182
- this.messages = sessionData.messages || [];
183
- this.stats = sessionData.stats || {
184
- totalQueries: 0,
185
- totalToolCalls: 0,
186
- totalDurationMs: 0
187
- };
188
- return true;
189
- } catch (error) {
190
-
191
- return false;
192
- }
193
- }
194
-
195
- async clearDisk() {
196
- try {
197
- await fs.unlink(SESSION_FILE);
198
- return true;
199
- } catch (error) {
200
- return false;
201
- }
202
- }
203
-
204
- async exportToMarkdown() {
205
- const exportDir = EXPORT_DIR;
206
- try {
207
- await fs.mkdir(exportDir, { recursive: true });
208
- } catch {}
209
-
210
- const now = new Date();
211
- const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
212
- const filename = `kiku-session-${timestamp}.md`;
213
- const filePath = path.join(exportDir, filename);
214
-
215
- let md = `# Kiku AI Agent - Session Transcript\n\n`;
216
- md += `**Session ID:** \`${this.id}\`\n`;
217
- md += `**Date:** ${now.toISOString().replace('T', ' ').slice(0, 19)} UTC\n`;
218
- md += `**Total Queries:** ${this.stats.totalQueries} | **Tool Invocations:** ${this.stats.totalToolCalls}\n\n---\n\n`;
219
-
220
- for (const msg of this.messages) {
221
- if (msg.role === 'user') {
222
- const textPart = msg.parts.find(p => p.type === 'text');
223
- md += `## User\n${textPart?.content || ''}\n\n`;
224
- } else if (msg.role === 'assistant') {
225
- md += `## Kiku AI\n\n`;
226
- for (const part of msg.parts) {
227
- if (part.type === 'reasoning' && part.content) {
228
- md += `<details><summary>Reasoning Process (${part.durationMs ? (part.durationMs/1000).toFixed(1) + 's' : 'Complete'})</summary>\n\n\`\`\`\n${part.content.trim()}\n\`\`\`\n</details>\n\n`;
229
- } else if (part.type === 'tool') {
230
- md += `> >> **Tool Execution: \`${part.name}\`** (${part.durationMs}ms)\n`;
231
- md += `> - Arguments: \`${JSON.stringify(part.args)}\`\n`;
232
- md += `> - Status: \`${part.result?.status || 'success'}\`\n\n`;
233
- } else if (part.type === 'text') {
234
- md += `${part.content}\n\n`;
235
- }
236
- }
237
- md += `---\n\n`;
238
- }
239
- }
240
-
241
- await fs.writeFile(filePath, md, 'utf-8');
242
- return { filePath, filename };
243
- }
244
- }
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { EXPORT_DIR, SESSION_DIR as DATA_SESSION_DIR } from './appPaths.js';
4
+
5
+ const SESSION_DIR = DATA_SESSION_DIR;
6
+ const SESSION_FILE = path.join(SESSION_DIR, 'current_session.json');
7
+
8
+ export class AgentSession {
9
+ constructor(options = {}) {
10
+ this.id = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
11
+ this.createdAt = new Date();
12
+ this.thinkingMode = options.thinkingMode || 'hide';
13
+ this.messages = [];
14
+ this.stats = {
15
+ totalQueries: 0,
16
+ totalToolCalls: 0,
17
+ totalDurationMs: 0,
18
+ };
19
+ this._storageWarningShown = false;
20
+ }
21
+
22
+ setThinkingMode(mode) {
23
+ if (mode === 'show' || mode === 'hide') {
24
+ this.thinkingMode = mode;
25
+ return true;
26
+ }
27
+ return false;
28
+ }
29
+
30
+ toggleThinkingMode() {
31
+ this.thinkingMode = this.thinkingMode === 'show' ? 'hide' : 'show';
32
+ return this.thinkingMode;
33
+ }
34
+
35
+ createUserMessage(text, meta = {}) {
36
+ this.stats.totalQueries++;
37
+ const msg = {
38
+ id: `msg_${Date.now()}_u`,
39
+ role: 'user',
40
+ queryNumber: meta.queryNumber || this.stats.totalQueries,
41
+ createdAt: new Date(),
42
+ parts: [
43
+ { type: 'text', content: String(text || '') }
44
+ ]
45
+ };
46
+ this.messages.push(msg);
47
+ this.saveToDisk().catch(() => {});
48
+ return msg;
49
+ }
50
+
51
+ createAssistantMessage(meta = {}) {
52
+ const msg = {
53
+ id: `msg_${Date.now()}_a`,
54
+ role: 'assistant',
55
+ queryNumber: meta.queryNumber || this.stats.totalQueries,
56
+ provider: meta.provider || null,
57
+ model: meta.model || null,
58
+ createdAt: new Date(),
59
+ durationMs: 0,
60
+ parts: []
61
+ };
62
+ this.messages.push(msg);
63
+ return msg;
64
+ }
65
+
66
+ addReasoningPart(message, reasoningText) {
67
+ if (!message || !Array.isArray(message.parts)) return null;
68
+ let part = message.parts.find(p => p.type === 'reasoning');
69
+ if (!part) {
70
+ part = {
71
+ type: 'reasoning',
72
+ id: `rsn_${Date.now()}`,
73
+ content: '',
74
+ startedAt: Date.now(),
75
+ endedAt: null,
76
+ durationMs: 0,
77
+ };
78
+ message.parts.push(part);
79
+ }
80
+ part.content += reasoningText;
81
+ return part;
82
+ }
83
+
84
+ finalizeReasoningPart(message) {
85
+ if (!message || !Array.isArray(message.parts)) return;
86
+ const part = message.parts.find(p => p.type === 'reasoning');
87
+ if (part && !part.endedAt) {
88
+ part.endedAt = Date.now();
89
+ part.durationMs = Math.max(0, part.endedAt - part.startedAt);
90
+ }
91
+ }
92
+
93
+ addToolPart(message, toolName, args) {
94
+ if (!message || !Array.isArray(message.parts)) return null;
95
+ const part = {
96
+ type: 'tool',
97
+ id: `tool_${Date.now()}_${message.parts.length}`,
98
+ name: toolName,
99
+ args: args || {},
100
+ result: null,
101
+ status: 'running',
102
+ startedAt: Date.now(),
103
+ endedAt: null,
104
+ durationMs: 0,
105
+ };
106
+ message.parts.push(part);
107
+ this.stats.totalToolCalls++;
108
+ return part;
109
+ }
110
+
111
+ finalizeToolPart(message, toolId, result) {
112
+ if (!message || !Array.isArray(message.parts)) return null;
113
+ const part = message.parts.find(p => p.id === toolId || (p.type === 'tool' && !p.endedAt));
114
+ if (part) {
115
+ part.result = result;
116
+ part.endedAt = Date.now();
117
+ part.durationMs = Math.max(0, part.endedAt - part.startedAt);
118
+ if (result?.status === 'error') {
119
+ part.status = 'error';
120
+ } else if (result?.status === 'not_found' || (result?.count === 0)) {
121
+ part.status = 'not_found';
122
+ } else {
123
+ part.status = 'ok';
124
+ }
125
+ }
126
+ return part;
127
+ }
128
+
129
+ addErrorPart(message, error) {
130
+ if (!message || !Array.isArray(message.parts)) return null;
131
+ const part = {
132
+ type: 'error',
133
+ id: `err_${Date.now()}`,
134
+ message: error?.message || String(error || 'Unknown error'),
135
+ code: error?.code || null,
136
+ createdAt: new Date(),
137
+ };
138
+ message.parts.push(part);
139
+ return part;
140
+ }
141
+
142
+ addTextPart(message, textChunk) {
143
+ if (!message || !Array.isArray(message.parts)) return null;
144
+ let part = message.parts.find(p => p.type === 'text');
145
+ if (!part) {
146
+ part = {
147
+ type: 'text',
148
+ content: ''
149
+ };
150
+ message.parts.push(part);
151
+ }
152
+ part.content += textChunk;
153
+ return part;
154
+ }
155
+
156
+ getToolStats() {
157
+ const stats = {};
158
+ for (const msg of this.messages) {
159
+ for (const part of msg.parts || []) {
160
+ if (part.type === 'tool') {
161
+ const name = part.name || 'unknown';
162
+ if (!stats[name]) {
163
+ stats[name] = { count: 0, totalDurationMs: 0, successCount: 0, errorCount: 0, notFoundCount: 0 };
164
+ }
165
+ stats[name].count++;
166
+ stats[name].totalDurationMs += part.durationMs || 0;
167
+ if (part.status === 'error') stats[name].errorCount++;
168
+ else if (part.status === 'not_found') stats[name].notFoundCount++;
169
+ else stats[name].successCount++;
170
+ }
171
+ }
172
+ }
173
+ return stats;
174
+ }
175
+
176
+ async saveToDisk() {
177
+ try {
178
+ await fs.mkdir(SESSION_DIR, { recursive: true });
179
+ const sessionData = {
180
+ id: this.id,
181
+ createdAt: this.createdAt,
182
+ thinkingMode: this.thinkingMode,
183
+ messages: this.messages,
184
+ stats: this.stats,
185
+ };
186
+ await fs.writeFile(SESSION_FILE, JSON.stringify(sessionData, null, 2), 'utf-8');
187
+ return true;
188
+ } catch (error) {
189
+ if (!this._storageWarningShown) {
190
+ this._storageWarningShown = true;
191
+ console.error(`Kiku session is running without persistence (${error.code || 'unknown'}) in ${SESSION_DIR}.`);
192
+ }
193
+ return false;
194
+ }
195
+ }
196
+
197
+ clear() {
198
+ this.messages = [];
199
+ this.stats.totalQueries = 0;
200
+ this.stats.totalToolCalls = 0;
201
+ this.stats.totalDurationMs = 0;
202
+ }
203
+
204
+ toSnapshot() {
205
+ return {
206
+ id: this.id,
207
+ createdAt: this.createdAt,
208
+ thinkingMode: this.thinkingMode,
209
+ messages: this.messages,
210
+ stats: { ...this.stats },
211
+ };
212
+ }
213
+
214
+ restoreFromSnapshot(sessionData) {
215
+ if (!sessionData) return false;
216
+ this.id = sessionData.id || `sess_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
217
+ this.createdAt = sessionData.createdAt ? new Date(sessionData.createdAt) : new Date();
218
+ this.thinkingMode = sessionData.thinkingMode || 'hide';
219
+ this.messages = Array.isArray(sessionData.messages) ? sessionData.messages : [];
220
+ this.stats = sessionData.stats || {
221
+ totalQueries: 0,
222
+ totalToolCalls: 0,
223
+ totalDurationMs: 0
224
+ };
225
+ return true;
226
+ }
227
+
228
+ reset() {
229
+ this.id = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
230
+ this.createdAt = new Date();
231
+ this.messages = [];
232
+ this.stats = {
233
+ totalQueries: 0,
234
+ totalToolCalls: 0,
235
+ totalDurationMs: 0
236
+ };
237
+ }
238
+
239
+ async loadFromDisk() {
240
+ try {
241
+ const data = await fs.readFile(SESSION_FILE, 'utf-8');
242
+ const sessionData = JSON.parse(data);
243
+
244
+ this.id = sessionData.id;
245
+ this.createdAt = new Date(sessionData.createdAt);
246
+ this.thinkingMode = sessionData.thinkingMode || 'hide';
247
+ this.messages = sessionData.messages || [];
248
+ this.stats = sessionData.stats || {
249
+ totalQueries: 0,
250
+ totalToolCalls: 0,
251
+ totalDurationMs: 0
252
+ };
253
+ return true;
254
+ } catch {
255
+ return false;
256
+ }
257
+ }
258
+
259
+ async clearDisk() {
260
+ try {
261
+ await fs.unlink(SESSION_FILE);
262
+ return true;
263
+ } catch {
264
+ return false;
265
+ }
266
+ }
267
+
268
+ getTranscriptMarkdown(options = {}) {
269
+ const now = new Date();
270
+ const createdStr = this.createdAt ? new Date(this.createdAt).toISOString().replace('T', ' ').slice(0, 19) : 'unknown';
271
+ const totalDurationSec = ((this.stats.totalDurationMs || 0) / 1000).toFixed(1);
272
+
273
+ let md = `# Kiku AI Agent — Session Transcript\n\n`;
274
+ md += `| Metadata | Value |\n`;
275
+ md += `| :--- | :--- |\n`;
276
+ md += `| **Session ID** | \`${this.id}\` |\n`;
277
+ md += `| **Created At** | ${createdStr} UTC |\n`;
278
+ md += `| **Total Queries** | ${this.stats.totalQueries} |\n`;
279
+ md += `| **Tool Invocations** | ${this.stats.totalToolCalls} |\n`;
280
+ md += `| **Total Duration** | ${totalDurationSec}s |\n`;
281
+ md += `| **Exported At** | ${now.toISOString().replace('T', ' ').slice(0, 19)} UTC |\n\n`;
282
+
283
+ const toolStats = this.getToolStats();
284
+ const toolEntries = Object.entries(toolStats);
285
+ if (toolEntries.length > 0) {
286
+ md += `### Tools Usage Summary\n\n`;
287
+ md += `| Tool Name | Invocations | OK | Not Found | Errors | Total Time |\n`;
288
+ md += `| :--- | :--- | :--- | :--- | :--- | :--- |\n`;
289
+ for (const [name, s] of toolEntries) {
290
+ const dur = (s.totalDurationMs / 1000).toFixed(2);
291
+ md += `| \`${name}\` | ${s.count} | ${s.successCount} | ${s.notFoundCount} | ${s.errorCount} | ${dur}s |\n`;
292
+ }
293
+ md += `\n---\n\n`;
294
+ }
295
+
296
+ let turnIndex = 1;
297
+ for (const msg of this.messages) {
298
+ if (msg.role === 'user') {
299
+ const textPart = msg.parts?.find(p => p.type === 'text');
300
+ const qNum = msg.queryNumber || turnIndex++;
301
+ md += `## Question ${qNum} (User)\n\n`;
302
+ md += `${textPart?.content || ''}\n\n`;
303
+ } else if (msg.role === 'assistant') {
304
+ const modelLabel = msg.model ? ` (${msg.model})` : '';
305
+ md += `### Kiku AI Response${modelLabel}\n\n`;
306
+
307
+ for (const part of msg.parts || []) {
308
+ if (part.type === 'reasoning' && part.content) {
309
+ const dur = part.durationMs ? `${(part.durationMs / 1000).toFixed(1)}s` : 'Complete';
310
+ md += `<details>\n<summary><b>Reasoning Process (${dur})</b></summary>\n\n\`\`\`\n${part.content.trim()}\n\`\`\`\n</details>\n\n`;
311
+ } else if (part.type === 'tool') {
312
+ const dur = part.durationMs !== undefined ? `${part.durationMs}ms` : '';
313
+ const statusTag = part.status === 'ok' ? '[OK]' : part.status === 'not_found' ? '[NOT FOUND]' : '[ERROR]';
314
+ md += `> **Tool Execution: \`${part.name}\`** ${statusTag} (${dur})\n`;
315
+ md += `> \n`;
316
+ md += `> Arguments:\n\`\`\`json\n${JSON.stringify(part.args, null, 2)}\n\`\`\`\n`;
317
+ if (part.result) {
318
+ md += `> Result:\n\`\`\`json\n${JSON.stringify(part.result, null, 2)}\n\`\`\`\n\n`;
319
+ }
320
+ } else if (part.type === 'error') {
321
+ md += `> **Error:** ${part.message}\n\n`;
322
+ } else if (part.type === 'text' && part.content) {
323
+ md += `${part.content}\n\n`;
324
+ }
325
+ }
326
+ md += `---\n\n`;
327
+ }
328
+ }
329
+
330
+ return md;
331
+ }
332
+
333
+ getTranscriptJson() {
334
+ return JSON.stringify({
335
+ id: this.id,
336
+ createdAt: this.createdAt,
337
+ stats: this.stats,
338
+ toolStats: this.getToolStats(),
339
+ messages: this.messages,
340
+ }, null, 2);
341
+ }
342
+
343
+ getTranscriptText() {
344
+ let out = `KIKU AGENT SESSION TRANSCRIPT\n`;
345
+ out += `Session ID: ${this.id}\n`;
346
+ out += `Created: ${this.createdAt}\n`;
347
+ out += `Queries: ${this.stats.totalQueries} | Tool Calls: ${this.stats.totalToolCalls}\n`;
348
+ out += `============================================================\n\n`;
349
+
350
+ for (const msg of this.messages) {
351
+ if (msg.role === 'user') {
352
+ const text = msg.parts?.find(p => p.type === 'text')?.content || '';
353
+ out += `[USER - Q${msg.queryNumber || '?'}]\n${text}\n\n`;
354
+ } else if (msg.role === 'assistant') {
355
+ out += `[KIKU AI${msg.model ? ` - ${msg.model}` : ''}]\n`;
356
+ for (const part of msg.parts || []) {
357
+ if (part.type === 'tool') {
358
+ out += ` -> Tool: ${part.name} (${part.status || 'done'})\n`;
359
+ } else if (part.type === 'text') {
360
+ out += `${part.content}\n`;
361
+ }
362
+ }
363
+ out += `\n------------------------------------------------------------\n\n`;
364
+ }
365
+ }
366
+ return out;
367
+ }
368
+
369
+ async exportToMarkdown(options = {}) {
370
+ const exportDir = EXPORT_DIR;
371
+ await fs.mkdir(exportDir, { recursive: true }).catch(() => {});
372
+
373
+ const now = new Date();
374
+ const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
375
+ const filename = `kiku-session-${timestamp}.md`;
376
+ const filePath = path.join(exportDir, filename);
377
+
378
+ const md = this.getTranscriptMarkdown(options);
379
+ await fs.writeFile(filePath, md, 'utf-8');
380
+ return { filePath, filename, format: 'markdown', size: md.length };
381
+ }
382
+
383
+ async exportToJson() {
384
+ const exportDir = EXPORT_DIR;
385
+ await fs.mkdir(exportDir, { recursive: true }).catch(() => {});
386
+
387
+ const now = new Date();
388
+ const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
389
+ const filename = `kiku-session-${timestamp}.json`;
390
+ const filePath = path.join(exportDir, filename);
391
+
392
+ const json = this.getTranscriptJson();
393
+ await fs.writeFile(filePath, json, 'utf-8');
394
+ return { filePath, filename, format: 'json', size: json.length };
395
+ }
396
+
397
+ async exportToText() {
398
+ const exportDir = EXPORT_DIR;
399
+ await fs.mkdir(exportDir, { recursive: true }).catch(() => {});
400
+
401
+ const now = new Date();
402
+ const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
403
+ const filename = `kiku-session-${timestamp}.txt`;
404
+ const filePath = path.join(exportDir, filename);
405
+
406
+ const text = this.getTranscriptText();
407
+ await fs.writeFile(filePath, text, 'utf-8');
408
+ return { filePath, filename, format: 'text', size: text.length };
409
+ }
410
+ }