@dtudury/streamo 0.1.1 → 0.2.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.
- package/.claude/settings.json +1 -0
- package/.claude/settings.local.json +5 -1
- package/.env.dev +4 -0
- package/CLAUDE.md +73 -0
- package/README.md +45 -23
- package/ROADMAP.md +85 -50
- package/bin/streamo.js +33 -44
- package/jsconfig.json +3 -2
- package/package.json +4 -3
- package/public/apps/chat/main.js +70 -87
- package/public/apps/chat/server.js +35 -0
- package/public/streamo/Repo.js +39 -1
- package/public/streamo/StreamoComponent.js +92 -0
- package/public/streamo/StreamoServer.js +71 -0
- package/public/streamo/chat-cli.js +4 -3
- package/public/streamo/mount.js +69 -20
- package/public/streamo/webSync.js +2 -2
- package/public/streamo/chat-server.js +0 -60
- package/scripts/serve.js +0 -15
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
"Bash(git add *)",
|
|
5
5
|
"Bash(git commit -m ' *)",
|
|
6
6
|
"Bash(git push *)",
|
|
7
|
-
"Bash(gh repo *)"
|
|
7
|
+
"Bash(gh repo *)",
|
|
8
|
+
"Bash(git rm *)",
|
|
9
|
+
"Bash(node --test)",
|
|
10
|
+
"Bash(node --input-type=module --eval \"import './bin/streamo.js'\" --help)",
|
|
11
|
+
"Bash(node bin/streamo.js --help)"
|
|
8
12
|
]
|
|
9
13
|
}
|
|
10
14
|
}
|
package/.env.dev
ADDED
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# streamo — Claude context
|
|
2
|
+
|
|
3
|
+
## what this project is
|
|
4
|
+
|
|
5
|
+
`@dtudury/streamo` is a peer-to-peer sync library with a reactive UI layer. The central
|
|
6
|
+
promise: no server holds authority over your data or your identity. Keys are derived
|
|
7
|
+
deterministically from credentials (no files to manage), every write is signed and
|
|
8
|
+
append-only, and the server is a relay — not a gatekeeper.
|
|
9
|
+
|
|
10
|
+
## the face of the project
|
|
11
|
+
|
|
12
|
+
These files are how people discover and understand streamo. Keep them in sync
|
|
13
|
+
after any meaningful change — not as an afterthought, but as part of the work:
|
|
14
|
+
|
|
15
|
+
- **`README.md`** — npm/GitHub landing page; imports, framing, and examples must reflect
|
|
16
|
+
the current package name (`@dtudury/streamo`) and current capabilities
|
|
17
|
+
- **`public/index.html`** — browser homepage; feature list and app cards should match
|
|
18
|
+
the README's framing
|
|
19
|
+
- **`package.json`** — version, name (`@dtudury/streamo`), description, and keywords;
|
|
20
|
+
version bumps immediately after the user says they've published
|
|
21
|
+
- **`ROADMAP.md`** — public on GitHub; mark items done when they ship, update "start
|
|
22
|
+
here" to the next priority, keep the "toward 1.0" list current
|
|
23
|
+
|
|
24
|
+
Stale public-facing docs erode trust faster than bugs do.
|
|
25
|
+
|
|
26
|
+
## publish rhythm
|
|
27
|
+
|
|
28
|
+
The user publishes to npm manually and notifies Claude when done. The moment they say
|
|
29
|
+
they've published, bump the patch version in `package.json`, commit, and push — before
|
|
30
|
+
starting any other work. This ritual matters to them.
|
|
31
|
+
|
|
32
|
+
## language and framing
|
|
33
|
+
|
|
34
|
+
Use this framing consistently across all public-facing text:
|
|
35
|
+
|
|
36
|
+
1. **No server holds authority** — the server is a relay, not a gatekeeper
|
|
37
|
+
2. **Your identity travels with you** — same credentials, same keypair, everywhere; no
|
|
38
|
+
key files, no seed phrases, no backup ritual
|
|
39
|
+
3. **Every write is provably yours** — signed commits, append-only, permanent
|
|
40
|
+
4. **Content-addressed** — data identified by what it is, not where it lives
|
|
41
|
+
|
|
42
|
+
"Content-addressed" is technically important but not the lead. Start with ownership.
|
|
43
|
+
|
|
44
|
+
## architecture notes
|
|
45
|
+
|
|
46
|
+
- `Streamo` — content-addressed, append-only byte store with self-describing codec
|
|
47
|
+
- `Repo` — wraps Streamo; every `set()` is a signed commit (message, date, address, parent)
|
|
48
|
+
- `Signer` — deterministic secp256k1 keypairs via PBKDF2 from username + password
|
|
49
|
+
- `Recaller` — fine-grained reactive dependency tracker; `watch(name, f)` / `unwatch(f)`
|
|
50
|
+
- `h` — tagged template literal HTML parser → HElement / HText virtual tree
|
|
51
|
+
- `mount` — reactive DOM renderer; slots are reactive cells; elements recycled by
|
|
52
|
+
`data-key` then tag on re-render; removed nodes cleaned up via `recaller.unwatch()`;
|
|
53
|
+
exports `dismount(root, recaller)` for custom element cleanup
|
|
54
|
+
- `StreamoComponent` — base class for hot-reloadable custom elements; `componentKey`
|
|
55
|
+
generates address-based names; `defineComponent` registers render functions; function
|
|
56
|
+
components `(props) => nodes` work directly as tags in `h` with no class needed
|
|
57
|
+
- `registrySync` — bidirectional multi-repo sync over a single WebSocket; works in Node
|
|
58
|
+
and browser; content-driven discovery via `follow`
|
|
59
|
+
- CLI `--chat-room` flag — when combined with `--web`, auto-accepts member announcements
|
|
60
|
+
and stores the member list in the server's own repo (backed by `archiveSync`);
|
|
61
|
+
the server's public key is the room key; `chat-server.js` is retired
|
|
62
|
+
|
|
63
|
+
## what's next (toward 1.0)
|
|
64
|
+
|
|
65
|
+
1. Chat signing — wire `repo.sign()` so messages are cryptographically verified
|
|
66
|
+
2. Rebuild the browser app with `h` / `mount`
|
|
67
|
+
|
|
68
|
+
## commit style
|
|
69
|
+
|
|
70
|
+
Commit and push at the end of every response that makes a change. Over-commit rather
|
|
71
|
+
than over-think. Co-author line on every commit:
|
|
72
|
+
|
|
73
|
+
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
package/README.md
CHANGED
|
@@ -2,25 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
> every device is an equal author
|
|
4
4
|
|
|
5
|
-
Streamo is a
|
|
5
|
+
Streamo is a peer-to-peer sync library built around a simple promise: **no server holds authority over your data or your identity.** The server is a relay, not a gatekeeper. Your keypair is your identity — derived from your credentials, not stored in a file. Your commit log is the source of truth, and every connected peer sees the same history.
|
|
6
6
|
|
|
7
7
|
## core ideas
|
|
8
8
|
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
9
|
+
- **No server holds authority** — the server is a relay; your data lives on your devices and can't be seized or censored. Disconnect the server and everything is still yours.
|
|
10
|
+
- **Your identity travels with you** — keys are derived with PBKDF2 from your username and password. Same credentials, same keypair, everywhere — no key files, no seed phrases, no backup ritual.
|
|
11
|
+
- **Every write is provably yours** — commits are signed with your keypair and append-only. History is permanent and can't be forged; peers reject unsigned or mis-signed data.
|
|
12
|
+
- **Content-addressed** — data is identified by what it is, not where it lives. The same value always lands at the same address; deduplication and diffing are structural.
|
|
13
13
|
|
|
14
14
|
## install
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
npm install streamo
|
|
17
|
+
npm install @dtudury/streamo
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
Or run the CLI directly:
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
npx streamo --help
|
|
23
|
+
npx @dtudury/streamo --help
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
## cli
|
|
@@ -56,8 +56,8 @@ streamo --env-file .env
|
|
|
56
56
|
### Streamo — reactive append-only store
|
|
57
57
|
|
|
58
58
|
```js
|
|
59
|
-
import { Streamo } from 'streamo/public/streamo/Streamo.js'
|
|
60
|
-
import { Recaller } from 'streamo/public/streamo/utils/Recaller.js'
|
|
59
|
+
import { Streamo } from '@dtudury/streamo/public/streamo/Streamo.js'
|
|
60
|
+
import { Recaller } from '@dtudury/streamo/public/streamo/utils/Recaller.js'
|
|
61
61
|
|
|
62
62
|
const store = new Streamo()
|
|
63
63
|
store.set({ name: 'alice', score: 42 })
|
|
@@ -72,20 +72,23 @@ Values are encoded with a self-describing codec (strings, numbers, dates, boolea
|
|
|
72
72
|
`Repo` wraps a `Streamo` so every `set()` becomes a commit — message, date, data address, and parent pointer. The raw commit log is what syncs over the wire.
|
|
73
73
|
|
|
74
74
|
```js
|
|
75
|
-
import { Repo } from 'streamo/public/streamo/Repo.js'
|
|
75
|
+
import { Repo } from '@dtudury/streamo/public/streamo/Repo.js'
|
|
76
76
|
|
|
77
77
|
const repo = new Repo()
|
|
78
|
+
repo.attachSigner(signer, 'my-dataset') // auto-sign every commit
|
|
78
79
|
repo.set({ name: 'alice', messages: [] })
|
|
79
80
|
repo.get('name') // 'alice'
|
|
80
81
|
repo.lastCommit // { message: '', date: Date, dataAddress: n, parent: n|undefined }
|
|
81
82
|
[...repo.history()] // newest-first iterator over commits
|
|
82
83
|
```
|
|
83
84
|
|
|
85
|
+
Signature chunks travel in the byte stream automatically — peers running `registrySync` or `originSync` verify every signature on receipt and reject data that doesn't match the repo's public key.
|
|
86
|
+
|
|
84
87
|
### Signer — deterministic identity
|
|
85
88
|
|
|
86
89
|
```js
|
|
87
|
-
import { Signer } from 'streamo/public/streamo/Signer.js'
|
|
88
|
-
import { bytesToHex } from 'streamo/public/streamo/utils.js'
|
|
90
|
+
import { Signer } from '@dtudury/streamo/public/streamo/Signer.js'
|
|
91
|
+
import { bytesToHex } from '@dtudury/streamo/public/streamo/utils.js'
|
|
89
92
|
|
|
90
93
|
const signer = new Signer('alice', 'my-password')
|
|
91
94
|
const { publicKey } = await signer.keysFor('my-dataset')
|
|
@@ -97,8 +100,8 @@ Keys are derived with PBKDF2 so the same username + password always produces the
|
|
|
97
100
|
### RepoRegistry — multi-repo store
|
|
98
101
|
|
|
99
102
|
```js
|
|
100
|
-
import { RepoRegistry } from 'streamo/public/streamo/RepoRegistry.js'
|
|
101
|
-
import { archiveSync } from 'streamo/public/streamo/archiveSync.js'
|
|
103
|
+
import { RepoRegistry } from '@dtudury/streamo/public/streamo/RepoRegistry.js'
|
|
104
|
+
import { archiveSync } from '@dtudury/streamo/public/streamo/archiveSync.js'
|
|
102
105
|
|
|
103
106
|
const registry = new RepoRegistry(async key => {
|
|
104
107
|
const repo = new Repo()
|
|
@@ -112,7 +115,7 @@ const repo = await registry.open(publicKeyHex)
|
|
|
112
115
|
### registrySync — peer sync over WebSocket
|
|
113
116
|
|
|
114
117
|
```js
|
|
115
|
-
import { registrySync } from 'streamo/public/streamo/registrySync.js'
|
|
118
|
+
import { registrySync } from '@dtudury/streamo/public/streamo/registrySync.js'
|
|
116
119
|
|
|
117
120
|
const session = await registrySync(registry, 'localhost', 8080, {
|
|
118
121
|
// only sync repos you care about
|
|
@@ -134,9 +137,9 @@ session.announce(myKey, rootKey) // tell interested peers about your repo
|
|
|
134
137
|
### h + mount — reactive UI
|
|
135
138
|
|
|
136
139
|
```js
|
|
137
|
-
import { h } from 'streamo/public/streamo/h.js'
|
|
138
|
-
import { mount } from 'streamo/public/streamo/mount.js'
|
|
139
|
-
import { Recaller } from 'streamo/public/streamo/utils/Recaller.js'
|
|
140
|
+
import { h } from '@dtudury/streamo/public/streamo/h.js'
|
|
141
|
+
import { mount } from '@dtudury/streamo/public/streamo/mount.js'
|
|
142
|
+
import { Recaller } from '@dtudury/streamo/public/streamo/utils/Recaller.js'
|
|
140
143
|
|
|
141
144
|
const recaller = new Recaller('app')
|
|
142
145
|
|
|
@@ -148,7 +151,21 @@ mount(h`
|
|
|
148
151
|
`, document.body, recaller)
|
|
149
152
|
```
|
|
150
153
|
|
|
151
|
-
Functions interpolated as `${() => ...}` are reactive cells — they re-run automatically whenever the data they read changes. No virtual DOM diffing; only the exact DOM nodes bound to changed data update.
|
|
154
|
+
Functions interpolated as `${() => ...}` are reactive cells — they re-run automatically whenever the data they read changes. No virtual DOM diffing; only the exact DOM nodes bound to changed data update. Elements are recycled across re-renders by `data-key` (or tag as a fallback), so user input and focus survive list reorders. SVG namespaces propagate automatically — `` h`<svg><path d="..."/></svg>` `` works without any extra wiring. `class` accepts an array (`['btn', isActive && 'active']`) or an object (`{btn: true, active: false}`); falsy entries are filtered out.
|
|
155
|
+
|
|
156
|
+
Any function can be used directly as a tag — it receives `{ ...attrs, children }` and returns virtual nodes:
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
import { StreamoComponent, componentKey, defineComponent } from '@dtudury/streamo/public/streamo/StreamoComponent.js'
|
|
160
|
+
|
|
161
|
+
function Card ({ title, children }) {
|
|
162
|
+
return h`<div class="card"><h2>${title}</h2>${children}</div>`
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
mount(h`<${Card} title="Hello"><p>hi</p></${Card}>`, document.body, recaller)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
For hot-reloading, `componentKey(prefix, address)` and `defineComponent(name, fn)` pair a content address to a unique custom element name. A new file version gets a new name; stale elements are naturally orphaned and cleaned up without any explicit bookkeeping.
|
|
152
169
|
|
|
153
170
|
## sync backends
|
|
154
171
|
|
|
@@ -165,17 +182,18 @@ Functions interpolated as `${() => ...}` are reactive cells — they re-run auto
|
|
|
165
182
|
## chat example
|
|
166
183
|
|
|
167
184
|
```bash
|
|
168
|
-
# start the server
|
|
169
|
-
|
|
185
|
+
# start the server — its public key becomes the room key
|
|
186
|
+
STREAMO_NAME=my-chat STREAMO_USERNAME=relay STREAMO_PASSWORD=secret \
|
|
187
|
+
node public/apps/chat/server.js
|
|
170
188
|
|
|
171
189
|
# join from the browser
|
|
172
|
-
open http://localhost:8080
|
|
190
|
+
open http://localhost:8080/apps/chat/
|
|
173
191
|
|
|
174
192
|
# join from the terminal
|
|
175
193
|
node public/streamo/chat-cli.js alice secret localhost 8080
|
|
176
194
|
```
|
|
177
195
|
|
|
178
|
-
Each participant owns their own message stream. The server holds
|
|
196
|
+
Each participant owns their own message stream. The server is just another streamo node — it holds the member list in its own repo and auto-accepts anyone who announces to it. Its public key is the room address. No special authority, no hidden state.
|
|
179
197
|
|
|
180
198
|
## tests
|
|
181
199
|
|
|
@@ -189,6 +207,10 @@ node --test public/streamo/Repo.test.js # single file
|
|
|
189
207
|
See [ROADMAP.md](./ROADMAP.md) for what's been built, what's next, and what we're
|
|
190
208
|
aiming at for 1.0.
|
|
191
209
|
|
|
210
|
+
## collaboration
|
|
211
|
+
|
|
212
|
+
Built with significant AI collaboration via [Claude Code](https://claude.ai/code). Human-directed; Claude is a co-author and contributor, not an autonomous builder.
|
|
213
|
+
|
|
192
214
|
## license
|
|
193
215
|
|
|
194
216
|
AGPL-3.0-only
|
package/ROADMAP.md
CHANGED
|
@@ -5,7 +5,7 @@ picture of where the project is and where it's headed.
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
## where we are (0.
|
|
8
|
+
## where we are (0.2.0)
|
|
9
9
|
|
|
10
10
|
The foundation is solid and working. Here's what's in:
|
|
11
11
|
|
|
@@ -14,7 +14,8 @@ The foundation is solid and working. Here's what's in:
|
|
|
14
14
|
self-describing codec. Same value always encodes to the same bytes; dedup and
|
|
15
15
|
diffing are free.
|
|
16
16
|
- `Repo` — every write is a signed commit. Message, date, data address, parent.
|
|
17
|
-
The full history is always there.
|
|
17
|
+
The full history is always there. `attachSigner(signer, name)` enables
|
|
18
|
+
automatic signing after every commit; concurrent commits are batched safely.
|
|
18
19
|
- `Signer` — deterministic secp256k1 keypairs from username + password via PBKDF2.
|
|
19
20
|
No key files to manage; same credentials always produce the same identity.
|
|
20
21
|
- `Recaller` — fine-grained reactive dependency tracker. Watchers re-run only when
|
|
@@ -32,51 +33,46 @@ The foundation is solid and working. Here's what's in:
|
|
|
32
33
|
any persistence.
|
|
33
34
|
|
|
34
35
|
**UI layer**
|
|
35
|
-
- `h` — tagged template literal parser. Turns
|
|
36
|
+
- `h` — tagged template literal parser. Turns `` h`<div class=${cls}>...` `` into a
|
|
36
37
|
virtual tree of `HElement` / `HText` / slot nodes.
|
|
37
38
|
- `mount` — reactive DOM renderer. Slots that are functions re-run automatically
|
|
38
39
|
when the data they read changes. No virtual DOM diffing — only the exact nodes
|
|
39
|
-
bound to mutated paths update.
|
|
40
|
+
bound to mutated paths update. Watcher cleanup is precise: removed nodes are
|
|
41
|
+
unwatched before removal so watchers never accumulate. Elements are recycled
|
|
42
|
+
across re-renders by `data-key` (exact) then tag (positional fallback), so user
|
|
43
|
+
input and focus survive list reorders. SVG namespaces propagate automatically —
|
|
44
|
+
`` h`<svg><path/></svg>` `` just works. `class` accepts an array
|
|
45
|
+
(`['btn', isActive && 'active']`) or an object (`{btn: true, active: false}`).
|
|
46
|
+
- `StreamoComponent` — base class for hot-reloadable custom element components.
|
|
47
|
+
Function components (`(props) => nodes`) work directly as tags in `h`. For
|
|
48
|
+
hot-reloading, `componentKey(prefix, address)` and `defineComponent(name, fn)`
|
|
49
|
+
pair a content address to a unique custom element name — a new file version gets
|
|
50
|
+
a new name, stale elements are naturally orphaned and cleaned up.
|
|
40
51
|
|
|
41
52
|
**Apps**
|
|
42
|
-
- Chat — full p2p messaging app. Each participant owns their own message
|
|
43
|
-
|
|
44
|
-
and
|
|
53
|
+
- Chat — full p2p messaging app. Each participant owns their own signed message
|
|
54
|
+
stream. `public/apps/chat/server.js` is the standalone server — its public key
|
|
55
|
+
is the room address, its member list is in its own repo, and it has no special
|
|
56
|
+
authority over anyone's data. Runs in the browser and from the terminal
|
|
57
|
+
(`chat-cli.js`).
|
|
45
58
|
- Homepage at `public/index.html`.
|
|
46
|
-
- `
|
|
59
|
+
- `StreamoServer` — reusable class that wraps signer, registry, and all sync
|
|
60
|
+
methods behind a clean API. `bin/streamo.js` is now a thin CLI parser on top
|
|
61
|
+
of it; `public/apps/chat/server.js` is a standalone chat server using the
|
|
62
|
+
same class.
|
|
63
|
+
- `npm run serve` — starts a streamo node (with REPL) using `.env.dev`
|
|
64
|
+
credentials. The dev server is a real peer, not a bare static file server.
|
|
47
65
|
|
|
48
66
|
---
|
|
49
67
|
|
|
50
68
|
## what's next
|
|
51
69
|
|
|
52
|
-
###
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
Functions as tags: `h\`<${Card} title="hi"/>\``. This is the difference between
|
|
59
|
-
a templating tool and a UI framework. Once watcher cleanup exists, components
|
|
60
|
-
become straightforward — a component is just a function that returns nodes and
|
|
61
|
-
cleans up after itself.
|
|
62
|
-
|
|
63
|
-
### SVG namespace
|
|
64
|
-
`mount` hardcodes the XHTML namespace. `h\`<svg><path/></svg>\`` won't render
|
|
65
|
-
correctly until `mount` auto-detects SVG elements and switches namespaces.
|
|
66
|
-
|
|
67
|
-
### `class` as array or object
|
|
68
|
-
`class=${['btn', isActive && 'active']}` is such a common pattern that not
|
|
69
|
-
supporting it is a daily papercut. Easy win.
|
|
70
|
-
|
|
71
|
-
### chat persistence
|
|
72
|
-
Right now the chat server is in-memory — restart it and history is gone. Wiring
|
|
73
|
-
`archiveSync` into `chat-server.js` is a small change with a big quality-of-life
|
|
74
|
-
improvement.
|
|
75
|
-
|
|
76
|
-
### chat signing
|
|
77
|
-
Messages aren't cryptographically verified yet. Anyone who knows a participant's
|
|
78
|
-
public key hex could theoretically spoof them. Wiring `repo.sign()` after each
|
|
79
|
-
`set()` closes this.
|
|
70
|
+
### chat persistence ← start here
|
|
71
|
+
The chat server (`public/apps/chat/server.js`) uses `StreamoServer` and wires
|
|
72
|
+
`archiveSync` — so the member list survives restarts automatically. Individual
|
|
73
|
+
message history lives in each participant's own repo; persistence there depends
|
|
74
|
+
on participants running with `--data-dir` set. The remaining work is ensuring the
|
|
75
|
+
browser chat client also persists across page reloads.
|
|
80
76
|
|
|
81
77
|
### presence indicators
|
|
82
78
|
Who's currently online? The `interest` / `announce` layer is ephemeral by design,
|
|
@@ -88,24 +84,63 @@ The old repository-browser app was left behind during the migration because its
|
|
|
88
84
|
imports broke. Rebuilding it with `h` / `mount` would be the first substantial
|
|
89
85
|
real-world test of the UI layer.
|
|
90
86
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## toward 1.0
|
|
90
|
+
|
|
91
|
+
One thing blocking a stable `1.0` claim:
|
|
94
92
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
1. **Chat persistence** — a chat app that loses history on restart isn't production-ready
|
|
94
|
+
|
|
95
|
+
Chat signing is done. Components, keyed list reconciliation, SVG namespaces,
|
|
96
|
+
`class` arrays/objects, and the CLI server unification are all done.
|
|
97
|
+
Persistence is the last mile.
|
|
98
98
|
|
|
99
99
|
---
|
|
100
100
|
|
|
101
|
-
##
|
|
101
|
+
## beyond 1.0
|
|
102
|
+
|
|
103
|
+
Ideas that follow naturally from the architecture but aren't blocking anything.
|
|
104
|
+
|
|
105
|
+
### Claude scratchpad repos
|
|
106
|
+
|
|
107
|
+
Every streamo node already has a signed, append-only repo. A Claude session
|
|
108
|
+
could write observations, notes, and work products to that repo during a
|
|
109
|
+
conversation — and the owner could watch them appear live in a browser via
|
|
110
|
+
`mount`. Between sessions, Claude reads the repo to reconstruct context
|
|
111
|
+
instead of relying on static memory files. The work is persistent and
|
|
112
|
+
provably Claude's, with the same integrity guarantees as any other streamo data.
|
|
113
|
+
|
|
114
|
+
### Claude-to-Claude networks
|
|
115
|
+
|
|
116
|
+
If each person's Claude has a scratchpad repo, those repos can sync the same
|
|
117
|
+
way any other repos do. The `follow` callback in `registrySync` already handles
|
|
118
|
+
content-driven discovery — subscribe to a member list, auto-follow everyone on
|
|
119
|
+
it. A Claude could watch its person's friends' scratchpads, surface what's
|
|
120
|
+
relevant, and filter what isn't.
|
|
121
|
+
|
|
122
|
+
The interesting architectural difference from a traditional social network: there
|
|
123
|
+
is no central moderator. Each Claude is an advocate for its person, not a
|
|
124
|
+
reporter to a platform. Judgment about what to surface or filter lives at the
|
|
125
|
+
edge, anchored to a real signed identity. Conflicts between Claudes are just
|
|
126
|
+
their people having different values — which is honest in a way platform
|
|
127
|
+
moderation usually isn't.
|
|
128
|
+
|
|
129
|
+
A natural extension: if a Claude scratchpad includes a `StreamoComponent` for
|
|
130
|
+
how its notes render, other people see those notes in Claude's own layout. The
|
|
131
|
+
presentation travels with the content — no server controls the framing.
|
|
102
132
|
|
|
103
|
-
|
|
133
|
+
### StreamoComponent demos — shared components as content
|
|
104
134
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
135
|
+
`StreamoComponent` makes most sense as a post-1.0 story, after chat signing
|
|
136
|
+
gives the trust foundation that running someone else's component requires.
|
|
137
|
+
The right first demo is a **tarot deck**: each card is a `StreamoComponent`
|
|
138
|
+
from its designer, stored in their signed repo at a content address.
|
|
139
|
+
`componentKey` generates a stable element name from that address. A reading
|
|
140
|
+
is a snapshot — cards freeze at the version they were drawn, which is a
|
|
141
|
+
feature, not a bug. The designer's signed key is provenance.
|
|
109
142
|
|
|
110
|
-
|
|
111
|
-
|
|
143
|
+
Other directions once the pattern is established: publisher-controlled article
|
|
144
|
+
cards that travel with syndicated content (the layout is the author's, not
|
|
145
|
+
the platform's); collaborative maps where each participant's marker is their
|
|
146
|
+
own component; shared instrument components in a live music session.
|
package/bin/streamo.js
CHANGED
|
@@ -6,16 +6,14 @@ import { Option, program } from 'commander'
|
|
|
6
6
|
import { config } from 'dotenv'
|
|
7
7
|
import { question, questionNewPassword } from 'readline-sync'
|
|
8
8
|
import { start as startRepl } from 'repl'
|
|
9
|
-
import {
|
|
9
|
+
import { StreamoServer } from '../public/streamo/StreamoServer.js'
|
|
10
10
|
import { Repo } from '../public/streamo/Repo.js'
|
|
11
11
|
import { RepoRegistry } from '../public/streamo/RepoRegistry.js'
|
|
12
12
|
import { archiveSync } from '../public/streamo/archiveSync.js'
|
|
13
13
|
import { fileSync } from '../public/streamo/fileSync.js'
|
|
14
14
|
import { outletSync } from '../public/streamo/outletSync.js'
|
|
15
15
|
import { originSync } from '../public/streamo/originSync.js'
|
|
16
|
-
import { webSync } from '../public/streamo/webSync.js'
|
|
17
16
|
import { s3Sync } from '../public/streamo/s3Sync.js'
|
|
18
|
-
import { stateFileSync } from '../public/streamo/stateFileSync.js'
|
|
19
17
|
|
|
20
18
|
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url)))
|
|
21
19
|
|
|
@@ -112,29 +110,32 @@ if (options.envFile) {
|
|
|
112
110
|
Object.assign(options, program.opts())
|
|
113
111
|
}
|
|
114
112
|
|
|
115
|
-
options.name
|
|
113
|
+
options.name ||= question('Name: ')
|
|
116
114
|
options.username ||= question('Username: ')
|
|
117
115
|
const password = options.password || questionNewPassword('Password [ATTENTION!: Backspace won\'t work here]: ', { min: 4, max: 999 })
|
|
118
116
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
const server = await StreamoServer.create({
|
|
118
|
+
name: options.name,
|
|
119
|
+
username: options.username,
|
|
120
|
+
password,
|
|
121
|
+
dataDir: options.dataDir,
|
|
122
|
+
keyIterations: options.keyIterations,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const { name, username, publicKeyHex, signer, streamo, registry } = server
|
|
122
126
|
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
? '/' + dirname(options.envFile).replace(/^public\//, '') + '/'
|
|
127
|
-
: '/'
|
|
128
|
-
const webUrl = options.web ? `http://localhost:${+options.web}${appPath}` : null
|
|
127
|
+
const envDir = options.envFile ? dirname(options.envFile).replace(/^public\//, '') : null
|
|
128
|
+
const appPath = (envDir && envDir !== '.') ? `/${envDir}/` : '/'
|
|
129
|
+
const webUrl = options.web ? `http://localhost:${+options.web}${appPath}` : null
|
|
129
130
|
const rows = [
|
|
130
|
-
['NAME',
|
|
131
|
-
['USERNAME',
|
|
131
|
+
['NAME', name],
|
|
132
|
+
['USERNAME', username],
|
|
132
133
|
['PUBLIC KEY', publicKeyHex],
|
|
133
134
|
...(webUrl ? [['URL', webUrl]] : []),
|
|
134
135
|
]
|
|
135
136
|
const maxLength = Math.max(...rows.map(([, v]) => v.length))
|
|
136
|
-
const pad
|
|
137
|
-
const div
|
|
137
|
+
const pad = (v) => v + ' '.repeat(maxLength - v.length)
|
|
138
|
+
const div = '─'.repeat(maxLength)
|
|
138
139
|
const label = (l) => l.padStart(16)
|
|
139
140
|
console.log(`\x1b[35m
|
|
140
141
|
╭${'─'.repeat(maxLength + 23)}╮
|
|
@@ -145,49 +146,40 @@ ${rows.map(([l, v], i) => [
|
|
|
145
146
|
].filter(Boolean).join('\n')).join('\n')}
|
|
146
147
|
╰──────────────────┴──${'━'.repeat(maxLength)}──╯\x1b[0m`)
|
|
147
148
|
|
|
148
|
-
const dataDir = options.dataDir
|
|
149
|
-
const registry = new RepoRegistry(async key => {
|
|
150
|
-
const repo = new Repo()
|
|
151
|
-
await archiveSync(repo, dataDir, key)
|
|
152
|
-
return repo
|
|
153
|
-
})
|
|
154
|
-
const streamo = await registry.open(publicKeyHex)
|
|
155
|
-
|
|
156
149
|
if (options.files) {
|
|
157
150
|
const folder = typeof options.files === 'string' ? options.files : '.'
|
|
158
|
-
await
|
|
151
|
+
await server.files(folder)
|
|
159
152
|
console.log(`\x1b[32mmirroring files: ${folder}\x1b[0m`)
|
|
160
153
|
}
|
|
161
154
|
|
|
162
155
|
if (options.stateFile) {
|
|
163
|
-
|
|
156
|
+
server.stateFile(options.stateFile)
|
|
164
157
|
console.log(`\x1b[32mstate file: ${options.stateFile}\x1b[0m`)
|
|
165
158
|
}
|
|
166
159
|
|
|
167
160
|
if (options.s3Bucket) {
|
|
168
|
-
await
|
|
169
|
-
bucket:
|
|
170
|
-
endpoint:
|
|
171
|
-
region:
|
|
172
|
-
accessKeyId:
|
|
173
|
-
secretAccessKey: options.s3SecretAccessKey
|
|
161
|
+
await server.s3({
|
|
162
|
+
bucket: options.s3Bucket,
|
|
163
|
+
endpoint: options.s3Endpoint,
|
|
164
|
+
region: options.s3Region,
|
|
165
|
+
accessKeyId: options.s3AccessKeyId,
|
|
166
|
+
secretAccessKey: options.s3SecretAccessKey,
|
|
174
167
|
})
|
|
175
168
|
console.log(`\x1b[32ms3: syncing to bucket ${options.s3Bucket}\x1b[0m`)
|
|
176
169
|
}
|
|
177
170
|
|
|
178
171
|
if (options.web) {
|
|
179
|
-
await
|
|
172
|
+
await server.web(+options.web)
|
|
180
173
|
}
|
|
181
174
|
|
|
182
175
|
if (options.outlet) {
|
|
183
176
|
const port = +options.outlet
|
|
184
|
-
|
|
177
|
+
server.outlet(port)
|
|
185
178
|
console.log(`\x1b[32moutlet: listening on port ${port}\x1b[0m`)
|
|
186
179
|
}
|
|
187
180
|
|
|
188
181
|
if (options.origin) {
|
|
189
|
-
|
|
190
|
-
await originSync(streamo, publicKeyHex, host, +port)
|
|
182
|
+
await server.connect(options.origin)
|
|
191
183
|
console.log(`\x1b[32morigin: connected to ${options.origin}\x1b[0m`)
|
|
192
184
|
}
|
|
193
185
|
|
|
@@ -197,13 +189,10 @@ if (options.verbose) {
|
|
|
197
189
|
}
|
|
198
190
|
|
|
199
191
|
if (options.interactive) {
|
|
200
|
-
const get
|
|
201
|
-
const set
|
|
202
|
-
const ls
|
|
203
|
-
const connect = (hostPort) =>
|
|
204
|
-
const [host, port] = hostPort.split(':')
|
|
205
|
-
return originSync(streamo, publicKeyHex, host, +port)
|
|
206
|
-
}
|
|
192
|
+
const get = (...args) => streamo.get(...args)
|
|
193
|
+
const set = (...args) => streamo.set(...args)
|
|
194
|
+
const ls = () => [...registry].map(([k, s]) => ({ key: k.slice(0, 8) + '…', bytes: s.byteLength }))
|
|
195
|
+
const connect = (hostPort) => server.connect(hostPort)
|
|
207
196
|
|
|
208
197
|
Object.assign(globalThis, {
|
|
209
198
|
// identity
|
package/jsconfig.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dtudury/streamo",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "peer-to-peer sync where your data and identity belong to you, not the server",
|
|
5
|
+
"keywords": ["p2p", "peer-to-peer", "sync", "reactive", "content-addressed", "websocket", "signed", "append-only", "offline-first", "cryptographic", "identity"],
|
|
5
6
|
"repository": "git@github.com:dtudury/streamo.git",
|
|
6
7
|
"author": "David Tudury <david.tudury@gmail.com>",
|
|
7
8
|
"license": "AGPL-3.0-only",
|
|
@@ -21,6 +22,6 @@
|
|
|
21
22
|
"ws": "^8.18.3"
|
|
22
23
|
},
|
|
23
24
|
"scripts": {
|
|
24
|
-
"serve": "node
|
|
25
|
+
"serve": "node bin/streamo.js --env-file .env.dev --web 3000 --interactive"
|
|
25
26
|
}
|
|
26
27
|
}
|