@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,513 @@
1
+ /** Shared, passive activation status for CLI, REST, and UI surfaces. */
2
+
3
+ import type {
4
+ ActivationStageName,
5
+ ActivationStageReceipt,
6
+ ActivationVerificationCode,
7
+ ActivationVerificationReceipt,
8
+ StorePort,
9
+ StoreResult,
10
+ } from "../store/types";
11
+ import type { EphemeralActivationProbePlan } from "./activation-probe-plan";
12
+ import type {
13
+ ConnectorVerificationCode,
14
+ ConnectorVerificationTarget,
15
+ } from "./connector-verifier";
16
+
17
+ import { createEphemeralActivationProbePlan } from "./activation-probe-plan";
18
+ import { verifyLexicalActivation } from "./activation-verifier";
19
+ import {
20
+ getConnectorActivationReceiptLookup,
21
+ getConnectorVerificationRemediation,
22
+ } from "./connector-verifier";
23
+
24
+ const DEFAULT_CONCURRENCY = 4;
25
+ const MAX_CONNECTOR_TARGETS = 16;
26
+ const MAX_CONNECTOR_PROJECTIONS = 64;
27
+ const CONNECTOR_CODES = new Set<ActivationVerificationCode>([
28
+ "connector_not_configured",
29
+ "connector_probe_unavailable",
30
+ "connector_unsupported_config",
31
+ "connector_start_failed",
32
+ "connector_timeout",
33
+ "connector_missing_tools",
34
+ "connector_status_failed",
35
+ "connector_search_failed",
36
+ "connector_result_mismatch",
37
+ "target_runtime_unverifiable",
38
+ ]);
39
+
40
+ export type SemanticAvailabilityCode =
41
+ | "models_missing"
42
+ | "embeddings_pending"
43
+ | "vector_unavailable"
44
+ | "semantic_not_checked";
45
+
46
+ export interface ActivationRemediation {
47
+ stage: ActivationStageName;
48
+ code: ActivationVerificationCode;
49
+ command: string;
50
+ message: string;
51
+ }
52
+
53
+ export interface ActivationCollectionStatus {
54
+ collection: string;
55
+ ready: boolean;
56
+ generatedAt: string | null;
57
+ stages: Record<ActivationStageName, ActivationStageReceipt>;
58
+ semanticAvailability: {
59
+ status: "pending" | "skipped";
60
+ code: SemanticAvailabilityCode;
61
+ command: string;
62
+ };
63
+ remediation: ActivationRemediation | null;
64
+ }
65
+
66
+ export interface ActivationConnectorStatus {
67
+ collection: string;
68
+ target: string;
69
+ status: ActivationStageReceipt["status"];
70
+ code?: ActivationVerificationCode;
71
+ remediation: string | null;
72
+ }
73
+
74
+ export interface ActivationStatus {
75
+ schemaVersion: "1.0";
76
+ usable: boolean;
77
+ healthy: boolean;
78
+ collections: ActivationCollectionStatus[];
79
+ /** Only fingerprint-current persisted connector receipts may appear here. */
80
+ connectors: ActivationConnectorStatus[];
81
+ connectorProjection: {
82
+ total: number;
83
+ projected: number;
84
+ truncated: boolean;
85
+ };
86
+ }
87
+
88
+ export interface ActivationStatusOptions {
89
+ concurrency?: number;
90
+ semantic?: {
91
+ modelsCached: boolean;
92
+ embeddingBacklog: number;
93
+ /** Omit when the passive caller has not initialized the vector runtime. */
94
+ vectorAvailable?: boolean;
95
+ };
96
+ /** Current target configs, inspected without starting their runtimes. */
97
+ connectorTargets?: readonly ConnectorVerificationTarget[];
98
+ /** Test seam. Production callers use the shipped local lexical verifier. */
99
+ verifyCollection?: (
100
+ store: StorePort,
101
+ collection: string,
102
+ plan?: EphemeralActivationProbePlan
103
+ ) => Promise<StoreResult<ActivationVerificationReceipt>>;
104
+ /** Test seam for proving fingerprint-scoped coalescing with a stub verifier. */
105
+ prepareCollection?: (
106
+ store: StorePort,
107
+ collection: string
108
+ ) => Promise<StoreResult<EphemeralActivationProbePlan>>;
109
+ }
110
+
111
+ interface VerifiedCollection {
112
+ projected: ActivationCollectionStatus;
113
+ receipt: ActivationVerificationReceipt | null;
114
+ }
115
+
116
+ const inflightByStore = new WeakMap<
117
+ StorePort,
118
+ Map<string, Promise<StoreResult<ActivationVerificationReceipt>>>
119
+ >();
120
+
121
+ function pendingStage(
122
+ code: ActivationVerificationCode
123
+ ): ActivationStageReceipt {
124
+ return {
125
+ status: "pending",
126
+ startedAt: null,
127
+ completedAt: null,
128
+ latencyMs: null,
129
+ code,
130
+ };
131
+ }
132
+
133
+ function failedStage(code: ActivationVerificationCode): ActivationStageReceipt {
134
+ return {
135
+ status: "failed",
136
+ startedAt: null,
137
+ completedAt: null,
138
+ latencyMs: null,
139
+ code,
140
+ };
141
+ }
142
+
143
+ function skippedStage(
144
+ code: ActivationVerificationCode
145
+ ): ActivationStageReceipt {
146
+ return {
147
+ status: "skipped",
148
+ startedAt: null,
149
+ completedAt: null,
150
+ latencyMs: null,
151
+ code,
152
+ };
153
+ }
154
+
155
+ function failureReceipt(collection: string): ActivationCollectionStatus {
156
+ const code = "index_query_failed" as const;
157
+ return {
158
+ collection,
159
+ ready: false,
160
+ generatedAt: null,
161
+ stages: {
162
+ index: failedStage(code),
163
+ lexical: skippedStage(code),
164
+ semantic: pendingStage("semantic_not_checked"),
165
+ connector: skippedStage("connector_not_requested"),
166
+ },
167
+ semanticAvailability: {
168
+ status: "pending",
169
+ code: "semantic_not_checked",
170
+ command: "gno status",
171
+ },
172
+ remediation: remediationFor(collection, "index", code),
173
+ };
174
+ }
175
+
176
+ function remediationFor(
177
+ collection: string,
178
+ stage: ActivationStageName,
179
+ code: ActivationVerificationCode
180
+ ): ActivationRemediation {
181
+ const command = `gno index ${collection} --no-embed`;
182
+ const messages: Partial<Record<ActivationVerificationCode, string>> = {
183
+ no_documents:
184
+ "Index at least one supported text document in this collection.",
185
+ no_probe_term:
186
+ "Add searchable text or adjust the collection filters, then reindex.",
187
+ index_query_failed:
188
+ "Repair the local index and rerun the collection-scoped lexical proof.",
189
+ index_out_of_sync:
190
+ "Rebuild this collection's lexical index so its FTS rows match the current mirrors.",
191
+ retrieval_mismatch:
192
+ "Rebuild the collection index so lexical results match the current source.",
193
+ };
194
+ return {
195
+ stage,
196
+ code,
197
+ command,
198
+ message:
199
+ messages[code] ??
200
+ "Repair the collection-scoped lexical proof, then check activation again.",
201
+ };
202
+ }
203
+
204
+ function semanticAvailability(
205
+ options: ActivationStatusOptions["semantic"]
206
+ ): ActivationCollectionStatus["semanticAvailability"] {
207
+ if (!options) {
208
+ return {
209
+ status: "pending",
210
+ code: "semantic_not_checked",
211
+ command: "gno status",
212
+ };
213
+ }
214
+ if (!options.modelsCached) {
215
+ return {
216
+ status: "pending",
217
+ code: "models_missing",
218
+ command: "gno models pull --embed",
219
+ };
220
+ }
221
+ if (options.embeddingBacklog > 0) {
222
+ return {
223
+ status: "pending",
224
+ code: "embeddings_pending",
225
+ command: "gno embed",
226
+ };
227
+ }
228
+ if (options.vectorAvailable === false) {
229
+ return {
230
+ status: "skipped",
231
+ code: "vector_unavailable",
232
+ command: "gno doctor",
233
+ };
234
+ }
235
+ return {
236
+ status: "pending",
237
+ code: "semantic_not_checked",
238
+ command: "gno status",
239
+ };
240
+ }
241
+
242
+ function projectReceipt(
243
+ receipt: ActivationVerificationReceipt,
244
+ semantic: ActivationCollectionStatus["semanticAvailability"]
245
+ ): ActivationCollectionStatus {
246
+ const failedStageEntry = (["index", "lexical"] as const).find(
247
+ (stage) => receipt.stages[stage].status !== "passed"
248
+ );
249
+ const failedStageReceipt = failedStageEntry
250
+ ? receipt.stages[failedStageEntry]
251
+ : undefined;
252
+ return {
253
+ collection: receipt.collection,
254
+ ready: receipt.ready,
255
+ generatedAt: receipt.generatedAt,
256
+ stages: receipt.stages,
257
+ semanticAvailability: semantic,
258
+ remediation:
259
+ failedStageEntry && failedStageReceipt?.code
260
+ ? remediationFor(
261
+ receipt.collection,
262
+ failedStageEntry,
263
+ failedStageReceipt.code
264
+ )
265
+ : null,
266
+ };
267
+ }
268
+
269
+ function connectorFallback(
270
+ collection: string,
271
+ target: ConnectorVerificationTarget,
272
+ lexicalReady: boolean
273
+ ): ActivationConnectorStatus {
274
+ let status: ActivationStageReceipt["status"] = "pending";
275
+ let code: ActivationVerificationCode = "connector_not_requested";
276
+ if (!lexicalReady) {
277
+ status = "skipped";
278
+ code = "connector_probe_unavailable";
279
+ } else if (target.configError) {
280
+ status = "failed";
281
+ code = "connector_unsupported_config";
282
+ } else if (target.kind === "skill") {
283
+ status = "skipped";
284
+ code = target.installed
285
+ ? "target_runtime_unverifiable"
286
+ : "connector_not_configured";
287
+ } else if (!target.configured) {
288
+ status = target.configError ? "failed" : "skipped";
289
+ code = target.configError
290
+ ? "connector_unsupported_config"
291
+ : "connector_not_configured";
292
+ }
293
+ const remediation =
294
+ code === "connector_not_requested"
295
+ ? `Run explicit read-only verification for ${target.id} from Connectors.`
296
+ : connectorRemediation(code, target.id);
297
+ return {
298
+ collection,
299
+ target: target.id,
300
+ status,
301
+ code,
302
+ remediation,
303
+ };
304
+ }
305
+
306
+ function connectorRemediation(
307
+ code: ActivationVerificationCode,
308
+ target: string
309
+ ): string {
310
+ return CONNECTOR_CODES.has(code)
311
+ ? getConnectorVerificationRemediation(
312
+ code as ConnectorVerificationCode,
313
+ target
314
+ )
315
+ : `Repeat explicit read-only verification for ${target}.`;
316
+ }
317
+
318
+ async function buildConnectorStatuses(
319
+ store: StorePort,
320
+ collections: VerifiedCollection[],
321
+ targets: readonly ConnectorVerificationTarget[]
322
+ ): Promise<{ items: ActivationConnectorStatus[]; total: number }> {
323
+ const sortedTargets = [...targets].sort((a, b) => a.id.localeCompare(b.id));
324
+ const boundedTargets = sortedTargets.slice(0, MAX_CONNECTOR_TARGETS);
325
+ const allPairs = collections.flatMap((collection) =>
326
+ boundedTargets.map((target) => ({ collection, target }))
327
+ );
328
+ const items = await mapBounded(
329
+ allPairs.slice(0, MAX_CONNECTOR_PROJECTIONS),
330
+ DEFAULT_CONCURRENCY,
331
+ async ({ collection, target }) => {
332
+ const fallback = connectorFallback(
333
+ collection.projected.collection,
334
+ target,
335
+ collection.projected.ready
336
+ );
337
+ if (!collection.receipt || fallback.code !== "connector_not_requested") {
338
+ return fallback;
339
+ }
340
+ const lookup = getConnectorActivationReceiptLookup(
341
+ collection.receipt.fingerprint,
342
+ target
343
+ );
344
+ const cached = await store.getActivationReceipt(
345
+ collection.projected.collection,
346
+ lookup.fingerprint,
347
+ lookup.connectorTarget
348
+ );
349
+ if (!cached.ok || !cached.value) {
350
+ return fallback;
351
+ }
352
+ const stage = cached.value.stages.connector;
353
+ const code = stage.code;
354
+ return {
355
+ collection: collection.projected.collection,
356
+ target: target.id,
357
+ status: stage.status,
358
+ ...(code ? { code } : {}),
359
+ remediation:
360
+ code && code !== "connector_not_requested"
361
+ ? connectorRemediation(code, target.id)
362
+ : null,
363
+ };
364
+ }
365
+ );
366
+ return { items, total: collections.length * sortedTargets.length };
367
+ }
368
+
369
+ async function verifyCoalesced(
370
+ store: StorePort,
371
+ collection: string,
372
+ fingerprint: string,
373
+ verifyCollection: NonNullable<ActivationStatusOptions["verifyCollection"]>
374
+ ): Promise<StoreResult<ActivationVerificationReceipt>> {
375
+ let storeInflight = inflightByStore.get(store);
376
+ if (!storeInflight) {
377
+ storeInflight = new Map();
378
+ inflightByStore.set(store, storeInflight);
379
+ }
380
+ const key = `${collection}\0${fingerprint}`;
381
+ const existing = storeInflight.get(key);
382
+ if (existing) {
383
+ return existing;
384
+ }
385
+ const verification = verifyCollection(store, collection);
386
+ storeInflight.set(key, verification);
387
+ try {
388
+ return await verification;
389
+ } finally {
390
+ if (storeInflight.get(key) === verification) {
391
+ storeInflight.delete(key);
392
+ }
393
+ }
394
+ }
395
+
396
+ async function mapBounded<T, R>(
397
+ values: T[],
398
+ concurrency: number,
399
+ mapper: (value: T) => Promise<R>
400
+ ): Promise<R[]> {
401
+ const results: R[] = [];
402
+ results.length = values.length;
403
+ let cursor = 0;
404
+ const workers = Array.from(
405
+ { length: Math.min(concurrency, values.length) },
406
+ async () => {
407
+ while (cursor < values.length) {
408
+ const index = cursor;
409
+ cursor += 1;
410
+ const value = values[index];
411
+ if (value !== undefined) {
412
+ results[index] = await mapper(value);
413
+ }
414
+ }
415
+ }
416
+ );
417
+ await Promise.all(workers);
418
+ return results;
419
+ }
420
+
421
+ /**
422
+ * Build deterministic activation status without loading models, starting a
423
+ * connector, making a remote call, or retaining a settled cache.
424
+ */
425
+ export async function buildActivationStatus(
426
+ store: StorePort,
427
+ configuredCollections: readonly string[],
428
+ options: ActivationStatusOptions = {}
429
+ ): Promise<ActivationStatus> {
430
+ const collections = [...new Set(configuredCollections)].sort((a, b) =>
431
+ a.localeCompare(b)
432
+ );
433
+ const verifyCollection = options.verifyCollection;
434
+ const prepareCollection =
435
+ options.prepareCollection ??
436
+ (verifyCollection
437
+ ? null
438
+ : (targetStore: StorePort, collection: string) =>
439
+ createEphemeralActivationProbePlan(targetStore, collection, {
440
+ collectCandidates: false,
441
+ }));
442
+ const semantic = semanticAvailability(options.semantic);
443
+ const verified = await mapBounded(
444
+ collections,
445
+ Math.max(1, Math.floor(options.concurrency ?? DEFAULT_CONCURRENCY)),
446
+ async (collection) => {
447
+ try {
448
+ if (!prepareCollection && verifyCollection) {
449
+ const receipt = await verifyCoalesced(
450
+ store,
451
+ collection,
452
+ collection,
453
+ verifyCollection
454
+ );
455
+ return receipt.ok
456
+ ? {
457
+ projected: projectReceipt(receipt.value, semantic),
458
+ receipt: receipt.value,
459
+ }
460
+ : { projected: failureReceipt(collection), receipt: null };
461
+ }
462
+ if (!prepareCollection) {
463
+ return { projected: failureReceipt(collection), receipt: null };
464
+ }
465
+ const prepared = await prepareCollection(store, collection);
466
+ if (!prepared.ok) {
467
+ return { projected: failureReceipt(collection), receipt: null };
468
+ }
469
+ const runVerification = verifyCollection
470
+ ? (targetStore: StorePort, targetCollection: string) =>
471
+ verifyCollection(targetStore, targetCollection, prepared.value)
472
+ : (targetStore: StorePort, targetCollection: string) =>
473
+ verifyLexicalActivation(targetStore, targetCollection, {
474
+ plan: prepared.value,
475
+ });
476
+ const receipt = await verifyCoalesced(
477
+ store,
478
+ collection,
479
+ prepared.value.fingerprint,
480
+ runVerification
481
+ );
482
+ return receipt.ok
483
+ ? {
484
+ projected: projectReceipt(receipt.value, semantic),
485
+ receipt: receipt.value,
486
+ }
487
+ : { projected: failureReceipt(collection), receipt: null };
488
+ } catch {
489
+ return { projected: failureReceipt(collection), receipt: null };
490
+ }
491
+ }
492
+ );
493
+ const projected = verified.map(({ projected: collection }) => collection);
494
+ const connectorStatuses = await buildConnectorStatuses(
495
+ store,
496
+ verified,
497
+ options.connectorTargets ?? []
498
+ );
499
+
500
+ return {
501
+ schemaVersion: "1.0",
502
+ usable: projected.some((collection) => collection.ready),
503
+ healthy:
504
+ projected.length > 0 && projected.every((collection) => collection.ready),
505
+ collections: projected,
506
+ connectors: connectorStatuses.items,
507
+ connectorProjection: {
508
+ total: connectorStatuses.total,
509
+ projected: connectorStatuses.items.length,
510
+ truncated: connectorStatuses.total > connectorStatuses.items.length,
511
+ },
512
+ };
513
+ }