@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 +142 -0
- package/package.json +1 -1
- package/src/hosted-mcp/.env.example +3 -0
- package/src/hosted-mcp/demo/footer.js +70 -12
- package/src/hosted-mcp/demo/index.html +21 -9
- package/src/hosted-mcp/demo/login.html +548 -0
- package/src/hosted-mcp/demo/privacy.html +3 -10
- package/src/hosted-mcp/demo/tos.html +3 -10
- package/src/hosted-mcp/ecosystem.config.cjs +1 -0
- package/src/hosted-mcp/legal/internet-services/terms/site.html +205 -0
- package/src/hosted-mcp/legal/privacy/en-ww/index.html +230 -0
- package/src/hosted-mcp/nginx/mcp-oauth.conf +98 -0
- package/src/hosted-mcp/nginx/mcp-server.conf +17 -0
- package/src/hosted-mcp/nginx/wip.computer.conf +45 -0
- package/src/hosted-mcp/package-lock.json +2092 -0
- package/src/hosted-mcp/package.json +2 -0
- package/src/hosted-mcp/prisma/migrations/20260406233014_init/migration.sql +68 -0
- package/src/hosted-mcp/prisma/migrations/migration_lock.toml +3 -0
- package/src/hosted-mcp/prisma/schema.prisma +57 -0
- package/src/hosted-mcp/prisma.config.ts +14 -0
- package/src/hosted-mcp/server.mjs +487 -33
- package/templates/hooks/pre-commit +5 -0
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,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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 © 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> | '
|
|
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 © 2026 WIP Computer, Inc. All rights reserved. '
|
|
34
|
+
+ '<a href="/legal/privacy/en-ww/" style="color:#a8a4a0;text-decoration:none;">Privacy Policy</a> | '
|
|
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> | '
|
|
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
|
-
|
|
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()">
|
|
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
|
|
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
|
-
<
|
|
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');
|