@lopecode/channel 0.1.3 → 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 +188 -97
- package/lopecode-channel.ts +85 -1
- package/package.json +2 -2
- package/sync-module.ts +262 -0
- package/inject-module.js +0 -131
|
@@ -23,15 +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, realize, runtime, 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;
|
|
34
|
-
|
|
41
|
+
// Cache the observer factory (used by lopepage to render cells)
|
|
42
|
+
var ojs_observer = window.__ojs_observer || null;
|
|
35
43
|
|
|
36
44
|
function serializeValue(value, maxLen) {
|
|
37
45
|
maxLen = maxLen || 500;
|
|
@@ -79,22 +87,21 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
79
87
|
var name = cmd.params.name;
|
|
80
88
|
var moduleName = cmd.params.module;
|
|
81
89
|
var targetModule = findModule(runtime, moduleName);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
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
|
+
});
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
case "define-variable": {
|
|
@@ -102,32 +109,22 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
102
109
|
var definition = cmd.params.definition;
|
|
103
110
|
var inputs = cmd.params.inputs || [];
|
|
104
111
|
var moduleName = cmd.params.module;
|
|
105
|
-
var mod = runtime
|
|
106
|
-
if (!mod && runtime.mains) {
|
|
107
|
-
for (var entry of runtime.mains) {
|
|
108
|
-
if (!FRAMEWORK_MODULES.has(entry[0])) { mod = entry[1]; break; }
|
|
109
|
-
}
|
|
110
|
-
}
|
|
112
|
+
var mod = findModule(runtime, moduleName);
|
|
111
113
|
if (!mod) return { ok: false, error: "Module not found: " + (moduleName || "default") };
|
|
112
114
|
|
|
113
115
|
return realize([definition], runtime).then(function(results) {
|
|
114
116
|
var fn = results[0];
|
|
115
117
|
if (typeof fn !== "function") return { ok: false, error: "Definition must evaluate to a function" };
|
|
116
118
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (v._name === name && v._module === mod) { existingVar = v; break; }
|
|
120
|
-
}
|
|
121
|
-
|
|
119
|
+
// Use module._scope to check for existing variable
|
|
120
|
+
var existingVar = mod._scope.get(name);
|
|
122
121
|
if (existingVar) {
|
|
123
122
|
existingVar.define(name, inputs, fn);
|
|
124
123
|
} else {
|
|
125
|
-
|
|
126
|
-
for (var v of runtime._variables) {
|
|
127
|
-
if (v._name === "__ojs_observer" && typeof v._value === "function") { obsFactory = v._value; break; }
|
|
128
|
-
}
|
|
129
|
-
mod.variable(obsFactory ? obsFactory(name) : {}).define(name, inputs, fn);
|
|
124
|
+
mod.variable(ojs_observer ? ojs_observer(name) : {}).define(name, inputs, fn);
|
|
130
125
|
}
|
|
126
|
+
// Auto-watch the defined variable so the result arrives reactively
|
|
127
|
+
watchVariable(runtime, name, moduleName || null);
|
|
131
128
|
return { ok: true, result: { success: true, name: name, module: moduleName || "default" } };
|
|
132
129
|
}).catch(function(e) {
|
|
133
130
|
return { ok: false, error: "define failed: " + e.message };
|
|
@@ -139,24 +136,23 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
139
136
|
var moduleName = cmd.params.module;
|
|
140
137
|
var targetModule = findModule(runtime, moduleName);
|
|
141
138
|
if (!targetModule) return { ok: false, error: "Module not found: " + (moduleName || "main") };
|
|
142
|
-
|
|
143
|
-
if (v
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
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
|
+
});
|
|
149
144
|
}
|
|
150
145
|
|
|
151
146
|
case "list-variables": {
|
|
152
147
|
var moduleName = cmd.params.module;
|
|
153
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
|
|
154
151
|
var variables = [];
|
|
155
|
-
for (var
|
|
156
|
-
|
|
157
|
-
if (targetModule && v._module !== targetModule) continue;
|
|
152
|
+
for (var entry of targetModule._scope) {
|
|
153
|
+
var v = entry[1];
|
|
158
154
|
variables.push({
|
|
159
|
-
name:
|
|
155
|
+
name: entry[0],
|
|
160
156
|
module: (v._module && v._module._name) || "main",
|
|
161
157
|
hasValue: v._value !== undefined,
|
|
162
158
|
hasError: v._error !== undefined,
|
|
@@ -173,6 +169,28 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
173
169
|
return runTests(runtime, filter, timeout);
|
|
174
170
|
}
|
|
175
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
|
+
|
|
176
194
|
case "eval": {
|
|
177
195
|
var code = cmd.params.code;
|
|
178
196
|
try {
|
|
@@ -302,17 +320,14 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
302
320
|
var key = (moduleName || "main") + ":" + name;
|
|
303
321
|
if (watchers.has(key)) return { ok: true, result: { already_watching: true, key: key } };
|
|
304
322
|
|
|
305
|
-
|
|
306
|
-
var
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
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) {
|
|
313
328
|
if (!targetVar) return { ok: false, error: "Variable not found: " + name };
|
|
314
329
|
|
|
315
|
-
var
|
|
330
|
+
var resolvedModule = (targetVar._module && targetVar._module._name) || "main";
|
|
316
331
|
var debounceTimer = null;
|
|
317
332
|
var latestValue = undefined;
|
|
318
333
|
var latestError = undefined;
|
|
@@ -328,19 +343,19 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
328
343
|
var watches = viewof_cc_watches.value.slice();
|
|
329
344
|
var found = false;
|
|
330
345
|
for (var i = 0; i < watches.length; i++) {
|
|
331
|
-
if (watches[i].name === name && watches[i].module ===
|
|
332
|
-
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 };
|
|
333
348
|
found = true;
|
|
334
349
|
break;
|
|
335
350
|
}
|
|
336
351
|
}
|
|
337
|
-
if (!found) watches.push({ name: name, module:
|
|
352
|
+
if (!found) watches.push({ name: name, module: resolvedModule, value: serialized.slice(0, 200), updated: now });
|
|
338
353
|
viewof_cc_watches.value = watches;
|
|
339
354
|
viewof_cc_watches.dispatchEvent(new Event("input"));
|
|
340
355
|
|
|
341
356
|
// Send over WebSocket if connected
|
|
342
357
|
if (!paired || !ws) return;
|
|
343
|
-
var msg = { type: "variable-update", name: name, module:
|
|
358
|
+
var msg = { type: "variable-update", name: name, module: resolvedModule };
|
|
344
359
|
if (latestError) { msg.error = latestError.message || String(latestError); }
|
|
345
360
|
else { msg.value = serialized; }
|
|
346
361
|
ws.send(JSON.stringify(msg));
|
|
@@ -370,7 +385,7 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
370
385
|
cancel();
|
|
371
386
|
// Remove from watches table
|
|
372
387
|
var watches = viewof_cc_watches.value.filter(function(w) {
|
|
373
|
-
return !(w.name === name && w.module ===
|
|
388
|
+
return !(w.name === name && w.module === resolvedModule);
|
|
374
389
|
});
|
|
375
390
|
viewof_cc_watches.value = watches;
|
|
376
391
|
viewof_cc_watches.dispatchEvent(new Event("input"));
|
|
@@ -379,6 +394,7 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
379
394
|
});
|
|
380
395
|
|
|
381
396
|
return { ok: true, result: { watching: true, key: key } };
|
|
397
|
+
}); // close lookupVariable.then
|
|
382
398
|
}
|
|
383
399
|
|
|
384
400
|
function unwatchVariable(name, moduleName) {
|
|
@@ -421,6 +437,11 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
421
437
|
function connect(token) {
|
|
422
438
|
if (ws) { ws.close(); ws = null; }
|
|
423
439
|
|
|
440
|
+
// Persist token for reconnection across reloads
|
|
441
|
+
if (token) {
|
|
442
|
+
try { sessionStorage.setItem("lopecode_cc_token", token); } catch(e) {}
|
|
443
|
+
}
|
|
444
|
+
|
|
424
445
|
// Parse port from token format LOPE-PORT-XXXX
|
|
425
446
|
var connectPort = port;
|
|
426
447
|
if (token) {
|
|
@@ -472,11 +493,13 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
472
493
|
hash: location.hash
|
|
473
494
|
}));
|
|
474
495
|
|
|
475
|
-
//
|
|
496
|
+
// Set up watches from cc_watches (initialized with defaults via dependency resolution)
|
|
476
497
|
var runtime2 = window.__ojs_runtime;
|
|
477
498
|
if (runtime2) {
|
|
478
|
-
|
|
479
|
-
|
|
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
|
+
}
|
|
480
503
|
}
|
|
481
504
|
break;
|
|
482
505
|
|
|
@@ -496,6 +519,17 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
496
519
|
cc_messages.dispatchEvent(new Event("input"));
|
|
497
520
|
break;
|
|
498
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
|
+
|
|
499
533
|
case "command":
|
|
500
534
|
Promise.resolve(handleCommand(msg)).then(function(result) {
|
|
501
535
|
ws.send(JSON.stringify({
|
|
@@ -527,13 +561,23 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
|
|
|
527
561
|
ws.onerror = function() {};
|
|
528
562
|
}
|
|
529
563
|
|
|
530
|
-
// Auto-connect
|
|
531
|
-
// Hash format: #view=R100(...)&cc=LOPE-PORT-XXXX
|
|
564
|
+
// Auto-connect: check hash param first, then sessionStorage fallback
|
|
532
565
|
(function autoConnect() {
|
|
566
|
+
var token = null;
|
|
567
|
+
|
|
568
|
+
// 1. Check &cc=TOKEN in hash
|
|
533
569
|
var hash = location.hash || "";
|
|
534
570
|
var match = hash.match(/[&?]cc=(LOPE-[A-Z0-9-]+)/);
|
|
535
571
|
if (match) {
|
|
536
|
-
|
|
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) {
|
|
537
581
|
// Small delay to let the WebSocket server be ready
|
|
538
582
|
setTimeout(function() { connect(token); }, 500);
|
|
539
583
|
}
|
|
@@ -603,39 +647,41 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
|
|
|
603
647
|
disconnected: "#ef4444"
|
|
604
648
|
};
|
|
605
649
|
|
|
606
|
-
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
|
+
|
|
607
654
|
var tokenInput = document.createElement("input");
|
|
608
655
|
tokenInput.type = "text";
|
|
609
656
|
tokenInput.placeholder = "LOPE-XXXX";
|
|
610
|
-
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);";
|
|
611
658
|
|
|
612
659
|
var connectBtn = document.createElement("button");
|
|
613
660
|
connectBtn.textContent = "Connect";
|
|
614
|
-
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;";
|
|
615
662
|
connectBtn.onclick = function() {
|
|
616
663
|
var token = tokenInput.value.trim();
|
|
617
664
|
if (token) cc_ws.connect(token);
|
|
618
665
|
};
|
|
619
|
-
|
|
620
666
|
tokenInput.addEventListener("keydown", function(e) {
|
|
621
667
|
if (e.key === "Enter") connectBtn.click();
|
|
622
668
|
});
|
|
623
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
|
+
|
|
624
683
|
var container = document.createElement("div");
|
|
625
|
-
container.
|
|
626
|
-
container.innerHTML = '<div style="padding:24px;max-width:400px;margin:0 auto;text-align:center;">' +
|
|
627
|
-
'<div style="font-size:24px;margin-bottom:8px;">Claude Channel</div>' +
|
|
628
|
-
'<p style="color:#6b7280;margin-bottom:20px;font-size:14px;">Connect to Claude Code to chat with Claude from this notebook.</p>' +
|
|
629
|
-
'<div style="text-align:left;background:#f3f4f6;padding:16px;border-radius:8px;margin-bottom:20px;font-size:13px;">' +
|
|
630
|
-
'<div style="font-weight:600;margin-bottom:8px;">Setup:</div>' +
|
|
631
|
-
'<ol style="margin:0;padding-left:20px;line-height:1.8;">' +
|
|
632
|
-
'<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>' +
|
|
633
|
-
'<li>Copy the pairing token from the terminal</li>' +
|
|
634
|
-
'<li>Paste it below and click Connect</li>' +
|
|
635
|
-
'</ol></div>' +
|
|
636
|
-
'<div style="display:flex;gap:8px;justify-content:center;align-items:center;" class="cc-token-row"></div>' +
|
|
637
|
-
'</div>';
|
|
638
|
-
container.querySelector(".cc-token-row").append(tokenInput, connectBtn);
|
|
684
|
+
container.append(row, guide);
|
|
639
685
|
return container;
|
|
640
686
|
}
|
|
641
687
|
|
|
@@ -648,11 +694,11 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
|
|
|
648
694
|
textarea.className = "cc-input";
|
|
649
695
|
textarea.placeholder = "Message Claude...";
|
|
650
696
|
textarea.rows = 2;
|
|
651
|
-
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);";
|
|
652
698
|
|
|
653
699
|
var sendBtn = document.createElement("button");
|
|
654
700
|
sendBtn.textContent = "Send";
|
|
655
|
-
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;";
|
|
656
702
|
|
|
657
703
|
function sendMessage() {
|
|
658
704
|
var text = textarea.value.trim();
|
|
@@ -670,7 +716,7 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
|
|
|
670
716
|
});
|
|
671
717
|
|
|
672
718
|
var inputRow = document.createElement("div");
|
|
673
|
-
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);";
|
|
674
720
|
inputRow.append(textarea, sendBtn);
|
|
675
721
|
|
|
676
722
|
var container = document.createElement("div");
|
|
@@ -680,15 +726,47 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
|
|
|
680
726
|
|
|
681
727
|
function updateMessages() {
|
|
682
728
|
messagesDiv.innerHTML = "";
|
|
683
|
-
|
|
684
|
-
|
|
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
|
+
|
|
685
763
|
var bubble = document.createElement("div");
|
|
686
764
|
bubble.className = "cc-msg cc-msg-" + msg.role;
|
|
687
765
|
var isUser = msg.role === "user";
|
|
688
766
|
bubble.style.cssText = "max-width:80%;padding:10px 14px;border-radius:12px;font-size:14px;line-height:1.5;" +
|
|
689
767
|
(isUser
|
|
690
|
-
? "align-self:flex-end;background
|
|
691
|
-
: "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;");
|
|
692
770
|
|
|
693
771
|
if (isUser) {
|
|
694
772
|
bubble.textContent = msg.content;
|
|
@@ -700,6 +778,7 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
|
|
|
700
778
|
} catch(e) { bubble.textContent = msg.content; }
|
|
701
779
|
}
|
|
702
780
|
messagesDiv.appendChild(bubble);
|
|
781
|
+
i++;
|
|
703
782
|
}
|
|
704
783
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
705
784
|
}
|
|
@@ -709,13 +788,16 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
|
|
|
709
788
|
return container;
|
|
710
789
|
}
|
|
711
790
|
|
|
791
|
+
var isConnected = (cc_status.value === "connected");
|
|
792
|
+
|
|
712
793
|
var wrapper = document.createElement("div");
|
|
713
794
|
wrapper.className = "cc-chat";
|
|
714
|
-
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;" : "");
|
|
715
797
|
|
|
716
798
|
var statusBar = document.createElement("div");
|
|
717
799
|
statusBar.className = "cc-status-bar";
|
|
718
|
-
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);";
|
|
719
801
|
|
|
720
802
|
var statusDot = document.createElement("span");
|
|
721
803
|
statusDot.style.cssText = "width:8px;height:8px;border-radius:50%;display:inline-block;";
|
|
@@ -735,18 +817,21 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
|
|
|
735
817
|
|
|
736
818
|
function render() {
|
|
737
819
|
var status = cc_status.value || "disconnected";
|
|
820
|
+
var connected = (status === "connected");
|
|
738
821
|
statusDot.style.background = statusColors[status] || "#ef4444";
|
|
739
|
-
statusText.textContent =
|
|
822
|
+
statusText.textContent = connected ? "Connected to Claude Code"
|
|
740
823
|
: (status === "connecting" || status === "pairing") ? "Connecting..."
|
|
741
824
|
: "Not connected";
|
|
742
825
|
|
|
826
|
+
wrapper.style.height = connected ? "400px" : "";
|
|
827
|
+
|
|
743
828
|
body.innerHTML = "";
|
|
744
|
-
if (
|
|
829
|
+
if (connected) {
|
|
745
830
|
chatView = renderChat();
|
|
746
831
|
body.appendChild(chatView);
|
|
747
832
|
} else {
|
|
748
833
|
chatView = null;
|
|
749
|
-
body.appendChild(
|
|
834
|
+
body.appendChild(renderConnect());
|
|
750
835
|
}
|
|
751
836
|
}
|
|
752
837
|
|
|
@@ -788,7 +873,9 @@ export default function define(runtime, observer) {
|
|
|
788
873
|
$def("_cc_messages", "cc_messages", ["Inputs"], _cc_messages);
|
|
789
874
|
$def("_cc_watches", "viewof cc_watches", ["Inputs"], _cc_watches);
|
|
790
875
|
main.variable().define("cc_watches", ["Generators", "viewof cc_watches"], (G, v) => G.input(v));
|
|
791
|
-
$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);
|
|
792
879
|
$def("_cc_change_forwarder", "cc_change_forwarder", ["cc_ws","invalidation"], _cc_change_forwarder);
|
|
793
880
|
|
|
794
881
|
// Imports
|
|
@@ -800,6 +887,10 @@ export default function define(runtime, observer) {
|
|
|
800
887
|
main.define("runtime_variables", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("runtime_variables", _));
|
|
801
888
|
main.define("observe", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("observe", _));
|
|
802
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", _));
|
|
803
894
|
main.define("runtime", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("runtime", _));
|
|
804
895
|
main.define("module d/57d79353bac56631@44", async () => runtime.module((await import("/d/57d79353bac56631@44.js?v=4")).default));
|
|
805
896
|
main.define("hash", ["module d/57d79353bac56631@44", "@variable"], (_, v) => v.import("hash", _));
|
package/lopecode-channel.ts
CHANGED
|
@@ -90,6 +90,7 @@ 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
|
|
@@ -250,6 +251,30 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
250
251
|
},
|
|
251
252
|
},
|
|
252
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
|
+
},
|
|
253
278
|
{
|
|
254
279
|
name: "watch_variable",
|
|
255
280
|
description: "Subscribe to reactive updates for a variable. Changes are pushed as notifications.",
|
|
@@ -345,6 +370,14 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
345
370
|
timeout = 120000;
|
|
346
371
|
params = { suffix: args.suffix || null };
|
|
347
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;
|
|
348
381
|
case "watch_variable":
|
|
349
382
|
action = "watch";
|
|
350
383
|
params = { name: args.name, module: args.module || null };
|
|
@@ -544,7 +577,7 @@ await mcp.connect(new StdioServerTransport());
|
|
|
544
577
|
const server = Bun.serve({
|
|
545
578
|
port: REQUESTED_PORT,
|
|
546
579
|
hostname: "127.0.0.1",
|
|
547
|
-
fetch(req, server) {
|
|
580
|
+
async fetch(req, server) {
|
|
548
581
|
const url = new URL(req.url);
|
|
549
582
|
if (url.pathname === "/ws") {
|
|
550
583
|
if (server.upgrade(req)) return;
|
|
@@ -556,6 +589,51 @@ const server = Bun.serve({
|
|
|
556
589
|
pending: pendingConnections.size,
|
|
557
590
|
}), { headers: { "content-type": "application/json" } });
|
|
558
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
|
+
}
|
|
559
637
|
if (url.pathname === "/") {
|
|
560
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}`;
|
|
561
639
|
return new Response(null, {
|
|
@@ -576,5 +654,11 @@ const server = Bun.serve({
|
|
|
576
654
|
|
|
577
655
|
PORT = server.port; // read actual port (important when REQUESTED_PORT is 0)
|
|
578
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
|
+
|
|
579
663
|
process.stderr.write(`lopecode-channel: pairing token: ${PAIRING_TOKEN}\n`);
|
|
580
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`);
|