@silicaclaw/cli 2026.3.20-3 → 2026.3.20-5

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/INSTALL.md +2 -2
  3. package/README.md +2 -2
  4. package/VERSION +1 -1
  5. package/apps/local-console/dist/apps/local-console/src/server.d.ts +39 -0
  6. package/apps/local-console/dist/apps/local-console/src/server.js +229 -12
  7. package/apps/local-console/dist/packages/network/src/relayPreview.d.ts +4 -0
  8. package/apps/local-console/dist/packages/network/src/relayPreview.js +37 -6
  9. package/apps/local-console/public/app/app.js +293 -2
  10. package/apps/local-console/public/app/network.js +144 -32
  11. package/apps/local-console/public/app/overview.js +43 -15
  12. package/apps/local-console/public/app/social.js +135 -53
  13. package/apps/local-console/public/app/styles.css +86 -0
  14. package/apps/local-console/public/app/template.js +7 -1
  15. package/apps/local-console/public/app/translations.js +44 -0
  16. package/apps/local-console/src/server.ts +262 -14
  17. package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.d.ts +4 -0
  18. package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.js +37 -6
  19. package/node_modules/@silicaclaw/network/src/relayPreview.ts +41 -6
  20. package/openclaw-skills/silicaclaw-broadcast/VERSION +1 -1
  21. package/openclaw-skills/silicaclaw-broadcast/manifest.json +1 -1
  22. package/openclaw-skills/silicaclaw-owner-push/VERSION +1 -1
  23. package/openclaw-skills/silicaclaw-owner-push/manifest.json +1 -1
  24. package/openclaw-skills/silicaclaw-owner-push/references/runtime-setup.md +3 -0
  25. package/openclaw-skills/silicaclaw-owner-push/scripts/owner-push-forwarder.mjs +67 -8
  26. package/package.json +1 -1
  27. package/packages/network/dist/packages/network/src/relayPreview.d.ts +4 -0
  28. package/packages/network/dist/packages/network/src/relayPreview.js +37 -6
  29. package/packages/network/src/relayPreview.ts +41 -6
  30. package/scripts/silicaclaw-cli.mjs +4 -1
  31. package/scripts/silicaclaw-gateway.mjs +108 -0
@@ -1 +1 @@
1
- 2026.3.20-beta.3
1
+ 2026.3.20-beta.5
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "silicaclaw-broadcast",
3
- "version": "2026.3.20-beta.3",
3
+ "version": "2026.3.20-beta.5",
4
4
  "display_name": "SilicaClaw Broadcast",
5
5
  "description": "Official OpenClaw skill for a bounded local SilicaClaw broadcast workflow: read public broadcasts, publish public broadcasts, and optionally forward owner-relevant summaries through OpenClaw's native channel.",
6
6
  "entrypoints": {
@@ -1 +1 @@
1
- 2026.3.20-beta.1
1
+ 2026.3.20-beta.2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "silicaclaw-owner-push",
3
- "version": "2026.3.20-beta.1",
3
+ "version": "2026.3.20-beta.2",
4
4
  "display_name": "SilicaClaw Owner Push",
5
5
  "description": "Official OpenClaw skill for a bounded local SilicaClaw monitoring workflow: watch public broadcasts, filter owner-relevant updates, and push concise summaries through OpenClaw's native owner channel.",
6
6
  "entrypoints": {
@@ -19,6 +19,7 @@ export OPENCLAW_FORWARD_INCLUDE="approval,failed,blocked,completed"
19
19
  export OPENCLAW_FORWARD_EXCLUDE="heartbeat,debug"
20
20
  export OPENCLAW_FORWARDER_INTERVAL_MS="5000"
21
21
  export OPENCLAW_FORWARDER_LIMIT="30"
22
+ export OPENCLAW_FORWARD_LATEST_ONLY="true"
22
23
  ```
23
24
 
24
25
  ## Persistent cursor
@@ -33,6 +34,8 @@ Override it with:
33
34
  export OPENCLAW_OWNER_FORWARD_STATE_PATH="/custom/path/silicaclaw-owner-push.json"
34
35
  ```
35
36
 
37
+ The state file now also stores the last pushed message timestamp and message id so the forwarder can push only the latest qualifying message after that cursor and skip older messages permanently.
38
+
36
39
  ## Typical topology
37
40
 
38
41
  - A machine: runs SilicaClaw and publishes public broadcasts
@@ -12,6 +12,7 @@ const OWNER_FORWARD_CMD = String(process.env.OPENCLAW_OWNER_FORWARD_CMD || "").t
12
12
  const STATE_PATH = resolve(
13
13
  String(process.env.OPENCLAW_OWNER_FORWARD_STATE_PATH || resolve(homedir(), ".openclaw", "workspace", "state", "silicaclaw-owner-push.json"))
14
14
  );
15
+ const LATEST_ONLY = String(process.env.OPENCLAW_FORWARD_LATEST_ONLY || "true").trim().toLowerCase() !== "false";
15
16
  const ONCE = process.argv.includes("--once");
16
17
  const VERBOSE = process.argv.includes("--verbose");
17
18
 
@@ -61,14 +62,24 @@ function loadState() {
61
62
  return {
62
63
  seen_ids: [],
63
64
  pushed_at: {},
65
+ last_pushed_created_at: 0,
66
+ last_pushed_message_id: "",
64
67
  };
65
68
  }
66
69
  try {
67
- return JSON.parse(readFileSync(STATE_PATH, "utf8"));
70
+ const parsed = JSON.parse(readFileSync(STATE_PATH, "utf8"));
71
+ return {
72
+ seen_ids: Array.isArray(parsed?.seen_ids) ? parsed.seen_ids : [],
73
+ pushed_at: parsed?.pushed_at && typeof parsed.pushed_at === "object" ? parsed.pushed_at : {},
74
+ last_pushed_created_at: Number(parsed?.last_pushed_created_at || 0) || 0,
75
+ last_pushed_message_id: String(parsed?.last_pushed_message_id || ""),
76
+ };
68
77
  } catch {
69
78
  return {
70
79
  seen_ids: [],
71
80
  pushed_at: {},
81
+ last_pushed_created_at: 0,
82
+ last_pushed_message_id: "",
72
83
  };
73
84
  }
74
85
  }
@@ -85,6 +96,22 @@ function trimState(state) {
85
96
  state.pushed_at = Object.fromEntries(pushedEntries);
86
97
  }
87
98
 
99
+ function messageCreatedAt(item) {
100
+ const createdAt = Number(item?.created_at || 0);
101
+ return Number.isFinite(createdAt) && createdAt > 0 ? createdAt : 0;
102
+ }
103
+
104
+ function isNewerThanCursor(item, state) {
105
+ const createdAt = messageCreatedAt(item);
106
+ const lastCreatedAt = Number(state.last_pushed_created_at || 0) || 0;
107
+ const messageId = String(item?.message_id || "").trim();
108
+ const lastMessageId = String(state.last_pushed_message_id || "").trim();
109
+ if (createdAt > lastCreatedAt) return true;
110
+ if (createdAt < lastCreatedAt) return false;
111
+ if (!createdAt) return !state.seen_ids.includes(messageId);
112
+ return Boolean(messageId) && messageId !== lastMessageId && !state.seen_ids.includes(messageId);
113
+ }
114
+
88
115
  function shouldWatchTopic(message) {
89
116
  if (!TOPIC_FILTERS.length) return true;
90
117
  return TOPIC_FILTERS.includes(String(message?.topic || "global").toLowerCase());
@@ -165,29 +192,60 @@ function dispatchToOwner(route, summary, message) {
165
192
  async function pollOnce(state) {
166
193
  const payload = await request(`/api/openclaw/bridge/messages?limit=${LIMIT}`);
167
194
  const items = Array.isArray(payload?.items) ? payload.items.slice().reverse() : [];
195
+ const candidates = [];
168
196
 
169
197
  for (const item of items) {
170
198
  const messageId = String(item?.message_id || "").trim();
171
199
  if (!messageId) continue;
172
- if (state.seen_ids.includes(messageId)) continue;
173
-
174
- state.seen_ids.push(messageId);
200
+ if (!isNewerThanCursor(item, state)) {
201
+ if (!state.seen_ids.includes(messageId)) {
202
+ state.seen_ids.push(messageId);
203
+ }
204
+ continue;
205
+ }
175
206
 
176
207
  if (!shouldWatchTopic(item)) {
208
+ state.seen_ids.push(messageId);
177
209
  if (VERBOSE) console.log(`skip topic: ${messageId}`);
178
210
  continue;
179
211
  }
180
212
 
181
213
  const route = scoreRoute(item);
182
214
  if (route === "ignore") {
215
+ state.seen_ids.push(messageId);
183
216
  if (VERBOSE) console.log(`ignore low-signal: ${messageId}`);
184
217
  continue;
185
218
  }
186
219
 
187
- const summary = summarizeForOwner(item);
188
- await dispatchToOwner(route, summary, item);
189
- state.pushed_at[messageId] = new Date().toISOString();
190
- if (VERBOSE) console.log(`pushed to owner: ${messageId}`);
220
+ candidates.push({ item, messageId, route, createdAt: messageCreatedAt(item) });
221
+ }
222
+
223
+ const selected = LATEST_ONLY
224
+ ? candidates.sort((left, right) => {
225
+ if (left.createdAt !== right.createdAt) return right.createdAt - left.createdAt;
226
+ return left.messageId.localeCompare(right.messageId);
227
+ })[0] || null
228
+ : null;
229
+
230
+ const toPush = LATEST_ONLY ? (selected ? [selected] : []) : candidates;
231
+
232
+ for (const candidate of toPush) {
233
+ const summary = summarizeForOwner(candidate.item);
234
+ await dispatchToOwner(candidate.route, summary, candidate.item);
235
+ state.pushed_at[candidate.messageId] = new Date().toISOString();
236
+ state.last_pushed_created_at = candidate.createdAt || Date.now();
237
+ state.last_pushed_message_id = candidate.messageId;
238
+ if (VERBOSE) console.log(`pushed to owner: ${candidate.messageId}`);
239
+ }
240
+
241
+ if (LATEST_ONLY && selected) {
242
+ for (const candidate of candidates) {
243
+ state.seen_ids.push(candidate.messageId);
244
+ }
245
+ } else {
246
+ for (const candidate of candidates) {
247
+ state.seen_ids.push(candidate.messageId);
248
+ }
191
249
  }
192
250
 
193
251
  trimState(state);
@@ -199,6 +257,7 @@ async function main() {
199
257
  if (VERBOSE) {
200
258
  console.log(`SilicaClaw owner push watching ${API_BASE}`);
201
259
  console.log(`State file: ${STATE_PATH}`);
260
+ console.log(`Latest-only mode: ${LATEST_ONLY ? "on" : "off"}`);
202
261
  }
203
262
 
204
263
  do {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silicaclaw/cli",
3
- "version": "2026.3.20-3",
3
+ "version": "2026.3.20-5",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -22,6 +22,10 @@ type RelayPeer = {
22
22
  last_seen_at: number;
23
23
  messages_seen: number;
24
24
  reconnect_attempts: number;
25
+ meta?: {
26
+ signal_queue_size?: number;
27
+ relay_queue_size?: number;
28
+ };
25
29
  };
26
30
  type RelayDiagnostics = {
27
31
  adapter: "relay-preview";
@@ -109,7 +109,6 @@ class RelayPreviewAdapter {
109
109
  try {
110
110
  await this.joinRoom("start");
111
111
  this.started = true;
112
- await this.refreshPeers();
113
112
  await this.pollOnce();
114
113
  this.scheduleNextPoll(this.pollIntervalMs);
115
114
  this.recordDiscovery("signaling_connected", { endpoint: this.activeEndpoint });
@@ -258,8 +257,10 @@ class RelayPreviewAdapter {
258
257
  const payload = await this.get(`/peers?room=${encodeURIComponent(this.room)}`);
259
258
  this.lastPeerRefreshAt = Date.now();
260
259
  this.stats.peers_refresh_succeeded += 1;
261
- const peerIds = Array.isArray(payload?.peers) ? payload.peers : [];
262
- this.updatePeersFromList(peerIds);
260
+ const peerItems = Array.isArray(payload?.peer_details) && payload.peer_details.length
261
+ ? payload.peer_details
262
+ : Array.isArray(payload?.peers) ? payload.peers : [];
263
+ this.updatePeersFromList(peerItems);
263
264
  }
264
265
  onEnvelope(envelope) {
265
266
  this.stats.received_total += 1;
@@ -340,9 +341,13 @@ class RelayPreviewAdapter {
340
341
  }
341
342
  async joinRoom(reason) {
342
343
  this.stats.join_attempted += 1;
343
- await this.post("/join", { room: this.room, peer_id: this.peerId });
344
+ const payload = await this.post("/join", { room: this.room, peer_id: this.peerId });
344
345
  this.lastJoinAt = Date.now();
345
346
  this.stats.join_succeeded += 1;
347
+ if (Array.isArray(payload?.peers)) {
348
+ this.updatePeersFromList(payload.peers);
349
+ this.lastPeerRefreshAt = this.lastJoinAt;
350
+ }
346
351
  this.recordDiscovery("join_ok", { endpoint: this.activeEndpoint, detail: reason });
347
352
  }
348
353
  async maybeRefreshJoin(reason) {
@@ -407,13 +412,38 @@ class RelayPreviewAdapter {
407
412
  throw new Error(errors.join(" | "));
408
413
  }
409
414
  updatePeersFromList(values) {
410
- const peerIds = values.map((value) => String(value || "").trim()).filter(Boolean);
415
+ const parsedPeers = [];
416
+ for (const value of values) {
417
+ if (typeof value === "string") {
418
+ const peerId = String(value || "").trim();
419
+ if (peerId) {
420
+ parsedPeers.push({ peer_id: peerId });
421
+ }
422
+ continue;
423
+ }
424
+ if (value && typeof value === "object") {
425
+ const raw = value;
426
+ const peerId = String(raw.peer_id || "").trim();
427
+ if (!peerId) {
428
+ continue;
429
+ }
430
+ parsedPeers.push({
431
+ peer_id: peerId,
432
+ meta: {
433
+ signal_queue_size: Number(raw.signal_queue_size ?? 0),
434
+ relay_queue_size: Number(raw.relay_queue_size ?? 0),
435
+ },
436
+ });
437
+ }
438
+ }
439
+ const peerIds = parsedPeers.map((peer) => peer.peer_id);
411
440
  if (!peerIds.includes(this.peerId)) {
412
441
  void this.joinRoom("self_missing_from_peers").catch(() => { });
413
442
  }
414
443
  const now = Date.now();
415
444
  const next = new Map();
416
- for (const peerId of peerIds) {
445
+ for (const peerInfo of parsedPeers) {
446
+ const peerId = peerInfo.peer_id;
417
447
  if (peerId === this.peerId)
418
448
  continue;
419
449
  const existing = this.peers.get(peerId);
@@ -427,6 +457,7 @@ class RelayPreviewAdapter {
427
457
  last_seen_at: now,
428
458
  messages_seen: existing?.messages_seen ?? 0,
429
459
  reconnect_attempts: existing?.reconnect_attempts ?? 0,
460
+ meta: peerInfo.meta || existing?.meta,
430
461
  });
431
462
  }
432
463
  for (const peerId of this.peers.keys()) {
@@ -34,6 +34,10 @@ type RelayPeer = {
34
34
  last_seen_at: number;
35
35
  messages_seen: number;
36
36
  reconnect_attempts: number;
37
+ meta?: {
38
+ signal_queue_size?: number;
39
+ relay_queue_size?: number;
40
+ };
37
41
  };
38
42
 
39
43
  type RelayDiagnostics = {
@@ -227,7 +231,6 @@ export class RelayPreviewAdapter implements NetworkAdapter {
227
231
  try {
228
232
  await this.joinRoom("start");
229
233
  this.started = true;
230
- await this.refreshPeers();
231
234
  await this.pollOnce();
232
235
  this.scheduleNextPoll(this.pollIntervalMs);
233
236
  this.recordDiscovery("signaling_connected", { endpoint: this.activeEndpoint });
@@ -375,8 +378,10 @@ export class RelayPreviewAdapter implements NetworkAdapter {
375
378
  const payload = await this.get(`/peers?room=${encodeURIComponent(this.room)}`);
376
379
  this.lastPeerRefreshAt = Date.now();
377
380
  this.stats.peers_refresh_succeeded += 1;
378
- const peerIds = Array.isArray(payload?.peers) ? payload.peers : [];
379
- this.updatePeersFromList(peerIds);
381
+ const peerItems = Array.isArray(payload?.peer_details) && payload.peer_details.length
382
+ ? payload.peer_details
383
+ : Array.isArray(payload?.peers) ? payload.peers : [];
384
+ this.updatePeersFromList(peerItems);
380
385
  }
381
386
 
382
387
  private onEnvelope(envelope: unknown): void {
@@ -457,9 +462,13 @@ export class RelayPreviewAdapter implements NetworkAdapter {
457
462
 
458
463
  private async joinRoom(reason: string): Promise<void> {
459
464
  this.stats.join_attempted += 1;
460
- await this.post("/join", { room: this.room, peer_id: this.peerId });
465
+ const payload = await this.post("/join", { room: this.room, peer_id: this.peerId });
461
466
  this.lastJoinAt = Date.now();
462
467
  this.stats.join_succeeded += 1;
468
+ if (Array.isArray(payload?.peers)) {
469
+ this.updatePeersFromList(payload.peers);
470
+ this.lastPeerRefreshAt = this.lastJoinAt;
471
+ }
463
472
  this.recordDiscovery("join_ok", { endpoint: this.activeEndpoint, detail: reason });
464
473
  }
465
474
 
@@ -528,13 +537,38 @@ export class RelayPreviewAdapter implements NetworkAdapter {
528
537
  }
529
538
 
530
539
  private updatePeersFromList(values: unknown[]): void {
531
- const peerIds = values.map((value) => String(value || "").trim()).filter(Boolean);
540
+ const parsedPeers: Array<{ peer_id: string; meta?: RelayPeer["meta"] }> = [];
541
+ for (const value of values) {
542
+ if (typeof value === "string") {
543
+ const peerId = String(value || "").trim();
544
+ if (peerId) {
545
+ parsedPeers.push({ peer_id: peerId });
546
+ }
547
+ continue;
548
+ }
549
+ if (value && typeof value === "object") {
550
+ const raw = value as Record<string, unknown>;
551
+ const peerId = String(raw.peer_id || "").trim();
552
+ if (!peerId) {
553
+ continue;
554
+ }
555
+ parsedPeers.push({
556
+ peer_id: peerId,
557
+ meta: {
558
+ signal_queue_size: Number(raw.signal_queue_size ?? 0),
559
+ relay_queue_size: Number(raw.relay_queue_size ?? 0),
560
+ },
561
+ });
562
+ }
563
+ }
564
+ const peerIds = parsedPeers.map((peer) => peer.peer_id);
532
565
  if (!peerIds.includes(this.peerId)) {
533
566
  void this.joinRoom("self_missing_from_peers").catch(() => {});
534
567
  }
535
568
  const now = Date.now();
536
569
  const next = new Map<string, RelayPeer>();
537
- for (const peerId of peerIds) {
570
+ for (const peerInfo of parsedPeers) {
571
+ const peerId = peerInfo.peer_id;
538
572
  if (peerId === this.peerId) continue;
539
573
  const existing = this.peers.get(peerId);
540
574
  if (!existing) {
@@ -547,6 +581,7 @@ export class RelayPreviewAdapter implements NetworkAdapter {
547
581
  last_seen_at: now,
548
582
  messages_seen: existing?.messages_seen ?? 0,
549
583
  reconnect_attempts: existing?.reconnect_attempts ?? 0,
584
+ meta: peerInfo.meta || existing?.meta,
550
585
  });
551
586
  }
552
587
  for (const peerId of this.peers.keys()) {
@@ -224,10 +224,12 @@ function shellInitTargets() {
224
224
  shell.endsWith("/bash") ||
225
225
  process.env.BASH_VERSION ||
226
226
  existsSync(resolve(home, ".bashrc")) ||
227
- existsSync(resolve(home, ".bash_profile"))
227
+ existsSync(resolve(home, ".bash_profile")) ||
228
+ existsSync(resolve(home, ".profile"))
228
229
  ) {
229
230
  add(resolve(home, ".bashrc"));
230
231
  add(resolve(home, ".bash_profile"));
232
+ add(resolve(home, ".profile"));
231
233
  }
232
234
 
233
235
  if (targets.length === 0) {
@@ -280,6 +282,7 @@ function installPersistentCommand(specifier = readPackageVersion()) {
280
282
  kv("Command", "silicaclaw");
281
283
  console.log("");
282
284
  kv("Activate", `source "${envFile}"`);
285
+ kv("Current shell", "run Activate now if `silicaclaw` is not found yet");
283
286
  if (configuredFiles.length > 0) {
284
287
  kv("Startup", "configured");
285
288
  } else {
@@ -74,6 +74,11 @@ function readJson(file) {
74
74
  }
75
75
  }
76
76
 
77
+ function readPackageVersionFrom(dir) {
78
+ const pkg = readJson(resolve(dir, "package.json"));
79
+ return String(pkg?.version || "latest").trim() || "latest";
80
+ }
81
+
77
82
  function isSilicaClawDir(dir) {
78
83
  const pkgPath = join(dir, "package.json");
79
84
  if (!existsSync(pkgPath)) return false;
@@ -144,6 +149,107 @@ function ensureStateDir() {
144
149
  mkdirSync(STATE_DIR, { recursive: true });
145
150
  }
146
151
 
152
+ function userShimDir() {
153
+ return join(homedir(), ".silicaclaw", "bin");
154
+ }
155
+
156
+ function userShimPath() {
157
+ return join(userShimDir(), "silicaclaw");
158
+ }
159
+
160
+ function userEnvFile() {
161
+ return join(homedir(), ".silicaclaw", "env.sh");
162
+ }
163
+
164
+ function userNpmCacheDir() {
165
+ return join(homedir(), ".silicaclaw", "npm-cache");
166
+ }
167
+
168
+ function preferredShellRcFile() {
169
+ const shell = String(process.env.SHELL || "");
170
+ if (shell.endsWith("/zsh")) return resolve(homedir(), ".zshrc");
171
+ if (shell.endsWith("/bash")) return resolve(homedir(), ".bashrc");
172
+ if (process.env.ZSH_VERSION) return resolve(homedir(), ".zshrc");
173
+ return resolve(homedir(), ".bashrc");
174
+ }
175
+
176
+ function shellInitTargets() {
177
+ const home = homedir();
178
+ const shell = String(process.env.SHELL || "");
179
+ const targets = [];
180
+ const add = (filePath) => {
181
+ if (!targets.includes(filePath)) targets.push(filePath);
182
+ };
183
+
184
+ if (shell.endsWith("/zsh") || process.env.ZSH_VERSION || existsSync(resolve(home, ".zshrc"))) {
185
+ add(resolve(home, ".zshrc"));
186
+ }
187
+
188
+ if (
189
+ shell.endsWith("/bash") ||
190
+ process.env.BASH_VERSION ||
191
+ existsSync(resolve(home, ".bashrc")) ||
192
+ existsSync(resolve(home, ".bash_profile")) ||
193
+ existsSync(resolve(home, ".profile"))
194
+ ) {
195
+ add(resolve(home, ".bashrc"));
196
+ add(resolve(home, ".bash_profile"));
197
+ add(resolve(home, ".profile"));
198
+ }
199
+
200
+ if (targets.length === 0) {
201
+ add(preferredShellRcFile());
202
+ }
203
+ return targets;
204
+ }
205
+
206
+ function ensureLineInFile(filePath, block) {
207
+ try {
208
+ const current = existsSync(filePath) ? readFileSync(filePath, "utf8") : "";
209
+ if (current.includes(block.trim())) return true;
210
+ const next = `${current.replace(/\s*$/, "")}\n\n${block}\n`;
211
+ mkdirSync(dirname(filePath), { recursive: true });
212
+ writeFileSync(filePath, next, "utf8");
213
+ return true;
214
+ } catch {
215
+ return false;
216
+ }
217
+ }
218
+
219
+ function shimScriptText(specifier = "latest") {
220
+ return [
221
+ "#!/usr/bin/env bash",
222
+ "set -euo pipefail",
223
+ 'export npm_config_cache="${npm_config_cache:-$HOME/.silicaclaw/npm-cache}"',
224
+ `exec npx -y @silicaclaw/cli@${specifier} "$@"`,
225
+ "",
226
+ ].join("\n");
227
+ }
228
+
229
+ function ensureUserCommandInstalled() {
230
+ const version = readPackageVersionFrom(APP_DIR);
231
+ const envBlock = [
232
+ "#!/usr/bin/env bash",
233
+ 'export PATH="$HOME/.silicaclaw/bin:$PATH"',
234
+ 'export npm_config_cache="$HOME/.silicaclaw/npm-cache"',
235
+ "",
236
+ ].join("\n");
237
+ const rcBlock = [
238
+ "# >>> silicaclaw >>>",
239
+ '[ -f "$HOME/.silicaclaw/env.sh" ] && . "$HOME/.silicaclaw/env.sh"',
240
+ "# <<< silicaclaw <<<",
241
+ ].join("\n");
242
+
243
+ mkdirSync(userShimDir(), { recursive: true });
244
+ mkdirSync(userNpmCacheDir(), { recursive: true });
245
+ writeFileSync(userEnvFile(), envBlock, { encoding: "utf8", mode: 0o755 });
246
+ writeFileSync(userShimPath(), shimScriptText(version), { encoding: "utf8", mode: 0o755 });
247
+
248
+ for (const filePath of shellInitTargets()) {
249
+ ensureLineInFile(filePath, rcBlock);
250
+ }
251
+ }
252
+
147
253
  function ensureLaunchAgentsDir() {
148
254
  mkdirSync(LAUNCH_AGENTS_DIR, { recursive: true });
149
255
  }
@@ -797,6 +903,7 @@ async function stopAll() {
797
903
 
798
904
  async function startAll() {
799
905
  ensureStateDir();
906
+ ensureUserCommandInstalled();
800
907
 
801
908
  const mode = parseMode(parseFlag("mode", process.env.NETWORK_MODE || DEFAULT_NETWORK_MODE));
802
909
  const adapter = adapterForMode(mode);
@@ -923,6 +1030,7 @@ async function startAll() {
923
1030
  }
924
1031
 
925
1032
  async function restartAll() {
1033
+ ensureUserCommandInstalled();
926
1034
  if (!isLaunchdPlatform()) {
927
1035
  await stopAll();
928
1036
  await startAll();