@saltcorn/copilot 0.7.5 → 0.8.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.
@@ -14,7 +14,7 @@ const optionNames = (options = []) =>
14
14
  .map((opt) =>
15
15
  typeof opt === "string"
16
16
  ? opt
17
- : opt?.name || opt?.label || opt?.value || "",
17
+ : opt?.name || opt?.label || opt?.value || ""
18
18
  )
19
19
  .filter(Boolean);
20
20
 
@@ -39,8 +39,12 @@ const summarizeTriggerActionMatrix = () => {
39
39
  const pool = table_triggers.includes(when) ? allActions : noRowActions;
40
40
  return `* ${when}: ${joinOptionNames(pool)}`;
41
41
  });
42
- const notes = `Triggers requiring a table context: ${table_triggers.join(", ")}.
43
- Additional triggers that commonly use contextual only_if checks: ${additional_triggers_with_onlyif.join(", ")}.`;
42
+ const notes = `Triggers requiring a table context: ${table_triggers.join(
43
+ ", "
44
+ )}.
45
+ Additional triggers that commonly use contextual only_if checks: ${additional_triggers_with_onlyif.join(
46
+ ", "
47
+ )}.`;
44
48
  return `${notes}
45
49
  ${lines.join("\n")}`;
46
50
  } catch (e) {
@@ -58,8 +62,10 @@ const summarizeTables = async () => {
58
62
  const rows = tables.map((table) => {
59
63
  const description = table.description ? ` – ${table.description}` : "";
60
64
  const fields = (table.fields || [])
61
- .slice(0, 8)
62
- .map((f) => `${f.name}:${fieldType(f)}`)
65
+ .map((f) => {
66
+ const tag = f.calculated && !f.stored ? " (virtual, read-only)" : "";
67
+ return `${f.name}:${fieldType(f)}${tag}`;
68
+ })
63
69
  .join(", ");
64
70
  const fieldText = fields ? ` fields: ${fields}` : "";
65
71
  return `* ${table.name}${description}${fieldText}`;
@@ -77,7 +83,7 @@ const steps = async () => {
77
83
 
78
84
  let stateActions = getState().actions;
79
85
  const stateActionList = Object.entries(stateActions).filter(
80
- ([k, v]) => !v.disableInWorkflow,
86
+ ([k, v]) => !v.disableInWorkflow
81
87
  );
82
88
 
83
89
  const stepTypeAndCfg = Object.keys(actionExplainers).map((actionName) => {
@@ -85,7 +91,7 @@ const steps = async () => {
85
91
  step_type: { type: "string", enum: [actionName] },
86
92
  };
87
93
  const myFields = actionFields.filter(
88
- (f) => f.showIf?.wf_action_name === actionName,
94
+ (f) => f.showIf?.wf_action_name === actionName
89
95
  );
90
96
  const required = ["step_type"];
91
97
  myFields.forEach((f) => {
@@ -147,14 +153,15 @@ const steps = async () => {
147
153
  const table = Table.findOne({ id: trigger.table_id });
148
154
  const fieldSpecs = [];
149
155
  table.fields.forEach((f) => {
156
+ if (f.calculated && !f.stored) return; // virtual fields have no DB column
150
157
  // TODO fkeys dereferenced.
151
158
  fieldSpecs.push(`${f.name} with ${f.pretty_type} type`);
152
159
  });
153
160
  properties.row_expr = {
154
161
  type: "string",
155
162
  description: `JavaScript expression for the input to the action. This should be an expression for an object, with the following field name and types: ${fieldSpecs.join(
156
- "; ",
157
- )}.`,
163
+ "; "
164
+ )}. IMPORTANT: omit any non-stored calculated fields — they have no database column and cannot be written. Only include regular fields and stored calculated fields. Keep this expression simple: it should reference values already in the context, not perform inline queries or complex logic. If the values are not yet in context, add a dedicated preceding step (TableQuery, run_js_code, or whichever fits) to fetch or compute them first.`,
158
165
  };
159
166
  }
160
167
  const required = ["step_type"];
@@ -218,7 +225,9 @@ class GenerateWorkflow {
218
225
  enum: Trigger.when_options,
219
226
  },
220
227
  trigger_table: {
221
- description: `If the workflow trigger is ${table_triggers.join(", ")}, the name of the table that triggers the workflow`,
228
+ description: `If the workflow trigger is ${table_triggers.join(
229
+ ", "
230
+ )}, the name of the table that triggers the workflow`,
222
231
  type: "string",
223
232
  },
224
233
  },
@@ -229,7 +238,7 @@ class GenerateWorkflow {
229
238
  const actionExplainers = WorkflowStep.builtInActionExplainers();
230
239
  let stateActions = getState().actions;
231
240
  const stateActionList = Object.entries(stateActions).filter(
232
- ([k, v]) => !v.disableInWorkflow,
241
+ ([k, v]) => !v.disableInWorkflow
233
242
  );
234
243
  const [tableSummary, triggerMatrix] = await Promise.all([
235
244
  summarizeTables(),
@@ -242,30 +251,42 @@ class GenerateWorkflow {
242
251
  .filter(Boolean)
243
252
  .join("\n\n");
244
253
 
245
- return `Use the generate_workflow tool to construct computational workflows according to specifications. You must create
254
+ return `Use the generate_workflow tool to construct computational workflows according to specifications. You must create
246
255
  the workflow by calling the generate_workflow tool, with the step required to implement the specification.
247
-
256
+
248
257
  ${contextBlocks}
249
-
250
- The steps are specified as JSON objects. Each step has a name, specified in the step_name key in the JSON object.
258
+
259
+ The steps are specified as JSON objects. Each step has a name, specified in the step_name key in the JSON object.
251
260
  The step name should be a valid JavaScript identifier.
252
-
261
+
253
262
  When the user explicitly names an action/step type (for example "ForLoop" or "Toast"), ensure the generated workflow contains at least one step whose step_configuration.step_type exactly matches every requested action. Only skip this if the action genuinely does not exist; in that case, clearly explain the omission in the workflow description.
254
263
 
264
+ CRITICAL — non-stored calculated fields cannot be written:
265
+ In the table summary above, fields marked (virtual, read-only) are non-stored calculated fields — they have no database column and are computed on-the-fly by Saltcorn. Never include such fields in modify_row row expressions, run_js_code return objects, or SQL UPDATE statements. If you see a field in the table summary marked (virtual, read-only), treat it as read-only everywhere: omit it from any write operation. Only regular fields and stored calculated fields (not marked virtual) have database columns and can be written. Virtual fields refresh automatically when the fields they depend on change — no explicit update is needed.
266
+
267
+ IMPORTANT — keep row expressions simple; use dedicated steps for data fetching:
268
+ Row expressions (e.g. in modify_row) should be simple references to values already in the context — not inline queries or complex logic. If the values you need are not already in context, add a dedicated step before the row expression step to fetch or compute them and write the results into the context. Choose the step type that fits the job: TableQuery to query a table, run_js_code for custom computation, or any other appropriate step. Each step should do one clear thing; the row expression then just picks the relevant context values.
269
+
270
+ CRITICAL — every workflow must form a single connected chain from the first step to the last:
271
+ - Every step except the very last one MUST have a next_step that names another step in the workflow.
272
+ - The very last step must have next_step omitted or set to an empty string to terminate the workflow.
273
+ - No step may be an island: every step must be reachable by following next_step links from the first step.
274
+ - Before submitting, mentally trace the path: first_step → next_step → … → last_step. If any step is unreachable or any link is missing, fix it before calling the tool.
275
+
255
276
  Each run of the workflow is executed in the presence of a context, which is a JavaScript object that individual
256
- steps can read values from and write values to. This context is a state that is persisted on disk for each workflow
257
- run.
258
-
259
- Each step can have a next_step key which is the name of the next step, or a JavaScript expression which evaluates
260
- to the name of the next step based on the context. In the evaluation of the next step, each value in the context is
261
- in scope and can be addressed directly. Identifiers for the step names are also in scope, the name of the next step
262
- can be used directly without enclosing it in quotes to form a string.
263
-
277
+ steps can read values from and write values to. This context is a state that is persisted on disk for each workflow
278
+ run.
279
+
280
+ Each step can have a next_step key which is the name of the next step, or a JavaScript expression which evaluates
281
+ to the name of the next step based on the context. In the evaluation of the next step, each value in the context is
282
+ in scope and can be addressed directly. Identifiers for the step names are also in scope, the name of the next step
283
+ can be used directly without enclosing it in quotes to form a string.
284
+
264
285
  For example, if the context contains a value x which is an integer and you have steps named "too_low" and "too_high",
265
286
  and you would like the next step to be too_low if x is less than 10 and too_high otherwise,
266
287
  use this as the next_step expression: x<10 ? too_low : too_high
267
-
268
- If the next_step is omitted then the workflow terminates.
288
+
289
+ If the next_step is omitted then the workflow terminates. Only the final step should have next_step omitted.
269
290
 
270
291
  Each step has a step_configuration object which contains the step type and the specific parameters of
271
292
  that step type. You should specify the step type in the step_type subfield of the step_configuration
@@ -282,9 +303,11 @@ class GenerateWorkflow {
282
303
  step types:
283
304
 
284
305
  run_js_code: if the step_type is "run_js_code" then the step object should include the JavaScript code to be executed in the "code"
285
- key. You can use await in the code if you need to run asynchronous code. The values in the context are directly in scope and can be accessed using their name. In addition, the variable
306
+ key. You can use await in the code if you need to run asynchronous code. The values in the context are directly in scope and can be accessed using their name. In addition, the variable
286
307
  "context" is also in scope and can be used to address the context as a whole. To write values to the context, return an
287
- object. The values in this object will be written into the current context. If a value already exists in the context
308
+ object. The following Saltcorn models are already available in scope without any require: Table, Row, Field, User, View, Trigger, Page, File — use them directly.
309
+ When you need to require other modules, always use a plain require call, e.g. const moment = require('moment');
310
+ NEVER use the pattern const X = X || require(...) — this causes a ReferenceError because const variables cannot be referenced before initialization. The values in this object will be written into the current context. If a value already exists in the context
288
311
  it will be overwritten. For example, If the context contains values x and y which are numbers and you would like to push
289
312
  the value "sum" which is the sum of x and y, then use this as the code: return {sum: x+y}. You cannot set the next step in the
290
313
  return object or by returning a string from a run_js_code step, this will not work. To set the next step from a code action, always use the next_step property of the step object.
@@ -318,8 +341,25 @@ class GenerateWorkflow {
318
341
  static async execute(
319
342
  { workflow_steps, workflow_name, when_trigger, trigger_table },
320
343
  req,
344
+ context_vars
321
345
  ) {
322
- const steps = this.process_all_steps(workflow_steps);
346
+ let allSteps = workflow_steps;
347
+ let initContextStep = null;
348
+ if (context_vars && Object.keys(context_vars).length) {
349
+ const firstUserStep = allSteps[0]?.step_name || "";
350
+ initContextStep = {
351
+ step_name: "init_context",
352
+ only_if: "",
353
+ next_step: firstUserStep,
354
+ step_configuration: {
355
+ step_type: "run_js_code",
356
+ run_where: "Server",
357
+ code: `return ${JSON.stringify(context_vars, null, 2)};`,
358
+ },
359
+ };
360
+ allSteps = [initContextStep, ...allSteps];
361
+ }
362
+ const steps = this.process_all_steps(allSteps);
323
363
  let table_id;
324
364
  if (trigger_table) {
325
365
  const table = Table.findOne({ name: trigger_table });
@@ -333,10 +373,17 @@ class GenerateWorkflow {
333
373
  action: "Workflow",
334
374
  configuration: {},
335
375
  });
336
- for (const step of steps) {
376
+ // Insert user steps first so the init_context next_step target already
377
+ // exists in the DB when init_context is created.
378
+ const [initStep, ...userSteps] = initContextStep ? steps : [null, ...steps];
379
+ for (const step of initContextStep ? userSteps : steps) {
337
380
  step.trigger_id = trigger.id;
338
381
  await WorkflowStep.create(step);
339
382
  }
383
+ if (initStep) {
384
+ initStep.trigger_id = trigger.id;
385
+ await WorkflowStep.create(initStep);
386
+ }
340
387
  Trigger.emitEvent("AppChange", `Trigger ${trigger.name}`, req?.user, {
341
388
  entity_type: "Trigger",
342
389
  entity_name: trigger.name,
@@ -346,7 +393,7 @@ class GenerateWorkflow {
346
393
  "Workflow created. " +
347
394
  a(
348
395
  { target: "_blank", href: `/actions/configure/${trigger.id}` },
349
- "Configure workflow.",
396
+ "Configure workflow."
350
397
  ),
351
398
  };
352
399
  }
@@ -364,14 +411,14 @@ class GenerateWorkflow {
364
411
  step.id = ix + 1;
365
412
  });
366
413
  const mmdia = WorkflowStep.generate_diagram(
367
- steps.map((s) => new WorkflowStep(s)),
414
+ steps.map((s) => new WorkflowStep(s))
368
415
  );
369
416
  console.log({ mmdia });
370
417
  return (
371
418
  div(
372
419
  `${workflow_name}${when_trigger ? `: ${when_trigger}` : ""}${
373
420
  trigger_table ? ` on ${trigger_table}` : ""
374
- }`,
421
+ }`
375
422
  ) +
376
423
  pre({ class: "mermaid" }, mmdia) +
377
424
  script(
@@ -380,13 +427,13 @@ class GenerateWorkflow {
380
427
  mermaid.initialize({ startOnLoad: false });
381
428
  mermaid.run({ querySelector: ".mermaid" });
382
429
  });
383
- `),
384
- )
430
+ `)
431
+ )
385
432
  );
386
433
  }
387
434
 
388
435
  return `A workflow! Step names: ${workflow_steps.map(
389
- (s) => s.step_name,
436
+ (s) => s.step_name
390
437
  )}. Upgrade Saltcorn to see diagrams in copilot`;
391
438
  }
392
439
 
@@ -0,0 +1,103 @@
1
+ // Core install logic. Deprecated chat-copilot format;
2
+ // the Agent Chat structure uses agent-skills/install-plugin.js instead.
3
+ const { getState } = require("@saltcorn/data/db/state");
4
+ const db = require("@saltcorn/data/db");
5
+ const Plugin = require("@saltcorn/data/models/plugin");
6
+ const { div, span, a } = require("@saltcorn/markup/tags");
7
+
8
+ class InstallPluginAction {
9
+ static title = "Install Plugin";
10
+ static function_name = "install_plugin";
11
+ static description = "Install a Saltcorn plugin from the store or from npm";
12
+
13
+ static json_schema() {
14
+ return {
15
+ type: "object",
16
+ properties: {
17
+ plugin_name: {
18
+ description:
19
+ "Name of the plugin as it appears in the Saltcorn plugin store (e.g. 'maps', 'chart'). Use this when the user refers to a plugin by its friendly name.",
20
+ type: "string",
21
+ },
22
+ npm_package: {
23
+ description:
24
+ "NPM package name to install directly (e.g. '@saltcorn/fullcalendar'). Use this when the user specifies an npm package name.",
25
+ type: "string",
26
+ },
27
+ },
28
+ };
29
+ }
30
+
31
+ static async system_prompt() {
32
+ return (
33
+ `Use the install_plugin function to install a Saltcorn plugin when the user asks to install, add, or enable a plugin. ` +
34
+ `Prefer plugin_name (store lookup) when the user gives a human-readable name. ` +
35
+ `Use npm_package when the user supplies an npm package name. ` +
36
+ `Do not install the same plugin twice; check if it is already installed first.`
37
+ );
38
+ }
39
+
40
+ static render_html({ plugin_name, npm_package }) {
41
+ const label = plugin_name || npm_package;
42
+ return div(
43
+ { class: "mb-2" },
44
+ span({ class: "badge bg-secondary me-2" }, "Plugin"),
45
+ span({ class: "fw-bold" }, label)
46
+ );
47
+ }
48
+
49
+ static async execute({ plugin_name, npm_package }, req) {
50
+ const schema = db.getTenantSchema();
51
+
52
+ // Resolve the plugin object
53
+ let plugin;
54
+ if (plugin_name) {
55
+ plugin = await Plugin.store_by_name(plugin_name);
56
+ if (!plugin) {
57
+ return { postExec: `Plugin "${plugin_name}" not found in store.` };
58
+ }
59
+ // strip any existing DB id so we insert fresh
60
+ delete plugin.id;
61
+ } else if (npm_package) {
62
+ plugin = new Plugin({
63
+ name: npm_package,
64
+ source: "npm",
65
+ location: npm_package,
66
+ });
67
+ } else {
68
+ return { postExec: "Please provide a plugin name or npm package." };
69
+ }
70
+
71
+ // Check already installed
72
+ const existing = await Plugin.findOne({ name: plugin.name });
73
+ if (existing) {
74
+ return {
75
+ postExec:
76
+ `Plugin "${plugin.name}" is already installed. ` +
77
+ a({ target: "_blank", href: `/plugins` }, "Manage plugins."),
78
+ };
79
+ }
80
+
81
+ const force = schema === db.connectObj.default_schema;
82
+
83
+ try {
84
+ const msgs = await Plugin.loadAndSaveNewPlugin(
85
+ plugin,
86
+ force,
87
+ undefined,
88
+ (s) => s,
89
+ true // allowUnsafeOnTenantsWithoutConfigSetting
90
+ );
91
+ const warnings = (msgs || []).map((m) => `<br>⚠ ${m}`).join("");
92
+ return {
93
+ postExec:
94
+ `Plugin "${plugin.name}" installed successfully.${warnings} ` +
95
+ a({ target: "_blank", href: `/plugins` }, "Manage plugins."),
96
+ };
97
+ } catch (e) {
98
+ return { postExec: `Error installing plugin: ${e.message}` };
99
+ }
100
+ }
101
+ }
102
+
103
+ module.exports = InstallPluginAction;
@@ -0,0 +1,25 @@
1
+ const user_registration_hints = `User registration notes:
2
+ * The "users" table is built-in. Passwords are platform-managed — never add a password field to a view.
3
+ * Signup uses a built-in form, not an Edit view.
4
+ * For verification/confirmation emails after registration, create a Trigger with event "Insert" on "users".
5
+ The trigger must use a Workflow action with a single step of type "run_js_code". Do NOT use the send_email action — it will not work for verification. The run_js_code step must contain exactly:
6
+ const { send_verification_email } = require("@saltcorn/data/models/email");
7
+ await send_verification_email(row, req);`;
8
+
9
+ class AppConstructorContextSkill {
10
+ static skill_name = "AppConstructor Context";
11
+
12
+ get skill_label() {
13
+ return "AppConstructor Context";
14
+ }
15
+
16
+ constructor(cfg) {
17
+ Object.assign(this, cfg);
18
+ }
19
+
20
+ async systemPrompt() {
21
+ return user_registration_hints;
22
+ }
23
+ }
24
+
25
+ module.exports = AppConstructorContextSkill;