@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.
@@ -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 (const message of state.messages) {
2370
- if (message.role === "assistant") {
2371
- const assistantMsg = message;
2372
- toolCalls += assistantMsg.content.filter((c) => c.type === "toolCall").length;
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)
@@ -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";
@@ -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);
@@ -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.4",
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": {