@gmickel/gno 1.36.0 → 1.37.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.
- package/README.md +55 -26
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.36.0.zip → gno-browser-clipper-v1.37.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.37.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +4 -2
- package/spec/cli.md +12 -1
- package/src/cli/program.ts +23 -0
- package/src/core/context-evidence.ts +8 -2
- package/src/pipeline/hybrid.ts +15 -1
- package/src/pipeline/search.ts +15 -6
- package/src/pipeline/snippet.ts +203 -0
- package/src/pipeline/vsearch.ts +15 -6
- package/src/serve/public/app.tsx +44 -24
- package/src/serve/public/components/ai-elements/code-block.tsx +11 -6
- package/src/serve/public/index.html +0 -1
- package/src/serve/public/lib/code-language.ts +4 -2
- package/src/serve/public/lib/shiki-language-ids.ts +335 -0
- package/src/serve/public/pages/Dashboard.tsx +81 -44
- package/src/serve/public/pages/DocView.tsx +4 -4
- package/src/serve/public/pages/Search.tsx +1 -2
- package/src/serve/public/pages/doc-frontmatter-display.tsx +10 -0
- package/src/serve/public/pages/search-page-widgets.tsx +8 -0
- package/src/serve/server.ts +6 -6
- package/src/serve/spa-bundle-source.ts +98 -22
- package/src/serve/spa-production-build.ts +130 -0
- package/src/serve/spa-production.ts +43 -0
- package/src/types/file-assets.d.ts +4 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.36.0.zip.sha256 +0 -1
|
@@ -12,8 +12,6 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
12
12
|
import { resolveDepthPolicy } from "../../../core/depth-policy";
|
|
13
13
|
import { normalizeStructuredQueryInput } from "../../../core/structured-query";
|
|
14
14
|
import { Loader } from "../components/ai-elements/loader";
|
|
15
|
-
import { AIModelSelector } from "../components/AIModelSelector";
|
|
16
|
-
import { TagFacets } from "../components/TagFacets";
|
|
17
15
|
import {
|
|
18
16
|
ThoroughnessSelector,
|
|
19
17
|
type Thoroughness,
|
|
@@ -47,6 +45,7 @@ import {
|
|
|
47
45
|
type TagMode,
|
|
48
46
|
} from "../lib/retrieval-filters";
|
|
49
47
|
import { cn } from "../lib/utils";
|
|
48
|
+
import { AIModelSelector, TagFacets } from "./search-page-widgets";
|
|
50
49
|
|
|
51
50
|
/**
|
|
52
51
|
* Render snippet with <mark> tags as highlighted spans.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocView-only re-export of FrontmatterDisplay.
|
|
3
|
+
*
|
|
4
|
+
* Kept as a separate module so DOM tests can mock.module this path without
|
|
5
|
+
* sticky-mock pollution of the real FrontmatterDisplay suite.
|
|
6
|
+
*/
|
|
7
|
+
export {
|
|
8
|
+
FrontmatterDisplay,
|
|
9
|
+
parseFrontmatter,
|
|
10
|
+
} from "../components/FrontmatterDisplay";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search-only re-exports of sidebar widgets.
|
|
3
|
+
*
|
|
4
|
+
* Kept as a separate module so DOM tests can mock.module this path without
|
|
5
|
+
* sticky-mock pollution of the real TagFacets / AIModelSelector suites.
|
|
6
|
+
*/
|
|
7
|
+
export { AIModelSelector } from "../components/AIModelSelector";
|
|
8
|
+
export { TagFacets } from "../components/TagFacets";
|
package/src/serve/server.ts
CHANGED
|
@@ -304,16 +304,16 @@ export async function startServer(
|
|
|
304
304
|
|
|
305
305
|
// Start server with try/catch for port-in-use etc.
|
|
306
306
|
let server: ReturnType<typeof Bun.serve>;
|
|
307
|
-
// Bun HTMLBundle route values cannot carry custom headers.
|
|
308
|
-
//
|
|
309
|
-
// proxy bytes through this public listener's security envelope.
|
|
310
|
-
// server tests keep an in-listener bundle
|
|
311
|
-
// server cannot host the private source.
|
|
307
|
+
// Bun HTMLBundle / built-chunk route values cannot carry custom headers.
|
|
308
|
+
// Host them on a private Unix socket (ephemeral loopback on Windows), then
|
|
309
|
+
// proxy bytes through this public listener's security envelope. Production
|
|
310
|
+
// emits split JS chunks; injected-server tests keep an in-listener bundle
|
|
311
|
+
// route because their fake Bun server cannot host the private source.
|
|
312
312
|
let spaBundleSource: SpaBundleSource | null = null;
|
|
313
313
|
const usesInjectedServer = dependencies.serve !== undefined;
|
|
314
314
|
try {
|
|
315
315
|
if (!usesInjectedServer) {
|
|
316
|
-
spaBundleSource = createSpaBundleSource(homepage, isDev);
|
|
316
|
+
spaBundleSource = await createSpaBundleSource(homepage, isDev);
|
|
317
317
|
}
|
|
318
318
|
} catch (error) {
|
|
319
319
|
removeShutdownHandlers();
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import type { HTMLBundle, Server } from "bun";
|
|
2
2
|
|
|
3
|
-
// node:fs/promises — no Bun equivalent for
|
|
3
|
+
// node:fs/promises — no Bun equivalent for unlink of private sockets.
|
|
4
4
|
import { unlink } from "node:fs/promises";
|
|
5
5
|
// node:os — no Bun equivalent for the platform temporary directory.
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
|
-
// node:path — no Bun
|
|
7
|
+
// node:path — no Bun path utils.
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
|
|
10
|
+
import {
|
|
11
|
+
getProductionSpaAssets,
|
|
12
|
+
type ProductionSpaAssets,
|
|
13
|
+
} from "./spa-production";
|
|
14
|
+
|
|
10
15
|
type BunServer = Server<unknown>;
|
|
11
16
|
|
|
12
17
|
export type SpaBundleSource = {
|
|
@@ -17,33 +22,109 @@ export type SpaBundleSource = {
|
|
|
17
22
|
|
|
18
23
|
const notFound = (): Response => new Response("Not Found", { status: 404 });
|
|
19
24
|
|
|
25
|
+
const isEnoent = (error: unknown): boolean =>
|
|
26
|
+
Boolean(
|
|
27
|
+
error &&
|
|
28
|
+
typeof error === "object" &&
|
|
29
|
+
"code" in error &&
|
|
30
|
+
error.code === "ENOENT"
|
|
31
|
+
);
|
|
32
|
+
|
|
20
33
|
/**
|
|
21
|
-
* Host Bun's headerless
|
|
34
|
+
* Host Bun's headerless HTML / asset surface outside the public listener.
|
|
22
35
|
*
|
|
23
36
|
* Unix hosts use a private Unix-domain socket, so the raw bundle and generated
|
|
24
37
|
* assets have no TCP origin at all. Windows falls back to an ephemeral
|
|
25
38
|
* loopback listener plus an unguessable entry path. The public server proxies
|
|
26
39
|
* the bytes and applies its normal security envelope before browser delivery.
|
|
40
|
+
*
|
|
41
|
+
* Production (`isDev === false`) serves the split SPA snapshot embedded from
|
|
42
|
+
* `assets/spa-production.json.gz`. Source and compiled executables share that
|
|
43
|
+
* path so first listen does not wait on `Bun.build`. Compiled binaries also
|
|
44
|
+
* cannot call `Bun.build` on `/$bunfs` (ENOENT on the virtual root).
|
|
45
|
+
* Development keeps the live HTMLBundle so HMR still works. Refresh the
|
|
46
|
+
* snapshot with `bun scripts/build-spa-production.ts`.
|
|
27
47
|
*/
|
|
28
|
-
export function createSpaBundleSource(
|
|
48
|
+
export async function createSpaBundleSource(
|
|
29
49
|
bundle: HTMLBundle,
|
|
30
50
|
isDev: boolean
|
|
31
|
-
): SpaBundleSource {
|
|
51
|
+
): Promise<SpaBundleSource> {
|
|
32
52
|
const nonce = crypto.randomUUID().replaceAll("-", "");
|
|
33
53
|
const entryPath = `/__gno_spa_${nonce}`;
|
|
54
|
+
|
|
55
|
+
if (isDev) {
|
|
56
|
+
return hostPrivateSource({
|
|
57
|
+
entryPath,
|
|
58
|
+
isDev: true,
|
|
59
|
+
nonce,
|
|
60
|
+
routes: { [entryPath]: bundle },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return createSplitProductionSource(entryPath, nonce);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function createSplitProductionSource(
|
|
68
|
+
entryPath: string,
|
|
69
|
+
nonce: string
|
|
70
|
+
): Promise<SpaBundleSource> {
|
|
71
|
+
const assets = await getProductionSpaAssets();
|
|
72
|
+
return hostProductionAssets(assets, entryPath, nonce);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function hostProductionAssets(
|
|
76
|
+
assets: ProductionSpaAssets,
|
|
77
|
+
entryPath: string,
|
|
78
|
+
nonce: string
|
|
79
|
+
): Promise<SpaBundleSource> {
|
|
80
|
+
const resolve = (pathname: string): Response => {
|
|
81
|
+
if (pathname === entryPath) {
|
|
82
|
+
return new Response(assets.html, {
|
|
83
|
+
headers: { "Content-Type": "text/html;charset=utf-8" },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const file = assets.files[pathname];
|
|
87
|
+
if (!file) {
|
|
88
|
+
return notFound();
|
|
89
|
+
}
|
|
90
|
+
return new Response(file.text, {
|
|
91
|
+
headers: { "Content-Type": file.type },
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return hostPrivateSource({
|
|
96
|
+
entryPath,
|
|
97
|
+
fetchAsset: resolve,
|
|
98
|
+
isDev: false,
|
|
99
|
+
nonce,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function hostPrivateSource(options: {
|
|
104
|
+
entryPath: string;
|
|
105
|
+
fetchAsset?: (pathname: string) => Response;
|
|
106
|
+
isDev: boolean;
|
|
107
|
+
nonce: string;
|
|
108
|
+
routes?: Record<string, HTMLBundle>;
|
|
109
|
+
}): Promise<SpaBundleSource> {
|
|
34
110
|
let server: BunServer;
|
|
35
111
|
let fetchPrivate: (request: Request) => Promise<Response>;
|
|
36
112
|
let socketPath: string | null = null;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
113
|
+
const fallback = {
|
|
114
|
+
fetch: (request: Request): Response => {
|
|
115
|
+
if (!options.fetchAsset) {
|
|
116
|
+
return notFound();
|
|
117
|
+
}
|
|
118
|
+
return options.fetchAsset(new URL(request.url).pathname);
|
|
119
|
+
},
|
|
120
|
+
};
|
|
40
121
|
|
|
41
122
|
if (process.platform === "win32") {
|
|
42
123
|
server = Bun.serve({
|
|
124
|
+
development: options.isDev,
|
|
43
125
|
hostname: "127.0.0.1",
|
|
44
126
|
port: 0,
|
|
45
|
-
|
|
46
|
-
routes,
|
|
127
|
+
routes: options.routes,
|
|
47
128
|
...fallback,
|
|
48
129
|
});
|
|
49
130
|
const origin = `http://127.0.0.1:${server.port}`;
|
|
@@ -54,11 +135,11 @@ export function createSpaBundleSource(
|
|
|
54
135
|
});
|
|
55
136
|
};
|
|
56
137
|
} else {
|
|
57
|
-
socketPath = join(tmpdir(), `gno-spa-${nonce.slice(0, 20)}.sock`);
|
|
138
|
+
socketPath = join(tmpdir(), `gno-spa-${options.nonce.slice(0, 20)}.sock`);
|
|
58
139
|
server = Bun.serve({
|
|
140
|
+
development: options.isDev,
|
|
141
|
+
routes: options.routes,
|
|
59
142
|
unix: socketPath,
|
|
60
|
-
development: isDev,
|
|
61
|
-
routes,
|
|
62
143
|
...fallback,
|
|
63
144
|
});
|
|
64
145
|
fetchPrivate = async (request): Promise<Response> => {
|
|
@@ -72,9 +153,7 @@ export function createSpaBundleSource(
|
|
|
72
153
|
|
|
73
154
|
let closed = false;
|
|
74
155
|
return {
|
|
75
|
-
|
|
76
|
-
fetch: fetchPrivate,
|
|
77
|
-
async close(): Promise<void> {
|
|
156
|
+
close: async (): Promise<void> => {
|
|
78
157
|
if (closed) {
|
|
79
158
|
return;
|
|
80
159
|
}
|
|
@@ -84,16 +163,13 @@ export function createSpaBundleSource(
|
|
|
84
163
|
try {
|
|
85
164
|
await unlink(socketPath);
|
|
86
165
|
} catch (error) {
|
|
87
|
-
if (
|
|
88
|
-
!error ||
|
|
89
|
-
typeof error !== "object" ||
|
|
90
|
-
!("code" in error) ||
|
|
91
|
-
error.code !== "ENOENT"
|
|
92
|
-
) {
|
|
166
|
+
if (!isEnoent(error)) {
|
|
93
167
|
throw error;
|
|
94
168
|
}
|
|
95
169
|
}
|
|
96
170
|
}
|
|
97
171
|
},
|
|
172
|
+
entryPath: options.entryPath,
|
|
173
|
+
fetch: fetchPrivate,
|
|
98
174
|
};
|
|
99
175
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// node:fs/promises — no Bun equivalent for mkdir/rm of the temporary SPA outdir.
|
|
2
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
3
|
+
// node:os — no Bun equivalent for the platform temporary directory.
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
// node:path — no Bun path utils.
|
|
6
|
+
import { basename, join } from "node:path";
|
|
7
|
+
|
|
8
|
+
export const ROOT_MOUNT_MARKER = 'getElementById("root")';
|
|
9
|
+
|
|
10
|
+
export type ProductionSpaFile = {
|
|
11
|
+
text: string;
|
|
12
|
+
type: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type ProductionSpaAssets = {
|
|
16
|
+
files: Record<string, ProductionSpaFile>;
|
|
17
|
+
html: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const SCRIPT_TAG_RE = /<script\b[^>]*\bsrc="[^"]+"[^>]*><\/script>/iu;
|
|
21
|
+
const BASE_TAG_RE = /<base\b[^>]*>/iu;
|
|
22
|
+
const CROSSORIGIN_ATTR_RE = /\s+crossorigin(?:=(?:"[^"]*"|'[^']*'))?/gu;
|
|
23
|
+
|
|
24
|
+
const contentTypeFor = (path: string): string => {
|
|
25
|
+
if (path.endsWith(".css")) {
|
|
26
|
+
return "text/css;charset=utf-8";
|
|
27
|
+
}
|
|
28
|
+
if (path.endsWith(".js")) {
|
|
29
|
+
return "text/javascript;charset=utf-8";
|
|
30
|
+
}
|
|
31
|
+
if (path.endsWith(".map")) {
|
|
32
|
+
return "application/json";
|
|
33
|
+
}
|
|
34
|
+
return "application/octet-stream";
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const isStandaloneExecutable = (): boolean =>
|
|
38
|
+
import.meta.path.includes("/$bunfs/") ||
|
|
39
|
+
import.meta.path.includes("\\$bunfs\\");
|
|
40
|
+
|
|
41
|
+
export const isBunfsPath = (path: string): boolean =>
|
|
42
|
+
path.includes("/$bunfs/") || path.includes("\\$bunfs\\");
|
|
43
|
+
|
|
44
|
+
export const productionSpaEntryPath = (): string =>
|
|
45
|
+
join(import.meta.dir, "public", "index.html");
|
|
46
|
+
|
|
47
|
+
const rewriteProductionHtml = (html: string, jsEntryPath: string): string => {
|
|
48
|
+
const script = `<script type="module" src="/${basename(jsEntryPath)}"></script>`;
|
|
49
|
+
let next = html.replace(BASE_TAG_RE, "");
|
|
50
|
+
if (SCRIPT_TAG_RE.test(next)) {
|
|
51
|
+
next = next.replace(SCRIPT_TAG_RE, script);
|
|
52
|
+
} else {
|
|
53
|
+
throw new Error("Production SPA HTML is missing a module script tag");
|
|
54
|
+
}
|
|
55
|
+
return next.replace(CROSSORIGIN_ATTR_RE, "");
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const buildProductionSpaAssets = async (
|
|
59
|
+
entryPath: string = productionSpaEntryPath()
|
|
60
|
+
): Promise<ProductionSpaAssets> => {
|
|
61
|
+
if (isBunfsPath(entryPath)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Cannot Bun.build a production SPA from bunfs (${entryPath})`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const outdir = join(
|
|
68
|
+
tmpdir(),
|
|
69
|
+
`gno-spa-build-${crypto.randomUUID().replaceAll("-", "").slice(0, 20)}`
|
|
70
|
+
);
|
|
71
|
+
await mkdir(outdir, { recursive: true });
|
|
72
|
+
|
|
73
|
+
let result: Awaited<ReturnType<typeof Bun.build>>;
|
|
74
|
+
try {
|
|
75
|
+
result = await Bun.build({
|
|
76
|
+
entrypoints: [entryPath],
|
|
77
|
+
minify: true,
|
|
78
|
+
outdir,
|
|
79
|
+
publicPath: "/",
|
|
80
|
+
splitting: true,
|
|
81
|
+
target: "browser",
|
|
82
|
+
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
await rm(outdir, { recursive: true, force: true });
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!result.success) {
|
|
89
|
+
await rm(outdir, { recursive: true, force: true });
|
|
90
|
+
throw new Error("Production SPA split build failed");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const htmlArtifact = result.outputs.find((output) =>
|
|
95
|
+
output.path.endsWith(".html")
|
|
96
|
+
);
|
|
97
|
+
const jsEntry = result.outputs.find(
|
|
98
|
+
(output) => output.kind === "entry-point" && output.path.endsWith(".js")
|
|
99
|
+
);
|
|
100
|
+
if (!htmlArtifact || !jsEntry) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
"Production SPA split build did not emit HTML and JS entry"
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const jsEntryText = await jsEntry.text();
|
|
107
|
+
if (!jsEntryText.includes(ROOT_MOUNT_MARKER)) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
"Production SPA JS entry does not mount #root; HTML would leave the shell blank"
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const html = rewriteProductionHtml(await htmlArtifact.text(), jsEntry.path);
|
|
114
|
+
const files: Record<string, ProductionSpaFile> = {};
|
|
115
|
+
for (const output of result.outputs) {
|
|
116
|
+
if (output.path.endsWith(".html")) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const publicPath = `/${basename(output.path)}`;
|
|
120
|
+
files[publicPath] = {
|
|
121
|
+
text: await output.text(),
|
|
122
|
+
type: contentTypeFor(output.path),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { files, html };
|
|
127
|
+
} finally {
|
|
128
|
+
await rm(outdir, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import productionSpaGzip from "../../assets/spa-production.json.gz" with { type: "file" };
|
|
2
|
+
import {
|
|
3
|
+
ROOT_MOUNT_MARKER,
|
|
4
|
+
type ProductionSpaAssets,
|
|
5
|
+
} from "./spa-production-build";
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
buildProductionSpaAssets,
|
|
9
|
+
isBunfsPath,
|
|
10
|
+
isStandaloneExecutable,
|
|
11
|
+
productionSpaEntryPath,
|
|
12
|
+
ROOT_MOUNT_MARKER,
|
|
13
|
+
type ProductionSpaAssets,
|
|
14
|
+
type ProductionSpaFile,
|
|
15
|
+
} from "./spa-production-build";
|
|
16
|
+
|
|
17
|
+
export const loadEmbeddedProductionSpa =
|
|
18
|
+
async (): Promise<ProductionSpaAssets> => {
|
|
19
|
+
const compressed = await Bun.file(productionSpaGzip).arrayBuffer();
|
|
20
|
+
const json = new TextDecoder().decode(
|
|
21
|
+
Bun.gunzipSync(new Uint8Array(compressed))
|
|
22
|
+
);
|
|
23
|
+
const assets = JSON.parse(json) as ProductionSpaAssets;
|
|
24
|
+
const firstJsPath = assets.html.match(/src="(\/[^"]+\.js)"/u)?.[1];
|
|
25
|
+
const firstJs = firstJsPath ? assets.files[firstJsPath] : undefined;
|
|
26
|
+
if (!firstJs?.text.includes(ROOT_MOUNT_MARKER)) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
"Embedded production SPA is missing the #root mount entry"
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return assets;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const getProductionSpaAssets =
|
|
35
|
+
async (): Promise<ProductionSpaAssets> => {
|
|
36
|
+
// Source and compiled serve both load the committed snapshot. Rebuilding
|
|
37
|
+
// with Bun.build on every `gno serve` blocked first listen on Windows
|
|
38
|
+
// Bun 1.3.11 long enough for the watcher smoke (10s) to miss readiness.
|
|
39
|
+
// Refresh the snapshot with `bun scripts/build-spa-production.ts`.
|
|
40
|
+
// `--dev` keeps the live HTMLBundle. Tests that need a live rebuild call
|
|
41
|
+
// `buildProductionSpaAssets` directly.
|
|
42
|
+
return loadEmbeddedProductionSpa();
|
|
43
|
+
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
8d8b73a55081409e3dbba4e6927d864839329cbcdb98a8f78274c29688edeb9e gno-browser-clipper-v1.36.0.zip
|