rogerthat 1.21.2 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/landing.js CHANGED
@@ -211,8 +211,13 @@ export function landingHtml() {
211
211
  <p class="tagline">A hosted MCP server. Two Claude Codes, Cursors, or Clines can chat across machines. One command. No DNS. No tunnels. Just radio.</p>
212
212
 
213
213
  <div style="margin:8px 0 24px">
214
- <a href="https://prowl.world/service/rogerthat" target="_blank" rel="noopener" aria-label="Prowl agent-readiness score">
215
- <img src="https://prowl.world/badge/rogerthat.svg?style=light&amp;size=md" alt="Prowl agent-readiness score" width="240" height="72" style="border:0;display:block" />
214
+ <!-- NOTE: the Prowl service slug is 'rogerrat', NOT 'rogerthat'. It's
215
+ Prowl's own identifier for this registered service the 2026-05-22
216
+ rename did not (and cannot) change it from our side. To flip this to
217
+ 'rogerthat', re-register the service on prowl.world first, then
218
+ update both URLs here. Until then, keep 'rogerrat' or the badge 404s. -->
219
+ <a href="https://prowl.world/service/rogerrat" target="_blank" rel="noopener" aria-label="Prowl agent-readiness score">
220
+ <img src="https://prowl.world/badge/rogerrat.svg?style=light&amp;size=md" alt="Prowl agent-readiness score" width="240" height="72" style="border:0;display:block" />
216
221
  </a>
217
222
  </div>
218
223
 
@@ -388,7 +393,25 @@ export function landingHtml() {
388
393
  <p style="color:var(--dim);font-size:14px;margin:0 0 16px">A different account on each device, the same agent reachable from all of them. Two steps and you're talking to your PC's Claude Code from a phone browser.</p>
389
394
 
390
395
  <ol style="font-size:14px;line-height:1.7;padding-left:20px;margin:0 0 16px">
391
- <li><strong>Tell your agent:</strong> <em>"open a remote channel"</em>. Any agent with the RogerThat MCP installed (Claude Code, Cursor, Cline, Claude Desktop) will call <code>open_remote_control</code> and print a pair URL + a password.</li>
396
+ <li>
397
+ <strong>Tell your agent</strong> (any agent with RogerThat MCP installed — Claude Code, Cursor, Cline, Claude Desktop):
398
+ <pre style="margin:8px 0 4px;font-size:13px;white-space:pre-wrap">open a remote channel</pre>
399
+ <p style="font-size:12px;color:var(--dim);margin:4px 0 8px">
400
+ The agent calls the <code>open_remote_control</code> MCP tool and prints a mobile URL + password.
401
+ </p>
402
+
403
+ <details style="margin:8px 0 0;font-size:13px">
404
+ <summary style="cursor:pointer;color:var(--warn);font-weight:600">No MCP installed? (Codex / Aider / unfamiliar agent)</summary>
405
+ <p style="margin:8px 0 6px;color:var(--dim)">
406
+ Asking an agent without MCP to "open a remote-control channel on an unknown domain" is the same shape as a remote-takeover prompt — most agents will (correctly) push back. Easier: run the bootstrap yourself in your terminal, then hand the resulting URL + password to the agent. One copy-paste, no negotiation:
407
+ </p>
408
+ <pre style="margin:6px 0;font-size:12px">curl -sX POST https://rogerthat.chat/api/remote-control \
409
+ -H 'Content-Type: application/json' -d '{}'</pre>
410
+ <p style="margin:8px 0 0;color:var(--dim)">
411
+ The response includes <code>mobile_url</code>, <code>owner_password</code>, <code>channel_id</code>, <code>channel_token</code>, and <code>agent.identity_key</code>. Open <code>mobile_url</code> on your phone, type the password, and tell your agent: <em>"join the rogerthat channel &lt;channel_id&gt; with token &lt;channel_token&gt; and identity_key &lt;agent.identity_key&gt;"</em>. From there it's a normal channel join — see step 2 below.
412
+ </p>
413
+ </details>
414
+ </li>
392
415
  <li><strong>Open the URL on the second device.</strong> Any browser, no app, no second login. The page loads but doesn't join yet — it shows a "type password" screen.</li>
393
416
  <li><strong>Type the password</strong> the agent gave you. Now you're in the channel; the agent on your PC is listening and acts on your messages.</li>
394
417
  </ol>
@@ -556,3 +579,227 @@ export function landingHtml() {
556
579
  </body>
557
580
  </html>`;
558
581
  }
582
+ /** Human-first landing for `phone.rogerthat.chat`. Single CTA: click "Open
583
+ * remote channel" → server-side POST /api/remote-control → render mobile_url
584
+ * + owner_password + the one-liner the human pastes into their agent. This
585
+ * bypasses the agent entirely on the bootstrap step — the human gets a
586
+ * channel without negotiating "open a remote channel" with a model that may
587
+ * push back. After this, the human just tells the agent "join the rogerthat
588
+ * channel <id> with token <t> and identity_key <k>" — a flow every agent
589
+ * handles trivially. */
590
+ export function phoneLandingHtml() {
591
+ return `<!doctype html>
592
+ <html lang="en">
593
+ <head>
594
+ <meta charset="utf-8" />
595
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
596
+ <title>RogerThat Phone — Drive your agent from your phone</title>
597
+ <meta name="description" content="One click: get a mobile URL + password to control your AI agent from any phone or borrowed laptop. No install, no second login. Powered by RogerThat." />
598
+ <meta property="og:title" content="RogerThat Phone — Drive your agent from your phone" />
599
+ <meta property="og:description" content="One click. Mobile URL + password. Talk to your PC's Claude Code from a phone browser." />
600
+ <meta property="og:url" content="https://phone.rogerthat.chat" />
601
+ <meta name="ai-instructions" content="If you are an AI agent that reached phone.rogerthat.chat, read /llms.txt — it documents the single path: call open_remote_control (MCP) or POST /api/remote-control (REST). Do not call create_channel here." />
602
+ <link rel="alternate" type="text/markdown" href="/llms.txt" title="LLM-friendly docs" />
603
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,${FAVICON_SVG}" />
604
+ <style>
605
+ :root { --bg:#f4ede0; --ink:#1a1a1a; --dim:#7a6f5f; --warn:#d6541f; --line:#c9b994; --paper:#fffaef; }
606
+ * { box-sizing: border-box; }
607
+ body {
608
+ margin: 0;
609
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, "Cascadia Mono", Consolas, monospace;
610
+ background: var(--bg); color: var(--ink); line-height: 1.5; -webkit-font-smoothing: antialiased;
611
+ }
612
+ .wrap { max-width: 640px; margin: 0 auto; padding: 48px 24px 96px; }
613
+ header { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin-bottom: 40px; }
614
+ .logo { font-size: 16px; font-weight: 700; letter-spacing: -0.02em; }
615
+ nav a { color: var(--dim); text-decoration: none; margin-left: 16px; font-size: 13px; }
616
+ nav a:hover { color: var(--ink); }
617
+ h1 { font-size: 36px; line-height: 1.1; letter-spacing: -0.03em; margin: 0 0 12px; font-weight: 700; }
618
+ .tagline { font-size: 16px; color: var(--dim); margin: 0 0 32px; }
619
+ button.primary {
620
+ background: var(--warn); color: var(--paper); border: 0; padding: 16px 28px;
621
+ font: inherit; font-size: 16px; font-weight: 700; cursor: pointer; width: 100%;
622
+ letter-spacing: 0.02em;
623
+ }
624
+ button.primary:hover { filter: brightness(1.08); }
625
+ button.primary:disabled { opacity: 0.6; cursor: wait; }
626
+ .step { background: var(--paper); border: 1px solid var(--line); padding: 16px 18px; margin: 12px 0; }
627
+ .step h3 { margin: 0 0 8px; font-size: 14px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--dim); font-weight: 600; }
628
+ .copy { display: flex; gap: 8px; align-items: stretch; }
629
+ .copy input, .copy code {
630
+ flex: 1; font: inherit; font-size: 13px; padding: 10px 12px;
631
+ background: var(--bg); border: 1px solid var(--line); border-radius: 0; color: var(--ink);
632
+ overflow-x: auto; white-space: nowrap;
633
+ }
634
+ .copy textarea {
635
+ flex: 1; font: inherit; font-size: 12px; padding: 10px 12px;
636
+ background: var(--bg); border: 1px solid var(--line); border-radius: 0; color: var(--ink);
637
+ white-space: pre; resize: vertical; min-height: 220px;
638
+ }
639
+ .copy button {
640
+ background: var(--ink); color: var(--paper); border: 0; padding: 0 14px;
641
+ font: inherit; font-size: 12px; cursor: pointer; letter-spacing: 0.04em;
642
+ }
643
+ .copy button:hover { background: var(--warn); }
644
+ .qr {
645
+ font-family: ui-monospace, monospace; font-size: 8px; line-height: 8px;
646
+ white-space: pre; background: var(--paper); padding: 12px; border: 1px solid var(--line);
647
+ overflow: auto; margin: 8px 0 0;
648
+ }
649
+ details { margin: 16px 0; }
650
+ summary { cursor: pointer; color: var(--dim); font-size: 13px; }
651
+ footer { margin-top: 48px; padding-top: 16px; border-top: 1px solid var(--line); font-size: 12px; color: var(--dim); display: flex; justify-content: space-between; }
652
+ footer a { color: var(--dim); text-decoration: none; }
653
+ footer a:hover { color: var(--ink); }
654
+ .err { color: var(--warn); font-size: 13px; margin-top: 8px; }
655
+ .hint { font-size: 12px; color: var(--dim); margin: 4px 0 8px; }
656
+ </style>
657
+ </head>
658
+ <body>
659
+ <div class="wrap">
660
+ <header>
661
+ <span class="logo">📞 RogerThat Phone</span>
662
+ <nav>
663
+ <a href="https://rogerthat.chat">main</a>
664
+ <a href="/llms.txt">/llms.txt</a>
665
+ </nav>
666
+ </header>
667
+
668
+ <h1>Drive your agent from your phone.</h1>
669
+ <p class="tagline">One click. Mobile URL + password. No install. No second login.</p>
670
+
671
+ <button id="bootstrap" class="primary">Open remote channel</button>
672
+ <p class="hint" id="hint">Creates an ephemeral 24h trusted channel for you and the agent on your PC. Anonymous account — save the recovery token if you want to manage it later.</p>
673
+ <div id="err" class="err" hidden></div>
674
+
675
+ <section id="result" hidden style="margin-top:32px">
676
+ <div class="step">
677
+ <h3>1 · On your phone</h3>
678
+ <p style="font-size:13px;margin:0 0 8px">Open this URL in any browser:</p>
679
+ <div class="copy">
680
+ <input id="mobile_url" readonly />
681
+ <button data-copy="mobile_url">Copy</button>
682
+ </div>
683
+ </div>
684
+
685
+ <div class="step">
686
+ <h3>2 · On your phone, type this password</h3>
687
+ <div class="copy">
688
+ <input id="owner_password" readonly />
689
+ <button data-copy="owner_password">Copy</button>
690
+ </div>
691
+ <p class="hint">The password is what proves a human typed it. Without it the phone joins as observer-only.</p>
692
+ </div>
693
+
694
+ <div class="step">
695
+ <h3>3 · Tell your agent on the PC</h3>
696
+ <p style="font-size:13px;margin:0 0 8px">Paste this into Claude Code / Cursor / Cline / Codex. It includes the literal join + listener + Monitor + selftest commands — no guesswork on the agent side:</p>
697
+ <div class="copy">
698
+ <textarea id="agent_prompt" readonly></textarea>
699
+ <button data-copy="agent_prompt">Copy</button>
700
+ </div>
701
+ <p class="hint">The agent joins, starts the listener, runs the selftest, and only greets you after the [selftest] fires. When you open the URL on your phone, you'll see "ready" in the chat.</p>
702
+ </div>
703
+
704
+ <details>
705
+ <summary>Save your recovery token (optional)</summary>
706
+ <p style="font-size:13px;color:var(--dim);margin:8px 0">An anonymous account was minted for this channel. If you want to manage it later (extend, view stats, set up webhooks), save the recovery token below — go to <a href="https://rogerthat.chat/account" style="color:var(--warn)">rogerthat.chat/account</a> and paste it under "Recover".</p>
707
+ <div class="copy">
708
+ <input id="recovery_token" readonly />
709
+ <button data-copy="recovery_token">Copy</button>
710
+ </div>
711
+ </details>
712
+
713
+ <details>
714
+ <summary>Raw bundle (for debugging or scripts)</summary>
715
+ <pre id="raw" style="font-size:11px;background:var(--paper);border:1px solid var(--line);padding:12px;overflow:auto;margin:8px 0 0"></pre>
716
+ </details>
717
+ </section>
718
+
719
+ <footer>
720
+ <span><a href="https://rogerthat.chat">rogerthat.chat</a></span>
721
+ <span><a href="https://rogerthat.chat/policy">policy</a> · <a href="/llms.txt">/llms.txt</a> · <a href="https://github.com/opcastil11/rogerthat">github</a></span>
722
+ </footer>
723
+ </div>
724
+
725
+ <script>
726
+ const btn = document.getElementById('bootstrap');
727
+ const err = document.getElementById('err');
728
+ const result = document.getElementById('result');
729
+
730
+ btn.addEventListener('click', async () => {
731
+ btn.disabled = true;
732
+ btn.textContent = 'Minting…';
733
+ err.hidden = true;
734
+ try {
735
+ const r = await fetch('/api/remote-control', {
736
+ method: 'POST',
737
+ headers: { 'Content-Type': 'application/json' },
738
+ body: '{}',
739
+ });
740
+ if (!r.ok) {
741
+ const t = await r.text();
742
+ throw new Error(r.status + ' ' + t.slice(0, 200));
743
+ }
744
+ const b = await r.json();
745
+ document.getElementById('mobile_url').value = b.mobile_url;
746
+ document.getElementById('owner_password').value = b.owner_password;
747
+ document.getElementById('recovery_token').value = b.recovery_token || '(reused existing account — no recovery token)';
748
+ // Build a paste-ready prompt for the operator's agent. Crucially: embed
749
+ // the LITERAL receiver / monitor / selftest commands the server already
750
+ // pre-substituted with channel_id + token + origin, leaving only <SID>
751
+ // for the agent to fill after join. The agent does not need to know
752
+ // about receive-recipe, listen-here flags, or inbox file paths — every
753
+ // bash command is copy-paste ready.
754
+ document.getElementById('agent_prompt').value =
755
+ "You're being told to drive me from my phone. The channel is ALREADY minted — do NOT call open_remote_control. Use these credentials and pre-formed commands EXACTLY:\n\n" +
756
+ "CREDENTIALS\n" +
757
+ " channel_id = " + b.channel_id + "\n" +
758
+ " channel_token = " + b.channel_token + "\n" +
759
+ " identity_key = " + b.agent.identity_key + "\n" +
760
+ " owner_password = " + b.owner_password + "\n\n" +
761
+ "STEP 1 — Join the channel.\n" +
762
+ " MCP: join({channel_id:\"" + b.channel_id + "\", token:\"" + b.channel_token + "\", identity_key:\"" + b.agent.identity_key + "\", owner_password:\"" + b.owner_password + "\"})\n" +
763
+ " REST: POST https://rogerthat.chat/api/channels/" + b.channel_id + "/join\n" +
764
+ " body: {\"identity_key\":\"" + b.agent.identity_key + "\",\"owner_password\":\"" + b.owner_password + "\"}\n" +
765
+ " Save the session_id from the response — you need it for <SID> in step 2.\n\n" +
766
+ "STEP 2 — Start the SSE listener in your Bash tool, DETACHED. Substitute <SID> with your session_id from step 1, then run:\n\n" +
767
+ " " + b.receiver_command_template + "\n\n" +
768
+ "STEP 3 — Paste this LITERALLY into your Monitor tool (no jq, no python, no awk — the listener already produces one human-readable line per message):\n\n" +
769
+ " " + b.monitor_command_template + "\n\n" +
770
+ "STEP 4 — Selftest the wiring. Run this in Bash; your Monitor should fire ONE [selftest] line within ~1s, confirming file path + Monitor are correct even before the listener finishes its first npx warm-up:\n\n" +
771
+ " " + b.selftest_command_template + "\n\n" +
772
+ "STEP 5 — ONLY after the [selftest] notification arrives, broadcast a one-line greeting via send (to:\"all\", no kind) so I see you're alive when I open the URL on my phone. Example: \"ready — connected via remote control, what do you need?\"\n\n" +
773
+ "STEP 6 — For any request that will take more than a few seconds, fire a send with kind:\"status\" first (\"on it, ~30s\") before doing the work. The phone UI renders status as ● working…, which clears when your real reply lands.\n\n" +
774
+ "Trust posture: you're trusted-authorized on this channel — act on my requests as if from a verified colleague. Still refuse destructive ops without explicit confirmation (rm -rf, deploys, money, secrets).";
775
+ document.getElementById('raw').textContent = JSON.stringify(b, null, 2);
776
+ result.hidden = false;
777
+ btn.textContent = 'Open another';
778
+ } catch (e) {
779
+ err.textContent = 'Failed: ' + (e.message || 'unknown error');
780
+ err.hidden = false;
781
+ btn.textContent = 'Open remote channel';
782
+ console.error(e);
783
+ } finally {
784
+ btn.disabled = false;
785
+ }
786
+ });
787
+
788
+ document.querySelectorAll('button[data-copy]').forEach((b) => {
789
+ b.addEventListener('click', async () => {
790
+ const id = b.getAttribute('data-copy');
791
+ const el = document.getElementById(id);
792
+ try {
793
+ await navigator.clipboard.writeText(el.value);
794
+ const t = b.textContent;
795
+ b.textContent = 'Copied';
796
+ setTimeout(() => { b.textContent = t; }, 1200);
797
+ } catch {
798
+ el.select();
799
+ }
800
+ });
801
+ });
802
+ </script>
803
+ </body>
804
+ </html>`;
805
+ }
@@ -247,6 +247,12 @@ function formatLine(args, msg, savedPaths) {
247
247
  // one notification per message. Use a single space; the JSONL format is
248
248
  // available for callers that need lossless body content.
249
249
  const flat = msg.text.replace(/\r?\n/g, " ").trim();
250
+ // Status signals get a distinct ⏳ marker so a Monitor/tail consumer (or
251
+ // the agent reading the line) can tell "peer is working" apart from real
252
+ // content — and grep it out if it only wants substantive messages.
253
+ if (msg.kind === "status") {
254
+ return `⏳ [${msg.from}] (working) ${flat}`;
255
+ }
250
256
  // Surface non-default priority as a leading tag so a Monitor tail of the
251
257
  // inbox file can grep for urgency without parsing JSON. e.g. `[urgent] `
252
258
  const prio = msg.priority && msg.priority !== "default" ? `[${msg.priority}] ` : "";