nebula-notebook 0.2.21 → 0.2.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{errorwidget-DLgJz_d8.js → errorwidget-DEs-OXNm.js} +1 -1
- package/dist/assets/{index-CkPIRPJ2.js → index-9VDGsU71.js} +1 -1
- package/dist/assets/{index-B0WDQN9I.css → index-BIAqM8U1.css} +1 -1
- package/dist/assets/{index-BogqIQSN.js → index-CuNblSZD.js} +200 -200
- package/dist/assets/{index-Dw5NWu65.js → index-Xz-JDPjg.js} +1 -1
- package/dist/assets/{index-DeQf1_EU.js → index-jxii65ah.js} +1 -1
- package/dist/assets/{services-shim-XWMjA5Ht.js → services-shim-hfUd4Jza.js} +1 -1
- package/dist/index.html +2 -2
- package/node-server/dist/auth/auth-service.js +2 -10
- package/node-server/dist/auth/setup-qr.d.ts +12 -0
- package/node-server/dist/auth/setup-qr.js +78 -0
- package/node-server/dist/routes/compute.js +14 -0
- package/node-server/dist/routes/notebook.js +12 -0
- package/node-server/dist/scheduler/allocation-service.d.ts +1 -0
- package/node-server/dist/scheduler/allocation-service.js +3 -0
- package/node-server/dist/scheduler/arm-setup-prompt.d.ts +27 -0
- package/node-server/dist/scheduler/arm-setup-prompt.js +110 -0
- package/node-server/dist/scripts/show-auth-qr.js +21 -44
- package/node-server/dist/terminal/driving-context.d.ts +27 -0
- package/node-server/dist/terminal/driving-context.js +63 -0
- package/node-server/dist/terminal/server.js +19 -0
- package/package.json +1 -1
|
@@ -42,6 +42,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
42
42
|
exports.default = computeRoutes;
|
|
43
43
|
const os = __importStar(require("os"));
|
|
44
44
|
const allocation_service_1 = require("../scheduler/allocation-service");
|
|
45
|
+
const arm_setup_prompt_1 = require("../scheduler/arm-setup-prompt");
|
|
45
46
|
function currentUser() {
|
|
46
47
|
return process.env.USER || process.env.LOGNAME || os.userInfo().username;
|
|
47
48
|
}
|
|
@@ -105,6 +106,19 @@ async function computeRoutes(fastify) {
|
|
|
105
106
|
const estimate = await sched.estimateStart(spec);
|
|
106
107
|
return reply.send(estimate);
|
|
107
108
|
});
|
|
109
|
+
/**
|
|
110
|
+
* Agent-executable ARM setup: this installation's facts + a rendered prompt
|
|
111
|
+
* the user pastes to an agent. configured=true means an arm64 runtime is
|
|
112
|
+
* already wired in (the UI then shows nothing).
|
|
113
|
+
*/
|
|
114
|
+
fastify.get('/compute/arm-setup', async (_req, reply) => {
|
|
115
|
+
const sched = allocation_service_1.allocationService.getScheduler();
|
|
116
|
+
const ctx = allocation_service_1.allocationService.getLaunchContext();
|
|
117
|
+
if (!sched || !ctx)
|
|
118
|
+
return reply.code(400).send({ error: 'scheduler not available' });
|
|
119
|
+
const facts = await (0, arm_setup_prompt_1.gatherArmSetupFacts)(sched, ctx);
|
|
120
|
+
return reply.send({ configured: facts.configured, prompt: (0, arm_setup_prompt_1.buildArmSetupPrompt)(facts), facts });
|
|
121
|
+
});
|
|
108
122
|
/** List current allocations (pending / running / active / ended). */
|
|
109
123
|
fastify.get('/compute/allocations', async (_req, reply) => {
|
|
110
124
|
return reply.send({ allocations: allocation_service_1.allocationService.list() });
|
|
@@ -8,6 +8,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.headlessHandler = exports.operationRouter = exports.fsService = void 0;
|
|
10
10
|
exports.default = notebookRoutes;
|
|
11
|
+
const driving_context_1 = require("../terminal/driving-context");
|
|
11
12
|
const fs_service_1 = require("../fs/fs-service");
|
|
12
13
|
Object.defineProperty(exports, "fsService", { enumerable: true, get: function () { return fs_service_1.fsService; } });
|
|
13
14
|
const operation_router_1 = require("../notebook/operation-router");
|
|
@@ -314,6 +315,17 @@ async function notebookRoutes(fastify) {
|
|
|
314
315
|
if (result.code === 'sealed_read_only') {
|
|
315
316
|
return reply.code(403).send(result);
|
|
316
317
|
}
|
|
318
|
+
// Drift notice: when the calling agent declared its terminal (env var →
|
|
319
|
+
// header) and the user is viewing a DIFFERENT notebook than this
|
|
320
|
+
// operation targets, say so once per switch — in the one channel the
|
|
321
|
+
// agent cannot miss. Never blocks the operation.
|
|
322
|
+
const agentTerminal = request.headers['x-nebula-agent-terminal'];
|
|
323
|
+
const opPath = typeof operation.notebookPath === 'string' ? operation.notebookPath : '';
|
|
324
|
+
if (typeof agentTerminal === 'string' && agentTerminal && opPath && result.success !== false) {
|
|
325
|
+
const notice = driving_context_1.drivingContext.driftNotice(agentTerminal, opPath);
|
|
326
|
+
if (notice)
|
|
327
|
+
result.notice = notice;
|
|
328
|
+
}
|
|
317
329
|
return reply.send(result);
|
|
318
330
|
}
|
|
319
331
|
catch (err) {
|
|
@@ -52,6 +52,7 @@ export declare class AllocationService {
|
|
|
52
52
|
private scheduleNextPoll;
|
|
53
53
|
isEnabled(): boolean;
|
|
54
54
|
getScheduler(): Scheduler | null;
|
|
55
|
+
getLaunchContext(): LaunchContext | null;
|
|
55
56
|
list(): Allocation[];
|
|
56
57
|
get(id: string): Allocation | undefined;
|
|
57
58
|
create(spec: JobSpec): Promise<Allocation>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-executable ARM setup prompt.
|
|
3
|
+
*
|
|
4
|
+
* Setting up aarch64 compute support is a one-time, judgment-heavy HPC task
|
|
5
|
+
* (storage policy, partition choice, whether compute nodes have internet) —
|
|
6
|
+
* wrong shape for one-click automation, right shape for an agent. The server
|
|
7
|
+
* contributes what it KNOWS (its own paths, node version, detected aarch64
|
|
8
|
+
* partitions + required QOS, the env var names); the prompt tells the agent
|
|
9
|
+
* to verify every step and finish with an end-to-end allocation proof.
|
|
10
|
+
*/
|
|
11
|
+
import type { Scheduler } from './types';
|
|
12
|
+
import type { LaunchContext } from './job-template';
|
|
13
|
+
export interface ArmSetupFacts {
|
|
14
|
+
configured: boolean;
|
|
15
|
+
serverArch: string;
|
|
16
|
+
nodeVersion: string;
|
|
17
|
+
nodeBin: string;
|
|
18
|
+
installDir: string;
|
|
19
|
+
suggestedArmDir: string;
|
|
20
|
+
suggestedArmNodeDir: string;
|
|
21
|
+
armPartitions: {
|
|
22
|
+
name: string;
|
|
23
|
+
qos: string[] | null;
|
|
24
|
+
}[];
|
|
25
|
+
}
|
|
26
|
+
export declare function gatherArmSetupFacts(scheduler: Scheduler, ctx: LaunchContext, serverArch?: string, nodeVersion?: string): Promise<ArmSetupFacts>;
|
|
27
|
+
export declare function buildArmSetupPrompt(f: ArmSetupFacts): string;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Agent-executable ARM setup prompt.
|
|
4
|
+
*
|
|
5
|
+
* Setting up aarch64 compute support is a one-time, judgment-heavy HPC task
|
|
6
|
+
* (storage policy, partition choice, whether compute nodes have internet) —
|
|
7
|
+
* wrong shape for one-click automation, right shape for an agent. The server
|
|
8
|
+
* contributes what it KNOWS (its own paths, node version, detected aarch64
|
|
9
|
+
* partitions + required QOS, the env var names); the prompt tells the agent
|
|
10
|
+
* to verify every step and finish with an end-to-end allocation proof.
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.gatherArmSetupFacts = gatherArmSetupFacts;
|
|
47
|
+
exports.buildArmSetupPrompt = buildArmSetupPrompt;
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const arch_1 = require("./arch");
|
|
50
|
+
async function gatherArmSetupFacts(scheduler, ctx, serverArch = process.arch, nodeVersion = process.version) {
|
|
51
|
+
const installDir = path.dirname(ctx.cwd); // ctx.cwd is <checkout>/node-server
|
|
52
|
+
const armPartitions = [];
|
|
53
|
+
try {
|
|
54
|
+
const load = await scheduler.load();
|
|
55
|
+
for (const p of load.partitions) {
|
|
56
|
+
// Homogeneous partitions whose arch differs from the server's. Mixed or
|
|
57
|
+
// unknown partitions are skipped — no safe pick exists (see arch.ts).
|
|
58
|
+
if (p.archs?.length === 1 && (0, arch_1.normalizeArch)(p.archs[0]) !== (0, arch_1.normalizeArch)(serverArch)) {
|
|
59
|
+
let qos = null;
|
|
60
|
+
try {
|
|
61
|
+
qos = await scheduler.allowedQos(p.name);
|
|
62
|
+
}
|
|
63
|
+
catch { /* optional */ }
|
|
64
|
+
armPartitions.push({ name: p.name, qos });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch { /* scheduler unavailable — prompt degrades to placeholders */ }
|
|
69
|
+
return {
|
|
70
|
+
configured: Boolean(ctx.archOverrides?.arm64),
|
|
71
|
+
serverArch,
|
|
72
|
+
nodeVersion,
|
|
73
|
+
nodeBin: ctx.nodeBin,
|
|
74
|
+
installDir,
|
|
75
|
+
suggestedArmDir: `${installDir}-arm64`,
|
|
76
|
+
// <prefix>/bin/node -> sibling <prefix>-arm64
|
|
77
|
+
suggestedArmNodeDir: `${path.dirname(path.dirname(ctx.nodeBin))}-arm64`,
|
|
78
|
+
armPartitions,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function buildArmSetupPrompt(f) {
|
|
82
|
+
const part = f.armPartitions[0];
|
|
83
|
+
const partName = part?.name ?? '<arm-partition>';
|
|
84
|
+
const qosFlag = part?.qos?.length ? ` --qos=${part.qos[0]}` : '';
|
|
85
|
+
const partitionList = f.armPartitions.length
|
|
86
|
+
? f.armPartitions.map((p) => `${p.name}${p.qos?.length ? ` (requires --qos, one of: ${p.qos.join(', ')})` : ''}`).join('; ')
|
|
87
|
+
: '<arm-partition> (none auto-detected — ask the user, or check `sinfo` + `scontrol show node` Arch= fields)';
|
|
88
|
+
const armNodeBin = `${f.suggestedArmNodeDir}/bin/node`;
|
|
89
|
+
return `You are enabling aarch64 (ARM) compute allocations for an existing Nebula Notebook server on an HPC cluster. Nebula re-launches its own install on compute nodes, so ARM partitions need an arm64 Node.js runtime and an arm64-installed copy of the checkout, both on shared storage. Work on the SERVER (where the Nebula server process runs). VERIFY each step before the next; when a step fails, diagnose it — do not skip. Ask the user only for genuinely missing facts (e.g. storage policy).
|
|
90
|
+
|
|
91
|
+
FACTS about this installation (real values, verified by the server):
|
|
92
|
+
- Server CPU arch: ${f.serverArch}; Node ${f.nodeVersion} at ${f.nodeBin}
|
|
93
|
+
- Nebula checkout: ${f.installDir}
|
|
94
|
+
- ARM partitions detected: ${partitionList}
|
|
95
|
+
- Env vars the server reads at startup: NEBULA_ARM64_NODE_BIN, NEBULA_ARM64_DIR
|
|
96
|
+
|
|
97
|
+
STEPS:
|
|
98
|
+
1. Confirm ${f.installDir} is on storage the compute nodes share (e.g. \`df -h\` / ask the user). Plan sibling paths: ${f.suggestedArmNodeDir} and ${f.suggestedArmDir} (or wherever policy dictates — same filesystem).
|
|
99
|
+
2. arm64 Node runtime: download https://nodejs.org/dist/${f.nodeVersion}/node-${f.nodeVersion}-linux-arm64.tar.xz, extract so ${armNodeBin} exists. Verify with \`file ${armNodeBin}\` (must say aarch64/ARM). Do NOT try to run it on this ${f.serverArch} machine.
|
|
100
|
+
3. Second checkout: \`git clone ${f.installDir} ${f.suggestedArmDir}\`, then set its origin to the same remote as the source checkout.
|
|
101
|
+
4. Install server deps ON an ARM node — native modules must be arm64 builds:
|
|
102
|
+
srun -p ${partName}${qosFlag} --time=30 --mem=8G -c4 bash -c 'export PATH=${f.suggestedArmNodeDir}/bin:$PATH && cd ${f.suggestedArmDir}/node-server && npm install --no-audit --no-fund && node -e "require(\"zeromq\"); require(\"better-sqlite3\"); require(\"@homebridge/node-pty-prebuilt-multiarch\"); console.log(\"native modules ok\")"'
|
|
103
|
+
The final line MUST print "native modules ok". If compute nodes have no internet, tell the user — an npm mirror or admin help is needed; do not fake arm64 installs from this ${f.serverArch} machine.
|
|
104
|
+
5. Build the workspace dependency (pure TypeScript, any arch works): \`cd ${f.suggestedArmDir}/packages/autocomplete && npm install && npm run build\` and verify dist/index.js exists.
|
|
105
|
+
6. Configure the server: set NEBULA_ARM64_NODE_BIN=${armNodeBin} and NEBULA_ARM64_DIR=${f.suggestedArmDir} in the environment the Nebula server launches with (its launcher script / service unit — find how it is started), then restart it and verify /api/health answers.
|
|
106
|
+
7. END-TO-END PROOF: \`nebula compute alloc --partition ${partName}${qosFlag ? ` --qos ${part.qos[0]}` : ''} --cpus 1 --mem 4 --walltime 30 --idle-timeout 10 --wait\` must reach state: active. Cancel it afterwards (\`nebula compute cancel <id>\`). If it fails, read the allocation's reason — it carries the job log tail.
|
|
107
|
+
8. Kernels on ARM nodes also need an aarch64 Python with ipykernel (x86_64 conda envs will not run there). Create one on shared storage via srun on the ARM node (e.g. \`/usr/bin/python3 -m venv <shared>/envs/arm64-py && <shared>/envs/arm64-py/bin/pip install ipykernel\`), verify the import, and tell the user to pick it via "Enter interpreter path" (env:<path>) for notebooks bound to ARM allocations.
|
|
108
|
+
|
|
109
|
+
Report at the end: what was installed where, the allocation id that went active, and anything you had to decide or could not verify.`;
|
|
110
|
+
}
|
|
@@ -1,37 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
4
|
};
|
|
@@ -40,7 +7,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
40
7
|
const os_1 = __importDefault(require("os"));
|
|
41
8
|
const path_1 = __importDefault(require("path"));
|
|
42
9
|
const otplib_1 = require("otplib");
|
|
43
|
-
const
|
|
10
|
+
const setup_qr_1 = require("../auth/setup-qr");
|
|
44
11
|
const AUTH_CONFIG_FILE = path_1.default.join(os_1.default.homedir(), '.nebula', 'auth.json');
|
|
45
12
|
const ISSUER = 'NebulaNotebook';
|
|
46
13
|
const ACCOUNT_NAME = 'local';
|
|
@@ -62,20 +29,30 @@ function readAuthConfig() {
|
|
|
62
29
|
}
|
|
63
30
|
}
|
|
64
31
|
function main() {
|
|
32
|
+
const args = process.argv.slice(2);
|
|
33
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
34
|
+
console.log(`show-auth-qr — reprint the 2FA setup QR for this machine's Nebula
|
|
35
|
+
|
|
36
|
+
npm run auth:qr # compact QR (Unicode half-blocks)
|
|
37
|
+
npm run auth:qr -- --big # font-independent QR (ANSI color blocks) —
|
|
38
|
+
# use when the compact one renders as dashes
|
|
39
|
+
npm run auth:qr -- --url # print only the otpauth:// URL (no QR)`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
65
42
|
const config = readAuthConfig();
|
|
66
43
|
const secret = typeof config.totpSecret === 'string' ? config.totpSecret.trim() : '';
|
|
67
44
|
if (!secret) {
|
|
68
|
-
fail(`[Auth] Missing
|
|
45
|
+
fail(`[Auth] Missing "totpSecret" in ${AUTH_CONFIG_FILE}`);
|
|
69
46
|
}
|
|
70
47
|
const otpAuthUrl = otplib_1.authenticator.keyuri(ACCOUNT_NAME, ISSUER, secret);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
console.log(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
48
|
+
if (args.includes('--url')) {
|
|
49
|
+
console.log(otpAuthUrl);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const mode = args.includes('--big') ? 'big' : 'small';
|
|
53
|
+
console.log((0, setup_qr_1.buildSetupInstructions)(secret, otpAuthUrl, (0, setup_qr_1.renderQr)(otpAuthUrl, mode)));
|
|
54
|
+
if (mode === 'small') {
|
|
55
|
+
console.log('Garbled? Re-run with --big for a font-independent QR.');
|
|
56
|
+
}
|
|
80
57
|
}
|
|
81
58
|
main();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Driving context — which notebook the user is currently viewing, per agent
|
|
3
|
+
* terminal. Reported by the browser (notebook switch, tab focus, launch);
|
|
4
|
+
* consumed by agents via `nebula context` (pull) and via a drift notice the
|
|
5
|
+
* operation route attaches to results (push, at act time — the one channel
|
|
6
|
+
* an agent cannot miss, so it never has to poll).
|
|
7
|
+
*
|
|
8
|
+
* In-memory by design: the browser re-reports on every focus/switch, so
|
|
9
|
+
* state heals within seconds of a server restart.
|
|
10
|
+
*/
|
|
11
|
+
declare class DrivingContext {
|
|
12
|
+
private byTerminal;
|
|
13
|
+
setDriving(terminalId: string, notebook: string): void;
|
|
14
|
+
getDriving(terminalId: string): {
|
|
15
|
+
notebook: string;
|
|
16
|
+
at: number;
|
|
17
|
+
} | null;
|
|
18
|
+
/**
|
|
19
|
+
* One-line drift notice when `targetNotebook` differs from what the user is
|
|
20
|
+
* viewing — once per switch. Operations on the driving notebook re-arm the
|
|
21
|
+
* notice (so a later switch away notifies again).
|
|
22
|
+
*/
|
|
23
|
+
driftNotice(terminalId: string, targetNotebook: string): string | null;
|
|
24
|
+
clear(): void;
|
|
25
|
+
}
|
|
26
|
+
export declare const drivingContext: DrivingContext;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Driving context — which notebook the user is currently viewing, per agent
|
|
4
|
+
* terminal. Reported by the browser (notebook switch, tab focus, launch);
|
|
5
|
+
* consumed by agents via `nebula context` (pull) and via a drift notice the
|
|
6
|
+
* operation route attaches to results (push, at act time — the one channel
|
|
7
|
+
* an agent cannot miss, so it never has to poll).
|
|
8
|
+
*
|
|
9
|
+
* In-memory by design: the browser re-reports on every focus/switch, so
|
|
10
|
+
* state heals within seconds of a server restart.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.drivingContext = void 0;
|
|
14
|
+
function ago(ms) {
|
|
15
|
+
const s = Math.max(1, Math.round(ms / 1000));
|
|
16
|
+
if (s < 90)
|
|
17
|
+
return `${s}s ago`;
|
|
18
|
+
const m = Math.round(s / 60);
|
|
19
|
+
return `${m}m ago`;
|
|
20
|
+
}
|
|
21
|
+
class DrivingContext {
|
|
22
|
+
byTerminal = new Map();
|
|
23
|
+
setDriving(terminalId, notebook) {
|
|
24
|
+
if (!terminalId?.trim() || !notebook?.trim())
|
|
25
|
+
return;
|
|
26
|
+
const existing = this.byTerminal.get(terminalId);
|
|
27
|
+
if (existing && existing.notebook === notebook)
|
|
28
|
+
return; // focus ping, not a switch
|
|
29
|
+
this.byTerminal.set(terminalId, {
|
|
30
|
+
notebook,
|
|
31
|
+
at: Date.now(),
|
|
32
|
+
notifiedFor: existing ? null : null,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
getDriving(terminalId) {
|
|
36
|
+
const d = this.byTerminal.get(terminalId);
|
|
37
|
+
return d ? { notebook: d.notebook, at: d.at } : null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* One-line drift notice when `targetNotebook` differs from what the user is
|
|
41
|
+
* viewing — once per switch. Operations on the driving notebook re-arm the
|
|
42
|
+
* notice (so a later switch away notifies again).
|
|
43
|
+
*/
|
|
44
|
+
driftNotice(terminalId, targetNotebook) {
|
|
45
|
+
const d = this.byTerminal.get(terminalId);
|
|
46
|
+
if (!d || !targetNotebook)
|
|
47
|
+
return null;
|
|
48
|
+
if (d.notebook === targetNotebook) {
|
|
49
|
+
d.notifiedFor = null; // aligned — future switches notify again
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (d.notifiedFor === d.notebook)
|
|
53
|
+
return null; // already told about this switch
|
|
54
|
+
d.notifiedFor = d.notebook;
|
|
55
|
+
return (`note: the user is now viewing ${d.notebook} (switched ${ago(Date.now() - d.at)}); ` +
|
|
56
|
+
`this operation targeted ${targetNotebook}. Keep going if your instruction named this ` +
|
|
57
|
+
`notebook explicitly; if it said "this notebook/cell" without a path, it likely means ${d.notebook} — confirm before editing.`);
|
|
58
|
+
}
|
|
59
|
+
clear() {
|
|
60
|
+
this.byTerminal.clear();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.drivingContext = new DrivingContext();
|
|
@@ -48,6 +48,7 @@ const agent_registry_1 = require("./agent-registry");
|
|
|
48
48
|
const binding_store_1 = require("./binding-store");
|
|
49
49
|
const remote_agent_config_1 = require("./remote-agent-config");
|
|
50
50
|
const remote_agent_identity_1 = require("./remote-agent-identity");
|
|
51
|
+
const driving_context_1 = require("./driving-context");
|
|
51
52
|
const fs_service_1 = require("../fs/fs-service");
|
|
52
53
|
// Get configured root directory for terminals
|
|
53
54
|
function getDefaultCwd() {
|
|
@@ -131,6 +132,24 @@ async function setupTerminalRoutes(fastify) {
|
|
|
131
132
|
? { pushed: true, path: `~/${remote_agent_identity_1.REMOTE_TOKEN_PATH}` }
|
|
132
133
|
: { pushed: false, reason: 'your machine did not accept the token over the reverse tunnel (tunnel down or key not authorized)' });
|
|
133
134
|
});
|
|
135
|
+
// Driving context: which notebook the user is viewing, per agent terminal.
|
|
136
|
+
// Browser reports on notebook switch / tab focus / launch; agents read it
|
|
137
|
+
// back with `nebula context`. In-memory — the browser re-reports on focus.
|
|
138
|
+
fastify.put('/api/terminals/driving', async (request, reply) => {
|
|
139
|
+
const body = request.body || {};
|
|
140
|
+
if (!body.terminal?.trim() || !body.notebook?.trim()) {
|
|
141
|
+
return reply.code(400).send({ error: 'terminal and notebook are required' });
|
|
142
|
+
}
|
|
143
|
+
driving_context_1.drivingContext.setDriving(body.terminal.trim(), body.notebook.trim());
|
|
144
|
+
return reply.send({ ok: true });
|
|
145
|
+
});
|
|
146
|
+
fastify.get('/api/terminals/driving', async (request, reply) => {
|
|
147
|
+
const terminal = String(request.query?.terminal || '').trim();
|
|
148
|
+
if (!terminal)
|
|
149
|
+
return reply.code(400).send({ error: 'terminal is required' });
|
|
150
|
+
const d = driving_context_1.drivingContext.getDriving(terminal);
|
|
151
|
+
return reply.send(d ?? { notebook: null });
|
|
152
|
+
});
|
|
134
153
|
fastify.put('/api/terminals/agent-config', async (request, reply) => {
|
|
135
154
|
const body = request.body || {};
|
|
136
155
|
// Only fields actually present are touched (undefined keeps, null erases).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nebula-notebook",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.23",
|
|
4
4
|
"description": "AI-native notebook computing environment — real Jupyter kernels, real filesystem, built to be driven by agents (Claude Code / Codex) via MCP",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|