@wipcomputer/wip-ldm-os 0.4.73-alpha.3 → 0.4.73-alpha.31
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/LICENSE +52 -0
- package/bin/ldm.js +512 -95
- package/dist/bridge/chunk-3RG5ZIWI.js +10 -0
- package/dist/bridge/{chunk-LF7EMFBY.js → chunk-7NH6JBIO.js} +127 -49
- package/dist/bridge/cli.js +2 -1
- package/dist/bridge/core.d.ts +13 -1
- package/dist/bridge/core.js +4 -1
- package/dist/bridge/mcp-server.js +52 -7
- package/dist/bridge/openclaw.d.ts +5 -0
- package/dist/bridge/openclaw.js +11 -0
- package/docs/doc-pipeline/README.md +74 -0
- package/docs/doc-pipeline/TECHNICAL.md +79 -0
- package/lib/deploy.mjs +94 -10
- package/lib/detect.mjs +20 -6
- package/package.json +2 -2
- package/shared/docs/README.md.tmpl +2 -2
- package/shared/docs/how-install-works.md.tmpl +22 -2
- package/shared/docs/how-releases-work.md.tmpl +58 -42
- package/shared/docs/how-worktrees-work.md.tmpl +12 -7
- package/shared/rules/git-conventions.md +3 -3
- package/shared/rules/release-pipeline.md +1 -1
- package/shared/rules/security.md +1 -1
- package/shared/rules/workspace-boundaries.md +1 -1
- package/shared/rules/writing-style.md +1 -1
- package/shared/templates/claude-md-level1.md +7 -3
- package/src/bridge/core.ts +160 -56
- package/src/bridge/mcp-server.ts +93 -8
- package/src/bridge/openclaw.ts +14 -0
- package/src/hooks/inbox-check-hook.mjs +232 -0
- package/src/hooks/inbox-rewake-hook.mjs +388 -0
- package/src/hosted-mcp/.env.example +3 -0
- package/src/hosted-mcp/demo/agent.html +300 -0
- package/src/hosted-mcp/demo/agent.txt +84 -0
- package/src/hosted-mcp/demo/fallback.jpg +0 -0
- package/src/hosted-mcp/demo/footer.js +74 -0
- package/src/hosted-mcp/demo/index.html +1303 -0
- package/src/hosted-mcp/demo/login.html +548 -0
- package/src/hosted-mcp/demo/privacy.html +223 -0
- package/src/hosted-mcp/demo/sprites.jpg +0 -0
- package/src/hosted-mcp/demo/sprites.png +0 -0
- package/src/hosted-mcp/demo/tos.html +198 -0
- package/src/hosted-mcp/deploy.sh +70 -0
- package/src/hosted-mcp/ecosystem.config.cjs +14 -0
- package/src/hosted-mcp/inbox.mjs +64 -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 +23 -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 +2079 -0
- package/src/hosted-mcp/tools.mjs +73 -0
- package/templates/hooks/pre-commit +5 -0
package/bin/ldm.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* ldm --version Show version
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, cpSync, chmodSync, unlinkSync, readlinkSync, renameSync, statSync } from 'node:fs';
|
|
23
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, cpSync, chmodSync, unlinkSync, readlinkSync, renameSync, statSync, lstatSync, symlinkSync } from 'node:fs';
|
|
24
24
|
import { join, basename, resolve, dirname } from 'node:path';
|
|
25
25
|
import { execSync } from 'node:child_process';
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
@@ -350,7 +350,7 @@ function cleanStaleHooks() {
|
|
|
350
350
|
|
|
351
351
|
function syncBootHook() {
|
|
352
352
|
const srcBoot = join(__dirname, '..', 'src', 'boot', 'boot-hook.mjs');
|
|
353
|
-
const destBoot = join(LDM_ROOT, '
|
|
353
|
+
const destBoot = join(LDM_ROOT, 'library', 'boot', 'boot-hook.mjs');
|
|
354
354
|
|
|
355
355
|
if (!existsSync(srcBoot)) return false;
|
|
356
356
|
|
|
@@ -368,6 +368,176 @@ function syncBootHook() {
|
|
|
368
368
|
return false;
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
+
// ── Inbox check hook sync ──
|
|
372
|
+
//
|
|
373
|
+
// Deploys src/hooks/inbox-check-hook.mjs to ~/.ldm/library/hooks/ and
|
|
374
|
+
// wires it into ~/.claude/settings.json as a UserPromptSubmit hook so
|
|
375
|
+
// that pending bridge messages in ~/.ldm/messages/ are surfaced as
|
|
376
|
+
// additionalContext before CC responds to each user prompt.
|
|
377
|
+
//
|
|
378
|
+
// Closes the loop between lesa-bridge fire-and-forget sends and
|
|
379
|
+
// CC-side message delivery. Without this hook, Claude Code only sees
|
|
380
|
+
// bridge messages when it explicitly calls lesa_check_inbox, which
|
|
381
|
+
// requires manual discipline and loses messages in practice.
|
|
382
|
+
//
|
|
383
|
+
// Idempotent: subsequent installs update the file only if its contents
|
|
384
|
+
// changed, and only add the settings.json entry if it isn't already
|
|
385
|
+
// wired to the exact same command path.
|
|
386
|
+
//
|
|
387
|
+
// See:
|
|
388
|
+
// ai/product/plans-prds/bridge/2026-04-06--cc-mini--bridge-master-product-plan.md
|
|
389
|
+
// ai/product/bugs/bridge/2026-04-06--cc-mini--bridge-async-inbox-plan.md
|
|
390
|
+
// ai/product/bugs/bridge/2026-04-10--cc-mini--bridge-reply-addressing-mismatch.md
|
|
391
|
+
function syncInboxCheckHook() {
|
|
392
|
+
const srcHook = join(__dirname, '..', 'src', 'hooks', 'inbox-check-hook.mjs');
|
|
393
|
+
const destHook = join(LDM_ROOT, 'library', 'hooks', 'inbox-check-hook.mjs');
|
|
394
|
+
let changed = false;
|
|
395
|
+
|
|
396
|
+
if (!existsSync(srcHook)) return false;
|
|
397
|
+
|
|
398
|
+
// 1. File deploy: copy src/hooks/inbox-check-hook.mjs to ~/.ldm/library/hooks/
|
|
399
|
+
try {
|
|
400
|
+
const srcContent = readFileSync(srcHook, 'utf8');
|
|
401
|
+
let destContent = '';
|
|
402
|
+
try { destContent = readFileSync(destHook, 'utf8'); } catch {}
|
|
403
|
+
|
|
404
|
+
if (srcContent !== destContent) {
|
|
405
|
+
mkdirSync(dirname(destHook), { recursive: true });
|
|
406
|
+
writeFileSync(destHook, srcContent);
|
|
407
|
+
changed = true;
|
|
408
|
+
}
|
|
409
|
+
} catch {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 2. Settings.json patch: wire the hook into hooks.UserPromptSubmit if absent.
|
|
414
|
+
const settingsPath = join(HOME, '.claude', 'settings.json');
|
|
415
|
+
if (!existsSync(settingsPath)) return changed;
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const raw = readFileSync(settingsPath, 'utf8');
|
|
419
|
+
const settings = JSON.parse(raw);
|
|
420
|
+
if (!settings.hooks) settings.hooks = {};
|
|
421
|
+
if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
|
|
422
|
+
|
|
423
|
+
const hookCommand = `node ${destHook}`;
|
|
424
|
+
const alreadyWired = settings.hooks.UserPromptSubmit.some(group =>
|
|
425
|
+
Array.isArray(group.hooks) &&
|
|
426
|
+
group.hooks.some(h => h.type === 'command' && h.command === hookCommand)
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
if (!alreadyWired) {
|
|
430
|
+
settings.hooks.UserPromptSubmit.push({
|
|
431
|
+
hooks: [{
|
|
432
|
+
type: 'command',
|
|
433
|
+
command: hookCommand,
|
|
434
|
+
timeout: 5,
|
|
435
|
+
}],
|
|
436
|
+
});
|
|
437
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
438
|
+
changed = true;
|
|
439
|
+
}
|
|
440
|
+
} catch {
|
|
441
|
+
// Settings file malformed or unreadable. File deploy still succeeded
|
|
442
|
+
// if changed was true; leave settings.json untouched and let the user
|
|
443
|
+
// see the file deploy message without the wire-up.
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return changed;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ── Inbox rewake hook sync ──
|
|
450
|
+
//
|
|
451
|
+
// Deploys src/hooks/inbox-rewake-hook.mjs to ~/.ldm/library/hooks/ and
|
|
452
|
+
// wires it into ~/.claude/settings.json as a Stop hook with
|
|
453
|
+
// `asyncRewake: true`. This is the autonomous push layer that wakes
|
|
454
|
+
// an idle Claude Code session when a bridge message arrives, without
|
|
455
|
+
// the user having to type anything.
|
|
456
|
+
//
|
|
457
|
+
// Mechanics: the Stop hook fires after every CC turn. The rewake hook
|
|
458
|
+
// acquires a per-session lock file (so concurrent Stop-event spawns do
|
|
459
|
+
// not stack), then holds a long-lived fs.watch on ~/.ldm/messages/.
|
|
460
|
+
// When a matching message file arrives, the hook writes the message to
|
|
461
|
+
// stderr and exits with code 2. The CC harness wraps that stderr into
|
|
462
|
+
// a system-reminder task-notification that wakes the idle model or
|
|
463
|
+
// gets injected mid-query if the model is busy. See Claude Code's
|
|
464
|
+
// `src/utils/hooks.ts` asyncRewake path for the exact mechanism.
|
|
465
|
+
//
|
|
466
|
+
// This closes the layer 1 gap from:
|
|
467
|
+
// ai/product/plans-prds/bridge/2026-04-11--cc-mini--autonomous-push-architecture.md
|
|
468
|
+
//
|
|
469
|
+
// Layers 2-4 (UserPromptSubmit inbox-check hook, SessionStart boot
|
|
470
|
+
// hook, manual lesa_check_inbox) remain as independent fallbacks.
|
|
471
|
+
//
|
|
472
|
+
// Idempotent: subsequent installs update the file only if its contents
|
|
473
|
+
// changed, and only add the settings.json entry if it isn't already
|
|
474
|
+
// wired to the exact same command path.
|
|
475
|
+
function syncInboxRewakeHook() {
|
|
476
|
+
const srcHook = join(__dirname, '..', 'src', 'hooks', 'inbox-rewake-hook.mjs');
|
|
477
|
+
const destHook = join(LDM_ROOT, 'library', 'hooks', 'inbox-rewake-hook.mjs');
|
|
478
|
+
let changed = false;
|
|
479
|
+
|
|
480
|
+
if (!existsSync(srcHook)) return false;
|
|
481
|
+
|
|
482
|
+
// 1. File deploy: copy src/hooks/inbox-rewake-hook.mjs to ~/.ldm/library/hooks/
|
|
483
|
+
try {
|
|
484
|
+
const srcContent = readFileSync(srcHook, 'utf8');
|
|
485
|
+
let destContent = '';
|
|
486
|
+
try { destContent = readFileSync(destHook, 'utf8'); } catch {}
|
|
487
|
+
|
|
488
|
+
if (srcContent !== destContent) {
|
|
489
|
+
mkdirSync(dirname(destHook), { recursive: true });
|
|
490
|
+
writeFileSync(destHook, srcContent);
|
|
491
|
+
changed = true;
|
|
492
|
+
}
|
|
493
|
+
} catch {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// 2. Settings.json patch: wire the hook into hooks.Stop as an
|
|
498
|
+
// asyncRewake background hook if absent.
|
|
499
|
+
const settingsPath = join(HOME, '.claude', 'settings.json');
|
|
500
|
+
if (!existsSync(settingsPath)) return changed;
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
const raw = readFileSync(settingsPath, 'utf8');
|
|
504
|
+
const settings = JSON.parse(raw);
|
|
505
|
+
if (!settings.hooks) settings.hooks = {};
|
|
506
|
+
if (!settings.hooks.Stop) settings.hooks.Stop = [];
|
|
507
|
+
|
|
508
|
+
const hookCommand = `node ${destHook}`;
|
|
509
|
+
const alreadyWired = settings.hooks.Stop.some(group =>
|
|
510
|
+
Array.isArray(group.hooks) &&
|
|
511
|
+
group.hooks.some(h =>
|
|
512
|
+
h.type === 'command' &&
|
|
513
|
+
h.command === hookCommand &&
|
|
514
|
+
h.asyncRewake === true,
|
|
515
|
+
),
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
if (!alreadyWired) {
|
|
519
|
+
settings.hooks.Stop.push({
|
|
520
|
+
hooks: [{
|
|
521
|
+
type: 'command',
|
|
522
|
+
command: hookCommand,
|
|
523
|
+
async: true,
|
|
524
|
+
asyncRewake: true,
|
|
525
|
+
// 6 hours: matches the rewake hook's internal hard timeout.
|
|
526
|
+
// The hook self-terminates well before this on parent death,
|
|
527
|
+
// hard cancel, or match, so this is just a runaway guard.
|
|
528
|
+
timeout: 21600,
|
|
529
|
+
}],
|
|
530
|
+
});
|
|
531
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
532
|
+
changed = true;
|
|
533
|
+
}
|
|
534
|
+
} catch {
|
|
535
|
+
// Settings file malformed or unreadable. Leave it alone.
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return changed;
|
|
539
|
+
}
|
|
540
|
+
|
|
371
541
|
// ── Catalog helpers ──
|
|
372
542
|
|
|
373
543
|
function loadCatalog() {
|
|
@@ -544,20 +714,14 @@ function deployDocs() {
|
|
|
544
714
|
return count;
|
|
545
715
|
}
|
|
546
716
|
|
|
547
|
-
// Deploy to
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
if (docsCount > 0) {
|
|
551
|
-
console.log(` + ${docsCount} personalized doc(s) deployed to ${docsDest.replace(HOME, '~')}/`);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Deploy to library/documentation/ (human-readable library copy)
|
|
717
|
+
// Deploy to library/documentation/ (the canonical doc path since Mar 28 rename).
|
|
718
|
+
// Previously also deployed to settings/docs/ which Parker renamed to library/documentation/.
|
|
719
|
+
// That created a ghost folder on every install. Removed 2026-04-05 per INST-1.
|
|
555
720
|
const libraryDest = join(workspacePath, 'library', 'documentation');
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
721
|
+
mkdirSync(libraryDest, { recursive: true });
|
|
722
|
+
const docsCount = renderTemplates(libraryDest);
|
|
723
|
+
if (docsCount > 0) {
|
|
724
|
+
console.log(` + ${docsCount} personalized doc(s) deployed to ${libraryDest.replace(HOME, '~')}/`);
|
|
561
725
|
}
|
|
562
726
|
|
|
563
727
|
return docsCount;
|
|
@@ -632,17 +796,58 @@ function checkBackupHealth() {
|
|
|
632
796
|
// After `npm install -g`, the updated files live at the npm package location but
|
|
633
797
|
// never get copied to ~/.ldm/extensions/lesa-bridge/dist/. This function fixes that.
|
|
634
798
|
|
|
799
|
+
function deployRules() {
|
|
800
|
+
const rulesSrc = join(__dirname, '..', 'shared', 'rules');
|
|
801
|
+
const rulesDest = join(LDM_ROOT, 'library', 'rules');
|
|
802
|
+
if (!existsSync(rulesSrc)) return;
|
|
803
|
+
mkdirSync(rulesDest, { recursive: true });
|
|
804
|
+
let rulesCount = 0;
|
|
805
|
+
for (const file of readdirSync(rulesSrc)) {
|
|
806
|
+
if (!file.endsWith('.md')) continue;
|
|
807
|
+
cpSync(join(rulesSrc, file), join(rulesDest, file));
|
|
808
|
+
rulesCount++;
|
|
809
|
+
}
|
|
810
|
+
if (rulesCount > 0) {
|
|
811
|
+
console.log(` + ${rulesCount} shared rules deployed to ~/.ldm/library/rules/`);
|
|
812
|
+
// Deploy to Claude Code harness (~/.claude/rules/)
|
|
813
|
+
const claudeRules = join(HOME, '.claude', 'rules');
|
|
814
|
+
if (existsSync(join(HOME, '.claude'))) {
|
|
815
|
+
mkdirSync(claudeRules, { recursive: true });
|
|
816
|
+
for (const file of readdirSync(rulesDest)) {
|
|
817
|
+
if (!file.endsWith('.md')) continue;
|
|
818
|
+
cpSync(join(rulesDest, file), join(claudeRules, file));
|
|
819
|
+
}
|
|
820
|
+
console.log(` + rules deployed to ~/.claude/rules/`);
|
|
821
|
+
}
|
|
822
|
+
// Deploy to OpenClaw harness (~/.openclaw/workspace/DEV-RULES.md)
|
|
823
|
+
const ocWorkspace = join(HOME, '.openclaw', 'workspace');
|
|
824
|
+
if (existsSync(ocWorkspace)) {
|
|
825
|
+
let combined = '# Dev Rules (deployed by ldm install)\n\n';
|
|
826
|
+
combined += '> Do not edit this file. It is regenerated by `ldm install`.\n';
|
|
827
|
+
combined += '> Source: ~/.ldm/library/rules/\n\n';
|
|
828
|
+
for (const file of readdirSync(rulesDest).sort()) {
|
|
829
|
+
if (!file.endsWith('.md')) continue;
|
|
830
|
+
combined += readFileSync(join(rulesDest, file), 'utf8') + '\n\n---\n\n';
|
|
831
|
+
}
|
|
832
|
+
writeFileSync(join(ocWorkspace, 'DEV-RULES.md'), combined);
|
|
833
|
+
console.log(` + rules deployed to ~/.openclaw/workspace/DEV-RULES.md`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
635
838
|
function deployBridge() {
|
|
636
839
|
const ldmBridgeDir = join(LDM_EXTENSIONS, 'lesa-bridge');
|
|
637
840
|
const ocBridgeDir = join(HOME, '.openclaw', 'extensions', 'lesa-bridge');
|
|
638
841
|
|
|
639
842
|
// Deploy targets: LDM path (canonical) and OpenClaw path (where the plugin loads)
|
|
843
|
+
// Create dirs if missing so first-time deploy works (don't skip with filter)
|
|
640
844
|
const targets = [
|
|
641
845
|
{ dir: ldmBridgeDir, label: '~/.ldm/extensions/lesa-bridge/dist/' },
|
|
642
846
|
{ dir: ocBridgeDir, label: '~/.openclaw/extensions/lesa-bridge/dist/' },
|
|
643
|
-
]
|
|
644
|
-
|
|
645
|
-
|
|
847
|
+
];
|
|
848
|
+
for (const t of targets) {
|
|
849
|
+
if (!existsSync(t.dir)) mkdirSync(t.dir, { recursive: true });
|
|
850
|
+
}
|
|
646
851
|
|
|
647
852
|
// Find the npm package bridge files. Try require.resolve first, fall back to known path.
|
|
648
853
|
let bridgeSrc = '';
|
|
@@ -773,10 +978,10 @@ async function cmdInit() {
|
|
|
773
978
|
join(LDM_ROOT, 'state'),
|
|
774
979
|
join(LDM_ROOT, 'sessions'),
|
|
775
980
|
join(LDM_ROOT, 'messages'),
|
|
776
|
-
join(LDM_ROOT, '
|
|
777
|
-
join(LDM_ROOT, '
|
|
778
|
-
join(LDM_ROOT, '
|
|
779
|
-
join(LDM_ROOT, '
|
|
981
|
+
join(LDM_ROOT, 'library', 'boot'),
|
|
982
|
+
join(LDM_ROOT, 'library', 'cron'),
|
|
983
|
+
join(LDM_ROOT, 'library', 'rules'),
|
|
984
|
+
join(LDM_ROOT, 'library', 'prompts'),
|
|
780
985
|
join(LDM_ROOT, 'hooks'),
|
|
781
986
|
];
|
|
782
987
|
|
|
@@ -820,10 +1025,16 @@ async function cmdInit() {
|
|
|
820
1025
|
// Scaffold workspace output dirs if workspace is configured
|
|
821
1026
|
const workspace = config.workspace;
|
|
822
1027
|
if (workspace && existsSync(workspace)) {
|
|
823
|
-
// Per-agent workspace dirs
|
|
824
|
-
|
|
1028
|
+
// Per-agent workspace dirs.
|
|
1029
|
+
// Resolve the team folder name from config.json agents[id].teamFolder
|
|
1030
|
+
// so agents with unicode names or custom folder names don't get ghost
|
|
1031
|
+
// folders created from their agent ID. Falls back to agent ID if no
|
|
1032
|
+
// override is configured. Fixed 2026-04-05 per INST-1: previously
|
|
1033
|
+
// hardcoded a map that only knew three agents and created ghost folders
|
|
1034
|
+
// for any others.
|
|
825
1035
|
for (const agentId of agentList) {
|
|
826
|
-
const
|
|
1036
|
+
const agentObj = typeof agentsObj[agentId] === 'object' ? agentsObj[agentId] : {};
|
|
1037
|
+
const teamName = agentObj.teamFolder || agentObj.name || agentId;
|
|
827
1038
|
for (const sub of ['journals', 'automated/memory/summaries/daily', 'automated/memory/summaries/weekly', 'automated/memory/summaries/monthly', 'automated/memory/summaries/quarterly']) {
|
|
828
1039
|
dirs.push(join(workspace, 'team', teamName, sub));
|
|
829
1040
|
}
|
|
@@ -944,68 +1155,29 @@ async function cmdInit() {
|
|
|
944
1155
|
// Deploy all scripts from scripts/ to ~/.ldm/bin/ (#119)
|
|
945
1156
|
deployScripts();
|
|
946
1157
|
|
|
947
|
-
|
|
948
|
-
const rulesSrc = join(__dirname, '..', 'shared', 'rules');
|
|
949
|
-
const rulesDest = join(LDM_ROOT, 'shared', 'rules');
|
|
950
|
-
if (existsSync(rulesSrc)) {
|
|
951
|
-
mkdirSync(rulesDest, { recursive: true });
|
|
952
|
-
let rulesCount = 0;
|
|
953
|
-
for (const file of readdirSync(rulesSrc)) {
|
|
954
|
-
if (!file.endsWith('.md')) continue;
|
|
955
|
-
cpSync(join(rulesSrc, file), join(rulesDest, file));
|
|
956
|
-
rulesCount++;
|
|
957
|
-
}
|
|
958
|
-
if (rulesCount > 0) {
|
|
959
|
-
console.log(` + ${rulesCount} shared rules deployed to ~/.ldm/shared/rules/`);
|
|
960
|
-
|
|
961
|
-
// Deploy to Claude Code harness (~/.claude/rules/)
|
|
962
|
-
const claudeRules = join(HOME, '.claude', 'rules');
|
|
963
|
-
if (existsSync(join(HOME, '.claude'))) {
|
|
964
|
-
mkdirSync(claudeRules, { recursive: true });
|
|
965
|
-
for (const file of readdirSync(rulesDest)) {
|
|
966
|
-
if (!file.endsWith('.md')) continue;
|
|
967
|
-
cpSync(join(rulesDest, file), join(claudeRules, file));
|
|
968
|
-
}
|
|
969
|
-
console.log(` + rules deployed to ~/.claude/rules/`);
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
// Deploy to OpenClaw harness (~/.openclaw/workspace/DEV-RULES.md)
|
|
973
|
-
const ocWorkspace = join(HOME, '.openclaw', 'workspace');
|
|
974
|
-
if (existsSync(ocWorkspace)) {
|
|
975
|
-
let combined = '# Dev Rules (deployed by ldm install)\n\n';
|
|
976
|
-
combined += '> Do not edit this file. It is regenerated by `ldm install`.\n';
|
|
977
|
-
combined += '> Source: ~/.ldm/shared/rules/\n\n';
|
|
978
|
-
for (const file of readdirSync(rulesDest).sort()) {
|
|
979
|
-
if (!file.endsWith('.md')) continue;
|
|
980
|
-
combined += readFileSync(join(rulesDest, file), 'utf8') + '\n\n---\n\n';
|
|
981
|
-
}
|
|
982
|
-
writeFileSync(join(ocWorkspace, 'DEV-RULES.md'), combined);
|
|
983
|
-
console.log(` + rules deployed to ~/.openclaw/workspace/DEV-RULES.md`);
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
}
|
|
1158
|
+
deployRules();
|
|
987
1159
|
|
|
988
|
-
// Deploy boot-config.json to ~/.ldm/
|
|
1160
|
+
// Deploy boot-config.json to ~/.ldm/library/boot/
|
|
989
1161
|
const bootSrc = join(__dirname, '..', 'shared', 'boot');
|
|
990
|
-
const bootDest = join(LDM_ROOT, '
|
|
1162
|
+
const bootDest = join(LDM_ROOT, 'library', 'boot');
|
|
991
1163
|
if (existsSync(bootSrc)) {
|
|
992
1164
|
mkdirSync(bootDest, { recursive: true });
|
|
993
1165
|
const bootConfig = join(bootSrc, 'boot-config.json');
|
|
994
1166
|
if (existsSync(bootConfig)) {
|
|
995
1167
|
cpSync(bootConfig, join(bootDest, 'boot-config.json'));
|
|
996
|
-
console.log(` + boot-config.json deployed to ~/.ldm/
|
|
1168
|
+
console.log(` + boot-config.json deployed to ~/.ldm/library/boot/`);
|
|
997
1169
|
}
|
|
998
1170
|
}
|
|
999
1171
|
|
|
1000
|
-
//
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1172
|
+
// CLAUDE.md files are NEVER deployed by the installer.
|
|
1173
|
+
// They are git-tracked files in their respective repos:
|
|
1174
|
+
// ~/.claude/CLAUDE.md ... wipcomputer-ldmos-wipcomputerinc-dot-claude-private
|
|
1175
|
+
// ~/wipcomputerinc/CLAUDE.md ... wipcomputerinc repo
|
|
1176
|
+
// ~/.openclaw/CLAUDE.md ... openclaw repo
|
|
1177
|
+
// Changes go through branches and PRs like any other file.
|
|
1178
|
+
// See: 2026-03-27--cc-mini--single-source-of-truth-reversed.md
|
|
1007
1179
|
|
|
1008
|
-
// Deploy shared templates to workspace
|
|
1180
|
+
// Deploy shared templates to workspace library/templates/
|
|
1009
1181
|
const templatesSrc = join(__dirname, '..', 'shared', 'templates');
|
|
1010
1182
|
if (existsSync(templatesSrc)) {
|
|
1011
1183
|
// Read workspace path from ~/.ldm/config.json
|
|
@@ -1015,7 +1187,7 @@ async function cmdInit() {
|
|
|
1015
1187
|
workspacePath = (ldmConfig.workspace || '').replace('~', HOME);
|
|
1016
1188
|
} catch {}
|
|
1017
1189
|
if (workspacePath && existsSync(workspacePath)) {
|
|
1018
|
-
const templatesDest = join(workspacePath, '
|
|
1190
|
+
const templatesDest = join(workspacePath, 'library', 'templates');
|
|
1019
1191
|
mkdirSync(templatesDest, { recursive: true });
|
|
1020
1192
|
let templatesCount = 0;
|
|
1021
1193
|
for (const file of readdirSync(templatesSrc)) {
|
|
@@ -1029,9 +1201,9 @@ async function cmdInit() {
|
|
|
1029
1201
|
}
|
|
1030
1202
|
}
|
|
1031
1203
|
|
|
1032
|
-
// Deploy shared prompts to ~/.ldm/
|
|
1204
|
+
// Deploy shared prompts to ~/.ldm/library/prompts/
|
|
1033
1205
|
const promptsSrc = join(__dirname, '..', 'shared', 'prompts');
|
|
1034
|
-
const promptsDest = join(LDM_ROOT, '
|
|
1206
|
+
const promptsDest = join(LDM_ROOT, 'library', 'prompts');
|
|
1035
1207
|
if (existsSync(promptsSrc)) {
|
|
1036
1208
|
mkdirSync(promptsDest, { recursive: true });
|
|
1037
1209
|
let promptsCount = 0;
|
|
@@ -1041,7 +1213,33 @@ async function cmdInit() {
|
|
|
1041
1213
|
promptsCount++;
|
|
1042
1214
|
}
|
|
1043
1215
|
if (promptsCount > 0) {
|
|
1044
|
-
console.log(` + ${promptsCount} shared prompts deployed to ~/.ldm/
|
|
1216
|
+
console.log(` + ${promptsCount} shared prompts deployed to ~/.ldm/library/prompts/`);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Backward-compat symlink: ~/.ldm/shared -> ~/.ldm/library
|
|
1221
|
+
// Anything still referencing shared/ will follow the symlink
|
|
1222
|
+
{
|
|
1223
|
+
const sharedPath = join(LDM_ROOT, 'shared');
|
|
1224
|
+
const libraryPath = join(LDM_ROOT, 'library');
|
|
1225
|
+
try {
|
|
1226
|
+
const stat = lstatSync(sharedPath);
|
|
1227
|
+
if (stat.isSymbolicLink()) {
|
|
1228
|
+
// Already a symlink, update target if needed
|
|
1229
|
+
const target = readlinkSync(sharedPath);
|
|
1230
|
+
if (target !== libraryPath) {
|
|
1231
|
+
unlinkSync(sharedPath);
|
|
1232
|
+
symlinkSync(libraryPath, sharedPath);
|
|
1233
|
+
}
|
|
1234
|
+
} else if (stat.isDirectory()) {
|
|
1235
|
+
// shared/ is a real directory (pre-rename state). Don't touch it.
|
|
1236
|
+
// The migration will handle this in a dedicated session.
|
|
1237
|
+
}
|
|
1238
|
+
} catch {
|
|
1239
|
+
// shared/ doesn't exist. Create symlink.
|
|
1240
|
+
try {
|
|
1241
|
+
symlinkSync(libraryPath, sharedPath);
|
|
1242
|
+
} catch {}
|
|
1045
1243
|
}
|
|
1046
1244
|
}
|
|
1047
1245
|
|
|
@@ -1321,23 +1519,31 @@ async function cmdInstall() {
|
|
|
1321
1519
|
|
|
1322
1520
|
// Check if target looks like an npm package (starts with @ or is a plain name without /)
|
|
1323
1521
|
if (resolvedTarget.startsWith('@') || (!resolvedTarget.includes('/') && !existsSync(resolve(resolvedTarget)))) {
|
|
1324
|
-
// Try npm
|
|
1522
|
+
// Try npm pack + tar extract to temp dir
|
|
1523
|
+
// npm install --prefix silently fails for scoped packages in temp directories...
|
|
1524
|
+
// it creates the lock file but doesn't extract files. npm pack is reliable.
|
|
1325
1525
|
const npmName = resolvedTarget;
|
|
1326
1526
|
const tempDir = join(LDM_TMP, `npm-${Date.now()}`);
|
|
1327
1527
|
console.log('');
|
|
1328
1528
|
console.log(` Installing ${npmName} from npm...`);
|
|
1329
1529
|
try {
|
|
1330
1530
|
mkdirSync(tempDir, { recursive: true });
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1531
|
+
// Use npm pack + tar instead of npm install --prefix
|
|
1532
|
+
const tarball = execSync(`npm pack ${npmName} --pack-destination "${tempDir}" 2>/dev/null`, {
|
|
1533
|
+
encoding: 'utf8', timeout: 60000, cwd: tempDir,
|
|
1534
|
+
}).trim();
|
|
1535
|
+
const tarPath = join(tempDir, tarball);
|
|
1536
|
+
if (existsSync(tarPath)) {
|
|
1537
|
+
execSync(`tar xzf "${tarPath}" -C "${tempDir}"`, { stdio: 'pipe' });
|
|
1538
|
+
const extracted = join(tempDir, 'package');
|
|
1539
|
+
if (existsSync(extracted)) {
|
|
1540
|
+
console.log(` + Installed from npm`);
|
|
1541
|
+
repoPath = extracted;
|
|
1542
|
+
} else {
|
|
1543
|
+
console.error(` x npm pack succeeded but extraction failed`);
|
|
1544
|
+
}
|
|
1338
1545
|
} else {
|
|
1339
|
-
console.error(` x
|
|
1340
|
-
process.exit(1);
|
|
1546
|
+
console.error(` x npm pack failed: tarball not found`);
|
|
1341
1547
|
}
|
|
1342
1548
|
} catch (e) {
|
|
1343
1549
|
// npm failed, fall through to git clone or path resolution
|
|
@@ -1596,9 +1802,10 @@ async function cmdInstallCatalog() {
|
|
|
1596
1802
|
// in the extension directories. This copies them to both LDM and OpenClaw targets.
|
|
1597
1803
|
deployBridge();
|
|
1598
1804
|
|
|
1599
|
-
// Deploy scripts and
|
|
1805
|
+
// Deploy scripts, docs, and rules on every install so fixes land without re-init
|
|
1600
1806
|
deployScripts();
|
|
1601
1807
|
deployDocs();
|
|
1808
|
+
deployRules();
|
|
1602
1809
|
|
|
1603
1810
|
// Check backup configuration
|
|
1604
1811
|
checkBackupHealth();
|
|
@@ -2206,13 +2413,56 @@ async function cmdInstallCatalog() {
|
|
|
2206
2413
|
continue;
|
|
2207
2414
|
}
|
|
2208
2415
|
|
|
2209
|
-
if (!entry.catalogRepo) {
|
|
2416
|
+
if (!entry.catalogRepo && !entry.catalogNpm) {
|
|
2210
2417
|
console.log(` Skipping ${entry.name}: no catalog repo (install manually with ldm install <org/repo>)`);
|
|
2211
2418
|
continue;
|
|
2212
2419
|
}
|
|
2213
|
-
|
|
2420
|
+
|
|
2421
|
+
// Source resolution chain (#264):
|
|
2422
|
+
// 1. npm (when --alpha/--beta or npm package available) - works online, any machine
|
|
2423
|
+
// 2. Local private repo (offline, developer machine) - works without internet
|
|
2424
|
+
// 3. GitHub clone (fallback) - works online, any machine
|
|
2425
|
+
let installSource = null;
|
|
2426
|
+
const npmTag = ALPHA_FLAG ? 'alpha' : BETA_FLAG ? 'beta' : null;
|
|
2427
|
+
|
|
2428
|
+
// Try npm first when using alpha/beta tracks or when npm is available
|
|
2429
|
+
if (entry.catalogNpm && (npmTag || !entry.catalogRepo)) {
|
|
2430
|
+
const ver = npmTag ? `${entry.catalogNpm}@${npmTag}` : `${entry.catalogNpm}@${entry.latestVersion}`;
|
|
2431
|
+
installSource = ver;
|
|
2432
|
+
console.log(` Updating ${entry.name} v${entry.currentVersion} -> v${entry.latestVersion} (from npm ${npmTag || 'latest'})...`);
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
// Try local private repo (for offline/developer installs)
|
|
2436
|
+
if (!installSource && entry.catalogRepo) {
|
|
2437
|
+
const repoName = basename(entry.catalogRepo);
|
|
2438
|
+
const privateRepoName = repoName + '-private';
|
|
2439
|
+
const WORKSPACE = join(HOME, 'wipcomputerinc');
|
|
2440
|
+
// Search known repo locations
|
|
2441
|
+
const searchDirs = ['repos/ldm-os/devops', 'repos/ldm-os/components', 'repos/ldm-os/utilities', 'repos/ldm-os/apps', 'repos/ldm-os/apis', 'repos/ldm-os/identity'];
|
|
2442
|
+
for (const dir of searchDirs) {
|
|
2443
|
+
const localPrivate = join(WORKSPACE, dir, privateRepoName);
|
|
2444
|
+
const localPublic = join(WORKSPACE, dir, repoName);
|
|
2445
|
+
if (existsSync(localPrivate)) {
|
|
2446
|
+
installSource = localPrivate;
|
|
2447
|
+
console.log(` Updating ${entry.name} v${entry.currentVersion} -> v${entry.latestVersion} (from local ${privateRepoName})...`);
|
|
2448
|
+
break;
|
|
2449
|
+
}
|
|
2450
|
+
if (existsSync(localPublic)) {
|
|
2451
|
+
installSource = localPublic;
|
|
2452
|
+
console.log(` Updating ${entry.name} v${entry.currentVersion} -> v${entry.latestVersion} (from local ${repoName})...`);
|
|
2453
|
+
break;
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
// Fallback: GitHub clone
|
|
2459
|
+
if (!installSource) {
|
|
2460
|
+
installSource = entry.catalogRepo;
|
|
2461
|
+
console.log(` Updating ${entry.name} v${entry.currentVersion} -> v${entry.latestVersion} (from ${entry.catalogRepo})...`);
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2214
2464
|
try {
|
|
2215
|
-
execSync(`ldm install ${
|
|
2465
|
+
execSync(`ldm install ${installSource}`, { stdio: 'inherit' });
|
|
2216
2466
|
updated++;
|
|
2217
2467
|
|
|
2218
2468
|
// For parent packages, update registry version for all sub-tools (#139, #262)
|
|
@@ -2326,7 +2576,32 @@ async function cmdInstallCatalog() {
|
|
|
2326
2576
|
|
|
2327
2577
|
// Sync boot hook from npm package (#49)
|
|
2328
2578
|
if (syncBootHook()) {
|
|
2329
|
-
|
|
2579
|
+
console.log(' + Boot hook updated (sessions, messages, updates now active)');
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
// Sync inbox-check hook: UserPromptSubmit hook that surfaces pending
|
|
2583
|
+
// bridge messages into CC context on every prompt. Closes the gap
|
|
2584
|
+
// between lesa-bridge writes and CC delivery.
|
|
2585
|
+
if (syncInboxCheckHook()) {
|
|
2586
|
+
console.log(' + Inbox-check hook updated (bridge messages surface automatically)');
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
// Sync inbox-rewake hook: Stop hook with asyncRewake that watches
|
|
2590
|
+
// ~/.ldm/messages/ in the background and wakes the model when a new
|
|
2591
|
+
// bridge message arrives, without requiring user interaction. Layer 1
|
|
2592
|
+
// of the April 11 autonomous-push-architecture plan.
|
|
2593
|
+
if (syncInboxRewakeHook()) {
|
|
2594
|
+
console.log(' + Inbox-rewake hook updated (autonomous push: wakes on new bridge message)');
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
// Deploy git pre-commit hook on every install (not just init)
|
|
2598
|
+
const hooksDir = join(LDM_ROOT, 'hooks');
|
|
2599
|
+
const preCommitDest = join(hooksDir, 'pre-commit');
|
|
2600
|
+
const preCommitSrc = join(__dirname, '..', 'templates', 'hooks', 'pre-commit');
|
|
2601
|
+
if (existsSync(preCommitSrc)) {
|
|
2602
|
+
if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });
|
|
2603
|
+
cpSync(preCommitSrc, preCommitDest);
|
|
2604
|
+
chmodSync(preCommitDest, 0o755);
|
|
2330
2605
|
}
|
|
2331
2606
|
|
|
2332
2607
|
console.log('');
|
|
@@ -3814,6 +4089,145 @@ async function main() {
|
|
|
3814
4089
|
process.exit(1);
|
|
3815
4090
|
}
|
|
3816
4091
|
|
|
4092
|
+
// ── ldm pair ────────────────────────────────────────────────────────
|
|
4093
|
+
// Device pairing for Bridge Phase A.
|
|
4094
|
+
// Links this machine to the user's Kaleidoscope account via passkey.
|
|
4095
|
+
//
|
|
4096
|
+
// Flow:
|
|
4097
|
+
// 1. Generate a human-readable code (BLUE-FISH-4729)
|
|
4098
|
+
// 2. POST the code to wip.computer/api/pair/request
|
|
4099
|
+
// 3. User goes to wip.computer/pair on their phone, signs in with passkey, enters code
|
|
4100
|
+
// 4. Poll GET /api/pair/status?code=X until approved or expired
|
|
4101
|
+
// 5. Store the device token at ~/.ldm/auth/kaleidoscope.json
|
|
4102
|
+
//
|
|
4103
|
+
// The code is shown in the terminal. The user navigates to the pairing
|
|
4104
|
+
// page themselves (CC does NOT open a URL, to prevent phishing).
|
|
4105
|
+
// The code expires after 120 seconds.
|
|
4106
|
+
|
|
4107
|
+
async function cmdPair() {
|
|
4108
|
+
const PAIR_API = process.env.LDM_PAIR_API || 'https://wip.computer';
|
|
4109
|
+
const AUTH_DIR = join(LDM_ROOT, 'auth');
|
|
4110
|
+
const TOKEN_PATH = join(AUTH_DIR, 'kaleidoscope.json');
|
|
4111
|
+
|
|
4112
|
+
// Check if already paired
|
|
4113
|
+
if (existsSync(TOKEN_PATH)) {
|
|
4114
|
+
try {
|
|
4115
|
+
const existing = JSON.parse(readFileSync(TOKEN_PATH, 'utf8'));
|
|
4116
|
+
if (existing.token) {
|
|
4117
|
+
console.log('');
|
|
4118
|
+
console.log(` Already paired as ${existing.userName || 'unknown'}`);
|
|
4119
|
+
console.log(` Paired: ${existing.pairedAt || 'unknown'}`);
|
|
4120
|
+
console.log(` Token: ${existing.token.slice(0, 8)}...`);
|
|
4121
|
+
console.log('');
|
|
4122
|
+
console.log(' To re-pair, delete ~/.ldm/auth/kaleidoscope.json and run ldm pair again.');
|
|
4123
|
+
console.log('');
|
|
4124
|
+
return;
|
|
4125
|
+
}
|
|
4126
|
+
} catch {}
|
|
4127
|
+
}
|
|
4128
|
+
|
|
4129
|
+
// Generate code
|
|
4130
|
+
const words = [
|
|
4131
|
+
'BLUE', 'RED', 'GREEN', 'GOLD', 'GRAY', 'PINK', 'DARK', 'WARM', 'COLD', 'WILD',
|
|
4132
|
+
'FISH', 'BIRD', 'WOLF', 'BEAR', 'DEER', 'HAWK', 'FROG', 'LYNX', 'DOVE', 'CROW',
|
|
4133
|
+
];
|
|
4134
|
+
const w1 = words[Math.floor(Math.random() * 10)];
|
|
4135
|
+
const w2 = words[10 + Math.floor(Math.random() * 10)];
|
|
4136
|
+
const num = String(Math.floor(1000 + Math.random() * 9000));
|
|
4137
|
+
const code = `${w1}-${w2}-${num}`;
|
|
4138
|
+
|
|
4139
|
+
// Detect device name
|
|
4140
|
+
const { hostname } = await import('node:os');
|
|
4141
|
+
const deviceName = hostname() || 'unknown';
|
|
4142
|
+
|
|
4143
|
+
// Read agent ID from config
|
|
4144
|
+
let agentId = 'cc-mini';
|
|
4145
|
+
try {
|
|
4146
|
+
const config = JSON.parse(readFileSync(join(LDM_ROOT, 'config.json'), 'utf8'));
|
|
4147
|
+
const agents = config.agents || {};
|
|
4148
|
+
for (const [id, agent] of Object.entries(agents)) {
|
|
4149
|
+
if (agent.harness === 'claude-code') { agentId = id; break; }
|
|
4150
|
+
}
|
|
4151
|
+
} catch {}
|
|
4152
|
+
|
|
4153
|
+
console.log('');
|
|
4154
|
+
console.log(' Pairing code:');
|
|
4155
|
+
console.log('');
|
|
4156
|
+
console.log(` ${code}`);
|
|
4157
|
+
console.log('');
|
|
4158
|
+
console.log(' Go to wip.computer/pair on your phone.');
|
|
4159
|
+
console.log(' Sign in with your passkey. Enter the code.');
|
|
4160
|
+
console.log('');
|
|
4161
|
+
console.log(' Waiting for approval...');
|
|
4162
|
+
|
|
4163
|
+
// Register the code with the server
|
|
4164
|
+
try {
|
|
4165
|
+
const registerRes = await fetch(`${PAIR_API}/api/pair/request`, {
|
|
4166
|
+
method: 'POST',
|
|
4167
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4168
|
+
body: JSON.stringify({ code, deviceName, agentId }),
|
|
4169
|
+
});
|
|
4170
|
+
if (!registerRes.ok) {
|
|
4171
|
+
const err = await registerRes.text();
|
|
4172
|
+
console.error(` x Failed to register pairing code: ${err}`);
|
|
4173
|
+
process.exit(1);
|
|
4174
|
+
}
|
|
4175
|
+
} catch (err) {
|
|
4176
|
+
console.error(` x Cannot reach ${PAIR_API}: ${err.message}`);
|
|
4177
|
+
console.error(' Make sure the server is running.');
|
|
4178
|
+
process.exit(1);
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4181
|
+
// Poll for approval (every 2 seconds, up to 120 seconds)
|
|
4182
|
+
const maxAttempts = 60;
|
|
4183
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
4184
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
4185
|
+
|
|
4186
|
+
try {
|
|
4187
|
+
const statusRes = await fetch(`${PAIR_API}/api/pair/status?code=${encodeURIComponent(code)}`);
|
|
4188
|
+
const data = await statusRes.json();
|
|
4189
|
+
|
|
4190
|
+
if (data.status === 'approved' && data.token) {
|
|
4191
|
+
// Store token
|
|
4192
|
+
mkdirSync(AUTH_DIR, { recursive: true });
|
|
4193
|
+
writeFileSync(TOKEN_PATH, JSON.stringify({
|
|
4194
|
+
token: data.token,
|
|
4195
|
+
userId: data.userId,
|
|
4196
|
+
userName: data.userName,
|
|
4197
|
+
deviceName,
|
|
4198
|
+
agentId,
|
|
4199
|
+
pairedAt: new Date().toISOString(),
|
|
4200
|
+
server: PAIR_API,
|
|
4201
|
+
}, null, 2) + '\n');
|
|
4202
|
+
|
|
4203
|
+
console.log('');
|
|
4204
|
+
console.log(` ✓ Paired as ${data.userName || 'User'} (${deviceName} / ${agentId})`);
|
|
4205
|
+
console.log(` Token stored at ~/.ldm/auth/kaleidoscope.json`);
|
|
4206
|
+
console.log('');
|
|
4207
|
+
return;
|
|
4208
|
+
}
|
|
4209
|
+
|
|
4210
|
+
if (statusRes.status === 404 || statusRes.status === 410) {
|
|
4211
|
+
console.error('');
|
|
4212
|
+
console.error(' x Code expired. Run ldm pair again.');
|
|
4213
|
+
console.error('');
|
|
4214
|
+
process.exit(1);
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4217
|
+
// Still pending. Keep polling.
|
|
4218
|
+
process.stdout.write('.');
|
|
4219
|
+
} catch {
|
|
4220
|
+
// Network error. Keep trying.
|
|
4221
|
+
process.stdout.write('x');
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4224
|
+
|
|
4225
|
+
console.error('');
|
|
4226
|
+
console.error(' x Timed out waiting for approval. Run ldm pair again.');
|
|
4227
|
+
console.error('');
|
|
4228
|
+
process.exit(1);
|
|
4229
|
+
}
|
|
4230
|
+
|
|
3817
4231
|
if (command === '--version' || command === '-v') {
|
|
3818
4232
|
console.log(PKG_VERSION);
|
|
3819
4233
|
process.exit(0);
|
|
@@ -3866,6 +4280,9 @@ async function main() {
|
|
|
3866
4280
|
case 'backup':
|
|
3867
4281
|
await cmdBackup();
|
|
3868
4282
|
break;
|
|
4283
|
+
case 'pair':
|
|
4284
|
+
await cmdPair();
|
|
4285
|
+
break;
|
|
3869
4286
|
default:
|
|
3870
4287
|
console.error(` Unknown command: ${command}`);
|
|
3871
4288
|
console.error(` Run: ldm --help`);
|