vibescc 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.
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # Vibes
2
+
3
+ Branded Claude Code. Custom crab colors + spinner verbs. One command.
4
+
5
+ ```
6
+ yc → orange crab stripe → purple crab
7
+ vercel → black crab supabase → green crab
8
+ ```
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ bunx vibes
14
+ ```
15
+
16
+ Or with git:
17
+
18
+ ```bash
19
+ git clone https://github.com/djessicatony/vibescc.git
20
+ cd vibescc
21
+ ./install.sh
22
+ ```
23
+
24
+ Pick packs, open a new tab, done:
25
+
26
+ ```bash
27
+ yc # orange crab
28
+ stripe --resume # purple crab, resume session
29
+ yc --verbs looksmaxxing # orange crab, custom verbs
30
+ yc --dangerously-skip-permissions # all claude flags work
31
+ ```
32
+
33
+ Uninstall: `bunx vibes uninstall` or `./uninstall.sh`
34
+
35
+ ## How it works
36
+
37
+ A PTY wrapper intercepts Claude's ANSI output and swaps the crab's color bytes before they hit your terminal. No binary patching. Pixel-perfect to the original — same crab, different paint. Survives Claude Code updates.
38
+
39
+ **Crab colors** = per tab. Different crabs in different tabs.
40
+
41
+ **Spinner verbs** = global (`~/.claude/settings.json`). Last write wins. Use `--verbs <pack>` to switch.
42
+
43
+ ## Brand packs
44
+
45
+ | Pack | Body | Eyes |
46
+ |------|------|------|
47
+ | **yc** | `#FB651E` | `#FFFFFF` |
48
+ | **stripe** | `#635BFF` | `#FFFFFF` |
49
+ | **vercel** | `#000000` | `#FFFFFF` |
50
+ | **supabase** | `#3ECF8E` | `#FAFAFA` |
51
+ | **looksmaxxing** | `#1A1A2E` | `#E94560` |
52
+
53
+ ### Create your own
54
+
55
+ ```bash
56
+ mkdir packs/mycompany
57
+ cat > packs/mycompany/pack.json << 'EOF'
58
+ {
59
+ "name": "My Company",
60
+ "slug": "mycompany",
61
+ "colors": { "body": "#FF0000", "eyes": "#FFFFFF" },
62
+ "verbs": ["Shipping", "Building", "Deploying"]
63
+ }
64
+ EOF
65
+ ```
66
+
67
+ Re-run installer to get the alias.
68
+
69
+ ## Requirements
70
+
71
+ - Python 3.6+ (stdlib only)
72
+ - Claude Code in PATH
73
+ - macOS or Linux
74
+
75
+ ## License
76
+
77
+ MIT
package/bin/vibes.mjs ADDED
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync, spawnSync } from "child_process";
4
+ import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync, readdirSync, readlinkSync } from "fs";
5
+ import { join, dirname } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { createInterface } from "readline";
8
+ import { homedir } from "os";
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const ROOT = join(__dirname, "..");
12
+ const HOME = homedir();
13
+ const INSTALL_DIR = join(HOME, ".vibes");
14
+ const SETTINGS = join(HOME, ".claude", "settings.json");
15
+
16
+ // ── Colors ──────────────────────────────────────────────────────────────
17
+ const c = {
18
+ reset: "\x1b[0m",
19
+ bold: "\x1b[1m",
20
+ dim: "\x1b[2m",
21
+ green: "\x1b[32m",
22
+ yellow: "\x1b[33m",
23
+ red: "\x1b[31m",
24
+ cyan: "\x1b[36m",
25
+ };
26
+
27
+ const info = (msg) => console.log(`${c.green}▸${c.reset} ${msg}`);
28
+ const warn = (msg) => console.log(`${c.yellow}▸${c.reset} ${msg}`);
29
+
30
+ // ── Banner ──────────────────────────────────────────────────────────────
31
+ function showBanner() {
32
+ try {
33
+ spawnSync("python3", [join(ROOT, "scripts", "banner.py"), "vibescc"], {
34
+ stdio: "inherit",
35
+ });
36
+ } catch {
37
+ console.log(`\n ${c.bold}VIBESCC${c.reset}\n`);
38
+ }
39
+ console.log(` ${c.dim}branded Claude Code${c.reset}\n`);
40
+ }
41
+
42
+ // ── Detect shell rc ─────────────────────────────────────────────────────
43
+ function getShellRc() {
44
+ const shell = process.env.SHELL || "";
45
+ if (shell.includes("zsh") || process.platform === "darwin") {
46
+ return join(HOME, ".zshrc");
47
+ }
48
+ return join(HOME, ".bashrc");
49
+ }
50
+
51
+ // ── Read packs ──────────────────────────────────────────────────────────
52
+ function loadPacks() {
53
+ const packsDir = join(ROOT, "packs");
54
+ return readdirSync(packsDir, { withFileTypes: true })
55
+ .filter((d) => d.isDirectory())
56
+ .map((d) => {
57
+ const packJson = JSON.parse(
58
+ readFileSync(join(packsDir, d.name, "pack.json"), "utf8")
59
+ );
60
+ return { slug: d.name, ...packJson };
61
+ })
62
+ .sort((a, b) => a.slug.localeCompare(b.slug));
63
+ }
64
+
65
+ // ── Prompt ──────────────────────────────────────────────────────────────
66
+ function ask(question) {
67
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
68
+ return new Promise((resolve) => {
69
+ rl.question(question, (answer) => {
70
+ rl.close();
71
+ resolve(answer.trim());
72
+ });
73
+ });
74
+ }
75
+
76
+ // ── Install ─────────────────────────────────────────────────────────────
77
+ async function main() {
78
+ showBanner();
79
+
80
+ const packs = loadPacks();
81
+
82
+ // Show packs
83
+ console.log(" Pick what to install:\n");
84
+ packs.forEach((p, i) => {
85
+ console.log(
86
+ ` ${c.bold}${i + 1})${c.reset} ${p.slug} ${c.dim}(${p.name}, ${p.colors.body})${c.reset}`
87
+ );
88
+ });
89
+ console.log(`\n ${c.bold}a)${c.reset} all of the above\n`);
90
+
91
+ const choice = (await ask(` Choice [a]: `)) || "a";
92
+
93
+ // Parse selection
94
+ let selected;
95
+ if (choice.toLowerCase() === "a") {
96
+ selected = packs;
97
+ } else {
98
+ const nums = choice.split(/[,\s]+/).map(Number);
99
+ selected = nums
100
+ .filter((n) => n >= 1 && n <= packs.length)
101
+ .map((n) => packs[n - 1]);
102
+ }
103
+
104
+ if (selected.length === 0) {
105
+ console.log(`${c.red}Nothing selected.${c.reset}`);
106
+ process.exit(1);
107
+ }
108
+
109
+ console.log();
110
+
111
+ // ── Copy files to ~/.vibescc ────────────────────────────────────────
112
+ info(`Installing to ${c.bold}${INSTALL_DIR}${c.reset}`);
113
+ mkdirSync(INSTALL_DIR, { recursive: true });
114
+ cpSync(join(ROOT, "scripts"), join(INSTALL_DIR, "scripts"), { recursive: true });
115
+ cpSync(join(ROOT, "packs"), join(INSTALL_DIR, "packs"), { recursive: true });
116
+
117
+ // ── Shell rc ────────────────────────────────────────────────────────
118
+ const shellRc = getShellRc();
119
+ if (!existsSync(shellRc)) writeFileSync(shellRc, "");
120
+ let rcContent = readFileSync(shellRc, "utf8");
121
+
122
+ // Remove old vibescc aliases
123
+ rcContent = rcContent
124
+ .split("\n")
125
+ .filter((line) => !line.includes("# vibes:"))
126
+ .join("\n");
127
+
128
+ const launcher = join(INSTALL_DIR, "scripts", "vibescc-launch.py");
129
+
130
+ for (const pack of selected) {
131
+ // Show banner for each pack
132
+ try {
133
+ spawnSync("python3", [join(ROOT, "scripts", "banner.py"), pack.slug], {
134
+ stdio: "inherit",
135
+ });
136
+ } catch {}
137
+
138
+ // Add alias
139
+ const packDir = join(INSTALL_DIR, "packs", pack.slug);
140
+ const alias = `alias ${pack.slug}='python3 ${launcher} --config ${packDir}' # vibes:${pack.slug}`;
141
+ rcContent += `\n${alias}`;
142
+
143
+ info(`Installed ${c.bold}${pack.name}${c.reset} → type ${c.bold}${pack.slug}${c.reset} to launch`);
144
+ }
145
+
146
+ // Write verbs from last selected pack
147
+ const lastPack = selected[selected.length - 1];
148
+ if (existsSync(SETTINGS)) {
149
+ try {
150
+ const settings = JSON.parse(readFileSync(SETTINGS, "utf8"));
151
+ settings.spinnerVerbs = { mode: "replace", verbs: lastPack.verbs };
152
+ writeFileSync(SETTINGS, JSON.stringify(settings, null, 2) + "\n");
153
+ } catch {}
154
+ }
155
+
156
+ writeFileSync(shellRc, rcContent.replace(/\n{3,}/g, "\n\n") + "\n");
157
+
158
+ // ── Done ────────────────────────────────────────────────────────────
159
+ console.log();
160
+ console.log(`${c.green}${c.bold}Done!${c.reset} Open a new terminal tab, then:\n`);
161
+ for (const pack of selected) {
162
+ console.log(` ${c.bold}${pack.slug}${c.reset} launch with branded crab`);
163
+ }
164
+ console.log(
165
+ `\n All claude flags work: ${c.bold}--resume${c.reset}, ${c.bold}--dangerously-skip-permissions${c.reset}, etc.`
166
+ );
167
+ console.log(` Switch verbs: ${c.bold}yc --verbs looksmaxxing${c.reset}`);
168
+ console.log(`\n To uninstall: ${c.bold}bunx vibes uninstall${c.reset}\n`);
169
+ }
170
+
171
+ // ── Uninstall ───────────────────────────────────────────────────────────
172
+ function uninstall() {
173
+ const shellRc = getShellRc();
174
+ if (existsSync(shellRc)) {
175
+ const cleaned = readFileSync(shellRc, "utf8")
176
+ .split("\n")
177
+ .filter((line) => !line.includes("# vibes:"))
178
+ .join("\n");
179
+ writeFileSync(shellRc, cleaned.replace(/\n{3,}/g, "\n\n") + "\n");
180
+ info("Removed aliases from " + shellRc);
181
+ }
182
+
183
+ if (existsSync(SETTINGS)) {
184
+ try {
185
+ const settings = JSON.parse(readFileSync(SETTINGS, "utf8"));
186
+ delete settings.spinnerVerbs;
187
+ writeFileSync(SETTINGS, JSON.stringify(settings, null, 2) + "\n");
188
+ info("Removed spinner verbs");
189
+ } catch {}
190
+ }
191
+
192
+ console.log(`\n${c.green}${c.bold}Done!${c.reset} All vibescc aliases and verbs removed.\n`);
193
+ }
194
+
195
+ // ── Entry ───────────────────────────────────────────────────────────────
196
+ if (process.argv[2] === "uninstall") {
197
+ uninstall();
198
+ } else {
199
+ main().catch(console.error);
200
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "vibescc",
3
+ "version": "0.2.0",
4
+ "description": "Branded Claude Code — custom crab colors and spinner verbs",
5
+ "bin": {
6
+ "vibes": "./bin/vibes.mjs"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "packs/",
11
+ "scripts/"
12
+ ],
13
+ "keywords": [
14
+ "claude-code",
15
+ "branding",
16
+ "cli",
17
+ "crab",
18
+ "mascot",
19
+ "customization"
20
+ ],
21
+ "author": "djessicatony",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/djessicatony/vibescc"
26
+ }
27
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "Looksmaxxing",
3
+ "slug": "looksmaxxing",
4
+ "colors": {
5
+ "body": "#1A1A2E",
6
+ "eyes": "#E94560"
7
+ },
8
+ "verbs": [
9
+ "Looksmaxxing",
10
+ "Jestermaxxing",
11
+ "Bone smashing",
12
+ "Frauding",
13
+ "Coping",
14
+ "Larping",
15
+ "Ascending",
16
+ "Hardmaxxing",
17
+ "Softmaxxing",
18
+ "Claudemaxxing",
19
+ "Retatrutiding",
20
+ "GHK-Cu'ing",
21
+ "MT-2'ing",
22
+ "BPC-157'ing",
23
+ "MK-677'ing",
24
+ "Mogging",
25
+ "Frame mogging",
26
+ "Brutally mogging",
27
+ "Mogging to death",
28
+ "Injecting steroids",
29
+ "Injecting peptides",
30
+ "Balding",
31
+ "Height mogging",
32
+ "Black pilling",
33
+ "Mewing",
34
+ "Hard mewing",
35
+ "Chin tucking",
36
+ "Thumb pulling",
37
+ "Chewing",
38
+ "Minoxidiling",
39
+ "Taking accutane",
40
+ "Tanning",
41
+ "Leanmaxxing"
42
+ ]
43
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "Stripe",
3
+ "slug": "stripe",
4
+ "colors": {
5
+ "body": "#635BFF",
6
+ "eyes": "#FFFFFF"
7
+ },
8
+ "verbs": [
9
+ "Grabbing your laptop",
10
+ "Pasting seven lines",
11
+ "Making roads",
12
+ "Sending swag kits",
13
+ "Not scaling",
14
+ "Sweating the details",
15
+ "Growing the internet",
16
+ "Staying curious",
17
+ "Earning stripes",
18
+ "Moving money",
19
+ "Ordering pizza",
20
+ "Debating energetically",
21
+ "Dropping out",
22
+ "Calling a friend",
23
+ "Answering emails",
24
+ "Posting on HN",
25
+ "Not waiting",
26
+ "Building in-house",
27
+ "Pushing that button",
28
+ "Hiring friends"
29
+ ]
30
+ }
@@ -0,0 +1,34 @@
1
+ # Stripe Spinner Verbs — Sources
2
+
3
+ Every verb traces to a real Stripe / Collison brothers moment.
4
+
5
+ | Verb | Source |
6
+ |---|---|
7
+ | Grabbing your laptop | The "Collison Install" — PG's *Do Things That Don't Scale*: "Right then, give me your laptop" and set them up on the spot |
8
+ | Pasting seven lines | Stripe's origin: 7 lines of code any dev could paste to accept payments |
9
+ | Making roads | Patrick Collison on Dwarkesh Podcast: "We make roads" — infrastructure, not glamour |
10
+ | Sending swag kits | Early Stripe sent physical swag kits to every customer after their first transaction |
11
+ | Not scaling | Paul Graham's 2013 essay *Do Things That Don't Scale* — Stripe is the central example |
12
+ | Sweating the details | Stripe culture: "if we make just one mistake, somebody's paycheck is wrong" |
13
+ | Growing the internet | Stripe's mission: "increase the GDP of the internet" |
14
+ | Staying curious | Stripe operating principle — "always seeking to learn" |
15
+ | Earning stripes | Wordplay on the company name |
16
+ | Moving money | What Stripe does, in two words |
17
+ | Ordering pizza | Free pizza and drinks at Stripe's monthly Capture The Flag hacking competitions |
18
+ | Debating energetically | Stripe operating principle: "question assumptions, debate energetically, abandon ideas" |
19
+ | Dropping out | Patrick left MIT his freshman year to build (after selling Auctomatic for $5M at 19) |
20
+ | Calling a friend | Early hack: when users signed up, Patrick called a friend at a payments gateway to manually open merchant accounts |
21
+ | Answering emails | Stripe engineers personally answered support emails within minutes in the early days |
22
+ | Posting on HN | Garry Tan posted on Hacker News: "need payments processing? email us" — got 300-550 waitlist signups |
23
+ | Not waiting | PG on the Collisons: "they weren't going to wait" — contrasted with diffident founders who'd say "we'll send you a link" |
24
+ | Building in-house | After realizing they couldn't control the UX through partnerships, they brought the entire payment pipeline in-house |
25
+ | Pushing that button | Patrick on Dwarkesh: "push the hell out of that button" — describing how eagerly they'd adopt better solutions |
26
+ | Hiring friends | 8 of Stripe's first 10 employees came from founder networks; they only hired people they'd want to hang out with on a Saturday |
27
+
28
+ ## Key Sources
29
+
30
+ - Paul Graham, [Do Things That Don't Scale](https://paulgraham.com/ds.html) (2013)
31
+ - Ali Abouelatta, [Stripe — First 1000](https://read.first1000.co/p/-stripe-6bb)
32
+ - Startup Grind, [The Collison Brothers: The Story Behind The Founding Of Stripe](https://medium.com/startup-grind/the-collison-brothers-the-story-behind-the-founding-of-stripe-ae013434c080)
33
+ - Dwarkesh Patel, [Patrick Collison — Craft, Beauty, & The Future of Payments](https://www.dwarkesh.com/p/patrick-collison)
34
+ - Stripe, [Operating Principles](https://stripe.com/jobs/culture)
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "Supabase",
3
+ "slug": "supabase",
4
+ "colors": {
5
+ "body": "#3ECF8E",
6
+ "eyes": "#FAFAFA"
7
+ },
8
+ "verbs": [
9
+ "Querying Postgres",
10
+ "Applying migrations",
11
+ "Syncing realtime",
12
+ "Authenticating users",
13
+ "Signing JWTs",
14
+ "Storing objects",
15
+ "Triggering edge functions",
16
+ "Seeding databases",
17
+ "Row-level securing",
18
+ "Broadcasting channels",
19
+ "Generating types",
20
+ "Branching databases",
21
+ "Vectorizing embeddings",
22
+ "Indexing full-text",
23
+ "Pooling connections",
24
+ "Replicating data",
25
+ "Backing up tables",
26
+ "Publishing APIs",
27
+ "Self-hosting",
28
+ "Open sourcing"
29
+ ]
30
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "Vercel",
3
+ "slug": "vercel",
4
+ "colors": {
5
+ "body": "#000000",
6
+ "eyes": "#FFFFFF"
7
+ },
8
+ "verbs": [
9
+ "Deploying to edge",
10
+ "Building previews",
11
+ "Generating static",
12
+ "Server rendering",
13
+ "Incrementally regenerating",
14
+ "Streaming responses",
15
+ "Caching at the edge",
16
+ "Revalidating paths",
17
+ "Splitting bundles",
18
+ "Hydrating components",
19
+ "Prefetching routes",
20
+ "Optimizing images",
21
+ "Compiling middleware",
22
+ "Shipping to production",
23
+ "Rolling back deploys",
24
+ "Running turborepo",
25
+ "Connecting domains",
26
+ "Measuring web vitals",
27
+ "Tree shaking",
28
+ "Zero-config deploying"
29
+ ]
30
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "Y Combinator",
3
+ "slug": "yc",
4
+ "colors": {
5
+ "body": "#FB651E",
6
+ "eyes": "#FFFFFF"
7
+ },
8
+ "verbs": [
9
+ "Making something people want",
10
+ "Doing things that don't scale",
11
+ "Talking to users",
12
+ "G-stacking",
13
+ "Writing code",
14
+ "Launching now",
15
+ "Shipping fast",
16
+ "Pivoting",
17
+ "Iterating",
18
+ "Finding product-market fit",
19
+ "Ramen profiting",
20
+ "Being relentlessly resourceful",
21
+ "Going default alive",
22
+ "Not dying",
23
+ "Being a cockroach",
24
+ "Surviving the Trough of Sorrow",
25
+ "Overcoming schlep blindness",
26
+ "Avoiding tarpit ideas",
27
+ "Finding the 90/10 solution",
28
+ "Measuring weekly growth",
29
+ "Living in the future",
30
+ "Keeping identity small",
31
+ "Ignoring competitors",
32
+ "Staying frighteningly ambitious",
33
+ "Demo day prepping",
34
+ "Office houring",
35
+ "Reading PG essays",
36
+ "Launching on HN"
37
+ ]
38
+ }
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ banner.py — Render brand names as gradient block-letter art.
4
+
5
+ Usage:
6
+ python3 banner.py vibes # default gradient
7
+ python3 banner.py YC --from FB651E --to FFAA44
8
+ python3 banner.py STRIPE --from 635BFF --to 8B7FFF
9
+ """
10
+
11
+ import sys
12
+
13
+ # ── Block letter font (6 rows tall) ────────────────────────────────────
14
+ FONT = {
15
+ "A": [
16
+ " █████╗ ",
17
+ "██╔══██╗",
18
+ "███████║",
19
+ "██╔══██║",
20
+ "██║ ██║",
21
+ "╚═╝ ╚═╝",
22
+ ],
23
+ "B": [
24
+ "██████╗ ",
25
+ "██╔══██╗",
26
+ "██████╔╝",
27
+ "██╔══██╗",
28
+ "██████╔╝",
29
+ "╚═════╝ ",
30
+ ],
31
+ "C": [
32
+ " ██████╗",
33
+ "██╔════╝",
34
+ "██║ ",
35
+ "██║ ",
36
+ "╚██████╗",
37
+ " ╚═════╝",
38
+ ],
39
+ "D": [
40
+ "██████╗ ",
41
+ "██╔══██╗",
42
+ "██║ ██║",
43
+ "██║ ██║",
44
+ "██████╔╝",
45
+ "╚═════╝ ",
46
+ ],
47
+ "E": [
48
+ "███████╗",
49
+ "██╔════╝",
50
+ "█████╗ ",
51
+ "██╔══╝ ",
52
+ "███████╗",
53
+ "╚══════╝",
54
+ ],
55
+ "F": [
56
+ "███████╗",
57
+ "██╔════╝",
58
+ "█████╗ ",
59
+ "██╔══╝ ",
60
+ "██║ ",
61
+ "╚═╝ ",
62
+ ],
63
+ "G": [
64
+ " ██████╗ ",
65
+ "██╔════╝ ",
66
+ "██║ ███╗",
67
+ "██║ ██║",
68
+ "╚██████╔╝",
69
+ " ╚═════╝ ",
70
+ ],
71
+ "H": [
72
+ "██╗ ██╗",
73
+ "██║ ██║",
74
+ "███████║",
75
+ "██╔══██║",
76
+ "██║ ██║",
77
+ "╚═╝ ╚═╝",
78
+ ],
79
+ "I": [
80
+ "██╗",
81
+ "██║",
82
+ "██║",
83
+ "██║",
84
+ "██║",
85
+ "╚═╝",
86
+ ],
87
+ "K": [
88
+ "██╗ ██╗",
89
+ "██║ ██╔╝",
90
+ "█████╔╝ ",
91
+ "██╔═██╗ ",
92
+ "██║ ██╗",
93
+ "╚═╝ ╚═╝",
94
+ ],
95
+ "L": [
96
+ "██╗ ",
97
+ "██║ ",
98
+ "██║ ",
99
+ "██║ ",
100
+ "███████╗",
101
+ "╚══════╝",
102
+ ],
103
+ "M": [
104
+ "███╗ ███╗",
105
+ "████╗ ████║",
106
+ "██╔████╔██║",
107
+ "██║╚██╔╝██║",
108
+ "██║ ╚═╝ ██║",
109
+ "╚═╝ ╚═╝",
110
+ ],
111
+ "N": [
112
+ "███╗ ██╗",
113
+ "████╗ ██║",
114
+ "██╔██╗ ██║",
115
+ "██║╚██╗██║",
116
+ "██║ ╚████║",
117
+ "╚═╝ ╚═══╝",
118
+ ],
119
+ "O": [
120
+ " ██████╗ ",
121
+ "██╔═══██╗",
122
+ "██║ ██║",
123
+ "██║ ██║",
124
+ "╚██████╔╝",
125
+ " ╚═════╝ ",
126
+ ],
127
+ "P": [
128
+ "██████╗ ",
129
+ "██╔══██╗",
130
+ "██████╔╝",
131
+ "██╔═══╝ ",
132
+ "██║ ",
133
+ "╚═╝ ",
134
+ ],
135
+ "R": [
136
+ "██████╗ ",
137
+ "██╔══██╗",
138
+ "██████╔╝",
139
+ "██╔══██╗",
140
+ "██║ ██║",
141
+ "╚═╝ ╚═╝",
142
+ ],
143
+ "S": [
144
+ "███████╗",
145
+ "██╔════╝",
146
+ "███████╗",
147
+ "╚════██║",
148
+ "███████║",
149
+ "╚══════╝",
150
+ ],
151
+ "T": [
152
+ "████████╗",
153
+ "╚══██╔══╝",
154
+ " ██║ ",
155
+ " ██║ ",
156
+ " ██║ ",
157
+ " ╚═╝ ",
158
+ ],
159
+ "U": [
160
+ "██╗ ██╗",
161
+ "██║ ██║",
162
+ "██║ ██║",
163
+ "██║ ██║",
164
+ "╚██████╔╝",
165
+ " ╚═════╝ ",
166
+ ],
167
+ "V": [
168
+ "██╗ ██╗",
169
+ "██║ ██║",
170
+ "██║ ██║",
171
+ "╚██╗ ██╔╝",
172
+ " ╚████╔╝ ",
173
+ " ╚═══╝ ",
174
+ ],
175
+ "W": [
176
+ "██╗ ██╗",
177
+ "██║ ██║",
178
+ "██║ █╗ ██║",
179
+ "██║███╗██║",
180
+ "╚███╔███╔╝",
181
+ " ╚══╝╚══╝ ",
182
+ ],
183
+ "X": [
184
+ "██╗ ██╗",
185
+ "╚██╗██╔╝",
186
+ " ╚███╔╝ ",
187
+ " ██╔██╗ ",
188
+ "██╔╝ ██╗",
189
+ "╚═╝ ╚═╝",
190
+ ],
191
+ "Y": [
192
+ "██╗ ██╗",
193
+ "╚██╗ ██╔╝",
194
+ " ╚████╔╝ ",
195
+ " ╚██╔╝ ",
196
+ " ██║ ",
197
+ " ╚═╝ ",
198
+ ],
199
+ "Z": [
200
+ "███████╗",
201
+ "╚══███╔╝",
202
+ " ███╔╝ ",
203
+ " ███╔╝ ",
204
+ "███████╗",
205
+ "╚══════╝",
206
+ ],
207
+ " ": [
208
+ " ",
209
+ " ",
210
+ " ",
211
+ " ",
212
+ " ",
213
+ " ",
214
+ ],
215
+ }
216
+
217
+
218
+ def hex_to_rgb(h):
219
+ h = h.lstrip("#")
220
+ return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
221
+
222
+
223
+ def lerp_color(c1, c2, t):
224
+ return tuple(int(a + (b - a) * t) for a, b in zip(c1, c2))
225
+
226
+
227
+ def colorize_char(char, r, g, b):
228
+ if char in (" ", ""):
229
+ return char
230
+ return f"\033[38;2;{r};{g};{b}m{char}\033[0m"
231
+
232
+
233
+ def render_banner(text, color_from, color_to, indent=4):
234
+ text = text.upper()
235
+ rows = [[] for _ in range(6)]
236
+
237
+ for ch in text:
238
+ glyph = FONT.get(ch, FONT[" "])
239
+ for i, line in enumerate(glyph):
240
+ rows[i].append(line)
241
+
242
+ # Join rows and apply horizontal gradient
243
+ output = []
244
+ for row_parts in rows:
245
+ full_line = "".join(row_parts)
246
+ total = max(len(full_line), 1)
247
+ colored = ""
248
+ pos = 0
249
+ for char in full_line:
250
+ t = pos / total
251
+ r, g, b = lerp_color(color_from, color_to, t)
252
+ colored += colorize_char(char, r, g, b)
253
+ pos += 1
254
+ output.append(" " * indent + colored)
255
+
256
+ return "\n".join(output)
257
+
258
+
259
+ # ── Preset gradients per brand ──────────────────────────────────────────
260
+ PRESETS = {
261
+ "vibes": {"text": "VIBES", "from": "FF6B6B", "to": "C084FC"},
262
+ "yc": {"text": "YC", "from": "FB651E", "to": "FFAA44"},
263
+ "stripe": {"text": "STRIPE", "from": "635BFF", "to": "A89BFF"},
264
+ "vercel": {"text": "VERCEL", "from": "888888", "to": "FFFFFF"},
265
+ "supabase": {"text": "SUPABASE", "from": "249361", "to": "3ECF8E"},
266
+ }
267
+
268
+
269
+ def main():
270
+ import argparse
271
+
272
+ parser = argparse.ArgumentParser()
273
+ parser.add_argument("name", help="Text to render (or preset name)")
274
+ parser.add_argument("--from", dest="color_from", default=None)
275
+ parser.add_argument("--to", dest="color_to", default=None)
276
+ parser.add_argument("--indent", type=int, default=4)
277
+ args = parser.parse_args()
278
+
279
+ name = args.name.lower()
280
+
281
+ if name in PRESETS and not args.color_from:
282
+ preset = PRESETS[name]
283
+ text = preset["text"]
284
+ c_from = hex_to_rgb(preset["from"])
285
+ c_to = hex_to_rgb(preset["to"])
286
+ else:
287
+ text = args.name.upper()
288
+ c_from = hex_to_rgb(args.color_from or "FFFFFF")
289
+ c_to = hex_to_rgb(args.color_to or args.color_from or "FFFFFF")
290
+
291
+ print()
292
+ print(render_banner(text, c_from, c_to, args.indent))
293
+ print()
294
+
295
+
296
+ if __name__ == "__main__":
297
+ main()
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ vibescc-launch.py — PTY wrapper that recolors the Claude Code crab.
4
+
5
+ Spawns `claude` inside a pseudoterminal, intercepts the ANSI output stream,
6
+ and replaces the crab's color codes with company branding colors.
7
+
8
+ The crab itself is pixel-perfect — same characters, same poses, same animation.
9
+ Only the paint changes.
10
+
11
+ Usage:
12
+ python3 vibescc-launch.py # default YC orange
13
+ python3 vibescc-launch.py --body 255,102,0 # custom body RGB
14
+ python3 vibescc-launch.py --config packs/yc # load from pack.json
15
+ python3 vibescc-launch.py -- --resume # pass flags to claude
16
+ """
17
+
18
+ import argparse
19
+ import json
20
+ import os
21
+ import pty
22
+ import re
23
+ import signal
24
+ import struct
25
+ import sys
26
+ import fcntl
27
+ import termios
28
+
29
+ # ── Original Clawd colors (consistent across CC versions) ──────────────
30
+ # Match both semicolon (38;2;R;G;B) and colon (38:2:R:G:B) formats.
31
+ # Ink/chalk may use either depending on version.
32
+ ORIGINALS = [
33
+ # (pattern, is_shimmer, is_background)
34
+ # Theme A — claude/clawd_body: rgb(215,119,87)
35
+ (b"38;2;215;119;87", False, False),
36
+ (b"38:2:215:119:87", False, False),
37
+ (b"48;2;215;119;87", False, True),
38
+ (b"48:2:215:119:87", False, True),
39
+ # Theme A — claudeShimmer: rgb(245,149,117)
40
+ (b"38;2;245;149;117", True, False),
41
+ (b"38:2:245:149:117", True, False),
42
+ (b"48;2;245;149;117", True, True),
43
+ (b"48:2:245:149:117", True, True),
44
+ # Theme B — claude: rgb(255,153,51)
45
+ (b"38;2;255;153;51", False, False),
46
+ (b"38:2:255:153:51", False, False),
47
+ (b"48;2;255;153;51", False, True),
48
+ (b"48:2:255:153:51", False, True),
49
+ # Theme B — claudeShimmer: rgb(255,183,101)
50
+ (b"38;2;255;183;101", True, False),
51
+ (b"38:2:255:183:101", True, False),
52
+ (b"48;2;255;183;101", True, True),
53
+ (b"48:2:255:183:101", True, True),
54
+ # Theme C — claudeShimmer: rgb(235,159,127)
55
+ (b"38;2;235;159;127", True, False),
56
+ (b"38:2:235:159:127", True, False),
57
+ (b"48;2;235;159;127", True, True),
58
+ (b"48:2:235:159:127", True, True),
59
+ ]
60
+ ORIGINAL_BG_SEMI = b"48;2;0;0;0"
61
+ ORIGINAL_BG_COLON = b"48:2:0:0:0"
62
+
63
+
64
+ def make_replacement(r, g, b):
65
+ """Build the replacement byte string for a color."""
66
+ return f"{r};{g};{b}".encode()
67
+
68
+
69
+ def make_shimmer(r, g, b):
70
+ """Generate a lighter shimmer variant of a color (same offset as claude→claudeShimmer)."""
71
+ return (min(r + 30, 255), min(g + 30, 255), min(b + 30, 255))
72
+
73
+
74
+ def build_filter(body_rgb, bg_rgb=None):
75
+ """Return a stateful function that replaces clawd colors in an ANSI byte stream.
76
+
77
+ Keeps a small overlap buffer between read() calls to catch color codes
78
+ that get split across chunk boundaries.
79
+ """
80
+ shimmer_rgb = make_shimmer(*body_rgb)
81
+
82
+ def rgb_semi(prefix, r, g, b):
83
+ return f"{prefix};2;{r};{g};{b}".encode()
84
+
85
+ def rgb_colon(prefix, r, g, b):
86
+ return f"{prefix}:2:{r}:{g}:{b}".encode()
87
+
88
+ replacements = []
89
+ for orig, is_shimmer, is_bg in ORIGINALS:
90
+ target_rgb = shimmer_rgb if is_shimmer else body_rgb
91
+ sep = ":" if b":" in orig else ";"
92
+ prefix = "48" if is_bg else "38"
93
+ new = f"{prefix}{sep}2{sep}{target_rgb[0]}{sep}{target_rgb[1]}{sep}{target_rgb[2]}".encode()
94
+ replacements.append((orig, new))
95
+
96
+ if bg_rgb:
97
+ new_bg_semi = rgb_semi("48", *bg_rgb)
98
+ new_bg_colon = rgb_colon("48", *bg_rgb)
99
+ replacements.append((ORIGINAL_BG_SEMI, new_bg_semi))
100
+ replacements.append((ORIGINAL_BG_COLON, new_bg_colon))
101
+
102
+ # Regex to strip terminal identification responses
103
+ strip_terminal_responses = re.compile(
104
+ rb"\x1bP[^\x1b]*\x1b\\|\x1b\[\?[0-9;]*c"
105
+ )
106
+
107
+ # Smart holdback: only keep tail bytes if they could be a partial
108
+ # match of a color pattern (contains digits/semicolons after ESC[).
109
+ # This avoids the latency of always holding back bytes.
110
+ max_pat = max(len(old) for old, _ in replacements)
111
+ partial_re = re.compile(rb"[34]8[;:]2[;:][\d;:]{0," + str(max_pat).encode() + rb"}$")
112
+ leftover = b""
113
+
114
+ def apply(data):
115
+ nonlocal leftover
116
+ buf = leftover + data
117
+
118
+ # Strip terminal ID sequences
119
+ buf = strip_terminal_responses.sub(b"", buf)
120
+
121
+ # Do replacements
122
+ for old, new in replacements:
123
+ buf = buf.replace(old, new)
124
+
125
+ # Smart holdback: only if tail looks like a partial color code
126
+ hold = 0
127
+ if len(buf) > max_pat:
128
+ tail = buf[-(max_pat):]
129
+ m = partial_re.search(tail)
130
+ if m:
131
+ hold = len(tail) - m.start()
132
+
133
+ if hold > 0:
134
+ leftover = buf[-hold:]
135
+ return buf[:-hold]
136
+ else:
137
+ leftover = b""
138
+ return buf
139
+
140
+ return apply
141
+
142
+
143
+ def parse_rgb(s):
144
+ """Parse '255,102,0' or '#FF6600' into (r, g, b) tuple."""
145
+ s = s.strip()
146
+ if s.startswith("#"):
147
+ h = s.lstrip("#")
148
+ return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
149
+ parts = s.split(",")
150
+ return (int(parts[0]), int(parts[1]), int(parts[2]))
151
+
152
+
153
+ def load_pack_colors(pack_dir):
154
+ """Load body and eyes colors from a vibescc pack.json."""
155
+ pack_json = os.path.join(pack_dir, "pack.json")
156
+ with open(pack_json) as f:
157
+ pack = json.load(f)
158
+ body = parse_rgb(pack["colors"]["body"])
159
+ eyes = parse_rgb(pack["colors"]["eyes"])
160
+ return body, eyes
161
+
162
+
163
+ def resolve_pack_dir(name):
164
+ """Resolve a pack name to its directory. Accepts a path or just a slug."""
165
+ if os.path.isdir(name):
166
+ return name
167
+ # Try relative to script's packs/ directory
168
+ script_dir = os.path.dirname(os.path.abspath(__file__))
169
+ packs_dir = os.path.join(script_dir, "..", "packs")
170
+ candidate = os.path.join(packs_dir, name)
171
+ if os.path.isdir(candidate):
172
+ return candidate
173
+ return None
174
+
175
+
176
+ def apply_verbs(pack_dir):
177
+ """Write a pack's verbs to ~/.claude/settings.json."""
178
+ pack_json = os.path.join(pack_dir, "pack.json")
179
+ with open(pack_json) as f:
180
+ pack = json.load(f)
181
+ verbs = pack.get("verbs", [])
182
+ if not verbs:
183
+ return
184
+
185
+ settings_path = os.path.expanduser("~/.claude/settings.json")
186
+ try:
187
+ with open(settings_path) as f:
188
+ settings = json.load(f)
189
+ except (FileNotFoundError, json.JSONDecodeError):
190
+ settings = {}
191
+
192
+ settings["spinnerVerbs"] = {"mode": "replace", "verbs": verbs}
193
+
194
+ with open(settings_path, "w") as f:
195
+ json.dump(settings, f, indent=2)
196
+ f.write("\n")
197
+
198
+
199
+ def sync_window_size(master_fd):
200
+ """Copy the real terminal's window size to the PTY master."""
201
+ try:
202
+ size = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, b"\x00" * 8)
203
+ fcntl.ioctl(master_fd, termios.TIOCSWINSZ, size)
204
+ except OSError:
205
+ pass
206
+
207
+
208
+ def main():
209
+ parser = argparse.ArgumentParser(description="Launch Claude Code with custom crab colors")
210
+ parser.add_argument(
211
+ "--body",
212
+ default=None,
213
+ help="Body color as R,G,B or #hex (default: YC orange #FF6600)",
214
+ )
215
+ parser.add_argument(
216
+ "--eyes",
217
+ default=None,
218
+ help="Eye color as R,G,B or #hex (default: unchanged)",
219
+ )
220
+ parser.add_argument(
221
+ "--config",
222
+ default=None,
223
+ help="Path to pack directory (reads colors from pack.json)",
224
+ )
225
+ parser.add_argument(
226
+ "--verbs",
227
+ default=None,
228
+ help="Pack name or path to load verbs from (e.g. looksmaxxing, stripe)",
229
+ )
230
+ args, claude_args = parser.parse_known_args()
231
+
232
+ # Apply verbs from a different pack if requested
233
+ if args.verbs:
234
+ verbs_dir = resolve_pack_dir(args.verbs)
235
+ if verbs_dir:
236
+ apply_verbs(verbs_dir)
237
+ else:
238
+ print(f"Warning: verb pack '{args.verbs}' not found, skipping", file=sys.stderr)
239
+
240
+ # Resolve colors
241
+ if args.config:
242
+ body_rgb, eyes_rgb = load_pack_colors(args.config)
243
+ else:
244
+ body_rgb = parse_rgb(args.body) if args.body else (255, 102, 0)
245
+ eyes_rgb = parse_rgb(args.eyes) if args.eyes else None
246
+
247
+ color_filter = build_filter(body_rgb, eyes_rgb)
248
+
249
+ # Build claude command — everything we didn't recognize goes to claude
250
+ claude_cmd = ["claude"] + claude_args
251
+
252
+ # ── PTY spawn with output filtering ────────────────────────────────
253
+ # pty.fork() handles setsid/TIOCSCTTY/dup2 correctly on macOS.
254
+ # We set window size immediately after fork, then SIGWINCH the child
255
+ # so claude/Ink re-reads the correct terminal dimensions.
256
+
257
+ pid, master_fd = pty.fork()
258
+
259
+ if pid == 0:
260
+ # Child: exec claude
261
+ os.environ.setdefault("TERM", "xterm-256color")
262
+ os.execvp(claude_cmd[0], claude_cmd)
263
+ sys.exit(1)
264
+
265
+ # Parent: set correct PTY size and tell child to re-read it
266
+ sync_window_size(master_fd)
267
+ import time
268
+ time.sleep(0.05) # let child start before SIGWINCH
269
+ try:
270
+ os.kill(pid, signal.SIGWINCH)
271
+ except OSError:
272
+ pass
273
+
274
+ # Handle SIGWINCH (terminal resize) — propagate to PTY
275
+ def handle_winch(signum, frame):
276
+ sync_window_size(master_fd)
277
+ os.kill(pid, signal.SIGWINCH)
278
+
279
+ signal.signal(signal.SIGWINCH, handle_winch)
280
+
281
+ # Put stdin in raw mode so keypresses pass through immediately
282
+ old_tty = termios.tcgetattr(sys.stdin)
283
+ try:
284
+ import tty
285
+
286
+ tty.setraw(sys.stdin)
287
+
288
+ # I/O relay loop
289
+ import select
290
+
291
+ while True:
292
+ try:
293
+ rlist, _, _ = select.select([sys.stdin, master_fd], [], [], 0.1)
294
+ except (select.error, ValueError):
295
+ break
296
+
297
+ if master_fd in rlist:
298
+ try:
299
+ data = os.read(master_fd, 65536)
300
+ except OSError:
301
+ break
302
+ if not data:
303
+ break
304
+ # Coalesce: grab any immediately available data to avoid
305
+ # color codes splitting across chunk boundaries
306
+ while True:
307
+ ready, _, _ = select.select([master_fd], [], [], 0)
308
+ if not ready:
309
+ break
310
+ try:
311
+ more = os.read(master_fd, 65536)
312
+ except OSError:
313
+ break
314
+ if not more:
315
+ break
316
+ data += more
317
+ # Apply color filter
318
+ filtered = color_filter(data)
319
+ if filtered:
320
+ os.write(sys.stdout.fileno(), filtered)
321
+
322
+ if sys.stdin in rlist:
323
+ try:
324
+ data = os.read(sys.stdin.fileno(), 4096)
325
+ except OSError:
326
+ break
327
+ if not data:
328
+ break
329
+ # Strip terminal ID responses from stdin before they
330
+ # enter the PTY and get echoed back to the screen
331
+ data = re.sub(
332
+ rb"\x1bP[^\x1b]*\x1b\\|\x1b\[\?[0-9;]*c",
333
+ b"",
334
+ data,
335
+ )
336
+ if data:
337
+ os.write(master_fd, data)
338
+
339
+
340
+
341
+ finally:
342
+ # Restore terminal
343
+ termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
344
+
345
+ # Wait for child and exit with its status
346
+ _, status = os.waitpid(pid, 0)
347
+ sys.exit(os.WEXITSTATUS(status) if os.WIFEXITED(status) else 1)
348
+
349
+
350
+ if __name__ == "__main__":
351
+ main()
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ # Launch Claude Code with YC-branded crab
3
+ # Usage: yc-claude [claude args...]
4
+ DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ exec python3 "$DIR/vibescc-launch.py" --config "$DIR/../packs/yc" -- "$@"