sema-cli 0.1.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.
@@ -0,0 +1,304 @@
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" />
6
+ <title>Sema — Pair</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: dark;
10
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
11
+ background: #101417;
12
+ color: #f4f1e8;
13
+ }
14
+
15
+ * { box-sizing: border-box; margin: 0; padding: 0; }
16
+
17
+ body {
18
+ min-height: 100vh;
19
+ min-height: 100dvh;
20
+ background: #101417;
21
+ display: flex;
22
+ flex-direction: column;
23
+ align-items: center;
24
+ justify-content: center;
25
+ padding: 24px;
26
+ }
27
+
28
+ .card {
29
+ background: #1a1f24;
30
+ border: 1px solid #2b3337;
31
+ border-radius: 12px;
32
+ padding: 32px 24px;
33
+ width: 100%;
34
+ max-width: 380px;
35
+ text-align: center;
36
+ }
37
+
38
+ h1 {
39
+ font-size: 20px;
40
+ font-weight: 700;
41
+ margin-bottom: 8px;
42
+ }
43
+
44
+ .subtitle {
45
+ color: #8a9099;
46
+ font-size: 14px;
47
+ margin-bottom: 24px;
48
+ }
49
+
50
+ .code-display {
51
+ font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, monospace;
52
+ font-size: 32px;
53
+ font-weight: 700;
54
+ letter-spacing: 4px;
55
+ color: #4ec9b0;
56
+ margin: 16px 0;
57
+ }
58
+
59
+ input[type="text"] {
60
+ width: 100%;
61
+ background: #0d1117;
62
+ border: 1px solid #2b3337;
63
+ border-radius: 8px;
64
+ padding: 14px 16px;
65
+ color: #f4f1e8;
66
+ font-family: "SF Mono", SFMono-Regular, ui-monospace, Menlo, monospace;
67
+ font-size: 20px;
68
+ text-align: center;
69
+ letter-spacing: 3px;
70
+ text-transform: uppercase;
71
+ outline: none;
72
+ transition: border-color 0.2s;
73
+ }
74
+
75
+ input[type="text"]:focus {
76
+ border-color: #4ec9b0;
77
+ }
78
+
79
+ input[type="text"]::placeholder {
80
+ color: #3a444a;
81
+ letter-spacing: 1px;
82
+ font-size: 14px;
83
+ text-transform: none;
84
+ }
85
+
86
+ button {
87
+ width: 100%;
88
+ margin-top: 16px;
89
+ background: #4ec9b0;
90
+ color: #101417;
91
+ border: none;
92
+ border-radius: 8px;
93
+ padding: 14px;
94
+ font-size: 16px;
95
+ font-weight: 600;
96
+ cursor: pointer;
97
+ transition: opacity 0.2s;
98
+ }
99
+
100
+ button:hover { opacity: 0.9; }
101
+ button:active { opacity: 0.8; }
102
+
103
+ button:disabled {
104
+ opacity: 0.5;
105
+ cursor: not-allowed;
106
+ }
107
+
108
+ .status {
109
+ margin-top: 20px;
110
+ padding: 12px;
111
+ border-radius: 8px;
112
+ font-size: 14px;
113
+ display: none;
114
+ }
115
+
116
+ .status.visible { display: block; }
117
+
118
+ .status.info {
119
+ background: #1a2332;
120
+ border: 1px solid #1e3a5f;
121
+ color: #79b8ff;
122
+ }
123
+
124
+ .status.error {
125
+ background: #2a1515;
126
+ border: 1px solid #5f1e1e;
127
+ color: #f97583;
128
+ }
129
+
130
+ .status.success {
131
+ background: #152a1a;
132
+ border: 1px solid #1e5f2e;
133
+ color: #4ec9b0;
134
+ }
135
+
136
+ .spinner {
137
+ display: inline-block;
138
+ width: 16px;
139
+ height: 16px;
140
+ border: 2px solid #79b8ff;
141
+ border-top-color: transparent;
142
+ border-radius: 50%;
143
+ animation: spin 0.8s linear infinite;
144
+ vertical-align: middle;
145
+ margin-right: 8px;
146
+ }
147
+
148
+ @keyframes spin {
149
+ to { transform: rotate(360deg); }
150
+ }
151
+
152
+ .logo {
153
+ font-size: 24px;
154
+ font-weight: 700;
155
+ margin-bottom: 24px;
156
+ color: #4ec9b0;
157
+ }
158
+
159
+ .help {
160
+ margin-top: 24px;
161
+ color: #5a6068;
162
+ font-size: 12px;
163
+ line-height: 1.5;
164
+ }
165
+
166
+ .back-link {
167
+ display: inline-block;
168
+ margin-top: 16px;
169
+ color: #4ec9b0;
170
+ text-decoration: none;
171
+ font-size: 14px;
172
+ }
173
+ </style>
174
+ </head>
175
+ <body>
176
+ <div class="card">
177
+ <div class="logo">Sema</div>
178
+ <h1>Pair Device</h1>
179
+ <p class="subtitle">Enter the code shown on your Mac</p>
180
+
181
+ <input
182
+ type="text"
183
+ id="codeInput"
184
+ placeholder="ABC-123"
185
+ maxlength="7"
186
+ autocomplete="off"
187
+ autocapitalize="characters"
188
+ spellcheck="false"
189
+ />
190
+
191
+ <button id="pairBtn" onclick="handlePair()">Connect</button>
192
+
193
+ <div id="status" class="status"></div>
194
+
195
+ <p class="help">
196
+ Scan the QR code on your Mac's terminal<br />
197
+ or enter the 6-character code manually.
198
+ </p>
199
+
200
+ <a href="/inbox.html" class="back-link">← Back to Sessions</a>
201
+ </div>
202
+
203
+ <script>
204
+ const codeInput = document.getElementById("codeInput");
205
+ const pairBtn = document.getElementById("pairBtn");
206
+ const statusEl = document.getElementById("status");
207
+
208
+ // Auto-format: insert dash after 3rd character
209
+ codeInput.addEventListener("input", () => {
210
+ let val = codeInput.value.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
211
+ if (val.length > 3) {
212
+ val = val.slice(0, 3) + "-" + val.slice(3, 6);
213
+ }
214
+ codeInput.value = val;
215
+ });
216
+
217
+ // Submit on Enter
218
+ codeInput.addEventListener("keydown", (e) => {
219
+ if (e.key === "Enter") handlePair();
220
+ });
221
+
222
+ function showStatus(message, type) {
223
+ statusEl.textContent = message;
224
+ statusEl.className = `status visible ${type}`;
225
+ }
226
+
227
+ function clearStatus() {
228
+ statusEl.className = "status";
229
+ }
230
+
231
+ async function handlePair() {
232
+ const code = codeInput.value.trim().toUpperCase();
233
+
234
+ if (!code || code.length < 6) {
235
+ showStatus("Please enter a valid code.", "error");
236
+ codeInput.focus();
237
+ return;
238
+ }
239
+
240
+ pairBtn.disabled = true;
241
+ showStatus("Connecting...", "info");
242
+
243
+ try {
244
+ const response = await fetch("/api/pair", {
245
+ method: "POST",
246
+ headers: { "content-type": "application/json" },
247
+ body: JSON.stringify({ code }),
248
+ });
249
+
250
+ const data = await response.json();
251
+
252
+ if (!response.ok) {
253
+ if (response.status === 404) {
254
+ showStatus("Invalid code. Please check and try again.", "error");
255
+ } else if (response.status === 410) {
256
+ showStatus(data.error || "Code expired. Please generate a new code on your Mac.", "error");
257
+ } else if (response.status === 429) {
258
+ const retry = data.retryAfter || 60;
259
+ showStatus(`Too many attempts. Please wait ${retry}s.`, "error");
260
+ } else {
261
+ showStatus(data.error || "Pairing failed. Please try again.", "error");
262
+ }
263
+ pairBtn.disabled = false;
264
+ return;
265
+ }
266
+
267
+ // Success — store token in localStorage (multi-session) + sessionStorage, redirect
268
+ showStatus("Connected! Redirecting...", "success");
269
+
270
+ // Multi-session storage
271
+ var sessions = {};
272
+ try { sessions = JSON.parse(localStorage.getItem("vc_sessions") || "{}"); } catch(e) {}
273
+ sessions[data.sessionId] = {
274
+ token: data.mobileToken,
275
+ macPublicKey: data.macPublicKey || null,
276
+ command: data.command || null,
277
+ pairedAt: Date.now(),
278
+ };
279
+ localStorage.setItem("vc_sessions", JSON.stringify(sessions));
280
+
281
+ // Backward compatibility
282
+ sessionStorage.setItem("sessionToken", data.mobileToken);
283
+ if (data.macPublicKey) {
284
+ sessionStorage.setItem("macPublicKey", data.macPublicKey);
285
+ }
286
+ window.location.href = `/?sessionId=${encodeURIComponent(data.sessionId)}`;
287
+ } catch (err) {
288
+ showStatus("Network error. Is the relay running?", "error");
289
+ pairBtn.disabled = false;
290
+ }
291
+ }
292
+
293
+ // Auto-pair if code is in URL (from QR scan)
294
+ const params = new URLSearchParams(location.search);
295
+ const urlCode = params.get("code");
296
+ if (urlCode) {
297
+ codeInput.value = urlCode.toUpperCase();
298
+ handlePair();
299
+ } else {
300
+ codeInput.focus();
301
+ }
302
+ </script>
303
+ </body>
304
+ </html>