pdf-brain 1.2.0 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdf-brain",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Local PDF & Markdown knowledge base with semantic search, AI enrichment, and SKOS taxonomy",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -51,6 +51,7 @@ ${docCount} documents indexed. Every command returns contextual next-action hint
51
51
  pdf-brain ingest <dir> [--enrich] [--auto-tag] [--recursive]
52
52
 
53
53
  ### Maintenance
54
+ pdf-brain update Self-update to latest release
54
55
  pdf-brain doctor [--fix] Health check (WAL, orphans, connectivity)
55
56
  pdf-brain config show|get|set View/modify configuration
56
57
  pdf-brain reindex [--clean] Re-embed all documents
package/src/cli.ts CHANGED
@@ -63,6 +63,7 @@ import {
63
63
  import { type CommandResult, generateHints } from "./agent/hints.js";
64
64
  import { formatHintBlock } from "./agent/format.js";
65
65
  import { renderHelp } from "./agent/manifest.js";
66
+ import { backgroundUpdateCheck, runUpdate } from "./updater.js";
66
67
 
67
68
  /**
68
69
  * Check if a string is a URL
@@ -1926,7 +1927,16 @@ process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
1926
1927
  // Handle taxonomy command separately (don't need full PDFLibrary)
1927
1928
  const args = process.argv.slice(2);
1928
1929
 
1929
- if (args[0] === "taxonomy") {
1930
+ // Background update check (fire-and-forget, once per day)
1931
+ backgroundUpdateCheck(VERSION);
1932
+
1933
+ // Handle update command before Effect machinery
1934
+ if (args[0] === "update") {
1935
+ runUpdate(VERSION).catch((err) => {
1936
+ console.error(`Update failed: ${err}`);
1937
+ process.exit(1);
1938
+ });
1939
+ } else if (args[0] === "taxonomy") {
1930
1940
  const quiet = args.includes("--quiet") || args.includes("--no-hints");
1931
1941
  const taxonomyProgram = Effect.gen(function* () {
1932
1942
  const subcommand = args[1];
package/src/updater.ts ADDED
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Self-updater for pdf-brain.
3
+ *
4
+ * - Background: silently downloads + replaces binary when a new version drops
5
+ * - `pdf-brain update` — force check + install now
6
+ *
7
+ * State file: ~/.pdf-brain/update-check.json
8
+ */
9
+
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, chmodSync } from "fs";
11
+ import { join } from "path";
12
+
13
+ const REPO = "joelhooks/pdf-library";
14
+ const STATE_DIR = join(process.env.HOME || "~", ".pdf-brain");
15
+ const STATE_FILE = join(STATE_DIR, "update-check.json");
16
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 1 day
17
+
18
+ interface UpdateState {
19
+ lastCheck: number;
20
+ latestVersion: string | null;
21
+ lastAutoUpdate: number;
22
+ }
23
+
24
+ function readState(): UpdateState {
25
+ try {
26
+ if (existsSync(STATE_FILE)) {
27
+ return JSON.parse(readFileSync(STATE_FILE, "utf-8"));
28
+ }
29
+ } catch {}
30
+ return { lastCheck: 0, latestVersion: null, lastAutoUpdate: 0 };
31
+ }
32
+
33
+ function writeState(state: UpdateState): void {
34
+ try {
35
+ if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true });
36
+ writeFileSync(STATE_FILE, JSON.stringify(state));
37
+ } catch {}
38
+ }
39
+
40
+ /**
41
+ * Fetch latest release tag from GitHub.
42
+ */
43
+ async function fetchLatestVersion(): Promise<string | null> {
44
+ try {
45
+ const resp = await fetch(
46
+ `https://api.github.com/repos/${REPO}/releases/latest`,
47
+ {
48
+ headers: { Accept: "application/vnd.github.v3+json" },
49
+ signal: AbortSignal.timeout(5000),
50
+ }
51
+ );
52
+ if (!resp.ok) return null;
53
+ const data = (await resp.json()) as { tag_name?: string };
54
+ return data.tag_name?.replace(/^v/, "") ?? null;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Compare semver strings. Returns 1 if a > b, -1 if a < b, 0 if equal.
62
+ */
63
+ function compareSemver(a: string, b: string): number {
64
+ const pa = a.split(".").map(Number);
65
+ const pb = b.split(".").map(Number);
66
+ for (let i = 0; i < 3; i++) {
67
+ const va = pa[i] || 0;
68
+ const vb = pb[i] || 0;
69
+ if (va > vb) return 1;
70
+ if (va < vb) return -1;
71
+ }
72
+ return 0;
73
+ }
74
+
75
+ /**
76
+ * Get the download URL for the current platform.
77
+ */
78
+ function getAssetUrl(version: string): string {
79
+ const os = process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "windows" : "linux";
80
+ const arch = process.arch === "arm64" ? "arm64" : "x64";
81
+ const ext = os === "windows" ? ".exe" : "";
82
+ return `https://github.com/${REPO}/releases/download/v${version}/pdf-brain-${os}-${arch}${ext}`;
83
+ }
84
+
85
+ /**
86
+ * Download binary and atomically replace the current one.
87
+ * Returns true on success, false on failure. Never throws.
88
+ */
89
+ async function downloadAndReplace(version: string): Promise<boolean> {
90
+ const url = getAssetUrl(version);
91
+ const binaryPath = process.execPath;
92
+ const tmpPath = `${binaryPath}.update-${Date.now()}`;
93
+
94
+ try {
95
+ const resp = await fetch(url, { signal: AbortSignal.timeout(30000) });
96
+ if (!resp.ok) return false;
97
+
98
+ const buffer = await resp.arrayBuffer();
99
+ await Bun.write(tmpPath, buffer);
100
+ chmodSync(tmpPath, 0o755);
101
+ renameSync(tmpPath, binaryPath);
102
+ return true;
103
+ } catch {
104
+ try { unlinkSync(tmpPath); } catch {}
105
+ return false;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Background auto-update. Runs silently on every invocation (once/day).
111
+ * If newer version exists, downloads and replaces the binary.
112
+ * Current invocation keeps running the old code — new version takes effect next run.
113
+ */
114
+ export function backgroundUpdateCheck(currentVersion: string): void {
115
+ // Don't auto-update in dev mode
116
+ if (currentVersion.includes("compiled") || currentVersion === "0.0.0") return;
117
+
118
+ const state = readState();
119
+ const now = Date.now();
120
+
121
+ // Checked recently, skip
122
+ if (now - state.lastCheck < CHECK_INTERVAL_MS) return;
123
+
124
+ // Fire-and-forget: check + update in background
125
+ (async () => {
126
+ const latest = await fetchLatestVersion();
127
+ const newState: UpdateState = {
128
+ lastCheck: now,
129
+ latestVersion: latest,
130
+ lastAutoUpdate: state.lastAutoUpdate,
131
+ };
132
+
133
+ if (latest && compareSemver(latest, currentVersion) > 0) {
134
+ const ok = await downloadAndReplace(latest);
135
+ if (ok) {
136
+ newState.lastAutoUpdate = now;
137
+ newState.latestVersion = latest;
138
+ // Brief note so they know why behavior might change
139
+ console.error(`\x1b[2mUpdated pdf-brain v${currentVersion} → v${latest}\x1b[0m`);
140
+ }
141
+ }
142
+
143
+ writeState(newState);
144
+ })().catch(() => {});
145
+ }
146
+
147
+ /**
148
+ * Explicit `pdf-brain update` command. Checks, downloads, replaces, verifies.
149
+ */
150
+ export async function runUpdate(currentVersion: string): Promise<void> {
151
+ console.log("Checking for updates...\n");
152
+
153
+ const latest = await fetchLatestVersion();
154
+ if (!latest) {
155
+ console.log("Could not reach GitHub. Check your connection.");
156
+ process.exit(1);
157
+ }
158
+
159
+ writeState({ lastCheck: Date.now(), latestVersion: latest, lastAutoUpdate: readState().lastAutoUpdate });
160
+
161
+ if (compareSemver(latest, currentVersion) <= 0) {
162
+ console.log(`Already on latest (v${currentVersion}).`);
163
+ return;
164
+ }
165
+
166
+ console.log(`v${currentVersion} → v${latest}`);
167
+ console.log(`Downloading...`);
168
+
169
+ const ok = await downloadAndReplace(latest);
170
+ if (!ok) {
171
+ console.error(`Download failed.`);
172
+ console.error(
173
+ `\nManual install:\n curl -fsSL https://raw.githubusercontent.com/${REPO}/main/scripts/install.sh | bash`
174
+ );
175
+ process.exit(1);
176
+ }
177
+
178
+ console.log(`Updated to v${latest}.`);
179
+
180
+ // Verify
181
+ const result = Bun.spawnSync([process.execPath, "--version"], { stdout: "pipe" });
182
+ const output = result.stdout.toString().trim();
183
+ if (output) console.log(output);
184
+ }