agentic-comments 0.1.0 → 0.3.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,172 @@
1
+ import {
2
+ Comments,
3
+ CommentsError,
4
+ Suggestions,
5
+ exact,
6
+ invalid,
7
+ textAnchor,
8
+ textOf,
9
+ textRange
10
+ } from "./chunk-GSAJOCDV.js";
11
+
12
+ // src/local-host.ts
13
+ import * as Y from "yjs";
14
+ var TEXT_ROOT = "text";
15
+ var VALUES_ROOT = "values";
16
+ var MAX_TEXT = 1e5;
17
+ var refuse = (message) => {
18
+ throw new CommentsError("INVALID_INPUT", message);
19
+ };
20
+ function valueKey(anchor) {
21
+ const id = anchor.key ?? anchor.elementId;
22
+ return typeof id === "string" ? `${anchor.type}:${id}` : refuse("this anchor has no key or element id");
23
+ }
24
+ var localHost = {
25
+ parse(change, { anchor }) {
26
+ const c = change ?? {};
27
+ const keys = Object.keys(c);
28
+ if (anchor.type === "text") {
29
+ if (keys.length === 1 && keys[0] === "replace" && typeof c.replace === "string" && c.replace.length <= MAX_TEXT) return { replace: c.replace };
30
+ return refuse('a change for a text anchor is { "replace": text }');
31
+ }
32
+ if (keys.length === 1 && keys[0] === "value") return { value: c.value };
33
+ return refuse(`a change for a ${anchor.type} anchor is { "value": JSON }`);
34
+ },
35
+ apply(change, ctx) {
36
+ const c = change;
37
+ if (ctx.anchor.type === "text") {
38
+ if (typeof c.replace !== "string") throw new Error('a text anchor needs { "replace": text }');
39
+ const range = textRange(ctx.anchor, ctx.ydoc);
40
+ if (!range) throw new Error("the anchored text is gone");
41
+ range.type.delete(range.from, range.to - range.from);
42
+ range.type.insert(range.from, c.replace);
43
+ return {
44
+ kind: "text",
45
+ start: Y.relativePositionToJSON(Y.createRelativePositionFromTypeIndex(range.type, range.from, 0)),
46
+ end: Y.relativePositionToJSON(Y.createRelativePositionFromTypeIndex(range.type, range.from + c.replace.length, -1))
47
+ };
48
+ }
49
+ if (!("value" in c)) throw new Error('this anchor needs { "value": JSON }');
50
+ ctx.ydoc.getMap(VALUES_ROOT).set(valueKey(ctx.anchor), c.value);
51
+ return { kind: "value" };
52
+ },
53
+ revert(receipt, ctx) {
54
+ const r = receipt ?? {};
55
+ if (r.kind === "value") return;
56
+ if (r.kind !== "text" || typeof r.start !== "object" || r.start === null || typeof r.end !== "object" || r.end === null) {
57
+ throw new Error("the receipt is not a text receipt");
58
+ }
59
+ const at = (json) => {
60
+ try {
61
+ return Y.createAbsolutePositionFromRelativePosition(Y.createRelativePositionFromJSON(json), ctx.ydoc);
62
+ } catch {
63
+ return null;
64
+ }
65
+ };
66
+ const from = at(r.start);
67
+ const to = at(r.end);
68
+ if (from && to && from.type === to.type && from.type instanceof Y.Text && to.index > from.index) {
69
+ from.type.delete(from.index, to.index - from.index);
70
+ }
71
+ }
72
+ };
73
+
74
+ // src/service.ts
75
+ var SHOWN = 5;
76
+ function anchorFrom(ydoc, spec) {
77
+ const type = spec?.type;
78
+ if (type === "cell") {
79
+ const given2 = exact(spec, ["type", "key"], "a cell anchor");
80
+ return { type: "cell", key: given2.key };
81
+ }
82
+ if (type === "slide" || type === "canvas") {
83
+ const given2 = exact(spec, ["type", "elementId"], `a ${type} anchor`);
84
+ return { type, elementId: given2.elementId };
85
+ }
86
+ const given = exact(spec, ["type", "quote", "occurrence"], "a text anchor");
87
+ if (given.type !== "text") return invalid('an anchor type is "text", "cell", "slide", or "canvas"');
88
+ const quote = given.quote;
89
+ if (typeof quote !== "string" || quote === "") return invalid("a text anchor needs a quote");
90
+ const text = ydoc.getText(TEXT_ROOT);
91
+ const value = textOf(text);
92
+ const at = [];
93
+ for (let i = value.indexOf(quote); i >= 0; i = value.indexOf(quote, i + 1)) at.push(i);
94
+ if (at.length === 0) throw new CommentsError("INVALID_ANCHOR", `no text matches ${JSON.stringify(quote.slice(0, 40))}`);
95
+ const occurrence = given.occurrence;
96
+ if (occurrence !== void 0 && (!Number.isSafeInteger(occurrence) || occurrence < 1 || occurrence > at.length)) {
97
+ throw new CommentsError("INVALID_ANCHOR", `the quote has ${at.length} matches, not ${String(occurrence)}`);
98
+ }
99
+ if (occurrence === void 0 && at.length > 1) {
100
+ const offsets = at.slice(0, SHOWN).join(", ");
101
+ throw new CommentsError("INVALID_ANCHOR", `the quote matches ${at.length} places, at character offsets ${offsets}${at.length > SHOWN ? ", and more" : ""}; pass an occurrence from 1 to ${at.length}`);
102
+ }
103
+ const start = at[occurrence ? occurrence - 1 : 0];
104
+ return textAnchor(text, start, start + quote.length);
105
+ }
106
+ var CommentsService = class {
107
+ constructor(store, actor) {
108
+ this.store = store;
109
+ this.actor = actor;
110
+ }
111
+ store;
112
+ actor;
113
+ open(ydoc) {
114
+ ydoc.getText(TEXT_ROOT);
115
+ const comments = new Comments(ydoc);
116
+ return { comments, suggestions: new Suggestions(comments, localHost) };
117
+ }
118
+ read(fn) {
119
+ return this.store.read((ydoc) => fn(this.open(ydoc), ydoc));
120
+ }
121
+ write(fn) {
122
+ return this.store.write((ydoc) => fn(this.open(ydoc), ydoc));
123
+ }
124
+ threads(filter = {}) {
125
+ const given = exact(filter, ["status", "anchorState", "mentioning"], "a filter");
126
+ return this.read(({ comments }) => comments.list(given));
127
+ }
128
+ thread(id) {
129
+ return this.read(({ comments }) => comments.get(id));
130
+ }
131
+ comment(input) {
132
+ const given = exact(input, ["anchor", "body", "mentions"], "a comment");
133
+ return this.write(({ comments }, ydoc) => comments.add({ anchor: anchorFrom(ydoc, given.anchor), body: given.body, mentions: given.mentions }, this.actor));
134
+ }
135
+ reply(threadId, input) {
136
+ return this.write(({ comments }) => comments.reply(threadId, exact(input, ["body", "mentions"], "a reply"), this.actor));
137
+ }
138
+ resolve(threadId) {
139
+ return this.write(({ comments }) => comments.resolve(threadId, this.actor));
140
+ }
141
+ reopen(threadId) {
142
+ return this.write(({ comments }) => comments.reopen(threadId, this.actor));
143
+ }
144
+ relocate(threadId) {
145
+ return this.write(({ comments }) => comments.relocate(threadId, this.actor));
146
+ }
147
+ suggestions(filter = {}) {
148
+ const given = exact(filter, ["status", "anchorState"], "a filter");
149
+ return this.read(({ suggestions }) => suggestions.list(given));
150
+ }
151
+ suggest(input) {
152
+ const given = exact(input, ["anchor", "change", "summary"], "a suggestion");
153
+ return this.write(({ suggestions }, ydoc) => suggestions.add({ anchor: anchorFrom(ydoc, given.anchor), change: given.change, summary: given.summary }, this.actor));
154
+ }
155
+ accept(suggestionId) {
156
+ return this.write(({ suggestions }) => suggestions.accept(suggestionId, this.actor));
157
+ }
158
+ reject(suggestionId) {
159
+ return this.write(({ suggestions }) => suggestions.reject(suggestionId, this.actor));
160
+ }
161
+ /** The content text of the store. */
162
+ text() {
163
+ return this.read((_o, ydoc) => ydoc.getText(TEXT_ROOT).toString());
164
+ }
165
+ };
166
+
167
+ export {
168
+ TEXT_ROOT,
169
+ VALUES_ROOT,
170
+ localHost,
171
+ CommentsService
172
+ };
@@ -0,0 +1,86 @@
1
+ import {
2
+ VERSION
3
+ } from "./chunk-GMA5I5L6.js";
4
+ import {
5
+ CommentsService
6
+ } from "./chunk-IB6ANO32.js";
7
+ import {
8
+ CommentsError
9
+ } from "./chunk-GSAJOCDV.js";
10
+
11
+ // src/mcp.ts
12
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
15
+ import { z } from "zod";
16
+ var anchor = z.discriminatedUnion("type", [
17
+ z.object({ type: z.literal("text"), quote: z.string().describe("Exact text to anchor to."), occurrence: z.number().int().min(1).optional().describe("Which match, when the quote occurs more than once.") }).strict(),
18
+ z.object({ type: z.literal("cell"), key: z.string() }).strict(),
19
+ z.object({ type: z.literal("slide"), elementId: z.string() }).strict(),
20
+ z.object({ type: z.literal("canvas"), elementId: z.string() }).strict()
21
+ ]).describe("Where the thread or suggestion points.");
22
+ var id = z.string().describe("The thread or suggestion id.");
23
+ var body = z.string().describe("The message text. Write @handle to mention someone.");
24
+ var mentions = z.array(z.string()).optional().describe("Extra handles to mention.");
25
+ function ok(value) {
26
+ return { content: [{ type: "text", text: JSON.stringify(value) }] };
27
+ }
28
+ function failure(error) {
29
+ const known = error instanceof CommentsError;
30
+ return { content: [{ type: "text", text: JSON.stringify({ error: { code: known ? error.code : "INTERNAL", message: error instanceof Error ? error.message : String(error) } }) }], isError: true };
31
+ }
32
+ function createMcpServer(store, actor) {
33
+ const service = new CommentsService(store, actor);
34
+ const server = new McpServer({ name: "agentic-comments", version: VERSION });
35
+ const tools = /* @__PURE__ */ new Map();
36
+ const tool = (name, description, shape, readOnly, run) => {
37
+ const strict = z.object(shape).strict();
38
+ tools.set(name, { strict, run });
39
+ server.registerTool(name, { description, inputSchema: strict, annotations: { readOnlyHint: readOnly, destructiveHint: false, openWorldHint: false } }, (() => {
40
+ throw new Error("calls are handled by the server");
41
+ }));
42
+ };
43
+ const state = z.enum(["attached", "edited", "orphaned", "unknown"]).optional();
44
+ tool("list_threads", "List comment threads. Each has an anchorState: an orphaned thread lost its content but stays readable.", { status: z.enum(["open", "resolved"]).optional(), anchorState: state, mentioning: z.string().optional() }, true, (i) => service.threads(i));
45
+ tool("get_thread", "Read one thread with its messages.", { id }, true, (i) => service.thread(i.id));
46
+ tool("add_comment", "Start a thread on text, a cell, a slide element, or a canvas element.", { anchor, body, mentions }, false, (i) => service.comment(i));
47
+ tool("reply_to_thread", "Reply to an open thread. Reopen a resolved thread first.", { id, body, mentions }, false, (i) => service.reply(i.id, { body: i.body, mentions: i.mentions }));
48
+ tool("resolve_thread", "Resolve a thread.", { id }, false, (i) => service.resolve(i.id));
49
+ tool("reopen_thread", "Reopen a resolved thread.", { id }, false, (i) => service.reopen(i.id));
50
+ tool("relocate_thread", "Move a text anchor to its quote when the quote occurs exactly once.", { id }, false, (i) => service.relocate(i.id));
51
+ tool("list_suggestions", "List suggested changes.", { status: z.enum(["pending", "accepted", "rejected"]).optional(), anchorState: state }, true, (i) => service.suggestions(i));
52
+ tool(
53
+ "suggest_change",
54
+ 'Suggest a change. For a text anchor the change is {"replace": "new text"}. For another anchor it is {"value": <JSON>}.',
55
+ { anchor, change: z.union([z.object({ replace: z.string() }).strict(), z.object({ value: z.unknown() }).strict()]), summary: z.string().optional() },
56
+ false,
57
+ (i) => service.suggest(i)
58
+ );
59
+ tool("accept_suggestion", "Accept a suggestion. Needs the accept scope for an agent, and never works on your own suggestion.", { id }, false, (i) => service.accept(i.id));
60
+ tool("reject_suggestion", "Reject a suggestion. Same rights as accept.", { id }, false, (i) => service.reject(i.id));
61
+ server.server.setRequestHandler(CallToolRequestSchema, (request) => {
62
+ const entry = tools.get(request.params.name);
63
+ if (!entry) return failure(new CommentsError("INVALID_INPUT", `there is no tool named ${JSON.stringify(request.params.name.slice(0, 60))}`));
64
+ try {
65
+ const checked = entry.strict.safeParse(request.params.arguments ?? {});
66
+ if (!checked.success) {
67
+ const where = checked.error.issues.map((i) => `${i.path.join(".") || "input"}: ${i.message}`).join("; ");
68
+ throw new CommentsError("INVALID_INPUT", `the tool input is refused: ${where.slice(0, 400)}`);
69
+ }
70
+ return ok(entry.run(checked.data));
71
+ } catch (error) {
72
+ return failure(error);
73
+ }
74
+ });
75
+ return server;
76
+ }
77
+ async function startMcp(store, actor) {
78
+ process.on("SIGXFSZ", () => void 0);
79
+ const server = createMcpServer(store, actor);
80
+ await server.connect(new StdioServerTransport());
81
+ }
82
+
83
+ export {
84
+ createMcpServer,
85
+ startMcp
86
+ };
@@ -0,0 +1,262 @@
1
+ import {
2
+ CommentsError,
3
+ PRINCIPAL_KINDS,
4
+ stamp
5
+ } from "./chunk-GSAJOCDV.js";
6
+
7
+ // src/file-store.ts
8
+ import { createHash } from "crypto";
9
+ import { closeSync, existsSync, fsyncSync, linkSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeSync } from "fs";
10
+ import { dirname, resolve } from "path";
11
+ import * as Y from "yjs";
12
+ var MAX_FILE = 50 * 1024 * 1024;
13
+ var MAGIC = Buffer.from("ACV1");
14
+ var HASH = 32;
15
+ var POLL_MS = 25;
16
+ var EMPTY_LOCK_MS = 2e3;
17
+ var LOCK_MAX_AGE_MS = 6e4;
18
+ var ATTEMPTS = 4;
19
+ function lockTimeoutFrom(env) {
20
+ const raw = env.AGENTIC_COMMENTS_LOCK_TIMEOUT_MS;
21
+ if (raw === void 0) return void 0;
22
+ if (!/^\d{1,9}$/.test(raw)) throw new CommentsError("USAGE", "AGENTIC_COMMENTS_LOCK_TIMEOUT_MS must be a whole number of milliseconds");
23
+ return Number(raw);
24
+ }
25
+ var sleep = (ms) => void Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
26
+ function alive(pid) {
27
+ try {
28
+ process.kill(pid, 0);
29
+ return true;
30
+ } catch (error) {
31
+ return error.code === "EPERM";
32
+ }
33
+ }
34
+ var digest = (data) => createHash("sha256").update(data).digest();
35
+ function encode(ydoc) {
36
+ const update = Y.encodeStateAsUpdate(ydoc);
37
+ return Buffer.concat([MAGIC, digest(update), update]);
38
+ }
39
+ function decode(bytes, path) {
40
+ const body = bytes.subarray(MAGIC.length + HASH);
41
+ if (bytes.length < MAGIC.length + HASH || !bytes.subarray(0, MAGIC.length).equals(MAGIC) || !bytes.subarray(MAGIC.length, MAGIC.length + HASH).equals(digest(body))) {
42
+ throw new CommentsError("STORE_CORRUPT", `${path} is not a saved document, or it is damaged`);
43
+ }
44
+ const ydoc = new Y.Doc();
45
+ try {
46
+ Y.applyUpdate(ydoc, new Uint8Array(body));
47
+ } catch {
48
+ throw new CommentsError("STORE_CORRUPT", `${path} is not a saved document, or it is damaged`);
49
+ }
50
+ return ydoc;
51
+ }
52
+ function writeAll(handle, data) {
53
+ let done = 0;
54
+ while (done < data.length) {
55
+ const wrote = writeSync(handle, data, done, data.length - done);
56
+ if (wrote <= 0) throw Object.assign(new Error("a write made no progress"), { code: "ENOSPC" });
57
+ done += wrote;
58
+ }
59
+ }
60
+ var FULL = /* @__PURE__ */ new Set(["ENOSPC", "EFBIG", "EDQUOT"]);
61
+ function writeDurably(path, data) {
62
+ let handle;
63
+ try {
64
+ handle = openSync(path, "wx");
65
+ writeAll(handle, data);
66
+ fsyncSync(handle);
67
+ } catch (error) {
68
+ if (handle !== void 0) closeSync(handle);
69
+ handle = void 0;
70
+ try {
71
+ unlinkSync(path);
72
+ } catch {
73
+ }
74
+ if (FULL.has(error.code ?? "")) throw new CommentsError("STORE_FULL", "the file could not be written: the disk is full or a file size limit applies");
75
+ throw error;
76
+ }
77
+ closeSync(handle);
78
+ }
79
+ function syncDirectory(path) {
80
+ try {
81
+ const handle = openSync(dirname(path), "r");
82
+ try {
83
+ fsyncSync(handle);
84
+ } finally {
85
+ closeSync(handle);
86
+ }
87
+ } catch {
88
+ }
89
+ }
90
+ var identity = (path) => {
91
+ const s = statSync(path);
92
+ return `${s.ino}:${s.size}:${s.mtimeMs}`;
93
+ };
94
+ var FileStore = class {
95
+ path;
96
+ lockTimeoutMs;
97
+ constructor(path, options = {}) {
98
+ const given = resolve(path);
99
+ this.path = existsSync(given) ? realpathSync(given) : given;
100
+ const timeout = options.lockTimeoutMs ?? 5e3;
101
+ if (!Number.isSafeInteger(timeout) || timeout < 0) throw new CommentsError("INVALID_INPUT", "lockTimeoutMs must be a whole number of milliseconds, 0 or more");
102
+ this.lockTimeoutMs = timeout;
103
+ }
104
+ /** Create the file. `seed` fills the document before the first save. Refuses to overwrite, even when two run at once. */
105
+ create(seed = () => void 0) {
106
+ mkdirSync(dirname(this.path), { recursive: true });
107
+ const ydoc = new Y.Doc();
108
+ seed(ydoc);
109
+ const data = encode(ydoc);
110
+ if (data.length > MAX_FILE) throw new CommentsError("STORE_FULL", `the document is larger than ${MAX_FILE} bytes`);
111
+ const temporary = `${this.path}.tmp-${process.pid}-${globalThis.crypto.randomUUID()}`;
112
+ writeDurably(temporary, data);
113
+ try {
114
+ linkSync(temporary, this.path);
115
+ } catch (error) {
116
+ const code = error.code;
117
+ if (code === "EEXIST") throw new CommentsError("STORE_EXISTS", `${this.path} already exists`);
118
+ if (code !== "EPERM" && code !== "ENOTSUP" && code !== "EXDEV" && code !== "ENOSYS") throw error;
119
+ if (existsSync(this.path)) throw new CommentsError("STORE_EXISTS", `${this.path} already exists`);
120
+ renameSync(temporary, this.path);
121
+ } finally {
122
+ try {
123
+ unlinkSync(temporary);
124
+ } catch {
125
+ }
126
+ }
127
+ syncDirectory(this.path);
128
+ }
129
+ load() {
130
+ if (!existsSync(this.path)) throw new CommentsError("STORE_MISSING", `${this.path} does not exist; run init first`);
131
+ if (statSync(this.path).size > MAX_FILE) throw new CommentsError("STORE_CORRUPT", `${this.path} is larger than ${MAX_FILE} bytes`);
132
+ const seen = identity(this.path);
133
+ return { ydoc: decode(readFileSync(this.path), this.path), seen };
134
+ }
135
+ /** Read the newest saved state. Anything `fn` changes is not saved. */
136
+ read(fn) {
137
+ return fn(this.load().ydoc);
138
+ }
139
+ acquire(lock, mine) {
140
+ const deadline = Date.now() + this.lockTimeoutMs;
141
+ for (; ; ) {
142
+ try {
143
+ const handle = openSync(lock, "wx");
144
+ try {
145
+ writeAll(handle, Buffer.from(mine));
146
+ } catch (error) {
147
+ closeSync(handle);
148
+ try {
149
+ unlinkSync(lock);
150
+ } catch {
151
+ }
152
+ if (FULL.has(error.code ?? "")) throw new CommentsError("STORE_FULL", "the lock could not be written: the disk is full");
153
+ throw error;
154
+ }
155
+ closeSync(handle);
156
+ return;
157
+ } catch (error) {
158
+ if (error instanceof CommentsError) throw error;
159
+ if (error.code !== "EEXIST") throw new CommentsError("STORE_LOCKED", `${lock} cannot be created: ${error.code ?? "an error"}`);
160
+ }
161
+ if (this.clearStale(lock, mine)) continue;
162
+ if (Date.now() >= deadline) {
163
+ throw new CommentsError("STORE_LOCKED", `${lock} is held by another process; remove it only if nothing uses ${this.path}`);
164
+ }
165
+ sleep(POLL_MS);
166
+ }
167
+ }
168
+ /**
169
+ * Clear a lock whose holder is dead (or is this process, with another mark), an empty one older than a
170
+ * short time, or any one more than a minute old or a minute in the future. The lock moves to a unique name
171
+ * first, so only one process clears it. If the moved lock is not the one we looked at, it goes back.
172
+ */
173
+ clearStale(lock, mine) {
174
+ let held;
175
+ let age;
176
+ try {
177
+ held = readFileSync(lock, "utf8").trim();
178
+ age = Date.now() - statSync(lock).mtimeMs;
179
+ } catch (error) {
180
+ return error.code === "ENOENT";
181
+ }
182
+ const pid = Number(held.split(":")[0]);
183
+ const well = /^\d{1,10}(:[\w-]{1,64})?$/.test(held) && pid > 0;
184
+ const left = well && (!alive(pid) || pid === process.pid && held !== mine);
185
+ const old = age > LOCK_MAX_AGE_MS || age < -LOCK_MAX_AGE_MS;
186
+ if (!left && !(held === "" && age > EMPTY_LOCK_MS) && !old) return false;
187
+ const tomb = `${lock}.stale-${process.pid}-${globalThis.crypto.randomUUID()}`;
188
+ try {
189
+ renameSync(lock, tomb);
190
+ } catch {
191
+ return true;
192
+ }
193
+ try {
194
+ if (readFileSync(tomb, "utf8").trim() !== held) {
195
+ try {
196
+ linkSync(tomb, lock);
197
+ } catch {
198
+ }
199
+ return false;
200
+ }
201
+ } catch {
202
+ return true;
203
+ } finally {
204
+ try {
205
+ unlinkSync(tomb);
206
+ } catch {
207
+ }
208
+ }
209
+ return true;
210
+ }
211
+ /** Lock, load the newest state, run `fn`, and save. When `fn` throws, nothing is saved. */
212
+ write(fn) {
213
+ const lock = `${this.path}.lock`;
214
+ const mine = `${process.pid}:${globalThis.crypto.randomUUID()}`;
215
+ this.acquire(lock, mine);
216
+ try {
217
+ for (let attempt = 1; ; attempt++) {
218
+ const { ydoc, seen } = this.load();
219
+ const result = fn(ydoc);
220
+ const data = encode(ydoc);
221
+ if (data.length > MAX_FILE) throw new CommentsError("STORE_FULL", `the document would pass ${MAX_FILE} bytes, so the change was not saved`);
222
+ if (identity(this.path) !== seen) {
223
+ if (attempt >= ATTEMPTS) throw new CommentsError("STORE_LOCKED", `${this.path} kept changing while it was saved`);
224
+ continue;
225
+ }
226
+ const temporary = `${this.path}.tmp-${process.pid}-${globalThis.crypto.randomUUID()}`;
227
+ writeDurably(temporary, data);
228
+ try {
229
+ renameSync(temporary, this.path);
230
+ } catch (error) {
231
+ unlinkSync(temporary);
232
+ throw error;
233
+ }
234
+ syncDirectory(this.path);
235
+ return result;
236
+ }
237
+ } finally {
238
+ try {
239
+ if (readFileSync(lock, "utf8") === mine) unlinkSync(lock);
240
+ } catch {
241
+ }
242
+ }
243
+ }
244
+ };
245
+
246
+ // src/operator.ts
247
+ function operatorActor(as, scopes) {
248
+ const value = as?.trim();
249
+ const cut = value === void 0 ? -1 : value.indexOf(":");
250
+ if (value === void 0 || cut < 1 || cut === value.length - 1) {
251
+ throw new CommentsError("INVALID_AUTHOR", `set the principal as kind:name, for example human:ada. Kinds: ${PRINCIPAL_KINDS.join(", ")}`);
252
+ }
253
+ const list = scopes === void 0 || scopes.trim() === "" ? [] : scopes.split(",").map((s) => s.trim());
254
+ return stamp({ kind: value.slice(0, cut), name: value.slice(cut + 1), scopes: list });
255
+ }
256
+
257
+ export {
258
+ MAX_FILE,
259
+ lockTimeoutFrom,
260
+ FileStore,
261
+ operatorActor
262
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ interface CliIo {
3
+ out(line: string): void;
4
+ err(line: string): void;
5
+ env: Record<string, string | undefined>;
6
+ }
7
+ interface Parsed {
8
+ command: string;
9
+ id?: string;
10
+ flags: Record<string, string>;
11
+ many: Record<string, string[]>;
12
+ }
13
+ declare function parseArgs(argv: string[]): Parsed;
14
+ declare function runCli(argv: string[], io: CliIo): Promise<number>;
15
+
16
+ export { type CliIo, parseArgs, runCli };