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

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
@@ -2380,6 +2380,16 @@ async function cmdInstallCatalog() {
2380
2380
  ok('Boot hook updated (sessions, messages, updates now active)');
2381
2381
  }
2382
2382
 
2383
+ // Deploy git pre-commit hook on every install (not just init)
2384
+ const hooksDir = join(LDM_ROOT, 'hooks');
2385
+ const preCommitDest = join(hooksDir, 'pre-commit');
2386
+ const preCommitSrc = join(__dirname, '..', 'templates', 'hooks', 'pre-commit');
2387
+ if (existsSync(preCommitSrc)) {
2388
+ if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });
2389
+ cpSync(preCommitSrc, preCommitDest);
2390
+ chmodSync(preCommitDest, 0o755);
2391
+ }
2392
+
2383
2393
  console.log('');
2384
2394
  console.log(` Updated ${updated}/${totalUpdates} extension(s).`);
2385
2395
  installLog(`ldm install complete: ${updated}/${totalUpdates} updated, ${healthFixes} health fix(es)`);
@@ -3865,6 +3875,145 @@ async function main() {
3865
3875
  process.exit(1);
3866
3876
  }
3867
3877
 
3878
+ // ── ldm pair ────────────────────────────────────────────────────────
3879
+ // Device pairing for Bridge Phase A.
3880
+ // Links this machine to the user's Kaleidoscope account via passkey.
3881
+ //
3882
+ // Flow:
3883
+ // 1. Generate a human-readable code (BLUE-FISH-4729)
3884
+ // 2. POST the code to wip.computer/api/pair/request
3885
+ // 3. User goes to wip.computer/pair on their phone, signs in with passkey, enters code
3886
+ // 4. Poll GET /api/pair/status?code=X until approved or expired
3887
+ // 5. Store the device token at ~/.ldm/auth/kaleidoscope.json
3888
+ //
3889
+ // The code is shown in the terminal. The user navigates to the pairing
3890
+ // page themselves (CC does NOT open a URL, to prevent phishing).
3891
+ // The code expires after 120 seconds.
3892
+
3893
+ async function cmdPair() {
3894
+ const PAIR_API = process.env.LDM_PAIR_API || 'https://wip.computer';
3895
+ const AUTH_DIR = join(LDM_ROOT, 'auth');
3896
+ const TOKEN_PATH = join(AUTH_DIR, 'kaleidoscope.json');
3897
+
3898
+ // Check if already paired
3899
+ if (existsSync(TOKEN_PATH)) {
3900
+ try {
3901
+ const existing = JSON.parse(readFileSync(TOKEN_PATH, 'utf8'));
3902
+ if (existing.token) {
3903
+ console.log('');
3904
+ console.log(` Already paired as ${existing.userName || 'unknown'}`);
3905
+ console.log(` Paired: ${existing.pairedAt || 'unknown'}`);
3906
+ console.log(` Token: ${existing.token.slice(0, 8)}...`);
3907
+ console.log('');
3908
+ console.log(' To re-pair, delete ~/.ldm/auth/kaleidoscope.json and run ldm pair again.');
3909
+ console.log('');
3910
+ return;
3911
+ }
3912
+ } catch {}
3913
+ }
3914
+
3915
+ // Generate code
3916
+ const words = [
3917
+ 'BLUE', 'RED', 'GREEN', 'GOLD', 'GRAY', 'PINK', 'DARK', 'WARM', 'COLD', 'WILD',
3918
+ 'FISH', 'BIRD', 'WOLF', 'BEAR', 'DEER', 'HAWK', 'FROG', 'LYNX', 'DOVE', 'CROW',
3919
+ ];
3920
+ const w1 = words[Math.floor(Math.random() * 10)];
3921
+ const w2 = words[10 + Math.floor(Math.random() * 10)];
3922
+ const num = String(Math.floor(1000 + Math.random() * 9000));
3923
+ const code = `${w1}-${w2}-${num}`;
3924
+
3925
+ // Detect device name
3926
+ const { hostname } = await import('node:os');
3927
+ const deviceName = hostname() || 'unknown';
3928
+
3929
+ // Read agent ID from config
3930
+ let agentId = 'cc-mini';
3931
+ try {
3932
+ const config = JSON.parse(readFileSync(join(LDM_ROOT, 'config.json'), 'utf8'));
3933
+ const agents = config.agents || {};
3934
+ for (const [id, agent] of Object.entries(agents)) {
3935
+ if (agent.harness === 'claude-code') { agentId = id; break; }
3936
+ }
3937
+ } catch {}
3938
+
3939
+ console.log('');
3940
+ console.log(' Pairing code:');
3941
+ console.log('');
3942
+ console.log(` ${code}`);
3943
+ console.log('');
3944
+ console.log(' Go to wip.computer/pair on your phone.');
3945
+ console.log(' Sign in with your passkey. Enter the code.');
3946
+ console.log('');
3947
+ console.log(' Waiting for approval...');
3948
+
3949
+ // Register the code with the server
3950
+ try {
3951
+ const registerRes = await fetch(`${PAIR_API}/api/pair/request`, {
3952
+ method: 'POST',
3953
+ headers: { 'Content-Type': 'application/json' },
3954
+ body: JSON.stringify({ code, deviceName, agentId }),
3955
+ });
3956
+ if (!registerRes.ok) {
3957
+ const err = await registerRes.text();
3958
+ console.error(` x Failed to register pairing code: ${err}`);
3959
+ process.exit(1);
3960
+ }
3961
+ } catch (err) {
3962
+ console.error(` x Cannot reach ${PAIR_API}: ${err.message}`);
3963
+ console.error(' Make sure the server is running.');
3964
+ process.exit(1);
3965
+ }
3966
+
3967
+ // Poll for approval (every 2 seconds, up to 120 seconds)
3968
+ const maxAttempts = 60;
3969
+ for (let i = 0; i < maxAttempts; i++) {
3970
+ await new Promise(r => setTimeout(r, 2000));
3971
+
3972
+ try {
3973
+ const statusRes = await fetch(`${PAIR_API}/api/pair/status?code=${encodeURIComponent(code)}`);
3974
+ const data = await statusRes.json();
3975
+
3976
+ if (data.status === 'approved' && data.token) {
3977
+ // Store token
3978
+ mkdirSync(AUTH_DIR, { recursive: true });
3979
+ writeFileSync(TOKEN_PATH, JSON.stringify({
3980
+ token: data.token,
3981
+ userId: data.userId,
3982
+ userName: data.userName,
3983
+ deviceName,
3984
+ agentId,
3985
+ pairedAt: new Date().toISOString(),
3986
+ server: PAIR_API,
3987
+ }, null, 2) + '\n');
3988
+
3989
+ console.log('');
3990
+ console.log(` ✓ Paired as ${data.userName || 'User'} (${deviceName} / ${agentId})`);
3991
+ console.log(` Token stored at ~/.ldm/auth/kaleidoscope.json`);
3992
+ console.log('');
3993
+ return;
3994
+ }
3995
+
3996
+ if (statusRes.status === 404 || statusRes.status === 410) {
3997
+ console.error('');
3998
+ console.error(' x Code expired. Run ldm pair again.');
3999
+ console.error('');
4000
+ process.exit(1);
4001
+ }
4002
+
4003
+ // Still pending. Keep polling.
4004
+ process.stdout.write('.');
4005
+ } catch {
4006
+ // Network error. Keep trying.
4007
+ process.stdout.write('x');
4008
+ }
4009
+ }
4010
+
4011
+ console.error('');
4012
+ console.error(' x Timed out waiting for approval. Run ldm pair again.');
4013
+ console.error('');
4014
+ process.exit(1);
4015
+ }
4016
+
3868
4017
  if (command === '--version' || command === '-v') {
3869
4018
  console.log(PKG_VERSION);
3870
4019
  process.exit(0);
@@ -3917,6 +4066,9 @@ async function main() {
3917
4066
  case 'backup':
3918
4067
  await cmdBackup();
3919
4068
  break;
4069
+ case 'pair':
4070
+ await cmdPair();
4071
+ break;
3920
4072
  default:
3921
4073
  console.error(` Unknown command: ${command}`);
3922
4074
  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.26",
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');