@nexalink/carecompass-sdk 0.1.0-beta.9 → 0.2.0-beta.11

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/dist/cli.js CHANGED
@@ -1,26 +1,36 @@
1
1
  #!/usr/bin/env node
2
+ import { randomUUID } from "node:crypto";
2
3
  import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync, } from "node:fs";
3
4
  import { dirname, join, resolve } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { createDefaultCareCompassConfig, validateCareCompassConfig, } from "./schema.js";
6
- import { resolveApiKey, resolveCredentialsPath, writeStoredApiKey, } from "./credentials.js";
7
+ import { normalizeApiKey, resolveApiKey, resolveCredentialsPath, writeStoredApiKey, } from "./credentials.js";
7
8
  import { generateCareCompassIframeSnippet, generateCareCompassSnippet, } from "./snippet.js";
9
+ import { CARECOMPASS_TELEMETRY_ERROR_CODES, CARECOMPASS_TELEMETRY_EVENTS, createCareCompassCliTelemetry, } from "./telemetry.js";
8
10
  const DEFAULT_OUTPUT_DIR = "carecompass";
9
11
  const DEFAULT_CONFIG_PATH = join(DEFAULT_OUTPUT_DIR, "carecompass.config.json");
10
12
  const DEFAULT_SNIPPET_PATH = join(DEFAULT_OUTPUT_DIR, "carecompass-embed.html");
11
- const DEFAULT_IFRAME_PATH = join(DEFAULT_OUTPUT_DIR, "carecompass-iframe.html");
12
- const DEFAULT_SETUP_GUIDE_PATH = join(DEFAULT_OUTPUT_DIR, "README.md");
13
13
  const DEFAULT_PROMPT_PATH = "system-prompt.md";
14
- const DEFAULT_SIGNUP_URL = "https://nexalink.care/signup?group=payer";
14
+ const DEFAULT_DEVELOPER_SIGNUP_URL = "https://www.nexalink.care/developer/signup";
15
15
  const DEFAULT_API_BASE_URL = "https://www.nexalink.care";
16
16
  const DEFAULT_SNIPPET_MODE = "bundler";
17
- const DEFAULT_SYSTEM_PROMPT = `You are CareCompass, a payer-approved care navigation assistant.
17
+ const LEGACY_BAA_INIT_FLAGS = [
18
+ "baa-action",
19
+ "baa-output",
20
+ "baa-template-output",
21
+ "legal-entity-name",
22
+ "signer-name",
23
+ "signer-email",
24
+ "signer-title",
25
+ "baa-confirmation",
26
+ ];
27
+ const DEFAULT_SYSTEM_PROMPT = `You are CareCompass, a customer-approved healthcare navigation assistant.
18
28
 
19
29
  Help members understand benefits, coverage navigation, in-network care options,
20
30
  support resources, and appropriate next steps. Stay out of diagnosis, treatment
21
31
  planning, emergency triage, legal advice, and coverage guarantees. When a member
22
- needs clinical, emergency, or plan-specific help that requires a human, route
23
- them to the payer's approved support resources.`;
32
+ needs clinical, emergency, plan-specific, or account-specific help that requires
33
+ a human, route them to the customer's approved support resources.`;
24
34
  function parseArgs(argv) {
25
35
  const [command = "help", ...rest] = argv;
26
36
  const flags = {};
@@ -28,6 +38,12 @@ function parseArgs(argv) {
28
38
  for (let index = 0; index < rest.length; index += 1) {
29
39
  const token = rest[index];
30
40
  if (!token.startsWith("--")) {
41
+ const trimmedToken = token.trimStart();
42
+ if (trimmedToken.startsWith("-")) {
43
+ // This catches a common copy/paste failure where a continued shell line
44
+ // leaves whitespace before a flag and turns it into a positional arg.
45
+ throw new Error(`Unexpected argument "${trimmedToken}". Flags must not have leading whitespace. When splitting a command across lines, place \\ immediately before the newline.`);
46
+ }
31
47
  positionals.push(token);
32
48
  continue;
33
49
  }
@@ -65,14 +81,274 @@ function requireApiKey(flags, io) {
65
81
  const credentialsPath = resolveCredentialsPath(io.env);
66
82
  const credentials = resolveApiKey(flagString(flags, "api-key"), io.env, credentialsPath);
67
83
  if (!credentials.apiKey) {
68
- throw new Error("Missing Nexalink API key. Run `npx @nexalink/carecompass-sdk login --api-key <key>` or set CARECOMPASS_API_KEY.");
84
+ throw new Error("Missing Nexalink API key. Run `npx @nexalink/carecompass-sdk init --api-key <key>` or set CARECOMPASS_API_KEY.");
69
85
  }
70
86
  return { ...credentials, apiKey: credentials.apiKey };
71
87
  }
88
+ async function promptFor(io, question) {
89
+ if (io.prompt)
90
+ return (await io.prompt(question)).trim();
91
+ if (!io.stdin) {
92
+ throw new Error("Missing Nexalink API key. Re-run `npx @nexalink/carecompass-sdk init --api-key <key>` in a terminal or set CARECOMPASS_API_KEY.");
93
+ }
94
+ const { createInterface } = await import("node:readline/promises");
95
+ const readline = createInterface({
96
+ input: io.stdin,
97
+ output: io.stdout,
98
+ });
99
+ try {
100
+ return (await readline.question(question)).trim();
101
+ }
102
+ finally {
103
+ readline.close();
104
+ }
105
+ }
106
+ async function promptForSecret(io, question) {
107
+ if (io.prompt)
108
+ return (await io.prompt(question)).trim();
109
+ const stdin = io.stdin;
110
+ if (!stdin?.isTTY || typeof stdin.setRawMode !== "function") {
111
+ // Test harnesses and non-interactive shells use the regular prompt path;
112
+ // interactive terminals get raw-mode input so the secret is not echoed.
113
+ return promptFor(io, question);
114
+ }
115
+ return new Promise((resolveValue, reject) => {
116
+ let answer = "";
117
+ const cleanup = () => {
118
+ stdin.off("data", onData);
119
+ stdin.setRawMode?.(false);
120
+ io.stdout.write("\n");
121
+ };
122
+ const finish = () => {
123
+ cleanup();
124
+ resolveValue(answer.trim());
125
+ };
126
+ const cancel = () => {
127
+ cleanup();
128
+ reject(new Error("CareCompass init cancelled."));
129
+ };
130
+ const onData = (chunk) => {
131
+ const value = chunk.toString("utf8");
132
+ for (const character of value) {
133
+ if (character === "\u0003") {
134
+ cancel();
135
+ return;
136
+ }
137
+ if (character === "\r" || character === "\n") {
138
+ finish();
139
+ return;
140
+ }
141
+ if (character === "\u007f" || character === "\b") {
142
+ answer = answer.slice(0, -1);
143
+ continue;
144
+ }
145
+ if (character >= " ") {
146
+ answer += character;
147
+ }
148
+ }
149
+ };
150
+ io.stdout.write(question);
151
+ stdin.setRawMode(true);
152
+ stdin.resume();
153
+ stdin.on("data", onData);
154
+ });
155
+ }
156
+ const normalizeAccountType = (value) => {
157
+ const normalized = value?.trim().toLowerCase();
158
+ if (normalized === "enterprise")
159
+ return "enterprise";
160
+ if (normalized === "developer" || normalized === "1")
161
+ return "developer";
162
+ return null;
163
+ };
164
+ const isExitInput = (value) => ["exit", "cancel", "quit", "q", "2"].includes(value.trim().toLowerCase());
165
+ const enterpriseUnavailableMessage = (developerSignupUrl) => `Enterprise signup is temporarily unavailable in the CLI. Use the developer flow for now:
166
+ Developer: ${developerSignupUrl}`;
167
+ async function selectAccountType(flags, io) {
168
+ const configuredType = flagString(flags, "account-type") ?? io.env.CARECOMPASS_ACCOUNT_TYPE;
169
+ if (configuredType) {
170
+ const accountType = normalizeAccountType(configuredType);
171
+ if (!accountType) {
172
+ throw new Error("--account-type must be developer.");
173
+ }
174
+ if (accountType === "enterprise") {
175
+ throw new Error(enterpriseUnavailableMessage(signupUrls(flags, io).developer));
176
+ }
177
+ return accountType;
178
+ }
179
+ while (true) {
180
+ const answer = await promptFor(io, `Select account type:
181
+ 1. Developer
182
+ 2. Exit
183
+
184
+ Choice: `);
185
+ if (isExitInput(answer))
186
+ throw new Error("CareCompass init cancelled.");
187
+ const accountType = normalizeAccountType(answer);
188
+ if (accountType === "enterprise") {
189
+ throw new Error(enterpriseUnavailableMessage(signupUrls(flags, io).developer));
190
+ }
191
+ if (accountType)
192
+ return accountType;
193
+ io.stdout.write("Choose 1 for Developer or 2 to exit.\n");
194
+ }
195
+ }
196
+ async function promptForAccountExists(io, accountType) {
197
+ while (true) {
198
+ const answer = (await promptFor(io, `Do you already have a ${accountType} account? (y/n, or type exit): `)).toLowerCase();
199
+ if (isExitInput(answer))
200
+ throw new Error("CareCompass init cancelled.");
201
+ if (answer === "y" || answer === "yes")
202
+ return true;
203
+ if (answer === "n" || answer === "no")
204
+ return false;
205
+ io.stdout.write("Answer y, n, or exit.\n");
206
+ }
207
+ }
208
+ const signupUrls = (flags, io) => ({
209
+ developer: flagString(flags, "developer-signup-url") ??
210
+ io.env.CARECOMPASS_DEVELOPER_SIGNUP_URL ??
211
+ DEFAULT_DEVELOPER_SIGNUP_URL,
212
+ });
213
+ function rejectLegacyBaaInitFlags(flags, io) {
214
+ const legacyFlag = LEGACY_BAA_INIT_FLAGS.find((flag) => flag in flags);
215
+ if (!legacyFlag)
216
+ return;
217
+ const urls = signupUrls(flags, io);
218
+ throw new Error(`BAA execution now happens in the Nexalink web app before API key creation. Remove --${legacyFlag} and complete signup instead:
219
+ Developer: ${urls.developer}`);
220
+ }
221
+ async function waitForSignupOrExit(io) {
222
+ const answer = await promptFor(io, "Press Enter after account creation to sign in, or type exit to cancel: ");
223
+ if (isExitInput(answer))
224
+ throw new Error("CareCompass init cancelled.");
225
+ }
226
+ function printSignupRoute(io, accountType, urls) {
227
+ if (accountType === "enterprise") {
228
+ io.stdout.write(`${enterpriseUnavailableMessage(urls.developer)}\n`);
229
+ return;
230
+ }
231
+ io.stdout.write(`Open ${urls.developer} to create a developer account.\n`);
232
+ }
233
+ function normalizeAuthSetup(value) {
234
+ // The auth service may return account defaults, but every field is narrowed
235
+ // before it is allowed to influence generated customer configuration.
236
+ if (!value || typeof value !== "object")
237
+ return undefined;
238
+ const source = value;
239
+ const setup = {};
240
+ if (typeof source.payerId === "string")
241
+ setup.payerId = source.payerId;
242
+ if (source.environment === "development" ||
243
+ source.environment === "staging" ||
244
+ source.environment === "production") {
245
+ setup.environment = source.environment;
246
+ }
247
+ if (typeof source.embedKey === "string")
248
+ setup.embedKey = source.embedKey;
249
+ if (Array.isArray(source.allowedOrigins) &&
250
+ source.allowedOrigins.every((origin) => typeof origin === "string")) {
251
+ setup.allowedOrigins = source.allowedOrigins;
252
+ }
253
+ return Object.keys(setup).length ? setup : undefined;
254
+ }
255
+ async function validateInitApiKey(apiBaseUrl, apiKey, accountType) {
256
+ const url = endpointUrl(apiBaseUrl, "/api/carecompass/auth/validate");
257
+ let response;
258
+ try {
259
+ response = await globalThis.fetch(url, {
260
+ method: "POST",
261
+ headers: {
262
+ authorization: `Bearer ${apiKey}`,
263
+ "content-type": "application/json",
264
+ },
265
+ body: JSON.stringify({ accountType }),
266
+ });
267
+ }
268
+ catch (error) {
269
+ const message = error instanceof Error ? error.message : "Network request failed";
270
+ throw new Error(`Could not reach CareCompass authentication API at ${url}. ${message}`);
271
+ }
272
+ const text = await response.text();
273
+ let payload = {};
274
+ try {
275
+ payload = text ? JSON.parse(text) : {};
276
+ }
277
+ catch {
278
+ payload = {};
279
+ }
280
+ if (!response.ok || payload.ok === false) {
281
+ return {
282
+ ok: false,
283
+ message: payload.error ?? "Invalid API key.",
284
+ };
285
+ }
286
+ const validatedType = typeof payload.accountType === "string"
287
+ ? normalizeAccountType(payload.accountType)
288
+ : null;
289
+ if (!validatedType) {
290
+ return {
291
+ ok: false,
292
+ message: "Credential validation succeeded but did not return a recognized account type. Contact support@nexalink.care.",
293
+ };
294
+ }
295
+ return {
296
+ ok: true,
297
+ accountType: validatedType,
298
+ setup: normalizeAuthSetup(payload.setup),
299
+ };
300
+ }
301
+ async function resolveInitApiKey(flags, io, apiBaseUrl) {
302
+ const credentialsPath = resolveCredentialsPath(io.env);
303
+ let credentials = resolveApiKey(flagString(flags, "api-key"), io.env, credentialsPath);
304
+ const urls = signupUrls(flags, io);
305
+ let useResolvedCredential = Boolean(credentials.apiKey);
306
+ while (true) {
307
+ const accountType = await selectAccountType(flags, io);
308
+ let hasAccount = useResolvedCredential
309
+ ? true
310
+ : await promptForAccountExists(io, accountType);
311
+ while (!hasAccount) {
312
+ printSignupRoute(io, accountType, urls);
313
+ await waitForSignupOrExit(io);
314
+ hasAccount = await promptForAccountExists(io, accountType);
315
+ }
316
+ const apiKey = useResolvedCredential
317
+ ? credentials.apiKey
318
+ : await promptForSecret(io, "Enter API key: ");
319
+ useResolvedCredential = false;
320
+ credentials = { apiKey: null, source: "missing" };
321
+ const normalizedApiKey = normalizeApiKey(apiKey);
322
+ if (!normalizedApiKey) {
323
+ io.stdout.write("Invalid or missing API key. Try again or type exit.\n");
324
+ continue;
325
+ }
326
+ io.stdout.write("Validating credentials...\n");
327
+ const validation = await validateInitApiKey(apiBaseUrl, normalizedApiKey, accountType);
328
+ if (!validation.ok) {
329
+ io.stdout.write(`${validation.message}\n`);
330
+ continue;
331
+ }
332
+ if (validation.accountType !== accountType) {
333
+ if (validation.accountType === "enterprise") {
334
+ io.stdout.write(`This API key belongs to an enterprise account. ${enterpriseUnavailableMessage(urls.developer)}\n`);
335
+ continue;
336
+ }
337
+ io.stdout.write(`This API key belongs to a ${validation.accountType} account. Select Developer to continue.\n`);
338
+ continue;
339
+ }
340
+ writeStoredApiKey(normalizedApiKey, credentialsPath);
341
+ io.stdout.write(`Stored CareCompass credentials at ${credentialsPath}\n`);
342
+ io.stdout.write("Sign-in successful.\n");
343
+ return { apiKey: normalizedApiKey, setup: validation.setup };
344
+ }
345
+ }
72
346
  function resolvePromptFile(config, configPath) {
73
347
  const promptFile = config.prompt.systemInstructionFile;
74
348
  if (!promptFile)
75
349
  return config;
350
+ // Prompt files are resolved relative to the config file so generated kits work
351
+ // after the caller changes directories.
76
352
  const resolvedPath = resolve(dirname(configPath), promptFile);
77
353
  const systemInstruction = readFileSync(resolvedPath, "utf8").trim();
78
354
  const validation = validateCareCompassConfig({
@@ -92,6 +368,8 @@ function runtimeConfig(config) {
92
368
  return config;
93
369
  const prompt = { ...config.prompt };
94
370
  delete prompt.systemInstructionFile;
371
+ // Browser snippets receive the prompt text, not the local prompt file path,
372
+ // because deployed embeds cannot read files from the integrator's machine.
95
373
  return {
96
374
  ...config,
97
375
  prompt,
@@ -108,16 +386,23 @@ const configHeaders = (config, apiKey, origin) => ({
108
386
  const endpointUrl = (baseUrl, path) => `${baseUrl.replace(/\/+$/, "")}${path}`;
109
387
  const setupGuide = (params) => `# CareCompass SDK Setup
110
388
 
111
- This folder contains the payer-owned CareCompass embed setup. The generated iframe is the easiest way to place the CareCompass LLM experience on a payer website.
389
+ This folder contains the CareCompass embed setup for your healthcare website or app. The generated iframe is the easiest way to place the CareCompass LLM experience where members or patients already seek help.
112
390
 
113
391
  ## Step 1: Confirm Credentials
114
392
 
115
- You need a Nexalink payer account and SDK API key before validation or runtime use. If you do not have a key, create or request payer SDK access at https://nexalink.care/signup?group=payer.
393
+ You need a Nexalink account with SDK access before validation or runtime use. Live regulated deployments require BAA coverage where PHI may be involved. If you do not have a key, create one in the Nexalink web app, where BAA execution happens before API key creation.
116
394
 
117
395
  \`\`\`bash
118
- npx @nexalink/carecompass-sdk login --api-key <key>
396
+ npx @nexalink/carecompass-sdk init --account-type developer --api-key <key>
119
397
  \`\`\`
120
398
 
399
+ The \`init\` flow routes developers without accounts to the configured signup path, validates the API key with Nexalink, stores the key locally only after successful sign-in, and generates this setup kit.
400
+
401
+ The API key and embed key serve different purposes:
402
+
403
+ - API key: secret credential for trusted setup and server-side/CLI workflows. Keep it out of browser code, generated embed HTML, frontend config, and source control.
404
+ - Embed key: public browser-facing identifier used by the CareCompass embed with the payer ID, environment, and allowed origins. It is safe to include in this generated config, but it is not a secret.
405
+
121
406
  ## Step 2: Review Config
122
407
 
123
408
  Open \`${params.configPath}\` and confirm:
@@ -126,13 +411,14 @@ Open \`${params.configPath}\` and confirm:
126
411
  - \`environment\`: ${params.config.environment}
127
412
  - \`apiBaseUrl\`: ${params.config.apiBaseUrl}
128
413
  - \`allowedOrigins\`: ${params.config.allowedOrigins.join(", ")}
129
- - \`embedKey\`: public embed key issued in the payer portal
414
+ - \`embedKey\`: public embed key issued in the Nexalink portal
415
+ - \`telemetry\`: enabled automatically with Nexalink-managed privacy-safe defaults
130
416
 
131
- The config file is safe to commit because it does not contain the secret API key.
417
+ The config file is safe to commit because it does not contain the secret API key. Telemetry does not require customer configuration and never includes prompts, responses, chat history, user context, credentials, or raw errors.
132
418
 
133
419
  ## Step 3: Edit System Prompt
134
420
 
135
- Open \`${params.promptPath}\` and replace the starter text with payer-approved instructions. Use it for plan context, support boundaries, escalation rules, and member-navigation tone.
421
+ Open \`${params.promptPath}\` and replace the starter text with customer-approved instructions. Use it for plan or program context, support boundaries, escalation rules, and member or patient navigation tone.
136
422
 
137
423
  Do not put secrets, raw PHI, diagnosis instructions, treatment instructions, or attempts to bypass Nexalink safety rules in this file.
138
424
 
@@ -142,11 +428,11 @@ Do not put secrets, raw PHI, diagnosis instructions, treatment instructions, or
142
428
  npx @nexalink/carecompass-sdk validate --config ${params.configPath}
143
429
  \`\`\`
144
430
 
145
- This checks local schema rules and verifies the config against Nexalink-hosted API routes.
431
+ This checks local schema rules and verifies the config against Nexalink-hosted API routes. The hosted validation request uses the public embed identity from the config; it does not send the secret API key from the browser runtime.
146
432
 
147
433
  ## Step 5: Place The Iframe
148
434
 
149
- Copy the iframe from \`${params.iframePath}\` into the payer page where the CareCompass LLM should appear.
435
+ Copy the iframe from \`${params.iframePath}\` into the page where the CareCompass LLM should appear.
150
436
 
151
437
  The iframe points at:
152
438
 
@@ -154,11 +440,13 @@ The iframe points at:
154
440
  ${params.iframeSrc}
155
441
  \`\`\`
156
442
 
157
- Make sure \`${params.embedPath}\` is served by the payer site at that URL. For many apps, that means copying the \`${params.outputDir}\` folder into the app's public/static assets folder before deployment.
443
+ Make sure \`${params.embedPath}\` is served by your site at that URL. For many apps, that means copying the \`${params.outputDir}\` folder into the app's public/static assets folder before deployment.
158
444
 
159
445
  ## Step 6: Test
160
446
 
161
- Load the payer page from an allowed origin and send a non-PHI test message. If the embed cannot connect, run:
447
+ Load the page from an allowed origin and send a non-PHI test message. If the embed cannot connect, run:
448
+
449
+ Nexalink hosts the CareCompass LLM runtime behind Nexalink-managed API routes. Some MVP and development model endpoints use scale-to-zero capacity to control cost, so the first request after an idle period may take several minutes while the endpoint warms up. The request is queued through Nexalink-hosted APIs during warm-up; the browser embed does not receive SageMaker credentials or call the model endpoint directly.
162
450
 
163
451
  \`\`\`bash
164
452
  npx @nexalink/carecompass-sdk doctor --config ${params.configPath}
@@ -177,7 +465,7 @@ async function assertHostedConfig(config, apiKey) {
177
465
  }
178
466
  catch (error) {
179
467
  const message = error instanceof Error ? error.message : "Network request failed";
180
- throw new Error(`Could not reach CareCompass API at ${url}. If apiBaseUrl is localhost, make sure the Nexalink Next.js app is running locally and the payer page is also local. Hosted sandboxes cannot reach your machine's localhost. ${message}`);
468
+ throw new Error(`Could not reach CareCompass API at ${url}. If apiBaseUrl is localhost, make sure the Nexalink Next.js app and integrating page are both running locally. Hosted sandboxes cannot reach your machine's localhost. ${message}`);
181
469
  }
182
470
  const text = await response.text();
183
471
  let payload = {};
@@ -203,19 +491,24 @@ function readConfig(path) {
203
491
  }
204
492
  return resolvePromptFile(validation.config, path);
205
493
  }
206
- function buildInitConfig(flags) {
494
+ function buildInitConfig(flags, setup) {
207
495
  const origins = flags.origin;
496
+ // `--origin` is repeatable, but accepting comma-separated input keeps the CLI
497
+ // usable in package scripts and dashboards that only expose single text fields.
208
498
  const allowedOrigins = Array.isArray(origins)
209
499
  ? origins
210
500
  : typeof origins === "string"
211
501
  ? origins.split(",").map((origin) => origin.trim())
212
- : ["https://example-payer.com"];
502
+ : (setup?.allowedOrigins ?? ["https://example-payer.com"]);
213
503
  return createDefaultCareCompassConfig({
214
- payerId: flagString(flags, "payer-id") ?? "payer-demo",
504
+ payerId: flagString(flags, "payer-id") ?? setup?.payerId ?? "payer-demo",
215
505
  environment: flagString(flags, "environment") ??
506
+ setup?.environment ??
216
507
  "staging",
217
508
  apiBaseUrl: flagString(flags, "api-base-url") ?? DEFAULT_API_BASE_URL,
218
- embedKey: flagString(flags, "embed-key") ?? "embed_demo_1234567890",
509
+ embedKey: flagString(flags, "embed-key") ??
510
+ setup?.embedKey ??
511
+ "embed_demo_1234567890",
219
512
  allowedOrigins,
220
513
  branding: {
221
514
  displayName: flagString(flags, "display-name") ?? "Example Health Plan",
@@ -242,50 +535,66 @@ function printHelp(io) {
242
535
  io.stdout.write(`CareCompass SDK CLI
243
536
 
244
537
  Run one-off commands with npx:
245
- npx @nexalink/carecompass-sdk login --api-key <key>
246
- npx @nexalink/carecompass-sdk init --payer-id <id> --origin <url>
538
+ npx @nexalink/carecompass-sdk init --account-type developer --api-key <key> --payer-id <id> --origin <url>
247
539
  npx @nexalink/carecompass-sdk validate --config carecompass/carecompass.config.json
248
540
  npx @nexalink/carecompass-sdk snippet --config carecompass.config.json
249
541
  npx @nexalink/carecompass-sdk doctor --config carecompass.config.json
250
542
 
251
543
  If installed globally or run from a package script:
252
- carecompass login --api-key <key>
253
- carecompass init [--payer-id <id>] [--origin <url>]
544
+ carecompass init [--account-type developer] [--api-key <key>] [--payer-id <id>] [--origin <url>]
254
545
 
255
546
  Commands:
256
- login --api-key <key>
257
- init [--payer-id <id>] [--origin <url>] [--output-dir carecompass]
547
+ init [--account-type developer] [--api-key <key>] [--payer-id <id>] [--origin <url>] [--output-dir carecompass]
258
548
  validate --config carecompass.config.json [--local-only]
259
549
  snippet --config carecompass.config.json [--output embed.html] [--snippet-mode bundler|cdn]
260
550
  doctor --config carecompass.config.json
551
+
552
+ Init validates developer sign-in before setup. BAA execution happens in the Nexalink web app before API key creation. The developer signup URL can be configured with --developer-signup-url or CARECOMPASS_DEVELOPER_SIGNUP_URL.
261
553
  `);
262
554
  }
263
555
  export async function runCareCompassCli(argv = process.argv.slice(2), io = {
264
556
  stdout: process.stdout,
265
557
  stderr: process.stderr,
266
558
  env: process.env,
559
+ stdin: process.stdin,
267
560
  }) {
561
+ const cliSessionId = randomUUID();
562
+ // Start with anonymous CLI telemetry, then rebuild it with payer context after
563
+ // a config has been generated or read.
564
+ let cliTelemetry = createCareCompassCliTelemetry(cliSessionId);
565
+ let activeCommand = "unknown";
566
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_STARTED);
268
567
  try {
269
568
  const { command, flags, positionals } = parseArgs(argv);
569
+ activeCommand = command;
270
570
  if (command === "help" || command === "--help" || command === "-h") {
271
571
  printHelp(io);
272
572
  return 0;
273
573
  }
274
574
  if (command === "login") {
575
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_LOGIN_STARTED);
275
576
  const apiKey = flagString(flags, "api-key") ?? positionals[0];
276
577
  if (!apiKey) {
277
- const signupUrl = flagString(flags, "signup-url") ?? DEFAULT_SIGNUP_URL;
278
- io.stderr.write(`Missing --api-key. Create a Nexalink payer account or request SDK access at ${signupUrl}\n`);
578
+ const signupUrl = flagString(flags, "signup-url") ?? DEFAULT_DEVELOPER_SIGNUP_URL;
579
+ io.stderr.write(`Missing --api-key. Create a Nexalink account or request SDK access at ${signupUrl}\n`);
580
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_LOGIN_FAILED, {
581
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.AUTHENTICATION_FAILED,
582
+ failureCategory: "authentication",
583
+ });
279
584
  return 1;
280
585
  }
281
586
  const credentialsPath = resolveCredentialsPath(io.env);
282
587
  writeStoredApiKey(apiKey, credentialsPath);
283
588
  io.stdout.write(`Stored CareCompass credentials at ${credentialsPath}\n`);
589
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_LOGIN_SUCCEEDED);
284
590
  return 0;
285
591
  }
286
592
  if (command === "init") {
287
- requireApiKey(flags, io);
593
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_INIT_STARTED);
594
+ rejectLegacyBaaInitFlags(flags, io);
288
595
  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);
289
598
  const output = flagString(flags, "output") ??
290
599
  join(outputDir, "carecompass.config.json");
291
600
  const snippetOutput = flagString(flags, "snippet-output") ??
@@ -308,7 +617,8 @@ export async function runCareCompassCli(argv = process.argv.slice(2), io = {
308
617
  }
309
618
  }
310
619
  mkdirSync(outputDir, { recursive: true });
311
- const config = buildInitConfig(flags);
620
+ const config = buildInitConfig(flags, initAuth.setup);
621
+ cliTelemetry = createCareCompassCliTelemetry(cliSessionId, config);
312
622
  writeFileSync(output, `${JSON.stringify(config, null, 2)}\n`);
313
623
  writeFileSync(promptOutput, `${DEFAULT_SYSTEM_PROMPT}\n`);
314
624
  const resolvedConfig = readConfig(output);
@@ -332,7 +642,7 @@ export async function runCareCompassCli(argv = process.argv.slice(2), io = {
332
642
  Step 1: Review config
333
643
  ${output}
334
644
 
335
- Step 2: Edit the payer-approved system prompt
645
+ Step 2: Edit the customer-approved system prompt
336
646
  ${promptOutput}
337
647
 
338
648
  Step 3: Serve the generated embed page from your site
@@ -347,20 +657,33 @@ Step 5: Follow the generated setup guide
347
657
  Next command:
348
658
  npx @nexalink/carecompass-sdk validate --config ${output}
349
659
  `);
660
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_SETUP_KIT_GENERATED);
350
661
  return 0;
351
662
  }
352
663
  if (command === "validate") {
353
- const credentials = requireApiKey(flags, io);
354
- const config = readConfig(flagString(flags, "config") ?? DEFAULT_CONFIG_PATH);
355
- if (flags["local-only"] !== true) {
356
- await assertHostedConfig(config, credentials.apiKey);
664
+ try {
665
+ const credentials = requireApiKey(flags, io);
666
+ const config = readConfig(flagString(flags, "config") ?? DEFAULT_CONFIG_PATH);
667
+ cliTelemetry = createCareCompassCliTelemetry(cliSessionId, config);
668
+ if (flags["local-only"] !== true) {
669
+ await assertHostedConfig(config, credentials.apiKey);
670
+ }
671
+ io.stdout.write(`CareCompass config is valid for ${config.payerId}. Credentials loaded from ${credentials.source}.\n`);
672
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_VALIDATION_SUCCEEDED);
673
+ return 0;
674
+ }
675
+ catch (error) {
676
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_VALIDATION_FAILED, {
677
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.CLI_COMMAND_FAILED,
678
+ failureCategory: "configuration",
679
+ });
680
+ throw error;
357
681
  }
358
- io.stdout.write(`CareCompass config is valid for ${config.payerId}. Credentials loaded from ${credentials.source}.\n`);
359
- return 0;
360
682
  }
361
683
  if (command === "snippet") {
362
684
  requireApiKey(flags, io);
363
685
  const config = readConfig(flagString(flags, "config") ?? DEFAULT_CONFIG_PATH);
686
+ cliTelemetry = createCareCompassCliTelemetry(cliSessionId, config);
364
687
  const snippet = generateCareCompassSnippet(runtimeConfig(config), {
365
688
  mode: snippetMode(flags),
366
689
  });
@@ -373,11 +696,13 @@ Next command:
373
696
  else {
374
697
  io.stdout.write(`${snippet}\n`);
375
698
  }
699
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_SNIPPET_GENERATED);
376
700
  return 0;
377
701
  }
378
702
  if (command === "doctor") {
379
703
  const credentials = requireApiKey(flags, io);
380
704
  const config = readConfig(flagString(flags, "config") ?? DEFAULT_CONFIG_PATH);
705
+ cliTelemetry = createCareCompassCliTelemetry(cliSessionId, config);
381
706
  let hostedStatus = "not checked";
382
707
  try {
383
708
  await assertHostedConfig(config, credentials.apiKey);
@@ -392,15 +717,27 @@ Payer: ${config.payerId}
392
717
  Environment: ${config.environment}
393
718
  API base URL: ${config.apiBaseUrl}
394
719
  Allowed origins: ${config.allowedOrigins.join(", ")}
720
+ Telemetry: enabled automatically with Nexalink-managed privacy-safe defaults
395
721
  Credentials: ${credentials.source}
396
722
  Hosted API: ${hostedStatus}
397
723
  Status: ready for staging verification
398
724
  `);
725
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_DOCTOR_COMPLETED);
399
726
  return 0;
400
727
  }
401
728
  throw new Error(`Unknown command: ${command}`);
402
729
  }
403
730
  catch (error) {
731
+ if (activeCommand === "init") {
732
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_INIT_FAILED, {
733
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.CLI_COMMAND_FAILED,
734
+ failureCategory: "unknown",
735
+ });
736
+ }
737
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_COMMAND_FAILED, {
738
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.CLI_COMMAND_FAILED,
739
+ failureCategory: "unknown",
740
+ });
404
741
  io.stderr.write(`${error.message}\n`);
405
742
  return 1;
406
743
  }
@@ -413,4 +750,3 @@ if (isMainModule()) {
413
750
  process.exitCode = code;
414
751
  });
415
752
  }
416
- //# sourceMappingURL=cli.js.map
package/dist/client.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 CareCompassAskRequest = {
3
3
  prompt: string;
4
4
  sessionId?: string;
@@ -12,13 +12,15 @@ export type CareCompassStatusResponse = {
12
12
  result?: string;
13
13
  };
14
14
  type Fetcher = typeof fetch;
15
- export declare function createCareCompassClient(config: CareCompassConfig, options?: {
15
+ export declare function createCareCompassClient(inputConfig: CareCompassConfig, options?: {
16
16
  fetcher?: Fetcher;
17
+ now?: () => number;
17
18
  }): {
18
19
  validateConfig: () => Promise<{
19
20
  ok: true;
20
21
  }>;
21
22
  ask: (request: CareCompassAskRequest) => Promise<CareCompassAskResponse>;
22
23
  status: (jobId: string) => Promise<CareCompassStatusResponse>;
24
+ recordInferenceTimeout: (jobId: string, durationMs: number) => void;
23
25
  };
24
26
  export {};