@wipcomputer/wip-ldm-os 0.4.73-alpha.26 → 0.4.73-alpha.27
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 +179 -65
- package/dist/bridge/{chunk-6TOUTUOG.js → chunk-7NH6JBIO.js} +1 -1
- package/dist/bridge/cli.js +1 -1
- package/dist/bridge/core.js +1 -1
- package/dist/bridge/mcp-server.js +1 -1
- package/lib/deploy.mjs +23 -0
- package/package.json +1 -1
- package/shared/docs/README.md.tmpl +2 -2
- package/shared/docs/how-releases-work.md.tmpl +3 -1
- package/shared/docs/how-worktrees-work.md.tmpl +12 -7
- package/shared/templates/claude-md-level1.md +7 -3
- package/src/bridge/core.ts +7 -3
- package/src/hosted-mcp/ecosystem.config.cjs +14 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Dual License: MIT + AGPLv3
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WIP Computer, Inc.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
1. MIT License (local and personal use)
|
|
7
|
+
---------------------------------------
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
2. GNU Affero General Public License v3.0 (commercial and cloud use)
|
|
29
|
+
--------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
If you run this software as part of a hosted service, cloud platform,
|
|
32
|
+
marketplace listing, or any network-accessible offering for commercial
|
|
33
|
+
purposes, the AGPLv3 terms apply. You must either:
|
|
34
|
+
|
|
35
|
+
a) Release your complete source code under AGPLv3, or
|
|
36
|
+
b) Obtain a commercial license.
|
|
37
|
+
|
|
38
|
+
This program is free software: you can redistribute it and/or modify
|
|
39
|
+
it under the terms of the GNU Affero General Public License as published
|
|
40
|
+
by the Free Software Foundation, either version 3 of the License, or
|
|
41
|
+
(at your option) any later version.
|
|
42
|
+
|
|
43
|
+
This program is distributed in the hope that it will be useful,
|
|
44
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
45
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
46
|
+
GNU Affero General Public License for more details.
|
|
47
|
+
|
|
48
|
+
You should have received a copy of the GNU Affero General Public License
|
|
49
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
AGPLv3 for personal use is free. Commercial licenses available.
|
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,84 @@ 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
|
+
|
|
371
449
|
// ── Catalog helpers ──
|
|
372
450
|
|
|
373
451
|
function loadCatalog() {
|
|
@@ -626,17 +704,58 @@ function checkBackupHealth() {
|
|
|
626
704
|
// After `npm install -g`, the updated files live at the npm package location but
|
|
627
705
|
// never get copied to ~/.ldm/extensions/lesa-bridge/dist/. This function fixes that.
|
|
628
706
|
|
|
707
|
+
function deployRules() {
|
|
708
|
+
const rulesSrc = join(__dirname, '..', 'shared', 'rules');
|
|
709
|
+
const rulesDest = join(LDM_ROOT, 'library', 'rules');
|
|
710
|
+
if (!existsSync(rulesSrc)) return;
|
|
711
|
+
mkdirSync(rulesDest, { recursive: true });
|
|
712
|
+
let rulesCount = 0;
|
|
713
|
+
for (const file of readdirSync(rulesSrc)) {
|
|
714
|
+
if (!file.endsWith('.md')) continue;
|
|
715
|
+
cpSync(join(rulesSrc, file), join(rulesDest, file));
|
|
716
|
+
rulesCount++;
|
|
717
|
+
}
|
|
718
|
+
if (rulesCount > 0) {
|
|
719
|
+
console.log(` + ${rulesCount} shared rules deployed to ~/.ldm/library/rules/`);
|
|
720
|
+
// Deploy to Claude Code harness (~/.claude/rules/)
|
|
721
|
+
const claudeRules = join(HOME, '.claude', 'rules');
|
|
722
|
+
if (existsSync(join(HOME, '.claude'))) {
|
|
723
|
+
mkdirSync(claudeRules, { recursive: true });
|
|
724
|
+
for (const file of readdirSync(rulesDest)) {
|
|
725
|
+
if (!file.endsWith('.md')) continue;
|
|
726
|
+
cpSync(join(rulesDest, file), join(claudeRules, file));
|
|
727
|
+
}
|
|
728
|
+
console.log(` + rules deployed to ~/.claude/rules/`);
|
|
729
|
+
}
|
|
730
|
+
// Deploy to OpenClaw harness (~/.openclaw/workspace/DEV-RULES.md)
|
|
731
|
+
const ocWorkspace = join(HOME, '.openclaw', 'workspace');
|
|
732
|
+
if (existsSync(ocWorkspace)) {
|
|
733
|
+
let combined = '# Dev Rules (deployed by ldm install)\n\n';
|
|
734
|
+
combined += '> Do not edit this file. It is regenerated by `ldm install`.\n';
|
|
735
|
+
combined += '> Source: ~/.ldm/library/rules/\n\n';
|
|
736
|
+
for (const file of readdirSync(rulesDest).sort()) {
|
|
737
|
+
if (!file.endsWith('.md')) continue;
|
|
738
|
+
combined += readFileSync(join(rulesDest, file), 'utf8') + '\n\n---\n\n';
|
|
739
|
+
}
|
|
740
|
+
writeFileSync(join(ocWorkspace, 'DEV-RULES.md'), combined);
|
|
741
|
+
console.log(` + rules deployed to ~/.openclaw/workspace/DEV-RULES.md`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
629
746
|
function deployBridge() {
|
|
630
747
|
const ldmBridgeDir = join(LDM_EXTENSIONS, 'lesa-bridge');
|
|
631
748
|
const ocBridgeDir = join(HOME, '.openclaw', 'extensions', 'lesa-bridge');
|
|
632
749
|
|
|
633
750
|
// Deploy targets: LDM path (canonical) and OpenClaw path (where the plugin loads)
|
|
751
|
+
// Create dirs if missing so first-time deploy works (don't skip with filter)
|
|
634
752
|
const targets = [
|
|
635
753
|
{ dir: ldmBridgeDir, label: '~/.ldm/extensions/lesa-bridge/dist/' },
|
|
636
754
|
{ dir: ocBridgeDir, label: '~/.openclaw/extensions/lesa-bridge/dist/' },
|
|
637
|
-
]
|
|
638
|
-
|
|
639
|
-
|
|
755
|
+
];
|
|
756
|
+
for (const t of targets) {
|
|
757
|
+
if (!existsSync(t.dir)) mkdirSync(t.dir, { recursive: true });
|
|
758
|
+
}
|
|
640
759
|
|
|
641
760
|
// Find the npm package bridge files. Try require.resolve first, fall back to known path.
|
|
642
761
|
let bridgeSrc = '';
|
|
@@ -767,10 +886,10 @@ async function cmdInit() {
|
|
|
767
886
|
join(LDM_ROOT, 'state'),
|
|
768
887
|
join(LDM_ROOT, 'sessions'),
|
|
769
888
|
join(LDM_ROOT, 'messages'),
|
|
770
|
-
join(LDM_ROOT, '
|
|
771
|
-
join(LDM_ROOT, '
|
|
772
|
-
join(LDM_ROOT, '
|
|
773
|
-
join(LDM_ROOT, '
|
|
889
|
+
join(LDM_ROOT, 'library', 'boot'),
|
|
890
|
+
join(LDM_ROOT, 'library', 'cron'),
|
|
891
|
+
join(LDM_ROOT, 'library', 'rules'),
|
|
892
|
+
join(LDM_ROOT, 'library', 'prompts'),
|
|
774
893
|
join(LDM_ROOT, 'hooks'),
|
|
775
894
|
];
|
|
776
895
|
|
|
@@ -944,68 +1063,29 @@ async function cmdInit() {
|
|
|
944
1063
|
// Deploy all scripts from scripts/ to ~/.ldm/bin/ (#119)
|
|
945
1064
|
deployScripts();
|
|
946
1065
|
|
|
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
|
-
}
|
|
1066
|
+
deployRules();
|
|
987
1067
|
|
|
988
|
-
// Deploy boot-config.json to ~/.ldm/
|
|
1068
|
+
// Deploy boot-config.json to ~/.ldm/library/boot/
|
|
989
1069
|
const bootSrc = join(__dirname, '..', 'shared', 'boot');
|
|
990
|
-
const bootDest = join(LDM_ROOT, '
|
|
1070
|
+
const bootDest = join(LDM_ROOT, 'library', 'boot');
|
|
991
1071
|
if (existsSync(bootSrc)) {
|
|
992
1072
|
mkdirSync(bootDest, { recursive: true });
|
|
993
1073
|
const bootConfig = join(bootSrc, 'boot-config.json');
|
|
994
1074
|
if (existsSync(bootConfig)) {
|
|
995
1075
|
cpSync(bootConfig, join(bootDest, 'boot-config.json'));
|
|
996
|
-
console.log(` + boot-config.json deployed to ~/.ldm/
|
|
1076
|
+
console.log(` + boot-config.json deployed to ~/.ldm/library/boot/`);
|
|
997
1077
|
}
|
|
998
1078
|
}
|
|
999
1079
|
|
|
1000
|
-
//
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1080
|
+
// CLAUDE.md files are NEVER deployed by the installer.
|
|
1081
|
+
// They are git-tracked files in their respective repos:
|
|
1082
|
+
// ~/.claude/CLAUDE.md ... wipcomputer-ldmos-wipcomputerinc-dot-claude-private
|
|
1083
|
+
// ~/wipcomputerinc/CLAUDE.md ... wipcomputerinc repo
|
|
1084
|
+
// ~/.openclaw/CLAUDE.md ... openclaw repo
|
|
1085
|
+
// Changes go through branches and PRs like any other file.
|
|
1086
|
+
// See: 2026-03-27--cc-mini--single-source-of-truth-reversed.md
|
|
1007
1087
|
|
|
1008
|
-
// Deploy shared templates to workspace
|
|
1088
|
+
// Deploy shared templates to workspace library/templates/
|
|
1009
1089
|
const templatesSrc = join(__dirname, '..', 'shared', 'templates');
|
|
1010
1090
|
if (existsSync(templatesSrc)) {
|
|
1011
1091
|
// Read workspace path from ~/.ldm/config.json
|
|
@@ -1015,7 +1095,7 @@ async function cmdInit() {
|
|
|
1015
1095
|
workspacePath = (ldmConfig.workspace || '').replace('~', HOME);
|
|
1016
1096
|
} catch {}
|
|
1017
1097
|
if (workspacePath && existsSync(workspacePath)) {
|
|
1018
|
-
const templatesDest = join(workspacePath, '
|
|
1098
|
+
const templatesDest = join(workspacePath, 'library', 'templates');
|
|
1019
1099
|
mkdirSync(templatesDest, { recursive: true });
|
|
1020
1100
|
let templatesCount = 0;
|
|
1021
1101
|
for (const file of readdirSync(templatesSrc)) {
|
|
@@ -1029,9 +1109,9 @@ async function cmdInit() {
|
|
|
1029
1109
|
}
|
|
1030
1110
|
}
|
|
1031
1111
|
|
|
1032
|
-
// Deploy shared prompts to ~/.ldm/
|
|
1112
|
+
// Deploy shared prompts to ~/.ldm/library/prompts/
|
|
1033
1113
|
const promptsSrc = join(__dirname, '..', 'shared', 'prompts');
|
|
1034
|
-
const promptsDest = join(LDM_ROOT, '
|
|
1114
|
+
const promptsDest = join(LDM_ROOT, 'library', 'prompts');
|
|
1035
1115
|
if (existsSync(promptsSrc)) {
|
|
1036
1116
|
mkdirSync(promptsDest, { recursive: true });
|
|
1037
1117
|
let promptsCount = 0;
|
|
@@ -1041,7 +1121,33 @@ async function cmdInit() {
|
|
|
1041
1121
|
promptsCount++;
|
|
1042
1122
|
}
|
|
1043
1123
|
if (promptsCount > 0) {
|
|
1044
|
-
console.log(` + ${promptsCount} shared prompts deployed to ~/.ldm/
|
|
1124
|
+
console.log(` + ${promptsCount} shared prompts deployed to ~/.ldm/library/prompts/`);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Backward-compat symlink: ~/.ldm/shared -> ~/.ldm/library
|
|
1129
|
+
// Anything still referencing shared/ will follow the symlink
|
|
1130
|
+
{
|
|
1131
|
+
const sharedPath = join(LDM_ROOT, 'shared');
|
|
1132
|
+
const libraryPath = join(LDM_ROOT, 'library');
|
|
1133
|
+
try {
|
|
1134
|
+
const stat = lstatSync(sharedPath);
|
|
1135
|
+
if (stat.isSymbolicLink()) {
|
|
1136
|
+
// Already a symlink, update target if needed
|
|
1137
|
+
const target = readlinkSync(sharedPath);
|
|
1138
|
+
if (target !== libraryPath) {
|
|
1139
|
+
unlinkSync(sharedPath);
|
|
1140
|
+
symlinkSync(libraryPath, sharedPath);
|
|
1141
|
+
}
|
|
1142
|
+
} else if (stat.isDirectory()) {
|
|
1143
|
+
// shared/ is a real directory (pre-rename state). Don't touch it.
|
|
1144
|
+
// The migration will handle this in a dedicated session.
|
|
1145
|
+
}
|
|
1146
|
+
} catch {
|
|
1147
|
+
// shared/ doesn't exist. Create symlink.
|
|
1148
|
+
try {
|
|
1149
|
+
symlinkSync(libraryPath, sharedPath);
|
|
1150
|
+
} catch {}
|
|
1045
1151
|
}
|
|
1046
1152
|
}
|
|
1047
1153
|
|
|
@@ -1604,9 +1710,10 @@ async function cmdInstallCatalog() {
|
|
|
1604
1710
|
// in the extension directories. This copies them to both LDM and OpenClaw targets.
|
|
1605
1711
|
deployBridge();
|
|
1606
1712
|
|
|
1607
|
-
// Deploy scripts and
|
|
1713
|
+
// Deploy scripts, docs, and rules on every install so fixes land without re-init
|
|
1608
1714
|
deployScripts();
|
|
1609
1715
|
deployDocs();
|
|
1716
|
+
deployRules();
|
|
1610
1717
|
|
|
1611
1718
|
// Check backup configuration
|
|
1612
1719
|
checkBackupHealth();
|
|
@@ -2380,6 +2487,13 @@ async function cmdInstallCatalog() {
|
|
|
2380
2487
|
ok('Boot hook updated (sessions, messages, updates now active)');
|
|
2381
2488
|
}
|
|
2382
2489
|
|
|
2490
|
+
// Sync inbox-check hook: UserPromptSubmit hook that surfaces pending
|
|
2491
|
+
// bridge messages into CC context on every prompt. Closes the gap
|
|
2492
|
+
// between lesa-bridge writes and CC delivery.
|
|
2493
|
+
if (syncInboxCheckHook()) {
|
|
2494
|
+
ok('Inbox-check hook updated (bridge messages surface automatically)');
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2383
2497
|
// Deploy git pre-commit hook on every install (not just init)
|
|
2384
2498
|
const hooksDir = join(LDM_ROOT, 'hooks');
|
|
2385
2499
|
const preCommitDest = join(hooksDir, 'pre-commit');
|
|
@@ -146,7 +146,7 @@ function refreshSessionIdentity() {
|
|
|
146
146
|
function parseTarget(to) {
|
|
147
147
|
if (to === "*") return { agent: "*", session: "*" };
|
|
148
148
|
const colonIdx = to.indexOf(":");
|
|
149
|
-
if (colonIdx === -1) return { agent: to, session: "
|
|
149
|
+
if (colonIdx === -1) return { agent: to, session: "*" };
|
|
150
150
|
return { agent: to.slice(0, colonIdx), session: to.slice(colonIdx + 1) };
|
|
151
151
|
}
|
|
152
152
|
function messageMatchesSession(msgTo, agentId, sessionName) {
|
package/dist/bridge/cli.js
CHANGED
package/dist/bridge/core.js
CHANGED
package/lib/deploy.mjs
CHANGED
|
@@ -537,6 +537,27 @@ function safeDeployDir(repoPath, destDir, name) {
|
|
|
537
537
|
function resolveOcPluginName(repoPath, toolName) {
|
|
538
538
|
// OpenClaw matches plugins by directory name, not plugin id.
|
|
539
539
|
// Check openclaw.json for existing references to this plugin.
|
|
540
|
+
/**
|
|
541
|
+
* Update tools.allow in openclaw.json to include a newly deployed plugin.
|
|
542
|
+
* OpenClaw 2026.4.8+ enforces tools.allow as an exclusive allowlist.
|
|
543
|
+
* Without this, newly installed plugins are blocked from running.
|
|
544
|
+
*/
|
|
545
|
+
function updateToolsAllow(pluginName) {
|
|
546
|
+
const ocConfigPath = join(OC_ROOT, 'openclaw.json');
|
|
547
|
+
if (!existsSync(ocConfigPath)) return;
|
|
548
|
+
try {
|
|
549
|
+
const raw = readFileSync(ocConfigPath, 'utf8');
|
|
550
|
+
const config = JSON.parse(raw);
|
|
551
|
+
if (!config.tools?.allow || !Array.isArray(config.tools.allow)) return;
|
|
552
|
+
if (config.tools.allow.includes(pluginName)) return;
|
|
553
|
+
config.tools.allow.push(pluginName);
|
|
554
|
+
writeFileSync(ocConfigPath, JSON.stringify(config, null, 2) + '\n');
|
|
555
|
+
log(`Added "${pluginName}" to openclaw.json tools.allow`);
|
|
556
|
+
} catch (e) {
|
|
557
|
+
log(`Warning: failed to update tools.allow for ${pluginName}: ${e.message}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
540
561
|
const ocConfigPath = join(OC_ROOT, 'openclaw.json');
|
|
541
562
|
const ocConfig = readJSON(ocConfigPath);
|
|
542
563
|
if (!ocConfig?.extensions) return toolName;
|
|
@@ -1072,6 +1093,8 @@ export function installSingleTool(toolPath) {
|
|
|
1072
1093
|
installed++;
|
|
1073
1094
|
registryInfo.ldmPath = join(LDM_EXTENSIONS, toolName);
|
|
1074
1095
|
registryInfo.ocPath = join(OC_EXTENSIONS, toolName);
|
|
1096
|
+
// Update tools.allow in openclaw.json so OC 2026.4.8+ doesn't block the plugin
|
|
1097
|
+
updateToolsAllow(toolName);
|
|
1075
1098
|
}
|
|
1076
1099
|
} else if (interfaces.mcp) {
|
|
1077
1100
|
const extName = basename(toolPath);
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@ Updates come from two sources:
|
|
|
6
6
|
- **config.json** ... when you change your org settings, the "Your System" sections regenerate
|
|
7
7
|
- **System hooks** ... when tools are installed, updated, or reconfigured, the relevant docs update automatically
|
|
8
8
|
|
|
9
|
-
If something is wrong, update
|
|
9
|
+
If something is wrong, update `~/.ldm/config.json` or run `ldm install`. The docs will follow.
|
|
10
10
|
|
|
11
11
|
## What's Here
|
|
12
12
|
|
|
@@ -32,4 +32,4 @@ Each doc has two sections:
|
|
|
32
32
|
1. **Universal** ... how the feature works for everyone (top of file)
|
|
33
33
|
2. **Your System** ... your specific configuration (bottom, after the `---` separator)
|
|
34
34
|
|
|
35
|
-
Universal content comes from the LDM OS repo. "Your System" content is generated from your
|
|
35
|
+
Universal content comes from the LDM OS repo. "Your System" content is generated from your `~/.ldm/config.json`.
|
|
@@ -23,8 +23,10 @@ cd .worktrees/repo--my-prefix--feature/
|
|
|
23
23
|
git push -u origin my-prefix/feature
|
|
24
24
|
gh pr create && gh pr merge --merge
|
|
25
25
|
|
|
26
|
-
# 3.
|
|
26
|
+
# 3. ALWAYS pull to main after merge (not optional)
|
|
27
27
|
cd /path/to/repo && git checkout main && git pull
|
|
28
|
+
|
|
29
|
+
# 4. Alpha release
|
|
28
30
|
wip-release alpha --notes="what changed"
|
|
29
31
|
|
|
30
32
|
# 4. Install and test
|
|
@@ -10,8 +10,8 @@ A git worktree is a second checkout of the same repo. Same history, same remote,
|
|
|
10
10
|
|
|
11
11
|
```
|
|
12
12
|
my-repo/ <- main branch (read-only)
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
.worktrees/my-repo--fix-bug/ <- your worktree (editable)
|
|
14
|
+
.worktrees/my-repo--new-feature/ <- someone else's worktree
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
All share the same `.git` database. Commits in any worktree are visible to all. But each has its own branch and files on disk.
|
|
@@ -23,22 +23,27 @@ cd my-repo
|
|
|
23
23
|
ldm worktree add my-prefix/fix-bug
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
This creates
|
|
26
|
+
This creates `.worktrees/my-repo--my-prefix--fix-bug/`.
|
|
27
27
|
|
|
28
28
|
## How to Work
|
|
29
29
|
|
|
30
30
|
Edit files in the worktree directory. Commit, push, PR, merge as normal:
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
cd
|
|
33
|
+
cd .worktrees/my-repo--my-prefix--fix-bug/
|
|
34
34
|
# edit, then:
|
|
35
35
|
git add <files>
|
|
36
36
|
git commit -m "description"
|
|
37
37
|
git push -u origin my-prefix/fix-bug
|
|
38
38
|
gh pr create
|
|
39
39
|
gh pr merge --merge --delete-branch
|
|
40
|
+
|
|
41
|
+
# CRITICAL: pull to main immediately after merge
|
|
42
|
+
cd /path/to/repo && git checkout main && git pull
|
|
40
43
|
```
|
|
41
44
|
|
|
45
|
+
**Always pull to main after merging a PR.** If you don't, the main working tree is stale and files won't show up. This is not optional. Every merge, every time.
|
|
46
|
+
|
|
42
47
|
## How to Clean Up
|
|
43
48
|
|
|
44
49
|
```bash
|
|
@@ -65,13 +70,13 @@ Switching branches changes every file in the directory. If another process (an a
|
|
|
65
70
|
|
|
66
71
|
## Your System
|
|
67
72
|
|
|
68
|
-
**Worktree location:** `~/wipcomputerinc/repos
|
|
73
|
+
**Worktree location:** `~/wipcomputerinc/repos/.worktrees/`
|
|
69
74
|
|
|
70
75
|
**Branch prefixes:**
|
|
71
76
|
- `cc-mini/` ... Claude Code on Mac mini
|
|
72
77
|
- `cc-air/` ... Claude Code on MacBook Air
|
|
73
78
|
- `lesa-mini/` ... Lesa on Mac mini
|
|
74
79
|
|
|
75
|
-
**Guard:** The branch guard warns if you create a worktree outside
|
|
80
|
+
**Guard:** The branch guard warns if you create a worktree outside `.worktrees/`. Suggests `ldm worktree add` instead.
|
|
76
81
|
|
|
77
|
-
**Auto-cleanup:** `wip-release` prunes merged worktrees from
|
|
82
|
+
**Auto-cleanup:** `wip-release` prunes merged worktrees from `.worktrees/` after every release.
|
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
Never use em dashes. Use periods, colons, semicolons, or ellipsis (...) instead.
|
|
6
6
|
Timezone: PST (Pacific), 24-hour clock. Parker is in Los Angeles.
|
|
7
7
|
|
|
8
|
+
## Don't Hedge
|
|
9
|
+
|
|
10
|
+
Never ask "should I stop?", "is this too much?", "what should we do now?", or "do you want me to continue?". If you have work to do, do it. If you're stuck, say what you're stuck on specifically. Don't express existential doubt about the task. Don't ask permission to keep working. Don't narrate your own uncertainty. Just work.
|
|
11
|
+
|
|
8
12
|
## Co-Authors on Every Commit
|
|
9
13
|
|
|
10
|
-
Read co-author lines from
|
|
14
|
+
Read co-author lines from `~/.ldm/config.json` coAuthors field. All contributors listed on every commit. No exceptions.
|
|
11
15
|
|
|
12
16
|
## 1Password CLI: Always Use Service Account Token
|
|
13
17
|
|
|
@@ -30,8 +34,8 @@ Before reaching for any external service or workaround: search memory first. Use
|
|
|
30
34
|
|
|
31
35
|
## Dev Conventions
|
|
32
36
|
|
|
33
|
-
For git workflow, releases, worktrees, and repo conventions: read `~/wipcomputerinc/
|
|
37
|
+
For git workflow, releases, worktrees, and repo conventions: read `~/wipcomputerinc/library/documentation/` on demand when doing repo work. Key docs:
|
|
34
38
|
- `how-worktrees-work.md` ... git worktrees, the convention, commands
|
|
35
39
|
- `how-releases-work.md` ... the full release pipeline
|
|
36
40
|
- `system-directories.md` ... what lives where
|
|
37
|
-
- Also read
|
|
41
|
+
- Also read `~/.ldm/shared/dev-guide-wipcomputerinc.md` for org-specific conventions
|
package/src/bridge/core.ts
CHANGED
|
@@ -258,14 +258,18 @@ export function refreshSessionIdentity(): void {
|
|
|
258
258
|
function parseTarget(to: string): { agent: string; session: string } {
|
|
259
259
|
if (to === "*") return { agent: "*", session: "*" };
|
|
260
260
|
const colonIdx = to.indexOf(":");
|
|
261
|
-
|
|
261
|
+
// Agent-only address (no colon, e.g. "cc-mini") is a broadcast to all
|
|
262
|
+
// sessions of that agent. Previously this defaulted to session "default"
|
|
263
|
+
// which silently dropped messages for any session with a non-default name.
|
|
264
|
+
// See: ai/product/bugs/bridge/2026-04-10--cc-mini--bridge-reply-addressing-mismatch.md
|
|
265
|
+
if (colonIdx === -1) return { agent: to, session: "*" };
|
|
262
266
|
return { agent: to.slice(0, colonIdx), session: to.slice(colonIdx + 1) };
|
|
263
267
|
}
|
|
264
268
|
|
|
265
269
|
/**
|
|
266
270
|
* Check if a message's "to" field matches this session.
|
|
267
271
|
* Matches: exact agent + session, agent broadcast (agent:*),
|
|
268
|
-
* global broadcast (*), or agent
|
|
272
|
+
* global broadcast (*), or agent-only address (no session qualifier).
|
|
269
273
|
*/
|
|
270
274
|
function messageMatchesSession(msgTo: string, agentId: string, sessionName: string): boolean {
|
|
271
275
|
// Global broadcast
|
|
@@ -276,7 +280,7 @@ function messageMatchesSession(msgTo: string, agentId: string, sessionName: stri
|
|
|
276
280
|
// Different agent entirely
|
|
277
281
|
if (target.agent !== "*" && target.agent !== agentId) return false;
|
|
278
282
|
|
|
279
|
-
// Agent broadcast (agent:*)
|
|
283
|
+
// Agent broadcast (agent:*) or agent-only address
|
|
280
284
|
if (target.session === "*") return true;
|
|
281
285
|
|
|
282
286
|
// Exact session match
|
|
@@ -1 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
// PM2 config for the hosted MCP server.
|
|
2
|
+
// API keys are resolved from 1Password at runtime via the op-secrets plugin.
|
|
3
|
+
// NEVER hardcode keys here. Use environment variables set by the deploy process,
|
|
4
|
+
// or read from 1Password at server startup.
|
|
5
|
+
module.exports = {
|
|
6
|
+
apps: [{
|
|
7
|
+
name: "mcp-server",
|
|
8
|
+
script: "server.mjs",
|
|
9
|
+
env: {
|
|
10
|
+
// XAI_API_KEY: resolved from 1Password at runtime (item: "x.ai - wip-computer-beta", field: "credential")
|
|
11
|
+
// DATABASE_URL: set in .env (gitignored)
|
|
12
|
+
}
|
|
13
|
+
}]
|
|
14
|
+
};
|