@kernel.chat/kbot 3.50.0 → 3.52.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.
Files changed (84) hide show
  1. package/README.md +43 -9
  2. package/dist/agent-protocol.test.d.ts +2 -0
  3. package/dist/agent-protocol.test.d.ts.map +1 -0
  4. package/dist/agent-protocol.test.js +730 -0
  5. package/dist/agent-protocol.test.js.map +1 -0
  6. package/dist/agent.d.ts.map +1 -1
  7. package/dist/agent.js +34 -10
  8. package/dist/agent.js.map +1 -1
  9. package/dist/auth.js +3 -3
  10. package/dist/auth.js.map +1 -1
  11. package/dist/bench.d.ts +64 -0
  12. package/dist/bench.d.ts.map +1 -0
  13. package/dist/bench.js +973 -0
  14. package/dist/bench.js.map +1 -0
  15. package/dist/cli.js +144 -29
  16. package/dist/cli.js.map +1 -1
  17. package/dist/cloud-agent.d.ts +77 -0
  18. package/dist/cloud-agent.d.ts.map +1 -0
  19. package/dist/cloud-agent.js +743 -0
  20. package/dist/cloud-agent.js.map +1 -0
  21. package/dist/context.test.d.ts +2 -0
  22. package/dist/context.test.d.ts.map +1 -0
  23. package/dist/context.test.js +561 -0
  24. package/dist/context.test.js.map +1 -0
  25. package/dist/evolution.d.ts.map +1 -1
  26. package/dist/evolution.js +4 -1
  27. package/dist/evolution.js.map +1 -1
  28. package/dist/github-release.d.ts +61 -0
  29. package/dist/github-release.d.ts.map +1 -0
  30. package/dist/github-release.js +451 -0
  31. package/dist/github-release.js.map +1 -0
  32. package/dist/graph-memory.test.d.ts +2 -0
  33. package/dist/graph-memory.test.d.ts.map +1 -0
  34. package/dist/graph-memory.test.js +946 -0
  35. package/dist/graph-memory.test.js.map +1 -0
  36. package/dist/init-science.d.ts +43 -0
  37. package/dist/init-science.d.ts.map +1 -0
  38. package/dist/init-science.js +477 -0
  39. package/dist/init-science.js.map +1 -0
  40. package/dist/lab.d.ts +45 -0
  41. package/dist/lab.d.ts.map +1 -0
  42. package/dist/lab.js +1020 -0
  43. package/dist/lab.js.map +1 -0
  44. package/dist/lsp-deep.d.ts +101 -0
  45. package/dist/lsp-deep.d.ts.map +1 -0
  46. package/dist/lsp-deep.js +689 -0
  47. package/dist/lsp-deep.js.map +1 -0
  48. package/dist/memory.test.d.ts +2 -0
  49. package/dist/memory.test.d.ts.map +1 -0
  50. package/dist/memory.test.js +369 -0
  51. package/dist/memory.test.js.map +1 -0
  52. package/dist/multi-session.d.ts +164 -0
  53. package/dist/multi-session.d.ts.map +1 -0
  54. package/dist/multi-session.js +885 -0
  55. package/dist/multi-session.js.map +1 -0
  56. package/dist/self-eval.d.ts.map +1 -1
  57. package/dist/self-eval.js +5 -2
  58. package/dist/self-eval.js.map +1 -1
  59. package/dist/streaming.d.ts.map +1 -1
  60. package/dist/streaming.js +0 -1
  61. package/dist/streaming.js.map +1 -1
  62. package/dist/teach.d.ts +136 -0
  63. package/dist/teach.d.ts.map +1 -0
  64. package/dist/teach.js +915 -0
  65. package/dist/teach.js.map +1 -0
  66. package/dist/telemetry.d.ts +1 -1
  67. package/dist/telemetry.d.ts.map +1 -1
  68. package/dist/telemetry.js.map +1 -1
  69. package/dist/tools/ableton.d.ts.map +1 -1
  70. package/dist/tools/ableton.js +255 -1
  71. package/dist/tools/ableton.js.map +1 -1
  72. package/dist/tools/browser-agent.js +2 -2
  73. package/dist/tools/browser-agent.js.map +1 -1
  74. package/dist/tools/forge.d.ts.map +1 -1
  75. package/dist/tools/forge.js +15 -26
  76. package/dist/tools/forge.js.map +1 -1
  77. package/dist/tools/git.d.ts.map +1 -1
  78. package/dist/tools/git.js +10 -7
  79. package/dist/tools/git.js.map +1 -1
  80. package/dist/voice-realtime.d.ts +54 -0
  81. package/dist/voice-realtime.d.ts.map +1 -0
  82. package/dist/voice-realtime.js +805 -0
  83. package/dist/voice-realtime.js.map +1 -0
  84. package/package.json +10 -3
package/dist/lab.js ADDED
@@ -0,0 +1,1020 @@
1
+ // kbot Lab — Interactive Science REPL
2
+ //
3
+ // A specialized REPL mode for scientific research. Manages lab sessions
4
+ // with domain-specific system prompts, automatic citation extraction,
5
+ // variable tracking, and notebook export.
6
+ //
7
+ // Usage:
8
+ // $ kbot lab # Start a general science lab
9
+ // $ kbot lab --domain physics # Start in physics domain
10
+ // $ kbot lab --resume <id> # Resume a previous session
11
+ // $ kbot lab --name "QFT Notes" # Start a named session
12
+ //
13
+ // REPL commands:
14
+ // /domain <name> — Switch scientific domain
15
+ // /notebook — Show the current notebook
16
+ // /export [format] — Export as markdown, latex, or json
17
+ // /cite <doi> — Add a citation
18
+ // /hypothesis <text> — Record a hypothesis
19
+ // /note <text> — Add a research note
20
+ // /variables — Show stored variables
21
+ // /set <name> <value> — Store a variable
22
+ // /history — List past lab sessions
23
+ // /resume <id> — Resume a previous session
24
+ // /clear — Clear current session
25
+ // /help — Show lab commands
26
+ // /quit — Exit the lab
27
+ import { createInterface } from 'node:readline';
28
+ import { homedir } from 'node:os';
29
+ import { join } from 'node:path';
30
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync, } from 'node:fs';
31
+ import chalk from 'chalk';
32
+ import { runAgent } from './agent.js';
33
+ import { gatherContext } from './context.js';
34
+ import { printError, printInfo } from './ui.js';
35
+ // ── Constants ──
36
+ const LAB_DIR = join(homedir(), '.kbot', 'lab', 'sessions');
37
+ const MAX_LAB_SESSIONS = 100;
38
+ // ── Domain Configuration ──
39
+ const DOMAIN_LABELS = {
40
+ physics: 'Physics',
41
+ chemistry: 'Chemistry',
42
+ biology: 'Biology',
43
+ math: 'Mathematics',
44
+ neuro: 'Neuroscience',
45
+ earth: 'Earth Science',
46
+ social: 'Social Science',
47
+ humanities: 'Humanities',
48
+ health: 'Health & Medicine',
49
+ general: 'General Science',
50
+ };
51
+ const DOMAIN_ICONS = {
52
+ physics: '\u269B\uFE0F', // atom
53
+ chemistry: '\u2697\uFE0F', // alembic
54
+ biology: '\uD83E\uDDEC', // dna
55
+ math: '\uD83D\uDCD0', // triangular ruler
56
+ neuro: '\uD83E\uDDE0', // brain
57
+ earth: '\uD83C\uDF0D', // globe
58
+ social: '\uD83D\uDC65', // people
59
+ humanities: '\uD83D\uDCDA', // books
60
+ health: '\u2695\uFE0F', // medical
61
+ general: '\uD83D\uDD2C', // microscope
62
+ };
63
+ const DOMAIN_COLORS = {
64
+ physics: '#60A5FA', // blue
65
+ chemistry: '#4ADE80', // green
66
+ biology: '#A78BFA', // violet
67
+ math: '#FBBF24', // amber
68
+ neuro: '#F472B6', // pink
69
+ earth: '#34D399', // emerald
70
+ social: '#FB923C', // orange
71
+ humanities: '#E879F9', // fuchsia
72
+ health: '#F87171', // red
73
+ general: '#67E8F9', // cyan
74
+ };
75
+ const DOMAIN_AGENTS = {
76
+ physics: 'researcher',
77
+ chemistry: 'researcher',
78
+ biology: 'researcher',
79
+ math: 'analyst',
80
+ neuro: 'researcher',
81
+ earth: 'researcher',
82
+ social: 'analyst',
83
+ humanities: 'writer',
84
+ health: 'researcher',
85
+ general: 'researcher',
86
+ };
87
+ /** Build a domain-specific system prompt that primes the agent for scientific work */
88
+ function buildDomainPrompt(domain, session) {
89
+ const base = `You are a scientific research assistant operating in kbot lab mode.
90
+ Domain: ${DOMAIN_LABELS[domain]}
91
+ Session: "${session.name}" (started ${new Date(session.startedAt).toLocaleDateString()})
92
+
93
+ IMPORTANT CONVENTIONS:
94
+ - Always cite sources with DOIs or arXiv IDs when referencing specific findings
95
+ - Use precise scientific terminology appropriate to ${DOMAIN_LABELS[domain]}
96
+ - When presenting numeric results, include units and significant figures
97
+ - Distinguish between established findings, recent results, and speculative claims
98
+ - Format mathematical expressions using standard notation
99
+ - When computations are performed, show the work step by step
100
+ - If you reference a paper, include: authors, year, title, journal/source, and DOI if known`;
101
+ const domainSpecific = {
102
+ physics: `
103
+ PHYSICS-SPECIFIC GUIDANCE:
104
+ - Use SI units by default; convert to natural units when appropriate for HEP/QFT
105
+ - For quantum mechanics: use Dirac notation where helpful
106
+ - For classical mechanics: specify coordinate systems and reference frames
107
+ - Dimensional analysis should accompany any derived formula
108
+ - Preferred tools: symbolic computation, unit conversion, physics constants lookup
109
+ - When discussing experiments, note the energy scale, detector type, and statistical significance`,
110
+ chemistry: `
111
+ CHEMISTRY-SPECIFIC GUIDANCE:
112
+ - Use IUPAC nomenclature for compounds
113
+ - Include molecular formulas, structural info when relevant
114
+ - For reactions: balance equations, specify conditions (temperature, pressure, catalyst)
115
+ - Note safety considerations (GHS hazard codes) for hazardous substances
116
+ - Preferred tools: compound search, reaction lookup, stoichiometry calc, spectroscopy data
117
+ - For organic chemistry: specify stereochemistry where relevant`,
118
+ biology: `
119
+ BIOLOGY-SPECIFIC GUIDANCE:
120
+ - Use standard gene/protein nomenclature (HGNC for human genes, italic for genes)
121
+ - For sequences: specify organism, accession numbers
122
+ - For ecology: note sample sizes, confidence intervals, effect sizes
123
+ - For cell biology: specify cell type, organism, culture conditions
124
+ - Preferred tools: gene lookup, BLAST search, pathway search, taxonomy lookup
125
+ - Always distinguish in vitro, in vivo, and in silico results`,
126
+ math: `
127
+ MATHEMATICS-SPECIFIC GUIDANCE:
128
+ - State theorems precisely with all hypotheses
129
+ - Distinguish between definitions, lemmas, theorems, corollaries, and conjectures
130
+ - Provide proofs or proof sketches when asked
131
+ - Use standard notation (LaTeX-compatible) for expressions
132
+ - Preferred tools: symbolic computation, OEIS lookup, number theory, graph theory
133
+ - For applied math: connect abstractions to concrete applications`,
134
+ neuro: `
135
+ NEUROSCIENCE-SPECIFIC GUIDANCE:
136
+ - Specify brain regions using standard atlases (MNI, Talairach coordinates)
137
+ - For neuroimaging: note modality (fMRI, EEG, MEG), preprocessing pipeline
138
+ - Use standard neurotransmitter and receptor nomenclature
139
+ - For behavioral experiments: specify paradigm, n, statistical tests
140
+ - Preferred tools: brain atlas, neurotransmitter lookup, connectome query, EEG analysis
141
+ - Distinguish between correlation and causation in neuroimaging findings`,
142
+ earth: `
143
+ EARTH SCIENCE-SPECIFIC GUIDANCE:
144
+ - Specify temporal scales (geological time) and spatial scales
145
+ - Use standard geological time periods and epoch names
146
+ - For climate data: note data source, temporal resolution, spatial coverage
147
+ - For seismology: specify magnitude type (Mw, ML), depth, focal mechanism
148
+ - Preferred tools: climate data, earthquake query, ocean data, geological query
149
+ - Always note measurement uncertainties and model limitations`,
150
+ social: `
151
+ SOCIAL SCIENCE-SPECIFIC GUIDANCE:
152
+ - Report effect sizes alongside p-values
153
+ - Specify methodology: qualitative, quantitative, mixed methods
154
+ - Note sample demographics and potential selection biases
155
+ - For surveys: report response rate, sampling method
156
+ - Preferred tools: statistical analysis, demographic model, survey design, sentiment analysis
157
+ - Distinguish between causal claims and correlational findings
158
+ - Note WEIRD (Western, Educated, Industrialized, Rich, Democratic) sampling biases`,
159
+ humanities: `
160
+ HUMANITIES-SPECIFIC GUIDANCE:
161
+ - Cite primary sources with full bibliographic detail
162
+ - Distinguish between historical fact, interpretation, and historiographic debate
163
+ - For textual analysis: note edition, translation, original language
164
+ - For philosophical arguments: reconstruct in premise-conclusion form when helpful
165
+ - Preferred tools: archival search, historical timeline, philosophical concepts, corpus analysis
166
+ - Engage with multiple interpretive frameworks rather than privileging one`,
167
+ health: `
168
+ HEALTH & MEDICINE-SPECIFIC GUIDANCE:
169
+ - Use standard medical terminology (ICD codes, MeSH terms) where appropriate
170
+ - For clinical studies: specify study design (RCT, cohort, case-control), CONSORT/STROBE compliance
171
+ - Note levels of evidence (systematic review > RCT > observational)
172
+ - For drug information: include mechanism of action, pharmacokinetics, contraindications
173
+ - Preferred tools: PubMed search, clinical trials, drug lookup, epidemiology calc
174
+ - Always include appropriate medical disclaimers for clinical information
175
+ - Specify NNT (number needed to treat) and NNH where relevant`,
176
+ general: `
177
+ GENERAL SCIENCE GUIDANCE:
178
+ - Adapt terminology and rigor to the specific scientific question
179
+ - Cross-reference multiple domains when the question spans fields
180
+ - Use tools from any scientific domain as appropriate
181
+ - Prioritize systematic reviews and meta-analyses for evidence claims`,
182
+ };
183
+ // Build variable context if any are set
184
+ let variableContext = '';
185
+ const varEntries = Object.entries(session.variables);
186
+ if (varEntries.length > 0) {
187
+ variableContext = '\n\nACTIVE VARIABLES:\n' +
188
+ varEntries.map(([k, v]) => ` ${k} = ${JSON.stringify(v)}`).join('\n') +
189
+ '\nYou may reference these variables in computations.';
190
+ }
191
+ // Build citation context
192
+ let citationContext = '';
193
+ if (session.citations.length > 0) {
194
+ citationContext = '\n\nSESSION CITATIONS:\n' +
195
+ session.citations.map((c, i) => ` [${i + 1}] ${c.authors.join(', ')} (${c.year}). ${c.title}. ${c.source}${c.doi ? ` DOI: ${c.doi}` : ''}`).join('\n');
196
+ }
197
+ return base + domainSpecific[domain] + variableContext + citationContext;
198
+ }
199
+ // ── Storage ──
200
+ function ensureLabDir() {
201
+ if (!existsSync(LAB_DIR))
202
+ mkdirSync(LAB_DIR, { recursive: true });
203
+ }
204
+ function sessionPath(id) {
205
+ return join(LAB_DIR, `${id}.json`);
206
+ }
207
+ function generateSessionId() {
208
+ const now = new Date();
209
+ const date = now.toISOString().split('T')[0].replace(/-/g, '');
210
+ const rand = Math.random().toString(36).slice(2, 6);
211
+ return `lab-${date}-${rand}`;
212
+ }
213
+ function generateEntryId() {
214
+ return `e-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 5)}`;
215
+ }
216
+ function saveLabSession(session) {
217
+ ensureLabDir();
218
+ session.lastActiveAt = new Date().toISOString();
219
+ writeFileSync(sessionPath(session.id), JSON.stringify(session, null, 2));
220
+ }
221
+ function loadLabSession(id) {
222
+ ensureLabDir();
223
+ const path = sessionPath(id);
224
+ if (!existsSync(path))
225
+ return null;
226
+ try {
227
+ return JSON.parse(readFileSync(path, 'utf-8'));
228
+ }
229
+ catch {
230
+ return null;
231
+ }
232
+ }
233
+ /** List all lab sessions, newest first */
234
+ export function listLabSessions() {
235
+ ensureLabDir();
236
+ const files = readdirSync(LAB_DIR).filter(f => f.endsWith('.json'));
237
+ const sessions = [];
238
+ for (const file of files) {
239
+ try {
240
+ const data = readFileSync(join(LAB_DIR, file), 'utf-8');
241
+ sessions.push(JSON.parse(data));
242
+ }
243
+ catch {
244
+ continue;
245
+ }
246
+ }
247
+ return sessions.sort((a, b) => new Date(b.lastActiveAt).getTime() - new Date(a.lastActiveAt).getTime());
248
+ }
249
+ /** Get a specific lab session by ID */
250
+ export function getLabSession(id) {
251
+ return loadLabSession(id);
252
+ }
253
+ /** Prune oldest sessions beyond the limit */
254
+ function pruneLabSessions() {
255
+ const sessions = listLabSessions();
256
+ if (sessions.length <= MAX_LAB_SESSIONS)
257
+ return;
258
+ const toRemove = sessions.slice(MAX_LAB_SESSIONS);
259
+ for (const s of toRemove) {
260
+ const path = sessionPath(s.id);
261
+ if (existsSync(path))
262
+ unlinkSync(path);
263
+ }
264
+ }
265
+ // ── Notebook Management ──
266
+ function addEntry(session, type, content, metadata) {
267
+ const entry = {
268
+ id: generateEntryId(),
269
+ type,
270
+ content,
271
+ timestamp: new Date().toISOString(),
272
+ metadata,
273
+ };
274
+ session.notebook.push(entry);
275
+ return entry;
276
+ }
277
+ function formatNotebook(session) {
278
+ const domainColor = chalk.hex(DOMAIN_COLORS[session.domain]);
279
+ const dim = chalk.dim;
280
+ const lines = [
281
+ '',
282
+ domainColor(` ${DOMAIN_ICONS[session.domain]} Lab Notebook: ${session.name}`),
283
+ dim(` Domain: ${DOMAIN_LABELS[session.domain]} | Started: ${new Date(session.startedAt).toLocaleString()}`),
284
+ dim(` Entries: ${session.notebook.length} | Variables: ${Object.keys(session.variables).length} | Citations: ${session.citations.length}`),
285
+ dim(' ' + '\u2500'.repeat(60)),
286
+ '',
287
+ ];
288
+ if (session.notebook.length === 0) {
289
+ lines.push(dim(' (empty notebook — start asking questions)'));
290
+ return lines.join('\n');
291
+ }
292
+ const TYPE_BADGES = {
293
+ query: chalk.hex('#60A5FA')(' Q '),
294
+ result: chalk.hex('#4ADE80')(' R '),
295
+ computation: chalk.hex('#FBBF24')(' C '),
296
+ note: chalk.hex('#67E8F9')(' N '),
297
+ citation: chalk.hex('#FB923C')(' @ '),
298
+ hypothesis: chalk.hex('#E879F9')(' H '),
299
+ figure: chalk.hex('#F472B6')(' F '),
300
+ };
301
+ for (const entry of session.notebook) {
302
+ const badge = TYPE_BADGES[entry.type] || dim(` ${entry.type} `);
303
+ const time = dim(new Date(entry.timestamp).toLocaleTimeString());
304
+ const preview = entry.content.length > 120
305
+ ? entry.content.slice(0, 120) + '...'
306
+ : entry.content;
307
+ // For multi-line content, only show first line in overview
308
+ const firstLine = preview.split('\n')[0];
309
+ lines.push(` ${badge} ${time} ${firstLine}`);
310
+ }
311
+ lines.push('');
312
+ return lines.join('\n');
313
+ }
314
+ // ── Citation Extraction ──
315
+ /** Regex patterns for common citation identifiers */
316
+ const DOI_PATTERN = /\b(10\.\d{4,}\/[^\s,;)\]]+)/g;
317
+ const ARXIV_PATTERN = /\barXiv:(\d{4}\.\d{4,5}(?:v\d+)?)/g;
318
+ const PMID_PATTERN = /\bPMID:?\s*(\d{7,8})/g;
319
+ /** Extract citation references from AI response text */
320
+ function extractCitations(text) {
321
+ const found = [];
322
+ const seen = new Set();
323
+ let match;
324
+ // DOIs
325
+ DOI_PATTERN.lastIndex = 0;
326
+ while ((match = DOI_PATTERN.exec(text)) !== null) {
327
+ const id = match[1];
328
+ if (!seen.has(id)) {
329
+ seen.add(id);
330
+ found.push({ type: 'doi', id });
331
+ }
332
+ }
333
+ // arXiv IDs
334
+ ARXIV_PATTERN.lastIndex = 0;
335
+ while ((match = ARXIV_PATTERN.exec(text)) !== null) {
336
+ const id = match[1];
337
+ if (!seen.has(id)) {
338
+ seen.add(id);
339
+ found.push({ type: 'arxiv', id });
340
+ }
341
+ }
342
+ // PMIDs
343
+ PMID_PATTERN.lastIndex = 0;
344
+ while ((match = PMID_PATTERN.exec(text)) !== null) {
345
+ const id = match[1];
346
+ if (!seen.has(id)) {
347
+ seen.add(id);
348
+ found.push({ type: 'pmid', id });
349
+ }
350
+ }
351
+ return found;
352
+ }
353
+ /** Convert extracted citation references to Citation objects */
354
+ function refsToStubCitations(refs) {
355
+ return refs.map(ref => {
356
+ const citeId = `cite-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 4)}`;
357
+ switch (ref.type) {
358
+ case 'doi':
359
+ return {
360
+ id: citeId,
361
+ doi: ref.id,
362
+ title: `[DOI: ${ref.id}]`,
363
+ authors: [],
364
+ year: 0,
365
+ source: `https://doi.org/${ref.id}`,
366
+ };
367
+ case 'arxiv':
368
+ return {
369
+ id: citeId,
370
+ title: `[arXiv: ${ref.id}]`,
371
+ authors: [],
372
+ year: parseInt(ref.id.slice(0, 2), 10) + 2000,
373
+ source: `https://arxiv.org/abs/${ref.id}`,
374
+ };
375
+ case 'pmid':
376
+ return {
377
+ id: citeId,
378
+ title: `[PMID: ${ref.id}]`,
379
+ authors: [],
380
+ year: 0,
381
+ source: `https://pubmed.ncbi.nlm.nih.gov/${ref.id}/`,
382
+ };
383
+ }
384
+ });
385
+ }
386
+ // ── Variable Extraction ──
387
+ /** Patterns for numeric results that might be worth storing */
388
+ const NUMERIC_RESULT_PATTERNS = [
389
+ /(?:result|answer|value|equals?|=)\s*[:=]?\s*([-+]?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)\s*([a-zA-Z/^*]+)?/gi,
390
+ /(?:approximately|roughly|about|~)\s*([-+]?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)\s*([a-zA-Z/^*]+)?/gi,
391
+ ];
392
+ /** Extract named numeric results from AI response */
393
+ function extractNumericResults(text) {
394
+ const results = [];
395
+ // Look for explicit assignments like "F = 9.8 N" or "result: 42.5 km"
396
+ const assignmentPattern = /\b([A-Za-z_]\w*)\s*[=:]\s*([-+]?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)\s*([a-zA-Z/^*\u00B0\u00B2\u00B3]+)?/g;
397
+ let match;
398
+ assignmentPattern.lastIndex = 0;
399
+ while ((match = assignmentPattern.exec(text)) !== null) {
400
+ const name = match[1];
401
+ const value = parseFloat(match[2]);
402
+ const unit = match[3];
403
+ // Skip common false positives
404
+ const skipNames = new Set(['the', 'and', 'for', 'with', 'from', 'http', 'https', 'www', 'doi', 'ref', 'fig', 'table', 'step', 'line', 'page']);
405
+ if (skipNames.has(name.toLowerCase()))
406
+ continue;
407
+ if (isNaN(value))
408
+ continue;
409
+ results.push({ name, value, unit });
410
+ }
411
+ return results;
412
+ }
413
+ // ── Export ──
414
+ /** Export the notebook in the requested format */
415
+ export function exportNotebook(session, format = 'markdown') {
416
+ switch (format) {
417
+ case 'json':
418
+ return JSON.stringify({
419
+ session: {
420
+ id: session.id,
421
+ name: session.name,
422
+ domain: session.domain,
423
+ startedAt: session.startedAt,
424
+ lastActiveAt: session.lastActiveAt,
425
+ },
426
+ notebook: session.notebook,
427
+ variables: session.variables,
428
+ citations: session.citations,
429
+ }, null, 2);
430
+ case 'latex':
431
+ return exportAsLatex(session);
432
+ case 'markdown':
433
+ default:
434
+ return exportAsMarkdown(session);
435
+ }
436
+ }
437
+ function exportAsMarkdown(session) {
438
+ const lines = [
439
+ `# Lab Notebook: ${session.name}`,
440
+ '',
441
+ `**Domain:** ${DOMAIN_LABELS[session.domain]}`,
442
+ `**Started:** ${new Date(session.startedAt).toLocaleString()}`,
443
+ `**Last Active:** ${new Date(session.lastActiveAt).toLocaleString()}`,
444
+ '',
445
+ '---',
446
+ '',
447
+ ];
448
+ // Variables section
449
+ const varEntries = Object.entries(session.variables);
450
+ if (varEntries.length > 0) {
451
+ lines.push('## Variables', '');
452
+ lines.push('| Name | Value |');
453
+ lines.push('|------|-------|');
454
+ for (const [name, value] of varEntries) {
455
+ lines.push(`| \`${name}\` | ${JSON.stringify(value)} |`);
456
+ }
457
+ lines.push('');
458
+ }
459
+ // Notebook entries
460
+ lines.push('## Entries', '');
461
+ for (const entry of session.notebook) {
462
+ const time = new Date(entry.timestamp).toLocaleString();
463
+ const typeLabel = entry.type.charAt(0).toUpperCase() + entry.type.slice(1);
464
+ switch (entry.type) {
465
+ case 'hypothesis':
466
+ lines.push(`### Hypothesis (${time})`, '', `> ${entry.content}`, '');
467
+ break;
468
+ case 'citation':
469
+ lines.push(`### Citation (${time})`, '', `- ${entry.content}`, '');
470
+ break;
471
+ case 'note':
472
+ lines.push(`### Note (${time})`, '', entry.content, '');
473
+ break;
474
+ case 'query':
475
+ lines.push(`### Query (${time})`, '', `**Q:** ${entry.content}`, '');
476
+ break;
477
+ case 'result':
478
+ lines.push(`### Result (${time})`, '', entry.content, '');
479
+ break;
480
+ case 'computation':
481
+ lines.push(`### Computation (${time})`, '', '```', entry.content, '```', '');
482
+ break;
483
+ case 'figure':
484
+ lines.push(`### Figure (${time})`, '', entry.content, '');
485
+ break;
486
+ default:
487
+ lines.push(`### ${typeLabel} (${time})`, '', entry.content, '');
488
+ }
489
+ }
490
+ // Citations section
491
+ if (session.citations.length > 0) {
492
+ lines.push('## References', '');
493
+ for (let i = 0; i < session.citations.length; i++) {
494
+ const c = session.citations[i];
495
+ const authors = c.authors.length > 0 ? c.authors.join(', ') : 'Unknown';
496
+ const year = c.year > 0 ? ` (${c.year})` : '';
497
+ const doi = c.doi ? ` DOI: ${c.doi}` : '';
498
+ lines.push(`${i + 1}. ${authors}${year}. *${c.title}*. ${c.source}${doi}`);
499
+ }
500
+ lines.push('');
501
+ }
502
+ return lines.join('\n');
503
+ }
504
+ function exportAsLatex(session) {
505
+ const lines = [
506
+ '\\documentclass{article}',
507
+ '\\usepackage[utf8]{inputenc}',
508
+ '\\usepackage{amsmath,amssymb}',
509
+ '\\usepackage{hyperref}',
510
+ '\\usepackage{booktabs}',
511
+ '',
512
+ `\\title{Lab Notebook: ${escapeLatex(session.name)}}`,
513
+ `\\date{${new Date(session.startedAt).toLocaleDateString()}}`,
514
+ '',
515
+ '\\begin{document}',
516
+ '\\maketitle',
517
+ '',
518
+ ];
519
+ // Variables
520
+ const varEntries = Object.entries(session.variables);
521
+ if (varEntries.length > 0) {
522
+ lines.push('\\section{Variables}', '');
523
+ lines.push('\\begin{tabular}{ll}', '\\toprule', 'Name & Value \\\\', '\\midrule');
524
+ for (const [name, value] of varEntries) {
525
+ lines.push(`\\texttt{${escapeLatex(name)}} & ${escapeLatex(String(value))} \\\\`);
526
+ }
527
+ lines.push('\\bottomrule', '\\end{tabular}', '');
528
+ }
529
+ // Entries
530
+ lines.push('\\section{Notebook}', '');
531
+ for (const entry of session.notebook) {
532
+ const time = new Date(entry.timestamp).toLocaleString();
533
+ switch (entry.type) {
534
+ case 'hypothesis':
535
+ lines.push(`\\subsection*{Hypothesis (${escapeLatex(time)})}`);
536
+ lines.push(`\\begin{quote}${escapeLatex(entry.content)}\\end{quote}`);
537
+ break;
538
+ case 'query':
539
+ lines.push(`\\subsection*{Query (${escapeLatex(time)})}`);
540
+ lines.push(`\\textbf{Q:} ${escapeLatex(entry.content)}`);
541
+ break;
542
+ case 'result':
543
+ lines.push(`\\subsection*{Result (${escapeLatex(time)})}`);
544
+ lines.push(escapeLatex(entry.content));
545
+ break;
546
+ case 'computation':
547
+ lines.push(`\\subsection*{Computation (${escapeLatex(time)})}`);
548
+ lines.push('\\begin{verbatim}');
549
+ lines.push(entry.content);
550
+ lines.push('\\end{verbatim}');
551
+ break;
552
+ default:
553
+ lines.push(`\\subsection*{${entry.type.charAt(0).toUpperCase() + entry.type.slice(1)} (${escapeLatex(time)})}`);
554
+ lines.push(escapeLatex(entry.content));
555
+ }
556
+ lines.push('');
557
+ }
558
+ // References
559
+ if (session.citations.length > 0) {
560
+ lines.push('\\section{References}', '', '\\begin{enumerate}');
561
+ for (const c of session.citations) {
562
+ const authors = c.authors.length > 0 ? c.authors.join(', ') : 'Unknown';
563
+ const year = c.year > 0 ? ` (${c.year})` : '';
564
+ const doi = c.doi ? ` \\texttt{${escapeLatex(c.doi)}}` : '';
565
+ lines.push(`\\item ${escapeLatex(authors)}${year}. \\textit{${escapeLatex(c.title)}}. ${escapeLatex(c.source)}${doi}`);
566
+ }
567
+ lines.push('\\end{enumerate}');
568
+ }
569
+ lines.push('', '\\end{document}');
570
+ return lines.join('\n');
571
+ }
572
+ function escapeLatex(text) {
573
+ return text
574
+ .replace(/\\/g, '\\textbackslash{}')
575
+ .replace(/[&%$#_{}]/g, m => '\\' + m)
576
+ .replace(/~/g, '\\textasciitilde{}')
577
+ .replace(/\^/g, '\\textasciicircum{}');
578
+ }
579
+ function parseDOICitation(doiOrUrl) {
580
+ // Accept bare DOI or full URL
581
+ const doi = doiOrUrl.replace(/^https?:\/\/doi\.org\//, '').trim();
582
+ return {
583
+ id: `cite-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 4)}`,
584
+ doi,
585
+ title: `[DOI: ${doi}]`,
586
+ authors: [],
587
+ year: 0,
588
+ source: `https://doi.org/${doi}`,
589
+ };
590
+ }
591
+ function handleCommand(input, session) {
592
+ const trimmed = input.trim();
593
+ const parts = trimmed.split(/\s+/);
594
+ const cmd = parts[0].toLowerCase();
595
+ const rest = trimmed.slice(parts[0].length).trim();
596
+ switch (cmd) {
597
+ case '/help':
598
+ return { continue: true, message: formatHelpText() };
599
+ case '/domain': {
600
+ if (!rest) {
601
+ const domains = Object.keys(DOMAIN_LABELS).map(d => ` ${d === session.domain ? chalk.green('*') : ' '} ${d} — ${DOMAIN_LABELS[d]}`).join('\n');
602
+ return { continue: true, message: `\nActive domain: ${chalk.bold(DOMAIN_LABELS[session.domain])}\n\n${domains}\n` };
603
+ }
604
+ const newDomain = rest.toLowerCase();
605
+ if (!(newDomain in DOMAIN_LABELS)) {
606
+ return { continue: true, message: chalk.red(`Unknown domain: ${rest}. Use /domain to see available domains.`) };
607
+ }
608
+ session.domain = newDomain;
609
+ addEntry(session, 'note', `Domain switched to ${DOMAIN_LABELS[newDomain]}`);
610
+ saveLabSession(session);
611
+ const color = chalk.hex(DOMAIN_COLORS[newDomain]);
612
+ return { continue: true, message: color(`\n ${DOMAIN_ICONS[newDomain]} Switched to ${DOMAIN_LABELS[newDomain]}\n`) };
613
+ }
614
+ case '/notebook':
615
+ return { continue: true, message: formatNotebook(session) };
616
+ case '/export': {
617
+ const fmt = (rest.toLowerCase() || 'markdown');
618
+ if (!['markdown', 'latex', 'json'].includes(fmt)) {
619
+ return { continue: true, message: chalk.red('Format must be one of: markdown, latex, json') };
620
+ }
621
+ const exported = exportNotebook(session, fmt);
622
+ const ext = fmt === 'markdown' ? 'md' : fmt === 'latex' ? 'tex' : 'json';
623
+ const outPath = join(process.cwd(), `${session.id}.${ext}`);
624
+ try {
625
+ writeFileSync(outPath, exported);
626
+ return { continue: true, message: chalk.green(` Exported to ${outPath}`) };
627
+ }
628
+ catch (err) {
629
+ return { continue: true, message: chalk.red(`Export failed: ${err}`) };
630
+ }
631
+ }
632
+ case '/cite': {
633
+ if (!rest) {
634
+ return { continue: true, message: chalk.red('Usage: /cite <doi or url>') };
635
+ }
636
+ const citation = parseDOICitation(rest);
637
+ session.citations.push(citation);
638
+ addEntry(session, 'citation', `Added citation: ${citation.doi || rest}`);
639
+ saveLabSession(session);
640
+ return { continue: true, message: chalk.hex('#FB923C')(` Added citation: ${citation.doi || rest}`) };
641
+ }
642
+ case '/hypothesis': {
643
+ if (!rest) {
644
+ return { continue: true, message: chalk.red('Usage: /hypothesis <your hypothesis>') };
645
+ }
646
+ addEntry(session, 'hypothesis', rest);
647
+ saveLabSession(session);
648
+ return { continue: true, message: chalk.hex('#E879F9')(` Hypothesis recorded: "${rest.slice(0, 80)}${rest.length > 80 ? '...' : ''}"`) };
649
+ }
650
+ case '/note': {
651
+ if (!rest) {
652
+ return { continue: true, message: chalk.red('Usage: /note <your note>') };
653
+ }
654
+ addEntry(session, 'note', rest);
655
+ saveLabSession(session);
656
+ return { continue: true, message: chalk.hex('#67E8F9')(` Note added.`) };
657
+ }
658
+ case '/variables': {
659
+ const vars = Object.entries(session.variables);
660
+ if (vars.length === 0) {
661
+ return { continue: true, message: chalk.dim(' No variables stored. Use /set <name> <value> to store one.') };
662
+ }
663
+ const varLines = vars.map(([k, v]) => ` ${chalk.cyan(k)} = ${chalk.white(JSON.stringify(v))}`);
664
+ return { continue: true, message: `\n ${chalk.bold('Variables')} (${vars.length})\n${varLines.join('\n')}\n` };
665
+ }
666
+ case '/set': {
667
+ const setParts = rest.split(/\s+/);
668
+ if (setParts.length < 2) {
669
+ return { continue: true, message: chalk.red('Usage: /set <name> <value>') };
670
+ }
671
+ const varName = setParts[0];
672
+ const varValueStr = setParts.slice(1).join(' ');
673
+ // Try to parse as number, then boolean, then keep as string
674
+ let varValue;
675
+ const num = Number(varValueStr);
676
+ if (!isNaN(num) && varValueStr.trim() !== '') {
677
+ varValue = num;
678
+ }
679
+ else if (varValueStr === 'true') {
680
+ varValue = true;
681
+ }
682
+ else if (varValueStr === 'false') {
683
+ varValue = false;
684
+ }
685
+ else {
686
+ // Try JSON parse for objects/arrays
687
+ try {
688
+ varValue = JSON.parse(varValueStr);
689
+ }
690
+ catch {
691
+ varValue = varValueStr;
692
+ }
693
+ }
694
+ session.variables[varName] = varValue;
695
+ saveLabSession(session);
696
+ return { continue: true, message: chalk.cyan(` ${varName} = ${JSON.stringify(varValue)}`) };
697
+ }
698
+ case '/history': {
699
+ const sessions = listLabSessions();
700
+ if (sessions.length === 0) {
701
+ return { continue: true, message: chalk.dim(' No lab sessions found.') };
702
+ }
703
+ const histLines = sessions.slice(0, 20).map(s => {
704
+ const date = new Date(s.lastActiveAt).toLocaleDateString();
705
+ const icon = DOMAIN_ICONS[s.domain];
706
+ const entries = s.notebook.length;
707
+ const isCurrent = s.id === session.id ? chalk.green(' (current)') : '';
708
+ return ` ${chalk.dim(s.id)} ${icon} ${DOMAIN_LABELS[s.domain].padEnd(14)} ${date} ${entries} entries "${s.name}"${isCurrent}`;
709
+ });
710
+ return { continue: true, message: `\n ${chalk.bold('Lab Sessions')} (${sessions.length} total)\n\n${histLines.join('\n')}\n` };
711
+ }
712
+ case '/resume': {
713
+ if (!rest) {
714
+ return { continue: true, message: chalk.red('Usage: /resume <session-id>') };
715
+ }
716
+ // Resuming is handled by the main loop — signal intent via metadata
717
+ return { continue: true, message: `__RESUME__:${rest}` };
718
+ }
719
+ case '/clear': {
720
+ session.notebook = [];
721
+ session.variables = {};
722
+ session.citations = [];
723
+ saveLabSession(session);
724
+ return { continue: true, message: chalk.dim(' Session cleared.') };
725
+ }
726
+ case '/quit':
727
+ case '/exit':
728
+ case '/q':
729
+ return { continue: false, message: undefined };
730
+ default:
731
+ return { continue: true, message: chalk.red(`Unknown command: ${cmd}. Type /help for available commands.`) };
732
+ }
733
+ }
734
+ function formatHelpText() {
735
+ const h = chalk.hex('#A78BFA');
736
+ const d = chalk.dim;
737
+ return `
738
+ ${h('Lab Commands')}
739
+
740
+ ${h('/domain')} ${d('[name]')} Switch scientific domain (or list domains)
741
+ ${h('/notebook')} Show the current session notebook
742
+ ${h('/export')} ${d('[format]')} Export notebook (markdown, latex, json)
743
+ ${h('/cite')} ${d('<doi>')} Add a citation by DOI
744
+ ${h('/hypothesis')} ${d('<text>')} Record a hypothesis
745
+ ${h('/note')} ${d('<text>')} Add a research note
746
+ ${h('/variables')} Show stored variables
747
+ ${h('/set')} ${d('<name> <value>')} Store a variable for reuse
748
+ ${h('/history')} List past lab sessions
749
+ ${h('/resume')} ${d('<id>')} Resume a previous session
750
+ ${h('/clear')} Clear current session
751
+ ${h('/help')} Show this help
752
+ ${h('/quit')} Exit the lab
753
+ `;
754
+ }
755
+ // ── Lab Banner ──
756
+ function printLabBanner(session) {
757
+ const domainColor = chalk.hex(DOMAIN_COLORS[session.domain]);
758
+ const dim = chalk.dim;
759
+ const banner = `
760
+ ${domainColor(' \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510')}
761
+ ${domainColor(' \u2502')} ${DOMAIN_ICONS[session.domain]} ${chalk.bold('kbot lab')} ${dim('— Interactive Science REPL')} ${domainColor('\u2502')}
762
+ ${domainColor(' \u2502')} ${domainColor('\u2502')}
763
+ ${domainColor(' \u2502')} Domain: ${domainColor(DOMAIN_LABELS[session.domain].padEnd(16))} ${domainColor('\u2502')}
764
+ ${domainColor(' \u2502')} Session: ${dim(session.name.slice(0, 30).padEnd(30))} ${domainColor('\u2502')}
765
+ ${domainColor(' \u2502')} ${domainColor('\u2502')}
766
+ ${domainColor(' \u2502')} ${dim('Type /help for commands, or ask a question.')} ${domainColor('\u2502')}
767
+ ${domainColor(' \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518')}
768
+ `;
769
+ console.error(banner);
770
+ }
771
+ // ── Prompt Builder ──
772
+ function buildLabPrompt(session) {
773
+ const domainColor = chalk.hex(DOMAIN_COLORS[session.domain]);
774
+ const dim = chalk.dim;
775
+ const icon = DOMAIN_ICONS[session.domain];
776
+ const domainShort = session.domain === 'general' ? 'sci' : session.domain.slice(0, 4);
777
+ // Status indicators
778
+ const varCount = Object.keys(session.variables).length;
779
+ const citeCount = session.citations.length;
780
+ const statusParts = [];
781
+ if (varCount > 0)
782
+ statusParts.push(dim(`${varCount}v`));
783
+ if (citeCount > 0)
784
+ statusParts.push(dim(`${citeCount}c`));
785
+ const status = statusParts.length > 0 ? ` ${dim('[')}${statusParts.join(dim('|'))}${dim(']')}` : '';
786
+ return `${icon} ${domainColor(`lab:${domainShort}`)}${status}${domainColor('>')} `;
787
+ }
788
+ // ── Main Entry Point ──
789
+ /**
790
+ * Start the interactive science lab REPL.
791
+ *
792
+ * @param opts.domain - Initial scientific domain (default: 'general')
793
+ * @param opts.resume - Session ID to resume
794
+ * @param opts.name - Name for the new session
795
+ */
796
+ export async function startLab(opts) {
797
+ const domain = opts?.domain || 'general';
798
+ let session;
799
+ // Resume existing session or create new one
800
+ if (opts?.resume) {
801
+ const existing = loadLabSession(opts.resume);
802
+ if (!existing) {
803
+ // Try fuzzy match
804
+ const allSessions = listLabSessions();
805
+ const match = allSessions.find(s => s.id.includes(opts.resume) || s.name.toLowerCase().includes(opts.resume.toLowerCase()));
806
+ if (match) {
807
+ session = match;
808
+ printInfo(`Resumed lab session: ${session.name} (${session.id})`);
809
+ }
810
+ else {
811
+ printError(`Lab session not found: ${opts.resume}`);
812
+ printInfo('Use /history to see available sessions.');
813
+ return;
814
+ }
815
+ }
816
+ else {
817
+ session = existing;
818
+ printInfo(`Resumed lab session: ${session.name} (${session.id})`);
819
+ }
820
+ }
821
+ else {
822
+ const id = generateSessionId();
823
+ const name = opts?.name || `${DOMAIN_LABELS[domain]} Lab — ${new Date().toLocaleDateString()}`;
824
+ session = {
825
+ id,
826
+ name,
827
+ domain,
828
+ notebook: [],
829
+ variables: {},
830
+ citations: [],
831
+ startedAt: new Date().toISOString(),
832
+ lastActiveAt: new Date().toISOString(),
833
+ };
834
+ saveLabSession(session);
835
+ }
836
+ // Display banner
837
+ printLabBanner(session);
838
+ // Gather project context for the agent (non-blocking, best-effort)
839
+ let projectContext;
840
+ try {
841
+ projectContext = await gatherContext();
842
+ }
843
+ catch {
844
+ // Context gathering is optional — lab works without it
845
+ }
846
+ // Create readline interface
847
+ const rl = createInterface({
848
+ input: process.stdin,
849
+ output: process.stderr, // Prompt goes to stderr (clig.dev convention)
850
+ prompt: buildLabPrompt(session),
851
+ terminal: true,
852
+ });
853
+ rl.prompt();
854
+ // Main REPL loop using event-driven readline
855
+ const handleLine = async (line) => {
856
+ const input = line.trim();
857
+ // Empty input — just re-prompt
858
+ if (!input) {
859
+ rl.prompt();
860
+ return;
861
+ }
862
+ // Handle slash commands
863
+ if (input.startsWith('/')) {
864
+ const result = handleCommand(input, session);
865
+ // Handle /resume with session switching
866
+ if (result.message?.startsWith('__RESUME__:')) {
867
+ const resumeId = result.message.slice('__RESUME__:'.length);
868
+ const resumeSession = loadLabSession(resumeId);
869
+ if (!resumeSession) {
870
+ // Fuzzy match
871
+ const all = listLabSessions();
872
+ const match = all.find(s => s.id.includes(resumeId) || s.name.toLowerCase().includes(resumeId.toLowerCase()));
873
+ if (match) {
874
+ Object.assign(session, match);
875
+ printInfo(`Resumed: ${session.name}`);
876
+ printLabBanner(session);
877
+ }
878
+ else {
879
+ console.error(chalk.red(` Session not found: ${resumeId}`));
880
+ }
881
+ }
882
+ else {
883
+ Object.assign(session, resumeSession);
884
+ printInfo(`Resumed: ${session.name}`);
885
+ printLabBanner(session);
886
+ }
887
+ rl.setPrompt(buildLabPrompt(session));
888
+ rl.prompt();
889
+ return;
890
+ }
891
+ if (result.message) {
892
+ console.error(result.message);
893
+ }
894
+ if (!result.continue) {
895
+ rl.close();
896
+ return;
897
+ }
898
+ // Update prompt (domain may have changed)
899
+ rl.setPrompt(buildLabPrompt(session));
900
+ rl.prompt();
901
+ return;
902
+ }
903
+ // Regular query — send to the agent
904
+ // Log the query to the notebook
905
+ addEntry(session, 'query', input);
906
+ // Build system prompt with domain context
907
+ const systemPrompt = buildDomainPrompt(session.domain, session);
908
+ // Determine the best agent for this domain
909
+ const agent = DOMAIN_AGENTS[session.domain];
910
+ // Prepare agent options
911
+ const agentOpts = {
912
+ agent,
913
+ stream: true,
914
+ context: projectContext,
915
+ };
916
+ try {
917
+ // Show thinking indicator
918
+ const domainColor = chalk.hex(DOMAIN_COLORS[session.domain]);
919
+ console.error(domainColor(' ...'));
920
+ // Wrap the user message with the lab system prompt context
921
+ const augmentedMessage = `[Lab System Prompt — do not repeat this to the user]\n${systemPrompt}\n[End Lab System Prompt]\n\nUser query: ${input}`;
922
+ const response = await runAgent(augmentedMessage, agentOpts);
923
+ // Log the result to the notebook
924
+ addEntry(session, 'result', response.content, {
925
+ agent: response.agent,
926
+ model: response.model,
927
+ toolCalls: response.toolCalls,
928
+ tokens: response.usage,
929
+ });
930
+ // Auto-extract citations from the response
931
+ const citationRefs = extractCitations(response.content);
932
+ if (citationRefs.length > 0) {
933
+ const newCitations = refsToStubCitations(citationRefs);
934
+ // Deduplicate against existing citations
935
+ for (const c of newCitations) {
936
+ const alreadyExists = session.citations.some(existing => (existing.doi && c.doi && existing.doi === c.doi) ||
937
+ existing.source === c.source);
938
+ if (!alreadyExists) {
939
+ session.citations.push(c);
940
+ addEntry(session, 'citation', `Auto-detected: ${c.doi || c.source}`, { auto: true });
941
+ }
942
+ }
943
+ if (newCitations.length > 0) {
944
+ const newCount = newCitations.filter(c => !session.citations.some(existing => existing.id !== c.id &&
945
+ ((existing.doi && c.doi && existing.doi === c.doi) || existing.source === c.source))).length;
946
+ if (newCount > 0) {
947
+ console.error(chalk.dim(` [${newCount} citation(s) auto-detected]`));
948
+ }
949
+ }
950
+ }
951
+ // Auto-extract and store numeric results
952
+ const numericResults = extractNumericResults(response.content);
953
+ let storedVarCount = 0;
954
+ for (const nr of numericResults) {
955
+ // Only auto-store if the variable name looks intentional (2+ chars, not common words)
956
+ if (nr.name.length >= 2 && !session.variables[nr.name]) {
957
+ const value = nr.unit ? `${nr.value} ${nr.unit}` : nr.value;
958
+ session.variables[nr.name] = value;
959
+ storedVarCount++;
960
+ }
961
+ }
962
+ if (storedVarCount > 0) {
963
+ console.error(chalk.dim(` [${storedVarCount} variable(s) auto-stored]`));
964
+ }
965
+ // Print the response if it wasn't streamed already
966
+ if (!response.streamed) {
967
+ console.log();
968
+ console.log(response.content);
969
+ console.log();
970
+ }
971
+ // Save session state
972
+ saveLabSession(session);
973
+ }
974
+ catch (err) {
975
+ const errorMsg = err instanceof Error ? err.message : String(err);
976
+ printError(`Lab agent error: ${errorMsg}`);
977
+ addEntry(session, 'note', `Error: ${errorMsg}`, { error: true });
978
+ saveLabSession(session);
979
+ }
980
+ // Update prompt and continue
981
+ rl.setPrompt(buildLabPrompt(session));
982
+ rl.prompt();
983
+ };
984
+ // Wire up the readline events
985
+ rl.on('line', (line) => {
986
+ // Pause readline during async processing to avoid prompt duplication
987
+ rl.pause();
988
+ handleLine(line).then(() => {
989
+ rl.resume();
990
+ }).catch((err) => {
991
+ printError(`Unexpected error: ${err}`);
992
+ rl.resume();
993
+ rl.prompt();
994
+ });
995
+ });
996
+ rl.on('close', () => {
997
+ // Final save
998
+ saveLabSession(session);
999
+ pruneLabSessions();
1000
+ const domainColor = chalk.hex(DOMAIN_COLORS[session.domain]);
1001
+ const entries = session.notebook.length;
1002
+ const vars = Object.keys(session.variables).length;
1003
+ const cites = session.citations.length;
1004
+ console.error('');
1005
+ console.error(domainColor(` ${DOMAIN_ICONS[session.domain]} Lab session saved.`));
1006
+ console.error(chalk.dim(` ID: ${session.id}`));
1007
+ console.error(chalk.dim(` ${entries} entries, ${vars} variables, ${cites} citations`));
1008
+ console.error(chalk.dim(` Resume with: kbot lab --resume ${session.id}`));
1009
+ console.error('');
1010
+ });
1011
+ // Handle SIGINT gracefully (Ctrl+C)
1012
+ rl.on('SIGINT', () => {
1013
+ rl.close();
1014
+ });
1015
+ // Return a promise that resolves when the REPL closes
1016
+ return new Promise((resolve) => {
1017
+ rl.on('close', () => resolve());
1018
+ });
1019
+ }
1020
+ //# sourceMappingURL=lab.js.map