agentic-comments 0.6.1 → 0.8.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/README.md +78 -2
- package/SKILL.md +5 -0
- package/dist/account.d.ts +62 -0
- package/dist/account.js +114 -0
- package/dist/{chunk-VAVKBAHM.js → chunk-4G5BDTBA.js} +574 -574
- package/dist/{chunk-H6JK62SP.js → chunk-6UQ6CDLJ.js} +1 -1
- package/dist/{chunk-UNJL3N2L.js → chunk-J6JGWDUP.js} +3 -3
- package/dist/{chunk-6N53KQVU.js → chunk-LFYTCHXS.js} +1 -1
- package/dist/{chunk-T77DPNIV.js → chunk-TTNT27PG.js} +1 -1
- package/dist/chunk-UEQL27KF.js +148 -0
- package/dist/cli.js +7 -7
- package/dist/client.d.ts +3 -3
- package/dist/client.js +4 -143
- package/dist/{errors-C-xvp24-.d.ts → errors-Cin-719h.d.ts} +3 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/local.d.ts +2 -2
- package/dist/local.js +3 -3
- package/dist/mcp-IFQQHOA2.js +11 -0
- package/dist/mcp-bin.js +9 -9
- package/dist/{service-BvIE6t5n.d.ts → service-ZdZPWrwI.d.ts} +1 -1
- package/dist/{suggestions-CnkEcTuY.d.ts → suggestions-G1seGf5d.d.ts} +5 -2
- package/dist/ui.css +54 -0
- package/dist/ui.d.ts +46 -0
- package/dist/ui.js +332 -0
- package/dist/worker.d.ts +26 -2
- package/dist/worker.js +91 -3
- package/llms.txt +2 -0
- package/package.json +39 -7
- package/dist/mcp-WBAHKMF4.js +0 -11
package/README.md
CHANGED
|
@@ -86,6 +86,36 @@ erased message or a thread that holds one.
|
|
|
86
86
|
Replicas that edit one message at once leave a copy each. When the copies agree on the author kind and time,
|
|
87
87
|
the read keeps the erased copy, or else the latest edit. An edit and a delete made at once keep the edit.
|
|
88
88
|
|
|
89
|
+
## Account export and erase
|
|
90
|
+
|
|
91
|
+
`import { commentsAccount } from "agentic-comments/account"` lets a host export and erase one person's data
|
|
92
|
+
without reading the storage layout. Pass the subject (`{ kind, name }`) and `{ documents, actor }`. Each
|
|
93
|
+
document is `{ id, comments, suggestions }`. The actor is made with `stamp()` and must name the subject.
|
|
94
|
+
`exportAccount` returns JSON records in `messages`, `statusChanges`, `edits`, `suggestions`, `decisions`,
|
|
95
|
+
`documents`, and `unread`. `eraseAccount` tombstones the same data as `erasePerson` and returns counts. A second
|
|
96
|
+
erase counts 0, except `unread`: see below.
|
|
97
|
+
|
|
98
|
+
The package keeps no grants or audit rows. A host that runs the HTTP worker passes `worker: docs` (the
|
|
99
|
+
`CommentsDocs` of the worker). Then the run also covers every stored document: the subject's messages and
|
|
100
|
+
suggestions in it, and the creator that the worker keeps on each row. The creators are listed under `documents`
|
|
101
|
+
and get the tombstone name on erase. Without `worker`, that data stays. A stored document is erased in one
|
|
102
|
+
transaction. A stored document that `documents` also lists is refused, before any write. The check compares ids
|
|
103
|
+
only. A row whose creator cannot be read shows in `unread` of the export, and in the `unread` count of the erase,
|
|
104
|
+
which leaves the row in place. `unread` counts every row it cannot read, whoever made it, so it can stay above 0 on a repeat. A store that loses a race gives `CONFLICT`: run the erase again.
|
|
105
|
+
|
|
106
|
+
A run covers only the subject's own kind: `human:ada` does not reach `agent_of_human:ada`. Names and mentions of
|
|
107
|
+
the subject inside other people's messages are neither exported nor erased. Records can name other people, such
|
|
108
|
+
as the moderator in `editedBy` or the person in `decidedBy`. A record in `unread` means the export is not
|
|
109
|
+
complete: the store holds entries of the subject that a read cannot show. A missing `documents` list is an
|
|
110
|
+
error, not an empty run. Erase goes document by document. If one fails, the error code tells why, the message names the document when the store allows, and the earlier
|
|
111
|
+
documents stay erased. A failed document in `documents` may be partly erased (its threads and its suggestions
|
|
112
|
+
are two steps). A repeat counts only what is left. The counts of a failed run are lost.
|
|
113
|
+
|
|
114
|
+
The ids in `documents` and the ids of the worker share one namespace. Erase can make a state a little larger, so a stored
|
|
115
|
+
document near the size limit may stop the run with `STORE_FULL`: the host must shrink or delete that document, and
|
|
116
|
+
run the erase again. The run reads the list of stored documents once. Block the subject before the erase, so no
|
|
117
|
+
document is made during it. A repeat erase finishes what a race left.
|
|
118
|
+
|
|
89
119
|
## Anchors for plain text
|
|
90
120
|
|
|
91
121
|
`agentic-comments/anchor` pins a range of a plain string. It needs no Yjs and no framework.
|
|
@@ -321,12 +351,42 @@ try {
|
|
|
321
351
|
- The client never follows a redirect and never retries. A write that you abort after the client sends it can still apply:
|
|
322
352
|
read the thread before you try again.
|
|
323
353
|
|
|
354
|
+
## React panel
|
|
355
|
+
|
|
356
|
+
`agentic-comments/ui` is a threaded comments panel for a React page. It shows the thread list, the quote each thread
|
|
357
|
+
points at, a reply box, resolve and reopen, and an author mark on every message. A person shows a name. An agent
|
|
358
|
+
shows the `AgentBadge` of `agentic-design`, with the words "Comment by an agent". Each suggestion has Accept and
|
|
359
|
+
Reject buttons. React 19 and `react-dom` are peer dependencies. The package depends on `agentic-design`.
|
|
360
|
+
|
|
361
|
+
```tsx
|
|
362
|
+
import { CommentsPanel, createClient } from "agentic-comments/ui";
|
|
363
|
+
import "agentic-design/styles.css";
|
|
364
|
+
import "agentic-comments/ui.css";
|
|
365
|
+
|
|
366
|
+
const client = createClient({ baseUrl: "https://comments.example.com", token });
|
|
367
|
+
<CommentsPanel client={client} docId="report-q3" />;
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
`client` is any object with the methods of `PanelClient`, so a host can pass its own. The panel reads who is signed
|
|
371
|
+
in from `client.me()`. It follows the product rule for suggestions: an agent never gets Accept or Reject for its own
|
|
372
|
+
suggestion. Through the Worker, an agent needs the `comments:admin` scope to decide any suggestion. With a client of your own, the `comments:accept` right is enough too. A person who holds
|
|
373
|
+
`comments:write` decides any suggestion. `mayDecide(me, suggestion)` is exported, so a host can use the same rule.
|
|
374
|
+
A hidden button is a help and not a lock: the Worker checks every call. The panel shows an error with its code, and
|
|
375
|
+
it never retries a call with other input. It needs a document whose text holds the quote, because a new thread
|
|
376
|
+
points at a quote. It meets WCAG 2.2 AA (axe runs on light, dark, and 390px) and its classes all start with `agd-`.
|
|
377
|
+
|
|
378
|
+
The demo page in `ui/` runs the panel with no token on in-memory data, with a switch for the signed-in principal. Open
|
|
379
|
+
`/#token=<bearer>&doc=<id>` on a Worker that serves it, and it talks to that Worker. `npm run test:ui` drives it in
|
|
380
|
+
Chromium against `wrangler dev`.
|
|
381
|
+
|
|
382
|
+

|
|
383
|
+
|
|
324
384
|
## Live demo
|
|
325
385
|
|
|
326
386
|
A demo Worker runs at <https://agentic-comments-demo.hello-fd1.workers.dev>. It is a staging site. Check it with
|
|
327
387
|
`GET /health`. Every `/api/v1` route needs a bearer token, and the demo does not publish one, so a call
|
|
328
388
|
with no token gets `401`. The data lives in a D1 database in the EU (`weur`), and the Worker sets the
|
|
329
|
-
region to `eu`.
|
|
389
|
+
region to `eu`. The panel demo page is at the root of the demo Worker.
|
|
330
390
|
|
|
331
391
|
To deploy your own copy, log in with `wrangler login` or set `CLOUDFLARE_API_TOKEN`, then:
|
|
332
392
|
|
|
@@ -436,13 +496,29 @@ const stop = notifications.subscribe(async (event) => {
|
|
|
436
496
|
- Mentions are stored in lowercase, with no leading `@`. A handle holds up to 64 letters, digits, marks,
|
|
437
497
|
and `. _ -`. A longer handle in a body is refused.
|
|
438
498
|
|
|
499
|
+
## Run locally
|
|
500
|
+
|
|
501
|
+
```sh
|
|
502
|
+
git clone https://github.com/pooriaarab/agentic-comments
|
|
503
|
+
cd agentic-comments
|
|
504
|
+
npm install && npm run dev
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
You can also use `bun install && bun run dev`. The `dev` script calls npm, so npm must be installed. The scripts need macOS, Linux, or WSL. The `dev` script builds the package, then starts `wrangler dev` on <http://localhost:8794>. Wrangler keeps a local D1 database. The `dev` script turns off Wrangler telemetry. Set `WRANGLER_SEND_METRICS=false` when you run other wrangler commands.
|
|
508
|
+
|
|
509
|
+
```sh
|
|
510
|
+
curl -i http://localhost:8794/health # 200
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
`npm run dev:smoke` runs the same check by itself. It starts the dev server. It waits for `/health` to answer 200. It stops the server. CI runs it as the "dev smoke" step. To use another port, change `8794` in the `dev` and `dev:smoke` scripts.
|
|
514
|
+
|
|
439
515
|
## Develop
|
|
440
516
|
|
|
441
517
|
```sh
|
|
442
518
|
git clone https://github.com/pooriaarab/agentic-comments
|
|
443
519
|
cd agentic-comments
|
|
444
520
|
npm ci
|
|
445
|
-
npm run ci:local
|
|
521
|
+
npm run ci:local # build, typecheck, unit tests, then the browser tests (npx playwright install chromium once)
|
|
446
522
|
```
|
|
447
523
|
|
|
448
524
|
## License
|
package/SKILL.md
CHANGED
|
@@ -32,6 +32,11 @@ anyone's with that scope). Check `unread` in an export.
|
|
|
32
32
|
Over MCP, call `list_threads`, `add_comment`, `reply_to_thread`, `resolve_thread`, `suggest_change`,
|
|
33
33
|
`accept_suggestion`, `reject_suggestion`, and the tools above. You never pass an author or a scope.
|
|
34
34
|
|
|
35
|
+
## Show threads in a React page
|
|
36
|
+
|
|
37
|
+
`import { CommentsPanel, createClient } from "agentic-comments/ui"` and load `agentic-design/styles.css` and `agentic-comments/ui.css`.
|
|
38
|
+
`<CommentsPanel client={createClient({ baseUrl, token })} docId="..." />`. The panel never offers an agent Accept or Reject for its own suggestion.
|
|
39
|
+
|
|
35
40
|
## Rules
|
|
36
41
|
|
|
37
42
|
- The host stamps the author. Never call `stamp()` yourself, and never pass an author in an input.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { C as Comments, c as Suggestions, b as Author, A as Actor } from './suggestions-G1seGf5d.js';
|
|
2
|
+
import 'yjs';
|
|
3
|
+
import 'agentic-identity';
|
|
4
|
+
|
|
5
|
+
/** One document the host holds comments on. The host loads it; the package never lists documents. */
|
|
6
|
+
interface CommentsAccountDocument {
|
|
7
|
+
/** A stable id for the document. It goes in each exported record. */
|
|
8
|
+
id: string;
|
|
9
|
+
comments: Comments;
|
|
10
|
+
suggestions: Suggestions;
|
|
11
|
+
}
|
|
12
|
+
/** What `CommentsDocs` offers the plug-in. */
|
|
13
|
+
interface AccountWorker {
|
|
14
|
+
/** The id of each stored document. */
|
|
15
|
+
accountDocumentIds(): Promise<string[]>;
|
|
16
|
+
/** Runs `fn` on each stored document. With `save`, the state is written back in the same transaction. A document deleted meanwhile is skipped. `committed` is called with the id after its transaction commits. */
|
|
17
|
+
eachAccountDocument(fn: (doc: CommentsAccountDocument) => void, save: boolean, committed?: (id: string) => void): Promise<void>;
|
|
18
|
+
/** The documents the subject created. `unread` names the rows whose creator cannot be read, so the export is not complete for them. */
|
|
19
|
+
exportCreated(author: Author): Promise<{
|
|
20
|
+
documents: {
|
|
21
|
+
documentId: string;
|
|
22
|
+
createdAt: unknown;
|
|
23
|
+
}[];
|
|
24
|
+
unread: string[];
|
|
25
|
+
}>;
|
|
26
|
+
/** `count` is the creators it tombstoned. `unread` names the rows whose creator cannot be read, which it leaves in place. */
|
|
27
|
+
eraseCreated(author: Author): Promise<{
|
|
28
|
+
count: number;
|
|
29
|
+
unread: string[];
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
interface CommentsAccountDeps {
|
|
33
|
+
/** Every document that may hold data of the subject. */
|
|
34
|
+
documents: Iterable<CommentsAccountDocument>;
|
|
35
|
+
/** Made with `stamp()`. It must name the subject: the same kind and name. A moderate scope does not widen the run. */
|
|
36
|
+
actor: Actor;
|
|
37
|
+
/**
|
|
38
|
+
* The document store of the HTTP worker (`CommentsDocs`), when the host runs it. Its rows hold the threads of each
|
|
39
|
+
* document and the creator of each document, outside what `documents` can reach. Pass it, and the run covers every
|
|
40
|
+
* stored document. Without it, a worker host leaves that data in place.
|
|
41
|
+
*/
|
|
42
|
+
worker?: AccountWorker;
|
|
43
|
+
}
|
|
44
|
+
type JsonRecord = Record<string, unknown>;
|
|
45
|
+
interface CommentsAccount {
|
|
46
|
+
id: "comments";
|
|
47
|
+
/**
|
|
48
|
+
* Collections, each record tagged with `documentId`: `messages` (with the thread anchor), `statusChanges`,
|
|
49
|
+
* `edits` (the subject's edits of other people's messages), `suggestions` (the ones the subject wrote),
|
|
50
|
+
* `decisions` (accepts and rejects by the subject), `documents` (the worker documents the subject created, only
|
|
51
|
+
* when `deps.worker` is set), and `unread` (stored entries of the subject that a read
|
|
52
|
+
* cannot show; the export is not complete when this has a record). The package keeps no grants or audit rows. Its
|
|
53
|
+
* one owner-like record is the creator of a worker document, which `deps.worker` covers. A host that runs the
|
|
54
|
+
* worker and omits `worker` leaves that creator in place.
|
|
55
|
+
*/
|
|
56
|
+
exportAccount(subject: Author, deps: CommentsAccountDeps): Promise<Record<"messages" | "statusChanges" | "edits" | "suggestions" | "decisions" | "documents" | "unread", JsonRecord[]>>;
|
|
57
|
+
/** Replaces what the subject wrote with a tombstone (see `Comments.erasePerson`). Counts by kind, summed over documents. The thread shape stays. A repeat finds nothing and counts 0. A decision that the subject made on their own suggestion is deleted and not counted, while `unread` in the export counts it. */
|
|
58
|
+
eraseAccount(subject: Author, deps: CommentsAccountDeps): Promise<Record<"messages" | "statuses" | "edits" | "suggestions" | "decisions" | "documents" | "unread", number>>;
|
|
59
|
+
}
|
|
60
|
+
declare const commentsAccount: CommentsAccount;
|
|
61
|
+
|
|
62
|
+
export { type AccountWorker, type CommentsAccount, type CommentsAccountDeps, type CommentsAccountDocument, commentsAccount };
|
package/dist/account.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Actor,
|
|
3
|
+
Comments,
|
|
4
|
+
CommentsError,
|
|
5
|
+
Suggestions,
|
|
6
|
+
assertActor,
|
|
7
|
+
readAuthor,
|
|
8
|
+
shown
|
|
9
|
+
} from "./chunk-4G5BDTBA.js";
|
|
10
|
+
|
|
11
|
+
// src/account.ts
|
|
12
|
+
import { StoreConflictError } from "agentic-identity";
|
|
13
|
+
var plain = (value) => JSON.parse(JSON.stringify(value));
|
|
14
|
+
function check(subject, deps) {
|
|
15
|
+
const who = readAuthor(subject);
|
|
16
|
+
if (!who) throw new CommentsError("INVALID_INPUT", "subject needs a known kind and a name");
|
|
17
|
+
const actor = assertActor(deps?.actor);
|
|
18
|
+
if (actor.kind !== who.kind || actor.name !== who.name) throw new CommentsError("FORBIDDEN", "actor does not match the subject");
|
|
19
|
+
if (typeof deps?.documents?.[Symbol.iterator] !== "function") throw new CommentsError("INVALID_INPUT", "deps.documents must be a list of documents");
|
|
20
|
+
const documents = [...deps.documents];
|
|
21
|
+
const ids = /* @__PURE__ */ new Set();
|
|
22
|
+
const seen = /* @__PURE__ */ new Set();
|
|
23
|
+
const bad = (why) => {
|
|
24
|
+
throw new CommentsError("INVALID_INPUT", `deps: ${why}`);
|
|
25
|
+
};
|
|
26
|
+
if (deps.worker !== void 0 && ["accountDocumentIds", "eachAccountDocument", "exportCreated", "eraseCreated"].some((m) => typeof deps.worker?.[m] !== "function")) bad("worker needs accountDocumentIds, eachAccountDocument, exportCreated, and eraseCreated");
|
|
27
|
+
for (const d of documents) {
|
|
28
|
+
if (typeof d?.id !== "string" || d.id === "" || ids.has(d.id)) bad("each document needs a unique id");
|
|
29
|
+
if (!(d.comments instanceof Comments) || !(d.suggestions instanceof Suggestions)) bad("each document needs Comments and Suggestions made by this package");
|
|
30
|
+
if (d.suggestions.comments.ydoc !== d.comments.ydoc) bad("suggestions must be made on the same comments");
|
|
31
|
+
if (seen.has(d.comments.ydoc)) bad("two documents share one Yjs document");
|
|
32
|
+
ids.add(d.id);
|
|
33
|
+
seen.add(d.comments.ydoc);
|
|
34
|
+
}
|
|
35
|
+
return { documents, own: Actor.of(who) };
|
|
36
|
+
}
|
|
37
|
+
function collect({ id: documentId, comments, suggestions }, own, out) {
|
|
38
|
+
const c = comments.exportPerson(own.name, own);
|
|
39
|
+
const s = suggestions.exportPerson(own.name, own);
|
|
40
|
+
for (const t of c.threads) {
|
|
41
|
+
for (const message of t.messages) out.messages.push({ ...message, documentId, threadId: t.threadId, anchor: t.anchor });
|
|
42
|
+
if (t.statusChange) out.statusChanges.push({ ...t.statusChange, documentId, threadId: t.threadId });
|
|
43
|
+
}
|
|
44
|
+
for (const e of c.edits) out.edits.push({ ...e, documentId });
|
|
45
|
+
for (const x of s.authored) out.suggestions.push({ ...x, documentId });
|
|
46
|
+
for (const d of s.decided) out.decisions.push({ documentId, suggestionId: d.id, status: d.status, at: d.at });
|
|
47
|
+
const unread = c.unread + s.unread;
|
|
48
|
+
if (unread > 0) out.unread.push({ documentId, count: unread });
|
|
49
|
+
}
|
|
50
|
+
async function stopAt(where, run) {
|
|
51
|
+
try {
|
|
52
|
+
return await run();
|
|
53
|
+
} catch (cause) {
|
|
54
|
+
const known = cause instanceof CommentsError;
|
|
55
|
+
const reason = cause instanceof Error ? cause.message : "unknown error";
|
|
56
|
+
if (!known && !(cause instanceof StoreConflictError)) throw cause;
|
|
57
|
+
throw new CommentsError(known ? cause.code : "CONFLICT", `erase stopped at ${where}: ${reason}`, { cause });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function refuseOverlap(documents, worker) {
|
|
61
|
+
const listed = new Set(documents.map((d) => d.id));
|
|
62
|
+
if ((await worker.accountDocumentIds()).some((id) => listed.has(id))) throw new CommentsError("INVALID_INPUT", "deps.worker holds a document that deps.documents also lists");
|
|
63
|
+
}
|
|
64
|
+
function wipe({ comments, suggestions }, own, counts) {
|
|
65
|
+
const c = comments.erasePerson(own.name, own);
|
|
66
|
+
const s = suggestions.erasePerson(own.name, own);
|
|
67
|
+
counts.messages += c.messages;
|
|
68
|
+
counts.statuses += c.statuses;
|
|
69
|
+
counts.edits += c.edits;
|
|
70
|
+
counts.suggestions += s.suggestions;
|
|
71
|
+
counts.decisions += s.decisions;
|
|
72
|
+
}
|
|
73
|
+
var commentsAccount = {
|
|
74
|
+
id: "comments",
|
|
75
|
+
async exportAccount(subject, deps) {
|
|
76
|
+
const { documents, own } = check(subject, deps);
|
|
77
|
+
const out = { messages: [], statusChanges: [], edits: [], suggestions: [], decisions: [], documents: [], unread: [] };
|
|
78
|
+
if (deps.worker) await refuseOverlap(documents, deps.worker);
|
|
79
|
+
for (const doc of documents) collect(doc, own, out);
|
|
80
|
+
if (deps.worker) {
|
|
81
|
+
await deps.worker.eachAccountDocument((doc) => collect(doc, own, out), false);
|
|
82
|
+
const created = await deps.worker.exportCreated(own);
|
|
83
|
+
out.documents.push(...created.documents);
|
|
84
|
+
for (const documentId of created.unread) out.unread.push({ documentId, count: 1 });
|
|
85
|
+
}
|
|
86
|
+
return plain(out);
|
|
87
|
+
},
|
|
88
|
+
async eraseAccount(subject, deps) {
|
|
89
|
+
const { documents, own } = check(subject, deps);
|
|
90
|
+
if (deps.worker) await refuseOverlap(documents, deps.worker);
|
|
91
|
+
const counts = { messages: 0, statuses: 0, edits: 0, suggestions: 0, decisions: 0, documents: 0, unread: 0 };
|
|
92
|
+
for (const doc of documents) await stopAt(`document ${shown(doc.id)}`, () => wipe(doc, own, counts));
|
|
93
|
+
if (deps.worker) {
|
|
94
|
+
const each = /* @__PURE__ */ new Map();
|
|
95
|
+
const done = /* @__PURE__ */ new Set();
|
|
96
|
+
await stopAt(
|
|
97
|
+
"a worker document",
|
|
98
|
+
() => deps.worker.eachAccountDocument((doc) => {
|
|
99
|
+
const one = { messages: 0, statuses: 0, edits: 0, suggestions: 0, decisions: 0, documents: 0, unread: 0 };
|
|
100
|
+
wipe(doc, own, one);
|
|
101
|
+
each.set(doc.id, one);
|
|
102
|
+
}, true, (id) => void done.add(id))
|
|
103
|
+
);
|
|
104
|
+
for (const [id, c] of each) if (done.has(id)) for (const k of Object.keys(counts)) counts[k] += c[k];
|
|
105
|
+
const creators = await stopAt("the worker creators", () => deps.worker.eraseCreated(own));
|
|
106
|
+
counts.documents = creators.count;
|
|
107
|
+
counts.unread = creators.unread.length;
|
|
108
|
+
}
|
|
109
|
+
return counts;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
export {
|
|
113
|
+
commentsAccount
|
|
114
|
+
};
|