@piagent/platform 1.1.9 → 1.2.1

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 (81) hide show
  1. package/CHANGELOG.md +175 -1
  2. package/README.md +2 -2
  3. package/docs/command-reference-vietnamese.md +19 -5
  4. package/docs/distribution-standard.md +7 -7
  5. package/docs/mcp-and-tools.md +154 -2
  6. package/docs/model-options.md +10 -8
  7. package/docs/operator-manual-vietnamese.md +25 -13
  8. package/docs/publishing-for-teams.md +2 -0
  9. package/docs/quickstart-vietnamese.md +2 -2
  10. package/docs/release-install-policy.md +6 -6
  11. package/docs/security-threat-model.md +4 -3
  12. package/docs/team-onboarding.md +4 -4
  13. package/docs/usage-observability.md +29 -0
  14. package/docs/vercel-docs-site.md +34 -4
  15. package/docs-site/architecture.html +152 -0
  16. package/docs-site/assets/docs.css +1001 -0
  17. package/docs-site/assets/docs.js +84 -0
  18. package/docs-site/commands.html +203 -0
  19. package/docs-site/content/architecture.html +29 -0
  20. package/docs-site/content/commands.html +80 -0
  21. package/docs-site/content/index.html +80 -0
  22. package/docs-site/content/mcp-approval.html +133 -0
  23. package/docs-site/content/mcp-auth.html +76 -0
  24. package/docs-site/content/mcp-servers.html +111 -0
  25. package/docs-site/content/mcp.html +120 -0
  26. package/docs-site/content/permissions.html +54 -0
  27. package/docs-site/content/profiles.html +68 -0
  28. package/docs-site/content/quickstart.html +144 -0
  29. package/docs-site/content/runtime.html +45 -0
  30. package/docs-site/content/scope.html +71 -0
  31. package/docs-site/content/security.html +51 -0
  32. package/docs-site/content/team.html +23 -0
  33. package/docs-site/content/workflows.html +47 -0
  34. package/docs-site/index.html +131 -1534
  35. package/docs-site/mcp-approval.html +256 -0
  36. package/docs-site/mcp-auth.html +199 -0
  37. package/docs-site/mcp-servers.html +234 -0
  38. package/docs-site/mcp.html +243 -0
  39. package/docs-site/permissions.html +177 -0
  40. package/docs-site/profiles.html +191 -0
  41. package/docs-site/quickstart.html +267 -0
  42. package/docs-site/runtime.html +168 -0
  43. package/docs-site/scope.html +194 -0
  44. package/docs-site/security.html +174 -0
  45. package/docs-site/team.html +142 -0
  46. package/docs-site/workflows.html +170 -0
  47. package/package.json +4 -2
  48. package/packages/piagent-core/capabilities/capability-core.js +58 -5
  49. package/packages/piagent-core/capabilities/capability-sources.js +73 -12
  50. package/packages/piagent-core/extensions/document-intake.ts +5 -5
  51. package/packages/piagent-core/extensions/piagent-guard.ts +432 -2
  52. package/packages/piagent-core/extensions/policy-core.js +1586 -98
  53. package/packages/piagent-core/mcp/mcp-approval-store.js +216 -0
  54. package/packages/piagent-core/mcp/mcp-auth-readiness.js +225 -0
  55. package/packages/piagent-core/mcp/mcp-command-actions.js +400 -0
  56. package/packages/piagent-core/mcp/mcp-config-layers.js +466 -0
  57. package/packages/piagent-core/mcp/mcp-import-kinds.js +240 -0
  58. package/packages/piagent-core/mcp/mcp-server-catalog.js +150 -0
  59. package/packages/piagent-core/mcp/mcp-server-entry.js +293 -0
  60. package/packages/piagent-core/mcp/mcp-session-view.js +236 -0
  61. package/packages/piagent-core/mcp/mcp-tool-naming.js +90 -0
  62. package/packages/piagent-core/package.json +7 -3
  63. package/packages/piagent-core/prompts/model-options.md +8 -8
  64. package/scripts/build-docs-site.mjs +421 -0
  65. package/scripts/configure-model-scope.sh +21 -5
  66. package/scripts/configure-subagents.sh +18 -5
  67. package/scripts/init-project.sh +15 -8
  68. package/scripts/install-global.sh +5 -5
  69. package/scripts/mcp-manage.mjs +656 -0
  70. package/scripts/pi-model-catalog.sh +11 -1
  71. package/scripts/pi-session-stats.sh +27 -1
  72. package/scripts/pi-usage-history.mjs +567 -0
  73. package/scripts/piagent-cli.mjs +1 -1
  74. package/scripts/preview-docs-site.mjs +88 -0
  75. package/scripts/quality-benchmark.sh +37 -9
  76. package/scripts/setup.sh +23 -15
  77. package/scripts/team-doctor.sh +18 -2
  78. package/scripts/uninstall-global.sh +10 -4
  79. package/scripts/verify-local.sh +65 -19
  80. package/scripts/verify-release-identity.mjs +37 -2
  81. package/scripts/configure-mcp.sh +0 -349
@@ -5,23 +5,49 @@ usage() {
5
5
  cat <<'USAGE'
6
6
  Usage:
7
7
  scripts/pi-session-stats.sh [project-path] [session-file]
8
+ scripts/pi-session-stats.sh --history [project-path] [history-options]
8
9
 
9
10
  Examples:
10
11
  scripts/pi-session-stats.sh .
11
12
  scripts/pi-session-stats.sh /path/to/project
12
13
  scripts/pi-session-stats.sh /path/to/project ~/.pi/agent/sessions/.../session.jsonl
14
+ scripts/pi-session-stats.sh --history . --days 7
15
+ scripts/pi-session-stats.sh --history --all-projects --csv
13
16
 
14
17
  Notes:
15
18
  - Reads exact Pi token/cache/cost totals via RPC get_session_stats.
16
19
  - If session-file is omitted, the newest persisted session whose header cwd matches project-path is used.
20
+ - Use --history for finished/old session totals read directly from Pi JSONL.
17
21
  USAGE
18
22
  }
19
23
 
24
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
+ HISTORY_SCRIPT="$SCRIPT_DIR/pi-usage-history.mjs"
26
+
20
27
  if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
21
28
  usage
22
29
  exit 0
23
30
  fi
24
31
 
32
+ for argument in "$@"; do
33
+ case "$argument" in
34
+ --history|--all-projects|--days|--since|--until|--format|--json|--csv|--markdown|--include-subagents|--no-subagents|--limit|--sessions-dir)
35
+ exec node "$HISTORY_SCRIPT" "$@"
36
+ ;;
37
+ esac
38
+ done
39
+
40
+ # Both arguments are positional and the project path comes first, so a flag in
41
+ # either slot is a typo rather than a path. Saying so beats failing later with a
42
+ # message about a directory that was never meant to be one.
43
+ for argument in "${1:-}" "${2:-}"; do
44
+ if [[ "$argument" == -* ]]; then
45
+ echo "FAIL: this script takes <project-path> [session-file], not flags; received: $argument" >&2
46
+ usage >&2
47
+ exit 2
48
+ fi
49
+ done
50
+
25
51
  PROJECT_PATH="${1:-.}"
26
52
  SESSION_FILE="${2:-}"
27
53
 
@@ -79,7 +105,7 @@ fi
79
105
 
80
106
  RPC_OUTPUT="$(
81
107
  printf '%s\n' '{"type":"get_session_stats","id":"piagent-usage"}' \
82
- | pi --mode rpc --session "$SESSION_FILE" --offline --approve
108
+ | pi -ne --mode rpc --session "$SESSION_FILE" --offline --approve
83
109
  )"
84
110
 
85
111
  node --input-type=module - "$RPC_OUTPUT" <<'NODE'
@@ -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
+ }
@@ -14,7 +14,7 @@ const scriptByCommand = {
14
14
  "piagent-usage": "scripts/pi-session-stats.sh",
15
15
  "piagent-models": "scripts/pi-model-catalog.sh",
16
16
  "piagent-model-scope": "scripts/configure-model-scope.sh",
17
- "piagent-mcp": "scripts/configure-mcp.sh",
17
+ "piagent-mcp": "scripts/mcp-manage.mjs",
18
18
  "piagent-subagents": "scripts/configure-subagents.sh",
19
19
  "piagent-capabilities": "scripts/capability-catalog.mjs",
20
20
  "piagent-migrate": "scripts/migrate-project-state.mjs",