agentic-comments 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ FileStore,
4
+ lockTimeoutFrom,
5
+ operatorActor
6
+ } from "./chunk-C43HLFQE.js";
7
+ import {
8
+ VERSION
9
+ } from "./chunk-RWMBGIAS.js";
10
+ import {
11
+ CommentsService,
12
+ TEXT_ROOT
13
+ } from "./chunk-ZCM2UDLN.js";
14
+ import {
15
+ CommentsError
16
+ } from "./chunk-C3P5TAC3.js";
17
+
18
+ // src/cli.ts
19
+ import { readFileSync, realpathSync } from "fs";
20
+ import { pathToFileURL } from "url";
21
+ var UsageError = class extends Error {
22
+ };
23
+ var FLAGS = {
24
+ "--file": "value",
25
+ "--as": "value",
26
+ "--body": "value",
27
+ "--mention": "many",
28
+ "--text-quote": "value",
29
+ "--occurrence": "value",
30
+ "--cell": "value",
31
+ "--slide": "value",
32
+ "--canvas": "value",
33
+ "--replace": "value",
34
+ "--value": "value",
35
+ "--summary": "value",
36
+ "--status": "value",
37
+ "--anchor-state": "value",
38
+ "--mentioning": "value",
39
+ "--text": "value",
40
+ "--text-file": "value"
41
+ };
42
+ var SHARED = ["--file", "--as"];
43
+ var ANCHOR = ["--text-quote", "--occurrence", "--cell", "--slide", "--canvas"];
44
+ var ALLOWED = {
45
+ init: ["--file", "--text", "--text-file"],
46
+ list: [...SHARED, "--status", "--anchor-state", "--mentioning"],
47
+ show: SHARED,
48
+ comment: [...SHARED, ...ANCHOR, "--body", "--mention"],
49
+ reply: [...SHARED, "--body", "--mention"],
50
+ resolve: SHARED,
51
+ reopen: SHARED,
52
+ relocate: SHARED,
53
+ suggestions: [...SHARED, "--status", "--anchor-state"],
54
+ suggest: [...SHARED, ...ANCHOR, "--replace", "--value", "--summary"],
55
+ accept: SHARED,
56
+ reject: SHARED,
57
+ mcp: SHARED
58
+ };
59
+ var TAKES_ID = ["show", "reply", "resolve", "reopen", "relocate", "accept", "reject"];
60
+ var COMMANDS = ["init", "list", "show", "comment", "reply", "resolve", "reopen", "relocate", "suggestions", "suggest", "accept", "reject", "mcp"];
61
+ function parseArgs(argv) {
62
+ const [command, ...rest] = argv;
63
+ if (command === void 0 || command === "--help" || command === "-h") return { command: "help", flags: {}, many: {} };
64
+ if (command === "--version" || command === "-V") return { command: "version", flags: {}, many: {} };
65
+ if (!COMMANDS.includes(command)) throw new UsageError(`unknown command "${command}"`);
66
+ const parsed = { command, flags: {}, many: {} };
67
+ for (let i = 0; i < rest.length; i++) {
68
+ const arg = rest[i];
69
+ if (!arg.startsWith("--")) {
70
+ if (parsed.id !== void 0) throw new UsageError(`unexpected argument "${arg}"`);
71
+ parsed.id = arg;
72
+ continue;
73
+ }
74
+ const kind = FLAGS[arg];
75
+ if (!kind) throw new UsageError(`unknown flag "${arg}"`);
76
+ const value = rest[++i];
77
+ if (value === void 0) throw new UsageError(`${arg} needs a value`);
78
+ if (kind === "many") (parsed.many[arg] ??= []).push(value);
79
+ else if (arg in parsed.flags) throw new UsageError(`${arg} was given twice`);
80
+ else parsed.flags[arg] = value;
81
+ }
82
+ const used = [...Object.keys(parsed.flags), ...Object.keys(parsed.many)];
83
+ const stray = used.find((f) => !ALLOWED[command].includes(f));
84
+ if (stray) throw new UsageError(`${stray} does not apply to ${command}`);
85
+ if (parsed.id !== void 0 && !TAKES_ID.includes(command)) throw new UsageError(`${command} takes no id`);
86
+ if (parsed.flags["--text"] !== void 0 && parsed.flags["--text-file"] !== void 0) throw new UsageError("give only one of --text, --text-file");
87
+ const occurrence = parsed.flags["--occurrence"];
88
+ if (occurrence !== void 0 && !/^[1-9]\d{0,8}$/.test(occurrence)) throw new UsageError("--occurrence must be a whole number, 1 or more");
89
+ return parsed;
90
+ }
91
+ function anchorOf(p) {
92
+ const given = ["--text-quote", "--cell", "--slide", "--canvas"].filter((f2) => p.flags[f2] !== void 0);
93
+ if (given.length !== 1) throw new UsageError("give exactly one of --text-quote, --cell, --slide, --canvas");
94
+ const f = given[0];
95
+ if (f === "--text-quote") {
96
+ const occurrence = p.flags["--occurrence"];
97
+ return { type: "text", quote: p.flags[f], ...occurrence === void 0 ? {} : { occurrence: Number(occurrence) } };
98
+ }
99
+ return f === "--cell" ? { type: "cell", key: p.flags[f] } : { type: f === "--slide" ? "slide" : "canvas", elementId: p.flags[f] };
100
+ }
101
+ function changeOf(p) {
102
+ const { "--replace": replace, "--value": value } = p.flags;
103
+ if (replace === void 0 === (value === void 0)) throw new UsageError("give exactly one of --replace, --value");
104
+ if (replace !== void 0) return { replace };
105
+ try {
106
+ return { value: JSON.parse(value) };
107
+ } catch {
108
+ throw new UsageError("--value must be JSON");
109
+ }
110
+ }
111
+ function need(p) {
112
+ if (p.id === void 0) throw new UsageError(`${p.command} needs an id`);
113
+ return p.id;
114
+ }
115
+ var HELP = `agentic-comments ${VERSION}
116
+ Comment threads and suggestions anchored to any content, from people and agents.
117
+
118
+ Usage: agentic-comments <command> [flags]
119
+
120
+ Setup:
121
+ --file <path> The store file. Or AGENTIC_COMMENTS_FILE. Default ./comments.ydoc.
122
+ --as <kind:name> Who acts. Or AGENTIC_COMMENTS_AS. Kinds: human, agent_of_human, agent_of_agent, agent, user_token.
123
+ Scopes come only from AGENTIC_COMMENTS_SCOPES, a comma list such as comments:accept.
124
+
125
+ Commands:
126
+ init [--text <t> | --text-file <p>] Create the store.
127
+ list [--status s] [--anchor-state s] [--mentioning h] List threads.
128
+ show <id> Show one thread.
129
+ comment <anchor> --body <t> [--mention h] Start a thread.
130
+ reply <id> --body <t> [--mention h] Reply.
131
+ resolve <id> | reopen <id> | relocate <id>
132
+ suggestions [--status s] [--anchor-state s] List suggestions.
133
+ suggest <anchor> (--replace <t> | --value <json>) [--summary <t>]
134
+ accept <id> | reject <id>
135
+ mcp Start the MCP server on stdio.
136
+
137
+ An anchor is one of: --text-quote <q> [--occurrence n], --cell <key>, --slide <id>, --canvas <id>.
138
+ Every result is JSON. An error is JSON on stderr, and the exit code is 1. A usage error exits 2.`;
139
+ async function runCli(argv, io) {
140
+ let p;
141
+ try {
142
+ p = parseArgs(argv);
143
+ } catch (error) {
144
+ io.err(JSON.stringify({ error: { code: "USAGE", message: error.message } }));
145
+ return 2;
146
+ }
147
+ if (p.command === "help") return io.out(HELP), 0;
148
+ if (p.command === "version") return io.out(VERSION), 0;
149
+ try {
150
+ const store = new FileStore(p.flags["--file"] ?? io.env.AGENTIC_COMMENTS_FILE ?? "./comments.ydoc", { lockTimeoutMs: lockTimeoutFrom(io.env) });
151
+ if (p.command === "init") {
152
+ const seed = p.flags["--text-file"] !== void 0 ? readFileSync(p.flags["--text-file"], "utf8") : p.flags["--text"] ?? "";
153
+ store.create((ydoc) => ydoc.getText(TEXT_ROOT).insert(0, seed));
154
+ io.out(JSON.stringify({ created: store.path }));
155
+ return 0;
156
+ }
157
+ if (p.command === "mcp") {
158
+ const { startMcp } = await import("./mcp-BOJM5NE6.js");
159
+ await startMcp(store, operatorActor(p.flags["--as"] ?? io.env.AGENTIC_COMMENTS_AS, io.env.AGENTIC_COMMENTS_SCOPES));
160
+ return 0;
161
+ }
162
+ const service = new CommentsService(store, operatorActor(p.flags["--as"] ?? io.env.AGENTIC_COMMENTS_AS, io.env.AGENTIC_COMMENTS_SCOPES));
163
+ const mentions = p.many["--mention"];
164
+ const filter = { status: p.flags["--status"], anchorState: p.flags["--anchor-state"], mentioning: p.flags["--mentioning"] };
165
+ const clean = Object.fromEntries(Object.entries(filter).filter(([, v]) => v !== void 0));
166
+ const result = p.command === "list" ? service.threads(clean) : p.command === "show" ? service.thread(need(p)) : p.command === "comment" ? service.comment({ anchor: anchorOf(p), body: p.flags["--body"], ...mentions ? { mentions } : {} }) : p.command === "reply" ? service.reply(need(p), { body: p.flags["--body"], ...mentions ? { mentions } : {} }) : p.command === "resolve" ? service.resolve(need(p)) : p.command === "reopen" ? service.reopen(need(p)) : p.command === "relocate" ? service.relocate(need(p)) : p.command === "suggestions" ? service.suggestions(Object.fromEntries(Object.entries(clean).filter(([k]) => k !== "mentioning"))) : p.command === "suggest" ? service.suggest({ anchor: anchorOf(p), change: changeOf(p), ...p.flags["--summary"] === void 0 ? {} : { summary: p.flags["--summary"] } }) : p.command === "accept" ? service.accept(need(p)) : service.reject(need(p));
167
+ io.out(JSON.stringify(result));
168
+ return 0;
169
+ } catch (error) {
170
+ if (error instanceof UsageError) {
171
+ io.err(JSON.stringify({ error: { code: "USAGE", message: error.message } }));
172
+ return 2;
173
+ }
174
+ if (error instanceof CommentsError) {
175
+ io.err(JSON.stringify({ error: { code: error.code, message: error.message } }));
176
+ return error.code === "USAGE" ? 2 : 1;
177
+ }
178
+ throw error;
179
+ }
180
+ }
181
+ function isMainModule() {
182
+ const invoked = process.argv[1];
183
+ if (typeof invoked !== "string") return false;
184
+ try {
185
+ return import.meta.url === pathToFileURL(realpathSync(invoked)).href;
186
+ } catch {
187
+ return false;
188
+ }
189
+ }
190
+ if (isMainModule()) {
191
+ process.on("SIGXFSZ", () => void 0);
192
+ runCli(process.argv.slice(2), { out: (l) => console.log(l), err: (l) => console.error(l), env: process.env }).then(
193
+ (code) => {
194
+ if (code !== 0) process.exit(code);
195
+ },
196
+ (error) => {
197
+ console.error(JSON.stringify({ error: { code: "INTERNAL", message: error instanceof Error ? error.message : String(error) } }));
198
+ process.exit(1);
199
+ }
200
+ );
201
+ }
202
+ export {
203
+ parseArgs,
204
+ runCli
205
+ };
package/dist/index.d.ts CHANGED
@@ -1,332 +1,105 @@
1
- import * as Y from 'yjs';
1
+ import { A as Author, C as Comments, S as Suggestions } from './suggestions-U8tXRLZ5.js';
2
+ export { a as ACCEPT_SCOPE, b as Actor, c as Anchor, d as AnchorKind, e as AnchorState, f as ApplyContext, g as CanvasAnchor, h as CellAnchor, i as CommentsOptions, M as Message, N as NewReply, j as NewSuggestion, k as NewThread, P as PRINCIPAL_KINDS, l as PrincipalKind, R as RelativePositionJson, m as RevertContext, n as SlideAnchor, o as StampInput, p as Suggestion, q as SuggestionFilter, r as SuggestionHost, T as TextAnchor, s as Thread, t as ThreadFilter, u as authorLabel, v as stamp, w as textAnchor, x as textOf } from './suggestions-U8tXRLZ5.js';
3
+ import 'yjs';
2
4
 
3
5
  declare const VERSION: string;
4
6
 
5
- /** A Yjs relative position as JSON. It names an item, not an index, so it follows its text. */
6
- interface RelativePositionJson {
7
- type?: {
8
- client: number;
9
- clock: number;
10
- };
11
- tname?: string;
12
- item?: {
13
- client: number;
14
- clock: number;
15
- };
16
- assoc?: number;
17
- }
18
- interface TextAnchor {
19
- type: "text";
20
- /** The text under the range when the comment was made. `Comments.add` checks it against the document. */
21
- text: string;
22
- start: RelativePositionJson;
23
- end: RelativePositionJson;
24
- }
25
- interface CellAnchor {
26
- type: "cell";
27
- /** A cell key the host app defines, such as `Sheet1!B4` or a stable cell id. */
28
- key: string;
29
- }
30
- interface SlideAnchor {
31
- type: "slide";
32
- elementId: string;
33
- }
34
- interface CanvasAnchor {
35
- type: "canvas";
36
- elementId: string;
37
- }
38
- /**
39
- * Where a thread points. A custom kind (see `AnchorKind`) returns its own shape, so narrow on `type`
40
- * before you read its fields.
41
- */
42
- type Anchor = TextAnchor | CellAnchor | SlideAnchor | CanvasAnchor;
43
- /** Teach `Comments` a new anchor type. `parse` returns a clean copy or throws. */
44
- interface AnchorKind {
45
- type: string;
46
- parse(raw: Record<string, unknown>): {
47
- type: string;
48
- };
49
- }
50
- /**
51
- * Whether the content under an anchor is still there.
52
- * `attached`: yes, as it was. `edited`: the range is there, with other text. `orphaned`: the content is
53
- * gone. `unknown`: nothing can tell, as for a cell with no host answer.
54
- */
55
- type AnchorState = {
56
- state: "attached";
57
- } | {
58
- state: "edited";
59
- currentText: string;
60
- } | {
61
- state: "orphaned";
62
- } | {
63
- state: "unknown";
64
- };
65
- /** The text of a Yjs text type. An embed counts as one character, so indexes match Yjs positions. */
66
- declare function textOf(type: Y.Text | Y.XmlText): string;
67
- /** Pin a range of a Yjs text. Later edits by any replica do not move the anchor off its text. */
68
- declare function textAnchor(type: Y.Text | Y.XmlText, from: number, to: number): TextAnchor;
69
-
70
- declare const PRINCIPAL_KINDS: readonly ["human", "agent_of_human", "agent_of_agent", "agent", "user_token"];
71
- type PrincipalKind = (typeof PRINCIPAL_KINDS)[number];
72
- /** What a thread keeps about who wrote a message. Scopes never reach storage. */
73
- interface Author {
74
- kind: PrincipalKind;
75
- name: string;
76
- }
77
- interface StampInput {
78
- kind: PrincipalKind;
79
- name: string;
80
- /** Rights the host granted this principal, such as `comments:accept`. Used to decide, never stored. */
81
- scopes?: string[];
82
- }
83
- /**
84
- * The only author a write accepts. The host makes one from the principal it authenticated, and an
85
- * operation never reads an author from its input. An object that `stamp()` did not make is refused,
86
- * and a stamped one cannot change.
87
- */
88
- declare class Actor implements Author {
89
- readonly kind: PrincipalKind;
90
- readonly name: string;
91
- readonly scopes: readonly string[];
92
- private constructor();
93
- static of(input: StampInput): Actor;
94
- static is(value: unknown): value is Actor;
95
- }
96
- /** Make the author for one authenticated principal. Throws when the kind or name is missing or wrong. */
97
- declare const stamp: (input: StampInput) => Actor;
98
- /** The label a UI shows. Agent work stays marked as machine-made. */
99
- declare function authorLabel(author: Author): "person" | "person via agent" | "agent";
100
-
101
- interface Message {
102
- id: string;
103
- at: string;
104
- author: Author;
105
- body: string;
106
- /** Handles named in the body or listed on the input, without the `@`. */
107
- mentions: string[];
108
- }
109
- interface Thread {
110
- id: string;
111
- anchor: Anchor;
112
- status: "open" | "resolved";
113
- /**
114
- * The status change that won, and when. For a new thread, who opened it. Replicas that change the
115
- * status at once agree on one change. Yjs picks it by client id, not by time.
116
- */
117
- statusBy: Author;
118
- statusAt: string;
119
- messages: Message[];
120
- /** Whether the anchored content is still there. A thread whose content is gone stays readable. */
121
- anchorState: AnchorState;
122
- /** How many stored entries this read did not show: invalid ones, repeated ids, and valid ones past the read limit. `problems()` names them. */
123
- skipped: number;
124
- }
125
- interface CommentsOptions {
126
- now?: () => Date;
127
- /** More anchor types. A type that is already built in is refused. */
128
- anchors?: AnchorKind[];
129
- /**
130
- * Tell whether the content a cell, slide, canvas, or custom anchor points at still exists. Return `true`
131
- * or `false`. Any other answer, or a throw, means unknown. Text anchors do not need it.
132
- */
133
- exists?: (anchor: Anchor) => boolean | undefined;
134
- }
135
- interface NewThread {
136
- anchor: Anchor;
137
- body: string;
138
- mentions?: string[];
139
- }
140
- interface NewReply {
141
- body: string;
142
- mentions?: string[];
143
- }
144
- interface ThreadFilter {
145
- status?: Thread["status"];
146
- anchorType?: string;
147
- /** A handle, with or without the `@`. */
148
- mentioning?: string;
149
- anchorState?: AnchorState["state"];
150
- }
151
- /** Comment threads on a Yjs document. Replies and status merge across replicas. */
152
- declare class Comments {
153
- readonly ydoc: Y.Doc;
154
- private readonly kinds;
155
- private readonly now;
156
- private readonly exists?;
157
- /** Every replica of a document must register the same custom anchor kinds. Others read those threads as malformed. */
158
- constructor(ydoc: Y.Doc, options?: CommentsOptions);
159
- private get map();
160
- /** The clock, as an ISO time. Used by `Suggestions`. */
161
- time(): string;
162
- /** Whether the content under an anchor is still there. This never throws. */
163
- anchorState(anchor: Anchor): AnchorState;
164
- /** Read an anchor from stored data. This does not check the document. */
165
- parseAnchor(raw: unknown): Anchor;
166
- /** Parse an anchor from input and check it against this document. Throws `INVALID_ANCHOR` when it fails. */
167
- checkAnchor(raw: unknown): Anchor;
168
- /** A text anchor must name text in this document, in order, and match its quote. */
169
- private checkInDocument;
170
- /** Build a message, and refuse it unless a read would show it back. Nothing is written yet. */
171
- private message;
172
- private readMessage;
173
- /** A thread with a bad anchor or status is malformed. A bad message is skipped and named in `issues`. */
174
- private read;
175
- private fields;
176
- /** Start a thread with its first message. The anchor and body are checked before anything is written. */
177
- add(input: NewThread, actor: Actor): Thread;
178
- /**
179
- * Add a message. The resolved check runs on this replica only. A reply and a resolve made at once
180
- * on two replicas both stay, and the merged thread is resolved with the reply in it.
181
- */
182
- reply(threadId: string, input: NewReply, actor: Actor): Message;
183
- private setStatus;
184
- /**
185
- * Move a text anchor whose range is gone or edited to its quote, when the quote occurs exactly once in
186
- * the same text. There is no fuzzy match. Throws `RELOCATE_FAILED` when it cannot say where. An attached
187
- * thread is returned as it is.
188
- */
189
- relocate(threadId: string, actor: Actor): Thread;
190
- resolve(threadId: string, actor: Actor): Thread;
191
- reopen(threadId: string, actor: Actor): Thread;
192
- get(threadId: string): Thread;
193
- /** Threads in the order they were started. A malformed thread is skipped; `problems()` names it. */
194
- list(filter?: ThreadFilter): Thread[];
195
- /** Threads or messages that could not be read, for example from a replica that wrote bad data. */
196
- problems(): {
197
- id: string;
198
- reason: string;
199
- }[];
200
- private readAll;
201
- }
202
-
203
- type CommentsErrorCode = "INVALID_AUTHOR" | "INVALID_INPUT" | "INVALID_ANCHOR" | "NO_THREAD" | "THREAD_RESOLVED" | "CORRUPT" | "NO_SUGGESTION" | "FORBIDDEN" | "ALREADY_DECIDED" | "APPLY_FAILED" | "RELOCATE_FAILED";
7
+ type CommentsErrorCode = "INVALID_AUTHOR" | "INVALID_INPUT" | "INVALID_ANCHOR" | "NO_THREAD" | "THREAD_RESOLVED" | "CORRUPT" | "NO_SUGGESTION" | "FORBIDDEN" | "ALREADY_DECIDED" | "APPLY_FAILED" | "RELOCATE_FAILED" | "STORE_MISSING" | "STORE_EXISTS" | "STORE_CORRUPT" | "STORE_LOCKED" | "STORE_FULL" | "USAGE";
204
8
  declare class CommentsError extends Error {
205
9
  readonly code: CommentsErrorCode;
206
10
  constructor(code: CommentsErrorCode, message: string);
207
11
  }
208
12
 
209
- /** The scope that lets an agent accept or reject a suggestion that another principal wrote. */
210
- declare const ACCEPT_SCOPE = "comments:accept";
211
- interface Suggestion {
212
- id: string;
213
- anchor: Anchor;
214
- /** Whether the anchored content is still there. Accept refuses an orphaned or edited one. */
215
- anchorState: AnchorState;
216
- /** What the host app proposes. This package never reads it. The host's `apply` does. */
217
- change: unknown;
218
- summary?: string;
219
- author: Author;
220
- createdAt: string;
221
- status: "pending" | "accepted" | "rejected";
222
- decidedBy?: Author;
223
- decidedAt?: string;
224
- }
225
- interface ApplyContext {
226
- suggestionId: string;
227
- anchor: Anchor;
228
- /** Who wrote the suggestion. */
229
- author: Author;
230
- /** Who accepts it. Label the edit with this author. */
231
- by: Author;
232
- ydoc: Y.Doc;
233
- }
234
- interface RevertContext {
235
- suggestionId: string;
236
- anchor: Anchor;
237
- /** Who wrote the suggestion. */
238
- author: Author;
239
- /** Who made the application being undone. */
240
- by: Author;
241
- ydoc: Y.Doc;
242
- }
13
+ type CommentEventType = "thread.created" | "message.added" | "mention" | "thread.resolved" | "thread.reopened" | "suggestion.created" | "suggestion.accepted" | "suggestion.rejected";
243
14
  /**
244
- * The host app's side. Each hook is synchronous, so the check and the edit stay in one step.
245
- * Make every edit in `ydoc`, so it merges with the rest.
15
+ * One change, in terms a notifier can route. It holds ids and never the text of a comment, a suggestion, or
16
+ * a change, so a log or a queue that stores it holds no content. Fetch the content with the ids.
246
17
  */
247
- interface SuggestionHost {
248
- /** Check a change when it is suggested. Return the clean change, or throw. */
249
- parse?(change: unknown): unknown;
18
+ interface CommentEvent {
250
19
  /**
251
- * Make the edit. Throw before you change anything when the edit cannot apply, for example when the
252
- * anchored content changed. Yjs cannot undo a partial edit. If `apply` edits and then fails, the
253
- * decision is recorded as accepted with no receipt, and `problems()` reports it. Return a receipt,
254
- * JSON data that lets `revert` undo this edit.
20
+ * The same change has the same id on every replica, so a notifier can drop a repeat. A replica also drops
21
+ * a repeat it has seen, within its last `seenLimit` ids. Two accepts at once give one id with the actor of
22
+ * whichever replica you hear first, and the winner can differ. A host clock coarser than the rate of status
23
+ * changes can give two status events one id.
255
24
  */
256
- apply(change: unknown, context: ApplyContext): unknown;
25
+ id: string;
26
+ type: CommentEventType;
27
+ /** When the change was made, as the data records it. */
28
+ at: string;
29
+ /** `local` when this replica made the change. `remote` when it came in from another replica. */
30
+ source: "local" | "remote";
31
+ /** Who made the change. `machineMade` is true for every kind but a signed-in person. */
32
+ actor: Author & {
33
+ machineMade: boolean;
34
+ };
35
+ /** What kind of content the thread or suggestion points at: text, cell, slide, canvas, or a custom kind. */
36
+ anchorType: string;
37
+ threadId?: string;
38
+ messageId?: string;
39
+ suggestionId?: string;
40
+ /** For a `mention` event: the handle that was named. */
41
+ mention?: string;
42
+ }
43
+ type CommentListener = (event: CommentEvent) => void | Promise<void>;
44
+ interface NotificationsOptions {
257
45
  /**
258
- * Two replicas can accept one suggestion before they meet. Each applies the edit. After a merge one
259
- * application stays and `revert` undoes each other. Every replica runs it at the same time, and the
260
- * results must merge to one outcome, as a Yjs delete of the same range does. Throw before you change
261
- * anything. A receipt comes from stored data. A peer that can write comment data can choose it, so
262
- * check it before you use it. The anchor and the author are stored data too, so a check against them
263
- * helps against a mistake and not against a peer that can write comment data.
46
+ * Called when a listener throws or rejects (`source` is `listener`), or when a change in the data cannot
47
+ * be read (`source` is `data`). Without it, errors go to `errors()`.
264
48
  */
265
- revert?(receipt: unknown, context: RevertContext): void;
266
- /** Say that applying the same change twice is harmless. Then `revert` is not needed, and a duplicate is not undone. */
267
- idempotent?: boolean;
268
- }
269
- interface NewSuggestion {
270
- anchor: Anchor;
271
- change: unknown;
272
- summary?: string;
273
- }
274
- interface SuggestionFilter {
275
- status?: Suggestion["status"];
276
- anchorState?: AnchorState["state"];
49
+ onError?: (error: unknown, source: "listener" | "data") => void;
50
+ /** Drop every event until `unmute()`. Create the stream muted, and unmute after the first load, so old history is not sent. */
51
+ muted?: boolean;
52
+ /** How many event ids to remember so a repeat is dropped. A whole number, 1 or more. Default 10,000. */
53
+ seenLimit?: number;
277
54
  }
278
55
  /**
279
- * Suggested changes to content, from people and agents. Nothing changes until a person, or an agent
280
- * with the accept scope that did not write it, accepts. Accept and reject are safe across replicas.
56
+ * An event stream for a notifier. It watches the thread and suggestion data, so it sees changes from other
57
+ * replicas too, and marks each as local or remote. It replays nothing it saw before it was created, but a first
58
+ * load into an empty document looks like new changes: create it muted and call `unmute()` after that load.
59
+ * Each id is delivered at most once per replica within the `seenLimit` window, and a listener that throws is
60
+ * not retried. A listener cannot break a write. A message past the read limit
61
+ * of a thread makes no event.
281
62
  */
282
- declare class Suggestions {
63
+ declare class Notifications {
283
64
  private readonly comments;
284
- private readonly host;
285
- private readonly onTransaction;
286
- private failures;
287
- constructor(comments: Comments, host: SuggestionHost);
288
- /** Stop listening to the document. */
65
+ private readonly suggestions?;
66
+ private readonly options;
67
+ private readonly listeners;
68
+ private readonly seen;
69
+ private readonly failures;
70
+ private readonly stops;
71
+ private readonly queue;
72
+ private flushing;
73
+ private closed;
74
+ private readonly seenLimit;
75
+ private muted;
76
+ constructor(comments: Comments, suggestions?: Suggestions | undefined, options?: NotificationsOptions);
77
+ /** Receive every event from now on. Returns a function that stops it. */
78
+ subscribe(listener: CommentListener): () => void;
79
+ /** Start sending events, for a stream created with `muted: true`. */
80
+ unmute(): void;
81
+ /** Stop watching the document and drop every listener. */
289
82
  close(): void;
290
- private get ydoc();
291
- private get map();
292
- private get applied();
293
- private get rejected();
294
- private change;
295
- /**
296
- * Records of one kind for one suggestion, in client order. A record that is not valid is not counted:
297
- * one with no author or time, a receipt that is not small JSON, or a decider who decided their own work.
298
- */
299
- private records;
300
- /** Group the entries of a decision map by suggestion id in one pass, so a read does not scan the map per suggestion. */
301
- private index;
302
- private read;
303
- private fields;
304
- /** Propose a change. Anyone can suggest. The host stamps the author. */
305
- add(input: NewSuggestion, actor: Actor): Suggestion;
306
- /** A person decides. An agent needs the accept scope, and never decides its own suggestion. */
307
- private mayDecide;
308
- /**
309
- * Apply the change as the accepting author and record the decision, in one step. Accepting an
310
- * accepted suggestion changes nothing. When `apply` throws, the suggestion stays pending.
311
- */
312
- accept(suggestionId: string, actor: Actor): Suggestion;
313
- /** Same right as accept. A person can withdraw their own suggestion. */
314
- reject(suggestionId: string, actor: Actor): Suggestion;
315
- get(suggestionId: string): Suggestion;
316
- /** In the order they were made. A malformed suggestion is skipped, and `problems()` names it. */
317
- list(filter?: SuggestionFilter): Suggestion[];
318
- /** Suggestions that could not be read, and reverts that failed. */
319
- problems(): {
320
- id: string;
321
- reason: string;
83
+ /** The most recent errors, when no `onError` was given. */
84
+ errors(): {
85
+ source: "listener" | "data";
86
+ error: unknown;
322
87
  }[];
323
- private readAll;
324
- /**
325
- * Two replicas can accept one suggestion before they meet. After a merge, the application of the
326
- * lowest client id stays, and the host reverts each other one. Every replica reaches the same result.
327
- * A revert that throws leaves its record, so the next merge tries again.
328
- */
329
- settle(): void;
88
+ private fail;
89
+ /** Data from a peer can be bad. A bad record makes no event, and never stops a write or the records after it. */
90
+ private guard;
91
+ private emit;
92
+ private flush;
93
+ private threadEvents;
94
+ private mentions;
95
+ private messageEvent;
96
+ private statusEvent;
97
+ private threadCreated;
98
+ private messagesAdded;
99
+ private statusChanged;
100
+ private created;
101
+ private decisionEvent;
102
+ private decided;
330
103
  }
331
104
 
332
- export { ACCEPT_SCOPE, Actor, type Anchor, type AnchorKind, type AnchorState, type ApplyContext, type Author, type CanvasAnchor, type CellAnchor, Comments, CommentsError, type CommentsErrorCode, type CommentsOptions, type Message, type NewReply, type NewSuggestion, type NewThread, PRINCIPAL_KINDS, type PrincipalKind, type RelativePositionJson, type RevertContext, type SlideAnchor, type StampInput, type Suggestion, type SuggestionFilter, type SuggestionHost, Suggestions, type TextAnchor, type Thread, type ThreadFilter, VERSION, authorLabel, stamp, textAnchor, textOf };
105
+ export { Author, type CommentEvent, type CommentEventType, type CommentListener, Comments, CommentsError, type CommentsErrorCode, Notifications, type NotificationsOptions, Suggestions, VERSION };