@thingd/cli 0.70.0 → 0.72.0

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.
Files changed (62) hide show
  1. package/package.json +2 -2
  2. package/dist/commands/cloud.d.ts +0 -3
  3. package/dist/commands/cloud.d.ts.map +0 -1
  4. package/dist/commands/cloud.js +0 -648
  5. package/dist/commands/mcp-connect.d.ts +0 -3
  6. package/dist/commands/mcp-connect.d.ts.map +0 -1
  7. package/dist/commands/mcp-connect.js +0 -154
  8. package/dist/dashboard/public/assets/favicon-CgGFvG_0.svg +0 -4
  9. package/dist/dashboard/public/assets/index-C6PkDB7y.css +0 -1
  10. package/dist/dashboard/public/assets/index-CsLTLPTP.js +0 -4
  11. package/dist/dashboard/public/index.html +0 -19
  12. package/dist/dashboard/server.d.ts +0 -6
  13. package/dist/dashboard/server.d.ts.map +0 -1
  14. package/dist/dashboard/server.js +0 -663
  15. package/dist/data-movement.d.ts +0 -6
  16. package/dist/data-movement.d.ts.map +0 -1
  17. package/dist/data-movement.js +0 -475
  18. package/dist/doctor.d.ts +0 -3
  19. package/dist/doctor.d.ts.map +0 -1
  20. package/dist/doctor.js +0 -108
  21. package/dist/index.d.ts +0 -47
  22. package/dist/index.d.ts.map +0 -1
  23. package/dist/index.js +0 -1221
  24. package/dist/install.d.ts +0 -3
  25. package/dist/install.d.ts.map +0 -1
  26. package/dist/install.js +0 -229
  27. package/dist/interactive.d.ts +0 -2
  28. package/dist/interactive.d.ts.map +0 -1
  29. package/dist/interactive.js +0 -3036
  30. package/dist/lib/cloud-api.d.ts +0 -143
  31. package/dist/lib/cloud-api.d.ts.map +0 -1
  32. package/dist/lib/cloud-api.js +0 -207
  33. package/dist/lib/cloud-config.d.ts +0 -33
  34. package/dist/lib/cloud-config.d.ts.map +0 -1
  35. package/dist/lib/cloud-config.js +0 -42
  36. package/dist/lib/mcp-config-writer.d.ts +0 -26
  37. package/dist/lib/mcp-config-writer.d.ts.map +0 -1
  38. package/dist/lib/mcp-config-writer.js +0 -86
  39. package/dist/logo.d.ts +0 -3
  40. package/dist/logo.d.ts.map +0 -1
  41. package/dist/logo.js +0 -8
  42. package/dist/mcp/cluster.d.ts +0 -69
  43. package/dist/mcp/cluster.d.ts.map +0 -1
  44. package/dist/mcp/cluster.js +0 -304
  45. package/dist/mcp/config.d.ts +0 -14
  46. package/dist/mcp/config.d.ts.map +0 -1
  47. package/dist/mcp/config.js +0 -67
  48. package/dist/mcp/http.d.ts +0 -25
  49. package/dist/mcp/http.d.ts.map +0 -1
  50. package/dist/mcp/http.js +0 -597
  51. package/dist/mcp/index.d.ts +0 -5
  52. package/dist/mcp/index.d.ts.map +0 -1
  53. package/dist/mcp/index.js +0 -3
  54. package/dist/mcp-http.d.ts +0 -3
  55. package/dist/mcp-http.d.ts.map +0 -1
  56. package/dist/mcp-http.js +0 -42
  57. package/dist/mcp.d.ts +0 -3
  58. package/dist/mcp.d.ts.map +0 -1
  59. package/dist/mcp.js +0 -28
  60. package/dist/paths.d.ts +0 -4
  61. package/dist/paths.d.ts.map +0 -1
  62. package/dist/paths.js +0 -14
@@ -1,304 +0,0 @@
1
- import { parseBooleanFlag, parsePort } from "./config.js";
2
- const DEFAULT_CLUSTER_PORT = 8757;
3
- export function readClusterOptionsFromEnv(env) {
4
- return {
5
- mode: parseClusterMode(env.THINGD_CLUSTER_MODE),
6
- advertiseUrl: env.THINGD_ADVERTISE_URL,
7
- leaderUrl: env.THINGD_CLUSTER_LEADER_URL,
8
- fallbackLeaderUrl: env.THINGD_CLUSTER_LEADER_FALLBACK_URL,
9
- peers: parsePeers(env.THINGD_CLUSTER_PEERS),
10
- discovery: parseClusterDiscovery(env.THINGD_CLUSTER_DISCOVERY),
11
- service: env.THINGD_CLUSTER_SERVICE,
12
- namespace: env.THINGD_CLUSTER_NAMESPACE,
13
- port: parsePort(env.THINGD_CLUSTER_PORT, DEFAULT_CLUSTER_PORT),
14
- forwardAuthToken: env.THINGD_CLUSTER_FORWARD_AUTH_TOKEN ?? env.THINGD_AUTH_TOKEN,
15
- leaderElection: parseBooleanFlag(env.THINGD_CLUSTER_LEADER_ELECTION, "THINGD_CLUSTER_LEADER_ELECTION"),
16
- electionMaxFailures: parsePositiveInt(env.THINGD_CLUSTER_LEADER_ELECTION_MAX_FAILURES, 3),
17
- };
18
- }
19
- export function resolveClusterOptions(options) {
20
- const mode = options?.mode ?? "single";
21
- const discovery = options?.discovery ?? (options?.peers?.length ? "static" : "none");
22
- const port = options?.port ?? DEFAULT_CLUSTER_PORT;
23
- const peers = resolvePeers({
24
- discovery,
25
- peers: options?.peers ?? [],
26
- service: options?.service,
27
- namespace: options?.namespace,
28
- port,
29
- });
30
- const leaderElection = options?.leaderElection ?? false;
31
- const electionMaxFailures = options?.electionMaxFailures ?? 3;
32
- // When leader election is enabled, derive leaderUrl from peer list if not explicitly set.
33
- if (mode === "follower" && !options?.leaderUrl && leaderElection && peers.length > 0) {
34
- options = { ...options, leaderUrl: peers[0] };
35
- }
36
- if (mode === "follower" && !options?.leaderUrl) {
37
- throw new Error("THINGD_CLUSTER_LEADER_URL is required when THINGD_CLUSTER_MODE=follower");
38
- }
39
- return {
40
- mode,
41
- advertiseUrl: options?.advertiseUrl,
42
- leaderUrl: options?.leaderUrl,
43
- fallbackLeaderUrl: options?.fallbackLeaderUrl,
44
- activeLeaderUrl: undefined,
45
- peers,
46
- discovery,
47
- service: options?.service,
48
- namespace: options?.namespace,
49
- port,
50
- forwardAuthToken: options?.forwardAuthToken,
51
- statusPath: options?.statusPath ?? "/cluster/status",
52
- peersPath: options?.peersPath ?? "/cluster/peers",
53
- leaderElection,
54
- electionMaxFailures,
55
- cachedReplicationLag: 0,
56
- };
57
- }
58
- export async function getClusterStatus(cluster, db, consecutiveFailures = 0) {
59
- let replication = "not-implemented";
60
- if (cluster.mode === "follower") {
61
- try {
62
- const status = await db.get("__thingd_meta", "replication_status");
63
- const lastSeq = status && typeof status.lastReplicatedSequence === "number"
64
- ? status.lastReplicatedSequence
65
- : 0;
66
- // Lag is updated asynchronously by the replication runner — read cached value
67
- // to avoid a synchronous outbound HTTP call on every /healthz request.
68
- replication = {
69
- lastReplicatedSequence: lastSeq,
70
- status: "syncing",
71
- lag: cluster.cachedReplicationLag,
72
- };
73
- }
74
- catch {
75
- replication = { lastReplicatedSequence: 0, status: "error" };
76
- }
77
- }
78
- else if (cluster.mode === "leader" || cluster.mode === "single") {
79
- try {
80
- const list = await db.events.list("__thingd:system:replication");
81
- const lastEvent = list[list.length - 1];
82
- const lastSeq = lastEvent ? Number.parseInt(lastEvent.id, 10) : 0;
83
- replication = {
84
- lastReplicatedSequence: lastSeq,
85
- status: "active",
86
- };
87
- }
88
- catch {
89
- replication = { lastReplicatedSequence: 0, status: "error" };
90
- }
91
- }
92
- return {
93
- mode: cluster.mode,
94
- writable: cluster.mode !== "follower",
95
- forwarding: cluster.mode === "follower",
96
- leaderUrl: cluster.leaderUrl,
97
- fallbackLeaderUrl: cluster.fallbackLeaderUrl,
98
- activeLeaderUrl: cluster.activeLeaderUrl,
99
- advertiseUrl: cluster.advertiseUrl,
100
- discovery: cluster.discovery,
101
- peers: cluster.peers,
102
- leaderElection: cluster.leaderElection,
103
- electionMaxFailures: cluster.electionMaxFailures,
104
- consecutiveFailures,
105
- replication,
106
- };
107
- }
108
- export async function forwardMcpRequestToLeader(cluster, mcpPath, request, response) {
109
- if (!cluster.leaderUrl) {
110
- writeForwardError(response, 503, "cluster_leader_unavailable");
111
- return;
112
- }
113
- const body = await readRequestBody(request);
114
- const urls = cluster.fallbackLeaderUrl
115
- ? [cluster.leaderUrl, cluster.fallbackLeaderUrl]
116
- : [cluster.leaderUrl];
117
- let lastError;
118
- for (const url of urls) {
119
- try {
120
- const upstreamUrl = leaderMcpUrl(url, mcpPath);
121
- const upstream = await fetch(upstreamUrl, {
122
- method: "POST",
123
- headers: forwardedHeaders(request, cluster.forwardAuthToken),
124
- body: new Uint8Array(body),
125
- signal: AbortSignal.timeout(30_000),
126
- });
127
- cluster.activeLeaderUrl = url;
128
- response.writeHead(upstream.status, responseHeaders(upstream.headers));
129
- response.end(Buffer.from(await upstream.arrayBuffer()));
130
- return;
131
- }
132
- catch (error) {
133
- lastError = error instanceof Error ? error : new Error(String(error));
134
- console.error(`Forward to leader ${url} failed:`, lastError.message);
135
- }
136
- }
137
- if (lastError) {
138
- console.error("All leader URLs exhausted for forward, last error:", lastError.message);
139
- }
140
- writeForwardError(response, 503, "cluster_leader_unavailable");
141
- }
142
- export function findNextLeaderCandidate(cluster) {
143
- const { advertiseUrl, peers } = cluster;
144
- const currentLeaderUrl = cluster.activeLeaderUrl ?? cluster.leaderUrl;
145
- if (!currentLeaderUrl || peers.length === 0) {
146
- return undefined;
147
- }
148
- const leaderIndex = findPeerIndex(peers, currentLeaderUrl);
149
- if (leaderIndex === -1) {
150
- return undefined;
151
- }
152
- // Scan from the next peer after the current leader.
153
- for (let i = leaderIndex + 1; i < peers.length; i++) {
154
- const candidate = peers[i];
155
- if (candidate) {
156
- const isSelf = typeof advertiseUrl === "string" && sameOrigin(advertiseUrl, candidate);
157
- return { url: candidate, isSelf };
158
- }
159
- }
160
- // Wrap around — all peers after leader's index exhausted, try from the beginning.
161
- for (let i = 0; i < leaderIndex; i++) {
162
- const candidate = peers[i];
163
- if (candidate) {
164
- const isSelf = typeof advertiseUrl === "string" && sameOrigin(advertiseUrl, candidate);
165
- return { url: candidate, isSelf };
166
- }
167
- }
168
- return undefined;
169
- }
170
- function findPeerIndex(peers, targetUrl) {
171
- return peers.findIndex((peer) => sameOrigin(peer, targetUrl));
172
- }
173
- /** Compare two URLs by origin (scheme + host + port). */
174
- function sameOrigin(a, b) {
175
- try {
176
- return new URL(a).origin === new URL(b).origin;
177
- }
178
- catch {
179
- return a === b;
180
- }
181
- }
182
- function parsePositiveInt(value, fallback) {
183
- if (!value) {
184
- return fallback;
185
- }
186
- const n = Number.parseInt(value, 10);
187
- if (!Number.isInteger(n) || n <= 0) {
188
- throw new Error(`Invalid positive integer: ${value}`);
189
- }
190
- return n;
191
- }
192
- function parseClusterMode(value) {
193
- if (!value) {
194
- return undefined;
195
- }
196
- if (value === "single" || value === "leader" || value === "follower") {
197
- return value;
198
- }
199
- throw new Error(`Unsupported THINGD_CLUSTER_MODE: ${value}`);
200
- }
201
- function parseClusterDiscovery(value) {
202
- if (!value) {
203
- return undefined;
204
- }
205
- if (value === "none" || value === "static" || value === "kubernetes") {
206
- return value;
207
- }
208
- throw new Error(`Unsupported THINGD_CLUSTER_DISCOVERY: ${value}`);
209
- }
210
- function parsePeers(value) {
211
- if (!value) {
212
- return undefined;
213
- }
214
- return value
215
- .split(",")
216
- .map((peer) => peer.trim())
217
- .filter(Boolean);
218
- }
219
- function resolvePeers(options) {
220
- if (options.discovery === "static") {
221
- return options.peers;
222
- }
223
- if (options.discovery !== "kubernetes" || !options.service) {
224
- return [];
225
- }
226
- const namespace = options.namespace ?? "default";
227
- return [`http://${options.service}.${namespace}.svc.cluster.local:${options.port}`];
228
- }
229
- function leaderMcpUrl(leaderUrl, mcpPath) {
230
- const url = new URL(leaderUrl);
231
- if (url.pathname === "/" || url.pathname === "") {
232
- url.pathname = mcpPath;
233
- }
234
- return url.toString();
235
- }
236
- function forwardedHeaders(request, forwardAuthToken) {
237
- const headers = new Headers();
238
- const contentType = request.headers["content-type"];
239
- const protocolVersion = request.headers["mcp-protocol-version"];
240
- const accept = request.headers.accept;
241
- if (typeof contentType === "string") {
242
- headers.set("Content-Type", contentType);
243
- }
244
- if (typeof accept === "string") {
245
- headers.set("Accept", accept);
246
- }
247
- if (typeof protocolVersion === "string") {
248
- headers.set("MCP-Protocol-Version", protocolVersion);
249
- }
250
- if (forwardAuthToken) {
251
- headers.set("Authorization", `Bearer ${forwardAuthToken}`);
252
- }
253
- else if (typeof request.headers.authorization === "string") {
254
- headers.set("Authorization", request.headers.authorization);
255
- }
256
- return headers;
257
- }
258
- function responseHeaders(headers) {
259
- const result = {};
260
- for (const [key, value] of headers) {
261
- if (!isHopByHopHeader(key)) {
262
- result[key] = value;
263
- }
264
- }
265
- return result;
266
- }
267
- function isHopByHopHeader(header) {
268
- return [
269
- "connection",
270
- "content-length",
271
- "keep-alive",
272
- "proxy-authenticate",
273
- "proxy-authorization",
274
- "te",
275
- "trailer",
276
- "transfer-encoding",
277
- "upgrade",
278
- ].includes(header.toLowerCase());
279
- }
280
- function readRequestBody(request) {
281
- return new Promise((resolve, reject) => {
282
- const chunks = [];
283
- request.on("data", (chunk) => {
284
- chunks.push(chunk);
285
- });
286
- request.on("end", () => {
287
- resolve(Buffer.concat(chunks));
288
- });
289
- request.on("error", reject);
290
- });
291
- }
292
- function writeForwardError(response, statusCode, error) {
293
- response.writeHead(statusCode, {
294
- "Content-Type": "application/json",
295
- });
296
- response.end(JSON.stringify({
297
- jsonrpc: "2.0",
298
- error: {
299
- code: -32_603,
300
- message: error,
301
- },
302
- id: null,
303
- }));
304
- }
@@ -1,14 +0,0 @@
1
- import type { ThingDDriver, ThingdMcpAuditOptions } from "@thingd/sdk";
2
- export type ThingDStorageDriver = Exclude<ThingDDriver, "cloud">;
3
- export type HttpRuntimeSafetyOptions = {
4
- host: string;
5
- authToken?: string;
6
- allowUnauthenticated?: boolean;
7
- };
8
- export declare function parseThingdDriver(value: string | undefined): ThingDStorageDriver | undefined;
9
- export declare function parsePort(value: string | undefined, fallback: number): number;
10
- export declare function parseBooleanFlag(value: string | undefined, name: string): boolean;
11
- export declare function readMcpAuditOptionsFromEnv(env: Record<string, string | undefined>): ThingdMcpAuditOptions | false;
12
- export declare function readCliValue(args: string[], index: number, name: string): string;
13
- export declare function ensureHttpRuntimeIsSafe(options: HttpRuntimeSafetyOptions): void;
14
- //# sourceMappingURL=config.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/mcp/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEvE,MAAM,MAAM,mBAAmB,GAAG,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;AAEjE,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,mBAAmB,GAAG,SAAS,CAU5F;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAY7E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAejF;AAED,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GACtC,qBAAqB,GAAG,KAAK,CAgB/B;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAQhF;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CAU/E"}
@@ -1,67 +0,0 @@
1
- export function parseThingdDriver(value) {
2
- if (!value) {
3
- return undefined;
4
- }
5
- if (value === "memory" || value === "native") {
6
- return value;
7
- }
8
- throw new Error(`Unsupported thingd driver: ${value}`);
9
- }
10
- export function parsePort(value, fallback) {
11
- if (!value) {
12
- return fallback;
13
- }
14
- const port = Number.parseInt(value, 10);
15
- if (!Number.isInteger(port) || port < 0 || port > 65_535) {
16
- throw new Error(`Invalid port: ${value}`);
17
- }
18
- return port;
19
- }
20
- export function parseBooleanFlag(value, name) {
21
- if (!value) {
22
- return false;
23
- }
24
- const normalized = value.toLowerCase();
25
- if (["1", "true", "yes", "on"].includes(normalized)) {
26
- return true;
27
- }
28
- if (["0", "false", "no", "off"].includes(normalized)) {
29
- return false;
30
- }
31
- throw new Error(`Invalid ${name}: expected true or false`);
32
- }
33
- export function readMcpAuditOptionsFromEnv(env) {
34
- const enabled = env.THINGD_MCP_AUDIT === undefined
35
- ? undefined
36
- : parseBooleanFlag(env.THINGD_MCP_AUDIT, "THINGD_MCP_AUDIT");
37
- if (enabled === false) {
38
- return false;
39
- }
40
- return {
41
- enabled,
42
- actor: env.THINGD_MCP_ACTOR,
43
- source: env.THINGD_MCP_SOURCE,
44
- stream: env.THINGD_MCP_AUDIT_STREAM,
45
- };
46
- }
47
- export function readCliValue(args, index, name) {
48
- const value = args[index + 1];
49
- if (!value) {
50
- throw new Error(`${name} requires a value`);
51
- }
52
- return value;
53
- }
54
- export function ensureHttpRuntimeIsSafe(options) {
55
- const authToken = options.authToken?.trim();
56
- if (authToken || options.allowUnauthenticated || isLoopbackHost(options.host)) {
57
- return;
58
- }
59
- throw new Error("THINGD_AUTH_TOKEN is required when the HTTP MCP runtime binds to a non-loopback host. Set THINGD_AUTH_TOKEN or THINGD_ALLOW_UNAUTHENTICATED=true for local-only experiments.");
60
- }
61
- function isLoopbackHost(host) {
62
- const normalized = host.toLowerCase();
63
- return (normalized === "localhost" ||
64
- normalized === "127.0.0.1" ||
65
- normalized === "::1" ||
66
- normalized === "[::1]");
67
- }
@@ -1,25 +0,0 @@
1
- import { type Server } from "node:http";
2
- import { type ThingdMcpAuditOptions, type ThingdMcpHardeningOptions } from "@thingd/sdk";
3
- import { type ThingdClusterOptions } from "./cluster.js";
4
- import { type ThingDStorageDriver } from "./config.js";
5
- export type ThingdHttpServerOptions = {
6
- path: string;
7
- driver?: ThingDStorageDriver;
8
- host?: string;
9
- port?: number;
10
- authToken?: string;
11
- allowUnauthenticated?: boolean;
12
- audit?: ThingdMcpAuditOptions | false;
13
- cluster?: ThingdClusterOptions;
14
- mcpPath?: string;
15
- healthPath?: string;
16
- hardening?: ThingdMcpHardeningOptions;
17
- };
18
- export type RunningThingdHttpServer = {
19
- server: Server;
20
- url: string;
21
- mcpUrl: string;
22
- close(): Promise<void>;
23
- };
24
- export declare function startThingdHttpServer(options: ThingdHttpServerOptions): Promise<RunningThingdHttpServer>;
25
- //# sourceMappingURL=http.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/mcp/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AAGjG,OAAO,EAKL,KAAK,qBAAqB,EAC1B,KAAK,yBAAyB,EAC/B,MAAM,aAAa,CAAC;AACrB,OAAO,EAML,KAAK,oBAAoB,EAC1B,MAAM,cAAc,CAAC;AACtB,OAAO,EAA2B,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEhF,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,KAAK,CAAC,EAAE,qBAAqB,GAAG,KAAK,CAAC;IACtC,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,yBAAyB,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB,CAAC;AAmBF,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,uBAAuB,CAAC,CAqDlC"}