@gmickel/gno 1.12.2 → 1.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/assets/skill/SKILL.md +3 -1
- package/package.json +2 -1
- package/src/core/indexed-reference.ts +68 -0
- package/src/core/ref-parser.ts +6 -1
- package/src/index.ts +11 -2
- package/src/ingestion/sync.ts +182 -15
- package/src/ingestion/types.ts +2 -0
- package/src/mcp/resources/index.ts +71 -47
- package/src/mcp/tools/get.ts +108 -93
- package/src/mcp/tools/multi-get.ts +116 -99
- package/src/sdk/client.ts +56 -31
- package/src/serve/public/components/AIModelSelector.tsx +22 -7
- package/src/serve/public/pages/Dashboard.tsx +1 -1
- package/src/serve/routes/api.ts +26 -1
- package/src/serve/server.ts +11 -2
- package/src/serve/status.ts +24 -0
- package/src/serve/watch-service.ts +2 -1
- package/src/store/sqlite/adapter.ts +99 -49
- package/src/store/sqlite/scoped-index.ts +68 -0
- package/src/store/types.ts +3 -1
package/src/serve/routes/api.ts
CHANGED
|
@@ -736,6 +736,31 @@ export function handleHealth(): Response {
|
|
|
736
736
|
return jsonResponse({ ok: true });
|
|
737
737
|
}
|
|
738
738
|
|
|
739
|
+
const statusBuilds = new WeakMap<
|
|
740
|
+
ServerContext,
|
|
741
|
+
Promise<Awaited<ReturnType<typeof buildAppStatus>>>
|
|
742
|
+
>();
|
|
743
|
+
|
|
744
|
+
async function getCoalescedAppStatus(
|
|
745
|
+
ctx: ServerContext,
|
|
746
|
+
deps?: StatusBuildDeps
|
|
747
|
+
): Promise<Awaited<ReturnType<typeof buildAppStatus>>> {
|
|
748
|
+
const existing = statusBuilds.get(ctx);
|
|
749
|
+
if (existing) {
|
|
750
|
+
return existing;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const build = buildAppStatus(ctx, deps ?? {});
|
|
754
|
+
statusBuilds.set(ctx, build);
|
|
755
|
+
try {
|
|
756
|
+
return await build;
|
|
757
|
+
} finally {
|
|
758
|
+
if (statusBuilds.get(ctx) === build) {
|
|
759
|
+
statusBuilds.delete(ctx);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
739
764
|
/**
|
|
740
765
|
* GET /api/status
|
|
741
766
|
* Returns index status matching status.schema.json.
|
|
@@ -745,7 +770,7 @@ export async function handleStatus(
|
|
|
745
770
|
deps?: StatusBuildDeps
|
|
746
771
|
): Promise<Response> {
|
|
747
772
|
try {
|
|
748
|
-
const status = await
|
|
773
|
+
const status = await getCoalescedAppStatus(ctx, deps);
|
|
749
774
|
return jsonResponse(status);
|
|
750
775
|
} catch (error) {
|
|
751
776
|
return errorResponse(
|
package/src/serve/server.ts
CHANGED
|
@@ -163,6 +163,10 @@ export async function startServer(
|
|
|
163
163
|
|
|
164
164
|
process.once("SIGINT", shutdown);
|
|
165
165
|
process.once("SIGTERM", shutdown);
|
|
166
|
+
const removeShutdownHandlers = (): void => {
|
|
167
|
+
process.off("SIGINT", shutdown);
|
|
168
|
+
process.off("SIGTERM", shutdown);
|
|
169
|
+
};
|
|
166
170
|
|
|
167
171
|
// Start server with try/catch for port-in-use etc.
|
|
168
172
|
let server: ReturnType<typeof Bun.serve>;
|
|
@@ -679,6 +683,7 @@ export async function startServer(
|
|
|
679
683
|
},
|
|
680
684
|
});
|
|
681
685
|
} catch (e) {
|
|
686
|
+
removeShutdownHandlers();
|
|
682
687
|
await runtime.dispose();
|
|
683
688
|
return {
|
|
684
689
|
success: false,
|
|
@@ -696,7 +701,11 @@ export async function startServer(
|
|
|
696
701
|
});
|
|
697
702
|
});
|
|
698
703
|
|
|
699
|
-
|
|
700
|
-
|
|
704
|
+
removeShutdownHandlers();
|
|
705
|
+
try {
|
|
706
|
+
await server.stop(true);
|
|
707
|
+
} finally {
|
|
708
|
+
await runtime.dispose();
|
|
709
|
+
}
|
|
701
710
|
return { success: true };
|
|
702
711
|
}
|
package/src/serve/status.ts
CHANGED
|
@@ -381,6 +381,29 @@ async function buildModelCheck(
|
|
|
381
381
|
};
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
+
function buildVectorCheck(ctx: ServerContext): HealthCheck | null {
|
|
385
|
+
const vectorIndex = ctx.vectorIndex;
|
|
386
|
+
if (!vectorIndex || vectorIndex.searchAvailable) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const reason = vectorIndex.loadError?.trim();
|
|
391
|
+
const guidance = vectorIndex.guidance?.trim();
|
|
392
|
+
return {
|
|
393
|
+
id: "vector-runtime",
|
|
394
|
+
title: "Vector search",
|
|
395
|
+
status: "warn",
|
|
396
|
+
summary: "Semantic search acceleration is unavailable",
|
|
397
|
+
detail: [
|
|
398
|
+
reason ? `sqlite-vec failed to load: ${reason}` : null,
|
|
399
|
+
guidance ?? "Run `gno doctor` for recovery guidance.",
|
|
400
|
+
"BM25 search remains available.",
|
|
401
|
+
]
|
|
402
|
+
.filter((part): part is string => part !== null)
|
|
403
|
+
.join(" "),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
384
407
|
async function buildDiskCheck(
|
|
385
408
|
status: IndexStatus,
|
|
386
409
|
deps: StatusBuildDeps
|
|
@@ -636,6 +659,7 @@ export async function buildAppStatus(
|
|
|
636
659
|
buildCollectionCheck(status),
|
|
637
660
|
buildIndexingCheck(status),
|
|
638
661
|
modelCheck,
|
|
662
|
+
buildVectorCheck(ctx),
|
|
639
663
|
diskCheck,
|
|
640
664
|
backgroundCheck,
|
|
641
665
|
].filter((check): check is HealthCheck => check !== null);
|
|
@@ -209,9 +209,10 @@ export class CollectionWatchService {
|
|
|
209
209
|
collection: collection.name,
|
|
210
210
|
relPaths,
|
|
211
211
|
});
|
|
212
|
-
const result = await defaultSyncService.
|
|
212
|
+
const result = await defaultSyncService.syncPaths(
|
|
213
213
|
collection,
|
|
214
214
|
this.#store,
|
|
215
|
+
relPaths,
|
|
215
216
|
{
|
|
216
217
|
...this.#syncOptions,
|
|
217
218
|
runUpdateCmd: false,
|
|
@@ -3014,18 +3014,35 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
3014
3014
|
}
|
|
3015
3015
|
}
|
|
3016
3016
|
|
|
3017
|
-
async backfillDocEdges(
|
|
3017
|
+
async backfillDocEdges(
|
|
3018
|
+
sourceDocumentIds?: number[]
|
|
3019
|
+
): Promise<StoreResult<{ inserted: number }>> {
|
|
3018
3020
|
try {
|
|
3019
3021
|
const db = this.ensureOpen();
|
|
3022
|
+
const sourceIds = sourceDocumentIds
|
|
3023
|
+
? [...new Set(sourceDocumentIds)].filter((id) => id > 0)
|
|
3024
|
+
: undefined;
|
|
3025
|
+
if (sourceIds?.length === 0) {
|
|
3026
|
+
return ok({ inserted: 0 });
|
|
3027
|
+
}
|
|
3028
|
+
const sourcePlaceholders = sourceIds
|
|
3029
|
+
? sourceIds.map(() => "?").join(", ")
|
|
3030
|
+
: "";
|
|
3031
|
+
const sourceFilter = sourceIds
|
|
3032
|
+
? `AND src.id IN (${sourcePlaceholders})`
|
|
3033
|
+
: "";
|
|
3020
3034
|
let inserted = 0;
|
|
3021
3035
|
|
|
3022
3036
|
const transaction = db.transaction(() => {
|
|
3023
|
-
db.run(
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3037
|
+
db.run(
|
|
3038
|
+
`DELETE FROM doc_edges
|
|
3039
|
+
WHERE source IN (?, ?)
|
|
3040
|
+
${sourceIds ? `AND src_doc_id IN (${sourcePlaceholders})` : ""}`,
|
|
3041
|
+
["wikilink", "markdown-link", ...(sourceIds ?? [])]
|
|
3042
|
+
);
|
|
3027
3043
|
|
|
3028
|
-
const insertWiki = db.run(
|
|
3044
|
+
const insertWiki = db.run(
|
|
3045
|
+
`
|
|
3029
3046
|
INSERT OR IGNORE INTO doc_edges (
|
|
3030
3047
|
src_doc_id, dst_doc_id, edge_type, confidence, source
|
|
3031
3048
|
)
|
|
@@ -3044,9 +3061,13 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
3044
3061
|
WHERE src.active = 1
|
|
3045
3062
|
AND tgt.active = 1
|
|
3046
3063
|
AND dl.link_type = 'wiki'
|
|
3047
|
-
|
|
3064
|
+
${sourceFilter}
|
|
3065
|
+
`,
|
|
3066
|
+
sourceIds ?? []
|
|
3067
|
+
);
|
|
3048
3068
|
|
|
3049
|
-
const insertMarkdown = db.run(
|
|
3069
|
+
const insertMarkdown = db.run(
|
|
3070
|
+
`
|
|
3050
3071
|
INSERT OR IGNORE INTO doc_edges (
|
|
3051
3072
|
src_doc_id, dst_doc_id, edge_type, confidence, source
|
|
3052
3073
|
)
|
|
@@ -3063,7 +3084,10 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
3063
3084
|
AND tgt.rel_path = dl.target_ref_norm
|
|
3064
3085
|
WHERE src.active = 1
|
|
3065
3086
|
AND dl.link_type = 'markdown'
|
|
3066
|
-
|
|
3087
|
+
${sourceFilter}
|
|
3088
|
+
`,
|
|
3089
|
+
sourceIds ?? []
|
|
3090
|
+
);
|
|
3067
3091
|
|
|
3068
3092
|
inserted = insertWiki.changes + insertMarkdown.changes;
|
|
3069
3093
|
});
|
|
@@ -3804,36 +3828,56 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
3804
3828
|
const collectionStats = db
|
|
3805
3829
|
.query<CollectionStat, [string | null, string | null, string | null]>(
|
|
3806
3830
|
`
|
|
3831
|
+
WITH document_stats AS (
|
|
3832
|
+
SELECT
|
|
3833
|
+
collection,
|
|
3834
|
+
COUNT(*) AS total,
|
|
3835
|
+
SUM(CASE WHEN active = 1 THEN 1 ELSE 0 END) AS active,
|
|
3836
|
+
SUM(CASE WHEN last_error_code IS NOT NULL THEN 1 ELSE 0 END) AS errored,
|
|
3837
|
+
SUM(CASE WHEN mirror_hash IS NOT NULL THEN 1 ELSE 0 END) AS chunked
|
|
3838
|
+
FROM documents
|
|
3839
|
+
GROUP BY collection
|
|
3840
|
+
),
|
|
3841
|
+
active_collection_mirrors AS (
|
|
3842
|
+
SELECT DISTINCT collection, mirror_hash
|
|
3843
|
+
FROM documents
|
|
3844
|
+
WHERE active = 1 AND mirror_hash IS NOT NULL
|
|
3845
|
+
),
|
|
3846
|
+
matching_vectors AS (
|
|
3847
|
+
SELECT mirror_hash, seq, MAX(embedded_at) AS embedded_at
|
|
3848
|
+
FROM content_vectors
|
|
3849
|
+
WHERE (? IS NULL OR (
|
|
3850
|
+
model = ? AND embed_fingerprint = ?
|
|
3851
|
+
))
|
|
3852
|
+
GROUP BY mirror_hash, seq
|
|
3853
|
+
),
|
|
3854
|
+
collection_chunks AS (
|
|
3855
|
+
SELECT
|
|
3856
|
+
acm.collection,
|
|
3857
|
+
COUNT(*) AS chunk_count,
|
|
3858
|
+
SUM(CASE
|
|
3859
|
+
WHEN mv.embedded_at >= cc.created_at THEN 1
|
|
3860
|
+
ELSE 0
|
|
3861
|
+
END) AS embedded_count
|
|
3862
|
+
FROM active_collection_mirrors acm
|
|
3863
|
+
JOIN content_chunks cc ON cc.mirror_hash = acm.mirror_hash
|
|
3864
|
+
LEFT JOIN matching_vectors mv
|
|
3865
|
+
ON mv.mirror_hash = cc.mirror_hash AND mv.seq = cc.seq
|
|
3866
|
+
GROUP BY acm.collection
|
|
3867
|
+
)
|
|
3807
3868
|
SELECT
|
|
3808
3869
|
c.name,
|
|
3809
3870
|
c.path,
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
(
|
|
3815
|
-
|
|
3816
|
-
WHERE d2.collection = c.name AND d2.active = 1) as chunk_count,
|
|
3817
|
-
(SELECT COUNT(*) FROM content_chunks cc
|
|
3818
|
-
WHERE EXISTS (
|
|
3819
|
-
SELECT 1 FROM documents d3
|
|
3820
|
-
WHERE d3.collection = c.name
|
|
3821
|
-
AND d3.active = 1
|
|
3822
|
-
AND d3.mirror_hash = cc.mirror_hash
|
|
3823
|
-
)
|
|
3824
|
-
AND EXISTS (
|
|
3825
|
-
SELECT 1 FROM content_vectors cv
|
|
3826
|
-
WHERE cv.mirror_hash = cc.mirror_hash
|
|
3827
|
-
AND cv.seq = cc.seq
|
|
3828
|
-
AND (? IS NULL OR (
|
|
3829
|
-
cv.model = ?
|
|
3830
|
-
AND cv.embed_fingerprint = ?
|
|
3831
|
-
))
|
|
3832
|
-
AND cv.embedded_at >= cc.created_at
|
|
3833
|
-
)) as embedded_count
|
|
3871
|
+
COALESCE(ds.total, 0) AS total,
|
|
3872
|
+
COALESCE(ds.active, 0) AS active,
|
|
3873
|
+
COALESCE(ds.errored, 0) AS errored,
|
|
3874
|
+
COALESCE(ds.chunked, 0) AS chunked,
|
|
3875
|
+
COALESCE(ch.chunk_count, 0) AS chunk_count,
|
|
3876
|
+
COALESCE(ch.embedded_count, 0) AS embedded_count
|
|
3834
3877
|
FROM collections c
|
|
3835
|
-
LEFT JOIN
|
|
3836
|
-
|
|
3878
|
+
LEFT JOIN document_stats ds ON ds.collection = c.name
|
|
3879
|
+
LEFT JOIN collection_chunks ch ON ch.collection = c.name
|
|
3880
|
+
ORDER BY c.name
|
|
3837
3881
|
`
|
|
3838
3882
|
)
|
|
3839
3883
|
.all(embedModel, embedModel, embedFingerprint);
|
|
@@ -3865,21 +3909,27 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
3865
3909
|
[string | null, string | null, string | null]
|
|
3866
3910
|
>(
|
|
3867
3911
|
`
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
WHERE
|
|
3872
|
-
)
|
|
3873
|
-
|
|
3874
|
-
SELECT
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
))
|
|
3881
|
-
AND v.embedded_at >= c.created_at
|
|
3912
|
+
WITH active_mirrors AS (
|
|
3913
|
+
SELECT DISTINCT mirror_hash
|
|
3914
|
+
FROM documents
|
|
3915
|
+
WHERE active = 1 AND mirror_hash IS NOT NULL
|
|
3916
|
+
),
|
|
3917
|
+
matching_vectors AS (
|
|
3918
|
+
SELECT mirror_hash, seq, MAX(embedded_at) AS embedded_at
|
|
3919
|
+
FROM content_vectors
|
|
3920
|
+
WHERE (? IS NULL OR (
|
|
3921
|
+
model = ? AND embed_fingerprint = ?
|
|
3922
|
+
))
|
|
3923
|
+
GROUP BY mirror_hash, seq
|
|
3882
3924
|
)
|
|
3925
|
+
SELECT COUNT(*) AS count
|
|
3926
|
+
FROM active_mirrors am
|
|
3927
|
+
JOIN content_chunks c ON c.mirror_hash = am.mirror_hash
|
|
3928
|
+
LEFT JOIN matching_vectors mv
|
|
3929
|
+
ON mv.mirror_hash = c.mirror_hash
|
|
3930
|
+
AND mv.seq = c.seq
|
|
3931
|
+
AND mv.embedded_at >= c.created_at
|
|
3932
|
+
WHERE mv.mirror_hash IS NULL
|
|
3883
3933
|
`
|
|
3884
3934
|
)
|
|
3885
3935
|
.get(embedModel, embedModel, embedFingerprint);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Config } from "../../config/types";
|
|
2
|
+
|
|
3
|
+
import { getIndexDbPath } from "../../app/constants";
|
|
4
|
+
import { indexesMatch } from "../../core/indexed-reference";
|
|
5
|
+
import { SqliteAdapter } from "./adapter";
|
|
6
|
+
|
|
7
|
+
export interface ScopedIndexStore {
|
|
8
|
+
store: SqliteAdapter;
|
|
9
|
+
indexName?: string;
|
|
10
|
+
owned: boolean;
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function openScopedIndexStore(options: {
|
|
15
|
+
activeStore: SqliteAdapter;
|
|
16
|
+
activeIndexName?: string;
|
|
17
|
+
requestedIndexName?: string;
|
|
18
|
+
config: Config;
|
|
19
|
+
configPath: string | null;
|
|
20
|
+
}): Promise<ScopedIndexStore> {
|
|
21
|
+
const requestedIndexName =
|
|
22
|
+
options.requestedIndexName ?? options.activeIndexName;
|
|
23
|
+
if (indexesMatch(requestedIndexName, options.activeIndexName)) {
|
|
24
|
+
return {
|
|
25
|
+
store: options.activeStore,
|
|
26
|
+
indexName: requestedIndexName,
|
|
27
|
+
owned: false,
|
|
28
|
+
close: () => Promise.resolve(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const dbPath = getIndexDbPath(requestedIndexName);
|
|
33
|
+
if (!(await Bun.file(dbPath).exists())) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Index "${requestedIndexName}" does not exist at ${dbPath}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const store = new SqliteAdapter();
|
|
40
|
+
store.setConfigPath(options.configPath ?? "<inline-config>");
|
|
41
|
+
const openResult = await store.open(dbPath, options.config.ftsTokenizer);
|
|
42
|
+
if (!openResult.ok) {
|
|
43
|
+
throw new Error(openResult.error.message);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const collectionsResult = await store.syncCollections(
|
|
47
|
+
options.config.collections
|
|
48
|
+
);
|
|
49
|
+
if (!collectionsResult.ok) {
|
|
50
|
+
await store.close();
|
|
51
|
+
throw new Error(collectionsResult.error.message);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const contextsResult = await store.syncContexts(
|
|
55
|
+
options.config.contexts ?? []
|
|
56
|
+
);
|
|
57
|
+
if (!contextsResult.ok) {
|
|
58
|
+
await store.close();
|
|
59
|
+
throw new Error(contextsResult.error.message);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
store,
|
|
64
|
+
indexName: requestedIndexName,
|
|
65
|
+
owned: true,
|
|
66
|
+
close: () => store.close(),
|
|
67
|
+
};
|
|
68
|
+
}
|
package/src/store/types.ts
CHANGED
|
@@ -1110,7 +1110,9 @@ export interface StorePort {
|
|
|
1110
1110
|
/**
|
|
1111
1111
|
* Rebuild derived semantic edges from currently indexed links.
|
|
1112
1112
|
*/
|
|
1113
|
-
backfillDocEdges(
|
|
1113
|
+
backfillDocEdges(
|
|
1114
|
+
sourceDocumentIds?: number[]
|
|
1115
|
+
): Promise<StoreResult<{ inserted: number }>>;
|
|
1114
1116
|
|
|
1115
1117
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1116
1118
|
// Graph
|