@nexalink/carecompass-sdk 0.3.0-beta.15 → 0.4.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/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { randomUUID } from "node:crypto";
3
- import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync, } from "node:fs";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync, } from "node:fs";
4
4
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { careCompassApiBaseUrlSchema, createDefaultCareCompassConfig, validateCareCompassConfig, } from "./schema.js";
@@ -32,13 +32,26 @@ 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
34
  const CLI_COMMAND_ALLOWLIST = new Set([
35
+ "auth",
36
+ "completion",
37
+ "config",
38
+ "configure",
35
39
  "doctor",
40
+ "embed",
41
+ "health",
36
42
  "help",
37
43
  "init",
38
44
  "login",
45
+ "logout",
46
+ "query",
47
+ "setup",
39
48
  "snippet",
40
49
  "validate",
50
+ "version",
41
51
  ]);
52
+ const UNAUTHENTICATED_AUTH_ATTEMPT_LIMIT = 2;
53
+ const UNAUTHENTICATED_AUTH_RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000;
54
+ const UNAUTHENTICATED_AUTH_RATE_LIMIT_FILE = "auth-rate-limit.json";
42
55
  class CliTelemetryError extends Error {
43
56
  constructor(message, telemetry) {
44
57
  super(message);
@@ -47,6 +60,8 @@ class CliTelemetryError extends Error {
47
60
  }
48
61
  const sanitizeCommandName = (command) => {
49
62
  const normalized = command === "--help" || command === "-h" ? "help" : command;
63
+ if (command === "--version" || command === "-v")
64
+ return "version";
50
65
  return CLI_COMMAND_ALLOWLIST.has(normalized) ? normalized : "unknown_command";
51
66
  };
52
67
  const authTelemetryForSource = (source) => {
@@ -58,6 +73,96 @@ const authTelemetryForSource = (source) => {
58
73
  }
59
74
  return { authMethod: "api_key", keySource: "cli_flag" };
60
75
  };
76
+ const unauthenticatedRateLimitPath = (io) => io.env.CARECOMPASS_AUTH_RATE_LIMIT_PATH ??
77
+ join(dirname(resolveCredentialsPath(io.env)), UNAUTHENTICATED_AUTH_RATE_LIMIT_FILE);
78
+ const unauthenticatedRateLimitKey = (io) => {
79
+ const keyMaterial = io.env.CARECOMPASS_AUTH_RATE_LIMIT_KEY
80
+ ? `explicit:${io.env.CARECOMPASS_AUTH_RATE_LIMIT_KEY}`
81
+ : io.env.CARECOMPASS_CLIENT_IP
82
+ ? `ip:${io.env.CARECOMPASS_CLIENT_IP}`
83
+ : `credentials:${resolveCredentialsPath(io.env)}`;
84
+ return createHash("sha256")
85
+ .update("carecompass unauthenticated auth rate limit")
86
+ .update(keyMaterial)
87
+ .digest("hex");
88
+ };
89
+ const emptyUnauthenticatedAuthLedger = () => ({
90
+ version: 1,
91
+ entries: {},
92
+ });
93
+ const readUnauthenticatedAuthLedger = (path) => {
94
+ try {
95
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
96
+ if (parsed.version !== 1 || typeof parsed.entries !== "object") {
97
+ return emptyUnauthenticatedAuthLedger();
98
+ }
99
+ const entries = {};
100
+ for (const [key, value] of Object.entries(parsed.entries)) {
101
+ if (typeof value === "object" &&
102
+ value !== null &&
103
+ typeof value.failures ===
104
+ "number" &&
105
+ typeof value.firstAttemptAt ===
106
+ "number" &&
107
+ typeof value.lastAttemptAt ===
108
+ "number") {
109
+ entries[key] = value;
110
+ }
111
+ }
112
+ return { version: 1, entries };
113
+ }
114
+ catch {
115
+ return emptyUnauthenticatedAuthLedger();
116
+ }
117
+ };
118
+ const writeUnauthenticatedAuthLedger = (path, ledger) => {
119
+ mkdirSync(dirname(path), { recursive: true });
120
+ writeFileSync(path, `${JSON.stringify(ledger, null, 2)}\n`, { mode: 0o600 });
121
+ try {
122
+ chmodSync(path, 0o600);
123
+ }
124
+ catch {
125
+ // Some filesystems do not support chmod; the ledger contains only hashed
126
+ // limiter keys and counters.
127
+ }
128
+ };
129
+ const pruneUnauthenticatedAuthLedger = (ledger, now) => {
130
+ for (const [key, entry] of Object.entries(ledger.entries)) {
131
+ if (now - entry.firstAttemptAt >
132
+ UNAUTHENTICATED_AUTH_RATE_LIMIT_WINDOW_MS) {
133
+ delete ledger.entries[key];
134
+ }
135
+ }
136
+ };
137
+ const recordUnauthenticatedAuthFailure = (io) => {
138
+ const path = unauthenticatedRateLimitPath(io);
139
+ const key = unauthenticatedRateLimitKey(io);
140
+ const now = Date.now();
141
+ const ledger = readUnauthenticatedAuthLedger(path);
142
+ pruneUnauthenticatedAuthLedger(ledger, now);
143
+ const existing = ledger.entries[key];
144
+ const entry = existing &&
145
+ now - existing.firstAttemptAt <= UNAUTHENTICATED_AUTH_RATE_LIMIT_WINDOW_MS
146
+ ? existing
147
+ : { failures: 0, firstAttemptAt: now, lastAttemptAt: now };
148
+ entry.failures += 1;
149
+ entry.lastAttemptAt = now;
150
+ ledger.entries[key] = entry;
151
+ writeUnauthenticatedAuthLedger(path, ledger);
152
+ const failures = entry.failures;
153
+ if (failures < UNAUTHENTICATED_AUTH_ATTEMPT_LIMIT)
154
+ return null;
155
+ return new CliTelemetryError("Too many failed authentication attempts. Wait before trying again or verify the API key in the Nexalink console.", {
156
+ errorCode: CARECOMPASS_TELEMETRY_ERROR_CODES.RATE_LIMIT_EXCEEDED,
157
+ failureCategory: "rate_limit_error",
158
+ });
159
+ };
160
+ const resetUnauthenticatedAuthFailures = (io) => {
161
+ const path = unauthenticatedRateLimitPath(io);
162
+ const ledger = readUnauthenticatedAuthLedger(path);
163
+ delete ledger.entries[unauthenticatedRateLimitKey(io)];
164
+ writeUnauthenticatedAuthLedger(path, ledger);
165
+ };
61
166
  const normalizeBaaStatus = (value) => {
62
167
  if (value === "signed" || value === "pending" || value === "not_required") {
63
168
  return value;
@@ -526,14 +631,11 @@ async function resolveInitApiKey(flags, io, apiBaseUrl, onAuthChecked = async ()
526
631
  failureCategory: validation.failureCategory,
527
632
  });
528
633
  io.stdout.write(`${validation.message}\n`);
634
+ const rateLimitError = recordUnauthenticatedAuthFailure(io);
635
+ if (rateLimitError)
636
+ throw rateLimitError;
529
637
  continue;
530
638
  }
531
- await onAuthChecked({
532
- authResult: "success",
533
- baaStatus: validation.baaStatus,
534
- keySource: authTelemetry.keySource,
535
- failureCategory: null,
536
- });
537
639
  if (validation.accountType !== accountType) {
538
640
  if (validation.accountType === "enterprise") {
539
641
  io.stdout.write(`This API key belongs to an enterprise account. ${enterpriseUnavailableMessage(urls.developer)}\n`);
@@ -543,12 +645,14 @@ async function resolveInitApiKey(flags, io, apiBaseUrl, onAuthChecked = async ()
543
645
  continue;
544
646
  }
545
647
  writeStoredApiKey(normalizedApiKey, credentialsPath);
648
+ resetUnauthenticatedAuthFailures(io);
546
649
  io.stdout.write(`Stored CareCompass credentials at ${credentialsPath}\n`);
547
650
  io.stdout.write("Sign-in successful.\n");
548
651
  return {
549
652
  apiKey: normalizedApiKey,
550
653
  authMethod: authTelemetry.authMethod,
551
654
  baaStatus: validation.baaStatus,
655
+ keySource: authTelemetry.keySource,
552
656
  setup: validation.setup,
553
657
  };
554
658
  }
@@ -790,12 +894,14 @@ export async function runCareCompassCli(argv = process.argv.slice(2), io = {
790
894
  const sessionStartedAt = Date.now();
791
895
  const rawCommand = argv[0] ?? "help";
792
896
  const telemetryCommandName = sanitizeCommandName(rawCommand);
793
- const commandStartedAt = Date.now();
897
+ let commandStartedAt = sessionStartedAt;
898
+ let commandStartedCaptured = false;
794
899
  let commandsAttempted = 0;
795
900
  let commandsSucceeded = 0;
796
901
  let commandsFailed = 0;
797
902
  let activeCommand = rawCommand;
798
903
  let authCheckedCaptured = false;
904
+ let initStartedCaptured = false;
799
905
  let sessionEndedCaptured = false;
800
906
  const captureAuthChecked = async (telemetry) => {
801
907
  if (authCheckedCaptured)
@@ -811,7 +917,19 @@ export async function runCareCompassCli(argv = process.argv.slice(2), io = {
811
917
  keySource: telemetry.keySource,
812
918
  });
813
919
  };
920
+ const captureCommandStarted = async () => {
921
+ if (commandStartedCaptured)
922
+ return;
923
+ commandStartedCaptured = true;
924
+ commandStartedAt = Date.now();
925
+ commandsAttempted += 1;
926
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_COMMAND_STARTED, {
927
+ commandName: telemetryCommandName,
928
+ commandArgsCount: Math.max(argv.length - 1, 0),
929
+ });
930
+ };
814
931
  const captureCommandCompleted = async () => {
932
+ await captureCommandStarted();
815
933
  commandsSucceeded += 1;
816
934
  await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_COMMAND_COMPLETED, {
817
935
  commandName: telemetryCommandName,
@@ -820,6 +938,34 @@ export async function runCareCompassCli(argv = process.argv.slice(2), io = {
820
938
  modelId: null,
821
939
  });
822
940
  };
941
+ const authenticateConfigCommand = async (flags) => {
942
+ let credentials;
943
+ try {
944
+ credentials = requireApiKey(flags, io);
945
+ }
946
+ catch (error) {
947
+ const failure = classifyCliFailure(error);
948
+ await captureAuthChecked({
949
+ authResult: "failure",
950
+ baaStatus: "signed",
951
+ keySource: "cli_flag",
952
+ errorCode: failure.errorCode,
953
+ failureCategory: failure.failureCategory,
954
+ });
955
+ const rateLimitError = recordUnauthenticatedAuthFailure(io);
956
+ throw rateLimitError ?? error;
957
+ }
958
+ const config = readConfig(flagString(flags, "config") ?? DEFAULT_CONFIG_PATH);
959
+ resetUnauthenticatedAuthFailures(io);
960
+ cliTelemetry = createCareCompassCliTelemetry(cliSessionId, config);
961
+ await captureAuthChecked({
962
+ authResult: "success",
963
+ baaStatus: "not_required",
964
+ keySource: authTelemetryForSource(credentials.source).keySource,
965
+ failureCategory: null,
966
+ });
967
+ return { config, credentials };
968
+ };
823
969
  const captureSessionEnded = async (exitReason) => {
824
970
  if (sessionEndedCaptured)
825
971
  return;
@@ -833,11 +979,6 @@ export async function runCareCompassCli(argv = process.argv.slice(2), io = {
833
979
  });
834
980
  };
835
981
  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;
841
982
  try {
842
983
  const { command, flags, positionals } = parseArgs(argv);
843
984
  activeCommand = command;
@@ -847,6 +988,7 @@ export async function runCareCompassCli(argv = process.argv.slice(2), io = {
847
988
  return 0;
848
989
  }
849
990
  if (command === "login") {
991
+ await captureCommandStarted();
850
992
  await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_LOGIN_STARTED);
851
993
  const apiKey = flagString(flags, "api-key") ?? positionals[0];
852
994
  if (!apiKey) {
@@ -867,12 +1009,22 @@ export async function runCareCompassCli(argv = process.argv.slice(2), io = {
867
1009
  return 0;
868
1010
  }
869
1011
  if (command === "init") {
870
- const initStartedAt = Date.now();
871
- await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_INIT_STARTED);
872
1012
  rejectLegacyBaaInitFlags(flags, io);
873
1013
  const outputDir = flagString(flags, "output-dir") ?? DEFAULT_OUTPUT_DIR;
874
1014
  const apiBaseUrl = resolveTrustedApiBaseUrl(flagString(flags, "api-base-url") ?? DEFAULT_API_BASE_URL);
875
1015
  const initAuth = await resolveInitApiKey(flags, io, apiBaseUrl, captureAuthChecked);
1016
+ const config = buildInitConfig(flags, initAuth.setup);
1017
+ cliTelemetry = createCareCompassCliTelemetry(cliSessionId, config);
1018
+ await captureAuthChecked({
1019
+ authResult: "success",
1020
+ baaStatus: initAuth.baaStatus,
1021
+ keySource: initAuth.keySource,
1022
+ failureCategory: null,
1023
+ });
1024
+ await captureCommandStarted();
1025
+ const initStartedAt = Date.now();
1026
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_INIT_STARTED);
1027
+ initStartedCaptured = true;
876
1028
  const output = flagString(flags, "output") ??
877
1029
  join(outputDir, "carecompass.config.json");
878
1030
  const snippetOutput = flagString(flags, "snippet-output") ??
@@ -895,8 +1047,6 @@ export async function runCareCompassCli(argv = process.argv.slice(2), io = {
895
1047
  }
896
1048
  }
897
1049
  mkdirSync(outputDir, { recursive: true });
898
- const config = buildInitConfig(flags, initAuth.setup);
899
- cliTelemetry = createCareCompassCliTelemetry(cliSessionId, config);
900
1050
  writeFileSync(output, `${JSON.stringify(config, null, 2)}\n`);
901
1051
  writeFileSync(promptOutput, `${DEFAULT_SYSTEM_PROMPT}\n`);
902
1052
  const resolvedConfig = readConfig(output);
@@ -946,9 +1096,8 @@ Next command:
946
1096
  }
947
1097
  if (command === "validate") {
948
1098
  try {
949
- const credentials = requireApiKey(flags, io);
950
- const config = readConfig(flagString(flags, "config") ?? DEFAULT_CONFIG_PATH);
951
- cliTelemetry = createCareCompassCliTelemetry(cliSessionId, config);
1099
+ const { config, credentials } = await authenticateConfigCommand(flags);
1100
+ await captureCommandStarted();
952
1101
  if (flags["local-only"] !== true) {
953
1102
  await assertHostedConfig(config, credentials.apiKey);
954
1103
  }
@@ -959,17 +1108,18 @@ Next command:
959
1108
  }
960
1109
  catch (error) {
961
1110
  const failure = classifyCliFailure(error);
962
- await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_VALIDATION_FAILED, {
963
- errorCode: failure.errorCode,
964
- failureCategory: failure.failureCategory,
965
- });
1111
+ if (commandStartedCaptured) {
1112
+ await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_VALIDATION_FAILED, {
1113
+ errorCode: failure.errorCode,
1114
+ failureCategory: failure.failureCategory,
1115
+ });
1116
+ }
966
1117
  throw error;
967
1118
  }
968
1119
  }
969
1120
  if (command === "snippet") {
970
- requireApiKey(flags, io);
971
- const config = readConfig(flagString(flags, "config") ?? DEFAULT_CONFIG_PATH);
972
- cliTelemetry = createCareCompassCliTelemetry(cliSessionId, config);
1121
+ const { config } = await authenticateConfigCommand(flags);
1122
+ await captureCommandStarted();
973
1123
  const snippet = generateCareCompassSnippet(runtimeConfig(config), {
974
1124
  mode: snippetMode(flags),
975
1125
  });
@@ -987,9 +1137,8 @@ Next command:
987
1137
  return 0;
988
1138
  }
989
1139
  if (command === "doctor") {
990
- const credentials = requireApiKey(flags, io);
991
- const config = readConfig(flagString(flags, "config") ?? DEFAULT_CONFIG_PATH);
992
- cliTelemetry = createCareCompassCliTelemetry(cliSessionId, config);
1140
+ const { config, credentials } = await authenticateConfigCommand(flags);
1141
+ await captureCommandStarted();
993
1142
  let hostedStatus = "not checked";
994
1143
  try {
995
1144
  await assertHostedConfig(config, credentials.apiKey);
@@ -1013,12 +1162,16 @@ Status: ready for staging verification
1013
1162
  await captureCommandCompleted();
1014
1163
  return 0;
1015
1164
  }
1165
+ await captureCommandStarted();
1016
1166
  throw new Error(`Unknown command: ${command}`);
1017
1167
  }
1018
1168
  catch (error) {
1019
1169
  commandsFailed += 1;
1170
+ if (!commandStartedCaptured && commandsAttempted === 0) {
1171
+ commandsAttempted = 1;
1172
+ }
1020
1173
  const failure = classifyCliFailure(error);
1021
- if (activeCommand === "init") {
1174
+ if (activeCommand === "init" && initStartedCaptured) {
1022
1175
  await cliTelemetry.captureAsync(CARECOMPASS_TELEMETRY_EVENTS.CLI_INIT_FAILED, {
1023
1176
  errorCode: failure.errorCode,
1024
1177
  failureCategory: failure.failureCategory,
@@ -1,5 +1,13 @@
1
+ import React from "react";
2
+ import { type CareCompassContext, type CareCompassCoreEvent, type CareCompassMessage } from "./core.js";
1
3
  import type { CareCompassConfig } from "./schema.js";
2
- export declare function CareCompassEmbed({ config, pollIntervalMs, }: {
4
+ export type CareCompassChatProps = {
3
5
  config: CareCompassConfig;
6
+ initialContext?: CareCompassContext;
7
+ onEvent?: (event: CareCompassCoreEvent) => void;
4
8
  pollIntervalMs?: number;
5
- }): import("react/jsx-runtime").JSX.Element;
9
+ renderMessage?: (message: CareCompassMessage) => React.ReactNode;
10
+ retryLabel?: string;
11
+ };
12
+ export declare function CareCompassChat({ config, initialContext, onEvent, pollIntervalMs, renderMessage, retryLabel, }: CareCompassChatProps): import("react/jsx-runtime").JSX.Element;
13
+ export declare const CareCompassEmbed: typeof CareCompassChat;
package/dist/component.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from "react";
3
- import { createCareCompassClient } from "./client.js";
3
+ import { createCareCompassCore, } from "./core.js";
4
+ import { CareCompassMarkdown } from "./markdown.js";
4
5
  import { CARECOMPASS_TELEMETRY_ERROR_CODES, CARECOMPASS_TELEMETRY_EVENTS, createCareCompassTelemetry, } from "./telemetry.js";
5
6
  // The embed should fail closed inside the host page: show the configured
6
7
  // fallback text, capture sanitized telemetry, and avoid crashing the integrator.
@@ -25,71 +26,56 @@ class CareCompassErrorBoundary extends React.Component {
25
26
  return this.props.children;
26
27
  }
27
28
  }
28
- function CareCompassEmbedContent({ config, telemetry, pollIntervalMs = 1500, }) {
29
- const client = React.useMemo(() => createCareCompassClient(config), [config]);
30
- const [messages, setMessages] = React.useState([
31
- {
32
- id: "welcome",
33
- role: "assistant",
34
- text: config.prompt.welcomeMessage,
35
- },
36
- ]);
29
+ function CareCompassChatContent({ config, initialContext, onEvent, telemetry, pollIntervalMs = 1500, renderMessage, retryLabel = "Retry", }) {
30
+ const core = React.useMemo(() => createCareCompassCore(config, {
31
+ initialContext,
32
+ pollIntervalMs,
33
+ }), [config, initialContext, pollIntervalMs]);
34
+ const [snapshot, setSnapshot] = React.useState(() => core.getSnapshot());
37
35
  const [input, setInput] = React.useState("");
38
- const [loading, setLoading] = React.useState(false);
39
- const [error, setError] = React.useState(null);
40
36
  React.useEffect(() => {
41
37
  telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.COMPONENT_RENDERED);
42
38
  telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.COMPONENT_OPENED);
39
+ const unsubscribe = core.subscribe(setSnapshot);
40
+ setSnapshot(core.getSnapshot());
43
41
  return () => {
42
+ unsubscribe();
43
+ core.dispose();
44
44
  telemetry.capture(CARECOMPASS_TELEMETRY_EVENTS.COMPONENT_CLOSED);
45
45
  };
46
- }, [telemetry]);
46
+ }, [core, telemetry]);
47
47
  async function sendMessage(event) {
48
48
  event.preventDefault();
49
49
  const prompt = input.trim();
50
- if (!prompt || loading)
50
+ if (!prompt || snapshot.loading)
51
51
  return;
52
52
  setInput("");
53
- setError(null);
54
- setLoading(true);
55
- setMessages((current) => [
56
- ...current,
57
- { id: `user-${Date.now()}`, role: "user", text: prompt },
58
- ]);
59
53
  try {
60
- const { jobId } = await client.ask({ prompt });
61
- const startedAt = Date.now();
62
- // The backend returns a job immediately, so the component polls until it
63
- // sees a terminal state or reaches the same timeout tracked by telemetry.
64
- while (Date.now() - startedAt < 120_000) {
65
- const status = await client.status(jobId);
66
- if (status.status === "complete" && status.result) {
67
- setMessages((current) => [
68
- ...current,
69
- {
70
- id: `assistant-${Date.now()}`,
71
- role: "assistant",
72
- text: status.result ?? "",
73
- },
74
- ]);
75
- return;
76
- }
77
- if (status.status === "failed") {
78
- throw new Error(status.result ?? config.labels.errorMessage);
79
- }
80
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
54
+ for await (const coreEvent of core.streamMessage(prompt)) {
55
+ onEvent?.(coreEvent);
81
56
  }
82
- client.recordInferenceTimeout(jobId, Date.now() - startedAt);
83
- throw new Error("CareCompass request timed out.");
84
57
  }
85
- catch (err) {
86
- setError(err?.message ??
87
- config.labels.errorMessage);
58
+ catch {
59
+ // The core snapshot already carries the user-visible error and retry
60
+ // state. Swallow here so host pages do not see unhandled form errors.
88
61
  }
89
- finally {
90
- setLoading(false);
62
+ }
63
+ async function retryMessage() {
64
+ try {
65
+ await core.retryLast();
66
+ }
67
+ catch {
68
+ // The core owns the visible retry failure state.
91
69
  }
92
70
  }
71
+ const messageContent = (message) => {
72
+ if (renderMessage)
73
+ return renderMessage(message);
74
+ if (message.role === "assistant") {
75
+ return _jsx(CareCompassMarkdown, { text: message.text });
76
+ }
77
+ return message.text;
78
+ };
93
79
  return (_jsxs("section", { "aria-label": config.labels.title, style: {
94
80
  "--carecompass-primary": config.theme.primaryColor,
95
81
  "--carecompass-bg": config.theme.backgroundColor,
@@ -118,7 +104,7 @@ function CareCompassEmbedContent({ config, telemetry, pollIntervalMs = 1500, })
118
104
  gap: 12,
119
105
  overflowY: "auto",
120
106
  padding: 16,
121
- }, children: [messages.map((message) => (_jsx("div", { style: {
107
+ }, children: [snapshot.messages.map((message) => (_jsx("div", { style: {
122
108
  alignSelf: message.role === "user" ? "flex-end" : "flex-start",
123
109
  background: message.role === "user"
124
110
  ? "var(--carecompass-primary)"
@@ -127,14 +113,14 @@ function CareCompassEmbedContent({ config, telemetry, pollIntervalMs = 1500, })
127
113
  color: message.role === "user" ? "#ffffff" : "inherit",
128
114
  maxInlineSize: "82%",
129
115
  padding: "10px 12px",
130
- whiteSpace: "pre-wrap",
131
- }, children: message.text }, message.id))), loading ? (_jsx("div", { style: { fontSize: 13, opacity: 0.72 }, children: "Thinking..." })) : null] }), _jsxs("form", { onSubmit: sendMessage, style: {
116
+ whiteSpace: message.role === "user" ? "pre-wrap" : "normal",
117
+ }, children: messageContent(message) }, message.id))), snapshot.loading ? (_jsx("div", { style: { fontSize: 13, opacity: 0.72 }, children: "Thinking..." })) : null] }), _jsxs("form", { onSubmit: sendMessage, style: {
132
118
  borderTop: "1px solid color-mix(in srgb, var(--carecompass-text) 12%, transparent)",
133
119
  display: "flex",
134
120
  flexDirection: "column",
135
121
  gap: 8,
136
122
  padding: 12,
137
- }, children: [_jsxs("div", { style: { display: "flex", gap: 8 }, children: [_jsx("input", { "aria-label": config.labels.placeholder, disabled: loading, onChange: (event) => setInput(event.currentTarget.value), placeholder: config.labels.placeholder, style: {
123
+ }, children: [_jsxs("div", { style: { display: "flex", gap: 8 }, children: [_jsx("input", { "aria-label": config.labels.placeholder, disabled: snapshot.loading, onChange: (event) => setInput(event.currentTarget.value), placeholder: config.labels.placeholder, style: {
138
124
  border: "1px solid color-mix(in srgb, var(--carecompass-text) 16%, transparent)",
139
125
  borderRadius: 8,
140
126
  color: "inherit",
@@ -142,20 +128,36 @@ function CareCompassEmbedContent({ config, telemetry, pollIntervalMs = 1500, })
142
128
  font: "inherit",
143
129
  minInlineSize: 0,
144
130
  padding: "10px 12px",
145
- }, value: input }), _jsx("button", { disabled: loading || !input.trim(), style: {
131
+ }, value: input }), _jsx("button", { disabled: snapshot.loading || !input.trim(), style: {
146
132
  background: "var(--carecompass-primary)",
147
133
  border: 0,
148
134
  borderRadius: 8,
149
135
  color: "#ffffff",
150
- cursor: loading ? "not-allowed" : "pointer",
136
+ cursor: snapshot.loading ? "not-allowed" : "pointer",
151
137
  font: "inherit",
152
138
  fontWeight: 700,
153
139
  padding: "0 14px",
154
- }, type: "submit", children: config.labels.sendButton })] }), error ? (_jsx("div", { role: "alert", style: { color: "#b91c1c", fontSize: 12 }, children: error })) : null, _jsx("div", { style: { fontSize: 11, opacity: 0.7 }, children: config.labels.disclaimer })] })] }));
140
+ }, type: "submit", children: config.labels.sendButton })] }), snapshot.error ? (_jsxs("div", { role: "alert", style: {
141
+ alignItems: "center",
142
+ color: "#b91c1c",
143
+ display: "flex",
144
+ flexWrap: "wrap",
145
+ fontSize: 12,
146
+ gap: 8,
147
+ }, children: [_jsx("span", { children: snapshot.error.message }), snapshot.canRetry ? (_jsx("button", { onClick: retryMessage, style: {
148
+ background: "transparent",
149
+ border: "1px solid color-mix(in srgb, #b91c1c 40%, transparent)",
150
+ borderRadius: 6,
151
+ color: "#b91c1c",
152
+ cursor: "pointer",
153
+ font: "inherit",
154
+ padding: "4px 8px",
155
+ }, type: "button", children: retryLabel })) : null] })) : null, _jsx("div", { style: { fontSize: 11, opacity: 0.7 }, children: config.labels.disclaimer })] })] }));
155
156
  }
156
- export function CareCompassEmbed({ config, pollIntervalMs = 1500, }) {
157
+ export function CareCompassChat({ config, initialContext, onEvent, pollIntervalMs = 1500, renderMessage, retryLabel, }) {
157
158
  // Telemetry is scoped to the validated public config; no per-message content
158
159
  // is passed through this component-level telemetry object.
159
160
  const telemetry = React.useMemo(() => createCareCompassTelemetry(config), [config]);
160
- return (_jsx(CareCompassErrorBoundary, { config: config, telemetry: telemetry, children: _jsx(CareCompassEmbedContent, { config: config, pollIntervalMs: pollIntervalMs, telemetry: telemetry }) }));
161
+ return (_jsx(CareCompassErrorBoundary, { config: config, telemetry: telemetry, children: _jsx(CareCompassChatContent, { config: config, initialContext: initialContext, onEvent: onEvent, pollIntervalMs: pollIntervalMs, renderMessage: renderMessage, retryLabel: retryLabel, telemetry: telemetry }) }));
161
162
  }
163
+ export const CareCompassEmbed = CareCompassChat;
package/dist/core.d.ts ADDED
@@ -0,0 +1,78 @@
1
+ import { type CareCompassStatusResponse } from "./client.js";
2
+ import type { CareCompassConfig } from "./schema.js";
3
+ type Fetcher = typeof fetch;
4
+ export type CareCompassContext = Record<string, unknown>;
5
+ export type CareCompassMessage = {
6
+ id: string;
7
+ role: "user" | "assistant";
8
+ text: string;
9
+ createdAt: number;
10
+ status: "complete" | "failed" | "pending";
11
+ };
12
+ export type CareCompassLifecyclePhase = "idle" | "submitting" | "streaming" | "complete" | "error" | "disposed";
13
+ export type CareCompassCoreErrorCode = "request_failed" | "timeout" | "disposed";
14
+ export type CareCompassCoreError = {
15
+ code: CareCompassCoreErrorCode;
16
+ message: string;
17
+ };
18
+ export type CareCompassRetryMetadata = {
19
+ prompt: string;
20
+ context: CareCompassContext;
21
+ attempts: number;
22
+ };
23
+ export type CareCompassCoreSnapshot = {
24
+ messages: CareCompassMessage[];
25
+ phase: CareCompassLifecyclePhase;
26
+ loading: boolean;
27
+ error: CareCompassCoreError | null;
28
+ context: CareCompassContext;
29
+ canRetry: boolean;
30
+ retry: CareCompassRetryMetadata | null;
31
+ activeJobId: string | null;
32
+ };
33
+ export type CareCompassCoreEvent = {
34
+ type: "message_submitted";
35
+ message: CareCompassMessage;
36
+ snapshot: CareCompassCoreSnapshot;
37
+ } | {
38
+ type: "status";
39
+ jobId: string;
40
+ status: CareCompassStatusResponse["status"];
41
+ snapshot: CareCompassCoreSnapshot;
42
+ } | {
43
+ type: "message_completed";
44
+ message: CareCompassMessage;
45
+ snapshot: CareCompassCoreSnapshot;
46
+ } | {
47
+ type: "error";
48
+ error: CareCompassCoreError;
49
+ snapshot: CareCompassCoreSnapshot;
50
+ } | {
51
+ type: "context_updated";
52
+ snapshot: CareCompassCoreSnapshot;
53
+ } | {
54
+ type: "reset";
55
+ snapshot: CareCompassCoreSnapshot;
56
+ };
57
+ export type CareCompassCoreOptions = {
58
+ fetcher?: Fetcher;
59
+ initialContext?: CareCompassContext;
60
+ now?: () => number;
61
+ pollIntervalMs?: number;
62
+ timeoutMs?: number;
63
+ };
64
+ export type CareCompassSendMessageOptions = {
65
+ context?: CareCompassContext;
66
+ };
67
+ export type CareCompassCore = {
68
+ getSnapshot: () => CareCompassCoreSnapshot;
69
+ subscribe: (listener: (snapshot: CareCompassCoreSnapshot) => void) => () => void;
70
+ setContext: (context: CareCompassContext | ((current: CareCompassContext) => CareCompassContext)) => void;
71
+ sendMessage: (prompt: string, options?: CareCompassSendMessageOptions) => Promise<CareCompassCoreSnapshot>;
72
+ streamMessage: (prompt: string, options?: CareCompassSendMessageOptions) => AsyncIterable<CareCompassCoreEvent>;
73
+ retryLast: () => Promise<CareCompassCoreSnapshot>;
74
+ reset: () => void;
75
+ dispose: () => void;
76
+ };
77
+ export declare function createCareCompassCore(config: CareCompassConfig, options?: CareCompassCoreOptions): CareCompassCore;
78
+ export {};