agentic-comments 0.4.0 → 0.5.1
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/README.md +37 -11
- package/dist/{chunk-HXXJNFEO.js → chunk-QJXI3P5D.js} +8 -1
- package/dist/{chunk-DRACF66Y.js → chunk-REFQNIEP.js} +1 -1
- package/dist/{chunk-R4LH36VG.js → chunk-TFRLOBJW.js} +1 -1
- package/dist/cli.js +3 -3
- package/dist/client.d.ts +72 -0
- package/dist/client.js +135 -0
- package/dist/errors-CNgWMVQT.d.ts +7 -0
- package/dist/index.d.ts +4 -9
- package/dist/index.js +1 -1
- package/dist/local.d.ts +2 -2
- package/dist/local.js +1 -1
- package/dist/{mcp-ACC4QIEM.js → mcp-XUV3RPOO.js} +2 -2
- package/dist/mcp-bin.js +3 -3
- package/dist/{service-Dxw9TVxY.d.ts → service-hpWbnSmc.d.ts} +1 -1
- package/dist/worker.d.ts +6 -3
- package/dist/worker.js +17 -7
- package/package.json +6 -2
- package/dist/{suggestions-oj8fd-nG.d.ts → suggestions-CvJwo6r4.d.ts} +29 -29
package/README.md
CHANGED
|
@@ -18,8 +18,6 @@ copy of `yjs`, because the package checks the Yjs types of your document. Instal
|
|
|
18
18
|
npm install agentic-comments yjs
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
To build from source, see Develop.
|
|
22
|
-
|
|
23
21
|
## Quick start
|
|
24
22
|
|
|
25
23
|
```ts
|
|
@@ -165,10 +163,6 @@ machine. It cannot check a lock held in another container or on another machine.
|
|
|
165
163
|
share a volume can have the same process id, and each then clears the other's lock, so they can lose writes. A save that
|
|
166
164
|
would pass 50 MB is refused.
|
|
167
165
|
|
|
168
|
-
If the version you install has no `agentic-comments` command, build from source (see [Develop](#develop)).
|
|
169
|
-
In the cloned repo, run `npm run build` and use `node dist/cli.js` where these examples say
|
|
170
|
-
`npx agentic-comments`.
|
|
171
|
-
|
|
172
166
|
```sh
|
|
173
167
|
export AGENTIC_COMMENTS_FILE=./comments.ydoc
|
|
174
168
|
export AGENTIC_COMMENTS_AS=human:ada # kind:name. The operator sets this.
|
|
@@ -180,10 +174,10 @@ npx agentic-comments accept <id>
|
|
|
180
174
|
```
|
|
181
175
|
|
|
182
176
|
- Commands: `init`, `list`, `show`, `comment`, `reply`, `resolve`, `reopen`, `relocate`, `suggestions`,
|
|
183
|
-
`suggest`, `accept`, `reject`, `mcp`. Run `agentic-comments --help` for flags.
|
|
177
|
+
`suggest`, `accept`, `reject`, `mcp`. Run `npx agentic-comments --help` for flags.
|
|
184
178
|
- Every result is JSON. An error is JSON on stderr with a `code`, and the exit code is 1. A usage error
|
|
185
179
|
exits 2.
|
|
186
|
-
- The principal comes from `--as` or `AGENTIC_COMMENTS_AS`. Scopes come only from
|
|
180
|
+
- The principal comes from `--as` or `AGENTIC_COMMENTS_AS`, as `kind:name`. The name starts with A-Z a-z 0-9 `_` `-`, then uses A-Z a-z 0-9 `.` `_` `-`, and is at most 128 characters. Scopes come only from
|
|
187
181
|
`AGENTIC_COMMENTS_SCOPES`, a comma list such as `comments:accept`. No flag takes a scope or an author,
|
|
188
182
|
and a flag that does not apply to a command is refused. `AGENTIC_COMMENTS_LOCK_TIMEOUT_MS` sets the wait
|
|
189
183
|
for a lock, in whole milliseconds.
|
|
@@ -203,9 +197,13 @@ npx agentic-comments accept <id>
|
|
|
203
197
|
document in a D1 database through the `Store` port of `agentic-identity`. It checks every request with the
|
|
204
198
|
`Auth` port, so the host picks who the caller is and no request carries an author.
|
|
205
199
|
|
|
206
|
-
Set the principals as a secret. It is JSON from a bearer token
|
|
207
|
-
`{ "as": "kind:name", "scopes": [...] }`.
|
|
208
|
-
|
|
200
|
+
Set the principals as a secret. It is JSON from a bearer token to `kind:name`, or to
|
|
201
|
+
`{ "as": "kind:name", "scopes": [...] }`. A token is 32 to 512 bytes, from A-Z a-z 0-9 `_` `-`, so a base64 token with `+` or `/` is refused.
|
|
202
|
+
A name starts with A-Z a-z 0-9 `_` `-`, then uses A-Z a-z 0-9 `.` `_` `-`, and is at most 128 characters.
|
|
203
|
+
One bad entry refuses the whole table. A request with no valid token gets `401`.
|
|
204
|
+
A request whose `Authorization` header is not `Bearer` followed by 1 to 512 bytes of A-Z a-z 0-9 `_` `-` gets `401`, before any custom `Auth` runs.
|
|
205
|
+
A write with no `Authorization` header (a session cookie, for example) gets `403` unless its `Origin` is in the `origins`
|
|
206
|
+
option of `createApp`. The default lists none.
|
|
209
207
|
|
|
210
208
|
```sh
|
|
211
209
|
wrangler secret put AGENTIC_COMMENTS_TOKENS
|
|
@@ -243,6 +241,34 @@ curl -H "authorization: Bearer $TOKEN" -H "content-type: application/json" \
|
|
|
243
241
|
http://127.0.0.1:8787/api/v1/docs/d1/threads
|
|
244
242
|
```
|
|
245
243
|
|
|
244
|
+
## HTTP client
|
|
245
|
+
|
|
246
|
+
`agentic-comments/client` is a typed client for the Worker routes. It uses only `fetch`, so it runs in browsers,
|
|
247
|
+
Workers, and Node.
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
import { createClient, CommentsApiError } from "agentic-comments/client";
|
|
251
|
+
|
|
252
|
+
const comments = createClient({ baseUrl: "https://comments.example.com", token });
|
|
253
|
+
await comments.createDocument({ id: "d1", text: "The quick brown fox" });
|
|
254
|
+
const thread = await comments.addThread("d1", { anchor: { type: "text", quote: "fox" }, body: "Which fox?" });
|
|
255
|
+
await comments.reply("d1", thread.id, { body: "This one." });
|
|
256
|
+
try {
|
|
257
|
+
await comments.accept("d1", "missing");
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (error instanceof CommentsApiError) console.error(error.code, error.status);
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
- The methods match the routes: `me`, `createDocument`, `getDocument`, `listThreads`, `getThread`, `addThread`, `reply`,
|
|
264
|
+
`resolve`, `reopen`, `relocate`, `listSuggestions`, `suggest`, `accept`, `reject`. Each takes an optional `{ signal }`.
|
|
265
|
+
- `baseUrl` is the Worker URL, `http` or `https`, with no query, hash, or credentials.
|
|
266
|
+
- The client never sends an author. The Worker stamps one from the token.
|
|
267
|
+
- A failed call throws `CommentsApiError`. `code` is the API error code, or `UNKNOWN` when the reply is not an API
|
|
268
|
+
error. `status` is the HTTP status. A `200` reply with the wrong shape also fails as `UNKNOWN`.
|
|
269
|
+
- The client never follows a redirect and never retries. A write that you abort after the client sends it can still apply:
|
|
270
|
+
read the thread before you try again.
|
|
271
|
+
|
|
246
272
|
## Live demo
|
|
247
273
|
|
|
248
274
|
A demo Worker runs at <https://agentic-comments-demo.hello-fd1.workers.dev>. It is a staging site. Check it with
|
|
@@ -244,6 +244,7 @@ var FileStore = class {
|
|
|
244
244
|
};
|
|
245
245
|
|
|
246
246
|
// src/operator.ts
|
|
247
|
+
import { PrincipalParseError, parsePrincipal } from "agentic-identity";
|
|
247
248
|
function operatorActor(as, scopes) {
|
|
248
249
|
const value = as?.trim();
|
|
249
250
|
const cut = value === void 0 ? -1 : value.indexOf(":");
|
|
@@ -251,7 +252,13 @@ function operatorActor(as, scopes) {
|
|
|
251
252
|
throw new CommentsError("INVALID_AUTHOR", `set the principal as kind:name, for example human:ada. Kinds: ${PRINCIPAL_KINDS.join(", ")}`);
|
|
252
253
|
}
|
|
253
254
|
const list = scopes === void 0 || scopes.trim() === "" ? [] : scopes.split(",").map((s) => s.trim());
|
|
254
|
-
|
|
255
|
+
try {
|
|
256
|
+
const { kind, id } = parsePrincipal({ kind: value.slice(0, cut), id: value.slice(cut + 1) });
|
|
257
|
+
return stamp({ kind, name: id, scopes: list });
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (!(error instanceof PrincipalParseError)) throw error;
|
|
260
|
+
throw new CommentsError("INVALID_AUTHOR", `the principal needs a known kind (${PRINCIPAL_KINDS.join(", ")}) and a name of A-Z a-z 0-9 . _ -`);
|
|
261
|
+
}
|
|
255
262
|
}
|
|
256
263
|
|
|
257
264
|
export {
|
package/dist/cli.js
CHANGED
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
FileStore,
|
|
4
4
|
lockTimeoutFrom,
|
|
5
5
|
operatorActor
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-QJXI3P5D.js";
|
|
7
7
|
import {
|
|
8
8
|
VERSION
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-REFQNIEP.js";
|
|
10
10
|
import {
|
|
11
11
|
CommentsService,
|
|
12
12
|
TEXT_ROOT
|
|
@@ -155,7 +155,7 @@ async function runCli(argv, io) {
|
|
|
155
155
|
return 0;
|
|
156
156
|
}
|
|
157
157
|
if (p.command === "mcp") {
|
|
158
|
-
const { startMcp } = await import("./mcp-
|
|
158
|
+
const { startMcp } = await import("./mcp-XUV3RPOO.js");
|
|
159
159
|
await startMcp(store, operatorActor(p.flags["--as"] ?? io.env.AGENTIC_COMMENTS_AS, io.env.AGENTIC_COMMENTS_SCOPES));
|
|
160
160
|
return 0;
|
|
161
161
|
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { L as ListFilter, A as AnchorSpec } from './service-hpWbnSmc.js';
|
|
2
|
+
import { C as CommentsErrorCode } from './errors-CNgWMVQT.js';
|
|
3
|
+
import { PrincipalKind } from 'agentic-identity';
|
|
4
|
+
export { PrincipalKind } from 'agentic-identity';
|
|
5
|
+
import { T as Thread, N as NewReply, M as Message, S as Suggestion } from './suggestions-CvJwo6r4.js';
|
|
6
|
+
import 'yjs';
|
|
7
|
+
|
|
8
|
+
/** Codes the comment rules return, plus the ones the Worker and its auth return. */
|
|
9
|
+
type ApiErrorCode = CommentsErrorCode | "UNAUTHENTICATED" | "NOT_FOUND" | "INTERNAL" | "UNKNOWN";
|
|
10
|
+
/** Thrown for every reply that is not a success. `code` is the API error code and `status` is the HTTP status. */
|
|
11
|
+
declare class CommentsApiError extends Error {
|
|
12
|
+
readonly code: ApiErrorCode;
|
|
13
|
+
readonly status: number;
|
|
14
|
+
constructor(code: ApiErrorCode, status: number, message: string);
|
|
15
|
+
}
|
|
16
|
+
interface ClientOptions {
|
|
17
|
+
/** The Worker origin, for example https://comments.example.com. The client adds /api/v1. */
|
|
18
|
+
baseUrl: string;
|
|
19
|
+
/** A bearer credential that the Worker's auth accepts. */
|
|
20
|
+
token: string;
|
|
21
|
+
fetch?: typeof fetch;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Every method takes an optional signal. An aborted call rejects with the signal reason, not a
|
|
25
|
+
* CommentsApiError. A write that was aborted after it was sent may still have applied: read the
|
|
26
|
+
* thread before you try again.
|
|
27
|
+
*/
|
|
28
|
+
interface CallOptions {
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
}
|
|
31
|
+
interface Me {
|
|
32
|
+
kind: PrincipalKind;
|
|
33
|
+
name: string;
|
|
34
|
+
scopes: string[];
|
|
35
|
+
}
|
|
36
|
+
type ThreadFilter = ListFilter;
|
|
37
|
+
type SuggestionListFilter = Pick<ListFilter, "status" | "anchorState">;
|
|
38
|
+
interface CommentsClient {
|
|
39
|
+
me(options?: CallOptions): Promise<Me>;
|
|
40
|
+
createDocument(input: {
|
|
41
|
+
id: string;
|
|
42
|
+
text?: string;
|
|
43
|
+
}, options?: CallOptions): Promise<{
|
|
44
|
+
id: string;
|
|
45
|
+
}>;
|
|
46
|
+
getDocument(docId: string, options?: CallOptions): Promise<{
|
|
47
|
+
id: string;
|
|
48
|
+
text: string;
|
|
49
|
+
}>;
|
|
50
|
+
listThreads(docId: string, filter?: ThreadFilter, options?: CallOptions): Promise<Thread[]>;
|
|
51
|
+
getThread(docId: string, threadId: string, options?: CallOptions): Promise<Thread>;
|
|
52
|
+
addThread(docId: string, input: {
|
|
53
|
+
anchor: AnchorSpec;
|
|
54
|
+
body: string;
|
|
55
|
+
mentions?: string[];
|
|
56
|
+
}, options?: CallOptions): Promise<Thread>;
|
|
57
|
+
reply(docId: string, threadId: string, input: NewReply, options?: CallOptions): Promise<Message>;
|
|
58
|
+
resolve(docId: string, threadId: string, options?: CallOptions): Promise<Thread>;
|
|
59
|
+
reopen(docId: string, threadId: string, options?: CallOptions): Promise<Thread>;
|
|
60
|
+
relocate(docId: string, threadId: string, options?: CallOptions): Promise<Thread>;
|
|
61
|
+
listSuggestions(docId: string, filter?: SuggestionListFilter, options?: CallOptions): Promise<Suggestion[]>;
|
|
62
|
+
suggest(docId: string, input: {
|
|
63
|
+
anchor: AnchorSpec;
|
|
64
|
+
change: unknown;
|
|
65
|
+
summary?: string;
|
|
66
|
+
}, options?: CallOptions): Promise<Suggestion>;
|
|
67
|
+
accept(docId: string, suggestionId: string, options?: CallOptions): Promise<Suggestion>;
|
|
68
|
+
reject(docId: string, suggestionId: string, options?: CallOptions): Promise<Suggestion>;
|
|
69
|
+
}
|
|
70
|
+
declare function createClient(options: ClientOptions): CommentsClient;
|
|
71
|
+
|
|
72
|
+
export { AnchorSpec, type ApiErrorCode, type CallOptions, type ClientOptions, CommentsApiError, type CommentsClient, CommentsErrorCode, ListFilter, type Me, Message, NewReply, Suggestion, type SuggestionListFilter, Thread, type ThreadFilter, createClient };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
var CODES = {
|
|
3
|
+
INVALID_AUTHOR: true,
|
|
4
|
+
INVALID_INPUT: true,
|
|
5
|
+
INVALID_ANCHOR: true,
|
|
6
|
+
NO_THREAD: true,
|
|
7
|
+
THREAD_RESOLVED: true,
|
|
8
|
+
CORRUPT: true,
|
|
9
|
+
NO_SUGGESTION: true,
|
|
10
|
+
FORBIDDEN: true,
|
|
11
|
+
ALREADY_DECIDED: true,
|
|
12
|
+
APPLY_FAILED: true,
|
|
13
|
+
RELOCATE_FAILED: true,
|
|
14
|
+
STORE_MISSING: true,
|
|
15
|
+
STORE_EXISTS: true,
|
|
16
|
+
STORE_CORRUPT: true,
|
|
17
|
+
STORE_LOCKED: true,
|
|
18
|
+
STORE_FULL: true,
|
|
19
|
+
NO_DOCUMENT: true,
|
|
20
|
+
DOCUMENT_EXISTS: true,
|
|
21
|
+
CONFLICT: true,
|
|
22
|
+
MISCONFIGURED: true,
|
|
23
|
+
USAGE: true,
|
|
24
|
+
UNAUTHENTICATED: true,
|
|
25
|
+
NOT_FOUND: true,
|
|
26
|
+
INTERNAL: true,
|
|
27
|
+
UNKNOWN: true
|
|
28
|
+
};
|
|
29
|
+
var isKnownCode = (v) => typeof v === "string" && Object.hasOwn(CODES, v);
|
|
30
|
+
var CommentsApiError = class extends Error {
|
|
31
|
+
code;
|
|
32
|
+
status;
|
|
33
|
+
constructor(code, status, message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "CommentsApiError";
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.status = status;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var KINDS = { human: true, agent_of_human: true, agent_of_agent: true, agent: true, user_token: true };
|
|
41
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
42
|
+
var isString = (v) => typeof v === "string";
|
|
43
|
+
var SHAPES = {
|
|
44
|
+
me: (v) => isObject(v) && (typeof v.kind === "string" && Object.hasOwn(KINDS, v.kind)) && isString(v.name) && Array.isArray(v.scopes),
|
|
45
|
+
created: (v) => isObject(v) && isString(v.id),
|
|
46
|
+
document: (v) => isObject(v) && isString(v.id) && isString(v.text),
|
|
47
|
+
thread: (v) => isObject(v) && isString(v.id) && isObject(v.anchor) && (v.status === "open" || v.status === "resolved") && Array.isArray(v.messages),
|
|
48
|
+
threads: (v) => Array.isArray(v) && v.every(SHAPES.thread),
|
|
49
|
+
message: (v) => isObject(v) && isString(v.id) && isString(v.body) && isObject(v.author),
|
|
50
|
+
suggestion: (v) => isObject(v) && isString(v.id) && isObject(v.anchor) && isString(v.status) && isObject(v.author),
|
|
51
|
+
suggestions: (v) => Array.isArray(v) && v.every(SHAPES.suggestion)
|
|
52
|
+
};
|
|
53
|
+
function createClient(options) {
|
|
54
|
+
if (typeof options.baseUrl !== "string" || options.baseUrl === "") throw new TypeError("createClient needs a baseUrl");
|
|
55
|
+
if (typeof options.token !== "string" || options.token === "") throw new TypeError("createClient needs a token");
|
|
56
|
+
let origin;
|
|
57
|
+
try {
|
|
58
|
+
origin = new URL(options.baseUrl);
|
|
59
|
+
} catch {
|
|
60
|
+
throw new TypeError("baseUrl must be an absolute URL");
|
|
61
|
+
}
|
|
62
|
+
if (origin.protocol !== "https:" && origin.protocol !== "http:") throw new TypeError("baseUrl must be an http or https URL");
|
|
63
|
+
if (origin.search !== "" || origin.hash !== "" || origin.username !== "" || origin.password !== "") {
|
|
64
|
+
throw new TypeError("baseUrl must have no query, hash, or credentials");
|
|
65
|
+
}
|
|
66
|
+
const root = `${origin.origin}${origin.pathname.replace(/\/+$/, "")}/api/v1`;
|
|
67
|
+
const token = options.token;
|
|
68
|
+
const send = (input, init) => {
|
|
69
|
+
const run = options.fetch ?? globalThis.fetch;
|
|
70
|
+
return run(input, init);
|
|
71
|
+
};
|
|
72
|
+
const seg = (value) => {
|
|
73
|
+
if (typeof value !== "string" || /^(?:\.|%2e){0,2}$/i.test(value)) throw new TypeError("an id must be a non-empty string that is not . or ..");
|
|
74
|
+
return encodeURIComponent(value);
|
|
75
|
+
};
|
|
76
|
+
const query = (filter) => {
|
|
77
|
+
const params = new URLSearchParams();
|
|
78
|
+
for (const [key, value] of Object.entries(filter ?? {})) if (value !== void 0) params.set(key, String(value));
|
|
79
|
+
const text = params.toString();
|
|
80
|
+
return text === "" ? "" : `?${text}`;
|
|
81
|
+
};
|
|
82
|
+
async function call(method, path, body, callOptions, shape) {
|
|
83
|
+
const headers = { authorization: `Bearer ${token}` };
|
|
84
|
+
if (body !== void 0) headers["content-type"] = "application/json";
|
|
85
|
+
const response = await send(`${root}${path}`, {
|
|
86
|
+
method,
|
|
87
|
+
headers,
|
|
88
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
89
|
+
signal: callOptions?.signal,
|
|
90
|
+
// Never follow a redirect: it would send a write to another host.
|
|
91
|
+
redirect: "manual"
|
|
92
|
+
});
|
|
93
|
+
const text = await response.text();
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = text === "" ? void 0 : JSON.parse(text);
|
|
97
|
+
} catch {
|
|
98
|
+
parsed = void 0;
|
|
99
|
+
}
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
const error = parsed?.error;
|
|
102
|
+
const code = isKnownCode(error?.code) ? error.code : "UNKNOWN";
|
|
103
|
+
const message = typeof error?.message === "string" ? error.message : `HTTP ${response.status}`;
|
|
104
|
+
throw new CommentsApiError(code, response.status, message);
|
|
105
|
+
}
|
|
106
|
+
if (!SHAPES[shape](parsed)) throw new CommentsApiError("UNKNOWN", response.status, `the reply is not a ${shape}`);
|
|
107
|
+
return parsed;
|
|
108
|
+
}
|
|
109
|
+
const doc = (id) => `/docs/${seg(id)}`;
|
|
110
|
+
const thread = (id, threadId) => `${doc(id)}/threads/${seg(threadId)}`;
|
|
111
|
+
const suggestion = (id, suggestionId) => `${doc(id)}/suggestions/${seg(suggestionId)}`;
|
|
112
|
+
const methods = {
|
|
113
|
+
me: (o) => call("GET", "/me", void 0, o, "me"),
|
|
114
|
+
createDocument: (input, o) => call("POST", "/docs", input, o, "created"),
|
|
115
|
+
getDocument: (id, o) => call("GET", doc(id), void 0, o, "document"),
|
|
116
|
+
listThreads: (id, filter, o) => call("GET", `${doc(id)}/threads${query(filter)}`, void 0, o, "threads"),
|
|
117
|
+
getThread: (id, threadId, o) => call("GET", thread(id, threadId), void 0, o, "thread"),
|
|
118
|
+
addThread: (id, input, o) => call("POST", `${doc(id)}/threads`, input, o, "thread"),
|
|
119
|
+
reply: (id, threadId, input, o) => call("POST", `${thread(id, threadId)}/replies`, input, o, "message"),
|
|
120
|
+
resolve: (id, threadId, o) => call("POST", `${thread(id, threadId)}/resolve`, void 0, o, "thread"),
|
|
121
|
+
reopen: (id, threadId, o) => call("POST", `${thread(id, threadId)}/reopen`, void 0, o, "thread"),
|
|
122
|
+
relocate: (id, threadId, o) => call("POST", `${thread(id, threadId)}/relocate`, void 0, o, "thread"),
|
|
123
|
+
listSuggestions: (id, filter, o) => call("GET", `${doc(id)}/suggestions${query(filter)}`, void 0, o, "suggestions"),
|
|
124
|
+
suggest: (id, input, o) => call("POST", `${doc(id)}/suggestions`, input, o, "suggestion"),
|
|
125
|
+
accept: (id, suggestionId, o) => call("POST", `${suggestion(id, suggestionId)}/accept`, void 0, o, "suggestion"),
|
|
126
|
+
reject: (id, suggestionId, o) => call("POST", `${suggestion(id, suggestionId)}/reject`, void 0, o, "suggestion")
|
|
127
|
+
};
|
|
128
|
+
return Object.fromEntries(
|
|
129
|
+
Object.entries(methods).map(([name, method]) => [name, async (...args) => method(...args)])
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
export {
|
|
133
|
+
CommentsApiError,
|
|
134
|
+
createClient
|
|
135
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
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" | "NO_DOCUMENT" | "DOCUMENT_EXISTS" | "CONFLICT" | "MISCONFIGURED" | "USAGE";
|
|
2
|
+
declare class CommentsError extends Error {
|
|
3
|
+
readonly code: CommentsErrorCode;
|
|
4
|
+
constructor(code: CommentsErrorCode, message: string);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export { type CommentsErrorCode as C, CommentsError as a };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
import { a as Author, C as Comments, b as Suggestions } from './suggestions-
|
|
2
|
-
export { c as ACCEPT_SCOPE, A as Actor, d as Anchor, e as AnchorKind, f as AnchorState, g as ApplyContext, h as CanvasAnchor, i as CellAnchor, j as CommentsOptions, M as Message, N as NewReply, k as NewSuggestion, l as NewThread, R as RelativePositionJson, m as RevertContext, n as SlideAnchor, o as StampInput, S as Suggestion, p as SuggestionFilter, q as SuggestionHost, r as TextAnchor, T as Thread, s as ThreadFilter, t as authorLabel, u as stamp, v as textAnchor, w as textOf } from './suggestions-
|
|
1
|
+
import { a as Author, C as Comments, b as Suggestions } from './suggestions-CvJwo6r4.js';
|
|
2
|
+
export { c as ACCEPT_SCOPE, A as Actor, d as Anchor, e as AnchorKind, f as AnchorState, g as ApplyContext, h as CanvasAnchor, i as CellAnchor, j as CommentsOptions, M as Message, N as NewReply, k as NewSuggestion, l as NewThread, R as RelativePositionJson, m as RevertContext, n as SlideAnchor, o as StampInput, S as Suggestion, p as SuggestionFilter, q as SuggestionHost, r as TextAnchor, T as Thread, s as ThreadFilter, t as authorLabel, u as stamp, v as textAnchor, w as textOf } from './suggestions-CvJwo6r4.js';
|
|
3
|
+
export { a as CommentsError, C as CommentsErrorCode } from './errors-CNgWMVQT.js';
|
|
3
4
|
export { PRINCIPAL_KINDS, PrincipalKind } from 'agentic-identity';
|
|
4
5
|
import 'yjs';
|
|
5
6
|
|
|
6
7
|
declare const VERSION: string;
|
|
7
8
|
|
|
8
|
-
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" | "NO_DOCUMENT" | "DOCUMENT_EXISTS" | "CONFLICT" | "MISCONFIGURED" | "USAGE";
|
|
9
|
-
declare class CommentsError extends Error {
|
|
10
|
-
readonly code: CommentsErrorCode;
|
|
11
|
-
constructor(code: CommentsErrorCode, message: string);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
9
|
type CommentEventType = "thread.created" | "message.added" | "mention" | "thread.resolved" | "thread.reopened" | "suggestion.created" | "suggestion.accepted" | "suggestion.rejected";
|
|
15
10
|
/**
|
|
16
11
|
* One change, in terms a notifier can route. It holds ids and never the text of a comment, a suggestion, or
|
|
@@ -103,4 +98,4 @@ declare class Notifications {
|
|
|
103
98
|
private decided;
|
|
104
99
|
}
|
|
105
100
|
|
|
106
|
-
export { Author, type CommentEvent, type CommentEventType, type CommentListener, Comments,
|
|
101
|
+
export { Author, type CommentEvent, type CommentEventType, type CommentListener, Comments, Notifications, type NotificationsOptions, Suggestions, VERSION };
|
package/dist/index.js
CHANGED
package/dist/local.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as Y from 'yjs';
|
|
2
|
-
export { A as AnchorSpec, C as CommentsService, L as ListFilter } from './service-
|
|
3
|
-
import { q as SuggestionHost, A as Actor } from './suggestions-
|
|
2
|
+
export { A as AnchorSpec, C as CommentsService, L as ListFilter } from './service-hpWbnSmc.js';
|
|
3
|
+
import { q as SuggestionHost, A as Actor } from './suggestions-CvJwo6r4.js';
|
|
4
4
|
import 'agentic-identity';
|
|
5
5
|
|
|
6
6
|
/** The most bytes a saved document holds. A save over this is refused, so a file never grows past what a load accepts. */
|
package/dist/local.js
CHANGED
package/dist/mcp-bin.js
CHANGED
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
FileStore,
|
|
4
4
|
lockTimeoutFrom,
|
|
5
5
|
operatorActor
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-QJXI3P5D.js";
|
|
7
7
|
import {
|
|
8
8
|
startMcp
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-TFRLOBJW.js";
|
|
10
10
|
import {
|
|
11
11
|
VERSION
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-REFQNIEP.js";
|
|
13
13
|
import "./chunk-NQWRFKJ6.js";
|
|
14
14
|
import "./chunk-BSNBDKR2.js";
|
|
15
15
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as Y from 'yjs';
|
|
2
|
-
import { A as Actor, T as Thread, M as Message, S as Suggestion } from './suggestions-
|
|
2
|
+
import { A as Actor, T as Thread, M as Message, S as Suggestion } from './suggestions-CvJwo6r4.js';
|
|
3
3
|
|
|
4
4
|
/** Where a comment or suggestion points, in terms a person or agent can write down. */
|
|
5
5
|
type AnchorSpec = {
|
package/dist/worker.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { Auth, Store, PrincipalContext, Region } from 'agentic-identity';
|
|
3
|
+
import { RequirePrincipalOptions } from 'agentic-identity/http';
|
|
3
4
|
import { D1Like } from 'agentic-identity/cloudflare';
|
|
4
|
-
import { A as Actor } from './suggestions-
|
|
5
|
-
import { C as CommentsService } from './service-
|
|
5
|
+
import { A as Actor } from './suggestions-CvJwo6r4.js';
|
|
6
|
+
import { C as CommentsService } from './service-hpWbnSmc.js';
|
|
6
7
|
import 'yjs';
|
|
7
8
|
|
|
8
9
|
interface WorkerEnv {
|
|
@@ -13,7 +14,7 @@ interface WorkerEnv {
|
|
|
13
14
|
/** D1 database: each document, with its threads, as a Yjs update. */
|
|
14
15
|
DB?: D1Like;
|
|
15
16
|
/**
|
|
16
|
-
* JSON object: bearer token (32 bytes
|
|
17
|
+
* JSON object: bearer token (32 to 512 bytes of A-Z a-z 0-9 _ -) to `kind:name` (the name uses A-Z a-z 0-9 . _ -), or to `{ "as": "kind:name", "scopes": ["comments:write"] }`.
|
|
17
18
|
* Set it as a secret. A short token or a bad entry refuses the whole config.
|
|
18
19
|
*/
|
|
19
20
|
AGENTIC_COMMENTS_TOKENS?: string;
|
|
@@ -39,6 +40,8 @@ type DocsSource = CommentsDocs | ((env: WorkerEnv) => Promise<CommentsDocs>);
|
|
|
39
40
|
interface AppOptions {
|
|
40
41
|
/** The credential check. The default reads `AGENTIC_COMMENTS_TOKENS`. Pass any `Auth` from agentic-identity to replace it. */
|
|
41
42
|
auth?: Auth | ((env: WorkerEnv) => Auth);
|
|
43
|
+
/** Origins that may send a write with a session cookie. A bearer token needs none. Passed to `requirePrincipal`. */
|
|
44
|
+
origins?: RequirePrincipalOptions["origins"];
|
|
42
45
|
}
|
|
43
46
|
type AppEnv = {
|
|
44
47
|
Bindings: WorkerEnv;
|
package/dist/worker.js
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
ACCEPT_SCOPE,
|
|
7
7
|
CommentsError,
|
|
8
|
-
PRINCIPAL_KINDS,
|
|
9
8
|
exact,
|
|
10
9
|
invalid,
|
|
11
10
|
stamp
|
|
@@ -13,7 +12,8 @@ import {
|
|
|
13
12
|
|
|
14
13
|
// src/worker.ts
|
|
15
14
|
import { Hono } from "hono";
|
|
16
|
-
import { StoreConflictError, assertRegion, isScope, parseRegion, hasScope } from "agentic-identity";
|
|
15
|
+
import { StoreConflictError, assertRegion, isScope, parseRegion, hasScope, parsePrincipal } from "agentic-identity";
|
|
16
|
+
import { requirePrincipal } from "agentic-identity/http";
|
|
17
17
|
import { D1_SCHEMA, createD1Store } from "agentic-identity/cloudflare";
|
|
18
18
|
import * as Y from "yjs";
|
|
19
19
|
var READ = "comments:read";
|
|
@@ -22,6 +22,7 @@ var WRITE = "comments:write";
|
|
|
22
22
|
var ADMIN = "comments:admin";
|
|
23
23
|
var DEFAULT_SCOPES = [COMMENT];
|
|
24
24
|
var MIN_TOKEN_BYTES = 32;
|
|
25
|
+
var TOKEN_SHAPE = /^[A-Za-z0-9_-]{1,512}$/;
|
|
25
26
|
var warned = /* @__PURE__ */ new Set();
|
|
26
27
|
function refuse(reason) {
|
|
27
28
|
if (!warned.has(reason)) {
|
|
@@ -40,9 +41,13 @@ function contextOf(entry) {
|
|
|
40
41
|
const who = typeof entry === "string" ? entry : object?.as;
|
|
41
42
|
const cut = typeof who === "string" ? who.indexOf(":") : -1;
|
|
42
43
|
if (typeof who !== "string" || cut < 1 || cut === who.length - 1) return void 0;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
let kind;
|
|
45
|
+
let name;
|
|
46
|
+
try {
|
|
47
|
+
({ kind, id: name } = parsePrincipal({ kind: who.slice(0, cut), id: who.slice(cut + 1) }));
|
|
48
|
+
} catch {
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
46
51
|
const given = object?.scopes;
|
|
47
52
|
if (given !== void 0 && !(Array.isArray(given) && given.every(isScope))) return void 0;
|
|
48
53
|
try {
|
|
@@ -61,6 +66,7 @@ function createTokenAuth(raw) {
|
|
|
61
66
|
for (const [token, entry] of Object.entries(parsed)) {
|
|
62
67
|
const context = contextOf(entry);
|
|
63
68
|
if (new TextEncoder().encode(token).length < MIN_TOKEN_BYTES) throw new Error(`a token is shorter than ${MIN_TOKEN_BYTES} bytes`);
|
|
69
|
+
if (!TOKEN_SHAPE.test(token)) throw new Error("a token holds a character outside A-Z a-z 0-9 _ - or is over 512 bytes");
|
|
64
70
|
if (!context) throw new Error("an entry is not a valid kind:name or { as, scopes }");
|
|
65
71
|
table.set(token, context);
|
|
66
72
|
}
|
|
@@ -186,8 +192,12 @@ function createApp(source, options = {}) {
|
|
|
186
192
|
app2.get("/health", (c) => c.json({ ok: true, pack: "agentic-comments" }));
|
|
187
193
|
app2.use("/api/v1/*", async (c, next) => {
|
|
188
194
|
const auth = typeof options.auth === "function" ? options.auth(c.env ?? {}) : options.auth ?? createTokenAuth(c.env?.AGENTIC_COMMENTS_TOKENS);
|
|
189
|
-
const context = await
|
|
190
|
-
if (
|
|
195
|
+
const context = await requirePrincipal(c.req.raw, auth, { origins: options.origins });
|
|
196
|
+
if (context instanceof Response) {
|
|
197
|
+
if (context.status === 401) return Response.json({ error: { code: "UNAUTHENTICATED", message: "send a valid bearer token" } }, { status: 401, headers: { "www-authenticate": "Bearer" } });
|
|
198
|
+
if (context.status === 403) return fail("FORBIDDEN", "this credential may not do this", 403);
|
|
199
|
+
return fail("INTERNAL", "the credential check is unavailable", context.status);
|
|
200
|
+
}
|
|
191
201
|
c.set("context", context);
|
|
192
202
|
await next();
|
|
193
203
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-comments",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Comment threads and suggestions anchored to any content, from people and agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"comments",
|
|
@@ -48,6 +48,10 @@
|
|
|
48
48
|
"types": "./dist/anchoring.d.ts",
|
|
49
49
|
"import": "./dist/anchoring.js"
|
|
50
50
|
},
|
|
51
|
+
"./client": {
|
|
52
|
+
"types": "./dist/client.d.ts",
|
|
53
|
+
"import": "./dist/client.js"
|
|
54
|
+
},
|
|
51
55
|
"./local": {
|
|
52
56
|
"types": "./dist/local.d.ts",
|
|
53
57
|
"import": "./dist/local.js"
|
|
@@ -72,7 +76,7 @@
|
|
|
72
76
|
},
|
|
73
77
|
"dependencies": {
|
|
74
78
|
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
75
|
-
"agentic-identity": "^0.
|
|
79
|
+
"agentic-identity": "^0.6.0",
|
|
76
80
|
"hono": "^4.13.14",
|
|
77
81
|
"zod": "^3.25.76"
|
|
78
82
|
},
|
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
import * as Y from 'yjs';
|
|
2
2
|
import { PrincipalKind } from 'agentic-identity';
|
|
3
3
|
|
|
4
|
+
/** What a thread keeps about who wrote a message. Scopes never reach storage. */
|
|
5
|
+
interface Author {
|
|
6
|
+
kind: PrincipalKind;
|
|
7
|
+
name: string;
|
|
8
|
+
}
|
|
9
|
+
interface StampInput {
|
|
10
|
+
kind: PrincipalKind;
|
|
11
|
+
name: string;
|
|
12
|
+
/** Rights the host granted this principal, such as `comments:accept`. Used to decide, never stored. */
|
|
13
|
+
scopes?: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The only author a write accepts. The host makes one from the principal it authenticated, and an
|
|
17
|
+
* operation never reads an author from its input. An object that `stamp()` did not make is refused,
|
|
18
|
+
* and a stamped one cannot change.
|
|
19
|
+
*/
|
|
20
|
+
declare class Actor implements Author {
|
|
21
|
+
readonly kind: PrincipalKind;
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly scopes: readonly string[];
|
|
24
|
+
private constructor();
|
|
25
|
+
static of(input: StampInput): Actor;
|
|
26
|
+
static is(value: unknown): value is Actor;
|
|
27
|
+
}
|
|
28
|
+
/** Make the author for one authenticated principal. Throws when the kind or name is missing or wrong. */
|
|
29
|
+
declare const stamp: (input: StampInput) => Actor;
|
|
30
|
+
/** The label a UI shows. Agent work stays marked as machine-made. */
|
|
31
|
+
declare function authorLabel(author: Author): "person" | "person via agent" | "agent";
|
|
32
|
+
|
|
4
33
|
/** A Yjs relative position as JSON. It names an item, not an index, so it follows its text. */
|
|
5
34
|
interface RelativePositionJson {
|
|
6
35
|
type?: {
|
|
@@ -66,35 +95,6 @@ declare function textOf(type: Y.Text | Y.XmlText): string;
|
|
|
66
95
|
/** Pin a range of a Yjs text. Later edits by any replica do not move the anchor off its text. */
|
|
67
96
|
declare function textAnchor(type: Y.Text | Y.XmlText, from: number, to: number): TextAnchor;
|
|
68
97
|
|
|
69
|
-
/** What a thread keeps about who wrote a message. Scopes never reach storage. */
|
|
70
|
-
interface Author {
|
|
71
|
-
kind: PrincipalKind;
|
|
72
|
-
name: string;
|
|
73
|
-
}
|
|
74
|
-
interface StampInput {
|
|
75
|
-
kind: PrincipalKind;
|
|
76
|
-
name: string;
|
|
77
|
-
/** Rights the host granted this principal, such as `comments:accept`. Used to decide, never stored. */
|
|
78
|
-
scopes?: string[];
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* The only author a write accepts. The host makes one from the principal it authenticated, and an
|
|
82
|
-
* operation never reads an author from its input. An object that `stamp()` did not make is refused,
|
|
83
|
-
* and a stamped one cannot change.
|
|
84
|
-
*/
|
|
85
|
-
declare class Actor implements Author {
|
|
86
|
-
readonly kind: PrincipalKind;
|
|
87
|
-
readonly name: string;
|
|
88
|
-
readonly scopes: readonly string[];
|
|
89
|
-
private constructor();
|
|
90
|
-
static of(input: StampInput): Actor;
|
|
91
|
-
static is(value: unknown): value is Actor;
|
|
92
|
-
}
|
|
93
|
-
/** Make the author for one authenticated principal. Throws when the kind or name is missing or wrong. */
|
|
94
|
-
declare const stamp: (input: StampInput) => Actor;
|
|
95
|
-
/** The label a UI shows. Agent work stays marked as machine-made. */
|
|
96
|
-
declare function authorLabel(author: Author): "person" | "person via agent" | "agent";
|
|
97
|
-
|
|
98
98
|
interface Message {
|
|
99
99
|
id: string;
|
|
100
100
|
at: string;
|