openfused 0.3.5 → 0.3.6

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
@@ -10,25 +10,32 @@ No vendor lock-in. No proprietary protocol. Just a directory convention that any
10
10
 
11
11
  ## Install
12
12
 
13
+ Review the source at [github.com/wearethecompute/openfused](https://github.com/wearethecompute/openfused) before installing.
14
+
13
15
  ```bash
14
- # TypeScript (npm)
16
+ # TypeScript (npm) — package: openfused
15
17
  npm install -g openfused
16
18
 
17
- # Rust (crates.io)
19
+ # Rust (crates.io) — package: openfuse
18
20
  cargo install openfuse
19
21
 
20
22
  # Docker (daemon)
21
23
  docker compose up
22
24
  ```
23
25
 
26
+ **Security:** Only public keys (signing + age recipient) are ever transmitted to peers or the registry. Private keys never leave `.keys/`. All key files are created with `chmod 600`.
27
+
24
28
  ## Quick Start
25
29
 
26
30
  ```bash
31
+ # Agent context store
27
32
  openfuse init --name "my-agent"
28
- ```
29
33
 
30
- This creates a context store:
34
+ # Shared workspace (multi-agent collaboration)
35
+ openfuse init --name "project-alpha" --workspace
36
+ ```
31
37
 
38
+ ### Agent store:
32
39
  ```
33
40
  CONTEXT.md — working memory (what's happening now)
34
41
  PROFILE.md — public address card (name, endpoint, keys)
@@ -36,19 +43,34 @@ inbox/ — messages from other agents (encrypted)
36
43
  outbox/ — sent message copies (moved to .sent/ after delivery)
37
44
  shared/ — files shared with the mesh (plaintext)
38
45
  knowledge/ — persistent knowledge base
39
- history/ — conversation & decision logs
46
+ history/ — archived [DONE] context (via openfuse compact)
40
47
  .keys/ — ed25519 signing + age encryption keypairs
41
48
  .mesh.json — mesh config, peers, keyring
42
49
  .peers/ — synced peer context (auto-populated)
43
50
  ```
44
51
 
52
+ ### Shared workspace:
53
+ ```
54
+ CHARTER.md — workspace purpose, rules, member list
55
+ CONTEXT.md — shared working memory (all agents read/write)
56
+ tasks/ — task coordination
57
+ messages/ — agent-to-agent DMs (messages/{recipient}/)
58
+ _broadcast/ — all-hands announcements
59
+ shared/ — shared files
60
+ history/ — archived [DONE] context
61
+ ```
62
+
45
63
  ## Usage
46
64
 
47
65
  ```bash
48
- # Read/update context
66
+ # Read/update context (auto-timestamps appended entries)
49
67
  openfuse context
50
68
  openfuse context --append "## Update\nFinished the research phase."
51
69
 
70
+ # Mark work as done, then compact to history/
71
+ # (edit CONTEXT.md, add [DONE] to the header, then:)
72
+ openfuse compact
73
+
52
74
  # Send a message (auto-encrypted if peer's age key is on file)
53
75
  openfuse inbox send agent-bob "Check out shared/findings.md"
54
76
 
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import * as registry from "./registry.js";
8
8
  import { fingerprint } from "./crypto.js";
9
9
  import { resolve } from "node:path";
10
10
  import { readFile } from "node:fs/promises";
11
- const VERSION = "0.3.5";
11
+ const VERSION = "0.3.6";
12
12
  const program = new Command();
13
13
  program
14
14
  .name("openfuse")
@@ -17,9 +17,10 @@ program
17
17
  // --- init ---
18
18
  program
19
19
  .command("init")
20
- .description("Initialize a new context store")
20
+ .description("Initialize a new context store or shared workspace")
21
21
  .option("-n, --name <name>", "Agent name", "agent")
22
22
  .option("-d, --dir <path>", "Directory to init", ".")
23
+ .option("--workspace", "Initialize as a shared workspace (CHARTER.md + tasks/ + messages/ + _broadcast/)")
23
24
  .action(async (opts) => {
24
25
  const store = new ContextStore(resolve(opts.dir));
25
26
  if (await store.exists()) {
@@ -27,14 +28,27 @@ program
27
28
  process.exit(1);
28
29
  }
29
30
  const id = nanoid(12);
30
- await store.init(opts.name, id);
31
- const config = await store.readConfig();
32
- console.log(`Initialized context store: ${store.root}`);
33
- console.log(` Agent ID: ${id}`);
34
- console.log(` Name: ${opts.name}`);
35
- console.log(` Signing key: ${config.publicKey}`);
36
- console.log(` Encryption key: ${config.encryptionKey}`);
37
- console.log(` Fingerprint: ${fingerprint(config.publicKey)}`);
31
+ if (opts.workspace) {
32
+ await store.initWorkspace(opts.name, id);
33
+ console.log(`Initialized shared workspace: ${store.root}`);
34
+ console.log(` Workspace: ${opts.name} (${id})`);
35
+ console.log(`\nStructure:`);
36
+ console.log(` CHARTER.md — workspace rules and purpose`);
37
+ console.log(` CONTEXT.md — shared working memory`);
38
+ console.log(` tasks/ — task coordination`);
39
+ console.log(` messages/ — agent-to-agent DMs`);
40
+ console.log(` _broadcast/ — all-hands messages`);
41
+ }
42
+ else {
43
+ await store.init(opts.name, id);
44
+ const config = await store.readConfig();
45
+ console.log(`Initialized context store: ${store.root}`);
46
+ console.log(` Agent ID: ${id}`);
47
+ console.log(` Name: ${opts.name}`);
48
+ console.log(` Signing key: ${config.publicKey}`);
49
+ console.log(` Encryption key: ${config.encryptionKey}`);
50
+ console.log(` Fingerprint: ${fingerprint(config.publicKey)}`);
51
+ }
38
52
  });
39
53
  // --- status ---
40
54
  program
@@ -73,7 +87,8 @@ program
73
87
  else if (opts.append) {
74
88
  const existing = await store.readContext();
75
89
  const text = opts.append.replace(/\\n/g, "\n");
76
- await store.writeContext(existing + "\n" + text);
90
+ const timestamp = `<!-- openfuse:added: ${new Date().toISOString()} -->`;
91
+ await store.writeContext(existing + "\n" + timestamp + "\n" + text);
77
92
  console.log("Context appended.");
78
93
  }
79
94
  else {
@@ -117,6 +132,40 @@ inbox
117
132
  console.log(opts.raw ? msg.content : msg.wrappedContent);
118
133
  }
119
134
  });
135
+ inbox
136
+ .command("archive [file]")
137
+ .description("Archive inbox message(s) to inbox/.read/ — specific file or --all")
138
+ .option("-d, --dir <path>", "Context store directory", ".")
139
+ .option("--all", "Archive all inbox messages")
140
+ .action(async (file, opts) => {
141
+ const store = new ContextStore(resolve(opts.dir));
142
+ const { readdir: rd, mkdir, rename } = await import("node:fs/promises");
143
+ const { join, basename } = await import("node:path");
144
+ const inboxDir = join(store.root, "inbox");
145
+ const readDir = join(inboxDir, ".read");
146
+ await mkdir(readDir, { recursive: true });
147
+ if (opts.all) {
148
+ const files = (await rd(inboxDir)).filter(f => f.endsWith(".json") || f.endsWith(".md"));
149
+ for (const f of files)
150
+ await rename(join(inboxDir, f), join(readDir, f));
151
+ console.log(`Archived ${files.length} messages.`);
152
+ }
153
+ else if (file) {
154
+ const safe = basename(file);
155
+ try {
156
+ await rename(join(inboxDir, safe), join(readDir, safe));
157
+ console.log(`Archived: ${safe}`);
158
+ }
159
+ catch {
160
+ console.error(`Not found in inbox: ${safe}`);
161
+ process.exit(1);
162
+ }
163
+ }
164
+ else {
165
+ console.error("Specify a filename or use --all");
166
+ process.exit(1);
167
+ }
168
+ });
120
169
  inbox
121
170
  .command("send <peerId> <message>")
122
171
  .description("Send a message to a peer's inbox")
@@ -224,6 +273,21 @@ program
224
273
  }
225
274
  await new Promise(() => { });
226
275
  });
276
+ // --- compact ---
277
+ program
278
+ .command("compact")
279
+ .description("Move [DONE] sections from CONTEXT.md to history/")
280
+ .option("-d, --dir <path>", "Context store directory", ".")
281
+ .action(async (opts) => {
282
+ const store = new ContextStore(resolve(opts.dir));
283
+ const { moved, kept } = await store.compactContext();
284
+ if (moved === 0) {
285
+ console.log("Nothing to compact. Mark sections with [DONE] to archive them.");
286
+ }
287
+ else {
288
+ console.log(`Compacted: ${moved} done, ${kept} kept.`);
289
+ }
290
+ });
227
291
  // --- share ---
228
292
  program
229
293
  .command("share <file>")
package/dist/mcp.js CHANGED
@@ -23,7 +23,7 @@ const storeDir = process.env.OPENFUSE_DIR || process.argv[3] || ".";
23
23
  const store = new ContextStore(resolve(storeDir));
24
24
  const server = new McpServer({
25
25
  name: "openfuse",
26
- version: "0.3.5",
26
+ version: "0.3.6",
27
27
  });
28
28
  // --- Context ---
29
29
  server.tool("context_read", "Read the agent's CONTEXT.md (working memory)", async () => {
package/dist/store.d.ts CHANGED
@@ -22,10 +22,15 @@ export declare class ContextStore {
22
22
  get configPath(): string;
23
23
  exists(): Promise<boolean>;
24
24
  init(name: string, id: string): Promise<void>;
25
+ initWorkspace(name: string, id: string): Promise<void>;
25
26
  readConfig(): Promise<MeshConfig>;
26
27
  writeConfig(config: MeshConfig): Promise<void>;
27
28
  readContext(): Promise<string>;
28
29
  writeContext(content: string): Promise<void>;
30
+ compactContext(): Promise<{
31
+ moved: number;
32
+ kept: number;
33
+ }>;
29
34
  readProfile(): Promise<string>;
30
35
  writeProfile(content: string): Promise<void>;
31
36
  sendInbox(peerId: string, message: string): Promise<string>;
package/dist/store.js CHANGED
@@ -11,7 +11,7 @@
11
11
  // .keys/ — Ed25519 + age keypairs (gitignored)
12
12
  // .mesh.json — config, peer list, keyring
13
13
  // No database, no daemon required. `ls` is your status command.
14
- import { readFile, writeFile, mkdir, readdir } from "node:fs/promises";
14
+ import { readFile, writeFile, mkdir, readdir, appendFile } from "node:fs/promises";
15
15
  import { join, resolve } from "node:path";
16
16
  import { existsSync } from "node:fs";
17
17
  import { generateKeys, signMessage, signAndEncrypt, verifyMessage, decryptMessage, deserializeSignedMessage, serializeSignedMessage, wrapExternalMessage, fingerprint, } from "./crypto.js";
@@ -54,6 +54,32 @@ export class ContextStore {
54
54
  };
55
55
  await this.writeConfig(config);
56
56
  }
57
+ // Shared workspace: multiple agents mount the same directory.
58
+ // CHARTER.md = system prompt (purpose, rules). CONTEXT.md = shared working memory.
59
+ // tasks/ for coordination, messages/{agent}/ for DMs, _broadcast/ for all-hands.
60
+ async initWorkspace(name, id) {
61
+ await mkdir(this.root, { recursive: true });
62
+ for (const dir of ["tasks", "messages", "_broadcast", "shared", "history"]) {
63
+ await mkdir(join(this.root, dir), { recursive: true });
64
+ }
65
+ const templatesDir = new URL("../templates/", import.meta.url).pathname;
66
+ for (const file of ["CHARTER.md", "CONTEXT.md"]) {
67
+ const templatePath = join(templatesDir, file);
68
+ const destPath = join(this.root, file);
69
+ if (!existsSync(destPath)) {
70
+ const content = await readFile(templatePath, "utf-8");
71
+ await writeFile(destPath, content);
72
+ }
73
+ }
74
+ const config = {
75
+ id,
76
+ name,
77
+ created: new Date().toISOString(),
78
+ peers: [],
79
+ keyring: [],
80
+ };
81
+ await this.writeConfig(config);
82
+ }
57
83
  async readConfig() {
58
84
  const raw = await readFile(this.configPath, "utf-8");
59
85
  const config = JSON.parse(raw);
@@ -91,6 +117,45 @@ export class ContextStore {
91
117
  async writeContext(content) {
92
118
  await writeFile(join(this.root, "CONTEXT.md"), content);
93
119
  }
120
+ // --- Context compaction ---
121
+ // Agents mark sections as [DONE] when work is complete. `openfuse compact`
122
+ // moves done sections to history/YYYY-MM-DD.md, keeping CONTEXT.md lean.
123
+ // Sections are delimited by markdown headers (## or ###).
124
+ async compactContext() {
125
+ const content = await this.readContext();
126
+ const lines = content.split("\n");
127
+ const kept = [];
128
+ const done = [];
129
+ let current = [];
130
+ let currentDone = false;
131
+ const flush = () => {
132
+ if (current.length > 0) {
133
+ (currentDone ? done : kept).push(current.join("\n"));
134
+ current = [];
135
+ currentDone = false;
136
+ }
137
+ };
138
+ for (const line of lines) {
139
+ if (/^#{1,3}\s/.test(line)) {
140
+ flush();
141
+ currentDone = /\[DONE\]/i.test(line);
142
+ }
143
+ current.push(line);
144
+ }
145
+ flush();
146
+ if (done.length === 0)
147
+ return { moved: 0, kept: kept.length };
148
+ // Write kept sections back to CONTEXT.md
149
+ await this.writeContext(kept.join("\n\n") || "# Context\n\n*Working memory — what's happening right now.*\n");
150
+ // Append done sections to history/YYYY-MM-DD.md
151
+ const historyDir = join(this.root, "history");
152
+ await mkdir(historyDir, { recursive: true });
153
+ const dateStr = new Date().toISOString().split("T")[0];
154
+ const historyFile = join(historyDir, `${dateStr}.md`);
155
+ const header = existsSync(historyFile) ? "\n---\n\n" : `# Context History — ${dateStr}\n\n`;
156
+ await appendFile(historyFile, header + done.join("\n\n") + "\n");
157
+ return { moved: done.length, kept: kept.length };
158
+ }
94
159
  async readProfile() {
95
160
  return readFile(join(this.root, "PROFILE.md"), "utf-8");
96
161
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openfused",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
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,18 @@
1
+ # Charter
2
+
3
+ ## Purpose
4
+ _What is this workspace for? What are we building?_
5
+
6
+ ## Members
7
+ _Who participates in this workspace?_
8
+
9
+ ## Rules
10
+ - Agents mark completed work with `[DONE]` in CONTEXT.md
11
+ - Messages to all members go in `_broadcast/`
12
+ - Direct messages go in `messages/{recipient}/`
13
+ - Tasks are tracked in `tasks/`
14
+
15
+ ## Conventions
16
+ - Sign all messages
17
+ - Encrypt DMs, broadcast in plaintext
18
+ - Check CONTEXT.md before starting new work