@lopecode/channel 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,14 +23,23 @@ const _cc_messages = function _cc_messages(Inputs){return(
23
23
  )};
24
24
 
25
25
  const _cc_watches = function _cc_watches(Inputs){return(
26
- Inputs.input([])
26
+ Inputs.input([
27
+ { name: "hash", module: null },
28
+ { name: "currentModules", module: null }
29
+ ])
30
+ )};
31
+
32
+ const _cc_module = function _cc_module(thisModule){return(
33
+ thisModule()
27
34
  )};
28
35
 
29
- const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages, viewof_cc_watches, summarizeJS, observe, invalidation){return(
36
+ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages, viewof_cc_watches, summarizeJS, observe, realize, createModule, deleteModule, lookupVariable, cc_module, runtime, invalidation){return(
30
37
  (function() {
31
38
  var port = cc_config.port, host = cc_config.host;
32
39
  var ws = null;
33
40
  var paired = false;
41
+ // Cache the observer factory (used by lopepage to render cells)
42
+ var ojs_observer = window.__ojs_observer || null;
34
43
 
35
44
  function serializeValue(value, maxLen) {
36
45
  maxLen = maxLen || 500;
@@ -38,12 +47,25 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
38
47
  catch(e) { return String(value).slice(0, maxLen); }
39
48
  }
40
49
 
50
+ // Framework modules that should not be the default target for define_variable
51
+ var FRAMEWORK_MODULES = new Set([
52
+ "bootloader", "builtin",
53
+ "@tomlarkworthy/lopepage",
54
+ "@tomlarkworthy/claude-code-pairing",
55
+ "@tomlarkworthy/module-selection"
56
+ ]);
57
+
41
58
  function findModule(runtime, moduleName) {
42
- if (!moduleName) return null; // no module filter match any module
43
- for (var v of runtime._variables) {
44
- if (v._module && v._module._name === moduleName) return v._module;
45
- if (v._name && v._name.startsWith("module ") && v._name === "module " + moduleName)
46
- return v._module;
59
+ // Use runtime.mains (Map<name, Module>) the actual module refs used by the runtime
60
+ var mains = runtime.mains;
61
+ if (mains && mains instanceof Map) {
62
+ if (moduleName) {
63
+ return mains.get(moduleName) || null;
64
+ }
65
+ // No name specified — return the first non-framework main
66
+ for (var entry of mains) {
67
+ if (!FRAMEWORK_MODULES.has(entry[0])) return entry[1];
68
+ }
47
69
  }
48
70
  return null;
49
71
  }
@@ -65,22 +87,21 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
65
87
  var name = cmd.params.name;
66
88
  var moduleName = cmd.params.module;
67
89
  var targetModule = findModule(runtime, moduleName);
68
- for (var v of runtime._variables) {
69
- if (v._name === name && (!targetModule || v._module === targetModule)) {
70
- return {
71
- ok: true,
72
- result: {
73
- name: v._name,
74
- hasValue: v._value !== undefined,
75
- hasError: v._error !== undefined,
76
- value: serializeValue(v._value),
77
- error: v._error ? v._error.message : undefined,
78
- reachable: v._reachable
79
- }
80
- };
81
- }
82
- }
83
- return { ok: false, error: "Variable not found: " + name };
90
+ if (!targetModule) return { ok: false, error: "Module not found: " + (moduleName || "default") };
91
+ return lookupVariable(name, targetModule).then(function(v) {
92
+ if (!v) return { ok: false, error: "Variable not found: " + name };
93
+ return {
94
+ ok: true,
95
+ result: {
96
+ name: v._name,
97
+ hasValue: v._value !== undefined,
98
+ hasError: v._error !== undefined,
99
+ value: serializeValue(v._value),
100
+ error: v._error ? v._error.message : undefined,
101
+ reachable: v._reachable
102
+ }
103
+ };
104
+ });
84
105
  }
85
106
 
86
107
  case "define-variable": {
@@ -88,31 +109,26 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
88
109
  var definition = cmd.params.definition;
89
110
  var inputs = cmd.params.inputs || [];
90
111
  var moduleName = cmd.params.module;
91
- var targetModule = findModule(runtime, moduleName);
92
- if (!targetModule) return { ok: false, error: "Module not found: " + (moduleName || "main") };
112
+ var mod = findModule(runtime, moduleName);
113
+ if (!mod) return { ok: false, error: "Module not found: " + (moduleName || "default") };
93
114
 
94
- var fn;
95
- try {
96
- eval("fn = " + definition);
115
+ return realize([definition], runtime).then(function(results) {
116
+ var fn = results[0];
97
117
  if (typeof fn !== "function") return { ok: false, error: "Definition must evaluate to a function" };
98
- } catch (e) {
99
- return { ok: false, error: "Failed to parse definition: " + e.message };
100
- }
101
-
102
- var existingVar = null;
103
- for (var v of runtime._variables) {
104
- if (v._name === name && v._module === targetModule) { existingVar = v; break; }
105
- }
106
118
 
107
- try {
108
- if (existingVar) { existingVar.define(name, inputs, fn); }
109
- else { var nv = targetModule.variable({}); nv.define(name, inputs, fn); }
110
- var actualRuntime = findActualRuntime(runtime);
111
- if (actualRuntime && actualRuntime._computeNow) actualRuntime._computeNow();
112
- return { ok: true, result: { success: true, name: name, module: targetModule._name || "main", redefined: !!existingVar } };
113
- } catch (e) {
114
- return { ok: false, error: "Failed to define variable: " + e.message };
115
- }
119
+ // Use module._scope to check for existing variable
120
+ var existingVar = mod._scope.get(name);
121
+ if (existingVar) {
122
+ existingVar.define(name, inputs, fn);
123
+ } else {
124
+ mod.variable(ojs_observer ? ojs_observer(name) : {}).define(name, inputs, fn);
125
+ }
126
+ // Auto-watch the defined variable so the result arrives reactively
127
+ watchVariable(runtime, name, moduleName || null);
128
+ return { ok: true, result: { success: true, name: name, module: moduleName || "default" } };
129
+ }).catch(function(e) {
130
+ return { ok: false, error: "define failed: " + e.message };
131
+ });
116
132
  }
117
133
 
118
134
  case "delete-variable": {
@@ -120,24 +136,23 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
120
136
  var moduleName = cmd.params.module;
121
137
  var targetModule = findModule(runtime, moduleName);
122
138
  if (!targetModule) return { ok: false, error: "Module not found: " + (moduleName || "main") };
123
- for (var v of runtime._variables) {
124
- if (v._name === name && v._module === targetModule) {
125
- v.delete();
126
- return { ok: true, result: { success: true, name: name, module: targetModule._name || "main" } };
127
- }
128
- }
129
- return { ok: false, error: "Variable not found: " + name + " in module " + (moduleName || "main") };
139
+ return lookupVariable(name, targetModule).then(function(v) {
140
+ if (!v) return { ok: false, error: "Variable not found: " + name + " in module " + (moduleName || "main") };
141
+ v.delete();
142
+ return { ok: true, result: { success: true, name: name, module: targetModule._name || "main" } };
143
+ });
130
144
  }
131
145
 
132
146
  case "list-variables": {
133
147
  var moduleName = cmd.params.module;
134
148
  var targetModule = findModule(runtime, moduleName);
149
+ if (!targetModule) return { ok: false, error: "Module not found: " + (moduleName || "default") };
150
+ // Use module._scope instead of scanning runtime._variables
135
151
  var variables = [];
136
- for (var v of runtime._variables) {
137
- if (!v._name) continue;
138
- if (targetModule && v._module !== targetModule) continue;
152
+ for (var entry of targetModule._scope) {
153
+ var v = entry[1];
139
154
  variables.push({
140
- name: v._name,
155
+ name: entry[0],
141
156
  module: (v._module && v._module._name) || "main",
142
157
  hasValue: v._value !== undefined,
143
158
  hasError: v._error !== undefined,
@@ -154,6 +169,28 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
154
169
  return runTests(runtime, filter, timeout);
155
170
  }
156
171
 
172
+ case "create-module": {
173
+ var moduleName = cmd.params.name;
174
+ if (!moduleName) return { ok: false, error: "Module name is required" };
175
+ try {
176
+ createModule(moduleName, runtime);
177
+ return { ok: true, result: { success: true, name: moduleName } };
178
+ } catch (e) {
179
+ return { ok: false, error: e.message };
180
+ }
181
+ }
182
+
183
+ case "delete-module": {
184
+ var moduleName = cmd.params.name;
185
+ if (!moduleName) return { ok: false, error: "Module name is required" };
186
+ try {
187
+ deleteModule(moduleName, runtime);
188
+ return { ok: true, result: { success: true, name: moduleName } };
189
+ } catch (e) {
190
+ return { ok: false, error: e.message };
191
+ }
192
+ }
193
+
157
194
  case "eval": {
158
195
  var code = cmd.params.code;
159
196
  try {
@@ -283,17 +320,14 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
283
320
  var key = (moduleName || "main") + ":" + name;
284
321
  if (watchers.has(key)) return { ok: true, result: { already_watching: true, key: key } };
285
322
 
286
- var targetModule = findModule(runtime, moduleName);
287
- var targetVar = null;
288
- for (var v of runtime._variables) {
289
- if (v._name === name && (!targetModule || v._module === targetModule)) {
290
- targetVar = v;
291
- break;
292
- }
293
- }
323
+ // Use findModule for named modules, cc_module (thisModule) for unqualified lookups
324
+ var targetModule = moduleName ? findModule(runtime, moduleName) : cc_module;
325
+ if (!targetModule) return Promise.resolve({ ok: false, error: "Module not found: " + (moduleName || "default") });
326
+
327
+ return lookupVariable(name, targetModule).then(function(targetVar) {
294
328
  if (!targetVar) return { ok: false, error: "Variable not found: " + name };
295
329
 
296
- var moduleName = (targetVar._module && targetVar._module._name) || "main";
330
+ var resolvedModule = (targetVar._module && targetVar._module._name) || "main";
297
331
  var debounceTimer = null;
298
332
  var latestValue = undefined;
299
333
  var latestError = undefined;
@@ -309,19 +343,19 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
309
343
  var watches = viewof_cc_watches.value.slice();
310
344
  var found = false;
311
345
  for (var i = 0; i < watches.length; i++) {
312
- if (watches[i].name === name && watches[i].module === moduleName) {
313
- 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 };
314
348
  found = true;
315
349
  break;
316
350
  }
317
351
  }
318
- 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 });
319
353
  viewof_cc_watches.value = watches;
320
354
  viewof_cc_watches.dispatchEvent(new Event("input"));
321
355
 
322
356
  // Send over WebSocket if connected
323
357
  if (!paired || !ws) return;
324
- var msg = { type: "variable-update", name: name, module: moduleName };
358
+ var msg = { type: "variable-update", name: name, module: resolvedModule };
325
359
  if (latestError) { msg.error = latestError.message || String(latestError); }
326
360
  else { msg.value = serialized; }
327
361
  ws.send(JSON.stringify(msg));
@@ -351,7 +385,7 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
351
385
  cancel();
352
386
  // Remove from watches table
353
387
  var watches = viewof_cc_watches.value.filter(function(w) {
354
- return !(w.name === name && w.module === moduleName);
388
+ return !(w.name === name && w.module === resolvedModule);
355
389
  });
356
390
  viewof_cc_watches.value = watches;
357
391
  viewof_cc_watches.dispatchEvent(new Event("input"));
@@ -360,6 +394,7 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
360
394
  });
361
395
 
362
396
  return { ok: true, result: { watching: true, key: key } };
397
+ }); // close lookupVariable.then
363
398
  }
364
399
 
365
400
  function unwatchVariable(name, moduleName) {
@@ -381,7 +416,7 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
381
416
  function handleFork(runtime) {
382
417
  return new Promise(function(resolve) {
383
418
  for (var v of runtime._variables) {
384
- if (v._name === "_exportToHTML" && typeof v._value === "function") {
419
+ if ((v._name === "_exportToHTML" || v._name === "exportToHTML") && typeof v._value === "function") {
385
420
  try {
386
421
  Promise.resolve(v._value()).then(function(html) {
387
422
  resolve({ ok: true, result: { html: html } });
@@ -402,6 +437,11 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
402
437
  function connect(token) {
403
438
  if (ws) { ws.close(); ws = null; }
404
439
 
440
+ // Persist token for reconnection across reloads
441
+ if (token) {
442
+ try { sessionStorage.setItem("lopecode_cc_token", token); } catch(e) {}
443
+ }
444
+
405
445
  // Parse port from token format LOPE-PORT-XXXX
406
446
  var connectPort = port;
407
447
  if (token) {
@@ -453,11 +493,13 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
453
493
  hash: location.hash
454
494
  }));
455
495
 
456
- // Auto-watch default variables
496
+ // Set up watches from cc_watches (initialized with defaults via dependency resolution)
457
497
  var runtime2 = window.__ojs_runtime;
458
498
  if (runtime2) {
459
- watchVariable(runtime2, "hash", null);
460
- 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
+ }
461
503
  }
462
504
  break;
463
505
 
@@ -477,6 +519,17 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
477
519
  cc_messages.dispatchEvent(new Event("input"));
478
520
  break;
479
521
 
522
+ case "tool-activity":
523
+ var msgs2 = cc_messages.value.concat([{
524
+ role: "tool",
525
+ tool_name: msg.tool_name,
526
+ content: msg.summary,
527
+ timestamp: msg.timestamp || Date.now()
528
+ }]);
529
+ cc_messages.value = msgs2;
530
+ cc_messages.dispatchEvent(new Event("input"));
531
+ break;
532
+
480
533
  case "command":
481
534
  Promise.resolve(handleCommand(msg)).then(function(result) {
482
535
  ws.send(JSON.stringify({
@@ -508,13 +561,23 @@ const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages
508
561
  ws.onerror = function() {};
509
562
  }
510
563
 
511
- // Auto-connect if cc=TOKEN is in the hash fragment
512
- // Hash format: #view=R100(...)&cc=LOPE-PORT-XXXX
564
+ // Auto-connect: check hash param first, then sessionStorage fallback
513
565
  (function autoConnect() {
566
+ var token = null;
567
+
568
+ // 1. Check &cc=TOKEN in hash
514
569
  var hash = location.hash || "";
515
570
  var match = hash.match(/[&?]cc=(LOPE-[A-Z0-9-]+)/);
516
571
  if (match) {
517
- 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) {
518
581
  // Small delay to let the WebSocket server be ready
519
582
  setTimeout(function() { connect(token); }, 500);
520
583
  }
@@ -584,39 +647,41 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
584
647
  disconnected: "#ef4444"
585
648
  };
586
649
 
587
- function 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
+
588
654
  var tokenInput = document.createElement("input");
589
655
  tokenInput.type = "text";
590
656
  tokenInput.placeholder = "LOPE-XXXX";
591
- tokenInput.style.cssText = "font-family:monospace;font-size:16px;padding:8px 12px;border:2px solid #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);";
592
658
 
593
659
  var connectBtn = document.createElement("button");
594
660
  connectBtn.textContent = "Connect";
595
- 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;";
596
662
  connectBtn.onclick = function() {
597
663
  var token = tokenInput.value.trim();
598
664
  if (token) cc_ws.connect(token);
599
665
  };
600
-
601
666
  tokenInput.addEventListener("keydown", function(e) {
602
667
  if (e.key === "Enter") connectBtn.click();
603
668
  });
604
669
 
670
+ row.append(tokenInput, connectBtn);
671
+
672
+ var guide = document.createElement("details");
673
+ guide.style.cssText = "padding:0 16px 16px;font-size:13px;color:var(--theme-foreground);";
674
+ guide.innerHTML = '<summary style="cursor:pointer;font-weight:600;font-size:14px;">Setup guide</summary>' +
675
+ '<ol style="margin:8px 0 0;padding-left:20px;line-height:1.8;">' +
676
+ '<li>Install <a href="https://bun.sh" target="_blank">Bun</a> if needed, then:<br>' +
677
+ '<code style="background:var(--theme-background-alt);padding:2px 6px;border-radius:3px;font-size:12px;">bun install -g @lopecode/channel</code><br>' +
678
+ '<code style="background:var(--theme-background-alt);padding:2px 6px;border-radius:3px;font-size:12px;">claude mcp add lopecode bunx @lopecode/channel</code></li>' +
679
+ '<li>Start Claude Code:<br><code style="background:var(--theme-background-alt);padding:2px 6px;border-radius:3px;font-size:12px;">claude --dangerously-load-development-channels server:lopecode</code></li>' +
680
+ '<li>Ask Claude to connect, or paste a pairing token above</li>' +
681
+ '</ol>';
682
+
605
683
  var container = document.createElement("div");
606
- container.className = "cc-setup";
607
- container.innerHTML = '<div style="padding:24px;max-width:400px;margin:0 auto;text-align:center;">' +
608
- '<div style="font-size:24px;margin-bottom:8px;">Claude Channel</div>' +
609
- '<p style="color:#6b7280;margin-bottom:20px;font-size:14px;">Connect to Claude Code to chat with Claude from this notebook.</p>' +
610
- '<div style="text-align:left;background:#f3f4f6;padding:16px;border-radius:8px;margin-bottom:20px;font-size:13px;">' +
611
- '<div style="font-weight:600;margin-bottom:8px;">Setup:</div>' +
612
- '<ol style="margin:0;padding-left:20px;line-height:1.8;">' +
613
- '<li>Start Claude with the channel flag:<br><code style="background:#e5e7eb;padding:2px 6px;border-radius:3px;font-size:12px;">claude --dangerously-load-development-channels server:lopecode</code></li>' +
614
- '<li>Copy the pairing token from the terminal</li>' +
615
- '<li>Paste it below and click Connect</li>' +
616
- '</ol></div>' +
617
- '<div style="display:flex;gap:8px;justify-content:center;align-items:center;" class="cc-token-row"></div>' +
618
- '</div>';
619
- container.querySelector(".cc-token-row").append(tokenInput, connectBtn);
684
+ container.append(row, guide);
620
685
  return container;
621
686
  }
622
687
 
@@ -629,11 +694,11 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
629
694
  textarea.className = "cc-input";
630
695
  textarea.placeholder = "Message Claude...";
631
696
  textarea.rows = 2;
632
- textarea.style.cssText = "width:100%;box-sizing:border-box;resize:none;border:1px solid #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);";
633
698
 
634
699
  var sendBtn = document.createElement("button");
635
700
  sendBtn.textContent = "Send";
636
- 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;";
637
702
 
638
703
  function sendMessage() {
639
704
  var text = textarea.value.trim();
@@ -651,7 +716,7 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
651
716
  });
652
717
 
653
718
  var inputRow = document.createElement("div");
654
- inputRow.style.cssText = "display:flex;gap:8px;padding:12px;align-items:flex-end;border-top:1px solid #e5e7eb;";
719
+ inputRow.style.cssText = "display:flex;gap:8px;padding:12px;align-items:flex-end;border-top:1px solid var(--theme-foreground-faint);";
655
720
  inputRow.append(textarea, sendBtn);
656
721
 
657
722
  var container = document.createElement("div");
@@ -661,15 +726,47 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
661
726
 
662
727
  function updateMessages() {
663
728
  messagesDiv.innerHTML = "";
664
- for (var i = 0; i < cc_messages.value.length; i++) {
665
- 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
+
666
763
  var bubble = document.createElement("div");
667
764
  bubble.className = "cc-msg cc-msg-" + msg.role;
668
765
  var isUser = msg.role === "user";
669
766
  bubble.style.cssText = "max-width:80%;padding:10px 14px;border-radius:12px;font-size:14px;line-height:1.5;" +
670
767
  (isUser
671
- ? "align-self:flex-end;background:#2563eb;color:white;border-bottom-right-radius:4px;"
672
- : "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;");
673
770
 
674
771
  if (isUser) {
675
772
  bubble.textContent = msg.content;
@@ -681,6 +778,7 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
681
778
  } catch(e) { bubble.textContent = msg.content; }
682
779
  }
683
780
  messagesDiv.appendChild(bubble);
781
+ i++;
684
782
  }
685
783
  messagesDiv.scrollTop = messagesDiv.scrollHeight;
686
784
  }
@@ -690,13 +788,16 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
690
788
  return container;
691
789
  }
692
790
 
791
+ var isConnected = (cc_status.value === "connected");
792
+
693
793
  var wrapper = document.createElement("div");
694
794
  wrapper.className = "cc-chat";
695
- wrapper.style.cssText = "border:1px solid #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;" : "");
696
797
 
697
798
  var statusBar = document.createElement("div");
698
799
  statusBar.className = "cc-status-bar";
699
- statusBar.style.cssText = "display:flex;align-items:center;gap:6px;padding:8px 12px;border-bottom:1px solid #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);";
700
801
 
701
802
  var statusDot = document.createElement("span");
702
803
  statusDot.style.cssText = "width:8px;height:8px;border-radius:50%;display:inline-block;";
@@ -716,18 +817,21 @@ const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Input
716
817
 
717
818
  function render() {
718
819
  var status = cc_status.value || "disconnected";
820
+ var connected = (status === "connected");
719
821
  statusDot.style.background = statusColors[status] || "#ef4444";
720
- statusText.textContent = status === "connected" ? "Connected to Claude Code"
822
+ statusText.textContent = connected ? "Connected to Claude Code"
721
823
  : (status === "connecting" || status === "pairing") ? "Connecting..."
722
824
  : "Not connected";
723
825
 
826
+ wrapper.style.height = connected ? "400px" : "";
827
+
724
828
  body.innerHTML = "";
725
- if (status === "connected") {
829
+ if (connected) {
726
830
  chatView = renderChat();
727
831
  body.appendChild(chatView);
728
832
  } else {
729
833
  chatView = null;
730
- body.appendChild(renderSetup());
834
+ body.appendChild(renderConnect());
731
835
  }
732
836
  }
733
837
 
@@ -769,7 +873,9 @@ export default function define(runtime, observer) {
769
873
  $def("_cc_messages", "cc_messages", ["Inputs"], _cc_messages);
770
874
  $def("_cc_watches", "viewof cc_watches", ["Inputs"], _cc_watches);
771
875
  main.variable().define("cc_watches", ["Generators", "viewof cc_watches"], (G, v) => G.input(v));
772
- $def("_cc_ws", "cc_ws", ["cc_config","cc_notebook_id","cc_status","cc_messages","viewof cc_watches","summarizeJS","observe","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);
773
879
  $def("_cc_change_forwarder", "cc_change_forwarder", ["cc_ws","invalidation"], _cc_change_forwarder);
774
880
 
775
881
  // Imports
@@ -780,6 +886,12 @@ export default function define(runtime, observer) {
780
886
  main.define("viewof runtime_variables", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("viewof runtime_variables", _));
781
887
  main.define("runtime_variables", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("runtime_variables", _));
782
888
  main.define("observe", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("observe", _));
889
+ main.define("realize", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("realize", _));
890
+ main.define("createModule", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("createModule", _));
891
+ main.define("deleteModule", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("deleteModule", _));
892
+ main.define("lookupVariable", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("lookupVariable", _));
893
+ main.define("thisModule", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("thisModule", _));
894
+ main.define("runtime", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("runtime", _));
783
895
  main.define("module d/57d79353bac56631@44", async () => runtime.module((await import("/d/57d79353bac56631@44.js?v=4")).default));
784
896
  main.define("hash", ["module d/57d79353bac56631@44", "@variable"], (_, v) => v.import("hash", _));
785
897
  main.define("module @tomlarkworthy/summarizejs", async () => runtime.module((await import("/@tomlarkworthy/summarizejs.js?v=4")).default));
@@ -90,11 +90,14 @@ Variable updates (when watching):
90
90
 
91
91
  - reply: Send markdown to notebook chat
92
92
  - get_variable / define_variable / delete_variable / list_variables: Interact with runtime
93
+ - create_module / delete_module: Create or remove modules (registered in runtime.mains, visible in moduleMap)
93
94
  - watch_variable / unwatch_variable: Subscribe to reactive variable updates
94
95
  - run_tests: Run test_* variables
95
96
  - eval_code: Evaluate JS in browser context
96
97
  - fork_notebook: Create a copy as sibling HTML file
97
98
 
99
+ IMPORTANT: Always specify the module parameter when calling define_variable, get_variable, etc.
100
+ Use the currentModules watch to identify the user's content module (not lopepage, module-selection, or claude-code-pairing).
98
101
  When multiple notebooks are connected, specify notebook_id (the URL). When only one is connected, it's used automatically.`,
99
102
  }
100
103
  );
@@ -248,6 +251,30 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
248
251
  },
249
252
  },
250
253
  },
254
+ {
255
+ name: "create_module",
256
+ description: "Create a new empty module in the runtime. The module is registered in runtime.mains so it appears in moduleMap/currentModules.",
257
+ inputSchema: {
258
+ type: "object",
259
+ properties: {
260
+ notebook_id: { type: "string" },
261
+ name: { type: "string", description: "Module name, e.g. '@tomlarkworthy/my-module'" },
262
+ },
263
+ required: ["name"],
264
+ },
265
+ },
266
+ {
267
+ name: "delete_module",
268
+ description: "Delete a module and all its variables from the runtime.",
269
+ inputSchema: {
270
+ type: "object",
271
+ properties: {
272
+ notebook_id: { type: "string" },
273
+ name: { type: "string", description: "Module name to delete" },
274
+ },
275
+ required: ["name"],
276
+ },
277
+ },
251
278
  {
252
279
  name: "watch_variable",
253
280
  description: "Subscribe to reactive updates for a variable. Changes are pushed as notifications.",
@@ -308,15 +335,19 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
308
335
  action = "get-variable";
309
336
  params = { name: args.name, module: args.module || null };
310
337
  break;
311
- case "define_variable":
338
+ case "define_variable": {
312
339
  action = "define-variable";
340
+ let inputs = args.inputs;
341
+ if (typeof inputs === "string") inputs = JSON.parse(inputs);
342
+ if (!Array.isArray(inputs)) inputs = [];
313
343
  params = {
314
344
  name: args.name,
315
345
  definition: args.definition,
316
- inputs: (args.inputs as string[]) || [],
346
+ inputs,
317
347
  module: args.module || null,
318
348
  };
319
349
  break;
350
+ }
320
351
  case "delete_variable":
321
352
  action = "delete-variable";
322
353
  params = { name: args.name, module: args.module || null };
@@ -339,6 +370,14 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
339
370
  timeout = 120000;
340
371
  params = { suffix: args.suffix || null };
341
372
  break;
373
+ case "create_module":
374
+ action = "create-module";
375
+ params = { name: args.name };
376
+ break;
377
+ case "delete_module":
378
+ action = "delete-module";
379
+ params = { name: args.name };
380
+ break;
342
381
  case "watch_variable":
343
382
  action = "watch";
344
383
  params = { name: args.name, module: args.module || null };
@@ -538,7 +577,7 @@ await mcp.connect(new StdioServerTransport());
538
577
  const server = Bun.serve({
539
578
  port: REQUESTED_PORT,
540
579
  hostname: "127.0.0.1",
541
- fetch(req, server) {
580
+ async fetch(req, server) {
542
581
  const url = new URL(req.url);
543
582
  if (url.pathname === "/ws") {
544
583
  if (server.upgrade(req)) return;
@@ -550,6 +589,51 @@ const server = Bun.serve({
550
589
  pending: pendingConnections.size,
551
590
  }), { headers: { "content-type": "application/json" } });
552
591
  }
592
+
593
+ // Tool activity endpoint — receives PostToolUse hook data and broadcasts to notebooks
594
+ if (url.pathname === "/tool-activity" && req.method === "POST") {
595
+ try {
596
+ const body = await req.json();
597
+ const toolName = body.tool_name || "unknown";
598
+ const toolInput = body.tool_input || {};
599
+ const toolResponse = body.tool_response;
600
+
601
+ // Build a compact summary for the chat widget
602
+ let summary = toolName;
603
+ if (toolName === "Read" && toolInput.file_path) {
604
+ summary = `Read ${toolInput.file_path}`;
605
+ } else if (toolName === "Edit" && toolInput.file_path) {
606
+ summary = `Edit ${toolInput.file_path}`;
607
+ } else if (toolName === "Write" && toolInput.file_path) {
608
+ summary = `Write ${toolInput.file_path}`;
609
+ } else if (toolName === "Bash" && toolInput.command) {
610
+ summary = `$ ${toolInput.command.slice(0, 120)}`;
611
+ } else if (toolName === "Grep" && toolInput.pattern) {
612
+ summary = `Grep "${toolInput.pattern}"`;
613
+ } else if (toolName === "Glob" && toolInput.pattern) {
614
+ summary = `Glob "${toolInput.pattern}"`;
615
+ } else if (toolName === "Agent" && toolInput.description) {
616
+ summary = `Agent: ${toolInput.description}`;
617
+ } else if (toolName.startsWith("mcp__lopecode__")) {
618
+ // Our own MCP tools — skip broadcasting to avoid echo
619
+ return new Response("ok", { status: 200 });
620
+ }
621
+
622
+ // Broadcast to all paired notebooks
623
+ const msg = JSON.stringify({
624
+ type: "tool-activity",
625
+ tool_name: toolName,
626
+ summary,
627
+ timestamp: Date.now(),
628
+ });
629
+ for (const ws of pairedConnections.values()) {
630
+ ws.send(msg);
631
+ }
632
+ return new Response("ok", { status: 200 });
633
+ } catch (e) {
634
+ return new Response("bad request", { status: 400 });
635
+ }
636
+ }
553
637
  if (url.pathname === "/") {
554
638
  const notebookUrl = `https://tomlarkworthy.github.io/lopecode/notebooks/@tomlarkworthy_blank-notebook.html#view=R100(S50(@tomlarkworthy/blank-notebook),S25(@tomlarkworthy/module-selection),S25(@tomlarkworthy/claude-code-pairing))&cc=${PAIRING_TOKEN}`;
555
639
  return new Response(null, {
@@ -570,5 +654,11 @@ const server = Bun.serve({
570
654
 
571
655
  PORT = server.port; // read actual port (important when REQUESTED_PORT is 0)
572
656
  PAIRING_TOKEN = generateToken();
657
+
658
+ // Write port file so hooks can find us
659
+ const portFilePath = join(import.meta.dir, ".lopecode-port");
660
+ await Bun.write(portFilePath, String(PORT));
661
+ process.on("exit", () => { try { require("fs").unlinkSync(portFilePath); } catch {} });
662
+
573
663
  process.stderr.write(`lopecode-channel: pairing token: ${PAIRING_TOKEN}\n`);
574
664
  process.stderr.write(`lopecode-channel: WebSocket server on ws://127.0.0.1:${PORT}/ws\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lopecode/channel",
3
- "version": "0.1.2",
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`);