codex-snapshots 0.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/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "codex-snapshots",
3
+ "version": "0.1.0",
4
+ "description": "Local-first read-only snapshots for Codex, Claude Code, and Trae sessions.",
5
+ "type": "module",
6
+ "packageManager": "pnpm@10.20.0",
7
+ "bin": {
8
+ "codex-snapshot": "bin/codex-snapshot.mjs",
9
+ "codex-snapshots": "bin/codex-snapshot.mjs",
10
+ "codex-snapshot-share": "server/share-api.mjs",
11
+ "codex-snapshots-site": "scripts/serve-site.mjs"
12
+ },
13
+ "keywords": [
14
+ "codex",
15
+ "claude",
16
+ "trae",
17
+ "agent",
18
+ "snapshot",
19
+ "sessions",
20
+ "redaction"
21
+ ],
22
+ "homepage": "https://ffffhx.github.io/codex-snapshots/",
23
+ "bugs": {
24
+ "url": "https://github.com/ffffhx/codex-snapshots/issues"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/ffffhx/codex-snapshots.git"
29
+ },
30
+ "author": "ffffhx",
31
+ "files": [
32
+ "bin",
33
+ "server",
34
+ "scripts",
35
+ "site",
36
+ "LICENSE",
37
+ "README.md"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "registry": "https://registry.npmjs.org/"
42
+ },
43
+ "scripts": {
44
+ "dev": "node bin/codex-snapshot.mjs serve --port 4321",
45
+ "snapshot": "node bin/codex-snapshot.mjs",
46
+ "snapshot:daemon": "node bin/codex-snapshot.mjs serve --port 4321",
47
+ "snapshot:install-daemon": "node scripts/launch-agent.mjs install",
48
+ "snapshot:uninstall-daemon": "node scripts/launch-agent.mjs uninstall",
49
+ "snapshot:daemon:status": "node scripts/launch-agent.mjs status",
50
+ "snapshot:daemon:logs": "node scripts/launch-agent.mjs logs",
51
+ "site:dev": "node scripts/serve-site.mjs",
52
+ "share:server": "node server/share-api.mjs",
53
+ "test:smoke": "node bin/codex-snapshot.mjs --help && node server/share-api.mjs --help && node scripts/serve-site.mjs --help && node --check bin/codex-snapshot.mjs && node --check server/share-api.mjs && node --check scripts/launch-agent.mjs && node --check scripts/serve-site.mjs"
54
+ },
55
+ "engines": {
56
+ "node": ">=18"
57
+ },
58
+ "license": "MIT",
59
+ "dependencies": {
60
+ "highlight.js": "^11.11.1",
61
+ "markdown-it": "^14.1.1"
62
+ }
63
+ }
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFile } from "node:child_process";
4
+ import { existsSync } from "node:fs";
5
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { promisify } from "node:util";
10
+
11
+ const execFileAsync = promisify(execFile);
12
+ const label = process.env.SNAPSHOT_LAUNCH_AGENT_LABEL || "com.codex-snapshots.viewer";
13
+ const uid = process.getuid?.() ?? Number.parseInt(process.env.UID || "", 10);
14
+ const homeDir = os.homedir();
15
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
16
+ const repoRoot = path.resolve(scriptDir, "..");
17
+ const launchAgentsDir = path.join(homeDir, "Library", "LaunchAgents");
18
+ const logsDir = path.join(homeDir, "Library", "Logs", "codex-snapshots");
19
+ const plistPath = path.join(launchAgentsDir, `${label}.plist`);
20
+ const stdoutPath = path.join(logsDir, "codex-snapshot.out.log");
21
+ const stderrPath = path.join(logsDir, "codex-snapshot.err.log");
22
+ const defaultApiUrl = "http://127.0.0.1:8787";
23
+ const defaultSiteUrl = "http://127.0.0.1:8787";
24
+
25
+ const command = process.argv[2] || "status";
26
+
27
+ main().catch((error) => {
28
+ console.error(error instanceof Error ? error.message : String(error));
29
+ process.exitCode = 1;
30
+ });
31
+
32
+ async function main() {
33
+ if (command === "install") {
34
+ await install();
35
+ return;
36
+ }
37
+ if (command === "uninstall") {
38
+ await uninstall();
39
+ return;
40
+ }
41
+ if (command === "status") {
42
+ await status();
43
+ return;
44
+ }
45
+ if (command === "logs") {
46
+ await logs();
47
+ return;
48
+ }
49
+ printHelp();
50
+ process.exitCode = 1;
51
+ }
52
+
53
+ async function install() {
54
+ const pnpmPath = await resolvePnpmPath();
55
+ await mkdir(launchAgentsDir, { recursive: true });
56
+ await mkdir(logsDir, { recursive: true });
57
+
58
+ const plist = renderPlist({
59
+ pnpmPath,
60
+ apiUrl: process.env.SNAPSHOT_SHARE_API_URL || defaultApiUrl,
61
+ siteUrl: process.env.SNAPSHOT_SHARE_SITE_URL || defaultSiteUrl,
62
+ });
63
+
64
+ await writeFile(plistPath, plist, "utf8");
65
+ await bootoutIfLoaded();
66
+ await execLaunchctl(["bootstrap", guiDomain(), plistPath]);
67
+ await execLaunchctl(["kickstart", "-k", `${guiDomain()}/${label}`]);
68
+
69
+ console.log(`Installed ${label}`);
70
+ console.log(`Plist: ${plistPath}`);
71
+ console.log(`Logs: ${stdoutPath}`);
72
+ console.log(`Preview: http://127.0.0.1:4321/`);
73
+ }
74
+
75
+ async function uninstall() {
76
+ await bootoutIfLoaded();
77
+ await rm(plistPath, { force: true });
78
+ console.log(`Uninstalled ${label}`);
79
+ }
80
+
81
+ async function status() {
82
+ if (!existsSync(plistPath)) {
83
+ console.log(`Not installed: ${plistPath}`);
84
+ return;
85
+ }
86
+ try {
87
+ const { stdout } = await execLaunchctl(["print", `${guiDomain()}/${label}`]);
88
+ const state = stdout.match(/state = ([^\n]+)/)?.[1]?.trim() || "unknown";
89
+ const pid = stdout.match(/pid = (\d+)/)?.[1] || "";
90
+ console.log(`${label}: ${state}${pid ? `, pid=${pid}` : ""}`);
91
+ console.log(`Plist: ${plistPath}`);
92
+ console.log(`Preview: http://127.0.0.1:4321/`);
93
+ } catch (error) {
94
+ console.log(`${label}: installed but not loaded`);
95
+ console.log(`Plist: ${plistPath}`);
96
+ if (error instanceof Error && error.message) {
97
+ console.log(error.message);
98
+ }
99
+ }
100
+ }
101
+
102
+ async function logs() {
103
+ console.log(`==> ${stdoutPath}`);
104
+ console.log(await tailFile(stdoutPath));
105
+ console.log(`==> ${stderrPath}`);
106
+ console.log(await tailFile(stderrPath));
107
+ }
108
+
109
+ async function bootoutIfLoaded() {
110
+ try {
111
+ await execLaunchctl(["bootout", guiDomain(), plistPath]);
112
+ } catch {}
113
+ try {
114
+ await execLaunchctl(["bootout", `${guiDomain()}/${label}`]);
115
+ } catch {}
116
+ }
117
+
118
+ async function resolvePnpmPath() {
119
+ if (process.env.PNPM_EXECUTABLE) {
120
+ return process.env.PNPM_EXECUTABLE;
121
+ }
122
+ try {
123
+ const { stdout } = await execFileAsync("/bin/zsh", ["-lc", "command -v pnpm"], {
124
+ cwd: repoRoot,
125
+ maxBuffer: 1024 * 1024,
126
+ });
127
+ const pnpmPath = stdout.trim().split("\n")[0];
128
+ if (pnpmPath) {
129
+ return pnpmPath;
130
+ }
131
+ } catch {}
132
+ throw new Error("Cannot find pnpm. Install pnpm first, or run with PNPM_EXECUTABLE=/absolute/path/to/pnpm.");
133
+ }
134
+
135
+ function renderPlist({ pnpmPath, apiUrl, siteUrl }) {
136
+ const shellCommand = `cd ${shellQuote(repoRoot)} && exec ${shellQuote(pnpmPath)} snapshot:daemon`;
137
+ const stablePath = [
138
+ path.dirname(pnpmPath),
139
+ "/opt/homebrew/bin",
140
+ "/usr/local/bin",
141
+ "/usr/bin",
142
+ "/bin",
143
+ "/usr/sbin",
144
+ "/sbin",
145
+ ].join(":");
146
+ const env = {
147
+ PATH: process.env.SNAPSHOT_DAEMON_PATH || stablePath,
148
+ SNAPSHOT_SHARE_API_URL: apiUrl,
149
+ SNAPSHOT_SHARE_SITE_URL: siteUrl,
150
+ SNAPSHOT_VIEWER_ALLOWED_ORIGINS:
151
+ process.env.SNAPSHOT_VIEWER_ALLOWED_ORIGINS ||
152
+ "http://127.0.0.1:3000,http://localhost:3000",
153
+ };
154
+
155
+ return `<?xml version="1.0" encoding="UTF-8"?>
156
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
157
+ <plist version="1.0">
158
+ <dict>
159
+ <key>Label</key>
160
+ <string>${xmlEscape(label)}</string>
161
+ <key>ProgramArguments</key>
162
+ <array>
163
+ <string>/bin/zsh</string>
164
+ <string>-lc</string>
165
+ <string>${xmlEscape(shellCommand)}</string>
166
+ </array>
167
+ <key>WorkingDirectory</key>
168
+ <string>${xmlEscape(repoRoot)}</string>
169
+ <key>EnvironmentVariables</key>
170
+ <dict>
171
+ ${Object.entries(env)
172
+ .map(([key, value]) => ` <key>${xmlEscape(key)}</key>\n <string>${xmlEscape(value)}</string>`)
173
+ .join("\n")}
174
+ </dict>
175
+ <key>RunAtLoad</key>
176
+ <true/>
177
+ <key>KeepAlive</key>
178
+ <true/>
179
+ <key>ThrottleInterval</key>
180
+ <integer>10</integer>
181
+ <key>ProcessType</key>
182
+ <string>Background</string>
183
+ <key>StandardOutPath</key>
184
+ <string>${xmlEscape(stdoutPath)}</string>
185
+ <key>StandardErrorPath</key>
186
+ <string>${xmlEscape(stderrPath)}</string>
187
+ </dict>
188
+ </plist>
189
+ `;
190
+ }
191
+
192
+ function guiDomain() {
193
+ if (!Number.isFinite(uid)) {
194
+ throw new Error("Cannot determine current macOS user id.");
195
+ }
196
+ return `gui/${uid}`;
197
+ }
198
+
199
+ async function execLaunchctl(args) {
200
+ return execFileAsync("/bin/launchctl", args, {
201
+ cwd: repoRoot,
202
+ maxBuffer: 1024 * 1024,
203
+ });
204
+ }
205
+
206
+ async function tailFile(filePath, lines = 80) {
207
+ try {
208
+ const text = await readFile(filePath, "utf8");
209
+ return text.split(/\r?\n/).slice(-lines).join("\n").trimEnd() || "(empty)";
210
+ } catch {
211
+ return "(missing)";
212
+ }
213
+ }
214
+
215
+ function shellQuote(value) {
216
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
217
+ }
218
+
219
+ function xmlEscape(value) {
220
+ return String(value)
221
+ .replace(/&/g, "&amp;")
222
+ .replace(/</g, "&lt;")
223
+ .replace(/>/g, "&gt;")
224
+ .replace(/"/g, "&quot;")
225
+ .replace(/'/g, "&apos;");
226
+ }
227
+
228
+ function printHelp() {
229
+ console.log(`Usage:
230
+ pnpm snapshot:install-daemon
231
+ pnpm snapshot:daemon:status
232
+ pnpm snapshot:daemon:logs
233
+ pnpm snapshot:uninstall-daemon
234
+
235
+ Environment:
236
+ PNPM_EXECUTABLE=/absolute/path/to/pnpm
237
+ SNAPSHOT_SHARE_API_URL=${defaultApiUrl}
238
+ SNAPSHOT_SHARE_SITE_URL=${defaultSiteUrl}
239
+ SNAPSHOT_VIEWER_ALLOWED_ORIGINS=http://127.0.0.1:3000,http://localhost:3000
240
+ `);
241
+ }
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createReadStream } from "node:fs";
4
+ import { stat } from "node:fs/promises";
5
+ import http from "node:http";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "site");
10
+ const parsed = parseArgs(process.argv.slice(2));
11
+ const host = parsed.host || "127.0.0.1";
12
+ const port = Number(parsed.port || 4323);
13
+
14
+ if (parsed.help) {
15
+ printHelp();
16
+ process.exit(0);
17
+ }
18
+
19
+ const server = http.createServer(async (request, response) => {
20
+ try {
21
+ const url = new URL(request.url || "/", `http://${request.headers.host || `${host}:${port}`}`);
22
+ const filePath = await resolveFile(url.pathname);
23
+ response.writeHead(200, {
24
+ "content-type": contentType(filePath),
25
+ "cache-control": "no-store",
26
+ });
27
+ createReadStream(filePath).pipe(response);
28
+ } catch {
29
+ response.writeHead(404, {
30
+ "content-type": "text/plain; charset=utf-8",
31
+ "cache-control": "no-store",
32
+ });
33
+ response.end("not found");
34
+ }
35
+ });
36
+
37
+ await new Promise((resolve, reject) => {
38
+ server.once("error", reject);
39
+ server.listen(port, host, resolve);
40
+ });
41
+
42
+ console.log(`Codex Snapshots website is running at http://${host}:${port}`);
43
+
44
+ async function resolveFile(pathname) {
45
+ const decoded = decodeURIComponent(pathname);
46
+ const normalized = path.normalize(decoded).replace(/^(\.\.[/\\])+/, "");
47
+ const requested = path.join(root, normalized);
48
+ const safePath = requested.startsWith(root) ? requested : root;
49
+ const info = await stat(safePath);
50
+
51
+ if (info.isDirectory()) {
52
+ return path.join(safePath, "index.html");
53
+ }
54
+
55
+ return safePath;
56
+ }
57
+
58
+ function contentType(filePath) {
59
+ const ext = path.extname(filePath).toLowerCase();
60
+ if (ext === ".html") return "text/html; charset=utf-8";
61
+ if (ext === ".css") return "text/css; charset=utf-8";
62
+ if (ext === ".js") return "application/javascript; charset=utf-8";
63
+ if (ext === ".svg") return "image/svg+xml";
64
+ if (ext === ".png") return "image/png";
65
+ if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
66
+ return "application/octet-stream";
67
+ }
68
+
69
+ function parseArgs(args) {
70
+ const options = {
71
+ help: false,
72
+ host: "",
73
+ port: "",
74
+ };
75
+
76
+ for (let index = 0; index < args.length; index += 1) {
77
+ const arg = args[index];
78
+ if (arg === "--") continue;
79
+ if (arg === "-h" || arg === "--help") {
80
+ options.help = true;
81
+ continue;
82
+ }
83
+ if (arg === "--host") {
84
+ options.host = String(args[++index] || "");
85
+ continue;
86
+ }
87
+ if (arg === "--port" || arg === "-p") {
88
+ options.port = String(args[++index] || "");
89
+ continue;
90
+ }
91
+ throw new Error(`unknown option: ${arg}`);
92
+ }
93
+
94
+ return options;
95
+ }
96
+
97
+ function printHelp() {
98
+ console.log(`codex-snapshots site server
99
+
100
+ Usage:
101
+ node scripts/serve-site.mjs [--host 127.0.0.1] [--port 4323]
102
+ `);
103
+ }