@primitivedotdev/cli 0.33.0 → 0.34.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.
@@ -1,3 +1,4 @@
1
+ import { A as createClient, C as saveCliCredentials, D as loadChatConversationByLocalId, E as loadActiveChatState, O as saveActiveChatState, S as resolveCliAuth, T as deleteChatState, _ as deleteCliCredentials, a as normalizeCliEnvironmentName, b as normalizeApiBaseUrl1, c as resolveConfigEnvironment, d as validateCliHeaderName, f as validateCliHeaderValue, g as credentialsPath, h as credentialsLockPath, i as loadCliConfig, j as createConfig, k as PrimitiveApiClient, l as saveCliConfig, m as cliAccessTokenExpiresAt, n as deleteCliConfig, o as redactCliEnvironment, p as acquireCliCredentialsLock, r as emptyCliConfig, s as removeCliEnvironment, u as upsertCliEnvironment, v as deleteCliCredentialsLock, w as chatStatePath, x as normalizeApiBaseUrl2, y as loadCliCredentials } from "../cli-config-D9nB6fOW.js";
1
2
  import { Args, Command, Errors, Flags } from "@oclif/core";
2
3
  import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
4
  import { randomUUID } from "node:crypto";
@@ -18,614 +19,6 @@ var __exportAll = (all, no_symbols) => {
18
19
  return target;
19
20
  };
20
21
  //#endregion
21
- //#region ../packages/api-core/src/api/core/bodySerializer.gen.ts
22
- const jsonBodySerializer = { bodySerializer: (body) => JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value) };
23
- //#endregion
24
- //#region ../packages/api-core/src/api/core/serverSentEvents.gen.ts
25
- function createSseClient({ onRequest, onSseError, onSseEvent, responseTransformer, responseValidator, sseDefaultRetryDelay, sseMaxRetryAttempts, sseMaxRetryDelay, sseSleepFn, url, ...options }) {
26
- let lastEventId;
27
- const sleep = sseSleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
28
- const createStream = async function* () {
29
- let retryDelay = sseDefaultRetryDelay ?? 3e3;
30
- let attempt = 0;
31
- const signal = options.signal ?? new AbortController().signal;
32
- while (true) {
33
- if (signal.aborted) break;
34
- attempt++;
35
- const headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers);
36
- if (lastEventId !== void 0) headers.set("Last-Event-ID", lastEventId);
37
- try {
38
- const requestInit = {
39
- redirect: "follow",
40
- ...options,
41
- body: options.serializedBody,
42
- headers,
43
- signal
44
- };
45
- let request = new Request(url, requestInit);
46
- if (onRequest) request = await onRequest(url, requestInit);
47
- const response = await (options.fetch ?? globalThis.fetch)(request);
48
- if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`);
49
- if (!response.body) throw new Error("No body in SSE response");
50
- const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
51
- let buffer = "";
52
- const abortHandler = () => {
53
- try {
54
- reader.cancel();
55
- } catch {}
56
- };
57
- signal.addEventListener("abort", abortHandler);
58
- try {
59
- while (true) {
60
- const { done, value } = await reader.read();
61
- if (done) break;
62
- buffer += value;
63
- buffer = buffer.replace(/\r\n?/g, "\n");
64
- const chunks = buffer.split("\n\n");
65
- buffer = chunks.pop() ?? "";
66
- for (const chunk of chunks) {
67
- const lines = chunk.split("\n");
68
- const dataLines = [];
69
- let eventName;
70
- for (const line of lines) if (line.startsWith("data:")) dataLines.push(line.replace(/^data:\s*/, ""));
71
- else if (line.startsWith("event:")) eventName = line.replace(/^event:\s*/, "");
72
- else if (line.startsWith("id:")) lastEventId = line.replace(/^id:\s*/, "");
73
- else if (line.startsWith("retry:")) {
74
- const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10);
75
- if (!Number.isNaN(parsed)) retryDelay = parsed;
76
- }
77
- let data;
78
- let parsedJson = false;
79
- if (dataLines.length) {
80
- const rawData = dataLines.join("\n");
81
- try {
82
- data = JSON.parse(rawData);
83
- parsedJson = true;
84
- } catch {
85
- data = rawData;
86
- }
87
- }
88
- if (parsedJson) {
89
- if (responseValidator) await responseValidator(data);
90
- if (responseTransformer) data = await responseTransformer(data);
91
- }
92
- onSseEvent?.({
93
- data,
94
- event: eventName,
95
- id: lastEventId,
96
- retry: retryDelay
97
- });
98
- if (dataLines.length) yield data;
99
- }
100
- }
101
- } finally {
102
- signal.removeEventListener("abort", abortHandler);
103
- reader.releaseLock();
104
- }
105
- break;
106
- } catch (error) {
107
- onSseError?.(error);
108
- if (sseMaxRetryAttempts !== void 0 && attempt >= sseMaxRetryAttempts) break;
109
- await sleep(Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 3e4));
110
- }
111
- }
112
- };
113
- return { stream: createStream() };
114
- }
115
- //#endregion
116
- //#region ../packages/api-core/src/api/core/pathSerializer.gen.ts
117
- const separatorArrayExplode = (style) => {
118
- switch (style) {
119
- case "label": return ".";
120
- case "matrix": return ";";
121
- case "simple": return ",";
122
- default: return "&";
123
- }
124
- };
125
- const separatorArrayNoExplode = (style) => {
126
- switch (style) {
127
- case "form": return ",";
128
- case "pipeDelimited": return "|";
129
- case "spaceDelimited": return "%20";
130
- default: return ",";
131
- }
132
- };
133
- const separatorObjectExplode = (style) => {
134
- switch (style) {
135
- case "label": return ".";
136
- case "matrix": return ";";
137
- case "simple": return ",";
138
- default: return "&";
139
- }
140
- };
141
- const serializeArrayParam = ({ allowReserved, explode, name, style, value }) => {
142
- if (!explode) {
143
- const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v))).join(separatorArrayNoExplode(style));
144
- switch (style) {
145
- case "label": return `.${joinedValues}`;
146
- case "matrix": return `;${name}=${joinedValues}`;
147
- case "simple": return joinedValues;
148
- default: return `${name}=${joinedValues}`;
149
- }
150
- }
151
- const separator = separatorArrayExplode(style);
152
- const joinedValues = value.map((v) => {
153
- if (style === "label" || style === "simple") return allowReserved ? v : encodeURIComponent(v);
154
- return serializePrimitiveParam({
155
- allowReserved,
156
- name,
157
- value: v
158
- });
159
- }).join(separator);
160
- return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
161
- };
162
- const serializePrimitiveParam = ({ allowReserved, name, value }) => {
163
- if (value === void 0 || value === null) return "";
164
- if (typeof value === "object") throw new Error("Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.");
165
- return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
166
- };
167
- const serializeObjectParam = ({ allowReserved, explode, name, style, value, valueOnly }) => {
168
- if (value instanceof Date) return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
169
- if (style !== "deepObject" && !explode) {
170
- let values = [];
171
- Object.entries(value).forEach(([key, v]) => {
172
- values = [
173
- ...values,
174
- key,
175
- allowReserved ? v : encodeURIComponent(v)
176
- ];
177
- });
178
- const joinedValues = values.join(",");
179
- switch (style) {
180
- case "form": return `${name}=${joinedValues}`;
181
- case "label": return `.${joinedValues}`;
182
- case "matrix": return `;${name}=${joinedValues}`;
183
- default: return joinedValues;
184
- }
185
- }
186
- const separator = separatorObjectExplode(style);
187
- const joinedValues = Object.entries(value).map(([key, v]) => serializePrimitiveParam({
188
- allowReserved,
189
- name: style === "deepObject" ? `${name}[${key}]` : key,
190
- value: v
191
- })).join(separator);
192
- return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
193
- };
194
- //#endregion
195
- //#region ../packages/api-core/src/api/core/utils.gen.ts
196
- const PATH_PARAM_RE = /\{[^{}]+\}/g;
197
- const defaultPathSerializer = ({ path, url: _url }) => {
198
- let url = _url;
199
- const matches = _url.match(PATH_PARAM_RE);
200
- if (matches) for (const match of matches) {
201
- let explode = false;
202
- let name = match.substring(1, match.length - 1);
203
- let style = "simple";
204
- if (name.endsWith("*")) {
205
- explode = true;
206
- name = name.substring(0, name.length - 1);
207
- }
208
- if (name.startsWith(".")) {
209
- name = name.substring(1);
210
- style = "label";
211
- } else if (name.startsWith(";")) {
212
- name = name.substring(1);
213
- style = "matrix";
214
- }
215
- const value = path[name];
216
- if (value === void 0 || value === null) continue;
217
- if (Array.isArray(value)) {
218
- url = url.replace(match, serializeArrayParam({
219
- explode,
220
- name,
221
- style,
222
- value
223
- }));
224
- continue;
225
- }
226
- if (typeof value === "object") {
227
- url = url.replace(match, serializeObjectParam({
228
- explode,
229
- name,
230
- style,
231
- value,
232
- valueOnly: true
233
- }));
234
- continue;
235
- }
236
- if (style === "matrix") {
237
- url = url.replace(match, `;${serializePrimitiveParam({
238
- name,
239
- value
240
- })}`);
241
- continue;
242
- }
243
- const replaceValue = encodeURIComponent(style === "label" ? `.${value}` : value);
244
- url = url.replace(match, replaceValue);
245
- }
246
- return url;
247
- };
248
- const getUrl = ({ baseUrl, path, query, querySerializer, url: _url }) => {
249
- const pathUrl = _url.startsWith("/") ? _url : `/${_url}`;
250
- let url = (baseUrl ?? "") + pathUrl;
251
- if (path) url = defaultPathSerializer({
252
- path,
253
- url
254
- });
255
- let search = query ? querySerializer(query) : "";
256
- if (search.startsWith("?")) search = search.substring(1);
257
- if (search) url += `?${search}`;
258
- return url;
259
- };
260
- function getValidRequestBody(options) {
261
- const hasBody = options.body !== void 0;
262
- if (hasBody && options.bodySerializer) {
263
- if ("serializedBody" in options) return options.serializedBody !== void 0 && options.serializedBody !== "" ? options.serializedBody : null;
264
- return options.body !== "" ? options.body : null;
265
- }
266
- if (hasBody) return options.body;
267
- }
268
- //#endregion
269
- //#region ../packages/api-core/src/api/core/auth.gen.ts
270
- const getAuthToken = async (auth, callback) => {
271
- const token = typeof callback === "function" ? await callback(auth) : callback;
272
- if (!token) return;
273
- if (auth.scheme === "bearer") return `Bearer ${token}`;
274
- if (auth.scheme === "basic") return `Basic ${btoa(token)}`;
275
- return token;
276
- };
277
- //#endregion
278
- //#region ../packages/api-core/src/api/client/utils.gen.ts
279
- const createQuerySerializer = ({ parameters = {}, ...args } = {}) => {
280
- const querySerializer = (queryParams) => {
281
- const search = [];
282
- if (queryParams && typeof queryParams === "object") for (const name in queryParams) {
283
- const value = queryParams[name];
284
- if (value === void 0 || value === null) continue;
285
- const options = parameters[name] || args;
286
- if (Array.isArray(value)) {
287
- const serializedArray = serializeArrayParam({
288
- allowReserved: options.allowReserved,
289
- explode: true,
290
- name,
291
- style: "form",
292
- value,
293
- ...options.array
294
- });
295
- if (serializedArray) search.push(serializedArray);
296
- } else if (typeof value === "object") {
297
- const serializedObject = serializeObjectParam({
298
- allowReserved: options.allowReserved,
299
- explode: true,
300
- name,
301
- style: "deepObject",
302
- value,
303
- ...options.object
304
- });
305
- if (serializedObject) search.push(serializedObject);
306
- } else {
307
- const serializedPrimitive = serializePrimitiveParam({
308
- allowReserved: options.allowReserved,
309
- name,
310
- value
311
- });
312
- if (serializedPrimitive) search.push(serializedPrimitive);
313
- }
314
- }
315
- return search.join("&");
316
- };
317
- return querySerializer;
318
- };
319
- /**
320
- * Infers parseAs value from provided Content-Type header.
321
- */
322
- const getParseAs = (contentType) => {
323
- if (!contentType) return "stream";
324
- const cleanContent = contentType.split(";")[0]?.trim();
325
- if (!cleanContent) return;
326
- if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) return "json";
327
- if (cleanContent === "multipart/form-data") return "formData";
328
- if ([
329
- "application/",
330
- "audio/",
331
- "image/",
332
- "video/"
333
- ].some((type) => cleanContent.startsWith(type))) return "blob";
334
- if (cleanContent.startsWith("text/")) return "text";
335
- };
336
- const checkForExistence = (options, name) => {
337
- if (!name) return false;
338
- if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) return true;
339
- return false;
340
- };
341
- const setAuthParams = async ({ security, ...options }) => {
342
- for (const auth of security) {
343
- if (checkForExistence(options, auth.name)) continue;
344
- const token = await getAuthToken(auth, options.auth);
345
- if (!token) continue;
346
- const name = auth.name ?? "Authorization";
347
- switch (auth.in) {
348
- case "query":
349
- if (!options.query) options.query = {};
350
- options.query[name] = token;
351
- break;
352
- case "cookie":
353
- options.headers.append("Cookie", `${name}=${token}`);
354
- break;
355
- default:
356
- options.headers.set(name, token);
357
- break;
358
- }
359
- }
360
- };
361
- const buildUrl = (options) => getUrl({
362
- baseUrl: options.baseUrl,
363
- path: options.path,
364
- query: options.query,
365
- querySerializer: typeof options.querySerializer === "function" ? options.querySerializer : createQuerySerializer(options.querySerializer),
366
- url: options.url
367
- });
368
- const mergeConfigs = (a, b) => {
369
- const config = {
370
- ...a,
371
- ...b
372
- };
373
- if (config.baseUrl?.endsWith("/")) config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
374
- config.headers = mergeHeaders$1(a.headers, b.headers);
375
- return config;
376
- };
377
- const headersEntries = (headers) => {
378
- const entries = [];
379
- headers.forEach((value, key) => {
380
- entries.push([key, value]);
381
- });
382
- return entries;
383
- };
384
- const mergeHeaders$1 = (...headers) => {
385
- const mergedHeaders = new Headers();
386
- for (const header of headers) {
387
- if (!header) continue;
388
- const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header);
389
- for (const [key, value] of iterator) if (value === null) mergedHeaders.delete(key);
390
- else if (Array.isArray(value)) for (const v of value) mergedHeaders.append(key, v);
391
- else if (value !== void 0) mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : value);
392
- }
393
- return mergedHeaders;
394
- };
395
- var Interceptors = class {
396
- fns = [];
397
- clear() {
398
- this.fns = [];
399
- }
400
- eject(id) {
401
- const index = this.getInterceptorIndex(id);
402
- if (this.fns[index]) this.fns[index] = null;
403
- }
404
- exists(id) {
405
- const index = this.getInterceptorIndex(id);
406
- return Boolean(this.fns[index]);
407
- }
408
- getInterceptorIndex(id) {
409
- if (typeof id === "number") return this.fns[id] ? id : -1;
410
- return this.fns.indexOf(id);
411
- }
412
- update(id, fn) {
413
- const index = this.getInterceptorIndex(id);
414
- if (this.fns[index]) {
415
- this.fns[index] = fn;
416
- return id;
417
- }
418
- return false;
419
- }
420
- use(fn) {
421
- this.fns.push(fn);
422
- return this.fns.length - 1;
423
- }
424
- };
425
- const createInterceptors = () => ({
426
- error: new Interceptors(),
427
- request: new Interceptors(),
428
- response: new Interceptors()
429
- });
430
- const defaultQuerySerializer = createQuerySerializer({
431
- allowReserved: false,
432
- array: {
433
- explode: true,
434
- style: "form"
435
- },
436
- object: {
437
- explode: true,
438
- style: "deepObject"
439
- }
440
- });
441
- const defaultHeaders = { "Content-Type": "application/json" };
442
- const createConfig = (override = {}) => ({
443
- ...jsonBodySerializer,
444
- headers: defaultHeaders,
445
- parseAs: "auto",
446
- querySerializer: defaultQuerySerializer,
447
- ...override
448
- });
449
- //#endregion
450
- //#region ../packages/api-core/src/api/client/client.gen.ts
451
- const createClient = (config = {}) => {
452
- let _config = mergeConfigs(createConfig(), config);
453
- const getConfig = () => ({ ..._config });
454
- const setConfig = (config) => {
455
- _config = mergeConfigs(_config, config);
456
- return getConfig();
457
- };
458
- const interceptors = createInterceptors();
459
- const beforeRequest = async (options) => {
460
- const opts = {
461
- ..._config,
462
- ...options,
463
- fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
464
- headers: mergeHeaders$1(_config.headers, options.headers),
465
- serializedBody: void 0
466
- };
467
- if (opts.security) await setAuthParams({
468
- ...opts,
469
- security: opts.security
470
- });
471
- if (opts.requestValidator) await opts.requestValidator(opts);
472
- if (opts.body !== void 0 && opts.bodySerializer) opts.serializedBody = opts.bodySerializer(opts.body);
473
- if (opts.body === void 0 || opts.serializedBody === "") opts.headers.delete("Content-Type");
474
- const resolvedOpts = opts;
475
- return {
476
- opts: resolvedOpts,
477
- url: buildUrl(resolvedOpts)
478
- };
479
- };
480
- const request = async (options) => {
481
- const { opts, url } = await beforeRequest(options);
482
- const requestInit = {
483
- redirect: "follow",
484
- ...opts,
485
- body: getValidRequestBody(opts)
486
- };
487
- let request = new Request(url, requestInit);
488
- for (const fn of interceptors.request.fns) if (fn) request = await fn(request, opts);
489
- const _fetch = opts.fetch;
490
- let response;
491
- try {
492
- response = await _fetch(request);
493
- } catch (error) {
494
- let finalError = error;
495
- for (const fn of interceptors.error.fns) if (fn) finalError = await fn(error, void 0, request, opts);
496
- finalError = finalError || {};
497
- if (opts.throwOnError) throw finalError;
498
- return opts.responseStyle === "data" ? void 0 : {
499
- error: finalError,
500
- request,
501
- response: void 0
502
- };
503
- }
504
- for (const fn of interceptors.response.fns) if (fn) response = await fn(response, request, opts);
505
- const result = {
506
- request,
507
- response
508
- };
509
- if (response.ok) {
510
- const parseAs = (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json";
511
- if (response.status === 204 || response.headers.get("Content-Length") === "0") {
512
- let emptyData;
513
- switch (parseAs) {
514
- case "arrayBuffer":
515
- case "blob":
516
- case "text":
517
- emptyData = await response[parseAs]();
518
- break;
519
- case "formData":
520
- emptyData = new FormData();
521
- break;
522
- case "stream":
523
- emptyData = response.body;
524
- break;
525
- default:
526
- emptyData = {};
527
- break;
528
- }
529
- return opts.responseStyle === "data" ? emptyData : {
530
- data: emptyData,
531
- ...result
532
- };
533
- }
534
- let data;
535
- switch (parseAs) {
536
- case "arrayBuffer":
537
- case "blob":
538
- case "formData":
539
- case "text":
540
- data = await response[parseAs]();
541
- break;
542
- case "json": {
543
- const text = await response.text();
544
- data = text ? JSON.parse(text) : {};
545
- break;
546
- }
547
- case "stream": return opts.responseStyle === "data" ? response.body : {
548
- data: response.body,
549
- ...result
550
- };
551
- }
552
- if (parseAs === "json") {
553
- if (opts.responseValidator) await opts.responseValidator(data);
554
- if (opts.responseTransformer) data = await opts.responseTransformer(data);
555
- }
556
- return opts.responseStyle === "data" ? data : {
557
- data,
558
- ...result
559
- };
560
- }
561
- const textError = await response.text();
562
- let jsonError;
563
- try {
564
- jsonError = JSON.parse(textError);
565
- } catch {}
566
- const error = jsonError ?? textError;
567
- let finalError = error;
568
- for (const fn of interceptors.error.fns) if (fn) finalError = await fn(error, response, request, opts);
569
- finalError = finalError || {};
570
- if (opts.throwOnError) throw finalError;
571
- return opts.responseStyle === "data" ? void 0 : {
572
- error: finalError,
573
- ...result
574
- };
575
- };
576
- const makeMethodFn = (method) => (options) => request({
577
- ...options,
578
- method
579
- });
580
- const makeSseFn = (method) => async (options) => {
581
- const { opts, url } = await beforeRequest(options);
582
- return createSseClient({
583
- ...opts,
584
- body: opts.body,
585
- headers: opts.headers,
586
- method,
587
- onRequest: async (url, init) => {
588
- let request = new Request(url, init);
589
- for (const fn of interceptors.request.fns) if (fn) request = await fn(request, opts);
590
- return request;
591
- },
592
- serializedBody: getValidRequestBody(opts),
593
- url
594
- });
595
- };
596
- const _buildUrl = (options) => buildUrl({
597
- ..._config,
598
- ...options
599
- });
600
- return {
601
- buildUrl: _buildUrl,
602
- connect: makeMethodFn("CONNECT"),
603
- delete: makeMethodFn("DELETE"),
604
- get: makeMethodFn("GET"),
605
- getConfig,
606
- head: makeMethodFn("HEAD"),
607
- interceptors,
608
- options: makeMethodFn("OPTIONS"),
609
- patch: makeMethodFn("PATCH"),
610
- post: makeMethodFn("POST"),
611
- put: makeMethodFn("PUT"),
612
- request,
613
- setConfig,
614
- sse: {
615
- connect: makeSseFn("CONNECT"),
616
- delete: makeSseFn("DELETE"),
617
- get: makeSseFn("GET"),
618
- head: makeSseFn("HEAD"),
619
- options: makeSseFn("OPTIONS"),
620
- patch: makeSseFn("PATCH"),
621
- post: makeSseFn("POST"),
622
- put: makeSseFn("PUT"),
623
- trace: makeSseFn("TRACE")
624
- },
625
- trace: makeMethodFn("TRACE")
626
- };
627
- };
628
- //#endregion
629
22
  //#region ../packages/api-core/src/api/client.gen.ts
630
23
  const client = createClient(createConfig({ baseUrl: "https://www.primitive.dev/api/v1" }));
631
24
  //#endregion
@@ -648,6 +41,7 @@ var sdk_gen_exports = /* @__PURE__ */ __exportAll({
648
41
  downloadDomainZoneFile: () => downloadDomainZoneFile,
649
42
  downloadRawEmail: () => downloadRawEmail,
650
43
  getAccount: () => getAccount,
44
+ getConversation: () => getConversation,
651
45
  getEmail: () => getEmail,
652
46
  getFunction: () => getFunction,
653
47
  getFunctionTestRunTrace: () => getFunctionTestRunTrace,
@@ -1267,6 +661,37 @@ const discardEmailContent = (options) => (options.client ?? client).post({
1267
661
  ...options
1268
662
  });
1269
663
  /**
664
+ * Get the conversation an email belongs to
665
+ *
666
+ * Returns the full conversation the given inbound email belongs
667
+ * to, as ordered, ready-to-prompt turns WITH bodies. It resolves
668
+ * the thread from the email and returns every message oldest-first,
669
+ * so an agent that received an email can pass `messages` straight
670
+ * to a chat model in one call instead of walking `/threads/{id}`
671
+ * plus `/emails/{id}` and `/sent-emails/{id}` per message.
672
+ *
673
+ * Each message carries a `direction` (`inbound` | `outbound`) and a
674
+ * derived `role`: `inbound` -> `user`, `outbound` -> `assistant`
675
+ * (your own prior replies). The role mapping assumes the caller
676
+ * owns the outbound side, which is the agent-reply case this exists
677
+ * for. If the email has no thread yet (a brand-new message), the
678
+ * conversation is just that one message as a single user turn.
679
+ *
680
+ * The message list is capped; check `truncated` to detect when
681
+ * older messages were omitted. Consecutive same-role turns are not
682
+ * merged here; that normalization is model-specific and left to the
683
+ * caller.
684
+ *
685
+ */
686
+ const getConversation = (options) => (options.client ?? client).get({
687
+ security: [{
688
+ scheme: "bearer",
689
+ type: "http"
690
+ }],
691
+ url: "/emails/{id}/conversation",
692
+ ...options
693
+ });
694
+ /**
1270
695
  * List webhook endpoints
1271
696
  *
1272
697
  * Returns all active (non-deleted) webhook endpoints.
@@ -3026,6 +2451,27 @@ const openapiDocument = {
3026
2451
  }
3027
2452
  }
3028
2453
  },
2454
+ "/emails/{id}/conversation": {
2455
+ "parameters": [{ "$ref": "#/components/parameters/ResourceId" }],
2456
+ "get": {
2457
+ "operationId": "getConversation",
2458
+ "summary": "Get the conversation an email belongs to",
2459
+ "description": "Returns the full conversation the given inbound email belongs\nto, as ordered, ready-to-prompt turns WITH bodies. It resolves\nthe thread from the email and returns every message oldest-first,\nso an agent that received an email can pass `messages` straight\nto a chat model in one call instead of walking `/threads/{id}`\nplus `/emails/{id}` and `/sent-emails/{id}` per message.\n\nEach message carries a `direction` (`inbound` | `outbound`) and a\nderived `role`: `inbound` -> `user`, `outbound` -> `assistant`\n(your own prior replies). The role mapping assumes the caller\nowns the outbound side, which is the agent-reply case this exists\nfor. If the email has no thread yet (a brand-new message), the\nconversation is just that one message as a single user turn.\n\nThe message list is capped; check `truncated` to detect when\nolder messages were omitted. Consecutive same-role turns are not\nmerged here; that normalization is model-specific and left to the\ncaller.\n",
2460
+ "tags": ["Emails"],
2461
+ "responses": {
2462
+ "200": {
2463
+ "description": "Conversation",
2464
+ "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
2465
+ "type": "object",
2466
+ "properties": { "data": { "$ref": "#/components/schemas/Conversation" } }
2467
+ }] } } }
2468
+ },
2469
+ "400": { "$ref": "#/components/responses/ValidationError" },
2470
+ "401": { "$ref": "#/components/responses/Unauthorized" },
2471
+ "404": { "$ref": "#/components/responses/NotFound" }
2472
+ }
2473
+ }
2474
+ },
3029
2475
  "/endpoints": {
3030
2476
  "get": {
3031
2477
  "operationId": "listEndpoints",
@@ -5864,6 +5310,78 @@ const openapiDocument = {
5864
5310
  },
5865
5311
  "required": ["direction", "id"]
5866
5312
  },
5313
+ "Conversation": {
5314
+ "type": "object",
5315
+ "description": "The full conversation an inbound email belongs to, as ordered,\nready-to-prompt turns with bodies. Resolves the thread from the\nemail and returns every message oldest-first, so an agent that\nreceived an email can pass `messages` straight to a chat model in\none call.\n",
5316
+ "properties": {
5317
+ "thread_id": {
5318
+ "type": ["string", "null"],
5319
+ "format": "uuid",
5320
+ "description": "The thread this email belongs to, or null when the email\nisn't threaded yet (the conversation is then just this one\nmessage).\n"
5321
+ },
5322
+ "subject": {
5323
+ "type": ["string", "null"],
5324
+ "description": "Normalized thread subject (Re/Fwd prefixes stripped), or the\nemail's own subject when it isn't threaded.\n"
5325
+ },
5326
+ "message_count": {
5327
+ "type": "integer",
5328
+ "description": "Total messages in the thread. `messages` is capped, so\n`truncated` is true (and this can exceed `messages.length`)\nwhen older messages were omitted.\n"
5329
+ },
5330
+ "truncated": {
5331
+ "type": "boolean",
5332
+ "description": "True when `messages` omits part of the conversation because\nthe thread exceeds the per-call cap.\n"
5333
+ },
5334
+ "messages": {
5335
+ "type": "array",
5336
+ "items": { "$ref": "#/components/schemas/ConversationMessage" }
5337
+ }
5338
+ },
5339
+ "required": [
5340
+ "thread_id",
5341
+ "message_count",
5342
+ "truncated",
5343
+ "messages"
5344
+ ]
5345
+ },
5346
+ "ConversationMessage": {
5347
+ "type": "object",
5348
+ "description": "One message in the conversation, with its body and a chat role.",
5349
+ "properties": {
5350
+ "role": {
5351
+ "type": "string",
5352
+ "enum": ["user", "assistant"],
5353
+ "description": "Chat role derived from `direction`: `user` for inbound\n(received) messages, `assistant` for outbound (your own prior\nreplies). Lets `messages` be passed directly to a chat model.\n"
5354
+ },
5355
+ "direction": {
5356
+ "type": "string",
5357
+ "enum": ["inbound", "outbound"],
5358
+ "description": "`inbound` for a received email (`/emails/{id}`), `outbound`\nfor a send (`/sent-emails/{id}`).\n"
5359
+ },
5360
+ "id": {
5361
+ "type": "string",
5362
+ "format": "uuid"
5363
+ },
5364
+ "message_id": { "type": ["string", "null"] },
5365
+ "from": { "type": ["string", "null"] },
5366
+ "to": { "type": ["string", "null"] },
5367
+ "subject": { "type": ["string", "null"] },
5368
+ "text": {
5369
+ "type": "string",
5370
+ "description": "Plain-text body. Empty string when the message has no text\npart or its content was discarded by retention.\n"
5371
+ },
5372
+ "timestamp": {
5373
+ "type": ["string", "null"],
5374
+ "format": "date-time",
5375
+ "description": "received_at for inbound, created_at for outbound."
5376
+ }
5377
+ },
5378
+ "required": [
5379
+ "role",
5380
+ "direction",
5381
+ "id",
5382
+ "text"
5383
+ ]
5384
+ },
5867
5385
  "SendMailAttachment": {
5868
5386
  "type": "object",
5869
5387
  "additionalProperties": false,
@@ -9023,12 +8541,12 @@ const operationManifest = [
9023
8541
  {
9024
8542
  "binaryResponse": false,
9025
8543
  "bodyRequired": false,
9026
- "command": "get-email",
9027
- "description": "Returns the full record for an inbound email received at one\nof your verified domains, including the parsed text and HTML\nbodies, threading metadata, SMTP envelope detail, webhook\ndelivery state, and a `replies` array for any outbound sends\nrecorded as replies to this inbound.\n\nFor listing inbound emails (with cursor pagination, status\nand date filters, and free-text search), use\n`/emails`. Outbound (sent) email records are NOT returned\nhere; use `/sent-emails/{id}` for those.\n\nThe response carries four sender-shaped fields whose\nmeanings overlap. `from_email` is the canonical \"who sent\nthis\" field for most use cases (parsed bare address from\nthe `From:` header, with a `sender` fallback). `from_header`\nis the raw header including any display name. `sender` and\n`smtp_mail_from` both carry the SMTP envelope MAIL FROM\n(return-path) and are equal by construction; `sender` is\nthe older field name retained for compatibility. See\n`primitive describe emails:get-email | jq '.responseSchema.properties'`\nfor per-field detail.\n",
8544
+ "command": "get-conversation",
8545
+ "description": "Returns the full conversation the given inbound email belongs\nto, as ordered, ready-to-prompt turns WITH bodies. It resolves\nthe thread from the email and returns every message oldest-first,\nso an agent that received an email can pass `messages` straight\nto a chat model in one call instead of walking `/threads/{id}`\nplus `/emails/{id}` and `/sent-emails/{id}` per message.\n\nEach message carries a `direction` (`inbound` | `outbound`) and a\nderived `role`: `inbound` -> `user`, `outbound` -> `assistant`\n(your own prior replies). The role mapping assumes the caller\nowns the outbound side, which is the agent-reply case this exists\nfor. If the email has no thread yet (a brand-new message), the\nconversation is just that one message as a single user turn.\n\nThe message list is capped; check `truncated` to detect when\nolder messages were omitted. Consecutive same-role turns are not\nmerged here; that normalization is model-specific and left to the\ncaller.\n",
9028
8546
  "hasJsonBody": false,
9029
8547
  "method": "GET",
9030
- "operationId": "getEmail",
9031
- "path": "/emails/{id}",
8548
+ "operationId": "getConversation",
8549
+ "path": "/emails/{id}/conversation",
9032
8550
  "pathParams": [{
9033
8551
  "description": "Resource UUID",
9034
8552
  "enum": null,
@@ -9040,45 +8558,139 @@ const operationManifest = [
9040
8558
  "requestSchema": null,
9041
8559
  "responseSchema": {
9042
8560
  "type": "object",
8561
+ "description": "The full conversation an inbound email belongs to, as ordered,\nready-to-prompt turns with bodies. Resolves the thread from the\nemail and returns every message oldest-first, so an agent that\nreceived an email can pass `messages` straight to a chat model in\none call.\n",
9043
8562
  "properties": {
9044
- "id": {
9045
- "type": "string",
9046
- "format": "uuid"
9047
- },
9048
- "message_id": { "type": ["string", "null"] },
9049
- "domain_id": {
9050
- "type": ["string", "null"],
9051
- "format": "uuid"
9052
- },
9053
- "org_id": {
8563
+ "thread_id": {
9054
8564
  "type": ["string", "null"],
9055
- "format": "uuid"
9056
- },
9057
- "sender": {
9058
- "type": "string",
9059
- "description": "SMTP envelope sender (return-path) the inbound mail server\naccepted. Same value as `smtp_mail_from`; both fields exist\nso protocol-aware tooling can use whichever name it expects.\n\nFor most legitimate mail this equals `from_email`; for\nmailing lists, bounce handlers, and forwarders it is\ntypically the bounce-handling address rather than the\nhuman-visible sender.\n\n**For the canonical \"who sent this email\" value, use\n`from_email`.**\n"
8565
+ "format": "uuid",
8566
+ "description": "The thread this email belongs to, or null when the email\nisn't threaded yet (the conversation is then just this one\nmessage).\n"
9060
8567
  },
9061
- "recipient": { "type": "string" },
9062
- "subject": { "type": ["string", "null"] },
9063
- "body_text": {
8568
+ "subject": {
9064
8569
  "type": ["string", "null"],
9065
- "description": "Plain-text body parsed from the inbound MIME, matching the `email.parsed.body_text` field on the webhook payload. Null when the message had no text part or parsing failed."
8570
+ "description": "Normalized thread subject (Re/Fwd prefixes stripped), or the\nemail's own subject when it isn't threaded.\n"
9066
8571
  },
9067
- "body_html": {
9068
- "type": ["string", "null"],
9069
- "description": "HTML body parsed from the inbound MIME, matching the `email.parsed.body_html` field on the webhook payload. Null when the message had no HTML part or parsing failed."
8572
+ "message_count": {
8573
+ "type": "integer",
8574
+ "description": "Total messages in the thread. `messages` is capped, so\n`truncated` is true (and this can exceed `messages.length`)\nwhen older messages were omitted.\n"
9070
8575
  },
9071
- "status": {
9072
- "type": "string",
9073
- "description": "Lifecycle status of an INBOUND email (a row in the `emails`\ntable). Distinct from `SentEmailStatus`, which describes\nthe OUTBOUND lifecycle (the `sent_emails` table) and uses\na different vocabulary because the lifecycles differ.\nPossible values:\n\n - `pending`: the row was inserted at ingestion (mx_main)\n and has not yet completed the spam / filter / auth\n pipeline. Body and parsed fields are present; webhook\n delivery is not yet scheduled. Most rows transition out\n of `pending` within seconds.\n - `accepted`: the inbound passed the policy gates and is\n queued for webhook delivery. The `webhook_status` field\n tracks the separate webhook-delivery lifecycle from\n this point.\n - `completed`: terminal success. Webhook delivery\n attempted and acknowledged by every active endpoint, OR\n no endpoints are configured, so the row is durably\n archived.\n - `rejected`: terminal failure at ingestion (spam, blocked\n sender, filter rule, malformed). The body and metadata\n are stored for auditing but no webhook fires and the\n row is not repliable.\n\nSee also `webhook_status` (separate enum tracking the\nwebhook-delivery state machine) and `SentEmailStatus` (the\noutbound vocabulary).\n",
9074
- "enum": [
9075
- "pending",
9076
- "accepted",
9077
- "completed",
9078
- "rejected"
9079
- ]
8576
+ "truncated": {
8577
+ "type": "boolean",
8578
+ "description": "True when `messages` omits part of the conversation because\nthe thread exceeds the per-call cap.\n"
9080
8579
  },
9081
- "domain": { "type": "string" },
8580
+ "messages": {
8581
+ "type": "array",
8582
+ "items": {
8583
+ "type": "object",
8584
+ "description": "One message in the conversation, with its body and a chat role.",
8585
+ "properties": {
8586
+ "role": {
8587
+ "type": "string",
8588
+ "enum": ["user", "assistant"],
8589
+ "description": "Chat role derived from `direction`: `user` for inbound\n(received) messages, `assistant` for outbound (your own prior\nreplies). Lets `messages` be passed directly to a chat model.\n"
8590
+ },
8591
+ "direction": {
8592
+ "type": "string",
8593
+ "enum": ["inbound", "outbound"],
8594
+ "description": "`inbound` for a received email (`/emails/{id}`), `outbound`\nfor a send (`/sent-emails/{id}`).\n"
8595
+ },
8596
+ "id": {
8597
+ "type": "string",
8598
+ "format": "uuid"
8599
+ },
8600
+ "message_id": { "type": ["string", "null"] },
8601
+ "from": { "type": ["string", "null"] },
8602
+ "to": { "type": ["string", "null"] },
8603
+ "subject": { "type": ["string", "null"] },
8604
+ "text": {
8605
+ "type": "string",
8606
+ "description": "Plain-text body. Empty string when the message has no text\npart or its content was discarded by retention.\n"
8607
+ },
8608
+ "timestamp": {
8609
+ "type": ["string", "null"],
8610
+ "format": "date-time",
8611
+ "description": "received_at for inbound, created_at for outbound."
8612
+ }
8613
+ },
8614
+ "required": [
8615
+ "role",
8616
+ "direction",
8617
+ "id",
8618
+ "text"
8619
+ ]
8620
+ }
8621
+ }
8622
+ },
8623
+ "required": [
8624
+ "thread_id",
8625
+ "message_count",
8626
+ "truncated",
8627
+ "messages"
8628
+ ]
8629
+ },
8630
+ "sdkName": "getConversation",
8631
+ "summary": "Get the conversation an email belongs to",
8632
+ "tag": "Emails",
8633
+ "tagCommand": "emails"
8634
+ },
8635
+ {
8636
+ "binaryResponse": false,
8637
+ "bodyRequired": false,
8638
+ "command": "get-email",
8639
+ "description": "Returns the full record for an inbound email received at one\nof your verified domains, including the parsed text and HTML\nbodies, threading metadata, SMTP envelope detail, webhook\ndelivery state, and a `replies` array for any outbound sends\nrecorded as replies to this inbound.\n\nFor listing inbound emails (with cursor pagination, status\nand date filters, and free-text search), use\n`/emails`. Outbound (sent) email records are NOT returned\nhere; use `/sent-emails/{id}` for those.\n\nThe response carries four sender-shaped fields whose\nmeanings overlap. `from_email` is the canonical \"who sent\nthis\" field for most use cases (parsed bare address from\nthe `From:` header, with a `sender` fallback). `from_header`\nis the raw header including any display name. `sender` and\n`smtp_mail_from` both carry the SMTP envelope MAIL FROM\n(return-path) and are equal by construction; `sender` is\nthe older field name retained for compatibility. See\n`primitive describe emails:get-email | jq '.responseSchema.properties'`\nfor per-field detail.\n",
8640
+ "hasJsonBody": false,
8641
+ "method": "GET",
8642
+ "operationId": "getEmail",
8643
+ "path": "/emails/{id}",
8644
+ "pathParams": [{
8645
+ "description": "Resource UUID",
8646
+ "enum": null,
8647
+ "name": "id",
8648
+ "required": true,
8649
+ "type": "string"
8650
+ }],
8651
+ "queryParams": [],
8652
+ "requestSchema": null,
8653
+ "responseSchema": {
8654
+ "type": "object",
8655
+ "properties": {
8656
+ "id": {
8657
+ "type": "string",
8658
+ "format": "uuid"
8659
+ },
8660
+ "message_id": { "type": ["string", "null"] },
8661
+ "domain_id": {
8662
+ "type": ["string", "null"],
8663
+ "format": "uuid"
8664
+ },
8665
+ "org_id": {
8666
+ "type": ["string", "null"],
8667
+ "format": "uuid"
8668
+ },
8669
+ "sender": {
8670
+ "type": "string",
8671
+ "description": "SMTP envelope sender (return-path) the inbound mail server\naccepted. Same value as `smtp_mail_from`; both fields exist\nso protocol-aware tooling can use whichever name it expects.\n\nFor most legitimate mail this equals `from_email`; for\nmailing lists, bounce handlers, and forwarders it is\ntypically the bounce-handling address rather than the\nhuman-visible sender.\n\n**For the canonical \"who sent this email\" value, use\n`from_email`.**\n"
8672
+ },
8673
+ "recipient": { "type": "string" },
8674
+ "subject": { "type": ["string", "null"] },
8675
+ "body_text": {
8676
+ "type": ["string", "null"],
8677
+ "description": "Plain-text body parsed from the inbound MIME, matching the `email.parsed.body_text` field on the webhook payload. Null when the message had no text part or parsing failed."
8678
+ },
8679
+ "body_html": {
8680
+ "type": ["string", "null"],
8681
+ "description": "HTML body parsed from the inbound MIME, matching the `email.parsed.body_html` field on the webhook payload. Null when the message had no HTML part or parsing failed."
8682
+ },
8683
+ "status": {
8684
+ "type": "string",
8685
+ "description": "Lifecycle status of an INBOUND email (a row in the `emails`\ntable). Distinct from `SentEmailStatus`, which describes\nthe OUTBOUND lifecycle (the `sent_emails` table) and uses\na different vocabulary because the lifecycles differ.\nPossible values:\n\n - `pending`: the row was inserted at ingestion (mx_main)\n and has not yet completed the spam / filter / auth\n pipeline. Body and parsed fields are present; webhook\n delivery is not yet scheduled. Most rows transition out\n of `pending` within seconds.\n - `accepted`: the inbound passed the policy gates and is\n queued for webhook delivery. The `webhook_status` field\n tracks the separate webhook-delivery lifecycle from\n this point.\n - `completed`: terminal success. Webhook delivery\n attempted and acknowledged by every active endpoint, OR\n no endpoints are configured, so the row is durably\n archived.\n - `rejected`: terminal failure at ingestion (spam, blocked\n sender, filter rule, malformed). The body and metadata\n are stored for auditing but no webhook fires and the\n row is not repliable.\n\nSee also `webhook_status` (separate enum tracking the\nwebhook-delivery state machine) and `SentEmailStatus` (the\noutbound vocabulary).\n",
8686
+ "enum": [
8687
+ "pending",
8688
+ "accepted",
8689
+ "completed",
8690
+ "rejected"
8691
+ ]
8692
+ },
8693
+ "domain": { "type": "string" },
9082
8694
  "spam_score": { "type": ["number", "null"] },
9083
8695
  "raw_size_bytes": { "type": ["integer", "null"] },
9084
8696
  "raw_sha256": { "type": ["string", "null"] },
@@ -13093,532 +12705,6 @@ const operationManifest = [
13093
12705
  }
13094
12706
  ];
13095
12707
  //#endregion
13096
- //#region ../packages/api-core/src/client.ts
13097
- /**
13098
- * Host-aware Primitive API client and shared error type.
13099
- *
13100
- * Lives in api-core (instead of sdk-node) so the CLI can build a
13101
- * configured request client without taking a dependency on sdk-node.
13102
- * The higher-level `PrimitiveClient` (with `.send`, `.reply`,
13103
- * `.forward`) still lives in sdk-node because it needs the
13104
- * `ReceivedEmail` type from the webhook parsing surface.
13105
- */
13106
- const DEFAULT_API_BASE_URL_1 = "https://www.primitive.dev/api/v1";
13107
- const DEFAULT_API_BASE_URL_2 = "https://api.primitive.dev/v1";
13108
- function createDefaultAuth(apiKey) {
13109
- return (security) => {
13110
- if (security.type === "http" && security.scheme === "bearer") return apiKey;
13111
- };
13112
- }
13113
- var PrimitiveApiClient = class {
13114
- /**
13115
- * Generated client targeting the primary API host (apiBaseUrl1). Use
13116
- * this when passing `client: ...` to a generated operation function
13117
- * for every endpoint EXCEPT /send-mail. The hand-written
13118
- * PrimitiveClient.send / .reply / .forward methods on the subclass
13119
- * route /send-mail to the host-2 client internally.
13120
- */
13121
- client;
13122
- /**
13123
- * @internal Generated client targeting the attachments-supporting
13124
- * send host (apiBaseUrl2). Used by PrimitiveClient.send() under the
13125
- * hood. Exposed for the CLI's hand-rolled send command, which calls
13126
- * the generated sendEmail directly; not part of the publicly-
13127
- * documented SDK surface. Customer code should call .send() on the
13128
- * subclass instead.
13129
- */
13130
- _sendClient;
13131
- constructor(options = {}) {
13132
- const { apiKey, auth, apiBaseUrl1 = DEFAULT_API_BASE_URL_1, apiBaseUrl2 = DEFAULT_API_BASE_URL_2, ...config } = options;
13133
- const resolvedAuth = auth ?? createDefaultAuth(apiKey);
13134
- this.client = createClient(createConfig({
13135
- ...config,
13136
- auth: resolvedAuth,
13137
- baseUrl: apiBaseUrl1
13138
- }));
13139
- this._sendClient = createClient(createConfig({
13140
- ...config,
13141
- auth: resolvedAuth,
13142
- baseUrl: apiBaseUrl2
13143
- }));
13144
- }
13145
- getConfig() {
13146
- return this.client.getConfig();
13147
- }
13148
- setConfig(config) {
13149
- return this.client.setConfig(config);
13150
- }
13151
- };
13152
- //#endregion
13153
- //#region src/oclif/auth.ts
13154
- const CREDENTIALS_FILE = "credentials.json";
13155
- const CREDENTIALS_LOCK_DIR = "credentials.lock";
13156
- const CREDENTIALS_LOCK_OWNER_FILE = "owner.json";
13157
- const CREDENTIALS_LOCK_STALE_MS = 1800 * 1e3;
13158
- const MALFORMED_CREDENTIALS_HINT = "Run `primitive logout` and then `primitive signin`.";
13159
- const CREDENTIALS_LOCK_CLEANUP_SIGNALS = [
13160
- "SIGINT",
13161
- "SIGTERM",
13162
- "SIGHUP"
13163
- ];
13164
- function isRecord$2(value) {
13165
- return value !== null && typeof value === "object" && !Array.isArray(value);
13166
- }
13167
- function requireString(value, key) {
13168
- const raw = value[key];
13169
- 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}`);
13170
- return raw;
13171
- }
13172
- /**
13173
- * Sentinel returned by parseCredentials when the on-disk credentials were
13174
- * written by an API-key-based CLI. The caller treats this as "not logged in"
13175
- * after clearing the local file. The backing API key is intentionally not
13176
- * revoked; API keys still work when passed explicitly via --api-key/env.
13177
- */
13178
- var LegacyApiKeyCredentialFormatError = class extends Error {
13179
- constructor() {
13180
- super("legacy_api_key_credential_format");
13181
- this.name = "LegacyApiKeyCredentialFormatError";
13182
- }
13183
- };
13184
- function parseCredentials(raw) {
13185
- if (!isRecord$2(raw)) throw new Error(`Stored Primitive CLI credentials are malformed: expected a JSON object. ${MALFORMED_CREDENTIALS_HINT}`);
13186
- if (raw.auth_method !== "oauth") {
13187
- if (typeof raw.api_key === "string" || typeof raw.key_id === "string" || typeof raw.base_url === "string") throw new LegacyApiKeyCredentialFormatError();
13188
- throw new Error(`Stored Primitive CLI credentials are malformed: auth_method must be oauth. ${MALFORMED_CREDENTIALS_HINT}`);
13189
- }
13190
- const orgName = raw.org_name;
13191
- 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}`);
13192
- if (requireString(raw, "token_type") !== "Bearer") throw new Error(`Stored Primitive CLI credentials are malformed: token_type must be Bearer. ${MALFORMED_CREDENTIALS_HINT}`);
13193
- return {
13194
- auth_method: "oauth",
13195
- access_token: requireString(raw, "access_token"),
13196
- refresh_token: requireString(raw, "refresh_token"),
13197
- token_type: "Bearer",
13198
- expires_at: requireString(raw, "expires_at"),
13199
- oauth_grant_id: requireString(raw, "oauth_grant_id"),
13200
- oauth_client_id: requireString(raw, "oauth_client_id"),
13201
- org_id: requireString(raw, "org_id"),
13202
- org_name: orgName,
13203
- api_base_url_1: requireString(raw, "api_base_url_1"),
13204
- created_at: requireString(raw, "created_at")
13205
- };
13206
- }
13207
- function credentialsPath(configDir) {
13208
- return join(configDir, CREDENTIALS_FILE);
13209
- }
13210
- function credentialsLockPath(configDir) {
13211
- return join(configDir, CREDENTIALS_LOCK_DIR);
13212
- }
13213
- function normalize(url, fallback) {
13214
- const trimmed = url?.trim();
13215
- if (!trimmed) return fallback;
13216
- return trimmed.replace(/\/+$/, "");
13217
- }
13218
- function normalizeApiBaseUrl1(url) {
13219
- return normalize(url, DEFAULT_API_BASE_URL_1);
13220
- }
13221
- function normalizeApiBaseUrl2(url) {
13222
- return normalize(url, DEFAULT_API_BASE_URL_2);
13223
- }
13224
- function cliAccessTokenExpiresAt(expiresInSeconds, now = Date.now) {
13225
- return new Date(now() + expiresInSeconds * 1e3).toISOString();
13226
- }
13227
- function loadCliCredentials(configDir) {
13228
- const path = credentialsPath(configDir);
13229
- let contents;
13230
- try {
13231
- contents = readFileSync(path, "utf8");
13232
- } catch (error) {
13233
- if (error && typeof error === "object" && error.code === "ENOENT") return null;
13234
- const detail = error instanceof Error ? error.message : String(error);
13235
- throw new Error(`Could not read Primitive CLI credentials: ${detail}`);
13236
- }
13237
- try {
13238
- return parseCredentials(JSON.parse(contents));
13239
- } catch (error) {
13240
- if (error instanceof LegacyApiKeyCredentialFormatError) {
13241
- try {
13242
- rmSync(path, { force: true });
13243
- } catch {}
13244
- 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");
13245
- return null;
13246
- }
13247
- if (error instanceof SyntaxError) throw new Error("Stored Primitive CLI credentials are not valid JSON. Run `primitive logout` and then `primitive signin`.");
13248
- throw error;
13249
- }
13250
- }
13251
- function saveCliCredentials(configDir, credentials) {
13252
- mkdirSync(configDir, {
13253
- mode: 448,
13254
- recursive: true
13255
- });
13256
- const path = credentialsPath(configDir);
13257
- const tempPath = join(configDir, `${CREDENTIALS_FILE}.${process.pid}.${randomUUID()}.tmp`);
13258
- try {
13259
- writeFileSync(tempPath, `${JSON.stringify(credentials, null, 2)}\n`, { mode: 384 });
13260
- chmodSync(tempPath, 384);
13261
- renameSync(tempPath, path);
13262
- chmodSync(path, 384);
13263
- } catch (error) {
13264
- rmSync(tempPath, { force: true });
13265
- throw error;
13266
- }
13267
- }
13268
- function deleteCliCredentials(configDir) {
13269
- rmSync(credentialsPath(configDir), { force: true });
13270
- }
13271
- function deleteCliCredentialsLock(configDir) {
13272
- rmSync(credentialsLockPath(configDir), {
13273
- force: true,
13274
- recursive: true
13275
- });
13276
- }
13277
- function errorCode(error) {
13278
- return error && typeof error === "object" ? error.code : void 0;
13279
- }
13280
- function removeStaleCliCredentialsLock(lockPath, staleMs, now) {
13281
- try {
13282
- const stats = statSync(lockPath);
13283
- if (now() - stats.mtimeMs < staleMs) return false;
13284
- } catch (error) {
13285
- if (errorCode(error) === "ENOENT") return true;
13286
- throw error;
13287
- }
13288
- rmSync(lockPath, {
13289
- force: true,
13290
- recursive: true
13291
- });
13292
- return true;
13293
- }
13294
- function readCliCredentialsLockOwner(lockPath) {
13295
- let raw;
13296
- try {
13297
- raw = readFileSync(join(lockPath, CREDENTIALS_LOCK_OWNER_FILE), "utf8");
13298
- } catch (error) {
13299
- if (errorCode(error) === "ENOENT") return null;
13300
- throw error;
13301
- }
13302
- try {
13303
- const pid = JSON.parse(raw)?.pid;
13304
- return Number.isInteger(pid) && pid > 0 ? { pid } : null;
13305
- } catch {
13306
- return null;
13307
- }
13308
- }
13309
- function processIsRunning(pid) {
13310
- try {
13311
- process.kill(pid, 0);
13312
- return true;
13313
- } catch (error) {
13314
- if (errorCode(error) === "ESRCH") return false;
13315
- return true;
13316
- }
13317
- }
13318
- function removeRecoverableCliCredentialsLock(params) {
13319
- const owner = readCliCredentialsLockOwner(params.lockPath);
13320
- if (owner && params.isRunning(owner.pid)) return false;
13321
- if (owner) {
13322
- rmSync(params.lockPath, {
13323
- force: true,
13324
- recursive: true
13325
- });
13326
- return true;
13327
- }
13328
- return removeStaleCliCredentialsLock(params.lockPath, params.staleMs, params.now);
13329
- }
13330
- function writeCliCredentialsLockOwner(lockPath) {
13331
- const ownerPath = join(lockPath, CREDENTIALS_LOCK_OWNER_FILE);
13332
- writeFileSync(ownerPath, `${JSON.stringify({
13333
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
13334
- pid: process.pid
13335
- })}\n`, { mode: 384 });
13336
- chmodSync(ownerPath, 384);
13337
- }
13338
- function installCredentialsLockSignalCleanup(lockPath) {
13339
- let active = true;
13340
- const listeners = CREDENTIALS_LOCK_CLEANUP_SIGNALS.map((signal) => {
13341
- const listener = () => {
13342
- if (!active) return;
13343
- active = false;
13344
- rmSync(lockPath, {
13345
- force: true,
13346
- recursive: true
13347
- });
13348
- process.exit(signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 129);
13349
- };
13350
- process.once(signal, listener);
13351
- return {
13352
- listener,
13353
- signal
13354
- };
13355
- });
13356
- return () => {
13357
- if (!active) return;
13358
- active = false;
13359
- for (const { listener, signal } of listeners) process.removeListener(signal, listener);
13360
- };
13361
- }
13362
- function credentialsLockInProgressMessage(lockPath) {
13363
- 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}.`;
13364
- }
13365
- function acquireCliCredentialsLock(configDir, options = {}) {
13366
- mkdirSync(configDir, {
13367
- mode: 448,
13368
- recursive: true
13369
- });
13370
- const lockPath = credentialsLockPath(configDir);
13371
- const installSignalHandlers = options.installSignalHandlers ?? true;
13372
- const isRunning = options.isProcessRunning ?? processIsRunning;
13373
- const now = options.now ?? Date.now;
13374
- const staleMs = options.staleMs ?? CREDENTIALS_LOCK_STALE_MS;
13375
- let acquired = false;
13376
- for (let attempt = 0; attempt < 2; attempt += 1) try {
13377
- mkdirSync(lockPath, { mode: 448 });
13378
- acquired = true;
13379
- break;
13380
- } catch (error) {
13381
- if (errorCode(error) !== "EEXIST") throw error;
13382
- if (removeRecoverableCliCredentialsLock({
13383
- isRunning,
13384
- lockPath,
13385
- now,
13386
- staleMs
13387
- })) continue;
13388
- throw new Error(credentialsLockInProgressMessage(lockPath));
13389
- }
13390
- if (!acquired) throw new Error(credentialsLockInProgressMessage(lockPath));
13391
- try {
13392
- writeCliCredentialsLockOwner(lockPath);
13393
- } catch (error) {
13394
- rmSync(lockPath, {
13395
- force: true,
13396
- recursive: true
13397
- });
13398
- throw error;
13399
- }
13400
- const removeSignalCleanup = installSignalHandlers ? installCredentialsLockSignalCleanup(lockPath) : () => void 0;
13401
- let released = false;
13402
- return () => {
13403
- if (released) return;
13404
- released = true;
13405
- removeSignalCleanup();
13406
- rmSync(lockPath, {
13407
- force: true,
13408
- recursive: true
13409
- });
13410
- };
13411
- }
13412
- function resolveCliAuth(params) {
13413
- const apiKey = params.apiKey?.trim();
13414
- const apiBaseUrl2 = normalizeApiBaseUrl2(params.apiBaseUrl2);
13415
- if (apiKey) return {
13416
- apiKey,
13417
- apiBaseUrl1: normalizeApiBaseUrl1(params.apiBaseUrl1),
13418
- apiBaseUrl2,
13419
- credentials: null,
13420
- source: "flag-or-env"
13421
- };
13422
- const credentials = loadCliCredentials(params.configDir);
13423
- if (credentials) return {
13424
- apiKey: credentials.access_token,
13425
- apiBaseUrl1: credentials.api_base_url_1,
13426
- apiBaseUrl2,
13427
- credentials,
13428
- source: "stored"
13429
- };
13430
- return {
13431
- apiKey: void 0,
13432
- apiBaseUrl1: normalizeApiBaseUrl1(params.apiBaseUrl1),
13433
- apiBaseUrl2,
13434
- credentials: null,
13435
- source: "none"
13436
- };
13437
- }
13438
- //#endregion
13439
- //#region src/oclif/cli-config.ts
13440
- const CONFIG_FILE = "config.json";
13441
- const CONFIG_VERSION = 1;
13442
- const DEFAULT_ENVIRONMENT = "default";
13443
- function cliConfigPath(configDir) {
13444
- return join(configDir, CONFIG_FILE);
13445
- }
13446
- function cliConfigError(message) {
13447
- return new Errors.CLIError(`${message} Run \`primitive config reset\` to clear the local CLI config.`, { exit: 1 });
13448
- }
13449
- function isRecord$1(value) {
13450
- return value !== null && typeof value === "object" && !Array.isArray(value);
13451
- }
13452
- function normalizeCliEnvironmentName(name) {
13453
- const trimmed = name?.trim();
13454
- if (!trimmed) throw new Errors.CLIError("Environment name must be a non-empty string.", { exit: 1 });
13455
- 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 });
13456
- return trimmed;
13457
- }
13458
- function validateCliHeaderName(name) {
13459
- const trimmed = name.trim();
13460
- if (!trimmed) throw new Errors.CLIError("Header name must be a non-empty string.", { exit: 1 });
13461
- if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(trimmed)) throw new Errors.CLIError(`Invalid header name: ${name}`, { exit: 1 });
13462
- if (trimmed.toLowerCase() === "authorization") throw new Errors.CLIError("The Authorization header is managed by PRIMITIVE_API_KEY or saved OAuth CLI credentials.", { exit: 1 });
13463
- return trimmed;
13464
- }
13465
- function validateCliHeaderValue(value, name) {
13466
- if (value.length === 0) throw new Errors.CLIError(`Header ${name} value must not be empty.`, { exit: 1 });
13467
- if (/[\r\n\0]/.test(value)) throw new Errors.CLIError(`Header ${name} value must not contain CR, LF, or NUL characters.`, { exit: 1 });
13468
- return value;
13469
- }
13470
- function parseHeaderAssignment(assignment) {
13471
- const separator = assignment.indexOf("=");
13472
- if (separator <= 0) throw new Errors.CLIError("Header values must use name=value syntax, for example `x-custom=secret`.", { exit: 1 });
13473
- const name = validateCliHeaderName(assignment.slice(0, separator));
13474
- return [name, validateCliHeaderValue(assignment.slice(separator + 1), name)];
13475
- }
13476
- function parseHeaders(raw, context) {
13477
- if (raw === void 0) return {};
13478
- if (!isRecord$1(raw)) throw cliConfigError(`${context} headers must be a JSON object.`);
13479
- const headers = {};
13480
- for (const [rawName, rawValue] of Object.entries(raw)) {
13481
- const name = validateCliHeaderName(rawName);
13482
- if (typeof rawValue !== "string") throw cliConfigError(`${context} header ${name} must be a string.`);
13483
- headers[name] = validateCliHeaderValue(rawValue, name);
13484
- }
13485
- return headers;
13486
- }
13487
- function parseEnvironmentConfig(raw, context) {
13488
- if (!isRecord$1(raw)) throw cliConfigError(`${context} must be a JSON object.`);
13489
- const env = {};
13490
- if (raw.api_base_url_1 !== void 0) {
13491
- if (typeof raw.api_base_url_1 !== "string") throw cliConfigError(`${context}.api_base_url_1 must be a string.`);
13492
- env.api_base_url_1 = normalizeApiBaseUrl1(raw.api_base_url_1);
13493
- }
13494
- if (raw.api_base_url_2 !== void 0) {
13495
- if (typeof raw.api_base_url_2 !== "string") throw cliConfigError(`${context}.api_base_url_2 must be a string.`);
13496
- env.api_base_url_2 = normalizeApiBaseUrl2(raw.api_base_url_2);
13497
- }
13498
- const headers = parseHeaders(raw.headers, context);
13499
- if (Object.keys(headers).length > 0) env.headers = headers;
13500
- return env;
13501
- }
13502
- function parseStoredCliConfig(raw) {
13503
- if (!isRecord$1(raw)) throw cliConfigError("Primitive CLI config must be a JSON object.");
13504
- if (raw.version !== CONFIG_VERSION) throw cliConfigError(`Primitive CLI config version must be ${CONFIG_VERSION}.`);
13505
- const currentRaw = raw.current_environment;
13506
- const current_environment = currentRaw === null || currentRaw === void 0 ? null : typeof currentRaw === "string" ? normalizeCliEnvironmentName(currentRaw) : (() => {
13507
- throw cliConfigError("Primitive CLI config current_environment must be a string or null.");
13508
- })();
13509
- if (!isRecord$1(raw.environments)) throw cliConfigError("Primitive CLI config environments must be an object.");
13510
- const environments = {};
13511
- for (const [rawName, rawEnv] of Object.entries(raw.environments)) {
13512
- const name = normalizeCliEnvironmentName(rawName);
13513
- environments[name] = parseEnvironmentConfig(rawEnv, `Primitive CLI config environment ${name}`);
13514
- }
13515
- if (current_environment && !environments[current_environment]) throw cliConfigError(`Primitive CLI config current environment ${current_environment} does not exist.`);
13516
- return {
13517
- version: CONFIG_VERSION,
13518
- current_environment,
13519
- environments
13520
- };
13521
- }
13522
- function emptyCliConfig() {
13523
- return {
13524
- version: CONFIG_VERSION,
13525
- current_environment: null,
13526
- environments: {}
13527
- };
13528
- }
13529
- function loadCliConfig(configDir) {
13530
- const path = cliConfigPath(configDir);
13531
- let contents;
13532
- try {
13533
- contents = readFileSync(path, "utf8");
13534
- } catch (error) {
13535
- if (error && typeof error === "object" && error.code === "ENOENT") return null;
13536
- throw cliConfigError(`Could not read Primitive CLI config: ${error instanceof Error ? error.message : String(error)}.`);
13537
- }
13538
- try {
13539
- return parseStoredCliConfig(JSON.parse(contents));
13540
- } catch (error) {
13541
- if (error instanceof SyntaxError) throw cliConfigError("Primitive CLI config is not valid JSON.");
13542
- throw error;
13543
- }
13544
- }
13545
- function saveCliConfig(configDir, config) {
13546
- mkdirSync(configDir, {
13547
- mode: 448,
13548
- recursive: true
13549
- });
13550
- const path = cliConfigPath(configDir);
13551
- const tempPath = join(configDir, `${CONFIG_FILE}.${process.pid}.${randomUUID()}.tmp`);
13552
- try {
13553
- writeFileSync(tempPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
13554
- renameSync(tempPath, path);
13555
- } catch (error) {
13556
- rmSync(tempPath, { force: true });
13557
- throw error;
13558
- }
13559
- }
13560
- function deleteCliConfig(configDir) {
13561
- rmSync(cliConfigPath(configDir), { force: true });
13562
- }
13563
- function resolveConfigEnvironment(config) {
13564
- if (!config) return null;
13565
- const current = config.current_environment;
13566
- if (current) {
13567
- const environment = config.environments[current];
13568
- return environment ? {
13569
- name: current,
13570
- config: environment
13571
- } : null;
13572
- }
13573
- const defaultEnvironment = config.environments[DEFAULT_ENVIRONMENT];
13574
- return defaultEnvironment ? {
13575
- name: DEFAULT_ENVIRONMENT,
13576
- config: defaultEnvironment
13577
- } : null;
13578
- }
13579
- function upsertCliEnvironment(params) {
13580
- const name = normalizeCliEnvironmentName(params.environmentName ?? "default");
13581
- const existing = params.config.environments[name] ?? {};
13582
- const nextHeaders = { ...existing.headers ?? {} };
13583
- for (const assignment of params.headers ?? []) {
13584
- const [headerName, value] = parseHeaderAssignment(assignment);
13585
- nextHeaders[headerName] = value;
13586
- }
13587
- for (const rawName of params.unsetHeaders ?? []) delete nextHeaders[validateCliHeaderName(rawName)];
13588
- const nextEnvironment = {
13589
- ...existing,
13590
- ...params.apiBaseUrl1 !== void 0 ? { api_base_url_1: normalizeApiBaseUrl1(params.apiBaseUrl1) } : {},
13591
- ...params.apiBaseUrl2 !== void 0 ? { api_base_url_2: normalizeApiBaseUrl2(params.apiBaseUrl2) } : {},
13592
- ...Object.keys(nextHeaders).length > 0 ? { headers: nextHeaders } : {}
13593
- };
13594
- if (Object.keys(nextHeaders).length === 0) delete nextEnvironment.headers;
13595
- return {
13596
- ...params.config,
13597
- current_environment: params.use === false ? params.config.current_environment : name,
13598
- environments: {
13599
- ...params.config.environments,
13600
- [name]: nextEnvironment
13601
- }
13602
- };
13603
- }
13604
- function removeCliEnvironment(config, environmentName) {
13605
- const name = normalizeCliEnvironmentName(environmentName);
13606
- const environments = { ...config.environments };
13607
- delete environments[name];
13608
- return {
13609
- ...config,
13610
- current_environment: config.current_environment === name ? null : config.current_environment,
13611
- environments
13612
- };
13613
- }
13614
- function redactCliEnvironment(environment) {
13615
- const headers = environment.headers && Object.keys(environment.headers).length > 0 ? Object.fromEntries(Object.keys(environment.headers).map((name) => [name, "***"])) : void 0;
13616
- return {
13617
- ...environment,
13618
- ...headers ? { headers } : {}
13619
- };
13620
- }
13621
- //#endregion
13622
12708
  //#region src/oclif/api-client.ts
13623
12709
  const API_HEADERS_ENV = "PRIMITIVE_API_HEADERS";
13624
12710
  const OAUTH_REFRESH_SKEW_MS = 60 * 1e3;
@@ -14535,15 +13621,11 @@ async function fetchEmailSearchPage(params) {
14535
13621
  function sleep$1(ms) {
14536
13622
  return new Promise((resolve) => setTimeout(resolve, ms));
14537
13623
  }
14538
- //#endregion
14539
- //#region src/oclif/commands/chat.ts
14540
- const DEFAULT_CHAT_TIMEOUT_SECONDS = 120;
14541
- const DEFAULT_STRICT_PHASE_SECONDS = 60;
14542
13624
  function cliError$6(message) {
14543
13625
  return new Errors.CLIError(message, { exit: 1 });
14544
13626
  }
14545
- async function readStdinToString() {
14546
- if (process.stdin.isTTY) throw cliError$6("No message provided. Pass the message as the second positional argument or pipe it via stdin.");
13627
+ async function readStdinToString(missingMessage = "No message provided. Pass the message as the second positional argument or pipe it via stdin.") {
13628
+ if (process.stdin.isTTY) throw cliError$6(missingMessage);
14547
13629
  const chunks = [];
14548
13630
  for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
14549
13631
  return Buffer.concat(chunks).toString("utf8");
@@ -14655,6 +13737,11 @@ function shellQuote(value) {
14655
13737
  function commandFromArgv(argv) {
14656
13738
  return argv.map(shellQuote).join(" ");
14657
13739
  }
13740
+ function parseLocalChatIdArg(value) {
13741
+ if (value === void 0 || !/^(0|[1-9]\d*)$/.test(value)) return null;
13742
+ const parsed = Number(value);
13743
+ return Number.isSafeInteger(parsed) ? parsed : null;
13744
+ }
14658
13745
  function resolveChatResponseBody(reply) {
14659
13746
  if (reply.body_text && reply.body_text.length > 0) return {
14660
13747
  body: reply.body_text,
@@ -14709,10 +13796,35 @@ function buildCommand(kind, description, argv, options = {}) {
14709
13796
  requires_message: requiresMessage
14710
13797
  };
14711
13798
  }
13799
+ function shouldPreferStrictContinuation(context) {
13800
+ const hasCustomStrictPhase = context.strictPhaseSeconds !== 60;
13801
+ return context.strictOnly || context.matchStrategy === "strict" && !hasCustomStrictPhase;
13802
+ }
14712
13803
  function buildChatFollowUpCommands(context) {
14713
13804
  const commands = [];
14714
- const hasCustomStrictPhase = context.strictPhaseSeconds !== DEFAULT_STRICT_PHASE_SECONDS;
14715
- const shouldPreferStrictContinuation = context.strictOnly || context.matchStrategy === "strict" && !hasCustomStrictPhase;
13805
+ const hasCustomStrictPhase = context.strictPhaseSeconds !== 60;
13806
+ const preferStrictContinuation = shouldPreferStrictContinuation(context);
13807
+ if (context.localChatId !== void 0) {
13808
+ const localContinueParts = [
13809
+ "primitive",
13810
+ "chat",
13811
+ "reply",
13812
+ String(context.localChatId),
13813
+ "<message>"
13814
+ ];
13815
+ if (context.json) localContinueParts.push("--json");
13816
+ if (context.quiet) localContinueParts.push("--quiet");
13817
+ commands.push(buildCommand("continue_chat", "Continue this chat", localContinueParts, { requiresMessage: true }));
13818
+ const activeContinueParts = [
13819
+ "primitive",
13820
+ "chat",
13821
+ "reply",
13822
+ "<message>"
13823
+ ];
13824
+ if (context.json) activeContinueParts.push("--json");
13825
+ if (context.quiet) activeContinueParts.push("--quiet");
13826
+ commands.push(buildCommand("continue_active_chat", "Continue the active chat", activeContinueParts, { requiresMessage: true }));
13827
+ }
14716
13828
  const continueParts = [
14717
13829
  "primitive",
14718
13830
  "chat",
@@ -14728,9 +13840,9 @@ function buildChatFollowUpCommands(context) {
14728
13840
  ];
14729
13841
  if (context.json) continueParts.push("--json");
14730
13842
  if (context.quiet) continueParts.push("--quiet");
14731
- if (shouldPreferStrictContinuation) continueParts.push("--strict-only");
13843
+ if (preferStrictContinuation) continueParts.push("--strict-only");
14732
13844
  else if (hasCustomStrictPhase) continueParts.push("--strict-phase-seconds", String(context.strictPhaseSeconds));
14733
- commands.push(buildCommand("continue_chat", "Continue this chat", continueParts, { requiresMessage: true }));
13845
+ commands.push(buildCommand(context.localChatId === void 0 ? "continue_chat" : "continue_chat_explicit", context.localChatId === void 0 ? "Continue this chat" : "Continue this chat explicitly", continueParts, { requiresMessage: true }));
14734
13846
  commands.push(buildCommand("reply_direct", "Reply directly to the inbound email", [
14735
13847
  "primitive",
14736
13848
  "reply",
@@ -14804,6 +13916,7 @@ function buildChatJsonEnvelope(context) {
14804
13916
  return {
14805
13917
  sent: context.sent,
14806
13918
  reply: context.reply,
13919
+ local_chat_id: context.localChatId ?? null,
14807
13920
  response_body: responseBody.body,
14808
13921
  response_body_format: responseBody.format,
14809
13922
  match: {
@@ -14814,6 +13927,25 @@ function buildChatJsonEnvelope(context) {
14814
13927
  follow_up_commands: buildChatFollowUpCommands(context)
14815
13928
  };
14816
13929
  }
13930
+ function persistActiveChat(params) {
13931
+ try {
13932
+ return saveActiveChatState(params.configDir, {
13933
+ from: params.context.from,
13934
+ last_reply_email_id: params.context.reply.id,
13935
+ last_reply_received_at: params.context.reply.received_at,
13936
+ last_sent_email_id: params.context.sent.id,
13937
+ recipient: params.context.recipient,
13938
+ strict_only: shouldPreferStrictContinuation(params.context),
13939
+ strict_phase_seconds: params.context.strictPhaseSeconds,
13940
+ thread_id: params.context.reply.thread_id ?? null,
13941
+ timeout_seconds: params.context.timeoutSeconds
13942
+ }, { preferredLocalId: params.preferredLocalId }).local_id;
13943
+ } catch (error) {
13944
+ const detail = error instanceof Error ? error.message : String(error);
13945
+ params.writeWarning?.(`Warning: could not save local chat state: ${detail}\n`);
13946
+ return null;
13947
+ }
13948
+ }
14817
13949
  function formatChatResponse(context) {
14818
13950
  const accepted = context.sent.accepted.join(", ") || context.recipient;
14819
13951
  const responseBody = resolveChatResponseBody(context.reply);
@@ -14837,6 +13969,7 @@ function formatChatResponse(context) {
14837
13969
  ];
14838
13970
  if (context.reply.reply_to_sent_email_id) lines.push(` Reply to sent email id: ${context.reply.reply_to_sent_email_id}`);
14839
13971
  if (context.reply.message_id) lines.push(` Message-Id: ${context.reply.message_id}`);
13972
+ if (context.localChatId !== void 0) lines.push(` Local chat id: ${context.localChatId}`);
14840
13973
  lines.push("", "Helpful follow-up commands", " Replace <message> before running commands that include it.", " Commands are templates; use --json for parse-safe output.", " When shown, --strict-only prefers timing out over matching the wrong reply.");
14841
13974
  for (const { description, command } of buildChatFollowUpCommands(context)) lines.push(` ${description}:`, ` ${command}`);
14842
13975
  lines.push("", `Response body (${responseBody.format}; use --json for parsing)`, "----- BEGIN RESPONSE -----", responseBody.body || "(empty response)", "----- END RESPONSE -----");
@@ -14926,6 +14059,8 @@ var ChatCommand = class ChatCommand extends Command {
14926
14059
  --reply-to-email-id <inbound-email-id>. Reply mode uses Primitive's
14927
14060
  reply endpoint, so the reply subject and threading headers are
14928
14061
  derived from the inbound email instead of copied into CLI flags.
14062
+ Successful chat turns also save an active local chat, so the next
14063
+ follow-up can be sent with \`primitive chat reply '<message>'\`.
14929
14064
 
14930
14065
  --json emits a structured envelope with both sides of the exchange,
14931
14066
  a direct response_body field, match details, and follow-up command
@@ -14945,6 +14080,7 @@ var ChatCommand = class ChatCommand extends Command {
14945
14080
  static examples = [
14946
14081
  "<%= config.bin %> chat help@agent.acme.dev 'how do I rotate my API key?'",
14947
14082
  "cat error.log | <%= config.bin %> chat help@agent.acme.dev",
14083
+ "<%= config.bin %> chat reply 'one more thing'",
14948
14084
  "<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing'",
14949
14085
  "<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing' --reply-to-email-id <inbound-email-id>",
14950
14086
  "<%= config.bin %> chat help@agent.acme.dev 'follow up question' --json",
@@ -14980,15 +14116,20 @@ var ChatCommand = class ChatCommand extends Command {
14980
14116
  reply: Flags.string({ description: "Reply body. Continues the latest inbound email from the recipient to your sender address; pass --reply-to-email-id for an exact thread." }),
14981
14117
  "reply-to-email-id": Flags.string({ description: "Inbound email id to continue exactly. Uses Primitive's reply endpoint, so recipient, subject, and threading headers are derived from the inbound email." }),
14982
14118
  "in-reply-to": Flags.string({ description: "Raw Message-Id of the parent email to thread a new send against. Prefer --reply-to-email-id with --reply when continuing an inbound email stored by Primitive." }),
14119
+ "chat-local-id": Flags.integer({
14120
+ description: "Local chat id to update after this command succeeds. Internal plumbing for `primitive chat reply`.",
14121
+ hidden: true,
14122
+ min: 0
14123
+ }),
14983
14124
  json: Flags.boolean({ description: "Emit a structured JSON envelope { sent, reply, response_body, response_body_format, match, follow_up_commands } on stdout instead of the human-readable transcript." }),
14984
14125
  quiet: Flags.boolean({ description: "Suppress stderr progress updates while sending and waiting. Errors and recovery commands are still written to stderr." }),
14985
14126
  timeout: Flags.integer({
14986
- default: DEFAULT_CHAT_TIMEOUT_SECONDS,
14127
+ default: 120,
14987
14128
  description: "Seconds to wait for a reply before exiting non-zero; 0 waits forever.",
14988
14129
  min: 0
14989
14130
  }),
14990
14131
  "strict-phase-seconds": Flags.integer({
14991
- default: DEFAULT_STRICT_PHASE_SECONDS,
14132
+ default: 60,
14992
14133
  description: "Seconds to wait in strict-threading mode (filter by reply_to_sent_email_id) before falling back to time-window matching. Set to the full --timeout to disable the fallback; --strict-only is the explicit way to do that.",
14993
14134
  min: 1
14994
14135
  }),
@@ -15172,16 +14313,133 @@ var ChatCommand = class ChatCommand extends Command {
15172
14313
  return;
15173
14314
  }
15174
14315
  progress?.succeed(`Reply received from ${replyResult.reply.from_email}`);
15175
- const outputContext = {
14316
+ let outputContext = {
15176
14317
  ...baseContext,
15177
14318
  matchStrategy: replyResult.matchStrategy,
15178
14319
  reply: replyResult.reply
15179
14320
  };
14321
+ const localChatId = persistActiveChat({
14322
+ configDir: this.config.configDir,
14323
+ context: outputContext,
14324
+ preferredLocalId: flags["chat-local-id"],
14325
+ writeWarning: (message) => process.stderr.write(message)
14326
+ });
14327
+ if (localChatId !== null) outputContext = {
14328
+ ...outputContext,
14329
+ localChatId
14330
+ };
15180
14331
  if (flags.json) this.log(JSON.stringify(buildChatJsonEnvelope(outputContext), null, 2));
15181
14332
  else this.log(formatChatResponse(outputContext));
15182
14333
  });
15183
14334
  }
15184
14335
  };
14336
+ var ChatReplyCommand = class ChatReplyCommand extends Command {
14337
+ static description = `Reply in the active chat.
14338
+
14339
+ A successful \`primitive chat <email> <message>\` saves the latest
14340
+ inbound reply as a local chat and makes it active. Use
14341
+ \`primitive chat reply <message>\` for the active chat, or
14342
+ \`primitive chat reply <local-id> <message>\` / \`--id <local-id>\`
14343
+ for a specific local chat. The command uses Primitive's real reply
14344
+ endpoint against the stored inbound email id, so the recipient,
14345
+ subject, and threading headers are derived server-side from the
14346
+ thread.
14347
+
14348
+ If no chat is open, start one with \`primitive chat <email> '<message>'\`.
14349
+ For explicit control, use \`primitive chat <email> --reply '<message>'
14350
+ --reply-to-email-id <inbound-email-id>\`.`;
14351
+ static summary = "Reply in the active chat";
14352
+ static examples = [
14353
+ "<%= config.bin %> chat reply 'one more thing'",
14354
+ "<%= config.bin %> chat reply 0 'one more thing'",
14355
+ "<%= config.bin %> chat reply --id 0 'one more thing'",
14356
+ "cat follow-up.txt | <%= config.bin %> chat reply"
14357
+ ];
14358
+ static args = {
14359
+ idOrMessage: Args.string({ description: "Reply body, or a local chat id when followed by a separate message." }),
14360
+ message: Args.string({ description: "Reply body when the first positional argument is an id." })
14361
+ };
14362
+ static flags = {
14363
+ "api-key": Flags.string({
14364
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive signin` credentials)",
14365
+ env: "PRIMITIVE_API_KEY"
14366
+ }),
14367
+ "api-base-url-1": Flags.string({
14368
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
14369
+ env: "PRIMITIVE_API_BASE_URL_1",
14370
+ hidden: true
14371
+ }),
14372
+ "api-base-url-2": Flags.string({
14373
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
14374
+ env: "PRIMITIVE_API_BASE_URL_2",
14375
+ hidden: true
14376
+ }),
14377
+ id: Flags.integer({
14378
+ description: "Local chat id to reply in. Omit to use the most recent active chat.",
14379
+ min: 0
14380
+ }),
14381
+ json: Flags.boolean({ description: "Emit a structured JSON envelope { sent, reply, response_body, response_body_format, match, follow_up_commands } on stdout instead of the human-readable transcript." }),
14382
+ quiet: Flags.boolean({ description: "Suppress stderr progress updates while sending and waiting. Errors and recovery commands are still written to stderr." }),
14383
+ timeout: Flags.integer({
14384
+ description: "Seconds to wait for a reply before exiting non-zero. Defaults to the active chat's last timeout.",
14385
+ min: 0
14386
+ }),
14387
+ "strict-phase-seconds": Flags.integer({
14388
+ description: "Seconds to wait in strict-threading mode before falling back. Defaults to the active chat's last setting.",
14389
+ min: 1
14390
+ }),
14391
+ "strict-only": Flags.boolean({ description: "Disable the time-window fallback. If the active chat was saved from a strict match, this is already the default." }),
14392
+ interval: Flags.integer({
14393
+ description: "Seconds between polls while waiting for the reply.",
14394
+ min: 1
14395
+ }),
14396
+ "page-size": Flags.integer({
14397
+ description: "Inbound emails to fetch per poll while waiting (1-100). Internal tuning knob.",
14398
+ max: 100,
14399
+ min: 1,
14400
+ hidden: true
14401
+ }),
14402
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
14403
+ };
14404
+ async run() {
14405
+ const { args, flags } = await this.parse(ChatReplyCommand);
14406
+ const positionalLocalId = flags.id === void 0 && args.message !== void 0 ? parseLocalChatIdArg(args.idOrMessage) : void 0;
14407
+ if (flags.id === void 0 && args.message !== void 0 && positionalLocalId === null) throw cliError$6("When passing two positional arguments to `primitive chat reply`, the first must be a local chat id. Use `primitive chat reply '<message>'` for the active chat or `primitive chat reply --id <id> '<message>'` for a specific chat.");
14408
+ if (flags.id !== void 0 && args.message !== void 0) throw cliError$6("With --id, pass the reply body as a single positional argument or pipe it via stdin.");
14409
+ const localId = flags.id ?? (typeof positionalLocalId === "number" ? positionalLocalId : void 0);
14410
+ const state = localId === void 0 ? loadActiveChatState(this.config.configDir) : loadChatConversationByLocalId(this.config.configDir, localId);
14411
+ if (!state) throw cliError$6(localId === void 0 ? "No open chat. Start one with `primitive chat <email> '<message>'`." : `No local chat ${localId}. Start one with \`primitive chat <email> '<message>'\` or omit --id to use the active chat.`);
14412
+ const message = args.message !== void 0 ? args.message : args.idOrMessage !== void 0 && args.idOrMessage !== "" ? args.idOrMessage : await readStdinToString("No reply body provided. Pass the reply body as a positional argument or pipe it via stdin.");
14413
+ if (!message.trim()) throw cliError$6("Reply body is empty.");
14414
+ const argv = [
14415
+ state.recipient,
14416
+ "--reply",
14417
+ message,
14418
+ "--from",
14419
+ state.from,
14420
+ "--reply-to-email-id",
14421
+ state.last_reply_email_id,
14422
+ "--timeout",
14423
+ String(flags.timeout ?? state.timeout_seconds),
14424
+ "--strict-phase-seconds",
14425
+ String(flags["strict-phase-seconds"] ?? state.strict_phase_seconds),
14426
+ "--interval",
14427
+ String(flags.interval ?? 2),
14428
+ "--page-size",
14429
+ String(flags["page-size"] ?? 50),
14430
+ "--chat-local-id",
14431
+ String(state.local_id)
14432
+ ];
14433
+ if (flags["api-key"] !== void 0) argv.push("--api-key", flags["api-key"]);
14434
+ if (flags["api-base-url-1"] !== void 0) argv.push("--api-base-url-1", flags["api-base-url-1"]);
14435
+ if (flags["api-base-url-2"] !== void 0) argv.push("--api-base-url-2", flags["api-base-url-2"]);
14436
+ if (flags.json) argv.push("--json");
14437
+ if (flags.quiet) argv.push("--quiet");
14438
+ if (state.strict_only || flags["strict-only"]) argv.push("--strict-only");
14439
+ if (flags.time) argv.push("--time");
14440
+ await ChatCommand.run(argv, { root: this.config.root });
14441
+ }
14442
+ };
15185
14443
  async function waitForReply(params) {
15186
14444
  const notice = params.notice ?? ((message) => {
15187
14445
  process.stderr.write(`${message}\n`);
@@ -17186,8 +16444,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
17186
16444
  name: "Primitive Team",
17187
16445
  url: "https://primitive.dev"
17188
16446
  };
17189
- const SDK_VERSION_RANGE = "^0.33.0";
17190
- const CLI_VERSION_RANGE = "^0.33.0";
16447
+ const SDK_VERSION_RANGE = "^0.34.0";
16448
+ const CLI_VERSION_RANGE = "^0.34.0";
17191
16449
  const ESBUILD_VERSION_RANGE = "^0.27.0";
17192
16450
  function renderHandler() {
17193
16451
  return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
@@ -18904,6 +18162,7 @@ var LoginCommand$1 = class extends Command {
18904
18162
  if (polled.data) {
18905
18163
  const login = unwrapData$2(polled.data);
18906
18164
  if (!login) throw cliError$3("Primitive API returned an empty CLI poll response.");
18165
+ deleteChatState(this.config.configDir);
18907
18166
  saveCliCredentials(this.config.configDir, {
18908
18167
  access_token: login.access_token,
18909
18168
  api_base_url_1: apiBaseUrl1,
@@ -19125,6 +18384,7 @@ async function checkExistingCredentials(params) {
19125
18384
  throw cliError$2(`Already logged in${existing.org_name ? ` for ${existing.org_name}` : ""}. Run \`primitive logout\` before ${copy.actionGerund}.`);
19126
18385
  }
19127
18386
  function saveSignupCredentials(params) {
18387
+ deleteChatState(params.configDir);
19128
18388
  saveCliCredentials(params.configDir, {
19129
18389
  access_token: params.signup.access_token,
19130
18390
  api_base_url_1: params.apiBaseUrl1,
@@ -19606,6 +18866,7 @@ function runForceLogout(params) {
19606
18866
  const lockPath = credentialsLockPath(params.configDir);
19607
18867
  const removed = [
19608
18868
  existsSync(localCredentialsPath) ? "local Primitive CLI credentials" : null,
18869
+ existsSync(chatStatePath(params.configDir)) ? "local chat reply state" : null,
19609
18870
  existsSync(pendingPath) ? "pending email-code auth state" : null,
19610
18871
  existsSync(lockPath) ? "credential lock" : null
19611
18872
  ].filter((value) => value !== null);
@@ -20659,6 +19920,7 @@ const CANONICAL_OPERATION_ALIASES = {
20659
19920
  "domains:list": "domains:list-domains",
20660
19921
  "domains:update": "domains:update-domain",
20661
19922
  "domains:verify": "domains:verify-domain",
19923
+ "emails:conversation": "emails:get-conversation",
20662
19924
  "emails:delete": "emails:delete-email",
20663
19925
  "emails:discard-content": "emails:discard-email-content",
20664
19926
  "emails:download-raw": "emails:download-raw-email",
@@ -20718,6 +19980,7 @@ const COMMANDS = {
20718
19980
  send: SendCommand,
20719
19981
  reply: ReplyCommand,
20720
19982
  chat: ChatCommand,
19983
+ "chat:reply": ChatReplyCommand,
20721
19984
  login: LoginCommand,
20722
19985
  "login:browser": LoginBrowserCommand,
20723
19986
  "login:confirm": LoginConfirmCommand,