@hiai-gg/docsmint 0.5.8 → 0.6.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.
Files changed (45) hide show
  1. package/LICENSE +39 -8
  2. package/README.md +67 -43
  3. package/dist/backend/index.js +53 -14
  4. package/dist/client.d.ts +3 -1
  5. package/dist/client.js +2 -0
  6. package/dist/frontend/{CategoryDialog-BYAFjX7E.js → CategoryDialog-CbXBMBZB.js} +3 -3
  7. package/dist/frontend/HiAiEditor-DSVG3GNd.js +2986 -0
  8. package/dist/frontend/SettingsDialog-BDNgf5rv.js +969 -0
  9. package/dist/frontend/ShareDialog-2Mty7hm4.js +258 -0
  10. package/dist/frontend/{Sidebar-B-zCJT9i.js → Sidebar-DX08utdV.js} +15 -15
  11. package/dist/frontend/app-shell.js +122 -65
  12. package/dist/frontend/components/category-dialog.js +1 -1
  13. package/dist/frontend/components/editor/compact-editor.d.ts +3 -0
  14. package/dist/frontend/components/editor/compact-editor.js +36 -0
  15. package/dist/frontend/components/editor/document-editor.js +2 -2986
  16. package/dist/frontend/components/settings.js +1 -1
  17. package/dist/frontend/components/share-dialog.d.ts +2 -1
  18. package/dist/frontend/components/share-dialog.js +1 -1
  19. package/dist/frontend/components/sidebar.js +1 -1
  20. package/dist/frontend/dashboard.js +1 -1
  21. package/dist/frontend/editor-preferences.d.ts +4 -0
  22. package/dist/frontend/editor-preferences.js +2 -0
  23. package/dist/frontend/editor-preferences.svelte-Bt_A9k7n.js +61 -0
  24. package/dist/frontend/frontend.css +1 -1
  25. package/dist/frontend-ssr/app-shell.js +46 -2
  26. package/dist/frontend-ssr/assets/{CategoryDialog-BLxVoynx.js → CategoryDialog-QHEv_9N4.js} +4 -4
  27. package/dist/frontend-ssr/assets/HiAiEditor-CUOJmKzT.js +2358 -0
  28. package/dist/frontend-ssr/assets/{SettingsDialog-DVp0OxPO.js → SettingsDialog-BjJIiVLc.js} +2 -64
  29. package/dist/frontend-ssr/assets/{ShareDialog-BZd28sg-.js → ShareDialog-C9C-XTm4.js} +1 -1
  30. package/dist/frontend-ssr/assets/{Sidebar-CO9LZRPa.js → Sidebar-C2sUfGNk.js} +18 -18
  31. package/dist/frontend-ssr/assets/editor-preferences.svelte-iI5jJM1x.js +65 -0
  32. package/dist/frontend-ssr/components/category-dialog.js +1 -1
  33. package/dist/frontend-ssr/components/editor/compact-editor.js +19 -0
  34. package/dist/frontend-ssr/components/editor/document-editor.js +1 -2357
  35. package/dist/frontend-ssr/components/settings.js +1 -1
  36. package/dist/frontend-ssr/components/share-dialog.js +1 -1
  37. package/dist/frontend-ssr/components/sidebar.js +1 -1
  38. package/dist/frontend-ssr/dashboard.js +1 -1
  39. package/dist/frontend-ssr/editor-preferences.js +2 -0
  40. package/dist/types.d.ts +5 -0
  41. package/package.json +16 -2
  42. package/packages/cli/src/index.ts +1 -1
  43. package/packages/mcp-server/src/index.ts +1 -1
  44. package/dist/frontend/SettingsDialog-CyZSOjLE.js +0 -1024
  45. package/dist/frontend/ShareDialog-BC-Jl6v1.js +0 -257
package/LICENSE CHANGED
@@ -1,4 +1,5 @@
1
- Apache License
1
+
2
+ Apache License
2
3
  Version 2.0, January 2004
3
4
  http://www.apache.org/licenses/
4
5
 
@@ -34,7 +35,8 @@ Apache License
34
35
 
35
36
  "Work" shall mean the work of authorship, whether in Source or
36
37
  Object form, made available under the License, as indicated by a
37
- copyright notice that is included in or attached to the work.
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
38
40
 
39
41
  "Derivative Works" shall mean any work, whether in Source or Object
40
42
  form, that is based on (or derived from) the Work and for which the
@@ -90,16 +92,17 @@ Apache License
90
92
  modifications, and in Source or Object form, provided that You
91
93
  meet the following conditions:
92
94
 
93
- (a) You must give any other recipients of the Work or Derivative Works
94
- a copy of this License; and
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
95
97
 
96
98
  (b) You must cause any modified files to carry prominent notices
97
99
  stating that You changed the files; and
98
100
 
99
- (c) You must retain, in the Source form of any Derivative Works that
100
- You distribute, all copyright, patent, trademark, and attribution
101
- notices from the Source form of the Work, excluding those notices
102
- that do not pertain to any part of the Derivative Works; and
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
103
106
 
104
107
  (d) If the Work includes a "NOTICE" text file as part of its
105
108
  distribution, then any Derivative Works that You distribute must
@@ -129,6 +132,9 @@ Apache License
129
132
  any Contribution intentionally submitted for inclusion in the Work
130
133
  by You to the Licensor shall be under the terms and conditions of
131
134
  this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
132
138
 
133
139
  6. Trademarks. This License does not grant permission to use the trade
134
140
  names, trademarks, service marks, or product names of the Licensor,
@@ -169,3 +175,28 @@ Apache License
169
175
  of your accepting any such warranty or additional liability.
170
176
 
171
177
  END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # DocsMint
2
2
 
3
- **A self-hosted, AI-native knowledge workspace for people, applications, and agents.**
3
+ **A self-hosted AI-native knowledge workspace and installable PWA for people,
4
+ applications, and AI agents.**
4
5
 
5
6
  DocsMint stores documents in a structured JSON editor model first. Markdown is
6
7
  the convenient second format for editing, importing, and exporting content.
@@ -38,24 +39,36 @@ server.
38
39
  - **Own the full stack**: application data, vectors, graph, queue, and files run
39
40
  on infrastructure you control.
40
41
 
41
- ## What is new in the 0.3.0 release line?
42
-
43
- - **Installable Progressive Web App (PWA)** with a manifest, service worker,
44
- responsive mobile shell, install prompt, update safety, and an offline
45
- fallback shell.
46
- - **Offline document access** through identity-partitioned IndexedDB/Dexie
47
- snapshots. Offline mode is cached read plus explicit local drafts; reconnect
48
- never silently replays mutations.
49
- - **Explicit local drafts** with local autosave, review/apply, optimistic
50
- concurrency (`expectedUpdatedAt`), and actionable conflict handling.
51
- - **Composable self-hosted UI** with typed dashboard/search hosts and extension
52
- slots.
53
- - **Mobile-first editor polish** including a single responsive sidebar, safe
54
- PWA updates, raw Markdown auto-height, and accessible narrow-screen controls.
55
-
56
- Search terms: installable self-hosted PWA knowledge base, offline document
57
- reading, local drafts, semantic multilingual search, GraphRAG, MCP, TypeScript
58
- SDK, CLI, and frontend extension hosts.
42
+ ## What's new in DocsMint 0.6.0?
43
+
44
+ - **Installable, update-safe PWA.** A versioned service worker, responsive app
45
+ shell, install prompt, and deterministic offline fallback make DocsMint feel
46
+ at home on desktop and mobile browsers.
47
+ - **Private offline reading and explicit drafts.** Identity-partitioned local
48
+ snapshots keep selected documents readable offline. Drafts autosave locally,
49
+ then return through a deliberate review-and-apply flow after reconnecting;
50
+ DocsMint never silently replays offline mutations.
51
+ - **Touch-safe mobile navigation.** Sidebar document actions, Copy controls, and
52
+ overflow menus remain reachable on touch devices without depending on hover,
53
+ with accessible targets and narrow-screen layouts.
54
+ - **A flexible editor contract.** Reactive visual/Markdown mode preferences,
55
+ Minimal toolbar support, and a reusable compact TipTap editor let hosts match
56
+ the writing surface to the task without forking editor internals.
57
+ - **Stable document discovery.** The public API and TypeScript SDK provide
58
+ deterministic cursor pagination and global sorting by title, category,
59
+ folder, or update time, with canonical category and folder metadata.
60
+ - **Clear public and restricted sharing.** Share creation exposes the resulting
61
+ URL for both public links and restricted invitation workflows, with an
62
+ explicit Copy action that works on narrow screens.
63
+ - **Knowledge built for agents.** REST, the typed SDK, CLI, and MCP server expose
64
+ scoped document operations, while multilingual hybrid search and GraphRAG
65
+ help people and AI agents retrieve connected knowledge instead of isolated
66
+ keyword matches.
67
+
68
+ DocsMint combines an installable self-hosted PWA knowledge base, mobile document
69
+ workspace, offline reading, local drafts, multilingual semantic search,
70
+ GraphRAG, MCP, a TypeScript SDK, CLI, and composable frontend hosts in one
71
+ open-source system.
59
72
 
60
73
  For self-hosted customization, see [PWA hosting](docs/PWA_HOSTING.md) and
61
74
  [extension points](docs/EXTENDING.md).
@@ -96,6 +109,7 @@ git clone https://github.com/HiAi-gg/docsmint.git
96
109
  cd docsmint
97
110
  bash scripts/quickstart.sh
98
111
  ```
112
+
99
113
  On its first run, the script creates an ignored root `.env`, generates the
100
114
  database, authentication, and storage secrets, builds the PostgreSQL image,
101
115
  applies migrations, and starts the complete application.
@@ -105,12 +119,14 @@ For OpenRouter, add one value to `.env` and run the script again:
105
119
  ```dotenv
106
120
  OPENROUTER_API_KEY=sk-or-your-key
107
121
  ```
122
+
108
123
  For Ollama, select the local provider instead:
109
124
 
110
125
  ```dotenv
111
126
  AI_PROVIDER=ollama
112
127
  OLLAMA_PORT=11434
113
128
  ```
129
+
114
130
  Then make sure the configured local models are available:
115
131
 
116
132
  ```bash
@@ -118,6 +134,7 @@ ollama pull bge-m3
118
134
  ollama pull qwen3:8b
119
135
  bash scripts/quickstart.sh
120
136
  ```
137
+
121
138
  Open **http://localhost:50701**. The API health endpoint is
122
139
  **http://localhost:50700/api/health**.
123
140
 
@@ -133,14 +150,14 @@ Open **http://localhost:50701**. The API health endpoint is
133
150
 
134
151
  The canonical local ports are:
135
152
 
136
- | Service | Port |
137
- |---|---:|
138
- | Web application | `50701` |
139
- | REST API | `50700` |
140
- | PostgreSQL | `5437` |
141
- | Redis | `6384` |
153
+ | Service | Port |
154
+ | -------------------- | ------: |
155
+ | Web application | `50701` |
156
+ | REST API | `50700` |
157
+ | PostgreSQL | `5437` |
158
+ | Redis | `6384` |
142
159
  | SeaweedFS S3 gateway | `50702` |
143
- | SeaweedFS filer UI | `50703` |
160
+ | SeaweedFS filer UI | `50703` |
144
161
 
145
162
  See [Deployment](docs/DEPLOYMENT.md) for domains, TLS, provider tuning,
146
163
  backups, and production operation.
@@ -165,6 +182,7 @@ bunx --package @hiai-gg/docsmint docsmint read <document-id>
165
182
  bunx --package @hiai-gg/docsmint docsmint create \
166
183
  --title "Release notes" --content "# Version 0.2.9"
167
184
  ```
185
+
168
186
  Credentials can also be supplied through `HIAI_DOCS_URL` and
169
187
  `HIAI_DOCS_API_KEY`. See the [CLI guide](packages/cli/README.md) for every
170
188
  command and configuration precedence.
@@ -188,11 +206,13 @@ snapshots, history, and export as MCP tools.
188
206
  }
189
207
  }
190
208
  ```
209
+
191
210
  Run the server directly to verify the installation:
192
211
 
193
212
  ```bash
194
213
  bunx --package @hiai-gg/docsmint docsmint-mcp
195
214
  ```
215
+
196
216
  The server uses stdio and works with MCP-capable clients such as Claude
197
217
  Desktop, Cursor, and coding agents that accept standard MCP configuration. See
198
218
  the [MCP guide](packages/mcp-server/README.md) for its ten tools and routes.
@@ -211,22 +231,24 @@ the CLI, SDK, or REST API.
211
231
  ```bash
212
232
  bun add @hiai-gg/docsmint
213
233
  ```
234
+
214
235
  ```ts
215
- import { DocsClient } from "@hiai-gg/docsmint";
236
+ import { DocsClient } from '@hiai-gg/docsmint';
216
237
 
217
238
  const docs = new DocsClient({
218
- baseUrl: "http://localhost:50700",
239
+ baseUrl: 'http://localhost:50700',
219
240
  apiKey: process.env.HIAI_DOCS_API_KEY,
220
241
  });
221
242
 
222
243
  const created = await docs.createDoc({
223
- title: "Meeting notes",
224
- content: "# Agenda",
244
+ title: 'Meeting notes',
245
+ content: '# Agenda',
225
246
  });
226
247
 
227
- const results = await docs.search("what did we decide?");
248
+ const results = await docs.search('what did we decide?');
228
249
  console.log(created.id, results.items);
229
250
  ```
251
+
230
252
  The SDK is a typed `fetch` client with retries for transient failures. See the
231
253
  [SDK reference](packages/sdk/README.md) and [REST API](docs/API.md).
232
254
 
@@ -234,11 +256,11 @@ The SDK is a typed `fetch` client with retries for transient failures. See the
234
256
 
235
257
  Create and revoke integration keys from **Settings → API**.
236
258
 
237
- | Credential | Intended use | Access |
238
- |---|---|---|
239
- | Global API key | Trusted owner-wide CLI, MCP, SDK, or service | All owner content |
240
- | Category key | Least-privilege agent or product integration | One category with selected permissions |
241
- | Operator key | Administration and reindex operations | `/api/admin/*` only |
259
+ | Credential | Intended use | Access |
260
+ | -------------- | -------------------------------------------- | -------------------------------------- |
261
+ | Global API key | Trusted owner-wide CLI, MCP, SDK, or service | All owner content |
262
+ | Category key | Least-privilege agent or product integration | One category with selected permissions |
263
+ | Operator key | Administration and reindex operations | `/api/admin/*` only |
242
264
 
243
265
  Category permissions are explicit and non-hierarchical:
244
266
 
@@ -263,6 +285,7 @@ packages/cli/ Terminal client
263
285
  packages/mcp-server/ MCP stdio server
264
286
  postgres/ PostgreSQL image with vector and graph extensions
265
287
  ```
288
+
266
289
  The Docker deployment runs:
267
290
 
268
291
  - **Web** — document editor, folders, categories, sharing, settings, and search;
@@ -307,13 +330,13 @@ For pipeline internals and tuning, see [Architecture](docs/ARCHITECTURE.md) and
307
330
  DocsMint overlaps with several excellent open-source knowledge tools, but its
308
331
  focus is a compact knowledge runtime shared equally by humans and agents.
309
332
 
310
- | Project | Primary strength | Difference from DocsMint |
311
- |---|---|---|
312
- | [Outline](https://github.com/outline/outline) | Polished team wiki and collaboration | DocsMint emphasizes built-in retrieval, GraphRAG, scoped agent access, CLI, and MCP |
313
- | [Docmost](https://github.com/docmost/docmost) | Collaborative wiki and real-time editing | DocsMint centers automatic embeddings and agent-facing integration surfaces |
314
- | [AppFlowy](https://github.com/AppFlowy-IO/AppFlowy) | Broad local-first productivity workspace | DocsMint is narrower: a self-hosted document and retrieval service |
315
- | [AnythingLLM](https://github.com/Mintplex-Labs/anything-llm) | Chat-oriented RAG over imported sources | DocsMint starts with the editable knowledge base and exposes it to many clients |
316
- | [Danswer](https://github.com/danswer-ai/danswer) / Onyx | Enterprise search across external connectors | DocsMint owns and edits its native corpus rather than primarily indexing other systems |
333
+ | Project | Primary strength | Difference from DocsMint |
334
+ | ------------------------------------------------------------ | -------------------------------------------- | -------------------------------------------------------------------------------------- |
335
+ | [Outline](https://github.com/outline/outline) | Polished team wiki and collaboration | DocsMint emphasizes built-in retrieval, GraphRAG, scoped agent access, CLI, and MCP |
336
+ | [Docmost](https://github.com/docmost/docmost) | Collaborative wiki and real-time editing | DocsMint centers automatic embeddings and agent-facing integration surfaces |
337
+ | [AppFlowy](https://github.com/AppFlowy-IO/AppFlowy) | Broad local-first productivity workspace | DocsMint is narrower: a self-hosted document and retrieval service |
338
+ | [AnythingLLM](https://github.com/Mintplex-Labs/anything-llm) | Chat-oriented RAG over imported sources | DocsMint starts with the editable knowledge base and exposes it to many clients |
339
+ | [Danswer](https://github.com/danswer-ai/danswer) / Onyx | Enterprise search across external connectors | DocsMint owns and edits its native corpus rather than primarily indexing other systems |
317
340
 
318
341
  This is a product-positioning summary, not a claim that every listed project
319
342
  lacks a feature. Check each project's current documentation when choosing a
@@ -340,6 +363,7 @@ bun run typecheck
340
363
  bun run test
341
364
  bun run build
342
365
  ```
366
+
343
367
  Read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request. Please
344
368
  report vulnerabilities through [SECURITY.md](SECURITY.md), not a public issue.
345
369
 
@@ -194469,11 +194469,17 @@ async function reembedDocsInFolder(folderId, ownerId, workspaceId) {
194469
194469
  return enqueued;
194470
194470
  }
194471
194471
  async function reembedDocsInFolderAdmin(folderId) {
194472
- const limit = config3.FOLDER_REEMBED_BATCH_SIZE;
194473
- const rows = await withTenant(REEMBED_ADMIN_TENANT, (tx) => {
194472
+ return reembedDocsInFolderAdminWith(folderId, loadAdminFolderDocumentIds);
194473
+ }
194474
+ async function loadAdminFolderDocumentIds(folderId, limit) {
194475
+ return withTenant(REEMBED_ADMIN_TENANT, (tx) => {
194474
194476
  const query = tx.select({ id: documents.id }).from(documents).where(eq(documents.folderId, folderId));
194475
194477
  return limit > 0 ? query.limit(limit) : query;
194476
194478
  });
194479
+ }
194480
+ async function reembedDocsInFolderAdminWith(folderId, loadRows) {
194481
+ const limit = config3.FOLDER_REEMBED_BATCH_SIZE;
194482
+ const rows = await loadRows(folderId, limit);
194477
194483
  const enqueued = await enqueueReembed(rows.map((r4) => r4.id));
194478
194484
  if (enqueued > 0) {
194479
194485
  logger3.info({ folderId, enqueued, limit, scope: "admin" }, "Re-embedding documents after admin folder reindex");
@@ -227113,7 +227119,9 @@ var listQuerySchema2 = exports_external2.object({
227113
227119
  var cursorListQuerySchema = exports_external2.object({
227114
227120
  categoryId: exports_external2.string().uuid().optional(),
227115
227121
  cursor: exports_external2.string().min(1).optional(),
227116
- limit: exports_external2.coerce.number().int().min(1).max(100).default(50)
227122
+ limit: exports_external2.coerce.number().int().min(1).max(100).default(50),
227123
+ sortBy: exports_external2.enum(["title", "category", "folder", "updated"]).default("updated"),
227124
+ sortOrder: exports_external2.enum(["asc", "desc"]).default("desc")
227117
227125
  });
227118
227126
  function cursorScopeHash(input) {
227119
227127
  return Buffer.from(new Bun.CryptoHasher("sha256").update(input).digest("hex"), "utf8").toString("base64url");
@@ -227128,7 +227136,7 @@ function decodeDocumentCursor(value, expectedScopeHash) {
227128
227136
  if (!cursor || typeof cursor !== "object")
227129
227137
  throw new Error("Malformed cursor");
227130
227138
  const candidate = cursor;
227131
- if (candidate.v !== 1 || typeof candidate.updatedAt !== "string" || Number.isNaN(Date.parse(candidate.updatedAt)) || typeof candidate.id !== "string" || typeof candidate.scopeHash !== "string" || candidate.scopeHash !== expectedScopeHash) {
227139
+ if (candidate.v !== 2 || !["title", "category", "folder", "updated"].includes(String(candidate.sortBy)) || !["asc", "desc"].includes(String(candidate.sortOrder)) || candidate.value !== null && typeof candidate.value !== "string" || typeof candidate.id !== "string" || typeof candidate.scopeHash !== "string" || candidate.scopeHash !== expectedScopeHash) {
227132
227140
  throw new Error("Cursor does not match this listing scope");
227133
227141
  }
227134
227142
  return candidate;
@@ -227372,12 +227380,20 @@ var documentRoutes = new Elysia({ prefix: "/api" }).get("/documents", async ({ q
227372
227380
  set2.status = 400;
227373
227381
  return { error: "Invalid query", details: parsed.error.flatten() };
227374
227382
  }
227375
- const { categoryId, cursor: encodedCursor, limit } = parsed.data;
227383
+ const {
227384
+ categoryId,
227385
+ cursor: encodedCursor,
227386
+ limit,
227387
+ sortBy,
227388
+ sortOrder
227389
+ } = parsed.data;
227376
227390
  const effectiveCategoryId = access.restricted ? access.categoryId ?? categoryId : categoryId;
227377
227391
  const scopeHash = cursorScopeHash(JSON.stringify({
227378
227392
  workspaceId: ctx.workspaceId ?? null,
227379
227393
  actorUserId: access.userId,
227380
- categoryId: effectiveCategoryId ?? null
227394
+ categoryId: effectiveCategoryId ?? null,
227395
+ sortBy,
227396
+ sortOrder
227381
227397
  }));
227382
227398
  let cursor;
227383
227399
  if (encodedCursor) {
@@ -227390,31 +227406,54 @@ var documentRoutes = new Elysia({ prefix: "/api" }).get("/documents", async ({ q
227390
227406
  };
227391
227407
  }
227392
227408
  }
227409
+ const sortValue = sortBy === "title" ? sql`lower(${documents.title})` : sortBy === "category" ? sql`lower(${categories.name})` : sortBy === "folder" ? sql`lower(${folders.name})` : sql`${documents.updatedAt}`;
227393
227410
  const conditions2 = [
227394
227411
  tenantOwnerCondition(documents.ownerId, documents.workspaceId, ctx),
227395
227412
  isNull(documents.deletedAt),
227396
- ...effectiveCategoryId ? [eq(documents.categoryId, effectiveCategoryId)] : [],
227397
- ...cursor ? [
227398
- or(lt(documents.updatedAt, new Date(cursor.updatedAt)), and(eq(documents.updatedAt, new Date(cursor.updatedAt)), lt(documents.id, cursor.id)))
227399
- ] : []
227413
+ ...effectiveCategoryId ? [eq(documents.categoryId, effectiveCategoryId)] : []
227400
227414
  ];
227415
+ if (cursor) {
227416
+ const idAfter = sortOrder === "asc" ? gt(documents.id, cursor.id) : lt(documents.id, cursor.id);
227417
+ if (sortBy === "updated" && cursor.value) {
227418
+ const cursorDate = new Date(cursor.value);
227419
+ const valueAfter = sortOrder === "asc" ? gt(documents.updatedAt, cursorDate) : lt(documents.updatedAt, cursorDate);
227420
+ const cursorCondition = or(valueAfter, and(eq(documents.updatedAt, cursorDate), idAfter));
227421
+ if (cursorCondition)
227422
+ conditions2.push(cursorCondition);
227423
+ } else if (cursor.value === null) {
227424
+ const cursorCondition = and(isNull(sortValue), idAfter);
227425
+ if (cursorCondition)
227426
+ conditions2.push(cursorCondition);
227427
+ } else {
227428
+ const valueAfter = sortOrder === "asc" ? gt(sortValue, cursor.value) : lt(sortValue, cursor.value);
227429
+ const cursorCondition = or(valueAfter, and(eq(sortValue, cursor.value), idAfter), isNull(sortValue));
227430
+ if (cursorCondition)
227431
+ conditions2.push(cursorCondition);
227432
+ }
227433
+ }
227401
227434
  try {
227402
227435
  const rows = await withTenant(ctx, (tx) => tx.select({
227403
227436
  id: documents.id,
227404
227437
  title: documents.title,
227405
227438
  content: sql`LEFT(${documents.content}, 200)`.as("content"),
227406
227439
  folderId: documents.folderId,
227440
+ folderName: folders.name,
227441
+ categoryId: documents.categoryId,
227442
+ categoryName: categories.name,
227407
227443
  createdAt: documents.createdAt,
227408
227444
  updatedAt: documents.updatedAt
227409
- }).from(documents).where(and(...conditions2)).orderBy(desc(documents.updatedAt), desc(documents.id)).limit(limit + 1));
227445
+ }).from(documents).leftJoin(folders, eq(folders.id, documents.folderId)).leftJoin(categories, eq(categories.id, documents.categoryId)).where(and(...conditions2)).orderBy(sortOrder === "asc" ? sql`${sortValue} asc nulls last` : sql`${sortValue} desc nulls last`, sortOrder === "asc" ? asc(documents.id) : desc(documents.id)).limit(limit + 1));
227410
227446
  const hasMore = rows.length > limit;
227411
227447
  const page = rows.slice(0, limit);
227412
227448
  const last2 = page.at(-1);
227449
+ const lastValue = last2 ? sortBy === "title" ? last2.title.toLocaleLowerCase() : sortBy === "category" ? last2.categoryName?.toLocaleLowerCase() ?? null : sortBy === "folder" ? last2.folderName?.toLocaleLowerCase() ?? null : new Date(last2.updatedAt).toISOString() : null;
227413
227450
  return {
227414
227451
  items: await withTags(ctx, page),
227415
227452
  nextCursor: hasMore && last2 ? encodeDocumentCursor({
227416
- v: 1,
227417
- updatedAt: new Date(last2.updatedAt).toISOString(),
227453
+ v: 2,
227454
+ sortBy,
227455
+ sortOrder,
227456
+ value: lastValue,
227418
227457
  id: last2.id,
227419
227458
  scopeHash
227420
227459
  }) : null
@@ -234280,7 +234319,7 @@ var swaggerConfig = {
234280
234319
  },
234281
234320
  info: {
234282
234321
  title: "DocsMint API",
234283
- version: "0.5.8",
234322
+ version: "0.6.0",
234284
234323
  description: "Self-hosted AI-first documentation platform. Full-text + semantic search, version history, sharing, and folder organization.",
234285
234324
  contact: { name: "HiAi-gg", url: "https://github.com/HiAi-gg/docsmint" },
234286
234325
  license: {
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, DocsDocumentListResponse, DocsDocumentCursorPage, DocsDocumentPipeline, 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, DocsCategoryInput, DocsCategoryUpdate, DocsDocument, DocsDocumentCreateInput, DocsDocumentCursorPage, DocsDocumentListResponse, DocsDocumentPipeline, 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 hiai-docs API, e.g. `http://localhost:50700`. */
13
13
  baseUrl: string;
@@ -76,6 +76,8 @@ export declare class DocsClient {
76
76
  categoryId?: string;
77
77
  cursor?: string;
78
78
  limit?: number;
79
+ sortBy?: import("./types").DocsDocumentSortField;
80
+ sortOrder?: import("./types").DocsSortOrder;
79
81
  }, context?: DocsRequestContext): Promise<DocsDocumentCursorPage>;
80
82
  duplicateDoc(id: string, context?: DocsRequestContext): Promise<DocsDocument>;
81
83
  getDocumentPipeline(id: string, context?: DocsRequestContext): Promise<DocsDocumentPipeline>;
package/dist/client.js CHANGED
@@ -112,6 +112,8 @@ export class DocsClient {
112
112
  categoryId: options.categoryId,
113
113
  cursor: options.cursor,
114
114
  limit: options.limit,
115
+ sortBy: options.sortBy,
116
+ sortOrder: options.sortOrder,
115
117
  }),
116
118
  }, context);
117
119
  }
@@ -26,7 +26,7 @@ function S(e, t) {
26
26
  }
27
27
  //#endregion
28
28
  //#region src/lib/components/sidebar/CategoryDialog.svelte
29
- var _e = x.from_html("<span class=\"font-medium text-foreground\"> </span> ", 1), ve = x.from_html("Delete <span class=\"font-medium text-foreground\"> </span> ", 1), C = x.from_html("<!> <!>", 1), ye = x.from_html("<div class=\"flex items-start gap-3 rounded-lg border border-primary/30 bg-primary/10 p-4\" role=\"status\" aria-live=\"polite\"><!> <div><p class=\"text-sm font-medium text-foreground\"> </p> <p class=\"text-sm text-muted-foreground\"><span class=\"font-medium text-foreground\"> </span> </p></div></div>"), be = x.from_html("<!> Copy API key", 1), xe = x.from_html("<div class=\"rounded-md border border-primary/20 bg-background/80 p-3\"><p class=\"text-xs font-medium text-foreground\">Copy the API key now. It remains available in Settings → API.</p> <code class=\"mt-2 block break-all rounded bg-muted p-2 text-xs text-foreground\"> </code> <!></div>"), w = x.from_html("<p class=\"text-xs text-destructive\" role=\"alert\"> </p>"), Se = x.from_html("<div class=\"space-y-3 rounded-lg border border-primary/30 bg-primary/10 p-4\" role=\"status\" aria-live=\"polite\"><div class=\"flex items-start gap-3\"><!> <div><p class=\"text-sm font-medium text-foreground\">Category created</p> <p class=\"text-sm text-muted-foreground\"> </p></div></div> <!> <!></div>"), Ce = x.from_html("<p id=\"category-dialog-name-error\" class=\"text-xs text-destructive\" role=\"alert\"> </p>"), T = x.from_html("<!> <!> <!>", 1), we = x.from_html("<div class=\"space-y-3 rounded-md border border-border/70 bg-muted/30 p-3\"><div class=\"flex flex-wrap items-center justify-between gap-2\"><!> <div class=\"flex flex-wrap gap-2\"><!> <!></div></div> <div class=\"grid gap-2 sm:grid-cols-3\"><label class=\"flex items-center gap-2 rounded-md border border-border/60 bg-background px-3 py-2 text-sm\"><input type=\"checkbox\" class=\"size-4 rounded border-input\"/> <span>Read</span></label> <label class=\"flex items-center gap-2 rounded-md border border-border/60 bg-background px-3 py-2 text-sm\"><input type=\"checkbox\" class=\"size-4 rounded border-input\"/> <span>Edit</span></label> <label class=\"flex items-center gap-2 rounded-md border border-border/60 bg-background px-3 py-2 text-sm\"><input type=\"checkbox\" class=\"size-4 rounded border-input\"/> <span>Write</span></label></div></div>"), Te = x.from_html("<div class=\"rounded-lg border border-primary/30 bg-primary/10 p-3 text-primary\"><p class=\"text-xs font-medium\">Copy now — this raw key is shown once.</p> <code class=\"mt-1 block break-all rounded bg-background/80 p-2 text-xs text-foreground\"> </code> <!></div>"), Ee = x.from_html("<div class=\"flex items-center justify-between gap-3 rounded-md bg-muted/40 px-3 py-2 text-xs\"><span> </span> <div class=\"flex shrink-0 gap-2\"><!> <!></div></div>"), De = x.from_html("<div class=\"space-y-3 rounded-md border border-border/70 p-3\"><div class=\"flex items-center justify-between gap-2\"><div><!><p class=\"text-xs text-muted-foreground\">The current permissions are saved before the key is issued.</p></div> <!></div> <!> <div class=\"max-h-48 space-y-2 overflow-y-auto pr-1\"></div></div>"), Oe = x.from_html("<form class=\"space-y-4\"><div class=\"space-y-2\"><!> <!> <!></div> <div class=\"space-y-2\"><!> <!></div> <!> <!></form>"), ke = x.from_html("<!> ", 1);
29
+ var _e = x.from_html("<span class=\"font-medium text-foreground\"> </span> ", 1), ve = x.from_html("Delete <span class=\"font-medium text-foreground\"> </span> ", 1), C = x.from_html("<!> <!>", 1), ye = x.from_html("<div class=\"flex items-start gap-3 rounded-lg border border-primary/30 bg-primary/10 p-4\" role=\"status\" aria-live=\"polite\"><!> <div><p class=\"text-sm font-medium text-foreground\"> </p> <p class=\"text-sm text-muted-foreground\"><span class=\"font-medium text-foreground\"> </span> </p></div></div>"), be = x.from_html("<!> Copy API key", 1), xe = x.from_html("<div class=\"rounded-md border border-primary/20 bg-background/80 p-3\"><p class=\"text-xs font-medium text-foreground\">Copy the API key now. It remains available in Settings → API.</p> <code class=\"mt-2 block break-all rounded bg-muted p-2 text-xs text-foreground\"> </code> <!></div>"), w = x.from_html("<p class=\"text-xs text-destructive\" role=\"alert\"> </p>"), Se = x.from_html("<div class=\"space-y-3 rounded-lg border border-primary/30 bg-primary/10 p-4\" role=\"status\" aria-live=\"polite\"><div class=\"flex items-start gap-3\"><!> <div><p class=\"text-sm font-medium text-foreground\">Category created</p> <p class=\"text-sm text-muted-foreground\"> </p></div></div> <!> <!></div>"), Ce = x.from_html("<p id=\"category-dialog-name-error\" class=\"text-xs text-destructive\" role=\"alert\"> </p>"), T = x.from_html("<!> <!> <!>", 1), we = x.from_html("<div class=\"space-y-3 rounded-md border border-border/70 bg-muted/30 p-3\"><div class=\"flex flex-wrap items-center justify-between gap-2\"><!> <div class=\"flex flex-wrap gap-2\"><!> <!></div></div> <div class=\"grid gap-2 sm:grid-cols-3\"><label class=\"flex items-center gap-2 rounded-md border border-border/60 bg-background px-3 py-2 text-sm\"><input type=\"checkbox\" class=\"size-4 rounded border-input\"/> <span>Read</span></label> <label class=\"flex items-center gap-2 rounded-md border border-border/60 bg-background px-3 py-2 text-sm\"><input type=\"checkbox\" class=\"size-4 rounded border-input\"/> <span>Edit</span></label> <label class=\"flex items-center gap-2 rounded-md border border-border/60 bg-background px-3 py-2 text-sm\"><input type=\"checkbox\" class=\"size-4 rounded border-input\"/> <span>Write</span></label></div></div>"), Te = x.from_html("<div class=\"rounded-lg border border-primary/30 bg-primary/10 p-3 text-primary\"><p class=\"text-xs font-medium\">Copy now — this raw key is shown once.</p> <code class=\"mt-1 block break-all rounded bg-background/80 p-2 text-xs text-foreground\"> </code> <!></div>"), Ee = x.from_html("<div class=\"flex items-center justify-between gap-3 rounded-md bg-muted/40 px-3 py-2 text-xs\"><span> </span> <div class=\"flex shrink-0 gap-2\"><!> <!></div></div>"), De = x.from_html("<div class=\"space-y-3 rounded-md border border-border/70 p-3\"><div class=\"flex items-center justify-between gap-2 max-sm:flex-col max-sm:items-stretch\"><div><!> <p class=\"text-xs text-muted-foreground\">The current permissions are saved before the key is issued.</p></div> <!></div> <!> <div class=\"max-h-48 space-y-2 overflow-y-auto pr-1\"></div></div>"), Oe = x.from_html("<form data-category-dialog-scroll=\"\" class=\"max-h-[calc(100dvh-12rem)] space-y-4 overflow-y-auto overscroll-contain pr-1\"><div class=\"space-y-2\"><!> <!> <!></div> <div class=\"space-y-2\"><!> <!></div> <!> <!></form>"), ke = x.from_html("<!> ", 1);
30
30
  function E(E, D) {
31
31
  x.push(D, !0);
32
32
  let O = {
@@ -215,7 +215,7 @@ function E(E, D) {
215
215
  var a = x.sibling(r);
216
216
  x.template_effect((e) => {
217
217
  x.set_text(i, `“${D.category?.name ?? "Category" ?? ""}”`), x.set_text(a, `?
218
- ${e ?? ""}`);
218
+ ${e ?? ""}`);
219
219
  }, [() => c()]), x.append(e, n);
220
220
  },
221
221
  $$slots: { default: !0 }
@@ -475,7 +475,7 @@ function E(E, D) {
475
475
  x.append(e, n);
476
476
  },
477
477
  $$slots: { default: !0 }
478
- }), x.next(), x.reset(r);
478
+ }), x.next(2), x.reset(r);
479
479
  var i = x.sibling(r, 2);
480
480
  {
481
481
  let e = x.derived(() => x.get(N) || x.get(V) || !x.get(q) || !x.get(Y));