@saltcorn/copilot 0.8.2 → 0.8.3

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.
@@ -1,15 +1,15 @@
1
- const Field = require("@saltcorn/data/models/field");
2
1
  const Table = require("@saltcorn/data/models/table");
3
- const Form = require("@saltcorn/data/models/form");
4
2
  const MetaData = require("@saltcorn/data/models/metadata");
5
3
  const View = require("@saltcorn/data/models/view");
4
+ const Page = require("@saltcorn/data/models/page");
6
5
  const Trigger = require("@saltcorn/data/models/trigger");
7
- const { findType } = require("@saltcorn/data/models/discovery");
8
- const { save_menu_items } = require("@saltcorn/data/models/config");
6
+ const Plugin = require("@saltcorn/data/models/plugin");
9
7
  const db = require("@saltcorn/data/db");
10
8
  const WorkflowRun = require("@saltcorn/data/models/workflow_run");
11
9
  const User = require("@saltcorn/data/models/user");
12
- const { viewname } = require("./common");
10
+ const { getState } = require("@saltcorn/data/db/state");
11
+ const { viewname, TaskType } = require("./common");
12
+ const { PromptGenerator } = require("./prompt-generator");
13
13
 
14
14
  /**
15
15
  * @param {number} md_id - MetaData id of the task to run
@@ -21,65 +21,84 @@ const runTask = async (md_id, req) => {
21
21
  });
22
22
 
23
23
  if (!md) return { error: "Task not found" };
24
- const spec = await MetaData.findOne({
25
- type: "CopilotConstructMgr",
26
- name: "spec",
27
- });
28
- if (!spec) return { error: "Specification not found" };
24
+
25
+ const taskType = md.body.task_type || TaskType.FEATURE;
26
+
29
27
  const agent_action = new Trigger({
30
28
  action: "Agent",
31
29
  when_trigger: "Never",
32
30
  configuration: {
33
31
  viewname: viewname,
34
32
  sys_prompt:
35
- "Each task creates exactly one view or one page. " +
36
- "Never create more than one view or page per task, even if the description mentions multiple. " +
37
- "Call the view or page tool exactly once and then stop.",
33
+ taskType === TaskType.PLUGIN
34
+ ? "Each task installs exactly one plugin from the Saltcorn plugin store. " +
35
+ "Use the Install Plugin skill to find and install it. Call the skill once and then stop."
36
+ : taskType === TaskType.DATA_MODEL
37
+ ? "Each task creates or modifies database tables/fields or configures platform-level settings (such as custom roles). " +
38
+ "Use the database design tool for schema changes. Use the Registry editor (set_entity) for platform configuration such as creating custom roles. " +
39
+ "Call only the tools needed for the task and then stop. Do not create any views, pages, or triggers."
40
+ : "Each task creates exactly one primary artifact: one view, one page, or one workflow trigger. " +
41
+ "Never create more than one view or page per task, even if the description mentions multiple. " +
42
+ "Exception: if the task description explicitly says to both create a workflow trigger AND update an existing view to add an action button for it, do both — create the workflow first, then update the specified view. " +
43
+ "After completing the primary artifact (and the explicitly described action button update, if any), stop.",
38
44
  prompt: "{{prompt}}",
39
- skills: [
40
- { skill_type: "Generate Page", yoloMode: true },
41
- { skill_type: "Database design", yoloMode: true },
42
- { skill_type: "Generate Workflow", yoloMode: true },
43
- { skill_type: "Generate trigger", yoloMode: true },
44
- { skill_type: "Generate View", yoloMode: true },
45
- { skill_type: "Install Plugin", yoloMode: true },
46
- { skill_type: "Registry editor", yoloMode: true },
47
- ],
45
+ skills:
46
+ taskType === TaskType.PLUGIN
47
+ ? [{ skill_type: "Install Plugin", yoloMode: true }]
48
+ : taskType === TaskType.DATA_MODEL
49
+ ? [
50
+ { skill_type: "Database design", yoloMode: true },
51
+ { skill_type: "Registry editor", yoloMode: true },
52
+ ]
53
+ : [
54
+ { skill_type: "Generate Page", yoloMode: true },
55
+ { skill_type: "Generate Workflow", yoloMode: true },
56
+ { skill_type: "Generate trigger", yoloMode: true },
57
+ { skill_type: "Generate View", yoloMode: true },
58
+ { skill_type: "Install Plugin", yoloMode: true },
59
+ { skill_type: "Registry editor", yoloMode: true },
60
+ ],
48
61
  },
49
62
  });
50
- const prompt = `You are engaged in building the following application:
51
-
52
- Description: ${spec.body.description}
53
- Audience: ${spec.body.audience}
54
- Core features: ${spec.body.core_features}
55
- Out of scope: ${spec.body.out_of_scope}
56
- Visual style: ${spec.body.visual_style}
57
-
58
- Important: The database schema is already fully implemented. Do NOT use generate_tables or modify any tables or fields — all tables and fields already exist.
59
-
60
- Important: Some fields are non-stored (virtual) calculated fields — they have no database column and are computed on-the-fly by Saltcorn. Never include such fields in modify_row, SQL UPDATE statements, or recalculate_stored_fields calls. Only fields that exist as actual database columns (regular fields and stored calculated fields) can be written. If a calculated field needs updating, it will refresh automatically when the fields it depends on change.
61
-
62
- Important: The "users" table is built-in. Passwords are platform-managed — never add a password field to a view. Signup uses the built-in page at /auth/signup, login at /auth/login. Do NOT create triggers for registration or email verification — the platform handles this natively.
63
-
64
- Important: On landing pages, place Log in / Create account buttons in no more than two locations (e.g. navbar and one hero call-to-action). Do not repeat them in a third "Get started" section or anywhere else. For links that take an already-authenticated user to their dashboard, use href="/" — not /auth/login.
65
-
66
- Important: Do not name any page or view "Admin dashboard" — that name is reserved by the Saltcorn platform. For pages intended for role 1 (admin), use a name like "App admin dashboard" or prefix it with the application name (e.g. "Law Firm admin dashboard").
67
-
68
- Important: When creating a page or view, always set min_role based on the intended audience: 1 for admin-only, 40 for staff and above, 80 for logged-in users and above, 100 for public. Never default to public (100) unless the page or view is explicitly intended for unauthenticated users (e.g. a landing page). A dashboard or view for clients/users is role 80, a staff page or view is role 40, an admin page or view is role 1.
69
63
 
70
- Important: Two-factor authentication (2FA/TOTP) is fully built into the platform. To configure it, call set_entity directly with entity_type "system-configuration-value" and entity_name "twofa_policy_by_role". The entity_definition must be the plain JSON object itself — for example: {"1": "Mandatory", "100": "Disabled"}. Do NOT wrap it in {"type": "json", "value": ...} or any other envelope. Read the current value first with get_entity and merge rather than overwrite. Do NOT create a workflow or trigger to do this.
64
+ const generator = await PromptGenerator.createInstance();
65
+ if (!generator.spec) return { error: "Specification not found" };
66
+ const prompt = generator.taskExecPrompt(taskType, md.body.description);
71
67
 
72
- Important: To set a page as the home page for a role, call set_entity directly with entity_type "system-configuration-value" and entity_name "home_page_by_role". The value is a JSON object mapping role IDs to page names — Role IDs: public=100, user=80, staff=40, admin=1. The entity_definition must be the plain JSON object itself — for example: {"100": "landing", "80": "client_dashboard"}. Do NOT wrap it in {"type": "json", "value": ...} or any other envelope. Read the current value first with get_entity so you can merge rather than overwrite. Do NOT create a workflow or trigger to do this — use set_entity directly.
68
+ const safeReq =
69
+ req?.__ && req?.getLocale
70
+ ? req
71
+ : {
72
+ ...req,
73
+ __: req?.__ || ((s) => s),
74
+ getLocale: req?.getLocale || (() => "en"),
75
+ user: req?.user,
76
+ };
73
77
 
74
- Important: If the task description mentions adding a viewlink, linking rows to another view, or a button that opens another view from a list — that viewlink column MUST be present in the finished view. Do not skip it. Viewlinks require calling get_relation_paths first to obtain the relation string before generating the layout.
75
-
76
- Important: Before creating or updating any view or page that embeds, links to, or opens another view (including viewlinks, action buttons, and ajax_modal calls), call list_entities (entity_type "view") to get all existing view names. Only reference views that appear in that list — never invent a name or assume a view exists. If a view is not in the list, omit the reference entirely. Do the same for pages: call list_entities (entity_type "page") before linking to any page by name.
77
-
78
- Your task now is:
79
- ${md.body.description}`;
80
- const safeReq = req?.__ ? req : { ...req, __: (s) => s, user: req?.user };
78
+ const tableNamesBefore =
79
+ taskType === TaskType.DATA_MODEL
80
+ ? new Set((await Table.find({})).map((t) => t.name))
81
+ : null;
82
+ const viewNamesBefore =
83
+ taskType === TaskType.FEATURE && md.body.phase_idx !== undefined
84
+ ? new Set((await View.find({})).map((v) => v.name))
85
+ : null;
86
+ const pageNamesBefore =
87
+ taskType === TaskType.FEATURE && md.body.phase_idx !== undefined
88
+ ? new Set((await Page.find({})).map((p) => p.name))
89
+ : null;
90
+ const pluginNamesBefore =
91
+ taskType === TaskType.PLUGIN && md.body.phase_idx !== undefined
92
+ ? new Set((await Plugin.find({})).map((p) => p.name))
93
+ : null;
81
94
 
82
95
  await md.update({ body: { ...md.body, status: "Running" } });
96
+ try {
97
+ getState().emitDynamicUpdate(db.getTenantSchema(), {
98
+ eval_js:
99
+ "if(typeof copilotRefreshTasks==='function')copilotRefreshTasks();",
100
+ });
101
+ } catch (_) {}
83
102
  try {
84
103
  const actionres = await agent_action.runWithoutRow({
85
104
  row: { prompt },
@@ -98,25 +117,133 @@ ${md.body.description}`;
98
117
  user: safeReq.user,
99
118
  });
100
119
  const updatedRun = await WorkflowRun.findOne({ id: run_id });
101
- const lastInteraction =
102
- updatedRun.context.interactions[
103
- updatedRun.context.interactions.length - 1
104
- ];
105
- const lastText =
106
- typeof lastInteraction.content === "string"
107
- ? lastInteraction.content
108
- : lastInteraction.content.text
109
- ? lastInteraction.content.text
110
- : Array.isArray(lastInteraction.content)
111
- ? lastInteraction.content[0].text
112
- : lastInteraction.content;
120
+ const extractText = (content) => {
121
+ if (!content) return "";
122
+ if (typeof content === "string") return content;
123
+ if (
124
+ typeof content === "object" &&
125
+ !Array.isArray(content) &&
126
+ content.text
127
+ )
128
+ return content.text;
129
+ if (Array.isArray(content)) {
130
+ const tb = content.find((b) => b?.type === "text" && b?.text);
131
+ return tb?.text || "";
132
+ }
133
+ return "";
134
+ };
135
+ const interactions = updatedRun.context.interactions || [];
136
+ let lastText = "";
137
+ for (let i = interactions.length - 1; i >= 0; i--) {
138
+ const t = extractText(interactions[i]?.content);
139
+ if (t) {
140
+ lastText = t;
141
+ break;
142
+ }
143
+ }
144
+ if (!lastText) lastText = md.body.description || md.body.name || "";
113
145
  await MetaData.create({
114
146
  type: "CopilotConstructMgr",
115
147
  name: "progress",
116
- body: { text: lastText, run_id, task_id: md.id },
148
+ body: {
149
+ text: lastText,
150
+ run_id,
151
+ task_id: md.id,
152
+ phase_idx: md.body.phase_idx ?? null,
153
+ },
117
154
  user_id: req?.user?.id,
118
155
  });
119
156
  await md.update({ body: { ...md.body, status: "Done", run_id } });
157
+ if (
158
+ taskType === TaskType.DATA_MODEL &&
159
+ tableNamesBefore &&
160
+ md.body.phase_idx !== undefined
161
+ ) {
162
+ const tablesAfter = await Table.find({});
163
+ const newTables = tablesAfter.filter(
164
+ (t) => !t.name.startsWith("_sc_") && !tableNamesBefore.has(t.name)
165
+ );
166
+ for (const table of newTables) {
167
+ await MetaData.create({
168
+ type: "CopilotConstructMgr",
169
+ name: "table_phase",
170
+ body: {
171
+ table_name: table.name,
172
+ phase_idx: md.body.phase_idx,
173
+ phase_name: md.body.phase_name,
174
+ },
175
+ user_id: req?.user?.id,
176
+ });
177
+ }
178
+ }
179
+ if (
180
+ taskType === TaskType.PLUGIN &&
181
+ pluginNamesBefore &&
182
+ md.body.phase_idx !== undefined
183
+ ) {
184
+ const pluginsAfter = await Plugin.find({});
185
+ for (const p of pluginsAfter.filter(
186
+ (p) => !pluginNamesBefore.has(p.name)
187
+ )) {
188
+ await MetaData.create({
189
+ type: "CopilotConstructMgr",
190
+ name: "plugin_phase",
191
+ body: {
192
+ plugin_name: p.name,
193
+ phase_idx: md.body.phase_idx,
194
+ phase_name: md.body.phase_name,
195
+ },
196
+ user_id: req?.user?.id,
197
+ });
198
+ }
199
+ }
200
+ if (
201
+ taskType === TaskType.FEATURE &&
202
+ viewNamesBefore &&
203
+ md.body.phase_idx !== undefined
204
+ ) {
205
+ const viewsAfter = await View.find({});
206
+ for (const v of viewsAfter.filter((v) => !viewNamesBefore.has(v.name))) {
207
+ await MetaData.create({
208
+ type: "CopilotConstructMgr",
209
+ name: "view_phase",
210
+ body: {
211
+ view_name: v.name,
212
+ viewtemplate: v.viewtemplate,
213
+ phase_idx: md.body.phase_idx,
214
+ phase_name: md.body.phase_name,
215
+ },
216
+ user_id: req?.user?.id,
217
+ });
218
+ }
219
+ const pagesAfter = await Page.find({});
220
+ for (const p of pagesAfter.filter((p) => !pageNamesBefore.has(p.name))) {
221
+ await MetaData.create({
222
+ type: "CopilotConstructMgr",
223
+ name: "view_phase",
224
+ body: {
225
+ view_name: p.name,
226
+ viewtemplate: "page",
227
+ phase_idx: md.body.phase_idx,
228
+ phase_name: md.body.phase_name,
229
+ },
230
+ user_id: req?.user?.id,
231
+ });
232
+ }
233
+ }
234
+ try {
235
+ const phaseIdx = md.body.phase_idx;
236
+ getState().emitDynamicUpdate(db.getTenantSchema(), {
237
+ eval_js:
238
+ "if(typeof copilotRefreshTasks==='function')copilotRefreshTasks();" +
239
+ (taskType === TaskType.DATA_MODEL
240
+ ? "if(typeof copilotRefreshSchema==='function')copilotRefreshSchema();"
241
+ : "") +
242
+ (phaseIdx != null
243
+ ? `if(typeof copilotRefreshPhaseProgress==='function')copilotRefreshPhaseProgress(${phaseIdx});`
244
+ : ""),
245
+ });
246
+ } catch (_) {}
120
247
  } catch (e) {
121
248
  await md.update({ body: { ...md.body, status: "To do" } });
122
249
  throw e;
@@ -161,7 +288,11 @@ const runNextTask = async (once = false) => {
161
288
  const taskUser = startable[0].user_id
162
289
  ? await User.findOne({ id: startable[0].user_id })
163
290
  : null;
164
- await runTask(startable[0].id, { user: taskUser, __: (s) => s });
291
+ await runTask(startable[0].id, {
292
+ user: taskUser,
293
+ __: (s) => s,
294
+ getLocale: () => "en",
295
+ });
165
296
  if (!once) await runNextTask();
166
297
  } else if (!once) {
167
298
  const settings = await MetaData.findOne({