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 +99 -98
- package/dist/cli.js +58 -9
- package/dist/mcp.js +5 -5
- package/dist/store.d.ts +2 -2
- package/dist/store.js +5 -5
- package/dist/sync.js +2 -2
- package/dist/watch.d.ts +6 -0
- package/dist/watch.js +31 -2
- package/package.json +1 -1
- package/templates/PROFILE.md +12 -0
- package/templates/SOUL.md +0 -13
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OpenFused
|
|
2
2
|
|
|
3
|
-
Decentralized context mesh for AI agents.
|
|
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
|
-
##
|
|
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
|
|
158
|
+
# Sync
|
|
121
159
|
openfuse sync
|
|
122
|
-
|
|
123
|
-
# Sync one peer
|
|
124
|
-
openfuse sync wisp
|
|
125
160
|
```
|
|
126
161
|
|
|
127
|
-
Sync pulls: `CONTEXT.md`, `
|
|
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
|
|
165
|
+
SSH transport uses hostnames from `~/.ssh/config` — not raw IPs.
|
|
131
166
|
|
|
132
|
-
##
|
|
167
|
+
## MCP Server
|
|
133
168
|
|
|
134
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
182
|
+
13 tools: `context_read/write/append`, `soul_read/write`, `inbox_list/send`, `shared_list/read/write`, `status`, `peer_list/add`.
|
|
149
183
|
|
|
150
|
-
|
|
184
|
+
## Docker
|
|
151
185
|
|
|
152
186
|
```bash
|
|
153
|
-
#
|
|
154
|
-
|
|
187
|
+
# Daemon only (LAN/VPS — public IP or port forwarding)
|
|
188
|
+
docker compose up
|
|
155
189
|
|
|
156
|
-
#
|
|
157
|
-
|
|
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
|
|
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
|
-
##
|
|
202
|
+
## Reachability
|
|
167
203
|
|
|
168
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
// ---
|
|
83
|
+
// --- profile ---
|
|
84
84
|
program
|
|
85
|
-
.command("
|
|
86
|
-
.description("Read or update
|
|
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
|
|
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.
|
|
93
|
-
console.log("
|
|
92
|
+
await store.writeProfile(opts.set);
|
|
93
|
+
console.log("Profile updated.");
|
|
94
94
|
}
|
|
95
95
|
else {
|
|
96
|
-
console.log(await store.
|
|
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
|
|
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("
|
|
38
|
-
const content = await store.
|
|
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("
|
|
42
|
-
await store.
|
|
43
|
-
return { content: [{ type: "text", text: "
|
|
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
|
-
|
|
30
|
-
|
|
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", "
|
|
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
|
|
81
|
-
return readFile(join(this.root, "
|
|
80
|
+
async readProfile() {
|
|
81
|
+
return readFile(join(this.root, "PROFILE.md"), "utf-8");
|
|
82
82
|
}
|
|
83
|
-
async
|
|
84
|
-
await writeFile(join(this.root, "
|
|
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"]) {
|
|
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"]) {
|
|
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
|
|
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
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?_
|