@primitivedotdev/cli 0.33.0 → 0.35.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.
@@ -0,0 +1,1304 @@
1
+ import { Errors } from "@oclif/core";
2
+ import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { randomUUID } from "node:crypto";
4
+ import { join } from "node:path";
5
+ //#region ../packages/api-core/src/api/core/bodySerializer.gen.ts
6
+ const jsonBodySerializer = { bodySerializer: (body) => JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value) };
7
+ //#endregion
8
+ //#region ../packages/api-core/src/api/core/serverSentEvents.gen.ts
9
+ function createSseClient({ onRequest, onSseError, onSseEvent, responseTransformer, responseValidator, sseDefaultRetryDelay, sseMaxRetryAttempts, sseMaxRetryDelay, sseSleepFn, url, ...options }) {
10
+ let lastEventId;
11
+ const sleep = sseSleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
12
+ const createStream = async function* () {
13
+ let retryDelay = sseDefaultRetryDelay ?? 3e3;
14
+ let attempt = 0;
15
+ const signal = options.signal ?? new AbortController().signal;
16
+ while (true) {
17
+ if (signal.aborted) break;
18
+ attempt++;
19
+ const headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers);
20
+ if (lastEventId !== void 0) headers.set("Last-Event-ID", lastEventId);
21
+ try {
22
+ const requestInit = {
23
+ redirect: "follow",
24
+ ...options,
25
+ body: options.serializedBody,
26
+ headers,
27
+ signal
28
+ };
29
+ let request = new Request(url, requestInit);
30
+ if (onRequest) request = await onRequest(url, requestInit);
31
+ const response = await (options.fetch ?? globalThis.fetch)(request);
32
+ if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`);
33
+ if (!response.body) throw new Error("No body in SSE response");
34
+ const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
35
+ let buffer = "";
36
+ const abortHandler = () => {
37
+ try {
38
+ reader.cancel();
39
+ } catch {}
40
+ };
41
+ signal.addEventListener("abort", abortHandler);
42
+ try {
43
+ while (true) {
44
+ const { done, value } = await reader.read();
45
+ if (done) break;
46
+ buffer += value;
47
+ buffer = buffer.replace(/\r\n?/g, "\n");
48
+ const chunks = buffer.split("\n\n");
49
+ buffer = chunks.pop() ?? "";
50
+ for (const chunk of chunks) {
51
+ const lines = chunk.split("\n");
52
+ const dataLines = [];
53
+ let eventName;
54
+ for (const line of lines) if (line.startsWith("data:")) dataLines.push(line.replace(/^data:\s*/, ""));
55
+ else if (line.startsWith("event:")) eventName = line.replace(/^event:\s*/, "");
56
+ else if (line.startsWith("id:")) lastEventId = line.replace(/^id:\s*/, "");
57
+ else if (line.startsWith("retry:")) {
58
+ const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10);
59
+ if (!Number.isNaN(parsed)) retryDelay = parsed;
60
+ }
61
+ let data;
62
+ let parsedJson = false;
63
+ if (dataLines.length) {
64
+ const rawData = dataLines.join("\n");
65
+ try {
66
+ data = JSON.parse(rawData);
67
+ parsedJson = true;
68
+ } catch {
69
+ data = rawData;
70
+ }
71
+ }
72
+ if (parsedJson) {
73
+ if (responseValidator) await responseValidator(data);
74
+ if (responseTransformer) data = await responseTransformer(data);
75
+ }
76
+ onSseEvent?.({
77
+ data,
78
+ event: eventName,
79
+ id: lastEventId,
80
+ retry: retryDelay
81
+ });
82
+ if (dataLines.length) yield data;
83
+ }
84
+ }
85
+ } finally {
86
+ signal.removeEventListener("abort", abortHandler);
87
+ reader.releaseLock();
88
+ }
89
+ break;
90
+ } catch (error) {
91
+ onSseError?.(error);
92
+ if (sseMaxRetryAttempts !== void 0 && attempt >= sseMaxRetryAttempts) break;
93
+ await sleep(Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 3e4));
94
+ }
95
+ }
96
+ };
97
+ return { stream: createStream() };
98
+ }
99
+ //#endregion
100
+ //#region ../packages/api-core/src/api/core/pathSerializer.gen.ts
101
+ const separatorArrayExplode = (style) => {
102
+ switch (style) {
103
+ case "label": return ".";
104
+ case "matrix": return ";";
105
+ case "simple": return ",";
106
+ default: return "&";
107
+ }
108
+ };
109
+ const separatorArrayNoExplode = (style) => {
110
+ switch (style) {
111
+ case "form": return ",";
112
+ case "pipeDelimited": return "|";
113
+ case "spaceDelimited": return "%20";
114
+ default: return ",";
115
+ }
116
+ };
117
+ const separatorObjectExplode = (style) => {
118
+ switch (style) {
119
+ case "label": return ".";
120
+ case "matrix": return ";";
121
+ case "simple": return ",";
122
+ default: return "&";
123
+ }
124
+ };
125
+ const serializeArrayParam = ({ allowReserved, explode, name, style, value }) => {
126
+ if (!explode) {
127
+ const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v))).join(separatorArrayNoExplode(style));
128
+ switch (style) {
129
+ case "label": return `.${joinedValues}`;
130
+ case "matrix": return `;${name}=${joinedValues}`;
131
+ case "simple": return joinedValues;
132
+ default: return `${name}=${joinedValues}`;
133
+ }
134
+ }
135
+ const separator = separatorArrayExplode(style);
136
+ const joinedValues = value.map((v) => {
137
+ if (style === "label" || style === "simple") return allowReserved ? v : encodeURIComponent(v);
138
+ return serializePrimitiveParam({
139
+ allowReserved,
140
+ name,
141
+ value: v
142
+ });
143
+ }).join(separator);
144
+ return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
145
+ };
146
+ const serializePrimitiveParam = ({ allowReserved, name, value }) => {
147
+ if (value === void 0 || value === null) return "";
148
+ if (typeof value === "object") throw new Error("Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.");
149
+ return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
150
+ };
151
+ const serializeObjectParam = ({ allowReserved, explode, name, style, value, valueOnly }) => {
152
+ if (value instanceof Date) return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
153
+ if (style !== "deepObject" && !explode) {
154
+ let values = [];
155
+ Object.entries(value).forEach(([key, v]) => {
156
+ values = [
157
+ ...values,
158
+ key,
159
+ allowReserved ? v : encodeURIComponent(v)
160
+ ];
161
+ });
162
+ const joinedValues = values.join(",");
163
+ switch (style) {
164
+ case "form": return `${name}=${joinedValues}`;
165
+ case "label": return `.${joinedValues}`;
166
+ case "matrix": return `;${name}=${joinedValues}`;
167
+ default: return joinedValues;
168
+ }
169
+ }
170
+ const separator = separatorObjectExplode(style);
171
+ const joinedValues = Object.entries(value).map(([key, v]) => serializePrimitiveParam({
172
+ allowReserved,
173
+ name: style === "deepObject" ? `${name}[${key}]` : key,
174
+ value: v
175
+ })).join(separator);
176
+ return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
177
+ };
178
+ //#endregion
179
+ //#region ../packages/api-core/src/api/core/utils.gen.ts
180
+ const PATH_PARAM_RE = /\{[^{}]+\}/g;
181
+ const defaultPathSerializer = ({ path, url: _url }) => {
182
+ let url = _url;
183
+ const matches = _url.match(PATH_PARAM_RE);
184
+ if (matches) for (const match of matches) {
185
+ let explode = false;
186
+ let name = match.substring(1, match.length - 1);
187
+ let style = "simple";
188
+ if (name.endsWith("*")) {
189
+ explode = true;
190
+ name = name.substring(0, name.length - 1);
191
+ }
192
+ if (name.startsWith(".")) {
193
+ name = name.substring(1);
194
+ style = "label";
195
+ } else if (name.startsWith(";")) {
196
+ name = name.substring(1);
197
+ style = "matrix";
198
+ }
199
+ const value = path[name];
200
+ if (value === void 0 || value === null) continue;
201
+ if (Array.isArray(value)) {
202
+ url = url.replace(match, serializeArrayParam({
203
+ explode,
204
+ name,
205
+ style,
206
+ value
207
+ }));
208
+ continue;
209
+ }
210
+ if (typeof value === "object") {
211
+ url = url.replace(match, serializeObjectParam({
212
+ explode,
213
+ name,
214
+ style,
215
+ value,
216
+ valueOnly: true
217
+ }));
218
+ continue;
219
+ }
220
+ if (style === "matrix") {
221
+ url = url.replace(match, `;${serializePrimitiveParam({
222
+ name,
223
+ value
224
+ })}`);
225
+ continue;
226
+ }
227
+ const replaceValue = encodeURIComponent(style === "label" ? `.${value}` : value);
228
+ url = url.replace(match, replaceValue);
229
+ }
230
+ return url;
231
+ };
232
+ const getUrl = ({ baseUrl, path, query, querySerializer, url: _url }) => {
233
+ const pathUrl = _url.startsWith("/") ? _url : `/${_url}`;
234
+ let url = (baseUrl ?? "") + pathUrl;
235
+ if (path) url = defaultPathSerializer({
236
+ path,
237
+ url
238
+ });
239
+ let search = query ? querySerializer(query) : "";
240
+ if (search.startsWith("?")) search = search.substring(1);
241
+ if (search) url += `?${search}`;
242
+ return url;
243
+ };
244
+ function getValidRequestBody(options) {
245
+ const hasBody = options.body !== void 0;
246
+ if (hasBody && options.bodySerializer) {
247
+ if ("serializedBody" in options) return options.serializedBody !== void 0 && options.serializedBody !== "" ? options.serializedBody : null;
248
+ return options.body !== "" ? options.body : null;
249
+ }
250
+ if (hasBody) return options.body;
251
+ }
252
+ //#endregion
253
+ //#region ../packages/api-core/src/api/core/auth.gen.ts
254
+ const getAuthToken = async (auth, callback) => {
255
+ const token = typeof callback === "function" ? await callback(auth) : callback;
256
+ if (!token) return;
257
+ if (auth.scheme === "bearer") return `Bearer ${token}`;
258
+ if (auth.scheme === "basic") return `Basic ${btoa(token)}`;
259
+ return token;
260
+ };
261
+ //#endregion
262
+ //#region ../packages/api-core/src/api/client/utils.gen.ts
263
+ const createQuerySerializer = ({ parameters = {}, ...args } = {}) => {
264
+ const querySerializer = (queryParams) => {
265
+ const search = [];
266
+ if (queryParams && typeof queryParams === "object") for (const name in queryParams) {
267
+ const value = queryParams[name];
268
+ if (value === void 0 || value === null) continue;
269
+ const options = parameters[name] || args;
270
+ if (Array.isArray(value)) {
271
+ const serializedArray = serializeArrayParam({
272
+ allowReserved: options.allowReserved,
273
+ explode: true,
274
+ name,
275
+ style: "form",
276
+ value,
277
+ ...options.array
278
+ });
279
+ if (serializedArray) search.push(serializedArray);
280
+ } else if (typeof value === "object") {
281
+ const serializedObject = serializeObjectParam({
282
+ allowReserved: options.allowReserved,
283
+ explode: true,
284
+ name,
285
+ style: "deepObject",
286
+ value,
287
+ ...options.object
288
+ });
289
+ if (serializedObject) search.push(serializedObject);
290
+ } else {
291
+ const serializedPrimitive = serializePrimitiveParam({
292
+ allowReserved: options.allowReserved,
293
+ name,
294
+ value
295
+ });
296
+ if (serializedPrimitive) search.push(serializedPrimitive);
297
+ }
298
+ }
299
+ return search.join("&");
300
+ };
301
+ return querySerializer;
302
+ };
303
+ /**
304
+ * Infers parseAs value from provided Content-Type header.
305
+ */
306
+ const getParseAs = (contentType) => {
307
+ if (!contentType) return "stream";
308
+ const cleanContent = contentType.split(";")[0]?.trim();
309
+ if (!cleanContent) return;
310
+ if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) return "json";
311
+ if (cleanContent === "multipart/form-data") return "formData";
312
+ if ([
313
+ "application/",
314
+ "audio/",
315
+ "image/",
316
+ "video/"
317
+ ].some((type) => cleanContent.startsWith(type))) return "blob";
318
+ if (cleanContent.startsWith("text/")) return "text";
319
+ };
320
+ const checkForExistence = (options, name) => {
321
+ if (!name) return false;
322
+ if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) return true;
323
+ return false;
324
+ };
325
+ const setAuthParams = async ({ security, ...options }) => {
326
+ for (const auth of security) {
327
+ if (checkForExistence(options, auth.name)) continue;
328
+ const token = await getAuthToken(auth, options.auth);
329
+ if (!token) continue;
330
+ const name = auth.name ?? "Authorization";
331
+ switch (auth.in) {
332
+ case "query":
333
+ if (!options.query) options.query = {};
334
+ options.query[name] = token;
335
+ break;
336
+ case "cookie":
337
+ options.headers.append("Cookie", `${name}=${token}`);
338
+ break;
339
+ default:
340
+ options.headers.set(name, token);
341
+ break;
342
+ }
343
+ }
344
+ };
345
+ const buildUrl = (options) => getUrl({
346
+ baseUrl: options.baseUrl,
347
+ path: options.path,
348
+ query: options.query,
349
+ querySerializer: typeof options.querySerializer === "function" ? options.querySerializer : createQuerySerializer(options.querySerializer),
350
+ url: options.url
351
+ });
352
+ const mergeConfigs = (a, b) => {
353
+ const config = {
354
+ ...a,
355
+ ...b
356
+ };
357
+ if (config.baseUrl?.endsWith("/")) config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
358
+ config.headers = mergeHeaders(a.headers, b.headers);
359
+ return config;
360
+ };
361
+ const headersEntries = (headers) => {
362
+ const entries = [];
363
+ headers.forEach((value, key) => {
364
+ entries.push([key, value]);
365
+ });
366
+ return entries;
367
+ };
368
+ const mergeHeaders = (...headers) => {
369
+ const mergedHeaders = new Headers();
370
+ for (const header of headers) {
371
+ if (!header) continue;
372
+ const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header);
373
+ for (const [key, value] of iterator) if (value === null) mergedHeaders.delete(key);
374
+ else if (Array.isArray(value)) for (const v of value) mergedHeaders.append(key, v);
375
+ else if (value !== void 0) mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : value);
376
+ }
377
+ return mergedHeaders;
378
+ };
379
+ var Interceptors = class {
380
+ fns = [];
381
+ clear() {
382
+ this.fns = [];
383
+ }
384
+ eject(id) {
385
+ const index = this.getInterceptorIndex(id);
386
+ if (this.fns[index]) this.fns[index] = null;
387
+ }
388
+ exists(id) {
389
+ const index = this.getInterceptorIndex(id);
390
+ return Boolean(this.fns[index]);
391
+ }
392
+ getInterceptorIndex(id) {
393
+ if (typeof id === "number") return this.fns[id] ? id : -1;
394
+ return this.fns.indexOf(id);
395
+ }
396
+ update(id, fn) {
397
+ const index = this.getInterceptorIndex(id);
398
+ if (this.fns[index]) {
399
+ this.fns[index] = fn;
400
+ return id;
401
+ }
402
+ return false;
403
+ }
404
+ use(fn) {
405
+ this.fns.push(fn);
406
+ return this.fns.length - 1;
407
+ }
408
+ };
409
+ const createInterceptors = () => ({
410
+ error: new Interceptors(),
411
+ request: new Interceptors(),
412
+ response: new Interceptors()
413
+ });
414
+ const defaultQuerySerializer = createQuerySerializer({
415
+ allowReserved: false,
416
+ array: {
417
+ explode: true,
418
+ style: "form"
419
+ },
420
+ object: {
421
+ explode: true,
422
+ style: "deepObject"
423
+ }
424
+ });
425
+ const defaultHeaders = { "Content-Type": "application/json" };
426
+ const createConfig = (override = {}) => ({
427
+ ...jsonBodySerializer,
428
+ headers: defaultHeaders,
429
+ parseAs: "auto",
430
+ querySerializer: defaultQuerySerializer,
431
+ ...override
432
+ });
433
+ //#endregion
434
+ //#region ../packages/api-core/src/api/client/client.gen.ts
435
+ const createClient = (config = {}) => {
436
+ let _config = mergeConfigs(createConfig(), config);
437
+ const getConfig = () => ({ ..._config });
438
+ const setConfig = (config) => {
439
+ _config = mergeConfigs(_config, config);
440
+ return getConfig();
441
+ };
442
+ const interceptors = createInterceptors();
443
+ const beforeRequest = async (options) => {
444
+ const opts = {
445
+ ..._config,
446
+ ...options,
447
+ fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
448
+ headers: mergeHeaders(_config.headers, options.headers),
449
+ serializedBody: void 0
450
+ };
451
+ if (opts.security) await setAuthParams({
452
+ ...opts,
453
+ security: opts.security
454
+ });
455
+ if (opts.requestValidator) await opts.requestValidator(opts);
456
+ if (opts.body !== void 0 && opts.bodySerializer) opts.serializedBody = opts.bodySerializer(opts.body);
457
+ if (opts.body === void 0 || opts.serializedBody === "") opts.headers.delete("Content-Type");
458
+ const resolvedOpts = opts;
459
+ return {
460
+ opts: resolvedOpts,
461
+ url: buildUrl(resolvedOpts)
462
+ };
463
+ };
464
+ const request = async (options) => {
465
+ const { opts, url } = await beforeRequest(options);
466
+ const requestInit = {
467
+ redirect: "follow",
468
+ ...opts,
469
+ body: getValidRequestBody(opts)
470
+ };
471
+ let request = new Request(url, requestInit);
472
+ for (const fn of interceptors.request.fns) if (fn) request = await fn(request, opts);
473
+ const _fetch = opts.fetch;
474
+ let response;
475
+ try {
476
+ response = await _fetch(request);
477
+ } catch (error) {
478
+ let finalError = error;
479
+ for (const fn of interceptors.error.fns) if (fn) finalError = await fn(error, void 0, request, opts);
480
+ finalError = finalError || {};
481
+ if (opts.throwOnError) throw finalError;
482
+ return opts.responseStyle === "data" ? void 0 : {
483
+ error: finalError,
484
+ request,
485
+ response: void 0
486
+ };
487
+ }
488
+ for (const fn of interceptors.response.fns) if (fn) response = await fn(response, request, opts);
489
+ const result = {
490
+ request,
491
+ response
492
+ };
493
+ if (response.ok) {
494
+ const parseAs = (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json";
495
+ if (response.status === 204 || response.headers.get("Content-Length") === "0") {
496
+ let emptyData;
497
+ switch (parseAs) {
498
+ case "arrayBuffer":
499
+ case "blob":
500
+ case "text":
501
+ emptyData = await response[parseAs]();
502
+ break;
503
+ case "formData":
504
+ emptyData = new FormData();
505
+ break;
506
+ case "stream":
507
+ emptyData = response.body;
508
+ break;
509
+ default:
510
+ emptyData = {};
511
+ break;
512
+ }
513
+ return opts.responseStyle === "data" ? emptyData : {
514
+ data: emptyData,
515
+ ...result
516
+ };
517
+ }
518
+ let data;
519
+ switch (parseAs) {
520
+ case "arrayBuffer":
521
+ case "blob":
522
+ case "formData":
523
+ case "text":
524
+ data = await response[parseAs]();
525
+ break;
526
+ case "json": {
527
+ const text = await response.text();
528
+ data = text ? JSON.parse(text) : {};
529
+ break;
530
+ }
531
+ case "stream": return opts.responseStyle === "data" ? response.body : {
532
+ data: response.body,
533
+ ...result
534
+ };
535
+ }
536
+ if (parseAs === "json") {
537
+ if (opts.responseValidator) await opts.responseValidator(data);
538
+ if (opts.responseTransformer) data = await opts.responseTransformer(data);
539
+ }
540
+ return opts.responseStyle === "data" ? data : {
541
+ data,
542
+ ...result
543
+ };
544
+ }
545
+ const textError = await response.text();
546
+ let jsonError;
547
+ try {
548
+ jsonError = JSON.parse(textError);
549
+ } catch {}
550
+ const error = jsonError ?? textError;
551
+ let finalError = error;
552
+ for (const fn of interceptors.error.fns) if (fn) finalError = await fn(error, response, request, opts);
553
+ finalError = finalError || {};
554
+ if (opts.throwOnError) throw finalError;
555
+ return opts.responseStyle === "data" ? void 0 : {
556
+ error: finalError,
557
+ ...result
558
+ };
559
+ };
560
+ const makeMethodFn = (method) => (options) => request({
561
+ ...options,
562
+ method
563
+ });
564
+ const makeSseFn = (method) => async (options) => {
565
+ const { opts, url } = await beforeRequest(options);
566
+ return createSseClient({
567
+ ...opts,
568
+ body: opts.body,
569
+ headers: opts.headers,
570
+ method,
571
+ onRequest: async (url, init) => {
572
+ let request = new Request(url, init);
573
+ for (const fn of interceptors.request.fns) if (fn) request = await fn(request, opts);
574
+ return request;
575
+ },
576
+ serializedBody: getValidRequestBody(opts),
577
+ url
578
+ });
579
+ };
580
+ const _buildUrl = (options) => buildUrl({
581
+ ..._config,
582
+ ...options
583
+ });
584
+ return {
585
+ buildUrl: _buildUrl,
586
+ connect: makeMethodFn("CONNECT"),
587
+ delete: makeMethodFn("DELETE"),
588
+ get: makeMethodFn("GET"),
589
+ getConfig,
590
+ head: makeMethodFn("HEAD"),
591
+ interceptors,
592
+ options: makeMethodFn("OPTIONS"),
593
+ patch: makeMethodFn("PATCH"),
594
+ post: makeMethodFn("POST"),
595
+ put: makeMethodFn("PUT"),
596
+ request,
597
+ setConfig,
598
+ sse: {
599
+ connect: makeSseFn("CONNECT"),
600
+ delete: makeSseFn("DELETE"),
601
+ get: makeSseFn("GET"),
602
+ head: makeSseFn("HEAD"),
603
+ options: makeSseFn("OPTIONS"),
604
+ patch: makeSseFn("PATCH"),
605
+ post: makeSseFn("POST"),
606
+ put: makeSseFn("PUT"),
607
+ trace: makeSseFn("TRACE")
608
+ },
609
+ trace: makeMethodFn("TRACE")
610
+ };
611
+ };
612
+ //#endregion
613
+ //#region ../packages/api-core/src/client.ts
614
+ /**
615
+ * Host-aware Primitive API client and shared error type.
616
+ *
617
+ * Lives in api-core (instead of sdk-node) so the CLI can build a
618
+ * configured request client without taking a dependency on sdk-node.
619
+ * The higher-level `PrimitiveClient` (with `.send`, `.reply`,
620
+ * `.forward`) still lives in sdk-node because it needs the
621
+ * `ReceivedEmail` type from the webhook parsing surface.
622
+ */
623
+ const DEFAULT_API_BASE_URL_1 = "https://www.primitive.dev/api/v1";
624
+ const DEFAULT_API_BASE_URL_2 = "https://api.primitive.dev/v1";
625
+ function createDefaultAuth(apiKey) {
626
+ return (security) => {
627
+ if (security.type === "http" && security.scheme === "bearer") return apiKey;
628
+ };
629
+ }
630
+ var PrimitiveApiClient = class {
631
+ /**
632
+ * Generated client targeting the primary API host (apiBaseUrl1). Use
633
+ * this when passing `client: ...` to a generated operation function
634
+ * for every endpoint EXCEPT attachment-capable message sends. The
635
+ * hand-written PrimitiveClient.send / .reply / .forward methods on
636
+ * the subclass route those sends to the host-2 client internally.
637
+ */
638
+ client;
639
+ /**
640
+ * @internal Generated client targeting the attachments-supporting
641
+ * send host (apiBaseUrl2). Used by PrimitiveClient.send() and
642
+ * PrimitiveClient.reply() under the hood. Exposed for the CLI's
643
+ * hand-rolled send/reply commands, which call generated operations
644
+ * directly; not part of the publicly-documented SDK surface.
645
+ * Customer code should call .send() / .reply() on the subclass
646
+ * instead.
647
+ */
648
+ _sendClient;
649
+ constructor(options = {}) {
650
+ const { apiKey, auth, apiBaseUrl1 = DEFAULT_API_BASE_URL_1, apiBaseUrl2 = DEFAULT_API_BASE_URL_2, ...config } = options;
651
+ const resolvedAuth = auth ?? createDefaultAuth(apiKey);
652
+ this.client = createClient(createConfig({
653
+ ...config,
654
+ auth: resolvedAuth,
655
+ baseUrl: apiBaseUrl1
656
+ }));
657
+ this._sendClient = createClient(createConfig({
658
+ ...config,
659
+ auth: resolvedAuth,
660
+ baseUrl: apiBaseUrl2
661
+ }));
662
+ }
663
+ getConfig() {
664
+ return this.client.getConfig();
665
+ }
666
+ setConfig(config) {
667
+ return this.client.setConfig(config);
668
+ }
669
+ };
670
+ //#endregion
671
+ //#region src/oclif/chat-state.ts
672
+ const CHAT_STATE_FILE = "chat-state.json";
673
+ const CHAT_STATE_VERSION = 2;
674
+ const MAX_CONVERSATIONS = 50;
675
+ function isRecord$2(value) {
676
+ return value !== null && typeof value === "object" && !Array.isArray(value);
677
+ }
678
+ function nonEmptyString(value) {
679
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
680
+ }
681
+ function positiveInteger(value) {
682
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null;
683
+ }
684
+ function nonNegativeInteger(value) {
685
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
686
+ }
687
+ function parseLocalId(value) {
688
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= Number.MAX_SAFE_INTEGER ? value : null;
689
+ }
690
+ function chatStatePath(configDir) {
691
+ return join(configDir, CHAT_STATE_FILE);
692
+ }
693
+ function deleteChatState(configDir) {
694
+ rmSync(chatStatePath(configDir), { force: true });
695
+ }
696
+ function chatConversationState(params) {
697
+ return {
698
+ ...params.input,
699
+ local_id: params.localId,
700
+ updated_at: params.input.updated_at ?? (/* @__PURE__ */ new Date()).toISOString()
701
+ };
702
+ }
703
+ function parseConversation(raw) {
704
+ if (!isRecord$2(raw)) return null;
705
+ const localId = parseLocalId(raw.local_id);
706
+ const recipient = nonEmptyString(raw.recipient);
707
+ const from = nonEmptyString(raw.from);
708
+ const lastReplyEmailId = nonEmptyString(raw.last_reply_email_id);
709
+ const lastSentEmailId = nonEmptyString(raw.last_sent_email_id);
710
+ const lastReplyReceivedAt = nonEmptyString(raw.last_reply_received_at);
711
+ const updatedAt = nonEmptyString(raw.updated_at);
712
+ const timeoutSeconds = nonNegativeInteger(raw.timeout_seconds);
713
+ const strictPhaseSeconds = positiveInteger(raw.strict_phase_seconds);
714
+ if (localId === null || !recipient || !from || !lastReplyEmailId || !lastSentEmailId || !lastReplyReceivedAt || !updatedAt || timeoutSeconds === null || strictPhaseSeconds === null || typeof raw.strict_only !== "boolean") return null;
715
+ const threadId = raw.thread_id !== null && raw.thread_id !== void 0 ? nonEmptyString(raw.thread_id) : null;
716
+ if (raw.thread_id !== null && raw.thread_id !== void 0 && !threadId) return null;
717
+ return {
718
+ from,
719
+ last_reply_email_id: lastReplyEmailId,
720
+ last_reply_received_at: lastReplyReceivedAt,
721
+ last_sent_email_id: lastSentEmailId,
722
+ local_id: localId,
723
+ recipient,
724
+ strict_only: raw.strict_only,
725
+ strict_phase_seconds: strictPhaseSeconds,
726
+ thread_id: threadId,
727
+ timeout_seconds: timeoutSeconds,
728
+ updated_at: updatedAt
729
+ };
730
+ }
731
+ function parseLegacyActiveState(raw) {
732
+ if (!isRecord$2(raw) || raw.version !== 1) return null;
733
+ const legacy = parseConversation({
734
+ ...raw,
735
+ local_id: 0
736
+ });
737
+ if (!legacy) return null;
738
+ return {
739
+ active_local_id: 0,
740
+ conversations: [legacy],
741
+ next_local_id: 1,
742
+ version: CHAT_STATE_VERSION
743
+ };
744
+ }
745
+ function parseChatState(raw) {
746
+ if (!isRecord$2(raw)) return null;
747
+ if (raw.version === 1) return parseLegacyActiveState(raw);
748
+ if (raw.version !== CHAT_STATE_VERSION) return null;
749
+ if (!Array.isArray(raw.conversations)) return null;
750
+ const conversations = raw.conversations.map((entry) => parseConversation(entry)).filter((entry) => entry !== null);
751
+ if (conversations.length !== raw.conversations.length) return null;
752
+ const ids = /* @__PURE__ */ new Set();
753
+ for (const conversation of conversations) {
754
+ if (ids.has(conversation.local_id)) return null;
755
+ ids.add(conversation.local_id);
756
+ }
757
+ const activeLocalId = raw.active_local_id === null ? null : parseLocalId(raw.active_local_id);
758
+ if (activeLocalId === null && raw.active_local_id !== null) return null;
759
+ if (activeLocalId !== null && !ids.has(activeLocalId)) return null;
760
+ const nextLocalId = nonNegativeInteger(raw.next_local_id) ?? 0;
761
+ return {
762
+ active_local_id: activeLocalId,
763
+ conversations,
764
+ next_local_id: Math.max(nextLocalId, ...conversations.map((conversation) => conversation.local_id + 1), 0),
765
+ version: CHAT_STATE_VERSION
766
+ };
767
+ }
768
+ function loadChatState(configDir) {
769
+ let contents;
770
+ try {
771
+ contents = readFileSync(chatStatePath(configDir), "utf8");
772
+ } catch {
773
+ return null;
774
+ }
775
+ try {
776
+ return parseChatState(JSON.parse(contents));
777
+ } catch {
778
+ return null;
779
+ }
780
+ }
781
+ function loadActiveChatState(configDir) {
782
+ const state = loadChatState(configDir);
783
+ if (state?.active_local_id === null || state === null) return null;
784
+ return state.conversations.find((conversation) => conversation.local_id === state.active_local_id) ?? null;
785
+ }
786
+ function loadChatConversationByLocalId(configDir, localId) {
787
+ return loadChatState(configDir)?.conversations.find((conversation) => conversation.local_id === localId) ?? null;
788
+ }
789
+ function saveChatState(configDir, state) {
790
+ mkdirSync(configDir, {
791
+ mode: 448,
792
+ recursive: true
793
+ });
794
+ const path = chatStatePath(configDir);
795
+ const tempPath = join(configDir, `${CHAT_STATE_FILE}.${process.pid}.${randomUUID()}.tmp`);
796
+ try {
797
+ writeFileSync(tempPath, `${JSON.stringify(state, null, 2)}\n`, { mode: 384 });
798
+ renameSync(tempPath, path);
799
+ } catch (error) {
800
+ rmSync(tempPath, { force: true });
801
+ throw error;
802
+ }
803
+ }
804
+ function compareConversationUpdatedAt(left, right) {
805
+ return right.updated_at.localeCompare(left.updated_at);
806
+ }
807
+ function saveActiveChatState(configDir, input, options = {}) {
808
+ const existing = loadChatState(configDir) ?? {
809
+ active_local_id: null,
810
+ conversations: [],
811
+ next_local_id: 0,
812
+ version: CHAT_STATE_VERSION
813
+ };
814
+ const existingByPreferredId = options.preferredLocalId === void 0 ? void 0 : existing.conversations.find((conversation) => conversation.local_id === options.preferredLocalId);
815
+ const existingByThread = input.thread_id === null ? void 0 : existing.conversations.find((conversation) => conversation.thread_id === input.thread_id);
816
+ const localId = existingByPreferredId?.local_id ?? existingByThread?.local_id ?? existing.next_local_id;
817
+ const conversation = chatConversationState({
818
+ input,
819
+ localId
820
+ });
821
+ const conversations = [conversation, ...existing.conversations.filter((item) => item.local_id !== localId)].sort(compareConversationUpdatedAt).slice(0, MAX_CONVERSATIONS);
822
+ const activeStillPresent = conversations.some((item) => item.local_id === localId);
823
+ const nextLocalId = Math.max(existing.next_local_id, localId + 1, ...conversations.map((item) => item.local_id + 1));
824
+ saveChatState(configDir, {
825
+ active_local_id: activeStillPresent ? localId : null,
826
+ conversations,
827
+ next_local_id: nextLocalId,
828
+ version: CHAT_STATE_VERSION
829
+ });
830
+ return conversation;
831
+ }
832
+ //#endregion
833
+ //#region src/oclif/auth.ts
834
+ const CREDENTIALS_FILE = "credentials.json";
835
+ const CREDENTIALS_LOCK_DIR = "credentials.lock";
836
+ const CREDENTIALS_LOCK_OWNER_FILE = "owner.json";
837
+ const CREDENTIALS_LOCK_STALE_MS = 1800 * 1e3;
838
+ const MALFORMED_CREDENTIALS_HINT = "Run `primitive logout` and then `primitive signin`.";
839
+ const CREDENTIALS_LOCK_CLEANUP_SIGNALS = [
840
+ "SIGINT",
841
+ "SIGTERM",
842
+ "SIGHUP"
843
+ ];
844
+ function isRecord$1(value) {
845
+ return value !== null && typeof value === "object" && !Array.isArray(value);
846
+ }
847
+ function requireString(value, key) {
848
+ const raw = value[key];
849
+ if (typeof raw !== "string" || raw.trim().length === 0) throw new Error(`Stored Primitive CLI credentials are malformed: ${key} must be a non-empty string. ${MALFORMED_CREDENTIALS_HINT}`);
850
+ return raw;
851
+ }
852
+ /**
853
+ * Sentinel returned by parseCredentials when the on-disk credentials were
854
+ * written by an API-key-based CLI. The caller treats this as "not logged in"
855
+ * after clearing the local file. The backing API key is intentionally not
856
+ * revoked; API keys still work when passed explicitly via --api-key/env.
857
+ */
858
+ var LegacyApiKeyCredentialFormatError = class extends Error {
859
+ constructor() {
860
+ super("legacy_api_key_credential_format");
861
+ this.name = "LegacyApiKeyCredentialFormatError";
862
+ }
863
+ };
864
+ function parseCredentials(raw) {
865
+ if (!isRecord$1(raw)) throw new Error(`Stored Primitive CLI credentials are malformed: expected a JSON object. ${MALFORMED_CREDENTIALS_HINT}`);
866
+ if (raw.auth_method !== "oauth") {
867
+ if (typeof raw.api_key === "string" || typeof raw.key_id === "string" || typeof raw.base_url === "string") throw new LegacyApiKeyCredentialFormatError();
868
+ throw new Error(`Stored Primitive CLI credentials are malformed: auth_method must be oauth. ${MALFORMED_CREDENTIALS_HINT}`);
869
+ }
870
+ const orgName = raw.org_name;
871
+ if (orgName !== null && typeof orgName !== "string") throw new Error(`Stored Primitive CLI credentials are malformed: org_name must be a string or null. ${MALFORMED_CREDENTIALS_HINT}`);
872
+ if (requireString(raw, "token_type") !== "Bearer") throw new Error(`Stored Primitive CLI credentials are malformed: token_type must be Bearer. ${MALFORMED_CREDENTIALS_HINT}`);
873
+ return {
874
+ auth_method: "oauth",
875
+ access_token: requireString(raw, "access_token"),
876
+ refresh_token: requireString(raw, "refresh_token"),
877
+ token_type: "Bearer",
878
+ expires_at: requireString(raw, "expires_at"),
879
+ oauth_grant_id: requireString(raw, "oauth_grant_id"),
880
+ oauth_client_id: requireString(raw, "oauth_client_id"),
881
+ org_id: requireString(raw, "org_id"),
882
+ org_name: orgName,
883
+ api_base_url_1: requireString(raw, "api_base_url_1"),
884
+ created_at: requireString(raw, "created_at")
885
+ };
886
+ }
887
+ function credentialsPath(configDir) {
888
+ return join(configDir, CREDENTIALS_FILE);
889
+ }
890
+ function credentialsLockPath(configDir) {
891
+ return join(configDir, CREDENTIALS_LOCK_DIR);
892
+ }
893
+ function normalize(url, fallback) {
894
+ const trimmed = url?.trim();
895
+ if (!trimmed) return fallback;
896
+ return trimmed.replace(/\/+$/, "");
897
+ }
898
+ function normalizeApiBaseUrl1(url) {
899
+ return normalize(url, DEFAULT_API_BASE_URL_1);
900
+ }
901
+ function normalizeApiBaseUrl2(url) {
902
+ return normalize(url, DEFAULT_API_BASE_URL_2);
903
+ }
904
+ function cliAccessTokenExpiresAt(expiresInSeconds, now = Date.now) {
905
+ return new Date(now() + expiresInSeconds * 1e3).toISOString();
906
+ }
907
+ function loadCliCredentials(configDir) {
908
+ const path = credentialsPath(configDir);
909
+ let contents;
910
+ try {
911
+ contents = readFileSync(path, "utf8");
912
+ } catch (error) {
913
+ if (error && typeof error === "object" && error.code === "ENOENT") return null;
914
+ const detail = error instanceof Error ? error.message : String(error);
915
+ throw new Error(`Could not read Primitive CLI credentials: ${detail}`);
916
+ }
917
+ try {
918
+ return parseCredentials(JSON.parse(contents));
919
+ } catch (error) {
920
+ if (error instanceof LegacyApiKeyCredentialFormatError) {
921
+ try {
922
+ rmSync(path, { force: true });
923
+ deleteChatState(configDir);
924
+ } catch {}
925
+ process.stderr.write("Removed local Primitive CLI API-key login state. API keys are still valid when passed explicitly, but saved CLI auth now uses OAuth. Run `primitive signin` to create an OAuth session. No API key was revoked.\n");
926
+ return null;
927
+ }
928
+ if (error instanceof SyntaxError) throw new Error("Stored Primitive CLI credentials are not valid JSON. Run `primitive logout` and then `primitive signin`.");
929
+ throw error;
930
+ }
931
+ }
932
+ function saveCliCredentials(configDir, credentials) {
933
+ mkdirSync(configDir, {
934
+ mode: 448,
935
+ recursive: true
936
+ });
937
+ const path = credentialsPath(configDir);
938
+ const tempPath = join(configDir, `${CREDENTIALS_FILE}.${process.pid}.${randomUUID()}.tmp`);
939
+ try {
940
+ writeFileSync(tempPath, `${JSON.stringify(credentials, null, 2)}\n`, { mode: 384 });
941
+ chmodSync(tempPath, 384);
942
+ renameSync(tempPath, path);
943
+ chmodSync(path, 384);
944
+ } catch (error) {
945
+ rmSync(tempPath, { force: true });
946
+ throw error;
947
+ }
948
+ }
949
+ function deleteCliCredentials(configDir) {
950
+ rmSync(credentialsPath(configDir), { force: true });
951
+ deleteChatState(configDir);
952
+ }
953
+ function deleteCliCredentialsLock(configDir) {
954
+ rmSync(credentialsLockPath(configDir), {
955
+ force: true,
956
+ recursive: true
957
+ });
958
+ }
959
+ function errorCode(error) {
960
+ return error && typeof error === "object" ? error.code : void 0;
961
+ }
962
+ function removeStaleCliCredentialsLock(lockPath, staleMs, now) {
963
+ try {
964
+ const stats = statSync(lockPath);
965
+ if (now() - stats.mtimeMs < staleMs) return false;
966
+ } catch (error) {
967
+ if (errorCode(error) === "ENOENT") return true;
968
+ throw error;
969
+ }
970
+ rmSync(lockPath, {
971
+ force: true,
972
+ recursive: true
973
+ });
974
+ return true;
975
+ }
976
+ function readCliCredentialsLockOwner(lockPath) {
977
+ let raw;
978
+ try {
979
+ raw = readFileSync(join(lockPath, CREDENTIALS_LOCK_OWNER_FILE), "utf8");
980
+ } catch (error) {
981
+ if (errorCode(error) === "ENOENT") return null;
982
+ throw error;
983
+ }
984
+ try {
985
+ const pid = JSON.parse(raw)?.pid;
986
+ return Number.isInteger(pid) && pid > 0 ? { pid } : null;
987
+ } catch {
988
+ return null;
989
+ }
990
+ }
991
+ function processIsRunning(pid) {
992
+ try {
993
+ process.kill(pid, 0);
994
+ return true;
995
+ } catch (error) {
996
+ if (errorCode(error) === "ESRCH") return false;
997
+ return true;
998
+ }
999
+ }
1000
+ function removeRecoverableCliCredentialsLock(params) {
1001
+ const owner = readCliCredentialsLockOwner(params.lockPath);
1002
+ if (owner && params.isRunning(owner.pid)) return false;
1003
+ if (owner) {
1004
+ rmSync(params.lockPath, {
1005
+ force: true,
1006
+ recursive: true
1007
+ });
1008
+ return true;
1009
+ }
1010
+ return removeStaleCliCredentialsLock(params.lockPath, params.staleMs, params.now);
1011
+ }
1012
+ function writeCliCredentialsLockOwner(lockPath) {
1013
+ const ownerPath = join(lockPath, CREDENTIALS_LOCK_OWNER_FILE);
1014
+ writeFileSync(ownerPath, `${JSON.stringify({
1015
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
1016
+ pid: process.pid
1017
+ })}\n`, { mode: 384 });
1018
+ chmodSync(ownerPath, 384);
1019
+ }
1020
+ function installCredentialsLockSignalCleanup(lockPath) {
1021
+ let active = true;
1022
+ const listeners = CREDENTIALS_LOCK_CLEANUP_SIGNALS.map((signal) => {
1023
+ const listener = () => {
1024
+ if (!active) return;
1025
+ active = false;
1026
+ rmSync(lockPath, {
1027
+ force: true,
1028
+ recursive: true
1029
+ });
1030
+ process.exit(signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 129);
1031
+ };
1032
+ process.once(signal, listener);
1033
+ return {
1034
+ listener,
1035
+ signal
1036
+ };
1037
+ });
1038
+ return () => {
1039
+ if (!active) return;
1040
+ active = false;
1041
+ for (const { listener, signal } of listeners) process.removeListener(signal, listener);
1042
+ };
1043
+ }
1044
+ function credentialsLockInProgressMessage(lockPath) {
1045
+ return `Another Primitive CLI credential operation is already in progress. Wait for it to finish, then retry. If no Primitive auth command is still running, run \`primitive logout --force\` to clear local CLI auth state and remove ${lockPath}.`;
1046
+ }
1047
+ function acquireCliCredentialsLock(configDir, options = {}) {
1048
+ mkdirSync(configDir, {
1049
+ mode: 448,
1050
+ recursive: true
1051
+ });
1052
+ const lockPath = credentialsLockPath(configDir);
1053
+ const installSignalHandlers = options.installSignalHandlers ?? true;
1054
+ const isRunning = options.isProcessRunning ?? processIsRunning;
1055
+ const now = options.now ?? Date.now;
1056
+ const staleMs = options.staleMs ?? CREDENTIALS_LOCK_STALE_MS;
1057
+ let acquired = false;
1058
+ for (let attempt = 0; attempt < 2; attempt += 1) try {
1059
+ mkdirSync(lockPath, { mode: 448 });
1060
+ acquired = true;
1061
+ break;
1062
+ } catch (error) {
1063
+ if (errorCode(error) !== "EEXIST") throw error;
1064
+ if (removeRecoverableCliCredentialsLock({
1065
+ isRunning,
1066
+ lockPath,
1067
+ now,
1068
+ staleMs
1069
+ })) continue;
1070
+ throw new Error(credentialsLockInProgressMessage(lockPath));
1071
+ }
1072
+ if (!acquired) throw new Error(credentialsLockInProgressMessage(lockPath));
1073
+ try {
1074
+ writeCliCredentialsLockOwner(lockPath);
1075
+ } catch (error) {
1076
+ rmSync(lockPath, {
1077
+ force: true,
1078
+ recursive: true
1079
+ });
1080
+ throw error;
1081
+ }
1082
+ const removeSignalCleanup = installSignalHandlers ? installCredentialsLockSignalCleanup(lockPath) : () => void 0;
1083
+ let released = false;
1084
+ return () => {
1085
+ if (released) return;
1086
+ released = true;
1087
+ removeSignalCleanup();
1088
+ rmSync(lockPath, {
1089
+ force: true,
1090
+ recursive: true
1091
+ });
1092
+ };
1093
+ }
1094
+ function resolveCliAuth(params) {
1095
+ const apiKey = params.apiKey?.trim();
1096
+ const apiBaseUrl2 = normalizeApiBaseUrl2(params.apiBaseUrl2);
1097
+ if (apiKey) return {
1098
+ apiKey,
1099
+ apiBaseUrl1: normalizeApiBaseUrl1(params.apiBaseUrl1),
1100
+ apiBaseUrl2,
1101
+ credentials: null,
1102
+ source: "flag-or-env"
1103
+ };
1104
+ const credentials = loadCliCredentials(params.configDir);
1105
+ if (credentials) return {
1106
+ apiKey: credentials.access_token,
1107
+ apiBaseUrl1: credentials.api_base_url_1,
1108
+ apiBaseUrl2,
1109
+ credentials,
1110
+ source: "stored"
1111
+ };
1112
+ return {
1113
+ apiKey: void 0,
1114
+ apiBaseUrl1: normalizeApiBaseUrl1(params.apiBaseUrl1),
1115
+ apiBaseUrl2,
1116
+ credentials: null,
1117
+ source: "none"
1118
+ };
1119
+ }
1120
+ //#endregion
1121
+ //#region src/oclif/cli-config.ts
1122
+ const CONFIG_FILE = "config.json";
1123
+ const CONFIG_VERSION = 1;
1124
+ const DEFAULT_ENVIRONMENT = "default";
1125
+ function cliConfigPath(configDir) {
1126
+ return join(configDir, CONFIG_FILE);
1127
+ }
1128
+ function cliConfigError(message) {
1129
+ return new Errors.CLIError(`${message} Run \`primitive config reset\` to clear the local CLI config.`, { exit: 1 });
1130
+ }
1131
+ function isRecord(value) {
1132
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1133
+ }
1134
+ function normalizeCliEnvironmentName(name) {
1135
+ const trimmed = name?.trim();
1136
+ if (!trimmed) throw new Errors.CLIError("Environment name must be a non-empty string.", { exit: 1 });
1137
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$/.test(trimmed)) throw new Errors.CLIError("Environment name must start with a letter or number and may only contain letters, numbers, '.', '_', or '-'.", { exit: 1 });
1138
+ return trimmed;
1139
+ }
1140
+ function validateCliHeaderName(name) {
1141
+ const trimmed = name.trim();
1142
+ if (!trimmed) throw new Errors.CLIError("Header name must be a non-empty string.", { exit: 1 });
1143
+ if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(trimmed)) throw new Errors.CLIError(`Invalid header name: ${name}`, { exit: 1 });
1144
+ if (trimmed.toLowerCase() === "authorization") throw new Errors.CLIError("The Authorization header is managed by PRIMITIVE_API_KEY or saved OAuth CLI credentials.", { exit: 1 });
1145
+ return trimmed;
1146
+ }
1147
+ function validateCliHeaderValue(value, name) {
1148
+ if (value.length === 0) throw new Errors.CLIError(`Header ${name} value must not be empty.`, { exit: 1 });
1149
+ if (/[\r\n\0]/.test(value)) throw new Errors.CLIError(`Header ${name} value must not contain CR, LF, or NUL characters.`, { exit: 1 });
1150
+ return value;
1151
+ }
1152
+ function parseHeaderAssignment(assignment) {
1153
+ const separator = assignment.indexOf("=");
1154
+ if (separator <= 0) throw new Errors.CLIError("Header values must use name=value syntax, for example `x-custom=secret`.", { exit: 1 });
1155
+ const name = validateCliHeaderName(assignment.slice(0, separator));
1156
+ return [name, validateCliHeaderValue(assignment.slice(separator + 1), name)];
1157
+ }
1158
+ function parseHeaders(raw, context) {
1159
+ if (raw === void 0) return {};
1160
+ if (!isRecord(raw)) throw cliConfigError(`${context} headers must be a JSON object.`);
1161
+ const headers = {};
1162
+ for (const [rawName, rawValue] of Object.entries(raw)) {
1163
+ const name = validateCliHeaderName(rawName);
1164
+ if (typeof rawValue !== "string") throw cliConfigError(`${context} header ${name} must be a string.`);
1165
+ headers[name] = validateCliHeaderValue(rawValue, name);
1166
+ }
1167
+ return headers;
1168
+ }
1169
+ function parseEnvironmentConfig(raw, context) {
1170
+ if (!isRecord(raw)) throw cliConfigError(`${context} must be a JSON object.`);
1171
+ const env = {};
1172
+ if (raw.api_base_url_1 !== void 0) {
1173
+ if (typeof raw.api_base_url_1 !== "string") throw cliConfigError(`${context}.api_base_url_1 must be a string.`);
1174
+ env.api_base_url_1 = normalizeApiBaseUrl1(raw.api_base_url_1);
1175
+ }
1176
+ if (raw.api_base_url_2 !== void 0) {
1177
+ if (typeof raw.api_base_url_2 !== "string") throw cliConfigError(`${context}.api_base_url_2 must be a string.`);
1178
+ env.api_base_url_2 = normalizeApiBaseUrl2(raw.api_base_url_2);
1179
+ }
1180
+ const headers = parseHeaders(raw.headers, context);
1181
+ if (Object.keys(headers).length > 0) env.headers = headers;
1182
+ return env;
1183
+ }
1184
+ function parseStoredCliConfig(raw) {
1185
+ if (!isRecord(raw)) throw cliConfigError("Primitive CLI config must be a JSON object.");
1186
+ if (raw.version !== CONFIG_VERSION) throw cliConfigError(`Primitive CLI config version must be ${CONFIG_VERSION}.`);
1187
+ const currentRaw = raw.current_environment;
1188
+ const current_environment = currentRaw === null || currentRaw === void 0 ? null : typeof currentRaw === "string" ? normalizeCliEnvironmentName(currentRaw) : (() => {
1189
+ throw cliConfigError("Primitive CLI config current_environment must be a string or null.");
1190
+ })();
1191
+ if (!isRecord(raw.environments)) throw cliConfigError("Primitive CLI config environments must be an object.");
1192
+ const environments = {};
1193
+ for (const [rawName, rawEnv] of Object.entries(raw.environments)) {
1194
+ const name = normalizeCliEnvironmentName(rawName);
1195
+ environments[name] = parseEnvironmentConfig(rawEnv, `Primitive CLI config environment ${name}`);
1196
+ }
1197
+ if (current_environment && !environments[current_environment]) throw cliConfigError(`Primitive CLI config current environment ${current_environment} does not exist.`);
1198
+ return {
1199
+ version: CONFIG_VERSION,
1200
+ current_environment,
1201
+ environments
1202
+ };
1203
+ }
1204
+ function emptyCliConfig() {
1205
+ return {
1206
+ version: CONFIG_VERSION,
1207
+ current_environment: null,
1208
+ environments: {}
1209
+ };
1210
+ }
1211
+ function loadCliConfig(configDir) {
1212
+ const path = cliConfigPath(configDir);
1213
+ let contents;
1214
+ try {
1215
+ contents = readFileSync(path, "utf8");
1216
+ } catch (error) {
1217
+ if (error && typeof error === "object" && error.code === "ENOENT") return null;
1218
+ throw cliConfigError(`Could not read Primitive CLI config: ${error instanceof Error ? error.message : String(error)}.`);
1219
+ }
1220
+ try {
1221
+ return parseStoredCliConfig(JSON.parse(contents));
1222
+ } catch (error) {
1223
+ if (error instanceof SyntaxError) throw cliConfigError("Primitive CLI config is not valid JSON.");
1224
+ throw error;
1225
+ }
1226
+ }
1227
+ function saveCliConfig(configDir, config) {
1228
+ mkdirSync(configDir, {
1229
+ mode: 448,
1230
+ recursive: true
1231
+ });
1232
+ const path = cliConfigPath(configDir);
1233
+ const tempPath = join(configDir, `${CONFIG_FILE}.${process.pid}.${randomUUID()}.tmp`);
1234
+ try {
1235
+ writeFileSync(tempPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
1236
+ renameSync(tempPath, path);
1237
+ } catch (error) {
1238
+ rmSync(tempPath, { force: true });
1239
+ throw error;
1240
+ }
1241
+ }
1242
+ function deleteCliConfig(configDir) {
1243
+ rmSync(cliConfigPath(configDir), { force: true });
1244
+ }
1245
+ function resolveConfigEnvironment(config) {
1246
+ if (!config) return null;
1247
+ const current = config.current_environment;
1248
+ if (current) {
1249
+ const environment = config.environments[current];
1250
+ return environment ? {
1251
+ name: current,
1252
+ config: environment
1253
+ } : null;
1254
+ }
1255
+ const defaultEnvironment = config.environments[DEFAULT_ENVIRONMENT];
1256
+ return defaultEnvironment ? {
1257
+ name: DEFAULT_ENVIRONMENT,
1258
+ config: defaultEnvironment
1259
+ } : null;
1260
+ }
1261
+ function upsertCliEnvironment(params) {
1262
+ const name = normalizeCliEnvironmentName(params.environmentName ?? "default");
1263
+ const existing = params.config.environments[name] ?? {};
1264
+ const nextHeaders = { ...existing.headers ?? {} };
1265
+ for (const assignment of params.headers ?? []) {
1266
+ const [headerName, value] = parseHeaderAssignment(assignment);
1267
+ nextHeaders[headerName] = value;
1268
+ }
1269
+ for (const rawName of params.unsetHeaders ?? []) delete nextHeaders[validateCliHeaderName(rawName)];
1270
+ const nextEnvironment = {
1271
+ ...existing,
1272
+ ...params.apiBaseUrl1 !== void 0 ? { api_base_url_1: normalizeApiBaseUrl1(params.apiBaseUrl1) } : {},
1273
+ ...params.apiBaseUrl2 !== void 0 ? { api_base_url_2: normalizeApiBaseUrl2(params.apiBaseUrl2) } : {},
1274
+ ...Object.keys(nextHeaders).length > 0 ? { headers: nextHeaders } : {}
1275
+ };
1276
+ if (Object.keys(nextHeaders).length === 0) delete nextEnvironment.headers;
1277
+ return {
1278
+ ...params.config,
1279
+ current_environment: params.use === false ? params.config.current_environment : name,
1280
+ environments: {
1281
+ ...params.config.environments,
1282
+ [name]: nextEnvironment
1283
+ }
1284
+ };
1285
+ }
1286
+ function removeCliEnvironment(config, environmentName) {
1287
+ const name = normalizeCliEnvironmentName(environmentName);
1288
+ const environments = { ...config.environments };
1289
+ delete environments[name];
1290
+ return {
1291
+ ...config,
1292
+ current_environment: config.current_environment === name ? null : config.current_environment,
1293
+ environments
1294
+ };
1295
+ }
1296
+ function redactCliEnvironment(environment) {
1297
+ const headers = environment.headers && Object.keys(environment.headers).length > 0 ? Object.fromEntries(Object.keys(environment.headers).map((name) => [name, "***"])) : void 0;
1298
+ return {
1299
+ ...environment,
1300
+ ...headers ? { headers } : {}
1301
+ };
1302
+ }
1303
+ //#endregion
1304
+ export { createClient as A, saveCliCredentials as C, loadChatConversationByLocalId as D, loadActiveChatState as E, saveActiveChatState as O, resolveCliAuth as S, deleteChatState as T, deleteCliCredentials as _, normalizeCliEnvironmentName as a, normalizeApiBaseUrl1 as b, resolveConfigEnvironment as c, validateCliHeaderName as d, validateCliHeaderValue as f, credentialsPath as g, credentialsLockPath as h, loadCliConfig as i, createConfig as j, PrimitiveApiClient as k, saveCliConfig as l, cliAccessTokenExpiresAt as m, deleteCliConfig as n, redactCliEnvironment as o, acquireCliCredentialsLock as p, emptyCliConfig as r, removeCliEnvironment as s, DEFAULT_ENVIRONMENT as t, upsertCliEnvironment as u, deleteCliCredentialsLock as v, chatStatePath as w, normalizeApiBaseUrl2 as x, loadCliCredentials as y };