@hiai-gg/docsmint 0.8.6 → 0.8.8
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 +38 -26
- package/dist/backend/index.js +167 -140
- package/dist/mcp-cli.js +113 -68
- package/dist/mcp-server.d.ts +4 -0
- package/dist/mcp-server.js +113 -68
- package/package.json +2 -2
- package/packages/cli/src/index.ts +1 -1
- package/server.json +6 -6
package/README.md
CHANGED
|
@@ -10,15 +10,22 @@ agents access to the same documents through MCP, REST, the SDK, or CLI.
|
|
|
10
10
|
operating the stack, or **[self-host with Docker](#quickstart)** to run the
|
|
11
11
|
Apache-2.0 application on your own infrastructure.
|
|
12
12
|
|
|
13
|
-
[](
|
|
13
|
+
[](LICENSE)
|
|
14
14
|
[](https://github.com/hiai-gg/docsmint/releases)
|
|
15
15
|
[](https://www.npmjs.com/package/@hiai-gg/docsmint)
|
|
16
|
+
[](https://hub.docker.com/r/vgalibov/docsmint)
|
|
17
|
+
[](https://github.com/hiai-gg/docsmint/stargazers)
|
|
16
18
|
[](https://github.com/hiai-gg/docsmint/actions/workflows/ci.yml)
|
|
17
|
-
|
|
18
|
-
[
|
|
19
|
-
[
|
|
20
|
-
[
|
|
21
|
-
[
|
|
19
|
+
[](https://bun.sh)
|
|
20
|
+
[](https://www.typescriptlang.org)
|
|
21
|
+
[](https://svelte.dev)
|
|
22
|
+
[](https://elysiajs.com)
|
|
23
|
+
[](https://tailwindcss.com)
|
|
24
|
+
[](https://orm.drizzle.team)
|
|
25
|
+
[](CONTRIBUTING.md)
|
|
26
|
+
[](https://lobehub.com/mcp/hiai-gg-docsmint)
|
|
27
|
+
[](https://docsmint.com/mcp/connect?source=github_mcp)
|
|
28
|
+
[](https://registry.modelcontextprotocol.io/v0.1/servers/io.github.HiAi-gg%2Fdocsmint/versions/latest)
|
|
22
29
|
|
|
23
30
|
<img width="1920" height="974" alt="DocsMint installable document workspace" src="https://github.com/user-attachments/assets/94701d01-a361-4ca1-b16d-de2a0c64d684" />
|
|
24
31
|
|
|
@@ -35,17 +42,21 @@ instructions, and verify the connection. Hosted MCP follows your plan and
|
|
|
35
42
|
workspace permissions. Prefer workspace-bound or category-scoped credentials.
|
|
36
43
|
|
|
37
44
|
The Cloud endpoint is `https://docsmint.com/mcp` (Streamable HTTP). OAuth-capable
|
|
38
|
-
clients discover
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
clients discover the hosted resource and authorization-server metadata; the current
|
|
46
|
+
authorization metadata advertises client registration at
|
|
47
|
+
`https://docsmint.com/oauth/register` (DCR). The hosted flow uses authorization-code
|
|
48
|
+
PKCE S256; metadata advertises only the `authorization_code` grant and no token
|
|
49
|
+
endpoint client authentication. CIMD is not part of the current contract. Browser consent binds the
|
|
50
|
+
one-hour opaque access token to the selected workspace, optional category, and
|
|
51
|
+
requested scopes. Refresh tokens are not issued. API-key clients can instead send
|
|
52
|
+
`Authorization: Bearer <key>` with an MCP/API credential created in the authenticated
|
|
53
|
+
browser UI. Credentials cannot create or elevate other credentials; lifecycle
|
|
54
|
+
management belongs to the signed-in browser session.
|
|
44
55
|
|
|
45
56
|
**Self-hosted alternative:** run [DocsMint with Docker](#quickstart), create an API
|
|
46
57
|
key in its browser UI, and configure `HIAI_DOCS_URL` and `HIAI_DOCS_API_KEY` for
|
|
47
58
|
`npx --yes --package @hiai-gg/docsmint docsmint-mcp`. The npm package is a stdio
|
|
48
|
-
bridge to your
|
|
59
|
+
bridge to your own API, not a server installer or hosted OAuth server. See the
|
|
49
60
|
[MCP guide](https://github.com/HiAi-gg/docsmint/blob/main/packages/mcp-server/README.md) for client configuration.
|
|
50
61
|
|
|
51
62
|
## Why DocsMint?
|
|
@@ -63,19 +74,20 @@ bridge to your running deployment, not a server installer. See the
|
|
|
63
74
|
- **Choose how you run it.** Use [managed DocsMint](https://docsmint.com) or
|
|
64
75
|
self-host the application, database, search, queues, and files.
|
|
65
76
|
|
|
66
|
-
## What's new in 0.8.
|
|
67
|
-
|
|
68
|
-
This patch completes the publication improvements from 0.8.5:
|
|
77
|
+
## What's new in 0.8.8?
|
|
69
78
|
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
-
|
|
73
|
-
|
|
74
|
-
-
|
|
79
|
+
- Clarify that hosted MCP authorization belongs to DocsMint Cloud and that the
|
|
80
|
+
stdio bridge uses an API key to connect to your own DocsMint API.
|
|
81
|
+
- Synchronize MCP Registry and LobeHub descriptions with the current Cloud
|
|
82
|
+
authorization contract and the self-hosted boundary.
|
|
83
|
+
- Keep the published catalog aligned with the implementation: 21 tools,
|
|
84
|
+
2 prompts, and 3 resources, with regression checks for route and metadata drift.
|
|
85
|
+
- Update vulnerability reporting details and clarify that OAuth protocol flaws
|
|
86
|
+
remain in scope for the product where they occur.
|
|
75
87
|
|
|
76
|
-
No
|
|
77
|
-
[release notes](https://github.com/HiAi-gg/docsmint/releases/tag/v0.8.
|
|
78
|
-
[changelog](https://github.com/HiAi-gg/docsmint/blob/main/CHANGELOG.md)
|
|
88
|
+
No MCP capabilities, existing integrations, or database schema change in this
|
|
89
|
+
release. See the [release notes](https://github.com/HiAi-gg/docsmint/releases/tag/v0.8.8)
|
|
90
|
+
and [changelog](https://github.com/HiAi-gg/docsmint/blob/main/CHANGELOG.md).
|
|
79
91
|
|
|
80
92
|
## Install with an AI agent
|
|
81
93
|
|
|
@@ -127,7 +139,7 @@ docker pull vgalibov/docsmint:web-latest
|
|
|
127
139
|
docker pull vgalibov/docsmint:caddy-latest
|
|
128
140
|
```
|
|
129
141
|
|
|
130
|
-
Use versioned tags `api-v0.8.
|
|
142
|
+
Use versioned tags `api-v0.8.8`, `web-v0.8.8`, and `caddy-v0.8.8` for
|
|
131
143
|
reproducible deploys. Caddy is the supporting reverse proxy with rate limiting;
|
|
132
144
|
it is separate from the API and web application. The quickstart still builds the Compose stack from this repository so PostgreSQL,
|
|
133
145
|
Redis, and SeaweedFS start together with the application.
|
|
@@ -212,7 +224,7 @@ command and configuration precedence.
|
|
|
212
224
|
## Connect an MCP client
|
|
213
225
|
|
|
214
226
|
Give agents a secure path to search, read, and maintain your knowledge without
|
|
215
|
-
database or filesystem access. DocsMint publishes
|
|
227
|
+
database or filesystem access. DocsMint publishes 21 tools plus ready-made
|
|
216
228
|
research prompts, scoped resources, and a document-manager skill.
|
|
217
229
|
|
|
218
230
|
### Hosted DocsMint
|
package/dist/backend/index.js
CHANGED
|
@@ -231613,6 +231613,106 @@ function stripQuotes(value) {
|
|
|
231613
231613
|
return trimmed;
|
|
231614
231614
|
}
|
|
231615
231615
|
|
|
231616
|
+
// ../../backend/src/lib/graph/search-query.ts
|
|
231617
|
+
init_drizzle_orm();
|
|
231618
|
+
|
|
231619
|
+
// ../../backend/src/search/query-analyzer.ts
|
|
231620
|
+
var CYRILLIC_LETTER = /\p{Script=Cyrillic}/u;
|
|
231621
|
+
var LATIN_LETTER = /\p{Script=Latin}/u;
|
|
231622
|
+
var LETTER2 = /\p{L}/u;
|
|
231623
|
+
function normalizeQuery(query) {
|
|
231624
|
+
const normalized = query.normalize("NFC");
|
|
231625
|
+
let result = "";
|
|
231626
|
+
let inQuotedPhrase = false;
|
|
231627
|
+
let escaped = false;
|
|
231628
|
+
let pendingWhitespace = false;
|
|
231629
|
+
for (const character of normalized) {
|
|
231630
|
+
if (inQuotedPhrase) {
|
|
231631
|
+
result += character;
|
|
231632
|
+
if (escaped) {
|
|
231633
|
+
escaped = false;
|
|
231634
|
+
} else if (character === "\\") {
|
|
231635
|
+
escaped = true;
|
|
231636
|
+
} else if (character === '"') {
|
|
231637
|
+
inQuotedPhrase = false;
|
|
231638
|
+
}
|
|
231639
|
+
continue;
|
|
231640
|
+
}
|
|
231641
|
+
if (character === '"') {
|
|
231642
|
+
if (pendingWhitespace && result.length > 0)
|
|
231643
|
+
result += " ";
|
|
231644
|
+
pendingWhitespace = false;
|
|
231645
|
+
result += character;
|
|
231646
|
+
inQuotedPhrase = true;
|
|
231647
|
+
continue;
|
|
231648
|
+
}
|
|
231649
|
+
if (/\s/u.test(character)) {
|
|
231650
|
+
pendingWhitespace = true;
|
|
231651
|
+
continue;
|
|
231652
|
+
}
|
|
231653
|
+
if (pendingWhitespace && result.length > 0)
|
|
231654
|
+
result += " ";
|
|
231655
|
+
pendingWhitespace = false;
|
|
231656
|
+
result += character;
|
|
231657
|
+
}
|
|
231658
|
+
return result;
|
|
231659
|
+
}
|
|
231660
|
+
function detectLanguage(query) {
|
|
231661
|
+
const normalized = normalizeQuery(query);
|
|
231662
|
+
let cyrillic = 0;
|
|
231663
|
+
let latin = 0;
|
|
231664
|
+
for (const token of normalized.split(" ")) {
|
|
231665
|
+
if (token.includes("/"))
|
|
231666
|
+
continue;
|
|
231667
|
+
for (const character of token) {
|
|
231668
|
+
if (!LETTER2.test(character))
|
|
231669
|
+
continue;
|
|
231670
|
+
if (CYRILLIC_LETTER.test(character))
|
|
231671
|
+
cyrillic += 1;
|
|
231672
|
+
else if (LATIN_LETTER.test(character))
|
|
231673
|
+
latin += 1;
|
|
231674
|
+
}
|
|
231675
|
+
}
|
|
231676
|
+
if (cyrillic > 0 && latin > 0)
|
|
231677
|
+
return "mixed";
|
|
231678
|
+
if (cyrillic > 0)
|
|
231679
|
+
return "ru";
|
|
231680
|
+
if (latin > 0)
|
|
231681
|
+
return "en";
|
|
231682
|
+
return "und";
|
|
231683
|
+
}
|
|
231684
|
+
function analyzeQuery(query) {
|
|
231685
|
+
return {
|
|
231686
|
+
original: query,
|
|
231687
|
+
normalized: normalizeQuery(query),
|
|
231688
|
+
detectedLanguage: detectLanguage(query),
|
|
231689
|
+
translations: [],
|
|
231690
|
+
synonyms: [],
|
|
231691
|
+
concepts: [],
|
|
231692
|
+
namedEntities: []
|
|
231693
|
+
};
|
|
231694
|
+
}
|
|
231695
|
+
|
|
231696
|
+
// ../../backend/src/lib/graph/search-query.ts
|
|
231697
|
+
function graphSearchDocumentQuery(ctx, documentIds, query) {
|
|
231698
|
+
const normalized = normalizeQuery(query ?? "");
|
|
231699
|
+
const english = sql`websearch_to_tsquery('english', ${normalized})`;
|
|
231700
|
+
const simple = sql`websearch_to_tsquery('simple', ${normalized})`;
|
|
231701
|
+
const exact = sql`lower(trim(d.title)) = lower(${normalized})`;
|
|
231702
|
+
const match2 = normalized ? sql`AND (${exact} OR d.search_vector @@ ${english} OR d.search_vector_simple @@ ${simple})` : sql``;
|
|
231703
|
+
const order = normalized ? sql`${exact} DESC, GREATEST(ts_rank(d.search_vector, ${english}), ts_rank(d.search_vector_simple, ${simple})) DESC,` : sql``;
|
|
231704
|
+
const allowed2 = documentIds.length ? sql`d.id IN (${sql.join(documentIds.map((id3) => sql`${id3}`), sql`, `)})` : sql`false`;
|
|
231705
|
+
return sql`
|
|
231706
|
+
SELECT d.id, d.title, d.content
|
|
231707
|
+
FROM documents d
|
|
231708
|
+
WHERE ${tenantOwnerSql("d", ctx)}
|
|
231709
|
+
AND d.deleted_at IS NULL
|
|
231710
|
+
AND ${allowed2}
|
|
231711
|
+
${match2}
|
|
231712
|
+
ORDER BY ${order} d.id ASC
|
|
231713
|
+
`;
|
|
231714
|
+
}
|
|
231715
|
+
|
|
231616
231716
|
// ../../backend/src/api/routes/graph.ts
|
|
231617
231717
|
init_logger3();
|
|
231618
231718
|
var entitiesQuerySchema = exports_external2.object({
|
|
@@ -231621,8 +231721,11 @@ var entitiesQuerySchema = exports_external2.object({
|
|
|
231621
231721
|
var relatedParamsSchema = exports_external2.object({
|
|
231622
231722
|
docId: exports_external2.string().min(1)
|
|
231623
231723
|
});
|
|
231724
|
+
var relatedQuerySchema = exports_external2.object({
|
|
231725
|
+
limit: exports_external2.coerce.number().int().min(1).max(100).default(20)
|
|
231726
|
+
});
|
|
231624
231727
|
var graphSearchBodySchema = exports_external2.object({
|
|
231625
|
-
query: exports_external2.string().optional(),
|
|
231728
|
+
query: exports_external2.string().max(2000).optional(),
|
|
231626
231729
|
docIds: exports_external2.array(exports_external2.string().min(1)).min(1).max(50),
|
|
231627
231730
|
maxResults: exports_external2.number().int().min(1).max(100).optional()
|
|
231628
231731
|
});
|
|
@@ -231668,7 +231771,7 @@ var graphRoutes = new Elysia({ prefix: "/api/graph" }).get("/entities", async ({
|
|
|
231668
231771
|
tags: ["Graph"],
|
|
231669
231772
|
summary: "List entities linked to a document"
|
|
231670
231773
|
}
|
|
231671
|
-
}).get("/related/:docId", async ({ params: params2, set: set2, request }) => {
|
|
231774
|
+
}).get("/related/:docId", async ({ params: params2, query, set: set2, request }) => {
|
|
231672
231775
|
const rl = await applyRateLimit(request, set2);
|
|
231673
231776
|
if (!rl.ok)
|
|
231674
231777
|
return rl.response;
|
|
@@ -231687,6 +231790,11 @@ var graphRoutes = new Elysia({ prefix: "/api/graph" }).get("/entities", async ({
|
|
|
231687
231790
|
set2.status = 400;
|
|
231688
231791
|
return { error: "Invalid params", details: parsed.error.flatten() };
|
|
231689
231792
|
}
|
|
231793
|
+
const parsedQuery = relatedQuerySchema.safeParse(query);
|
|
231794
|
+
if (!parsedQuery.success) {
|
|
231795
|
+
set2.status = 400;
|
|
231796
|
+
return { error: "Invalid query", details: parsedQuery.error.flatten() };
|
|
231797
|
+
}
|
|
231690
231798
|
try {
|
|
231691
231799
|
const seedIds = await allowedGraphDocumentIds(access, [
|
|
231692
231800
|
parsed.data.docId
|
|
@@ -231698,7 +231806,7 @@ var graphRoutes = new Elysia({ prefix: "/api/graph" }).get("/entities", async ({
|
|
|
231698
231806
|
if (!config3.GRAPH_SEARCH_ENABLED)
|
|
231699
231807
|
return { related: [] };
|
|
231700
231808
|
const related = await fetchRelatedDocuments(ctx, parsed.data.docId, access);
|
|
231701
|
-
return { related };
|
|
231809
|
+
return { related: related.slice(0, parsedQuery.data.limit) };
|
|
231702
231810
|
} catch (err) {
|
|
231703
231811
|
logger3.warn({ err, docId: parsed.data.docId }, "Graph related lookup failed \u2014 returning empty");
|
|
231704
231812
|
return { related: [] };
|
|
@@ -231737,7 +231845,7 @@ var graphRoutes = new Elysia({ prefix: "/api/graph" }).get("/entities", async ({
|
|
|
231737
231845
|
if (!config3.GRAPH_SEARCH_ENABLED) {
|
|
231738
231846
|
return { query, entities: [], relatedDocs: [] };
|
|
231739
231847
|
}
|
|
231740
|
-
const result = await graphRagLookup(ctx, docIds, maxResults, access);
|
|
231848
|
+
const result = await graphRagLookup(ctx, docIds, maxResults, access, query);
|
|
231741
231849
|
return { query, ...result };
|
|
231742
231850
|
} catch (err) {
|
|
231743
231851
|
logger3.warn({ err, docIds: docIds.length }, "Graph RAG search failed \u2014 returning empty");
|
|
@@ -231805,7 +231913,7 @@ async function fetchRelatedDocuments(ctx, docId, access) {
|
|
|
231805
231913
|
const allowedIds = access ? await allowedGraphDocumentIds(access, neighborIds) : await filterToOwnedDocuments(ctx, neighborIds);
|
|
231806
231914
|
return current.filter((neighbor) => allowedIds.has(neighbor.docId));
|
|
231807
231915
|
}
|
|
231808
|
-
async function graphRagLookup(ctx, docIds, maxResults, access) {
|
|
231916
|
+
async function graphRagLookup(ctx, docIds, maxResults, access, query) {
|
|
231809
231917
|
const sql6 = await getGraphDb();
|
|
231810
231918
|
if (!sql6)
|
|
231811
231919
|
return { entities: [], relatedDocs: [] };
|
|
@@ -231838,12 +231946,13 @@ async function graphRagLookup(ctx, docIds, maxResults, access) {
|
|
|
231838
231946
|
if (ownedNeighborIds.length === 0) {
|
|
231839
231947
|
return { entities: Array.from(entityMap.values()), relatedDocs: [] };
|
|
231840
231948
|
}
|
|
231841
|
-
const rows = await loadDocumentSummaries(ctx, ownedNeighborIds);
|
|
231949
|
+
const rows = await loadDocumentSummaries(ctx, ownedNeighborIds, query);
|
|
231842
231950
|
const byId = new Map;
|
|
231843
231951
|
for (const r4 of rows)
|
|
231844
231952
|
byId.set(r4.id, r4);
|
|
231845
231953
|
const relatedDocs = [];
|
|
231846
|
-
|
|
231954
|
+
const rankedIds = query?.trim() ? rows.map((row) => row.id) : ownedNeighborIds;
|
|
231955
|
+
for (const id3 of rankedIds) {
|
|
231847
231956
|
const meta3 = neighborMap.get(id3);
|
|
231848
231957
|
const row = byId.get(id3);
|
|
231849
231958
|
if (!meta3 || !row)
|
|
@@ -231919,19 +232028,11 @@ function currentGraphNeighbors(expansion, activeGenerations) {
|
|
|
231919
232028
|
}
|
|
231920
232029
|
return [...current.values()].sort((left, right) => left.hopDistance - right.hopDistance || left.docId.localeCompare(right.docId));
|
|
231921
232030
|
}
|
|
231922
|
-
async function loadDocumentSummaries(ctx, docIds) {
|
|
232031
|
+
async function loadDocumentSummaries(ctx, docIds, query) {
|
|
231923
232032
|
if (docIds.length === 0)
|
|
231924
232033
|
return [];
|
|
231925
232034
|
return withTenant(ctx, async (tx) => {
|
|
231926
|
-
return tx.
|
|
231927
|
-
id: documents.id,
|
|
231928
|
-
title: documents.title,
|
|
231929
|
-
content: documents.content,
|
|
231930
|
-
folderId: documents.folderId,
|
|
231931
|
-
folderName: folders.name,
|
|
231932
|
-
createdAt: documents.createdAt,
|
|
231933
|
-
updatedAt: documents.updatedAt
|
|
231934
|
-
}).from(documents).leftJoin(folders, eq(folders.id, documents.folderId)).where(and(tenantOwnerCondition(documents.ownerId, documents.workspaceId, ctx), inArray(documents.id, docIds), isNull(documents.deletedAt)));
|
|
232035
|
+
return await tx.execute(graphSearchDocumentQuery(ctx, docIds, query));
|
|
231935
232036
|
});
|
|
231936
232037
|
}
|
|
231937
232038
|
function cypherDocReplace(cypher, docId) {
|
|
@@ -231958,50 +232059,6 @@ function stripQuotes2(value) {
|
|
|
231958
232059
|
return trimmed;
|
|
231959
232060
|
}
|
|
231960
232061
|
|
|
231961
|
-
// ../../backend/src/api/routes/health.ts
|
|
231962
|
-
function createHealthRoutes(dependencies) {
|
|
231963
|
-
return new Elysia().get("/api/health", async ({ request, set: set2 }) => {
|
|
231964
|
-
const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip") ?? "unknown";
|
|
231965
|
-
const rateLimit = await dependencies.rateLimiter(ip, request);
|
|
231966
|
-
set2.headers = {
|
|
231967
|
-
"X-RateLimit-Remaining": String(rateLimit.remaining),
|
|
231968
|
-
...rateLimit.retryAfter ? { "Retry-After": String(rateLimit.retryAfter) } : {}
|
|
231969
|
-
};
|
|
231970
|
-
if (!rateLimit.allowed) {
|
|
231971
|
-
set2.status = 429;
|
|
231972
|
-
return { error: "Too many requests" };
|
|
231973
|
-
}
|
|
231974
|
-
const [databaseAvailable, redisAvailable, storageAvailable] = await Promise.all([
|
|
231975
|
-
dependencies.databaseAvailable(),
|
|
231976
|
-
dependencies.redisAvailable(),
|
|
231977
|
-
dependencies.storageAvailable()
|
|
231978
|
-
]);
|
|
231979
|
-
const queueAvailable = dependencies.queueAvailable();
|
|
231980
|
-
const readiness = evaluatePipelineHealth({
|
|
231981
|
-
databaseAvailable,
|
|
231982
|
-
redisAvailable,
|
|
231983
|
-
storageAvailable,
|
|
231984
|
-
queueAvailable,
|
|
231985
|
-
recoveryAvailable: true,
|
|
231986
|
-
oldestInteractiveWaitMs: 0,
|
|
231987
|
-
interactiveSloMs: Number.POSITIVE_INFINITY,
|
|
231988
|
-
graphAvailable: true
|
|
231989
|
-
});
|
|
231990
|
-
if (readiness.status === "unhealthy")
|
|
231991
|
-
set2.status = 503;
|
|
231992
|
-
return {
|
|
231993
|
-
status: readiness.status === "unhealthy" ? "unhealthy" : "ok",
|
|
231994
|
-
service: "docsmint",
|
|
231995
|
-
timestamp: new Date().toISOString(),
|
|
231996
|
-
database: databaseAvailable ? "ok" : "error",
|
|
231997
|
-
redis: redisAvailable ? "ok" : "error",
|
|
231998
|
-
storage: storageAvailable ? "ok" : "error",
|
|
231999
|
-
queue: queueAvailable ? "ok" : "error",
|
|
232000
|
-
...readiness.reasons.length ? { reasons: readiness.reasons } : {}
|
|
232001
|
-
};
|
|
232002
|
-
});
|
|
232003
|
-
}
|
|
232004
|
-
|
|
232005
232062
|
// ../../backend/src/api/routes/keys.ts
|
|
232006
232063
|
init_schema2();
|
|
232007
232064
|
init_drizzle_orm();
|
|
@@ -232493,83 +232550,6 @@ function evaluateConfidence(results, _plan, thresholds = { vectorMinSimilarity:
|
|
|
232493
232550
|
return { confident: reasons.length === 0, reasons };
|
|
232494
232551
|
}
|
|
232495
232552
|
|
|
232496
|
-
// ../../backend/src/search/query-analyzer.ts
|
|
232497
|
-
var CYRILLIC_LETTER = /\p{Script=Cyrillic}/u;
|
|
232498
|
-
var LATIN_LETTER = /\p{Script=Latin}/u;
|
|
232499
|
-
var LETTER2 = /\p{L}/u;
|
|
232500
|
-
function normalizeQuery(query) {
|
|
232501
|
-
const normalized = query.normalize("NFC");
|
|
232502
|
-
let result = "";
|
|
232503
|
-
let inQuotedPhrase = false;
|
|
232504
|
-
let escaped = false;
|
|
232505
|
-
let pendingWhitespace = false;
|
|
232506
|
-
for (const character of normalized) {
|
|
232507
|
-
if (inQuotedPhrase) {
|
|
232508
|
-
result += character;
|
|
232509
|
-
if (escaped) {
|
|
232510
|
-
escaped = false;
|
|
232511
|
-
} else if (character === "\\") {
|
|
232512
|
-
escaped = true;
|
|
232513
|
-
} else if (character === '"') {
|
|
232514
|
-
inQuotedPhrase = false;
|
|
232515
|
-
}
|
|
232516
|
-
continue;
|
|
232517
|
-
}
|
|
232518
|
-
if (character === '"') {
|
|
232519
|
-
if (pendingWhitespace && result.length > 0)
|
|
232520
|
-
result += " ";
|
|
232521
|
-
pendingWhitespace = false;
|
|
232522
|
-
result += character;
|
|
232523
|
-
inQuotedPhrase = true;
|
|
232524
|
-
continue;
|
|
232525
|
-
}
|
|
232526
|
-
if (/\s/u.test(character)) {
|
|
232527
|
-
pendingWhitespace = true;
|
|
232528
|
-
continue;
|
|
232529
|
-
}
|
|
232530
|
-
if (pendingWhitespace && result.length > 0)
|
|
232531
|
-
result += " ";
|
|
232532
|
-
pendingWhitespace = false;
|
|
232533
|
-
result += character;
|
|
232534
|
-
}
|
|
232535
|
-
return result;
|
|
232536
|
-
}
|
|
232537
|
-
function detectLanguage(query) {
|
|
232538
|
-
const normalized = normalizeQuery(query);
|
|
232539
|
-
let cyrillic = 0;
|
|
232540
|
-
let latin = 0;
|
|
232541
|
-
for (const token of normalized.split(" ")) {
|
|
232542
|
-
if (token.includes("/"))
|
|
232543
|
-
continue;
|
|
232544
|
-
for (const character of token) {
|
|
232545
|
-
if (!LETTER2.test(character))
|
|
232546
|
-
continue;
|
|
232547
|
-
if (CYRILLIC_LETTER.test(character))
|
|
232548
|
-
cyrillic += 1;
|
|
232549
|
-
else if (LATIN_LETTER.test(character))
|
|
232550
|
-
latin += 1;
|
|
232551
|
-
}
|
|
232552
|
-
}
|
|
232553
|
-
if (cyrillic > 0 && latin > 0)
|
|
232554
|
-
return "mixed";
|
|
232555
|
-
if (cyrillic > 0)
|
|
232556
|
-
return "ru";
|
|
232557
|
-
if (latin > 0)
|
|
232558
|
-
return "en";
|
|
232559
|
-
return "und";
|
|
232560
|
-
}
|
|
232561
|
-
function analyzeQuery(query) {
|
|
232562
|
-
return {
|
|
232563
|
-
original: query,
|
|
232564
|
-
normalized: normalizeQuery(query),
|
|
232565
|
-
detectedLanguage: detectLanguage(query),
|
|
232566
|
-
translations: [],
|
|
232567
|
-
synonyms: [],
|
|
232568
|
-
concepts: [],
|
|
232569
|
-
namedEntities: []
|
|
232570
|
-
};
|
|
232571
|
-
}
|
|
232572
|
-
|
|
232573
232553
|
// ../../backend/src/search/query-expander.ts
|
|
232574
232554
|
init_zod();
|
|
232575
232555
|
init_config();
|
|
@@ -235935,6 +235915,53 @@ var webhookRoutes = new Elysia({ prefix: "/api/webhooks" }).post("/storage", asy
|
|
|
235935
235915
|
return { received: true };
|
|
235936
235916
|
});
|
|
235937
235917
|
|
|
235918
|
+
// ../../backend/src/api/register-routes.ts
|
|
235919
|
+
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);
|
|
235920
|
+
|
|
235921
|
+
// ../../backend/src/api/routes/health.ts
|
|
235922
|
+
function createHealthRoutes(dependencies) {
|
|
235923
|
+
return new Elysia().get("/api/health", async ({ request, set: set2 }) => {
|
|
235924
|
+
const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip") ?? "unknown";
|
|
235925
|
+
const rateLimit = await dependencies.rateLimiter(ip, request);
|
|
235926
|
+
set2.headers = {
|
|
235927
|
+
"X-RateLimit-Remaining": String(rateLimit.remaining),
|
|
235928
|
+
...rateLimit.retryAfter ? { "Retry-After": String(rateLimit.retryAfter) } : {}
|
|
235929
|
+
};
|
|
235930
|
+
if (!rateLimit.allowed) {
|
|
235931
|
+
set2.status = 429;
|
|
235932
|
+
return { error: "Too many requests" };
|
|
235933
|
+
}
|
|
235934
|
+
const [databaseAvailable, redisAvailable, storageAvailable] = await Promise.all([
|
|
235935
|
+
dependencies.databaseAvailable(),
|
|
235936
|
+
dependencies.redisAvailable(),
|
|
235937
|
+
dependencies.storageAvailable()
|
|
235938
|
+
]);
|
|
235939
|
+
const queueAvailable = dependencies.queueAvailable();
|
|
235940
|
+
const readiness = evaluatePipelineHealth({
|
|
235941
|
+
databaseAvailable,
|
|
235942
|
+
redisAvailable,
|
|
235943
|
+
storageAvailable,
|
|
235944
|
+
queueAvailable,
|
|
235945
|
+
recoveryAvailable: true,
|
|
235946
|
+
oldestInteractiveWaitMs: 0,
|
|
235947
|
+
interactiveSloMs: Number.POSITIVE_INFINITY,
|
|
235948
|
+
graphAvailable: true
|
|
235949
|
+
});
|
|
235950
|
+
if (readiness.status === "unhealthy")
|
|
235951
|
+
set2.status = 503;
|
|
235952
|
+
return {
|
|
235953
|
+
status: readiness.status === "unhealthy" ? "unhealthy" : "ok",
|
|
235954
|
+
service: "docsmint",
|
|
235955
|
+
timestamp: new Date().toISOString(),
|
|
235956
|
+
database: databaseAvailable ? "ok" : "error",
|
|
235957
|
+
redis: redisAvailable ? "ok" : "error",
|
|
235958
|
+
storage: storageAvailable ? "ok" : "error",
|
|
235959
|
+
queue: queueAvailable ? "ok" : "error",
|
|
235960
|
+
...readiness.reasons.length ? { reasons: readiness.reasons } : {}
|
|
235961
|
+
};
|
|
235962
|
+
});
|
|
235963
|
+
}
|
|
235964
|
+
|
|
235938
235965
|
// ../../backend/src/lib/api-key-owner.ts
|
|
235939
235966
|
init_schema2();
|
|
235940
235967
|
init_config();
|
|
@@ -238398,7 +238425,7 @@ var swaggerConfig = {
|
|
|
238398
238425
|
},
|
|
238399
238426
|
info: {
|
|
238400
238427
|
title: "DocsMint API",
|
|
238401
|
-
version: "0.8.
|
|
238428
|
+
version: "0.8.8",
|
|
238402
238429
|
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.",
|
|
238403
238430
|
contact: { name: "HiAi-gg", url: "https://github.com/HiAi-gg/docsmint" },
|
|
238404
238431
|
license: {
|
|
@@ -238452,7 +238479,7 @@ var app = new Elysia().use(bodySizeLimit).onError(({ error: error53, set: set2 }
|
|
|
238452
238479
|
redisAvailable: () => redis.ping().then(() => true, () => false),
|
|
238453
238480
|
storageAvailable: () => storage.send(new ListBucketsCommand({})).then(() => true, () => false),
|
|
238454
238481
|
queueAvailable: () => pipelineRuntime.workers.size === PIPELINE_STAGES.length && [...pipelineRuntime.workers.values()].every((worker2) => worker2.isRunning?.() === true)
|
|
238455
|
-
})).use(csrfMiddleware).use(authMiddleware).use(
|
|
238482
|
+
})).use(csrfMiddleware).use(authMiddleware).use(selfHostedApiRoutes);
|
|
238456
238483
|
app.listen({
|
|
238457
238484
|
port: config3.API_PORT,
|
|
238458
238485
|
development: config3.NODE_ENV !== "production",
|