@jeremyy_prt/cc-config 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/README.md +159 -0
- package/agents/corriger-orthographe.md +49 -0
- package/agents/explorer-code.md +63 -0
- package/agents/explorer-docs.md +87 -0
- package/agents/recherche-web.md +46 -0
- package/cli.js +213 -0
- package/commands/commit.md +47 -0
- package/commands/corriger-orthographe.md +59 -0
- package/commands/creer-agent.md +126 -0
- package/commands/creer-commande.md +225 -0
- package/commands/liste-commande.md +103 -0
- package/commands/memoire-claude.md +190 -0
- package/commands/surveiller-ci.md +65 -0
- package/package.json +44 -0
- package/scripts/statusline/CLAUDE.md +178 -0
- package/scripts/statusline/README.md +105 -0
- package/scripts/statusline/biome.json +34 -0
- package/scripts/statusline/bun.lockb +0 -0
- package/scripts/statusline/data/.gitignore +5 -0
- package/scripts/statusline/fixtures/test-input.json +25 -0
- package/scripts/statusline/package.json +21 -0
- package/scripts/statusline/src/commands/CLAUDE.md +3 -0
- package/scripts/statusline/src/commands/spend-month.ts +60 -0
- package/scripts/statusline/src/commands/spend-today.ts +42 -0
- package/scripts/statusline/src/index.ts +199 -0
- package/scripts/statusline/src/lib/context.ts +103 -0
- package/scripts/statusline/src/lib/formatters.ts +218 -0
- package/scripts/statusline/src/lib/git.ts +100 -0
- package/scripts/statusline/src/lib/spend.ts +119 -0
- package/scripts/statusline/src/lib/types.ts +25 -0
- package/scripts/statusline/src/lib/usage-limits.ts +147 -0
- package/scripts/statusline/statusline.config.ts +125 -0
- package/scripts/statusline/test.ts +20 -0
- package/scripts/statusline/tsconfig.json +27 -0
- package/scripts/validate-command.js +707 -0
- package/scripts/validate-command.readme.md +283 -0
- package/settings.json +42 -0
- package/song/finish.mp3 +0 -0
- package/song/need-human.mp3 +0 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import type { StatuslineConfig } from "../statusline.config";
|
|
4
|
+
import { defaultConfig } from "../statusline.config";
|
|
5
|
+
import { getContextData } from "./lib/context";
|
|
6
|
+
import {
|
|
7
|
+
colors,
|
|
8
|
+
formatBranch,
|
|
9
|
+
formatCost,
|
|
10
|
+
formatDuration,
|
|
11
|
+
formatPath,
|
|
12
|
+
formatProgressBar,
|
|
13
|
+
formatResetTime,
|
|
14
|
+
formatSession,
|
|
15
|
+
} from "./lib/formatters";
|
|
16
|
+
import { getGitStatus } from "./lib/git";
|
|
17
|
+
import { saveSession } from "./lib/spend";
|
|
18
|
+
import type { HookInput } from "./lib/types";
|
|
19
|
+
import { getUsageLimits } from "./lib/usage-limits";
|
|
20
|
+
|
|
21
|
+
function buildFirstLine(
|
|
22
|
+
branch: string,
|
|
23
|
+
dirPath: string,
|
|
24
|
+
modelName: string,
|
|
25
|
+
showSonnetModel: boolean,
|
|
26
|
+
separator: string,
|
|
27
|
+
numberOfLines: 1 | 2 | 3,
|
|
28
|
+
): string {
|
|
29
|
+
const isSonnet = modelName.toLowerCase().includes("sonnet");
|
|
30
|
+
const sep = `${colors.GRAY} ${separator} ${colors.LIGHT_GRAY}`;
|
|
31
|
+
|
|
32
|
+
// Mode 3 lignes : chemin + git (pas de model)
|
|
33
|
+
if (numberOfLines === 3) {
|
|
34
|
+
if (branch) {
|
|
35
|
+
return `${colors.LIGHT_GRAY}${dirPath}${sep}${branch}${colors.RESET}`;
|
|
36
|
+
}
|
|
37
|
+
return `${colors.LIGHT_GRAY}${dirPath}${colors.RESET}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Mode 1 ou 2 lignes : comportement original
|
|
41
|
+
if (isSonnet && !showSonnetModel) {
|
|
42
|
+
return `${colors.LIGHT_GRAY}${branch} ${sep} ${dirPath}${colors.RESET}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return `${colors.LIGHT_GRAY}${branch} ${sep} ${dirPath} ${sep} ${modelName}${colors.RESET}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function buildSecondLine(
|
|
49
|
+
sessionCost: string,
|
|
50
|
+
_sessionDuration: string,
|
|
51
|
+
tokensUsed: number,
|
|
52
|
+
tokensMax: number,
|
|
53
|
+
contextPercentage: number,
|
|
54
|
+
fiveHourUtilization: number | null,
|
|
55
|
+
fiveHourReset: string | null,
|
|
56
|
+
sessionConfig: StatuslineConfig["session"],
|
|
57
|
+
limitsConfig: StatuslineConfig["limits"],
|
|
58
|
+
separator: string,
|
|
59
|
+
numberOfLines: 1 | 2 | 3,
|
|
60
|
+
): string {
|
|
61
|
+
let line = formatSession(
|
|
62
|
+
sessionCost,
|
|
63
|
+
tokensUsed,
|
|
64
|
+
tokensMax,
|
|
65
|
+
contextPercentage,
|
|
66
|
+
sessionConfig,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Mode 3 lignes : ne pas ajouter les limites sur la ligne 2
|
|
70
|
+
if (numberOfLines === 3) {
|
|
71
|
+
line += colors.RESET;
|
|
72
|
+
return line;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Mode 1 ou 2 lignes : ajouter les limites sur la même ligne
|
|
76
|
+
if (fiveHourUtilization !== null && fiveHourReset) {
|
|
77
|
+
const resetTime = formatResetTime(fiveHourReset);
|
|
78
|
+
const sep = `${colors.GRAY}${separator}`;
|
|
79
|
+
|
|
80
|
+
if (limitsConfig.showProgressBar) {
|
|
81
|
+
const bar = formatProgressBar(
|
|
82
|
+
fiveHourUtilization,
|
|
83
|
+
limitsConfig.progressBarLength,
|
|
84
|
+
limitsConfig.color,
|
|
85
|
+
);
|
|
86
|
+
line += ` ${sep} ${colors.GRAY}Session:${colors.LIGHT_GRAY} ${bar} ${colors.LIGHT_GRAY}${fiveHourUtilization}${colors.GRAY}% ${colors.GRAY}(${resetTime} restant)`;
|
|
87
|
+
} else {
|
|
88
|
+
line += ` ${sep} ${colors.GRAY}Session:${colors.LIGHT_GRAY} ${fiveHourUtilization}${colors.GRAY}% ${colors.GRAY}(${resetTime} restant)`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
line += colors.RESET;
|
|
93
|
+
|
|
94
|
+
return line;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildThirdLine(
|
|
98
|
+
fiveHourUtilization: number | null,
|
|
99
|
+
fiveHourReset: string | null,
|
|
100
|
+
limitsConfig: StatuslineConfig["limits"],
|
|
101
|
+
): string {
|
|
102
|
+
if (fiveHourUtilization === null || !fiveHourReset) {
|
|
103
|
+
return "";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const resetTime = formatResetTime(fiveHourReset);
|
|
107
|
+
let line = "";
|
|
108
|
+
|
|
109
|
+
if (limitsConfig.showProgressBar) {
|
|
110
|
+
const bar = formatProgressBar(
|
|
111
|
+
fiveHourUtilization,
|
|
112
|
+
limitsConfig.progressBarLength,
|
|
113
|
+
limitsConfig.color,
|
|
114
|
+
);
|
|
115
|
+
line = `${colors.GRAY}Session:${colors.LIGHT_GRAY} ${bar} ${colors.LIGHT_GRAY}${fiveHourUtilization}${colors.GRAY}% ${colors.GRAY}(${resetTime} restant)${colors.RESET}`;
|
|
116
|
+
} else {
|
|
117
|
+
line = `${colors.GRAY}Session:${colors.LIGHT_GRAY} ${fiveHourUtilization}${colors.GRAY}% ${colors.GRAY}(${resetTime} restant)${colors.RESET}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return line;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function main() {
|
|
124
|
+
try {
|
|
125
|
+
const input: HookInput = await Bun.stdin.json();
|
|
126
|
+
|
|
127
|
+
await saveSession(input);
|
|
128
|
+
|
|
129
|
+
const git = await getGitStatus();
|
|
130
|
+
const branch = formatBranch(git, defaultConfig.git);
|
|
131
|
+
const dirPath = formatPath(
|
|
132
|
+
input.workspace.current_dir,
|
|
133
|
+
defaultConfig.pathDisplayMode,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const contextData = await getContextData({
|
|
137
|
+
transcriptPath: input.transcript_path,
|
|
138
|
+
maxContextTokens: defaultConfig.context.maxContextTokens,
|
|
139
|
+
autocompactBufferTokens: defaultConfig.context.autocompactBufferTokens,
|
|
140
|
+
useUsableContextOnly: defaultConfig.context.useUsableContextOnly,
|
|
141
|
+
overheadTokens: defaultConfig.context.overheadTokens,
|
|
142
|
+
});
|
|
143
|
+
const usageLimits = await getUsageLimits();
|
|
144
|
+
|
|
145
|
+
const sessionCost = formatCost(input.cost.total_cost_usd);
|
|
146
|
+
const sessionDuration = formatDuration(input.cost.total_duration_ms);
|
|
147
|
+
|
|
148
|
+
const firstLine = buildFirstLine(
|
|
149
|
+
branch,
|
|
150
|
+
dirPath,
|
|
151
|
+
input.model.display_name,
|
|
152
|
+
defaultConfig.showSonnetModel,
|
|
153
|
+
defaultConfig.separator,
|
|
154
|
+
defaultConfig.numberOfLines,
|
|
155
|
+
);
|
|
156
|
+
const secondLine = buildSecondLine(
|
|
157
|
+
sessionCost,
|
|
158
|
+
sessionDuration,
|
|
159
|
+
contextData.tokens,
|
|
160
|
+
defaultConfig.context.maxContextTokens,
|
|
161
|
+
contextData.percentage,
|
|
162
|
+
usageLimits.five_hour?.utilization ?? null,
|
|
163
|
+
usageLimits.five_hour?.resets_at ?? null,
|
|
164
|
+
defaultConfig.session,
|
|
165
|
+
defaultConfig.limits,
|
|
166
|
+
defaultConfig.separator,
|
|
167
|
+
defaultConfig.numberOfLines,
|
|
168
|
+
);
|
|
169
|
+
const thirdLine = buildThirdLine(
|
|
170
|
+
usageLimits.five_hour?.utilization ?? null,
|
|
171
|
+
usageLimits.five_hour?.resets_at ?? null,
|
|
172
|
+
defaultConfig.limits,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (defaultConfig.numberOfLines === 1) {
|
|
176
|
+
// Mode 1 ligne : tout sur une seule ligne
|
|
177
|
+
const sep = ` ${colors.GRAY}${defaultConfig.separator}${colors.LIGHT_GRAY} `;
|
|
178
|
+
console.log(`${firstLine}${sep}${secondLine}`);
|
|
179
|
+
console.log(""); // Empty second line for spacing
|
|
180
|
+
} else if (defaultConfig.numberOfLines === 2) {
|
|
181
|
+
// Mode 2 lignes : chemin+git sur ligne 1, session+limites sur ligne 2
|
|
182
|
+
console.log(firstLine);
|
|
183
|
+
console.log(secondLine);
|
|
184
|
+
} else {
|
|
185
|
+
// Mode 3 lignes : chemin sur ligne 1, session sur ligne 2, limites sur ligne 3
|
|
186
|
+
console.log(firstLine);
|
|
187
|
+
console.log(secondLine);
|
|
188
|
+
console.log(thirdLine);
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
192
|
+
console.log(
|
|
193
|
+
`${colors.RED}Error:${colors.LIGHT_GRAY} ${errorMessage}${colors.RESET}`,
|
|
194
|
+
);
|
|
195
|
+
console.log(`${colors.GRAY}Check statusline configuration${colors.RESET}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
main();
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
export interface TokenUsage {
|
|
4
|
+
input_tokens: number;
|
|
5
|
+
output_tokens: number;
|
|
6
|
+
cache_creation_input_tokens?: number;
|
|
7
|
+
cache_read_input_tokens?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TranscriptLine {
|
|
11
|
+
message?: { usage?: TokenUsage };
|
|
12
|
+
timestamp?: string;
|
|
13
|
+
isSidechain?: boolean;
|
|
14
|
+
isApiErrorMessage?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ContextResult {
|
|
18
|
+
tokens: number;
|
|
19
|
+
percentage: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function getContextLength(
|
|
23
|
+
transcriptPath: string,
|
|
24
|
+
): Promise<number> {
|
|
25
|
+
try {
|
|
26
|
+
const content = await Bun.file(transcriptPath).text();
|
|
27
|
+
const lines = content.trim().split("\n");
|
|
28
|
+
|
|
29
|
+
if (lines.length === 0) return 0;
|
|
30
|
+
|
|
31
|
+
let mostRecentMainChainEntry: TranscriptLine | null = null;
|
|
32
|
+
let mostRecentTimestamp: Date | null = null;
|
|
33
|
+
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
try {
|
|
36
|
+
const data = JSON.parse(line) as TranscriptLine;
|
|
37
|
+
|
|
38
|
+
if (!data.message?.usage) continue;
|
|
39
|
+
if (data.isSidechain === true) continue;
|
|
40
|
+
if (data.isApiErrorMessage === true) continue;
|
|
41
|
+
if (!data.timestamp) continue;
|
|
42
|
+
|
|
43
|
+
const entryTime = new Date(data.timestamp);
|
|
44
|
+
|
|
45
|
+
if (!mostRecentTimestamp || entryTime > mostRecentTimestamp) {
|
|
46
|
+
mostRecentTimestamp = entryTime;
|
|
47
|
+
mostRecentMainChainEntry = data;
|
|
48
|
+
}
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!mostRecentMainChainEntry?.message?.usage) {
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const usage = mostRecentMainChainEntry.message.usage;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
(usage.input_tokens || 0) +
|
|
60
|
+
(usage.cache_read_input_tokens ?? 0) +
|
|
61
|
+
(usage.cache_creation_input_tokens ?? 0)
|
|
62
|
+
);
|
|
63
|
+
} catch {
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ContextDataParams {
|
|
69
|
+
transcriptPath: string;
|
|
70
|
+
maxContextTokens: number;
|
|
71
|
+
autocompactBufferTokens: number;
|
|
72
|
+
useUsableContextOnly?: boolean;
|
|
73
|
+
overheadTokens?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function getContextData({
|
|
77
|
+
transcriptPath,
|
|
78
|
+
maxContextTokens,
|
|
79
|
+
autocompactBufferTokens,
|
|
80
|
+
useUsableContextOnly = false,
|
|
81
|
+
overheadTokens = 0,
|
|
82
|
+
}: ContextDataParams): Promise<ContextResult> {
|
|
83
|
+
if (!transcriptPath || !existsSync(transcriptPath)) {
|
|
84
|
+
return { tokens: 0, percentage: 0 };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const contextLength = await getContextLength(transcriptPath);
|
|
88
|
+
let totalTokens = contextLength + overheadTokens;
|
|
89
|
+
|
|
90
|
+
// If useUsableContextOnly is true, add the autocompact buffer to displayed tokens
|
|
91
|
+
if (useUsableContextOnly) {
|
|
92
|
+
totalTokens += autocompactBufferTokens;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Always calculate percentage based on max context window
|
|
96
|
+
// (matching /context display behavior)
|
|
97
|
+
const percentage = Math.min(100, (totalTokens / maxContextTokens) * 100);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
tokens: totalTokens,
|
|
101
|
+
percentage: Math.round(percentage),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -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}Contexte:${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
|
+
}
|