claude-view 0.2.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.
Files changed (3) hide show
  1. package/README.md +46 -0
  2. package/index.js +223 -0
  3. package/package.json +27 -0
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # claude-view
2
+
3
+ Browse and search your Claude Code sessions in a beautiful local web UI.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx claude-view
9
+ ```
10
+
11
+ This downloads the pre-built binary for your platform, caches it locally, and starts a local web server. Open the printed URL in your browser to explore your Claude Code sessions.
12
+
13
+ ## What It Does
14
+
15
+ - Reads your local `~/.claude/` session files (JSONL)
16
+ - Indexes them into a local SQLite database for fast search
17
+ - Serves a React UI at `http://localhost:47892`
18
+ - Everything stays on your machine -- no data is sent anywhere
19
+
20
+ ## Configuration
21
+
22
+ | Env Variable | Default | Description |
23
+ |---|---|---|
24
+ | `CLAUDE_VIEW_PORT` | `47892` | Port for the local server |
25
+ | `PORT` | `47892` | Alternative port override |
26
+
27
+ ## Supported Platforms
28
+
29
+ | OS | Architecture |
30
+ |---|---|
31
+ | macOS | Apple Silicon (arm64), Intel (x64) |
32
+ | Linux | x64 |
33
+ | Windows | x64 |
34
+
35
+ ## How It Works
36
+
37
+ On first run, `npx claude-view` downloads a platform-specific tarball from GitHub Releases containing a compiled Rust binary and bundled frontend assets. The binary is cached at `~/.cache/claude-view/` so subsequent runs start instantly.
38
+
39
+ ## Links
40
+
41
+ - [GitHub Repository](https://github.com/tombelieber/claude-view)
42
+ - [Report an Issue](https://github.com/tombelieber/claude-view/issues)
43
+
44
+ ## License
45
+
46
+ MIT
package/index.js ADDED
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { execFileSync, spawn } = require("child_process");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const os = require("os");
7
+ const https = require("https");
8
+ const zlib = require("zlib");
9
+
10
+ const VERSION = require("./package.json").version;
11
+ const REPO = "tombelieber/claude-view";
12
+ const BINARY_NAME = process.platform === "win32" ? "vibe-recall.exe" : "vibe-recall";
13
+
14
+ // --- Platform detection ---
15
+
16
+ const PLATFORM_MAP = {
17
+ "darwin-arm64": { artifact: "claude-view-darwin-arm64.tar.gz", ext: "tar.gz" },
18
+ "darwin-x64": { artifact: "claude-view-darwin-x64.tar.gz", ext: "tar.gz" },
19
+ "linux-x64": { artifact: "claude-view-linux-x64.tar.gz", ext: "tar.gz" },
20
+ "win32-x64": { artifact: "claude-view-win32-x64.zip", ext: "zip" },
21
+ };
22
+
23
+ const platformKey = `${process.platform}-${process.arch}`;
24
+ const platformInfo = PLATFORM_MAP[platformKey];
25
+
26
+ if (!platformInfo) {
27
+ console.error(
28
+ `Error: Unsupported platform "${process.platform}" with architecture "${process.arch}".\n` +
29
+ `Supported: macOS (arm64, x64), Linux (x64), Windows (x64).`
30
+ );
31
+ process.exit(1);
32
+ }
33
+
34
+ // --- Cache paths ---
35
+
36
+ const cacheDir = path.join(os.homedir(), ".cache", "claude-view");
37
+ const binDir = path.join(cacheDir, "bin");
38
+ const versionFile = path.join(cacheDir, "version");
39
+ const binaryPath = path.join(binDir, BINARY_NAME);
40
+ const distDir = path.join(binDir, "dist");
41
+
42
+ // --- Helpers ---
43
+
44
+ function download(url) {
45
+ return new Promise((resolve, reject) => {
46
+ const request = https.get(url, (res) => {
47
+ // Follow redirects (GitHub releases redirect to S3/CDN)
48
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
49
+ return download(res.headers.location).then(resolve, reject);
50
+ }
51
+ if (res.statusCode !== 200) {
52
+ reject(new Error(`Download failed: HTTP ${res.statusCode} from ${url}`));
53
+ res.resume();
54
+ return;
55
+ }
56
+ const chunks = [];
57
+ res.on("data", (chunk) => chunks.push(chunk));
58
+ res.on("end", () => resolve(Buffer.concat(chunks)));
59
+ res.on("error", reject);
60
+ });
61
+ request.on("error", reject);
62
+ });
63
+ }
64
+
65
+ function extractTarGz(buffer, destDir) {
66
+ // Use system tar — available on macOS, Linux, and modern Windows (tar ships with Win10+)
67
+ fs.mkdirSync(destDir, { recursive: true });
68
+ const tmpFile = path.join(os.tmpdir(), `claude-view-${Date.now()}.tar.gz`);
69
+ fs.writeFileSync(tmpFile, buffer);
70
+ try {
71
+ execFileSync("tar", ["xzf", tmpFile, "-C", destDir], { stdio: "pipe" });
72
+ } finally {
73
+ fs.unlinkSync(tmpFile);
74
+ }
75
+ }
76
+
77
+ function extractZip(buffer, destDir) {
78
+ // Use system tar on Windows 10+ (supports zip) or PowerShell as fallback
79
+ fs.mkdirSync(destDir, { recursive: true });
80
+ const tmpFile = path.join(os.tmpdir(), `claude-view-${Date.now()}.zip`);
81
+ fs.writeFileSync(tmpFile, buffer);
82
+ try {
83
+ if (process.platform === "win32") {
84
+ execFileSync(
85
+ "powershell",
86
+ ["-Command", `Expand-Archive -Force -Path '${tmpFile}' -DestinationPath '${destDir}'`],
87
+ { stdio: "pipe" }
88
+ );
89
+ } else {
90
+ execFileSync("unzip", ["-o", tmpFile, "-d", destDir], { stdio: "pipe" });
91
+ }
92
+ } finally {
93
+ fs.unlinkSync(tmpFile);
94
+ }
95
+ }
96
+
97
+ function downloadChecksums(version) {
98
+ const url = `https://github.com/${REPO}/releases/download/v${version}/checksums.txt`;
99
+ return download(url)
100
+ .then((buf) => {
101
+ const map = {};
102
+ const lines = buf.toString("utf-8").split("\n");
103
+ for (const line of lines) {
104
+ // Format: "<64-hex-chars> <filename>" (two spaces between hash and filename)
105
+ const match = line.match(/^([0-9a-f]{64}) (.+)$/);
106
+ if (match) {
107
+ map[match[2]] = match[1];
108
+ }
109
+ }
110
+ return map;
111
+ })
112
+ .catch(() => null); // Graceful fallback for older releases without checksums
113
+ }
114
+
115
+ function verifyChecksum(buffer, expectedHash) {
116
+ const crypto = require("crypto");
117
+ const actualHash = crypto.createHash("sha256").update(buffer).digest("hex");
118
+ if (actualHash !== expectedHash) {
119
+ console.error(`Checksum verification failed.`);
120
+ console.error(` Expected: ${expectedHash}`);
121
+ console.error(` Actual: ${actualHash}`);
122
+ process.exit(1);
123
+ }
124
+ }
125
+
126
+ // --- Main ---
127
+
128
+ async function main() {
129
+ // Check if cached version matches
130
+ let needsDownload = true;
131
+ if (fs.existsSync(versionFile) && fs.existsSync(binaryPath)) {
132
+ const cached = fs.readFileSync(versionFile, "utf-8").trim();
133
+ if (cached === VERSION) {
134
+ needsDownload = false;
135
+ }
136
+ }
137
+
138
+ if (needsDownload) {
139
+ const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${platformInfo.artifact}`;
140
+ console.log(`Downloading claude-view v${VERSION} for ${platformKey}...`);
141
+
142
+ let buffer;
143
+ try {
144
+ buffer = await download(url);
145
+ } catch (err) {
146
+ console.error(`\nFailed to download claude-view:\n ${err.message}`);
147
+ console.error(`\nURL: ${url}`);
148
+ console.error(`\nCheck that release v${VERSION} exists at https://github.com/${REPO}/releases`);
149
+ process.exit(1);
150
+ }
151
+
152
+ // Verify checksum if available
153
+ const checksums = await downloadChecksums(VERSION);
154
+ if (checksums && checksums[platformInfo.artifact]) {
155
+ verifyChecksum(buffer, checksums[platformInfo.artifact]);
156
+ console.log("Checksum verified.");
157
+ }
158
+
159
+ // Clean previous install
160
+ fs.rmSync(binDir, { recursive: true, force: true });
161
+ fs.mkdirSync(binDir, { recursive: true });
162
+
163
+ // Extract
164
+ try {
165
+ if (platformInfo.ext === "zip") {
166
+ extractZip(buffer, binDir);
167
+ } else {
168
+ extractTarGz(buffer, binDir);
169
+ }
170
+ } catch (err) {
171
+ console.error(`\nFailed to extract archive:\n ${err.message}`);
172
+ process.exit(1);
173
+ }
174
+
175
+ // Make binary executable (no-op on Windows)
176
+ if (process.platform !== "win32") {
177
+ fs.chmodSync(binaryPath, 0o755);
178
+ }
179
+
180
+ // macOS Gatekeeper: remove quarantine flag from downloaded binary
181
+ if (process.platform === "darwin") {
182
+ try {
183
+ execFileSync("xattr", ["-dr", "com.apple.quarantine", binDir], { stdio: "pipe" });
184
+ } catch {
185
+ // Ignore — xattr may not be available or quarantine flag may not be set
186
+ }
187
+ }
188
+
189
+ // Write version marker
190
+ fs.mkdirSync(cacheDir, { recursive: true });
191
+ fs.writeFileSync(versionFile, VERSION);
192
+
193
+ console.log(`Installed to ${binDir}`);
194
+ }
195
+
196
+ // Verify binary exists
197
+ if (!fs.existsSync(binaryPath)) {
198
+ console.error(`Error: Binary not found at ${binaryPath}`);
199
+ console.error("Try deleting ~/.cache/claude-view/ and running again.");
200
+ process.exit(1);
201
+ }
202
+
203
+ // Set STATIC_DIR so the server finds the frontend assets
204
+ const env = { ...process.env, STATIC_DIR: distDir };
205
+
206
+ // Run the server, forwarding signals and exit code
207
+ const child = spawn(binaryPath, process.argv.slice(2), { stdio: "inherit", env });
208
+
209
+ // Forward signals to child process
210
+ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
211
+ process.on(sig, () => child.kill(sig));
212
+ }
213
+
214
+ child.on("exit", (code, signal) => {
215
+ if (signal) {
216
+ process.kill(process.pid, signal);
217
+ } else {
218
+ process.exit(code ?? 1);
219
+ }
220
+ });
221
+ }
222
+
223
+ main();
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "claude-view",
3
+ "version": "0.2.0",
4
+ "description": "Browse and search your Claude Code sessions in a beautiful local web UI",
5
+ "bin": {
6
+ "claude-view": "./index.js"
7
+ },
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/tombelieber/claude-view"
12
+ },
13
+ "keywords": [
14
+ "claude",
15
+ "claude-code",
16
+ "sessions",
17
+ "viewer",
18
+ "browser"
19
+ ],
20
+ "engines": {
21
+ "node": ">=16"
22
+ },
23
+ "files": [
24
+ "index.js",
25
+ "README.md"
26
+ ]
27
+ }