@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.
@@ -1,4 +1,5 @@
1
- import { Args, Command, Errors, Flags } from "@oclif/core";
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-SktG2dzR.js";
2
+ import { Args, Command, Errors, Flags, ux } 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";
4
5
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
@@ -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,
@@ -674,6 +68,7 @@ var sdk_gen_exports = /* @__PURE__ */ __exportAll({
674
68
  resendCliSignupVerification: () => resendCliSignupVerification,
675
69
  rotateWebhookSecret: () => rotateWebhookSecret,
676
70
  searchEmails: () => searchEmails,
71
+ semanticSearch: () => semanticSearch,
677
72
  sendEmail: () => sendEmail,
678
73
  setFunctionSecret: () => setFunctionSecret,
679
74
  startAgentSignup: () => startAgentSignup,
@@ -1201,9 +596,9 @@ const downloadAttachments = (options) => (options.client ?? client).get({
1201
596
  * derivation (Reply-To, then From, then bare sender), and the
1202
597
  * `Re:` subject prefix are all derived server-side from the
1203
598
  * stored inbound row. The request body carries only the message
1204
- * body and optional `wait` flag; passing any header or recipient
1205
- * override is rejected by the schema (`additionalProperties:
1206
- * false`).
599
+ * body, optional From override, optional attachments, and optional
600
+ * `wait` flag; passing any header or recipient override is
601
+ * rejected by the schema (`additionalProperties: false`).
1207
602
  *
1208
603
  * Forwards through the same gates as `/send-mail`: the response
1209
604
  * status, error envelope, and `idempotent_replay` flag mirror
@@ -1267,6 +662,37 @@ const discardEmailContent = (options) => (options.client ?? client).post({
1267
662
  ...options
1268
663
  });
1269
664
  /**
665
+ * Get the conversation an email belongs to
666
+ *
667
+ * Returns the full conversation the given inbound email belongs
668
+ * to, as ordered, ready-to-prompt turns WITH bodies. It resolves
669
+ * the thread from the email and returns every message oldest-first,
670
+ * so an agent that received an email can pass `messages` straight
671
+ * to a chat model in one call instead of walking `/threads/{id}`
672
+ * plus `/emails/{id}` and `/sent-emails/{id}` per message.
673
+ *
674
+ * Each message carries a `direction` (`inbound` | `outbound`) and a
675
+ * derived `role`: `inbound` -> `user`, `outbound` -> `assistant`
676
+ * (your own prior replies). The role mapping assumes the caller
677
+ * owns the outbound side, which is the agent-reply case this exists
678
+ * for. If the email has no thread yet (a brand-new message), the
679
+ * conversation is just that one message as a single user turn.
680
+ *
681
+ * The message list is capped; check `truncated` to detect when
682
+ * older messages were omitted. Consecutive same-role turns are not
683
+ * merged here; that normalization is model-specific and left to the
684
+ * caller.
685
+ *
686
+ */
687
+ const getConversation = (options) => (options.client ?? client).get({
688
+ security: [{
689
+ scheme: "bearer",
690
+ type: "http"
691
+ }],
692
+ url: "/emails/{id}/conversation",
693
+ ...options
694
+ });
695
+ /**
1270
696
  * List webhook endpoints
1271
697
  *
1272
698
  * Returns all active (non-deleted) webhook endpoints.
@@ -1531,6 +957,42 @@ const sendEmail = (options) => (options.client ?? client).post({
1531
957
  }
1532
958
  });
1533
959
  /**
960
+ * Semantic search across received and sent mail
961
+ *
962
+ * Ranked search across both received and sent mail. The `mode`
963
+ * field selects the ranking strategy:
964
+ *
965
+ * - `keyword`: lexical full-text matching only (no embeddings).
966
+ * - `semantic`: meaning-based matching using vector embeddings.
967
+ * - `hybrid` (default): blends the semantic and keyword signals.
968
+ *
969
+ * Results are ordered by a relevance `score`. Every row reports the
970
+ * fields it matched (`matched_fields`), a match-centered excerpt per
971
+ * field (`snippets`), and a `score_breakdown` whose components account
972
+ * for the `score`. Page through results by passing the prior
973
+ * response's `meta.cursor` back as `cursor`.
974
+ *
975
+ * Requires the Pro plan and the `semantic_search_enabled`
976
+ * entitlement; callers without them receive `403`.
977
+ *
978
+ * Host routing: this operation is served only by the search host
979
+ * (`https://api.primitive.dev/v1`). The typed SDKs route it there
980
+ * automatically.
981
+ *
982
+ */
983
+ const semanticSearch = (options) => (options.client ?? client).post({
984
+ security: [{
985
+ scheme: "bearer",
986
+ type: "http"
987
+ }],
988
+ url: "/semantic-search",
989
+ ...options,
990
+ headers: {
991
+ ...options.body !== void 0 && { "Content-Type": "application/json" },
992
+ ...options.headers
993
+ }
994
+ });
995
+ /**
1534
996
  * List outbound sent emails
1535
997
  *
1536
998
  * Returns a paginated list of OUTBOUND emails the caller's
@@ -1953,6 +1415,10 @@ const openapiDocument = {
1953
1415
  "name": "Emails",
1954
1416
  "description": "List, inspect, and manage received emails"
1955
1417
  },
1418
+ {
1419
+ "name": "Search",
1420
+ "description": "Semantic and hybrid search across received and sent mail"
1421
+ },
1956
1422
  {
1957
1423
  "name": "Sending",
1958
1424
  "description": "Send outbound emails through the Primitive API"
@@ -2941,7 +2407,14 @@ const openapiDocument = {
2941
2407
  "post": {
2942
2408
  "operationId": "replyToEmail",
2943
2409
  "summary": "Reply to an inbound email",
2944
- "description": "Sends an outbound reply to the inbound email identified by `id`.\nThreading headers (`In-Reply-To`, `References`), recipient\nderivation (Reply-To, then From, then bare sender), and the\n`Re:` subject prefix are all derived server-side from the\nstored inbound row. The request body carries only the message\nbody and optional `wait` flag; passing any header or recipient\noverride is rejected by the schema (`additionalProperties:\nfalse`).\n\nForwards through the same gates as `/send-mail`: the response\nstatus, error envelope, and `idempotent_replay` flag mirror\nthe send-mail contract verbatim.\n",
2410
+ "description": "Sends an outbound reply to the inbound email identified by `id`.\nThreading headers (`In-Reply-To`, `References`), recipient\nderivation (Reply-To, then From, then bare sender), and the\n`Re:` subject prefix are all derived server-side from the\nstored inbound row. The request body carries only the message\nbody, optional From override, optional attachments, and optional\n`wait` flag; passing any header or recipient override is\nrejected by the schema (`additionalProperties: false`).\n\nForwards through the same gates as `/send-mail`: the response\nstatus, error envelope, and `idempotent_replay` flag mirror\nthe send-mail contract verbatim.\n",
2411
+ "servers": [{
2412
+ "url": "https://api.primitive.dev/v1",
2413
+ "description": "Attachments-supporting send host (recommended)"
2414
+ }, {
2415
+ "url": "https://www.primitive.dev/api/v1",
2416
+ "description": "Primary host (attachment-free replies only)"
2417
+ }],
2945
2418
  "tags": ["Sending"],
2946
2419
  "requestBody": {
2947
2420
  "required": true,
@@ -3026,6 +2499,27 @@ const openapiDocument = {
3026
2499
  }
3027
2500
  }
3028
2501
  },
2502
+ "/emails/{id}/conversation": {
2503
+ "parameters": [{ "$ref": "#/components/parameters/ResourceId" }],
2504
+ "get": {
2505
+ "operationId": "getConversation",
2506
+ "summary": "Get the conversation an email belongs to",
2507
+ "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",
2508
+ "tags": ["Emails"],
2509
+ "responses": {
2510
+ "200": {
2511
+ "description": "Conversation",
2512
+ "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
2513
+ "type": "object",
2514
+ "properties": { "data": { "$ref": "#/components/schemas/Conversation" } }
2515
+ }] } } }
2516
+ },
2517
+ "400": { "$ref": "#/components/responses/ValidationError" },
2518
+ "401": { "$ref": "#/components/responses/Unauthorized" },
2519
+ "404": { "$ref": "#/components/responses/NotFound" }
2520
+ }
2521
+ }
2522
+ },
3029
2523
  "/endpoints": {
3030
2524
  "get": {
3031
2525
  "operationId": "listEndpoints",
@@ -3370,6 +2864,42 @@ const openapiDocument = {
3370
2864
  "503": { "$ref": "#/components/responses/ServiceUnavailable" }
3371
2865
  }
3372
2866
  } },
2867
+ "/semantic-search": { "post": {
2868
+ "operationId": "semanticSearch",
2869
+ "summary": "Semantic search across received and sent mail",
2870
+ "description": "Ranked search across both received and sent mail. The `mode`\nfield selects the ranking strategy:\n\n- `keyword`: lexical full-text matching only (no embeddings).\n- `semantic`: meaning-based matching using vector embeddings.\n- `hybrid` (default): blends the semantic and keyword signals.\n\nResults are ordered by a relevance `score`. Every row reports the\nfields it matched (`matched_fields`), a match-centered excerpt per\nfield (`snippets`), and a `score_breakdown` whose components account\nfor the `score`. Page through results by passing the prior\nresponse's `meta.cursor` back as `cursor`.\n\nRequires the Pro plan and the `semantic_search_enabled`\nentitlement; callers without them receive `403`.\n\nHost routing: this operation is served only by the search host\n(`https://api.primitive.dev/v1`). The typed SDKs route it there\nautomatically.\n",
2871
+ "servers": [{
2872
+ "url": "https://api.primitive.dev/v1",
2873
+ "description": "Search host"
2874
+ }],
2875
+ "tags": ["Search"],
2876
+ "requestBody": {
2877
+ "required": true,
2878
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SemanticSearchInput" } } }
2879
+ },
2880
+ "responses": {
2881
+ "200": {
2882
+ "description": "Ranked search results",
2883
+ "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
2884
+ "type": "object",
2885
+ "properties": {
2886
+ "data": {
2887
+ "type": "array",
2888
+ "items": { "$ref": "#/components/schemas/SemanticSearchResult" }
2889
+ },
2890
+ "meta": { "$ref": "#/components/schemas/SemanticSearchMeta" }
2891
+ },
2892
+ "required": ["data", "meta"]
2893
+ }] } } }
2894
+ },
2895
+ "400": { "$ref": "#/components/responses/ValidationError" },
2896
+ "401": { "$ref": "#/components/responses/Unauthorized" },
2897
+ "403": { "$ref": "#/components/responses/Forbidden" },
2898
+ "429": { "$ref": "#/components/responses/RateLimited" },
2899
+ "500": { "$ref": "#/components/responses/InternalError" },
2900
+ "503": { "$ref": "#/components/responses/ServiceUnavailable" }
2901
+ }
2902
+ } },
3373
2903
  "/sent-emails": { "get": {
3374
2904
  "operationId": "listSentEmails",
3375
2905
  "summary": "List outbound sent emails",
@@ -5864,46 +5394,118 @@ const openapiDocument = {
5864
5394
  },
5865
5395
  "required": ["direction", "id"]
5866
5396
  },
5867
- "SendMailAttachment": {
5397
+ "Conversation": {
5868
5398
  "type": "object",
5869
- "additionalProperties": false,
5399
+ "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",
5870
5400
  "properties": {
5871
- "filename": {
5872
- "type": "string",
5873
- "minLength": 1,
5874
- "maxLength": 255,
5875
- "description": "Attachment filename. Control characters are rejected."
5401
+ "thread_id": {
5402
+ "type": ["string", "null"],
5403
+ "format": "uuid",
5404
+ "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"
5876
5405
  },
5877
- "content_type": {
5878
- "type": "string",
5879
- "minLength": 1,
5880
- "maxLength": 255,
5881
- "description": "Optional MIME content type. Control characters are rejected."
5406
+ "subject": {
5407
+ "type": ["string", "null"],
5408
+ "description": "Normalized thread subject (Re/Fwd prefixes stripped), or the\nemail's own subject when it isn't threaded.\n"
5882
5409
  },
5883
- "content_base64": {
5884
- "type": "string",
5885
- "minLength": 1,
5886
- "maxLength": 44040192,
5887
- "description": "Base64-encoded attachment bytes."
5410
+ "message_count": {
5411
+ "type": "integer",
5412
+ "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"
5413
+ },
5414
+ "truncated": {
5415
+ "type": "boolean",
5416
+ "description": "True when `messages` omits part of the conversation because\nthe thread exceeds the per-call cap.\n"
5417
+ },
5418
+ "messages": {
5419
+ "type": "array",
5420
+ "items": { "$ref": "#/components/schemas/ConversationMessage" }
5888
5421
  }
5889
5422
  },
5890
- "required": ["filename", "content_base64"]
5423
+ "required": [
5424
+ "thread_id",
5425
+ "message_count",
5426
+ "truncated",
5427
+ "messages"
5428
+ ]
5891
5429
  },
5892
- "SendMailInput": {
5430
+ "ConversationMessage": {
5893
5431
  "type": "object",
5894
- "additionalProperties": false,
5432
+ "description": "One message in the conversation, with its body and a chat role.",
5895
5433
  "properties": {
5896
- "from": {
5434
+ "role": {
5897
5435
  "type": "string",
5898
- "minLength": 3,
5899
- "maxLength": 998,
5900
- "description": "RFC 5322 From header. The sender domain must be a verified outbound domain for your organization."
5436
+ "enum": ["user", "assistant"],
5437
+ "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"
5901
5438
  },
5902
- "to": {
5439
+ "direction": {
5903
5440
  "type": "string",
5904
- "minLength": 3,
5905
- "maxLength": 320,
5906
- "description": "Recipient address. Recipient eligibility depends on your account's outbound entitlements."
5441
+ "enum": ["inbound", "outbound"],
5442
+ "description": "`inbound` for a received email (`/emails/{id}`), `outbound`\nfor a send (`/sent-emails/{id}`).\n"
5443
+ },
5444
+ "id": {
5445
+ "type": "string",
5446
+ "format": "uuid"
5447
+ },
5448
+ "message_id": { "type": ["string", "null"] },
5449
+ "from": { "type": ["string", "null"] },
5450
+ "to": { "type": ["string", "null"] },
5451
+ "subject": { "type": ["string", "null"] },
5452
+ "text": {
5453
+ "type": "string",
5454
+ "description": "Plain-text body. Empty string when the message has no text\npart or its content was discarded by retention.\n"
5455
+ },
5456
+ "timestamp": {
5457
+ "type": ["string", "null"],
5458
+ "format": "date-time",
5459
+ "description": "received_at for inbound, created_at for outbound."
5460
+ }
5461
+ },
5462
+ "required": [
5463
+ "role",
5464
+ "direction",
5465
+ "id",
5466
+ "text"
5467
+ ]
5468
+ },
5469
+ "SendMailAttachment": {
5470
+ "type": "object",
5471
+ "additionalProperties": false,
5472
+ "properties": {
5473
+ "filename": {
5474
+ "type": "string",
5475
+ "minLength": 1,
5476
+ "maxLength": 255,
5477
+ "description": "Attachment filename. Control characters are rejected."
5478
+ },
5479
+ "content_type": {
5480
+ "type": "string",
5481
+ "minLength": 1,
5482
+ "maxLength": 255,
5483
+ "description": "Optional MIME content type. Control characters are rejected."
5484
+ },
5485
+ "content_base64": {
5486
+ "type": "string",
5487
+ "minLength": 1,
5488
+ "maxLength": 44040192,
5489
+ "description": "Base64-encoded attachment bytes."
5490
+ }
5491
+ },
5492
+ "required": ["filename", "content_base64"]
5493
+ },
5494
+ "SendMailInput": {
5495
+ "type": "object",
5496
+ "additionalProperties": false,
5497
+ "properties": {
5498
+ "from": {
5499
+ "type": "string",
5500
+ "minLength": 3,
5501
+ "maxLength": 998,
5502
+ "description": "RFC 5322 From header. The sender domain must be a verified outbound domain for your organization."
5503
+ },
5504
+ "to": {
5505
+ "type": "string",
5506
+ "minLength": 3,
5507
+ "maxLength": 320,
5508
+ "description": "Recipient address. Recipient eligibility depends on your account's outbound entitlements."
5907
5509
  },
5908
5510
  "subject": {
5909
5511
  "type": "string",
@@ -6142,6 +5744,236 @@ const openapiDocument = {
6142
5744
  "body_size_bytes"
6143
5745
  ]
6144
5746
  },
5747
+ "SemanticSearchField": {
5748
+ "type": "string",
5749
+ "enum": [
5750
+ "subject",
5751
+ "headers",
5752
+ "addresses",
5753
+ "body"
5754
+ ],
5755
+ "description": "A searchable email field."
5756
+ },
5757
+ "SemanticSearchInput": {
5758
+ "type": "object",
5759
+ "properties": {
5760
+ "query": {
5761
+ "type": "string",
5762
+ "minLength": 1,
5763
+ "maxLength": 2048,
5764
+ "description": "Free-text query. Required for `semantic` and `hybrid` modes;\noptional for `keyword` mode.\n"
5765
+ },
5766
+ "mode": {
5767
+ "type": "string",
5768
+ "enum": [
5769
+ "hybrid",
5770
+ "semantic",
5771
+ "keyword"
5772
+ ],
5773
+ "default": "hybrid",
5774
+ "description": "Ranking strategy. `keyword` is lexical only, `semantic` is\nembedding-based, `hybrid` blends both.\n"
5775
+ },
5776
+ "corpus": {
5777
+ "type": "array",
5778
+ "items": {
5779
+ "type": "string",
5780
+ "enum": ["inbound", "outbound"]
5781
+ },
5782
+ "minItems": 1,
5783
+ "maxItems": 2,
5784
+ "description": "Which mail to search. Defaults to both received (`inbound`)\nand sent (`outbound`).\n"
5785
+ },
5786
+ "search_in": {
5787
+ "type": "array",
5788
+ "items": { "$ref": "#/components/schemas/SemanticSearchField" },
5789
+ "description": "Restrict matching to these fields. Defaults to all."
5790
+ },
5791
+ "exclude": {
5792
+ "type": "array",
5793
+ "items": { "$ref": "#/components/schemas/SemanticSearchField" },
5794
+ "description": "Exclude these fields from matching."
5795
+ },
5796
+ "date_from": {
5797
+ "type": "string",
5798
+ "format": "date-time",
5799
+ "description": "Only include mail at or after this timestamp."
5800
+ },
5801
+ "date_to": {
5802
+ "type": "string",
5803
+ "format": "date-time",
5804
+ "description": "Only include mail at or before this timestamp."
5805
+ },
5806
+ "include": {
5807
+ "type": "array",
5808
+ "items": {
5809
+ "type": "string",
5810
+ "enum": ["coverage"]
5811
+ },
5812
+ "description": "Opt-in extras. `coverage` adds an index-coverage snapshot to\n`meta`. Matched fields, snippets, and the score breakdown are\nalways returned regardless of this field.\n"
5813
+ },
5814
+ "limit": {
5815
+ "type": "integer",
5816
+ "minimum": 1,
5817
+ "maximum": 100,
5818
+ "default": 10,
5819
+ "description": "Maximum number of results to return."
5820
+ },
5821
+ "cursor": {
5822
+ "type": "string",
5823
+ "description": "Opaque pagination cursor from a prior response's `meta.cursor`."
5824
+ }
5825
+ }
5826
+ },
5827
+ "SemanticSearchSnippet": {
5828
+ "type": "object",
5829
+ "properties": {
5830
+ "field": {
5831
+ "type": "string",
5832
+ "description": "The field this excerpt came from."
5833
+ },
5834
+ "text": {
5835
+ "type": "string",
5836
+ "description": "Plain-text excerpt centered on the match (no markup)."
5837
+ }
5838
+ },
5839
+ "required": ["field", "text"]
5840
+ },
5841
+ "SemanticSearchScoreBreakdown": {
5842
+ "type": "object",
5843
+ "description": "Additive contributions to `score`. `semantic` and `keyword` are the\nraw signals times the mode's weight (null when not applicable);\nthese plus `field_boost` and `recency` sum to `score` before each\nvalue is independently rounded to 5 decimal places.\n",
5844
+ "properties": {
5845
+ "semantic": { "type": ["number", "null"] },
5846
+ "keyword": { "type": ["number", "null"] },
5847
+ "field_boost": { "type": "number" },
5848
+ "recency": { "type": "number" }
5849
+ },
5850
+ "required": [
5851
+ "semantic",
5852
+ "keyword",
5853
+ "field_boost",
5854
+ "recency"
5855
+ ]
5856
+ },
5857
+ "SemanticSearchResult": {
5858
+ "type": "object",
5859
+ "properties": {
5860
+ "source_type": {
5861
+ "type": "string",
5862
+ "enum": ["inbound_email", "sent_email"],
5863
+ "description": "Whether this row is a received or sent message."
5864
+ },
5865
+ "id": {
5866
+ "type": "string",
5867
+ "description": "Message id. Combine with `api_url` to fetch the full record."
5868
+ },
5869
+ "subject": { "type": ["string", "null"] },
5870
+ "from": { "type": ["string", "null"] },
5871
+ "to": { "type": ["string", "null"] },
5872
+ "timestamp": {
5873
+ "type": "string",
5874
+ "description": "Message timestamp (received_at for inbound, created_at for sent)."
5875
+ },
5876
+ "status": {
5877
+ "type": "string",
5878
+ "description": "Lifecycle status of the message."
5879
+ },
5880
+ "score": {
5881
+ "type": "number",
5882
+ "description": "Overall relevance score; the `score_breakdown` components account for it."
5883
+ },
5884
+ "semantic_score": {
5885
+ "type": ["number", "null"],
5886
+ "description": "Raw semantic similarity signal, or null when not applicable."
5887
+ },
5888
+ "keyword_score": {
5889
+ "type": ["number", "null"],
5890
+ "description": "Raw keyword (lexical) signal, or null when not applicable."
5891
+ },
5892
+ "matched_fields": {
5893
+ "type": "array",
5894
+ "items": { "$ref": "#/components/schemas/SemanticSearchField" },
5895
+ "description": "Fields where the query matched."
5896
+ },
5897
+ "snippets": {
5898
+ "type": "array",
5899
+ "items": { "$ref": "#/components/schemas/SemanticSearchSnippet" },
5900
+ "description": "Match-centered excerpts, one per matched field."
5901
+ },
5902
+ "score_breakdown": { "$ref": "#/components/schemas/SemanticSearchScoreBreakdown" },
5903
+ "api_url": {
5904
+ "type": ["string", "null"],
5905
+ "description": "Relative API path to fetch the full message."
5906
+ }
5907
+ },
5908
+ "required": [
5909
+ "source_type",
5910
+ "id",
5911
+ "subject",
5912
+ "from",
5913
+ "to",
5914
+ "timestamp",
5915
+ "status",
5916
+ "score",
5917
+ "semantic_score",
5918
+ "keyword_score",
5919
+ "matched_fields",
5920
+ "snippets",
5921
+ "score_breakdown",
5922
+ "api_url"
5923
+ ]
5924
+ },
5925
+ "SemanticSearchCoverage": {
5926
+ "type": "object",
5927
+ "description": "Index-coverage snapshot for the org, returned only when the `coverage` include option is requested.",
5928
+ "properties": {
5929
+ "embedded_chunks": { "type": "integer" },
5930
+ "pending_chunks": { "type": "integer" },
5931
+ "skipped_plan_chunks": { "type": "integer" },
5932
+ "skipped_quota_chunks": { "type": "integer" },
5933
+ "unsupported_attachment_chunks": { "type": "integer" },
5934
+ "failed_chunks": { "type": "integer" }
5935
+ },
5936
+ "required": [
5937
+ "embedded_chunks",
5938
+ "pending_chunks",
5939
+ "skipped_plan_chunks",
5940
+ "skipped_quota_chunks",
5941
+ "unsupported_attachment_chunks",
5942
+ "failed_chunks"
5943
+ ]
5944
+ },
5945
+ "SemanticSearchMeta": {
5946
+ "type": "object",
5947
+ "properties": {
5948
+ "limit": {
5949
+ "type": "integer",
5950
+ "description": "Page size used for this request."
5951
+ },
5952
+ "cursor": {
5953
+ "type": ["string", "null"],
5954
+ "description": "Cursor for the next page, or null if there are no more results."
5955
+ },
5956
+ "mode": {
5957
+ "type": "string",
5958
+ "enum": [
5959
+ "hybrid",
5960
+ "semantic",
5961
+ "keyword"
5962
+ ],
5963
+ "description": "Ranking mode used for this response."
5964
+ },
5965
+ "coverage": {
5966
+ "oneOf": [{ "$ref": "#/components/schemas/SemanticSearchCoverage" }, { "type": "null" }],
5967
+ "description": "Index-coverage snapshot, present only when requested via\n`include: [coverage]`; otherwise null.\n"
5968
+ }
5969
+ },
5970
+ "required": [
5971
+ "limit",
5972
+ "cursor",
5973
+ "mode",
5974
+ "coverage"
5975
+ ]
5976
+ },
6145
5977
  "SentEmailDetail": {
6146
5978
  "description": "Full sent-email record, including `body_text` and\n`body_html`. Returned by /sent-emails/{id}.\n",
6147
5979
  "allOf": [{ "$ref": "#/components/schemas/SentEmailSummary" }, {
@@ -6180,6 +6012,12 @@ const openapiDocument = {
6180
6012
  "wait": {
6181
6013
  "type": "boolean",
6182
6014
  "description": "When true, wait for the first downstream SMTP delivery outcome before returning, mirroring the send-mail `wait` semantics."
6015
+ },
6016
+ "attachments": {
6017
+ "type": "array",
6018
+ "maxItems": 100,
6019
+ "description": "Inline attachments for this reply. Use https://api.primitive.dev/v1 for replies with attachments. Combined raw decoded attachment bytes must be at most 31457280.",
6020
+ "items": { "$ref": "#/components/schemas/SendMailAttachment" }
6183
6021
  }
6184
6022
  }
6185
6023
  },
@@ -9023,12 +8861,12 @@ const operationManifest = [
9023
8861
  {
9024
8862
  "binaryResponse": false,
9025
8863
  "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",
8864
+ "command": "get-conversation",
8865
+ "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
8866
  "hasJsonBody": false,
9029
8867
  "method": "GET",
9030
- "operationId": "getEmail",
9031
- "path": "/emails/{id}",
8868
+ "operationId": "getConversation",
8869
+ "path": "/emails/{id}/conversation",
9032
8870
  "pathParams": [{
9033
8871
  "description": "Resource UUID",
9034
8872
  "enum": null,
@@ -9040,71 +8878,165 @@ const operationManifest = [
9040
8878
  "requestSchema": null,
9041
8879
  "responseSchema": {
9042
8880
  "type": "object",
8881
+ "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
8882
  "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": {
9054
- "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"
9060
- },
9061
- "recipient": { "type": "string" },
9062
- "subject": { "type": ["string", "null"] },
9063
- "body_text": {
8883
+ "thread_id": {
9064
8884
  "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."
8885
+ "format": "uuid",
8886
+ "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"
9066
8887
  },
9067
- "body_html": {
8888
+ "subject": {
9068
8889
  "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."
9070
- },
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
- ]
9080
- },
9081
- "domain": { "type": "string" },
9082
- "spam_score": { "type": ["number", "null"] },
9083
- "raw_size_bytes": { "type": ["integer", "null"] },
9084
- "raw_sha256": { "type": ["string", "null"] },
9085
- "created_at": {
9086
- "type": "string",
9087
- "format": "date-time"
8890
+ "description": "Normalized thread subject (Re/Fwd prefixes stripped), or the\nemail's own subject when it isn't threaded.\n"
9088
8891
  },
9089
- "received_at": {
9090
- "type": "string",
9091
- "format": "date-time"
8892
+ "message_count": {
8893
+ "type": "integer",
8894
+ "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"
9092
8895
  },
9093
- "rejection_reason": { "type": ["string", "null"] },
9094
- "webhook_status": {
9095
- "type": ["string", "null"],
9096
- "description": "Webhook-delivery state for an inbound email. Tracks a\nSEPARATE lifecycle from the email's `status` field; the\nsame row carries both. Possible values:\n\n - `pending`: ingestion is past `pending` (the email itself\n is `accepted`) but the webhook fan-out has not yet\n started for this row.\n - `in_flight`: at least one delivery attempt is in flight.\n - `fired`: terminal success. Every active endpoint\n acknowledged the delivery (or accepted it after retries).\n - `failed`: terminal partial-failure. At least one endpoint\n exhausted its retry budget; some endpoints may still\n have succeeded.\n - `exhausted`: terminal failure. Every endpoint exhausted\n its retry budget without success.\n - `null`: no endpoints configured, so no webhook lifecycle\n applies.\n\nNote that the value `pending` here does NOT mean the email\nis `pending`; it means the email is past ingestion but\nwebhook delivery has not yet begun. Two overlapping uses\nof the word `pending` for distinct lifecycle phases.\n",
9097
- "enum": [
9098
- "pending",
9099
- "in_flight",
9100
- "fired",
9101
- "failed",
9102
- "exhausted",
9103
- null
9104
- ]
8896
+ "truncated": {
8897
+ "type": "boolean",
8898
+ "description": "True when `messages` omits part of the conversation because\nthe thread exceeds the per-call cap.\n"
9105
8899
  },
9106
- "webhook_attempt_count": { "type": "integer" },
9107
- "webhook_last_attempt_at": {
8900
+ "messages": {
8901
+ "type": "array",
8902
+ "items": {
8903
+ "type": "object",
8904
+ "description": "One message in the conversation, with its body and a chat role.",
8905
+ "properties": {
8906
+ "role": {
8907
+ "type": "string",
8908
+ "enum": ["user", "assistant"],
8909
+ "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"
8910
+ },
8911
+ "direction": {
8912
+ "type": "string",
8913
+ "enum": ["inbound", "outbound"],
8914
+ "description": "`inbound` for a received email (`/emails/{id}`), `outbound`\nfor a send (`/sent-emails/{id}`).\n"
8915
+ },
8916
+ "id": {
8917
+ "type": "string",
8918
+ "format": "uuid"
8919
+ },
8920
+ "message_id": { "type": ["string", "null"] },
8921
+ "from": { "type": ["string", "null"] },
8922
+ "to": { "type": ["string", "null"] },
8923
+ "subject": { "type": ["string", "null"] },
8924
+ "text": {
8925
+ "type": "string",
8926
+ "description": "Plain-text body. Empty string when the message has no text\npart or its content was discarded by retention.\n"
8927
+ },
8928
+ "timestamp": {
8929
+ "type": ["string", "null"],
8930
+ "format": "date-time",
8931
+ "description": "received_at for inbound, created_at for outbound."
8932
+ }
8933
+ },
8934
+ "required": [
8935
+ "role",
8936
+ "direction",
8937
+ "id",
8938
+ "text"
8939
+ ]
8940
+ }
8941
+ }
8942
+ },
8943
+ "required": [
8944
+ "thread_id",
8945
+ "message_count",
8946
+ "truncated",
8947
+ "messages"
8948
+ ]
8949
+ },
8950
+ "sdkName": "getConversation",
8951
+ "summary": "Get the conversation an email belongs to",
8952
+ "tag": "Emails",
8953
+ "tagCommand": "emails"
8954
+ },
8955
+ {
8956
+ "binaryResponse": false,
8957
+ "bodyRequired": false,
8958
+ "command": "get-email",
8959
+ "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",
8960
+ "hasJsonBody": false,
8961
+ "method": "GET",
8962
+ "operationId": "getEmail",
8963
+ "path": "/emails/{id}",
8964
+ "pathParams": [{
8965
+ "description": "Resource UUID",
8966
+ "enum": null,
8967
+ "name": "id",
8968
+ "required": true,
8969
+ "type": "string"
8970
+ }],
8971
+ "queryParams": [],
8972
+ "requestSchema": null,
8973
+ "responseSchema": {
8974
+ "type": "object",
8975
+ "properties": {
8976
+ "id": {
8977
+ "type": "string",
8978
+ "format": "uuid"
8979
+ },
8980
+ "message_id": { "type": ["string", "null"] },
8981
+ "domain_id": {
8982
+ "type": ["string", "null"],
8983
+ "format": "uuid"
8984
+ },
8985
+ "org_id": {
8986
+ "type": ["string", "null"],
8987
+ "format": "uuid"
8988
+ },
8989
+ "sender": {
8990
+ "type": "string",
8991
+ "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"
8992
+ },
8993
+ "recipient": { "type": "string" },
8994
+ "subject": { "type": ["string", "null"] },
8995
+ "body_text": {
8996
+ "type": ["string", "null"],
8997
+ "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."
8998
+ },
8999
+ "body_html": {
9000
+ "type": ["string", "null"],
9001
+ "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."
9002
+ },
9003
+ "status": {
9004
+ "type": "string",
9005
+ "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",
9006
+ "enum": [
9007
+ "pending",
9008
+ "accepted",
9009
+ "completed",
9010
+ "rejected"
9011
+ ]
9012
+ },
9013
+ "domain": { "type": "string" },
9014
+ "spam_score": { "type": ["number", "null"] },
9015
+ "raw_size_bytes": { "type": ["integer", "null"] },
9016
+ "raw_sha256": { "type": ["string", "null"] },
9017
+ "created_at": {
9018
+ "type": "string",
9019
+ "format": "date-time"
9020
+ },
9021
+ "received_at": {
9022
+ "type": "string",
9023
+ "format": "date-time"
9024
+ },
9025
+ "rejection_reason": { "type": ["string", "null"] },
9026
+ "webhook_status": {
9027
+ "type": ["string", "null"],
9028
+ "description": "Webhook-delivery state for an inbound email. Tracks a\nSEPARATE lifecycle from the email's `status` field; the\nsame row carries both. Possible values:\n\n - `pending`: ingestion is past `pending` (the email itself\n is `accepted`) but the webhook fan-out has not yet\n started for this row.\n - `in_flight`: at least one delivery attempt is in flight.\n - `fired`: terminal success. Every active endpoint\n acknowledged the delivery (or accepted it after retries).\n - `failed`: terminal partial-failure. At least one endpoint\n exhausted its retry budget; some endpoints may still\n have succeeded.\n - `exhausted`: terminal failure. Every endpoint exhausted\n its retry budget without success.\n - `null`: no endpoints configured, so no webhook lifecycle\n applies.\n\nNote that the value `pending` here does NOT mean the email\nis `pending`; it means the email is past ingestion but\nwebhook delivery has not yet begun. Two overlapping uses\nof the word `pending` for distinct lifecycle phases.\n",
9029
+ "enum": [
9030
+ "pending",
9031
+ "in_flight",
9032
+ "fired",
9033
+ "failed",
9034
+ "exhausted",
9035
+ null
9036
+ ]
9037
+ },
9038
+ "webhook_attempt_count": { "type": "integer" },
9039
+ "webhook_last_attempt_at": {
9108
9040
  "type": ["string", "null"],
9109
9041
  "format": "date-time"
9110
9042
  },
@@ -11844,6 +11776,218 @@ const operationManifest = [
11844
11776
  "tag": "Inbox",
11845
11777
  "tagCommand": "inbox"
11846
11778
  },
11779
+ {
11780
+ "binaryResponse": false,
11781
+ "bodyRequired": true,
11782
+ "command": "semantic-search",
11783
+ "description": "Ranked search across both received and sent mail. The `mode`\nfield selects the ranking strategy:\n\n- `keyword`: lexical full-text matching only (no embeddings).\n- `semantic`: meaning-based matching using vector embeddings.\n- `hybrid` (default): blends the semantic and keyword signals.\n\nResults are ordered by a relevance `score`. Every row reports the\nfields it matched (`matched_fields`), a match-centered excerpt per\nfield (`snippets`), and a `score_breakdown` whose components account\nfor the `score`. Page through results by passing the prior\nresponse's `meta.cursor` back as `cursor`.\n\nRequires the Pro plan and the `semantic_search_enabled`\nentitlement; callers without them receive `403`.\n\nHost routing: this operation is served only by the search host\n(`https://api.primitive.dev/v1`). The typed SDKs route it there\nautomatically.\n",
11784
+ "hasJsonBody": true,
11785
+ "method": "POST",
11786
+ "operationId": "semanticSearch",
11787
+ "path": "/semantic-search",
11788
+ "pathParams": [],
11789
+ "queryParams": [],
11790
+ "requestSchema": {
11791
+ "type": "object",
11792
+ "properties": {
11793
+ "query": {
11794
+ "type": "string",
11795
+ "minLength": 1,
11796
+ "maxLength": 2048,
11797
+ "description": "Free-text query. Required for `semantic` and `hybrid` modes;\noptional for `keyword` mode.\n"
11798
+ },
11799
+ "mode": {
11800
+ "type": "string",
11801
+ "enum": [
11802
+ "hybrid",
11803
+ "semantic",
11804
+ "keyword"
11805
+ ],
11806
+ "default": "hybrid",
11807
+ "description": "Ranking strategy. `keyword` is lexical only, `semantic` is\nembedding-based, `hybrid` blends both.\n"
11808
+ },
11809
+ "corpus": {
11810
+ "type": "array",
11811
+ "items": {
11812
+ "type": "string",
11813
+ "enum": ["inbound", "outbound"]
11814
+ },
11815
+ "minItems": 1,
11816
+ "maxItems": 2,
11817
+ "description": "Which mail to search. Defaults to both received (`inbound`)\nand sent (`outbound`).\n"
11818
+ },
11819
+ "search_in": {
11820
+ "type": "array",
11821
+ "items": {
11822
+ "type": "string",
11823
+ "enum": [
11824
+ "subject",
11825
+ "headers",
11826
+ "addresses",
11827
+ "body"
11828
+ ],
11829
+ "description": "A searchable email field."
11830
+ },
11831
+ "description": "Restrict matching to these fields. Defaults to all."
11832
+ },
11833
+ "exclude": {
11834
+ "type": "array",
11835
+ "items": {
11836
+ "type": "string",
11837
+ "enum": [
11838
+ "subject",
11839
+ "headers",
11840
+ "addresses",
11841
+ "body"
11842
+ ],
11843
+ "description": "A searchable email field."
11844
+ },
11845
+ "description": "Exclude these fields from matching."
11846
+ },
11847
+ "date_from": {
11848
+ "type": "string",
11849
+ "format": "date-time",
11850
+ "description": "Only include mail at or after this timestamp."
11851
+ },
11852
+ "date_to": {
11853
+ "type": "string",
11854
+ "format": "date-time",
11855
+ "description": "Only include mail at or before this timestamp."
11856
+ },
11857
+ "include": {
11858
+ "type": "array",
11859
+ "items": {
11860
+ "type": "string",
11861
+ "enum": ["coverage"]
11862
+ },
11863
+ "description": "Opt-in extras. `coverage` adds an index-coverage snapshot to\n`meta`. Matched fields, snippets, and the score breakdown are\nalways returned regardless of this field.\n"
11864
+ },
11865
+ "limit": {
11866
+ "type": "integer",
11867
+ "minimum": 1,
11868
+ "maximum": 100,
11869
+ "default": 10,
11870
+ "description": "Maximum number of results to return."
11871
+ },
11872
+ "cursor": {
11873
+ "type": "string",
11874
+ "description": "Opaque pagination cursor from a prior response's `meta.cursor`."
11875
+ }
11876
+ }
11877
+ },
11878
+ "responseSchema": {
11879
+ "type": "array",
11880
+ "items": {
11881
+ "type": "object",
11882
+ "properties": {
11883
+ "source_type": {
11884
+ "type": "string",
11885
+ "enum": ["inbound_email", "sent_email"],
11886
+ "description": "Whether this row is a received or sent message."
11887
+ },
11888
+ "id": {
11889
+ "type": "string",
11890
+ "description": "Message id. Combine with `api_url` to fetch the full record."
11891
+ },
11892
+ "subject": { "type": ["string", "null"] },
11893
+ "from": { "type": ["string", "null"] },
11894
+ "to": { "type": ["string", "null"] },
11895
+ "timestamp": {
11896
+ "type": "string",
11897
+ "description": "Message timestamp (received_at for inbound, created_at for sent)."
11898
+ },
11899
+ "status": {
11900
+ "type": "string",
11901
+ "description": "Lifecycle status of the message."
11902
+ },
11903
+ "score": {
11904
+ "type": "number",
11905
+ "description": "Overall relevance score; the `score_breakdown` components account for it."
11906
+ },
11907
+ "semantic_score": {
11908
+ "type": ["number", "null"],
11909
+ "description": "Raw semantic similarity signal, or null when not applicable."
11910
+ },
11911
+ "keyword_score": {
11912
+ "type": ["number", "null"],
11913
+ "description": "Raw keyword (lexical) signal, or null when not applicable."
11914
+ },
11915
+ "matched_fields": {
11916
+ "type": "array",
11917
+ "items": {
11918
+ "type": "string",
11919
+ "enum": [
11920
+ "subject",
11921
+ "headers",
11922
+ "addresses",
11923
+ "body"
11924
+ ],
11925
+ "description": "A searchable email field."
11926
+ },
11927
+ "description": "Fields where the query matched."
11928
+ },
11929
+ "snippets": {
11930
+ "type": "array",
11931
+ "items": {
11932
+ "type": "object",
11933
+ "properties": {
11934
+ "field": {
11935
+ "type": "string",
11936
+ "description": "The field this excerpt came from."
11937
+ },
11938
+ "text": {
11939
+ "type": "string",
11940
+ "description": "Plain-text excerpt centered on the match (no markup)."
11941
+ }
11942
+ },
11943
+ "required": ["field", "text"]
11944
+ },
11945
+ "description": "Match-centered excerpts, one per matched field."
11946
+ },
11947
+ "score_breakdown": {
11948
+ "type": "object",
11949
+ "description": "Additive contributions to `score`. `semantic` and `keyword` are the\nraw signals times the mode's weight (null when not applicable);\nthese plus `field_boost` and `recency` sum to `score` before each\nvalue is independently rounded to 5 decimal places.\n",
11950
+ "properties": {
11951
+ "semantic": { "type": ["number", "null"] },
11952
+ "keyword": { "type": ["number", "null"] },
11953
+ "field_boost": { "type": "number" },
11954
+ "recency": { "type": "number" }
11955
+ },
11956
+ "required": [
11957
+ "semantic",
11958
+ "keyword",
11959
+ "field_boost",
11960
+ "recency"
11961
+ ]
11962
+ },
11963
+ "api_url": {
11964
+ "type": ["string", "null"],
11965
+ "description": "Relative API path to fetch the full message."
11966
+ }
11967
+ },
11968
+ "required": [
11969
+ "source_type",
11970
+ "id",
11971
+ "subject",
11972
+ "from",
11973
+ "to",
11974
+ "timestamp",
11975
+ "status",
11976
+ "score",
11977
+ "semantic_score",
11978
+ "keyword_score",
11979
+ "matched_fields",
11980
+ "snippets",
11981
+ "score_breakdown",
11982
+ "api_url"
11983
+ ]
11984
+ }
11985
+ },
11986
+ "sdkName": "semanticSearch",
11987
+ "summary": "Semantic search across received and sent mail",
11988
+ "tag": "Search",
11989
+ "tagCommand": "search"
11990
+ },
11847
11991
  {
11848
11992
  "binaryResponse": false,
11849
11993
  "bodyRequired": false,
@@ -12496,7 +12640,7 @@ const operationManifest = [
12496
12640
  "binaryResponse": false,
12497
12641
  "bodyRequired": true,
12498
12642
  "command": "reply-to-email",
12499
- "description": "Sends an outbound reply to the inbound email identified by `id`.\nThreading headers (`In-Reply-To`, `References`), recipient\nderivation (Reply-To, then From, then bare sender), and the\n`Re:` subject prefix are all derived server-side from the\nstored inbound row. The request body carries only the message\nbody and optional `wait` flag; passing any header or recipient\noverride is rejected by the schema (`additionalProperties:\nfalse`).\n\nForwards through the same gates as `/send-mail`: the response\nstatus, error envelope, and `idempotent_replay` flag mirror\nthe send-mail contract verbatim.\n",
12643
+ "description": "Sends an outbound reply to the inbound email identified by `id`.\nThreading headers (`In-Reply-To`, `References`), recipient\nderivation (Reply-To, then From, then bare sender), and the\n`Re:` subject prefix are all derived server-side from the\nstored inbound row. The request body carries only the message\nbody, optional From override, optional attachments, and optional\n`wait` flag; passing any header or recipient override is\nrejected by the schema (`additionalProperties: false`).\n\nForwards through the same gates as `/send-mail`: the response\nstatus, error envelope, and `idempotent_replay` flag mirror\nthe send-mail contract verbatim.\n",
12500
12644
  "hasJsonBody": true,
12501
12645
  "method": "POST",
12502
12646
  "operationId": "replyToEmail",
@@ -12531,6 +12675,36 @@ const operationManifest = [
12531
12675
  "wait": {
12532
12676
  "type": "boolean",
12533
12677
  "description": "When true, wait for the first downstream SMTP delivery outcome before returning, mirroring the send-mail `wait` semantics."
12678
+ },
12679
+ "attachments": {
12680
+ "type": "array",
12681
+ "maxItems": 100,
12682
+ "description": "Inline attachments for this reply. Use https://api.primitive.dev/v1 for replies with attachments. Combined raw decoded attachment bytes must be at most 31457280.",
12683
+ "items": {
12684
+ "type": "object",
12685
+ "additionalProperties": false,
12686
+ "properties": {
12687
+ "filename": {
12688
+ "type": "string",
12689
+ "minLength": 1,
12690
+ "maxLength": 255,
12691
+ "description": "Attachment filename. Control characters are rejected."
12692
+ },
12693
+ "content_type": {
12694
+ "type": "string",
12695
+ "minLength": 1,
12696
+ "maxLength": 255,
12697
+ "description": "Optional MIME content type. Control characters are rejected."
12698
+ },
12699
+ "content_base64": {
12700
+ "type": "string",
12701
+ "minLength": 1,
12702
+ "maxLength": 44040192,
12703
+ "description": "Base64-encoded attachment bytes."
12704
+ }
12705
+ },
12706
+ "required": ["filename", "content_base64"]
12707
+ }
12534
12708
  }
12535
12709
  }
12536
12710
  },
@@ -13093,532 +13267,6 @@ const operationManifest = [
13093
13267
  }
13094
13268
  ];
13095
13269
  //#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
13270
  //#region src/oclif/api-client.ts
13623
13271
  const API_HEADERS_ENV = "PRIMITIVE_API_HEADERS";
13624
13272
  const OAUTH_REFRESH_SKEW_MS = 60 * 1e3;
@@ -14163,7 +13811,7 @@ async function runWithTiming(enabled, fn) {
14163
13811
  const TIME_FLAG_DESCRIPTION = "Print the wall-clock duration of this command to stderr after it completes (e.g. `[time: 1.34s]`). Useful for measuring `--wait` send latency, comparing CLI overhead, or capturing timing in scripts.";
14164
13812
  const API_BASE_URL_1_FLAG_DESCRIPTION = "Override the primary API base URL. Internal testing only; not documented to customers.";
14165
13813
  const API_BASE_URL_2_FLAG_DESCRIPTION = "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.";
14166
- const HOST_2_OPERATIONS = new Set(["sendEmail"]);
13814
+ const HOST_2_OPERATIONS = new Set(["sendEmail", "replyToEmail"]);
14167
13815
  const RESERVED_FLAG_NAMES = new Set([
14168
13816
  "api-key",
14169
13817
  "api-base-url-1",
@@ -14394,6 +14042,39 @@ function canonicalizeCliReferences(description) {
14394
14042
  return description.replaceAll("`primitive emails:latest`", "`primitive emails latest`").replaceAll("`primitive describe emails:get-email | jq '.responseSchema.properties'`", "`primitive describe emails:get | jq '.responseSchema.properties'`");
14395
14043
  }
14396
14044
  //#endregion
14045
+ //#region src/oclif/attachments.ts
14046
+ function readAttachmentBytes(path, readFile) {
14047
+ try {
14048
+ return Buffer.from(readFile(path));
14049
+ } catch (error) {
14050
+ const detail = error instanceof Error ? error.message : String(error);
14051
+ throw new Errors.CLIError(`Could not read --attachment ${path}: ${detail}`, { exit: 1 });
14052
+ }
14053
+ }
14054
+ function hasControlCharacter(value) {
14055
+ return Array.from(value).some((character) => {
14056
+ const code = character.charCodeAt(0);
14057
+ return code <= 31 || code >= 127 && code <= 159;
14058
+ });
14059
+ }
14060
+ function validateAttachmentFilename(path, filename) {
14061
+ if (!filename) throw new Errors.CLIError(`Could not derive an attachment filename from ${path}. Pass a file path.`, { exit: 1 });
14062
+ if (hasControlCharacter(filename)) throw new Errors.CLIError(`Attachment filename ${filename} contains control characters.`, { exit: 1 });
14063
+ }
14064
+ function readAttachmentFiles(paths, readFile = readFileSync) {
14065
+ if (!paths || paths.length === 0) return void 0;
14066
+ return paths.map((path) => {
14067
+ const filename = basename(path);
14068
+ validateAttachmentFilename(path, filename);
14069
+ const bytes = readAttachmentBytes(path, readFile);
14070
+ if (bytes.length === 0) throw new Errors.CLIError(`Attachment file ${path} is empty. Attachments must contain at least one byte.`, { exit: 1 });
14071
+ return {
14072
+ content_base64: bytes.toString("base64"),
14073
+ filename
14074
+ };
14075
+ });
14076
+ }
14077
+ //#endregion
14397
14078
  //#region src/oclif/outbound-defaults.ts
14398
14079
  const SUBJECT_MAX_LENGTH = 200;
14399
14080
  function deriveSubject(body) {
@@ -14535,19 +14216,39 @@ async function fetchEmailSearchPage(params) {
14535
14216
  function sleep$1(ms) {
14536
14217
  return new Promise((resolve) => setTimeout(resolve, ms));
14537
14218
  }
14538
- //#endregion
14539
- //#region src/oclif/commands/chat.ts
14540
- const DEFAULT_CHAT_TIMEOUT_SECONDS = 120;
14541
- const DEFAULT_STRICT_PHASE_SECONDS = 60;
14542
14219
  function cliError$6(message) {
14543
14220
  return new Errors.CLIError(message, { exit: 1 });
14544
14221
  }
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.");
14222
+ async function readStdinToString(missingMessage = "No message provided. Pass the message as the second positional argument or pipe it via stdin.") {
14223
+ if (process.stdin.isTTY) throw cliError$6(missingMessage);
14547
14224
  const chunks = [];
14548
14225
  for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
14549
14226
  return Buffer.concat(chunks).toString("utf8");
14550
14227
  }
14228
+ function chatColor(color, text) {
14229
+ return ux.colorize(color, text);
14230
+ }
14231
+ function chatCommandText(command) {
14232
+ return chatColor("cyan", command);
14233
+ }
14234
+ function chatDetailLine(line) {
14235
+ return chatColor("dim", line);
14236
+ }
14237
+ function chatFailureText(message) {
14238
+ return chatColor("red", message);
14239
+ }
14240
+ function chatHeading(text) {
14241
+ return chatColor("bold", text);
14242
+ }
14243
+ function chatNoticeText(message) {
14244
+ return chatColor("yellow", message);
14245
+ }
14246
+ function chatProgressText(message) {
14247
+ return chatColor("cyan", message);
14248
+ }
14249
+ function chatSuccessText(message) {
14250
+ return chatColor("greenBright", message);
14251
+ }
14551
14252
  var ChatProgressIndicator = class {
14552
14253
  currentMessage = null;
14553
14254
  frameIndex = 0;
@@ -14568,7 +14269,7 @@ var ChatProgressIndicator = class {
14568
14269
  this.timer.unref?.();
14569
14270
  return;
14570
14271
  }
14571
- this.stream.write(`${message}\n`);
14272
+ this.stream.write(`${chatProgressText(message)}\n`);
14572
14273
  }
14573
14274
  update(message, options = {}) {
14574
14275
  this.currentMessage = message;
@@ -14581,10 +14282,10 @@ var ChatProgressIndicator = class {
14581
14282
  return;
14582
14283
  }
14583
14284
  this.stopTimer();
14584
- this.stream.write(`${message}\n`);
14285
+ this.stream.write(`${chatProgressText(message)}\n`);
14585
14286
  if (options.heartbeatMs !== void 0) {
14586
14287
  this.timer = setInterval(() => {
14587
- this.stream.write(`${formatWaitingHeartbeat(message, this.now() - this.startedAt, options.timeoutSeconds)}\n`);
14288
+ this.stream.write(`${chatProgressText(formatWaitingHeartbeat(message, this.now() - this.startedAt, options.timeoutSeconds))}\n`);
14588
14289
  }, options.heartbeatMs);
14589
14290
  this.timer.unref?.();
14590
14291
  }
@@ -14593,17 +14294,17 @@ var ChatProgressIndicator = class {
14593
14294
  if (this.stream.isTTY) {
14594
14295
  const currentMessage = this.currentMessage;
14595
14296
  this.clearLine();
14596
- this.stream.write(`${message}\n`);
14297
+ this.stream.write(`${chatNoticeText(message)}\n`);
14597
14298
  if (currentMessage !== null && this.timer !== null) this.render(currentMessage);
14598
14299
  return;
14599
14300
  }
14600
- this.stream.write(`${message}\n`);
14301
+ this.stream.write(`${chatNoticeText(message)}\n`);
14601
14302
  }
14602
14303
  succeed(message) {
14603
- this.finish(`${message} after ${formatElapsed(this.now() - this.startedAt)}.`);
14304
+ this.finish(chatSuccessText(`${message} after ${formatElapsed(this.now() - this.startedAt)}.`));
14604
14305
  }
14605
14306
  fail(message) {
14606
- this.finish(message);
14307
+ this.finish(chatFailureText(message));
14607
14308
  }
14608
14309
  finish(message) {
14609
14310
  this.stopTimer();
@@ -14620,9 +14321,10 @@ var ChatProgressIndicator = class {
14620
14321
  ];
14621
14322
  const frame = frames[this.frameIndex % frames.length];
14622
14323
  this.frameIndex += 1;
14623
- const line = `${frame} ${message} (${formatElapsed(this.now() - this.startedAt)})`;
14624
- this.lastLineLength = Math.max(this.lastLineLength, line.length);
14625
- this.stream.write(`\r${line}`);
14324
+ const elapsed = `(${formatElapsed(this.now() - this.startedAt)})`;
14325
+ const plainLine = `${frame} ${message} ${elapsed}`;
14326
+ this.lastLineLength = Math.max(this.lastLineLength, plainLine.length);
14327
+ this.stream.write(`\r${chatProgressText(`${frame} ${message}`)} ${chatColor("dim", elapsed)}`);
14626
14328
  }
14627
14329
  clearLine() {
14628
14330
  if (this.lastLineLength > 0) {
@@ -14655,6 +14357,11 @@ function shellQuote(value) {
14655
14357
  function commandFromArgv(argv) {
14656
14358
  return argv.map(shellQuote).join(" ");
14657
14359
  }
14360
+ function parseLocalChatIdArg(value) {
14361
+ if (value === void 0 || !/^(0|[1-9]\d*)$/.test(value)) return null;
14362
+ const parsed = Number(value);
14363
+ return Number.isSafeInteger(parsed) ? parsed : null;
14364
+ }
14658
14365
  function resolveChatResponseBody(reply) {
14659
14366
  if (reply.body_text && reply.body_text.length > 0) return {
14660
14367
  body: reply.body_text,
@@ -14709,10 +14416,35 @@ function buildCommand(kind, description, argv, options = {}) {
14709
14416
  requires_message: requiresMessage
14710
14417
  };
14711
14418
  }
14419
+ function shouldPreferStrictContinuation(context) {
14420
+ const hasCustomStrictPhase = context.strictPhaseSeconds !== 60;
14421
+ return context.strictOnly || context.matchStrategy === "strict" && !hasCustomStrictPhase;
14422
+ }
14712
14423
  function buildChatFollowUpCommands(context) {
14713
14424
  const commands = [];
14714
- const hasCustomStrictPhase = context.strictPhaseSeconds !== DEFAULT_STRICT_PHASE_SECONDS;
14715
- const shouldPreferStrictContinuation = context.strictOnly || context.matchStrategy === "strict" && !hasCustomStrictPhase;
14425
+ const hasCustomStrictPhase = context.strictPhaseSeconds !== 60;
14426
+ const preferStrictContinuation = shouldPreferStrictContinuation(context);
14427
+ if (context.localChatId !== void 0) {
14428
+ const localContinueParts = [
14429
+ "primitive",
14430
+ "chat",
14431
+ "reply",
14432
+ String(context.localChatId),
14433
+ "<message>"
14434
+ ];
14435
+ if (context.json) localContinueParts.push("--json");
14436
+ if (context.quiet) localContinueParts.push("--quiet");
14437
+ commands.push(buildCommand("continue_chat", "Continue this chat", localContinueParts, { requiresMessage: true }));
14438
+ const activeContinueParts = [
14439
+ "primitive",
14440
+ "chat",
14441
+ "reply",
14442
+ "<message>"
14443
+ ];
14444
+ if (context.json) activeContinueParts.push("--json");
14445
+ if (context.quiet) activeContinueParts.push("--quiet");
14446
+ commands.push(buildCommand("continue_active_chat", "Continue the active chat", activeContinueParts, { requiresMessage: true }));
14447
+ }
14716
14448
  const continueParts = [
14717
14449
  "primitive",
14718
14450
  "chat",
@@ -14728,9 +14460,9 @@ function buildChatFollowUpCommands(context) {
14728
14460
  ];
14729
14461
  if (context.json) continueParts.push("--json");
14730
14462
  if (context.quiet) continueParts.push("--quiet");
14731
- if (shouldPreferStrictContinuation) continueParts.push("--strict-only");
14463
+ if (preferStrictContinuation) continueParts.push("--strict-only");
14732
14464
  else if (hasCustomStrictPhase) continueParts.push("--strict-phase-seconds", String(context.strictPhaseSeconds));
14733
- commands.push(buildCommand("continue_chat", "Continue this chat", continueParts, { requiresMessage: true }));
14465
+ 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
14466
  commands.push(buildCommand("reply_direct", "Reply directly to the inbound email", [
14735
14467
  "primitive",
14736
14468
  "reply",
@@ -14804,6 +14536,7 @@ function buildChatJsonEnvelope(context) {
14804
14536
  return {
14805
14537
  sent: context.sent,
14806
14538
  reply: context.reply,
14539
+ local_chat_id: context.localChatId ?? null,
14807
14540
  response_body: responseBody.body,
14808
14541
  response_body_format: responseBody.format,
14809
14542
  match: {
@@ -14814,48 +14547,69 @@ function buildChatJsonEnvelope(context) {
14814
14547
  follow_up_commands: buildChatFollowUpCommands(context)
14815
14548
  };
14816
14549
  }
14550
+ function persistActiveChat(params) {
14551
+ try {
14552
+ return saveActiveChatState(params.configDir, {
14553
+ from: params.context.from,
14554
+ last_reply_email_id: params.context.reply.id,
14555
+ last_reply_received_at: params.context.reply.received_at,
14556
+ last_sent_email_id: params.context.sent.id,
14557
+ recipient: params.context.recipient,
14558
+ strict_only: shouldPreferStrictContinuation(params.context),
14559
+ strict_phase_seconds: params.context.strictPhaseSeconds,
14560
+ thread_id: params.context.reply.thread_id ?? null,
14561
+ timeout_seconds: params.context.timeoutSeconds
14562
+ }, { preferredLocalId: params.preferredLocalId }).local_id;
14563
+ } catch (error) {
14564
+ const detail = error instanceof Error ? error.message : String(error);
14565
+ params.writeWarning?.(`Warning: could not save local chat state: ${detail}\n`);
14566
+ return null;
14567
+ }
14568
+ }
14817
14569
  function formatChatResponse(context) {
14818
14570
  const accepted = context.sent.accepted.join(", ") || context.recipient;
14819
14571
  const responseBody = resolveChatResponseBody(context.reply);
14820
14572
  const lines = [
14821
- "Reply received",
14573
+ chatSuccessText("Reply received"),
14822
14574
  "",
14823
- "Sent",
14824
- ` To: ${accepted}`,
14825
- ` From: ${context.sent.from || context.from}`,
14826
- ` Subject: ${context.subject}`,
14827
- ` Sent email id: ${context.sent.id}`,
14828
- ` Delivery status: ${context.sent.delivery_status ?? context.sent.status}`,
14575
+ chatHeading("Sent"),
14576
+ chatDetailLine(` To: ${accepted}`),
14577
+ chatDetailLine(` From: ${context.sent.from || context.from}`),
14578
+ chatDetailLine(` Subject: ${context.subject}`),
14579
+ chatDetailLine(` Sent email id: ${context.sent.id}`),
14580
+ chatColor("green", ` Delivery status: ${context.sent.delivery_status ?? context.sent.status}`),
14829
14581
  "",
14830
- "Reply",
14831
- ` Email id: ${context.reply.id}`,
14832
- ` From: ${context.reply.from_email}`,
14833
- ` To: ${context.reply.to_email}`,
14834
- ` Subject: ${context.reply.subject ?? "(no subject)"}`,
14835
- ` Received: ${context.reply.received_at}`,
14836
- ` Match: ${matchDescription(context.matchStrategy)}`
14582
+ chatHeading("Reply"),
14583
+ chatDetailLine(` Email id: ${context.reply.id}`),
14584
+ chatDetailLine(` From: ${context.reply.from_email}`),
14585
+ chatDetailLine(` To: ${context.reply.to_email}`),
14586
+ chatDetailLine(` Subject: ${context.reply.subject ?? "(no subject)"}`),
14587
+ chatDetailLine(` Received: ${context.reply.received_at}`),
14588
+ chatDetailLine(` Match: ${matchDescription(context.matchStrategy)}`)
14837
14589
  ];
14838
- if (context.reply.reply_to_sent_email_id) lines.push(` Reply to sent email id: ${context.reply.reply_to_sent_email_id}`);
14839
- if (context.reply.message_id) lines.push(` Message-Id: ${context.reply.message_id}`);
14840
- 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
- for (const { description, command } of buildChatFollowUpCommands(context)) lines.push(` ${description}:`, ` ${command}`);
14842
- lines.push("", `Response body (${responseBody.format}; use --json for parsing)`, "----- BEGIN RESPONSE -----", responseBody.body || "(empty response)", "----- END RESPONSE -----");
14590
+ if (context.reply.reply_to_sent_email_id) lines.push(chatDetailLine(` Reply to sent email id: ${context.reply.reply_to_sent_email_id}`));
14591
+ if (context.reply.message_id) lines.push(chatDetailLine(` Message-Id: ${context.reply.message_id}`));
14592
+ if (context.localChatId !== void 0) lines.push(chatDetailLine(` Local chat id: ${context.localChatId}`));
14593
+ lines.push("", chatHeading("Helpful follow-up commands"), chatDetailLine(" Replace <message> before running commands that include it."), chatDetailLine(" Commands are templates; use --json for parse-safe output."), chatDetailLine(" When shown, --strict-only prefers timing out over matching the wrong reply."));
14594
+ for (const { description, command } of buildChatFollowUpCommands(context)) lines.push(chatHeading(` ${description}:`), ` ${chatCommandText(command)}`);
14595
+ lines.push("", chatHeading(`Response body (${responseBody.format}; use --json for parsing)`), "----- BEGIN RESPONSE -----", responseBody.body || "(empty response)", "----- END RESPONSE -----");
14843
14596
  return lines.join("\n");
14844
14597
  }
14845
14598
  function formatChatRecoveryContext(context) {
14599
+ const accepted = context.sent.accepted.join(", ") || context.recipient;
14846
14600
  const lines = [
14847
14601
  "",
14848
- "Sent message context",
14849
- ` To: ${context.sent.accepted.join(", ") || context.recipient}`,
14850
- ` From: ${context.sent.from || context.from}`,
14851
- ` Subject: ${context.subject}`,
14852
- ` Sent email id: ${context.sent.id}`,
14853
- ` Delivery status: ${context.sent.delivery_status ?? context.sent.status}`,
14854
- ` Poll since: ${context.sentAtIso}`,
14602
+ chatHeading("Sent message context"),
14603
+ chatDetailLine(` To: ${accepted}`),
14604
+ chatDetailLine(` From: ${context.sent.from || context.from}`),
14605
+ chatDetailLine(` Subject: ${context.subject}`),
14606
+ chatDetailLine(` Sent email id: ${context.sent.id}`),
14607
+ chatColor("green", ` Delivery status: ${context.sent.delivery_status ?? context.sent.status}`),
14608
+ chatDetailLine(` Poll since: ${context.sentAtIso}`),
14855
14609
  "",
14856
- "Helpful recovery commands"
14610
+ chatHeading("Helpful recovery commands")
14857
14611
  ];
14858
- for (const { description, command } of buildChatRecoveryCommands(context)) lines.push(` ${description}:`, ` ${command}`);
14612
+ for (const { description, command } of buildChatRecoveryCommands(context)) lines.push(chatHeading(` ${description}:`), ` ${chatCommandText(command)}`);
14859
14613
  return lines.join("\n");
14860
14614
  }
14861
14615
  async function loadInboundEmailDetail(params) {
@@ -14926,6 +14680,8 @@ var ChatCommand = class ChatCommand extends Command {
14926
14680
  --reply-to-email-id <inbound-email-id>. Reply mode uses Primitive's
14927
14681
  reply endpoint, so the reply subject and threading headers are
14928
14682
  derived from the inbound email instead of copied into CLI flags.
14683
+ Successful chat turns also save an active local chat, so the next
14684
+ follow-up can be sent with \`primitive chat reply '<message>'\`.
14929
14685
 
14930
14686
  --json emits a structured envelope with both sides of the exchange,
14931
14687
  a direct response_body field, match details, and follow-up command
@@ -14945,8 +14701,11 @@ var ChatCommand = class ChatCommand extends Command {
14945
14701
  static examples = [
14946
14702
  "<%= config.bin %> chat help@agent.acme.dev 'how do I rotate my API key?'",
14947
14703
  "cat error.log | <%= config.bin %> chat help@agent.acme.dev",
14704
+ "<%= config.bin %> chat reply 'one more thing'",
14705
+ "<%= config.bin %> chat reply 'see attached' --attachment ./report.pdf",
14948
14706
  "<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing'",
14949
14707
  "<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing' --reply-to-email-id <inbound-email-id>",
14708
+ "<%= config.bin %> chat help@agent.acme.dev 'can you review this?' --attachment ./report.pdf",
14950
14709
  "<%= config.bin %> chat help@agent.acme.dev 'follow up question' --json",
14951
14710
  "<%= config.bin %> chat help@agent.acme.dev 'one more thing' --timeout 300"
14952
14711
  ];
@@ -14980,15 +14739,25 @@ var ChatCommand = class ChatCommand extends Command {
14980
14739
  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
14740
  "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
14741
  "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." }),
14742
+ attachment: Flags.string({
14743
+ char: "a",
14744
+ description: "Attach a file to this chat message. Repeat --attachment to attach multiple files.",
14745
+ multiple: true
14746
+ }),
14747
+ "chat-local-id": Flags.integer({
14748
+ description: "Local chat id to update after this command succeeds. Internal plumbing for `primitive chat reply`.",
14749
+ hidden: true,
14750
+ min: 0
14751
+ }),
14983
14752
  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
14753
  quiet: Flags.boolean({ description: "Suppress stderr progress updates while sending and waiting. Errors and recovery commands are still written to stderr." }),
14985
14754
  timeout: Flags.integer({
14986
- default: DEFAULT_CHAT_TIMEOUT_SECONDS,
14755
+ default: 120,
14987
14756
  description: "Seconds to wait for a reply before exiting non-zero; 0 waits forever.",
14988
14757
  min: 0
14989
14758
  }),
14990
14759
  "strict-phase-seconds": Flags.integer({
14991
- default: DEFAULT_STRICT_PHASE_SECONDS,
14760
+ default: 60,
14992
14761
  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
14762
  min: 1
14994
14763
  }),
@@ -15028,6 +14797,7 @@ var ChatCommand = class ChatCommand extends Command {
15028
14797
  configDir: this.config.configDir
15029
14798
  };
15030
14799
  const progress = flags.quiet ? null : new ChatProgressIndicator(process.stderr);
14800
+ const attachments = readAttachmentFiles(flags.attachment);
15031
14801
  let from;
15032
14802
  let parentReply;
15033
14803
  let subject;
@@ -15086,9 +14856,10 @@ var ChatCommand = class ChatCommand extends Command {
15086
14856
  const sendResult = parentReply !== void 0 ? await replyToEmail({
15087
14857
  body: {
15088
14858
  body_text: message,
15089
- from
14859
+ from,
14860
+ ...attachments !== void 0 ? { attachments } : {}
15090
14861
  },
15091
- client: apiClient.client,
14862
+ client: apiClient._sendClient,
15092
14863
  path: { id: parentReply.id },
15093
14864
  responseStyle: "fields"
15094
14865
  }) : await sendEmail({
@@ -15097,7 +14868,8 @@ var ChatCommand = class ChatCommand extends Command {
15097
14868
  to: args.recipient,
15098
14869
  subject,
15099
14870
  body_text: message,
15100
- ...flags["in-reply-to"] !== void 0 ? { in_reply_to: flags["in-reply-to"] } : {}
14871
+ ...flags["in-reply-to"] !== void 0 ? { in_reply_to: flags["in-reply-to"] } : {},
14872
+ ...attachments !== void 0 ? { attachments } : {}
15101
14873
  },
15102
14874
  client: apiClient._sendClient,
15103
14875
  responseStyle: "fields"
@@ -15172,16 +14944,140 @@ var ChatCommand = class ChatCommand extends Command {
15172
14944
  return;
15173
14945
  }
15174
14946
  progress?.succeed(`Reply received from ${replyResult.reply.from_email}`);
15175
- const outputContext = {
14947
+ let outputContext = {
15176
14948
  ...baseContext,
15177
14949
  matchStrategy: replyResult.matchStrategy,
15178
14950
  reply: replyResult.reply
15179
14951
  };
14952
+ const localChatId = persistActiveChat({
14953
+ configDir: this.config.configDir,
14954
+ context: outputContext,
14955
+ preferredLocalId: flags["chat-local-id"],
14956
+ writeWarning: (message) => process.stderr.write(message)
14957
+ });
14958
+ if (localChatId !== null) outputContext = {
14959
+ ...outputContext,
14960
+ localChatId
14961
+ };
15180
14962
  if (flags.json) this.log(JSON.stringify(buildChatJsonEnvelope(outputContext), null, 2));
15181
14963
  else this.log(formatChatResponse(outputContext));
15182
14964
  });
15183
14965
  }
15184
14966
  };
14967
+ var ChatReplyCommand = class ChatReplyCommand extends Command {
14968
+ static description = `Reply in the active chat.
14969
+
14970
+ A successful \`primitive chat <email> <message>\` saves the latest
14971
+ inbound reply as a local chat and makes it active. Use
14972
+ \`primitive chat reply <message>\` for the active chat, or
14973
+ \`primitive chat reply <local-id> <message>\` / \`--id <local-id>\`
14974
+ for a specific local chat. The command uses Primitive's real reply
14975
+ endpoint against the stored inbound email id, so the recipient,
14976
+ subject, and threading headers are derived server-side from the
14977
+ thread.
14978
+
14979
+ If no chat is open, start one with \`primitive chat <email> '<message>'\`.
14980
+ For explicit control, use \`primitive chat <email> --reply '<message>'
14981
+ --reply-to-email-id <inbound-email-id>\`.`;
14982
+ static summary = "Reply in the active chat";
14983
+ static examples = [
14984
+ "<%= config.bin %> chat reply 'one more thing'",
14985
+ "<%= config.bin %> chat reply 0 'one more thing'",
14986
+ "<%= config.bin %> chat reply --id 0 'one more thing'",
14987
+ "<%= config.bin %> chat reply 'see attached' --attachment ./report.pdf",
14988
+ "cat follow-up.txt | <%= config.bin %> chat reply"
14989
+ ];
14990
+ static args = {
14991
+ idOrMessage: Args.string({ description: "Reply body, or a local chat id when followed by a separate message." }),
14992
+ message: Args.string({ description: "Reply body when the first positional argument is an id." })
14993
+ };
14994
+ static flags = {
14995
+ "api-key": Flags.string({
14996
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive signin` credentials)",
14997
+ env: "PRIMITIVE_API_KEY"
14998
+ }),
14999
+ "api-base-url-1": Flags.string({
15000
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
15001
+ env: "PRIMITIVE_API_BASE_URL_1",
15002
+ hidden: true
15003
+ }),
15004
+ "api-base-url-2": Flags.string({
15005
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
15006
+ env: "PRIMITIVE_API_BASE_URL_2",
15007
+ hidden: true
15008
+ }),
15009
+ id: Flags.integer({
15010
+ description: "Local chat id to reply in. Omit to use the most recent active chat.",
15011
+ min: 0
15012
+ }),
15013
+ 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." }),
15014
+ quiet: Flags.boolean({ description: "Suppress stderr progress updates while sending and waiting. Errors and recovery commands are still written to stderr." }),
15015
+ timeout: Flags.integer({
15016
+ description: "Seconds to wait for a reply before exiting non-zero. Defaults to the active chat's last timeout.",
15017
+ min: 0
15018
+ }),
15019
+ "strict-phase-seconds": Flags.integer({
15020
+ description: "Seconds to wait in strict-threading mode before falling back. Defaults to the active chat's last setting.",
15021
+ min: 1
15022
+ }),
15023
+ "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." }),
15024
+ attachment: Flags.string({
15025
+ char: "a",
15026
+ description: "Attach a file to the reply. Repeat --attachment to attach multiple files.",
15027
+ multiple: true
15028
+ }),
15029
+ interval: Flags.integer({
15030
+ description: "Seconds between polls while waiting for the reply.",
15031
+ min: 1
15032
+ }),
15033
+ "page-size": Flags.integer({
15034
+ description: "Inbound emails to fetch per poll while waiting (1-100). Internal tuning knob.",
15035
+ max: 100,
15036
+ min: 1,
15037
+ hidden: true
15038
+ }),
15039
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
15040
+ };
15041
+ async run() {
15042
+ const { args, flags } = await this.parse(ChatReplyCommand);
15043
+ const positionalLocalId = flags.id === void 0 && args.message !== void 0 ? parseLocalChatIdArg(args.idOrMessage) : void 0;
15044
+ 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.");
15045
+ 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.");
15046
+ const localId = flags.id ?? (typeof positionalLocalId === "number" ? positionalLocalId : void 0);
15047
+ const state = localId === void 0 ? loadActiveChatState(this.config.configDir) : loadChatConversationByLocalId(this.config.configDir, localId);
15048
+ 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.`);
15049
+ 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.");
15050
+ if (!message.trim()) throw cliError$6("Reply body is empty.");
15051
+ const argv = [
15052
+ state.recipient,
15053
+ "--reply",
15054
+ message,
15055
+ "--from",
15056
+ state.from,
15057
+ "--reply-to-email-id",
15058
+ state.last_reply_email_id,
15059
+ "--timeout",
15060
+ String(flags.timeout ?? state.timeout_seconds),
15061
+ "--strict-phase-seconds",
15062
+ String(flags["strict-phase-seconds"] ?? state.strict_phase_seconds),
15063
+ "--interval",
15064
+ String(flags.interval ?? 2),
15065
+ "--page-size",
15066
+ String(flags["page-size"] ?? 50),
15067
+ "--chat-local-id",
15068
+ String(state.local_id)
15069
+ ];
15070
+ if (flags["api-key"] !== void 0) argv.push("--api-key", flags["api-key"]);
15071
+ if (flags["api-base-url-1"] !== void 0) argv.push("--api-base-url-1", flags["api-base-url-1"]);
15072
+ if (flags["api-base-url-2"] !== void 0) argv.push("--api-base-url-2", flags["api-base-url-2"]);
15073
+ if (flags.json) argv.push("--json");
15074
+ if (flags.quiet) argv.push("--quiet");
15075
+ for (const attachment of flags.attachment ?? []) argv.push("--attachment", attachment);
15076
+ if (state.strict_only || flags["strict-only"]) argv.push("--strict-only");
15077
+ if (flags.time) argv.push("--time");
15078
+ await ChatCommand.run(argv, { root: this.config.root });
15079
+ }
15080
+ };
15185
15081
  async function waitForReply(params) {
15186
15082
  const notice = params.notice ?? ((message) => {
15187
15083
  process.stderr.write(`${message}\n`);
@@ -17186,8 +17082,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
17186
17082
  name: "Primitive Team",
17187
17083
  url: "https://primitive.dev"
17188
17084
  };
17189
- const SDK_VERSION_RANGE = "^0.33.0";
17190
- const CLI_VERSION_RANGE = "^0.33.0";
17085
+ const SDK_VERSION_RANGE = "^0.35.0";
17086
+ const CLI_VERSION_RANGE = "^0.35.0";
17191
17087
  const ESBUILD_VERSION_RANGE = "^0.27.0";
17192
17088
  function renderHandler() {
17193
17089
  return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
@@ -18560,6 +18456,7 @@ const DOMAIN_DISPLAY_WIDTH = 34;
18560
18456
  const STATUS_DISPLAY_WIDTH = 12;
18561
18457
  const BOOL_DISPLAY_WIDTH = 7;
18562
18458
  const NUM_DISPLAY_WIDTH = 6;
18459
+ const DEFAULT_PRIMITIVE_LOCAL_PART = "agent";
18563
18460
  function plural(count, singular, pluralValue = `${singular}s`) {
18564
18461
  return `${count} ${count === 1 ? singular : pluralValue}`;
18565
18462
  }
@@ -18595,6 +18492,14 @@ function domainSummary(domain) {
18595
18492
  default: return `${domain.domain} has status ${String(domain.status)}.`;
18596
18493
  }
18597
18494
  }
18495
+ function findSuggestedPrimitiveAddress(domains) {
18496
+ const domain = domains.find((entry) => entry.managed && entry.active && entry.receiving_ready);
18497
+ if (!domain) return null;
18498
+ return {
18499
+ address: `${DEFAULT_PRIMITIVE_LOCAL_PART}@${domain.domain}`,
18500
+ domain: domain.domain
18501
+ };
18502
+ }
18598
18503
  function focusInboxStatus(status, domainName) {
18599
18504
  const normalized = domainName.toLowerCase();
18600
18505
  const domain = status.domains.find((entry) => entry.domain.toLowerCase() === normalized);
@@ -18641,12 +18546,14 @@ function formatInboxStatus(status) {
18641
18546
  "",
18642
18547
  "Domains"
18643
18548
  ];
18549
+ const suggestedAddress = findSuggestedPrimitiveAddress(status.domains);
18644
18550
  if (status.domains.length === 0) lines.push("No domains configured.");
18645
18551
  else {
18646
18552
  lines.push(formatDomainHeader());
18647
18553
  for (const domain of status.domains) lines.push(formatDomainRow(domain));
18648
18554
  }
18649
18555
  lines.push("", `Endpoints: ${status.endpoints.enabled}/${status.endpoints.total} enabled (${status.endpoints.fallback_enabled} fallback, ${status.endpoints.domain_scoped_enabled} domain-scoped, ${status.endpoints.function_enabled} function)`, `Functions: ${status.functions.deployed}/${status.functions.total} deployed (${status.functions.pending} pending, ${status.functions.failed} failed)`, `Recent inbound: ${plural(status.recent_emails.total, "email")} latest ${formatInboxDate(status.recent_emails.latest_received_at)}`);
18556
+ if (suggestedAddress) lines.push("", `Primitive address: ${suggestedAddress.address}`, ` Any local-part at ${suggestedAddress.domain} can receive mail.`, ` Try: primitive send --to ${suggestedAddress.address} --subject "hello" --body "test"`);
18650
18557
  if (status.next_actions.length > 0) {
18651
18558
  lines.push("", "Next actions");
18652
18559
  for (const action of status.next_actions) lines.push(formatNextAction(action));
@@ -18723,6 +18630,180 @@ var InboxStatusCommand = class InboxStatusCommand extends Command {
18723
18630
  }
18724
18631
  };
18725
18632
  //#endregion
18633
+ //#region src/oclif/commands/inbox-setup.ts
18634
+ const DEFAULT_FUNCTION_NAME = "inbound-reply";
18635
+ const DEFAULT_LOCAL_PART = "inbox";
18636
+ const FUNCTION_ID_PLACEHOLDER = "<function-id>";
18637
+ function firstUsableManagedDomain(status) {
18638
+ return status.domains.find((domain) => domain.managed && domain.receiving_ready && domain.active) ?? status.domains.find((domain) => domain.managed && domain.receiving_ready) ?? null;
18639
+ }
18640
+ function buildInboxSetupCommands(functionName = DEFAULT_FUNCTION_NAME) {
18641
+ return {
18642
+ scaffold: [
18643
+ `primitive functions init ${functionName}`,
18644
+ `cd ${functionName}`,
18645
+ "npm install",
18646
+ "npm run build",
18647
+ `primitive functions deploy --name ${functionName} --file ./dist/handler.js --wait`,
18648
+ `primitive functions test --id ${FUNCTION_ID_PLACEHOLDER} --wait --show-sends`
18649
+ ],
18650
+ logs: `primitive functions logs --id ${FUNCTION_ID_PLACEHOLDER}`,
18651
+ status: "primitive inbox status"
18652
+ };
18653
+ }
18654
+ function buildInboxSetupProof(commands) {
18655
+ return {
18656
+ after_test: [
18657
+ "inbound id for the generated test email",
18658
+ "function id matching the deployed Function",
18659
+ "invocation status completed, failed, or send_failed",
18660
+ "reply/send result emitted by the handler"
18661
+ ],
18662
+ logs_command: commands.logs
18663
+ };
18664
+ }
18665
+ function buildInboxSetupGuide(status) {
18666
+ const domain = firstUsableManagedDomain(status);
18667
+ const commands = buildInboxSetupCommands();
18668
+ const mode = !status.receiving_ready ? "not_receiving" : status.processing_ready ? "actively_processed" : "stored_only";
18669
+ return {
18670
+ readiness: {
18671
+ ready: status.ready,
18672
+ receiving_ready: status.receiving_ready,
18673
+ processing_ready: status.processing_ready,
18674
+ mode,
18675
+ summary: status.summary
18676
+ },
18677
+ receive: {
18678
+ address: domain ? `${DEFAULT_LOCAL_PART}@${domain.domain}` : null,
18679
+ domain: domain?.domain ?? null,
18680
+ managed: domain?.managed ?? false,
18681
+ placeholder_local_part: domain ? DEFAULT_LOCAL_PART : null
18682
+ },
18683
+ processing: {
18684
+ stored_only: status.receiving_ready && !status.processing_ready,
18685
+ active: status.processing_ready,
18686
+ enabled_endpoints: status.endpoints.enabled,
18687
+ deployed_functions: status.functions.deployed
18688
+ },
18689
+ commands,
18690
+ proof: buildInboxSetupProof(commands),
18691
+ status
18692
+ };
18693
+ }
18694
+ function formatReadiness(guide) {
18695
+ const readiness = guide.readiness.ready ? "ready" : "not ready";
18696
+ const receiving = guide.readiness.receiving_ready ? "yes" : "no";
18697
+ const processing = guide.readiness.processing_ready ? "yes" : "no";
18698
+ const mode = guide.readiness.mode === "actively_processed" ? "actively processed" : guide.readiness.mode === "stored_only" ? "stored-only" : "not receiving";
18699
+ return [
18700
+ `Readiness: ${readiness}`,
18701
+ `Receiving: ${receiving}`,
18702
+ `Processing: ${processing}`,
18703
+ `Mode: ${mode}`
18704
+ ].join("\n");
18705
+ }
18706
+ function formatReceiveAddress(guide) {
18707
+ if (!guide.receive.domain || !guide.receive.address) return "Receive address: none found on a receiving-ready Primitive-managed domain";
18708
+ return [`Receive address: ${guide.receive.address}`, `Receive domain: ${guide.receive.domain} (Primitive-managed)`].join("\n");
18709
+ }
18710
+ function formatDomainDetails(status) {
18711
+ if (status.domains.length === 0) return ["Domains: none configured"];
18712
+ return status.domains.map((domain) => `- ${domain.domain}: ${statusText(domain.status)}, receive ${domain.receiving_ready ? "yes" : "no"}, process ${domain.processing_ready ? "yes" : "no"}, routes ${domain.processing_route_count}`);
18713
+ }
18714
+ function formatScaffoldCommands(commands) {
18715
+ return commands.scaffold.map((command) => ` ${command}`);
18716
+ }
18717
+ function formatInboxSetupGuide(guide) {
18718
+ const lines = [
18719
+ "Inbound setup",
18720
+ "",
18721
+ guide.readiness.summary,
18722
+ "",
18723
+ formatReadiness(guide),
18724
+ "",
18725
+ formatReceiveAddress(guide),
18726
+ "",
18727
+ "Domains",
18728
+ ...formatDomainDetails(guide.status),
18729
+ "",
18730
+ `Processing routes: ${guide.processing.enabled_endpoints} enabled endpoint(s), ${guide.processing.deployed_functions} deployed Function(s)`
18731
+ ];
18732
+ if (guide.readiness.mode === "not_receiving") lines.push("", "Next actions", "Make a receiving-ready domain available, then re-run:", ` ${guide.commands.status}`);
18733
+ else if (!guide.processing.active) lines.push("", "Next actions", "No processing route is enabled. Scaffold, deploy, and test an email Function:", ...formatScaffoldCommands(guide.commands));
18734
+ else lines.push("", "Next actions", "Inbound mail has an active processing route. Run a Function test when you know the Function id:", ` primitive functions test --id ${FUNCTION_ID_PLACEHOLDER} --wait --show-sends`);
18735
+ if (guide.status.next_actions.length > 0) {
18736
+ lines.push("", "API suggested actions");
18737
+ for (const action of guide.status.next_actions) lines.push(action.command ? `- ${action.message}\n ${action.command}` : `- ${action.message}`);
18738
+ }
18739
+ lines.push("", "Proof after functions test", "- Inbound id: the generated test email should have an inbound id.", "- Function id: the run should point at the Function id you deployed.", "- Invocation status: expect completed; failed or send_failed identifies the failing stage.", "- Reply/send result: --show-sends should show the handler's outbound result when it replies or sends.", "- Logs:", ` ${guide.proof.logs_command}`);
18740
+ return lines.join("\n");
18741
+ }
18742
+ var InboxSetupCommand = class InboxSetupCommand extends Command {
18743
+ static description = `Guide inbound email setup from the server-owned inbox status API.
18744
+
18745
+ This command does not scaffold, deploy, or run tests. It verifies auth, fetches inbox readiness, shows the first usable Primitive-managed receive address/domain, explains whether inbound mail is stored-only or actively processed, and prints the exact commands to add a Function processing route when one is missing.`;
18746
+ static summary = "Guide inbound email setup";
18747
+ static examples = ["<%= config.bin %> inbox setup", "<%= config.bin %> inbox setup --json"];
18748
+ static flags = {
18749
+ "api-key": Flags.string({
18750
+ description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
18751
+ env: "PRIMITIVE_API_KEY"
18752
+ }),
18753
+ "api-base-url-1": Flags.string({
18754
+ description: API_BASE_URL_1_FLAG_DESCRIPTION,
18755
+ env: "PRIMITIVE_API_BASE_URL_1",
18756
+ hidden: true
18757
+ }),
18758
+ "api-base-url-2": Flags.string({
18759
+ description: API_BASE_URL_2_FLAG_DESCRIPTION,
18760
+ env: "PRIMITIVE_API_BASE_URL_2",
18761
+ hidden: true
18762
+ }),
18763
+ json: Flags.boolean({ description: "Print structured readiness, receive address, commands, proof metadata, and raw status as JSON." }),
18764
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
18765
+ };
18766
+ async run() {
18767
+ const { flags } = await this.parse(InboxSetupCommand);
18768
+ await runWithTiming(flags.time, async () => {
18769
+ const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
18770
+ apiKey: flags["api-key"],
18771
+ apiBaseUrl1: flags["api-base-url-1"],
18772
+ apiBaseUrl2: flags["api-base-url-2"],
18773
+ configDir: this.config.configDir
18774
+ });
18775
+ const result = await getInboxStatus({
18776
+ client: apiClient.client,
18777
+ responseStyle: "fields"
18778
+ });
18779
+ if (result.error) {
18780
+ const errorPayload = extractErrorPayload(result.error);
18781
+ writeErrorWithHints(errorPayload);
18782
+ surfaceUnauthorizedHint({
18783
+ auth,
18784
+ baseUrlOverridden,
18785
+ configDir: this.config.configDir,
18786
+ payload: errorPayload
18787
+ });
18788
+ process.exitCode = 1;
18789
+ return;
18790
+ }
18791
+ const envelope = result.data ?? {};
18792
+ const status = envelope.data;
18793
+ if (!status) throw new Errors.CLIError("Primitive API returned no inbox status.", { exit: 1 });
18794
+ const guide = buildInboxSetupGuide(status);
18795
+ if (flags.json) {
18796
+ this.log(JSON.stringify({
18797
+ ...envelope,
18798
+ data: guide
18799
+ }, null, 2));
18800
+ return;
18801
+ }
18802
+ this.log(formatInboxSetupGuide(guide));
18803
+ });
18804
+ }
18805
+ };
18806
+ //#endregion
18726
18807
  //#region src/oclif/commands/login.ts
18727
18808
  const MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS = 60;
18728
18809
  function cliError$3(message) {
@@ -18904,6 +18985,7 @@ var LoginCommand$1 = class extends Command {
18904
18985
  if (polled.data) {
18905
18986
  const login = unwrapData$2(polled.data);
18906
18987
  if (!login) throw cliError$3("Primitive API returned an empty CLI poll response.");
18988
+ deleteChatState(this.config.configDir);
18907
18989
  saveCliCredentials(this.config.configDir, {
18908
18990
  access_token: login.access_token,
18909
18991
  api_base_url_1: apiBaseUrl1,
@@ -19044,11 +19126,100 @@ function loadPendingAgentSignup(configDir, apiBaseUrl1) {
19044
19126
  expires_in: Math.max(0, Math.ceil((new Date(pending.expires_at).getTime() - Date.now()) / 1e3))
19045
19127
  };
19046
19128
  }
19129
+ function readPendingAgentSignupState(configDir, apiBaseUrl1) {
19130
+ const path = pendingSignupPath(configDir);
19131
+ let contents;
19132
+ try {
19133
+ contents = readFileSync(path, "utf8");
19134
+ } catch (error) {
19135
+ if (error && typeof error === "object" && error.code === "ENOENT") return null;
19136
+ throw error;
19137
+ }
19138
+ let pending;
19139
+ try {
19140
+ pending = pendingSignupFromJson(JSON.parse(contents));
19141
+ } catch {
19142
+ pending = null;
19143
+ }
19144
+ if (!pending) {
19145
+ deletePendingAgentSignup(configDir);
19146
+ return null;
19147
+ }
19148
+ if (pending.api_base_url_1 !== apiBaseUrl1) return null;
19149
+ return pending;
19150
+ }
19151
+ function pendingSignupStartCommand(email) {
19152
+ return `primitive signup ${email ?? "<email>"} --signup-code <invite-code> --accept-terms`;
19153
+ }
19154
+ function buildSignupStatus(params) {
19155
+ const copy = params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY;
19156
+ const pending = readPendingAgentSignupState(params.configDir, params.apiBaseUrl1);
19157
+ if (!pending) return {
19158
+ code_length: null,
19159
+ confirm_command: null,
19160
+ email: null,
19161
+ expired: false,
19162
+ expires_at: null,
19163
+ expires_in: null,
19164
+ pending: false,
19165
+ resend_after: null,
19166
+ resend_command: null,
19167
+ signup_command: pendingSignupStartCommand(params.email)
19168
+ };
19169
+ if (params.email && normalizeEmail(pending.email) !== normalizeEmail(params.email)) throw cliError$2(`Pending ${copy.actionNoun} is for ${pending.email}, not ${params.email}. Run \`primitive signup status\` without an email argument to inspect it.`);
19170
+ const expiresAtMs = new Date(pending.expires_at).getTime();
19171
+ const expiresIn = Number.isFinite(expiresAtMs) ? Math.ceil((expiresAtMs - Date.now()) / 1e3) : null;
19172
+ return {
19173
+ code_length: pending.verification_code_length,
19174
+ confirm_command: `primitive ${copy.confirmCommand(pending.email)}`,
19175
+ email: pending.email,
19176
+ expired: expiresIn !== null && expiresIn <= 0,
19177
+ expires_at: pending.expires_at,
19178
+ expires_in: expiresIn === null ? null : Math.max(0, expiresIn),
19179
+ pending: true,
19180
+ resend_after: pending.resend_after,
19181
+ resend_command: `primitive ${copy.resendCommand(pending.email)}`
19182
+ };
19183
+ }
19184
+ function writeSignupStatus(status) {
19185
+ if (!status.pending) {
19186
+ process$1.stdout.write("No pending Primitive signup found.\n");
19187
+ process$1.stdout.write(`Start one with \`${status.signup_command ?? pendingSignupStartCommand()}\`.\n`);
19188
+ return;
19189
+ }
19190
+ process$1.stdout.write(`Pending Primitive signup for ${status.email}.\n`);
19191
+ if (status.code_length !== null) process$1.stdout.write(`Verification code length: ${status.code_length}\n`);
19192
+ if (status.expires_at) if (status.expired) process$1.stdout.write(`Expired at: ${status.expires_at}\n`);
19193
+ else {
19194
+ process$1.stdout.write(`Expires at: ${status.expires_at}\n`);
19195
+ process$1.stdout.write(`Expires in: ${formatSignupSeconds(status.expires_in)}\n`);
19196
+ }
19197
+ if (status.resend_after !== null) process$1.stdout.write(`Resend after: ${formatSignupSeconds(status.resend_after)}\n`);
19198
+ if (status.confirm_command) process$1.stdout.write(`Confirm: ${status.confirm_command}\n`);
19199
+ if (status.resend_command) process$1.stdout.write(`Resend: ${status.resend_command}\n`);
19200
+ }
19201
+ function runSignupStatus(params) {
19202
+ const { requestConfig } = createCliApiClient({
19203
+ apiBaseUrl1: params.flags["api-base-url-1"],
19204
+ configDir: params.configDir
19205
+ });
19206
+ const status = buildSignupStatus({
19207
+ apiBaseUrl1: requestConfig.resolvedApiBaseUrl1,
19208
+ configDir: params.configDir,
19209
+ copy: params.copy,
19210
+ email: params.email
19211
+ });
19212
+ if (params.flags.json) {
19213
+ process$1.stdout.write(`${JSON.stringify(status, null, 2)}\n`);
19214
+ return;
19215
+ }
19216
+ writeSignupStatus(status);
19217
+ }
19047
19218
  function requirePendingSignupForEmail(params) {
19048
19219
  const copy = params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY;
19049
19220
  const pending = loadPendingAgentSignup(params.configDir, params.apiBaseUrl1);
19050
- if (!pending) throw cliError$2(`No pending ${copy.actionNoun} for ${params.email}. Run \`primitive ${copy.startCommand(params.email)}\` first.`);
19051
- if (normalizeEmail(pending.email) !== normalizeEmail(params.email)) throw cliError$2(`Pending ${copy.actionNoun} is for ${pending.email}, not ${params.email}. Run \`primitive ${copy.startCommand(params.email)} --force\` to replace it.`);
19221
+ if (!pending) throw cliError$2(`No pending ${copy.actionNoun} for ${params.email}. Run \`primitive signup status ${params.email}\` to inspect pending state, or \`primitive ${copy.startCommand(params.email)}\` first.`);
19222
+ if (normalizeEmail(pending.email) !== normalizeEmail(params.email)) throw cliError$2(`Pending ${copy.actionNoun} is for ${pending.email}, not ${params.email}. Run \`primitive signup status\` to inspect it, or \`primitive ${copy.startCommand(params.email)} --force\` to replace it.`);
19052
19223
  return pending;
19053
19224
  }
19054
19225
  function retryAfterSeconds(result) {
@@ -19125,6 +19296,7 @@ async function checkExistingCredentials(params) {
19125
19296
  throw cliError$2(`Already logged in${existing.org_name ? ` for ${existing.org_name}` : ""}. Run \`primitive logout\` before ${copy.actionGerund}.`);
19126
19297
  }
19127
19298
  function saveSignupCredentials(params) {
19299
+ deleteChatState(params.configDir);
19128
19300
  saveCliCredentials(params.configDir, {
19129
19301
  access_token: params.signup.access_token,
19130
19302
  api_base_url_1: params.apiBaseUrl1,
@@ -19150,13 +19322,13 @@ async function startSignup(params) {
19150
19322
  if (existingPending && !params.flags.force) {
19151
19323
  if (normalizeEmail(existingPending.email) === normalizeEmail(params.email)) {
19152
19324
  process$1.stderr.write(`Continuing pending Primitive ${copy.actionNoun} for ${existingPending.email}.\n`);
19153
- process$1.stderr.write(`Run \`primitive ${copy.confirmCommand(existingPending.email)}\` to finish, or \`primitive ${copy.resendCommand(existingPending.email)}\` to send a new code.\n`);
19325
+ process$1.stderr.write(`Run \`primitive ${copy.confirmCommand(existingPending.email)}\` to finish, \`primitive ${copy.resendCommand(existingPending.email)}\` to send a new code, or \`primitive signup status\` to inspect it.\n`);
19154
19326
  return {
19155
19327
  pending: existingPending,
19156
19328
  started: false
19157
19329
  };
19158
19330
  }
19159
- throw cliError$2(`Pending ${copy.actionNoun} is for ${existingPending.email}. Run \`primitive ${copy.startCommand(params.email)} --force\` to replace it.`);
19331
+ throw cliError$2(`Pending ${copy.actionNoun} is for ${existingPending.email}. Run \`primitive signup status\` to inspect it, or \`primitive ${copy.startCommand(params.email)} --force\` to replace it.`);
19160
19332
  }
19161
19333
  if (params.flags.force) deletePendingAgentSignup(params.configDir);
19162
19334
  const promptRequiredFn = params.deps.promptRequired ?? promptRequired;
@@ -19292,23 +19464,25 @@ async function runSignupConfirmWithCredentialLock(params) {
19292
19464
  }
19293
19465
  const payload = extractErrorPayload(verified.error);
19294
19466
  const code = extractErrorCode(payload);
19295
- if (code === INVALID_VERIFICATION_CODE) throw cliError$2(`Invalid verification code. Try again or run ${(params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY).resendCommand(params.email)}.`);
19467
+ if (code === INVALID_VERIFICATION_CODE) throw cliError$2(`Invalid verification code. Try again, run ${(params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY).resendCommand(params.email)}, or run primitive signup status.`);
19296
19468
  if (code === EXPIRED_TOKEN || code === INVALID_SIGNUP_TOKEN) deletePendingAgentSignup(configDir);
19297
19469
  writeErrorWithHints(payload);
19298
19470
  throw cliError$2("Primitive agent signup failed while verifying the account.");
19299
19471
  }
19300
19472
  async function runSignupResendWithCredentialLock(params) {
19301
19473
  const deps = params.deps ?? {};
19474
+ const copy = params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY;
19302
19475
  const { apiClient, requestConfig } = createCliApiClient({
19303
19476
  apiBaseUrl1: params.flags["api-base-url-1"],
19304
19477
  configDir: params.configDir
19305
19478
  });
19306
- const pending = requirePendingSignupForEmail({
19479
+ const pending = params.email ? requirePendingSignupForEmail({
19307
19480
  apiBaseUrl1: requestConfig.resolvedApiBaseUrl1,
19308
- copy: params.copy,
19481
+ copy,
19309
19482
  configDir: params.configDir,
19310
19483
  email: params.email
19311
- });
19484
+ }) : loadPendingAgentSignup(params.configDir, requestConfig.resolvedApiBaseUrl1);
19485
+ if (!pending) throw cliError$2(`No pending ${copy.actionNoun} found. Run \`primitive signup status\` to inspect pending state, or start one with \`${pendingSignupStartCommand()}\`.`);
19312
19486
  const resend = await resendVerificationCode({
19313
19487
  apiBaseUrl1: requestConfig.resolvedApiBaseUrl1,
19314
19488
  apiClient,
@@ -19481,12 +19655,12 @@ var SignupConfirmCommand = class SignupConfirmCommand extends Command {
19481
19655
  };
19482
19656
  var SignupResendCommand = class SignupResendCommand extends Command {
19483
19657
  static args = { email: Args.string({
19484
- description: "Email address used to start signup",
19485
- required: true
19658
+ description: "Email address used to start signup. Defaults to the saved pending signup.",
19659
+ required: false
19486
19660
  }) };
19487
19661
  static description = "Resend the verification code for a pending signup.";
19488
19662
  static summary = "Resend signup verification code";
19489
- static examples = ["<%= config.bin %> signup resend user@example.com"];
19663
+ static examples = ["<%= config.bin %> signup resend", "<%= config.bin %> signup resend user@example.com"];
19490
19664
  static flags = { "api-base-url-1": Flags.string({
19491
19665
  description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19492
19666
  env: "PRIMITIVE_API_BASE_URL_1",
@@ -19511,6 +19685,35 @@ var SignupResendCommand = class SignupResendCommand extends Command {
19511
19685
  }
19512
19686
  }
19513
19687
  };
19688
+ var SignupStatusCommand = class SignupStatusCommand extends Command {
19689
+ static args = { email: Args.string({
19690
+ description: "Email address expected in the pending signup",
19691
+ required: false
19692
+ }) };
19693
+ static description = "Inspect the locally saved pending Primitive signup state.";
19694
+ static summary = "Show pending signup status";
19695
+ static examples = [
19696
+ "<%= config.bin %> signup status",
19697
+ "<%= config.bin %> signup status user@example.com",
19698
+ "<%= config.bin %> signup status --json"
19699
+ ];
19700
+ static flags = {
19701
+ "api-base-url-1": Flags.string({
19702
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19703
+ env: "PRIMITIVE_API_BASE_URL_1",
19704
+ hidden: true
19705
+ }),
19706
+ json: Flags.boolean({ description: "Print pending signup status as JSON" })
19707
+ };
19708
+ async run() {
19709
+ const { args, flags } = await this.parse(SignupStatusCommand);
19710
+ runSignupStatus({
19711
+ configDir: this.config.configDir,
19712
+ email: args.email,
19713
+ flags
19714
+ });
19715
+ }
19716
+ };
19514
19717
  var SignupInteractiveCommand = class SignupInteractiveCommand extends Command {
19515
19718
  static description = "Run the full signup flow in one interactive terminal session.";
19516
19719
  static summary = "Run interactive account signup";
@@ -19606,6 +19809,7 @@ function runForceLogout(params) {
19606
19809
  const lockPath = credentialsLockPath(params.configDir);
19607
19810
  const removed = [
19608
19811
  existsSync(localCredentialsPath) ? "local Primitive CLI credentials" : null,
19812
+ existsSync(chatStatePath(params.configDir)) ? "local chat reply state" : null,
19609
19813
  existsSync(pendingPath) ? "pending email-code auth state" : null,
19610
19814
  existsSync(lockPath) ? "credential lock" : null
19611
19815
  ].filter((value) => value !== null);
@@ -19770,6 +19974,7 @@ var ReplyCommand = class ReplyCommand extends Command {
19770
19974
  static examples = [
19771
19975
  "<%= config.bin %> reply --id <inbound-email-id> --body 'Thanks, got it.'",
19772
19976
  "<%= config.bin %> reply --id <inbound-email-id> --body-file ./reply.txt",
19977
+ "<%= config.bin %> reply --id <inbound-email-id> --body 'See attached.' --attachment ./report.pdf",
19773
19978
  "<%= config.bin %> reply --id <inbound-email-id> --html '<p>Thanks, got it.</p>' --wait",
19774
19979
  "<%= config.bin %> reply --id <inbound-email-id> --from 'Support <support@example.com>' --body 'Thanks!'"
19775
19980
  ];
@@ -19793,12 +19998,17 @@ var ReplyCommand = class ReplyCommand extends Command {
19793
19998
  required: true
19794
19999
  }),
19795
20000
  body: Flags.string({ description: "Plain-text reply body. Either --body or --html (or both) is required." }),
19796
- "body-file": Flags.string({ description: "Read the plain-text reply body from a UTF-8 file. Mutually exclusive with --body and --body-stdin." }),
20001
+ "body-file": Flags.string({ description: "Read the plain-text reply body from a UTF-8 file; this does not attach the file. Use --attachment for attachments. Mutually exclusive with --body and --body-stdin." }),
19797
20002
  "body-stdin": Flags.boolean({ description: "Read the plain-text reply body from stdin. Mutually exclusive with --body and --body-file. Stdin can only be consumed once." }),
19798
20003
  html: Flags.string({ description: "HTML reply body. Either --body or --html (or both) is required." }),
19799
- "html-file": Flags.string({ description: "Read the HTML reply body from a UTF-8 file. Mutually exclusive with --html and --html-stdin." }),
20004
+ "html-file": Flags.string({ description: "Read the HTML reply body from a UTF-8 file; this does not attach the file. Use --attachment for attachments. Mutually exclusive with --html and --html-stdin." }),
19800
20005
  "html-stdin": Flags.boolean({ description: "Read the HTML reply body from stdin. Mutually exclusive with --html and --html-file. Stdin can only be consumed once." }),
19801
20006
  from: Flags.string({ description: "Optional From header override. Defaults to the inbound recipient." }),
20007
+ attachment: Flags.string({
20008
+ char: "a",
20009
+ description: "Attach a file to the reply. Repeat --attachment to attach multiple files.",
20010
+ multiple: true
20011
+ }),
19802
20012
  wait: Flags.boolean({ description: "Block until the receiving MTA returns an outcome. Without --wait, the call returns once Primitive has accepted the reply for delivery." }),
19803
20013
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
19804
20014
  };
@@ -19820,14 +20030,16 @@ var ReplyCommand = class ReplyCommand extends Command {
19820
20030
  apiBaseUrl2: flags["api-base-url-2"],
19821
20031
  configDir: this.config.configDir
19822
20032
  });
20033
+ const attachments = readAttachmentFiles(flags.attachment);
19823
20034
  const result = await replyToEmail({
19824
20035
  body: {
19825
20036
  ...bodies.body !== void 0 ? { body_text: bodies.body } : {},
19826
20037
  ...bodies.html !== void 0 ? { body_html: bodies.html } : {},
19827
20038
  ...flags.from !== void 0 ? { from: flags.from } : {},
20039
+ ...attachments !== void 0 ? { attachments } : {},
19828
20040
  ...flags.wait !== void 0 ? { wait: flags.wait } : {}
19829
20041
  },
19830
- client: apiClient.client,
20042
+ client: apiClient._sendClient,
19831
20043
  path: { id: flags.id },
19832
20044
  responseStyle: "fields"
19833
20045
  });
@@ -19852,39 +20064,6 @@ var ReplyCommand = class ReplyCommand extends Command {
19852
20064
  }
19853
20065
  };
19854
20066
  //#endregion
19855
- //#region src/oclif/attachments.ts
19856
- function readAttachmentBytes(path, readFile) {
19857
- try {
19858
- return Buffer.from(readFile(path));
19859
- } catch (error) {
19860
- const detail = error instanceof Error ? error.message : String(error);
19861
- throw new Errors.CLIError(`Could not read --attachment ${path}: ${detail}`, { exit: 1 });
19862
- }
19863
- }
19864
- function hasControlCharacter(value) {
19865
- return Array.from(value).some((character) => {
19866
- const code = character.charCodeAt(0);
19867
- return code <= 31 || code >= 127 && code <= 159;
19868
- });
19869
- }
19870
- function validateAttachmentFilename(path, filename) {
19871
- if (!filename) throw new Errors.CLIError(`Could not derive an attachment filename from ${path}. Pass a file path.`, { exit: 1 });
19872
- if (hasControlCharacter(filename)) throw new Errors.CLIError(`Attachment filename ${filename} contains control characters.`, { exit: 1 });
19873
- }
19874
- function readAttachmentFiles(paths, readFile = readFileSync) {
19875
- if (!paths || paths.length === 0) return void 0;
19876
- return paths.map((path) => {
19877
- const filename = basename(path);
19878
- validateAttachmentFilename(path, filename);
19879
- const bytes = readAttachmentBytes(path, readFile);
19880
- if (bytes.length === 0) throw new Errors.CLIError(`Attachment file ${path} is empty. Attachments must contain at least one byte.`, { exit: 1 });
19881
- return {
19882
- content_base64: bytes.toString("base64"),
19883
- filename
19884
- };
19885
- });
19886
- }
19887
- //#endregion
19888
20067
  //#region src/oclif/commands/send.ts
19889
20068
  var SendCommand = class SendCommand extends Command {
19890
20069
  static description = `Send an outbound email. Agent-grade shortcut for \`sending send\` with sensible defaults.
@@ -20659,6 +20838,7 @@ const CANONICAL_OPERATION_ALIASES = {
20659
20838
  "domains:list": "domains:list-domains",
20660
20839
  "domains:update": "domains:update-domain",
20661
20840
  "domains:verify": "domains:verify-domain",
20841
+ "emails:conversation": "emails:get-conversation",
20662
20842
  "emails:delete": "emails:delete-email",
20663
20843
  "emails:discard-content": "emails:discard-email-content",
20664
20844
  "emails:download-raw": "emails:download-raw-email",
@@ -20718,6 +20898,7 @@ const COMMANDS = {
20718
20898
  send: SendCommand,
20719
20899
  reply: ReplyCommand,
20720
20900
  chat: ChatCommand,
20901
+ "chat:reply": ChatReplyCommand,
20721
20902
  login: LoginCommand,
20722
20903
  "login:browser": LoginBrowserCommand,
20723
20904
  "login:confirm": LoginConfirmCommand,
@@ -20739,6 +20920,7 @@ const COMMANDS = {
20739
20920
  "signup:confirm": SignupConfirmCommand,
20740
20921
  "signup:interactive": SignupInteractiveCommand,
20741
20922
  "signup:resend": SignupResendCommand,
20923
+ "signup:status": SignupStatusCommand,
20742
20924
  logout: LogoutCommand,
20743
20925
  whoami: WhoamiCommand,
20744
20926
  doctor: DoctorCommand,
@@ -20747,6 +20929,7 @@ const COMMANDS = {
20747
20929
  "emails:wait": EmailsWaitCommand,
20748
20930
  "domains:zone-file": DomainsZoneFileCommand,
20749
20931
  "domains:download-domain-zone-file": DomainsZoneFileCommand,
20932
+ "inbox:setup": InboxSetupCommand,
20750
20933
  "inbox:status": InboxStatusCommand,
20751
20934
  "inbox:get-inbox-status": InboxStatusCommand,
20752
20935
  "functions:init": FunctionsInitCommand,