@wipcomputer/wip-ldm-os 0.4.73-alpha.32 → 0.4.73-alpha.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/ldm.js CHANGED
@@ -1746,6 +1746,67 @@ function autoDetectExtensions() {
1746
1746
  return found;
1747
1747
  }
1748
1748
 
1749
+ // ── 1Password SA token shell profile setup ──
1750
+
1751
+ /**
1752
+ * Ensure the 1Password SA token is exported in the user's shell profile so
1753
+ * Claude Code sessions, MCP servers, cron jobs, and launch agents can all
1754
+ * read secrets on demand via `op` without a biometric popup.
1755
+ *
1756
+ * Background: The op-secrets plugin injects OP_SERVICE_ACCOUNT_TOKEN into
1757
+ * the OpenClaw gateway process env at startup. But processes outside the
1758
+ * gateway's inheritance tree (Claude Code sessions, their hooks, MCPs, cron
1759
+ * jobs) never see it. The cleanest fix is to put it in the user's shell
1760
+ * profile so every shell and every CC session inherits it, and hooks can
1761
+ * then do `op read` on demand to fetch actual API keys. Only the SA token
1762
+ * (the key that unlocks other keys) lands in env; actual API keys stay in
1763
+ * 1Password and are fetched per-process.
1764
+ *
1765
+ * Idempotent. Skips if marker already present. Creates the profile file if
1766
+ * none of the candidates exist.
1767
+ *
1768
+ * See: ai/product/bugs/memory-crystal/2026-04-15--cc-mini--sa-token-env-and-hook-failfast.md
1769
+ */
1770
+ function ensureShellProfileSaToken() {
1771
+ const saTokenPath = join(HOME, '.openclaw/secrets/op-sa-token');
1772
+ if (!existsSync(saTokenPath)) return false;
1773
+
1774
+ const marker = '# LDM OS: 1Password SA token (for headless op CLI lookups)';
1775
+ 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`;
1776
+
1777
+ const shell = process.env.SHELL || '';
1778
+ const isZsh = shell.includes('zsh') || !shell;
1779
+ const candidates = isZsh
1780
+ ? [join(HOME, '.zprofile'), join(HOME, '.zshrc')]
1781
+ : [join(HOME, '.bash_profile'), join(HOME, '.profile'), join(HOME, '.bashrc')];
1782
+
1783
+ let targetPath = candidates.find(p => existsSync(p));
1784
+ if (!targetPath) targetPath = isZsh ? candidates[1] : candidates[0];
1785
+
1786
+ let existing = '';
1787
+ try {
1788
+ if (existsSync(targetPath)) existing = readFileSync(targetPath, 'utf-8');
1789
+ } catch {}
1790
+
1791
+ if (existing.includes(marker)) return false;
1792
+
1793
+ if (DRY_RUN) {
1794
+ console.log(` [dry run] Would append OP_SERVICE_ACCOUNT_TOKEN export to ${targetPath.replace(HOME, '~')}`);
1795
+ return false;
1796
+ }
1797
+
1798
+ try {
1799
+ appendFileSync(targetPath, block);
1800
+ const displayPath = targetPath.replace(HOME, '~');
1801
+ console.log(` + Shell profile updated: appended OP_SERVICE_ACCOUNT_TOKEN export to ${displayPath}`);
1802
+ console.log(` Open a new terminal or run: source ${displayPath}`);
1803
+ return true;
1804
+ } catch (err) {
1805
+ console.log(` - Could not update ${targetPath.replace(HOME, '~')}: ${err.message}`);
1806
+ return false;
1807
+ }
1808
+ }
1809
+
1749
1810
  // ── ldm install (bare): scan system, show real state, update if needed ──
1750
1811
 
1751
1812
  async function cmdInstallCatalog() {
@@ -2594,6 +2655,11 @@ async function cmdInstallCatalog() {
2594
2655
  console.log(' + Inbox-rewake hook updated (autonomous push: wakes on new bridge message)');
2595
2656
  }
2596
2657
 
2658
+ // Ensure 1Password SA token is exported in shell profile so Claude Code
2659
+ // sessions, MCPs, hooks, cron jobs all inherit it and can op read secrets
2660
+ // on demand. Idempotent; no-op if the export line is already present.
2661
+ ensureShellProfileSaToken();
2662
+
2597
2663
  // Deploy git pre-commit hook on every install (not just init)
2598
2664
  const hooksDir = join(LDM_ROOT, 'hooks');
2599
2665
  const preCommitDest = join(hooksDir, 'pre-commit');
@@ -100,6 +100,92 @@ This enables CC-to-CC awareness without a broker daemon. Any session can discove
100
100
  | `lib/sessions.mjs` | Session registration, discovery, PID liveness |
101
101
  | `dist/bridge/` | Compiled output (ships with npm package) |
102
102
 
103
+ ## ChatCompletions Routing (Fork Patches)
104
+
105
+ OpenClaw's gateway exposes an OpenAI-compatible chatCompletions endpoint at `http://localhost:18789/v1/chat/completions`. Upstream OpenClaw does not route these requests to the main agent session. We carry 4 patches on our fork to make this work.
106
+
107
+ **Patch 1: Session routing via `user=main`.**
108
+ When a CC session or external client sends a chatCompletions request, the gateway needs to know which OpenClaw session to route it to. This patch reads the `user` field from the request body. If `user=main` (or `user=openclaw`), the request routes to the main agent session (`agent:main:main`). Without this, bridge messages get "no session found" errors.
109
+
110
+ ```
111
+ POST /v1/chat/completions
112
+ Authorization: Bearer <gateway-token>
113
+ Content-Type: application/json
114
+
115
+ {"model":"openclaw","messages":[{"role":"user","content":"hi"}],"user":"main"}
116
+ ```
117
+
118
+ **Patch 2-3: Steer-backlog queue integration.**
119
+ When the agent is already busy (processing an iMessage from Parker), a concurrent chatCompletions request would fail or get dropped. These patches wire the chatCompletions endpoint into OpenClaw's `steer-backlog` queue (config: `messages.queue.mode: "steer-backlog"`). The message waits and gets processed after the current turn finishes. Works for both streaming and non-streaming responses. The gateway returns an `x-openclaw-queued: next-turn` header when a message is queued.
120
+
121
+ **Patch 4: Header rename.**
122
+ Cosmetic rename of the queue response header from `x-openclaw-queued: steer` to `x-openclaw-queued: next-turn` for clarity.
123
+
124
+ **Source:** `src/gateway/openai-http.ts`. Total patch size: ~100 lines. Carried on branch `cc-mini/chat-completions-v<version>`, rebased on each OpenClaw upgrade.
125
+
126
+ **Why not upstream:** OpenClaw's chatCompletions endpoint is designed for external API compatibility, not for multi-agent bridge routing. Our use case (CC sessions talking to an OpenClaw agent on the same machine) is specific to the LDM OS architecture.
127
+
128
+ ## Cooperative Push Architecture (Shipped Apr 11)
129
+
130
+ The original bridge used a pull model: CC sessions called `lesa_check_inbox` to check for messages. Messages sat unread until the next manual check. This was replaced with a cooperative push system where messages are delivered automatically.
131
+
132
+ ### Four Delivery Layers
133
+
134
+ Messages flow through four layers in order of priority. All four cooperate via shared `read: true` state on disk so a message delivered by one layer is skipped by the others.
135
+
136
+ | # | Layer | Fires when | Hook type | File |
137
+ |---|-------|-----------|-----------|------|
138
+ | 1 | **asyncRewake** (Stop hook) | New message arrives while session is idle | `fs.watch` on `~/.ldm/messages/` | `src/hooks/inbox-rewake-hook.mjs` |
139
+ | 2 | **UserPromptSubmit** | Next user prompt (typed or automated) | Claude Code hook | `src/hooks/inbox-check-hook.mjs` |
140
+ | 3 | **SessionStart** | New CC session boots | Claude Code hook | `src/hooks/boot-hook.mjs` |
141
+ | 4 | **Manual** | Explicit tool call | MCP tool | `lesa_check_inbox` |
142
+
143
+ **Layer 1 (asyncRewake)** is the autonomous push mechanism. It holds a long-lived `fs.watch` on `~/.ldm/messages/`, uses a per-session lockfile to prevent watcher stacking, and exits code 2 on a match to wake the idle model via Claude Code's task-notification path. It fires `fireBatch()` to deliver all pending matches in one wake cycle (cost linear in unique messages, not in layers).
144
+
145
+ **Layer 2 (UserPromptSubmit)** surfaces messages as `additionalContext` before each prompt. Messages appear in the session context without the user calling `lesa_check_inbox`.
146
+
147
+ **Deduplication:** Each layer marks messages `read: true` on disk after delivery. Subsequent layers check this flag and skip already-delivered messages. No double delivery. Cost is linear in unique messages, not in layers.
148
+
149
+ ### File Inbox
150
+
151
+ Messages live as JSON files at `~/.ldm/messages/`:
152
+
153
+ ```json
154
+ {
155
+ "id": "uuid",
156
+ "type": "chat",
157
+ "from": "lesa",
158
+ "to": "cc-mini:session-name",
159
+ "body": "message text",
160
+ "read": false,
161
+ "timestamp": "2026-04-11T19:05:00-07:00"
162
+ }
163
+ ```
164
+
165
+ ### Addressing
166
+
167
+ | Format | Meaning |
168
+ |--------|---------|
169
+ | `cc-mini` | Default session of agent cc-mini |
170
+ | `cc-mini:brainstorm` | Named session "brainstorm" on cc-mini |
171
+ | `cc-mini:*` | Broadcast to ALL sessions of cc-mini |
172
+ | `*` | Broadcast to all agents on the machine |
173
+ | `lesa` | The OpenClaw agent (routes through gateway chatCompletions) |
174
+
175
+ **Known issue (Apr 11):** Agent-broadcast without session specifier (`to: cc-mini`) fans out to ALL matching sessions. Three sessions replied independently to the same message. The addressing logic needs dedup for agent-broadcast targeting.
176
+
177
+ ### Tools
178
+
179
+ | Tool | Direction | Transport |
180
+ |------|-----------|-----------|
181
+ | `ldm_send_message` | Any agent → file inbox | Writes JSON to `~/.ldm/messages/` |
182
+ | `lesa_send_message` | CC → OpenClaw agent | HTTP POST to gateway chatCompletions |
183
+ | `lesa_check_inbox` | CC ← OpenClaw agent | Reads + drains `~/.ldm/messages/` for this session |
184
+
185
+ ### Plan Document
186
+
187
+ Full architecture: `ai/product/plans-prds/bridge/2026-04-11--cc-mini--autonomous-push-architecture.md` (377 lines, 8 open questions, Phase A shipped, Phase B deferred for CloudKit cross-machine transport).
188
+
103
189
  ## Node Communication (Future)
104
190
 
105
191
  Bridge currently works localhost only (Core). For Node -> Core communication:
package/lib/deploy.mjs CHANGED
@@ -867,7 +867,6 @@ function registerMCP(repoPath, door, toolName) {
867
867
  const envFlag = existsSync(OC_ROOT) ? ` -e OPENCLAW_HOME="${OC_ROOT}"` : '';
868
868
  execSync(`claude mcp add --scope user ${name}${envFlag} -- node "${mcpPath}"`, { stdio: 'pipe' });
869
869
  ok(`MCP: registered ${name} at user scope`);
870
- return true;
871
870
  } catch (e) {
872
871
  // Fallback: write to ~/.claude.json directly
873
872
  try {
@@ -879,12 +878,29 @@ function registerMCP(repoPath, door, toolName) {
879
878
  };
880
879
  writeJSON(ccUserPath, mcpConfig);
881
880
  ok(`MCP: registered ${name} in ~/.claude.json (fallback)`);
882
- return true;
883
881
  } catch (e2) {
884
882
  fail(`MCP: registration failed. ${e.message}`);
885
883
  return false;
886
884
  }
887
885
  }
886
+
887
+ // Also register with OpenClaw so the MCP tools are available to all
888
+ // OpenClaw agents (e.g. Lēsa) without exec-approval gates. CC
889
+ // registration alone only gives tools to Claude Code sessions, not
890
+ // to OpenClaw's agent pipeline. Discovered 2026-04-11 when Lēsa
891
+ // lost xAI image gen tools after switching from Grok to Claude CLI.
892
+ try {
893
+ const ocMcpConfig = JSON.stringify({ command: 'node', args: [mcpPath] });
894
+ execSync(`openclaw mcp set ${name} '${ocMcpConfig}'`, { stdio: 'pipe' });
895
+ ok(`MCP: registered ${name} with OpenClaw`);
896
+ } catch {
897
+ // Non-fatal: OpenClaw may not be installed on all machines
898
+ }
899
+
900
+ // Add to OpenClaw tools.allow so the MCP tools are pre-authorized
901
+ updateToolsAllow(name);
902
+
903
+ return true;
888
904
  }
889
905
 
890
906
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ldm-os",
3
- "version": "0.4.73-alpha.32",
3
+ "version": "0.4.73-alpha.34",
4
4
  "type": "module",
5
5
  "description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
6
6
  "engines": {