@piagent/platform 1.2.0 → 1.2.2
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 +149 -1
- package/README.md +1 -1
- package/catalog/capabilities.json +4 -4
- package/docs/command-reference-vietnamese.md +15 -4
- package/docs/context-window-policy.md +1 -0
- package/docs/distribution-standard.md +7 -7
- package/docs/mcp-and-tools.md +49 -3
- package/docs/operator-manual-vietnamese.md +52 -11
- package/docs/publishing-for-teams.md +2 -0
- package/docs/quickstart-vietnamese.md +4 -2
- package/docs/release-install-policy.md +6 -6
- package/docs/security-threat-model.md +4 -3
- package/docs/team-onboarding.md +3 -3
- package/docs/usage-observability.md +49 -0
- package/docs-site/architecture.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/mcp-approval.html +54 -2
- package/docs-site/content/quickstart.html +1 -1
- package/docs-site/index.html +1 -1
- package/docs-site/mcp-approval.html +55 -3
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +2 -2
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/package.json +1 -1
- package/packages/piagent-core/README.md +2 -0
- package/packages/piagent-core/capabilities/capability-core.js +41 -4
- package/packages/piagent-core/capabilities/capability-sources.js +73 -12
- package/packages/piagent-core/extensions/piagent-guard.ts +539 -70
- package/packages/piagent-core/extensions/policy-core.js +1586 -98
- package/packages/piagent-core/mcp/mcp-approval-store.js +56 -15
- package/packages/piagent-core/mcp/mcp-auth-readiness.js +31 -3
- package/packages/piagent-core/mcp/mcp-command-actions.js +139 -30
- package/packages/piagent-core/mcp/mcp-config-layers.js +339 -6
- package/packages/piagent-core/mcp/mcp-import-kinds.js +240 -0
- package/packages/piagent-core/mcp/mcp-server-entry.js +51 -0
- package/packages/piagent-core/mcp/mcp-session-view.js +55 -4
- package/packages/piagent-core/mcp/mcp-tool-naming.js +90 -0
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/prompts/be-to-fe.md +1 -0
- package/packages/piagent-core/prompts/piagent-commands.md +1 -0
- package/packages/piagent-core/prompts/platform-improve.md +1 -0
- package/packages/piagent-core/prompts/review.md +1 -0
- package/packages/piagent-core/prompts/scout.md +1 -0
- package/packages/piagent-core/prompts/task.md +1 -0
- package/scripts/configure-model-scope.sh +19 -3
- package/scripts/configure-subagents.sh +17 -4
- package/scripts/init-project.sh +15 -8
- package/scripts/mcp-manage.mjs +111 -22
- package/scripts/pi-model-catalog.sh +11 -1
- package/scripts/pi-session-stats.sh +27 -1
- package/scripts/pi-usage-history.mjs +567 -0
- package/scripts/quality-benchmark.sh +37 -9
- package/scripts/setup.sh +21 -13
- package/scripts/uninstall-global.sh +10 -4
- package/scripts/verify-local.sh +19 -0
- package/scripts/verify-release-identity.mjs +29 -0
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
const USAGE = `Usage:
|
|
8
|
+
scripts/pi-usage-history.mjs [project-path] [options]
|
|
9
|
+
scripts/pi-usage-history.mjs --all-projects [options]
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
--history Accepted for piagent-usage wrapper compatibility.
|
|
13
|
+
--all-projects Include every Pi session under the session directory.
|
|
14
|
+
--days N Include activity from the last N days.
|
|
15
|
+
--since YYYY-MM-DD Include activity on or after this local date.
|
|
16
|
+
--until YYYY-MM-DD Include activity on or before this local date.
|
|
17
|
+
--format table|json|csv|markdown
|
|
18
|
+
--json Alias for --format json.
|
|
19
|
+
--csv Alias for --format csv.
|
|
20
|
+
--markdown Alias for --format markdown.
|
|
21
|
+
--include-subagents Include subagent session files. Default.
|
|
22
|
+
--no-subagents Exclude files under sessions/subagent/.
|
|
23
|
+
--limit N Max session rows in table/markdown output. Default: 20.
|
|
24
|
+
--sessions-dir PATH Override Pi session root for tests or custom installs.
|
|
25
|
+
-h, --help Show this help.
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
piagent-usage --history .
|
|
29
|
+
piagent-usage --history --days 7 --all-projects
|
|
30
|
+
piagent-usage --history /path/to/project --json
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
34
|
+
|
|
35
|
+
function fail(message) {
|
|
36
|
+
console.error(`FAIL: ${message}`);
|
|
37
|
+
process.exit(2);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parsePositiveInteger(raw, label) {
|
|
41
|
+
if (!/^[1-9]\d*$/.test(String(raw ?? ""))) fail(`${label} must be a positive integer`);
|
|
42
|
+
return Number(raw);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseLocalDateStart(raw, label) {
|
|
46
|
+
const match = String(raw ?? "").match(DATE_ONLY);
|
|
47
|
+
if (!match) fail(`${label} must use YYYY-MM-DD`);
|
|
48
|
+
const [, y, m, d] = match;
|
|
49
|
+
const date = new Date(Number(y), Number(m) - 1, Number(d), 0, 0, 0, 0);
|
|
50
|
+
if (Number.isNaN(date.getTime())) fail(`${label} is not a valid date`);
|
|
51
|
+
return date;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseLocalDateEnd(raw, label) {
|
|
55
|
+
const start = parseLocalDateStart(raw, label);
|
|
56
|
+
return new Date(start.getFullYear(), start.getMonth(), start.getDate(), 23, 59, 59, 999);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseArgs(argv) {
|
|
60
|
+
const options = {
|
|
61
|
+
allProjects: false,
|
|
62
|
+
format: "table",
|
|
63
|
+
includeSubagents: true,
|
|
64
|
+
limit: 20,
|
|
65
|
+
sessionsDir: process.env.PI_CODING_AGENT_SESSION_DIR
|
|
66
|
+
|| path.join(process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent"), "sessions"),
|
|
67
|
+
projectPath: undefined,
|
|
68
|
+
since: undefined,
|
|
69
|
+
until: undefined
|
|
70
|
+
};
|
|
71
|
+
const positionals = [];
|
|
72
|
+
|
|
73
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
74
|
+
const arg = argv[index];
|
|
75
|
+
const readValue = (name) => {
|
|
76
|
+
const value = argv[index + 1];
|
|
77
|
+
if (value === undefined || value.startsWith("-")) fail(`Missing value for ${name}`);
|
|
78
|
+
index += 1;
|
|
79
|
+
return value;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
switch (arg) {
|
|
83
|
+
case "-h":
|
|
84
|
+
case "--help":
|
|
85
|
+
options.help = true;
|
|
86
|
+
break;
|
|
87
|
+
case "--history":
|
|
88
|
+
break;
|
|
89
|
+
case "--all-projects":
|
|
90
|
+
options.allProjects = true;
|
|
91
|
+
break;
|
|
92
|
+
case "--days": {
|
|
93
|
+
const days = parsePositiveInteger(readValue("--days"), "--days");
|
|
94
|
+
options.since = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case "--since":
|
|
98
|
+
options.since = parseLocalDateStart(readValue("--since"), "--since");
|
|
99
|
+
break;
|
|
100
|
+
case "--until":
|
|
101
|
+
options.until = parseLocalDateEnd(readValue("--until"), "--until");
|
|
102
|
+
break;
|
|
103
|
+
case "--format": {
|
|
104
|
+
const format = readValue("--format");
|
|
105
|
+
if (!["table", "json", "csv", "markdown"].includes(format)) {
|
|
106
|
+
fail("--format must be one of table, json, csv, markdown");
|
|
107
|
+
}
|
|
108
|
+
options.format = format;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case "--json":
|
|
112
|
+
options.format = "json";
|
|
113
|
+
break;
|
|
114
|
+
case "--csv":
|
|
115
|
+
options.format = "csv";
|
|
116
|
+
break;
|
|
117
|
+
case "--markdown":
|
|
118
|
+
options.format = "markdown";
|
|
119
|
+
break;
|
|
120
|
+
case "--include-subagents":
|
|
121
|
+
options.includeSubagents = true;
|
|
122
|
+
break;
|
|
123
|
+
case "--no-subagents":
|
|
124
|
+
options.includeSubagents = false;
|
|
125
|
+
break;
|
|
126
|
+
case "--limit":
|
|
127
|
+
options.limit = parsePositiveInteger(readValue("--limit"), "--limit");
|
|
128
|
+
break;
|
|
129
|
+
case "--sessions-dir":
|
|
130
|
+
options.sessionsDir = path.resolve(readValue("--sessions-dir"));
|
|
131
|
+
break;
|
|
132
|
+
default:
|
|
133
|
+
if (arg.startsWith("-")) fail(`unknown option: ${arg}`);
|
|
134
|
+
positionals.push(arg);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (positionals.length > 1) fail(`expected at most one project path, received ${positionals.length}`);
|
|
140
|
+
if (positionals[0]) {
|
|
141
|
+
const projectPath = path.resolve(positionals[0]);
|
|
142
|
+
if (!fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory()) {
|
|
143
|
+
fail(`project path does not exist: ${projectPath}`);
|
|
144
|
+
}
|
|
145
|
+
options.projectPath = fs.realpathSync(projectPath);
|
|
146
|
+
} else if (!options.allProjects) {
|
|
147
|
+
options.projectPath = fs.realpathSync(process.cwd());
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (options.since && options.until && options.since > options.until) {
|
|
151
|
+
fail("--since must be before or equal to --until");
|
|
152
|
+
}
|
|
153
|
+
return options;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function walkJsonl(root) {
|
|
157
|
+
const out = [];
|
|
158
|
+
if (!fs.existsSync(root)) return out;
|
|
159
|
+
const stack = [root];
|
|
160
|
+
while (stack.length) {
|
|
161
|
+
const dir = stack.pop();
|
|
162
|
+
let entries = [];
|
|
163
|
+
try {
|
|
164
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
165
|
+
} catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
for (const entry of entries) {
|
|
169
|
+
const target = path.join(dir, entry.name);
|
|
170
|
+
if (entry.isDirectory()) {
|
|
171
|
+
stack.push(target);
|
|
172
|
+
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
173
|
+
out.push(target);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseIso(raw) {
|
|
181
|
+
if (!raw) return undefined;
|
|
182
|
+
const date = new Date(raw);
|
|
183
|
+
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function inRange(date, options) {
|
|
187
|
+
if (!date) return true;
|
|
188
|
+
if (options.since && date < options.since) return false;
|
|
189
|
+
if (options.until && date > options.until) return false;
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function emptyTotals() {
|
|
194
|
+
return {
|
|
195
|
+
input: 0,
|
|
196
|
+
output: 0,
|
|
197
|
+
cacheRead: 0,
|
|
198
|
+
cacheWrite: 0,
|
|
199
|
+
reasoning: 0,
|
|
200
|
+
total: 0,
|
|
201
|
+
cost: 0
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function addTotals(target, usage) {
|
|
206
|
+
target.input += numberValue(usage.input);
|
|
207
|
+
target.output += numberValue(usage.output);
|
|
208
|
+
target.cacheRead += numberValue(usage.cacheRead);
|
|
209
|
+
target.cacheWrite += numberValue(usage.cacheWrite);
|
|
210
|
+
target.reasoning += numberValue(usage.reasoning);
|
|
211
|
+
target.total += numberValue(usage.totalTokens)
|
|
212
|
+
|| numberValue(usage.input) + numberValue(usage.output) + numberValue(usage.cacheRead) + numberValue(usage.cacheWrite);
|
|
213
|
+
const cost = usage.cost;
|
|
214
|
+
target.cost += typeof cost === "object" && cost !== null
|
|
215
|
+
? numberValue(cost.total)
|
|
216
|
+
: numberValue(cost);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function numberValue(raw) {
|
|
220
|
+
const n = Number(raw ?? 0);
|
|
221
|
+
return Number.isFinite(n) ? n : 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function extractTextLength(content) {
|
|
225
|
+
if (typeof content === "string") return content.length;
|
|
226
|
+
if (!Array.isArray(content)) return 0;
|
|
227
|
+
return content.reduce((sum, block) => {
|
|
228
|
+
if (block?.type === "text" && typeof block.text === "string") return sum + block.text.length;
|
|
229
|
+
return sum;
|
|
230
|
+
}, 0);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function summarizeSession(file, options) {
|
|
234
|
+
let stat;
|
|
235
|
+
try {
|
|
236
|
+
stat = fs.statSync(file);
|
|
237
|
+
} catch {
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const summary = {
|
|
242
|
+
id: path.basename(file, ".jsonl"),
|
|
243
|
+
name: "",
|
|
244
|
+
cwd: "",
|
|
245
|
+
file,
|
|
246
|
+
isSubagent: file.split(path.sep).includes("subagent"),
|
|
247
|
+
provider: "",
|
|
248
|
+
modelId: "",
|
|
249
|
+
thinkingLevel: "",
|
|
250
|
+
firstTimestamp: undefined,
|
|
251
|
+
lastTimestamp: undefined,
|
|
252
|
+
countedFirstTimestamp: undefined,
|
|
253
|
+
countedLastTimestamp: undefined,
|
|
254
|
+
messages: { user: 0, assistant: 0, toolCalls: 0, toolResults: 0, total: 0 },
|
|
255
|
+
promptChars: 0,
|
|
256
|
+
tokens: emptyTotals(),
|
|
257
|
+
toolNames: {},
|
|
258
|
+
sizeBytes: stat.size,
|
|
259
|
+
mtime: stat.mtime
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
let hasCountedActivity = false;
|
|
263
|
+
const lines = fs.readFileSync(file, "utf8").split(/\n/);
|
|
264
|
+
for (const line of lines) {
|
|
265
|
+
if (!line) continue;
|
|
266
|
+
let entry;
|
|
267
|
+
try {
|
|
268
|
+
entry = JSON.parse(line);
|
|
269
|
+
} catch {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const ts = parseIso(entry.timestamp ?? entry.message?.timestamp);
|
|
274
|
+
if (ts) {
|
|
275
|
+
if (!summary.firstTimestamp || ts < summary.firstTimestamp) summary.firstTimestamp = ts;
|
|
276
|
+
if (!summary.lastTimestamp || ts > summary.lastTimestamp) summary.lastTimestamp = ts;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (entry.type === "session") {
|
|
280
|
+
if (entry.id) summary.id = String(entry.id);
|
|
281
|
+
if (entry.cwd) summary.cwd = String(entry.cwd);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (entry.type === "model_change") {
|
|
285
|
+
summary.provider = String(entry.provider ?? summary.provider);
|
|
286
|
+
summary.modelId = String(entry.modelId ?? summary.modelId);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (entry.type === "thinking_level_change") {
|
|
290
|
+
summary.thinkingLevel = String(entry.thinkingLevel ?? summary.thinkingLevel);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (entry.type === "session_info" && entry.name) {
|
|
294
|
+
summary.name = String(entry.name);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (entry.type !== "message" || !entry.message) continue;
|
|
298
|
+
|
|
299
|
+
const counted = inRange(ts ?? stat.mtime, options);
|
|
300
|
+
if (!counted) continue;
|
|
301
|
+
hasCountedActivity = true;
|
|
302
|
+
const countedTs = ts ?? stat.mtime;
|
|
303
|
+
if (!summary.countedFirstTimestamp || countedTs < summary.countedFirstTimestamp) summary.countedFirstTimestamp = countedTs;
|
|
304
|
+
if (!summary.countedLastTimestamp || countedTs > summary.countedLastTimestamp) summary.countedLastTimestamp = countedTs;
|
|
305
|
+
|
|
306
|
+
const message = entry.message;
|
|
307
|
+
const role = message.role;
|
|
308
|
+
summary.messages.total += 1;
|
|
309
|
+
if (role === "user") {
|
|
310
|
+
summary.messages.user += 1;
|
|
311
|
+
summary.promptChars += extractTextLength(message.content);
|
|
312
|
+
} else if (role === "assistant") {
|
|
313
|
+
summary.messages.assistant += 1;
|
|
314
|
+
if (message.provider && !summary.provider) summary.provider = String(message.provider);
|
|
315
|
+
if (message.model && !summary.modelId) summary.modelId = String(message.model);
|
|
316
|
+
if (message.usage) addTotals(summary.tokens, message.usage);
|
|
317
|
+
if (Array.isArray(message.content)) {
|
|
318
|
+
for (const block of message.content) {
|
|
319
|
+
if (block?.type !== "toolCall") continue;
|
|
320
|
+
summary.messages.toolCalls += 1;
|
|
321
|
+
const name = String(block.name ?? "tool");
|
|
322
|
+
summary.toolNames[name] = (summary.toolNames[name] ?? 0) + 1;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} else if (role === "toolResult") {
|
|
326
|
+
summary.messages.toolResults += 1;
|
|
327
|
+
const name = String(message.toolName ?? "tool");
|
|
328
|
+
summary.toolNames[name] = summary.toolNames[name] ?? 0;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!summary.cwd) summary.cwd = "(unknown)";
|
|
333
|
+
if (!summary.firstTimestamp) summary.firstTimestamp = stat.mtime;
|
|
334
|
+
if (!summary.lastTimestamp) summary.lastTimestamp = stat.mtime;
|
|
335
|
+
if (!summary.countedFirstTimestamp) summary.countedFirstTimestamp = summary.firstTimestamp;
|
|
336
|
+
if (!summary.countedLastTimestamp) summary.countedLastTimestamp = summary.lastTimestamp;
|
|
337
|
+
|
|
338
|
+
if (!hasCountedActivity && (options.since || options.until)) return undefined;
|
|
339
|
+
return summary;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function loadReport(options) {
|
|
343
|
+
const files = walkJsonl(options.sessionsDir);
|
|
344
|
+
const sessions = [];
|
|
345
|
+
for (const file of files) {
|
|
346
|
+
const isSubagent = file.split(path.sep).includes("subagent");
|
|
347
|
+
if (isSubagent && !options.includeSubagents) continue;
|
|
348
|
+
const session = summarizeSession(file, options);
|
|
349
|
+
if (!session) continue;
|
|
350
|
+
if (!options.allProjects && options.projectPath && session.cwd !== options.projectPath) continue;
|
|
351
|
+
sessions.push(session);
|
|
352
|
+
}
|
|
353
|
+
sessions.sort((a, b) => b.countedLastTimestamp - a.countedLastTimestamp);
|
|
354
|
+
return buildReport(options, sessions);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function buildReport(options, sessions) {
|
|
358
|
+
const totals = {
|
|
359
|
+
sessions: sessions.length,
|
|
360
|
+
mainSessions: sessions.filter((s) => !s.isSubagent).length,
|
|
361
|
+
subagentSessions: sessions.filter((s) => s.isSubagent).length,
|
|
362
|
+
messages: { user: 0, assistant: 0, toolCalls: 0, toolResults: 0, total: 0 },
|
|
363
|
+
promptChars: 0,
|
|
364
|
+
tokens: emptyTotals()
|
|
365
|
+
};
|
|
366
|
+
const projects = new Map();
|
|
367
|
+
const tools = {};
|
|
368
|
+
for (const session of sessions) {
|
|
369
|
+
for (const key of Object.keys(totals.messages)) totals.messages[key] += session.messages[key];
|
|
370
|
+
totals.promptChars += session.promptChars;
|
|
371
|
+
for (const key of Object.keys(totals.tokens)) totals.tokens[key] += session.tokens[key];
|
|
372
|
+
const project = projects.get(session.cwd) ?? {
|
|
373
|
+
cwd: session.cwd,
|
|
374
|
+
sessions: 0,
|
|
375
|
+
mainSessions: 0,
|
|
376
|
+
subagentSessions: 0,
|
|
377
|
+
tokens: emptyTotals(),
|
|
378
|
+
cost: 0
|
|
379
|
+
};
|
|
380
|
+
project.sessions += 1;
|
|
381
|
+
if (session.isSubagent) project.subagentSessions += 1;
|
|
382
|
+
else project.mainSessions += 1;
|
|
383
|
+
for (const key of Object.keys(project.tokens)) project.tokens[key] += session.tokens[key];
|
|
384
|
+
project.cost = project.tokens.cost;
|
|
385
|
+
projects.set(session.cwd, project);
|
|
386
|
+
for (const [name, count] of Object.entries(session.toolNames)) {
|
|
387
|
+
tools[name] = (tools[name] ?? 0) + count;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const projectRows = [...projects.values()].sort((a, b) => b.tokens.total - a.tokens.total);
|
|
392
|
+
const toolRows = Object.entries(tools)
|
|
393
|
+
.map(([name, count]) => ({ name, count }))
|
|
394
|
+
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
generatedAt: new Date().toISOString(),
|
|
398
|
+
scope: {
|
|
399
|
+
projectPath: options.allProjects ? null : options.projectPath,
|
|
400
|
+
allProjects: options.allProjects,
|
|
401
|
+
includeSubagents: options.includeSubagents,
|
|
402
|
+
sessionsDir: options.sessionsDir,
|
|
403
|
+
since: options.since?.toISOString() ?? null,
|
|
404
|
+
until: options.until?.toISOString() ?? null
|
|
405
|
+
},
|
|
406
|
+
totals,
|
|
407
|
+
projects: projectRows,
|
|
408
|
+
tools: toolRows,
|
|
409
|
+
sessions
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function compact(n) {
|
|
414
|
+
const abs = Math.abs(n);
|
|
415
|
+
if (abs >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)}B`;
|
|
416
|
+
if (abs >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
417
|
+
if (abs >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
|
|
418
|
+
return String(Math.round(n));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function money(n) {
|
|
422
|
+
return `$${n.toFixed(6)}`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function iso(date) {
|
|
426
|
+
return date ? date.toISOString() : "";
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function localLabel(date) {
|
|
430
|
+
if (!date) return "?";
|
|
431
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
432
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function rangeLabel(report) {
|
|
436
|
+
const { since, until } = report.scope;
|
|
437
|
+
if (!since && !until) return "all time";
|
|
438
|
+
return `${since ? since.slice(0, 10) : "beginning"} -> ${until ? until.slice(0, 10) : "now"}`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function renderTable(report, limit) {
|
|
442
|
+
const lines = [];
|
|
443
|
+
lines.push("PiAgent usage history");
|
|
444
|
+
lines.push(`Scope: ${report.scope.allProjects ? "all projects" : report.scope.projectPath}`);
|
|
445
|
+
lines.push(`Range: ${rangeLabel(report)}`);
|
|
446
|
+
lines.push(`Sessions: ${report.totals.sessions} (${report.totals.mainSessions} main, ${report.totals.subagentSessions} subagent)`);
|
|
447
|
+
lines.push(`Tokens: ${compact(report.totals.tokens.total)} total (${compact(report.totals.tokens.input)} input, ${compact(report.totals.tokens.output)} output, ${compact(report.totals.tokens.cacheRead)} cache read, ${compact(report.totals.tokens.cacheWrite)} cache write, ${compact(report.totals.tokens.reasoning)} reasoning)`);
|
|
448
|
+
lines.push(`Cost: ${money(report.totals.tokens.cost)}`);
|
|
449
|
+
lines.push(`Messages: ${report.totals.messages.user} user, ${report.totals.messages.assistant} assistant, ${report.totals.messages.toolCalls} tool calls, ${report.totals.messages.toolResults} tool results`);
|
|
450
|
+
if (report.projects.length) {
|
|
451
|
+
lines.push("");
|
|
452
|
+
lines.push("Projects:");
|
|
453
|
+
for (const project of report.projects.slice(0, 10)) {
|
|
454
|
+
lines.push(` ${compact(project.tokens.total).padStart(8)} tok ${money(project.tokens.cost).padStart(11)} ${String(project.sessions).padStart(3)} sessions ${project.cwd}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (report.sessions.length) {
|
|
458
|
+
lines.push("");
|
|
459
|
+
lines.push(`Top sessions (${Math.min(limit, report.sessions.length)} of ${report.sessions.length}):`);
|
|
460
|
+
for (const session of report.sessions.slice(0, limit)) {
|
|
461
|
+
const kind = session.isSubagent ? "sub" : "main";
|
|
462
|
+
const model = session.modelId || session.provider || "?";
|
|
463
|
+
lines.push(` ${localLabel(session.countedLastTimestamp)} ${kind.padEnd(4)} ${compact(session.tokens.total).padStart(8)} tok ${money(session.tokens.cost).padStart(11)} ${String(session.messages.user).padStart(2)}p/${String(session.messages.toolCalls).padStart(2)}t ${model} ${session.name || session.id}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return `${lines.join("\n")}\n`;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function renderMarkdown(report, limit) {
|
|
470
|
+
const lines = [];
|
|
471
|
+
lines.push("# PiAgent Usage History");
|
|
472
|
+
lines.push("");
|
|
473
|
+
lines.push(`- Scope: ${report.scope.allProjects ? "all projects" : report.scope.projectPath}`);
|
|
474
|
+
lines.push(`- Range: ${rangeLabel(report)}`);
|
|
475
|
+
lines.push(`- Sessions: ${report.totals.sessions} (${report.totals.mainSessions} main, ${report.totals.subagentSessions} subagent)`);
|
|
476
|
+
lines.push(`- Tokens: ${report.totals.tokens.total.toLocaleString("en-US")}`);
|
|
477
|
+
lines.push(`- Cost: ${money(report.totals.tokens.cost)}`);
|
|
478
|
+
lines.push("");
|
|
479
|
+
lines.push("## Projects");
|
|
480
|
+
lines.push("");
|
|
481
|
+
lines.push("| Project | Sessions | Tokens | Cost |");
|
|
482
|
+
lines.push("|---|---:|---:|---:|");
|
|
483
|
+
for (const project of report.projects.slice(0, 20)) {
|
|
484
|
+
lines.push(`| ${escapeMarkdown(project.cwd)} | ${project.sessions} | ${project.tokens.total} | ${money(project.tokens.cost)} |`);
|
|
485
|
+
}
|
|
486
|
+
lines.push("");
|
|
487
|
+
lines.push("## Sessions");
|
|
488
|
+
lines.push("");
|
|
489
|
+
lines.push("| Last activity | Kind | Session | Model | Prompts | Tools | Tokens | Cost |");
|
|
490
|
+
lines.push("|---|---|---|---|---:|---:|---:|---:|");
|
|
491
|
+
for (const session of report.sessions.slice(0, limit)) {
|
|
492
|
+
lines.push(`| ${localLabel(session.countedLastTimestamp)} | ${session.isSubagent ? "subagent" : "main"} | ${escapeMarkdown(session.name || session.id)} | ${escapeMarkdown(session.modelId || session.provider || "?")} | ${session.messages.user} | ${session.messages.toolCalls} | ${session.tokens.total} | ${money(session.tokens.cost)} |`);
|
|
493
|
+
}
|
|
494
|
+
return `${lines.join("\n")}\n`;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function escapeMarkdown(raw) {
|
|
498
|
+
return String(raw).replaceAll("|", "\\|");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function csvEscape(raw) {
|
|
502
|
+
const s = String(raw ?? "");
|
|
503
|
+
if (!/[",\n]/.test(s)) return s;
|
|
504
|
+
return `"${s.replaceAll("\"", "\"\"")}"`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function renderCsv(report) {
|
|
508
|
+
const columns = [
|
|
509
|
+
"session_id", "name", "cwd", "kind", "first_timestamp", "last_timestamp",
|
|
510
|
+
"provider", "model", "thinking_level", "user_messages", "assistant_messages",
|
|
511
|
+
"tool_calls", "tool_results", "input_tokens", "output_tokens", "cache_read_tokens",
|
|
512
|
+
"cache_write_tokens", "reasoning_tokens", "total_tokens", "cost_usd", "file"
|
|
513
|
+
];
|
|
514
|
+
const rows = [columns.join(",")];
|
|
515
|
+
for (const s of report.sessions) {
|
|
516
|
+
rows.push([
|
|
517
|
+
s.id, s.name, s.cwd, s.isSubagent ? "subagent" : "main", iso(s.firstTimestamp), iso(s.lastTimestamp),
|
|
518
|
+
s.provider, s.modelId, s.thinkingLevel, s.messages.user, s.messages.assistant,
|
|
519
|
+
s.messages.toolCalls, s.messages.toolResults, s.tokens.input, s.tokens.output, s.tokens.cacheRead,
|
|
520
|
+
s.tokens.cacheWrite, s.tokens.reasoning, s.tokens.total, s.tokens.cost.toFixed(6), s.file
|
|
521
|
+
].map(csvEscape).join(","));
|
|
522
|
+
}
|
|
523
|
+
return `${rows.join("\n")}\n`;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function toJson(report) {
|
|
527
|
+
return JSON.stringify(report, (_key, value) => value instanceof Date ? value.toISOString() : value, 2) + "\n";
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function render(report, options) {
|
|
531
|
+
switch (options.format) {
|
|
532
|
+
case "json":
|
|
533
|
+
return toJson(report);
|
|
534
|
+
case "csv":
|
|
535
|
+
return renderCsv(report);
|
|
536
|
+
case "markdown":
|
|
537
|
+
return renderMarkdown(report, options.limit);
|
|
538
|
+
default:
|
|
539
|
+
return renderTable(report, options.limit);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export {
|
|
544
|
+
buildReport,
|
|
545
|
+
loadReport,
|
|
546
|
+
parseArgs,
|
|
547
|
+
render,
|
|
548
|
+
summarizeSession,
|
|
549
|
+
walkJsonl
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
async function main() {
|
|
553
|
+
const options = parseArgs(process.argv.slice(2));
|
|
554
|
+
if (options.help) {
|
|
555
|
+
process.stdout.write(USAGE);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const report = loadReport(options);
|
|
559
|
+
process.stdout.write(render(report, options));
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (import.meta.url === pathToFileURL(process.argv[1] || "").href) {
|
|
563
|
+
main().catch((error) => {
|
|
564
|
+
console.error(`FAIL: ${error?.message ?? error}`);
|
|
565
|
+
process.exit(1);
|
|
566
|
+
});
|
|
567
|
+
}
|
|
@@ -31,6 +31,14 @@ if [[ -z "$PROJECT_PATH" ]]; then
|
|
|
31
31
|
usage
|
|
32
32
|
exit 2
|
|
33
33
|
fi
|
|
34
|
+
# The project path is positional and comes first, so omitting it hands the first
|
|
35
|
+
# flag to `cd` and the run dies somewhere further down complaining about an
|
|
36
|
+
# option. Say what is actually wrong instead.
|
|
37
|
+
if [[ "$PROJECT_PATH" == -* ]]; then
|
|
38
|
+
echo "FAIL: the project path comes first, before any flag; received: $PROJECT_PATH" >&2
|
|
39
|
+
usage >&2
|
|
40
|
+
exit 2
|
|
41
|
+
fi
|
|
34
42
|
shift || true
|
|
35
43
|
|
|
36
44
|
PROJECT_PATH="$(cd "$PROJECT_PATH" && pwd)"
|
|
@@ -45,6 +53,17 @@ COST=""
|
|
|
45
53
|
DURATION=""
|
|
46
54
|
NOTES=""
|
|
47
55
|
|
|
56
|
+
# A flag left without a value otherwise consumes the next flag as its value, and
|
|
57
|
+
# the run continues on a record that says something nobody typed.
|
|
58
|
+
require_value() {
|
|
59
|
+
local option="$1"
|
|
60
|
+
local value="${2:-}"
|
|
61
|
+
if [[ -z "$value" || "$value" == --* ]]; then
|
|
62
|
+
echo "Missing value for $option" >&2
|
|
63
|
+
exit 2
|
|
64
|
+
fi
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
while [[ $# -gt 0 ]]; do
|
|
49
68
|
case "$1" in
|
|
50
69
|
--init)
|
|
@@ -56,39 +75,48 @@ while [[ $# -gt 0 ]]; do
|
|
|
56
75
|
shift
|
|
57
76
|
;;
|
|
58
77
|
--scenario)
|
|
59
|
-
|
|
78
|
+
require_value "$1" "${2:-}"
|
|
79
|
+
SCENARIO="$2"
|
|
60
80
|
shift 2
|
|
61
81
|
;;
|
|
62
82
|
--surface|--agent)
|
|
63
|
-
|
|
83
|
+
require_value "$1" "${2:-}"
|
|
84
|
+
SURFACE="$2"
|
|
64
85
|
shift 2
|
|
65
86
|
;;
|
|
66
87
|
--result)
|
|
67
|
-
|
|
88
|
+
require_value "$1" "${2:-}"
|
|
89
|
+
RESULT="$2"
|
|
68
90
|
shift 2
|
|
69
91
|
;;
|
|
70
92
|
--task-file)
|
|
71
|
-
|
|
93
|
+
require_value "$1" "${2:-}"
|
|
94
|
+
TASK_FILE="$2"
|
|
72
95
|
shift 2
|
|
73
96
|
;;
|
|
74
97
|
--verify)
|
|
75
|
-
|
|
98
|
+
require_value "$1" "${2:-}"
|
|
99
|
+
VERIFY_CMD="$2"
|
|
76
100
|
shift 2
|
|
77
101
|
;;
|
|
78
102
|
--tokens)
|
|
79
|
-
|
|
103
|
+
require_value "$1" "${2:-}"
|
|
104
|
+
TOKENS="$2"
|
|
80
105
|
shift 2
|
|
81
106
|
;;
|
|
82
107
|
--cost)
|
|
83
|
-
|
|
108
|
+
require_value "$1" "${2:-}"
|
|
109
|
+
COST="$2"
|
|
84
110
|
shift 2
|
|
85
111
|
;;
|
|
86
112
|
--duration)
|
|
87
|
-
|
|
113
|
+
require_value "$1" "${2:-}"
|
|
114
|
+
DURATION="$2"
|
|
88
115
|
shift 2
|
|
89
116
|
;;
|
|
90
117
|
--notes)
|
|
91
|
-
|
|
118
|
+
require_value "$1" "${2:-}"
|
|
119
|
+
NOTES="$2"
|
|
92
120
|
shift 2
|
|
93
121
|
;;
|
|
94
122
|
-h|--help)
|