@pencil-agent/nano-pencil 1.9.4 → 1.9.5
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/core/agent-session.d.ts +12 -0
- package/dist/core/agent-session.js +32 -5
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +1 -0
- package/dist/core/sdk.js +3 -0
- package/dist/core/slash-commands.js +1 -0
- package/dist/core/usage-tracker.d.ts +39 -0
- package/dist/core/usage-tracker.js +168 -0
- package/dist/modes/interactive/components/footer.js +27 -0
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.js +32 -0
- package/dist/modes/print-mode.js +16 -0
- package/dist/modes/rpc/rpc-client.d.ts +5 -0
- package/dist/modes/rpc/rpc-client.js +7 -0
- package/dist/modes/rpc/rpc-mode.js +4 -0
- package/dist/modes/rpc/rpc-types.d.ts +10 -0
- package/package.json +1 -1
|
@@ -24,6 +24,8 @@ import type { ResourceLoader } from "./resource-loader.js";
|
|
|
24
24
|
import type { BranchSummaryEntry, SessionManager } from "./session-manager.js";
|
|
25
25
|
import type { SettingsManager } from "./settings-manager.js";
|
|
26
26
|
import type { BashOperations } from "./tools/bash.js";
|
|
27
|
+
import type { UsageTracker } from "./usage-tracker.js";
|
|
28
|
+
import type { UsageStats } from "./usage-tracker.js";
|
|
27
29
|
/** Parsed skill block from a user message */
|
|
28
30
|
export interface ParsedSkillBlock {
|
|
29
31
|
name: string;
|
|
@@ -86,6 +88,8 @@ export interface AgentSessionConfig {
|
|
|
86
88
|
extensionRunnerRef?: {
|
|
87
89
|
current?: ExtensionRunner;
|
|
88
90
|
};
|
|
91
|
+
/** Optional global usage tracker for day/month/total stats */
|
|
92
|
+
usageTracker?: UsageTracker;
|
|
89
93
|
}
|
|
90
94
|
export interface ExtensionBindings {
|
|
91
95
|
uiContext?: ExtensionUIContext;
|
|
@@ -169,6 +173,7 @@ export declare class AgentSession {
|
|
|
169
173
|
private _modelRegistry;
|
|
170
174
|
private _toolRegistry;
|
|
171
175
|
private _baseSystemPrompt;
|
|
176
|
+
private _usageTracker?;
|
|
172
177
|
constructor(config: AgentSessionConfig);
|
|
173
178
|
/** Model registry for API key resolution and model discovery */
|
|
174
179
|
get modelRegistry(): ModelRegistry;
|
|
@@ -579,8 +584,15 @@ export declare class AgentSession {
|
|
|
579
584
|
private _extractUserMessageText;
|
|
580
585
|
/**
|
|
581
586
|
* Get session statistics.
|
|
587
|
+
* Token and cost totals are computed from session entries (same as footer) so they
|
|
588
|
+
* remain accurate after compaction. Message counts and tool counts use in-memory state.
|
|
582
589
|
*/
|
|
583
590
|
getSessionStats(): SessionStats;
|
|
591
|
+
/**
|
|
592
|
+
* Get global usage statistics (by day, by month, total).
|
|
593
|
+
* Returns empty stats if no usage tracker is configured.
|
|
594
|
+
*/
|
|
595
|
+
getUsageStats(): UsageStats;
|
|
584
596
|
getContextUsage(): ContextUsage | undefined;
|
|
585
597
|
/**
|
|
586
598
|
* Export session to HTML.
|
|
@@ -118,6 +118,7 @@ export class AgentSession {
|
|
|
118
118
|
_toolRegistry = new Map();
|
|
119
119
|
// Base system prompt (without extension appends) - used to apply fresh appends each turn
|
|
120
120
|
_baseSystemPrompt = "";
|
|
121
|
+
_usageTracker;
|
|
121
122
|
constructor(config) {
|
|
122
123
|
this.agent = config.agent;
|
|
123
124
|
this.sessionManager = config.sessionManager;
|
|
@@ -131,6 +132,7 @@ export class AgentSession {
|
|
|
131
132
|
this._soulManager = config.soulManager;
|
|
132
133
|
this._initialActiveToolNames = config.initialActiveToolNames;
|
|
133
134
|
this._baseToolsOverride = config.baseToolsOverride;
|
|
135
|
+
this._usageTracker = config.usageTracker;
|
|
134
136
|
// Always subscribe to agent events for internal handling
|
|
135
137
|
// (session persistence, extensions, auto-compaction, retry logic)
|
|
136
138
|
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
@@ -196,9 +198,11 @@ export class AgentSession {
|
|
|
196
198
|
// Track assistant message for auto-compaction (checked on agent_end)
|
|
197
199
|
if (event.message.role === "assistant") {
|
|
198
200
|
this._lastAssistantMessage = event.message;
|
|
201
|
+
// Record global usage for day/month/total stats
|
|
202
|
+
const assistantMsg = event.message;
|
|
203
|
+
this._usageTracker?.record(assistantMsg.usage, assistantMsg.timestamp);
|
|
199
204
|
// Reset retry counter immediately on successful assistant response
|
|
200
205
|
// This prevents accumulation across multiple LLM calls within a turn
|
|
201
|
-
const assistantMsg = event.message;
|
|
202
206
|
if (assistantMsg.stopReason !== "error" && this._retryAttempt > 0) {
|
|
203
207
|
this._emit({
|
|
204
208
|
type: "auto_retry_end",
|
|
@@ -2354,6 +2358,8 @@ export class AgentSession {
|
|
|
2354
2358
|
}
|
|
2355
2359
|
/**
|
|
2356
2360
|
* Get session statistics.
|
|
2361
|
+
* Token and cost totals are computed from session entries (same as footer) so they
|
|
2362
|
+
* remain accurate after compaction. Message counts and tool counts use in-memory state.
|
|
2357
2363
|
*/
|
|
2358
2364
|
getSessionStats() {
|
|
2359
2365
|
const state = this.state;
|
|
@@ -2366,10 +2372,10 @@ export class AgentSession {
|
|
|
2366
2372
|
let totalCacheRead = 0;
|
|
2367
2373
|
let totalCacheWrite = 0;
|
|
2368
2374
|
let totalCost = 0;
|
|
2369
|
-
for
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2375
|
+
// Use entries for token/cost so totals match footer and persist after compaction
|
|
2376
|
+
for (const entry of this.sessionManager.getEntries()) {
|
|
2377
|
+
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
2378
|
+
const assistantMsg = entry.message;
|
|
2373
2379
|
totalInput += assistantMsg.usage.input;
|
|
2374
2380
|
totalOutput += assistantMsg.usage.output;
|
|
2375
2381
|
totalCacheRead += assistantMsg.usage.cacheRead;
|
|
@@ -2377,6 +2383,13 @@ export class AgentSession {
|
|
|
2377
2383
|
totalCost += assistantMsg.usage.cost.total;
|
|
2378
2384
|
}
|
|
2379
2385
|
}
|
|
2386
|
+
// Tool call count from state (entries don't change this)
|
|
2387
|
+
for (const message of state.messages) {
|
|
2388
|
+
if (message.role === "assistant") {
|
|
2389
|
+
const assistantMsg = message;
|
|
2390
|
+
toolCalls += assistantMsg.content.filter((c) => c.type === "toolCall").length;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2380
2393
|
return {
|
|
2381
2394
|
sessionFile: this.sessionFile,
|
|
2382
2395
|
sessionId: this.sessionId,
|
|
@@ -2395,6 +2408,20 @@ export class AgentSession {
|
|
|
2395
2408
|
cost: totalCost,
|
|
2396
2409
|
};
|
|
2397
2410
|
}
|
|
2411
|
+
/**
|
|
2412
|
+
* Get global usage statistics (by day, by month, total).
|
|
2413
|
+
* Returns empty stats if no usage tracker is configured.
|
|
2414
|
+
*/
|
|
2415
|
+
getUsageStats() {
|
|
2416
|
+
if (!this._usageTracker) {
|
|
2417
|
+
return {
|
|
2418
|
+
total: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 },
|
|
2419
|
+
byDay: {},
|
|
2420
|
+
byMonth: {},
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
return this._usageTracker.getUsageStats();
|
|
2424
|
+
}
|
|
2398
2425
|
getContextUsage() {
|
|
2399
2426
|
const model = this.model;
|
|
2400
2427
|
if (!model)
|
package/dist/core/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Core modules shared between all run modes.
|
|
3
3
|
*/
|
|
4
4
|
export { AgentSession, type AgentSessionConfig, type AgentSessionEvent, type AgentSessionEventListener, type ModelCycleResult, type PromptOptions, type SessionStats, } from "./agent-session.js";
|
|
5
|
+
export type { UsageStats, UsageTotals } from "./usage-tracker.js";
|
|
6
|
+
export { UsageTracker } from "./usage-tracker.js";
|
|
5
7
|
export { type BashExecutorOptions, type BashResult, executeBash, executeBashWithOperations, } from "./bash-executor.js";
|
|
6
8
|
export type { CompactionResult } from "./compaction/index.js";
|
|
7
9
|
export { createEventBus, type EventBus, type EventBusController, } from "./event-bus.js";
|
package/dist/core/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Core modules shared between all run modes.
|
|
3
3
|
*/
|
|
4
4
|
export { AgentSession, } from "./agent-session.js";
|
|
5
|
+
export { UsageTracker } from "./usage-tracker.js";
|
|
5
6
|
export { executeBash, executeBashWithOperations, } from "./bash-executor.js";
|
|
6
7
|
export { createEventBus, } from "./event-bus.js";
|
|
7
8
|
// Extensions system
|
package/dist/core/sdk.js
CHANGED
|
@@ -12,6 +12,7 @@ import { DefaultResourceLoader } from "./resource-loader.js";
|
|
|
12
12
|
import { SessionManager } from "./session-manager.js";
|
|
13
13
|
import { SettingsManager } from "./settings-manager.js";
|
|
14
14
|
import { time } from "./timings.js";
|
|
15
|
+
import { UsageTracker } from "./usage-tracker.js";
|
|
15
16
|
import { isSoulEnabled, createSoulManager, } from "./soul-integration.js";
|
|
16
17
|
import { allTools, bashTool, codingTools, createBashTool, createCodingTools, createEditTool, createFindTool, createGrepTool, createLsTool, createReadOnlyTools, createReadTool, createWriteTool, editTool, findTool, grepTool, lsTool, readOnlyTools, readTool, writeTool, } from "./tools/index.js";
|
|
17
18
|
export {
|
|
@@ -275,6 +276,7 @@ export async function createAgentSession(options = {}) {
|
|
|
275
276
|
console.warn(`Failed to initialize Soul: ${error}`);
|
|
276
277
|
}
|
|
277
278
|
}
|
|
279
|
+
const usageTracker = new UsageTracker(agentDir);
|
|
278
280
|
const session = new AgentSession({
|
|
279
281
|
agent,
|
|
280
282
|
sessionManager,
|
|
@@ -287,6 +289,7 @@ export async function createAgentSession(options = {}) {
|
|
|
287
289
|
initialActiveToolNames,
|
|
288
290
|
extensionRunnerRef,
|
|
289
291
|
soulManager,
|
|
292
|
+
usageTracker,
|
|
290
293
|
});
|
|
291
294
|
const extensionsResult = resourceLoader.getExtensions();
|
|
292
295
|
return {
|
|
@@ -17,6 +17,7 @@ export const BUILTIN_SLASH_COMMANDS = [
|
|
|
17
17
|
{ name: "copy", description: "Copy last agent message to clipboard" },
|
|
18
18
|
{ name: "name", description: "Set session display name" },
|
|
19
19
|
{ name: "session", description: "Show session info and stats" },
|
|
20
|
+
{ name: "usage", description: "Show token usage (today, month, total)" },
|
|
20
21
|
{ name: "changelog", description: "Show changelog entries" },
|
|
21
22
|
{ name: "hotkeys", description: "Show all keyboard shortcuts" },
|
|
22
23
|
{ name: "fork", description: "Create a new fork from a previous message" },
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Usage } from "@pencil-agent/ai";
|
|
2
|
+
export interface UsageTotals {
|
|
3
|
+
input: number;
|
|
4
|
+
output: number;
|
|
5
|
+
cacheRead: number;
|
|
6
|
+
cacheWrite: number;
|
|
7
|
+
cost: number;
|
|
8
|
+
}
|
|
9
|
+
export interface UsageStats {
|
|
10
|
+
total: UsageTotals;
|
|
11
|
+
byDay: Record<string, UsageTotals>;
|
|
12
|
+
byMonth: Record<string, UsageTotals>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Tracks token usage across all sessions and runs.
|
|
16
|
+
*
|
|
17
|
+
* Writes an append-only JSONL log (usage.jsonl) plus a small running total file
|
|
18
|
+
* (usage-total.json) for fast "global total" queries.
|
|
19
|
+
*/
|
|
20
|
+
export declare class UsageTracker {
|
|
21
|
+
private agentDir;
|
|
22
|
+
private usageLogPath;
|
|
23
|
+
private totalPath;
|
|
24
|
+
constructor(agentDir: string);
|
|
25
|
+
/**
|
|
26
|
+
* Record usage for a single assistant response.
|
|
27
|
+
*
|
|
28
|
+
* Safe to call frequently; errors are swallowed so as not to affect the agent.
|
|
29
|
+
*/
|
|
30
|
+
record(usage: Usage, timestampMs?: number): void;
|
|
31
|
+
/**
|
|
32
|
+
* Get aggregated usage statistics:
|
|
33
|
+
* - total: global totals across all time
|
|
34
|
+
* - byDay: grouped by YYYY-MM-DD
|
|
35
|
+
* - byMonth: grouped by YYYY-MM
|
|
36
|
+
*/
|
|
37
|
+
getUsageStats(): UsageStats;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=usage-tracker.d.ts.map
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { mkdirSync, existsSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
function emptyTotals() {
|
|
4
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
5
|
+
}
|
|
6
|
+
function addInto(target, delta) {
|
|
7
|
+
target.input += delta.input;
|
|
8
|
+
target.output += delta.output;
|
|
9
|
+
target.cacheRead += delta.cacheRead;
|
|
10
|
+
target.cacheWrite += delta.cacheWrite;
|
|
11
|
+
target.cost += delta.cost;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Tracks token usage across all sessions and runs.
|
|
15
|
+
*
|
|
16
|
+
* Writes an append-only JSONL log (usage.jsonl) plus a small running total file
|
|
17
|
+
* (usage-total.json) for fast "global total" queries.
|
|
18
|
+
*/
|
|
19
|
+
export class UsageTracker {
|
|
20
|
+
agentDir;
|
|
21
|
+
usageLogPath;
|
|
22
|
+
totalPath;
|
|
23
|
+
constructor(agentDir) {
|
|
24
|
+
this.agentDir = agentDir;
|
|
25
|
+
this.usageLogPath = join(agentDir, "usage.jsonl");
|
|
26
|
+
this.totalPath = join(agentDir, "usage-total.json");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Record usage for a single assistant response.
|
|
30
|
+
*
|
|
31
|
+
* Safe to call frequently; errors are swallowed so as not to affect the agent.
|
|
32
|
+
*/
|
|
33
|
+
record(usage, timestampMs) {
|
|
34
|
+
try {
|
|
35
|
+
// Ensure base directory exists
|
|
36
|
+
if (!existsSync(this.agentDir)) {
|
|
37
|
+
mkdirSync(this.agentDir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
const date = timestampMs ? new Date(timestampMs) : new Date();
|
|
40
|
+
const iso = date.toISOString();
|
|
41
|
+
const day = iso.slice(0, 10); // YYYY-MM-DD
|
|
42
|
+
const month = iso.slice(0, 7); // YYYY-MM
|
|
43
|
+
const delta = {
|
|
44
|
+
input: usage.input ?? 0,
|
|
45
|
+
output: usage.output ?? 0,
|
|
46
|
+
cacheRead: usage.cacheRead ?? 0,
|
|
47
|
+
cacheWrite: usage.cacheWrite ?? 0,
|
|
48
|
+
cost: usage.cost?.total ?? 0,
|
|
49
|
+
};
|
|
50
|
+
// Append one line to usage.jsonl
|
|
51
|
+
const logEntry = JSON.stringify({
|
|
52
|
+
date: day,
|
|
53
|
+
month,
|
|
54
|
+
input: delta.input,
|
|
55
|
+
output: delta.output,
|
|
56
|
+
cacheRead: delta.cacheRead,
|
|
57
|
+
cacheWrite: delta.cacheWrite,
|
|
58
|
+
cost: delta.cost,
|
|
59
|
+
});
|
|
60
|
+
appendFileSync(this.usageLogPath, logEntry + "\n");
|
|
61
|
+
// Update running total (best-effort)
|
|
62
|
+
let total = emptyTotals();
|
|
63
|
+
if (existsSync(this.totalPath)) {
|
|
64
|
+
try {
|
|
65
|
+
const raw = readFileSync(this.totalPath, "utf8");
|
|
66
|
+
const parsed = JSON.parse(raw);
|
|
67
|
+
total = {
|
|
68
|
+
input: parsed.input ?? 0,
|
|
69
|
+
output: parsed.output ?? 0,
|
|
70
|
+
cacheRead: parsed.cacheRead ?? 0,
|
|
71
|
+
cacheWrite: parsed.cacheWrite ?? 0,
|
|
72
|
+
cost: parsed.cost ?? 0,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Ignore parse errors and start fresh
|
|
77
|
+
total = emptyTotals();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
addInto(total, delta);
|
|
81
|
+
writeFileSync(this.totalPath, JSON.stringify(total));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Swallow all errors - usage tracking must never break the agent
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get aggregated usage statistics:
|
|
89
|
+
* - total: global totals across all time
|
|
90
|
+
* - byDay: grouped by YYYY-MM-DD
|
|
91
|
+
* - byMonth: grouped by YYYY-MM
|
|
92
|
+
*/
|
|
93
|
+
getUsageStats() {
|
|
94
|
+
const total = emptyTotals();
|
|
95
|
+
const byDay = Object.create(null);
|
|
96
|
+
const byMonth = Object.create(null);
|
|
97
|
+
// Load global total if available
|
|
98
|
+
if (existsSync(this.totalPath)) {
|
|
99
|
+
try {
|
|
100
|
+
const raw = readFileSync(this.totalPath, "utf8");
|
|
101
|
+
const parsed = JSON.parse(raw);
|
|
102
|
+
total.input = parsed.input ?? 0;
|
|
103
|
+
total.output = parsed.output ?? 0;
|
|
104
|
+
total.cacheRead = parsed.cacheRead ?? 0;
|
|
105
|
+
total.cacheWrite = parsed.cacheWrite ?? 0;
|
|
106
|
+
total.cost = parsed.cost ?? 0;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Ignore and fall back to computing from log
|
|
110
|
+
total.input = 0;
|
|
111
|
+
total.output = 0;
|
|
112
|
+
total.cacheRead = 0;
|
|
113
|
+
total.cacheWrite = 0;
|
|
114
|
+
total.cost = 0;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (existsSync(this.usageLogPath)) {
|
|
118
|
+
try {
|
|
119
|
+
const raw = readFileSync(this.usageLogPath, "utf8");
|
|
120
|
+
const lines = raw.split("\n");
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
const trimmed = line.trim();
|
|
123
|
+
if (!trimmed)
|
|
124
|
+
continue;
|
|
125
|
+
let entry;
|
|
126
|
+
try {
|
|
127
|
+
entry = JSON.parse(trimmed);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const day = typeof entry.date === "string" ? entry.date : undefined;
|
|
133
|
+
const month = typeof entry.month === "string" ? entry.month : undefined;
|
|
134
|
+
const delta = {
|
|
135
|
+
input: Number(entry.input) || 0,
|
|
136
|
+
output: Number(entry.output) || 0,
|
|
137
|
+
cacheRead: Number(entry.cacheRead) || 0,
|
|
138
|
+
cacheWrite: Number(entry.cacheWrite) || 0,
|
|
139
|
+
cost: Number(entry.cost) || 0,
|
|
140
|
+
};
|
|
141
|
+
// If total file was missing or corrupt, rebuild it from log
|
|
142
|
+
if (!existsSync(this.totalPath)) {
|
|
143
|
+
addInto(total, delta);
|
|
144
|
+
}
|
|
145
|
+
if (day) {
|
|
146
|
+
if (!byDay[day])
|
|
147
|
+
byDay[day] = emptyTotals();
|
|
148
|
+
addInto(byDay[day], delta);
|
|
149
|
+
}
|
|
150
|
+
if (month) {
|
|
151
|
+
if (!byMonth[month])
|
|
152
|
+
byMonth[month] = emptyTotals();
|
|
153
|
+
addInto(byMonth[month], delta);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// If we had to rebuild total, persist it for next time
|
|
157
|
+
if (!existsSync(this.totalPath)) {
|
|
158
|
+
writeFileSync(this.totalPath, JSON.stringify(total));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// Ignore log read errors
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { total, byDay, byMonth };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=usage-tracker.js.map
|
|
@@ -197,6 +197,33 @@ export class FooterComponent {
|
|
|
197
197
|
const remainder = statsLine.slice(statsLeft.length); // padding + rightSide
|
|
198
198
|
const dimRemainder = theme.fg("dim", remainder);
|
|
199
199
|
const lines = [theme.fg("dim", pwd), dimStatsLeft + dimRemainder];
|
|
200
|
+
// Global usage line: Today | Month | Total (only when any has data)
|
|
201
|
+
const usageStats = this.session.getUsageStats();
|
|
202
|
+
const todayKey = new Date().toISOString().slice(0, 10);
|
|
203
|
+
const monthKey = new Date().toISOString().slice(0, 7);
|
|
204
|
+
const today = usageStats.byDay[todayKey];
|
|
205
|
+
const month = usageStats.byMonth[monthKey];
|
|
206
|
+
const total = usageStats.total;
|
|
207
|
+
const hasGlobal = (today && (today.input > 0 || today.output > 0)) ||
|
|
208
|
+
(month && (month.input > 0 || month.output > 0)) ||
|
|
209
|
+
total.input > 0 ||
|
|
210
|
+
total.output > 0;
|
|
211
|
+
if (hasGlobal) {
|
|
212
|
+
const seg = (label, t) => `${label}: ↑${formatTokens(t.input)} ↓${formatTokens(t.output)}`;
|
|
213
|
+
const parts = [];
|
|
214
|
+
if (today && (today.input > 0 || today.output > 0)) {
|
|
215
|
+
parts.push(seg("Today", today));
|
|
216
|
+
}
|
|
217
|
+
if (month && (month.input > 0 || month.output > 0)) {
|
|
218
|
+
parts.push(seg("Month", month));
|
|
219
|
+
}
|
|
220
|
+
if (total.input > 0 || total.output > 0) {
|
|
221
|
+
parts.push(seg("Total", total));
|
|
222
|
+
}
|
|
223
|
+
if (parts.length > 0) {
|
|
224
|
+
lines.push(theme.fg("dim", truncateToWidth(parts.join(" | "), width, theme.fg("dim", "..."))));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
200
227
|
// Add extension statuses on a single line, sorted by key alphabetically
|
|
201
228
|
const extensionStatuses = this.footerData.getExtensionStatuses();
|
|
202
229
|
if (extensionStatuses.size > 0) {
|
|
@@ -302,6 +302,7 @@ export declare class InteractiveMode {
|
|
|
302
302
|
private handleCopyCommand;
|
|
303
303
|
private handleNameCommand;
|
|
304
304
|
private handleSessionCommand;
|
|
305
|
+
private handleUsageCommand;
|
|
305
306
|
private handleChangelogCommand;
|
|
306
307
|
/**
|
|
307
308
|
* Capitalize keybinding for display (e.g., "ctrl+c" -> "Ctrl+C").
|
|
@@ -1579,6 +1579,11 @@ export class InteractiveMode {
|
|
|
1579
1579
|
this.editor.setText("");
|
|
1580
1580
|
return;
|
|
1581
1581
|
}
|
|
1582
|
+
if (text === "/usage") {
|
|
1583
|
+
this.handleUsageCommand();
|
|
1584
|
+
this.editor.setText("");
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1582
1587
|
if (text === "/changelog") {
|
|
1583
1588
|
this.handleChangelogCommand();
|
|
1584
1589
|
this.editor.setText("");
|
|
@@ -3594,6 +3599,33 @@ export class InteractiveMode {
|
|
|
3594
3599
|
this.chatContainer.addChild(new Text(info, 1, 0));
|
|
3595
3600
|
this.ui.requestRender();
|
|
3596
3601
|
}
|
|
3602
|
+
handleUsageCommand() {
|
|
3603
|
+
const stats = this.session.getUsageStats();
|
|
3604
|
+
const todayKey = new Date().toISOString().slice(0, 10);
|
|
3605
|
+
const monthKey = new Date().toISOString().slice(0, 7);
|
|
3606
|
+
const today = stats.byDay[todayKey] ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
3607
|
+
const month = stats.byMonth[monthKey] ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
3608
|
+
const total = stats.total;
|
|
3609
|
+
const formatBlock = (label, t) => {
|
|
3610
|
+
let block = `${theme.bold(label)}\n`;
|
|
3611
|
+
block += `${theme.fg("dim", "Input:")} ${t.input.toLocaleString()}\n`;
|
|
3612
|
+
block += `${theme.fg("dim", "Output:")} ${t.output.toLocaleString()}\n`;
|
|
3613
|
+
if (t.cacheRead > 0)
|
|
3614
|
+
block += `${theme.fg("dim", "Cache Read:")} ${t.cacheRead.toLocaleString()}\n`;
|
|
3615
|
+
if (t.cacheWrite > 0)
|
|
3616
|
+
block += `${theme.fg("dim", "Cache Write:")} ${t.cacheWrite.toLocaleString()}\n`;
|
|
3617
|
+
if (t.cost > 0)
|
|
3618
|
+
block += `${theme.fg("dim", "Cost:")} $${t.cost.toFixed(4)}\n`;
|
|
3619
|
+
return block;
|
|
3620
|
+
};
|
|
3621
|
+
let info = `${theme.bold("Token Usage")}\n\n`;
|
|
3622
|
+
info += formatBlock("Today", today) + "\n";
|
|
3623
|
+
info += formatBlock("This Month", month) + "\n";
|
|
3624
|
+
info += formatBlock("Total", total);
|
|
3625
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
3626
|
+
this.chatContainer.addChild(new Text(info, 1, 0));
|
|
3627
|
+
this.ui.requestRender();
|
|
3628
|
+
}
|
|
3597
3629
|
handleChangelogCommand() {
|
|
3598
3630
|
const changelogPath = getChangelogPath();
|
|
3599
3631
|
const allEntries = parseChangelog(changelogPath);
|
package/dist/modes/print-mode.js
CHANGED
|
@@ -85,6 +85,22 @@ export async function runPrintMode(session, options) {
|
|
|
85
85
|
console.log(content.text);
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
+
// Output token usage for this response
|
|
89
|
+
const u = assistantMsg.usage;
|
|
90
|
+
if (u.input > 0 || u.output > 0 || u.cacheRead > 0 || u.cacheWrite > 0 || (u.cost?.total ?? 0) > 0) {
|
|
91
|
+
const parts = [];
|
|
92
|
+
if (u.input > 0)
|
|
93
|
+
parts.push(`input ${u.input.toLocaleString()}`);
|
|
94
|
+
if (u.output > 0)
|
|
95
|
+
parts.push(`output ${u.output.toLocaleString()}`);
|
|
96
|
+
if (u.cacheRead > 0)
|
|
97
|
+
parts.push(`cacheRead ${u.cacheRead.toLocaleString()}`);
|
|
98
|
+
if (u.cacheWrite > 0)
|
|
99
|
+
parts.push(`cacheWrite ${u.cacheWrite.toLocaleString()}`);
|
|
100
|
+
if (u.cost?.total != null && u.cost.total > 0)
|
|
101
|
+
parts.push(`cost $${u.cost.total.toFixed(4)}`);
|
|
102
|
+
console.error(`Tokens: ${parts.join(", ")}`);
|
|
103
|
+
}
|
|
88
104
|
}
|
|
89
105
|
}
|
|
90
106
|
// Ensure stdout is fully flushed before returning
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { AgentEvent, AgentMessage, ThinkingLevel } from "@pencil-agent/agent-core";
|
|
7
7
|
import type { ImageContent } from "@pencil-agent/ai";
|
|
8
8
|
import type { SessionStats } from "../../core/agent-session.js";
|
|
9
|
+
import type { UsageStats } from "../../core/usage-tracker.js";
|
|
9
10
|
import type { BashResult } from "../../core/bash-executor.js";
|
|
10
11
|
import type { CompactionResult } from "../../core/compaction/index.js";
|
|
11
12
|
import type { RpcSessionState, RpcSlashCommand } from "./rpc-types.js";
|
|
@@ -153,6 +154,10 @@ export declare class RpcClient {
|
|
|
153
154
|
* Get session statistics.
|
|
154
155
|
*/
|
|
155
156
|
getSessionStats(): Promise<SessionStats>;
|
|
157
|
+
/**
|
|
158
|
+
* Get global token usage statistics (today, by month, total).
|
|
159
|
+
*/
|
|
160
|
+
getUsageStats(): Promise<UsageStats>;
|
|
156
161
|
/**
|
|
157
162
|
* Export session to HTML.
|
|
158
163
|
*/
|
|
@@ -237,6 +237,13 @@ export class RpcClient {
|
|
|
237
237
|
const response = await this.send({ type: "get_session_stats" });
|
|
238
238
|
return this.getData(response);
|
|
239
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Get global token usage statistics (today, by month, total).
|
|
242
|
+
*/
|
|
243
|
+
async getUsageStats() {
|
|
244
|
+
const response = await this.send({ type: "get_usage_stats" });
|
|
245
|
+
return this.getData(response);
|
|
246
|
+
}
|
|
240
247
|
/**
|
|
241
248
|
* Export session to HTML.
|
|
242
249
|
*/
|
|
@@ -384,6 +384,10 @@ export async function runRpcMode(session) {
|
|
|
384
384
|
const stats = session.getSessionStats();
|
|
385
385
|
return success(id, "get_session_stats", stats);
|
|
386
386
|
}
|
|
387
|
+
case "get_usage_stats": {
|
|
388
|
+
const usageStats = session.getUsageStats();
|
|
389
|
+
return success(id, "get_usage_stats", usageStats);
|
|
390
|
+
}
|
|
387
391
|
case "export_html": {
|
|
388
392
|
const path = await session.exportToHtml(command.outputPath);
|
|
389
393
|
return success(id, "export_html", { path });
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { AgentMessage, ThinkingLevel } from "@pencil-agent/agent-core";
|
|
8
8
|
import type { ImageContent, Model } from "@pencil-agent/ai";
|
|
9
9
|
import type { SessionStats } from "../../core/agent-session.js";
|
|
10
|
+
import type { UsageStats } from "../../core/usage-tracker.js";
|
|
10
11
|
import type { BashResult } from "../../core/bash-executor.js";
|
|
11
12
|
import type { CompactionResult } from "../../core/compaction/index.js";
|
|
12
13
|
export type RpcCommand = {
|
|
@@ -86,6 +87,9 @@ export type RpcCommand = {
|
|
|
86
87
|
} | {
|
|
87
88
|
id?: string;
|
|
88
89
|
type: "get_session_stats";
|
|
90
|
+
} | {
|
|
91
|
+
id?: string;
|
|
92
|
+
type: "get_usage_stats";
|
|
89
93
|
} | {
|
|
90
94
|
id?: string;
|
|
91
95
|
type: "export_html";
|
|
@@ -261,6 +265,12 @@ export type RpcResponse = {
|
|
|
261
265
|
command: "get_session_stats";
|
|
262
266
|
success: true;
|
|
263
267
|
data: SessionStats;
|
|
268
|
+
} | {
|
|
269
|
+
id?: string;
|
|
270
|
+
type: "response";
|
|
271
|
+
command: "get_usage_stats";
|
|
272
|
+
success: true;
|
|
273
|
+
data: UsageStats;
|
|
264
274
|
} | {
|
|
265
275
|
id?: string;
|
|
266
276
|
type: "response";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pencil-agent/nano-pencil",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.5",
|
|
4
4
|
"description": "CLI writing agent with read, bash, edit, write tools and session management. Based on pi; supports DashScope Coding Plan. Soul enabled by default for AI personality evolution.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|