@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 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, 'shared', 'boot', 'boot-hook.mjs');
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
- ].filter(t => existsSync(t.dir)); // Only deploy if the extension dir exists
638
-
639
- if (targets.length === 0) return 0;
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, 'shared', 'boot'),
771
- join(LDM_ROOT, 'shared', 'cron'),
772
- join(LDM_ROOT, 'shared', 'rules'),
773
- join(LDM_ROOT, 'shared', 'prompts'),
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
- // Deploy shared rules to ~/.ldm/shared/rules/ and to harnesses
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/shared/boot/
1068
+ // Deploy boot-config.json to ~/.ldm/library/boot/
989
1069
  const bootSrc = join(__dirname, '..', 'shared', 'boot');
990
- const bootDest = join(LDM_ROOT, 'shared', 'boot');
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/shared/boot/`);
1076
+ console.log(` + boot-config.json deployed to ~/.ldm/library/boot/`);
997
1077
  }
998
1078
  }
999
1079
 
1000
- // Deploy Level 1 CLAUDE.md template to ~/.claude/CLAUDE.md
1001
- const claudeMdTemplate = join(__dirname, '..', 'shared', 'templates', 'claude-md-level1.md');
1002
- const claudeMdDest = join(HOME, '.claude', 'CLAUDE.md');
1003
- if (existsSync(claudeMdTemplate) && existsSync(join(HOME, '.claude'))) {
1004
- cpSync(claudeMdTemplate, claudeMdDest);
1005
- console.log(` + Level 1 CLAUDE.md deployed to ~/.claude/CLAUDE.md`);
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 settings/templates/
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, 'settings', 'templates');
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/shared/prompts/
1112
+ // Deploy shared prompts to ~/.ldm/library/prompts/
1033
1113
  const promptsSrc = join(__dirname, '..', 'shared', 'prompts');
1034
- const promptsDest = join(LDM_ROOT, 'shared', 'prompts');
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/shared/prompts/`);
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 docs on every install so fixes land without re-init
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: "default" };
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) {
@@ -8,7 +8,7 @@ import {
8
8
  searchConversations,
9
9
  searchWorkspace,
10
10
  sendMessage
11
- } from "./chunk-6TOUTUOG.js";
11
+ } from "./chunk-7NH6JBIO.js";
12
12
  import "./chunk-3RG5ZIWI.js";
13
13
 
14
14
  // cli.ts
@@ -24,7 +24,7 @@ import {
24
24
  sendLdmMessage,
25
25
  sendMessage,
26
26
  setSessionIdentity
27
- } from "./chunk-6TOUTUOG.js";
27
+ } from "./chunk-7NH6JBIO.js";
28
28
  import "./chunk-3RG5ZIWI.js";
29
29
  export {
30
30
  LDM_ROOT,
@@ -15,7 +15,7 @@ import {
15
15
  sendLdmMessage,
16
16
  sendMessage,
17
17
  setSessionIdentity
18
- } from "./chunk-6TOUTUOG.js";
18
+ } from "./chunk-7NH6JBIO.js";
19
19
  import {
20
20
  __require
21
21
  } from "./chunk-3RG5ZIWI.js";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ldm-os",
3
- "version": "0.4.73-alpha.26",
3
+ "version": "0.4.73-alpha.27",
4
4
  "type": "module",
5
5
  "description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
6
6
  "engines": {
@@ -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 `settings/config.json` or run `ldm install`. The docs will follow.
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 `settings/config.json`.
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. Alpha release
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
- _worktrees/my-repo--fix-bug/ <- your worktree (editable)
14
- _worktrees/my-repo--new-feature/ <- someone else's worktree
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 `_worktrees/my-repo--my-prefix--fix-bug/`.
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 _worktrees/my-repo--my-prefix--fix-bug/
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/_worktrees/`
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 `_worktrees/`. Suggests `ldm worktree add` instead.
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 `_worktrees/` after every release.
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 `~/wipcomputerinc/settings/config.json` coAuthors field. All contributors listed on every commit. No exceptions.
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/settings/docs/` on demand when doing repo work. Key docs:
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 `~/wipcomputerinc/settings/templates/dev-guide-private.md` for org-specific conventions
41
+ - Also read `~/.ldm/shared/dev-guide-wipcomputerinc.md` for org-specific conventions
@@ -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
- if (colonIdx === -1) return { agent: to, session: "default" };
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 with default session.
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
- module.exports = { apps: [{ name: "mcp-server", script: "server.mjs", env: { XAI_API_KEY: "xai-49EPIbxGkCGIUzoUCuG8Fa63BTHxrNSkQ6uyV7zXI3vjJ1pAzD6gKVT6WGNsFZoABjI2DBCGkjd8OAgk" } }] };
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
+ };