@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.
@@ -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
- for (var v of runtime._variables) {
83
- if (v._name === name && (!targetModule || v._module === targetModule)) {
84
- return {
85
- ok: true,
86
- result: {
87
- name: v._name,
88
- hasValue: v._value !== undefined,
89
- hasError: v._error !== undefined,
90
- value: serializeValue(v._value),
91
- error: v._error ? v._error.message : undefined,
92
- reachable: v._reachable
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.mains && runtime.mains.get(moduleName);
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
- var existingVar = null;
118
- for (var v of runtime._variables) {
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
- var obsFactory = null;
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
- for (var v of runtime._variables) {
143
- if (v._name === name && v._module === targetModule) {
144
- v.delete();
145
- return { ok: true, result: { success: true, name: name, module: targetModule._name || "main" } };
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 v of runtime._variables) {
156
- if (!v._name) continue;
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: v._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
- var targetModule = findModule(runtime, moduleName);
306
- var targetVar = null;
307
- for (var v of runtime._variables) {
308
- if (v._name === name && (!targetModule || v._module === targetModule)) {
309
- targetVar = v;
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 moduleName = (targetVar._module && targetVar._module._name) || "main";
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 === moduleName) {
332
- watches[i] = { name: name, module: moduleName, value: serialized.slice(0, 200), updated: now };
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: moduleName, value: serialized.slice(0, 200), updated: now });
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: moduleName };
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 === moduleName);
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
- // Auto-watch default variables
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
- watchVariable(runtime2, "hash", null);
479
- watchVariable(runtime2, "currentModules", null);
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 if cc=TOKEN is in the hash fragment
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
- var token = match[1];
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 renderSetup() {
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 #d1d5db;border-radius:6px;width:140px;text-transform:uppercase;letter-spacing:2px;";
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:#2563eb;color:white;border:none;border-radius:6px;cursor:pointer;font-size:14px;font-weight:500;";
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.className = "cc-setup";
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 #d1d5db;border-radius:8px;padding:10px 12px;font-family:inherit;font-size:14px;outline:none;";
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:#2563eb;color:white;border:none;border-radius:6px;cursor:pointer;font-size:13px;font-weight:500;margin-left:auto;";
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 #e5e7eb;";
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
- for (var i = 0; i < cc_messages.value.length; i++) {
684
- var msg = cc_messages.value[i];
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:#2563eb;color:white;border-bottom-right-radius:4px;"
691
- : "align-self:flex-start;background:#f3f4f6;color:#1f2937;border-bottom-left-radius:4px;");
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 #e5e7eb;border-radius:12px;overflow:hidden;height:400px;display:flex;flex-direction:column;background:white;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;";
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 #e5e7eb;font-size:12px;color:#6b7280;background:#fafafa;";
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 = status === "connected" ? "Connected to Claude Code"
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 (status === "connected") {
829
+ if (connected) {
745
830
  chatView = renderChat();
746
831
  body.appendChild(chatView);
747
832
  } else {
748
833
  chatView = null;
749
- body.appendChild(renderSetup());
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("_cc_ws", "cc_ws", ["cc_config","cc_notebook_id","cc_status","cc_messages","viewof cc_watches","summarizeJS","observe","realize","runtime","invalidation"], _cc_ws);
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", _));
@@ -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",
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
- "inject-module.js"
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`);