@gajae-code/stats 0.18.0 → 0.18.5
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 +77 -5
- 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
|
|
|
@@ -85,6 +94,69 @@ The web dashboard provides:
|
|
|
85
94
|
- Per-folder breakdown table
|
|
86
95
|
- Auto-refresh every 30 seconds
|
|
87
96
|
|
|
97
|
+
## Tips & Common Patterns
|
|
98
|
+
|
|
99
|
+
### Sync and print a summary
|
|
100
|
+
|
|
101
|
+
Every `gjc stats` invocation syncs session files before it does anything else, so there is no separate sync flag.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
gjc stats --summary
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### JSON output for scripting
|
|
108
|
+
|
|
109
|
+
With `--json`, stdout carries only the JSON document; the sync progress and the `Synced N new entries ...` summary go to stderr.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
gjc stats --json | jq '.overall.totalCost'
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Dashboard on a custom port
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
gjc stats --port 3000
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`--summary` and `--json` print and exit without starting the server, so `--port` has no effect when combined with them.
|
|
122
|
+
|
|
123
|
+
### Programmatic: highest-cost folder
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { getDashboardStats, syncAllSessions } from "@gajae-code/stats";
|
|
127
|
+
|
|
128
|
+
await syncAllSessions();
|
|
129
|
+
const stats = await getDashboardStats();
|
|
130
|
+
|
|
131
|
+
const [first, ...rest] = stats.byFolder;
|
|
132
|
+
if (first) {
|
|
133
|
+
const topFolder = rest.reduce((a, b) => (b.totalCost > a.totalCost ? b : a), first);
|
|
134
|
+
console.log(`Highest cost folder: ${topFolder.folder} ($${topFolder.totalCost.toFixed(2)})`);
|
|
135
|
+
} else {
|
|
136
|
+
console.log("No folder data available");
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Programmatic: most-requested model
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { getDashboardStats, syncAllSessions } from "@gajae-code/stats";
|
|
144
|
+
|
|
145
|
+
await syncAllSessions();
|
|
146
|
+
const stats = await getDashboardStats();
|
|
147
|
+
|
|
148
|
+
// byModel is ordered by request count, highest first.
|
|
149
|
+
const topModel = stats.byModel[0];
|
|
150
|
+
if (topModel) {
|
|
151
|
+
console.log(`Most requested model: ${topModel.model} (${topModel.totalRequests} requests)`);
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Troubleshooting
|
|
156
|
+
|
|
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.
|
|
158
|
+
- **Dashboard not starting?** Check that port 3847 (or the port passed to `--port`) is free.
|
|
159
|
+
|
|
88
160
|
## License
|
|
89
161
|
|
|
90
162
|
MIT
|
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.5",
|
|
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.5",
|
|
42
|
+
"@gajae-code/utils": "0.18.5",
|
|
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
|
}
|