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 +21 -0
- package/README.md +87 -0
- package/bin/setup.js +138 -0
- package/package.json +26 -0
- package/statusline.js +184 -0
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
|
+
});
|