@axplusb/kepler 2.0.4 → 2.0.6
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/package.json
CHANGED
package/src/auth/tarang-auth.mjs
CHANGED
|
@@ -38,7 +38,7 @@ export class TarangAuth {
|
|
|
38
38
|
this._config = {};
|
|
39
39
|
}
|
|
40
40
|
return {
|
|
41
|
-
token: this._config.token || null,
|
|
41
|
+
token: process.env.KEPLER_TOKEN || this._config.token || null,
|
|
42
42
|
openRouterKey: this._config.openrouter_key || process.env.OPENROUTER_API_KEY || null,
|
|
43
43
|
anthropicKey: this._config.anthropic_api_key || process.env.ANTHROPIC_API_KEY || null,
|
|
44
44
|
openaiKey: this._config.openai_api_key || process.env.OPENAI_API_KEY || null,
|
package/src/core/backend-url.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
const BACKEND_URLS = {
|
|
11
11
|
local: 'http://127.0.0.1:8000',
|
|
12
12
|
development: 'https://codekepler-backend-dev.kindisland-9034322d.eastus.azurecontainerapps.io',
|
|
13
|
-
production: 'https://codekepler-backend-prod.
|
|
13
|
+
production: 'https://codekepler-backend-prod.gentlerock-9816c6b8.centralus.azurecontainerapps.io',
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
// Aliases
|
|
@@ -67,7 +67,7 @@ async function checkAuthAndBackend(auth, { timeoutMs = 2500 } = {}) {
|
|
|
67
67
|
if (resp.ok) {
|
|
68
68
|
const user = await resp.json().catch(() => null);
|
|
69
69
|
const who = user?.github_username || user?.email || 'user';
|
|
70
|
-
return { status: 'ok', label: `Signed in as ${who} · connected
|
|
70
|
+
return { status: 'ok', label: `Signed in as ${who} · connected`, user };
|
|
71
71
|
}
|
|
72
72
|
if (resp.status === 401 || resp.status === 403) {
|
|
73
73
|
return { status: 'warn', label: 'Token expired · connected', hint: '/login again to refresh' };
|
|
@@ -78,6 +78,51 @@ async function checkAuthAndBackend(auth, { timeoutMs = 2500 } = {}) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Fetch subscription tier + remaining credits from /api/billing/balance.
|
|
83
|
+
* Skipped when not signed in or when the backend is offline.
|
|
84
|
+
*
|
|
85
|
+
* Returns one of:
|
|
86
|
+
* { status, label } — shown as a preflight row
|
|
87
|
+
* null — silent (e.g. BYOK or no signal)
|
|
88
|
+
*/
|
|
89
|
+
async function checkCreditsAndPlan(auth, { timeoutMs = 2000 } = {}) {
|
|
90
|
+
const creds = auth.loadCredentials();
|
|
91
|
+
if (!creds.token || !creds.backendUrl) return null;
|
|
92
|
+
try {
|
|
93
|
+
const ctrl = new AbortController();
|
|
94
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
95
|
+
let resp;
|
|
96
|
+
try {
|
|
97
|
+
resp = await fetch(`${creds.backendUrl}/api/billing/balance`, {
|
|
98
|
+
headers: { 'Authorization': `Bearer ${creds.token}` },
|
|
99
|
+
signal: ctrl.signal,
|
|
100
|
+
});
|
|
101
|
+
} finally { clearTimeout(t); }
|
|
102
|
+
if (!resp.ok) return null;
|
|
103
|
+
const data = await resp.json().catch(() => null);
|
|
104
|
+
if (!data) return null;
|
|
105
|
+
|
|
106
|
+
if (data.byok_enabled) {
|
|
107
|
+
return { status: 'ok', label: `Plan: ${(data.tier || 'byok').toUpperCase()} · billed by your provider` };
|
|
108
|
+
}
|
|
109
|
+
const tier = (data.tier || 'free').toUpperCase();
|
|
110
|
+
const remaining = data.balance?.total;
|
|
111
|
+
if (typeof remaining !== 'number') {
|
|
112
|
+
return { status: 'ok', label: `Plan: ${tier}` };
|
|
113
|
+
}
|
|
114
|
+
if (remaining <= 0) {
|
|
115
|
+
return { status: 'fail', label: `Plan: ${tier} · 0 credits remaining`, hint: 'codekepler.ai/pricing to purchase or upgrade' };
|
|
116
|
+
}
|
|
117
|
+
if (remaining < 25) {
|
|
118
|
+
return { status: 'warn', label: `Plan: ${tier} · ${remaining} credits remaining`, hint: 'low balance — codekepler.ai/pricing' };
|
|
119
|
+
}
|
|
120
|
+
return { status: 'ok', label: `Plan: ${tier} · ${remaining} credits remaining` };
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
81
126
|
function checkGit(cwd) {
|
|
82
127
|
if (!hasGitDir(cwd)) return { status: 'warn', label: 'Not a git repository', hint: '`git init` to enable diff / checkpoints' };
|
|
83
128
|
try {
|
|
@@ -272,7 +317,14 @@ export async function runPreflight({ auth, cwd, version, silent = false } = {})
|
|
|
272
317
|
write('\n' + header + '\n\n');
|
|
273
318
|
|
|
274
319
|
const checks = [];
|
|
275
|
-
|
|
320
|
+
const authCheck = await checkAuthAndBackend(auth);
|
|
321
|
+
checks.push(authCheck);
|
|
322
|
+
// Only ask the backend for plan + credits when the auth row is OK; no point
|
|
323
|
+
// hitting /balance if we are not signed in or the backend is offline.
|
|
324
|
+
if (authCheck.status === 'ok') {
|
|
325
|
+
const plan = await checkCreditsAndPlan(auth);
|
|
326
|
+
if (plan) checks.push(plan);
|
|
327
|
+
}
|
|
276
328
|
checks.push(checkGit(cwd));
|
|
277
329
|
checks.push(checkLinters(cwd));
|
|
278
330
|
checks.push(checkProjectMap(cwd));
|
package/src/terminal/repl.mjs
CHANGED
|
@@ -123,6 +123,15 @@ const session = {
|
|
|
123
123
|
totalCost: 0, // accumulated session cost (USD)
|
|
124
124
|
costAccurate: false, // true if backend provides per-model breakdown
|
|
125
125
|
isByok: false, // set from session_info; hides cost + credits when true
|
|
126
|
+
// ── Subscription / credit state (server-authoritative; set from
|
|
127
|
+
// session_info + complete events) ──
|
|
128
|
+
subscriptionTier: null, // 'free' | 'cli' | 'pro' | 'pro_plus' | 'enterprise'
|
|
129
|
+
creditsTotal: null, // remaining credits (included + purchased)
|
|
130
|
+
creditsIncluded: null, // remaining included credits this period
|
|
131
|
+
creditsPurchased: null, // remaining purchased credits
|
|
132
|
+
creditsLimit: null, // per-period included credits limit
|
|
133
|
+
creditsCharged: 0, // session-cumulative server-reported charges
|
|
134
|
+
creditsLowWarned: false, // emit the low-balance hint only once per turn
|
|
126
135
|
};
|
|
127
136
|
|
|
128
137
|
// ── Commands ──
|
|
@@ -206,9 +215,14 @@ function buildContextStrip() {
|
|
|
206
215
|
const elapsed = formatElapsed(session.startTime);
|
|
207
216
|
|
|
208
217
|
// BYOK: user pays the provider directly, suppress credits entirely.
|
|
218
|
+
// Otherwise prefer the server-authoritative session counter, falling back
|
|
219
|
+
// to the local estimate when the backend hasn't pushed any number yet.
|
|
220
|
+
const usedCr = session.creditsCharged > 0
|
|
221
|
+
? session.creditsCharged
|
|
222
|
+
: costToCredits(session.totalCost);
|
|
209
223
|
const right = [
|
|
210
224
|
c.dim(`${formatTokens(totalTokens)} tok`),
|
|
211
|
-
...(session.isByok ? [] : [c.dim(formatCredits(
|
|
225
|
+
...(session.isByok ? [] : [c.dim(formatCredits(usedCr))]),
|
|
212
226
|
c.dim(elapsed),
|
|
213
227
|
].join(c.dim(' · '));
|
|
214
228
|
|
|
@@ -767,6 +781,15 @@ function renderEvent(event) {
|
|
|
767
781
|
// BYOK users pay their model provider directly; the platform does not
|
|
768
782
|
// charge them credits. Hide cost + credits when this flag is set.
|
|
769
783
|
if (typeof data?.is_byok === 'boolean') session.isByok = data.is_byok;
|
|
784
|
+
// Subscription tier + credit balance — backend is authoritative.
|
|
785
|
+
if (data?.subscription_tier) session.subscriptionTier = data.subscription_tier;
|
|
786
|
+
if (typeof data?.credits_included_limit === 'number') session.creditsLimit = data.credits_included_limit;
|
|
787
|
+
const bal = data?.credits_balance;
|
|
788
|
+
if (bal && typeof bal === 'object') {
|
|
789
|
+
if (typeof bal.total === 'number') session.creditsTotal = bal.total;
|
|
790
|
+
if (typeof bal.included === 'number') session.creditsIncluded = bal.included;
|
|
791
|
+
if (typeof bal.purchased === 'number') session.creditsPurchased = bal.purchased;
|
|
792
|
+
}
|
|
770
793
|
break;
|
|
771
794
|
}
|
|
772
795
|
|
|
@@ -826,6 +849,33 @@ function renderEvent(event) {
|
|
|
826
849
|
|
|
827
850
|
session.lastTurnDuration = data?.duration_s || 0;
|
|
828
851
|
|
|
852
|
+
// ── Server-authoritative credits ──
|
|
853
|
+
// Backend sends usage.credits_charged (this turn) + balance (remaining)
|
|
854
|
+
// in the complete event. CLI uses these instead of the local
|
|
855
|
+
// costToCredits estimate so /status and /cost match the dashboard.
|
|
856
|
+
if (!session.isByok) {
|
|
857
|
+
const charged = data?.usage?.credits_charged;
|
|
858
|
+
if (typeof charged === 'number') session.creditsCharged += charged;
|
|
859
|
+
const bal = data?.balance;
|
|
860
|
+
if (bal && typeof bal === 'object') {
|
|
861
|
+
if (typeof bal.total === 'number') session.creditsTotal = bal.total;
|
|
862
|
+
if (typeof bal.included === 'number') session.creditsIncluded = bal.included;
|
|
863
|
+
if (typeof bal.purchased === 'number') session.creditsPurchased = bal.purchased;
|
|
864
|
+
}
|
|
865
|
+
// Warn once per turn when the remaining credits drop below 20% of the
|
|
866
|
+
// tier's included limit (or below 10 absolute for tiny tiers).
|
|
867
|
+
if (!session.creditsLowWarned && typeof session.creditsTotal === 'number' && session.creditsLimit) {
|
|
868
|
+
const threshold = Math.max(10, Math.floor(session.creditsLimit * 0.2));
|
|
869
|
+
if (session.creditsTotal <= threshold && session.creditsTotal > 0) {
|
|
870
|
+
process.stderr.write(`\n ${c.yellow('⚠')} ${c.dim(`${session.creditsTotal} of ${session.creditsLimit} credits remaining on the ${session.subscriptionTier || 'free'} plan. Upgrade at codekepler.ai/pricing.`)}\n`);
|
|
871
|
+
session.creditsLowWarned = true;
|
|
872
|
+
} else if (session.creditsTotal <= 0) {
|
|
873
|
+
process.stderr.write(`\n ${c.red('✗')} ${c.dim(`Credit balance exhausted on the ${session.subscriptionTier || 'free'} plan. Purchase credits at codekepler.ai/pricing or switch to BYOK.`)}\n`);
|
|
874
|
+
session.creditsLowWarned = true;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
829
879
|
// Sync cumulative session cost into the orbit (status bar shows it).
|
|
830
880
|
if (_orbit) _orbit.onCost(session.totalCost);
|
|
831
881
|
|
|
@@ -948,7 +998,18 @@ async function handleCommand(input, ctx) {
|
|
|
948
998
|
if (session.isByok) {
|
|
949
999
|
process.stderr.write(` ${c.dim('Billing')} ${c.green('BYOK')} ${c.dim('(provider-billed)')}\n`);
|
|
950
1000
|
} else {
|
|
951
|
-
|
|
1001
|
+
// Server-authoritative remaining balance; fall back to the per-session
|
|
1002
|
+
// charged tally when balance hasn't been pushed yet.
|
|
1003
|
+
if (session.subscriptionTier) {
|
|
1004
|
+
process.stderr.write(` ${c.dim('Plan')} ${c.brand(session.subscriptionTier.toUpperCase())}\n`);
|
|
1005
|
+
}
|
|
1006
|
+
if (typeof session.creditsTotal === 'number') {
|
|
1007
|
+
const limit = session.creditsLimit ? ` ${c.dim('/ ' + formatCredits(session.creditsLimit))}` : '';
|
|
1008
|
+
const used = session.creditsCharged ? ` ${c.dim(`(${formatCredits(session.creditsCharged)} used this session)`)}` : '';
|
|
1009
|
+
process.stderr.write(` ${c.dim('Credits')} ${formatCredits(session.creditsTotal)}${limit}${used}\n`);
|
|
1010
|
+
} else if (session.creditsCharged) {
|
|
1011
|
+
process.stderr.write(` ${c.dim('Credits')} ${formatCredits(session.creditsCharged)} ${c.dim('(used this session)')}\n`);
|
|
1012
|
+
}
|
|
952
1013
|
}
|
|
953
1014
|
process.stderr.write(` ${c.dim('CWD')} ${safeCwd()}\n`);
|
|
954
1015
|
|
|
@@ -1031,11 +1092,17 @@ async function handleCommand(input, ctx) {
|
|
|
1031
1092
|
process.stderr.write(`\n ${c.bold('Billing')} ${c.green('BYOK')} ${c.dim('— you pay your model provider directly. Kepler does not charge credits for BYOK usage.')}\n\n`);
|
|
1032
1093
|
return;
|
|
1033
1094
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
}
|
|
1095
|
+
// Prefer server-authoritative numbers when available.
|
|
1096
|
+
const used = session.creditsCharged || 0;
|
|
1097
|
+
const usedLabel = formatCredits(used);
|
|
1098
|
+
process.stderr.write(`\n ${c.bold('Session Credits')} ${c.brand(usedLabel)}`);
|
|
1099
|
+
if (used > 0 && !session.creditsCharged) process.stderr.write(` ${c.yellow('(estimated)')}`);
|
|
1038
1100
|
process.stderr.write('\n');
|
|
1101
|
+
if (session.subscriptionTier && typeof session.creditsTotal === 'number') {
|
|
1102
|
+
const remaining = formatCredits(session.creditsTotal);
|
|
1103
|
+
const limit = session.creditsLimit ? ` / ${formatCredits(session.creditsLimit)}` : '';
|
|
1104
|
+
process.stderr.write(` ${c.dim('Plan')} ${c.brand(session.subscriptionTier.toUpperCase())} ${c.dim('· remaining')} ${c.brand(remaining)}${c.dim(limit)}\n`);
|
|
1105
|
+
}
|
|
1039
1106
|
process.stderr.write(` ${c.dim('─'.repeat(70))}\n`);
|
|
1040
1107
|
|
|
1041
1108
|
if (session.costBreakdown.length > 0) {
|
|
@@ -1602,6 +1669,7 @@ export async function startTerminalRepl() {
|
|
|
1602
1669
|
session.subAgentCounts = {};
|
|
1603
1670
|
session.savedUsd = 0;
|
|
1604
1671
|
session._lastEmittedThinking = '';
|
|
1672
|
+
session.creditsLowWarned = false;
|
|
1605
1673
|
|
|
1606
1674
|
// Tell the orbit a new turn started — switches to DISCOVERY and updates
|
|
1607
1675
|
// task / turn counters in the status bar.
|