@promptowl/contextnest-community 1.8.0 → 1.10.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/CONFIGURATION.md +38 -2
- package/dist/chunk-3JTODC3Y.js +48 -0
- package/dist/chunk-7PQREKSM.js +103 -0
- package/dist/{chunk-PLAHCDQG.js → chunk-BC6KFUZH.js} +442 -25
- package/dist/chunk-FRQJWGN3.js +8 -0
- package/dist/{chunk-PJTAUVD7.js → chunk-KFGZIECB.js} +107 -74
- package/dist/{chunk-XNGOO6RI.js → chunk-UHUU3VAK.js} +4 -2
- package/dist/chunk-XQ46F76G.js +16 -0
- package/dist/chunk-ZM4F7MY7.js +1343 -0
- package/dist/client-VZLX4THW.js +11 -0
- package/dist/email-R7DFS6E5.js +6 -0
- package/dist/grants-service-6H4WUA2F.js +21 -0
- package/dist/index.js +4120 -1049
- package/dist/{migrations.postgres-VCG45LMK.js → migrations.postgres-4XYY3CTF.js} +203 -1
- package/dist/{review-service-G5US3SMG.js → review-service-VLSUT6KK.js} +7 -4
- package/dist/{stewardship-service-ZXAMFTXO.js → stewardship-service-D5PDWXBR.js} +4 -2
- package/dist/{version-service-B3SDOJQE.js → version-service-A2YAKYQW.js} +3 -2
- package/dist/web3/assets/index-B2PkvzmY.js +1007 -0
- package/dist/web3/assets/index-C-tX933G.css +1 -0
- package/dist/web3/index.html +2 -2
- package/package.json +16 -6
- package/dist/chunk-Z3Y46ZGU.js +0 -362
- package/dist/web3/assets/index-Den-5nzY.js +0 -904
- package/dist/web3/assets/index-Jfj-2MHm.css +0 -1
|
@@ -2,6 +2,11 @@ import {
|
|
|
2
2
|
ANON_USER_ID
|
|
3
3
|
} from "./chunk-SLTQACJW.js";
|
|
4
4
|
|
|
5
|
+
// src/db/client.ts
|
|
6
|
+
import Database from "better-sqlite3";
|
|
7
|
+
import { mkdirSync, readFileSync } from "fs";
|
|
8
|
+
import { dirname as dirname2 } from "path";
|
|
9
|
+
|
|
5
10
|
// src/config.ts
|
|
6
11
|
import { join, dirname } from "path";
|
|
7
12
|
import { existsSync } from "fs";
|
|
@@ -21,6 +26,14 @@ if (envFileLoaded && !isTestRun) {
|
|
|
21
26
|
var canonicalEnvFile = process.env.ENV_FILE_PATH || join(dataRoot(), ".env");
|
|
22
27
|
var canonicalEnvLoaded = null;
|
|
23
28
|
var slackUrlWarned = false;
|
|
29
|
+
var emailFromWarned = false;
|
|
30
|
+
var emailToWarned = false;
|
|
31
|
+
function isEmailish(v) {
|
|
32
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) && !/[\r\n]/.test(v);
|
|
33
|
+
}
|
|
34
|
+
function isEmailListish(v) {
|
|
35
|
+
return v.split(",").every((e) => isEmailish(e.trim()));
|
|
36
|
+
}
|
|
24
37
|
if (!isTestRun && canonicalEnvFile !== envFileLoaded && existsSync(canonicalEnvFile)) {
|
|
25
38
|
dotenv.config({ path: canonicalEnvFile, override: true });
|
|
26
39
|
canonicalEnvLoaded = canonicalEnvFile;
|
|
@@ -111,6 +124,18 @@ var config = {
|
|
|
111
124
|
get PROMPTOWL_KEY() {
|
|
112
125
|
return process.env.PROMPTOWL_KEY || "";
|
|
113
126
|
},
|
|
127
|
+
/** Vercel automation-bypass secret for PO calls; unset = no bypass. */
|
|
128
|
+
get PROMPTOWL_BYPASS_SECRET() {
|
|
129
|
+
return process.env.PROMPTOWL_BYPASS_SECRET || "";
|
|
130
|
+
},
|
|
131
|
+
/** Outbound headers for PO fetches; bypass header only when secret set. */
|
|
132
|
+
get PROMPTOWL_FETCH_HEADERS() {
|
|
133
|
+
const h = { "Content-Type": "application/json" };
|
|
134
|
+
if (this.PROMPTOWL_BYPASS_SECRET) {
|
|
135
|
+
h["x-vercel-protection-bypass"] = this.PROMPTOWL_BYPASS_SECRET;
|
|
136
|
+
}
|
|
137
|
+
return h;
|
|
138
|
+
},
|
|
114
139
|
/**
|
|
115
140
|
* Shared secret for one-click SSO auto-login from PromptOwl (PO).
|
|
116
141
|
*
|
|
@@ -151,6 +176,24 @@ var config = {
|
|
|
151
176
|
const v = (process.env.PROMPTOWL_SIGN_IN_GATE || "open").trim().toLowerCase();
|
|
152
177
|
return v === "admin-only" || v === "disabled" ? v : "open";
|
|
153
178
|
},
|
|
179
|
+
/**
|
|
180
|
+
* Manual (email + password) sign-in. Three modes, independent of
|
|
181
|
+
* `PROMPTOWL_SIGN_IN_GATE` (the two methods are controlled separately):
|
|
182
|
+
* "open" — anyone may log in and self-register (default).
|
|
183
|
+
* "invite-only" — existing + admin-invited users may log in and an invited
|
|
184
|
+
* placeholder may set its password ("claim"), but brand-new
|
|
185
|
+
* self-registration is refused. Use this when the admin
|
|
186
|
+
* provisions accounts (invite / share / steward) and users
|
|
187
|
+
* shouldn't be able to create their own.
|
|
188
|
+
* "disabled" — no email/password sign-in at all (PromptOwl only).
|
|
189
|
+
* Enforced at POST /auth/login and POST /auth/register. The /admin/settings
|
|
190
|
+
* validator refuses "disabled" while PromptOwl is also disabled, so a server
|
|
191
|
+
* can never be left with no way to sign in.
|
|
192
|
+
*/
|
|
193
|
+
get MANUAL_SIGN_IN() {
|
|
194
|
+
const v = (process.env.MANUAL_SIGN_IN || "open").trim().toLowerCase();
|
|
195
|
+
return v === "invite-only" || v === "disabled" ? v : "open";
|
|
196
|
+
},
|
|
154
197
|
/**
|
|
155
198
|
* Path to the .env file the server reads its config from and the license
|
|
156
199
|
* install flow persists PROMPTOWL_KEY into. Defaults UNDER DATA_ROOT (not
|
|
@@ -168,6 +211,18 @@ var config = {
|
|
|
168
211
|
get TELEMETRY_INTERVAL_MS() {
|
|
169
212
|
return parseInt(process.env.TELEMETRY_INTERVAL_MS || "3600000", 10);
|
|
170
213
|
},
|
|
214
|
+
/**
|
|
215
|
+
* Activity-trace retention window in days (the api_events table behind
|
|
216
|
+
* GET /admin/trace and GET /nests/:id/trace). 0 = keep forever (pruning is
|
|
217
|
+
* skipped entirely). Invalid / negative → default 14; capped at 3650 (ten
|
|
218
|
+
* years) so a typo can't schedule a prune cutoff in the distant past.
|
|
219
|
+
* Editable at runtime from the admin Settings page (/admin/settings).
|
|
220
|
+
*/
|
|
221
|
+
get TRACE_RETENTION_DAYS() {
|
|
222
|
+
const n = parseInt(process.env.TRACE_RETENTION_DAYS || "14", 10);
|
|
223
|
+
if (!Number.isFinite(n) || n < 0) return 14;
|
|
224
|
+
return Math.min(n, 3650);
|
|
225
|
+
},
|
|
171
226
|
/**
|
|
172
227
|
* Optional custom logo URL shown in UI header + login screen.
|
|
173
228
|
* Must be an absolute https://, http://, or data:image/… URL. Other
|
|
@@ -187,14 +242,17 @@ var config = {
|
|
|
187
242
|
}
|
|
188
243
|
return raw;
|
|
189
244
|
},
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Workflow plane ("turing" track): typed edges, edge-type registry, runs.
|
|
247
|
+
* OFF by default — an optional feature an admin turns on (env or Settings).
|
|
248
|
+
* This flag is also the future license-tier hook: gating a paid tier here
|
|
249
|
+
* is a one-line change because every plane route checks it per request.
|
|
250
|
+
*/
|
|
193
251
|
/**
|
|
194
252
|
* Optional Slack incoming-webhook URL for team notifications (review
|
|
195
|
-
* submitted/approved/rejected
|
|
196
|
-
*
|
|
197
|
-
*
|
|
253
|
+
* submitted/approved/rejected). Empty/unset = connector off. https only —
|
|
254
|
+
* a webhook carries an implicit secret in its path, so it never travels
|
|
255
|
+
* plaintext. (Ported from development PR #105.)
|
|
198
256
|
*/
|
|
199
257
|
get SLACK_WEBHOOK_URL() {
|
|
200
258
|
const raw = process.env.SLACK_WEBHOOK_URL?.trim();
|
|
@@ -210,6 +268,114 @@ var config = {
|
|
|
210
268
|
}
|
|
211
269
|
return raw;
|
|
212
270
|
},
|
|
271
|
+
get FEATURE_WORKFLOW_PLANE() {
|
|
272
|
+
return process.env.FEATURE_WORKFLOW_PLANE === "true";
|
|
273
|
+
},
|
|
274
|
+
/**
|
|
275
|
+
* Sub-agent runs: lets a run spawn nested runs (an external runner calling
|
|
276
|
+
* back into POST /run for a child agent). Gated SEPARATELY from the base
|
|
277
|
+
* plane because recursion is the risky surface — unbounded fan-out,
|
|
278
|
+
* re-entrancy, resource exhaustion. Off by default; must clear a security
|
|
279
|
+
* review before an operator turns it on. Requires FEATURE_WORKFLOW_PLANE too.
|
|
280
|
+
*/
|
|
281
|
+
get FEATURE_SUBAGENT_RUNS() {
|
|
282
|
+
return process.env.FEATURE_SUBAGENT_RUNS === "true";
|
|
283
|
+
},
|
|
284
|
+
/** Hard ceiling on sub-agent nesting depth — keeps the call tree finite
|
|
285
|
+
* (haltable / statically bounded). Clamped to [1, 32]. */
|
|
286
|
+
get SUBAGENT_MAX_DEPTH() {
|
|
287
|
+
const n = parseInt(process.env.SUBAGENT_MAX_DEPTH || "8", 10);
|
|
288
|
+
return Number.isFinite(n) ? Math.min(32, Math.max(1, n)) : 8;
|
|
289
|
+
},
|
|
290
|
+
/** Max direct children a single run may spawn. Depth bounds the tree's
|
|
291
|
+
* height; this bounds its width — together they cap total size so a
|
|
292
|
+
* runner can't fork-bomb the DB. Clamped to [1, 128]. */
|
|
293
|
+
get SUBAGENT_MAX_CHILDREN() {
|
|
294
|
+
const n = parseInt(process.env.SUBAGENT_MAX_CHILDREN || "16", 10);
|
|
295
|
+
return Number.isFinite(n) ? Math.min(128, Math.max(1, n)) : 16;
|
|
296
|
+
},
|
|
297
|
+
/** Max steps a single run may accumulate — bounds a runaway/hostile
|
|
298
|
+
* runner's step log. Clamped to [10, 100000]. */
|
|
299
|
+
get RUN_MAX_STEPS() {
|
|
300
|
+
const n = parseInt(process.env.RUN_MAX_STEPS || "10000", 10);
|
|
301
|
+
return Number.isFinite(n) ? Math.min(1e5, Math.max(10, n)) : 1e4;
|
|
302
|
+
},
|
|
303
|
+
/** Max concurrently-RUNNING root (depth-0) runs per nest. The subagent
|
|
304
|
+
* depth/fan-out caps bound a single tree; this bounds how many trees a
|
|
305
|
+
* caller can start at once, so root triggers can't flood the nest. */
|
|
306
|
+
get RUN_MAX_CONCURRENT_ROOTS() {
|
|
307
|
+
const n = parseInt(process.env.RUN_MAX_CONCURRENT_ROOTS || "50", 10);
|
|
308
|
+
return Number.isFinite(n) ? Math.min(1e3, Math.max(1, n)) : 50;
|
|
309
|
+
},
|
|
310
|
+
get AUTH_MODE() {
|
|
311
|
+
return process.env.AUTH_MODE || "key";
|
|
312
|
+
},
|
|
313
|
+
/**
|
|
314
|
+
* Optional SMTP connection URL for email notifications, e.g.
|
|
315
|
+
* smtps://user:pass@smtp.example.com:465. smtp:// or smtps:// only.
|
|
316
|
+
* Empty/unset = email connector off. Requires NOTIFY_EMAIL_FROM/_TO.
|
|
317
|
+
*/
|
|
318
|
+
get SMTP_URL() {
|
|
319
|
+
const raw = process.env.SMTP_URL?.trim();
|
|
320
|
+
if (!raw) return null;
|
|
321
|
+
if (!/^smtps?:\/\//i.test(raw)) {
|
|
322
|
+
console.warn(
|
|
323
|
+
"[config] SMTP_URL rejected: must be an smtp:// or smtps:// URL. Email notifications disabled."
|
|
324
|
+
);
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
return raw;
|
|
328
|
+
},
|
|
329
|
+
/**
|
|
330
|
+
* From address for notification emails. Shape-validated here (not only in
|
|
331
|
+
* the /admin/settings PATCH) so a value set directly via env/Docker can't
|
|
332
|
+
* reach nodemailer's envelope unchecked. Invalid = connector off.
|
|
333
|
+
*/
|
|
334
|
+
get NOTIFY_EMAIL_FROM() {
|
|
335
|
+
const raw = process.env.NOTIFY_EMAIL_FROM?.trim();
|
|
336
|
+
if (!raw) return null;
|
|
337
|
+
if (!isEmailish(raw)) {
|
|
338
|
+
if (!emailFromWarned) {
|
|
339
|
+
emailFromWarned = true;
|
|
340
|
+
console.warn(
|
|
341
|
+
"[config] NOTIFY_EMAIL_FROM rejected: must be a plain email address. Email notifications disabled."
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
return raw;
|
|
347
|
+
},
|
|
348
|
+
/**
|
|
349
|
+
* Comma-separated recipient list for notification emails. Shape-validated
|
|
350
|
+
* here for the same env-bypass reason as NOTIFY_EMAIL_FROM. Invalid (any
|
|
351
|
+
* entry malformed) = connector off.
|
|
352
|
+
*/
|
|
353
|
+
get NOTIFY_EMAIL_TO() {
|
|
354
|
+
const raw = process.env.NOTIFY_EMAIL_TO?.trim();
|
|
355
|
+
if (!raw) return null;
|
|
356
|
+
if (!isEmailListish(raw)) {
|
|
357
|
+
if (!emailToWarned) {
|
|
358
|
+
emailToWarned = true;
|
|
359
|
+
console.warn(
|
|
360
|
+
"[config] NOTIFY_EMAIL_TO rejected: must be a comma-separated list of email addresses. Email notifications disabled."
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
return raw;
|
|
366
|
+
},
|
|
367
|
+
/**
|
|
368
|
+
* Per-nest notification digest window in milliseconds. A burst of governance
|
|
369
|
+
* events on one nest inside this window collapses into a single digest
|
|
370
|
+
* message instead of one post per event. Empty / non-numeric / negative /
|
|
371
|
+
* unset → 15000 (15s).
|
|
372
|
+
*/
|
|
373
|
+
get NOTIFY_DEBOUNCE_MS() {
|
|
374
|
+
const raw = process.env.NOTIFY_DEBOUNCE_MS?.trim();
|
|
375
|
+
if (!raw) return 15e3;
|
|
376
|
+
const n = Number(raw);
|
|
377
|
+
return Number.isFinite(n) && n >= 0 ? n : 15e3;
|
|
378
|
+
},
|
|
213
379
|
/**
|
|
214
380
|
* CORS origin allowlist. Comma-separated list of origins or "*".
|
|
215
381
|
* Defaults to "*" in open mode (read-heavy, no credentials to steal)
|
|
@@ -231,11 +397,6 @@ var config = {
|
|
|
231
397
|
}
|
|
232
398
|
};
|
|
233
399
|
|
|
234
|
-
// src/db/client.ts
|
|
235
|
-
import Database from "better-sqlite3";
|
|
236
|
-
import { mkdirSync, readFileSync } from "fs";
|
|
237
|
-
import { dirname as dirname2 } from "path";
|
|
238
|
-
|
|
239
400
|
// src/db/migrations.ts
|
|
240
401
|
function runMigrations(db) {
|
|
241
402
|
db.exec(`
|
|
@@ -802,6 +963,27 @@ function runMigrations(db) {
|
|
|
802
963
|
})();
|
|
803
964
|
recordMigration("012_merge_case_colliding_users");
|
|
804
965
|
}
|
|
966
|
+
if (!hasMigration("019_grants")) {
|
|
967
|
+
db.transaction(() => {
|
|
968
|
+
db.exec(`
|
|
969
|
+
CREATE TABLE IF NOT EXISTS grants (
|
|
970
|
+
id TEXT PRIMARY KEY,
|
|
971
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
972
|
+
target_type TEXT NOT NULL CHECK(target_type IN ('document', 'folder')),
|
|
973
|
+
target TEXT NOT NULL, -- node id (document) or path prefix (folder)
|
|
974
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
975
|
+
role TEXT NOT NULL CHECK(role IN ('read', 'write')),
|
|
976
|
+
granted_by TEXT NOT NULL,
|
|
977
|
+
created_at TEXT NOT NULL,
|
|
978
|
+
updated_at TEXT NOT NULL
|
|
979
|
+
);
|
|
980
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_grants_unique
|
|
981
|
+
ON grants(nest_id, target_type, target, user_id);
|
|
982
|
+
CREATE INDEX IF NOT EXISTS idx_grants_user ON grants(nest_id, user_id);
|
|
983
|
+
`);
|
|
984
|
+
})();
|
|
985
|
+
recordMigration("019_grants");
|
|
986
|
+
}
|
|
805
987
|
if (!hasMigration("013_node_deletion_tombstones")) {
|
|
806
988
|
db.transaction(() => {
|
|
807
989
|
db.exec(`
|
|
@@ -839,6 +1021,246 @@ function runMigrations(db) {
|
|
|
839
1021
|
})();
|
|
840
1022
|
recordMigration("014_api_events");
|
|
841
1023
|
}
|
|
1024
|
+
if (!hasMigration("015_definitions")) {
|
|
1025
|
+
db.transaction(() => {
|
|
1026
|
+
db.exec(`
|
|
1027
|
+
CREATE TABLE IF NOT EXISTS definitions (
|
|
1028
|
+
id TEXT PRIMARY KEY,
|
|
1029
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1030
|
+
term TEXT NOT NULL,
|
|
1031
|
+
definition TEXT NOT NULL,
|
|
1032
|
+
linked_tag TEXT, -- optional #tag this term maps to
|
|
1033
|
+
defined_by TEXT NOT NULL, -- email
|
|
1034
|
+
created_at TEXT NOT NULL,
|
|
1035
|
+
updated_at TEXT NOT NULL
|
|
1036
|
+
);
|
|
1037
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_definitions_term
|
|
1038
|
+
ON definitions(nest_id, term COLLATE NOCASE);
|
|
1039
|
+
`);
|
|
1040
|
+
})();
|
|
1041
|
+
recordMigration("015_definitions");
|
|
1042
|
+
}
|
|
1043
|
+
if (!hasMigration("016_workflow_edges")) {
|
|
1044
|
+
db.transaction(() => {
|
|
1045
|
+
db.exec(`
|
|
1046
|
+
CREATE TABLE IF NOT EXISTS edge_types (
|
|
1047
|
+
id TEXT PRIMARY KEY,
|
|
1048
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1049
|
+
name TEXT NOT NULL,
|
|
1050
|
+
description TEXT NOT NULL,
|
|
1051
|
+
direction TEXT NOT NULL DEFAULT 'directed'
|
|
1052
|
+
CHECK(direction IN ('directed', 'undirected')),
|
|
1053
|
+
is_flow INTEGER NOT NULL DEFAULT 0, -- flow types form the DAG
|
|
1054
|
+
condition_schema TEXT, -- JSON template | NULL
|
|
1055
|
+
color TEXT,
|
|
1056
|
+
created_by TEXT NOT NULL,
|
|
1057
|
+
created_at TEXT NOT NULL,
|
|
1058
|
+
updated_at TEXT NOT NULL
|
|
1059
|
+
);
|
|
1060
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_edge_types_name
|
|
1061
|
+
ON edge_types(nest_id, LOWER(name));
|
|
1062
|
+
|
|
1063
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
1064
|
+
id TEXT PRIMARY KEY,
|
|
1065
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1066
|
+
from_node TEXT NOT NULL,
|
|
1067
|
+
to_node TEXT NOT NULL,
|
|
1068
|
+
-- RESTRICT (not CASCADE): the app refuses to delete an edge type
|
|
1069
|
+
-- while edges reference it; the DB backs that invariant instead of
|
|
1070
|
+
-- silently cascade-deleting live edges if the app check is bypassed.
|
|
1071
|
+
type_id TEXT NOT NULL REFERENCES edge_types(id) ON DELETE RESTRICT,
|
|
1072
|
+
condition_mode TEXT
|
|
1073
|
+
CHECK(condition_mode IN ('structured', 'nl', 'open')),
|
|
1074
|
+
condition TEXT,
|
|
1075
|
+
metadata TEXT,
|
|
1076
|
+
created_by TEXT NOT NULL,
|
|
1077
|
+
created_at TEXT NOT NULL,
|
|
1078
|
+
updated_at TEXT NOT NULL
|
|
1079
|
+
);
|
|
1080
|
+
CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(nest_id, from_node);
|
|
1081
|
+
CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(nest_id, to_node);
|
|
1082
|
+
CREATE INDEX IF NOT EXISTS idx_edges_type ON edges(nest_id, type_id);
|
|
1083
|
+
`);
|
|
1084
|
+
})();
|
|
1085
|
+
recordMigration("016_workflow_edges");
|
|
1086
|
+
}
|
|
1087
|
+
if (!hasMigration("017_workflow_runs")) {
|
|
1088
|
+
db.transaction(() => {
|
|
1089
|
+
db.exec(`
|
|
1090
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
1091
|
+
id TEXT PRIMARY KEY,
|
|
1092
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1093
|
+
agent_node TEXT NOT NULL,
|
|
1094
|
+
triggered_by TEXT NOT NULL,
|
|
1095
|
+
status TEXT NOT NULL DEFAULT 'running'
|
|
1096
|
+
CHECK(status IN ('running', 'succeeded', 'failed', 'cancelled')),
|
|
1097
|
+
inputs TEXT,
|
|
1098
|
+
trace TEXT,
|
|
1099
|
+
started_at TEXT NOT NULL,
|
|
1100
|
+
finished_at TEXT
|
|
1101
|
+
);
|
|
1102
|
+
CREATE INDEX IF NOT EXISTS idx_runs_nest ON runs(nest_id, started_at);
|
|
1103
|
+
CREATE INDEX IF NOT EXISTS idx_runs_agent ON runs(nest_id, agent_node);
|
|
1104
|
+
|
|
1105
|
+
CREATE TABLE IF NOT EXISTS run_steps (
|
|
1106
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1107
|
+
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
1108
|
+
seq INTEGER NOT NULL,
|
|
1109
|
+
node_id TEXT,
|
|
1110
|
+
edge_id TEXT,
|
|
1111
|
+
action TEXT NOT NULL,
|
|
1112
|
+
detail TEXT,
|
|
1113
|
+
at TEXT NOT NULL
|
|
1114
|
+
);
|
|
1115
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_run_steps ON run_steps(run_id, seq);
|
|
1116
|
+
`);
|
|
1117
|
+
})();
|
|
1118
|
+
recordMigration("017_workflow_runs");
|
|
1119
|
+
}
|
|
1120
|
+
if (!hasMigration("018_subagent_runs")) {
|
|
1121
|
+
db.transaction(() => {
|
|
1122
|
+
db.exec(`
|
|
1123
|
+
ALTER TABLE runs ADD COLUMN parent_run_id TEXT;
|
|
1124
|
+
ALTER TABLE runs ADD COLUMN depth INTEGER NOT NULL DEFAULT 0;
|
|
1125
|
+
CREATE INDEX IF NOT EXISTS idx_runs_parent ON runs(parent_run_id);
|
|
1126
|
+
`);
|
|
1127
|
+
})();
|
|
1128
|
+
recordMigration("018_subagent_runs");
|
|
1129
|
+
}
|
|
1130
|
+
if (!hasMigration("020_server_settings")) {
|
|
1131
|
+
db.transaction(() => {
|
|
1132
|
+
db.exec(`
|
|
1133
|
+
CREATE TABLE IF NOT EXISTS server_settings (
|
|
1134
|
+
key TEXT PRIMARY KEY,
|
|
1135
|
+
value TEXT,
|
|
1136
|
+
env_at_write TEXT,
|
|
1137
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1138
|
+
);
|
|
1139
|
+
`);
|
|
1140
|
+
recordMigration("020_server_settings");
|
|
1141
|
+
})();
|
|
1142
|
+
}
|
|
1143
|
+
if (!hasMigration("020_schedules")) {
|
|
1144
|
+
db.transaction(() => {
|
|
1145
|
+
db.exec(`
|
|
1146
|
+
CREATE TABLE IF NOT EXISTS schedules (
|
|
1147
|
+
id TEXT PRIMARY KEY,
|
|
1148
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1149
|
+
agent_node TEXT NOT NULL,
|
|
1150
|
+
every_minutes INTEGER NOT NULL CHECK(every_minutes >= 5),
|
|
1151
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
1152
|
+
last_run_at TEXT,
|
|
1153
|
+
created_by TEXT NOT NULL,
|
|
1154
|
+
created_at TEXT NOT NULL,
|
|
1155
|
+
updated_at TEXT NOT NULL
|
|
1156
|
+
);
|
|
1157
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_schedules_agent
|
|
1158
|
+
ON schedules(nest_id, agent_node);
|
|
1159
|
+
`);
|
|
1160
|
+
})();
|
|
1161
|
+
recordMigration("020_schedules");
|
|
1162
|
+
}
|
|
1163
|
+
if (!hasMigration("021_schedule_bounds")) {
|
|
1164
|
+
db.transaction(() => {
|
|
1165
|
+
db.exec(`
|
|
1166
|
+
ALTER TABLE schedules ADD COLUMN max_runs INTEGER;
|
|
1167
|
+
ALTER TABLE schedules ADD COLUMN runs_created INTEGER NOT NULL DEFAULT 0;
|
|
1168
|
+
`);
|
|
1169
|
+
})();
|
|
1170
|
+
recordMigration("021_schedule_bounds");
|
|
1171
|
+
}
|
|
1172
|
+
if (!hasMigration("022_tools_and_watchers")) {
|
|
1173
|
+
db.transaction(() => {
|
|
1174
|
+
db.exec(`
|
|
1175
|
+
CREATE TABLE IF NOT EXISTS nest_env (
|
|
1176
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1177
|
+
key TEXT NOT NULL,
|
|
1178
|
+
value TEXT NOT NULL,
|
|
1179
|
+
updated_by TEXT NOT NULL,
|
|
1180
|
+
updated_at TEXT NOT NULL,
|
|
1181
|
+
PRIMARY KEY (nest_id, key)
|
|
1182
|
+
);
|
|
1183
|
+
CREATE TABLE IF NOT EXISTS watchers (
|
|
1184
|
+
id TEXT PRIMARY KEY,
|
|
1185
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1186
|
+
node_id TEXT NOT NULL,
|
|
1187
|
+
user_email TEXT NOT NULL,
|
|
1188
|
+
created_by TEXT NOT NULL,
|
|
1189
|
+
created_at TEXT NOT NULL
|
|
1190
|
+
);
|
|
1191
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_watchers_unique
|
|
1192
|
+
ON watchers(nest_id, node_id, user_email);
|
|
1193
|
+
CREATE TABLE IF NOT EXISTS notifications (
|
|
1194
|
+
id TEXT PRIMARY KEY,
|
|
1195
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1196
|
+
user_email TEXT NOT NULL,
|
|
1197
|
+
kind TEXT NOT NULL,
|
|
1198
|
+
subject_id TEXT,
|
|
1199
|
+
message TEXT NOT NULL,
|
|
1200
|
+
created_at TEXT NOT NULL,
|
|
1201
|
+
read_at TEXT
|
|
1202
|
+
);
|
|
1203
|
+
CREATE INDEX IF NOT EXISTS idx_notifications_user
|
|
1204
|
+
ON notifications(user_email, read_at, created_at);
|
|
1205
|
+
`);
|
|
1206
|
+
})();
|
|
1207
|
+
recordMigration("022_tools_and_watchers");
|
|
1208
|
+
}
|
|
1209
|
+
if (!hasMigration("023_connectors")) {
|
|
1210
|
+
db.transaction(() => {
|
|
1211
|
+
db.exec(`
|
|
1212
|
+
CREATE TABLE IF NOT EXISTS connectors (
|
|
1213
|
+
id TEXT PRIMARY KEY,
|
|
1214
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1215
|
+
channel TEXT NOT NULL CHECK(channel IN ('slack', 'teams', 'webhook')),
|
|
1216
|
+
url TEXT NOT NULL, -- https://\u2026 or env:KEY
|
|
1217
|
+
events TEXT NOT NULL, -- JSON array of kinds, or ["*"]
|
|
1218
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
1219
|
+
created_by TEXT NOT NULL,
|
|
1220
|
+
created_at TEXT NOT NULL,
|
|
1221
|
+
updated_at TEXT NOT NULL
|
|
1222
|
+
);
|
|
1223
|
+
CREATE INDEX IF NOT EXISTS idx_connectors_nest ON connectors(nest_id);
|
|
1224
|
+
`);
|
|
1225
|
+
})();
|
|
1226
|
+
recordMigration("023_connectors");
|
|
1227
|
+
}
|
|
1228
|
+
if (!hasMigration("024_trigger_hooks")) {
|
|
1229
|
+
db.transaction(() => {
|
|
1230
|
+
db.exec(`
|
|
1231
|
+
CREATE TABLE IF NOT EXISTS trigger_hooks (
|
|
1232
|
+
id TEXT PRIMARY KEY, -- the token (hk_\u2026), capability auth
|
|
1233
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1234
|
+
agent_node TEXT NOT NULL,
|
|
1235
|
+
preset TEXT NOT NULL CHECK(preset IN ('slack', 'teams', 'webhook')),
|
|
1236
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
1237
|
+
fire_count INTEGER NOT NULL DEFAULT 0,
|
|
1238
|
+
last_fired_at TEXT,
|
|
1239
|
+
created_by TEXT NOT NULL,
|
|
1240
|
+
created_at TEXT NOT NULL
|
|
1241
|
+
);
|
|
1242
|
+
CREATE INDEX IF NOT EXISTS idx_hooks_nest ON trigger_hooks(nest_id);
|
|
1243
|
+
`);
|
|
1244
|
+
})();
|
|
1245
|
+
recordMigration("024_trigger_hooks");
|
|
1246
|
+
}
|
|
1247
|
+
if (!hasMigration("025_run_claims")) {
|
|
1248
|
+
db.transaction(() => {
|
|
1249
|
+
db.exec(`
|
|
1250
|
+
ALTER TABLE runs ADD COLUMN claimed_by TEXT;
|
|
1251
|
+
ALTER TABLE runs ADD COLUMN claimed_at TEXT;
|
|
1252
|
+
`);
|
|
1253
|
+
})();
|
|
1254
|
+
recordMigration("025_run_claims");
|
|
1255
|
+
}
|
|
1256
|
+
if (!hasMigration("026_api_events_nest_index")) {
|
|
1257
|
+
db.transaction(() => {
|
|
1258
|
+
db.exec(`
|
|
1259
|
+
CREATE INDEX IF NOT EXISTS idx_api_events_nest_ts ON api_events(nest_id, id);
|
|
1260
|
+
`);
|
|
1261
|
+
})();
|
|
1262
|
+
recordMigration("026_api_events_nest_index");
|
|
1263
|
+
}
|
|
842
1264
|
}
|
|
843
1265
|
function mergeCaseCollidingUsers(db) {
|
|
844
1266
|
const groups = db.prepare(
|
|
@@ -1056,7 +1478,7 @@ async function initDb() {
|
|
|
1056
1478
|
if (config.DB_DRIVER === "postgres") {
|
|
1057
1479
|
const { Pool } = await import("pg");
|
|
1058
1480
|
const { PostgresAdapter } = await import("./adapter.postgres-YOODX2BI.js");
|
|
1059
|
-
const { runPostgresMigrations } = await import("./migrations.postgres-
|
|
1481
|
+
const { runPostgresMigrations } = await import("./migrations.postgres-4XYY3CTF.js");
|
|
1060
1482
|
const pool = new Pool(buildPgConfig());
|
|
1061
1483
|
adapter = new PostgresAdapter(pool);
|
|
1062
1484
|
await runPostgresMigrations(adapter);
|
|
@@ -1076,23 +1498,18 @@ function getDb() {
|
|
|
1076
1498
|
}
|
|
1077
1499
|
return adapter;
|
|
1078
1500
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
}
|
|
1084
|
-
function insertOrIgnore(db, insertSql) {
|
|
1085
|
-
return db.dialect === "sqlite" ? insertSql.replace(/^\s*INSERT\s+INTO/i, "INSERT OR IGNORE INTO") : `${insertSql} ON CONFLICT DO NOTHING`;
|
|
1086
|
-
}
|
|
1087
|
-
function insertOrReplace(db, insertSql, conflictCols, set) {
|
|
1088
|
-
return db.dialect === "sqlite" ? insertSql.replace(/^\s*INSERT\s+INTO/i, "INSERT OR REPLACE INTO") : `${insertSql} ON CONFLICT (${conflictCols.join(", ")}) DO UPDATE SET ${set}`;
|
|
1501
|
+
async function resetDb() {
|
|
1502
|
+
if (adapter) {
|
|
1503
|
+
await adapter.close();
|
|
1504
|
+
adapter = null;
|
|
1505
|
+
}
|
|
1089
1506
|
}
|
|
1090
1507
|
|
|
1091
1508
|
export {
|
|
1509
|
+
isEmailish,
|
|
1510
|
+
isEmailListish,
|
|
1092
1511
|
config,
|
|
1093
1512
|
initDb,
|
|
1094
1513
|
getDb,
|
|
1095
|
-
|
|
1096
|
-
insertOrIgnore,
|
|
1097
|
-
insertOrReplace
|
|
1514
|
+
resetDb
|
|
1098
1515
|
};
|