agentic-comments 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +272 -2
- package/SKILL.md +35 -0
- package/dist/chunk-C3P5TAC3.js +770 -0
- package/dist/chunk-C43HLFQE.js +262 -0
- package/dist/chunk-RWMBGIAS.js +7 -0
- package/dist/chunk-UE5ULEXU.js +86 -0
- package/dist/chunk-ZCM2UDLN.js +172 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +205 -0
- package/dist/index.d.ts +105 -0
- package/dist/index.js +223 -0
- package/dist/local.d.ts +112 -0
- package/dist/local.js +23 -0
- package/dist/mcp-BOJM5NE6.js +11 -0
- package/dist/mcp-bin.d.ts +1 -0
- package/dist/mcp-bin.js +21 -0
- package/dist/suggestions-U8tXRLZ5.d.ts +334 -0
- package/llms.txt +15 -0
- package/package.json +76 -4
package/dist/cli.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
FileStore,
|
|
4
|
+
lockTimeoutFrom,
|
|
5
|
+
operatorActor
|
|
6
|
+
} from "./chunk-C43HLFQE.js";
|
|
7
|
+
import {
|
|
8
|
+
VERSION
|
|
9
|
+
} from "./chunk-RWMBGIAS.js";
|
|
10
|
+
import {
|
|
11
|
+
CommentsService,
|
|
12
|
+
TEXT_ROOT
|
|
13
|
+
} from "./chunk-ZCM2UDLN.js";
|
|
14
|
+
import {
|
|
15
|
+
CommentsError
|
|
16
|
+
} from "./chunk-C3P5TAC3.js";
|
|
17
|
+
|
|
18
|
+
// src/cli.ts
|
|
19
|
+
import { readFileSync, realpathSync } from "fs";
|
|
20
|
+
import { pathToFileURL } from "url";
|
|
21
|
+
var UsageError = class extends Error {
|
|
22
|
+
};
|
|
23
|
+
var FLAGS = {
|
|
24
|
+
"--file": "value",
|
|
25
|
+
"--as": "value",
|
|
26
|
+
"--body": "value",
|
|
27
|
+
"--mention": "many",
|
|
28
|
+
"--text-quote": "value",
|
|
29
|
+
"--occurrence": "value",
|
|
30
|
+
"--cell": "value",
|
|
31
|
+
"--slide": "value",
|
|
32
|
+
"--canvas": "value",
|
|
33
|
+
"--replace": "value",
|
|
34
|
+
"--value": "value",
|
|
35
|
+
"--summary": "value",
|
|
36
|
+
"--status": "value",
|
|
37
|
+
"--anchor-state": "value",
|
|
38
|
+
"--mentioning": "value",
|
|
39
|
+
"--text": "value",
|
|
40
|
+
"--text-file": "value"
|
|
41
|
+
};
|
|
42
|
+
var SHARED = ["--file", "--as"];
|
|
43
|
+
var ANCHOR = ["--text-quote", "--occurrence", "--cell", "--slide", "--canvas"];
|
|
44
|
+
var ALLOWED = {
|
|
45
|
+
init: ["--file", "--text", "--text-file"],
|
|
46
|
+
list: [...SHARED, "--status", "--anchor-state", "--mentioning"],
|
|
47
|
+
show: SHARED,
|
|
48
|
+
comment: [...SHARED, ...ANCHOR, "--body", "--mention"],
|
|
49
|
+
reply: [...SHARED, "--body", "--mention"],
|
|
50
|
+
resolve: SHARED,
|
|
51
|
+
reopen: SHARED,
|
|
52
|
+
relocate: SHARED,
|
|
53
|
+
suggestions: [...SHARED, "--status", "--anchor-state"],
|
|
54
|
+
suggest: [...SHARED, ...ANCHOR, "--replace", "--value", "--summary"],
|
|
55
|
+
accept: SHARED,
|
|
56
|
+
reject: SHARED,
|
|
57
|
+
mcp: SHARED
|
|
58
|
+
};
|
|
59
|
+
var TAKES_ID = ["show", "reply", "resolve", "reopen", "relocate", "accept", "reject"];
|
|
60
|
+
var COMMANDS = ["init", "list", "show", "comment", "reply", "resolve", "reopen", "relocate", "suggestions", "suggest", "accept", "reject", "mcp"];
|
|
61
|
+
function parseArgs(argv) {
|
|
62
|
+
const [command, ...rest] = argv;
|
|
63
|
+
if (command === void 0 || command === "--help" || command === "-h") return { command: "help", flags: {}, many: {} };
|
|
64
|
+
if (command === "--version" || command === "-V") return { command: "version", flags: {}, many: {} };
|
|
65
|
+
if (!COMMANDS.includes(command)) throw new UsageError(`unknown command "${command}"`);
|
|
66
|
+
const parsed = { command, flags: {}, many: {} };
|
|
67
|
+
for (let i = 0; i < rest.length; i++) {
|
|
68
|
+
const arg = rest[i];
|
|
69
|
+
if (!arg.startsWith("--")) {
|
|
70
|
+
if (parsed.id !== void 0) throw new UsageError(`unexpected argument "${arg}"`);
|
|
71
|
+
parsed.id = arg;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const kind = FLAGS[arg];
|
|
75
|
+
if (!kind) throw new UsageError(`unknown flag "${arg}"`);
|
|
76
|
+
const value = rest[++i];
|
|
77
|
+
if (value === void 0) throw new UsageError(`${arg} needs a value`);
|
|
78
|
+
if (kind === "many") (parsed.many[arg] ??= []).push(value);
|
|
79
|
+
else if (arg in parsed.flags) throw new UsageError(`${arg} was given twice`);
|
|
80
|
+
else parsed.flags[arg] = value;
|
|
81
|
+
}
|
|
82
|
+
const used = [...Object.keys(parsed.flags), ...Object.keys(parsed.many)];
|
|
83
|
+
const stray = used.find((f) => !ALLOWED[command].includes(f));
|
|
84
|
+
if (stray) throw new UsageError(`${stray} does not apply to ${command}`);
|
|
85
|
+
if (parsed.id !== void 0 && !TAKES_ID.includes(command)) throw new UsageError(`${command} takes no id`);
|
|
86
|
+
if (parsed.flags["--text"] !== void 0 && parsed.flags["--text-file"] !== void 0) throw new UsageError("give only one of --text, --text-file");
|
|
87
|
+
const occurrence = parsed.flags["--occurrence"];
|
|
88
|
+
if (occurrence !== void 0 && !/^[1-9]\d{0,8}$/.test(occurrence)) throw new UsageError("--occurrence must be a whole number, 1 or more");
|
|
89
|
+
return parsed;
|
|
90
|
+
}
|
|
91
|
+
function anchorOf(p) {
|
|
92
|
+
const given = ["--text-quote", "--cell", "--slide", "--canvas"].filter((f2) => p.flags[f2] !== void 0);
|
|
93
|
+
if (given.length !== 1) throw new UsageError("give exactly one of --text-quote, --cell, --slide, --canvas");
|
|
94
|
+
const f = given[0];
|
|
95
|
+
if (f === "--text-quote") {
|
|
96
|
+
const occurrence = p.flags["--occurrence"];
|
|
97
|
+
return { type: "text", quote: p.flags[f], ...occurrence === void 0 ? {} : { occurrence: Number(occurrence) } };
|
|
98
|
+
}
|
|
99
|
+
return f === "--cell" ? { type: "cell", key: p.flags[f] } : { type: f === "--slide" ? "slide" : "canvas", elementId: p.flags[f] };
|
|
100
|
+
}
|
|
101
|
+
function changeOf(p) {
|
|
102
|
+
const { "--replace": replace, "--value": value } = p.flags;
|
|
103
|
+
if (replace === void 0 === (value === void 0)) throw new UsageError("give exactly one of --replace, --value");
|
|
104
|
+
if (replace !== void 0) return { replace };
|
|
105
|
+
try {
|
|
106
|
+
return { value: JSON.parse(value) };
|
|
107
|
+
} catch {
|
|
108
|
+
throw new UsageError("--value must be JSON");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function need(p) {
|
|
112
|
+
if (p.id === void 0) throw new UsageError(`${p.command} needs an id`);
|
|
113
|
+
return p.id;
|
|
114
|
+
}
|
|
115
|
+
var HELP = `agentic-comments ${VERSION}
|
|
116
|
+
Comment threads and suggestions anchored to any content, from people and agents.
|
|
117
|
+
|
|
118
|
+
Usage: agentic-comments <command> [flags]
|
|
119
|
+
|
|
120
|
+
Setup:
|
|
121
|
+
--file <path> The store file. Or AGENTIC_COMMENTS_FILE. Default ./comments.ydoc.
|
|
122
|
+
--as <kind:name> Who acts. Or AGENTIC_COMMENTS_AS. Kinds: human, agent_of_human, agent_of_agent, agent, user_token.
|
|
123
|
+
Scopes come only from AGENTIC_COMMENTS_SCOPES, a comma list such as comments:accept.
|
|
124
|
+
|
|
125
|
+
Commands:
|
|
126
|
+
init [--text <t> | --text-file <p>] Create the store.
|
|
127
|
+
list [--status s] [--anchor-state s] [--mentioning h] List threads.
|
|
128
|
+
show <id> Show one thread.
|
|
129
|
+
comment <anchor> --body <t> [--mention h] Start a thread.
|
|
130
|
+
reply <id> --body <t> [--mention h] Reply.
|
|
131
|
+
resolve <id> | reopen <id> | relocate <id>
|
|
132
|
+
suggestions [--status s] [--anchor-state s] List suggestions.
|
|
133
|
+
suggest <anchor> (--replace <t> | --value <json>) [--summary <t>]
|
|
134
|
+
accept <id> | reject <id>
|
|
135
|
+
mcp Start the MCP server on stdio.
|
|
136
|
+
|
|
137
|
+
An anchor is one of: --text-quote <q> [--occurrence n], --cell <key>, --slide <id>, --canvas <id>.
|
|
138
|
+
Every result is JSON. An error is JSON on stderr, and the exit code is 1. A usage error exits 2.`;
|
|
139
|
+
async function runCli(argv, io) {
|
|
140
|
+
let p;
|
|
141
|
+
try {
|
|
142
|
+
p = parseArgs(argv);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
io.err(JSON.stringify({ error: { code: "USAGE", message: error.message } }));
|
|
145
|
+
return 2;
|
|
146
|
+
}
|
|
147
|
+
if (p.command === "help") return io.out(HELP), 0;
|
|
148
|
+
if (p.command === "version") return io.out(VERSION), 0;
|
|
149
|
+
try {
|
|
150
|
+
const store = new FileStore(p.flags["--file"] ?? io.env.AGENTIC_COMMENTS_FILE ?? "./comments.ydoc", { lockTimeoutMs: lockTimeoutFrom(io.env) });
|
|
151
|
+
if (p.command === "init") {
|
|
152
|
+
const seed = p.flags["--text-file"] !== void 0 ? readFileSync(p.flags["--text-file"], "utf8") : p.flags["--text"] ?? "";
|
|
153
|
+
store.create((ydoc) => ydoc.getText(TEXT_ROOT).insert(0, seed));
|
|
154
|
+
io.out(JSON.stringify({ created: store.path }));
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
if (p.command === "mcp") {
|
|
158
|
+
const { startMcp } = await import("./mcp-BOJM5NE6.js");
|
|
159
|
+
await startMcp(store, operatorActor(p.flags["--as"] ?? io.env.AGENTIC_COMMENTS_AS, io.env.AGENTIC_COMMENTS_SCOPES));
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
const service = new CommentsService(store, operatorActor(p.flags["--as"] ?? io.env.AGENTIC_COMMENTS_AS, io.env.AGENTIC_COMMENTS_SCOPES));
|
|
163
|
+
const mentions = p.many["--mention"];
|
|
164
|
+
const filter = { status: p.flags["--status"], anchorState: p.flags["--anchor-state"], mentioning: p.flags["--mentioning"] };
|
|
165
|
+
const clean = Object.fromEntries(Object.entries(filter).filter(([, v]) => v !== void 0));
|
|
166
|
+
const result = p.command === "list" ? service.threads(clean) : p.command === "show" ? service.thread(need(p)) : p.command === "comment" ? service.comment({ anchor: anchorOf(p), body: p.flags["--body"], ...mentions ? { mentions } : {} }) : p.command === "reply" ? service.reply(need(p), { body: p.flags["--body"], ...mentions ? { mentions } : {} }) : p.command === "resolve" ? service.resolve(need(p)) : p.command === "reopen" ? service.reopen(need(p)) : p.command === "relocate" ? service.relocate(need(p)) : p.command === "suggestions" ? service.suggestions(Object.fromEntries(Object.entries(clean).filter(([k]) => k !== "mentioning"))) : p.command === "suggest" ? service.suggest({ anchor: anchorOf(p), change: changeOf(p), ...p.flags["--summary"] === void 0 ? {} : { summary: p.flags["--summary"] } }) : p.command === "accept" ? service.accept(need(p)) : service.reject(need(p));
|
|
167
|
+
io.out(JSON.stringify(result));
|
|
168
|
+
return 0;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error instanceof UsageError) {
|
|
171
|
+
io.err(JSON.stringify({ error: { code: "USAGE", message: error.message } }));
|
|
172
|
+
return 2;
|
|
173
|
+
}
|
|
174
|
+
if (error instanceof CommentsError) {
|
|
175
|
+
io.err(JSON.stringify({ error: { code: error.code, message: error.message } }));
|
|
176
|
+
return error.code === "USAGE" ? 2 : 1;
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function isMainModule() {
|
|
182
|
+
const invoked = process.argv[1];
|
|
183
|
+
if (typeof invoked !== "string") return false;
|
|
184
|
+
try {
|
|
185
|
+
return import.meta.url === pathToFileURL(realpathSync(invoked)).href;
|
|
186
|
+
} catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (isMainModule()) {
|
|
191
|
+
process.on("SIGXFSZ", () => void 0);
|
|
192
|
+
runCli(process.argv.slice(2), { out: (l) => console.log(l), err: (l) => console.error(l), env: process.env }).then(
|
|
193
|
+
(code) => {
|
|
194
|
+
if (code !== 0) process.exit(code);
|
|
195
|
+
},
|
|
196
|
+
(error) => {
|
|
197
|
+
console.error(JSON.stringify({ error: { code: "INTERNAL", message: error instanceof Error ? error.message : String(error) } }));
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
export {
|
|
203
|
+
parseArgs,
|
|
204
|
+
runCli
|
|
205
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { A as Author, C as Comments, S as Suggestions } from './suggestions-U8tXRLZ5.js';
|
|
2
|
+
export { a as ACCEPT_SCOPE, b as Actor, c as Anchor, d as AnchorKind, e as AnchorState, f as ApplyContext, g as CanvasAnchor, h as CellAnchor, i as CommentsOptions, M as Message, N as NewReply, j as NewSuggestion, k as NewThread, P as PRINCIPAL_KINDS, l as PrincipalKind, R as RelativePositionJson, m as RevertContext, n as SlideAnchor, o as StampInput, p as Suggestion, q as SuggestionFilter, r as SuggestionHost, T as TextAnchor, s as Thread, t as ThreadFilter, u as authorLabel, v as stamp, w as textAnchor, x as textOf } from './suggestions-U8tXRLZ5.js';
|
|
3
|
+
import 'yjs';
|
|
4
|
+
|
|
5
|
+
declare const VERSION: string;
|
|
6
|
+
|
|
7
|
+
type CommentsErrorCode = "INVALID_AUTHOR" | "INVALID_INPUT" | "INVALID_ANCHOR" | "NO_THREAD" | "THREAD_RESOLVED" | "CORRUPT" | "NO_SUGGESTION" | "FORBIDDEN" | "ALREADY_DECIDED" | "APPLY_FAILED" | "RELOCATE_FAILED" | "STORE_MISSING" | "STORE_EXISTS" | "STORE_CORRUPT" | "STORE_LOCKED" | "STORE_FULL" | "USAGE";
|
|
8
|
+
declare class CommentsError extends Error {
|
|
9
|
+
readonly code: CommentsErrorCode;
|
|
10
|
+
constructor(code: CommentsErrorCode, message: string);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type CommentEventType = "thread.created" | "message.added" | "mention" | "thread.resolved" | "thread.reopened" | "suggestion.created" | "suggestion.accepted" | "suggestion.rejected";
|
|
14
|
+
/**
|
|
15
|
+
* One change, in terms a notifier can route. It holds ids and never the text of a comment, a suggestion, or
|
|
16
|
+
* a change, so a log or a queue that stores it holds no content. Fetch the content with the ids.
|
|
17
|
+
*/
|
|
18
|
+
interface CommentEvent {
|
|
19
|
+
/**
|
|
20
|
+
* The same change has the same id on every replica, so a notifier can drop a repeat. A replica also drops
|
|
21
|
+
* a repeat it has seen, within its last `seenLimit` ids. Two accepts at once give one id with the actor of
|
|
22
|
+
* whichever replica you hear first, and the winner can differ. A host clock coarser than the rate of status
|
|
23
|
+
* changes can give two status events one id.
|
|
24
|
+
*/
|
|
25
|
+
id: string;
|
|
26
|
+
type: CommentEventType;
|
|
27
|
+
/** When the change was made, as the data records it. */
|
|
28
|
+
at: string;
|
|
29
|
+
/** `local` when this replica made the change. `remote` when it came in from another replica. */
|
|
30
|
+
source: "local" | "remote";
|
|
31
|
+
/** Who made the change. `machineMade` is true for every kind but a signed-in person. */
|
|
32
|
+
actor: Author & {
|
|
33
|
+
machineMade: boolean;
|
|
34
|
+
};
|
|
35
|
+
/** What kind of content the thread or suggestion points at: text, cell, slide, canvas, or a custom kind. */
|
|
36
|
+
anchorType: string;
|
|
37
|
+
threadId?: string;
|
|
38
|
+
messageId?: string;
|
|
39
|
+
suggestionId?: string;
|
|
40
|
+
/** For a `mention` event: the handle that was named. */
|
|
41
|
+
mention?: string;
|
|
42
|
+
}
|
|
43
|
+
type CommentListener = (event: CommentEvent) => void | Promise<void>;
|
|
44
|
+
interface NotificationsOptions {
|
|
45
|
+
/**
|
|
46
|
+
* Called when a listener throws or rejects (`source` is `listener`), or when a change in the data cannot
|
|
47
|
+
* be read (`source` is `data`). Without it, errors go to `errors()`.
|
|
48
|
+
*/
|
|
49
|
+
onError?: (error: unknown, source: "listener" | "data") => void;
|
|
50
|
+
/** Drop every event until `unmute()`. Create the stream muted, and unmute after the first load, so old history is not sent. */
|
|
51
|
+
muted?: boolean;
|
|
52
|
+
/** How many event ids to remember so a repeat is dropped. A whole number, 1 or more. Default 10,000. */
|
|
53
|
+
seenLimit?: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* An event stream for a notifier. It watches the thread and suggestion data, so it sees changes from other
|
|
57
|
+
* replicas too, and marks each as local or remote. It replays nothing it saw before it was created, but a first
|
|
58
|
+
* load into an empty document looks like new changes: create it muted and call `unmute()` after that load.
|
|
59
|
+
* Each id is delivered at most once per replica within the `seenLimit` window, and a listener that throws is
|
|
60
|
+
* not retried. A listener cannot break a write. A message past the read limit
|
|
61
|
+
* of a thread makes no event.
|
|
62
|
+
*/
|
|
63
|
+
declare class Notifications {
|
|
64
|
+
private readonly comments;
|
|
65
|
+
private readonly suggestions?;
|
|
66
|
+
private readonly options;
|
|
67
|
+
private readonly listeners;
|
|
68
|
+
private readonly seen;
|
|
69
|
+
private readonly failures;
|
|
70
|
+
private readonly stops;
|
|
71
|
+
private readonly queue;
|
|
72
|
+
private flushing;
|
|
73
|
+
private closed;
|
|
74
|
+
private readonly seenLimit;
|
|
75
|
+
private muted;
|
|
76
|
+
constructor(comments: Comments, suggestions?: Suggestions | undefined, options?: NotificationsOptions);
|
|
77
|
+
/** Receive every event from now on. Returns a function that stops it. */
|
|
78
|
+
subscribe(listener: CommentListener): () => void;
|
|
79
|
+
/** Start sending events, for a stream created with `muted: true`. */
|
|
80
|
+
unmute(): void;
|
|
81
|
+
/** Stop watching the document and drop every listener. */
|
|
82
|
+
close(): void;
|
|
83
|
+
/** The most recent errors, when no `onError` was given. */
|
|
84
|
+
errors(): {
|
|
85
|
+
source: "listener" | "data";
|
|
86
|
+
error: unknown;
|
|
87
|
+
}[];
|
|
88
|
+
private fail;
|
|
89
|
+
/** Data from a peer can be bad. A bad record makes no event, and never stops a write or the records after it. */
|
|
90
|
+
private guard;
|
|
91
|
+
private emit;
|
|
92
|
+
private flush;
|
|
93
|
+
private threadEvents;
|
|
94
|
+
private mentions;
|
|
95
|
+
private messageEvent;
|
|
96
|
+
private statusEvent;
|
|
97
|
+
private threadCreated;
|
|
98
|
+
private messagesAdded;
|
|
99
|
+
private statusChanged;
|
|
100
|
+
private created;
|
|
101
|
+
private decisionEvent;
|
|
102
|
+
private decided;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export { Author, type CommentEvent, type CommentEventType, type CommentListener, Comments, CommentsError, type CommentsErrorCode, Notifications, type NotificationsOptions, Suggestions, VERSION };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import {
|
|
2
|
+
VERSION
|
|
3
|
+
} from "./chunk-RWMBGIAS.js";
|
|
4
|
+
import {
|
|
5
|
+
ACCEPT_SCOPE,
|
|
6
|
+
Actor,
|
|
7
|
+
Comments,
|
|
8
|
+
CommentsError,
|
|
9
|
+
PRINCIPAL_KINDS,
|
|
10
|
+
Suggestions,
|
|
11
|
+
authorLabel,
|
|
12
|
+
readAuthor,
|
|
13
|
+
stamp,
|
|
14
|
+
textAnchor,
|
|
15
|
+
textOf,
|
|
16
|
+
validDate
|
|
17
|
+
} from "./chunk-C3P5TAC3.js";
|
|
18
|
+
|
|
19
|
+
// src/notifications.ts
|
|
20
|
+
import * as Y from "yjs";
|
|
21
|
+
var DEFAULT_SEEN = 1e4;
|
|
22
|
+
var ERROR_LIMIT = 100;
|
|
23
|
+
var actorOf = (author) => ({ kind: author.kind, name: author.name, machineMade: author.kind !== "human" });
|
|
24
|
+
var Notifications = class {
|
|
25
|
+
constructor(comments, suggestions, options = {}) {
|
|
26
|
+
this.comments = comments;
|
|
27
|
+
this.suggestions = suggestions;
|
|
28
|
+
this.options = options;
|
|
29
|
+
this.seenLimit = options.seenLimit ?? DEFAULT_SEEN;
|
|
30
|
+
if (!Number.isSafeInteger(this.seenLimit) || this.seenLimit < 1) throw new TypeError("seenLimit must be a whole number, 1 or more");
|
|
31
|
+
this.muted = options.muted === true;
|
|
32
|
+
const flush = () => this.flush();
|
|
33
|
+
comments.ydoc.on("afterAllTransactions", flush);
|
|
34
|
+
this.stops.push(() => comments.ydoc.off("afterAllTransactions", flush));
|
|
35
|
+
const threads = comments.threadsMap;
|
|
36
|
+
const onThreads = (events, tr) => this.threadEvents(events, tr.local ? "local" : "remote");
|
|
37
|
+
threads.observeDeep(onThreads);
|
|
38
|
+
this.stops.push(() => threads.unobserveDeep(onThreads));
|
|
39
|
+
if (suggestions) {
|
|
40
|
+
const { suggestions: made, applied, rejected } = suggestions.maps;
|
|
41
|
+
const onMade = (event, tr) => this.created(event, tr.local ? "local" : "remote");
|
|
42
|
+
const onApplied = (event, tr) => this.decided(event, tr.local ? "local" : "remote", "suggestion.accepted");
|
|
43
|
+
const onRejected = (event, tr) => this.decided(event, tr.local ? "local" : "remote", "suggestion.rejected");
|
|
44
|
+
made.observe(onMade);
|
|
45
|
+
applied.observe(onApplied);
|
|
46
|
+
rejected.observe(onRejected);
|
|
47
|
+
this.stops.push(() => made.unobserve(onMade), () => applied.unobserve(onApplied), () => rejected.unobserve(onRejected));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
comments;
|
|
51
|
+
suggestions;
|
|
52
|
+
options;
|
|
53
|
+
listeners = /* @__PURE__ */ new Set();
|
|
54
|
+
seen = /* @__PURE__ */ new Set();
|
|
55
|
+
failures = [];
|
|
56
|
+
stops = [];
|
|
57
|
+
queue = [];
|
|
58
|
+
flushing = false;
|
|
59
|
+
closed = false;
|
|
60
|
+
seenLimit;
|
|
61
|
+
muted;
|
|
62
|
+
/** Receive every event from now on. Returns a function that stops it. */
|
|
63
|
+
subscribe(listener) {
|
|
64
|
+
if (typeof listener !== "function") throw new TypeError("a listener must be a function");
|
|
65
|
+
this.listeners.add(listener);
|
|
66
|
+
return () => void this.listeners.delete(listener);
|
|
67
|
+
}
|
|
68
|
+
/** Start sending events, for a stream created with `muted: true`. */
|
|
69
|
+
unmute() {
|
|
70
|
+
this.muted = false;
|
|
71
|
+
}
|
|
72
|
+
/** Stop watching the document and drop every listener. */
|
|
73
|
+
close() {
|
|
74
|
+
this.closed = true;
|
|
75
|
+
for (const stop of this.stops.splice(0)) stop();
|
|
76
|
+
this.listeners.clear();
|
|
77
|
+
this.queue.length = 0;
|
|
78
|
+
}
|
|
79
|
+
/** The most recent errors, when no `onError` was given. */
|
|
80
|
+
errors() {
|
|
81
|
+
return [...this.failures];
|
|
82
|
+
}
|
|
83
|
+
fail(error, source) {
|
|
84
|
+
try {
|
|
85
|
+
if (this.options.onError) this.options.onError(error, source);
|
|
86
|
+
else if (this.failures.push({ source, error }) > ERROR_LIMIT) this.failures.shift();
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Data from a peer can be bad. A bad record makes no event, and never stops a write or the records after it. */
|
|
91
|
+
guard(fn) {
|
|
92
|
+
try {
|
|
93
|
+
fn();
|
|
94
|
+
} catch (error) {
|
|
95
|
+
this.fail(error, "data");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
emit(event) {
|
|
99
|
+
if (this.seen.has(event.id)) return;
|
|
100
|
+
this.seen.add(event.id);
|
|
101
|
+
if (this.seen.size > this.seenLimit) this.seen.delete(this.seen.values().next().value);
|
|
102
|
+
if (!this.muted) this.queue.push(event);
|
|
103
|
+
}
|
|
104
|
+
flush() {
|
|
105
|
+
if (this.flushing) return;
|
|
106
|
+
this.flushing = true;
|
|
107
|
+
try {
|
|
108
|
+
for (let event = this.queue.shift(); event; event = this.queue.shift()) {
|
|
109
|
+
for (const listener of [...this.listeners]) {
|
|
110
|
+
if (this.closed) return;
|
|
111
|
+
try {
|
|
112
|
+
const result = listener(event);
|
|
113
|
+
if (typeof result?.then === "function") Promise.resolve(result).catch((error) => this.fail(error, "listener"));
|
|
114
|
+
} catch (error) {
|
|
115
|
+
this.fail(error, "listener");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
this.flushing = false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
threadEvents(events, source) {
|
|
124
|
+
const threads = this.comments.threadsMap;
|
|
125
|
+
for (const event of events) {
|
|
126
|
+
this.guard(() => {
|
|
127
|
+
const path = event.path;
|
|
128
|
+
if (event.target === threads) {
|
|
129
|
+
for (const [id, change] of event.keys) if (change.action === "add") this.guard(() => this.threadCreated(id, source));
|
|
130
|
+
} else if (path.length === 1 && event.target instanceof Y.Map) {
|
|
131
|
+
const keys = event.keys;
|
|
132
|
+
if (keys.get("messages")?.action === "add") this.threadCreated(String(path[0]), source);
|
|
133
|
+
const change = keys.get("state");
|
|
134
|
+
if (change) this.statusChanged(String(path[0]), change.oldValue, source);
|
|
135
|
+
} else if (path.length === 2 && path[1] === "messages" && event.target instanceof Y.Array) {
|
|
136
|
+
const ids = /* @__PURE__ */ new Set();
|
|
137
|
+
for (const item of event.changes.added) for (const raw of item.content.getContent()) ids.add(raw?.id);
|
|
138
|
+
this.messagesAdded(String(path[0]), ids, source);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
mentions(thread, message, source) {
|
|
144
|
+
for (const handle of message.mentions) {
|
|
145
|
+
this.emit({ id: `mention:${thread.id}:${message.id}:${handle}`, type: "mention", at: message.at, source, actor: actorOf(message.author), anchorType: thread.anchor.type, threadId: thread.id, messageId: message.id, mention: handle });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
messageEvent(thread, message, source) {
|
|
149
|
+
this.emit({ id: `message.added:${thread.id}:${message.id}`, type: "message.added", at: message.at, source, actor: actorOf(message.author), anchorType: thread.anchor.type, threadId: thread.id, messageId: message.id });
|
|
150
|
+
this.mentions(thread, message, source);
|
|
151
|
+
}
|
|
152
|
+
statusEvent(thread, source) {
|
|
153
|
+
const type = thread.status === "resolved" ? "thread.resolved" : "thread.reopened";
|
|
154
|
+
this.emit({ id: `${type}:${thread.id}:${thread.statusAt}:${thread.statusBy.kind}:${thread.statusBy.name}`, type, at: thread.statusAt, source, actor: actorOf(thread.statusBy), anchorType: thread.anchor.type, threadId: thread.id });
|
|
155
|
+
}
|
|
156
|
+
threadCreated(id, source) {
|
|
157
|
+
const thread = this.comments.get(id);
|
|
158
|
+
const stored = this.comments.threadsMap.get(id)?.get("messages");
|
|
159
|
+
const firstId = stored instanceof Y.Array ? stored.get(0)?.id : void 0;
|
|
160
|
+
const first = thread.messages.find((m) => m.id === firstId);
|
|
161
|
+
if (first) {
|
|
162
|
+
this.emit({ id: `thread.created:${id}`, type: "thread.created", at: first.at, source, actor: actorOf(first.author), anchorType: thread.anchor.type, threadId: id, messageId: first.id });
|
|
163
|
+
this.mentions(thread, first, source);
|
|
164
|
+
} else {
|
|
165
|
+
this.fail(new Error(`thread ${JSON.stringify(id.slice(0, 64))} has no valid first message, so it makes no thread.created event`), "data");
|
|
166
|
+
}
|
|
167
|
+
for (const message of thread.messages) if (message !== first) this.messageEvent(thread, message, source);
|
|
168
|
+
if (thread.status === "resolved") this.statusEvent(thread, source);
|
|
169
|
+
}
|
|
170
|
+
messagesAdded(threadId, ids, source) {
|
|
171
|
+
const thread = this.comments.get(threadId);
|
|
172
|
+
for (const message of thread.messages) if (ids.has(message.id)) this.messageEvent(thread, message, source);
|
|
173
|
+
}
|
|
174
|
+
statusChanged(threadId, before, source) {
|
|
175
|
+
const thread = this.comments.get(threadId);
|
|
176
|
+
const old = before ?? {};
|
|
177
|
+
const real = (old.status === "open" || old.status === "resolved") && readAuthor(old.by) !== void 0;
|
|
178
|
+
if (real && old.status !== thread.status) this.statusEvent(thread, source);
|
|
179
|
+
}
|
|
180
|
+
created(event, source) {
|
|
181
|
+
for (const [id, change] of event.keys) {
|
|
182
|
+
if (change.action !== "add") continue;
|
|
183
|
+
this.guard(() => {
|
|
184
|
+
const s = this.suggestions.get(id);
|
|
185
|
+
this.emit({ id: `suggestion.created:${id}`, type: "suggestion.created", at: s.createdAt, source, actor: actorOf(s.author), anchorType: s.anchor.type, suggestionId: id });
|
|
186
|
+
if (s.status !== "pending") this.decisionEvent(s, source);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
decisionEvent(s, source) {
|
|
191
|
+
if (s.status === "pending" || !s.decidedBy || !s.decidedAt) return;
|
|
192
|
+
const type = s.status === "accepted" ? "suggestion.accepted" : "suggestion.rejected";
|
|
193
|
+
this.emit({ id: `${type}:${s.id}`, type, at: s.decidedAt, source, actor: actorOf(s.decidedBy), anchorType: s.anchor.type, suggestionId: s.id });
|
|
194
|
+
}
|
|
195
|
+
decided(event, source, type) {
|
|
196
|
+
for (const [key, change] of event.keys) {
|
|
197
|
+
if (change.action !== "add") continue;
|
|
198
|
+
try {
|
|
199
|
+
const raw = event.target.get(key);
|
|
200
|
+
if (!raw || readAuthor(raw.by) === void 0 || !validDate(raw.at)) throw new Error("a decision record is not valid");
|
|
201
|
+
const id = key.slice(0, key.lastIndexOf(":"));
|
|
202
|
+
const s = this.suggestions.get(id);
|
|
203
|
+
if (s.status === (type === "suggestion.accepted" ? "accepted" : "rejected")) this.decisionEvent(s, source);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
this.fail(error, "data");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
export {
|
|
211
|
+
ACCEPT_SCOPE,
|
|
212
|
+
Actor,
|
|
213
|
+
Comments,
|
|
214
|
+
CommentsError,
|
|
215
|
+
Notifications,
|
|
216
|
+
PRINCIPAL_KINDS,
|
|
217
|
+
Suggestions,
|
|
218
|
+
VERSION,
|
|
219
|
+
authorLabel,
|
|
220
|
+
stamp,
|
|
221
|
+
textAnchor,
|
|
222
|
+
textOf
|
|
223
|
+
};
|
package/dist/local.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
import { b as Actor, s as Thread, M as Message, p as Suggestion, r as SuggestionHost } from './suggestions-U8tXRLZ5.js';
|
|
3
|
+
|
|
4
|
+
/** The most bytes a saved document holds. A save over this is refused, so a file never grows past what a load accepts. */
|
|
5
|
+
declare const MAX_FILE: number;
|
|
6
|
+
interface FileStoreOptions {
|
|
7
|
+
/** How long a write waits for another process. A whole number of ms, 0 or more. Default 5,000. */
|
|
8
|
+
lockTimeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
/** Read the lock timeout from `AGENTIC_COMMENTS_LOCK_TIMEOUT_MS`. Anything but a whole number of ms is an error. */
|
|
11
|
+
declare function lockTimeoutFrom(env: Record<string, string | undefined>): number | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* One file that holds a Yjs document, with a hash so damage is seen. A write takes a lock file, loads the
|
|
14
|
+
* newest state, runs, and saves with a rename. If the file changed while it ran, it runs again on the new
|
|
15
|
+
* state. This is built for processes on one machine that share one process table. A lock held across
|
|
16
|
+
* containers or machines cannot be checked for a dead holder.
|
|
17
|
+
*/
|
|
18
|
+
declare class FileStore {
|
|
19
|
+
readonly path: string;
|
|
20
|
+
private readonly lockTimeoutMs;
|
|
21
|
+
constructor(path: string, options?: FileStoreOptions);
|
|
22
|
+
/** Create the file. `seed` fills the document before the first save. Refuses to overwrite, even when two run at once. */
|
|
23
|
+
create(seed?: (ydoc: Y.Doc) => void): void;
|
|
24
|
+
private load;
|
|
25
|
+
/** Read the newest saved state. Anything `fn` changes is not saved. */
|
|
26
|
+
read<T>(fn: (ydoc: Y.Doc) => T): T;
|
|
27
|
+
private acquire;
|
|
28
|
+
/**
|
|
29
|
+
* Clear a lock whose holder is dead (or is this process, with another mark), an empty one older than a
|
|
30
|
+
* short time, or any one more than a minute old or a minute in the future. The lock moves to a unique name
|
|
31
|
+
* first, so only one process clears it. If the moved lock is not the one we looked at, it goes back.
|
|
32
|
+
*/
|
|
33
|
+
private clearStale;
|
|
34
|
+
/** Lock, load the newest state, run `fn`, and save. When `fn` throws, nothing is saved. */
|
|
35
|
+
write<T>(fn: (ydoc: Y.Doc) => T): T;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Where a comment or suggestion points, in terms a person or agent can write down. */
|
|
39
|
+
type AnchorSpec = {
|
|
40
|
+
type: "text";
|
|
41
|
+
quote: string;
|
|
42
|
+
occurrence?: number;
|
|
43
|
+
} | {
|
|
44
|
+
type: "cell";
|
|
45
|
+
key: string;
|
|
46
|
+
} | {
|
|
47
|
+
type: "slide";
|
|
48
|
+
elementId: string;
|
|
49
|
+
} | {
|
|
50
|
+
type: "canvas";
|
|
51
|
+
elementId: string;
|
|
52
|
+
};
|
|
53
|
+
interface ListFilter {
|
|
54
|
+
status?: string;
|
|
55
|
+
anchorState?: string;
|
|
56
|
+
mentioning?: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Every action the CLI and MCP server offer, on a local file store. Each call takes the actor the operator
|
|
60
|
+
* stamped, and no input carries an author, so both faces return the same shapes and obey the same rules.
|
|
61
|
+
*/
|
|
62
|
+
declare class CommentsService {
|
|
63
|
+
private readonly store;
|
|
64
|
+
private readonly actor;
|
|
65
|
+
constructor(store: FileStore, actor: Actor);
|
|
66
|
+
private open;
|
|
67
|
+
private read;
|
|
68
|
+
private write;
|
|
69
|
+
threads(filter?: ListFilter): Thread[];
|
|
70
|
+
thread(id: string): Thread;
|
|
71
|
+
comment(input: {
|
|
72
|
+
anchor: AnchorSpec;
|
|
73
|
+
body: string;
|
|
74
|
+
mentions?: string[];
|
|
75
|
+
}): Thread;
|
|
76
|
+
reply(threadId: string, input: {
|
|
77
|
+
body: string;
|
|
78
|
+
mentions?: string[];
|
|
79
|
+
}): Message;
|
|
80
|
+
resolve(threadId: string): Thread;
|
|
81
|
+
reopen(threadId: string): Thread;
|
|
82
|
+
relocate(threadId: string): Thread;
|
|
83
|
+
suggestions(filter?: ListFilter): Suggestion[];
|
|
84
|
+
suggest(input: {
|
|
85
|
+
anchor: AnchorSpec;
|
|
86
|
+
change: unknown;
|
|
87
|
+
summary?: string;
|
|
88
|
+
}): Suggestion;
|
|
89
|
+
accept(suggestionId: string): Suggestion;
|
|
90
|
+
reject(suggestionId: string): Suggestion;
|
|
91
|
+
/** The content text of the store. */
|
|
92
|
+
text(): string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The root Y.Text that a local store keeps its content in. */
|
|
96
|
+
declare const TEXT_ROOT = "text";
|
|
97
|
+
/** The root Y.Map that holds values for cell, slide, canvas, and custom anchors. */
|
|
98
|
+
declare const VALUES_ROOT = "values";
|
|
99
|
+
/**
|
|
100
|
+
* The host for a local store. A text anchor takes `{ "replace": "text" }` and swaps the anchored range.
|
|
101
|
+
* Any other anchor takes `{ "value": <JSON> }` and sets that key in the values map.
|
|
102
|
+
*/
|
|
103
|
+
declare const localHost: SuggestionHost;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Stamp the principal the operator set. `as` is `kind:name`, from a flag the operator wrote or from
|
|
107
|
+
* `AGENTIC_COMMENTS_AS`. Scopes come only from `AGENTIC_COMMENTS_SCOPES`, a comma list. A tool input
|
|
108
|
+
* never reaches this function, so an agent that calls a tool cannot pick its own kind or scopes.
|
|
109
|
+
*/
|
|
110
|
+
declare function operatorActor(as: string | undefined, scopes: string | undefined): Actor;
|
|
111
|
+
|
|
112
|
+
export { type AnchorSpec, CommentsService, FileStore, type FileStoreOptions, type ListFilter, MAX_FILE, TEXT_ROOT, VALUES_ROOT, localHost, lockTimeoutFrom, operatorActor };
|
package/dist/local.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FileStore,
|
|
3
|
+
MAX_FILE,
|
|
4
|
+
lockTimeoutFrom,
|
|
5
|
+
operatorActor
|
|
6
|
+
} from "./chunk-C43HLFQE.js";
|
|
7
|
+
import {
|
|
8
|
+
CommentsService,
|
|
9
|
+
TEXT_ROOT,
|
|
10
|
+
VALUES_ROOT,
|
|
11
|
+
localHost
|
|
12
|
+
} from "./chunk-ZCM2UDLN.js";
|
|
13
|
+
import "./chunk-C3P5TAC3.js";
|
|
14
|
+
export {
|
|
15
|
+
CommentsService,
|
|
16
|
+
FileStore,
|
|
17
|
+
MAX_FILE,
|
|
18
|
+
TEXT_ROOT,
|
|
19
|
+
VALUES_ROOT,
|
|
20
|
+
localHost,
|
|
21
|
+
lockTimeoutFrom,
|
|
22
|
+
operatorActor
|
|
23
|
+
};
|