@nexalink/carecompass-sdk 0.2.0-beta.14 → 0.3.0-beta.15

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 CHANGED
@@ -1,29 +1,84 @@
1
- # CareCompass Healthcare SDK
1
+ # CareCompass Healthcare AI Infrastructure SDK
2
2
 
3
- CareCompass is a healthcare navigation AI for healthcare apps and portals. The SDK gives payers, providers, employers, and healthcare technology teams a branded member-facing chat experience backed by Nexalink-hosted AI infrastructure.
3
+ CareCompass helps healthcare teams launch branded healthcare navigation assistants, member support assistants, and patient-facing AI experiences inside their own portals or applications. The SDK combines an embeddable chat experience with Nexalink-hosted healthcare AI infrastructure, reducing the implementation effort required to move from idea to deployable healthcare navigation workflow.
4
4
 
5
- CareCompass is not a general-purpose LLM SDK, medical diagnosis tool, replacement for clinical decision making, or system for autonomous treatment recommendations.
5
+ The SDK is publicly installable for developer convenience, but live regulated use requires Nexalink onboarding, issued credentials, and BAA coverage where PHI may be involved. CareCompass is designed for healthcare navigation and support workflows. It is not a general-purpose LLM SDK, not a medical diagnosis tool, not autonomous treatment recommendation software, and not a replacement for clinicians or clinical judgment.
6
+
7
+ ## Who It's For
8
+
9
+ - Healthcare startups building AI-powered patient experiences
10
+ - Payer innovation and member engagement teams
11
+ - Employer-benefits platforms
12
+ - Provider organizations adding patient-facing support workflows
13
+ - Digital health engineering teams
14
+ - Healthcare product teams that need AI infrastructure quickly
15
+
16
+ ## Why Teams Use CareCompass
17
+
18
+ - **Faster deployment**: add a healthcare-focused AI assistant to an existing healthcare product without months of infrastructure work.
19
+ - **Reduced engineering effort**: avoid building and operating the supporting healthcare AI infrastructure around the assistant.
20
+ - **Hosted healthcare AI runtime**: chat requests route through Nexalink-hosted APIs and the CareCompass AI runtime.
21
+ - **Embedded branded experience**: use the React component, JavaScript mount helper, generated embed page, or iframe with your display name, labels, theme, logo, and support links.
22
+ - **Prompt and configuration controls**: define approved prompt context, welcome copy, placement, allowed origins, and deployment environment.
23
+ - **Deployment controls**: use key management, origin allowlists, hosted validation, and staging/production credential separation.
24
+ - **Enterprise onboarding**: support BAA-gated onboarding where PHI may be involved.
25
+
26
+ ## Why Not Build It Yourself?
27
+
28
+ Most healthcare teams can access foundation models through OpenAI, Anthropic, Bedrock, Azure OpenAI, or similar providers. The harder work is not obtaining an LLM. The harder work is building and operating the healthcare AI infrastructure around it:
29
+
30
+ - Healthcare-focused prompts and guardrails
31
+ - Hosted inference infrastructure
32
+ - Tenant isolation
33
+ - Key management
34
+ - Deployment validation
35
+ - Browser embedding
36
+ - Usage tracking
37
+ - Compliance-oriented onboarding
38
+
39
+ CareCompass provides a managed healthcare AI infrastructure layer for these workflows, while keeping the embedded experience configurable for your application and brand.
40
+
41
+ ## What You Avoid Building
42
+
43
+ - Model gateway
44
+ - Chat embed
45
+ - Prompt validation layer
46
+ - Tenant and key management
47
+ - Usage tracking
48
+ - Hosted inference path
49
+ - Staging and production credential separation
50
+ - Deployment validation tooling
51
+ - Compliance-oriented onboarding workflow
6
52
 
7
53
  ## What You Can Build
8
54
 
9
- - Member navigation assistants
10
- - Benefits guidance tools
11
- - Provider discovery experiences
12
- - Healthcare support chat interfaces
55
+ - Member support assistants
56
+ - Benefits navigation assistants
57
+ - Provider discovery support flows
13
58
  - Employer healthcare navigation portals
14
59
  - Payer member engagement tools
60
+ - Digital health support chat experiences
61
+
62
+ ## Example Questions CareCompass Can Help With
63
+
64
+ CareCompass is intended for healthcare navigation and support, not diagnosis or treatment recommendations. Example navigation questions include:
15
65
 
16
- ## Why CareCompass
66
+ - "How do I find an in-network cardiologist?"
67
+ - "What does my deductible mean?"
68
+ - "How do I schedule a specialist appointment?"
69
+ - "Where can I find behavioral health resources?"
70
+ - "What support resources are available through my plan?"
17
71
 
18
- - **Healthcare-focused experience**: prompts, labels, guardrails, and defaults are oriented around healthcare navigation.
19
- - **Hosted AI infrastructure**: chat requests go through Nexalink-hosted APIs and the Nexalink-managed AI runtime.
20
- - **Branding and customization**: configure display name, logo, support links, theme, labels, placement, and approved prompt context.
21
- - **Usage tracking**: Nexalink tracks usage under issued keys for deployment monitoring and future metered billing.
22
- - **Enterprise deployment support**: use separate staging/production keys, origin allowlists, validation, BAA-gated onboarding where PHI may be involved, and security controls.
72
+ ## What CareCompass Is Not
73
+
74
+ - Not a general-purpose LLM SDK
75
+ - Not a diagnosis tool
76
+ - Not a replacement for clinical judgment
77
+ - Not autonomous treatment recommendation software
23
78
 
24
79
  ## Quick Start
25
80
 
26
- The package is publicly installable for developer convenience. Live regulated use requires Nexalink onboarding, BAA coverage where PHI may be involved, and Nexalink-issued credentials.
81
+ Account creation and credentials are required because CareCompass is healthcare AI infrastructure for regulated deployment workflows, not an unauthenticated demo widget. The CLI requires Node.js 18 or newer.
27
82
 
28
83
  ```bash
29
84
  npx @nexalink/carecompass-sdk init --account-type developer --api-key <nexalink-api-key> --payer-id acme-health --origin https://members.acme-health.com
@@ -39,7 +94,7 @@ npx @nexalink/carecompass-sdk doctor --config carecompass/carecompass.config.jso
39
94
  - `carecompass/carecompass-iframe.html`
40
95
  - `carecompass/README.md`
41
96
 
42
- `npx` does not install a permanent `carecompass` command. Keep using `npx @nexalink/carecompass-sdk <command>` or install the package. The CLI requires Node.js 18 or newer.
97
+ `npx` does not install a permanent `carecompass` command. Keep using `npx @nexalink/carecompass-sdk <command>` or install the package.
43
98
 
44
99
  ## How It Works
45
100
 
@@ -53,19 +108,22 @@ Nexalink Hosted APIs
53
108
  CareCompass AI Runtime
54
109
  ```
55
110
 
56
- Your application renders the React component, JavaScript mount helper, generated embed page, or iframe. The browser receives only public embed configuration: payer ID, environment, public embed key, branding, labels, theme, support links, approved prompt settings, and telemetry defaults. Chat goes to Nexalink-hosted CareCompass API routes.
111
+ Your application renders the CareCompass UI through the SDK. The browser receives only public embed configuration: payer ID, environment, public embed key, branding, labels, theme, support links, approved prompt settings, placement, and telemetry defaults. The browser does not receive the secret API key, private model endpoint details, SageMaker credentials, AppSync signing roles, or Nexalink server-side allowlists.
112
+
113
+ Chat requests go to Nexalink-hosted CareCompass API routes. Those hosted routes are responsible for authorization, origin validation, tenant isolation, prompt guardrails, rate limiting, request attribution, usage tracking, and routing to the CareCompass AI runtime.
114
+
115
+ ## Account, Credentials, and BAA Requirements
57
116
 
58
- ## Account Requirements
117
+ A Nexalink account with SDK access is required beyond package installation so Nexalink can validate payer identity, deployment environment, allowed origins, approved prompt configuration, credentials, and API usage.
59
118
 
60
- A Nexalink account with SDK access is required beyond package installation so Nexalink can validate payer identity, environment, secret API key, public embed key, allowed origins, approved prompt configuration, and API usage. API keys are issued in the Nexalink portal. When PHI may be involved, a tenant owner or admin executes the canonical BAA before generating the first key. The secret API key is shown once, stored locally, and must never be committed or placed in browser code.
119
+ CareCompass uses two different keys:
61
120
 
62
- ## Common Use Cases
121
+ - **API key**: a secret credential for trusted setup and server-side or CLI workflows, including `init` sign-in and validation. Never include it in browser code, generated embed HTML, frontend config, or source control.
122
+ - **Embed key**: a public browser-facing identifier for the CareCompass embed. It is safe to include in `carecompass.config.json` and generated snippets. It identifies the payer, environment, and embed client for runtime requests, but it is not a secret or proof of identity on its own.
63
123
 
64
- - Payers: branded member assistants for benefits, coverage navigation, support resources, and next steps.
65
- - Employers: healthcare navigation portals that route employees to plan resources and approved support channels.
66
- - Providers and digital health companies: support chat with model access, validation, and usage tracking behind Nexalink-hosted APIs.
124
+ API keys are issued in the Nexalink portal. When PHI may be involved, a tenant owner or admin may need to execute the canonical BAA before generating the first key. The secret API key is shown once, stored locally by the developer, and should be handled as a server-side or CLI credential.
67
125
 
68
- ## React Example
126
+ ## React Usage
69
127
 
70
128
  ```tsx
71
129
  import { CareCompassEmbed } from "@nexalink/carecompass-sdk";
@@ -76,7 +134,7 @@ export function MemberSupport() {
76
134
  }
77
135
  ```
78
136
 
79
- ## JavaScript Example
137
+ ## JavaScript Usage
80
138
 
81
139
  ```ts
82
140
  import { mountCareCompass } from "@nexalink/carecompass-sdk";
@@ -85,76 +143,6 @@ import config from "./carecompass/carecompass.config.json";
85
143
  mountCareCompass(document.getElementById("carecompass-root"), config);
86
144
  ```
87
145
 
88
- ## Enterprise Features
89
-
90
- CareCompass uses separate credentials for trusted setup and browser runtime:
91
-
92
- - **API key**: a secret credential for trusted setup and server-side/CLI workflows, including `init` sign-in and validation. Never include it in browser code, generated embed HTML, frontend config, or source control.
93
- - **Embed key**: a public browser-facing identifier for the CareCompass embed. It is safe to include in `carecompass.config.json` and generated snippets. It identifies the payer, environment, and embed client for runtime requests, but it is not a secret or proof of identity on its own.
94
-
95
- Enterprise concepts include API keys, embed keys, origin allowlists, BAA requirements where PHI may be involved, usage tracking, future Stripe metered billing support, hosted validation, prompt guardrails, rate limiting, request attribution, tenant isolation, and staging/production separation. Enterprise signup and BAA execution happen in the Nexalink web app before API key creation.
96
-
97
- Use separate keys and config files for staging and production:
98
-
99
- ```bash
100
- npx @nexalink/carecompass-sdk validate --config carecompass.staging.json
101
- npx @nexalink/carecompass-sdk snippet --config carecompass.staging.json --output carecompass/staging-embed.html
102
- npx @nexalink/carecompass-sdk doctor --config carecompass.staging.json
103
- ```
104
-
105
- Before production, confirm the exact origin allowlist, a production-specific `embedKey`, approved Nexalink-hosted `apiBaseUrl`, support links, prompt instructions, and no secret API key in generated source code.
106
-
107
- ## Security & Compliance
108
-
109
- CareCompass keeps secret SDK API keys out of the browser. The browser uses public `embedKey`, payer ID, environment, and allowed origin metadata to call Nexalink-hosted embed API routes. Staging and production `apiBaseUrl` values must be Nexalink-hosted HTTPS URLs; `localhost` is accepted only for `development`.
110
-
111
- The browser does not receive SageMaker credentials, AppSync signing roles, private model endpoint details, Nexalink server-side allowlists, or the secret SDK API key. Generated snippets escape serialized config, and React renders chat messages and labels as text.
112
-
113
- Telemetry is enabled automatically with Nexalink-managed defaults. The SDK sends only allowlisted anonymous deployment-health events to PostHog's public ingestion API. It does not bundle `posthog-js`, enable session replay or autocapture, identify users, set cookies, or persist analytics state. Prompts, responses, chat history, user context, PHI/PII, credentials, raw errors, stack traces, CLI arguments, terminal input, and file paths are excluded.
114
-
115
- Some MVP and development model endpoints may use scale-to-zero capacity; first request after an idle period can take several minutes while the endpoint warms through Nexalink-hosted APIs.
116
-
117
- ## CLI Reference
118
-
119
- ### `init`
120
-
121
- Runs first-time setup: routes users to signup, validates the API key, stores it locally, and generates the config, prompt starter, embed page, iframe, and setup guide with only public embed configuration.
122
-
123
- Credentials are stored at `~/.nexalink/carecompass/credentials.json` by default. You can also set `CARECOMPASS_API_KEY`. For scripted setup, pass `--account-type developer`. Configure signup with `--developer-signup-url` or `CARECOMPASS_DEVELOPER_SIGNUP_URL`; default: `https://www.nexalink.care/developer/signup`.
124
-
125
- Generated HTML defaults to bundler-friendly imports from `@nexalink/carecompass-sdk`. For a raw browser HTML demo that is not processed by a bundler, add `--snippet-mode cdn`.
126
-
127
- ```bash
128
- npx @nexalink/carecompass-sdk init --account-type developer --api-key <nexalink-api-key> --payer-id acme-health --origin https://members.acme-health.com
129
- npx @nexalink/carecompass-sdk init --account-type developer --snippet-mode cdn --payer-id acme-health --origin https://members.acme-health.com
130
- ```
131
-
132
- ### `validate`
133
-
134
- Checks local config shape and credentials, then hosted validation for `apiBaseUrl`, `embedKey`, `payerId`, `environment`, and `allowedOrigins` unless `--local-only` is passed. Browser-side validation uses public embed headers and does not send the secret API key.
135
-
136
- ```bash
137
- npx @nexalink/carecompass-sdk validate --config carecompass/carecompass.config.json
138
- npx @nexalink/carecompass-sdk validate --local-only --config carecompass/carecompass.config.json
139
- ```
140
-
141
- ### `snippet`
142
-
143
- Regenerates the HTML embed snippet after config, branding, origin, or prompt changes. `init` already runs this once.
144
-
145
- ```bash
146
- npx @nexalink/carecompass-sdk snippet --config carecompass/carecompass.config.json
147
- npx @nexalink/carecompass-sdk snippet --snippet-mode cdn --config carecompass/carecompass.config.json
148
- ```
149
-
150
- ### `doctor`
151
-
152
- Prints a deployment readiness summary for the selected config.
153
-
154
- ```bash
155
- npx @nexalink/carecompass-sdk doctor --config carecompass/carecompass.config.json
156
- ```
157
-
158
146
  ## Configuration Reference
159
147
 
160
148
  `carecompass/carecompass.config.json` is safe to commit when it contains only public embed configuration. Store the secret API key only in local CLI credentials or an approved server-side secret store.
@@ -212,6 +200,61 @@ npx @nexalink/carecompass-sdk doctor --config carecompass/carecompass.config.jso
212
200
 
213
201
  Serve `carecompass/carecompass-embed.html` at the iframe `src`, often by copying the generated `carecompass` folder into public/static assets.
214
202
 
203
+ ## CLI Reference
204
+
205
+ ### `init`
206
+
207
+ Runs first-time setup: routes users to signup, validates the API key, stores it locally, and generates the config, prompt starter, embed page, iframe, and setup guide with only public embed configuration.
208
+
209
+ Credentials are stored at `~/.nexalink/carecompass/credentials.json` by default. You can also set `CARECOMPASS_API_KEY`. For scripted setup, pass `--account-type developer`. Configure signup with `--developer-signup-url` or `CARECOMPASS_DEVELOPER_SIGNUP_URL`; default: `https://www.nexalink.care/developer/signup`.
210
+
211
+ Generated HTML defaults to bundler-friendly imports from `@nexalink/carecompass-sdk`. For a raw browser HTML demo that is not processed by a bundler, add `--snippet-mode cdn`.
212
+
213
+ ```bash
214
+ npx @nexalink/carecompass-sdk init --account-type developer --api-key <nexalink-api-key> --payer-id acme-health --origin https://members.acme-health.com
215
+ npx @nexalink/carecompass-sdk init --account-type developer --snippet-mode cdn --payer-id acme-health --origin https://members.acme-health.com
216
+ ```
217
+
218
+ ### `validate`
219
+
220
+ Checks local config shape and credentials, then hosted validation for `apiBaseUrl`, `embedKey`, `payerId`, `environment`, and `allowedOrigins` unless `--local-only` is passed. Browser-side validation uses public embed headers and does not send the secret API key.
221
+
222
+ ```bash
223
+ npx @nexalink/carecompass-sdk validate --config carecompass/carecompass.config.json
224
+ npx @nexalink/carecompass-sdk validate --local-only --config carecompass/carecompass.config.json
225
+ ```
226
+
227
+ ### `snippet`
228
+
229
+ Regenerates the HTML embed snippet after config, branding, origin, or prompt changes. `init` already runs this once.
230
+
231
+ ```bash
232
+ npx @nexalink/carecompass-sdk snippet --config carecompass/carecompass.config.json
233
+ npx @nexalink/carecompass-sdk snippet --snippet-mode cdn --config carecompass/carecompass.config.json
234
+ ```
235
+
236
+ ### `doctor`
237
+
238
+ Prints a deployment readiness summary for the selected config.
239
+
240
+ ```bash
241
+ npx @nexalink/carecompass-sdk doctor --config carecompass/carecompass.config.json
242
+ ```
243
+
244
+ ## Security and Compliance Model
245
+
246
+ CareCompass is BAA-gated where PHI may be involved and HIPAA-oriented for regulated healthcare deployment workflows, but this README does not claim blanket HIPAA compliance for every implementation or deployment.
247
+
248
+ The SDK keeps secret keys out of the browser. Browser code receives public embed configuration only. Staging and production `apiBaseUrl` values must be Nexalink-hosted HTTPS URLs; `localhost` is accepted only for `development`.
249
+
250
+ Nexalink-hosted API routes are responsible for authorization, origin allowlists, tenant isolation, prompt guardrails, rate limiting, request attribution, and usage tracking. Generated snippets escape serialized config, and React renders chat messages and labels as text.
251
+
252
+ Telemetry is enabled automatically with Nexalink-managed defaults. The SDK sends only allowlisted anonymous deployment-health events to PostHog's public ingestion API. It does not bundle `posthog-js`, enable session replay or autocapture, identify users, set cookies, or persist analytics state. Prompts, responses, chat history, user context, PHI/PII, credentials, raw errors, stack traces, CLI arguments, terminal input, and file paths are excluded from telemetry.
253
+
254
+ ## Runtime Notes
255
+
256
+ Some development and MVP environments may use scale-to-zero infrastructure, which can add cold-start latency to the first request after an idle period. The request still routes through Nexalink-hosted APIs; the browser embed does not receive infrastructure credentials or call the model endpoint directly.
257
+
215
258
  ## Troubleshooting
216
259
 
217
260
  - `Missing Nexalink API key`: run `init`, choose Developer, and sign in. For scripts, pass `--account-type developer` with `--api-key <key>`, or set `CARECOMPASS_API_KEY` with `CARECOMPASS_ACCOUNT_TYPE=developer`.
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync, } from "node:fs";
4
- import { dirname, join, resolve } from "node:path";
4
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { createDefaultCareCompassConfig, validateCareCompassConfig, } from "./schema.js";
6
+ import { careCompassApiBaseUrlSchema, createDefaultCareCompassConfig, validateCareCompassConfig, } from "./schema.js";
7
7
  import { normalizeApiKey, resolveApiKey, resolveCredentialsPath, writeStoredApiKey, } from "./credentials.js";
8
8
  import { generateCareCompassIframeSnippet, generateCareCompassSnippet, } from "./snippet.js";
9
9
  import { CARECOMPASS_TELEMETRY_ERROR_CODES, CARECOMPASS_TELEMETRY_EVENTS, createCareCompassCliTelemetry, } from "./telemetry.js";
@@ -31,6 +31,172 @@ support resources, and appropriate next steps. Stay out of diagnosis, treatment
31
31
  planning, emergency triage, legal advice, and coverage guarantees. When a member
32
32
  needs clinical, emergency, plan-specific, or account-specific help that requires
33
33
  a human, route them to the customer's approved support resources.`;
34
+ const CLI_COMMAND_ALLOWLIST = new Set([
35
+ "doctor",
36
+ "help",
37
+ "init",
38
+ "login",
39
+ "snippet",
40
+ "validate",
41
+ ]);
42
+ class CliTelemetryError extends Error {
43
+ constructor(message, telemetry) {
44
+ super(message);
45
+ this.telemetry = telemetry;
46
+ }
47
+ }
48
+ const sanitizeCommandName = (command) => {
49
+ const normalized = command === "--help" || command === "-h" ? "help" : command;
50
+ return CLI_COMMAND_ALLOWLIST.has(normalized) ? normalized : "unknown_command";
51
+ };
52
+ const authTelemetryForSource = (source) => {
53
+ if (source === "environment") {
54
+ return { authMethod: "env_var", keySource: "env_var" };
55
+ }
56
+ if (source === "file") {
57
+ return { authMethod: "config_file", keySource: "config_file" };
58
+ }
59
+ return { authMethod: "api_key", keySource: "cli_flag" };
60
+ };
61
+ const normalizeBaaStatus = (value) => {
62
+ if (value === "signed" || value === "pending" || value === "not_required") {
63
+ return value;
64
+ }
65
+ return "signed";
66
+ };
67
+ const classifyAuthFailure = (status, message) => {
68
+ const normalized = message.toLowerCase();
69
+ if (status === 429 || normalized.includes("rate limit")) {
70
+ return {
71
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.RATE_LIMIT_EXCEEDED,
72
+ failureCategory: "rate_limit_error",
73
+ baaStatus: "signed",
74
+ };
75
+ }
76
+ if (status === 403 ||
77
+ normalized.includes("baa") ||
78
+ normalized.includes("business associate")) {
79
+ return {
80
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.BAA_REQUIRED,
81
+ failureCategory: "baa_not_signed",
82
+ baaStatus: "pending",
83
+ };
84
+ }
85
+ if (normalized.includes("expired")) {
86
+ return {
87
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.AUTH_KEY_EXPIRED,
88
+ failureCategory: "auth_error",
89
+ baaStatus: "signed",
90
+ };
91
+ }
92
+ return {
93
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.AUTH_KEY_INVALID,
94
+ failureCategory: "auth_error",
95
+ baaStatus: "signed",
96
+ };
97
+ };
98
+ const classifyCliFailure = (error) => {
99
+ if (error instanceof CliTelemetryError)
100
+ return error.telemetry;
101
+ const code = error && typeof error === "object" && "code" in error
102
+ ? String(error.code)
103
+ : "";
104
+ const message = error instanceof Error ? error.message : String(error);
105
+ const normalized = message.toLowerCase();
106
+ if (code === "EACCES" || code === "EPERM") {
107
+ return {
108
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.PERMISSION_DENIED,
109
+ failureCategory: "permission_error",
110
+ };
111
+ }
112
+ if (code === "ENOENT" || normalized.includes("was not found")) {
113
+ return {
114
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.CONFIG_NOT_FOUND,
115
+ failureCategory: "config_error",
116
+ };
117
+ }
118
+ if (normalized.includes("invalid carecompass config") ||
119
+ normalized.includes("unexpected argument") ||
120
+ normalized.includes("must be either") ||
121
+ normalized.includes("api base url")) {
122
+ return {
123
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.CONFIG_INVALID,
124
+ failureCategory: "config_error",
125
+ };
126
+ }
127
+ if (normalized.includes("already exists")) {
128
+ return {
129
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.CONFIG_ALREADY_EXISTS,
130
+ failureCategory: "config_error",
131
+ };
132
+ }
133
+ if (normalized.includes("missing nexalink api key")) {
134
+ return {
135
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.AUTH_KEY_MISSING,
136
+ failureCategory: "auth_error",
137
+ };
138
+ }
139
+ if (normalized.includes("api key format") ||
140
+ normalized.includes("invalid api key")) {
141
+ return {
142
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.AUTH_KEY_INVALID,
143
+ failureCategory: "auth_error",
144
+ };
145
+ }
146
+ if (normalized.includes("baa execution") ||
147
+ normalized.includes("business associate")) {
148
+ return {
149
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.BAA_REQUIRED,
150
+ failureCategory: "baa_not_signed",
151
+ };
152
+ }
153
+ if (normalized.includes("rate limit") || normalized.includes("quota")) {
154
+ return {
155
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.RATE_LIMIT_EXCEEDED,
156
+ failureCategory: "rate_limit_error",
157
+ };
158
+ }
159
+ if (normalized.includes("timed out") || normalized.includes("timeout")) {
160
+ return {
161
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.NET_TIMEOUT,
162
+ failureCategory: "network_error",
163
+ };
164
+ }
165
+ if (normalized.includes("could not reach")) {
166
+ return {
167
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.NET_UNREACHABLE,
168
+ failureCategory: "network_error",
169
+ };
170
+ }
171
+ if (normalized.includes("model load")) {
172
+ return {
173
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.MODEL_LOAD_FAILED,
174
+ failureCategory: "model_error",
175
+ };
176
+ }
177
+ if (normalized.includes("model") || normalized.includes("inference")) {
178
+ return {
179
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.MODEL_INFERENCE_FAILED,
180
+ failureCategory: "model_error",
181
+ };
182
+ }
183
+ if (normalized.includes("unknown command")) {
184
+ return {
185
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.CLI_COMMAND_UNKNOWN,
186
+ failureCategory: "unknown",
187
+ };
188
+ }
189
+ if (normalized.includes("cancelled")) {
190
+ return {
191
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.CLI_COMMAND_CANCELLED,
192
+ failureCategory: "unknown",
193
+ };
194
+ }
195
+ return {
196
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.UNKNOWN_ERROR,
197
+ failureCategory: "unknown",
198
+ };
199
+ };
34
200
  function parseArgs(argv) {
35
201
  const [command = "help", ...rest] = argv;
36
202
  const flags = {};
@@ -210,6 +376,12 @@ const signupUrls = (flags, io) => ({
210
376
  io.env.CARECOMPASS_DEVELOPER_SIGNUP_URL ??
211
377
  DEFAULT_DEVELOPER_SIGNUP_URL,
212
378
  });
379
+ function resolveTrustedApiBaseUrl(value) {
380
+ const result = careCompassApiBaseUrlSchema.safeParse(value);
381
+ if (result.success)
382
+ return result.data;
383
+ throw new Error(result.error.issues.map((issue) => issue.message).join("\n"));
384
+ }
213
385
  function rejectLegacyBaaInitFlags(flags, io) {
214
386
  const legacyFlag = LEGACY_BAA_INIT_FLAGS.find((flag) => flag in flags);
215
387
  if (!legacyFlag)
@@ -267,7 +439,7 @@ async function validateInitApiKey(apiBaseUrl, apiKey, accountType) {
267
439
  }
268
440
  catch (error) {
269
441
  const message = error instanceof Error ? error.message : "Network request failed";
270
- throw new Error(`Could not reach CareCompass authentication API at ${url}. ${message}`);
442
+ throw new CliTelemetryError(`Could not reach CareCompass authentication API at ${url}. ${message}`, classifyCliFailure(new Error(`Could not reach CareCompass authentication API. ${message}`)));
271
443
  }
272
444
  const text = await response.text();
273
445
  let payload = {};
@@ -278,27 +450,33 @@ async function validateInitApiKey(apiBaseUrl, apiKey, accountType) {
278
450
  payload = {};
279
451
  }
280
452
  if (!response.ok || payload.ok === false) {
453
+ const message = payload.error ?? "Invalid API key.";
454
+ const failure = classifyAuthFailure(response.status, message);
281
455
  return {
282
456
  ok: false,
283
- message: payload.error ?? "Invalid API key.",
457
+ message,
458
+ ...failure,
284
459
  };
285
460
  }
286
461
  const validatedType = typeof payload.accountType === "string"
287
462
  ? normalizeAccountType(payload.accountType)
288
463
  : null;
289
464
  if (!validatedType) {
465
+ const message = "Credential validation succeeded but did not return a recognized account type. Contact support@nexalink.care.";
290
466
  return {
291
467
  ok: false,
292
- message: "Credential validation succeeded but did not return a recognized account type. Contact support@nexalink.care.",
468
+ message,
469
+ ...classifyAuthFailure(401, message),
293
470
  };
294
471
  }
295
472
  return {
296
473
  ok: true,
297
474
  accountType: validatedType,
475
+ baaStatus: normalizeBaaStatus(payload.baaStatus),
298
476
  setup: normalizeAuthSetup(payload.setup),
299
477
  };
300
478
  }
301
- async function resolveInitApiKey(flags, io, apiBaseUrl) {
479
+ async function resolveInitApiKey(flags, io, apiBaseUrl, onAuthChecked = async () => undefined) {
302
480
  const credentialsPath = resolveCredentialsPath(io.env);
303
481
  let credentials = resolveApiKey(flagString(flags, "api-key"), io.env, credentialsPath);
304
482
  const urls = signupUrls(flags, io);
@@ -316,6 +494,7 @@ async function resolveInitApiKey(flags, io, apiBaseUrl) {
316
494
  const apiKey = useResolvedCredential
317
495
  ? credentials.apiKey
318
496
  : await promptForSecret(io, "Enter API key: ");
497
+ const authTelemetry = authTelemetryForSource(credentials.source);
319
498
  useResolvedCredential = false;
320
499
  credentials = { apiKey: null, source: "missing" };
321
500
  const normalizedApiKey = normalizeApiKey(apiKey);
@@ -324,11 +503,37 @@ async function resolveInitApiKey(flags, io, apiBaseUrl) {
324
503
  continue;
325
504
  }
326
505
  io.stdout.write("Validating credentials...\n");
327
- const validation = await validateInitApiKey(apiBaseUrl, normalizedApiKey, accountType);
506
+ let validation;
507
+ try {
508
+ validation = await validateInitApiKey(apiBaseUrl, normalizedApiKey, accountType);
509
+ }
510
+ catch (error) {
511
+ const failure = classifyCliFailure(error);
512
+ await onAuthChecked({
513
+ authResult: "failure",
514
+ baaStatus: "signed",
515
+ keySource: authTelemetry.keySource,
516
+ ...failure,
517
+ });
518
+ throw error;
519
+ }
328
520
  if (!validation.ok) {
521
+ await onAuthChecked({
522
+ authResult: "failure",
523
+ baaStatus: validation.baaStatus,
524
+ keySource: authTelemetry.keySource,
525
+ errorCode: validation.errorCode,
526
+ failureCategory: validation.failureCategory,
527
+ });
329
528
  io.stdout.write(`${validation.message}\n`);
330
529
  continue;
331
530
  }
531
+ await onAuthChecked({
532
+ authResult: "success",
533
+ baaStatus: validation.baaStatus,
534
+ keySource: authTelemetry.keySource,
535
+ failureCategory: null,
536
+ });
332
537
  if (validation.accountType !== accountType) {
333
538
  if (validation.accountType === "enterprise") {
334
539
  io.stdout.write(`This API key belongs to an enterprise account. ${enterpriseUnavailableMessage(urls.developer)}\n`);
@@ -340,7 +545,12 @@ async function resolveInitApiKey(flags, io, apiBaseUrl) {
340
545
  writeStoredApiKey(normalizedApiKey, credentialsPath);
341
546
  io.stdout.write(`Stored CareCompass credentials at ${credentialsPath}\n`);
342
547
  io.stdout.write("Sign-in successful.\n");
343
- return { apiKey: normalizedApiKey, setup: validation.setup };
548
+ return {
549
+ apiKey: normalizedApiKey,
550
+ authMethod: authTelemetry.authMethod,
551
+ baaStatus: validation.baaStatus,
552
+ setup: validation.setup,
553
+ };
344
554
  }
345
555
  }
346
556
  function resolvePromptFile(config, configPath) {
@@ -349,8 +559,23 @@ function resolvePromptFile(config, configPath) {
349
559
  return config;
350
560
  // Prompt files are resolved relative to the config file so generated kits work
351
561
  // after the caller changes directories.
352
- const resolvedPath = resolve(dirname(configPath), promptFile);
353
- const systemInstruction = readFileSync(resolvedPath, "utf8").trim();
562
+ if (isAbsolute(promptFile)) {
563
+ throw new Error("Prompt file must stay inside the config directory.");
564
+ }
565
+ const configDir = realpathSync(resolve(dirname(configPath)));
566
+ const resolvedPath = resolve(configDir, promptFile);
567
+ let realPromptPath;
568
+ try {
569
+ realPromptPath = realpathSync(resolvedPath);
570
+ }
571
+ catch {
572
+ throw new Error(`CareCompass prompt file was not found: ${promptFile}`);
573
+ }
574
+ const relativePromptPath = relative(configDir, realPromptPath);
575
+ if (relativePromptPath.startsWith("..") || isAbsolute(relativePromptPath)) {
576
+ throw new Error("Prompt file must stay inside the config directory.");
577
+ }
578
+ const systemInstruction = readFileSync(realPromptPath, "utf8").trim();
354
579
  const validation = validateCareCompassConfig({
355
580
  ...config,
356
581
  prompt: {
@@ -562,13 +787,63 @@ export async function runCareCompassCli(argv = process.argv.slice(2), io = {
562
787
  // Start with anonymous CLI telemetry, then rebuild it with payer context after
563
788
  // a config has been generated or read.
564
789
  let cliTelemetry = createCareCompassCliTelemetry(cliSessionId);
565
- let activeCommand = "unknown";
790
+ const sessionStartedAt = Date.now();
791
+ const rawCommand = argv[0] ?? "help";
792
+ const telemetryCommandName = sanitizeCommandName(rawCommand);
793
+ const commandStartedAt = Date.now();
794
+ let commandsAttempted = 0;
795
+ let commandsSucceeded = 0;
796
+ let commandsFailed = 0;
797
+ let activeCommand = rawCommand;
798
+ let authCheckedCaptured = false;
799
+ let sessionEndedCaptured = false;
800
+ const captureAuthChecked = async (telemetry) => {
801
+ if (authCheckedCaptured)
802
+ return;
803
+ authCheckedCaptured = true;
804
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_AUTH_CHECKED, {
805
+ authResult: telemetry.authResult,
806
+ failureCategory: telemetry.authResult === "success"
807
+ ? null
808
+ : (telemetry.failureCategory ?? "unknown"),
809
+ ...(telemetry.errorCode ? { errorCode: telemetry.errorCode } : {}),
810
+ baaStatus: telemetry.baaStatus,
811
+ keySource: telemetry.keySource,
812
+ });
813
+ };
814
+ const captureCommandCompleted = async () => {
815
+ commandsSucceeded += 1;
816
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_COMMAND_COMPLETED, {
817
+ commandName: telemetryCommandName,
818
+ commandDurationMs: Date.now() - commandStartedAt,
819
+ tokensUsed: null,
820
+ modelId: null,
821
+ });
822
+ };
823
+ const captureSessionEnded = async (exitReason) => {
824
+ if (sessionEndedCaptured)
825
+ return;
826
+ sessionEndedCaptured = true;
827
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_SESSION_ENDED, {
828
+ sessionDurationMs: Date.now() - sessionStartedAt,
829
+ commandsAttempted,
830
+ commandsSucceeded,
831
+ commandsFailed,
832
+ exitReason,
833
+ });
834
+ };
566
835
  await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_STARTED);
836
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_COMMAND_STARTED, {
837
+ commandName: telemetryCommandName,
838
+ commandArgsCount: Math.max(argv.length - 1, 0),
839
+ });
840
+ commandsAttempted += 1;
567
841
  try {
568
842
  const { command, flags, positionals } = parseArgs(argv);
569
843
  activeCommand = command;
570
844
  if (command === "help" || command === "--help" || command === "-h") {
571
845
  printHelp(io);
846
+ await captureCommandCompleted();
572
847
  return 0;
573
848
  }
574
849
  if (command === "login") {
@@ -581,20 +856,23 @@ export async function runCareCompassCli(argv = process.argv.slice(2), io = {
581
856
  errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.AUTHENTICATION_FAILED,
582
857
  failureCategory: "authentication",
583
858
  });
859
+ commandsFailed += 1;
584
860
  return 1;
585
861
  }
586
862
  const credentialsPath = resolveCredentialsPath(io.env);
587
863
  writeStoredApiKey(apiKey, credentialsPath);
588
864
  io.stdout.write(`Stored CareCompass credentials at ${credentialsPath}\n`);
589
865
  await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_LOGIN_SUCCEEDED);
866
+ await captureCommandCompleted();
590
867
  return 0;
591
868
  }
592
869
  if (command === "init") {
870
+ const initStartedAt = Date.now();
593
871
  await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_INIT_STARTED);
594
872
  rejectLegacyBaaInitFlags(flags, io);
595
873
  const outputDir = flagString(flags, "output-dir") ?? DEFAULT_OUTPUT_DIR;
596
- const apiBaseUrl = flagString(flags, "api-base-url") ?? DEFAULT_API_BASE_URL;
597
- const initAuth = await resolveInitApiKey(flags, io, apiBaseUrl);
874
+ const apiBaseUrl = resolveTrustedApiBaseUrl(flagString(flags, "api-base-url") ?? DEFAULT_API_BASE_URL);
875
+ const initAuth = await resolveInitApiKey(flags, io, apiBaseUrl, captureAuthChecked);
598
876
  const output = flagString(flags, "output") ??
599
877
  join(outputDir, "carecompass.config.json");
600
878
  const snippetOutput = flagString(flags, "snippet-output") ??
@@ -657,7 +935,13 @@ Step 5: Follow the generated setup guide
657
935
  Next command:
658
936
  npx @nexalink/carecompass-sdk validate --config ${output}
659
937
  `);
938
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_INIT_COMPLETED, {
939
+ initDurationMs: Date.now() - initStartedAt,
940
+ authMethod: initAuth.authMethod,
941
+ baaStatus: initAuth.baaStatus,
942
+ });
660
943
  await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_SETUP_KIT_GENERATED);
944
+ await captureCommandCompleted();
661
945
  return 0;
662
946
  }
663
947
  if (command === "validate") {
@@ -670,12 +954,14 @@ Next command:
670
954
  }
671
955
  io.stdout.write(`CareCompass config is valid for ${config.payerId}. Credentials loaded from ${credentials.source}.\n`);
672
956
  await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_VALIDATION_SUCCEEDED);
957
+ await captureCommandCompleted();
673
958
  return 0;
674
959
  }
675
960
  catch (error) {
961
+ const failure = classifyCliFailure(error);
676
962
  await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_VALIDATION_FAILED, {
677
- errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.CLI_COMMAND_FAILED,
678
- failureCategory: "configuration",
963
+ errorCode: failure.errorCode,
964
+ failureCategory: failure.failureCategory,
679
965
  });
680
966
  throw error;
681
967
  }
@@ -697,6 +983,7 @@ Next command:
697
983
  io.stdout.write(`${snippet}\n`);
698
984
  }
699
985
  await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_SNIPPET_GENERATED);
986
+ await captureCommandCompleted();
700
987
  return 0;
701
988
  }
702
989
  if (command === "doctor") {
@@ -723,24 +1010,31 @@ Hosted API: ${hostedStatus}
723
1010
  Status: ready for staging verification
724
1011
  `);
725
1012
  await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_DOCTOR_COMPLETED);
1013
+ await captureCommandCompleted();
726
1014
  return 0;
727
1015
  }
728
1016
  throw new Error(`Unknown command: ${command}`);
729
1017
  }
730
1018
  catch (error) {
1019
+ commandsFailed += 1;
1020
+ const failure = classifyCliFailure(error);
731
1021
  if (activeCommand === "init") {
732
1022
  await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_INIT_FAILED, {
733
- errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.CLI_COMMAND_FAILED,
734
- failureCategory: "unknown",
1023
+ errorCode: failure.errorCode,
1024
+ failureCategory: failure.failureCategory,
735
1025
  });
736
1026
  }
737
1027
  await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_COMMAND_FAILED, {
738
- errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.CLI_COMMAND_FAILED,
739
- failureCategory: "unknown",
1028
+ errorCode: failure.errorCode,
1029
+ failureCategory: failure.failureCategory,
1030
+ commandName: telemetryCommandName,
740
1031
  });
741
1032
  io.stderr.write(`${error.message}\n`);
742
1033
  return 1;
743
1034
  }
1035
+ finally {
1036
+ await captureSessionEnded(commandsFailed > 0 ? "error" : "user_exit");
1037
+ }
744
1038
  }
745
1039
  const isMainModule = () => Boolean(process.argv[1]) &&
746
1040
  realpathSync(fileURLToPath(import.meta.url)) ===
package/dist/snippet.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { CareCompassConfig } from "./schema.js";
1
+ import { type CareCompassConfig } from "./schema.js";
2
2
  export type CareCompassSnippetMode = "bundler" | "cdn";
3
3
  export declare function generateCareCompassIframeSnippet(config: CareCompassConfig, options?: {
4
4
  src?: string;
package/dist/snippet.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { careCompassThemeSchema } from "./schema.js";
1
2
  const SCRIPT_JSON_ESCAPES = {
2
3
  "<": "\\u003c",
3
4
  ">": "\\u003e",
@@ -16,12 +17,14 @@ const serializeScriptJson = (value) => JSON.stringify(value, null, 2).replace(/[
16
17
  export function generateCareCompassIframeSnippet(config, options = {}) {
17
18
  const src = options.src ?? "/carecompass/carecompass-embed.html";
18
19
  const title = `${config.branding.displayName} CareCompass`;
20
+ const theme = careCompassThemeSchema.safeParse(config.theme);
21
+ const borderRadius = theme.success ? theme.data.borderRadius : "8px";
19
22
  // The iframe form is intentionally static so it can be pasted into CMS pages
20
23
  // or server-rendered apps without needing a bundler.
21
24
  return `<iframe
22
25
  title="${escapeHtmlAttribute(title)}"
23
26
  src="${escapeHtmlAttribute(src)}"
24
- style="width: 100%; min-height: 640px; border: 0; border-radius: ${config.theme.borderRadius};"
27
+ style="width: 100%; min-height: 640px; border: 0; border-radius: ${borderRadius};"
25
28
  loading="lazy"
26
29
  ></iframe>`;
27
30
  }
@@ -21,6 +21,11 @@ export declare const CARECOMPASS_TELEMETRY_EVENTS: {
21
21
  readonly CLI_LOGIN_FAILED: "carecompass_cli_login_failed";
22
22
  readonly CLI_INIT_STARTED: "carecompass_cli_init_started";
23
23
  readonly CLI_INIT_FAILED: "carecompass_cli_init_failed";
24
+ readonly CLI_INIT_COMPLETED: "carecompass_cli_init_completed";
25
+ readonly CLI_COMMAND_STARTED: "carecompass_cli_command_started";
26
+ readonly CLI_COMMAND_COMPLETED: "carecompass_cli_command_completed";
27
+ readonly CLI_SESSION_ENDED: "carecompass_cli_session_ended";
28
+ readonly CLI_AUTH_CHECKED: "carecompass_cli_auth_checked";
24
29
  readonly CLI_SETUP_KIT_GENERATED: "carecompass_cli_setup_kit_generated";
25
30
  readonly CLI_VALIDATION_SUCCEEDED: "carecompass_cli_validation_succeeded";
26
31
  readonly CLI_VALIDATION_FAILED: "carecompass_cli_validation_failed";
@@ -41,14 +46,50 @@ export declare const CARECOMPASS_TELEMETRY_ERROR_CODES: {
41
46
  readonly RATE_LIMITED: "rate_limited";
42
47
  readonly RENDER_FAILED: "render_failed";
43
48
  readonly CLI_COMMAND_FAILED: "cli_command_failed";
49
+ readonly AUTH_KEY_MISSING: "AUTH_KEY_MISSING";
50
+ readonly AUTH_KEY_INVALID: "AUTH_KEY_INVALID";
51
+ readonly AUTH_KEY_EXPIRED: "AUTH_KEY_EXPIRED";
52
+ readonly BAA_REQUIRED: "BAA_REQUIRED";
53
+ readonly NET_TIMEOUT: "NET_TIMEOUT";
54
+ readonly NET_UNREACHABLE: "NET_UNREACHABLE";
55
+ readonly CONFIG_NOT_FOUND: "CONFIG_NOT_FOUND";
56
+ readonly CONFIG_INVALID: "CONFIG_INVALID";
57
+ readonly CONFIG_ALREADY_EXISTS: "CONFIG_ALREADY_EXISTS";
58
+ readonly PERMISSION_DENIED: "PERMISSION_DENIED";
59
+ readonly MODEL_LOAD_FAILED: "MODEL_LOAD_FAILED";
60
+ readonly MODEL_INFERENCE_FAILED: "MODEL_INFERENCE_FAILED";
61
+ readonly RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED";
62
+ readonly CLI_COMMAND_CANCELLED: "CLI_COMMAND_CANCELLED";
63
+ readonly CLI_COMMAND_UNKNOWN: "CLI_COMMAND_UNKNOWN";
64
+ readonly UNKNOWN_ERROR: "UNKNOWN_ERROR";
44
65
  };
45
66
  export type CareCompassTelemetryErrorCode = (typeof CARECOMPASS_TELEMETRY_ERROR_CODES)[keyof typeof CARECOMPASS_TELEMETRY_ERROR_CODES];
46
- export type CareCompassTelemetryFailureCategory = "authentication" | "configuration" | "inference" | "network" | "rate_limit" | "rendering" | "timeout" | "unknown";
67
+ export type CareCompassTelemetryFailureCategory = "authentication" | "auth_error" | "baa_not_signed" | "configuration" | "config_error" | "inference" | "model_error" | "network" | "network_error" | "permission_error" | "rate_limit" | "rate_limit_error" | "rendering" | "timeout" | "unknown";
68
+ export type CareCompassTelemetryBaaStatus = "signed" | "pending" | "not_required";
69
+ export type CareCompassTelemetryAuthMethod = "api_key" | "env_var" | "config_file";
70
+ export type CareCompassTelemetryKeySource = "env_var" | "config_file" | "cli_flag";
71
+ export type CareCompassTelemetryExitReason = "user_exit" | "error" | "timeout" | "signal";
72
+ export type CareCompassTelemetryAuthResult = "success" | "failure";
47
73
  export type CareCompassTelemetryDurationBucket = "under_250_ms" | "250_to_999_ms" | "1_to_4_999_ms" | "5_to_29_999_ms" | "30_s_or_more";
48
74
  export type CareCompassTelemetryCaptureOptions = {
49
75
  durationMs?: number;
50
76
  errorCode?: CareCompassTelemetryErrorCode;
51
- failureCategory?: CareCompassTelemetryFailureCategory;
77
+ failureCategory?: CareCompassTelemetryFailureCategory | null;
78
+ initDurationMs?: number;
79
+ authMethod?: CareCompassTelemetryAuthMethod;
80
+ baaStatus?: CareCompassTelemetryBaaStatus;
81
+ commandName?: string;
82
+ commandArgsCount?: number;
83
+ commandDurationMs?: number;
84
+ tokensUsed?: number | null;
85
+ modelId?: string | null;
86
+ sessionDurationMs?: number;
87
+ commandsAttempted?: number;
88
+ commandsSucceeded?: number;
89
+ commandsFailed?: number;
90
+ exitReason?: CareCompassTelemetryExitReason;
91
+ authResult?: CareCompassTelemetryAuthResult;
92
+ keySource?: CareCompassTelemetryKeySource;
52
93
  };
53
94
  type Fetcher = typeof fetch;
54
95
  export type CareCompassTelemetry = {
package/dist/telemetry.js CHANGED
@@ -21,6 +21,11 @@ export const CARECOMPASS_TELEMETRY_EVENTS = {
21
21
  CLI_LOGIN_FAILED: "carecompass_cli_login_failed",
22
22
  CLI_INIT_STARTED: "carecompass_cli_init_started",
23
23
  CLI_INIT_FAILED: "carecompass_cli_init_failed",
24
+ CLI_INIT_COMPLETED: "carecompass_cli_init_completed",
25
+ CLI_COMMAND_STARTED: "carecompass_cli_command_started",
26
+ CLI_COMMAND_COMPLETED: "carecompass_cli_command_completed",
27
+ CLI_SESSION_ENDED: "carecompass_cli_session_ended",
28
+ CLI_AUTH_CHECKED: "carecompass_cli_auth_checked",
24
29
  CLI_SETUP_KIT_GENERATED: "carecompass_cli_setup_kit_generated",
25
30
  CLI_VALIDATION_SUCCEEDED: "carecompass_cli_validation_succeeded",
26
31
  CLI_VALIDATION_FAILED: "carecompass_cli_validation_failed",
@@ -40,6 +45,22 @@ export const CARECOMPASS_TELEMETRY_ERROR_CODES = {
40
45
  RATE_LIMITED: "rate_limited",
41
46
  RENDER_FAILED: "render_failed",
42
47
  CLI_COMMAND_FAILED: "cli_command_failed",
48
+ AUTH_KEY_MISSING: "AUTH_KEY_MISSING",
49
+ AUTH_KEY_INVALID: "AUTH_KEY_INVALID",
50
+ AUTH_KEY_EXPIRED: "AUTH_KEY_EXPIRED",
51
+ BAA_REQUIRED: "BAA_REQUIRED",
52
+ NET_TIMEOUT: "NET_TIMEOUT",
53
+ NET_UNREACHABLE: "NET_UNREACHABLE",
54
+ CONFIG_NOT_FOUND: "CONFIG_NOT_FOUND",
55
+ CONFIG_INVALID: "CONFIG_INVALID",
56
+ CONFIG_ALREADY_EXISTS: "CONFIG_ALREADY_EXISTS",
57
+ PERMISSION_DENIED: "PERMISSION_DENIED",
58
+ MODEL_LOAD_FAILED: "MODEL_LOAD_FAILED",
59
+ MODEL_INFERENCE_FAILED: "MODEL_INFERENCE_FAILED",
60
+ RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
61
+ CLI_COMMAND_CANCELLED: "CLI_COMMAND_CANCELLED",
62
+ CLI_COMMAND_UNKNOWN: "CLI_COMMAND_UNKNOWN",
63
+ UNKNOWN_ERROR: "UNKNOWN_ERROR",
43
64
  };
44
65
  const DEPLOYMENT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{2,127}$/;
45
66
  const POSTHOG_PROJECT_TOKEN_RE = /^phc_[A-Za-z0-9_-]{8,}$/;
@@ -86,6 +107,8 @@ export const careCompassTelemetryDurationBucket = (durationMs) => {
86
107
  return "5_to_29_999_ms";
87
108
  return "30_s_or_more";
88
109
  };
110
+ const integerMs = (value) => Math.max(0, Math.round(value));
111
+ const integerCount = (value) => Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
89
112
  function createTelemetry(context, options = {}) {
90
113
  const projectToken = CARECOMPASS_DEFAULT_POSTHOG_PROJECT_TOKEN;
91
114
  // Invalid identifiers disable telemetry instead of producing malformed events
@@ -120,12 +143,71 @@ function createTelemetry(context, options = {}) {
120
143
  : {
121
144
  duration_bucket: careCompassTelemetryDurationBucket(captureOptions.durationMs),
122
145
  }),
123
- ...(captureOptions.failureCategory
124
- ? { failure_category: captureOptions.failureCategory }
125
- : {}),
146
+ ...(captureOptions.failureCategory === undefined
147
+ ? {}
148
+ : { failure_category: captureOptions.failureCategory }),
126
149
  ...(captureOptions.errorCode
127
150
  ? { error_code: captureOptions.errorCode }
128
151
  : {}),
152
+ ...(captureOptions.initDurationMs === undefined
153
+ ? {}
154
+ : { init_duration_ms: integerMs(captureOptions.initDurationMs) }),
155
+ ...(captureOptions.authMethod
156
+ ? { auth_method: captureOptions.authMethod }
157
+ : {}),
158
+ ...(captureOptions.baaStatus
159
+ ? { baa_status: captureOptions.baaStatus }
160
+ : {}),
161
+ ...(captureOptions.commandName
162
+ ? { command_name: captureOptions.commandName }
163
+ : {}),
164
+ ...(captureOptions.commandArgsCount === undefined
165
+ ? {}
166
+ : {
167
+ command_args_count: integerCount(captureOptions.commandArgsCount),
168
+ }),
169
+ ...(captureOptions.commandDurationMs === undefined
170
+ ? {}
171
+ : {
172
+ command_duration_ms: integerMs(captureOptions.commandDurationMs),
173
+ }),
174
+ ...(captureOptions.tokensUsed === undefined
175
+ ? {}
176
+ : {
177
+ tokens_used: captureOptions.tokensUsed === null
178
+ ? null
179
+ : integerCount(captureOptions.tokensUsed),
180
+ }),
181
+ ...(captureOptions.modelId === undefined
182
+ ? {}
183
+ : { model_id: captureOptions.modelId }),
184
+ ...(captureOptions.sessionDurationMs === undefined
185
+ ? {}
186
+ : {
187
+ session_duration_ms: integerMs(captureOptions.sessionDurationMs),
188
+ }),
189
+ ...(captureOptions.commandsAttempted === undefined
190
+ ? {}
191
+ : {
192
+ commands_attempted: integerCount(captureOptions.commandsAttempted),
193
+ }),
194
+ ...(captureOptions.commandsSucceeded === undefined
195
+ ? {}
196
+ : {
197
+ commands_succeeded: integerCount(captureOptions.commandsSucceeded),
198
+ }),
199
+ ...(captureOptions.commandsFailed === undefined
200
+ ? {}
201
+ : { commands_failed: integerCount(captureOptions.commandsFailed) }),
202
+ ...(captureOptions.exitReason
203
+ ? { exit_reason: captureOptions.exitReason }
204
+ : {}),
205
+ ...(captureOptions.authResult
206
+ ? { auth_result: captureOptions.authResult }
207
+ : {}),
208
+ ...(captureOptions.keySource
209
+ ? { key_source: captureOptions.keySource }
210
+ : {}),
129
211
  };
130
212
  const controller = new AbortController();
131
213
  const timeout = setTimeout(() => controller.abort(), TELEMETRY_CAPTURE_TIMEOUT_MS);
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
- export const CARECOMPASS_SDK_VERSION = "0.2.0-beta.13"; // x-release-please-version
1
+ export const CARECOMPASS_SDK_VERSION = "0.3.0-beta.15"; // x-release-please-version
2
2
  export const CARECOMPASS_COMPONENT_VERSION = "1.0.0";
3
3
  export const CARECOMPASS_TELEMETRY_CONTRACT_VERSION = "1.0.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexalink/carecompass-sdk",
3
- "version": "0.2.0-beta.14",
3
+ "version": "0.3.0-beta.15",
4
4
  "description": "Embeddable CareCompass SDK and CLI for healthcare ecosystem integrations with Nexalink onboarding, BAA coverage, and API keys.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.9",
@@ -64,13 +64,13 @@
64
64
  "react-dom": "^18.3.0 || ^19.0.0"
65
65
  },
66
66
  "devDependencies": {
67
+ "@eslint/js": "^9.39.4",
67
68
  "@testing-library/jest-dom": "^6.9.1",
68
69
  "@testing-library/react": "^16.3.2",
69
70
  "@types/jest": "^30.0.0",
70
- "@types/node": "^24.12.4",
71
+ "@types/node": "^25.9.3",
71
72
  "@types/react": "^19.2.15",
72
73
  "@types/react-dom": "^19.2.3",
73
- "@eslint/js": "^9.39.4",
74
74
  "eslint": "^9.39.4",
75
75
  "globals": "^16.5.0",
76
76
  "jest": "^30.4.2",
@@ -79,8 +79,11 @@
79
79
  "react": "^19.2.6",
80
80
  "react-dom": "^19.2.6",
81
81
  "ts-jest": "^29.4.1",
82
- "typescript-eslint": "^8.48.1",
83
- "typescript": "^5.9.3"
82
+ "typescript": "^5.9.3",
83
+ "typescript-eslint": "^8.48.1"
84
+ },
85
+ "overrides": {
86
+ "js-yaml": "4.2.0"
84
87
  },
85
88
  "publishConfig": {
86
89
  "access": "public",