@armadra/agent 0.2.1 → 0.4.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.
- package/CHANGELOG.md +99 -0
- package/README.md +142 -49
- package/dist/agent/retry.d.ts +1 -1
- package/dist/agent/retry.js +2 -1
- package/dist/agent/session-cache.js +6 -3
- package/dist/agent/session-classifier.d.ts +18 -0
- package/dist/agent/session-classifier.js +103 -0
- package/dist/agent/session-core.d.ts +13 -0
- package/dist/agent/session-state.js +2 -1
- package/dist/agent/session-sync.js +9 -2
- package/dist/agent/session-tools.js +24 -3
- package/dist/agent/session.d.ts +3 -0
- package/dist/agent/session.js +20 -10
- package/dist/agent/tool-runner.js +15 -2
- package/dist/agent/types.d.ts +9 -1
- package/dist/ai/apis/anthropic-messages.js +3 -2
- package/dist/ai/apis/google-generative-ai.js +3 -2
- package/dist/ai/apis/openai-completions.js +3 -2
- package/dist/ai/apis/openai-responses.js +5 -3
- package/dist/ai/cache/fingerprint.d.ts +1 -1
- package/dist/ai/cache/fingerprint.js +1 -1
- package/dist/ai/cache/reporting.js +2 -1
- package/dist/ai/http.d.ts +28 -6
- package/dist/ai/http.js +41 -8
- package/dist/ai/providers/channels.d.ts +51 -0
- package/dist/ai/providers/channels.js +96 -0
- package/dist/ai/providers/enrich.d.ts +34 -0
- package/dist/ai/providers/enrich.js +86 -0
- package/dist/ai/providers/models-dev-cache.d.ts +53 -0
- package/dist/ai/providers/models-dev-cache.js +147 -0
- package/dist/ai/providers/models-dev.d.ts +99 -0
- package/dist/ai/providers/models-dev.js +315 -0
- package/dist/ai/providers/registry.d.ts +42 -4
- package/dist/ai/providers/registry.js +195 -34
- package/dist/ai/providers/suggest.d.ts +18 -0
- package/dist/ai/providers/suggest.js +72 -0
- package/dist/ai/sse.d.ts +6 -2
- package/dist/ai/sse.js +22 -2
- package/dist/ai/types.d.ts +30 -3
- package/dist/bundle/ama.cjs +17893 -10698
- package/dist/cli/args.d.ts +23 -5
- package/dist/cli/args.js +113 -13
- package/dist/cli/bootstrap.js +28 -3
- package/dist/cli/codemode-notice.d.ts +20 -0
- package/dist/cli/codemode-notice.js +55 -0
- package/dist/cli/compose-providers.js +3 -0
- package/dist/cli/compose-session.d.ts +8 -0
- package/dist/cli/compose-session.js +32 -1
- package/dist/cli/compose-store.d.ts +1 -1
- package/dist/cli/compose-store.js +3 -1
- package/dist/cli/compose.d.ts +17 -5
- package/dist/cli/compose.js +53 -19
- package/dist/cli/default-model.d.ts +38 -1
- package/dist/cli/default-model.js +96 -9
- package/dist/cli/deps.d.ts +38 -0
- package/dist/cli/exit-codes.d.ts +2 -0
- package/dist/cli/exit-codes.js +3 -0
- package/dist/cli/fake-visibility.d.ts +13 -0
- package/dist/cli/fake-visibility.js +26 -0
- package/dist/cli/from-prompt.d.ts +18 -0
- package/dist/cli/from-prompt.js +49 -0
- package/dist/cli/main.d.ts +10 -2
- package/dist/cli/main.js +88 -2
- package/dist/cli/proxy.d.ts +51 -0
- package/dist/cli/proxy.js +135 -0
- package/dist/cli/startup-screen.d.ts +29 -0
- package/dist/cli/startup-screen.js +47 -0
- package/dist/cli/startup-steps.d.ts +1 -1
- package/dist/cli/startup-steps.js +20 -13
- package/dist/cli/subcommands/config.d.ts +29 -4
- package/dist/cli/subcommands/config.js +188 -25
- package/dist/cli/subcommands/context.js +5 -1
- package/dist/cli/subcommands/doctor.js +12 -1
- package/dist/cli/subcommands/init.d.ts +6 -0
- package/dist/cli/subcommands/init.js +23 -0
- package/dist/cli/subcommands/model-meta.d.ts +16 -0
- package/dist/cli/subcommands/model-meta.js +54 -0
- package/dist/cli/subcommands/models-cache-probe.js +1 -1
- package/dist/cli/subcommands/models-discover.d.ts +18 -6
- package/dist/cli/subcommands/models-discover.js +87 -52
- package/dist/cli/subcommands/models.js +26 -18
- package/dist/cli/subcommands/probe-runner.d.ts +96 -0
- package/dist/cli/subcommands/probe-runner.js +264 -0
- package/dist/cli/subcommands/providers-list.d.ts +8 -0
- package/dist/cli/subcommands/providers-list.js +97 -0
- package/dist/cli/subcommands/providers-plan.d.ts +79 -0
- package/dist/cli/subcommands/providers-plan.js +215 -0
- package/dist/cli/subcommands/providers-probe.d.ts +34 -0
- package/dist/cli/subcommands/providers-probe.js +87 -0
- package/dist/cli/subcommands/providers.d.ts +28 -0
- package/dist/cli/subcommands/providers.js +436 -0
- package/dist/cli/subcommands/sessions-export.d.ts +10 -0
- package/dist/cli/subcommands/sessions-export.js +59 -0
- package/dist/cli/subcommands/sessions-search.d.ts +13 -0
- package/dist/cli/subcommands/sessions-search.js +103 -0
- package/dist/cli/subcommands/sessions.d.ts +3 -2
- package/dist/cli/subcommands/sessions.js +22 -1
- package/dist/cli/subcommands/stats.d.ts +17 -0
- package/dist/cli/subcommands/stats.js +198 -0
- package/dist/cli/system-prompt-arg.d.ts +11 -0
- package/dist/cli/system-prompt-arg.js +34 -0
- package/dist/codemode/modes.d.ts +4 -11
- package/dist/codemode/modes.js +5 -27
- package/dist/codemode/tool.d.ts +18 -14
- package/dist/codemode/tool.js +71 -28
- package/dist/config/init.d.ts +34 -0
- package/dist/config/init.js +99 -0
- package/dist/config/json-schema.d.ts +15 -0
- package/dist/config/json-schema.js +215 -0
- package/dist/config/key-docs.d.ts +22 -0
- package/dist/config/key-docs.js +105 -0
- package/dist/config/merge.d.ts +9 -8
- package/dist/config/merge.js +22 -7
- package/dist/config/schema.d.ts +1 -1
- package/dist/config/schema.js +97 -15
- package/dist/config/types.d.ts +57 -11
- package/dist/config/types.js +19 -1
- package/dist/modes/commands-core.js +6 -5
- package/dist/modes/image-input.d.ts +27 -0
- package/dist/modes/image-input.js +78 -0
- package/dist/modes/interactive/approval-dialog.d.ts +21 -5
- package/dist/modes/interactive/approval-dialog.js +106 -27
- package/dist/modes/interactive/commands.d.ts +11 -2
- package/dist/modes/interactive/commands.js +58 -19
- package/dist/modes/interactive/interactive-mode.d.ts +2 -1
- package/dist/modes/interactive/interactive-mode.js +70 -82
- package/dist/modes/interactive/key-dispatch.d.ts +3 -1
- package/dist/modes/interactive/key-dispatch.js +5 -6
- package/dist/modes/interactive/line/line-mode.d.ts +1 -0
- package/dist/modes/interactive/line/line-mode.js +9 -5
- package/dist/modes/interactive/line/line-render.d.ts +4 -0
- package/dist/modes/interactive/line/line-render.js +28 -6
- package/dist/modes/interactive/message-view.d.ts +48 -9
- package/dist/modes/interactive/message-view.js +238 -44
- package/dist/modes/interactive/panels.d.ts +18 -0
- package/dist/modes/interactive/panels.js +143 -0
- package/dist/modes/interactive/pickers.d.ts +23 -2
- package/dist/modes/interactive/pickers.js +48 -15
- package/dist/modes/interactive/run-indicator.d.ts +51 -0
- package/dist/modes/interactive/run-indicator.js +189 -0
- package/dist/modes/interactive/startup-header.d.ts +40 -0
- package/dist/modes/interactive/startup-header.js +169 -0
- package/dist/modes/interactive/startup-ui.d.ts +7 -2
- package/dist/modes/interactive/startup-ui.js +41 -11
- package/dist/modes/interactive/status-bar.d.ts +18 -15
- package/dist/modes/interactive/status-bar.js +98 -56
- package/dist/modes/interactive/tool-summary.d.ts +46 -0
- package/dist/modes/interactive/tool-summary.js +218 -0
- package/dist/modes/interactive/tool-view.d.ts +48 -15
- package/dist/modes/interactive/tool-view.js +203 -145
- package/dist/modes/print/print-mode.d.ts +33 -4
- package/dist/modes/print/print-mode.js +128 -8
- package/dist/modes/rpc/commands.js +12 -2
- package/dist/permissions/auto-safe.d.ts +60 -0
- package/dist/permissions/auto-safe.js +529 -0
- package/dist/permissions/classifier.d.ts +64 -0
- package/dist/permissions/classifier.js +184 -0
- package/dist/permissions/dangerous.d.ts +5 -0
- package/dist/permissions/dangerous.js +1 -1
- package/dist/permissions/modes.d.ts +30 -0
- package/dist/permissions/modes.js +78 -0
- package/dist/permissions/pipeline.d.ts +31 -4
- package/dist/permissions/pipeline.js +196 -6
- package/dist/permissions/protected.d.ts +19 -0
- package/dist/permissions/protected.js +74 -0
- package/dist/permissions/rules.js +3 -0
- package/dist/permissions/types.d.ts +50 -3
- package/dist/permissions/types.js +3 -0
- package/dist/rpc.d.ts +2 -0
- package/dist/sdk.d.ts +9 -3
- package/dist/sdk.js +10 -2
- package/dist/session/export.d.ts +32 -0
- package/dist/session/export.js +187 -0
- package/dist/session/projection.js +5 -1
- package/dist/session/redact.d.ts +15 -0
- package/dist/session/redact.js +55 -0
- package/dist/session/reuse.d.ts +33 -0
- package/dist/session/reuse.js +86 -0
- package/dist/session/scan.d.ts +34 -0
- package/dist/session/scan.js +140 -0
- package/dist/session/search.d.ts +52 -0
- package/dist/session/search.js +211 -0
- package/dist/session/stats-aggregate.d.ts +63 -0
- package/dist/session/stats-aggregate.js +163 -0
- package/dist/session/stats-index.d.ts +26 -0
- package/dist/session/stats-index.js +91 -0
- package/dist/session/stats-scan.d.ts +54 -0
- package/dist/session/stats-scan.js +236 -0
- package/dist/session/types.d.ts +2 -0
- package/dist/tools/image-file.d.ts +31 -0
- package/dist/tools/image-file.js +114 -0
- package/dist/tools/presets.d.ts +35 -8
- package/dist/tools/presets.js +56 -17
- package/dist/tools/read.d.ts +4 -8
- package/dist/tools/read.js +15 -64
- package/dist/tui/component.d.ts +8 -2
- package/dist/tui/component.js +3 -1
- package/dist/tui/components/box.d.ts +6 -1
- package/dist/tui/components/box.js +16 -6
- package/dist/tui/components/card.d.ts +23 -0
- package/dist/tui/components/card.js +37 -0
- package/dist/tui/components/editor-history.d.ts +6 -0
- package/dist/tui/components/editor-history.js +45 -0
- package/dist/tui/components/editor-paste.d.ts +1 -1
- package/dist/tui/components/editor-paste.js +4 -4
- package/dist/tui/components/editor.d.ts +11 -5
- package/dist/tui/components/editor.js +52 -58
- package/dist/tui/components/key-value.d.ts +3 -0
- package/dist/tui/components/key-value.js +16 -6
- package/dist/tui/components/loader.d.ts +37 -7
- package/dist/tui/components/loader.js +84 -21
- package/dist/tui/components/markdown.d.ts +5 -1
- package/dist/tui/components/markdown.js +45 -21
- package/dist/tui/components/meter.d.ts +3 -3
- package/dist/tui/components/meter.js +13 -11
- package/dist/tui/components/select-list.d.ts +23 -1
- package/dist/tui/components/select-list.js +76 -13
- package/dist/tui/glyphs.d.ts +68 -0
- package/dist/tui/glyphs.js +114 -0
- package/dist/tui/theme.d.ts +30 -6
- package/dist/tui/theme.js +103 -17
- package/dist/tui.d.ts +4 -2
- package/dist/tui.js +3 -1
- package/docs/codemode.md +23 -9
- package/docs/hooks.md +10 -10
- package/docs/permissions.md +148 -0
- package/docs/providers.md +241 -3
- package/docs/rpc.md +38 -38
- package/docs/session-format.md +3 -2
- package/docs/sessions.md +134 -0
- package/docs/tui.md +140 -61
- package/package.json +3 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ama stats` 的汇总(纯函数):文件摘要 → 合计、分组、按来源的请求数、工具 Top N。[W4-D]
|
|
3
|
+
*
|
|
4
|
+
* 口径(与 `/session`、`get_session_stats` 一致):
|
|
5
|
+
* - 请求 = 每次模型请求(回合里的 assistant、保温、权限分类、压缩摘要…),按 kind 分开计;
|
|
6
|
+
* - 命中率 = cacheRead /(input + cacheRead + cacheWrite),只算「报告缓存」的端点:同一
|
|
7
|
+
* provider/model@channel 在扫描范围内出现过任何非零缓存读写才算;其余端点不进分母;
|
|
8
|
+
* - 费用只加有价请求;`unpriced` 是无价请求数(全无价时 cost 缺省,展示层只给 token)。
|
|
9
|
+
*/
|
|
10
|
+
export const STATS_GROUPS = [
|
|
11
|
+
"day",
|
|
12
|
+
"week",
|
|
13
|
+
"month",
|
|
14
|
+
"provider",
|
|
15
|
+
"channel",
|
|
16
|
+
"model",
|
|
17
|
+
"project",
|
|
18
|
+
];
|
|
19
|
+
const newAcc = () => ({
|
|
20
|
+
requests: 0,
|
|
21
|
+
turns: 0,
|
|
22
|
+
input: 0,
|
|
23
|
+
output: 0,
|
|
24
|
+
cacheRead: 0,
|
|
25
|
+
cacheWrite: 0,
|
|
26
|
+
cost: 0,
|
|
27
|
+
costed: 0,
|
|
28
|
+
hitRead: 0,
|
|
29
|
+
hitPrompt: 0,
|
|
30
|
+
errors: 0,
|
|
31
|
+
retries: 0,
|
|
32
|
+
turnMs: 0,
|
|
33
|
+
timedTurns: 0,
|
|
34
|
+
sessions: new Set(),
|
|
35
|
+
});
|
|
36
|
+
export function endpointOf(b) {
|
|
37
|
+
return `${b.provider}/${b.model}${b.channel !== undefined ? `@${b.channel}` : ""}`;
|
|
38
|
+
}
|
|
39
|
+
function add(acc, b, reported, session) {
|
|
40
|
+
acc.requests += b.requests;
|
|
41
|
+
acc.turns += b.turns;
|
|
42
|
+
acc.input += b.input;
|
|
43
|
+
acc.output += b.output;
|
|
44
|
+
acc.cacheRead += b.cacheRead;
|
|
45
|
+
acc.cacheWrite += b.cacheWrite;
|
|
46
|
+
acc.cost += b.cost;
|
|
47
|
+
acc.costed += b.costed;
|
|
48
|
+
if (reported) {
|
|
49
|
+
acc.hitRead += b.cacheRead;
|
|
50
|
+
acc.hitPrompt += b.input + b.cacheRead + b.cacheWrite;
|
|
51
|
+
}
|
|
52
|
+
acc.errors += b.errors;
|
|
53
|
+
acc.retries += b.retries;
|
|
54
|
+
acc.turnMs += b.turnMs;
|
|
55
|
+
acc.timedTurns += b.timedTurns;
|
|
56
|
+
acc.sessions.add(session);
|
|
57
|
+
}
|
|
58
|
+
function totals(acc) {
|
|
59
|
+
const out = {
|
|
60
|
+
requests: acc.requests,
|
|
61
|
+
turns: acc.turns,
|
|
62
|
+
input: acc.input,
|
|
63
|
+
output: acc.output,
|
|
64
|
+
cacheRead: acc.cacheRead,
|
|
65
|
+
cacheWrite: acc.cacheWrite,
|
|
66
|
+
unpriced: acc.requests - acc.costed,
|
|
67
|
+
errors: acc.errors,
|
|
68
|
+
retries: acc.retries,
|
|
69
|
+
};
|
|
70
|
+
if (acc.costed > 0)
|
|
71
|
+
out.cost = acc.cost;
|
|
72
|
+
if (acc.hitPrompt > 0)
|
|
73
|
+
out.hitRate = acc.hitRead / acc.hitPrompt;
|
|
74
|
+
if (acc.timedTurns > 0)
|
|
75
|
+
out.avgTurnMs = acc.turnMs / acc.timedTurns;
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
/** 本地日期 → ISO 周 `2026-W40`。 */
|
|
79
|
+
export function isoWeek(day) {
|
|
80
|
+
const [y = 0, m = 1, d = 1] = day.split("-").map(Number);
|
|
81
|
+
const date = new Date(Date.UTC(y, m - 1, d));
|
|
82
|
+
const weekday = date.getUTCDay() || 7;
|
|
83
|
+
date.setUTCDate(date.getUTCDate() + 4 - weekday);
|
|
84
|
+
const yearStart = Date.UTC(date.getUTCFullYear(), 0, 1);
|
|
85
|
+
const week = Math.ceil(((date.getTime() - yearStart) / 86_400_000 + 1) / 7);
|
|
86
|
+
return `${date.getUTCFullYear()}-W${week < 10 ? `0${week}` : week}`;
|
|
87
|
+
}
|
|
88
|
+
function groupKey(by, b, cwd) {
|
|
89
|
+
switch (by) {
|
|
90
|
+
case "day":
|
|
91
|
+
return b.day;
|
|
92
|
+
case "week":
|
|
93
|
+
return isoWeek(b.day);
|
|
94
|
+
case "month":
|
|
95
|
+
return b.day.slice(0, 7);
|
|
96
|
+
case "provider":
|
|
97
|
+
return b.provider;
|
|
98
|
+
case "channel":
|
|
99
|
+
return endpointOf(b);
|
|
100
|
+
case "model":
|
|
101
|
+
return `${b.provider}/${b.model}`;
|
|
102
|
+
case "project":
|
|
103
|
+
return cwd;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function aggregateStats(summaries, options = {}) {
|
|
107
|
+
const inRange = (day) => (options.since === undefined || day >= options.since) &&
|
|
108
|
+
(options.until === undefined || day <= options.until);
|
|
109
|
+
const reported = new Set();
|
|
110
|
+
const endpoints = new Set();
|
|
111
|
+
for (const s of summaries)
|
|
112
|
+
for (const b of s.buckets) {
|
|
113
|
+
endpoints.add(endpointOf(b));
|
|
114
|
+
if (b.cacheSeen)
|
|
115
|
+
reported.add(endpointOf(b));
|
|
116
|
+
}
|
|
117
|
+
const all = newAcc();
|
|
118
|
+
const groups = new Map();
|
|
119
|
+
const byKind = {};
|
|
120
|
+
const tools = new Map();
|
|
121
|
+
for (const s of summaries) {
|
|
122
|
+
for (const b of s.buckets) {
|
|
123
|
+
if (!inRange(b.day))
|
|
124
|
+
continue;
|
|
125
|
+
const isReported = reported.has(endpointOf(b));
|
|
126
|
+
add(all, b, isReported, s.id);
|
|
127
|
+
byKind[b.kind] = (byKind[b.kind] ?? 0) + b.requests;
|
|
128
|
+
if (options.by !== undefined) {
|
|
129
|
+
const key = groupKey(options.by, b, s.cwd);
|
|
130
|
+
let acc = groups.get(key);
|
|
131
|
+
if (acc === undefined)
|
|
132
|
+
groups.set(key, (acc = newAcc()));
|
|
133
|
+
add(acc, b, isReported, s.id);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const [day, name, count] of s.tools)
|
|
137
|
+
if (inRange(day))
|
|
138
|
+
tools.set(name, (tools.get(name) ?? 0) + count);
|
|
139
|
+
}
|
|
140
|
+
const report = {
|
|
141
|
+
sessions: all.sessions.size,
|
|
142
|
+
totals: totals(all),
|
|
143
|
+
byKind,
|
|
144
|
+
tools: [...tools]
|
|
145
|
+
.map(([name, count]) => ({ name, count }))
|
|
146
|
+
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name))
|
|
147
|
+
.slice(0, options.topTools ?? 10),
|
|
148
|
+
endpoints: { reported: reported.size, total: endpoints.size },
|
|
149
|
+
};
|
|
150
|
+
if (options.since !== undefined)
|
|
151
|
+
report.since = options.since;
|
|
152
|
+
if (options.until !== undefined)
|
|
153
|
+
report.until = options.until;
|
|
154
|
+
if (options.by !== undefined) {
|
|
155
|
+
const timeOrdered = options.by === "day" || options.by === "week" || options.by === "month";
|
|
156
|
+
report.groups = [...groups]
|
|
157
|
+
.map(([key, acc]) => ({ key, sessions: acc.sessions.size, ...totals(acc) }))
|
|
158
|
+
.sort((a, b) => timeOrdered
|
|
159
|
+
? a.key.localeCompare(b.key)
|
|
160
|
+
: b.requests - a.requests || a.key.localeCompare(b.key));
|
|
161
|
+
}
|
|
162
|
+
return report;
|
|
163
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ama stats` 的增量索引缓存:`<数据目录>/stats-index.json`。[W4-D]
|
|
3
|
+
*
|
|
4
|
+
* - 每个会话文件一条:`{ mtimeMs, size, summary }`;mtime 或 size 变了就重扫,否则直接用摘要。
|
|
5
|
+
* - 摘要里的日期是本地日期,所以索引记下时区(`Intl` 的 timeZone 与当前 UTC 偏移);变了整份作废。
|
|
6
|
+
* - 只是缓存:读不到、版本不对、JSON 坏了都当没有;写入走临时文件 + rename,失败忽略。
|
|
7
|
+
* - 扫描全部目录时顺带删掉已不存在的文件的条目;只扫一个项目时保留别的条目。
|
|
8
|
+
*/
|
|
9
|
+
import { type FileStatsSummary } from "./stats-scan.js";
|
|
10
|
+
export declare const STATS_INDEX_FILE = "stats-index.json";
|
|
11
|
+
export declare function timeZoneKey(now?: Date): string;
|
|
12
|
+
export interface CollectResult {
|
|
13
|
+
summaries: FileStatsSummary[];
|
|
14
|
+
/** 本次重扫的文件数(其余命中缓存)。 */
|
|
15
|
+
scanned: number;
|
|
16
|
+
cached: number;
|
|
17
|
+
/** 无效 / 不可读的文件数。 */
|
|
18
|
+
invalid: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 取一批文件的摘要。`indexFile` 为 undefined 时不用缓存;`prune` 为 true 时删掉不在 `files` 里的条目。
|
|
22
|
+
*/
|
|
23
|
+
export declare function collectSummaries(files: readonly string[], options?: {
|
|
24
|
+
indexFile?: string;
|
|
25
|
+
prune?: boolean;
|
|
26
|
+
}): CollectResult;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ama stats` 的增量索引缓存:`<数据目录>/stats-index.json`。[W4-D]
|
|
3
|
+
*
|
|
4
|
+
* - 每个会话文件一条:`{ mtimeMs, size, summary }`;mtime 或 size 变了就重扫,否则直接用摘要。
|
|
5
|
+
* - 摘要里的日期是本地日期,所以索引记下时区(`Intl` 的 timeZone 与当前 UTC 偏移);变了整份作废。
|
|
6
|
+
* - 只是缓存:读不到、版本不对、JSON 坏了都当没有;写入走临时文件 + rename,失败忽略。
|
|
7
|
+
* - 扫描全部目录时顺带删掉已不存在的文件的条目;只扫一个项目时保留别的条目。
|
|
8
|
+
*/
|
|
9
|
+
import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { dirname } from "node:path";
|
|
11
|
+
import { summarizeSessionFile } from "./stats-scan.js";
|
|
12
|
+
export const STATS_INDEX_FILE = "stats-index.json";
|
|
13
|
+
const INDEX_VERSION = 1;
|
|
14
|
+
export function timeZoneKey(now = new Date()) {
|
|
15
|
+
let zone = "";
|
|
16
|
+
try {
|
|
17
|
+
zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// 无 Intl 时只看偏移
|
|
21
|
+
}
|
|
22
|
+
return `${zone}|${now.getTimezoneOffset()}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 取一批文件的摘要。`indexFile` 为 undefined 时不用缓存;`prune` 为 true 时删掉不在 `files` 里的条目。
|
|
26
|
+
*/
|
|
27
|
+
export function collectSummaries(files, options = {}) {
|
|
28
|
+
const zone = timeZoneKey();
|
|
29
|
+
let index = { version: INDEX_VERSION, zone, files: {} };
|
|
30
|
+
if (options.indexFile !== undefined) {
|
|
31
|
+
try {
|
|
32
|
+
const loaded = JSON.parse(readFileSync(options.indexFile, "utf8"));
|
|
33
|
+
if (loaded.version === INDEX_VERSION &&
|
|
34
|
+
loaded.zone === zone &&
|
|
35
|
+
typeof loaded.files === "object")
|
|
36
|
+
index = loaded;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// 没有或坏了:重建
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
let dirty = false;
|
|
43
|
+
const result = { summaries: [], scanned: 0, cached: 0, invalid: 0 };
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
let stat;
|
|
46
|
+
try {
|
|
47
|
+
stat = statSync(file);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
result.invalid++;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const hit = index.files[file];
|
|
54
|
+
let summary;
|
|
55
|
+
if (hit !== undefined && hit.mtimeMs === stat.mtimeMs && hit.size === stat.size) {
|
|
56
|
+
summary = hit.summary;
|
|
57
|
+
result.cached++;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
summary = summarizeSessionFile(file) ?? null;
|
|
61
|
+
result.scanned++;
|
|
62
|
+
index.files[file] = { mtimeMs: stat.mtimeMs, size: stat.size, summary };
|
|
63
|
+
dirty = true;
|
|
64
|
+
}
|
|
65
|
+
if (summary === null)
|
|
66
|
+
result.invalid++;
|
|
67
|
+
else
|
|
68
|
+
result.summaries.push(summary);
|
|
69
|
+
}
|
|
70
|
+
if (options.prune === true) {
|
|
71
|
+
const keep = new Set(files);
|
|
72
|
+
for (const key of Object.keys(index.files)) {
|
|
73
|
+
if (!keep.has(key)) {
|
|
74
|
+
delete index.files[key];
|
|
75
|
+
dirty = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (dirty && options.indexFile !== undefined) {
|
|
80
|
+
try {
|
|
81
|
+
mkdirSync(dirname(options.indexFile), { recursive: true, mode: 0o700 });
|
|
82
|
+
const tmp = `${options.indexFile}.${process.pid}.tmp`;
|
|
83
|
+
writeFileSync(tmp, JSON.stringify(index), { mode: 0o600 });
|
|
84
|
+
renameSync(tmp, options.indexFile);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// 缓存写不进去不影响结果
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ama stats` 的单文件摘要(只读、按行)。[W4-D]
|
|
3
|
+
*
|
|
4
|
+
* 一个会话文件 → 若干「桶」:按(本地日期、请求来源 kind、provider、model、channel)累加。
|
|
5
|
+
* - 请求来源:assistant 消息 = `turn`;`usage` 条目用自己的 `kind`(cache_warm、permission_classify…);
|
|
6
|
+
* `compaction` / `branch_summary` 带的 usage 记为同名 kind(模型取最近一条 assistant 的)。
|
|
7
|
+
* - token:input(不含缓存)/ output / cacheRead / cacheWrite;费用只累加有 `usage.cost` 的请求,
|
|
8
|
+
* `costed` 记有价请求数,展示层据此区分「有价」「部分无价」「全无价」。
|
|
9
|
+
* - 缓存三态:桶记下是否出现过 cacheRead > 0 或 cacheWrite > 0(`cacheSeen`);汇总时同一端点
|
|
10
|
+
* (provider/model@channel)任一桶出现过即算报告缓存,其余端点不进命中率分母(同会话层三态)。
|
|
11
|
+
* - 回合:非 `steer` 的用户消息开始一个回合,到下一个回合开始为止;回合计入首条 assistant 的桶,
|
|
12
|
+
* 耗时 = 最后一条 assistant 条目时间 − 用户消息条目时间(都是落盘时间戳)。没有 assistant 的回合不计。
|
|
13
|
+
* - 工具调用:assistant 内容里的 toolCall 块按名字计(codemode 内层调用不展开)。
|
|
14
|
+
* - 重试:`context_edit{reason:"retry"}` 计入当时最近一条 assistant 的桶;`stopReason:"error"` 计错误。
|
|
15
|
+
* - toolResult、custom、label 等行按前缀跳过、不解析;末尾半行忽略;中间坏行跳过并计数。
|
|
16
|
+
* - channel:最近一条 `model_change` 与该请求同 provider / model 时取它的 channel。
|
|
17
|
+
*/
|
|
18
|
+
export interface StatsBucket {
|
|
19
|
+
/** 本地日期 YYYY-MM-DD。 */
|
|
20
|
+
day: string;
|
|
21
|
+
kind: string;
|
|
22
|
+
provider: string;
|
|
23
|
+
model: string;
|
|
24
|
+
channel?: string;
|
|
25
|
+
requests: number;
|
|
26
|
+
input: number;
|
|
27
|
+
output: number;
|
|
28
|
+
cacheRead: number;
|
|
29
|
+
cacheWrite: number;
|
|
30
|
+
/** 有价请求的费用合计(美元)。 */
|
|
31
|
+
cost: number;
|
|
32
|
+
/** 有价请求数。 */
|
|
33
|
+
costed: number;
|
|
34
|
+
/** 出现过 cacheRead > 0 或 cacheWrite > 0。 */
|
|
35
|
+
cacheSeen: boolean;
|
|
36
|
+
errors: number;
|
|
37
|
+
retries: number;
|
|
38
|
+
turns: number;
|
|
39
|
+
/** 有耗时的回合的耗时合计与个数。 */
|
|
40
|
+
turnMs: number;
|
|
41
|
+
timedTurns: number;
|
|
42
|
+
}
|
|
43
|
+
export interface FileStatsSummary {
|
|
44
|
+
id: string;
|
|
45
|
+
cwd: string;
|
|
46
|
+
buckets: StatsBucket[];
|
|
47
|
+
/** `[日期, 工具名, 次数]`。 */
|
|
48
|
+
tools: Array<[string, string, number]>;
|
|
49
|
+
/** 跳过的坏行数。 */
|
|
50
|
+
badLines: number;
|
|
51
|
+
}
|
|
52
|
+
/** ISO → 本地日期;不可解析 → undefined。 */
|
|
53
|
+
export declare function localDay(iso: unknown): string | undefined;
|
|
54
|
+
export declare function summarizeSessionFile(file: string): FileStatsSummary | undefined;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ama stats` 的单文件摘要(只读、按行)。[W4-D]
|
|
3
|
+
*
|
|
4
|
+
* 一个会话文件 → 若干「桶」:按(本地日期、请求来源 kind、provider、model、channel)累加。
|
|
5
|
+
* - 请求来源:assistant 消息 = `turn`;`usage` 条目用自己的 `kind`(cache_warm、permission_classify…);
|
|
6
|
+
* `compaction` / `branch_summary` 带的 usage 记为同名 kind(模型取最近一条 assistant 的)。
|
|
7
|
+
* - token:input(不含缓存)/ output / cacheRead / cacheWrite;费用只累加有 `usage.cost` 的请求,
|
|
8
|
+
* `costed` 记有价请求数,展示层据此区分「有价」「部分无价」「全无价」。
|
|
9
|
+
* - 缓存三态:桶记下是否出现过 cacheRead > 0 或 cacheWrite > 0(`cacheSeen`);汇总时同一端点
|
|
10
|
+
* (provider/model@channel)任一桶出现过即算报告缓存,其余端点不进命中率分母(同会话层三态)。
|
|
11
|
+
* - 回合:非 `steer` 的用户消息开始一个回合,到下一个回合开始为止;回合计入首条 assistant 的桶,
|
|
12
|
+
* 耗时 = 最后一条 assistant 条目时间 − 用户消息条目时间(都是落盘时间戳)。没有 assistant 的回合不计。
|
|
13
|
+
* - 工具调用:assistant 内容里的 toolCall 块按名字计(codemode 内层调用不展开)。
|
|
14
|
+
* - 重试:`context_edit{reason:"retry"}` 计入当时最近一条 assistant 的桶;`stopReason:"error"` 计错误。
|
|
15
|
+
* - toolResult、custom、label 等行按前缀跳过、不解析;末尾半行忽略;中间坏行跳过并计数。
|
|
16
|
+
* - channel:最近一条 `model_change` 与该请求同 provider / model 时取它的 channel。
|
|
17
|
+
*/
|
|
18
|
+
import { forEachLine, lineType } from "./scan.js";
|
|
19
|
+
const SKIP_TYPES = new Set([
|
|
20
|
+
"custom",
|
|
21
|
+
"custom_message",
|
|
22
|
+
"label",
|
|
23
|
+
"leaf",
|
|
24
|
+
"session_info",
|
|
25
|
+
"thinking_level_change",
|
|
26
|
+
]);
|
|
27
|
+
function pad(n) {
|
|
28
|
+
return n < 10 ? `0${n}` : String(n);
|
|
29
|
+
}
|
|
30
|
+
/** ISO → 本地日期;不可解析 → undefined。 */
|
|
31
|
+
export function localDay(iso) {
|
|
32
|
+
if (typeof iso !== "string")
|
|
33
|
+
return undefined;
|
|
34
|
+
const ms = Date.parse(iso);
|
|
35
|
+
if (!Number.isFinite(ms))
|
|
36
|
+
return undefined;
|
|
37
|
+
const d = new Date(ms);
|
|
38
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
39
|
+
}
|
|
40
|
+
const isRec = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
41
|
+
const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
|
|
42
|
+
export function summarizeSessionFile(file) {
|
|
43
|
+
let header;
|
|
44
|
+
const buckets = new Map();
|
|
45
|
+
const tools = new Map();
|
|
46
|
+
let badLines = 0;
|
|
47
|
+
let channel;
|
|
48
|
+
let lastAssistant;
|
|
49
|
+
let lastBucket;
|
|
50
|
+
let turn;
|
|
51
|
+
const bucketOf = (day, kind, ref) => {
|
|
52
|
+
const key = `${day}\u0000${kind}\u0000${ref.provider}\u0000${ref.model}\u0000${ref.channel ?? ""}`;
|
|
53
|
+
let bucket = buckets.get(key);
|
|
54
|
+
if (bucket === undefined) {
|
|
55
|
+
bucket = {
|
|
56
|
+
day,
|
|
57
|
+
kind,
|
|
58
|
+
provider: ref.provider,
|
|
59
|
+
model: ref.model,
|
|
60
|
+
requests: 0,
|
|
61
|
+
input: 0,
|
|
62
|
+
output: 0,
|
|
63
|
+
cacheRead: 0,
|
|
64
|
+
cacheWrite: 0,
|
|
65
|
+
cost: 0,
|
|
66
|
+
costed: 0,
|
|
67
|
+
cacheSeen: false,
|
|
68
|
+
errors: 0,
|
|
69
|
+
retries: 0,
|
|
70
|
+
turns: 0,
|
|
71
|
+
turnMs: 0,
|
|
72
|
+
timedTurns: 0,
|
|
73
|
+
};
|
|
74
|
+
if (ref.channel !== undefined)
|
|
75
|
+
bucket.channel = ref.channel;
|
|
76
|
+
buckets.set(key, bucket);
|
|
77
|
+
}
|
|
78
|
+
return bucket;
|
|
79
|
+
};
|
|
80
|
+
const addUsage = (bucket, usage) => {
|
|
81
|
+
bucket.requests++;
|
|
82
|
+
if (!isRec(usage))
|
|
83
|
+
return;
|
|
84
|
+
const u = usage;
|
|
85
|
+
bucket.input += num(u.input);
|
|
86
|
+
bucket.output += num(u.output);
|
|
87
|
+
bucket.cacheRead += num(u.cacheRead);
|
|
88
|
+
bucket.cacheWrite += num(u.cacheWrite);
|
|
89
|
+
if (num(u.cacheRead) > 0 || num(u.cacheWrite) > 0)
|
|
90
|
+
bucket.cacheSeen = true;
|
|
91
|
+
if (isRec(u.cost) && typeof u.cost["total"] === "number") {
|
|
92
|
+
bucket.cost += num(u.cost["total"]);
|
|
93
|
+
bucket.costed++;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const refFor = (provider, model) => {
|
|
97
|
+
const ref = { provider, model };
|
|
98
|
+
if (channel?.channel !== undefined &&
|
|
99
|
+
channel.provider === provider &&
|
|
100
|
+
channel.model === model) {
|
|
101
|
+
ref.channel = channel.channel;
|
|
102
|
+
}
|
|
103
|
+
return ref;
|
|
104
|
+
};
|
|
105
|
+
const closeTurn = () => {
|
|
106
|
+
if (turn?.bucket !== undefined) {
|
|
107
|
+
turn.bucket.turns++;
|
|
108
|
+
if (turn.end !== undefined && turn.end >= turn.start) {
|
|
109
|
+
turn.bucket.turnMs += turn.end - turn.start;
|
|
110
|
+
turn.bucket.timedTurns++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
turn = undefined;
|
|
114
|
+
};
|
|
115
|
+
try {
|
|
116
|
+
forEachLine(file, (line, index, last) => {
|
|
117
|
+
if (index > 0) {
|
|
118
|
+
const quick = lineType(line);
|
|
119
|
+
if (quick !== undefined) {
|
|
120
|
+
if (SKIP_TYPES.has(quick.type))
|
|
121
|
+
return;
|
|
122
|
+
if (quick.type === "message" && quick.role === "toolResult")
|
|
123
|
+
return;
|
|
124
|
+
if (quick.type === "message" && quick.role === "system")
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
let value;
|
|
129
|
+
try {
|
|
130
|
+
value = JSON.parse(line);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
if (!last)
|
|
134
|
+
badLines++;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!isRec(value))
|
|
138
|
+
return;
|
|
139
|
+
if (index === 0) {
|
|
140
|
+
if (value["type"] !== "session" || typeof value["cwd"] !== "string")
|
|
141
|
+
return false;
|
|
142
|
+
header = value;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const ts = value["timestamp"];
|
|
146
|
+
const day = localDay(ts);
|
|
147
|
+
if (day === undefined)
|
|
148
|
+
return;
|
|
149
|
+
switch (value["type"]) {
|
|
150
|
+
case "model_change": {
|
|
151
|
+
const ref = {
|
|
152
|
+
provider: String(value["provider"]),
|
|
153
|
+
model: String(value["modelId"]),
|
|
154
|
+
};
|
|
155
|
+
if (typeof value["channel"] === "string")
|
|
156
|
+
ref.channel = value["channel"];
|
|
157
|
+
channel = ref;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
case "message": {
|
|
161
|
+
const message = value["message"];
|
|
162
|
+
if (!isRec(message))
|
|
163
|
+
return;
|
|
164
|
+
if (message["role"] === "user") {
|
|
165
|
+
if (message["origin"] === "steer" && turn !== undefined)
|
|
166
|
+
return;
|
|
167
|
+
closeTurn();
|
|
168
|
+
turn = { start: Date.parse(ts), day };
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (message["role"] !== "assistant")
|
|
172
|
+
return;
|
|
173
|
+
const ref = refFor(String(message["provider"]), String(message["model"]));
|
|
174
|
+
lastAssistant = ref;
|
|
175
|
+
const bucket = bucketOf(day, "turn", ref);
|
|
176
|
+
lastBucket = bucket;
|
|
177
|
+
addUsage(bucket, message["usage"]);
|
|
178
|
+
if (message["stopReason"] === "error")
|
|
179
|
+
bucket.errors++;
|
|
180
|
+
const content = message["content"];
|
|
181
|
+
if (Array.isArray(content)) {
|
|
182
|
+
for (const block of content) {
|
|
183
|
+
if (isRec(block) &&
|
|
184
|
+
block["type"] === "toolCall" &&
|
|
185
|
+
typeof block["name"] === "string") {
|
|
186
|
+
const key = `${day}\u0000${block["name"]}`;
|
|
187
|
+
tools.set(key, (tools.get(key) ?? 0) + 1);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (turn !== undefined) {
|
|
192
|
+
if (turn.bucket === undefined)
|
|
193
|
+
turn.bucket = bucketOf(turn.day, "turn", ref);
|
|
194
|
+
turn.end = Date.parse(ts);
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
case "usage": {
|
|
199
|
+
const ref = refFor(String(value["provider"]), String(value["model"]));
|
|
200
|
+
addUsage(bucketOf(day, String(value["kind"] ?? "usage"), ref), value["usage"]);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
case "compaction":
|
|
204
|
+
case "branch_summary": {
|
|
205
|
+
if (value["usage"] === undefined)
|
|
206
|
+
return;
|
|
207
|
+
const ref = lastAssistant ?? { provider: "?", model: "?" };
|
|
208
|
+
addUsage(bucketOf(day, value["type"], ref), value["usage"]);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
case "context_edit":
|
|
212
|
+
if (value["reason"] === "retry" && lastBucket !== undefined)
|
|
213
|
+
lastBucket.retries++;
|
|
214
|
+
return;
|
|
215
|
+
default:
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
closeTurn();
|
|
224
|
+
if (header === undefined)
|
|
225
|
+
return undefined;
|
|
226
|
+
return {
|
|
227
|
+
id: String(header["id"]),
|
|
228
|
+
cwd: String(header["cwd"]),
|
|
229
|
+
buckets: [...buckets.values()],
|
|
230
|
+
tools: [...tools].map(([key, count]) => {
|
|
231
|
+
const [day = "", name = ""] = key.split("\u0000");
|
|
232
|
+
return [day, name, count];
|
|
233
|
+
}),
|
|
234
|
+
badLines,
|
|
235
|
+
};
|
|
236
|
+
}
|
package/dist/session/types.d.ts
CHANGED
|
@@ -72,6 +72,8 @@ export interface ModelChangeEntry extends EntryBase {
|
|
|
72
72
|
type: "model_change";
|
|
73
73
|
provider: string;
|
|
74
74
|
modelId: string;
|
|
75
|
+
/** 所选渠道(多渠道供应商);没有时走模型的首选渠道。 */
|
|
76
|
+
channel?: string;
|
|
75
77
|
}
|
|
76
78
|
export interface ThinkingLevelChangeEntry extends EntryBase {
|
|
77
79
|
type: "thinking_level_change";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图片文件(`read` 工具、`ama -p --image`、界面里的 `@图片路径` 共用):MIME 检测、尺寸、大小上限。
|
|
3
|
+
*
|
|
4
|
+
* - MIME 以文件头为准(PNG / JPEG / GIF / WebP),文件头认不出时退回扩展名;
|
|
5
|
+
* - 单张上限 5 MB(各家图片上限里最小的 Anthropic);
|
|
6
|
+
* - 不解码像素,只读文件头取尺寸。
|
|
7
|
+
*/
|
|
8
|
+
import type { ImageBlock } from "../ai/types.js";
|
|
9
|
+
export declare const MAX_IMAGE_BYTES: number;
|
|
10
|
+
export declare const IMAGE_EXTENSIONS: Readonly<Record<string, string>>;
|
|
11
|
+
/** 按扩展名(不分大小写);不是图片返回 undefined。 */
|
|
12
|
+
export declare function imageMimeFromPath(path: string): string | undefined;
|
|
13
|
+
/** 按文件头识别;认不出返回 undefined。 */
|
|
14
|
+
export declare function sniffImageMime(buf: Buffer): string | undefined;
|
|
15
|
+
export interface ImageSize {
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
}
|
|
19
|
+
/** 只读文件头取尺寸;不认识返回 undefined。 */
|
|
20
|
+
export declare function imageSize(buf: Buffer, mimeType: string): ImageSize | undefined;
|
|
21
|
+
export interface LoadedImage {
|
|
22
|
+
block: ImageBlock;
|
|
23
|
+
path: string;
|
|
24
|
+
mimeType: string;
|
|
25
|
+
bytes: number;
|
|
26
|
+
size?: ImageSize;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 读一张图片作为附件。文件不存在、不是图片、超过上限 → AmaError(`invalid_arguments`),文案给用户看。
|
|
30
|
+
*/
|
|
31
|
+
export declare function loadImageFile(path: string): Promise<LoadedImage>;
|