@aixle/insights 0.1.1 → 0.2.1-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 +154 -3
- package/dist/auth/credentials.d.ts +7 -1
- package/dist/auth/credentials.js +71 -14
- package/dist/auth/exchange.d.ts +1 -1
- package/dist/auth/exchange.js +1 -1
- package/dist/auth/flow.d.ts +10 -1
- package/dist/auth/flow.js +38 -5
- package/dist/auth/keycloak.d.ts +1 -1
- package/dist/auth/keycloak.js +20 -1
- package/dist/cli.d.ts +7 -3
- package/dist/cli.js +87 -21
- package/dist/collect-cursor-payloads.d.ts +4 -3
- package/dist/collect-cursor-payloads.js +8 -5
- package/dist/cursor-checkpoints.d.ts +2 -2
- package/dist/cursor-payload-contract.d.ts +5 -5
- package/dist/cursor-payload-contract.js +7 -0
- package/dist/cursor-settings.d.ts +9 -4
- package/dist/cursor-settings.js +80 -10
- package/dist/cursor-store-audit.d.ts +2 -2
- package/dist/cursor-store-audit.js +22 -11
- package/dist/daily-stats-versions.d.ts +3 -1
- package/dist/daily-stats-versions.js +6 -7
- package/dist/health.d.ts +3 -1
- package/dist/health.js +13 -1
- package/dist/hooks/cursor-hooks-mapper.d.ts +3 -3
- package/dist/hooks/cursor-hooks-mapper.js +1 -1
- package/dist/hooks/cursor-hooks-reader.d.ts +2 -0
- package/dist/hooks/cursor-hooks-reader.js +2 -2
- package/dist/install/cursor.d.ts +34 -0
- package/dist/install/cursor.js +193 -0
- package/dist/install/index.d.ts +6 -4
- package/dist/install/index.js +6 -1
- package/dist/lib/client.d.ts +7 -0
- package/dist/lib/client.js +17 -0
- package/dist/lib/config.js +7 -2
- package/dist/lib/project-resolver.d.ts +5 -4
- package/dist/lib/project-resolver.js +20 -8
- package/dist/lib/transport-security.d.ts +13 -0
- package/dist/lib/transport-security.js +47 -0
- package/dist/pricing.d.ts +9 -1
- package/dist/pricing.js +39 -8
- package/dist/readers/claude.d.ts +54 -6
- package/dist/readers/claude.js +158 -6
- package/dist/readers/cursor-sqlite.d.ts +23 -0
- package/dist/readers/cursor-sqlite.js +68 -0
- package/dist/readers/cursor.d.ts +11 -8
- package/dist/readers/cursor.js +149 -31
- package/dist/server.d.ts +20 -3
- package/dist/server.js +101 -67
- package/dist/state.js +7 -2
- package/dist/sync.d.ts +4 -2
- package/dist/sync.js +61 -46
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,6 +2,83 @@
|
|
|
2
2
|
|
|
3
3
|
stdio **MCP server** for AI coding-assistant telemetry. Ingests Claude Code JSONL transcripts and Cursor IDE SQLite telemetry into your organization's ingest API, exposes operator tools on the MCP bridge, and pairs with `aixle-insights init` so teammates can onboard without juggling cron jobs or brittle shell hooks.
|
|
4
4
|
|
|
5
|
+
## Architecture reference
|
|
6
|
+
|
|
7
|
+
For implementation architecture, design decisions, and package direction, see [`ARD.md`](./ARD.md).
|
|
8
|
+
|
|
9
|
+
## Choosing a version
|
|
10
|
+
|
|
11
|
+
Two channels are published. Pick one deliberately — they are **not** interchangeable.
|
|
12
|
+
|
|
13
|
+
| | Production | Staging (QA) |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| Install | `npm i -g @aixle/insights` | `npm i -g @aixle/insights@staging` |
|
|
16
|
+
| Version looks like | `0.2.0` | `0.2.1-staging` |
|
|
17
|
+
| Points at | the production API | the staging API |
|
|
18
|
+
| Who should use it | **everyone** | QA validating unreleased work |
|
|
19
|
+
| Stability | released, supported | may change or break without notice |
|
|
20
|
+
|
|
21
|
+
### Production — use this unless told otherwise
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# One-shot via npx (recommended — always pulls the current release):
|
|
25
|
+
npx -y @aixle/insights init \
|
|
26
|
+
--host https://insights.aixle.com \
|
|
27
|
+
--keycloak-url https://YOUR-KEYCLOAK/realms/YOUR_REALM
|
|
28
|
+
|
|
29
|
+
# Or global install:
|
|
30
|
+
npm i -g @aixle/insights
|
|
31
|
+
aixle-insights --version # e.g. 0.2.0 (no suffix)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Staging — QA only
|
|
35
|
+
|
|
36
|
+
Staging builds carry a `-staging` suffix and live on the `staging` dist-tag. You must ask for
|
|
37
|
+
them explicitly; a plain `npm install` will never give you one.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# One-shot via npx:
|
|
41
|
+
npx -y @aixle/insights@staging init \
|
|
42
|
+
--host https://staging.insights.aixle.com \
|
|
43
|
+
--keycloak-url https://YOUR-STAGING-KEYCLOAK/realms/YOUR_REALM
|
|
44
|
+
|
|
45
|
+
# Or global install:
|
|
46
|
+
npm i -g @aixle/insights@staging
|
|
47
|
+
aixle-insights --version # e.g. 0.2.1-staging (note the suffix)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Point a staging build at the **staging** API host. Sending staging telemetry to production
|
|
51
|
+
pollutes production analytics.
|
|
52
|
+
|
|
53
|
+
### Which one do I have?
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
aixle-insights --version # a -staging suffix means a QA build
|
|
57
|
+
npm view @aixle/insights dist-tags # what each channel currently resolves to
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Expected output — `latest` and `staging` move independently:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
{ latest: '0.2.0', staging: '0.2.1-staging' }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Switching back to production
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm i -g @aixle/insights@latest
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Then re-run `init` against the production host, since credentials and the MCP entry are
|
|
73
|
+
per-host.
|
|
74
|
+
|
|
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.0`, `~0.2.0`
|
|
77
|
+
> and `>=0.1.0` all select `0.2.0` even when `0.2.1-staging` exists. Staging builds are
|
|
78
|
+
> reachable only by exact version or the `staging` dist-tag.
|
|
79
|
+
|
|
80
|
+
Maintainers: see [`../RELEASING.md`](../RELEASING.md) for how each channel is cut.
|
|
81
|
+
|
|
5
82
|
## Install
|
|
6
83
|
|
|
7
84
|
```bash
|
|
@@ -21,7 +98,7 @@ After `init` succeeds:
|
|
|
21
98
|
|
|
22
99
|
1. **Restart Claude Code** — it discovers the new MCP server on the next launch.
|
|
23
100
|
2. Open Claude Code; confirm `/mcp` lists **aixle-insights**.
|
|
24
|
-
3. During a Claude session invoke the **`
|
|
101
|
+
3. During a Claude session invoke the **`aixle_insights_status`** MCP tool (or `aixle-insights health` from the shell) to see connectivity + last sync metadata.
|
|
25
102
|
|
|
26
103
|
## Multi-org
|
|
27
104
|
|
|
@@ -36,6 +113,14 @@ npx -y @aixle/insights init \
|
|
|
36
113
|
|
|
37
114
|
You can also set `DB90_ORGANIZATION_ID=<uuid>` in your shell environment, or pin it via `mcpServers.aixle-insights.env` in `~/.claude.json`. The CLI flag overrides the env var when both are set.
|
|
38
115
|
|
|
116
|
+
## First run and backfill
|
|
117
|
+
|
|
118
|
+
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.
|
|
119
|
+
|
|
120
|
+
The moment `init` succeeds, this package's per-credential state starts from empty (no watermark, no dedupe checkpoint). That means **the very first sync after `init` treats "everything currently on disk" as new** — Claude transcript JSONL files and Cursor SQLite history alike — and sends all of it, not just activity from that point forward. There is no separate "backfill mode" to opt into; it is simply what an empty watermark means the first time `run --once` or the background sync loop executes.
|
|
121
|
+
|
|
122
|
+
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.
|
|
123
|
+
|
|
39
124
|
## Commands
|
|
40
125
|
|
|
41
126
|
| Command | What it does |
|
|
@@ -44,6 +129,8 @@ You can also set `DB90_ORGANIZATION_ID=<uuid>` in your shell environment, or pin
|
|
|
44
129
|
| `aixle-insights run --once` | Perform one multi-tool sync, exit. Useful for cron / manual flushes. |
|
|
45
130
|
| `aixle-insights run --once --full` | Backfill: ignore Cursor watermarks and commit-hash dedupe. |
|
|
46
131
|
| `aixle-insights init` | Keycloak device login + persist credentials + merge `~/.claude.json` entry. |
|
|
132
|
+
| `aixle-insights init --host <url>` | Use a DB90 API origin for token exchange. Remote hosts must use HTTPS; `http://localhost` and loopback addresses are allowed for local development. |
|
|
133
|
+
| `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. |
|
|
47
134
|
| `aixle-insights init --hooks --tool-name cursor` | Also install the Cursor-side hook forwarder (opt-in; requires Cursor restart). |
|
|
48
135
|
| `aixle-insights uninstall-hooks` | Remove the hook forwarder + restore `~/.cursor/hooks.json` backup. |
|
|
49
136
|
| `aixle-insights verify-hooks` | Print hooks install status + queue depth as JSON. |
|
|
@@ -59,6 +146,8 @@ You can also set `DB90_ORGANIZATION_ID=<uuid>` in your shell environment, or pin
|
|
|
59
146
|
| `DB90_ORGANIZATION_ID` | Optional UUID scoping `init` to that org membership (header `X-Organization-ID`). |
|
|
60
147
|
| `AIXLE_INSIGHTS_HOME` | Override the local state directory (defaults to `~/.aixle-insights/`). |
|
|
61
148
|
|
|
149
|
+
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.
|
|
150
|
+
|
|
62
151
|
Note: the `DB90_*` variables above are retained as compatibility names for the deployment-side Keycloak realm/client identifiers. Future versions may rename them with a deprecation window.
|
|
63
152
|
|
|
64
153
|
Optional `~/.aixle-insights/config.json` accepts Cursor line-cost overrides (per-model rates):
|
|
@@ -73,6 +162,32 @@ Optional `~/.aixle-insights/config.json` accepts Cursor line-cost overrides (per
|
|
|
73
162
|
}
|
|
74
163
|
```
|
|
75
164
|
|
|
165
|
+
## Security
|
|
166
|
+
|
|
167
|
+
`@aixle/insights` enforces HTTPS for any remote host. Plaintext `http://` is allowed only for loopback (`localhost`, `127.0.0.0/8`, `[::1]`) so that local-dev flows against `make up` continue to work without friction.
|
|
168
|
+
|
|
169
|
+
### Three gates
|
|
170
|
+
|
|
171
|
+
| Gate | Where it fires | What it checks |
|
|
172
|
+
|---|---|---|
|
|
173
|
+
| CLI `--host` gate | `runInit()` at the top of `aixle-insights init`, before any network call to Keycloak | The `--host` value the user typed |
|
|
174
|
+
| Post-exchange `ingestHost` gate | `auth/flow.ts`, immediately after the OIDC-for-ingest-token exchange returns, before persisting credentials to the keychain | The `ingestHost` returned by the server, in case it differs from `--host` |
|
|
175
|
+
| Runtime send/lookup gate | `lib/client.ts`'s `postEvent` and `lib/project-resolver.ts`'s `lookupProjectByRemote`, immediately before every ingest POST and project-attribution GET | The `host` loaded from stored credentials, on **every** sync cycle — not just at `init` |
|
|
176
|
+
|
|
177
|
+
All three gates use the same pure utility, `evaluateTransportSecurity()` in `src/lib/transport-security.ts`. The first two rejecting aborts `init` with exit code 1 and a single-line error naming the offending host. The third rejecting drops that send/lookup (logged via `console.error`, no retry) without aborting the whole sync cycle — a single tampered credential shouldn't crash background sync, it should just refuse to leak the token.
|
|
178
|
+
|
|
179
|
+
The runtime gate exists because `init`'s two gates only run once, at login time. If `~/.aixle-insights/credentials.json` is edited afterward (by hand, by malware, or by disk corruption) to point at a plaintext `http://` remote, nothing previously re-checked the scheme before every subsequent sync sent the bearer token — DB90DV-539 closed that gap.
|
|
180
|
+
|
|
181
|
+
### `--insecure` (init-only, consent persists to runtime)
|
|
182
|
+
|
|
183
|
+
`aixle-insights init --insecure --host http://<remote>` downgrades the first two gates from "reject" to "warn + continue." It is intended only for trusted non-production test endpoints (e.g. a self-hosted staging on a private network without a TLS cert).
|
|
184
|
+
|
|
185
|
+
`--insecure` is rejected on the `run` subcommand by design — the long-running MCP server should never run insecurely, and since `run` is normally spawned non-interactively (by Claude Code, via `~/.claude.json`), there is no ergonomic way to pass a flag to it per-cycle anyway. Instead, when `init --insecure` is used, that consent is recorded as `insecureHttpAllowed: true` on the stored credential (`StoredCredentials.insecureHttpAllowed` in `auth/credentials.ts`) and every later `run` reads it back — so a legitimately-approved trusted HTTP endpoint keeps syncing normally. A `credentials.json` that has an `http://` remote host **without** this flag set (e.g. because someone hand-edited the file after the fact, bypassing `init` entirely) is rejected by the runtime gate on every send.
|
|
186
|
+
|
|
187
|
+
### What is NOT gated
|
|
188
|
+
|
|
189
|
+
The Keycloak issuer URL (`--keycloak-url` / `KEYCLOAK_ISSUER`) is **not** TLS-gated by this package. Same threat model, different ticket — tracked separately. For now, use HTTPS for any remote Keycloak issuer; the OIDC device-flow library will fail the request if the cert is invalid, but it will not refuse to attempt plaintext.
|
|
190
|
+
|
|
76
191
|
## Cursor hook forwarder (opt-in)
|
|
77
192
|
|
|
78
193
|
`aixle-insights init --hooks --tool-name cursor` installs a Node script as a Cursor hook (`~/.cursor/hooks.json`). The script appends redacted hook payloads to `~/.aixle-insights/hooks-queue.ndjson`; the background sync drains the queue on its next cycle and POSTs the events with accurate per-turn model attribution. Requires a Cursor restart after install. To remove, run `aixle-insights uninstall-hooks` and restart Cursor again.
|
|
@@ -80,9 +195,25 @@ Optional `~/.aixle-insights/config.json` accepts Cursor line-cost overrides (per
|
|
|
80
195
|
## State + credentials
|
|
81
196
|
|
|
82
197
|
- **App home directory**: `~/.aixle-insights/` (override with `AIXLE_INSIGHTS_HOME`).
|
|
83
|
-
- **Credentials**: OS keychain
|
|
198
|
+
- **Credentials**: the OS keychain is the source of truth. On read, the keychain is consulted **first**; the file is only a fallback when the keychain is unavailable, empty, or explicitly disabled (`DB90_MCP_DISABLE_KEYTAR`). On write, credentials go to the keychain and the file is removed when the keychain write succeeds.
|
|
84
199
|
- **State files**: `state-<hostname>-<token-hash>.json` per credential, plus `state.lock` advisory lock, `mcp.log` rotating diagnostic log, optional `hooks-queue.ndjson`.
|
|
85
200
|
|
|
201
|
+
### Credential storage by OS
|
|
202
|
+
|
|
203
|
+
The secure store is the platform's native keychain, accessed via `keytar` (service `aixle-insights`):
|
|
204
|
+
|
|
205
|
+
| OS | Secure store | Availability | Fallback file protection |
|
|
206
|
+
| --- | --- | --- | --- |
|
|
207
|
+
| **macOS** | Keychain | reliably present | `credentials.json` written `chmod 0600` |
|
|
208
|
+
| **Windows** | Credential Manager | reliably present | `credentials.json` best-effort locked via `icacls` (Node `chmod` cannot set NTFS ACLs) |
|
|
209
|
+
| **Linux** | Secret Service (libsecret / GNOME Keyring / KWallet) | **often absent** on headless servers, minimal Docker images, and CI — no D-Bus secret service | `credentials.json` written `chmod 0600` |
|
|
210
|
+
|
|
211
|
+
Notes:
|
|
212
|
+
|
|
213
|
+
- **Windows**: don't set `DB90_MCP_DISABLE_KEYTAR` — Credential Manager is reliably present, and the plaintext fallback file cannot be locked down as tightly as the keychain. The `icacls` hardening is best-effort defense-in-depth.
|
|
214
|
+
- **Linux**: when no Secret Service is running (common in headless/CI/container contexts), the tool degrades to the `chmod 0600` fallback file **by design** — this is the one environment where the file path is routinely exercised, and POSIX permissions protect it there.
|
|
215
|
+
- A stale `credentials.json` sitting alongside a populated keychain entry is logged as drift (`credentials_file_shadowed_by_keychain` in `mcp.log`) and ignored in favour of the keychain.
|
|
216
|
+
|
|
86
217
|
The internal state-file shape is implementation-detail; don't depend on it from outside this package.
|
|
87
218
|
|
|
88
219
|
## Diagnostics
|
|
@@ -92,7 +223,21 @@ aixle-insights health # connectivity + last sync metadata
|
|
|
92
223
|
aixle-insights verify-hooks # JSON: hooks installed + queue depth
|
|
93
224
|
```
|
|
94
225
|
|
|
95
|
-
`mcp.log` (rotates at 5 MiB to `mcp.log.1`) under the app home directory captures operational events. Inside Claude Code, the **`
|
|
226
|
+
`mcp.log` (rotates at 5 MiB to `mcp.log.1`) under the app home directory captures operational events. Inside Claude Code, the **`aixle_insights_status`** MCP tool returns the same diagnostic structure as `aixle-insights health`.
|
|
227
|
+
|
|
228
|
+
## Troubleshooting
|
|
229
|
+
|
|
230
|
+
| Symptom | Most likely cause | Fix |
|
|
231
|
+
|---|---|---|
|
|
232
|
+
| `Error: DB90 API host <name> uses remote plaintext HTTP.` | You passed `--host http://<remote>` without `--insecure`. | Use `https://...`, or add `--insecure` if you know the endpoint is trusted and non-production. |
|
|
233
|
+
| `Blocked event send — DB90 ingest host <name> uses remote plaintext HTTP.` (or `Blocked project lookup — ...`) in `mcp.log` / console during `run` | `credentials.json` (or the keychain entry) has an `http://` remote `host` without a recorded `--insecure` consent — most likely because it was edited outside of `init`. | Re-run `aixle-insights init --host https://... ` (or `init --insecure --host http://...` if the endpoint is genuinely trusted non-prod) to re-establish credentials with an explicit, recorded decision. |
|
|
234
|
+
| `Auth failed: fetch failed` during `init` | The `--keycloak-url` host doesn't resolve (NXDOMAIN), is behind a VPN, or the TLS cert is bad. | Verify with `curl -sS https://<host>/realms/<realm>/.well-known/openid-configuration`. For DB90 staging, the canonical Keycloak URL is embedded in the SPA — `curl https://<APP_HOST> \| grep keycloakUrl` extracts the current value. |
|
|
235
|
+
| `Failed to post event: HTTP 401 Unauthorized` repeated for every turn | Your saved ingest token has been rotated, revoked, or invalidated by a server redeploy. The ingest token is distinct from the Keycloak access token that `health` reports as `authenticated: true`. | Reset the keychain entry and re-run `init`: `security delete-generic-password -s "aixle-insights" -a "aixle-insights-ingest-credential"` then `rm -f ~/.aixle-insights/credentials.json` then `aixle-insights init --host ... --keycloak-url ...`. State files are **not** deleted, so already-sent sessions stay deduped. |
|
|
236
|
+
| `health` shows `authenticated: true` but `last_result` is `sent: 0, failed: N` cycle after cycle | Same as the 401 row above. `authenticated` only proves the OIDC token was acquired, not that the ingest token still validates server-side. | Re-init as above. |
|
|
237
|
+
| `last_result` reports `sent: N` but the Events UI shows nothing | The Temporal worker is not running. The ingest endpoint returns HTTP 202 (queued) regardless of worker state. | `make worker` (or check `docker ps` for `db90-worker`). See [LOCAL-DEV.md](./LOCAL-DEV.md) §1. |
|
|
238
|
+
| `sync_lock_skip {reason: "advisory_lock_held"}` in the log | Another sync cycle is still holding `~/.aixle-insights/state.lock`. | Wait for it to finish; only delete the lock file (`rm -f ~/.aixle-insights/state.lock`) after confirming no `aixle-insights run` process is alive (`pgrep -fa aixle-insights`). |
|
|
239
|
+
| `aixle-insights --help` doesn't list `--insecure` | You're running an older published version of the package, not the local source. | `which aixle-insights` shows the path. To run local source: `cd packages/tools/aixle-insights && npm run build && npm link`. To return to the published version: `npm unlink -g @aixle/insights && npm install -g @aixle/insights@latest`. |
|
|
240
|
+
| Not sure whether `aixle-insights` is a `npm link` or a real install | Real installs are regular files; `npm link` is a symlink chain into the repo. | `readlink "$(which aixle-insights)"` shows the link target if any. A linked install will trace back to a path under your monorepo checkout. |
|
|
96
241
|
|
|
97
242
|
## Local development — `/aixle-reset` skill
|
|
98
243
|
|
|
@@ -131,6 +276,12 @@ After the script reports success: **quit and reopen Claude Code / Cursor** so ea
|
|
|
131
276
|
|
|
132
277
|
- Node.js ≥ 20.
|
|
133
278
|
- macOS / Linux / Windows. On Windows, the package writes a `cmd /c npx …` wrapper in `~/.claude.json` so Claude Code can spawn the MCP server reliably.
|
|
279
|
+
- `better-sqlite3` is a native module. After a Node upgrade, if SQLite reads start failing, rebuild it from the tools workspace:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
cd packages/tools
|
|
283
|
+
npm rebuild better-sqlite3
|
|
284
|
+
```
|
|
134
285
|
|
|
135
286
|
## License
|
|
136
287
|
|
|
@@ -5,6 +5,12 @@ export interface StoredCredentials {
|
|
|
5
5
|
host: string;
|
|
6
6
|
organizationId?: string;
|
|
7
7
|
accounts: Partial<Record<TelemetryToolId, string>>;
|
|
8
|
+
/**
|
|
9
|
+
* Set only when the user explicitly passed `init --insecure` for this host.
|
|
10
|
+
* There is no `run --insecure` flag (by design — see README § Security), so
|
|
11
|
+
* runtime sync honors this persisted consent instead of re-prompting.
|
|
12
|
+
*/
|
|
13
|
+
insecureHttpAllowed?: boolean;
|
|
8
14
|
}
|
|
9
15
|
export declare const KEYTAR_SERVICE = "aixle-insights";
|
|
10
16
|
/** Returns true when at least one tool has a non-empty token. */
|
|
@@ -12,7 +18,7 @@ export declare function credentialsHaveAnyToken(creds: StoredCredentials): boole
|
|
|
12
18
|
export declare function pickProjectLookupToken(creds: StoredCredentials): string | null;
|
|
13
19
|
/** Read credentials from disk only (tests / fallback). */
|
|
14
20
|
export declare function loadCredentialsFromFileOnly(appDir?: string): StoredCredentials | null;
|
|
15
|
-
/** Prefer OS keychain when keytar works;
|
|
21
|
+
/** Prefer the OS keychain when keytar works; fall back to `credentials.json`. */
|
|
16
22
|
export declare function loadCredentials(appDir?: string): Promise<StoredCredentials | null>;
|
|
17
23
|
/**
|
|
18
24
|
* Persist multi-tool ingest tokens for one host namespace.
|
package/dist/auth/credentials.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { userInfo } from "node:os";
|
|
2
4
|
import { join } from "node:path";
|
|
3
5
|
import { getAppDir } from "../state.js";
|
|
6
|
+
import { mcpLog } from "../log.js";
|
|
4
7
|
export const KEYTAR_SERVICE = "aixle-insights";
|
|
5
8
|
const KEYTAR_ACCOUNT = "aixle-insights-ingest-credential";
|
|
6
9
|
function credentialsPath(appDir) {
|
|
@@ -45,6 +48,7 @@ function normalizeLoadedCredentials(raw) {
|
|
|
45
48
|
host,
|
|
46
49
|
accounts: out,
|
|
47
50
|
organizationId: typeof org === "string" ? org : undefined,
|
|
51
|
+
...(o.insecureHttpAllowed === true ? { insecureHttpAllowed: true } : {}),
|
|
48
52
|
};
|
|
49
53
|
}
|
|
50
54
|
const token = o.token;
|
|
@@ -62,22 +66,33 @@ export function loadCredentialsFromFileOnly(appDir = getAppDir()) {
|
|
|
62
66
|
const raw = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
63
67
|
return normalizeLoadedCredentials(raw);
|
|
64
68
|
}
|
|
65
|
-
catch {
|
|
69
|
+
catch (err) {
|
|
70
|
+
// File exists (checked above) but failed to parse/normalize — distinguishes tampering from "never created".
|
|
71
|
+
mcpLog.warn("credentials_parse_failed", { path: filePath, error: err instanceof Error ? err.message : String(err) }, false);
|
|
66
72
|
return null;
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
75
|
async function tryKeytarGet() {
|
|
70
76
|
if (keytarDisabled())
|
|
71
77
|
return null;
|
|
78
|
+
let raw;
|
|
72
79
|
try {
|
|
73
80
|
const keytar = await import("keytar");
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
raw = await keytar.default.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Keytar unavailable (native module missing/unbuilt, no Secret Service, etc.) — silent fallback to file, same as tryKeytarSet.
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (!raw)
|
|
88
|
+
return null;
|
|
89
|
+
try {
|
|
77
90
|
const parsed = JSON.parse(raw);
|
|
78
91
|
return normalizeLoadedCredentials(parsed);
|
|
79
92
|
}
|
|
80
|
-
catch {
|
|
93
|
+
catch (err) {
|
|
94
|
+
// Keychain entry exists (checked above) but failed to parse/normalize — distinguishes tampering from "no entry".
|
|
95
|
+
mcpLog.warn("credentials_keytar_parse_failed", { keytarService: KEYTAR_SERVICE, error: err instanceof Error ? err.message : String(err) }, false);
|
|
81
96
|
return null;
|
|
82
97
|
}
|
|
83
98
|
}
|
|
@@ -100,8 +115,32 @@ async function tryKeytarDelete() {
|
|
|
100
115
|
const keytar = await import("keytar");
|
|
101
116
|
await keytar.default.deletePassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);
|
|
102
117
|
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
// Non-fatal, but a stale keychain entry can mislead later loads — record it.
|
|
120
|
+
mcpLog.warn("keytar_delete_failed", { keytarService: KEYTAR_SERVICE, error: err instanceof Error ? err.message : String(err) }, false);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Best-effort NTFS ACL lock-down for the fallback credentials file on Windows.
|
|
125
|
+
*
|
|
126
|
+
* Node's file `mode` / `chmodSync` only toggle the read-only bit on Windows — they do NOT
|
|
127
|
+
* map to NTFS ACLs — so the POSIX `0o600` we set on other platforms has no real effect there.
|
|
128
|
+
* We shell out to the built-in `icacls` to drop inherited ACEs and grant only the current user
|
|
129
|
+
* full control. Mirrors the POSIX `chmod` best-effort: any failure is swallowed (the user-profile
|
|
130
|
+
* directory already blocks cross-user reads, and Windows Credential Manager — the preferred store
|
|
131
|
+
* via keytar — makes this fallback file rare in the first place).
|
|
132
|
+
*/
|
|
133
|
+
function restrictWindowsAclBestEffort(filePath) {
|
|
134
|
+
try {
|
|
135
|
+
const user = userInfo().username;
|
|
136
|
+
if (!user)
|
|
137
|
+
return;
|
|
138
|
+
execFileSync("icacls", [filePath, "/inheritance:r", "/grant:r", `${user}:F`], {
|
|
139
|
+
stdio: "ignore",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
103
142
|
catch {
|
|
104
|
-
//
|
|
143
|
+
// best-effort, non-fatal — same posture as the POSIX chmod
|
|
105
144
|
}
|
|
106
145
|
}
|
|
107
146
|
function writeFileCredential(appDir, creds) {
|
|
@@ -112,12 +151,16 @@ function writeFileCredential(appDir, creds) {
|
|
|
112
151
|
host: creds.host,
|
|
113
152
|
organizationId: creds.organizationId,
|
|
114
153
|
accounts: { ...creds.accounts },
|
|
154
|
+
...(creds.insecureHttpAllowed ? { insecureHttpAllowed: true } : {}),
|
|
115
155
|
};
|
|
116
156
|
writeFileSync(filePath, `${JSON.stringify(body, null, 2)}\n`, {
|
|
117
157
|
encoding: "utf-8",
|
|
118
158
|
mode: 0o600,
|
|
119
159
|
});
|
|
120
|
-
if (process.platform
|
|
160
|
+
if (process.platform === "win32") {
|
|
161
|
+
restrictWindowsAclBestEffort(filePath);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
121
164
|
try {
|
|
122
165
|
chmodSync(filePath, 0o600);
|
|
123
166
|
}
|
|
@@ -132,19 +175,27 @@ function removeFileCredential(appDir) {
|
|
|
132
175
|
try {
|
|
133
176
|
unlinkSync(filePath);
|
|
134
177
|
}
|
|
135
|
-
catch {
|
|
136
|
-
//
|
|
178
|
+
catch (err) {
|
|
179
|
+
// A stale plaintext credentials.json left behind here is exactly the drift loadCredentials warns about.
|
|
180
|
+
mcpLog.warn("credentials_file_remove_failed", { path: filePath, error: err instanceof Error ? err.message : String(err) }, false);
|
|
137
181
|
}
|
|
138
182
|
}
|
|
139
183
|
}
|
|
140
|
-
/** Prefer OS keychain when keytar works;
|
|
184
|
+
/** Prefer the OS keychain when keytar works; fall back to `credentials.json`. */
|
|
141
185
|
export async function loadCredentials(appDir = getAppDir()) {
|
|
186
|
+
const fromKeytar = await tryKeytarGet();
|
|
187
|
+
if (fromKeytar) {
|
|
188
|
+
// Keychain is the source of truth; a lingering file is stale and silently shadowed it before this fix.
|
|
189
|
+
// Warn-only (log file, no stderr mirror): drift is recorded without spamming the background sync loop,
|
|
190
|
+
// and the next saveStoredCredentials removes the file. We do not mutate disk on a read.
|
|
191
|
+
if (existsSync(credentialsPath(appDir))) {
|
|
192
|
+
mcpLog.warn("credentials_file_shadowed_by_keychain", { path: credentialsPath(appDir), keytarService: KEYTAR_SERVICE }, false);
|
|
193
|
+
}
|
|
194
|
+
return fromKeytar;
|
|
195
|
+
}
|
|
142
196
|
const fromFile = loadCredentialsFromFileOnly(appDir);
|
|
143
197
|
if (fromFile)
|
|
144
198
|
return fromFile;
|
|
145
|
-
const fromKeytar = await tryKeytarGet();
|
|
146
|
-
if (fromKeytar)
|
|
147
|
-
return fromKeytar;
|
|
148
199
|
return null;
|
|
149
200
|
}
|
|
150
201
|
/**
|
|
@@ -154,7 +205,13 @@ export async function saveStoredCredentials(creds, appDir = getAppDir()) {
|
|
|
154
205
|
if (!credentialsHaveAnyToken(creds)) {
|
|
155
206
|
throw new Error("saveStoredCredentials requires at least one account token");
|
|
156
207
|
}
|
|
157
|
-
const payload = JSON.stringify({
|
|
208
|
+
const payload = JSON.stringify({
|
|
209
|
+
version: 2,
|
|
210
|
+
host: creds.host,
|
|
211
|
+
organizationId: creds.organizationId,
|
|
212
|
+
accounts: { ...creds.accounts },
|
|
213
|
+
...(creds.insecureHttpAllowed ? { insecureHttpAllowed: true } : {}),
|
|
214
|
+
});
|
|
158
215
|
const keytarOk = await tryKeytarSet(payload);
|
|
159
216
|
if (keytarOk) {
|
|
160
217
|
removeFileCredential(appDir);
|
package/dist/auth/exchange.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface ExchangeResult {
|
|
|
11
11
|
}
|
|
12
12
|
type ExchangeToolId = "claude_code" | "cursor";
|
|
13
13
|
export declare function exchangeIngestToken(params: {
|
|
14
|
-
|
|
14
|
+
apiHost: string;
|
|
15
15
|
keycloakAccessToken: string;
|
|
16
16
|
/** Legacy single-tool mint. Omit when tools is provided. */
|
|
17
17
|
toolName?: ExchangeToolId;
|
package/dist/auth/exchange.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export async function exchangeIngestToken(params) {
|
|
2
2
|
const fetchFn = params.fetchImpl ?? fetch;
|
|
3
3
|
const requestedTools = params.tools?.length ? [...params.tools] : params.toolName ? [params.toolName] : [];
|
|
4
|
-
const base = params.
|
|
4
|
+
const base = params.apiHost.replace(/\/$/, "");
|
|
5
5
|
const url = `${base}/api/v1/integrations/mcp/exchange`;
|
|
6
6
|
const body = {};
|
|
7
7
|
if (params.tools?.length) {
|
package/dist/auth/flow.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defaultKeycloakClientId, defaultKeycloakIssuer } from "./keycloak.js";
|
|
2
2
|
import type { TelemetryToolId } from "./credentials.js";
|
|
3
3
|
export interface LoginAndPersistOptions {
|
|
4
|
-
|
|
4
|
+
apiHost: string;
|
|
5
5
|
keycloakIssuer: string;
|
|
6
6
|
/** @deprecated Prefer `tools`; kept for callers that mint a single ingest account. */
|
|
7
7
|
toolName?: string;
|
|
@@ -11,12 +11,21 @@ export interface LoginAndPersistOptions {
|
|
|
11
11
|
exchangeOrganizationId?: string;
|
|
12
12
|
clientId?: string;
|
|
13
13
|
appDir?: string;
|
|
14
|
+
allowInsecureHttp?: boolean;
|
|
15
|
+
/** Skip the "already authenticated" short-circuit and re-run the device flow. */
|
|
16
|
+
force?: boolean;
|
|
17
|
+
onSecurityWarning?: (message: string) => void;
|
|
14
18
|
onVisitInstructions?: (verification_uri: string, user_code: string) => void;
|
|
15
19
|
fetchImpl?: typeof fetch;
|
|
16
20
|
}
|
|
17
21
|
export declare function loginAndPersistCredentials(opts: LoginAndPersistOptions): Promise<{
|
|
18
22
|
ok: true;
|
|
19
23
|
organizationId: string;
|
|
24
|
+
} | {
|
|
25
|
+
ok: true;
|
|
26
|
+
alreadyAuthenticated: true;
|
|
27
|
+
host: string;
|
|
28
|
+
organizationId?: string;
|
|
20
29
|
} | {
|
|
21
30
|
ok: false;
|
|
22
31
|
error: string;
|
package/dist/auth/flow.js
CHANGED
|
@@ -2,15 +2,37 @@ import { exchangeIngestToken } from "./exchange.js";
|
|
|
2
2
|
import { defaultKeycloakClientId, defaultKeycloakIssuer, obtainKeycloakAccessTokenViaDeviceFlow } from "./keycloak.js";
|
|
3
3
|
import { loadCredentials, saveStoredCredentials } from "./credentials.js";
|
|
4
4
|
import { getAppDir } from "../state.js";
|
|
5
|
+
import { evaluateTransportSecurity } from "../lib/transport-security.js";
|
|
6
|
+
function normalizeHostForComparison(host) {
|
|
7
|
+
return host.replace(/\/$/, "");
|
|
8
|
+
}
|
|
9
|
+
function credentialsCoverRequestedTools(creds, requestedTools) {
|
|
10
|
+
return requestedTools.every((tool) => {
|
|
11
|
+
const token = creds.accounts[tool];
|
|
12
|
+
return typeof token === "string" && token.length > 0;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function credentialsMatchRequestedScope(creds, opts, requestedTools) {
|
|
16
|
+
if (normalizeHostForComparison(creds.host) !== normalizeHostForComparison(opts.apiHost))
|
|
17
|
+
return false;
|
|
18
|
+
if (opts.exchangeOrganizationId && creds.organizationId !== opts.exchangeOrganizationId)
|
|
19
|
+
return false;
|
|
20
|
+
return credentialsCoverRequestedTools(creds, requestedTools);
|
|
21
|
+
}
|
|
5
22
|
export async function loginAndPersistCredentials(opts) {
|
|
23
|
+
const appDir = opts.appDir ?? getAppDir();
|
|
24
|
+
const requestedTools = opts.tools ?? ((opts.toolName ?? "claude_code") === "cursor" ? ["cursor"] : ["claude_code"]);
|
|
25
|
+
if (!opts.force) {
|
|
26
|
+
const existing = await loadCredentials(appDir);
|
|
27
|
+
if (existing && credentialsMatchRequestedScope(existing, opts, requestedTools)) {
|
|
28
|
+
return { ok: true, alreadyAuthenticated: true, host: existing.host, organizationId: existing.organizationId };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
6
31
|
const issuer = opts.keycloakIssuer.trim();
|
|
7
32
|
if (!issuer) {
|
|
8
33
|
return { ok: false, error: "Keycloak issuer is empty; set KEYCLOAK_ISSUER or pass --keycloak-url." };
|
|
9
34
|
}
|
|
10
35
|
const clientId = opts.clientId?.trim() || defaultKeycloakClientId();
|
|
11
|
-
const appDir = opts.appDir ?? getAppDir();
|
|
12
|
-
const requestedTools = opts.tools ??
|
|
13
|
-
((opts.toolName ?? "claude_code") === "cursor" ? ["cursor"] : ["claude_code"]);
|
|
14
36
|
let accessToken;
|
|
15
37
|
try {
|
|
16
38
|
accessToken = await obtainKeycloakAccessTokenViaDeviceFlow({
|
|
@@ -27,7 +49,7 @@ export async function loginAndPersistCredentials(opts) {
|
|
|
27
49
|
try {
|
|
28
50
|
if (requestedTools.length > 1) {
|
|
29
51
|
exchanged = await exchangeIngestToken({
|
|
30
|
-
|
|
52
|
+
apiHost: opts.apiHost,
|
|
31
53
|
keycloakAccessToken: accessToken,
|
|
32
54
|
tools: requestedTools,
|
|
33
55
|
deviceLabel: opts.deviceLabel,
|
|
@@ -37,7 +59,7 @@ export async function loginAndPersistCredentials(opts) {
|
|
|
37
59
|
}
|
|
38
60
|
else {
|
|
39
61
|
exchanged = await exchangeIngestToken({
|
|
40
|
-
|
|
62
|
+
apiHost: opts.apiHost,
|
|
41
63
|
keycloakAccessToken: accessToken,
|
|
42
64
|
toolName: requestedTools[0],
|
|
43
65
|
deviceLabel: opts.deviceLabel,
|
|
@@ -45,11 +67,22 @@ export async function loginAndPersistCredentials(opts) {
|
|
|
45
67
|
fetchImpl: opts.fetchImpl,
|
|
46
68
|
});
|
|
47
69
|
}
|
|
70
|
+
const ingestHostSecurity = evaluateTransportSecurity(exchanged.ingestHost, {
|
|
71
|
+
allowInsecureHttp: opts.allowInsecureHttp === true,
|
|
72
|
+
label: "DB90 ingest host",
|
|
73
|
+
});
|
|
74
|
+
if (!ingestHostSecurity.ok) {
|
|
75
|
+
return { ok: false, error: ingestHostSecurity.error };
|
|
76
|
+
}
|
|
77
|
+
if (ingestHostSecurity.warning) {
|
|
78
|
+
opts.onSecurityWarning?.(ingestHostSecurity.warning);
|
|
79
|
+
}
|
|
48
80
|
const existing = await loadCredentials(appDir);
|
|
49
81
|
const stored = {
|
|
50
82
|
host: exchanged.ingestHost,
|
|
51
83
|
organizationId: exchanged.organizationId,
|
|
52
84
|
accounts: existing?.host === exchanged.ingestHost ? { ...existing.accounts } : {},
|
|
85
|
+
...(opts.allowInsecureHttp === true ? { insecureHttpAllowed: true } : {}),
|
|
53
86
|
};
|
|
54
87
|
for (const tid of ["claude_code", "cursor"]) {
|
|
55
88
|
const acc = exchanged.accounts[tid];
|
package/dist/auth/keycloak.d.ts
CHANGED
|
@@ -31,5 +31,5 @@ export declare function obtainKeycloakAccessTokenViaDeviceFlow(params: {
|
|
|
31
31
|
onInstructions?: (verification_uri: string, user_code: string) => void;
|
|
32
32
|
fetchImpl?: typeof fetch;
|
|
33
33
|
}): Promise<string>;
|
|
34
|
-
export declare function defaultKeycloakIssuer(): string;
|
|
34
|
+
export declare function defaultKeycloakIssuer(ingestHost?: string): string;
|
|
35
35
|
export declare function defaultKeycloakClientId(): string;
|
package/dist/auth/keycloak.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* RFC 8628 OAuth 2.0 Device Authorization Grant against Keycloak OIDC endpoints.
|
|
3
3
|
*/
|
|
4
4
|
import { createHash, randomBytes } from "node:crypto";
|
|
5
|
+
import { isLoopbackHost } from "../lib/transport-security.js";
|
|
5
6
|
function normalizeIssuer(issuer) {
|
|
6
7
|
return issuer.replace(/\/$/, "");
|
|
7
8
|
}
|
|
@@ -154,13 +155,31 @@ export async function obtainKeycloakAccessTokenViaDeviceFlow(params) {
|
|
|
154
155
|
fetchImpl: params.fetchImpl,
|
|
155
156
|
});
|
|
156
157
|
}
|
|
157
|
-
export function defaultKeycloakIssuer() {
|
|
158
|
+
export function defaultKeycloakIssuer(ingestHost) {
|
|
158
159
|
const fromEnv = process.env["DB90_KEYCLOAK_ISSUER"]?.trim() ||
|
|
159
160
|
process.env["KEYCLOAK_ISSUER"]?.trim();
|
|
160
161
|
if (fromEnv)
|
|
161
162
|
return fromEnv.replace(/\/$/, "");
|
|
162
163
|
const useLocalDefault = ["1", "true", "yes"].includes(process.env["DB90_MCP_USE_LOCAL_KEYCLOAK_DEFAULT"]?.toLowerCase() ?? "");
|
|
163
164
|
if (useLocalDefault || process.env["NODE_ENV"] === "development") {
|
|
165
|
+
if (ingestHost) {
|
|
166
|
+
let hostname;
|
|
167
|
+
try {
|
|
168
|
+
hostname = new URL(ingestHost).hostname;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
hostname = ingestHost;
|
|
172
|
+
}
|
|
173
|
+
if (!isLoopbackHost(hostname)) {
|
|
174
|
+
const reason = useLocalDefault
|
|
175
|
+
? `DB90_MCP_USE_LOCAL_KEYCLOAK_DEFAULT=${process.env["DB90_MCP_USE_LOCAL_KEYCLOAK_DEFAULT"]}`
|
|
176
|
+
: `NODE_ENV=development`;
|
|
177
|
+
console.error(`[aixle-insights] Warning: ${reason} would redirect Keycloak authentication to ` +
|
|
178
|
+
`http://localhost:8080, but the ingest host "${ingestHost}" is not localhost. ` +
|
|
179
|
+
`Ignoring the local Keycloak default. Set DB90_KEYCLOAK_ISSUER explicitly.`);
|
|
180
|
+
return "";
|
|
181
|
+
}
|
|
182
|
+
}
|
|
164
183
|
return "http://localhost:8080/realms/db90";
|
|
165
184
|
}
|
|
166
185
|
return "";
|
package/dist/cli.d.ts
CHANGED
|
@@ -7,8 +7,9 @@ import { migrateLegacyState, getAppDir } from "./state.js";
|
|
|
7
7
|
import { syncTelemetryTools } from "./sync.js";
|
|
8
8
|
import { mergePricing } from "./pricing.js";
|
|
9
9
|
import { type InstallClaudeUserMcpOptions, type InstallResult } from "./install/claude.js";
|
|
10
|
+
import { type InstallCursorUserMcpOptions } from "./install/cursor.js";
|
|
10
11
|
export interface Args {
|
|
11
|
-
command: "init" | "health" | "run" | "help" | "uninstall-hooks" | "verify-hooks";
|
|
12
|
+
command: "init" | "health" | "run" | "help" | "uninstall-hooks" | "verify-hooks" | "uninstall-cursor-mcp";
|
|
12
13
|
help: boolean;
|
|
13
14
|
once: boolean;
|
|
14
15
|
/** With `run --once`: ignore Cursor watermarks and commit hash dedupe. */
|
|
@@ -21,6 +22,8 @@ export interface Args {
|
|
|
21
22
|
force?: boolean;
|
|
22
23
|
/** When set on init, install the Cursor hooks forwarder into ~/.cursor/hooks.json. */
|
|
23
24
|
hooks?: boolean;
|
|
25
|
+
/** Allow remote plaintext HTTP hosts for trusted non-production test environments. */
|
|
26
|
+
insecure?: boolean;
|
|
24
27
|
}
|
|
25
28
|
interface RunOnceDeps {
|
|
26
29
|
loadCredentials: typeof loadCredentials;
|
|
@@ -37,12 +40,13 @@ interface InitDeps {
|
|
|
37
40
|
defaultKeycloakIssuer: typeof defaultKeycloakIssuer;
|
|
38
41
|
getAppDir: typeof getAppDir;
|
|
39
42
|
installClaudeUserMcp: (options: InstallClaudeUserMcpOptions) => InstallResult;
|
|
43
|
+
installCursorUserMcp: (options: InstallCursorUserMcpOptions) => InstallResult;
|
|
40
44
|
log: (message: string) => void;
|
|
41
45
|
error: (message: string) => void;
|
|
42
46
|
}
|
|
43
47
|
/** Matches DB90 Rails `McpController` UUID check for `X-Organization-ID` (RFC 4122 variant). */
|
|
44
|
-
export declare const
|
|
45
|
-
export declare function
|
|
48
|
+
export declare const ORGANIZATION_UUID_PATTERN: RegExp;
|
|
49
|
+
export declare function isValidOrganizationUuid(value: string): boolean;
|
|
46
50
|
export declare function parseArgs(argv: string[]): Args;
|
|
47
51
|
export declare function runInit(cliArgs: Args, deps?: Partial<InitDeps>): Promise<number>;
|
|
48
52
|
export declare function runOnce(deps?: Partial<RunOnceDeps>, options?: {
|