@gajae-code/stats 0.18.1 → 0.18.6
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/README.md +17 -8
- package/package.json +3 -3
- package/src/server.ts +28 -7
package/README.md
CHANGED
|
@@ -56,13 +56,22 @@ console.log(stats.byAgent.find(agent => agent.agent === "executor")?.totalCost);
|
|
|
56
56
|
|
|
57
57
|
## API Endpoints
|
|
58
58
|
|
|
59
|
+
Endpoints marked *range* accept `?range=1h|24h|7d|30d|90d|all` (default `24h`; an unknown value falls back to `24h`). Time series use hourly buckets for `1h` and `24h` and daily buckets for longer ranges.
|
|
60
|
+
|
|
59
61
|
| Endpoint | Description |
|
|
60
62
|
|----------|-------------|
|
|
61
|
-
| `GET /api/stats` |
|
|
62
|
-
| `GET /api/stats/
|
|
63
|
-
| `GET /api/stats/
|
|
64
|
-
| `GET /api/stats/
|
|
65
|
-
| `
|
|
63
|
+
| `GET /api/stats` | The full `DashboardStats` object: overall, failures, per-model/folder/agent breakdowns, the time, model, model-performance, and cost series, and the cache-miss attribution (*range*) |
|
|
64
|
+
| `GET /api/stats/overview` | `overall` and `timeSeries` only (*range*) |
|
|
65
|
+
| `GET /api/stats/models` | Per-model statistics, ordered by request count (*range*) |
|
|
66
|
+
| `GET /api/stats/model-dashboard` | `byModel`, `modelSeries`, and `modelPerformanceSeries` (*range*) |
|
|
67
|
+
| `GET /api/stats/folders` | Per-folder/project statistics (*range*) |
|
|
68
|
+
| `GET /api/stats/timeseries` | Requests, errors, tokens, and cost per time bucket (*range*) |
|
|
69
|
+
| `GET /api/stats/costs` | Daily cost per model and provider, split into input/output/cache (*range*) |
|
|
70
|
+
| `GET /api/stats/behavior` | User-message behavior signals (yelling, profanity, repetition, and others): overall, per model, and over time (*range*) |
|
|
71
|
+
| `GET /api/stats/recent` | Most recent requests, newest first (`?limit=`, default 100) |
|
|
72
|
+
| `GET /api/stats/errors` | Most recent requests that stopped with an error (`?limit=`, default 100) |
|
|
73
|
+
| `GET /api/request/:id` | One request with its stored messages and output; `404` if unknown |
|
|
74
|
+
| `POST /api/sync` | Sync session files and return `{ processed, files, totalMessages }`; `409` while a sync is already running |
|
|
66
75
|
|
|
67
76
|
## Local server security
|
|
68
77
|
|
|
@@ -97,10 +106,10 @@ gjc stats --summary
|
|
|
97
106
|
|
|
98
107
|
### JSON output for scripting
|
|
99
108
|
|
|
100
|
-
|
|
109
|
+
With `--json`, stdout carries only the JSON document; the sync progress and the `Synced N new entries ...` summary go to stderr.
|
|
101
110
|
|
|
102
111
|
```bash
|
|
103
|
-
gjc stats --json |
|
|
112
|
+
gjc stats --json | jq '.overall.totalCost'
|
|
104
113
|
```
|
|
105
114
|
|
|
106
115
|
### Dashboard on a custom port
|
|
@@ -145,7 +154,7 @@ if (topModel) {
|
|
|
145
154
|
|
|
146
155
|
### Troubleshooting
|
|
147
156
|
|
|
148
|
-
- **No data shown?** Check that session logs exist under `~/.gjc/agent/sessions/`.
|
|
157
|
+
- **No data shown?** Check that session logs exist under `~/.gjc/agent/sessions/` (`~/<GJC_CONFIG_DIR>/agent/sessions/` if that variable is set). With a custom `GJC_CODING_AGENT_DIR`, sessions are in `$GJC_CODING_AGENT_DIR/sessions/`. Otherwise, on Linux and macOS, if `XDG_DATA_HOME` is set and `$XDG_DATA_HOME/gjc` exists (created by `gjc config init-xdg`), sessions are in `$XDG_DATA_HOME/gjc/sessions/`. These variables must come from your shell environment. A project `.env` cannot set them, and if the `.env` entry uses `$` expansion it blocks the shell value as well; in both cases the default path applies.
|
|
149
158
|
- **Dashboard not starting?** Check that port 3847 (or the port passed to `--port`) is free.
|
|
150
159
|
|
|
151
160
|
## License
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/stats",
|
|
4
|
-
"version": "0.18.
|
|
4
|
+
"version": "0.18.6",
|
|
5
5
|
"description": "Local observability dashboard for pi AI usage statistics",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
7
7
|
"author": "Yeachan-Heo",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"fmt": "biome format --write ."
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@gajae-code/ai": "0.18.
|
|
42
|
-
"@gajae-code/utils": "0.18.
|
|
41
|
+
"@gajae-code/ai": "0.18.6",
|
|
42
|
+
"@gajae-code/utils": "0.18.6",
|
|
43
43
|
"@tailwindcss/node": "^4.2.4",
|
|
44
44
|
"chart.js": "^4.5.1",
|
|
45
45
|
"date-fns": "^4.1.0",
|
package/src/server.ts
CHANGED
|
@@ -167,14 +167,16 @@ async function handleApi(req: Request, context: ApiContext): Promise<Response> {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
if (path === "/api/stats/recent") {
|
|
170
|
-
const limit = url.searchParams.get("limit");
|
|
171
|
-
|
|
170
|
+
const limit = parseLimitParam(url.searchParams.get("limit"));
|
|
171
|
+
if (limit === null) return badRequest();
|
|
172
|
+
const stats = await getRecentRequests(limit);
|
|
172
173
|
return Response.json(stats);
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
if (path === "/api/stats/errors") {
|
|
176
|
-
const limit = url.searchParams.get("limit");
|
|
177
|
-
|
|
177
|
+
const limit = parseLimitParam(url.searchParams.get("limit"));
|
|
178
|
+
if (limit === null) return badRequest();
|
|
179
|
+
const stats = await getRecentErrors(limit);
|
|
178
180
|
return Response.json(stats);
|
|
179
181
|
}
|
|
180
182
|
|
|
@@ -194,9 +196,9 @@ async function handleApi(req: Request, context: ApiContext): Promise<Response> {
|
|
|
194
196
|
}
|
|
195
197
|
|
|
196
198
|
if (path.startsWith("/api/request/")) {
|
|
197
|
-
const id = path.
|
|
198
|
-
if (
|
|
199
|
-
const details = await getRequestDetails(
|
|
199
|
+
const id = parseIntegerParam(path.slice("/api/request/".length));
|
|
200
|
+
if (id === null) return badRequest();
|
|
201
|
+
const details = await getRequestDetails(id);
|
|
200
202
|
if (!details) return new Response("Not Found", { status: 404 });
|
|
201
203
|
return Response.json(details);
|
|
202
204
|
}
|
|
@@ -218,6 +220,25 @@ async function handleApi(req: Request, context: ApiContext): Promise<Response> {
|
|
|
218
220
|
return new Response("Not Found", { status: 404 });
|
|
219
221
|
}
|
|
220
222
|
|
|
223
|
+
/**
|
|
224
|
+
* A request parameter that must be a whole decimal number. `parseInt` alone
|
|
225
|
+
* accepts "1abc" and "-1", and SQLite rejects NaN with a datatype mismatch and
|
|
226
|
+
* treats a negative LIMIT as unlimited, so anything but plain digits is refused.
|
|
227
|
+
*/
|
|
228
|
+
function parseIntegerParam(value: string): number | null {
|
|
229
|
+
if (!/^\d{1,15}$/.test(value)) return null;
|
|
230
|
+
return Number(value);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** `?limit=`: absent keeps the query's own default; present must be a whole number. */
|
|
234
|
+
function parseLimitParam(value: string | null): number | undefined | null {
|
|
235
|
+
return value === null ? undefined : parseIntegerParam(value);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function badRequest(): Response {
|
|
239
|
+
return new Response("Bad Request", { status: 400 });
|
|
240
|
+
}
|
|
241
|
+
|
|
221
242
|
function forbidden(): Response {
|
|
222
243
|
return new Response("Forbidden", { status: 403 });
|
|
223
244
|
}
|