agentic-comments 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pooria Arab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,273 @@
1
- # Temporary Holding Version
1
+ # agentic-comments
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![MIT license](https://img.shields.io/badge/license-MIT-blue)](https://opensource.org/license/mit)
4
+
5
+ [Source](https://github.com/pooriaarab/agentic-comments) · [Support](https://github.com/pooriaarab/agentic-comments/issues)
6
+
7
+ Comment threads and suggestions anchored to any content, from people and agents.
8
+
9
+ > Status: see [npm](https://www.npmjs.com/package/agentic-comments) for the latest released version. The
10
+ > `main` branch can have changes that npm does not have yet.
11
+
12
+ ## Install
13
+
14
+ Requires Node.js 20 or later. `yjs` is a peer dependency. Your app and this package must use the same
15
+ copy of `yjs`, because the package checks the Yjs types of your document. Install both:
16
+
17
+ ```sh
18
+ npm install agentic-comments yjs
19
+ ```
20
+
21
+ To build from source, see Develop.
22
+
23
+ ## Quick start
24
+
25
+ ```ts
26
+ import * as Y from "yjs";
27
+ import { Comments, stamp, textAnchor } from "agentic-comments";
28
+
29
+ const doc = new Y.Doc();
30
+ doc.getText("body").insert(0, "Hello brave world");
31
+
32
+ const comments = new Comments(doc);
33
+ const ada = stamp({ kind: "human", name: "ada" });
34
+ const scribe = stamp({ kind: "agent_of_human", name: "scribe" });
35
+
36
+ const thread = comments.add({ anchor: textAnchor(doc.getText("body"), 6, 11), body: "Word choice? @scribe" }, ada);
37
+ comments.reply(thread.id, { body: "I suggest a shorter word." }, scribe);
38
+ comments.resolve(thread.id, ada);
39
+ ```
40
+
41
+ The `Comments` object lives on a Yjs document. Send the document updates with your
42
+ normal sync, and every replica sees the same threads.
43
+
44
+ ## Features
45
+
46
+ - Threads anchored to a text range, a cell key, a slide element, or a canvas element.
47
+ You can add your own anchor type.
48
+ - A text anchor uses Yjs relative positions, so it follows its text through edits.
49
+ - Messages with mentions. Resolve and reopen a thread.
50
+ - Suggestions: a change your app proposes. A person, or an agent with the accept scope, accepts it.
51
+ An agent never accepts or rejects its own. Two replicas that accept at once apply it once.
52
+ - The host stamps every author. An operation never reads an author from its input.
53
+ - Every message keeps the author kind, so agent comments stay marked as machine-made.
54
+ - Replies and status merge across replicas. Two replicas that resolve at once agree.
55
+ - A malformed thread from a bad replica is skipped. A bad message is left out, and `skipped` on the
56
+ thread counts those. `problems()` names them.
57
+
58
+ ## When the content changes
59
+
60
+ Every thread and suggestion has an `anchorState`:
61
+
62
+ | State | Meaning |
63
+ | --- | --- |
64
+ | `attached` | The content is there, as it was. |
65
+ | `edited` | A text range is there, with other text. `currentText` shows it. |
66
+ | `orphaned` | The content is gone. The thread stays readable and takes replies. |
67
+ | `unknown` | Nothing can tell. A cell, slide, canvas, or custom anchor has no host answer. |
68
+
69
+ - A text anchor follows its text through edits by any replica. Deleting the whole range orphans it.
70
+ - For other anchors, pass `exists(anchor)` to `new Comments`. Return `true` or `false`. Any other answer,
71
+ or a throw, means `unknown`.
72
+ - `list({ anchorState: "orphaned" })` finds threads to review.
73
+ - `relocate(threadId, actor)` moves a text anchor to its quote when the quote occurs exactly once in the
74
+ same text. It never guesses. It throws `RELOCATE_FAILED` for no match or many.
75
+ - Accept refuses a suggestion whose anchor is orphaned or edited. The suggestion stays pending, and a
76
+ person can reject it.
77
+ - `currentText` is document text. It can hold any character, so a UI should treat it as untrusted.
78
+
79
+ ## Suggestions
80
+
81
+ Your app owns the change. This package stores it as JSON, tracks the decision, and calls your `apply`.
82
+
83
+ ```ts
84
+ import { ACCEPT_SCOPE, Suggestions } from "agentic-comments";
85
+
86
+ const suggestions = new Suggestions(comments, {
87
+ apply(change, { anchor, by, ydoc }) {
88
+ // Edit ydoc here. Throw before you edit when the change cannot apply.
89
+ return { /* a receipt, JSON that lets revert undo this edit */ };
90
+ },
91
+ revert(receipt, { ydoc }) {
92
+ // Undo a duplicate edit. Every replica runs this at once, so use an edit that merges to one result.
93
+ // Throw before you change anything.
94
+ },
95
+ });
96
+
97
+ const s = suggestions.add({ anchor, change: { replacement: "bold" }, summary: "Word choice" }, scribe);
98
+ suggestions.accept(s.id, ada);
99
+ ```
100
+
101
+ - A signed-in person (`human`) accepts or rejects. Every other kind, including `user_token`, needs the
102
+ `comments:accept` scope, and never decides a suggestion it wrote. Names match exactly, in Unicode NFC form (a name in another form is refused), and two
103
+ principals that are not signed-in people with the same name count as one, whatever their kinds.
104
+ - Accept checks that a text anchor still matches the document, then runs the edit and writes the
105
+ record in one step. If `apply` throws before it edits, the suggestion stays pending. If it edits and
106
+ then fails, the decision is recorded without a receipt, and `problems()` reports it. A host that cannot undo a double apply must say `idempotent: true`.
107
+ - Every replica of a document must use the same host mode: the same `revert`, or the same `idempotent`
108
+ flag. A mix can leave a duplicate edit in the document.
109
+ - `problems()` entries about an edit that failed after it landed, or a duplicate with no receipt, live in
110
+ memory on the replica that saw them. Other replicas and a reload do not show them.
111
+ - A decision by an agent with the accept scope is final for everyone, including people.
112
+ - An accept on a replica that has not yet seen a reject wins, even when a person made the reject long before.
113
+ The window is any time the replicas were apart. An agent with the scope can override a person this way.
114
+ A host that wants the person to win can accept only while its replica is in sync.
115
+ - Two replicas that accept before they meet each apply the edit. After the merge, the application of
116
+ the lowest client id stays and `revert` undoes each other one. An accept and a reject at once end as
117
+ accepted.
118
+ - A receipt is stored data. A peer that can write comment data can choose one, and can rewrite the
119
+ `anchor` to match. Check the receipt in `revert` before you use it. The sync layer's write control is
120
+ the real protection. A record with no receipt (an `apply` that edited and
121
+ then failed) always stays, and the other application is undone. If two records have none, the extra
122
+ edit cannot be undone. `problems()` reports it. With `idempotent: true`, a duplicate is never undone.
123
+ - `apply` and `revert` must be synchronous, and must edit only the Yjs document. `accept` cannot run
124
+ inside another Yjs transaction, so not inside an observer or an `apply`.
125
+ - A peer that can write the document can write a decision record. Records with a bad author, time, or
126
+ receipt, or from a decider who wrote the suggestion as an agent, are not counted. A well-formed forged
127
+ record is, and it can win settle, so every replica undoes the real edit. The sync layer must decide who
128
+ may write.
129
+
130
+ ## CLI and MCP server
131
+
132
+ Both run on a local store: one file that holds a Yjs document. The text is in the `text` root, and
133
+ non-text values are in the `values` map. A file starts with a magic mark and a hash, so a damaged or zero-filled file is refused and not read as empty. A write
134
+ takes a lock file, saves with a rename, and runs again if the file changed meanwhile, so parallel processes
135
+ on one machine lose nothing. A full disk or a file size limit fails the write and leaves the old file. A lock left by a dead process, or by this process under another mark, is cleared. So is an empty lock older
136
+ than two seconds and any lock older than a minute. The store is for one
137
+ machine. It cannot check a lock held in another container or on another machine. Two containers that
138
+ share a volume can have the same process id, and each then clears the other's lock, so they can lose writes. A save that
139
+ would pass 50 MB is refused.
140
+
141
+ If the version you install has no `agentic-comments` command, build from source (see [Develop](#develop)).
142
+ In the cloned repo, run `npm run build` and use `node dist/cli.js` where these examples say
143
+ `npx agentic-comments`.
144
+
145
+ ```sh
146
+ export AGENTIC_COMMENTS_FILE=./comments.ydoc
147
+ export AGENTIC_COMMENTS_AS=human:ada # kind:name. The operator sets this.
148
+ npx agentic-comments init --text "Hello brave world"
149
+ npx agentic-comments comment --text-quote brave --body "Word choice? @scribe"
150
+ npx agentic-comments list --status open
151
+ AGENTIC_COMMENTS_AS=agent_of_human:scribe npx agentic-comments suggest --text-quote brave --replace bold
152
+ npx agentic-comments accept <id>
153
+ ```
154
+
155
+ - Commands: `init`, `list`, `show`, `comment`, `reply`, `resolve`, `reopen`, `relocate`, `suggestions`,
156
+ `suggest`, `accept`, `reject`, `mcp`. Run `agentic-comments --help` for flags.
157
+ - Every result is JSON. An error is JSON on stderr with a `code`, and the exit code is 1. A usage error
158
+ exits 2.
159
+ - The principal comes from `--as` or `AGENTIC_COMMENTS_AS`. Scopes come only from
160
+ `AGENTIC_COMMENTS_SCOPES`, a comma list such as `comments:accept`. No flag takes a scope or an author,
161
+ and a flag that does not apply to a command is refused. `AGENTIC_COMMENTS_LOCK_TIMEOUT_MS` sets the wait
162
+ for a lock, in whole milliseconds.
163
+ - `npx agentic-comments mcp` (or the `agentic-comments-mcp` binary, which reads the same environment) starts
164
+ the MCP server on stdio. The operator fixes the principal and the
165
+ scopes when the server starts. No tool takes an author or a scope. A call with any key a tool does not list is refused, so an agent
166
+ cannot pick its own kind or rights. Tools: `list_threads`, `get_thread`, `add_comment`, `reply_to_thread`, `resolve_thread`,
167
+ `reopen_thread`, `relocate_thread`, `list_suggestions`, `suggest_change`, `accept_suggestion`,
168
+ `reject_suggestion`.
169
+ - A text change is `{"replace": "new text"}`. A change for a cell, slide, or canvas anchor is
170
+ `{"value": <JSON>}`.
171
+ - From code: `import { FileStore, CommentsService, operatorActor } from "agentic-comments/local"`.
172
+
173
+ ## Notifications
174
+
175
+ `Notifications` turns changes into events that your app can send to a notifier. It watches the data, so it
176
+ sees changes from other replicas too. It replays nothing from before you created it. A first load into an
177
+ empty document looks like new changes, so create the stream with `muted: true` and call `unmute()` after
178
+ that load.
179
+
180
+ ```ts
181
+ import { Notifications } from "agentic-comments";
182
+
183
+ const notifications = new Notifications(comments, suggestions, { muted: true, onError: (error, source) => report(error, source) });
184
+ // ...load the document or connect the provider, then:
185
+ notifications.unmute();
186
+ const stop = notifications.subscribe(async (event) => {
187
+ if (event.type === "mention") await notify(event.mention, event.threadId); // fetch the text with the ids
188
+ });
189
+ ```
190
+
191
+ - Events: `thread.created`, `message.added`, `mention` (one per handle), `thread.resolved`,
192
+ `thread.reopened`, `suggestion.created`, `suggestion.accepted`, `suggestion.rejected`.
193
+ - An event holds ids, the actor, `machineMade`, the anchor type, `source` (`local` or `remote`), and a time.
194
+ It never holds the text of a comment, a suggestion, or a change. Fetch that with the ids.
195
+ - The same change has the same `id` on every replica. So a notifier that must
196
+ send once keeps the ids it has sent. Within one replica, an id is delivered at most once, and a listener
197
+ that throws is not retried. A replica remembers its last 10,000 ids (`seenLimit`) and drops a
198
+ repeat within that window. Two accepts at once give one id, and the actor can differ between replicas. A `suggestion.rejected` event
199
+ can be overturned: if another replica accepted at the same time, the merge ends as accepted, and a
200
+ `suggestion.accepted` event follows.
201
+ An id for a status change includes the clock time, so a clock coarser than the changes can merge two.
202
+ - Use `source` to decide where to send. If every replica runs a notifier, filter on `local`, or each
203
+ replica sends its own copy of every remote change.
204
+ - Events are delivered after the Yjs transaction that caused them has finished, so a listener can write to
205
+ the document. A listener that throws, or returns a rejected promise, cannot break a write or another
206
+ listener. The
207
+ error goes to `onError`, or to `errors()`.
208
+ - Write a thread in one Yjs transaction, as `add` does. A thread whose map arrives empty and gets its fields
209
+ and messages in one later transaction is treated as created then. A thread built in more steps than that
210
+ can make no event.
211
+ - A peer that deletes a thread or a suggestion and adds it back under the same id makes a `created` event
212
+ again once the id has left the `seenLimit` window.
213
+ - A change that arrives as a whole thread, with its replies and status inside, makes an event for each
214
+ message and for a resolved status. A reopen inside such a thread makes none. A message past the thread's read limit makes no event.
215
+
216
+ ## Author kinds
217
+
218
+ | Kind | Meaning | Label |
219
+ | --- | --- | --- |
220
+ | `human` | A person signed in. | person |
221
+ | `user_token` | A person, through a token an agent holds. | person via agent |
222
+ | `agent_of_human` | An agent that acts for a person. | agent |
223
+ | `agent_of_agent` | An agent that acts for another agent. | agent |
224
+ | `agent` | An agent with its own identity. | agent |
225
+
226
+ ## Limits
227
+
228
+ - No HTTP API yet. The file store is for one machine. It is not a sync server.
229
+ - `add` checks that a text anchor points into this document and matches its quote.
230
+ - The library trusts the host to call `stamp()` only for a principal it authenticated.
231
+ - A reply to a resolved thread is refused on the replica that sees the resolve. A reply and a
232
+ resolve made at once on two replicas both stay. The merged thread is resolved and holds the reply.
233
+ - Author and kind are only as trustworthy as every peer that can write the document. A peer can write
234
+ any author straight into the Yjs data. The sync layer must decide who may write.
235
+ - `relocate` and `exists` never touch a thread whose anchor is attached. `exists` runs for every thread
236
+ on every read, so keep it fast.
237
+ - A local reply stops at 1,000 messages. Replicas that reply at once can pass that. A read shows the first
238
+ 2,000 valid messages. `skipped` and `problems()` report the rest.
239
+ - If several processes clear one stale lock at the same moment, two writes can very rarely overlap. The
240
+ save check catches almost every such case. A symlink as the store path is followed. A filesystem with no
241
+ hard links makes `init` use a check and a rename, which can race.
242
+ - The CLI trusts its caller: a process that can set `AGENTIC_COMMENTS_AS` and `AGENTIC_COMMENTS_SCOPES` can
243
+ act as anyone. Give an agent the MCP server, where the operator fixes both. Do not give it a shell with
244
+ the store file and the CLI.
245
+ - Events come from the data and not from the calls, so a status change that two replicas make at once gives
246
+ each replica its own event, and a thread that arrives whole gives events for its current state only.
247
+ - Every replica of a document must register the same custom anchor kinds.
248
+ - A repeated message id hides every copy. A peer that appends a copy of a thread's only message makes
249
+ the thread unreadable.
250
+ - The quoted text of a text anchor is not checked when a thread is read. A peer can store any text there.
251
+ `textAnchor` refuses a quote whose JSON form passes 4,096 characters. Quotes with many newlines or
252
+ quotation marks hit that before 2,000 characters.
253
+ - The first visible message decides when a thread started, so a peer that inserts an older message at
254
+ the front moves the thread in `list()`.
255
+ - A read scans every stored entry. A peer that writes millions of junk entries slows every read.
256
+ - A body mentions anyone named as `@handle`. Put `@` text in backticks to avoid that. Any two backticks in a body pair up, so a loose backtick
257
+ can hide a real mention that sits between two of them. A scoped
258
+ package name such as `@scope/pkg` is not a mention.
259
+ - Mentions are stored in lowercase, with no leading `@`. A handle holds up to 64 letters, digits, marks,
260
+ and `. _ -`. A longer handle in a body is refused.
261
+
262
+ ## Develop
263
+
264
+ ```sh
265
+ git clone https://github.com/pooriaarab/agentic-comments
266
+ cd agentic-comments
267
+ npm ci
268
+ npm run ci:local
269
+ ```
270
+
271
+ ## License
272
+
273
+ MIT.
package/SKILL.md ADDED
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: agentic-comments
3
+ description: Comment threads and suggestions anchored to any content, from people and agents. Keywords: comments,threads,mentions,suggestions,annotations,collaboration,agents,yjs.
4
+ ---
5
+
6
+ # agentic-comments
7
+
8
+ Comment threads and suggestions anchored to any content, from people and agents.
9
+
10
+ ## Use the SDK
11
+
12
+ The host gives you an `Actor` that names you. Pass it to each call.
13
+
14
+ ```ts
15
+ import { Comments } from "agentic-comments";
16
+
17
+ const comments = new Comments(ydoc);
18
+ const thread = comments.add({ anchor: { type: "cell", key: "Sheet1!B4" }, body: "Check this total." }, actor);
19
+ comments.reply(thread.id, { body: "Fixed." }, actor);
20
+ ```
21
+
22
+ Check `thread.anchorState`. An `orphaned` thread has lost its content. Read it, reply, or call
23
+ `comments.relocate(id, actor)`. Do not treat it as bad data.
24
+
25
+ Suggest a change with `suggestions.add({ anchor, change, summary }, actor)`. Accept needs a person or
26
+ the `comments:accept` scope. You never accept or reject your own suggestion.
27
+
28
+ Over MCP, call `list_threads`, `add_comment`, `reply_to_thread`, `resolve_thread`, `suggest_change`,
29
+ `accept_suggestion`, and `reject_suggestion`. You never pass an author or a scope.
30
+
31
+ ## Rules
32
+
33
+ - The host stamps the author. Never call `stamp()` yourself, and never pass an author in an input.
34
+ - Reopen a resolved thread before you reply.
35
+ - Report refusals and errors as failures.