@hiai-gg/docsmint 0.8.7 → 0.9.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/README.md CHANGED
@@ -42,17 +42,19 @@ instructions, and verify the connection. Hosted MCP follows your plan and
42
42
  workspace permissions. Prefer workspace-bound or category-scoped credentials.
43
43
 
44
44
  The Cloud endpoint is `https://docsmint.com/mcp` (Streamable HTTP). OAuth-capable
45
- clients discover authorization and open DocsMint login and consent in the browser.
46
- Choose your workspace and explicit scopes; the authorization code uses PKCE S256.
47
- Access tokens expire after one hour; reconnect through browser authorization when
48
- they expire (no refresh tokens). API-key clients
49
- send `Authorization: Bearer <key>`. Credentials cannot create or elevate other
50
- credentials; lifecycle management belongs to the signed-in browser session.
45
+ clients discover the hosted resource and authorization-server metadata; current
46
+ authorization and client-connection methods are described in the
47
+ [DocsMint Cloud setup guide](https://docsmint.com/mcp/connect?source=github_mcp).
48
+ Browser consent binds the one-hour opaque access token to the selected workspace,
49
+ optional category, and requested scopes. Refresh tokens are not issued. API-key clients can instead send
50
+ `Authorization: Bearer <key>` with an MCP/API credential created in the authenticated
51
+ browser UI. Credentials cannot create or elevate other credentials; lifecycle
52
+ management belongs to the signed-in browser session.
51
53
 
52
54
  **Self-hosted alternative:** run [DocsMint with Docker](#quickstart), create an API
53
55
  key in its browser UI, and configure `HIAI_DOCS_URL` and `HIAI_DOCS_API_KEY` for
54
56
  `npx --yes --package @hiai-gg/docsmint docsmint-mcp`. The npm package is a stdio
55
- bridge to your running deployment, not a server installer. See the
57
+ bridge to your own API, not a server installer or hosted OAuth server. See the
56
58
  [MCP guide](https://github.com/HiAi-gg/docsmint/blob/main/packages/mcp-server/README.md) for client configuration.
57
59
 
58
60
  ## Why DocsMint?
@@ -70,20 +72,19 @@ bridge to your running deployment, not a server installer. See the
70
72
  - **Choose how you run it.** Use [managed DocsMint](https://docsmint.com) or
71
73
  self-host the application, database, search, queues, and files.
72
74
 
73
- ## What's new in 0.8.7?
75
+ ## What's new in 0.9.0?
74
76
 
75
- Agents can now complete more document workflows without leaving MCP:
77
+ - Keep MCP `list_documents` output aligned with the public SDK by returning
78
+ folder and category names, and invalidate stale list entries after metadata
79
+ changes for every member of the affected workspace.
80
+ - Publish all 21 MCP tools with stable schemas and clearer parameter guidance;
81
+ expose the canonical `capabilityCatalog` for hosted contract checks.
82
+ - Reject category-scoped document creation when the selected folder belongs to
83
+ a different category.
84
+ - Keep category-list response types and Cloud connection guidance aligned with
85
+ the public API and supported setup flow.
76
86
 
77
- - Move documents to trash, remove folders or categories, and restore saved versions.
78
- - Keep changes within workspace/category permissions; version restoration requires edit access.
79
- - Discover clearer tool descriptions, parameter guidance, and destructive-operation annotations.
80
- - Apply GraphRAG query text and related-document result limits consistently.
81
-
82
- The MCP surface grows additively from 17 to 21 tools. Existing tool names remain
83
- available. No database migration is required. Hosted clients receive these tools
84
- when their DocsMint host adopts this release.
85
-
86
- Read the [release notes](https://github.com/HiAi-gg/docsmint/releases/tag/v0.8.7)
87
+ This release adds no database migration. See the [release notes](https://github.com/HiAi-gg/docsmint/releases/tag/v0.9.0)
87
88
  and [changelog](https://github.com/HiAi-gg/docsmint/blob/main/CHANGELOG.md).
88
89
 
89
90
  ## Install with an AI agent
@@ -136,7 +137,7 @@ docker pull vgalibov/docsmint:web-latest
136
137
  docker pull vgalibov/docsmint:caddy-latest
137
138
  ```
138
139
 
139
- Use versioned tags `api-v0.8.7`, `web-v0.8.7`, and `caddy-v0.8.7` for
140
+ Use versioned tags `api-v0.9.0`, `web-v0.9.0`, and `caddy-v0.9.0` for
140
141
  reproducible deploys. Caddy is the supporting reverse proxy with rate limiting;
141
142
  it is separate from the API and web application. The quickstart still builds the Compose stack from this repository so PostgreSQL,
142
143
  Redis, and SeaweedFS start together with the application.
@@ -221,7 +222,7 @@ command and configuration precedence.
221
222
  ## Connect an MCP client
222
223
 
223
224
  Give agents a secure path to search, read, and maintain your knowledge without
224
- database or filesystem access. DocsMint publishes 21 tools plus ready-made
225
+ database or filesystem access. DocsMint publishes MCP tools plus ready-made
225
226
  research prompts, scoped resources, and a document-manager skill.
226
227
 
227
228
  ### Hosted DocsMint
@@ -200640,6 +200640,112 @@ function buildApiAccessValues(input) {
200640
200640
  };
200641
200641
  }
200642
200642
 
200643
+ // ../../backend/src/lib/doc-cache.ts
200644
+ init_logger3();
200645
+ init_redis();
200646
+ var LIST_PREFIX = "hiai-docs:cache:docs:list:";
200647
+ var SINGLE_PREFIX = "hiai-docs:cache:docs:single:";
200648
+ function docListKey(userId, folderId, tag2, page = 1, limit = 20, workspaceId) {
200649
+ const parts = [`${LIST_PREFIX}${userId}`];
200650
+ if (workspaceId)
200651
+ parts.push(`w:${workspaceId}`);
200652
+ if (folderId)
200653
+ parts.push(`f:${folderId}`);
200654
+ if (tag2)
200655
+ parts.push(`t:${tag2}`);
200656
+ parts.push(`p:${page}`, `l:${limit}`);
200657
+ return parts.join(":");
200658
+ }
200659
+ function docSingleKey(docId, userId, workspaceId) {
200660
+ return `${SINGLE_PREFIX}${userId}${workspaceId ? `:w:${workspaceId}` : ""}:${docId}`;
200661
+ }
200662
+ async function cacheGetOrSet(key, ttl, compute, options = {}) {
200663
+ try {
200664
+ const cached2 = await redis.get(key);
200665
+ if (cached2)
200666
+ return JSON.parse(cached2);
200667
+ } catch (err) {
200668
+ logger3.warn({ err, key }, "Redis get failed, falling through to DB");
200669
+ }
200670
+ const value = await compute();
200671
+ if (options.shouldCache && !options.shouldCache(value))
200672
+ return value;
200673
+ try {
200674
+ await redis.set(key, JSON.stringify(value), "EX", ttl);
200675
+ } catch (err) {
200676
+ logger3.warn({ err, key }, "Redis set failed");
200677
+ }
200678
+ return value;
200679
+ }
200680
+ function isHttpCacheEnvelope(value) {
200681
+ return typeof value === "object" && value !== null && "version" in value && value.version === 1 && "kind" in value && value.kind === "http-response" && "status" in value && typeof value.status === "number" && Number.isInteger(value.status) && value.status >= 100 && value.status <= 599 && "body" in value;
200682
+ }
200683
+ async function cacheHttpResponse(key, ttl, compute, options = {}) {
200684
+ try {
200685
+ const cached2 = await redis.get(key);
200686
+ if (cached2) {
200687
+ const parsed = JSON.parse(cached2);
200688
+ if (isHttpCacheEnvelope(parsed)) {
200689
+ return {
200690
+ status: parsed.status,
200691
+ body: parsed.body,
200692
+ cacheHit: true
200693
+ };
200694
+ }
200695
+ }
200696
+ } catch (err) {
200697
+ logger3.warn({ err, key }, "Redis HTTP cache get failed, falling through to DB");
200698
+ }
200699
+ const value = await compute();
200700
+ if (options.shouldCache && !options.shouldCache(value)) {
200701
+ return { ...value, cacheHit: false };
200702
+ }
200703
+ const envelope = {
200704
+ version: 1,
200705
+ kind: "http-response",
200706
+ status: value.status,
200707
+ body: value.body
200708
+ };
200709
+ try {
200710
+ await redis.set(key, JSON.stringify(envelope), "EX", ttl);
200711
+ } catch (err) {
200712
+ logger3.warn({ err, key }, "Redis HTTP cache set failed");
200713
+ }
200714
+ return { ...value, cacheHit: false };
200715
+ }
200716
+ async function invalidateDocListCache(userId, workspaceId) {
200717
+ const patterns = new Set([`${LIST_PREFIX}${userId}:*`]);
200718
+ if (workspaceId)
200719
+ patterns.add(`${LIST_PREFIX}*:w:${workspaceId}:*`);
200720
+ try {
200721
+ for (const pattern of patterns) {
200722
+ let cursor = "0";
200723
+ do {
200724
+ const [newCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 100);
200725
+ cursor = newCursor;
200726
+ if (keys.length > 0)
200727
+ await redis.del(...keys);
200728
+ } while (cursor !== "0");
200729
+ }
200730
+ } catch (err) {
200731
+ logger3.warn({ err, userId, workspaceId }, "Failed to invalidate doc list cache");
200732
+ }
200733
+ }
200734
+ async function invalidateDocCache(docId) {
200735
+ const pattern = `${SINGLE_PREFIX}*:${docId}:scope:*`;
200736
+ try {
200737
+ let cursor = "0";
200738
+ do {
200739
+ const [newCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 100);
200740
+ cursor = newCursor;
200741
+ if (keys.length > 0)
200742
+ await redis.del(...keys);
200743
+ } while (cursor !== "0");
200744
+ } catch (err) {
200745
+ logger3.warn({ err, docId }, "Failed to invalidate doc cache");
200746
+ }
200747
+ }
200748
+
200643
200749
  // ../../backend/src/api/routes/categories.ts
200644
200750
  init_logger3();
200645
200751
  var createCategorySchema = exports_external2.object({
@@ -200861,6 +200967,7 @@ var categoryRoutes = new Elysia({ prefix: "/api" }).get("/categories", async ({
200861
200967
  set2.status = 404;
200862
200968
  return { error: "Category not found" };
200863
200969
  }
200970
+ await invalidateDocListCache(ctx.userId, ctx.workspaceId);
200864
200971
  if (parsed.data.name !== undefined) {
200865
200972
  dispatchMetadataReembedOutbox(updated.operationId, metadataReembedPageSize("category"));
200866
200973
  }
@@ -200908,6 +201015,7 @@ var categoryRoutes = new Elysia({ prefix: "/api" }).get("/categories", async ({
200908
201015
  set2.status = 404;
200909
201016
  return { error: "Category not found" };
200910
201017
  }
201018
+ await invalidateDocListCache(ctx.userId, ctx.workspaceId);
200911
201019
  dispatchMetadataReembedOutbox(deletion.operationId, metadataReembedPageSize("category"));
200912
201020
  return { success: true };
200913
201021
  } catch (err) {
@@ -228024,110 +228132,6 @@ function createYjsRoomKey(input) {
228024
228132
 
228025
228133
  // ../../backend/src/api/routes/collaboration.ts
228026
228134
  init_config();
228027
-
228028
- // ../../backend/src/lib/doc-cache.ts
228029
- init_logger3();
228030
- init_redis();
228031
- var LIST_PREFIX = "hiai-docs:cache:docs:list:";
228032
- var SINGLE_PREFIX = "hiai-docs:cache:docs:single:";
228033
- function docListKey(userId, folderId, tag2, page = 1, limit = 20, workspaceId) {
228034
- const parts = [`${LIST_PREFIX}${userId}`];
228035
- if (workspaceId)
228036
- parts.push(`w:${workspaceId}`);
228037
- if (folderId)
228038
- parts.push(`f:${folderId}`);
228039
- if (tag2)
228040
- parts.push(`t:${tag2}`);
228041
- parts.push(`p:${page}`, `l:${limit}`);
228042
- return parts.join(":");
228043
- }
228044
- function docSingleKey(docId, userId, workspaceId) {
228045
- return `${SINGLE_PREFIX}${userId}${workspaceId ? `:w:${workspaceId}` : ""}:${docId}`;
228046
- }
228047
- async function cacheGetOrSet(key, ttl, compute, options = {}) {
228048
- try {
228049
- const cached2 = await redis.get(key);
228050
- if (cached2)
228051
- return JSON.parse(cached2);
228052
- } catch (err) {
228053
- logger3.warn({ err, key }, "Redis get failed, falling through to DB");
228054
- }
228055
- const value = await compute();
228056
- if (options.shouldCache && !options.shouldCache(value))
228057
- return value;
228058
- try {
228059
- await redis.set(key, JSON.stringify(value), "EX", ttl);
228060
- } catch (err) {
228061
- logger3.warn({ err, key }, "Redis set failed");
228062
- }
228063
- return value;
228064
- }
228065
- function isHttpCacheEnvelope(value) {
228066
- return typeof value === "object" && value !== null && "version" in value && value.version === 1 && "kind" in value && value.kind === "http-response" && "status" in value && typeof value.status === "number" && Number.isInteger(value.status) && value.status >= 100 && value.status <= 599 && "body" in value;
228067
- }
228068
- async function cacheHttpResponse(key, ttl, compute, options = {}) {
228069
- try {
228070
- const cached2 = await redis.get(key);
228071
- if (cached2) {
228072
- const parsed = JSON.parse(cached2);
228073
- if (isHttpCacheEnvelope(parsed)) {
228074
- return {
228075
- status: parsed.status,
228076
- body: parsed.body,
228077
- cacheHit: true
228078
- };
228079
- }
228080
- }
228081
- } catch (err) {
228082
- logger3.warn({ err, key }, "Redis HTTP cache get failed, falling through to DB");
228083
- }
228084
- const value = await compute();
228085
- if (options.shouldCache && !options.shouldCache(value)) {
228086
- return { ...value, cacheHit: false };
228087
- }
228088
- const envelope = {
228089
- version: 1,
228090
- kind: "http-response",
228091
- status: value.status,
228092
- body: value.body
228093
- };
228094
- try {
228095
- await redis.set(key, JSON.stringify(envelope), "EX", ttl);
228096
- } catch (err) {
228097
- logger3.warn({ err, key }, "Redis HTTP cache set failed");
228098
- }
228099
- return { ...value, cacheHit: false };
228100
- }
228101
- async function invalidateDocListCache(userId) {
228102
- const pattern = `${LIST_PREFIX}${userId}:*`;
228103
- try {
228104
- let cursor = "0";
228105
- do {
228106
- const [newCursor, keys4] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 100);
228107
- cursor = newCursor;
228108
- if (keys4.length > 0)
228109
- await redis.del(...keys4);
228110
- } while (cursor !== "0");
228111
- } catch (err) {
228112
- logger3.warn({ err, userId }, "Failed to invalidate doc list cache");
228113
- }
228114
- }
228115
- async function invalidateDocCache(docId) {
228116
- const pattern = `${SINGLE_PREFIX}*:${docId}:scope:*`;
228117
- try {
228118
- let cursor = "0";
228119
- do {
228120
- const [newCursor, keys4] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 100);
228121
- cursor = newCursor;
228122
- if (keys4.length > 0)
228123
- await redis.del(...keys4);
228124
- } while (cursor !== "0");
228125
- } catch (err) {
228126
- logger3.warn({ err, docId }, "Failed to invalidate doc cache");
228127
- }
228128
- }
228129
-
228130
- // ../../backend/src/api/routes/collaboration.ts
228131
228135
  init_logger3();
228132
228136
 
228133
228137
  // ../../node_modules/.bun/y-protocols@1.0.7+48ebd0cf55a3d4ab/node_modules/y-protocols/sync.js
@@ -228514,7 +228518,7 @@ collaborationRoutes.ws("/api/ws/collab/:documentId", {
228514
228518
  });
228515
228519
  await Promise.all([
228516
228520
  invalidateDocCache(documentId),
228517
- invalidateDocListCache(access.userId)
228521
+ invalidateDocListCache(access.userId, access.ctx.workspaceId)
228518
228522
  ]);
228519
228523
  enqueueReembed([
228520
228524
  {
@@ -228982,6 +228986,27 @@ async function withTags(ctx, rows) {
228982
228986
  }
228983
228987
  return rows.map((r4) => ({ ...r4, tags: byDoc.get(r4.id) ?? [] }));
228984
228988
  }
228989
+ async function withDocumentLocationNames(ctx, rows) {
228990
+ if (rows.length === 0)
228991
+ return [];
228992
+ const folderIds = [
228993
+ ...new Set(rows.flatMap((row) => row.folderId ? [row.folderId] : []))
228994
+ ];
228995
+ const categoryIds = [
228996
+ ...new Set(rows.flatMap((row) => row.categoryId ? [row.categoryId] : []))
228997
+ ];
228998
+ const [folderRows, categoryRows] = await withTenant(ctx, async (tx) => Promise.all([
228999
+ folderIds.length > 0 ? tx.select({ id: folders.id, name: folders.name }).from(folders).where(inArray(folders.id, folderIds)) : Promise.resolve([]),
229000
+ categoryIds.length > 0 ? tx.select({ id: categories.id, name: categories.name }).from(categories).where(inArray(categories.id, categoryIds)) : Promise.resolve([])
229001
+ ]));
229002
+ const folderNames = new Map(folderRows.map((folder) => [folder.id, folder.name]));
229003
+ const categoryNames = new Map(categoryRows.map((category) => [category.id, category.name]));
229004
+ return rows.map((row) => ({
229005
+ ...row,
229006
+ folderName: row.folderId ? folderNames.get(row.folderId) ?? null : null,
229007
+ categoryName: row.categoryId ? categoryNames.get(row.categoryId) ?? null : null
229008
+ }));
229009
+ }
228985
229010
  var documentRoutes = new Elysia({ prefix: "/api" }).get("/documents", async ({ query, set: set2, request }) => {
228986
229011
  const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip") ?? "unknown";
228987
229012
  const rl = await documentRateLimiter(ip, request);
@@ -229009,7 +229034,7 @@ var documentRoutes = new Elysia({ prefix: "/api" }).get("/documents", async ({ q
229009
229034
  }
229010
229035
  const { folderId, tag: tag2, page, limit } = parsed.data;
229011
229036
  const offset = (page - 1) * limit;
229012
- const cacheKey = `${docListKey(userId, folderId, tag2, page, limit, ctx.workspaceId)}:scope:${access.categoryId ?? "all"}`;
229037
+ const cacheKey = `${docListKey(userId, folderId, tag2, page, limit, ctx.workspaceId)}:scope:${access.categoryId ?? "all"}:response:v2`;
229013
229038
  try {
229014
229039
  return await cacheGetOrSet(cacheKey, 30, async () => {
229015
229040
  const conditions2 = [
@@ -229038,7 +229063,7 @@ var documentRoutes = new Elysia({ prefix: "/api" }).get("/documents", async ({ q
229038
229063
  ]);
229039
229064
  });
229040
229065
  return {
229041
- items: await withTags(ctx, rows2),
229066
+ items: await withDocumentLocationNames(ctx, await withTags(ctx, rows2)),
229042
229067
  total: countResult2[0]?.total ?? 0,
229043
229068
  page,
229044
229069
  limit
@@ -229060,7 +229085,7 @@ var documentRoutes = new Elysia({ prefix: "/api" }).get("/documents", async ({ q
229060
229085
  ]);
229061
229086
  });
229062
229087
  return {
229063
- items: await withTags(ctx, rows),
229088
+ items: await withDocumentLocationNames(ctx, await withTags(ctx, rows)),
229064
229089
  total: countResult[0]?.total ?? 0,
229065
229090
  page,
229066
229091
  limit
@@ -229231,9 +229256,9 @@ var documentRoutes = new Elysia({ prefix: "/api" }).get("/documents", async ({ q
229231
229256
  await acquireTenantTopologyLock(tx, ctx);
229232
229257
  }
229233
229258
  let categoryId = requestedCategoryId;
229234
- if (folderId && !categoryId) {
229235
- const resolvedCategoryId = await resolveFolderEffectiveCategory(tx, ctx, folderId);
229236
- if (resolvedCategoryId === undefined) {
229259
+ if (folderId) {
229260
+ const folderCategoryId = await resolveFolderEffectiveCategory(tx, ctx, folderId);
229261
+ if (folderCategoryId === undefined) {
229237
229262
  return {
229238
229263
  row: null,
229239
229264
  replayed: false,
@@ -229241,7 +229266,16 @@ var documentRoutes = new Elysia({ prefix: "/api" }).get("/documents", async ({ q
229241
229266
  forbidden: true
229242
229267
  };
229243
229268
  }
229244
- categoryId = resolvedCategoryId;
229269
+ if (access.restricted && !isAuthorizedCategory(access, folderCategoryId)) {
229270
+ return {
229271
+ row: null,
229272
+ replayed: false,
229273
+ conflict: false,
229274
+ forbidden: true
229275
+ };
229276
+ }
229277
+ if (!categoryId)
229278
+ categoryId = folderCategoryId;
229245
229279
  }
229246
229280
  if (!isAuthorizedCategory(access, categoryId)) {
229247
229281
  return {
@@ -229325,7 +229359,7 @@ var documentRoutes = new Elysia({ prefix: "/api" }).get("/documents", async ({ q
229325
229359
  revision: contentHash(created.title, created.content ?? ""),
229326
229360
  source: "interactive"
229327
229361
  }).catch((err) => logger3.warn({ err, documentId: created.id }, "Pipeline enqueue failed"));
229328
- invalidateDocListCache(userId);
229362
+ invalidateDocListCache(userId, ctx.workspaceId);
229329
229363
  set2.status = 201;
229330
229364
  const ipAddress = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip") ?? "";
229331
229365
  const userAgent = request.headers.get("user-agent") ?? "";
@@ -230051,7 +230085,7 @@ var documentRoutes = new Elysia({ prefix: "/api" }).get("/documents", async ({ q
230051
230085
  }
230052
230086
  await Promise.all([
230053
230087
  invalidateDocCache(params2.id),
230054
- invalidateDocListCache(userId)
230088
+ invalidateDocListCache(userId, ctx.workspaceId)
230055
230089
  ]);
230056
230090
  const ipAddress = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip") ?? "";
230057
230091
  const userAgent = request.headers.get("user-agent") ?? "";
@@ -230316,7 +230350,7 @@ var documentRoutes = new Elysia({ prefix: "/api" }).get("/documents", async ({ q
230316
230350
  revision: contentHash(copy3.title, copy3.content ?? ""),
230317
230351
  source: "interactive"
230318
230352
  }).catch((err) => logger3.warn({ err, documentId: copy3.id }, "Pipeline enqueue failed"));
230319
- invalidateDocListCache(userId);
230353
+ invalidateDocListCache(userId, ctx.workspaceId);
230320
230354
  set2.status = 201;
230321
230355
  return copy3;
230322
230356
  } catch (err) {
@@ -230386,7 +230420,7 @@ var documentRoutes = new Elysia({ prefix: "/api" }).get("/documents", async ({ q
230386
230420
  return { error: "Document not found" };
230387
230421
  }
230388
230422
  invalidateDocCache(params2.id);
230389
- invalidateDocListCache(ctx.userId);
230423
+ invalidateDocListCache(ctx.userId, ctx.workspaceId);
230390
230424
  return { success: true };
230391
230425
  }).delete("/trash/documents/:id", async ({ params: params2, request, set: set2 }) => {
230392
230426
  const access = await resolveContentAccess(request);
@@ -230496,7 +230530,7 @@ var documentRoutes = new Elysia({ prefix: "/api" }).get("/documents", async ({ q
230496
230530
  });
230497
230531
  }
230498
230532
  invalidateDocCache(params2.id);
230499
- invalidateDocListCache(ctx.userId);
230533
+ invalidateDocListCache(ctx.userId, ctx.workspaceId);
230500
230534
  return { success: true };
230501
230535
  } catch (error53) {
230502
230536
  if (error53 instanceof DocumentPurgeNotFoundError) {
@@ -230552,7 +230586,7 @@ var documentRoutes = new Elysia({ prefix: "/api" }).get("/documents", async ({ q
230552
230586
  return { error: "Document not found" };
230553
230587
  }
230554
230588
  invalidateDocCache(params2.id);
230555
- invalidateDocListCache(userId);
230589
+ invalidateDocListCache(userId, ctx.workspaceId);
230556
230590
  const ipAddress = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip") ?? "";
230557
230591
  const userAgent = request.headers.get("user-agent") ?? "";
230558
230592
  recordAuditEvent({
@@ -231397,7 +231431,7 @@ var folderRoutes = new Elysia({ prefix: "/api/folders" }).get("/:id", async ({ p
231397
231431
  }
231398
231432
  if (parsed.data.name !== undefined || parsed.data.parentId !== undefined || parsed.data.categoryId !== undefined) {
231399
231433
  dispatchMetadataReembedOutbox(result.operationId, metadataReembedPageSize("folder"));
231400
- invalidateDocListCache(userId);
231434
+ await invalidateDocListCache(userId, ctx.workspaceId);
231401
231435
  }
231402
231436
  return result.updated;
231403
231437
  } catch (err) {
@@ -231450,6 +231484,7 @@ var folderRoutes = new Elysia({ prefix: "/api/folders" }).get("/:id", async ({ p
231450
231484
  set2.status = 404;
231451
231485
  return { error: "Folder not found" };
231452
231486
  }
231487
+ await invalidateDocListCache(ctx.userId, ctx.workspaceId);
231453
231488
  dispatchMetadataReembedOutbox(deletion.operationId, metadataReembedPageSize("folder"));
231454
231489
  return { success: true };
231455
231490
  } catch (err) {
@@ -232059,50 +232094,6 @@ function stripQuotes2(value) {
232059
232094
  return trimmed;
232060
232095
  }
232061
232096
 
232062
- // ../../backend/src/api/routes/health.ts
232063
- function createHealthRoutes(dependencies) {
232064
- return new Elysia().get("/api/health", async ({ request, set: set2 }) => {
232065
- const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip") ?? "unknown";
232066
- const rateLimit = await dependencies.rateLimiter(ip, request);
232067
- set2.headers = {
232068
- "X-RateLimit-Remaining": String(rateLimit.remaining),
232069
- ...rateLimit.retryAfter ? { "Retry-After": String(rateLimit.retryAfter) } : {}
232070
- };
232071
- if (!rateLimit.allowed) {
232072
- set2.status = 429;
232073
- return { error: "Too many requests" };
232074
- }
232075
- const [databaseAvailable, redisAvailable, storageAvailable] = await Promise.all([
232076
- dependencies.databaseAvailable(),
232077
- dependencies.redisAvailable(),
232078
- dependencies.storageAvailable()
232079
- ]);
232080
- const queueAvailable = dependencies.queueAvailable();
232081
- const readiness = evaluatePipelineHealth({
232082
- databaseAvailable,
232083
- redisAvailable,
232084
- storageAvailable,
232085
- queueAvailable,
232086
- recoveryAvailable: true,
232087
- oldestInteractiveWaitMs: 0,
232088
- interactiveSloMs: Number.POSITIVE_INFINITY,
232089
- graphAvailable: true
232090
- });
232091
- if (readiness.status === "unhealthy")
232092
- set2.status = 503;
232093
- return {
232094
- status: readiness.status === "unhealthy" ? "unhealthy" : "ok",
232095
- service: "docsmint",
232096
- timestamp: new Date().toISOString(),
232097
- database: databaseAvailable ? "ok" : "error",
232098
- redis: redisAvailable ? "ok" : "error",
232099
- storage: storageAvailable ? "ok" : "error",
232100
- queue: queueAvailable ? "ok" : "error",
232101
- ...readiness.reasons.length ? { reasons: readiness.reasons } : {}
232102
- };
232103
- });
232104
- }
232105
-
232106
232097
  // ../../backend/src/api/routes/keys.ts
232107
232098
  init_schema2();
232108
232099
  init_drizzle_orm();
@@ -235959,6 +235950,53 @@ var webhookRoutes = new Elysia({ prefix: "/api/webhooks" }).post("/storage", asy
235959
235950
  return { received: true };
235960
235951
  });
235961
235952
 
235953
+ // ../../backend/src/api/register-routes.ts
235954
+ var selfHostedApiRoutes = new Elysia().use(authRoutes).use(tagRoutes).use(categoryRoutes).use(attachmentRoutes).use(shareRoutes).use(searchRoutes).use(documentRoutes).use(folderRoutes).use(versionRoutes).use(webhookRoutes).use(collaborationRoutes).use(graphRoutes).use(keysRoutes).use(pluginsRoutes).use(visibilityRoutes).use(adminRoutes).use(metricsRoutes);
235955
+
235956
+ // ../../backend/src/api/routes/health.ts
235957
+ function createHealthRoutes(dependencies) {
235958
+ return new Elysia().get("/api/health", async ({ request, set: set2 }) => {
235959
+ const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip") ?? "unknown";
235960
+ const rateLimit = await dependencies.rateLimiter(ip, request);
235961
+ set2.headers = {
235962
+ "X-RateLimit-Remaining": String(rateLimit.remaining),
235963
+ ...rateLimit.retryAfter ? { "Retry-After": String(rateLimit.retryAfter) } : {}
235964
+ };
235965
+ if (!rateLimit.allowed) {
235966
+ set2.status = 429;
235967
+ return { error: "Too many requests" };
235968
+ }
235969
+ const [databaseAvailable, redisAvailable, storageAvailable] = await Promise.all([
235970
+ dependencies.databaseAvailable(),
235971
+ dependencies.redisAvailable(),
235972
+ dependencies.storageAvailable()
235973
+ ]);
235974
+ const queueAvailable = dependencies.queueAvailable();
235975
+ const readiness = evaluatePipelineHealth({
235976
+ databaseAvailable,
235977
+ redisAvailable,
235978
+ storageAvailable,
235979
+ queueAvailable,
235980
+ recoveryAvailable: true,
235981
+ oldestInteractiveWaitMs: 0,
235982
+ interactiveSloMs: Number.POSITIVE_INFINITY,
235983
+ graphAvailable: true
235984
+ });
235985
+ if (readiness.status === "unhealthy")
235986
+ set2.status = 503;
235987
+ return {
235988
+ status: readiness.status === "unhealthy" ? "unhealthy" : "ok",
235989
+ service: "docsmint",
235990
+ timestamp: new Date().toISOString(),
235991
+ database: databaseAvailable ? "ok" : "error",
235992
+ redis: redisAvailable ? "ok" : "error",
235993
+ storage: storageAvailable ? "ok" : "error",
235994
+ queue: queueAvailable ? "ok" : "error",
235995
+ ...readiness.reasons.length ? { reasons: readiness.reasons } : {}
235996
+ };
235997
+ });
235998
+ }
235999
+
235962
236000
  // ../../backend/src/lib/api-key-owner.ts
235963
236001
  init_schema2();
235964
236002
  init_config();
@@ -238422,7 +238460,7 @@ var swaggerConfig = {
238422
238460
  },
238423
238461
  info: {
238424
238462
  title: "DocsMint API",
238425
- version: "0.8.7",
238463
+ version: "0.9.0",
238426
238464
  description: "Self-hosted AI-native knowledge workspace and installable PWA with hybrid search, GraphRAG, REST, SDK, CLI, and MCP access for people and AI agents.",
238427
238465
  contact: { name: "HiAi-gg", url: "https://github.com/HiAi-gg/docsmint" },
238428
238466
  license: {
@@ -238476,7 +238514,7 @@ var app = new Elysia().use(bodySizeLimit).onError(({ error: error53, set: set2 }
238476
238514
  redisAvailable: () => redis.ping().then(() => true, () => false),
238477
238515
  storageAvailable: () => storage.send(new ListBucketsCommand({})).then(() => true, () => false),
238478
238516
  queueAvailable: () => pipelineRuntime.workers.size === PIPELINE_STAGES.length && [...pipelineRuntime.workers.values()].every((worker2) => worker2.isRunning?.() === true)
238479
- })).use(csrfMiddleware).use(authMiddleware).use(authRoutes).use(tagRoutes).use(categoryRoutes).use(attachmentRoutes).use(shareRoutes).use(searchRoutes).use(documentRoutes).use(folderRoutes).use(versionRoutes).use(webhookRoutes).use(collaborationRoutes).use(graphRoutes).use(keysRoutes).use(pluginsRoutes).use(visibilityRoutes).use(adminRoutes).use(metricsRoutes);
238517
+ })).use(csrfMiddleware).use(authMiddleware).use(selfHostedApiRoutes);
238480
238518
  app.listen({
238481
238519
  port: config3.API_PORT,
238482
238520
  development: config3.NODE_ENV !== "production",
package/dist/client.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * failures (502 / 503 / 504 / timeout / network reset) are retried
8
8
  * with exponential backoff up to `config.retries` attempts.
9
9
  */
10
- import type { DocsApiKeyCreated, DocsApiKeyListResponse, DocsAttachment, DocsAttachmentConfirmInput, DocsAttachmentListResponse, DocsAttachmentPresignInput, DocsAttachmentPresignResponse, DocsCategory, DocsCategoryInput, DocsCategoryUpdate, DocsDocument, DocsDocumentCreateInput, DocsDocumentCursorPage, DocsDocumentIndexRefresh, DocsDocumentIndexStatus, DocsDocumentKnowledgeSummary, DocsDocumentListResponse, DocsDocumentPipeline, DocsPipelineWarningRetry, DocsDocumentUpdateInput, DocsFolder, DocsFolderCreateInput, DocsFolderUpdateInput, DocsGraphEntitiesResponse, DocsGraphRelatedResponse, DocsGraphSearchResponse, DocsHealthResponse, DocsRequestContext, DocsSearchOptions, DocsSearchResponse, DocsSearchSuggestItem, DocsSharedContent, DocsShareLink, DocsShareListResponse, DocsShareRole, DocsTag, DocsVersion, DocsVersionDiff } from "./types.js";
10
+ import type { DocsApiKeyCreated, DocsApiKeyListResponse, DocsAttachment, DocsAttachmentConfirmInput, DocsAttachmentListResponse, DocsAttachmentPresignInput, DocsAttachmentPresignResponse, DocsCategory, DocsCategoryListItem, DocsCategoryInput, DocsCategoryUpdate, DocsDocument, DocsDocumentCreateInput, DocsDocumentCursorPage, DocsDocumentIndexRefresh, DocsDocumentIndexStatus, DocsDocumentKnowledgeSummary, DocsDocumentListResponse, DocsDocumentPipeline, DocsPipelineWarningRetry, DocsDocumentUpdateInput, DocsFolder, DocsFolderCreateInput, DocsFolderUpdateInput, DocsGraphEntitiesResponse, DocsGraphRelatedResponse, DocsGraphSearchResponse, DocsHealthResponse, DocsRequestContext, DocsSearchOptions, DocsSearchResponse, DocsSearchSuggestItem, DocsSharedContent, DocsShareLink, DocsShareListResponse, DocsShareRole, DocsTag, DocsVersion, DocsVersionDiff } from "./types.js";
11
11
  export interface DocsClientConfig {
12
12
  /** Base URL of the DocsMint API, e.g. `http://localhost:50700`. */
13
13
  baseUrl: string;
@@ -126,7 +126,7 @@ export declare class DocsClient {
126
126
  deleteTag(id: string, context?: DocsRequestContext): Promise<void>;
127
127
  addTagToDoc(documentId: string, tagId: string, context?: DocsRequestContext): Promise<void>;
128
128
  removeTagFromDoc(documentId: string, tagId: string, context?: DocsRequestContext): Promise<void>;
129
- listCategories(context?: DocsRequestContext): Promise<DocsCategory[]>;
129
+ listCategories(context?: DocsRequestContext): Promise<DocsCategoryListItem[]>;
130
130
  createCategory(input: DocsCategoryInput, context?: DocsRequestContext): Promise<DocsCategory>;
131
131
  updateCategory(id: string, updates: DocsCategoryUpdate, context?: DocsRequestContext): Promise<DocsCategory>;
132
132
  deleteCategory(id: string, context?: DocsRequestContext): Promise<void>;