nebula-notebook 0.1.1 → 0.2.1
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 +110 -16
- package/dist/assets/errorwidget-Wy5ajPiM.js +5 -0
- package/dist/assets/fa-brands-400-CEJbCg16.woff +0 -0
- package/dist/assets/fa-brands-400-CSYNqBb_.ttf +0 -0
- package/dist/assets/fa-brands-400-DnkPfk3o.eot +0 -0
- package/dist/assets/fa-brands-400-UxlILjvJ.woff2 +0 -0
- package/dist/assets/fa-brands-400-cH1MgKbP.svg +3717 -0
- package/dist/assets/fa-regular-400-BhTwtT8w.eot +0 -0
- package/dist/assets/fa-regular-400-D1vz6WBx.ttf +0 -0
- package/dist/assets/fa-regular-400-DFnMcJPd.woff +0 -0
- package/dist/assets/fa-regular-400-DGzu1beS.woff2 +0 -0
- package/dist/assets/fa-regular-400-gwj8Pxq-.svg +801 -0
- package/dist/assets/fa-solid-900-B4ZZ7kfP.svg +5034 -0
- package/dist/assets/fa-solid-900-B6Axprfb.eot +0 -0
- package/dist/assets/fa-solid-900-BUswJgRo.woff2 +0 -0
- package/dist/assets/fa-solid-900-DOXgCApm.woff +0 -0
- package/dist/assets/fa-solid-900-mxuxnBEa.ttf +0 -0
- package/dist/assets/index-B_Bs5koo.js +1 -0
- package/dist/assets/index-BtWv4MIT.css +7 -0
- package/dist/assets/index-DK4nS74r.js +829 -0
- package/dist/assets/index-DeQN03nW.js +81 -0
- package/dist/assets/index-DzjpmUak.css +32 -0
- package/dist/assets/index-g6MjT-VG.js +1 -0
- package/dist/assets/services-shim-BirHVsih.js +33 -0
- package/dist/assets/viewlist-uomDf7I7.js +1 -0
- package/dist/assets/widgets-X7J3NxEn.css +1 -0
- package/dist/index.html +2 -2
- package/node-server/dist/auth/auth-middleware.js +22 -4
- package/node-server/dist/auth/auth-service.js +37 -7
- package/node-server/dist/cluster/client-registration.d.ts +12 -1
- package/node-server/dist/cluster/client-registration.js +38 -2
- package/node-server/dist/cluster/kernel-proxy.js +43 -12
- package/node-server/dist/cluster/server-registry.d.ts +17 -0
- package/node-server/dist/cluster/server-registry.js +57 -7
- package/node-server/dist/discovery/conda-locations.d.ts +73 -0
- package/node-server/dist/discovery/conda-locations.js +427 -0
- package/node-server/dist/discovery/discovery-service.d.ts +73 -4
- package/node-server/dist/discovery/discovery-service.js +211 -127
- package/node-server/dist/discovery/types.d.ts +0 -1
- package/node-server/dist/discovery/types.js +1 -2
- package/node-server/dist/environment.d.ts +12 -0
- package/node-server/dist/environment.js +104 -0
- package/node-server/dist/fs/fs-service.d.ts +86 -8
- package/node-server/dist/fs/fs-service.js +640 -99
- package/node-server/dist/fs/notebook-formats/percent.d.ts +25 -0
- package/node-server/dist/fs/notebook-formats/percent.js +286 -0
- package/node-server/dist/fs/notebook-formats/qmd.d.ts +29 -0
- package/node-server/dist/fs/notebook-formats/qmd.js +307 -0
- package/node-server/dist/fs/notebook-formats/registry.d.ts +12 -0
- package/node-server/dist/fs/notebook-formats/registry.js +77 -0
- package/node-server/dist/fs/notebook-formats/types.d.ts +37 -0
- package/node-server/dist/fs/notebook-formats/types.js +13 -0
- package/node-server/dist/fs/types.d.ts +9 -0
- package/node-server/dist/fs/types.js +8 -0
- package/node-server/dist/idle-exit.d.ts +52 -0
- package/node-server/dist/idle-exit.js +83 -0
- package/node-server/dist/index.js +198 -8
- package/node-server/dist/kernel/kernel-service.d.ts +130 -2
- package/node-server/dist/kernel/kernel-service.js +834 -64
- package/node-server/dist/kernel/kernelspec.d.ts +27 -0
- package/node-server/dist/kernel/kernelspec.js +75 -4
- package/node-server/dist/notebook/headless-handler.d.ts +12 -0
- package/node-server/dist/notebook/headless-handler.js +125 -29
- package/node-server/dist/notebook/operation-router.js +7 -2
- package/node-server/dist/notebook/undoRedoManager.d.ts +12 -1
- package/node-server/dist/notebook/undoRedoManager.js +49 -18
- package/node-server/dist/output/display-data.js +2 -0
- package/node-server/dist/private-tmp.d.ts +14 -0
- package/node-server/dist/private-tmp.js +70 -0
- package/node-server/dist/routes/autocomplete.d.ts +19 -0
- package/node-server/dist/routes/autocomplete.js +294 -0
- package/node-server/dist/routes/cluster.js +2 -2
- package/node-server/dist/routes/compute.d.ts +8 -0
- package/node-server/dist/routes/compute.js +136 -0
- package/node-server/dist/routes/fs.js +23 -6
- package/node-server/dist/routes/kernel.js +156 -3
- package/node-server/dist/routes/notebook.js +37 -3
- package/node-server/dist/routes/python.d.ts +1 -2
- package/node-server/dist/routes/python.js +139 -3
- package/node-server/dist/scheduler/allocation-service.d.ts +63 -0
- package/node-server/dist/scheduler/allocation-service.js +285 -0
- package/node-server/dist/scheduler/job-template.d.ts +30 -0
- package/node-server/dist/scheduler/job-template.js +85 -0
- package/node-server/dist/scheduler/mock-scheduler.d.ts +30 -0
- package/node-server/dist/scheduler/mock-scheduler.js +125 -0
- package/node-server/dist/scheduler/slurm-scheduler.d.ts +31 -0
- package/node-server/dist/scheduler/slurm-scheduler.js +406 -0
- package/node-server/dist/scheduler/types.d.ts +118 -0
- package/node-server/dist/scheduler/types.js +8 -0
- package/node-server/dist/scheduler/util.d.ts +7 -0
- package/node-server/dist/scheduler/util.js +20 -0
- package/node-server/dist/terminal/agent-registry.d.ts +87 -0
- package/node-server/dist/terminal/agent-registry.js +246 -0
- package/node-server/dist/terminal/binding-store.d.ts +57 -0
- package/node-server/dist/terminal/binding-store.js +0 -0
- package/node-server/dist/terminal/pty-manager.d.ts +23 -6
- package/node-server/dist/terminal/pty-manager.js +138 -19
- package/node-server/dist/terminal/server.js +155 -23
- package/node-server/dist/terminal/types.d.ts +2 -0
- package/node-server/dist/update-check.d.ts +20 -0
- package/node-server/dist/update-check.js +114 -0
- package/node-server/package.json +2 -0
- package/node_modules/nebula-autocomplete/README.md +145 -0
- package/node_modules/nebula-autocomplete/dist/backends/claude.d.ts +69 -0
- package/node_modules/nebula-autocomplete/dist/backends/claude.js +449 -0
- package/node_modules/nebula-autocomplete/dist/backends/codex.d.ts +45 -0
- package/node_modules/nebula-autocomplete/dist/backends/codex.js +108 -0
- package/node_modules/nebula-autocomplete/dist/client/fetcher.d.ts +16 -0
- package/node_modules/nebula-autocomplete/dist/client/fetcher.js +41 -0
- package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.d.ts +43 -0
- package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.js +175 -0
- package/node_modules/nebula-autocomplete/dist/core/engine.d.ts +17 -0
- package/node_modules/nebula-autocomplete/dist/core/engine.js +135 -0
- package/node_modules/nebula-autocomplete/dist/core/lru.d.ts +7 -0
- package/node_modules/nebula-autocomplete/dist/core/lru.js +26 -0
- package/node_modules/nebula-autocomplete/dist/core/prompt.d.ts +12 -0
- package/node_modules/nebula-autocomplete/dist/core/prompt.js +0 -0
- package/node_modules/nebula-autocomplete/dist/core/text.d.ts +53 -0
- package/node_modules/nebula-autocomplete/dist/core/text.js +169 -0
- package/node_modules/nebula-autocomplete/dist/index.d.ts +7 -0
- package/node_modules/nebula-autocomplete/dist/index.js +5 -0
- package/node_modules/nebula-autocomplete/dist/server/fastify.d.ts +40 -0
- package/node_modules/nebula-autocomplete/dist/server/fastify.js +52 -0
- package/node_modules/nebula-autocomplete/dist/transport.d.ts +55 -0
- package/node_modules/nebula-autocomplete/dist/transport.js +66 -0
- package/node_modules/nebula-autocomplete/dist/types.d.ts +103 -0
- package/node_modules/nebula-autocomplete/dist/types.js +1 -0
- package/node_modules/nebula-autocomplete/package.json +58 -0
- package/node_modules/nebula-autocomplete/server.d.ts +3 -0
- package/node_modules/nebula-autocomplete/server.js +3 -0
- package/package.json +18 -5
- package/scripts/postinstall.cjs +25 -5
- package/dist/assets/index-BvrHu37J.js +0 -648
- package/dist/assets/index-Dfj_xsKU.css +0 -32
- package/node-server/dist/llm/index.d.ts +0 -5
- package/node-server/dist/llm/index.js +0 -21
- package/node-server/dist/llm/llm-service.d.ts +0 -77
- package/node-server/dist/llm/llm-service.js +0 -454
- package/node-server/dist/llm/types.d.ts +0 -40
- package/node-server/dist/llm/types.js +0 -15
- package/node-server/dist/routes/llm.d.ts +0 -8
- package/node-server/dist/routes/llm.js +0 -105
|
@@ -40,8 +40,12 @@ exports.setupTerminalRoutes = setupTerminalRoutes;
|
|
|
40
40
|
exports.setupTerminalWebSocket = setupTerminalWebSocket;
|
|
41
41
|
exports.cleanupTerminals = cleanupTerminals;
|
|
42
42
|
const ws_1 = require("ws");
|
|
43
|
+
const net = __importStar(require("net"));
|
|
44
|
+
const os = __importStar(require("os"));
|
|
43
45
|
const path = __importStar(require("path"));
|
|
44
46
|
const pty_manager_1 = require("./pty-manager");
|
|
47
|
+
const agent_registry_1 = require("./agent-registry");
|
|
48
|
+
const binding_store_1 = require("./binding-store");
|
|
45
49
|
const fs_service_1 = require("../fs/fs-service");
|
|
46
50
|
// Get configured root directory for terminals
|
|
47
51
|
function getDefaultCwd() {
|
|
@@ -59,11 +63,82 @@ const wsConnections = new Map();
|
|
|
59
63
|
*/
|
|
60
64
|
async function setupTerminalRoutes(fastify) {
|
|
61
65
|
// Terminal health check (includes terminal count and repo root for the
|
|
62
|
-
// path-qualified MCP setup hint shown in the agent terminal UI
|
|
66
|
+
// path-qualified MCP setup hint shown in the agent terminal UI, plus
|
|
67
|
+
// hostname/port so the UI can compose an exact SSH tunnel command)
|
|
63
68
|
fastify.get('/api/terminals/health', async (_request, reply) => {
|
|
64
|
-
return reply.send({
|
|
69
|
+
return reply.send({
|
|
70
|
+
status: 'ok',
|
|
71
|
+
terminals: pty_manager_1.ptyManager.list().length,
|
|
72
|
+
repo_root: NEBULA_REPO_ROOT,
|
|
73
|
+
hostname: os.hostname(),
|
|
74
|
+
port: Number(process.env.PORT) || 3000,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
// Probe a loopback port on THIS host — used by remote-agent mode to detect
|
|
78
|
+
// whether the user's reverse SSH channel (ssh -R <port>:localhost:22) is up.
|
|
79
|
+
// Loopback-only by construction; the port is user-chosen and random.
|
|
80
|
+
fastify.get('/api/terminals/reverse-check', async (request, reply) => {
|
|
81
|
+
const port = Number(request.query.port);
|
|
82
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535) {
|
|
83
|
+
return reply.status(400).send({ error: 'port must be an integer in 1024-65535' });
|
|
84
|
+
}
|
|
85
|
+
// Two-signal probe at the cost of one connection: `up` = the listener
|
|
86
|
+
// accepted (fast path, unchanged); `ssh` = an SSH banner arrived within
|
|
87
|
+
// 700ms of connecting (an sshd greets immediately with "SSH-2.0-...").
|
|
88
|
+
// ssh=false means the port is forwarded but nothing SSH answers — the
|
|
89
|
+
// classic Remote-Login-off case; ssh=null means banner unknown (slow
|
|
90
|
+
// network) and MUST NOT be treated as a failure.
|
|
91
|
+
const result = await new Promise((resolve) => {
|
|
92
|
+
const sock = net.connect({ host: '127.0.0.1', port, timeout: 1200 });
|
|
93
|
+
let bannerTimer = null;
|
|
94
|
+
const finish = (up, ssh) => {
|
|
95
|
+
if (bannerTimer)
|
|
96
|
+
clearTimeout(bannerTimer);
|
|
97
|
+
sock.destroy();
|
|
98
|
+
resolve({ up, ssh });
|
|
99
|
+
};
|
|
100
|
+
sock.once('connect', () => {
|
|
101
|
+
bannerTimer = setTimeout(() => finish(true, null), 700);
|
|
102
|
+
sock.once('data', (buf) => finish(true, buf.toString('latin1').startsWith('SSH-')));
|
|
103
|
+
});
|
|
104
|
+
sock.once('timeout', () => finish(false, null));
|
|
105
|
+
sock.once('error', () => finish(false, null));
|
|
106
|
+
});
|
|
107
|
+
return reply.send({ up: result.up, ssh: result.ssh });
|
|
65
108
|
});
|
|
66
109
|
// List all terminals
|
|
110
|
+
// ---- Agent registry: project-scoped agent sessions (see agent-registry.ts) ----
|
|
111
|
+
fastify.get('/api/agents', async (_request, reply) => {
|
|
112
|
+
return reply.send({ agents: await agent_registry_1.agentRegistry.listEnriched() });
|
|
113
|
+
});
|
|
114
|
+
fastify.post('/api/agents/register', async (request, reply) => {
|
|
115
|
+
const b = (request.body ?? {});
|
|
116
|
+
if (!b.terminalId || !b.kind || !b.workdir) {
|
|
117
|
+
return reply.code(400).send({ error: 'terminalId, kind, workdir required' });
|
|
118
|
+
}
|
|
119
|
+
const record = agent_registry_1.agentRegistry.register({
|
|
120
|
+
terminalId: b.terminalId,
|
|
121
|
+
kind: b.kind === 'codex' ? 'codex' : 'claude',
|
|
122
|
+
workdir: b.workdir,
|
|
123
|
+
location: b.location === 'remote' ? 'remote' : 'server',
|
|
124
|
+
sessionId: b.sessionId,
|
|
125
|
+
launchedFrom: b.launchedFrom,
|
|
126
|
+
mirrorSlug: b.mirrorSlug,
|
|
127
|
+
});
|
|
128
|
+
return reply.send(record);
|
|
129
|
+
});
|
|
130
|
+
fastify.post('/api/agents/:id/hibernate', async (request, reply) => {
|
|
131
|
+
const { id } = request.params;
|
|
132
|
+
if (!agent_registry_1.agentRegistry.hibernate(id))
|
|
133
|
+
return reply.code(404).send({ error: 'unknown agent' });
|
|
134
|
+
return reply.send({ ok: true });
|
|
135
|
+
});
|
|
136
|
+
fastify.delete('/api/agents/:id', async (request, reply) => {
|
|
137
|
+
const { id } = request.params;
|
|
138
|
+
if (!agent_registry_1.agentRegistry.remove(id))
|
|
139
|
+
return reply.code(404).send({ error: 'unknown agent' });
|
|
140
|
+
return reply.send({ ok: true });
|
|
141
|
+
});
|
|
67
142
|
fastify.get('/api/terminals', async (_request, reply) => {
|
|
68
143
|
return reply.send(pty_manager_1.ptyManager.list());
|
|
69
144
|
});
|
|
@@ -104,6 +179,63 @@ async function setupTerminalRoutes(fastify) {
|
|
|
104
179
|
});
|
|
105
180
|
}
|
|
106
181
|
});
|
|
182
|
+
// --- Terminal bindings: which named pty a notebook's panel attaches to ---
|
|
183
|
+
// GET returns the stored binding, or the plane default when none is stored
|
|
184
|
+
// (shell → server-shared srv-main; agent → project scope, name resolved
|
|
185
|
+
// client-side from the chosen workdir).
|
|
186
|
+
fastify.get('/api/terminals/binding', async (request, reply) => {
|
|
187
|
+
const { file_path, plane } = request.query;
|
|
188
|
+
if (!file_path || (plane !== 'shell' && plane !== 'agent')) {
|
|
189
|
+
return reply.code(400).send({ detail: 'file_path and plane (shell|agent) are required' });
|
|
190
|
+
}
|
|
191
|
+
const stored = binding_store_1.terminalBindings.get(file_path, plane);
|
|
192
|
+
if (stored) {
|
|
193
|
+
return reply.send({
|
|
194
|
+
plane, scope: stored.scope, name: stored.name,
|
|
195
|
+
custom_name: stored.customName ?? null, stored: true,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (plane === 'shell') {
|
|
199
|
+
return reply.send({ plane, scope: 'server', name: binding_store_1.SHARED_SHELL_NAME, custom_name: null, stored: false });
|
|
200
|
+
}
|
|
201
|
+
return reply.send({ plane, scope: 'project', name: null, custom_name: null, stored: false });
|
|
202
|
+
});
|
|
203
|
+
fastify.put('/api/terminals/binding', async (request, reply) => {
|
|
204
|
+
const { file_path, plane, scope, name } = request.body || {};
|
|
205
|
+
if (!file_path || (plane !== 'shell' && plane !== 'agent')) {
|
|
206
|
+
return reply.code(400).send({ detail: 'file_path and plane (shell|agent) are required' });
|
|
207
|
+
}
|
|
208
|
+
if (!['server', 'project', 'notebook', 'named'].includes(scope || '')) {
|
|
209
|
+
return reply.code(400).send({ detail: 'scope must be server|project|notebook|named' });
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const binding = binding_store_1.terminalBindings.set(file_path, plane, scope, name);
|
|
213
|
+
return reply.send({
|
|
214
|
+
plane, scope: binding.scope, name: binding.name,
|
|
215
|
+
custom_name: binding.customName ?? null, stored: true,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
return reply.code(400).send({ detail: e instanceof Error ? e.message : 'Invalid binding' });
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
fastify.delete('/api/terminals/binding', async (request, reply) => {
|
|
223
|
+
const { file_path, plane } = request.query;
|
|
224
|
+
if (!file_path || (plane !== 'shell' && plane !== 'agent')) {
|
|
225
|
+
return reply.code(400).send({ detail: 'file_path and plane (shell|agent) are required' });
|
|
226
|
+
}
|
|
227
|
+
binding_store_1.terminalBindings.delete(file_path, plane);
|
|
228
|
+
return reply.send({ ok: true });
|
|
229
|
+
});
|
|
230
|
+
// Process-table probe: does this pty's shell have a live child? Clients MUST
|
|
231
|
+
// check this before typing a launch command into an existing pty — records
|
|
232
|
+
// can mis-score liveness (a hand-resumed agent never re-registers), and a
|
|
233
|
+
// launch line typed into a live TUI becomes a chat prompt (lab report).
|
|
234
|
+
// busy:null = unknown (pty gone / pgrep unavailable) — claim nothing.
|
|
235
|
+
fastify.get('/api/terminals/:id/busy', async (request, reply) => {
|
|
236
|
+
const busy = await pty_manager_1.ptyManager.hasLiveChild(request.params.id);
|
|
237
|
+
return reply.send({ busy });
|
|
238
|
+
});
|
|
107
239
|
// Get terminal info
|
|
108
240
|
fastify.get('/api/terminals/:id', async (request, reply) => {
|
|
109
241
|
const terminal = pty_manager_1.ptyManager.getTerminalInfo(request.params.id);
|
|
@@ -184,28 +316,28 @@ function setupTerminalWebSocket(server) {
|
|
|
184
316
|
// Send buffered output for reconnection
|
|
185
317
|
const buffer = pty_manager_1.ptyManager.getOutputBuffer(terminalId);
|
|
186
318
|
if (buffer) {
|
|
187
|
-
|
|
319
|
+
// Append an authoritative reassert of the sticky input modes so a
|
|
320
|
+
// reconnect never leaves mouse/focus reporting dangling on at the shell
|
|
321
|
+
// (the buffer trim can drop or split the app's own mode-reset).
|
|
322
|
+
const info = pty_manager_1.ptyManager.getTerminalInfo(terminalId);
|
|
323
|
+
const replayMsg = {
|
|
324
|
+
type: 'replay',
|
|
325
|
+
data: buffer + pty_manager_1.ptyManager.getModeReset(terminalId),
|
|
326
|
+
cols: info?.cols,
|
|
327
|
+
rows: info?.rows,
|
|
328
|
+
};
|
|
188
329
|
ws.send(JSON.stringify(replayMsg));
|
|
189
330
|
}
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (conn.readyState === ws_1.WebSocket.OPEN) {
|
|
196
|
-
conn.send(msgStr);
|
|
197
|
-
}
|
|
331
|
+
// One subscription per websocket, sending only to ITS socket — the pty
|
|
332
|
+
// layer now multicasts, so no shared-closure broadcast trickery needed.
|
|
333
|
+
const unsubData = pty_manager_1.ptyManager.addDataListener(terminalId, (data) => {
|
|
334
|
+
if (ws.readyState === ws_1.WebSocket.OPEN) {
|
|
335
|
+
ws.send(JSON.stringify({ type: 'output', data }));
|
|
198
336
|
}
|
|
199
|
-
};
|
|
200
|
-
pty_manager_1.ptyManager.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const msg = { type: 'exit', code };
|
|
204
|
-
const msgStr = JSON.stringify(msg);
|
|
205
|
-
for (const conn of connections) {
|
|
206
|
-
if (conn.readyState === ws_1.WebSocket.OPEN) {
|
|
207
|
-
conn.send(msgStr);
|
|
208
|
-
}
|
|
337
|
+
});
|
|
338
|
+
const unsubExit = pty_manager_1.ptyManager.addExitListener(terminalId, (code) => {
|
|
339
|
+
if (ws.readyState === ws_1.WebSocket.OPEN) {
|
|
340
|
+
ws.send(JSON.stringify({ type: 'exit', code }));
|
|
209
341
|
}
|
|
210
342
|
});
|
|
211
343
|
// Handle incoming messages
|
|
@@ -230,13 +362,13 @@ function setupTerminalWebSocket(server) {
|
|
|
230
362
|
// Handle disconnection
|
|
231
363
|
ws.on('close', () => {
|
|
232
364
|
console.log(`[Terminal] WebSocket disconnected from terminal ${terminalId}`);
|
|
365
|
+
unsubData();
|
|
366
|
+
unsubExit();
|
|
233
367
|
const connections = wsConnections.get(terminalId);
|
|
234
368
|
if (connections) {
|
|
235
369
|
connections.delete(ws);
|
|
236
370
|
if (connections.size === 0) {
|
|
237
371
|
wsConnections.delete(terminalId);
|
|
238
|
-
// Clear the data listener when no connections
|
|
239
|
-
pty_manager_1.ptyManager.setOnData(terminalId, null);
|
|
240
372
|
}
|
|
241
373
|
}
|
|
242
374
|
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update check — notify-only, never auto-installs.
|
|
3
|
+
*
|
|
4
|
+
* Once at startup (and daily after), asks the npm registry for the latest
|
|
5
|
+
* published nebula-notebook version and compares it to the running one.
|
|
6
|
+
* Result is surfaced in /api/health (`update`) for the UI pill and logged
|
|
7
|
+
* once to the server console. Opt out with NEBULA_NO_UPDATE_CHECK=1.
|
|
8
|
+
* Network failures are silent: an update check must never affect operation.
|
|
9
|
+
*/
|
|
10
|
+
export interface UpdateInfo {
|
|
11
|
+
current: string;
|
|
12
|
+
latest: string | null;
|
|
13
|
+
update_available: boolean;
|
|
14
|
+
checked_at: number | null;
|
|
15
|
+
}
|
|
16
|
+
/** Numeric semver compare on MAJOR.MINOR.PATCH; ignores prerelease tags. */
|
|
17
|
+
export declare function isNewerVersion(latest: string, current: string): boolean;
|
|
18
|
+
export declare function startUpdateChecker(): void;
|
|
19
|
+
export declare function stopUpdateChecker(): void;
|
|
20
|
+
export declare function getUpdateInfo(): UpdateInfo;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Update check — notify-only, never auto-installs.
|
|
4
|
+
*
|
|
5
|
+
* Once at startup (and daily after), asks the npm registry for the latest
|
|
6
|
+
* published nebula-notebook version and compares it to the running one.
|
|
7
|
+
* Result is surfaced in /api/health (`update`) for the UI pill and logged
|
|
8
|
+
* once to the server console. Opt out with NEBULA_NO_UPDATE_CHECK=1.
|
|
9
|
+
* Network failures are silent: an update check must never affect operation.
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.isNewerVersion = isNewerVersion;
|
|
46
|
+
exports.startUpdateChecker = startUpdateChecker;
|
|
47
|
+
exports.stopUpdateChecker = stopUpdateChecker;
|
|
48
|
+
exports.getUpdateInfo = getUpdateInfo;
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/nebula-notebook/latest';
|
|
52
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // daily
|
|
53
|
+
const FETCH_TIMEOUT_MS = 4000;
|
|
54
|
+
let info = { current: readCurrentVersion(), latest: null, update_available: false, checked_at: null };
|
|
55
|
+
let timer = null;
|
|
56
|
+
/** Running version from the repo/package root (this file is at node-server/{src,dist}/ — 2 levels up). */
|
|
57
|
+
function readCurrentVersion() {
|
|
58
|
+
try {
|
|
59
|
+
const pkgPath = path.resolve(__dirname, '..', '..', 'package.json');
|
|
60
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
61
|
+
if (typeof pkg.version === 'string')
|
|
62
|
+
return pkg.version;
|
|
63
|
+
}
|
|
64
|
+
catch { /* fall through */ }
|
|
65
|
+
return '0.0.0';
|
|
66
|
+
}
|
|
67
|
+
/** Numeric semver compare on MAJOR.MINOR.PATCH; ignores prerelease tags. */
|
|
68
|
+
function isNewerVersion(latest, current) {
|
|
69
|
+
const parse = (v) => v.replace(/^v/, '').split('-')[0].split('.').map(n => parseInt(n, 10) || 0);
|
|
70
|
+
const [lMaj, lMin, lPat] = parse(latest);
|
|
71
|
+
const [cMaj, cMin, cPat] = parse(current);
|
|
72
|
+
if (lMaj !== cMaj)
|
|
73
|
+
return lMaj > cMaj;
|
|
74
|
+
if (lMin !== cMin)
|
|
75
|
+
return lMin > cMin;
|
|
76
|
+
return lPat > cPat;
|
|
77
|
+
}
|
|
78
|
+
async function checkOnce() {
|
|
79
|
+
try {
|
|
80
|
+
const controller = new AbortController();
|
|
81
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
82
|
+
const res = await fetch(REGISTRY_URL, { signal: controller.signal });
|
|
83
|
+
clearTimeout(timeout);
|
|
84
|
+
if (!res.ok)
|
|
85
|
+
return;
|
|
86
|
+
const data = (await res.json());
|
|
87
|
+
if (typeof data.version !== 'string')
|
|
88
|
+
return;
|
|
89
|
+
const updateAvailable = isNewerVersion(data.version, info.current);
|
|
90
|
+
const firstDiscovery = updateAvailable && !info.update_available;
|
|
91
|
+
info = { ...info, latest: data.version, update_available: updateAvailable, checked_at: Date.now() };
|
|
92
|
+
if (firstDiscovery) {
|
|
93
|
+
console.log(`[Update] nebula-notebook v${data.version} is available (running v${info.current}) — ` +
|
|
94
|
+
`update with: npx nebula-notebook@latest (or git pull for source installs)`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch { /* offline / registry down / air-gapped cluster — stay quiet */ }
|
|
98
|
+
}
|
|
99
|
+
function startUpdateChecker() {
|
|
100
|
+
if (process.env.NEBULA_NO_UPDATE_CHECK)
|
|
101
|
+
return;
|
|
102
|
+
void checkOnce();
|
|
103
|
+
timer = setInterval(() => void checkOnce(), CHECK_INTERVAL_MS);
|
|
104
|
+
timer.unref?.(); // never keep the process alive for this
|
|
105
|
+
}
|
|
106
|
+
function stopUpdateChecker() {
|
|
107
|
+
if (timer) {
|
|
108
|
+
clearInterval(timer);
|
|
109
|
+
timer = null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function getUpdateInfo() {
|
|
113
|
+
return info;
|
|
114
|
+
}
|
package/node-server/package.json
CHANGED
|
@@ -20,10 +20,12 @@
|
|
|
20
20
|
"better-sqlite3": "^11.0.0",
|
|
21
21
|
"fastify": "^5.8.2",
|
|
22
22
|
"jsonwebtoken": "^9.0.2",
|
|
23
|
+
"nebula-autocomplete": "file:../packages/autocomplete",
|
|
23
24
|
"otplib": "^12.0.1",
|
|
24
25
|
"qrcode-terminal": "^0.12.0",
|
|
25
26
|
"uuid": "^9.0.1",
|
|
26
27
|
"ws": "^8.16.0",
|
|
28
|
+
"yaml": "^2.9.0",
|
|
27
29
|
"zeromq": "^6.4.2"
|
|
28
30
|
},
|
|
29
31
|
"devDependencies": {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# nebula-autocomplete
|
|
2
|
+
|
|
3
|
+
LLM code autocomplete for notebook editors, backed by **Claude Code CLI** or
|
|
4
|
+
**Codex CLI** subscription auth (no API key). Three entry points:
|
|
5
|
+
|
|
6
|
+
| Import | What it is |
|
|
7
|
+
|---|---|
|
|
8
|
+
| `nebula-autocomplete` | `AutocompleteEngine` + `ClaudeBackend` / `CodexBackend` (Node) |
|
|
9
|
+
| `nebula-autocomplete/server` | `registerAutocompleteRoute(fastify, engine)` — SSE endpoint |
|
|
10
|
+
| `nebula-autocomplete/client` | `createCompletionFetcher(url)` — browser SSE client |
|
|
11
|
+
| `nebula-autocomplete/codemirror` | `ghostText(fetcher)` — CodeMirror 6 inline ghost text |
|
|
12
|
+
|
|
13
|
+
> **Licensing:** subscription auth is for personal use. Anthropic/OpenAI
|
|
14
|
+
> consumer terms prohibit routing other users through your subscription or
|
|
15
|
+
> offering subscription login in a distributed app. To ship this to others,
|
|
16
|
+
> implement a `CompletionBackend` that calls the provider API with a key —
|
|
17
|
+
> the rest of the stack is unchanged.
|
|
18
|
+
|
|
19
|
+
## Measured latency (2026-07, M-series Mac)
|
|
20
|
+
|
|
21
|
+
| Backend | Model | Warm TTFB | Notes |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| `ClaudeBackend` | haiku 4.5 | ~1.4–2.1s | token-streamed; warm process pool |
|
|
24
|
+
| `CodexBackend` | gpt-5.6-luna, low effort | ~2.2–2.7s | buffered; one-shot exec |
|
|
25
|
+
|
|
26
|
+
Cache hits return in 0ms. Claude's harness costs ~24k input tokens per
|
|
27
|
+
completion, codex ~12.6k — that overhead is inherent to CLI backends and is
|
|
28
|
+
why a raw-API backend (~0.6–1s) is the upgrade path for keystroke-grade UX.
|
|
29
|
+
|
|
30
|
+
## Quick start (server side)
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { AutocompleteEngine, ClaudeBackend } from "nebula-autocomplete";
|
|
34
|
+
import { registerAutocompleteRoute } from "nebula-autocomplete/server";
|
|
35
|
+
|
|
36
|
+
const engine = new AutocompleteEngine({
|
|
37
|
+
backend: new ClaudeBackend(), // or new CodexBackend({ codexHome })
|
|
38
|
+
cacheSize: 128,
|
|
39
|
+
contextBudget: 6000, // chars of cross-cell context
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// inside your Fastify route registration (prefix "/api"):
|
|
43
|
+
registerAutocompleteRoute(fastify, engine); // POST /api/autocomplete (SSE)
|
|
44
|
+
|
|
45
|
+
process.on("exit", () => engine.dispose());
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`CodexBackend` tip: create a clean `CODEX_HOME` containing only `auth.json`
|
|
49
|
+
(`mkdir -p .codex-home && cp ~/.codex/auth.json .codex-home/`) and pass it as
|
|
50
|
+
`codexHome` — it skips global MCP servers/plugins/hooks (~2s/request faster).
|
|
51
|
+
`gpt-5.6-luna` is the fast model that works with ChatGPT-subscription auth;
|
|
52
|
+
`*-codex-mini` variants are API-key-only.
|
|
53
|
+
|
|
54
|
+
## Quick start (client side, CodeMirror 6)
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { createCompletionFetcher } from "nebula-autocomplete/client";
|
|
58
|
+
import { ghostText } from "nebula-autocomplete/codemirror";
|
|
59
|
+
|
|
60
|
+
const fetchCompletion = createCompletionFetcher("/api/autocomplete");
|
|
61
|
+
|
|
62
|
+
const extension = ghostText(
|
|
63
|
+
({ prefix, suffix }, { signal, onChunk }) =>
|
|
64
|
+
fetchCompletion(
|
|
65
|
+
{
|
|
66
|
+
prefix,
|
|
67
|
+
suffix,
|
|
68
|
+
language: "python",
|
|
69
|
+
cells: allCellsRef.current ?? [], // cross-cell context
|
|
70
|
+
activeCellIndex,
|
|
71
|
+
sessionKey: cellId, // supersedes stale requests
|
|
72
|
+
},
|
|
73
|
+
{ signal, onChunk },
|
|
74
|
+
).then((r) => r.text),
|
|
75
|
+
{ debounceMs: 400 },
|
|
76
|
+
);
|
|
77
|
+
// add `extension` to the editor's extensions array
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Tab accepts the suggestion, Escape dismisses, any edit or cursor move
|
|
81
|
+
invalidates it. Ghost text renders via a `.cm-ghost-text` span (style it in
|
|
82
|
+
your theme if the default 0.45 opacity doesn't fit).
|
|
83
|
+
|
|
84
|
+
## Integrating into nebula-notebook
|
|
85
|
+
|
|
86
|
+
This package now lives inside the nebula-notebook repo at
|
|
87
|
+
`packages/autocomplete` and is wired up already:
|
|
88
|
+
|
|
89
|
+
1. **Dependency** (in-repo `file:` link; bundled into the published tarball via
|
|
90
|
+
`bundledDependencies`):
|
|
91
|
+
- root `package.json`: `"nebula-autocomplete": "file:packages/autocomplete"`
|
|
92
|
+
- `node-server/package.json`: `"nebula-autocomplete": "file:../packages/autocomplete"`
|
|
93
|
+
`dist/` is built automatically by this package's `prepare` script on
|
|
94
|
+
`npm install` (see `scripts/postinstall.cjs`).
|
|
95
|
+
2. **Server**: in `node-server/src/index.ts` where routes are registered under
|
|
96
|
+
`/api` (~line 302), create one engine at startup and call
|
|
97
|
+
`registerAutocompleteRoute(app, engine)`. Dispose it on shutdown.
|
|
98
|
+
3. **Client**: in `components/CodeEditor.tsx`, add the `ghostText(...)`
|
|
99
|
+
extension to the extensions `useMemo` (~line 725). The existing
|
|
100
|
+
kernel/static dropdown completion (`createCombinedCompletionSource`, line
|
|
101
|
+
533) stays as-is — ghost text is complementary, not a replacement. Cells
|
|
102
|
+
context is already available via the `allCellsRef` prop (line 51).
|
|
103
|
+
4. **Dev proxy**: nothing to add — Vite already proxies `/api` to the node
|
|
104
|
+
server (`vite.config.ts:19`).
|
|
105
|
+
|
|
106
|
+
## API sketch
|
|
107
|
+
|
|
108
|
+
- `engine.complete(req, { signal, onChunk }) → Promise<CompletionResult>` —
|
|
109
|
+
builds the prompt (instruction + nearest-cells context within budget +
|
|
110
|
+
`<CURSOR>` marker), checks the LRU cache, and calls the backend. Requests
|
|
111
|
+
sharing a `sessionKey` supersede each other. Output is fence-stripped and
|
|
112
|
+
prefix-echo-trimmed.
|
|
113
|
+
- `CompletionBackend` — implement `{ name, complete(prompt, {signal, onChunk}), dispose() }`
|
|
114
|
+
to add a backend (e.g. Anthropic API for production).
|
|
115
|
+
|
|
116
|
+
## Trajectory hygiene
|
|
117
|
+
|
|
118
|
+
Completions must not spam CLI history/trajectory stores. Verified behavior:
|
|
119
|
+
|
|
120
|
+
- **claude**: print mode + stream-json + `--tools ""` was observed to persist
|
|
121
|
+
nothing on macOS (claude 2.1.201), but Claude Code **can** write
|
|
122
|
+
`~/.claude/projects/<munged-cwd>/*.jsonl` session transcripts on other
|
|
123
|
+
versions/platforms — so we don't rely on that. Each worker runs in its own
|
|
124
|
+
UUID cwd; the backend deletes the matching `~/.claude/projects/*` transcript
|
|
125
|
+
dir on worker recycle (per-worker, so it never touches an active worker) and
|
|
126
|
+
the whole set on `dispose()`. Matching is by UUID token via
|
|
127
|
+
`transcriptDirMatchesToken`, which is munge-algorithm-independent and cannot
|
|
128
|
+
collide with a real project. Respects `CLAUDE_CONFIG_DIR`. The throwaway
|
|
129
|
+
`workspaceDir` (also removed on dispose) is a second guard.
|
|
130
|
+
- **codex**: `--ephemeral` (no session/rollout files),
|
|
131
|
+
`history.persistence="none"` (no prompt history), and
|
|
132
|
+
`features.plugins=false` (skips the curated-plugins sync, which otherwise
|
|
133
|
+
clones tens of MB into `CODEX_HOME/.tmp` on **every** exec). Verified: zero
|
|
134
|
+
files and stable directory size across repeated completions. Codex still
|
|
135
|
+
maintains a few small bounded sqlite files in `CODEX_HOME`.
|
|
136
|
+
|
|
137
|
+
## Development
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
npm install
|
|
141
|
+
npm run build # tsc → dist/
|
|
142
|
+
npm test # vitest unit tests (no CLI calls)
|
|
143
|
+
npm run smoke # live end-to-end vs claude (uses your subscription)
|
|
144
|
+
NEBULA_CODEX_HOME=path/to/.codex-home npm run smoke codex
|
|
145
|
+
```
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { CompletionBackend } from "../types.js";
|
|
2
|
+
import { type Transport } from "../transport.js";
|
|
3
|
+
export interface ClaudeBackendOptions {
|
|
4
|
+
/** Model passed to --model. Default "haiku" (fastest tier). */
|
|
5
|
+
model?: string;
|
|
6
|
+
/** Warm worker processes. Default 2. */
|
|
7
|
+
poolSize?: number;
|
|
8
|
+
/** Turns before a worker is recycled (stream-json turns share history). Default 8. */
|
|
9
|
+
maxTurnsPerWorker?: number;
|
|
10
|
+
/** Accumulated prompt chars before a worker is recycled. History size — not
|
|
11
|
+
* turn count — is what drags TTFB, so this is the primary recycle driver.
|
|
12
|
+
* Default 18_000 (~3 big-notebook prompts). */
|
|
13
|
+
maxHistoryCharsPerWorker?: number;
|
|
14
|
+
/** Per-turn timeout. Default 45_000 ms. */
|
|
15
|
+
turnTimeoutMs?: number;
|
|
16
|
+
/** Working directory for workers — keep it empty so no CLAUDE.md loads. */
|
|
17
|
+
workspaceDir?: string;
|
|
18
|
+
/** Path to the claude binary. Default "claude". */
|
|
19
|
+
binary?: string;
|
|
20
|
+
/** Where the claude process runs: locally (default) or over ssh on the user's
|
|
21
|
+
* machine (remote Nebula + local agent). Default { kind: "local" }. */
|
|
22
|
+
transport?: Transport;
|
|
23
|
+
/** Thinking-token budget. Default 0: thinking never streams as ghost text
|
|
24
|
+
* (observed burning a whole turn invisibly), but a budget can improve
|
|
25
|
+
* tricky completions — surfaced as an Advanced setting. */
|
|
26
|
+
maxThinkingTokens?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Persistent warm pool of `claude` CLI processes (subscription auth) in
|
|
30
|
+
* stream-json mode. Workers are recycled after maxTurnsPerWorker completions
|
|
31
|
+
* so conversation history never bloats the context.
|
|
32
|
+
*/
|
|
33
|
+
export declare class ClaudeBackend implements CompletionBackend {
|
|
34
|
+
readonly name = "claude";
|
|
35
|
+
private pool;
|
|
36
|
+
private opts;
|
|
37
|
+
private disposed;
|
|
38
|
+
private readonly wsToken;
|
|
39
|
+
constructor(options?: ClaudeBackendOptions);
|
|
40
|
+
/**
|
|
41
|
+
* Delete any Claude Code session-transcript directories that belong to this
|
|
42
|
+
* backend. Claude writes `~/.claude/projects/<munged-cwd>/*.jsonl` on some
|
|
43
|
+
* versions/platforms; the munged name preserves alphanumerics, so our
|
|
44
|
+
* wsToken (a UUID) always survives. We match by comparing alphanumeric-only
|
|
45
|
+
* forms, which is munge-algorithm-independent and cannot collide with a real
|
|
46
|
+
* project path. Best-effort: never throws into the caller.
|
|
47
|
+
*/
|
|
48
|
+
private pruneTranscripts;
|
|
49
|
+
private ensurePool;
|
|
50
|
+
/**
|
|
51
|
+
* Spawn a worker wired for background self-healing: the claude CLI exits on
|
|
52
|
+
* its own after long idle, and replacing it lazily (at the next request)
|
|
53
|
+
* put the whole cold start — process boot + ssh reconnect + warmup turn —
|
|
54
|
+
* on the user's first post-idle completion (multi-second "autocomplete is
|
|
55
|
+
* dead after I come back" lag). Respawning the moment a worker dies keeps
|
|
56
|
+
* the pool warm through idle, so the first keystroke after a break pays the
|
|
57
|
+
* same ~1.5s as any other. Crash-loop guard: only self-respawn for workers
|
|
58
|
+
* that were healthy (survived 60s or completed a turn) — a broken binary
|
|
59
|
+
* dies instantly and would otherwise spawn-loop forever.
|
|
60
|
+
*/
|
|
61
|
+
private spawnWorker;
|
|
62
|
+
complete(prompt: string, { signal, onChunk, diag }?: {
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
onChunk?: (t: string) => void;
|
|
65
|
+
diag?: import("../types.js").CompletionDiag;
|
|
66
|
+
}): Promise<string>;
|
|
67
|
+
private attempt;
|
|
68
|
+
dispose(): void;
|
|
69
|
+
}
|