aiblueprint-cli 1.4.81 → 1.4.83
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 +25 -18
- package/agents-config/claude-config/scripts/statusline/data/.gitignore +8 -0
- package/agents-config/claude-config/scripts/statusline/data/.gitkeep +0 -0
- package/agents-config/claude-config/scripts/statusline/defaults.json +10 -10
- package/agents-config/claude-config/scripts/statusline/src/commands/interactive-config.ts +118 -6
- package/agents-config/claude-config/scripts/statusline/src/index.ts +100 -9
- package/agents-config/claude-config/scripts/statusline/src/lib/config.ts +28 -2
- package/agents-config/claude-config/scripts/statusline/src/lib/features/limits/commands/weekly-analysis.ts +108 -0
- package/agents-config/claude-config/scripts/statusline/src/lib/features/limits/index.ts +164 -0
- package/agents-config/claude-config/scripts/statusline/src/lib/features/limits/types.ts +15 -0
- package/agents-config/claude-config/scripts/statusline/src/lib/features/spend/commands/migrate-to-sqlite.ts +136 -0
- package/agents-config/claude-config/scripts/statusline/src/lib/features/spend/commands/spend-day.ts +79 -0
- package/agents-config/claude-config/scripts/statusline/src/lib/features/spend/commands/spend-month.ts +66 -0
- package/agents-config/claude-config/scripts/statusline/src/lib/features/spend/commands/spend-project.ts +85 -0
- package/agents-config/claude-config/scripts/statusline/src/lib/features/spend/database.ts +395 -0
- package/agents-config/claude-config/scripts/statusline/src/lib/features/spend/index.ts +178 -0
- package/agents-config/claude-config/scripts/statusline/src/lib/features/spend/payload-logger.ts +163 -0
- package/agents-config/claude-config/scripts/statusline/src/lib/features/spend/types.ts +37 -0
- package/agents-config/claude-config/scripts/statusline/src/lib/presets.ts +13 -13
- package/agents-config/claude-config/scripts/statusline/statusline.config.free.json +4 -1
- package/agents-config/claude-config/scripts/statusline/statusline.config.json +44 -23
- package/agents-config/scripts/statusline/data/.gitignore +8 -0
- package/agents-config/scripts/statusline/data/.gitkeep +0 -0
- package/agents-config/scripts/statusline/defaults.json +10 -10
- package/agents-config/scripts/statusline/src/commands/interactive-config.ts +118 -6
- package/agents-config/scripts/statusline/src/index.ts +100 -9
- package/agents-config/scripts/statusline/src/lib/config.ts +28 -2
- package/agents-config/scripts/statusline/src/lib/features/limits/commands/weekly-analysis.ts +108 -0
- package/agents-config/scripts/statusline/src/lib/features/limits/index.ts +164 -0
- package/agents-config/scripts/statusline/src/lib/features/limits/types.ts +15 -0
- package/agents-config/scripts/statusline/src/lib/features/spend/commands/migrate-to-sqlite.ts +136 -0
- package/agents-config/scripts/statusline/src/lib/features/spend/commands/spend-day.ts +79 -0
- package/agents-config/scripts/statusline/src/lib/features/spend/commands/spend-month.ts +66 -0
- package/agents-config/scripts/statusline/src/lib/features/spend/commands/spend-project.ts +85 -0
- package/agents-config/scripts/statusline/src/lib/features/spend/database.ts +395 -0
- package/agents-config/scripts/statusline/src/lib/features/spend/index.ts +178 -0
- package/agents-config/scripts/statusline/src/lib/features/spend/payload-logger.ts +163 -0
- package/agents-config/scripts/statusline/src/lib/features/spend/types.ts +37 -0
- package/agents-config/scripts/statusline/src/lib/presets.ts +13 -13
- package/agents-config/scripts/statusline/statusline.config.free.json +4 -1
- package/agents-config/scripts/statusline/statusline.config.json +44 -23
- package/agents-config/skills/{agents-managers → agents-manager}/SKILL.md +1 -1
- package/agents-config/skills/{agents-managers → agents-manager}/references/writing-agent-prompts.md +1 -1
- package/agents-config/skills/apex/SKILL.md +61 -0
- package/agents-config/skills/appstore-connect/SKILL.md +2 -2
- package/agents-config/skills/{appstore-connect-setup/SKILL.md → appstore-connect/references/setup.md} +3 -6
- package/agents-config/skills/environments-manager/SKILL.md +6 -6
- package/agents-config/skills/environments-manager/examples/{claude/commands/dev.md → skills/dev/SKILL.md} +2 -1
- package/agents-config/skills/environments-manager/examples/{claude/commands/lint.md → skills/lint/SKILL.md} +2 -1
- package/agents-config/skills/environments-manager/examples/{claude/commands/test.md → skills/test/SKILL.md} +2 -1
- package/agents-config/skills/environments-manager/examples/{claude/commands/typecheck.md → skills/typecheck/SKILL.md} +2 -1
- package/agents-config/skills/environments-manager/references/claude.md +13 -9
- package/agents-config/skills/environments-manager/references/cursor.md +2 -2
- package/agents-config/skills/ios-testflight/SKILL.md +2 -2
- package/agents-config/{commands/prompts/prompt.md → skills/prompt/SKILL.md} +5 -0
- package/agents-config/{commands/prompts/tools.md → skills/tools/SKILL.md} +1 -0
- package/agents-config/skills/ultrathink/SKILL.md +1 -1
- package/agents-config/skills/use-style/SKILL.md +7 -4
- package/agents-config/skills/use-style/examples/luma.html +221 -0
- package/agents-config/skills/use-style/examples/testspirite.html +340 -0
- package/agents-config/skills/use-style/styles/ios-app.md +350 -0
- package/agents-config/skills/use-style/styles/luma.md +422 -0
- package/agents-config/skills/use-style/styles/new-york-times.md +1 -1
- package/agents-config/skills/use-style/styles/split-auth.md +2 -2
- package/agents-config/skills/use-style/styles/testspirite.md +397 -0
- package/dist/cli.js +56 -118
- package/package.json +1 -1
- package/agents-config/claude-config/scripts/.claude/commands/fix-on-my-computer.md +0 -87
- package/agents-config/commands/prompts/create-vitejs-app.md +0 -272
- package/agents-config/commands/prompts/nextjs-add-prisma-db.md +0 -136
- package/agents-config/commands/prompts/nextjs-setup-better-auth.md +0 -173
- package/agents-config/commands/prompts/nextjs-setup-project.md +0 -200
- package/agents-config/commands/prompts/saas-challenge-idea.md +0 -135
- package/agents-config/commands/prompts/saas-create-architecture.md +0 -242
- package/agents-config/commands/prompts/saas-create-headline.md +0 -132
- package/agents-config/commands/prompts/saas-create-landing-copywritting.md +0 -267
- package/agents-config/commands/prompts/saas-create-legals-docs.md +0 -176
- package/agents-config/commands/prompts/saas-create-logos.md +0 -240
- package/agents-config/commands/prompts/saas-create-prd.md +0 -195
- package/agents-config/commands/prompts/saas-create-tasks.md +0 -240
- package/agents-config/commands/prompts/saas-define-pricing.md +0 -293
- package/agents-config/commands/prompts/saas-find-domain-name.md +0 -190
- package/agents-config/commands/prompts/saas-implement-landing-page.md +0 -257
- package/agents-config/commands/prompts/setup-tmux.md +0 -160
- /package/agents-config/skills/{agents-managers → agents-manager}/references/agents.md +0 -0
- /package/agents-config/skills/{agents-managers → agents-manager}/references/context-management.md +0 -0
- /package/agents-config/skills/{agents-managers → agents-manager}/references/debugging-agents.md +0 -0
- /package/agents-config/skills/{agents-managers → agents-manager}/references/error-handling-and-recovery.md +0 -0
- /package/agents-config/skills/{agents-managers → agents-manager}/references/evaluation-and-testing.md +0 -0
- /package/agents-config/skills/{agents-managers → agents-manager}/references/orchestration-patterns.md +0 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
loadConfig as loadStatuslineConfig,
|
|
7
|
+
type StatuslineConfig,
|
|
8
|
+
} from "./lib/config";
|
|
6
9
|
import { getContextData } from "./lib/context";
|
|
7
10
|
import {
|
|
8
11
|
colors,
|
|
@@ -19,6 +22,45 @@ import {
|
|
|
19
22
|
} from "./lib/render-pure";
|
|
20
23
|
import type { HookInput } from "./lib/types";
|
|
21
24
|
|
|
25
|
+
// Optional feature imports - just delete the folder to disable!
|
|
26
|
+
let getUsageLimits: any = null;
|
|
27
|
+
let normalizeResetsAt: any = null;
|
|
28
|
+
let getPeriodCost: any = null;
|
|
29
|
+
let getTodayCostV2: any = null;
|
|
30
|
+
let saveSessionV2: any = null;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const limitsModule = await import("./lib/features/limits");
|
|
34
|
+
getUsageLimits = limitsModule.getUsageLimits;
|
|
35
|
+
} catch {
|
|
36
|
+
// Limits feature not available - that's OK!
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const utilsModule = await import("./lib/utils");
|
|
41
|
+
normalizeResetsAt = utilsModule.normalizeResetsAt;
|
|
42
|
+
} catch {
|
|
43
|
+
// Fallback normalizeResetsAt
|
|
44
|
+
normalizeResetsAt = (resetsAt: string) => resetsAt;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let getTodayRealCost: (() => number) | null = null;
|
|
48
|
+
try {
|
|
49
|
+
const spendModule = await import("./lib/features/spend");
|
|
50
|
+
getPeriodCost = spendModule.getPeriodCost;
|
|
51
|
+
getTodayCostV2 = spendModule.getTodayCostV2;
|
|
52
|
+
saveSessionV2 = spendModule.saveSessionV2;
|
|
53
|
+
} catch {
|
|
54
|
+
// Spend tracking feature not available - that's OK!
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const loggerModule = await import("./lib/features/spend/payload-logger");
|
|
59
|
+
getTodayRealCost = loggerModule.getTodayRealCost;
|
|
60
|
+
} catch {
|
|
61
|
+
// Payload logger not available
|
|
62
|
+
}
|
|
63
|
+
|
|
22
64
|
// Re-export from render-pure for backwards compatibility
|
|
23
65
|
export {
|
|
24
66
|
renderStatusline,
|
|
@@ -26,7 +68,12 @@ export {
|
|
|
26
68
|
type UsageLimit,
|
|
27
69
|
} from "./lib/render-pure";
|
|
28
70
|
|
|
29
|
-
const
|
|
71
|
+
const LAST_PAYLOAD_PATH = join(
|
|
72
|
+
import.meta.dir,
|
|
73
|
+
"..",
|
|
74
|
+
"data",
|
|
75
|
+
"last_payload.txt",
|
|
76
|
+
);
|
|
30
77
|
const CLAUDE_SETTINGS_PATH = join(
|
|
31
78
|
process.env.HOME || "",
|
|
32
79
|
".claude",
|
|
@@ -48,21 +95,30 @@ async function loadClaudeSettings(): Promise<ClaudeSettings> {
|
|
|
48
95
|
}
|
|
49
96
|
|
|
50
97
|
async function loadConfig(): Promise<StatuslineConfig> {
|
|
51
|
-
|
|
52
|
-
const content = await readFile(CONFIG_FILE_PATH, "utf-8");
|
|
53
|
-
return JSON.parse(content);
|
|
54
|
-
} catch {
|
|
55
|
-
return defaultConfig;
|
|
56
|
-
}
|
|
98
|
+
return loadStatuslineConfig();
|
|
57
99
|
}
|
|
58
100
|
|
|
59
101
|
async function main() {
|
|
60
102
|
try {
|
|
61
103
|
const input: HookInput = await Bun.stdin.json();
|
|
62
104
|
|
|
105
|
+
// Save last payload for debugging
|
|
106
|
+
await writeFile(LAST_PAYLOAD_PATH, JSON.stringify(input, null, 2));
|
|
107
|
+
|
|
63
108
|
const config = await loadConfig();
|
|
64
109
|
const claudeSettings = await loadClaudeSettings();
|
|
65
110
|
|
|
111
|
+
// Get usage limits (if feature exists)
|
|
112
|
+
const usageLimits = getUsageLimits
|
|
113
|
+
? await getUsageLimits()
|
|
114
|
+
: { five_hour: null, seven_day: null };
|
|
115
|
+
const currentResetsAt = usageLimits.five_hour?.resets_at ?? undefined;
|
|
116
|
+
|
|
117
|
+
// Save session with current period context (if feature exists)
|
|
118
|
+
if (saveSessionV2) {
|
|
119
|
+
await saveSessionV2(input, currentResetsAt);
|
|
120
|
+
}
|
|
121
|
+
|
|
66
122
|
const git = await getGitStatus();
|
|
67
123
|
|
|
68
124
|
let contextTokens: number | null;
|
|
@@ -102,6 +158,24 @@ async function main() {
|
|
|
102
158
|
contextPercentage = contextData.percentage;
|
|
103
159
|
}
|
|
104
160
|
|
|
161
|
+
// Get period cost from SQLite (if feature exists)
|
|
162
|
+
let periodCost: number | undefined;
|
|
163
|
+
let todayCost: number | undefined;
|
|
164
|
+
|
|
165
|
+
if (getPeriodCost && normalizeResetsAt) {
|
|
166
|
+
const normalizedPeriodId = currentResetsAt
|
|
167
|
+
? normalizeResetsAt(currentResetsAt)
|
|
168
|
+
: null;
|
|
169
|
+
periodCost = normalizedPeriodId ? getPeriodCost(normalizedPeriodId) : 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Get today's cost from payloads (more accurate than DB)
|
|
173
|
+
if (getTodayRealCost) {
|
|
174
|
+
todayCost = getTodayRealCost();
|
|
175
|
+
} else if (getTodayCostV2) {
|
|
176
|
+
todayCost = getTodayCostV2();
|
|
177
|
+
}
|
|
178
|
+
|
|
105
179
|
const data: StatuslineData = {
|
|
106
180
|
branch: formatBranch(git, config.git),
|
|
107
181
|
dirPath: formatPath(input.workspace.current_dir, config.pathDisplayMode),
|
|
@@ -121,6 +195,23 @@ async function main() {
|
|
|
121
195
|
sessionDuration: formatDuration(input.cost.total_duration_ms),
|
|
122
196
|
contextTokens,
|
|
123
197
|
contextPercentage,
|
|
198
|
+
...(getUsageLimits && {
|
|
199
|
+
usageLimits: {
|
|
200
|
+
five_hour: usageLimits.five_hour
|
|
201
|
+
? {
|
|
202
|
+
utilization: usageLimits.five_hour.utilization,
|
|
203
|
+
resets_at: usageLimits.five_hour.resets_at,
|
|
204
|
+
}
|
|
205
|
+
: null,
|
|
206
|
+
seven_day: usageLimits.seven_day
|
|
207
|
+
? {
|
|
208
|
+
utilization: usageLimits.seven_day.utilization,
|
|
209
|
+
resets_at: usageLimits.seven_day.resets_at,
|
|
210
|
+
}
|
|
211
|
+
: null,
|
|
212
|
+
},
|
|
213
|
+
}),
|
|
214
|
+
...((getPeriodCost || getTodayCostV2) && { periodCost, todayCost }),
|
|
124
215
|
thinkingEnabled: claudeSettings.alwaysThinkingEnabled ?? true,
|
|
125
216
|
};
|
|
126
217
|
|
|
@@ -10,11 +10,37 @@ export const defaultConfig: StatuslineConfig = JSON.parse(
|
|
|
10
10
|
readFileSync(DEFAULTS_PATH, "utf-8"),
|
|
11
11
|
);
|
|
12
12
|
|
|
13
|
+
function cloneDefaultConfig(): StatuslineConfig {
|
|
14
|
+
return JSON.parse(JSON.stringify(defaultConfig));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
18
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function mergeConfigDefaults<T>(defaults: T, overrides: unknown): T {
|
|
22
|
+
if (!isRecord(defaults) || !isRecord(overrides)) {
|
|
23
|
+
return overrides === undefined ? defaults : (overrides as T);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const merged: Record<string, unknown> = { ...defaults };
|
|
27
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
28
|
+
merged[key] = mergeConfigDefaults(
|
|
29
|
+
(defaults as Record<string, unknown>)[key],
|
|
30
|
+
value,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return merged as T;
|
|
34
|
+
}
|
|
35
|
+
|
|
13
36
|
export function loadConfig(): StatuslineConfig {
|
|
14
37
|
try {
|
|
15
|
-
return
|
|
38
|
+
return mergeConfigDefaults(
|
|
39
|
+
defaultConfig,
|
|
40
|
+
JSON.parse(readFileSync(CONFIG_PATH, "utf-8")),
|
|
41
|
+
);
|
|
16
42
|
} catch {
|
|
17
|
-
return
|
|
43
|
+
return cloneDefaultConfig();
|
|
18
44
|
}
|
|
19
45
|
}
|
|
20
46
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { colors, formatProgressBar } from "../../../formatters";
|
|
4
|
+
import { getUsageLimits } from "../index";
|
|
5
|
+
|
|
6
|
+
const WEEKLY_HOURS = 168; // 7 days * 24 hours
|
|
7
|
+
|
|
8
|
+
function calculateTimeProgress(resetsAt: string | null): {
|
|
9
|
+
hoursRemaining: number;
|
|
10
|
+
hoursElapsed: number;
|
|
11
|
+
timeElapsedPercent: number;
|
|
12
|
+
} {
|
|
13
|
+
if (!resetsAt) {
|
|
14
|
+
return {
|
|
15
|
+
hoursRemaining: 0,
|
|
16
|
+
hoursElapsed: WEEKLY_HOURS,
|
|
17
|
+
timeElapsedPercent: 100,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const resetDate = new Date(resetsAt);
|
|
22
|
+
const now = new Date();
|
|
23
|
+
const diffMs = resetDate.getTime() - now.getTime();
|
|
24
|
+
const hoursRemaining = Math.max(0, diffMs / 3600000);
|
|
25
|
+
const hoursElapsed = WEEKLY_HOURS - hoursRemaining;
|
|
26
|
+
const timeElapsedPercent = (hoursElapsed / WEEKLY_HOURS) * 100;
|
|
27
|
+
|
|
28
|
+
return { hoursRemaining, hoursElapsed, timeElapsedPercent };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatDelta(delta: number): string {
|
|
32
|
+
const sign = delta >= 0 ? "+" : "";
|
|
33
|
+
const value = `${sign}${delta.toFixed(1)}%`;
|
|
34
|
+
|
|
35
|
+
if (delta > 5) return colors.green(value);
|
|
36
|
+
if (delta > 0) return colors.lightGray(value);
|
|
37
|
+
if (delta > -5) return colors.yellow(value);
|
|
38
|
+
if (delta > -15) return colors.orange(value);
|
|
39
|
+
return colors.red(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function main() {
|
|
43
|
+
const limits = await getUsageLimits();
|
|
44
|
+
|
|
45
|
+
console.log("");
|
|
46
|
+
console.log(colors.bold("📊 Weekly Usage Analysis"));
|
|
47
|
+
console.log(colors.gray("─".repeat(40)));
|
|
48
|
+
|
|
49
|
+
if (!limits.seven_day) {
|
|
50
|
+
console.log(colors.red("No weekly limit data available"));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { utilization, resets_at } = limits.seven_day;
|
|
55
|
+
const { timeElapsedPercent } = calculateTimeProgress(resets_at);
|
|
56
|
+
|
|
57
|
+
const expectedUsage = timeElapsedPercent;
|
|
58
|
+
const delta = utilization - expectedUsage;
|
|
59
|
+
|
|
60
|
+
console.log("");
|
|
61
|
+
console.log(colors.gray("Weekly Limits Overview"));
|
|
62
|
+
console.log(colors.gray("─".repeat(50)));
|
|
63
|
+
console.log("");
|
|
64
|
+
|
|
65
|
+
const usageBar = formatProgressBar({
|
|
66
|
+
percentage: utilization,
|
|
67
|
+
length: 20,
|
|
68
|
+
style: "filled",
|
|
69
|
+
colorMode: "blue",
|
|
70
|
+
background: "none",
|
|
71
|
+
});
|
|
72
|
+
console.log(
|
|
73
|
+
`${colors.blue("Usage")} ${usageBar} ${colors.lightGray(utilization.toFixed(1))}${colors.gray("%")}`,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const timeBar = formatProgressBar({
|
|
77
|
+
percentage: timeElapsedPercent,
|
|
78
|
+
length: 20,
|
|
79
|
+
style: "filled",
|
|
80
|
+
colorMode: "purple",
|
|
81
|
+
background: "none",
|
|
82
|
+
});
|
|
83
|
+
console.log(
|
|
84
|
+
`${colors.purple("Time ")} ${timeBar} ${colors.lightGray(timeElapsedPercent.toFixed(1))}${colors.gray("%")} ${colors.gray("·")} ${formatDelta(delta)}`,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
console.log("");
|
|
88
|
+
console.log(colors.gray("─".repeat(50)));
|
|
89
|
+
console.log("");
|
|
90
|
+
|
|
91
|
+
if (limits.five_hour) {
|
|
92
|
+
console.log(colors.gray("─".repeat(40)));
|
|
93
|
+
console.log("");
|
|
94
|
+
console.log(colors.gray("Session Limit (5h):"));
|
|
95
|
+
console.log(
|
|
96
|
+
` ${formatProgressBar({
|
|
97
|
+
percentage: limits.five_hour.utilization,
|
|
98
|
+
length: 15,
|
|
99
|
+
style: "filled",
|
|
100
|
+
colorMode: "progressive",
|
|
101
|
+
background: "none",
|
|
102
|
+
})} ${colors.lightGray(limits.five_hour.utilization.toString())}${colors.gray("%")}`,
|
|
103
|
+
);
|
|
104
|
+
console.log("");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
main();
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import type { CachedUsageLimits, UsageLimits } from "./types";
|
|
8
|
+
|
|
9
|
+
const CACHE_DURATION_MS = 60 * 1000; // 1 minute
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
|
|
12
|
+
function getCacheFilePath(): string {
|
|
13
|
+
const projectRoot = join(import.meta.dir, "..", "..", "..", "..");
|
|
14
|
+
return join(projectRoot, "data", "usage-limits-cache.json");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function getCredentials(): Promise<string | null> {
|
|
18
|
+
return (
|
|
19
|
+
(await getClaudeCodeTokenFromKeychain()) ?? getClaudeCodeTokenFromFile()
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function getClaudeCodeTokenFromKeychain(): Promise<string | null> {
|
|
24
|
+
if (process.platform !== "darwin") {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const { stdout } = await execFileAsync("security", [
|
|
30
|
+
"find-generic-password",
|
|
31
|
+
"-s",
|
|
32
|
+
"Claude Code-credentials",
|
|
33
|
+
"-w",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
return parseClaudeCodeToken(stdout.trim());
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function getClaudeCodeTokenFromFile(): Promise<string | null> {
|
|
43
|
+
try {
|
|
44
|
+
const credentialsPath = join(homedir(), ".claude", ".credentials.json");
|
|
45
|
+
if (!existsSync(credentialsPath)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const content = await readFile(credentialsPath, "utf-8");
|
|
50
|
+
return parseClaudeCodeToken(content);
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseClaudeCodeToken(content: string): string | null {
|
|
57
|
+
if (!content) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const credentials = JSON.parse(content);
|
|
63
|
+
return (
|
|
64
|
+
credentials.claudeAiOauth?.accessToken ?? credentials.accessToken ?? null
|
|
65
|
+
);
|
|
66
|
+
} catch {
|
|
67
|
+
return content.startsWith("sk-") ? content : null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function fetchUsageLimits(
|
|
72
|
+
token: string,
|
|
73
|
+
): Promise<UsageLimits | null> {
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
|
|
76
|
+
method: "GET",
|
|
77
|
+
headers: {
|
|
78
|
+
Accept: "application/json, text/plain, */*",
|
|
79
|
+
"Content-Type": "application/json",
|
|
80
|
+
"User-Agent": "claude-code/2.0.31",
|
|
81
|
+
Authorization: `Bearer ${token}`,
|
|
82
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
83
|
+
"Accept-Encoding": "gzip, compress, deflate, br",
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const data = await response.json();
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
five_hour: data.five_hour || null,
|
|
95
|
+
seven_day: data.seven_day || null,
|
|
96
|
+
};
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function loadCache(): Promise<CachedUsageLimits | null> {
|
|
103
|
+
try {
|
|
104
|
+
const cacheFile = getCacheFilePath();
|
|
105
|
+
if (!existsSync(cacheFile)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const content = await readFile(cacheFile, "utf-8");
|
|
110
|
+
const cached: CachedUsageLimits = JSON.parse(content);
|
|
111
|
+
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
if (now - cached.timestamp < CACHE_DURATION_MS) {
|
|
114
|
+
return cached;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return null;
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function saveCache(data: UsageLimits): Promise<void> {
|
|
124
|
+
try {
|
|
125
|
+
const cacheFile = getCacheFilePath();
|
|
126
|
+
const cached: CachedUsageLimits = {
|
|
127
|
+
data,
|
|
128
|
+
timestamp: Date.now(),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
await writeFile(cacheFile, JSON.stringify(cached, null, 2));
|
|
132
|
+
} catch {
|
|
133
|
+
// Fail silently
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function getUsageLimits(): Promise<UsageLimits> {
|
|
138
|
+
try {
|
|
139
|
+
const cached = await loadCache();
|
|
140
|
+
if (cached) {
|
|
141
|
+
return cached.data;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const token = await getCredentials();
|
|
145
|
+
|
|
146
|
+
if (!token) {
|
|
147
|
+
return { five_hour: null, seven_day: null };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const limits = await fetchUsageLimits(token);
|
|
151
|
+
|
|
152
|
+
if (!limits) {
|
|
153
|
+
return { five_hour: null, seven_day: null };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await saveCache(limits);
|
|
157
|
+
|
|
158
|
+
return limits;
|
|
159
|
+
} catch {
|
|
160
|
+
return { five_hour: null, seven_day: null };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export type { CachedUsageLimits, UsageLimits } from "./types";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface UsageLimits {
|
|
2
|
+
five_hour: {
|
|
3
|
+
utilization: number;
|
|
4
|
+
resets_at: string | null;
|
|
5
|
+
} | null;
|
|
6
|
+
seven_day: {
|
|
7
|
+
utilization: number;
|
|
8
|
+
resets_at: string | null;
|
|
9
|
+
} | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CachedUsageLimits {
|
|
13
|
+
data: UsageLimits;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { getDb, upsertPeriod, upsertSession } from "../database";
|
|
7
|
+
|
|
8
|
+
interface OldSpendSession {
|
|
9
|
+
id: string;
|
|
10
|
+
cost: number;
|
|
11
|
+
date: string;
|
|
12
|
+
duration_ms: number;
|
|
13
|
+
lines_added: number;
|
|
14
|
+
lines_removed: number;
|
|
15
|
+
cwd: string;
|
|
16
|
+
last_resets_at?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface OldSpendData {
|
|
20
|
+
sessions: OldSpendSession[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface OldDailyUsageEntry {
|
|
24
|
+
date: string;
|
|
25
|
+
utilization: number;
|
|
26
|
+
resets_at: string;
|
|
27
|
+
timestamp: number;
|
|
28
|
+
period_cost: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function loadOldSpendData(): Promise<OldSpendData | null> {
|
|
32
|
+
const projectRoot = join(import.meta.dir, "..", "..", "..", "..", "..");
|
|
33
|
+
const spendFile = join(projectRoot, "data", "spend.json");
|
|
34
|
+
|
|
35
|
+
if (!existsSync(spendFile)) {
|
|
36
|
+
console.log("No spend.json found, skipping session migration");
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const content = await readFile(spendFile, "utf-8");
|
|
41
|
+
return JSON.parse(content);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function loadOldDailyUsage(): Promise<OldDailyUsageEntry[]> {
|
|
45
|
+
const projectRoot = join(import.meta.dir, "..", "..", "..", "..", "..");
|
|
46
|
+
const dailyUsageFile = join(projectRoot, "data", "daily-usage.json");
|
|
47
|
+
|
|
48
|
+
if (!existsSync(dailyUsageFile)) {
|
|
49
|
+
console.log("No daily-usage.json found, skipping period migration");
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const content = await readFile(dailyUsageFile, "utf-8");
|
|
54
|
+
return JSON.parse(content);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function migrate() {
|
|
58
|
+
console.log("🔄 Starting migration to SQLite...\n");
|
|
59
|
+
|
|
60
|
+
const db = getDb();
|
|
61
|
+
console.log("✅ Database initialized\n");
|
|
62
|
+
|
|
63
|
+
const oldSpendData = await loadOldSpendData();
|
|
64
|
+
if (oldSpendData) {
|
|
65
|
+
console.log(`📦 Migrating ${oldSpendData.sessions.length} sessions...`);
|
|
66
|
+
|
|
67
|
+
let migrated = 0;
|
|
68
|
+
let skipped = 0;
|
|
69
|
+
|
|
70
|
+
for (const session of oldSpendData.sessions) {
|
|
71
|
+
try {
|
|
72
|
+
upsertSession({
|
|
73
|
+
session_id: session.id,
|
|
74
|
+
total_cost: session.cost,
|
|
75
|
+
cwd: session.cwd,
|
|
76
|
+
date: session.date,
|
|
77
|
+
duration_ms: session.duration_ms,
|
|
78
|
+
lines_added: session.lines_added,
|
|
79
|
+
lines_removed: session.lines_removed,
|
|
80
|
+
last_resets_at: session.last_resets_at ?? null,
|
|
81
|
+
cumulative_counted: 0,
|
|
82
|
+
});
|
|
83
|
+
migrated++;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(` ❌ Failed to migrate session ${session.id}:`, error);
|
|
86
|
+
skipped++;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log(` ✅ Migrated: ${migrated}`);
|
|
91
|
+
console.log(` ⚠️ Skipped: ${skipped}\n`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const oldDailyUsage = await loadOldDailyUsage();
|
|
95
|
+
if (oldDailyUsage.length > 0) {
|
|
96
|
+
console.log(`📅 Migrating ${oldDailyUsage.length} period entries...`);
|
|
97
|
+
|
|
98
|
+
let migrated = 0;
|
|
99
|
+
|
|
100
|
+
for (const entry of oldDailyUsage) {
|
|
101
|
+
try {
|
|
102
|
+
upsertPeriod({
|
|
103
|
+
period_id: entry.resets_at,
|
|
104
|
+
total_cost: 0,
|
|
105
|
+
utilization: entry.utilization,
|
|
106
|
+
date: entry.date,
|
|
107
|
+
});
|
|
108
|
+
migrated++;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(
|
|
111
|
+
` ❌ Failed to migrate period ${entry.resets_at}:`,
|
|
112
|
+
error,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(` ✅ Migrated: ${migrated}\n`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const sessionCount = db
|
|
121
|
+
.query<{ count: number }, []>("SELECT COUNT(*) as count FROM sessions")
|
|
122
|
+
.get();
|
|
123
|
+
const periodCount = db
|
|
124
|
+
.query<{ count: number }, []>("SELECT COUNT(*) as count FROM periods")
|
|
125
|
+
.get();
|
|
126
|
+
|
|
127
|
+
console.log("📊 Migration Summary:");
|
|
128
|
+
console.log(` Sessions in DB: ${sessionCount?.count ?? 0}`);
|
|
129
|
+
console.log(` Periods in DB: ${periodCount?.count ?? 0}`);
|
|
130
|
+
console.log("\n✅ Migration complete!");
|
|
131
|
+
console.log("\n⚠️ Note: Period costs have been reset to 0.");
|
|
132
|
+
console.log(" They will be recalculated correctly as you use Claude Code.");
|
|
133
|
+
console.log(" The old JSON files are preserved as backup.");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
migrate().catch(console.error);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { table } from "table";
|
|
4
|
+
import { formatCost, formatDuration } from "../../../formatters";
|
|
5
|
+
import { getSessionsByDate } from "../database";
|
|
6
|
+
|
|
7
|
+
function parseDate(input?: string): string {
|
|
8
|
+
if (!input) {
|
|
9
|
+
return new Date().toISOString().split("T")[0];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Try parsing as YYYY-MM-DD
|
|
13
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
|
|
14
|
+
return input;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Try parsing as MM-DD (current year)
|
|
18
|
+
if (/^\d{2}-\d{2}$/.test(input)) {
|
|
19
|
+
const year = new Date().getFullYear();
|
|
20
|
+
return `${year}-${input}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Try parsing as natural date
|
|
24
|
+
const parsed = new Date(input);
|
|
25
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
26
|
+
return parsed.toISOString().split("T")[0];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(`⚠️ Invalid date format: "${input}". Using today.`);
|
|
30
|
+
return new Date().toISOString().split("T")[0];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isToday(date: string): boolean {
|
|
34
|
+
return date === new Date().toISOString().split("T")[0];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function main() {
|
|
38
|
+
const dateArg = process.env.DATE || process.argv[2];
|
|
39
|
+
const targetDate = parseDate(dateArg);
|
|
40
|
+
const sessions = getSessionsByDate(targetDate);
|
|
41
|
+
|
|
42
|
+
const dateLabel = isToday(targetDate) ? "Today" : targetDate;
|
|
43
|
+
|
|
44
|
+
if (sessions.length === 0) {
|
|
45
|
+
console.log(`📊 No sessions for ${dateLabel}`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const totalCost = sessions.reduce((sum, s) => sum + s.total_cost, 0);
|
|
50
|
+
const totalDuration = sessions.reduce((sum, s) => sum + s.duration_ms, 0);
|
|
51
|
+
|
|
52
|
+
console.log(`\n📊 Spend for ${dateLabel}\n`);
|
|
53
|
+
console.log(`Sessions: ${sessions.length}`);
|
|
54
|
+
console.log(`Total Cost: $${formatCost(totalCost)}`);
|
|
55
|
+
console.log(`Total Duration: ${formatDuration(totalDuration)}`);
|
|
56
|
+
console.log("\n📝 Sessions:\n");
|
|
57
|
+
|
|
58
|
+
const filteredSessions = sessions
|
|
59
|
+
.filter((session) => session.total_cost >= 0.1)
|
|
60
|
+
.sort((a, b) => b.total_cost - a.total_cost);
|
|
61
|
+
|
|
62
|
+
const tableData = [
|
|
63
|
+
["ID", "Cost", "Duration", "Changes", "Directory"],
|
|
64
|
+
...filteredSessions.map((session) => [
|
|
65
|
+
session.session_id.slice(0, 8),
|
|
66
|
+
`$${formatCost(session.total_cost)}`,
|
|
67
|
+
formatDuration(session.duration_ms),
|
|
68
|
+
`+${session.lines_added} -${session.lines_removed}`,
|
|
69
|
+
session.cwd.replace(
|
|
70
|
+
/^(?:\/Users\/[^/]+\/|[A-Z]:\\Users\\[^\\]+\\)/,
|
|
71
|
+
"~/",
|
|
72
|
+
),
|
|
73
|
+
]),
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
console.log(table(tableData));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
main();
|