@cspeach/cli 1.1.32 → 1.1.33
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/commands/cost.js +6 -2
- package/dist/commands/help.js +2 -1
- package/dist/commands/usage.js +106 -0
- package/dist/cost/month-usage.js +45 -0
- package/dist/repl/builtin-commands.js +3 -0
- package/dist/repl/slash-completer.js +1 -0
- package/dist/repl.js +12 -0
- package/dist/skills/bundled-skills.js +1 -1
- package/package.json +1 -1
package/dist/commands/cost.js
CHANGED
|
@@ -83,7 +83,7 @@ export async function runCostCommand(sessionId, credits = globalCredits.snapshot
|
|
|
83
83
|
emit('');
|
|
84
84
|
emit(panelRow('Readers', `${s.readerCalls} run${s.readerCalls === 1 ? '' : 's'} ${formatCost(s.readerCost)}`));
|
|
85
85
|
}
|
|
86
|
-
if (s.byModel.length > 1) {
|
|
86
|
+
if (s.byModel.length > 1 && !inCredits) {
|
|
87
87
|
emit('');
|
|
88
88
|
emit(chalk.bold('By model:'));
|
|
89
89
|
for (const m of s.byModel) {
|
|
@@ -93,7 +93,9 @@ export async function runCostCommand(sessionId, credits = globalCredits.snapshot
|
|
|
93
93
|
emit(panelRow(modelText(m.model), `${m.turns} turn${m.turns === 1 ? '' : 's'}${tail}`, 24));
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
else if (s.byModel[0]) {
|
|
96
|
+
else if (s.byModel[0] && !inCredits) {
|
|
97
|
+
// 2026-10-09 (owner): no model line for managed customers — they pay in
|
|
98
|
+
// credits and cannot change the model here; it only invites questions.
|
|
97
99
|
emit('');
|
|
98
100
|
emit(panelRow('Model', modelText(s.byModel[0].model)));
|
|
99
101
|
}
|
|
@@ -124,6 +126,8 @@ export async function runCostCommand(sessionId, credits = globalCredits.snapshot
|
|
|
124
126
|
+ `their usage is included in a later turn's figure.`).trimEnd());
|
|
125
127
|
}
|
|
126
128
|
emit('');
|
|
129
|
+
if (inCredits)
|
|
130
|
+
emit(formatInfo('This month and your top skills: /usage').trimEnd());
|
|
127
131
|
emit(formatInfo(`Detailed JSONL: ~/.cspeach/sessions/${sessionId}-cost.jsonl`).trimEnd());
|
|
128
132
|
emit('');
|
|
129
133
|
}
|
package/dist/commands/help.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { SKILL_CATALOG } from '../skill-catalog.js';
|
|
3
|
-
import { costCommandDescription } from '../repl/builtin-commands.js';
|
|
3
|
+
import { costCommandDescription, USAGE_DESCRIPTION } from '../repl/builtin-commands.js';
|
|
4
4
|
import { panelHeading } from '../renderer/panel-rows.js';
|
|
5
5
|
import { paint } from '../renderer/theme.js';
|
|
6
6
|
import { uiWidth, truncateToWidth } from '../renderer/ui-width.js';
|
|
@@ -52,6 +52,7 @@ export const HELP_GROUPS = [
|
|
|
52
52
|
{ name: '/new', description: fixed('Next prompt picks a skill afresh (also /reset)') },
|
|
53
53
|
{ name: '/recap', description: fixed('Show the recap (last session, open tasks, plan)') },
|
|
54
54
|
{ name: '/cost', description: costCommandDescription },
|
|
55
|
+
{ name: '/usage', description: fixed(USAGE_DESCRIPTION) },
|
|
55
56
|
{ name: '/context', description: fixed("Show what fills the model's context window") },
|
|
56
57
|
{ name: '/compact', description: fixed('Shrink a long session and keep the facts') },
|
|
57
58
|
{ name: '/help', description: fixed('This screen (also /skills)') },
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/usage` slash command (2026-10-09) — this month at a glance, in credits:
|
|
3
|
+
* a bar of what this month used against what was available, the balance,
|
|
4
|
+
* the top skills and this session. `/cost` stays the per-turn detail.
|
|
5
|
+
*
|
|
6
|
+
* Managed only: the numbers come from the proxy's credit ledger
|
|
7
|
+
* (GET /v1/me/usage). Own key / local mode: no CSPeach ledger exists, so the
|
|
8
|
+
* panel says where monthly usage lives instead. Never a dollar figure.
|
|
9
|
+
* Colours: the panels palette only (panelHeading / panelRow / paint).
|
|
10
|
+
*/
|
|
11
|
+
import { loadConfig } from '../config/loader.js';
|
|
12
|
+
import { getStore } from '../auth/api-key.js';
|
|
13
|
+
import { creditsActive, formatCredits, globalCredits } from '../cost/credits.js';
|
|
14
|
+
import { fetchMonthUsage } from '../cost/month-usage.js';
|
|
15
|
+
import { panelHeading, panelRow } from '../renderer/panel-rows.js';
|
|
16
|
+
import { formatInfo } from '../renderer/notice-shape.js';
|
|
17
|
+
import { paint } from '../renderer/theme.js';
|
|
18
|
+
import { sanitizeControlChars } from '../renderer/sanitize.js';
|
|
19
|
+
import { shouldUseUnicodeBoxChars } from '../renderer/tty.js';
|
|
20
|
+
const BAR_WIDTH = 20;
|
|
21
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
22
|
+
/** used / (used + left), as filled cells of BAR_WIDTH; a non-zero use shows at least one cell. */
|
|
23
|
+
export function usageBar(used, left, unicode = shouldUseUnicodeBoxChars()) {
|
|
24
|
+
const total = used + Math.max(0, left ?? 0);
|
|
25
|
+
let filled = total > 0 ? Math.round((BAR_WIDTH * used) / total) : 0;
|
|
26
|
+
if (used > 0 && filled === 0)
|
|
27
|
+
filled = 1;
|
|
28
|
+
filled = Math.min(BAR_WIDTH, filled);
|
|
29
|
+
const [on, off] = unicode ? ['█', '░'] : ['#', '-'];
|
|
30
|
+
return paint('brand', on.repeat(filled)) + paint('frame', off.repeat(BAR_WIDTH - filled));
|
|
31
|
+
}
|
|
32
|
+
function monthName(iso) {
|
|
33
|
+
const d = iso ? new Date(iso) : new Date();
|
|
34
|
+
return MONTHS[(Number.isNaN(d.getTime()) ? new Date() : d).getUTCMonth()];
|
|
35
|
+
}
|
|
36
|
+
async function defaultDeps() {
|
|
37
|
+
const cfg = await loadConfig();
|
|
38
|
+
return {
|
|
39
|
+
mode: cfg.llm?.mode ?? 'managed',
|
|
40
|
+
fetchUsage: () => fetchMonthUsage(cfg, async () => (await (await getStore()).get()) ?? ''),
|
|
41
|
+
// Credits seen this run (balance deltas); unknown outside credits mode.
|
|
42
|
+
sessionCredits: creditsActive() ? globalCredits.snapshot().sessionTotal : null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export async function runUsageCommand(emit = (l) => console.log(l), deps) {
|
|
46
|
+
// Review fix: a config that fails to load mid-session must not throw out of
|
|
47
|
+
// the REPL handler — fall back to the "could not reach" line.
|
|
48
|
+
let d;
|
|
49
|
+
try {
|
|
50
|
+
d = deps ?? await defaultDeps();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
d = { mode: 'managed', fetchUsage: async () => null, sessionCredits: null };
|
|
54
|
+
}
|
|
55
|
+
emit('');
|
|
56
|
+
if (d.mode !== 'managed') {
|
|
57
|
+
emit(panelHeading('Usage'));
|
|
58
|
+
emit(formatInfo('You are on your own model key, so CSPeach keeps no monthly usage. It is in your provider account. /cost shows this session.').trimEnd());
|
|
59
|
+
emit('');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const u = await d.fetchUsage();
|
|
63
|
+
if (!u) {
|
|
64
|
+
emit(panelHeading('Usage'));
|
|
65
|
+
emit(formatInfo('Could not reach CSPeach for this month’s usage. Try again in a moment. /cost shows this session.').trimEnd());
|
|
66
|
+
emit('');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (!u.metered) {
|
|
70
|
+
emit(panelHeading('Usage'));
|
|
71
|
+
emit(formatInfo('This account is not billed in credits, so there is no monthly credit usage to show. /cost shows this session.').trimEnd());
|
|
72
|
+
emit('');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const unit = u.unitLabel;
|
|
76
|
+
const used = u.monthUsed ?? 0;
|
|
77
|
+
emit(panelHeading('Usage', `this month (${monthName(u.monthStart)}, UTC)`));
|
|
78
|
+
emit('');
|
|
79
|
+
emit(panelRow('This month', `${usageBar(used, u.balance)} ${formatCredits(used, unit)} used`, 14));
|
|
80
|
+
if (u.balance !== null) {
|
|
81
|
+
const split = u.subscription !== null && u.purchased !== null
|
|
82
|
+
? paint('dim', ` monthly ${Math.round(u.subscription).toLocaleString('en-US')} · bought ${Math.round(u.purchased).toLocaleString('en-US')}`)
|
|
83
|
+
: '';
|
|
84
|
+
emit(panelRow('Balance', `${formatCredits(u.balance, unit)} left${split}`, 14));
|
|
85
|
+
}
|
|
86
|
+
if (u.topSkills.length > 0) {
|
|
87
|
+
emit('');
|
|
88
|
+
emit(panelHeading('Top skills this month'));
|
|
89
|
+
const width = Math.max(14, ...u.topSkills.map((s) => sanitizeControlChars(s.skill).length + 2));
|
|
90
|
+
const valueWidth = Math.max(...u.topSkills.map((s) => Math.round(s.credits).toLocaleString('en-US').length));
|
|
91
|
+
for (const s of u.topSkills) {
|
|
92
|
+
emit(panelRow(sanitizeControlChars(s.skill), Math.round(s.credits).toLocaleString('en-US').padStart(valueWidth), width));
|
|
93
|
+
}
|
|
94
|
+
if (u.otherSkills > 0) {
|
|
95
|
+
emit(panelRow('other skills', Math.round(u.otherSkills).toLocaleString('en-US').padStart(valueWidth), width));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
emit('');
|
|
99
|
+
if (d.sessionCredits !== null) {
|
|
100
|
+
emit(panelRow('This session', `${formatCredits(d.sessionCredits, unit)}${paint('dim', ' details: /cost')}`, 14));
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
emit(formatInfo('Details for this session: /cost').trimEnd());
|
|
104
|
+
}
|
|
105
|
+
emit('');
|
|
106
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export const MONTH_USAGE_TIMEOUT_MS = 5_000;
|
|
2
|
+
function num(v) {
|
|
3
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : null;
|
|
4
|
+
}
|
|
5
|
+
export async function fetchMonthUsage(cfg, getBearer, fetchFn = fetch) {
|
|
6
|
+
const controller = new AbortController();
|
|
7
|
+
const timer = setTimeout(() => controller.abort(), MONTH_USAGE_TIMEOUT_MS);
|
|
8
|
+
try {
|
|
9
|
+
const key = await getBearer();
|
|
10
|
+
if (!key)
|
|
11
|
+
return null;
|
|
12
|
+
const r = await fetchFn(`${cfg.proxy_url.replace(/\/$/, '')}/v1/me/usage`, {
|
|
13
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
14
|
+
signal: controller.signal,
|
|
15
|
+
});
|
|
16
|
+
if (!r.ok)
|
|
17
|
+
return null;
|
|
18
|
+
const b = await r.json();
|
|
19
|
+
if (typeof b !== 'object' || b === null)
|
|
20
|
+
return null;
|
|
21
|
+
const o = b;
|
|
22
|
+
const skills = Array.isArray(o.top_skills) ? o.top_skills : [];
|
|
23
|
+
return {
|
|
24
|
+
metered: o.metered === true,
|
|
25
|
+
unitLabel: typeof o.unit_label === 'string' && o.unit_label ? o.unit_label : 'credits',
|
|
26
|
+
monthStart: typeof o.month_start === 'string' ? o.month_start : null,
|
|
27
|
+
monthUsed: num(o.month_used_credits),
|
|
28
|
+
topSkills: skills.flatMap((s) => {
|
|
29
|
+
const e = s;
|
|
30
|
+
const credits = num(e?.credits);
|
|
31
|
+
return typeof e?.skill === 'string' && credits !== null ? [{ skill: e.skill, credits }] : [];
|
|
32
|
+
}),
|
|
33
|
+
otherSkills: num(o.other_skills_credits) ?? 0,
|
|
34
|
+
balance: num(o.balance_credits),
|
|
35
|
+
subscription: num(o.subscription_credits),
|
|
36
|
+
purchased: num(o.purchased_credits),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -14,6 +14,8 @@ import { BUNDLED_RUNS } from '../runs/bundled-runs.js';
|
|
|
14
14
|
export const COST_DESCRIPTION_USD = 'Show this session\'s spend so far ($ and tokens)';
|
|
15
15
|
export const COST_DESCRIPTION_CREDITS = 'Show the credits this session has used so far';
|
|
16
16
|
/** Piece 2 §10: managed customers see credits, never "$" (cost/display-mode.ts). */
|
|
17
|
+
/** /usage (2026-10-09): this month in credits — see commands/usage.ts. */
|
|
18
|
+
export const USAGE_DESCRIPTION = 'Show this month’s credits, your balance and the top skills';
|
|
17
19
|
export function costCommandDescription() {
|
|
18
20
|
return creditsActive() ? COST_DESCRIPTION_CREDITS : COST_DESCRIPTION_USD;
|
|
19
21
|
}
|
|
@@ -21,6 +23,7 @@ export const BUILTIN_ENTRIES = [
|
|
|
21
23
|
// Piece 2: /cancel is not listed — it exists only in the classic loop
|
|
22
24
|
// (repl.tsx), and Esc / Ctrl+C already cancel under Ink.
|
|
23
25
|
{ name: '/cost', get description() { return costCommandDescription(); } },
|
|
26
|
+
{ name: '/usage', description: USAGE_DESCRIPTION },
|
|
24
27
|
{ name: '/compact', description: 'Shrink a long session and keep the facts (later turns cost far less)' },
|
|
25
28
|
{ name: '/export', description: 'Export the signable session audit record — /export audit [filename]' },
|
|
26
29
|
{ name: '/context', description: 'Show what fills the model context window — a composition grid of bars' },
|
package/dist/repl.js
CHANGED
|
@@ -1068,6 +1068,12 @@ export async function runRepl(opts) {
|
|
|
1068
1068
|
printHelp((line) => chunkEmitter.emit('chunk', line + '\n'));
|
|
1069
1069
|
return;
|
|
1070
1070
|
}
|
|
1071
|
+
// /usage (2026-10-09) — this month in credits, from the proxy's ledger.
|
|
1072
|
+
if (trimmed === '/usage') {
|
|
1073
|
+
const { runUsageCommand } = await import('./commands/usage.js');
|
|
1074
|
+
await runUsageCommand((l) => chunkEmitter.emit('chunk', l + '\n'));
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1071
1077
|
// Phase 1 cost tracking (2026-05-16) — /cost prints session $ summary
|
|
1072
1078
|
// from the JSONL log written each turn. Read-only, no network.
|
|
1073
1079
|
if (trimmed === '/cost') {
|
|
@@ -2992,6 +2998,12 @@ export async function runRepl(opts) {
|
|
|
2992
2998
|
printHelp();
|
|
2993
2999
|
continue;
|
|
2994
3000
|
}
|
|
3001
|
+
// /usage (2026-10-09) — classic mirror of the Ink handler above.
|
|
3002
|
+
if (trimmed === '/usage') {
|
|
3003
|
+
const { runUsageCommand } = await import('./commands/usage.js');
|
|
3004
|
+
await runUsageCommand();
|
|
3005
|
+
continue;
|
|
3006
|
+
}
|
|
2995
3007
|
// Phase 1 cost tracking (2026-05-16) — classic-mode /cost.
|
|
2996
3008
|
// Mirrors the Ink-mode handler above. /cost prints session $ summary
|
|
2997
3009
|
// from the JSONL log written each turn. Read-only, no network.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// GENERATED FILE — bundled skills for local mode.
|
|
2
2
|
// Source: https://manifest.cspeach.dev/v1.json
|
|
3
|
-
// Generated: 2026-10-
|
|
3
|
+
// Generated: 2026-10-10T00:40:56.198Z
|
|
4
4
|
// Manifest signature verified at build time.
|
|
5
5
|
export const BUNDLED_SKILLS = {
|
|
6
6
|
"abap-app": {
|