evolclaw 3.2.0 → 3.3.0

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.
Files changed (83) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +1 -2
  3. package/dist/agents/{resolve.js → baseagent.js} +34 -5
  4. package/dist/agents/claude-runner.js +120 -27
  5. package/dist/agents/codex-app-server-client.js +364 -0
  6. package/dist/agents/codex-runner.js +1069 -141
  7. package/dist/agents/gemini-runner.js +2 -2
  8. package/dist/agents/runner-types.js +28 -0
  9. package/dist/aun/aid/store.js +1 -1
  10. package/dist/aun/storage/download.js +1 -1
  11. package/dist/aun/storage/upload.js +13 -1
  12. package/dist/channels/aun.js +406 -293
  13. package/dist/channels/dingtalk.js +77 -140
  14. package/dist/channels/feishu.js +97 -150
  15. package/dist/channels/qqbot.js +75 -138
  16. package/dist/channels/wechat.js +75 -136
  17. package/dist/channels/wecom.js +75 -138
  18. package/dist/cli/agent.js +8 -5
  19. package/dist/cli/index.js +177 -44
  20. package/dist/cli/init.js +33 -6
  21. package/dist/cli/model.js +1 -1
  22. package/dist/cli/stats.js +558 -0
  23. package/dist/cli/version.js +87 -0
  24. package/dist/cli/watch-msg.js +5 -2
  25. package/dist/config-store.js +12 -6
  26. package/dist/core/channel-loader.js +84 -82
  27. package/dist/core/command-handler.js +473 -114
  28. package/dist/core/evolagent-registry.js +1 -0
  29. package/dist/core/evolagent.js +1 -1
  30. package/dist/core/interaction-router.js +8 -0
  31. package/dist/core/message/command-handler-agent-control.js +63 -1
  32. package/dist/core/message/im-renderer.js +35 -13
  33. package/dist/core/message/items-formatter.js +9 -1
  34. package/dist/core/message/message-bridge.js +49 -21
  35. package/dist/core/message/message-log.js +1 -0
  36. package/dist/core/message/message-processor.js +295 -35
  37. package/dist/core/message/message-queue.js +2 -2
  38. package/dist/core/message/pending-hints.js +232 -0
  39. package/dist/core/message/response-depth.js +56 -0
  40. package/dist/core/model/model-catalog.js +1 -1
  41. package/dist/core/model/model-scope.js +2 -2
  42. package/dist/core/permission.js +9 -12
  43. package/dist/core/relation/peer-identity.js +16 -1
  44. package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
  45. package/dist/core/session/session-manager.js +27 -13
  46. package/dist/core/session/session-title.js +26 -0
  47. package/dist/core/stats/billing.js +151 -0
  48. package/dist/core/stats/budget.js +93 -0
  49. package/dist/core/stats/db.js +314 -0
  50. package/dist/core/stats/eck-vars.js +84 -0
  51. package/dist/core/stats/index.js +10 -0
  52. package/dist/core/stats/normalizer.js +78 -0
  53. package/dist/core/stats/query.js +760 -0
  54. package/dist/core/stats/writer.js +115 -0
  55. package/dist/core/trigger/manager.js +34 -0
  56. package/dist/core/trigger/parser.js +9 -3
  57. package/dist/core/trigger/scheduler.js +20 -17
  58. package/dist/{agents → eck}/manifest-engine.js +20 -1
  59. package/dist/{agents → eck}/message-renderer.js +24 -1
  60. package/dist/index.js +130 -8
  61. package/dist/ipc.js +17 -1
  62. package/dist/utils/cross-platform.js +23 -5
  63. package/dist/utils/ecweb-pair.js +20 -0
  64. package/dist/utils/stats.js +14 -0
  65. package/kits/docs/evolclaw/INDEX.md +3 -1
  66. package/kits/docs/evolclaw/fs-architecture.md +1215 -0
  67. package/kits/docs/evolclaw/fs.md +131 -0
  68. package/kits/docs/evolclaw/group-fs.md +209 -0
  69. package/kits/docs/evolclaw/stats.md +70 -0
  70. package/kits/docs/venues/aun-group.md +29 -6
  71. package/kits/docs/venues/group.md +5 -4
  72. package/kits/eck_manifest.json +12 -0
  73. package/kits/eck_message_manifest.json +30 -3
  74. package/kits/rules/05-venue.md +1 -1
  75. package/kits/templates/message-fragments/inject-default.md +2 -0
  76. package/kits/templates/system-fragments/response-depth.md +16 -0
  77. package/package.json +4 -4
  78. package/dist/agents/baseagent-normalize.js +0 -19
  79. package/dist/core/relation/peer-key.js +0 -16
  80. package/dist/evolclaw-config.js +0 -11
  81. package/dist/utils/channel-helpers.js +0 -46
  82. /package/dist/core/{cache/file-cache.js → daemon-file-cache.js} +0 -0
  83. /package/dist/{agents → eck}/kit-renderer.js +0 -0
package/dist/cli/init.js CHANGED
@@ -4,10 +4,9 @@ import readline from 'readline';
4
4
  import { resolvePaths, ensureDataDirs } from '../paths.js';
5
5
  import { commandExists } from '../utils/cross-platform.js';
6
6
  import { scanInstances } from '../utils/instance-registry.js';
7
- import { saveDefaultsSafe, loadAllAgents, migrateProcessConfigIfNeeded } from '../config-store.js';
8
- import { loadEvolclawConfig, saveEvolclawConfig } from '../evolclaw-config.js';
7
+ import { saveDefaultsSafe, loadAllAgents, migrateProcessConfigIfNeeded, loadEvolclawConfig, saveEvolclawConfig } from '../config-store.js';
9
8
  import { generateControlAid } from '../aun/aid/control-aid.js';
10
- import { isCodexSdkAvailable } from '../agents/codex-runner.js';
9
+ import { getCodexAppServerAvailability, isCodexAppServerAvailable } from '../agents/codex-runner.js';
11
10
  // ==================== Helpers ====================
12
11
  function ask(rl, question) {
13
12
  return new Promise(resolve => rl.question(question, resolve));
@@ -15,7 +14,7 @@ function ask(rl, question) {
15
14
  const BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini'];
16
15
  function isBaseagentAvailable(baseagent) {
17
16
  if (baseagent === 'codex')
18
- return isCodexSdkAvailable();
17
+ return isCodexAppServerAvailable();
19
18
  return commandExists(baseagent);
20
19
  }
21
20
  function detectAvailable() {
@@ -80,7 +79,7 @@ export async function cmdInit(options) {
80
79
  console.log('❌ 未检测到可用 baseagent。请安装至少一款:');
81
80
  console.log(' - claude CLI');
82
81
  console.log(' - gemini CLI');
83
- console.log(' - optional dependency @openai/codex-sdk');
82
+ console.log(' - codex CLI with app-server');
84
83
  console.log('\n安装后重新运行 evolclaw init');
85
84
  return;
86
85
  }
@@ -104,7 +103,10 @@ export async function cmdInit(options) {
104
103
  return; // 硬错误:不落 tail
105
104
  }
106
105
  if (!available.includes(options.baseagent)) {
107
- console.log(`❌ ${options.baseagent} 当前环境不可用(可用: ${available.join('/')})`);
106
+ const reason = options.baseagent === 'codex'
107
+ ? getCodexAppServerAvailability().reason
108
+ : undefined;
109
+ console.log(`❌ ${options.baseagent} 当前环境不可用${reason ? `:${reason}` : `(可用: ${available.join('/')})`}`);
108
110
  return; // 硬错误:不落 tail
109
111
  }
110
112
  chosen = options.baseagent;
@@ -251,6 +253,31 @@ export async function initTail() {
251
253
  catch { /* ignore */ }
252
254
  }
253
255
  }
256
+ // ECWeb:交互式 + 未配置时询问是否启用
257
+ if (process.stdin.isTTY) {
258
+ const evcEcweb = loadEvolclawConfig();
259
+ if (evcEcweb.ecweb?.enabled === undefined) {
260
+ const rlEcweb = readline.createInterface({ input: process.stdin, output: process.stdout });
261
+ try {
262
+ const ans = (await ask(rlEcweb, '\n是否在 evolclaw start 时自动启动 ECWeb 控制台?[y/N] ')).trim().toLowerCase();
263
+ if (ans === 'y' || ans === 'yes') {
264
+ saveEvolclawConfig({ ...loadEvolclawConfig(), ecweb: { enabled: true } });
265
+ console.log(' ✓ 已启用 ECWeb(evolclaw start 将自动在后台启动)');
266
+ console.log(' 提示:首次访问运行 ec watch web 查看配对码和 URL');
267
+ }
268
+ else {
269
+ saveEvolclawConfig({ ...loadEvolclawConfig(), ecweb: { enabled: false } });
270
+ console.log(' 已跳过(可日后运行 ec watch web 手动启动,或编辑 evolclaw.json)');
271
+ }
272
+ }
273
+ finally {
274
+ try {
275
+ rlEcweb.close();
276
+ }
277
+ catch { }
278
+ }
279
+ }
280
+ }
254
281
  }
255
282
  /**
256
283
  * Present instance selection menu when existing instances are found.
package/dist/cli/model.js CHANGED
@@ -13,7 +13,7 @@
13
13
  import { isHelpFlag, wantsHelp, getArgValue } from './help.js';
14
14
  import { ModelScopeError, normalizePeer, determineScope, activeBaseagent, readScope, writeScope, clearScope, resolveEffectiveModel, } from '../core/model/model-scope.js';
15
15
  import { loadDefaults, loadAgent } from '../config-store.js';
16
- import { resolveAnthropicConfig } from '../agents/resolve.js';
16
+ import { resolveAnthropicConfig } from '../agents/baseagent.js';
17
17
  import { getCatalog, getModelInfo } from '../core/model/model-catalog.js';
18
18
  const ALL_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'auto'];
19
19
  const SCOPE_LABEL = {
@@ -0,0 +1,558 @@
1
+ /**
2
+ * ec stats — Token 用量统计 CLI 命令。
3
+ */
4
+ import { resolveRoot } from '../paths.js';
5
+ import { wantsHelp } from './help.js';
6
+ import { queryAggregated, querySessionTurns, queryContextBreakdown, queryTopPeers, queryTopModels, queryMessageAggregated, queryPeerList, querySummary, queryPeerDaily, queryTaskModelCalls, querySessionModelCalls } from '../core/stats/query.js';
7
+ import { getBudgetStatus } from '../core/stats/budget.js';
8
+ const HELP = `ec stats — Token 用量与费用统计
9
+
10
+ Usage: ec stats [options]
11
+
12
+ 时间范围:
13
+ (无参数) 今日概览
14
+ --today 同上
15
+ --hour 最近 24 小时,按小时分组
16
+ --week 本周
17
+ --month 本月
18
+ --from <date> --to <date> 任意区间(YYYY-MM-DD)
19
+
20
+ 维度过滤(可组合):
21
+ --agent <aid> 指定 agent
22
+ --peer <X> 对端(裸 AID 自动前缀 aun#,或 channel#id 格式)
23
+ --model <model-id> 指定模型
24
+ --session <id> 指定会话
25
+
26
+ 聚合粒度:
27
+ --by hour|day|week|month|model|peer|agent
28
+
29
+ 快捷视图:
30
+ --session <id> 单个会话明细
31
+ --session <id> --last 会话最后一轮用量 + 累计(等价 status.completed)
32
+ --context <id> 会话 context breakdown 细目
33
+ --budget 预算状态
34
+ --top-peers [--limit N] 对端排行
35
+ --sql "<query>" 直接执行只读 SQL
36
+ --rebuild 全量重建日聚合表 usage_daily(运维兜底/排查)
37
+ --peers [--limit N] 私聊对端列表(带累计 token/calls/活跃日)
38
+ --groups [--limit N] 群聊列表(同上)
39
+ --summary 指定时间范围总消耗汇总(token/USD/CNY)
40
+ --peer-detail <id> 指定对端 AID 或 peer_key,按天返回消耗明细
41
+ --task-calls <taskId> 一个 task 的逐次大模型调用明细
42
+ --session-calls <id> 一个会话的逐次大模型调用明细
43
+
44
+ 输出格式:
45
+ --format json JSON 输出
46
+ --help, -h 显示此帮助
47
+
48
+ 示例:
49
+ ec stats
50
+ ec stats --month --agent bot.agentid.pub
51
+ ec stats --peer alice.aid.pub
52
+ ec stats --session abc123 --format json`;
53
+ const RESET = '\x1b[0m';
54
+ const BOLD = '\x1b[1m';
55
+ const DIM = '\x1b[2m';
56
+ const GREEN = '\x1b[32m';
57
+ const BLUE = '\x1b[34m';
58
+ const CYAN = '\x1b[36m';
59
+ const YELLOW = '\x1b[33m';
60
+ const RED = '\x1b[31m';
61
+ function fail(formatJson, code, message) {
62
+ if (formatJson) {
63
+ console.log(JSON.stringify({ ok: false, code, error: message }, null, 2));
64
+ }
65
+ else {
66
+ console.error(`❌ ${message} (${code})`);
67
+ }
68
+ process.exit(1);
69
+ }
70
+ function pad(s, n) { return s.padEnd(n); }
71
+ function padR(s, n) { return s.padStart(n); }
72
+ function fmtTokens(n) {
73
+ if (n >= 1_000_000)
74
+ return (n / 1_000_000).toFixed(1) + 'M';
75
+ if (n >= 1_000)
76
+ return (n / 1_000).toFixed(1) + 'k';
77
+ return String(n);
78
+ }
79
+ function fmtBytes(n) {
80
+ if (n >= 1_073_741_824)
81
+ return (n / 1_073_741_824).toFixed(1) + 'GB';
82
+ if (n >= 1_048_576)
83
+ return (n / 1_048_576).toFixed(1) + 'MB';
84
+ if (n >= 1_024)
85
+ return (n / 1_024).toFixed(1) + 'KB';
86
+ return n + 'B';
87
+ }
88
+ function printTable(headers, rows) {
89
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map(r => (r[i] || '').length)));
90
+ const sep = widths.map(w => '─'.repeat(w + 2)).join('┼');
91
+ console.log(headers.map((h, i) => ` ${pad(h, widths[i])} `).join('│'));
92
+ console.log(sep);
93
+ for (const row of rows) {
94
+ console.log(row.map((c, i) => ` ${pad(c || '', widths[i])} `).join('│'));
95
+ }
96
+ }
97
+ function parseDateArg(s) {
98
+ const d = new Date(s + 'T00:00:00Z');
99
+ if (isNaN(d.getTime()))
100
+ throw new Error(`Invalid date: ${s}`);
101
+ return d.getTime();
102
+ }
103
+ export async function handleStats(args) {
104
+ // Help
105
+ if (wantsHelp(args)) {
106
+ console.log(HELP);
107
+ return;
108
+ }
109
+ const home = resolveRoot();
110
+ const flags = {};
111
+ const positional = [];
112
+ // Parse args
113
+ for (let i = 0; i < args.length; i++) {
114
+ const a = args[i];
115
+ if (a.startsWith('--')) {
116
+ const key = a.slice(2);
117
+ const next = args[i + 1];
118
+ if (next && !next.startsWith('--')) {
119
+ flags[key] = next;
120
+ i++;
121
+ }
122
+ else {
123
+ flags[key] = true;
124
+ }
125
+ }
126
+ else {
127
+ positional.push(a);
128
+ }
129
+ }
130
+ const isJson = flags.format === 'json';
131
+ // 全量重建日聚合表(运维兜底/排查),不依赖时间范围。
132
+ if (flags.rebuild) {
133
+ const { rebuildDailyRollup } = await import('../core/stats/db.js');
134
+ const startedAt = Date.now();
135
+ const n = rebuildDailyRollup(home);
136
+ const ms = Date.now() - startedAt;
137
+ if (n < 0)
138
+ fail(isJson, 'REBUILD_FAILED', 'usage_daily 重建失败(DB 不可用或 SQL 错误)');
139
+ if (isJson) {
140
+ console.log(JSON.stringify({ ok: true, rows: n, duration_ms: ms }, null, 2));
141
+ }
142
+ else {
143
+ console.log(`✓ usage_daily 重建完成:${n} 行,耗时 ${ms}ms`);
144
+ }
145
+ return;
146
+ }
147
+ // Determine time range
148
+ let from_ts;
149
+ let to_ts;
150
+ let granularity = 'day';
151
+ const now = new Date();
152
+ const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
153
+ if (flags.from && typeof flags.from === 'string') {
154
+ from_ts = parseDateArg(flags.from);
155
+ if (flags.to && typeof flags.to === 'string')
156
+ to_ts = parseDateArg(flags.to);
157
+ }
158
+ else if (flags.hour) {
159
+ from_ts = Date.now() - 24 * 60 * 60 * 1000;
160
+ granularity = 'hour';
161
+ }
162
+ else if (flags.week) {
163
+ const dayOfWeek = now.getDay();
164
+ from_ts = todayStart - dayOfWeek * 86400000;
165
+ granularity = 'day';
166
+ }
167
+ else if (flags.month) {
168
+ from_ts = new Date(now.getFullYear(), now.getMonth(), 1).getTime();
169
+ granularity = 'day';
170
+ }
171
+ else {
172
+ // default: today
173
+ from_ts = todayStart;
174
+ granularity = 'hour';
175
+ }
176
+ // Granularity override
177
+ if (flags.by && typeof flags.by === 'string') {
178
+ if (['hour', 'day', 'week', 'month', 'model', 'peer', 'agent'].includes(flags.by)) {
179
+ granularity = flags.by;
180
+ }
181
+ }
182
+ // Filter
183
+ const filter = { from_ts, to_ts };
184
+ if (flags.agent && typeof flags.agent === 'string')
185
+ filter.agent_aid = flags.agent;
186
+ if (flags.peer && typeof flags.peer === 'string') {
187
+ // AUN 简写:不含 # 时默认 aun 渠道
188
+ filter.peer_key = flags.peer.includes('#') ? flags.peer : `aun#${encodeURIComponent(flags.peer)}`;
189
+ }
190
+ if (flags.model && typeof flags.model === 'string')
191
+ filter.model = flags.model;
192
+ if (flags.session && typeof flags.session === 'string')
193
+ filter.session_id = flags.session;
194
+ // ── 结构化查询:私聊/群聊列表、总消耗汇总、对端按天明细 ──────────────────
195
+ if (flags.peers || flags.groups) {
196
+ const peerType = flags.peers ? 'private' : 'group';
197
+ const limit = typeof flags.limit === 'string' ? parseInt(flags.limit) || 50 : 50;
198
+ const rows = queryPeerList(home, { peer_type: peerType, from_ts, to_ts, agent_aid: filter.agent_aid, limit });
199
+ if (isJson) {
200
+ console.log(JSON.stringify(rows, null, 2));
201
+ return;
202
+ }
203
+ console.log(`\n${BOLD}📊 ${peerType === 'private' ? '私聊' : '群聊'}列表${RESET}\n`);
204
+ if (!rows.length) {
205
+ console.log(' (无数据)');
206
+ return;
207
+ }
208
+ const headers = ['#', 'Peer ID', 'Tokens', 'Calls', 'Sessions', 'First', 'Last'];
209
+ const tableRows = rows.map((r, i) => [
210
+ String(i + 1), r.peer_id, fmtTokens(r.total_tokens), String(r.calls),
211
+ String(r.session_count), r.first_day, r.last_day,
212
+ ]);
213
+ printTable(headers, tableRows);
214
+ console.log();
215
+ return;
216
+ }
217
+ if (flags.summary) {
218
+ const result = querySummary(home, { from_ts, to_ts, agent_aid: filter.agent_aid, peer_key: filter.peer_key });
219
+ if (isJson) {
220
+ console.log(JSON.stringify(result, null, 2));
221
+ return;
222
+ }
223
+ console.log(`\n${BOLD}📊 用量汇总${RESET}\n`);
224
+ console.log(` Input: ${fmtTokens(result.input_tokens)}`);
225
+ console.log(` Output: ${fmtTokens(result.output_tokens)}`);
226
+ console.log(` Cache read: ${fmtTokens(result.cache_read_tokens)}`);
227
+ console.log(` Cache write: ${fmtTokens(result.cache_creation_tokens)}`);
228
+ console.log(` Total tokens: ${fmtTokens(result.total_tokens)}`);
229
+ console.log(` Calls: ${result.calls}`);
230
+ console.log(` Cache hit: ${(result.cache_hit_rate * 100).toFixed(1)}%`);
231
+ if (result.usd > 0)
232
+ console.log(` Cost USD: ${GREEN}$${result.usd.toFixed(4)}${RESET}`);
233
+ if (result.cny > 0)
234
+ console.log(` Cost CNY: ${GREEN}¥${result.cny.toFixed(4)}${RESET}`);
235
+ console.log();
236
+ return;
237
+ }
238
+ if (flags['peer-detail'] && typeof flags['peer-detail'] === 'string') {
239
+ const input = flags['peer-detail'];
240
+ // 含 # 视为完整 peer_key(精确匹配),否则视为裸 peer_id(LIKE 匹配)
241
+ const peerOpts = input.includes('#')
242
+ ? { peer_key: input, from_ts, to_ts, agent_aid: filter.agent_aid }
243
+ : { peer_id: input, from_ts, to_ts, agent_aid: filter.agent_aid };
244
+ const rows = queryPeerDaily(home, peerOpts);
245
+ if (isJson) {
246
+ console.log(JSON.stringify(rows, null, 2));
247
+ return;
248
+ }
249
+ if (!rows.length) {
250
+ console.log('No data for the specified peer and range.');
251
+ return;
252
+ }
253
+ console.log(`\n${BOLD}📊 对端明细 — ${input}${RESET}\n`);
254
+ const headers = ['Day', 'Input', 'Output', 'Cache', 'Calls', 'HitRate', 'USD', 'CNY'];
255
+ const tableRows = rows.map(r => [
256
+ r.period, fmtTokens(r.input_tokens), fmtTokens(r.output_tokens),
257
+ fmtTokens(r.cache_read_tokens), String(r.call_count),
258
+ (r.cache_hit_rate * 100).toFixed(0) + '%',
259
+ r.usd > 0 ? '$' + r.usd.toFixed(4) : '—',
260
+ r.cny > 0 ? '¥' + r.cny.toFixed(4) : '—',
261
+ ]);
262
+ printTable(headers, tableRows);
263
+ console.log();
264
+ return;
265
+ }
266
+ // 逐次大模型调用明细
267
+ if ((flags['task-calls'] && typeof flags['task-calls'] === 'string') ||
268
+ (flags['session-calls'] && typeof flags['session-calls'] === 'string')) {
269
+ const byTask = typeof flags['task-calls'] === 'string';
270
+ const key = (byTask ? flags['task-calls'] : flags['session-calls']);
271
+ const rows = byTask ? queryTaskModelCalls(home, key) : querySessionModelCalls(home, key);
272
+ if (isJson) {
273
+ console.log(JSON.stringify(rows, null, 2));
274
+ return;
275
+ }
276
+ if (!rows.length) {
277
+ console.log('No model calls found.');
278
+ return;
279
+ }
280
+ console.log(`\n${BOLD}📊 大模型调用明细 — ${key}${RESET}\n`);
281
+ const headers = ['#', 'Model', 'Input', 'Output', 'CacheR', 'CacheW', 'Task', 'Deg'];
282
+ const tableRows = rows.map(r => [
283
+ String(r.call_index),
284
+ r.model.split('-').slice(0, 3).join('-'),
285
+ fmtTokens(r.input_tokens), fmtTokens(r.output_tokens),
286
+ fmtTokens(r.cache_read_tokens), fmtTokens(r.cache_creation_tokens),
287
+ r.task_id, r.degraded ? 'Y' : '',
288
+ ]);
289
+ printTable(headers, tableRows);
290
+ console.log();
291
+ return;
292
+ }
293
+ // Special views
294
+ if (flags.sql && typeof flags.sql === 'string') {
295
+ // 直接执行 SQL(只读,仅 SELECT)
296
+ const sql = flags.sql.trim();
297
+ if (!/^\s*select/i.test(sql)) {
298
+ console.error('Only SELECT queries are allowed.');
299
+ return;
300
+ }
301
+ const { openReadonlyDb, getDbPath } = await import('../core/stats/db.js');
302
+ const db = openReadonlyDb(getDbPath(home));
303
+ if (!db) {
304
+ console.error('Stats DB not available.');
305
+ return;
306
+ }
307
+ try {
308
+ const rows = db.prepare(sql).all();
309
+ if (isJson) {
310
+ console.log(JSON.stringify(rows, null, 2));
311
+ }
312
+ else if (rows.length === 0) {
313
+ console.log('(empty result)');
314
+ }
315
+ else {
316
+ const keys = Object.keys(rows[0]);
317
+ printTable(keys, rows.map(r => keys.map(k => String(r[k] ?? ''))));
318
+ }
319
+ }
320
+ catch (e) {
321
+ console.error(`SQL error: ${e.message || e}`);
322
+ }
323
+ finally {
324
+ db.close();
325
+ }
326
+ return;
327
+ }
328
+ if (flags.budget) {
329
+ const status = getBudgetStatus(home, filter.agent_aid, filter.peer_key);
330
+ if (isJson) {
331
+ console.log(JSON.stringify(status, null, 2));
332
+ return;
333
+ }
334
+ console.log(`\n${BOLD}📊 Budget Status${RESET}\n`);
335
+ console.log(` Daily limit: ${status.daily_limit_usd >= 0 ? '$' + status.daily_limit_usd.toFixed(2) : 'unlimited'}`);
336
+ console.log(` Daily used: ${GREEN}$${status.daily_used_usd.toFixed(4)}${RESET}`);
337
+ console.log(` Daily remaining: ${status.daily_remaining_usd >= 0 ? '$' + status.daily_remaining_usd.toFixed(2) : 'unlimited'}`);
338
+ console.log(` Monthly limit: ${status.monthly_limit_usd >= 0 ? '$' + status.monthly_limit_usd.toFixed(2) : 'unlimited'}`);
339
+ console.log(` Monthly used: ${GREEN}$${status.monthly_used_usd.toFixed(4)}${RESET}`);
340
+ console.log(` Monthly remaining: ${status.monthly_remaining_usd >= 0 ? '$' + status.monthly_remaining_usd.toFixed(2) : 'unlimited'}`);
341
+ console.log(` Usage: ${status.pct_used > 80 ? RED : status.pct_used > 60 ? YELLOW : GREEN}${status.pct_used.toFixed(1)}%${RESET}`);
342
+ console.log(` Hard blocked: ${status.hard_blocked ? RED + 'YES' + RESET : 'no'}`);
343
+ console.log(` Soft warn: ${status.soft_warn ? YELLOW + 'yes' + RESET : 'no'}`);
344
+ console.log(` Auto warn: ${status.auto_warn ? YELLOW + 'yes' + RESET : 'no'}`);
345
+ console.log();
346
+ return;
347
+ }
348
+ if (flags.context && typeof flags.context === 'string') {
349
+ const rows = queryContextBreakdown(home, flags.context);
350
+ if (isJson) {
351
+ console.log(JSON.stringify(rows, null, 2));
352
+ return;
353
+ }
354
+ if (!rows.length) {
355
+ console.log('No context breakdown data for this session.');
356
+ return;
357
+ }
358
+ console.log(`\n${BOLD}📊 Context Breakdown — session ${flags.context}${RESET}\n`);
359
+ const headers = ['Turn', 'System', 'Tools', 'MCP', 'Agents', 'Memory', 'Skills', 'Messages', 'Free', 'Total', 'Max'];
360
+ const tableRows = rows.map(r => [
361
+ String(r.turn_count),
362
+ fmtTokens(r.system_prompt ?? 0), fmtTokens(r.system_tools ?? 0),
363
+ fmtTokens(r.mcp_tools ?? 0), fmtTokens(r.custom_agents ?? 0),
364
+ fmtTokens(r.memory_files ?? 0), fmtTokens(r.skills ?? 0),
365
+ fmtTokens(r.messages ?? 0), fmtTokens(r.free_space ?? 0),
366
+ fmtTokens(r.total_estimated ?? 0), fmtTokens(r.max_tokens ?? 0),
367
+ ]);
368
+ printTable(headers, tableRows);
369
+ console.log();
370
+ return;
371
+ }
372
+ if (flags['top-peers']) {
373
+ const limit = typeof flags.limit === 'string' ? parseInt(flags.limit) || 10 : 10;
374
+ const rows = queryTopPeers(home, filter, limit);
375
+ if (isJson) {
376
+ console.log(JSON.stringify(rows, null, 2));
377
+ return;
378
+ }
379
+ console.log(`\n${BOLD}📊 Top Peers${RESET}\n`);
380
+ const headers = ['#', 'Peer', 'Tokens', 'Calls'];
381
+ const tableRows = rows.map((r, i) => [
382
+ String(i + 1), r.peer_key, fmtTokens(r.total_tokens), String(r.call_count),
383
+ ]);
384
+ printTable(headers, tableRows);
385
+ console.log();
386
+ return;
387
+ }
388
+ if (flags['top-models']) {
389
+ const limit = typeof flags.limit === 'string' ? parseInt(flags.limit) || 10 : 10;
390
+ const rows = queryTopModels(home, filter, limit);
391
+ if (isJson) {
392
+ console.log(JSON.stringify(rows, null, 2));
393
+ return;
394
+ }
395
+ console.log(`\n${BOLD}📊 Top Models${RESET}\n`);
396
+ const headers = ['#', 'Model', 'Tokens', 'Calls'];
397
+ const tableRows = rows.map((r, i) => [
398
+ String(i + 1), r.model, fmtTokens(r.total_tokens), String(r.call_count),
399
+ ]);
400
+ printTable(headers, tableRows);
401
+ console.log();
402
+ return;
403
+ }
404
+ if (flags.traffic) {
405
+ const rows = queryMessageAggregated(home, granularity, filter);
406
+ if (isJson) {
407
+ console.log(JSON.stringify(rows, null, 2));
408
+ return;
409
+ }
410
+ if (!rows.length) {
411
+ console.log('No traffic data for the selected range.');
412
+ return;
413
+ }
414
+ const periodLabel = flags.hour ? 'Last 24h' : flags.week ? 'This Week' : flags.month ? 'This Month' : 'Today';
415
+ console.log(`\n${BOLD}📊 ${periodLabel} — Network Traffic${RESET}\n`);
416
+ const headers = ['Period', 'Msg In', 'Msg Out', 'Bytes In', 'Bytes Out'];
417
+ const tableRows = rows.map(r => [
418
+ r.period,
419
+ String(r.msg_in), String(r.msg_out),
420
+ fmtBytes(r.bytes_in), fmtBytes(r.bytes_out),
421
+ ]);
422
+ printTable(headers, tableRows);
423
+ const totIn = rows.reduce((s, r) => s + r.msg_in, 0);
424
+ const totOut = rows.reduce((s, r) => s + r.msg_out, 0);
425
+ const totBIn = rows.reduce((s, r) => s + r.bytes_in, 0);
426
+ const totBOut = rows.reduce((s, r) => s + r.bytes_out, 0);
427
+ console.log(`\n ${DIM}Total: in=${totIn} out=${totOut} bytes_in=${fmtBytes(totBIn)} bytes_out=${fmtBytes(totBOut)}${RESET}\n`);
428
+ return;
429
+ }
430
+ if (flags.session && typeof flags.session === 'string') {
431
+ const rows = querySessionTurns(home, flags.session);
432
+ // --last: 返回最后一轮用量 + 会话累计(与 status.completed metadata 等价)
433
+ if (flags.last) {
434
+ if (!rows.length) {
435
+ if (isJson) {
436
+ console.log(JSON.stringify({ ok: false, error: 'No usage data for this session' }, null, 2));
437
+ }
438
+ else {
439
+ console.log('No usage data for this session.');
440
+ }
441
+ return;
442
+ }
443
+ const last = rows[rows.length - 1];
444
+ const totIn = rows.reduce((s, r) => s + r.input_tokens, 0);
445
+ const totOut = rows.reduce((s, r) => s + r.output_tokens, 0);
446
+ const totCacheRead = rows.reduce((s, r) => s + r.cache_read_tokens, 0);
447
+ const totCacheCreation = rows.reduce((s, r) => s + r.cache_creation_tokens, 0);
448
+ const totUsd = rows.reduce((s, r) => s + r.usd, 0);
449
+ const totCny = rows.reduce((s, r) => s + r.cny, 0);
450
+ const totCacheAll = totIn + totCacheRead;
451
+ const sessionCacheHitRate = totCacheAll > 0 ? totCacheRead / totCacheAll : 0;
452
+ // model_spec
453
+ const { resolveModelSpec } = await import('../core/stats/billing.js');
454
+ const spec = resolveModelSpec(home, last.model);
455
+ const result = {
456
+ turn: {
457
+ input_tokens: last.input_tokens,
458
+ output_tokens: last.output_tokens,
459
+ cache_read_tokens: last.cache_read_tokens,
460
+ cache_creation_tokens: last.cache_creation_tokens,
461
+ model: last.model,
462
+ cost_usd: last.usd,
463
+ cost_cny: last.cny,
464
+ cache_hit_rate: last.cache_read_tokens / ((last.input_tokens + last.cache_read_tokens) || 1),
465
+ context_window_pct: last.context_window_pct ?? 0,
466
+ duration_ms: last.duration_ms ?? 0,
467
+ },
468
+ session_total: {
469
+ input_tokens: totIn,
470
+ output_tokens: totOut,
471
+ cache_read_tokens: totCacheRead,
472
+ cache_creation_tokens: totCacheCreation,
473
+ cost_usd: totUsd,
474
+ cost_cny: totCny,
475
+ call_count: rows.length,
476
+ cache_hit_rate: sessionCacheHitRate,
477
+ },
478
+ model_spec: {
479
+ context_window: spec.context_window,
480
+ max_input_tokens: spec.max_input_tokens,
481
+ max_output_tokens: spec.max_output_tokens,
482
+ },
483
+ };
484
+ if (isJson) {
485
+ console.log(JSON.stringify(result, null, 2));
486
+ return;
487
+ }
488
+ console.log(`\n${BOLD}📊 Session Last Turn — ${flags.session}${RESET}\n`);
489
+ console.log(` Model: ${CYAN}${result.turn.model}${RESET}`);
490
+ console.log(` Input: ${fmtTokens(result.turn.input_tokens)}`);
491
+ console.log(` Output: ${fmtTokens(result.turn.output_tokens)}`);
492
+ console.log(` Cache read: ${fmtTokens(result.turn.cache_read_tokens)}`);
493
+ console.log(` Cache hit: ${(result.turn.cache_hit_rate * 100).toFixed(1)}%`);
494
+ console.log(` Context: ${result.turn.context_window_pct}%`);
495
+ console.log(` Cost: ${result.turn.cost_usd > 0 ? GREEN + '$' + result.turn.cost_usd.toFixed(4) + RESET : ''}${result.turn.cost_cny > 0 ? GREEN + ' ¥' + result.turn.cost_cny.toFixed(4) + RESET : ''}`);
496
+ console.log(` Duration: ${result.turn.duration_ms}ms`);
497
+ console.log(`\n ${DIM}Session total: input=${fmtTokens(result.session_total.input_tokens)} output=${fmtTokens(result.session_total.output_tokens)} USD=$${totUsd.toFixed(4)} CNY=¥${totCny.toFixed(4)} calls=${result.session_total.call_count}${RESET}\n`);
498
+ return;
499
+ }
500
+ if (isJson) {
501
+ console.log(JSON.stringify(rows, null, 2));
502
+ return;
503
+ }
504
+ if (!rows.length) {
505
+ console.log('No usage data for this session.');
506
+ return;
507
+ }
508
+ console.log(`\n${BOLD}📊 Session ${flags.session}${RESET}\n`);
509
+ const headers = ['Time', 'Model', 'Input', 'Output', 'Cache', 'Ctx%', 'USD', 'CNY'];
510
+ const tableRows = rows.map(r => {
511
+ const t = new Date(r.ts).toLocaleTimeString();
512
+ return [
513
+ t, r.model.split('-').slice(0, 2).join('-'),
514
+ fmtTokens(r.input_tokens), fmtTokens(r.output_tokens),
515
+ fmtTokens(r.cache_read_tokens),
516
+ r.context_window_pct != null ? r.context_window_pct.toFixed(0) + '%' : '—',
517
+ r.usd > 0 ? '$' + r.usd.toFixed(4) : '—',
518
+ r.cny > 0 ? '¥' + r.cny.toFixed(4) : '—',
519
+ ];
520
+ });
521
+ printTable(headers, tableRows);
522
+ // Totals
523
+ const totIn = rows.reduce((s, r) => s + r.input_tokens, 0);
524
+ const totOut = rows.reduce((s, r) => s + r.output_tokens, 0);
525
+ const totUsd = rows.reduce((s, r) => s + r.usd, 0);
526
+ const totCny = rows.reduce((s, r) => s + r.cny, 0);
527
+ console.log(`\n ${DIM}Total: input=${fmtTokens(totIn)} output=${fmtTokens(totOut)} USD=$${totUsd.toFixed(4)} CNY=¥${totCny.toFixed(4)} turns=${rows.length}${RESET}\n`);
528
+ return;
529
+ }
530
+ // Default: aggregated view
531
+ const rows = queryAggregated(home, granularity, filter);
532
+ if (isJson) {
533
+ console.log(JSON.stringify(rows, null, 2));
534
+ return;
535
+ }
536
+ if (!rows.length) {
537
+ console.log('No usage data for the selected range.');
538
+ return;
539
+ }
540
+ const periodLabel = flags.hour ? 'Last 24h' : flags.week ? 'This Week' : flags.month ? 'This Month' : 'Today';
541
+ console.log(`\n${BOLD}📊 ${periodLabel} — Token Usage${RESET}\n`);
542
+ const headers = ['Period', 'Input', 'Output', 'Cache↑', 'CacheHit', 'Calls', 'HitRate'];
543
+ const tableRows = rows.map(r => [
544
+ r.period,
545
+ fmtTokens(r.input_tokens), fmtTokens(r.output_tokens),
546
+ fmtTokens(r.cache_creation_tokens), fmtTokens(r.cache_read_tokens),
547
+ String(r.call_count),
548
+ (r.cache_hit_rate * 100).toFixed(0) + '%',
549
+ ]);
550
+ printTable(headers, tableRows);
551
+ // Summary
552
+ const totIn = rows.reduce((s, r) => s + r.input_tokens, 0);
553
+ const totOut = rows.reduce((s, r) => s + r.output_tokens, 0);
554
+ const totCalls = rows.reduce((s, r) => s + r.call_count, 0);
555
+ const totCacheRead = rows.reduce((s, r) => s + r.cache_read_tokens, 0);
556
+ const overallHit = (totIn + totCacheRead) > 0 ? (totCacheRead / (totIn + totCacheRead) * 100).toFixed(1) : '0';
557
+ console.log(`\n ${DIM}Total: input=${fmtTokens(totIn)} output=${fmtTokens(totOut)} calls=${totCalls} cache_hit_rate=${overallHit}%${RESET}\n`);
558
+ }