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.
Files changed (142) hide show
  1. package/README.md +110 -16
  2. package/dist/assets/errorwidget-Wy5ajPiM.js +5 -0
  3. package/dist/assets/fa-brands-400-CEJbCg16.woff +0 -0
  4. package/dist/assets/fa-brands-400-CSYNqBb_.ttf +0 -0
  5. package/dist/assets/fa-brands-400-DnkPfk3o.eot +0 -0
  6. package/dist/assets/fa-brands-400-UxlILjvJ.woff2 +0 -0
  7. package/dist/assets/fa-brands-400-cH1MgKbP.svg +3717 -0
  8. package/dist/assets/fa-regular-400-BhTwtT8w.eot +0 -0
  9. package/dist/assets/fa-regular-400-D1vz6WBx.ttf +0 -0
  10. package/dist/assets/fa-regular-400-DFnMcJPd.woff +0 -0
  11. package/dist/assets/fa-regular-400-DGzu1beS.woff2 +0 -0
  12. package/dist/assets/fa-regular-400-gwj8Pxq-.svg +801 -0
  13. package/dist/assets/fa-solid-900-B4ZZ7kfP.svg +5034 -0
  14. package/dist/assets/fa-solid-900-B6Axprfb.eot +0 -0
  15. package/dist/assets/fa-solid-900-BUswJgRo.woff2 +0 -0
  16. package/dist/assets/fa-solid-900-DOXgCApm.woff +0 -0
  17. package/dist/assets/fa-solid-900-mxuxnBEa.ttf +0 -0
  18. package/dist/assets/index-B_Bs5koo.js +1 -0
  19. package/dist/assets/index-BtWv4MIT.css +7 -0
  20. package/dist/assets/index-DK4nS74r.js +829 -0
  21. package/dist/assets/index-DeQN03nW.js +81 -0
  22. package/dist/assets/index-DzjpmUak.css +32 -0
  23. package/dist/assets/index-g6MjT-VG.js +1 -0
  24. package/dist/assets/services-shim-BirHVsih.js +33 -0
  25. package/dist/assets/viewlist-uomDf7I7.js +1 -0
  26. package/dist/assets/widgets-X7J3NxEn.css +1 -0
  27. package/dist/index.html +2 -2
  28. package/node-server/dist/auth/auth-middleware.js +22 -4
  29. package/node-server/dist/auth/auth-service.js +37 -7
  30. package/node-server/dist/cluster/client-registration.d.ts +12 -1
  31. package/node-server/dist/cluster/client-registration.js +38 -2
  32. package/node-server/dist/cluster/kernel-proxy.js +43 -12
  33. package/node-server/dist/cluster/server-registry.d.ts +17 -0
  34. package/node-server/dist/cluster/server-registry.js +57 -7
  35. package/node-server/dist/discovery/conda-locations.d.ts +73 -0
  36. package/node-server/dist/discovery/conda-locations.js +427 -0
  37. package/node-server/dist/discovery/discovery-service.d.ts +73 -4
  38. package/node-server/dist/discovery/discovery-service.js +211 -127
  39. package/node-server/dist/discovery/types.d.ts +0 -1
  40. package/node-server/dist/discovery/types.js +1 -2
  41. package/node-server/dist/environment.d.ts +12 -0
  42. package/node-server/dist/environment.js +104 -0
  43. package/node-server/dist/fs/fs-service.d.ts +86 -8
  44. package/node-server/dist/fs/fs-service.js +640 -99
  45. package/node-server/dist/fs/notebook-formats/percent.d.ts +25 -0
  46. package/node-server/dist/fs/notebook-formats/percent.js +286 -0
  47. package/node-server/dist/fs/notebook-formats/qmd.d.ts +29 -0
  48. package/node-server/dist/fs/notebook-formats/qmd.js +307 -0
  49. package/node-server/dist/fs/notebook-formats/registry.d.ts +12 -0
  50. package/node-server/dist/fs/notebook-formats/registry.js +77 -0
  51. package/node-server/dist/fs/notebook-formats/types.d.ts +37 -0
  52. package/node-server/dist/fs/notebook-formats/types.js +13 -0
  53. package/node-server/dist/fs/types.d.ts +9 -0
  54. package/node-server/dist/fs/types.js +8 -0
  55. package/node-server/dist/idle-exit.d.ts +52 -0
  56. package/node-server/dist/idle-exit.js +83 -0
  57. package/node-server/dist/index.js +198 -8
  58. package/node-server/dist/kernel/kernel-service.d.ts +130 -2
  59. package/node-server/dist/kernel/kernel-service.js +834 -64
  60. package/node-server/dist/kernel/kernelspec.d.ts +27 -0
  61. package/node-server/dist/kernel/kernelspec.js +75 -4
  62. package/node-server/dist/notebook/headless-handler.d.ts +12 -0
  63. package/node-server/dist/notebook/headless-handler.js +125 -29
  64. package/node-server/dist/notebook/operation-router.js +7 -2
  65. package/node-server/dist/notebook/undoRedoManager.d.ts +12 -1
  66. package/node-server/dist/notebook/undoRedoManager.js +49 -18
  67. package/node-server/dist/output/display-data.js +2 -0
  68. package/node-server/dist/private-tmp.d.ts +14 -0
  69. package/node-server/dist/private-tmp.js +70 -0
  70. package/node-server/dist/routes/autocomplete.d.ts +19 -0
  71. package/node-server/dist/routes/autocomplete.js +294 -0
  72. package/node-server/dist/routes/cluster.js +2 -2
  73. package/node-server/dist/routes/compute.d.ts +8 -0
  74. package/node-server/dist/routes/compute.js +136 -0
  75. package/node-server/dist/routes/fs.js +23 -6
  76. package/node-server/dist/routes/kernel.js +156 -3
  77. package/node-server/dist/routes/notebook.js +37 -3
  78. package/node-server/dist/routes/python.d.ts +1 -2
  79. package/node-server/dist/routes/python.js +139 -3
  80. package/node-server/dist/scheduler/allocation-service.d.ts +63 -0
  81. package/node-server/dist/scheduler/allocation-service.js +285 -0
  82. package/node-server/dist/scheduler/job-template.d.ts +30 -0
  83. package/node-server/dist/scheduler/job-template.js +85 -0
  84. package/node-server/dist/scheduler/mock-scheduler.d.ts +30 -0
  85. package/node-server/dist/scheduler/mock-scheduler.js +125 -0
  86. package/node-server/dist/scheduler/slurm-scheduler.d.ts +31 -0
  87. package/node-server/dist/scheduler/slurm-scheduler.js +406 -0
  88. package/node-server/dist/scheduler/types.d.ts +118 -0
  89. package/node-server/dist/scheduler/types.js +8 -0
  90. package/node-server/dist/scheduler/util.d.ts +7 -0
  91. package/node-server/dist/scheduler/util.js +20 -0
  92. package/node-server/dist/terminal/agent-registry.d.ts +87 -0
  93. package/node-server/dist/terminal/agent-registry.js +246 -0
  94. package/node-server/dist/terminal/binding-store.d.ts +57 -0
  95. package/node-server/dist/terminal/binding-store.js +0 -0
  96. package/node-server/dist/terminal/pty-manager.d.ts +23 -6
  97. package/node-server/dist/terminal/pty-manager.js +138 -19
  98. package/node-server/dist/terminal/server.js +155 -23
  99. package/node-server/dist/terminal/types.d.ts +2 -0
  100. package/node-server/dist/update-check.d.ts +20 -0
  101. package/node-server/dist/update-check.js +114 -0
  102. package/node-server/package.json +2 -0
  103. package/node_modules/nebula-autocomplete/README.md +145 -0
  104. package/node_modules/nebula-autocomplete/dist/backends/claude.d.ts +69 -0
  105. package/node_modules/nebula-autocomplete/dist/backends/claude.js +449 -0
  106. package/node_modules/nebula-autocomplete/dist/backends/codex.d.ts +45 -0
  107. package/node_modules/nebula-autocomplete/dist/backends/codex.js +108 -0
  108. package/node_modules/nebula-autocomplete/dist/client/fetcher.d.ts +16 -0
  109. package/node_modules/nebula-autocomplete/dist/client/fetcher.js +41 -0
  110. package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.d.ts +43 -0
  111. package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.js +175 -0
  112. package/node_modules/nebula-autocomplete/dist/core/engine.d.ts +17 -0
  113. package/node_modules/nebula-autocomplete/dist/core/engine.js +135 -0
  114. package/node_modules/nebula-autocomplete/dist/core/lru.d.ts +7 -0
  115. package/node_modules/nebula-autocomplete/dist/core/lru.js +26 -0
  116. package/node_modules/nebula-autocomplete/dist/core/prompt.d.ts +12 -0
  117. package/node_modules/nebula-autocomplete/dist/core/prompt.js +0 -0
  118. package/node_modules/nebula-autocomplete/dist/core/text.d.ts +53 -0
  119. package/node_modules/nebula-autocomplete/dist/core/text.js +169 -0
  120. package/node_modules/nebula-autocomplete/dist/index.d.ts +7 -0
  121. package/node_modules/nebula-autocomplete/dist/index.js +5 -0
  122. package/node_modules/nebula-autocomplete/dist/server/fastify.d.ts +40 -0
  123. package/node_modules/nebula-autocomplete/dist/server/fastify.js +52 -0
  124. package/node_modules/nebula-autocomplete/dist/transport.d.ts +55 -0
  125. package/node_modules/nebula-autocomplete/dist/transport.js +66 -0
  126. package/node_modules/nebula-autocomplete/dist/types.d.ts +103 -0
  127. package/node_modules/nebula-autocomplete/dist/types.js +1 -0
  128. package/node_modules/nebula-autocomplete/package.json +58 -0
  129. package/node_modules/nebula-autocomplete/server.d.ts +3 -0
  130. package/node_modules/nebula-autocomplete/server.js +3 -0
  131. package/package.json +18 -5
  132. package/scripts/postinstall.cjs +25 -5
  133. package/dist/assets/index-BvrHu37J.js +0 -648
  134. package/dist/assets/index-Dfj_xsKU.css +0 -32
  135. package/node-server/dist/llm/index.d.ts +0 -5
  136. package/node-server/dist/llm/index.js +0 -21
  137. package/node-server/dist/llm/llm-service.d.ts +0 -77
  138. package/node-server/dist/llm/llm-service.js +0 -454
  139. package/node-server/dist/llm/types.d.ts +0 -40
  140. package/node-server/dist/llm/types.js +0 -15
  141. package/node-server/dist/routes/llm.d.ts +0 -8
  142. 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({ status: 'ok', terminals: pty_manager_1.ptyManager.list().length, repo_root: NEBULA_REPO_ROOT });
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
- const replayMsg = { type: 'replay', data: buffer };
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
- // Collaborative mode: broadcast output to ALL connected clients
191
- const broadcastOutput = (data) => {
192
- const msg = { type: 'output', data };
193
- const msgStr = JSON.stringify(msg);
194
- for (const conn of connections) {
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.setOnData(terminalId, broadcastOutput);
201
- // Set up exit listener - broadcast to all connections
202
- pty_manager_1.ptyManager.setOnExit(terminalId, (code) => {
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
  });
@@ -35,6 +35,8 @@ export type ServerMessage = {
35
35
  } | {
36
36
  type: 'replay';
37
37
  data: string;
38
+ cols?: number;
39
+ rows?: number;
38
40
  } | {
39
41
  type: 'exit';
40
42
  code: number;
@@ -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
+ }
@@ -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
+ }