claude-blip 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 davebream
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # claude-blip
2
+
3
+ A single-file statusline for Claude Code.
4
+
5
+ Zero dependencies. Just a blip.
6
+
7
+ ```
8
+ Fixing auth bug │ myapp │ main │ opus │ ━━━━━━──── 120K
9
+ ```
10
+
11
+ ## What you get
12
+
13
+ ```
14
+ task │ project │ branch │ model │ context
15
+ ```
16
+
17
+ - **Task** — what Claude is doing right now (bold, from todo list)
18
+ - **Project** — directory name
19
+ - **Branch** — current git branch
20
+ - **Model** — opus, sonnet, haiku
21
+ - **Context** — usage bar that goes from dim → yellow → red as you fill up
22
+
23
+ Segments drop off the left when your terminal is narrow. Context bar always stays.
24
+
25
+ ## Install
26
+
27
+ ```sh
28
+ npx claude-blip
29
+ ```
30
+
31
+ That's it. Restart Claude Code.
32
+
33
+ ### Scopes
34
+
35
+ ```sh
36
+ npx claude-blip # global (recommended)
37
+ npx claude-blip --project # .claude/settings.json (shareable)
38
+ npx claude-blip --local # .claude/settings.local.json (gitignored)
39
+ ```
40
+
41
+ ### Uninstall
42
+
43
+ ```sh
44
+ npx claude-blip --uninstall
45
+ ```
46
+
47
+ ## Examples
48
+
49
+ Normal usage — everything is dim except the task:
50
+
51
+ ```
52
+ Fixing auth bug │ myapp │ main │ opus │ ━━──────── 40K
53
+ ```
54
+
55
+ Getting warm (yellow at 70%):
56
+
57
+ ```
58
+ myapp │ main │ opus │ ━━━━━━━─── 140K
59
+ ```
60
+
61
+ Running hot (red at 90%):
62
+
63
+ ```
64
+ myapp │ main │ opus │ ━━━━━━━━━─ 180K
65
+ ```
66
+
67
+ No task, no git — it adapts:
68
+
69
+ ```
70
+ scripts │ sonnet │ ━━━━────── 80K
71
+ ```
72
+
73
+ ## How it works
74
+
75
+ One file. 150 lines. Node.js only (ships with Claude Code).
76
+
77
+ Claude Code pipes session JSON to your statusline script via stdin. This script reads it, picks out the useful bits, formats them, and writes one line to stdout.
78
+
79
+ The context bar scales to 80% — that's roughly where Claude starts compressing context, so 100% on the bar means "you're about to lose history."
80
+
81
+ ## Debug
82
+
83
+ Set `debug: true` in the CONFIG object at the top of `statusline.js` to dump the full JSON payload to stderr.
84
+
85
+ ## License
86
+
87
+ MIT
package/bin/setup.js ADDED
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ // claude-blip setup — one command, done.
3
+ //
4
+ // Usage:
5
+ // npx claude-blip (global — recommended)
6
+ // npx claude-blip --project (this project, shareable)
7
+ // npx claude-blip --local (this project, gitignored)
8
+ // npx claude-blip --uninstall (remove from all scopes)
9
+
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const os = require("os");
13
+
14
+ const HOOK_SOURCE = path.resolve(__dirname, "..", "statusline.js");
15
+
16
+ const SCOPES = {
17
+ global: () => path.join(os.homedir(), ".claude", "settings.json"),
18
+ project: () => path.join(process.cwd(), ".claude", "settings.json"),
19
+ local: () => path.join(process.cwd(), ".claude", "settings.local.json"),
20
+ };
21
+
22
+ const args = process.argv.slice(2);
23
+ const uninstall = args.includes("--uninstall");
24
+ const scope = args.includes("--project")
25
+ ? "project"
26
+ : args.includes("--local")
27
+ ? "local"
28
+ : "global";
29
+
30
+ const DIM = "\x1b[2m";
31
+ const BOLD = "\x1b[1m";
32
+ const GREEN = "\x1b[32m";
33
+ const RED = "\x1b[31m";
34
+ const RESET = "\x1b[0m";
35
+
36
+ function log(msg) {
37
+ console.log(` ${msg}`);
38
+ }
39
+
40
+ function getInstallDir() {
41
+ if (scope === "global") {
42
+ return path.join(os.homedir(), ".claude", "hooks");
43
+ }
44
+ return path.join(process.cwd(), ".claude", "hooks");
45
+ }
46
+
47
+ function install() {
48
+ const installDir = getInstallDir();
49
+ const dest = path.join(installDir, "statusline.js");
50
+ const settingsPath = SCOPES[scope]();
51
+ const settingsDir = path.dirname(settingsPath);
52
+
53
+ // 1. Copy the statusline script
54
+ fs.mkdirSync(installDir, { recursive: true });
55
+ fs.copyFileSync(HOOK_SOURCE, dest);
56
+ fs.chmodSync(dest, 0o755);
57
+
58
+ // 2. Update settings.json
59
+ fs.mkdirSync(settingsDir, { recursive: true });
60
+ let settings = {};
61
+ if (fs.existsSync(settingsPath)) {
62
+ try {
63
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
64
+ } catch {
65
+ // Corrupted settings — start fresh
66
+ }
67
+ }
68
+
69
+ settings.statusLine = {
70
+ type: "command",
71
+ command: dest,
72
+ };
73
+
74
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
75
+
76
+ console.log();
77
+ log(`${GREEN}${BOLD}blip${RESET} ${DIM}installed${RESET}`);
78
+ log(`${DIM}scope: ${RESET}${scope}`);
79
+ log(`${DIM}hook: ${RESET}${dest}`);
80
+ log(`${DIM}config: ${RESET}${settingsPath}`);
81
+ console.log();
82
+ log(`${DIM}Restart Claude Code to see it.${RESET}`);
83
+ console.log();
84
+ }
85
+
86
+ function removeStatusLine(settingsPath) {
87
+ if (!fs.existsSync(settingsPath)) return false;
88
+ try {
89
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
90
+ if (!settings.statusLine) return false;
91
+ delete settings.statusLine;
92
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
93
+ return true;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ function uninstallAll() {
100
+ let removed = false;
101
+
102
+ // Remove from all settings scopes
103
+ for (const [name, getPath] of Object.entries(SCOPES)) {
104
+ if (removeStatusLine(getPath())) {
105
+ log(`${DIM}Removed statusLine from ${name} settings${RESET}`);
106
+ removed = true;
107
+ }
108
+ }
109
+
110
+ // Remove hook files
111
+ const locations = [
112
+ path.join(os.homedir(), ".claude", "hooks", "statusline.js"),
113
+ path.join(process.cwd(), ".claude", "hooks", "statusline.js"),
114
+ ];
115
+ for (const loc of locations) {
116
+ if (fs.existsSync(loc)) {
117
+ fs.unlinkSync(loc);
118
+ log(`${DIM}Removed ${loc}${RESET}`);
119
+ removed = true;
120
+ }
121
+ }
122
+
123
+ console.log();
124
+ if (removed) {
125
+ log(`${GREEN}${BOLD}blip${RESET} ${DIM}uninstalled${RESET}`);
126
+ } else {
127
+ log(`${DIM}Nothing to remove — blip wasn't installed.${RESET}`);
128
+ }
129
+ console.log();
130
+ }
131
+
132
+ // ─────────────────────────────────────────────────────────────
133
+
134
+ if (uninstall) {
135
+ uninstallAll();
136
+ } else {
137
+ install();
138
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "claude-blip",
3
+ "version": "1.0.0",
4
+ "description": "A single-file statusline for Claude Code. Zero dependencies. Just a blip.",
5
+ "license": "MIT",
6
+ "author": "davebream",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/davebream/claude-blip.git"
10
+ },
11
+ "keywords": [
12
+ "claude",
13
+ "claude-code",
14
+ "statusline",
15
+ "status-bar",
16
+ "cli",
17
+ "developer-tools"
18
+ ],
19
+ "bin": {
20
+ "claude-blip": "bin/setup.js"
21
+ },
22
+ "files": [
23
+ "statusline.js",
24
+ "bin/setup.js"
25
+ ]
26
+ }
package/statusline.js ADDED
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ // claude-blip — a single-file statusline for Claude Code
3
+ // Shows: task │ project │ branch │ model │ context usage
4
+
5
+ const { execSync } = require("child_process");
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+ const os = require("os");
9
+
10
+ // ─────────────────────────────────────────────────────────────
11
+ // Config
12
+ // ─────────────────────────────────────────────────────────────
13
+ const CONFIG = {
14
+ warnThreshold: 0.7,
15
+ criticalThreshold: 0.9,
16
+ barWidth: 10,
17
+ // Debug: set to true to log full input data to stderr
18
+ debug: false,
19
+ };
20
+
21
+ // ─────────────────────────────────────────────────────────────
22
+ // ANSI
23
+ // ─────────────────────────────────────────────────────────────
24
+ const ANSI = {
25
+ dim: "\x1b[2m",
26
+ bold: "\x1b[1m",
27
+ reset: "\x1b[0m",
28
+ yellow: "\x1b[38;5;222m",
29
+ red: "\x1b[38;5;167m",
30
+ };
31
+
32
+ const dim = (s) => `${ANSI.dim}${s}${ANSI.reset}`;
33
+ const bold = (s) => `${ANSI.bold}${s}${ANSI.reset}`;
34
+ const color = (s, c) => `${c}${s}${ANSI.reset}`;
35
+ const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
36
+
37
+ // ─────────────────────────────────────────────────────────────
38
+ // Helpers
39
+ // ─────────────────────────────────────────────────────────────
40
+
41
+ /**
42
+ * Format token count (e.g., 12400 → "12.4K")
43
+ */
44
+ function formatTokens(n) {
45
+ if (n >= 1000) return `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}K`;
46
+ return n.toString();
47
+ }
48
+
49
+ // ─────────────────────────────────────────────────────────────
50
+ // Data Fetchers
51
+ // ─────────────────────────────────────────────────────────────
52
+
53
+ function getCurrentTask(sessionId) {
54
+ if (!sessionId) return null;
55
+
56
+ const todosDir = path.join(os.homedir(), ".claude", "todos");
57
+ if (!fs.existsSync(todosDir)) return null;
58
+
59
+ try {
60
+ const files = fs
61
+ .readdirSync(todosDir)
62
+ .filter(
63
+ (f) =>
64
+ f.startsWith(sessionId) &&
65
+ f.includes("-agent-") &&
66
+ f.endsWith(".json"),
67
+ )
68
+ .map((f) => ({
69
+ name: f,
70
+ mtime: fs.statSync(path.join(todosDir, f)).mtime,
71
+ }))
72
+ .sort((a, b) => b.mtime - a.mtime);
73
+
74
+ if (files.length === 0) return null;
75
+
76
+ const todos = JSON.parse(
77
+ fs.readFileSync(path.join(todosDir, files[0].name), "utf8"),
78
+ );
79
+ const inProgress = todos.find((t) => t.status === "in_progress");
80
+ return inProgress?.activeForm || null;
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+
86
+ function getContextDisplay(ctxWindow) {
87
+ const rawUsed = ctxWindow?.used_percentage;
88
+ const maxTokens = ctxWindow?.context_window_size || 200_000;
89
+ if (rawUsed == null) return null;
90
+
91
+ // Scale to 80% effective limit (Claude compresses around 80%)
92
+ const scaled = Math.min(100, Math.round((rawUsed / 80) * 100));
93
+ const ratio = scaled / 100;
94
+
95
+ // Bar: thin line (━ filled, ─ empty)
96
+ const filled = Math.round(ratio * CONFIG.barWidth);
97
+ const empty = CONFIG.barWidth - filled;
98
+ const bar = "\u2501".repeat(filled) + "\u2500".repeat(empty);
99
+
100
+ const tokenStr = formatTokens(Math.round(maxTokens * (rawUsed / 100)));
101
+
102
+ if (ratio < CONFIG.warnThreshold) {
103
+ return dim(`${bar} ${tokenStr}`);
104
+ } else if (ratio < CONFIG.criticalThreshold) {
105
+ return color(`${bar} ${tokenStr}`, ANSI.yellow);
106
+ } else {
107
+ return color(`${bar} ${tokenStr}`, ANSI.red);
108
+ }
109
+ }
110
+
111
+ // ─────────────────────────────────────────────────────────────
112
+ // Output Builder
113
+ // ─────────────────────────────────────────────────────────────
114
+
115
+ function buildStatusline(input) {
116
+ const data = JSON.parse(input);
117
+
118
+ if (CONFIG.debug) {
119
+ console.error("[blip]", JSON.stringify(data, null, 2));
120
+ }
121
+
122
+ const dir = data.workspace?.current_dir || process.cwd();
123
+ const sessionId = data.session_id || "";
124
+
125
+ const parts = [];
126
+
127
+ // 1. Current task (bold — only segment that stands out)
128
+ const task = getCurrentTask(sessionId);
129
+ if (task) {
130
+ parts.push(bold(task));
131
+ }
132
+
133
+ // 2. Project name
134
+ const project = path.basename(dir);
135
+ parts.push(dim(project));
136
+
137
+ // 3. Git branch
138
+ try {
139
+ const branch = execSync("git branch --show-current", {
140
+ cwd: dir,
141
+ encoding: "utf8",
142
+ stdio: ["pipe", "pipe", "ignore"],
143
+ }).trim();
144
+ if (branch) parts.push(dim(branch));
145
+ } catch {
146
+ // Not a git repo or git not available
147
+ }
148
+
149
+ // 4. Model
150
+ const model = data.model?.display_name;
151
+ if (model) {
152
+ parts.push(dim(model.toLowerCase()));
153
+ }
154
+
155
+ // 5. Context window (bar + token count)
156
+ const ctx = getContextDisplay(data.context_window);
157
+ if (ctx) {
158
+ parts.push(ctx);
159
+ }
160
+
161
+ // Truncate if wider than terminal — drops segments from the left (task first)
162
+ const sep = dim(" \u2502 ");
163
+ const cols = process.stdout.columns || 80;
164
+ while (parts.length > 1 && stripAnsi(parts.join(sep)).length > cols) {
165
+ parts.shift();
166
+ }
167
+ return parts.join(sep);
168
+ }
169
+
170
+ // ─────────────────────────────────────────────────────────────
171
+ // Main
172
+ // ─────────────────────────────────────────────────────────────
173
+
174
+ let input = "";
175
+ process.stdin.setEncoding("utf8");
176
+ process.stdin.on("data", (chunk) => (input += chunk));
177
+ process.stdin.on("end", () => {
178
+ try {
179
+ const output = buildStatusline(input);
180
+ process.stdout.write(output);
181
+ } catch {
182
+ // Silent fail — a broken statusline should never interrupt your flow
183
+ }
184
+ });