@filsilva/helios-cli 0.10.0

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 (39) hide show
  1. package/README.md +171 -0
  2. package/bin/helios.js +34 -0
  3. package/dist/client/assets/HeliosSessionWorker.browser-BYYjDIKH.js +3 -0
  4. package/dist/client/assets/HeliosSessionWorker.browser-BYYjDIKH.js.map +1 -0
  5. package/dist/client/assets/d3force3dWorker-BKANL9of.js +2 -0
  6. package/dist/client/assets/d3force3dWorker-BKANL9of.js.map +1 -0
  7. package/dist/client/assets/index-CP7mSmLx.js +9530 -0
  8. package/dist/client/assets/index-CP7mSmLx.js.map +1 -0
  9. package/dist/client/assets/layoutWorker-Lc8iIdmf.js +2 -0
  10. package/dist/client/assets/layoutWorker-Lc8iIdmf.js.map +1 -0
  11. package/dist/client/index.html +27 -0
  12. package/package.json +40 -0
  13. package/skills/helios-cli/SKILL.md +118 -0
  14. package/skills/helios-cli/references/behaviors.md +47 -0
  15. package/skills/helios-cli/references/layouts.md +77 -0
  16. package/skills/helios-cli/references/mappers.md +119 -0
  17. package/skills/helios-cli/references/metrics.md +83 -0
  18. package/skills/helios-cli/references/networks.md +53 -0
  19. package/skills/helios-cli/references/persistence.md +136 -0
  20. package/skills/helios-cli/references/positions.md +63 -0
  21. package/skills/helios-cli/references/rendering-export.md +56 -0
  22. package/skills/helios-cli/references/rpc-methods.md +83 -0
  23. package/src/cli.js +488 -0
  24. package/src/client/index.html +27 -0
  25. package/src/client/main.js +2210 -0
  26. package/src/daemon/SessionDaemon.js +1065 -0
  27. package/src/daemon/entry.js +36 -0
  28. package/src/protocol/jsonl.js +88 -0
  29. package/src/shared/cliConfig.js +52 -0
  30. package/src/shared/fileSessionStore.js +202 -0
  31. package/src/shared/fs.js +59 -0
  32. package/src/shared/networkFormats.js +55 -0
  33. package/src/shared/networkInspect.js +81 -0
  34. package/src/shared/paths.js +43 -0
  35. package/src/shared/sessionClient.js +88 -0
  36. package/src/shared/sessionId.js +5 -0
  37. package/src/shared/sessionRegistry.js +53 -0
  38. package/src/shared/sessionSurfaces.js +199 -0
  39. package/vite.config.js +47 -0
@@ -0,0 +1,2210 @@
1
+ import HeliosNetwork, { AttributeType } from 'helios-network';
2
+ import { Helios, HeliosUI, EVENTS, Mapper } from 'helios-web';
3
+
4
+ function wsUrlForCurrentLocation() {
5
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
6
+ return `${protocol}//${window.location.host}/bridge`;
7
+ }
8
+
9
+ function cliPersistenceId(sessionId) {
10
+ return String(sessionId ?? 'unknown');
11
+ }
12
+
13
+ function isDesktopRuntime(config = {}) {
14
+ const runtime = String(config.runtime ?? new URLSearchParams(window.location.search).get('runtime') ?? '').toLowerCase();
15
+ return runtime === 'desktop' || runtime === 'mac';
16
+ }
17
+
18
+ function publishRuntimeState(helios) {
19
+ window.__HELIOS_CLI_RUNTIME__ = {
20
+ renderer: helios.renderer?.device?.type ?? null,
21
+ mode: helios.mode(),
22
+ ready: true,
23
+ };
24
+ }
25
+
26
+ function normalizeLayoutKey(value, fallback = 'gpu-force') {
27
+ const normalized = String(value ?? fallback).trim().toLowerCase();
28
+ if (normalized === 'static' || normalized === 'none') return 'static';
29
+ if (normalized === 'd3force3d' || normalized === 'd3-force-3d') return 'd3force3d';
30
+ if (normalized === 'worker:jitter' || normalized === 'jitter') return 'worker:jitter';
31
+ if (normalized === 'worker:force3d' || normalized === 'worker' || normalized === 'force3d') return 'worker:force3d';
32
+ return 'gpu-force';
33
+ }
34
+
35
+ function readNetworkScalarAttribute(network, name) {
36
+ const info = network?.getNetworkAttributeInfo?.(name) ?? null;
37
+ if (!info || Number(info.dimension ?? 1) !== 1) return undefined;
38
+ if (Number(info.type) === AttributeType.String || Number(info.type) === AttributeType.Category) {
39
+ return network.getNetworkStringAttribute?.(name);
40
+ }
41
+ let value;
42
+ const read = () => {
43
+ value = network.getNetworkAttributeBuffer?.(name)?.view?.[0];
44
+ };
45
+ if (typeof network.withBufferAccess === 'function') network.withBufferAccess(read);
46
+ else read();
47
+ return value;
48
+ }
49
+
50
+ function networkHasUmapForceMetadata(network) {
51
+ const value = readNetworkScalarAttribute(network, 'umap');
52
+ if (typeof value === 'string') {
53
+ return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());
54
+ }
55
+ return Number(value) !== 0 && Number.isFinite(Number(value));
56
+ }
57
+
58
+ function resolveRendererPreference(value) {
59
+ const normalized = String(value ?? '').trim().toLowerCase();
60
+ if (normalized === 'webgl' || normalized === 'webgpu') return normalized;
61
+ return null;
62
+ }
63
+
64
+ function cloneJsonSafe(value) {
65
+ if (ArrayBuffer.isView(value)) return Array.from(value);
66
+ if (value instanceof Set) return Array.from(value).map((entry) => cloneJsonSafe(entry));
67
+ if (value instanceof Map) {
68
+ const out = {};
69
+ for (const [key, entry] of value.entries()) out[key] = cloneJsonSafe(entry);
70
+ return out;
71
+ }
72
+ if (typeof value === 'bigint') return value.toString();
73
+ if (Array.isArray(value)) return value.map((entry) => cloneJsonSafe(entry));
74
+ if (!value || typeof value !== 'object') return value;
75
+ const out = {};
76
+ for (const [key, entry] of Object.entries(value)) {
77
+ if (typeof entry === 'function') continue;
78
+ if (key.startsWith('__')) continue;
79
+ out[key] = cloneJsonSafe(entry);
80
+ }
81
+ return out;
82
+ }
83
+
84
+ function serializeChannelConfig(config) {
85
+ return cloneJsonSafe(config ?? null);
86
+ }
87
+
88
+ function serializeMapperCollection(collection) {
89
+ const mapper = collection?.defaultMapper ?? null;
90
+ const channels = {};
91
+ if (!mapper?.channels) return channels;
92
+ for (const [name, config] of mapper.channels.entries()) {
93
+ channels[name] = serializeChannelConfig(config);
94
+ }
95
+ return channels;
96
+ }
97
+
98
+ function serializeLayoutBinding(binding) {
99
+ return {
100
+ key: binding?.key ?? null,
101
+ label: binding?.label ?? null,
102
+ type: binding?.type ?? null,
103
+ value: typeof binding?.get === 'function' ? cloneJsonSafe(binding.get()) : null,
104
+ min: binding?.min ?? null,
105
+ max: binding?.max ?? null,
106
+ options: Array.isArray(binding?.options) ? cloneJsonSafe(binding.options) : null,
107
+ };
108
+ }
109
+
110
+ function findLayoutBinding(helios, key) {
111
+ const layout = helios.layout();
112
+ const descriptor = typeof layout?.getParameterBindings === 'function'
113
+ ? layout.getParameterBindings()
114
+ : null;
115
+ const binding = descriptor?.bindings?.find((entry) => entry?.key === key) ?? null;
116
+ if (!binding) throw new Error(`Unknown layout parameter "${key}"`);
117
+ return binding;
118
+ }
119
+
120
+ function normalizeBindingValue(binding, value) {
121
+ if (binding?.type === 'boolean') return value === true || value === 'true' || value === 1;
122
+ if (binding?.type === 'number' || typeof binding?.get?.() === 'number') {
123
+ const numeric = Number(value);
124
+ if (!Number.isFinite(numeric)) throw new Error(`Layout parameter "${binding.key}" expects a finite number`);
125
+ return numeric;
126
+ }
127
+ return value;
128
+ }
129
+
130
+ function normalizeAttributeWriteValue(value) {
131
+ if (value && typeof value === 'object' && value.__typedArray) {
132
+ const ctor = globalThis[value.__typedArray];
133
+ if (typeof ctor !== 'function') throw new Error(`Unsupported typed array "${value.__typedArray}"`);
134
+ return new ctor(value.values ?? []);
135
+ }
136
+ return value;
137
+ }
138
+
139
+ function normalizeAttributeWriteOptions(options = {}) {
140
+ return {
141
+ ...(options ?? {}),
142
+ type: options?.type ?? 'float',
143
+ };
144
+ }
145
+
146
+ function compileAttributeFunction(source) {
147
+ if (typeof source !== 'string' || !source.trim()) return null;
148
+ const body = source.trim();
149
+ if (/\breturn\b/.test(body) || /[;{}]/.test(body)) {
150
+ return new Function('current', 'id', 'ordinal', 'network', 'context', body);
151
+ }
152
+ return new Function('current', 'id', 'ordinal', 'network', 'context', `return (${body});`);
153
+ }
154
+
155
+ function writeNetworkAttribute(network, params = {}) {
156
+ const scope = String(params.scope ?? 'node').trim().toLowerCase();
157
+ const name = params.name ?? params.attribute;
158
+ const names = params.names ?? params.attributes;
159
+ const options = normalizeAttributeWriteOptions(params.options ?? {
160
+ type: params.type,
161
+ dimension: params.dimension,
162
+ indexBy: params.indexBy,
163
+ });
164
+ const context = cloneJsonSafe(params.context ?? {});
165
+ const functionCode = params.functionCode ?? params.valueCode ?? null;
166
+ const value = functionCode
167
+ ? (current, id, ordinal, net) => compileAttributeFunction(functionCode)(current, id, ordinal, net, context)
168
+ : normalizeAttributeWriteValue(params.values ?? params.value);
169
+
170
+ if (scope === 'node') {
171
+ if (Array.isArray(names)) return network.nodeAttributes(names, value, options);
172
+ return network.nodeAttribute(name, value, options);
173
+ }
174
+ if (scope === 'edge') {
175
+ if (Array.isArray(names)) return network.edgeAttributes(names, value, options);
176
+ return network.edgeAttribute(name, value, options);
177
+ }
178
+ if (scope === 'network') {
179
+ if (Array.isArray(names)) return network.networkAttributes(names, value, options);
180
+ return network.networkAttribute(name, value, options);
181
+ }
182
+ throw new Error('network.attributeSet scope must be "node", "edge", or "network"');
183
+ }
184
+
185
+ function applyLayoutParameters(helios, params = {}) {
186
+ const values = params.values ?? params.parameters ?? params;
187
+ if (!values || typeof values !== 'object' || Array.isArray(values)) {
188
+ throw new Error('layout.setParameters expects an object of parameter values');
189
+ }
190
+ const changed = {};
191
+ for (const [key, rawValue] of Object.entries(values)) {
192
+ if (key === 'values' || key === 'parameters' || key === 'reheat' || key === 'start') continue;
193
+ const binding = findLayoutBinding(helios, key);
194
+ if (typeof binding.set !== 'function') {
195
+ throw new Error(`Layout parameter "${key}" is read-only`);
196
+ }
197
+ const value = normalizeBindingValue(binding, rawValue);
198
+ const statePath = `layout.parameters.${key}`;
199
+ if (typeof helios.states?.entry === 'function' && helios.states.entry(statePath)) {
200
+ helios.states.set(statePath, value, {
201
+ source: 'cli',
202
+ reason: params.reason ?? 'layout.setParameters',
203
+ scope: params.scope ?? 'network',
204
+ trackOverride: params.trackOverride !== false,
205
+ });
206
+ } else {
207
+ binding.set(value);
208
+ }
209
+ changed[key] = typeof binding.get === 'function' ? cloneJsonSafe(binding.get()) : value;
210
+ }
211
+ if (params.start === true) helios.startLayout?.();
212
+ helios.requestRender?.();
213
+ return { changed, layout: getLayoutState(helios) };
214
+ }
215
+
216
+ function identifyLayout(layout) {
217
+ if (!layout) return 'none';
218
+ const descriptor = typeof layout.getParameterBindings === 'function'
219
+ ? layout.getParameterBindings()
220
+ : null;
221
+ if (descriptor?.key) return descriptor.key;
222
+ const name = String(layout.constructor?.name ?? '').toLowerCase();
223
+ if (name.includes('gpuforce')) return 'gpu-force';
224
+ if (name.includes('d3force')) return 'd3force3d';
225
+ if (name.includes('static')) return 'static';
226
+ if (name.includes('worker')) {
227
+ const variant = String(layout.options?.layout ?? '').toLowerCase();
228
+ if (variant === 'jitter') return 'worker:jitter';
229
+ return 'worker:force3d';
230
+ }
231
+ return name || 'unknown';
232
+ }
233
+
234
+ function buildLayoutOptions(helios, key) {
235
+ const mode = helios.mode();
236
+ const nodeCount = Math.max(1, Number(helios.network?.nodeCount ?? 200));
237
+ const radius = 220 * Math.sqrt(nodeCount / 1000);
238
+ const depth = mode === '3d' ? 140 : 0;
239
+ const normalized = normalizeLayoutKey(key);
240
+ if (normalized === 'static') {
241
+ return { type: 'static', options: { bounds: [-500, -500, 500, 500] } };
242
+ }
243
+ if (normalized === 'd3force3d') {
244
+ return {
245
+ type: 'd3force3d',
246
+ options: { settings: { use2D: mode !== '3d', alphaDecay: 0.003 } },
247
+ };
248
+ }
249
+ if (normalized === 'worker:jitter') {
250
+ return {
251
+ type: 'worker',
252
+ options: { layout: 'jitter', mode, center: [0, 0, 0], radius, depth, jitter: 3 },
253
+ };
254
+ }
255
+ if (normalized === 'worker:force3d') {
256
+ return {
257
+ type: 'worker',
258
+ options: {
259
+ layout: 'force3d',
260
+ mode,
261
+ center: [0, 0, 0],
262
+ radius,
263
+ depth,
264
+ kRepulsion: 3,
265
+ kAttraction: 0.003,
266
+ kGravity: 0.0008,
267
+ repulsionStrategy: 'barnes-hut',
268
+ negativesPerNode: 64,
269
+ negativeSampling: true,
270
+ },
271
+ };
272
+ }
273
+ return {
274
+ type: 'gpu-force',
275
+ options: networkHasUmapForceMetadata(helios.network)
276
+ ? {
277
+ mode,
278
+ center: [0, 0, 0],
279
+ radius,
280
+ depth,
281
+ }
282
+ : {
283
+ mode,
284
+ center: [0, 0, 0],
285
+ radius,
286
+ depth,
287
+ outputScale: 6.5,
288
+ linkDistance: 1,
289
+ kRepulsion: 0.07,
290
+ kAttraction: 0.62,
291
+ kGravity: 0.005,
292
+ eta: 0.4,
293
+ damping: 0.92,
294
+ maxStep: 2.5,
295
+ minDistance: 0.15,
296
+ },
297
+ };
298
+ }
299
+
300
+ function seedGridPositions(network, nodeCount, mode, options = {}) {
301
+ network.defineNodeAttribute('_helios_visuals_position', AttributeType.Float, 3);
302
+ network.withBufferAccess(() => {
303
+ const pos = network.getNodeAttributeBuffer('_helios_visuals_position').view;
304
+ if (mode === '3d') {
305
+ const side = clampInteger(options.side, Math.ceil(Math.cbrt(nodeCount)), 1);
306
+ const spacing = 24;
307
+ for (let i = 0; i < nodeCount; i += 1) {
308
+ const z = Math.floor(i / (side * side));
309
+ const rem = i - z * side * side;
310
+ const y = Math.floor(rem / side);
311
+ const x = rem - y * side;
312
+ const offset = i * 3;
313
+ pos[offset] = (x - (side - 1) / 2) * spacing;
314
+ pos[offset + 1] = (y - (side - 1) / 2) * spacing;
315
+ pos[offset + 2] = (z - (side - 1) / 2) * spacing;
316
+ }
317
+ } else {
318
+ const columns = clampInteger(options.columns, Math.ceil(Math.sqrt(nodeCount)), 1);
319
+ const rows = clampInteger(options.rows, Math.ceil(nodeCount / columns), 1);
320
+ const spacing = 24;
321
+ for (let i = 0; i < nodeCount; i += 1) {
322
+ const row = Math.floor(i / columns);
323
+ const col = i - row * columns;
324
+ const offset = i * 3;
325
+ pos[offset] = (col - (columns - 1) / 2) * spacing;
326
+ pos[offset + 1] = (row - (rows - 1) / 2) * spacing;
327
+ pos[offset + 2] = 0;
328
+ }
329
+ }
330
+ });
331
+ }
332
+
333
+ function seedRandomPositions(network, nodeCount, mode) {
334
+ network.defineNodeAttribute('_helios_visuals_position', AttributeType.Float, 3);
335
+ network.withBufferAccess(() => {
336
+ const pos = network.getNodeAttributeBuffer('_helios_visuals_position').view;
337
+ const depth = mode === '3d' ? 200 : 0;
338
+ for (let i = 0; i < nodeCount; i += 1) {
339
+ const offset = i * 3;
340
+ pos[offset] = (Math.random() - 0.5) * 400;
341
+ pos[offset + 1] = (Math.random() - 0.5) * 400;
342
+ pos[offset + 2] = (Math.random() - 0.5) * depth;
343
+ }
344
+ });
345
+ }
346
+
347
+ function seedPositionsFromGenerator(network, nodeCount) {
348
+ if (!network.hasNodeAttribute?.('_helios_generator_position')) return false;
349
+ network.defineNodeAttribute('_helios_visuals_position', AttributeType.Float, 3);
350
+ network.withBufferAccess(() => {
351
+ const source = network.getNodeAttributeBuffer('_helios_generator_position').view;
352
+ const pos = network.getNodeAttributeBuffer('_helios_visuals_position').view;
353
+ for (let i = 0; i < nodeCount; i += 1) {
354
+ pos[i * 3] = (source[i * 2] - 0.5) * 400;
355
+ pos[(i * 3) + 1] = (source[(i * 2) + 1] - 0.5) * 400;
356
+ pos[(i * 3) + 2] = 0;
357
+ }
358
+ });
359
+ return true;
360
+ }
361
+
362
+ function clampInteger(value, fallback, min = 1, max = 1_000_000) {
363
+ const numeric = Number(value);
364
+ const fallbackNumeric = Number(fallback);
365
+ const candidate = Number.isFinite(numeric) ? numeric : fallbackNumeric;
366
+ return Math.min(max, Math.max(min, Math.floor(Number.isFinite(candidate) ? candidate : min)));
367
+ }
368
+
369
+ function clampNumber(value, fallback, min = -Infinity, max = Infinity) {
370
+ const numeric = Number(value);
371
+ if (!Number.isFinite(numeric)) return fallback;
372
+ return Math.min(max, Math.max(min, numeric));
373
+ }
374
+
375
+ function decorateSyntheticNetwork(network, options = {}) {
376
+ const nodeCount = network.nodeCount ?? 0;
377
+ const edgeCount = network.edgeCount ?? 0;
378
+ const labelNodes = nodeCount <= 50_000;
379
+ network.defineNodeAttribute('_helios_visuals_size', AttributeType.Float, 1);
380
+ network.defineNodeAttribute('_helios_visuals_color', AttributeType.Float, 4);
381
+ network.defineEdgeAttribute('_helios_visuals_edge_color', AttributeType.Float, 8);
382
+ network.defineEdgeAttribute('_helios_visuals_edge_width', AttributeType.Float, 2);
383
+ network.defineNodeAttribute('weight', AttributeType.Float, 1);
384
+ network.defineEdgeAttribute('intensity', AttributeType.Float, 1);
385
+ if (labelNodes) {
386
+ network.defineNodeAttribute('label', AttributeType.String, 1);
387
+ network.defineNodeAttribute('category', AttributeType.String, 1);
388
+ }
389
+ network.withBufferAccess(() => {
390
+ const nodeIds = network.nodeIndices;
391
+ const edgeIds = network.edgeIndices;
392
+ const size = network.getNodeAttributeBuffer('_helios_visuals_size').view;
393
+ const color = network.getNodeAttributeBuffer('_helios_visuals_color').view;
394
+ const edgeColor = network.getEdgeAttributeBuffer('_helios_visuals_edge_color').view;
395
+ const edgeWidth = network.getEdgeAttributeBuffer('_helios_visuals_edge_width').view;
396
+ const weight = network.getNodeAttributeBuffer('weight').view;
397
+ const intensity = network.getEdgeAttributeBuffer('intensity').view;
398
+ for (let ordinal = 0; ordinal < nodeIds.length; ordinal += 1) {
399
+ const id = nodeIds[ordinal];
400
+ const ratio = ordinal / Math.max(1, nodeIds.length - 1);
401
+ size[id] = options.nodeSize ?? 9;
402
+ weight[id] = ratio;
403
+ const hue = (ordinal * 37) % 360;
404
+ const phase = hue / 60;
405
+ const x = 1 - Math.abs((phase % 2) - 1);
406
+ const palette = phase < 1 ? [1, x, 0]
407
+ : phase < 2 ? [x, 1, 0]
408
+ : phase < 3 ? [0, 1, x]
409
+ : phase < 4 ? [0, x, 1]
410
+ : phase < 5 ? [x, 0, 1]
411
+ : [1, 0, x];
412
+ color.set([
413
+ palette[0] * 0.55 + 0.25,
414
+ palette[1] * 0.55 + 0.25,
415
+ palette[2] * 0.55 + 0.25,
416
+ 1,
417
+ ], id * 4);
418
+ }
419
+ for (let ordinal = 0; ordinal < edgeIds.length; ordinal += 1) {
420
+ const edgeId = edgeIds[ordinal];
421
+ const value = ordinal / Math.max(1, edgeCount - 1);
422
+ intensity[edgeId] = value;
423
+ edgeColor.set([0.16, 0.28, 0.42, 0.42, 0.16, 0.28, 0.42, 0.42], edgeId * 8);
424
+ edgeWidth[edgeId * 2] = options.edgeWidth ?? 1.2;
425
+ edgeWidth[(edgeId * 2) + 1] = options.edgeWidth ?? 1.2;
426
+ }
427
+ }, { nodeIndices: true, edgeIndices: true });
428
+ if (labelNodes) {
429
+ let nodes = [];
430
+ network.withBufferAccess(() => {
431
+ nodes = Uint32Array.from(network.nodeIndices);
432
+ }, { nodeIndices: true });
433
+ const categoryCount = 8;
434
+ for (let ordinal = 0; ordinal < nodes.length; ordinal += 1) {
435
+ const id = nodes[ordinal];
436
+ const bucket = Math.min(categoryCount - 1, Math.floor((ordinal / Math.max(1, nodes.length)) * categoryCount));
437
+ network.setNodeStringAttribute('label', id, `node-${ordinal}`);
438
+ network.setNodeStringAttribute('category', id, `category${bucket + 1}`);
439
+ }
440
+ network.categorizeNodeAttribute?.('category', { sortOrder: 'frequency' });
441
+ }
442
+ const layout = normalizeLayoutKey(options.layout ?? 'static');
443
+ if (layout === 'static') {
444
+ if (!seedPositionsFromGenerator(network, nodeCount)) {
445
+ seedGridPositions(network, nodeCount, options.mode ?? '2d', options);
446
+ }
447
+ } else {
448
+ seedRandomPositions(network, nodeCount, options.mode ?? '2d');
449
+ }
450
+ return network;
451
+ }
452
+
453
+ async function createGrid2DNetwork(options = {}) {
454
+ const columns = clampInteger(options.columns, 50);
455
+ const rows = clampInteger(options.rows, 50);
456
+ const neighborLevel = clampInteger(options.neighborLevel, 1, 1, 64);
457
+ const periodic = options.periodic === true;
458
+ const network = await HeliosNetwork.generateLattice2D({
459
+ rows,
460
+ columns,
461
+ neighborLevel,
462
+ periodic,
463
+ directed: options.directed === true,
464
+ });
465
+ return decorateSyntheticNetwork(network, {
466
+ ...options,
467
+ rows,
468
+ columns,
469
+ nodeCount: rows * columns,
470
+ mode: '2d',
471
+ layout: options.layout ?? 'static',
472
+ });
473
+ }
474
+
475
+ async function createGrid3DNetwork(options = {}) {
476
+ const requestedNodeCount = clampInteger(options.nodeCount, 4096);
477
+ const side = clampInteger(options.side, Math.ceil(Math.cbrt(requestedNodeCount)));
478
+ const nodeCount = Math.min(requestedNodeCount, side ** 3);
479
+ const neighborLevel = clampInteger(options.neighborLevel, 1, 1, 64);
480
+ const periodic = options.periodic === true;
481
+ const edgeCountEstimate = nodeCount * 3 * neighborLevel;
482
+ const network = await HeliosNetwork.create({
483
+ directed: false,
484
+ initialNodes: nodeCount,
485
+ initialEdges: edgeCountEstimate,
486
+ });
487
+ const edges = new Uint32Array(edgeCountEstimate * 2);
488
+ let edgeOffset = 0;
489
+ const indexAt = (x, y, z) => z * side * side + y * side + x;
490
+ const pushEdge = (from, x, y, z) => {
491
+ let nx = x;
492
+ let ny = y;
493
+ let nz = z;
494
+ if (periodic) {
495
+ nx = (nx + side) % side;
496
+ ny = (ny + side) % side;
497
+ nz = (nz + side) % side;
498
+ }
499
+ if (nx < 0 || nx >= side || ny < 0 || ny >= side || nz < 0 || nz >= side) return;
500
+ const to = indexAt(nx, ny, nz);
501
+ if (to >= nodeCount || to === from) return;
502
+ edges[edgeOffset] = from;
503
+ edges[edgeOffset + 1] = to;
504
+ edgeOffset += 2;
505
+ };
506
+ for (let z = 0; z < side; z += 1) {
507
+ for (let y = 0; y < side; y += 1) {
508
+ for (let x = 0; x < side; x += 1) {
509
+ const from = indexAt(x, y, z);
510
+ if (from >= nodeCount) break;
511
+ for (let level = 1; level <= neighborLevel; level += 1) {
512
+ pushEdge(from, x + level, y, z);
513
+ pushEdge(from, x, y + level, z);
514
+ pushEdge(from, x, y, z + level);
515
+ }
516
+ }
517
+ }
518
+ }
519
+ if (edgeOffset > 0) network.addEdges(edges.subarray(0, edgeOffset));
520
+ return decorateSyntheticNetwork(network, {
521
+ ...options,
522
+ side,
523
+ nodeCount,
524
+ mode: '3d',
525
+ layout: options.layout ?? 'static',
526
+ });
527
+ }
528
+
529
+ async function createSmallWorldNetwork(options = {}) {
530
+ const nodeCount = clampInteger(options.nodeCount, 1000);
531
+ const neighborLevel = clampInteger(options.neighborLevel, 2, 1, Math.max(1, Math.floor(nodeCount / 2)));
532
+ const rewiringProbability = clampNumber(options.rewiringProbability, 0.01, 0, 1);
533
+ const seed = clampInteger(options.seed, 1, 1, 0x7fffffff);
534
+ const network = await HeliosNetwork.generateWattsStrogatz({
535
+ nodeCount,
536
+ neighborLevel,
537
+ rewiringProbability,
538
+ seed,
539
+ directed: options.directed === true,
540
+ });
541
+ return decorateSyntheticNetwork(network, { ...options, mode: options.mode ?? '2d', layout: options.layout ?? 'gpu-force' });
542
+ }
543
+
544
+ async function createBarabasiAlbertNetwork(options = {}) {
545
+ const nodeCount = clampInteger(options.nodeCount, 1000);
546
+ const edgesPerNewNode = clampInteger(options.edgesPerNewNode, 2, 1, Math.max(1, nodeCount - 1));
547
+ const initialCliqueSize = clampInteger(options.initialCliqueSize, edgesPerNewNode + 1, 2, nodeCount);
548
+ const seed = clampInteger(options.seed, 1, 1, 0x7fffffff);
549
+ const network = await HeliosNetwork.generateBarabasiAlbert({
550
+ nodeCount,
551
+ edgesPerNewNode,
552
+ initialCliqueSize,
553
+ directed: options.directed === true,
554
+ seed,
555
+ });
556
+ return decorateSyntheticNetwork(network, { ...options, mode: options.mode ?? '2d', layout: options.layout ?? 'gpu-force' });
557
+ }
558
+
559
+ async function createRandomGeometricNetwork(options = {}) {
560
+ const nodeCount = clampInteger(options.nodeCount, 1000, 1, 100_000);
561
+ const radius = clampNumber(options.radius, 0.05, 0, 1);
562
+ const seed = clampInteger(options.seed, 1, 1, 0x7fffffff);
563
+ const network = await HeliosNetwork.generateRandomGeometric({
564
+ nodeCount,
565
+ radius,
566
+ directed: options.directed === true,
567
+ seed,
568
+ });
569
+ return decorateSyntheticNetwork(network, { ...options, mode: options.mode ?? '2d', layout: options.layout ?? 'static' });
570
+ }
571
+
572
+ async function createWaxmanNetwork(options = {}) {
573
+ const nodeCount = clampInteger(options.nodeCount, 1000, 1, 100_000);
574
+ const alpha = clampNumber(options.alpha, 0.4, 0.001, 10);
575
+ const beta = clampNumber(options.beta, 0.2, 0, 1);
576
+ const seed = clampInteger(options.seed, 1, 1, 0x7fffffff);
577
+ const network = await HeliosNetwork.generateWaxman({
578
+ nodeCount,
579
+ alpha,
580
+ beta,
581
+ directed: options.directed === true,
582
+ seed,
583
+ });
584
+ return decorateSyntheticNetwork(network, { ...options, mode: options.mode ?? '2d', layout: options.layout ?? 'static' });
585
+ }
586
+
587
+ async function createStochasticBlockNetwork(options = {}) {
588
+ const blockCount = clampInteger(options.blockCount, 4, 1, 64);
589
+ const blockSize = clampInteger(options.blockSize, 50, 1, 20_000);
590
+ const intraProbability = clampNumber(options.intraProbability, 0.08, 0, 1);
591
+ const interProbability = clampNumber(options.interProbability, 0.01, 0, 1);
592
+ const seed = clampInteger(options.seed, 1, 1, 0x7fffffff);
593
+ const blockSizes = Array.from({ length: blockCount }, () => blockSize);
594
+ const probabilities = Array.from({ length: blockCount }, (_, row) => (
595
+ Array.from({ length: blockCount }, (_, column) => (row === column ? intraProbability : interProbability))
596
+ ));
597
+ const network = await HeliosNetwork.generateStochasticBlockModel({
598
+ blockSizes,
599
+ probabilities,
600
+ directed: options.directed === true,
601
+ seed,
602
+ });
603
+ return decorateSyntheticNetwork(network, {
604
+ ...options,
605
+ nodeCount: blockCount * blockSize,
606
+ mode: options.mode ?? '2d',
607
+ layout: options.layout ?? 'gpu-force',
608
+ });
609
+ }
610
+
611
+ async function createConfigurationModelNetwork(options = {}) {
612
+ const nodeCount = clampInteger(options.nodeCount, 500, 1, 200_000);
613
+ const maxDegree = options.allowSelfLoops === true || options.allowMultiEdges === true ? 10_000 : Math.max(0, nodeCount - 1);
614
+ const degree = clampInteger(options.degree, 4, 0, maxDegree);
615
+ const seed = clampInteger(options.seed, 1, 1, 0x7fffffff);
616
+ const degrees = Array.from({ length: nodeCount }, () => degree);
617
+ if ((degree * nodeCount) % 2 !== 0) {
618
+ degrees[nodeCount - 1] = Math.max(0, degree - 1);
619
+ }
620
+ const network = await HeliosNetwork.generateConfigurationModel({
621
+ degrees,
622
+ directed: options.directed === true,
623
+ allowSelfLoops: options.allowSelfLoops === true,
624
+ allowMultiEdges: options.allowMultiEdges !== false,
625
+ seed,
626
+ });
627
+ return decorateSyntheticNetwork(network, { ...options, mode: options.mode ?? '2d', layout: options.layout ?? 'gpu-force' });
628
+ }
629
+
630
+ async function createSeedNetwork({ nodeCount = 200, mode = '2d', layout = 'gpu-force' } = {}) {
631
+ const network = await HeliosNetwork.create({ directed: false, initialNodes: 0 });
632
+ network.defineNodeAttribute('_helios_visuals_size', AttributeType.Float, 1);
633
+ network.defineNodeAttribute('_helios_visuals_color', AttributeType.Float, 4);
634
+ network.defineEdgeAttribute('_helios_visuals_edge_color', AttributeType.Float, 8);
635
+ network.defineEdgeAttribute('_helios_visuals_edge_width', AttributeType.Float, 2);
636
+ network.defineNodeAttribute('weight', AttributeType.Float, 1);
637
+ network.defineNodeAttribute('label', AttributeType.String, 1);
638
+ const nodes = network.addNodes(nodeCount);
639
+ const edges = [];
640
+ for (let index = 0; index < nodes.length; index += 1) {
641
+ edges.push([nodes[index], nodes[(index + 1) % nodes.length]]);
642
+ }
643
+ const edgeIds = network.addEdges(edges);
644
+ network.withBufferAccess(() => {
645
+ const size = network.getNodeAttributeBuffer('_helios_visuals_size').view;
646
+ const color = network.getNodeAttributeBuffer('_helios_visuals_color').view;
647
+ const edgeColor = network.getEdgeAttributeBuffer('_helios_visuals_edge_color').view;
648
+ const edgeWidth = network.getEdgeAttributeBuffer('_helios_visuals_edge_width').view;
649
+ const weight = network.getNodeAttributeBuffer('weight').view;
650
+ for (let i = 0; i < nodes.length; i += 1) {
651
+ const id = nodes[i];
652
+ size[id] = 10;
653
+ weight[id] = i / Math.max(1, nodes.length - 1);
654
+ const c = [(i * 97) % 255, (i * 57) % 255, (i * 17) % 255].map((v) => (v / 255) * 0.9 + 0.1);
655
+ color.set([c[0], c[1], c[2], 1], id * 4);
656
+ }
657
+ for (const edgeId of edgeIds) {
658
+ edgeColor.set([0.35, 0.55, 1.0, 0.5, 0.35, 0.55, 1.0, 0.5], edgeId * 8);
659
+ edgeWidth[edgeId * 2] = 1.5;
660
+ edgeWidth[(edgeId * 2) + 1] = 1.5;
661
+ }
662
+ });
663
+ for (let i = 0; i < nodes.length; i += 1) {
664
+ network.setNodeStringAttribute('label', nodes[i], `node-${i}`);
665
+ }
666
+ if (normalizeLayoutKey(layout) === 'static') {
667
+ seedGridPositions(network, nodeCount, mode);
668
+ } else {
669
+ seedRandomPositions(network, nodeCount, mode);
670
+ }
671
+ return network;
672
+ }
673
+
674
+ async function createSyntheticNetwork(options = {}) {
675
+ const model = String(options.model ?? options.name ?? 'ring').trim().toLowerCase();
676
+ if (['grid', 'grid2d', 'lattice', 'lattice2d'].includes(model)) return createGrid2DNetwork(options);
677
+ if (['grid3d', 'lattice3d'].includes(model)) return createGrid3DNetwork(options);
678
+ if (['sw', 'small-world', 'smallworld', 'watts-strogatz', 'watts_strogatz'].includes(model)) return createSmallWorldNetwork(options);
679
+ if (['ba', 'barabasi-albert', 'barabasi_albert', 'preferential-attachment'].includes(model)) return createBarabasiAlbertNetwork(options);
680
+ if (['random-geometric', 'random_geometric', 'geometric'].includes(model)) return createRandomGeometricNetwork(options);
681
+ if (model === 'waxman') return createWaxmanNetwork(options);
682
+ if (['sbm', 'stochastic-block', 'stochastic_block', 'stochastic-block-model'].includes(model)) return createStochasticBlockNetwork(options);
683
+ if (['configuration', 'configuration-model', 'configuration_model'].includes(model)) return createConfigurationModelNetwork(options);
684
+ return createSeedNetwork({
685
+ nodeCount: clampInteger(options.nodeCount, 200),
686
+ mode: options.mode ?? '2d',
687
+ layout: options.layout ?? 'gpu-force',
688
+ });
689
+ }
690
+
691
+ async function blobToBase64(blob) {
692
+ const buffer = await blob.arrayBuffer();
693
+ let binary = '';
694
+ const bytes = new Uint8Array(buffer);
695
+ for (let i = 0; i < bytes.length; i += 1) binary += String.fromCharCode(bytes[i]);
696
+ return btoa(binary);
697
+ }
698
+
699
+ function base64ToUint8Array(base64) {
700
+ const binary = atob(String(base64 ?? ''));
701
+ const bytes = new Uint8Array(binary.length);
702
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
703
+ return bytes;
704
+ }
705
+
706
+ function fileFromBase64({ name, base64, mimeType = 'application/octet-stream' }) {
707
+ const bytes = base64ToUint8Array(base64);
708
+ return new File([bytes], name, { type: mimeType });
709
+ }
710
+
711
+ function encodeBinaryForJson(value) {
712
+ if (value == null || typeof value !== 'object') return value;
713
+ if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
714
+ const bytes = value instanceof ArrayBuffer
715
+ ? new Uint8Array(value)
716
+ : new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
717
+ let binary = '';
718
+ for (let i = 0; i < bytes.length; i += 1) binary += String.fromCharCode(bytes[i]);
719
+ return {
720
+ __heliosBinary: 'base64',
721
+ type: value.constructor?.name ?? 'Uint8Array',
722
+ byteLength: bytes.byteLength,
723
+ data: btoa(binary),
724
+ };
725
+ }
726
+ if (Array.isArray(value)) return value.map((entry) => encodeBinaryForJson(entry));
727
+ const out = {};
728
+ for (const [key, entry] of Object.entries(value)) out[key] = encodeBinaryForJson(entry);
729
+ return out;
730
+ }
731
+
732
+ function decodeBinaryFromJson(value) {
733
+ if (value == null || typeof value !== 'object') return value;
734
+ if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) return value;
735
+ if (value.__heliosBinary === 'base64') return base64ToUint8Array(value.data ?? '');
736
+ if (Array.isArray(value)) return value.map((entry) => decodeBinaryFromJson(entry));
737
+ const out = {};
738
+ for (const [key, entry] of Object.entries(value)) out[key] = decodeBinaryFromJson(entry);
739
+ return out;
740
+ }
741
+
742
+ class CliStorageClient {
743
+ constructor({ baseUrl = '' } = {}) {
744
+ this.baseUrl = baseUrl;
745
+ }
746
+
747
+ async request(path, options = {}) {
748
+ const response = await fetch(`${this.baseUrl}${path}`, {
749
+ ...options,
750
+ headers: {
751
+ ...(options.body ? { 'content-type': 'application/json' } : {}),
752
+ ...(options.headers ?? {}),
753
+ },
754
+ });
755
+ const payload = decodeBinaryFromJson(await response.json().catch(() => null));
756
+ if (!response.ok) {
757
+ throw new Error(payload?.error ?? `CLI storage request failed with HTTP ${response.status}`);
758
+ }
759
+ return payload;
760
+ }
761
+
762
+ putSession(record) {
763
+ return this.request('/api/storage/session', {
764
+ method: 'POST',
765
+ body: JSON.stringify(encodeBinaryForJson(record)),
766
+ });
767
+ }
768
+
769
+ getSession(id) {
770
+ return this.request(`/api/storage/session/${encodeURIComponent(String(id))}`)
771
+ .catch((error) => {
772
+ if (String(error?.message ?? '').includes('not-found')) return null;
773
+ throw error;
774
+ });
775
+ }
776
+
777
+ listSessions() {
778
+ return this.request('/api/storage/sessions');
779
+ }
780
+
781
+ deleteSession(id) {
782
+ return this.request(`/api/storage/session/${encodeURIComponent(String(id))}`, { method: 'DELETE' })
783
+ .then((result) => result.deleted === true);
784
+ }
785
+
786
+ getUnfinishedSessionId(workspaceId = null) {
787
+ const query = workspaceId == null ? '' : `?workspaceId=${encodeURIComponent(String(workspaceId))}`;
788
+ return this.request(`/api/storage/unfinished${query}`).then((result) => result.sessionId ?? null);
789
+ }
790
+
791
+ setUnfinishedSessionId(id, workspaceId = null) {
792
+ return this.request('/api/storage/unfinished', {
793
+ method: 'PUT',
794
+ body: JSON.stringify({ sessionId: id ?? null, workspaceId }),
795
+ }).then((result) => result.sessionId ?? null);
796
+ }
797
+ }
798
+
799
+ function getNetworkStats(helios) {
800
+ const network = helios.network;
801
+ const typeNames = new Map(Object.entries(AttributeType).map(([name, value]) => [value, name]));
802
+ const serializeInfo = (name, info) => {
803
+ const type = info?.type ?? AttributeType.Unknown;
804
+ return {
805
+ name,
806
+ type,
807
+ typeName: typeNames.get(type) ?? `Unknown(${type})`,
808
+ dimension: Number(info?.dimension ?? 1),
809
+ complex: info?.complex === true,
810
+ categorical: type === AttributeType.Category || type === AttributeType.MultiCategory,
811
+ stringLike: type === AttributeType.String,
812
+ };
813
+ };
814
+ const inspectScope = (scope) => {
815
+ const names = network?.[`get${scope}AttributeNames`]?.() ?? [];
816
+ return names.map((name) => serializeInfo(name, network?.[`get${scope}AttributeInfo`]?.(name)));
817
+ };
818
+ return {
819
+ nodeCount: network?.nodeCount ?? 0,
820
+ edgeCount: network?.edgeCount ?? 0,
821
+ directed: Boolean(network?.directed),
822
+ nodeAttributes: network?.getNodeAttributeNames?.() ?? [],
823
+ edgeAttributes: network?.getEdgeAttributeNames?.() ?? [],
824
+ networkAttributes: network?.getNetworkAttributeNames?.() ?? [],
825
+ attributes: {
826
+ node: inspectScope('Node'),
827
+ edge: inspectScope('Edge'),
828
+ network: inspectScope('Network'),
829
+ },
830
+ };
831
+ }
832
+
833
+ function getLayoutState(helios) {
834
+ const layout = helios.layout();
835
+ const descriptor = typeof layout?.getParameterBindings === 'function'
836
+ ? layout.getParameterBindings()
837
+ : null;
838
+ return {
839
+ key: identifyLayout(layout),
840
+ label: descriptor?.label ?? layout?.constructor?.name ?? null,
841
+ runState: typeof helios.scheduler?.getLayoutState === 'function'
842
+ ? helios.scheduler.getLayoutState()
843
+ : (helios.scheduler?.layoutEnabled !== false ? 'running' : 'stopped'),
844
+ descriptor: descriptor
845
+ ? {
846
+ key: descriptor.key ?? null,
847
+ label: descriptor.label ?? null,
848
+ dynamic: descriptor.dynamic === true,
849
+ bindings: Array.isArray(descriptor.bindings) ? descriptor.bindings.map(serializeLayoutBinding) : [],
850
+ }
851
+ : null,
852
+ };
853
+ }
854
+
855
+ function serializeBehavior(behavior) {
856
+ if (!behavior) return null;
857
+ return {
858
+ id: behavior.id ?? behavior.constructor?.id ?? null,
859
+ options: cloneJsonSafe(behavior.options ?? {}),
860
+ state: cloneJsonSafe(behavior.state ?? null),
861
+ serialized: typeof behavior.serialize === 'function' ? cloneJsonSafe(behavior.serialize()) : null,
862
+ };
863
+ }
864
+
865
+ function getBehaviorState(helios) {
866
+ const entries = typeof helios.behaviors?.entries === 'function' ? helios.behaviors.entries() : [];
867
+ const attached = {};
868
+ for (const [id, behavior] of entries) {
869
+ attached[id] = serializeBehavior(behavior);
870
+ }
871
+ return {
872
+ attached,
873
+ serialized: cloneJsonSafe(helios.serializeBehaviorState?.() ?? {}),
874
+ };
875
+ }
876
+
877
+ function findBehavior(helios, id) {
878
+ const behavior = helios.getBehavior?.(id) ?? helios.behaviors?.get?.(id) ?? null;
879
+ if (!behavior) throw new Error(`Behavior "${id}" is not attached`);
880
+ return behavior;
881
+ }
882
+
883
+ function findOptionalBehavior(helios, id) {
884
+ return helios.getBehavior?.(id) ?? helios.behaviors?.get?.(id) ?? helios.behavior?.[id] ?? null;
885
+ }
886
+
887
+ function sanitizeFigureRpcOptions(params = {}) {
888
+ const {
889
+ outputPath,
890
+ useCurrentOptions,
891
+ current,
892
+ ...options
893
+ } = params && typeof params === 'object' ? params : {};
894
+ return options;
895
+ }
896
+
897
+ function resolveFigureRpcOptions(helios, params = {}) {
898
+ const exporter = findOptionalBehavior(helios, 'exporter');
899
+ const options = sanitizeFigureRpcOptions(params);
900
+ const useCurrentOptions = params.useCurrentOptions === true || params.current === true;
901
+ if (useCurrentOptions && typeof exporter?.getResolvedOptions === 'function') {
902
+ return exporter.getResolvedOptions(options);
903
+ }
904
+ if (typeof helios?._resolveFigureExportOptions === 'function') {
905
+ return helios._resolveFigureExportOptions(options);
906
+ }
907
+ return options;
908
+ }
909
+
910
+ async function exportFigureRpcBlob(helios, params = {}) {
911
+ const exporter = findOptionalBehavior(helios, 'exporter');
912
+ const options = sanitizeFigureRpcOptions(params);
913
+ const useCurrentOptions = params.useCurrentOptions === true || params.current === true;
914
+ if (useCurrentOptions && typeof exporter?.exportBlob === 'function') {
915
+ return exporter.exportBlob(options);
916
+ }
917
+ return helios.exportFigureBlob(resolveFigureRpcOptions(helios, params));
918
+ }
919
+
920
+ function setBehaviorEnabled(helios, id, enabled, options = {}) {
921
+ const behavior = findBehavior(helios, id);
922
+ const value = enabled !== false;
923
+ if (typeof behavior.enabled === 'function') {
924
+ behavior.enabled(value);
925
+ } else if (behavior.state && Object.prototype.hasOwnProperty.call(behavior.state, 'enabled')) {
926
+ behavior.state.enabled = value;
927
+ behavior.emit?.('change', { reason: 'cli-enabled', state: cloneJsonSafe(behavior.state) });
928
+ } else if (value === false && options.detach === true) {
929
+ helios.behaviors?.detach?.(id);
930
+ return getBehaviorState(helios);
931
+ } else if (typeof behavior.update === 'function') {
932
+ behavior.update({ enabled: value });
933
+ } else {
934
+ throw new Error(`Behavior "${id}" does not expose enabled state`);
935
+ }
936
+ setRegisteredCliState(helios, [`${id}.enabled`, `behaviors.${id}.enabled`], value, {
937
+ reason: options.reason ?? 'behaviors.setEnabled',
938
+ scope: options.scope ?? 'session',
939
+ trackOverride: options.trackOverride,
940
+ });
941
+ helios.requestRender?.();
942
+ return serializeBehavior(helios.getBehavior?.(id) ?? helios.behaviors?.get?.(id) ?? behavior);
943
+ }
944
+
945
+ function flattenObjectLeaves(value, prefix = '') {
946
+ if (!value || typeof value !== 'object' || Array.isArray(value) || ArrayBuffer.isView(value)) {
947
+ return prefix ? [[prefix, value]] : [];
948
+ }
949
+ const leaves = [];
950
+ for (const [key, entry] of Object.entries(value)) {
951
+ const next = prefix ? `${prefix}.${key}` : key;
952
+ if (entry && typeof entry === 'object' && !Array.isArray(entry) && !ArrayBuffer.isView(entry)) {
953
+ const childLeaves = flattenObjectLeaves(entry, next);
954
+ if (childLeaves.length > 0) leaves.push(...childLeaves);
955
+ else leaves.push([next, entry]);
956
+ } else {
957
+ leaves.push([next, entry]);
958
+ }
959
+ }
960
+ return leaves;
961
+ }
962
+
963
+ function setRegisteredCliState(helios, candidates, value, options = {}) {
964
+ for (const candidate of candidates) {
965
+ if (!candidate || !helios.states?.entry?.(candidate)) continue;
966
+ return helios.states.set(candidate, value, {
967
+ source: 'cli',
968
+ reason: options.reason ?? 'cli-rpc',
969
+ scope: options.scope ?? 'session',
970
+ trackOverride: options.trackOverride !== false,
971
+ applyBinding: options.applyBinding !== false,
972
+ debounceMs: options.debounceMs ?? 0,
973
+ });
974
+ }
975
+ return null;
976
+ }
977
+
978
+ function trackBehaviorOptionOverrides(helios, id, options = {}, detail = {}) {
979
+ const tracked = [];
980
+ for (const [path, value] of flattenObjectLeaves(options)) {
981
+ const result = setRegisteredCliState(helios, [
982
+ `${id}.${path}`,
983
+ `behaviors.${id}.${path}`,
984
+ ], value, {
985
+ reason: detail.reason ?? `behaviors.${id}`,
986
+ scope: detail.scope ?? 'session',
987
+ trackOverride: detail.trackOverride,
988
+ applyBinding: false,
989
+ });
990
+ if (result?.key) tracked.push(result.key);
991
+ }
992
+ return tracked;
993
+ }
994
+
995
+ function assignNested(target, path, value) {
996
+ const parts = String(path).split('.').filter(Boolean);
997
+ let cursor = target;
998
+ while (parts.length > 1) {
999
+ const part = parts.shift();
1000
+ if (!cursor[part] || typeof cursor[part] !== 'object' || Array.isArray(cursor[part])) cursor[part] = {};
1001
+ cursor = cursor[part];
1002
+ }
1003
+ if (parts.length === 1) cursor[parts[0]] = value;
1004
+ }
1005
+
1006
+ function applyAppearanceOverridesFromState(helios, overrides = {}) {
1007
+ const patch = {};
1008
+ for (const [key, value] of Object.entries(overrides)) {
1009
+ if (!key.startsWith('appearance.')) continue;
1010
+ assignNested(patch, key.slice('appearance.'.length), value);
1011
+ }
1012
+ if (Object.keys(patch).length === 0) return null;
1013
+ const behavior = helios.useBehavior?.('appearance', patch) ?? helios.behavior?.appearance;
1014
+ if (behavior && typeof behavior.update === 'function') behavior.update(patch);
1015
+ helios.requestRender?.();
1016
+ return patch;
1017
+ }
1018
+
1019
+ function reapplyRestoredStateBindings(helios, reason = 'cli-post-restore-bindings') {
1020
+ const overrides = helios.states?.getOverrides?.({ aliases: false });
1021
+ const preferredOverrides = helios.states?.getOverrides?.({ aliases: 'preferred' });
1022
+ if (!overrides || typeof overrides !== 'object' || Object.keys(overrides).length === 0) return null;
1023
+ const restored = helios.states?.restore?.(overrides, {
1024
+ source: 'restore',
1025
+ reason,
1026
+ trackOverride: true,
1027
+ });
1028
+ applyAppearanceOverridesFromState(helios, preferredOverrides ?? {});
1029
+ return restored;
1030
+ }
1031
+
1032
+ function invokeBehavior(helios, params = {}) {
1033
+ const id = params.id ?? params.behavior;
1034
+ const method = params.method ?? params.accessor;
1035
+ const args = Array.isArray(params.args) ? params.args : [];
1036
+ if (!method || typeof method !== 'string') throw new Error('behaviors.call expects a method name');
1037
+ if (method === 'attach' || method === 'detach' || method === 'constructor') {
1038
+ throw new Error(`Refusing to call behavior method "${method}" through behaviors.call`);
1039
+ }
1040
+ const behavior = findBehavior(helios, id);
1041
+ if (typeof behavior[method] !== 'function') {
1042
+ throw new Error(`Behavior "${id}" does not expose method "${method}"`);
1043
+ }
1044
+ const result = behavior[method](...args);
1045
+ helios.requestRender?.();
1046
+ return {
1047
+ result: result === behavior ? serializeBehavior(behavior) : cloneJsonSafe(result),
1048
+ behavior: serializeBehavior(behavior),
1049
+ };
1050
+ }
1051
+
1052
+ function getPositionSourceState(helios) {
1053
+ const raw = helios.positions?.() ?? null;
1054
+ const delegate = raw?.delegate ?? null;
1055
+ const source = {
1056
+ source: raw?.source ?? null,
1057
+ delegate: delegate
1058
+ ? {
1059
+ id: delegate.id ?? delegate.constructor?.name ?? 'delegate',
1060
+ type: delegate.constructor?.name ?? null,
1061
+ version: delegate.version ?? null,
1062
+ }
1063
+ : null,
1064
+ };
1065
+ return {
1066
+ ...source,
1067
+ choices: cloneJsonSafe(helios.getLayoutPositionAttributeChoices?.() ?? []),
1068
+ layout: getLayoutState(helios),
1069
+ };
1070
+ }
1071
+
1072
+ function readPositionAttribute(helios, attribute = '_helios_visuals_position', options = {}) {
1073
+ const network = helios.network;
1074
+ const info = network?.getNodeAttributeInfo?.(attribute) ?? null;
1075
+ if (!info) return { attribute, exists: false, count: 0, dimension: 0 };
1076
+ const dimension = Math.max(1, Number(info.dimension ?? 1));
1077
+ const includeValues = options.includeValues !== false;
1078
+ const limit = options.limit == null ? null : Math.max(0, Number(options.limit) || 0);
1079
+ let values = null;
1080
+ let count = 0;
1081
+ if (includeValues) {
1082
+ network.withBufferAccess?.(() => {
1083
+ const view = network.getNodeAttributeBuffer(attribute).view;
1084
+ count = Math.floor(view.length / dimension);
1085
+ const itemCount = limit == null ? count : Math.min(count, limit);
1086
+ values = Array.from(view.slice(0, itemCount * dimension));
1087
+ });
1088
+ } else {
1089
+ count = Math.floor((network.getNodeAttributeBuffer?.(attribute)?.view?.length ?? 0) / dimension);
1090
+ }
1091
+ return { attribute, exists: true, dimension, count, values };
1092
+ }
1093
+
1094
+ async function snapshotPositions(helios, params = {}) {
1095
+ const source = helios.positions?.() ?? { source: 'network' };
1096
+ if (source?.source === 'delegate') {
1097
+ const snapshot = await helios.snapshotDelegatePositions?.();
1098
+ const limit = params.limit == null ? null : Math.max(0, Number(params.limit) || 0);
1099
+ const values = snapshot
1100
+ ? Array.from(snapshot.slice(0, limit == null ? snapshot.length : Math.min(snapshot.length, limit * 3)))
1101
+ : null;
1102
+ return {
1103
+ source: 'delegate',
1104
+ dimension: 3,
1105
+ count: snapshot ? Math.floor(snapshot.length / 3) : 0,
1106
+ values,
1107
+ };
1108
+ }
1109
+ return { source: 'network', ...readPositionAttribute(helios, params.attribute ?? '_helios_visuals_position', params) };
1110
+ }
1111
+
1112
+ function applyPositionsFromAttribute(helios, params = {}) {
1113
+ const attribute = params.attribute ?? params.name ?? '_helios_visuals_position';
1114
+ if (params.stopLayout === true) helios.stopLayout?.('cli:positions-from-attribute');
1115
+ const wrote = helios.setLayoutPositionsFromNodeAttribute(attribute, params.options ?? {});
1116
+ if (!wrote) throw new Error(`Could not apply node attribute "${attribute}" as layout positions`);
1117
+ helios.behavior?.layout?.positionAttribute?.(attribute);
1118
+ if (params.start === true) helios.startLayout?.();
1119
+ helios.requestRender?.();
1120
+ return getPositionSourceState(helios);
1121
+ }
1122
+
1123
+ function setCustomPositions(helios, params = {}) {
1124
+ const attribute = params.attribute ?? '_helios_visuals_position';
1125
+ const rawValues = params.values ?? params.positions;
1126
+ if (!Array.isArray(rawValues) && !ArrayBuffer.isView(rawValues)) {
1127
+ throw new Error('positions.set expects a flat or nested values array');
1128
+ }
1129
+ const first = Array.isArray(rawValues) ? rawValues.find((entry) => entry != null) : null;
1130
+ const nested = Array.isArray(first) || ArrayBuffer.isView(first);
1131
+ const dimension = Number(params.dimension ?? (nested ? first.length : 3));
1132
+ const rows = nested
1133
+ ? rawValues.map((entry) => Array.from(entry))
1134
+ : Array.from({ length: Math.floor(rawValues.length / dimension) }, (_, index) => (
1135
+ Array.from(rawValues).slice(index * dimension, (index + 1) * dimension)
1136
+ ));
1137
+ const nodeIds = Array.isArray(params.nodes) ? params.nodes.map((entry) => Number(entry)) : null;
1138
+ const byNode = nodeIds ? new Map(nodeIds.map((node, index) => [node, rows[index]])) : null;
1139
+ writeNetworkAttribute(helios.network, {
1140
+ scope: 'node',
1141
+ name: attribute,
1142
+ value: (current, id, ordinal) => byNode?.get(id) ?? rows[ordinal] ?? current ?? new Array(dimension).fill(0),
1143
+ options: { type: params.type ?? 'float', dimension, indexBy: params.indexBy ?? 'auto' },
1144
+ });
1145
+ if (params.apply !== false) {
1146
+ applyPositionsFromAttribute(helios, { attribute, stopLayout: params.stopLayout, start: params.start });
1147
+ }
1148
+ return snapshotPositions(helios, { attribute, includeValues: params.includeValues === true, limit: params.limit });
1149
+ }
1150
+
1151
+ function getSceneState(helios) {
1152
+ const graphLayer = helios.renderer?.graphLayer ?? null;
1153
+ return {
1154
+ mode: helios.mode(),
1155
+ renderer: helios.renderer?.device?.type ?? null,
1156
+ rendererState: graphLayer ? {
1157
+ propagateHoveredNodeToEdges: graphLayer.propagateHoveredNodeToEdges === true,
1158
+ propagateSelectedNodesToEdges: graphLayer.propagateSelectedNodesToEdges === true,
1159
+ nodeNoStateStyleEnabled: graphLayer.nodeNoStateStyleEnabled === true,
1160
+ edgeNoStateStyleEnabled: graphLayer.edgeNoStateStyleEnabled === true,
1161
+ } : null,
1162
+ size: cloneJsonSafe(helios.size ?? helios.layers?.size ?? null),
1163
+ network: getNetworkStats(helios),
1164
+ camera: cloneJsonSafe(helios.cameraPose?.() ?? null),
1165
+ cameraControls: cloneJsonSafe(helios.cameraControls?.() ?? null),
1166
+ labels: cloneJsonSafe(helios.labels?.() ?? null),
1167
+ legends: cloneJsonSafe(helios.legends?.() ?? null),
1168
+ density: cloneJsonSafe(helios.density?.() ?? null),
1169
+ filter: cloneJsonSafe(helios.getGraphFilter?.() ?? null),
1170
+ behaviors: getBehaviorState(helios),
1171
+ positions: getPositionSourceState(helios),
1172
+ layout: getLayoutState(helios),
1173
+ mappers: {
1174
+ node: serializeMapperCollection(helios.nodeMapper),
1175
+ edge: serializeMapperCollection(helios.edgeMapper),
1176
+ },
1177
+ url: window.location.href,
1178
+ };
1179
+ }
1180
+
1181
+ function createCliPersistence({ helios, config }) {
1182
+ const sessionId = config.sessionId ?? new URLSearchParams(window.location.search).get('sessionId') ?? 'unknown';
1183
+ const persistenceId = cliPersistenceId(sessionId);
1184
+ let saveTimer = null;
1185
+ let pendingSave = Promise.resolve(null);
1186
+
1187
+ const save = async (options = {}) => {
1188
+ if (options.enabled === false) return null;
1189
+ const storage = helios.storage;
1190
+ if (!storage?.capabilities?.sessions) return { saved: false, id: persistenceId, reason: 'storage-unavailable' };
1191
+ const includeNetwork = options.fullSession !== false && options.includeNetwork !== false;
1192
+ const captureThumbnail = Object.hasOwn(options, 'captureThumbnail') && options.captureThumbnail !== undefined
1193
+ ? options.captureThumbnail
1194
+ : includeNetwork ? true : 'auto';
1195
+ const envelope = await storage.flush({
1196
+ id: persistenceId,
1197
+ reason: options.reason ?? 'cli-save',
1198
+ includeNetwork,
1199
+ includePositions: options.includePositions === true || includeNetwork,
1200
+ snapshotLayoutRuntime: options.snapshotLayoutRuntime === true,
1201
+ networkFormat: options.networkFormat ?? 'zxnet',
1202
+ captureThumbnail,
1203
+ thumbnail: options.thumbnail ?? options.sessionThumbnail,
1204
+ fullVisualizationState: options.fullVisualizationState === true,
1205
+ });
1206
+ const thumbnail = envelope?.payload?.thumbnail ?? null;
1207
+ return {
1208
+ storage: 'cli-filesystem',
1209
+ id: envelope?.id ?? envelope?.payload?.session?.id ?? storage.sessionId ?? persistenceId,
1210
+ updatedAt: envelope?.payload?.session?.updatedAt ?? Date.now(),
1211
+ session: envelope?.payload?.session ?? null,
1212
+ thumbnail: thumbnail ? {
1213
+ type: thumbnail.type ?? null,
1214
+ encoding: thumbnail.encoding ?? null,
1215
+ width: thumbnail.width ?? null,
1216
+ height: thumbnail.height ?? null,
1217
+ byteLength: thumbnail.byteLength ?? null,
1218
+ capturedAt: thumbnail.capturedAt ?? null,
1219
+ dataUrl: Boolean(thumbnail.dataUrl),
1220
+ } : null,
1221
+ networkData: envelope?.payload?.networkData ? {
1222
+ ...envelope.payload.networkData,
1223
+ data: undefined,
1224
+ byteLength: envelope.payload.networkData.byteLength
1225
+ ?? envelope.payload.networkData.data?.byteLength
1226
+ ?? null,
1227
+ } : null,
1228
+ positionData: envelope?.payload?.positionData ? {
1229
+ ...envelope.payload.positionData,
1230
+ data: undefined,
1231
+ byteLength: envelope.payload.positionData.byteLength
1232
+ ?? envelope.payload.positionData.data?.byteLength
1233
+ ?? null,
1234
+ storedByteLength: envelope.payload.positionData.storedByteLength
1235
+ ?? envelope.payload.positionData.data?.byteLength
1236
+ ?? null,
1237
+ } : null,
1238
+ };
1239
+ };
1240
+
1241
+ const scheduleSave = (options = {}) => {
1242
+ if (options.enabled === false) return pendingSave;
1243
+ const delay = Number.isFinite(options.delayMs) ? Math.max(0, Number(options.delayMs)) : 500;
1244
+ if (saveTimer) clearTimeout(saveTimer);
1245
+ saveTimer = setTimeout(() => {
1246
+ saveTimer = null;
1247
+ pendingSave = save(options);
1248
+ }, delay);
1249
+ return pendingSave;
1250
+ };
1251
+
1252
+ const restore = async (options = {}) => {
1253
+ const restored = await helios.storage?.restoreSession?.(persistenceId, {
1254
+ markFinished: false,
1255
+ disposeOld: true,
1256
+ recreateRenderer: true,
1257
+ restoreVisualizationState: options.restoreVisualizationState,
1258
+ reason: options.reason ?? 'cli-session-restore',
1259
+ });
1260
+ if (restored) {
1261
+ helios.requestRender?.();
1262
+ return {
1263
+ storage: 'cli-filesystem',
1264
+ id: restored?.id ?? restored?.payload?.session?.id ?? persistenceId,
1265
+ updatedAt: restored?.payload?.session?.updatedAt ?? null,
1266
+ };
1267
+ }
1268
+ return null;
1269
+ };
1270
+
1271
+ const clear = async () => {
1272
+ if (saveTimer) clearTimeout(saveTimer);
1273
+ saveTimer = null;
1274
+ await helios.storage?.deleteSession?.(persistenceId);
1275
+ return { cleared: true, id: persistenceId };
1276
+ };
1277
+
1278
+ const flush = async () => {
1279
+ if (saveTimer) {
1280
+ clearTimeout(saveTimer);
1281
+ saveTimer = null;
1282
+ pendingSave = save({ fullSession: false });
1283
+ }
1284
+ return pendingSave;
1285
+ };
1286
+
1287
+ return {
1288
+ id: persistenceId,
1289
+ save,
1290
+ scheduleSave,
1291
+ restore,
1292
+ clear,
1293
+ flush,
1294
+ isRestoring: () => false,
1295
+ };
1296
+ }
1297
+
1298
+ async function waitForSessionBaselineIdle() {
1299
+ if (typeof requestAnimationFrame !== 'function') {
1300
+ await new Promise((resolve) => setTimeout(resolve, 100));
1301
+ return;
1302
+ }
1303
+ await new Promise((resolve) => requestAnimationFrame(() => resolve()));
1304
+ await new Promise((resolve) => requestAnimationFrame(() => resolve()));
1305
+ await new Promise((resolve) => setTimeout(resolve, 150));
1306
+ }
1307
+
1308
+ function buildMapper(mode, network, descriptor) {
1309
+ if (!descriptor) return null;
1310
+ const mapper = new Mapper({ mode, network });
1311
+ const entries = Array.isArray(descriptor.channels)
1312
+ ? descriptor.channels.map((entry) => [entry.name, entry.config ?? entry])
1313
+ : Object.entries(descriptor);
1314
+ for (const [name, config] of entries) {
1315
+ if (!name || !config) continue;
1316
+ mapper.setChannel(name, config);
1317
+ }
1318
+ return mapper;
1319
+ }
1320
+
1321
+ function compileMapperFunction(source, label) {
1322
+ if (typeof source !== 'string' || !source.trim()) return null;
1323
+ // Agents can pass either an expression or a function body. Arguments mirror
1324
+ // Mapper.js custom callbacks: inputs, item, context.
1325
+ const body = source.trim();
1326
+ if (/\breturn\b/.test(body) || /[;{}]/.test(body)) {
1327
+ return new Function('inputs', 'item', 'context', body);
1328
+ }
1329
+ return new Function('inputs', 'item', 'context', `return (${body});`);
1330
+ }
1331
+
1332
+ function hydrateMapperFunctionConfig(config, label = 'mapper') {
1333
+ if (Array.isArray(config)) return config.map((entry, index) => hydrateMapperFunctionConfig(entry, `${label}[${index}]`));
1334
+ if (!config || typeof config !== 'object') return config;
1335
+ const next = {};
1336
+ for (const [key, value] of Object.entries(config)) {
1337
+ if (key === 'transformCode') {
1338
+ next.transform = compileMapperFunction(value, `${label}.transformCode`);
1339
+ next.meta = { ...(next.meta ?? config.meta ?? {}), transformCode: value };
1340
+ continue;
1341
+ }
1342
+ if (key === 'scaleCode') {
1343
+ next.scale = compileMapperFunction(value, `${label}.scaleCode`);
1344
+ next.meta = { ...(next.meta ?? config.meta ?? {}), scaleCode: value };
1345
+ continue;
1346
+ }
1347
+ if (key === 'whenCode') {
1348
+ next.when = compileMapperFunction(value, `${label}.whenCode`);
1349
+ next.meta = { ...(next.meta ?? config.meta ?? {}), whenCode: value };
1350
+ continue;
1351
+ }
1352
+ next[key] = hydrateMapperFunctionConfig(value, `${label}.${key}`);
1353
+ }
1354
+ return next;
1355
+ }
1356
+
1357
+ function buildMapperWithFunctions(mode, network, descriptor) {
1358
+ return buildMapper(mode, network, hydrateMapperFunctionConfig(descriptor, `${mode}Mapper`));
1359
+ }
1360
+
1361
+ function serializeMetricResult(result, { includeValuesByNode = false } = {}) {
1362
+ const normalized = cloneJsonSafe(result);
1363
+ if (!includeValuesByNode && normalized && typeof normalized === 'object') {
1364
+ delete normalized.valuesByNode;
1365
+ }
1366
+ return normalized;
1367
+ }
1368
+
1369
+ async function runSteppableSession(session, options = {}) {
1370
+ const budget = Math.max(1, Number(options.budget ?? 500) || 500);
1371
+ const maxSteps = Math.max(1, Number(options.maxSteps ?? 10000) || 10000);
1372
+ let progress = null;
1373
+ let steps = 0;
1374
+ while (steps < maxSteps) {
1375
+ progress = session.step({ budget });
1376
+ steps += 1;
1377
+ const phase = Number(progress?.phase ?? progress?.status ?? 0);
1378
+ if (progress?.done === true || phase === 3 || phase === 5) break;
1379
+ if (steps % 20 === 0) await new Promise((resolve) => setTimeout(resolve, 0));
1380
+ }
1381
+ if (steps >= maxSteps) throw new Error(`Metric session did not finish within ${maxSteps} steps`);
1382
+ const result = session.finalize(options.finalize ?? {});
1383
+ session.dispose?.();
1384
+ return { steps, progress: cloneJsonSafe(progress), result };
1385
+ }
1386
+
1387
+ async function measureNetworkMetric(network, params = {}) {
1388
+ const metric = String(params.metric ?? params.name ?? params.measure ?? '').trim();
1389
+ const options = params.options ?? { ...params };
1390
+ delete options.metric;
1391
+ delete options.name;
1392
+ delete options.measure;
1393
+ const includeValuesByNode = params.includeValuesByNode === true || options.includeValuesByNode === true;
1394
+ delete options.includeValuesByNode;
1395
+
1396
+ switch (metric) {
1397
+ case 'degree':
1398
+ return serializeMetricResult(network.measureDegree(options), { includeValuesByNode });
1399
+ case 'strength':
1400
+ return serializeMetricResult(network.measureStrength(options), { includeValuesByNode });
1401
+ case 'localClustering':
1402
+ case 'localClusteringCoefficient':
1403
+ case 'clustering':
1404
+ return serializeMetricResult(network.measureLocalClusteringCoefficient(options), { includeValuesByNode });
1405
+ case 'coreness':
1406
+ return serializeMetricResult(network.measureCoreness(options), { includeValuesByNode });
1407
+ case 'eigenvector':
1408
+ case 'eigenvectorCentrality':
1409
+ return serializeMetricResult(network.measureEigenvectorCentrality(options), { includeValuesByNode });
1410
+ case 'betweenness':
1411
+ case 'betweennessCentrality':
1412
+ return serializeMetricResult(network.measureBetweennessCentrality(options), { includeValuesByNode });
1413
+ case 'connectedComponents':
1414
+ case 'components':
1415
+ return serializeMetricResult(network.measureConnectedComponents(options), { includeValuesByNode });
1416
+ case 'dimension':
1417
+ return serializeMetricResult(network.measureDimension(options), { includeValuesByNode: true });
1418
+ case 'nodeDimension':
1419
+ return serializeMetricResult(network.measureNodeDimension(params.node ?? options.node, options), { includeValuesByNode: true });
1420
+ case 'leiden':
1421
+ case 'leidenModularity':
1422
+ return serializeMetricResult(network.leidenModularity(options), { includeValuesByNode: true });
1423
+ case 'corenessSession': {
1424
+ const session = network.createCorenessSession(options);
1425
+ return serializeMetricResult(await runSteppableSession(session, params), { includeValuesByNode });
1426
+ }
1427
+ case 'connectedComponentsSession': {
1428
+ const session = network.createConnectedComponentsSession(options);
1429
+ return serializeMetricResult(await runSteppableSession(session, params), { includeValuesByNode });
1430
+ }
1431
+ case 'dimensionSession': {
1432
+ const session = network.createDimensionSession(options);
1433
+ return serializeMetricResult(await runSteppableSession(session, params), { includeValuesByNode: true });
1434
+ }
1435
+ default:
1436
+ throw new Error(`Unknown metric "${metric}". Use degree, strength, localClustering, coreness, eigenvectorCentrality, betweennessCentrality, connectedComponents, dimension, nodeDimension, or leiden.`);
1437
+ }
1438
+ }
1439
+
1440
+ const MUTATING_METHODS = new Set([
1441
+ 'network.attributeSet',
1442
+ 'network.loadPayload',
1443
+ 'network.replace',
1444
+ 'scene.requestRender',
1445
+ 'scene.setMode',
1446
+ 'camera.setPose',
1447
+ 'camera.transition',
1448
+ 'camera.frame',
1449
+ 'camera.controls',
1450
+ 'camera.targetNodes',
1451
+ 'layout.set',
1452
+ 'layout.setParameters',
1453
+ 'layout.applyPositionAttribute',
1454
+ 'layout.start',
1455
+ 'layout.stop',
1456
+ 'mappers.set',
1457
+ 'mappers.reset',
1458
+ 'behaviors.use',
1459
+ 'behaviors.detach',
1460
+ 'behaviors.setEnabled',
1461
+ 'behaviors.update',
1462
+ 'behaviors.restore',
1463
+ 'behaviors.call',
1464
+ 'positions.set',
1465
+ 'positions.fromAttribute',
1466
+ 'filters.set',
1467
+ 'filters.clear',
1468
+ 'labels.set',
1469
+ 'legends.set',
1470
+ 'density.set',
1471
+ 'metrics.measure',
1472
+ 'aesthetic.measure',
1473
+ ]);
1474
+
1475
+ class BrowserBridge {
1476
+ constructor(socket, helios, ui) {
1477
+ this.socket = socket;
1478
+ this.helios = helios;
1479
+ this.ui = ui;
1480
+ this.persistence = window.__HELIOS_CLI_PERSISTENCE__ ?? null;
1481
+ this.checkpointSeq = 0;
1482
+ this.handlers = this.buildHandlers();
1483
+ this.unsubscribers = [];
1484
+ this.bindEvents();
1485
+ setTimeout(() => this.snapshotPersistenceState('bridge-ready'), 0);
1486
+ }
1487
+
1488
+ bindEvents() {
1489
+ const forward = (type, detail) => {
1490
+ this.notify('bridge.event', { type, detail });
1491
+ };
1492
+ const on = (eventName, type = eventName) => {
1493
+ const off = this.helios.on(eventName, (event) => {
1494
+ forward(type, event?.detail ?? null);
1495
+ });
1496
+ this.unsubscribers.push(off);
1497
+ };
1498
+ on(EVENTS.MODE_CHANGED, 'helios.modeChanged');
1499
+ on(EVENTS.NETWORK_REPLACED, 'helios.networkReplaced');
1500
+ on(EVENTS.GRAPH_FILTER_CHANGED, 'helios.graphFilterChanged');
1501
+ on(EVENTS.LAYOUT_START, 'helios.layoutStart');
1502
+ on(EVENTS.LAYOUT_STOP, 'helios.layoutStop');
1503
+ on(EVENTS.CAMERA_MOVE, 'helios.cameraMove');
1504
+
1505
+ if (this.persistence) {
1506
+ const schedule = () => this.persistence.scheduleSave({ fullSession: false, delayMs: 750 });
1507
+ for (const behavior of this.helios.behaviors?.values?.() ?? []) {
1508
+ if (typeof behavior?.on === 'function') this.unsubscribers.push(behavior.on('change', schedule));
1509
+ }
1510
+ this.unsubscribers.push(this.helios.on(EVENTS.MODE_CHANGED, schedule));
1511
+ this.unsubscribers.push(this.helios.on(EVENTS.NETWORK_REPLACED, schedule));
1512
+ this.unsubscribers.push(this.helios.on(EVENTS.GRAPH_FILTER_CHANGED, schedule));
1513
+ this.unsubscribers.push(this.helios.on(EVENTS.LAYOUT_STOP, schedule));
1514
+ this.unsubscribers.push(this.helios.on(EVENTS.CAMERA_MOVE, () => {
1515
+ this.persistence.scheduleSave({ fullSession: false, delayMs: 1000 });
1516
+ }));
1517
+ }
1518
+ }
1519
+
1520
+ notify(method, params) {
1521
+ this.socket.send(JSON.stringify({ jsonrpc: '2.0', method, params }));
1522
+ }
1523
+
1524
+ snapshotPersistenceState(reason = 'snapshot') {
1525
+ const status = this.helios.storage?.persistenceStatus?.() ?? null;
1526
+ const overrides = this.helios.states?.getOverrides?.({ aliases: 'preferred' }) ?? {};
1527
+ const dirtyState = this.helios.states?.dirtyState?.() ?? { controls: {}, sections: {}, panels: {} };
1528
+ const journal = this.helios.states?.journal ?? [];
1529
+ this.notify('bridge.event', {
1530
+ type: 'persistence.snapshot',
1531
+ detail: {
1532
+ reason,
1533
+ persistenceId: this.persistence?.id ?? status?.sessionId ?? null,
1534
+ storage: {
1535
+ cli: 'filesystem',
1536
+ },
1537
+ status,
1538
+ backendStatus: [],
1539
+ overrides,
1540
+ dirtyState,
1541
+ journal,
1542
+ checkpointSeq: this.checkpointSeq,
1543
+ networkData: status?.networkData ?? null,
1544
+ savedAt: Date.now(),
1545
+ },
1546
+ });
1547
+ }
1548
+
1549
+ async handleMessage(raw) {
1550
+ const message = JSON.parse(String(raw));
1551
+ if (!message?.method) return;
1552
+ const handler = this.handlers[message.method];
1553
+ if (!handler) {
1554
+ this.socket.send(JSON.stringify({
1555
+ jsonrpc: '2.0',
1556
+ id: message.id ?? null,
1557
+ error: { code: -32601, message: `Unknown bridge method ${message.method}` },
1558
+ }));
1559
+ return;
1560
+ }
1561
+ try {
1562
+ const mutates = MUTATING_METHODS.has(message.method);
1563
+ const execute = () => handler(message.params ?? {});
1564
+ const result = await execute();
1565
+ const networkMutation = message.method === 'network.attributeSet'
1566
+ || message.method === 'network.loadPayload'
1567
+ || message.method === 'network.replace';
1568
+ const positionMutation = message.method === 'positions.set'
1569
+ || message.method === 'positions.fromAttribute'
1570
+ || message.method === 'layout.applyPositionAttribute';
1571
+ if (mutates) {
1572
+ if (networkMutation) {
1573
+ this.helios.storage?.markNetworkDirty?.(message.method);
1574
+ }
1575
+ if (positionMutation) {
1576
+ this.helios.storage?.markPositionsDirty?.(message.method);
1577
+ }
1578
+ }
1579
+ if (this.persistence && mutates) {
1580
+ await this.persistence.save({
1581
+ fullSession: networkMutation,
1582
+ includePositions: positionMutation,
1583
+ reason: message.method,
1584
+ });
1585
+ this.snapshotPersistenceState(message.method);
1586
+ }
1587
+ this.socket.send(JSON.stringify({ jsonrpc: '2.0', id: message.id ?? null, result }));
1588
+ } catch (error) {
1589
+ this.socket.send(JSON.stringify({
1590
+ jsonrpc: '2.0',
1591
+ id: message.id ?? null,
1592
+ error: { code: error?.code ?? -32000, message: error?.message ?? String(error) },
1593
+ }));
1594
+ }
1595
+ }
1596
+
1597
+ buildHandlers() {
1598
+ const readPersistenceStatus = () => {
1599
+ const status = this.helios.storage?.persistenceStatus?.() ?? null;
1600
+ if (!status) return null;
1601
+ const journal = this.helios.states?.journal ?? [];
1602
+ const maxSeq = Math.max(0, ...journal.map((entry) => Number(entry.seq ?? 0)));
1603
+ const dirtyByJournal = maxSeq > this.checkpointSeq;
1604
+ const networkData = status.networkData ?? {};
1605
+ return {
1606
+ ...status,
1607
+ backendStatus: [],
1608
+ journalCount: maxSeq,
1609
+ checkpointSeq: this.checkpointSeq,
1610
+ hasUnsavedChanges: dirtyByJournal
1611
+ || networkData.dirty === true
1612
+ || networkData.positionsDirty === true
1613
+ || status.sessionSync?.pending === true,
1614
+ };
1615
+ };
1616
+ const stateJournal = (params = {}) => {
1617
+ let entries = Array.isArray(this.helios.states?.journal) ? this.helios.states.journal : [];
1618
+ if (params.sinceCheckpoint !== false) {
1619
+ entries = entries.filter((entry) => Number(entry.seq ?? 0) > this.checkpointSeq);
1620
+ }
1621
+ if (params.since != null) {
1622
+ entries = entries.filter((entry) => Number(entry.seq ?? 0) > Number(params.since));
1623
+ }
1624
+ if (params.source) entries = entries.filter((entry) => entry.source === params.source);
1625
+ if (Number.isFinite(params.limit)) entries = entries.slice(-Math.max(0, Number(params.limit)));
1626
+ const aliases = params.aliases ?? 'preferred';
1627
+ return cloneJsonSafe(entries.map((entry) => {
1628
+ if (!(aliases === true || aliases === 'preferred')) return entry;
1629
+ const preferred = this.helios.states?.preferredKey?.(entry.key ?? entry.path);
1630
+ if (!preferred || preferred === entry.path) return entry;
1631
+ return { ...entry, canonicalPath: entry.path, path: preferred };
1632
+ }));
1633
+ };
1634
+ return {
1635
+ 'session.getInfo': async () => getSceneState(this.helios),
1636
+ 'state.get': async (params) => {
1637
+ const path = params.path ?? params.key ?? null;
1638
+ if (!path) {
1639
+ return {
1640
+ snapshot: cloneJsonSafe(this.helios.states?.snapshot?.({ aliases: params.aliases ?? 'preferred', includeJournal: params.includeJournal === true }) ?? null),
1641
+ status: readPersistenceStatus(),
1642
+ };
1643
+ }
1644
+ return {
1645
+ path,
1646
+ value: cloneJsonSafe(this.helios.states?.get?.(path)),
1647
+ status: cloneJsonSafe(this.helios.states?.status?.(path) ?? null),
1648
+ entry: cloneJsonSafe(this.helios.states?.entry?.(path) ?? null),
1649
+ };
1650
+ },
1651
+ 'state.set': async (params) => {
1652
+ const path = params.path ?? params.key;
1653
+ if (!path) throw new Error('state.set requires params.path');
1654
+ const result = this.helios.states?.set?.(path, params.value, {
1655
+ source: 'cli',
1656
+ reason: params.reason ?? 'cli-state-set',
1657
+ scope: params.scope ?? 'session',
1658
+ trackOverride: params.trackOverride !== false,
1659
+ debounceMs: params.debounceMs ?? 0,
1660
+ });
1661
+ await this.persistence?.save?.({ fullSession: false, reason: params.reason ?? 'state.set' });
1662
+ this.snapshotPersistenceState('state.set');
1663
+ return {
1664
+ result: cloneJsonSafe(result),
1665
+ value: cloneJsonSafe(this.helios.states?.get?.(path)),
1666
+ status: cloneJsonSafe(this.helios.states?.status?.(path) ?? null),
1667
+ };
1668
+ },
1669
+ 'state.reset': async (params) => {
1670
+ const path = params.path ?? params.key ?? params.scope;
1671
+ if (!path) throw new Error('state.reset requires params.path');
1672
+ const result = this.helios.states?.reset?.(path, {
1673
+ source: 'cli',
1674
+ reason: params.reason ?? 'cli-state-reset',
1675
+ });
1676
+ await this.persistence?.save?.({ fullSession: false, reason: params.reason ?? 'state.reset' });
1677
+ this.snapshotPersistenceState('state.reset');
1678
+ return cloneJsonSafe(result);
1679
+ },
1680
+ 'persistence.get': async () => ({
1681
+ id: this.persistence?.id ?? null,
1682
+ available: Boolean(this.persistence),
1683
+ status: readPersistenceStatus(),
1684
+ backendStatus: [],
1685
+ }),
1686
+ 'persistence.save': async (params) => {
1687
+ const result = await (this.persistence?.save({
1688
+ fullSession: params.fullSession !== false,
1689
+ networkFormat: params.networkFormat ?? 'zxnet',
1690
+ captureThumbnail: Object.hasOwn(params, 'captureThumbnail') ? params.captureThumbnail : undefined,
1691
+ thumbnail: params.thumbnail ?? params.sessionThumbnail,
1692
+ }) ?? { saved: false });
1693
+ this.snapshotPersistenceState('persistence.save');
1694
+ return result;
1695
+ },
1696
+ 'persistence.restore': async (params) => {
1697
+ const result = await (this.persistence?.restore(params) ?? { restored: false });
1698
+ this.snapshotPersistenceState('persistence.restore');
1699
+ return result;
1700
+ },
1701
+ 'persistence.clear': async () => {
1702
+ const result = await (this.persistence?.clear() ?? { cleared: false });
1703
+ this.snapshotPersistenceState('persistence.clear');
1704
+ return result;
1705
+ },
1706
+ 'persistence.changes': async (params) => stateJournal(params),
1707
+ 'persistence.checkpoint': async (params) => {
1708
+ const maxSeq = Math.max(0, ...((this.helios.states?.journal ?? []).map((entry) => Number(entry.seq ?? 0))));
1709
+ this.checkpointSeq = Number.isFinite(Number(params.seq)) ? Number(params.seq) : maxSeq;
1710
+ const result = { checkpointSeq: this.checkpointSeq };
1711
+ this.snapshotPersistenceState('persistence.checkpoint');
1712
+ return result;
1713
+ },
1714
+ 'persistence.overrides': async () => ({
1715
+ overrides: this.helios.states?.getOverrides?.({ aliases: 'preferred' }) ?? {},
1716
+ dirtyState: this.helios.states?.dirtyState?.() ?? { controls: {}, sections: {}, panels: {} },
1717
+ }),
1718
+ 'persistence.reset': async (params) => {
1719
+ const result = this.helios.states?.reset?.(params.path ?? params.scope, {
1720
+ source: 'cli',
1721
+ reason: params.reason ?? 'persistence.reset',
1722
+ }) ?? { reset: false };
1723
+ await this.persistence?.save?.({ fullSession: false, reason: 'persistence.reset' });
1724
+ this.snapshotPersistenceState('persistence.reset');
1725
+ return result;
1726
+ },
1727
+ 'persistence.flush': async (params) => {
1728
+ const result = await (this.helios.storage?.flush?.({
1729
+ includeNetwork: params.includeNetwork === true,
1730
+ includePositions: params.includePositions === true,
1731
+ snapshotLayoutRuntime: params.snapshotLayoutRuntime !== false,
1732
+ network: params.network ?? {},
1733
+ networkFormat: params.networkFormat ?? params.network?.format ?? 'zxnet',
1734
+ captureThumbnail: params.captureThumbnail,
1735
+ thumbnail: params.thumbnail ?? params.sessionThumbnail,
1736
+ reason: params.reason ?? 'persistence.flush',
1737
+ }) ?? null);
1738
+ this.snapshotPersistenceState('persistence.flush');
1739
+ return result;
1740
+ },
1741
+ 'persistence.status': async () => readPersistenceStatus(),
1742
+ 'persistence.backendStatus': async () => [],
1743
+ 'persistence.exportDocumentState': async (params) => cloneJsonSafe(
1744
+ await this.helios.storage?.serializeNetworkSnapshot?.({
1745
+ reason: params.reason ?? 'desktop-document-save',
1746
+ includeCurrentPositions: params.includeCurrentPositions !== false,
1747
+ trackedOnly: params.trackedOnly !== false,
1748
+ fullVisualizationState: params.fullVisualizationState === true,
1749
+ }) ?? null,
1750
+ ),
1751
+ 'persistence.restoreDocumentState': async (params) => {
1752
+ const snapshot = params.visualizationState ?? params.snapshot ?? params;
1753
+ if (!snapshot) return null;
1754
+ if (this.helios.importVisualizationState) {
1755
+ await this.helios.importVisualizationState(snapshot, {
1756
+ restoreLayoutRunState: params.restoreLayoutRunState !== false,
1757
+ hydratePersistence: false,
1758
+ refreshPersistence: false,
1759
+ source: 'restore',
1760
+ reason: params.reason ?? 'desktop-document-restore',
1761
+ });
1762
+ } else if (snapshot?.payload?.storageState) {
1763
+ this.helios.storage?.restoreSnapshot?.(snapshot.payload.storageState, {
1764
+ source: 'restore',
1765
+ reason: params.reason ?? 'desktop-document-restore',
1766
+ });
1767
+ }
1768
+ reapplyRestoredStateBindings(this.helios, 'desktop-document-restore-bindings');
1769
+ this.helios.requestRender?.();
1770
+ this.snapshotPersistenceState('persistence.restoreDocumentState');
1771
+ return { restored: true };
1772
+ },
1773
+ 'persistence.documentSaved': async (params) => {
1774
+ const storage = this.helios.storage ?? null;
1775
+ const maxSeq = Math.max(0, ...((this.helios.states?.journal ?? []).map((entry) => Number(entry.seq ?? 0))));
1776
+ this.checkpointSeq = maxSeq;
1777
+ if (storage?.networkData) {
1778
+ const savedAt = Date.now();
1779
+ storage.sessionSavedAt = savedAt;
1780
+ storage.sessionSaveError = null;
1781
+ storage.networkData = {
1782
+ ...storage.networkData,
1783
+ enabled: true,
1784
+ status: 'saved',
1785
+ dirty: false,
1786
+ positionsDirty: false,
1787
+ dirtyAt: null,
1788
+ savedAt,
1789
+ format: params.format ?? storage.networkData.format ?? null,
1790
+ documentPath: params.filePath ?? storage.networkData.documentPath ?? null,
1791
+ };
1792
+ storage._pendingStateOverrideDeltas?.clear?.();
1793
+ storage.dispatchEvent?.(new CustomEvent('change', {
1794
+ detail: { reason: params.reason ?? 'document-saved', status: storage.persistenceStatus?.() ?? null },
1795
+ }));
1796
+ }
1797
+ this.snapshotPersistenceState(params.reason ?? 'persistence.documentSaved');
1798
+ return readPersistenceStatus();
1799
+ },
1800
+ 'network.stats': async () => getNetworkStats(this.helios),
1801
+ 'network.inspect': async () => getNetworkStats(this.helios),
1802
+ 'network.attributeSet': async (params) => {
1803
+ writeNetworkAttribute(this.helios.network, params);
1804
+ if (params.applyAsPositions === true || params.positionAttribute === true) {
1805
+ applyPositionsFromAttribute(this.helios, { attribute: params.name ?? params.attribute });
1806
+ }
1807
+ this.helios.requestRender?.();
1808
+ return getNetworkStats(this.helios);
1809
+ },
1810
+ 'network.loadPayload': async (params) => {
1811
+ const file = fileFromBase64({
1812
+ name: params.name ?? `network.${params.format ?? 'bxnet'}`,
1813
+ base64: params.base64,
1814
+ });
1815
+ await this.helios.loadNetwork(file, {
1816
+ format: params.format,
1817
+ disposeOld: true,
1818
+ recreateRenderer: true,
1819
+ keepCamera: false,
1820
+ ...(params.options ?? {}),
1821
+ });
1822
+ return getSceneState(this.helios);
1823
+ },
1824
+ 'network.replace': async (params) => {
1825
+ if (params.base64) {
1826
+ return this.handlers['network.loadPayload'](params);
1827
+ }
1828
+ if (params.synthetic) {
1829
+ const network = await createSyntheticNetwork({
1830
+ ...params.synthetic,
1831
+ mode: params.synthetic.mode ?? this.helios.mode(),
1832
+ layout: params.synthetic.layout ?? identifyLayout(this.helios.layout()),
1833
+ });
1834
+ await this.helios.replaceNetwork(network, params.options ?? {});
1835
+ return getSceneState(this.helios);
1836
+ }
1837
+ throw new Error('network.replace requires a base64 payload or synthetic descriptor');
1838
+ },
1839
+ 'network.savePayload': async (params) => {
1840
+ const format = params.format ?? 'bxnet';
1841
+ const blob = await this.helios.savePortableNetwork(format, {
1842
+ output: 'blob',
1843
+ includeVisualization: params.includeVisualization === true,
1844
+ trackedOnly: params.trackedOnly === true,
1845
+ includeCurrentPositions: params.includeCurrentPositions === true,
1846
+ fullVisualizationState: params.fullVisualizationState === true,
1847
+ });
1848
+ return {
1849
+ format,
1850
+ mimeType: blob.type || 'application/octet-stream',
1851
+ filename: params.filename ?? `network.${format}`,
1852
+ base64: await blobToBase64(blob),
1853
+ };
1854
+ },
1855
+ 'scene.getState': async () => getSceneState(this.helios),
1856
+ 'scene.requestRender': async () => {
1857
+ this.helios.requestRender();
1858
+ return getSceneState(this.helios);
1859
+ },
1860
+ 'scene.setMode': async (params) => {
1861
+ await this.helios.setMode(params.mode, {
1862
+ ...(params.options ?? {}),
1863
+ source: 'cli',
1864
+ reason: params.reason ?? 'scene.setMode',
1865
+ trackOverride: params.trackOverride ?? true,
1866
+ });
1867
+ return getSceneState(this.helios);
1868
+ },
1869
+ 'camera.getPose': async () => cloneJsonSafe(this.helios.cameraPose()),
1870
+ 'camera.setPose': async (params) => {
1871
+ this.helios.setCameraPose(params.pose ?? params, {
1872
+ ...(params.options ?? {}),
1873
+ source: 'cli',
1874
+ reason: params.reason ?? 'camera.setPose',
1875
+ });
1876
+ return cloneJsonSafe(this.helios.cameraPose());
1877
+ },
1878
+ 'camera.transition': async (params) => {
1879
+ await this.helios.transitionCamera(params.pose ?? params, {
1880
+ ...(params.options ?? {}),
1881
+ source: 'cli',
1882
+ reason: params.reason ?? 'camera.transition',
1883
+ });
1884
+ return cloneJsonSafe(this.helios.cameraPose());
1885
+ },
1886
+ 'camera.frame': async (params) => {
1887
+ const ok = this.helios.frameNetwork(params ?? {});
1888
+ return { ok, pose: cloneJsonSafe(this.helios.cameraPose()) };
1889
+ },
1890
+ 'camera.controls': async (params) => {
1891
+ if (!params || Object.keys(params).length === 0) return cloneJsonSafe(this.helios.cameraControls());
1892
+ this.helios.cameraControls(params, {
1893
+ source: 'cli',
1894
+ reason: params.reason ?? 'camera.controls',
1895
+ });
1896
+ return cloneJsonSafe(this.helios.cameraControls());
1897
+ },
1898
+ 'camera.targetNodes': async (params) => {
1899
+ if (!params || !Object.prototype.hasOwnProperty.call(params, 'nodeIndices')) {
1900
+ return cloneJsonSafe(this.helios.cameraTargetNodes());
1901
+ }
1902
+ this.helios.cameraTargetNodes(params.nodeIndices, params.options ?? {});
1903
+ return cloneJsonSafe(this.helios.cameraTargetNodes());
1904
+ },
1905
+ 'layout.get': async () => getLayoutState(this.helios),
1906
+ 'layout.set': async (params) => {
1907
+ const key = params.layout ?? params.key;
1908
+ if (this.helios.states?.entry?.('layout.layoutType')) {
1909
+ this.helios.states.set('layout.layoutType', normalizeLayoutKey(key), {
1910
+ source: 'cli',
1911
+ reason: params.reason ?? 'layout.set',
1912
+ scope: params.scope ?? 'network',
1913
+ trackOverride: params.trackOverride !== false,
1914
+ });
1915
+ return getLayoutState(this.helios);
1916
+ }
1917
+ const instance = this.helios.createLayout(buildLayoutOptions(this.helios, key));
1918
+ this.helios.layout(instance);
1919
+ return getLayoutState(this.helios);
1920
+ },
1921
+ 'layout.setParameters': async (params) => applyLayoutParameters(this.helios, params),
1922
+ 'layout.applyPositionAttribute': async (params) => applyPositionsFromAttribute(this.helios, params),
1923
+ 'layout.start': async (params) => {
1924
+ this.helios.startLayout(params?.algo ?? null, params?.params ?? null);
1925
+ return getLayoutState(this.helios);
1926
+ },
1927
+ 'layout.stop': async (params) => {
1928
+ this.helios.stopLayout(params?.reason ?? 'user');
1929
+ return getLayoutState(this.helios);
1930
+ },
1931
+ 'mappers.get': async () => ({
1932
+ node: serializeMapperCollection(this.helios.nodeMapper),
1933
+ edge: serializeMapperCollection(this.helios.edgeMapper),
1934
+ }),
1935
+ 'mappers.set': async (params) => {
1936
+ const payload = {};
1937
+ if (params.nodeMapper) payload.nodeMapper = buildMapperWithFunctions('node', this.helios.network, params.nodeMapper);
1938
+ if (params.edgeMapper) payload.edgeMapper = buildMapperWithFunctions('edge', this.helios.network, params.edgeMapper);
1939
+ this.helios.mappers(payload);
1940
+ for (const [mode, descriptor] of [['node', params.nodeMapper], ['edge', params.edgeMapper]]) {
1941
+ if (!descriptor || typeof descriptor !== 'object') continue;
1942
+ for (const [channel, config] of Object.entries(descriptor)) {
1943
+ setRegisteredCliState(this.helios, [
1944
+ `mappers.${mode}.${channel}`,
1945
+ `behaviors.mappers.${mode}.${channel}`,
1946
+ ], cloneJsonSafe(config), {
1947
+ reason: params.reason ?? 'mappers.set',
1948
+ scope: params.scope ?? 'network',
1949
+ trackOverride: params.trackOverride,
1950
+ applyBinding: false,
1951
+ });
1952
+ }
1953
+ }
1954
+ return {
1955
+ node: serializeMapperCollection(this.helios.nodeMapper),
1956
+ edge: serializeMapperCollection(this.helios.edgeMapper),
1957
+ };
1958
+ },
1959
+ 'mappers.reset': async () => {
1960
+ this.helios.mappers({ nodeMapper: null, edgeMapper: null });
1961
+ this.helios.states?.reset?.('mappers', { source: 'cli', reason: 'mappers.reset' });
1962
+ return {
1963
+ node: serializeMapperCollection(this.helios.nodeMapper),
1964
+ edge: serializeMapperCollection(this.helios.edgeMapper),
1965
+ };
1966
+ },
1967
+ 'behaviors.get': async () => getBehaviorState(this.helios),
1968
+ 'behaviors.use': async (params) => {
1969
+ const id = params.id ?? params.behavior;
1970
+ const behavior = this.helios.useBehavior(id, params.options ?? true);
1971
+ return serializeBehavior(behavior);
1972
+ },
1973
+ 'behaviors.detach': async (params) => {
1974
+ const id = params.id ?? params.behavior;
1975
+ const detached = this.helios.behaviors?.detach?.(id) === true;
1976
+ this.helios.requestRender?.();
1977
+ return { detached, behaviors: getBehaviorState(this.helios) };
1978
+ },
1979
+ 'behaviors.setEnabled': async (params) => setBehaviorEnabled(
1980
+ this.helios,
1981
+ params.id ?? params.behavior,
1982
+ params.enabled,
1983
+ params.options ?? {},
1984
+ ),
1985
+ 'behaviors.update': async (params) => {
1986
+ const id = params.id ?? params.behavior;
1987
+ const behavior = this.helios.useBehavior(id, params.options ?? {});
1988
+ trackBehaviorOptionOverrides(this.helios, id, params.options ?? {}, {
1989
+ reason: params.reason ?? 'behaviors.update',
1990
+ scope: params.scope,
1991
+ trackOverride: params.trackOverride,
1992
+ });
1993
+ this.helios.requestRender?.();
1994
+ return serializeBehavior(behavior);
1995
+ },
1996
+ 'behaviors.restore': async (params) => {
1997
+ this.helios.restoreBehaviorState(params.snapshot ?? params);
1998
+ this.helios.requestRender?.();
1999
+ return getBehaviorState(this.helios);
2000
+ },
2001
+ 'behaviors.call': async (params) => invokeBehavior(this.helios, params),
2002
+ 'positions.get': async () => getPositionSourceState(this.helios),
2003
+ 'positions.snapshot': async (params) => snapshotPositions(this.helios, params),
2004
+ 'positions.set': async (params) => setCustomPositions(this.helios, params),
2005
+ 'positions.fromAttribute': async (params) => applyPositionsFromAttribute(this.helios, params),
2006
+ 'filters.get': async () => cloneJsonSafe(this.helios.getGraphFilter()),
2007
+ 'filters.set': async (params) => {
2008
+ this.helios.setGraphFilter(params);
2009
+ setRegisteredCliState(this.helios, ['filters.rules', 'behaviors.filter.rules'], cloneJsonSafe(params), {
2010
+ reason: params.reason ?? 'filters.set',
2011
+ scope: params.scope ?? 'network',
2012
+ trackOverride: params.trackOverride,
2013
+ });
2014
+ return cloneJsonSafe(this.helios.getGraphFilter());
2015
+ },
2016
+ 'filters.clear': async () => {
2017
+ this.helios.clearGraphFilter();
2018
+ this.helios.states?.reset?.('filters', { source: 'cli', reason: 'filters.clear' });
2019
+ return cloneJsonSafe(this.helios.getGraphFilter());
2020
+ },
2021
+ 'labels.get': async () => cloneJsonSafe(this.helios.labels()),
2022
+ 'labels.set': async (params) => {
2023
+ this.helios.labels(params);
2024
+ for (const [path, value] of flattenObjectLeaves(params)) {
2025
+ setRegisteredCliState(this.helios, [`labels.${path}`, `behaviors.labels.${path}`], cloneJsonSafe(value), {
2026
+ reason: params.reason ?? 'labels.set',
2027
+ scope: params.scope ?? 'network',
2028
+ trackOverride: params.trackOverride,
2029
+ });
2030
+ }
2031
+ return cloneJsonSafe(this.helios.labels());
2032
+ },
2033
+ 'legends.get': async () => cloneJsonSafe(this.helios.legends()),
2034
+ 'legends.set': async (params) => {
2035
+ this.helios.legends(params);
2036
+ for (const [path, value] of flattenObjectLeaves(params)) {
2037
+ setRegisteredCliState(this.helios, [`legends.${path}`, `behaviors.legends.${path}`], cloneJsonSafe(value), {
2038
+ reason: params.reason ?? 'legends.set',
2039
+ scope: params.scope ?? 'network',
2040
+ trackOverride: params.trackOverride,
2041
+ });
2042
+ }
2043
+ return cloneJsonSafe(this.helios.legends());
2044
+ },
2045
+ 'density.get': async () => cloneJsonSafe(this.helios.density()),
2046
+ 'density.set': async (params) => {
2047
+ this.helios.density(params);
2048
+ for (const [path, value] of flattenObjectLeaves(params)) {
2049
+ setRegisteredCliState(this.helios, [`density.${path}`, `behaviors.density.${path}`], cloneJsonSafe(value), {
2050
+ reason: params.reason ?? 'density.set',
2051
+ scope: params.scope ?? 'network',
2052
+ trackOverride: params.trackOverride,
2053
+ });
2054
+ }
2055
+ return cloneJsonSafe(this.helios.density());
2056
+ },
2057
+ 'metrics.measure': async (params) => measureNetworkMetric(this.helios.network, params),
2058
+ 'aesthetic.measure': async (params) => measureNetworkMetric(this.helios.network, params),
2059
+ 'picking.pick': async (params) => this.helios.pickAttributesAt(params.x, params.y),
2060
+ 'export.figurePayload': async (params) => {
2061
+ const resolved = resolveFigureRpcOptions(this.helios, params);
2062
+ const blob = await exportFigureRpcBlob(this.helios, params);
2063
+ return {
2064
+ format: resolved.format ?? params.format ?? 'png',
2065
+ mimeType: blob.type || 'application/octet-stream',
2066
+ filename: params.filename ?? resolved.filename ?? `figure.${resolved.format ?? params.format ?? 'png'}`,
2067
+ base64: await blobToBase64(blob),
2068
+ };
2069
+ },
2070
+ 'export.figureOptions': async (params) => {
2071
+ const exporter = findOptionalBehavior(this.helios, 'exporter');
2072
+ return {
2073
+ options: cloneJsonSafe(resolveFigureRpcOptions(this.helios, params)),
2074
+ state: cloneJsonSafe(exporter?.getPublicState?.() ?? null),
2075
+ };
2076
+ },
2077
+ 'events.subscribe': async () => ({ supported: true }),
2078
+ 'events.unsubscribe': async () => ({ supported: true }),
2079
+ };
2080
+ }
2081
+ }
2082
+
2083
+ async function bootstrap() {
2084
+ const config = await fetch('/api/config').then((response) => response.json());
2085
+ const sessionId = config.sessionId ?? new URLSearchParams(window.location.search).get('sessionId') ?? 'unknown';
2086
+ const persistenceId = cliPersistenceId(sessionId);
2087
+ const desktopRuntime = isDesktopRuntime(config);
2088
+ const network = await createSeedNetwork({
2089
+ mode: config.mode === '3d' ? '3d' : '2d',
2090
+ layout: config.layout,
2091
+ });
2092
+ const helios = new Helios(network, {
2093
+ container: document.getElementById('app'),
2094
+ mode: config.mode === '3d' ? '3d' : '2d',
2095
+ clearColor: [0, 0, 0, 1],
2096
+ projection: 'perspective',
2097
+ ui: false,
2098
+ layout: buildLayoutOptions({ network, mode: () => config.mode }, config.layout),
2099
+ renderer: resolveRendererPreference(config.renderer) ?? undefined,
2100
+ workspaceId: persistenceId,
2101
+ storage: desktopRuntime
2102
+ ? {
2103
+ type: 'dummy',
2104
+ workspaceId: persistenceId,
2105
+ sessionId: persistenceId,
2106
+ restore: false,
2107
+ persistNetwork: false,
2108
+ networkPersistence: { enabled: true, autosave: false, format: 'zxnet' },
2109
+ positionPersistence: { enabled: true, autosave: false },
2110
+ autosyncPayloadLimits: config.autosyncPayloadLimits,
2111
+ }
2112
+ : {
2113
+ type: 'remote',
2114
+ workspaceId: persistenceId,
2115
+ sessionId: persistenceId,
2116
+ restore: false,
2117
+ persistNetwork: true,
2118
+ client: new CliStorageClient(),
2119
+ networkPersistence: { enabled: true, autosave: true, format: 'zxnet' },
2120
+ positionPersistence: { enabled: true, autosave: true },
2121
+ autosyncPayloadLimits: config.autosyncPayloadLimits,
2122
+ },
2123
+ networkPersistence: { enabled: true, autosave: !desktopRuntime, format: 'zxnet' },
2124
+ positionPersistence: { enabled: true, autosave: !desktopRuntime },
2125
+ persistNetwork: !desktopRuntime,
2126
+ sessionThumbnail: { enabled: true },
2127
+ session: {
2128
+ id: persistenceId,
2129
+ sessionId: persistenceId,
2130
+ saveInitialManifest: true,
2131
+ restore: !desktopRuntime,
2132
+ networkPersistence: { enabled: true, autosave: !desktopRuntime, format: 'zxnet' },
2133
+ },
2134
+ persistence: false,
2135
+ });
2136
+ await helios.ready;
2137
+ const ui = new HeliosUI({ helios, theme: 'dark', allowDrag: true });
2138
+ window.__helios = helios;
2139
+ window.__heliosUI = ui;
2140
+ publishRuntimeState(helios);
2141
+ helios.on?.(EVENTS.MODE_CHANGED, () => publishRuntimeState(helios));
2142
+ const buildCliInterface = () => {
2143
+ ui.createDemoPanel({
2144
+ showNetworkFileActions: !desktopRuntime,
2145
+ showPersistenceSync: !desktopRuntime,
2146
+ showSessionTab: !desktopRuntime,
2147
+ });
2148
+ ui.createMetricsPanel();
2149
+ ui.createMappersPanel({ dock: 'top-right', position: { x: 16, y: 16 } });
2150
+ ui.createLayoutPanel({ dock: 'top-right', position: { x: 16, y: 360 } });
2151
+ ui.createLegendsPanel({ dock: 'top-right', position: { x: 16, y: 560 } });
2152
+ ui.createFilterPanel({ dock: 'top-right' });
2153
+ ui.createCameraPanel({ dock: 'top-right' });
2154
+ ui.createSelectionPanel({ dock: 'top-right' });
2155
+ helios.enableAttributeTracking('$index', '$index', {
2156
+ resolutionScale: 1,
2157
+ trackDepth: true,
2158
+ autoUpdate: true,
2159
+ autoUpdateMaxFps: 60,
2160
+ });
2161
+ };
2162
+ buildCliInterface();
2163
+ await waitForSessionBaselineIdle();
2164
+ helios.storage?.setOverrideTrackingReady?.(true);
2165
+ const persistence = createCliPersistence({ helios, config });
2166
+ window.__HELIOS_CLI_PERSISTENCE__ = persistence;
2167
+ const restored = desktopRuntime
2168
+ ? null
2169
+ : helios._sessionRestoreResult
2170
+ ? { storage: 'cli-filesystem', id: helios.storage?.persistenceStatus?.()?.sessionId ?? null }
2171
+ : await persistence.restore({ reason: 'page-load' });
2172
+ if (restored) {
2173
+ reapplyRestoredStateBindings(helios, 'cli-session-restore-bindings');
2174
+ console.info('Helios CLI restored persisted session state', restored);
2175
+ } else {
2176
+ await persistence.save({ fullSession: false });
2177
+ }
2178
+ window.addEventListener('beforeunload', () => {
2179
+ try {
2180
+ persistence.save({ fullSession: false });
2181
+ } catch (_) {
2182
+ // best-effort only during page teardown
2183
+ }
2184
+ });
2185
+ publishRuntimeState(helios);
2186
+ const socket = new WebSocket(wsUrlForCurrentLocation());
2187
+ socket.addEventListener('error', (event) => {
2188
+ console.error('Helios CLI bridge socket error', event);
2189
+ });
2190
+ socket.addEventListener('open', () => {
2191
+ const bridge = new BrowserBridge(socket, helios, ui);
2192
+ socket.addEventListener('message', (event) => {
2193
+ bridge.handleMessage(event.data);
2194
+ });
2195
+ socket.send(JSON.stringify({
2196
+ jsonrpc: '2.0',
2197
+ method: 'bridge.ready',
2198
+ params: {
2199
+ mode: helios.mode(),
2200
+ renderer: helios.renderer?.device?.type ?? null,
2201
+ nodeCount: helios.network?.nodeCount ?? 0,
2202
+ edgeCount: helios.network?.edgeCount ?? 0,
2203
+ },
2204
+ }));
2205
+ });
2206
+ }
2207
+
2208
+ bootstrap().catch((error) => {
2209
+ console.error('Failed to bootstrap Helios CLI client', error);
2210
+ });