agentic-documents 0.3.0 → 0.5.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 +48 -0
- package/README.md +143 -5
- package/dist/{chunk-YBBQNCMC.js → chunk-6QOYS63L.js} +14 -189
- package/dist/{chunk-TFCRPEEM.js → chunk-7R7ZPPRF.js} +1 -1
- package/dist/{chunk-R5XUTBC3.js → chunk-E7B6T5WN.js} +29 -8
- package/dist/{chunk-IU3YIQ4Y.js → chunk-FX755I7V.js} +30 -8
- package/dist/{chunk-GYQFTF2E.js → chunk-L6W2AQUF.js} +15 -146
- package/dist/chunk-S6MQDWFK.js +150 -0
- package/dist/chunk-UPEGODUV.js +195 -0
- package/dist/{chunk-FZA5LQV3.js → chunk-YNHZCIGO.js} +86 -11
- package/dist/cli.js +8 -6
- package/dist/client.d.ts +188 -0
- package/dist/client.js +121 -0
- package/dist/{service-BKkxEnaJ.d.ts → doc-mJPajXEQ.d.ts} +5 -56
- package/dist/export-Cwx276UP.d.ts +17 -0
- package/dist/formats.d.ts +49 -0
- package/dist/formats.js +234 -0
- package/dist/index.d.ts +7 -99
- package/dist/index.js +11 -7
- package/dist/live-worker.d.ts +2 -1
- package/dist/live-worker.js +6 -4
- package/dist/live.d.ts +6 -1
- package/dist/live.js +4 -3
- package/dist/{mcp-P2C75IIG.js → mcp-ESSEG46C.js} +4 -2
- package/dist/review-Hgq8OgvD.d.ts +85 -0
- package/dist/service-CqKE9DKz.d.ts +91 -0
- package/dist/ui/_headers +4 -0
- package/dist/ui/app.css +1 -0
- package/dist/ui/app.js +270 -0
- package/dist/ui/assets/inter-latin-ext-wght-normal-HA22NDSG.woff2 +0 -0
- package/dist/ui/assets/inter-latin-wght-normal-NRMW37G5.woff2 +0 -0
- package/dist/ui/assets/jetbrains-mono-latin-ext-wght-normal-TSLVDSQR.woff2 +0 -0
- package/dist/ui/assets/jetbrains-mono-latin-wght-normal-VBS632QH.woff2 +0 -0
- package/dist/ui/index.html +13 -0
- package/dist/worker.d.ts +2 -1
- package/dist/worker.js +6 -4
- package/llms.txt +2 -0
- package/package.json +23 -3
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Suggestions,
|
|
3
|
+
Threads,
|
|
4
|
+
isPerson
|
|
5
|
+
} from "./chunk-UPEGODUV.js";
|
|
1
6
|
import {
|
|
2
7
|
AgenticDoc,
|
|
3
8
|
DocError,
|
|
4
|
-
PRINCIPAL_KINDS
|
|
5
|
-
|
|
6
|
-
Threads
|
|
7
|
-
} from "./chunk-YBBQNCMC.js";
|
|
9
|
+
PRINCIPAL_KINDS
|
|
10
|
+
} from "./chunk-6QOYS63L.js";
|
|
8
11
|
|
|
9
12
|
// src/service.ts
|
|
10
13
|
import * as Y from "yjs";
|
|
@@ -16,7 +19,10 @@ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
|
16
19
|
var isString = (v) => typeof v === "string";
|
|
17
20
|
var isWho = (v) => isObject(v) && PRINCIPAL_KINDS.includes(v.kind) && isString(v.name) && v.name !== "";
|
|
18
21
|
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"]);
|
|
22
|
+
var ROOTS = /* @__PURE__ */ new Set(["default", "edits", "suggestions", "threads", "applied", "__tiptap_position_migrations:root:default"]);
|
|
23
|
+
var MAX_TITLE = 120;
|
|
24
|
+
var REMOVE_SCOPE = "docs:remove";
|
|
25
|
+
var MAX_LISTED = 500;
|
|
20
26
|
var DocsService = class {
|
|
21
27
|
constructor(ports, options = {}) {
|
|
22
28
|
this.options = options;
|
|
@@ -47,7 +53,7 @@ var DocsService = class {
|
|
|
47
53
|
* has none. A merge is safe to repeat and to run in any order. The save is conditional, so two
|
|
48
54
|
* checkpoints at once both land.
|
|
49
55
|
*/
|
|
50
|
-
async checkpoint(id, update, author) {
|
|
56
|
+
async checkpoint(id, update, author, options = {}) {
|
|
51
57
|
const key = this.key(id);
|
|
52
58
|
const scratch = new Y.Doc();
|
|
53
59
|
try {
|
|
@@ -62,6 +68,7 @@ var DocsService = class {
|
|
|
62
68
|
for (let attempt = 0; attempt < ATTEMPTS; attempt++) {
|
|
63
69
|
const found = await this.blobs.get(key);
|
|
64
70
|
if (!found) {
|
|
71
|
+
if (options.create === false) throw new DocError("NO_DOCUMENT", `no document with id "${id}"`);
|
|
65
72
|
const created = AgenticDoc.fromUpdate(update, this.options);
|
|
66
73
|
const fresh = this.checked(id, created);
|
|
67
74
|
this.fits(id, fresh);
|
|
@@ -73,7 +80,10 @@ var DocsService = class {
|
|
|
73
80
|
doc.applyUpdate(update);
|
|
74
81
|
const merged = this.checked(id, doc);
|
|
75
82
|
this.fits(id, merged);
|
|
76
|
-
if (await this.blobs.put(key, merged, { ifMatch: found.version }))
|
|
83
|
+
if (await this.blobs.put(key, merged, { ifMatch: found.version })) {
|
|
84
|
+
await this.touch(id, author);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
77
87
|
}
|
|
78
88
|
throw new DocError("CONFLICT", `document "${id}" kept changing; try again`);
|
|
79
89
|
}
|
|
@@ -86,7 +96,7 @@ var DocsService = class {
|
|
|
86
96
|
const refuse = (what) => new DocError("INVALID_INPUT", `the update would leave document "${id}" unreadable: ${what}`);
|
|
87
97
|
try {
|
|
88
98
|
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");
|
|
99
|
+
if (!doc.edits().every((e) => isObject(e) && isWho(e.author) && isString(e.at) && isString(e.op) && (e.client === void 0 || Number.isSafeInteger(e.client) && e.client >= 0))) throw refuse("an edit log entry is malformed");
|
|
90
100
|
for (const [, t] of doc.ydoc.getMap("threads")) {
|
|
91
101
|
const messages = t instanceof Y.Map ? t.get("messages") : void 0;
|
|
92
102
|
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));
|
|
@@ -108,9 +118,71 @@ var DocsService = class {
|
|
|
108
118
|
}
|
|
109
119
|
async catalog(id, author) {
|
|
110
120
|
await this.store.transaction(async (tx) => {
|
|
111
|
-
if (await tx.get(CATALOG, id) === null)
|
|
121
|
+
if (await tx.get(CATALOG, id) === null) {
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
const who = { kind: author.kind, name: author.name };
|
|
124
|
+
await tx.put(CATALOG, id, { createdBy: who, createdAt: now, updatedAt: now, updatedBy: who });
|
|
125
|
+
}
|
|
112
126
|
});
|
|
113
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Record a save. The catalog is a side record that follows the blob, so a failure here never fails a save that worked:
|
|
130
|
+
* the caller would repeat a change that is not safe to repeat. A document with no catalog entry stays without one.
|
|
131
|
+
*/
|
|
132
|
+
async touch(id, author) {
|
|
133
|
+
if (!author) return;
|
|
134
|
+
try {
|
|
135
|
+
await this.store.transaction(async (tx) => {
|
|
136
|
+
const entry = await tx.get(CATALOG, id);
|
|
137
|
+
if (entry !== null) await tx.put(CATALOG, id, { ...entry, updatedAt: Date.now(), updatedBy: { kind: author.kind, name: author.name } });
|
|
138
|
+
});
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Every cataloged document, newest first, up to ${MAX_LISTED}. A damaged entry is skipped, so one bad entry never hides the list. */
|
|
143
|
+
async list() {
|
|
144
|
+
const found = [];
|
|
145
|
+
let cursor;
|
|
146
|
+
do {
|
|
147
|
+
const page = await this.store.query(CATALOG, { orderBy: { field: "createdAt", direction: "desc" }, limit: 100, cursor });
|
|
148
|
+
for (const { id, data } of page.items) {
|
|
149
|
+
const entry = isObject(data) ? data : {};
|
|
150
|
+
const { createdBy, createdAt } = entry;
|
|
151
|
+
if (!isWho(createdBy) || typeof createdAt !== "number") continue;
|
|
152
|
+
const by = entry.updatedBy;
|
|
153
|
+
if (by !== void 0 && !isWho(by)) continue;
|
|
154
|
+
found.push({ id, title: typeof entry.title === "string" && entry.title !== "" ? entry.title : id, createdBy, createdAt, updatedAt: typeof entry.updatedAt === "number" ? entry.updatedAt : createdAt, updatedBy: by ?? createdBy });
|
|
155
|
+
}
|
|
156
|
+
cursor = page.nextCursor;
|
|
157
|
+
} while (cursor !== void 0 && found.length < MAX_LISTED);
|
|
158
|
+
return found.slice(0, MAX_LISTED);
|
|
159
|
+
}
|
|
160
|
+
/** Give the document a name. It does not change the text. */
|
|
161
|
+
async rename(id, title, author) {
|
|
162
|
+
const key = this.key(id);
|
|
163
|
+
const clean = title.trim();
|
|
164
|
+
if (clean === "" || [...clean].length > MAX_TITLE || /[\u0000-\u001f\u007f-\u009f\u00ad\u200b-\u200f\u2028-\u202e\u2066-\u2069\ufeff]/.test(clean)) {
|
|
165
|
+
throw new DocError("INVALID_INPUT", `a title is 1 to ${MAX_TITLE} characters of plain text`);
|
|
166
|
+
}
|
|
167
|
+
const found = await this.blobs.get(key);
|
|
168
|
+
if (!found) throw new DocError("NO_DOCUMENT", `no document with id "${id}"`);
|
|
169
|
+
const who = { kind: author.kind, name: author.name };
|
|
170
|
+
const first = AgenticDoc.fromUpdate(found.data, this.options).edits()[0];
|
|
171
|
+
const created = first ? { createdBy: { kind: first.author.kind, name: first.author.name }, createdAt: Date.parse(first.at) } : { createdBy: who, createdAt: Date.now() };
|
|
172
|
+
await this.store.transaction(async (tx) => {
|
|
173
|
+
const entry = await tx.get(CATALOG, id);
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
await tx.put(CATALOG, id, entry === null ? { ...created, createdAt: Number.isFinite(created.createdAt) ? created.createdAt : now, title: clean, updatedAt: now, updatedBy: who } : { ...entry, title: clean, updatedAt: now, updatedBy: who });
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/** Delete the document and its catalog entry. An agent needs the remove scope. A live room keeps its own copy of the text. */
|
|
179
|
+
async remove(id, author) {
|
|
180
|
+
const key = this.key(id);
|
|
181
|
+
if (!isPerson(author) && author.scopes?.includes(REMOVE_SCOPE) !== true) throw new DocError("FORBIDDEN", `an agent needs the ${REMOVE_SCOPE} scope to remove a document`);
|
|
182
|
+
if (!await this.blobs.head(key)) throw new DocError("NO_DOCUMENT", `no document with id "${id}"`);
|
|
183
|
+
await this.store.delete(CATALOG, id);
|
|
184
|
+
await this.blobs.delete(key);
|
|
185
|
+
}
|
|
114
186
|
/** Who created the document and when, or null when it has no catalog entry (made before the catalog, or a failed catalog write). */
|
|
115
187
|
async info(id) {
|
|
116
188
|
this.key(id);
|
|
@@ -131,14 +203,17 @@ var DocsService = class {
|
|
|
131
203
|
* Load, run the change, and save only if nobody saved in between. When someone did, load again and
|
|
132
204
|
* run the change again, so a check such as "is it still pending" sees the new state.
|
|
133
205
|
*/
|
|
134
|
-
async write(id, run) {
|
|
206
|
+
async write(id, run, author) {
|
|
135
207
|
const key = this.key(id);
|
|
136
208
|
for (let attempt = 0; attempt < ATTEMPTS; attempt++) {
|
|
137
209
|
const found = await this.blobs.get(key);
|
|
138
210
|
if (!found) throw new DocError("NO_DOCUMENT", `no document with id "${id}"`);
|
|
139
211
|
const doc = AgenticDoc.fromUpdate(found.data, this.options);
|
|
140
212
|
const result = run(doc);
|
|
141
|
-
if (await this.blobs.put(key, doc.encode(), { ifMatch: found.version }))
|
|
213
|
+
if (await this.blobs.put(key, doc.encode(), { ifMatch: found.version })) {
|
|
214
|
+
await this.touch(id, author);
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
142
217
|
}
|
|
143
218
|
throw new DocError("CONFLICT", `document "${id}" kept changing; try again`);
|
|
144
219
|
}
|
package/dist/cli.js
CHANGED
|
@@ -2,24 +2,26 @@
|
|
|
2
2
|
import {
|
|
3
3
|
createFileBlobs,
|
|
4
4
|
createFileStore
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-7R7ZPPRF.js";
|
|
6
6
|
import {
|
|
7
7
|
LiveService,
|
|
8
8
|
whoAmI
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-E7B6T5WN.js";
|
|
10
10
|
import {
|
|
11
11
|
DocsService
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-YNHZCIGO.js";
|
|
13
13
|
import {
|
|
14
14
|
VERSION
|
|
15
15
|
} from "./chunk-5Y66UCTE.js";
|
|
16
16
|
import {
|
|
17
17
|
commands
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-S6MQDWFK.js";
|
|
19
|
+
import "./chunk-L6W2AQUF.js";
|
|
20
|
+
import "./chunk-UPEGODUV.js";
|
|
19
21
|
import {
|
|
20
22
|
DocError,
|
|
21
23
|
parseAuthor
|
|
22
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-6QOYS63L.js";
|
|
23
25
|
|
|
24
26
|
// src/cli.ts
|
|
25
27
|
import { readFileSync, writeFileSync } from "fs";
|
|
@@ -118,7 +120,7 @@ async function main(argv, env = process.env) {
|
|
|
118
120
|
author = named && scopes?.length ? { ...named, scopes } : named;
|
|
119
121
|
}
|
|
120
122
|
if (name === "mcp") {
|
|
121
|
-
const { startMcp } = await import("./mcp-
|
|
123
|
+
const { startMcp } = await import("./mcp-ESSEG46C.js");
|
|
122
124
|
await startMcp(service, author);
|
|
123
125
|
return 0;
|
|
124
126
|
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { P as PrincipalKind, S as Section, E as EditEntry } from './doc-mJPajXEQ.js';
|
|
2
|
+
export { A as Anchor } from './doc-mJPajXEQ.js';
|
|
3
|
+
import { E as ExportFormat } from './export-Cwx276UP.js';
|
|
4
|
+
import { D as DocErrorCode, S as Suggestion, T as Thread } from './review-Hgq8OgvD.js';
|
|
5
|
+
import { D as DocSummary } from './service-CqKE9DKz.js';
|
|
6
|
+
import 'yjs';
|
|
7
|
+
import 'agentic-identity';
|
|
8
|
+
|
|
9
|
+
/** The place an edit goes: a section id, or exact text. */
|
|
10
|
+
type Where = {
|
|
11
|
+
section: string;
|
|
12
|
+
at?: "start" | "end";
|
|
13
|
+
} | {
|
|
14
|
+
text: string;
|
|
15
|
+
occurrence?: number;
|
|
16
|
+
at?: "before" | "after";
|
|
17
|
+
};
|
|
18
|
+
interface Me {
|
|
19
|
+
kind: PrincipalKind;
|
|
20
|
+
name: string;
|
|
21
|
+
scopes: string[];
|
|
22
|
+
role: "editor" | "viewer";
|
|
23
|
+
}
|
|
24
|
+
interface Contributor {
|
|
25
|
+
name: string;
|
|
26
|
+
kind: string;
|
|
27
|
+
machine: boolean;
|
|
28
|
+
}
|
|
29
|
+
interface ClientOptions {
|
|
30
|
+
/** The Worker origin, for example `https://docs.example.com`. */
|
|
31
|
+
baseUrl: string;
|
|
32
|
+
/** A bearer token. It sets the author and the role. */
|
|
33
|
+
token: string;
|
|
34
|
+
/** Your own `fetch`. The default is the global one. */
|
|
35
|
+
fetch?: typeof fetch;
|
|
36
|
+
}
|
|
37
|
+
/** Every call takes this last. `signal` cancels the request. */
|
|
38
|
+
interface CallOptions {
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
}
|
|
41
|
+
/** The codes a call can fail with. `BAD_RESPONSE` means a 2xx reply that does not have the expected shape. */
|
|
42
|
+
type DocsApiErrorCode = DocErrorCode | "UNAUTHENTICATED" | "NOT_FOUND" | "UPGRADE_REQUIRED" | "INTERNAL" | "HTTP_ERROR" | "BAD_RESPONSE";
|
|
43
|
+
/** The API refused a call, or sent a reply that is not what it should be. `code` is the API error code. */
|
|
44
|
+
declare class DocsApiError extends Error {
|
|
45
|
+
readonly code: DocsApiErrorCode;
|
|
46
|
+
readonly status: number;
|
|
47
|
+
readonly matches: {
|
|
48
|
+
occurrence: number;
|
|
49
|
+
context: string;
|
|
50
|
+
}[];
|
|
51
|
+
constructor(code: DocsApiErrorCode, message: string, status: number, matches?: {
|
|
52
|
+
occurrence: number;
|
|
53
|
+
context: string;
|
|
54
|
+
}[]);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A client for one Worker. A call that fails with an API error throws `DocsApiError`. A network
|
|
58
|
+
* failure or an abort throws what `fetch` throws. The client never retries: a write is not safe to repeat.
|
|
59
|
+
*/
|
|
60
|
+
declare function createClient(options: ClientOptions): {
|
|
61
|
+
/** `GET /health`. It needs no token, but the client sends it anyway. */
|
|
62
|
+
health(call?: CallOptions): Promise<{
|
|
63
|
+
ok: boolean;
|
|
64
|
+
pack: string;
|
|
65
|
+
}>;
|
|
66
|
+
/** `GET /api/v1/me`: who the token stands for. */
|
|
67
|
+
me: (call?: CallOptions) => Promise<Me>;
|
|
68
|
+
/** `POST /api/v1/docs`. */
|
|
69
|
+
create: (input: {
|
|
70
|
+
id: string;
|
|
71
|
+
markdown?: string;
|
|
72
|
+
}, call?: CallOptions) => Promise<{
|
|
73
|
+
id: string;
|
|
74
|
+
}>;
|
|
75
|
+
/** `GET /api/v1/docs`: documents with titles, creators, and last edits. */
|
|
76
|
+
list: (call?: CallOptions) => Promise<DocSummary[]>;
|
|
77
|
+
/** `POST /rename`: give a document a title. */
|
|
78
|
+
rename: (id: string, input: {
|
|
79
|
+
title: string;
|
|
80
|
+
}, call?: CallOptions) => Promise<{
|
|
81
|
+
ok: true;
|
|
82
|
+
}>;
|
|
83
|
+
/** `POST /remove`: delete a document. An agent needs the `docs:remove` scope. */
|
|
84
|
+
remove: (id: string, call?: CallOptions) => Promise<{
|
|
85
|
+
ok: true;
|
|
86
|
+
}>;
|
|
87
|
+
/** `GET /read`: the whole document, or one section, as Markdown. */
|
|
88
|
+
read(id: string, input?: {
|
|
89
|
+
section?: string;
|
|
90
|
+
}, call?: CallOptions): Promise<string>;
|
|
91
|
+
outline: (id: string, call?: CallOptions) => Promise<Section[]>;
|
|
92
|
+
edits: (id: string, call?: CallOptions) => Promise<EditEntry[]>;
|
|
93
|
+
insert: (id: string, input: Where & {
|
|
94
|
+
content: string;
|
|
95
|
+
}, call?: CallOptions) => Promise<{
|
|
96
|
+
ok: true;
|
|
97
|
+
}>;
|
|
98
|
+
replace: (id: string, input: Where & {
|
|
99
|
+
content: string;
|
|
100
|
+
}, call?: CallOptions) => Promise<{
|
|
101
|
+
ok: true;
|
|
102
|
+
}>;
|
|
103
|
+
delete: (id: string, input: Where, call?: CallOptions) => Promise<{
|
|
104
|
+
ok: true;
|
|
105
|
+
}>;
|
|
106
|
+
append: (id: string, input: {
|
|
107
|
+
content: string;
|
|
108
|
+
}, call?: CallOptions) => Promise<{
|
|
109
|
+
ok: true;
|
|
110
|
+
}>;
|
|
111
|
+
suggest: (id: string, input: {
|
|
112
|
+
text: string;
|
|
113
|
+
replacement: string;
|
|
114
|
+
occurrence?: number;
|
|
115
|
+
at?: "before" | "after";
|
|
116
|
+
}, call?: CallOptions) => Promise<{
|
|
117
|
+
suggestion: string;
|
|
118
|
+
}>;
|
|
119
|
+
suggestions: (id: string, input?: {
|
|
120
|
+
status?: Suggestion["status"];
|
|
121
|
+
}, call?: CallOptions) => Promise<Suggestion[]>;
|
|
122
|
+
accept: (id: string, input: {
|
|
123
|
+
suggestion: string;
|
|
124
|
+
}, call?: CallOptions) => Promise<{
|
|
125
|
+
ok: true;
|
|
126
|
+
}>;
|
|
127
|
+
reject: (id: string, input: {
|
|
128
|
+
suggestion: string;
|
|
129
|
+
}, call?: CallOptions) => Promise<{
|
|
130
|
+
ok: true;
|
|
131
|
+
}>;
|
|
132
|
+
comment: (id: string, input: {
|
|
133
|
+
text: string;
|
|
134
|
+
body: string;
|
|
135
|
+
occurrence?: number;
|
|
136
|
+
}, call?: CallOptions) => Promise<{
|
|
137
|
+
thread: string;
|
|
138
|
+
}>;
|
|
139
|
+
reply: (id: string, input: {
|
|
140
|
+
thread: string;
|
|
141
|
+
body: string;
|
|
142
|
+
}, call?: CallOptions) => Promise<{
|
|
143
|
+
ok: true;
|
|
144
|
+
}>;
|
|
145
|
+
resolve: (id: string, input: {
|
|
146
|
+
thread: string;
|
|
147
|
+
}, call?: CallOptions) => Promise<{
|
|
148
|
+
ok: true;
|
|
149
|
+
}>;
|
|
150
|
+
reopen: (id: string, input: {
|
|
151
|
+
thread: string;
|
|
152
|
+
}, call?: CallOptions) => Promise<{
|
|
153
|
+
ok: true;
|
|
154
|
+
}>;
|
|
155
|
+
threads: (id: string, input?: {
|
|
156
|
+
status?: Thread["status"];
|
|
157
|
+
}, call?: CallOptions) => Promise<Thread[]>;
|
|
158
|
+
/** `GET /export`. `md` and `html` return text. `odt` returns the file bytes. */
|
|
159
|
+
export: {
|
|
160
|
+
(id: string, input: {
|
|
161
|
+
format: "odt";
|
|
162
|
+
remoteImages?: boolean;
|
|
163
|
+
}, call?: CallOptions): Promise<Uint8Array>;
|
|
164
|
+
(id: string, input: {
|
|
165
|
+
format: "md" | "html";
|
|
166
|
+
remoteImages?: boolean;
|
|
167
|
+
}, call?: CallOptions): Promise<string>;
|
|
168
|
+
(id: string, input: {
|
|
169
|
+
format: ExportFormat;
|
|
170
|
+
remoteImages?: boolean;
|
|
171
|
+
}, call?: CallOptions): Promise<string | Uint8Array>;
|
|
172
|
+
};
|
|
173
|
+
/** `GET /room`: who sent updates in the live room. */
|
|
174
|
+
room: (id: string, call?: CallOptions) => Promise<{
|
|
175
|
+
contributors: Contributor[];
|
|
176
|
+
}>;
|
|
177
|
+
/** `GET /state`: the stored document as a Yjs update. */
|
|
178
|
+
state(id: string, call?: CallOptions): Promise<Uint8Array>;
|
|
179
|
+
/** `POST /checkpoint`: merge a Yjs update into the stored document. It makes the document when there is none, unless `create` is false: then it fails with `NO_DOCUMENT`. */
|
|
180
|
+
checkpoint(id: string, update: Uint8Array, call?: CallOptions & {
|
|
181
|
+
create?: boolean;
|
|
182
|
+
}): Promise<{
|
|
183
|
+
ok: true;
|
|
184
|
+
}>;
|
|
185
|
+
};
|
|
186
|
+
type DocsClient = ReturnType<typeof createClient>;
|
|
187
|
+
|
|
188
|
+
export { type CallOptions, type ClientOptions, type Contributor, DocSummary, DocsApiError, type DocsApiErrorCode, type DocsClient, EditEntry, ExportFormat, type Me, Section, Suggestion, Thread, type Where, createClient };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
var DocsApiError = class extends Error {
|
|
3
|
+
constructor(code, message, status, matches = []) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.matches = matches;
|
|
8
|
+
this.name = "DocsApiError";
|
|
9
|
+
}
|
|
10
|
+
code;
|
|
11
|
+
status;
|
|
12
|
+
matches;
|
|
13
|
+
};
|
|
14
|
+
var enc = encodeURIComponent;
|
|
15
|
+
function createClient(options) {
|
|
16
|
+
if (!options.token) throw new TypeError("createClient needs a token");
|
|
17
|
+
const base = options.baseUrl.replace(/\/+$/, "");
|
|
18
|
+
const doFetch = options.fetch ?? ((...args) => globalThis.fetch(...args));
|
|
19
|
+
const bad = (status, why) => new DocsApiError("BAD_RESPONSE", `the server sent a reply that is not valid: ${why}`, status);
|
|
20
|
+
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
21
|
+
const isMatch = (m) => isObject(m) && typeof m.occurrence === "number" && typeof m.context === "string";
|
|
22
|
+
async function send(path, init, call = {}, expect = "application/json") {
|
|
23
|
+
const params = new URLSearchParams();
|
|
24
|
+
for (const [key, value] of Object.entries(init.query ?? {})) if (value !== void 0) params.set(key, String(value));
|
|
25
|
+
const qs = params.toString();
|
|
26
|
+
const url = `${base}${path}${qs ? `?${qs}` : ""}`;
|
|
27
|
+
const headers = { authorization: `Bearer ${options.token}` };
|
|
28
|
+
if (init.json) headers["content-type"] = "application/json";
|
|
29
|
+
const response = await doFetch(url, { method: init.method, headers, body: init.body, signal: call.signal });
|
|
30
|
+
if (response.ok) {
|
|
31
|
+
const type = (response.headers.get("content-type") ?? "").toLowerCase();
|
|
32
|
+
if (!type.startsWith(expect)) throw bad(response.status, `unexpected content type "${type}"`);
|
|
33
|
+
return response;
|
|
34
|
+
}
|
|
35
|
+
let error;
|
|
36
|
+
try {
|
|
37
|
+
const parsed = await response.json();
|
|
38
|
+
if (isObject(parsed.error)) error = parsed.error;
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
throw new DocsApiError(
|
|
42
|
+
typeof error?.code === "string" ? error.code : "HTTP_ERROR",
|
|
43
|
+
typeof error?.message === "string" ? error.message : `the server answered ${response.status}`,
|
|
44
|
+
response.status,
|
|
45
|
+
Array.isArray(error?.matches) ? error.matches.filter(isMatch) : []
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
async function json(response, check, what) {
|
|
49
|
+
let value;
|
|
50
|
+
try {
|
|
51
|
+
value = await response.json();
|
|
52
|
+
} catch {
|
|
53
|
+
throw bad(response.status, "the body is not JSON");
|
|
54
|
+
}
|
|
55
|
+
if (!check(value)) throw bad(response.status, `expected ${what}`);
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
const doc = (id) => `/api/v1/docs/${enc(id)}`;
|
|
59
|
+
const isOk = (v) => isObject(v) && v.ok === true;
|
|
60
|
+
const isList = (v) => Array.isArray(v) && v.every(isObject);
|
|
61
|
+
const get = async (path, query, check, what, call) => await json(await send(path, { method: "GET", query }, call), check, what);
|
|
62
|
+
const post = async (path, body, check, what, call) => await json(await send(path, { method: "POST", json: true, body: JSON.stringify(body) }, call), check, what);
|
|
63
|
+
const ok = (path, body, call) => post(path, body, isOk, "{ ok: true }", call);
|
|
64
|
+
const list = (path, query, call) => get(path, query, isList, "a list", call);
|
|
65
|
+
const withKey = (path, body, key, call) => post(path, body, (v) => isObject(v) && typeof v[key] === "string", `{ ${key}: string }`, call);
|
|
66
|
+
return {
|
|
67
|
+
/** `GET /health`. It needs no token, but the client sends it anyway. */
|
|
68
|
+
async health(call) {
|
|
69
|
+
return await json(await send("/health", { method: "GET" }, call), (v) => isObject(v) && v.ok === true && typeof v.pack === "string", "{ ok, pack }");
|
|
70
|
+
},
|
|
71
|
+
/** `GET /api/v1/me`: who the token stands for. */
|
|
72
|
+
me: (call) => get("/api/v1/me", {}, (v) => isObject(v) && typeof v.kind === "string" && typeof v.name === "string" && Array.isArray(v.scopes) && (v.role === "editor" || v.role === "viewer"), "the token identity", call),
|
|
73
|
+
/** `POST /api/v1/docs`. */
|
|
74
|
+
create: (input, call) => withKey("/api/v1/docs", input, "id", call),
|
|
75
|
+
/** `GET /api/v1/docs`: documents with titles, creators, and last edits. */
|
|
76
|
+
list: (call) => list("/api/v1/docs", {}, call),
|
|
77
|
+
/** `POST /rename`: give a document a title. */
|
|
78
|
+
rename: (id, input, call) => ok(`${doc(id)}/rename`, input, call),
|
|
79
|
+
/** `POST /remove`: delete a document. An agent needs the `docs:remove` scope. */
|
|
80
|
+
remove: (id, call) => ok(`${doc(id)}/remove`, {}, call),
|
|
81
|
+
/** `GET /read`: the whole document, or one section, as Markdown. */
|
|
82
|
+
async read(id, input = {}, call) {
|
|
83
|
+
return (await send(`${doc(id)}/read`, { method: "GET", query: input }, call, "text/markdown")).text();
|
|
84
|
+
},
|
|
85
|
+
outline: (id, call) => list(`${doc(id)}/outline`, {}, call),
|
|
86
|
+
edits: (id, call) => list(`${doc(id)}/edits`, {}, call),
|
|
87
|
+
insert: (id, input, call) => ok(`${doc(id)}/insert`, input, call),
|
|
88
|
+
replace: (id, input, call) => ok(`${doc(id)}/replace`, input, call),
|
|
89
|
+
delete: (id, input, call) => ok(`${doc(id)}/delete`, input, call),
|
|
90
|
+
append: (id, input, call) => ok(`${doc(id)}/append`, input, call),
|
|
91
|
+
suggest: (id, input, call) => withKey(`${doc(id)}/suggest`, input, "suggestion", call),
|
|
92
|
+
suggestions: (id, input = {}, call) => list(`${doc(id)}/suggestions`, input, call),
|
|
93
|
+
accept: (id, input, call) => ok(`${doc(id)}/accept`, input, call),
|
|
94
|
+
reject: (id, input, call) => ok(`${doc(id)}/reject`, input, call),
|
|
95
|
+
comment: (id, input, call) => withKey(`${doc(id)}/comment`, input, "thread", call),
|
|
96
|
+
reply: (id, input, call) => ok(`${doc(id)}/reply`, input, call),
|
|
97
|
+
resolve: (id, input, call) => ok(`${doc(id)}/resolve`, input, call),
|
|
98
|
+
reopen: (id, input, call) => ok(`${doc(id)}/reopen`, input, call),
|
|
99
|
+
threads: (id, input = {}, call) => list(`${doc(id)}/threads`, input, call),
|
|
100
|
+
/** `GET /export`. `md` and `html` return text. `odt` returns the file bytes. */
|
|
101
|
+
export: (async (id, input, call) => {
|
|
102
|
+
const response = await send(`${doc(id)}/export`, { method: "GET", query: input }, call, { md: "text/markdown", html: "text/html", odt: "application/vnd.oasis.opendocument.text" }[input.format]);
|
|
103
|
+
return input.format === "odt" ? new Uint8Array(await response.arrayBuffer()) : response.text();
|
|
104
|
+
}),
|
|
105
|
+
/** `GET /room`: who sent updates in the live room. */
|
|
106
|
+
room: (id, call) => get(`${doc(id)}/room`, {}, (v) => isObject(v) && Array.isArray(v.contributors), "{ contributors }", call),
|
|
107
|
+
/** `GET /state`: the stored document as a Yjs update. */
|
|
108
|
+
async state(id, call) {
|
|
109
|
+
return new Uint8Array(await (await send(`${doc(id)}/state`, { method: "GET" }, call, "application/octet-stream")).arrayBuffer());
|
|
110
|
+
},
|
|
111
|
+
/** `POST /checkpoint`: merge a Yjs update into the stored document. It makes the document when there is none, unless `create` is false: then it fails with `NO_DOCUMENT`. */
|
|
112
|
+
async checkpoint(id, update, call) {
|
|
113
|
+
const query = call?.create === false ? { create: false } : void 0;
|
|
114
|
+
return await json(await send(`${doc(id)}/checkpoint`, { method: "POST", body: update, query }, call), isOk, "{ ok: true }");
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export {
|
|
119
|
+
DocsApiError,
|
|
120
|
+
createClient
|
|
121
|
+
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as Y from 'yjs';
|
|
2
|
-
import { Blobs, Store } from 'agentic-identity';
|
|
3
2
|
|
|
4
3
|
/** A ProseMirror node in JSON form, the shape y-prosemirror reads and writes. */
|
|
5
4
|
interface PmNode {
|
|
@@ -59,6 +58,8 @@ interface EditEntry {
|
|
|
59
58
|
inserted: number;
|
|
60
59
|
deleted: number;
|
|
61
60
|
section?: string;
|
|
61
|
+
/** The Yjs client id of the replica that made the edit. Text it wrote carries this id, so a UI can mark text by author kind. Written by the client, like the rest of the log. */
|
|
62
|
+
client?: number;
|
|
62
63
|
/** Set when an accepted suggestion caused the edit. `author` is who suggested it. */
|
|
63
64
|
suggestion?: {
|
|
64
65
|
id: string;
|
|
@@ -88,6 +89,8 @@ declare class AgenticDoc {
|
|
|
88
89
|
readonly now: () => Date;
|
|
89
90
|
constructor(ydoc?: Y.Doc, options?: DocOptions);
|
|
90
91
|
static fromMarkdown(markdown: string, author: Principal, options?: DocOptions): AgenticDoc;
|
|
92
|
+
/** Make a document from blocks in ProseMirror JSON form. Blocks that the schema does not allow fail with INVALID_INPUT. */
|
|
93
|
+
static fromBlocks(content: PmNode[], author: Principal, options?: DocOptions): AgenticDoc;
|
|
91
94
|
static fromUpdate(update: Uint8Array, options?: DocOptions): AgenticDoc;
|
|
92
95
|
encode(): Uint8Array;
|
|
93
96
|
applyUpdate(update: Uint8Array): void;
|
|
@@ -124,58 +127,4 @@ declare class AgenticDoc {
|
|
|
124
127
|
append(markdown: string, author: Principal): void;
|
|
125
128
|
}
|
|
126
129
|
|
|
127
|
-
|
|
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
|
-
/** What every command needs: make a document, read it, and change it. A live room can stand behind it as well as the ports. */
|
|
142
|
-
interface DocAccess {
|
|
143
|
-
create(id: string, markdown: string, author: Principal): Promise<void>;
|
|
144
|
-
read<T>(id: string, run: (doc: AgenticDoc) => T): Promise<T>;
|
|
145
|
-
write<T>(id: string, run: (doc: AgenticDoc) => T): Promise<T>;
|
|
146
|
-
}
|
|
147
|
-
/** Loads and saves documents as Yjs updates in a Blobs port. The CLI, MCP server, and API share it. */
|
|
148
|
-
declare class DocsService implements DocAccess {
|
|
149
|
-
private readonly options;
|
|
150
|
-
private readonly blobs;
|
|
151
|
-
private readonly store;
|
|
152
|
-
constructor(ports: DocsPorts, options?: DocOptions);
|
|
153
|
-
private key;
|
|
154
|
-
create(id: string, markdown: string, author: Principal): Promise<void>;
|
|
155
|
-
/** The stored document as a Yjs update. A live client seeds its room from it. */
|
|
156
|
-
state(id: string): Promise<Uint8Array>;
|
|
157
|
-
/**
|
|
158
|
-
* Merge a Yjs update from a live room into the stored document, and create the document when it
|
|
159
|
-
* has none. A merge is safe to repeat and to run in any order. The save is conditional, so two
|
|
160
|
-
* checkpoints at once both land.
|
|
161
|
-
*/
|
|
162
|
-
checkpoint(id: string, update: Uint8Array, author: Principal): Promise<void>;
|
|
163
|
-
/**
|
|
164
|
-
* The encoded document, once it reads back and every stored entry has the shape the commands rely on. Yjs does not
|
|
165
|
-
* check types, so an update can put text where the editor expects blocks, or a thread with no messages, and then a
|
|
166
|
-
* command fails on every later call.
|
|
167
|
-
*/
|
|
168
|
-
private checked;
|
|
169
|
-
private fits;
|
|
170
|
-
private catalog;
|
|
171
|
-
/** Who created the document and when, or null when it has no catalog entry (made before the catalog, or a failed catalog write). */
|
|
172
|
-
info(id: string): Promise<DocInfo | null>;
|
|
173
|
-
read<T>(id: string, run: (doc: AgenticDoc) => T): Promise<T>;
|
|
174
|
-
/**
|
|
175
|
-
* Load, run the change, and save only if nobody saved in between. When someone did, load again and
|
|
176
|
-
* run the change again, so a check such as "is it still pending" sees the new state.
|
|
177
|
-
*/
|
|
178
|
-
write<T>(id: string, run: (doc: AgenticDoc) => T): Promise<T>;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export { type Anchor as A, type DocAccess 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, DocsService as g, PRINCIPAL_KINDS as h, type PrincipalKind as i, type SectionTarget as j, type Target as k, blocksToMarkdown as l, markdownToBlocks as m, parseAuthor as p };
|
|
130
|
+
export { type Anchor as A, type DocOptions as D, type EditEntry as E, type PrincipalKind as P, type Section as S, type Target as T, AgenticDoc as a, type PmNode as b, type Principal as c, type AnchorTarget as d, PRINCIPAL_KINDS as e, type SectionTarget as f, type TextTarget as g, blocksToMarkdown as h, markdownToBlocks as m, parseAuthor as p };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { a as AgenticDoc } from './doc-mJPajXEQ.js';
|
|
2
|
+
|
|
3
|
+
declare const EXPORT_FORMATS: readonly ["md", "html", "odt"];
|
|
4
|
+
type ExportFormat = (typeof EXPORT_FORMATS)[number];
|
|
5
|
+
interface ExportOptions {
|
|
6
|
+
/** Keep `<img>` for web and relative sources. Opening the page then asks those hosts for the image. Off by default. */
|
|
7
|
+
remoteImages?: boolean;
|
|
8
|
+
}
|
|
9
|
+
interface ExportResult {
|
|
10
|
+
format: ExportFormat;
|
|
11
|
+
contentType: string;
|
|
12
|
+
encoding: "utf8" | "base64";
|
|
13
|
+
data: string;
|
|
14
|
+
}
|
|
15
|
+
declare function exportDoc(doc: AgenticDoc, format: ExportFormat, options?: ExportOptions): ExportResult;
|
|
16
|
+
|
|
17
|
+
export { type ExportFormat as E, type ExportOptions as a, EXPORT_FORMATS as b, type ExportResult as c, exportDoc as e };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Limits, TextDoc } from 'agentic-formats';
|
|
2
|
+
import { D as DocOptions, a as AgenticDoc, b as PmNode, c as Principal } from './doc-mJPajXEQ.js';
|
|
3
|
+
import { a as ExportOptions } from './export-Cwx276UP.js';
|
|
4
|
+
import 'yjs';
|
|
5
|
+
|
|
6
|
+
declare const IMPORT_FORMATS: readonly ["md", "odt", "docx"];
|
|
7
|
+
type ImportFormat = (typeof IMPORT_FORMATS)[number];
|
|
8
|
+
declare const FILE_FORMATS: readonly ["md", "html", "odt", "docx"];
|
|
9
|
+
type FileFormat = (typeof FILE_FORMATS)[number];
|
|
10
|
+
declare const DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
11
|
+
interface ImportOptions {
|
|
12
|
+
/** Caps for the file. A file that goes past one is refused, never trimmed. The defaults are those of agentic-formats. */
|
|
13
|
+
limits?: Partial<Limits>;
|
|
14
|
+
doc?: DocOptions;
|
|
15
|
+
}
|
|
16
|
+
interface ImportResult {
|
|
17
|
+
doc: AgenticDoc;
|
|
18
|
+
/** The title the file holds, if any. */
|
|
19
|
+
title?: string;
|
|
20
|
+
/** One line for each part of the file that the document cannot hold. Empty when nothing was lost. */
|
|
21
|
+
warnings: string[];
|
|
22
|
+
}
|
|
23
|
+
interface FileResult {
|
|
24
|
+
format: FileFormat;
|
|
25
|
+
contentType: string;
|
|
26
|
+
/** Text for `md` and `html`. File bytes for `odt` and `docx`. */
|
|
27
|
+
data: string | Uint8Array;
|
|
28
|
+
/** One line for each part of the document that the format cannot hold. Empty when nothing was lost. */
|
|
29
|
+
warnings: string[];
|
|
30
|
+
}
|
|
31
|
+
/** Map document blocks to the neutral text model. A part the model cannot hold keeps its text and adds a warning. */
|
|
32
|
+
declare function toTextDoc(blocks: PmNode[], title?: string): {
|
|
33
|
+
model: TextDoc;
|
|
34
|
+
warnings: string[];
|
|
35
|
+
};
|
|
36
|
+
/** Map the neutral text model to document blocks. A part the editor cannot hold keeps its text and adds a warning. */
|
|
37
|
+
declare function fromTextDoc(model: TextDoc): {
|
|
38
|
+
blocks: PmNode[];
|
|
39
|
+
title?: string;
|
|
40
|
+
warnings: string[];
|
|
41
|
+
};
|
|
42
|
+
/** Read a file into a new document. The author makes the one `create` edit, so text from an agent stays labeled. */
|
|
43
|
+
declare function importFile(input: string | Uint8Array, format: ImportFormat, author: Principal, options?: ImportOptions): ImportResult;
|
|
44
|
+
/** Write a document as a file. `title` goes into the ODT and DOCX metadata. */
|
|
45
|
+
declare function exportFile(doc: AgenticDoc, format: FileFormat, options?: ExportOptions & {
|
|
46
|
+
title?: string;
|
|
47
|
+
}): FileResult;
|
|
48
|
+
|
|
49
|
+
export { DOCX_MIME, FILE_FORMATS, type FileFormat, type FileResult, IMPORT_FORMATS, type ImportFormat, type ImportOptions, type ImportResult, exportFile, fromTextDoc, importFile, toTextDoc };
|