@aixle/insights 0.2.7-staging → 0.2.9-staging
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 +177 -13
- package/dist/auth/keycloak.js +7 -5
- package/dist/cli.js +2 -2
- package/dist/lib/project-resolver.d.ts +1 -1
- package/dist/readers/claude.d.ts +3 -1
- package/dist/readers/claude.js +7 -2
- package/dist/server.js +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Two channels are published. Pick one deliberately — they are **not** interchan
|
|
|
13
13
|
| | Production | Staging (QA) |
|
|
14
14
|
|---|---|---|
|
|
15
15
|
| Install | `npm i -g @aixle/insights` | `npm i -g @aixle/insights@staging` |
|
|
16
|
-
| Version looks like | `0.2.
|
|
16
|
+
| Version looks like | `0.2.1` | `0.2.6-staging` |
|
|
17
17
|
| Points at | the production API | the staging API |
|
|
18
18
|
| Who should use it | **everyone** | QA validating unreleased work |
|
|
19
19
|
| Stability | released, supported | may change or break without notice |
|
|
@@ -28,7 +28,7 @@ npx -y @aixle/insights init \
|
|
|
28
28
|
|
|
29
29
|
# Or global install:
|
|
30
30
|
npm i -g @aixle/insights
|
|
31
|
-
aixle
|
|
31
|
+
npm ls -g @aixle/insights # e.g. @aixle/insights@0.2.1 (no suffix)
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
### Staging — QA only
|
|
@@ -44,7 +44,7 @@ npx -y @aixle/insights@staging init \
|
|
|
44
44
|
|
|
45
45
|
# Or global install:
|
|
46
46
|
npm i -g @aixle/insights@staging
|
|
47
|
-
aixle
|
|
47
|
+
npm ls -g @aixle/insights # e.g. @aixle/insights@0.2.6-staging (note the suffix)
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
Point a staging build at the **staging** API host. Sending staging telemetry to production
|
|
@@ -53,14 +53,14 @@ pollutes production analytics.
|
|
|
53
53
|
### Which one do I have?
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
aixle
|
|
56
|
+
npm ls -g @aixle/insights # a -staging suffix means a QA build
|
|
57
57
|
npm view @aixle/insights dist-tags # what each channel currently resolves to
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
Expected output — `latest` and `staging` move independently:
|
|
61
61
|
|
|
62
62
|
```
|
|
63
|
-
{ latest: '0.2.
|
|
63
|
+
{ latest: '0.2.1', staging: '0.2.6-staging' }
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
### Switching back to production
|
|
@@ -73,8 +73,8 @@ Then re-run `init` against the production host, since credentials and the MCP en
|
|
|
73
73
|
per-host.
|
|
74
74
|
|
|
75
75
|
> **Why `npm install` never surprises you with a staging build:** `-staging` versions are semver
|
|
76
|
-
> prereleases, and no ordinary version range resolves to a prerelease. `*`, `^0.2.
|
|
77
|
-
> and `>=0.1.0` all select `0.2.
|
|
76
|
+
> prereleases, and no ordinary version range resolves to a prerelease. `*`, `^0.2.1`, `~0.2.1`
|
|
77
|
+
> and `>=0.1.0` all select `0.2.1` even when `0.2.6-staging` exists. Staging builds are
|
|
78
78
|
> reachable only by exact version or the `staging` dist-tag.
|
|
79
79
|
|
|
80
80
|
Maintainers: see [`../RELEASING.md`](../RELEASING.md) for how each channel is cut.
|
|
@@ -137,6 +137,45 @@ Once `init` succeeds it reports the bound org (`Credentials saved (organization
|
|
|
137
137
|
|
|
138
138
|
> **Preferences caveat — this is non-obvious.** The web app's "current org" (the one you appear to be viewing) is normally the **last-used** org, remembered in your browser's `localStorage`. That is **not** the same as your **Default Organization** preference, which is what `init` reads. A multi-org user who has switched orgs in the UI but never explicitly set **Default Organization** in web Preferences has no server-side preference for `init` to use — so `init` will hit the org-selection error above until you either set the Default Organization preference or pass `--organization-id`.
|
|
139
139
|
|
|
140
|
+
## Cursor hooks (optional)
|
|
141
|
+
|
|
142
|
+
**Claude Code needs nothing beyond `init`** — the MCP transcript reader already captures model, token counts (including cache tokens), prompt and assistant text, risk scan, and tool uses.
|
|
143
|
+
|
|
144
|
+
Cursor is different: its local store does not reliably record *which model* answered a turn, so events can land with `model: "unknown"`. Two things address it — the automatic `state.vscdb` fallback (DB90DV-540), and this opt-in hook forwarder for per-turn attribution captured as it happens:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
npx -y @aixle/insights init --hooks --tool-name cursor --host <host> --keycloak-url <realm>
|
|
148
|
+
|
|
149
|
+
aixle-insights verify-hooks # JSON: installed? queue depth?
|
|
150
|
+
aixle-insights uninstall-hooks # remove, restoring the ~/.cursor/hooks.json backup
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Restart Cursor afterwards** — hooks are read at launch. The forwarder appends redacted payloads to `~/.aixle-insights/hooks-queue.ndjson`, which the next sync drains, so events arrive on the normal cycle rather than instantly.
|
|
154
|
+
|
|
155
|
+
> Editor-level hooks (`~/.claude/settings.json` `PostToolUse` / `Stop`) are a **fallback for environments that cannot run an MCP server**, not an upgrade. They carry no model, token, or cost data, so the MCP path is strictly better wherever it is available.
|
|
156
|
+
|
|
157
|
+
## Ingest tokens and rotation
|
|
158
|
+
|
|
159
|
+
`init` mints an ingest token per tool — distinct from your Keycloak login — and stores it in the **OS keychain** (service `aixle-insights`), falling back to `~/.aixle-insights/credentials.json` at mode 0600 where no keychain is available. Tokens are `aixle_<64 hex>` and the server retains only a SHA-256 hash, so a token cannot be recovered after `init`; losing it means re-running `init`.
|
|
160
|
+
|
|
161
|
+
You never need to paste a token for the MCP path — `init` obtains its own.
|
|
162
|
+
|
|
163
|
+
> **Rotation policy: TBD.** There is no self-service rotation command and no documented expiry or cadence. Placeholders until an owner defines them:
|
|
164
|
+
>
|
|
165
|
+
> | | Placeholder |
|
|
166
|
+
> |---|---|
|
|
167
|
+
> | Token lifetime | _undefined — tokens do not self-expire today_ |
|
|
168
|
+
> | Rotation cadence | _TBD_ |
|
|
169
|
+
> | Who can revoke | _TBD — no CLI path; server-side only_ |
|
|
170
|
+
|
|
171
|
+
**If a token is revoked, rotated server-side, or invalidated by a redeploy**, every sync fails with `HTTP 401` while `health` still reports `authenticated: true` — that flag only covers the OIDC login. Clear the stored credential and re-run `init`; state files are preserved, so already-sent sessions stay deduped:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
security delete-generic-password -s "aixle-insights" -a "aixle-insights-ingest-credential" # macOS
|
|
175
|
+
rm -f ~/.aixle-insights/credentials.json
|
|
176
|
+
# then re-run init
|
|
177
|
+
```
|
|
178
|
+
|
|
140
179
|
## First run and backfill
|
|
141
180
|
|
|
142
181
|
Before you run `init`, the MCP server is a deliberate no-op: it does not read, buffer, or send anything, and it does not create any state files. Nothing is lost during this window — Claude Code's transcripts and Cursor's local telemetry stores are unaffected by whether `@aixle/insights` is watching them.
|
|
@@ -145,6 +184,49 @@ The moment `init` succeeds, this package's per-credential state starts from empt
|
|
|
145
184
|
|
|
146
185
|
If the MCP has been installed but never connected, `aixle-insights health` (or the `status` MCP tool) reports `needs_init: true` with a human-readable `onboarding_message` explaining exactly this — install-but-uninitialized is not a state you need to worry about losing data in.
|
|
147
186
|
|
|
187
|
+
Once `init` succeeds it reports the bound org (`Credentials saved (organization <uuid>).`), and `aixle-insights health` (or the `aixle_insights_status` MCP tool) shows the bound `organization_id`.
|
|
188
|
+
|
|
189
|
+
> **Preferences caveat — this is non-obvious.** The web app's "current org" (the one you appear to be viewing) is normally the **last-used** org, remembered in your browser's `localStorage`. That is **not** the same as your **Default Organization** preference, which is what `init` reads. A multi-org user who has switched orgs in the UI but never explicitly set **Default Organization** in web Preferences has no server-side preference for `init` to use — so `init` will hit the org-selection error above until you either set the Default Organization preference or pass `--organization-id`.
|
|
190
|
+
|
|
191
|
+
## Cursor hooks (optional)
|
|
192
|
+
|
|
193
|
+
**Claude Code needs nothing beyond `init`** — the MCP transcript reader already captures model, token counts (including cache tokens), prompt and assistant text, risk scan, and tool uses.
|
|
194
|
+
|
|
195
|
+
Cursor is different: its local store does not reliably record *which model* answered a turn, so events can land with `model: "unknown"`. Two things address it — the automatic `state.vscdb` fallback (DB90DV-540), and this opt-in hook forwarder for per-turn attribution captured as it happens:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
npx -y @aixle/insights init --hooks --tool-name cursor --host <host> --keycloak-url <realm>
|
|
199
|
+
|
|
200
|
+
aixle-insights verify-hooks # JSON: installed? queue depth?
|
|
201
|
+
aixle-insights uninstall-hooks # remove, restoring the ~/.cursor/hooks.json backup
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Restart Cursor afterwards** — hooks are read at launch. The forwarder appends redacted payloads to `~/.aixle-insights/hooks-queue.ndjson`, which the next sync drains, so events arrive on the normal cycle rather than instantly.
|
|
205
|
+
|
|
206
|
+
> Editor-level hooks (`~/.claude/settings.json` `PostToolUse` / `Stop`) are a **fallback for environments that cannot run an MCP server**, not an upgrade. They carry no model, token, or cost data, so the MCP path is strictly better wherever it is available.
|
|
207
|
+
|
|
208
|
+
## Ingest tokens and rotation
|
|
209
|
+
|
|
210
|
+
`init` mints an ingest token per tool — distinct from your Keycloak login — and stores it in the **OS keychain** (service `aixle-insights`), falling back to `~/.aixle-insights/credentials.json` at mode 0600 where no keychain is available. Tokens are `aixle_<64 hex>` and the server retains only a SHA-256 hash, so a token cannot be recovered after `init`; losing it means re-running `init`.
|
|
211
|
+
|
|
212
|
+
You never need to paste a token for the MCP path — `init` obtains its own.
|
|
213
|
+
|
|
214
|
+
> **Rotation policy: TBD.** There is no self-service rotation command and no documented expiry or cadence. Placeholders until an owner defines them:
|
|
215
|
+
>
|
|
216
|
+
> | | Placeholder |
|
|
217
|
+
> |---|---|
|
|
218
|
+
> | Token lifetime | _undefined — tokens do not self-expire today_ |
|
|
219
|
+
> | Rotation cadence | _TBD_ |
|
|
220
|
+
> | Who can revoke | _TBD — no CLI path; server-side only_ |
|
|
221
|
+
|
|
222
|
+
**If a token is revoked, rotated server-side, or invalidated by a redeploy**, every sync fails with `HTTP 401` while `health` still reports `authenticated: true` — that flag only covers the OIDC login. Clear the stored credential and re-run `init`; state files are preserved, so already-sent sessions stay deduped:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
security delete-generic-password -s "aixle-insights" -a "aixle-insights-ingest-credential" # macOS
|
|
226
|
+
rm -f ~/.aixle-insights/credentials.json
|
|
227
|
+
# then re-run init
|
|
228
|
+
```
|
|
229
|
+
|
|
148
230
|
## Commands
|
|
149
231
|
|
|
150
232
|
| Command | What it does |
|
|
@@ -156,7 +238,10 @@ If the MCP has been installed but never connected, `aixle-insights health` (or t
|
|
|
156
238
|
| `aixle-insights init --host <url>` | Use an Aixle Insights API origin for token exchange. Remote hosts must use HTTPS; `http://localhost` and loopback addresses are allowed for local development. |
|
|
157
239
|
| `aixle-insights init --insecure --host http://...` | Allow a remote plaintext HTTP host for a trusted non-production test endpoint. Prints a warning because tokens and telemetry can be exposed. |
|
|
158
240
|
| `aixle-insights init --hooks --tool-name cursor` | Also install the Cursor-side hook forwarder (opt-in; requires Cursor restart). |
|
|
241
|
+
| `aixle-insights init --force` | Re-run `init` even when credentials already exist — re-mints ingest tokens and re-writes the MCP entry. Use after changing host, org, or realm. |
|
|
242
|
+
| `aixle-insights init --tool-name <tool>` | Scope `init` to a single connector. **Omit it** so one login covers both Claude Code and Cursor. |
|
|
159
243
|
| `aixle-insights uninstall-hooks` | Remove the hook forwarder + restore `~/.cursor/hooks.json` backup. |
|
|
244
|
+
| `aixle-insights uninstall-cursor-mcp` | Remove (or restore from backup) only the aixle-insights entry in `~/.cursor/mcp.json`, leaving sibling MCP servers untouched. |
|
|
160
245
|
| `aixle-insights verify-hooks` | Print hooks install status + queue depth as JSON. |
|
|
161
246
|
| `aixle-insights health` | Multi-line diagnostic (credentials, sync, log path, state files). |
|
|
162
247
|
|
|
@@ -166,9 +251,14 @@ If the MCP has been installed but never connected, `aixle-insights health` (or t
|
|
|
166
251
|
|---|---|
|
|
167
252
|
| `AIXLE_INSIGHTS_API_URL` (deprecated: `DB90_API_URL`) | API origin for ingestion + MCP exchange (defaults to `http://localhost:3000`; `init --host` overrides). |
|
|
168
253
|
| `KEYCLOAK_ISSUER` / `AIXLE_INSIGHTS_KEYCLOAK_ISSUER` (deprecated: `DB90_KEYCLOAK_ISSUER`) | Realm issuer URLs (`KEYCLOAK_ISSUER` is preferred on servers + CI). |
|
|
169
|
-
| `AIXLE_INSIGHTS_KEYCLOAK_CLIENT_ID` (deprecated: `DB90_KEYCLOAK_CLIENT_ID`) | Defaults to `
|
|
254
|
+
| `KEYCLOAK_CLIENT_ID` / `AIXLE_INSIGHTS_KEYCLOAK_CLIENT_ID` (deprecated: `DB90_KEYCLOAK_CLIENT_ID`) | Defaults to `aixle-insights-web`; must allow device authorization in Keycloak. |
|
|
170
255
|
| `AIXLE_INSIGHTS_ORGANIZATION_ID` (deprecated: `DB90_ORGANIZATION_ID`) | Optional UUID scoping `init` to that org membership (header `X-Organization-ID`). |
|
|
171
256
|
| `AIXLE_INSIGHTS_HOME` | Override the local state directory (defaults to `~/.aixle-insights/`). |
|
|
257
|
+
| `AIXLE_INSIGHTS_MCP_DISABLE_KEYTAR` (deprecated: `DB90_MCP_DISABLE_KEYTAR`) | Set to skip the OS keychain entirely and use the `credentials.json` fallback. Useful on headless Linux/CI/Docker where Secret Service is absent. |
|
|
258
|
+
| `AIXLE_INSIGHTS_MCP_USE_LOCAL_KEYCLOAK_DEFAULT` (deprecated: `DB90_MCP_USE_LOCAL_KEYCLOAK_DEFAULT`) | Default the Keycloak issuer to the local stack (`http://localhost:8080/realms/db90`) so `--keycloak-url` can be omitted during local development. |
|
|
259
|
+
| `AIXLE_INSIGHTS_CLAUDE_USER_CONFIG_PATH` (deprecated: `DB90_CLAUDE_USER_CONFIG_PATH`) | Override the path to `~/.claude.json` that `init` merges the MCP entry into. Primarily a test seam; also lets you target a non-default Claude Code profile. |
|
|
260
|
+
|
|
261
|
+
Also read, but not intended as user-facing configuration: `APPDATA` and `XDG_CONFIG_HOME` (platform config-directory discovery) and `NODE_ENV`.
|
|
172
262
|
|
|
173
263
|
Remote API and ingest hosts must use `https://`. Plaintext `http://` is local-dev-only for `localhost`, `127.0.0.0/8`, and `[::1]`; those loopback URLs work without warnings. A remote `http://` host is rejected during `init` unless you pass `--insecure`, which should only be used for trusted non-production test endpoints and will print a warning because ingest tokens and telemetry can cross the network unencrypted.
|
|
174
264
|
|
|
@@ -186,11 +276,81 @@ Optional `~/.aixle-insights/config.json` accepts Cursor line-cost overrides (per
|
|
|
186
276
|
}
|
|
187
277
|
```
|
|
188
278
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
`
|
|
192
|
-
|
|
193
|
-
|
|
279
|
+
### Configuration reference
|
|
280
|
+
|
|
281
|
+
`~/.aixle-insights/config.json` is optional — `init` does not create it. Every key is optional too.
|
|
282
|
+
|
|
283
|
+
| Key | Type | Purpose |
|
|
284
|
+
|---|---|---|
|
|
285
|
+
| `host` | string | Ingest API origin. Same value as `--host`. |
|
|
286
|
+
| `token` | string | Ingest token. Rarely needed — `init` stores tokens in the keychain instead, and this is the plaintext alternative. |
|
|
287
|
+
| `project_id` | string | Pin every event to one project instead of resolving from the git remote. |
|
|
288
|
+
| `cursor.line_costs.<model>` | `{ input_per_line, output_per_line }` | Per-model cost rates for Cursor line-based accounting. |
|
|
289
|
+
|
|
290
|
+
**Precedence:** CLI flag → environment variable → `config.json`. A flag always wins; the config file is the fallback of last resort. An absent `config.json` is the normal case and is silent. A file that is present but unusable — malformed JSON, or valid JSON that isn't an object, including a **top-level array** (a common mistake when writing per-model rates) — is ignored entirely: every override falls back to its default, and a `config_parse_failed` line is written to `mcp.log` rather than a crash. Nothing is printed to the terminal, so check the log if an override appears to have no effect.
|
|
291
|
+
|
|
292
|
+
Worked example — pin a project and override Cursor rates for two models:
|
|
293
|
+
|
|
294
|
+
```json
|
|
295
|
+
{
|
|
296
|
+
"host": "https://staging.insights.aixle.com",
|
|
297
|
+
"project_id": "3f6c1e28-9b4a-4c7f-8d21-5ac0e7b91f04",
|
|
298
|
+
"cursor": {
|
|
299
|
+
"line_costs": {
|
|
300
|
+
"claude-sonnet-4-5": { "input_per_line": 0.0002, "output_per_line": 0.0008 },
|
|
301
|
+
"gpt-4o": { "input_per_line": 0.0001, "output_per_line": 0.0004 }
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Clean slate — full uninstall / reset
|
|
308
|
+
|
|
309
|
+
Use this when a breaking change lands, when switching between the production and staging channels, or when you want to prove a problem is not stale local state. Steps are ordered least to most destructive; stop wherever your problem clears.
|
|
310
|
+
|
|
311
|
+
**1. Remove editor integration**
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
aixle-insights uninstall-hooks # Cursor hook forwarder + restore ~/.cursor/hooks.json backup
|
|
315
|
+
aixle-insights uninstall-cursor-mcp # remove only our entry from ~/.cursor/mcp.json
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Then delete the `mcpServers.aixle-insights` entry from `~/.claude.json` by hand — `init` writes it and there is no command to remove it.
|
|
319
|
+
|
|
320
|
+
**2. Clear credentials** — forces a fresh `init` and re-mints ingest tokens:
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
security delete-generic-password -s "aixle-insights" -a "aixle-insights-ingest-credential" # macOS
|
|
324
|
+
rm -f ~/.aixle-insights/credentials.json
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**3. Reset sync state** — ⚠️ **this is the destructive one.** State files hold the watermarks and dedupe checkpoints. Deleting them makes the next sync treat all local history as new, so you get a **full re-backfill**. Ingest upserts by `metadata.session_id`, so you should get updates rather than duplicates, but volume will spike:
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
rm -f ~/.aixle-insights/state-*.json
|
|
331
|
+
rm -f ~/.aixle-insights/state.lock # only if no `aixle-insights run` is alive — check with: pgrep -fa aixle-insights
|
|
332
|
+
rm -f ~/.aixle-insights/hooks-queue.ndjson # discards hook events not yet drained
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**4. Nuclear** — remove everything the package created locally:
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
rm -rf ~/.aixle-insights/ # credentials, state, logs, hook queue
|
|
339
|
+
npm uninstall -g @aixle/insights
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Nothing here touches your Claude Code transcripts or Cursor's own store — those belong to the editors and are only ever read. Events already delivered to the server are unaffected; this is purely local state.
|
|
343
|
+
|
|
344
|
+
| Artifact | Created by | Removed in step |
|
|
345
|
+
|---|---|---|
|
|
346
|
+
| `~/.aixle-insights/credentials.json` / keychain entry | `init` | 2 |
|
|
347
|
+
| `~/.aixle-insights/state-<host>-<hash>.json` | first sync | 3 |
|
|
348
|
+
| `~/.aixle-insights/state.lock` | `run` | 3 |
|
|
349
|
+
| `~/.aixle-insights/hooks-queue.ndjson` | hook forwarder | 3 |
|
|
350
|
+
| `~/.aixle-insights/mcp.log`, `mcp.log.1` | any run | 4 |
|
|
351
|
+
| `~/.cursor/hooks.json` entry | `init --hooks` | 1 |
|
|
352
|
+
| `~/.cursor/mcp.json` entry | `init` | 1 |
|
|
353
|
+
| `mcpServers.aixle-insights` in `~/.claude.json` | `init` | 1 (manual) |
|
|
194
354
|
|
|
195
355
|
## Security
|
|
196
356
|
|
|
@@ -362,6 +522,10 @@ cd packages/tools
|
|
|
362
522
|
npm rebuild better-sqlite3
|
|
363
523
|
```
|
|
364
524
|
|
|
525
|
+
## Changelog
|
|
526
|
+
|
|
527
|
+
See [CHANGELOG.md](https://github.com/dualboot-partners/db90-rails/blob/develop/packages/tools/aixle-insights/CHANGELOG.md) — npm's registry page doesn't render this file directly, so it's linked here instead of duplicated.
|
|
528
|
+
|
|
365
529
|
## License
|
|
366
530
|
|
|
367
531
|
MIT.
|
package/dist/auth/keycloak.js
CHANGED
|
@@ -193,9 +193,11 @@ export function defaultKeycloakIssuer(ingestHost) {
|
|
|
193
193
|
return "";
|
|
194
194
|
}
|
|
195
195
|
export function defaultKeycloakClientId() {
|
|
196
|
-
return (
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
196
|
+
return (process.env["KEYCLOAK_CLIENT_ID"]?.trim() ||
|
|
197
|
+
readEnvWithDeprecatedAlias({
|
|
198
|
+
current: "AIXLE_INSIGHTS_KEYCLOAK_CLIENT_ID",
|
|
199
|
+
deprecated: "DB90_KEYCLOAK_CLIENT_ID",
|
|
200
|
+
onDeprecatedUse: warnDeprecatedEnvVar,
|
|
201
|
+
}) ||
|
|
202
|
+
"aixle-insights-web");
|
|
201
203
|
}
|
package/dist/cli.js
CHANGED
|
@@ -238,7 +238,7 @@ export async function runInit(cliArgs, deps) {
|
|
|
238
238
|
}
|
|
239
239
|
const kcIssuer = (cliArgs.keycloakUrl ?? runtime.defaultKeycloakIssuer(apiHost)).trim();
|
|
240
240
|
if (!kcIssuer) {
|
|
241
|
-
runtime.error("Error: Keycloak issuer is not configured. Pass --keycloak-url or set KEYCLOAK_ISSUER / AIXLE_INSIGHTS_KEYCLOAK_ISSUER
|
|
241
|
+
runtime.error("Error: Keycloak issuer is not configured. Pass --keycloak-url or set KEYCLOAK_ISSUER / AIXLE_INSIGHTS_KEYCLOAK_ISSUER.");
|
|
242
242
|
return 1;
|
|
243
243
|
}
|
|
244
244
|
if (cliArgs.toolName !== undefined && !["claude_code", "cursor"].includes(cliArgs.toolName)) {
|
|
@@ -258,7 +258,7 @@ export async function runInit(cliArgs, deps) {
|
|
|
258
258
|
});
|
|
259
259
|
const exchangeOrganizationId = fromFlag || fromEnv;
|
|
260
260
|
if (exchangeOrganizationId && !isValidOrganizationUuid(exchangeOrganizationId)) {
|
|
261
|
-
runtime.error("Error: --organization-id / AIXLE_INSIGHTS_ORGANIZATION_ID
|
|
261
|
+
runtime.error("Error: --organization-id / AIXLE_INSIGHTS_ORGANIZATION_ID must be a valid UUID (RFC 4122, version 1–5, variant per Aixle Insights API).");
|
|
262
262
|
return 1;
|
|
263
263
|
}
|
|
264
264
|
const result = await runtime.loginAndPersistCredentials({
|
|
@@ -33,7 +33,7 @@ export declare function canonicalizeGitRemote(remote: string, verbose: boolean):
|
|
|
33
33
|
*/
|
|
34
34
|
export declare function repoNameToGitRemoteCandidates(repoName: string): string[];
|
|
35
35
|
export declare function lookupProjectByRepoName(repoName: string, host: string, token: string, verbose: boolean, allowInsecureHttp?: boolean): Promise<LookupResult | "not-found" | null>;
|
|
36
|
-
/** Payload shape shared by
|
|
36
|
+
/** Payload shape shared by the Cursor and Claude commit mappers. */
|
|
37
37
|
export interface CommitAttributionPayload {
|
|
38
38
|
event_type?: string;
|
|
39
39
|
project_id?: string;
|
package/dist/readers/claude.d.ts
CHANGED
|
@@ -59,7 +59,6 @@ export interface ClaudeTranscriptTurn {
|
|
|
59
59
|
toolUses: ClaudeCollectedToolUse[];
|
|
60
60
|
navToolCalls: number;
|
|
61
61
|
totalToolCalls: number;
|
|
62
|
-
messageIds: string[];
|
|
63
62
|
/**
|
|
64
63
|
* Fingerprint of the turn's content (prompt + assistant text + tool-use set).
|
|
65
64
|
* A turn keeps the same turnId as Claude appends more tool_use blocks to it,
|
|
@@ -67,6 +66,8 @@ export interface ClaudeTranscriptTurn {
|
|
|
67
66
|
* instead of skipping the turn forever on its unchanged id (DB90DV-259).
|
|
68
67
|
*/
|
|
69
68
|
contentHash: string;
|
|
69
|
+
messageIds: string[];
|
|
70
|
+
requestIds: string[];
|
|
70
71
|
}
|
|
71
72
|
/** Payload shape for the parent chat turn (carries full token cost). */
|
|
72
73
|
export interface ClaudePayload extends IngestPayload {
|
|
@@ -99,6 +100,7 @@ export interface ClaudePayload extends IngestPayload {
|
|
|
99
100
|
nav_tool_calls: number;
|
|
100
101
|
total_tool_calls: number;
|
|
101
102
|
message_ids?: string[];
|
|
103
|
+
request_ids?: string[];
|
|
102
104
|
};
|
|
103
105
|
}
|
|
104
106
|
/** Payload shape for derivative tool-use children (cost_usd: 0, no tokens). */
|
package/dist/readers/claude.js
CHANGED
|
@@ -206,8 +206,9 @@ function newTurn(sessionId, turnIndex, filePath, fileSize, occurredAt, promptId)
|
|
|
206
206
|
toolUses: [],
|
|
207
207
|
navToolCalls: 0,
|
|
208
208
|
totalToolCalls: 0,
|
|
209
|
-
messageIds: [],
|
|
210
209
|
contentHash: "",
|
|
210
|
+
messageIds: [],
|
|
211
|
+
requestIds: [],
|
|
211
212
|
persisted: false,
|
|
212
213
|
};
|
|
213
214
|
}
|
|
@@ -227,7 +228,7 @@ function computeTurnContentHash(turn) {
|
|
|
227
228
|
turn.promptText,
|
|
228
229
|
turn.assistantText,
|
|
229
230
|
toolFingerprint,
|
|
230
|
-
].join("
|
|
231
|
+
].join(" ");
|
|
231
232
|
return createHash("sha256").update(material).digest("hex").slice(0, 32);
|
|
232
233
|
}
|
|
233
234
|
function appendText(existing, addition) {
|
|
@@ -394,6 +395,9 @@ export async function parseTranscriptFile(filePath, verbose = false) {
|
|
|
394
395
|
if (entry.message.id && !currentTurn.messageIds.includes(entry.message.id)) {
|
|
395
396
|
currentTurn.messageIds.push(entry.message.id);
|
|
396
397
|
}
|
|
398
|
+
if (entry.requestId && !currentTurn.requestIds.includes(entry.requestId)) {
|
|
399
|
+
currentTurn.requestIds.push(entry.requestId);
|
|
400
|
+
}
|
|
397
401
|
currentTurn.occurredAt = timestamp > currentTurn.occurredAt ? timestamp : currentTurn.occurredAt;
|
|
398
402
|
const text = extractContentText(entry.message.content).join("\n\n").trim();
|
|
399
403
|
currentTurn.assistantText = appendText(currentTurn.assistantText, text);
|
|
@@ -449,6 +453,7 @@ export function mapTranscriptTurn(turn, options) {
|
|
|
449
453
|
nav_tool_calls: turn.navToolCalls,
|
|
450
454
|
total_tool_calls: turn.totalToolCalls,
|
|
451
455
|
message_ids: turn.messageIds.length > 0 ? turn.messageIds : undefined,
|
|
456
|
+
request_ids: turn.requestIds.length > 0 ? turn.requestIds : undefined,
|
|
452
457
|
},
|
|
453
458
|
};
|
|
454
459
|
if (turn.model)
|
package/dist/server.js
CHANGED
|
@@ -160,7 +160,7 @@ async function authenticateHandler(args) {
|
|
|
160
160
|
if (!kc) {
|
|
161
161
|
return jsonContent({
|
|
162
162
|
ok: false,
|
|
163
|
-
error: "keycloakUrl or KEYCLOAK_ISSUER / AIXLE_INSIGHTS_KEYCLOAK_ISSUER
|
|
163
|
+
error: "keycloakUrl or KEYCLOAK_ISSUER / AIXLE_INSIGHTS_KEYCLOAK_ISSUER is required",
|
|
164
164
|
});
|
|
165
165
|
}
|
|
166
166
|
const clientId = args.clientId?.trim() || defaultKeycloakClientId();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aixle/insights",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9-staging",
|
|
4
4
|
"description": "stdio MCP server for AI coding-assistant telemetry — Claude transcript sync + Cursor SQLite ingest.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"lint": "eslint . --max-warnings 0",
|
|
47
47
|
"verify:cursor-dry-run": "tsx scripts/verify-cursor-dry-run.ts",
|
|
48
48
|
"audit:local-stores": "tsx scripts/audit-local-stores.ts",
|
|
49
|
+
"nightly:resolve": "tsx scripts/nightly-release-resolve.ts",
|
|
49
50
|
"dev": "tsx src/cli.ts",
|
|
50
51
|
"prepublishOnly": "npm run build"
|
|
51
52
|
},
|