@lopecode/channel 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-code-pairing-module.js +220 -108
- package/lopecode-channel.ts +93 -3
- package/package.json +2 -2
- package/sync-module.ts +262 -0
- package/inject-module.js +0 -131
|
@@ -23,14 +23,23 @@ const _cc_messages = function _cc_messages(Inputs){return(
|
|
|
23
23
|
)};
|
|
24
24
|
|
|
25
25
|
const _cc_watches = function _cc_watches(Inputs){return(
|
|
26
|
-
Inputs.input([
|
|
26
|
+
Inputs.input([
|
|
27
|
+
{ name: "hash", module: null },
|
|
28
|
+
{ name: "currentModules", module: null }
|
|
29
|
+
])
|
|
30
|
+
)};
|
|
31
|
+
|
|
32
|
+
const _cc_module = function _cc_module(thisModule){return(
|
|
33
|
+
thisModule()
|
|
27
34
|
)};
|
|
28
35
|
|
|
29
|
-
const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages, viewof_cc_watches, summarizeJS, observe, invalidation){return(
|
|
36
|
+
const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages, viewof_cc_watches, summarizeJS, observe, realize, createModule, deleteModule, lookupVariable, cc_module, runtime, invalidation){return(
|
|
30
37
|
(function() {
|
|
31
38
|
var port = cc_config.port, host = cc_config.host;
|
|
32
39
|
var ws = null;
|
|
33
40
|
var paired = false;
|
|
41
|
+
// Cache the observer factory (used by lopepage to render cells)
|
|
42
|
+
var ojs_observer = window.__ojs_observer || null;
|
|
34
43
|
|
|
35
44
|
function serializeValue(value, maxLen) {
|
|
36
45
|
maxLen = maxLen || 500;
|
|
@@ -38,12 +47,25 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
38
47
|
catch(e) { return String(value).slice(0, maxLen); }
|
|
39
48
|
}
|
|
40
49
|
|
|
50
|
+
// Framework modules that should not be the default target for define_variable
|
|
51
|
+
var FRAMEWORK_MODULES = new Set([
|
|
52
|
+
"bootloader", "builtin",
|
|
53
|
+
"@tomlarkworthy/lopepage",
|
|
54
|
+
"@tomlarkworthy/claude-code-pairing",
|
|
55
|
+
"@tomlarkworthy/module-selection"
|
|
56
|
+
]);
|
|
57
|
+
|
|
41
58
|
function findModule(runtime, moduleName) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
return
|
|
59
|
+
// Use runtime.mains (Map<name, Module>) — the actual module refs used by the runtime
|
|
60
|
+
var mains = runtime.mains;
|
|
61
|
+
if (mains && mains instanceof Map) {
|
|
62
|
+
if (moduleName) {
|
|
63
|
+
return mains.get(moduleName) || null;
|
|
64
|
+
}
|
|
65
|
+
// No name specified — return the first non-framework main
|
|
66
|
+
for (var entry of mains) {
|
|
67
|
+
if (!FRAMEWORK_MODULES.has(entry[0])) return entry[1];
|
|
68
|
+
}
|
|
47
69
|
}
|
|
48
70
|
return null;
|
|
49
71
|
}
|
|
@@ -65,22 +87,21 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
65
87
|
var name = cmd.params.name;
|
|
66
88
|
var moduleName = cmd.params.module;
|
|
67
89
|
var targetModule = findModule(runtime, moduleName);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return { ok: false, error: "Variable not found: " + name };
|
|
90
|
+
if (!targetModule) return { ok: false, error: "Module not found: " + (moduleName || "default") };
|
|
91
|
+
return lookupVariable(name, targetModule).then(function(v) {
|
|
92
|
+
if (!v) return { ok: false, error: "Variable not found: " + name };
|
|
93
|
+
return {
|
|
94
|
+
ok: true,
|
|
95
|
+
result: {
|
|
96
|
+
name: v._name,
|
|
97
|
+
hasValue: v._value !== undefined,
|
|
98
|
+
hasError: v._error !== undefined,
|
|
99
|
+
value: serializeValue(v._value),
|
|
100
|
+
error: v._error ? v._error.message : undefined,
|
|
101
|
+
reachable: v._reachable
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
});
|
|
84
105
|
}
|
|
85
106
|
|
|
86
107
|
case "define-variable": {
|
|
@@ -88,31 +109,26 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
88
109
|
var definition = cmd.params.definition;
|
|
89
110
|
var inputs = cmd.params.inputs || [];
|
|
90
111
|
var moduleName = cmd.params.module;
|
|
91
|
-
var
|
|
92
|
-
if (!
|
|
112
|
+
var mod = findModule(runtime, moduleName);
|
|
113
|
+
if (!mod) return { ok: false, error: "Module not found: " + (moduleName || "default") };
|
|
93
114
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
eval("fn = " + definition);
|
|
115
|
+
return realize([definition], runtime).then(function(results) {
|
|
116
|
+
var fn = results[0];
|
|
97
117
|
if (typeof fn !== "function") return { ok: false, error: "Definition must evaluate to a function" };
|
|
98
|
-
} catch (e) {
|
|
99
|
-
return { ok: false, error: "Failed to parse definition: " + e.message };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
var existingVar = null;
|
|
103
|
-
for (var v of runtime._variables) {
|
|
104
|
-
if (v._name === name && v._module === targetModule) { existingVar = v; break; }
|
|
105
|
-
}
|
|
106
118
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
// Use module._scope to check for existing variable
|
|
120
|
+
var existingVar = mod._scope.get(name);
|
|
121
|
+
if (existingVar) {
|
|
122
|
+
existingVar.define(name, inputs, fn);
|
|
123
|
+
} else {
|
|
124
|
+
mod.variable(ojs_observer ? ojs_observer(name) : {}).define(name, inputs, fn);
|
|
125
|
+
}
|
|
126
|
+
// Auto-watch the defined variable so the result arrives reactively
|
|
127
|
+
watchVariable(runtime, name, moduleName || null);
|
|
128
|
+
return { ok: true, result: { success: true, name: name, module: moduleName || "default" } };
|
|
129
|
+
}).catch(function(e) {
|
|
130
|
+
return { ok: false, error: "define failed: " + e.message };
|
|
131
|
+
});
|
|
116
132
|
}
|
|
117
133
|
|
|
118
134
|
case "delete-variable": {
|
|
@@ -120,24 +136,23 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
120
136
|
var moduleName = cmd.params.module;
|
|
121
137
|
var targetModule = findModule(runtime, moduleName);
|
|
122
138
|
if (!targetModule) return { ok: false, error: "Module not found: " + (moduleName || "main") };
|
|
123
|
-
|
|
124
|
-
if (v
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
return { ok: false, error: "Variable not found: " + name + " in module " + (moduleName || "main") };
|
|
139
|
+
return lookupVariable(name, targetModule).then(function(v) {
|
|
140
|
+
if (!v) return { ok: false, error: "Variable not found: " + name + " in module " + (moduleName || "main") };
|
|
141
|
+
v.delete();
|
|
142
|
+
return { ok: true, result: { success: true, name: name, module: targetModule._name || "main" } };
|
|
143
|
+
});
|
|
130
144
|
}
|
|
131
145
|
|
|
132
146
|
case "list-variables": {
|
|
133
147
|
var moduleName = cmd.params.module;
|
|
134
148
|
var targetModule = findModule(runtime, moduleName);
|
|
149
|
+
if (!targetModule) return { ok: false, error: "Module not found: " + (moduleName || "default") };
|
|
150
|
+
// Use module._scope instead of scanning runtime._variables
|
|
135
151
|
var variables = [];
|
|
136
|
-
for (var
|
|
137
|
-
|
|
138
|
-
if (targetModule && v._module !== targetModule) continue;
|
|
152
|
+
for (var entry of targetModule._scope) {
|
|
153
|
+
var v = entry[1];
|
|
139
154
|
variables.push({
|
|
140
|
-
name:
|
|
155
|
+
name: entry[0],
|
|
141
156
|
module: (v._module && v._module._name) || "main",
|
|
142
157
|
hasValue: v._value !== undefined,
|
|
143
158
|
hasError: v._error !== undefined,
|
|
@@ -154,6 +169,28 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
154
169
|
return runTests(runtime, filter, timeout);
|
|
155
170
|
}
|
|
156
171
|
|
|
172
|
+
case "create-module": {
|
|
173
|
+
var moduleName = cmd.params.name;
|
|
174
|
+
if (!moduleName) return { ok: false, error: "Module name is required" };
|
|
175
|
+
try {
|
|
176
|
+
createModule(moduleName, runtime);
|
|
177
|
+
return { ok: true, result: { success: true, name: moduleName } };
|
|
178
|
+
} catch (e) {
|
|
179
|
+
return { ok: false, error: e.message };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
case "delete-module": {
|
|
184
|
+
var moduleName = cmd.params.name;
|
|
185
|
+
if (!moduleName) return { ok: false, error: "Module name is required" };
|
|
186
|
+
try {
|
|
187
|
+
deleteModule(moduleName, runtime);
|
|
188
|
+
return { ok: true, result: { success: true, name: moduleName } };
|
|
189
|
+
} catch (e) {
|
|
190
|
+
return { ok: false, error: e.message };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
157
194
|
case "eval": {
|
|
158
195
|
var code = cmd.params.code;
|
|
159
196
|
try {
|
|
@@ -283,17 +320,14 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
283
320
|
var key = (moduleName || "main") + ":" + name;
|
|
284
321
|
if (watchers.has(key)) return { ok: true, result: { already_watching: true, key: key } };
|
|
285
322
|
|
|
286
|
-
|
|
287
|
-
var
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
break;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
323
|
+
// Use findModule for named modules, cc_module (thisModule) for unqualified lookups
|
|
324
|
+
var targetModule = moduleName ? findModule(runtime, moduleName) : cc_module;
|
|
325
|
+
if (!targetModule) return Promise.resolve({ ok: false, error: "Module not found: " + (moduleName || "default") });
|
|
326
|
+
|
|
327
|
+
return lookupVariable(name, targetModule).then(function(targetVar) {
|
|
294
328
|
if (!targetVar) return { ok: false, error: "Variable not found: " + name };
|
|
295
329
|
|
|
296
|
-
var
|
|
330
|
+
var resolvedModule = (targetVar._module && targetVar._module._name) || "main";
|
|
297
331
|
var debounceTimer = null;
|
|
298
332
|
var latestValue = undefined;
|
|
299
333
|
var latestError = undefined;
|
|
@@ -309,19 +343,19 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
309
343
|
var watches = viewof_cc_watches.value.slice();
|
|
310
344
|
var found = false;
|
|
311
345
|
for (var i = 0; i < watches.length; i++) {
|
|
312
|
-
if (watches[i].name === name && watches[i].module ===
|
|
313
|
-
watches[i] = { name: name, module:
|
|
346
|
+
if (watches[i].name === name && (watches[i].module === resolvedModule || watches[i].module == null)) {
|
|
347
|
+
watches[i] = { name: name, module: resolvedModule, value: serialized.slice(0, 200), updated: now };
|
|
314
348
|
found = true;
|
|
315
349
|
break;
|
|
316
350
|
}
|
|
317
351
|
}
|
|
318
|
-
if (!found) watches.push({ name: name, module:
|
|
352
|
+
if (!found) watches.push({ name: name, module: resolvedModule, value: serialized.slice(0, 200), updated: now });
|
|
319
353
|
viewof_cc_watches.value = watches;
|
|
320
354
|
viewof_cc_watches.dispatchEvent(new Event("input"));
|
|
321
355
|
|
|
322
356
|
// Send over WebSocket if connected
|
|
323
357
|
if (!paired || !ws) return;
|
|
324
|
-
var msg = { type: "variable-update", name: name, module:
|
|
358
|
+
var msg = { type: "variable-update", name: name, module: resolvedModule };
|
|
325
359
|
if (latestError) { msg.error = latestError.message || String(latestError); }
|
|
326
360
|
else { msg.value = serialized; }
|
|
327
361
|
ws.send(JSON.stringify(msg));
|
|
@@ -351,7 +385,7 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
351
385
|
cancel();
|
|
352
386
|
// Remove from watches table
|
|
353
387
|
var watches = viewof_cc_watches.value.filter(function(w) {
|
|
354
|
-
return !(w.name === name && w.module ===
|
|
388
|
+
return !(w.name === name && w.module === resolvedModule);
|
|
355
389
|
});
|
|
356
390
|
viewof_cc_watches.value = watches;
|
|
357
391
|
viewof_cc_watches.dispatchEvent(new Event("input"));
|
|
@@ -360,6 +394,7 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
360
394
|
});
|
|
361
395
|
|
|
362
396
|
return { ok: true, result: { watching: true, key: key } };
|
|
397
|
+
}); // close lookupVariable.then
|
|
363
398
|
}
|
|
364
399
|
|
|
365
400
|
function unwatchVariable(name, moduleName) {
|
|
@@ -381,7 +416,7 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
381
416
|
function handleFork(runtime) {
|
|
382
417
|
return new Promise(function(resolve) {
|
|
383
418
|
for (var v of runtime._variables) {
|
|
384
|
-
if (v._name === "_exportToHTML" && typeof v._value === "function") {
|
|
419
|
+
if ((v._name === "_exportToHTML" || v._name === "exportToHTML") && typeof v._value === "function") {
|
|
385
420
|
try {
|
|
386
421
|
Promise.resolve(v._value()).then(function(html) {
|
|
387
422
|
resolve({ ok: true, result: { html: html } });
|
|
@@ -402,6 +437,11 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
402
437
|
function connect(token) {
|
|
403
438
|
if (ws) { ws.close(); ws = null; }
|
|
404
439
|
|
|
440
|
+
// Persist token for reconnection across reloads
|
|
441
|
+
if (token) {
|
|
442
|
+
try { sessionStorage.setItem("lopecode_cc_token", token); } catch(e) {}
|
|
443
|
+
}
|
|
444
|
+
|
|
405
445
|
// Parse port from token format LOPE-PORT-XXXX
|
|
406
446
|
var connectPort = port;
|
|
407
447
|
if (token) {
|
|
@@ -453,11 +493,13 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
453
493
|
hash: location.hash
|
|
454
494
|
}));
|
|
455
495
|
|
|
456
|
-
//
|
|
496
|
+
// Set up watches from cc_watches (initialized with defaults via dependency resolution)
|
|
457
497
|
var runtime2 = window.__ojs_runtime;
|
|
458
498
|
if (runtime2) {
|
|
459
|
-
|
|
460
|
-
|
|
499
|
+
var initialWatches = viewof_cc_watches.value || [];
|
|
500
|
+
for (var i = 0; i < initialWatches.length; i++) {
|
|
501
|
+
watchVariable(runtime2, initialWatches[i].name, initialWatches[i].module);
|
|
502
|
+
}
|
|
461
503
|
}
|
|
462
504
|
break;
|
|
463
505
|
|
|
@@ -477,6 +519,17 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
477
519
|
cc_messages.dispatchEvent(new Event("input"));
|
|
478
520
|
break;
|
|
479
521
|
|
|
522
|
+
case "tool-activity":
|
|
523
|
+
var msgs2 = cc_messages.value.concat([{
|
|
524
|
+
role: "tool",
|
|
525
|
+
tool_name: msg.tool_name,
|
|
526
|
+
content: msg.summary,
|
|
527
|
+
timestamp: msg.timestamp || Date.now()
|
|
528
|
+
}]);
|
|
529
|
+
cc_messages.value = msgs2;
|
|
530
|
+
cc_messages.dispatchEvent(new Event("input"));
|
|
531
|
+
break;
|
|
532
|
+
|
|
480
533
|
case "command":
|
|
481
534
|
Promise.resolve(handleCommand(msg)).then(function(result) {
|
|
482
535
|
ws.send(JSON.stringify({
|
|
@@ -508,13 +561,23 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
508
561
|
ws.onerror = function() {};
|
|
509
562
|
}
|
|
510
563
|
|
|
511
|
-
// Auto-connect
|
|
512
|
-
// Hash format: #view=R100(...)&cc=LOPE-PORT-XXXX
|
|
564
|
+
// Auto-connect: check hash param first, then sessionStorage fallback
|
|
513
565
|
(function autoConnect() {
|
|
566
|
+
var token = null;
|
|
567
|
+
|
|
568
|
+
// 1. Check &cc=TOKEN in hash
|
|
514
569
|
var hash = location.hash || "";
|
|
515
570
|
var match = hash.match(/[&?]cc=(LOPE-[A-Z0-9-]+)/);
|
|
516
571
|
if (match) {
|
|
517
|
-
|
|
572
|
+
token = match[1];
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// 2. Fallback to sessionStorage (survives reloads even if hash is mangled)
|
|
576
|
+
if (!token) {
|
|
577
|
+
try { token = sessionStorage.getItem("lopecode_cc_token"); } catch(e) {}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (token) {
|
|
518
581
|
// Small delay to let the WebSocket server be ready
|
|
519
582
|
setTimeout(function() { connect(token); }, 500);
|
|
520
583
|
}
|
|
@@ -584,39 +647,41 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
|
|
|
584
647
|
disconnected: "#ef4444"
|
|
585
648
|
};
|
|
586
649
|
|
|
587
|
-
function
|
|
650
|
+
function renderConnect() {
|
|
651
|
+
var row = document.createElement("div");
|
|
652
|
+
row.style.cssText = "display:flex;gap:8px;justify-content:center;align-items:center;padding:16px;";
|
|
653
|
+
|
|
588
654
|
var tokenInput = document.createElement("input");
|
|
589
655
|
tokenInput.type = "text";
|
|
590
656
|
tokenInput.placeholder = "LOPE-XXXX";
|
|
591
|
-
tokenInput.style.cssText = "font-family:monospace;font-size:16px;padding:8px 12px;border:2px solid
|
|
657
|
+
tokenInput.style.cssText = "font-family:var(--monospace, monospace);font-size:16px;padding:8px 12px;border:2px solid var(--theme-foreground-faint);border-radius:6px;width:140px;text-transform:uppercase;letter-spacing:2px;background:var(--theme-background-a);color:var(--theme-foreground);";
|
|
592
658
|
|
|
593
659
|
var connectBtn = document.createElement("button");
|
|
594
660
|
connectBtn.textContent = "Connect";
|
|
595
|
-
connectBtn.style.cssText = "padding:8px 20px;background
|
|
661
|
+
connectBtn.style.cssText = "padding:8px 20px;background:var(--theme-foreground);color:var(--theme-background-a);border:none;border-radius:6px;cursor:pointer;font-size:14px;font-weight:500;";
|
|
596
662
|
connectBtn.onclick = function() {
|
|
597
663
|
var token = tokenInput.value.trim();
|
|
598
664
|
if (token) cc_ws.connect(token);
|
|
599
665
|
};
|
|
600
|
-
|
|
601
666
|
tokenInput.addEventListener("keydown", function(e) {
|
|
602
667
|
if (e.key === "Enter") connectBtn.click();
|
|
603
668
|
});
|
|
604
669
|
|
|
670
|
+
row.append(tokenInput, connectBtn);
|
|
671
|
+
|
|
672
|
+
var guide = document.createElement("details");
|
|
673
|
+
guide.style.cssText = "padding:0 16px 16px;font-size:13px;color:var(--theme-foreground);";
|
|
674
|
+
guide.innerHTML = '<summary style="cursor:pointer;font-weight:600;font-size:14px;">Setup guide</summary>' +
|
|
675
|
+
'<ol style="margin:8px 0 0;padding-left:20px;line-height:1.8;">' +
|
|
676
|
+
'<li>Install <a href="https://bun.sh" target="_blank">Bun</a> if needed, then:<br>' +
|
|
677
|
+
'<code style="background:var(--theme-background-alt);padding:2px 6px;border-radius:3px;font-size:12px;">bun install -g @lopecode/channel</code><br>' +
|
|
678
|
+
'<code style="background:var(--theme-background-alt);padding:2px 6px;border-radius:3px;font-size:12px;">claude mcp add lopecode bunx @lopecode/channel</code></li>' +
|
|
679
|
+
'<li>Start Claude Code:<br><code style="background:var(--theme-background-alt);padding:2px 6px;border-radius:3px;font-size:12px;">claude --dangerously-load-development-channels server:lopecode</code></li>' +
|
|
680
|
+
'<li>Ask Claude to connect, or paste a pairing token above</li>' +
|
|
681
|
+
'</ol>';
|
|
682
|
+
|
|
605
683
|
var container = document.createElement("div");
|
|
606
|
-
container.
|
|
607
|
-
container.innerHTML = '<div style="padding:24px;max-width:400px;margin:0 auto;text-align:center;">' +
|
|
608
|
-
'<div style="font-size:24px;margin-bottom:8px;">Claude Channel</div>' +
|
|
609
|
-
'<p style="color:#6b7280;margin-bottom:20px;font-size:14px;">Connect to Claude Code to chat with Claude from this notebook.</p>' +
|
|
610
|
-
'<div style="text-align:left;background:#f3f4f6;padding:16px;border-radius:8px;margin-bottom:20px;font-size:13px;">' +
|
|
611
|
-
'<div style="font-weight:600;margin-bottom:8px;">Setup:</div>' +
|
|
612
|
-
'<ol style="margin:0;padding-left:20px;line-height:1.8;">' +
|
|
613
|
-
'<li>Start Claude with the channel flag:<br><code style="background:#e5e7eb;padding:2px 6px;border-radius:3px;font-size:12px;">claude --dangerously-load-development-channels server:lopecode</code></li>' +
|
|
614
|
-
'<li>Copy the pairing token from the terminal</li>' +
|
|
615
|
-
'<li>Paste it below and click Connect</li>' +
|
|
616
|
-
'</ol></div>' +
|
|
617
|
-
'<div style="display:flex;gap:8px;justify-content:center;align-items:center;" class="cc-token-row"></div>' +
|
|
618
|
-
'</div>';
|
|
619
|
-
container.querySelector(".cc-token-row").append(tokenInput, connectBtn);
|
|
684
|
+
container.append(row, guide);
|
|
620
685
|
return container;
|
|
621
686
|
}
|
|
622
687
|
|
|
@@ -629,11 +694,11 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
|
|
|
629
694
|
textarea.className = "cc-input";
|
|
630
695
|
textarea.placeholder = "Message Claude...";
|
|
631
696
|
textarea.rows = 2;
|
|
632
|
-
textarea.style.cssText = "width:100%;box-sizing:border-box;resize:none;border:1px solid
|
|
697
|
+
textarea.style.cssText = "width:100%;box-sizing:border-box;resize:none;border:1px solid var(--theme-foreground-faint);border-radius:8px;padding:10px 12px;font-family:inherit;font-size:14px;outline:none;background:var(--theme-background-a);color:var(--theme-foreground);";
|
|
633
698
|
|
|
634
699
|
var sendBtn = document.createElement("button");
|
|
635
700
|
sendBtn.textContent = "Send";
|
|
636
|
-
sendBtn.style.cssText = "padding:8px 16px;background
|
|
701
|
+
sendBtn.style.cssText = "padding:8px 16px;background:var(--theme-foreground);color:var(--theme-background-a);border:none;border-radius:6px;cursor:pointer;font-size:13px;font-weight:500;margin-left:auto;";
|
|
637
702
|
|
|
638
703
|
function sendMessage() {
|
|
639
704
|
var text = textarea.value.trim();
|
|
@@ -651,7 +716,7 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
|
|
|
651
716
|
});
|
|
652
717
|
|
|
653
718
|
var inputRow = document.createElement("div");
|
|
654
|
-
inputRow.style.cssText = "display:flex;gap:8px;padding:12px;align-items:flex-end;border-top:1px solid
|
|
719
|
+
inputRow.style.cssText = "display:flex;gap:8px;padding:12px;align-items:flex-end;border-top:1px solid var(--theme-foreground-faint);";
|
|
655
720
|
inputRow.append(textarea, sendBtn);
|
|
656
721
|
|
|
657
722
|
var container = document.createElement("div");
|
|
@@ -661,15 +726,47 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
|
|
|
661
726
|
|
|
662
727
|
function updateMessages() {
|
|
663
728
|
messagesDiv.innerHTML = "";
|
|
664
|
-
|
|
665
|
-
|
|
729
|
+
var msgs = cc_messages.value;
|
|
730
|
+
var i = 0;
|
|
731
|
+
while (i < msgs.length) {
|
|
732
|
+
var msg = msgs[i];
|
|
733
|
+
|
|
734
|
+
// Group consecutive tool messages into a collapsible block
|
|
735
|
+
if (msg.role === "tool") {
|
|
736
|
+
var toolGroup = [];
|
|
737
|
+
while (i < msgs.length && msgs[i].role === "tool") {
|
|
738
|
+
toolGroup.push(msgs[i]);
|
|
739
|
+
i++;
|
|
740
|
+
}
|
|
741
|
+
var details = document.createElement("details");
|
|
742
|
+
details.style.cssText = "max-width:90%;align-self:flex-start;font-size:12px;opacity:0.85;margin:2px 0;";
|
|
743
|
+
var summary = document.createElement("summary");
|
|
744
|
+
summary.style.cssText = "cursor:pointer;padding:4px 10px;border-radius:8px;" +
|
|
745
|
+
"background:var(--theme-background-alt, #f0f0f0);color:var(--theme-foreground, #333);" +
|
|
746
|
+
"font-family:var(--monospace, monospace);border-left:2px solid var(--theme-foreground-faint, #ccc);list-style:inside;font-size:12px;";
|
|
747
|
+
summary.textContent = toolGroup.length === 1
|
|
748
|
+
? "\u{1F527} " + toolGroup[0].content
|
|
749
|
+
: "\u{1F527} " + toolGroup.length + " tool calls \u2014 " + toolGroup[toolGroup.length - 1].content;
|
|
750
|
+
details.appendChild(summary);
|
|
751
|
+
var list = document.createElement("div");
|
|
752
|
+
list.style.cssText = "padding:4px 10px 4px 20px;font-family:var(--monospace, monospace);color:var(--theme-foreground, #333);line-height:1.6;font-size:11px;";
|
|
753
|
+
for (var j = 0; j < toolGroup.length; j++) {
|
|
754
|
+
var line = document.createElement("div");
|
|
755
|
+
line.textContent = toolGroup[j].content;
|
|
756
|
+
list.appendChild(line);
|
|
757
|
+
}
|
|
758
|
+
details.appendChild(list);
|
|
759
|
+
messagesDiv.appendChild(details);
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
|
|
666
763
|
var bubble = document.createElement("div");
|
|
667
764
|
bubble.className = "cc-msg cc-msg-" + msg.role;
|
|
668
765
|
var isUser = msg.role === "user";
|
|
669
766
|
bubble.style.cssText = "max-width:80%;padding:10px 14px;border-radius:12px;font-size:14px;line-height:1.5;" +
|
|
670
767
|
(isUser
|
|
671
|
-
? "align-self:flex-end;background
|
|
672
|
-
: "align-self:flex-start;background
|
|
768
|
+
? "align-self:flex-end;background:var(--theme-foreground);color:var(--theme-background-a);border-bottom-right-radius:4px;"
|
|
769
|
+
: "align-self:flex-start;background:var(--theme-background-b);color:var(--theme-foreground);border-bottom-left-radius:4px;");
|
|
673
770
|
|
|
674
771
|
if (isUser) {
|
|
675
772
|
bubble.textContent = msg.content;
|
|
@@ -681,6 +778,7 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
|
|
|
681
778
|
} catch(e) { bubble.textContent = msg.content; }
|
|
682
779
|
}
|
|
683
780
|
messagesDiv.appendChild(bubble);
|
|
781
|
+
i++;
|
|
684
782
|
}
|
|
685
783
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
686
784
|
}
|
|
@@ -690,13 +788,16 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
|
|
|
690
788
|
return container;
|
|
691
789
|
}
|
|
692
790
|
|
|
791
|
+
var isConnected = (cc_status.value === "connected");
|
|
792
|
+
|
|
693
793
|
var wrapper = document.createElement("div");
|
|
694
794
|
wrapper.className = "cc-chat";
|
|
695
|
-
wrapper.style.cssText = "border:1px solid
|
|
795
|
+
wrapper.style.cssText = "border:1px solid var(--theme-foreground-faint);border-radius:12px;overflow:hidden;display:flex;flex-direction:column;background:var(--theme-background-a);font-family:inherit;" +
|
|
796
|
+
(isConnected ? "height:400px;" : "");
|
|
696
797
|
|
|
697
798
|
var statusBar = document.createElement("div");
|
|
698
799
|
statusBar.className = "cc-status-bar";
|
|
699
|
-
statusBar.style.cssText = "display:flex;align-items:center;gap:6px;padding:8px 12px;border-bottom:1px solid
|
|
800
|
+
statusBar.style.cssText = "display:flex;align-items:center;gap:6px;padding:8px 12px;border-bottom:1px solid var(--theme-foreground-faint);font-size:12px;color:var(--theme-foreground-faint);background:var(--theme-background-b);";
|
|
700
801
|
|
|
701
802
|
var statusDot = document.createElement("span");
|
|
702
803
|
statusDot.style.cssText = "width:8px;height:8px;border-radius:50%;display:inline-block;";
|
|
@@ -716,18 +817,21 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
|
|
|
716
817
|
|
|
717
818
|
function render() {
|
|
718
819
|
var status = cc_status.value || "disconnected";
|
|
820
|
+
var connected = (status === "connected");
|
|
719
821
|
statusDot.style.background = statusColors[status] || "#ef4444";
|
|
720
|
-
statusText.textContent =
|
|
822
|
+
statusText.textContent = connected ? "Connected to Claude Code"
|
|
721
823
|
: (status === "connecting" || status === "pairing") ? "Connecting..."
|
|
722
824
|
: "Not connected";
|
|
723
825
|
|
|
826
|
+
wrapper.style.height = connected ? "400px" : "";
|
|
827
|
+
|
|
724
828
|
body.innerHTML = "";
|
|
725
|
-
if (
|
|
829
|
+
if (connected) {
|
|
726
830
|
chatView = renderChat();
|
|
727
831
|
body.appendChild(chatView);
|
|
728
832
|
} else {
|
|
729
833
|
chatView = null;
|
|
730
|
-
body.appendChild(
|
|
834
|
+
body.appendChild(renderConnect());
|
|
731
835
|
}
|
|
732
836
|
}
|
|
733
837
|
|
|
@@ -769,7 +873,9 @@ export default function define(runtime, observer) {
|
|
|
769
873
|
$def("_cc_messages", "cc_messages", ["Inputs"], _cc_messages);
|
|
770
874
|
$def("_cc_watches", "viewof cc_watches", ["Inputs"], _cc_watches);
|
|
771
875
|
main.variable().define("cc_watches", ["Generators", "viewof cc_watches"], (G, v) => G.input(v));
|
|
772
|
-
$def("
|
|
876
|
+
$def("_cc_module", "viewof cc_module", ["thisModule"], _cc_module);
|
|
877
|
+
main.variable().define("cc_module", ["Generators", "viewof cc_module"], (G, v) => G.input(v));
|
|
878
|
+
$def("_cc_ws", "cc_ws", ["cc_config","cc_notebook_id","cc_status","cc_messages","viewof cc_watches","summarizeJS","observe","realize","createModule","deleteModule","lookupVariable","cc_module","runtime","invalidation"], _cc_ws);
|
|
773
879
|
$def("_cc_change_forwarder", "cc_change_forwarder", ["cc_ws","invalidation"], _cc_change_forwarder);
|
|
774
880
|
|
|
775
881
|
// Imports
|
|
@@ -780,6 +886,12 @@ export default function define(runtime, observer) {
|
|
|
780
886
|
main.define("viewof runtime_variables", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("viewof runtime_variables", _));
|
|
781
887
|
main.define("runtime_variables", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("runtime_variables", _));
|
|
782
888
|
main.define("observe", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("observe", _));
|
|
889
|
+
main.define("realize", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("realize", _));
|
|
890
|
+
main.define("createModule", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("createModule", _));
|
|
891
|
+
main.define("deleteModule", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("deleteModule", _));
|
|
892
|
+
main.define("lookupVariable", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("lookupVariable", _));
|
|
893
|
+
main.define("thisModule", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("thisModule", _));
|
|
894
|
+
main.define("runtime", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("runtime", _));
|
|
783
895
|
main.define("module d/57d79353bac56631@44", async () => runtime.module((await import("/d/57d79353bac56631@44.js?v=4")).default));
|
|
784
896
|
main.define("hash", ["module d/57d79353bac56631@44", "@variable"], (_, v) => v.import("hash", _));
|
|
785
897
|
main.define("module @tomlarkworthy/summarizejs", async () => runtime.module((await import("/@tomlarkworthy/summarizejs.js?v=4")).default));
|
package/lopecode-channel.ts
CHANGED
|
@@ -90,11 +90,14 @@ Variable updates (when watching):
|
|
|
90
90
|
|
|
91
91
|
- reply: Send markdown to notebook chat
|
|
92
92
|
- get_variable / define_variable / delete_variable / list_variables: Interact with runtime
|
|
93
|
+
- create_module / delete_module: Create or remove modules (registered in runtime.mains, visible in moduleMap)
|
|
93
94
|
- watch_variable / unwatch_variable: Subscribe to reactive variable updates
|
|
94
95
|
- run_tests: Run test_* variables
|
|
95
96
|
- eval_code: Evaluate JS in browser context
|
|
96
97
|
- fork_notebook: Create a copy as sibling HTML file
|
|
97
98
|
|
|
99
|
+
IMPORTANT: Always specify the module parameter when calling define_variable, get_variable, etc.
|
|
100
|
+
Use the currentModules watch to identify the user's content module (not lopepage, module-selection, or claude-code-pairing).
|
|
98
101
|
When multiple notebooks are connected, specify notebook_id (the URL). When only one is connected, it's used automatically.`,
|
|
99
102
|
}
|
|
100
103
|
);
|
|
@@ -248,6 +251,30 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
248
251
|
},
|
|
249
252
|
},
|
|
250
253
|
},
|
|
254
|
+
{
|
|
255
|
+
name: "create_module",
|
|
256
|
+
description: "Create a new empty module in the runtime. The module is registered in runtime.mains so it appears in moduleMap/currentModules.",
|
|
257
|
+
inputSchema: {
|
|
258
|
+
type: "object",
|
|
259
|
+
properties: {
|
|
260
|
+
notebook_id: { type: "string" },
|
|
261
|
+
name: { type: "string", description: "Module name, e.g. '@tomlarkworthy/my-module'" },
|
|
262
|
+
},
|
|
263
|
+
required: ["name"],
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: "delete_module",
|
|
268
|
+
description: "Delete a module and all its variables from the runtime.",
|
|
269
|
+
inputSchema: {
|
|
270
|
+
type: "object",
|
|
271
|
+
properties: {
|
|
272
|
+
notebook_id: { type: "string" },
|
|
273
|
+
name: { type: "string", description: "Module name to delete" },
|
|
274
|
+
},
|
|
275
|
+
required: ["name"],
|
|
276
|
+
},
|
|
277
|
+
},
|
|
251
278
|
{
|
|
252
279
|
name: "watch_variable",
|
|
253
280
|
description: "Subscribe to reactive updates for a variable. Changes are pushed as notifications.",
|
|
@@ -308,15 +335,19 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
308
335
|
action = "get-variable";
|
|
309
336
|
params = { name: args.name, module: args.module || null };
|
|
310
337
|
break;
|
|
311
|
-
case "define_variable":
|
|
338
|
+
case "define_variable": {
|
|
312
339
|
action = "define-variable";
|
|
340
|
+
let inputs = args.inputs;
|
|
341
|
+
if (typeof inputs === "string") inputs = JSON.parse(inputs);
|
|
342
|
+
if (!Array.isArray(inputs)) inputs = [];
|
|
313
343
|
params = {
|
|
314
344
|
name: args.name,
|
|
315
345
|
definition: args.definition,
|
|
316
|
-
inputs
|
|
346
|
+
inputs,
|
|
317
347
|
module: args.module || null,
|
|
318
348
|
};
|
|
319
349
|
break;
|
|
350
|
+
}
|
|
320
351
|
case "delete_variable":
|
|
321
352
|
action = "delete-variable";
|
|
322
353
|
params = { name: args.name, module: args.module || null };
|
|
@@ -339,6 +370,14 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
339
370
|
timeout = 120000;
|
|
340
371
|
params = { suffix: args.suffix || null };
|
|
341
372
|
break;
|
|
373
|
+
case "create_module":
|
|
374
|
+
action = "create-module";
|
|
375
|
+
params = { name: args.name };
|
|
376
|
+
break;
|
|
377
|
+
case "delete_module":
|
|
378
|
+
action = "delete-module";
|
|
379
|
+
params = { name: args.name };
|
|
380
|
+
break;
|
|
342
381
|
case "watch_variable":
|
|
343
382
|
action = "watch";
|
|
344
383
|
params = { name: args.name, module: args.module || null };
|
|
@@ -538,7 +577,7 @@ await mcp.connect(new StdioServerTransport());
|
|
|
538
577
|
const server = Bun.serve({
|
|
539
578
|
port: REQUESTED_PORT,
|
|
540
579
|
hostname: "127.0.0.1",
|
|
541
|
-
fetch(req, server) {
|
|
580
|
+
async fetch(req, server) {
|
|
542
581
|
const url = new URL(req.url);
|
|
543
582
|
if (url.pathname === "/ws") {
|
|
544
583
|
if (server.upgrade(req)) return;
|
|
@@ -550,6 +589,51 @@ const server = Bun.serve({
|
|
|
550
589
|
pending: pendingConnections.size,
|
|
551
590
|
}), { headers: { "content-type": "application/json" } });
|
|
552
591
|
}
|
|
592
|
+
|
|
593
|
+
// Tool activity endpoint — receives PostToolUse hook data and broadcasts to notebooks
|
|
594
|
+
if (url.pathname === "/tool-activity" && req.method === "POST") {
|
|
595
|
+
try {
|
|
596
|
+
const body = await req.json();
|
|
597
|
+
const toolName = body.tool_name || "unknown";
|
|
598
|
+
const toolInput = body.tool_input || {};
|
|
599
|
+
const toolResponse = body.tool_response;
|
|
600
|
+
|
|
601
|
+
// Build a compact summary for the chat widget
|
|
602
|
+
let summary = toolName;
|
|
603
|
+
if (toolName === "Read" && toolInput.file_path) {
|
|
604
|
+
summary = `Read ${toolInput.file_path}`;
|
|
605
|
+
} else if (toolName === "Edit" && toolInput.file_path) {
|
|
606
|
+
summary = `Edit ${toolInput.file_path}`;
|
|
607
|
+
} else if (toolName === "Write" && toolInput.file_path) {
|
|
608
|
+
summary = `Write ${toolInput.file_path}`;
|
|
609
|
+
} else if (toolName === "Bash" && toolInput.command) {
|
|
610
|
+
summary = `$ ${toolInput.command.slice(0, 120)}`;
|
|
611
|
+
} else if (toolName === "Grep" && toolInput.pattern) {
|
|
612
|
+
summary = `Grep "${toolInput.pattern}"`;
|
|
613
|
+
} else if (toolName === "Glob" && toolInput.pattern) {
|
|
614
|
+
summary = `Glob "${toolInput.pattern}"`;
|
|
615
|
+
} else if (toolName === "Agent" && toolInput.description) {
|
|
616
|
+
summary = `Agent: ${toolInput.description}`;
|
|
617
|
+
} else if (toolName.startsWith("mcp__lopecode__")) {
|
|
618
|
+
// Our own MCP tools — skip broadcasting to avoid echo
|
|
619
|
+
return new Response("ok", { status: 200 });
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Broadcast to all paired notebooks
|
|
623
|
+
const msg = JSON.stringify({
|
|
624
|
+
type: "tool-activity",
|
|
625
|
+
tool_name: toolName,
|
|
626
|
+
summary,
|
|
627
|
+
timestamp: Date.now(),
|
|
628
|
+
});
|
|
629
|
+
for (const ws of pairedConnections.values()) {
|
|
630
|
+
ws.send(msg);
|
|
631
|
+
}
|
|
632
|
+
return new Response("ok", { status: 200 });
|
|
633
|
+
} catch (e) {
|
|
634
|
+
return new Response("bad request", { status: 400 });
|
|
635
|
+
}
|
|
636
|
+
}
|
|
553
637
|
if (url.pathname === "/") {
|
|
554
638
|
const notebookUrl = `https://tomlarkworthy.github.io/lopecode/notebooks/@tomlarkworthy_blank-notebook.html#view=R100(S50(@tomlarkworthy/blank-notebook),S25(@tomlarkworthy/module-selection),S25(@tomlarkworthy/claude-code-pairing))&cc=${PAIRING_TOKEN}`;
|
|
555
639
|
return new Response(null, {
|
|
@@ -570,5 +654,11 @@ const server = Bun.serve({
|
|
|
570
654
|
|
|
571
655
|
PORT = server.port; // read actual port (important when REQUESTED_PORT is 0)
|
|
572
656
|
PAIRING_TOKEN = generateToken();
|
|
657
|
+
|
|
658
|
+
// Write port file so hooks can find us
|
|
659
|
+
const portFilePath = join(import.meta.dir, ".lopecode-port");
|
|
660
|
+
await Bun.write(portFilePath, String(PORT));
|
|
661
|
+
process.on("exit", () => { try { require("fs").unlinkSync(portFilePath); } catch {} });
|
|
662
|
+
|
|
573
663
|
process.stderr.write(`lopecode-channel: pairing token: ${PAIRING_TOKEN}\n`);
|
|
574
664
|
process.stderr.write(`lopecode-channel: WebSocket server on ws://127.0.0.1:${PORT}/ws\n`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lopecode/channel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Pair program with Claude inside Lopecode notebooks. MCP server bridging browser notebooks and Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"lopecode-channel.ts",
|
|
12
12
|
"claude-code-pairing-module.js",
|
|
13
|
-
"
|
|
13
|
+
"sync-module.ts"
|
|
14
14
|
],
|
|
15
15
|
"keywords": [
|
|
16
16
|
"mcp",
|
package/sync-module.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Syncs a module between files. Works with .js module files and .html notebook files.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun tools/channel/sync-module.ts --module @author/name --source src --target dest.html
|
|
7
|
+
* bun tools/channel/sync-module.ts --module @author/name --source src --target dest.html --watch
|
|
8
|
+
*
|
|
9
|
+
* Source can be:
|
|
10
|
+
* - A .js file containing the module's define() function
|
|
11
|
+
* - A .html notebook file containing a <script id="@author/name"> block
|
|
12
|
+
*
|
|
13
|
+
* If source is a .js file that doesn't exist, extracts the module from target first,
|
|
14
|
+
* creating the .js file as a starting point for editing.
|
|
15
|
+
*
|
|
16
|
+
* Target must be an .html notebook file.
|
|
17
|
+
*
|
|
18
|
+
* This:
|
|
19
|
+
* 1. Reads module content from source (.js file or .html <script> block)
|
|
20
|
+
* 2. If the module <script> already exists in target, replaces its content (upsert)
|
|
21
|
+
* 3. If not, inserts it before the bootloader marker
|
|
22
|
+
* 4. Ensures the module is in bootconf.json mains
|
|
23
|
+
* 5. Updates the hash URL to include the module in the layout
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { readFileSync, writeFileSync, existsSync, watch } from "fs";
|
|
27
|
+
import { resolve, extname } from "path";
|
|
28
|
+
|
|
29
|
+
function parseArgs() {
|
|
30
|
+
const args = process.argv.slice(2);
|
|
31
|
+
let moduleName = "";
|
|
32
|
+
let sourcePath = "";
|
|
33
|
+
let targetPath = "";
|
|
34
|
+
let watchMode = false;
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < args.length; i++) {
|
|
37
|
+
switch (args[i]) {
|
|
38
|
+
case "--module":
|
|
39
|
+
moduleName = args[++i];
|
|
40
|
+
break;
|
|
41
|
+
case "--source":
|
|
42
|
+
sourcePath = args[++i];
|
|
43
|
+
break;
|
|
44
|
+
case "--target":
|
|
45
|
+
targetPath = args[++i];
|
|
46
|
+
break;
|
|
47
|
+
case "--watch":
|
|
48
|
+
watchMode = true;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!moduleName || !sourcePath || !targetPath) {
|
|
54
|
+
console.error(
|
|
55
|
+
"Usage: bun tools/channel/sync-module.ts --module <@author/name> --source <file> --target <notebook.html> [--watch]"
|
|
56
|
+
);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
moduleName,
|
|
62
|
+
sourcePath: resolve(sourcePath),
|
|
63
|
+
targetPath: resolve(targetPath),
|
|
64
|
+
watchMode,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function extractModuleFromHtml(html: string, moduleId: string): string | null {
|
|
69
|
+
const escaped = moduleId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
70
|
+
const pattern = new RegExp(
|
|
71
|
+
`<script\\s+id="${escaped}"[^>]*>[\\s\\S]*?</script>`
|
|
72
|
+
);
|
|
73
|
+
const m = html.match(pattern);
|
|
74
|
+
return m ? m[0] : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function extractModuleContent(html: string, moduleId: string): string | null {
|
|
78
|
+
const escaped = moduleId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
79
|
+
const pattern = new RegExp(
|
|
80
|
+
`<script\\s+id="${escaped}"[^>]*>([\\s\\S]*?)</script>`
|
|
81
|
+
);
|
|
82
|
+
const m = html.match(pattern);
|
|
83
|
+
return m ? m[1].replace(/^\n/, "").replace(/\n$/, "") : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readModuleSource(
|
|
87
|
+
sourcePath: string,
|
|
88
|
+
moduleId: string
|
|
89
|
+
): { content: string; isJs: boolean } {
|
|
90
|
+
const ext = extname(sourcePath).toLowerCase();
|
|
91
|
+
|
|
92
|
+
if (ext === ".js" || ext === ".ts") {
|
|
93
|
+
return { content: readFileSync(sourcePath, "utf8"), isJs: true };
|
|
94
|
+
} else if (ext === ".html") {
|
|
95
|
+
const html = readFileSync(sourcePath, "utf8");
|
|
96
|
+
const content = extractModuleContent(html, moduleId);
|
|
97
|
+
if (!content) {
|
|
98
|
+
console.error(
|
|
99
|
+
`Module ${moduleId} not found in ${sourcePath}`
|
|
100
|
+
);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
return { content, isJs: false };
|
|
104
|
+
} else {
|
|
105
|
+
console.error(`Unsupported source file type: ${ext}`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function inject(
|
|
111
|
+
sourcePath: string,
|
|
112
|
+
targetPath: string,
|
|
113
|
+
moduleId: string
|
|
114
|
+
): void {
|
|
115
|
+
let html = readFileSync(targetPath, "utf8");
|
|
116
|
+
const { content, isJs } = readModuleSource(sourcePath, moduleId);
|
|
117
|
+
|
|
118
|
+
// Build the script block
|
|
119
|
+
const scriptBlock = `<script id="${moduleId}"\n type="text/plain"\n data-mime="application/javascript"\n>\n${content}\n</script>`;
|
|
120
|
+
|
|
121
|
+
// Check if module already exists in target
|
|
122
|
+
const escaped = moduleId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
123
|
+
const scriptPattern = new RegExp(
|
|
124
|
+
`<script\\s+id="${escaped}"[^>]*>[\\s\\S]*?</script>`
|
|
125
|
+
);
|
|
126
|
+
const existing = html.match(scriptPattern);
|
|
127
|
+
|
|
128
|
+
if (existing) {
|
|
129
|
+
// Replace in-place using string replacement (preserves backslashes)
|
|
130
|
+
html = html.replace(existing[0], scriptBlock);
|
|
131
|
+
console.log(`Updated existing ${moduleId} module`);
|
|
132
|
+
} else {
|
|
133
|
+
// Insert before bootloader
|
|
134
|
+
const bootconfMarker = "<!-- Bootloader -->";
|
|
135
|
+
const bootconfIdx = html.lastIndexOf(bootconfMarker);
|
|
136
|
+
if (bootconfIdx === -1) {
|
|
137
|
+
console.error("Could not find '<!-- Bootloader -->' marker in HTML");
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
html = html.slice(0, bootconfIdx) + scriptBlock + "\n\n" + html.slice(bootconfIdx);
|
|
141
|
+
console.log(`Inserted new ${moduleId} module`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Ensure module is in bootconf.json mains
|
|
145
|
+
html = ensureBootconf(html, moduleId);
|
|
146
|
+
|
|
147
|
+
writeFileSync(targetPath, html);
|
|
148
|
+
const size = (html.length / 1024 / 1024).toFixed(2);
|
|
149
|
+
console.log(`Wrote ${targetPath} (${size} MB)`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function ensureBootconf(html: string, moduleId: string): string {
|
|
153
|
+
const bootconfPattern = 'id="bootconf.json"';
|
|
154
|
+
let bootconfScriptStart = html.lastIndexOf(bootconfPattern);
|
|
155
|
+
if (bootconfScriptStart === -1) {
|
|
156
|
+
console.warn("Could not find bootconf.json — skipping mains/hash update");
|
|
157
|
+
return html;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const next200 = html.substring(
|
|
161
|
+
bootconfScriptStart,
|
|
162
|
+
bootconfScriptStart + 200
|
|
163
|
+
);
|
|
164
|
+
if (!next200.includes("application/json")) {
|
|
165
|
+
console.warn(
|
|
166
|
+
"Found bootconf.json but it's not application/json — skipping"
|
|
167
|
+
);
|
|
168
|
+
return html;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const bootconfContentStart = html.indexOf(">", bootconfScriptStart) + 1;
|
|
172
|
+
const bootconfContentEnd = html.indexOf("</script>", bootconfContentStart);
|
|
173
|
+
const bootconfContent = html.slice(bootconfContentStart, bootconfContentEnd);
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const bootconf = JSON.parse(bootconfContent);
|
|
177
|
+
|
|
178
|
+
// Add to mains if missing
|
|
179
|
+
if (!bootconf.mains.includes(moduleId)) {
|
|
180
|
+
bootconf.mains.push(moduleId);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Update hash to include module in layout
|
|
184
|
+
const currentHash: string = bootconf.hash || "";
|
|
185
|
+
if (!currentHash.includes(moduleId.split("/").pop()!)) {
|
|
186
|
+
const modulePattern = /S(\d+)\(([^)]+)\)/g;
|
|
187
|
+
const moduleRefs: { weight: number; module: string }[] = [];
|
|
188
|
+
let m;
|
|
189
|
+
while ((m = modulePattern.exec(currentHash)) !== null) {
|
|
190
|
+
moduleRefs.push({ weight: parseInt(m[1]), module: m[2] });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (moduleRefs.length > 0) {
|
|
194
|
+
const totalWeight = moduleRefs.reduce((sum, r) => sum + r.weight, 0);
|
|
195
|
+
const scaled = moduleRefs.map((r) => ({
|
|
196
|
+
weight: Math.round((r.weight / totalWeight) * 75),
|
|
197
|
+
module: r.module,
|
|
198
|
+
}));
|
|
199
|
+
scaled.push({ weight: 25, module: moduleId });
|
|
200
|
+
const parts = scaled
|
|
201
|
+
.map((r) => `S${r.weight}(${r.module})`)
|
|
202
|
+
.join(",");
|
|
203
|
+
bootconf.hash = `#view=R100(${parts})`;
|
|
204
|
+
} else {
|
|
205
|
+
bootconf.hash = `#view=R100(S75(@tomlarkworthy/debugger),S25(${moduleId}))`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const newBootconfContent =
|
|
210
|
+
"\n" + JSON.stringify(bootconf, null, 2) + "\n";
|
|
211
|
+
html =
|
|
212
|
+
html.slice(0, bootconfContentStart) +
|
|
213
|
+
newBootconfContent +
|
|
214
|
+
html.slice(bootconfContentEnd);
|
|
215
|
+
} catch (e: any) {
|
|
216
|
+
console.warn("Failed to parse bootconf.json:", e.message);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return html;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function extractToJs(targetPath: string, moduleId: string, jsPath: string): void {
|
|
223
|
+
const html = readFileSync(targetPath, "utf8");
|
|
224
|
+
const content = extractModuleContent(html, moduleId);
|
|
225
|
+
if (!content) {
|
|
226
|
+
console.error(`Module ${moduleId} not found in ${targetPath} — cannot extract`);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
writeFileSync(jsPath, content);
|
|
230
|
+
console.log(`Extracted ${moduleId} from ${targetPath} → ${jsPath}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// CLI
|
|
234
|
+
const { moduleName, sourcePath, targetPath, watchMode } = parseArgs();
|
|
235
|
+
|
|
236
|
+
// If source is .js and doesn't exist, extract from target first
|
|
237
|
+
const sourceExt = extname(sourcePath).toLowerCase();
|
|
238
|
+
if ((sourceExt === ".js" || sourceExt === ".ts") && !existsSync(sourcePath)) {
|
|
239
|
+
console.log(`Source ${sourcePath} not found — extracting from target`);
|
|
240
|
+
extractToJs(targetPath, moduleName, sourcePath);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Initial injection
|
|
244
|
+
inject(sourcePath, targetPath, moduleName);
|
|
245
|
+
|
|
246
|
+
if (watchMode) {
|
|
247
|
+
console.log(`Watching ${sourcePath} for changes...`);
|
|
248
|
+
let debounce: ReturnType<typeof setTimeout> | null = null;
|
|
249
|
+
watch(sourcePath, () => {
|
|
250
|
+
if (debounce) clearTimeout(debounce);
|
|
251
|
+
debounce = setTimeout(() => {
|
|
252
|
+
console.log(
|
|
253
|
+
`\n${new Date().toLocaleTimeString()} — source changed, re-injecting...`
|
|
254
|
+
);
|
|
255
|
+
try {
|
|
256
|
+
inject(sourcePath, targetPath, moduleName);
|
|
257
|
+
} catch (e: any) {
|
|
258
|
+
console.error("Injection failed:", e.message);
|
|
259
|
+
}
|
|
260
|
+
}, 200);
|
|
261
|
+
});
|
|
262
|
+
}
|
package/inject-module.js
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Injects the @tomlarkworthy/claude-code-pairing module into a lopecode notebook HTML.
|
|
4
|
-
*
|
|
5
|
-
* Usage: node tools/channel/inject-module.js <input.html> <output.html>
|
|
6
|
-
*
|
|
7
|
-
* This:
|
|
8
|
-
* 1. Reads the source notebook HTML
|
|
9
|
-
* 2. Inserts the claude-code-pairing module as a <script> block before the bootloader
|
|
10
|
-
* 3. Adds "@tomlarkworthy/claude-code-pairing" to bootconf.json mains
|
|
11
|
-
* 4. Updates the hash URL to include the channel module in the layout
|
|
12
|
-
* 5. Writes the result to output.html
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { readFileSync, writeFileSync } from "fs";
|
|
16
|
-
import { resolve } from "path";
|
|
17
|
-
|
|
18
|
-
const [,, inputPath, outputPath] = process.argv;
|
|
19
|
-
|
|
20
|
-
if (!inputPath || !outputPath) {
|
|
21
|
-
console.error("Usage: node tools/channel/inject-module.js <input.html> <output.html>");
|
|
22
|
-
process.exit(1);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const html = readFileSync(resolve(inputPath), "utf8");
|
|
26
|
-
const moduleSource = readFileSync(resolve(import.meta.dirname, "claude-code-pairing-module.js"), "utf8");
|
|
27
|
-
|
|
28
|
-
// 1. Find the bootconf.json script and insert our module before it
|
|
29
|
-
const bootconfMarker = '<!-- Bootloader -->';
|
|
30
|
-
const bootconfIdx = html.lastIndexOf(bootconfMarker);
|
|
31
|
-
if (bootconfIdx === -1) {
|
|
32
|
-
console.error("Could not find '<!-- Bootloader -->' marker in HTML");
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const moduleScript = `
|
|
37
|
-
<script id="@tomlarkworthy/claude-code-pairing"
|
|
38
|
-
type="text/plain"
|
|
39
|
-
data-mime="application/javascript"
|
|
40
|
-
>
|
|
41
|
-
${moduleSource}
|
|
42
|
-
</script>
|
|
43
|
-
|
|
44
|
-
`;
|
|
45
|
-
|
|
46
|
-
let result = html.slice(0, bootconfIdx) + moduleScript + html.slice(bootconfIdx);
|
|
47
|
-
|
|
48
|
-
// 2. Find the actual bootconf.json script block
|
|
49
|
-
// Look for the specific pattern: <script id="bootconf.json" with type="text/plain" and data-mime="application/json"
|
|
50
|
-
// Use lastIndexOf to skip templates embedded in exporter modules
|
|
51
|
-
const bootconfPattern = 'id="bootconf.json" \n type="text/plain"\n data-mime="application/json"';
|
|
52
|
-
let bootconfScriptStart = result.lastIndexOf(bootconfPattern);
|
|
53
|
-
if (bootconfScriptStart === -1) {
|
|
54
|
-
// Try alternative formatting
|
|
55
|
-
bootconfScriptStart = result.lastIndexOf('id="bootconf.json"');
|
|
56
|
-
// Verify it's followed by data-mime="application/json"
|
|
57
|
-
const next200 = result.substring(bootconfScriptStart, bootconfScriptStart + 200);
|
|
58
|
-
if (!next200.includes('application/json')) {
|
|
59
|
-
console.error("Could not find bootconf.json with application/json mime type");
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
if (bootconfScriptStart === -1) {
|
|
64
|
-
console.error("Could not find bootconf.json script block");
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
|
-
const bootconfContentStart = result.indexOf('>', bootconfScriptStart) + 1;
|
|
68
|
-
const bootconfContentEnd = result.indexOf('</script>', bootconfContentStart);
|
|
69
|
-
let bootconfContent = result.slice(bootconfContentStart, bootconfContentEnd);
|
|
70
|
-
|
|
71
|
-
// Parse the JSON-like content
|
|
72
|
-
try {
|
|
73
|
-
const bootconf = JSON.parse(bootconfContent);
|
|
74
|
-
|
|
75
|
-
// Add claude-code-pairing to mains
|
|
76
|
-
if (!bootconf.mains.includes("@tomlarkworthy/claude-code-pairing")) {
|
|
77
|
-
bootconf.mains.push("@tomlarkworthy/claude-code-pairing");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Update hash to include claude-code-pairing in layout
|
|
81
|
-
// Lopepage only supports flat R(S,S,...) — no nesting
|
|
82
|
-
// Extract existing module references and add ours as a new panel
|
|
83
|
-
const currentHash = bootconf.hash || "";
|
|
84
|
-
if (!currentHash.includes("claude-code-pairing")) {
|
|
85
|
-
// Parse existing modules from hash like R100(S70(@mod1),S30(@mod2))
|
|
86
|
-
const moduleRefs = [];
|
|
87
|
-
const modulePattern = /S(\d+)\(([^)]+)\)/g;
|
|
88
|
-
let m;
|
|
89
|
-
while ((m = modulePattern.exec(currentHash)) !== null) {
|
|
90
|
-
moduleRefs.push({ weight: parseInt(m[1]), module: m[2] });
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (moduleRefs.length > 0) {
|
|
94
|
-
// Scale existing weights to 75% and add claude-code-pairing at 25%
|
|
95
|
-
const totalWeight = moduleRefs.reduce((sum, r) => sum + r.weight, 0);
|
|
96
|
-
const scaled = moduleRefs.map(r => ({
|
|
97
|
-
weight: Math.round((r.weight / totalWeight) * 75),
|
|
98
|
-
module: r.module,
|
|
99
|
-
}));
|
|
100
|
-
scaled.push({ weight: 25, module: "@tomlarkworthy/claude-code-pairing" });
|
|
101
|
-
const parts = scaled.map(r => `S${r.weight}(${r.module})`).join(",");
|
|
102
|
-
bootconf.hash = `#view=R100(${parts})`;
|
|
103
|
-
} else {
|
|
104
|
-
// Simple fallback
|
|
105
|
-
bootconf.hash = "#view=R100(S75(@tomlarkworthy/debugger),S25(@tomlarkworthy/claude-code-pairing))";
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const newBootconfContent = "\n" + JSON.stringify(bootconf, null, 2) + "\n";
|
|
110
|
-
result = result.slice(0, bootconfContentStart) + newBootconfContent + result.slice(bootconfContentEnd);
|
|
111
|
-
} catch (e) {
|
|
112
|
-
console.error("Failed to parse bootconf.json:", e.message);
|
|
113
|
-
console.error("Content:", bootconfContent.slice(0, 200));
|
|
114
|
-
process.exit(1);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// 4. Update the title
|
|
118
|
-
const titleRegex = /<title>[^<]*<\/title>/;
|
|
119
|
-
const titleMatch = result.match(titleRegex);
|
|
120
|
-
if (titleMatch) {
|
|
121
|
-
const currentTitle = titleMatch[0].replace(/<\/?title>/g, "");
|
|
122
|
-
if (!currentTitle.includes("claude-channel")) {
|
|
123
|
-
result = result.replace(titleRegex, `<title>${currentTitle} + claude-channel</title>`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
writeFileSync(resolve(outputPath), result);
|
|
128
|
-
const inputSize = (html.length / 1024 / 1024).toFixed(2);
|
|
129
|
-
const outputSize = (result.length / 1024 / 1024).toFixed(2);
|
|
130
|
-
console.log(`Injected @tomlarkworthy/claude-code-pairing into ${outputPath}`);
|
|
131
|
-
console.log(`Input: ${inputSize} MB → Output: ${outputSize} MB`);
|