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
|
@@ -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;
|
|
Binary file
|
|
@@ -13,8 +13,9 @@ interface TerminalSession {
|
|
|
13
13
|
created: number;
|
|
14
14
|
lastActivity: number;
|
|
15
15
|
outputBuffer: string;
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
*
|
|
91
|
+
* Subscribe to a terminal's output. Returns an unsubscribe function
|
|
92
|
+
* (no-op if the terminal is gone).
|
|
84
93
|
*/
|
|
85
|
-
|
|
94
|
+
addDataListener(id: string, callback: (data: string) => void): () => void;
|
|
86
95
|
/**
|
|
87
|
-
*
|
|
96
|
+
* Subscribe to a terminal's exit. Returns an unsubscribe function.
|
|
88
97
|
*/
|
|
89
|
-
|
|
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
|
-
//
|
|
80
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
*
|
|
299
|
+
* Subscribe to a terminal's output. Returns an unsubscribe function
|
|
300
|
+
* (no-op if the terminal is gone).
|
|
212
301
|
*/
|
|
213
|
-
|
|
302
|
+
addDataListener(id, callback) {
|
|
214
303
|
const session = this.sessions.get(id);
|
|
215
|
-
if (session)
|
|
216
|
-
|
|
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
|
-
*
|
|
310
|
+
* Subscribe to a terminal's exit. Returns an unsubscribe function.
|
|
221
311
|
*/
|
|
222
|
-
|
|
312
|
+
addExitListener(id, callback) {
|
|
223
313
|
const session = this.sessions.get(id);
|
|
224
|
-
if (session)
|
|
225
|
-
|
|
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
|
*/
|