create-byan-agent 2.8.1 → 2.9.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/lib/exchange/agent-packager.js +464 -0
- package/lib/exchange/conversation-exporter.js +171 -0
- package/lib/exchange/gist-client.js +257 -0
- package/package.json +1 -1
- package/src/webui/api.js +536 -1
- package/src/webui/chat/bridge.js +92 -0
- package/src/webui/chat/claude-adapter.js +174 -0
- package/src/webui/chat/cli-detector.js +156 -0
- package/src/webui/chat/codex-adapter.js +57 -0
- package/src/webui/chat/copilot-adapter.js +67 -0
- package/src/webui/chat/session-manager.js +185 -0
- package/src/webui/public/chat.css +1628 -0
- package/src/webui/public/chat.html +202 -0
- package/src/webui/public/chat.js +1512 -0
- package/src/webui/server.js +123 -2
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const yaml = require('js-yaml');
|
|
5
|
+
const zlib = require('zlib');
|
|
6
|
+
|
|
7
|
+
const BYAN_MIN_VERSION = '2.8.0';
|
|
8
|
+
const FORMAT_VERSION = '1.0';
|
|
9
|
+
const SAFE_FILENAME_RE = /^[a-zA-Z0-9_\-]+\.md$/;
|
|
10
|
+
|
|
11
|
+
class AgentPackager {
|
|
12
|
+
constructor(projectRoot) {
|
|
13
|
+
this.projectRoot = path.resolve(projectRoot);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async exportAgent(agentName, options = {}) {
|
|
17
|
+
const sanitized = sanitizeName(agentName);
|
|
18
|
+
const agentFile = await this._findAgentFile(sanitized);
|
|
19
|
+
if (!agentFile) {
|
|
20
|
+
throw new Error(`Agent "${sanitized}" not found in project`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const agentContent = await fs.readFile(agentFile, 'utf8');
|
|
24
|
+
const metadata = this._parseMetadata(agentContent, sanitized, options);
|
|
25
|
+
const files = { 'agent.md': toBase64(agentContent) };
|
|
26
|
+
|
|
27
|
+
const associated = await this._findAssociatedFiles(agentFile, sanitized);
|
|
28
|
+
for (const [key, filePath] of Object.entries(associated)) {
|
|
29
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
30
|
+
files[key] = toBase64(content);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
metadata.checksum = this._computeChecksum(files);
|
|
34
|
+
|
|
35
|
+
const pkg = {
|
|
36
|
+
format: 'byan-agent',
|
|
37
|
+
version: FORMAT_VERSION,
|
|
38
|
+
metadata,
|
|
39
|
+
files
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const jsonStr = JSON.stringify(pkg, null, 2);
|
|
43
|
+
if (options.compress) {
|
|
44
|
+
return gzipBuffer(Buffer.from(jsonStr, 'utf8'));
|
|
45
|
+
}
|
|
46
|
+
return Buffer.from(jsonStr, 'utf8');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async importAgent(data, options = {}) {
|
|
50
|
+
const pkg = await this._parsePackage(data);
|
|
51
|
+
const validation = this._validateStructure(pkg);
|
|
52
|
+
if (!validation.valid) {
|
|
53
|
+
throw new Error(`Invalid package: ${validation.errors.join(', ')}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!this._verifyChecksum(pkg)) {
|
|
57
|
+
throw new Error('Checksum verification failed — package may be corrupted');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const agentName = sanitizeName(pkg.metadata.name);
|
|
61
|
+
const targetDir = this._resolveTargetDir(agentName, options.targetModule);
|
|
62
|
+
await fs.ensureDir(targetDir);
|
|
63
|
+
|
|
64
|
+
const installedFiles = [];
|
|
65
|
+
|
|
66
|
+
const agentContent = fromBase64(pkg.files['agent.md']);
|
|
67
|
+
const agentPath = path.join(targetDir, `${agentName}.md`);
|
|
68
|
+
if (!options.overwrite && await fs.pathExists(agentPath)) {
|
|
69
|
+
throw new Error(`Agent "${agentName}" already exists at ${agentPath}. Use overwrite option.`);
|
|
70
|
+
}
|
|
71
|
+
await fs.writeFile(agentPath, agentContent, 'utf8');
|
|
72
|
+
installedFiles.push(agentPath);
|
|
73
|
+
|
|
74
|
+
const optionalFiles = {
|
|
75
|
+
'soul.md': `${agentName}-soul.md`,
|
|
76
|
+
'tao.md': `${agentName}-tao.md`,
|
|
77
|
+
'soul-memory.md': `${agentName}-soul-memory.md`
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
for (const [pkgKey, diskName] of Object.entries(optionalFiles)) {
|
|
81
|
+
if (pkg.files[pkgKey]) {
|
|
82
|
+
const content = fromBase64(pkg.files[pkgKey]);
|
|
83
|
+
const filePath = path.join(targetDir, diskName);
|
|
84
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
85
|
+
installedFiles.push(filePath);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (pkg.files['config-snippet.yaml']) {
|
|
90
|
+
const snippetPath = path.join(targetDir, 'config-snippet.yaml');
|
|
91
|
+
await fs.writeFile(snippetPath, fromBase64(pkg.files['config-snippet.yaml']), 'utf8');
|
|
92
|
+
installedFiles.push(snippetPath);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const stubFiles = await this._generateStubs(agentName, pkg.metadata);
|
|
96
|
+
installedFiles.push(...stubFiles);
|
|
97
|
+
|
|
98
|
+
return { success: true, agentName, installedFiles };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async listExportableAgents() {
|
|
102
|
+
const agents = [];
|
|
103
|
+
const scanDirs = await this._getAgentDirectories();
|
|
104
|
+
|
|
105
|
+
for (const { dir, module: mod } of scanDirs) {
|
|
106
|
+
if (!await fs.pathExists(dir)) continue;
|
|
107
|
+
const files = await fs.readdir(dir);
|
|
108
|
+
for (const file of files) {
|
|
109
|
+
if (!file.endsWith('.md')) continue;
|
|
110
|
+
if (file.includes('.backup.') || file.includes('.optimized')) continue;
|
|
111
|
+
const fullPath = path.join(dir, file);
|
|
112
|
+
const stat = await fs.stat(fullPath);
|
|
113
|
+
if (!stat.isFile()) continue;
|
|
114
|
+
|
|
115
|
+
const name = file.replace(/\.md$/, '');
|
|
116
|
+
const content = await fs.readFile(fullPath, 'utf8');
|
|
117
|
+
const fm = parseFrontmatter(content);
|
|
118
|
+
|
|
119
|
+
const hasSoul = await this._hasSoulFile(dir, name);
|
|
120
|
+
const hasTao = await this._hasTaoFile(dir, name);
|
|
121
|
+
|
|
122
|
+
agents.push({
|
|
123
|
+
name,
|
|
124
|
+
displayName: fm.description || fm.name || name,
|
|
125
|
+
module: mod,
|
|
126
|
+
path: path.relative(this.projectRoot, fullPath),
|
|
127
|
+
hasSoul,
|
|
128
|
+
hasTao
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return agents;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async validate(data) {
|
|
137
|
+
try {
|
|
138
|
+
const pkg = await this._parsePackage(data);
|
|
139
|
+
const validation = this._validateStructure(pkg);
|
|
140
|
+
if (!validation.valid) {
|
|
141
|
+
return { valid: false, metadata: pkg.metadata || null, errors: validation.errors };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const checksumOk = this._verifyChecksum(pkg);
|
|
145
|
+
if (!checksumOk) {
|
|
146
|
+
return { valid: false, metadata: pkg.metadata, errors: ['Checksum mismatch'] };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { valid: true, metadata: pkg.metadata, errors: [] };
|
|
150
|
+
} catch (err) {
|
|
151
|
+
return { valid: false, metadata: null, errors: [err.message] };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async _findAgentFile(name) {
|
|
156
|
+
const candidates = [
|
|
157
|
+
path.join(this.projectRoot, '_byan', 'agents', `${name}.md`),
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
const bmadModules = ['core', 'bmm', 'bmb', 'tea', 'cis'];
|
|
161
|
+
for (const mod of bmadModules) {
|
|
162
|
+
candidates.push(path.join(this.projectRoot, '_bmad', mod, 'agents', `${name}.md`));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const candidate of candidates) {
|
|
166
|
+
if (await fs.pathExists(candidate)) return candidate;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const githubDir = path.join(this.projectRoot, '.github', 'agents');
|
|
170
|
+
if (await fs.pathExists(githubDir)) {
|
|
171
|
+
const files = await fs.readdir(githubDir);
|
|
172
|
+
const match = files.find(f => f.includes(name) && f.endsWith('.md'));
|
|
173
|
+
if (match) return path.join(githubDir, match);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async _findAssociatedFiles(agentFile, name) {
|
|
180
|
+
const dir = path.dirname(agentFile);
|
|
181
|
+
const found = {};
|
|
182
|
+
const checks = [
|
|
183
|
+
{ key: 'soul.md', patterns: [`${name}-soul.md`, 'soul.md'] },
|
|
184
|
+
{ key: 'tao.md', patterns: [`${name}-tao.md`, 'tao.md'] },
|
|
185
|
+
{ key: 'soul-memory.md', patterns: [`${name}-soul-memory.md`, 'soul-memory.md'] }
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
for (const { key, patterns } of checks) {
|
|
189
|
+
for (const pattern of patterns) {
|
|
190
|
+
const candidate = path.join(dir, pattern);
|
|
191
|
+
if (await fs.pathExists(candidate)) {
|
|
192
|
+
found[key] = candidate;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return found;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
_parseMetadata(content, name, options) {
|
|
202
|
+
const fm = parseFrontmatter(content);
|
|
203
|
+
let displayName = fm.description || fm.name || name;
|
|
204
|
+
|
|
205
|
+
const titleMatch = content.match(/title="([^"]+)"/);
|
|
206
|
+
if (titleMatch) {
|
|
207
|
+
displayName = titleMatch[1];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
name,
|
|
212
|
+
displayName,
|
|
213
|
+
description: fm.description || displayName,
|
|
214
|
+
author: options.author || 'unknown',
|
|
215
|
+
created: new Date().toISOString(),
|
|
216
|
+
byanMinVersion: BYAN_MIN_VERSION,
|
|
217
|
+
checksum: ''
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
_computeChecksum(files) {
|
|
222
|
+
const hash = crypto.createHash('sha256');
|
|
223
|
+
const keys = Object.keys(files).sort();
|
|
224
|
+
for (const key of keys) {
|
|
225
|
+
hash.update(files[key]);
|
|
226
|
+
}
|
|
227
|
+
return hash.digest('hex');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
_verifyChecksum(pkg) {
|
|
231
|
+
const filesCopy = Object.assign({}, pkg.files);
|
|
232
|
+
const expected = pkg.metadata.checksum;
|
|
233
|
+
const actual = this._computeChecksum(filesCopy);
|
|
234
|
+
return expected === actual;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
_validateStructure(pkg) {
|
|
238
|
+
const errors = [];
|
|
239
|
+
|
|
240
|
+
if (!pkg || typeof pkg !== 'object') {
|
|
241
|
+
return { valid: false, errors: ['Package is not a valid object'] };
|
|
242
|
+
}
|
|
243
|
+
if (pkg.format !== 'byan-agent') {
|
|
244
|
+
errors.push(`Invalid format: expected "byan-agent", got "${pkg.format}"`);
|
|
245
|
+
}
|
|
246
|
+
if (!pkg.version) {
|
|
247
|
+
errors.push('Missing version field');
|
|
248
|
+
}
|
|
249
|
+
if (!pkg.metadata || typeof pkg.metadata !== 'object') {
|
|
250
|
+
errors.push('Missing or invalid metadata');
|
|
251
|
+
} else {
|
|
252
|
+
if (!pkg.metadata.name) errors.push('Missing metadata.name');
|
|
253
|
+
if (!pkg.metadata.checksum) errors.push('Missing metadata.checksum');
|
|
254
|
+
}
|
|
255
|
+
if (!pkg.files || typeof pkg.files !== 'object') {
|
|
256
|
+
errors.push('Missing or invalid files section');
|
|
257
|
+
} else {
|
|
258
|
+
if (!pkg.files['agent.md']) {
|
|
259
|
+
errors.push('Missing required file: agent.md');
|
|
260
|
+
}
|
|
261
|
+
for (const key of Object.keys(pkg.files)) {
|
|
262
|
+
if (!SAFE_FILENAME_RE.test(key) && key !== 'config-snippet.yaml') {
|
|
263
|
+
errors.push(`Unsafe filename in package: "${key}"`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { valid: errors.length === 0, errors };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
_resolveTargetDir(agentName, targetModule) {
|
|
272
|
+
if (targetModule) {
|
|
273
|
+
const moduleDir = path.join(this.projectRoot, '_bmad', sanitizeName(targetModule), 'agents');
|
|
274
|
+
assertInsideRoot(moduleDir, this.projectRoot);
|
|
275
|
+
return moduleDir;
|
|
276
|
+
}
|
|
277
|
+
return path.join(this.projectRoot, '_bmad-output', 'bmb-creations', agentName);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async _generateStubs(agentName, metadata) {
|
|
281
|
+
const stubs = [];
|
|
282
|
+
const displayName = metadata.displayName || agentName;
|
|
283
|
+
|
|
284
|
+
const githubDir = path.join(this.projectRoot, '.github', 'agents');
|
|
285
|
+
if (await fs.pathExists(githubDir)) {
|
|
286
|
+
const stubName = `bmad-agent-${agentName}.md`;
|
|
287
|
+
const stubPath = path.join(githubDir, stubName);
|
|
288
|
+
const stubContent = [
|
|
289
|
+
'---',
|
|
290
|
+
`name: '${agentName}'`,
|
|
291
|
+
`description: '${escapeYamlString(displayName)}'`,
|
|
292
|
+
'---',
|
|
293
|
+
'',
|
|
294
|
+
"You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.",
|
|
295
|
+
'',
|
|
296
|
+
'<agent-activation CRITICAL="TRUE">',
|
|
297
|
+
`1. LOAD the FULL agent file from {project-root}/_bmad-output/bmb-creations/${agentName}/${agentName}.md`,
|
|
298
|
+
'2. READ its entire contents - this contains the complete agent persona, menu, and instructions',
|
|
299
|
+
'3. LOAD the soul activation protocol from {project-root}/_byan/core/activation/soul-activation.md and EXECUTE it silently',
|
|
300
|
+
'4. FOLLOW every step in the <activation> section precisely',
|
|
301
|
+
'5. DISPLAY the welcome/greeting as instructed',
|
|
302
|
+
'6. PRESENT the numbered menu exactly as defined in the file',
|
|
303
|
+
'7. WAIT for user input before proceeding',
|
|
304
|
+
'</agent-activation>',
|
|
305
|
+
''
|
|
306
|
+
].join('\n');
|
|
307
|
+
await fs.writeFile(stubPath, stubContent, 'utf8');
|
|
308
|
+
stubs.push(stubPath);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const codexDir = path.join(this.projectRoot, '.codex', 'prompts');
|
|
312
|
+
if (await fs.pathExists(codexDir)) {
|
|
313
|
+
const stubName = `bmad-agent-${agentName}.md`;
|
|
314
|
+
const stubPath = path.join(codexDir, stubName);
|
|
315
|
+
const stubContent = [
|
|
316
|
+
'---',
|
|
317
|
+
`name: '${agentName}'`,
|
|
318
|
+
`description: '${escapeYamlString(displayName)}'`,
|
|
319
|
+
'disable-model-invocation: true',
|
|
320
|
+
'---',
|
|
321
|
+
'',
|
|
322
|
+
"You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.",
|
|
323
|
+
'',
|
|
324
|
+
'<agent-activation CRITICAL="TRUE">',
|
|
325
|
+
`1. LOAD the FULL agent file from @bmad-output/bmb-creations/${agentName}/${agentName}.md`,
|
|
326
|
+
'2. READ its entire contents - this contains the complete agent persona, menu, and instructions',
|
|
327
|
+
'3. LOAD the soul activation protocol from @bmad/core/activation/soul-activation.md and EXECUTE it silently',
|
|
328
|
+
'4. Execute ALL activation steps exactly as written in the agent file',
|
|
329
|
+
'5. Follow the agent\'s persona and menu system precisely',
|
|
330
|
+
'6. Stay in character throughout the session',
|
|
331
|
+
'</agent-activation>',
|
|
332
|
+
''
|
|
333
|
+
].join('\n');
|
|
334
|
+
await fs.writeFile(stubPath, stubContent, 'utf8');
|
|
335
|
+
stubs.push(stubPath);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return stubs;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async _getAgentDirectories() {
|
|
342
|
+
const dirs = [
|
|
343
|
+
{ dir: path.join(this.projectRoot, '_byan', 'agents'), module: 'byan' }
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
const bmadModules = ['core', 'bmm', 'bmb', 'tea', 'cis'];
|
|
347
|
+
for (const mod of bmadModules) {
|
|
348
|
+
dirs.push({
|
|
349
|
+
dir: path.join(this.projectRoot, '_bmad', mod, 'agents'),
|
|
350
|
+
module: mod
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const bmadOutputDir = path.join(this.projectRoot, '_bmad-output', 'bmb-creations');
|
|
355
|
+
if (await fs.pathExists(bmadOutputDir)) {
|
|
356
|
+
const entries = await fs.readdir(bmadOutputDir, { withFileTypes: true });
|
|
357
|
+
for (const entry of entries) {
|
|
358
|
+
if (entry.isDirectory()) {
|
|
359
|
+
dirs.push({
|
|
360
|
+
dir: path.join(bmadOutputDir, entry.name),
|
|
361
|
+
module: 'bmb-creations'
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return dirs;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async _hasSoulFile(dir, name) {
|
|
371
|
+
return await fs.pathExists(path.join(dir, `${name}-soul.md`)) ||
|
|
372
|
+
await fs.pathExists(path.join(dir, 'soul.md'));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async _hasTaoFile(dir, name) {
|
|
376
|
+
return await fs.pathExists(path.join(dir, `${name}-tao.md`)) ||
|
|
377
|
+
await fs.pathExists(path.join(dir, 'tao.md'));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async _parsePackage(data) {
|
|
381
|
+
let buffer;
|
|
382
|
+
if (typeof data === 'string') {
|
|
383
|
+
if (await fs.pathExists(data)) {
|
|
384
|
+
buffer = await fs.readFile(data);
|
|
385
|
+
} else {
|
|
386
|
+
buffer = Buffer.from(data, 'utf8');
|
|
387
|
+
}
|
|
388
|
+
} else if (Buffer.isBuffer(data)) {
|
|
389
|
+
buffer = data;
|
|
390
|
+
} else {
|
|
391
|
+
throw new Error('Invalid input: expected Buffer, string, or file path');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
let jsonStr;
|
|
395
|
+
if (isGzipped(buffer)) {
|
|
396
|
+
jsonStr = (await gunzipBuffer(buffer)).toString('utf8');
|
|
397
|
+
} else {
|
|
398
|
+
jsonStr = buffer.toString('utf8');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
return JSON.parse(jsonStr);
|
|
403
|
+
} catch {
|
|
404
|
+
throw new Error('Failed to parse package: invalid JSON');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function sanitizeName(name) {
|
|
410
|
+
return String(name).replace(/[^a-zA-Z0-9_\-]/g, '').substring(0, 100);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function assertInsideRoot(targetPath, rootPath) {
|
|
414
|
+
const resolved = path.resolve(targetPath);
|
|
415
|
+
if (!resolved.startsWith(rootPath)) {
|
|
416
|
+
throw new Error('Path traversal detected: target is outside project root');
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function parseFrontmatter(content) {
|
|
421
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
422
|
+
if (!match) return {};
|
|
423
|
+
try {
|
|
424
|
+
return yaml.load(match[1]) || {};
|
|
425
|
+
} catch {
|
|
426
|
+
return {};
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function escapeYamlString(str) {
|
|
431
|
+
return String(str).replace(/'/g, "''");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function toBase64(str) {
|
|
435
|
+
return Buffer.from(str, 'utf8').toString('base64');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function fromBase64(str) {
|
|
439
|
+
return Buffer.from(str, 'base64').toString('utf8');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function isGzipped(buffer) {
|
|
443
|
+
return buffer.length >= 2 && buffer[0] === 0x1f && buffer[1] === 0x8b;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function gzipBuffer(buffer) {
|
|
447
|
+
return new Promise((resolve, reject) => {
|
|
448
|
+
zlib.gzip(buffer, (err, result) => {
|
|
449
|
+
if (err) reject(err);
|
|
450
|
+
else resolve(result);
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function gunzipBuffer(buffer) {
|
|
456
|
+
return new Promise((resolve, reject) => {
|
|
457
|
+
zlib.gunzip(buffer, (err, result) => {
|
|
458
|
+
if (err) reject(err);
|
|
459
|
+
else resolve(result);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
module.exports = AgentPackager;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
class ConversationExporter {
|
|
2
|
+
exportJSON(session) {
|
|
3
|
+
validateSession(session);
|
|
4
|
+
|
|
5
|
+
return JSON.stringify({
|
|
6
|
+
format: 'byan-chat',
|
|
7
|
+
version: '1.0',
|
|
8
|
+
metadata: {
|
|
9
|
+
sessionId: session.id,
|
|
10
|
+
agent: session.agent || 'unknown',
|
|
11
|
+
cli: session.cli || 'unknown',
|
|
12
|
+
created: session.created || new Date().toISOString(),
|
|
13
|
+
exported: new Date().toISOString(),
|
|
14
|
+
messageCount: (session.messages || []).length
|
|
15
|
+
},
|
|
16
|
+
messages: (session.messages || []).map(m => ({
|
|
17
|
+
role: m.role,
|
|
18
|
+
content: m.content,
|
|
19
|
+
timestamp: m.timestamp || null
|
|
20
|
+
}))
|
|
21
|
+
}, null, 2);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
exportMarkdown(session) {
|
|
25
|
+
validateSession(session);
|
|
26
|
+
|
|
27
|
+
const agentName = session.agent || 'unknown';
|
|
28
|
+
const date = formatDate(session.created);
|
|
29
|
+
const messages = session.messages || [];
|
|
30
|
+
const lines = [];
|
|
31
|
+
|
|
32
|
+
lines.push(`# BYAN Chat -- ${agentName} -- ${date}`);
|
|
33
|
+
lines.push('');
|
|
34
|
+
lines.push('## Session Info');
|
|
35
|
+
lines.push(`- Agent: ${agentName}`);
|
|
36
|
+
lines.push(`- CLI: ${session.cli || 'unknown'}`);
|
|
37
|
+
lines.push(`- Messages: ${messages.length}`);
|
|
38
|
+
lines.push('');
|
|
39
|
+
lines.push('---');
|
|
40
|
+
lines.push('');
|
|
41
|
+
|
|
42
|
+
for (const msg of messages) {
|
|
43
|
+
const heading = msg.role === 'user'
|
|
44
|
+
? '### User'
|
|
45
|
+
: `### Assistant (${agentName})`;
|
|
46
|
+
lines.push(heading);
|
|
47
|
+
lines.push('');
|
|
48
|
+
lines.push(sanitizeMarkdown(msg.content));
|
|
49
|
+
lines.push('');
|
|
50
|
+
lines.push('---');
|
|
51
|
+
lines.push('');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return lines.join('\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
exportTemplate(session) {
|
|
58
|
+
validateSession(session);
|
|
59
|
+
|
|
60
|
+
const prompts = (session.messages || [])
|
|
61
|
+
.filter(m => m.role === 'user')
|
|
62
|
+
.map(m => m.content);
|
|
63
|
+
|
|
64
|
+
return JSON.stringify({
|
|
65
|
+
format: 'byan-template',
|
|
66
|
+
version: '1.0',
|
|
67
|
+
metadata: {
|
|
68
|
+
agent: session.agent || 'unknown',
|
|
69
|
+
description: `Template from session ${session.id || 'unknown'}`,
|
|
70
|
+
promptCount: prompts.length
|
|
71
|
+
},
|
|
72
|
+
prompts
|
|
73
|
+
}, null, 2);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
importJSON(data) {
|
|
77
|
+
const parsed = parseJSONSafe(data);
|
|
78
|
+
if (!parsed || parsed.format !== 'byan-chat') {
|
|
79
|
+
throw new Error('Invalid .byan-chat format');
|
|
80
|
+
}
|
|
81
|
+
if (!parsed.version) {
|
|
82
|
+
throw new Error('Missing version in .byan-chat');
|
|
83
|
+
}
|
|
84
|
+
if (!Array.isArray(parsed.messages)) {
|
|
85
|
+
throw new Error('Missing or invalid messages array');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const msg of parsed.messages) {
|
|
89
|
+
if (!msg.role || typeof msg.content !== 'string') {
|
|
90
|
+
throw new Error('Invalid message structure: each message must have role and content');
|
|
91
|
+
}
|
|
92
|
+
if (!['user', 'assistant', 'system'].includes(msg.role)) {
|
|
93
|
+
throw new Error(`Invalid message role: "${msg.role}"`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
id: (parsed.metadata && parsed.metadata.sessionId) || null,
|
|
99
|
+
agent: (parsed.metadata && parsed.metadata.agent) || 'unknown',
|
|
100
|
+
cli: (parsed.metadata && parsed.metadata.cli) || 'unknown',
|
|
101
|
+
created: (parsed.metadata && parsed.metadata.created) || new Date().toISOString(),
|
|
102
|
+
messages: parsed.messages
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
importTemplate(data) {
|
|
107
|
+
const parsed = parseJSONSafe(data);
|
|
108
|
+
if (!parsed || parsed.format !== 'byan-template') {
|
|
109
|
+
throw new Error('Invalid .byan-template format');
|
|
110
|
+
}
|
|
111
|
+
if (!Array.isArray(parsed.prompts)) {
|
|
112
|
+
throw new Error('Missing or invalid prompts array');
|
|
113
|
+
}
|
|
114
|
+
for (const prompt of parsed.prompts) {
|
|
115
|
+
if (typeof prompt !== 'string') {
|
|
116
|
+
throw new Error('Each prompt must be a string');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
agent: (parsed.metadata && parsed.metadata.agent) || 'unknown',
|
|
122
|
+
description: (parsed.metadata && parsed.metadata.description) || '',
|
|
123
|
+
prompts: parsed.prompts
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function validateSession(session) {
|
|
129
|
+
if (!session || typeof session !== 'object') {
|
|
130
|
+
throw new Error('Invalid session object');
|
|
131
|
+
}
|
|
132
|
+
if (!Array.isArray(session.messages)) {
|
|
133
|
+
throw new Error('Session must contain a messages array');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parseJSONSafe(data) {
|
|
138
|
+
if (typeof data === 'string') {
|
|
139
|
+
try {
|
|
140
|
+
return JSON.parse(data);
|
|
141
|
+
} catch {
|
|
142
|
+
throw new Error('Failed to parse JSON data');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (Buffer.isBuffer(data)) {
|
|
146
|
+
try {
|
|
147
|
+
return JSON.parse(data.toString('utf8'));
|
|
148
|
+
} catch {
|
|
149
|
+
throw new Error('Failed to parse JSON data from buffer');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (typeof data === 'object') {
|
|
153
|
+
return data;
|
|
154
|
+
}
|
|
155
|
+
throw new Error('Invalid input: expected string, Buffer, or object');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function sanitizeMarkdown(content) {
|
|
159
|
+
return String(content || '');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function formatDate(isoString) {
|
|
163
|
+
if (!isoString) return new Date().toISOString().split('T')[0];
|
|
164
|
+
try {
|
|
165
|
+
return new Date(isoString).toISOString().split('T')[0];
|
|
166
|
+
} catch {
|
|
167
|
+
return isoString;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = ConversationExporter;
|