@wipcomputer/wip-ldm-os 0.4.73-alpha.9 → 0.4.75-alpha.1
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/SKILL.md +8 -1
- package/bin/ldm.js +600 -81
- 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/bridge/TECHNICAL.md +86 -0
- package/docs/doc-pipeline/README.md +74 -0
- package/docs/doc-pipeline/TECHNICAL.md +79 -0
- package/lib/deploy.mjs +175 -13
- package/lib/detect.mjs +20 -6
- package/package.json +2 -2
- package/shared/docs/README.md.tmpl +2 -2
- package/shared/docs/dev-guide-wipcomputerinc.md.tmpl +378 -0
- package/shared/docs/how-releases-work.md.tmpl +3 -1
- 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 +2093 -0
- package/src/hosted-mcp/shared/kaleidoscope.css +139 -0
- package/src/hosted-mcp/shared/kaleidoscope.js +192 -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,23 +714,31 @@ function deployDocs() {
|
|
|
544
714
|
return count;
|
|
545
715
|
}
|
|
546
716
|
|
|
547
|
-
// Deploy to
|
|
548
|
-
|
|
549
|
-
|
|
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.
|
|
720
|
+
const libraryDest = join(workspacePath, 'library', 'documentation');
|
|
721
|
+
mkdirSync(libraryDest, { recursive: true });
|
|
722
|
+
const docsCount = renderTemplates(libraryDest);
|
|
550
723
|
if (docsCount > 0) {
|
|
551
|
-
console.log(` + ${docsCount} personalized doc(s) deployed to ${
|
|
724
|
+
console.log(` + ${docsCount} personalized doc(s) deployed to ${libraryDest.replace(HOME, '~')}/`);
|
|
552
725
|
}
|
|
553
726
|
|
|
554
|
-
//
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
727
|
+
// Also deploy to ~/.ldm/library/documentation/ (the agent library, for internal docs
|
|
728
|
+
// like dev-guide-wipcomputerinc.md that should be available to agents but not
|
|
729
|
+
// surfaced in the human-facing workspace library).
|
|
730
|
+
// Added 2026-04-19 as part of retiring the sourceless `~/.ldm/shared/` deploy path
|
|
731
|
+
// for the private dev guide. The shared -> library migration across the board is
|
|
732
|
+
// pending; this change moves one file cleanly into the new location without
|
|
733
|
+
// pre-empting the broader migration.
|
|
734
|
+
const agentLibraryDest = join(LDM_ROOT, 'library', 'documentation');
|
|
735
|
+
mkdirSync(agentLibraryDest, { recursive: true });
|
|
736
|
+
const agentDocsCount = renderTemplates(agentLibraryDest);
|
|
737
|
+
if (agentDocsCount > 0) {
|
|
738
|
+
console.log(` + ${agentDocsCount} personalized doc(s) deployed to ${agentLibraryDest.replace(HOME, '~')}/`);
|
|
561
739
|
}
|
|
562
740
|
|
|
563
|
-
return docsCount;
|
|
741
|
+
return docsCount + agentDocsCount;
|
|
564
742
|
}
|
|
565
743
|
|
|
566
744
|
// Check backup health: is a trigger configured, did it run recently, is iCloud set up?
|
|
@@ -632,17 +810,58 @@ function checkBackupHealth() {
|
|
|
632
810
|
// After `npm install -g`, the updated files live at the npm package location but
|
|
633
811
|
// never get copied to ~/.ldm/extensions/lesa-bridge/dist/. This function fixes that.
|
|
634
812
|
|
|
813
|
+
function deployRules() {
|
|
814
|
+
const rulesSrc = join(__dirname, '..', 'shared', 'rules');
|
|
815
|
+
const rulesDest = join(LDM_ROOT, 'library', 'rules');
|
|
816
|
+
if (!existsSync(rulesSrc)) return;
|
|
817
|
+
mkdirSync(rulesDest, { recursive: true });
|
|
818
|
+
let rulesCount = 0;
|
|
819
|
+
for (const file of readdirSync(rulesSrc)) {
|
|
820
|
+
if (!file.endsWith('.md')) continue;
|
|
821
|
+
cpSync(join(rulesSrc, file), join(rulesDest, file));
|
|
822
|
+
rulesCount++;
|
|
823
|
+
}
|
|
824
|
+
if (rulesCount > 0) {
|
|
825
|
+
console.log(` + ${rulesCount} shared rules deployed to ~/.ldm/library/rules/`);
|
|
826
|
+
// Deploy to Claude Code harness (~/.claude/rules/)
|
|
827
|
+
const claudeRules = join(HOME, '.claude', 'rules');
|
|
828
|
+
if (existsSync(join(HOME, '.claude'))) {
|
|
829
|
+
mkdirSync(claudeRules, { recursive: true });
|
|
830
|
+
for (const file of readdirSync(rulesDest)) {
|
|
831
|
+
if (!file.endsWith('.md')) continue;
|
|
832
|
+
cpSync(join(rulesDest, file), join(claudeRules, file));
|
|
833
|
+
}
|
|
834
|
+
console.log(` + rules deployed to ~/.claude/rules/`);
|
|
835
|
+
}
|
|
836
|
+
// Deploy to OpenClaw harness (~/.openclaw/workspace/DEV-RULES.md)
|
|
837
|
+
const ocWorkspace = join(HOME, '.openclaw', 'workspace');
|
|
838
|
+
if (existsSync(ocWorkspace)) {
|
|
839
|
+
let combined = '# Dev Rules (deployed by ldm install)\n\n';
|
|
840
|
+
combined += '> Do not edit this file. It is regenerated by `ldm install`.\n';
|
|
841
|
+
combined += '> Source: ~/.ldm/library/rules/\n\n';
|
|
842
|
+
for (const file of readdirSync(rulesDest).sort()) {
|
|
843
|
+
if (!file.endsWith('.md')) continue;
|
|
844
|
+
combined += readFileSync(join(rulesDest, file), 'utf8') + '\n\n---\n\n';
|
|
845
|
+
}
|
|
846
|
+
writeFileSync(join(ocWorkspace, 'DEV-RULES.md'), combined);
|
|
847
|
+
console.log(` + rules deployed to ~/.openclaw/workspace/DEV-RULES.md`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
635
852
|
function deployBridge() {
|
|
636
853
|
const ldmBridgeDir = join(LDM_EXTENSIONS, 'lesa-bridge');
|
|
637
854
|
const ocBridgeDir = join(HOME, '.openclaw', 'extensions', 'lesa-bridge');
|
|
638
855
|
|
|
639
856
|
// Deploy targets: LDM path (canonical) and OpenClaw path (where the plugin loads)
|
|
857
|
+
// Create dirs if missing so first-time deploy works (don't skip with filter)
|
|
640
858
|
const targets = [
|
|
641
859
|
{ dir: ldmBridgeDir, label: '~/.ldm/extensions/lesa-bridge/dist/' },
|
|
642
860
|
{ dir: ocBridgeDir, label: '~/.openclaw/extensions/lesa-bridge/dist/' },
|
|
643
|
-
]
|
|
644
|
-
|
|
645
|
-
|
|
861
|
+
];
|
|
862
|
+
for (const t of targets) {
|
|
863
|
+
if (!existsSync(t.dir)) mkdirSync(t.dir, { recursive: true });
|
|
864
|
+
}
|
|
646
865
|
|
|
647
866
|
// Find the npm package bridge files. Try require.resolve first, fall back to known path.
|
|
648
867
|
let bridgeSrc = '';
|
|
@@ -773,10 +992,10 @@ async function cmdInit() {
|
|
|
773
992
|
join(LDM_ROOT, 'state'),
|
|
774
993
|
join(LDM_ROOT, 'sessions'),
|
|
775
994
|
join(LDM_ROOT, 'messages'),
|
|
776
|
-
join(LDM_ROOT, '
|
|
777
|
-
join(LDM_ROOT, '
|
|
778
|
-
join(LDM_ROOT, '
|
|
779
|
-
join(LDM_ROOT, '
|
|
995
|
+
join(LDM_ROOT, 'library', 'boot'),
|
|
996
|
+
join(LDM_ROOT, 'library', 'cron'),
|
|
997
|
+
join(LDM_ROOT, 'library', 'rules'),
|
|
998
|
+
join(LDM_ROOT, 'library', 'prompts'),
|
|
780
999
|
join(LDM_ROOT, 'hooks'),
|
|
781
1000
|
];
|
|
782
1001
|
|
|
@@ -820,10 +1039,16 @@ async function cmdInit() {
|
|
|
820
1039
|
// Scaffold workspace output dirs if workspace is configured
|
|
821
1040
|
const workspace = config.workspace;
|
|
822
1041
|
if (workspace && existsSync(workspace)) {
|
|
823
|
-
// Per-agent workspace dirs
|
|
824
|
-
|
|
1042
|
+
// Per-agent workspace dirs.
|
|
1043
|
+
// Resolve the team folder name from config.json agents[id].teamFolder
|
|
1044
|
+
// so agents with unicode names or custom folder names don't get ghost
|
|
1045
|
+
// folders created from their agent ID. Falls back to agent ID if no
|
|
1046
|
+
// override is configured. Fixed 2026-04-05 per INST-1: previously
|
|
1047
|
+
// hardcoded a map that only knew three agents and created ghost folders
|
|
1048
|
+
// for any others.
|
|
825
1049
|
for (const agentId of agentList) {
|
|
826
|
-
const
|
|
1050
|
+
const agentObj = typeof agentsObj[agentId] === 'object' ? agentsObj[agentId] : {};
|
|
1051
|
+
const teamName = agentObj.teamFolder || agentObj.name || agentId;
|
|
827
1052
|
for (const sub of ['journals', 'automated/memory/summaries/daily', 'automated/memory/summaries/weekly', 'automated/memory/summaries/monthly', 'automated/memory/summaries/quarterly']) {
|
|
828
1053
|
dirs.push(join(workspace, 'team', teamName, sub));
|
|
829
1054
|
}
|
|
@@ -944,68 +1169,29 @@ async function cmdInit() {
|
|
|
944
1169
|
// Deploy all scripts from scripts/ to ~/.ldm/bin/ (#119)
|
|
945
1170
|
deployScripts();
|
|
946
1171
|
|
|
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
|
-
}
|
|
1172
|
+
deployRules();
|
|
987
1173
|
|
|
988
|
-
// Deploy boot-config.json to ~/.ldm/
|
|
1174
|
+
// Deploy boot-config.json to ~/.ldm/library/boot/
|
|
989
1175
|
const bootSrc = join(__dirname, '..', 'shared', 'boot');
|
|
990
|
-
const bootDest = join(LDM_ROOT, '
|
|
1176
|
+
const bootDest = join(LDM_ROOT, 'library', 'boot');
|
|
991
1177
|
if (existsSync(bootSrc)) {
|
|
992
1178
|
mkdirSync(bootDest, { recursive: true });
|
|
993
1179
|
const bootConfig = join(bootSrc, 'boot-config.json');
|
|
994
1180
|
if (existsSync(bootConfig)) {
|
|
995
1181
|
cpSync(bootConfig, join(bootDest, 'boot-config.json'));
|
|
996
|
-
console.log(` + boot-config.json deployed to ~/.ldm/
|
|
1182
|
+
console.log(` + boot-config.json deployed to ~/.ldm/library/boot/`);
|
|
997
1183
|
}
|
|
998
1184
|
}
|
|
999
1185
|
|
|
1000
|
-
//
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1186
|
+
// CLAUDE.md files are NEVER deployed by the installer.
|
|
1187
|
+
// They are git-tracked files in their respective repos:
|
|
1188
|
+
// ~/.claude/CLAUDE.md ... wipcomputer-ldmos-wipcomputerinc-dot-claude-private
|
|
1189
|
+
// ~/wipcomputerinc/CLAUDE.md ... wipcomputerinc repo
|
|
1190
|
+
// ~/.openclaw/CLAUDE.md ... openclaw repo
|
|
1191
|
+
// Changes go through branches and PRs like any other file.
|
|
1192
|
+
// See: 2026-03-27--cc-mini--single-source-of-truth-reversed.md
|
|
1007
1193
|
|
|
1008
|
-
// Deploy shared templates to workspace
|
|
1194
|
+
// Deploy shared templates to workspace library/templates/
|
|
1009
1195
|
const templatesSrc = join(__dirname, '..', 'shared', 'templates');
|
|
1010
1196
|
if (existsSync(templatesSrc)) {
|
|
1011
1197
|
// Read workspace path from ~/.ldm/config.json
|
|
@@ -1015,7 +1201,7 @@ async function cmdInit() {
|
|
|
1015
1201
|
workspacePath = (ldmConfig.workspace || '').replace('~', HOME);
|
|
1016
1202
|
} catch {}
|
|
1017
1203
|
if (workspacePath && existsSync(workspacePath)) {
|
|
1018
|
-
const templatesDest = join(workspacePath, '
|
|
1204
|
+
const templatesDest = join(workspacePath, 'library', 'templates');
|
|
1019
1205
|
mkdirSync(templatesDest, { recursive: true });
|
|
1020
1206
|
let templatesCount = 0;
|
|
1021
1207
|
for (const file of readdirSync(templatesSrc)) {
|
|
@@ -1029,9 +1215,9 @@ async function cmdInit() {
|
|
|
1029
1215
|
}
|
|
1030
1216
|
}
|
|
1031
1217
|
|
|
1032
|
-
// Deploy shared prompts to ~/.ldm/
|
|
1218
|
+
// Deploy shared prompts to ~/.ldm/library/prompts/
|
|
1033
1219
|
const promptsSrc = join(__dirname, '..', 'shared', 'prompts');
|
|
1034
|
-
const promptsDest = join(LDM_ROOT, '
|
|
1220
|
+
const promptsDest = join(LDM_ROOT, 'library', 'prompts');
|
|
1035
1221
|
if (existsSync(promptsSrc)) {
|
|
1036
1222
|
mkdirSync(promptsDest, { recursive: true });
|
|
1037
1223
|
let promptsCount = 0;
|
|
@@ -1041,7 +1227,33 @@ async function cmdInit() {
|
|
|
1041
1227
|
promptsCount++;
|
|
1042
1228
|
}
|
|
1043
1229
|
if (promptsCount > 0) {
|
|
1044
|
-
console.log(` + ${promptsCount} shared prompts deployed to ~/.ldm/
|
|
1230
|
+
console.log(` + ${promptsCount} shared prompts deployed to ~/.ldm/library/prompts/`);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Backward-compat symlink: ~/.ldm/shared -> ~/.ldm/library
|
|
1235
|
+
// Anything still referencing shared/ will follow the symlink
|
|
1236
|
+
{
|
|
1237
|
+
const sharedPath = join(LDM_ROOT, 'shared');
|
|
1238
|
+
const libraryPath = join(LDM_ROOT, 'library');
|
|
1239
|
+
try {
|
|
1240
|
+
const stat = lstatSync(sharedPath);
|
|
1241
|
+
if (stat.isSymbolicLink()) {
|
|
1242
|
+
// Already a symlink, update target if needed
|
|
1243
|
+
const target = readlinkSync(sharedPath);
|
|
1244
|
+
if (target !== libraryPath) {
|
|
1245
|
+
unlinkSync(sharedPath);
|
|
1246
|
+
symlinkSync(libraryPath, sharedPath);
|
|
1247
|
+
}
|
|
1248
|
+
} else if (stat.isDirectory()) {
|
|
1249
|
+
// shared/ is a real directory (pre-rename state). Don't touch it.
|
|
1250
|
+
// The migration will handle this in a dedicated session.
|
|
1251
|
+
}
|
|
1252
|
+
} catch {
|
|
1253
|
+
// shared/ doesn't exist. Create symlink.
|
|
1254
|
+
try {
|
|
1255
|
+
symlinkSync(libraryPath, sharedPath);
|
|
1256
|
+
} catch {}
|
|
1045
1257
|
}
|
|
1046
1258
|
}
|
|
1047
1259
|
|
|
@@ -1548,6 +1760,135 @@ function autoDetectExtensions() {
|
|
|
1548
1760
|
return found;
|
|
1549
1761
|
}
|
|
1550
1762
|
|
|
1763
|
+
// ── Claude Code env override cleanup ──
|
|
1764
|
+
|
|
1765
|
+
/**
|
|
1766
|
+
* Strip stale Claude Code env overrides from ~/.claude/settings.json.
|
|
1767
|
+
*
|
|
1768
|
+
* These env vars were set manually during the Opus 4.6 era to force max
|
|
1769
|
+
* effort and disable adaptive thinking. With Opus 4.7+ the model picks
|
|
1770
|
+
* sensible defaults on its own and these forced overrides interfere with
|
|
1771
|
+
* adaptive behavior. They were never deployed by a template, so there is
|
|
1772
|
+
* no source-of-truth to fix ... the only place they exist is the user's
|
|
1773
|
+
* deployed settings.json.
|
|
1774
|
+
*
|
|
1775
|
+
* Idempotent: removes only the listed STALE_ENV_KEYS if present, drops
|
|
1776
|
+
* the env block entirely if it becomes empty, preserves any other env
|
|
1777
|
+
* keys untouched, silent no-op if nothing to remove.
|
|
1778
|
+
*
|
|
1779
|
+
* Adding more obsolete env keys to STALE_ENV_KEYS is the maintenance path
|
|
1780
|
+
* if other forced overrides need cleanup later.
|
|
1781
|
+
*/
|
|
1782
|
+
function cleanupStaleClaudeCodeEnv() {
|
|
1783
|
+
const settingsPath = join(HOME, '.claude/settings.json');
|
|
1784
|
+
if (!existsSync(settingsPath)) return false;
|
|
1785
|
+
|
|
1786
|
+
const STALE_ENV_KEYS = [
|
|
1787
|
+
'CLAUDE_CODE_EFFORT_LEVEL',
|
|
1788
|
+
'CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING',
|
|
1789
|
+
];
|
|
1790
|
+
|
|
1791
|
+
let settings;
|
|
1792
|
+
try {
|
|
1793
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
1794
|
+
} catch (err) {
|
|
1795
|
+
console.log(` - Could not parse ~/.claude/settings.json: ${err.message}`);
|
|
1796
|
+
return false;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
if (!settings.env || typeof settings.env !== 'object') return false;
|
|
1800
|
+
|
|
1801
|
+
const removed = [];
|
|
1802
|
+
for (const key of STALE_ENV_KEYS) {
|
|
1803
|
+
if (key in settings.env) {
|
|
1804
|
+
delete settings.env[key];
|
|
1805
|
+
removed.push(key);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
if (removed.length === 0) return false;
|
|
1810
|
+
|
|
1811
|
+
if (Object.keys(settings.env).length === 0) {
|
|
1812
|
+
delete settings.env;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
if (DRY_RUN) {
|
|
1816
|
+
console.log(` [dry run] Would remove stale env keys from ~/.claude/settings.json: ${removed.join(', ')}`);
|
|
1817
|
+
return false;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
try {
|
|
1821
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
1822
|
+
console.log(` + Removed stale env keys from ~/.claude/settings.json: ${removed.join(', ')}`);
|
|
1823
|
+
console.log(` New CC sessions will use Opus 4.7+ default effort and adaptive thinking`);
|
|
1824
|
+
return true;
|
|
1825
|
+
} catch (err) {
|
|
1826
|
+
console.log(` - Could not write ~/.claude/settings.json: ${err.message}`);
|
|
1827
|
+
return false;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// ── 1Password SA token shell profile setup ──
|
|
1832
|
+
|
|
1833
|
+
/**
|
|
1834
|
+
* Ensure the 1Password SA token is exported in the user's shell profile so
|
|
1835
|
+
* Claude Code sessions, MCP servers, cron jobs, and launch agents can all
|
|
1836
|
+
* read secrets on demand via `op` without a biometric popup.
|
|
1837
|
+
*
|
|
1838
|
+
* Background: The op-secrets plugin injects OP_SERVICE_ACCOUNT_TOKEN into
|
|
1839
|
+
* the OpenClaw gateway process env at startup. But processes outside the
|
|
1840
|
+
* gateway's inheritance tree (Claude Code sessions, their hooks, MCPs, cron
|
|
1841
|
+
* jobs) never see it. The cleanest fix is to put it in the user's shell
|
|
1842
|
+
* profile so every shell and every CC session inherits it, and hooks can
|
|
1843
|
+
* then do `op read` on demand to fetch actual API keys. Only the SA token
|
|
1844
|
+
* (the key that unlocks other keys) lands in env; actual API keys stay in
|
|
1845
|
+
* 1Password and are fetched per-process.
|
|
1846
|
+
*
|
|
1847
|
+
* Idempotent. Skips if marker already present. Creates the profile file if
|
|
1848
|
+
* none of the candidates exist.
|
|
1849
|
+
*
|
|
1850
|
+
* See: ai/product/bugs/memory-crystal/2026-04-15--cc-mini--sa-token-env-and-hook-failfast.md
|
|
1851
|
+
*/
|
|
1852
|
+
function ensureShellProfileSaToken() {
|
|
1853
|
+
const saTokenPath = join(HOME, '.openclaw/secrets/op-sa-token');
|
|
1854
|
+
if (!existsSync(saTokenPath)) return false;
|
|
1855
|
+
|
|
1856
|
+
const marker = '# LDM OS: 1Password SA token (for headless op CLI lookups)';
|
|
1857
|
+
const block = `\n${marker}\nif [ -f "$HOME/.openclaw/secrets/op-sa-token" ]; then\n export OP_SERVICE_ACCOUNT_TOKEN="$(cat "$HOME/.openclaw/secrets/op-sa-token")"\nfi\n`;
|
|
1858
|
+
|
|
1859
|
+
const shell = process.env.SHELL || '';
|
|
1860
|
+
const isZsh = shell.includes('zsh') || !shell;
|
|
1861
|
+
const candidates = isZsh
|
|
1862
|
+
? [join(HOME, '.zprofile'), join(HOME, '.zshrc')]
|
|
1863
|
+
: [join(HOME, '.bash_profile'), join(HOME, '.profile'), join(HOME, '.bashrc')];
|
|
1864
|
+
|
|
1865
|
+
let targetPath = candidates.find(p => existsSync(p));
|
|
1866
|
+
if (!targetPath) targetPath = isZsh ? candidates[1] : candidates[0];
|
|
1867
|
+
|
|
1868
|
+
let existing = '';
|
|
1869
|
+
try {
|
|
1870
|
+
if (existsSync(targetPath)) existing = readFileSync(targetPath, 'utf-8');
|
|
1871
|
+
} catch {}
|
|
1872
|
+
|
|
1873
|
+
if (existing.includes(marker)) return false;
|
|
1874
|
+
|
|
1875
|
+
if (DRY_RUN) {
|
|
1876
|
+
console.log(` [dry run] Would append OP_SERVICE_ACCOUNT_TOKEN export to ${targetPath.replace(HOME, '~')}`);
|
|
1877
|
+
return false;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
try {
|
|
1881
|
+
appendFileSync(targetPath, block);
|
|
1882
|
+
const displayPath = targetPath.replace(HOME, '~');
|
|
1883
|
+
console.log(` + Shell profile updated: appended OP_SERVICE_ACCOUNT_TOKEN export to ${displayPath}`);
|
|
1884
|
+
console.log(` Open a new terminal or run: source ${displayPath}`);
|
|
1885
|
+
return true;
|
|
1886
|
+
} catch (err) {
|
|
1887
|
+
console.log(` - Could not update ${targetPath.replace(HOME, '~')}: ${err.message}`);
|
|
1888
|
+
return false;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1551
1892
|
// ── ldm install (bare): scan system, show real state, update if needed ──
|
|
1552
1893
|
|
|
1553
1894
|
async function cmdInstallCatalog() {
|
|
@@ -1604,9 +1945,10 @@ async function cmdInstallCatalog() {
|
|
|
1604
1945
|
// in the extension directories. This copies them to both LDM and OpenClaw targets.
|
|
1605
1946
|
deployBridge();
|
|
1606
1947
|
|
|
1607
|
-
// Deploy scripts and
|
|
1948
|
+
// Deploy scripts, docs, and rules on every install so fixes land without re-init
|
|
1608
1949
|
deployScripts();
|
|
1609
1950
|
deployDocs();
|
|
1951
|
+
deployRules();
|
|
1610
1952
|
|
|
1611
1953
|
// Check backup configuration
|
|
1612
1954
|
checkBackupHealth();
|
|
@@ -2377,7 +2719,37 @@ async function cmdInstallCatalog() {
|
|
|
2377
2719
|
|
|
2378
2720
|
// Sync boot hook from npm package (#49)
|
|
2379
2721
|
if (syncBootHook()) {
|
|
2380
|
-
|
|
2722
|
+
console.log(' + Boot hook updated (sessions, messages, updates now active)');
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
// Sync inbox-check hook: UserPromptSubmit hook that surfaces pending
|
|
2726
|
+
// bridge messages into CC context on every prompt. Closes the gap
|
|
2727
|
+
// between lesa-bridge writes and CC delivery.
|
|
2728
|
+
if (syncInboxCheckHook()) {
|
|
2729
|
+
console.log(' + Inbox-check hook updated (bridge messages surface automatically)');
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
// Sync inbox-rewake hook: Stop hook with asyncRewake that watches
|
|
2733
|
+
// ~/.ldm/messages/ in the background and wakes the model when a new
|
|
2734
|
+
// bridge message arrives, without requiring user interaction. Layer 1
|
|
2735
|
+
// of the April 11 autonomous-push-architecture plan.
|
|
2736
|
+
if (syncInboxRewakeHook()) {
|
|
2737
|
+
console.log(' + Inbox-rewake hook updated (autonomous push: wakes on new bridge message)');
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
// Ensure 1Password SA token is exported in shell profile so Claude Code
|
|
2741
|
+
// sessions, MCPs, hooks, cron jobs all inherit it and can op read secrets
|
|
2742
|
+
// on demand. Idempotent; no-op if the export line is already present.
|
|
2743
|
+
ensureShellProfileSaToken();
|
|
2744
|
+
|
|
2745
|
+
// Deploy git pre-commit hook on every install (not just init)
|
|
2746
|
+
const hooksDir = join(LDM_ROOT, 'hooks');
|
|
2747
|
+
const preCommitDest = join(hooksDir, 'pre-commit');
|
|
2748
|
+
const preCommitSrc = join(__dirname, '..', 'templates', 'hooks', 'pre-commit');
|
|
2749
|
+
if (existsSync(preCommitSrc)) {
|
|
2750
|
+
if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });
|
|
2751
|
+
cpSync(preCommitSrc, preCommitDest);
|
|
2752
|
+
chmodSync(preCommitDest, 0o755);
|
|
2381
2753
|
}
|
|
2382
2754
|
|
|
2383
2755
|
console.log('');
|
|
@@ -2515,6 +2887,11 @@ async function cmdDoctor() {
|
|
|
2515
2887
|
}
|
|
2516
2888
|
}
|
|
2517
2889
|
|
|
2890
|
+
// --fix: clean stale Claude Code env overrides (Opus 4.6 era) from ~/.claude/settings.json
|
|
2891
|
+
if (FIX_FLAG) {
|
|
2892
|
+
cleanupStaleClaudeCodeEnv();
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2518
2895
|
// 4. Check sacred locations
|
|
2519
2896
|
const sacred = [
|
|
2520
2897
|
{ path: join(LDM_ROOT, 'memory'), label: 'memory/' },
|
|
@@ -3865,6 +4242,145 @@ async function main() {
|
|
|
3865
4242
|
process.exit(1);
|
|
3866
4243
|
}
|
|
3867
4244
|
|
|
4245
|
+
// ── ldm pair ────────────────────────────────────────────────────────
|
|
4246
|
+
// Device pairing for Bridge Phase A.
|
|
4247
|
+
// Links this machine to the user's Kaleidoscope account via passkey.
|
|
4248
|
+
//
|
|
4249
|
+
// Flow:
|
|
4250
|
+
// 1. Generate a human-readable code (BLUE-FISH-4729)
|
|
4251
|
+
// 2. POST the code to wip.computer/api/pair/request
|
|
4252
|
+
// 3. User goes to wip.computer/pair on their phone, signs in with passkey, enters code
|
|
4253
|
+
// 4. Poll GET /api/pair/status?code=X until approved or expired
|
|
4254
|
+
// 5. Store the device token at ~/.ldm/auth/kaleidoscope.json
|
|
4255
|
+
//
|
|
4256
|
+
// The code is shown in the terminal. The user navigates to the pairing
|
|
4257
|
+
// page themselves (CC does NOT open a URL, to prevent phishing).
|
|
4258
|
+
// The code expires after 120 seconds.
|
|
4259
|
+
|
|
4260
|
+
async function cmdPair() {
|
|
4261
|
+
const PAIR_API = process.env.LDM_PAIR_API || 'https://wip.computer';
|
|
4262
|
+
const AUTH_DIR = join(LDM_ROOT, 'auth');
|
|
4263
|
+
const TOKEN_PATH = join(AUTH_DIR, 'kaleidoscope.json');
|
|
4264
|
+
|
|
4265
|
+
// Check if already paired
|
|
4266
|
+
if (existsSync(TOKEN_PATH)) {
|
|
4267
|
+
try {
|
|
4268
|
+
const existing = JSON.parse(readFileSync(TOKEN_PATH, 'utf8'));
|
|
4269
|
+
if (existing.token) {
|
|
4270
|
+
console.log('');
|
|
4271
|
+
console.log(` Already paired as ${existing.userName || 'unknown'}`);
|
|
4272
|
+
console.log(` Paired: ${existing.pairedAt || 'unknown'}`);
|
|
4273
|
+
console.log(` Token: ${existing.token.slice(0, 8)}...`);
|
|
4274
|
+
console.log('');
|
|
4275
|
+
console.log(' To re-pair, delete ~/.ldm/auth/kaleidoscope.json and run ldm pair again.');
|
|
4276
|
+
console.log('');
|
|
4277
|
+
return;
|
|
4278
|
+
}
|
|
4279
|
+
} catch {}
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
// Generate code
|
|
4283
|
+
const words = [
|
|
4284
|
+
'BLUE', 'RED', 'GREEN', 'GOLD', 'GRAY', 'PINK', 'DARK', 'WARM', 'COLD', 'WILD',
|
|
4285
|
+
'FISH', 'BIRD', 'WOLF', 'BEAR', 'DEER', 'HAWK', 'FROG', 'LYNX', 'DOVE', 'CROW',
|
|
4286
|
+
];
|
|
4287
|
+
const w1 = words[Math.floor(Math.random() * 10)];
|
|
4288
|
+
const w2 = words[10 + Math.floor(Math.random() * 10)];
|
|
4289
|
+
const num = String(Math.floor(1000 + Math.random() * 9000));
|
|
4290
|
+
const code = `${w1}-${w2}-${num}`;
|
|
4291
|
+
|
|
4292
|
+
// Detect device name
|
|
4293
|
+
const { hostname } = await import('node:os');
|
|
4294
|
+
const deviceName = hostname() || 'unknown';
|
|
4295
|
+
|
|
4296
|
+
// Read agent ID from config
|
|
4297
|
+
let agentId = 'cc-mini';
|
|
4298
|
+
try {
|
|
4299
|
+
const config = JSON.parse(readFileSync(join(LDM_ROOT, 'config.json'), 'utf8'));
|
|
4300
|
+
const agents = config.agents || {};
|
|
4301
|
+
for (const [id, agent] of Object.entries(agents)) {
|
|
4302
|
+
if (agent.harness === 'claude-code') { agentId = id; break; }
|
|
4303
|
+
}
|
|
4304
|
+
} catch {}
|
|
4305
|
+
|
|
4306
|
+
console.log('');
|
|
4307
|
+
console.log(' Pairing code:');
|
|
4308
|
+
console.log('');
|
|
4309
|
+
console.log(` ${code}`);
|
|
4310
|
+
console.log('');
|
|
4311
|
+
console.log(' Go to wip.computer/pair on your phone.');
|
|
4312
|
+
console.log(' Sign in with your passkey. Enter the code.');
|
|
4313
|
+
console.log('');
|
|
4314
|
+
console.log(' Waiting for approval...');
|
|
4315
|
+
|
|
4316
|
+
// Register the code with the server
|
|
4317
|
+
try {
|
|
4318
|
+
const registerRes = await fetch(`${PAIR_API}/api/pair/request`, {
|
|
4319
|
+
method: 'POST',
|
|
4320
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4321
|
+
body: JSON.stringify({ code, deviceName, agentId }),
|
|
4322
|
+
});
|
|
4323
|
+
if (!registerRes.ok) {
|
|
4324
|
+
const err = await registerRes.text();
|
|
4325
|
+
console.error(` x Failed to register pairing code: ${err}`);
|
|
4326
|
+
process.exit(1);
|
|
4327
|
+
}
|
|
4328
|
+
} catch (err) {
|
|
4329
|
+
console.error(` x Cannot reach ${PAIR_API}: ${err.message}`);
|
|
4330
|
+
console.error(' Make sure the server is running.');
|
|
4331
|
+
process.exit(1);
|
|
4332
|
+
}
|
|
4333
|
+
|
|
4334
|
+
// Poll for approval (every 2 seconds, up to 120 seconds)
|
|
4335
|
+
const maxAttempts = 60;
|
|
4336
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
4337
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
4338
|
+
|
|
4339
|
+
try {
|
|
4340
|
+
const statusRes = await fetch(`${PAIR_API}/api/pair/status?code=${encodeURIComponent(code)}`);
|
|
4341
|
+
const data = await statusRes.json();
|
|
4342
|
+
|
|
4343
|
+
if (data.status === 'approved' && data.token) {
|
|
4344
|
+
// Store token
|
|
4345
|
+
mkdirSync(AUTH_DIR, { recursive: true });
|
|
4346
|
+
writeFileSync(TOKEN_PATH, JSON.stringify({
|
|
4347
|
+
token: data.token,
|
|
4348
|
+
userId: data.userId,
|
|
4349
|
+
userName: data.userName,
|
|
4350
|
+
deviceName,
|
|
4351
|
+
agentId,
|
|
4352
|
+
pairedAt: new Date().toISOString(),
|
|
4353
|
+
server: PAIR_API,
|
|
4354
|
+
}, null, 2) + '\n');
|
|
4355
|
+
|
|
4356
|
+
console.log('');
|
|
4357
|
+
console.log(` ✓ Paired as ${data.userName || 'User'} (${deviceName} / ${agentId})`);
|
|
4358
|
+
console.log(` Token stored at ~/.ldm/auth/kaleidoscope.json`);
|
|
4359
|
+
console.log('');
|
|
4360
|
+
return;
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4363
|
+
if (statusRes.status === 404 || statusRes.status === 410) {
|
|
4364
|
+
console.error('');
|
|
4365
|
+
console.error(' x Code expired. Run ldm pair again.');
|
|
4366
|
+
console.error('');
|
|
4367
|
+
process.exit(1);
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4370
|
+
// Still pending. Keep polling.
|
|
4371
|
+
process.stdout.write('.');
|
|
4372
|
+
} catch {
|
|
4373
|
+
// Network error. Keep trying.
|
|
4374
|
+
process.stdout.write('x');
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
|
|
4378
|
+
console.error('');
|
|
4379
|
+
console.error(' x Timed out waiting for approval. Run ldm pair again.');
|
|
4380
|
+
console.error('');
|
|
4381
|
+
process.exit(1);
|
|
4382
|
+
}
|
|
4383
|
+
|
|
3868
4384
|
if (command === '--version' || command === '-v') {
|
|
3869
4385
|
console.log(PKG_VERSION);
|
|
3870
4386
|
process.exit(0);
|
|
@@ -3917,6 +4433,9 @@ async function main() {
|
|
|
3917
4433
|
case 'backup':
|
|
3918
4434
|
await cmdBackup();
|
|
3919
4435
|
break;
|
|
4436
|
+
case 'pair':
|
|
4437
|
+
await cmdPair();
|
|
4438
|
+
break;
|
|
3920
4439
|
default:
|
|
3921
4440
|
console.error(` Unknown command: ${command}`);
|
|
3922
4441
|
console.error(` Run: ldm --help`);
|