claude-skill-lord 2.0.2 → 2.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/.claude-plugin/plugin.json +1 -1
- package/.commitlintrc.json +27 -0
- package/.env.example +26 -0
- package/.releaserc.json +119 -0
- package/.repomixignore +22 -0
- package/CLAUDE.md +14 -0
- package/README.md +26 -0
- package/docs/code-standards.md +949 -0
- package/hooks/hooks.json +6 -0
- package/package.json +6 -2
- package/scripts/statusline.js +263 -0
- package/scripts/statusline.ps1 +312 -0
- package/scripts/statusline.sh +141 -0
package/hooks/hooks.json
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
|
3
|
+
"includeCoAuthoredBy": false,
|
|
4
|
+
"statusLine": {
|
|
5
|
+
"type": "command",
|
|
6
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/statusline.js\"",
|
|
7
|
+
"padding": 0
|
|
8
|
+
},
|
|
3
9
|
"hooks": {
|
|
4
10
|
"PreToolUse": [
|
|
5
11
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-skill-lord",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "Curated Claude Code plugin — 44 agents, 170 skills, 115 commands, 13 language rules with intelligent skill routing",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Dong Anh",
|
|
@@ -51,7 +51,11 @@
|
|
|
51
51
|
"CLAUDE.md",
|
|
52
52
|
"README.md",
|
|
53
53
|
"LICENSE",
|
|
54
|
-
"LICENSE-ui-ux-pro-max.txt"
|
|
54
|
+
"LICENSE-ui-ux-pro-max.txt",
|
|
55
|
+
".env.example",
|
|
56
|
+
".repomixignore",
|
|
57
|
+
".commitlintrc.json",
|
|
58
|
+
".releaserc.json"
|
|
55
59
|
],
|
|
56
60
|
"engines": {
|
|
57
61
|
"node": ">=18.0.0"
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Custom Claude Code statusline for Node.js
|
|
6
|
+
* Cross-platform support: Windows, macOS, Linux
|
|
7
|
+
* Theme: detailed | Colors: true | Features: directory, git, model, usage, session, tokens
|
|
8
|
+
* No external dependencies - uses only Node.js built-in modules
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { stdin, stdout, env } = require('process');
|
|
12
|
+
const { execSync } = require('child_process');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
// Configuration
|
|
16
|
+
const USE_COLOR = !env.NO_COLOR && stdout.isTTY;
|
|
17
|
+
|
|
18
|
+
// Color helpers
|
|
19
|
+
const color = (code) => USE_COLOR ? `\x1b[${code}m` : '';
|
|
20
|
+
const reset = () => USE_COLOR ? '\x1b[0m' : '';
|
|
21
|
+
|
|
22
|
+
// Color definitions
|
|
23
|
+
const DirColor = color('1;36'); // cyan
|
|
24
|
+
const GitColor = color('1;32'); // green
|
|
25
|
+
const ModelColor = color('1;35'); // magenta
|
|
26
|
+
const VersionColor = color('1;33'); // yellow
|
|
27
|
+
const UsageColor = color('1;35'); // magenta
|
|
28
|
+
const CostColor = color('1;36'); // cyan
|
|
29
|
+
const Reset = reset();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Safe command execution wrapper
|
|
33
|
+
*/
|
|
34
|
+
function exec(cmd) {
|
|
35
|
+
try {
|
|
36
|
+
return execSync(cmd, {
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
39
|
+
windowsHide: true
|
|
40
|
+
}).trim();
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Convert ISO8601 timestamp to Unix epoch
|
|
48
|
+
*/
|
|
49
|
+
function toEpoch(timestamp) {
|
|
50
|
+
try {
|
|
51
|
+
const date = new Date(timestamp);
|
|
52
|
+
return Math.floor(date.getTime() / 1000);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Format epoch timestamp as HH:mm
|
|
60
|
+
*/
|
|
61
|
+
function formatTimeHM(epoch) {
|
|
62
|
+
try {
|
|
63
|
+
const date = new Date(epoch * 1000);
|
|
64
|
+
const hours = date.getHours().toString().padStart(2, '0');
|
|
65
|
+
const minutes = date.getMinutes().toString().padStart(2, '0');
|
|
66
|
+
return `${hours}:${minutes}`;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return '00:00';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get session color based on remaining percentage
|
|
74
|
+
*/
|
|
75
|
+
function getSessionColor(sessionPercent) {
|
|
76
|
+
if (!USE_COLOR) return '';
|
|
77
|
+
|
|
78
|
+
const remaining = 100 - sessionPercent;
|
|
79
|
+
if (remaining <= 10) {
|
|
80
|
+
return '\x1b[1;31m'; // red
|
|
81
|
+
} else if (remaining <= 25) {
|
|
82
|
+
return '\x1b[1;33m'; // yellow
|
|
83
|
+
} else {
|
|
84
|
+
return '\x1b[1;32m'; // green
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Expand home directory to ~
|
|
90
|
+
*/
|
|
91
|
+
function expandHome(path) {
|
|
92
|
+
const homeDir = os.homedir();
|
|
93
|
+
if (path.startsWith(homeDir)) {
|
|
94
|
+
return path.replace(homeDir, '~');
|
|
95
|
+
}
|
|
96
|
+
return path;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Read stdin asynchronously
|
|
101
|
+
*/
|
|
102
|
+
async function readStdin() {
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
const chunks = [];
|
|
105
|
+
stdin.setEncoding('utf8');
|
|
106
|
+
|
|
107
|
+
stdin.on('data', (chunk) => {
|
|
108
|
+
chunks.push(chunk);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
stdin.on('end', () => {
|
|
112
|
+
resolve(chunks.join(''));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
stdin.on('error', (err) => {
|
|
116
|
+
reject(err);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Main function
|
|
123
|
+
*/
|
|
124
|
+
async function main() {
|
|
125
|
+
try {
|
|
126
|
+
// Read and parse JSON input
|
|
127
|
+
const input = await readStdin();
|
|
128
|
+
if (!input.trim()) {
|
|
129
|
+
console.error('No input provided');
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const data = JSON.parse(input);
|
|
134
|
+
|
|
135
|
+
// Extract basic information
|
|
136
|
+
let currentDir = 'unknown';
|
|
137
|
+
if (data.workspace?.current_dir) {
|
|
138
|
+
currentDir = data.workspace.current_dir;
|
|
139
|
+
} else if (data.cwd) {
|
|
140
|
+
currentDir = data.cwd;
|
|
141
|
+
}
|
|
142
|
+
currentDir = expandHome(currentDir);
|
|
143
|
+
|
|
144
|
+
const modelName = data.model?.display_name || 'Claude';
|
|
145
|
+
const modelVersion = data.model?.version && data.model.version !== 'null' ? data.model.version : '';
|
|
146
|
+
|
|
147
|
+
// Git branch detection
|
|
148
|
+
let gitBranch = '';
|
|
149
|
+
const gitCheck = exec('git rev-parse --git-dir');
|
|
150
|
+
if (gitCheck) {
|
|
151
|
+
gitBranch = exec('git branch --show-current');
|
|
152
|
+
if (!gitBranch) {
|
|
153
|
+
gitBranch = exec('git rev-parse --short HEAD');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ccusage integration
|
|
158
|
+
let sessionText = '';
|
|
159
|
+
let sessionPercent = 0;
|
|
160
|
+
let costUSD = '';
|
|
161
|
+
let costPerHour = '';
|
|
162
|
+
let totalTokens = '';
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// Try npx first, then ccusage
|
|
166
|
+
let blocksOutput = exec('npx ccusage@latest blocks --json');
|
|
167
|
+
if (!blocksOutput) {
|
|
168
|
+
blocksOutput = exec('ccusage blocks --json');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (blocksOutput) {
|
|
172
|
+
const blocks = JSON.parse(blocksOutput);
|
|
173
|
+
const activeBlock = blocks.blocks?.find(b => b.isActive === true);
|
|
174
|
+
|
|
175
|
+
if (activeBlock) {
|
|
176
|
+
costUSD = activeBlock.costUSD || '';
|
|
177
|
+
costPerHour = activeBlock.burnRate?.costPerHour || '';
|
|
178
|
+
totalTokens = activeBlock.totalTokens || '';
|
|
179
|
+
|
|
180
|
+
// Session time calculation
|
|
181
|
+
const resetTimeStr = activeBlock.usageLimitResetTime || activeBlock.endTime;
|
|
182
|
+
const startTimeStr = activeBlock.startTime;
|
|
183
|
+
|
|
184
|
+
if (resetTimeStr && startTimeStr) {
|
|
185
|
+
const startSec = toEpoch(startTimeStr);
|
|
186
|
+
const endSec = toEpoch(resetTimeStr);
|
|
187
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
188
|
+
|
|
189
|
+
let total = endSec - startSec;
|
|
190
|
+
if (total < 1) total = 1;
|
|
191
|
+
|
|
192
|
+
let elapsed = nowSec - startSec;
|
|
193
|
+
if (elapsed < 0) elapsed = 0;
|
|
194
|
+
if (elapsed > total) elapsed = total;
|
|
195
|
+
|
|
196
|
+
sessionPercent = Math.floor(elapsed * 100 / total);
|
|
197
|
+
let remaining = endSec - nowSec;
|
|
198
|
+
if (remaining < 0) remaining = 0;
|
|
199
|
+
|
|
200
|
+
const rh = Math.floor(remaining / 3600);
|
|
201
|
+
const rm = Math.floor((remaining % 3600) / 60);
|
|
202
|
+
const endHM = formatTimeHM(endSec);
|
|
203
|
+
|
|
204
|
+
sessionText = `${rh}h ${rm}m until reset at ${endHM}`;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
// Silent fail - ccusage not available or error
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Render statusline
|
|
213
|
+
let output = '';
|
|
214
|
+
|
|
215
|
+
// Directory
|
|
216
|
+
output += `📁 ${DirColor}${currentDir}${Reset}`;
|
|
217
|
+
|
|
218
|
+
// Git branch
|
|
219
|
+
if (gitBranch) {
|
|
220
|
+
output += ` 🌿 ${GitColor}${gitBranch}${Reset}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Model
|
|
224
|
+
output += ` 🤖 ${ModelColor}${modelName}${Reset}`;
|
|
225
|
+
|
|
226
|
+
// Model version
|
|
227
|
+
if (modelVersion) {
|
|
228
|
+
output += ` 🏷️ ${VersionColor}${modelVersion}${Reset}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Session time
|
|
232
|
+
if (sessionText) {
|
|
233
|
+
const sessionColorCode = getSessionColor(sessionPercent);
|
|
234
|
+
output += ` ⌛ ${sessionColorCode}${sessionText}${Reset}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Cost
|
|
238
|
+
if (costUSD && /^\d+(\.\d+)?$/.test(costUSD)) {
|
|
239
|
+
const costUSDNum = parseFloat(costUSD);
|
|
240
|
+
if (costPerHour && /^\d+(\.\d+)?$/.test(costPerHour)) {
|
|
241
|
+
const costPerHourNum = parseFloat(costPerHour);
|
|
242
|
+
output += ` 💵 ${CostColor}$${costUSDNum.toFixed(2)} ($${costPerHourNum.toFixed(2)}/h)${Reset}`;
|
|
243
|
+
} else {
|
|
244
|
+
output += ` 💵 ${CostColor}$${costUSDNum.toFixed(2)}${Reset}`;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Tokens
|
|
249
|
+
if (totalTokens && /^\d+$/.test(totalTokens.toString())) {
|
|
250
|
+
output += ` 📊 ${UsageColor}${totalTokens} tok${Reset}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
console.log(output);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
console.error('Error:', err.message);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
main().catch(err => {
|
|
261
|
+
console.error('Fatal error:', err);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
});
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
#Requires -Version 5.1
|
|
2
|
+
# Custom Claude Code statusline for PowerShell
|
|
3
|
+
# Cross-platform support: Windows PowerShell 5.1+, PowerShell Core 7+
|
|
4
|
+
# Theme: detailed | Colors: true | Features: directory, git, model, usage, session, tokens
|
|
5
|
+
|
|
6
|
+
# Set UTF-8 encoding
|
|
7
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
8
|
+
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
9
|
+
|
|
10
|
+
# Enable virtual terminal sequences for ANSI colors
|
|
11
|
+
function Enable-VirtualTerminal {
|
|
12
|
+
if ($PSVersionTable.PSVersion.Major -ge 6) {
|
|
13
|
+
# PowerShell Core 7+ has built-in support
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# Windows PowerShell 5.1 - enable virtual terminal
|
|
18
|
+
try {
|
|
19
|
+
$null = [System.Console]::OutputEncoding
|
|
20
|
+
if ($PSVersionTable.PSVersion.Major -eq 5) {
|
|
21
|
+
# Attempt to enable virtual terminal processing
|
|
22
|
+
$signature = @'
|
|
23
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
24
|
+
public static extern IntPtr GetStdHandle(int nStdHandle);
|
|
25
|
+
|
|
26
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
27
|
+
public static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);
|
|
28
|
+
|
|
29
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
30
|
+
public static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);
|
|
31
|
+
'@
|
|
32
|
+
$type = Add-Type -MemberDefinition $signature -Name 'WinAPI' -Namespace 'VirtualTerminal' -PassThru -ErrorAction SilentlyContinue
|
|
33
|
+
if ($type) {
|
|
34
|
+
$handle = $type::GetStdHandle(-11) # STD_OUTPUT_HANDLE
|
|
35
|
+
$mode = 0
|
|
36
|
+
$null = $type::GetConsoleMode($handle, [ref]$mode)
|
|
37
|
+
$mode = $mode -bor 0x0004 # ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
|
38
|
+
$null = $type::SetConsoleMode($handle, $mode)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
# Silent fail - colors just won't work
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Initialize color support
|
|
48
|
+
$script:UseColor = $true
|
|
49
|
+
if ($env:NO_COLOR) {
|
|
50
|
+
$script:UseColor = $false
|
|
51
|
+
}
|
|
52
|
+
elseif (-not [Console]::IsOutputRedirected -and [Console]::OutputEncoding) {
|
|
53
|
+
Enable-VirtualTerminal
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
$script:UseColor = $false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Color helper functions
|
|
60
|
+
function Get-Color {
|
|
61
|
+
param([string]$Code)
|
|
62
|
+
if ($script:UseColor) { return "`e[${Code}m" }
|
|
63
|
+
return ""
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function Get-Reset {
|
|
67
|
+
if ($script:UseColor) { return "`e[0m" }
|
|
68
|
+
return ""
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Color definitions
|
|
72
|
+
$DirColor = Get-Color "1;36" # cyan
|
|
73
|
+
$GitColor = Get-Color "1;32" # green
|
|
74
|
+
$ModelColor = Get-Color "1;35" # magenta
|
|
75
|
+
$VersionColor = Get-Color "1;33" # yellow
|
|
76
|
+
$UsageColor = Get-Color "1;35" # magenta
|
|
77
|
+
$CostColor = Get-Color "1;36" # cyan
|
|
78
|
+
$Reset = Get-Reset
|
|
79
|
+
|
|
80
|
+
# Time conversion functions
|
|
81
|
+
function ConvertTo-Epoch {
|
|
82
|
+
param([string]$Timestamp)
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
# Parse ISO8601 timestamp
|
|
86
|
+
$dt = [DateTime]::Parse($Timestamp).ToUniversalTime()
|
|
87
|
+
$epoch = [DateTimeOffset]::new($dt).ToUnixTimeSeconds()
|
|
88
|
+
return $epoch
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return 0
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function Format-TimeHM {
|
|
96
|
+
param([long]$Epoch)
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
$dt = [DateTimeOffset]::FromUnixTimeSeconds($Epoch).LocalDateTime
|
|
100
|
+
return $dt.ToString("HH:mm")
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return "00:00"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function Get-ProgressBar {
|
|
108
|
+
param(
|
|
109
|
+
[int]$Percent = 0,
|
|
110
|
+
[int]$Width = 10
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if ($Percent -lt 0) { $Percent = 0 }
|
|
114
|
+
if ($Percent -gt 100) { $Percent = 100 }
|
|
115
|
+
|
|
116
|
+
$filled = [Math]::Floor($Percent * $Width / 100)
|
|
117
|
+
$empty = $Width - $filled
|
|
118
|
+
|
|
119
|
+
$bar = ("=" * $filled) + ("-" * $empty)
|
|
120
|
+
return $bar
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function Get-SessionColor {
|
|
124
|
+
param([int]$SessionPercent)
|
|
125
|
+
|
|
126
|
+
if (-not $script:UseColor) { return "" }
|
|
127
|
+
|
|
128
|
+
$remaining = 100 - $SessionPercent
|
|
129
|
+
if ($remaining -le 10) {
|
|
130
|
+
return "`e[1;31m" # red
|
|
131
|
+
}
|
|
132
|
+
elseif ($remaining -le 25) {
|
|
133
|
+
return "`e[1;33m" # yellow
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
return "`e[1;32m" # green
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Read JSON from stdin
|
|
141
|
+
try {
|
|
142
|
+
$inputLines = @()
|
|
143
|
+
while ($null -ne ($line = [Console]::In.ReadLine())) {
|
|
144
|
+
$inputLines += $line
|
|
145
|
+
}
|
|
146
|
+
$inputJson = $inputLines -join "`n"
|
|
147
|
+
|
|
148
|
+
if ([string]::IsNullOrWhiteSpace($inputJson)) {
|
|
149
|
+
Write-Error "No input provided"
|
|
150
|
+
exit 1
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
$data = $inputJson | ConvertFrom-Json
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
Write-Error "Failed to parse JSON input: $_"
|
|
157
|
+
exit 1
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# Extract basic information
|
|
161
|
+
$currentDir = "unknown"
|
|
162
|
+
if ($data.workspace.current_dir) {
|
|
163
|
+
$currentDir = $data.workspace.current_dir
|
|
164
|
+
}
|
|
165
|
+
elseif ($data.cwd) {
|
|
166
|
+
$currentDir = $data.cwd
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# Replace home directory with ~
|
|
170
|
+
$homeDir = $env:USERPROFILE
|
|
171
|
+
if (-not $homeDir) {
|
|
172
|
+
$homeDir = $env:HOME
|
|
173
|
+
}
|
|
174
|
+
if ($homeDir -and $currentDir.StartsWith($homeDir)) {
|
|
175
|
+
$currentDir = $currentDir.Replace($homeDir, "~")
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
$modelName = "Claude"
|
|
179
|
+
if ($data.model.display_name) {
|
|
180
|
+
$modelName = $data.model.display_name
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
$modelVersion = ""
|
|
184
|
+
if ($data.model.version -and $data.model.version -ne "null") {
|
|
185
|
+
$modelVersion = $data.model.version
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# Git branch detection
|
|
189
|
+
$gitBranch = ""
|
|
190
|
+
try {
|
|
191
|
+
$null = git rev-parse --git-dir 2>$null
|
|
192
|
+
if ($LASTEXITCODE -eq 0) {
|
|
193
|
+
$gitBranch = git branch --show-current 2>$null
|
|
194
|
+
if ([string]::IsNullOrWhiteSpace($gitBranch)) {
|
|
195
|
+
$gitBranch = git rev-parse --short HEAD 2>$null
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
# Not in a git repository
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# ccusage integration
|
|
204
|
+
$sessionText = ""
|
|
205
|
+
$sessionPercent = 0
|
|
206
|
+
$sessionBar = ""
|
|
207
|
+
$costUSD = ""
|
|
208
|
+
$costPerHour = ""
|
|
209
|
+
$totalTokens = ""
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
# Try npx first, then ccusage
|
|
213
|
+
$blocksOutput = $null
|
|
214
|
+
try {
|
|
215
|
+
$blocksOutput = npx ccusage@latest blocks --json 2>$null
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
try {
|
|
219
|
+
$blocksOutput = ccusage blocks --json 2>$null
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
# ccusage not available
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if ($blocksOutput) {
|
|
227
|
+
$blocks = $blocksOutput | ConvertFrom-Json
|
|
228
|
+
$activeBlock = $blocks.blocks | Where-Object { $_.isActive -eq $true } | Select-Object -First 1
|
|
229
|
+
|
|
230
|
+
if ($activeBlock) {
|
|
231
|
+
if ($activeBlock.costUSD) {
|
|
232
|
+
$costUSD = $activeBlock.costUSD
|
|
233
|
+
}
|
|
234
|
+
if ($activeBlock.burnRate.costPerHour) {
|
|
235
|
+
$costPerHour = $activeBlock.burnRate.costPerHour
|
|
236
|
+
}
|
|
237
|
+
if ($activeBlock.totalTokens) {
|
|
238
|
+
$totalTokens = $activeBlock.totalTokens
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# Session time calculation
|
|
242
|
+
$resetTimeStr = $activeBlock.usageLimitResetTime
|
|
243
|
+
if (-not $resetTimeStr) {
|
|
244
|
+
$resetTimeStr = $activeBlock.endTime
|
|
245
|
+
}
|
|
246
|
+
$startTimeStr = $activeBlock.startTime
|
|
247
|
+
|
|
248
|
+
if ($resetTimeStr -and $startTimeStr) {
|
|
249
|
+
$startSec = ConvertTo-Epoch $startTimeStr
|
|
250
|
+
$endSec = ConvertTo-Epoch $resetTimeStr
|
|
251
|
+
$nowSec = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
|
252
|
+
|
|
253
|
+
$total = $endSec - $startSec
|
|
254
|
+
if ($total -lt 1) { $total = 1 }
|
|
255
|
+
|
|
256
|
+
$elapsed = $nowSec - $startSec
|
|
257
|
+
if ($elapsed -lt 0) { $elapsed = 0 }
|
|
258
|
+
if ($elapsed -gt $total) { $elapsed = $total }
|
|
259
|
+
|
|
260
|
+
$sessionPercent = [Math]::Floor($elapsed * 100 / $total)
|
|
261
|
+
$remaining = $endSec - $nowSec
|
|
262
|
+
if ($remaining -lt 0) { $remaining = 0 }
|
|
263
|
+
|
|
264
|
+
$rh = [Math]::Floor($remaining / 3600)
|
|
265
|
+
$rm = [Math]::Floor(($remaining % 3600) / 60)
|
|
266
|
+
$endHM = Format-TimeHM $endSec
|
|
267
|
+
|
|
268
|
+
$sessionText = "${rh}h ${rm}m until reset at ${endHM}"
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
# Directory
|
|
273
|
+
$output += "📁 ${DirColor}${currentDir}${Reset}"
|
|
274
|
+
|
|
275
|
+
# Git branch
|
|
276
|
+
if ($gitBranch) {
|
|
277
|
+
$output += " 🌿 ${GitColor}${gitBranch}${Reset}"
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# Model
|
|
281
|
+
$output += " 🤖 ${ModelColor}${modelName}${Reset}"
|
|
282
|
+
|
|
283
|
+
# Model version
|
|
284
|
+
if ($modelVersion) {
|
|
285
|
+
$output += " 🏷️ ${VersionColor}${modelVersion}${Reset}"
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
# Session time
|
|
289
|
+
if ($sessionText) {
|
|
290
|
+
$sessionColorCode = Get-SessionColor $sessionPercent
|
|
291
|
+
$output += " ⌛ ${sessionColorCode}${sessionText}${Reset}"
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
# Cost
|
|
295
|
+
if ($costUSD -and $costUSD -match '^\d+(\.\d+)?$') {
|
|
296
|
+
if ($costPerHour -and $costPerHour -match '^\d+(\.\d+)?$') {
|
|
297
|
+
$costUSDFormatted = [string]::Format("{0:F2}", [double]$costUSD)
|
|
298
|
+
$costPerHourFormatted = [string]::Format("{0:F2}", [double]$costPerHour)
|
|
299
|
+
$output += " 💵 ${CostColor}`$$costUSDFormatted (`$$costPerHourFormatted/h)${Reset}"
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
$costUSDFormatted = [string]::Format("{0:F2}", [double]$costUSD)
|
|
303
|
+
$output += " 💵 ${CostColor}`$$costUSDFormatted${Reset}"
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
# Tokens
|
|
308
|
+
if ($totalTokens -and $totalTokens -match '^\d+$') {
|
|
309
|
+
$output += " 📊 ${UsageColor}${totalTokens} tok${Reset}"
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
Write-Host $output
|