osborn 0.9.3 → 0.9.6
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/.claude/settings.local.json +9 -0
- package/.claude/skills/browser-apply/SKILL.md +114 -0
- package/.claude/skills/markdown-to-pdf/SKILL.md +29 -0
- package/.claude/skills/pdf-to-markdown/SKILL.md +28 -0
- package/.claude/skills/playwright-browser/SKILL.md +90 -0
- package/.claude/skills/shadcn/SKILL.md +232 -0
- package/.claude/skills/shadcn/image.png +0 -0
- package/.claude/skills/youtube-transcript/SKILL.md +24 -0
- package/caresource-apply.js +50 -0
- package/caresource-apply.mjs +34 -0
- package/dist/conversation-brain.d.ts +92 -0
- package/dist/conversation-brain.js +360 -0
- package/dist/fast-llm.d.ts +15 -0
- package/dist/fast-llm.js +81 -0
- package/dist/index.js +265 -79
- package/dist/pipeline-direct-llm.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,11 +10,11 @@ initializeLogger({ pretty: true, level: 'info' });
|
|
|
10
10
|
import { setMaxListeners } from 'node:events';
|
|
11
11
|
setMaxListeners(50);
|
|
12
12
|
import { createServer } from 'http';
|
|
13
|
-
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, mkdtempSync, cpSync } from 'node:fs';
|
|
13
|
+
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, mkdtempSync, cpSync, rmSync, renameSync, statSync, createWriteStream } from 'node:fs';
|
|
14
14
|
import { dirname, join } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import { spawn } from 'node:child_process';
|
|
17
|
-
import { homedir } from 'node:os';
|
|
17
|
+
import { homedir, tmpdir } from 'node:os';
|
|
18
18
|
// Resolve __dirname for this ESM module so we can find sibling files (e.g.
|
|
19
19
|
// meeting-output.html) relative to the compiled JS location, NOT process.cwd().
|
|
20
20
|
// In production cwd is the user's workspace; the static file lives next to dist/index.js.
|
|
@@ -308,47 +308,61 @@ function startApiServer(workingDir, port) {
|
|
|
308
308
|
return;
|
|
309
309
|
}
|
|
310
310
|
}
|
|
311
|
-
const
|
|
311
|
+
const claudeDir = join(homedir(), '.claude');
|
|
312
|
+
const projectsDir = join(claudeDir, 'projects');
|
|
312
313
|
const workDir = url.searchParams.get('workDir');
|
|
313
|
-
if (workDir) {
|
|
314
|
-
const slug = workDir.replace(/\//g, '-');
|
|
315
|
-
const slugDir = join(projectsDir, slug);
|
|
316
|
-
if (!existsSync(slugDir)) {
|
|
317
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
318
|
-
res.end(JSON.stringify({ error: 'Project not found', slug }));
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
res.writeHead(200, {
|
|
322
|
-
'Content-Type': 'application/gzip',
|
|
323
|
-
'Content-Disposition': `attachment; filename="claude-sessions-${slug}.tar.gz"`,
|
|
324
|
-
'Access-Control-Allow-Origin': '*',
|
|
325
|
-
});
|
|
326
|
-
// Use '--' to prevent tar from interpreting the leading-hyphen slug as flags
|
|
327
|
-
const tar = spawn('tar', ['-czf', '-', '-C', projectsDir, '--', slug]);
|
|
328
|
-
tar.stdout.pipe(res);
|
|
329
|
-
tar.stderr.on('data', (d) => console.error('[export]', d.toString()));
|
|
330
|
-
tar.on('close', (code) => { if (code !== 0)
|
|
331
|
-
res.destroy(); });
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
314
|
if (!existsSync(projectsDir)) {
|
|
335
315
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
336
316
|
res.end(JSON.stringify({ error: 'No sessions found' }));
|
|
337
317
|
return;
|
|
338
318
|
}
|
|
319
|
+
const tarArgs = ['-czf', '-', '-C', claudeDir, 'projects'];
|
|
320
|
+
void workDir; // workDir param accepted but full projects/ export is returned
|
|
339
321
|
res.writeHead(200, {
|
|
340
322
|
'Content-Type': 'application/gzip',
|
|
341
323
|
'Content-Disposition': 'attachment; filename="claude-sessions.tar.gz"',
|
|
342
324
|
'Access-Control-Allow-Origin': '*',
|
|
343
325
|
});
|
|
344
326
|
// Stream tar output directly to response
|
|
345
|
-
const tar = spawn('tar',
|
|
327
|
+
const tar = spawn('tar', tarArgs);
|
|
346
328
|
tar.stdout.pipe(res);
|
|
347
329
|
tar.stderr.on('data', (d) => console.error('[export]', d.toString()));
|
|
348
330
|
tar.on('close', (code) => { if (code !== 0)
|
|
349
331
|
res.destroy(); });
|
|
350
332
|
return;
|
|
351
333
|
}
|
|
334
|
+
// GET /sessions/manifest — return mtime+size for all .jsonl files per slug (public, no auth)
|
|
335
|
+
if (req.method === 'GET' && url.pathname === '/sessions/manifest') {
|
|
336
|
+
const claudeDir = join(homedir(), '.claude', 'projects');
|
|
337
|
+
const slugMap = {};
|
|
338
|
+
try {
|
|
339
|
+
const slugs = readdirSync(claudeDir, { withFileTypes: true })
|
|
340
|
+
.filter(d => d.isDirectory())
|
|
341
|
+
.map(d => d.name);
|
|
342
|
+
for (const slug of slugs) {
|
|
343
|
+
const slugDir = join(claudeDir, slug);
|
|
344
|
+
try {
|
|
345
|
+
const jsonlFiles = readdirSync(slugDir)
|
|
346
|
+
.filter(f => f.endsWith('.jsonl'));
|
|
347
|
+
const fileStats = {};
|
|
348
|
+
for (const file of jsonlFiles) {
|
|
349
|
+
const st = statSync(join(slugDir, file));
|
|
350
|
+
fileStats[file] = { mtime: st.mtimeMs, size: st.size };
|
|
351
|
+
}
|
|
352
|
+
slugMap[slug] = { files: fileStats };
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
// skip unreadable dirs
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// projects dir doesn't exist yet — return empty
|
|
361
|
+
}
|
|
362
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
363
|
+
res.end(JSON.stringify({ slugs: slugMap }));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
352
366
|
// POST /sessions/import — accept a gzipped tar and extract into ~/.claude/projects/
|
|
353
367
|
if (req.method === 'POST' && url.pathname === '/sessions/import') {
|
|
354
368
|
if (syncToken) {
|
|
@@ -360,78 +374,250 @@ function startApiServer(workingDir, port) {
|
|
|
360
374
|
}
|
|
361
375
|
}
|
|
362
376
|
const targetWorkDir = url.searchParams.get('targetWorkDir');
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
377
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'osborn-import-'));
|
|
378
|
+
const tarProc = spawn('tar', ['-xf', '-', '-C', tmpDir]);
|
|
379
|
+
// Streaming: pipe request body directly to tar stdin — no buffering
|
|
380
|
+
req.pipe(tarProc.stdin);
|
|
381
|
+
tarProc.stdin.on('error', (err) => {
|
|
382
|
+
console.error('[import] tar stdin error', err);
|
|
383
|
+
tarProc.kill('SIGTERM');
|
|
384
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
385
|
+
if (!res.headersSent) {
|
|
386
|
+
res.writeHead(500);
|
|
387
|
+
res.end(JSON.stringify({ error: 'upload error' }));
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
req.on('aborted', () => {
|
|
391
|
+
tarProc.kill('SIGTERM');
|
|
392
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
393
|
+
});
|
|
394
|
+
tarProc.stderr.on('data', (d) => console.error('[import]', d.toString()));
|
|
395
|
+
tarProc.on('close', async (code) => {
|
|
368
396
|
try {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
397
|
+
if (code !== 0) {
|
|
398
|
+
res.writeHead(500);
|
|
399
|
+
res.end(JSON.stringify({ error: 'tar extraction failed', code }));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const claudeDir = join(homedir(), '.claude');
|
|
403
|
+
const projectsDir = join(claudeDir, 'projects');
|
|
404
|
+
mkdirSync(projectsDir, { recursive: true });
|
|
405
|
+
// The archive should contain a 'projects' subdirectory
|
|
406
|
+
const extractedProjects = join(tmpDir, 'projects');
|
|
407
|
+
const sourceDir = existsSync(extractedProjects) ? extractedProjects : tmpDir;
|
|
408
|
+
// Optionally remap slug: if targetWorkDir is provided, find slug(s)
|
|
409
|
+
// that don't match the target and rename them
|
|
410
|
+
const remapped = {};
|
|
411
|
+
if (targetWorkDir) {
|
|
412
|
+
const targetSlug = targetWorkDir.replace(/\//g, '-');
|
|
413
|
+
const sourceSlugs = readdirSync(sourceDir);
|
|
414
|
+
for (const slug of sourceSlugs) {
|
|
415
|
+
if (slug !== targetSlug && !slug.startsWith('.')) {
|
|
416
|
+
remapped[slug] = targetSlug;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// Copy subdirectories into ~/.claude/projects/, merging and updating existing files
|
|
421
|
+
let filesWritten = 0;
|
|
422
|
+
const slugsInSource = readdirSync(sourceDir);
|
|
423
|
+
for (const slug of slugsInSource) {
|
|
424
|
+
const effectiveSlug = remapped[slug] ?? slug;
|
|
425
|
+
const destSlug = join(projectsDir, effectiveSlug);
|
|
426
|
+
mkdirSync(destSlug, { recursive: true });
|
|
427
|
+
try {
|
|
428
|
+
renameSync(join(sourceDir, slug), destSlug);
|
|
429
|
+
}
|
|
430
|
+
catch (e) {
|
|
431
|
+
if (e.code === 'EXDEV') {
|
|
432
|
+
// Cross-filesystem fallback
|
|
433
|
+
cpSync(join(sourceDir, slug), destSlug, { recursive: true, force: true, errorOnExist: false });
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
throw e;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
filesWritten++;
|
|
440
|
+
}
|
|
441
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
442
|
+
res.end(JSON.stringify({ ok: true, filesWritten, remapped }));
|
|
443
|
+
}
|
|
444
|
+
catch (err) {
|
|
445
|
+
console.error('[import] merge error:', err);
|
|
446
|
+
if (!res.headersSent) {
|
|
447
|
+
res.writeHead(500);
|
|
448
|
+
res.end(JSON.stringify({ error: 'Failed to merge sessions', detail: String(err) }));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
// POST /sessions/import-chunk — accept a single chunk of a multi-part upload
|
|
458
|
+
if (req.method === 'POST' && url.pathname === '/sessions/import-chunk') {
|
|
459
|
+
if (syncToken) {
|
|
460
|
+
const authHeader = req.headers['authorization'] ?? '';
|
|
461
|
+
if (authHeader !== `Bearer ${syncToken}`) {
|
|
462
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
463
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const uploadId = url.searchParams.get('uploadId');
|
|
468
|
+
const chunkIndex = parseInt(url.searchParams.get('chunk') || '0');
|
|
469
|
+
if (!uploadId) {
|
|
470
|
+
res.writeHead(400);
|
|
471
|
+
res.end(JSON.stringify({ error: 'uploadId required' }));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
// Chunk storage dir: /tmp/osborn-upload-<uploadId>/
|
|
475
|
+
const uploadDir = join(tmpdir(), `osborn-upload-${uploadId}`);
|
|
476
|
+
mkdirSync(uploadDir, { recursive: true });
|
|
477
|
+
// Write chunk to padded filename for correct sort order
|
|
478
|
+
const chunkPath = join(uploadDir, `chunk-${String(chunkIndex).padStart(6, '0')}`);
|
|
479
|
+
const writeStream = createWriteStream(chunkPath);
|
|
480
|
+
req.pipe(writeStream);
|
|
481
|
+
writeStream.on('finish', () => {
|
|
482
|
+
res.writeHead(200);
|
|
483
|
+
res.end(JSON.stringify({ ok: true, chunk: chunkIndex }));
|
|
484
|
+
});
|
|
485
|
+
writeStream.on('error', (err) => {
|
|
486
|
+
console.error('[import-chunk] write error', err);
|
|
487
|
+
if (!res.headersSent) {
|
|
488
|
+
res.writeHead(500);
|
|
489
|
+
res.end(JSON.stringify({ error: 'write failed' }));
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
req.on('aborted', () => { writeStream.destroy(); });
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
// POST /sessions/import-finalize — reassemble chunks, extract, apply slug merge
|
|
496
|
+
if (req.method === 'POST' && url.pathname === '/sessions/import-finalize') {
|
|
497
|
+
if (syncToken) {
|
|
498
|
+
const authHeader = req.headers['authorization'] ?? '';
|
|
499
|
+
if (authHeader !== `Bearer ${syncToken}`) {
|
|
500
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
501
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const uploadId = url.searchParams.get('uploadId');
|
|
506
|
+
const total = parseInt(url.searchParams.get('total') || '0');
|
|
507
|
+
const targetWorkDir = url.searchParams.get('targetWorkDir') ?? undefined;
|
|
508
|
+
if (!uploadId || total === 0) {
|
|
509
|
+
res.writeHead(400);
|
|
510
|
+
res.end(JSON.stringify({ error: 'uploadId and total required' }));
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const uploadDir = join(tmpdir(), `osborn-upload-${uploadId}`);
|
|
514
|
+
// Verify all chunks present
|
|
515
|
+
const expectedChunks = Array.from({ length: total }, (_, i) => `chunk-${String(i).padStart(6, '0')}`);
|
|
516
|
+
const presentChunks = existsSync(uploadDir) ? readdirSync(uploadDir).filter(f => f.startsWith('chunk-')).sort() : [];
|
|
517
|
+
const missing = expectedChunks.filter(c => !presentChunks.includes(c));
|
|
518
|
+
if (missing.length > 0) {
|
|
519
|
+
res.writeHead(400);
|
|
520
|
+
res.end(JSON.stringify({ error: 'missing chunks', missing }));
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const tmpExtractDir = mkdtempSync(join(tmpdir(), 'osborn-import-'));
|
|
524
|
+
try {
|
|
525
|
+
// Reassemble chunks into a single stream and pipe to tar
|
|
526
|
+
const tarProc = spawn('tar', ['-xf', '-', '-C', tmpExtractDir]);
|
|
527
|
+
// Stream chunks in order to tar stdin
|
|
528
|
+
const streamChunks = async () => {
|
|
529
|
+
for (const chunkFile of expectedChunks) {
|
|
530
|
+
const chunkData = readFileSync(join(uploadDir, chunkFile));
|
|
531
|
+
await new Promise((resolve, reject) => {
|
|
532
|
+
tarProc.stdin.write(chunkData, (err) => {
|
|
533
|
+
if (err)
|
|
534
|
+
reject(err);
|
|
535
|
+
else
|
|
536
|
+
resolve();
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
}
|
|
372
540
|
tarProc.stdin.end();
|
|
373
|
-
|
|
374
|
-
|
|
541
|
+
};
|
|
542
|
+
streamChunks().catch(err => {
|
|
543
|
+
console.error('[import-finalize] chunk stream error', err);
|
|
544
|
+
tarProc.kill('SIGTERM');
|
|
545
|
+
});
|
|
546
|
+
tarProc.stderr.on('data', (d) => console.error('[import-finalize]', d.toString()));
|
|
547
|
+
tarProc.on('close', async (code) => {
|
|
548
|
+
try {
|
|
375
549
|
if (code !== 0) {
|
|
376
|
-
res.writeHead(500
|
|
550
|
+
res.writeHead(500);
|
|
377
551
|
res.end(JSON.stringify({ error: 'tar extraction failed', code }));
|
|
378
552
|
return;
|
|
379
553
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
remapped[slug] = newSlug;
|
|
396
|
-
}
|
|
554
|
+
const claudeDir = join(homedir(), '.claude');
|
|
555
|
+
const projectsDir = join(claudeDir, 'projects');
|
|
556
|
+
mkdirSync(projectsDir, { recursive: true });
|
|
557
|
+
// The archive should contain a 'projects' subdirectory
|
|
558
|
+
const extractedProjects = join(tmpExtractDir, 'projects');
|
|
559
|
+
const sourceDir = existsSync(extractedProjects) ? extractedProjects : tmpExtractDir;
|
|
560
|
+
// Optionally remap slug: if targetWorkDir is provided, find slug(s)
|
|
561
|
+
// that don't match the target and rename them
|
|
562
|
+
const remapped = {};
|
|
563
|
+
if (targetWorkDir) {
|
|
564
|
+
const targetSlug = targetWorkDir.replace(/\//g, '-');
|
|
565
|
+
const sourceSlugs = readdirSync(sourceDir);
|
|
566
|
+
for (const slug of sourceSlugs) {
|
|
567
|
+
if (slug !== targetSlug && !slug.startsWith('.')) {
|
|
568
|
+
remapped[slug] = targetSlug;
|
|
397
569
|
}
|
|
398
570
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
errorOnExist: false,
|
|
410
|
-
});
|
|
411
|
-
filesWritten++;
|
|
571
|
+
}
|
|
572
|
+
// Copy subdirectories into ~/.claude/projects/, merging and updating existing files
|
|
573
|
+
let filesWritten = 0;
|
|
574
|
+
const slugsInSource = readdirSync(sourceDir);
|
|
575
|
+
for (const slug of slugsInSource) {
|
|
576
|
+
const effectiveSlug = remapped[slug] ?? slug;
|
|
577
|
+
const destSlug = join(projectsDir, effectiveSlug);
|
|
578
|
+
mkdirSync(destSlug, { recursive: true });
|
|
579
|
+
try {
|
|
580
|
+
renameSync(join(sourceDir, slug), destSlug);
|
|
412
581
|
}
|
|
413
|
-
|
|
414
|
-
|
|
582
|
+
catch (e) {
|
|
583
|
+
if (e.code === 'EXDEV') {
|
|
584
|
+
// Cross-filesystem fallback
|
|
585
|
+
cpSync(join(sourceDir, slug), destSlug, { recursive: true, force: true, errorOnExist: false });
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
throw e;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
filesWritten++;
|
|
415
592
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
593
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
594
|
+
res.end(JSON.stringify({ ok: true, filesWritten, remapped }));
|
|
595
|
+
}
|
|
596
|
+
catch (err) {
|
|
597
|
+
console.error('[import-finalize] merge error:', err);
|
|
598
|
+
if (!res.headersSent) {
|
|
599
|
+
res.writeHead(500);
|
|
419
600
|
res.end(JSON.stringify({ error: 'Failed to merge sessions', detail: String(err) }));
|
|
420
601
|
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
602
|
+
}
|
|
603
|
+
finally {
|
|
604
|
+
rmSync(uploadDir, { recursive: true, force: true });
|
|
605
|
+
rmSync(tmpExtractDir, { recursive: true, force: true });
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
catch (err) {
|
|
610
|
+
rmSync(uploadDir, { recursive: true, force: true });
|
|
611
|
+
rmSync(tmpExtractDir, { recursive: true, force: true });
|
|
612
|
+
throw err;
|
|
613
|
+
}
|
|
429
614
|
return;
|
|
430
615
|
}
|
|
431
616
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
432
617
|
res.end(JSON.stringify({ error: 'Not found' }));
|
|
433
618
|
});
|
|
434
619
|
const host = process.env.HOST || '0.0.0.0';
|
|
620
|
+
server.requestTimeout = 0; // no timeout — large uploads can take minutes
|
|
435
621
|
server.listen(port, host, () => {
|
|
436
622
|
console.log(`🌐 API server listening on http://${host}:${port}`);
|
|
437
623
|
console.log(` Sessions: http://${host}:${port}/sessions`);
|
|
@@ -106,7 +106,7 @@ export class PipelineDirectLLM extends llm.LLM {
|
|
|
106
106
|
`- If it's a quick side question, answer it then continue where you left off`,
|
|
107
107
|
`- If they want to change direction, acknowledge and follow their lead`,
|
|
108
108
|
`- Clarify when asked to or the question requires going over what you just said`,
|
|
109
|
-
`-
|
|
109
|
+
`- If relevant details were cut off — whether they answer the current question or an earlier one — weave them back in naturally so the user stays in context without having to ask again.`,
|
|
110
110
|
].join('\n');
|
|
111
111
|
// Modify the last user message in chatCtx
|
|
112
112
|
for (let i = chatCtx.items.length - 1; i >= 0; i--) {
|