@getmegabrain/cli 0.1.6 → 0.1.8
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/dist/science/agent/agent.js +107 -0
- package/dist/science/agent/model.js +46 -0
- package/dist/science/kernel.js +42 -0
- package/dist/science/pages.js +88 -37
- package/dist/science/server.js +67 -15
- package/dist/science/session-client.js +191 -0
- package/dist/science/store.js +18 -0
- package/package.json +1 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
function codeOf(input) {
|
|
2
|
+
return typeof input.code === 'string' ? input.code : '';
|
|
3
|
+
}
|
|
4
|
+
export function pythonTool(bridge) {
|
|
5
|
+
return {
|
|
6
|
+
definition: {
|
|
7
|
+
name: 'run_python',
|
|
8
|
+
description: 'Execute Python in the shared local kernel on the user’s machine. State (variables, imports) persists across calls. Returns stdout and stderr. Prefer small, verifiable steps.',
|
|
9
|
+
input_schema: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: { code: { type: 'string', description: 'Python source to run' } },
|
|
12
|
+
required: ['code'],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
describe: input => codeOf(input),
|
|
16
|
+
run: input => bridge.runCell(codeOf(input)),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const SYSTEM_PROMPT = `You are the MegaBrain Science research agent. You work in a live
|
|
20
|
+
notebook, sharing one Python kernel with the user — variables and imports persist across
|
|
21
|
+
cells and everything runs locally on the user's machine. Use run_python to write and run
|
|
22
|
+
code and iterate on its output. Prefer small, verifiable steps. When you have the answer,
|
|
23
|
+
stop calling tools and give a concise final summary of what you did and found.`;
|
|
24
|
+
const MAX_TURNS = 16;
|
|
25
|
+
export class AgentSession {
|
|
26
|
+
model;
|
|
27
|
+
emit;
|
|
28
|
+
agentContext;
|
|
29
|
+
cellCounter = 0;
|
|
30
|
+
tools;
|
|
31
|
+
messages;
|
|
32
|
+
constructor(model, tools, emit, agentContext = '', history = []) {
|
|
33
|
+
this.model = model;
|
|
34
|
+
this.emit = emit;
|
|
35
|
+
this.agentContext = agentContext;
|
|
36
|
+
this.tools = new Map(tools.map(t => [t.definition.name, t]));
|
|
37
|
+
this.messages = [...history];
|
|
38
|
+
}
|
|
39
|
+
/** The full model-visible message history (prior turns + this run) — persist to give the
|
|
40
|
+
* session memory across sends. */
|
|
41
|
+
getMessages() {
|
|
42
|
+
return this.messages;
|
|
43
|
+
}
|
|
44
|
+
async run(prompt) {
|
|
45
|
+
const system = this.agentContext
|
|
46
|
+
? `${SYSTEM_PROMPT}\n\nProject context: ${this.agentContext}`
|
|
47
|
+
: SYSTEM_PROMPT;
|
|
48
|
+
const messages = this.messages;
|
|
49
|
+
messages.push({ role: 'user', content: [{ type: 'text', text: prompt }] });
|
|
50
|
+
const toolDefs = [...this.tools.values()].map(t => t.definition);
|
|
51
|
+
try {
|
|
52
|
+
for (let turn = 0; turn < MAX_TURNS; turn += 1) {
|
|
53
|
+
const response = await this.model.complete({ system, messages, tools: toolDefs });
|
|
54
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
55
|
+
for (const block of response.content) {
|
|
56
|
+
if (block.type === 'text' && block.text.trim()) {
|
|
57
|
+
this.emit({ type: 'agent-text', text: block.text });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const toolUses = response.content.filter((b) => b.type === 'tool_use');
|
|
61
|
+
if (toolUses.length === 0) {
|
|
62
|
+
this.emit({ type: 'agent-done', text: collectText(response.content) });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const results = [];
|
|
66
|
+
for (const toolUse of toolUses) {
|
|
67
|
+
this.cellCounter += 1;
|
|
68
|
+
const cellId = `agent-${this.cellCounter}`;
|
|
69
|
+
const tool = this.tools.get(toolUse.name);
|
|
70
|
+
if (!tool) {
|
|
71
|
+
results.push({
|
|
72
|
+
type: 'tool_result',
|
|
73
|
+
tool_use_id: toolUse.id,
|
|
74
|
+
content: `Unknown tool: ${toolUse.name}`,
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
this.emit({
|
|
79
|
+
type: 'tool-call',
|
|
80
|
+
cellId,
|
|
81
|
+
tool: toolUse.name,
|
|
82
|
+
code: tool.describe(toolUse.input),
|
|
83
|
+
});
|
|
84
|
+
const { stdout, stderr } = await tool.run(toolUse.input);
|
|
85
|
+
this.emit({ type: 'tool-result', cellId, stdout, stderr });
|
|
86
|
+
const content = [stdout, stderr && `[stderr]\n${stderr}`].filter(Boolean).join('\n') || '(no output)';
|
|
87
|
+
results.push({ type: 'tool_result', tool_use_id: toolUse.id, content });
|
|
88
|
+
}
|
|
89
|
+
messages.push({ role: 'user', content: results });
|
|
90
|
+
}
|
|
91
|
+
this.emit({ type: 'agent-error', error: `Agent stopped after ${MAX_TURNS} turns.` });
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
this.emit({
|
|
95
|
+
type: 'agent-error',
|
|
96
|
+
error: error instanceof Error ? error.message : String(error),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function collectText(content) {
|
|
102
|
+
return content
|
|
103
|
+
.filter((b) => b.type === 'text')
|
|
104
|
+
.map(b => b.text)
|
|
105
|
+
.join('')
|
|
106
|
+
.trim();
|
|
107
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal model-client contract (Anthropic Messages / tool-use shape) + the MegaBrain
|
|
3
|
+
* Gateway client. Ported from the retired Electron app. The agent loop is written against
|
|
4
|
+
* `ModelClient` so the transport is swappable; in production it's the Gateway
|
|
5
|
+
* (`/api/gateway/v1/messages`) authenticated with the signed-in account token.
|
|
6
|
+
*/
|
|
7
|
+
export class GatewayModel {
|
|
8
|
+
url;
|
|
9
|
+
apiKey;
|
|
10
|
+
model;
|
|
11
|
+
maxTokens;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.url =
|
|
14
|
+
options.url ??
|
|
15
|
+
process.env.MB_GATEWAY_URL ??
|
|
16
|
+
`${(process.env.MEGABRAIN_APP_URL ?? 'https://getmegabrain.com').replace(/\/+$/, '')}/api/gateway/v1/messages`;
|
|
17
|
+
this.apiKey = options.apiKey;
|
|
18
|
+
this.model = options.model ?? process.env.MB_SCIENCE_MODEL ?? 'claude-opus-4-8';
|
|
19
|
+
this.maxTokens = options.maxTokens ?? 4096;
|
|
20
|
+
}
|
|
21
|
+
async complete(request) {
|
|
22
|
+
if (!this.apiKey)
|
|
23
|
+
throw new Error('Sign in to MegaBrain to use the agent.');
|
|
24
|
+
const response = await fetch(this.url, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: {
|
|
27
|
+
'content-type': 'application/json',
|
|
28
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
29
|
+
'anthropic-version': '2023-06-01',
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
model: this.model,
|
|
33
|
+
max_tokens: this.maxTokens,
|
|
34
|
+
system: request.system,
|
|
35
|
+
messages: request.messages,
|
|
36
|
+
tools: request.tools,
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
const detail = await response.text();
|
|
41
|
+
throw new Error(`Gateway request failed (${response.status}): ${detail.slice(0, 500)}`);
|
|
42
|
+
}
|
|
43
|
+
const data = (await response.json());
|
|
44
|
+
return { content: data.content ?? [], stopReason: data.stop_reason ?? 'end_turn' };
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/science/kernel.js
CHANGED
|
@@ -63,6 +63,48 @@ export class KernelBridge {
|
|
|
63
63
|
this.start();
|
|
64
64
|
this.child?.stdin.write(`${JSON.stringify(request)}\n`);
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Execute a cell and resolve with its collected output once the kernel returns to idle.
|
|
68
|
+
* Used by the agent's run_python tool (it needs the result to feed back to the model),
|
|
69
|
+
* while the same frames also stream live to the transcript.
|
|
70
|
+
*/
|
|
71
|
+
runCell(code, timeoutMs = 120_000) {
|
|
72
|
+
if (!this.child)
|
|
73
|
+
this.start();
|
|
74
|
+
this.cellCounter += 1;
|
|
75
|
+
const cellId = `cell-${this.cellCounter}`;
|
|
76
|
+
return new Promise(resolve => {
|
|
77
|
+
let stdout = '';
|
|
78
|
+
let stderr = '';
|
|
79
|
+
let done = false;
|
|
80
|
+
const finish = (extraErr = '') => {
|
|
81
|
+
if (done)
|
|
82
|
+
return;
|
|
83
|
+
done = true;
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
off();
|
|
86
|
+
resolve({ stdout, stderr: stderr + extraErr });
|
|
87
|
+
};
|
|
88
|
+
const off = this.onFrame(f => {
|
|
89
|
+
if (f.type === 'stream' && f.cellId === cellId) {
|
|
90
|
+
if (f.kind === 'stderr')
|
|
91
|
+
stderr += f.data;
|
|
92
|
+
else
|
|
93
|
+
stdout += f.data;
|
|
94
|
+
}
|
|
95
|
+
else if (f.type === 'status' &&
|
|
96
|
+
f.cellId === cellId &&
|
|
97
|
+
(f.state === 'idle' || f.state === 'error')) {
|
|
98
|
+
finish(f.state === 'error' && f.error ? f.error : '');
|
|
99
|
+
}
|
|
100
|
+
else if (f.type === 'status' && f.state === 'error' && !f.cellId) {
|
|
101
|
+
finish(f.error ? `\n${f.error}` : '\nkernel error');
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
const timer = setTimeout(() => finish('\n(timed out)'), timeoutMs);
|
|
105
|
+
this.send({ type: 'execute', cellId, code });
|
|
106
|
+
});
|
|
107
|
+
}
|
|
66
108
|
restart() {
|
|
67
109
|
this.stop();
|
|
68
110
|
this.buffer = '';
|
package/dist/science/pages.js
CHANGED
|
@@ -253,43 +253,94 @@ export function startWizardPage(d) {
|
|
|
253
253
|
</script>`);
|
|
254
254
|
}
|
|
255
255
|
/**
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
256
|
+
* The working session — a full-window Claude-Science-like workspace (#271/#276/#277): a left
|
|
257
|
+
* sidebar of sessions, a chat transcript of the agent's steps, and an "Ask anything" composer
|
|
258
|
+
* that drives the agent (Gateway → local kernel). Full-page layout (not the centered card),
|
|
259
|
+
* with the client logic served as /session-client.js.
|
|
259
260
|
*/
|
|
260
261
|
export function sessionPage(projectId, sessionId) {
|
|
261
|
-
const
|
|
262
|
-
return
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
262
|
+
const boot = JSON.stringify({ sid: sessionId, pid: projectId });
|
|
263
|
+
return `<!doctype html>
|
|
264
|
+
<html lang="en">
|
|
265
|
+
<head>
|
|
266
|
+
<meta charset="utf-8" />
|
|
267
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
268
|
+
<title>Session — MegaBrain Science</title>
|
|
269
|
+
<style>
|
|
270
|
+
:root { color-scheme: light dark; --bg:#fff; --panel:#faf9f8; --border:#eceae7; --fg:#1a1a1a; --muted:#8a8a8a; --accent:#c8492a; }
|
|
271
|
+
@media (prefers-color-scheme: dark) { :root { --bg:#111; --panel:#161513; --border:#262626; --fg:#ededed; --muted:#8a8a8a; --accent:#e8623a; } }
|
|
272
|
+
* { box-sizing: border-box; }
|
|
273
|
+
html, body { margin:0; height:100%; }
|
|
274
|
+
body { display:flex; height:100vh; overflow:hidden; background:var(--bg); color:var(--fg);
|
|
275
|
+
font:15px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif; }
|
|
276
|
+
/* Sidebar */
|
|
277
|
+
.sidebar { width:264px; flex-shrink:0; border-right:1px solid var(--border); background:var(--panel);
|
|
278
|
+
display:flex; flex-direction:column; padding:14px; gap:10px; }
|
|
279
|
+
.brand { display:flex; align-items:center; gap:8px; font-weight:600; font-size:14px; }
|
|
280
|
+
.mark { width:26px; height:26px; border-radius:7px; background:linear-gradient(135deg,#e8623a,#c8492a);
|
|
281
|
+
color:#fff; display:grid; place-items:center; font-size:12px; font-weight:700; }
|
|
282
|
+
.newbtn { border:1px solid var(--border); background:var(--bg); color:inherit; border-radius:10px;
|
|
283
|
+
padding:9px 12px; font:inherit; font-weight:600; cursor:pointer; text-align:left; }
|
|
284
|
+
.newbtn:hover { border-color:var(--accent); }
|
|
285
|
+
.proj { font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin-top:6px; }
|
|
286
|
+
.sessions { display:flex; flex-direction:column; gap:2px; overflow:auto; flex:1; margin:0 -6px; }
|
|
287
|
+
.session { display:block; padding:8px 10px; border-radius:8px; color:inherit; text-decoration:none;
|
|
288
|
+
font-size:14px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
289
|
+
.session:hover { background:var(--bg); }
|
|
290
|
+
.session.active { background:var(--bg); font-weight:600; }
|
|
291
|
+
.side-foot { display:flex; gap:12px; font-size:13px; }
|
|
292
|
+
.side-foot a { color:var(--muted); text-decoration:none; } .side-foot a:hover { color:var(--fg); }
|
|
293
|
+
/* Main */
|
|
294
|
+
.main { flex:1; display:flex; flex-direction:column; min-width:0; }
|
|
295
|
+
.topbar { height:52px; flex-shrink:0; border-bottom:1px solid var(--border); display:flex; align-items:center;
|
|
296
|
+
gap:10px; padding:0 20px; font-weight:600; }
|
|
297
|
+
.kstate { font-size:12px; color:var(--muted); font-weight:400; margin-left:auto; }
|
|
298
|
+
.transcript { flex:1; overflow:auto; padding:24px; display:flex; flex-direction:column; gap:16px; }
|
|
299
|
+
.wrap { width:100%; max-width:760px; margin:0 auto; display:flex; flex-direction:column; gap:16px; }
|
|
300
|
+
.msg.user { display:flex; justify-content:flex-end; }
|
|
301
|
+
.msg.user .bubble { background:var(--accent); color:#fff; border-radius:14px 14px 4px 14px; padding:9px 14px; max-width:80%; white-space:pre-wrap; }
|
|
302
|
+
.msg.agent .prose { white-space:pre-wrap; }
|
|
303
|
+
.step { border:1px solid var(--border); border-radius:12px; overflow:hidden; background:var(--panel); }
|
|
304
|
+
.step-head { display:flex; align-items:center; gap:8px; padding:9px 12px; font-size:13px; font-weight:600; }
|
|
305
|
+
.step-dot { width:7px; height:7px; border-radius:50%; background:var(--accent); }
|
|
306
|
+
.step .code, .step .out { margin:0; padding:10px 12px; font:12px/1.5 ui-monospace,Menlo,monospace;
|
|
307
|
+
white-space:pre-wrap; overflow-x:auto; border-top:1px solid var(--border); }
|
|
308
|
+
.step .out { color:var(--muted); background:var(--bg); }
|
|
309
|
+
.step .out.err { color:#c0392b; }
|
|
310
|
+
/* Composer */
|
|
311
|
+
.composer { flex-shrink:0; border-top:1px solid var(--border); padding:14px 20px; }
|
|
312
|
+
.composer-inner { max-width:760px; margin:0 auto; display:flex; gap:10px; align-items:flex-end;
|
|
313
|
+
border:1px solid var(--border); border-radius:16px; padding:10px 12px; background:var(--panel); }
|
|
314
|
+
.composer-inner:focus-within { border-color:var(--accent); }
|
|
315
|
+
textarea#composer { flex:1; border:0; background:transparent; resize:none; color:inherit; font:inherit;
|
|
316
|
+
outline:none; max-height:160px; }
|
|
317
|
+
.model { font-size:12px; color:var(--muted); align-self:center; white-space:nowrap; }
|
|
318
|
+
#send { border:0; border-radius:10px; background:var(--accent); color:#fff; font:inherit; font-weight:600;
|
|
319
|
+
padding:8px 16px; cursor:pointer; }
|
|
320
|
+
#send:disabled { opacity:.5; cursor:default; }
|
|
321
|
+
</style>
|
|
322
|
+
</head>
|
|
323
|
+
<body>
|
|
324
|
+
<aside class="sidebar">
|
|
325
|
+
<div class="brand"><span class="mark">MB</span> MegaBrain Science</div>
|
|
326
|
+
<button class="newbtn" id="new-session">+ New session</button>
|
|
327
|
+
<div class="proj" id="project-name">Project</div>
|
|
328
|
+
<nav class="sessions" id="session-list"></nav>
|
|
329
|
+
<div class="side-foot"><a href="/">Home</a><a href="/projects/${esc(projectId)}">Project</a></div>
|
|
330
|
+
</aside>
|
|
331
|
+
<main class="main">
|
|
332
|
+
<div class="topbar"><span id="session-title">Session</span><span class="kstate" id="kstate">kernel: idle</span></div>
|
|
333
|
+
<div class="transcript"><div class="wrap" id="transcript"></div></div>
|
|
334
|
+
<div class="composer">
|
|
335
|
+
<div class="composer-inner">
|
|
336
|
+
<textarea id="composer" rows="1" placeholder="Ask anything — the agent runs code locally…"></textarea>
|
|
337
|
+
<span class="model">Opus 4.8</span>
|
|
338
|
+
<button id="send">Send</button>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
</main>
|
|
342
|
+
<script>window.__MB__ = ${boot};</script>
|
|
343
|
+
<script src="/session-client.js"></script>
|
|
344
|
+
</body>
|
|
345
|
+
</html>`;
|
|
295
346
|
}
|
package/dist/science/server.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { createServer } from 'node:http';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
3
5
|
import { readCredentials, writeCredentials, clearCredentials } from '../credentials.js';
|
|
4
6
|
import { startDeviceAuth } from '../device-auth.js';
|
|
5
7
|
import { NonceStore } from './nonce.js';
|
|
6
8
|
import { homePage, loginPage, nonceInvalidPage, nonceLandingPage, projectPage, sessionPage, startWizardPage, } from './pages.js';
|
|
7
9
|
import { FEATURED_CONNECTORS, NETWORK_CATEGORIES, WorkspaceStore } from './store.js';
|
|
8
10
|
import { KernelManager } from './kernel.js';
|
|
11
|
+
import { AgentSession, pythonTool } from './agent/agent.js';
|
|
12
|
+
import { GatewayModel } from './agent/model.js';
|
|
9
13
|
/**
|
|
10
14
|
* The `megabrain science` local daemon's HTTP server. Serves the sign-in hand-off on
|
|
11
15
|
* localhost (docs/megabrain-science/reference/claude-science-flow.md §1.1) and, once signed
|
|
@@ -15,12 +19,25 @@ import { KernelManager } from './kernel.js';
|
|
|
15
19
|
*/
|
|
16
20
|
const COOKIE = 'mb_sci';
|
|
17
21
|
const PREFERRED_PORT = 8765;
|
|
22
|
+
const SESSION_CLIENT = fileURLToPath(new URL('./session-client.js', import.meta.url));
|
|
18
23
|
export async function startScienceServer(host = '127.0.0.1') {
|
|
19
24
|
const nonces = new NonceStore();
|
|
20
25
|
const trusted = new Set();
|
|
21
26
|
const store = new WorkspaceStore();
|
|
22
27
|
const kernels = new KernelManager();
|
|
23
28
|
let pending = null;
|
|
29
|
+
// Per-session Server-Sent-Events fan-out. Kernel frames + agent transcript events both
|
|
30
|
+
// flow to every browser watching a session, tagged with a `channel`.
|
|
31
|
+
const sseClients = new Map();
|
|
32
|
+
const kernelHooked = new Set();
|
|
33
|
+
const fanout = (sid, channel, payload) => {
|
|
34
|
+
const set = sseClients.get(sid);
|
|
35
|
+
if (!set)
|
|
36
|
+
return;
|
|
37
|
+
const line = `data: ${JSON.stringify({ channel, payload })}\n\n`;
|
|
38
|
+
for (const r of set)
|
|
39
|
+
r.write(line);
|
|
40
|
+
};
|
|
24
41
|
const isSignedIn = () => readCredentials() !== null;
|
|
25
42
|
const readBody = (req) => new Promise(resolve => {
|
|
26
43
|
let raw = '';
|
|
@@ -70,6 +87,15 @@ export async function startScienceServer(host = '127.0.0.1') {
|
|
|
70
87
|
const path = url.pathname;
|
|
71
88
|
const method = req.method ?? 'GET';
|
|
72
89
|
const seg = path.split('/').filter(Boolean);
|
|
90
|
+
// Static workspace client (referenced by the session page).
|
|
91
|
+
if (path === '/session-client.js' && method === 'GET') {
|
|
92
|
+
res.writeHead(200, {
|
|
93
|
+
'content-type': 'text/javascript; charset=utf-8',
|
|
94
|
+
'cache-control': 'no-cache',
|
|
95
|
+
});
|
|
96
|
+
res.end(readFileSync(SESSION_CLIENT, 'utf8'));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
73
99
|
// ── Sign-in hand-off ───────────────────────────────────────────────
|
|
74
100
|
if (path === '/' && url.searchParams.has('nonce')) {
|
|
75
101
|
const result = nonces.claim(url.searchParams.get('nonce') ?? '');
|
|
@@ -191,33 +217,59 @@ export async function startScienceServer(host = '127.0.0.1') {
|
|
|
191
217
|
});
|
|
192
218
|
return json(res, { projectId: project.id, sessionId: session?.id ?? null }, 201);
|
|
193
219
|
}
|
|
194
|
-
// ──
|
|
195
|
-
|
|
196
|
-
|
|
220
|
+
// ── Session event stream + agent (#274/#276/#277). Signed-in only. ──
|
|
221
|
+
// One SSE per session carries two channels: `kernel` (raw kernel status/output frames)
|
|
222
|
+
// and `agent` (the agent loop's transcript events). The agent's model runs in the cloud
|
|
223
|
+
// (Gateway) but drives the local kernel — compute stays on the machine.
|
|
224
|
+
if (seg[0] === 'api' &&
|
|
225
|
+
seg[1] === 'sessions' &&
|
|
226
|
+
seg[2] &&
|
|
227
|
+
(seg[3] === 'events' || seg[3] === 'agent' || seg[3] === 'kernel' || seg[3] === 'transcript')) {
|
|
228
|
+
const creds = readCredentials();
|
|
229
|
+
if (!creds)
|
|
197
230
|
return json(res, { error: 'signed-out' }, 401);
|
|
198
231
|
const sid = seg[2];
|
|
199
|
-
if (seg[
|
|
232
|
+
if (seg[3] === 'transcript' && method === 'GET') {
|
|
233
|
+
return json(res, store.getMessages(sid));
|
|
234
|
+
}
|
|
235
|
+
if (seg[3] === 'events' && method === 'GET') {
|
|
200
236
|
res.writeHead(200, {
|
|
201
237
|
'content-type': 'text/event-stream',
|
|
202
238
|
'cache-control': 'no-cache',
|
|
203
239
|
connection: 'keep-alive',
|
|
204
240
|
});
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
241
|
+
let set = sseClients.get(sid);
|
|
242
|
+
if (!set) {
|
|
243
|
+
set = new Set();
|
|
244
|
+
sseClients.set(sid, set);
|
|
245
|
+
}
|
|
246
|
+
set.add(res);
|
|
247
|
+
if (!kernelHooked.has(sid)) {
|
|
248
|
+
kernelHooked.add(sid);
|
|
249
|
+
kernels.get(sid).onFrame(frame => fanout(sid, 'kernel', frame));
|
|
250
|
+
}
|
|
251
|
+
req.on('close', () => sseClients.get(sid)?.delete(res));
|
|
209
252
|
return;
|
|
210
253
|
}
|
|
211
|
-
if (seg[4] === '
|
|
212
|
-
const b = await readBody(req);
|
|
213
|
-
const cellId = kernels.get(sid).execute(str(b.code) ?? '');
|
|
214
|
-
store.touchSession(sid);
|
|
215
|
-
return json(res, { cellId });
|
|
216
|
-
}
|
|
217
|
-
if (seg[4] === 'restart' && method === 'POST') {
|
|
254
|
+
if (seg[3] === 'kernel' && seg[4] === 'restart' && method === 'POST') {
|
|
218
255
|
kernels.restart(sid);
|
|
219
256
|
return json(res, { ok: true });
|
|
220
257
|
}
|
|
258
|
+
if (seg[3] === 'agent' && method === 'POST') {
|
|
259
|
+
const b = await readBody(req);
|
|
260
|
+
const prompt = str(b.prompt)?.trim() ?? '';
|
|
261
|
+
if (!prompt)
|
|
262
|
+
return json(res, { error: 'prompt-required' }, 400);
|
|
263
|
+
const session = store.getSession(sid);
|
|
264
|
+
const project = session ? store.getProject(session.projectId) : null;
|
|
265
|
+
store.touchSession(sid, session && !session.summary ? { summary: prompt } : undefined);
|
|
266
|
+
fanout(sid, 'agent', { type: 'user', text: prompt });
|
|
267
|
+
const agent = new AgentSession(new GatewayModel({ apiKey: creds.token }), [pythonTool(kernels.get(sid))], evt => fanout(sid, 'agent', evt), project?.agentContext ?? '', store.getMessages(sid));
|
|
268
|
+
// Persist the full history when the turn ends, so the session remembers it and the
|
|
269
|
+
// transcript survives a refresh.
|
|
270
|
+
void agent.run(prompt).finally(() => store.setMessages(sid, agent.getMessages()));
|
|
271
|
+
return json(res, { ok: true });
|
|
272
|
+
}
|
|
221
273
|
}
|
|
222
274
|
// ── Workspace API (projects/sessions, #270/#271). Signed-in only. ────
|
|
223
275
|
if (path.startsWith('/api/projects') || path.startsWith('/api/sessions')) {
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Workspace client for a MegaBrain Science session (#271/#276/#277). Renders the sidebar
|
|
2
|
+
// (sessions), the transcript (agent steps), and the composer; streams agent + kernel events
|
|
3
|
+
// over SSE. Shipped as a static asset (see copy-assets) so it needs no build-time escaping.
|
|
4
|
+
(function () {
|
|
5
|
+
var MB = window.__MB__ || {};
|
|
6
|
+
var sid = MB.sid,
|
|
7
|
+
pid = MB.pid;
|
|
8
|
+
|
|
9
|
+
var $ = function (id) {
|
|
10
|
+
return document.getElementById(id);
|
|
11
|
+
};
|
|
12
|
+
var transcript = $('transcript');
|
|
13
|
+
var scroller = document.querySelector('.transcript');
|
|
14
|
+
var composer = $('composer');
|
|
15
|
+
var sendBtn = $('send');
|
|
16
|
+
var kstate = $('kstate');
|
|
17
|
+
var lastCard = null;
|
|
18
|
+
|
|
19
|
+
function el(tag, cls, text) {
|
|
20
|
+
var e = document.createElement(tag);
|
|
21
|
+
if (cls) e.className = cls;
|
|
22
|
+
if (text != null) e.textContent = text;
|
|
23
|
+
return e;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function scroll() {
|
|
27
|
+
scroller.scrollTop = scroller.scrollHeight;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function addUser(text) {
|
|
31
|
+
var row = el('div', 'msg user');
|
|
32
|
+
row.appendChild(el('div', 'bubble', text));
|
|
33
|
+
transcript.appendChild(row);
|
|
34
|
+
scroll();
|
|
35
|
+
}
|
|
36
|
+
function addAgentText(text) {
|
|
37
|
+
var row = el('div', 'msg agent');
|
|
38
|
+
row.appendChild(el('div', 'prose', text));
|
|
39
|
+
transcript.appendChild(row);
|
|
40
|
+
scroll();
|
|
41
|
+
}
|
|
42
|
+
function addToolCall(tool, code) {
|
|
43
|
+
var card = el('div', 'step');
|
|
44
|
+
var head = el('div', 'step-head');
|
|
45
|
+
head.appendChild(el('span', 'step-dot'));
|
|
46
|
+
head.appendChild(el('span', 'step-title', tool === 'run_python' ? 'Ran Python' : tool));
|
|
47
|
+
card.appendChild(head);
|
|
48
|
+
var pre = el('pre', 'code');
|
|
49
|
+
pre.textContent = code;
|
|
50
|
+
card.appendChild(pre);
|
|
51
|
+
var out = el('pre', 'out');
|
|
52
|
+
out.hidden = true;
|
|
53
|
+
card.appendChild(out);
|
|
54
|
+
transcript.appendChild(card);
|
|
55
|
+
lastCard = card;
|
|
56
|
+
scroll();
|
|
57
|
+
}
|
|
58
|
+
function addToolResult(stdout, stderr) {
|
|
59
|
+
if (!lastCard) return;
|
|
60
|
+
var out = lastCard.querySelector('.out');
|
|
61
|
+
var text = (stdout || '') + (stderr ? (stdout ? '\n' : '') + stderr : '');
|
|
62
|
+
out.textContent = text || '(no output)';
|
|
63
|
+
out.hidden = false;
|
|
64
|
+
if (stderr) out.classList.add('err');
|
|
65
|
+
scroll();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function setRunning(on) {
|
|
69
|
+
sendBtn.disabled = on;
|
|
70
|
+
sendBtn.textContent = on ? 'Running…' : 'Send';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Replay a persisted conversation (model messages) into the transcript on load.
|
|
74
|
+
function renderHistory(messages) {
|
|
75
|
+
messages.forEach(function (m) {
|
|
76
|
+
(m.content || []).forEach(function (b) {
|
|
77
|
+
if (m.role === 'user') {
|
|
78
|
+
if (b.type === 'text') addUser(b.text);
|
|
79
|
+
else if (b.type === 'tool_result') addToolResult(b.content, '');
|
|
80
|
+
} else {
|
|
81
|
+
if (b.type === 'text') addAgentText(b.text);
|
|
82
|
+
else if (b.type === 'tool_use') addToolCall(b.name, (b.input && b.input.code) || '');
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Sidebar: this project's sessions ────────────────────────────────
|
|
89
|
+
function loadSidebar() {
|
|
90
|
+
if (!pid) return;
|
|
91
|
+
fetch('/api/projects/' + pid)
|
|
92
|
+
.then(function (r) {
|
|
93
|
+
return r.json();
|
|
94
|
+
})
|
|
95
|
+
.then(function (p) {
|
|
96
|
+
if (p.project) $('project-name').textContent = p.project.name;
|
|
97
|
+
var list = $('session-list');
|
|
98
|
+
list.textContent = '';
|
|
99
|
+
(p.sessions || []).forEach(function (s) {
|
|
100
|
+
var a = el('a', 'session' + (s.id === sid ? ' active' : ''), s.title);
|
|
101
|
+
a.href = '/projects/' + pid + '/frames/' + s.id;
|
|
102
|
+
list.appendChild(a);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Live event stream ───────────────────────────────────────────────
|
|
108
|
+
function connect() {
|
|
109
|
+
var es = new EventSource('/api/sessions/' + sid + '/events');
|
|
110
|
+
es.onmessage = function (e) {
|
|
111
|
+
var m = JSON.parse(e.data);
|
|
112
|
+
if (m.channel === 'kernel') {
|
|
113
|
+
var f = m.payload;
|
|
114
|
+
if (f.type === 'status') kstate.textContent = f.state === 'busy' ? 'running' : f.state;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
var p = m.payload;
|
|
118
|
+
if (p.type === 'user') addUser(p.text);
|
|
119
|
+
else if (p.type === 'agent-text') addAgentText(p.text);
|
|
120
|
+
else if (p.type === 'tool-call') addToolCall(p.tool, p.code);
|
|
121
|
+
else if (p.type === 'tool-result') addToolResult(p.stdout, p.stderr);
|
|
122
|
+
else if (p.type === 'agent-done') setRunning(false);
|
|
123
|
+
else if (p.type === 'agent-error') {
|
|
124
|
+
addAgentText('⚠ ' + p.error);
|
|
125
|
+
setRunning(false);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
es.onerror = function () {
|
|
129
|
+
kstate.textContent = 'reconnecting…';
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function send() {
|
|
134
|
+
var text = composer.value.trim();
|
|
135
|
+
if (!text || sendBtn.disabled) return;
|
|
136
|
+
composer.value = '';
|
|
137
|
+
setRunning(true);
|
|
138
|
+
fetch('/api/sessions/' + sid + '/agent', {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: { 'content-type': 'application/json' },
|
|
141
|
+
body: JSON.stringify({ prompt: text }),
|
|
142
|
+
}).catch(function () {
|
|
143
|
+
setRunning(false);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
sendBtn.addEventListener('click', send);
|
|
148
|
+
composer.addEventListener('keydown', function (e) {
|
|
149
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
send();
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
$('new-session').addEventListener('click', function () {
|
|
155
|
+
fetch('/api/projects/' + pid + '/sessions', {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: { 'content-type': 'application/json' },
|
|
158
|
+
body: JSON.stringify({ title: 'New session' }),
|
|
159
|
+
})
|
|
160
|
+
.then(function (r) {
|
|
161
|
+
return r.json();
|
|
162
|
+
})
|
|
163
|
+
.then(function (s) {
|
|
164
|
+
location.href = '/projects/' + pid + '/frames/' + s.id;
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
fetch('/api/sessions/' + sid + '/transcript')
|
|
169
|
+
.then(function (r) {
|
|
170
|
+
return r.json();
|
|
171
|
+
})
|
|
172
|
+
.then(function (msgs) {
|
|
173
|
+
var has = Array.isArray(msgs) && msgs.length > 0;
|
|
174
|
+
if (has) renderHistory(msgs);
|
|
175
|
+
return fetch('/api/sessions/' + sid)
|
|
176
|
+
.then(function (r) {
|
|
177
|
+
return r.json();
|
|
178
|
+
})
|
|
179
|
+
.then(function (s) {
|
|
180
|
+
if (s && s.title) $('session-title').textContent = s.title;
|
|
181
|
+
// Prefill the composer with the seeded first task only for a fresh session.
|
|
182
|
+
if (!has && s && (s.summary || s.title) && !composer.value)
|
|
183
|
+
composer.value = s.summary || s.title;
|
|
184
|
+
});
|
|
185
|
+
})
|
|
186
|
+
.catch(function () {})
|
|
187
|
+
.then(function () {
|
|
188
|
+
loadSidebar();
|
|
189
|
+
connect();
|
|
190
|
+
});
|
|
191
|
+
})();
|
package/dist/science/store.js
CHANGED
|
@@ -28,6 +28,8 @@ function defaultSettings() {
|
|
|
28
28
|
profile: '',
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
+
const MAX_MESSAGES = 60;
|
|
32
|
+
const MAX_TOOL_RESULT_CHARS = 4000;
|
|
31
33
|
const STORE_FILE = path.join(MEGABRAIN_CONFIG_DIR, 'science-workspace.json');
|
|
32
34
|
function id(prefix) {
|
|
33
35
|
return `${prefix}_${randomBytes(8).toString('hex')}`;
|
|
@@ -48,6 +50,7 @@ export class WorkspaceStore {
|
|
|
48
50
|
projects: parsed.projects ?? [],
|
|
49
51
|
sessions: parsed.sessions ?? [],
|
|
50
52
|
settings: parsed.settings,
|
|
53
|
+
transcripts: parsed.transcripts ?? {},
|
|
51
54
|
};
|
|
52
55
|
}
|
|
53
56
|
catch {
|
|
@@ -157,4 +160,19 @@ export class WorkspaceStore {
|
|
|
157
160
|
this.save();
|
|
158
161
|
return next;
|
|
159
162
|
}
|
|
163
|
+
// ── Per-session conversation history (agent memory + transcript replay) ──
|
|
164
|
+
getMessages(sessionId) {
|
|
165
|
+
return this.data.transcripts?.[sessionId] ?? [];
|
|
166
|
+
}
|
|
167
|
+
setMessages(sessionId, messages) {
|
|
168
|
+
// Cap history length and truncate large tool outputs so the on-disk workspace stays small.
|
|
169
|
+
const capped = messages.slice(-MAX_MESSAGES).map(m => ({
|
|
170
|
+
role: m.role,
|
|
171
|
+
content: m.content.map(b => b.type === 'tool_result' && b.content.length > MAX_TOOL_RESULT_CHARS
|
|
172
|
+
? { ...b, content: `${b.content.slice(0, MAX_TOOL_RESULT_CHARS)}\n…(truncated)` }
|
|
173
|
+
: b),
|
|
174
|
+
}));
|
|
175
|
+
(this.data.transcripts ??= {})[sessionId] = capped;
|
|
176
|
+
this.save();
|
|
177
|
+
}
|
|
160
178
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getmegabrain/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "MegaBrain CLI — sign in once, then code from your terminal via OpenCode wired to the MegaBrain Gateway, or run `megabrain science` for the local research workbench.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|