@gmickel/gno 1.0.4 → 1.1.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
@@ -420,7 +420,7 @@ gno search "incident review" --tags-all "status/active,team/platform"
420
420
  # Export a publish artifact for gno.sh
421
421
  gno publish export work-docs --out ~/Downloads/work-docs.json
422
422
  gno publish export "gno://work-docs/runbooks/deploy.md" --out ~/Downloads/deploy.json
423
- # Or let GNO choose ~/Downloads/<slug>-<YYYYMMDD>.json automatically
423
+ # Or let GNO choose your Downloads folder automatically
424
424
  gno publish export work-docs
425
425
  ```
426
426
 
@@ -631,7 +631,7 @@ gno publish export "gno://work-docs/runbooks/deploy.md" \
631
631
  --passphrase "correct horse battery staple" \
632
632
  --out ~/Downloads/deploy-encrypted.json
633
633
 
634
- # Let GNO pick the path (~/Downloads/<slug>-<YYYYMMDD>.json)
634
+ # Let GNO pick the path in your Downloads folder
635
635
  gno publish export work-docs
636
636
  ```
637
637
 
@@ -964,3 +964,7 @@ Interpretation:
964
964
  <p align="center">
965
965
  made with ❤️ by <a href="https://twitter.com/gmickel">@gmickel</a>
966
966
  </p>
967
+
968
+ ## Download history
969
+
970
+ [![ClawHub download history](https://skill-history.com/chart/gmickel/gno.svg)](https://skill-history.com/gmickel/gno)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "1.0.4",
3
+ "version": "1.1.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",
@@ -155,7 +155,7 @@
155
155
  "commander": "^14.0.2",
156
156
  "embla-carousel-react": "^8.6.0",
157
157
  "franc": "^6.2.0",
158
- "lucide-react": "^0.563.0",
158
+ "lucide-react": "^1.8.0",
159
159
  "markitdown-ts": "^0.0.9",
160
160
  "minimatch": "^10.1.1",
161
161
  "nanoid": "^5.1.6",
@@ -168,7 +168,7 @@
168
168
  "react-markdown": "^10.1.0",
169
169
  "rehype-sanitize": "^6.0.0",
170
170
  "remark-gfm": "^4.0.1",
171
- "shiki": "^3.20.0",
171
+ "shiki": "^4.0.2",
172
172
  "sqlite-vec": "^0.1.7-alpha.2",
173
173
  "streamdown": "^2.0.1",
174
174
  "tailwind-merge": "^3.4.0",
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { getIndexDbPath } from "../../../app/constants";
6
- import { loadConfig } from "../../../config";
6
+ import { ensureDirectories, loadConfig } from "../../../config";
7
7
  import { resolveModelUri } from "../../../llm/registry";
8
8
  import { SqliteAdapter } from "../../../store/sqlite/adapter";
9
9
  import { CliError } from "../../errors";
@@ -33,6 +33,11 @@ export async function collectionClearEmbeddings(
33
33
  throw new CliError("VALIDATION", `Collection not found: ${name}`);
34
34
  }
35
35
 
36
+ const ensureResult = await ensureDirectories();
37
+ if (!ensureResult.ok) {
38
+ throw new CliError("RUNTIME", ensureResult.error.message);
39
+ }
40
+
36
41
  const store = new SqliteAdapter();
37
42
  const openResult = await store.open(getIndexDbPath(), config.ftsTokenizer);
38
43
  if (!openResult.ok) {
@@ -5,7 +5,6 @@
5
5
  */
6
6
 
7
7
  import { mkdir, writeFile } from "node:fs/promises";
8
- import { homedir } from "node:os";
9
8
  import { dirname } from "node:path";
10
9
  import { join } from "node:path";
11
10
 
@@ -15,6 +14,7 @@ import type {
15
14
  } from "../../publish/artifact";
16
15
  import type { SanitizeWarning } from "../../publish/obsidian-sanitize";
17
16
 
17
+ import { resolveDownloadsDir } from "../../core/user-dirs";
18
18
  import { derivePublishArtifactFilename, slugify } from "../../publish/artifact";
19
19
  import { exportPublishArtifact } from "../../publish/export-service";
20
20
  import { formatSanitizeWarnings } from "../../publish/obsidian-sanitize";
@@ -50,16 +50,16 @@ function formatExportDateStamp(isoTimestamp: string): string {
50
50
  return isoTimestamp.slice(0, 10).replaceAll("-", "");
51
51
  }
52
52
 
53
- export function buildDefaultPublishExportPath(
53
+ export async function buildDefaultPublishExportPath(
54
54
  artifact: PublishArtifact
55
- ): string {
55
+ ): Promise<string> {
56
56
  const fileName = derivePublishArtifactFilename(artifact).replace(
57
57
  /\.json$/u,
58
58
  ""
59
59
  );
60
+ const downloadsDir = await resolveDownloadsDir();
60
61
  return join(
61
- homedir(),
62
- "Downloads",
62
+ downloadsDir,
63
63
  `${fileName}-${formatExportDateStamp(artifact.exportedAt)}.json`
64
64
  );
65
65
  }
@@ -114,7 +114,7 @@ export async function publishExport(
114
114
  }
115
115
 
116
116
  const outPath =
117
- options.out?.trim() || buildDefaultPublishExportPath(artifact);
117
+ options.out?.trim() || (await buildDefaultPublishExportPath(artifact));
118
118
 
119
119
  await mkdir(dirname(outPath), { recursive: true });
120
120
  await writeFile(outPath, JSON.stringify(artifact, null, 2));
@@ -0,0 +1,86 @@
1
+ // node:os homedir: no Bun equivalent.
2
+ import { homedir } from "node:os";
3
+ // node:path join/normalize: no Bun path utilities.
4
+ import { join, normalize } from "node:path";
5
+
6
+ interface ResolveDownloadsDirDeps {
7
+ env?: Record<string, string | undefined>;
8
+ homeDir?: string;
9
+ platform?: NodeJS.Platform;
10
+ readTextFile?: (path: string) => Promise<string | null>;
11
+ }
12
+
13
+ const XDG_DOWNLOAD_DIR_REGEX = /^XDG_DOWNLOAD_DIR=(?:"([^"]+)"|([^\r\n#]+))$/mu;
14
+
15
+ function expandEnvPath(
16
+ value: string,
17
+ env: Record<string, string | undefined>,
18
+ homeDir: string
19
+ ): string {
20
+ return normalize(
21
+ value
22
+ .trim()
23
+ .replaceAll(/\$HOME|\$\{HOME\}/gu, homeDir)
24
+ .replace(/^~(?=$|[\\/])/u, homeDir)
25
+ .replaceAll(
26
+ /%([^%]+)%/gu,
27
+ (_match, key: string) => env[key] ?? env[key.toUpperCase()] ?? ""
28
+ )
29
+ );
30
+ }
31
+
32
+ function parseXdgDownloadsDir(
33
+ fileContents: string,
34
+ env: Record<string, string | undefined>,
35
+ homeDir: string
36
+ ): string | null {
37
+ const match = fileContents.match(XDG_DOWNLOAD_DIR_REGEX);
38
+ const rawValue = match?.[1] ?? match?.[2];
39
+ if (!rawValue) {
40
+ return null;
41
+ }
42
+ return expandEnvPath(rawValue, env, homeDir);
43
+ }
44
+
45
+ async function defaultReadTextFile(path: string): Promise<string | null> {
46
+ const file = Bun.file(path);
47
+ if (!(await file.exists())) {
48
+ return null;
49
+ }
50
+ return file.text();
51
+ }
52
+
53
+ export async function resolveDownloadsDir(
54
+ deps: ResolveDownloadsDirDeps = {}
55
+ ): Promise<string> {
56
+ const env = deps.env ?? process.env;
57
+ const platform = deps.platform ?? process.platform;
58
+ const homeDir = deps.homeDir ?? homedir();
59
+ const readTextFile = deps.readTextFile ?? defaultReadTextFile;
60
+
61
+ if (platform === "linux") {
62
+ const explicit = env.XDG_DOWNLOAD_DIR?.trim();
63
+ if (explicit) {
64
+ return expandEnvPath(explicit, env, homeDir);
65
+ }
66
+
67
+ const xdgConfigHome =
68
+ env.XDG_CONFIG_HOME?.trim() || join(homeDir, ".config");
69
+ const userDirs = await readTextFile(join(xdgConfigHome, "user-dirs.dirs"));
70
+ if (userDirs) {
71
+ const parsed = parseXdgDownloadsDir(userDirs, env, homeDir);
72
+ if (parsed) {
73
+ return parsed;
74
+ }
75
+ }
76
+ }
77
+
78
+ if (platform === "win32") {
79
+ const userProfile = env.USERPROFILE?.trim();
80
+ if (userProfile) {
81
+ return join(userProfile, "Downloads");
82
+ }
83
+ }
84
+
85
+ return join(homeDir, "Downloads");
86
+ }