@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.
- package/CHANGELOG.md +12 -0
- package/INSTALL.md +2 -2
- package/README.md +2 -2
- package/VERSION +1 -1
- package/apps/local-console/dist/apps/local-console/src/server.d.ts +39 -0
- package/apps/local-console/dist/apps/local-console/src/server.js +229 -12
- package/apps/local-console/dist/packages/network/src/relayPreview.d.ts +4 -0
- package/apps/local-console/dist/packages/network/src/relayPreview.js +37 -6
- package/apps/local-console/public/app/app.js +293 -2
- package/apps/local-console/public/app/network.js +144 -32
- package/apps/local-console/public/app/overview.js +43 -15
- package/apps/local-console/public/app/social.js +135 -53
- package/apps/local-console/public/app/styles.css +86 -0
- package/apps/local-console/public/app/template.js +7 -1
- package/apps/local-console/public/app/translations.js +44 -0
- package/apps/local-console/src/server.ts +262 -14
- package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.d.ts +4 -0
- package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.js +37 -6
- package/node_modules/@silicaclaw/network/src/relayPreview.ts +41 -6
- package/openclaw-skills/silicaclaw-broadcast/VERSION +1 -1
- package/openclaw-skills/silicaclaw-broadcast/manifest.json +1 -1
- package/openclaw-skills/silicaclaw-owner-push/VERSION +1 -1
- package/openclaw-skills/silicaclaw-owner-push/manifest.json +1 -1
- package/openclaw-skills/silicaclaw-owner-push/references/runtime-setup.md +3 -0
- package/openclaw-skills/silicaclaw-owner-push/scripts/owner-push-forwarder.mjs +67 -8
- package/package.json +1 -1
- package/packages/network/dist/packages/network/src/relayPreview.d.ts +4 -0
- package/packages/network/dist/packages/network/src/relayPreview.js +37 -6
- package/packages/network/src/relayPreview.ts +41 -6
- package/scripts/silicaclaw-cli.mjs +4 -1
- package/scripts/silicaclaw-gateway.mjs +108 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
2026.3.20-beta.
|
|
1
|
+
2026.3.20-beta.5
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "silicaclaw-broadcast",
|
|
3
|
-
"version": "2026.3.20-beta.
|
|
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
|
+
2026.3.20-beta.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "silicaclaw-owner-push",
|
|
3
|
-
"version": "2026.3.20-beta.
|
|
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
|
-
|
|
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
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
@@ -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
|
|
262
|
-
|
|
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
|
|
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
|
|
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
|
|
379
|
-
|
|
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
|
|
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
|
|
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();
|