kt-wrapped 0.0.0-stage → 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/README.md CHANGED
@@ -1,3 +1,152 @@
1
- # Temporary Holding Version
1
+ <div align="center">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # kt-wrapped
4
+
5
+ **Your year in code, beautifully visualized.**
6
+
7
+ <p>
8
+ <strong>Credit:</strong> Modified from
9
+ <a href="https://github.com/numman-ali/codex-wrapped">numman-ali/codex-wrapped</a>,
10
+ which was built on top of
11
+ <a href="https://github.com/moddi3/opencode-wrapped">opencode-wrapped</a>.
12
+ </p>
13
+ <p>
14
+ Built for
15
+ <a href="https://github.com/Kohaku-Lab/KohakuTerrarium">Kohaku-Lab/KohakuTerrarium</a>.
16
+ </p>
17
+
18
+ Generate a personalized "Spotify Wrapped"-style summary of your
19
+ [KohakuTerrarium](https://github.com/Kohaku-Lab/KohakuTerrarium) usage.
20
+
21
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
22
+ [![Bun](https://img.shields.io/badge/Bun-%23000000.svg?logo=bun&logoColor=white)](https://bun.sh)
23
+
24
+ <img src="./assets/images/demo-wrapped.png" alt="KT Wrapped Example" width="600" />
25
+
26
+ </div>
27
+
28
+ ---
29
+
30
+ ## Installation
31
+
32
+ ### Quick Start
33
+
34
+ Run directly without installing:
35
+
36
+ ```bash
37
+ npx kt-wrapped # or bunx, or yarn/pnpm dlx
38
+ ```
39
+
40
+ ### Global Install
41
+
42
+ ```bash
43
+ npm install -g kt-wrapped # or bun/yarn/pnpm
44
+ ```
45
+
46
+ Then run anywhere:
47
+
48
+ ```bash
49
+ kt-wrapped
50
+ ```
51
+
52
+ ## Usage Options
53
+
54
+ | Option | Description |
55
+ | --------------- | ------------------------------------ |
56
+ | `--year, -y` | Generate wrapped for a specific year |
57
+ | `--help, -h` | Show help message |
58
+ | `--version, -v` | Show version number |
59
+
60
+ ## Features
61
+
62
+ - Sessions, turns, tokens, projects, and streaks
63
+ - GitHub-style activity heatmap
64
+ - Top models and providers breakdown
65
+ - Usage cost (when available)
66
+ - Shareable PNG image
67
+ - Inline image display (Ghostty, Kitty, iTerm2, WezTerm, Konsole)
68
+ - Auto-copy to clipboard
69
+
70
+ ## Terminal Support
71
+
72
+ The wrapped image displays natively in terminals that support inline images:
73
+
74
+ | Terminal | Protocol | Status |
75
+ | ------------------------------------------ | -------------- | --------------------------- |
76
+ | [Ghostty](https://ghostty.org) | Kitty Graphics | Full support |
77
+ | [Kitty](https://sw.kovidgoyal.net/kitty/) | Kitty Graphics | Full support |
78
+ | [WezTerm](https://wezfurlong.org/wezterm/) | Kitty + iTerm2 | Full support |
79
+ | [iTerm2](https://iterm2.com) | iTerm2 Inline | Full support |
80
+ | [Konsole](https://konsole.kde.org) | Kitty Graphics | Full support |
81
+ | Other terminals | — | Image saved to file only |
82
+
83
+ ## Output
84
+
85
+ The tool generates:
86
+
87
+ 1. **Terminal Summary** — Quick stats overview in your terminal
88
+ 2. **PNG Image** — A beautiful, shareable wrapped card saved to your home directory
89
+ 3. **Clipboard** — Automatically copies the image to your clipboard
90
+
91
+ ## Data Source
92
+
93
+ `kt-wrapped` reads data from your local KohakuTerrarium session store:
94
+
95
+ ```
96
+ ~/.kohakuterrarium/sessions
97
+ ```
98
+
99
+ No data is sent anywhere. Everything is processed locally.
100
+
101
+ Token pricing for cost estimates is also local and versioned in:
102
+
103
+ ```
104
+ src/pricing-data.ts
105
+ ```
106
+
107
+ If a model is missing from that table, cost is treated as unavailable for that model until it is added in a new release.
108
+
109
+ ## Building
110
+
111
+ ### Development
112
+
113
+ ```bash
114
+ # Run in development mode with hot reload
115
+ bun run dev
116
+ ```
117
+
118
+ ### Production Build
119
+
120
+ ```bash
121
+ # Build for all platforms
122
+ bun run build
123
+ ```
124
+
125
+ ### Releasing
126
+
127
+ Releases are automated via [semantic-release](https://semantic-release.gitbook.io). Merging PRs with [conventional commits](https://www.conventionalcommits.org) to `main` triggers a release.
128
+
129
+ ## Tech Stack
130
+
131
+ - **Runtime**: [Bun](https://bun.sh)
132
+ - **Image Generation**: [Satori](https://github.com/vercel/satori) + [Resvg](https://github.com/nicolo-ribaudo/resvg-js)
133
+ - **CLI UI**: [@clack/prompts](https://github.com/natemoo-re/clack)
134
+ - **Font**: IBM Plex Mono
135
+
136
+ ## Contributing
137
+
138
+ Contributions are welcome! Please feel free to submit a Pull Request.
139
+
140
+ ## License
141
+
142
+ MIT License - see [LICENSE](LICENSE) for details.
143
+
144
+ ---
145
+
146
+ <div align="center">
147
+
148
+ Built for the KohakuTerrarium community
149
+
150
+ Credit: <a href="https://github.com/ryoppippi/ccusage">ccusage</a>
151
+
152
+ </div>
Binary file
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import os from "node:os";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const projectRoot = path.resolve(__dirname, "..");
12
+
13
+ function run(command, args) {
14
+ const result = spawnSync(command, args, {
15
+ stdio: "inherit",
16
+ });
17
+ if (result.error) {
18
+ console.error(result.error.message);
19
+ process.exit(1);
20
+ }
21
+ const code = typeof result.status === "number" ? result.status : 0;
22
+ process.exit(code);
23
+ }
24
+
25
+ const localEntry = path.join(projectRoot, "src", "index.ts");
26
+ if (fs.existsSync(localEntry)) {
27
+ run("bun", [localEntry, ...process.argv.slice(2)]);
28
+ }
29
+
30
+ const platformMap = {
31
+ darwin: "darwin",
32
+ linux: "linux",
33
+ win32: "windows",
34
+ };
35
+ const archMap = {
36
+ x64: "x64",
37
+ arm64: "arm64",
38
+ };
39
+
40
+ let platform = platformMap[os.platform()];
41
+ if (!platform) {
42
+ platform = os.platform();
43
+ }
44
+ let arch = archMap[os.arch()];
45
+ if (!arch) {
46
+ arch = os.arch();
47
+ }
48
+
49
+ const base = "kt-wrapped-" + platform + "-" + arch;
50
+ const binary = platform === "windows" ? "kt-wrapped.exe" : "kt-wrapped";
51
+
52
+ function findBinary(startDir) {
53
+ let current = startDir;
54
+ for (;;) {
55
+ const modules = path.join(current, "node_modules");
56
+ if (fs.existsSync(modules)) {
57
+ const entries = fs.readdirSync(modules);
58
+ for (const entry of entries) {
59
+ if (!entry.startsWith(base)) {
60
+ continue;
61
+ }
62
+ const candidate = path.join(modules, entry, "bin", binary);
63
+ if (fs.existsSync(candidate)) {
64
+ return candidate;
65
+ }
66
+ }
67
+ }
68
+ const parent = path.dirname(current);
69
+ if (parent === current) {
70
+ return;
71
+ }
72
+ current = parent;
73
+ }
74
+ }
75
+
76
+ const resolved = findBinary(__dirname);
77
+ if (!resolved) {
78
+ console.error(
79
+ 'Unable to find a runnable kt-wrapped binary. For local development, run "bun src/index.ts".'
80
+ );
81
+ process.exit(1);
82
+ }
83
+
84
+ run(resolved, process.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
2
  "name": "kt-wrapped",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Generate a personalized Spotify Wrapped-style summary of your KohakuTerrarium usage",
5
+ "bin": {
6
+ "kt-wrapped": "bin/kt-wrapped.mjs"
7
+ },
8
+ "scripts": {
9
+ "postinstall": "node ./postinstall.mjs"
10
+ },
11
+ "optionalDependencies": {
12
+ "kt-wrapped-linux-arm64": "0.1.0",
13
+ "kt-wrapped-linux-x64": "0.1.0",
14
+ "kt-wrapped-linux-x64-baseline": "0.1.0",
15
+ "kt-wrapped-linux-arm64-musl": "0.1.0",
16
+ "kt-wrapped-linux-x64-musl": "0.1.0",
17
+ "kt-wrapped-linux-x64-baseline-musl": "0.1.0",
18
+ "kt-wrapped-darwin-arm64": "0.1.0",
19
+ "kt-wrapped-darwin-x64": "0.1.0",
20
+ "kt-wrapped-darwin-x64-baseline": "0.1.0",
21
+ "kt-wrapped-windows-x64": "0.1.0",
22
+ "kt-wrapped-windows-x64-baseline": "0.1.0"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/Kohaku-Lab/KohakuTerrarium.git"
27
+ },
28
+ "homepage": "https://github.com/Kohaku-Lab/KohakuTerrarium",
29
+ "bugs": {
30
+ "url": "https://github.com/Kohaku-Lab/KohakuTerrarium/issues"
31
+ },
32
+ "keywords": [
33
+ "kohakuterrarium",
34
+ "kt",
35
+ "wrapped",
36
+ "kt-wrapped"
37
+ ],
38
+ "author": {
39
+ "name": "Numman Ali",
40
+ "email": "numman.ali@gmail.com"
41
+ },
42
+ "license": "MIT"
6
43
  }
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Postinstall script for codex-wrapped-noyrlimit
5
+ *
6
+ * This script runs after npm install and symlinks the correct platform-specific
7
+ * binary to the bin directory. It auto-detects:
8
+ * - Platform (darwin, linux, windows)
9
+ * - Architecture (arm64, x64)
10
+ * - Libc (glibc, musl) for Linux
11
+ * - AVX2 support (baseline vs optimized) for x64
12
+ */
13
+
14
+ import fs from "fs";
15
+ import path from "path";
16
+ import os from "os";
17
+ import { execSync } from "child_process";
18
+ import { fileURLToPath } from "url";
19
+ import { createRequire } from "module";
20
+
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+ const require = createRequire(import.meta.url);
23
+
24
+ /**
25
+ * Detect if the system uses musl libc (Alpine Linux, etc.)
26
+ */
27
+ function detectMusl() {
28
+ if (os.platform() !== "linux") return false;
29
+
30
+ try {
31
+ // Method 1: Check ldd output
32
+ const lddOutput = execSync("ldd --version 2>&1 || true", { encoding: "utf8" });
33
+ if (lddOutput.toLowerCase().includes("musl")) {
34
+ return true;
35
+ }
36
+
37
+ // Method 2: Check for musl loader
38
+ const files = fs.readdirSync("/lib").filter((f) => f.startsWith("ld-musl-"));
39
+ if (files.length > 0) {
40
+ return true;
41
+ }
42
+ } catch {
43
+ // Ignore errors
44
+ }
45
+
46
+ return false;
47
+ }
48
+
49
+ /**
50
+ * Detect if the CPU supports AVX2 instructions
51
+ */
52
+ function detectAVX2() {
53
+ if (os.arch() !== "x64") return true; // Only relevant for x64
54
+
55
+ try {
56
+ if (os.platform() === "linux") {
57
+ const cpuinfo = fs.readFileSync("/proc/cpuinfo", "utf8");
58
+ return cpuinfo.toLowerCase().includes("avx2");
59
+ }
60
+
61
+ if (os.platform() === "darwin") {
62
+ const output = execSync("sysctl -n machdep.cpu.features 2>/dev/null || true", {
63
+ encoding: "utf8",
64
+ });
65
+ return output.toLowerCase().includes("avx2");
66
+ }
67
+
68
+ if (os.platform() === "win32") {
69
+ // Windows: Assume AVX2 support on modern systems
70
+ // A more robust check would require native code
71
+ return true;
72
+ }
73
+ } catch {
74
+ // If we can't detect, assume AVX2 is supported
75
+ }
76
+
77
+ return true;
78
+ }
79
+
80
+ /**
81
+ * Get the platform-specific package name
82
+ */
83
+ function getPackageName() {
84
+ let platform;
85
+ switch (os.platform()) {
86
+ case "darwin":
87
+ platform = "darwin";
88
+ break;
89
+ case "linux":
90
+ platform = "linux";
91
+ break;
92
+ case "win32":
93
+ platform = "windows";
94
+ break;
95
+ default:
96
+ return null;
97
+ }
98
+
99
+ let arch;
100
+ switch (os.arch()) {
101
+ case "x64":
102
+ arch = "x64";
103
+ break;
104
+ case "arm64":
105
+ arch = "arm64";
106
+ break;
107
+ default:
108
+ return null;
109
+ }
110
+
111
+ // Build package name parts
112
+ const parts = ["codex-wrapped-noyrlimit", platform, arch];
113
+
114
+ // Add baseline suffix for x64 without AVX2
115
+ if (arch === "x64" && !detectAVX2()) {
116
+ parts.push("baseline");
117
+ }
118
+
119
+ // Add musl suffix for Linux with musl libc
120
+ if (platform === "linux" && detectMusl()) {
121
+ parts.push("musl");
122
+ }
123
+
124
+ return parts.join("-");
125
+ }
126
+
127
+ /**
128
+ * Find the binary from the platform package
129
+ */
130
+ function findBinary(packageName) {
131
+ const binaryName =
132
+ os.platform() === "win32" ? "codex-wrapped-noyrlimit.exe" : "codex-wrapped-noyrlimit";
133
+
134
+ try {
135
+ const packageJsonPath = require.resolve(`${packageName}/package.json`);
136
+ const packageDir = path.dirname(packageJsonPath);
137
+ const binaryPath = path.join(packageDir, "bin", binaryName);
138
+
139
+ if (fs.existsSync(binaryPath)) {
140
+ return { binaryPath, binaryName };
141
+ }
142
+ } catch {
143
+ // Package not found via require.resolve
144
+ }
145
+
146
+ // Fallback: try common paths
147
+ const fallbackPaths = [
148
+ path.join(__dirname, "..", packageName, "bin", binaryName),
149
+ path.join(__dirname, "node_modules", packageName, "bin", binaryName),
150
+ ];
151
+
152
+ for (const p of fallbackPaths) {
153
+ if (fs.existsSync(p)) {
154
+ return { binaryPath: p, binaryName };
155
+ }
156
+ }
157
+
158
+ return null;
159
+ }
160
+
161
+ /**
162
+ * Prepare the bin directory
163
+ */
164
+ function prepareBinDirectory(binaryName) {
165
+ const binDir = path.join(__dirname, "bin");
166
+ const targetPath = path.join(binDir, binaryName);
167
+
168
+ // Ensure bin directory exists
169
+ if (!fs.existsSync(binDir)) {
170
+ fs.mkdirSync(binDir, { recursive: true });
171
+ }
172
+
173
+ // Remove existing binary/symlink if it exists
174
+ if (fs.existsSync(targetPath)) {
175
+ fs.unlinkSync(targetPath);
176
+ }
177
+
178
+ return { binDir, targetPath };
179
+ }
180
+
181
+ /**
182
+ * Create symlink (or copy on Windows)
183
+ */
184
+ function linkBinary(sourcePath, binaryName) {
185
+ const { targetPath } = prepareBinDirectory(binaryName);
186
+
187
+ if (os.platform() === "win32") {
188
+ // Windows: copy instead of symlink (symlinks require admin)
189
+ fs.copyFileSync(sourcePath, targetPath);
190
+ } else {
191
+ fs.symlinkSync(sourcePath, targetPath);
192
+ }
193
+
194
+ // Verify the file exists
195
+ if (!fs.existsSync(targetPath)) {
196
+ throw new Error(`Failed to create binary at ${targetPath}`);
197
+ }
198
+ }
199
+
200
+ async function main() {
201
+ try {
202
+ const packageName = getPackageName();
203
+
204
+ if (!packageName) {
205
+ console.error(`codex-wrapped-noyrlimit: Unsupported platform: ${os.platform()}-${os.arch()}`);
206
+ console.error("Please download the binary manually from:");
207
+ console.error("https://github.com/numman-ali/codex-wrapped/releases");
208
+ process.exit(0); // Exit gracefully
209
+ }
210
+
211
+ console.log(`codex-wrapped-noyrlimit: Detected platform package: ${packageName}`);
212
+
213
+ const result = findBinary(packageName);
214
+
215
+ if (!result) {
216
+ // Try fallback without baseline/musl
217
+ const baseParts = packageName.split("-").slice(0, 3);
218
+ const basePackage = baseParts.join("-");
219
+
220
+ if (basePackage !== packageName) {
221
+ console.log(`codex-wrapped-noyrlimit: Trying fallback package: ${basePackage}`);
222
+ const fallbackResult = findBinary(basePackage);
223
+
224
+ if (fallbackResult) {
225
+ linkBinary(fallbackResult.binaryPath, fallbackResult.binaryName);
226
+ return;
227
+ }
228
+ }
229
+
230
+ console.error(`codex-wrapped-noyrlimit: Could not find binary for ${packageName}`);
231
+ console.error("The optional dependency may have failed to install.");
232
+ console.error("Please download the binary manually from:");
233
+ console.error("https://github.com/numman-ali/codex-wrapped/releases");
234
+ process.exit(0);
235
+ }
236
+
237
+ linkBinary(result.binaryPath, result.binaryName);
238
+ } catch (error) {
239
+ console.error("codex-wrapped-noyrlimit: Postinstall error:", error.message);
240
+ process.exit(0); // Exit gracefully to not break npm install
241
+ }
242
+ }
243
+
244
+ main();