@simmalugnt-se/payload-editor-assistant 0.14.0 → 0.15.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/CHANGELOG.md +18 -0
- package/README.md +72 -4
- package/dist/config/validate.d.ts +1 -0
- package/dist/config/validate.js +10 -0
- package/dist/endpoints/chat.js +2 -1
- package/dist/extensions/contract.d.ts +62 -0
- package/dist/extensions/contract.js +6 -0
- package/dist/extensions/input.d.ts +7 -0
- package/dist/extensions/input.js +81 -0
- package/dist/extensions/resolve.d.ts +27 -0
- package/dist/extensions/resolve.js +187 -0
- package/dist/form/relation-shape.d.ts +2 -1
- package/dist/form/relation-shape.js +10 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/plugin.js +10 -1
- package/dist/runtime/agent.js +16 -23
- package/dist/runtime/capabilities/admin-navigate.d.ts +2 -0
- package/dist/runtime/capabilities/admin-navigate.js +49 -0
- package/dist/runtime/capabilities/content-find-by-id.d.ts +2 -0
- package/dist/runtime/capabilities/content-find-by-id.js +25 -0
- package/dist/runtime/capabilities/content-find.d.ts +2 -0
- package/dist/runtime/capabilities/content-find.js +22 -0
- package/dist/runtime/capabilities/form-read.d.ts +2 -0
- package/dist/runtime/capabilities/form-read.js +25 -0
- package/dist/runtime/capabilities/index.d.ts +7 -0
- package/dist/runtime/capabilities/index.js +21 -0
- package/dist/runtime/capabilities/media-view.d.ts +2 -0
- package/dist/runtime/capabilities/media-view.js +48 -0
- package/dist/runtime/capabilities/propose.d.ts +3 -0
- package/dist/runtime/capabilities/propose.js +79 -0
- package/dist/runtime/capabilities/schema-discover.d.ts +2 -0
- package/dist/runtime/capabilities/schema-discover.js +17 -0
- package/dist/runtime/capabilities/tool.d.ts +33 -0
- package/dist/runtime/capabilities/tool.js +12 -0
- package/dist/runtime/system-prompt.d.ts +2 -1
- package/dist/runtime/system-prompt.js +29 -8
- package/dist/runtime/tools.d.ts +10 -6
- package/dist/runtime/tools.js +15 -243
- package/dist/types.d.ts +7 -0
- package/dist/view.d.ts +10 -2
- package/dist/view.js +13 -3
- package/package.json +2 -6
package/dist/runtime/tools.js
CHANGED
|
@@ -1,245 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"schema.discover": {
|
|
10
|
-
type: "object",
|
|
11
|
-
properties: {},
|
|
12
|
-
},
|
|
13
|
-
"content.find": {
|
|
14
|
-
type: "object",
|
|
15
|
-
additionalProperties: false,
|
|
16
|
-
properties: {
|
|
17
|
-
collection: { type: "string" },
|
|
18
|
-
query: { type: "string" },
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
"content.findById": {
|
|
22
|
-
type: "object",
|
|
23
|
-
additionalProperties: false,
|
|
24
|
-
properties: {
|
|
25
|
-
collection: { type: "string" },
|
|
26
|
-
global: { type: "string" },
|
|
27
|
-
documentId: { type: ["string", "number"] },
|
|
28
|
-
locale: { type: "string" },
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
"form.read": {
|
|
32
|
-
type: "object",
|
|
33
|
-
properties: {},
|
|
34
|
-
},
|
|
35
|
-
"media.view": {
|
|
36
|
-
type: "object",
|
|
37
|
-
additionalProperties: false,
|
|
38
|
-
properties: {
|
|
39
|
-
collection: { type: "string" },
|
|
40
|
-
ids: { type: "array", items: { type: ["string", "number"] }, maxItems: MAX_IMAGES_PER_CALL },
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
"admin.navigate": {
|
|
44
|
-
type: "object",
|
|
45
|
-
additionalProperties: false,
|
|
46
|
-
properties: {
|
|
47
|
-
collection: { type: "string" },
|
|
48
|
-
global: { type: "string" },
|
|
49
|
-
documentId: { type: ["string", "number"] },
|
|
50
|
-
query: { type: "string" },
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
"form.propose": {
|
|
54
|
-
type: "object",
|
|
55
|
-
properties: {
|
|
56
|
-
operations: {
|
|
57
|
-
description: 'Array of patches. Use dotted paths such as layout.0.headline. Example item: {"op":"set","path":"layout.0.headline","value":"Välkommen"}.',
|
|
58
|
-
},
|
|
59
|
-
patches: {},
|
|
60
|
-
changes: {},
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
"draft.create": {
|
|
64
|
-
type: "object",
|
|
65
|
-
additionalProperties: false,
|
|
66
|
-
properties: {
|
|
67
|
-
collection: { type: "string" },
|
|
68
|
-
operations: { type: "array" },
|
|
69
|
-
},
|
|
70
|
-
required: ["collection", "operations"],
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
export const TOOL_DESCRIPTIONS = {
|
|
74
|
-
"schema.discover": "List allowlisted collections, globals, fields, and constraints. Never invent fields.",
|
|
75
|
-
"content.find": "Search or list allowlisted documents by title, slug, or id. Omit query to list recent documents. Returns identity only, not a summary.",
|
|
76
|
-
"content.findById": "Read one allowlisted document or global. Pass locale to read another language of the same document. For the currently open form, prefer form.read.",
|
|
77
|
-
"form.read": "Read the currently open unsaved form in the current language. No document ID needed.",
|
|
78
|
-
"media.view": `Look at images. Without ids: the image of the open media document; call it before writing or judging an alt text, caption or anything else about what it shows. With ids (from content.find on a media collection): up to ${MAX_IMAGES_PER_CALL} images to compare when picking one, ${MAX_IMAGES_PER_TURN} per turn at most.`,
|
|
79
|
-
"form.propose": 'Stage a structured patch against the open unsaved form in the current language. Call this as soon as you have the new field values. Do not ask the editor to confirm in chat; they approve in the panel. Example: {"op":"set","path":"layout.0.headline","value":"Välkommen"}. Requires editor approval.',
|
|
80
|
-
"admin.navigate": "Navigate Admin to an allowlisted collection, global, or unique document match.",
|
|
81
|
-
"draft.create": 'Stage creation of one draft document. Requires editor approval. Never publishes. The document starts empty: set top-level fields with {"op":"set","path":"title","value":"Start"}, and add each layout block with {"op":"blocks.insert","path":"layout","index":0,"blockType":"hero","data":{"headline":"Välkommen"}} using only block types and fields from the quoted context. Rich text fields take Markdown (headings, lists, **bold**, [links](https://…)).',
|
|
82
|
-
};
|
|
83
|
-
/** Tools a batch turn has no use for: it stays on its one saved document. */
|
|
84
|
-
const NOT_IN_BATCH = new Set(["admin.navigate", "draft.create"]);
|
|
85
|
-
/** The tools the model is given for this turn. */
|
|
86
|
-
export function toolNamesFor(bridge) {
|
|
87
|
-
return Object.keys(TOOL_JSON_SCHEMAS).filter((name) => !(bridge.batch && NOT_IN_BATCH.has(name)));
|
|
1
|
+
import { BUILTIN_TOOLS } from "./capabilities/index.js";
|
|
2
|
+
/** The tools the model is given for this turn: the built-in ones, then the extensions'. */
|
|
3
|
+
export function toolsFor(bridge, extensions = []) {
|
|
4
|
+
return [...BUILTIN_TOOLS, ...extensions.flatMap((extension) => extension.tools)].filter((tool) => !(bridge.batch && tool.notInBatch));
|
|
5
|
+
}
|
|
6
|
+
/** Capability ids of the tools the model is given for this turn. */
|
|
7
|
+
export function toolNamesFor(bridge, extensions) {
|
|
8
|
+
return toolsFor(bridge, extensions).map((tool) => tool.name);
|
|
88
9
|
}
|
|
89
10
|
/** Provider APIs reject dots in tool names (`^[a-zA-Z0-9_-]+$`). Capability IDs stay dotted. */
|
|
90
11
|
export function toProviderToolName(capability) {
|
|
91
12
|
return capability.replaceAll(".", "_");
|
|
92
13
|
}
|
|
93
|
-
export function fromProviderToolName(name) {
|
|
94
|
-
const match =
|
|
95
|
-
return match ?? name;
|
|
14
|
+
export function fromProviderToolName(name, extensions) {
|
|
15
|
+
const match = toolsFor({ locale: "" }, extensions).find((tool) => toProviderToolName(tool.name) === name);
|
|
16
|
+
return match?.name ?? name;
|
|
96
17
|
}
|
|
97
18
|
export async function executeTool(name, input, ctx) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
ok: true,
|
|
102
|
-
capability: name,
|
|
103
|
-
data: discoverSchema(ctx.req, ctx.options),
|
|
104
|
-
};
|
|
105
|
-
case "content.find":
|
|
106
|
-
return wrap(name, await findContent(ctx.req, ctx.options, {
|
|
107
|
-
collection: asString(input.collection),
|
|
108
|
-
query: asString(input.query) ?? "",
|
|
109
|
-
locale: ctx.bridge.locale,
|
|
110
|
-
}));
|
|
111
|
-
case "content.findById":
|
|
112
|
-
return wrap(name, await findContentById(ctx.req, ctx.options, {
|
|
113
|
-
collection: asString(input.collection) ?? ctx.bridge.collection,
|
|
114
|
-
global: asString(input.global) ?? ctx.bridge.global,
|
|
115
|
-
documentId: input.documentId ?? ctx.bridge.documentId,
|
|
116
|
-
locale: asString(input.locale) ?? ctx.bridge.locale,
|
|
117
|
-
}));
|
|
118
|
-
case "form.read": {
|
|
119
|
-
if (!ctx.bridge.collection && !ctx.bridge.global) {
|
|
120
|
-
return {
|
|
121
|
-
ok: false,
|
|
122
|
-
capability: name,
|
|
123
|
-
error: "tool_denied",
|
|
124
|
-
message: "No document is open. Open an allowlisted document to read the form.",
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
const fields = entityFields(ctx.req, ctx.options, {
|
|
128
|
-
collection: ctx.bridge.collection,
|
|
129
|
-
global: ctx.bridge.global,
|
|
130
|
-
});
|
|
131
|
-
const projected = projectRecord(ctx.formData ?? {}, fields, ctx.options.redact, ctx.bridge.collection ?? ctx.bridge.global);
|
|
132
|
-
return { ok: true, capability: name, data: projected };
|
|
133
|
-
}
|
|
134
|
-
case "media.view": {
|
|
135
|
-
ctx.imageBudget ??= { remaining: MAX_IMAGES_PER_TURN };
|
|
136
|
-
const ids = Array.isArray(input.ids)
|
|
137
|
-
? input.ids.filter((id) => typeof id === "string" || typeof id === "number")
|
|
138
|
-
: [];
|
|
139
|
-
if (ids.length > 0) {
|
|
140
|
-
const listed = await viewMediaList(ctx.req, ctx.options, {
|
|
141
|
-
collection: asString(input.collection),
|
|
142
|
-
ids,
|
|
143
|
-
budget: ctx.imageBudget,
|
|
144
|
-
});
|
|
145
|
-
if (!listed.ok) {
|
|
146
|
-
return { ok: false, capability: name, error: listed.error, message: listed.message };
|
|
147
|
-
}
|
|
148
|
-
return { ok: true, capability: name, data: listed.data, images: listed.images };
|
|
149
|
-
}
|
|
150
|
-
// The open document: its id comes from the bridge, never from the model.
|
|
151
|
-
if (ctx.imageBudget.remaining <= 0) {
|
|
152
|
-
return {
|
|
153
|
-
ok: false,
|
|
154
|
-
capability: name,
|
|
155
|
-
error: "tool_denied",
|
|
156
|
-
message: `You have looked at ${MAX_IMAGES_PER_TURN} images this turn, the most allowed.`,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
const viewed = await viewMedia(ctx.req, ctx.options, {
|
|
160
|
-
collection: ctx.bridge.collection,
|
|
161
|
-
documentId: ctx.bridge.documentId,
|
|
162
|
-
});
|
|
163
|
-
if (!viewed.ok) {
|
|
164
|
-
return { ok: false, capability: name, error: viewed.error, message: viewed.message };
|
|
165
|
-
}
|
|
166
|
-
ctx.imageBudget.remaining -= 1;
|
|
167
|
-
return { ok: true, capability: name, data: viewed.data, images: [viewed.image] };
|
|
168
|
-
}
|
|
169
|
-
case "admin.navigate": {
|
|
170
|
-
if (!canNavigate(ctx.options)) {
|
|
171
|
-
return {
|
|
172
|
-
ok: false,
|
|
173
|
-
capability: name,
|
|
174
|
-
error: "tool_denied",
|
|
175
|
-
message: "Nothing is allowlisted.",
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
const result = await navigateAdmin(ctx.req, ctx.options, {
|
|
179
|
-
collection: asString(input.collection),
|
|
180
|
-
global: asString(input.global),
|
|
181
|
-
documentId: input.documentId,
|
|
182
|
-
query: asString(input.query),
|
|
183
|
-
locale: ctx.bridge.locale,
|
|
184
|
-
});
|
|
185
|
-
if (!result.ok) {
|
|
186
|
-
return { ok: false, capability: name, error: result.error, message: result.message };
|
|
187
|
-
}
|
|
188
|
-
await writeAudit(ctx.req, ctx.options, {
|
|
189
|
-
capability: name,
|
|
190
|
-
risk: "auto",
|
|
191
|
-
outcome: "executed",
|
|
192
|
-
targetCollection: asString(input.collection),
|
|
193
|
-
targetGlobal: asString(input.global),
|
|
194
|
-
locale: ctx.bridge.locale,
|
|
195
|
-
});
|
|
196
|
-
if ("href" in result) {
|
|
197
|
-
return { ok: true, capability: name, navigate: { href: result.href, label: result.label } };
|
|
198
|
-
}
|
|
199
|
-
return { ok: true, capability: name, navigate: { candidates: result.candidates } };
|
|
200
|
-
}
|
|
201
|
-
case "form.propose":
|
|
202
|
-
case "draft.create": {
|
|
203
|
-
if (ctx.bridge.batch && name === "draft.create") {
|
|
204
|
-
return { ok: false, capability: name, error: "tool_denied", message: "Not in a batch." };
|
|
205
|
-
}
|
|
206
|
-
const batch = ctx.bridge.batch;
|
|
207
|
-
const capability = batch ? "content.update" : name;
|
|
208
|
-
const proposed = await proposeChange(ctx.req, ctx.options, {
|
|
209
|
-
capability,
|
|
210
|
-
allowedPaths: batch?.allowedPaths,
|
|
211
|
-
requestId: batch?.requestId,
|
|
212
|
-
collection: name === "draft.create" ? asString(input.collection) : ctx.bridge.collection,
|
|
213
|
-
global: name === "form.propose" ? ctx.bridge.global : undefined,
|
|
214
|
-
documentId: name === "form.propose" ? ctx.bridge.documentId : undefined,
|
|
215
|
-
locale: ctx.bridge.locale,
|
|
216
|
-
language: ctx.bridge.uiLanguage ?? ctx.bridge.language,
|
|
217
|
-
operations: input.operations ?? input.patches ?? input.changes ?? input.ops,
|
|
218
|
-
formData: name === "form.propose" && !batch ? ctx.formData : undefined,
|
|
219
|
-
});
|
|
220
|
-
if (!proposed.ok) {
|
|
221
|
-
return { ok: false, capability: name, error: proposed.error, message: proposed.message };
|
|
222
|
-
}
|
|
223
|
-
// Models count characters badly; the count lets them correct a text that misses a length.
|
|
224
|
-
const counted = Object.keys(proposed.characters).length > 0;
|
|
225
|
-
return {
|
|
226
|
-
ok: true,
|
|
227
|
-
capability: name,
|
|
228
|
-
...(counted ? { data: { characters: proposed.characters } } : {}),
|
|
229
|
-
pendingApproval: {
|
|
230
|
-
title: proposed.title,
|
|
231
|
-
summary: proposed.summary,
|
|
232
|
-
approvalId: proposed.approvalId,
|
|
233
|
-
nonce: proposed.nonce,
|
|
234
|
-
capability,
|
|
235
|
-
},
|
|
236
|
-
lines: proposed.lines,
|
|
237
|
-
preview: proposed.preview,
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
default:
|
|
241
|
-
return { ok: false, capability: name, error: "tool_denied", message: "Unknown tool." };
|
|
19
|
+
const tool = toolsFor(ctx.bridge, ctx.extensions).find((entry) => entry.name === name);
|
|
20
|
+
if (!tool) {
|
|
21
|
+
return { ok: false, capability: name, error: "tool_denied", message: "Unknown tool." };
|
|
242
22
|
}
|
|
23
|
+
return tool.run(input, ctx);
|
|
243
24
|
}
|
|
244
25
|
/**
|
|
245
26
|
* What the model gets back from a tool: images as file parts, each after a line with its id when it
|
|
@@ -272,12 +53,3 @@ export function toolModelOutput(outcome) {
|
|
|
272
53
|
],
|
|
273
54
|
};
|
|
274
55
|
}
|
|
275
|
-
function wrap(capability, result) {
|
|
276
|
-
if (!result.ok) {
|
|
277
|
-
return { ok: false, capability, error: result.error, message: result.message };
|
|
278
|
-
}
|
|
279
|
-
return { ok: true, capability, data: "results" in result ? result.results : result.data };
|
|
280
|
-
}
|
|
281
|
-
function asString(value) {
|
|
282
|
-
return typeof value === "string" && value.trim() ? value : undefined;
|
|
283
|
-
}
|
package/dist/types.d.ts
CHANGED
|
@@ -52,5 +52,12 @@ export type EditorAssistantPluginOptions = {
|
|
|
52
52
|
audit?: {
|
|
53
53
|
collection?: string;
|
|
54
54
|
};
|
|
55
|
+
/**
|
|
56
|
+
* Extensions from other packages to turn on, by id: `true` for all their tools, or the tools
|
|
57
|
+
* allowed. Off until named here. See `AssistantExtension` in `extensions/contract.ts`.
|
|
58
|
+
*/
|
|
59
|
+
extensions?: Record<string, true | {
|
|
60
|
+
tools: string[];
|
|
61
|
+
}>;
|
|
55
62
|
};
|
|
56
63
|
export declare function defineEditorAssistantConfig<T extends EditorAssistantPluginOptions>(options: T): T;
|
package/dist/view.d.ts
CHANGED
|
@@ -10,6 +10,11 @@ export type AdminView = {
|
|
|
10
10
|
} | {
|
|
11
11
|
kind: "global";
|
|
12
12
|
global: string;
|
|
13
|
+
}
|
|
14
|
+
/** Another Admin view, such as one a plugin adds: its path under the admin route. */
|
|
15
|
+
| {
|
|
16
|
+
kind: "custom";
|
|
17
|
+
path: string;
|
|
13
18
|
} | {
|
|
14
19
|
kind: "other";
|
|
15
20
|
};
|
|
@@ -21,8 +26,11 @@ export declare function parseAdminView(pathname: string, adminRoute?: string): A
|
|
|
21
26
|
* user is known but the login page is still open.
|
|
22
27
|
*/
|
|
23
28
|
export declare function isAuthView(pathname: string, adminRoute: string, routes?: Partial<Record<string, string>>): boolean;
|
|
24
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Keep only views that point at something the host allowlisted, and custom views an enabled
|
|
31
|
+
* extension describes (`customPaths`).
|
|
32
|
+
*/
|
|
25
33
|
export declare function sanitizeAdminView(value: unknown, allowed: {
|
|
26
34
|
collections: Record<string, unknown>;
|
|
27
35
|
globals: Record<string, unknown>;
|
|
28
|
-
}): AdminView;
|
|
36
|
+
}, customPaths?: string[]): AdminView;
|
package/dist/view.js
CHANGED
|
@@ -22,7 +22,10 @@ export function parseAdminView(pathname, adminRoute = "/admin") {
|
|
|
22
22
|
if (section === "globals" && slug) {
|
|
23
23
|
return { kind: "global", global: slug };
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
if (section === "collections" || section === "globals") {
|
|
26
|
+
return { kind: "other" };
|
|
27
|
+
}
|
|
28
|
+
return { kind: "custom", path: `/${segments.join("/")}` };
|
|
26
29
|
}
|
|
27
30
|
/** Payload's own sign-in routes (`admin.routes`), with Payload's defaults. */
|
|
28
31
|
const AUTH_ROUTES = {
|
|
@@ -51,8 +54,11 @@ export function isAuthView(pathname, adminRoute, routes = {}) {
|
|
|
51
54
|
return Boolean(route) && (rest === route || rest.startsWith(`${route}/`));
|
|
52
55
|
});
|
|
53
56
|
}
|
|
54
|
-
/**
|
|
55
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Keep only views that point at something the host allowlisted, and custom views an enabled
|
|
59
|
+
* extension describes (`customPaths`).
|
|
60
|
+
*/
|
|
61
|
+
export function sanitizeAdminView(value, allowed, customPaths = []) {
|
|
56
62
|
if (!value || typeof value !== "object") {
|
|
57
63
|
return { kind: "other" };
|
|
58
64
|
}
|
|
@@ -76,6 +82,10 @@ export function sanitizeAdminView(value, allowed) {
|
|
|
76
82
|
return has(allowed.globals, view.global)
|
|
77
83
|
? { kind: "global", global: view.global }
|
|
78
84
|
: { kind: "other" };
|
|
85
|
+
case "custom":
|
|
86
|
+
return typeof view.path === "string" && customPaths.includes(view.path)
|
|
87
|
+
? { kind: "custom", path: view.path }
|
|
88
|
+
: { kind: "other" };
|
|
79
89
|
default:
|
|
80
90
|
return { kind: "other" };
|
|
81
91
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simmalugnt-se/payload-editor-assistant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Context-aware editor assistant plugin for Payload Admin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"payload",
|
|
@@ -8,11 +8,7 @@
|
|
|
8
8
|
],
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"author": "Simmalugnt AB",
|
|
11
|
-
"
|
|
12
|
-
"type": "git",
|
|
13
|
-
"url": "git+https://github.com/simmalugnt-se/payload-plugins.git",
|
|
14
|
-
"directory": "packages/payload-editor-assistant"
|
|
15
|
-
},
|
|
11
|
+
"homepage": "https://simmalugnt.se",
|
|
16
12
|
"type": "module",
|
|
17
13
|
"engines": {
|
|
18
14
|
"node": ">=22"
|