@glyphteck/veyl 0.58.0 → 0.59.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/docs/agents.md CHANGED
@@ -54,12 +54,13 @@ await veyl.listen({
54
54
 
55
55
  This is a live subscription to shared chat-list and transfer state, not a polling loop. By default it emits compact peer-authored events and opens a changed chat only long enough to process it, so hundreds of peers do not become hundreds of retained message listeners. Set `compact: false` or `incomingOnly: false` when complete payloads or self-authored messages are required. A long-lived fleet can opt into `persistentChats: true` with `persistentChatIdleMs` to keep active conversations mounted, mark peer messages read before policy effects, and release only idle conversations; the account-level chat-list subscription reopens them when new activity arrives. Each listener has independent replay/filter state. Slow callbacks should hand work to an application queue if they must preserve event throughput.
56
56
 
57
- Compact message events include reusable `replyTo` and `reactTo` targets. Transaction events carry the same simplified transfer shape returned by wallet history.
57
+ Compact message events include stable `chatId`, verified sender identity, member summaries, and reusable `replyTo` and `reactTo` targets. Transaction events carry the same simplified transfer shape returned by wallet history.
58
58
 
59
59
  When an agent is actively working inside one conversation, mount its shared route instead of repeatedly taking one-shot reads:
60
60
 
61
61
  ```js
62
- const chat = await veyl.chat.enter('@alice', { readPolicy: 'auto' });
62
+ const group = await veyl.chat.create(['@alice', '@bob'], { title: 'operators' });
63
+ const chat = await veyl.chat.enterById(group.id, { readPolicy: 'auto' });
63
64
  const unsubscribe = chat.subscribe((snapshot) => {
64
65
  memory.replace(snapshot.messages);
65
66
  });
@@ -69,7 +70,7 @@ chat.leave();
69
70
  unsubscribe();
70
71
  ```
71
72
 
72
- The mounted route preserves its coherent transcript and retention lifecycle until leave. Account-wide `listen` remains the wakeup surface; entered chat is the focused conversation surface.
73
+ The mounted route preserves its coherent epoch-spanning transcript and retention lifecycle until leave. Account-wide `listen` remains the wakeup surface; `enterById` is the primary focused notes/direct/group surface. `enter('@alice')` remains a canonical-direct convenience.
73
74
 
74
75
  For multiple agent identities, run one local `openFleetOwner`. It derives every account's machine credential, vault key, and Veyl master seed from one versioned root and account index. It opens one normal public client per secret-free manifest profile, tags each event with its account, and runs host policies above those clients. It grants no admin access and bypasses no product owner.
75
76
 
@@ -109,6 +110,8 @@ Then issue commands from other processes through the owner-only local socket:
109
110
  ```bash
110
111
  veyl --profile runner --session default wallet balance
111
112
  veyl --profile runner --session default chat send @alice "hello"
113
+ veyl --profile runner --session default chat create-group @alice,@bob --title operators
114
+ veyl --profile runner --session default chat enter-by-id CHAT_ID
112
115
  veyl --profile runner --session default chat enter @alice
113
116
  veyl --profile runner --session default session events
114
117
  veyl --profile runner --session default session stop
@@ -116,7 +119,7 @@ veyl --profile runner --session default session stop
116
119
 
117
120
  Commands share the unlocked client and can run concurrently where the shared owners permit it. Same-chat sends remain ordered, disjoint chats can progress independently, and wallet mutations stay serialized.
118
121
 
119
- An entered chat remains mounted in the foreground session if its output shell disconnects. Use `chat entered` to inspect it and `chat leave @peer` when the agent is finished.
122
+ An entered chat remains mounted in the foreground session if its output shell disconnects. Use `chat entered` to inspect it and `chat exit-by-id CHAT_ID` when the agent is finished.
120
123
 
121
124
  ## Safety rules
122
125
 
package/docs/api.md CHANGED
@@ -149,13 +149,13 @@ unsubscribe();
149
149
  account.close();
150
150
  ```
151
151
 
152
- `openAccount()` owns authenticated user observation, username and avatar publication, vault observation and creation, vault unlock/lock, encrypted settings/network selection, presence, late wallet readiness, public Bitcoin data, support/report commands, chat and peer/profile composition, account switching, and secret-bearing session teardown. Its snapshot exposes `user`, `vault`, `vaultReady`, `vaultError`, `session`, `wallet`, `walletError`, `network`, and `lockState`; stable domain owners such as `bitcoin` and `support` live directly on the returned account owner.
152
+ `openAccount()` owns authenticated user observation, username and avatar publication, vault observation and creation, vault unlock/lock, encrypted settings/network selection, presence, late wallet readiness, public Bitcoin data, support/report commands, chat and peer/profile composition, account switching, and secret-bearing session teardown. Focused-chat presence is separately owned by the chat session's encrypted ephemeral live transport. The account snapshot exposes `user`, `vault`, `vaultReady`, `vaultError`, `session`, `wallet`, `walletError`, `network`, and `lockState`; stable domain owners such as `bitcoin` and `support` live directly on the returned account owner.
153
153
 
154
154
  `account.profile` owns the server mutation after a platform has prepared avatar bytes or collected a username. Browser canvas work and native image manipulation remain platform-local; both then call the same `setAvatar`, `clearAvatar`, or `setUsername` command. Successful avatar commands update the shared user owner before returning.
155
155
 
156
156
  `account.peers` is the shared peer-directory owner used by Node, web, and iOS. `openAccount()` supplies its chat, wallet, blocked-user, and encrypted-cache sources directly, including missing-profile chat cleanup. Graphical adapters only subscribe to the same snapshot and profile selectors already defined by core. `openSearch('profiles')` creates profile-only search; web may also use `openSearch('mainmenu')` for its combined local-action and remote-profile menu. Active searches track peer and blocked-user changes and release those subscriptions when cleared or closed.
157
157
 
158
- `account.chat` is the one account-scoped encrypted chat owner used by Node, web, and iOS. It starts with the account, receives authenticated-user and unlocked-secret sources directly from `openAccount()`, and clears list, route-batch, local-message, delivery, and key-bearing state on lock or account switch. Its snapshot owns chat-list paging, encrypted inbox/private delivery, mounted-message batch inputs, sends, attachments, reactions, receipts, retention, saves, message deletion, and whole-chat deletion. Graphical chat providers only expose this owner through React; `@veyl/react` retains mounted-route lifecycle, while browser/native media preparation and foreground/native-crypto behavior enter through the `chat` platform ports.
158
+ `account.chat` is the one account-scoped protocol-v3 chat owner used by Node, web, and iOS. It owns notes/direct/group creation, stable logical chat IDs, membership epochs, encrypted list/inbox convergence, epoch-spanning history, delivery, messages, receipts, signed encrypted chat settings, member changes, leaving, and deletion. It clears every route, cache, timer, and key-bearing state on lock/account switch. Graphical providers only expose this owner through React; platform media preparation and native behavior enter through explicit ports.
159
159
 
160
160
  `account.wallet` is the stable account-scoped wallet composition used by Node, web, and iOS. It owns the one live `core/wallet/session.js` engine, its transfer store, cached pre-Spark display state, transaction aggregation, wallet-derived peer recency, late Spark attachment, and lock teardown. `getSnapshot()` preserves the shared `{ value, txValue }` contract; `transactions` exposes aggregate/search/chart data and `transfers` exposes focused list and keyed subscriptions. Graphical wallet providers only adapt those SDK subscriptions and add browser/native behavior through the `wallet` platform ports.
161
161
 
@@ -262,6 +262,33 @@ Peer resolution accepts `@username`, username, chat public key, or wallet public
262
262
 
263
263
  ```js
264
264
  const chats = await veyl.chat.list({ count: 30 });
265
+ const notes = await veyl.chat.notes();
266
+ const direct = await veyl.chat.openDirect('@alice');
267
+ const group = await veyl.chat.create(['@alice', '@bob'], { title: 'builders' });
268
+ const activeGroup = await veyl.chat.create(['@alice', '@bob'], {
269
+ title: 'builders',
270
+ initialMessage: 'first light',
271
+ });
272
+
273
+ await veyl.chat.addMembers(group.id, ['@carol']);
274
+ const expanded = await veyl.chat.get(group.id);
275
+ await veyl.chat.updateSettings(group.id, {
276
+ title: 'night builders',
277
+ avatarRef: expanded.members[0].chatPK,
278
+ });
279
+ await veyl.chat.kickMember(group.id, memberChatPK);
280
+
281
+ const groupPage = await veyl.chat.readById(group.id, { count: 30 });
282
+ await veyl.chat.markReadIn(group.id);
283
+ const enteredGroup = await veyl.chat.enterById(group.id, { readPolicy: 'auto' });
284
+ const groupMessage = await veyl.chat.sendTo(group.id, 'hello everyone');
285
+ await veyl.chat.replyIn(group.id, groupMessage.message.id, 'follow up');
286
+ await veyl.chat.reactIn(group.id, groupMessage.message.id, '+1');
287
+ enteredGroup.leave();
288
+
289
+ await veyl.chat.leaveMembership(group.id);
290
+
291
+ // Direct-peer conveniences resolve the canonical direct chat first.
265
292
  const page = await veyl.chat.read('@alice', { count: 30 });
266
293
  await veyl.chat.markRead('@alice');
267
294
 
@@ -289,9 +316,13 @@ await veyl.chat.retention('@alice', '24h');
289
316
  await veyl.chat.deleteChat('@alice', { cleanup: true });
290
317
  ```
291
318
 
292
- `read` advances the shared older-history loader until `count` visible messages are covered or history ends, then marks the newest applicable peer message read. `markRead` stays on the latest window and writes only the newest applicable receipt. `retry` accepts the CID of a failed local text or attachment send and uses the shared retry queue. `reply` and `reactTo` also accept a compact event object produced by `listen`. Chat reads, reduction, signatures, local pending state, send ordering, whole-chat mutation exclusion, stale-generation retry, retention, expiry holds, deletes, and encrypted cache behavior are shared with web/iOS.
319
+ All materialized chats are stable logical objects. `notes`, `openDirect`, and `create` without `initialMessage` first return local provisional routes; they consume no KeyPackage and create no remote state or chat-list row until first content is sent. Direct open and first send both resolve the complete encrypted owner history before creating a fresh random direct. Only a membership change rotates a fresh epoch beneath an existing `chatId`; title, picture, retention, reads, reactions, and ordinary messages remain inside the current epoch. Added members do not receive old keys. A chat that becomes a group keeps permanent group lineage and never replaces a later ordinary direct chat.
320
+
321
+ `avatarRef` selects one current member chat key whose live profile picture represents the chat; `null` restores automatic direct/notes/composite presentation. Arbitrary uploaded group artwork is not part of the launch storage contract. `forever` is reserved protocol state and is not exposed by official clients.
322
+
323
+ `readById` pages current and owner-authorized historical epochs until `count` visible messages are covered or history ends. `markReadIn` writes the latest applicable receipt by stable chat id. Direct `read`/`markRead` are canonical-peer conveniences. Every projected message includes stable chat target and verified actor identity; group actions never need a fake direct peer.
293
324
 
294
- `enter` mounts that chat's framework-free shared route and returns a stable handle with `getSnapshot`, `subscribe`, `loadOlder`, `markRead`, and `leave`. Re-entering the same peer in one client returns the existing handle. The route keeps its transcript reconciled and retains already-visible retention-expired rows until explicit leave; explicit deletion still removes a row immediately. `readPolicy` is `auto` by default, `manual` disables automatic receipts but permits `entered.markRead()`, and `none` disables both. `chat.entered()` returns serializable snapshots for every mounted route, and `chat.leave(peer)` releases one without requiring the original handle.
325
+ `enterById` mounts a logical chat route and returns a stable handle with `getSnapshot`, `subscribe`, `loadOlder`, `markRead`, and `leave`. `enter(peer)` does the same after resolving the canonical direct. Re-entering the same `chatId` reuses the handle. `readPolicy` is `auto`, `manual`, or `none`. Route-local retention holds and explicit-delete behavior match web/iOS.
295
326
 
296
327
  Attachments:
297
328
 
@@ -404,9 +435,9 @@ await veyl.listen({
404
435
  });
405
436
  ```
406
437
 
407
- `listen` subscribes to the shared live chat-list and transfer-store owners. It emits `ready`, `message`, `message-delete`, `transaction`, and `error` events. Compact, incoming-only events and transient chat subscriptions are the defaults: a changed conversation is opened briefly, processed, and released instead of retaining one message listener per chat. This keeps an account with hundreds of peers from multiplying long-lived Firestore listeners. Set `compact: false`, `incomingOnly: false`, or `persistentChats: true` only when the caller needs those broader behaviors.
438
+ `listen` subscribes to the shared live chat-list and transfer-store owners. It emits `ready`, `message`, `message-delete`, `transaction`, and `error` events. Message events carry a stable `chatId`, verified actor/member summaries, and a qualified message target. Compact, incoming-only events and transient chat subscriptions are the defaults: the encrypted chat list acts as a wake index, so a changed chat opens briefly, processes its current and history pages, and releases instead of retaining one listener per chat. Set `compact: false`, `incomingOnly: false`, or `persistentChats: true` only when the caller needs those broader behaviors.
408
439
 
409
- Use `listen` for account-wide agent wakeups. Use `chat.enter` when an agent is actively inside one conversation and needs that mounted transcript's exact lifecycle, history, retention, and read semantics.
440
+ Use `listen` for account-wide agent wakeups. Use `chat.enterById` when an agent is actively inside any notes/direct/group chat; use `chat.enter(peer)` only as a canonical-direct convenience.
410
441
 
411
442
  ## Local fleet owner
412
443
 
package/docs/cli.md CHANGED
@@ -63,12 +63,36 @@ veyl peers block @alice
63
63
  veyl peers unblock @alice
64
64
  ```
65
65
 
66
- Blocking yourself is rejected. Blocking a peer immediately hides the local chat and rejects later sealed-inbox delivery from that account. Encrypted stream validation remains client-owned.
66
+ Blocking yourself is rejected. Blocking deletes the canonical direct chat and leaves every decrypted group containing that UID. A future successor containing the block is dropped locally; no readable server membership graph exists.
67
67
 
68
68
  ## Chat
69
69
 
70
70
  ```bash
71
71
  veyl chat list [--count 30]
72
+ veyl chat get CHAT_ID
73
+ veyl chat notes
74
+ veyl chat open-direct @alice
75
+ veyl chat create-group @alice,@bob --title "builders"
76
+ veyl chat add-members CHAT_ID @carol
77
+ veyl chat kick-member CHAT_ID MEMBER_CHAT_PK
78
+ veyl chat update-settings CHAT_ID '{"title":"night builders"}'
79
+ veyl chat messages-by-id CHAT_ID [--count 30]
80
+ veyl chat enter-by-id CHAT_ID [--read-policy auto|manual|none]
81
+ veyl chat mark-read-by-id CHAT_ID [MESSAGE_ID]
82
+ veyl chat send-to CHAT_ID "hello everyone"
83
+ veyl chat reply-in CHAT_ID MESSAGE_ID "got it"
84
+ veyl chat react-in CHAT_ID MESSAGE_ID "+1"
85
+ veyl chat unreact-in CHAT_ID MESSAGE_ID
86
+ veyl chat save-in CHAT_ID MESSAGE_ID
87
+ veyl chat unsave-in CHAT_ID MESSAGE_ID
88
+ veyl chat edit-in CHAT_ID MESSAGE_ID "corrected text"
89
+ veyl chat delete-message-in CHAT_ID MESSAGE_ID
90
+ veyl chat send-file-to CHAT_ID ./photo.webp
91
+ veyl chat download-by-id CHAT_ID MESSAGE_ID ./downloaded-file
92
+ veyl chat leave-membership CHAT_ID
93
+ veyl chat delete-by-id CHAT_ID
94
+
95
+ # Canonical-direct convenience commands:
72
96
  veyl chat enter @alice [--count 30] [--read-policy auto|manual|none]
73
97
  veyl chat entered
74
98
  veyl chat leave @alice
@@ -97,9 +121,11 @@ veyl chat share @alice MESSAGE_ID @bob,@carol
97
121
  veyl chat download @alice MESSAGE_ID ./downloaded-file
98
122
  ```
99
123
 
100
- `chat messages` pages the same shared older-history loader used by the app until `--count` visible messages are covered or history ends, then marks the newest applicable peer message read. The live listener likewise marks incoming plaintext it decrypts as read. `chat mark-read` remains available for an explicit latest-only write without paging older history. Explicit message delete is immediate; retention expiry remains held in an already-mounted route until that route is released.
124
+ The `*-by-id`, `*-to`, and `*-in` commands are the primary notes/direct/group surface. Direct peer commands first resolve the canonical private direct alias. A chat that once became a group remains a separate group-lineage object even if reduced to two members.
101
125
 
102
- `chat enter` emits the initial mounted transcript and every later route snapshot as newline-delimited JSON. A one-off process owns the route only until that process exits. With `--session`, the foreground runtime owns the route: disconnecting or pressing Ctrl-C only detaches that output stream, `chat entered` can inspect the retained transcript later, and `chat leave` explicitly releases it. `--read-policy manual` requires `chat mark-read` or the JavaScript handle's `markRead`; `none` suppresses read writes for that entered route.
126
+ `chat messages-by-id` and `chat messages` page the same epoch-spanning older-history loader used by the app, then mark the newest applicable message read. `chat mark-read-by-id` is the stable-chat latest-only write. Explicit message delete is immediate; retention expiry remains held in an already-mounted route until release.
127
+
128
+ `chat enter-by-id` and `chat enter` emit mounted snapshots as newline-delimited JSON. A one-off process owns the route until exit. With `--session`, the foreground runtime retains it after output disconnect; `chat entered` inspects retained routes and `chat exit-by-id` releases any chat by stable id. `manual` requires an explicit mark-read; `none` suppresses read writes.
103
129
 
104
130
  ## Wallet
105
131
 
@@ -45,7 +45,7 @@ API version 3 establishes `open()` as the public SDK composition root and remove
45
45
  | Settings | 2 / 2 | shared user settings owner and normalization |
46
46
  | Cache | 2 / 2 | shared vault-encrypted local-data cache |
47
47
  | Peers | 6 / 6 | `core/peerlist.js`, `core/peers.js`, and shared search sessions |
48
- | Chat | 21 / 21 | `core/chat/session.js`, the framework-free mounted message route, shared actions, message batches, history, retention, and attachment owners |
48
+ | Chat | 44 / 44 | `core/chat/session.js`, stable-id notes/direct/group creation, membership epochs, mounted routes, shared actions, history, retention, and attachment owners |
49
49
  | Wallet | 10 / 10 | `core/wallet/session.js`, claim, transfer-store, and tx-data owners |
50
50
  | Lightning | 5 / 5 | the same shared wallet session and Spark request owners |
51
51
  | Withdrawal | 3 / 3 | shared wallet session, fee normalization, and generic payment-intent/L1 review owner |
@@ -171,11 +171,11 @@ Cold one-off commands are inherently inefficient for agent loops because Spark b
171
171
 
172
172
  The executable source is `costs/model.mjs`.
173
173
 
174
- An established visible encrypted send with one active notification route is modeled as 10 Firestore read-equivalent operations, 4 writes, and 1 Function invocation; delivery with no active route is one read cheaper. Read receipts and reaction removals are encrypted stream-only actions with 3 rules reads and 1 write, while adding or changing a reaction intentionally uses the visible notification path.
174
+ Protocol v3 separates constant content cost from recipient availability cost. One visible action commits one ciphertext, one logical-chat budget update, and at most one coalesced sender owner row: 6 planning reads and 3 writes independent of group size. Each accepted recipient wake adds two route/slot reads, one minimum push-route query when APNs is due, one overwritten slot write, and one Function invocation. The default one-edge/direct upper bound is therefore 9 reads, 4 writes, and 1 invocation. Read receipts add one shared 6-read/2-write action with no wake.
175
175
 
176
- The current planning bundle is about 495 Firestore read-equivalent operations, 138 writes, 28 Function invocations, and one Storage Class A operation per active user/day. Headline projections are about `$2,300/month` at 100k DAU and `$23,021/month` at 1M DAU. At a sustained one visible send per second, the model is `$35.25` gross per month, or `$44.58` with one receipt per send.
176
+ The current one-edge planning bundle is about 542 Firestore read-equivalent operations, 163 writes, 28 Function invocations, and one Storage Class A operation per active user/day. Headline projections are about `$2,519/month` at 100k DAU and `$25,217/month` at 1M DAU. At a sustained one visible send per second, the model is `$33.70` gross per month, or `$52.36` with one receipt per send. Set `MESSAGE_RECIPIENT_EDGES` to model larger groups; sender, wake-slot, and APNs coalescing reduce the isolated-send upper bound.
177
177
 
178
- Live Cloud Run logs matched the intended invocation boundary: each visible message, added reaction, and explicit delete produced one `push` request; read receipts and reaction removal produced none. Current source also treats push docs as revocable authorization leases. Registration revalidates session generation and credential ownership inside its transaction, while account-wide and credential-scoped revocation delete leases transactionally; delivery therefore avoids two recipient-auth reads per active destination on every visible send. These are dated observations; current deployment state must be verified independently before a release decision.
178
+ Earlier live Cloud Run measurements covered the retired direct-chat delivery contract and must not be treated as v3 evidence. Current source uses anonymous opaque capability delivery, a recipient-owned overwritten slot, prompt-plus-trailing sender burst coalescing, a 250 ms slot debounce, and a 2 s APNs debounce. Dev traces must establish the real accepted-wake ratio before production projections are tightened.
179
179
 
180
180
  These totals intentionally exclude Function CPU/memory duration, outbound network, APNS delivery, Spark/vendor/network fees, media download bytes, and moderation labor until real rates are measured.
181
181
 
@@ -16,6 +16,12 @@ function canRespond(account) {
16
16
  return roles.has('echo') || roles.has('faucet');
17
17
  }
18
18
 
19
+ function willRespond(account) {
20
+ return canRespond(account)
21
+ && account?.enabled !== false
22
+ && (account?.state == null || account.state === 'ready');
23
+ }
24
+
19
25
  function eventId(event) {
20
26
  const chatId = String(
21
27
  event?.message?.chatId || event?.chat?.id || ''
@@ -66,11 +72,50 @@ function usernameTarget(value) {
66
72
  return username ? `@${username}` : '';
67
73
  }
68
74
 
75
+ function chatKey(value) {
76
+ return String(value || '').trim().toLowerCase();
77
+ }
78
+
79
+ function isGroupMessage(event) {
80
+ return event?.chat?.lineage === 'group';
81
+ }
82
+
83
+ function isFleetGeneratedMessage(event) {
84
+ const id = String(event?.message?.cid || event?.message?.id || '').trim();
85
+ return id.startsWith('_bot_') || id.startsWith('_traffic_');
86
+ }
87
+
69
88
  function requestAmount(event) {
70
89
  const amount = Number(event?.message?.amountSats);
71
90
  return Number.isSafeInteger(amount) && amount > 0 ? amount : null;
72
91
  }
73
92
 
93
+ function eventChatId(event) {
94
+ return String(event?.message?.chatId || event?.chat?.id || '').trim();
95
+ }
96
+
97
+ function sendToEventChat(client, event, message, options) {
98
+ const chatId = eventChatId(event);
99
+ return chatId
100
+ ? client.chat.sendTo(chatId, message, options)
101
+ : client.chat.send(event.peer, message, options);
102
+ }
103
+
104
+ function sendAttachmentToEventChat(client, event, attachment, options) {
105
+ const chatId = eventChatId(event);
106
+ return chatId
107
+ ? client.chat.sendAttachmentTo(chatId, attachment, options)
108
+ : client.chat.sendAttachment(event.peer, attachment, options);
109
+ }
110
+
111
+ function markEventChatRead(client, event) {
112
+ const options = { messageId: event.message.id };
113
+ const chatId = eventChatId(event);
114
+ return chatId
115
+ ? client.chat.markReadIn(chatId, options)
116
+ : client.chat.markRead(event.peer, options);
117
+ }
118
+
74
119
  async function runAction(
75
120
  journal,
76
121
  account,
@@ -106,7 +151,7 @@ async function sendUnderfunded(
106
151
  event,
107
152
  'faucet-insufficient',
108
153
  { replaySafe: true },
109
- () => client.chat.send(event.peer, BOT_UNDERFUNDED_TEXT, {
154
+ () => sendToEventChat(client, event, BOT_UNDERFUNDED_TEXT, {
110
155
  cid: botActionCid(event, 'funds', account),
111
156
  })
112
157
  );
@@ -116,8 +161,9 @@ async function sendUnderfunded(
116
161
  event,
117
162
  'faucet-balance',
118
163
  { replaySafe: true },
119
- () => client.chat.send(
120
- event.peer,
164
+ () => sendToEventChat(
165
+ client,
166
+ event,
121
167
  `i only have ${availableSats} sats`,
122
168
  { cid: botActionCid(event, 'funds-balance', account) }
123
169
  )
@@ -125,17 +171,22 @@ async function sendUnderfunded(
125
171
  }
126
172
 
127
173
  async function mirrorAttachment(account, client, event) {
128
- const downloaded = await client.chat.readAttachment(
129
- event.peer,
130
- event.message.id
131
- );
174
+ const chatId = eventChatId(event);
175
+ const downloaded = chatId
176
+ ? await client.chat.readAttachmentIn(chatId, event.message.id)
177
+ : await client.chat.readAttachment(event.peer, event.message.id);
132
178
  const payload = downloaded.message?.payload || {};
133
- return client.chat.sendAttachment(event.peer, {
179
+ return sendAttachmentToEventChat(client, event, {
134
180
  type: event.message.type,
135
181
  data: downloaded.bytes,
136
182
  mimeType: event.message.mime || payload.m,
137
183
  name: event.message.name || payload.n,
138
184
  caption: event.message.caption || payload.c,
185
+ size: event.message.size || payload.z,
186
+ width: event.message.width || payload.w,
187
+ height: event.message.height || payload.h,
188
+ duration: event.message.duration || payload.d,
189
+ audio: typeof event.message.audio === 'boolean' ? event.message.audio : payload.audio,
139
190
  }, {
140
191
  cid: botActionCid(event, 'echo', account),
141
192
  });
@@ -149,12 +200,13 @@ async function mirrorEcho(journal, account, client, event) {
149
200
  event,
150
201
  'echo-text',
151
202
  { replaySafe: true },
152
- () => client.chat.send(event.peer, event.message.text || '', {
203
+ () => sendToEventChat(client, event, event.message.text || '', {
153
204
  cid: botActionCid(event, 'echo', account),
154
205
  })
155
206
  );
156
207
  }
157
208
  if (event.message.type === 'req') {
209
+ if (event.chat?.lineage && event.chat.lineage !== 'direct') return null;
158
210
  const amountSats = requestAmount(event);
159
211
  if (!amountSats) return null;
160
212
  return runAction(
@@ -182,6 +234,7 @@ async function mirrorEcho(journal, account, client, event) {
182
234
  }
183
235
 
184
236
  async function serveFaucet(journal, account, client, event) {
237
+ if (event.chat?.lineage && event.chat.lineage !== 'direct') return null;
185
238
  const amountSats = requestAmount(event);
186
239
  if (!amountSats || !event.message.requestId) return null;
187
240
  const paymentActionId = actionId(account, event, 'faucet-payment');
@@ -310,14 +363,40 @@ export function createBotFleetPolicy(options = {}) {
310
363
  throw new Error('bot fleet baseline checkpoint required');
311
364
  }
312
365
  const managedUsernames = new Set();
366
+ const managedChatKeys = new Set();
313
367
  const usernameByProfile = new Map();
368
+ const responderProfiles = new Set();
369
+ const chatKeyByProfile = new Map();
314
370
  const claimStates = new Map();
315
371
 
372
+ function shouldMirror(event) {
373
+ if (isFleetGeneratedMessage(event)) return false;
374
+ if (!isGroupMessage(event)) {
375
+ return !managedUsernames.has(usernameTarget(event.peer));
376
+ }
377
+ if (
378
+ responderProfiles.size === 0
379
+ || chatKeyByProfile.size !== responderProfiles.size
380
+ ) {
381
+ return false;
382
+ }
383
+ const senderChatPK = chatKey(event.message?.senderChatPK);
384
+ if (!senderChatPK) return false;
385
+ return !managedChatKeys.has(senderChatPK);
386
+ }
387
+
316
388
  return Object.freeze({
317
389
  startFleet({ profiles }) {
390
+ managedUsernames.clear();
391
+ managedChatKeys.clear();
392
+ responderProfiles.clear();
393
+ chatKeyByProfile.clear();
318
394
  for (const profile of profiles) {
319
395
  assertRoles(profile);
320
396
  if (!canRespond(profile)) continue;
397
+ if (willRespond(profile)) {
398
+ responderProfiles.add(profile.profile);
399
+ }
321
400
  const username = usernameTarget(profile.username);
322
401
  if (username) managedUsernames.add(username);
323
402
  }
@@ -338,7 +417,18 @@ export function createBotFleetPolicy(options = {}) {
338
417
  );
339
418
  }
340
419
  usernameByProfile.set(account.profile, username);
341
- if (canRespond(account)) managedUsernames.add(username);
420
+ if (canRespond(account)) {
421
+ const accountChatPK = chatKey(current?.chatPK);
422
+ if (!accountChatPK) {
423
+ throw new Error(
424
+ `bot fleet profile has no chat identity: ${account.profile}`
425
+ );
426
+ }
427
+ responderProfiles.add(account.profile);
428
+ chatKeyByProfile.set(account.profile, accountChatPK);
429
+ managedChatKeys.add(accountChatPK);
430
+ managedUsernames.add(username);
431
+ }
342
432
  if (!baseline) {
343
433
  startClaimLoop(account, client, claimStates);
344
434
  }
@@ -346,11 +436,17 @@ export function createBotFleetPolicy(options = {}) {
346
436
  async stop({ account }) {
347
437
  await stopClaimLoop(account.profile, claimStates);
348
438
  const username = usernameByProfile.get(account.profile);
439
+ const accountChatPK = chatKeyByProfile.get(account.profile);
349
440
  usernameByProfile.delete(account.profile);
441
+ chatKeyByProfile.delete(account.profile);
442
+ if (accountChatPK) managedChatKeys.delete(accountChatPK);
350
443
  if (username) managedUsernames.delete(username);
351
444
  },
352
445
  async stopFleet() {
353
446
  managedUsernames.clear();
447
+ managedChatKeys.clear();
448
+ responderProfiles.clear();
449
+ chatKeyByProfile.clear();
354
450
  await checkpoint?.flush();
355
451
  },
356
452
  async onEvent({ account, client, event }) {
@@ -386,7 +482,7 @@ export function createBotFleetPolicy(options = {}) {
386
482
  );
387
483
  } else if (
388
484
  roles.has('echo') &&
389
- !managedUsernames.has(usernameTarget(event.peer))
485
+ shouldMirror(event)
390
486
  ) {
391
487
  result = await mirrorEcho(
392
488
  journal,
@@ -397,9 +493,7 @@ export function createBotFleetPolicy(options = {}) {
397
493
  }
398
494
  }
399
495
  if (markRead) {
400
- await client.chat.markRead(event.peer, {
401
- messageId: event.message.id,
402
- });
496
+ await markEventChatRead(client, event);
403
497
  }
404
498
  await checkpoint?.mark(account.profile, event);
405
499
  return result;
package/package.json CHANGED
@@ -41,5 +41,5 @@
41
41
  "start": "node src/cli.js",
42
42
  "lint": "eslint src --quiet"
43
43
  },
44
- "version": "0.58.0"
44
+ "version": "0.59.0"
45
45
  }
package/readme.md CHANGED
@@ -1,108 +1,61 @@
1
1
  # @glyphteck/veyl
2
2
 
3
- Programmable Veyl SDK with a high-level Node.js 22+ runtime and provider-neutral auth/passkey and account/vault entries for graphical clients. It exposes account, vault, profile, peer, encrypted chat, wallet, Lightning, withdrawal, invite-link, passkey, support, cache, settings, and fleet operations through one JavaScript client and a JSON CLI. A separate read-only MCP resource server exposes the packaged documentation.
3
+ Use a Veyl account from Node.js or the shell. The package provides:
4
4
 
5
- The SDK is a real client, not an admin API. It uses the same shared account, vault, peer, chat, encrypted-cache, and Spark wallet owners as web and iOS. Authentication, vault decryption, message encryption/signing, attachment encryption, and wallet signing remain local.
5
+ - account, profile, and peer management
6
+ - end-to-end encrypted notes, direct chats, groups, files, reactions, and payment requests
7
+ - a self-custodial Spark wallet for peer payments, Lightning, and Bitcoin withdrawals
8
+ - a JavaScript API, JSON CLI, and live events for agents and automations
6
9
 
7
- The public `open()` factory selects the Node runtime ports for filesystem profiles, Firebase transport, Spark, machine credentials, browser-assisted passkeys, and local encrypted cache. Official web and iOS clients use the `@glyphteck/veyl/auth` and `@glyphteck/veyl/account` entries with platform ports for the same passkey orchestration, authenticated user, encrypted settings, public-profile mutation, vault, network, presence, wallet, transfer, transaction, public Bitcoin data, chat-session, peer-directory, profile-search, support/reporting, authenticated push leases, account deletion, and teardown owners. They do not carry separate auth, passkey, account, profile, peer, search, chat, wallet, Bitcoin, transfer, transaction, settings, support, or deletion state machines. The browser/native credential ceremony, native notification presentation, and device-local credential cleanup remain platform behavior.
10
+ Requires Node.js 22 or newer.
8
11
 
9
- ## Install the SDK
12
+ ## install
10
13
 
11
14
  ```bash
12
- npm install @glyphteck/veyl
13
- # or: pnpm add @glyphteck/veyl
14
- # or: bun add @glyphteck/veyl
15
+ bun add @glyphteck/veyl
16
+ # or: npm install @glyphteck/veyl
15
17
  ```
16
18
 
17
- ## JavaScript quick start
18
-
19
- Creating an account accepts Veyl's [Terms](https://veyl.glyphteck.com/legal#terms) and [Community Rules](https://veyl.glyphteck.com/community-rules).
19
+ ## quick start
20
20
 
21
21
  ```js
22
22
  import { open } from '@glyphteck/veyl';
23
23
 
24
- const veyl = await open({ network: 'REGTEST' });
24
+ const veyl = await open({ profile: 'runner', network: 'REGTEST' });
25
25
  const accountKey = await veyl.account.create({ username: 'runner' });
26
26
  const vaultKey = await veyl.vault.create();
27
+
27
28
  await veyl.chat.send('@alice', 'hello');
28
29
  console.log(await veyl.wallet.balance());
29
30
  await veyl.close();
30
31
  ```
31
32
 
32
- `account.create()` returns the account key used to authenticate the machine. `vault.create()` returns the vault key that unlocks every vaulted feature. Both keys are saved in the local profile by default and must never be logged or committed.
33
-
34
- Machine credentials and passkeys describe authentication capability, not whether a person or agent operates the account. Third-party agents use ordinary public profiles; only namespace-authorized Glyphteck services carry the managed bot marker. Use `REGTEST` for disposable automation and `MAINNET` only for real funds.
33
+ `accountKey` authenticates the account. `vaultKey` unlocks its chat and wallet. By default, both are saved in `~/.veyl/profiles/runner.json` with owner-only permissions so later runs can call `account.login()` and `vault.unlock()` without passing them again.
35
34
 
36
- ## Optional CLI
35
+ To store the keys in your own secret manager, pass `saveKey: false` to both creation calls and provide them later through `open({ accountKey, vaultKey })`, `VEYL_ACCOUNT_KEY`, and `VEYL_VAULT_KEY`. Never log or commit either key.
37
36
 
38
- The package also ships a CLI for one-off shell work:
39
-
40
- ```bash
41
- npx veyl --network REGTEST account create @runner
42
- npx veyl vault create
43
- npx veyl account show
44
- npx veyl wallet address
45
- ```
37
+ Use `REGTEST` for development and disposable accounts. `MAINNET` uses real funds. Creating an account accepts Veyl's [Terms](https://veyl.glyphteck.com/legal#terms) and [Community Rules](https://veyl.glyphteck.com/community-rules).
46
38
 
47
- To create or log into a normal passkey account from a browser-capable terminal, add `--passkey` to `account create` or `account login`.
39
+ ## CLI
48
40
 
49
- Glyphteck's owner-only reserved namespace uses one local key:
41
+ The CLI prints JSON, which makes it suitable for shell scripts and agents.
50
42
 
51
43
  ```bash
52
- npx veyl namespace init
53
- npx veyl --network REGTEST account create @faucet --reserved
54
- ```
55
-
56
- Initialization writes the private seed only to `~/.veyl/namespace.seed` with mode `0600` and prints its public identity. The explicit `--reserved` account flow and Glyphteck fleet provisioning sign an exact, short-lived reserved-name claim when that file exists. General users do not possess the key and remain unable to claim reserved names.
57
-
58
- Long-running agents should keep one unlocked client or foreground session alive. This reuses the same wallet, peer, chat, cache, and live-listener owners instead of paying cold login and wallet boot cost for every command.
59
-
60
- Agents can also mount one conversation through `veyl.chat.enter('@alice')`, or run one local `openFleetOwner(...)` for many accounts. A versioned fleet root derives isolated account keys, vault keys, and Veyl master seeds by monotonically allocated account index, so the operator backs up one root for an unbounded fleet. Fleet policies remain normal API consumers; they receive no Firebase Admin or wallet shortcuts.
61
-
62
- The public SDK keeps ordinary errors simple. Only an ambiguous money mutation has a stable `operation_outcome_unknown` contract, including its caller correlation id and whether an identical retry is safe. This prevents an agent from self-correcting a network error into a duplicate spend without building a large error taxonomy.
63
-
64
- The [SDK bot fleet example](examples/bot-fleet/readme.md) is a complete public-client policy with replay checkpoints and a durable effect journal. The Glyphteck runtime consumes that same reference policy without receiving privileged backend or wallet access.
65
-
66
- ```bash
67
- veyl --profile runner session start
68
- # another shell
69
- veyl --profile runner --session default wallet balance
70
- veyl --profile runner --session default chat enter @alice
71
- veyl --profile runner --session default session events
72
- veyl --profile runner --session default session stop
73
- ```
74
-
75
- ## Documentation discovery
76
-
77
- ```bash
78
- veyl docs mcp
79
- ```
80
-
81
- The local stdio server advertises packaged Markdown as read-only MCP resources. It cannot access accounts, credentials, chats, or wallets. Agents that control their runtime should import the SDK directly.
82
-
83
- ## Local state
84
-
85
- Profiles live under `~/.veyl` by default. The directory and sensitive files use owner-only permissions.
86
-
87
- ```text
88
- VEYL_HOME=/secure/veyl-home
89
- VEYL_PROFILE=runner
90
- VEYL_NETWORK=REGTEST
91
- VEYL_ACCOUNT_KEY=...
92
- VEYL_VAULT_KEY=...
93
- VEYL_WEB_URL=https://veyl.glyphteck.com
44
+ bunx veyl --network REGTEST account create @runner
45
+ bunx veyl vault create
46
+ bunx veyl chat send @alice "hello"
47
+ bunx veyl wallet balance
94
48
  ```
95
49
 
96
- Use `REGTEST` for development and disposable accounts. Use `MAINNET` only for real funds.
50
+ Run `bunx veyl help` for the full command list. Long-running agents should keep one JavaScript client or persistent CLI session open instead of starting a new wallet and chat session for every action.
97
51
 
98
- ## Documentation
52
+ ## documentation
99
53
 
100
- - [CLI](docs/cli.md)
101
54
  - [JavaScript API](docs/api.md)
102
- - [Agent operation](docs/agents.md)
103
- - [Documentation discovery](docs/discovery.md)
104
- - [Parity and validation](docs/validation.md)
55
+ - [CLI reference](docs/cli.md)
56
+ - [agent operation](docs/agents.md)
57
+ - [MCP documentation discovery](docs/discovery.md)
105
58
 
106
- ## License
59
+ ## license
107
60
 
108
- The official Veyl JavaScript SDK is distributed under its [proprietary SDK license](LICENSE); it is not currently open source. The license permits use of the unmodified SDK with Veyl, including normal compiled or bundled use, but not modified or standalone redistribution.
61
+ This package is proprietary. The unmodified SDK may be used with Veyl; see the [license](LICENSE) for terms.