@openmarket/rooms-client 0.1.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.
Files changed (87) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +25 -0
  3. package/dist/agent/armed-mode.d.ts +187 -0
  4. package/dist/agent/armed-mode.js +306 -0
  5. package/dist/agent/clients/common.d.ts +70 -0
  6. package/dist/agent/clients/common.js +46 -0
  7. package/dist/agent/lane-draft.d.ts +15 -0
  8. package/dist/agent/lane-draft.js +43 -0
  9. package/dist/agent/lane-prompts.d.ts +102 -0
  10. package/dist/agent/lane-prompts.js +120 -0
  11. package/dist/agent/library-context.d.ts +38 -0
  12. package/dist/agent/library-context.js +57 -0
  13. package/dist/agent/library-model.d.ts +41 -0
  14. package/dist/agent/library-model.js +173 -0
  15. package/dist/agent/room-context.d.ts +151 -0
  16. package/dist/agent/room-context.js +251 -0
  17. package/dist/agent/sidebar-model.d.ts +86 -0
  18. package/dist/agent/sidebar-model.js +162 -0
  19. package/dist/agent/topic-inbox.d.ts +119 -0
  20. package/dist/agent/topic-inbox.js +266 -0
  21. package/dist/agent/topic-view-batcher.d.ts +54 -0
  22. package/dist/agent/topic-view-batcher.js +115 -0
  23. package/dist/client.d.ts +421 -0
  24. package/dist/client.js +1428 -0
  25. package/dist/doc-reconcile.d.ts +100 -0
  26. package/dist/doc-reconcile.js +110 -0
  27. package/dist/doc-uri.d.ts +29 -0
  28. package/dist/doc-uri.js +55 -0
  29. package/dist/endpoints.d.ts +9 -0
  30. package/dist/endpoints.js +13 -0
  31. package/dist/jwt.d.ts +10 -0
  32. package/dist/jwt.js +51 -0
  33. package/dist/library-publisher.d.ts +52 -0
  34. package/dist/library-publisher.js +49 -0
  35. package/dist/merge3.d.ts +50 -0
  36. package/dist/merge3.js +280 -0
  37. package/dist/names.d.ts +6 -0
  38. package/dist/names.js +35 -0
  39. package/dist/shared/emoji-data.d.ts +22 -0
  40. package/dist/shared/emoji-data.js +8834 -0
  41. package/dist/shared/mentions.d.ts +6 -0
  42. package/dist/shared/mentions.js +32 -0
  43. package/dist/shared/rooms-protocol.d.ts +1183 -0
  44. package/dist/shared/rooms-protocol.js +160 -0
  45. package/dist/social-client.d.ts +361 -0
  46. package/dist/social-client.js +686 -0
  47. package/dist/social-types.d.ts +338 -0
  48. package/dist/social-types.js +1 -0
  49. package/dist/suggestion-attribution.d.ts +10 -0
  50. package/dist/suggestion-attribution.js +56 -0
  51. package/dist/topic-uri.d.ts +26 -0
  52. package/dist/topic-uri.js +40 -0
  53. package/dist/types.d.ts +2 -0
  54. package/dist/types.js +1 -0
  55. package/dist/version.d.ts +2 -0
  56. package/dist/version.js +2 -0
  57. package/dist/ws-client.d.ts +115 -0
  58. package/dist/ws-client.js +491 -0
  59. package/package.json +180 -0
  60. package/src/agent/armed-mode.ts +368 -0
  61. package/src/agent/clients/common.ts +91 -0
  62. package/src/agent/lane-draft.ts +47 -0
  63. package/src/agent/lane-prompts.ts +267 -0
  64. package/src/agent/library-context.ts +97 -0
  65. package/src/agent/library-model.ts +210 -0
  66. package/src/agent/room-context.ts +351 -0
  67. package/src/agent/sidebar-model.ts +235 -0
  68. package/src/agent/topic-inbox.ts +297 -0
  69. package/src/agent/topic-view-batcher.ts +134 -0
  70. package/src/client.ts +2331 -0
  71. package/src/doc-reconcile.ts +160 -0
  72. package/src/doc-uri.ts +83 -0
  73. package/src/endpoints.ts +14 -0
  74. package/src/jwt.ts +59 -0
  75. package/src/library-publisher.ts +93 -0
  76. package/src/merge3.ts +326 -0
  77. package/src/names.ts +44 -0
  78. package/src/shared/emoji-data.ts +8868 -0
  79. package/src/shared/mentions.ts +32 -0
  80. package/src/shared/rooms-protocol.ts +1339 -0
  81. package/src/social-client.ts +1287 -0
  82. package/src/social-types.ts +376 -0
  83. package/src/suggestion-attribution.ts +83 -0
  84. package/src/topic-uri.ts +64 -0
  85. package/src/types.ts +83 -0
  86. package/src/version.ts +2 -0
  87. package/src/ws-client.ts +611 -0
package/src/merge3.ts ADDED
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Line-based three-way merge (diff3 semantics) for the library mirror's
3
+ * agent-mediated conflict resolution. Pure, no deps, byte-faithful: content
4
+ * is split on "\n" only and re-joined identically, so CRLF and no-trailing-
5
+ * newline round-trip exactly.
6
+ *
7
+ * The contract callers rely on:
8
+ * - `clean: true` means every collision region was one-sided or identical on
9
+ * both sides; `merged` is the full merged document.
10
+ * - `clean: false` means at least one region genuinely diverged (or an input
11
+ * bound tripped); `merged` is "" so no caller can write a half-merge, and
12
+ * `regions` carries the diverging chunks for a mediator to look at.
13
+ * - Identical changes on BOTH sides merge clean. This is load-bearing: a
14
+ * crash between the merged-file write and the manifest save re-runs the
15
+ * merge with mine already containing theirs' lines, and it must converge.
16
+ * - Touching-or-overlapping changes from the two sides conflict (git's
17
+ * conservative rule); insertions at the same anchor conflict.
18
+ */
19
+
20
+ export interface Merge3Region {
21
+ base: string[];
22
+ mine: string[];
23
+ theirs: string[];
24
+ }
25
+
26
+ /** The not-clean document as resolved line chunks with placeholders where
27
+ * the conflict regions go: everything a region-scoped mediator needs to
28
+ * splice resolved regions back without regenerating the whole document. */
29
+ export type MergeSkeletonPart =
30
+ | { kind: "lines"; lines: string[] }
31
+ | { kind: "region"; index: number };
32
+
33
+ export interface Merge3Result {
34
+ clean: boolean;
35
+ merged: string;
36
+ regions: Merge3Region[];
37
+ /** Present exactly when regions exist. */
38
+ skeleton?: MergeSkeletonPart[];
39
+ }
40
+
41
+ /** Reassemble a skeleton with one resolved text per region ("" = the region
42
+ * resolves to nothing). The counterpart of a region-scoped mediation. */
43
+ export function spliceSkeleton(skeleton: MergeSkeletonPart[], resolved: string[]): string {
44
+ const lines: string[] = [];
45
+ for (const part of skeleton) {
46
+ if (part.kind === "lines") {
47
+ lines.push(...part.lines);
48
+ } else {
49
+ const text = resolved[part.index];
50
+ if (text === undefined) throw new Error(`missing resolution for region ${part.index}`);
51
+ if (text !== "") lines.push(...text.split("\n"));
52
+ }
53
+ }
54
+ return lines.join("\n");
55
+ }
56
+
57
+ export interface Merge3Options {
58
+ /** Per-side line cap; above it merge3 bails to not-clean. */
59
+ maxLines?: number;
60
+ /** Myers edit-distance bound per diff; above it merge3 bails to not-clean. */
61
+ maxEditDistance?: number;
62
+ }
63
+
64
+ const DEFAULT_MAX_LINES = 30_000;
65
+ const DEFAULT_MAX_EDIT_DISTANCE = 2_048;
66
+
67
+ /** Base range [aStart,aEnd) was replaced by side range [bStart,bEnd). */
68
+ interface DiffHunk {
69
+ aStart: number;
70
+ aEnd: number;
71
+ bStart: number;
72
+ bEnd: number;
73
+ }
74
+
75
+ /**
76
+ * Myers O(ND) shortest-edit-script diff at line granularity, returning
77
+ * base-anchored replacement hunks. Returns null when the edit distance
78
+ * exceeds maxD (caller bails to not-clean, which degrades to the existing
79
+ * theirs-file behavior).
80
+ */
81
+ function diffLines(a: readonly string[], b: readonly string[], maxD: number): DiffHunk[] | null {
82
+ // Trim the common prefix/suffix first: cheap, and it keeps the Myers band
83
+ // proportional to the actual change, not the file.
84
+ let start = 0;
85
+ const minLen = Math.min(a.length, b.length);
86
+ while (start < minLen && a[start] === b[start]) start++;
87
+ let endA = a.length;
88
+ let endB = b.length;
89
+ while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
90
+ endA--;
91
+ endB--;
92
+ }
93
+ const n = endA - start;
94
+ const m = endB - start;
95
+ if (n === 0 && m === 0) return [];
96
+ if (n === 0) return [{ aStart: start, aEnd: start, bStart: start, bEnd: endB }];
97
+ if (m === 0) return [{ aStart: start, aEnd: endA, bStart: start, bEnd: start }];
98
+
99
+ const max = Math.min(n + m, maxD);
100
+ const offset = max;
101
+ const width = 2 * max + 1;
102
+ let v = new Int32Array(width);
103
+ const trace: Int32Array[] = [];
104
+ let foundD = -1;
105
+
106
+ outer: for (let d = 0; d <= max; d++) {
107
+ trace.push(v.slice());
108
+ const next = v.slice();
109
+ for (let k = -d; k <= d; k += 2) {
110
+ if (k < -max || k > max) continue;
111
+ let x: number;
112
+ if (k === -d || (k !== d && (v[k - 1 + offset] ?? 0) < (v[k + 1 + offset] ?? 0))) {
113
+ x = v[k + 1 + offset] ?? 0;
114
+ } else {
115
+ x = (v[k - 1 + offset] ?? 0) + 1;
116
+ }
117
+ let y = x - k;
118
+ while (x < n && y < m && a[start + x] === b[start + y]) {
119
+ x++;
120
+ y++;
121
+ }
122
+ next[k + offset] = x;
123
+ if (x >= n && y >= m) {
124
+ v = next;
125
+ foundD = d;
126
+ break outer;
127
+ }
128
+ }
129
+ v = next;
130
+ }
131
+ if (foundD < 0) return null;
132
+
133
+ // Backtrack: one del/ins per d, snakes in between. Collect edits in
134
+ // reverse, then group contiguous edits into hunks.
135
+ interface Edit {
136
+ type: "del" | "ins";
137
+ x: number;
138
+ y: number;
139
+ }
140
+ const edits: Edit[] = [];
141
+ let x = n;
142
+ let y = m;
143
+ for (let d = foundD; d > 0; d--) {
144
+ const prev = trace[d];
145
+ if (!prev) return null;
146
+ const k = x - y;
147
+ let prevK: number;
148
+ if (k === -d || (k !== d && (prev[k - 1 + offset] ?? 0) < (prev[k + 1 + offset] ?? 0))) {
149
+ prevK = k + 1;
150
+ } else {
151
+ prevK = k - 1;
152
+ }
153
+ const prevX = prev[prevK + offset] ?? 0;
154
+ const prevY = prevX - prevK;
155
+ if (prevK === k + 1) {
156
+ // Vertical move: b[prevY] was inserted at base position prevX.
157
+ edits.push({ type: "ins", x: prevX, y: prevY });
158
+ } else {
159
+ // Horizontal move: a[prevX] was deleted.
160
+ edits.push({ type: "del", x: prevX, y: prevY });
161
+ }
162
+ x = prevX;
163
+ y = prevY;
164
+ }
165
+ edits.reverse();
166
+
167
+ const hunks: DiffHunk[] = [];
168
+ let current: DiffHunk | null = null;
169
+ let pathX = -1;
170
+ let pathY = -1;
171
+ for (const edit of edits) {
172
+ const contiguous = current !== null && edit.x === pathX && edit.y === pathY;
173
+ if (!contiguous) {
174
+ if (current) hunks.push(current);
175
+ current = { aStart: edit.x, aEnd: edit.x, bStart: edit.y, bEnd: edit.y };
176
+ }
177
+ if (!current) continue;
178
+ if (edit.type === "del") {
179
+ current.aEnd = edit.x + 1;
180
+ pathX = edit.x + 1;
181
+ pathY = edit.y;
182
+ } else {
183
+ current.bEnd = edit.y + 1;
184
+ pathX = edit.x;
185
+ pathY = edit.y + 1;
186
+ }
187
+ }
188
+ if (current) hunks.push(current);
189
+
190
+ // Re-anchor to full coordinates (the prefix trim shifted everything).
191
+ return hunks.map((hunk) => ({
192
+ aStart: hunk.aStart + start,
193
+ aEnd: hunk.aEnd + start,
194
+ bStart: hunk.bStart + start,
195
+ bEnd: hunk.bEnd + start,
196
+ }));
197
+ }
198
+
199
+ /** A side's replacement lines for base range [aStart,aEnd), given the side's
200
+ * cluster hunks (linear offset mapping holds between hunks). */
201
+ function sideSlice(
202
+ sideLines: readonly string[],
203
+ baseLines: readonly string[],
204
+ hunks: readonly DiffHunk[],
205
+ aStart: number,
206
+ aEnd: number,
207
+ ): string[] {
208
+ if (hunks.length === 0) return baseLines.slice(aStart, aEnd);
209
+ const first = hunks[0] as DiffHunk;
210
+ const last = hunks[hunks.length - 1] as DiffHunk;
211
+ const sStart = first.bStart - (first.aStart - aStart);
212
+ const sEnd = last.bEnd + (aEnd - last.aEnd);
213
+ return sideLines.slice(sStart, sEnd);
214
+ }
215
+
216
+ function sameLines(a: readonly string[], b: readonly string[]): boolean {
217
+ if (a.length !== b.length) return false;
218
+ for (let i = 0; i < a.length; i++) {
219
+ if (a[i] !== b[i]) return false;
220
+ }
221
+ return true;
222
+ }
223
+
224
+ function notClean(base: string[], mine: string[], theirs: string[]): Merge3Result {
225
+ return {
226
+ clean: false,
227
+ merged: "",
228
+ regions: [{ base, mine, theirs }],
229
+ skeleton: [{ kind: "region", index: 0 }],
230
+ };
231
+ }
232
+
233
+ export function merge3(
234
+ base: string,
235
+ mine: string,
236
+ theirs: string,
237
+ options: Merge3Options = {},
238
+ ): Merge3Result {
239
+ if (mine === theirs) return { clean: true, merged: mine, regions: [] };
240
+ if (base === mine) return { clean: true, merged: theirs, regions: [] };
241
+ if (base === theirs) return { clean: true, merged: mine, regions: [] };
242
+
243
+ const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
244
+ const maxD = options.maxEditDistance ?? DEFAULT_MAX_EDIT_DISTANCE;
245
+ const baseLines = base.split("\n");
246
+ const mineLines = mine.split("\n");
247
+ const theirsLines = theirs.split("\n");
248
+ if (baseLines.length > maxLines || mineLines.length > maxLines || theirsLines.length > maxLines) {
249
+ return notClean(baseLines, mineLines, theirsLines);
250
+ }
251
+
252
+ const mineDiff = diffLines(baseLines, mineLines, maxD);
253
+ const theirsDiff = diffLines(baseLines, theirsLines, maxD);
254
+ if (!mineDiff || !theirsDiff) return notClean(baseLines, mineLines, theirsLines);
255
+
256
+ const out: string[] = [];
257
+ const regions: Merge3Region[] = [];
258
+ const skeleton: MergeSkeletonPart[] = [];
259
+ const emitLines = (lines: string[]): void => {
260
+ if (lines.length === 0) return;
261
+ out.push(...lines);
262
+ const last = skeleton[skeleton.length - 1];
263
+ if (last?.kind === "lines") last.lines.push(...lines);
264
+ else skeleton.push({ kind: "lines", lines: [...lines] });
265
+ };
266
+ let cursor = 0;
267
+ let mi = 0;
268
+ let ti = 0;
269
+
270
+ while (mi < mineDiff.length || ti < theirsDiff.length) {
271
+ const nextMine = mineDiff[mi];
272
+ const nextTheirs = theirsDiff[ti];
273
+ const clusterStart = Math.min(
274
+ nextMine ? nextMine.aStart : Number.POSITIVE_INFINITY,
275
+ nextTheirs ? nextTheirs.aStart : Number.POSITIVE_INFINITY,
276
+ );
277
+ // Copy the untouched base span before the next change.
278
+ emitLines(baseLines.slice(cursor, clusterStart));
279
+ cursor = clusterStart;
280
+
281
+ // Grow the cluster while hunks from either side touch it. Touching (not
282
+ // just overlapping) is deliberate: adjacent edits conflict, like git.
283
+ let clusterEnd = clusterStart;
284
+ const mineFirst = mi;
285
+ const theirsFirst = ti;
286
+ let grew = true;
287
+ while (grew) {
288
+ grew = false;
289
+ while (mi < mineDiff.length && (mineDiff[mi] as DiffHunk).aStart <= clusterEnd) {
290
+ clusterEnd = Math.max(clusterEnd, (mineDiff[mi] as DiffHunk).aEnd);
291
+ mi++;
292
+ grew = true;
293
+ }
294
+ while (ti < theirsDiff.length && (theirsDiff[ti] as DiffHunk).aStart <= clusterEnd) {
295
+ clusterEnd = Math.max(clusterEnd, (theirsDiff[ti] as DiffHunk).aEnd);
296
+ ti++;
297
+ grew = true;
298
+ }
299
+ }
300
+
301
+ const mineHunks = mineDiff.slice(mineFirst, mi);
302
+ const theirsHunks = theirsDiff.slice(theirsFirst, ti);
303
+ const mineChunk = sideSlice(mineLines, baseLines, mineHunks, clusterStart, clusterEnd);
304
+ const theirsChunk = sideSlice(theirsLines, baseLines, theirsHunks, clusterStart, clusterEnd);
305
+
306
+ if (theirsHunks.length === 0) {
307
+ emitLines(mineChunk);
308
+ } else if (mineHunks.length === 0) {
309
+ emitLines(theirsChunk);
310
+ } else if (sameLines(mineChunk, theirsChunk)) {
311
+ emitLines(mineChunk);
312
+ } else {
313
+ skeleton.push({ kind: "region", index: regions.length });
314
+ regions.push({
315
+ base: baseLines.slice(clusterStart, clusterEnd),
316
+ mine: mineChunk,
317
+ theirs: theirsChunk,
318
+ });
319
+ }
320
+ cursor = clusterEnd;
321
+ }
322
+ emitLines(baseLines.slice(cursor));
323
+
324
+ if (regions.length > 0) return { clean: false, merged: "", regions, skeleton };
325
+ return { clean: true, merged: out.join("\n"), regions: [] };
326
+ }
package/src/names.ts ADDED
@@ -0,0 +1,44 @@
1
+ export const RESERVED_ROOM_PREFIXES = ["workspace-"];
2
+
3
+ const ROOM_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,78}[a-z0-9])?$/;
4
+ const WORKSPACE_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,126}[A-Za-z0-9])?$/;
5
+
6
+ export function normalizeRoomNameInput(raw: string): string {
7
+ return raw.trim().replace(/^#/, "").toLowerCase();
8
+ }
9
+
10
+ export function normalizeCommunityRoomName(raw: string): string {
11
+ const name = normalizeRoomNameInput(raw);
12
+ assertCommunityRoomName(name);
13
+ return name;
14
+ }
15
+
16
+ export function assertCommunityRoomName(name: string): void {
17
+ assertRoomSlug(name, "Room name");
18
+ if (RESERVED_ROOM_PREFIXES.some((prefix) => name.startsWith(prefix))) {
19
+ throw new Error("Room names cannot use the reserved workspace- prefix");
20
+ }
21
+ if (/^room-[a-f0-9]{12}$/.test(name)) {
22
+ throw new Error("Room names cannot use the reserved generated room id format");
23
+ }
24
+ }
25
+
26
+ export function normalizeWorkspaceId(raw: string): string {
27
+ const id = raw.trim();
28
+ if (!WORKSPACE_ID_PATTERN.test(id)) {
29
+ throw new Error("Workspace ID must contain only letters, numbers, and hyphens");
30
+ }
31
+ return id;
32
+ }
33
+
34
+ export function workspaceRoomName(workspaceId: string): string {
35
+ return `workspace-${normalizeWorkspaceId(workspaceId).toLowerCase()}`;
36
+ }
37
+
38
+ function assertRoomSlug(value: string, label: string): void {
39
+ if (!ROOM_NAME_PATTERN.test(value)) {
40
+ throw new Error(
41
+ `${label} must be 1-80 characters, start and end with a letter or number, and contain only lowercase letters, numbers, and hyphens`,
42
+ );
43
+ }
44
+ }