@harborclient/team-hub 0.2.4 → 0.4.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/dist/cli.js CHANGED
@@ -25,6 +25,35 @@ import path from "path";
25
25
  import { parse as parseYaml } from "yaml";
26
26
 
27
27
  // src/config/llmConfig.ts
28
+ function headerRowsFromSingleKeyObject(record) {
29
+ const rows = [];
30
+ for (const [key, value] of Object.entries(record)) {
31
+ const trimmedKey = key.trim();
32
+ if (trimmedKey.length > 0) {
33
+ rows.push({ key: trimmedKey, value: String(value) });
34
+ }
35
+ }
36
+ return rows;
37
+ }
38
+ function normalizeHubMcpHeaders(headers) {
39
+ if (!headers) {
40
+ return [];
41
+ }
42
+ if (!Array.isArray(headers)) {
43
+ return headerRowsFromSingleKeyObject(headers);
44
+ }
45
+ const rows = [];
46
+ for (const item of headers) {
47
+ if (Array.isArray(item)) {
48
+ for (const nested of item) {
49
+ rows.push(...headerRowsFromSingleKeyObject(nested));
50
+ }
51
+ continue;
52
+ }
53
+ rows.push(...headerRowsFromSingleKeyObject(item));
54
+ }
55
+ return rows;
56
+ }
28
57
  function normalizeLlmConfig(section) {
29
58
  const providers = {};
30
59
  if (section.providers.openai?.apiKey) {
@@ -36,9 +65,15 @@ function normalizeLlmConfig(section) {
36
65
  if (section.providers.gemini?.apiKey) {
37
66
  providers.gemini = { apiKey: section.providers.gemini.apiKey };
38
67
  }
68
+ const mcp = section.mcp && section.mcp.length > 0 ? section.mcp.map((entry) => ({
69
+ name: entry.name.trim(),
70
+ url: entry.url.trim().replace(/\/+$/, ""),
71
+ headers: normalizeHubMcpHeaders(entry.headers)
72
+ })) : void 0;
39
73
  return {
40
74
  providers,
41
- ...section.models && section.models.length > 0 ? { models: section.models } : {}
75
+ ...section.models && section.models.length > 0 ? { models: section.models } : {},
76
+ ...mcp && mcp.length > 0 ? { mcp } : {}
42
77
  };
43
78
  }
44
79
 
@@ -116,6 +151,15 @@ var redisSectionSchema = z.object({
116
151
  var llmProviderEntrySchema = z.object({
117
152
  apiKey: z.string().trim().min(1, { message: "LLM provider apiKey must not be empty." })
118
153
  });
154
+ var hubMcpHeadersSchema = z.union([
155
+ z.record(z.string(), z.string()),
156
+ z.array(z.union([z.record(z.string(), z.string()), z.array(z.record(z.string(), z.string()))]))
157
+ ]);
158
+ var hubMcpServerEntrySchema = z.object({
159
+ name: z.string().trim().min(1),
160
+ url: z.string().trim().min(1),
161
+ headers: hubMcpHeadersSchema.optional()
162
+ });
119
163
  var llmSectionSchema = z.object({
120
164
  providers: z.object({
121
165
  openai: llmProviderEntrySchema.optional(),
@@ -125,7 +169,8 @@ var llmSectionSchema = z.object({
125
169
  (providers) => Boolean(providers.openai?.apiKey || providers.claude?.apiKey || providers.gemini?.apiKey),
126
170
  { message: "llm.providers must include at least one provider with an apiKey." }
127
171
  ),
128
- models: z.array(z.string().trim().min(1)).optional()
172
+ models: z.array(z.string().trim().min(1)).optional(),
173
+ mcp: z.array(hubMcpServerEntrySchema).optional()
129
174
  });
130
175
  var pluginsSectionSchema = z.object({
131
176
  catalogs: z.array(z.string().trim().url()).optional(),
@@ -313,6 +358,7 @@ var USERS_COLLECTION = "users";
313
358
  var API_TOKENS_COLLECTION = "apiTokens";
314
359
  var COLLECTIONS_COLLECTION = "collections";
315
360
  var ENVIRONMENTS_COLLECTION = "environments";
361
+ var SNIPPETS_COLLECTION = "snippets";
316
362
  var FOLDERS_COLLECTION = "folders";
317
363
  var REQUESTS_COLLECTION = "requests";
318
364
  var AUDIT_LOG_COLLECTION = "auditLog";
@@ -339,7 +385,8 @@ function createSystemUserInput() {
339
385
  name: SYSTEM_USER_NAME,
340
386
  role: "admin",
341
387
  collectionAccess: [],
342
- environmentAccess: []
388
+ environmentAccess: [],
389
+ snippetAccess: []
343
390
  };
344
391
  }
345
392
 
@@ -353,7 +400,7 @@ var firestoreConfigSchema = z2.object({
353
400
 
354
401
  // src/db/firestore/utils.ts
355
402
  function parseAuditEntityType(value) {
356
- if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "folder" || value === "request") {
403
+ if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request") {
357
404
  return value;
358
405
  }
359
406
  throw new Error(`Invalid audit entity type: ${value}`);
@@ -382,6 +429,7 @@ function mapFirestoreUser(id, data) {
382
429
  role: data.role,
383
430
  collectionAccess: data.collectionAccess,
384
431
  environmentAccess: data.environmentAccess,
432
+ snippetAccess: data.snippetAccess ?? [],
385
433
  llmAccess: data.llmAccess ?? false,
386
434
  llmModels: data.llmModels ?? [],
387
435
  llmMonthlyTokenLimit: data.llmMonthlyTokenLimit ?? null,
@@ -447,6 +495,20 @@ function mapFirestoreEnvironment(id, data) {
447
495
  deletionLocked: data.deletionLocked ?? false
448
496
  };
449
497
  }
498
+ function mapFirestoreSnippet(id, data) {
499
+ return {
500
+ id,
501
+ name: data.name,
502
+ code: data.code,
503
+ scope: data.scope,
504
+ sortOrder: data.sortOrder,
505
+ createdAt: data.createdAt,
506
+ updatedAt: data.updatedAt ?? data.createdAt,
507
+ createdByUserId: data.createdByUserId ?? null,
508
+ updatedByUserId: data.updatedByUserId ?? null,
509
+ deletionLocked: data.deletionLocked ?? false
510
+ };
511
+ }
450
512
  function mapFirestoreFolder(id, data) {
451
513
  return {
452
514
  id,
@@ -654,6 +716,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
654
716
  async migrate() {
655
717
  await this.ensureSystemUser();
656
718
  await this.migrateOrphanTokensToBootstrapUser();
719
+ await this.migrateSnippetAccessBackfill();
657
720
  }
658
721
  /**
659
722
  * Returns the stable identifier of the internal system user, when provisioned.
@@ -700,6 +763,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
700
763
  role: input.role,
701
764
  collectionAccess: input.collectionAccess,
702
765
  environmentAccess: input.environmentAccess,
766
+ snippetAccess: input.snippetAccess,
703
767
  llmAccess: input.llmAccess ?? false,
704
768
  llmModels: input.llmModels ?? [],
705
769
  llmMonthlyTokenLimit: input.llmMonthlyTokenLimit ?? null,
@@ -771,6 +835,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
771
835
  const role = input.role ?? existing.role;
772
836
  const collectionAccess = input.collectionAccess ?? existing.collectionAccess;
773
837
  const environmentAccess = input.environmentAccess ?? existing.environmentAccess;
838
+ const snippetAccess = input.snippetAccess ?? existing.snippetAccess;
774
839
  const llmAccess = input.llmAccess ?? existing.llmAccess;
775
840
  const llmModels = input.llmModels ?? existing.llmModels;
776
841
  const llmMonthlyTokenLimit = input.llmMonthlyTokenLimit !== void 0 ? input.llmMonthlyTokenLimit : existing.llmMonthlyTokenLimit;
@@ -780,6 +845,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
780
845
  role,
781
846
  collectionAccess,
782
847
  environmentAccess,
848
+ snippetAccess,
783
849
  llmAccess,
784
850
  llmModels,
785
851
  llmMonthlyTokenLimit,
@@ -834,7 +900,8 @@ var FirestoreDatabase = class _FirestoreDatabase {
834
900
  name: BOOTSTRAP_USER_NAME,
835
901
  role: "user",
836
902
  collectionAccess: ["*"],
837
- environmentAccess: ["*"]
903
+ environmentAccess: ["*"],
904
+ snippetAccess: ["*"]
838
905
  },
839
906
  systemUserId
840
907
  );
@@ -848,6 +915,37 @@ var FirestoreDatabase = class _FirestoreDatabase {
848
915
  await batch.commit();
849
916
  }
850
917
  }
918
+ /**
919
+ * Grants wildcard snippet access to user accounts that already have wildcard collection access.
920
+ */
921
+ async migrateSnippetAccessBackfill() {
922
+ const client = this.requireClient();
923
+ const snapshot = await client.collection(USERS_COLLECTION).where("role", "==", "user").get();
924
+ if (snapshot.docs.length === 0) {
925
+ return;
926
+ }
927
+ let batch = client.batch();
928
+ let batchSize = 0;
929
+ for (const doc of snapshot.docs) {
930
+ const data = doc.data();
931
+ if ((data.snippetAccess?.length ?? 0) > 0) {
932
+ continue;
933
+ }
934
+ if (!data.collectionAccess?.includes("*")) {
935
+ continue;
936
+ }
937
+ batch.update(doc.ref, { snippetAccess: ["*"] });
938
+ batchSize += 1;
939
+ if (batchSize >= WRITE_BATCH_LIMIT) {
940
+ await batch.commit();
941
+ batch = client.batch();
942
+ batchSize = 0;
943
+ }
944
+ }
945
+ if (batchSize > 0) {
946
+ await batch.commit();
947
+ }
948
+ }
851
949
  /**
852
950
  * Inserts a new API token document.
853
951
  *
@@ -1205,6 +1303,129 @@ var FirestoreDatabase = class _FirestoreDatabase {
1205
1303
  updatedByUserId: actingUserId
1206
1304
  });
1207
1305
  }
1306
+ /**
1307
+ * Lists all snippets ordered by sort order then name.
1308
+ */
1309
+ async listSnippets() {
1310
+ const snapshot = await this.requireClient().collection(SNIPPETS_COLLECTION).get();
1311
+ return snapshot.docs.map((doc) => mapFirestoreSnippet(doc.id, doc.data())).sort((left, right) => {
1312
+ if (left.sortOrder !== right.sortOrder) {
1313
+ return left.sortOrder - right.sortOrder;
1314
+ }
1315
+ return left.name.localeCompare(right.name);
1316
+ });
1317
+ }
1318
+ /**
1319
+ * Creates a new snippet with the given fields.
1320
+ *
1321
+ * @param name - Display name for the snippet.
1322
+ * @param code - JavaScript source for the snippet.
1323
+ * @param scope - Execution scope for the snippet.
1324
+ * @param actingUserId - User performing the create action.
1325
+ */
1326
+ async createSnippet(name, code, scope, actingUserId) {
1327
+ const trimmedName = trimRequiredName(name, "Snippet name");
1328
+ const id = randomUUID2();
1329
+ const now = /* @__PURE__ */ new Date();
1330
+ const existing = await this.listSnippets();
1331
+ const maxOrder = existing.reduce((max, snippet) => Math.max(max, snippet.sortOrder), -1);
1332
+ const data = {
1333
+ name: trimmedName,
1334
+ code,
1335
+ scope,
1336
+ sortOrder: maxOrder + 1,
1337
+ createdAt: now,
1338
+ updatedAt: now,
1339
+ createdByUserId: actingUserId,
1340
+ updatedByUserId: actingUserId,
1341
+ deletionLocked: false
1342
+ };
1343
+ await this.requireClient().collection(SNIPPETS_COLLECTION).doc(id).set(data);
1344
+ await this.recordAuditEntry(actingUserId, "create", "snippet", id);
1345
+ return mapFirestoreSnippet(id, data);
1346
+ }
1347
+ /**
1348
+ * Updates a snippet's name, code, and scope. Sort order is left unchanged.
1349
+ *
1350
+ * @param actingUserId - User performing the update action.
1351
+ */
1352
+ async updateSnippet(id, name, code, scope, actingUserId) {
1353
+ const trimmedName = trimRequiredName(name, "Snippet name");
1354
+ const updatedAt = /* @__PURE__ */ new Date();
1355
+ const docRef = this.requireClient().collection(SNIPPETS_COLLECTION).doc(id);
1356
+ const snapshot = await docRef.get();
1357
+ if (!snapshot.exists) {
1358
+ throw new Error("Snippet not found");
1359
+ }
1360
+ const existing = snapshot.data();
1361
+ const updated = {
1362
+ ...existing,
1363
+ name: trimmedName,
1364
+ code,
1365
+ scope,
1366
+ updatedAt,
1367
+ updatedByUserId: actingUserId
1368
+ };
1369
+ await docRef.update({
1370
+ name: trimmedName,
1371
+ code,
1372
+ scope,
1373
+ updatedAt,
1374
+ updatedByUserId: actingUserId
1375
+ });
1376
+ await this.recordAuditEntry(actingUserId, "update", "snippet", id);
1377
+ return mapFirestoreSnippet(id, updated);
1378
+ }
1379
+ /**
1380
+ * Deletes a snippet.
1381
+ *
1382
+ * @param id - Snippet ID to delete.
1383
+ * @param actingUserId - User performing the delete action.
1384
+ */
1385
+ async deleteSnippet(id, actingUserId) {
1386
+ await this.recordAuditEntry(actingUserId, "delete", "snippet", id);
1387
+ await this.requireClient().collection(SNIPPETS_COLLECTION).doc(id).delete();
1388
+ }
1389
+ /**
1390
+ * Finds a snippet by stable identifier.
1391
+ *
1392
+ * @param id - Snippet ID to look up.
1393
+ */
1394
+ async findSnippetById(id) {
1395
+ const snapshot = await this.requireClient().collection(SNIPPETS_COLLECTION).doc(id).get();
1396
+ if (!snapshot.exists) {
1397
+ return null;
1398
+ }
1399
+ return mapFirestoreSnippet(id, snapshot.data());
1400
+ }
1401
+ /**
1402
+ * Updates whether non-admin users may delete a snippet.
1403
+ *
1404
+ * @param id - Snippet ID to update.
1405
+ * @param deletionLocked - When true, user-role tokens cannot delete the snippet.
1406
+ * @param actingUserId - Admin user performing the update.
1407
+ */
1408
+ async setSnippetDeletionLocked(id, deletionLocked, actingUserId) {
1409
+ const docRef = this.requireClient().collection(SNIPPETS_COLLECTION).doc(id);
1410
+ const snapshot = await docRef.get();
1411
+ if (!snapshot.exists) {
1412
+ throw new Error("Snippet not found");
1413
+ }
1414
+ const updatedAt = /* @__PURE__ */ new Date();
1415
+ await docRef.update({
1416
+ deletionLocked,
1417
+ updatedAt,
1418
+ updatedByUserId: actingUserId
1419
+ });
1420
+ await this.recordAuditEntry(actingUserId, "update", "snippet", id);
1421
+ const existing = snapshot.data();
1422
+ return mapFirestoreSnippet(id, {
1423
+ ...existing,
1424
+ deletionLocked,
1425
+ updatedAt,
1426
+ updatedByUserId: actingUserId
1427
+ });
1428
+ }
1208
1429
  /**
1209
1430
  * Lists all saved requests in a collection.
1210
1431
  *
@@ -1671,6 +1892,7 @@ var FirestoreDatabase = class _FirestoreDatabase {
1671
1892
  role: input.role,
1672
1893
  collectionAccess: input.collectionAccess,
1673
1894
  environmentAccess: input.environmentAccess,
1895
+ snippetAccess: input.snippetAccess,
1674
1896
  llmAccess: false,
1675
1897
  llmModels: [],
1676
1898
  llmMonthlyTokenLimit: null,
@@ -1754,7 +1976,7 @@ function parseAuditAction(value) {
1754
1976
  throw new Error(`Invalid audit action: ${value}`);
1755
1977
  }
1756
1978
  function parseAuditEntityType2(value) {
1757
- if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "folder" || value === "request") {
1979
+ if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request") {
1758
1980
  return value;
1759
1981
  }
1760
1982
  throw new Error(`Invalid audit entity type: ${value}`);
@@ -1831,6 +2053,26 @@ function mapEnvironmentSqlRow(row) {
1831
2053
  deletionLocked: Boolean(row.deletion_locked)
1832
2054
  };
1833
2055
  }
2056
+ function parseSnippetScope(value) {
2057
+ if (value === "pre-request" || value === "post-request" || value === "any") {
2058
+ return value;
2059
+ }
2060
+ throw new Error(`Invalid snippet scope: ${value}`);
2061
+ }
2062
+ function mapSnippetSqlRow(row) {
2063
+ return {
2064
+ id: row.id,
2065
+ name: row.name,
2066
+ code: row.code,
2067
+ scope: parseSnippetScope(row.scope),
2068
+ sortOrder: row.sort_order,
2069
+ createdAt: row.created_at,
2070
+ updatedAt: row.updated_at ?? row.created_at,
2071
+ createdByUserId: row.created_by_user_id ?? null,
2072
+ updatedByUserId: row.updated_by_user_id ?? null,
2073
+ deletionLocked: Boolean(row.deletion_locked)
2074
+ };
2075
+ }
1834
2076
  function mapFolderSqlRow(row) {
1835
2077
  return {
1836
2078
  id: row.id,
@@ -1915,6 +2157,22 @@ CREATE TABLE IF NOT EXISTS environments (
1915
2157
  FOREIGN KEY (updated_by_user_id) REFERENCES users(id) ON DELETE SET NULL
1916
2158
  )
1917
2159
  `.trim();
2160
+ var SNIPPETS_MIGRATION_SQL = `
2161
+ CREATE TABLE IF NOT EXISTS snippets (
2162
+ id VARCHAR(36) PRIMARY KEY,
2163
+ name VARCHAR(255) NOT NULL,
2164
+ code LONGTEXT NOT NULL,
2165
+ scope VARCHAR(32) NOT NULL DEFAULT 'any',
2166
+ sort_order INT NOT NULL DEFAULT 0,
2167
+ created_at DATETIME NOT NULL,
2168
+ updated_at DATETIME NOT NULL,
2169
+ created_by_user_id VARCHAR(36) NULL,
2170
+ updated_by_user_id VARCHAR(36) NULL,
2171
+ deletion_locked TINYINT(1) NOT NULL DEFAULT 0,
2172
+ FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL,
2173
+ FOREIGN KEY (updated_by_user_id) REFERENCES users(id) ON DELETE SET NULL
2174
+ )
2175
+ `.trim();
1918
2176
  var FOLDERS_MIGRATION_SQL = `
1919
2177
  CREATE TABLE IF NOT EXISTS folders (
1920
2178
  id VARCHAR(36) PRIMARY KEY,
@@ -1964,6 +2222,7 @@ CREATE TABLE IF NOT EXISTS users (
1964
2222
  role VARCHAR(16) NOT NULL,
1965
2223
  collection_access LONGTEXT NOT NULL,
1966
2224
  environment_access LONGTEXT NOT NULL,
2225
+ snippet_access LONGTEXT NOT NULL,
1967
2226
  created_at DATETIME NOT NULL,
1968
2227
  updated_at DATETIME NOT NULL,
1969
2228
  created_by_user_id VARCHAR(36) NULL,
@@ -2079,11 +2338,23 @@ var ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL = `
2079
2338
  ALTER TABLE environments
2080
2339
  ADD COLUMN IF NOT EXISTS deletion_locked TINYINT(1) NOT NULL DEFAULT 0;
2081
2340
  `.trim();
2341
+ var USERS_SNIPPET_ACCESS_MIGRATION_SQL = `
2342
+ ALTER TABLE users
2343
+ ADD COLUMN IF NOT EXISTS snippet_access LONGTEXT NOT NULL DEFAULT '[]';
2344
+ `.trim();
2345
+ var USERS_SNIPPET_ACCESS_BACKFILL_SQL = `
2346
+ UPDATE users
2347
+ SET snippet_access = '["*"]'
2348
+ WHERE role = 'user'
2349
+ AND snippet_access = '[]'
2350
+ AND collection_access LIKE '%"*"%';
2351
+ `.trim();
2082
2352
  var MYSQL_MIGRATIONS = [
2083
2353
  USERS_MIGRATION_SQL,
2084
2354
  API_TOKENS_MIGRATION_SQL,
2085
2355
  COLLECTIONS_MIGRATION_SQL,
2086
2356
  ENVIRONMENTS_MIGRATION_SQL,
2357
+ SNIPPETS_MIGRATION_SQL,
2087
2358
  FOLDERS_MIGRATION_SQL,
2088
2359
  REQUESTS_MIGRATION_SQL,
2089
2360
  AUDIT_LOG_MIGRATION_SQL,
@@ -2101,7 +2372,9 @@ var MYSQL_MIGRATIONS = [
2101
2372
  LLM_USAGE_MIGRATION_SQL,
2102
2373
  LLM_USAGE_LOG_MIGRATION_SQL,
2103
2374
  COLLECTIONS_DELETION_LOCKED_MIGRATION_SQL,
2104
- ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL
2375
+ ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL,
2376
+ USERS_SNIPPET_ACCESS_MIGRATION_SQL,
2377
+ USERS_SNIPPET_ACCESS_BACKFILL_SQL
2105
2378
  ];
2106
2379
 
2107
2380
  // src/db/mysql/schemas.ts
@@ -2145,6 +2418,7 @@ function mapUserSqlRow(row) {
2145
2418
  role: parseUserRole(row.role),
2146
2419
  collectionAccess: parseAccessList(row.collection_access),
2147
2420
  environmentAccess: parseAccessList(row.environment_access),
2421
+ snippetAccess: parseAccessList(row.snippet_access),
2148
2422
  llmAccess: Boolean(row.llm_access),
2149
2423
  llmModels: parseAccessList(row.llm_models),
2150
2424
  llmMonthlyTokenLimit: row.llm_monthly_token_limit,
@@ -2157,9 +2431,10 @@ function mapUserSqlRow(row) {
2157
2431
  function serializeAccessList(access) {
2158
2432
  return JSON.stringify(access);
2159
2433
  }
2160
- var USER_SELECT_COLUMNS = `id, name, role, collection_access, environment_access, llm_access, llm_models, llm_monthly_token_limit, created_at, updated_at, created_by_user_id, updated_by_user_id`;
2434
+ var USER_SELECT_COLUMNS = `id, name, role, collection_access, environment_access, snippet_access, llm_access, llm_models, llm_monthly_token_limit, created_at, updated_at, created_by_user_id, updated_by_user_id`;
2161
2435
  var COLLECTION_SELECT_COLUMNS = `id, name, variables, headers, auth, pre_request_script, post_request_script, created_at, updated_at, created_by_user_id, updated_by_user_id, deletion_locked`;
2162
2436
  var ENVIRONMENT_SELECT_COLUMNS = `id, name, variables, created_at, updated_at, created_by_user_id, updated_by_user_id, deletion_locked`;
2437
+ var SNIPPET_SELECT_COLUMNS = `id, name, code, scope, sort_order, created_at, updated_at, created_by_user_id, updated_by_user_id, deletion_locked`;
2163
2438
  var FOLDER_SELECT_COLUMNS = `id, collection_id, name, sort_order, created_at, updated_at, created_by_user_id, updated_by_user_id`;
2164
2439
  var REQUEST_SELECT_COLUMNS = `id, collection_id, folder_id, name, method, url, headers, params, auth, body, body_type, pre_request_script, post_request_script, comment, sort_order, created_at, updated_at, created_by_user_id, updated_by_user_id`;
2165
2440
  var API_TOKEN_SELECT_COLUMNS = `id, user_id, name, token_hash, token_prefix, created_at, last_used_at, revoked_at, created_by_user_id, updated_by_user_id`;
@@ -2202,6 +2477,7 @@ var LLM_USAGE_SELECT_COLUMNS = `id, user_id, period, prompt_tokens, completion_t
2202
2477
  // src/db/mysql/MysqlDatabase.ts
2203
2478
  var COLLECTION_SELECT = `SELECT ${COLLECTION_SELECT_COLUMNS} FROM collections`;
2204
2479
  var ENVIRONMENT_SELECT = `SELECT ${ENVIRONMENT_SELECT_COLUMNS} FROM environments`;
2480
+ var SNIPPET_SELECT = `SELECT ${SNIPPET_SELECT_COLUMNS} FROM snippets`;
2205
2481
  var USER_SELECT = `SELECT ${USER_SELECT_COLUMNS} FROM users`;
2206
2482
  var API_TOKEN_SELECT = `SELECT ${API_TOKEN_SELECT_COLUMNS} FROM api_tokens`;
2207
2483
  var FOLDER_SELECT = `SELECT ${FOLDER_SELECT_COLUMNS} FROM folders`;
@@ -2339,6 +2615,7 @@ var MysqlDatabase = class _MysqlDatabase {
2339
2615
  role,
2340
2616
  collection_access,
2341
2617
  environment_access,
2618
+ snippet_access,
2342
2619
  llm_access,
2343
2620
  llm_models,
2344
2621
  llm_monthly_token_limit,
@@ -2346,13 +2623,14 @@ var MysqlDatabase = class _MysqlDatabase {
2346
2623
  updated_at,
2347
2624
  created_by_user_id,
2348
2625
  updated_by_user_id
2349
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2626
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2350
2627
  [
2351
2628
  id,
2352
2629
  trimmedName,
2353
2630
  input.role,
2354
2631
  serializeAccessList(input.collectionAccess),
2355
2632
  serializeAccessList(input.environmentAccess),
2633
+ serializeAccessList(input.snippetAccess),
2356
2634
  input.llmAccess ? 1 : 0,
2357
2635
  serializeAccessList(input.llmModels ?? []),
2358
2636
  input.llmMonthlyTokenLimit ?? null,
@@ -2425,6 +2703,7 @@ var MysqlDatabase = class _MysqlDatabase {
2425
2703
  const role = input.role ?? existing.role;
2426
2704
  const collectionAccess = input.collectionAccess ?? existing.collectionAccess;
2427
2705
  const environmentAccess = input.environmentAccess ?? existing.environmentAccess;
2706
+ const snippetAccess = input.snippetAccess ?? existing.snippetAccess;
2428
2707
  const llmAccess = input.llmAccess ?? existing.llmAccess;
2429
2708
  const llmModels = input.llmModels ?? existing.llmModels;
2430
2709
  const llmMonthlyTokenLimit = input.llmMonthlyTokenLimit !== void 0 ? input.llmMonthlyTokenLimit : existing.llmMonthlyTokenLimit;
@@ -2435,6 +2714,7 @@ var MysqlDatabase = class _MysqlDatabase {
2435
2714
  role = ?,
2436
2715
  collection_access = ?,
2437
2716
  environment_access = ?,
2717
+ snippet_access = ?,
2438
2718
  llm_access = ?,
2439
2719
  llm_models = ?,
2440
2720
  llm_monthly_token_limit = ?,
@@ -2446,6 +2726,7 @@ var MysqlDatabase = class _MysqlDatabase {
2446
2726
  role,
2447
2727
  serializeAccessList(collectionAccess),
2448
2728
  serializeAccessList(environmentAccess),
2729
+ serializeAccessList(snippetAccess),
2449
2730
  llmAccess ? 1 : 0,
2450
2731
  serializeAccessList(llmModels),
2451
2732
  llmMonthlyTokenLimit,
@@ -2507,7 +2788,8 @@ var MysqlDatabase = class _MysqlDatabase {
2507
2788
  name: BOOTSTRAP_USER_NAME,
2508
2789
  role: "user",
2509
2790
  collectionAccess: ["*"],
2510
- environmentAccess: ["*"]
2791
+ environmentAccess: ["*"],
2792
+ snippetAccess: ["*"]
2511
2793
  },
2512
2794
  systemUserId
2513
2795
  );
@@ -2922,6 +3204,143 @@ var MysqlDatabase = class _MysqlDatabase {
2922
3204
  }
2923
3205
  return mapEnvironmentSqlRow(row);
2924
3206
  }
3207
+ /**
3208
+ * Lists all snippets ordered by sort order then name.
3209
+ */
3210
+ async listSnippets() {
3211
+ const rows = await this.queryRows(
3212
+ `${SNIPPET_SELECT} ORDER BY sort_order ASC, name ASC`
3213
+ );
3214
+ return rows.map(mapSnippetSqlRow);
3215
+ }
3216
+ /**
3217
+ * Creates a new snippet with the given fields.
3218
+ *
3219
+ * @param name - Display name for the snippet.
3220
+ * @param code - JavaScript source for the snippet.
3221
+ * @param scope - Execution scope for the snippet.
3222
+ * @param actingUserId - User performing the create action.
3223
+ */
3224
+ async createSnippet(name, code, scope, actingUserId) {
3225
+ const trimmedName = trimRequiredName(name, "Snippet name");
3226
+ const id = randomUUID3();
3227
+ const now = /* @__PURE__ */ new Date();
3228
+ const maxRows = await this.queryRows(
3229
+ "SELECT COALESCE(MAX(sort_order), -1) AS max_order FROM snippets"
3230
+ );
3231
+ const maxOrder = maxRows[0]?.max_order ?? -1;
3232
+ await this.executeStatement(
3233
+ `INSERT INTO snippets (
3234
+ id,
3235
+ name,
3236
+ code,
3237
+ scope,
3238
+ sort_order,
3239
+ created_at,
3240
+ updated_at,
3241
+ created_by_user_id,
3242
+ updated_by_user_id,
3243
+ deletion_locked
3244
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3245
+ [id, trimmedName, code, scope, maxOrder + 1, now, now, actingUserId, actingUserId, 0]
3246
+ );
3247
+ await this.recordAuditEntry(actingUserId, "create", "snippet", id);
3248
+ const rows = await this.queryRows(
3249
+ `${SNIPPET_SELECT} WHERE id = ?`,
3250
+ [id]
3251
+ );
3252
+ const row = rows[0];
3253
+ if (!row) {
3254
+ throw new Error("Snippet not found after insert");
3255
+ }
3256
+ return mapSnippetSqlRow(row);
3257
+ }
3258
+ /**
3259
+ * Updates a snippet's name, code, and scope. Sort order is left unchanged.
3260
+ *
3261
+ * @param actingUserId - User performing the update action.
3262
+ */
3263
+ async updateSnippet(id, name, code, scope, actingUserId) {
3264
+ const trimmedName = trimRequiredName(name, "Snippet name");
3265
+ const updatedAt = /* @__PURE__ */ new Date();
3266
+ const result = await this.executeStatement(
3267
+ `UPDATE snippets
3268
+ SET name = ?,
3269
+ code = ?,
3270
+ scope = ?,
3271
+ updated_at = ?,
3272
+ updated_by_user_id = ?
3273
+ WHERE id = ?`,
3274
+ [trimmedName, code, scope, updatedAt, actingUserId, id]
3275
+ );
3276
+ if ((result.affectedRows ?? 0) === 0) {
3277
+ throw new Error("Snippet not found");
3278
+ }
3279
+ await this.recordAuditEntry(actingUserId, "update", "snippet", id);
3280
+ const rows = await this.queryRows(
3281
+ `${SNIPPET_SELECT} WHERE id = ?`,
3282
+ [id]
3283
+ );
3284
+ const row = rows[0];
3285
+ if (!row) {
3286
+ throw new Error("Snippet not found");
3287
+ }
3288
+ return mapSnippetSqlRow(row);
3289
+ }
3290
+ /**
3291
+ * Deletes a snippet.
3292
+ *
3293
+ * @param id - Snippet ID to delete.
3294
+ * @param actingUserId - User performing the delete action.
3295
+ */
3296
+ async deleteSnippet(id, actingUserId) {
3297
+ await this.recordAuditEntry(actingUserId, "delete", "snippet", id);
3298
+ await this.executeStatement("DELETE FROM snippets WHERE id = ?", [id]);
3299
+ }
3300
+ /**
3301
+ * Finds a snippet by stable identifier.
3302
+ *
3303
+ * @param id - Snippet ID to look up.
3304
+ */
3305
+ async findSnippetById(id) {
3306
+ const rows = await this.queryRows(
3307
+ `${SNIPPET_SELECT} WHERE id = ?`,
3308
+ [id]
3309
+ );
3310
+ const row = rows[0];
3311
+ return row ? mapSnippetSqlRow(row) : null;
3312
+ }
3313
+ /**
3314
+ * Updates whether non-admin users may delete a snippet.
3315
+ *
3316
+ * @param id - Snippet ID to update.
3317
+ * @param deletionLocked - When true, user-role tokens cannot delete the snippet.
3318
+ * @param actingUserId - Admin user performing the update.
3319
+ */
3320
+ async setSnippetDeletionLocked(id, deletionLocked, actingUserId) {
3321
+ const updatedAt = /* @__PURE__ */ new Date();
3322
+ const result = await this.executeStatement(
3323
+ `UPDATE snippets
3324
+ SET deletion_locked = ?,
3325
+ updated_at = ?,
3326
+ updated_by_user_id = ?
3327
+ WHERE id = ?`,
3328
+ [deletionLocked ? 1 : 0, updatedAt, actingUserId, id]
3329
+ );
3330
+ if ((result.affectedRows ?? 0) === 0) {
3331
+ throw new Error("Snippet not found");
3332
+ }
3333
+ await this.recordAuditEntry(actingUserId, "update", "snippet", id);
3334
+ const rows = await this.queryRows(
3335
+ `${SNIPPET_SELECT} WHERE id = ?`,
3336
+ [id]
3337
+ );
3338
+ const row = rows[0];
3339
+ if (!row) {
3340
+ throw new Error("Snippet not found");
3341
+ }
3342
+ return mapSnippetSqlRow(row);
3343
+ }
2925
3344
  /**
2926
3345
  * Lists all saved requests in a collection.
2927
3346
  *
@@ -3484,6 +3903,7 @@ var MysqlDatabase = class _MysqlDatabase {
3484
3903
  role,
3485
3904
  collection_access,
3486
3905
  environment_access,
3906
+ snippet_access,
3487
3907
  llm_access,
3488
3908
  llm_models,
3489
3909
  llm_monthly_token_limit,
@@ -3491,13 +3911,14 @@ var MysqlDatabase = class _MysqlDatabase {
3491
3911
  updated_at,
3492
3912
  created_by_user_id,
3493
3913
  updated_by_user_id
3494
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3914
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3495
3915
  [
3496
3916
  id,
3497
3917
  trimmedName,
3498
3918
  input.role,
3499
3919
  serializeAccessList(input.collectionAccess),
3500
3920
  serializeAccessList(input.environmentAccess),
3921
+ serializeAccessList(input.snippetAccess),
3501
3922
  0,
3502
3923
  serializeAccessList([]),
3503
3924
  null,
@@ -3626,6 +4047,20 @@ CREATE TABLE IF NOT EXISTS environments (
3626
4047
  updated_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL
3627
4048
  );
3628
4049
  `.trim();
4050
+ var SNIPPETS_MIGRATION_SQL2 = `
4051
+ CREATE TABLE IF NOT EXISTS snippets (
4052
+ id TEXT PRIMARY KEY,
4053
+ name TEXT NOT NULL,
4054
+ code TEXT NOT NULL DEFAULT '',
4055
+ scope TEXT NOT NULL DEFAULT 'any' CHECK (scope IN ('pre-request', 'post-request', 'any')),
4056
+ sort_order INT NOT NULL DEFAULT 0,
4057
+ created_at TIMESTAMPTZ NOT NULL,
4058
+ updated_at TIMESTAMPTZ NOT NULL,
4059
+ created_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
4060
+ updated_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
4061
+ deletion_locked BOOLEAN NOT NULL DEFAULT FALSE
4062
+ );
4063
+ `.trim();
3629
4064
  var FOLDERS_MIGRATION_SQL2 = `
3630
4065
  CREATE TABLE IF NOT EXISTS folders (
3631
4066
  id TEXT PRIMARY KEY,
@@ -3671,6 +4106,7 @@ CREATE TABLE IF NOT EXISTS users (
3671
4106
  role TEXT NOT NULL CHECK (role IN ('admin', 'user')),
3672
4107
  collection_access TEXT NOT NULL DEFAULT '[]',
3673
4108
  environment_access TEXT NOT NULL DEFAULT '[]',
4109
+ snippet_access TEXT NOT NULL DEFAULT '[]',
3674
4110
  created_at TIMESTAMPTZ NOT NULL,
3675
4111
  updated_at TIMESTAMPTZ NOT NULL,
3676
4112
  created_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
@@ -3782,11 +4218,23 @@ var ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL2 = `
3782
4218
  ALTER TABLE environments
3783
4219
  ADD COLUMN IF NOT EXISTS deletion_locked BOOLEAN NOT NULL DEFAULT FALSE;
3784
4220
  `.trim();
4221
+ var USERS_SNIPPET_ACCESS_MIGRATION_SQL2 = `
4222
+ ALTER TABLE users
4223
+ ADD COLUMN IF NOT EXISTS snippet_access TEXT NOT NULL DEFAULT '[]';
4224
+ `.trim();
4225
+ var USERS_SNIPPET_ACCESS_BACKFILL_SQL2 = `
4226
+ UPDATE users
4227
+ SET snippet_access = '["*"]'
4228
+ WHERE role = 'user'
4229
+ AND snippet_access = '[]'
4230
+ AND collection_access LIKE '%"*"%';
4231
+ `.trim();
3785
4232
  var POSTGRES_MIGRATIONS = [
3786
4233
  USERS_MIGRATION_SQL2,
3787
4234
  API_TOKENS_MIGRATION_SQL2,
3788
4235
  COLLECTIONS_MIGRATION_SQL2,
3789
4236
  ENVIRONMENTS_MIGRATION_SQL2,
4237
+ SNIPPETS_MIGRATION_SQL2,
3790
4238
  FOLDERS_MIGRATION_SQL2,
3791
4239
  REQUESTS_MIGRATION_SQL2,
3792
4240
  AUDIT_LOG_MIGRATION_SQL2,
@@ -3804,7 +4252,9 @@ var POSTGRES_MIGRATIONS = [
3804
4252
  LLM_USAGE_MIGRATION_SQL2,
3805
4253
  LLM_USAGE_LOG_MIGRATION_SQL2,
3806
4254
  COLLECTIONS_DELETION_LOCKED_MIGRATION_SQL2,
3807
- ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL2
4255
+ ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL2,
4256
+ USERS_SNIPPET_ACCESS_MIGRATION_SQL2,
4257
+ USERS_SNIPPET_ACCESS_BACKFILL_SQL2
3808
4258
  ];
3809
4259
 
3810
4260
  // src/db/postgres/schemas.ts
@@ -3828,6 +4278,7 @@ var postgresConfigSchema = z4.object({
3828
4278
  var { Pool } = pg;
3829
4279
  var COLLECTION_SELECT2 = `SELECT ${COLLECTION_SELECT_COLUMNS} FROM collections`;
3830
4280
  var ENVIRONMENT_SELECT2 = `SELECT ${ENVIRONMENT_SELECT_COLUMNS} FROM environments`;
4281
+ var SNIPPET_SELECT2 = `SELECT ${SNIPPET_SELECT_COLUMNS} FROM snippets`;
3831
4282
  var USER_SELECT2 = `SELECT ${USER_SELECT_COLUMNS} FROM users`;
3832
4283
  var API_TOKEN_SELECT2 = `SELECT ${API_TOKEN_SELECT_COLUMNS} FROM api_tokens`;
3833
4284
  var FOLDER_SELECT2 = `SELECT ${FOLDER_SELECT_COLUMNS} FROM folders`;
@@ -3968,6 +4419,7 @@ var PostgresDatabase = class _PostgresDatabase {
3968
4419
  role,
3969
4420
  collection_access,
3970
4421
  environment_access,
4422
+ snippet_access,
3971
4423
  llm_access,
3972
4424
  llm_models,
3973
4425
  llm_monthly_token_limit,
@@ -3975,7 +4427,7 @@ var PostgresDatabase = class _PostgresDatabase {
3975
4427
  updated_at,
3976
4428
  created_by_user_id,
3977
4429
  updated_by_user_id
3978
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
4430
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
3979
4431
  RETURNING ${USER_SELECT_COLUMNS}`,
3980
4432
  [
3981
4433
  id,
@@ -3983,6 +4435,7 @@ var PostgresDatabase = class _PostgresDatabase {
3983
4435
  input.role,
3984
4436
  serializeAccessList(input.collectionAccess),
3985
4437
  serializeAccessList(input.environmentAccess),
4438
+ serializeAccessList(input.snippetAccess),
3986
4439
  input.llmAccess ?? false,
3987
4440
  serializeAccessList(input.llmModels ?? []),
3988
4441
  input.llmMonthlyTokenLimit ?? null,
@@ -4047,6 +4500,7 @@ var PostgresDatabase = class _PostgresDatabase {
4047
4500
  const role = input.role ?? existing.role;
4048
4501
  const collectionAccess = input.collectionAccess ?? existing.collectionAccess;
4049
4502
  const environmentAccess = input.environmentAccess ?? existing.environmentAccess;
4503
+ const snippetAccess = input.snippetAccess ?? existing.snippetAccess;
4050
4504
  const llmAccess = input.llmAccess ?? existing.llmAccess;
4051
4505
  const llmModels = input.llmModels ?? existing.llmModels;
4052
4506
  const llmMonthlyTokenLimit = input.llmMonthlyTokenLimit !== void 0 ? input.llmMonthlyTokenLimit : existing.llmMonthlyTokenLimit;
@@ -4057,17 +4511,19 @@ var PostgresDatabase = class _PostgresDatabase {
4057
4511
  role = $2,
4058
4512
  collection_access = $3,
4059
4513
  environment_access = $4,
4060
- llm_access = $5,
4061
- llm_models = $6,
4062
- llm_monthly_token_limit = $7,
4063
- updated_at = $8,
4064
- updated_by_user_id = $9
4065
- WHERE id = $10`,
4514
+ snippet_access = $5,
4515
+ llm_access = $6,
4516
+ llm_models = $7,
4517
+ llm_monthly_token_limit = $8,
4518
+ updated_at = $9,
4519
+ updated_by_user_id = $10
4520
+ WHERE id = $11`,
4066
4521
  [
4067
4522
  name,
4068
4523
  role,
4069
4524
  serializeAccessList(collectionAccess),
4070
4525
  serializeAccessList(environmentAccess),
4526
+ serializeAccessList(snippetAccess),
4071
4527
  llmAccess,
4072
4528
  serializeAccessList(llmModels),
4073
4529
  llmMonthlyTokenLimit,
@@ -4129,7 +4585,8 @@ var PostgresDatabase = class _PostgresDatabase {
4129
4585
  name: BOOTSTRAP_USER_NAME,
4130
4586
  role: "user",
4131
4587
  collectionAccess: ["*"],
4132
- environmentAccess: ["*"]
4588
+ environmentAccess: ["*"],
4589
+ snippetAccess: ["*"]
4133
4590
  },
4134
4591
  systemUserId
4135
4592
  );
@@ -4526,27 +4983,151 @@ var PostgresDatabase = class _PostgresDatabase {
4526
4983
  return mapEnvironmentSqlRow(row);
4527
4984
  }
4528
4985
  /**
4529
- * Lists all saved requests in a collection.
4530
- *
4531
- * @param collectionId - Collection to query.
4986
+ * Lists all snippets ordered by sort order then name.
4532
4987
  */
4533
- async listRequests(collectionId) {
4988
+ async listSnippets() {
4534
4989
  const result = await this.query(
4535
- `${REQUEST_SELECT2} WHERE collection_id = $1 ORDER BY sort_order ASC, name ASC`,
4536
- [collectionId]
4990
+ `${SNIPPET_SELECT2} ORDER BY sort_order ASC, name ASC`
4537
4991
  );
4538
- return result.rows.map(mapRequestSqlRow);
4992
+ return result.rows.map(mapSnippetSqlRow);
4539
4993
  }
4540
4994
  /**
4541
- * Finds a saved request by id.
4995
+ * Creates a new snippet with the given fields.
4542
4996
  *
4543
- * @param id - Request identifier to look up.
4997
+ * @param name - Display name for the snippet.
4998
+ * @param code - JavaScript source for the snippet.
4999
+ * @param scope - Execution scope for the snippet.
5000
+ * @param actingUserId - User performing the create action.
4544
5001
  */
4545
- async findRequestById(id) {
4546
- const result = await this.query(`${REQUEST_SELECT2} WHERE id = $1 LIMIT 1`, [id]);
4547
- const row = result.rows[0];
4548
- return row ? mapRequestSqlRow(row) : null;
4549
- }
5002
+ async createSnippet(name, code, scope, actingUserId) {
5003
+ const trimmedName = trimRequiredName(name, "Snippet name");
5004
+ const id = randomUUID4();
5005
+ const now = /* @__PURE__ */ new Date();
5006
+ const maxResult = await this.query(
5007
+ "SELECT COALESCE(MAX(sort_order), -1) AS max_order FROM snippets"
5008
+ );
5009
+ const maxOrder = maxResult.rows[0]?.max_order ?? -1;
5010
+ const result = await this.query(
5011
+ `INSERT INTO snippets (
5012
+ id,
5013
+ name,
5014
+ code,
5015
+ scope,
5016
+ sort_order,
5017
+ created_at,
5018
+ updated_at,
5019
+ created_by_user_id,
5020
+ updated_by_user_id
5021
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
5022
+ RETURNING ${SNIPPET_SELECT_COLUMNS}`,
5023
+ [id, trimmedName, code, scope, maxOrder + 1, now, now, actingUserId, actingUserId]
5024
+ );
5025
+ const row = result.rows[0];
5026
+ if (!row) {
5027
+ throw new Error("Snippet not found after insert");
5028
+ }
5029
+ await this.recordAuditEntry(actingUserId, "create", "snippet", id);
5030
+ return mapSnippetSqlRow(row);
5031
+ }
5032
+ /**
5033
+ * Updates a snippet's name, code, and scope. Sort order is left unchanged.
5034
+ *
5035
+ * @param actingUserId - User performing the update action.
5036
+ */
5037
+ async updateSnippet(id, name, code, scope, actingUserId) {
5038
+ const trimmedName = trimRequiredName(name, "Snippet name");
5039
+ const updatedAt = /* @__PURE__ */ new Date();
5040
+ const result = await this.query(
5041
+ `UPDATE snippets
5042
+ SET name = $1,
5043
+ code = $2,
5044
+ scope = $3,
5045
+ updated_at = $4,
5046
+ updated_by_user_id = $5
5047
+ WHERE id = $6`,
5048
+ [trimmedName, code, scope, updatedAt, actingUserId, id]
5049
+ );
5050
+ if ((result.rowCount ?? 0) === 0) {
5051
+ throw new Error("Snippet not found");
5052
+ }
5053
+ await this.recordAuditEntry(actingUserId, "update", "snippet", id);
5054
+ const selectResult = await this.query(`${SNIPPET_SELECT2} WHERE id = $1`, [id]);
5055
+ const row = selectResult.rows[0];
5056
+ if (!row) {
5057
+ throw new Error("Snippet not found");
5058
+ }
5059
+ return mapSnippetSqlRow(row);
5060
+ }
5061
+ /**
5062
+ * Deletes a snippet.
5063
+ *
5064
+ * @param id - Snippet ID to delete.
5065
+ * @param actingUserId - User performing the delete action.
5066
+ */
5067
+ async deleteSnippet(id, actingUserId) {
5068
+ await this.recordAuditEntry(actingUserId, "delete", "snippet", id);
5069
+ await this.query("DELETE FROM snippets WHERE id = $1", [id]);
5070
+ }
5071
+ /**
5072
+ * Finds a snippet by stable identifier.
5073
+ *
5074
+ * @param id - Snippet ID to look up.
5075
+ */
5076
+ async findSnippetById(id) {
5077
+ const result = await this.query(`${SNIPPET_SELECT2} WHERE id = $1`, [id]);
5078
+ const row = result.rows[0];
5079
+ return row ? mapSnippetSqlRow(row) : null;
5080
+ }
5081
+ /**
5082
+ * Updates whether non-admin users may delete a snippet.
5083
+ *
5084
+ * @param id - Snippet ID to update.
5085
+ * @param deletionLocked - When true, user-role tokens cannot delete the snippet.
5086
+ * @param actingUserId - Admin user performing the update.
5087
+ */
5088
+ async setSnippetDeletionLocked(id, deletionLocked, actingUserId) {
5089
+ const updatedAt = /* @__PURE__ */ new Date();
5090
+ const result = await this.query(
5091
+ `UPDATE snippets
5092
+ SET deletion_locked = $1,
5093
+ updated_at = $2,
5094
+ updated_by_user_id = $3
5095
+ WHERE id = $4`,
5096
+ [deletionLocked, updatedAt, actingUserId, id]
5097
+ );
5098
+ if ((result.rowCount ?? 0) === 0) {
5099
+ throw new Error("Snippet not found");
5100
+ }
5101
+ await this.recordAuditEntry(actingUserId, "update", "snippet", id);
5102
+ const selectResult = await this.query(`${SNIPPET_SELECT2} WHERE id = $1`, [id]);
5103
+ const row = selectResult.rows[0];
5104
+ if (!row) {
5105
+ throw new Error("Snippet not found");
5106
+ }
5107
+ return mapSnippetSqlRow(row);
5108
+ }
5109
+ /**
5110
+ * Lists all saved requests in a collection.
5111
+ *
5112
+ * @param collectionId - Collection to query.
5113
+ */
5114
+ async listRequests(collectionId) {
5115
+ const result = await this.query(
5116
+ `${REQUEST_SELECT2} WHERE collection_id = $1 ORDER BY sort_order ASC, name ASC`,
5117
+ [collectionId]
5118
+ );
5119
+ return result.rows.map(mapRequestSqlRow);
5120
+ }
5121
+ /**
5122
+ * Finds a saved request by id.
5123
+ *
5124
+ * @param id - Request identifier to look up.
5125
+ */
5126
+ async findRequestById(id) {
5127
+ const result = await this.query(`${REQUEST_SELECT2} WHERE id = $1 LIMIT 1`, [id]);
5128
+ const row = result.rows[0];
5129
+ return row ? mapRequestSqlRow(row) : null;
5130
+ }
4550
5131
  /**
4551
5132
  * Inserts a new request or updates an existing one.
4552
5133
  *
@@ -5082,6 +5663,7 @@ var PostgresDatabase = class _PostgresDatabase {
5082
5663
  role,
5083
5664
  collection_access,
5084
5665
  environment_access,
5666
+ snippet_access,
5085
5667
  llm_access,
5086
5668
  llm_models,
5087
5669
  llm_monthly_token_limit,
@@ -5089,13 +5671,14 @@ var PostgresDatabase = class _PostgresDatabase {
5089
5671
  updated_at,
5090
5672
  created_by_user_id,
5091
5673
  updated_by_user_id
5092
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
5674
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
5093
5675
  [
5094
5676
  id,
5095
5677
  SYSTEM_USER_NAME,
5096
5678
  input.role,
5097
5679
  serializeAccessList(input.collectionAccess),
5098
5680
  serializeAccessList(input.environmentAccess),
5681
+ serializeAccessList(input.snippetAccess),
5099
5682
  false,
5100
5683
  serializeAccessList([]),
5101
5684
  null,
@@ -5395,21 +5978,26 @@ function normalizeLlmForRole(role, llmAccess, llmModels) {
5395
5978
  llmModels
5396
5979
  };
5397
5980
  }
5398
- function normalizeAccessForRole(role, collectionAccess, environmentAccess) {
5981
+ function normalizeAccessForRole(role, collectionAccess, environmentAccess, snippetAccess) {
5399
5982
  if (role === "admin") {
5400
- if (collectionAccess.length > 0 || environmentAccess.length > 0) {
5401
- throw new ValidationError("Admin users cannot have collection or environment access.");
5983
+ if (collectionAccess.length > 0 || environmentAccess.length > 0 || snippetAccess.length > 0) {
5984
+ throw new ValidationError(
5985
+ "Admin users cannot have collection, environment, or snippet access."
5986
+ );
5402
5987
  }
5403
5988
  return {
5404
5989
  collectionAccess: [],
5405
- environmentAccess: []
5990
+ environmentAccess: [],
5991
+ snippetAccess: []
5406
5992
  };
5407
5993
  }
5408
5994
  validateAccessList(collectionAccess);
5409
5995
  validateAccessList(environmentAccess);
5996
+ validateAccessList(snippetAccess);
5410
5997
  return {
5411
5998
  collectionAccess,
5412
- environmentAccess
5999
+ environmentAccess,
6000
+ snippetAccess
5413
6001
  };
5414
6002
  }
5415
6003
  function findUnknownAccessIds(access, knownIds) {
@@ -5435,6 +6023,9 @@ function validateSubmittedAccessLists(submitted, catalogs) {
5435
6023
  "environment"
5436
6024
  );
5437
6025
  }
6026
+ if (submitted.snippetAccess !== void 0) {
6027
+ validateKnownAccessIds(submitted.snippetAccess, catalogs.knownSnippetIds, "snippet");
6028
+ }
5438
6029
  }
5439
6030
  if (submitted.llmModels !== void 0 && catalogs.knownLlmModelIds !== null) {
5440
6031
  validateKnownAccessIds(submitted.llmModels, catalogs.knownLlmModelIds, "LLM model");
@@ -5448,6 +6039,9 @@ function buildAccessListWarnings(stored, catalogs) {
5448
6039
  for (const id of findUnknownAccessIds(stored.environmentAccess, catalogs.knownEnvironmentIds)) {
5449
6040
  warnings.push(`Unknown environment id "${id}".`);
5450
6041
  }
6042
+ for (const id of findUnknownAccessIds(stored.snippetAccess, catalogs.knownSnippetIds)) {
6043
+ warnings.push(`Unknown snippet id "${id}".`);
6044
+ }
5451
6045
  if (catalogs.knownLlmModelIds !== null) {
5452
6046
  for (const id of findUnknownAccessIds(stored.llmModels, catalogs.knownLlmModelIds)) {
5453
6047
  warnings.push(`Unknown LLM model id "${id}".`);
@@ -5455,10 +6049,11 @@ function buildAccessListWarnings(stored, catalogs) {
5455
6049
  }
5456
6050
  return warnings;
5457
6051
  }
5458
- function buildAccessCatalogIds(collections, environments, llmModelIds) {
6052
+ function buildAccessCatalogIds(collections, environments, snippets, llmModelIds) {
5459
6053
  return {
5460
6054
  knownCollectionIds: new Set(collections.map((collection) => collection.id)),
5461
6055
  knownEnvironmentIds: new Set(environments.map((environment) => environment.id)),
6056
+ knownSnippetIds: new Set(snippets.map((snippet) => snippet.id)),
5462
6057
  knownLlmModelIds: llmModelIds === null ? null : new Set(llmModelIds)
5463
6058
  };
5464
6059
  }
@@ -5466,7 +6061,8 @@ function buildAdminUserUpdateInput(existing, body) {
5466
6061
  const role = body.role ?? existing.role;
5467
6062
  const collectionAccess = role === "admin" ? [] : body.collectionAccess ?? existing.collectionAccess;
5468
6063
  const environmentAccess = role === "admin" ? [] : body.environmentAccess ?? existing.environmentAccess;
5469
- const access = normalizeAccessForRole(role, collectionAccess, environmentAccess);
6064
+ const snippetAccess = role === "admin" ? [] : body.snippetAccess ?? existing.snippetAccess;
6065
+ const access = normalizeAccessForRole(role, collectionAccess, environmentAccess, snippetAccess);
5470
6066
  const llmAccess = role === "admin" ? false : body.llmAccess ?? existing.llmAccess;
5471
6067
  const llmModels = role === "admin" ? [] : body.llmModels ?? existing.llmModels;
5472
6068
  const llm = normalizeLlmForRole(role, llmAccess, llmModels);
@@ -5475,6 +6071,7 @@ function buildAdminUserUpdateInput(existing, body) {
5475
6071
  role: body.role,
5476
6072
  collectionAccess: access.collectionAccess,
5477
6073
  environmentAccess: access.environmentAccess,
6074
+ snippetAccess: access.snippetAccess,
5478
6075
  llmAccess: llm.llmAccess,
5479
6076
  llmModels: llm.llmModels,
5480
6077
  llmMonthlyTokenLimit: body.llmMonthlyTokenLimit
@@ -5483,7 +6080,13 @@ function buildAdminUserUpdateInput(existing, body) {
5483
6080
  function buildAdminUserCreateInput(body) {
5484
6081
  const collectionAccess = body.collectionAccess ?? [];
5485
6082
  const environmentAccess = body.environmentAccess ?? [];
5486
- const access = normalizeAccessForRole(body.role, collectionAccess, environmentAccess);
6083
+ const snippetAccess = body.snippetAccess ?? [];
6084
+ const access = normalizeAccessForRole(
6085
+ body.role,
6086
+ collectionAccess,
6087
+ environmentAccess,
6088
+ snippetAccess
6089
+ );
5487
6090
  const llmAccess = body.role === "admin" ? false : body.llmAccess ?? false;
5488
6091
  const llmModels = body.role === "admin" ? [] : body.llmModels ?? [];
5489
6092
  const llm = normalizeLlmForRole(body.role, llmAccess, llmModels);
@@ -5492,6 +6095,7 @@ function buildAdminUserCreateInput(body) {
5492
6095
  role: body.role,
5493
6096
  collectionAccess: access.collectionAccess ?? [],
5494
6097
  environmentAccess: access.environmentAccess ?? [],
6098
+ snippetAccess: access.snippetAccess ?? [],
5495
6099
  llmAccess: llm.llmAccess ?? false,
5496
6100
  llmModels: llm.llmModels ?? [],
5497
6101
  llmMonthlyTokenLimit: body.llmMonthlyTokenLimit ?? null
@@ -5590,6 +6194,7 @@ function printUser(user, usage) {
5590
6194
  console.log(` role: ${user.role}`);
5591
6195
  console.log(` collection access: ${formatAccessList(user.collectionAccess)}`);
5592
6196
  console.log(` environment access: ${formatAccessList(user.environmentAccess)}`);
6197
+ console.log(` snippet access: ${formatAccessList(user.snippetAccess)}`);
5593
6198
  console.log(` llm access: ${user.llmAccess ? "enabled" : "disabled"}`);
5594
6199
  console.log(` llm models: ${formatAccessList(user.llmModels)}`);
5595
6200
  console.log(
@@ -5609,13 +6214,15 @@ function printCreatedApiToken(user, record, secret) {
5609
6214
  console.log(secret);
5610
6215
  }
5611
6216
  async function loadAccessCatalogs(db, llm) {
5612
- const [collections, environments] = await Promise.all([
6217
+ const [collections, environments, snippets] = await Promise.all([
5613
6218
  db.listCollections(),
5614
- db.listEnvironments()
6219
+ db.listEnvironments(),
6220
+ db.listSnippets()
5615
6221
  ]);
5616
6222
  return buildAccessCatalogIds(
5617
6223
  collections,
5618
6224
  environments,
6225
+ snippets,
5619
6226
  llm ? listHubOfferedModels(llm).map((model) => model.id) : null
5620
6227
  );
5621
6228
  }
@@ -5641,7 +6248,12 @@ async function userCreateCommand(options) {
5641
6248
  const config = loadServerConfig(options.config);
5642
6249
  const db = createDatabase(config.db);
5643
6250
  const access = mapValidationError(
5644
- () => normalizeAccessForRole(options.role, options.collectionAccess, options.environmentAccess)
6251
+ () => normalizeAccessForRole(
6252
+ options.role,
6253
+ options.collectionAccess,
6254
+ options.environmentAccess,
6255
+ options.snippetAccess
6256
+ )
5645
6257
  );
5646
6258
  await db.connect();
5647
6259
  const actingUserId = await requireSystemUserId(db);
@@ -5655,6 +6267,7 @@ async function userCreateCommand(options) {
5655
6267
  role: options.role,
5656
6268
  collectionAccess: access.collectionAccess,
5657
6269
  environmentAccess: access.environmentAccess,
6270
+ snippetAccess: access.snippetAccess,
5658
6271
  llmModels
5659
6272
  },
5660
6273
  catalogs
@@ -5665,6 +6278,7 @@ async function userCreateCommand(options) {
5665
6278
  role: options.role,
5666
6279
  collectionAccess: access.collectionAccess ?? [],
5667
6280
  environmentAccess: access.environmentAccess ?? [],
6281
+ snippetAccess: access.snippetAccess ?? [],
5668
6282
  llmAccess: llm.llmAccess,
5669
6283
  llmModels: llm.llmModels,
5670
6284
  llmMonthlyTokenLimit: options.llmMonthlyTokens ?? null
@@ -5702,6 +6316,7 @@ async function userListCommand(options) {
5702
6316
  {
5703
6317
  collectionAccess: user.collectionAccess,
5704
6318
  environmentAccess: user.environmentAccess,
6319
+ snippetAccess: user.snippetAccess,
5705
6320
  llmModels: user.llmModels
5706
6321
  },
5707
6322
  catalogs
@@ -5731,6 +6346,7 @@ async function userShowCommand(options) {
5731
6346
  {
5732
6347
  collectionAccess: user.collectionAccess,
5733
6348
  environmentAccess: user.environmentAccess,
6349
+ snippetAccess: user.snippetAccess,
5734
6350
  llmModels: user.llmModels
5735
6351
  },
5736
6352
  catalogs
@@ -5752,8 +6368,9 @@ async function userUpdateCommand(options) {
5752
6368
  const role = options.role ?? existing.role;
5753
6369
  const collectionAccess = options.collectionAccess ?? (options.role === "admin" ? [] : existing.collectionAccess);
5754
6370
  const environmentAccess = options.environmentAccess ?? (options.role === "admin" ? [] : existing.environmentAccess);
6371
+ const snippetAccess = options.snippetAccess ?? (options.role === "admin" ? [] : existing.snippetAccess);
5755
6372
  const access = mapValidationError(
5756
- () => normalizeAccessForRole(role, collectionAccess, environmentAccess)
6373
+ () => normalizeAccessForRole(role, collectionAccess, environmentAccess, snippetAccess)
5757
6374
  );
5758
6375
  const llmAccess = role === "admin" ? false : options.llmAccess ?? existing.llmAccess;
5759
6376
  const llmModels = role === "admin" ? [] : options.llmModels ?? existing.llmModels;
@@ -5764,6 +6381,7 @@ async function userUpdateCommand(options) {
5764
6381
  role,
5765
6382
  collectionAccess: options.collectionAccess,
5766
6383
  environmentAccess: options.environmentAccess,
6384
+ snippetAccess: options.snippetAccess,
5767
6385
  llmModels: options.llmModels
5768
6386
  },
5769
6387
  catalogs
@@ -5773,6 +6391,7 @@ async function userUpdateCommand(options) {
5773
6391
  role: options.role,
5774
6392
  collectionAccess: access.collectionAccess,
5775
6393
  environmentAccess: access.environmentAccess,
6394
+ snippetAccess: access.snippetAccess,
5776
6395
  llmAccess: llm.llmAccess,
5777
6396
  llmModels: llm.llmModels,
5778
6397
  llmMonthlyTokenLimit: options.llmMonthlyTokens !== void 0 ? options.llmMonthlyTokens : void 0
@@ -5856,6 +6475,11 @@ function registerUserCommand(program, handlers = {}) {
5856
6475
  "Environment id or * (repeatable)",
5857
6476
  parseAccessFlag,
5858
6477
  []
6478
+ ).option(
6479
+ "--snippet-access <id>",
6480
+ "Snippet id or * (repeatable)",
6481
+ parseAccessFlag,
6482
+ []
5859
6483
  ).option("--llm-access", "Enable hub-proxied LLM access for the user").option("--llm-model <id>", "LLM model id or * (repeatable)", parseAccessFlag, []).option("--llm-monthly-tokens <count>", "Monthly LLM token limit", parseMonthlyTokenLimit).action(
5860
6484
  /**
5861
6485
  * Runs the user create subcommand after merging global CLI options.
@@ -5890,6 +6514,11 @@ function registerUserCommand(program, handlers = {}) {
5890
6514
  "Replacement environment id or * (repeatable)",
5891
6515
  parseAccessFlag,
5892
6516
  []
6517
+ ).option(
6518
+ "--snippet-access <id>",
6519
+ "Replacement snippet id or * (repeatable)",
6520
+ parseAccessFlag,
6521
+ []
5893
6522
  ).option("--llm-access", "Enable hub-proxied LLM access for the user").option("--no-llm-access", "Disable hub-proxied LLM access for the user").option(
5894
6523
  "--llm-model <id>",
5895
6524
  "Replacement LLM model id or * (repeatable)",
@@ -5905,6 +6534,7 @@ function registerUserCommand(program, handlers = {}) {
5905
6534
  ...merged,
5906
6535
  collectionAccess: (options.collectionAccess ?? []).length > 0 ? options.collectionAccess : void 0,
5907
6536
  environmentAccess: (options.environmentAccess ?? []).length > 0 ? options.environmentAccess : void 0,
6537
+ snippetAccess: (options.snippetAccess ?? []).length > 0 ? options.snippetAccess : void 0,
5908
6538
  llmModels: (() => {
5909
6539
  const llmModels = readLlmModelsOption(options);
5910
6540
  return llmModels.length > 0 ? llmModels : void 0;
@@ -6022,6 +6652,9 @@ function canListCollections(user) {
6022
6652
  function canListEnvironments(user) {
6023
6653
  return canUseDataApi(user) || canUseManagementApi(user);
6024
6654
  }
6655
+ function canListSnippets(user) {
6656
+ return canUseDataApi(user) || canUseManagementApi(user);
6657
+ }
6025
6658
  function hasWildcardAccess(access) {
6026
6659
  return access.includes("*");
6027
6660
  }
@@ -6043,6 +6676,15 @@ function canAccessEnvironment(user, environmentId) {
6043
6676
  }
6044
6677
  return user.environmentAccess.includes(environmentId);
6045
6678
  }
6679
+ function canAccessSnippet(user, snippetId) {
6680
+ if (user.role === "admin") {
6681
+ return false;
6682
+ }
6683
+ if (hasWildcardAccess(user.snippetAccess)) {
6684
+ return true;
6685
+ }
6686
+ return user.snippetAccess.includes(snippetId);
6687
+ }
6046
6688
  function canDeleteCollection(user, collection) {
6047
6689
  return canUseDataApi(user) && canAccessCollection(user, collection.id) && collection.createdByUserId === user.id && !collection.deletionLocked;
6048
6690
  }
@@ -6055,6 +6697,9 @@ function canCreateCollection(user) {
6055
6697
  function canCreateEnvironment(user) {
6056
6698
  return user.role === "user" && hasWildcardAccess(user.environmentAccess);
6057
6699
  }
6700
+ function canCreateSnippet(user) {
6701
+ return user.role === "user" && hasWildcardAccess(user.snippetAccess);
6702
+ }
6058
6703
  function filterAccessibleCollections(user, collections) {
6059
6704
  if (user.role === "admin" || hasWildcardAccess(user.collectionAccess)) {
6060
6705
  return collections;
@@ -6069,6 +6714,13 @@ function filterAccessibleEnvironments(user, environments) {
6069
6714
  const allowed = new Set(user.environmentAccess);
6070
6715
  return environments.filter((environment) => allowed.has(environment.id));
6071
6716
  }
6717
+ function filterAccessibleSnippets(user, snippets) {
6718
+ if (user.role === "admin" || hasWildcardAccess(user.snippetAccess)) {
6719
+ return snippets;
6720
+ }
6721
+ const allowed = new Set(user.snippetAccess);
6722
+ return snippets.filter((snippet) => allowed.has(snippet.id));
6723
+ }
6072
6724
  function canUseLlm(user) {
6073
6725
  return user.role !== "admin" && user.llmAccess;
6074
6726
  }
@@ -6297,12 +6949,18 @@ var updateAdminCollectionBodySchema = z8.object({
6297
6949
  var updateAdminEnvironmentBodySchema = z8.object({
6298
6950
  deletionLocked: z8.boolean()
6299
6951
  });
6952
+ var updateAdminSnippetBodySchema = z8.object({
6953
+ deletionLocked: z8.boolean()
6954
+ });
6300
6955
  var listAdminCollectionsResponseSchema = z8.object({
6301
6956
  collections: z8.array(adminResourceOptionSchema)
6302
6957
  });
6303
6958
  var listAdminEnvironmentsResponseSchema = z8.object({
6304
6959
  environments: z8.array(adminResourceOptionSchema)
6305
6960
  });
6961
+ var listAdminSnippetsResponseSchema = z8.object({
6962
+ snippets: z8.array(adminResourceOptionSchema)
6963
+ });
6306
6964
  var listAdminLlmModelsResponseSchema = listLlmModelsResponseSchema;
6307
6965
  var hubUserRecordSchema = z8.object({
6308
6966
  id: z8.string(),
@@ -6310,6 +6968,7 @@ var hubUserRecordSchema = z8.object({
6310
6968
  role: userRoleSchema,
6311
6969
  collectionAccess: z8.array(z8.string()),
6312
6970
  environmentAccess: z8.array(z8.string()),
6971
+ snippetAccess: z8.array(z8.string()),
6313
6972
  llmAccess: z8.boolean(),
6314
6973
  llmModels: z8.array(z8.string()),
6315
6974
  llmMonthlyTokenLimit: z8.number().int().nonnegative().nullable(),
@@ -6327,6 +6986,7 @@ var updateAdminUserBodySchema = z8.object({
6327
6986
  role: userRoleSchema.optional(),
6328
6987
  collectionAccess: z8.array(z8.string()).optional(),
6329
6988
  environmentAccess: z8.array(z8.string()).optional(),
6989
+ snippetAccess: z8.array(z8.string()).optional(),
6330
6990
  llmAccess: z8.boolean().optional(),
6331
6991
  llmModels: z8.array(z8.string()).optional(),
6332
6992
  llmMonthlyTokenLimit: z8.number().int().nonnegative().nullable().optional()
@@ -6336,6 +6996,7 @@ var createAdminUserBodySchema = z8.object({
6336
6996
  role: userRoleSchema,
6337
6997
  collectionAccess: z8.array(z8.string()).optional(),
6338
6998
  environmentAccess: z8.array(z8.string()).optional(),
6999
+ snippetAccess: z8.array(z8.string()).optional(),
6339
7000
  llmAccess: z8.boolean().optional(),
6340
7001
  llmModels: z8.array(z8.string()).optional(),
6341
7002
  llmMonthlyTokenLimit: z8.number().int().nonnegative().nullable().optional()
@@ -6380,6 +7041,7 @@ function serializeHubUser(user) {
6380
7041
  role: user.role,
6381
7042
  collectionAccess: user.collectionAccess,
6382
7043
  environmentAccess: user.environmentAccess,
7044
+ snippetAccess: user.snippetAccess,
6383
7045
  llmAccess: user.llmAccess,
6384
7046
  llmModels: user.llmModels,
6385
7047
  llmMonthlyTokenLimit: user.llmMonthlyTokenLimit,
@@ -6425,6 +7087,19 @@ var environmentRecordSchema = z9.object({
6425
7087
  updatedByUserId: z9.string().nullable(),
6426
7088
  deletionLocked: z9.boolean()
6427
7089
  });
7090
+ var snippetScopeSchema = z9.enum(["pre-request", "post-request", "any"]);
7091
+ var snippetRecordSchema = z9.object({
7092
+ id: z9.string(),
7093
+ name: z9.string(),
7094
+ code: z9.string(),
7095
+ scope: snippetScopeSchema,
7096
+ sortOrder: z9.number().int(),
7097
+ createdAt: timestampSchema,
7098
+ updatedAt: timestampSchema,
7099
+ createdByUserId: z9.string().nullable(),
7100
+ updatedByUserId: z9.string().nullable(),
7101
+ deletionLocked: z9.boolean()
7102
+ });
6428
7103
  var folderRecordSchema = z9.object({
6429
7104
  id: z9.string(),
6430
7105
  collectionId: z9.string(),
@@ -6474,6 +7149,16 @@ var updateEnvironmentBodySchema = z9.object({
6474
7149
  name: z9.string().trim().min(1),
6475
7150
  variables: z9.array(variableSchema)
6476
7151
  });
7152
+ var createSnippetBodySchema = z9.object({
7153
+ name: z9.string().trim().min(1),
7154
+ code: z9.string().default(""),
7155
+ scope: snippetScopeSchema.default("any")
7156
+ });
7157
+ var updateSnippetBodySchema = z9.object({
7158
+ name: z9.string().trim().min(1),
7159
+ code: z9.string(),
7160
+ scope: snippetScopeSchema
7161
+ });
6477
7162
  var createFolderBodySchema = z9.object({
6478
7163
  name: z9.string().trim().min(1)
6479
7164
  });
@@ -6514,6 +7199,9 @@ var listCollectionsResponseSchema = z9.object({
6514
7199
  var listEnvironmentsResponseSchema = z9.object({
6515
7200
  environments: z9.array(environmentRecordSchema)
6516
7201
  });
7202
+ var listSnippetsResponseSchema = z9.object({
7203
+ snippets: z9.array(snippetRecordSchema)
7204
+ });
6517
7205
  var listFoldersResponseSchema = z9.object({
6518
7206
  folders: z9.array(folderRecordSchema)
6519
7207
  });
@@ -6535,6 +7223,13 @@ function serializeEnvironment(record) {
6535
7223
  updatedAt: record.updatedAt.toISOString()
6536
7224
  };
6537
7225
  }
7226
+ function serializeSnippet(record) {
7227
+ return {
7228
+ ...record,
7229
+ createdAt: record.createdAt.toISOString(),
7230
+ updatedAt: record.updatedAt.toISOString()
7231
+ };
7232
+ }
6538
7233
  function serializeFolder(record) {
6539
7234
  return {
6540
7235
  ...record,
@@ -6603,14 +7298,16 @@ async function registerAdminRoutes(app, options) {
6603
7298
  }
6604
7299
  const systemUserId = db.getSystemUserId();
6605
7300
  const llm = getLlm();
6606
- const [users, collections, environments] = await Promise.all([
7301
+ const [users, collections, environments, snippets] = await Promise.all([
6607
7302
  db.listUsers(),
6608
7303
  db.listCollections(),
6609
- db.listEnvironments()
7304
+ db.listEnvironments(),
7305
+ db.listSnippets()
6610
7306
  ]);
6611
7307
  const catalogs = buildAccessCatalogIds(
6612
7308
  collections,
6613
7309
  environments,
7310
+ snippets,
6614
7311
  llm ? listHubOfferedModels(llm).map((model) => model.id) : null
6615
7312
  );
6616
7313
  return reply.send({
@@ -6620,6 +7317,7 @@ async function registerAdminRoutes(app, options) {
6620
7317
  {
6621
7318
  collectionAccess: record.collectionAccess,
6622
7319
  environmentAccess: record.environmentAccess,
7320
+ snippetAccess: record.snippetAccess,
6623
7321
  llmModels: record.llmModels
6624
7322
  },
6625
7323
  catalogs
@@ -6656,13 +7354,15 @@ async function registerAdminRoutes(app, options) {
6656
7354
  }
6657
7355
  const input = buildAdminUserCreateInput(request.body);
6658
7356
  const llm = getLlm();
6659
- const [collections, environments] = await Promise.all([
7357
+ const [collections, environments, snippets] = await Promise.all([
6660
7358
  db.listCollections(),
6661
- db.listEnvironments()
7359
+ db.listEnvironments(),
7360
+ db.listSnippets()
6662
7361
  ]);
6663
7362
  const catalogs = buildAccessCatalogIds(
6664
7363
  collections,
6665
7364
  environments,
7365
+ snippets,
6666
7366
  llm ? listHubOfferedModels(llm).map((model) => model.id) : null
6667
7367
  );
6668
7368
  validateSubmittedAccessLists(
@@ -6670,6 +7370,7 @@ async function registerAdminRoutes(app, options) {
6670
7370
  role: request.body.role,
6671
7371
  collectionAccess: request.body.collectionAccess,
6672
7372
  environmentAccess: request.body.environmentAccess,
7373
+ snippetAccess: request.body.snippetAccess,
6673
7374
  llmModels: request.body.llmModels
6674
7375
  },
6675
7376
  catalogs
@@ -6832,6 +7533,40 @@ async function registerAdminRoutes(app, options) {
6832
7533
  }
6833
7534
  }
6834
7535
  });
7536
+ routes.route({
7537
+ method: "GET",
7538
+ url: "/admin/snippets",
7539
+ schema: {
7540
+ response: {
7541
+ 200: listAdminSnippetsResponseSchema,
7542
+ 403: errorResponseSchema
7543
+ }
7544
+ },
7545
+ /**
7546
+ * Lists all snippets for operator user management.
7547
+ */
7548
+ handler: async (request, reply) => {
7549
+ try {
7550
+ const user = requireAuthenticatedUser(request);
7551
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
7552
+ return;
7553
+ }
7554
+ const snippets = await db.listSnippets();
7555
+ return reply.send({
7556
+ snippets: snippets.map((snippet) => ({
7557
+ id: snippet.id,
7558
+ name: snippet.name,
7559
+ deletionLocked: snippet.deletionLocked
7560
+ }))
7561
+ });
7562
+ } catch (error) {
7563
+ if (handleDbError(reply, error)) {
7564
+ return;
7565
+ }
7566
+ throw error;
7567
+ }
7568
+ }
7569
+ });
6835
7570
  routes.route({
6836
7571
  method: "DELETE",
6837
7572
  url: "/admin/collections/:id",
@@ -6902,6 +7637,80 @@ async function registerAdminRoutes(app, options) {
6902
7637
  }
6903
7638
  }
6904
7639
  });
7640
+ routes.route({
7641
+ method: "DELETE",
7642
+ url: "/admin/snippets/:id",
7643
+ schema: {
7644
+ params: idParamSchema,
7645
+ response: {
7646
+ 204: emptyResponseSchema,
7647
+ 403: errorResponseSchema,
7648
+ 404: errorResponseSchema
7649
+ }
7650
+ },
7651
+ /**
7652
+ * Deletes a snippet regardless of deletion lock state.
7653
+ */
7654
+ handler: async (request, reply) => {
7655
+ try {
7656
+ const user = requireAuthenticatedUser(request);
7657
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
7658
+ return;
7659
+ }
7660
+ const snippet = await db.findSnippetById(request.params.id);
7661
+ if (!snippet) {
7662
+ void reply.code(404).send({ error: "Snippet not found" });
7663
+ return;
7664
+ }
7665
+ await db.deleteSnippet(request.params.id, user.id);
7666
+ return reply.code(204).send(null);
7667
+ } catch (error) {
7668
+ if (handleDbError(reply, error)) {
7669
+ return;
7670
+ }
7671
+ throw error;
7672
+ }
7673
+ }
7674
+ });
7675
+ routes.route({
7676
+ method: "PUT",
7677
+ url: "/admin/snippets/:id",
7678
+ schema: {
7679
+ params: idParamSchema,
7680
+ body: updateAdminSnippetBodySchema,
7681
+ response: {
7682
+ 200: adminEntityConfigSchema,
7683
+ 403: errorResponseSchema,
7684
+ 404: errorResponseSchema
7685
+ }
7686
+ },
7687
+ /**
7688
+ * Updates admin configuration for a snippet.
7689
+ */
7690
+ handler: async (request, reply) => {
7691
+ try {
7692
+ const user = requireAuthenticatedUser(request);
7693
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
7694
+ return;
7695
+ }
7696
+ const snippet = await db.setSnippetDeletionLocked(
7697
+ request.params.id,
7698
+ request.body.deletionLocked,
7699
+ user.id
7700
+ );
7701
+ return reply.send({
7702
+ id: snippet.id,
7703
+ name: snippet.name,
7704
+ deletionLocked: snippet.deletionLocked
7705
+ });
7706
+ } catch (error) {
7707
+ if (handleDbError(reply, error)) {
7708
+ return;
7709
+ }
7710
+ throw error;
7711
+ }
7712
+ }
7713
+ });
6905
7714
  routes.route({
6906
7715
  method: "PUT",
6907
7716
  url: "/admin/collections/:id",
@@ -7082,13 +7891,15 @@ async function registerAdminRoutes(app, options) {
7082
7891
  const input = buildAdminUserUpdateInput(existing, request.body);
7083
7892
  const role = request.body.role ?? existing.role;
7084
7893
  const llm = getLlm();
7085
- const [collections, environments] = await Promise.all([
7894
+ const [collections, environments, snippets] = await Promise.all([
7086
7895
  db.listCollections(),
7087
- db.listEnvironments()
7896
+ db.listEnvironments(),
7897
+ db.listSnippets()
7088
7898
  ]);
7089
7899
  const catalogs = buildAccessCatalogIds(
7090
7900
  collections,
7091
7901
  environments,
7902
+ snippets,
7092
7903
  llm ? listHubOfferedModels(llm).map((model) => model.id) : null
7093
7904
  );
7094
7905
  validateSubmittedAccessLists(
@@ -7096,6 +7907,7 @@ async function registerAdminRoutes(app, options) {
7096
7907
  role,
7097
7908
  collectionAccess: request.body.collectionAccess,
7098
7909
  environmentAccess: request.body.environmentAccess,
7910
+ snippetAccess: request.body.snippetAccess,
7099
7911
  llmModels: request.body.llmModels
7100
7912
  },
7101
7913
  catalogs
@@ -7632,6 +8444,150 @@ async function registerEnvironmentRoutes(app, db) {
7632
8444
  });
7633
8445
  }
7634
8446
 
8447
+ // src/server/routes/snippets.ts
8448
+ async function registerSnippetRoutes(app, db) {
8449
+ const routes = app.withTypeProvider();
8450
+ routes.route({
8451
+ method: "GET",
8452
+ url: "/snippets",
8453
+ schema: {
8454
+ response: {
8455
+ 200: listSnippetsResponseSchema
8456
+ }
8457
+ },
8458
+ /**
8459
+ * Lists all snippets ordered by sort order then name.
8460
+ */
8461
+ handler: async (request, reply) => {
8462
+ try {
8463
+ const user = requireAuthenticatedUser(request);
8464
+ if (denyUnlessAllowed(reply, canListSnippets(user))) {
8465
+ return;
8466
+ }
8467
+ const snippets = await db.listSnippets();
8468
+ return reply.send({
8469
+ snippets: filterAccessibleSnippets(user, snippets).map(
8470
+ (snippet) => serializeSnippet(snippet)
8471
+ )
8472
+ });
8473
+ } catch (error) {
8474
+ if (handleDbError(reply, error)) {
8475
+ return;
8476
+ }
8477
+ throw error;
8478
+ }
8479
+ }
8480
+ });
8481
+ routes.route({
8482
+ method: "POST",
8483
+ url: "/snippets",
8484
+ schema: {
8485
+ body: createSnippetBodySchema,
8486
+ response: {
8487
+ 200: snippetRecordSchema,
8488
+ 400: errorResponseSchema
8489
+ }
8490
+ },
8491
+ /**
8492
+ * Creates a new snippet with the given display name and content.
8493
+ */
8494
+ handler: async (request, reply) => {
8495
+ try {
8496
+ const user = requireAuthenticatedUser(request);
8497
+ if (denyUnlessAllowed(reply, canUseDataApi(user) && canCreateSnippet(user))) {
8498
+ return;
8499
+ }
8500
+ const snippet = await db.createSnippet(
8501
+ request.body.name,
8502
+ request.body.code,
8503
+ request.body.scope,
8504
+ user.id
8505
+ );
8506
+ return reply.send(serializeSnippet(snippet));
8507
+ } catch (error) {
8508
+ if (handleDbError(reply, error)) {
8509
+ return;
8510
+ }
8511
+ throw error;
8512
+ }
8513
+ }
8514
+ });
8515
+ routes.route({
8516
+ method: "PUT",
8517
+ url: "/snippets/:id",
8518
+ schema: {
8519
+ params: idParamSchema,
8520
+ body: updateSnippetBodySchema,
8521
+ response: {
8522
+ 200: snippetRecordSchema,
8523
+ 400: errorResponseSchema,
8524
+ 404: errorResponseSchema
8525
+ }
8526
+ },
8527
+ /**
8528
+ * Updates a snippet's name, code, and scope. Sort order is preserved.
8529
+ */
8530
+ handler: async (request, reply) => {
8531
+ try {
8532
+ const user = requireAuthenticatedUser(request);
8533
+ if (denyUnlessAllowed(reply, canUseDataApi(user) && canAccessSnippet(user, request.params.id))) {
8534
+ return;
8535
+ }
8536
+ const snippet = await db.updateSnippet(
8537
+ request.params.id,
8538
+ request.body.name,
8539
+ request.body.code,
8540
+ request.body.scope,
8541
+ user.id
8542
+ );
8543
+ return reply.send(serializeSnippet(snippet));
8544
+ } catch (error) {
8545
+ if (handleDbError(reply, error)) {
8546
+ return;
8547
+ }
8548
+ throw error;
8549
+ }
8550
+ }
8551
+ });
8552
+ routes.route({
8553
+ method: "DELETE",
8554
+ url: "/snippets/:id",
8555
+ schema: {
8556
+ params: idParamSchema,
8557
+ response: {
8558
+ 204: emptyResponseSchema,
8559
+ 404: errorResponseSchema
8560
+ }
8561
+ },
8562
+ /**
8563
+ * Deletes a snippet by id.
8564
+ */
8565
+ handler: async (request, reply) => {
8566
+ try {
8567
+ const user = requireAuthenticatedUser(request);
8568
+ if (denyUnlessAllowed(reply, canUseDataApi(user) && canAccessSnippet(user, request.params.id))) {
8569
+ return;
8570
+ }
8571
+ const snippet = await db.findSnippetById(request.params.id);
8572
+ if (!snippet) {
8573
+ void reply.code(404).send({ error: "Snippet not found" });
8574
+ return;
8575
+ }
8576
+ if (snippet.deletionLocked) {
8577
+ throw new DeletionLockedError("snippet");
8578
+ }
8579
+ await db.deleteSnippet(request.params.id, user.id);
8580
+ return reply.code(204).send(null);
8581
+ } catch (error) {
8582
+ if (handleDbError(reply, error)) {
8583
+ return;
8584
+ }
8585
+ throw error;
8586
+ }
8587
+ }
8588
+ });
8589
+ }
8590
+
7635
8591
  // src/server/routes/folders.ts
7636
8592
  async function registerFolderRoutes(app, db) {
7637
8593
  const routes = app.withTypeProvider();
@@ -8216,6 +9172,258 @@ async function runLlmCompletion(config, input) {
8216
9172
  return parseCompletionResponse(json);
8217
9173
  }
8218
9174
 
9175
+ // src/server/llm/hubMcpToolNames.ts
9176
+ var HUB_MCP_TOOL_PREFIX = "hubmcp__";
9177
+ function encodeHubMcpToolName(serverIndex, toolName) {
9178
+ return `${HUB_MCP_TOOL_PREFIX}${serverIndex}__${toolName}`;
9179
+ }
9180
+ function decodeHubMcpToolName(prefixed) {
9181
+ if (!prefixed.startsWith(HUB_MCP_TOOL_PREFIX)) {
9182
+ return null;
9183
+ }
9184
+ const rest = prefixed.slice(HUB_MCP_TOOL_PREFIX.length);
9185
+ const separatorIndex = rest.indexOf("__");
9186
+ if (separatorIndex <= 0) {
9187
+ return null;
9188
+ }
9189
+ const serverIndex = Number.parseInt(rest.slice(0, separatorIndex), 10);
9190
+ if (!Number.isInteger(serverIndex) || serverIndex < 0) {
9191
+ return null;
9192
+ }
9193
+ const toolName = rest.slice(separatorIndex + 2);
9194
+ if (!toolName) {
9195
+ return null;
9196
+ }
9197
+ return { serverIndex, toolName };
9198
+ }
9199
+ function isHubMcpToolName(name) {
9200
+ return decodeHubMcpToolName(name) !== null;
9201
+ }
9202
+
9203
+ // src/server/llm/mcpClient.ts
9204
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
9205
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
9206
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
9207
+ var connectedClients = /* @__PURE__ */ new Map();
9208
+ var activeConfigSignature = "";
9209
+ function buildRequestHeaders(headers) {
9210
+ const result = {};
9211
+ for (const row of headers) {
9212
+ if (row.key.trim()) {
9213
+ result[row.key.trim()] = row.value;
9214
+ }
9215
+ }
9216
+ return result;
9217
+ }
9218
+ function buildMcpConfigSignature(config) {
9219
+ return JSON.stringify(config.mcp ?? []);
9220
+ }
9221
+ function flattenMcpToolContent(content) {
9222
+ if (!Array.isArray(content)) {
9223
+ return JSON.stringify(content ?? null);
9224
+ }
9225
+ const parts = content.map((item) => {
9226
+ if (!item || typeof item !== "object") {
9227
+ return String(item);
9228
+ }
9229
+ const record = item;
9230
+ if (record.type === "text" && typeof record.text === "string") {
9231
+ return record.text;
9232
+ }
9233
+ if (record.type === "resource" && typeof record.uri === "string") {
9234
+ return record.uri;
9235
+ }
9236
+ return JSON.stringify(record);
9237
+ });
9238
+ return parts.join("\n");
9239
+ }
9240
+ function toOpenAiTool(serverIndex, tool) {
9241
+ return {
9242
+ type: "function",
9243
+ function: {
9244
+ name: encodeHubMcpToolName(serverIndex, tool.name),
9245
+ description: tool.description ?? `MCP tool ${tool.name}`,
9246
+ parameters: tool.inputSchema ?? {
9247
+ type: "object",
9248
+ properties: {},
9249
+ additionalProperties: true
9250
+ }
9251
+ }
9252
+ };
9253
+ }
9254
+ async function connectHubMcpServer(serverIndex, server) {
9255
+ const headers = buildRequestHeaders(server.headers);
9256
+ const url = new URL(server.url);
9257
+ const client = new Client(
9258
+ {
9259
+ name: "team-hub",
9260
+ version: "1.0.0"
9261
+ },
9262
+ {}
9263
+ );
9264
+ let transport;
9265
+ try {
9266
+ transport = new StreamableHTTPClientTransport(url, {
9267
+ requestInit: { headers }
9268
+ });
9269
+ await client.connect(transport);
9270
+ } catch {
9271
+ transport = new SSEClientTransport(url, {
9272
+ requestInit: { headers }
9273
+ });
9274
+ await client.connect(transport);
9275
+ }
9276
+ const { tools } = await client.listTools();
9277
+ return {
9278
+ serverIndex,
9279
+ client,
9280
+ remoteTools: tools
9281
+ };
9282
+ }
9283
+ async function ensureHubMcpConnections(config) {
9284
+ const servers = config.mcp ?? [];
9285
+ const signature = buildMcpConfigSignature(config);
9286
+ if (signature !== activeConfigSignature) {
9287
+ await disposeHubMcpConnections();
9288
+ activeConfigSignature = signature;
9289
+ }
9290
+ if (servers.length === 0) {
9291
+ return;
9292
+ }
9293
+ for (const [index, server] of servers.entries()) {
9294
+ if (connectedClients.has(index)) {
9295
+ continue;
9296
+ }
9297
+ try {
9298
+ const connected = await connectHubMcpServer(index, server);
9299
+ connectedClients.set(index, connected);
9300
+ } catch (error) {
9301
+ const message = error instanceof Error ? error.message : "Connection failed";
9302
+ console.warn(`Hub MCP server "${server.name}" failed to connect: ${message}`);
9303
+ }
9304
+ }
9305
+ }
9306
+ function listHubMcpTools() {
9307
+ const tools = [];
9308
+ for (const entry of connectedClients.values()) {
9309
+ for (const tool of entry.remoteTools) {
9310
+ tools.push(toOpenAiTool(entry.serverIndex, tool));
9311
+ }
9312
+ }
9313
+ return tools;
9314
+ }
9315
+ async function callHubMcpTool(prefixedName, args) {
9316
+ const decoded = decodeHubMcpToolName(prefixedName);
9317
+ if (!decoded) {
9318
+ return JSON.stringify({ error: `Unknown hub MCP tool: ${prefixedName}` });
9319
+ }
9320
+ const entry = connectedClients.get(decoded.serverIndex);
9321
+ if (!entry) {
9322
+ return JSON.stringify({ error: `Hub MCP server ${decoded.serverIndex} is not connected.` });
9323
+ }
9324
+ try {
9325
+ const result = await entry.client.callTool({
9326
+ name: decoded.toolName,
9327
+ arguments: args ?? {}
9328
+ });
9329
+ if (result.isError) {
9330
+ return JSON.stringify({
9331
+ error: flattenMcpToolContent(result.content)
9332
+ });
9333
+ }
9334
+ return flattenMcpToolContent(result.content);
9335
+ } catch (error) {
9336
+ const message = error instanceof Error ? error.message : "MCP tool execution failed.";
9337
+ return JSON.stringify({ error: message });
9338
+ }
9339
+ }
9340
+ async function disposeHubMcpConnections() {
9341
+ const closePromises = [...connectedClients.values()].map(async (entry) => {
9342
+ try {
9343
+ await entry.client.close();
9344
+ } catch {
9345
+ }
9346
+ });
9347
+ connectedClients.clear();
9348
+ activeConfigSignature = "";
9349
+ await Promise.allSettled(closePromises);
9350
+ }
9351
+
9352
+ // src/server/llm/agent.ts
9353
+ var HUB_CHAT_STEP_MAX_ITERATIONS = 8;
9354
+ function addUsage(current, next) {
9355
+ return {
9356
+ promptTokens: current.promptTokens + next.promptTokens,
9357
+ completionTokens: current.completionTokens + next.completionTokens,
9358
+ totalTokens: current.totalTokens + next.totalTokens
9359
+ };
9360
+ }
9361
+ function parseToolArguments(raw) {
9362
+ try {
9363
+ return JSON.parse(raw);
9364
+ } catch {
9365
+ return {};
9366
+ }
9367
+ }
9368
+ async function runHubChatStep(config, input, deps = {}) {
9369
+ const runCompletion = deps.runCompletion ?? runLlmCompletion;
9370
+ const ensureConnections = deps.ensureConnections ?? ensureHubMcpConnections;
9371
+ const listTools = deps.listTools ?? listHubMcpTools;
9372
+ const callTool = deps.callTool ?? callHubMcpTool;
9373
+ await ensureConnections(config);
9374
+ const hubTools = listTools();
9375
+ const mergedTools = hubTools.length > 0 || (input.tools?.length ?? 0) > 0 ? [...hubTools, ...input.tools ?? []] : void 0;
9376
+ let messages = [...input.messages];
9377
+ let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
9378
+ let lastContent = null;
9379
+ for (let iteration = 0; iteration < HUB_CHAT_STEP_MAX_ITERATIONS; iteration += 1) {
9380
+ const result = await runCompletion(config, {
9381
+ model: input.model,
9382
+ messages,
9383
+ systemPrompt: input.systemPrompt,
9384
+ tools: mergedTools
9385
+ });
9386
+ usage = addUsage(usage, result.usage);
9387
+ lastContent = result.content;
9388
+ const toolCalls = result.toolCalls ?? [];
9389
+ if (toolCalls.length === 0) {
9390
+ return {
9391
+ content: result.content,
9392
+ usage
9393
+ };
9394
+ }
9395
+ const hubCalls = toolCalls.filter((call) => isHubMcpToolName(call.name));
9396
+ const passthroughCalls = toolCalls.filter((call) => !isHubMcpToolName(call.name));
9397
+ if (passthroughCalls.length > 0) {
9398
+ return {
9399
+ content: result.content,
9400
+ toolCalls: passthroughCalls,
9401
+ usage
9402
+ };
9403
+ }
9404
+ messages = [
9405
+ ...messages,
9406
+ {
9407
+ role: "assistant",
9408
+ content: result.content,
9409
+ tool_calls: hubCalls
9410
+ }
9411
+ ];
9412
+ for (const call of hubCalls) {
9413
+ const toolResult = await callTool(call.name, parseToolArguments(call.arguments));
9414
+ messages.push({
9415
+ role: "tool",
9416
+ tool_call_id: call.id,
9417
+ content: toolResult
9418
+ });
9419
+ }
9420
+ }
9421
+ return {
9422
+ content: lastContent,
9423
+ usage
9424
+ };
9425
+ }
9426
+
8219
9427
  // src/server/routes/llm.ts
8220
9428
  function sendMonthlyLimitExceeded(reply) {
8221
9429
  return reply.code(402).send({
@@ -8333,7 +9541,7 @@ async function registerLlmRoutes(app, options) {
8333
9541
  if (isNewTurn && isOverMonthlyLimit(totalTokens, user.llmMonthlyTokenLimit)) {
8334
9542
  return sendMonthlyLimitExceeded(reply);
8335
9543
  }
8336
- const result = await runLlmCompletion(llm, {
9544
+ const result = await runHubChatStep(llm, {
8337
9545
  model,
8338
9546
  messages,
8339
9547
  tools,
@@ -8481,6 +9689,7 @@ async function registerProtectedRoutes(app, options) {
8481
9689
  });
8482
9690
  await registerCollectionRoutes(app, options.db);
8483
9691
  await registerEnvironmentRoutes(app, options.db);
9692
+ await registerSnippetRoutes(app, options.db);
8484
9693
  await registerFolderRoutes(app, options.db);
8485
9694
  await registerRequestRoutes(app, options.db);
8486
9695
  await registerLlmRoutes(app, { db: options.db, getLlm: options.getLlm });
@@ -8825,6 +10034,7 @@ function registerGracefulShutdown(app, ctx) {
8825
10034
  const shutdown = async (signal) => {
8826
10035
  app.log.info(`Received ${signal}, shutting down.`);
8827
10036
  await app.close();
10037
+ await disposeHubMcpConnections();
8828
10038
  await disconnectAll(ctx);
8829
10039
  process.exit(0);
8830
10040
  };