@gmickel/gno 0.41.1 → 1.0.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 CHANGED
@@ -8,6 +8,9 @@
8
8
  [![Twitter](./assets/badges/twitter.svg)](https://twitter.com/gmickel)
9
9
  [![Discord](./assets/badges/discord.svg)](https://discord.gg/nHEmyJB5tg)
10
10
 
11
+ > [!TIP]
12
+ > **[gno.sh/publish](https://gno.sh/publish) is live.** Turn any GNO note or collection into a polished, reader-first URL — editorial typography, scoped search, and four visibility modes from public to encrypted-before-upload. **[See the reader →](#publish-to-gnosh)**
13
+
11
14
  > **ClawdHub**: GNO skills bundled for Clawdbot — [clawdhub.com/gmickel/gno](https://clawdhub.com/gmickel/gno)
12
15
 
13
16
  ![GNO](./assets/og-image.png)
@@ -27,6 +30,7 @@ Use it when:
27
30
  - **Real retrieval surfaces**: CLI, Web UI, REST API, MCP, SDK
28
31
  - **Local-first answers**: grounded synthesis with citations when you want answers, raw retrieval when you do not
29
32
  - **Connected knowledge**: backlinks, related notes, graph view, cross-collection navigation
33
+ - **Shareable, not synced**: export a note or collection to [gno.sh](https://gno.sh/publish) as a polished reader page — public, secret, invite-only, or locally encrypted before upload
30
34
  - **Operational fit**: daemon mode, model presets, remote GPU backends, safe config/state on disk
31
35
 
32
36
  ### One-Minute Tour
@@ -74,6 +78,7 @@ gno daemon
74
78
  - [Search Modes](#search-modes)
75
79
  - [Agent Integration](#agent-integration)
76
80
  - [Web UI](#web-ui)
81
+ - [Publish to gno.sh](#publish-to-gnosh)
77
82
  - [REST API](#rest-api)
78
83
  - [SDK](#sdk)
79
84
  - [How It Works](#how-it-works)
@@ -87,9 +92,10 @@ gno daemon
87
92
 
88
93
  ## What's New
89
94
 
90
- > Latest release: [v0.40.2](./CHANGELOG.md#0402---2026-04-06)
95
+ > Latest release: [v1.0.0](./CHANGELOG.md#100---2026-04-16)
91
96
  > Full release history: [CHANGELOG.md](./CHANGELOG.md)
92
97
 
98
+ - **Publish to [gno.sh](https://gno.sh/publish)**: new `gno publish export` CLI and Web UI action produce a self-contained artifact you upload to the hosted reader — public, secret, invite-only, or locally encrypted before upload
93
99
  - **Retrieval Quality Upgrade**: stronger BM25 lexical handling, code-aware chunking, terminal result hyperlinks, and per-collection model overrides
94
100
  - **Code Embedding Benchmarks**: new benchmark workflow across canonical, real-GNO, and pinned OSS slices for comparing alternate embedding models
95
101
  - **Default Embed Model**: built-in presets now use `Qwen3-Embedding-0.6B-GGUF` after it beat `bge-m3` on both code and multilingual prose benchmark lanes
@@ -410,8 +416,22 @@ gno query "refresh token rotation" --explain
410
416
  # Work with filters
411
417
  gno query "meeting notes" --since "last month" --category "meeting,notes"
412
418
  gno search "incident review" --tags-all "status/active,team/platform"
419
+
420
+ # Export a publish artifact for gno.sh
421
+ gno publish export work-docs --out ~/Downloads/work-docs.json
422
+ gno publish export "gno://work-docs/runbooks/deploy.md" --out ~/Downloads/deploy.json
423
+ # Or let GNO choose ~/Downloads/<slug>-<YYYYMMDD>.json automatically
424
+ gno publish export work-docs
413
425
  ```
414
426
 
427
+ The local web UI exposes the same export flow:
428
+
429
+ - Collections page → collection menu → `Export for gno.sh`
430
+ - Document view → `Export for gno.sh`
431
+
432
+ Both actions download the same JSON artifact the CLI writes, ready for upload at
433
+ `https://gno.sh/studio`.
434
+
415
435
  ### Retrieval V2 Controls
416
436
 
417
437
  Existing query calls still work. Retrieval v2 adds optional structured intent control and deeper explain output.
@@ -590,6 +610,53 @@ Everything runs locally. No cloud, no accounts, no data leaving your machine.
590
610
 
591
611
  ---
592
612
 
613
+ ## Publish to gno.sh
614
+
615
+ GNO is local-first, but sometimes you want a URL to send someone. [**gno.sh**](https://gno.sh/publish) is the hosted reader on top of GNO — a polished, reading-first page for a single note or a whole collection, without mounting your vault or syncing anything.
616
+
617
+ ![gno.sh publish reader](./assets/screenshots/publish-reader.jpg)
618
+
619
+ The workflow is deliberately explicit: **export locally → upload artifact → share URL**. Your private notes and metadata stay on your machine. Only what you export leaves.
620
+
621
+ ```bash
622
+ # Export a single note
623
+ gno publish export "gno://work-docs/runbooks/deploy.md" --out ~/Downloads/deploy.json
624
+
625
+ # Export a whole collection
626
+ gno publish export work-docs --out ~/Downloads/work-docs.json
627
+
628
+ # Export an encrypted note (ciphertext is created locally before upload)
629
+ gno publish export "gno://work-docs/runbooks/deploy.md" \
630
+ --visibility encrypted \
631
+ --passphrase "correct horse battery staple" \
632
+ --out ~/Downloads/deploy-encrypted.json
633
+
634
+ # Let GNO pick the path (~/Downloads/<slug>-<YYYYMMDD>.json)
635
+ gno publish export work-docs
636
+ ```
637
+
638
+ Or use the Web UI:
639
+
640
+ - **Collections page** → collection menu → **Export for gno.sh**
641
+ - **Document view** → **Export for gno.sh**
642
+
643
+ Upload the artifact at [gno.sh/studio](https://gno.sh/studio) and pick a visibility mode:
644
+
645
+ | Mode | Use When |
646
+ | :-------------- | :------------------------------------------------------------- |
647
+ | **Public** | Open URL, indexable — talks, blog posts, portfolios |
648
+ | **Secret link** | Unguessable token, rotate / revoke / expire |
649
+ | **Invite-only** | Private space for specific people |
650
+ | **Encrypted** | GNO encrypts locally before upload; readers decrypt in-browser |
651
+
652
+ **Reader experience**: editorial serif typography, drop caps, hanging punctuation, table of contents, keyboard shortcuts (`j/k`, `/`), scoped Pagefind-style search, and backlinks restricted to the published subset. Nothing leaks that you didn't publish.
653
+
654
+ Republishing a public, secret-link, or invite-only artifact updates the same URL. Encrypted shares should be replaced from a fresh local export so the server never needs your plaintext.
655
+
656
+ > **Full story**: [gno.sh/publish](https://gno.sh/publish) · **Try it**: [gno.sh/studio](https://gno.sh/studio)
657
+
658
+ ---
659
+
593
660
  ## REST API
594
661
 
595
662
  Programmatic access to all GNO features via HTTP.
@@ -22,6 +22,7 @@ Fast local semantic search. Index once, search instantly. No cloud, no API keys.
22
22
  - User wants to **tag, categorize, or filter** documents
23
23
  - User asks about **backlinks, wiki links, or related notes**
24
24
  - User wants to **visualize document connections** or see a **knowledge graph**
25
+ - User wants to **export a note or collection for gno.sh publishing**
25
26
 
26
27
  ## Quick Start
27
28
 
@@ -44,6 +45,7 @@ gno search "your query" # BM25 keyword search
44
45
  | **Context** | `context add/list/rm/check` | Add hints to improve search relevance |
45
46
  | **Models** | `models list/use/pull/clear/path` | Manage local AI models |
46
47
  | **Serve** | `serve` | Web UI for browsing and searching |
48
+ | **Publish** | `publish export` | Export gno.sh publish artifacts |
47
49
  | **MCP** | `mcp`, `mcp install/uninstall/status` | AI assistant integration |
48
50
  | **Skill** | `skill install/uninstall/show/paths` | Install skill for AI agents |
49
51
  | **Admin** | `status`, `doctor`, `cleanup`, `reset`, `vec`, `completion` | Maintenance and diagnostics |
@@ -494,6 +494,34 @@ gno serve [options]
494
494
 
495
495
  Features: Dashboard, search, browse collections, document viewer, AI Q&A with citations.
496
496
 
497
+ ## Publish
498
+
499
+ ### gno publish export
500
+
501
+ Export a note or collection as a gno.sh publish artifact JSON.
502
+
503
+ ```bash
504
+ gno publish export <target> [--out <path.json>] [options]
505
+ ```
506
+
507
+ | Option | Default | Description |
508
+ | -------------- | ------- | ------------------------------------------------------------- |
509
+ | `--out` | auto | Output path, defaults to `~/Downloads/<slug>-<YYYYMMDD>.json` |
510
+ | `--visibility` | public | One of `public`, `secret-link`, `invite-only`, `encrypted` |
511
+ | `--slug` | auto | Override the published route slug |
512
+ | `--title` | auto | Override the exported title |
513
+ | `--summary` | auto | Override the exported summary |
514
+ | `--json` | false | Structured result output |
515
+
516
+ Examples:
517
+
518
+ ```bash
519
+ gno publish export work-docs --out ~/Downloads/work-docs.json
520
+ gno publish export "gno://work-docs/runbooks/deploy.md" --out ~/Downloads/deploy.json
521
+ ```
522
+
523
+ On success, upload the JSON file at `https://gno.sh/studio`.
524
+
497
525
  ## Skill Management
498
526
 
499
527
  ### gno skill install
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "0.41.1",
3
+ "version": "1.0.0",
4
4
  "description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
5
5
  "keywords": [
6
6
  "embeddings",
@@ -88,6 +88,12 @@ export {
88
88
  type StatusResult,
89
89
  status,
90
90
  } from "./status";
91
+ export {
92
+ formatPublishExport,
93
+ publishExport,
94
+ type PublishExportOptions,
95
+ type PublishExportResult,
96
+ } from "./publish";
91
97
  export {
92
98
  formatUpdate,
93
99
  type UpdateOptions,
@@ -0,0 +1,142 @@
1
+ /**
2
+ * gno publish export command.
3
+ *
4
+ * @module src/cli/commands/publish
5
+ */
6
+
7
+ import { mkdir, writeFile } from "node:fs/promises";
8
+ import { homedir } from "node:os";
9
+ import { dirname } from "node:path";
10
+ import { join } from "node:path";
11
+
12
+ import type {
13
+ PublishArtifact,
14
+ PublishVisibility,
15
+ } from "../../publish/artifact";
16
+
17
+ import { derivePublishArtifactFilename, slugify } from "../../publish/artifact";
18
+ import { exportPublishArtifact } from "../../publish/export-service";
19
+ import { initStore } from "./shared";
20
+
21
+ export interface PublishExportOptions {
22
+ configPath?: string;
23
+ encryptionPassphrase?: string;
24
+ json?: boolean;
25
+ out?: string;
26
+ slug?: string;
27
+ summary?: string;
28
+ title?: string;
29
+ visibility?: PublishVisibility;
30
+ }
31
+
32
+ export type PublishExportResult =
33
+ | {
34
+ success: true;
35
+ data: {
36
+ artifact: PublishArtifact;
37
+ outPath: string;
38
+ uploadUrl: string;
39
+ };
40
+ }
41
+ | { success: false; error: string; isValidation?: boolean };
42
+
43
+ function formatExportDateStamp(isoTimestamp: string): string {
44
+ return isoTimestamp.slice(0, 10).replaceAll("-", "");
45
+ }
46
+
47
+ export function buildDefaultPublishExportPath(
48
+ artifact: PublishArtifact
49
+ ): string {
50
+ const fileName = derivePublishArtifactFilename(artifact).replace(
51
+ /\.json$/u,
52
+ ""
53
+ );
54
+ return join(
55
+ homedir(),
56
+ "Downloads",
57
+ `${fileName}-${formatExportDateStamp(artifact.exportedAt)}.json`
58
+ );
59
+ }
60
+
61
+ export async function publishExport(
62
+ target: string,
63
+ options: PublishExportOptions
64
+ ): Promise<PublishExportResult> {
65
+ const initResult = await initStore({
66
+ configPath: options.configPath,
67
+ syncConfig: false,
68
+ });
69
+ if (!initResult.ok) {
70
+ return { success: false, error: initResult.error };
71
+ }
72
+
73
+ const { collections, store } = initResult;
74
+
75
+ try {
76
+ const artifact = await exportPublishArtifact({
77
+ collections,
78
+ options: {
79
+ routeSlug: options.slug,
80
+ encryptionPassphrase: options.encryptionPassphrase,
81
+ summary: options.summary,
82
+ title: options.title,
83
+ visibility: options.visibility,
84
+ },
85
+ store,
86
+ target,
87
+ });
88
+ const outPath =
89
+ options.out?.trim() || buildDefaultPublishExportPath(artifact);
90
+
91
+ await mkdir(dirname(outPath), { recursive: true });
92
+ await writeFile(outPath, JSON.stringify(artifact, null, 2));
93
+
94
+ return {
95
+ success: true,
96
+ data: {
97
+ artifact,
98
+ outPath,
99
+ uploadUrl: "https://gno.sh/studio",
100
+ },
101
+ };
102
+ } catch (error) {
103
+ return {
104
+ success: false,
105
+ error: error instanceof Error ? error.message : String(error),
106
+ };
107
+ } finally {
108
+ await store.close();
109
+ }
110
+ }
111
+
112
+ export function formatPublishExport(
113
+ result: PublishExportResult,
114
+ options: Pick<PublishExportOptions, "json">
115
+ ): string {
116
+ if (!result.success) {
117
+ if (options.json) {
118
+ return JSON.stringify({
119
+ error: {
120
+ code: result.isValidation ? "VALIDATION" : "RUNTIME",
121
+ message: result.error,
122
+ },
123
+ });
124
+ }
125
+ return `Error: ${result.error}`;
126
+ }
127
+
128
+ if (options.json) {
129
+ return JSON.stringify(result.data, null, 2);
130
+ }
131
+
132
+ const { artifact, outPath, uploadUrl } = result.data;
133
+ const space = artifact.spaces[0];
134
+
135
+ return [
136
+ `Exported ${space?.sourceType ?? "artifact"} to ${outPath}`,
137
+ `Route slug: ${space?.routeSlug ?? slugify(artifact.source)}`,
138
+ `Visibility: ${space?.visibility ?? "public"}`,
139
+ `Filename: ${derivePublishArtifactFilename(artifact)}`,
140
+ `Next: open ${uploadUrl} and drop ${outPath} into the upload zone.`,
141
+ ].join("\n");
142
+ }
@@ -27,6 +27,7 @@ export const CMD = {
27
27
  multiGet: "multi-get",
28
28
  ls: "ls",
29
29
  status: "status",
30
+ publishExport: "publish.export",
30
31
  collectionList: "collection.list",
31
32
  contextList: "context.list",
32
33
  contextCheck: "context.check",
@@ -49,6 +50,7 @@ const FORMAT_SUPPORT: Record<CommandId, OutputFormat[]> = {
49
50
  [CMD.multiGet]: ["terminal", "json", "files", "md"],
50
51
  [CMD.ls]: ["terminal", "json", "files", "md"],
51
52
  [CMD.status]: ["terminal", "json"],
53
+ [CMD.publishExport]: ["terminal", "json"],
52
54
  [CMD.collectionList]: ["terminal", "json", "md"],
53
55
  [CMD.contextList]: ["terminal", "json", "md"],
54
56
  [CMD.contextCheck]: ["terminal", "json", "md"],
@@ -216,6 +216,7 @@ export function createProgram(): Command {
216
216
  wireSearchCommands(program);
217
217
  wireOnboardingCommands(program);
218
218
  wireManagementCommands(program);
219
+ wirePublishCommand(program);
219
220
  wireVecCommands(program);
220
221
  wireRetrievalCommands(program);
221
222
  wireTagsCommands(program);
@@ -1623,6 +1624,90 @@ function wireVecCommands(program: Command): void {
1623
1624
  });
1624
1625
  }
1625
1626
 
1627
+ function wirePublishCommand(program: Command): void {
1628
+ const visibilityValues = [
1629
+ "public",
1630
+ "secret-link",
1631
+ "invite-only",
1632
+ "encrypted",
1633
+ ] as const;
1634
+ const publishCmd = program
1635
+ .command("publish")
1636
+ .description("Export publish artifacts for gno.sh");
1637
+
1638
+ publishCmd
1639
+ .command("export <target>")
1640
+ .description("Export a collection or document ref as a gno.sh artifact")
1641
+ .option("--out <path>", "output artifact path")
1642
+ .option(
1643
+ "--visibility <mode>",
1644
+ "visibility mode (public, secret-link, invite-only, encrypted)",
1645
+ "public"
1646
+ )
1647
+ .option(
1648
+ "--passphrase <value>",
1649
+ "required for encrypted export; encrypts locally before upload"
1650
+ )
1651
+ .option("--slug <slug>", "route slug override")
1652
+ .option("--title <title>", "space title override")
1653
+ .option("--summary <summary>", "space summary override")
1654
+ .option("--json", "JSON output")
1655
+ .action(async (target: string, cmdOpts: Record<string, unknown>) => {
1656
+ const format = getFormat(cmdOpts);
1657
+ assertFormatSupported(CMD.publishExport, format);
1658
+ const globals = getGlobals();
1659
+
1660
+ const visibility = cmdOpts.visibility as string;
1661
+ const passphrase =
1662
+ typeof cmdOpts.passphrase === "string" ? cmdOpts.passphrase : undefined;
1663
+ if (
1664
+ !visibilityValues.includes(
1665
+ visibility as (typeof visibilityValues)[number]
1666
+ )
1667
+ ) {
1668
+ throw new CliError(
1669
+ "VALIDATION",
1670
+ `Invalid visibility: ${visibility}. Must be public, secret-link, invite-only, or encrypted.`
1671
+ );
1672
+ }
1673
+ if (visibility === "encrypted" && !passphrase?.trim()) {
1674
+ throw new CliError(
1675
+ "VALIDATION",
1676
+ "Encrypted publish export requires --passphrase."
1677
+ );
1678
+ }
1679
+
1680
+ const { formatPublishExport, publishExport } =
1681
+ await import("./commands/publish");
1682
+ const result = await publishExport(target, {
1683
+ configPath: globals.config,
1684
+ json: format === "json",
1685
+ out: typeof cmdOpts.out === "string" ? cmdOpts.out : undefined,
1686
+ encryptionPassphrase: passphrase,
1687
+ slug: cmdOpts.slug as string | undefined,
1688
+ summary: cmdOpts.summary as string | undefined,
1689
+ title: cmdOpts.title as string | undefined,
1690
+ visibility: visibility as
1691
+ | "encrypted"
1692
+ | "invite-only"
1693
+ | "public"
1694
+ | "secret-link",
1695
+ });
1696
+
1697
+ if (!result.success) {
1698
+ throw new CliError(
1699
+ result.isValidation ? "VALIDATION" : "RUNTIME",
1700
+ result.error
1701
+ );
1702
+ }
1703
+
1704
+ await writeOutput(
1705
+ formatPublishExport(result, { json: format === "json" }),
1706
+ format
1707
+ );
1708
+ });
1709
+ }
1710
+
1626
1711
  // ─────────────────────────────────────────────────────────────────────────────
1627
1712
  // Skill Commands (install, uninstall, show, paths)
1628
1713
  // ─────────────────────────────────────────────────────────────────────────────