@captiveorigins/payload-plugin-polyglot 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.
@@ -0,0 +1,337 @@
1
+ import { fingerprint, memoryKey, textsOf, visitPaths } from './text.js';
2
+ import { clearStop, loadSettings, recordStop, recordUsage } from './settings.js';
3
+ import { QuotaExceededError } from './types.js';
4
+ export const polyglotOf = (payload) => payload.config.custom?.polyglot;
5
+ const rulesOf = (o) => ({ skip: o.skip, skipKeys: new Set(o.skipKeys) });
6
+ const topFields = (o, slug) => [...new Set((o.collections[slug] ?? []).map((p) => p.split('.')[0]))];
7
+ const stateKey = (collection, id, locale) => `${collection}:${id}:${locale}`;
8
+ const engineFor = (o, locale) => o.translatorFor?.(locale) ?? o.translator;
9
+ /** Is this document one Polyglot translates? */
10
+ export function included(o, collection, doc) {
11
+ if (!o.collections[collection])
12
+ return false;
13
+ if (o.include)
14
+ return o.include({ collection, doc });
15
+ return doc._status === undefined || doc._status === 'published';
16
+ }
17
+ async function loadSource(payload, o, slug, id) {
18
+ const doc = (await payload.findByID({
19
+ collection: slug,
20
+ id,
21
+ locale: o.defaultLocale,
22
+ depth: 0,
23
+ draft: false,
24
+ overrideAccess: true,
25
+ disableErrors: true,
26
+ }));
27
+ if (!doc || !included(o, slug, doc))
28
+ return null;
29
+ const source = Object.fromEntries(topFields(o, slug).filter((k) => doc[k] !== undefined && doc[k] !== null).map((k) => [k, structuredClone(doc[k])]));
30
+ const texts = textsOf(source, o.collections[slug], rulesOf(o));
31
+ const reviewed = new Set((doc[o.statusField]?.reviewed ?? []));
32
+ const { docs } = await payload.find({
33
+ collection: o.stateSlug,
34
+ where: { and: [{ collection: { equals: slug } }, { doc: { equals: id } }] },
35
+ pagination: false,
36
+ depth: 0,
37
+ overrideAccess: true,
38
+ });
39
+ const state = new Map(docs.map((r) => [r.locale, r]));
40
+ return { doc, source, texts, hash: fingerprint(texts), state, reviewed };
41
+ }
42
+ async function saveState(payload, o, collection, id, locale, hash, stale, existing) {
43
+ const data = { key: stateKey(collection, id, locale), collection, doc: id, locale, hash, stale, at: new Date().toISOString() };
44
+ if (existing)
45
+ await payload.update({ collection: o.stateSlug, id: existing.id, data: data, overrideAccess: true });
46
+ else
47
+ await payload.create({ collection: o.stateSlug, data: data, overrideAccess: true });
48
+ }
49
+ /** Translations known in memory for these texts in one language. */
50
+ async function recall(payload, o, locale, texts) {
51
+ const known = new Map();
52
+ if (!texts.length)
53
+ return known;
54
+ const { docs } = await payload.find({
55
+ collection: o.memorySlug,
56
+ where: { key: { in: texts.map((t) => memoryKey(locale, t)) } },
57
+ pagination: false,
58
+ depth: 0,
59
+ overrideAccess: true,
60
+ select: { key: true, text: true },
61
+ });
62
+ const byKey = new Map(docs.map((d) => [d.key, d.text]));
63
+ for (const t of texts) {
64
+ const hit = byKey.get(memoryKey(locale, t));
65
+ if (hit)
66
+ known.set(t, hit);
67
+ }
68
+ return known;
69
+ }
70
+ async function remember(payload, o, locale, pairs) {
71
+ for (const [source, text] of pairs)
72
+ await payload
73
+ .create({ collection: o.memorySlug, data: { key: memoryKey(locale, source), locale, source, text }, overrideAccess: true })
74
+ .catch(() => undefined); // already remembered (unique key)
75
+ }
76
+ const CHUNK = 80;
77
+ const chunks = (list) => Array.from({ length: Math.ceil(list.length / CHUNK) }, (_, i) => list.slice(i * CHUNK, (i + 1) * CHUNK));
78
+ const argsFor = (o, s, locale, items, context) => ({
79
+ locale,
80
+ items,
81
+ context,
82
+ sourceLocale: o.defaultLocale,
83
+ keep: [...new Set([...o.keep, ...s.keep])],
84
+ instructions: s.instructions,
85
+ glossary: s.glossary.filter((g) => g.locale === locale).map(({ term, translation }) => ({ term, translation })),
86
+ });
87
+ const localesOf = (o, s, wanted) => (wanted ?? o.locales).filter((l) => !s.skipLocales.has(l));
88
+ /** A per-run character budget (free tiers). */
89
+ export class Budget {
90
+ max;
91
+ used = 0;
92
+ constructor(max) {
93
+ this.max = max;
94
+ }
95
+ allows(chars) {
96
+ return this.max === undefined || this.used + chars <= this.max;
97
+ }
98
+ spend(chars) {
99
+ this.used += chars;
100
+ }
101
+ }
102
+ /**
103
+ * Bring one document up to date in every language (or `locales`). Unchanged
104
+ * source costs nothing (the fingerprint matches); known sentences come from
105
+ * memory; only new ones reach the translator. Reviewed languages are never
106
+ * overwritten: they are marked stale. Languages are written one at a time.
107
+ */
108
+ export async function translateDocument(payload, collection, id, opts = {}) {
109
+ const o = polyglotOf(payload);
110
+ const settings = opts.settings ?? (await loadSettings(payload, o));
111
+ const result = { translated: [], upToDate: [], reviewedStale: [], failed: [], pending: [] };
112
+ const src = await loadSource(payload, o, collection, id);
113
+ if (!src)
114
+ return result;
115
+ const todo = [];
116
+ for (const locale of localesOf(o, settings, opts.locales)) {
117
+ const row = src.state.get(locale);
118
+ if (row?.hash === src.hash)
119
+ result.upToDate.push(locale);
120
+ else if (src.reviewed.has(locale)) {
121
+ result.reviewedStale.push(locale);
122
+ if (!opts.dryRun && row && !row.stale)
123
+ await saveState(payload, o, collection, id, locale, row.hash, true, row);
124
+ }
125
+ else
126
+ todo.push(locale);
127
+ }
128
+ if (opts.dryRun)
129
+ return { ...result, pending: todo };
130
+ const unique = [...new Set(src.texts)];
131
+ const context = { ...(o.writeContext?.({ inRequest: Boolean(opts.inRequest) }) ?? {}), polyglot: true };
132
+ let sent = 0;
133
+ let recovered = !settings.stop;
134
+ try {
135
+ for (const [n, locale] of todo.entries()) {
136
+ try {
137
+ let known = await recall(payload, o, locale, unique);
138
+ const missing = unique.filter((t) => !known.has(t));
139
+ if (missing.length && !opts.memoryOnly) {
140
+ const chars = missing.reduce((s, t) => s + t.length, 0);
141
+ if (opts.budget && !opts.budget.allows(chars)) {
142
+ result.pending.push(...todo.slice(n));
143
+ break;
144
+ }
145
+ for (const part of chunks(missing)) {
146
+ const items = part.map((text, i) => ({ key: String(i), text }));
147
+ const out = await engineFor(o, locale).translate(argsFor(o, settings, locale, items, o.describe?.(collection)));
148
+ const chars = part.reduce((s, t) => s + t.length, 0);
149
+ opts.budget?.spend(chars);
150
+ sent += chars;
151
+ if (!recovered && out.size) {
152
+ recovered = true;
153
+ await clearStop(payload, o);
154
+ }
155
+ await remember(payload, o, locale, items.flatMap((it) => (out.get(it.key) ? [[it.text, out.get(it.key)]] : [])));
156
+ }
157
+ known = await recall(payload, o, locale, unique);
158
+ }
159
+ if (unique.some((t) => !known.has(t))) {
160
+ ;
161
+ (opts.memoryOnly ? result.pending : result.failed).push(locale);
162
+ continue;
163
+ }
164
+ const data = structuredClone(src.source);
165
+ visitPaths(data, o.collections[collection], rulesOf(o), (t, set) => set(known.get(t) ?? t));
166
+ await payload.update({
167
+ collection: collection,
168
+ id,
169
+ locale: locale,
170
+ data: { ...data, ...(src.doc._status ? { _status: 'published' } : {}) },
171
+ overrideAccess: true,
172
+ context,
173
+ });
174
+ await saveState(payload, o, collection, id, locale, src.hash, false, src.state.get(locale));
175
+ result.translated.push(locale);
176
+ }
177
+ catch (error) {
178
+ if (error instanceof QuotaExceededError) {
179
+ result.pending.push(...todo.slice(n));
180
+ await recordStop(payload, o, error).catch((err) => payload.logger.warn({ err }, 'Polyglot: could not record the stop'));
181
+ throw Object.assign(error, { result });
182
+ }
183
+ payload.logger.error({ err: error, collection, id, locale }, 'Polyglot: translation failed');
184
+ result.failed.push(locale);
185
+ }
186
+ }
187
+ }
188
+ finally {
189
+ await recordUsage(payload, o, sent).catch((err) => payload.logger.warn({ err }, 'Polyglot: could not record usage'));
190
+ }
191
+ return result;
192
+ }
193
+ /**
194
+ * Find all content and bring it up to date (the sweep). With a translator that
195
+ * has `batch` and `useBatch`, every missing sentence of every language goes in
196
+ * one batch first (cheaper), then documents are written from memory. Stops
197
+ * cleanly at `maxCharactersPerRun` or when the engine's quota is used up; the
198
+ * next run continues. `dryRun` only reports what is pending. Respects the
199
+ * Translation settings (paused, skipped languages, cap); `force` runs even when
200
+ * paused (Translate now).
201
+ */
202
+ export async function syncAll(payload, opts = {}) {
203
+ const o = polyglotOf(payload);
204
+ const report = { documents: 0, upToDate: 0, translated: 0, pending: 0, reviewedStale: 0, failed: 0, characters: 0, stopped: 'done' };
205
+ const settings = await loadSettings(payload, o);
206
+ if (settings.paused && !opts.force && !opts.dryRun)
207
+ return { ...report, stopped: 'paused' };
208
+ const budget = new Budget(settings.maxCharactersPerRun ?? o.maxCharactersPerRun);
209
+ const targets = [];
210
+ for (const collection of Object.keys(o.collections).filter((c) => !opts.collections || opts.collections.includes(c))) {
211
+ const { docs } = await payload.find({ collection: collection, locale: o.defaultLocale, depth: 0, pagination: false, overrideAccess: true, draft: false });
212
+ for (const d of docs)
213
+ if (included(o, collection, d))
214
+ targets.push({ collection, id: String(d.id) });
215
+ }
216
+ const locales = localesOf(o, settings, opts.locales);
217
+ const work = opts.limit ? targets.slice(0, opts.limit) : targets;
218
+ const batching = Boolean(opts.useBatch && o.translator.batch && !o.translatorFor && !opts.dryRun);
219
+ if (batching) {
220
+ const missing = new Map();
221
+ for (const t of work) {
222
+ const src = await loadSource(payload, o, t.collection, t.id);
223
+ if (!src)
224
+ continue;
225
+ for (const locale of locales) {
226
+ if (src.state.get(locale)?.hash === src.hash || src.reviewed.has(locale))
227
+ continue;
228
+ const set = missing.get(locale) ?? new Set();
229
+ for (const text of src.texts)
230
+ set.add(text);
231
+ missing.set(locale, set);
232
+ }
233
+ }
234
+ const requests = [];
235
+ for (const [locale, set] of missing) {
236
+ const known = await recall(payload, o, locale, [...set]);
237
+ for (const part of chunks([...set].filter((t) => !known.has(t)))) {
238
+ const chars = part.reduce((s, t) => s + t.length, 0);
239
+ if (!budget.allows(chars)) {
240
+ report.stopped = 'budget';
241
+ break;
242
+ }
243
+ budget.spend(chars);
244
+ requests.push(argsFor(o, settings, locale, part.map((text, i) => ({ key: String(i), text }))));
245
+ }
246
+ }
247
+ if (requests.length) {
248
+ let results;
249
+ try {
250
+ results = await o.translator.batch(requests);
251
+ }
252
+ catch (error) {
253
+ if (!(error instanceof QuotaExceededError))
254
+ throw error;
255
+ await recordStop(payload, o, error);
256
+ return { ...report, stopped: 'quota', stopReason: error.reason, pending: requests.length };
257
+ }
258
+ await recordUsage(payload, o, budget.used);
259
+ if (settings.stop && results.some((m) => m.size))
260
+ await clearStop(payload, o);
261
+ for (const [i, r] of requests.entries())
262
+ await remember(payload, o, r.locale, r.items.flatMap((it) => (results[i]?.get(it.key) ? [[it.text, results[i].get(it.key)]] : [])));
263
+ }
264
+ }
265
+ for (const t of work) {
266
+ let r;
267
+ try {
268
+ r = await translateDocument(payload, t.collection, t.id, { locales, dryRun: opts.dryRun, memoryOnly: batching, budget: batching ? undefined : budget, settings });
269
+ }
270
+ catch (error) {
271
+ if (!(error instanceof QuotaExceededError))
272
+ throw error;
273
+ r = error.result;
274
+ report.stopped = 'quota';
275
+ report.stopReason = error.reason;
276
+ }
277
+ report.documents++;
278
+ report.upToDate += r.upToDate.length;
279
+ report.translated += r.translated.length;
280
+ report.pending += r.pending.length;
281
+ report.reviewedStale += r.reviewedStale.length;
282
+ report.failed += r.failed.length;
283
+ if (report.stopped === 'quota')
284
+ break;
285
+ if (!batching && r.pending.length && !opts.dryRun)
286
+ report.stopped = 'budget';
287
+ }
288
+ report.characters = budget.used;
289
+ return report;
290
+ }
291
+ /** Translation status of one document in one language, for notices: source, reviewed, stale, machine or pending. */
292
+ export async function languageStatus(payload, collection, doc, locale) {
293
+ const o = polyglotOf(payload);
294
+ if (locale === o.defaultLocale)
295
+ return 'source';
296
+ const reviewed = (doc[o.statusField]?.reviewed ?? []).includes(locale);
297
+ const { docs } = await payload.find({
298
+ collection: o.stateSlug,
299
+ where: { key: { equals: stateKey(collection, String(doc.id), locale) } },
300
+ limit: 1,
301
+ depth: 0,
302
+ overrideAccess: true,
303
+ });
304
+ const row = docs[0];
305
+ if (reviewed)
306
+ return row?.stale ? 'stale' : 'reviewed';
307
+ return row ? 'machine' : 'pending';
308
+ }
309
+ /** Translation coverage per collection and language (for the admin view and reports). */
310
+ export async function coverage(payload, opts = {}) {
311
+ const o = polyglotOf(payload);
312
+ const rows = [];
313
+ for (const collection of Object.keys(o.collections).filter((c) => !opts.collections || opts.collections.includes(c))) {
314
+ const row = { collection, documents: 0, locales: Object.fromEntries(o.locales.map((l) => [l, { translated: 0, pending: 0, reviewed: 0, stale: 0 }])) };
315
+ const { docs } = await payload.find({ collection: collection, locale: o.defaultLocale, depth: 0, pagination: false, overrideAccess: true, draft: false });
316
+ for (const d of docs) {
317
+ if (!included(o, collection, d))
318
+ continue;
319
+ const src = await loadSource(payload, o, collection, String(d.id));
320
+ if (!src)
321
+ continue;
322
+ row.documents++;
323
+ for (const locale of o.locales) {
324
+ const r = src.state.get(locale);
325
+ const c = row.locales[locale];
326
+ if (src.reviewed.has(locale))
327
+ r && r.hash !== src.hash ? c.stale++ : c.reviewed++;
328
+ else if (r?.hash === src.hash)
329
+ c.translated++;
330
+ else
331
+ c.pending++;
332
+ }
333
+ }
334
+ rows.push(row);
335
+ }
336
+ return rows;
337
+ }
@@ -0,0 +1,119 @@
1
+ import type { Payload, PayloadRequest, RequestContext } from 'payload';
2
+ /** One piece of text to translate, with a caller-chosen key. */
3
+ export type Item = {
4
+ key: string;
5
+ text: string;
6
+ };
7
+ /**
8
+ * A translation engine. `translate` returns translations by key (missing keys
9
+ * stay untranslated). `batch` (optional) translates many requests at once,
10
+ * usually at a lower price and more slowly; the sweep prefers it.
11
+ */
12
+ export type TranslateArgs = {
13
+ locale: string;
14
+ items: Item[];
15
+ context?: string;
16
+ sourceLocale: string;
17
+ keep: string[];
18
+ /** Editors' notes (tone, audience); AI engines use them, others may ignore them. */
19
+ instructions?: string;
20
+ /** Preferred translations for this language; AI engines use them, others may ignore them. */
21
+ glossary?: {
22
+ term: string;
23
+ translation: string;
24
+ }[];
25
+ };
26
+ export type Translator = {
27
+ translate: (args: TranslateArgs) => Promise<Map<string, string>>;
28
+ batch?: (requests: TranslateArgs[]) => Promise<Map<string, string>[]>;
29
+ };
30
+ /** Why an engine stopped: out of credits (paid), a free quota used up, or the key refused. */
31
+ export type StopReason = 'credits' | 'quota' | 'auth';
32
+ /**
33
+ * Thrown by a translator when it cannot continue until someone acts (credits,
34
+ * quota, key): the run stops cleanly, admins are told why, and the next run
35
+ * after the fix continues.
36
+ */
37
+ export declare class QuotaExceededError extends Error {
38
+ readonly reason: StopReason;
39
+ constructor(message?: string, reason?: StopReason);
40
+ }
41
+ /** A recorded stop, shown to admins until a translation succeeds again. */
42
+ export type EngineStop = {
43
+ reason: StopReason;
44
+ message: string;
45
+ at: string;
46
+ };
47
+ export type PolyglotOptions = {
48
+ /** Fields to keep in every language, per collection. Dotted paths reach into arrays/groups: `sections.heading`. */
49
+ collections: Record<string, string[]>;
50
+ translator: Translator;
51
+ /** May this request/run call the translator? Default: always. Use it for keys, budgets or environments. */
52
+ enabled?: (args: {
53
+ payload: Payload;
54
+ req?: PayloadRequest;
55
+ }) => boolean | Promise<boolean>;
56
+ /** Which documents to translate. Default: published ones (or all, without drafts). */
57
+ include?: (args: {
58
+ collection: string;
59
+ doc: Record<string, unknown>;
60
+ }) => boolean;
61
+ /** Terms never translated: product names, brands, scientific names… */
62
+ keep?: string[];
63
+ /** Engine per language (e.g. DeepL for European languages, another for the rest). Default: `translator`. */
64
+ translatorFor?: (locale: string) => Translator | undefined;
65
+ /** Stop a sweep after sending this many characters (stay inside a free tier). Default: no cap. */
66
+ maxCharactersPerRun?: number;
67
+ /** Text never sent (placeholders). Default: lorem ipsum and friends. */
68
+ skip?: RegExp;
69
+ /** Keys whose string values are never text (urls, ids, option values…). Added to the defaults. */
70
+ skipKeys?: string[];
71
+ /** Fields that must never change when another language is saved (computed from the default language). */
72
+ identityFields?: string[];
73
+ /** Slug of the translation memory collection. Default `polyglot-memory`. */
74
+ memorySlug?: string;
75
+ /** Slug of the per-language fingerprint collection. Default `polyglot-state`. */
76
+ stateSlug?: string;
77
+ /** Name of the per-document status group (reviewed languages + fingerprints). Default `translations`. */
78
+ statusField?: string;
79
+ /** Context for writes of translated languages, e.g. `{ disableRevalidate: true }` outside a web request. */
80
+ writeContext?: (args: {
81
+ inRequest: boolean;
82
+ }) => RequestContext;
83
+ /** Short description for the translator ("Content of a page on a reptile-care website"). */
84
+ describe?: (collection: string) => string;
85
+ /** Who may see coverage and start translations in the admin. Default: any signed-in user. */
86
+ canManage?: (args: {
87
+ req: PayloadRequest;
88
+ }) => boolean | Promise<boolean>;
89
+ /** Who may change Translation settings (pause, languages, glossary…). Default: `canManage`. */
90
+ canConfigure?: (args: {
91
+ req: PayloadRequest;
92
+ }) => boolean | Promise<boolean>;
93
+ /** Add the Translation settings global (admin-editable pause, skipped languages, glossary, usage). Default true. */
94
+ settings?: boolean;
95
+ /** Slug of the settings global. Default `polyglot-settings`. */
96
+ settingsSlug?: string;
97
+ /** Admin group of the settings global. Default `Settings`. */
98
+ settingsGroup?: string;
99
+ /** Add the Polyglot admin view (coverage per collection and language, Translate now). Default true. */
100
+ adminView?: boolean;
101
+ /** Translate after a document is published (in Next.js, after the response). Default true. */
102
+ onPublish?: boolean;
103
+ /** Called once when translation stops (credits, quota, key), e.g. to email admins. Not called again until it has recovered. */
104
+ onStop?: (args: {
105
+ payload: Payload;
106
+ stop: EngineStop;
107
+ }) => void | Promise<void>;
108
+ };
109
+ export type ResolvedPolyglot = Required<Omit<PolyglotOptions, 'enabled' | 'include' | 'writeContext' | 'describe' | 'translatorFor' | 'maxCharactersPerRun' | 'canManage' | 'canConfigure' | 'onStop'>> & Pick<PolyglotOptions, 'enabled' | 'include' | 'writeContext' | 'describe' | 'translatorFor' | 'maxCharactersPerRun' | 'canManage' | 'canConfigure' | 'onStop'> & {
110
+ defaultLocale: string;
111
+ locales: string[];
112
+ };
113
+ export type DocumentResult = {
114
+ translated: string[];
115
+ upToDate: string[];
116
+ reviewedStale: string[];
117
+ failed: string[];
118
+ pending: string[];
119
+ };
package/dist/types.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Thrown by a translator when it cannot continue until someone acts (credits,
3
+ * quota, key): the run stops cleanly, admins are told why, and the next run
4
+ * after the fix continues.
5
+ */
6
+ export class QuotaExceededError extends Error {
7
+ reason;
8
+ constructor(message = 'Translation quota used up', reason = 'quota') {
9
+ super(message);
10
+ this.reason = reason;
11
+ this.name = 'QuotaExceededError';
12
+ }
13
+ }
package/package.json CHANGED
@@ -1,6 +1,88 @@
1
1
  {
2
2
  "name": "@captiveorigins/payload-plugin-polyglot",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Keeps all your Payload content translated into every language you have, a little at a time: finds content on its own, translates on publish and in a sweep, remembers every sentence, skips placeholder text, and never overwrites a human-reviewed translation.",
5
+ "keywords": [
6
+ "payload",
7
+ "payload-plugin",
8
+ "payloadcms",
9
+ "i18n",
10
+ "localization",
11
+ "translation",
12
+ "machine-translation",
13
+ "claude"
14
+ ],
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/mgilardi/payload-plugin-polyglot.git"
19
+ },
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js"
25
+ },
26
+ "./claude": {
27
+ "types": "./dist/claude.d.ts",
28
+ "import": "./dist/claude.js"
29
+ },
30
+ "./rsc": {
31
+ "types": "./dist/rsc/index.d.ts",
32
+ "import": "./dist/rsc/index.js"
33
+ },
34
+ "./client": {
35
+ "types": "./dist/client/index.d.ts",
36
+ "import": "./dist/client/index.js"
37
+ }
38
+ },
39
+ "main": "./dist/index.js",
40
+ "types": "./dist/index.d.ts",
41
+ "files": [
42
+ "dist",
43
+ "README.md",
44
+ "LICENSE"
45
+ ],
46
+ "engines": {
47
+ "node": ">=20.9"
48
+ },
49
+ "scripts": {
50
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
51
+ "typecheck": "tsc --noEmit",
52
+ "test": "vitest run",
53
+ "prepack": "npm run build",
54
+ "prepublishOnly": "npm run typecheck && npm test"
55
+ },
56
+ "peerDependencies": {
57
+ "@anthropic-ai/sdk": ">=1.0.0",
58
+ "@payloadcms/next": "^3.80.0",
59
+ "@payloadcms/ui": "^3.80.0",
60
+ "next": ">=15",
61
+ "payload": "^3.80.0",
62
+ "react": ">=19"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "@anthropic-ai/sdk": {
66
+ "optional": true
67
+ },
68
+ "next": {
69
+ "optional": true
70
+ }
71
+ },
72
+ "devDependencies": {
73
+ "@anthropic-ai/sdk": "1.3.0",
74
+ "@payloadcms/db-mongodb": "3.92.1",
75
+ "@payloadcms/next": "3.92.1",
76
+ "@payloadcms/ui": "3.92.1",
77
+ "@types/node": "^24.0.0",
78
+ "@types/react": "^19.3.0",
79
+ "graphql": "^16.14.2",
80
+ "mongodb-memory-server": "^11.3.0",
81
+ "next": "16.3.3",
82
+ "payload": "3.92.1",
83
+ "react": "^19.3.1",
84
+ "react-dom": "^19.3.1",
85
+ "typescript": "^5.9.0",
86
+ "vitest": "^3.2.0"
87
+ }
88
+ }