@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.
- package/CHANGELOG.md +175 -1
- package/README.md +2 -2
- package/docs/command-reference-vietnamese.md +19 -5
- package/docs/distribution-standard.md +7 -7
- package/docs/mcp-and-tools.md +154 -2
- package/docs/model-options.md +10 -8
- package/docs/operator-manual-vietnamese.md +25 -13
- package/docs/publishing-for-teams.md +2 -0
- package/docs/quickstart-vietnamese.md +2 -2
- package/docs/release-install-policy.md +6 -6
- package/docs/security-threat-model.md +4 -3
- package/docs/team-onboarding.md +4 -4
- package/docs/usage-observability.md +29 -0
- package/docs/vercel-docs-site.md +34 -4
- package/docs-site/architecture.html +152 -0
- package/docs-site/assets/docs.css +1001 -0
- package/docs-site/assets/docs.js +84 -0
- package/docs-site/commands.html +203 -0
- package/docs-site/content/architecture.html +29 -0
- package/docs-site/content/commands.html +80 -0
- package/docs-site/content/index.html +80 -0
- package/docs-site/content/mcp-approval.html +133 -0
- package/docs-site/content/mcp-auth.html +76 -0
- package/docs-site/content/mcp-servers.html +111 -0
- package/docs-site/content/mcp.html +120 -0
- package/docs-site/content/permissions.html +54 -0
- package/docs-site/content/profiles.html +68 -0
- package/docs-site/content/quickstart.html +144 -0
- package/docs-site/content/runtime.html +45 -0
- package/docs-site/content/scope.html +71 -0
- package/docs-site/content/security.html +51 -0
- package/docs-site/content/team.html +23 -0
- package/docs-site/content/workflows.html +47 -0
- package/docs-site/index.html +131 -1534
- package/docs-site/mcp-approval.html +256 -0
- package/docs-site/mcp-auth.html +199 -0
- package/docs-site/mcp-servers.html +234 -0
- package/docs-site/mcp.html +243 -0
- package/docs-site/permissions.html +177 -0
- package/docs-site/profiles.html +191 -0
- package/docs-site/quickstart.html +267 -0
- package/docs-site/runtime.html +168 -0
- package/docs-site/scope.html +194 -0
- package/docs-site/security.html +174 -0
- package/docs-site/team.html +142 -0
- package/docs-site/workflows.html +170 -0
- package/package.json +4 -2
- package/packages/piagent-core/capabilities/capability-core.js +58 -5
- package/packages/piagent-core/capabilities/capability-sources.js +73 -12
- package/packages/piagent-core/extensions/document-intake.ts +5 -5
- package/packages/piagent-core/extensions/piagent-guard.ts +432 -2
- package/packages/piagent-core/extensions/policy-core.js +1586 -98
- package/packages/piagent-core/mcp/mcp-approval-store.js +216 -0
- package/packages/piagent-core/mcp/mcp-auth-readiness.js +225 -0
- package/packages/piagent-core/mcp/mcp-command-actions.js +400 -0
- package/packages/piagent-core/mcp/mcp-config-layers.js +466 -0
- package/packages/piagent-core/mcp/mcp-import-kinds.js +240 -0
- package/packages/piagent-core/mcp/mcp-server-catalog.js +150 -0
- package/packages/piagent-core/mcp/mcp-server-entry.js +293 -0
- package/packages/piagent-core/mcp/mcp-session-view.js +236 -0
- package/packages/piagent-core/mcp/mcp-tool-naming.js +90 -0
- package/packages/piagent-core/package.json +7 -3
- package/packages/piagent-core/prompts/model-options.md +8 -8
- package/scripts/build-docs-site.mjs +421 -0
- package/scripts/configure-model-scope.sh +21 -5
- package/scripts/configure-subagents.sh +18 -5
- package/scripts/init-project.sh +15 -8
- package/scripts/install-global.sh +5 -5
- package/scripts/mcp-manage.mjs +656 -0
- 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/piagent-cli.mjs +1 -1
- package/scripts/preview-docs-site.mjs +88 -0
- package/scripts/quality-benchmark.sh +37 -9
- package/scripts/setup.sh +23 -15
- package/scripts/team-doctor.sh +18 -2
- package/scripts/uninstall-global.sh +10 -4
- package/scripts/verify-local.sh +65 -19
- package/scripts/verify-release-identity.mjs +37 -2
- 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
|
+
}
|
package/scripts/piagent-cli.mjs
CHANGED
|
@@ -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/
|
|
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",
|