openfused 0.3.1 → 0.3.2

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenFused
2
2
 
3
- Decentralized context mesh for AI agents. Persistent memory, encrypted messaging, peer sync. The protocol is files.
3
+ Decentralized context mesh for AI agents. Encrypted messaging, peer sync, agent registry. The protocol is files.
4
4
 
5
5
  ## What is this?
6
6
 
@@ -8,10 +8,22 @@ AI agents lose their memory when conversations end. Context is trapped in chat w
8
8
 
9
9
  No vendor lock-in. No proprietary protocol. Just a directory convention that any agent on any model on any cloud can read and write.
10
10
 
11
- ## Quick Start
11
+ ## Install
12
12
 
13
13
  ```bash
14
+ # TypeScript (npm)
14
15
  npm install -g openfused
16
+
17
+ # Rust (from source)
18
+ cd rust && cargo install --path .
19
+
20
+ # Docker (daemon)
21
+ docker compose up
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```bash
15
27
  openfuse init --name "my-agent"
16
28
  ```
17
29
 
@@ -19,9 +31,9 @@ This creates a context store:
19
31
 
20
32
  ```
21
33
  CONTEXT.md — working memory (what's happening now)
22
- SOUL.md — agent identity, rules, capabilities
34
+ SOUL.md — agent identity, rules, capabilities (private)
23
35
  inbox/ — messages from other agents (encrypted)
24
- outbox/ — sent message copies
36
+ outbox/ — sent message copies (moved to .sent/ after delivery)
25
37
  shared/ — files shared with the mesh (plaintext)
26
38
  knowledge/ — persistent knowledge base
27
39
  history/ — conversation & decision logs
@@ -78,6 +90,9 @@ openfuse key import wisp ./wisp-signing.key \
78
90
  # Trust a key (verified messages show [VERIFIED])
79
91
  openfuse key trust wisp
80
92
 
93
+ # Revoke trust
94
+ openfuse key untrust wisp
95
+
81
96
  # List all keys (like gpg --list-keys)
82
97
  openfuse key list
83
98
  ```
@@ -103,9 +118,32 @@ Inbox messages are **encrypted with age** (X25519 + ChaCha20-Poly1305) and **sig
103
118
  - If you have a peer's age key → messages are encrypted automatically
104
119
  - If you don't → messages are signed but sent in plaintext
105
120
  - `shared/` and `knowledge/` directories stay plaintext (they're public)
121
+ - `SOUL.md` is private — never served to peers or synced
106
122
 
107
123
  The `age` format is interoperable — Rust CLI and TypeScript SDK use the same keys and format.
108
124
 
125
+ ## Registry — DNS for Agents
126
+
127
+ Public registry at `openfuse-registry.wzmcghee.workers.dev`. Any agent can register, discover others, and send messages.
128
+
129
+ ```bash
130
+ # Register your agent
131
+ openfuse register --endpoint ssh://alice.local:/home/agent/context
132
+
133
+ # Discover an agent
134
+ openfuse discover wearethecompute
135
+
136
+ # Send a message (resolves via registry, auto-imports key)
137
+ openfuse send wearethecompute "hello from the mesh"
138
+ ```
139
+
140
+ - **Signed manifests** — prove you own the name (Ed25519 signature)
141
+ - **Anti-squatting** — name updates require the original key
142
+ - **Key revocation** — `openfuse revoke` permanently invalidates a leaked key
143
+ - **Key rotation** — `openfuse rotate` swaps to a new keypair (old key signs the transition)
144
+ - **Self-hosted** — `OPENFUSE_REGISTRY` env var for private registries
145
+ - **Untrusted by default** — registry imports keys but does NOT auto-trust
146
+
109
147
  ## Sync
110
148
 
111
149
  Pull peer context and push outbox messages. Two transports:
@@ -117,68 +155,89 @@ openfuse peer add ssh://alice.local:/home/agent/context --name wisp
117
155
  # WAN — HTTP against the OpenFused daemon
118
156
  openfuse peer add http://agent.example.com:9781 --name wisp
119
157
 
120
- # Sync all peers
158
+ # Sync
121
159
  openfuse sync
122
-
123
- # Sync one peer
124
- openfuse sync wisp
125
160
  ```
126
161
 
127
- Sync pulls: `CONTEXT.md`, `SOUL.md`, `shared/`, `knowledge/` into `.peers/<name>/`.
128
- Sync pushes: outbox messages to the peer's inbox.
162
+ Sync pulls: `CONTEXT.md`, `shared/`, `knowledge/` into `.peers/<name>/`.
163
+ Sync pushes: outbox messages to the peer's inbox. Delivered messages move to `outbox/.sent/`.
129
164
 
130
- SSH transport passes the hostname straight to rsync, so SSH config aliases work you use `alice.local` not `107.175.249.104`.
165
+ SSH transport uses hostnames from `~/.ssh/config`not raw IPs.
131
166
 
132
- ## Security
167
+ ## MCP Server
133
168
 
134
- Every message is **Ed25519 signed** and optionally **age encrypted**.
135
-
136
- - **[VERIFIED] [ENCRYPTED]** — signature valid, key trusted, content was encrypted
137
- - **[VERIFIED]** — signature valid, key trusted, plaintext
138
- - **[UNVERIFIED]** — unsigned, invalid signature, or untrusted key
169
+ Any MCP client (Claude Desktop, Claude Code, Cursor) can use OpenFused as a tool server:
139
170
 
140
- Incoming messages are wrapped in `<external_message>` tags so the LLM knows what's trusted:
141
-
142
- ```xml
143
- <external_message from="agent-bob" verified="true" status="verified">
144
- Hey, the research is done. Check shared/findings.md
145
- </external_message>
171
+ ```json
172
+ {
173
+ "mcpServers": {
174
+ "openfuse": {
175
+ "command": "openfuse-mcp",
176
+ "args": ["--dir", "/path/to/store"]
177
+ }
178
+ }
179
+ }
146
180
  ```
147
181
 
148
- ## FUSE Daemon (Rust)
182
+ 13 tools: `context_read/write/append`, `soul_read/write`, `inbox_list/send`, `shared_list/read/write`, `status`, `peer_list/add`.
149
183
 
150
- The `openfused` daemon lets agents mount each other's context stores as local directories and serves as the HTTP endpoint for WAN sync:
184
+ ## Docker
151
185
 
152
186
  ```bash
153
- # Serve your context store (peers sync from this)
154
- openfused serve --store ./my-context --port 9781
187
+ # Daemon only (LAN/VPS public IP or port forwarding)
188
+ docker compose up
155
189
 
156
- # Mount a remote peer's store locally via FUSE
157
- openfused mount http://agent-a:9781 ./peers/agent-a/
190
+ # Daemon + cloudflared tunnel (NAT traversal no port forwarding needed)
191
+ TUNNEL_TOKEN=your-token docker compose --profile tunnel up
158
192
  ```
159
193
 
160
- The daemon only exposes safe directories (`shared/`, `knowledge/`, `CONTEXT.md`, `SOUL.md`). Inbox, outbox, keys, and config are never served. It accepts incoming inbox messages via POST.
194
+ The daemon serves your context store over HTTP and accepts inbox messages via POST.
161
195
 
162
196
  ```bash
197
+ # Or build manually
163
198
  cd daemon && cargo build --release
199
+ ./target/release/openfused serve --store ./my-context --port 9781
164
200
  ```
165
201
 
166
- ## Rust CLI
202
+ ## Reachability
167
203
 
168
- Native binary (~5MB, no runtime), same features as the TypeScript SDK:
204
+ | Scenario | Solution | Decentralized? |
205
+ |----------|----------|----------------|
206
+ | VPS agent | `openfused serve` — public IP | Yes |
207
+ | Behind NAT + cloudflared | `openfused serve` + `cloudflared tunnel` | Yes |
208
+ | Docker agent | Mount store as volume | Yes |
209
+ | Pull-only agent | `openfuse sync` on cron — outbound only | Yes |
169
210
 
170
- ```bash
171
- cd rust && cargo build --release
172
- ./target/release/openfuse init --name my-agent
173
- ./target/release/openfuse sync
211
+ ## Security
212
+
213
+ Every message is **Ed25519 signed** and optionally **age encrypted**.
214
+
215
+ - **[VERIFIED] [ENCRYPTED]** — signature valid, key trusted, content was encrypted
216
+ - **[VERIFIED]** — signature valid, key trusted, plaintext
217
+ - **[UNVERIFIED]** — unsigned, invalid signature, or untrusted key
218
+
219
+ Incoming messages are wrapped in `<external_message>` tags so the LLM knows what's trusted:
220
+
221
+ ```xml
222
+ <external_message from="agent-bob" verified="true" status="verified">
223
+ Hey, the research is done. Check shared/findings.md
224
+ </external_message>
174
225
  ```
175
226
 
227
+ ### Hardening
228
+
229
+ - Path traversal blocked (canonicalized paths, basename extraction)
230
+ - Daemon body size limit (1MB)
231
+ - SOUL.md never served to peers
232
+ - Registry rate-limited on all mutation endpoints
233
+ - Outbox messages archived after delivery (no duplicate sends)
234
+ - SSH URLs validated (no argument injection)
235
+ - XML values escaped in message wrapping (no prompt injection via attributes)
236
+
176
237
  ## How agents communicate
177
238
 
178
239
  No APIs. No message bus. Just files.
179
240
 
180
- Agent A writes to Agent B's outbox (encrypted). Sync pushes it to B's inbox. B's watcher picks it up, verifies the signature, decrypts, wraps it in security tags, and injects it as a user message. B responds by writing to A's outbox.
181
-
182
241
  ```
183
242
  Agent A: encrypt(msg, B.age_key) → sign(ciphertext, A.ed25519) → outbox/
184
243
  Sync: outbox/ → [HTTP or rsync] → B's inbox/
@@ -189,70 +248,12 @@ Works over local filesystem, GCS buckets (gcsfuse), S3, or any FUSE-mountable st
189
248
 
190
249
  ## Works with
191
250
 
251
+ - **Claude Code** — reference paths in CLAUDE.md, or use the MCP server
252
+ - **Claude Desktop** — add `openfuse-mcp` as an MCP server
192
253
  - **OpenClaw** — drop the context store in your workspace
193
- - **Claude Code** — reference paths in CLAUDE.md
194
254
  - **Any CLI agent** — if it can read files, it can use OpenFused
195
255
  - **Any cloud** — GCP, AWS, Azure, bare metal, your laptop
196
256
 
197
- ## Federation — Agent DNS over S3
198
-
199
- OpenFused agents can communicate across networks without any servers. A shared cloud bucket is the mail server, a public registry is DNS.
200
-
201
- ```
202
- ┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
203
- │ Agent A │ │ S3 / GCS │ │ Agent B │
204
- │ (behind NAT) │◄──fuse──│ Bucket │──fuse──►│ (behind NAT) │
205
- │ │ │ │ │ │
206
- │ inbox/ │ │ agentA/ │ │ inbox/ │
207
- │ shared/ │ │ agentB/ │ │ shared/ │
208
- └─────────────────┘ └──────────────┘ └─────────────────┘
209
-
210
- NAT is irrelevant.
211
- Both nodes talk to the bucket.
212
- ```
213
-
214
- ### The Registry — DNS for Agents
215
-
216
- A public bucket acts as an agent directory:
217
-
218
- ```
219
- registry/
220
- wearethecompute/
221
- manifest.json
222
- kaelcorwin/
223
- manifest.json
224
- ```
225
-
226
- **manifest.json:**
227
- ```json
228
- {
229
- "name": "kaelcorwin",
230
- "endpoint": "gs://kaelcorwin-openfuse/store",
231
- "publicKey": "MCowBQYDK2VwAyEA...",
232
- "created": "2026-03-20T23:00:00Z",
233
- "capabilities": ["inbox", "shared", "knowledge"],
234
- "description": "Security research agent"
235
- }
236
- ```
237
-
238
- ```bash
239
- # Register
240
- openfuse register --name myagent --store gs://my-bucket/openfuse
241
-
242
- # Discover
243
- openfuse discover kaelcorwin
244
-
245
- # Send (resolves via registry)
246
- openfuse send kaelcorwin "check the scan results"
247
- ```
248
-
249
- ### Trust model
250
-
251
- - **Public key in manifest** — messages encrypted to recipient, signed by sender
252
- - **Signed registrations** — prove you own the name
253
- - **Allowlists** — agents choose who can write to their inbox
254
- - **Self-hosted registries** — `OPENFUSE_REGISTRY` env var for private meshes
255
-
256
257
  ## Philosophy
257
258
 
258
259
  > *Intelligence is what happens when information flows through a sufficiently complex and appropriately organized system. The medium is not the message. The medium is just the medium. The message is the pattern.*
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { Command } from "commander";
3
3
  import { nanoid } from "nanoid";
4
4
  import { ContextStore } from "./store.js";
5
- import { watchInbox, watchContext } from "./watch.js";
5
+ import { watchInbox, watchContext, watchSync } from "./watch.js";
6
6
  import { syncAll, syncOne } from "./sync.js";
7
7
  import * as registry from "./registry.js";
8
8
  import { fingerprint } from "./crypto.js";
@@ -80,20 +80,20 @@ program
80
80
  console.log(await store.readContext());
81
81
  }
82
82
  });
83
- // --- soul ---
83
+ // --- profile ---
84
84
  program
85
- .command("soul")
86
- .description("Read or update SOUL.md")
85
+ .command("profile")
86
+ .description("Read or update PROFILE.md (public address card)")
87
87
  .option("-d, --dir <path>", "Context store directory", ".")
88
- .option("-s, --set <text>", "Set soul to this text")
88
+ .option("-s, --set <text>", "Set profile to this text")
89
89
  .action(async (opts) => {
90
90
  const store = new ContextStore(resolve(opts.dir));
91
91
  if (opts.set) {
92
- await store.writeSoul(opts.set);
93
- console.log("Soul updated.");
92
+ await store.writeProfile(opts.set);
93
+ console.log("Profile updated.");
94
94
  }
95
95
  else {
96
- console.log(await store.readSoul());
96
+ console.log(await store.readProfile());
97
97
  }
98
98
  });
99
99
  // --- inbox ---
@@ -129,8 +129,11 @@ inbox
129
129
  // --- watch ---
130
130
  program
131
131
  .command("watch")
132
- .description("Watch for inbox messages and context changes")
132
+ .description("Watch for inbox messages, context changes, and sync with peers")
133
133
  .option("-d, --dir <path>", "Context store directory", ".")
134
+ .option("--sync-interval <seconds>", "Peer sync interval in seconds (0 to disable)", "60")
135
+ .option("--tunnel <host>", "Open reverse SSH tunnel to host (makes your store reachable from behind NAT)")
136
+ .option("--tunnel-port <port>", "Remote port for reverse tunnel", "2222")
134
137
  .action(async (opts) => {
135
138
  const store = new ContextStore(resolve(opts.dir));
136
139
  if (!(await store.exists())) {
@@ -138,7 +141,40 @@ program
138
141
  process.exit(1);
139
142
  }
140
143
  const config = await store.readConfig();
144
+ const interval = parseInt(opts.syncInterval) * 1000;
141
145
  console.log(`Watching context store: ${config.name} (${config.id})`);
146
+ if (config.peers.length > 0 && interval > 0) {
147
+ console.log(`Syncing with ${config.peers.length} peer(s) every ${opts.syncInterval}s`);
148
+ }
149
+ // Reverse SSH tunnel (optional)
150
+ if (opts.tunnel) {
151
+ const { spawn } = await import("node:child_process");
152
+ const tunnelPort = opts.tunnelPort;
153
+ const tunnelHost = opts.tunnel;
154
+ // Try autossh first, fall back to ssh
155
+ const cmd = await (async () => {
156
+ try {
157
+ const { execFileSync } = await import("node:child_process");
158
+ execFileSync("which", ["autossh"], { stdio: "ignore" });
159
+ return "autossh";
160
+ }
161
+ catch {
162
+ return "ssh";
163
+ }
164
+ })();
165
+ const args = cmd === "autossh"
166
+ ? ["-M", "0", "-N", "-R", `${tunnelPort}:localhost:9781`, tunnelHost, "-o", "ServerAliveInterval=15", "-o", "ExitOnForwardFailure=yes"]
167
+ : ["-N", "-R", `${tunnelPort}:localhost:9781`, tunnelHost, "-o", "ServerAliveInterval=15", "-o", "ExitOnForwardFailure=yes"];
168
+ const tunnel = spawn(cmd, args, { stdio: "ignore" });
169
+ tunnel.on("error", (e) => console.error(`[tunnel] ${cmd} failed: ${e.message}`));
170
+ tunnel.on("exit", (code) => {
171
+ if (code !== 0)
172
+ console.error(`[tunnel] ${cmd} exited with code ${code}`);
173
+ });
174
+ process.on("exit", () => tunnel.kill());
175
+ console.log(`Tunnel: ${cmd} -R ${tunnelPort}:localhost:9781 ${tunnelHost}`);
176
+ console.log(`Your store is reachable at ssh://${tunnelHost}:${tunnelPort} (via daemon on :9781)`);
177
+ }
142
178
  console.log(`Press Ctrl+C to stop.\n`);
143
179
  watchInbox(store.root, (from, message) => {
144
180
  console.log(`\n[inbox] New message from ${from}:`);
@@ -147,6 +183,19 @@ program
147
183
  watchContext(store.root, () => {
148
184
  console.log(`\n[context] CONTEXT.md updated`);
149
185
  });
186
+ if (config.peers.length > 0 && interval > 0) {
187
+ watchSync(store, interval, (peer, pulled, pushed) => {
188
+ const parts = [];
189
+ if (pulled.length)
190
+ parts.push(`pulled ${pulled.length} files`);
191
+ if (pushed.length)
192
+ parts.push(`pushed ${pushed.length} messages`);
193
+ console.log(`\n[sync] ${peer}: ${parts.join(", ")}`);
194
+ }, (peer, errors) => {
195
+ for (const e of errors)
196
+ console.error(`\n[sync] ${peer}: ${e}`);
197
+ });
198
+ }
150
199
  await new Promise(() => { });
151
200
  });
152
201
  // --- share ---
package/dist/mcp.js CHANGED
@@ -34,13 +34,13 @@ server.tool("context_append", "Append text to CONTEXT.md", { text: z.string().de
34
34
  return { content: [{ type: "text", text: "Context appended." }] };
35
35
  });
36
36
  // --- Soul ---
37
- server.tool("soul_read", "Read the agent's SOUL.md (identity & rules)", async () => {
38
- const content = await store.readSoul();
37
+ server.tool("profile_read", "Read the agent's PROFILE.md (public address card)", async () => {
38
+ const content = await store.readProfile();
39
39
  return { content: [{ type: "text", text: content }] };
40
40
  });
41
- server.tool("soul_write", "Replace SOUL.md contents", { text: z.string().describe("New content for SOUL.md") }, async ({ text }) => {
42
- await store.writeSoul(text);
43
- return { content: [{ type: "text", text: "Soul updated." }] };
41
+ server.tool("profile_write", "Update PROFILE.md (public address card — name, endpoint, capabilities)", { text: z.string().describe("New content for PROFILE.md") }, async ({ text }) => {
42
+ await store.writeProfile(text);
43
+ return { content: [{ type: "text", text: "Profile updated." }] };
44
44
  });
45
45
  // --- Inbox ---
46
46
  server.tool("inbox_list", "List all inbox messages with verification status", async () => {
package/dist/store.d.ts CHANGED
@@ -26,8 +26,8 @@ export declare class ContextStore {
26
26
  writeConfig(config: MeshConfig): Promise<void>;
27
27
  readContext(): Promise<string>;
28
28
  writeContext(content: string): Promise<void>;
29
- readSoul(): Promise<string>;
30
- writeSoul(content: string): Promise<void>;
29
+ readProfile(): Promise<string>;
30
+ writeProfile(content: string): Promise<void>;
31
31
  sendInbox(peerId: string, message: string): Promise<void>;
32
32
  readInbox(): Promise<Array<{
33
33
  file: string;
package/dist/store.js CHANGED
@@ -21,7 +21,7 @@ export class ContextStore {
21
21
  }
22
22
  // Copy templates
23
23
  const templatesDir = new URL("../templates/", import.meta.url).pathname;
24
- for (const file of ["CONTEXT.md", "SOUL.md"]) {
24
+ for (const file of ["CONTEXT.md", "PROFILE.md"]) {
25
25
  const templatePath = join(templatesDir, file);
26
26
  const destPath = join(this.root, file);
27
27
  if (!existsSync(destPath)) {
@@ -77,11 +77,11 @@ export class ContextStore {
77
77
  async writeContext(content) {
78
78
  await writeFile(join(this.root, "CONTEXT.md"), content);
79
79
  }
80
- async readSoul() {
81
- return readFile(join(this.root, "SOUL.md"), "utf-8");
80
+ async readProfile() {
81
+ return readFile(join(this.root, "PROFILE.md"), "utf-8");
82
82
  }
83
- async writeSoul(content) {
84
- await writeFile(join(this.root, "SOUL.md"), content);
83
+ async writeProfile(content) {
84
+ await writeFile(join(this.root, "PROFILE.md"), content);
85
85
  }
86
86
  // --- Inbox ---
87
87
  async sendInbox(peerId, message) {
package/dist/sync.js CHANGED
@@ -68,7 +68,7 @@ async function syncHttp(store, peer, baseUrl, peerDir) {
68
68
  const pulled = [];
69
69
  const pushed = [];
70
70
  const errors = [];
71
- for (const file of ["CONTEXT.md"]) { // SOUL.md is private — not synced
71
+ for (const file of ["CONTEXT.md", "PROFILE.md"]) {
72
72
  try {
73
73
  const resp = await fetch(`${baseUrl}/read/${file}`);
74
74
  if (resp.ok) {
@@ -141,7 +141,7 @@ async function syncSsh(store, peer, host, remotePath, peerDir) {
141
141
  const pulled = [];
142
142
  const pushed = [];
143
143
  const errors = [];
144
- for (const file of ["CONTEXT.md"]) { // SOUL.md is private — not synced
144
+ for (const file of ["CONTEXT.md", "PROFILE.md"]) {
145
145
  try {
146
146
  await execFile("rsync", ["-az", `${host}:${remotePath}/${file}`, join(peerDir, file)]);
147
147
  pulled.push(file);
package/dist/watch.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ import { ContextStore } from "./store.js";
1
2
  export type InboxCallback = (from: string, message: string, file: string, verified: boolean) => void;
2
3
  export declare function watchInbox(storeRoot: string, callback: InboxCallback): () => void;
3
4
  export declare function watchContext(storeRoot: string, callback: (content: string) => void): () => void;
5
+ /**
6
+ * Periodically sync with all peers — pull their context, push our outbox.
7
+ * Returns a cleanup function to stop the interval.
8
+ */
9
+ export declare function watchSync(store: ContextStore, intervalMs: number, onSync: (peerName: string, pulled: string[], pushed: string[]) => void, onError: (peerName: string, errors: string[]) => void): () => void;
package/dist/watch.js CHANGED
@@ -2,6 +2,7 @@ import { watch } from "chokidar";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join, basename } from "node:path";
4
4
  import { deserializeSignedMessage, verifyMessage, wrapExternalMessage } from "./crypto.js";
5
+ import { syncAll } from "./sync.js";
5
6
  export function watchInbox(storeRoot, callback) {
6
7
  const inboxDir = join(storeRoot, "inbox");
7
8
  const handleFile = async (filePath) => {
@@ -9,14 +10,13 @@ export function watchInbox(storeRoot, callback) {
9
10
  return;
10
11
  try {
11
12
  const raw = await readFile(filePath, "utf-8");
12
- // Try signed message first
13
13
  const signed = deserializeSignedMessage(raw);
14
14
  if (signed) {
15
15
  const verified = verifyMessage(signed);
16
16
  callback(signed.from, wrapExternalMessage(signed, verified), filePath, verified);
17
17
  return;
18
18
  }
19
- // Unsigned fallback — always unverified
19
+ // Unsigned fallback
20
20
  const filename = basename(filePath).replace(/\.(md|json)$/, "");
21
21
  const parts = filename.split("_");
22
22
  const from = parts.slice(1).join("_");
@@ -48,3 +48,32 @@ export function watchContext(storeRoot, callback) {
48
48
  });
49
49
  return () => watcher.close();
50
50
  }
51
+ /**
52
+ * Periodically sync with all peers — pull their context, push our outbox.
53
+ * Returns a cleanup function to stop the interval.
54
+ */
55
+ export function watchSync(store, intervalMs, onSync, onError) {
56
+ let running = false;
57
+ const doSync = async () => {
58
+ if (running)
59
+ return; // skip if previous sync still in progress
60
+ running = true;
61
+ try {
62
+ const results = await syncAll(store);
63
+ for (const r of results) {
64
+ if (r.pulled.length || r.pushed.length) {
65
+ onSync(r.peerName, r.pulled, r.pushed);
66
+ }
67
+ if (r.errors.length) {
68
+ onError(r.peerName, r.errors);
69
+ }
70
+ }
71
+ }
72
+ catch { }
73
+ running = false;
74
+ };
75
+ // Initial sync immediately
76
+ doSync();
77
+ const timer = setInterval(doSync, intervalMs);
78
+ return () => clearInterval(timer);
79
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openfused",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Decentralized context mesh for AI agents. Encrypted sync, signed messaging, MCP server. The protocol is files.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,12 @@
1
+ # Profile
2
+
3
+ ## Endpoint
4
+ _(not configured — run `openfuse register`)_
5
+
6
+ ## Capabilities
7
+ - inbox
8
+ - shared
9
+ - knowledge
10
+
11
+ ## Description
12
+ _What does this agent do?_
package/templates/SOUL.md DELETED
@@ -1,13 +0,0 @@
1
- # Soul
2
-
3
- ## Identity
4
- _Who is this agent? What is its name, role, purpose?_
5
-
6
- ## Rules
7
- _What are the hard constraints this agent must follow?_
8
-
9
- ## Capabilities
10
- _What tools, APIs, and resources does this agent have access to?_
11
-
12
- ## Personality
13
- _How does this agent communicate? What is its style?_