@wipcomputer/wip-ldm-os 0.4.73-alpha.24 → 0.4.73-alpha.25

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/bin/ldm.js CHANGED
@@ -3865,6 +3865,145 @@ async function main() {
3865
3865
  process.exit(1);
3866
3866
  }
3867
3867
 
3868
+ // ── ldm pair ────────────────────────────────────────────────────────
3869
+ // Device pairing for Bridge Phase A.
3870
+ // Links this machine to the user's Kaleidoscope account via passkey.
3871
+ //
3872
+ // Flow:
3873
+ // 1. Generate a human-readable code (BLUE-FISH-4729)
3874
+ // 2. POST the code to wip.computer/api/pair/request
3875
+ // 3. User goes to wip.computer/pair on their phone, signs in with passkey, enters code
3876
+ // 4. Poll GET /api/pair/status?code=X until approved or expired
3877
+ // 5. Store the device token at ~/.ldm/auth/kaleidoscope.json
3878
+ //
3879
+ // The code is shown in the terminal. The user navigates to the pairing
3880
+ // page themselves (CC does NOT open a URL, to prevent phishing).
3881
+ // The code expires after 120 seconds.
3882
+
3883
+ async function cmdPair() {
3884
+ const PAIR_API = process.env.LDM_PAIR_API || 'https://wip.computer';
3885
+ const AUTH_DIR = join(LDM_ROOT, 'auth');
3886
+ const TOKEN_PATH = join(AUTH_DIR, 'kaleidoscope.json');
3887
+
3888
+ // Check if already paired
3889
+ if (existsSync(TOKEN_PATH)) {
3890
+ try {
3891
+ const existing = JSON.parse(readFileSync(TOKEN_PATH, 'utf8'));
3892
+ if (existing.token) {
3893
+ console.log('');
3894
+ console.log(` Already paired as ${existing.userName || 'unknown'}`);
3895
+ console.log(` Paired: ${existing.pairedAt || 'unknown'}`);
3896
+ console.log(` Token: ${existing.token.slice(0, 8)}...`);
3897
+ console.log('');
3898
+ console.log(' To re-pair, delete ~/.ldm/auth/kaleidoscope.json and run ldm pair again.');
3899
+ console.log('');
3900
+ return;
3901
+ }
3902
+ } catch {}
3903
+ }
3904
+
3905
+ // Generate code
3906
+ const words = [
3907
+ 'BLUE', 'RED', 'GREEN', 'GOLD', 'GRAY', 'PINK', 'DARK', 'WARM', 'COLD', 'WILD',
3908
+ 'FISH', 'BIRD', 'WOLF', 'BEAR', 'DEER', 'HAWK', 'FROG', 'LYNX', 'DOVE', 'CROW',
3909
+ ];
3910
+ const w1 = words[Math.floor(Math.random() * 10)];
3911
+ const w2 = words[10 + Math.floor(Math.random() * 10)];
3912
+ const num = String(Math.floor(1000 + Math.random() * 9000));
3913
+ const code = `${w1}-${w2}-${num}`;
3914
+
3915
+ // Detect device name
3916
+ const { hostname } = await import('node:os');
3917
+ const deviceName = hostname() || 'unknown';
3918
+
3919
+ // Read agent ID from config
3920
+ let agentId = 'cc-mini';
3921
+ try {
3922
+ const config = JSON.parse(readFileSync(join(LDM_ROOT, 'config.json'), 'utf8'));
3923
+ const agents = config.agents || {};
3924
+ for (const [id, agent] of Object.entries(agents)) {
3925
+ if (agent.harness === 'claude-code') { agentId = id; break; }
3926
+ }
3927
+ } catch {}
3928
+
3929
+ console.log('');
3930
+ console.log(' Pairing code:');
3931
+ console.log('');
3932
+ console.log(` ${code}`);
3933
+ console.log('');
3934
+ console.log(' Go to wip.computer/pair on your phone.');
3935
+ console.log(' Sign in with your passkey. Enter the code.');
3936
+ console.log('');
3937
+ console.log(' Waiting for approval...');
3938
+
3939
+ // Register the code with the server
3940
+ try {
3941
+ const registerRes = await fetch(`${PAIR_API}/api/pair/request`, {
3942
+ method: 'POST',
3943
+ headers: { 'Content-Type': 'application/json' },
3944
+ body: JSON.stringify({ code, deviceName, agentId }),
3945
+ });
3946
+ if (!registerRes.ok) {
3947
+ const err = await registerRes.text();
3948
+ console.error(` x Failed to register pairing code: ${err}`);
3949
+ process.exit(1);
3950
+ }
3951
+ } catch (err) {
3952
+ console.error(` x Cannot reach ${PAIR_API}: ${err.message}`);
3953
+ console.error(' Make sure the server is running.');
3954
+ process.exit(1);
3955
+ }
3956
+
3957
+ // Poll for approval (every 2 seconds, up to 120 seconds)
3958
+ const maxAttempts = 60;
3959
+ for (let i = 0; i < maxAttempts; i++) {
3960
+ await new Promise(r => setTimeout(r, 2000));
3961
+
3962
+ try {
3963
+ const statusRes = await fetch(`${PAIR_API}/api/pair/status?code=${encodeURIComponent(code)}`);
3964
+ const data = await statusRes.json();
3965
+
3966
+ if (data.status === 'approved' && data.token) {
3967
+ // Store token
3968
+ mkdirSync(AUTH_DIR, { recursive: true });
3969
+ writeFileSync(TOKEN_PATH, JSON.stringify({
3970
+ token: data.token,
3971
+ userId: data.userId,
3972
+ userName: data.userName,
3973
+ deviceName,
3974
+ agentId,
3975
+ pairedAt: new Date().toISOString(),
3976
+ server: PAIR_API,
3977
+ }, null, 2) + '\n');
3978
+
3979
+ console.log('');
3980
+ console.log(` ✓ Paired as ${data.userName || 'User'} (${deviceName} / ${agentId})`);
3981
+ console.log(` Token stored at ~/.ldm/auth/kaleidoscope.json`);
3982
+ console.log('');
3983
+ return;
3984
+ }
3985
+
3986
+ if (statusRes.status === 404 || statusRes.status === 410) {
3987
+ console.error('');
3988
+ console.error(' x Code expired. Run ldm pair again.');
3989
+ console.error('');
3990
+ process.exit(1);
3991
+ }
3992
+
3993
+ // Still pending. Keep polling.
3994
+ process.stdout.write('.');
3995
+ } catch {
3996
+ // Network error. Keep trying.
3997
+ process.stdout.write('x');
3998
+ }
3999
+ }
4000
+
4001
+ console.error('');
4002
+ console.error(' x Timed out waiting for approval. Run ldm pair again.');
4003
+ console.error('');
4004
+ process.exit(1);
4005
+ }
4006
+
3868
4007
  if (command === '--version' || command === '-v') {
3869
4008
  console.log(PKG_VERSION);
3870
4009
  process.exit(0);
@@ -3917,6 +4056,9 @@ async function main() {
3917
4056
  case 'backup':
3918
4057
  await cmdBackup();
3919
4058
  break;
4059
+ case 'pair':
4060
+ await cmdPair();
4061
+ break;
3920
4062
  default:
3921
4063
  console.error(` Unknown command: ${command}`);
3922
4064
  console.error(` Run: ldm --help`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ldm-os",
3
- "version": "0.4.73-alpha.24",
3
+ "version": "0.4.73-alpha.25",
4
4
  "type": "module",
5
5
  "description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
6
6
  "engines": {
@@ -0,0 +1,3 @@
1
+ # Kaleidoscope server environment
2
+ # Copy to .env and fill in values. Never commit .env itself.
3
+ DATABASE_URL=postgresql://kaleidoscope:YOUR_PASSWORD@localhost:5432/kaleidoscope
@@ -1,16 +1,74 @@
1
1
  // Shared footer for all Kaleidoscope pages
2
- // Include with: <script src="/demo/footer.js"></script>
2
+ // Include with: <div id="kscope-footer"></div><script src="/demo/footer.js"></script>
3
3
  (function() {
4
- var footer = document.createElement('div');
5
- footer.innerHTML = '<p style="color:#8a8580;font-size:15px;margin:0;">WIP Computer, Inc.</p>'
6
- + '<p style="color:#a8a4a0;font-size:14px;margin:4px 0 0;">Learning Dreaming Machines</p>'
7
- + '<p style="color:#a8a4a0;font-size:13px;margin:6px 0 0;">'
8
- + '<a href="/demo/agent.txt" style="color:#a8a4a0;text-decoration:none;">Are you an AI Agent?</a> | '
9
- + '<a href="/demo/privacy.html" style="color:#a8a4a0;text-decoration:none;">Privacy Policy</a> | '
10
- + '<a href="/demo/tos.html" style="color:#a8a4a0;text-decoration:none;">Terms of Use</a></p>'
11
- + '<p style="color:#b0aaa4;font-size:12px;margin:4px 0 0;">Made in California.</p>';
12
- var target = document.getElementById('kscope-footer');
13
- if (target) {
14
- target.innerHTML = footer.innerHTML;
4
+ var container = document.getElementById('kscope-footer');
5
+ if (!container) return;
6
+
7
+ var mobile = navigator.maxTouchPoints > 0 && window.innerWidth < 768;
8
+
9
+ // Desktop: fixed at bottom. Mobile: in page flow (below fold).
10
+ if (mobile) {
11
+ container.style.cssText = 'background:#FFFDF5;padding:16px 0;';
12
+ } else {
13
+ container.style.cssText = 'position:fixed;bottom:0;left:0;right:0;background:#FFFDF5;padding:16px 0;';
15
14
  }
15
+
16
+ var inner = document.createElement('div');
17
+ inner.style.cssText = 'max-width:980px;margin:0 auto;padding:0 24px;border-top:1px solid rgba(0,0,0,0.06);padding-top:16px;text-align:left;font-size:13px;color:#a8a4a0;line-height:1.6;';
18
+
19
+ // On mobile, copyright and links on separate lines (like Apple)
20
+ if (mobile) {
21
+ inner.innerHTML = '<p style="margin:0;">WIP Computer, Inc.</p>'
22
+ + '<p style="margin:2px 0 0;">Learning Dreaming Machines</p>'
23
+ + '<p style="margin:8px 0 0;">Copyright &copy; 2026 WIP Computer, Inc. All rights reserved.</p>'
24
+ + '<p style="margin:4px 0 0;">'
25
+ + '<a href="/legal/privacy/en-ww/" style="color:#a8a4a0;text-decoration:none;">Privacy Policy</a> &nbsp;|&nbsp; '
26
+ + '<a href="/legal/internet-services/terms/site.html" style="color:#a8a4a0;text-decoration:none;">Terms of Use</a></p>'
27
+ + '<p style="margin:4px 0 0;">'
28
+ + '<a href="/agent.txt" style="color:#a8a4a0;text-decoration:none;">Are you an AI Agent?</a></p>'
29
+ + '<p style="margin:4px 0 0;">Made in California.</p>';
30
+ } else {
31
+ inner.innerHTML = '<p style="margin:0;">WIP Computer, Inc.</p>'
32
+ + '<p style="margin:2px 0 0;">Learning Dreaming Machines</p>'
33
+ + '<p style="margin:8px 0 0;">Copyright &copy; 2026 WIP Computer, Inc. All rights reserved. &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'
34
+ + '<a href="/legal/privacy/en-ww/" style="color:#a8a4a0;text-decoration:none;">Privacy Policy</a> &nbsp;|&nbsp; '
35
+ + '<a href="/legal/internet-services/terms/site.html" style="color:#a8a4a0;text-decoration:none;">Terms of Use</a></p>'
36
+ + '<p style="margin:4px 0 0;">'
37
+ + '<a href="/agent.txt" style="color:#a8a4a0;text-decoration:none;">Are you an AI Agent?</a> &nbsp;|&nbsp; '
38
+ + '<a id="localPasskeysToggle" onclick="toggleLocalPasskeys()" style="color:#a8a4a0;text-decoration:none;cursor:pointer;display:inline-flex;align-items:center;gap:4px;vertical-align:middle;">'
39
+ + '<span id="passkeys-dot" style="display:inline-block;width:8px;height:8px;border-radius:50%;"></span> '
40
+ + '<span id="passkeys-label">Local passkeys off</span></a></p>'
41
+ + '<p style="margin:4px 0 0;">Made in California.</p>';
42
+ }
43
+
44
+ container.appendChild(inner);
45
+
46
+ // Local passkeys toggle
47
+ if (!window.isLocalPasskeysOn) {
48
+ window.isLocalPasskeysOn = function() { return localStorage.getItem('localPasskeys') === 'on'; };
49
+ }
50
+ if (!window.toggleLocalPasskeys) {
51
+ window.toggleLocalPasskeys = function() {
52
+ var on = isLocalPasskeysOn();
53
+ localStorage.setItem('localPasskeys', on ? 'off' : 'on');
54
+ updatePasskeysDot();
55
+ };
56
+ }
57
+ if (!window.updatePasskeysDot) {
58
+ window.updatePasskeysDot = function() {
59
+ var dot = document.getElementById('passkeys-dot');
60
+ var label = document.getElementById('passkeys-label');
61
+ if (!dot) return;
62
+ if (isLocalPasskeysOn()) {
63
+ dot.style.background = '#2E7D32';
64
+ dot.style.opacity = '1';
65
+ if (label) label.textContent = 'Local passkeys on';
66
+ } else {
67
+ dot.style.background = '#D32F2F';
68
+ dot.style.opacity = '0.4';
69
+ if (label) label.textContent = 'Local passkeys off';
70
+ }
71
+ };
72
+ }
73
+ updatePasskeysDot();
16
74
  })();
@@ -68,7 +68,10 @@ html, body {
68
68
  color: var(--text);
69
69
  -webkit-text-size-adjust: 100%;
70
70
  -webkit-font-smoothing: antialiased;
71
- overflow: hidden;
71
+ }
72
+
73
+ @media (min-width: 768px) {
74
+ html, body { overflow: hidden; }
72
75
  }
73
76
 
74
77
  /* ── Login Page ── */
@@ -78,8 +81,8 @@ html, body {
78
81
  flex-direction: column;
79
82
  align-items: center;
80
83
  justify-content: center;
81
- height: 100vh;
82
- height: 100dvh;
84
+ min-height: 100vh;
85
+ min-height: 100dvh;
83
86
  padding: 24px;
84
87
  padding-top: calc(24px + env(safe-area-inset-top, 0px));
85
88
  overflow-y: auto;
@@ -553,19 +556,20 @@ html, body {
553
556
 
554
557
 
555
558
  <div class="login-buttons">
556
- <button class="btn btn-primary" id="createBtn" onclick="doCreateAccount()">Look Inside</button>
559
+ <button class="btn btn-primary" id="createBtn" onclick="doCreateAccount()">Enter the Kaleidoscope</button>
557
560
  </div>
558
- <div style="margin-top:12px;">
559
- <input type="text" id="handleInput" name="kaleidoscope-handle" placeholder="What does Lēsa call you? (optional)" autocapitalize="none" autocorrect="off" autocomplete="off" spellcheck="false" data-1p-ignore="true" data-lpignore="true" onfocus="setTimeout(function(){document.getElementById('handleInput').scrollIntoView({behavior:'smooth',block:'center'})},300)" style="width:100%;padding:16px 18px;border:1px solid #E0DDD6;border-radius:12px;font-size:18px;font-family:var(--font);background:#F5F3ED;color:#1a1a1a;outline:none;text-align:center;" />
561
+ <div id="handleInputWrap" style="margin-top:12px;">
562
+ <input type="text" id="handleInput" name="kaleidoscope-handle" placeholder="What should Lēsa call you? (optional)" autocapitalize="none" autocorrect="off" autocomplete="off" spellcheck="false" data-1p-ignore="true" data-lpignore="true" onfocus="setTimeout(function(){document.getElementById('handleInput').scrollIntoView({behavior:'smooth',block:'center'})},300)" style="width:100%;padding:16px 18px;border:1px solid #E0DDD6;border-radius:12px;font-size:18px;font-family:var(--font);background:#F5F3ED;color:#1a1a1a;outline:none;text-align:center;" />
560
563
  </div>
561
- <div style="margin-top:20px;text-align:center;">
564
+ <p style="color:#b0aaa4;font-size:13px;font-style:italic;margin:16px 0 0;text-align:center;opacity:0.8;">Use your phone to securely create your account</p>
565
+ <div style="margin-top:12px;text-align:center;">
562
566
  <a id="signInBtn" onclick="doSignIn()" style="color:var(--accent);font-size:16px;cursor:pointer;text-decoration:none;">Already have an account? Sign in.</a>
563
567
  </div>
564
568
  <div class="login-status" id="loginStatus" style="position:absolute;left:0;right:0;margin-top:16px;text-align:center;"></div>
565
569
  </div>
566
- <div id="kscope-footer" style="position:fixed;bottom:30px;left:0;right:0;text-align:center;"></div>
567
- <script src="/demo/footer.js"></script>
568
570
  </div>
571
+ <div id="kscope-footer"></div>
572
+ <script src="/demo/footer.js"></script>
569
573
 
570
574
  <!-- ── CHAT PAGE ── -->
571
575
  <div class="chat-page" id="chatPage">
@@ -1229,6 +1233,14 @@ if (sessionStorage.getItem('lesa-token')) {
1229
1233
  showChat();
1230
1234
  }
1231
1235
 
1236
+ // If user has an account (created at /login), show sign-in mode
1237
+ if (localStorage.getItem('kscope-has-account') && !sessionStorage.getItem('lesa-token')) {
1238
+ document.getElementById('createBtn').textContent = 'Enter the Kaleidoscope';
1239
+ document.getElementById('createBtn').onclick = function() { doSignIn(); };
1240
+ document.getElementById('handleInputWrap').style.display = 'none';
1241
+ document.getElementById('signInBtn').parentElement.style.display = 'none';
1242
+ }
1243
+
1232
1244
  // Handle send (not used in demo flow, but wired up)
1233
1245
  function handleSend() {
1234
1246
  var input = document.getElementById('chatInput');