@harborclient/team-hub 0.4.2 → 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
  }
@@ -6470,6 +6496,14 @@ var LLM_MODEL_CATALOG = [
6470
6496
  function hasProviderKey(config, provider) {
6471
6497
  return Boolean(config.providers[provider]?.apiKey.trim());
6472
6498
  }
6499
+ function hasHubOpenAiProvider(config) {
6500
+ return hasProviderKey(config, "openai");
6501
+ }
6502
+ function getHubLlmCapabilities(config) {
6503
+ return {
6504
+ openai: hasProviderKey(config, "openai")
6505
+ };
6506
+ }
6473
6507
  function listHubOfferedModels(config) {
6474
6508
  const allowList = config.models ? new Set(config.models) : null;
6475
6509
  return LLM_MODEL_CATALOG.filter((model) => {
@@ -7285,8 +7319,12 @@ var llmModelSchema = z7.object({
7285
7319
  label: z7.string(),
7286
7320
  provider: z7.enum(["openai", "claude", "gemini"])
7287
7321
  });
7322
+ var llmCapabilitiesSchema = z7.object({
7323
+ openai: z7.boolean()
7324
+ });
7288
7325
  var listLlmModelsResponseSchema = z7.object({
7289
- models: z7.array(llmModelSchema)
7326
+ models: z7.array(llmModelSchema),
7327
+ capabilities: llmCapabilitiesSchema
7290
7328
  });
7291
7329
  var llmUsageSummaryResponseSchema = z7.object({
7292
7330
  period: z7.string(),
@@ -7637,7 +7675,7 @@ var listAdminTokensResponseSchema = z9.object({
7637
7675
  tokens: z9.array(hubApiTokenRecordSchema)
7638
7676
  });
7639
7677
  var reloadConfigSectionResultSchema = z9.object({
7640
- section: z9.enum(["db", "redis", "llm", "plugins", "server"]),
7678
+ section: z9.enum(["db", "redis", "llm", "plugins", "docs", "server"]),
7641
7679
  status: z9.enum(["reloaded", "unchanged", "failed", "restart-required"]),
7642
7680
  error: z9.string().optional()
7643
7681
  });
@@ -8377,7 +8415,7 @@ async function registerAdminRoutes(app, options) {
8377
8415
  label: model.label,
8378
8416
  provider: model.provider
8379
8417
  }));
8380
- return reply.send({ models });
8418
+ return reply.send({ models, capabilities: getHubLlmCapabilities(llm) });
8381
8419
  }
8382
8420
  });
8383
8421
  routes.route({
@@ -9832,6 +9870,165 @@ async function runLlmCompletion(config, input) {
9832
9870
  return parseCompletionResponse(json);
9833
9871
  }
9834
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
+
9835
10032
  // src/server/llm/hubMcpToolNames.ts
9836
10033
  var HUB_MCP_TOOL_PREFIX = "hubmcp__";
9837
10034
  function encodeHubMcpToolName(serverIndex, toolName) {
@@ -10025,14 +10222,16 @@ function parseToolArguments(raw) {
10025
10222
  return {};
10026
10223
  }
10027
10224
  }
10028
- async function runHubChatStep(config, input, deps = {}) {
10225
+ async function runHubChatStep(config, input, deps = {}, docsConfig = null) {
10029
10226
  const runCompletion = deps.runCompletion ?? runLlmCompletion;
10030
10227
  const ensureConnections = deps.ensureConnections ?? ensureHubMcpConnections;
10031
10228
  const listTools = deps.listTools ?? listHubMcpTools;
10032
10229
  const callTool = deps.callTool ?? callHubMcpTool;
10230
+ const callNativeTool = deps.callNativeTool ?? callHubNativeTool;
10033
10231
  await ensureConnections(config);
10034
10232
  const hubTools = listTools();
10035
- 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;
10036
10235
  let messages = [...input.messages];
10037
10236
  let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
10038
10237
  let lastContent = null;
@@ -10052,8 +10251,11 @@ async function runHubChatStep(config, input, deps = {}) {
10052
10251
  usage
10053
10252
  };
10054
10253
  }
10254
+ const nativeCalls = toolCalls.filter((call) => isHubNativeToolName(call.name));
10055
10255
  const hubCalls = toolCalls.filter((call) => isHubMcpToolName(call.name));
10056
- const passthroughCalls = toolCalls.filter((call) => !isHubMcpToolName(call.name));
10256
+ const passthroughCalls = toolCalls.filter(
10257
+ (call) => !isHubNativeToolName(call.name) && !isHubMcpToolName(call.name)
10258
+ );
10057
10259
  if (passthroughCalls.length > 0) {
10058
10260
  return {
10059
10261
  content: result.content,
@@ -10061,14 +10263,28 @@ async function runHubChatStep(config, input, deps = {}) {
10061
10263
  usage
10062
10264
  };
10063
10265
  }
10266
+ const serverCalls = [...nativeCalls, ...hubCalls];
10064
10267
  messages = [
10065
10268
  ...messages,
10066
10269
  {
10067
10270
  role: "assistant",
10068
10271
  content: result.content,
10069
- tool_calls: hubCalls
10272
+ tool_calls: serverCalls
10070
10273
  }
10071
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
+ }
10072
10288
  for (const call of hubCalls) {
10073
10289
  const toolResult = await callTool(call.name, parseToolArguments(call.arguments));
10074
10290
  messages.push({
@@ -10127,7 +10343,8 @@ async function registerLlmRoutes(app, options) {
10127
10343
  id: model.id,
10128
10344
  label: model.label,
10129
10345
  provider: model.provider
10130
- }))
10346
+ })),
10347
+ capabilities: getHubLlmCapabilities(llm)
10131
10348
  });
10132
10349
  }
10133
10350
  });
@@ -10201,12 +10418,17 @@ async function registerLlmRoutes(app, options) {
10201
10418
  if (isNewTurn && isOverMonthlyLimit(totalTokens, user.llmMonthlyTokenLimit)) {
10202
10419
  return sendMonthlyLimitExceeded(reply);
10203
10420
  }
10204
- const result = await runHubChatStep(llm, {
10205
- model,
10206
- messages,
10207
- tools,
10208
- systemPrompt
10209
- });
10421
+ const result = await runHubChatStep(
10422
+ llm,
10423
+ {
10424
+ model,
10425
+ messages,
10426
+ tools,
10427
+ systemPrompt
10428
+ },
10429
+ {},
10430
+ options.getDocs()
10431
+ );
10210
10432
  const catalogModel = getHubModelById(model);
10211
10433
  if (!catalogModel) {
10212
10434
  throw new Error(`Unknown hub model: ${model}`);
@@ -10353,7 +10575,11 @@ async function registerProtectedRoutes(app, options) {
10353
10575
  await registerFolderRoutes(app, options.db);
10354
10576
  await registerRequestRoutes(app, options.db);
10355
10577
  await registerRunResultRoutes(app, options.db);
10356
- await registerLlmRoutes(app, { db: options.db, getLlm: options.getLlm });
10578
+ await registerLlmRoutes(app, {
10579
+ db: options.db,
10580
+ getLlm: options.getLlm,
10581
+ getDocs: options.getDocs
10582
+ });
10357
10583
  await registerPluginsRoutes(app, { getPlugins: options.getPlugins });
10358
10584
  }
10359
10585
  async function registerRoutes(app, options) {
@@ -10388,6 +10614,7 @@ async function createServer(ctxOrConfig, options = {}) {
10388
10614
  throttleStore,
10389
10615
  getLlm: ctx ? () => ctx.getLlm() : () => legacyConfig?.llm ?? null,
10390
10616
  getPlugins: ctx ? () => ctx.getPlugins() : () => legacyConfig?.plugins ?? null,
10617
+ getDocs: ctx ? () => ctx.getDocs() : () => legacyConfig?.docs ?? null,
10391
10618
  reloadConfig: options.reloadConfig ?? (async () => ({ sections: [] }))
10392
10619
  });
10393
10620
  return app;
@@ -10574,7 +10801,8 @@ function createRuntimeContext(config, configPath) {
10574
10801
  dbHolder: { underlying: createDatabase(config.db) },
10575
10802
  throttleHolder: { underlying: createThrottleStore(config.redis) },
10576
10803
  llm: config.llm,
10577
- plugins: config.plugins
10804
+ plugins: config.plugins,
10805
+ docs: config.docs
10578
10806
  };
10579
10807
  const ctx = {
10580
10808
  configPath: state.configPath,
@@ -10588,6 +10816,7 @@ function createRuntimeContext(config, configPath) {
10588
10816
  throttleStore: createSwappableProxy(state.throttleHolder),
10589
10817
  getLlm: () => state.llm,
10590
10818
  getPlugins: () => state.plugins,
10819
+ getDocs: () => state.docs,
10591
10820
  logger: createLogger(config.logging)
10592
10821
  };
10593
10822
  runtimeContextStates.set(ctx, state);
@@ -10661,6 +10890,8 @@ async function reloadRuntimeConfig(ctx) {
10661
10890
  sections.push({ section: "llm", status: "reloaded" });
10662
10891
  state.plugins = nextConfig.plugins;
10663
10892
  sections.push({ section: "plugins", status: "reloaded" });
10893
+ state.docs = nextConfig.docs;
10894
+ sections.push({ section: "docs", status: "reloaded" });
10664
10895
  sections.push(reloadServerSection(state, nextConfig));
10665
10896
  return { sections };
10666
10897
  }