@codemieai/code 0.15.3 → 0.15.4
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/dist/agents/core/AgentCLI.d.ts.map +1 -1
- package/dist/agents/core/AgentCLI.js +9 -0
- package/dist/agents/core/AgentCLI.js.map +1 -1
- package/dist/agents/core/BaseAgentAdapter.d.ts.map +1 -1
- package/dist/agents/core/BaseAgentAdapter.js +34 -9
- package/dist/agents/core/BaseAgentAdapter.js.map +1 -1
- package/dist/agents/plugins/claude/claude.models.d.ts +58 -0
- package/dist/agents/plugins/claude/claude.models.d.ts.map +1 -1
- package/dist/agents/plugins/claude/claude.models.js +196 -10
- package/dist/agents/plugins/claude/claude.models.js.map +1 -1
- package/dist/agents/plugins/claude/claude.plugin.d.ts.map +1 -1
- package/dist/agents/plugins/claude/claude.plugin.js +147 -45
- package/dist/agents/plugins/claude/claude.plugin.js.map +1 -1
- package/dist/agents/plugins/claude/plugin/statusline.bundle.mjs +729 -0
- package/dist/agents/plugins/claude/statusline-installer.d.ts +1 -1
- package/dist/agents/plugins/claude/statusline-installer.d.ts.map +1 -1
- package/dist/agents/plugins/claude/statusline-installer.js +41 -3
- package/dist/agents/plugins/claude/statusline-installer.js.map +1 -1
- package/dist/agents/plugins/codex/codex-models.d.ts.map +1 -1
- package/dist/agents/plugins/codex/codex-models.js +109 -8
- package/dist/agents/plugins/codex/codex-models.js.map +1 -1
- package/dist/agents/plugins/codex/codex.correlation.d.ts +39 -0
- package/dist/agents/plugins/codex/codex.correlation.d.ts.map +1 -0
- package/dist/agents/plugins/codex/codex.correlation.js +150 -0
- package/dist/agents/plugins/codex/codex.correlation.js.map +1 -0
- package/dist/agents/plugins/codex/codex.plugin.d.ts.map +1 -1
- package/dist/agents/plugins/codex/codex.plugin.js +8 -40
- package/dist/agents/plugins/codex/codex.plugin.js.map +1 -1
- package/dist/agents/plugins/kimi/kimi.env-model.d.ts +27 -0
- package/dist/agents/plugins/kimi/kimi.env-model.d.ts.map +1 -0
- package/dist/agents/plugins/kimi/kimi.env-model.js +52 -0
- package/dist/agents/plugins/kimi/kimi.env-model.js.map +1 -0
- package/dist/agents/plugins/kimi/kimi.session.d.ts +6 -0
- package/dist/agents/plugins/kimi/kimi.session.d.ts.map +1 -1
- package/dist/agents/plugins/kimi/kimi.session.js +24 -2
- package/dist/agents/plugins/kimi/kimi.session.js.map +1 -1
- package/dist/agents/plugins/opencode/opencode-dynamic-models.js +1 -1
- package/dist/agents/plugins/opencode/opencode-dynamic-models.js.map +1 -1
- package/dist/cli/commands/analytics/cost/cost-enricher.d.ts +19 -2
- package/dist/cli/commands/analytics/cost/cost-enricher.d.ts.map +1 -1
- package/dist/cli/commands/analytics/cost/cost-enricher.js +171 -14
- package/dist/cli/commands/analytics/cost/cost-enricher.js.map +1 -1
- package/dist/cli/commands/analytics/cost/local-models.d.ts +15 -0
- package/dist/cli/commands/analytics/cost/local-models.d.ts.map +1 -0
- package/dist/cli/commands/analytics/cost/local-models.js +21 -0
- package/dist/cli/commands/analytics/cost/local-models.js.map +1 -0
- package/dist/cli/commands/analytics/cost/types.d.ts +60 -0
- package/dist/cli/commands/analytics/cost/types.d.ts.map +1 -1
- package/dist/cli/commands/analytics/cost/types.js.map +1 -1
- package/dist/cli/commands/analytics/cost/usage-readers.d.ts +25 -2
- package/dist/cli/commands/analytics/cost/usage-readers.d.ts.map +1 -1
- package/dist/cli/commands/analytics/cost/usage-readers.js +61 -3
- package/dist/cli/commands/analytics/cost/usage-readers.js.map +1 -1
- package/dist/cli/commands/analytics/formatter.d.ts +11 -0
- package/dist/cli/commands/analytics/formatter.d.ts.map +1 -1
- package/dist/cli/commands/analytics/formatter.js +28 -0
- package/dist/cli/commands/analytics/formatter.js.map +1 -1
- package/dist/cli/commands/analytics/index.d.ts.map +1 -1
- package/dist/cli/commands/analytics/index.js +70 -73
- package/dist/cli/commands/analytics/index.js.map +1 -1
- package/dist/cli/commands/analytics/otel-loader.d.ts.map +1 -1
- package/dist/cli/commands/analytics/otel-loader.js +60 -14
- package/dist/cli/commands/analytics/otel-loader.js.map +1 -1
- package/dist/cli/commands/analytics/otel-pricing.d.ts +38 -0
- package/dist/cli/commands/analytics/otel-pricing.d.ts.map +1 -0
- package/dist/cli/commands/analytics/otel-pricing.js +33 -0
- package/dist/cli/commands/analytics/otel-pricing.js.map +1 -0
- package/dist/cli/commands/analytics/report/client/app.js +279 -18
- package/dist/cli/commands/analytics/report/output-target.d.ts +22 -0
- package/dist/cli/commands/analytics/report/output-target.d.ts.map +1 -0
- package/dist/cli/commands/analytics/report/output-target.js +56 -0
- package/dist/cli/commands/analytics/report/output-target.js.map +1 -0
- package/dist/cli/commands/analytics/report/payload-builder.d.ts.map +1 -1
- package/dist/cli/commands/analytics/report/payload-builder.js +6 -0
- package/dist/cli/commands/analytics/report/payload-builder.js.map +1 -1
- package/dist/cli/commands/analytics/report/report-generator.d.ts +1 -1
- package/dist/cli/commands/analytics/report/report-generator.js +1 -1
- package/dist/cli/commands/analytics/report/session-report.d.ts +1 -3
- package/dist/cli/commands/analytics/report/session-report.d.ts.map +1 -1
- package/dist/cli/commands/analytics/report/session-report.js +1 -3
- package/dist/cli/commands/analytics/report/session-report.js.map +1 -1
- package/dist/cli/commands/analytics/report/template.html +6 -6
- package/dist/cli/commands/analytics/report/types.d.ts +17 -1
- package/dist/cli/commands/analytics/report/types.d.ts.map +1 -1
- package/dist/cli/commands/analytics/sources/sessions-source.d.ts.map +1 -1
- package/dist/cli/commands/analytics/sources/sessions-source.js +9 -11
- package/dist/cli/commands/analytics/sources/sessions-source.js.map +1 -1
- package/dist/cli/commands/analytics/sources/types.d.ts +0 -2
- package/dist/cli/commands/analytics/sources/types.d.ts.map +1 -1
- package/dist/cli/commands/analytics/types.d.ts +8 -8
- package/dist/cli/commands/analytics/types.d.ts.map +1 -1
- package/dist/cli/commands/models.d.ts.map +1 -1
- package/dist/cli/commands/models.js +16 -3
- package/dist/cli/commands/models.js.map +1 -1
- package/dist/cli/commands/proxy/connect-orchestrator.js +1 -1
- package/dist/cli/commands/proxy/connect-orchestrator.js.map +1 -1
- package/dist/cli/commands/proxy/connectors/tenant-catalog.d.ts +16 -4
- package/dist/cli/commands/proxy/connectors/tenant-catalog.d.ts.map +1 -1
- package/dist/cli/commands/proxy/connectors/tenant-catalog.js +42 -14
- package/dist/cli/commands/proxy/connectors/tenant-catalog.js.map +1 -1
- package/dist/cli/commands/proxy/connectors/vscode-models.d.ts +18 -0
- package/dist/cli/commands/proxy/connectors/vscode-models.d.ts.map +1 -1
- package/dist/cli/commands/proxy/connectors/vscode-models.js +58 -0
- package/dist/cli/commands/proxy/connectors/vscode-models.js.map +1 -1
- package/dist/cli/commands/proxy/connectors/vscode.d.ts +2 -2
- package/dist/cli/commands/proxy/connectors/vscode.d.ts.map +1 -1
- package/dist/cli/commands/proxy/connectors/vscode.js +27 -38
- package/dist/cli/commands/proxy/connectors/vscode.js.map +1 -1
- package/dist/providers/core/codemie-auth-helpers.d.ts.map +1 -1
- package/dist/providers/core/codemie-auth-helpers.js +6 -1
- package/dist/providers/core/codemie-auth-helpers.js.map +1 -1
- package/dist/providers/core/default-agent-hooks.d.ts.map +1 -1
- package/dist/providers/core/default-agent-hooks.js +9 -0
- package/dist/providers/core/default-agent-hooks.js.map +1 -1
- package/dist/providers/plugins/bedrock/bedrock.template.d.ts.map +1 -1
- package/dist/providers/plugins/bedrock/bedrock.template.js +5 -31
- package/dist/providers/plugins/bedrock/bedrock.template.js.map +1 -1
- package/dist/providers/plugins/sso/proxy/plugins/background-request-normalizer.plugin.d.ts +44 -0
- package/dist/providers/plugins/sso/proxy/plugins/background-request-normalizer.plugin.d.ts.map +1 -0
- package/dist/providers/plugins/sso/proxy/plugins/background-request-normalizer.plugin.js +106 -0
- package/dist/providers/plugins/sso/proxy/plugins/background-request-normalizer.plugin.js.map +1 -0
- package/dist/providers/plugins/sso/proxy/plugins/index.d.ts +3 -1
- package/dist/providers/plugins/sso/proxy/plugins/index.d.ts.map +1 -1
- package/dist/providers/plugins/sso/proxy/plugins/index.js +5 -1
- package/dist/providers/plugins/sso/proxy/plugins/index.js.map +1 -1
- package/dist/providers/plugins/sso/proxy/plugins/routing-header-injector.plugin.d.ts +67 -0
- package/dist/providers/plugins/sso/proxy/plugins/routing-header-injector.plugin.d.ts.map +1 -0
- package/dist/providers/plugins/sso/proxy/plugins/routing-header-injector.plugin.js +203 -0
- package/dist/providers/plugins/sso/proxy/plugins/routing-header-injector.plugin.js.map +1 -0
- package/dist/providers/plugins/sso/sso.http-client.d.ts +95 -0
- package/dist/providers/plugins/sso/sso.http-client.d.ts.map +1 -1
- package/dist/providers/plugins/sso/sso.http-client.js +79 -0
- package/dist/providers/plugins/sso/sso.http-client.js.map +1 -1
- package/dist/providers/plugins/sso/sso.models.d.ts +7 -0
- package/dist/providers/plugins/sso/sso.models.d.ts.map +1 -1
- package/dist/providers/plugins/sso/sso.models.js +22 -14
- package/dist/providers/plugins/sso/sso.models.js.map +1 -1
- package/dist/utils/bedrock-pricing.mjs +111 -0
- package/dist/utils/cli-updater.d.ts.map +1 -1
- package/dist/utils/cli-updater.js +32 -7
- package/dist/utils/cli-updater.js.map +1 -1
- package/dist/utils/credential-crypto.d.ts +51 -0
- package/dist/utils/credential-crypto.d.ts.map +1 -0
- package/dist/utils/credential-crypto.js +96 -0
- package/dist/utils/credential-crypto.js.map +1 -0
- package/dist/utils/model-normalizer.d.ts +6 -0
- package/dist/utils/model-normalizer.d.ts.map +1 -1
- package/dist/utils/model-normalizer.js +37 -19
- package/dist/utils/model-normalizer.js.map +1 -1
- package/dist/utils/price-resolution.d.ts +42 -0
- package/dist/utils/price-resolution.d.ts.map +1 -0
- package/dist/utils/price-resolution.js +112 -0
- package/dist/utils/price-resolution.js.map +1 -0
- package/dist/utils/pricing.d.ts +65 -6
- package/dist/utils/pricing.d.ts.map +1 -1
- package/dist/utils/pricing.js +91 -117
- package/dist/utils/pricing.js.map +1 -1
- package/dist/utils/pricing.json +201 -20
- package/dist/utils/routing-headers.mjs +104 -0
- package/dist/utils/security.d.ts +0 -32
- package/dist/utils/security.d.ts.map +1 -1
- package/dist/utils/security.js +20 -102
- package/dist/utils/security.js.map +1 -1
- package/package.json +4 -2
- package/scripts/bundle-statusline.mjs +37 -0
- package/scripts/copy-plugins.js +19 -2
- package/src/utils/tips.json +9 -9
- package/dist/agents/plugins/claude/plugin/__tests__/statusline.test.ts +0 -323
- package/dist/agents/plugins/claude/plugin/statusline.mjs +0 -282
- package/dist/cli/commands/analytics/exporter.d.ts +0 -20
- package/dist/cli/commands/analytics/exporter.d.ts.map +0 -1
- package/dist/cli/commands/analytics/exporter.js +0 -91
- package/dist/cli/commands/analytics/exporter.js.map +0 -1
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { resolve } from 'path';
|
|
3
|
-
import { pathToFileURL } from 'url';
|
|
4
|
-
import {
|
|
5
|
-
matchBudgetRow,
|
|
6
|
-
formatBudgetSegment,
|
|
7
|
-
extractBasicInfo,
|
|
8
|
-
formatDuration,
|
|
9
|
-
fmt,
|
|
10
|
-
buildStatusLine,
|
|
11
|
-
resolveBudget,
|
|
12
|
-
isMainModule,
|
|
13
|
-
ctxBar,
|
|
14
|
-
} from '../statusline.mjs';
|
|
15
|
-
|
|
16
|
-
const YELLOW = '\x1b[0;33m';
|
|
17
|
-
const GREEN = '\x1b[0;32m';
|
|
18
|
-
const RED = '\x1b[0;31m';
|
|
19
|
-
|
|
20
|
-
describe('matchBudgetRow', () => {
|
|
21
|
-
const rows = [
|
|
22
|
-
{ project_name: 'nikita_levyankov@epam.com (cli)', current_spending: 500.26, total: 100.05, budget_reset_at: '2026-07-13T00:00:18.663000Z' },
|
|
23
|
-
{ project_name: 'nikita_levyankov@epam.com', current_spending: 6.05, total: 5.04, budget_reset_at: '2026-07-17T09:30:34.278000Z' },
|
|
24
|
-
{ project_name: 'nikita_levyankov@epam.com (premium)', current_spending: 24.93, total: 83.11, budget_reset_at: '2026-07-18T16:55:53.270000Z' },
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
it('matches only the "(cli)" suffixed row for the given email', () => {
|
|
28
|
-
const row = matchBudgetRow(rows, 'nikita_levyankov@epam.com');
|
|
29
|
-
expect(row).toEqual(rows[0]);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('returns null when no row matches the email', () => {
|
|
33
|
-
expect(matchBudgetRow(rows, 'someone-else@epam.com')).toBeNull();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('returns null when rows is not an array', () => {
|
|
37
|
-
expect(matchBudgetRow(undefined, 'x@y.com')).toBeNull();
|
|
38
|
-
expect(matchBudgetRow(null, 'x@y.com')).toBeNull();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('returns null when userEmail is falsy', () => {
|
|
42
|
-
expect(matchBudgetRow(rows, '')).toBeNull();
|
|
43
|
-
expect(matchBudgetRow(rows, undefined)).toBeNull();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('matches regardless of email casing or surrounding whitespace', () => {
|
|
47
|
-
expect(matchBudgetRow(rows, 'Nikita_Levyankov@EPAM.com')).toEqual(rows[0]);
|
|
48
|
-
expect(matchBudgetRow(rows, ' nikita_levyankov@epam.com ')).toEqual(rows[0]);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe('formatBudgetSegment', () => {
|
|
53
|
-
it('formats current spend, percentage, and reset date — never budget_limit', () => {
|
|
54
|
-
const row = { current_spending: 12.34, budget_limit: 999, total: 41, budget_reset_at: '2026-07-15T00:00:00.000Z' };
|
|
55
|
-
const result = formatBudgetSegment(row);
|
|
56
|
-
expect(result.text).toContain('$12.34');
|
|
57
|
-
expect(result.text).toContain('41%');
|
|
58
|
-
expect(result.text).not.toContain('999');
|
|
59
|
-
expect(result.pct).toBe(41);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('returns null for a null row', () => {
|
|
63
|
-
expect(formatBudgetSegment(null)).toBeNull();
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe('extractBasicInfo', () => {
|
|
68
|
-
it('extracts model, project, context, cost, and duration from a full Claude Code payload', () => {
|
|
69
|
-
const ctx = {
|
|
70
|
-
workspace: { current_dir: '/Users/me/repos/my-project' },
|
|
71
|
-
model: { display_name: 'Claude Sonnet 5' },
|
|
72
|
-
context_window: { used_percentage: 42, total_input_tokens: 12345, total_output_tokens: 678 },
|
|
73
|
-
cost: { total_cost_usd: 1.2345, total_duration_ms: 125000 },
|
|
74
|
-
};
|
|
75
|
-
const info = extractBasicInfo(ctx);
|
|
76
|
-
expect(info.projectName).toBe('my-project');
|
|
77
|
-
expect(info.model).toBe('Claude Sonnet 5');
|
|
78
|
-
expect(info.ctxPct).toBe(42);
|
|
79
|
-
expect(info.tokIn).toBe(12345);
|
|
80
|
-
expect(info.tokOut).toBe(678);
|
|
81
|
-
expect(info.cost).toBe(1.2345);
|
|
82
|
-
expect(info.durationMs).toBe(125000);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('returns safe defaults for an empty/malformed payload', () => {
|
|
86
|
-
const info = extractBasicInfo({});
|
|
87
|
-
expect(info.projectName).toBe('');
|
|
88
|
-
expect(info.model).toBe('');
|
|
89
|
-
expect(info.ctxPct).toBeNull();
|
|
90
|
-
expect(info.cost).toBeNull();
|
|
91
|
-
expect(info.durationMs).toBeNull();
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
describe('formatDuration', () => {
|
|
96
|
-
it('formats milliseconds as "Xm Ys"', () => {
|
|
97
|
-
expect(formatDuration(125000)).toBe('2m 5s');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('returns null for null/undefined input', () => {
|
|
101
|
-
expect(formatDuration(null)).toBeNull();
|
|
102
|
-
expect(formatDuration(undefined)).toBeNull();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('returns null instead of "NaNm NaNs" for non-numeric or negative input', () => {
|
|
106
|
-
expect(formatDuration(NaN)).toBeNull();
|
|
107
|
-
expect(formatDuration(-1)).toBeNull();
|
|
108
|
-
expect(formatDuration('not-a-number')).toBeNull();
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe('fmt', () => {
|
|
113
|
-
it('formats large numbers with k/M suffixes', () => {
|
|
114
|
-
expect(fmt(500)).toBe('500');
|
|
115
|
-
expect(fmt(1500)).toBe('1.5k');
|
|
116
|
-
expect(fmt(2_500_000)).toBe('2.5M');
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe('ctxBar', () => {
|
|
121
|
-
it('renders a 10-segment filled/empty bar plus the percentage', () => {
|
|
122
|
-
const bar = ctxBar(50);
|
|
123
|
-
expect(bar).toContain('50%');
|
|
124
|
-
expect(bar).toContain('█████░░░░░');
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('colors the bar green below 70%, yellow from 70-89%, red at 90%+', () => {
|
|
128
|
-
expect(ctxBar(50)).toContain(GREEN);
|
|
129
|
-
expect(ctxBar(75)).toContain(YELLOW);
|
|
130
|
-
expect(ctxBar(95)).toContain(RED);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('returns null for non-numeric or missing input', () => {
|
|
134
|
-
expect(ctxBar(null)).toBeNull();
|
|
135
|
-
expect(ctxBar(undefined)).toBeNull();
|
|
136
|
-
expect(ctxBar(NaN)).toBeNull();
|
|
137
|
-
expect(ctxBar('not-a-number')).toBeNull();
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
describe('buildStatusLine', () => {
|
|
142
|
-
const basic = {
|
|
143
|
-
projectName: 'my-project', branch: 'main', model: 'Claude Sonnet 5',
|
|
144
|
-
ctxPct: 42, tokIn: 1000, tokOut: 200, cost: 1.5, durationMs: 65000,
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
it('always renders basic info (including session cost and duration) with no budget/profile', () => {
|
|
148
|
-
const line = buildStatusLine({ ...basic, budget: null, budgetError: null });
|
|
149
|
-
expect(line).not.toContain('⚠');
|
|
150
|
-
expect(line).toContain('$1.5000');
|
|
151
|
-
expect(line).toContain('1m 5s');
|
|
152
|
-
expect(line).toContain('[my-project]');
|
|
153
|
-
expect(line).toContain('(main)');
|
|
154
|
-
expect(line).toContain('[Claude Sonnet 5]');
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('renders the context-% as a colored bar, and the cost in its own distinct (yellow) color', () => {
|
|
158
|
-
const line = buildStatusLine({ ...basic, budget: null, budgetError: null });
|
|
159
|
-
expect(line).toContain('42%');
|
|
160
|
-
expect(line).toContain('████░░░░░░'); // 42% -> 4 filled segments
|
|
161
|
-
expect(line).toContain(`${YELLOW}$1.5000${'\x1b[0m'}`);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('shows a minimal warning indicator (not blocking basic info) when the budget fetch fails', () => {
|
|
165
|
-
const line = buildStatusLine({ ...basic, budget: null, budgetError: 'reauthenticate' });
|
|
166
|
-
expect(line).toContain('⚠ reauthenticate');
|
|
167
|
-
expect(line).toContain('$1.5000');
|
|
168
|
-
expect(line).toContain('[my-project]');
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('shows the budget segment (and no warning) when budget resolves successfully', () => {
|
|
172
|
-
const line = buildStatusLine({ ...basic, budget: { text: '$12.34 (41%) resets 7/15/2026', pct: 41 }, budgetError: null });
|
|
173
|
-
expect(line).toContain('$12.34 (41%)');
|
|
174
|
-
expect(line).not.toContain('⚠');
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('does not throw and omits the cost segment when cost is non-numeric', () => {
|
|
178
|
-
expect(() => buildStatusLine({ ...basic, cost: 'not-a-number', budget: null, budgetError: null })).not.toThrow();
|
|
179
|
-
const line = buildStatusLine({ ...basic, cost: 'not-a-number', budget: null, budgetError: null });
|
|
180
|
-
expect(line).not.toContain('NaN');
|
|
181
|
-
expect(line).toContain('[my-project]'); // basic info still renders
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
describe('resolveBudget', () => {
|
|
186
|
-
it('skips silently (no error) when there is no CodeMie config at all', async () => {
|
|
187
|
-
const readFile = vi.fn().mockRejectedValue(new Error('ENOENT'));
|
|
188
|
-
const result = await resolveBudget({ readFile, writeFile: vi.fn(), fetchImpl: vi.fn(), getAuthHeadersImpl: vi.fn() });
|
|
189
|
-
expect(result).toEqual({ budget: null, budgetError: null });
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('skips silently when the profile is missing codeMieUrl/baseUrl/userEmail', async () => {
|
|
193
|
-
const readFile = vi.fn()
|
|
194
|
-
.mockRejectedValueOnce(new Error('no cache'))
|
|
195
|
-
.mockResolvedValueOnce(JSON.stringify({ activeProfile: 'default', profiles: { default: {} } }));
|
|
196
|
-
const result = await resolveBudget({ readFile, writeFile: vi.fn(), fetchImpl: vi.fn(), getAuthHeadersImpl: vi.fn() });
|
|
197
|
-
expect(result).toEqual({ budget: null, budgetError: null });
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('skips silently when codeMieUrl/userEmail only exist on the profile — migration 006 moved them to workspace/top-level', async () => {
|
|
201
|
-
const readFile = vi.fn()
|
|
202
|
-
.mockRejectedValueOnce(new Error('no cache'))
|
|
203
|
-
.mockResolvedValueOnce(JSON.stringify({
|
|
204
|
-
activeProfile: 'default',
|
|
205
|
-
// Pre-fix (stale) shape: codeMieUrl/userEmail stranded on the profile with no
|
|
206
|
-
// top-level `workspace`/`userEmail`. Must not be read from the profile object —
|
|
207
|
-
// regression test for the statusline reading raw profile fields post-migration.
|
|
208
|
-
profiles: { default: { codeMieUrl: 'https://x', baseUrl: 'https://x/api', userEmail: 'me@x.com' } },
|
|
209
|
-
}));
|
|
210
|
-
const result = await resolveBudget({ readFile, writeFile: vi.fn(), fetchImpl: vi.fn(), getAuthHeadersImpl: vi.fn() });
|
|
211
|
-
expect(result).toEqual({ budget: null, budgetError: null });
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('returns a "reauthenticate" error when no auth headers are available', async () => {
|
|
215
|
-
const readFile = vi.fn()
|
|
216
|
-
.mockRejectedValueOnce(new Error('no cache'))
|
|
217
|
-
.mockResolvedValueOnce(JSON.stringify({
|
|
218
|
-
activeProfile: 'default',
|
|
219
|
-
userEmail: 'me@x.com',
|
|
220
|
-
workspace: { codeMieUrl: 'https://x' },
|
|
221
|
-
profiles: { default: { baseUrl: 'https://x/api' } },
|
|
222
|
-
}));
|
|
223
|
-
const getAuthHeadersImpl = vi.fn().mockResolvedValue(null);
|
|
224
|
-
const result = await resolveBudget({ readFile, writeFile: vi.fn(), fetchImpl: vi.fn(), getAuthHeadersImpl });
|
|
225
|
-
expect(result).toEqual({ budget: null, budgetError: 'reauthenticate' });
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('returns the HTTP error message when the fetch fails', async () => {
|
|
229
|
-
const readFile = vi.fn()
|
|
230
|
-
.mockRejectedValueOnce(new Error('no cache'))
|
|
231
|
-
.mockResolvedValueOnce(JSON.stringify({
|
|
232
|
-
activeProfile: 'default',
|
|
233
|
-
userEmail: 'me@x.com',
|
|
234
|
-
workspace: { codeMieUrl: 'https://x' },
|
|
235
|
-
profiles: { default: { baseUrl: 'https://x/api' } },
|
|
236
|
-
}));
|
|
237
|
-
const getAuthHeadersImpl = vi.fn().mockResolvedValue({ cookie: 'a=b' });
|
|
238
|
-
const fetchImpl = vi.fn().mockResolvedValue({ ok: false, status: 500 });
|
|
239
|
-
const result = await resolveBudget({ readFile, writeFile: vi.fn(), fetchImpl, getAuthHeadersImpl });
|
|
240
|
-
expect(result).toEqual({ budget: null, budgetError: 'HTTP 500' });
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('resolves and caches the matched budget row on success', async () => {
|
|
244
|
-
const readFile = vi.fn()
|
|
245
|
-
.mockRejectedValueOnce(new Error('no cache'))
|
|
246
|
-
.mockResolvedValueOnce(JSON.stringify({
|
|
247
|
-
activeProfile: 'default',
|
|
248
|
-
userEmail: 'me@x.com',
|
|
249
|
-
workspace: { codeMieUrl: 'https://x' },
|
|
250
|
-
profiles: { default: { baseUrl: 'https://x/api' } },
|
|
251
|
-
}));
|
|
252
|
-
const getAuthHeadersImpl = vi.fn().mockResolvedValue({ cookie: 'a=b' });
|
|
253
|
-
const fetchImpl = vi.fn().mockResolvedValue({
|
|
254
|
-
ok: true,
|
|
255
|
-
json: async () => ({ data: { rows: [{ project_name: 'me@x.com (cli)', current_spending: 5, total: 10, budget_reset_at: '2026-07-15T00:00:00.000Z' }] } }),
|
|
256
|
-
});
|
|
257
|
-
const writeFile = vi.fn().mockResolvedValue(undefined);
|
|
258
|
-
const result = await resolveBudget({ readFile, writeFile, fetchImpl, getAuthHeadersImpl });
|
|
259
|
-
expect(result.budgetError).toBeNull();
|
|
260
|
-
expect(result.budget.text).toContain('$5.00');
|
|
261
|
-
expect(writeFile).toHaveBeenCalledWith(expect.stringContaining('budget-cache.json'), expect.any(String), 'utf8');
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it('returns the fresh cached value without touching config/network when cache is fresh', async () => {
|
|
265
|
-
const readFile = vi.fn().mockResolvedValueOnce(JSON.stringify({ schema: 2, ts: Date.now(), value: { text: 'cached', pct: 5 } }));
|
|
266
|
-
const fetchImpl = vi.fn();
|
|
267
|
-
const result = await resolveBudget({ readFile, writeFile: vi.fn(), fetchImpl, getAuthHeadersImpl: vi.fn() });
|
|
268
|
-
expect(result).toEqual({ budget: { text: 'cached', pct: 5 }, budgetError: null });
|
|
269
|
-
expect(fetchImpl).not.toHaveBeenCalled();
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it('treats a pre-upgrade string-shaped cache entry as a cache miss instead of using it', async () => {
|
|
273
|
-
// Old cache format: value was a plain string, not { text, pct }.
|
|
274
|
-
const readFile = vi.fn()
|
|
275
|
-
.mockResolvedValueOnce(JSON.stringify({ ts: Date.now(), value: '$5.00/$10 (50%)', pct: 50 }))
|
|
276
|
-
.mockRejectedValueOnce(new Error('no config'));
|
|
277
|
-
const fetchImpl = vi.fn();
|
|
278
|
-
const result = await resolveBudget({ readFile, writeFile: vi.fn(), fetchImpl, getAuthHeadersImpl: vi.fn() });
|
|
279
|
-
expect(result).toEqual({ budget: null, budgetError: null });
|
|
280
|
-
expect(fetchImpl).not.toHaveBeenCalled();
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it('returns a graceful budgetError instead of an uncaught rejection when getAuthHeadersImpl throws', async () => {
|
|
284
|
-
const readFile = vi.fn()
|
|
285
|
-
.mockRejectedValueOnce(new Error('no cache'))
|
|
286
|
-
.mockResolvedValueOnce(JSON.stringify({
|
|
287
|
-
activeProfile: 'default',
|
|
288
|
-
userEmail: 'me@x.com',
|
|
289
|
-
workspace: { codeMieUrl: 'https://x' },
|
|
290
|
-
profiles: { default: { baseUrl: 'https://x/api' } },
|
|
291
|
-
}));
|
|
292
|
-
const getAuthHeadersImpl = vi.fn().mockRejectedValue(new Error('keychain locked'));
|
|
293
|
-
const result = await resolveBudget({ readFile, writeFile: vi.fn(), fetchImpl: vi.fn(), getAuthHeadersImpl });
|
|
294
|
-
expect(result).toEqual({ budget: null, budgetError: 'keychain locked' });
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
describe('isMainModule', () => {
|
|
299
|
-
// Build the file URL from a real, platform-resolved absolute path (via pathToFileURL)
|
|
300
|
-
// rather than hardcoding POSIX-style "file:///..." strings, so this test is symmetric
|
|
301
|
-
// on Windows too — fileURLToPath() decodes to a backslash-separated path there.
|
|
302
|
-
it('matches when the raw path equals the decoded file URL', () => {
|
|
303
|
-
const scriptPath = resolve('Users', 'me', 'script.mjs');
|
|
304
|
-
expect(isMainModule(scriptPath, pathToFileURL(scriptPath).href)).toBe(true);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it('matches even when the path contains spaces (percent-encoded in the URL)', () => {
|
|
308
|
-
const scriptPath = resolve('Users', 'John Doe', '.claude', 'codemie-budget-status.js');
|
|
309
|
-
expect(isMainModule(scriptPath, pathToFileURL(scriptPath).href)).toBe(true);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it('returns false for a different path', () => {
|
|
313
|
-
const scriptPath = resolve('Users', 'me', 'script.mjs');
|
|
314
|
-
const otherPath = resolve('Users', 'me', 'other.mjs');
|
|
315
|
-
expect(isMainModule(otherPath, pathToFileURL(scriptPath).href)).toBe(false);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it('returns false when argv1 is falsy', () => {
|
|
319
|
-
const url = pathToFileURL(resolve('Users', 'me', 'script.mjs')).href;
|
|
320
|
-
expect(isMainModule('', url)).toBe(false);
|
|
321
|
-
expect(isMainModule(undefined, url)).toBe(false);
|
|
322
|
-
});
|
|
323
|
-
});
|
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// CodeMie statusline — shows model, project, branch, context, session cost/duration,
|
|
3
|
-
// and (when a CodeMie profile is configured) the CLI budget for the authenticated user.
|
|
4
|
-
// Deployed to ~/.claude/ by `codemie install statusline` (also triggered by the `--status`
|
|
5
|
-
// CLI flag, which calls the same installer). Runs standalone — Node builtins only, no
|
|
6
|
-
// project imports, since it executes via `node <path>` after the project process exits.
|
|
7
|
-
import crypto from 'crypto';
|
|
8
|
-
import { exec } from 'child_process';
|
|
9
|
-
import fs from 'fs/promises';
|
|
10
|
-
import os from 'os';
|
|
11
|
-
import path from 'path';
|
|
12
|
-
import { fileURLToPath } from 'url';
|
|
13
|
-
|
|
14
|
-
const HOME = process.env.CODEMIE_HOME || path.join(os.homedir(), '.codemie');
|
|
15
|
-
const CACHE_FILE = path.join(HOME, 'budget-cache.json');
|
|
16
|
-
const CONFIG_FILE = path.join(HOME, 'codemie-cli.config.json');
|
|
17
|
-
const CREDS_DIR = path.join(HOME, 'credentials');
|
|
18
|
-
const CACHE_TTL_MS = 60_000;
|
|
19
|
-
const CACHE_SCHEMA = 2; // bump when the cache.value shape changes, to discard stale pre-upgrade entries
|
|
20
|
-
|
|
21
|
-
const ENCRYPTION_KEY = (() => {
|
|
22
|
-
const id = os.hostname() + os.platform() + os.arch();
|
|
23
|
-
const hex = crypto.createHash('sha256').update(id).digest('hex');
|
|
24
|
-
return crypto.createHash('sha256').update(hex).digest();
|
|
25
|
-
})();
|
|
26
|
-
|
|
27
|
-
function decrypt(text) {
|
|
28
|
-
const parts = text.split(':');
|
|
29
|
-
if (parts.length === 3) {
|
|
30
|
-
const iv = Buffer.from(parts[0], 'hex');
|
|
31
|
-
const authTag = Buffer.from(parts[1], 'hex');
|
|
32
|
-
const d = crypto.createDecipheriv('aes-256-gcm', ENCRYPTION_KEY, iv);
|
|
33
|
-
d.setAuthTag(authTag);
|
|
34
|
-
return d.update(parts[2], 'hex', 'utf8') + d.final('utf8');
|
|
35
|
-
}
|
|
36
|
-
// Legacy CBC format: iv:encrypted (backward compat for existing stored credentials)
|
|
37
|
-
const iv = Buffer.from(parts[0], 'hex');
|
|
38
|
-
const d = crypto.createDecipheriv('aes-256-cbc', ENCRYPTION_KEY, iv);
|
|
39
|
-
return d.update(parts[1], 'hex', 'utf8') + d.final('utf8');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function urlHash(rawUrl) {
|
|
43
|
-
const normalized = rawUrl.replace(/\/$/, '').toLowerCase();
|
|
44
|
-
return crypto.createHash('sha256').update(normalized).digest('hex');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function readCredsFile(filePath) {
|
|
48
|
-
try {
|
|
49
|
-
return JSON.parse(decrypt(await fs.readFile(filePath, 'utf8')));
|
|
50
|
-
} catch {
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export async function getAuthHeaders(codeMieUrl) {
|
|
56
|
-
const hash = urlHash(codeMieUrl);
|
|
57
|
-
|
|
58
|
-
const sso = await readCredsFile(path.join(CREDS_DIR, `sso-${hash}.enc`));
|
|
59
|
-
if (sso?.cookies) {
|
|
60
|
-
return { cookie: Object.entries(sso.cookies).map(([k, v]) => `${k}=${v}`).join(';') };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const jwt = await readCredsFile(path.join(CREDS_DIR, `jwt-sso-${hash}.enc`));
|
|
64
|
-
if (jwt?.token) {
|
|
65
|
-
return { authorization: `Bearer ${jwt.token}` };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// --- Pure functions (unit-testable, no filesystem/network access) ---
|
|
72
|
-
|
|
73
|
-
export function matchBudgetRow(rows, userEmail) {
|
|
74
|
-
if (!Array.isArray(rows) || !userEmail) return null;
|
|
75
|
-
const target = `${userEmail.trim().toLowerCase()} (cli)`;
|
|
76
|
-
return rows.find(r => r.project_name?.trim().toLowerCase() === target) ?? null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function formatBudgetSegment(row) {
|
|
80
|
-
if (!row) return null;
|
|
81
|
-
const pct = Math.round(row.total ?? 0);
|
|
82
|
-
const reset = row.budget_reset_at ? new Date(row.budget_reset_at).toLocaleDateString() : '?';
|
|
83
|
-
return {
|
|
84
|
-
text: `$${row.current_spending.toFixed(2)} (${pct}%) resets ${reset}`,
|
|
85
|
-
pct,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function extractBasicInfo(ctx) {
|
|
90
|
-
const cwd = ctx?.workspace?.current_dir ?? ctx?.cwd ?? '';
|
|
91
|
-
return {
|
|
92
|
-
projectName: cwd ? path.basename(cwd) : '',
|
|
93
|
-
cwd,
|
|
94
|
-
model: ctx?.model?.display_name ?? '',
|
|
95
|
-
ctxPct: ctx?.context_window?.used_percentage ?? null,
|
|
96
|
-
tokIn: ctx?.context_window?.total_input_tokens ?? null,
|
|
97
|
-
tokOut: ctx?.context_window?.total_output_tokens ?? null,
|
|
98
|
-
cost: ctx?.cost?.total_cost_usd ?? null,
|
|
99
|
-
durationMs: ctx?.cost?.total_duration_ms ?? null,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function formatDuration(ms) {
|
|
104
|
-
if (typeof ms !== 'number' || Number.isNaN(ms) || ms < 0) return null;
|
|
105
|
-
const mins = Math.floor(ms / 60000);
|
|
106
|
-
const secs = Math.floor((ms % 60000) / 1000);
|
|
107
|
-
return `${mins}m ${secs}s`;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function fmt(n) {
|
|
111
|
-
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
112
|
-
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
|
|
113
|
-
return String(n);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const C = {
|
|
117
|
-
reset: '\x1b[0m',
|
|
118
|
-
purple: '\x1b[38;2;177;185;249m',
|
|
119
|
-
green: '\x1b[0;32m',
|
|
120
|
-
yellow: '\x1b[0;33m',
|
|
121
|
-
red: '\x1b[0;31m',
|
|
122
|
-
cyan: '\x1b[0;36m',
|
|
123
|
-
blue: '\x1b[0;94m',
|
|
124
|
-
gray: '\x1b[0;37m',
|
|
125
|
-
};
|
|
126
|
-
const c = (color, text) => `${color}${text}${C.reset}`;
|
|
127
|
-
|
|
128
|
-
function budgetColor(pct) {
|
|
129
|
-
return pct > 85 ? C.red : pct > 30 ? C.yellow : C.green;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function ctxBar(pct) {
|
|
133
|
-
if (typeof pct !== 'number' || Number.isNaN(pct)) return null;
|
|
134
|
-
const clamped = Math.max(0, Math.min(100, pct));
|
|
135
|
-
const color = clamped >= 90 ? C.red : clamped >= 70 ? C.yellow : C.green;
|
|
136
|
-
const filled = Math.floor(clamped / 10);
|
|
137
|
-
const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
|
|
138
|
-
return `${c(color, bar)} ${pct}%`;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function buildStatusLine({ projectName, branch, model, ctxPct, tokIn, tokOut, cost, durationMs, budget, budgetError }) {
|
|
142
|
-
const parts = [];
|
|
143
|
-
|
|
144
|
-
if (projectName) parts.push(c(C.purple, `[${projectName}]`));
|
|
145
|
-
if (budget) parts.push(c(budgetColor(budget.pct), budget.text));
|
|
146
|
-
else if (budgetError) parts.push(c(C.yellow, `⚠ ${budgetError}`));
|
|
147
|
-
if (branch) parts.push(c(C.blue, `(${branch})`));
|
|
148
|
-
if (model) parts.push(c(C.cyan, `[${model}]`));
|
|
149
|
-
|
|
150
|
-
const bar = ctxBar(ctxPct);
|
|
151
|
-
if (bar) parts.push(bar);
|
|
152
|
-
|
|
153
|
-
const stats = [];
|
|
154
|
-
if (tokIn != null) stats.push(`in:${fmt(tokIn)}`);
|
|
155
|
-
if (tokOut != null) stats.push(`out:${fmt(tokOut)}`);
|
|
156
|
-
if (stats.length) parts.push(c(C.gray, stats.join(' ')));
|
|
157
|
-
|
|
158
|
-
if (typeof cost === 'number' && !Number.isNaN(cost)) parts.push(c(C.yellow, `$${cost.toFixed(4)}`));
|
|
159
|
-
|
|
160
|
-
const dur = formatDuration(durationMs);
|
|
161
|
-
if (dur) parts.push(c(C.gray, dur));
|
|
162
|
-
|
|
163
|
-
return parts.join(' | ');
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function readStdin() {
|
|
167
|
-
return new Promise(resolve => {
|
|
168
|
-
let data = '';
|
|
169
|
-
process.stdin.setEncoding('utf8');
|
|
170
|
-
process.stdin.on('data', chunk => { data += chunk; });
|
|
171
|
-
process.stdin.on('end', () => resolve(data));
|
|
172
|
-
process.stdin.on('error', () => resolve(data));
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function gitBranch(cwd) {
|
|
177
|
-
return new Promise(resolve => {
|
|
178
|
-
exec(
|
|
179
|
-
'git --no-optional-locks symbolic-ref --short HEAD 2>/dev/null || git --no-optional-locks rev-parse --short HEAD 2>/dev/null',
|
|
180
|
-
{ cwd, timeout: 2000 },
|
|
181
|
-
(_, stdout) => resolve(stdout.trim() || '')
|
|
182
|
-
);
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// --- Budget resolution (network/filesystem; dependencies injectable for tests) ---
|
|
187
|
-
|
|
188
|
-
export async function resolveBudget({
|
|
189
|
-
readFile = fs.readFile,
|
|
190
|
-
writeFile = fs.writeFile,
|
|
191
|
-
fetchImpl = fetch,
|
|
192
|
-
getAuthHeadersImpl = getAuthHeaders,
|
|
193
|
-
} = {}) {
|
|
194
|
-
// Fast path: fresh cache, skip config/network entirely. Discard any cache entry that
|
|
195
|
-
// isn't this schema version (e.g. a pre-upgrade string-shaped value) instead of trusting it.
|
|
196
|
-
try {
|
|
197
|
-
const cacheRaw = await readFile(CACHE_FILE, 'utf8');
|
|
198
|
-
const cache = JSON.parse(cacheRaw);
|
|
199
|
-
const validShape = cache.schema === CACHE_SCHEMA
|
|
200
|
-
&& typeof cache.value === 'object' && cache.value !== null
|
|
201
|
-
&& typeof cache.value.text === 'string';
|
|
202
|
-
if (validShape && Date.now() - cache.ts < CACHE_TTL_MS) {
|
|
203
|
-
return { budget: cache.value, budgetError: null };
|
|
204
|
-
}
|
|
205
|
-
} catch {}
|
|
206
|
-
|
|
207
|
-
let config;
|
|
208
|
-
try {
|
|
209
|
-
config = JSON.parse(await readFile(CONFIG_FILE, 'utf8'));
|
|
210
|
-
} catch {
|
|
211
|
-
return { budget: null, budgetError: null }; // no CodeMie config at all → skip silently
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const profile = config.profiles?.[config.activeProfile];
|
|
215
|
-
const { baseUrl } = profile ?? {};
|
|
216
|
-
// codeMieUrl now lives on the scope-level workspace object (migration 006), and
|
|
217
|
-
// userEmail is a top-level MultiProviderConfig field — neither is per-profile anymore.
|
|
218
|
-
const codeMieUrl = config.workspace?.codeMieUrl;
|
|
219
|
-
const userEmail = config.userEmail;
|
|
220
|
-
if (!profile || !codeMieUrl || !baseUrl || !userEmail) {
|
|
221
|
-
return { budget: null, budgetError: null }; // no CodeMie profile configured → skip silently
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
let headers;
|
|
225
|
-
try {
|
|
226
|
-
headers = await getAuthHeadersImpl(codeMieUrl);
|
|
227
|
-
} catch (e) {
|
|
228
|
-
return { budget: null, budgetError: e.message };
|
|
229
|
-
}
|
|
230
|
-
if (!headers) {
|
|
231
|
-
return { budget: null, budgetError: 'reauthenticate' };
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
try {
|
|
235
|
-
const res = await fetchImpl(`${baseUrl}/v1/analytics/budget_usage`, {
|
|
236
|
-
headers: { 'Content-Type': 'application/json', 'X-CodeMie-Client': 'codemie-cli', ...headers },
|
|
237
|
-
});
|
|
238
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
239
|
-
|
|
240
|
-
const json = await res.json();
|
|
241
|
-
const row = matchBudgetRow(json?.data?.rows, userEmail);
|
|
242
|
-
if (!row) throw new Error('budget row not found');
|
|
243
|
-
|
|
244
|
-
const budget = formatBudgetSegment(row);
|
|
245
|
-
await writeFile(CACHE_FILE, JSON.stringify({ schema: CACHE_SCHEMA, ts: Date.now(), value: budget }), 'utf8');
|
|
246
|
-
return { budget, budgetError: null };
|
|
247
|
-
} catch (e) {
|
|
248
|
-
return { budget: null, budgetError: e.message };
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
export async function main() {
|
|
253
|
-
const stdinRaw = await readStdin();
|
|
254
|
-
|
|
255
|
-
let basic;
|
|
256
|
-
try {
|
|
257
|
-
basic = extractBasicInfo(JSON.parse(stdinRaw));
|
|
258
|
-
} catch {
|
|
259
|
-
basic = extractBasicInfo({});
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const branchPromise = basic.cwd ? gitBranch(basic.cwd) : Promise.resolve('');
|
|
263
|
-
const [budgetResult, branch] = await Promise.all([resolveBudget(), branchPromise]);
|
|
264
|
-
|
|
265
|
-
process.stdout.write(buildStatusLine({ ...basic, branch, ...budgetResult }));
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Compares decoded paths (not raw strings) so this correctly matches even when the
|
|
269
|
-
// script's path contains characters import.meta.url percent-encodes (e.g. spaces).
|
|
270
|
-
export function isMainModule(argv1, metaUrl) {
|
|
271
|
-
if (!argv1) return false;
|
|
272
|
-
try {
|
|
273
|
-
return fileURLToPath(metaUrl) === argv1;
|
|
274
|
-
} catch {
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (isMainModule(process.argv[1], import.meta.url)) {
|
|
280
|
-
// Statusline must never crash Claude Code — swallow any unexpected error.
|
|
281
|
-
main().catch(() => { process.stdout.write(''); });
|
|
282
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Export functionality for analytics data
|
|
3
|
-
*/
|
|
4
|
-
import type { RootAnalytics } from './types.js';
|
|
5
|
-
export declare class AnalyticsExporter {
|
|
6
|
-
/**
|
|
7
|
-
* Export analytics to JSON file
|
|
8
|
-
*/
|
|
9
|
-
static exportJSON(analytics: RootAnalytics, outputPath: string): void;
|
|
10
|
-
/**
|
|
11
|
-
* Export analytics to CSV file
|
|
12
|
-
* Exports session-level data in flat format
|
|
13
|
-
*/
|
|
14
|
-
static exportCSV(analytics: RootAnalytics, outputPath: string): void;
|
|
15
|
-
/**
|
|
16
|
-
* Auto-determine output path based on format
|
|
17
|
-
*/
|
|
18
|
-
static getDefaultOutputPath(format: 'json' | 'csv', cwd: string): string;
|
|
19
|
-
}
|
|
20
|
-
//# sourceMappingURL=exporter.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"exporter.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/analytics/exporter.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhD,qBAAa,iBAAiB;IAC5B;;OAEG;IACH,MAAM,CAAC,UAAU,CAAC,SAAS,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IAWrE;;;OAGG;IACH,MAAM,CAAC,SAAS,CAAC,SAAS,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IA8DpE;;OAEG;IACH,MAAM,CAAC,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM;CAIzE"}
|