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.
- package/LICENSE +21 -0
- package/README.md +184 -2
- package/SECURITY.md +23 -0
- package/SKILL.md +35 -0
- package/dist/chunk-4YVWD5PH.js +1123 -0
- package/dist/chunk-5Y66UCTE.js +7 -0
- package/dist/chunk-BSXUNQDR.js +224 -0
- package/dist/chunk-LRPLNXOF.js +67 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +129 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.js +42 -0
- package/dist/mcp-2VZIGHIX.js +38 -0
- package/dist/service-eYISo6-b.d.ts +159 -0
- package/dist/worker.d.ts +37 -0
- package/dist/worker.js +162 -0
- package/llms.txt +15 -0
- package/package.json +93 -4
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DocError
|
|
3
|
+
} from "./chunk-4YVWD5PH.js";
|
|
4
|
+
|
|
5
|
+
// src/adapters/file-blobs.ts
|
|
6
|
+
import { createHash, randomUUID } from "crypto";
|
|
7
|
+
import { link, mkdir, open, readFile, rename, rm, stat, writeFile } from "fs/promises";
|
|
8
|
+
import { resolve } from "path";
|
|
9
|
+
import { assertBlobPutOptions, assertResponseHeaders, parseRegion } from "agentic-identity";
|
|
10
|
+
var sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
11
|
+
var missing = (error) => error.code === "ENOENT";
|
|
12
|
+
var HEADER = 4;
|
|
13
|
+
function pack(data, contentType) {
|
|
14
|
+
const head = Buffer.from(JSON.stringify(contentType === void 0 ? {} : { contentType }));
|
|
15
|
+
const length = Buffer.alloc(HEADER);
|
|
16
|
+
length.writeUInt32BE(head.length);
|
|
17
|
+
return Buffer.concat([length, head, data]);
|
|
18
|
+
}
|
|
19
|
+
function unpack(file) {
|
|
20
|
+
const end = HEADER + file.readUInt32BE(0);
|
|
21
|
+
const { contentType } = JSON.parse(file.subarray(HEADER, end).toString("utf8"));
|
|
22
|
+
return {
|
|
23
|
+
data: new Uint8Array(file.subarray(end)),
|
|
24
|
+
version: createHash("sha256").update(file).digest("hex"),
|
|
25
|
+
...contentType === void 0 ? {} : { contentType }
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function createLocker(root, options = {}) {
|
|
29
|
+
const staleMs = options.lockStaleMs ?? 1e4;
|
|
30
|
+
const timeoutMs = options.lockTimeoutMs ?? 5e3;
|
|
31
|
+
async function clearStale(lock) {
|
|
32
|
+
let token;
|
|
33
|
+
let age;
|
|
34
|
+
try {
|
|
35
|
+
token = await readFile(lock, "utf8");
|
|
36
|
+
age = Date.now() - (await stat(lock)).mtimeMs;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (missing(error)) return true;
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
if (!(age > staleMs)) return false;
|
|
42
|
+
const moved = `${lock}.stale-${randomUUID()}`;
|
|
43
|
+
try {
|
|
44
|
+
await rename(lock, moved);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (missing(error)) return true;
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
if (await readFile(moved, "utf8").catch(() => token) !== token) await link(moved, lock).catch(() => void 0);
|
|
50
|
+
await rm(moved, { force: true });
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
async function locked(full, run) {
|
|
54
|
+
await mkdir(root, { recursive: true });
|
|
55
|
+
const lock = `${full}.lock`;
|
|
56
|
+
const token = randomUUID();
|
|
57
|
+
const deadline = Date.now() + timeoutMs;
|
|
58
|
+
for (; ; ) {
|
|
59
|
+
try {
|
|
60
|
+
const file = await open(lock, "wx");
|
|
61
|
+
await file.writeFile(token).finally(() => file.close());
|
|
62
|
+
break;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error.code !== "EEXIST") throw error;
|
|
65
|
+
if (await clearStale(lock)) continue;
|
|
66
|
+
if (Date.now() >= deadline) throw new DocError("LOCKED", "another writer holds this document; try again");
|
|
67
|
+
await sleep(5 + Math.random() * 20);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const held = async () => await readFile(lock, "utf8").catch(() => null) === token;
|
|
71
|
+
try {
|
|
72
|
+
return await run(held);
|
|
73
|
+
} finally {
|
|
74
|
+
if (await held()) await rm(lock, { force: true });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return locked;
|
|
78
|
+
}
|
|
79
|
+
function createFileBlobs(dir, options = {}) {
|
|
80
|
+
const root = resolve(dir);
|
|
81
|
+
const locked = createLocker(root, options);
|
|
82
|
+
const now = options.now ?? Date.now;
|
|
83
|
+
const path = (key) => {
|
|
84
|
+
if (key === "") throw new Error("blob key must not be empty");
|
|
85
|
+
return resolve(root, createHash("sha256").update(key).digest("hex"));
|
|
86
|
+
};
|
|
87
|
+
async function read(full) {
|
|
88
|
+
try {
|
|
89
|
+
return unpack(await readFile(full));
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (missing(error)) return null;
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
region: options.region === void 0 ? "unknown" : parseRegion(options.region),
|
|
97
|
+
async put(key, data, opts) {
|
|
98
|
+
const full = path(key);
|
|
99
|
+
assertBlobPutOptions(opts);
|
|
100
|
+
return locked(full, async (held) => {
|
|
101
|
+
if (opts?.ifNoneMatch === "*" || opts?.ifMatch !== void 0) {
|
|
102
|
+
const current = await read(full);
|
|
103
|
+
if (opts.ifNoneMatch === "*" ? current !== null : current?.version !== opts.ifMatch) return null;
|
|
104
|
+
}
|
|
105
|
+
const file = pack(data, opts?.contentType);
|
|
106
|
+
const temp = `${full}.${randomUUID()}.tmp`;
|
|
107
|
+
try {
|
|
108
|
+
await writeFile(temp, file);
|
|
109
|
+
if (!await held()) throw new DocError("LOCKED", "the lock was taken over while writing; try again");
|
|
110
|
+
await rename(temp, full);
|
|
111
|
+
} finally {
|
|
112
|
+
await rm(temp, { force: true });
|
|
113
|
+
}
|
|
114
|
+
return { version: createHash("sha256").update(file).digest("hex") };
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
get: (key) => read(path(key)),
|
|
118
|
+
async head(key) {
|
|
119
|
+
const found = await read(path(key));
|
|
120
|
+
return found ? { size: found.data.length, version: found.version, ...found.contentType === void 0 ? {} : { contentType: found.contentType } } : null;
|
|
121
|
+
},
|
|
122
|
+
async delete(key) {
|
|
123
|
+
const full = path(key);
|
|
124
|
+
await locked(full, () => rm(full, { force: true }));
|
|
125
|
+
},
|
|
126
|
+
/** A local path with its expiry. Nothing serves or checks it: it exists so the port is whole. */
|
|
127
|
+
async signedUrl(key, opts) {
|
|
128
|
+
const full = path(key);
|
|
129
|
+
const { method, expiresInSeconds } = opts;
|
|
130
|
+
if (!Number.isInteger(expiresInSeconds) || expiresInSeconds < 1 || expiresInSeconds > 604800) {
|
|
131
|
+
throw new RangeError("expiresInSeconds must be an integer from 1 to 604800");
|
|
132
|
+
}
|
|
133
|
+
assertResponseHeaders(opts);
|
|
134
|
+
const extra = [
|
|
135
|
+
opts.responseContentDisposition === void 0 ? "" : `&disposition=${encodeURIComponent(opts.responseContentDisposition)}`,
|
|
136
|
+
opts.responseContentType === void 0 ? "" : `&type=${encodeURIComponent(opts.responseContentType)}`
|
|
137
|
+
].join("");
|
|
138
|
+
return `file://${full}?method=${method}&expires=${now() + expiresInSeconds * 1e3}${extra}`;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/adapters/file-store.ts
|
|
144
|
+
import { createMemoryStore, StoreConflictError } from "agentic-identity";
|
|
145
|
+
import { resolve as resolve2 } from "path";
|
|
146
|
+
var KEY = "store";
|
|
147
|
+
var ATTEMPTS = 64;
|
|
148
|
+
function createFileStore(dir, options = {}) {
|
|
149
|
+
const { shared: _shared, ...rest } = options;
|
|
150
|
+
const memoryOptions = { region: rest.region, now: rest.now };
|
|
151
|
+
const blobs = createFileBlobs(dir, options);
|
|
152
|
+
const locked = createLocker(resolve2(dir), options);
|
|
153
|
+
const mutex = resolve2(dir, "store-mutex");
|
|
154
|
+
const region = createMemoryStore(memoryOptions).region;
|
|
155
|
+
async function load() {
|
|
156
|
+
const found = await blobs.get(KEY);
|
|
157
|
+
const rows = /* @__PURE__ */ new Map();
|
|
158
|
+
const memory = createMemoryStore(memoryOptions);
|
|
159
|
+
if (found) {
|
|
160
|
+
for (const row of JSON.parse(new TextDecoder().decode(found.data))) {
|
|
161
|
+
rows.set(JSON.stringify([row[0], row[1]]), row);
|
|
162
|
+
await memory.put(row[0], row[1], row[2]);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { memory, rows, version: found?.version };
|
|
166
|
+
}
|
|
167
|
+
async function save(rows, changes, version) {
|
|
168
|
+
if (changes.length === 0) return true;
|
|
169
|
+
for (const [collection, id, doc] of changes) {
|
|
170
|
+
const key = JSON.stringify([collection, id]);
|
|
171
|
+
if (doc === null) rows.delete(key);
|
|
172
|
+
else rows.set(key, [collection, id, doc]);
|
|
173
|
+
}
|
|
174
|
+
const data = new TextEncoder().encode(JSON.stringify([...rows.values()]));
|
|
175
|
+
const saved = await blobs.put(KEY, data, version === void 0 ? { ifNoneMatch: "*" } : { ifMatch: version });
|
|
176
|
+
return saved !== null;
|
|
177
|
+
}
|
|
178
|
+
const recording = (ops, changes) => ({
|
|
179
|
+
...ops,
|
|
180
|
+
put: async (collection, id, doc) => {
|
|
181
|
+
await ops.put(collection, id, doc);
|
|
182
|
+
changes.push([collection, id, doc]);
|
|
183
|
+
},
|
|
184
|
+
delete: async (collection, id) => {
|
|
185
|
+
await ops.delete(collection, id);
|
|
186
|
+
changes.push([collection, id, null]);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
const transaction = async (fn) => {
|
|
190
|
+
for (let attempt = 0; attempt < ATTEMPTS; attempt++) {
|
|
191
|
+
const done = await locked(mutex, async (held) => {
|
|
192
|
+
const { memory, rows, version } = await load();
|
|
193
|
+
let changes = [];
|
|
194
|
+
const result = await memory.transaction((tx) => {
|
|
195
|
+
changes = [];
|
|
196
|
+
return fn(recording(tx, changes));
|
|
197
|
+
});
|
|
198
|
+
if (changes.length > 0 && !await held()) throw new DocError("LOCKED", "the lock was taken over during the transaction; try again");
|
|
199
|
+
return await save(rows, changes, version) ? { result } : void 0;
|
|
200
|
+
});
|
|
201
|
+
if (done) return done.result;
|
|
202
|
+
}
|
|
203
|
+
throw new StoreConflictError(`transaction kept conflicting after ${ATTEMPTS} attempts`);
|
|
204
|
+
};
|
|
205
|
+
return {
|
|
206
|
+
region,
|
|
207
|
+
get: async (collection, id) => (await load()).memory.get(collection, id),
|
|
208
|
+
query: async (collection, query) => (await load()).memory.query(collection, query),
|
|
209
|
+
put: (collection, id, doc) => transaction(async (tx) => {
|
|
210
|
+
await tx.get(collection, id);
|
|
211
|
+
await tx.put(collection, id, doc);
|
|
212
|
+
}),
|
|
213
|
+
delete: (collection, id) => transaction(async (tx) => {
|
|
214
|
+
await tx.get(collection, id);
|
|
215
|
+
await tx.delete(collection, id);
|
|
216
|
+
}),
|
|
217
|
+
transaction
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export {
|
|
222
|
+
createFileBlobs,
|
|
223
|
+
createFileStore
|
|
224
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AgenticDoc,
|
|
3
|
+
DocError,
|
|
4
|
+
PRINCIPAL_KINDS
|
|
5
|
+
} from "./chunk-4YVWD5PH.js";
|
|
6
|
+
|
|
7
|
+
// src/service.ts
|
|
8
|
+
var ATTEMPTS = 64;
|
|
9
|
+
var ID = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
10
|
+
var CATALOG = "documents";
|
|
11
|
+
var DocsService = class {
|
|
12
|
+
constructor(ports, options = {}) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
this.blobs = ports.blobs;
|
|
15
|
+
this.store = ports.store;
|
|
16
|
+
}
|
|
17
|
+
options;
|
|
18
|
+
blobs;
|
|
19
|
+
store;
|
|
20
|
+
key(id) {
|
|
21
|
+
if (!ID.test(id)) throw new DocError("INVALID_INPUT", `invalid document id "${id}": use 1 to 64 of a-z, 0-9, "-" and "_"`);
|
|
22
|
+
return `${id}.ydoc`;
|
|
23
|
+
}
|
|
24
|
+
async create(id, markdown, author) {
|
|
25
|
+
const key = this.key(id);
|
|
26
|
+
const created = await this.blobs.put(key, AgenticDoc.fromMarkdown(markdown, author, this.options).encode(), { ifNoneMatch: "*" });
|
|
27
|
+
if (!created) throw new DocError("DOCUMENT_EXISTS", `document "${id}" already exists`);
|
|
28
|
+
await this.store.transaction(async (tx) => {
|
|
29
|
+
if (await tx.get(CATALOG, id) === null) await tx.put(CATALOG, id, { createdBy: { kind: author.kind, name: author.name }, createdAt: Date.now() });
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** Who created the document and when, or null when it has no catalog entry (made before the catalog, or a failed catalog write). */
|
|
33
|
+
async info(id) {
|
|
34
|
+
this.key(id);
|
|
35
|
+
const entry = await this.store.get(CATALOG, id);
|
|
36
|
+
if (entry === null) return null;
|
|
37
|
+
const { createdBy, createdAt } = entry;
|
|
38
|
+
if (!createdBy || !PRINCIPAL_KINDS.includes(createdBy.kind) || typeof createdBy.name !== "string" || typeof createdAt !== "number") {
|
|
39
|
+
throw new DocError("INVALID_INPUT", `the catalog entry for "${id}" is damaged`);
|
|
40
|
+
}
|
|
41
|
+
return { createdBy, createdAt };
|
|
42
|
+
}
|
|
43
|
+
async read(id, run) {
|
|
44
|
+
const found = await this.blobs.get(this.key(id));
|
|
45
|
+
if (!found) throw new DocError("NO_DOCUMENT", `no document with id "${id}"`);
|
|
46
|
+
return run(AgenticDoc.fromUpdate(found.data, this.options));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Load, run the change, and save only if nobody saved in between. When someone did, load again and
|
|
50
|
+
* run the change again, so a check such as "is it still pending" sees the new state.
|
|
51
|
+
*/
|
|
52
|
+
async write(id, run) {
|
|
53
|
+
const key = this.key(id);
|
|
54
|
+
for (let attempt = 0; attempt < ATTEMPTS; attempt++) {
|
|
55
|
+
const found = await this.blobs.get(key);
|
|
56
|
+
if (!found) throw new DocError("NO_DOCUMENT", `no document with id "${id}"`);
|
|
57
|
+
const doc = AgenticDoc.fromUpdate(found.data, this.options);
|
|
58
|
+
const result = run(doc);
|
|
59
|
+
if (await this.blobs.put(key, doc.encode(), { ifMatch: found.version })) return result;
|
|
60
|
+
}
|
|
61
|
+
throw new DocError("CONFLICT", `document "${id}" kept changing; try again`);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
DocsService
|
|
67
|
+
};
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createFileBlobs,
|
|
4
|
+
createFileStore
|
|
5
|
+
} from "./chunk-BSXUNQDR.js";
|
|
6
|
+
import {
|
|
7
|
+
DocsService
|
|
8
|
+
} from "./chunk-LRPLNXOF.js";
|
|
9
|
+
import {
|
|
10
|
+
VERSION
|
|
11
|
+
} from "./chunk-5Y66UCTE.js";
|
|
12
|
+
import {
|
|
13
|
+
DocError,
|
|
14
|
+
commands,
|
|
15
|
+
parseAuthor
|
|
16
|
+
} from "./chunk-4YVWD5PH.js";
|
|
17
|
+
|
|
18
|
+
// src/cli.ts
|
|
19
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
20
|
+
import { join } from "path";
|
|
21
|
+
import { parseArgs } from "util";
|
|
22
|
+
var NAME = "agentic-documents";
|
|
23
|
+
var flagNames = [...new Set(commands.flatMap((c) => Object.keys(c.shape)))].filter((k) => k !== "id");
|
|
24
|
+
var options = {
|
|
25
|
+
...Object.fromEntries(flagNames.map((k) => [k, { type: "string" }])),
|
|
26
|
+
as: { type: "string" },
|
|
27
|
+
scopes: { type: "string" },
|
|
28
|
+
dir: { type: "string" },
|
|
29
|
+
file: { type: "string" },
|
|
30
|
+
out: { type: "string" },
|
|
31
|
+
help: { type: "boolean", short: "h" },
|
|
32
|
+
version: { type: "boolean", short: "V" }
|
|
33
|
+
};
|
|
34
|
+
function help() {
|
|
35
|
+
const lines = commands.map((c) => ` ${c.name.padEnd(12)}${c.description}`);
|
|
36
|
+
return [
|
|
37
|
+
`${NAME} ${VERSION}`,
|
|
38
|
+
"",
|
|
39
|
+
`Usage: ${NAME} <command> <id> [flags]`,
|
|
40
|
+
"",
|
|
41
|
+
"Commands:",
|
|
42
|
+
...lines,
|
|
43
|
+
" mcp Start the MCP server on stdio.",
|
|
44
|
+
"",
|
|
45
|
+
"Flags:",
|
|
46
|
+
" --as <kind:name> Who makes the change. Or set AGENTIC_DOCS_AS. Kinds: human, agent_of_human,",
|
|
47
|
+
" agent_of_agent, agent, user_token.",
|
|
48
|
+
" --scopes <a,b> Rights for the author, such as docs:accept. Or set AGENTIC_DOCS_SCOPES. An agent",
|
|
49
|
+
" needs docs:accept to accept a suggestion.",
|
|
50
|
+
" --dir <path> Where documents live. Or set AGENTIC_DOCS_DIR. Default: .agentic-documents",
|
|
51
|
+
" --file <path> Read content (or markdown for new) from a file.",
|
|
52
|
+
" --out <path> Write the export to a file.",
|
|
53
|
+
" --<field> <value> Any field of the command, such as --text, --section, --occurrence."
|
|
54
|
+
].join("\n");
|
|
55
|
+
}
|
|
56
|
+
function bindValues(argv) {
|
|
57
|
+
const out = [];
|
|
58
|
+
for (let i = 0; i < argv.length; i++) {
|
|
59
|
+
const name = argv[i].startsWith("--") && !argv[i].includes("=") ? argv[i].slice(2) : void 0;
|
|
60
|
+
const takesValue = name !== void 0 && options[name]?.type === "string";
|
|
61
|
+
out.push(takesValue && i + 1 < argv.length ? `${argv[i]}=${argv[++i]}` : argv[i]);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
async function main(argv, env = process.env) {
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = parseArgs({ args: bindValues(argv), options, allowPositionals: true });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
71
|
+
return 2;
|
|
72
|
+
}
|
|
73
|
+
const { values, positionals } = parsed;
|
|
74
|
+
const [name, id] = positionals;
|
|
75
|
+
if (values.version || values.help || name === void 0) {
|
|
76
|
+
console.log(values.version ? VERSION : help());
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
const dir = values.dir ?? env.AGENTIC_DOCS_DIR ?? ".agentic-documents";
|
|
80
|
+
const service = new DocsService({ blobs: createFileBlobs(join(dir, "blobs")), store: createFileStore(join(dir, "store")) });
|
|
81
|
+
const scopes = (values.scopes ?? env.AGENTIC_DOCS_SCOPES)?.split(",").map((s) => s.trim()).filter(Boolean);
|
|
82
|
+
const named = parseAuthor(values.as ?? env.AGENTIC_DOCS_AS);
|
|
83
|
+
const author = named && scopes?.length ? { ...named, scopes } : named;
|
|
84
|
+
if (name === "mcp") {
|
|
85
|
+
const { startMcp } = await import("./mcp-2VZIGHIX.js");
|
|
86
|
+
await startMcp(service, author);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
const command = commands.find((c) => c.name === name);
|
|
90
|
+
if (!command) {
|
|
91
|
+
console.error(`unknown command "${name}". Run "${NAME} --help".`);
|
|
92
|
+
return 2;
|
|
93
|
+
}
|
|
94
|
+
const { as: _as, scopes: _scopes, dir: _dir, file, out, help: _help, version: _version, ...fields } = values;
|
|
95
|
+
const input = { ...command.shape.id ? { id } : {}, ...fields };
|
|
96
|
+
try {
|
|
97
|
+
if (file !== void 0) {
|
|
98
|
+
const key = "content" in command.shape ? "content" : "markdown" in command.shape ? "markdown" : void 0;
|
|
99
|
+
if (!key) throw new DocError("INVALID_INPUT", `"${name}" takes no --file`);
|
|
100
|
+
if (key in input) throw new DocError("INVALID_INPUT", `pass --file or --${key}, not both`);
|
|
101
|
+
input[key] = readFileSync(file, "utf8");
|
|
102
|
+
}
|
|
103
|
+
const result = await command.run(service, input, author);
|
|
104
|
+
if (name === "export") {
|
|
105
|
+
const { data, encoding } = result;
|
|
106
|
+
const bytes = encoding === "base64" ? Buffer.from(data, "base64") : data;
|
|
107
|
+
if (out === void 0) process.stdout.write(bytes);
|
|
108
|
+
else writeFileSync(out, bytes);
|
|
109
|
+
} else console.log(typeof result === "string" ? result.replace(/\n$/, "") : JSON.stringify(result, null, 2));
|
|
110
|
+
return 0;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (!(error instanceof DocError)) throw error;
|
|
113
|
+
console.error(`${error.code}: ${error.message}`);
|
|
114
|
+
for (const m of error.matches) console.error(` ${m.occurrence}: ...${m.context}...`);
|
|
115
|
+
return error.code === "INVALID_INPUT" ? 2 : 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
main(process.argv.slice(2)).then(
|
|
119
|
+
(code) => {
|
|
120
|
+
if (code !== 0) process.exit(code);
|
|
121
|
+
},
|
|
122
|
+
(error) => {
|
|
123
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
export {
|
|
128
|
+
main
|
|
129
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { P as Principal, A as Anchor, a as AgenticDoc, T as TextTarget, b as AnchorTarget, D as DocsService, c as PmNode } from './service-eYISo6-b.js';
|
|
2
|
+
export { d as DocInfo, e as DocOptions, f as DocsPorts, E as EditEntry, g as PRINCIPAL_KINDS, h as PrincipalKind, S as Section, i as SectionTarget, j as Target, k as blocksToMarkdown, m as markdownToBlocks, p as parseAuthor } from './service-eYISo6-b.js';
|
|
3
|
+
import { Schema } from 'prosemirror-model';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { AdapterOptions, Blobs, Store } from 'agentic-identity';
|
|
6
|
+
import 'yjs';
|
|
7
|
+
|
|
8
|
+
type DocErrorCode = "NO_MATCH" | "AMBIGUOUS_MATCH" | "STALE_ANCHOR" | "NO_SECTION" | "INVALID_AUTHOR" | "INVALID_INPUT" | "NO_THREAD" | "NO_SUGGESTION" | "ALREADY_DECIDED" | "FORBIDDEN" | "NO_DOCUMENT" | "DOCUMENT_EXISTS" | "LOCKED" | "CONFLICT" | "MISCONFIGURED";
|
|
9
|
+
declare class DocError extends Error {
|
|
10
|
+
readonly code: DocErrorCode;
|
|
11
|
+
readonly matches: {
|
|
12
|
+
occurrence: number;
|
|
13
|
+
context: string;
|
|
14
|
+
}[];
|
|
15
|
+
constructor(code: DocErrorCode, message: string, matches?: {
|
|
16
|
+
occurrence: number;
|
|
17
|
+
context: string;
|
|
18
|
+
}[]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
declare const schema: Schema<string, string>;
|
|
22
|
+
|
|
23
|
+
interface Message {
|
|
24
|
+
id: string;
|
|
25
|
+
at: string;
|
|
26
|
+
author: Principal;
|
|
27
|
+
body: string;
|
|
28
|
+
}
|
|
29
|
+
interface Thread {
|
|
30
|
+
id: string;
|
|
31
|
+
anchor: Anchor;
|
|
32
|
+
status: "open" | "resolved";
|
|
33
|
+
resolvedBy?: Principal;
|
|
34
|
+
resolvedAt?: string;
|
|
35
|
+
messages: Message[];
|
|
36
|
+
/** The anchored text no longer matches what the comment was made on. */
|
|
37
|
+
stale: boolean;
|
|
38
|
+
/** The text now under the anchor, or null when it is gone. */
|
|
39
|
+
currentText: string | null;
|
|
40
|
+
}
|
|
41
|
+
interface Suggestion {
|
|
42
|
+
id: string;
|
|
43
|
+
kind: "insert" | "replace" | "delete";
|
|
44
|
+
at?: "before" | "after";
|
|
45
|
+
anchor: Anchor;
|
|
46
|
+
replacement: string;
|
|
47
|
+
author: Principal;
|
|
48
|
+
createdAt: string;
|
|
49
|
+
status: "pending" | "accepted" | "rejected";
|
|
50
|
+
decidedBy?: Principal;
|
|
51
|
+
decidedAt?: string;
|
|
52
|
+
stale: boolean;
|
|
53
|
+
}
|
|
54
|
+
/** Comment threads anchored to text. Replies and status merge across replicas. */
|
|
55
|
+
declare class Threads {
|
|
56
|
+
private readonly doc;
|
|
57
|
+
constructor(doc: AgenticDoc);
|
|
58
|
+
private get map();
|
|
59
|
+
private get;
|
|
60
|
+
private message;
|
|
61
|
+
add(target: TextTarget | AnchorTarget, body: string, author: Principal): string;
|
|
62
|
+
reply(id: string, body: string, author: Principal): void;
|
|
63
|
+
private setStatus;
|
|
64
|
+
resolve(id: string, author: Principal): void;
|
|
65
|
+
reopen(id: string, author: Principal): void;
|
|
66
|
+
list(filter?: {
|
|
67
|
+
status?: Thread["status"];
|
|
68
|
+
}): Thread[];
|
|
69
|
+
}
|
|
70
|
+
/** Suggested edits. Nothing changes in the text until a person, or an agent with the accept scope, accepts one. */
|
|
71
|
+
declare class Suggestions {
|
|
72
|
+
private readonly doc;
|
|
73
|
+
constructor(doc: AgenticDoc);
|
|
74
|
+
private get map();
|
|
75
|
+
private get;
|
|
76
|
+
private read;
|
|
77
|
+
/** An `at` on the target means insert. An empty replacement means delete. */
|
|
78
|
+
add(target: TextTarget | AnchorTarget, replacement: string, author: Principal): string;
|
|
79
|
+
private decide;
|
|
80
|
+
/**
|
|
81
|
+
* Apply the edit as the accepting author. The check and the edit share one transaction. Accepting
|
|
82
|
+
* an accepted suggestion changes nothing. A stale anchor throws and the suggestion stays pending.
|
|
83
|
+
*/
|
|
84
|
+
accept(id: string, author: Principal): void;
|
|
85
|
+
/** Same right as accept. A person can withdraw their own suggestion. An agent can never decide its own. */
|
|
86
|
+
reject(id: string, author: Principal): void;
|
|
87
|
+
list(filter?: {
|
|
88
|
+
status?: Suggestion["status"];
|
|
89
|
+
}): Suggestion[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** One table drives the CLI, the MCP server, and the HTTP API, so they stay the same. */
|
|
93
|
+
interface Command {
|
|
94
|
+
name: string;
|
|
95
|
+
description: string;
|
|
96
|
+
shape: z.ZodRawShape;
|
|
97
|
+
readOnly: boolean;
|
|
98
|
+
run(service: DocsService, input: Record<string, unknown>, author: Principal | undefined): Promise<unknown>;
|
|
99
|
+
}
|
|
100
|
+
declare const commands: Command[];
|
|
101
|
+
|
|
102
|
+
declare const EXPORT_FORMATS: readonly ["md", "html", "odt"];
|
|
103
|
+
type ExportFormat = (typeof EXPORT_FORMATS)[number];
|
|
104
|
+
interface ExportOptions {
|
|
105
|
+
/** Keep `<img>` for web and relative sources. Opening the page then asks those hosts for the image. Off by default. */
|
|
106
|
+
remoteImages?: boolean;
|
|
107
|
+
}
|
|
108
|
+
interface ExportResult {
|
|
109
|
+
format: ExportFormat;
|
|
110
|
+
contentType: string;
|
|
111
|
+
encoding: "utf8" | "base64";
|
|
112
|
+
data: string;
|
|
113
|
+
}
|
|
114
|
+
declare function exportDoc(doc: AgenticDoc, format: ExportFormat, options?: ExportOptions): ExportResult;
|
|
115
|
+
|
|
116
|
+
interface FileBlobsOptions extends AdapterOptions {
|
|
117
|
+
/** A lock older than this is taken over. Default 10 seconds. A save takes milliseconds. */
|
|
118
|
+
lockStaleMs?: number;
|
|
119
|
+
/** How long a writer waits for a lock before it fails with LOCKED. Default 5 seconds. */
|
|
120
|
+
lockTimeoutMs?: number;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Blobs kept as files under one directory. File names are key hashes, so no key leaves the folder.
|
|
124
|
+
* A write takes a lock file, checks the condition again, then writes a temp file and renames it into
|
|
125
|
+
* place. A reader never sees a torn file, and two processes cannot both pass the version check.
|
|
126
|
+
*/
|
|
127
|
+
declare function createFileBlobs(dir: string, options?: FileBlobsOptions): Blobs;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A Store kept in one file. The file holds every doc as one snapshot, saved through the file Blobs
|
|
131
|
+
* adapter with a conditional write. Each call loads the newest snapshot, so several processes (CLI
|
|
132
|
+
* runs, MCP servers) share one dataset. A transaction holds a lock file from its first read to its
|
|
133
|
+
* commit, so a rival write waits and lands after it. The conditional write is a second guard: if the
|
|
134
|
+
* lock was lost and the snapshot changed, the transaction runs again on the new snapshot.
|
|
135
|
+
* The store re-reads and re-writes the whole snapshot on each call: it suits a small local catalog.
|
|
136
|
+
*/
|
|
137
|
+
declare function createFileStore(dir: string, options?: AdapterOptions & Pick<FileBlobsOptions, "lockStaleMs" | "lockTimeoutMs"> & {
|
|
138
|
+
shared?: string;
|
|
139
|
+
}): Store;
|
|
140
|
+
|
|
141
|
+
declare const ODT_MIME = "application/vnd.oasis.opendocument.text";
|
|
142
|
+
/** Write blocks as an OpenDocument text file (.odt): content, styles, and images as embedded files. */
|
|
143
|
+
declare function exportOdt(blocks: PmNode[]): Uint8Array;
|
|
144
|
+
|
|
145
|
+
export { AgenticDoc, Anchor, AnchorTarget, type Command, DocError, type DocErrorCode, DocsService, EXPORT_FORMATS, type ExportFormat, type ExportOptions, type ExportResult, type FileBlobsOptions, type Message, ODT_MIME, PmNode, Principal, type Suggestion, Suggestions, TextTarget, type Thread, Threads, commands, createFileBlobs, createFileStore, exportDoc, exportOdt, schema };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createFileBlobs,
|
|
3
|
+
createFileStore
|
|
4
|
+
} from "./chunk-BSXUNQDR.js";
|
|
5
|
+
import {
|
|
6
|
+
DocsService
|
|
7
|
+
} from "./chunk-LRPLNXOF.js";
|
|
8
|
+
import {
|
|
9
|
+
AgenticDoc,
|
|
10
|
+
DocError,
|
|
11
|
+
EXPORT_FORMATS,
|
|
12
|
+
ODT_MIME,
|
|
13
|
+
PRINCIPAL_KINDS,
|
|
14
|
+
Suggestions,
|
|
15
|
+
Threads,
|
|
16
|
+
blocksToMarkdown,
|
|
17
|
+
commands,
|
|
18
|
+
exportDoc,
|
|
19
|
+
exportOdt,
|
|
20
|
+
markdownToBlocks,
|
|
21
|
+
parseAuthor,
|
|
22
|
+
schema
|
|
23
|
+
} from "./chunk-4YVWD5PH.js";
|
|
24
|
+
export {
|
|
25
|
+
AgenticDoc,
|
|
26
|
+
DocError,
|
|
27
|
+
DocsService,
|
|
28
|
+
EXPORT_FORMATS,
|
|
29
|
+
ODT_MIME,
|
|
30
|
+
PRINCIPAL_KINDS,
|
|
31
|
+
Suggestions,
|
|
32
|
+
Threads,
|
|
33
|
+
blocksToMarkdown,
|
|
34
|
+
commands,
|
|
35
|
+
createFileBlobs,
|
|
36
|
+
createFileStore,
|
|
37
|
+
exportDoc,
|
|
38
|
+
exportOdt,
|
|
39
|
+
markdownToBlocks,
|
|
40
|
+
parseAuthor,
|
|
41
|
+
schema
|
|
42
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
VERSION
|
|
3
|
+
} from "./chunk-5Y66UCTE.js";
|
|
4
|
+
import {
|
|
5
|
+
DocError,
|
|
6
|
+
commands
|
|
7
|
+
} from "./chunk-4YVWD5PH.js";
|
|
8
|
+
|
|
9
|
+
// src/mcp.ts
|
|
10
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
|
+
function createMcpServer(service, author) {
|
|
13
|
+
const server = new McpServer({ name: "agentic-documents", version: VERSION });
|
|
14
|
+
for (const command of commands) {
|
|
15
|
+
server.registerTool(
|
|
16
|
+
`doc_${command.name}`,
|
|
17
|
+
{ description: command.description, inputSchema: command.shape, annotations: { readOnlyHint: command.readOnly } },
|
|
18
|
+
async (input) => {
|
|
19
|
+
try {
|
|
20
|
+
const result = await command.run(service, input, author);
|
|
21
|
+
return { content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result) }] };
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (!(error instanceof DocError)) throw error;
|
|
24
|
+
const body = { error: { code: error.code, message: error.message, matches: error.matches } };
|
|
25
|
+
return { content: [{ type: "text", text: JSON.stringify(body) }], isError: true };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return server;
|
|
31
|
+
}
|
|
32
|
+
async function startMcp(service, author) {
|
|
33
|
+
await createMcpServer(service, author).connect(new StdioServerTransport());
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
createMcpServer,
|
|
37
|
+
startMcp
|
|
38
|
+
};
|