@idleware/ringi 0.7.2 → 0.8.1

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.
Files changed (201) hide show
  1. package/README.md +18 -11
  2. package/package.json +1 -1
  3. package/src/main/resources/skill/ringi/SKILL.md +3 -4
  4. package/target/dts/adapters/{transport-git → store-git}/branches.d.ts +1 -3
  5. package/target/dts/adapters/{transport-git → store-git}/index.d.ts +0 -1
  6. package/target/dts/adapters/store-git/marks.d.ts +21 -0
  7. package/target/dts/adapters/{transport-git → store-git}/remote/push.d.ts +0 -1
  8. package/target/dts/adapters/store-git/remote/stream-check.d.ts +12 -0
  9. package/target/dts/adapters/store-git/remote/stream-control.d.ts +8 -0
  10. package/target/dts/adapters/store-git/remote/stream-facts.d.ts +4 -0
  11. package/target/dts/adapters/store-git/remote/streams.d.ts +31 -0
  12. package/target/dts/adapters/store-git/remote/sync-streams.d.ts +2 -0
  13. package/target/dts/adapters/{transport-git → store-git}/remote/types.d.ts +5 -4
  14. package/target/dts/adapters/{transport-git → store-git}/remote.d.ts +1 -4
  15. package/target/dts/adapters/{transport-git → store-git}/repo.d.ts +3 -2
  16. package/target/dts/adapters/{transport-git → store-git}/signing.d.ts +0 -1
  17. package/target/dts/clients/cli/operator/hub-commands.d.ts +0 -2
  18. package/target/dts/clients/cli/operator/index.d.ts +1 -1
  19. package/target/dts/clients/cli/operator/sessions.d.ts +0 -1
  20. package/target/dts/clients/cli/operator/where-format.d.ts +1 -1
  21. package/target/dts/clients/cli/shared/index.d.ts +2 -2
  22. package/target/dts/clients/cli/shared/messages.d.ts +0 -1
  23. package/target/dts/clients/cli/shared/profile.d.ts +0 -1
  24. package/target/dts/clients/cli/skill/index.d.ts +2 -1
  25. package/target/dts/core/agents/overview.d.ts +4 -7
  26. package/target/dts/core/agents/sessions/index.d.ts +0 -1
  27. package/target/dts/core/agents/sessions/join.d.ts +1 -2
  28. package/target/dts/core/hub/history/index.d.ts +1 -10
  29. package/target/dts/core/hub/ports/store.d.ts +35 -23
  30. package/target/dts/core/hub/rooms/address.d.ts +6 -0
  31. package/target/dts/core/hub/rooms/adoption.d.ts +7 -0
  32. package/target/dts/core/hub/rooms/compat.d.ts +3 -1
  33. package/target/dts/core/hub/rooms/contracts.d.ts +16 -0
  34. package/target/dts/core/hub/rooms/hubs.d.ts +3 -3
  35. package/target/dts/core/hub/rooms/index.d.ts +5 -6
  36. package/target/dts/core/hub/rooms/room-catalogue.d.ts +9 -0
  37. package/target/dts/core/hub/rooms/room-life.d.ts +10 -0
  38. package/target/dts/core/hub/rooms/room-view.d.ts +3 -0
  39. package/target/dts/core/hub/rooms/rooms.d.ts +4 -2
  40. package/target/dts/core/hub/signals/check.d.ts +2 -2
  41. package/target/dts/core/hub/signals/model.d.ts +2 -10
  42. package/target/dts/core/hub/signals/pins.d.ts +2 -2
  43. package/target/dts/core/hub/usecases/config.d.ts +0 -1
  44. package/target/dts/core/hub/usecases/context.d.ts +1 -2
  45. package/target/dts/core/hub/usecases/hubs.d.ts +1 -8
  46. package/target/dts/core/hub/usecases/invite.d.ts +12 -0
  47. package/target/dts/core/hub/usecases/room.d.ts +0 -1
  48. package/target/dts/core/hub/usecases/rooms.d.ts +9 -7
  49. package/target/dts/core/hub/usecases/roster.d.ts +1 -2
  50. package/target/dts/core/hub/work/index.d.ts +0 -1
  51. package/target/dts/index.d.ts +1 -1
  52. package/target/esm/adapters/{tool-claude → agent-claude}/index.js +2 -2
  53. package/target/esm/adapters/{tool-codex → agent-codex}/index.js +2 -2
  54. package/target/esm/adapters/{tool-cursor → agent-cursor}/index.js +1 -1
  55. package/target/esm/adapters/{tool-traces → agent-traces}/index.js +1 -1
  56. package/target/esm/adapters/state-fs/index.js +2 -2
  57. package/target/esm/adapters/{transport-git → store-git}/branches.js +21 -27
  58. package/target/esm/adapters/{transport-git → store-git}/index.js +10 -28
  59. package/target/esm/adapters/store-git/marks.js +86 -0
  60. package/target/esm/adapters/{transport-git → store-git}/remote/commits.js +3 -1
  61. package/target/esm/adapters/{transport-git → store-git}/remote/git.js +8 -1
  62. package/target/esm/adapters/{transport-git → store-git}/remote/keys.js +8 -3
  63. package/target/esm/adapters/{transport-git → store-git}/remote/push-retry.js +24 -6
  64. package/target/esm/adapters/store-git/remote/push-validate.js +19 -0
  65. package/target/esm/adapters/{transport-git → store-git}/remote/push.js +13 -14
  66. package/target/esm/adapters/store-git/remote/stream-check.js +44 -0
  67. package/target/esm/adapters/store-git/remote/stream-control.js +33 -0
  68. package/target/esm/adapters/store-git/remote/stream-facts.js +40 -0
  69. package/target/esm/adapters/store-git/remote/streams.js +76 -0
  70. package/target/esm/adapters/store-git/remote/sync-streams.js +28 -0
  71. package/target/esm/adapters/store-git/remote/sync.js +59 -0
  72. package/target/esm/adapters/{transport-git → store-git}/remote/types.js +0 -3
  73. package/target/esm/adapters/{transport-git → store-git}/remote.js +1 -4
  74. package/target/esm/adapters/{transport-git → store-git}/repo.js +42 -4
  75. package/target/esm/adapters/{transport-git → store-git}/signing.js +1 -9
  76. package/target/esm/adapters/store-git/transport.js +60 -0
  77. package/target/esm/clients/cli/agent/index.js +1 -1
  78. package/target/esm/clients/cli/agent/session.js +10 -10
  79. package/target/esm/clients/cli/commands.js +5 -5
  80. package/target/esm/clients/cli/context.js +1 -1
  81. package/target/esm/clients/cli/help.js +15 -21
  82. package/target/esm/clients/cli/operator/heartbeat-format.js +1 -1
  83. package/target/esm/clients/cli/operator/heartbeat.js +1 -2
  84. package/target/esm/clients/cli/operator/hub-commands.js +6 -19
  85. package/target/esm/clients/cli/operator/hub.js +2 -2
  86. package/target/esm/clients/cli/operator/index.js +2 -2
  87. package/target/esm/clients/cli/operator/room.js +25 -32
  88. package/target/esm/clients/cli/operator/sessions.js +4 -22
  89. package/target/esm/clients/cli/operator/where-format.js +2 -1
  90. package/target/esm/clients/cli/operator/where.js +5 -7
  91. package/target/esm/clients/cli/shared/index.js +2 -2
  92. package/target/esm/clients/cli/shared/messages.js +2 -12
  93. package/target/esm/clients/cli/shared/profile.js +1 -6
  94. package/target/esm/clients/cli/skill/index.js +14 -19
  95. package/target/esm/composition/index.js +4 -4
  96. package/target/esm/core/agents/heartbeat/pulse.js +4 -4
  97. package/target/esm/core/agents/overview.js +13 -25
  98. package/target/esm/core/agents/sessions/binding.js +1 -1
  99. package/target/esm/core/agents/sessions/index.js +3 -2
  100. package/target/esm/core/agents/sessions/join.js +17 -9
  101. package/target/esm/core/agents/sessions/resume.js +3 -6
  102. package/target/esm/core/hub/capabilities.js +1 -4
  103. package/target/esm/core/hub/history/index.js +23 -13
  104. package/target/esm/core/hub/ports/store.js +2 -2
  105. package/target/esm/core/hub/rooms/address.js +18 -0
  106. package/target/esm/core/hub/rooms/adoption.js +32 -0
  107. package/target/esm/core/hub/rooms/compat.js +39 -14
  108. package/target/esm/core/hub/rooms/contracts.js +62 -0
  109. package/target/esm/core/hub/rooms/hubs.js +33 -56
  110. package/target/esm/core/hub/rooms/index.js +2 -2
  111. package/target/esm/core/hub/rooms/messages.js +6 -6
  112. package/target/esm/core/hub/rooms/room-catalogue.js +31 -0
  113. package/target/esm/core/hub/rooms/room-life.js +59 -0
  114. package/target/esm/core/hub/rooms/room-view.js +39 -0
  115. package/target/esm/core/hub/rooms/rooms.js +26 -37
  116. package/target/esm/core/hub/signals/check.js +21 -26
  117. package/target/esm/core/hub/signals/index.js +10 -10
  118. package/target/esm/core/hub/signals/model.js +0 -2
  119. package/target/esm/core/hub/signals/pins.js +44 -16
  120. package/target/esm/core/hub/signals/write.js +8 -10
  121. package/target/esm/core/hub/usecases/actor.js +1 -1
  122. package/target/esm/core/hub/usecases/config.js +2 -2
  123. package/target/esm/core/hub/usecases/context.js +5 -8
  124. package/target/esm/core/hub/usecases/hubs.js +16 -33
  125. package/target/esm/core/hub/usecases/invite.js +26 -0
  126. package/target/esm/core/hub/usecases/room.js +4 -5
  127. package/target/esm/core/hub/usecases/rooms.js +40 -35
  128. package/target/esm/core/hub/usecases/roster.js +12 -21
  129. package/target/esm/core/hub/usecases/send.js +10 -6
  130. package/target/esm/core/hub/work/parse.js +1 -1
  131. package/target/esm/lib/fail.js +2 -2
  132. package/target/dts/adapters/transport-git/marks.d.ts +0 -20
  133. package/target/dts/adapters/transport-git/remote/bootstrap.d.ts +0 -7
  134. package/target/dts/adapters/transport-git/remote/heartbeat-name.d.ts +0 -6
  135. package/target/dts/adapters/transport-git/remote/heartbeat-object.d.ts +0 -10
  136. package/target/dts/adapters/transport-git/remote/heartbeat-push.d.ts +0 -2
  137. package/target/dts/adapters/transport-git/remote/heartbeat.d.ts +0 -7
  138. package/target/dts/adapters/transport-git/remote/signals.d.ts +0 -15
  139. package/target/dts/adapters/transport-git/remote/sync-heartbeat.d.ts +0 -2
  140. package/target/dts/adapters/transport-git/remote/sync-rooms.d.ts +0 -6
  141. package/target/dts/adapters/transport-git/remote/sync-tags.d.ts +0 -8
  142. package/target/dts/adapters/transport-git/tags.d.ts +0 -15
  143. package/target/dts/clients/cli/skill/targets.d.ts +0 -3
  144. package/target/dts/core/hub/usecases/staging.d.ts +0 -8
  145. package/target/esm/adapters/transport-git/marks.js +0 -82
  146. package/target/esm/adapters/transport-git/remote/bootstrap.js +0 -47
  147. package/target/esm/adapters/transport-git/remote/heartbeat-name.js +0 -19
  148. package/target/esm/adapters/transport-git/remote/heartbeat-object.js +0 -40
  149. package/target/esm/adapters/transport-git/remote/heartbeat-push.js +0 -44
  150. package/target/esm/adapters/transport-git/remote/heartbeat.js +0 -44
  151. package/target/esm/adapters/transport-git/remote/push-validate.js +0 -39
  152. package/target/esm/adapters/transport-git/remote/signals.js +0 -63
  153. package/target/esm/adapters/transport-git/remote/sync-heartbeat.js +0 -26
  154. package/target/esm/adapters/transport-git/remote/sync-rooms.js +0 -38
  155. package/target/esm/adapters/transport-git/remote/sync-tags.js +0 -48
  156. package/target/esm/adapters/transport-git/remote/sync.js +0 -38
  157. package/target/esm/adapters/transport-git/tags.js +0 -21
  158. package/target/esm/adapters/transport-git/transport.js +0 -29
  159. package/target/esm/clients/cli/skill/targets.js +0 -20
  160. package/target/esm/core/hub/usecases/staging.js +0 -60
  161. /package/target/dts/adapters/{tool-claude → agent-claude}/index.d.ts +0 -0
  162. /package/target/dts/adapters/{tool-codex → agent-codex}/index.d.ts +0 -0
  163. /package/target/dts/adapters/{tool-cursor → agent-cursor}/index.d.ts +0 -0
  164. /package/target/dts/adapters/{tool-traces → agent-traces}/index.d.ts +0 -0
  165. /package/target/dts/adapters/{transport-git → store-git}/files.d.ts +0 -0
  166. /package/target/dts/adapters/{transport-git → store-git}/remote/admit.d.ts +0 -0
  167. /package/target/dts/adapters/{transport-git → store-git}/remote/alarms.d.ts +0 -0
  168. /package/target/dts/adapters/{transport-git → store-git}/remote/branch-commit.d.ts +0 -0
  169. /package/target/dts/adapters/{transport-git → store-git}/remote/branch-control.d.ts +0 -0
  170. /package/target/dts/adapters/{transport-git → store-git}/remote/branch-scan.d.ts +0 -0
  171. /package/target/dts/adapters/{transport-git → store-git}/remote/branch.d.ts +0 -0
  172. /package/target/dts/adapters/{transport-git → store-git}/remote/cache.d.ts +0 -0
  173. /package/target/dts/adapters/{transport-git → store-git}/remote/commits.d.ts +0 -0
  174. /package/target/dts/adapters/{transport-git → store-git}/remote/forget.d.ts +0 -0
  175. /package/target/dts/adapters/{transport-git → store-git}/remote/git.d.ts +0 -0
  176. /package/target/dts/adapters/{transport-git → store-git}/remote/heartbeat-facts.d.ts +0 -0
  177. /package/target/dts/adapters/{transport-git → store-git}/remote/keys.d.ts +0 -0
  178. /package/target/dts/adapters/{transport-git → store-git}/remote/main.d.ts +0 -0
  179. /package/target/dts/adapters/{transport-git → store-git}/remote/process.d.ts +0 -0
  180. /package/target/dts/adapters/{transport-git → store-git}/remote/push-retry.d.ts +0 -0
  181. /package/target/dts/adapters/{transport-git → store-git}/remote/push-validate.d.ts +0 -0
  182. /package/target/dts/adapters/{transport-git → store-git}/remote/roster.d.ts +0 -0
  183. /package/target/dts/adapters/{transport-git → store-git}/remote/signature.d.ts +0 -0
  184. /package/target/dts/adapters/{transport-git → store-git}/remote/sync.d.ts +0 -0
  185. /package/target/dts/adapters/{transport-git → store-git}/run.d.ts +0 -0
  186. /package/target/dts/adapters/{transport-git → store-git}/transport.d.ts +0 -0
  187. /package/target/esm/adapters/{transport-git → store-git}/files.js +0 -0
  188. /package/target/esm/adapters/{transport-git → store-git}/remote/admit.js +0 -0
  189. /package/target/esm/adapters/{transport-git → store-git}/remote/alarms.js +0 -0
  190. /package/target/esm/adapters/{transport-git → store-git}/remote/branch-commit.js +0 -0
  191. /package/target/esm/adapters/{transport-git → store-git}/remote/branch-control.js +0 -0
  192. /package/target/esm/adapters/{transport-git → store-git}/remote/branch-scan.js +0 -0
  193. /package/target/esm/adapters/{transport-git → store-git}/remote/branch.js +0 -0
  194. /package/target/esm/adapters/{transport-git → store-git}/remote/cache.js +0 -0
  195. /package/target/esm/adapters/{transport-git → store-git}/remote/forget.js +0 -0
  196. /package/target/esm/adapters/{transport-git → store-git}/remote/heartbeat-facts.js +0 -0
  197. /package/target/esm/adapters/{transport-git → store-git}/remote/main.js +0 -0
  198. /package/target/esm/adapters/{transport-git → store-git}/remote/process.js +0 -0
  199. /package/target/esm/adapters/{transport-git → store-git}/remote/roster.js +0 -0
  200. /package/target/esm/adapters/{transport-git → store-git}/remote/signature.js +0 -0
  201. /package/target/esm/adapters/{transport-git → store-git}/run.js +0 -0
@@ -9,7 +9,7 @@ export const toMessage = (R, from, x, seq) => {
9
9
  if (d.invalid !== undefined) {
10
10
  return { ...base, invalid: d.invalid, subject: '', body: '', to: [], re: null, kind: null, state: null, assign: null, roles: null, hop: 0, bad: d.invalid, trailers: {} };
11
11
  }
12
- const p = parse(d.text, s => R.hub.store.isRef(s));
12
+ const p = parse(d.text, s => R.store.isRef(s));
13
13
  // The room profile's states are part of a message's validity.
14
14
  const state = p.state && !p.bad && !R.profile.states.includes(p.state) ? `State ${p.state} is not in profile ${R.profile.name}` : null;
15
15
  return { ...base, invalid: null, ...p, bad: p.bad ?? state };
@@ -17,15 +17,15 @@ export const toMessage = (R, from, x, seq) => {
17
17
  // The room as stored, read without a reader's lock or state: for an author working out the signals of its own message
18
18
  // (append runs inside reader locks too). Readers check signals themselves.
19
19
  const asStored = (R) => {
20
- const s = R.hub.store.room(R.name) ?? R.s;
21
- return link(new Map(R.hub.store.logIds(s).map(id => [id, R.hub.store.messages(s, id, R.hub.store.logHead(s, id)).map((x, seq) => toMessage(R, id, x, seq))])));
20
+ const s = R.store.room(R.name) ?? R.s;
21
+ return link(new Map(R.store.logIds(s).map(id => [id, R.store.messages(s, id, R.store.logHead(s, id)).map((x, seq) => toMessage(R, id, x, seq))])));
22
22
  };
23
23
  // Whether a message can make signals: it opens, assigns or moves work.
24
24
  const CARRIES = (p, R) => !p.bad && (p.kind === R.profile.opens || !!p.assign || !!p.state);
25
25
  // A message on the author's log, and the signals it made (core/hub/signals/write.ts).
26
26
  export const append = (R, who, text, entries = []) => {
27
- const id = R.hub.store.append(R.s, who, text, entries);
28
- if (CARRIES(parse(text, s => R.hub.store.isRef(s)), R)) {
27
+ const id = R.store.append(R.s, who, text, entries);
28
+ if (CARRIES(parse(text, s => R.store.isRef(s)), R)) {
29
29
  const all = asStored(R);
30
30
  const m = all.get(id);
31
31
  if (m)
@@ -36,7 +36,7 @@ export const append = (R, who, text, entries = []) => {
36
36
  export const withSizes = (R, m) => {
37
37
  if (!m.files.length)
38
38
  return [];
39
- const sizes = R.hub.store.sizes(m.id, m.files.map(f => f.path));
39
+ const sizes = R.store.sizes(m.id, m.files.map(f => f.path));
40
40
  return m.files.map(f => ({ ...f, size: sizes.get(f.path) ?? null }));
41
41
  };
42
42
  //# sourceMappingURL=messages.js.map
@@ -0,0 +1,31 @@
1
+ // Local endpoint addresses, independent store handles and machine-wide room names.
2
+ import path from 'node:path';
3
+ import { hubPorts } from '../ports/index.js';
4
+ import { fail } from '../../../lib/index.js';
5
+ import { ID_RE } from '../protocol/index.js';
6
+ import { hubNames } from './hubs.js';
7
+ import { storeOf, transportOf } from './address.js';
8
+ export const endpointKey = (H, name) => ['hub', H.name, 'rooms', name, 'endpoint'];
9
+ export const roomDir = (H, name) => path.join(H.dir, 'rooms', name);
10
+ export const transportFor = (H, e) => transportOf(H.name, e.store.remote ? storeOf(e.store.remote).type : H.d.store.type);
11
+ export const roomLock = (H, name) => ID_RE.test(name) ? `room-${H.d.genesis}-${name}` : fail(`bad room name ${name}`);
12
+ export const uniqueName = (H, name) => {
13
+ for (const hub of hubNames())
14
+ if (hub !== H.name && hubPorts().state.list(['hub', hub, 'rooms']).includes(name))
15
+ fail(`room name ${name} already belongs to hub ${hub}; nothing is replaced`);
16
+ };
17
+ export const roomNames = (H) => hubPorts().state.list(['hub', H.name, 'rooms'])
18
+ .filter(name => ID_RE.test(name) && hubPorts().state.read(endpointKey(H, name), null) !== null);
19
+ const stores = new Map();
20
+ export const selectedStore = (H, endpoint) => {
21
+ const key = JSON.stringify([H.dir, H.d, endpoint]);
22
+ const cached = stores.get(key);
23
+ if (cached)
24
+ return cached;
25
+ const store = transportFor(H, endpoint).open(roomDir(H, endpoint.name), H.d, endpoint);
26
+ stores.set(key, store);
27
+ if (stores.size > 128)
28
+ stores.delete(stores.keys().next().value);
29
+ return store;
30
+ };
31
+ //# sourceMappingURL=room-catalogue.js.map
@@ -0,0 +1,59 @@
1
+ // Explicit room creation, attachment and local purge. Every installation preserves existing pins on failure.
2
+ import { hubPorts, locked } from '../ports/index.js';
3
+ import { fail } from '../../../lib/index.js';
4
+ import { ID_RE } from '../protocol/index.js';
5
+ import { openContract } from './contracts.js';
6
+ import { endpointRefusal } from './compat.js';
7
+ import { openedRoom } from './room-view.js';
8
+ import { endpointKey, roomDir, roomLock, transportFor, uniqueName } from './room-catalogue.js';
9
+ const checkName = (H, name) => {
10
+ if (!ID_RE.test(name) || name === 'main' || name === 'chat')
11
+ fail(`bad room name ${name}`);
12
+ uniqueName(H, name);
13
+ };
14
+ const sameEndpoint = (name, old, next) => {
15
+ if (!old)
16
+ return;
17
+ const identity = old.genesis === next.genesis && old.hubGenesis === next.hubGenesis;
18
+ if (!identity || old.store.path !== next.store.path)
19
+ fail(`room ${name}: an endpoint already pins another history; nothing is replaced`);
20
+ };
21
+ export const createRoom = (H, name, { who, text, files, remote }) => locked('room-names', () => locked(roomLock(H, name), () => {
22
+ checkName(H, name);
23
+ if (remote && !H.signed)
24
+ fail('a remote room requires a signed hub');
25
+ const st = hubPorts().state;
26
+ if (st.list(['hub', H.name, 'rooms']).includes(name))
27
+ fail(`room ${name} exists; an unregistered repository is retained for review`);
28
+ const { store: source } = openContract(H);
29
+ const endpoint = { schema: 'ringi-room/1', name, hubGenesis: H.d.genesis, genesis: '', store: { path: 'store', ...(remote ? { remote } : {}) } };
30
+ const store = transportFor(H, endpoint).fork(roomDir(H, name), H.d, source, endpoint);
31
+ endpoint.genesis = store.createRoom(name, who, text, files);
32
+ const R = openedRoom(H, endpoint, store);
33
+ st.write(endpointKey(H, name), endpoint);
34
+ return R;
35
+ }));
36
+ export const attachRoom = (H, name, endpoint) => locked('room-names', () => locked(roomLock(H, name), () => {
37
+ checkName(H, name);
38
+ const why = endpointRefusal(H.d, name, endpoint);
39
+ if (why)
40
+ fail(why);
41
+ const st = hubPorts().state;
42
+ const old = st.read(endpointKey(H, name), null);
43
+ sameEndpoint(name, old, endpoint);
44
+ const t = transportFor(H, endpoint);
45
+ const store = old ? t.open(roomDir(H, name), H.d, endpoint) : t.attachRoom(roomDir(H, name), H.d, endpoint);
46
+ store.sync();
47
+ const R = openedRoom(H, endpoint, store);
48
+ st.write(endpointKey(H, name), endpoint);
49
+ return R;
50
+ }));
51
+ // The caller requires the operator's explicit local-purge confirmation. Missing remotes never invoke this.
52
+ export const deleteRoom = (R) => locked(roomLock(R.hub, R.name), () => {
53
+ const current = hubPorts().state.read(endpointKey(R.hub, R.name), null);
54
+ if (!current || current.genesis !== R.genesis || current.hubGenesis !== R.hub.d.genesis)
55
+ fail('room endpoint moved before local purge; nothing is removed');
56
+ hubPorts().state.remove(['hub', R.hub.name, 'rooms', R.name]);
57
+ hubPorts().state.remove(['readers', R.hub.d.genesis, R.genesis]);
58
+ });
59
+ //# sourceMappingURL=room-life.js.map
@@ -0,0 +1,39 @@
1
+ // A selected, verified repository interpreted as one room with its frozen contract and profile.
2
+ import { DEFAULT_PROFILE, parseProfile } from '../work/index.js';
3
+ import { fail, strictJson } from '../../../lib/index.js';
4
+ import { checkedContract, readProfileResource } from './contracts.js';
5
+ import { contractAt } from './compat.js';
6
+ // The genesis names its room, in the root's hand: a history offered under another name is not that room.
7
+ const checkNamed = (s, name) => {
8
+ const subject = s.control[0]?.rev === s.genesis ? s.control[0].text.split('\n')[0] : '(none)';
9
+ if (subject !== `room ${name}`)
10
+ fail(`room ${name}: its genesis is "${subject}"; refusing a room's history under another name`);
11
+ };
12
+ // The room's profile is fixed when the room is made: its content is copied into the genesis control entry, so no
13
+ // later change of the hub's profiles or contract reinterprets the room's history. Rooms made before this carry none
14
+ // and use the default profile of the contract they were created on.
15
+ const profileOf = (H, store, s, name) => {
16
+ const base = contractAt(store, H.name, s.base).profile ?? DEFAULT_PROFILE;
17
+ const fallback = () => base === DEFAULT_PROFILE ? readProfileResource(DEFAULT_PROFILE) : fail(`room ${name}: unknown profile ${base}`);
18
+ return store.controlFile(s, 'profile.json', s.genesis) ?? store.contractFile(s.base, `profiles/${base}.json`) ?? fallback();
19
+ };
20
+ const membersOf = (store, s, name) => store.roster(s)?.members ?? strictJson(store.controlFile(s, 'members.json') ?? '{}', `room ${name}: members.json`);
21
+ // The root's control records set the quorum; the latest one counts.
22
+ const quorumOf = (s) => s.control.reduce((q, rec) => Number(/^Quorum: ([1-9][0-9]?)$/m.exec(rec.text)?.[1] ?? NaN) || q, null);
23
+ export const openedRoom = (H, endpoint, store) => {
24
+ const { name } = endpoint;
25
+ checkedContract(H, store);
26
+ const s = store.room(name) ?? fail(`room ${name}: its endpoint has no chat branch`);
27
+ if (store.contractHead() !== s.base)
28
+ fail(`ALARM room ${name}: its frozen contract changed; nothing is delivered`);
29
+ if (s.genesis !== endpoint.genesis)
30
+ fail(`ALARM room ${name}: chat changes its pinned genesis`);
31
+ checkNamed(s, name);
32
+ const current = s.base;
33
+ if (!store.extends(current, store.contractHead()))
34
+ fail(`room ${name}: its applied contract is outside main`);
35
+ const contract = contractAt(store, H.name, current);
36
+ return { hub: H, endpoint, store, s, name, head: s.head, genesis: s.genesis, contractRev: current, contract,
37
+ profile: parseProfile(profileOf(H, store, s, name), `room ${name}: profile`), members: membersOf(store, s, name), quorum: quorumOf(s) };
38
+ };
39
+ //# sourceMappingURL=room-view.js.map
@@ -1,55 +1,44 @@
1
1
  // rooms: a room opened with its contract, profile, members and quorum; and the root's control changes.
2
- import { DEFAULT_PROFILE, parseProfile } from '../work/index.js';
3
- import { locked } from '../ports/index.js';
2
+ import { DEFAULT_PROFILE } from '../work/index.js';
3
+ import { hubPorts, locked } from '../ports/index.js';
4
4
  import { fail, strictJson } from '../../../lib/index.js';
5
5
  import { compose } from '../protocol/index.js';
6
- import { readProfileResource } from './hubs.js';
7
- import { contractAt } from './compat.js';
8
- export const roomNames = (H) => H.store.roomNames();
6
+ import { openContract, readProfileResource } from './hubs.js';
7
+ import { endpointRefusal } from './compat.js';
8
+ import { ID_RE } from '../protocol/index.js';
9
+ import { endpointKey, roomLock, roomNames, selectedStore } from './room-catalogue.js';
10
+ import { openedRoom } from './room-view.js';
11
+ export { roomNames } from './room-catalogue.js';
12
+ export { createRoom, attachRoom, deleteRoom } from './room-life.js';
9
13
  const missing = (H, name, quiet) => quiet ? null : fail(`no room ${name} in hub ${H.name}; rooms: ${roomNames(H).join(', ') || 'none'}`);
10
- // The genesis names its room, in the root's hand: a history offered under another name is not that room.
11
- const checkNamed = (s, name) => {
12
- const subject = s.control[0]?.rev === s.genesis ? s.control[0].text.split('\n')[0] : '(none)';
13
- if (subject !== `room ${name}`)
14
- fail(`room ${name}: its genesis is "${subject}"; refusing a room's history under another name`);
15
- };
16
- // The contract the room follows: the hub contract it was created on, or the last one the operator moved it to.
17
- const contractRev = (s) => s.control.reduce((current, rec) => /^Contract: ([0-9a-f]{40,64})$/m.exec(rec.text)?.[1] ?? current, s.base);
18
- // The room's profile is fixed when the room is made: its content is copied into the genesis control entry, so no
19
- // later change of the hub's profiles or contract reinterprets the room's history. Rooms made before this carry none
20
- // and use the default profile of the contract they were created on.
21
- const profileOf = (H, s, name) => {
22
- const base = contractAt(H.store, H.name, s.base).profile ?? DEFAULT_PROFILE;
23
- const fallback = () => base === DEFAULT_PROFILE ? readProfileResource(DEFAULT_PROFILE) : fail(`room ${name}: unknown profile ${base}`);
24
- return H.store.controlFile(s, 'profile.json', s.genesis) ?? H.store.contractFile(s.base, `profiles/${base}.json`) ?? fallback();
25
- };
26
- const membersOf = (H, s, name) => H.store.roster(s)?.members ?? strictJson(H.store.controlFile(s, 'members.json') ?? '{}', `room ${name}: members.json`);
27
- // The root's control records set the quorum; the latest one counts.
28
- const quorumOf = (s) => s.control.reduce((q, rec) => Number(/^Quorum: ([1-9][0-9]?)$/m.exec(rec.text)?.[1] ?? NaN) || q, null);
29
- export const openRoom = (H, name, { quiet = false } = {}) => {
14
+ export const openRoom = (H, name, { quiet = false, refresh = false } = {}) => {
30
15
  if (!name)
31
16
  return fail('no room: the operator creates one with ringi room create <id>, or picks one with ringi room switch <id>');
32
- const s = H.store.room(name);
33
- if (!s)
17
+ if (!ID_RE.test(name))
18
+ fail(`bad room name ${name}`);
19
+ const endpoint = hubPorts().state.read(endpointKey(H, name), null);
20
+ if (!endpoint)
34
21
  return missing(H, name, quiet);
35
- checkNamed(s, name);
36
- const current = contractRev(s);
37
- const contract = current === H.main ? H.contract : contractAt(H.store, H.name, current);
38
- return { hub: H, s, name, head: s.head, genesis: s.genesis, contractRev: current, contract,
39
- profile: parseProfile(profileOf(H, s, name), `room ${name}: profile`), members: membersOf(H, s, name), quorum: quorumOf(s) };
22
+ const why = endpointRefusal(H.d, name, endpoint);
23
+ if (why)
24
+ fail(why);
25
+ const store = selectedStore(H, endpoint);
26
+ void refresh;
27
+ store.sync();
28
+ return openedRoom(H, endpoint, store);
40
29
  };
41
30
  export const room = (H, name) => openRoom(H, name);
42
31
  export const ROOM_MD = (name) => `# room ${name}\n\nA ringi room: members.json lists who takes part; profile.json is what a piece of work is here, fixed when the room was made.\n`;
43
- export const profileText = (H, name) => H.store.contractFile(H.main, `profiles/${name}.json`) ?? (name === DEFAULT_PROFILE ? readProfileResource(DEFAULT_PROFILE) : null);
32
+ export const profileText = (H, name) => (() => { const { store, main } = openContract(H); return store.contractFile(main, `profiles/${name}.json`) ?? (name === DEFAULT_PROFILE ? readProfileResource(DEFAULT_PROFILE) : null); })();
44
33
  // A control change under the hub lock. In a signed room membership changes only through `room admit`.
45
- export const controlChange = (R, who, subject, mutate, extra = []) => locked(`hub-${R.hub.name}`, () => {
46
- const s = R.hub.store.room(R.name) ?? fail(`room ${R.name} is gone`);
47
- const members = strictJson(R.hub.store.controlFile(s, 'members.json') ?? '{}', `room ${R.name}: members.json`);
34
+ export const controlChange = (R, who, subject, mutate, extra = []) => locked(roomLock(R.hub, R.name), () => {
35
+ const s = R.store.room(R.name) ?? fail(`room ${R.name} is gone`);
36
+ const members = strictJson(R.store.controlFile(s, 'members.json') ?? '{}', `room ${R.name}: members.json`);
48
37
  const before = JSON.stringify(members);
49
38
  mutate(members);
50
39
  if (R.hub.signed && JSON.stringify(members) !== before)
51
40
  fail('in a signed room, membership changes only by ringi room admit');
52
- R.head = R.hub.store.updateControl(s, who, compose(subject, [['To', '*'], ['Kind', 'room'], ...extra]), { 'members.json': JSON.stringify(members, null, 2) + '\n' });
41
+ R.head = R.store.updateControl(s, who, compose(subject, [['To', '*'], ['Kind', 'room'], ...extra]), { 'members.json': JSON.stringify(members, null, 2) + '\n' });
53
42
  R.members = members;
54
43
  });
55
44
  //# sourceMappingURL=rooms.js.map
@@ -1,33 +1,28 @@
1
- // check: a signal holds when its author made it, it is named after and points at a message of its author in this room,
2
- // and that message made exactly this signal; a mark holds when it is possible.
3
1
  import { effective } from './effective.js';
4
2
  import { markWhy } from './marks.js';
5
3
  import { MARKS } from './model.js';
6
- const namedAfter = (id, target) => id.length === 12 && target.startsWith(id);
7
- const signalWhy = (R, all, actor, m, signal) => {
8
- if (m.from !== actor)
4
+ const meaning = (R, all, x, m) => {
5
+ if (x.roomGenesis !== R.genesis)
6
+ return 'names another room genesis';
7
+ if (MARKS.includes(x.signal))
8
+ return markWhy(R, x.actor, m, x.signal);
9
+ if (m.from !== x.actor)
9
10
  return `points at a message of ${m.from}`;
10
- return effective(R, all, m).includes(signal) ? undefined : `its message did not make ${signal}`;
11
+ return effective(R, all, m).includes(x.signal) ? undefined : `its message did not make ${x.signal}`;
11
12
  };
12
- const whyNot = (R, all, x, id, signal) => {
13
- const m = all.get(x.target);
14
- const shape = [
15
- [!x.target, 'is not an annotated tag on a message'],
16
- [x.tagger !== x.actor, `was made by ${x.tagger}, not ${x.actor}`],
17
- [!namedAfter(id, x.target), 'is named after another message'],
18
- [!m, 'points at no accepted message of this room'],
19
- ];
20
- const wrong = shape.find(([bad]) => bad);
21
- if (wrong)
22
- return wrong[1];
23
- const meaning = MARKS.includes(signal) ? markWhy(R, x.actor, m, signal) : signalWhy(R, all, x.actor, m, signal);
24
- // A signal is its name and its target; any text besides a signature is not one.
25
- return meaning ?? (x.text === '' ? undefined : 'carries text; a signal has none');
13
+ export const check = (R, all, records) => {
14
+ const stopped = new Set(), tuples = new Set();
15
+ return records.map(x => {
16
+ const m = all.get(x.target), pending = stopped.has(x.actor) || !m;
17
+ const why = pending ? 'awaits an accepted stream prefix and message' : meaning(R, all, x, m);
18
+ if (why)
19
+ stopped.add(x.actor);
20
+ const tuple = JSON.stringify([x.actor, x.target, x.signal]);
21
+ const duplicate = tuples.has(tuple);
22
+ if (!why)
23
+ tuples.add(tuple);
24
+ return { ref: x.id, actor: x.actor, signal: x.signal, target: x.target, ok: !why && !duplicate, why,
25
+ ...((pending || duplicate) ? { pending: true } : {}) };
26
+ });
26
27
  };
27
- export const check = (R, all, records) => records.map(x => {
28
- const dot = x.name.lastIndexOf('.');
29
- const signal = x.name.slice(0, dot);
30
- const why = whyNot(R, all, x, x.name.slice(dot + 1), signal);
31
- return { ref: x.ref, actor: x.actor, signal, target: x.target, ok: !why, why };
32
- });
33
28
  //# sourceMappingURL=check.js.map
@@ -1,16 +1,16 @@
1
- // signals: explicit markers of what happens in a room, beside the messages that carry it. Every effective transition
2
- // (work opened, assigned, a state reached, a proposal, a counted stamp, the decision, a counted usage receipt) is also
3
- // a signal of its author on the message that made it: in a git store an annotated tag `<author>.<signal>.<message12>`
4
- // on that message's commit, with no text of its own, signed on a signed hub. The message's id keeps the name unique;
5
- // the name, target, author and time are the tag itself. Signals are visible and checkable with the store's own tools
6
- // (`git tag -v`, a forge's tag list and tag-push webhooks) and cheap to poll.
1
+ // signals: explicit markers of what happens in a room, beside the messages that carry it (signals-2). Every effective
2
+ // transition (work opened, assigned, a state reached) is also a signal of its author: a record of the author's own
3
+ // stream naming the room, the control it writes under, the message and the signal; in a git store a commit on
4
+ // `<author>/signals`, signed on a signed hub. The author, message and signal identify it, so writing it again adds
5
+ // nothing. Signals are visible with the store's own tools (`git log`, a forge's branches and push webhooks) and cheap
6
+ // to poll.
7
7
  //
8
8
  // The messages stay the truth. A signal is made only for a transition the fold actually counted (a State the author
9
- // had no right to makes none). A reader checks every signal against its message and pins the stored object it
10
- // accepted; a signal that disagrees, or an accepted one that changes or disappears, is an alarm. A signal missing
11
- // after a crash between message and tag changes nothing and is restored with `signals --repair`.
9
+ // had no right to makes none). A reader checks every signal against its message and pins each stream's accepted
10
+ // prefix; a signal that disagrees, or an accepted stream that changes or disappears, is an alarm. A signal missing
11
+ // after a crash between message and signal changes nothing and is restored with `signals --repair`.
12
12
  //
13
- // Marks are the second layer, in the same namespace: where attention is, not what was decided. `read` says a message
13
+ // Marks are the second layer, in the same stream: where attention is, not what was decided. `read` says a message
14
14
  // reached its reader (made on delivery); `taken` says a member took a piece of work. A mark changes no state
15
15
  // or assignment. It is checked only for being possible: its target is an accepted message of the room, its
16
16
  // actor a member; `read` only for a message addressed to the actor or to all, `taken` only for a message that opens
@@ -1,5 +1,3 @@
1
1
  // model: what the folds need of a room, and a signal as a store returns it.
2
2
  export const MARKS = ['read', 'taken'];
3
- // A signal's name after its actor: the signal and the message's short id.
4
- export const signalName = (id, signal) => `${signal}.${id.slice(0, 12)}`;
5
3
  //# sourceMappingURL=model.js.map
@@ -1,21 +1,49 @@
1
- // pins: a reader's pins, the stored object of each signal it accepted. A changed or vanished one is an alarm.
2
- // A signal's identity in any store: its tag's name `<actor>.<signal>.<message12>`.
3
- const keyOf = (ref) => ref.split('/').at(-1);
1
+ const grouped = (records) => {
2
+ const streams = new Map();
3
+ for (const x of records) {
4
+ const stream = streams.get(x.actor) ?? [];
5
+ stream.push(x);
6
+ streams.set(x.actor, stream);
7
+ }
8
+ return streams;
9
+ };
10
+ const suffix = (stream, pin) => {
11
+ if (!pin)
12
+ return stream;
13
+ const at = stream.findIndex(x => x.id === pin);
14
+ return at === -1 ? null : stream.slice(at + 1);
15
+ };
16
+ const advance = (stream, verdict, parent) => {
17
+ for (const x of stream) {
18
+ if (x.parent !== parent)
19
+ return { head: parent, alarm: `signal stream ${x.actor} is not a continuation of ${parent}` };
20
+ const c = verdict.get(x.id);
21
+ if (!c || (!c.ok && c.why))
22
+ break;
23
+ // Duplicate valid tuples extend the prefix, though they count only once in the fold.
24
+ parent = x.id;
25
+ }
26
+ return { head: parent };
27
+ };
28
+ const acceptActor = (actor, stream, verdict, pin) => {
29
+ const tail = suffix(stream, pin);
30
+ if (!tail)
31
+ return { head: pin, alarm: `signal stream ${actor} does not include its accepted prefix ${pin}` };
32
+ return advance(tail, verdict, pin ?? stream[0].roomGenesis);
33
+ };
4
34
  export const pins = (records, checked, accepted = {}) => {
5
- const now = new Map(records.map(x => [`${x.actor}.${x.name}`, x]));
6
- const alarms = [];
7
- const next = {};
8
- for (const [k, object] of Object.entries(accepted)) {
9
- const key = keyOf(k), x = now.get(key);
10
- next[key] = object;
11
- if (!x)
12
- alarms.push(`signal ${key} was removed after it was accepted`);
13
- else if (x.object !== object)
14
- alarms.push(`signal ${x.ref} was replaced after it was accepted`);
35
+ const next = { ...accepted }, alarms = [], streams = grouped(records);
36
+ const verdict = new Map(checked.map(c => [c.ref, c]));
37
+ for (const actor of Object.keys(accepted))
38
+ if (!streams.has(actor))
39
+ alarms.push(`signal stream ${actor} was removed after it was accepted`);
40
+ for (const [actor, stream] of streams) {
41
+ const result = acceptActor(actor, stream, verdict, accepted[actor]);
42
+ if (result.head !== stream[0].roomGenesis)
43
+ next[actor] = result.head;
44
+ if (result.alarm)
45
+ alarms.push(result.alarm);
15
46
  }
16
- for (const c of checked)
17
- if (c.ok && !next[keyOf(c.ref)])
18
- next[keyOf(c.ref)] = now.get(keyOf(c.ref)).object;
19
47
  return { accepted: next, alarms };
20
48
  };
21
49
  //# sourceMappingURL=pins.js.map
@@ -1,26 +1,24 @@
1
1
  // signals (write side): an author stores the signals its message made and the marks it puts; readers check them
2
2
  // (index.ts).
3
- import { effective, markWhy, signalName } from './index.js';
3
+ import { effective, markWhy } from './index.js';
4
4
  import { fail } from '../../../lib/index.js';
5
5
  // Puts one of the actor's marks on a message; false if it was already there.
6
6
  export const mark = (R, who, m, name) => {
7
7
  const why = markWhy(R, who, m, name);
8
8
  if (why)
9
9
  fail(`cannot mark ${m.id.slice(0, 12)} ${name}: ${why}`);
10
- if (R.hub.store.signals(R.s).some(x => x.actor === who && x.name === signalName(m.id, name)))
11
- return false;
12
- R.hub.store.signal(R.s, who, m.id, signalName(m.id, name));
13
- return true;
10
+ const exists = R.store.signals(R.s).some(x => x.actor === who && x.target === m.id && x.signal === name);
11
+ R.store.signal(R.s, who, m.id, name);
12
+ return !exists;
14
13
  };
15
14
  // Writes the signals of one of the author's own messages that are not stored yet.
16
15
  export const emit = (R, who, all, m) => {
17
- const have = new Set(R.hub.store.signals(R.s).filter(x => x.actor === who).map(x => x.name));
16
+ const have = new Set(R.store.signals(R.s).filter(x => x.actor === who).map(x => JSON.stringify([x.target, x.signal])));
18
17
  let n = 0;
19
18
  for (const signal of effective(R, all, m)) {
20
- if (have.has(signalName(m.id, signal)))
21
- continue;
22
- R.hub.store.signal(R.s, who, m.id, signalName(m.id, signal));
23
- n++;
19
+ R.store.signal(R.s, who, m.id, signal);
20
+ if (!have.has(JSON.stringify([m.id, signal])))
21
+ n++;
24
22
  }
25
23
  return n;
26
24
  };
@@ -1,3 +1,3 @@
1
1
  // actor: who acts, and in which room; every operation of a participant takes one.
2
- export const isRef = (R) => (x) => R.hub.store.isRef(x);
2
+ export const isRef = (R) => (x) => R.store.isRef(x);
3
3
  //# sourceMappingURL=actor.js.map
@@ -1,5 +1,5 @@
1
- // config: the operator's terminal context on this machine: the hub and the room a command means when it names none.
1
+ // config: the operator's terminal context on this machine: the room a command means when it names none.
2
2
  import { hubPorts } from '../ports/index.js';
3
- export const config = () => ({ hub: 'local', room: null, ...hubPorts().state.read(['config'], {}) });
3
+ export const config = () => ({ room: null, ...hubPorts().state.read(['config'], {}) });
4
4
  export const setConfig = (patch) => hubPorts().state.write(['config'], { ...config(), ...patch });
5
5
  //# sourceMappingURL=config.js.map
@@ -1,13 +1,10 @@
1
- // context: who acts as the operator and where (its terminal context, unless a command names a hub or a room), and what
1
+ // context: who acts as the operator and in which room (its terminal context, unless a command names one), and what
2
2
  // this build is. An agent acts through its session (core/agents/sessions/context.ts).
3
3
  import { hubPorts } from '../ports/index.js';
4
- import { CAPS, HUB_MAJOR, OPERATOR, config, openHub, room as roomOf } from '../rooms/index.js';
5
- // The operator, in the named hub and room, else in the terminal's current ones.
6
- export const operator = ({ hub, room } = {}) => {
7
- const H = openHub(hub ?? config().hub);
8
- const name = room ?? (hub && hub !== config().hub ? null : config().room);
9
- return { R: roomOf(H, name), me: OPERATOR };
10
- };
4
+ import { CAPS, HUB_MAJOR, OPERATOR, config } from '../rooms/index.js';
5
+ import { roomNamed } from './rooms.js';
6
+ // The operator, in the named room, else in the terminal's current one.
7
+ export const operator = ({ room } = {}) => ({ R: roomNamed(room), me: OPERATOR });
11
8
  export const about = () => ({ version: hubPorts().build.version, home: hubPorts().state.root, resources: hubPorts().build.resources, contract: `${HUB_MAJOR}.x`, capabilities: [...CAPS] });
12
9
  export { config };
13
10
  //# sourceMappingURL=context.js.map
@@ -1,47 +1,30 @@
1
- // hubs: the operator's hubs on this machine: list, create, switch, add a signed replica, amend the shared contract.
1
+ // hubs: the operator's hubs on this machine: list, create, amend the shared contract.
2
2
  import { DEFAULT_PROFILE, PROFILES_CAPABILITY, parseProfile } from '../work/index.js';
3
- import { OPERATOR, config, createHub, hubNames, openHub, roomNames, setConfig, transportOf } from '../rooms/index.js';
3
+ import { OPERATOR, amendContract, createHub, hubNames, openContract, openHub, roomNames } from '../rooms/index.js';
4
4
  import { hubPorts } from '../ports/index.js';
5
5
  import { fail, strictJson, withoutCredentials, words } from '../../../lib/index.js';
6
6
  import { ID_RE, compose } from '../protocol/index.js';
7
7
  // A remote as shown and handed to other machines: never with a password or token in it; each machine has its own.
8
8
  const shown = (url) => url ? withoutCredentials(url) : null;
9
- // The operator's terminal context picks the hub unless one is named.
10
- export const hubOf = (name) => openHub(name ?? config().hub);
9
+ // The named hub, or `local`.
10
+ export const hubOf = (name) => openHub(name ?? 'local');
11
11
  export const hubs = () => {
12
12
  hubPorts().state.ensure();
13
13
  if (!hubNames().length)
14
14
  openHub('local');
15
- const cur = config().hub;
16
15
  return hubNames().map(n => {
17
16
  const H = openHub(n);
18
- return { name: H.name, current: H.name === cur, store: H.d.store.type, trust: H.contract.trust, contract: H.contract.version,
19
- rooms: roomNames(H).length, remote: shown(H.d.store.remote), dir: H.dir };
17
+ return { name: H.name, store: H.d.store.type, trust: H.d.trust, contract: H.d.contract ? openContract(H).contract.version : null,
18
+ rooms: roomNames(H).length, remote: shown(H.d.contract?.remote), dir: H.dir };
20
19
  });
21
20
  };
22
- // A new hub; a signed one makes its root key here and returns what other machines need to pin it.
21
+ // A new hub; a signed one makes its root key here. Agents elsewhere never attach the hub: each joins its room.
23
22
  export const create = (name, { signed = false, url } = {}) => {
24
23
  if (!ID_RE.test(name))
25
24
  fail('usage: ringi hub create <name> [--signed [--url <git url>]]');
26
25
  const { custody } = createHub(name, { signed, url });
27
- const H = openHub(name), remote = shown(H.d.store.remote);
28
- return { H, rootKey: custody, remote,
29
- add: H.signed ? `ringi hub add ${name} ${remote ?? '<url>'} --root '${H.d.root}' --genesis ${H.d.genesis}` : null };
30
- };
31
- export const use = (name) => {
32
- openHub(name);
33
- setConfig({ hub: name, room: null });
34
- };
35
- // A replica of a signed hub. The root key comes from the operator, out of band: a key the hub advertises proves nothing.
36
- export const add = (name, url, { root, genesis }) => {
37
- if (!ID_RE.test(name) || !url)
38
- fail('usage: ringi hub add <name> <git url> --root <public key|file> [--genesis <sha>]');
39
- hubPorts().state.ensure();
40
- const t = transportOf(name, hubPorts().store);
41
- if (!t.attach)
42
- return fail(`a ${hubPorts().store} store cannot replicate a published hub`);
43
- t.attach(name, url, { root, genesis });
44
- return openHub(name);
26
+ const H = openHub(name);
27
+ return { H, rootKey: custody, remote: shown(H.d.contract?.remote) };
45
28
  };
46
29
  const SETTABLE = ['version', 'capabilities', 'protocol', 'profile'];
47
30
  const json = (x) => JSON.stringify(x, null, 2) + '\n';
@@ -65,11 +48,11 @@ const extraFiles = (x) => {
65
48
  return files;
66
49
  };
67
50
  // The default profile must be built in, or in the hub after this amendment.
68
- const checkDefault = (H, contract, files) => {
51
+ const checkDefault = (source, contract, files) => {
69
52
  const p = contract.profile;
70
53
  if (typeof p !== 'string' || p === DEFAULT_PROFILE || files[`profiles/${p}.json`])
71
54
  return;
72
- if (H.store.contractFile(H.main, `profiles/${p}.json`) === null)
55
+ if (source.store.contractFile(source.main, `profiles/${p}.json`) === null)
73
56
  fail(`the default profile ${p} is not in the hub; attach it: --attach <file>:profiles/${p}.json`);
74
57
  };
75
58
  // Profiles other than the built-in A2A rules need clients that read them: the contract says so.
@@ -80,15 +63,15 @@ const needsProfiles = (contract, files, set) => {
80
63
  if (!caps.includes(PROFILES_CAPABILITY))
81
64
  contract.capabilities = [...caps, PROFILES_CAPABILITY];
82
65
  };
83
- // A new version of the shared contract on `main`. Rooms keep their base until moved (rooms.rebase).
66
+ // A new version of the shared contract on `main`. Rooms made from then on follow it; existing ones keep theirs.
84
67
  export const amend = (H, x) => {
85
- const name = 'contract.json';
86
- const contract = strictJson(H.store.contractFile(H.main, name), name);
68
+ const name = 'contract.json', source = openContract(H);
69
+ const contract = strictJson(source.store.contractFile(source.main, name), name);
87
70
  setKeys(contract, x.set ?? []);
88
71
  const files = extraFiles(x);
89
- checkDefault(H, contract, files);
72
+ checkDefault(source, contract, files);
90
73
  needsProfiles(contract, files, x.set ?? []);
91
- H.store.amendContract(OPERATOR, compose(x.text, [['To', '*'], ['Kind', 'hub']]), { [name]: json(contract), ...files });
74
+ amendContract(H, OPERATOR, compose(x.text, [['To', '*'], ['Kind', 'hub']]), { [name]: json(contract), ...files });
92
75
  return { version: String(contract.version) };
93
76
  };
94
77
  //# sourceMappingURL=hubs.js.map
@@ -0,0 +1,26 @@
1
+ // invite: the one line an agent's machine joins a room by, like a VPN key: the room's address (a URI whose scheme names
2
+ // its store: git+ssh://…), which history it must be and the root that signs it. Nothing secret: an address without
3
+ // credentials, a public key.
4
+ import { fail, strictJson, withoutCredentials } from '../../../lib/index.js';
5
+ const PREFIX = 'ringi:';
6
+ const OID = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/;
7
+ // The invitation to a room published at its endpoint's remote; a room that lives only here has none.
8
+ export const invite = (R) => {
9
+ if (!R.hub.signed || !R.endpoint.store.remote || !R.hub.d.root)
10
+ return null;
11
+ const x = { schema: 'ringi-invite/1', room: R.name, address: withoutCredentials(R.endpoint.store.remote), genesis: R.genesis,
12
+ hubGenesis: R.hub.d.genesis, trust: 'signed', root: R.hub.d.root };
13
+ return PREFIX + Buffer.from(JSON.stringify(x)).toString('base64url');
14
+ };
15
+ // An invitation carries no credentials: an address with any it refuses rather than keeps.
16
+ const valid = (x) => [x.schema === 'ringi-invite/1', x.trust === 'signed',
17
+ ...[x.room, x.address, x.root].map(v => typeof v === 'string'), ...[x.genesis, x.hubGenesis].map(v => OID.test(v ?? '')),
18
+ withoutCredentials(String(x.address)) === x.address].every(Boolean);
19
+ // An invitation, if `s` is one; anything else is a room's name.
20
+ export const readInvite = (s) => {
21
+ if (!s.startsWith(PREFIX))
22
+ return null;
23
+ const x = strictJson(Buffer.from(s.slice(PREFIX.length), 'base64url').toString('utf8'), 'the invitation');
24
+ return typeof x === 'object' && x !== null && valid(x) ? x : fail('this invitation is not one this ringi reads (ringi-invite/1); ask the operator for a new one');
25
+ };
26
+ //# sourceMappingURL=invite.js.map