@kernel.chat/kbot 3.51.0 → 3.54.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/README.md +43 -9
- package/dist/agent-protocol.test.d.ts +2 -0
- package/dist/agent-protocol.test.d.ts.map +1 -0
- package/dist/agent-protocol.test.js +730 -0
- package/dist/agent-protocol.test.js.map +1 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +34 -10
- package/dist/agent.js.map +1 -1
- package/dist/agents/replit.js +1 -1
- package/dist/auth.js +3 -3
- package/dist/auth.js.map +1 -1
- package/dist/behaviour.d.ts +30 -0
- package/dist/behaviour.d.ts.map +1 -0
- package/dist/behaviour.js +191 -0
- package/dist/behaviour.js.map +1 -0
- package/dist/bench.d.ts +64 -0
- package/dist/bench.d.ts.map +1 -0
- package/dist/bench.js +973 -0
- package/dist/bench.js.map +1 -0
- package/dist/bootstrap.js +1 -1
- package/dist/bootstrap.js.map +1 -1
- package/dist/cli.js +144 -29
- package/dist/cli.js.map +1 -1
- package/dist/cloud-agent.d.ts +77 -0
- package/dist/cloud-agent.d.ts.map +1 -0
- package/dist/cloud-agent.js +743 -0
- package/dist/cloud-agent.js.map +1 -0
- package/dist/context.test.d.ts +2 -0
- package/dist/context.test.d.ts.map +1 -0
- package/dist/context.test.js +561 -0
- package/dist/context.test.js.map +1 -0
- package/dist/evolution.d.ts.map +1 -1
- package/dist/evolution.js +4 -1
- package/dist/evolution.js.map +1 -1
- package/dist/github-release.d.ts +61 -0
- package/dist/github-release.d.ts.map +1 -0
- package/dist/github-release.js +451 -0
- package/dist/github-release.js.map +1 -0
- package/dist/graph-memory.test.d.ts +2 -0
- package/dist/graph-memory.test.d.ts.map +1 -0
- package/dist/graph-memory.test.js +946 -0
- package/dist/graph-memory.test.js.map +1 -0
- package/dist/init-science.d.ts +43 -0
- package/dist/init-science.d.ts.map +1 -0
- package/dist/init-science.js +477 -0
- package/dist/init-science.js.map +1 -0
- package/dist/integrations/ableton-m4l.d.ts +124 -0
- package/dist/integrations/ableton-m4l.d.ts.map +1 -0
- package/dist/integrations/ableton-m4l.js +338 -0
- package/dist/integrations/ableton-m4l.js.map +1 -0
- package/dist/integrations/ableton-osc.d.ts.map +1 -1
- package/dist/integrations/ableton-osc.js +6 -2
- package/dist/integrations/ableton-osc.js.map +1 -1
- package/dist/lab.d.ts +45 -0
- package/dist/lab.d.ts.map +1 -0
- package/dist/lab.js +1020 -0
- package/dist/lab.js.map +1 -0
- package/dist/lsp-deep.d.ts +101 -0
- package/dist/lsp-deep.d.ts.map +1 -0
- package/dist/lsp-deep.js +689 -0
- package/dist/lsp-deep.js.map +1 -0
- package/dist/memory.test.d.ts +2 -0
- package/dist/memory.test.d.ts.map +1 -0
- package/dist/memory.test.js +369 -0
- package/dist/memory.test.js.map +1 -0
- package/dist/multi-session.d.ts +164 -0
- package/dist/multi-session.d.ts.map +1 -0
- package/dist/multi-session.js +885 -0
- package/dist/multi-session.js.map +1 -0
- package/dist/music-learning.d.ts +181 -0
- package/dist/music-learning.d.ts.map +1 -0
- package/dist/music-learning.js +340 -0
- package/dist/music-learning.js.map +1 -0
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +5 -2
- package/dist/self-eval.js.map +1 -1
- package/dist/skill-system.d.ts +68 -0
- package/dist/skill-system.d.ts.map +1 -0
- package/dist/skill-system.js +386 -0
- package/dist/skill-system.js.map +1 -0
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js +0 -1
- package/dist/streaming.js.map +1 -1
- package/dist/teach.d.ts +136 -0
- package/dist/teach.d.ts.map +1 -0
- package/dist/teach.js +915 -0
- package/dist/teach.js.map +1 -0
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js.map +1 -1
- package/dist/tools/ableton.d.ts.map +1 -1
- package/dist/tools/ableton.js +24 -8
- package/dist/tools/ableton.js.map +1 -1
- package/dist/tools/arrangement-engine.d.ts +2 -0
- package/dist/tools/arrangement-engine.d.ts.map +1 -0
- package/dist/tools/arrangement-engine.js +644 -0
- package/dist/tools/arrangement-engine.js.map +1 -0
- package/dist/tools/browser-agent.js +2 -2
- package/dist/tools/browser-agent.js.map +1 -1
- package/dist/tools/forge.d.ts.map +1 -1
- package/dist/tools/forge.js +15 -26
- package/dist/tools/forge.js.map +1 -1
- package/dist/tools/git.d.ts.map +1 -1
- package/dist/tools/git.js +10 -7
- package/dist/tools/git.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +5 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/producer-engine.d.ts +71 -0
- package/dist/tools/producer-engine.d.ts.map +1 -0
- package/dist/tools/producer-engine.js +1859 -0
- package/dist/tools/producer-engine.js.map +1 -0
- package/dist/tools/sound-designer.d.ts +2 -0
- package/dist/tools/sound-designer.d.ts.map +1 -0
- package/dist/tools/sound-designer.js +896 -0
- package/dist/tools/sound-designer.js.map +1 -0
- package/dist/voice-realtime.d.ts +54 -0
- package/dist/voice-realtime.d.ts.map +1 -0
- package/dist/voice-realtime.js +805 -0
- package/dist/voice-realtime.js.map +1 -0
- package/package.json +11 -4
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
|