@gmickel/gno 1.12.3 → 1.13.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 (69) hide show
  1. package/README.md +57 -30
  2. package/assets/skill/SKILL.md +6 -1
  3. package/assets/skill/cli-reference.md +16 -6
  4. package/assets/skill/mcp-reference.md +22 -3
  5. package/package.json +2 -1
  6. package/src/app/constants.ts +43 -10
  7. package/src/app/index-name.ts +127 -0
  8. package/src/cli/commands/doctor-activation.ts +151 -0
  9. package/src/cli/commands/doctor.ts +41 -16
  10. package/src/cli/commands/get.ts +18 -0
  11. package/src/cli/commands/mcp/atomic-config-write.ts +118 -0
  12. package/src/cli/commands/mcp/config-discovery.ts +42 -0
  13. package/src/cli/commands/mcp/config-editors.ts +432 -0
  14. package/src/cli/commands/mcp/config.ts +63 -160
  15. package/src/cli/commands/mcp/install.ts +75 -37
  16. package/src/cli/commands/mcp/paths.ts +141 -136
  17. package/src/cli/commands/mcp/server-entry.ts +66 -0
  18. package/src/cli/commands/mcp/status.ts +189 -57
  19. package/src/cli/commands/mcp/target-display.ts +30 -0
  20. package/src/cli/commands/mcp/uninstall.ts +29 -31
  21. package/src/cli/commands/mcp/yaml-config-editor.ts +257 -0
  22. package/src/cli/commands/mcp/yaml-layout-scanner.ts +447 -0
  23. package/src/cli/commands/multi-get.ts +31 -6
  24. package/src/cli/commands/status.ts +107 -11
  25. package/src/cli/program.ts +66 -20
  26. package/src/core/activation-connector-health.ts +19 -0
  27. package/src/core/activation-probe-plan.ts +321 -0
  28. package/src/core/activation-probe.ts +138 -0
  29. package/src/core/activation-receipt-store.ts +39 -0
  30. package/src/core/activation-status.ts +513 -0
  31. package/src/core/activation-verifier.ts +416 -0
  32. package/src/core/connector-environment.ts +68 -0
  33. package/src/core/connector-policy.ts +233 -0
  34. package/src/core/connector-verification-target.ts +150 -0
  35. package/src/core/connector-verifier.ts +497 -0
  36. package/src/core/context-resolver.ts +285 -0
  37. package/src/core/indexed-reference.ts +33 -8
  38. package/src/core/runtime-entrypoint.ts +24 -0
  39. package/src/mcp/activation-verification-mode.ts +4 -0
  40. package/src/mcp/server.ts +9 -2
  41. package/src/mcp/tools/index.ts +3 -3
  42. package/src/pipeline/answer-prompt.ts +80 -0
  43. package/src/pipeline/answer.ts +12 -26
  44. package/src/pipeline/hybrid.ts +2 -0
  45. package/src/pipeline/result-context.ts +51 -0
  46. package/src/pipeline/search.ts +5 -1
  47. package/src/pipeline/vsearch.ts +2 -0
  48. package/src/sdk/client.ts +7 -0
  49. package/src/sdk/types.ts +1 -0
  50. package/src/serve/activation-health.ts +91 -0
  51. package/src/serve/background-runtime.ts +11 -1
  52. package/src/serve/connectors.ts +164 -19
  53. package/src/serve/public/components/BootstrapStatus.tsx +94 -1
  54. package/src/serve/public/components/FirstRunWizard.tsx +13 -51
  55. package/src/serve/public/components/HealthCenter.tsx +8 -2
  56. package/src/serve/public/globals.built.css +1 -1
  57. package/src/serve/public/pages/Connectors.tsx +216 -55
  58. package/src/serve/public/pages/Dashboard.tsx +1 -0
  59. package/src/serve/routes/api.ts +152 -8
  60. package/src/serve/server.ts +44 -9
  61. package/src/serve/status-model.ts +4 -0
  62. package/src/serve/status.ts +79 -35
  63. package/src/store/activation-receipts.ts +390 -0
  64. package/src/store/index.ts +8 -0
  65. package/src/store/migrations/012-activation-receipts.ts +38 -0
  66. package/src/store/migrations/013-fts-sync-marker.ts +39 -0
  67. package/src/store/migrations/index.ts +4 -0
  68. package/src/store/sqlite/adapter.ts +320 -53
  69. package/src/store/types.ts +124 -0
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Ephemeral activation probe planning shared by local and connector proofs.
3
+ *
4
+ * Raw probe terms never leave this in-memory plan and must not be serialized,
5
+ * logged, or copied into activation receipts.
6
+ */
7
+
8
+ import type { FtsTokenizer } from "../config/types";
9
+ import type {
10
+ ActivationIndexDocument,
11
+ ActivationIndexIdentity,
12
+ StorePort,
13
+ StoreResult,
14
+ } from "../store/types";
15
+
16
+ import { err, ok } from "../store/types";
17
+ import {
18
+ extractActivationProbeTerms,
19
+ fingerprintActivationIndex,
20
+ } from "./activation-probe";
21
+
22
+ const MAX_PROBE_DOCUMENTS = 16;
23
+ const MAX_PROBE_DOCUMENT_SCANS = 64;
24
+ const MAX_PROBE_ATTEMPTS = 64;
25
+ const MAX_PROBE_CONTENT_CHARS = 32_768;
26
+ const PROBE_RESULT_LIMIT = 8;
27
+
28
+ interface ProbeDocumentReference {
29
+ document: ActivationIndexDocument;
30
+ mirrorHash: string;
31
+ }
32
+
33
+ interface ProbeDocumentCandidates extends ProbeDocumentReference {
34
+ terms: Array<{ term: string; occurrence: number }>;
35
+ }
36
+
37
+ export interface EphemeralActivationProbeCandidate {
38
+ /** Sensitive corpus-derived term. Never serialize or log. */
39
+ term: string;
40
+ documents: ProbeDocumentReference[];
41
+ }
42
+
43
+ export interface EphemeralActivationProbePlan {
44
+ collection: string;
45
+ fingerprint: string;
46
+ identity: ActivationIndexIdentity;
47
+ activeDocuments: ActivationIndexDocument[];
48
+ candidates: EphemeralActivationProbeCandidate[];
49
+ }
50
+
51
+ export interface EphemeralActivationProbeMatch {
52
+ /** Sensitive corpus-derived term. Never serialize or log. */
53
+ term: string;
54
+ probeHash: string;
55
+ resultUri: string;
56
+ resultSourceHash: string;
57
+ }
58
+
59
+ export interface ActivationProbePlanRevalidation {
60
+ stable: boolean;
61
+ currentPlan: EphemeralActivationProbePlan;
62
+ }
63
+
64
+ export type ActivationProbeMatchResult =
65
+ | { kind: "matched"; value: EphemeralActivationProbeMatch }
66
+ | { kind: "no_probe_term" }
67
+ | { kind: "retrieval_mismatch"; probeHash?: string };
68
+
69
+ function sha256(value: string): string {
70
+ const hasher = new Bun.CryptoHasher("sha256");
71
+ hasher.update(value);
72
+ return hasher.digest("hex");
73
+ }
74
+
75
+ function compareText(left: string, right: string): number {
76
+ if (left === right) {
77
+ return 0;
78
+ }
79
+ return left < right ? -1 : 1;
80
+ }
81
+
82
+ async function collectProbeCandidates(
83
+ store: StorePort,
84
+ documents: ActivationIndexDocument[],
85
+ ftsTokenizer: FtsTokenizer
86
+ ): Promise<StoreResult<EphemeralActivationProbeCandidate[]>> {
87
+ const probeDocuments: ProbeDocumentCandidates[] = [];
88
+ let probeDocumentReads = 0;
89
+ for (const document of documents) {
90
+ if (!document.mirrorHash) {
91
+ continue;
92
+ }
93
+ if (probeDocumentReads >= MAX_PROBE_DOCUMENT_SCANS) {
94
+ break;
95
+ }
96
+ probeDocumentReads += 1;
97
+ const content = await store.getContentPrefix(
98
+ document.mirrorHash,
99
+ MAX_PROBE_CONTENT_CHARS
100
+ );
101
+ if (!content.ok) {
102
+ return err(
103
+ content.error.code,
104
+ content.error.message,
105
+ content.error.cause
106
+ );
107
+ }
108
+ if (content.value === null) {
109
+ continue;
110
+ }
111
+ const terms = extractActivationProbeTerms(content.value, ftsTokenizer);
112
+ if (terms.length === 0) {
113
+ continue;
114
+ }
115
+ probeDocuments.push({
116
+ document,
117
+ mirrorHash: document.mirrorHash,
118
+ terms: terms.map((term, occurrence) => ({ term, occurrence })),
119
+ });
120
+ if (probeDocuments.length >= MAX_PROBE_DOCUMENTS) {
121
+ break;
122
+ }
123
+ }
124
+
125
+ const documentsByTerm = new Map<string, ProbeDocumentReference[]>();
126
+ for (const probeDocument of probeDocuments) {
127
+ for (const { term } of probeDocument.terms) {
128
+ const references = documentsByTerm.get(term) ?? [];
129
+ references.push({
130
+ document: probeDocument.document,
131
+ mirrorHash: probeDocument.mirrorHash,
132
+ });
133
+ documentsByTerm.set(term, references);
134
+ }
135
+ }
136
+
137
+ for (const probeDocument of probeDocuments) {
138
+ probeDocument.terms.sort((left, right) => {
139
+ const frequencyDifference =
140
+ (documentsByTerm.get(left.term)?.length ?? 0) -
141
+ (documentsByTerm.get(right.term)?.length ?? 0);
142
+ if (frequencyDifference !== 0) {
143
+ return frequencyDifference;
144
+ }
145
+ if (left.occurrence !== right.occurrence) {
146
+ return left.occurrence - right.occurrence;
147
+ }
148
+ return compareText(left.term, right.term);
149
+ });
150
+ }
151
+
152
+ const candidates: EphemeralActivationProbeCandidate[] = [];
153
+ const seenTerms = new Set<string>();
154
+ const rounds = Math.max(
155
+ 0,
156
+ ...probeDocuments.map(({ terms }) => terms.length)
157
+ );
158
+ for (let round = 0; round < rounds; round += 1) {
159
+ for (const probeDocument of probeDocuments) {
160
+ const term = probeDocument.terms[round]?.term;
161
+ if (!term || seenTerms.has(term)) {
162
+ continue;
163
+ }
164
+ seenTerms.add(term);
165
+ candidates.push({
166
+ term,
167
+ documents: documentsByTerm.get(term) ?? [],
168
+ });
169
+ if (candidates.length >= MAX_PROBE_ATTEMPTS) {
170
+ return ok(candidates);
171
+ }
172
+ }
173
+ }
174
+ return ok(candidates);
175
+ }
176
+
177
+ export async function createEphemeralActivationProbePlan(
178
+ store: StorePort,
179
+ collection: string,
180
+ options: { collectCandidates?: boolean } = {}
181
+ ): Promise<StoreResult<EphemeralActivationProbePlan>> {
182
+ const snapshotResult = await store.getActivationIndexSnapshot(collection);
183
+ if (!snapshotResult.ok) {
184
+ return err(
185
+ snapshotResult.error.code,
186
+ snapshotResult.error.message,
187
+ snapshotResult.error.cause
188
+ );
189
+ }
190
+ const { documents, identity } = snapshotResult.value;
191
+ const activeDocuments = documents
192
+ .filter((document) => document.active)
193
+ .sort((left, right) => {
194
+ const uriDifference = compareText(left.uri, right.uri);
195
+ return uriDifference === 0 ? left.id - right.id : uriDifference;
196
+ });
197
+ const fingerprint = fingerprintActivationIndex({
198
+ collection,
199
+ indexName: identity.indexName,
200
+ schemaVersion: identity.schemaVersion,
201
+ ftsTokenizer: identity.ftsTokenizer,
202
+ ftsStateHash: identity.ftsStateHash,
203
+ documents: activeDocuments,
204
+ });
205
+ const candidates =
206
+ options.collectCandidates === false
207
+ ? ok([])
208
+ : await collectProbeCandidates(
209
+ store,
210
+ activeDocuments,
211
+ identity.ftsTokenizer
212
+ );
213
+ if (!candidates.ok) {
214
+ return candidates;
215
+ }
216
+ return ok({
217
+ collection,
218
+ fingerprint,
219
+ identity,
220
+ activeDocuments,
221
+ candidates: candidates.value,
222
+ });
223
+ }
224
+
225
+ export async function populateEphemeralActivationProbePlan(
226
+ store: StorePort,
227
+ plan: EphemeralActivationProbePlan
228
+ ): Promise<StoreResult<EphemeralActivationProbePlan>> {
229
+ const candidates = await collectProbeCandidates(
230
+ store,
231
+ plan.activeDocuments,
232
+ plan.identity.ftsTokenizer
233
+ );
234
+ if (!candidates.ok) {
235
+ return candidates;
236
+ }
237
+ return ok({ ...plan, candidates: candidates.value });
238
+ }
239
+
240
+ /**
241
+ * Re-read the metadata-only activation snapshot and compare it with a plan
242
+ * created before an asynchronous proof. Callers use this optimistic guard
243
+ * immediately before accepting or persisting proof derived from that plan.
244
+ */
245
+ export async function revalidateEphemeralActivationProbePlan(
246
+ store: StorePort,
247
+ plan: EphemeralActivationProbePlan
248
+ ): Promise<StoreResult<ActivationProbePlanRevalidation>> {
249
+ const current = await createEphemeralActivationProbePlan(
250
+ store,
251
+ plan.collection,
252
+ { collectCandidates: false }
253
+ );
254
+ if (!current.ok) {
255
+ return current;
256
+ }
257
+ return ok({
258
+ stable: current.value.fingerprint === plan.fingerprint,
259
+ currentPlan: current.value,
260
+ });
261
+ }
262
+
263
+ export async function findEphemeralActivationProbeMatch(
264
+ store: StorePort,
265
+ plan: EphemeralActivationProbePlan
266
+ ): Promise<StoreResult<ActivationProbeMatchResult>> {
267
+ if (plan.candidates.length === 0) {
268
+ return ok({ kind: "no_probe_term" });
269
+ }
270
+
271
+ const activeDocumentsByUri = new Map(
272
+ plan.activeDocuments.map((document) => [document.uri, document])
273
+ );
274
+ let finalProbeHash: string | undefined;
275
+ for (const candidate of plan.candidates) {
276
+ const digestKey = candidate.documents
277
+ .map(({ mirrorHash }) => mirrorHash)
278
+ .sort(compareText)
279
+ .join("\0");
280
+ finalProbeHash = sha256(`${digestKey}\0${candidate.term}`);
281
+ const search = await store.searchFts(candidate.term, {
282
+ collection: plan.collection,
283
+ limit: PROBE_RESULT_LIMIT,
284
+ snippet: false,
285
+ });
286
+ if (!search.ok) {
287
+ return err(search.error.code, search.error.message, search.error.cause);
288
+ }
289
+ const matchedResult = search.value.find((result) => {
290
+ const activeDocument = result.uri
291
+ ? activeDocumentsByUri.get(result.uri)
292
+ : undefined;
293
+ return (
294
+ activeDocument !== undefined &&
295
+ activeDocument.mirrorHash !== null &&
296
+ result.sourceHash === activeDocument.sourceHash &&
297
+ result.mirrorHash === activeDocument.mirrorHash
298
+ );
299
+ });
300
+ if (!matchedResult) {
301
+ continue;
302
+ }
303
+ const matchedDocument = matchedResult.uri
304
+ ? activeDocumentsByUri.get(matchedResult.uri)
305
+ : undefined;
306
+ if (!matchedDocument) {
307
+ continue;
308
+ }
309
+ return ok({
310
+ kind: "matched",
311
+ value: {
312
+ term: candidate.term,
313
+ probeHash: finalProbeHash,
314
+ resultUri: matchedResult.uri ?? matchedDocument.uri,
315
+ resultSourceHash:
316
+ matchedResult.sourceHash ?? matchedDocument.sourceHash,
317
+ },
318
+ });
319
+ }
320
+ return ok({ kind: "retrieval_mismatch", probeHash: finalProbeHash });
321
+ }
@@ -0,0 +1,138 @@
1
+ import type { FtsTokenizer } from "../config/types";
2
+ import type { ActivationIndexDocument } from "../store/types";
3
+
4
+ const RECEIPT_SCHEMA_VERSION = "1.0";
5
+ const INDEX_IMPLEMENTATION_ID = "documents_fts-bm25-v1";
6
+ const MAX_CONTENT_CHARS_PER_DOCUMENT = 32_768;
7
+ const MAX_CANDIDATES_PER_DOCUMENT = 32;
8
+ const MAX_TERM_CODEPOINTS = 64;
9
+ const CJK_PREFIX_CODEPOINTS = 4;
10
+ const TOKEN_PATTERN = /[\p{L}\p{N}_]+/gu;
11
+ const LETTER_PATTERN = /\p{L}/u;
12
+ const CJK_PATTERN =
13
+ /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
14
+
15
+ const STOPWORDS = new Set([
16
+ "a",
17
+ "an",
18
+ "and",
19
+ "are",
20
+ "as",
21
+ "at",
22
+ "be",
23
+ "by",
24
+ "for",
25
+ "from",
26
+ "in",
27
+ "is",
28
+ "it",
29
+ "of",
30
+ "on",
31
+ "or",
32
+ "that",
33
+ "the",
34
+ "this",
35
+ "to",
36
+ "with",
37
+ "der",
38
+ "die",
39
+ "das",
40
+ "und",
41
+ "ein",
42
+ "eine",
43
+ "le",
44
+ "la",
45
+ "les",
46
+ "et",
47
+ "un",
48
+ "une",
49
+ "il",
50
+ "e",
51
+ "per",
52
+ "con",
53
+ ]);
54
+
55
+ function sha256(value: string): string {
56
+ const hasher = new Bun.CryptoHasher("sha256");
57
+ hasher.update(value);
58
+ return hasher.digest("hex");
59
+ }
60
+
61
+ function compareText(left: string, right: string): number {
62
+ if (left === right) {
63
+ return 0;
64
+ }
65
+ return left < right ? -1 : 1;
66
+ }
67
+
68
+ function normalizeTerm(raw: string, ftsTokenizer: FtsTokenizer): string | null {
69
+ const normalized = raw.normalize("NFC").toLocaleLowerCase("und");
70
+ const codepoints = Array.from(normalized);
71
+ const minimumCodepoints = ftsTokenizer === "trigram" ? 3 : 2;
72
+ if (
73
+ codepoints.length < minimumCodepoints ||
74
+ !LETTER_PATTERN.test(normalized) ||
75
+ STOPWORDS.has(normalized)
76
+ ) {
77
+ return null;
78
+ }
79
+ if (codepoints.length <= MAX_TERM_CODEPOINTS) {
80
+ return normalized;
81
+ }
82
+ if (CJK_PATTERN.test(normalized)) {
83
+ return codepoints.slice(0, CJK_PREFIX_CODEPOINTS).join("");
84
+ }
85
+ return null;
86
+ }
87
+
88
+ /** Extract bounded Unicode FTS-compatible candidates in occurrence order. */
89
+ export function extractActivationProbeTerms(
90
+ text: string,
91
+ ftsTokenizer: FtsTokenizer = "unicode61"
92
+ ): string[] {
93
+ const terms: string[] = [];
94
+ const seen = new Set<string>();
95
+ const input = text.slice(0, MAX_CONTENT_CHARS_PER_DOCUMENT);
96
+ for (const match of input.matchAll(TOKEN_PATTERN)) {
97
+ const term = normalizeTerm(match[0], ftsTokenizer);
98
+ if (!term || seen.has(term)) {
99
+ continue;
100
+ }
101
+ seen.add(term);
102
+ terms.push(term);
103
+ if (terms.length >= MAX_CANDIDATES_PER_DOCUMENT) {
104
+ break;
105
+ }
106
+ }
107
+ return terms;
108
+ }
109
+
110
+ export function fingerprintActivationIndex(input: {
111
+ collection: string;
112
+ indexName: string;
113
+ schemaVersion: number;
114
+ ftsTokenizer: string;
115
+ ftsStateHash: string;
116
+ documents: ActivationIndexDocument[];
117
+ }): string {
118
+ const documents = input.documents
119
+ .filter((document) => document.active)
120
+ .map((document) => ({
121
+ uri: document.uri,
122
+ sourceHash: document.sourceHash,
123
+ mirrorHash: document.mirrorHash,
124
+ }))
125
+ .sort((left, right) => compareText(left.uri, right.uri));
126
+ return sha256(
127
+ JSON.stringify({
128
+ receiptSchemaVersion: RECEIPT_SCHEMA_VERSION,
129
+ indexImplementation: INDEX_IMPLEMENTATION_ID,
130
+ indexName: input.indexName,
131
+ schemaVersion: input.schemaVersion,
132
+ ftsTokenizer: input.ftsTokenizer,
133
+ ftsStateHash: input.ftsStateHash,
134
+ collection: input.collection,
135
+ documents,
136
+ })
137
+ );
138
+ }
@@ -0,0 +1,39 @@
1
+ import type {
2
+ ActivationVerificationReceipt,
3
+ StorePort,
4
+ StoreResult,
5
+ } from "../store/types";
6
+
7
+ import { err, ok } from "../store/types";
8
+
9
+ /**
10
+ * Persist a receipt only after the collection has been synchronized into the
11
+ * store. Config can name a collection before its first sync creates the parent
12
+ * row required by the receipt table's foreign key.
13
+ */
14
+ export async function persistActivationReceiptForKnownCollection(
15
+ store: StorePort,
16
+ receipt: ActivationVerificationReceipt
17
+ ): Promise<StoreResult<ActivationVerificationReceipt>> {
18
+ const collections = await store.getCollections();
19
+ if (!collections.ok) {
20
+ return err(
21
+ collections.error.code,
22
+ collections.error.message,
23
+ collections.error.cause
24
+ );
25
+ }
26
+ if (!collections.value.some(({ name }) => name === receipt.collection)) {
27
+ return ok(receipt);
28
+ }
29
+
30
+ const persisted = await store.upsertActivationReceipt(receipt);
31
+ if (!persisted.ok) {
32
+ return err(
33
+ persisted.error.code,
34
+ persisted.error.message,
35
+ persisted.error.cause
36
+ );
37
+ }
38
+ return ok(receipt);
39
+ }