agentic-documents 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/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +237 -2
- package/SECURITY.md +23 -0
- package/SKILL.md +35 -0
- package/dist/chunk-5Y66UCTE.js +7 -0
- package/dist/chunk-FZA5LQV3.js +150 -0
- package/dist/chunk-GYQFTF2E.js +389 -0
- package/dist/chunk-IU3YIQ4Y.js +232 -0
- package/dist/chunk-R5XUTBC3.js +177 -0
- package/dist/chunk-TFCRPEEM.js +224 -0
- package/dist/chunk-YBBQNCMC.js +744 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +165 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.js +44 -0
- package/dist/live-worker.d.ts +20 -0
- package/dist/live-worker.js +27 -0
- package/dist/live.d.ts +73 -0
- package/dist/live.js +12 -0
- package/dist/mcp-P2C75IIG.js +40 -0
- package/dist/service-BKkxEnaJ.d.ts +181 -0
- package/dist/worker.d.ts +50 -0
- package/dist/worker.js +15 -0
- package/llms.txt +15 -0
- package/package.json +104 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Breaking changes
|
|
6
|
+
|
|
7
|
+
- **Worker deployments from the repo template.** `wrangler.jsonc` now sets `main` to
|
|
8
|
+
`src/live-worker.ts`. To host live rooms, add the `SYNC_ROOM` Durable Object binding and the `v1`
|
|
9
|
+
migration (`new_sqlite_classes`). Without the binding, the room routes fail with `MISCONFIGURED`.
|
|
10
|
+
The published package does not include `wrangler.jsonc`.
|
|
11
|
+
- **Token table.** An `AGENTIC_DOCS_TOKENS` entry in object form may now carry a `role`. A `role` that
|
|
12
|
+
is not `editor` or `viewer` refuses the whole token table, so a typo never leaves a token live with
|
|
13
|
+
the wrong role. Entries with no `role` stay editors, as before.
|
|
14
|
+
- **Dependencies.** `agentic-identity` moves from `^0.2.0` to `^0.3.0`.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Live rooms. Several people and agents edit one document at the same time. Import `LiveDoc` from
|
|
19
|
+
`agentic-documents/live`. Deploy `agentic-documents/live-worker` to host the rooms.
|
|
20
|
+
- The `agentic-realtime` dependency (`^0.1.0`, the version on npm) for the live rooms.
|
|
21
|
+
- `--live <url>` on the CLI and the MCP server, or `AGENTIC_DOCS_LIVE_URL`. Set `AGENTIC_DOCS_TOKEN`
|
|
22
|
+
to join a room. The server sets the author kind and the role from the token.
|
|
23
|
+
- With `--live`, the CLI refuses `--as`, `--scopes`, and `--dir`, and exits 2. The server sets these
|
|
24
|
+
from the token.
|
|
25
|
+
- A `viewer` role. A viewer reads and cannot write, over HTTP or in a live room. The default role is
|
|
26
|
+
`editor`.
|
|
27
|
+
- Worker routes for live rooms: `GET /api/v1/me`, `GET /api/v1/docs/:id/room`,
|
|
28
|
+
`GET /api/v1/docs/:id/ws`, `GET /api/v1/docs/:id/state`, and `POST /api/v1/docs/:id/checkpoint`.
|
|
29
|
+
- Live rooms save through the shared `Store` and `Blobs` ports. A stored document too big for a room
|
|
30
|
+
fails the join with a clear error.
|
|
31
|
+
- `npm pack` and `npm publish` build `dist/` first (`prepack`).
|
|
32
|
+
|
|
33
|
+
## 0.1.0
|
|
34
|
+
|
|
35
|
+
- First release. The document model, the Markdown and ODT export, the text operations, the CLI, the
|
|
36
|
+
MCP server, and the Worker HTTP API.
|
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,238 @@
|
|
|
1
|
-
#
|
|
1
|
+
# agentic-documents
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/agentic-documents)
|
|
4
|
+
[](https://opensource.org/license/mit)
|
|
5
|
+
|
|
6
|
+
[Package](https://www.npmjs.com/package/agentic-documents) · [Source](https://github.com/pooriaarab/agentic-documents) · [Support](https://github.com/pooriaarab/agentic-documents/issues)
|
|
7
|
+
|
|
8
|
+
Write documents with people and agents in real time.
|
|
9
|
+
|
|
10
|
+
> Status: early, version 0.2.0. The document model, the text operations, the CLI, the MCP server,
|
|
11
|
+
> the Worker API, and live editing in a room work. The web UI is not built yet.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install agentic-documents
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { AgenticDoc, Suggestions, Threads } from "agentic-documents";
|
|
23
|
+
|
|
24
|
+
const agent = { kind: "agent_of_human", name: "scribe" } as const;
|
|
25
|
+
|
|
26
|
+
const doc = AgenticDoc.fromMarkdown("# Plan\n\nShip on Friday.", agent);
|
|
27
|
+
const [plan] = doc.outline();
|
|
28
|
+
|
|
29
|
+
doc.replace({ text: "Friday" }, "Monday", agent);
|
|
30
|
+
doc.insert({ section: plan.id, at: "end" }, "- [ ] Write notes", agent);
|
|
31
|
+
|
|
32
|
+
console.log(doc.toMarkdown());
|
|
33
|
+
console.log(doc.edits()); // each edit keeps the author kind and name
|
|
34
|
+
|
|
35
|
+
const person = { kind: "human", name: "Ada" } as const;
|
|
36
|
+
const suggestions = new Suggestions(doc);
|
|
37
|
+
const id = suggestions.add({ text: "Monday" }, "Tuesday", agent);
|
|
38
|
+
suggestions.accept(id, person); // the text changes now
|
|
39
|
+
new Threads(doc).add({ text: "Tuesday" }, "Is this date firm?", agent);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## CLI
|
|
43
|
+
|
|
44
|
+
Every change needs an author: `--as <kind>:<name>` or `AGENTIC_DOCS_AS`. An agent that may accept
|
|
45
|
+
suggestions also needs `--scopes docs:accept` (or `AGENTIC_DOCS_SCOPES`). Documents live in
|
|
46
|
+
`--dir` (or `AGENTIC_DOCS_DIR`, default `.agentic-documents`) as Yjs update files.
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
export AGENTIC_DOCS_AS=agent_of_human:scribe
|
|
50
|
+
printf '# Plan\n\nShip on Friday.\n' > plan.md
|
|
51
|
+
npx agentic-documents new plan --file plan.md
|
|
52
|
+
npx agentic-documents outline plan
|
|
53
|
+
npx agentic-documents replace plan --text Friday --content Monday
|
|
54
|
+
npx agentic-documents suggest plan --text Monday --replacement Tuesday
|
|
55
|
+
npx agentic-documents comment plan --text Monday --body "Is this date firm?"
|
|
56
|
+
npx agentic-documents export plan --format md --out plan.md
|
|
57
|
+
npx agentic-documents export plan --format odt --out plan.odt # OpenDocument text
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Run `npx agentic-documents --help` for every command.
|
|
61
|
+
|
|
62
|
+
## MCP
|
|
63
|
+
|
|
64
|
+
`npx agentic-documents mcp --as agent_of_human:claude` starts a stdio server. It has one tool
|
|
65
|
+
per command, named `doc_<command>` (`doc_read`, `doc_replace`, `doc_suggest`, and so on). The
|
|
66
|
+
author is fixed when the server starts. A tool call cannot pick another author.
|
|
67
|
+
|
|
68
|
+
## HTTP API
|
|
69
|
+
|
|
70
|
+
A Cloudflare Worker (Hono) mirrors the CLI one to one under `/api/v1/docs`. A read command is a
|
|
71
|
+
`GET` (flags are query parameters). A write command is a `POST` (flags are the JSON body).
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
curl -X POST $URL/api/v1/docs -H "authorization: Bearer $TOKEN" -d '{"id":"plan","markdown":"# Plan"}'
|
|
75
|
+
curl "$URL/api/v1/docs/plan/outline" -H "authorization: Bearer $TOKEN"
|
|
76
|
+
curl -X POST $URL/api/v1/docs/plan/replace -H "authorization: Bearer $TOKEN" -d '{"text":"Plan","content":"Roadmap"}'
|
|
77
|
+
curl "$URL/api/v1/docs/plan/export?format=html" -H "authorization: Bearer $TOKEN"
|
|
78
|
+
# add &remoteImages=true to keep web images; the default shows only images stored inside the document
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- Each token stands for one author. Set `AGENTIC_DOCS_TOKENS` as a secret:
|
|
82
|
+
`wrangler secret put AGENTIC_DOCS_TOKENS`. The value is a JSON object from token to author:
|
|
83
|
+
`{"<32 or more random bytes>":"agent_of_human:scribe"}`. To give an agent the right to accept
|
|
84
|
+
suggestions, use `{"as":"agent_of_human:approver","scopes":["docs:accept"]}` as the value.
|
|
85
|
+
Make a token with `openssl rand -hex 32`.
|
|
86
|
+
- A token under 32 bytes, a value that is not a valid author, or a value that is not a JSON
|
|
87
|
+
object makes the Worker refuse every request (401). It logs the reason once, never the token.
|
|
88
|
+
A body cannot name its own author. With no tokens the API refuses every request.
|
|
89
|
+
- Errors are `{ "error": { "code", "message", "matches" } }`. Status: 400 bad input, 401 no
|
|
90
|
+
valid token, 403 `FORBIDDEN` (for example an agent without `docs:accept` that accepts), 404 not
|
|
91
|
+
found, 409 a conflict (ambiguous match, stale anchor, already decided), 503 `LOCKED`.
|
|
92
|
+
- `GET /health` needs no token. It reveals nothing else.
|
|
93
|
+
- Create an R2 bucket with `wrangler r2 bucket create agentic-documents-data --jurisdiction eu` and a D1
|
|
94
|
+
database with `wrangler d1 create agentic-documents --jurisdiction eu`. Put the database id in
|
|
95
|
+
`wrangler.jsonc`. Set `AGENTIC_DOCS_REGION` to `eu` or `us`. It is a claim that you make: it has no
|
|
96
|
+
default, and the Worker answers 500 `MISCONFIGURED` without it. Local D1 reports no region, so under
|
|
97
|
+
`wrangler dev` only, add `--var AGENTIC_DOCS_REGION:unknown --var AGENTIC_DOCS_UNSAFE_REGION:1`. That turns the
|
|
98
|
+
residency check off.
|
|
99
|
+
- Run it locally with `npx wrangler dev`. Set `WRANGLER_SEND_METRICS=false` to turn off wrangler telemetry.
|
|
100
|
+
|
|
101
|
+
## Features
|
|
102
|
+
|
|
103
|
+
- A Yjs document that a Tiptap editor can open. It uses the y-prosemirror layout.
|
|
104
|
+
- Headings, paragraphs, lists, task lists, tables, code, images, links, and marks.
|
|
105
|
+
- Markdown in and out.
|
|
106
|
+
- An outline. Each section has an id that stays the same when people edit around it. Blocks before the
|
|
107
|
+
first heading are the `preamble` section (level 0). It is listed when they exist, and it is always a
|
|
108
|
+
valid target. A replace or delete on it has no heading to keep.
|
|
109
|
+
- Insert, replace, and delete by section id or by exact text. Append to the end.
|
|
110
|
+
- Text anchors that follow their text when other replicas edit.
|
|
111
|
+
- Clear errors: `NO_MATCH`, `AMBIGUOUS_MATCH`, `STALE_ANCHOR`, `NO_SECTION`, `FORBIDDEN`.
|
|
112
|
+
- Comment threads on text, with replies and resolve. A thread follows its text.
|
|
113
|
+
- Suggested edits (insert, replace, delete). Nothing changes until a person accepts.
|
|
114
|
+
An accepted edit is logged with the accepter as editor and the suggester as origin.
|
|
115
|
+
- Who may decide: a `human` or a `user_token` who is not the suggester. An agent cannot accept
|
|
116
|
+
unless its principal carries the `docs:accept` scope. Nobody accepts their own suggestion.
|
|
117
|
+
A person can withdraw their own suggestion. An agent can never accept or reject its own. Other
|
|
118
|
+
deciders get `FORBIDDEN`. Accepting an accepted
|
|
119
|
+
suggestion changes nothing. Editor roles come with `agentic-identity`.
|
|
120
|
+
- Two replicas can accept one suggestion before they meet. After the merge every replica keeps
|
|
121
|
+
one application and undoes the other, so the text and the edit log hold it once. An accept
|
|
122
|
+
beats a reject made at the same moment.
|
|
123
|
+
- Export to ODT (OpenDocument text): headings, paragraphs, bold, italic, strike, code, links, lists,
|
|
124
|
+
quotes, rules, tables, and images as embedded files. The package follows ODF 1.3 with the mimetype
|
|
125
|
+
stored first. The CLI and the API send the file bytes. LibreOffice opens it, lists keep their
|
|
126
|
+
numbers, and its own re-save reads back to the same Markdown (checked by hand with LibreOffice 24.8
|
|
127
|
+
in the PR that added it).
|
|
128
|
+
- Export to Markdown and to self-contained HTML. The HTML loads nothing from other hosts. An image
|
|
129
|
+
stored in the document as a `data:` URI (PNG, JPEG, GIF, or WebP) shows. Any other image becomes
|
|
130
|
+
its alt text, because a remote image tells its host who opened the file. `remoteImages` turns
|
|
131
|
+
that back on.
|
|
132
|
+
- Every edit, message, and suggestion needs an author. The edit log keeps the kind and the name, so
|
|
133
|
+
agent work stays labeled. The log holds no content. Over the CLI, MCP, and HTTP the server or
|
|
134
|
+
the operator sets the author. In a live room a client writes its own log entries, suggestion decisions, and
|
|
135
|
+
threads. A save merges them into the stored document. So after a live client saves, treat all three
|
|
136
|
+
as advisory. The room's own record of who sent updates in the room is at `GET /api/v1/docs/:id/room`. It does not cover
|
|
137
|
+
text written over HTTP, the CLI, or MCP before the room opened: the client that seeds the room appears there.
|
|
138
|
+
|
|
139
|
+
## Live editing
|
|
140
|
+
|
|
141
|
+
A person and an agent can edit one document at the same time. Each sees the other's changes. The Worker
|
|
142
|
+
hosts one room per document, on a Durable Object, through [agentic-realtime](https://www.npmjs.com/package/agentic-realtime).
|
|
143
|
+
Deploy `agentic-documents/live-worker`. The sample `wrangler.jsonc` binds `SYNC_ROOM` and sets the migration.
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { LiveDoc } from "agentic-documents/live";
|
|
147
|
+
|
|
148
|
+
const live = await LiveDoc.join({ server: "https://docs.example.com", id: "plan", token });
|
|
149
|
+
await live.edit((doc, author) => doc.append("Ship on Friday.", author));
|
|
150
|
+
console.log(live.peers); // the other people and agents. `machine` is true for an agent.
|
|
151
|
+
await live.save(); // write the room state to the stored document
|
|
152
|
+
await live.leave();
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The CLI and the MCP server join rooms too. Every command then runs in the room and saves:
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
export AGENTIC_DOCS_TOKEN=... # a bearer token. The server sets the author kind and the role.
|
|
159
|
+
export AGENTIC_DOCS_LIVE_URL=https://docs.example.com # or pass --live <url>
|
|
160
|
+
npx agentic-documents append plan --content "Ship on Friday." --live https://docs.example.com
|
|
161
|
+
npx agentic-documents mcp --live https://docs.example.com
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- The server reads your author kind and role from the token (`GET /api/v1/me`). An agent token writes
|
|
165
|
+
agent edits. `--as`, `--scopes`, and `--dir` do not apply with `--live`: the CLI refuses them and exits 2. This
|
|
166
|
+
holds when `AGENTIC_DOCS_LIVE_URL` is set too, so unset it for a local call.
|
|
167
|
+
- A room starts from the stored document. `save` merges the room back into the stored document, so
|
|
168
|
+
`read`, `export`, and the other commands see live edits. A save also creates a document that has none.
|
|
169
|
+
The CLI and MCP server save after each change. If a save fails, the room keeps the change and the error says so: do not repeat it. `new` makes the document in the store, and a room picks it up on the next join. A document over 1,000,000 bytes cannot open in a room or be saved from one. A save refuses an update with a wrong
|
|
170
|
+
kind of content or a malformed thread, suggestion, or log entry. HTTP edits reach an open room only when a client joins and seeds it.
|
|
171
|
+
- A `viewer` token joins and reads. The room drops its updates, and `edit` and `save` fail with `FORBIDDEN`.
|
|
172
|
+
Set a role in `AGENTIC_DOCS_TOKENS` with `{ "as": "human:Vic", "role": "viewer" }`. A token with no role is an editor.
|
|
173
|
+
- `GET /api/v1/docs/:id/room` lists who sent updates in the room, and whether each is machine-made.
|
|
174
|
+
- Pass `reconnect: {}` to `join` to keep trying after a drop. Edits made offline merge on return.
|
|
175
|
+
A room that restarts recovers its state from the Durable Object.
|
|
176
|
+
|
|
177
|
+
Limits of live editing:
|
|
178
|
+
|
|
179
|
+
- The edit log, the suggestion decisions, and the threads live in the document, and a client writes them.
|
|
180
|
+
A modified client can write a log entry with another author, or accept its own suggestion. The room's
|
|
181
|
+
list at `/room` is the record to trust for who sent updates. It does not cover text written before the
|
|
182
|
+
room opened.
|
|
183
|
+
- Until a client saves, a command over HTTP reads the stored copy, not the room.
|
|
184
|
+
- After the room refuses an edit, the local copy of `LiveDoc` still holds it. Join again.
|
|
185
|
+
- Durable Objects have no US jurisdiction. With the region `us` a room sits near the first client that opens it.
|
|
186
|
+
With `eu` it stays in the EU. The local `wrangler dev` cannot place rooms.
|
|
187
|
+
|
|
188
|
+
## Rules for text matching
|
|
189
|
+
|
|
190
|
+
- A match is exact and case sensitive.
|
|
191
|
+
- A match stays inside one block. It cannot cross a paragraph, a cell, or a hard break.
|
|
192
|
+
- More than one match is an error, and matches may overlap: `ana` matches twice in `banana`. Pass `occurrence` (1 is the first start) to choose.
|
|
193
|
+
- A replacement takes the marks of the first matched character.
|
|
194
|
+
|
|
195
|
+
## Limits
|
|
196
|
+
|
|
197
|
+
- No web UI yet.
|
|
198
|
+
- The Worker keeps each document in R2 and a catalog (who made it, and when) in D1. Both come from the
|
|
199
|
+
shared `agentic-identity` ports. A save is conditional on the version it read, so two writers cannot
|
|
200
|
+
overwrite each other. The CLI and MCP server keep the same data in local files.
|
|
201
|
+
- The CLI and MCP server keep files under `<dir>/blobs` and `<dir>/store`. Files from an older release that sit
|
|
202
|
+
directly in `<dir>` are not read.
|
|
203
|
+
- Token auth is a stopgap until the `Auth` port exists. A token has one role for the whole server: `editor` (the default) or `viewer`. A viewer cannot call a write command or write in a live room. There is no per-document sharing yet.
|
|
204
|
+
- A save is a compare-and-set. The file store takes a lock file, checks the version, and renames a
|
|
205
|
+
uniquely named temp file into place. A writer that loses loads the document again and runs
|
|
206
|
+
its change again. A lock older than 10 seconds counts as dead and is taken over. A lock that
|
|
207
|
+
cannot be read, or that is dated in the future, counts as live. A writer that waits more than
|
|
208
|
+
5 seconds fails with `LOCKED`. A writer that loses 64 times in a row fails with `CONFLICT`. The lock
|
|
209
|
+
is for one machine. Two machines on a network file system need a store with its own
|
|
210
|
+
conditional write.
|
|
211
|
+
- `--as` and `AGENTIC_DOCS_AS` are self-declared labels. The CLI does not prove who runs it, so
|
|
212
|
+
`--as human:Ada` labels work as human on any machine that can run the command. Only the HTTP API
|
|
213
|
+
(tokens) and an MCP server that a trusted host starts fix the author for the caller.
|
|
214
|
+
- Suggestions work on text, not on whole sections.
|
|
215
|
+
- A scope on a principal is trusted as given. Nothing here proves who a caller is.
|
|
216
|
+
- Export covers Markdown, HTML, and ODT. DOCX export, and ODT and DOCX import, are planned.
|
|
217
|
+
- ODT export: comments and suggestions are not written. A code block loses its language. A table
|
|
218
|
+
cell span is not written (the model has none yet). A task list becomes a bullet list that starts each
|
|
219
|
+
item with `☐` or `☑`. A remote image is not linked, because an office program would ask its host
|
|
220
|
+
for it when the file opens. It becomes its alt text.
|
|
221
|
+
- Markdown with footnotes, or any syntax the model has no place for, is refused with
|
|
222
|
+
`INVALID_INPUT`. Nothing is stored. Reference links and image references are resolved to
|
|
223
|
+
their URL. An unused reference definition is dropped.
|
|
224
|
+
- Markdown import drops link titles, table column alignment, and the part of a code fence
|
|
225
|
+
line after the language.
|
|
226
|
+
- Markdown output trims whitespace at the edge of a block and skips empty paragraphs. A text
|
|
227
|
+
target that includes a trailing space can match in the document and not in `read` output.
|
|
228
|
+
|
|
229
|
+
## Develop
|
|
230
|
+
|
|
231
|
+
```sh
|
|
232
|
+
npm ci
|
|
233
|
+
npm run ci:local
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
MIT.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Report a vulnerability
|
|
4
|
+
|
|
5
|
+
Do not open a public issue for a security problem. Report it in private with
|
|
6
|
+
GitHub security advisories:
|
|
7
|
+
https://github.com/pooriaarab/agentic-documents/security/advisories/new
|
|
8
|
+
|
|
9
|
+
Include the version (`npm ls agentic-documents`), the steps to reproduce, and the
|
|
10
|
+
impact you expect. We aim to reply within 7 days.
|
|
11
|
+
|
|
12
|
+
## Supported versions
|
|
13
|
+
|
|
14
|
+
Only the latest published version gets fixes.
|
|
15
|
+
|
|
16
|
+
## Scope
|
|
17
|
+
|
|
18
|
+
In scope: the package code (the SDK, CLI, MCP server, and Worker API) and the
|
|
19
|
+
export formats it writes.
|
|
20
|
+
|
|
21
|
+
Out of scope: a deployment that sets its own tokens or region without following
|
|
22
|
+
the README, and a self-declared `--as` label on the CLI. The README says that
|
|
23
|
+
label is not proof of identity.
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agentic-documents
|
|
3
|
+
description: Read and edit rich-text documents by section or exact text, as Markdown, with every edit labeled by author. Keywords: documents,editor,rich-text,tiptap,yjs,agents.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# agentic-documents
|
|
7
|
+
|
|
8
|
+
Write documents with people and agents in real time.
|
|
9
|
+
|
|
10
|
+
## Use the SDK
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { AgenticDoc } from "agentic-documents";
|
|
14
|
+
|
|
15
|
+
const me = { kind: "agent_of_human", name: "scribe" } as const;
|
|
16
|
+
const doc = AgenticDoc.fromMarkdown("# Plan\n\nShip on Friday.", me);
|
|
17
|
+
doc.replace({ text: "Friday" }, "Monday", me);
|
|
18
|
+
doc.toMarkdown();
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Use the CLI or MCP
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
agentic-documents outline plan --as agent_of_human:scribe
|
|
25
|
+
agentic-documents mcp --as agent_of_human:scribe
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Tools are named `doc_<command>`: `doc_read`, `doc_outline`, `doc_replace`, `doc_suggest`.
|
|
29
|
+
|
|
30
|
+
## Rules
|
|
31
|
+
|
|
32
|
+
- Pass your author kind and name on every change.
|
|
33
|
+
- Read the outline, then the section, before you edit.
|
|
34
|
+
- On an ambiguous match, pass `occurrence`.
|
|
35
|
+
- Report errors as failures.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AgenticDoc,
|
|
3
|
+
DocError,
|
|
4
|
+
PRINCIPAL_KINDS,
|
|
5
|
+
Suggestions,
|
|
6
|
+
Threads
|
|
7
|
+
} from "./chunk-YBBQNCMC.js";
|
|
8
|
+
|
|
9
|
+
// src/service.ts
|
|
10
|
+
import * as Y from "yjs";
|
|
11
|
+
var ATTEMPTS = 64;
|
|
12
|
+
var ID = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
13
|
+
var CATALOG = "documents";
|
|
14
|
+
var MAX_LIVE_BYTES = 1e6;
|
|
15
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
16
|
+
var isString = (v) => typeof v === "string";
|
|
17
|
+
var isWho = (v) => isObject(v) && PRINCIPAL_KINDS.includes(v.kind) && isString(v.name) && v.name !== "";
|
|
18
|
+
var isAnchor = (v) => isObject(v) && isString(v.text) && isObject(v.start) && isObject(v.end);
|
|
19
|
+
var ROOTS = /* @__PURE__ */ new Set(["default", "edits", "suggestions", "threads", "applied"]);
|
|
20
|
+
var DocsService = class {
|
|
21
|
+
constructor(ports, options = {}) {
|
|
22
|
+
this.options = options;
|
|
23
|
+
this.blobs = ports.blobs;
|
|
24
|
+
this.store = ports.store;
|
|
25
|
+
}
|
|
26
|
+
options;
|
|
27
|
+
blobs;
|
|
28
|
+
store;
|
|
29
|
+
key(id) {
|
|
30
|
+
if (!ID.test(id)) throw new DocError("INVALID_INPUT", `invalid document id "${id}": use 1 to 64 of a-z, 0-9, "-" and "_"`);
|
|
31
|
+
return `${id}.ydoc`;
|
|
32
|
+
}
|
|
33
|
+
async create(id, markdown, author) {
|
|
34
|
+
const key = this.key(id);
|
|
35
|
+
const created = await this.blobs.put(key, AgenticDoc.fromMarkdown(markdown, author, this.options).encode(), { ifNoneMatch: "*" });
|
|
36
|
+
if (!created) throw new DocError("DOCUMENT_EXISTS", `document "${id}" already exists`);
|
|
37
|
+
await this.catalog(id, author);
|
|
38
|
+
}
|
|
39
|
+
/** The stored document as a Yjs update. A live client seeds its room from it. */
|
|
40
|
+
async state(id) {
|
|
41
|
+
const found = await this.blobs.get(this.key(id));
|
|
42
|
+
if (!found) throw new DocError("NO_DOCUMENT", `no document with id "${id}"`);
|
|
43
|
+
return found.data;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Merge a Yjs update from a live room into the stored document, and create the document when it
|
|
47
|
+
* has none. A merge is safe to repeat and to run in any order. The save is conditional, so two
|
|
48
|
+
* checkpoints at once both land.
|
|
49
|
+
*/
|
|
50
|
+
async checkpoint(id, update, author) {
|
|
51
|
+
const key = this.key(id);
|
|
52
|
+
const scratch = new Y.Doc();
|
|
53
|
+
try {
|
|
54
|
+
Y.applyUpdate(scratch, update);
|
|
55
|
+
} catch {
|
|
56
|
+
throw new DocError("INVALID_INPUT", "the update is not a Yjs update");
|
|
57
|
+
}
|
|
58
|
+
if (scratch.store.clients.size === 0 || scratch.store.pendingStructs || scratch.store.pendingDs) {
|
|
59
|
+
throw new DocError("INVALID_INPUT", "the update is empty or lacks the changes it depends on");
|
|
60
|
+
}
|
|
61
|
+
for (const root of scratch.share.keys()) if (!ROOTS.has(root)) throw new DocError("INVALID_INPUT", `the update holds a part this package does not use: "${root}"`);
|
|
62
|
+
for (let attempt = 0; attempt < ATTEMPTS; attempt++) {
|
|
63
|
+
const found = await this.blobs.get(key);
|
|
64
|
+
if (!found) {
|
|
65
|
+
const created = AgenticDoc.fromUpdate(update, this.options);
|
|
66
|
+
const fresh = this.checked(id, created);
|
|
67
|
+
this.fits(id, fresh);
|
|
68
|
+
if (!await this.blobs.put(key, fresh, { ifNoneMatch: "*" })) continue;
|
|
69
|
+
await this.catalog(id, author);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const doc = AgenticDoc.fromUpdate(found.data, this.options);
|
|
73
|
+
doc.applyUpdate(update);
|
|
74
|
+
const merged = this.checked(id, doc);
|
|
75
|
+
this.fits(id, merged);
|
|
76
|
+
if (await this.blobs.put(key, merged, { ifMatch: found.version })) return;
|
|
77
|
+
}
|
|
78
|
+
throw new DocError("CONFLICT", `document "${id}" kept changing; try again`);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The encoded document, once it reads back and every stored entry has the shape the commands rely on. Yjs does not
|
|
82
|
+
* check types, so an update can put text where the editor expects blocks, or a thread with no messages, and then a
|
|
83
|
+
* command fails on every later call.
|
|
84
|
+
*/
|
|
85
|
+
checked(id, doc) {
|
|
86
|
+
const refuse = (what) => new DocError("INVALID_INPUT", `the update would leave document "${id}" unreadable: ${what}`);
|
|
87
|
+
try {
|
|
88
|
+
doc.toMarkdown();
|
|
89
|
+
if (!doc.edits().every((e) => isObject(e) && isWho(e.author) && isString(e.at) && isString(e.op))) throw refuse("an edit log entry is malformed");
|
|
90
|
+
for (const [, t] of doc.ydoc.getMap("threads")) {
|
|
91
|
+
const messages = t instanceof Y.Map ? t.get("messages") : void 0;
|
|
92
|
+
const ok = t instanceof Y.Map && isAnchor(t.get("anchor")) && ["open", "resolved"].includes(t.get("status")) && messages instanceof Y.Array && messages.length > 0 && messages.toArray().every((m) => isObject(m) && isString(m.id) && isString(m.at) && isString(m.body) && isWho(m.author));
|
|
93
|
+
if (!ok) throw refuse("a thread is malformed");
|
|
94
|
+
}
|
|
95
|
+
for (const [, f] of doc.ydoc.getMap("suggestions")) {
|
|
96
|
+
const ok = f instanceof Y.Map && ["insert", "replace", "delete"].includes(f.get("kind")) && ["pending", "accepted", "rejected"].includes(f.get("status")) && isAnchor(f.get("anchor")) && isString(f.get("replacement")) && isString(f.get("createdAt")) && isWho(f.get("author"));
|
|
97
|
+
if (!ok) throw refuse("a suggestion is malformed");
|
|
98
|
+
}
|
|
99
|
+
new Suggestions(doc).list();
|
|
100
|
+
new Threads(doc).list();
|
|
101
|
+
} catch (error) {
|
|
102
|
+
throw error instanceof DocError ? error : refuse("a part holds the wrong kind of content");
|
|
103
|
+
}
|
|
104
|
+
return doc.encode();
|
|
105
|
+
}
|
|
106
|
+
fits(id, state) {
|
|
107
|
+
if (state.length > MAX_LIVE_BYTES) throw new DocError("INVALID_INPUT", `document "${id}" would pass ${MAX_LIVE_BYTES} bytes, the most a live room takes`);
|
|
108
|
+
}
|
|
109
|
+
async catalog(id, author) {
|
|
110
|
+
await this.store.transaction(async (tx) => {
|
|
111
|
+
if (await tx.get(CATALOG, id) === null) await tx.put(CATALOG, id, { createdBy: { kind: author.kind, name: author.name }, createdAt: Date.now() });
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/** Who created the document and when, or null when it has no catalog entry (made before the catalog, or a failed catalog write). */
|
|
115
|
+
async info(id) {
|
|
116
|
+
this.key(id);
|
|
117
|
+
const entry = await this.store.get(CATALOG, id);
|
|
118
|
+
if (entry === null) return null;
|
|
119
|
+
const { createdBy, createdAt } = entry;
|
|
120
|
+
if (!createdBy || !PRINCIPAL_KINDS.includes(createdBy.kind) || typeof createdBy.name !== "string" || typeof createdAt !== "number") {
|
|
121
|
+
throw new DocError("INVALID_INPUT", `the catalog entry for "${id}" is damaged`);
|
|
122
|
+
}
|
|
123
|
+
return { createdBy, createdAt };
|
|
124
|
+
}
|
|
125
|
+
async read(id, run) {
|
|
126
|
+
const found = await this.blobs.get(this.key(id));
|
|
127
|
+
if (!found) throw new DocError("NO_DOCUMENT", `no document with id "${id}"`);
|
|
128
|
+
return run(AgenticDoc.fromUpdate(found.data, this.options));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Load, run the change, and save only if nobody saved in between. When someone did, load again and
|
|
132
|
+
* run the change again, so a check such as "is it still pending" sees the new state.
|
|
133
|
+
*/
|
|
134
|
+
async write(id, run) {
|
|
135
|
+
const key = this.key(id);
|
|
136
|
+
for (let attempt = 0; attempt < ATTEMPTS; attempt++) {
|
|
137
|
+
const found = await this.blobs.get(key);
|
|
138
|
+
if (!found) throw new DocError("NO_DOCUMENT", `no document with id "${id}"`);
|
|
139
|
+
const doc = AgenticDoc.fromUpdate(found.data, this.options);
|
|
140
|
+
const result = run(doc);
|
|
141
|
+
if (await this.blobs.put(key, doc.encode(), { ifMatch: found.version })) return result;
|
|
142
|
+
}
|
|
143
|
+
throw new DocError("CONFLICT", `document "${id}" kept changing; try again`);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export {
|
|
148
|
+
MAX_LIVE_BYTES,
|
|
149
|
+
DocsService
|
|
150
|
+
};
|