@sksoftofficial/ocduet 0.2.1

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.
@@ -0,0 +1,70 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
6
+ <meta name="theme-color" content="#151716" />
7
+ <title>ocduet — your workspace, anywhere</title>
8
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' rx='12' fill='%23b8e5c7'/%3E%3Cpath d='M10 12h8v16h-8zm12 0h8v16h-8z' fill='%23151716'/%3E%3C/svg%3E" />
9
+ <link rel="stylesheet" href="/app.css" />
10
+ </head>
11
+ <body>
12
+ <svg class="svg-defs" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><defs>
13
+ <symbol id="i-panels" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="3"/><path d="M9 4v16"/></symbol>
14
+ <symbol id="i-chat" viewBox="0 0 24 24"><path d="M21 11.5a8.5 8.5 0 0 1-8.5 8.5H4l-1 1v-9.5a9 9 0 0 1 18 0Z"/><path d="M8 9h8M8 13h5"/></symbol>
15
+ <symbol id="i-arrow" viewBox="0 0 24 24"><path d="M12 19V5m-6 6 6-6 6 6"/></symbol>
16
+ <symbol id="i-terminal" viewBox="0 0 24 24"><path d="m5 7 5 5-5 5m8 0h6"/></symbol>
17
+ <symbol id="i-search" viewBox="0 0 24 24"><circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 4 4"/></symbol>
18
+ <symbol id="i-close" viewBox="0 0 24 24"><path d="m6 6 12 12M6 18 18 6"/></symbol>
19
+ <symbol id="i-shield" viewBox="0 0 24 24"><path d="M12 3l7 3v5c0 5-3.2 8.2-7 10-3.8-1.8-7-5-7-10V6z"/><path d="M12 9v4m0 3h.01"/></symbol>
20
+ <symbol id="i-activity" viewBox="0 0 24 24"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></symbol>
21
+ <symbol id="i-bolt" viewBox="0 0 24 24"><path d="M13 2 3 14h7l-1 8 10-12h-7z"/></symbol>
22
+ <symbol id="i-chevron" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></symbol>
23
+ <symbol id="i-check" viewBox="0 0 24 24"><path d="m5 13 4 4L19 7"/></symbol>
24
+ <symbol id="i-dots" viewBox="0 0 24 24"><circle cx="12" cy="5" r="1.5" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="12" cy="19" r="1.5" fill="currentColor" stroke="none"/></symbol>
25
+ <symbol id="i-trash" viewBox="0 0 24 24"><path d="M4 7h16"/><path d="M10 11v6M14 11v6"/><path d="M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12"/><path d="M9 7V5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/></symbol>
26
+ </defs></svg>
27
+
28
+ <button id="sidebar-backdrop" aria-label="Close sessions" tabindex="-1" hidden></button>
29
+ <aside id="sessions" aria-label="Sessions">
30
+ <div class="brand"><span class="brand-mark" aria-hidden="true"><i></i><i></i></span><span>ocduet<span class="brand-period">.</span></span><span class="brand-tag">COMPANION</span><button id="close-sessions" class="icon-button" aria-label="Close sessions"><svg><use href="#i-close"/></svg></button></div>
31
+ <button id="new-session" class="new-session"><svg><use href="#i-bolt"/></svg>New session</button>
32
+ <div class="section-label"><span>SESSIONS</span><span id="session-count">0</span></div>
33
+ <label class="search"><svg><use href="#i-search"/></svg><input id="session-search" type="search" placeholder="Find a session…" aria-label="Find a session" autocomplete="off"/><kbd>/</kbd></label>
34
+ <nav id="session-list" aria-label="Session list"></nav>
35
+ </aside>
36
+
37
+ <div id="workspace">
38
+ <header id="topbar">
39
+ <button id="sessions-btn" class="icon-button" aria-label="Open sessions" aria-controls="sessions" aria-expanded="false"><svg><use href="#i-panels"/></svg></button>
40
+ <div class="header-icon"><svg><use href="#i-chat"/></svg></div>
41
+ <div id="title"><span id="project-name">Your workspace, anywhere</span><span id="session-subtitle">The mobile companion for opencode</span></div>
42
+ <button id="menu-btn" class="icon-button" aria-label="Session menu" aria-haspopup="menu" aria-expanded="false" disabled><svg><use href="#i-dots"/></svg></button>
43
+ <div id="session-menu" class="menu" role="menu" hidden><button id="menu-usage" role="menuitem"><svg><use href="#i-activity"/></svg>Usage</button><button id="menu-level" role="menuitem"><svg><use href="#i-shield"/></svg>Permission level</button><button id="menu-delete" role="menuitem" class="danger"><svg><use href="#i-trash"/></svg>Delete session</button></div>
44
+ </header>
45
+
46
+ <main id="main">
47
+ <div id="timeline"></div>
48
+ <section id="empty" class="empty">
49
+ <div class="connection-art" aria-hidden="true"><span class="device desktop"><svg><use href="#i-terminal"/></svg></span><span class="connection-line"><i></i><i></i><i></i></span><span class="device phone"><span class="brand-mark"><i></i><i></i></span></span></div>
50
+ <span class="eyebrow">PICK UP WHERE YOU LEFT OFF</span>
51
+ <h1 id="empty-title">Good work travels with you.</h1>
52
+ <p id="empty-description">Your desktop session, right here. Choose a session<br class="desktop-break"/> to keep the conversation going.</p>
53
+ <div id="pairing-steps" class="pairing-steps" hidden><div><span>01</span><p>Open <strong>opencode</strong> on your desktop</p></div><div><span>02</span><p>Open the <strong>Duet</strong> panel and select <strong>connect</strong></p></div><div><span>03</span><p>Scan the QR code with your phone</p></div></div>
54
+ <div class="empty-note"><span class="status-dot"></span>Same session. Same context. In sync.</div>
55
+ </section>
56
+ <div id="activity" role="status" aria-live="polite" hidden><span class="loop-loader" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></span><span id="activity-label">Working</span><span id="activity-time" aria-hidden="true"></span><button id="stop-btn" title="Stop agent" aria-label="Stop agent"><span class="stop-square"></span>Stop</button></div>
57
+ <div id="questions" aria-live="polite"></div>
58
+ <div id="permissions" aria-live="polite"></div>
59
+ </main>
60
+
61
+ <footer id="composer">
62
+ <div class="composer-box"><label class="sr-only" for="input">Message opencode</label><textarea id="input" rows="2" placeholder="Send message..." disabled></textarea><div class="composer-toolbar"><span class="composer-context"><button class="pill" id="cmd-btn" aria-label="Commands" title="Commands" disabled><svg><use href="#i-terminal"/></svg></button><span class="pills"><button class="pill" id="model-pill" aria-label="Switch model"><span id="model-pill-label">Model</span><svg class="pill-arrow"><use href="#i-chevron"/></svg></button><button class="pill" id="variant-pill" aria-label="Switch variant" hidden><span id="variant-pill-label">Variant</span><svg class="pill-arrow"><use href="#i-chevron"/></svg></button><div class="pill context-pill" id="context-pill" hidden><svg class="ring" viewBox="0 0 20 20" aria-hidden="true"><circle class="ring-bg" cx="10" cy="10" r="8"/><circle class="ring-fg" id="context-ring" cx="10" cy="10" r="8"/></svg><span id="context-pill-label">—</span></div></span></span><button id="send-btn" aria-label="Send message" disabled><svg><use href="#i-arrow"/></svg></button></div></div>
63
+ <div class="composer-caption"><span class="sync-status"><span class="status-dot" id="sync-dot"></span><span id="sync-note">Connecting…</span></span><span class="keyboard-hint"><kbd>Enter</kbd> to send · <kbd>Shift Enter</kbd> for a new line</span></div>
64
+ </footer>
65
+ </div>
66
+ <div id="modal-root" hidden></div>
67
+ <div id="toast" role="status" hidden></div>
68
+ <script src="/app.js" defer></script>
69
+ </body>
70
+ </html>
@@ -0,0 +1,131 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
+ <title>ocduet — pair this phone</title>
7
+ <style>
8
+ :root { --bg: #12140f; --panel: #1d221e; --border: #3e4940; --fg: #e8ece5; --muted: #8a948b; --accent: #7eab89; --err: #d98a8a; }
9
+ * { box-sizing: border-box; margin: 0; }
10
+ body { background: var(--bg); color: var(--fg); font: 16px/1.6 Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; min-height: 100dvh; display: flex; align-items: center; justify-content: center; padding: 24px; }
11
+ .card { width: min(420px, 100%); background: var(--panel); border: 1px solid var(--border); border-radius: 16px; padding: 28px 26px; }
12
+ h1 { font-size: 20px; font-weight: 600; margin-bottom: 6px; }
13
+ .sub { color: var(--muted); font-size: 13px; margin-bottom: 22px; }
14
+ .words { display: flex; flex-wrap: wrap; gap: 8px; margin: 14px 0 22px; }
15
+ .word { background: var(--bg); border: 1px solid var(--border); border-radius: 10px; padding: 10px 14px; font: 600 17px/1 ui-monospace, monospace; letter-spacing: .5px; }
16
+ .num { color: var(--muted); font-size: 10px; margin-right: 6px; font-weight: 400; }
17
+ button { width: 100%; background: var(--accent); color: #10130f; border: 0; border-radius: 12px; padding: 13px; font: 600 15px Inter, sans-serif; cursor: pointer; }
18
+ button:disabled { opacity: .5; }
19
+ .ghost { background: transparent; color: var(--muted); border: 1px solid var(--border); margin-top: 10px; font-weight: 400; }
20
+ .err { color: var(--err); font-size: 13px; margin-top: 14px; min-height: 20px; }
21
+ a { color: var(--accent); }
22
+ </style>
23
+ </head>
24
+ <body>
25
+ <div class="card">
26
+ <div id="step-ask">
27
+ <h1>Pair this phone</h1>
28
+ <div class="sub">Run <b>ocduet token</b> on your desktop and compare these words with the ones it shows. If they differ, stop — something between this phone and your machine is impersonating the daemon.</div>
29
+ <div class="words" id="words"></div>
30
+ <button id="pair-btn" disabled>Words match — pair this phone</button>
31
+ <button id="retry-btn" class="ghost" hidden>They don't match — reload</button>
32
+ <div class="err" id="err"></div>
33
+ </div>
34
+ </div>
35
+ <script type="module">
36
+ const $ = (id) => document.getElementById(id);
37
+ const err = (m) => { $("err").textContent = m; $("pair-btn").disabled = !!m; };
38
+
39
+ const b64 = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)));
40
+ const unb64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
41
+ const te = new TextEncoder();
42
+
43
+ function token() {
44
+ const m = location.hash.match(/[#&]t=([^&]+)/);
45
+ if (m) { try { localStorage.setItem("ocduet-token", decodeURIComponent(m[1])); } catch {} }
46
+ return localStorage.getItem("ocduet-token");
47
+ }
48
+
49
+ function idb() {
50
+ return new Promise((resolve, reject) => {
51
+ const r = indexedDB.open("ocduet-keys", 1);
52
+ r.onupgradeneeded = () => r.result.createObjectStore("keys");
53
+ r.onsuccess = () => resolve(r.result);
54
+ r.onerror = () => reject(r.error);
55
+ });
56
+ }
57
+ async function idbSet(key, val) {
58
+ const db = await idb();
59
+ return new Promise((resolve, reject) => {
60
+ const tx = db.transaction("keys", "readwrite");
61
+ tx.objectStore("keys").put(val, key);
62
+ tx.oncomplete = resolve;
63
+ tx.onerror = () => reject(tx.error);
64
+ });
65
+ }
66
+ async function idbGet(key) {
67
+ const db = await idb();
68
+ return new Promise((resolve, reject) => {
69
+ const tx = db.transaction("keys", "readonly");
70
+ const rq = tx.objectStore("keys").get(key);
71
+ rq.onsuccess = () => resolve(rq.result || null);
72
+ rq.onerror = () => reject(rq.error);
73
+ });
74
+ }
75
+
76
+ const pairMsg = (phoneId, phonePubB64, tokenStr) => `ocduet-pair|${phoneId}|${phonePubB64}|${tokenStr}`;
77
+
78
+ async function main() {
79
+ const t = token();
80
+ if (!t) { err("no pairing token — open the URL from `ocduet token` on your desktop"); return; }
81
+ if (await idbGet(location.origin)) {
82
+ // already paired; offer to redo (generates a new phone key)
83
+ const redo = document.createElement("button");
84
+ redo.className = "ghost";
85
+ redo.textContent = "This phone is already paired — re-pair";
86
+ redo.onclick = async () => { await idbSet(location.origin, undefined); location.reload(); };
87
+ $("err").textContent = "";
88
+ redo.disabled = false;
89
+ $("step-ask").appendChild(redo);
90
+ }
91
+ const info = await (await fetch("/pair/info", { cache: "no-store" })).json();
92
+ $("words").innerHTML = "";
93
+ info.words.forEach((w, i) => {
94
+ const el = document.createElement("span");
95
+ el.className = "word";
96
+ el.innerHTML = `<span class="num">${i + 1}</span>${w}`;
97
+ $("words").appendChild(el);
98
+ });
99
+ $("retry-btn").hidden = false;
100
+ $("pair-btn").disabled = false;
101
+
102
+ $("retry-btn").onclick = () => location.reload();
103
+ $("pair-btn").onclick = async () => {
104
+ err("");
105
+ $("pair-btn").disabled = true;
106
+ try {
107
+ if (!crypto?.subtle?.generateKey) throw new Error("this browser lacks WebCrypto");
108
+ const kp = await crypto.subtle.generateKey({ name: "Ed25519" }, false, ["sign", "verify"]);
109
+ const rawPub = new Uint8Array(await crypto.subtle.exportKey("raw", kp.publicKey));
110
+ const phonePubB64 = b64(rawPub);
111
+ const phoneId = crypto.randomUUID().slice(0, 8);
112
+ const sig = await crypto.subtle.sign({ name: "Ed25519" }, kp.privateKey, te.encode(pairMsg(phoneId, phonePubB64, t)));
113
+ const res = await fetch("/pair/register", {
114
+ method: "POST",
115
+ headers: { "content-type": "application/json" },
116
+ body: JSON.stringify({ phoneId, phonePub: phonePubB64, token: t, sig: b64(sig) }),
117
+ });
118
+ const out = await res.json().catch(() => ({}));
119
+ if (!res.ok || !out.ok) throw new Error(out.error || `pairing rejected (${res.status})`);
120
+ await idbSet(location.origin, { origin: location.origin, daemonPub: info.pub, phoneId, priv: kp.privateKey, phonePubB64 });
121
+ location.href = "/";
122
+ } catch (e) {
123
+ err(String(e?.message || e));
124
+ $("pair-btn").disabled = false;
125
+ }
126
+ };
127
+ }
128
+ main().catch((e) => err(String(e?.message || e)));
129
+ </script>
130
+ </body>
131
+ </html>