@silicaclaw/cli 1.0.0-beta.0

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 (77) hide show
  1. package/ARCHITECTURE.md +137 -0
  2. package/CHANGELOG.md +411 -0
  3. package/DEMO_GUIDE.md +89 -0
  4. package/INSTALL.md +156 -0
  5. package/README.md +244 -0
  6. package/RELEASE_NOTES_v1.0.md +65 -0
  7. package/ROADMAP.md +48 -0
  8. package/SOCIAL_MD_SPEC.md +122 -0
  9. package/VERSION +1 -0
  10. package/apps/local-console/package.json +23 -0
  11. package/apps/local-console/public/assets/README.md +5 -0
  12. package/apps/local-console/public/assets/silicaclaw-logo.png +0 -0
  13. package/apps/local-console/public/index.html +1602 -0
  14. package/apps/local-console/src/server.ts +1656 -0
  15. package/apps/local-console/src/socialRoutes.ts +90 -0
  16. package/apps/local-console/tsconfig.json +7 -0
  17. package/apps/public-explorer/package.json +20 -0
  18. package/apps/public-explorer/public/assets/README.md +5 -0
  19. package/apps/public-explorer/public/assets/silicaclaw-logo.png +0 -0
  20. package/apps/public-explorer/public/index.html +483 -0
  21. package/apps/public-explorer/src/server.ts +32 -0
  22. package/apps/public-explorer/tsconfig.json +7 -0
  23. package/docs/QUICK_START.md +48 -0
  24. package/docs/assets/README.md +8 -0
  25. package/docs/assets/banner.svg +25 -0
  26. package/docs/assets/silicaclaw-logo.png +0 -0
  27. package/docs/assets/silicaclaw-og.png +0 -0
  28. package/docs/release/GITHUB_RELEASE_v1.0-beta.md +143 -0
  29. package/docs/screenshots/README.md +8 -0
  30. package/docs/screenshots/v0.3.1-explorer-search.svg +9 -0
  31. package/docs/screenshots/v0.3.1-machine-a-network.svg +9 -0
  32. package/docs/screenshots/v0.3.1-machine-b-peers.svg +9 -0
  33. package/docs/screenshots/v0.3.1-stale-transition.svg +9 -0
  34. package/openclaw.social.md.example +28 -0
  35. package/package.json +64 -0
  36. package/packages/core/package.json +13 -0
  37. package/packages/core/src/crypto.ts +55 -0
  38. package/packages/core/src/directory.ts +171 -0
  39. package/packages/core/src/identity.ts +14 -0
  40. package/packages/core/src/index.ts +11 -0
  41. package/packages/core/src/indexing.ts +42 -0
  42. package/packages/core/src/presence.ts +24 -0
  43. package/packages/core/src/profile.ts +39 -0
  44. package/packages/core/src/publicProfileSummary.ts +180 -0
  45. package/packages/core/src/socialConfig.ts +440 -0
  46. package/packages/core/src/socialResolver.ts +281 -0
  47. package/packages/core/src/socialTemplate.ts +97 -0
  48. package/packages/core/src/types.ts +43 -0
  49. package/packages/core/tsconfig.json +7 -0
  50. package/packages/network/package.json +10 -0
  51. package/packages/network/src/abstractions/messageEnvelope.ts +80 -0
  52. package/packages/network/src/abstractions/peerDiscovery.ts +49 -0
  53. package/packages/network/src/abstractions/topicCodec.ts +4 -0
  54. package/packages/network/src/abstractions/transport.ts +40 -0
  55. package/packages/network/src/codec/jsonMessageEnvelopeCodec.ts +22 -0
  56. package/packages/network/src/codec/jsonTopicCodec.ts +11 -0
  57. package/packages/network/src/discovery/heartbeatPeerDiscovery.ts +173 -0
  58. package/packages/network/src/index.ts +16 -0
  59. package/packages/network/src/localEventBus.ts +61 -0
  60. package/packages/network/src/mock.ts +27 -0
  61. package/packages/network/src/realPreview.ts +436 -0
  62. package/packages/network/src/transport/udpLanBroadcastTransport.ts +173 -0
  63. package/packages/network/src/types.ts +6 -0
  64. package/packages/network/src/webrtcPreview.ts +1052 -0
  65. package/packages/network/tsconfig.json +7 -0
  66. package/packages/storage/package.json +13 -0
  67. package/packages/storage/src/index.ts +3 -0
  68. package/packages/storage/src/jsonRepo.ts +25 -0
  69. package/packages/storage/src/repos.ts +46 -0
  70. package/packages/storage/src/socialRuntimeRepo.ts +51 -0
  71. package/packages/storage/tsconfig.json +7 -0
  72. package/scripts/functional-check.mjs +165 -0
  73. package/scripts/install-logo.sh +53 -0
  74. package/scripts/quickstart.sh +144 -0
  75. package/scripts/silicaclaw-cli.mjs +88 -0
  76. package/scripts/webrtc-signaling-server.mjs +249 -0
  77. package/social.md.example +30 -0
@@ -0,0 +1,97 @@
1
+ import { SocialRuntimeConfig } from "./socialConfig";
2
+
3
+ function asBool(value: unknown, fallback: boolean): boolean {
4
+ return typeof value === "boolean" ? value : fallback;
5
+ }
6
+
7
+ function asString(value: unknown, fallback: string): string {
8
+ return typeof value === "string" ? value : fallback;
9
+ }
10
+
11
+ function asStringArray(value: unknown, fallback: string[]): string[] {
12
+ if (!Array.isArray(value)) return fallback;
13
+ return value.map((item) => String(item).trim()).filter(Boolean);
14
+ }
15
+
16
+ function yamlString(input: string): string {
17
+ return JSON.stringify(input ?? "");
18
+ }
19
+
20
+ function yamlStringList(values: string[], indent = " "): string {
21
+ if (!values.length) {
22
+ return `${indent}[]`;
23
+ }
24
+ return values.map((value) => `${indent}- ${yamlString(value)}`).join("\n");
25
+ }
26
+
27
+ export function generateSocialMdTemplate(runtimeConfig: SocialRuntimeConfig | null | undefined): string {
28
+ const enabled = asBool(runtimeConfig?.enabled, true);
29
+ const publicEnabled = asBool(runtimeConfig?.public_enabled, false);
30
+ const profile = runtimeConfig?.resolved_profile ?? null;
31
+ const network = runtimeConfig?.resolved_network ?? null;
32
+ const discovery = runtimeConfig?.resolved_discovery ?? null;
33
+ const visibility = runtimeConfig?.visibility ?? null;
34
+ const openclaw = runtimeConfig?.openclaw ?? null;
35
+
36
+ const displayName = asString(profile?.display_name, "");
37
+ const bio = asString(profile?.bio, "");
38
+ const tags = asStringArray(profile?.tags, ["openclaw", "local-first"]);
39
+ const mode =
40
+ network?.mode === "local" || network?.mode === "lan" || network?.mode === "global-preview"
41
+ ? network.mode
42
+ : "lan";
43
+
44
+ const discoverable = asBool(discovery?.discoverable, true);
45
+ const allowProfileBroadcast = asBool(discovery?.allow_profile_broadcast, true);
46
+ const allowPresenceBroadcast = asBool(discovery?.allow_presence_broadcast, true);
47
+
48
+ const showDisplayName = asBool(visibility?.show_display_name, true);
49
+ const showBio = asBool(visibility?.show_bio, true);
50
+ const showTags = asBool(visibility?.show_tags, true);
51
+ const showAgentId = asBool(visibility?.show_agent_id, true);
52
+ const showLastSeen = asBool(visibility?.show_last_seen, true);
53
+ const showCapabilitiesSummary = asBool(visibility?.show_capabilities_summary, true);
54
+
55
+ const bindExistingIdentity = asBool(openclaw?.bind_existing_identity, true);
56
+ const useOpenClawProfile = asBool(openclaw?.use_openclaw_profile_if_available, true);
57
+
58
+ return `---
59
+ enabled: ${enabled}
60
+ public_enabled: ${publicEnabled}
61
+
62
+ identity:
63
+ display_name: ${yamlString(displayName)}
64
+ bio: ${yamlString(bio)}
65
+ tags:
66
+ ${yamlStringList(tags.map((tag) => asString(tag, "")), " ")}
67
+
68
+ network:
69
+ mode: ${yamlString(mode)}
70
+
71
+ discovery:
72
+ discoverable: ${discoverable}
73
+ allow_profile_broadcast: ${allowProfileBroadcast}
74
+ allow_presence_broadcast: ${allowPresenceBroadcast}
75
+
76
+ visibility:
77
+ show_display_name: ${showDisplayName}
78
+ show_bio: ${showBio}
79
+ show_tags: ${showTags}
80
+ show_agent_id: ${showAgentId}
81
+ show_last_seen: ${showLastSeen}
82
+ show_capabilities_summary: ${showCapabilitiesSummary}
83
+
84
+ openclaw:
85
+ bind_existing_identity: ${bindExistingIdentity}
86
+ use_openclaw_profile_if_available: ${useOpenClawProfile}
87
+ ---
88
+
89
+ # Social
90
+
91
+ Generated from current SilicaClaw runtime state.
92
+
93
+ - Save as \`social.md\` in your OpenClaw workspace.
94
+ - This export does not auto-overwrite any existing file.
95
+ - Advanced network fields are intentionally hidden in template and resolved in runtime.
96
+ `;
97
+ }
@@ -0,0 +1,43 @@
1
+ export type AgentIdentity = {
2
+ agent_id: string;
3
+ public_key: string;
4
+ private_key: string;
5
+ created_at: number;
6
+ };
7
+
8
+ export type PublicProfile = {
9
+ agent_id: string;
10
+ display_name: string;
11
+ bio: string;
12
+ tags: string[];
13
+ avatar_url?: string;
14
+ public_enabled: boolean;
15
+ updated_at: number;
16
+ signature: string;
17
+ };
18
+
19
+ export type SignedProfileRecord = {
20
+ type: "profile";
21
+ profile: PublicProfile;
22
+ };
23
+
24
+ export type PresenceRecord = {
25
+ type: "presence";
26
+ agent_id: string;
27
+ timestamp: number;
28
+ signature: string;
29
+ };
30
+
31
+ export type IndexRefRecord = {
32
+ type: "index";
33
+ key: string;
34
+ agent_id: string;
35
+ };
36
+
37
+ export type DirectoryState = {
38
+ profiles: Record<string, PublicProfile>;
39
+ presence: Record<string, number>;
40
+ index: Record<string, string[]>;
41
+ };
42
+
43
+ export type ProfileInput = Omit<PublicProfile, "signature" | "updated_at">;
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist"
5
+ },
6
+ "include": ["src"]
7
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "@silicaclaw/network",
3
+ "version": "0.1.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "scripts": {
7
+ "build": "tsc -p tsconfig.json",
8
+ "check": "tsc -p tsconfig.json --noEmit"
9
+ }
10
+ }
@@ -0,0 +1,80 @@
1
+ export type NetworkMessageEnvelope<TPayload = unknown> = {
2
+ version: 1;
3
+ message_id: string;
4
+ topic: string;
5
+ source_peer_id: string;
6
+ timestamp: number;
7
+ payload: TPayload;
8
+ };
9
+
10
+ export type DecodedNetworkMessage = {
11
+ envelope: NetworkMessageEnvelope;
12
+ raw: Buffer;
13
+ };
14
+
15
+ export interface MessageEnvelopeCodec {
16
+ encode(envelope: NetworkMessageEnvelope): Buffer;
17
+ decode(raw: Buffer): DecodedNetworkMessage | null;
18
+ }
19
+
20
+ export type EnvelopeValidationOptions = {
21
+ now?: number;
22
+ max_future_drift_ms: number;
23
+ max_past_drift_ms: number;
24
+ };
25
+
26
+ export type EnvelopeValidationResult = {
27
+ ok: boolean;
28
+ reason?:
29
+ | "not_object"
30
+ | "invalid_version"
31
+ | "invalid_message_id"
32
+ | "invalid_topic"
33
+ | "invalid_source_peer_id"
34
+ | "invalid_timestamp"
35
+ | "missing_payload"
36
+ | "timestamp_future_drift"
37
+ | "timestamp_past_drift";
38
+ envelope?: NetworkMessageEnvelope;
39
+ drift_ms?: number;
40
+ };
41
+
42
+ export function validateNetworkMessageEnvelope(
43
+ value: unknown,
44
+ options: EnvelopeValidationOptions
45
+ ): EnvelopeValidationResult {
46
+ if (typeof value !== "object" || value === null) {
47
+ return { ok: false, reason: "not_object" };
48
+ }
49
+
50
+ const envelope = value as Partial<NetworkMessageEnvelope>;
51
+ if (envelope.version !== 1) {
52
+ return { ok: false, reason: "invalid_version" };
53
+ }
54
+ if (typeof envelope.message_id !== "string" || envelope.message_id.trim().length === 0) {
55
+ return { ok: false, reason: "invalid_message_id" };
56
+ }
57
+ if (typeof envelope.topic !== "string" || envelope.topic.trim().length === 0) {
58
+ return { ok: false, reason: "invalid_topic" };
59
+ }
60
+ if (typeof envelope.source_peer_id !== "string" || envelope.source_peer_id.trim().length === 0) {
61
+ return { ok: false, reason: "invalid_source_peer_id" };
62
+ }
63
+ if (!Number.isFinite(envelope.timestamp)) {
64
+ return { ok: false, reason: "invalid_timestamp" };
65
+ }
66
+ if (!("payload" in envelope)) {
67
+ return { ok: false, reason: "missing_payload" };
68
+ }
69
+
70
+ const now = options.now ?? Date.now();
71
+ const drift = Number(envelope.timestamp) - now;
72
+ if (drift > options.max_future_drift_ms) {
73
+ return { ok: false, reason: "timestamp_future_drift", drift_ms: drift };
74
+ }
75
+ if (drift < -options.max_past_drift_ms) {
76
+ return { ok: false, reason: "timestamp_past_drift", drift_ms: drift };
77
+ }
78
+
79
+ return { ok: true, envelope: envelope as NetworkMessageEnvelope, drift_ms: drift };
80
+ }
@@ -0,0 +1,49 @@
1
+ import { NetworkMessageEnvelope } from "./messageEnvelope";
2
+
3
+ export type PeerStatus = "online" | "stale";
4
+
5
+ export type PeerSnapshot = {
6
+ peer_id: string;
7
+ first_seen_at: number;
8
+ last_seen_at: number;
9
+ status: PeerStatus;
10
+ stale_since_at?: number;
11
+ messages_seen: number;
12
+ meta?: Record<string, unknown>;
13
+ };
14
+
15
+ export type PeerDiscoveryContext = {
16
+ self_peer_id: string;
17
+ publishControl: (topic: string, payload: unknown) => Promise<void>;
18
+ };
19
+
20
+ export type PeerDiscoveryStats = {
21
+ observe_calls: number;
22
+ peers_added: number;
23
+ peers_removed: number;
24
+ peers_marked_stale: number;
25
+ heartbeat_sent: number;
26
+ heartbeat_send_errors: number;
27
+ reconcile_runs: number;
28
+ last_observed_at: number;
29
+ last_heartbeat_at: number;
30
+ last_reconcile_at: number;
31
+ last_error_at: number;
32
+ };
33
+
34
+ export type PeerDiscoveryConfigSnapshot = {
35
+ discovery: string;
36
+ heartbeat_topic?: string;
37
+ heartbeat_interval_ms?: number;
38
+ stale_after_ms?: number;
39
+ remove_after_ms?: number;
40
+ };
41
+
42
+ export interface PeerDiscovery {
43
+ start(context: PeerDiscoveryContext): Promise<void>;
44
+ stop(): Promise<void>;
45
+ observeEnvelope(envelope: NetworkMessageEnvelope): void;
46
+ listPeers(): PeerSnapshot[];
47
+ getStats?(): PeerDiscoveryStats;
48
+ getConfig?(): PeerDiscoveryConfigSnapshot;
49
+ }
@@ -0,0 +1,4 @@
1
+ export interface TopicCodec {
2
+ encode(topic: string, payload: unknown): unknown;
3
+ decode(topic: string, payload: unknown): unknown;
4
+ }
@@ -0,0 +1,40 @@
1
+ export type TransportMessageMeta = {
2
+ remote_address: string;
3
+ remote_port: number;
4
+ transport: string;
5
+ };
6
+
7
+ export type TransportLifecycleState = "stopped" | "starting" | "running" | "stopping" | "error";
8
+
9
+ export type TransportStats = {
10
+ starts: number;
11
+ stops: number;
12
+ start_errors: number;
13
+ stop_errors: number;
14
+ sent_messages: number;
15
+ sent_bytes: number;
16
+ send_errors: number;
17
+ received_messages: number;
18
+ received_bytes: number;
19
+ receive_errors: number;
20
+ last_sent_at: number;
21
+ last_received_at: number;
22
+ last_error_at: number;
23
+ };
24
+
25
+ export type TransportConfigSnapshot = {
26
+ transport: string;
27
+ state: TransportLifecycleState;
28
+ bind_address?: string;
29
+ broadcast_address?: string;
30
+ port?: number;
31
+ };
32
+
33
+ export interface NetworkTransport {
34
+ start(): Promise<void>;
35
+ stop(): Promise<void>;
36
+ send(data: Buffer): Promise<void>;
37
+ onMessage(handler: (data: Buffer, meta: TransportMessageMeta) => void): () => void;
38
+ getStats?(): TransportStats;
39
+ getConfig?(): TransportConfigSnapshot;
40
+ }
@@ -0,0 +1,22 @@
1
+ import { DecodedNetworkMessage, MessageEnvelopeCodec, NetworkMessageEnvelope } from "../abstractions/messageEnvelope";
2
+
3
+ export class JsonMessageEnvelopeCodec implements MessageEnvelopeCodec {
4
+ encode(envelope: NetworkMessageEnvelope): Buffer {
5
+ return Buffer.from(JSON.stringify(envelope), "utf8");
6
+ }
7
+
8
+ decode(raw: Buffer): DecodedNetworkMessage | null {
9
+ try {
10
+ const parsed = JSON.parse(raw.toString("utf8")) as unknown;
11
+ if (typeof parsed !== "object" || parsed === null) {
12
+ return null;
13
+ }
14
+ return {
15
+ envelope: parsed as NetworkMessageEnvelope,
16
+ raw,
17
+ };
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,11 @@
1
+ import { TopicCodec } from "../abstractions/topicCodec";
2
+
3
+ export class JsonTopicCodec implements TopicCodec {
4
+ encode(_topic: string, payload: unknown): unknown {
5
+ return payload;
6
+ }
7
+
8
+ decode(_topic: string, payload: unknown): unknown {
9
+ return payload;
10
+ }
11
+ }
@@ -0,0 +1,173 @@
1
+ import {
2
+ PeerDiscovery,
3
+ PeerDiscoveryConfigSnapshot,
4
+ PeerDiscoveryContext,
5
+ PeerDiscoveryStats,
6
+ PeerSnapshot,
7
+ } from "../abstractions/peerDiscovery";
8
+ import { NetworkMessageEnvelope } from "../abstractions/messageEnvelope";
9
+
10
+ type HeartbeatPeerDiscoveryOptions = {
11
+ heartbeatIntervalMs?: number;
12
+ staleAfterMs?: number;
13
+ removeAfterMs?: number;
14
+ topic?: string;
15
+ };
16
+
17
+ export class HeartbeatPeerDiscovery implements PeerDiscovery {
18
+ private peers = new Map<string, PeerSnapshot>();
19
+ private timer: NodeJS.Timeout | null = null;
20
+ private context: PeerDiscoveryContext | null = null;
21
+
22
+ private heartbeatIntervalMs: number;
23
+ private staleAfterMs: number;
24
+ private removeAfterMs: number;
25
+ private topic: string;
26
+ private stats: PeerDiscoveryStats = {
27
+ observe_calls: 0,
28
+ peers_added: 0,
29
+ peers_removed: 0,
30
+ peers_marked_stale: 0,
31
+ heartbeat_sent: 0,
32
+ heartbeat_send_errors: 0,
33
+ reconcile_runs: 0,
34
+ last_observed_at: 0,
35
+ last_heartbeat_at: 0,
36
+ last_reconcile_at: 0,
37
+ last_error_at: 0,
38
+ };
39
+
40
+ constructor(options: HeartbeatPeerDiscoveryOptions = {}) {
41
+ this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? 12_000;
42
+ this.staleAfterMs = options.staleAfterMs ?? 45_000;
43
+ this.removeAfterMs = options.removeAfterMs ?? 180_000;
44
+ this.topic = options.topic ?? "__discovery/heartbeat";
45
+ }
46
+
47
+ async start(context: PeerDiscoveryContext): Promise<void> {
48
+ this.context = context;
49
+ this.reconcilePeerHealth();
50
+ await this.sendHeartbeat();
51
+
52
+ this.timer = setInterval(async () => {
53
+ await this.sendHeartbeat();
54
+ this.reconcilePeerHealth();
55
+ }, this.heartbeatIntervalMs);
56
+ }
57
+
58
+ async stop(): Promise<void> {
59
+ if (this.timer) {
60
+ clearInterval(this.timer);
61
+ this.timer = null;
62
+ }
63
+ }
64
+
65
+ observeEnvelope(envelope: NetworkMessageEnvelope): void {
66
+ this.stats.observe_calls += 1;
67
+ this.stats.last_observed_at = Date.now();
68
+
69
+ if (!this.context) {
70
+ return;
71
+ }
72
+ if (envelope.source_peer_id === this.context.self_peer_id) {
73
+ return;
74
+ }
75
+
76
+ const now = Date.now();
77
+ const existing = this.peers.get(envelope.source_peer_id);
78
+ if (!existing) {
79
+ this.stats.peers_added += 1;
80
+ }
81
+
82
+ this.peers.set(envelope.source_peer_id, {
83
+ peer_id: envelope.source_peer_id,
84
+ first_seen_at: existing?.first_seen_at ?? now,
85
+ last_seen_at: now,
86
+ status: "online",
87
+ stale_since_at: undefined,
88
+ messages_seen: (existing?.messages_seen ?? 0) + 1,
89
+ meta:
90
+ envelope.topic === this.topic && typeof envelope.payload === "object" && envelope.payload !== null
91
+ ? (envelope.payload as Record<string, unknown>)
92
+ : existing?.meta,
93
+ });
94
+ }
95
+
96
+ listPeers(): PeerSnapshot[] {
97
+ this.reconcilePeerHealth();
98
+ return Array.from(this.peers.values()).sort((a, b) => {
99
+ const score = (p: PeerSnapshot) => (p.status === "online" ? 1 : 0);
100
+ const byStatus = score(b) - score(a);
101
+ if (byStatus !== 0) {
102
+ return byStatus;
103
+ }
104
+ return b.last_seen_at - a.last_seen_at;
105
+ });
106
+ }
107
+
108
+ getStats(): PeerDiscoveryStats {
109
+ return { ...this.stats };
110
+ }
111
+
112
+ getConfig(): PeerDiscoveryConfigSnapshot {
113
+ return {
114
+ discovery: "heartbeat-peer-discovery",
115
+ heartbeat_topic: this.topic,
116
+ heartbeat_interval_ms: this.heartbeatIntervalMs,
117
+ stale_after_ms: this.staleAfterMs,
118
+ remove_after_ms: this.removeAfterMs,
119
+ };
120
+ }
121
+
122
+ private async sendHeartbeat(): Promise<void> {
123
+ if (!this.context) {
124
+ return;
125
+ }
126
+ try {
127
+ await this.context.publishControl(this.topic, {
128
+ kind: "heartbeat",
129
+ at: Date.now(),
130
+ });
131
+ this.stats.heartbeat_sent += 1;
132
+ this.stats.last_heartbeat_at = Date.now();
133
+ } catch {
134
+ this.stats.heartbeat_send_errors += 1;
135
+ this.stats.last_error_at = Date.now();
136
+ }
137
+ }
138
+
139
+ private reconcilePeerHealth(): void {
140
+ const now = Date.now();
141
+ this.stats.reconcile_runs += 1;
142
+ this.stats.last_reconcile_at = now;
143
+ for (const [peerId, peer] of this.peers.entries()) {
144
+ const age = now - peer.last_seen_at;
145
+
146
+ if (age > this.removeAfterMs) {
147
+ this.peers.delete(peerId);
148
+ this.stats.peers_removed += 1;
149
+ continue;
150
+ }
151
+
152
+ if (age > this.staleAfterMs) {
153
+ if (peer.status !== "stale") {
154
+ this.stats.peers_marked_stale += 1;
155
+ }
156
+ this.peers.set(peerId, {
157
+ ...peer,
158
+ status: "stale",
159
+ stale_since_at: peer.stale_since_at ?? now,
160
+ });
161
+ continue;
162
+ }
163
+
164
+ if (peer.status !== "online") {
165
+ this.peers.set(peerId, {
166
+ ...peer,
167
+ status: "online",
168
+ stale_since_at: undefined,
169
+ });
170
+ }
171
+ }
172
+ }
173
+ }
@@ -0,0 +1,16 @@
1
+ export * from "./types";
2
+ export * from "./mock";
3
+ export * from "./localEventBus";
4
+ export * from "./realPreview";
5
+ export * from "./webrtcPreview";
6
+
7
+ export * from "./abstractions/messageEnvelope";
8
+ export * from "./abstractions/topicCodec";
9
+ export * from "./abstractions/transport";
10
+ export * from "./abstractions/peerDiscovery";
11
+
12
+ export * from "./codec/jsonMessageEnvelopeCodec";
13
+ export * from "./codec/jsonTopicCodec";
14
+
15
+ export * from "./discovery/heartbeatPeerDiscovery";
16
+ export * from "./transport/udpLanBroadcastTransport";
@@ -0,0 +1,61 @@
1
+ import { EventEmitter } from "events";
2
+ import { NetworkAdapter } from "./types";
3
+
4
+ type EventBusPayload = {
5
+ topic: string;
6
+ data: any;
7
+ };
8
+
9
+ const CHANNEL_NAME = "silicaclaw-local-event-bus";
10
+
11
+ function getNodeBus(): EventEmitter {
12
+ const g = globalThis as typeof globalThis & {
13
+ __silicaclaw_bus?: EventEmitter;
14
+ };
15
+ if (!g.__silicaclaw_bus) {
16
+ g.__silicaclaw_bus = new EventEmitter();
17
+ }
18
+ return g.__silicaclaw_bus;
19
+ }
20
+
21
+ export class LocalEventBusAdapter implements NetworkAdapter {
22
+ private started = false;
23
+ private emitter = getNodeBus();
24
+
25
+ async start(): Promise<void> {
26
+ this.started = true;
27
+ }
28
+
29
+ async stop(): Promise<void> {
30
+ this.started = false;
31
+ }
32
+
33
+ async publish(topic: string, data: any): Promise<void> {
34
+ if (!this.started) {
35
+ return;
36
+ }
37
+
38
+ if (typeof BroadcastChannel !== "undefined") {
39
+ const channel = new BroadcastChannel(CHANNEL_NAME);
40
+ channel.postMessage({ topic, data } satisfies EventBusPayload);
41
+ channel.close();
42
+ return;
43
+ }
44
+
45
+ setImmediate(() => this.emitter.emit(topic, data));
46
+ }
47
+
48
+ subscribe(topic: string, handler: (data: any) => void): void {
49
+ if (typeof BroadcastChannel !== "undefined") {
50
+ const channel = new BroadcastChannel(CHANNEL_NAME);
51
+ channel.onmessage = (event: MessageEvent<EventBusPayload>) => {
52
+ if (event.data?.topic === topic) {
53
+ handler(event.data.data);
54
+ }
55
+ };
56
+ return;
57
+ }
58
+
59
+ this.emitter.on(topic, handler);
60
+ }
61
+ }
@@ -0,0 +1,27 @@
1
+ import { EventEmitter } from "events";
2
+ import { NetworkAdapter } from "./types";
3
+
4
+ const bus = new EventEmitter();
5
+
6
+ export class MockNetworkAdapter implements NetworkAdapter {
7
+ private started = false;
8
+
9
+ async start(): Promise<void> {
10
+ this.started = true;
11
+ }
12
+
13
+ async stop(): Promise<void> {
14
+ this.started = false;
15
+ }
16
+
17
+ async publish(topic: string, data: any): Promise<void> {
18
+ if (!this.started) {
19
+ return;
20
+ }
21
+ setImmediate(() => bus.emit(topic, data));
22
+ }
23
+
24
+ subscribe(topic: string, handler: (data: any) => void): void {
25
+ bus.on(topic, handler);
26
+ }
27
+ }