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
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ /**
3
+ * Agent registry — the server-side ledger of agent sessions (Claude Code /
4
+ * Codex CLIs running inside terminal ptys).
5
+ *
6
+ * Agents are project-scoped (keyed by the working directory they were
7
+ * launched in) and decoupled from notebooks: switching notebooks never kills
8
+ * or switches an agent; the user does, through the agent manager. Records
9
+ * persist to disk so agents survive as *resumable* entities even when their
10
+ * pty dies (browser gone, server restart, user hibernates them): the CLI's
11
+ * own on-disk trajectory (`claude --resume <id>` / `codex resume`) can
12
+ * reconstruct the conversation — the registry just remembers what exists,
13
+ * where, and how to revive it. The client owns launch/revive command
14
+ * construction (it knows about remote-agent mode); the server owns truth
15
+ * about what is live.
16
+ *
17
+ * States: 'live' (pty exists) → 'hibernated' (pty gone, trajectory on disk).
18
+ * All records load as 'hibernated' on boot — ptys never survive a restart.
19
+ */
20
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ var desc = Object.getOwnPropertyDescriptor(m, k);
23
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
24
+ desc = { enumerable: true, get: function() { return m[k]; } };
25
+ }
26
+ Object.defineProperty(o, k2, desc);
27
+ }) : (function(o, m, k, k2) {
28
+ if (k2 === undefined) k2 = k;
29
+ o[k2] = m[k];
30
+ }));
31
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
32
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
33
+ }) : function(o, v) {
34
+ o["default"] = v;
35
+ });
36
+ var __importStar = (this && this.__importStar) || (function () {
37
+ var ownKeys = function(o) {
38
+ ownKeys = Object.getOwnPropertyNames || function (o) {
39
+ var ar = [];
40
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
41
+ return ar;
42
+ };
43
+ return ownKeys(o);
44
+ };
45
+ return function (mod) {
46
+ if (mod && mod.__esModule) return mod;
47
+ var result = {};
48
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
49
+ __setModuleDefault(result, mod);
50
+ return result;
51
+ };
52
+ })();
53
+ Object.defineProperty(exports, "__esModule", { value: true });
54
+ exports.agentRegistry = void 0;
55
+ const fs = __importStar(require("fs"));
56
+ const path = __importStar(require("path"));
57
+ const os = __importStar(require("os"));
58
+ const pty_manager_1 = require("./pty-manager");
59
+ const STATE_FILE = process.env.NEBULA_AGENTS_FILE || path.join(os.homedir(), '.nebula', 'agents.json');
60
+ const MAX_RECORDS = 50; // oldest hibernated records beyond this are dropped
61
+ class AgentRegistry {
62
+ records = new Map();
63
+ exitUnsubs = new Map();
64
+ loaded = false;
65
+ ensureLoaded() {
66
+ if (this.loaded)
67
+ return;
68
+ this.loaded = true;
69
+ try {
70
+ if (fs.existsSync(STATE_FILE)) {
71
+ const list = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
72
+ for (const r of list) {
73
+ if (!r?.terminalId)
74
+ continue;
75
+ // Ptys never survive a server restart — everything reloads hibernated.
76
+ r.state = 'hibernated';
77
+ this.records.set(r.terminalId, r);
78
+ }
79
+ }
80
+ }
81
+ catch (err) {
82
+ console.error('[AgentRegistry] failed to load state:', err);
83
+ }
84
+ }
85
+ persist() {
86
+ try {
87
+ const dir = path.dirname(STATE_FILE);
88
+ fs.mkdirSync(dir, { recursive: true });
89
+ const all = [...this.records.values()].sort((a, b) => b.lastLaunchAt - a.lastLaunchAt);
90
+ const live = all.filter((r) => r.state === 'live');
91
+ const hibernated = all.filter((r) => r.state !== 'live').slice(0, MAX_RECORDS - live.length);
92
+ const tmp = `${STATE_FILE}.tmp`;
93
+ fs.writeFileSync(tmp, JSON.stringify([...live, ...hibernated], null, 2));
94
+ fs.renameSync(tmp, STATE_FILE);
95
+ }
96
+ catch (err) {
97
+ console.error('[AgentRegistry] failed to persist state:', err);
98
+ }
99
+ }
100
+ /**
101
+ * Register (or re-register on revive) an agent launched in a terminal.
102
+ * MERGE semantics: fields the caller didn't supply NEVER overwrite stored
103
+ * truth — a resume relaunch that doesn't know the sessionId must not erase
104
+ * the registry's copy (that id is the conversation; losing it downgrades
105
+ * every later Continue to a picker).
106
+ */
107
+ register(rec) {
108
+ this.ensureLoaded();
109
+ const existing = this.records.get(rec.terminalId);
110
+ const record = {
111
+ ...existing,
112
+ ...rec,
113
+ sessionId: rec.sessionId ?? existing?.sessionId,
114
+ mirrorSlug: rec.mirrorSlug ?? existing?.mirrorSlug,
115
+ launchedFrom: rec.launchedFrom ?? existing?.launchedFrom,
116
+ state: 'live',
117
+ createdAt: existing?.createdAt ?? Date.now(),
118
+ lastLaunchAt: Date.now(),
119
+ };
120
+ this.records.set(rec.terminalId, record);
121
+ // When the pty dies for any reason (user exits the CLI and closes the
122
+ // terminal, hibernate, server-side kill), the agent is not gone — its
123
+ // trajectory is on disk. Mark it hibernated, ready to revive.
124
+ this.exitUnsubs.get(rec.terminalId)?.();
125
+ const unsub = pty_manager_1.ptyManager.addExitListener(rec.terminalId, () => {
126
+ const r = this.records.get(rec.terminalId);
127
+ if (r && r.state === 'live') {
128
+ r.state = 'hibernated';
129
+ this.persist();
130
+ }
131
+ this.exitUnsubs.delete(rec.terminalId);
132
+ });
133
+ this.exitUnsubs.set(rec.terminalId, unsub);
134
+ // SERVER-SIDE liveness: the pty stream passes through this process, so the
135
+ // agent-alive state machine lives HERE, not in whichever browser happens to
136
+ // be attached. TUI teardown with no re-init within the grace window means
137
+ // the agent exited to the shell — the record flips to 'hibernated' even
138
+ // though the pty lives on (revive types the resume command into it).
139
+ this.watchLiveness(rec.terminalId);
140
+ this.persist();
141
+ return record;
142
+ }
143
+ static TUI_INIT_RE = /\x1b\[\?(1049|1004|1000|1002|1003)h/;
144
+ static TUI_TEARDOWN_RE = /\x1b\[\?(1049|1004|1000|1002|1003)l/;
145
+ dataUnsubs = new Map();
146
+ graceTimers = new Map();
147
+ watchLiveness(terminalId) {
148
+ this.dataUnsubs.get(terminalId)?.();
149
+ const unsub = pty_manager_1.ptyManager.addDataListener(terminalId, (chunk) => {
150
+ this.observeOutput(terminalId, chunk);
151
+ });
152
+ if (unsub)
153
+ this.dataUnsubs.set(terminalId, unsub);
154
+ }
155
+ /** Feed pty output through the liveness machine (public for tests). */
156
+ observeOutput(terminalId, chunk) {
157
+ const r = this.records.get(terminalId);
158
+ if (!r)
159
+ return;
160
+ if (AgentRegistry.TUI_INIT_RE.test(chunk)) {
161
+ const t = this.graceTimers.get(terminalId);
162
+ if (t) {
163
+ clearTimeout(t);
164
+ this.graceTimers.delete(terminalId);
165
+ }
166
+ if (r.state !== 'live') {
167
+ r.state = 'live';
168
+ r.lastLaunchAt = Date.now();
169
+ this.persist();
170
+ }
171
+ return;
172
+ }
173
+ if (AgentRegistry.TUI_TEARDOWN_RE.test(chunk) && r.state === 'live' && !this.graceTimers.has(terminalId)) {
174
+ this.graceTimers.set(terminalId, setTimeout(() => {
175
+ this.graceTimers.delete(terminalId);
176
+ const rec = this.records.get(terminalId);
177
+ if (rec && rec.state === 'live') {
178
+ rec.state = 'hibernated'; // agent exited; pty may still be a plain shell
179
+ this.persist();
180
+ }
181
+ }, 2000));
182
+ }
183
+ }
184
+ /** List all agents, reconciling 'live' against actual pty existence. */
185
+ list() {
186
+ this.ensureLoaded();
187
+ for (const r of this.records.values()) {
188
+ if (r.state === 'live' && !pty_manager_1.ptyManager.get(r.terminalId)) {
189
+ r.state = 'hibernated';
190
+ }
191
+ }
192
+ return [...this.records.values()].sort((a, b) => b.lastLaunchAt - a.lastLaunchAt);
193
+ }
194
+ /**
195
+ * list() plus process-table facts for every record whose pty exists:
196
+ * - `busy`: the pty's shell has a live child — SOMETHING is running there.
197
+ * Crucially reported on HIBERNATED records too: the TUI-stream liveness
198
+ * machine can mis-score (an agent resumed by hand never re-registers), and
199
+ * a client that trusts 'hibernated' then types a launch command straight
200
+ * into the running TUI's input box (lab report: a live codex politely
201
+ * declined to run the pasted ssh line). The process table is authoritative.
202
+ * - `idleShell` on live records: the inverse — 'live' but nothing running
203
+ * (a hung-then-dead ssh hop leaves exactly this; a dead transport emits
204
+ * no teardown, so only the process table can see it).
205
+ * Unknown checks claim nothing.
206
+ */
207
+ async listEnriched() {
208
+ return Promise.all(this.list().map(async (r) => {
209
+ if (!pty_manager_1.ptyManager.get(r.terminalId))
210
+ return r;
211
+ const hasChild = await pty_manager_1.ptyManager.hasLiveChild(r.terminalId);
212
+ if (hasChild === null)
213
+ return r;
214
+ return r.state === 'live'
215
+ ? { ...r, idleShell: !hasChild, busy: hasChild }
216
+ : { ...r, busy: hasChild };
217
+ }));
218
+ }
219
+ /** Hibernate: close the pty; the record (and on-disk trajectory) remain. */
220
+ hibernate(terminalId) {
221
+ this.ensureLoaded();
222
+ const r = this.records.get(terminalId);
223
+ if (!r)
224
+ return false;
225
+ if (pty_manager_1.ptyManager.get(terminalId))
226
+ pty_manager_1.ptyManager.kill(terminalId); // exit listener marks hibernated
227
+ r.state = 'hibernated';
228
+ this.persist();
229
+ return true;
230
+ }
231
+ /** Forget the agent entirely (pty closed; registry record removed). */
232
+ remove(terminalId) {
233
+ this.ensureLoaded();
234
+ const r = this.records.get(terminalId);
235
+ if (!r)
236
+ return false;
237
+ this.exitUnsubs.get(terminalId)?.();
238
+ this.exitUnsubs.delete(terminalId);
239
+ if (pty_manager_1.ptyManager.get(terminalId))
240
+ pty_manager_1.ptyManager.kill(terminalId);
241
+ this.records.delete(terminalId);
242
+ this.persist();
243
+ return true;
244
+ }
245
+ }
246
+ exports.agentRegistry = new AgentRegistry();
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Terminal Bindings
3
+ *
4
+ * A notebook has exactly one binding per plane (shell / agent) that decides
5
+ * which named pty its panel attaches to — attach is always deterministic and
6
+ * duplicates are impossible by construction (get-or-create on the name).
7
+ *
8
+ * Scopes:
9
+ * server → the per-server shared terminal (well-known name `srv-main`)
10
+ * project → shared by all notebooks in the same directory (`proj-<hash>-<dir>`)
11
+ * notebook → private to the notebook (`nb-<hash>-<plane>-<slug>` — the exact
12
+ * scheme the panel has always used, so pre-binding ptys with live
13
+ * jobs are grandfathered in, not orphaned)
14
+ * named → a user-chosen name (normalized like every pty name)
15
+ *
16
+ * Persisted as JSON at ~/.nebula/terminal-bindings.json so a notebook's
17
+ * binding follows it across browsers (same pattern as the agent registry).
18
+ */
19
+ export type TerminalPlane = 'shell' | 'agent';
20
+ export type TerminalBindingScope = 'server' | 'project' | 'notebook' | 'named';
21
+ export interface TerminalBinding {
22
+ filePath: string;
23
+ plane: TerminalPlane;
24
+ scope: TerminalBindingScope;
25
+ /** Resolved pty name the panel attaches to (get-or-create). Null for
26
+ * agent/project bindings — the client derives it from the chosen workdir. */
27
+ name: string | null;
28
+ /** The raw user-supplied name for scope 'named'. */
29
+ customName?: string;
30
+ updatedAt: number;
31
+ }
32
+ /** Well-known name of the per-server shared shell. */
33
+ export declare const SHARED_SHELL_NAME = "srv-main";
34
+ /** Well-known name of the per-server shared AGENT terminal. */
35
+ export declare const SHARED_AGENT_NAME = "agent-srv";
36
+ /**
37
+ * Resolve a (plane, scope) to the pty name it binds to. Pure — no I/O.
38
+ *
39
+ * Agent-plane specifics: names double as agent-record keys. `project` returns
40
+ * null — the workdir is a client-side choice (workdir chip), so the client
41
+ * derives the legacy `agent-<hash>-<slug>` name from it (which is also why
42
+ * every pre-binding record keeps working). Named agents get an `agent-`
43
+ * prefix so they can never collide with a shell terminal of the same name.
44
+ */
45
+ export declare function resolveBindingName(plane: TerminalPlane, scope: TerminalBindingScope, filePath: string, customName?: string): string | null;
46
+ export declare class TerminalBindingStore {
47
+ private file;
48
+ private bindings;
49
+ constructor(file?: string);
50
+ private key;
51
+ private load;
52
+ private persist;
53
+ get(filePath: string, plane: TerminalPlane): TerminalBinding | null;
54
+ set(filePath: string, plane: TerminalPlane, scope: TerminalBindingScope, customName?: string): TerminalBinding;
55
+ delete(filePath: string, plane: TerminalPlane): void;
56
+ }
57
+ export declare const terminalBindings: TerminalBindingStore;
@@ -13,8 +13,9 @@ interface TerminalSession {
13
13
  created: number;
14
14
  lastActivity: number;
15
15
  outputBuffer: string;
16
- onData: ((data: string) => void) | null;
17
- onExit: ((code: number) => void) | null;
16
+ activeModes: Set<number>;
17
+ dataListeners: Set<(data: string) => void>;
18
+ exitListeners: Set<(code: number) => void>;
18
19
  }
19
20
  export declare class PtyManager {
20
21
  private sessions;
@@ -48,6 +49,13 @@ export declare class PtyManager {
48
49
  * Get a terminal session by ID
49
50
  */
50
51
  get(id: string): TerminalSession | undefined;
52
+ /**
53
+ * Whether the pty's shell has any child process — i.e. something (an agent
54
+ * CLI, an ssh hop, a user command) is actually running in it. `false` means
55
+ * a bare idle shell with certainty; `null` means unknown (no such pty, or
56
+ * pgrep unavailable) and callers must not conclude anything from it.
57
+ */
58
+ hasLiveChild(id: string): Promise<boolean | null>;
51
59
  /**
52
60
  * Get terminal info by ID
53
61
  */
@@ -80,17 +88,26 @@ export declare class PtyManager {
80
88
  */
81
89
  kill(id: string): boolean;
82
90
  /**
83
- * Set data listener for a terminal (for WebSocket connection)
91
+ * Subscribe to a terminal's output. Returns an unsubscribe function
92
+ * (no-op if the terminal is gone).
84
93
  */
85
- setOnData(id: string, callback: ((data: string) => void) | null): void;
94
+ addDataListener(id: string, callback: (data: string) => void): () => void;
86
95
  /**
87
- * Set exit listener for a terminal
96
+ * Subscribe to a terminal's exit. Returns an unsubscribe function.
88
97
  */
89
- setOnExit(id: string, callback: ((code: number) => void) | null): void;
98
+ addExitListener(id: string, callback: (code: number) => void): () => void;
90
99
  /**
91
100
  * Get buffered output for reconnection
92
101
  */
93
102
  getOutputBuffer(id: string): string;
103
+ /**
104
+ * Sequences that reassert the sticky input modes (see TRACKED_INPUT_MODES) to
105
+ * the authoritative state observed on the full pty stream. Appended after the
106
+ * replay buffer so a reconnect never leaves mouse/focus reporting dangling on
107
+ * at the shell: the 100 KB buffer trim can drop or split the app's own
108
+ * mode-reset, and this puts it back deterministically.
109
+ */
110
+ getModeReset(id: string): string;
94
111
  /**
95
112
  * Kill all terminal sessions (for cleanup)
96
113
  */
@@ -37,9 +37,24 @@ var __importStar = (this && this.__importStar) || (function () {
37
37
  })();
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.ptyManager = exports.PtyManager = void 0;
40
+ const path = __importStar(require("path"));
41
+ const fs_1 = require("fs");
42
+ const child_process_1 = require("child_process");
40
43
  const pty = __importStar(require("@homebridge/node-pty-prebuilt-multiarch"));
41
44
  const uuid_1 = require("uuid");
42
45
  const types_1 = require("./types");
46
+ // Sticky DEC private modes that turn mouse moves / focus changes into garbage
47
+ // *input* when left enabled at a shell prompt (the classic "reconnected terminal
48
+ // spews <b;x;yM sequences"). Tracked from the raw pty stream so a reconnect can
49
+ // restore the AUTHORITATIVE state even when the enabling/disabling sequence has
50
+ // scrolled out of — or been split by — the 100 KB buffer trim. 1000 click /
51
+ // 1002 button-drag / 1003 any-motion mouse · 1004 focus events · 1006 SGR
52
+ // encoding. (Bracketed paste 2004 is intentionally excluded — the shell prompt
53
+ // toggles it itself, so forcing it would fight the shell.)
54
+ const TRACKED_INPUT_MODES = [1000, 1002, 1003, 1004, 1006];
55
+ // DECSET (…h) / DECRST (…l); params may be ;-separated, e.g. \x1b[?1000;1006h
56
+ // eslint-disable-next-line no-control-regex
57
+ const DEC_PRIVATE_MODE_RE = /\x1b\[\?([0-9;]+)([hl])/g;
43
58
  class PtyManager {
44
59
  sessions = new Map();
45
60
  /**
@@ -76,8 +91,20 @@ class PtyManager {
76
91
  const cwd = options?.cwd || process.cwd();
77
92
  const cols = options?.cols || 80;
78
93
  const rows = options?.rows || 24;
79
- // Spawn PTY process
80
- const ptyProcess = pty.spawn(shell, [], {
94
+ // Make the `nebula` agent CLI resolvable in every Nebula terminal, and
95
+ // pre-point it at this server. The CLI is a thin layer over the same
96
+ // client the MCP uses (packages/mcp) — agents in the built-in terminal
97
+ // can drive notebooks without any MCP registration.
98
+ const nebulaCliBinDir = path.resolve(__dirname, '..', '..', '..', 'packages', 'mcp', 'bin');
99
+ // Spawn as a LOGIN shell (-l): Nebula terminals must see the same
100
+ // environment as the user's ssh sessions. Non-login shells skip
101
+ // .zprofile/.bash_profile, so env set there (agent auth tokens like
102
+ // CLAUDE_CODE_OAUTH_TOKEN, module inits, PATH) silently vanished from
103
+ // server-side agent launches — Claude then fell back to its stored OAuth
104
+ // session and demanded /login whenever that had expired. Windows shells
105
+ // don't take -l; posix only.
106
+ const shellArgs = process.platform === 'win32' ? [] : ['-l'];
107
+ const ptyProcess = pty.spawn(shell, shellArgs, {
81
108
  name: 'xterm-256color',
82
109
  cols,
83
110
  rows,
@@ -86,6 +113,8 @@ class PtyManager {
86
113
  ...process.env,
87
114
  TERM: 'xterm-256color',
88
115
  COLORTERM: 'truecolor',
116
+ NEBULA_URL: process.env.NEBULA_URL ?? `http://localhost:${process.env.PORT || 3000}`,
117
+ PATH: `${nebulaCliBinDir}:${process.env.PATH ?? ''}`,
89
118
  },
90
119
  });
91
120
  const session = {
@@ -98,27 +127,50 @@ class PtyManager {
98
127
  created: Date.now(),
99
128
  lastActivity: Date.now(),
100
129
  outputBuffer: '',
101
- onData: null,
102
- onExit: null,
130
+ activeModes: new Set(),
131
+ dataListeners: new Set(),
132
+ exitListeners: new Set(),
103
133
  };
104
134
  // Handle PTY output
105
135
  ptyProcess.onData((data) => {
106
136
  session.lastActivity = Date.now();
107
137
  // Append to output buffer for reconnection
108
138
  session.outputBuffer += data;
139
+ // Track sticky input-mode transitions from the RAW stream (before the
140
+ // trim below can drop or split them) so replay can reassert the true state.
141
+ DEC_PRIVATE_MODE_RE.lastIndex = 0;
142
+ let modeMatch;
143
+ while ((modeMatch = DEC_PRIVATE_MODE_RE.exec(data)) !== null) {
144
+ const enable = modeMatch[2] === 'h';
145
+ for (const p of modeMatch[1].split(';')) {
146
+ const n = Number(p);
147
+ if (TRACKED_INPUT_MODES.includes(n)) {
148
+ if (enable)
149
+ session.activeModes.add(n);
150
+ else
151
+ session.activeModes.delete(n);
152
+ }
153
+ }
154
+ }
109
155
  // Trim buffer if too large
110
156
  if (session.outputBuffer.length > types_1.OUTPUT_BUFFER_MAX_SIZE) {
111
157
  session.outputBuffer = session.outputBuffer.slice(-types_1.OUTPUT_BUFFER_TRIM_SIZE);
112
158
  }
113
- // Forward to listener if connected
114
- if (session.onData) {
115
- session.onData(data);
159
+ // Forward to all listeners (websocket clients, watchers)
160
+ for (const cb of session.dataListeners) {
161
+ try {
162
+ cb(data);
163
+ }
164
+ catch { /* one bad listener must not break the rest */ }
116
165
  }
117
166
  });
118
167
  // Handle PTY exit
119
168
  ptyProcess.onExit(({ exitCode }) => {
120
- if (session.onExit) {
121
- session.onExit(exitCode);
169
+ for (const cb of session.exitListeners) {
170
+ try {
171
+ cb(exitCode);
172
+ }
173
+ catch { /* see above */ }
122
174
  }
123
175
  this.sessions.delete(id);
124
176
  });
@@ -146,6 +198,36 @@ class PtyManager {
146
198
  get(id) {
147
199
  return this.sessions.get(id);
148
200
  }
201
+ /**
202
+ * Whether the pty's shell has any child process — i.e. something (an agent
203
+ * CLI, an ssh hop, a user command) is actually running in it. `false` means
204
+ * a bare idle shell with certainty; `null` means unknown (no such pty, or
205
+ * pgrep unavailable) and callers must not conclude anything from it.
206
+ */
207
+ async hasLiveChild(id) {
208
+ const session = this.sessions.get(id);
209
+ if (!session)
210
+ return null;
211
+ const pid = session.pty.pid;
212
+ // Fast path (Linux): the kernel lists a process's children directly —
213
+ // an O(1) ~3ms read, vs pgrep's O(all processes) table scan (~50ms on a
214
+ // busy shared login node with 1300+ processes). Absent on macOS and on
215
+ // kernels without CONFIG_PROC_CHILDREN — fall back to pgrep there.
216
+ try {
217
+ const children = await fs_1.promises.readFile(`/proc/${pid}/task/${pid}/children`, 'utf-8');
218
+ return children.trim().length > 0;
219
+ }
220
+ catch { /* not Linux (or file gone with the process) — pgrep decides */ }
221
+ return new Promise((resolve) => {
222
+ (0, child_process_1.execFile)('pgrep', ['-P', String(pid)], (error) => {
223
+ if (!error)
224
+ return resolve(true); // exit 0: at least one child
225
+ // pgrep exits 1 for "no processes matched" — that's a definitive no.
226
+ const code = error.code;
227
+ resolve(code === 1 ? false : null);
228
+ });
229
+ });
230
+ }
149
231
  /**
150
232
  * Get terminal info by ID
151
233
  */
@@ -177,6 +259,12 @@ class PtyManager {
177
259
  const session = this.sessions.get(id);
178
260
  if (!session)
179
261
  return false;
262
+ // Skip no-op resizes: TIOCSWINSZ raises SIGWINCH even when the size is
263
+ // unchanged, forcing a full-screen repaint from any running TUI. On a
264
+ // reconnect several resize messages arrive with the SAME dimensions, and
265
+ // that repaint burst is what stalls input after a refresh.
266
+ if (session.cols === cols && session.rows === rows)
267
+ return true;
180
268
  session.pty.resize(cols, rows);
181
269
  session.cols = cols;
182
270
  session.rows = rows;
@@ -208,22 +296,25 @@ class PtyManager {
208
296
  return true;
209
297
  }
210
298
  /**
211
- * Set data listener for a terminal (for WebSocket connection)
299
+ * Subscribe to a terminal's output. Returns an unsubscribe function
300
+ * (no-op if the terminal is gone).
212
301
  */
213
- setOnData(id, callback) {
302
+ addDataListener(id, callback) {
214
303
  const session = this.sessions.get(id);
215
- if (session) {
216
- session.onData = callback;
217
- }
304
+ if (!session)
305
+ return () => { };
306
+ session.dataListeners.add(callback);
307
+ return () => { this.sessions.get(id)?.dataListeners.delete(callback); };
218
308
  }
219
309
  /**
220
- * Set exit listener for a terminal
310
+ * Subscribe to a terminal's exit. Returns an unsubscribe function.
221
311
  */
222
- setOnExit(id, callback) {
312
+ addExitListener(id, callback) {
223
313
  const session = this.sessions.get(id);
224
- if (session) {
225
- session.onExit = callback;
226
- }
314
+ if (!session)
315
+ return () => { };
316
+ session.exitListeners.add(callback);
317
+ return () => { this.sessions.get(id)?.exitListeners.delete(callback); };
227
318
  }
228
319
  /**
229
320
  * Get buffered output for reconnection
@@ -232,6 +323,34 @@ class PtyManager {
232
323
  const session = this.sessions.get(id);
233
324
  return session?.outputBuffer || '';
234
325
  }
326
+ /**
327
+ * Sequences that reassert the sticky input modes (see TRACKED_INPUT_MODES) to
328
+ * the authoritative state observed on the full pty stream. Appended after the
329
+ * replay buffer so a reconnect never leaves mouse/focus reporting dangling on
330
+ * at the shell: the 100 KB buffer trim can drop or split the app's own
331
+ * mode-reset, and this puts it back deterministically.
332
+ */
333
+ getModeReset(id) {
334
+ const session = this.sessions.get(id);
335
+ if (!session)
336
+ return '';
337
+ // If the pty's FOREGROUND process is a plain shell, no TUI can be holding
338
+ // the sticky input modes — any tracked "on" state is stale (a TUI died
339
+ // without its teardown bytes reaching the pty, e.g. an agent riding a
340
+ // collapsed reverse tunnel, or a SIGKILL). Reasserting it would turn the
341
+ // user's mouse movements into SGR garbage typed at the prompt, so reset
342
+ // everything OFF instead.
343
+ try {
344
+ const fg = (session.pty.process || '').split('/').pop() || '';
345
+ if (/^-?(bash|zsh|sh|dash|fish|tcsh|ksh)$/.test(fg)) {
346
+ session.activeModes.clear();
347
+ }
348
+ }
349
+ catch { /* can't inspect foreground — keep tracked state */ }
350
+ return TRACKED_INPUT_MODES
351
+ .map((n) => `\x1b[?${n}${session.activeModes.has(n) ? 'h' : 'l'}`)
352
+ .join('');
353
+ }
235
354
  /**
236
355
  * Kill all terminal sessions (for cleanup)
237
356
  */