agentic-documents 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,159 @@
1
+ import * as Y from 'yjs';
2
+ import { Blobs, Store } from 'agentic-identity';
3
+
4
+ /** A ProseMirror node in JSON form, the shape y-prosemirror reads and writes. */
5
+ interface PmNode {
6
+ type: string;
7
+ attrs?: Record<string, unknown>;
8
+ content?: PmNode[];
9
+ text?: string;
10
+ marks?: {
11
+ type: string;
12
+ attrs?: Record<string, unknown>;
13
+ }[];
14
+ }
15
+ /** Parse GitHub-flavored Markdown into top-level ProseMirror blocks. */
16
+ declare function markdownToBlocks(markdown: string): PmNode[];
17
+ /** Serialize top-level ProseMirror blocks to GitHub-flavored Markdown. */
18
+ declare function blocksToMarkdown(nodes: PmNode[]): string;
19
+
20
+ declare const PRINCIPAL_KINDS: readonly ["human", "agent_of_human", "agent_of_agent", "agent", "user_token"];
21
+ type PrincipalKind = (typeof PRINCIPAL_KINDS)[number];
22
+ /** Who made a change. Every mutation needs one, so machine-made work stays labeled. */
23
+ interface Principal {
24
+ kind: PrincipalKind;
25
+ name: string;
26
+ /** Rights this principal was granted, such as `docs:accept`. Used to decide, never stored in the doc. */
27
+ scopes?: string[];
28
+ }
29
+ /** A text range that follows its text through concurrent edits. */
30
+ interface Anchor {
31
+ text: string;
32
+ start: Record<string, unknown>;
33
+ end: Record<string, unknown>;
34
+ }
35
+ type TextTarget = {
36
+ text: string;
37
+ occurrence?: number;
38
+ at?: "before" | "after";
39
+ };
40
+ type AnchorTarget = {
41
+ anchor: Anchor;
42
+ at?: "before" | "after";
43
+ };
44
+ type SectionTarget = {
45
+ section: string;
46
+ at?: "start" | "end";
47
+ };
48
+ type Target = TextTarget | AnchorTarget | SectionTarget;
49
+ interface Section {
50
+ id: string;
51
+ level: number;
52
+ title: string;
53
+ }
54
+ interface EditEntry {
55
+ op: "create" | "append" | "insert" | "replace" | "delete";
56
+ at: string;
57
+ author: Principal;
58
+ /** Counts of characters (text targets) or blocks (section targets). Never content. */
59
+ inserted: number;
60
+ deleted: number;
61
+ section?: string;
62
+ /** Set when an accepted suggestion caused the edit. `author` is who suggested it. */
63
+ suggestion?: {
64
+ id: string;
65
+ author: Principal;
66
+ };
67
+ }
68
+ /** When the edit was logged, and where it put new text (relative positions that follow the text). */
69
+ interface EditResult {
70
+ at: string;
71
+ inserted?: {
72
+ start: Record<string, unknown>;
73
+ end: Record<string, unknown>;
74
+ };
75
+ }
76
+ interface DocOptions {
77
+ now?: () => Date;
78
+ }
79
+ type Range = {
80
+ unit: Y.XmlText;
81
+ start: number;
82
+ end: number;
83
+ };
84
+ /** `kind:name`, for example `agent_of_human:scribe`. Validation happens when a change needs it. */
85
+ declare function parseAuthor(value: string | undefined): Principal | undefined;
86
+ declare class AgenticDoc {
87
+ readonly ydoc: Y.Doc;
88
+ readonly now: () => Date;
89
+ constructor(ydoc?: Y.Doc, options?: DocOptions);
90
+ static fromMarkdown(markdown: string, author: Principal, options?: DocOptions): AgenticDoc;
91
+ static fromUpdate(update: Uint8Array, options?: DocOptions): AgenticDoc;
92
+ encode(): Uint8Array;
93
+ applyUpdate(update: Uint8Array): void;
94
+ /** A new replica with its own client id, holding the same state. */
95
+ fork(): AgenticDoc;
96
+ private get fragment();
97
+ private get log();
98
+ edits(): EditEntry[];
99
+ private change;
100
+ toJSON(): PmNode;
101
+ toMarkdown(): string;
102
+ /** Sections in order. Blocks before the first heading come first, as the `preamble` section at level 0. */
103
+ outline(): Section[];
104
+ readSection(id: string): string;
105
+ private headings;
106
+ /**
107
+ * A section runs from its heading to the next heading of the same or a higher level. The preamble
108
+ * has no heading and runs to the first heading. It is always a valid target, even when empty.
109
+ */
110
+ private range;
111
+ private find;
112
+ private pick;
113
+ /** Pin a text range so later edits by other replicas do not move it. */
114
+ anchor(text: string, occurrence?: number): Anchor;
115
+ resolve(target: TextTarget | AnchorTarget): Range;
116
+ edit(op: "insert" | "replace" | "delete", target: TextTarget | AnchorTarget, content: string, author: Principal, extra?: Pick<EditEntry, "suggestion">): EditResult;
117
+ private editSection;
118
+ /** Insert Markdown into a section, or plain text next to matched text. */
119
+ insert(target: Target, content: string, author: Principal): void;
120
+ /** Replace a section body (the heading stays), or the matched text. */
121
+ replace(target: Target, content: string, author: Principal): void;
122
+ /** Delete a whole section with its subsections, or the matched text. */
123
+ delete(target: Target, author: Principal): void;
124
+ append(markdown: string, author: Principal): void;
125
+ }
126
+
127
+ /** What the catalog keeps for each document. The author kind stays, so machine-made work stays labeled. */
128
+ interface DocInfo {
129
+ createdBy: {
130
+ kind: PrincipalKind;
131
+ name: string;
132
+ };
133
+ createdAt: number;
134
+ }
135
+ interface DocsPorts {
136
+ /** Holds each document as a Yjs update. Saves are conditional on the version that was read. */
137
+ blobs: Blobs;
138
+ /** Holds the catalog: who created each document, and when. */
139
+ store: Store;
140
+ }
141
+ /** Loads and saves documents as Yjs updates in a Blobs port. The CLI, MCP server, and API share it. */
142
+ declare class DocsService {
143
+ private readonly options;
144
+ private readonly blobs;
145
+ private readonly store;
146
+ constructor(ports: DocsPorts, options?: DocOptions);
147
+ private key;
148
+ create(id: string, markdown: string, author: Principal): Promise<void>;
149
+ /** Who created the document and when, or null when it has no catalog entry (made before the catalog, or a failed catalog write). */
150
+ info(id: string): Promise<DocInfo | null>;
151
+ read<T>(id: string, run: (doc: AgenticDoc) => T): Promise<T>;
152
+ /**
153
+ * Load, run the change, and save only if nobody saved in between. When someone did, load again and
154
+ * run the change again, so a check such as "is it still pending" sees the new state.
155
+ */
156
+ write<T>(id: string, run: (doc: AgenticDoc) => T): Promise<T>;
157
+ }
158
+
159
+ export { type Anchor as A, DocsService as D, type EditEntry as E, type Principal as P, type Section as S, type TextTarget as T, AgenticDoc as a, type AnchorTarget as b, type PmNode as c, type DocInfo as d, type DocOptions as e, type DocsPorts as f, PRINCIPAL_KINDS as g, type PrincipalKind as h, type SectionTarget as i, type Target as j, blocksToMarkdown as k, markdownToBlocks as m, parseAuthor as p };
@@ -0,0 +1,37 @@
1
+ import { Hono } from 'hono';
2
+ import { D1Like, R2Like } from 'agentic-identity/cloudflare';
3
+ import { D as DocsService, P as Principal } from './service-eYISo6-b.js';
4
+ import 'yjs';
5
+ import 'agentic-identity';
6
+
7
+ interface WorkerEnv {
8
+ /**
9
+ * Where the data lives: "eu" or "us". It is a claim you make at deploy time, with no default.
10
+ * "unknown" skips the region check. Local D1 reports no region, so "unknown" needs `AGENTIC_DOCS_UNSAFE_REGION=1` too.
11
+ */
12
+ AGENTIC_DOCS_REGION?: string;
13
+ /** Set to "1" to accept the region "unknown" under `wrangler dev`. Never set it in production. */
14
+ AGENTIC_DOCS_UNSAFE_REGION?: string;
15
+ /** D1 database: the catalog of documents. Create it with `wrangler d1 create agentic-documents --location weur`. */
16
+ DB?: D1Like;
17
+ /** R2 bucket: each document as a Yjs update. Create it with `--jurisdiction eu`. */
18
+ DOCS?: R2Like;
19
+ /**
20
+ * JSON object: bearer token (32 bytes or more) to `kind:name`, or to `{ "as": "kind:name", "scopes": ["docs:accept"] }`.
21
+ * Set it as a secret. A short token or a bad entry refuses the whole config.
22
+ */
23
+ AGENTIC_DOCS_TOKENS?: string;
24
+ }
25
+ /** A service, or a function that makes one from the Worker bindings. */
26
+ type ServiceSource = DocsService | ((env: WorkerEnv) => Promise<DocsService>);
27
+ declare function createApp(source: ServiceSource): Hono<{
28
+ Bindings: WorkerEnv;
29
+ Variables: {
30
+ author: Principal;
31
+ };
32
+ }>;
33
+ declare const _default: {
34
+ fetch: (request: Request, env: WorkerEnv, ctx: unknown) => Response | Promise<Response>;
35
+ };
36
+
37
+ export { type ServiceSource, type WorkerEnv, createApp, _default as default };
package/dist/worker.js ADDED
@@ -0,0 +1,162 @@
1
+ import {
2
+ DocsService
3
+ } from "./chunk-LRPLNXOF.js";
4
+ import {
5
+ DocError,
6
+ PRINCIPAL_KINDS,
7
+ commands,
8
+ parseAuthor
9
+ } from "./chunk-4YVWD5PH.js";
10
+
11
+ // src/worker.ts
12
+ import { Hono } from "hono";
13
+ import { assertRegion, parseRegion } from "agentic-identity";
14
+ import { D1_SCHEMA, createD1Store, createR2Blobs } from "agentic-identity/cloudflare";
15
+ var STATUS = {
16
+ NO_MATCH: 404,
17
+ NO_SECTION: 404,
18
+ NO_THREAD: 404,
19
+ NO_SUGGESTION: 404,
20
+ NO_DOCUMENT: 404,
21
+ AMBIGUOUS_MATCH: 409,
22
+ STALE_ANCHOR: 409,
23
+ ALREADY_DECIDED: 409,
24
+ DOCUMENT_EXISTS: 409,
25
+ CONFLICT: 409,
26
+ FORBIDDEN: 403,
27
+ LOCKED: 503,
28
+ MISCONFIGURED: 500
29
+ };
30
+ function same(a, b) {
31
+ let diff = a.length ^ b.length;
32
+ for (let i = 0; i < Math.max(a.length, b.length); i++) diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
33
+ return diff === 0;
34
+ }
35
+ var MIN_TOKEN_BYTES = 32;
36
+ var warned = /* @__PURE__ */ new Set();
37
+ function refuse(reason) {
38
+ if (!warned.has(reason)) {
39
+ warned.add(reason);
40
+ console.error(`AGENTIC_DOCS_TOKENS is refused: ${reason}`);
41
+ }
42
+ return void 0;
43
+ }
44
+ function principalOf(entry) {
45
+ const object = typeof entry === "object" && entry !== null && !Array.isArray(entry) ? entry : void 0;
46
+ const who = typeof entry === "string" ? entry : object?.as;
47
+ const author = typeof who === "string" ? parseAuthor(who) : void 0;
48
+ if (!author || !PRINCIPAL_KINDS.includes(author.kind) || !author.name) return void 0;
49
+ if (object?.scopes === void 0) return author;
50
+ const scopes = object.scopes;
51
+ return Array.isArray(scopes) && scopes.every((s) => typeof s === "string" && s !== "") ? { ...author, scopes } : void 0;
52
+ }
53
+ function readTokens(raw) {
54
+ let table;
55
+ try {
56
+ table = JSON.parse(raw ?? "{}");
57
+ } catch {
58
+ return refuse("not valid JSON");
59
+ }
60
+ if (typeof table !== "object" || table === null || Array.isArray(table)) return refuse("not a JSON object");
61
+ const found = /* @__PURE__ */ new Map();
62
+ for (const [token, entry] of Object.entries(table)) {
63
+ if (new TextEncoder().encode(token).length < MIN_TOKEN_BYTES) return refuse(`a token is shorter than ${MIN_TOKEN_BYTES} bytes`);
64
+ const principal = principalOf(entry);
65
+ if (!principal) return refuse("an entry is not a valid kind:name or { as, scopes }");
66
+ found.set(token, principal);
67
+ }
68
+ return found;
69
+ }
70
+ function principalFor(tokens, bearer) {
71
+ let found;
72
+ for (const [token, principal] of readTokens(tokens) ?? []) if (same(token, bearer)) found = principal;
73
+ return found;
74
+ }
75
+ var fail = (code, message, status, matches = []) => Response.json({ error: { code, message, matches } }, { status });
76
+ function createApp(source) {
77
+ const app2 = new Hono();
78
+ app2.use("*", async (c, next) => {
79
+ await next();
80
+ c.header("x-content-type-options", "nosniff");
81
+ });
82
+ app2.get("/health", (c) => c.json({ ok: true, pack: "agentic-documents" }));
83
+ app2.use("/api/v1/*", async (c, next) => {
84
+ const bearer = /^Bearer (.+)$/.exec(c.req.header("authorization") ?? "")?.[1];
85
+ const author = bearer === void 0 ? void 0 : principalFor(c.env?.AGENTIC_DOCS_TOKENS, bearer);
86
+ if (!author) return fail("UNAUTHENTICATED", "send a valid bearer token", 401);
87
+ c.set("author", author);
88
+ await next();
89
+ });
90
+ async function run(c, command, input) {
91
+ try {
92
+ const result = await command.run(await (typeof source === "function" ? source(c.env ?? {}) : source), input, c.get("author"));
93
+ if (command.name === "export") {
94
+ const { data, contentType, encoding, format } = result;
95
+ const images = input.remoteImages === true || input.remoteImages === "true" ? "data: https:" : "data:";
96
+ const disposition = format === "odt" ? { "content-disposition": `attachment; filename="${input.id}.odt"` } : {};
97
+ const headers = { ...disposition, "content-type": contentType, "content-security-policy": `default-src 'none'; style-src 'unsafe-inline'; img-src ${images}; sandbox` };
98
+ return encoding === "base64" ? c.body(Uint8Array.from(atob(data), (ch) => ch.charCodeAt(0)), 200, headers) : c.body(data, 200, headers);
99
+ }
100
+ if (typeof result === "string") return c.body(result, 200, { "content-type": "text/markdown; charset=utf-8" });
101
+ return c.json(result);
102
+ } catch (error) {
103
+ if (!(error instanceof DocError)) throw error;
104
+ return fail(error.code, error.message, STATUS[error.code] ?? 400, error.matches);
105
+ }
106
+ }
107
+ async function body(c) {
108
+ const parsed = await c.req.json().catch(() => void 0);
109
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
110
+ throw new DocError("INVALID_INPUT", "the body must be a JSON object");
111
+ }
112
+ return parsed;
113
+ }
114
+ const create = commands.find((c) => c.name === "new");
115
+ app2.post("/api/v1/docs", async (c) => run(c, create, await body(c)));
116
+ for (const command of commands.filter((c) => c !== create)) {
117
+ const path = `/api/v1/docs/:id/${command.name}`;
118
+ if (command.readOnly) app2.get(path, (c) => run(c, command, { ...c.req.query(), id: c.req.param("id") }));
119
+ else app2.post(path, async (c) => run(c, command, { ...await body(c), id: c.req.param("id") }));
120
+ }
121
+ app2.notFound(() => fail("NOT_FOUND", "no such route", 404));
122
+ app2.onError((error) => {
123
+ if (error instanceof DocError) return fail(error.code, error.message, STATUS[error.code] ?? 400, error.matches);
124
+ console.error(error instanceof Error ? error.name : "error");
125
+ return fail("INTERNAL", "the server could not finish the request", 500);
126
+ });
127
+ return app2;
128
+ }
129
+ var schemas = /* @__PURE__ */ new WeakMap();
130
+ var urlSecret;
131
+ async function serviceFor(env) {
132
+ if (!env.DB || !env.DOCS) throw new DocError("MISCONFIGURED", "bind a D1 database as DB and an R2 bucket as DOCS");
133
+ let region;
134
+ try {
135
+ region = parseRegion(env.AGENTIC_DOCS_REGION);
136
+ } catch {
137
+ throw new DocError("MISCONFIGURED", 'set AGENTIC_DOCS_REGION to "eu" or "us" ("unknown" only for local development)');
138
+ }
139
+ if (region === "unknown" && env.AGENTIC_DOCS_UNSAFE_REGION !== "1") {
140
+ throw new DocError("MISCONFIGURED", 'the region "unknown" turns the residency check off; set AGENTIC_DOCS_UNSAFE_REGION=1 to accept that for local development');
141
+ }
142
+ const db = env.DB;
143
+ let ready = schemas.get(db);
144
+ if (!ready) {
145
+ ready = db.prepare(D1_SCHEMA).run().catch((error) => {
146
+ schemas.delete(db);
147
+ throw error;
148
+ });
149
+ schemas.set(db, ready);
150
+ }
151
+ await ready;
152
+ const store = createD1Store(db, { region });
153
+ const blobs = createR2Blobs(env.DOCS, { region, urlBase: "https://invalid.example/unused", urlSecret: urlSecret ??= `${crypto.randomUUID()}${crypto.randomUUID()}` });
154
+ assertRegion(region, store, blobs);
155
+ return new DocsService({ store, blobs });
156
+ }
157
+ var app = createApp(serviceFor);
158
+ var worker_default = { fetch: (request, env, ctx) => app.fetch(request, env, ctx) };
159
+ export {
160
+ createApp,
161
+ worker_default as default
162
+ };
package/llms.txt ADDED
@@ -0,0 +1,15 @@
1
+ # agentic-documents
2
+
3
+ Write documents with people and agents in real time.
4
+
5
+ Keywords: documents, editor, rich-text, tiptap, yjs, collaboration, agents.
6
+
7
+ - SDK: import { AgenticDoc } from "agentic-documents".
8
+ - Read as Markdown, read an outline, edit by section id or exact text.
9
+ - CLI: npx agentic-documents <command> <id>. MCP: npx agentic-documents mcp --as <kind>:<name>.
10
+ - Every edit carries an author kind and name.
11
+
12
+ ## Docs
13
+
14
+ - README: https://github.com/pooriaarab/agentic-documents
15
+ - License: MIT
package/package.json CHANGED
@@ -1,6 +1,95 @@
1
1
  {
2
2
  "name": "agentic-documents",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Write documents with people and agents in real time.",
5
+ "keywords": [
6
+ "documents",
7
+ "editor",
8
+ "rich-text",
9
+ "tiptap",
10
+ "collaboration",
11
+ "agents",
12
+ "mcp",
13
+ "cli",
14
+ "cloudflare"
15
+ ],
16
+ "license": "MIT",
17
+ "author": "Pooria Arab",
18
+ "homepage": "https://github.com/pooriaarab/agentic-documents#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/pooriaarab/agentic-documents/issues"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/pooriaarab/agentic-documents.git"
25
+ },
26
+ "type": "module",
27
+ "private": false,
28
+ "engines": {
29
+ "node": ">=22"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "SECURITY.md",
34
+ "SKILL.md",
35
+ "llms.txt"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "mcpName": "io.github.pooriaarab/agentic-documents",
41
+ "main": "./dist/index.js",
42
+ "types": "./dist/index.d.ts",
43
+ "exports": {
44
+ ".": {
45
+ "types": "./dist/index.d.ts",
46
+ "import": "./dist/index.js"
47
+ },
48
+ "./worker": {
49
+ "types": "./dist/worker.d.ts",
50
+ "import": "./dist/worker.js"
51
+ }
52
+ },
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "typecheck": "tsc --noEmit",
56
+ "test": "vitest run",
57
+ "clean": "rm -rf dist",
58
+ "ci:local": "npm run build && npm run typecheck && npm test"
59
+ },
60
+ "dependencies": {
61
+ "@modelcontextprotocol/sdk": "^1.12.0",
62
+ "agentic-identity": "^0.2.0",
63
+ "fflate": "^0.8.3",
64
+ "hono": "^4.13.14",
65
+ "lib0": "^0.2.119",
66
+ "mdast-util-from-markdown": "^2.1.0",
67
+ "mdast-util-gfm": "^3.1.0",
68
+ "mdast-util-to-markdown": "^2.2.1",
69
+ "micromark-extension-gfm": "^3.0.0",
70
+ "prosemirror-model": "^1.25.12",
71
+ "prosemirror-state": "^1.4.4",
72
+ "prosemirror-view": "^1.42.8",
73
+ "y-prosemirror": "^1.3.7",
74
+ "y-protocols": "^1.0.7",
75
+ "yjs": "^13.6.33",
76
+ "zod": "^3.25.0"
77
+ },
78
+ "devDependencies": {
79
+ "@tiptap/core": "^3.32.1",
80
+ "@tiptap/extension-image": "^3.32.1",
81
+ "@tiptap/extension-list": "^3.32.1",
82
+ "@tiptap/extension-table": "^3.32.1",
83
+ "@tiptap/pm": "^3.32.1",
84
+ "@tiptap/starter-kit": "^3.32.1",
85
+ "@types/mdast": "^4.0.4",
86
+ "@types/node": "^22.0.0",
87
+ "tsup": "^8.0.0",
88
+ "typescript": "^5.6.0",
89
+ "vitest": "^3.0.0",
90
+ "wrangler": "^4.155.0"
91
+ },
92
+ "bin": {
93
+ "agentic-documents": "./dist/cli.js"
94
+ }
95
+ }