@silicaclaw/cli 2026.3.20-3 → 2026.3.20-4
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 +6 -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 +34 -0
- package/apps/local-console/dist/apps/local-console/src/server.js +185 -0
- package/apps/local-console/public/app/app.js +249 -1
- package/apps/local-console/public/app/network.js +111 -30
- 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 +51 -0
- package/apps/local-console/public/app/template.js +6 -1
- package/apps/local-console/public/app/translations.js +32 -0
- package/apps/local-console/src/server.ts +209 -1
- 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/scripts/silicaclaw-cli.mjs +4 -1
- package/scripts/silicaclaw-gateway.mjs +108 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import express, { NextFunction, Request, Response } from "express";
|
|
2
2
|
import cors from "cors";
|
|
3
|
-
import { execFile, spawnSync } from "child_process";
|
|
3
|
+
import { execFile, spawn, spawnSync } from "child_process";
|
|
4
4
|
import { resolve } from "path";
|
|
5
5
|
import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "fs";
|
|
6
6
|
import { createHash } from "crypto";
|
|
@@ -90,6 +90,7 @@ const DEFAULT_BRIDGE_API_BASE = defaults.bridge.api_base;
|
|
|
90
90
|
const OPENCLAW_GATEWAY_PORT = defaults.ports.openclaw_gateway;
|
|
91
91
|
const OPENCLAW_GATEWAY_URL = `http://${OPENCLAW_GATEWAY_HOST}:${OPENCLAW_GATEWAY_PORT}/`;
|
|
92
92
|
const NETWORK_PEER_REMOVE_AFTER_MS = Number(process.env.NETWORK_PEER_REMOVE_AFTER_MS || 180_000);
|
|
93
|
+
const DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT = Number(process.env.DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT || 1000);
|
|
93
94
|
const NETWORK_UDP_BIND_ADDRESS = process.env.NETWORK_UDP_BIND_ADDRESS || "0.0.0.0";
|
|
94
95
|
const NETWORK_UDP_BROADCAST_ADDRESS = process.env.NETWORK_UDP_BROADCAST_ADDRESS || "255.255.255.255";
|
|
95
96
|
const NETWORK_PEER_ID = process.env.NETWORK_PEER_ID;
|
|
@@ -157,6 +158,10 @@ function normalizeVersionText(value: unknown): string {
|
|
|
157
158
|
return text.startsWith("v") ? text.slice(1) : text;
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
function formatBytesToMiB(value: number): number {
|
|
162
|
+
return Math.round((value / (1024 * 1024)) * 10) / 10;
|
|
163
|
+
}
|
|
164
|
+
|
|
160
165
|
function tokenizeVersion(value: unknown): Array<number | string> {
|
|
161
166
|
return normalizeVersionText(value)
|
|
162
167
|
.split(/[^0-9A-Za-z]+/)
|
|
@@ -186,6 +191,10 @@ function compareVersionTokens(left: unknown, right: unknown): number {
|
|
|
186
191
|
return 0;
|
|
187
192
|
}
|
|
188
193
|
|
|
194
|
+
function userNpmCacheDir(): string {
|
|
195
|
+
return resolve(homedir(), ".silicaclaw", "npm-cache");
|
|
196
|
+
}
|
|
197
|
+
|
|
189
198
|
function resolveWorkspaceRoot(cwd = process.cwd()): string {
|
|
190
199
|
if (existsSync(resolve(cwd, "apps", "local-console", "package.json"))) {
|
|
191
200
|
return cwd;
|
|
@@ -1201,6 +1210,7 @@ export class LocalNodeService {
|
|
|
1201
1210
|
const relayCapable = this.adapterMode === "webrtc-preview" || this.adapterMode === "relay-preview";
|
|
1202
1211
|
const peers: Array<{ status?: string }> = diagnostics?.peers?.items ?? [];
|
|
1203
1212
|
const online = peers.filter((peer: { status?: string }) => peer.status === "online").length;
|
|
1213
|
+
const memory = process.memoryUsage();
|
|
1204
1214
|
|
|
1205
1215
|
return {
|
|
1206
1216
|
adapter: this.adapterMode,
|
|
@@ -1225,6 +1235,23 @@ export class LocalNodeService {
|
|
|
1225
1235
|
adapter_stats: diagnostics?.stats ?? null,
|
|
1226
1236
|
adapter_transport_stats: diagnostics?.transport_stats ?? null,
|
|
1227
1237
|
adapter_discovery_stats: diagnostics?.discovery_stats ?? null,
|
|
1238
|
+
runtime_diagnostics: {
|
|
1239
|
+
memory_mib: {
|
|
1240
|
+
rss: formatBytesToMiB(memory.rss),
|
|
1241
|
+
heap_used: formatBytesToMiB(memory.heapUsed),
|
|
1242
|
+
heap_total: formatBytesToMiB(memory.heapTotal),
|
|
1243
|
+
external: formatBytesToMiB(memory.external),
|
|
1244
|
+
},
|
|
1245
|
+
directory: {
|
|
1246
|
+
profile_count: Object.keys(this.directory.profiles).length,
|
|
1247
|
+
presence_count: Object.keys(this.directory.presence).length,
|
|
1248
|
+
index_key_count: Object.keys(this.directory.index).length,
|
|
1249
|
+
},
|
|
1250
|
+
social: {
|
|
1251
|
+
message_count: this.socialMessages.length,
|
|
1252
|
+
observation_count: this.socialMessageObservations.length,
|
|
1253
|
+
},
|
|
1254
|
+
},
|
|
1228
1255
|
adapter_diagnostics_summary: relayCapable || diagnostics
|
|
1229
1256
|
? {
|
|
1230
1257
|
started: this.networkStarted,
|
|
@@ -1341,6 +1368,88 @@ export class LocalNodeService {
|
|
|
1341
1368
|
};
|
|
1342
1369
|
}
|
|
1343
1370
|
|
|
1371
|
+
getAppUpdateStatus() {
|
|
1372
|
+
const currentVersion = normalizeVersionText(this.appVersion) || "unknown";
|
|
1373
|
+
const fallback = {
|
|
1374
|
+
current_version: currentVersion,
|
|
1375
|
+
latest_version: currentVersion,
|
|
1376
|
+
update_available: false,
|
|
1377
|
+
channel: "latest",
|
|
1378
|
+
platform: process.platform,
|
|
1379
|
+
checked_at: Date.now(),
|
|
1380
|
+
can_update: true,
|
|
1381
|
+
check_error: null as string | null,
|
|
1382
|
+
};
|
|
1383
|
+
try {
|
|
1384
|
+
const result = spawnSync("npm", ["view", "@silicaclaw/cli", "dist-tags", "--json"], {
|
|
1385
|
+
cwd: this.projectRoot,
|
|
1386
|
+
encoding: "utf8",
|
|
1387
|
+
env: {
|
|
1388
|
+
...process.env,
|
|
1389
|
+
SILICACLAW_WORKSPACE_DIR: this.projectRoot,
|
|
1390
|
+
SILICACLAW_APP_DIR: this.workspaceRoot,
|
|
1391
|
+
npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
|
|
1392
|
+
},
|
|
1393
|
+
});
|
|
1394
|
+
if ((result.status ?? 1) !== 0) {
|
|
1395
|
+
return {
|
|
1396
|
+
...fallback,
|
|
1397
|
+
check_error: String(result.stderr || result.stdout || "npm view failed").trim() || "npm view failed",
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
const tags = JSON.parse(String(result.stdout || "{}").trim() || "{}") as { latest?: string };
|
|
1401
|
+
const latestVersion = normalizeVersionText(tags.latest || currentVersion) || currentVersion;
|
|
1402
|
+
return {
|
|
1403
|
+
...fallback,
|
|
1404
|
+
latest_version: latestVersion,
|
|
1405
|
+
update_available: compareVersionTokens(latestVersion, currentVersion) > 0,
|
|
1406
|
+
};
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
return {
|
|
1409
|
+
...fallback,
|
|
1410
|
+
check_error: error instanceof Error ? error.message : String(error),
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
startAppUpdate(): { started: boolean; target_version: string; platform: string; reason?: string } {
|
|
1416
|
+
const status = this.getAppUpdateStatus();
|
|
1417
|
+
if (!status.update_available || !status.latest_version) {
|
|
1418
|
+
return {
|
|
1419
|
+
started: false,
|
|
1420
|
+
target_version: status.latest_version || status.current_version,
|
|
1421
|
+
platform: process.platform,
|
|
1422
|
+
reason: status.check_error || "already_current",
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
const scriptPath = resolve(this.workspaceRoot, "scripts", "silicaclaw-cli.mjs");
|
|
1426
|
+
if (!existsSync(scriptPath)) {
|
|
1427
|
+
return {
|
|
1428
|
+
started: false,
|
|
1429
|
+
target_version: status.latest_version,
|
|
1430
|
+
platform: process.platform,
|
|
1431
|
+
reason: "missing_cli_script",
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
const child = spawn(process.execPath, [scriptPath, "update"], {
|
|
1435
|
+
cwd: this.projectRoot,
|
|
1436
|
+
detached: true,
|
|
1437
|
+
stdio: "ignore",
|
|
1438
|
+
env: {
|
|
1439
|
+
...process.env,
|
|
1440
|
+
SILICACLAW_WORKSPACE_DIR: this.projectRoot,
|
|
1441
|
+
SILICACLAW_APP_DIR: this.workspaceRoot,
|
|
1442
|
+
npm_config_cache: process.env.npm_config_cache || userNpmCacheDir(),
|
|
1443
|
+
},
|
|
1444
|
+
});
|
|
1445
|
+
child.unref();
|
|
1446
|
+
return {
|
|
1447
|
+
started: true,
|
|
1448
|
+
target_version: status.latest_version,
|
|
1449
|
+
platform: process.platform,
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1344
1453
|
getIntegrationSummary() {
|
|
1345
1454
|
const status = this.getIntegrationStatus();
|
|
1346
1455
|
const runtimeGenerated = Boolean(this.socialRuntime && this.socialRuntime.last_loaded_at > 0);
|
|
@@ -2670,9 +2779,66 @@ export class LocalNodeService {
|
|
|
2670
2779
|
this.networkReconnectDelayMs = Math.min(30_000, Math.max(5_000, Math.floor(delayMs * 1.5)));
|
|
2671
2780
|
}
|
|
2672
2781
|
|
|
2782
|
+
private pruneRemoteProfilesInMemory(now = Date.now()): number {
|
|
2783
|
+
if (!Number.isFinite(DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) || DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT <= 0) {
|
|
2784
|
+
return 0;
|
|
2785
|
+
}
|
|
2786
|
+
const selfAgentId = this.profile?.agent_id || this.identity?.agent_id || "";
|
|
2787
|
+
const remoteProfiles = Object.values(this.directory.profiles).filter((profile) => profile.agent_id !== selfAgentId);
|
|
2788
|
+
if (remoteProfiles.length <= DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT) {
|
|
2789
|
+
return 0;
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
const onlineRemoteProfiles = remoteProfiles.filter((profile) =>
|
|
2793
|
+
isAgentOnline(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS)
|
|
2794
|
+
);
|
|
2795
|
+
const offlineRemoteProfiles = remoteProfiles
|
|
2796
|
+
.filter((profile) => !isAgentOnline(this.directory.presence[profile.agent_id], now, PRESENCE_TTL_MS))
|
|
2797
|
+
.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
|
|
2798
|
+
|
|
2799
|
+
const keepOfflineCount = Math.max(0, DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT - onlineRemoteProfiles.length);
|
|
2800
|
+
const keptRemoteProfiles = [
|
|
2801
|
+
...onlineRemoteProfiles,
|
|
2802
|
+
...offlineRemoteProfiles.slice(0, keepOfflineCount),
|
|
2803
|
+
];
|
|
2804
|
+
const keptRemoteIds = new Set(keptRemoteProfiles.map((profile) => profile.agent_id));
|
|
2805
|
+
const removedIds = remoteProfiles
|
|
2806
|
+
.map((profile) => profile.agent_id)
|
|
2807
|
+
.filter((agentId) => !keptRemoteIds.has(agentId));
|
|
2808
|
+
if (removedIds.length === 0) {
|
|
2809
|
+
return 0;
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
const next = createEmptyDirectoryState();
|
|
2813
|
+
const selfProfile = selfAgentId ? this.directory.profiles[selfAgentId] : null;
|
|
2814
|
+
if (selfProfile) {
|
|
2815
|
+
next.profiles[selfAgentId] = selfProfile;
|
|
2816
|
+
const selfPresence = this.directory.presence[selfAgentId];
|
|
2817
|
+
if (typeof selfPresence === "number" && Number.isFinite(selfPresence)) {
|
|
2818
|
+
next.presence[selfAgentId] = selfPresence;
|
|
2819
|
+
}
|
|
2820
|
+
const rebuilt = rebuildIndexForProfile(next, selfProfile);
|
|
2821
|
+
next.index = rebuilt.index;
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
for (const profile of keptRemoteProfiles) {
|
|
2825
|
+
next.profiles[profile.agent_id] = profile;
|
|
2826
|
+
const seenAt = this.directory.presence[profile.agent_id];
|
|
2827
|
+
if (typeof seenAt === "number" && Number.isFinite(seenAt)) {
|
|
2828
|
+
next.presence[profile.agent_id] = seenAt;
|
|
2829
|
+
}
|
|
2830
|
+
const rebuilt = rebuildIndexForProfile(next, profile);
|
|
2831
|
+
next.index = rebuilt.index;
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
this.directory = dedupeIndex(next);
|
|
2835
|
+
return removedIds.length;
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2673
2838
|
private compactCacheInMemory(): number {
|
|
2674
2839
|
const cleaned = cleanupExpiredPresence(this.directory, Date.now(), PRESENCE_TTL_MS);
|
|
2675
2840
|
this.directory = dedupeIndex(cleaned.state);
|
|
2841
|
+
this.pruneRemoteProfilesInMemory();
|
|
2676
2842
|
return cleaned.removed;
|
|
2677
2843
|
}
|
|
2678
2844
|
|
|
@@ -3411,6 +3577,48 @@ export async function main() {
|
|
|
3411
3577
|
sendOk(res, node.getRuntimePaths());
|
|
3412
3578
|
});
|
|
3413
3579
|
|
|
3580
|
+
app.get("/api/app/update-status", (_req, res) => {
|
|
3581
|
+
sendOk(res, node.getAppUpdateStatus());
|
|
3582
|
+
});
|
|
3583
|
+
|
|
3584
|
+
app.post(
|
|
3585
|
+
"/api/app/update",
|
|
3586
|
+
asyncRoute(async (_req, res) => {
|
|
3587
|
+
const status = node.getAppUpdateStatus();
|
|
3588
|
+
if (!status.update_available || !status.latest_version) {
|
|
3589
|
+
sendOk(
|
|
3590
|
+
res,
|
|
3591
|
+
{
|
|
3592
|
+
started: false,
|
|
3593
|
+
current_version: status.current_version,
|
|
3594
|
+
latest_version: status.latest_version,
|
|
3595
|
+
platform: status.platform,
|
|
3596
|
+
reason: status.check_error || "already_current",
|
|
3597
|
+
},
|
|
3598
|
+
{ message: "Already on the latest version" }
|
|
3599
|
+
);
|
|
3600
|
+
return;
|
|
3601
|
+
}
|
|
3602
|
+
sendOk(
|
|
3603
|
+
res,
|
|
3604
|
+
{
|
|
3605
|
+
started: true,
|
|
3606
|
+
current_version: status.current_version,
|
|
3607
|
+
target_version: status.latest_version,
|
|
3608
|
+
platform: status.platform,
|
|
3609
|
+
},
|
|
3610
|
+
{ message: `Updating to ${status.latest_version}` }
|
|
3611
|
+
);
|
|
3612
|
+
setTimeout(() => {
|
|
3613
|
+
try {
|
|
3614
|
+
node.startAppUpdate();
|
|
3615
|
+
} catch {
|
|
3616
|
+
// best effort after response has been sent
|
|
3617
|
+
}
|
|
3618
|
+
}, 150);
|
|
3619
|
+
})
|
|
3620
|
+
);
|
|
3621
|
+
|
|
3414
3622
|
app.put(
|
|
3415
3623
|
"/api/profile",
|
|
3416
3624
|
asyncRoute(async (req, res) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
2026.3.20-beta.
|
|
1
|
+
2026.3.20-beta.4
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "silicaclaw-broadcast",
|
|
3
|
-
"version": "2026.3.20-beta.
|
|
3
|
+
"version": "2026.3.20-beta.4",
|
|
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
|
@@ -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();
|