@gmickel/gno 1.26.0 → 1.27.1

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 (54) hide show
  1. package/README.md +2 -1
  2. package/assets/skill/SKILL.md +8 -0
  3. package/browser-extension/artifacts/{gno-browser-clipper-v1.26.0.zip → gno-browser-clipper-v1.27.1.zip} +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip.sha256 +1 -0
  5. package/browser-extension/dist/manifest.json +1 -1
  6. package/package.json +1 -1
  7. package/spec/cli.md +15 -2
  8. package/spec/evals-agentic.md +17 -0
  9. package/spec/mcp.md +25 -6
  10. package/spec/output-schemas/ask.schema.json +3 -0
  11. package/spec/output-schemas/query-diagnose.schema.json +61 -4
  12. package/spec/output-schemas/search-results.schema.json +87 -1
  13. package/spec/output-schemas/status.schema.json +24 -0
  14. package/spec/project-profile.schema.json +3 -1
  15. package/src/app/context-runtime-contract.ts +4 -1
  16. package/src/app/context-runtime-types.ts +2 -0
  17. package/src/app/context-runtime.ts +26 -0
  18. package/src/app/verified-ask.ts +6 -1
  19. package/src/cli/commands/ask.ts +8 -1
  20. package/src/cli/commands/query.ts +6 -3
  21. package/src/cli/commands/search.ts +6 -1
  22. package/src/cli/commands/status.ts +43 -7
  23. package/src/cli/program.ts +2 -0
  24. package/src/config/content-types.ts +82 -0
  25. package/src/config/index.ts +8 -0
  26. package/src/config/project-profile.ts +8 -1
  27. package/src/config/types.ts +11 -2
  28. package/src/core/context-compiler.ts +38 -1
  29. package/src/core/retrieval-replay-candidate.ts +6 -1
  30. package/src/ingestion/sync-options.ts +6 -2
  31. package/src/ingestion/sync.ts +21 -29
  32. package/src/ingestion/types.ts +1 -1
  33. package/src/mcp/tools/ask.ts +1 -0
  34. package/src/mcp/tools/index.ts +4 -0
  35. package/src/mcp/tools/query.ts +4 -2
  36. package/src/mcp/tools/search.ts +3 -0
  37. package/src/mcp/tools/status.ts +4 -0
  38. package/src/pipeline/content-type-boost.ts +264 -0
  39. package/src/pipeline/diagnose.ts +46 -19
  40. package/src/pipeline/explain.ts +15 -2
  41. package/src/pipeline/hybrid.ts +170 -74
  42. package/src/pipeline/rerank.ts +45 -15
  43. package/src/pipeline/search.ts +29 -11
  44. package/src/pipeline/types.ts +13 -4
  45. package/src/pipeline/vsearch.ts +30 -10
  46. package/src/sdk/client.ts +19 -3
  47. package/src/sdk/index.ts +1 -0
  48. package/src/sdk/types.ts +21 -5
  49. package/src/serve/routes/api.ts +17 -3
  50. package/src/serve/status-model.ts +2 -0
  51. package/src/serve/status.ts +4 -0
  52. package/src/store/sqlite/adapter.ts +3 -0
  53. package/src/store/types.ts +3 -2
  54. package/browser-extension/artifacts/gno-browser-clipper-v1.26.0.zip.sha256 +0 -1
@@ -11,13 +11,19 @@ import type { StorePort } from "../store/types";
11
11
  import type { VectorIndexPort } from "../store/vector/types";
12
12
  import type { SearchOptions, SearchResult, SearchResults } from "./types";
13
13
 
14
+ import { normalizeContentTypes } from "../config/content-types";
14
15
  import { getContentBatch } from "../store/content-batch";
15
16
  import { err, ok } from "../store/types";
16
17
  import { createChunkLookup } from "./chunk-lookup";
18
+ import {
19
+ applyContentTypeBoost,
20
+ hasAuxiliaryRanking,
21
+ sortByFinalScoreStable,
22
+ } from "./content-type-boost";
17
23
  import { formatQueryForEmbedding } from "./contextual";
18
24
  import { matchesExcludedChunks, matchesExcludedText } from "./exclude";
19
25
  import { selectBestChunkForSteering } from "./intent";
20
- import { applyProjectAffinity, hasProjectAffinity } from "./project-affinity";
26
+ import { hasProjectAffinity } from "./project-affinity";
21
27
  import { detectQueryLanguage } from "./query-language";
22
28
  import { attachSearchResultContexts } from "./result-context";
23
29
  import {
@@ -83,9 +89,17 @@ export async function searchVectorWithEmbedding(
83
89
  const { store, vectorIndex } = deps;
84
90
  const limit = options.limit ?? 20;
85
91
  const minScore = options.minScore ?? 0;
86
- const affinityActive = hasProjectAffinity(options.projectAffinity);
92
+ const contentTypeRules =
93
+ options.contentTypeRules ??
94
+ normalizeContentTypes(deps.config.contentTypes ?? []).rules;
95
+ const auxiliaryRankingActive = hasAuxiliaryRanking(
96
+ options.projectAffinity,
97
+ contentTypeRules
98
+ );
99
+ const projectAffinityActive = hasProjectAffinity(options.projectAffinity);
87
100
  const recencySort = shouldSortByRecency(query);
88
- const retrievalLimit = recencySort || affinityActive ? limit * 3 : limit;
101
+ const retrievalLimit =
102
+ recencySort || projectAffinityActive ? limit * 3 : limit;
89
103
  const temporalRange = resolveTemporalRange(
90
104
  query,
91
105
  options.since,
@@ -106,7 +120,7 @@ export async function searchVectorWithEmbedding(
106
120
  queryEmbedding,
107
121
  retrievalLimit,
108
122
  {
109
- minScore: affinityActive ? undefined : minScore,
123
+ minScore: projectAffinityActive ? undefined : minScore,
110
124
  allowedMirrorHashes: options.retrievalScope?.allowedMirrorHashes,
111
125
  }
112
126
  );
@@ -165,7 +179,7 @@ export async function searchVectorWithEmbedding(
165
179
 
166
180
  for (const vec of vecResults) {
167
181
  const baseScore = normalizeVectorScore(vec.distance);
168
- if (!affinityActive && baseScore < minScore) {
182
+ if (!projectAffinityActive && baseScore < minScore) {
169
183
  continue;
170
184
  }
171
185
 
@@ -196,7 +210,7 @@ export async function searchVectorWithEmbedding(
196
210
  if (!matchingDocs || matchingDocs.length === 0) {
197
211
  continue;
198
212
  }
199
- const docs = affinityActive ? matchingDocs : [matchingDocs.at(-1)!];
213
+ const docs = auxiliaryRankingActive ? matchingDocs : [matchingDocs.at(-1)!];
200
214
  for (const doc of docs) {
201
215
  const collectionPath = collectionPaths.get(doc.collection);
202
216
  const excluded =
@@ -218,7 +232,7 @@ export async function searchVectorWithEmbedding(
218
232
  continue;
219
233
  }
220
234
 
221
- const scoredResult = applyProjectAffinity(
235
+ const scoredResult = applyContentTypeBoost(
222
236
  {
223
237
  docid: doc.docid,
224
238
  score: baseScore,
@@ -254,7 +268,9 @@ export async function searchVectorWithEmbedding(
254
268
  : undefined,
255
269
  },
256
270
  doc.collection,
271
+ contentTypeRules,
257
272
  options.projectAffinity,
273
+ doc.contentTypeSource,
258
274
  { kind: "vector_distance", score: vec.distance }
259
275
  );
260
276
  if (scoredResult.score < minScore) continue;
@@ -317,7 +333,7 @@ export async function searchVectorWithEmbedding(
317
333
 
318
334
  const collectionPath = collectionPaths.get(doc.collection);
319
335
 
320
- const result = applyProjectAffinity(
336
+ const result = applyContentTypeBoost(
321
337
  {
322
338
  docid: doc.docid,
323
339
  score,
@@ -353,7 +369,9 @@ export async function searchVectorWithEmbedding(
353
369
  : undefined,
354
370
  },
355
371
  doc.collection,
372
+ contentTypeRules,
356
373
  options.projectAffinity,
374
+ doc.contentTypeSource,
357
375
  { kind: "vector_distance", score: rawDistance }
358
376
  );
359
377
  results.push(
@@ -390,8 +408,8 @@ export async function searchVectorWithEmbedding(
390
408
  }
391
409
  return b.score - a.score;
392
410
  });
393
- } else if (affinityActive) {
394
- results.sort((a, b) => b.score - a.score);
411
+ } else if (auxiliaryRankingActive) {
412
+ sortByFinalScoreStable(results);
395
413
  }
396
414
 
397
415
  const finalResults = results.slice(0, limit);
@@ -485,6 +503,7 @@ interface DocumentInfo {
485
503
  relPath: string;
486
504
  author: string | null;
487
505
  contentType: string | null;
506
+ contentTypeSource: string | null;
488
507
  categories: string[] | null;
489
508
  sourceHash: string;
490
509
  sourceMime: string;
@@ -625,6 +644,7 @@ async function buildDocumentMap(
625
644
  relPath: doc.relPath,
626
645
  author: doc.author ?? null,
627
646
  contentType: doc.contentType ?? null,
647
+ contentTypeSource: doc.contentTypeSource ?? null,
628
648
  categories: doc.categories ?? null,
629
649
  sourceHash: doc.sourceHash,
630
650
  sourceMime: doc.sourceMime,
package/src/sdk/client.ts CHANGED
@@ -11,7 +11,7 @@ import type { Config } from "../config/types";
11
11
  import type { DownloadPolicy } from "../llm/policy";
12
12
  import type { EmbeddingPort, GenerationPort, RerankPort } from "../llm/types";
13
13
  import type { AskResult, SearchResults } from "../pipeline/types";
14
- import type { IndexStatus, StoreResult } from "../store/types";
14
+ import type { StoreResult } from "../store/types";
15
15
  import type { VectorIndexPort } from "../store/vector";
16
16
  import type {
17
17
  GnoAskOptions,
@@ -32,6 +32,7 @@ import type {
32
32
  GnoGetOptions,
33
33
  GnoIndexOptions,
34
34
  GnoIndexResult,
35
+ GnoIndexStatus,
35
36
  GnoListOptions,
36
37
  GnoMoveNoteOptions,
37
38
  GnoMultiGetOptions,
@@ -65,9 +66,11 @@ import {
65
66
  } from "../app/index-name";
66
67
  import { buildVerifiedAsk } from "../app/verified-ask";
67
68
  import {
69
+ buildContentTypeBoostStatus,
68
70
  ConfigSchema,
69
71
  loadConfig,
70
72
  normalizeConfigContentTypes,
73
+ normalizeContentTypes,
71
74
  } from "../config";
72
75
  import {
73
76
  buildCaptureReceipt,
@@ -516,6 +519,9 @@ class GnoClientImpl implements GnoClient {
516
519
  await searchBm25(this.store, query, {
517
520
  ...searchOptions,
518
521
  projectAffinity,
522
+ contentTypeRules: normalizeContentTypes(
523
+ this.config.contentTypes ?? []
524
+ ).rules,
519
525
  traceSession: traceSession ?? undefined,
520
526
  })
521
527
  )
@@ -883,6 +889,7 @@ class GnoClientImpl implements GnoClient {
883
889
  noExpand: options.noExpand,
884
890
  noRerank: options.noRerank,
885
891
  candidateLimit: options.candidateLimit,
892
+ explain: options.explain,
886
893
  queryLanguageHint: options.queryLanguageHint,
887
894
  projectAffinity,
888
895
  traceSession: traceSession ?? undefined,
@@ -945,6 +952,9 @@ class GnoClientImpl implements GnoClient {
945
952
  answerGenerated,
946
953
  totalResults: searchResult.results.length,
947
954
  answerContext,
955
+ ...(options.explain && searchResult.meta.explain
956
+ ? { explain: searchResult.meta.explain }
957
+ : {}),
948
958
  },
949
959
  };
950
960
  if (answerRequested && traceSession) {
@@ -1169,13 +1179,19 @@ class GnoClientImpl implements GnoClient {
1169
1179
  );
1170
1180
  }
1171
1181
 
1172
- async status(): Promise<IndexStatus> {
1182
+ async status(): Promise<GnoIndexStatus> {
1173
1183
  this.assertOpen();
1174
- return unwrapStore(
1184
+ const status = unwrapStore(
1175
1185
  await this.store.getStatus({
1176
1186
  embedModel: resolveModelUri(this.config, "embed"),
1177
1187
  })
1178
1188
  );
1189
+ return {
1190
+ ...status,
1191
+ contentTypeBoost: buildContentTypeBoostStatus(
1192
+ this.config.contentTypes ?? []
1193
+ ),
1194
+ };
1179
1195
  }
1180
1196
 
1181
1197
  async listRetrievalTraces(
package/src/sdk/index.ts CHANGED
@@ -43,6 +43,7 @@ export type {
43
43
  GnoGetResult,
44
44
  GnoIndexOptions,
45
45
  GnoIndexResult,
46
+ GnoIndexStatus,
46
47
  GnoListDocument,
47
48
  GnoListOptions,
48
49
  GnoListResult,
package/src/sdk/types.ts CHANGED
@@ -8,6 +8,7 @@ import type {
8
8
  ContextCapsuleBuildInput,
9
9
  ContextRuntimeErrorCode,
10
10
  } from "../app/context-runtime";
11
+ import type { ContentTypeBoostStatus } from "../config/content-types";
11
12
  import type { Config } from "../config/types";
12
13
  import type { CaptureInput, CaptureReceipt } from "../core/capture";
13
14
  import type {
@@ -102,15 +103,27 @@ export interface GnoProjectHintOptions {
102
103
  projectHints?: string[];
103
104
  }
104
105
 
105
- export type GnoSearchOptions = Omit<SearchOptions, "projectAffinity"> &
106
+ export type GnoSearchOptions = Omit<
107
+ SearchOptions,
108
+ "contentTypeRules" | "projectAffinity"
109
+ > &
106
110
  GnoProjectHintOptions;
107
- export type GnoQueryOptions = Omit<HybridSearchOptions, "projectAffinity"> &
111
+ export type GnoQueryOptions = Omit<
112
+ HybridSearchOptions,
113
+ "contentTypeRules" | "projectAffinity"
114
+ > &
108
115
  GnoModelOverrides &
109
116
  GnoProjectHintOptions;
110
- export type GnoAskOptions = Omit<AskOptions, "projectAffinity"> &
117
+ export type GnoAskOptions = Omit<
118
+ AskOptions,
119
+ "contentTypeRules" | "projectAffinity"
120
+ > &
111
121
  GnoModelOverrides &
112
122
  GnoProjectHintOptions;
113
- export type GnoVectorSearchOptions = Omit<SearchOptions, "projectAffinity"> &
123
+ export type GnoVectorSearchOptions = Omit<
124
+ SearchOptions,
125
+ "contentTypeRules" | "projectAffinity"
126
+ > &
114
127
  GnoProjectHintOptions & {
115
128
  model?: string;
116
129
  };
@@ -118,6 +131,9 @@ export type GnoVectorSearchOptions = Omit<SearchOptions, "projectAffinity"> &
118
131
  export type GnoContextInput = Omit<ContextCapsuleBuildInput, "indexName"> &
119
132
  GnoProjectHintOptions;
120
133
  export type GnoContextResult = ContextCapsuleV1;
134
+ export type GnoIndexStatus = IndexStatus & {
135
+ contentTypeBoost: ContentTypeBoostStatus;
136
+ };
121
137
  export type GnoContextVerificationResult = ContextCapsuleVerification;
122
138
  export type GnoContextErrorCode =
123
139
  | ContextRuntimeErrorCode
@@ -268,7 +284,7 @@ export interface GnoClient {
268
284
  ref: string,
269
285
  options?: KnowledgeImpactInput
270
286
  ): Promise<KnowledgeImpactResult>;
271
- status(): Promise<IndexStatus>;
287
+ status(): Promise<GnoIndexStatus>;
272
288
  listRetrievalTraces(
273
289
  options?: RetrievalTraceListRequest
274
290
  ): Promise<RetrievalTraceListResult>;
@@ -45,7 +45,7 @@ import {
45
45
  updateCollection,
46
46
  } from "../../collection";
47
47
  import {
48
- fingerprintContentTypeRules,
48
+ fingerprintContentTypeMetadataRules,
49
49
  normalizeContentTypes,
50
50
  } from "../../config";
51
51
  import { type PublicCaptureInput } from "../../core/capture";
@@ -272,6 +272,7 @@ export interface QueryRequestBody {
272
272
  noRerank?: boolean;
273
273
  noGraph?: boolean;
274
274
  graph?: boolean;
275
+ explain?: boolean;
275
276
  /** Comma-separated tags - filter to docs having ALL (AND) */
276
277
  tagsAll?: string;
277
278
  /** Comma-separated tags - filter to docs having ANY (OR) */
@@ -306,6 +307,7 @@ export interface AskRequestBody {
306
307
  noRerank?: boolean;
307
308
  graph?: boolean;
308
309
  noGraph?: boolean;
310
+ explain?: boolean;
309
311
  /** Comma-separated tags - filter to docs having ALL (AND) */
310
312
  tagsAll?: string;
311
313
  /** Comma-separated tags - filter to docs having ANY (OR) */
@@ -335,6 +337,7 @@ const ASK_REQUEST_KEYS = new Set<keyof AskRequestBody>([
335
337
  "noRerank",
336
338
  "graph",
337
339
  "noGraph",
340
+ "explain",
338
341
  "tagsAll",
339
342
  "tagsAny",
340
343
  ]);
@@ -3510,7 +3513,6 @@ export async function handleSearch(
3510
3513
  if (body.author !== undefined && typeof body.author !== "string") {
3511
3514
  return errorResponse("VALIDATION", "author must be a string");
3512
3515
  }
3513
-
3514
3516
  // Parse tag filters
3515
3517
  let tagsAll: string[] | undefined;
3516
3518
  let tagsAny: string[] | undefined;
@@ -3572,6 +3574,9 @@ export async function handleSearch(
3572
3574
  categories,
3573
3575
  author,
3574
3576
  projectAffinity,
3577
+ contentTypeRules: context
3578
+ ? normalizeContentTypes(context.config.contentTypes ?? []).rules
3579
+ : undefined,
3575
3580
  };
3576
3581
 
3577
3582
  const trace = context
@@ -3679,6 +3684,9 @@ export async function handleQuery(
3679
3684
  if (body.author !== undefined && typeof body.author !== "string") {
3680
3685
  return errorResponse("VALIDATION", "author must be a string");
3681
3686
  }
3687
+ if (body.explain !== undefined && typeof body.explain !== "boolean") {
3688
+ return errorResponse("VALIDATION", "explain must be a boolean");
3689
+ }
3682
3690
 
3683
3691
  const { queryModes, error: queryModesError } = parseQueryModesInput(
3684
3692
  body.queryModes
@@ -3768,6 +3776,7 @@ export async function handleQuery(
3768
3776
  categories,
3769
3777
  author,
3770
3778
  projectAffinity,
3779
+ explain: body.explain,
3771
3780
  };
3772
3781
  const trace = await startRestTrace(ctx, {
3773
3782
  query: normalizedQuery,
@@ -3998,7 +4007,7 @@ export async function handleQueryDiagnose(
3998
4007
  projectAffinity,
3999
4008
  contentTypeRules,
4000
4009
  contentTypeRulesFingerprint:
4001
- fingerprintContentTypeRules(contentTypeRules),
4010
+ fingerprintContentTypeMetadataRules(contentTypeRules),
4002
4011
  }
4003
4012
  );
4004
4013
 
@@ -4047,6 +4056,7 @@ export async function handleAsk(
4047
4056
  "noRerank",
4048
4057
  "graph",
4049
4058
  "noGraph",
4059
+ "explain",
4050
4060
  ] as const) {
4051
4061
  if (body[field] !== undefined && typeof body[field] !== "boolean") {
4052
4062
  return errorResponse("VALIDATION", `${field} must be a boolean`);
@@ -4230,6 +4240,7 @@ export async function handleAsk(
4230
4240
  contextBudgetBytes: body.contextBudgetBytes,
4231
4241
  maxAnswerTokens: body.maxAnswerTokens,
4232
4242
  projectAffinity,
4243
+ explain: body.explain,
4233
4244
  };
4234
4245
  const trace = await startRestTrace(ctx, {
4235
4246
  query: normalizedQuery,
@@ -4446,6 +4457,9 @@ export async function handleAsk(
4446
4457
  answerGenerated,
4447
4458
  totalResults: results.length,
4448
4459
  answerContext,
4460
+ ...(body.explain && searchResult.value.meta.explain
4461
+ ? { explain: searchResult.value.meta.explain }
4462
+ : {}),
4449
4463
  },
4450
4464
  };
4451
4465
 
@@ -1,3 +1,4 @@
1
+ import type { ContentTypeBoostStatus } from "../config/content-types";
1
2
  import type { ActivationStatus } from "../core/activation-status";
2
3
 
3
4
  export type HealthCheckStatus = "ok" | "warn" | "error";
@@ -196,6 +197,7 @@ export interface AppStatusResponse {
196
197
  hybrid: boolean;
197
198
  answer: boolean;
198
199
  };
200
+ contentTypeBoost: ContentTypeBoostStatus;
199
201
  activation: ActivationStatus;
200
202
  onboarding: OnboardingState;
201
203
  health: HealthCenterState;
@@ -13,6 +13,7 @@ import type {
13
13
  } from "./status-model";
14
14
 
15
15
  import { getModelsCachePath } from "../app/constants";
16
+ import { buildContentTypeBoostStatus } from "../config/content-types";
16
17
  import {
17
18
  type ActivationStatus,
18
19
  buildActivationStatus,
@@ -740,6 +741,9 @@ export async function buildAppStatus(
740
741
  name: preset.name,
741
742
  },
742
743
  capabilities: ctx.capabilities,
744
+ contentTypeBoost: buildContentTypeBoostStatus(
745
+ ctx.config.contentTypes ?? []
746
+ ),
743
747
  activation,
744
748
  onboarding: buildOnboarding(
745
749
  status,
@@ -2186,6 +2186,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
2186
2186
  d.source_size,
2187
2187
  d.source_hash,
2188
2188
  d.content_type,
2189
+ d.content_type_source,
2189
2190
  d.categories
2190
2191
  FROM fts_matches fm
2191
2192
  JOIN documents d ON d.id = fm.rowid AND d.active = 1
@@ -2212,6 +2213,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
2212
2213
  source_size: number | null;
2213
2214
  source_hash: string | null;
2214
2215
  content_type: string | null;
2216
+ content_type_source: string | null;
2215
2217
  categories: string | null;
2216
2218
  }
2217
2219
 
@@ -2250,6 +2252,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
2250
2252
  sourceSize: r.source_size ?? undefined,
2251
2253
  sourceHash: r.source_hash ?? undefined,
2252
2254
  contentType: r.content_type ?? undefined,
2255
+ contentTypeSource: r.content_type_source ?? undefined,
2253
2256
  categories: parseCategoriesJson(r.categories) ?? undefined,
2254
2257
  }))
2255
2258
  );
@@ -119,7 +119,7 @@ export interface DocumentRow {
119
119
  active: boolean;
120
120
  /** Ingest schema version for backfill detection */
121
121
  ingestVersion: number | null;
122
- /** Fingerprint of normalized content type rules used for derived metadata */
122
+ /** Fingerprint of metadata-affecting content type rules used for derivation. */
123
123
  contentTypeRulesFingerprint?: string | null;
124
124
 
125
125
  // Error tracking
@@ -304,7 +304,7 @@ export interface DocumentInput {
304
304
  lastErrorMessage?: string;
305
305
  /** Ingest schema version for backfill detection */
306
306
  ingestVersion?: number;
307
- /** Fingerprint of normalized content type rules used for derived metadata */
307
+ /** Fingerprint of metadata-affecting content type rules used for derivation. */
308
308
  contentTypeRulesFingerprint?: string;
309
309
  /**
310
310
  * Change-journal metadata for a source lifecycle write. Conversion failures
@@ -569,6 +569,7 @@ export interface FtsResult {
569
569
  sourceSize?: number;
570
570
  sourceHash?: string;
571
571
  contentType?: string;
572
+ contentTypeSource?: string;
572
573
  categories?: string[];
573
574
  }
574
575
 
@@ -1 +0,0 @@
1
- a0cf108985e47f6e36026b20538fd95b9fac9d908cf1a13eb77aedcb38a49643 gno-browser-clipper-v1.26.0.zip