aiblueprint-cli 1.1.4 → 1.1.8
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 +496 -41
- package/claude-code-config/agents/action.md +36 -0
- package/claude-code-config/agents/explore-codebase.md +6 -0
- package/claude-code-config/agents/explore-docs.md +88 -0
- package/claude-code-config/agents/fix-grammar.md +49 -0
- package/claude-code-config/agents/snipper.md +2 -0
- package/claude-code-config/agents/websearch.md +1 -0
- package/claude-code-config/commands/commit.md +1 -1
- package/claude-code-config/commands/debug.md +91 -0
- package/claude-code-config/commands/epct/code.md +171 -0
- package/claude-code-config/commands/epct/deploy.md +116 -0
- package/claude-code-config/commands/epct/explore.md +97 -0
- package/claude-code-config/commands/epct/plan.md +132 -0
- package/claude-code-config/commands/epct/tasks.md +206 -0
- package/claude-code-config/commands/explore.md +45 -0
- package/claude-code-config/commands/melvynx-plugin.md +1 -0
- package/claude-code-config/commands/oneshot.md +57 -0
- package/claude-code-config/hooks/hooks.json +15 -0
- package/claude-code-config/scripts/statusline/CLAUDE.md +178 -0
- package/claude-code-config/scripts/statusline/README.md +105 -0
- package/claude-code-config/scripts/statusline/biome.json +34 -0
- package/claude-code-config/scripts/statusline/bun.lockb +0 -0
- package/claude-code-config/scripts/statusline/data/.gitignore +5 -0
- package/claude-code-config/scripts/statusline/fixtures/test-input.json +25 -0
- package/claude-code-config/scripts/statusline/package.json +21 -0
- package/claude-code-config/scripts/statusline/src/commands/CLAUDE.md +3 -0
- package/claude-code-config/scripts/statusline/src/commands/spend-month.ts +60 -0
- package/claude-code-config/scripts/statusline/src/commands/spend-today.ts +42 -0
- package/claude-code-config/scripts/statusline/src/index.ts +141 -0
- package/claude-code-config/scripts/statusline/src/lib/context.ts +103 -0
- package/claude-code-config/scripts/statusline/src/lib/formatters.ts +218 -0
- package/claude-code-config/scripts/statusline/src/lib/git.ts +100 -0
- package/claude-code-config/scripts/statusline/src/lib/spend.ts +119 -0
- package/claude-code-config/scripts/statusline/src/lib/types.ts +25 -0
- package/claude-code-config/scripts/statusline/src/lib/usage-limits.ts +147 -0
- package/claude-code-config/scripts/statusline/statusline.config.ts +122 -0
- package/claude-code-config/scripts/statusline/test.ts +20 -0
- package/claude-code-config/scripts/statusline/tsconfig.json +27 -0
- package/dist/cli.js +722 -256
- package/package.json +1 -2
- package/claude-code-config/output-styles/assistant.md +0 -15
- package/claude-code-config/output-styles/honnest.md +0 -9
- package/claude-code-config/output-styles/senior-dev.md +0 -14
- package/claude-code-config/scripts/statusline-ccusage.sh +0 -188
- package/claude-code-config/scripts/statusline.readme.md +0 -194
- /package/claude-code-config/{hooks → scripts}/hook-post-file.ts +0 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type { Separator, StatuslineConfig } from "../../statusline.config";
|
|
2
|
+
import type { GitStatus } from "./git";
|
|
3
|
+
|
|
4
|
+
export const colors = {
|
|
5
|
+
GREEN: "\x1b[0;32m",
|
|
6
|
+
RED: "\x1b[0;31m",
|
|
7
|
+
PURPLE: "\x1b[0;35m",
|
|
8
|
+
YELLOW: "\x1b[0;33m",
|
|
9
|
+
ORANGE: "\x1b[38;5;208m",
|
|
10
|
+
GRAY: "\x1b[0;90m",
|
|
11
|
+
LIGHT_GRAY: "\x1b[0;37m",
|
|
12
|
+
RESET: "\x1b[0m",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export function formatBranch(
|
|
16
|
+
git: GitStatus,
|
|
17
|
+
gitConfig: StatuslineConfig["git"],
|
|
18
|
+
): string {
|
|
19
|
+
let result = "";
|
|
20
|
+
|
|
21
|
+
if (gitConfig.showBranch) {
|
|
22
|
+
result = git.branch;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (git.hasChanges) {
|
|
26
|
+
const changes: string[] = [];
|
|
27
|
+
|
|
28
|
+
if (gitConfig.showDirtyIndicator) {
|
|
29
|
+
result += `${colors.PURPLE}*${colors.RESET}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (gitConfig.showChanges) {
|
|
33
|
+
const totalAdded = git.staged.added + git.unstaged.added;
|
|
34
|
+
const totalDeleted = git.staged.deleted + git.unstaged.deleted;
|
|
35
|
+
|
|
36
|
+
if (totalAdded > 0) {
|
|
37
|
+
changes.push(`${colors.GREEN}+${totalAdded}${colors.RESET}`);
|
|
38
|
+
}
|
|
39
|
+
if (totalDeleted > 0) {
|
|
40
|
+
changes.push(`${colors.RED}-${totalDeleted}${colors.RESET}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (gitConfig.showStaged && git.staged.files > 0) {
|
|
45
|
+
changes.push(`${colors.GRAY}~${git.staged.files}${colors.RESET}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (gitConfig.showUnstaged && git.unstaged.files > 0) {
|
|
49
|
+
changes.push(`${colors.YELLOW}~${git.unstaged.files}${colors.RESET}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (changes.length > 0) {
|
|
53
|
+
result += ` ${changes.join(" ")}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function formatPath(
|
|
61
|
+
path: string,
|
|
62
|
+
mode: "full" | "truncated" | "basename" = "truncated",
|
|
63
|
+
): string {
|
|
64
|
+
const home = process.env.HOME || "";
|
|
65
|
+
let formattedPath = path;
|
|
66
|
+
|
|
67
|
+
if (home && path.startsWith(home)) {
|
|
68
|
+
formattedPath = `~${path.slice(home.length)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (mode === "basename") {
|
|
72
|
+
const segments = path.split("/").filter((s) => s.length > 0);
|
|
73
|
+
return segments[segments.length - 1] || path;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (mode === "truncated") {
|
|
77
|
+
const segments = formattedPath.split("/").filter((s) => s.length > 0);
|
|
78
|
+
if (segments.length > 2) {
|
|
79
|
+
return `/${segments.slice(-2).join("/")}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return formattedPath;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function formatCost(cost: number): string {
|
|
87
|
+
return cost.toFixed(2);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function formatTokens(tokens: number, showDecimals = true): string {
|
|
91
|
+
if (tokens >= 1000000) {
|
|
92
|
+
const value = tokens / 1000000;
|
|
93
|
+
const number = showDecimals
|
|
94
|
+
? value.toFixed(1)
|
|
95
|
+
: Math.round(value).toString();
|
|
96
|
+
return `${number}${colors.GRAY}m${colors.LIGHT_GRAY}`;
|
|
97
|
+
}
|
|
98
|
+
if (tokens >= 1000) {
|
|
99
|
+
const value = tokens / 1000;
|
|
100
|
+
const number = showDecimals
|
|
101
|
+
? value.toFixed(1)
|
|
102
|
+
: Math.round(value).toString();
|
|
103
|
+
return `${number}${colors.GRAY}k${colors.LIGHT_GRAY}`;
|
|
104
|
+
}
|
|
105
|
+
return tokens.toString();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function formatDuration(ms: number): string {
|
|
109
|
+
const minutes = Math.floor(ms / 60000);
|
|
110
|
+
const hours = Math.floor(minutes / 60);
|
|
111
|
+
const mins = minutes % 60;
|
|
112
|
+
|
|
113
|
+
if (hours > 0) {
|
|
114
|
+
return `${hours}h ${mins}m`;
|
|
115
|
+
}
|
|
116
|
+
return `${mins}m`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function formatResetTime(resetsAt: string): string {
|
|
120
|
+
try {
|
|
121
|
+
const resetDate = new Date(resetsAt);
|
|
122
|
+
const now = new Date();
|
|
123
|
+
const diffMs = resetDate.getTime() - now.getTime();
|
|
124
|
+
|
|
125
|
+
if (diffMs <= 0) {
|
|
126
|
+
return "now";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const hours = Math.floor(diffMs / 3600000);
|
|
130
|
+
const minutes = Math.floor((diffMs % 3600000) / 60000);
|
|
131
|
+
|
|
132
|
+
if (hours > 0) {
|
|
133
|
+
return `${hours}h${minutes}m`;
|
|
134
|
+
}
|
|
135
|
+
return `${minutes}m`;
|
|
136
|
+
} catch {
|
|
137
|
+
return "N/A";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function formatProgressBar(
|
|
142
|
+
percentage: number,
|
|
143
|
+
length: number,
|
|
144
|
+
colorMode: "progressive" | "green" | "yellow" | "red",
|
|
145
|
+
): string {
|
|
146
|
+
const filled = Math.round((percentage / 100) * length);
|
|
147
|
+
const empty = length - filled;
|
|
148
|
+
|
|
149
|
+
const filledBar = "█".repeat(filled);
|
|
150
|
+
const emptyBar = "░".repeat(empty);
|
|
151
|
+
|
|
152
|
+
let barColor: string;
|
|
153
|
+
if (colorMode === "progressive") {
|
|
154
|
+
if (percentage < 50) {
|
|
155
|
+
barColor = colors.GRAY;
|
|
156
|
+
} else if (percentage < 70) {
|
|
157
|
+
barColor = colors.YELLOW;
|
|
158
|
+
} else if (percentage < 90) {
|
|
159
|
+
barColor = colors.ORANGE;
|
|
160
|
+
} else {
|
|
161
|
+
barColor = colors.RED;
|
|
162
|
+
}
|
|
163
|
+
} else if (colorMode === "green") {
|
|
164
|
+
barColor = colors.GREEN;
|
|
165
|
+
} else if (colorMode === "yellow") {
|
|
166
|
+
barColor = colors.YELLOW;
|
|
167
|
+
} else {
|
|
168
|
+
barColor = colors.RED;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return `${barColor}${filledBar}${colors.GRAY}${emptyBar}${colors.RESET}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface SessionConfig {
|
|
175
|
+
infoSeparator: Separator | null;
|
|
176
|
+
showCost: boolean;
|
|
177
|
+
showTokens: boolean;
|
|
178
|
+
showMaxTokens: boolean;
|
|
179
|
+
showTokenDecimals: boolean;
|
|
180
|
+
showPercentage: boolean;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function formatSession(
|
|
184
|
+
cost: string,
|
|
185
|
+
tokensUsed: number,
|
|
186
|
+
tokensMax: number,
|
|
187
|
+
percentage: number,
|
|
188
|
+
config: SessionConfig,
|
|
189
|
+
): string {
|
|
190
|
+
const sessionItems: string[] = [];
|
|
191
|
+
|
|
192
|
+
if (config.showCost) {
|
|
193
|
+
sessionItems.push(`$${cost}`);
|
|
194
|
+
}
|
|
195
|
+
if (config.showTokens) {
|
|
196
|
+
const formattedUsed = formatTokens(tokensUsed, config.showTokenDecimals);
|
|
197
|
+
if (config.showMaxTokens) {
|
|
198
|
+
const formattedMax = formatTokens(tokensMax, config.showTokenDecimals);
|
|
199
|
+
sessionItems.push(
|
|
200
|
+
`${formattedUsed}${colors.GRAY}/${formattedMax}${colors.LIGHT_GRAY}`,
|
|
201
|
+
);
|
|
202
|
+
} else {
|
|
203
|
+
sessionItems.push(formattedUsed);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (config.showPercentage) {
|
|
207
|
+
sessionItems.push(`${percentage}${colors.GRAY}%${colors.LIGHT_GRAY}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (sessionItems.length === 0) {
|
|
211
|
+
return "";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const infoSep = config.infoSeparator
|
|
215
|
+
? ` ${colors.GRAY}${config.infoSeparator}${colors.LIGHT_GRAY} `
|
|
216
|
+
: " ";
|
|
217
|
+
return `${colors.GRAY}S:${colors.LIGHT_GRAY} ${sessionItems.join(infoSep)}`;
|
|
218
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { $ } from "bun";
|
|
2
|
+
|
|
3
|
+
export interface GitStatus {
|
|
4
|
+
branch: string;
|
|
5
|
+
hasChanges: boolean;
|
|
6
|
+
staged: {
|
|
7
|
+
added: number;
|
|
8
|
+
deleted: number;
|
|
9
|
+
files: number;
|
|
10
|
+
};
|
|
11
|
+
unstaged: {
|
|
12
|
+
added: number;
|
|
13
|
+
deleted: number;
|
|
14
|
+
files: number;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function getGitStatus(): Promise<GitStatus> {
|
|
19
|
+
try {
|
|
20
|
+
const isGitRepo = await $`git rev-parse --git-dir`.quiet().nothrow();
|
|
21
|
+
if (isGitRepo.exitCode !== 0) {
|
|
22
|
+
return {
|
|
23
|
+
branch: "no-git",
|
|
24
|
+
hasChanges: false,
|
|
25
|
+
staged: { added: 0, deleted: 0, files: 0 },
|
|
26
|
+
unstaged: { added: 0, deleted: 0, files: 0 },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const branchResult = await $`git branch --show-current`.quiet().text();
|
|
31
|
+
const branch = branchResult.trim() || "detached";
|
|
32
|
+
|
|
33
|
+
const diffCheck = await $`git diff-index --quiet HEAD --`.quiet().nothrow();
|
|
34
|
+
const cachedCheck = await $`git diff-index --quiet --cached HEAD --`
|
|
35
|
+
.quiet()
|
|
36
|
+
.nothrow();
|
|
37
|
+
|
|
38
|
+
if (diffCheck.exitCode !== 0 || cachedCheck.exitCode !== 0) {
|
|
39
|
+
const unstagedDiff = await $`git diff --numstat`.quiet().text();
|
|
40
|
+
const stagedDiff = await $`git diff --cached --numstat`.quiet().text();
|
|
41
|
+
const stagedFilesResult = await $`git diff --cached --name-only`
|
|
42
|
+
.quiet()
|
|
43
|
+
.text();
|
|
44
|
+
const unstagedFilesResult = await $`git diff --name-only`.quiet().text();
|
|
45
|
+
|
|
46
|
+
const parseStats = (diff: string) => {
|
|
47
|
+
let added = 0;
|
|
48
|
+
let deleted = 0;
|
|
49
|
+
for (const line of diff.split("\n")) {
|
|
50
|
+
if (!line.trim()) continue;
|
|
51
|
+
const [a, d] = line
|
|
52
|
+
.split("\t")
|
|
53
|
+
.map((n) => Number.parseInt(n, 10) || 0);
|
|
54
|
+
added += a;
|
|
55
|
+
deleted += d;
|
|
56
|
+
}
|
|
57
|
+
return { added, deleted };
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const unstagedStats = parseStats(unstagedDiff);
|
|
61
|
+
const stagedStats = parseStats(stagedDiff);
|
|
62
|
+
|
|
63
|
+
const stagedFilesCount = stagedFilesResult
|
|
64
|
+
.split("\n")
|
|
65
|
+
.filter((f) => f.trim()).length;
|
|
66
|
+
const unstagedFilesCount = unstagedFilesResult
|
|
67
|
+
.split("\n")
|
|
68
|
+
.filter((f) => f.trim()).length;
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
branch,
|
|
72
|
+
hasChanges: true,
|
|
73
|
+
staged: {
|
|
74
|
+
added: stagedStats.added,
|
|
75
|
+
deleted: stagedStats.deleted,
|
|
76
|
+
files: stagedFilesCount,
|
|
77
|
+
},
|
|
78
|
+
unstaged: {
|
|
79
|
+
added: unstagedStats.added,
|
|
80
|
+
deleted: unstagedStats.deleted,
|
|
81
|
+
files: unstagedFilesCount,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
branch,
|
|
88
|
+
hasChanges: false,
|
|
89
|
+
staged: { added: 0, deleted: 0, files: 0 },
|
|
90
|
+
unstaged: { added: 0, deleted: 0, files: 0 },
|
|
91
|
+
};
|
|
92
|
+
} catch {
|
|
93
|
+
return {
|
|
94
|
+
branch: "no-git",
|
|
95
|
+
hasChanges: false,
|
|
96
|
+
staged: { added: 0, deleted: 0, files: 0 },
|
|
97
|
+
unstaged: { added: 0, deleted: 0, files: 0 },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { HookInput } from "./types";
|
|
5
|
+
|
|
6
|
+
export interface SpendSession {
|
|
7
|
+
id: string;
|
|
8
|
+
cost: number;
|
|
9
|
+
date: string;
|
|
10
|
+
duration_ms: number;
|
|
11
|
+
lines_added: number;
|
|
12
|
+
lines_removed: number;
|
|
13
|
+
cwd: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SpendData {
|
|
17
|
+
sessions: SpendSession[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getSpendFilePath(): string {
|
|
21
|
+
// Use the project's data folder instead of ~/.claude
|
|
22
|
+
const projectRoot = join(import.meta.dir, "..", "..");
|
|
23
|
+
return join(projectRoot, "data", "spend.json");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function loadSpendData(): Promise<SpendData> {
|
|
27
|
+
const spendFile = getSpendFilePath();
|
|
28
|
+
|
|
29
|
+
if (!existsSync(spendFile)) {
|
|
30
|
+
return { sessions: [] };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const content = await readFile(spendFile, "utf-8");
|
|
35
|
+
return JSON.parse(content);
|
|
36
|
+
} catch {
|
|
37
|
+
return { sessions: [] };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function saveSpendData(data: SpendData): Promise<void> {
|
|
42
|
+
const spendFile = getSpendFilePath();
|
|
43
|
+
const projectRoot = join(import.meta.dir, "..", "..");
|
|
44
|
+
const dataDir = join(projectRoot, "data");
|
|
45
|
+
|
|
46
|
+
if (!existsSync(dataDir)) {
|
|
47
|
+
mkdirSync(dataDir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await writeFile(spendFile, JSON.stringify(data, null, 2));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function saveSession(input: HookInput): Promise<void> {
|
|
54
|
+
if (
|
|
55
|
+
!input.session_id ||
|
|
56
|
+
input.cost.total_cost_usd === 0 ||
|
|
57
|
+
input.cost.total_cost_usd === null
|
|
58
|
+
) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const data = await loadSpendData();
|
|
63
|
+
const isoDate = new Date().toISOString().split("T")[0];
|
|
64
|
+
|
|
65
|
+
const session: SpendSession = {
|
|
66
|
+
id: input.session_id,
|
|
67
|
+
cost: input.cost.total_cost_usd,
|
|
68
|
+
date: isoDate,
|
|
69
|
+
duration_ms: input.cost.total_duration_ms,
|
|
70
|
+
lines_added: input.cost.total_lines_added,
|
|
71
|
+
lines_removed: input.cost.total_lines_removed,
|
|
72
|
+
cwd: input.cwd,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const existingIndex = data.sessions.findIndex(
|
|
76
|
+
(s) => s.id === input.session_id,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (existingIndex !== -1) {
|
|
80
|
+
data.sessions[existingIndex] = session;
|
|
81
|
+
} else {
|
|
82
|
+
data.sessions.push(session);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await saveSpendData(data);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function filterSessionsByDate(
|
|
89
|
+
sessions: SpendSession[],
|
|
90
|
+
startDate: Date,
|
|
91
|
+
endDate?: Date,
|
|
92
|
+
): SpendSession[] {
|
|
93
|
+
return sessions.filter((session) => {
|
|
94
|
+
const sessionDate = new Date(session.date);
|
|
95
|
+
if (endDate) {
|
|
96
|
+
return sessionDate >= startDate && sessionDate <= endDate;
|
|
97
|
+
}
|
|
98
|
+
return sessionDate >= startDate;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function calculateTotalCost(sessions: SpendSession[]): number {
|
|
103
|
+
return sessions.reduce((sum, session) => sum + session.cost, 0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function calculateTotalDuration(sessions: SpendSession[]): number {
|
|
107
|
+
return sessions.reduce((sum, session) => sum + session.duration_ms, 0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function getTodayStart(): Date {
|
|
111
|
+
const today = new Date();
|
|
112
|
+
today.setHours(0, 0, 0, 0);
|
|
113
|
+
return today;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getMonthStart(): Date {
|
|
117
|
+
const today = new Date();
|
|
118
|
+
return new Date(today.getFullYear(), today.getMonth(), 1);
|
|
119
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface HookInput {
|
|
2
|
+
session_id: string;
|
|
3
|
+
transcript_path: string;
|
|
4
|
+
cwd: string;
|
|
5
|
+
model: {
|
|
6
|
+
id: string;
|
|
7
|
+
display_name: string;
|
|
8
|
+
};
|
|
9
|
+
workspace: {
|
|
10
|
+
current_dir: string;
|
|
11
|
+
project_dir: string;
|
|
12
|
+
};
|
|
13
|
+
version: string;
|
|
14
|
+
output_style: {
|
|
15
|
+
name: string;
|
|
16
|
+
};
|
|
17
|
+
cost: {
|
|
18
|
+
total_cost_usd: number;
|
|
19
|
+
total_duration_ms: number;
|
|
20
|
+
total_api_duration_ms: number;
|
|
21
|
+
total_lines_added: number;
|
|
22
|
+
total_lines_removed: number;
|
|
23
|
+
};
|
|
24
|
+
exceeds_200k_tokens?: boolean;
|
|
25
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { $ } from "bun";
|
|
5
|
+
|
|
6
|
+
export interface UsageLimits {
|
|
7
|
+
five_hour: {
|
|
8
|
+
utilization: number;
|
|
9
|
+
resets_at: string | null;
|
|
10
|
+
} | null;
|
|
11
|
+
seven_day: {
|
|
12
|
+
utilization: number;
|
|
13
|
+
resets_at: string | null;
|
|
14
|
+
} | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface CachedUsageLimits {
|
|
18
|
+
data: UsageLimits;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const CACHE_DURATION_MS = 60 * 1000; // 1 minute
|
|
23
|
+
|
|
24
|
+
function getCacheFilePath(): string {
|
|
25
|
+
const projectRoot = join(import.meta.dir, "..", "..");
|
|
26
|
+
return join(projectRoot, "data", "usage-limits-cache.json");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface Credentials {
|
|
30
|
+
claudeAiOauth: {
|
|
31
|
+
accessToken: string;
|
|
32
|
+
refreshToken: string;
|
|
33
|
+
expiresAt: number;
|
|
34
|
+
scopes: string[];
|
|
35
|
+
subscriptionType: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function getCredentials(): Promise<string | null> {
|
|
40
|
+
try {
|
|
41
|
+
const result =
|
|
42
|
+
await $`security find-generic-password -s "Claude Code-credentials" -w`
|
|
43
|
+
.quiet()
|
|
44
|
+
.text();
|
|
45
|
+
const creds: Credentials = JSON.parse(result.trim());
|
|
46
|
+
return creds.claudeAiOauth.accessToken;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function fetchUsageLimits(
|
|
53
|
+
token: string,
|
|
54
|
+
): Promise<UsageLimits | null> {
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
|
|
57
|
+
method: "GET",
|
|
58
|
+
headers: {
|
|
59
|
+
Accept: "application/json, text/plain, */*",
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
"User-Agent": "claude-code/2.0.31",
|
|
62
|
+
Authorization: `Bearer ${token}`,
|
|
63
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
64
|
+
"Accept-Encoding": "gzip, compress, deflate, br",
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const data = await response.json();
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
five_hour: data.five_hour || null,
|
|
76
|
+
seven_day: data.seven_day || null,
|
|
77
|
+
};
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function loadCache(): Promise<CachedUsageLimits | null> {
|
|
84
|
+
try {
|
|
85
|
+
const cacheFile = getCacheFilePath();
|
|
86
|
+
if (!existsSync(cacheFile)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const content = await readFile(cacheFile, "utf-8");
|
|
91
|
+
const cached: CachedUsageLimits = JSON.parse(content);
|
|
92
|
+
|
|
93
|
+
// Check if cache is still valid (< 1 minute old)
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
if (now - cached.timestamp < CACHE_DURATION_MS) {
|
|
96
|
+
return cached;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function saveCache(data: UsageLimits): Promise<void> {
|
|
106
|
+
try {
|
|
107
|
+
const cacheFile = getCacheFilePath();
|
|
108
|
+
const cached: CachedUsageLimits = {
|
|
109
|
+
data,
|
|
110
|
+
timestamp: Date.now(),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
await writeFile(cacheFile, JSON.stringify(cached, null, 2));
|
|
114
|
+
} catch {
|
|
115
|
+
// Fail silently
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function getUsageLimits(): Promise<UsageLimits> {
|
|
120
|
+
try {
|
|
121
|
+
// Try to load from cache first
|
|
122
|
+
const cached = await loadCache();
|
|
123
|
+
if (cached) {
|
|
124
|
+
return cached.data;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Cache miss or expired - fetch from API
|
|
128
|
+
const token = await getCredentials();
|
|
129
|
+
|
|
130
|
+
if (!token) {
|
|
131
|
+
return { five_hour: null, seven_day: null };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const limits = await fetchUsageLimits(token);
|
|
135
|
+
|
|
136
|
+
if (!limits) {
|
|
137
|
+
return { five_hour: null, seven_day: null };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Save to cache
|
|
141
|
+
await saveCache(limits);
|
|
142
|
+
|
|
143
|
+
return limits;
|
|
144
|
+
} catch {
|
|
145
|
+
return { five_hour: null, seven_day: null };
|
|
146
|
+
}
|
|
147
|
+
}
|