@hasna/statusline 0.0.1 → 0.0.3
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 +266 -22
- package/dist/accounts.d.ts +50 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1303 -0
- package/dist/config.d.ts +11 -0
- package/dist/context-window.d.ts +14 -0
- package/dist/format.d.ts +4 -0
- package/dist/git.d.ts +6 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +920 -0
- package/dist/install.d.ts +16 -0
- package/dist/mcp/index.d.ts +4 -0
- package/dist/mcp/index.js +20591 -0
- package/dist/providers/claude.d.ts +7 -0
- package/dist/providers/types.d.ts +47 -0
- package/dist/render.d.ts +4 -0
- package/dist/segments/index.d.ts +3 -0
- package/package.json +34 -4
- package/src/config.ts +0 -53
- package/src/context-window.ts +0 -51
- package/src/format.ts +0 -36
- package/src/git.ts +0 -55
- package/src/index.ts +0 -143
- package/src/install.ts +0 -20
- package/src/providers/claude.ts +0 -35
- package/src/providers/types.ts +0 -23
- package/src/render.ts +0 -19
- package/src/segments/index.ts +0 -187
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { StatusContext } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parse the JSON payload Claude Code pipes to a `statusLine` command.
|
|
4
|
+
* Every field is optional — a partial or empty payload must still
|
|
5
|
+
* produce a usable context.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseClaudeInput(input: Record<string, any>): StatusContext;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface StatusContext {
|
|
2
|
+
provider: "claude";
|
|
3
|
+
cwd: string;
|
|
4
|
+
projectDir?: string;
|
|
5
|
+
model?: {
|
|
6
|
+
id?: string;
|
|
7
|
+
displayName?: string;
|
|
8
|
+
};
|
|
9
|
+
cost?: {
|
|
10
|
+
totalCostUsd?: number;
|
|
11
|
+
totalDurationMs?: number;
|
|
12
|
+
totalLinesAdded?: number;
|
|
13
|
+
totalLinesRemoved?: number;
|
|
14
|
+
};
|
|
15
|
+
transcriptPath?: string;
|
|
16
|
+
sessionId?: string;
|
|
17
|
+
/** Thread title the user set with `/rename`; absent until they do. */
|
|
18
|
+
sessionName?: string;
|
|
19
|
+
version?: string;
|
|
20
|
+
outputStyle?: string;
|
|
21
|
+
/** Reasoning effort level, on models that expose one. */
|
|
22
|
+
effort?: string;
|
|
23
|
+
/** Whether extended thinking is on. */
|
|
24
|
+
thinking?: boolean;
|
|
25
|
+
/** Rate-limit windows, present only once the host reports limit data. */
|
|
26
|
+
rateLimits?: {
|
|
27
|
+
fiveHour?: {
|
|
28
|
+
usedPercentage?: number;
|
|
29
|
+
};
|
|
30
|
+
sevenDay?: {
|
|
31
|
+
usedPercentage?: number;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** ANSI hints a segment can ask the renderer for. */
|
|
36
|
+
export type SegmentColor = "cyan" | "magenta" | "blue" | "green" | "yellow" | "red" | "bold" | "dim";
|
|
37
|
+
export interface Segment {
|
|
38
|
+
id: string;
|
|
39
|
+
description: string;
|
|
40
|
+
defaultEnabled: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Colour applied by the renderer when colours are on. A function lets a
|
|
43
|
+
* segment escalate with its value (e.g. a limit gauge turning red).
|
|
44
|
+
*/
|
|
45
|
+
color?: SegmentColor | ((ctx: StatusContext) => SegmentColor | null);
|
|
46
|
+
render(ctx: StatusContext): Promise<string | null> | string | null;
|
|
47
|
+
}
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { StatusContext } from "./providers/types.js";
|
|
2
|
+
import type { StatuslineConfig } from "./config.js";
|
|
3
|
+
/** Render the configured segments; a failing segment is dropped, never fatal. */
|
|
4
|
+
export declare function renderLine(ctx: StatusContext, cfg: StatuslineConfig): Promise<string>;
|
package/package.json
CHANGED
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/statusline",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Tiny composable statusline CLI for AI coding agents — pre-built toggleable segments for Claude Code, with a provider model for others",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
7
9
|
"bin": {
|
|
8
|
-
"statusline": "./
|
|
10
|
+
"statusline": "./dist/cli.js",
|
|
11
|
+
"statusline-mcp": "./dist/mcp/index.js"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./cli": {
|
|
19
|
+
"types": "./dist/cli.d.ts",
|
|
20
|
+
"import": "./dist/cli.js"
|
|
21
|
+
},
|
|
22
|
+
"./mcp": {
|
|
23
|
+
"types": "./dist/mcp/index.d.ts",
|
|
24
|
+
"import": "./dist/mcp/index.js"
|
|
25
|
+
}
|
|
9
26
|
},
|
|
10
27
|
"files": [
|
|
11
|
-
"
|
|
28
|
+
"dist",
|
|
12
29
|
"LICENSE",
|
|
13
30
|
"README.md"
|
|
14
31
|
],
|
|
15
32
|
"scripts": {
|
|
16
|
-
"
|
|
33
|
+
"build": "rm -rf dist && bun build src/index.ts src/cli.ts src/mcp/index.ts --outdir dist --target bun && tsc -p tsconfig.build.json && chmod +x dist/cli.js dist/mcp/index.js",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"test": "bun test",
|
|
36
|
+
"check": "bun run typecheck && bun test && bun run build",
|
|
37
|
+
"prepack": "bun run build",
|
|
38
|
+
"prepublishOnly": "bun run typecheck && bun test && bun run build"
|
|
17
39
|
},
|
|
18
40
|
"repository": {
|
|
19
41
|
"type": "git",
|
|
@@ -36,5 +58,13 @@
|
|
|
36
58
|
},
|
|
37
59
|
"engines": {
|
|
38
60
|
"bun": ">=1.0.0"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
64
|
+
"zod": "^3.25.76"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@types/bun": "^1.2.4",
|
|
68
|
+
"typescript": "^5.7.3"
|
|
39
69
|
}
|
|
40
70
|
}
|
package/src/config.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
|
|
5
|
-
export interface StatuslineConfig {
|
|
6
|
-
separator: string;
|
|
7
|
-
/** Ordered list of enabled segment ids. */
|
|
8
|
-
segments: string[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function defaultConfig(): StatuslineConfig {
|
|
12
|
-
return {
|
|
13
|
-
separator: " · ",
|
|
14
|
-
segments: [
|
|
15
|
-
"machine",
|
|
16
|
-
"project",
|
|
17
|
-
"commit-age",
|
|
18
|
-
"loc",
|
|
19
|
-
"model-context",
|
|
20
|
-
"context-remaining",
|
|
21
|
-
"cost",
|
|
22
|
-
],
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function configPath(): string {
|
|
27
|
-
if (process.env.STATUSLINE_CONFIG) return process.env.STATUSLINE_CONFIG;
|
|
28
|
-
const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
29
|
-
return join(base, "statusline", "config.json");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function loadConfig(): StatuslineConfig {
|
|
33
|
-
const path = configPath();
|
|
34
|
-
if (!existsSync(path)) return defaultConfig();
|
|
35
|
-
try {
|
|
36
|
-
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
37
|
-
const fallback = defaultConfig();
|
|
38
|
-
return {
|
|
39
|
-
separator: typeof raw.separator === "string" ? raw.separator : fallback.separator,
|
|
40
|
-
segments: Array.isArray(raw.segments)
|
|
41
|
-
? raw.segments.filter((s: unknown) => typeof s === "string")
|
|
42
|
-
: fallback.segments,
|
|
43
|
-
};
|
|
44
|
-
} catch {
|
|
45
|
-
return defaultConfig();
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function saveConfig(cfg: StatuslineConfig): void {
|
|
50
|
-
const path = configPath();
|
|
51
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
52
|
-
writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n");
|
|
53
|
-
}
|
package/src/context-window.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import type { StatusContext } from "./providers/types";
|
|
3
|
-
|
|
4
|
-
export interface ContextUsage {
|
|
5
|
-
/** Tokens currently occupying the context window (input side). */
|
|
6
|
-
used: number;
|
|
7
|
-
/** Output tokens of the latest assistant turn. */
|
|
8
|
-
output: number;
|
|
9
|
-
/** Context window size in tokens. */
|
|
10
|
-
window: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Derive context usage from the session transcript (JSONL). The last
|
|
15
|
-
* assistant entry's usage block reflects what the context window holds.
|
|
16
|
-
*/
|
|
17
|
-
export function contextUsage(ctx: StatusContext): ContextUsage | null {
|
|
18
|
-
const path = ctx.transcriptPath;
|
|
19
|
-
if (!path || !existsSync(path)) return null;
|
|
20
|
-
let lines: string[];
|
|
21
|
-
try {
|
|
22
|
-
lines = readFileSync(path, "utf8").split("\n");
|
|
23
|
-
} catch {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
27
|
-
const line = lines[i];
|
|
28
|
-
if (!line || !line.includes('"usage"')) continue;
|
|
29
|
-
try {
|
|
30
|
-
const entry = JSON.parse(line);
|
|
31
|
-
const usage = entry?.message?.usage;
|
|
32
|
-
if (!usage || typeof usage.input_tokens !== "number") continue;
|
|
33
|
-
const used =
|
|
34
|
-
usage.input_tokens +
|
|
35
|
-
(usage.cache_creation_input_tokens || 0) +
|
|
36
|
-
(usage.cache_read_input_tokens || 0);
|
|
37
|
-
return {
|
|
38
|
-
used,
|
|
39
|
-
output: usage.output_tokens || 0,
|
|
40
|
-
window: windowSize(ctx),
|
|
41
|
-
};
|
|
42
|
-
} catch {
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function windowSize(ctx: StatusContext): number {
|
|
50
|
-
return ctx.model?.id?.includes("[1m]") ? 1_000_000 : 200_000;
|
|
51
|
-
}
|
package/src/format.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
export function compactAge(seconds: number): string {
|
|
2
|
-
if (seconds < 60) return `${Math.floor(seconds)}s`;
|
|
3
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
|
4
|
-
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
|
|
5
|
-
if (seconds < 604800) return `${Math.floor(seconds / 86400)}d`;
|
|
6
|
-
if (seconds < 2592000) return `${Math.floor(seconds / 604800)}w`;
|
|
7
|
-
if (seconds < 31536000) return `${Math.floor(seconds / 2592000)}mo`;
|
|
8
|
-
return `${Math.floor(seconds / 31536000)}y`;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function trimmed(value: number): string {
|
|
12
|
-
const fixed = value.toFixed(1);
|
|
13
|
-
return fixed.endsWith(".0") ? fixed.slice(0, -2) : fixed;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function compactNum(n: number): string {
|
|
17
|
-
if (n >= 1_000_000) return `${trimmed(n / 1_000_000)}m`;
|
|
18
|
-
if (n >= 10_000) return `${Math.floor(n / 1000)}k`;
|
|
19
|
-
if (n >= 1000) return `${trimmed(n / 1000)}k`;
|
|
20
|
-
return `${n}`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function money(n: number): string {
|
|
24
|
-
return `$${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function compactDuration(ms: number): string {
|
|
28
|
-
const s = Math.floor(ms / 1000);
|
|
29
|
-
if (s < 60) return `${s}s`;
|
|
30
|
-
if (s < 3600) {
|
|
31
|
-
const rem = s % 60;
|
|
32
|
-
return rem ? `${Math.floor(s / 60)}m${rem}s` : `${Math.floor(s / 60)}m`;
|
|
33
|
-
}
|
|
34
|
-
const rem = Math.floor((s % 3600) / 60);
|
|
35
|
-
return rem ? `${Math.floor(s / 3600)}h${rem}m` : `${Math.floor(s / 3600)}h`;
|
|
36
|
-
}
|
package/src/git.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/** Minimal git helpers — every call degrades to null outside a repo. */
|
|
2
|
-
|
|
3
|
-
function run(cwd: string, args: string[]): string | null {
|
|
4
|
-
try {
|
|
5
|
-
const proc = Bun.spawnSync(["git", "-c", "gc.auto=0", ...args], {
|
|
6
|
-
cwd,
|
|
7
|
-
stdout: "pipe",
|
|
8
|
-
stderr: "ignore",
|
|
9
|
-
});
|
|
10
|
-
if (proc.exitCode !== 0) return null;
|
|
11
|
-
const out = proc.stdout.toString().trim();
|
|
12
|
-
return out.length ? out : null;
|
|
13
|
-
} catch {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function gitRoot(cwd: string): string | null {
|
|
19
|
-
return run(cwd, ["rev-parse", "--show-toplevel"]);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function gitBranch(cwd: string): string | null {
|
|
23
|
-
return run(cwd, ["branch", "--show-current"]);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function gitProjectName(cwd: string): string | null {
|
|
27
|
-
const root = gitRoot(cwd);
|
|
28
|
-
if (!root) return null;
|
|
29
|
-
const remote = run(root, ["config", "--get", "remote.origin.url"]);
|
|
30
|
-
if (remote) return remote.replace(/\.git$/, "").split("/").pop() || null;
|
|
31
|
-
return root.split("/").pop() || null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function lastCommitEpoch(cwd: string): number | null {
|
|
35
|
-
const out = run(cwd, ["log", "-1", "--format=%ct"]);
|
|
36
|
-
if (!out) return null;
|
|
37
|
-
const ts = Number(out);
|
|
38
|
-
return Number.isFinite(ts) ? ts : null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function trackedLineCount(cwd: string): number | null {
|
|
42
|
-
const root = gitRoot(cwd);
|
|
43
|
-
if (!root) return null;
|
|
44
|
-
try {
|
|
45
|
-
const proc = Bun.spawnSync(
|
|
46
|
-
["bash", "-c", "git -c gc.auto=0 ls-files -z | xargs -0 wc -l 2>/dev/null | awk 'END {print $1+0}'"],
|
|
47
|
-
{ cwd: root, stdout: "pipe", stderr: "ignore" },
|
|
48
|
-
);
|
|
49
|
-
if (proc.exitCode !== 0) return null;
|
|
50
|
-
const n = Number(proc.stdout.toString().trim());
|
|
51
|
-
return Number.isFinite(n) && n > 0 ? n : null;
|
|
52
|
-
} catch {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import { defaultConfig, loadConfig, saveConfig, configPath } from "./config";
|
|
3
|
-
import { parseClaudeInput } from "./providers/claude";
|
|
4
|
-
import { renderLine } from "./render";
|
|
5
|
-
import { segments, getSegment } from "./segments";
|
|
6
|
-
import { installClaude } from "./install";
|
|
7
|
-
import pkg from "../package.json";
|
|
8
|
-
|
|
9
|
-
const [, , command = "render", ...args] = process.argv;
|
|
10
|
-
|
|
11
|
-
async function readStdinJson(): Promise<Record<string, any>> {
|
|
12
|
-
try {
|
|
13
|
-
const text = await Bun.stdin.text();
|
|
14
|
-
return text.trim() ? JSON.parse(text) : {};
|
|
15
|
-
} catch {
|
|
16
|
-
return {};
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function requireKnown(ids: string[]): string[] {
|
|
21
|
-
const unknown = ids.filter((id) => !getSegment(id));
|
|
22
|
-
if (unknown.length) {
|
|
23
|
-
console.error(`unknown segment(s): ${unknown.join(", ")}\nrun \`statusline list\` to see available segments`);
|
|
24
|
-
process.exit(1);
|
|
25
|
-
}
|
|
26
|
-
return ids;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
switch (command) {
|
|
30
|
-
case "render": {
|
|
31
|
-
const input = await readStdinJson();
|
|
32
|
-
console.log(await renderLine(parseClaudeInput(input), loadConfig()));
|
|
33
|
-
break;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
case "list": {
|
|
37
|
-
const enabled = new Set(loadConfig().segments);
|
|
38
|
-
const width = Math.max(...segments.map((s) => s.id.length));
|
|
39
|
-
for (const s of segments) {
|
|
40
|
-
const mark = enabled.has(s.id) ? "[x]" : "[ ]";
|
|
41
|
-
console.log(` ${mark} ${s.id.padEnd(width)} ${s.description}`);
|
|
42
|
-
}
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
case "enable": {
|
|
47
|
-
const ids = requireKnown(args);
|
|
48
|
-
const cfg = loadConfig();
|
|
49
|
-
for (const id of ids) if (!cfg.segments.includes(id)) cfg.segments.push(id);
|
|
50
|
-
saveConfig(cfg);
|
|
51
|
-
console.log(`enabled: ${ids.join(", ")}\norder: ${cfg.segments.join(", ")}`);
|
|
52
|
-
break;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
case "disable": {
|
|
56
|
-
const ids = requireKnown(args);
|
|
57
|
-
const cfg = loadConfig();
|
|
58
|
-
cfg.segments = cfg.segments.filter((id) => !ids.includes(id));
|
|
59
|
-
saveConfig(cfg);
|
|
60
|
-
console.log(`disabled: ${ids.join(", ")}\norder: ${cfg.segments.join(", ") || "(none)"}`);
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
case "order": {
|
|
65
|
-
const ids = requireKnown(args);
|
|
66
|
-
if (!ids.length) {
|
|
67
|
-
console.error("usage: statusline order <segment> [segment...]");
|
|
68
|
-
process.exit(1);
|
|
69
|
-
}
|
|
70
|
-
const cfg = loadConfig();
|
|
71
|
-
cfg.segments = ids;
|
|
72
|
-
saveConfig(cfg);
|
|
73
|
-
console.log(`order: ${cfg.segments.join(", ")}`);
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
case "separator": {
|
|
78
|
-
if (!args.length) {
|
|
79
|
-
console.error('usage: statusline separator " · "');
|
|
80
|
-
process.exit(1);
|
|
81
|
-
}
|
|
82
|
-
const cfg = loadConfig();
|
|
83
|
-
cfg.separator = args.join(" ");
|
|
84
|
-
saveConfig(cfg);
|
|
85
|
-
console.log(`separator: "${cfg.separator}"`);
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
case "reset": {
|
|
90
|
-
saveConfig(defaultConfig());
|
|
91
|
-
console.log(`reset to defaults (${configPath()})`);
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
case "preview": {
|
|
96
|
-
const sample = {
|
|
97
|
-
cwd: process.cwd(),
|
|
98
|
-
model: { id: "claude-fable-5[1m]", display_name: "Fable" },
|
|
99
|
-
cost: { total_cost_usd: 12.34, total_duration_ms: 5400000, total_lines_added: 42, total_lines_removed: 7 },
|
|
100
|
-
version: pkg.version,
|
|
101
|
-
session_id: "preview0-0000-0000-0000-000000000000",
|
|
102
|
-
};
|
|
103
|
-
console.log(await renderLine(parseClaudeInput(sample), loadConfig()));
|
|
104
|
-
break;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
case "install": {
|
|
108
|
-
const target = args[0] || "claude";
|
|
109
|
-
if (target !== "claude") {
|
|
110
|
-
console.error(`unsupported target "${target}" — only "claude" is supported today (OpenCode has no statusline hook yet)`);
|
|
111
|
-
process.exit(1);
|
|
112
|
-
}
|
|
113
|
-
const path = installClaude();
|
|
114
|
-
console.log(`installed into ${path} — Claude Code picks it up on the next status refresh`);
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
case "--version":
|
|
119
|
-
case "-v":
|
|
120
|
-
case "version": {
|
|
121
|
-
console.log(pkg.version);
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
default: {
|
|
126
|
-
console.log(`statusline ${pkg.version} — composable statusline for AI coding agents
|
|
127
|
-
|
|
128
|
-
usage:
|
|
129
|
-
statusline render read agent JSON on stdin, print the statusline (default)
|
|
130
|
-
statusline list show all segments and which are enabled
|
|
131
|
-
statusline enable <id...> enable segments (appended to the end)
|
|
132
|
-
statusline disable <id...> disable segments
|
|
133
|
-
statusline order <id...> set the exact segment order (also defines enabled set)
|
|
134
|
-
statusline separator <str> set the separator (default " · ")
|
|
135
|
-
statusline preview render with a sample payload from the current directory
|
|
136
|
-
statusline reset restore default config
|
|
137
|
-
statusline install [claude] wire into Claude Code (~/.claude/settings.json)
|
|
138
|
-
statusline version print version
|
|
139
|
-
|
|
140
|
-
config: ${configPath()}`);
|
|
141
|
-
if (command !== "help" && command !== "--help" && command !== "-h") process.exit(1);
|
|
142
|
-
}
|
|
143
|
-
}
|
package/src/install.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Wire the statusline into Claude Code's user settings. Existing settings
|
|
7
|
-
* are preserved; the previous file is backed up alongside it.
|
|
8
|
-
*/
|
|
9
|
-
export function installClaude(settingsPath = join(homedir(), ".claude", "settings.json")): string {
|
|
10
|
-
const binary = Bun.which("statusline") || "statusline";
|
|
11
|
-
let settings: Record<string, any> = {};
|
|
12
|
-
if (existsSync(settingsPath)) {
|
|
13
|
-
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
14
|
-
copyFileSync(settingsPath, `${settingsPath}.bak-statusline`);
|
|
15
|
-
}
|
|
16
|
-
settings.statusLine = { type: "command", command: `${binary} render` };
|
|
17
|
-
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
18
|
-
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
19
|
-
return settingsPath;
|
|
20
|
-
}
|
package/src/providers/claude.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { StatusContext } from "./types";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Parse the JSON payload Claude Code pipes to a `statusLine` command.
|
|
5
|
-
* Every field is optional — a partial or empty payload must still
|
|
6
|
-
* produce a usable context.
|
|
7
|
-
*/
|
|
8
|
-
export function parseClaudeInput(input: Record<string, any>): StatusContext {
|
|
9
|
-
const cwd: string =
|
|
10
|
-
input?.cwd || input?.workspace?.current_dir || process.cwd();
|
|
11
|
-
return {
|
|
12
|
-
provider: "claude",
|
|
13
|
-
cwd,
|
|
14
|
-
projectDir: input?.workspace?.project_dir || undefined,
|
|
15
|
-
model: input?.model
|
|
16
|
-
? { id: input.model.id || undefined, displayName: input.model.display_name || undefined }
|
|
17
|
-
: undefined,
|
|
18
|
-
cost: input?.cost
|
|
19
|
-
? {
|
|
20
|
-
totalCostUsd: numberOr(input.cost.total_cost_usd),
|
|
21
|
-
totalDurationMs: numberOr(input.cost.total_duration_ms),
|
|
22
|
-
totalLinesAdded: numberOr(input.cost.total_lines_added),
|
|
23
|
-
totalLinesRemoved: numberOr(input.cost.total_lines_removed),
|
|
24
|
-
}
|
|
25
|
-
: undefined,
|
|
26
|
-
transcriptPath: input?.transcript_path || undefined,
|
|
27
|
-
sessionId: input?.session_id || undefined,
|
|
28
|
-
version: input?.version || undefined,
|
|
29
|
-
outputStyle: input?.output_style?.name || undefined,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function numberOr(v: unknown): number | undefined {
|
|
34
|
-
return typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
35
|
-
}
|
package/src/providers/types.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export interface StatusContext {
|
|
2
|
-
provider: "claude";
|
|
3
|
-
cwd: string;
|
|
4
|
-
projectDir?: string;
|
|
5
|
-
model?: { id?: string; displayName?: string };
|
|
6
|
-
cost?: {
|
|
7
|
-
totalCostUsd?: number;
|
|
8
|
-
totalDurationMs?: number;
|
|
9
|
-
totalLinesAdded?: number;
|
|
10
|
-
totalLinesRemoved?: number;
|
|
11
|
-
};
|
|
12
|
-
transcriptPath?: string;
|
|
13
|
-
sessionId?: string;
|
|
14
|
-
version?: string;
|
|
15
|
-
outputStyle?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface Segment {
|
|
19
|
-
id: string;
|
|
20
|
-
description: string;
|
|
21
|
-
defaultEnabled: boolean;
|
|
22
|
-
render(ctx: StatusContext): Promise<string | null> | string | null;
|
|
23
|
-
}
|
package/src/render.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { StatusContext } from "./providers/types";
|
|
2
|
-
import type { StatuslineConfig } from "./config";
|
|
3
|
-
import { getSegment } from "./segments";
|
|
4
|
-
|
|
5
|
-
/** Render the configured segments; a failing segment is dropped, never fatal. */
|
|
6
|
-
export async function renderLine(ctx: StatusContext, cfg: StatuslineConfig): Promise<string> {
|
|
7
|
-
const parts: string[] = [];
|
|
8
|
-
for (const id of cfg.segments) {
|
|
9
|
-
const segment = getSegment(id);
|
|
10
|
-
if (!segment) continue;
|
|
11
|
-
try {
|
|
12
|
-
const out = await segment.render(ctx);
|
|
13
|
-
if (out) parts.push(out);
|
|
14
|
-
} catch {
|
|
15
|
-
// statuslines must never break the host UI
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return parts.join(cfg.separator);
|
|
19
|
-
}
|