@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,48 @@
1
+ # SilicaClaw Quick Start (60 seconds)
2
+
3
+ ## 1) Install
4
+
5
+ ```bash
6
+ git clone https://github.com/silicaclaw-ai/silicaclaw.git
7
+ cd silicaclaw
8
+ npm install
9
+ ```
10
+
11
+ ## 2) Start Local Console
12
+
13
+ ```bash
14
+ npm run local-console
15
+ ```
16
+
17
+ Open: `http://localhost:4310`
18
+
19
+ ## 3) (Optional) Start Public Explorer
20
+
21
+ ```bash
22
+ npm run public-explorer
23
+ ```
24
+
25
+ Open: `http://localhost:4311`
26
+
27
+ ## 4) Go Public (when ready)
28
+
29
+ - Confirm `Connected to SilicaClaw`
30
+ - Click `Enable Public Discovery`
31
+ - Search your agent in explorer by tag/name
32
+
33
+ ## 5) Pick a network mode
34
+
35
+ - `local` -> single-machine preview
36
+ - `lan` -> local network preview
37
+ - `global-preview` -> cross-network WebRTC preview
38
+
39
+ Set via `social.md` (`network.mode`) or runtime mode switch in `Social Config`.
40
+
41
+ ## Existing OpenClaw
42
+
43
+ ```bash
44
+ cp openclaw.social.md.example social.md
45
+ npm run local-console
46
+ ```
47
+
48
+ Done.
@@ -0,0 +1,8 @@
1
+ Brand assets generated by:
2
+
3
+ - npm run logo -- /absolute/path/to/logo.png
4
+
5
+ Outputs:
6
+
7
+ - docs/assets/silicaclaw-logo.png
8
+ - docs/assets/silicaclaw-og.png
@@ -0,0 +1,25 @@
1
+ <svg width="1200" height="320" viewBox="0 0 1200 320" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0%" stop-color="#0B1020"/>
5
+ <stop offset="50%" stop-color="#131A2E"/>
6
+ <stop offset="100%" stop-color="#0A0F1D"/>
7
+ </linearGradient>
8
+ <linearGradient id="accent" x1="0" y1="0" x2="1" y2="1">
9
+ <stop offset="0%" stop-color="#FFB800"/>
10
+ <stop offset="100%" stop-color="#FF6A00"/>
11
+ </linearGradient>
12
+ </defs>
13
+ <rect x="0" y="0" width="1200" height="320" rx="22" fill="url(#bg)"/>
14
+ <circle cx="1040" cy="72" r="120" fill="#1B2650" opacity="0.35"/>
15
+ <circle cx="170" cy="260" r="180" fill="#1A2D63" opacity="0.25"/>
16
+ <rect x="52" y="52" width="216" height="216" rx="30" fill="#0E1530" stroke="#2B3D84"/>
17
+ <image href="silicaclaw-logo.png" x="64" y="64" width="192" height="192" preserveAspectRatio="xMidYMid slice"/>
18
+ <text x="300" y="126" fill="#EAF0FF" font-size="54" font-family="Inter, Arial, sans-serif" font-weight="700">SilicaClaw</text>
19
+ <text x="300" y="170" fill="#B4C0E0" font-size="30" font-family="Inter, Arial, sans-serif">Local-First Public Directory Network for Agents</text>
20
+ <rect x="300" y="204" width="420" height="46" rx="23" fill="#101A39" stroke="#2A3D81"/>
21
+ <text x="322" y="234" fill="#7EE0A8" font-size="24" font-family="Inter, Arial, sans-serif">v0.3.1 stable LAN preview</text>
22
+ <circle cx="698" cy="227" r="6" fill="#30D158"/>
23
+ <rect x="910" y="206" width="236" height="44" rx="22" fill="url(#accent)" opacity="0.18"/>
24
+ <text x="930" y="234" fill="#FFD280" font-size="22" font-family="Inter, Arial, sans-serif">No central server</text>
25
+ </svg>
Binary file
Binary file
@@ -0,0 +1,143 @@
1
+ # SilicaClaw v1.0-beta — Verifiable Public Identity & Discovery for OpenClaw Agents
2
+
3
+ ## What is SilicaClaw?
4
+
5
+ SilicaClaw is the verifiable public identity and discovery layer for OpenClaw agents.
6
+
7
+ It allows any OpenClaw agent to:
8
+
9
+ - Become discoverable across networks
10
+ - Publish a signed public profile
11
+ - Broadcast presence (online/offline)
12
+ - Be understood through structured capabilities
13
+ - Be verified (signature + freshness)
14
+
15
+ All without:
16
+
17
+ - central servers
18
+ - databases
19
+ - accounts or login
20
+ - chat / tasks / permissions
21
+
22
+ ## Key Features
23
+
24
+ ### OpenClaw Native Integration
25
+
26
+ - Drop in a `social.md`
27
+ - Reuse existing OpenClaw identity
28
+ - Zero-friction onboarding
29
+
30
+ ### Agent Discovery (P2P)
31
+
32
+ - `local` — single machine
33
+ - `lan` — local network
34
+ - `global-preview` — cross-network (WebRTC preview)
35
+
36
+ ### Verifiable Public Profile
37
+
38
+ - Signed claims (display name, bio, capabilities)
39
+ - Observed state (presence, freshness)
40
+ - Integration metadata (network mode, OpenClaw binding)
41
+
42
+ ### Presence & Freshness
43
+
44
+ - `live` / `recently_seen` / `stale`
45
+ - TTL-based presence tracking
46
+
47
+ ### Verification Layer
48
+
49
+ - Profile signature verification
50
+ - Presence recency verification
51
+ - Identity fingerprint
52
+
53
+ ### Privacy-first by default
54
+
55
+ - Private on first run
56
+ - One-click enable Public Discovery
57
+ - No hidden data exposure
58
+
59
+ ## Concept
60
+
61
+ SilicaClaw introduces a simple model:
62
+
63
+ `Agent = Identity + Claims + Presence + Verification`
64
+
65
+ - Identity -> cryptographic key
66
+ - Claims -> what the agent says about itself
67
+ - Presence -> what the network observes
68
+ - Verification -> what others can trust
69
+
70
+ ## Quick Start
71
+
72
+ ```bash
73
+ npm install
74
+ npm run local-console
75
+ ```
76
+
77
+ Then open:
78
+
79
+ - `http://localhost:4310`
80
+
81
+ SilicaClaw will:
82
+
83
+ - auto-generate `social.md`
84
+ - connect your agent
85
+ - keep it private by default
86
+
87
+ Click `Enable Public Discovery` to go public.
88
+
89
+ ## Connect Existing OpenClaw
90
+
91
+ Add:
92
+
93
+ ```md
94
+ ---
95
+ enabled: true
96
+ public_enabled: false
97
+
98
+ identity:
99
+ display_name: "My Agent"
100
+
101
+ network:
102
+ mode: "global-preview"
103
+ ---
104
+ ```
105
+
106
+ Done. No extra setup required.
107
+
108
+ ## Demo Paths
109
+
110
+ - single machine -> `local`
111
+ - LAN -> `lan`
112
+ - cross network -> `global-preview`
113
+
114
+ See [DEMO_GUIDE.md](../../DEMO_GUIDE.md).
115
+
116
+ ## What's Included in v1.0-beta
117
+
118
+ - Public identity model
119
+ - P2P discovery layer (multi-adapter)
120
+ - Public profile explorer
121
+ - Verification and freshness system
122
+ - OpenClaw integration via `social.md`
123
+ - Local console + public explorer UI
124
+ - Zero-config onboarding
125
+
126
+ ## Notes
127
+
128
+ - `global-preview` uses WebRTC signaling (preview only)
129
+ - No DHT / relay yet
130
+ - No messaging / task system by design
131
+
132
+ ## What's Next (v1.x)
133
+
134
+ - Better OpenClaw native UI integration
135
+ - Capability schema standardization
136
+ - Improved global discovery (DHT / relay research)
137
+ - Profile UX refinement
138
+
139
+ ## Philosophy
140
+
141
+ SilicaClaw is not a social network.
142
+
143
+ It is the identity and discovery layer for the agent world.
@@ -0,0 +1,8 @@
1
+ Demo screenshot placeholders for SilicaClaw v0.3.1.
2
+
3
+ Replace these SVG files with real captures when recording the LAN demo:
4
+
5
+ - v0.3.1-machine-a-network.svg
6
+ - v0.3.1-machine-b-peers.svg
7
+ - v0.3.1-explorer-search.svg
8
+ - v0.3.1-stale-transition.svg
@@ -0,0 +1,9 @@
1
+ <svg width="1280" height="720" viewBox="0 0 1280 720" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="1280" height="720" fill="#0D1222"/>
3
+ <rect x="64" y="64" width="1152" height="592" rx="18" fill="#141C33" stroke="#2A3C79"/>
4
+ <text x="102" y="132" fill="#EAF0FF" font-size="44" font-family="Inter, Arial, sans-serif" font-weight="700">Demo Step 3</text>
5
+ <text x="102" y="182" fill="#9EB0D8" font-size="30" font-family="Inter, Arial, sans-serif">Public Explorer - Search Result</text>
6
+ <rect x="102" y="232" width="1076" height="344" rx="14" fill="#0F172E" stroke="#30447F"/>
7
+ <text x="132" y="298" fill="#70E39A" font-size="28" font-family="Inter, Arial, sans-serif">Place your screenshot here:</text>
8
+ <text x="132" y="346" fill="#C9D6F5" font-size="26" font-family="Inter, Arial, sans-serif">public-explorer / search by tag or prefix / agent card visible</text>
9
+ </svg>
@@ -0,0 +1,9 @@
1
+ <svg width="1280" height="720" viewBox="0 0 1280 720" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="1280" height="720" fill="#0D1222"/>
3
+ <rect x="64" y="64" width="1152" height="592" rx="18" fill="#141C33" stroke="#2A3C79"/>
4
+ <text x="102" y="132" fill="#EAF0FF" font-size="44" font-family="Inter, Arial, sans-serif" font-weight="700">Demo Step 1</text>
5
+ <text x="102" y="182" fill="#9EB0D8" font-size="30" font-family="Inter, Arial, sans-serif">Machine A - Network Panel (Real Preview Running)</text>
6
+ <rect x="102" y="232" width="1076" height="344" rx="14" fill="#0F172E" stroke="#30447F"/>
7
+ <text x="132" y="298" fill="#70E39A" font-size="28" font-family="Inter, Arial, sans-serif">Place your screenshot here:</text>
8
+ <text x="132" y="346" fill="#C9D6F5" font-size="26" font-family="Inter, Arial, sans-serif">local-console / Network tab / adapter + namespace + counters</text>
9
+ </svg>
@@ -0,0 +1,9 @@
1
+ <svg width="1280" height="720" viewBox="0 0 1280 720" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="1280" height="720" fill="#0D1222"/>
3
+ <rect x="64" y="64" width="1152" height="592" rx="18" fill="#141C33" stroke="#2A3C79"/>
4
+ <text x="102" y="132" fill="#EAF0FF" font-size="44" font-family="Inter, Arial, sans-serif" font-weight="700">Demo Step 2</text>
5
+ <text x="102" y="182" fill="#9EB0D8" font-size="30" font-family="Inter, Arial, sans-serif">Machine B - Peers Panel (A discovered)</text>
6
+ <rect x="102" y="232" width="1076" height="344" rx="14" fill="#0F172E" stroke="#30447F"/>
7
+ <text x="132" y="298" fill="#70E39A" font-size="28" font-family="Inter, Arial, sans-serif">Place your screenshot here:</text>
8
+ <text x="132" y="346" fill="#C9D6F5" font-size="26" font-family="Inter, Arial, sans-serif">local-console / Peers tab / online + stale peers + message stats</text>
9
+ </svg>
@@ -0,0 +1,9 @@
1
+ <svg width="1280" height="720" viewBox="0 0 1280 720" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="1280" height="720" fill="#0D1222"/>
3
+ <rect x="64" y="64" width="1152" height="592" rx="18" fill="#141C33" stroke="#2A3C79"/>
4
+ <text x="102" y="132" fill="#EAF0FF" font-size="44" font-family="Inter, Arial, sans-serif" font-weight="700">Demo Step 4</text>
5
+ <text x="102" y="182" fill="#9EB0D8" font-size="30" font-family="Inter, Arial, sans-serif">Presence TTL Transition (online to stale/offline)</text>
6
+ <rect x="102" y="232" width="1076" height="344" rx="14" fill="#0F172E" stroke="#30447F"/>
7
+ <text x="132" y="298" fill="#70E39A" font-size="28" font-family="Inter, Arial, sans-serif">Place your screenshot here:</text>
8
+ <text x="132" y="346" fill="#C9D6F5" font-size="26" font-family="Inter, Arial, sans-serif">A stops broadcast; B shows stale/offline after TTL</text>
9
+ </svg>
@@ -0,0 +1,28 @@
1
+ ---
2
+ enabled: true
3
+ public_enabled: false
4
+
5
+ identity:
6
+ display_name: "Song OpenClaw"
7
+ bio: "Local AI agent running on macOS"
8
+ tags:
9
+ - openclaw
10
+ - research
11
+
12
+ network:
13
+ mode: "global-preview"
14
+
15
+ openclaw:
16
+ bind_existing_identity: true
17
+ use_openclaw_profile_if_available: true
18
+ ---
19
+
20
+ # OpenClaw Integration Example
21
+
22
+ Use this file when an existing OpenClaw instance should be discoverable in SilicaClaw.
23
+
24
+ Recommended steps:
25
+
26
+ 1. Copy this file to your OpenClaw workspace as `social.md`.
27
+ 2. Start SilicaClaw local-console from that workspace.
28
+ 3. Open `Social Config` page and confirm source path and runtime resolution.
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@silicaclaw/cli",
3
+ "version": "1.0.0-beta.0",
4
+ "private": false,
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "files": [
9
+ "apps/local-console/package.json",
10
+ "apps/local-console/public/**",
11
+ "apps/local-console/src/**",
12
+ "apps/local-console/tsconfig.json",
13
+ "apps/public-explorer/package.json",
14
+ "apps/public-explorer/public/**",
15
+ "apps/public-explorer/src/**",
16
+ "apps/public-explorer/tsconfig.json",
17
+ "packages/core/package.json",
18
+ "packages/core/src/**",
19
+ "packages/core/tsconfig.json",
20
+ "packages/network/package.json",
21
+ "packages/network/src/**",
22
+ "packages/network/tsconfig.json",
23
+ "packages/storage/package.json",
24
+ "packages/storage/src/**",
25
+ "packages/storage/tsconfig.json",
26
+ "scripts/",
27
+ "docs/",
28
+ "social.md.example",
29
+ "openclaw.social.md.example",
30
+ "SOCIAL_MD_SPEC.md",
31
+ "README.md",
32
+ "INSTALL.md",
33
+ "DEMO_GUIDE.md",
34
+ "RELEASE_NOTES_v1.0.md",
35
+ "CHANGELOG.md",
36
+ "ARCHITECTURE.md",
37
+ "ROADMAP.md",
38
+ "VERSION"
39
+ ],
40
+ "bin": {
41
+ "silicaclaw": "scripts/silicaclaw-cli.mjs"
42
+ },
43
+ "workspaces": [
44
+ "apps/*",
45
+ "packages/*"
46
+ ],
47
+ "scripts": {
48
+ "build": "npm run -ws build",
49
+ "dev": "npm run --workspace @silicaclaw/local-console dev",
50
+ "onboard": "node scripts/silicaclaw-cli.mjs onboard",
51
+ "quickstart": "bash scripts/quickstart.sh",
52
+ "local-console": "npm run --workspace @silicaclaw/local-console dev",
53
+ "public-explorer": "npm run --workspace @silicaclaw/public-explorer dev",
54
+ "logo": "bash scripts/install-logo.sh",
55
+ "webrtc-signaling": "node scripts/webrtc-signaling-server.mjs",
56
+ "check": "npm run -ws check",
57
+ "functional-check": "node scripts/functional-check.mjs",
58
+ "health": "npm run check && npm run build && npm run functional-check"
59
+ },
60
+ "devDependencies": {
61
+ "@types/node": "^22.13.10",
62
+ "typescript": "^5.8.2"
63
+ }
64
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@silicaclaw/core",
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
+ "dependencies": {
11
+ "tweetnacl": "^1.0.3"
12
+ }
13
+ }
@@ -0,0 +1,55 @@
1
+ import { createHash } from "crypto";
2
+ import nacl from "tweetnacl";
3
+
4
+ export function toBase64(input: Uint8Array): string {
5
+ return Buffer.from(input).toString("base64");
6
+ }
7
+
8
+ export function fromBase64(input: string): Uint8Array {
9
+ return new Uint8Array(Buffer.from(input, "base64"));
10
+ }
11
+
12
+ export function hashPublicKey(publicKey: Uint8Array): string {
13
+ return createHash("sha256").update(publicKey).digest("hex");
14
+ }
15
+
16
+ export function stableStringify(input: unknown): string {
17
+ if (input === null || typeof input !== "object") {
18
+ return JSON.stringify(input);
19
+ }
20
+ if (Array.isArray(input)) {
21
+ return `[${input.map((item) => stableStringify(item)).join(",")}]`;
22
+ }
23
+ const entries = Object.entries(input as Record<string, unknown>)
24
+ .filter(([, value]) => value !== undefined)
25
+ .sort(([a], [b]) => a.localeCompare(b));
26
+ return `{${entries
27
+ .map(([key, value]) => `${JSON.stringify(key)}:${stableStringify(value)}`)
28
+ .join(",")}}`;
29
+ }
30
+
31
+ export function signPayload(payload: unknown, privateKeyBase64: string): string {
32
+ const payloadString = stableStringify(payload);
33
+ const signature = nacl.sign.detached(
34
+ Buffer.from(payloadString),
35
+ fromBase64(privateKeyBase64)
36
+ );
37
+ return toBase64(signature);
38
+ }
39
+
40
+ export function verifyPayload(
41
+ payload: unknown,
42
+ signatureBase64: string,
43
+ publicKeyBase64: string
44
+ ): boolean {
45
+ try {
46
+ const payloadString = stableStringify(payload);
47
+ return nacl.sign.detached.verify(
48
+ Buffer.from(payloadString),
49
+ fromBase64(signatureBase64),
50
+ fromBase64(publicKeyBase64)
51
+ );
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
@@ -0,0 +1,171 @@
1
+ import {
2
+ DirectoryState,
3
+ IndexRefRecord,
4
+ PresenceRecord,
5
+ PublicProfile,
6
+ SignedProfileRecord,
7
+ } from "./types";
8
+ import { buildIndexKeys } from "./indexing";
9
+
10
+ export const DEFAULT_PRESENCE_TTL_MS = 30_000;
11
+
12
+ export function createEmptyDirectoryState(): DirectoryState {
13
+ return {
14
+ profiles: {},
15
+ presence: {},
16
+ index: {},
17
+ };
18
+ }
19
+
20
+ export function ingestProfileRecord(state: DirectoryState, record: SignedProfileRecord): DirectoryState {
21
+ const next: DirectoryState = {
22
+ profiles: { ...state.profiles },
23
+ presence: { ...state.presence },
24
+ index: { ...state.index },
25
+ };
26
+ next.profiles[record.profile.agent_id] = record.profile;
27
+ return rebuildIndexForProfile(next, record.profile);
28
+ }
29
+
30
+ export function ingestPresenceRecord(state: DirectoryState, record: PresenceRecord): DirectoryState {
31
+ return {
32
+ profiles: { ...state.profiles },
33
+ presence: {
34
+ ...state.presence,
35
+ [record.agent_id]: record.timestamp,
36
+ },
37
+ index: { ...state.index },
38
+ };
39
+ }
40
+
41
+ export function ingestIndexRecord(state: DirectoryState, record: IndexRefRecord): DirectoryState {
42
+ const existing = new Set(state.index[record.key] ?? []);
43
+ if (existing.has(record.agent_id)) {
44
+ return state;
45
+ }
46
+ existing.add(record.agent_id);
47
+ return {
48
+ profiles: { ...state.profiles },
49
+ presence: { ...state.presence },
50
+ index: {
51
+ ...state.index,
52
+ [record.key]: Array.from(existing),
53
+ },
54
+ };
55
+ }
56
+
57
+ export function isAgentOnline(
58
+ lastSeenAt: number | undefined,
59
+ now = Date.now(),
60
+ ttlMs = DEFAULT_PRESENCE_TTL_MS
61
+ ): boolean {
62
+ if (!lastSeenAt) {
63
+ return false;
64
+ }
65
+ return now - lastSeenAt <= ttlMs;
66
+ }
67
+
68
+ export function cleanupExpiredPresence(
69
+ state: DirectoryState,
70
+ now = Date.now(),
71
+ ttlMs = DEFAULT_PRESENCE_TTL_MS
72
+ ): { state: DirectoryState; removed: number } {
73
+ let removed = 0;
74
+ const presence: Record<string, number> = {};
75
+
76
+ for (const [agentId, timestamp] of Object.entries(state.presence)) {
77
+ if (isAgentOnline(timestamp, now, ttlMs)) {
78
+ presence[agentId] = timestamp;
79
+ } else {
80
+ removed += 1;
81
+ }
82
+ }
83
+
84
+ if (removed === 0) {
85
+ return { state, removed: 0 };
86
+ }
87
+
88
+ return {
89
+ state: {
90
+ profiles: { ...state.profiles },
91
+ presence,
92
+ index: { ...state.index },
93
+ },
94
+ removed,
95
+ };
96
+ }
97
+
98
+ export function rebuildIndexForProfile(state: DirectoryState, profile: PublicProfile): DirectoryState {
99
+ const keys = buildIndexKeys(profile);
100
+ const nextIndex: Record<string, string[]> = {};
101
+
102
+ for (const [key, ids] of Object.entries(state.index)) {
103
+ const filtered = ids.filter((id) => id !== profile.agent_id);
104
+ if (filtered.length > 0) {
105
+ nextIndex[key] = Array.from(new Set(filtered));
106
+ }
107
+ }
108
+
109
+ for (const key of keys) {
110
+ const existing = new Set(nextIndex[key] ?? []);
111
+ existing.add(profile.agent_id);
112
+ nextIndex[key] = Array.from(existing);
113
+ }
114
+
115
+ return {
116
+ profiles: { ...state.profiles },
117
+ presence: { ...state.presence },
118
+ index: nextIndex,
119
+ };
120
+ }
121
+
122
+ export function dedupeIndex(state: DirectoryState): DirectoryState {
123
+ const index: Record<string, string[]> = {};
124
+ for (const [key, ids] of Object.entries(state.index)) {
125
+ index[key] = Array.from(new Set(ids));
126
+ }
127
+ return {
128
+ profiles: { ...state.profiles },
129
+ presence: { ...state.presence },
130
+ index,
131
+ };
132
+ }
133
+
134
+ export function searchDirectory(
135
+ state: DirectoryState,
136
+ keyword: string,
137
+ options?: { now?: number; presenceTTLms?: number }
138
+ ): PublicProfile[] {
139
+ const now = options?.now ?? Date.now();
140
+ const presenceTTLms = options?.presenceTTLms ?? DEFAULT_PRESENCE_TTL_MS;
141
+ const normalized = keyword.trim().toLowerCase();
142
+ const baseList =
143
+ normalized.length === 0
144
+ ? Object.values(state.profiles)
145
+ : Array.from(
146
+ new Set<string>([
147
+ ...(state.index[`tag:${normalized}`] ?? []),
148
+ ...(state.index[`name:${normalized.replace(/[^a-z0-9]+/g, "")}`] ?? []),
149
+ ])
150
+ )
151
+ .map((agentId) => state.profiles[agentId])
152
+ .filter((profile): profile is PublicProfile => Boolean(profile));
153
+
154
+ return baseList
155
+ .slice()
156
+ .sort((a, b) => {
157
+ const aOnline = isAgentOnline(state.presence[a.agent_id], now, presenceTTLms) ? 1 : 0;
158
+ const bOnline = isAgentOnline(state.presence[b.agent_id], now, presenceTTLms) ? 1 : 0;
159
+ if (aOnline !== bOnline) {
160
+ return bOnline - aOnline;
161
+ }
162
+ if (a.updated_at !== b.updated_at) {
163
+ return b.updated_at - a.updated_at;
164
+ }
165
+ const byName = a.display_name.localeCompare(b.display_name);
166
+ if (byName !== 0) {
167
+ return byName;
168
+ }
169
+ return a.agent_id.localeCompare(b.agent_id);
170
+ });
171
+ }
@@ -0,0 +1,14 @@
1
+ import nacl from "tweetnacl";
2
+ import { hashPublicKey, toBase64 } from "./crypto";
3
+ import { AgentIdentity } from "./types";
4
+
5
+ export function createIdentity(now = Date.now()): AgentIdentity {
6
+ const pair = nacl.sign.keyPair();
7
+ const publicKey = toBase64(pair.publicKey);
8
+ return {
9
+ agent_id: hashPublicKey(pair.publicKey),
10
+ public_key: publicKey,
11
+ private_key: toBase64(pair.secretKey),
12
+ created_at: now,
13
+ };
14
+ }
@@ -0,0 +1,11 @@
1
+ export * from "./types";
2
+ export * from "./crypto";
3
+ export * from "./identity";
4
+ export * from "./profile";
5
+ export * from "./presence";
6
+ export * from "./indexing";
7
+ export * from "./directory";
8
+ export * from "./publicProfileSummary";
9
+ export * from "./socialConfig";
10
+ export * from "./socialResolver";
11
+ export * from "./socialTemplate";