@simmalugnt-se/payload-editor-assistant 0.1.0 → 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/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ Breaking: options that were accepted but did nothing are removed.
6
+
7
+ - `tools` (and the `HostToolDefinition` type): host tools were validated but never given to the
8
+ model. They will come back when the assistant can run them.
9
+ - `audit.retentionDays`: nothing deleted old audit entries.
10
+
11
+ Also: README documents `validateProposal`.
12
+
13
+ ## 0.1.1
14
+
15
+ - Provider failures are logged on the server through Payload's logger (missing model, `ai` not
16
+ installed, or the provider's own error), instead of only showing `provider_error` in the drawer.
17
+ - README: how to connect the provider's API key, what the assistant may do, and the options.
18
+
19
+ ## 0.1.0
20
+
21
+ - First release.
22
+ - `ai` is a required peer: the AI SDK is loaded with a literal import so bundlers can resolve it.
package/README.md CHANGED
@@ -9,24 +9,85 @@ your choice.
9
9
 
10
10
  ## Setup
11
11
 
12
- ```ts
13
- import { deepSeek } from "@ai-sdk/deepseek";
14
- import { editorAssistantPlugin } from "@simmalugnt-se/payload-editor-assistant";
15
-
16
- plugins: [
17
- editorAssistantPlugin({
18
- model: deepSeek("deepseek-chat"),
19
- collections: {
20
- pages: {
21
- capabilities: ["content.find", "content.findById", "form.read", "form.propose", "draft.create"],
22
- },
23
- },
24
- globals: {
25
- header: { capabilities: ["content.findById", "form.read", "form.propose"] },
26
- },
27
- }),
28
- ];
29
- ```
30
-
31
- Then regenerate the import map (`pnpm payload generate:importmap`). See `EditorAssistantPluginOptions`
32
- for allowlists, host tools and instructions. Design notes live in `docs/research` and `.wayfinder`.
12
+ 1. Install the plugin, the AI SDK and a provider (DeepSeek here; any AI SDK provider works):
13
+
14
+ ```bash
15
+ pnpm add @simmalugnt-se/payload-editor-assistant ai @ai-sdk/deepseek
16
+ ```
17
+
18
+ 2. Put the provider's API key in the server environment, e.g. `.env.local` locally and the
19
+ hosting provider's environment variables in production. Never expose it to the browser
20
+ (no `NEXT_PUBLIC_` prefix):
21
+
22
+ ```bash
23
+ DEEPSEEK_API_KEY=sk-...
24
+ ```
25
+
26
+ 3. Create the model with that key and pass it to the plugin:
27
+
28
+ ```ts
29
+ import { createDeepSeek } from "@ai-sdk/deepseek";
30
+ import { editorAssistantPlugin } from "@simmalugnt-se/payload-editor-assistant";
31
+
32
+ const deepSeek = createDeepSeek({ apiKey: process.env.DEEPSEEK_API_KEY });
33
+
34
+ plugins: [
35
+ editorAssistantPlugin({
36
+ model: deepSeek("deepseek-chat"),
37
+ collections: {
38
+ pages: {
39
+ capabilities: ["content.find", "content.findById", "form.read", "form.propose", "draft.create"],
40
+ },
41
+ },
42
+ globals: {
43
+ header: { capabilities: ["content.findById", "form.read", "form.propose"] },
44
+ },
45
+ }),
46
+ ];
47
+ ```
48
+
49
+ 4. Regenerate the import map: `pnpm payload generate:importmap`.
50
+
51
+ ## API keys
52
+
53
+ The plugin has no provider or key of its own: it only receives the `model` you create, and the model
54
+ is called on the server. To switch provider, install another AI SDK provider and create the model
55
+ with its key, e.g. `createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY })`.
56
+
57
+ Providers also read a default environment variable (`DEEPSEEK_API_KEY`, `ANTHROPIC_API_KEY`, ...)
58
+ when no `apiKey` is passed, but passing it explicitly keeps the connection visible in your config.
59
+
60
+ If the key is missing or wrong, the drawer shows `provider_error` and the cause is logged on the
61
+ server through Payload's logger.
62
+
63
+ ## What the assistant may do
64
+
65
+ Nothing is reachable unless you list it. Each collection or global gets the capabilities it allows:
66
+
67
+ | Capability | What it does | Needs approval |
68
+ |---|---|---|
69
+ | `content.find` | search and list documents | no |
70
+ | `content.findById` | read one document | no |
71
+ | `form.read` | read the open edit form, including unsaved changes | no |
72
+ | `form.propose` | propose changes to the open form; the editor reviews them before they apply | yes |
73
+ | `draft.create` | create a new draft document (collections only) | yes |
74
+
75
+ The assistant never publishes. Approved form changes land in the open form and the editor saves as
76
+ usual; an approved `draft.create` saves a new, unpublished draft. The `users` collection, Payload's own collections and fields that look like
77
+ secrets (`password`, `token`, `apiKey`, ...) are always off limits.
78
+
79
+ ## Options
80
+
81
+ | Option | |
82
+ |---|---|
83
+ | `model` | The AI SDK model, or `({ userId }) => model` to choose per user. Required unless `disabled`. |
84
+ | `collections`, `globals` | `{ [slug]: { capabilities, fields? } }`. `fields` narrows what is visible: `{ include: [...] }` or `{ exclude: [...] }`. |
85
+ | `denyFields` | Field names hidden from the assistant in every collection and global. |
86
+ | `instructions` | Project rules the assistant cannot infer from the schema, e.g. "the start page has slug `home`". |
87
+ | `shortcuts` | One-click prompts in the drawer: `{ id, label, prompt, when?, entities? }`. |
88
+ | `enabled` | `({ userId }) => boolean` to offer the assistant to some users only. |
89
+ | `redact` | `({ slug, data }) => data` to strip values before documents reach the model. |
90
+ | `validateProposal` | `({ operations, data }) => ({ ok: true } \| { ok: false, errors })` for project rules. Runs before a proposal reaches the editor; errors go back to the assistant, which can correct itself. |
91
+ | `trustedOrigins` | Origins allowed to call the assistant's endpoints. Default: only the origin serving Admin. When set, only the listed origins. |
92
+ | `audit.collection` | Slug of the collection that logs assistant actions (default `editor-assistant-audit`). |
93
+ | `disabled` | Turn the plugin off without removing its config. |
@@ -1,15 +1,13 @@
1
- import type { EditorAssistantPluginOptions, EntityAllowlist, HostToolDefinition } from "../types.ts";
1
+ import type { EditorAssistantPluginOptions, EntityAllowlist } from "../types.ts";
2
2
  export type ValidatedEditorAssistantOptions = EditorAssistantPluginOptions & {
3
3
  disabled: boolean;
4
4
  collections: Record<string, EntityAllowlist>;
5
5
  globals: Record<string, EntityAllowlist>;
6
6
  denyFields: string[];
7
7
  shortcuts: NonNullable<EditorAssistantPluginOptions["shortcuts"]>;
8
- tools: HostToolDefinition[];
9
8
  trustedOrigins: string[];
10
9
  audit: {
11
10
  collection: string;
12
- retentionDays?: number;
13
11
  };
14
12
  };
15
13
  export declare function validateEditorAssistantOptions(options: EditorAssistantPluginOptions): ValidatedEditorAssistantOptions;
@@ -1,6 +1,5 @@
1
- import { BUILTIN_RISK, EMBEDDED_ONLY_CAPABILITIES, isBuiltinCapability, isEntityCapability, } from "../capabilities.js";
1
+ import { isEntityCapability } from "../capabilities.js";
2
2
  import { isDeniedCollectionSlug } from "./denied-fields.js";
3
- const RISK_ORDER = { auto: 0, approval: 1, forbidden: 2 };
4
3
  export function validateEditorAssistantOptions(options) {
5
4
  const disabled = options.disabled === true;
6
5
  const collections = options.collections ?? {};
@@ -11,7 +10,6 @@ export function validateEditorAssistantOptions(options) {
11
10
  }
12
11
  validateEntityMap("collections", collections, { allowDraftCreate: true });
13
12
  validateEntityMap("globals", globals, { allowDraftCreate: false });
14
- validateHostTools(options.tools ?? []);
15
13
  }
16
14
  return {
17
15
  ...options,
@@ -20,11 +18,9 @@ export function validateEditorAssistantOptions(options) {
20
18
  globals,
21
19
  denyFields: options.denyFields ?? [],
22
20
  shortcuts: options.shortcuts ?? [],
23
- tools: options.tools ?? [],
24
21
  trustedOrigins: options.trustedOrigins ?? [],
25
22
  audit: {
26
23
  collection: options.audit?.collection ?? "editor-assistant-audit",
27
- retentionDays: options.audit?.retentionDays,
28
24
  },
29
25
  };
30
26
  }
@@ -62,25 +58,3 @@ function validateFields(entry, label) {
62
58
  throw new Error(`payload-editor-assistant: ${label} fields.exclude must be an array.`);
63
59
  }
64
60
  }
65
- function validateHostTools(tools) {
66
- for (const tool of tools) {
67
- if (!tool.id || !tool.capabilityId) {
68
- throw new Error("payload-editor-assistant: host tools require id and capabilityId.");
69
- }
70
- const risk = tool.risk ?? "approval";
71
- if (risk !== "auto" && risk !== "approval" && risk !== "forbidden") {
72
- throw new Error(`payload-editor-assistant: host tool "${tool.id}" has invalid risk.`);
73
- }
74
- if (isBuiltinCapability(tool.capabilityId)) {
75
- if (RISK_ORDER[risk] < RISK_ORDER[BUILTIN_RISK[tool.capabilityId]]) {
76
- throw new Error(`payload-editor-assistant: host tool "${tool.id}" cannot loosen risk below ${tool.capabilityId}.`);
77
- }
78
- if (tool.mcp && EMBEDDED_ONLY_CAPABILITIES.has(tool.capabilityId)) {
79
- throw new Error(`payload-editor-assistant: host tool "${tool.id}" cannot mark ${tool.capabilityId} as MCP-eligible.`);
80
- }
81
- }
82
- if (risk === "auto" && tool.mcp === true && tool.capabilityId === "form.propose") {
83
- throw new Error(`payload-editor-assistant: host tool "${tool.id}" cannot expose form writes to MCP.`);
84
- }
85
- }
86
- }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export type { BuiltinCapability, CapabilityRisk, EntityCapability } from "./capabilities.ts";
2
2
  export { BUILTIN_CAPABILITIES, BUILTIN_RISK, ENTITY_CAPABILITIES } from "./capabilities.ts";
3
3
  export { editorAssistantPlugin } from "./plugin.ts";
4
- export type { AssistantShortcut, EditorAssistantPluginOptions, EntityAllowlist, FieldAllowlist, HostLanguageModel, HostToolDefinition, ModelResolver, ProposalValidationResult, } from "./types.ts";
4
+ export type { AssistantShortcut, EditorAssistantPluginOptions, EntityAllowlist, FieldAllowlist, HostLanguageModel, ModelResolver, ProposalValidationResult, } from "./types.ts";
5
5
  export { defineEditorAssistantConfig } from "./types.ts";
@@ -1,3 +1,4 @@
1
+ import { PACKAGE_NAME } from "../package-name.js";
1
2
  import { loadTurnContext } from "./context.js";
2
3
  import { needsProposeRetry } from "./propose-loop.js";
3
4
  import { buildSystemPrompt } from "./system-prompt.js";
@@ -9,10 +10,12 @@ const TIMEOUT_MS = 60_000;
9
10
  export async function runAgentTurn(req, options, input) {
10
11
  const model = await resolveModel(options, req.user?.id);
11
12
  if (!model) {
13
+ logProviderError(req, "no model: the `model` option resolved to nothing for this user");
12
14
  return { ok: false, error: "provider_error" };
13
15
  }
14
16
  const ai = await loadAiSdk();
15
17
  if (!ai) {
18
+ logProviderError(req, "could not load the AI SDK: is `ai` installed in the project?");
16
19
  return { ok: false, error: "provider_error" };
17
20
  }
18
21
  const ctx = {
@@ -39,6 +42,7 @@ export async function runAgentTurn(req, options, input) {
39
42
  signal,
40
43
  });
41
44
  if (!first) {
45
+ logProviderError(req, "the model returned no result");
42
46
  return { ok: false, error: "provider_error" };
43
47
  }
44
48
  const collected = collectResult(String(first.text ?? ""), first, captured, input.bridge.language ?? input.bridge.locale);
@@ -79,6 +83,7 @@ export async function runAgentTurn(req, options, input) {
79
83
  if (signal.aborted) {
80
84
  return { ok: false, error: "timeout" };
81
85
  }
86
+ logProviderError(req, "the model provider failed", error);
82
87
  return {
83
88
  ok: false,
84
89
  error: "provider_error",
@@ -246,6 +251,13 @@ async function resolveModel(options, userId) {
246
251
  }
247
252
  return options.model;
248
253
  }
254
+ /**
255
+ * The drawer only shows "provider_error"; the cause (missing key, wrong model id, provider outage)
256
+ * belongs in the server log where the host can see it.
257
+ */
258
+ function logProviderError(req, message, error) {
259
+ req.payload?.logger?.error({ err: error, msg: `${PACKAGE_NAME}: ${message}` });
260
+ }
249
261
  async function loadAiSdk() {
250
262
  try {
251
263
  // A literal specifier: bundlers only resolve `import()` they can read, and the host's `ai`
package/dist/types.d.ts CHANGED
@@ -22,13 +22,6 @@ export type AssistantShortcut = {
22
22
  /** Collection or global slugs where the shortcut is shown. Views without an entity (dashboard) are not filtered. */
23
23
  entities?: string[];
24
24
  };
25
- export type HostToolRisk = "auto" | "approval" | "forbidden";
26
- export type HostToolDefinition = {
27
- id: string;
28
- capabilityId: string;
29
- risk?: HostToolRisk;
30
- mcp?: boolean;
31
- };
32
25
  export type ProposalValidationResult = {
33
26
  ok: true;
34
27
  } | {
@@ -43,7 +36,6 @@ export type EditorAssistantPluginOptions = {
43
36
  denyFields?: string[];
44
37
  instructions?: string;
45
38
  shortcuts?: AssistantShortcut[];
46
- tools?: HostToolDefinition[];
47
39
  validateProposal?: (input: unknown) => ProposalValidationResult | Promise<ProposalValidationResult>;
48
40
  redact?: (input: {
49
41
  slug: string;
@@ -55,7 +47,6 @@ export type EditorAssistantPluginOptions = {
55
47
  trustedOrigins?: string[];
56
48
  audit?: {
57
49
  collection?: string;
58
- retentionDays?: number;
59
50
  };
60
51
  };
61
52
  export declare function defineEditorAssistantConfig<T extends EditorAssistantPluginOptions>(options: T): T;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simmalugnt-se/payload-editor-assistant",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Context-aware editor assistant plugin for Payload Admin",
5
5
  "keywords": [
6
6
  "payload",
@@ -18,7 +18,8 @@
18
18
  "node": ">=22"
19
19
  },
20
20
  "files": [
21
- "dist"
21
+ "dist",
22
+ "CHANGELOG.md"
22
23
  ],
23
24
  "sideEffects": [
24
25
  "*.css"