@nexalink/carecompass-sdk 0.1.0-beta.8 → 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/CHANGELOG.md +37 -0
- package/LICENSE +2 -1
- package/README.md +150 -39
- package/RELEASE.md +65 -0
- package/RUNTIME_SECURITY_AUDIT.md +180 -0
- package/SECURITY_AUDIT.md +78 -0
- package/TELEMETRY.md +129 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +377 -41
- package/dist/client.d.ts +4 -2
- package/dist/client.js +163 -24
- package/dist/component.js +41 -2
- package/dist/credentials.js +4 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -2
- package/dist/mount.js +10 -2
- package/dist/schema.d.ts +3 -1
- package/dist/schema.js +79 -5
- package/dist/snippet.js +22 -4
- package/dist/telemetry.d.ts +71 -0
- package/dist/telemetry.js +180 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.js +3 -0
- package/package.json +37 -9
- package/dist/cli.js.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/component.js.map +0 -1
- package/dist/credentials.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/mount.js.map +0 -1
- package/dist/schema.js.map +0 -1
- package/dist/snippet.js.map +0 -1
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
|
|
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
|
|
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
|
|
23
|
-
them to the
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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") ??
|
|
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
|
|
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
|
|
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
|
-
|
|
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") ??
|
|
278
|
-
io.stderr.write(`Missing --api-key. Create a Nexalink
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
|
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(
|
|
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 {};
|