@harborclient/team-hub 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -21,9 +21,22 @@ import { Command as Command2 } from "commander";
21
21
 
22
22
  // src/config/serverConfig.ts
23
23
  import { existsSync, readFileSync } from "fs";
24
- import path from "path";
24
+ import path2 from "path";
25
25
  import { parse as parseYaml } from "yaml";
26
26
 
27
+ // src/config/docsConfig.ts
28
+ import path from "path";
29
+ var DEFAULT_DOCS_SEARCH_INDEX_PATH = "/app/data/docsSearchIndex.json";
30
+ function resolveDocsSearchIndexPath(searchIndexPath) {
31
+ return path.isAbsolute(searchIndexPath) ? searchIndexPath : path.resolve(process.cwd(), searchIndexPath);
32
+ }
33
+ function normalizeDocsConfig(section) {
34
+ const trimmed = section.searchIndexPath?.trim();
35
+ return {
36
+ searchIndexPath: trimmed && trimmed.length > 0 ? trimmed : DEFAULT_DOCS_SEARCH_INDEX_PATH
37
+ };
38
+ }
39
+
27
40
  // src/config/llmConfig.ts
28
41
  function headerRowsFromSingleKeyObject(record) {
29
42
  const rows = [];
@@ -176,6 +189,9 @@ var pluginsSectionSchema = z.object({
176
189
  catalogs: z.array(z.string().trim().url()).optional(),
177
190
  trusted: z.array(z.string().trim().url()).optional()
178
191
  });
192
+ var docsSectionSchema = z.object({
193
+ searchIndexPath: z.string().trim().min(1).optional()
194
+ });
179
195
  var logLevelSchema = z.enum(["debug", "info", "warn", "error"]);
180
196
  var loggingSectionSchema = z.object({
181
197
  level: logLevelSchema.optional(),
@@ -188,6 +204,7 @@ var serverConfigDocumentSchema = z.object({
188
204
  redis: redisSectionSchema,
189
205
  llm: llmSectionSchema.optional(),
190
206
  plugins: pluginsSectionSchema.optional(),
207
+ docs: docsSectionSchema.optional(),
191
208
  logging: loggingSectionSchema.optional()
192
209
  });
193
210
 
@@ -205,7 +222,7 @@ var ConfigError = class extends Error {
205
222
  }
206
223
  };
207
224
  function resolveConfigPath(configPath) {
208
- return path.isAbsolute(configPath) ? configPath : path.resolve(process.cwd(), configPath);
225
+ return path2.isAbsolute(configPath) ? configPath : path2.resolve(process.cwd(), configPath);
209
226
  }
210
227
  function assertDocumentShape(document) {
211
228
  if (document === null || typeof document !== "object" || Array.isArray(document)) {
@@ -288,6 +305,14 @@ function parseServerConfig(document) {
288
305
  }
289
306
  plugins = normalizePluginsConfig(parsedPluginsSection.data);
290
307
  }
308
+ let docs = null;
309
+ if (root.docs !== void 0) {
310
+ const parsedDocsSection = docsSectionSchema.safeParse(root.docs);
311
+ if (!parsedDocsSection.success) {
312
+ throw new ConfigError(formatZodError(parsedDocsSection.error));
313
+ }
314
+ docs = normalizeDocsConfig(parsedDocsSection.data);
315
+ }
291
316
  let logging = DEFAULT_LOGGING_CONFIG;
292
317
  if (root.logging !== void 0) {
293
318
  const parsedLoggingSection = loggingSectionSchema.safeParse(root.logging);
@@ -303,6 +328,7 @@ function parseServerConfig(document) {
303
328
  redis: parsedRedisSection.data,
304
329
  llm,
305
330
  plugins,
331
+ docs,
306
332
  logging
307
333
  };
308
334
  }
@@ -364,6 +390,7 @@ var REQUESTS_COLLECTION = "requests";
364
390
  var AUDIT_LOG_COLLECTION = "auditLog";
365
391
  var LLM_USAGE_COLLECTION = "llmUsage";
366
392
  var LLM_USAGE_LOG_COLLECTION = "llmUsageLog";
393
+ var RUN_RESULTS_COLLECTION = "runResults";
367
394
  var WRITE_BATCH_LIMIT = 500;
368
395
 
369
396
  // src/db/systemUsers.ts
@@ -400,7 +427,7 @@ var firestoreConfigSchema = z2.object({
400
427
 
401
428
  // src/db/firestore/utils.ts
402
429
  function parseAuditEntityType(value) {
403
- if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request") {
430
+ if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request" || value === "run_result") {
404
431
  return value;
405
432
  }
406
433
  throw new Error(`Invalid audit entity type: ${value}`);
@@ -544,6 +571,19 @@ function mapFirestoreRequest(id, data) {
544
571
  updatedByUserId: data.updatedByUserId ?? null
545
572
  };
546
573
  }
574
+ function mapFirestoreRunResult(id, data) {
575
+ return {
576
+ id,
577
+ kind: data.kind,
578
+ label: data.label,
579
+ collectionName: data.collectionName,
580
+ requestName: data.requestName,
581
+ summary: data.summary,
582
+ payload: data.payload,
583
+ createdAt: data.createdAt,
584
+ createdByUserId: data.createdByUserId ?? null
585
+ };
586
+ }
547
587
  function mapFirestoreAuditLog(id, data) {
548
588
  return {
549
589
  id,
@@ -557,6 +597,53 @@ function mapFirestoreAuditLog(id, data) {
557
597
  };
558
598
  }
559
599
 
600
+ // src/db/runResultPayload.ts
601
+ function isRecord(value) {
602
+ return value != null && typeof value === "object" && !Array.isArray(value);
603
+ }
604
+ function summarizeResultRows(results) {
605
+ let passed = 0;
606
+ let failed = 0;
607
+ let skipped = 0;
608
+ for (const row of results) {
609
+ if (!isRecord(row)) {
610
+ continue;
611
+ }
612
+ const status = row.status;
613
+ if (status === "passed") {
614
+ passed += 1;
615
+ } else if (status === "failed") {
616
+ failed += 1;
617
+ } else if (status === "skipped") {
618
+ skipped += 1;
619
+ }
620
+ }
621
+ return { passed, failed, skipped };
622
+ }
623
+ function buildDefaultRunResultLabel(metadata) {
624
+ const target = metadata.requestName ?? metadata.collectionName ?? "Run";
625
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
626
+ return `${target} \u2014 ${timestamp}`;
627
+ }
628
+ function parseRunResultPayload(payload) {
629
+ const kind = payload.harborclientExport;
630
+ if (kind !== "collection-run-results" && kind !== "request-run-results") {
631
+ throw new Error("Invalid run result payload: harborclientExport is required");
632
+ }
633
+ const results = payload.results;
634
+ if (!Array.isArray(results) || results.length === 0) {
635
+ throw new Error("Invalid run result payload: results are required");
636
+ }
637
+ const collection = isRecord(payload.collection) ? payload.collection : void 0;
638
+ const request = isRecord(payload.request) ? payload.request : void 0;
639
+ return {
640
+ kind,
641
+ collectionName: typeof collection?.name === "string" ? collection.name : null,
642
+ requestName: typeof request?.name === "string" ? request.name : null,
643
+ summary: summarizeResultRows(results)
644
+ };
645
+ }
646
+
560
647
  // src/db/trimRequiredName.ts
561
648
  function trimRequiredName(name, label) {
562
649
  const trimmed = name.trim();
@@ -1822,6 +1909,78 @@ var FirestoreDatabase = class _FirestoreDatabase {
1822
1909
  }
1823
1910
  return usage;
1824
1911
  }
1912
+ /**
1913
+ * Lists run results saved by the given user, newest first.
1914
+ *
1915
+ * @param userId - Owning user identifier.
1916
+ */
1917
+ async listRunResultsForUser(userId) {
1918
+ const snapshot = await this.requireClient().collection(RUN_RESULTS_COLLECTION).where("createdByUserId", "==", userId).orderBy("createdAt", "desc").get();
1919
+ return snapshot.docs.map(
1920
+ (doc) => mapFirestoreRunResult(doc.id, doc.data())
1921
+ );
1922
+ }
1923
+ /**
1924
+ * Lists all run results for admin inspection, newest first.
1925
+ */
1926
+ async listAllRunResults() {
1927
+ const snapshot = await this.requireClient().collection(RUN_RESULTS_COLLECTION).orderBy("createdAt", "desc").get();
1928
+ return snapshot.docs.map(
1929
+ (doc) => mapFirestoreRunResult(doc.id, doc.data())
1930
+ );
1931
+ }
1932
+ /**
1933
+ * Creates a standalone run result snapshot.
1934
+ *
1935
+ * @param input - HarborClient export payload and optional label.
1936
+ * @param actingUserId - User performing the create action.
1937
+ */
1938
+ async createRunResult(input, actingUserId) {
1939
+ const metadata = parseRunResultPayload(input.payload);
1940
+ const label = input.label?.trim() || buildDefaultRunResultLabel(metadata);
1941
+ const id = randomUUID2();
1942
+ const now = /* @__PURE__ */ new Date();
1943
+ const data = {
1944
+ kind: metadata.kind,
1945
+ label,
1946
+ collectionName: metadata.collectionName,
1947
+ requestName: metadata.requestName,
1948
+ summary: metadata.summary,
1949
+ payload: input.payload,
1950
+ createdAt: now,
1951
+ createdByUserId: actingUserId
1952
+ };
1953
+ await this.requireClient().collection(RUN_RESULTS_COLLECTION).doc(id).set(data);
1954
+ await this.recordAuditEntry(actingUserId, "create", "run_result", id);
1955
+ return mapFirestoreRunResult(id, data);
1956
+ }
1957
+ /**
1958
+ * Finds a run result by stable identifier.
1959
+ *
1960
+ * @param id - Run result ID to look up.
1961
+ */
1962
+ async findRunResultById(id) {
1963
+ const snapshot = await this.requireClient().collection(RUN_RESULTS_COLLECTION).doc(id).get();
1964
+ if (!snapshot.exists) {
1965
+ return null;
1966
+ }
1967
+ return mapFirestoreRunResult(id, snapshot.data());
1968
+ }
1969
+ /**
1970
+ * Deletes a run result.
1971
+ *
1972
+ * @param id - Run result ID to delete.
1973
+ * @param actingUserId - User performing the delete action.
1974
+ */
1975
+ async deleteRunResult(id, actingUserId) {
1976
+ const docRef = this.requireClient().collection(RUN_RESULTS_COLLECTION).doc(id);
1977
+ const snapshot = await docRef.get();
1978
+ if (!snapshot.exists) {
1979
+ throw new Error("Run result not found");
1980
+ }
1981
+ await this.recordAuditEntry(actingUserId, "delete", "run_result", id);
1982
+ await docRef.delete();
1983
+ }
1825
1984
  /**
1826
1985
  * Inserts a per-request LLM usage log entry.
1827
1986
  *
@@ -1976,7 +2135,7 @@ function parseAuditAction(value) {
1976
2135
  throw new Error(`Invalid audit action: ${value}`);
1977
2136
  }
1978
2137
  function parseAuditEntityType2(value) {
1979
- if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request") {
2138
+ if (value === "user" || value === "api_token" || value === "collection" || value === "environment" || value === "snippet" || value === "folder" || value === "request" || value === "run_result") {
1980
2139
  return value;
1981
2140
  }
1982
2141
  throw new Error(`Invalid audit entity type: ${value}`);
@@ -2073,6 +2232,23 @@ function mapSnippetSqlRow(row) {
2073
2232
  deletionLocked: Boolean(row.deletion_locked)
2074
2233
  };
2075
2234
  }
2235
+ function mapRunResultSqlRow(row) {
2236
+ return {
2237
+ id: row.id,
2238
+ kind: row.kind,
2239
+ label: row.label,
2240
+ collectionName: row.collection_name,
2241
+ requestName: row.request_name,
2242
+ summary: {
2243
+ passed: row.summary_passed,
2244
+ failed: row.summary_failed,
2245
+ skipped: row.summary_skipped
2246
+ },
2247
+ payload: parseJson(row.payload, {}),
2248
+ createdAt: row.created_at,
2249
+ createdByUserId: row.created_by_user_id ?? null
2250
+ };
2251
+ }
2076
2252
  function mapFolderSqlRow(row) {
2077
2253
  return {
2078
2254
  id: row.id,
@@ -2349,6 +2525,22 @@ WHERE role = 'user'
2349
2525
  AND snippet_access = '[]'
2350
2526
  AND collection_access LIKE '%"*"%';
2351
2527
  `.trim();
2528
+ var RUN_RESULTS_MIGRATION_SQL = `
2529
+ CREATE TABLE IF NOT EXISTS run_results (
2530
+ id VARCHAR(36) PRIMARY KEY,
2531
+ kind VARCHAR(64) NOT NULL,
2532
+ label VARCHAR(512) NOT NULL,
2533
+ collection_name VARCHAR(512),
2534
+ request_name VARCHAR(512),
2535
+ summary_passed INT NOT NULL DEFAULT 0,
2536
+ summary_failed INT NOT NULL DEFAULT 0,
2537
+ summary_skipped INT NOT NULL DEFAULT 0,
2538
+ payload LONGTEXT NOT NULL,
2539
+ created_at DATETIME NOT NULL,
2540
+ created_by_user_id VARCHAR(36),
2541
+ INDEX run_results_created_idx (created_at)
2542
+ )
2543
+ `.trim();
2352
2544
  var MYSQL_MIGRATIONS = [
2353
2545
  USERS_MIGRATION_SQL,
2354
2546
  API_TOKENS_MIGRATION_SQL,
@@ -2374,7 +2566,8 @@ var MYSQL_MIGRATIONS = [
2374
2566
  COLLECTIONS_DELETION_LOCKED_MIGRATION_SQL,
2375
2567
  ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL,
2376
2568
  USERS_SNIPPET_ACCESS_MIGRATION_SQL,
2377
- USERS_SNIPPET_ACCESS_BACKFILL_SQL
2569
+ USERS_SNIPPET_ACCESS_BACKFILL_SQL,
2570
+ RUN_RESULTS_MIGRATION_SQL
2378
2571
  ];
2379
2572
 
2380
2573
  // src/db/mysql/schemas.ts
@@ -2478,6 +2671,8 @@ var LLM_USAGE_SELECT_COLUMNS = `id, user_id, period, prompt_tokens, completion_t
2478
2671
  var COLLECTION_SELECT = `SELECT ${COLLECTION_SELECT_COLUMNS} FROM collections`;
2479
2672
  var ENVIRONMENT_SELECT = `SELECT ${ENVIRONMENT_SELECT_COLUMNS} FROM environments`;
2480
2673
  var SNIPPET_SELECT = `SELECT ${SNIPPET_SELECT_COLUMNS} FROM snippets`;
2674
+ var RUN_RESULT_SELECT_COLUMNS = "id, kind, label, collection_name, request_name, summary_passed, summary_failed, summary_skipped, payload, created_at, created_by_user_id";
2675
+ var RUN_RESULT_SELECT = `SELECT ${RUN_RESULT_SELECT_COLUMNS} FROM run_results`;
2481
2676
  var USER_SELECT = `SELECT ${USER_SELECT_COLUMNS} FROM users`;
2482
2677
  var API_TOKEN_SELECT = `SELECT ${API_TOKEN_SELECT_COLUMNS} FROM api_tokens`;
2483
2678
  var FOLDER_SELECT = `SELECT ${FOLDER_SELECT_COLUMNS} FROM folders`;
@@ -3883,6 +4078,93 @@ var MysqlDatabase = class _MysqlDatabase {
3883
4078
  );
3884
4079
  return rows.map(mapLlmUsageLogSqlRow);
3885
4080
  }
4081
+ /**
4082
+ * Lists run results saved by the given user, newest first.
4083
+ */
4084
+ async listRunResultsForUser(userId) {
4085
+ const rows = await this.queryRows(
4086
+ `${RUN_RESULT_SELECT} WHERE created_by_user_id = ? ORDER BY created_at DESC`,
4087
+ [userId]
4088
+ );
4089
+ return rows.map(mapRunResultSqlRow);
4090
+ }
4091
+ /**
4092
+ * Lists all run results for admin inspection, newest first.
4093
+ */
4094
+ async listAllRunResults() {
4095
+ const rows = await this.queryRows(
4096
+ `${RUN_RESULT_SELECT} ORDER BY created_at DESC`
4097
+ );
4098
+ return rows.map(mapRunResultSqlRow);
4099
+ }
4100
+ /**
4101
+ * Creates a standalone run result snapshot.
4102
+ */
4103
+ async createRunResult(input, actingUserId) {
4104
+ const metadata = parseRunResultPayload(input.payload);
4105
+ const label = input.label?.trim() || buildDefaultRunResultLabel(metadata);
4106
+ const id = randomUUID3();
4107
+ const now = /* @__PURE__ */ new Date();
4108
+ await this.executeStatement(
4109
+ `INSERT INTO run_results (
4110
+ id,
4111
+ kind,
4112
+ label,
4113
+ collection_name,
4114
+ request_name,
4115
+ summary_passed,
4116
+ summary_failed,
4117
+ summary_skipped,
4118
+ payload,
4119
+ created_at,
4120
+ created_by_user_id
4121
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4122
+ [
4123
+ id,
4124
+ metadata.kind,
4125
+ label,
4126
+ metadata.collectionName,
4127
+ metadata.requestName,
4128
+ metadata.summary.passed,
4129
+ metadata.summary.failed,
4130
+ metadata.summary.skipped,
4131
+ JSON.stringify(input.payload),
4132
+ now,
4133
+ actingUserId
4134
+ ]
4135
+ );
4136
+ const rows = await this.queryRows(
4137
+ `${RUN_RESULT_SELECT} WHERE id = ?`,
4138
+ [id]
4139
+ );
4140
+ const row = rows[0];
4141
+ if (!row) {
4142
+ throw new Error("Run result not found after insert");
4143
+ }
4144
+ await this.recordAuditEntry(actingUserId, "create", "run_result", id);
4145
+ return mapRunResultSqlRow(row);
4146
+ }
4147
+ /**
4148
+ * Finds a run result by id.
4149
+ */
4150
+ async findRunResultById(id) {
4151
+ const rows = await this.queryRows(
4152
+ `${RUN_RESULT_SELECT} WHERE id = ?`,
4153
+ [id]
4154
+ );
4155
+ const row = rows[0];
4156
+ return row ? mapRunResultSqlRow(row) : null;
4157
+ }
4158
+ /**
4159
+ * Deletes a run result by id.
4160
+ */
4161
+ async deleteRunResult(id, actingUserId) {
4162
+ const result = await this.executeStatement("DELETE FROM run_results WHERE id = ?", [id]);
4163
+ if (result.affectedRows === 0) {
4164
+ throw new Error("Run result not found");
4165
+ }
4166
+ await this.recordAuditEntry(actingUserId, "delete", "run_result", id);
4167
+ }
3886
4168
  /**
3887
4169
  * Ensures the internal system user exists and caches its identifier.
3888
4170
  */
@@ -4229,6 +4511,22 @@ WHERE role = 'user'
4229
4511
  AND snippet_access = '[]'
4230
4512
  AND collection_access LIKE '%"*"%';
4231
4513
  `.trim();
4514
+ var RUN_RESULTS_MIGRATION_SQL2 = `
4515
+ CREATE TABLE IF NOT EXISTS run_results (
4516
+ id TEXT PRIMARY KEY,
4517
+ kind TEXT NOT NULL CHECK (kind IN ('collection-run-results', 'request-run-results')),
4518
+ label TEXT NOT NULL,
4519
+ collection_name TEXT,
4520
+ request_name TEXT,
4521
+ summary_passed INT NOT NULL DEFAULT 0,
4522
+ summary_failed INT NOT NULL DEFAULT 0,
4523
+ summary_skipped INT NOT NULL DEFAULT 0,
4524
+ payload TEXT NOT NULL,
4525
+ created_at TIMESTAMPTZ NOT NULL,
4526
+ created_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL
4527
+ );
4528
+ CREATE INDEX IF NOT EXISTS run_results_created_idx ON run_results (created_at DESC);
4529
+ `.trim();
4232
4530
  var POSTGRES_MIGRATIONS = [
4233
4531
  USERS_MIGRATION_SQL2,
4234
4532
  API_TOKENS_MIGRATION_SQL2,
@@ -4254,7 +4552,8 @@ var POSTGRES_MIGRATIONS = [
4254
4552
  COLLECTIONS_DELETION_LOCKED_MIGRATION_SQL2,
4255
4553
  ENVIRONMENTS_DELETION_LOCKED_MIGRATION_SQL2,
4256
4554
  USERS_SNIPPET_ACCESS_MIGRATION_SQL2,
4257
- USERS_SNIPPET_ACCESS_BACKFILL_SQL2
4555
+ USERS_SNIPPET_ACCESS_BACKFILL_SQL2,
4556
+ RUN_RESULTS_MIGRATION_SQL2
4258
4557
  ];
4259
4558
 
4260
4559
  // src/db/postgres/schemas.ts
@@ -4279,6 +4578,8 @@ var { Pool } = pg;
4279
4578
  var COLLECTION_SELECT2 = `SELECT ${COLLECTION_SELECT_COLUMNS} FROM collections`;
4280
4579
  var ENVIRONMENT_SELECT2 = `SELECT ${ENVIRONMENT_SELECT_COLUMNS} FROM environments`;
4281
4580
  var SNIPPET_SELECT2 = `SELECT ${SNIPPET_SELECT_COLUMNS} FROM snippets`;
4581
+ var RUN_RESULT_SELECT_COLUMNS2 = "id, kind, label, collection_name, request_name, summary_passed, summary_failed, summary_skipped, payload, created_at, created_by_user_id";
4582
+ var RUN_RESULT_SELECT2 = `SELECT ${RUN_RESULT_SELECT_COLUMNS2} FROM run_results`;
4282
4583
  var USER_SELECT2 = `SELECT ${USER_SELECT_COLUMNS} FROM users`;
4283
4584
  var API_TOKEN_SELECT2 = `SELECT ${API_TOKEN_SELECT_COLUMNS} FROM api_tokens`;
4284
4585
  var FOLDER_SELECT2 = `SELECT ${FOLDER_SELECT_COLUMNS} FROM folders`;
@@ -5629,6 +5930,87 @@ var PostgresDatabase = class _PostgresDatabase {
5629
5930
  );
5630
5931
  return result.rows.map(mapLlmUsageLogSqlRow);
5631
5932
  }
5933
+ /**
5934
+ * Lists run results saved by the given user, newest first.
5935
+ */
5936
+ async listRunResultsForUser(userId) {
5937
+ const result = await this.query(
5938
+ `${RUN_RESULT_SELECT2} WHERE created_by_user_id = $1 ORDER BY created_at DESC`,
5939
+ [userId]
5940
+ );
5941
+ return result.rows.map(mapRunResultSqlRow);
5942
+ }
5943
+ /**
5944
+ * Lists all run results for admin inspection, newest first.
5945
+ */
5946
+ async listAllRunResults() {
5947
+ const result = await this.query(
5948
+ `${RUN_RESULT_SELECT2} ORDER BY created_at DESC`
5949
+ );
5950
+ return result.rows.map(mapRunResultSqlRow);
5951
+ }
5952
+ /**
5953
+ * Creates a standalone run result snapshot.
5954
+ */
5955
+ async createRunResult(input, actingUserId) {
5956
+ const metadata = parseRunResultPayload(input.payload);
5957
+ const label = input.label?.trim() || buildDefaultRunResultLabel(metadata);
5958
+ const id = randomUUID4();
5959
+ const now = /* @__PURE__ */ new Date();
5960
+ const result = await this.query(
5961
+ `INSERT INTO run_results (
5962
+ id,
5963
+ kind,
5964
+ label,
5965
+ collection_name,
5966
+ request_name,
5967
+ summary_passed,
5968
+ summary_failed,
5969
+ summary_skipped,
5970
+ payload,
5971
+ created_at,
5972
+ created_by_user_id
5973
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
5974
+ RETURNING ${RUN_RESULT_SELECT_COLUMNS2}`,
5975
+ [
5976
+ id,
5977
+ metadata.kind,
5978
+ label,
5979
+ metadata.collectionName,
5980
+ metadata.requestName,
5981
+ metadata.summary.passed,
5982
+ metadata.summary.failed,
5983
+ metadata.summary.skipped,
5984
+ JSON.stringify(input.payload),
5985
+ now,
5986
+ actingUserId
5987
+ ]
5988
+ );
5989
+ const row = result.rows[0];
5990
+ if (!row) {
5991
+ throw new Error("Run result not found after insert");
5992
+ }
5993
+ await this.recordAuditEntry(actingUserId, "create", "run_result", id);
5994
+ return mapRunResultSqlRow(row);
5995
+ }
5996
+ /**
5997
+ * Finds a run result by id.
5998
+ */
5999
+ async findRunResultById(id) {
6000
+ const result = await this.query(`${RUN_RESULT_SELECT2} WHERE id = $1`, [id]);
6001
+ const row = result.rows[0];
6002
+ return row ? mapRunResultSqlRow(row) : null;
6003
+ }
6004
+ /**
6005
+ * Deletes a run result by id.
6006
+ */
6007
+ async deleteRunResult(id, actingUserId) {
6008
+ const result = await this.query("DELETE FROM run_results WHERE id = $1", [id]);
6009
+ if ((result.rowCount ?? 0) === 0) {
6010
+ throw new Error("Run result not found");
6011
+ }
6012
+ await this.recordAuditEntry(actingUserId, "delete", "run_result", id);
6013
+ }
5632
6014
  /**
5633
6015
  * Returns the active pool or throws when connect has not been called.
5634
6016
  *
@@ -6114,6 +6496,14 @@ var LLM_MODEL_CATALOG = [
6114
6496
  function hasProviderKey(config, provider) {
6115
6497
  return Boolean(config.providers[provider]?.apiKey.trim());
6116
6498
  }
6499
+ function hasHubOpenAiProvider(config) {
6500
+ return hasProviderKey(config, "openai");
6501
+ }
6502
+ function getHubLlmCapabilities(config) {
6503
+ return {
6504
+ openai: hasProviderKey(config, "openai")
6505
+ };
6506
+ }
6117
6507
  function listHubOfferedModels(config) {
6118
6508
  const allowList = config.models ? new Set(config.models) : null;
6119
6509
  return LLM_MODEL_CATALOG.filter((model) => {
@@ -6655,6 +7045,9 @@ function canListEnvironments(user) {
6655
7045
  function canListSnippets(user) {
6656
7046
  return canUseDataApi(user) || canUseManagementApi(user);
6657
7047
  }
7048
+ function canListRunResults(user) {
7049
+ return canUseDataApi(user) || canUseManagementApi(user);
7050
+ }
6658
7051
  function hasWildcardAccess(access) {
6659
7052
  return access.includes("*");
6660
7053
  }
@@ -6691,6 +7084,9 @@ function canDeleteCollection(user, collection) {
6691
7084
  function canDeleteRequest(user, request) {
6692
7085
  return canUseDataApi(user) && canAccessCollection(user, request.collectionId) && request.createdByUserId === user.id;
6693
7086
  }
7087
+ function canDeleteRunResult(user, runResult) {
7088
+ return canUseDataApi(user) && runResult.createdByUserId === user.id;
7089
+ }
6694
7090
  function canCreateCollection(user) {
6695
7091
  return user.role === "user" && hasWildcardAccess(user.collectionAccess);
6696
7092
  }
@@ -6923,8 +7319,12 @@ var llmModelSchema = z7.object({
6923
7319
  label: z7.string(),
6924
7320
  provider: z7.enum(["openai", "claude", "gemini"])
6925
7321
  });
7322
+ var llmCapabilitiesSchema = z7.object({
7323
+ openai: z7.boolean()
7324
+ });
6926
7325
  var listLlmModelsResponseSchema = z7.object({
6927
- models: z7.array(llmModelSchema)
7326
+ models: z7.array(llmModelSchema),
7327
+ capabilities: llmCapabilitiesSchema
6928
7328
  });
6929
7329
  var llmUsageSummaryResponseSchema = z7.object({
6930
7330
  period: z7.string(),
@@ -7115,6 +7515,48 @@ function serializeSavedRequest(record) {
7115
7515
  updatedAt: record.updatedAt.toISOString()
7116
7516
  };
7117
7517
  }
7518
+ var runResultRecordSchema = z8.object({
7519
+ id: z8.string(),
7520
+ kind: z8.enum(["collection-run-results", "request-run-results"]),
7521
+ label: z8.string(),
7522
+ collectionName: z8.string().nullable(),
7523
+ requestName: z8.string().nullable(),
7524
+ summary: z8.object({
7525
+ passed: z8.number().int().nonnegative(),
7526
+ failed: z8.number().int().nonnegative(),
7527
+ skipped: z8.number().int().nonnegative()
7528
+ }),
7529
+ createdAt: timestampSchema,
7530
+ createdByUserId: z8.string().nullable()
7531
+ });
7532
+ var runResultDetailSchema = runResultRecordSchema.extend({
7533
+ payload: z8.record(z8.string(), z8.unknown())
7534
+ });
7535
+ var createRunResultBodySchema = z8.object({
7536
+ label: z8.string().trim().min(1).optional(),
7537
+ payload: z8.record(z8.string(), z8.unknown())
7538
+ });
7539
+ var listRunResultsResponseSchema = z8.object({
7540
+ runResults: z8.array(runResultRecordSchema)
7541
+ });
7542
+ function serializeRunResult(record) {
7543
+ return {
7544
+ id: record.id,
7545
+ kind: record.kind,
7546
+ label: record.label,
7547
+ collectionName: record.collectionName,
7548
+ requestName: record.requestName,
7549
+ summary: record.summary,
7550
+ createdAt: record.createdAt.toISOString(),
7551
+ createdByUserId: record.createdByUserId
7552
+ };
7553
+ }
7554
+ function serializeRunResultDetail(record) {
7555
+ return {
7556
+ ...serializeRunResult(record),
7557
+ payload: record.payload
7558
+ };
7559
+ }
7118
7560
 
7119
7561
  // src/server/routes/schemas/admin.ts
7120
7562
  var adminResourceOptionSchema = z9.object({
@@ -7164,6 +7606,9 @@ var listAdminEnvironmentsResponseSchema = z9.object({
7164
7606
  var listAdminSnippetsResponseSchema = z9.object({
7165
7607
  snippets: z9.array(snippetRecordSchema)
7166
7608
  });
7609
+ var listAdminRunResultsResponseSchema = z9.object({
7610
+ runResults: z9.array(runResultRecordSchema)
7611
+ });
7167
7612
  var adminSnippetRecordSchema = snippetRecordSchema;
7168
7613
  var listAdminLlmModelsResponseSchema = listLlmModelsResponseSchema;
7169
7614
  var hubUserRecordSchema = z9.object({
@@ -7230,7 +7675,7 @@ var listAdminTokensResponseSchema = z9.object({
7230
7675
  tokens: z9.array(hubApiTokenRecordSchema)
7231
7676
  });
7232
7677
  var reloadConfigSectionResultSchema = z9.object({
7233
- section: z9.enum(["db", "redis", "llm", "plugins", "server"]),
7678
+ section: z9.enum(["db", "redis", "llm", "plugins", "docs", "server"]),
7234
7679
  status: z9.enum(["reloaded", "unchanged", "failed", "restart-required"]),
7235
7680
  error: z9.string().optional()
7236
7681
  });
@@ -7722,6 +8167,71 @@ async function registerAdminRoutes(app, options) {
7722
8167
  }
7723
8168
  }
7724
8169
  });
8170
+ routes.route({
8171
+ method: "GET",
8172
+ url: "/admin/run-results",
8173
+ schema: {
8174
+ response: {
8175
+ 200: listAdminRunResultsResponseSchema,
8176
+ 403: errorResponseSchema
8177
+ }
8178
+ },
8179
+ /**
8180
+ * Lists all run results for operator management.
8181
+ */
8182
+ handler: async (request, reply) => {
8183
+ try {
8184
+ const user = requireAuthenticatedUser(request);
8185
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
8186
+ return;
8187
+ }
8188
+ const runResults = await db.listAllRunResults();
8189
+ return reply.send({
8190
+ runResults: runResults.map((record) => serializeRunResult(record))
8191
+ });
8192
+ } catch (error) {
8193
+ if (handleDbError(reply, error)) {
8194
+ return;
8195
+ }
8196
+ throw error;
8197
+ }
8198
+ }
8199
+ });
8200
+ routes.route({
8201
+ method: "DELETE",
8202
+ url: "/admin/run-results/:id",
8203
+ schema: {
8204
+ params: idParamSchema,
8205
+ response: {
8206
+ 204: emptyResponseSchema,
8207
+ 403: errorResponseSchema,
8208
+ 404: errorResponseSchema
8209
+ }
8210
+ },
8211
+ /**
8212
+ * Deletes a run result regardless of creator ownership.
8213
+ */
8214
+ handler: async (request, reply) => {
8215
+ try {
8216
+ const user = requireAuthenticatedUser(request);
8217
+ if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
8218
+ return;
8219
+ }
8220
+ const record = await db.findRunResultById(request.params.id);
8221
+ if (!record) {
8222
+ void reply.code(404).send({ error: "Run result not found" });
8223
+ return;
8224
+ }
8225
+ await db.deleteRunResult(request.params.id, user.id);
8226
+ return reply.code(204).send(null);
8227
+ } catch (error) {
8228
+ if (handleDbError(reply, error)) {
8229
+ return;
8230
+ }
8231
+ throw error;
8232
+ }
8233
+ }
8234
+ });
7725
8235
  routes.route({
7726
8236
  method: "PUT",
7727
8237
  url: "/admin/snippets/:id",
@@ -7905,7 +8415,7 @@ async function registerAdminRoutes(app, options) {
7905
8415
  label: model.label,
7906
8416
  provider: model.provider
7907
8417
  }));
7908
- return reply.send({ models });
8418
+ return reply.send({ models, capabilities: getHubLlmCapabilities(llm) });
7909
8419
  }
7910
8420
  });
7911
8421
  routes.route({
@@ -9104,6 +9614,140 @@ async function registerRequestRoutes(app, db) {
9104
9614
  });
9105
9615
  }
9106
9616
 
9617
+ // src/server/routes/runResults.ts
9618
+ async function registerRunResultRoutes(app, db) {
9619
+ const routes = app.withTypeProvider();
9620
+ routes.route({
9621
+ method: "GET",
9622
+ url: "/run-results",
9623
+ schema: {
9624
+ response: {
9625
+ 200: listRunResultsResponseSchema
9626
+ }
9627
+ },
9628
+ /**
9629
+ * Lists run results saved by the authenticated user token.
9630
+ */
9631
+ handler: async (request, reply) => {
9632
+ try {
9633
+ const user = requireAuthenticatedUser(request);
9634
+ if (denyUnlessAllowed(reply, canListRunResults(user))) {
9635
+ return;
9636
+ }
9637
+ const runResults = await db.listRunResultsForUser(user.id);
9638
+ return reply.send({
9639
+ runResults: runResults.map((record) => serializeRunResult(record))
9640
+ });
9641
+ } catch (error) {
9642
+ if (handleDbError(reply, error)) {
9643
+ return;
9644
+ }
9645
+ throw error;
9646
+ }
9647
+ }
9648
+ });
9649
+ routes.route({
9650
+ method: "POST",
9651
+ url: "/run-results",
9652
+ schema: {
9653
+ body: createRunResultBodySchema,
9654
+ response: {
9655
+ 200: runResultDetailSchema,
9656
+ 400: errorResponseSchema
9657
+ }
9658
+ },
9659
+ /**
9660
+ * Saves a standalone run result snapshot.
9661
+ */
9662
+ handler: async (request, reply) => {
9663
+ try {
9664
+ const user = requireAuthenticatedUser(request);
9665
+ if (denyUnlessAllowed(reply, canUseDataApi(user))) {
9666
+ return;
9667
+ }
9668
+ const record = await db.createRunResult(request.body, user.id);
9669
+ return reply.send(serializeRunResultDetail(record));
9670
+ } catch (error) {
9671
+ if (handleDbError(reply, error)) {
9672
+ return;
9673
+ }
9674
+ throw error;
9675
+ }
9676
+ }
9677
+ });
9678
+ routes.route({
9679
+ method: "GET",
9680
+ url: "/run-results/:id",
9681
+ schema: {
9682
+ params: idParamSchema,
9683
+ response: {
9684
+ 200: runResultDetailSchema,
9685
+ 404: errorResponseSchema
9686
+ }
9687
+ },
9688
+ /**
9689
+ * Loads a run result snapshot by id for shared deep links.
9690
+ */
9691
+ handler: async (request, reply) => {
9692
+ try {
9693
+ const user = requireAuthenticatedUser(request);
9694
+ if (denyUnlessAllowed(reply, canUseDataApi(user))) {
9695
+ return;
9696
+ }
9697
+ const record = await db.findRunResultById(request.params.id);
9698
+ if (!record) {
9699
+ void reply.code(404).send({ error: "Run result not found" });
9700
+ return;
9701
+ }
9702
+ return reply.send(serializeRunResultDetail(record));
9703
+ } catch (error) {
9704
+ if (handleDbError(reply, error)) {
9705
+ return;
9706
+ }
9707
+ throw error;
9708
+ }
9709
+ }
9710
+ });
9711
+ routes.route({
9712
+ method: "DELETE",
9713
+ url: "/run-results/:id",
9714
+ schema: {
9715
+ params: idParamSchema,
9716
+ response: {
9717
+ 204: emptyResponseSchema,
9718
+ 403: errorResponseSchema,
9719
+ 404: errorResponseSchema
9720
+ }
9721
+ },
9722
+ /**
9723
+ * Deletes a run result saved by the authenticated user when permitted.
9724
+ */
9725
+ handler: async (request, reply) => {
9726
+ try {
9727
+ const user = requireAuthenticatedUser(request);
9728
+ if (denyUnlessAllowed(reply, canUseDataApi(user))) {
9729
+ return;
9730
+ }
9731
+ const record = await db.findRunResultById(request.params.id);
9732
+ if (!record) {
9733
+ void reply.code(404).send({ error: "Run result not found" });
9734
+ return;
9735
+ }
9736
+ if (denyUnlessAllowed(reply, canDeleteRunResult(user, record))) {
9737
+ return;
9738
+ }
9739
+ await db.deleteRunResult(request.params.id, user.id);
9740
+ return reply.code(204).send(null);
9741
+ } catch (error) {
9742
+ if (handleDbError(reply, error)) {
9743
+ return;
9744
+ }
9745
+ throw error;
9746
+ }
9747
+ }
9748
+ });
9749
+ }
9750
+
9107
9751
  // src/server/llm/client.ts
9108
9752
  var OPENAI_BASE_URL = "https://api.openai.com/v1";
9109
9753
  var CLAUDE_BASE_URL = "https://api.anthropic.com/v1";
@@ -9226,6 +9870,165 @@ async function runLlmCompletion(config, input) {
9226
9870
  return parseCompletionResponse(json);
9227
9871
  }
9228
9872
 
9873
+ // src/server/docs/docsSearch.ts
9874
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
9875
+ import path3 from "path";
9876
+ import { create, load, search } from "@orama/orama";
9877
+ import OpenAI from "openai";
9878
+ var DOCS_EMBEDDING_MODEL = "text-embedding-3-small";
9879
+ var DOCS_EMBEDDING_DIMENSIONS = 1536;
9880
+ var DEFAULT_RESULT_LIMIT = 5;
9881
+ var MAX_RESULT_LIMIT = 10;
9882
+ var DEFAULT_SIMILARITY = 0.5;
9883
+ var SNIPPET_MAX_CHARS = 600;
9884
+ var DOCS_SEARCH_SCHEMA = {
9885
+ id: "string",
9886
+ source: "string",
9887
+ path: "string",
9888
+ url: "string",
9889
+ title: "string",
9890
+ heading: "string",
9891
+ content: "string",
9892
+ embedding: `vector[${DOCS_EMBEDDING_DIMENSIONS}]`
9893
+ };
9894
+ var cachedDb = null;
9895
+ var indexUnavailable = false;
9896
+ function getDocsSearchIndexPaths(docsConfig) {
9897
+ const paths = /* @__PURE__ */ new Set();
9898
+ if (docsConfig?.searchIndexPath) {
9899
+ paths.add(resolveDocsSearchIndexPath(docsConfig.searchIndexPath));
9900
+ }
9901
+ paths.add(DEFAULT_DOCS_SEARCH_INDEX_PATH);
9902
+ paths.add(path3.resolve(process.cwd(), "data/docsSearchIndex.json"));
9903
+ return [...paths];
9904
+ }
9905
+ function isDocsSearchIndexAvailable(docsConfig) {
9906
+ return getDocsSearchIndexPaths(docsConfig).some((candidate) => existsSync2(candidate));
9907
+ }
9908
+ function truncateSnippet(content, maxChars = SNIPPET_MAX_CHARS) {
9909
+ if (content.length <= maxChars) {
9910
+ return content;
9911
+ }
9912
+ return `${content.slice(0, maxChars)}...`;
9913
+ }
9914
+ function clampResultLimit(limit) {
9915
+ if (limit == null || !Number.isFinite(limit)) {
9916
+ return DEFAULT_RESULT_LIMIT;
9917
+ }
9918
+ return Math.min(Math.max(1, Math.floor(limit)), MAX_RESULT_LIMIT);
9919
+ }
9920
+ function loadDocsSearchIndex(docsConfig, paths) {
9921
+ if (cachedDb != null) {
9922
+ return cachedDb;
9923
+ }
9924
+ if (indexUnavailable) {
9925
+ throw new Error(
9926
+ "Documentation search index is not available. Deploy docsSearchIndex.json to the Team Hub server."
9927
+ );
9928
+ }
9929
+ const candidates = paths ?? getDocsSearchIndexPaths(docsConfig);
9930
+ const catalogPath = candidates.find((candidate) => existsSync2(candidate));
9931
+ if (catalogPath == null) {
9932
+ indexUnavailable = true;
9933
+ throw new Error(
9934
+ "Documentation search index is not available. Deploy docsSearchIndex.json to the Team Hub server."
9935
+ );
9936
+ }
9937
+ const raw = JSON.parse(readFileSync3(catalogPath, "utf8"));
9938
+ const db = create({ schema: DOCS_SEARCH_SCHEMA });
9939
+ load(db, raw);
9940
+ cachedDb = db;
9941
+ return db;
9942
+ }
9943
+ async function embedDocsQuery(query, apiKey) {
9944
+ const client = new OpenAI({ apiKey });
9945
+ const response = await client.embeddings.create({
9946
+ model: DOCS_EMBEDDING_MODEL,
9947
+ input: query
9948
+ });
9949
+ const embedding = response.data[0]?.embedding;
9950
+ if (!Array.isArray(embedding) || embedding.length !== DOCS_EMBEDDING_DIMENSIONS) {
9951
+ throw new Error("Unexpected embedding size from OpenAI.");
9952
+ }
9953
+ return embedding;
9954
+ }
9955
+ async function searchDocs(llmConfig, docsConfig, args) {
9956
+ const query = args.query?.trim();
9957
+ if (!query) {
9958
+ throw new Error("query is required.");
9959
+ }
9960
+ const apiKey = llmConfig.providers.openai?.apiKey.trim();
9961
+ if (!apiKey) {
9962
+ throw new Error(
9963
+ "OpenAI provider is not configured on this Team Hub. Documentation search requires an OpenAI API key."
9964
+ );
9965
+ }
9966
+ const db = loadDocsSearchIndex(docsConfig);
9967
+ const embedding = await embedDocsQuery(query, apiKey);
9968
+ const limit = clampResultLimit(args.limit);
9969
+ const results = search(db, {
9970
+ mode: "vector",
9971
+ vector: {
9972
+ value: embedding,
9973
+ property: "embedding"
9974
+ },
9975
+ similarity: DEFAULT_SIMILARITY,
9976
+ limit,
9977
+ ...args.source != null ? { where: { source: args.source } } : {}
9978
+ });
9979
+ const resolved = results instanceof Promise ? await results : results;
9980
+ return resolved.hits.map((hit) => {
9981
+ const document = hit.document;
9982
+ return {
9983
+ title: document.title,
9984
+ heading: document.heading,
9985
+ url: document.url,
9986
+ source: document.source,
9987
+ path: document.path,
9988
+ score: hit.score,
9989
+ snippet: truncateSnippet(document.content)
9990
+ };
9991
+ });
9992
+ }
9993
+
9994
+ // src/server/llm/hubNativeTools.ts
9995
+ var HUB_NATIVE_TOOL_NAMES = ["search_docs"];
9996
+ function isHubNativeToolName(name) {
9997
+ return HUB_NATIVE_TOOL_NAMES.includes(name);
9998
+ }
9999
+ function canExecuteHubDocsSearch(llmConfig, docsConfig) {
10000
+ if (!llmConfig || !hasHubOpenAiProvider(llmConfig)) {
10001
+ return false;
10002
+ }
10003
+ return isDocsSearchIndexAvailable(docsConfig);
10004
+ }
10005
+ function filterClientToolsForHub(tools, llmConfig, docsConfig) {
10006
+ if (!tools || tools.length === 0) {
10007
+ return tools;
10008
+ }
10009
+ if (canExecuteHubDocsSearch(llmConfig, docsConfig)) {
10010
+ return tools;
10011
+ }
10012
+ const filtered = tools.filter((tool) => {
10013
+ const name = tool.function?.name;
10014
+ return name !== "search_docs";
10015
+ });
10016
+ return filtered.length > 0 ? filtered : void 0;
10017
+ }
10018
+ async function callHubNativeTool(name, args, llmConfig, docsConfig) {
10019
+ if (name !== "search_docs") {
10020
+ return JSON.stringify({ error: `Unknown hub-native tool: ${name}` });
10021
+ }
10022
+ try {
10023
+ const parsed = args;
10024
+ const hits = await searchDocs(llmConfig, docsConfig, parsed);
10025
+ return JSON.stringify(hits);
10026
+ } catch (error) {
10027
+ const message = error instanceof Error ? error.message : "Documentation search failed.";
10028
+ return JSON.stringify({ error: message });
10029
+ }
10030
+ }
10031
+
9229
10032
  // src/server/llm/hubMcpToolNames.ts
9230
10033
  var HUB_MCP_TOOL_PREFIX = "hubmcp__";
9231
10034
  function encodeHubMcpToolName(serverIndex, toolName) {
@@ -9419,14 +10222,16 @@ function parseToolArguments(raw) {
9419
10222
  return {};
9420
10223
  }
9421
10224
  }
9422
- async function runHubChatStep(config, input, deps = {}) {
10225
+ async function runHubChatStep(config, input, deps = {}, docsConfig = null) {
9423
10226
  const runCompletion = deps.runCompletion ?? runLlmCompletion;
9424
10227
  const ensureConnections = deps.ensureConnections ?? ensureHubMcpConnections;
9425
10228
  const listTools = deps.listTools ?? listHubMcpTools;
9426
10229
  const callTool = deps.callTool ?? callHubMcpTool;
10230
+ const callNativeTool = deps.callNativeTool ?? callHubNativeTool;
9427
10231
  await ensureConnections(config);
9428
10232
  const hubTools = listTools();
9429
- const mergedTools = hubTools.length > 0 || (input.tools?.length ?? 0) > 0 ? [...hubTools, ...input.tools ?? []] : void 0;
10233
+ const clientTools = filterClientToolsForHub(input.tools, config, docsConfig);
10234
+ const mergedTools = hubTools.length > 0 || (clientTools?.length ?? 0) > 0 ? [...hubTools, ...clientTools ?? []] : void 0;
9430
10235
  let messages = [...input.messages];
9431
10236
  let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
9432
10237
  let lastContent = null;
@@ -9446,8 +10251,11 @@ async function runHubChatStep(config, input, deps = {}) {
9446
10251
  usage
9447
10252
  };
9448
10253
  }
10254
+ const nativeCalls = toolCalls.filter((call) => isHubNativeToolName(call.name));
9449
10255
  const hubCalls = toolCalls.filter((call) => isHubMcpToolName(call.name));
9450
- const passthroughCalls = toolCalls.filter((call) => !isHubMcpToolName(call.name));
10256
+ const passthroughCalls = toolCalls.filter(
10257
+ (call) => !isHubNativeToolName(call.name) && !isHubMcpToolName(call.name)
10258
+ );
9451
10259
  if (passthroughCalls.length > 0) {
9452
10260
  return {
9453
10261
  content: result.content,
@@ -9455,14 +10263,28 @@ async function runHubChatStep(config, input, deps = {}) {
9455
10263
  usage
9456
10264
  };
9457
10265
  }
10266
+ const serverCalls = [...nativeCalls, ...hubCalls];
9458
10267
  messages = [
9459
10268
  ...messages,
9460
10269
  {
9461
10270
  role: "assistant",
9462
10271
  content: result.content,
9463
- tool_calls: hubCalls
10272
+ tool_calls: serverCalls
9464
10273
  }
9465
10274
  ];
10275
+ for (const call of nativeCalls) {
10276
+ const toolResult = await callNativeTool(
10277
+ call.name,
10278
+ parseToolArguments(call.arguments),
10279
+ config,
10280
+ docsConfig
10281
+ );
10282
+ messages.push({
10283
+ role: "tool",
10284
+ tool_call_id: call.id,
10285
+ content: toolResult
10286
+ });
10287
+ }
9466
10288
  for (const call of hubCalls) {
9467
10289
  const toolResult = await callTool(call.name, parseToolArguments(call.arguments));
9468
10290
  messages.push({
@@ -9521,7 +10343,8 @@ async function registerLlmRoutes(app, options) {
9521
10343
  id: model.id,
9522
10344
  label: model.label,
9523
10345
  provider: model.provider
9524
- }))
10346
+ })),
10347
+ capabilities: getHubLlmCapabilities(llm)
9525
10348
  });
9526
10349
  }
9527
10350
  });
@@ -9595,12 +10418,17 @@ async function registerLlmRoutes(app, options) {
9595
10418
  if (isNewTurn && isOverMonthlyLimit(totalTokens, user.llmMonthlyTokenLimit)) {
9596
10419
  return sendMonthlyLimitExceeded(reply);
9597
10420
  }
9598
- const result = await runHubChatStep(llm, {
9599
- model,
9600
- messages,
9601
- tools,
9602
- systemPrompt
9603
- });
10421
+ const result = await runHubChatStep(
10422
+ llm,
10423
+ {
10424
+ model,
10425
+ messages,
10426
+ tools,
10427
+ systemPrompt
10428
+ },
10429
+ {},
10430
+ options.getDocs()
10431
+ );
9604
10432
  const catalogModel = getHubModelById(model);
9605
10433
  if (!catalogModel) {
9606
10434
  throw new Error(`Unknown hub model: ${model}`);
@@ -9746,7 +10574,12 @@ async function registerProtectedRoutes(app, options) {
9746
10574
  await registerSnippetRoutes(app, options.db);
9747
10575
  await registerFolderRoutes(app, options.db);
9748
10576
  await registerRequestRoutes(app, options.db);
9749
- await registerLlmRoutes(app, { db: options.db, getLlm: options.getLlm });
10577
+ await registerRunResultRoutes(app, options.db);
10578
+ await registerLlmRoutes(app, {
10579
+ db: options.db,
10580
+ getLlm: options.getLlm,
10581
+ getDocs: options.getDocs
10582
+ });
9750
10583
  await registerPluginsRoutes(app, { getPlugins: options.getPlugins });
9751
10584
  }
9752
10585
  async function registerRoutes(app, options) {
@@ -9781,6 +10614,7 @@ async function createServer(ctxOrConfig, options = {}) {
9781
10614
  throttleStore,
9782
10615
  getLlm: ctx ? () => ctx.getLlm() : () => legacyConfig?.llm ?? null,
9783
10616
  getPlugins: ctx ? () => ctx.getPlugins() : () => legacyConfig?.plugins ?? null,
10617
+ getDocs: ctx ? () => ctx.getDocs() : () => legacyConfig?.docs ?? null,
9784
10618
  reloadConfig: options.reloadConfig ?? (async () => ({ sections: [] }))
9785
10619
  });
9786
10620
  return app;
@@ -9967,7 +10801,8 @@ function createRuntimeContext(config, configPath) {
9967
10801
  dbHolder: { underlying: createDatabase(config.db) },
9968
10802
  throttleHolder: { underlying: createThrottleStore(config.redis) },
9969
10803
  llm: config.llm,
9970
- plugins: config.plugins
10804
+ plugins: config.plugins,
10805
+ docs: config.docs
9971
10806
  };
9972
10807
  const ctx = {
9973
10808
  configPath: state.configPath,
@@ -9981,6 +10816,7 @@ function createRuntimeContext(config, configPath) {
9981
10816
  throttleStore: createSwappableProxy(state.throttleHolder),
9982
10817
  getLlm: () => state.llm,
9983
10818
  getPlugins: () => state.plugins,
10819
+ getDocs: () => state.docs,
9984
10820
  logger: createLogger(config.logging)
9985
10821
  };
9986
10822
  runtimeContextStates.set(ctx, state);
@@ -10054,6 +10890,8 @@ async function reloadRuntimeConfig(ctx) {
10054
10890
  sections.push({ section: "llm", status: "reloaded" });
10055
10891
  state.plugins = nextConfig.plugins;
10056
10892
  sections.push({ section: "plugins", status: "reloaded" });
10893
+ state.docs = nextConfig.docs;
10894
+ sections.push({ section: "docs", status: "reloaded" });
10057
10895
  sections.push(reloadServerSection(state, nextConfig));
10058
10896
  return { sections };
10059
10897
  }